cclaw-cli 0.5.3 → 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.
@@ -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) {
@@ -16,6 +16,22 @@ const STAGE_EXAMPLES = {
16
16
  **Q5 (CONSTRAINTS):** "What constraints are non-negotiable (runtime deps, latency, compatibility, policy)?"
17
17
  **A5:** "No new runtime dependencies; checks should stay under 2 minutes; compatible with current workflow."
18
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
+
19
35
  ### Alternatives comparison
20
36
 
21
37
  | Approach | Pros | Cons | Effort | Recommendation |
@@ -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
@@ -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
@@ -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.",
@@ -75,7 +80,10 @@ const BRAINSTORM = {
75
80
  { id: "brainstorm_discovery_boundaries", description: "Discovery captured failure modes, edge cases, and error-handling boundaries." },
76
81
  { id: "brainstorm_discovery_environment", description: "Discovery captured runtime/install/deployment environment assumptions." },
77
82
  { id: "brainstorm_discovery_constraints", description: "Discovery captured constraints (performance, compatibility, dependency limits) before deciding architecture." },
78
- { id: "brainstorm_problem_restated", description: "Problem was restated in agent's words and user confirmed the understanding." },
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
+ },
79
87
  { id: "brainstorm_options_compared", description: "At least two alternatives were compared with real trade-offs." },
80
88
  { id: "brainstorm_design_approved", description: "User approved a concrete design direction (with explicit statement of what was approved)." },
81
89
  { id: "brainstorm_self_review_passed", description: "Design doc passed placeholder/ambiguity/consistency checks." },
@@ -84,8 +92,12 @@ const BRAINSTORM = {
84
92
  requiredEvidence: [
85
93
  "Artifact written to `.cclaw/artifacts/01-brainstorm.md`.",
86
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.",
87
98
  "Discovery sections include explicit in-scope/out-of-scope boundaries and failure handling boundaries.",
88
99
  "Approved direction captured in artifact.",
100
+ "Restatement evidence shows a standalone non-question restatement message before clarification Q&A.",
89
101
  "Open questions explicitly listed (if any).",
90
102
  "Self-review pass completed with no unresolved issues."
91
103
  ],
@@ -117,6 +129,10 @@ const BRAINSTORM = {
117
129
  "Jumping directly into implementation",
118
130
  "Combining visual companion offer with a clarifying question",
119
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",
120
136
  "Asking bare 'approve?' or 'одобряете?' without stating WHAT is being approved",
121
137
  "Presenting a single summary and asking for blanket approval instead of section-by-section review",
122
138
  "Rushing through clarification — asking 1-2 generic questions then jumping to design",
@@ -135,6 +151,10 @@ const BRAINSTORM = {
135
151
  "Implementation-related actions before approval",
136
152
  "Self-review skipped or glossed over",
137
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",
138
158
  "Fewer than 3 clarifying questions asked for any non-trivial project",
139
159
  "Approval requested without stating what exactly is being approved"
140
160
  ],
@@ -163,7 +183,11 @@ const BRAINSTORM = {
163
183
  artifactValidation: [
164
184
  { section: "Problem Statement", required: true, validationRule: "Must describe the user problem, not the solution. Include WHO and WHY and success signal." },
165
185
  { section: "Known Context", required: true, validationRule: "Files, patterns, constraints discovered during exploration. Evidence that context was actually explored." },
166
- { section: "Clarification Log", required: true, validationRule: "At least 5 rows covering PURPOSE, SCOPE, BOUNDARIES, ENVIRONMENT, CONSTRAINTS." },
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
+ },
167
191
  { section: "Purpose & Beneficiaries", required: true, validationRule: "At least 3 meaningful lines describing why this exists and who benefits." },
168
192
  { section: "Scope Boundaries", required: true, validationRule: "At least 2 scope items including explicit out-of-scope boundaries." },
169
193
  { section: "Failure Boundaries", required: true, validationRule: "At least 2 failure/edge-case expectations and error visibility behavior." },
package/dist/doctor.js CHANGED
@@ -750,25 +750,39 @@ export async function doctorChecks(projectRoot, options = {}) {
750
750
  : `no gate reconciliation changes needed for stage "${reconciliation.stage}"`
751
751
  });
752
752
  }
753
+ const activeRunId = typeof flowState.activeRunId === "string" ? flowState.activeRunId.trim() : "";
754
+ const runActivationDeferred = activeRunId === "run-pending";
753
755
  checks.push({
754
756
  name: "flow_state:active_run_id",
755
- ok: typeof flowState.activeRunId === "string" && flowState.activeRunId.trim().length > 0,
756
- 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)`
757
759
  });
758
760
  checks.push({
759
761
  name: "run:active_artifacts",
760
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "artifacts")),
761
- 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`
762
768
  });
763
769
  checks.push({
764
770
  name: "run:active_metadata",
765
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "run.json")),
766
- 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`
767
777
  });
768
778
  checks.push({
769
779
  name: "run:active_handoff",
770
- ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs", flowState.activeRunId, "handoff.md")),
771
- details: `${RUNTIME_ROOT}/runs/${flowState.activeRunId}/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`
772
786
  });
773
787
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
774
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
@@ -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.3",
3
+ "version": "0.5.4",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {