cclaw-cli 0.5.2 → 0.5.4

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 CHANGED
@@ -119,7 +119,7 @@ Required repository secret:
119
119
  │ └── <activeRunId>/
120
120
  │ ├── artifacts/ # canonical run artifacts
121
121
  │ ├── run.json
122
- │ └── 00-handoff.md
122
+ │ └── handoff.md
123
123
  └── learnings.jsonl
124
124
  ```
125
125
 
@@ -85,6 +85,24 @@ function countListItems(sectionBody) {
85
85
  const tableDataRows = tableRows.length > 0 ? Math.max(0, tableRows.length - 1) : 0;
86
86
  return Math.max(bullets, tableDataRows);
87
87
  }
88
+ function parseMarkdownTableRow(line) {
89
+ return line
90
+ .trim()
91
+ .split("|")
92
+ .map((cell) => cell.trim())
93
+ .filter((cell) => cell.length > 0);
94
+ }
95
+ function tableHeaderCells(sectionBody) {
96
+ const lines = sectionBody.split(/\r?\n/).map((line) => line.trim());
97
+ const headerIndex = lines.findIndex((line) => /^\|.*\|$/u.test(line));
98
+ if (headerIndex < 0)
99
+ return null;
100
+ const separator = lines[headerIndex + 1];
101
+ if (!separator || !/^\|[-:| ]+\|$/u.test(separator)) {
102
+ return null;
103
+ }
104
+ return parseMarkdownTableRow(lines[headerIndex]);
105
+ }
88
106
  function extractMinItemsFromRule(rule) {
89
107
  const match = /at least\s+(\d+)/iu.exec(rule);
90
108
  if (!match)
@@ -129,6 +147,26 @@ function validateSectionBody(sectionBody, rule) {
129
147
  };
130
148
  }
131
149
  }
150
+ if (/table must use 4 columns/iu.test(rule)) {
151
+ const header = tableHeaderCells(sectionBody);
152
+ if (!header) {
153
+ return {
154
+ ok: false,
155
+ details: "Rule expects a markdown table header with a separator row."
156
+ };
157
+ }
158
+ const expected = ["Category", "Question asked", "User answer", "Evidence note"];
159
+ const normalizedHeader = header.map((cell) => cell.toLowerCase());
160
+ const normalizedExpected = expected.map((cell) => cell.toLowerCase());
161
+ const matches = normalizedHeader.length === normalizedExpected.length &&
162
+ normalizedHeader.every((cell, index) => cell === normalizedExpected[index]);
163
+ if (!matches) {
164
+ return {
165
+ ok: false,
166
+ details: `Rule expects Clarification Log header: ${expected.join(" | ")}.`
167
+ };
168
+ }
169
+ }
132
170
  if (/exactly one/iu.test(rule)) {
133
171
  const tokens = tokensFromRule(rule);
134
172
  if (tokens.length > 0) {
@@ -26,7 +26,7 @@ ${schema.hardGate}
26
26
 
27
27
  ## In / Out
28
28
  - **Reads:** ${readsLine}
29
- - **Writes:** \`.cclaw/artifacts/${schema.artifactFile}\` (canonical run copy: \`.cclaw/runs/<activeRunId>/artifacts/${schema.artifactFile}\`)
29
+ - **Writes:** \`.cclaw/artifacts/${schema.artifactFile}\` (run snapshot under \`.cclaw/runs/<activeRunId>/artifacts/${schema.artifactFile}\` is synchronized by cclaw runtime)
30
30
  - **Next:** \`/cc-next\` (updates flow-state and loads the next stage)
31
31
 
32
32
  ## Context Hydration (mandatory before stage work)
@@ -35,7 +35,7 @@ ${schema.hardGate}
35
35
  3. Load required upstream artifacts for this stage:
36
36
  ${hydrationLines}
37
37
  4. If a canonical run artifact is missing, fallback to the matching file under \`.cclaw/artifacts/\` and record that fallback in the stage artifact.
38
- 5. Write stage output to \`.cclaw/artifacts/${schema.artifactFile}\` and keep canonical run copy aligned at \`.cclaw/runs/<activeRunId>/artifacts/${schema.artifactFile}\`.
38
+ 5. Write stage output to \`.cclaw/artifacts/${schema.artifactFile}\`. Do NOT manually copy into run directories; cclaw sync/runtime keeps run snapshots aligned.
39
39
 
40
40
  ## Gates
41
41
  ${gateIds}
@@ -1,5 +1,38 @@
1
1
  const STAGE_EXAMPLES = {
2
- brainstorm: `### Alternatives comparison
2
+ brainstorm: `### Clarification sequence (Socratic)
3
+
4
+ **Q1 (PURPOSE):** "What decision will this demo help your audience make, and who exactly is that audience?"
5
+ **A1:** "Internal platform team; they should see whether our release flow can be trusted."
6
+
7
+ **Q2 (SCOPE):** "Which outcomes are explicitly OUT of scope for this demo so we avoid accidental expansion?"
8
+ **A2:** "No production rollout automation; only release metadata validation + publish safety checks."
9
+
10
+ **Q3 (BOUNDARIES):** "When release metadata is missing or invalid, what should happen and what should NEVER happen?"
11
+ **A3:** "Must block release with a clear reason; must never auto-publish anyway."
12
+
13
+ **Q4 (ENVIRONMENT):** "Where will this run in practice: local only, CI only, or both? How is it installed and invoked?"
14
+ **A4:** "CI-first in GitHub Actions, but reproducible locally with npm scripts."
15
+
16
+ **Q5 (CONSTRAINTS):** "What constraints are non-negotiable (runtime deps, latency, compatibility, policy)?"
17
+ **A5:** "No new runtime dependencies; checks should stay under 2 minutes; compatible with current workflow."
18
+
19
+ ### One-question-per-message discipline
20
+
21
+ **Bad (bundled):** "Where will this run? Which Python version? Stdlib only? Any perf limits?"
22
+
23
+ **Good (single ask):** "Where will this run in practice: local only, CI only, or both?"
24
+
25
+ **Then next turn:** "Which runtime version should we lock as minimum?"
26
+
27
+ **Then next turn:** "Should we treat 'stdlib-only' as a hard dependency constraint?"
28
+
29
+ ### Premise challenge and boundary stress-test
30
+
31
+ **Premise challenge:** "If the goal is only to demonstrate filesystem I/O, why is a CLI better than a tiny script or notebook for this demo?"
32
+
33
+ **Boundary stress-test:** "When a write fails midway (permissions or partial path issues), what outcome should NEVER happen?"
34
+
35
+ ### Alternatives comparison
3
36
 
4
37
  | Approach | Pros | Cons | Effort | Recommendation |
5
38
  | --- | --- | --- | --- | --- |
@@ -678,9 +678,98 @@ if [ "$CHECKPOINT_WRITTEN" -eq 0 ]; then
678
678
  CHECKPOINT_NOTE="Checkpoint update failed. Review ${RUNTIME_ROOT}/state/checkpoint.json manually."
679
679
  fi
680
680
 
681
+ RUN_SYNC_NOTE="Run metadata sync skipped."
682
+ if [ -n "$ACTIVE_RUN" ] && [ "$ACTIVE_RUN" != "none" ] && [ "$ACTIVE_RUN" != "run-pending" ]; then
683
+ RUN_DIR="$ROOT/${RUNTIME_ROOT}/runs/$ACTIVE_RUN"
684
+ RUN_META_FILE="$RUN_DIR/run.json"
685
+ RUN_HANDOFF_FILE="$RUN_DIR/handoff.md"
686
+ if [ -f "$RUN_META_FILE" ] && [ -f "$STATE_FILE" ] && command -v python3 >/dev/null 2>&1; then
687
+ if python3 - "$STATE_FILE" "$RUN_META_FILE" "$RUN_HANDOFF_FILE" "$ACTIVE_RUN" "$TS" <<'PY'
688
+ import json
689
+ import sys
690
+ from pathlib import Path
691
+
692
+ state_path, run_meta_path, handoff_path, active_run, timestamp = sys.argv[1:6]
693
+
694
+ try:
695
+ state = json.loads(Path(state_path).read_text(encoding="utf-8"))
696
+ except Exception:
697
+ raise SystemExit(1)
698
+
699
+ try:
700
+ meta = json.loads(Path(run_meta_path).read_text(encoding="utf-8"))
701
+ except Exception:
702
+ raise SystemExit(1)
703
+
704
+ if not isinstance(state, dict) or not isinstance(meta, dict):
705
+ raise SystemExit(1)
706
+
707
+ completed = state.get("completedStages")
708
+ if not isinstance(completed, list):
709
+ completed = []
710
+ completed = [stage for stage in completed if isinstance(stage, str)]
711
+
712
+ guard_evidence = state.get("guardEvidence")
713
+ if not isinstance(guard_evidence, dict):
714
+ guard_evidence = {}
715
+ guard_evidence = {k: v for k, v in guard_evidence.items() if isinstance(k, str) and isinstance(v, str)}
716
+
717
+ stage_gate_catalog = state.get("stageGateCatalog")
718
+ if not isinstance(stage_gate_catalog, dict):
719
+ stage_gate_catalog = {}
720
+
721
+ snapshot = {
722
+ "currentStage": state.get("currentStage") if isinstance(state.get("currentStage"), str) else "brainstorm",
723
+ "completedStages": completed,
724
+ "guardEvidence": guard_evidence,
725
+ "stageGateCatalog": stage_gate_catalog,
726
+ }
727
+ meta["stateSnapshot"] = snapshot
728
+ Path(run_meta_path).write_text(json.dumps(meta, indent=2) + "\\n", encoding="utf-8")
729
+
730
+ title = meta.get("title") if isinstance(meta.get("title"), str) and meta.get("title") else active_run
731
+ created_at = meta.get("createdAt") if isinstance(meta.get("createdAt"), str) and meta.get("createdAt") else "unknown"
732
+ archived_at = meta.get("archivedAt") if isinstance(meta.get("archivedAt"), str) and meta.get("archivedAt") else None
733
+ completed_display = ", ".join(completed) if completed else "(none)"
734
+
735
+ handoff_lines = [
736
+ "# Run Handoff",
737
+ "",
738
+ f"- ID: {active_run}",
739
+ f"- Title: {title}",
740
+ f"- Created: {created_at}",
741
+ f"- Archived: {archived_at if archived_at else 'active'}",
742
+ "",
743
+ "## Flow Snapshot",
744
+ f"- Active stage: {snapshot['currentStage']}",
745
+ f"- Completed stages: {completed_display}",
746
+ f"- Active run ID in flow-state: {state.get('activeRunId') if isinstance(state.get('activeRunId'), str) else active_run}",
747
+ "",
748
+ "## Paths",
749
+ f"- Active artifacts: ${RUNTIME_ROOT}/artifacts/",
750
+ f"- Run artifacts snapshot: ${RUNTIME_ROOT}/runs/{active_run}/artifacts/",
751
+ f"- Flow state: ${RUNTIME_ROOT}/state/flow-state.json",
752
+ "",
753
+ "## Resume",
754
+ "1. Open ${RUNTIME_ROOT}/state/flow-state.json and verify current stage.",
755
+ "2. Review ${RUNTIME_ROOT}/artifacts/ for the latest working artifacts.",
756
+ "3. Continue using /cc or /cc-next from the current stage.",
757
+ "",
758
+ f"- Last updated: {timestamp}",
759
+ ]
760
+ Path(handoff_path).write_text("\\n".join(handoff_lines) + "\\n", encoding="utf-8")
761
+ PY
762
+ then
763
+ RUN_SYNC_NOTE="Run metadata synchronized for $ACTIVE_RUN."
764
+ else
765
+ RUN_SYNC_NOTE="Run metadata sync failed for $ACTIVE_RUN."
766
+ fi
767
+ fi
768
+ fi
769
+
681
770
  # --- Escape for JSON ---
682
771
  ${ESCAPE_FN}
683
- MSG=$(escape_json "Cclaw: session ending (stage=$STAGE, run=$ACTIVE_RUN). $CHECKPOINT_NOTE Before stopping: (1) confirm flow-state reflects reality, (2) ensure artifact changes match active run intent, (3) log reusable learnings, (4) commit or revert pending changes.")
772
+ MSG=$(escape_json "Cclaw: session ending (stage=$STAGE, run=$ACTIVE_RUN). $CHECKPOINT_NOTE $RUN_SYNC_NOTE Before stopping: (1) confirm flow-state reflects reality, (2) ensure artifact changes match active run intent, (3) log reusable learnings, (4) commit or revert pending changes.")
684
773
 
685
774
  # --- Output harness-specific JSON ---
686
775
  case "$HARNESS" in
@@ -1078,6 +1167,14 @@ export default function cclawPlugin(ctx) {
1078
1167
  payload?.command,
1079
1168
  payload?.tool?.name,
1080
1169
  payload?.tool?.id,
1170
+ payload?.input?.tool,
1171
+ payload?.input?.toolName,
1172
+ payload?.input?.tool_name,
1173
+ payload?.input?.name,
1174
+ payload?.input?.id,
1175
+ payload?.input?.command,
1176
+ payload?.input?.tool?.name,
1177
+ payload?.input?.tool?.id
1081
1178
  ];
1082
1179
  for (const value of candidates) {
1083
1180
  if (typeof value === "string" && value.trim()) return value.trim();
@@ -1085,6 +1182,36 @@ export default function cclawPlugin(ctx) {
1085
1182
  return "unknown";
1086
1183
  }
1087
1184
 
1185
+ function normalizeToolPayload(input, output) {
1186
+ if (typeof output === "undefined") {
1187
+ return input ?? {};
1188
+ }
1189
+ return {
1190
+ input: input ?? {},
1191
+ output: output ?? {}
1192
+ };
1193
+ }
1194
+
1195
+ function resolveEventType(payload) {
1196
+ if (typeof payload === "string") return payload;
1197
+ if (payload && typeof payload === "object") {
1198
+ if (typeof payload.type === "string") return payload.type;
1199
+ if (typeof payload.name === "string") return payload.name;
1200
+ if (payload.event && typeof payload.event === "object") {
1201
+ if (typeof payload.event.type === "string") return payload.event.type;
1202
+ if (typeof payload.event.name === "string") return payload.event.name;
1203
+ }
1204
+ }
1205
+ return "";
1206
+ }
1207
+
1208
+ function resolveEventData(payload) {
1209
+ if (payload && typeof payload === "object" && payload.event && typeof payload.event === "object") {
1210
+ return payload.event;
1211
+ }
1212
+ return payload;
1213
+ }
1214
+
1088
1215
  function appendJsonLine(filePath, value) {
1089
1216
  try {
1090
1217
  appendFileSync(filePath, JSON.stringify(value) + "\\n", "utf8");
@@ -1120,28 +1247,43 @@ export default function cclawPlugin(ctx) {
1120
1247
  }
1121
1248
 
1122
1249
  return {
1123
- event: async (name, data) => {
1124
- if (name === "session.created" || name === "session.resumed" || name === "session.compacted" || name === "session.cleared") {
1250
+ event: async (payload) => {
1251
+ const eventType = resolveEventType(payload);
1252
+ const eventData = resolveEventData(payload);
1253
+ if (eventType === "session.created" || eventType === "session.resumed" || eventType === "session.compacted" || eventType === "session.cleared") {
1125
1254
  emitBootstrap();
1126
1255
  }
1127
- if (name === "session.updated") {
1256
+ if (eventType === "session.updated") {
1128
1257
  // no-op: tracked via activity log
1129
1258
  }
1130
- if (name === "session.idle") {
1259
+ if (eventType === "session.idle") {
1131
1260
  if (!observationEnabled()) return;
1132
1261
  await runHookScript("summarize-observations.sh");
1133
1262
  await runHookScript("stop-checkpoint.sh", { loop_count: 0 });
1134
1263
  }
1135
- if (name === "tool.execute.before") {
1136
- await runHookScript("prompt-guard.sh", data ?? {});
1137
- await runHookScript("workflow-guard.sh", data ?? {});
1138
- recordToolEvent("pre", data);
1264
+ if (eventType === "tool.execute.before") {
1265
+ const toolPayload = normalizeToolPayload(eventData, undefined);
1266
+ await runHookScript("prompt-guard.sh", toolPayload);
1267
+ await runHookScript("workflow-guard.sh", toolPayload);
1268
+ recordToolEvent("pre", toolPayload);
1139
1269
  }
1140
- if (name === "tool.execute.after") {
1141
- await runHookScript("context-monitor.sh", data ?? {});
1142
- recordToolEvent("post", data);
1270
+ if (eventType === "tool.execute.after") {
1271
+ const toolPayload = normalizeToolPayload(eventData, undefined);
1272
+ await runHookScript("context-monitor.sh", toolPayload);
1273
+ recordToolEvent("post", toolPayload);
1143
1274
  }
1144
1275
  },
1276
+ "tool.execute.before": async (input, output) => {
1277
+ const payload = normalizeToolPayload(input, output);
1278
+ await runHookScript("prompt-guard.sh", payload);
1279
+ await runHookScript("workflow-guard.sh", payload);
1280
+ recordToolEvent("pre", payload);
1281
+ },
1282
+ "tool.execute.after": async (input, output) => {
1283
+ const payload = normalizeToolPayload(input, output);
1284
+ await runHookScript("context-monitor.sh", payload);
1285
+ recordToolEvent("post", payload);
1286
+ },
1145
1287
  "experimental.chat.system.transform": (payload) => {
1146
1288
  const bootstrap = buildBootstrap();
1147
1289
  if (typeof payload === "string") {
@@ -47,7 +47,7 @@ Before starting work, ALWAYS:
47
47
  2. **Stages are workflows, not suggestions.** Follow the skill steps in order. Do not skip verification steps.
48
48
  3. **One stage at a time.** Complete the current stage before advancing to the next.
49
49
  4. **Gates must pass.** Every stage has required gates — the agent cannot claim completion without satisfying them.
50
- 5. **Artifacts are mandatory.** Each stage writes to \`.cclaw/artifacts/\` and keeps the active run copy in \`.cclaw/runs/<activeRunId>/artifacts/\` this is the evidence trail.
50
+ 5. **Artifacts are mandatory.** Each stage writes to \`.cclaw/artifacts/\`; cclaw sync/runtime keeps run snapshots in \`.cclaw/runs/<activeRunId>/artifacts/\` aligned as the evidence trail.
51
51
  6. **When in doubt, use \`/cc\`.** If the task is non-trivial and there's no prior artifact, run \`/cc <idea>\` to start brainstorming.
52
52
 
53
53
  ## Stage Quick Reference
@@ -39,7 +39,7 @@ INPUT=$(cat 2>/dev/null || echo '{}')
39
39
  TOOL="unknown"
40
40
  PAYLOAD=""
41
41
  if command -v jq >/dev/null 2>&1; then
42
- TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // .tool // "unknown"' 2>/dev/null || echo "unknown")
42
+ TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // .tool // .toolName // .name // .id // .command // .tool.name // .tool.id // .input.tool_name // .input.tool // .input.toolName // .input.name // .input.id // .input.command // .input.tool.name // .input.tool.id // "unknown"' 2>/dev/null || echo "unknown")
43
43
  PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
44
44
  elif command -v python3 >/dev/null 2>&1; then
45
45
  TOOL=$(INPUT_JSON="$INPUT" python3 - <<'PY'
@@ -50,8 +50,40 @@ try:
50
50
  value = json.loads(os.environ.get("INPUT_JSON", "{}"))
51
51
  except Exception:
52
52
  value = {}
53
- tool = value.get("tool_name") or value.get("tool") or "unknown"
54
- print(tool if isinstance(tool, str) else "unknown")
53
+
54
+ def pick_tool(payload):
55
+ if not isinstance(payload, dict):
56
+ return "unknown"
57
+ candidates = [
58
+ payload.get("tool_name"),
59
+ payload.get("tool"),
60
+ payload.get("toolName"),
61
+ payload.get("name"),
62
+ payload.get("id"),
63
+ payload.get("command")
64
+ ]
65
+ top_tool = payload.get("tool")
66
+ if isinstance(top_tool, dict):
67
+ candidates.extend([top_tool.get("name"), top_tool.get("id")])
68
+ nested = payload.get("input")
69
+ if isinstance(nested, dict):
70
+ candidates.extend([
71
+ nested.get("tool_name"),
72
+ nested.get("tool"),
73
+ nested.get("toolName"),
74
+ nested.get("name"),
75
+ nested.get("id"),
76
+ nested.get("command")
77
+ ])
78
+ nested_tool = nested.get("tool")
79
+ if isinstance(nested_tool, dict):
80
+ candidates.extend([nested_tool.get("name"), nested_tool.get("id")])
81
+ for candidate in candidates:
82
+ if isinstance(candidate, str) and candidate.strip():
83
+ return candidate.strip()
84
+ return "unknown"
85
+
86
+ print(pick_tool(value))
55
87
  PY
56
88
  )
57
89
  PAYLOAD=$(printf '%s' "$INPUT")
@@ -146,7 +178,7 @@ INPUT=$(cat 2>/dev/null || echo '{}')
146
178
  TOOL="unknown"
147
179
  PAYLOAD=""
148
180
  if command -v jq >/dev/null 2>&1; then
149
- TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // .tool // "unknown"' 2>/dev/null || echo "unknown")
181
+ TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // .tool // .toolName // .name // .id // .command // .tool.name // .tool.id // .input.tool_name // .input.tool // .input.toolName // .input.name // .input.id // .input.command // .input.tool.name // .input.tool.id // "unknown"' 2>/dev/null || echo "unknown")
150
182
  PAYLOAD=$(printf '%s' "$INPUT" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
151
183
  elif command -v python3 >/dev/null 2>&1; then
152
184
  TOOL=$(INPUT_JSON="$INPUT" python3 - <<'PY'
@@ -156,8 +188,40 @@ try:
156
188
  value = json.loads(os.environ.get("INPUT_JSON", "{}"))
157
189
  except Exception:
158
190
  value = {}
159
- tool = value.get("tool_name") or value.get("tool") or "unknown"
160
- print(tool if isinstance(tool, str) else "unknown")
191
+
192
+ def pick_tool(payload):
193
+ if not isinstance(payload, dict):
194
+ return "unknown"
195
+ candidates = [
196
+ payload.get("tool_name"),
197
+ payload.get("tool"),
198
+ payload.get("toolName"),
199
+ payload.get("name"),
200
+ payload.get("id"),
201
+ payload.get("command")
202
+ ]
203
+ top_tool = payload.get("tool")
204
+ if isinstance(top_tool, dict):
205
+ candidates.extend([top_tool.get("name"), top_tool.get("id")])
206
+ nested = payload.get("input")
207
+ if isinstance(nested, dict):
208
+ candidates.extend([
209
+ nested.get("tool_name"),
210
+ nested.get("tool"),
211
+ nested.get("toolName"),
212
+ nested.get("name"),
213
+ nested.get("id"),
214
+ nested.get("command")
215
+ ])
216
+ nested_tool = nested.get("tool")
217
+ if isinstance(nested_tool, dict):
218
+ candidates.extend([nested_tool.get("name"), nested_tool.get("id")])
219
+ for candidate in candidates:
220
+ if isinstance(candidate, str) and candidate.strip():
221
+ return candidate.strip()
222
+ return "unknown"
223
+
224
+ print(pick_tool(value))
161
225
  PY
162
226
  )
163
227
  PAYLOAD=$(printf '%s' "$INPUT")
@@ -527,7 +591,7 @@ INPUT=$(cat 2>/dev/null || echo '{}')
527
591
  TOOL="unknown"
528
592
  PAYLOAD=""
529
593
  if command -v jq >/dev/null 2>&1; then
530
- TOOL=$(echo "$INPUT" | jq -r '.tool_name // .tool // "unknown"' 2>/dev/null || echo "unknown")
594
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // .tool // .toolName // .name // .id // .command // .tool.name // .tool.id // .input.tool_name // .input.tool // .input.toolName // .input.name // .input.id // .input.command // .input.tool.name // .input.tool.id // "unknown"' 2>/dev/null || echo "unknown")
531
595
  if [ "$PHASE" = "pre" ]; then
532
596
  PAYLOAD=$(echo "$INPUT" | jq -r --arg max "$MAX_LEN" '.tool_input // .input // {} | tostring | .[0:($max|tonumber)]' 2>/dev/null || echo "")
533
597
  else
@@ -174,7 +174,7 @@ When all required gates are satisfied and the artifact is written:
174
174
  1. **Update \`${RUNTIME_ROOT}/state/flow-state.json\`:**
175
175
  ${stateUpdate}
176
176
  - For each passed gate, add an entry to \`guardEvidence\`: \`"<gate_id>": "<artifact path or excerpt proving the gate>"\`. Do NOT leave \`guardEvidence\` empty.
177
- 2. **Sync artifact** to \`${RUNTIME_ROOT}/runs/<activeRunId>/artifacts/${schema.artifactFile}\`
177
+ 2. **Persist artifact** at \`${RUNTIME_ROOT}/artifacts/${schema.artifactFile}\`. Do NOT manually copy into run directories; cclaw sync/runtime keeps \`${RUNTIME_ROOT}/runs/<activeRunId>/artifacts/\` aligned.
178
178
  ${nextAction}
179
179
 
180
180
  **STOP.** Do not load the next stage skill yourself. The user will run \`/cc-next\` when ready (same session or new session).
@@ -287,7 +287,7 @@ function quickStartBlock(stage) {
287
287
 
288
288
  > **Even if you read nothing else, do these 3 things:**
289
289
  > 1. Obey the HARD-GATE below — violating it invalidates the entire stage.
290
- > 2. Complete every checklist step in order and write the artifact to \`.cclaw/artifacts/${schema.artifactFile}\` (canonical run copy: \`.cclaw/runs/<activeRunId>/artifacts/${schema.artifactFile}\`).
290
+ > 2. Complete every checklist step in order and write the artifact to \`.cclaw/artifacts/${schema.artifactFile}\`. Run snapshots in \`.cclaw/runs/<activeRunId>/artifacts/\` are synchronized by cclaw runtime.
291
291
  > 3. Do not claim completion without satisfying gates: ${topGates}${schema.requiredGates.length > 3 ? ` (+${schema.requiredGates.length - 3} more)` : ""}.
292
292
  >
293
293
  > **After this stage:** update \`flow-state.json\` and tell the user to run \`/cc-next\`.
@@ -385,7 +385,7 @@ ${progressiveDisclosureBlock(stage)}
385
385
  ${selfImprovementBlock(stage)}
386
386
  ## Handoff
387
387
  - Next command: \`/cc-next\` (loads whatever stage is current in flow-state)
388
- - Required artifact: \`.cclaw/artifacts/${schema.artifactFile}\` (canonical: \`.cclaw/runs/<activeRunId>/artifacts/${schema.artifactFile}\`)
388
+ - Required artifact: \`.cclaw/artifacts/${schema.artifactFile}\` (run snapshot at \`.cclaw/runs/<activeRunId>/artifacts/${schema.artifactFile}\`, synchronized by cclaw runtime)
389
389
  - Stage stays blocked if any required gate is unsatisfied
390
390
  `;
