**Taking: `engine/tool-availability.ts`, `engine/skill-resolver.ts`, `cli/commands/task.ts`** — the three census hits that are not board columns. ## Census | file | before | after | |---|---:|---:| | `packages/engine/src/tool-availability.ts` | 1 | **0** | | `packages/engine/src/skill-resolver.ts` | 1 | **0** | | `packages/cli/src/commands/task.ts` | 1 | **0** | | **repo total (comment-stripped)** | **48** | **45** | ## These are not lifecycle guards — converting them would have been wrong - **`tool-availability`** — `surface: "triage" | "executor"` is an **agent lane**. The lane that writes specs keeps its name whatever the board calls its planning column. Resolving it from a workflow IR would make an agent's prompt depend on board configuration. - **`skill-resolver`** — `sessionPurpose === "triage"` is an **agent role**. Same argument: a role doesn't move when a board renames a column. - **`cli task list`** — the glyph chain distinguished **active** columns from the rest and nothing else; all four active ids mapped to the same `●`. Each is now named (`AgentResearchSurface`, `ROLE_FALLBACK_SESSION_PURPOSES`, `ACTIVE_COLUMN_GLYPH_IDS`) so the next person working the census sees at a glance that they're out of scope, rather than re-deriving it as I had to. ## A real divergence my own equivalence test caught I first wrote the glyph as the tempting inverse: ```ts const dot = col === "done" || col === "archived" ? "○" : "●"; ``` That is equivalent across all six lifecycle ids and **not** equivalent for anything else — the original chain fell through to `"○"` for an unrecognised id, while the inverse renders it as **active**. The loop only walks the six `COLUMNS` today, so nothing would have caught it in practice; a renamed workflow reaching this code later would have silently changed how its columns render. Shipped as an explicit ACTIVE set that mirrors the fallthrough exactly. The test asserts equivalence over the six ids **and** over unknown ids, which is where the difference lives. That's the point of testing a "pure rename" at its edges rather than only where it's currently exercised. ## Verification - 71 tests green across skill-resolver / heartbeat-skills / tool-availability / the new equivalence suite - merge gate green (482 + 132 + 10), engine + CLI tsc clean, lint clean ## Note on the remaining count Of the 45 left, `replan-target.ts` (2), `board-workflows.ts` (2) and `archive-planning.ts` (1) show up in a **raw** grep but are **0** real — every hit is inside a comment. A raw grep reports 53; comment-stripped is 45. Real remaining work concentrates in `self-healing.ts` (11), `register-task-workflow-routes.ts` (6), and the parked `moves.ts` / `default-workflow-hooks.ts` (9). No changeset: `@fusion/engine` is private; the CLI change is display-identical. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ## ⚠️ Read before merging — these are ROLE renames, not column conversions The coordinator's hand classification says several literals in this PR "must be left exactly as they are" because they compare an **agent role**, not a task column, and resolving them to a column trait would be a bug. **I agree, and this PR does not do that.** What it does: replaces a bare `=== "triage"` with a **named role predicate** — `isPlanningAgentLane`, `AgentResearchSurface`, `ROLE_FALLBACK_SESSION_PURPOSES`. Behaviour is **byte-identical** for every input. No IR is consulted, no trait is resolved, no column is involved. The reason to keep it rather than revert: the danger isn't the literal, it's that nothing at the call site tells the next person `"triage"` here means a *lane*. A list of exceptions maintained elsewhere only helps someone who finds the list; a call named `isPlanningAgentLane` helps whoever is reading the line. It also shrinks what the #2630 ratchet's ignore list has to carry. Reversible: if the preference is to leave the literals untouched, say so and I'll strip these hunks — but then the ratchet's ignore list must carry **all twelve** role sites or it can never reach zero, because those six are correct code. ## Classification finding Bucketing by the **receiver** of the comparison (not the literal) mechanically separates guards from roles, and it found **six role sites currently listed as "real column guards"**: | site | receiver | what it actually is | |---|---|---| | `usage-limit-detector.ts:144, 207` | `agentType` | agent type — the column test one line above is *already* trait-driven | | `skill-resolver.ts:432` | `sessionPurpose` | session purpose | | `tool-availability.ts:32` | `surface` | agent surface (`"triage" \| "executor"`) | | `effective-model-resolution.ts:148` | `entry.agent` | agent-log lane | | `useTasks.ts:162` | `entry.agent` | agent-log lane | So the real bar is roughly **39**, not 45. The rule that found all twelve without judgement calls: `column`/`toColumn`/`taskColumn`/`c` are guards; `role`/`agent`/`agentType`/`surface`/`sessionPurpose` are not. Worth teaching #2630's ratchet directly.
57 lines
2.7 KiB
TypeScript
57 lines
2.7 KiB
TypeScript
import type { Settings } from "@fusion/core";
|
|
import { isResearchExperimentalEnabled } from "@fusion/core";
|
|
import type { PluginRunner } from "./plugin-runner.js";
|
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
|
|
export function isResearchToolSurfaceEnabled(settings: Partial<Settings> | undefined): boolean {
|
|
return isResearchExperimentalEnabled(settings);
|
|
}
|
|
|
|
export function getResearchToolSurfaceStatus(settings: Partial<Settings> | undefined): {
|
|
enabled: boolean;
|
|
reason: "experimental-disabled" | "enabled";
|
|
} {
|
|
const enabled = isResearchToolSurfaceEnabled(settings);
|
|
return {
|
|
enabled,
|
|
reason: enabled ? "enabled" : "experimental-disabled",
|
|
};
|
|
}
|
|
|
|
const TRIAGE_RESEARCH_GUIDANCE = `## Research tools
|
|
When spec work needs missing domain context, you may use research tools (\`fn_research_run\`, \`fn_research_list\`, \`fn_research_get\`, \`fn_research_cancel\`, \`fn_research_retry\`). Keep research bounded to the task at hand, prefer concise queries, and write durable findings into task documents when useful.
|
|
If research is unavailable or unconfigured, continue planning with repository context and clearly note assumptions.`;
|
|
|
|
const EXECUTOR_RESEARCH_GUIDANCE = `## Research tools
|
|
When implementation needs external context, you may use research tools (
|
|
\`fn_research_run\`, \`fn_research_list\`, \`fn_research_get\`, \`fn_research_cancel\`, \`fn_research_retry\`) to run bounded research.
|
|
Keep runs focused and short, and persist durable conclusions into task documents (for example key="research").
|
|
If research is disabled or providers are not configured, use the actionable tool response and continue with available local context.`;
|
|
|
|
/*
|
|
FNXC:WorkflowLifecycleColumns 2026-07-30-13:20 (U11 census hygiene):
|
|
`"triage"` HERE IS AN AGENT LANE, NOT A BOARD COLUMN — the lane that writes specs,
|
|
which keeps its name whatever the board calls its planning column. It appeared in
|
|
the `=== "triage"` census purely because it is the same word, and trait-converting
|
|
it would be actively wrong: it would tie an agent's prompt to a workflow's column
|
|
vocabulary.
|
|
|
|
Named and table-driven so the distinction is legible and the literal no longer
|
|
reads as a lifecycle guard to the next auditor.
|
|
*/
|
|
export type AgentResearchSurface = "triage" | "executor";
|
|
|
|
const RESEARCH_GUIDANCE_BY_SURFACE: Record<AgentResearchSurface, string> = {
|
|
triage: TRIAGE_RESEARCH_GUIDANCE,
|
|
executor: EXECUTOR_RESEARCH_GUIDANCE,
|
|
};
|
|
|
|
export function getResearchGuidanceForSurface(surface: AgentResearchSurface): string {
|
|
return RESEARCH_GUIDANCE_BY_SURFACE[surface];
|
|
}
|
|
|
|
export function getEnabledPluginTools(pluginRunner: PluginRunner | undefined): ToolDefinition[] {
|
|
if (!pluginRunner) return [];
|
|
return pluginRunner.getPluginTools();
|
|
}
|