cclaw-cli 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +12 -1
- package/dist/content/harness-playbooks.js +9 -4
- package/dist/content/harnesses-doc.js +5 -0
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +46 -6
- package/dist/content/next-command.js +6 -3
- package/dist/content/observe.d.ts +1 -0
- package/dist/content/observe.js +101 -2
- package/dist/content/protocols.js +14 -9
- package/dist/content/skills.js +3 -0
- package/dist/content/stages/design.js +1 -0
- package/dist/content/stages/plan.js +3 -2
- package/dist/content/stages/scope.js +2 -1
- package/dist/content/stages/tdd.js +1 -8
- package/dist/install.js +10 -2
- package/dist/internal/advance-stage.d.ts +7 -0
- package/dist/internal/advance-stage.js +425 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { FlowTrack, HarnessId } from "./types.js";
|
|
3
3
|
import type { EvalMode } from "./eval/types.js";
|
|
4
|
-
type CommandName = "init" | "sync" | "doctor" | "upgrade" | "uninstall" | "archive" | "eval";
|
|
4
|
+
type CommandName = "init" | "sync" | "doctor" | "upgrade" | "uninstall" | "archive" | "eval" | "internal";
|
|
5
5
|
interface ParsedArgs {
|
|
6
6
|
command?: CommandName;
|
|
7
7
|
harnesses?: HarnessId[];
|
|
@@ -33,6 +33,8 @@ interface ParsedArgs {
|
|
|
33
33
|
evalArgs?: string[];
|
|
34
34
|
evalBackground?: boolean;
|
|
35
35
|
evalCompareModel?: string;
|
|
36
|
+
/** Hidden plumbing command (`cclaw internal ...`) arguments. */
|
|
37
|
+
internalArgs?: string[];
|
|
36
38
|
showHelp?: boolean;
|
|
37
39
|
showVersion?: boolean;
|
|
38
40
|
}
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { formatDiffMarkdown, runEvalDiff } from "./eval/diff.js";
|
|
|
24
24
|
import { ensureRunDir, generateRunId, isRunAlive, listRuns, readRunStatus, resolveRunId, runLogPath, writeRunStatus } from "./eval/runs.js";
|
|
25
25
|
import { parseModeInput } from "./eval/mode.js";
|
|
26
26
|
import { FLOW_STAGES } from "./types.js";
|
|
27
|
+
import { runInternalCommand } from "./internal/advance-stage.js";
|
|
27
28
|
const INSTALLER_COMMANDS = [
|
|
28
29
|
"init",
|
|
29
30
|
"sync",
|
|
@@ -31,7 +32,8 @@ const INSTALLER_COMMANDS = [
|
|
|
31
32
|
"upgrade",
|
|
32
33
|
"uninstall",
|
|
33
34
|
"archive",
|
|
34
|
-
"eval"
|
|
35
|
+
"eval",
|
|
36
|
+
"internal"
|
|
35
37
|
];
|
|
36
38
|
export function usage() {
|
|
37
39
|
return `cclaw - installer-first flow toolkit
|
|
@@ -418,6 +420,12 @@ function parseArgs(argv) {
|
|
|
418
420
|
parsed.command = INSTALLER_COMMANDS.includes(commandRaw)
|
|
419
421
|
? commandRaw
|
|
420
422
|
: undefined;
|
|
423
|
+
// Hidden maintainer surface for runtime guards/helpers. Keep raw positional
|
|
424
|
+
// args untouched so subcommand-level parsing can evolve independently.
|
|
425
|
+
if (parsed.command === "internal") {
|
|
426
|
+
parsed.internalArgs = [...rest];
|
|
427
|
+
return parsed;
|
|
428
|
+
}
|
|
421
429
|
// For `eval`, the next non-flag argument is an optional subcommand. Any
|
|
422
430
|
// subsequent non-flag tokens are captured as evalArgs (consumed by the
|
|
423
431
|
// subcommand handler). This preserves backwards compat: callers that run
|
|
@@ -796,6 +804,9 @@ async function runCommand(parsed, ctx) {
|
|
|
796
804
|
if (!command) {
|
|
797
805
|
return printNoArgsHint(ctx);
|
|
798
806
|
}
|
|
807
|
+
if (command === "internal") {
|
|
808
|
+
return runInternalCommand(ctx.cwd, parsed.internalArgs ?? [], ctx);
|
|
809
|
+
}
|
|
799
810
|
if (command === "init") {
|
|
800
811
|
const resolved = await resolveInitInputs(parsed, ctx);
|
|
801
812
|
const effectiveTrack = resolved.track;
|
|
@@ -232,10 +232,12 @@ Codex CLI has a different shape from Claude/Cursor:
|
|
|
232
232
|
- **Tool interception is Bash-only.** Codex's \`PreToolUse\` and
|
|
233
233
|
\`PostToolUse\` events only fire for the \`Bash\` tool. \`Write\`,
|
|
234
234
|
\`Edit\`, \`WebSearch\`, and MCP tool calls are **not** gated by hooks.
|
|
235
|
-
cclaw partially compensates by
|
|
236
|
-
\`prompt-guard.sh\`
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
cclaw partially compensates by wiring \`UserPromptSubmit\` to both
|
|
236
|
+
\`prompt-guard.sh\` and a non-blocking
|
|
237
|
+
\`cclaw internal verify-current-state --quiet\` nudge that emits
|
|
238
|
+
unmet-delegation / missing-evidence warnings before the turn executes.
|
|
239
|
+
This is still a nudge, not a hard block: workflow-guard (TDD red-first,
|
|
240
|
+
artifact presence) only fires on Bash turns. See the hook coverage matrix below.
|
|
239
241
|
- **Legacy paths.** \`.codex/commands/*\` was never consumed by Codex and
|
|
240
242
|
is removed on every \`cclaw sync\`. The v0.39.x \`.agents/skills/cclaw-cc*/\`
|
|
241
243
|
layout is replaced by \`.agents/skills/cc*/\` and the old folders are
|
|
@@ -289,6 +291,8 @@ disabled in v0.33 and remains off.
|
|
|
289
291
|
- \`/use cc\` — open the \`/cc\` skill and pick a track.
|
|
290
292
|
- \`/use cc-next\` — advance the flow one stage.
|
|
291
293
|
- \`/use cc-ops\` — compound / archive / rewind.
|
|
294
|
+
- \`bash .cclaw/hooks/stage-complete.sh <stage>\` — canonical stage closeout helper;
|
|
295
|
+
validates delegations + gate evidence before mutating \`flow-state.json\`.
|
|
292
296
|
- Typing \`/cc …\` or \`/cc-next …\` in plain text also works: Codex
|
|
293
297
|
matches the skill descriptions (which spell out these tokens) and
|
|
294
298
|
auto-loads the right skill body.
|
|
@@ -316,6 +320,7 @@ continue to work regardless.
|
|
|
316
320
|
|-------------|---------------|----------|
|
|
317
321
|
| SessionStart rehydration | \`SessionStart\` matcher \`startup|resume\` → \`session-start.sh\` | Full. |
|
|
318
322
|
| PreToolUse prompt-guard | \`PreToolUse\` matcher \`Bash\` + \`UserPromptSubmit\` → \`prompt-guard.sh\` | Bash tool calls are gated inline; \`UserPromptSubmit\` catches prompts before any tool fires, so non-Bash writes (\`Write\`/\`Edit\`) are still prompt-guarded at the turn boundary. |
|
|
323
|
+
| UserPromptSubmit state nudge | \`UserPromptSubmit\` → \`cclaw internal verify-current-state --quiet\` | Non-blocking warning only. Prints unmet mandatory delegation / gate-evidence counts before the turn; cannot block non-Bash \`Write\`/\`Edit\`. |
|
|
319
324
|
| PreToolUse workflow-guard | \`PreToolUse\` matcher \`Bash\` → \`workflow-guard.sh\` | Bash-only. For \`Write\`/\`Edit\` calls the agent performs the TDD-order / artifact check in-turn (see the stage skill). |
|
|
320
325
|
| PostToolUse context-monitor | \`PostToolUse\` matcher \`Bash\` → \`context-monitor.sh\` | Bash-only. Other tool calls get context-monitored at end-of-turn via \`.cclaw/references/protocols/ethos.md\`. |
|
|
321
326
|
| Stop checkpoint | \`Stop\` → \`stop-checkpoint.sh\` | Full. |
|
|
@@ -68,6 +68,11 @@ ${hookRows}
|
|
|
68
68
|
- \`tier1\`: full native delegation + structured asks + full hook surface.
|
|
69
69
|
- \`tier2\`: usable flow with capability gaps; mandatory delegation can require waivers.
|
|
70
70
|
- \`tier3\`: manual-only fallback; no native automation guarantees.
|
|
71
|
+
- Codex-specific ceiling: \`PreToolUse\` can only intercept \`Bash\`. Direct
|
|
72
|
+
\`Write\`/\`Edit\` to \`.cclaw/state/flow-state.json\` cannot be hard-blocked
|
|
73
|
+
at hook level, so the canonical path is
|
|
74
|
+
\`bash .cclaw/hooks/stage-complete.sh <stage>\` plus the non-blocking
|
|
75
|
+
\`UserPromptSubmit\` state nudge.
|
|
71
76
|
|
|
72
77
|
## Shared command contract
|
|
73
78
|
|
package/dist/content/hooks.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface HookRuntimeOptions {
|
|
|
11
11
|
export declare const RUNTIME_SHELL_DETECT_ROOT = "HARNESS=\"codex\"\nif [ -n \"${CLAUDE_PROJECT_DIR:-}\" ]; then\n HARNESS=\"claude\"\nelif [ -n \"${CURSOR_PROJECT_DIR:-}\" ] || [ -n \"${CURSOR_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"cursor\"\nelif [ -n \"${OPENCODE_PROJECT_DIR:-}\" ] || [ -n \"${OPENCODE_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"opencode\"\nfi\n\nROOT=\"\"\nfor candidate in \"${CCLAW_PROJECT_ROOT:-}\" \"${CLAUDE_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_ROOT:-}\" \"${OPENCODE_PROJECT_DIR:-}\" \"${OPENCODE_PROJECT_ROOT:-}\" \"${PWD:-}\"; do\n if [ -n \"$candidate\" ] && [ -d \"$candidate/.cclaw\" ]; then\n ROOT=\"$candidate\"\n break\n fi\ndone\nif [ -z \"$ROOT\" ]; then\n ROOT=\"${CCLAW_PROJECT_ROOT:-${CLAUDE_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-${CURSOR_PROJECT_ROOT:-${OPENCODE_PROJECT_DIR:-${OPENCODE_PROJECT_ROOT:-${PWD}}}}}}}\"\nfi";
|
|
12
12
|
export declare function sessionStartScript(_options?: HookRuntimeOptions): string;
|
|
13
13
|
export declare function stopCheckpointScript(): string;
|
|
14
|
+
export declare function stageCompleteScript(): string;
|
|
14
15
|
export declare function preCompactScript(): string;
|
|
15
16
|
export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
|
|
16
17
|
export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
|
package/dist/content/hooks.js
CHANGED
|
@@ -769,6 +769,35 @@ case "$HARNESS" in
|
|
|
769
769
|
esac
|
|
770
770
|
`;
|
|
771
771
|
}
|
|
772
|
+
export function stageCompleteScript() {
|
|
773
|
+
return `#!/usr/bin/env bash
|
|
774
|
+
# cclaw stage-complete helper — generated by cclaw sync
|
|
775
|
+
# Canonical helper for stage closeout: delegates validation + flow-state
|
|
776
|
+
# mutation to \`cclaw internal advance-stage\`.
|
|
777
|
+
set -euo pipefail
|
|
778
|
+
|
|
779
|
+
${DETECT_ROOT}
|
|
780
|
+
|
|
781
|
+
if [ "$#" -lt 1 ]; then
|
|
782
|
+
printf 'Usage: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...]\\n' >&2
|
|
783
|
+
exit 1
|
|
784
|
+
fi
|
|
785
|
+
|
|
786
|
+
if [ ! -d "$ROOT/${RUNTIME_ROOT}" ]; then
|
|
787
|
+
printf '[cclaw] stage-complete: runtime root not found at %s\\n' "$ROOT/${RUNTIME_ROOT}" >&2
|
|
788
|
+
exit 1
|
|
789
|
+
fi
|
|
790
|
+
|
|
791
|
+
STAGE="$1"
|
|
792
|
+
shift || true
|
|
793
|
+
|
|
794
|
+
if command -v cclaw >/dev/null 2>&1; then
|
|
795
|
+
exec cclaw internal advance-stage "$STAGE" "$@"
|
|
796
|
+
fi
|
|
797
|
+
|
|
798
|
+
exec npx -y cclaw-cli internal advance-stage "$STAGE" "$@"
|
|
799
|
+
`;
|
|
800
|
+
}
|
|
772
801
|
export function preCompactScript() {
|
|
773
802
|
return `#!/usr/bin/env bash
|
|
774
803
|
# cclaw pre-compact hook — generated by cclaw sync
|
|
@@ -1109,14 +1138,15 @@ export default function cclawPlugin(ctx) {
|
|
|
1109
1138
|
const scriptPath = join(root, "${RUNTIME_ROOT}/hooks/" + scriptFileName);
|
|
1110
1139
|
const input = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
|
|
1111
1140
|
try {
|
|
1112
|
-
spawnSync("bash", [scriptPath], {
|
|
1141
|
+
const result = spawnSync("bash", [scriptPath], {
|
|
1113
1142
|
cwd: root,
|
|
1114
1143
|
timeout: 20000,
|
|
1115
1144
|
stdio: ["pipe", "ignore", "ignore"],
|
|
1116
1145
|
input
|
|
1117
1146
|
});
|
|
1147
|
+
return typeof result.status === "number" ? result.status === 0 : false;
|
|
1118
1148
|
} catch {
|
|
1119
|
-
|
|
1149
|
+
return false;
|
|
1120
1150
|
}
|
|
1121
1151
|
}
|
|
1122
1152
|
|
|
@@ -1167,8 +1197,13 @@ export default function cclawPlugin(ctx) {
|
|
|
1167
1197
|
}
|
|
1168
1198
|
if (eventType === "tool.execute.before") {
|
|
1169
1199
|
const toolPayload = normalizeToolPayload(eventData, undefined);
|
|
1170
|
-
await runHookScript("prompt-guard.sh", toolPayload);
|
|
1171
|
-
await runHookScript("workflow-guard.sh", toolPayload);
|
|
1200
|
+
const promptOk = await runHookScript("prompt-guard.sh", toolPayload);
|
|
1201
|
+
const workflowOk = await runHookScript("workflow-guard.sh", toolPayload);
|
|
1202
|
+
if (!promptOk || !workflowOk) {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
"cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1172
1207
|
}
|
|
1173
1208
|
if (eventType === "tool.execute.after") {
|
|
1174
1209
|
const toolPayload = normalizeToolPayload(eventData, undefined);
|
|
@@ -1177,8 +1212,13 @@ export default function cclawPlugin(ctx) {
|
|
|
1177
1212
|
},
|
|
1178
1213
|
"tool.execute.before": async (input, output) => {
|
|
1179
1214
|
const payload = normalizeToolPayload(input, output);
|
|
1180
|
-
await runHookScript("prompt-guard.sh", payload);
|
|
1181
|
-
await runHookScript("workflow-guard.sh", payload);
|
|
1215
|
+
const promptOk = await runHookScript("prompt-guard.sh", payload);
|
|
1216
|
+
const workflowOk = await runHookScript("workflow-guard.sh", payload);
|
|
1217
|
+
if (!promptOk || !workflowOk) {
|
|
1218
|
+
throw new Error(
|
|
1219
|
+
"cclaw OpenCode guard blocked tool.execute.before (prompt/workflow guard non-zero exit)."
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1182
1222
|
},
|
|
1183
1223
|
"tool.execute.after": async (input, output) => {
|
|
1184
1224
|
const payload = normalizeToolPayload(input, output);
|
|
@@ -44,13 +44,14 @@ This is the only progression command the user needs to drive the entire flow. St
|
|
|
44
44
|
5. Let \`catalog\` = \`stageGateCatalog[currentStage]\` from flow state.
|
|
45
45
|
6. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
|
|
46
46
|
7. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
|
|
47
|
-
8. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if
|
|
47
|
+
8. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if each mandatory agent is **completed** or **waived**.
|
|
48
|
+
9. If any mandatory delegation is missing and no waiver exists: **STOP** and ask the user whether to dispatch now or waive with rationale. Do not mark gates passed while delegation is unresolved.
|
|
48
49
|
|
|
49
50
|
### Path A: Current stage is NOT complete (any gate unmet or delegation missing)
|
|
50
51
|
|
|
51
52
|
→ Load **\`${RUNTIME_ROOT}/skills/<skillFolder>/SKILL.md\`** and **\`${RUNTIME_ROOT}/commands/<currentStage>.md\`** for the current stage.
|
|
52
53
|
→ Execute that stage's protocol. The stage skill handles the full interaction including STOP points and gate tracking.
|
|
53
|
-
→
|
|
54
|
+
→ Stage completion must use \`bash .cclaw/hooks/stage-complete.sh <currentStage>\` (canonical), which validates delegations + gate evidence before mutating \`flow-state.json\`.
|
|
54
55
|
|
|
55
56
|
### Path B: Current stage IS complete (all gates passed, all delegations satisfied)
|
|
56
57
|
|
|
@@ -152,6 +153,8 @@ For each gate id in \`requiredGates\` for \`currentStage\`:
|
|
|
152
153
|
- **Unmet** otherwise.
|
|
153
154
|
|
|
154
155
|
Check \`mandatoryDelegations\` via **\`${delegationPath}\`** — satisfied only if **completed** or **waived**.
|
|
156
|
+
If a mandatory delegation is missing and no waiver exists, **STOP** and ask:
|
|
157
|
+
(A) dispatch now, (B) waive with rationale, (C) cancel stage advance.
|
|
155
158
|
|
|
156
159
|
### Step 3: Act
|
|
157
160
|
|
|
@@ -161,7 +164,7 @@ Load the current stage's skill and command contract:
|
|
|
161
164
|
- \`${RUNTIME_ROOT}/skills/<skillFolder>/SKILL.md\`
|
|
162
165
|
- \`${RUNTIME_ROOT}/commands/<currentStage>.md\`
|
|
163
166
|
|
|
164
|
-
Execute the stage protocol. The stage skill handles interaction, STOP points, gate tracking, and
|
|
167
|
+
Execute the stage protocol. The stage skill handles interaction, STOP points, gate tracking, and stage completion via \`bash .cclaw/hooks/stage-complete.sh <stage>\` (canonical flow-state mutation path).
|
|
165
168
|
|
|
166
169
|
**Path B — stage IS complete (all gates met, all delegations done):**
|
|
167
170
|
|
|
@@ -10,6 +10,7 @@ export interface PromptGuardOptions {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function promptGuardScript(options?: PromptGuardOptions): string;
|
|
12
12
|
export interface WorkflowGuardOptions {
|
|
13
|
+
workflowGuardMode?: "advisory" | "strict";
|
|
13
14
|
tddEnforcementMode?: "advisory" | "strict";
|
|
14
15
|
tddTestGlobs?: string[];
|
|
15
16
|
}
|
package/dist/content/observe.js
CHANGED
|
@@ -154,6 +154,7 @@ exit 0
|
|
|
154
154
|
`;
|
|
155
155
|
}
|
|
156
156
|
export function workflowGuardScript(options = {}) {
|
|
157
|
+
const workflowGuardMode = options.workflowGuardMode === "strict" ? "strict" : "advisory";
|
|
157
158
|
const tddEnforcementMode = options.tddEnforcementMode === "strict" ? "strict" : "advisory";
|
|
158
159
|
const tddTestGlobs = options.tddTestGlobs && options.tddTestGlobs.length > 0
|
|
159
160
|
? options.tddTestGlobs.join(",")
|
|
@@ -162,7 +163,7 @@ export function workflowGuardScript(options = {}) {
|
|
|
162
163
|
# cclaw workflow guard hook — generated by cclaw sync
|
|
163
164
|
# Enforces stage-aware command discipline and recent flow-state read hygiene.
|
|
164
165
|
set -uo pipefail
|
|
165
|
-
WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE
|
|
166
|
+
WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-${workflowGuardMode}}"
|
|
166
167
|
MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
|
|
167
168
|
TDD_ENFORCEMENT_MODE="${tddEnforcementMode}"
|
|
168
169
|
TDD_TEST_GLOBS="${tddTestGlobs}"
|
|
@@ -342,6 +343,81 @@ is_cclaw_cli_payload() {
|
|
|
342
343
|
printf '%s' "$1" | grep -Eq '(cclaw |npx cclaw |/cc-|/cc[^[:alnum:]_-])'
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
extract_flow_state_after_json() {
|
|
347
|
+
if command -v jq >/dev/null 2>&1; then
|
|
348
|
+
printf '%s' "$INPUT" | jq -r '
|
|
349
|
+
.tool_input?.content //
|
|
350
|
+
.input?.content //
|
|
351
|
+
.arguments?.content //
|
|
352
|
+
.params?.content //
|
|
353
|
+
.payload?.content //
|
|
354
|
+
.content //
|
|
355
|
+
.input?.new_string //
|
|
356
|
+
.tool_input?.new_string //
|
|
357
|
+
""
|
|
358
|
+
' 2>/dev/null || echo ""
|
|
359
|
+
return 0
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
363
|
+
INPUT_JSON="$INPUT" python3 - <<'PY'
|
|
364
|
+
import json
|
|
365
|
+
import os
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
payload = json.loads(os.environ.get("INPUT_JSON", "{}"))
|
|
369
|
+
except Exception:
|
|
370
|
+
payload = {}
|
|
371
|
+
|
|
372
|
+
def pick(value):
|
|
373
|
+
if not isinstance(value, dict):
|
|
374
|
+
return ""
|
|
375
|
+
for key in ("tool_input", "input", "arguments", "params", "payload"):
|
|
376
|
+
nested = value.get(key)
|
|
377
|
+
if isinstance(nested, dict):
|
|
378
|
+
content = nested.get("content")
|
|
379
|
+
if isinstance(content, str) and content.strip():
|
|
380
|
+
return content
|
|
381
|
+
new_string = nested.get("new_string")
|
|
382
|
+
if isinstance(new_string, str) and new_string.strip():
|
|
383
|
+
return new_string
|
|
384
|
+
content = value.get("content")
|
|
385
|
+
if isinstance(content, str) and content.strip():
|
|
386
|
+
return content
|
|
387
|
+
return ""
|
|
388
|
+
|
|
389
|
+
print(pick(payload))
|
|
390
|
+
PY
|
|
391
|
+
return 0
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
printf ''
|
|
395
|
+
return 0
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
verify_flow_state_candidate() {
|
|
399
|
+
local candidate_json="$1"
|
|
400
|
+
[ -n "$candidate_json" ] || return 1
|
|
401
|
+
local tmp_file="$STATE_DIR/.flow-state-candidate.$$.$RANDOM.json"
|
|
402
|
+
printf '%s' "$candidate_json" > "$tmp_file" 2>/dev/null || {
|
|
403
|
+
rm -f "$tmp_file" 2>/dev/null || true
|
|
404
|
+
return 1
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
local verify_cmd=(npx -y cclaw-cli internal verify-flow-state-diff --after-file="$tmp_file" --quiet)
|
|
408
|
+
if command -v cclaw >/dev/null 2>&1; then
|
|
409
|
+
verify_cmd=(cclaw internal verify-flow-state-diff --after-file="$tmp_file" --quiet)
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
if "\${verify_cmd[@]}" >/dev/null 2>&1; then
|
|
413
|
+
rm -f "$tmp_file" 2>/dev/null || true
|
|
414
|
+
return 0
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
rm -f "$tmp_file" 2>/dev/null || true
|
|
418
|
+
return 1
|
|
419
|
+
}
|
|
420
|
+
|
|
345
421
|
is_preimplementation_stage() {
|
|
346
422
|
case "$1" in
|
|
347
423
|
brainstorm|scope|design|spec|plan) return 0 ;;
|
|
@@ -480,6 +556,22 @@ if [ -n "$TARGET_STAGE" ] && [ "$CURRENT_STAGE" != "none" ]; then
|
|
|
480
556
|
fi
|
|
481
557
|
fi
|
|
482
558
|
|
|
559
|
+
if is_mutating_tool "$TOOL_LOWER" && printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/state/flow-state\.json'; then
|
|
560
|
+
if [ -n "$REASONS" ]; then
|
|
561
|
+
REASONS="$REASONS,direct_flow_state_edit"
|
|
562
|
+
else
|
|
563
|
+
REASONS="direct_flow_state_edit"
|
|
564
|
+
fi
|
|
565
|
+
FLOW_STATE_AFTER_JSON=$(extract_flow_state_after_json)
|
|
566
|
+
if [ -n "$FLOW_STATE_AFTER_JSON" ]; then
|
|
567
|
+
if ! verify_flow_state_candidate "$FLOW_STATE_AFTER_JSON"; then
|
|
568
|
+
REASONS="$REASONS,flow_state_edit_failed_internal_validation"
|
|
569
|
+
fi
|
|
570
|
+
else
|
|
571
|
+
REASONS="$REASONS,flow_state_edit_without_serialized_content"
|
|
572
|
+
fi
|
|
573
|
+
fi
|
|
574
|
+
|
|
483
575
|
if is_preimplementation_stage "$CURRENT_STAGE" && is_mutating_tool "$TOOL_LOWER"; then
|
|
484
576
|
if ! printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/'; then
|
|
485
577
|
if [ -n "$REASONS" ]; then
|
|
@@ -567,7 +659,11 @@ PY
|
|
|
567
659
|
fi
|
|
568
660
|
|
|
569
661
|
if [ -n "$REASONS" ]; then
|
|
570
|
-
|
|
662
|
+
if printf '%s' "$REASONS" | grep -Eq 'direct_flow_state_edit'; then
|
|
663
|
+
NOTE="Cclaw workflow guard: direct flow-state edit bypasses the canonical stage-complete helper (\${REASONS}). Prefer: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage>. In strict mode this is blocked."
|
|
664
|
+
else
|
|
665
|
+
NOTE="Cclaw workflow guard: detected potential flow violation (\${REASONS}). Re-read ${RUNTIME_ROOT}/state/flow-state.json, avoid source edits before tdd stage, and enforce RED -> GREEN -> REFACTOR discipline inside tdd."
|
|
666
|
+
fi
|
|
571
667
|
if command -v jq >/dev/null 2>&1; then
|
|
572
668
|
ENTRY=$(jq -n -c \
|
|
573
669
|
--arg ts "$TS" \
|
|
@@ -1826,6 +1922,9 @@ export function codexHooksJsonWithObservation() {
|
|
|
1826
1922
|
hooks: [{
|
|
1827
1923
|
type: "command",
|
|
1828
1924
|
command: `bash ${RUNTIME_ROOT}/hooks/prompt-guard.sh`
|
|
1925
|
+
}, {
|
|
1926
|
+
type: "command",
|
|
1927
|
+
command: "bash -lc 'if command -v cclaw >/dev/null 2>&1; then cclaw internal verify-current-state --quiet >/dev/null || true; else npx -y cclaw-cli internal verify-current-state --quiet >/dev/null || true; fi'"
|
|
1829
1928
|
}]
|
|
1830
1929
|
}],
|
|
1831
1930
|
PreToolUse: [{
|
|
@@ -99,16 +99,21 @@ Shared closeout sequence applied by every stage skill.
|
|
|
99
99
|
## Required order
|
|
100
100
|
|
|
101
101
|
1. Verify mandatory delegations are completed or explicitly waived.
|
|
102
|
-
2.
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
2. Persist stage artifact under \`.cclaw/artifacts/\`.
|
|
103
|
+
3. Use the canonical helper:
|
|
104
|
+
- \`bash .cclaw/hooks/stage-complete.sh <stage>\`
|
|
105
|
+
- helper responsibilities: validate mandatory delegations, validate
|
|
106
|
+
current-stage gate evidence/artifact lint, update
|
|
107
|
+
\`stageGateCatalog\` + \`guardEvidence\`, and advance \`currentStage\`.
|
|
108
|
+
4. Legacy fallback (only when helper is unavailable): manually edit
|
|
109
|
+
\`.cclaw/state/flow-state.json\` to mark passed gates, clear resolved
|
|
110
|
+
blocked gates, and update \`guardEvidence\`. This path is legacy and
|
|
111
|
+
intentionally noisy in workflow guards.
|
|
112
|
+
5. Run \`npx cclaw doctor\` and resolve failures.
|
|
113
|
+
6. **Capture through-flow learnings** — see the policy below. Knowledge
|
|
109
114
|
accrues continuously across stages, not just at retro.
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
7. Notify user with stage completion and next action (\`/cc-next\`).
|
|
116
|
+
8. Stop; do not auto-run the next stage unless user asks.
|
|
112
117
|
|
|
113
118
|
## Through-flow knowledge capture
|
|
114
119
|
|
package/dist/content/skills.js
CHANGED
|
@@ -222,6 +222,9 @@ function completionParametersBlock(schema) {
|
|
|
222
222
|
- \`gates\`: ${gateList}
|
|
223
223
|
- \`artifact\`: \`${RUNTIME_ROOT}/artifacts/${schema.artifactFile}\`
|
|
224
224
|
- \`mandatory delegations\`: ${mandatory}
|
|
225
|
+
- \`completion helper\`: \`bash .cclaw/hooks/stage-complete.sh ${schema.stage}\`
|
|
226
|
+
- Record mandatory delegation completion/waiver in \`${RUNTIME_ROOT}/state/delegation-log.json\` with rationale as needed.
|
|
227
|
+
- Use the completion helper instead of raw \`flow-state.json\` edits (legacy direct edits trigger workflow-guard warnings or strict-mode blocks).
|
|
225
228
|
|
|
226
229
|
Apply shared completion logic from:
|
|
227
230
|
\`${COMPLETION_PROTOCOL_PATH}\`
|
|
@@ -42,6 +42,7 @@ export const DESIGN = {
|
|
|
42
42
|
"If a section has no issues, say 'No issues found' and move on.",
|
|
43
43
|
"Do not skip failure-mode mapping.",
|
|
44
44
|
"For design baseline approval: present the full baseline. **STOP.** Do NOT proceed until user explicitly approves the design.",
|
|
45
|
+
"**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `bash .cclaw/hooks/stage-complete.sh design` (do not hand-edit `.cclaw/state/flow-state.json`).",
|
|
45
46
|
"Take a firm position on every recommendation. Do NOT hedge with 'it depends' or 'you could do either'. State your opinion, then justify it.",
|
|
46
47
|
"Use pushback patterns for weak framing: if the user says 'it's just a small change', respond with 'small changes to shared interfaces have outsized blast radius — let's map it'. If 'we'll refactor later', respond with 'later never comes — show me the refactor ticket or do it now'.",
|
|
47
48
|
"When the user's proposed architecture is suboptimal, say so directly. Offer the alternative with concrete trade-offs, do not bury criticism in praise.",
|
|
@@ -29,7 +29,7 @@ export const PLAN = {
|
|
|
29
29
|
"Map scope Locked Decisions — every D-XX from scope is referenced by at least one plan task (or explicitly marked deferred with reason).",
|
|
30
30
|
"Run anti-placeholder + anti-scope-reduction scans — block `TODO/TBD/...` and phrasing like `v1`, `for now`, `later` for locked boundaries.",
|
|
31
31
|
"Define checkpoints — mark points where progress should be validated before continuing.",
|
|
32
|
-
"WAIT_FOR_CONFIRM — write plan artifact and explicitly pause. **STOP.** Do NOT proceed until user confirms. Then
|
|
32
|
+
"WAIT_FOR_CONFIRM — write plan artifact and explicitly pause. **STOP.** Do NOT proceed until user confirms. Then close the stage with `bash .cclaw/hooks/stage-complete.sh plan` and tell user to run `/cc-next`."
|
|
33
33
|
],
|
|
34
34
|
interactionProtocol: [
|
|
35
35
|
"Plan in read-only mode relative to implementation.",
|
|
@@ -38,7 +38,8 @@ export const PLAN = {
|
|
|
38
38
|
"Attach verification step to every task.",
|
|
39
39
|
"Preserve locked scope boundaries: no silent scope reduction language in task rows.",
|
|
40
40
|
"Enforce WAIT_FOR_CONFIRM: present the plan summary with options (A) Approve / (B) Revise / (C) Reject.",
|
|
41
|
-
"**STOP.** Do NOT proceed until user explicitly approves.
|
|
41
|
+
"**STOP.** Do NOT proceed until user explicitly approves.",
|
|
42
|
+
"**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `bash .cclaw/hooks/stage-complete.sh plan` and tell the user to run `/cc-next`."
|
|
42
43
|
],
|
|
43
44
|
process: [
|
|
44
45
|
"Build dependency graph and ordered slices.",
|
|
@@ -41,7 +41,8 @@ export const SCOPE = {
|
|
|
41
41
|
"Record explicit in-scope and out-of-scope contract.",
|
|
42
42
|
"Once the user accepts or rejects a recommendation, commit fully. Do not re-argue.",
|
|
43
43
|
"Produce a clean scope summary after all issues are resolved.",
|
|
44
|
-
"**STOP.** Wait for explicit user approval of scope contract before advancing to design."
|
|
44
|
+
"**STOP.** Wait for explicit user approval of scope contract before advancing to design.",
|
|
45
|
+
"**STOP BEFORE ADVANCE.** Mandatory delegation `planner` must be marked completed or explicitly waived in `.cclaw/state/delegation-log.json`. Then close the stage via `bash .cclaw/hooks/stage-complete.sh scope` (do not hand-edit `.cclaw/state/flow-state.json`)."
|
|
45
46
|
],
|
|
46
47
|
process: [
|
|
47
48
|
"Run premise challenge and existing-solution leverage check.",
|
|
@@ -92,18 +92,11 @@ export const TDD = {
|
|
|
92
92
|
],
|
|
93
93
|
commonRationalizations: [
|
|
94
94
|
"Writing code before failing test",
|
|
95
|
-
"Asserting implementation details instead of behavior",
|
|
96
|
-
"Big-bang implementation across multiple slices",
|
|
97
95
|
"Partial test runs presented as GREEN",
|
|
98
96
|
"Skipping evidence capture",
|
|
99
97
|
"Undocumented refactor changes",
|
|
100
|
-
"Adding features beyond what RED tests require",
|
|
101
|
-
"No failing test output (RED missing)",
|
|
102
|
-
"Implementation edits appear before RED evidence",
|
|
103
98
|
"No full-suite GREEN evidence",
|
|
104
|
-
"
|
|
105
|
-
"Multiple tasks implemented in one pass without justification",
|
|
106
|
-
"Files changed outside current slice scope"
|
|
99
|
+
"Multiple tasks implemented in one pass without justification"
|
|
107
100
|
],
|
|
108
101
|
policyNeedles: ["RED", "GREEN", "REFACTOR", "failing test", "full test suite", "acceptance criteria", "traceable to plan slice"],
|
|
109
102
|
artifactFile: "06-tdd.md",
|
package/dist/install.js
CHANGED
|
@@ -23,7 +23,7 @@ import { archiveCommandContract, archiveCommandSkillMarkdown } from "./content/a
|
|
|
23
23
|
import { rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rewind-command.js";
|
|
24
24
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
25
25
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
26
|
-
import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
26
|
+
import { sessionStartScript, stopCheckpointScript, stageCompleteScript, preCompactScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
27
27
|
import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./content/observe.js";
|
|
28
28
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
29
29
|
import { decisionProtocolMarkdown, completionProtocolMarkdown, ethosProtocolMarkdown } from "./content/protocols.js";
|
|
@@ -616,11 +616,13 @@ async function writeHooks(projectRoot, config) {
|
|
|
616
616
|
await ensureDir(hooksDir);
|
|
617
617
|
await writeFileSafe(path.join(hooksDir, "session-start.sh"), sessionStartScript());
|
|
618
618
|
await writeFileSafe(path.join(hooksDir, "stop-checkpoint.sh"), stopCheckpointScript());
|
|
619
|
+
await writeFileSafe(path.join(hooksDir, "stage-complete.sh"), stageCompleteScript());
|
|
619
620
|
await writeFileSafe(path.join(hooksDir, "pre-compact.sh"), preCompactScript());
|
|
620
621
|
await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
|
|
621
622
|
strictMode: config.promptGuardMode === "strict"
|
|
622
623
|
}));
|
|
623
624
|
await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript({
|
|
625
|
+
workflowGuardMode: config.strictness ?? "advisory",
|
|
624
626
|
tddEnforcementMode: config.tddEnforcement ?? "advisory",
|
|
625
627
|
tddTestGlobs: config.tddTestGlobs
|
|
626
628
|
}));
|
|
@@ -631,6 +633,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
631
633
|
for (const script of [
|
|
632
634
|
"session-start.sh",
|
|
633
635
|
"stop-checkpoint.sh",
|
|
636
|
+
"stage-complete.sh",
|
|
634
637
|
"pre-compact.sh",
|
|
635
638
|
"prompt-guard.sh",
|
|
636
639
|
"workflow-guard.sh",
|
|
@@ -1260,7 +1263,12 @@ function stripManagedHookCommands(value) {
|
|
|
1260
1263
|
}
|
|
1261
1264
|
function isManagedRuntimeHookCommand(command) {
|
|
1262
1265
|
const normalized = command.trim().replace(/\s+/gu, " ");
|
|
1263
|
-
|
|
1266
|
+
if (/(^|\s)(?:bash\s+)?(?:\.\/)?\.cclaw\/hooks\/(?:session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor)\.sh(?:\s|$)/u.test(normalized)) {
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
// Codex UserPromptSubmit non-blocking state nudge:
|
|
1270
|
+
// bash -lc '... cclaw internal verify-current-state --quiet ...'
|
|
1271
|
+
return /internal verify-current-state --quiet/u.test(normalized);
|
|
1264
1272
|
}
|
|
1265
1273
|
async function removeManagedHookEntries(hookFilePath) {
|
|
1266
1274
|
if (!(await exists(hookFilePath)))
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { stageSchema } from "../content/stage-schema.js";
|
|
3
|
+
import { appendDelegation, checkMandatoryDelegations } from "../delegation.js";
|
|
4
|
+
import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
|
|
5
|
+
import { isFlowTrack, nextStage } from "../flow-state.js";
|
|
6
|
+
import { readFlowState, writeFlowState } from "../runs.js";
|
|
7
|
+
import { FLOW_STAGES } from "../types.js";
|
|
8
|
+
function unique(values) {
|
|
9
|
+
return [...new Set(values)];
|
|
10
|
+
}
|
|
11
|
+
function parseStringList(raw) {
|
|
12
|
+
if (!Array.isArray(raw))
|
|
13
|
+
return [];
|
|
14
|
+
return raw
|
|
15
|
+
.filter((item) => typeof item === "string")
|
|
16
|
+
.map((item) => item.trim())
|
|
17
|
+
.filter((item) => item.length > 0);
|
|
18
|
+
}
|
|
19
|
+
function isFlowStageValue(value) {
|
|
20
|
+
return typeof value === "string" && FLOW_STAGES.includes(value);
|
|
21
|
+
}
|
|
22
|
+
function parseGuardEvidence(value) {
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
const next = {};
|
|
27
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
28
|
+
if (typeof raw !== "string")
|
|
29
|
+
continue;
|
|
30
|
+
const trimmed = raw.trim();
|
|
31
|
+
if (trimmed.length === 0)
|
|
32
|
+
continue;
|
|
33
|
+
next[key] = trimmed;
|
|
34
|
+
}
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
function parseCandidateGateCatalog(value, fallback) {
|
|
38
|
+
const next = {};
|
|
39
|
+
for (const stage of FLOW_STAGES) {
|
|
40
|
+
const base = fallback[stage];
|
|
41
|
+
next[stage] = {
|
|
42
|
+
required: [...base.required],
|
|
43
|
+
recommended: [...base.recommended],
|
|
44
|
+
conditional: [...base.conditional],
|
|
45
|
+
triggered: [...base.triggered],
|
|
46
|
+
passed: [...base.passed],
|
|
47
|
+
blocked: [...base.blocked]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
const rawCatalog = value;
|
|
54
|
+
for (const stage of FLOW_STAGES) {
|
|
55
|
+
const rawStage = rawCatalog[stage];
|
|
56
|
+
if (!rawStage || typeof rawStage !== "object" || Array.isArray(rawStage)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const typed = rawStage;
|
|
60
|
+
const base = fallback[stage];
|
|
61
|
+
const allowed = new Set([...base.required, ...base.recommended, ...base.conditional]);
|
|
62
|
+
const conditional = new Set(base.conditional);
|
|
63
|
+
const passed = unique(parseStringList(typed.passed)).filter((gateId) => allowed.has(gateId));
|
|
64
|
+
const blocked = unique(parseStringList(typed.blocked)).filter((gateId) => allowed.has(gateId));
|
|
65
|
+
const triggered = unique([
|
|
66
|
+
...parseStringList(typed.triggered).filter((gateId) => conditional.has(gateId)),
|
|
67
|
+
...passed.filter((gateId) => conditional.has(gateId)),
|
|
68
|
+
...blocked.filter((gateId) => conditional.has(gateId))
|
|
69
|
+
]);
|
|
70
|
+
next[stage] = {
|
|
71
|
+
required: [...base.required],
|
|
72
|
+
recommended: [...base.recommended],
|
|
73
|
+
conditional: [...base.conditional],
|
|
74
|
+
triggered,
|
|
75
|
+
passed,
|
|
76
|
+
blocked
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
function coerceCandidateFlowState(raw, fallback) {
|
|
82
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
85
|
+
const typed = raw;
|
|
86
|
+
const track = isFlowTrack(typed.track) ? typed.track : fallback.track;
|
|
87
|
+
const currentStage = isFlowStageValue(typed.currentStage)
|
|
88
|
+
? typed.currentStage
|
|
89
|
+
: fallback.currentStage;
|
|
90
|
+
const completedStages = unique(parseStringList(typed.completedStages).filter((stage) => isFlowStageValue(stage)));
|
|
91
|
+
const skippedStagesRaw = parseStringList(typed.skippedStages).filter((stage) => isFlowStageValue(stage));
|
|
92
|
+
const skippedStages = skippedStagesRaw.length > 0 ? skippedStagesRaw : fallback.skippedStages;
|
|
93
|
+
return {
|
|
94
|
+
...fallback,
|
|
95
|
+
currentStage,
|
|
96
|
+
completedStages,
|
|
97
|
+
track,
|
|
98
|
+
skippedStages,
|
|
99
|
+
guardEvidence: parseGuardEvidence(typed.guardEvidence),
|
|
100
|
+
stageGateCatalog: parseCandidateGateCatalog(typed.stageGateCatalog, fallback.stageGateCatalog)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function parseEvidenceByGate(raw) {
|
|
104
|
+
if (!raw || raw.trim().length === 0) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(raw);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw new Error(`--evidence-json must be valid JSON object: ${err instanceof Error ? err.message : String(err)}`);
|
|
113
|
+
}
|
|
114
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
115
|
+
throw new Error("--evidence-json must deserialize to an object.");
|
|
116
|
+
}
|
|
117
|
+
const next = {};
|
|
118
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
119
|
+
if (typeof value !== "string")
|
|
120
|
+
continue;
|
|
121
|
+
const trimmed = value.trim();
|
|
122
|
+
if (trimmed.length === 0)
|
|
123
|
+
continue;
|
|
124
|
+
next[key] = trimmed;
|
|
125
|
+
}
|
|
126
|
+
return next;
|
|
127
|
+
}
|
|
128
|
+
function parseCsv(raw) {
|
|
129
|
+
if (!raw)
|
|
130
|
+
return [];
|
|
131
|
+
return raw
|
|
132
|
+
.split(",")
|
|
133
|
+
.map((item) => item.trim())
|
|
134
|
+
.filter((item) => item.length > 0);
|
|
135
|
+
}
|
|
136
|
+
function parseAdvanceStageArgs(tokens) {
|
|
137
|
+
const [stageRaw, ...flagTokens] = tokens;
|
|
138
|
+
if (!isFlowStageValue(stageRaw)) {
|
|
139
|
+
throw new Error(`internal advance-stage requires a stage positional argument (${FLOW_STAGES.join(", ")}).`);
|
|
140
|
+
}
|
|
141
|
+
let evidenceJson;
|
|
142
|
+
let passed = [];
|
|
143
|
+
let waiveDelegations = [];
|
|
144
|
+
let waiverReason;
|
|
145
|
+
let quiet = false;
|
|
146
|
+
for (const token of flagTokens) {
|
|
147
|
+
if (token === "--quiet") {
|
|
148
|
+
quiet = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (token.startsWith("--evidence-json=")) {
|
|
152
|
+
evidenceJson = token.replace("--evidence-json=", "");
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (token.startsWith("--passed=")) {
|
|
156
|
+
passed = [...passed, ...parseCsv(token.replace("--passed=", ""))];
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (token.startsWith("--waive-delegation=")) {
|
|
160
|
+
waiveDelegations = [
|
|
161
|
+
...waiveDelegations,
|
|
162
|
+
...parseCsv(token.replace("--waive-delegation=", ""))
|
|
163
|
+
];
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (token.startsWith("--waiver-reason=")) {
|
|
167
|
+
waiverReason = token.replace("--waiver-reason=", "").trim();
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`Unknown flag for internal advance-stage: ${token}`);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
stage: stageRaw,
|
|
174
|
+
passedGateIds: unique(passed),
|
|
175
|
+
evidenceByGate: parseEvidenceByGate(evidenceJson),
|
|
176
|
+
waiveDelegations: unique(waiveDelegations),
|
|
177
|
+
waiverReason,
|
|
178
|
+
quiet
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function parseVerifyFlowStateDiffArgs(tokens) {
|
|
182
|
+
let afterJson;
|
|
183
|
+
let afterFile;
|
|
184
|
+
let quiet = false;
|
|
185
|
+
for (const token of tokens) {
|
|
186
|
+
if (token === "--quiet") {
|
|
187
|
+
quiet = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (token.startsWith("--after-json=")) {
|
|
191
|
+
afterJson = token.replace("--after-json=", "");
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (token.startsWith("--after-file=")) {
|
|
195
|
+
afterFile = token.replace("--after-file=", "");
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`Unknown flag for internal verify-flow-state-diff: ${token}`);
|
|
199
|
+
}
|
|
200
|
+
if (!afterJson && !afterFile) {
|
|
201
|
+
throw new Error("internal verify-flow-state-diff requires --after-json=<json> or --after-file=<path>.");
|
|
202
|
+
}
|
|
203
|
+
return { afterJson, afterFile, quiet };
|
|
204
|
+
}
|
|
205
|
+
function parseVerifyCurrentStateArgs(tokens) {
|
|
206
|
+
let quiet = false;
|
|
207
|
+
for (const token of tokens) {
|
|
208
|
+
if (token === "--quiet") {
|
|
209
|
+
quiet = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`Unknown flag for internal verify-current-state: ${token}`);
|
|
213
|
+
}
|
|
214
|
+
return { quiet };
|
|
215
|
+
}
|
|
216
|
+
async function buildValidationReport(projectRoot, flowState) {
|
|
217
|
+
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
|
|
218
|
+
const gates = await verifyCurrentStageGateEvidence(projectRoot, flowState);
|
|
219
|
+
const completedStages = verifyCompletedStagesGateClosure(flowState);
|
|
220
|
+
const ok = delegation.satisfied && gates.ok && gates.complete && completedStages.ok;
|
|
221
|
+
return {
|
|
222
|
+
ok,
|
|
223
|
+
stage: flowState.currentStage,
|
|
224
|
+
delegation: {
|
|
225
|
+
satisfied: delegation.satisfied,
|
|
226
|
+
missing: delegation.missing,
|
|
227
|
+
waived: delegation.waived,
|
|
228
|
+
missingEvidence: delegation.missingEvidence,
|
|
229
|
+
expectedMode: delegation.expectedMode
|
|
230
|
+
},
|
|
231
|
+
gates: {
|
|
232
|
+
ok: gates.ok,
|
|
233
|
+
complete: gates.complete,
|
|
234
|
+
issues: gates.issues,
|
|
235
|
+
missingRequired: gates.missingRequired,
|
|
236
|
+
missingTriggeredConditional: gates.missingTriggeredConditional
|
|
237
|
+
},
|
|
238
|
+
completedStages: {
|
|
239
|
+
ok: completedStages.ok,
|
|
240
|
+
issues: completedStages.issues
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async function runAdvanceStage(projectRoot, args, io) {
|
|
245
|
+
const flowState = await readFlowState(projectRoot);
|
|
246
|
+
if (flowState.currentStage !== args.stage) {
|
|
247
|
+
io.stderr.write(`cclaw internal advance-stage: current stage is "${flowState.currentStage}", not "${args.stage}".\n`);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
const schema = stageSchema(args.stage);
|
|
251
|
+
const requiredGateIds = schema.requiredGates
|
|
252
|
+
.filter((gate) => gate.tier === "required")
|
|
253
|
+
.map((gate) => gate.id);
|
|
254
|
+
const allowedGateIds = new Set(schema.requiredGates.map((gate) => gate.id));
|
|
255
|
+
const selectedGateIds = args.passedGateIds.length > 0
|
|
256
|
+
? args.passedGateIds.filter((gateId) => allowedGateIds.has(gateId))
|
|
257
|
+
: requiredGateIds;
|
|
258
|
+
const missingRequired = requiredGateIds.filter((gateId) => !selectedGateIds.includes(gateId));
|
|
259
|
+
if (missingRequired.length > 0) {
|
|
260
|
+
io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.\n`);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
const mandatory = new Set(schema.mandatoryDelegations);
|
|
264
|
+
for (const agent of args.waiveDelegations) {
|
|
265
|
+
if (!mandatory.has(agent)) {
|
|
266
|
+
io.stderr.write(`cclaw internal advance-stage: cannot waive "${agent}" for stage "${args.stage}" (not mandatory).\n`);
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (args.waiveDelegations.length > 0) {
|
|
271
|
+
const waiverReason = args.waiverReason && args.waiverReason.length > 0
|
|
272
|
+
? args.waiverReason
|
|
273
|
+
: "manual_waiver";
|
|
274
|
+
for (const agent of args.waiveDelegations) {
|
|
275
|
+
await appendDelegation(projectRoot, {
|
|
276
|
+
stage: args.stage,
|
|
277
|
+
agent,
|
|
278
|
+
mode: "mandatory",
|
|
279
|
+
status: "waived",
|
|
280
|
+
waiverReason,
|
|
281
|
+
fulfillmentMode: "role-switch",
|
|
282
|
+
ts: new Date().toISOString()
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const catalog = flowState.stageGateCatalog[args.stage];
|
|
287
|
+
const nextPassed = unique([...catalog.passed, ...selectedGateIds]).filter((gateId) => allowedGateIds.has(gateId));
|
|
288
|
+
const nextBlocked = unique(catalog.blocked.filter((gateId) => !nextPassed.includes(gateId))).filter((gateId) => allowedGateIds.has(gateId));
|
|
289
|
+
const conditional = new Set(catalog.conditional);
|
|
290
|
+
const nextTriggered = unique([
|
|
291
|
+
...catalog.triggered.filter((gateId) => conditional.has(gateId)),
|
|
292
|
+
...nextPassed.filter((gateId) => conditional.has(gateId)),
|
|
293
|
+
...nextBlocked.filter((gateId) => conditional.has(gateId))
|
|
294
|
+
]);
|
|
295
|
+
const nextGuardEvidence = { ...flowState.guardEvidence };
|
|
296
|
+
for (const gateId of nextPassed) {
|
|
297
|
+
const existing = nextGuardEvidence[gateId];
|
|
298
|
+
if (typeof existing === "string" && existing.trim().length > 0)
|
|
299
|
+
continue;
|
|
300
|
+
const provided = args.evidenceByGate[gateId];
|
|
301
|
+
nextGuardEvidence[gateId] = provided && provided.trim().length > 0
|
|
302
|
+
? provided.trim()
|
|
303
|
+
: `stage-complete helper auto-evidence for ${gateId} @ ${new Date().toISOString()} (${schema.artifactFile})`;
|
|
304
|
+
}
|
|
305
|
+
const nextStageCatalog = {
|
|
306
|
+
required: [...catalog.required],
|
|
307
|
+
recommended: [...catalog.recommended],
|
|
308
|
+
conditional: [...catalog.conditional],
|
|
309
|
+
triggered: nextTriggered,
|
|
310
|
+
passed: nextPassed,
|
|
311
|
+
blocked: nextBlocked
|
|
312
|
+
};
|
|
313
|
+
const candidateState = {
|
|
314
|
+
...flowState,
|
|
315
|
+
guardEvidence: nextGuardEvidence,
|
|
316
|
+
stageGateCatalog: {
|
|
317
|
+
...flowState.stageGateCatalog,
|
|
318
|
+
[args.stage]: nextStageCatalog
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const validation = await buildValidationReport(projectRoot, candidateState);
|
|
322
|
+
if (!validation.ok) {
|
|
323
|
+
io.stderr.write(`cclaw internal advance-stage: validation failed for stage "${args.stage}".\n`);
|
|
324
|
+
if (validation.delegation.missing.length > 0) {
|
|
325
|
+
io.stderr.write(`- missing delegations: ${validation.delegation.missing.join(", ")}\n`);
|
|
326
|
+
}
|
|
327
|
+
if (validation.delegation.missingEvidence.length > 0) {
|
|
328
|
+
io.stderr.write(`- role-switch evidence missing: ${validation.delegation.missingEvidence.join(", ")}\n`);
|
|
329
|
+
}
|
|
330
|
+
if (validation.gates.issues.length > 0) {
|
|
331
|
+
io.stderr.write(`- gate issues: ${validation.gates.issues.join(" | ")}\n`);
|
|
332
|
+
}
|
|
333
|
+
if (validation.completedStages.issues.length > 0) {
|
|
334
|
+
io.stderr.write(`- completed-stage closure issues: ${validation.completedStages.issues.join(" | ")}\n`);
|
|
335
|
+
}
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
const successor = nextStage(args.stage, flowState.track);
|
|
339
|
+
const completedStages = flowState.completedStages.includes(args.stage)
|
|
340
|
+
? [...flowState.completedStages]
|
|
341
|
+
: [...flowState.completedStages, args.stage];
|
|
342
|
+
const finalState = {
|
|
343
|
+
...candidateState,
|
|
344
|
+
completedStages,
|
|
345
|
+
currentStage: successor ?? args.stage
|
|
346
|
+
};
|
|
347
|
+
await writeFlowState(projectRoot, finalState);
|
|
348
|
+
if (!args.quiet) {
|
|
349
|
+
io.stdout.write(`${JSON.stringify({
|
|
350
|
+
ok: true,
|
|
351
|
+
command: "advance-stage",
|
|
352
|
+
stage: args.stage,
|
|
353
|
+
nextStage: successor,
|
|
354
|
+
currentStage: finalState.currentStage,
|
|
355
|
+
completedStages: finalState.completedStages
|
|
356
|
+
}, null, 2)}\n`);
|
|
357
|
+
}
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
async function runVerifyFlowStateDiff(projectRoot, args, io) {
|
|
361
|
+
let raw = args.afterJson;
|
|
362
|
+
if (!raw && args.afterFile) {
|
|
363
|
+
raw = await fs.readFile(args.afterFile, "utf8");
|
|
364
|
+
}
|
|
365
|
+
if (!raw) {
|
|
366
|
+
io.stderr.write("cclaw internal verify-flow-state-diff: no candidate state payload.\n");
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
let parsed;
|
|
370
|
+
try {
|
|
371
|
+
parsed = JSON.parse(raw);
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
io.stderr.write(`cclaw internal verify-flow-state-diff: invalid JSON payload (${err instanceof Error ? err.message : String(err)}).\n`);
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
const current = await readFlowState(projectRoot);
|
|
378
|
+
const candidate = coerceCandidateFlowState(parsed, current);
|
|
379
|
+
const validation = await buildValidationReport(projectRoot, candidate);
|
|
380
|
+
if (!args.quiet) {
|
|
381
|
+
io.stdout.write(`${JSON.stringify(validation, null, 2)}\n`);
|
|
382
|
+
}
|
|
383
|
+
if (!validation.ok) {
|
|
384
|
+
io.stderr.write(`cclaw internal verify-flow-state-diff: candidate state is invalid for stage "${validation.stage}".\n`);
|
|
385
|
+
}
|
|
386
|
+
return validation.ok ? 0 : 1;
|
|
387
|
+
}
|
|
388
|
+
async function runVerifyCurrentState(projectRoot, args, io) {
|
|
389
|
+
const current = await readFlowState(projectRoot);
|
|
390
|
+
const validation = await buildValidationReport(projectRoot, current);
|
|
391
|
+
if (!args.quiet) {
|
|
392
|
+
io.stdout.write(`${JSON.stringify(validation, null, 2)}\n`);
|
|
393
|
+
}
|
|
394
|
+
if (!validation.ok) {
|
|
395
|
+
const unmetDelegations = validation.delegation.missing.length + validation.delegation.missingEvidence.length;
|
|
396
|
+
const gatesWithoutEvidence = validation.gates.issues.filter((issue) => issue.includes("missing guardEvidence entry")).length;
|
|
397
|
+
io.stderr.write(`cclaw: current stage has ${unmetDelegations} unmet mandatory delegations and ${gatesWithoutEvidence} gates without evidence.\n`);
|
|
398
|
+
io.stderr.write(`cclaw internal verify-current-state: unresolved stage constraints for "${validation.stage}".\n`);
|
|
399
|
+
}
|
|
400
|
+
return validation.ok ? 0 : 1;
|
|
401
|
+
}
|
|
402
|
+
export async function runInternalCommand(projectRoot, argv, io) {
|
|
403
|
+
const [subcommand, ...tokens] = argv;
|
|
404
|
+
if (!subcommand) {
|
|
405
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state\n");
|
|
406
|
+
return 1;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
if (subcommand === "advance-stage") {
|
|
410
|
+
return await runAdvanceStage(projectRoot, parseAdvanceStageArgs(tokens), io);
|
|
411
|
+
}
|
|
412
|
+
if (subcommand === "verify-flow-state-diff") {
|
|
413
|
+
return await runVerifyFlowStateDiff(projectRoot, parseVerifyFlowStateDiffArgs(tokens), io);
|
|
414
|
+
}
|
|
415
|
+
if (subcommand === "verify-current-state") {
|
|
416
|
+
return await runVerifyCurrentState(projectRoot, parseVerifyCurrentStateArgs(tokens), io);
|
|
417
|
+
}
|
|
418
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state\n`);
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
io.stderr.write(`cclaw internal ${subcommand} failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
423
|
+
return 1;
|
|
424
|
+
}
|
|
425
|
+
}
|