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.
- package/dist/artifact-linter.js +38 -0
- package/dist/content/examples.js +16 -0
- package/dist/content/hooks.js +90 -1
- package/dist/content/observe.js +71 -7
- package/dist/content/stage-schema.js +30 -6
- package/dist/doctor.js +22 -8
- package/dist/install.js +1 -1
- package/dist/runs.d.ts +5 -1
- package/dist/runs.js +5 -1
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -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) {
|
package/dist/content/examples.js
CHANGED
|
@@ -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 |
|
package/dist/content/hooks.js
CHANGED
|
@@ -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
|
package/dist/content/observe.js
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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:
|
|
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:
|
|
761
|
-
|
|
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:
|
|
766
|
-
|
|
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:
|
|
771
|
-
|
|
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",
|