391
391
  }
@@ -38,8 +38,9 @@ const BRAINSTORM = {
38
38
  checklist: [
39
39
  "Explore project context — check files, docs, recent commits, existing behavior. Summarize what you found (even for seemingly simple projects).",
40
40
  "Assess scope — if the request describes multiple independent subsystems, flag for decomposition before detailed questions.",
41
- "Restate the problem — in a SEPARATE message (no questions in this message), summarize what you understood the user wants and why. **STOP and wait** for user to confirm or correct before asking any clarifying questions.",
42
- "Ask clarifying questions — one at a time, one per message. You MUST cover these categories before proposing approaches: (a) PURPOSE — why this project exists, who it serves; (b) SCOPE — what it must do, what it must NOT do; (c) BOUNDARIES — error handling, edge cases, failure modes; (d) ENVIRONMENT — how it runs, deploys, installs; (e) CONSTRAINTS — performance, compatibility, dependencies. Skip a category only if the user already provided that info. Do NOT rush — 3 generic questions are never enough for a non-trivial project.",
41
+ "Restate the problem — in a SEPARATE message (no questions in this message), summarize what you understood the user wants and why. The restatement message must contain zero clarifying questions and no option prompts. **STOP and wait** for user to confirm or correct before asking any clarifying questions.",
42
+ "Ask clarifying questions — one at a time, one per message. Each clarification message must ask exactly one decision-seeking question (no bundled sub-questions). You MUST cover these categories before proposing approaches: (a) PURPOSE — why this project exists, who it serves; (b) SCOPE — what it must do, what it must NOT do; (c) BOUNDARIES — error handling, edge cases, failure modes; (d) ENVIRONMENT — how it runs, deploys, installs; (e) CONSTRAINTS — performance, compatibility, dependencies. Skip a category only if the user already provided that info. If user answers 'I don't know' / 'не знаю', convert it into explicit assumptions and ask for confirmation before moving on. Do NOT rush — 3 generic questions are never enough for a non-trivial project.",
43
+ "Before proposing approaches, perform at least one premise-challenge question (\"Is this the right problem framing?\") and at least one boundary stress-test question (\"What should never happen even on failure?\").",
43
44
  "Propose 2-3 approaches — with real trade-offs (not cosmetic differences) and your explicit recommendation with reasoning. Explain WHY you recommend this option over others.",
44
45
  "Present design — in sections. After each section, explicitly state what you are asking the user to approve: 'Do you approve [specific thing]?' Never ask a bare 'одобряете?/approve?' without context.",
45
46
  "Write design doc — save to `.cclaw/artifacts/01-brainstorm.md`.",
@@ -49,8 +50,11 @@ const BRAINSTORM = {
49
50
  ],
50
51
  interactionProtocol: [
51
52
  "Explore context first (files, docs, existing behavior). Share a brief summary of what you found.",
52
- "Restate the problem in your own words in a SEPARATE message. Do NOT add any questions to this message. Wait for user to confirm or correct.",
53
+ "Restate the problem in your own words in a SEPARATE message. Do NOT add any questions, options, or approval asks to this message. Wait for user to confirm or correct.",
53
54
  "Ask clarifying questions — one per message. Cover mandatory categories: PURPOSE, SCOPE, BOUNDARIES, ENVIRONMENT, CONSTRAINTS. Do NOT combine questions. Do NOT propose approaches until all categories are addressed.",
55
+ "Do not package multiple asks as bullet points in one message; split them into separate turns even if related.",
56
+ "When user answer is ambiguous or unknown (for example: 'не знаю'), propose explicit defaults as assumptions and get a one-message confirmation before treating that category as closed.",
57
+ "Include at least one premise-challenge and one boundary stress-test question before locking options. Do not accept 'simple demo' framing without challenge.",
54
58
  "For approach selection: use the Decision Protocol — present labeled options (A/B/C) with REAL trade-offs (not cosmetic) and mark one as (recommended) with clear reasoning. If AskQuestion/AskUserQuestion is available, send exactly ONE question per call, validate fields against runtime schema, and on schema error immediately fall back to plain-text question instead of retrying guessed payloads.",
55
59
  "Every approval question MUST state what exactly is being approved: 'Do you approve [the architecture / the API shape / the dependency choice]?' Never ask a bare 'approve?' or 'looks good?'.",
56
60
  "Get section-by-section approval before finalizing the design direction.",
@@ -59,8 +63,9 @@ const BRAINSTORM = {
59
63
  ],
60
64
  process: [
61
65
  "Explore project context — files, docs, behavior, recent changes. Share findings.",
62
- "Restate the problem — summarize what the user wants and why in a SEPARATE message. Wait for confirmation before questions.",
66
+ "Restate the problem — summarize what the user wants and why in a SEPARATE message. Keep this message declarative only; no questions. Wait for confirmation before questions.",
63
67
  "Clarify iteratively — ask questions one at a time covering mandatory categories: PURPOSE, SCOPE, BOUNDARIES, ENVIRONMENT, CONSTRAINTS. Do not skip to approaches early.",
68
+ "Convert unknown answers into explicit assumptions and confirm them before approaches.",
64
69
  "Identify whether request should be decomposed into smaller sub-problems.",
65
70
  "Offer 2-3 alternatives with real trade-offs and recommendation with rationale.",
66
71
  "Present design in sections. After each section explicitly name what you ask the user to approve.",
@@ -70,8 +75,15 @@ const BRAINSTORM = {
70
75
  "Handoff to scope stage only after approval is explicit."
71
76
  ],
72
77
  requiredGates: [
73
- { id: "brainstorm_context_explored", description: "Project context and constraints have been reviewed and summarized." },
74
- { id: "brainstorm_problem_restated", description: "Problem was restated in agent's words and user confirmed the understanding." },
78
+ { id: "brainstorm_discovery_purpose", description: "Discovery captured WHY this project exists and WHO it serves, with explicit user confirmation." },
79
+ { id: "brainstorm_discovery_scope", description: "Discovery captured explicit in-scope and out-of-scope boundaries before approach selection." },
80
+ { id: "brainstorm_discovery_boundaries", description: "Discovery captured failure modes, edge cases, and error-handling boundaries." },
81
+ { id: "brainstorm_discovery_environment", description: "Discovery captured runtime/install/deployment environment assumptions." },
82
+ { id: "brainstorm_discovery_constraints", description: "Discovery captured constraints (performance, compatibility, dependency limits) before deciding architecture." },
83
+ {
84
+ id: "brainstorm_problem_restated",
85
+ description: "Problem was restated in agent's words in a standalone non-question message, and user confirmed the understanding."
86
+ },
75
87
  { id: "brainstorm_options_compared", description: "At least two alternatives were compared with real trade-offs." },
76
88
  { id: "brainstorm_design_approved", description: "User approved a concrete design direction (with explicit statement of what was approved)." },
77
89
  { id: "brainstorm_self_review_passed", description: "Design doc passed placeholder/ambiguity/consistency checks." },
@@ -79,7 +91,13 @@ const BRAINSTORM = {
79
91
  ],
80
92
  requiredEvidence: [
81
93
  "Artifact written to `.cclaw/artifacts/01-brainstorm.md`.",
94
+ "Clarification log explicitly records PURPOSE, SCOPE, BOUNDARIES, ENVIRONMENT, and CONSTRAINTS coverage.",
95
+ "Clarification sequence evidence shows one explicit question per message (no bundled multi-question turns).",
96
+ "Clarification log includes at least one premise-challenge question and one boundary stress-test question before approach selection.",
97
+ "Unknown or ambiguous user answers are converted into explicit assumptions and confirmed (or explicitly waived) before closure.",
98
+ "Discovery sections include explicit in-scope/out-of-scope boundaries and failure handling boundaries.",
82
99
  "Approved direction captured in artifact.",
100
+ "Restatement evidence shows a standalone non-question restatement message before clarification Q&A.",
83
101
  "Open questions explicitly listed (if any).",
84
102
  "Self-review pass completed with no unresolved issues."
85
103
  ],
@@ -111,6 +129,10 @@ const BRAINSTORM = {
111
129
  "Jumping directly into implementation",
112
130
  "Combining visual companion offer with a clarifying question",
113
131
  "Invoking implementation skills before writing plans",
132
+ "Appending clarifying questions to the restatement message instead of waiting for confirmation",
133
+ "Packing multiple clarifying questions into one message (including bullet-list question bundles)",
134
+ "Silently filling defaults after 'не знаю'/'I don't know' without explicit assumption confirmation",
135
+ "Accepting 'simple/test/demo' framing without premise challenge or failure stress testing",
114
136
  "Asking bare 'approve?' or 'одобряете?' without stating WHAT is being approved",
115
137
  "Presenting a single summary and asking for blanket approval instead of section-by-section review",
116
138
  "Rushing through clarification — asking 1-2 generic questions then jumping to design",
@@ -129,6 +151,10 @@ const BRAINSTORM = {
129
151
  "Implementation-related actions before approval",
130
152
  "Self-review skipped or glossed over",
131
153
  "Artifact has TBD or placeholder sections",
154
+ "Restatement step includes clarifying questions or option prompts",
155
+ "Clarification turns repeatedly include multi-question bundles instead of single asks",
156
+ "Unknown user answers are treated as settled requirements without explicit assumption confirmation",
157
+ "No evidence of premise challenge or failure stress-test questions before options",
132
158
  "Fewer than 3 clarifying questions asked for any non-trivial project",
133
159
  "Approval requested without stating what exactly is being approved"
134
160
  ],
@@ -155,8 +181,18 @@ const BRAINSTORM = {
155
181
  traceabilityRule: "Every approved direction must be traceable forward through scope and design. Downstream stages must reference brainstorm decisions."
156
182
  },
157
183
  artifactValidation: [
158
- { section: "Problem Statement", required: true, validationRule: "Must describe the user problem, not the solution. Include WHO and WHY." },
184
+ { section: "Problem Statement", required: true, validationRule: "Must describe the user problem, not the solution. Include WHO and WHY and success signal." },
159
185
  { section: "Known Context", required: true, validationRule: "Files, patterns, constraints discovered during exploration. Evidence that context was actually explored." },
186
+ {
187
+ section: "Clarification Log",
188
+ required: true,
189
+ validationRule: "At least 5 rows covering PURPOSE, SCOPE, BOUNDARIES, ENVIRONMENT, CONSTRAINTS. Table must use 4 columns: Category, Question asked, User answer, Evidence note."
190
+ },
191
+ { section: "Purpose & Beneficiaries", required: true, validationRule: "At least 3 meaningful lines describing why this exists and who benefits." },
192
+ { section: "Scope Boundaries", required: true, validationRule: "At least 2 scope items including explicit out-of-scope boundaries." },
193
+ { section: "Failure Boundaries", required: true, validationRule: "At least 2 failure/edge-case expectations and error visibility behavior." },
194
+ { section: "Runtime Environment", required: true, validationRule: "At least 2 lines describing runtime, install/distribution, and execution environment." },
195
+ { section: "Constraints", required: true, validationRule: "At least 2 concrete constraints (performance, compatibility, dependency, or policy)." },
160
196
  { section: "Alternatives Table", required: true, validationRule: "At least 2 approaches with real trade-offs (not cosmetic) and recommendation with reasoning." },
161
197
  { section: "Approved Direction", required: true, validationRule: "Must contain explicit approval marker from user. State what was approved." },
162
198
  { section: "Assumptions & Risks", required: true, validationRule: "Explicit assumptions made during design. Known risks. If none, state 'None'." },
@@ -1,4 +1,4 @@
1
1
  export declare const ARTIFACT_TEMPLATES: Record<string, string>;
2
2
  export declare const RULEBOOK_MARKDOWN = "# Cclaw Rulebook\n\n## MUST_ALWAYS\n- Follow flow order: brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship\n- Require explicit user confirmation after plan before TDD\n- Keep evidence artifacts in `.cclaw/artifacts/`\n- Enforce RED before GREEN in TDD\n- Run two-layer review (spec_compliance and code_quality) before ship\n- Validate all inputs before processing \u2014 never trust external data without sanitization\n- Prefer immutable data patterns and pure functions where the language supports them\n- Follow existing repo conventions, patterns, and directory structure \u2014 match the codebase\n- Verify claims with fresh evidence: \"tests pass\" requires running tests in this message\n- Use conventional commits: `type(scope): description` (feat, fix, refactor, test, docs, chore)\n\n## MUST_NEVER\n- Skip RED phase and jump directly to GREEN in TDD\n- Ship with critical review findings\n- Start implementation during /brainstorm\n- Modify generated cclaw files manually when CLI can regenerate them\n- Commit `.cclaw/` or generated shim files\n- Expose secrets, tokens, API keys, or absolute system paths in agent output\n- Duplicate existing functionality without explicit justification \u2014 search before building\n- Bypass security checks, linting hooks, or type checking to \"move faster\"\n- Claim success (\"Done,\" \"All good,\" \"Tests pass\") without running verification in this message\n- Make changes outside the blast radius of the current task without user consent\n\n## DELEGATION\nWhen a task requires specialist knowledge (security audit, performance profiling, database review),\ndelegate to a specialized agent or skill if the harness supports it. The primary agent should:\n1. Identify the specialist domain\n2. Provide focused context (relevant files, the specific concern)\n3. Evaluate the specialist output before acting on it \u2014 do not blindly apply recommendations\n";
3
- export declare const CURSOR_WORKFLOW_RULE_MDC = "---\ndescription: cclaw workflow guardrails for Cursor agent sessions\nglobs:\n - \"**/*\"\nalwaysApply: true\n---\n\n<!-- cclaw-managed-cursor-workflow-rule -->\n\n# Cclaw Workflow Guardrails\n\n- Follow stage order: brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship.\n- Read `.cclaw/state/flow-state.json` before acting; continue from current stage when active.\n- Use `/cc-next` only after required gates pass; never bypass explicit pause/approval rules.\n- Keep evidence in `.cclaw/artifacts/` and canonical run copies in `.cclaw/runs/<activeRunId>/artifacts/`.\n- For machine-only checks in design/plan/tdd/review/ship, dispatch required specialists automatically when tooling supports it.\n- Ask for user input only at explicit approval gates (scope mode, plan approval, user challenge resolution, ship finalization).\n- Treat `.cclaw/skills/using-cclaw/SKILL.md` as routing source of truth; load contextual utility skills only when their triggers apply.\n";
3
+ export declare const CURSOR_WORKFLOW_RULE_MDC = "---\ndescription: cclaw workflow guardrails for Cursor agent sessions\nglobs:\n - \"**/*\"\nalwaysApply: true\n---\n\n<!-- cclaw-managed-cursor-workflow-rule -->\n\n# Cclaw Workflow Guardrails\n\n- Follow stage order: brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship.\n- Read `.cclaw/state/flow-state.json` before acting; continue from current stage when active.\n- Use `/cc-next` only after required gates pass; never bypass explicit pause/approval rules.\n- Keep evidence in `.cclaw/artifacts/`; cclaw sync/runtime maintains run snapshots in `.cclaw/runs/<activeRunId>/artifacts/`.\n- For machine-only checks in design/plan/tdd/review/ship, dispatch required specialists automatically when tooling supports it.\n- Ask for user input only at explicit approval gates (scope mode, plan approval, user challenge resolution, ship finalization).\n- Treat `.cclaw/skills/using-cclaw/SKILL.md` as routing source of truth; load contextual utility skills only when their triggers apply.\n";
4
4
  export declare function buildRulesJson(): Record<string, unknown>;
@@ -8,13 +8,48 @@ export const ARTIFACT_TEMPLATES = {
8
8
  - **Who benefits:**
9
9
  - **Why now:**
10
10
  - **Success signal:**
11
- - **Constraints:**
12
11
 
13
12
  ## Known Context
14
13
  - **Explored files/patterns:**
15
14
  - **Existing behavior:**
16
15
  - **Relevant dependencies:**
17
16
 
17
+ ## Clarification Log
18
+ | Category | Question asked | User answer | Evidence note |
19
+ |---|---|---|---|
20
+ | PURPOSE | | | |
21
+ | SCOPE | | | |
22
+ | BOUNDARIES | | | |
23
+ | ENVIRONMENT | | | |
24
+ | CONSTRAINTS | | | |
25
+
26
+ ## Purpose & Beneficiaries
27
+ - **Project purpose:**
28
+ - **Primary users:**
29
+ - **Value outcome:**
30
+
31
+ ## Scope Boundaries
32
+ ### In Scope
33
+ -
34
+
35
+ ### Out of Scope
36
+ -
37
+
38
+ ## Failure Boundaries
39
+ - **Edge cases to handle:**
40
+ - **Expected failures and behavior:**
41
+ - **Error visibility expectations:**
42
+
43
+ ## Runtime Environment
44
+ - **Runtime/platform:**
45
+ - **Install/distribution model:**
46
+ - **Execution context (local/CI/deploy):**
47
+
48
+ ## Constraints
49
+ - **Performance constraints:**
50
+ - **Compatibility constraints:**
51
+ - **Dependency constraints:**
52
+
18
53
  ## Alternatives Table
19
54
  | Option | Summary | Trade-offs | Recommendation |
20
55
  |---|---|---|---|
@@ -347,7 +382,7 @@ alwaysApply: true
347
382
  - Follow stage order: brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship.
348
383
  - Read \`.cclaw/state/flow-state.json\` before acting; continue from current stage when active.
349
384
  - Use \`/cc-next\` only after required gates pass; never bypass explicit pause/approval rules.
350
- - Keep evidence in \`.cclaw/artifacts/\` and canonical run copies in \`.cclaw/runs/<activeRunId>/artifacts/\`.
385
+ - Keep evidence in \`.cclaw/artifacts/\`; cclaw sync/runtime maintains run snapshots in \`.cclaw/runs/<activeRunId>/artifacts/\`.
351
386
  - For machine-only checks in design/plan/tdd/review/ship, dispatch required specialists automatically when tooling supports it.
352
387
  - Ask for user input only at explicit approval gates (scope mode, plan approval, user challenge resolution, ship finalization).
353
388
  - Treat \`.cclaw/skills/using-cclaw/SKILL.md\` as routing source of truth; load contextual utility skills only when their triggers apply.
package/dist/doctor.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { execFile } from "node:child_process";
4
+ import { pathToFileURL } from "node:url";
4
5
  import { promisify } from "node:util";
5
6
  import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
6
7
  import { CCLAW_AGENTS } from "./content/agents.js";
@@ -186,6 +187,47 @@ async function opencodeRegistrationCheck(projectRoot) {
186
187
  }
187
188
  return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${expected}` };
188
189
  }
190
+ async function opencodePluginRuntimeShapeCheck(projectRoot) {
191
+ const pluginPath = path.join(projectRoot, ".opencode/plugins/cclaw-plugin.mjs");
192
+ if (!(await exists(pluginPath))) {
193
+ return { ok: false, details: `${path.relative(projectRoot, pluginPath)} not found` };
194
+ }
195
+ try {
196
+ const moduleUrl = `${pathToFileURL(pluginPath).href}?doctor=${Date.now()}`;
197
+ const imported = await import(moduleUrl);
198
+ if (typeof imported.default !== "function") {
199
+ return {
200
+ ok: false,
201
+ details: `${path.relative(projectRoot, pluginPath)} must export a default plugin factory function`
202
+ };
203
+ }
204
+ const plugin = imported.default({ directory: projectRoot });
205
+ const requiredHandlers = [
206
+ "event",
207
+ "tool.execute.before",
208
+ "tool.execute.after",
209
+ "experimental.chat.system.transform"
210
+ ];
211
+ const missing = requiredHandlers.filter((name) => typeof plugin?.[name] !== "function");
212
+ if (missing.length > 0) {
213
+ return {
214
+ ok: false,
215
+ details: `${path.relative(projectRoot, pluginPath)} missing runtime handlers: ${missing.join(", ")}`
216
+ };
217
+ }
218
+ await plugin.event({ event: { type: "session.updated", data: {} } });
219
+ return {
220
+ ok: true,
221
+ details: `${path.relative(projectRoot, pluginPath)} exports compatible runtime handler shape`
222
+ };
223
+ }
224
+ catch (error) {
225
+ return {
226
+ ok: false,
227
+ details: `runtime load failed for .opencode/plugins/cclaw-plugin.mjs: ${error instanceof Error ? error.message : String(error)}`
228
+ };
229
+ }
230
+ }
189
231
  export async function doctorChecks(projectRoot, options = {}) {
190
232
  const checks = [];
191
233
  for (const dir of REQUIRED_DIRS) {
@@ -595,6 +637,12 @@ export async function doctorChecks(projectRoot, options = {}) {
595
637
  ok,
596
638
  details: `${file} must include event lifecycle handler, tool.execute.before/after with prompt/workflow/context hooks, session.idle summarization, and transform rehydration`
597
639
  });
640
+ const runtimeShape = await opencodePluginRuntimeShapeCheck(projectRoot);
641
+ checks.push({
642
+ name: "hook:opencode:runtime_shape",
643
+ ok: runtimeShape.ok,
644
+ details: runtimeShape.details
645
+ });
598
646
  const registration = await opencodeRegistrationCheck(projectRoot);
599
647
  checks.push({
600
648
  name: "hook:opencode:config_registration",
@@ -702,25 +750,39 @@ export async function doctorChecks(projectRoot, options = {}) {
702
750
  : `no gate reconciliation changes needed for stage "${reconciliation.stage}"`
703
751
  });
704
752
  }
753
+ const activeRunId = typeof flowState.activeRunId === "string" ? flowState.activeRunId.trim() : "";
754
+ const runActivationDeferred = activeRunId === "run-pending";
705
755
  checks.push({
706
756
  name: "flow_state:active_run_id",
707
- ok: typeof flowState.activeRunId === "string" && flowState.activeRunId.trim().length > 0,
708
- details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
757
+ ok: activeRunId.length > 0,
758
+ details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId (run-pending is allowed before first active run is materialized)`
709
759
  });
710
760
  checks.push({
711
761
  name: "run:active_artifacts",
712
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "artifacts")),
713
- details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/artifacts must exist`
762
+ ok: runActivationDeferred
763
+ ? true
764
+ : await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", activeRunId, "artifacts")),
765
+ details: runActivationDeferred
766
+ ? "active run artifacts are deferred until first feature run activation"
767
+ : `${RUNTIME_ROOT}/runs/${activeRunId}/artifacts must exist`
714
768
  });
715
769
  checks.push({
716
770
  name: "run:active_metadata",
717
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "run.json")),
718
- details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/run.json must exist`
771
+ ok: runActivationDeferred
772
+ ? true
773
+ : await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", activeRunId, "run.json")),
774
+ details: runActivationDeferred
775
+ ? "active run metadata is deferred until first feature run activation"
776
+ : `${RUNTIME_ROOT}/runs/${activeRunId}/run.json must exist`
719
777
  });
720
778
  checks.push({
721
779
  name: "run:active_handoff",
722
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "00-handoff.md")),
723
- details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/00-handoff.md must exist`
780
+ ok: runActivationDeferred
781
+ ? true
782
+ : await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", activeRunId, "handoff.md")),
783
+ details: runActivationDeferred
784
+ ? "active run handoff is deferred until first feature run activation"
785
+ : `${RUNTIME_ROOT}/runs/${activeRunId}/handoff.md must exist`
724
786
  });
725
787
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
726
788
  checks.push({
package/dist/install.js CHANGED
@@ -766,7 +766,7 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
766
766
  await writeArtifactTemplates(projectRoot);
767
767
  await writeRulebook(projectRoot);
768
768
  await writeState(projectRoot, forceStateReset);
769
- await ensureRunSystem(projectRoot);
769
+ await ensureRunSystem(projectRoot, { createIfMissing: false });
770
770
  await ensureSessionStateFiles(projectRoot);
771
771
  await writeAdapterManifest(projectRoot, harnesses);
772
772
  await ensureLearningsStore(projectRoot);
package/dist/runs.d.ts CHANGED
@@ -6,13 +6,17 @@ export interface CclawRunMeta {
6
6
  archivedAt?: string;
7
7
  stateSnapshot?: Omit<FlowState, "activeRunId">;
8
8
  }
9
+ interface EnsureRunSystemOptions {
10
+ createIfMissing?: boolean;
11
+ }
9
12
  export declare function readFlowState(projectRoot: string): Promise<FlowState>;
10
13
  export declare function writeFlowState(projectRoot: string, state: FlowState): Promise<void>;
11
14
  export declare function listRuns(projectRoot: string): Promise<CclawRunMeta[]>;
12
- export declare function ensureRunSystem(projectRoot: string): Promise<FlowState>;
15
+ export declare function ensureRunSystem(projectRoot: string, options?: EnsureRunSystemOptions): Promise<FlowState>;
13
16
  export declare function startNewFeatureRun(projectRoot: string, title?: string): Promise<CclawRunMeta>;
14
17
  export declare function resumeRun(projectRoot: string, runId: string): Promise<CclawRunMeta>;
15
18
  export declare function archiveRun(projectRoot: string, runId?: string): Promise<{
16
19
  archived: CclawRunMeta;
17
20
  active: CclawRunMeta;
18
21
  }>;
22
+ export {};
package/dist/runs.js CHANGED
@@ -7,7 +7,7 @@ const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
7
7
  const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
8
8
  const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
9
9
  const RUN_META_FILE = "run.json";
10
- const RUN_HANDOFF_FILE = "00-handoff.md";
10
+ const RUN_HANDOFF_FILE = "handoff.md";
11
11
  const FLOW_STAGE_SET = new Set(COMMAND_FILE_ORDER);
12
12
  function flowStatePath(projectRoot) {
13
13
  return path.join(projectRoot, FLOW_STATE_REL_PATH);
@@ -323,13 +323,17 @@ async function ensureRunHandoff(projectRoot, runId) {
323
323
  return;
324
324
  await writeFileSafe(runHandoffPath(projectRoot, runId), handoffMarkdown(meta, state));
325
325
  }
326
- export async function ensureRunSystem(projectRoot) {
326
+ export async function ensureRunSystem(projectRoot, options = {}) {
327
327
  await ensureDir(runsRoot(projectRoot));
328
328
  await ensureDir(activeArtifactsPath(projectRoot));
329
329
  let state = await readFlowState(projectRoot);
330
330
  let activeRunId = state.activeRunId;
331
+ const createIfMissing = options.createIfMissing !== false;
331
332
  const activeRunExists = activeRunId.trim().length > 0 && (await exists(runArtifactsPath(projectRoot, activeRunId)));
332
333
  if (!activeRunExists) {
334
+ if (!createIfMissing) {
335
+ return state;
336
+ }
333
337
  const activeHasArtifacts = (await listImmediateFiles(activeArtifactsPath(projectRoot))).length > 0;
334
338
  const initialRun = await createRun(projectRoot, {
335
339
  title: activeHasArtifacts ? "Migrated active run" : "Initial feature run",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {