feat(FN-WF): add the Coding (Ideas) V2 workflow with review-column gates
Selectable built-in `builtin:coding-ideas-v2`. It clones the Coding (Ideas) IR
without mutating it, so the manual `ideas` intake (`autoTriage: false`) and the
whole board shape are unchanged, and moves testing and documentation out of the
planner's implementation checklist into visible review-column gates:
in-progress : steps = implementation only
in-review : verification -> documentation-delivery -> code-review
-> completion-summary -> merge-gate -> merge
Ordering is load-bearing, not cosmetic. `execute-workflow-graph.ts` refuses any
write-capable node once a Code Review APPROVE exists, so that a passed review
seals the tree and nothing unreviewed reaches main. `verification-step` and
`documentation-delivery-step` are both write-capable and therefore run BEFORE
the review; `completion-summary` is `toolMode: "readonly"` and runs after it, so
the card blurb describes the state that was actually approved.
Both remediation loops re-enter at `verification`, never at `code-review`: a
REVISE replays verification AND documentation-delivery, so the docs and
changeset are regenerated to include what the review demanded before it re-reads
them. Documentation stays both current and reviewed.
The planner is switched to the `planning-implementation-only` seam so it stops
emitting "Testing & Verification" and "Documentation & Delivery" steps, which
would otherwise duplicate the gates under identical names.
Adds a ratchet running the production `workflowNodeRequiresWorktree` classifier
over the success chain: it reports zero offenders here and correctly flags
`documentation-delivery` on builtin:review-gated-coding, whose post-review
ordering deadlocks every task once its review approves.
This commit is contained in:
7
.changeset/coding-ideas-v2-workflow.md
Normal file
7
.changeset/coding-ideas-v2-workflow.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Add the Coding (Ideas) V2 workflow, with verification and documentation as visible review steps.
|
||||
category: feature
|
||||
dev: New selectable built-in `builtin:coding-ideas-v2` clones `BUILTIN_CODING_IDEAS_WORKFLOW_IR` without mutating it, keeps the manual `ideas` intake (`autoTriage: false`), and moves Testing/Verification and Documentation & Delivery out of the planner's implementation checklist into `in-review` gates: `steps → verification → documentation-delivery → code-review → completion-summary → merge-gate`. Both write-capable gates precede Code Review because `execute-workflow-graph.ts` refuses write-capable nodes once an APPROVE exists (`workspace-review-seal-required`); the readonly `completion-summary` runs after it. Remediation edges re-enter at `verification` so a REVISE replays documentation before re-review. `packages/engine/src/__tests__/coding-ideas-v2-review-seal.test.ts` runs the production `workflowNodeRequiresWorktree` classifier over the graph as a ratchet against re-introducing the ordering defect.
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveRequiredPreMergeStepIds } from "../merge/required-pre-merge-steps.js";
|
||||
import { BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR } from "../workflows/builtin-coding-ideas-v2-workflow-ir.js";
|
||||
import { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "../workflows/builtin-coding-ideas-workflow-ir.js";
|
||||
import { getBuiltinWorkflow } from "../workflows/builtin-workflows.js";
|
||||
import { parseWorkflowIr, serializeWorkflowIr } from "../workflows/workflow-ir.js";
|
||||
import { resolveWorkflowOptionalSteps } from "../workflows/workflow-optional-steps.js";
|
||||
|
||||
/** The single-success-edge walk an executing task actually follows from a node. */
|
||||
function successChainFrom(start: string): string[] {
|
||||
const chain: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
let current: string | undefined = start;
|
||||
while (current && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
const edge = BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.edges
|
||||
.find((candidate) => candidate.from === current && candidate.condition === "success");
|
||||
if (!edge) break;
|
||||
chain.push(edge.to);
|
||||
current = edge.to;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
describe("builtin:coding-ideas-v2", () => {
|
||||
it("is a selectable validated workflow that keeps the Ideas intake untouched", () => {
|
||||
const workflow = getBuiltinWorkflow("builtin:coding-ideas-v2");
|
||||
expect(workflow?.name).toBe("Coding (Ideas) V2");
|
||||
expect(parseWorkflowIr(serializeWorkflowIr(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR)))
|
||||
.toEqual(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR);
|
||||
|
||||
// The whole point of the Ideas board: cards park in a manual intake and the
|
||||
// engine must not plan them until an operator promotes them.
|
||||
expect(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.columns)
|
||||
.toEqual(BUILTIN_CODING_IDEAS_WORKFLOW_IR.columns);
|
||||
const intake = BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.columns.find((column) => column.id === "ideas");
|
||||
expect(intake?.traits).toEqual([{ trait: "intake", config: { autoTriage: false } }]);
|
||||
});
|
||||
|
||||
it("runs verify -> document -> review -> summarize -> merge in review", () => {
|
||||
expect(successChainFrom("steps")).toEqual([
|
||||
"verification",
|
||||
"documentation-delivery",
|
||||
"code-review",
|
||||
"completion-summary",
|
||||
"merge-gate",
|
||||
]);
|
||||
|
||||
for (const nodeId of ["verification", "documentation-delivery", "code-review", "completion-summary"]) {
|
||||
expect(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.nodes.find((node) => node.id === nodeId)?.column)
|
||||
.toBe("in-review");
|
||||
}
|
||||
|
||||
// Coding (Ideas) carries no post-merge-verification node, so V2 inherits none either.
|
||||
expect(resolveWorkflowOptionalSteps(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR).map((step) => step.templateId))
|
||||
.toEqual(["plan-review", "verification", "documentation-delivery", "code-review"]);
|
||||
expect(resolveRequiredPreMergeStepIds(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR, undefined))
|
||||
.toEqual(new Set(["plan-review", "verification", "documentation-delivery", "code-review"]));
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:CodingIdeasV2Workflow 2026-08-24-05:35:
|
||||
Both remediation loops must re-enter at `verification`, never at `code-review`. A REVISE has to
|
||||
replay documentation-delivery so the docs and changeset are regenerated to include what the review
|
||||
demanded; re-entering at the review would merge documentation describing a superseded tree.
|
||||
*/
|
||||
it("replays documentation on rework by re-entering upstream of it", () => {
|
||||
expect(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.edges).toEqual(expect.arrayContaining([
|
||||
{ from: "verification", to: "verification-remediation", condition: "failure" },
|
||||
{ from: "code-review", to: "code-review-remediation", condition: "failure" },
|
||||
{ from: "verification-remediation", to: "verification", condition: "success", kind: "rework" },
|
||||
{ from: "code-review-remediation", to: "verification", condition: "success", kind: "rework" },
|
||||
]));
|
||||
// Re-entering at `verification` only replays the docs because the doc node sits downstream of it.
|
||||
expect(successChainFrom("verification")).toContain("documentation-delivery");
|
||||
|
||||
for (const remediationId of ["verification-remediation", "code-review-remediation"]) {
|
||||
expect(BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR.nodes.find((node) => node.id === remediationId)?.column)
|
||||
.toBe("in-progress");
|
||||
}
|
||||
});
|
||||
|
||||
it("stops the planner emitting the gates as duplicate implementation steps", () => {
|
||||
const ir = BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR;
|
||||
const plan = ir.nodes.find((node) => node.id === "plan");
|
||||
const parse = ir.nodes.find((node) => node.id === "parse");
|
||||
const planReview = ir.nodes.find((node) => node.id === "plan-review");
|
||||
const planReviewTemplate = planReview?.config.template as { nodes?: Array<{ config?: Record<string, unknown> }> };
|
||||
|
||||
expect(plan?.config?.seam).toBe("planning-implementation-only");
|
||||
expect(parse?.config).toMatchObject({ implementationOnlySteps: true, preserveRemediationSteps: true });
|
||||
expect(planReviewTemplate.nodes?.[0]?.config).toMatchObject({ requireImplementationOnlySteps: true });
|
||||
});
|
||||
|
||||
it("never mutates the inherited Coding (Ideas) graph", () => {
|
||||
expect(BUILTIN_CODING_IDEAS_WORKFLOW_IR.nodes.some((node) => node.id === "verification")).toBe(false);
|
||||
expect(BUILTIN_CODING_IDEAS_WORKFLOW_IR.nodes.some((node) => node.id === "documentation-delivery")).toBe(false);
|
||||
expect(BUILTIN_CODING_IDEAS_WORKFLOW_IR.edges).toEqual(expect.arrayContaining([
|
||||
{ from: "steps", to: "completion-summary", condition: "success" },
|
||||
]));
|
||||
});
|
||||
});
|
||||
@@ -340,6 +340,7 @@ export type {
|
||||
} from "./agents/column-agent-resolver.js";
|
||||
export { BUILTIN_CODING_WORKFLOW_IR } from "./workflows/builtin-coding-workflow-ir.js";
|
||||
export { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./workflows/builtin-coding-ideas-workflow-ir.js";
|
||||
export { BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR } from "./workflows/builtin-coding-ideas-v2-workflow-ir.js";
|
||||
export { PLAN_REVIEW_GROUP_ID } from "./workflows/builtin-plan-review-group.js";
|
||||
export { BUILTIN_MARKETING_WORKFLOW_IR } from "./workflows/builtin-marketing-workflow-ir.js";
|
||||
export { evaluateForeachMergeProof } from "./workflow-merge-proof.js";
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { WorkflowIr } from "./workflow-ir-types.js";
|
||||
import { parseWorkflowIr } from "./workflow-ir.js";
|
||||
import { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./builtin-coding-ideas-workflow-ir.js";
|
||||
import { verificationOptionalGroupNode } from "./builtin-verification-gate-group.js";
|
||||
import { documentationDeliveryOptionalGroupNode } from "./builtin-documentation-delivery-group.js";
|
||||
import { verificationRemediationNode } from "./builtin-workflow-remediation-nodes.js";
|
||||
import { builtinPromptConfig } from "./builtin-workflow-prompts.js";
|
||||
|
||||
const clone = (ir: WorkflowIr): WorkflowIr => JSON.parse(JSON.stringify(ir)) as WorkflowIr;
|
||||
|
||||
/*
|
||||
FNXC:CodingIdeasV2Workflow 2026-08-24-05:35:
|
||||
Operator intent: keep the Coding (Ideas) board exactly as it is (manual "Ideas" intake, autoTriage
|
||||
false), but stop hiding testing and documentation inside the implementation checklist. They become
|
||||
VISIBLE review-column gates, and the merge is the last thing that happens after delivery.
|
||||
|
||||
in-progress : steps = implementation only
|
||||
in-review : verification -> documentation-delivery -> code-review -> completion-summary -> merge
|
||||
|
||||
Ordering is NOT cosmetic. `execute-workflow-graph.ts` refuses any write-capable node once a Code
|
||||
Review APPROVE exists (`workspace-review-seal-required`): a passed review seals the tree so nothing
|
||||
unreviewed can reach main. `verification-step` (its name matches the write-capable classifier) and
|
||||
`documentation-delivery-step` (`toolMode: "coding"`) are both write-capable, so both MUST precede
|
||||
`code-review`. builtin:review-gated-coding places them after it and therefore deadlocks on every
|
||||
task the moment the review approves — that defect is the reason this ordering is explicit here.
|
||||
|
||||
`completion-summary` is deliberately AFTER `code-review`: it is `toolMode: "readonly"`, so the seal
|
||||
does not apply, and writing it last lets the card blurb describe the state that was actually
|
||||
approved. It stays best-effort with a success-only edge — a summary failure must never wedge a task.
|
||||
*/
|
||||
const RAW_BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR: WorkflowIr = (() => {
|
||||
const ir = clone(BUILTIN_CODING_IDEAS_WORKFLOW_IR);
|
||||
ir.name = "builtin-coding-ideas-v2";
|
||||
|
||||
/*
|
||||
FNXC:CodingIdeasV2Workflow 2026-08-24-05:35:
|
||||
The planner must stop emitting "Testing & Verification" and "Documentation & Delivery" steps: they
|
||||
are gates now, and leaving them in PROMPT.md would run the same work twice under the same names.
|
||||
`planning-implementation-only` is the seam that carries that instruction.
|
||||
*/
|
||||
const plan = ir.nodes.find((node) => node.id === "plan");
|
||||
if (plan) plan.config = { ...plan.config, ...builtinPromptConfig("planning-implementation-only", "Plan") };
|
||||
const planReview = ir.nodes.find((node) => node.id === "plan-review");
|
||||
const planTemplate = planReview?.config?.template as { nodes?: Array<{ config?: Record<string, unknown> }> } | undefined;
|
||||
if (planTemplate?.nodes?.[0]?.config) planTemplate.nodes[0].config.requireImplementationOnlySteps = true;
|
||||
const parse = ir.nodes.find((node) => node.id === "parse");
|
||||
if (parse) parse.config = { ...parse.config, implementationOnlySteps: true, preserveRemediationSteps: true };
|
||||
|
||||
const codeReviewIndex = ir.nodes.findIndex((node) => node.id === "code-review");
|
||||
if (codeReviewIndex < 0) throw new Error("coding-ideas-v2 requires the inherited code-review gate");
|
||||
ir.nodes.splice(codeReviewIndex, 0, verificationOptionalGroupNode("in-review"), documentationDeliveryOptionalGroupNode("in-review"));
|
||||
ir.nodes.push(verificationRemediationNode());
|
||||
|
||||
ir.edges = ir.edges.filter((edge) => !(
|
||||
(edge.from === "steps" && edge.to === "completion-summary")
|
||||
|| (edge.from === "completion-summary" && edge.to === "code-review")
|
||||
|| (edge.from === "code-review" && edge.to === "merge-gate")
|
||||
|| (edge.from === "code-review-remediation" && edge.to === "code-review")
|
||||
));
|
||||
|
||||
/*
|
||||
FNXC:CodingIdeasV2Workflow 2026-08-24-05:35:
|
||||
Both remediation loops re-enter at `verification`, never directly at `code-review`. That is what
|
||||
keeps the documentation honest: a REVISE sends the fix back to in-progress, then the walk replays
|
||||
verification AND documentation-delivery, so the docs and changeset are regenerated to include what
|
||||
the review demanded before it re-reads them. Re-entering at `code-review` would leave the docs
|
||||
describing a tree that no longer exists. `verification` is the rework-region head (`reworkRegion:
|
||||
true`, `maxReworkCycles: 3`), which is what makes these edges legal.
|
||||
*/
|
||||
ir.edges.push(
|
||||
{ from: "steps", to: "verification", condition: "success" },
|
||||
{ from: "verification", to: "documentation-delivery", condition: "success" },
|
||||
{ from: "documentation-delivery", to: "code-review", condition: "success" },
|
||||
{ from: "code-review", to: "completion-summary", condition: "success" },
|
||||
{ from: "completion-summary", to: "merge-gate", condition: "success" },
|
||||
{ from: "verification", to: "verification-remediation", condition: "failure" },
|
||||
{ from: "verification-remediation", to: "verification", condition: "success", kind: "rework" },
|
||||
{ from: "code-review-remediation", to: "verification", condition: "success", kind: "rework" },
|
||||
);
|
||||
return ir;
|
||||
})();
|
||||
|
||||
export const BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR = parseWorkflowIr(RAW_BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js";
|
||||
import { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./builtin-coding-ideas-workflow-ir.js";
|
||||
import { BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR } from "./builtin-coding-ideas-v2-workflow-ir.js";
|
||||
import { BUILTIN_BRAINSTORMING_WORKFLOW_IR } from "./builtin-brainstorming-workflow-ir.js";
|
||||
import { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "./builtin-lead-generation-workflow-ir.js";
|
||||
import { BUILTIN_MARKETING_WORKFLOW_IR } from "./builtin-marketing-workflow-ir.js";
|
||||
@@ -601,6 +602,48 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
createdAt: BUILTIN_TS,
|
||||
updatedAt: BUILTIN_TS,
|
||||
},
|
||||
/*
|
||||
* FNXC:CodingIdeasV2Workflow 2026-08-24-05:35:
|
||||
* Same Ideas board as builtin:coding-ideas, but testing and documentation stop being hidden
|
||||
* checklist items inside the implementation steps and become visible in-review gates, with the
|
||||
* merge last. Every write-capable gate runs BEFORE Code Review because a passed review seals the
|
||||
* tree (`workspace-review-seal-required`); the readonly completion summary runs after it.
|
||||
*/
|
||||
{
|
||||
id: "builtin:coding-ideas-v2",
|
||||
name: "Coding (Ideas) V2",
|
||||
description:
|
||||
"Capture-first coding pipeline with visible review-column gates: park ideas in a manual intake, plan, implement per step, then verify, document, review, summarize, and merge.",
|
||||
kind: "workflow",
|
||||
ir: BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR,
|
||||
layout: {
|
||||
start: { x: 60, y: 160 },
|
||||
plan: { x: 230, y: 160 },
|
||||
"plan-review": { x: 400, y: 160 },
|
||||
"plan-replan": { x: 400, y: 320 },
|
||||
"plan-review-no-op": { x: 570, y: 320 },
|
||||
parse: { x: 570, y: 160 },
|
||||
steps: { x: 740, y: 160 },
|
||||
"review-pending-handoff": { x: 740, y: 320 },
|
||||
verification: { x: 910, y: 160 },
|
||||
"verification-remediation": { x: 910, y: 320 },
|
||||
"documentation-delivery": { x: 1080, y: 160 },
|
||||
"code-review": { x: 1250, y: 160 },
|
||||
"code-review-remediation": { x: 1250, y: 320 },
|
||||
"completion-summary": { x: 1420, y: 160 },
|
||||
"merge-gate": { x: 1590, y: 160 },
|
||||
"branch-group-member-integration": { x: 1760, y: 80 },
|
||||
"branch-group-promotion": { x: 1930, y: 80 },
|
||||
"merge-attempt": { x: 2100, y: 160 },
|
||||
"merge-retry": { x: 2270, y: 80 },
|
||||
"recovery-router": { x: 2270, y: 240 },
|
||||
"merge-manual-hold": { x: 1760, y: 240 },
|
||||
"post-merge-verification": { x: 2440, y: 160 },
|
||||
end: { x: 2610, y: 160 },
|
||||
},
|
||||
createdAt: BUILTIN_TS,
|
||||
updatedAt: BUILTIN_TS,
|
||||
},
|
||||
{
|
||||
id: "builtin:legacy-coding",
|
||||
name: "Legacy coding",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR, type WorkflowIr, type WorkflowIrNode } from "@fusion/core";
|
||||
import { workflowNodeRequiresWorktree } from "../workflows/workflow-node-execution-needs.js";
|
||||
|
||||
/*
|
||||
FNXC:CodingIdeasV2Workflow 2026-08-24-05:35:
|
||||
The ratchet FN-175 did not have. `execute-workflow-graph.ts` refuses any write-capable node once a
|
||||
Code Review APPROVE exists (`workspace-review-seal-required`), because a passed review seals the
|
||||
tree so nothing unreviewed reaches main. builtin:review-gated-coding routes code-review straight
|
||||
into two write-capable nodes, so every task on it deadlocks the moment the review approves — and
|
||||
nothing caught it, because its only coverage asserted graph shape by hand rather than running the
|
||||
production classifier over the graph.
|
||||
|
||||
This test runs the REAL classifier over the real success chain. It fails if anyone ever moves a
|
||||
write-capable gate after the review again.
|
||||
*/
|
||||
|
||||
/** The executed node for a gate: an optional-group runs its template's inner node. */
|
||||
function executableNodes(node: WorkflowIrNode): Array<{ node: WorkflowIrNode; optionalGroupId?: string }> {
|
||||
const template = node.config?.template as { nodes?: WorkflowIrNode[] } | undefined;
|
||||
if (node.kind === "optional-group" && Array.isArray(template?.nodes)) {
|
||||
return template.nodes.map((inner) => ({ node: inner, optionalGroupId: node.id }));
|
||||
}
|
||||
return [{ node }];
|
||||
}
|
||||
|
||||
function isWriteCapable(node: WorkflowIrNode): boolean {
|
||||
return executableNodes(node).some(({ node: executed, optionalGroupId }) =>
|
||||
workflowNodeRequiresWorktree(executed, { optionalGroupId }) || executed.kind === "code");
|
||||
}
|
||||
|
||||
function successChainFrom(ir: WorkflowIr, start: string): WorkflowIrNode[] {
|
||||
const chain: WorkflowIrNode[] = [];
|
||||
const seen = new Set<string>();
|
||||
let current: string | undefined = start;
|
||||
while (current && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
const edge = ir.edges.find((candidate) => candidate.from === current && candidate.condition === "success");
|
||||
if (!edge) break;
|
||||
const next = ir.nodes.find((node) => node.id === edge.to);
|
||||
if (next) chain.push(next);
|
||||
current = edge.to;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
describe("builtin:coding-ideas-v2 review seal", () => {
|
||||
const ir = BUILTIN_CODING_IDEAS_V2_WORKFLOW_IR as WorkflowIr;
|
||||
|
||||
it("has no write-capable node after Code Review", () => {
|
||||
const after = successChainFrom(ir, "code-review");
|
||||
// Guard the guard: an empty chain would make this assertion vacuously true.
|
||||
expect(after.map((node) => node.id)).toContain("completion-summary");
|
||||
|
||||
const offenders = after.filter(isWriteCapable).map((node) => node.id);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps both write-capable gates strictly before Code Review", () => {
|
||||
// If these ever stop being write-capable the ordering constraint is moot and this
|
||||
// whole ratchet is measuring nothing — so assert the premise, not just the outcome.
|
||||
for (const gateId of ["verification", "documentation-delivery"]) {
|
||||
const gate = ir.nodes.find((node) => node.id === gateId);
|
||||
expect(gate, `${gateId} is missing`).toBeDefined();
|
||||
expect(isWriteCapable(gate!), `${gateId} is expected to be write-capable`).toBe(true);
|
||||
}
|
||||
|
||||
const chain = successChainFrom(ir, "steps").map((node) => node.id);
|
||||
expect(chain.indexOf("verification")).toBeLessThan(chain.indexOf("code-review"));
|
||||
expect(chain.indexOf("documentation-delivery")).toBeLessThan(chain.indexOf("code-review"));
|
||||
});
|
||||
|
||||
it("keeps the completion summary readonly so it may run after the seal", () => {
|
||||
const summary = ir.nodes.find((node) => node.id === "completion-summary");
|
||||
expect(summary?.config?.toolMode).toBe("readonly");
|
||||
expect(isWriteCapable(summary!)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user