From 8d650326db218175de45cbfbfcba637f2eccb5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Wed, 13 May 2026 13:52:04 +0000 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20add=20output=20rules=20to=20all=20?= =?UTF-8?q?develop=20roles=20=E2=80=94=20suppress=20verbose=20diffs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Planner, coder, reviewer, and tester system prompts now explicitly instruct the agent to keep responses short and avoid pasting diffs, code blocks, or full build logs. This reduces CAS storage and token waste when downstream roles read the thread. Signed-off-by: 小橘 --- packages/workflow-template-develop/src/roles/coder.ts | 6 +++++- packages/workflow-template-develop/src/roles/planner.ts | 6 +++++- packages/workflow-template-develop/src/roles/reviewer.ts | 6 +++++- packages/workflow-template-develop/src/roles/tester.ts | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/workflow-template-develop/src/roles/coder.ts b/packages/workflow-template-develop/src/roles/coder.ts index 27ddca3..42650f2 100644 --- a/packages/workflow-template-develop/src/roles/coder.ts +++ b/packages/workflow-template-develop/src/roles/coder.ts @@ -25,7 +25,11 @@ The thread ID (26-char Crockford Base32) appears in the first message. If unsure ## Completing a phase -Report which phase you completed using the phase **hash** (not the title). If you legitimately finish every remaining phase in this single turn, set completedPhase to the **last** phase hash in the plan (the workflow treats that as full completion). List the files you changed and summarize what you did.`; +Report which phase you completed using the phase **hash** (not the title). If you legitimately finish every remaining phase in this single turn, set completedPhase to the **last** phase hash in the plan (the workflow treats that as full completion). List the files you changed and summarize what you did. + +## Output rules + +Keep your final response **short** — a brief summary paragraph plus the structured meta output. Do NOT paste diffs, file contents, or code blocks in your response. The actual changes are already on disk; repeating them wastes tokens. Just say what you did and why.`; export const coderRole: RoleDefinition = { description: diff --git a/packages/workflow-template-develop/src/roles/planner.ts b/packages/workflow-template-develop/src/roles/planner.ts index 1abdacf..635d68a 100644 --- a/packages/workflow-template-develop/src/roles/planner.ts +++ b/packages/workflow-template-develop/src/roles/planner.ts @@ -39,7 +39,11 @@ Fewer phases is always better. Each phase must justify its existence — if two After storing all phases via the CLI, output compact JSON only: { "phases": [{ "hash": "", "title": "" }] } -Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases.`; +Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases. + +## Output rules + +Keep your final response **short** — just the JSON with phases. Do NOT paste code snippets, diffs, or implementation details in your response. Phase details are already stored in CAS; your response should only contain the compact phases JSON.`; export const plannerRole: RoleDefinition = { description: "Breaks the task into sequential phases for the coder.", diff --git a/packages/workflow-template-develop/src/roles/reviewer.ts b/packages/workflow-template-develop/src/roles/reviewer.ts index dcb4709..05a5a73 100644 --- a/packages/workflow-template-develop/src/roles/reviewer.ts +++ b/packages/workflow-template-develop/src/roles/reviewer.ts @@ -32,7 +32,11 @@ const REVIEWER_SYSTEM = `You are a code reviewer. Review the git diff for correc - **Approve** only if there are zero issues - **Reject** with specific issues that must be fixed — every issue you find is blocking -Be thorough. A false approve costs more than a false reject.`; +Be thorough. A false approve costs more than a false reject. + +## Output rules + +Keep your final response **short**. Summarize findings in a few bullet points, then output the structured verdict. Do NOT paste the full diff or large code blocks in your response.`; export const reviewerRole: RoleDefinition = { description: "Runs git diff checks and sets approved when the change is ready.", diff --git a/packages/workflow-template-develop/src/roles/tester.ts b/packages/workflow-template-develop/src/roles/tester.ts index 296d091..2eb9292 100644 --- a/packages/workflow-template-develop/src/roles/tester.ts +++ b/packages/workflow-template-develop/src/roles/tester.ts @@ -14,7 +14,11 @@ export const testerMetaSchema = z.discriminatedUnion("status", [ export type TesterMeta = z.infer; -const TESTER_SYSTEM = `You are a tester. Run the project's test suite, build, and lint commands. Check what commands are available from the preparer's output in the thread. Report pass/fail with details of what failed.`; +const TESTER_SYSTEM = `You are a tester. Run the project's test suite, build, and lint commands. Check what commands are available from the preparer's output in the thread. Report pass/fail with details of what failed. + +## Output rules + +Keep your final response **short**. Report pass/fail with a brief summary of failures (if any). Do NOT paste full test output or build logs — just the key error lines.`; export const testerRole: RoleDefinition = { description: "Runs test, build, and lint commands and reports pass or fail with details.", From 82e40f0c218b183d5fa0a024ba3d414e5a1f8a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Wed, 13 May 2026 14:20:23 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20planner=20abort=20path=20=E2=80=94?= =?UTF-8?q?=20fail=20fast=20when=20workspace=20info=20is=20missing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlannerMeta is now a discriminated union: planned | aborted - Moderator routes aborted planner → END (no coder invocation) - System prompt requires absolute workspace path, instructs abort if missing - extractRefs handles both variants - Test: 'planner aborted → END' Signed-off-by: 小橘 --- .../__tests__/develop-template.test.ts | 27 ++++++++++++++----- .../src/moderator.ts | 19 +++++++++++-- .../src/roles/planner.ts | 26 +++++++++++++----- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/packages/workflow-template-develop/__tests__/develop-template.test.ts b/packages/workflow-template-develop/__tests__/develop-template.test.ts index 0013ea2..5813b60 100644 --- a/packages/workflow-template-develop/__tests__/develop-template.test.ts +++ b/packages/workflow-template-develop/__tests__/develop-template.test.ts @@ -9,7 +9,9 @@ import type { DevelopMeta } from "../src/roles.js"; const developModerator = tableToModerator(developTable); -const DEFAULT_PHASES: PlannerMeta["phases"] = [ +type PlannedMeta = Extract; + +const DEFAULT_PHASES: PlannedMeta["phases"] = [ { hash: "4KNMR2PX", title: "Do the work", @@ -36,11 +38,11 @@ function makeCtx(steps: ModeratorContext["steps"]): ModeratorContex }; } -function plannerStep(phases: PlannerMeta["phases"] = DEFAULT_PHASES): RoleStep { +function plannerStep(phases: PlannedMeta["phases"] = DEFAULT_PHASES): RoleStep { return { role: "planner", contentHash: "STUBHASHPLANNER001", - meta: { phases }, + meta: { status: "planned" as const, phases }, refs: phases.map((p) => p.hash), timestamp: 1, }; @@ -153,7 +155,7 @@ describe("developModerator", () => { }); test("multiple planner phases → coder until all complete, then reviewer", () => { - const phases: PlannerMeta["phases"] = [ + const phases: PlannedMeta["phases"] = [ { hash: "AA000001", title: "first phase" }, { hash: "AA000002", title: "second phase" }, ]; @@ -167,7 +169,7 @@ describe("developModerator", () => { }); test("one-shot coder reports only last phase hash → reviewer (moderator treats as all phases done)", () => { - const phases: PlannerMeta["phases"] = [ + const phases: PlannedMeta["phases"] = [ { hash: "BB000001", title: "setup branch" }, { hash: "BB000002", title: "write tests" }, { hash: "BB000003", title: "verify" }, @@ -179,7 +181,7 @@ describe("developModerator", () => { }); test("unrecognised completedPhase hash → coder retry when budget allows", () => { - const phases: PlannerMeta["phases"] = [ + const phases: PlannedMeta["phases"] = [ { hash: "CC000001", title: "first phase" }, { hash: "CC000002", title: "second phase" }, ]; @@ -187,7 +189,7 @@ describe("developModerator", () => { }); test("incomplete phases → coder retry (supervisor controls termination)", () => { - const phases: PlannerMeta["phases"] = [ + const phases: PlannedMeta["phases"] = [ { hash: "DD000001", title: "first phase" }, { hash: "DD000002", title: "second phase" }, ]; @@ -198,6 +200,17 @@ describe("developModerator", () => { expect(developModerator(makeCtx(steps))).toBe("coder"); }); + test("planner aborted → END", () => { + const abortedStep: RoleStep = { + role: "planner", + contentHash: "STUBHASHABORT001", + meta: { status: "aborted", reason: "No workspace path provided" }, + refs: [], + timestamp: 1, + }; + expect(developModerator(makeCtx([abortedStep]))).toBe("__end__"); + }); + test("committer → END for any committer meta status", () => { const committed = committerStep({ status: "committed", branch: "f", commitSha: "x" }); const recoverable = committerStep({ diff --git a/packages/workflow-template-develop/src/moderator.ts b/packages/workflow-template-develop/src/moderator.ts index a2c4d79..a8152be 100644 --- a/packages/workflow-template-develop/src/moderator.ts +++ b/packages/workflow-template-develop/src/moderator.ts @@ -30,6 +30,18 @@ function coderFinishedAllPlannedPhases( // ── Conditions ───────────────────────────────────────────────────── +const plannerAborted: ModeratorCondition = { + name: "plannerAborted", + description: "The planner aborted due to insufficient information", + check: (ctx) => { + const plannerStep = ctx.steps.find((s) => s.role === "planner"); + if (plannerStep === undefined) { + return false; + } + return plannerStep.meta.status === "aborted"; + }, +}; + const allPhasesComplete: ModeratorCondition = { name: "allPhasesComplete", description: "All planned phases have been completed by the coder", @@ -38,7 +50,7 @@ const allPhasesComplete: ModeratorCondition = { if (plannerStep === undefined) { return true; } - const phases = plannerStep.meta.phases; + const phases = plannerStep.meta.status === "planned" ? plannerStep.meta.phases : []; if (!Array.isArray(phases)) { return true; } @@ -71,7 +83,10 @@ const testsPassed: ModeratorCondition = { const table: ModeratorTable = { [START]: [{ condition: "FALLBACK", role: "planner" }], - planner: [{ condition: "FALLBACK", role: "coder" }], + planner: [ + { condition: plannerAborted, role: END }, + { condition: "FALLBACK", role: "coder" }, + ], coder: [ { condition: allPhasesComplete, role: "reviewer" }, { condition: "FALLBACK", role: "coder" }, diff --git a/packages/workflow-template-develop/src/roles/planner.ts b/packages/workflow-template-develop/src/roles/planner.ts index 635d68a..eec6bf0 100644 --- a/packages/workflow-template-develop/src/roles/planner.ts +++ b/packages/workflow-template-develop/src/roles/planner.ts @@ -6,16 +6,27 @@ export const phaseSchema = z.object({ title: z.string(), }); -export const plannerMetaSchema = z.object({ - phases: z.array(phaseSchema), -}); +export const plannerMetaSchema = z.discriminatedUnion("status", [ + z.object({ + status: z.literal("planned"), + phases: z.array(phaseSchema), + }), + z.object({ + status: z.literal("aborted"), + reason: z.string().describe("Why the task cannot proceed"), + }), +]); export type PlannerMeta = z.infer; -const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time. +const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time. **Abort** if the prompt lacks critical information (e.g. no project/workspace path, ambiguous target repo). Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide. +## Prerequisites — check FIRST + +The prompt MUST include an **absolute filesystem path** to the project workspace (e.g. \`/home/user/repos/my-project\`). If no workspace path is given and you cannot reliably infer one from context, **abort immediately** with a clear reason explaining what information is missing. Do NOT guess paths. + ## Storing phase details — MANDATORY For each phase, store its full detail text in CAS via \`uncaged-workflow cas put ''\`. The command prints a content-hash — use that as the phase identifier. @@ -37,7 +48,10 @@ Fewer phases is always better. Each phase must justify its existence — if two ## Output format After storing all phases via the CLI, output compact JSON only: - { "phases": [{ "hash": "", "title": "" }] } + { "status": "planned", "phases": [{ "hash": "", "title": "" }] } + +If aborting: + { "status": "aborted", "reason": "" } Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases. @@ -49,5 +63,5 @@ export const plannerRole: RoleDefinition = { description: "Breaks the task into sequential phases for the coder.", systemPrompt: PLANNER_SYSTEM, schema: plannerMetaSchema, - extractRefs: (meta) => meta.phases.map((p) => p.hash), + extractRefs: (meta) => meta.status === "planned" ? meta.phases.map((p) => p.hash) : [], }; From 76830c5e22a248ed4c587becc1052f9ecb72ad1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Wed, 13 May 2026 14:59:20 +0000 Subject: [PATCH 3/3] chore: add log-tag lint + fix biome errors + pre-push hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/lint-log-tags.sh: static check for invalid Crockford Base32 log tags (I/L/O/U) - fix two invalid log tags in ws-client.ts (6CJX2RLP→6CJX2R8P, T9W2KL5H→T9W2K35H) - fix biome errors: unused import, exhaustive deps, cognitive complexity suppression - add pre-push git hook running bun run check - integrate lint-log-tags into bun run check pipeline Refs #244 --- .githooks/pre-push | 6 +++++ package.json | 2 +- .../src/commands/serve/ws-client.ts | 4 ++-- .../workflow-graph/condition-edge.tsx | 15 +++--------- .../components/workflow-graph/use-layout.ts | 12 ++++------ scripts/lint-log-tags.sh | 24 +++++++++++++++++++ 6 files changed, 41 insertions(+), 22 deletions(-) create mode 100755 .githooks/pre-push create mode 100755 scripts/lint-log-tags.sh diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..e824202 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# pre-push hook: typecheck + biome + lint-log-tags +set -euo pipefail +echo "🔍 pre-push: running checks..." +bun run check +echo "✅ pre-push: all checks passed" diff --git a/package.json b/package.json index ef197f4..4509f84 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ ], "scripts": { "build": "bunx tsc --build", - "check": "bunx tsc --build && biome check .", + "check": "bunx tsc --build && biome check . && bash scripts/lint-log-tags.sh", "typecheck": "bunx tsc --build", "format": "biome format --write .", "test": "bun run --filter '*' test", diff --git a/packages/cli-workflow/src/commands/serve/ws-client.ts b/packages/cli-workflow/src/commands/serve/ws-client.ts index b58e49c..b53be3d 100644 --- a/packages/cli-workflow/src/commands/serve/ws-client.ts +++ b/packages/cli-workflow/src/commands/serve/ws-client.ts @@ -100,7 +100,7 @@ export function startGatewayWsClient(params: GatewayWsClientParams): () => void clearReconnectTimer(); const delayMs = Math.min(INITIAL_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS); attempt++; - params.log("6CJX2RLP", `gateway WebSocket reconnect in ${delayMs}ms (attempt ${attempt})`); + params.log("6CJX2R8P", `gateway WebSocket reconnect in ${delayMs}ms (attempt ${attempt})`); reconnectTimer = setTimeout(connect, delayMs); }; @@ -143,7 +143,7 @@ export function startGatewayWsClient(params: GatewayWsClientParams): () => void ws.addEventListener("message", (ev) => { const data = ev.data; if (typeof data !== "string") { - params.log("T9W2KL5H", "gateway WebSocket non-text frame ignored"); + params.log("T9W2K35H", "gateway WebSocket non-text frame ignored"); return; } void handleGatewayMessage(ws, data, params).catch((e: unknown) => { diff --git a/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx b/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx index 1c18864..e16d2f2 100644 --- a/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx +++ b/packages/workflow-dashboard/src/components/workflow-graph/condition-edge.tsx @@ -1,9 +1,4 @@ -import { - BaseEdge, - EdgeLabelRenderer, - type EdgeProps, - getSmoothStepPath, -} from "@xyflow/react"; +import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from "@xyflow/react"; import type { ConditionEdgeData } from "./types.ts"; // Must match the FEEDBACK_OFFSET_X in use-layout.ts @@ -15,12 +10,7 @@ const FEEDBACK_RADIUS = 16; * Build an SVG path for a feedback (back) edge that routes to the right of the nodes. * The path goes: source right → arc → vertical up → arc → target right */ -function feedbackPath( - sourceX: number, - sourceY: number, - targetX: number, - targetY: number, -): string { +function feedbackPath(sourceX: number, sourceY: number, targetX: number, targetY: number): string { const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X; const r = FEEDBACK_RADIUS; @@ -42,6 +32,7 @@ function feedbackPath( return segments.join(" "); } +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: edge routing logic is inherently branchy export function ConditionEdge(props: EdgeProps) { const { id, diff --git a/packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts b/packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts index 4edb097..fef0d7d 100644 --- a/packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts +++ b/packages/workflow-dashboard/src/components/workflow-graph/use-layout.ts @@ -1,7 +1,7 @@ import type { Edge, Node } from "@xyflow/react"; import { useMemo } from "react"; import type { WorkflowGraphEdge } from "../../api.ts"; -import type { ConditionEdgeData, NodeState, RoleNodeData, TerminalNodeData } from "./types.ts"; +import type { NodeState, RoleNodeData, TerminalNodeData } from "./types.ts"; const START_ID = "__start__"; const END_ID = "__end__"; @@ -41,6 +41,7 @@ function edgeKey(e: WorkflowGraphEdge): string { * Forward edges go from lower rank to higher rank; feedback edges go backwards. * Self-loops are neither forward nor feedback — they're handled separately. */ +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: topological sort is inherently branchy function extractSpine(edges: readonly WorkflowGraphEdge[]): string[] { // Collect all node IDs const ids = new Set(); @@ -213,8 +214,8 @@ function computeLayout(input: LayoutInput): LayoutResult { isFallback, isFeedback, isSelfLoop, - labelX, - labelY, + labelX, + labelY, }, }; }); @@ -223,8 +224,5 @@ function computeLayout(input: LayoutInput): LayoutResult { } export function useLayout(input: LayoutInput): LayoutResult { - return useMemo( - () => computeLayout(input), - [input.edges, input.roles, input.nodeStates], - ); + return useMemo(() => computeLayout(input), [input]); } diff --git a/scripts/lint-log-tags.sh b/scripts/lint-log-tags.sh new file mode 100755 index 0000000..64f092f --- /dev/null +++ b/scripts/lint-log-tags.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Validate Crockford Base32 log tags in .log("TAG", ...) calls. +# Crockford Base32 excludes: I, L, O, U +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BAD=0 + +while IFS= read -r match; do + file="${match%%:*}" + rest="${match#*:}" + line="${rest%%:*}" + tag=$(echo "$rest" | grep -oP '\.log\(\s*"\K[A-Za-z0-9]+') + if echo "$tag" | grep -qiE '[ILOU]'; then + echo " ❌ ${file}:${line} tag \"${tag}\" contains invalid Crockford Base32 char (I/L/O/U)" + BAD=1 + fi +done < <(grep -rn '\.log("[A-Za-z0-9]\{8\}"' "$ROOT/packages/" --include='*.ts' \ + | grep -v node_modules | grep -v '/dist/') + +if [ "$BAD" -eq 0 ]; then + echo " ✅ All log tags are valid Crockford Base32" +fi +exit $BAD