cclaw-cli 0.48.8 → 0.48.10

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.
@@ -1,1235 +1,127 @@
1
- /**
2
- * Hook generators for all supported harnesses.
3
- *
4
- * SessionStart: injects using-cclaw + flow state + knowledge + checkpoint/activity summary.
5
- * Stop: writes checkpoint.json and reminds about flow consistency.
6
- * Harness hook JSON wiring is generated in observe.ts.
7
- */
8
1
  import { RUNTIME_ROOT } from "../constants.js";
9
- import { META_SKILL_NAME } from "./meta-skill.js";
10
- const ESCAPE_FN = `escape_json() {
11
- local str="$1"
12
- str=\${str//\\\\/\\\\\\\\}
13
- str=\${str//\\"/\\\\\\"}
14
- str=\${str//$'\\t'/\\\\t}
15
- str=\${str//$'\\n'/\\\\n}
16
- printf '%s' "$str"
17
- }`;
18
- const HOOK_LIB_FILE = "_lib.sh";
19
- /** Shared bash preamble for generated hook scripts. */
20
- export const RUNTIME_SHELL_DETECT_ROOT = `CCLAW_HOOK_LIB_PATH=""
21
- for candidate in "\${CCLAW_PROJECT_ROOT:-}" "\${CLAUDE_PROJECT_DIR:-}" "\${CURSOR_PROJECT_DIR:-}" "\${CURSOR_PROJECT_ROOT:-}" "\${OPENCODE_PROJECT_DIR:-}" "\${OPENCODE_PROJECT_ROOT:-}" "\${PWD:-}"; do
22
- if [ -n "$candidate" ] && [ -f "$candidate/${RUNTIME_ROOT}/hooks/${HOOK_LIB_FILE}" ]; then
23
- CCLAW_HOOK_LIB_PATH="$candidate/${RUNTIME_ROOT}/hooks/${HOOK_LIB_FILE}"
24
- break
25
- fi
26
- done
27
- if [ -n "$CCLAW_HOOK_LIB_PATH" ] && [ -f "$CCLAW_HOOK_LIB_PATH" ]; then
28
- # shellcheck disable=SC1090
29
- . "$CCLAW_HOOK_LIB_PATH"
30
- fi
31
-
32
- if command -v cclaw_hook_detect_root >/dev/null 2>&1; then
33
- cclaw_hook_detect_root
34
- else
35
- HARNESS="codex"
36
- if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
37
- HARNESS="claude"
38
- elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
39
- HARNESS="cursor"
40
- elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
41
- HARNESS="opencode"
42
- fi
43
-
44
- ROOT=""
45
- for candidate in "\${CCLAW_PROJECT_ROOT:-}" "\${CLAUDE_PROJECT_DIR:-}" "\${CURSOR_PROJECT_DIR:-}" "\${CURSOR_PROJECT_ROOT:-}" "\${OPENCODE_PROJECT_DIR:-}" "\${OPENCODE_PROJECT_ROOT:-}" "\${PWD:-}"; do
46
- if [ -n "$candidate" ] && [ -d "$candidate/${RUNTIME_ROOT}" ]; then
47
- ROOT="$candidate"
48
- break
49
- fi
50
- done
51
- if [ -z "$ROOT" ]; then
52
- ROOT="\${CCLAW_PROJECT_ROOT:-\${CLAUDE_PROJECT_DIR:-\${CURSOR_PROJECT_DIR:-\${CURSOR_PROJECT_ROOT:-\${OPENCODE_PROJECT_DIR:-\${OPENCODE_PROJECT_ROOT:-\${PWD}}}}}}}"
53
- fi
54
- fi`;
55
- export function hookLibScript() {
56
- return `#!/usr/bin/env bash
57
- # cclaw shared hook library — generated by cclaw sync
58
- # Shared helper functions for root detection and lightweight JSON parsing.
59
-
60
- cclaw_hook_detect_root() {
61
- HARNESS="codex"
62
- if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
63
- HARNESS="claude"
64
- elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
65
- HARNESS="cursor"
66
- elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
67
- HARNESS="opencode"
68
- fi
69
-
70
- ROOT=""
71
- for candidate in "\${CCLAW_PROJECT_ROOT:-}" "\${CLAUDE_PROJECT_DIR:-}" "\${CURSOR_PROJECT_DIR:-}" "\${CURSOR_PROJECT_ROOT:-}" "\${OPENCODE_PROJECT_DIR:-}" "\${OPENCODE_PROJECT_ROOT:-}" "\${PWD:-}"; do
72
- if [ -n "$candidate" ] && [ -d "$candidate/${RUNTIME_ROOT}" ]; then
73
- ROOT="$candidate"
74
- break
75
- fi
76
- done
77
- if [ -z "$ROOT" ]; then
78
- ROOT="\${CCLAW_PROJECT_ROOT:-\${CLAUDE_PROJECT_DIR:-\${CURSOR_PROJECT_DIR:-\${CURSOR_PROJECT_ROOT:-\${OPENCODE_PROJECT_DIR:-\${OPENCODE_PROJECT_ROOT:-\${PWD}}}}}}}"
79
- fi
80
- }
81
-
82
- cclaw_hook_lower() {
83
- printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
84
- }
85
-
86
- cclaw_hook_extract_tool_and_payload() {
87
- local input_json="$1"
88
- CCLAW_HOOK_TOOL="unknown"
89
- CCLAW_HOOK_PAYLOAD=""
90
- if command -v jq >/dev/null 2>&1; then
91
- CCLAW_HOOK_TOOL=$(printf '%s' "$input_json" | 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")
92
- CCLAW_HOOK_PAYLOAD=$(printf '%s' "$input_json" | jq -r '.tool_input // .input // .arguments // .params // .payload // {} | tostring' 2>/dev/null || echo "")
93
- elif command -v python3 >/dev/null 2>&1; then
94
- CCLAW_HOOK_TOOL=$(INPUT_JSON="$input_json" python3 - <<'PY'
95
- import json
96
- import os
97
- try:
98
- value = json.loads(os.environ.get("INPUT_JSON", "{}"))
99
- except Exception:
100
- value = {}
101
-
102
- def pick_tool(payload):
103
- if not isinstance(payload, dict):
104
- return "unknown"
105
- candidates = [
106
- payload.get("tool_name"),
107
- payload.get("tool"),
108
- payload.get("toolName"),
109
- payload.get("name"),
110
- payload.get("id"),
111
- payload.get("command")
112
- ]
113
- top_tool = payload.get("tool")
114
- if isinstance(top_tool, dict):
115
- candidates.extend([top_tool.get("name"), top_tool.get("id")])
116
- nested = payload.get("input")
117
- if isinstance(nested, dict):
118
- candidates.extend([
119
- nested.get("tool_name"),
120
- nested.get("tool"),
121
- nested.get("toolName"),
122
- nested.get("name"),
123
- nested.get("id"),
124
- nested.get("command")
125
- ])
126
- nested_tool = nested.get("tool")
127
- if isinstance(nested_tool, dict):
128
- candidates.extend([nested_tool.get("name"), nested_tool.get("id")])
129
- for candidate in candidates:
130
- if isinstance(candidate, str) and candidate.strip():
131
- return candidate.strip()
132
- return "unknown"
133
-
134
- print(pick_tool(value))
135
- PY
136
- )
137
- CCLAW_HOOK_PAYLOAD=$(printf '%s' "$input_json")
138
- else
139
- CCLAW_HOOK_PAYLOAD=$(printf '%s' "$input_json")
140
- fi
141
- [ -n "$CCLAW_HOOK_PAYLOAD" ] || CCLAW_HOOK_PAYLOAD=$(printf '%s' "$input_json")
142
- [ -n "$CCLAW_HOOK_TOOL" ] || CCLAW_HOOK_TOOL="unknown"
143
- }
144
-
145
- cclaw_hook_read_flow_state_minimal() {
146
- local flow_state_file="$1"
147
- CCLAW_HOOK_FLOW_STAGE="none"
148
- CCLAW_HOOK_FLOW_RUN_ID="active"
149
- CCLAW_HOOK_FLOW_COMPLETED="0"
150
- [ -f "$flow_state_file" ] || return 0
151
-
152
- if command -v jq >/dev/null 2>&1; then
153
- CCLAW_HOOK_FLOW_STAGE=$(jq -r '.currentStage // "none"' "$flow_state_file" 2>/dev/null || echo "none")
154
- CCLAW_HOOK_FLOW_RUN_ID=$(jq -r '.activeRunId // "active"' "$flow_state_file" 2>/dev/null || echo "active")
155
- CCLAW_HOOK_FLOW_COMPLETED=$(jq -r '(.completedStages // []) | length' "$flow_state_file" 2>/dev/null || echo "0")
156
- return 0
157
- fi
158
-
159
- if command -v python3 >/dev/null 2>&1; then
160
- local flow_meta
161
- flow_meta=$(python3 - "$flow_state_file" <<'PY'
162
- import json
163
- import sys
164
- stage = "none"
165
- run_id = "active"
166
- completed = 0
167
- try:
168
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
169
- payload = json.load(fh)
170
- stage_value = payload.get("currentStage")
171
- run_value = payload.get("activeRunId")
172
- completed_value = payload.get("completedStages")
173
- if isinstance(stage_value, str) and stage_value:
174
- stage = stage_value
175
- if isinstance(run_value, str) and run_value:
176
- run_id = run_value
177
- if isinstance(completed_value, list):
178
- completed = len(completed_value)
179
- except Exception:
180
- pass
181
- print(stage)
182
- print(run_id)
183
- print(completed)
184
- PY
185
- )
186
- {
187
- IFS= read -r CCLAW_HOOK_FLOW_STAGE
188
- IFS= read -r CCLAW_HOOK_FLOW_RUN_ID
189
- IFS= read -r CCLAW_HOOK_FLOW_COMPLETED
190
- } <<EOF
191
- $flow_meta
192
- EOF
193
- [ -n "$CCLAW_HOOK_FLOW_STAGE" ] || CCLAW_HOOK_FLOW_STAGE="none"
194
- [ -n "$CCLAW_HOOK_FLOW_RUN_ID" ] || CCLAW_HOOK_FLOW_RUN_ID="active"
195
- [ -n "$CCLAW_HOOK_FLOW_COMPLETED" ] || CCLAW_HOOK_FLOW_COMPLETED="0"
196
- fi
197
- }
198
- `;
199
- }
200
- export function sessionStartScript(_options = {}) {
201
- return `#!/usr/bin/env bash
202
- # cclaw session-start hook — generated by cclaw sync
203
- # Injects using-cclaw + flow status + active artifacts + compact knowledge digest + checkpoint/activity summary.
204
- set -euo pipefail
205
-
206
- ${RUNTIME_SHELL_DETECT_ROOT}
207
-
208
- STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
209
- ACTIVE_FEATURE_FILE="$ROOT/${RUNTIME_ROOT}/state/active-feature.json"
210
- CHECKPOINT_FILE="$ROOT/${RUNTIME_ROOT}/state/checkpoint.json"
211
- ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
212
- IRON_LAWS_FILE="$ROOT/${RUNTIME_ROOT}/state/iron-laws.json"
213
- SUGGESTION_MEMORY_FILE="$ROOT/${RUNTIME_ROOT}/state/suggestion-memory.json"
214
- CONTEXT_WARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/state/context-warnings.jsonl"
215
- CONTEXT_MODE_FILE="$ROOT/${RUNTIME_ROOT}/state/context-mode.json"
216
- CONTEXTS_DIR="$ROOT/${RUNTIME_ROOT}/contexts"
217
- KNOWLEDGE_FILE="$ROOT/${RUNTIME_ROOT}/knowledge.jsonl"
218
- KNOWLEDGE_DIGEST_FILE="$ROOT/${RUNTIME_ROOT}/state/knowledge-digest.md"
219
- META_SKILL="$ROOT/${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"
220
-
221
- # --- Read flow state ---
222
- STAGE="none"
223
- COMPLETED="0"
224
- ACTIVE_RUN="none"
225
- ACTIVE_FEATURE="default"
226
- ACTIVE_CONTEXT_MODE="default"
227
- STALE_STAGES=""
228
- CONTEXT_MODE_NOTE=""
229
- if [ -f "$STATE_FILE" ]; then
230
- if command -v jq >/dev/null 2>&1; then
231
- STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
232
- COMPLETED=$(jq -r '(.completedStages | length) // 0' "$STATE_FILE" 2>/dev/null || echo "0")
233
- ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
234
- STALE_STAGES=$(jq -r '(.staleStages // {} | keys | join(", "))' "$STATE_FILE" 2>/dev/null || echo "")
235
- else
236
- if command -v python3 >/dev/null 2>&1; then
237
- STAGE=$(python3 - "$STATE_FILE" <<'PY'
238
- import json
239
- import sys
240
-
241
- stage = "none"
242
- try:
243
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
244
- data = json.load(fh)
245
- value = data.get("currentStage")
246
- if isinstance(value, str) and value:
247
- stage = value
248
- except Exception:
249
- pass
250
- print(stage)
251
- PY
252
- )
253
- COMPLETED=$(python3 - "$STATE_FILE" <<'PY'
254
- import json
255
- import sys
256
-
257
- count = 0
258
- try:
259
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
260
- data = json.load(fh)
261
- value = data.get("completedStages")
262
- if isinstance(value, list):
263
- count = len(value)
264
- except Exception:
265
- pass
266
- print(count)
267
- PY
268
- )
269
- ACTIVE_RUN=$(python3 - "$STATE_FILE" <<'PY'
270
- import json
271
- import sys
272
-
273
- run = "none"
274
- try:
275
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
276
- data = json.load(fh)
277
- value = data.get("activeRunId")
278
- if isinstance(value, str) and value:
279
- run = value
280
- except Exception:
281
- pass
282
- print(run)
283
- PY
284
- )
285
- STALE_STAGES=$(python3 - "$STATE_FILE" <<'PY'
286
- import json
287
- import sys
288
- value = ""
289
- try:
290
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
291
- data = json.load(fh)
292
- stale = data.get("staleStages", {})
293
- if isinstance(stale, dict):
294
- keys = [k for k, v in stale.items() if isinstance(v, dict)]
295
- value = ", ".join(keys)
296
- except Exception:
297
- pass
298
- print(value)
299
- PY
300
- )
301
- else
302
- STAGE=$(grep -o '"currentStage"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
303
- COMPLETED_RAW=$(grep -o '"completedStages"[[:space:]]*:[[:space:]]*\\[[^]]*\\]' "$STATE_FILE" 2>/dev/null | head -1 || echo "")
304
- if [ -n "$COMPLETED_RAW" ]; then
305
- COMPLETED=$(printf '%s' "$COMPLETED_RAW" | grep -o '"[^"]*"' | wc -l | tr -d ' ' 2>/dev/null || echo "0")
306
- else
307
- COMPLETED="0"
308
- fi
309
- ACTIVE_RUN=$(grep -o '"activeRunId"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
310
- fi
311
- fi
312
- fi
313
-
314
- if [ -f "$ACTIVE_FEATURE_FILE" ]; then
315
- if command -v jq >/dev/null 2>&1; then
316
- ACTIVE_FEATURE=$(jq -r '.activeFeature // "default"' "$ACTIVE_FEATURE_FILE" 2>/dev/null || echo "default")
317
- elif command -v python3 >/dev/null 2>&1; then
318
- ACTIVE_FEATURE=$(python3 - "$ACTIVE_FEATURE_FILE" <<'PY'
319
- import json
320
- import sys
321
- feature = "default"
322
- try:
323
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
324
- data = json.load(fh)
325
- value = data.get("activeFeature")
326
- if isinstance(value, str) and value:
327
- feature = value
328
- except Exception:
329
- pass
330
- print(feature)
331
- PY
332
- )
333
- fi
334
- fi
335
-
336
- if [ -f "$CONTEXT_MODE_FILE" ]; then
337
- if command -v jq >/dev/null 2>&1; then
338
- ACTIVE_CONTEXT_MODE=$(jq -r '.activeMode // "default"' "$CONTEXT_MODE_FILE" 2>/dev/null || echo "default")
339
- elif command -v python3 >/dev/null 2>&1; then
340
- ACTIVE_CONTEXT_MODE=$(python3 - "$CONTEXT_MODE_FILE" <<'PY'
341
- import json
342
- import sys
343
- mode = "default"
344
- try:
345
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
346
- data = json.load(fh)
347
- value = data.get("activeMode")
348
- if isinstance(value, str) and value:
349
- mode = value
350
- except Exception:
351
- pass
352
- print(mode)
353
- PY
354
- )
355
- fi
356
- fi
357
-
358
- if [ -f "$CONTEXTS_DIR/$ACTIVE_CONTEXT_MODE.md" ]; then
359
- CONTEXT_MODE_NOTE="Context mode: $ACTIVE_CONTEXT_MODE (guide: ${RUNTIME_ROOT}/contexts/$ACTIVE_CONTEXT_MODE.md)"
360
- else
361
- CONTEXT_MODE_NOTE="Context mode: $ACTIVE_CONTEXT_MODE"
362
- fi
363
-
364
- # --- Checkpoint summary ---
365
- CHECKPOINT_SUMMARY=""
366
- if [ -f "$CHECKPOINT_FILE" ]; then
367
- if command -v jq >/dev/null 2>&1; then
368
- CHECKPOINT_SUMMARY=$(jq -r '"Checkpoint: stage=" + (.stage // "none") + ", status=" + (.status // "unknown") + ", run=" + (.runId // "none") + ", at=" + (.timestamp // "unknown")' "$CHECKPOINT_FILE" 2>/dev/null || echo "")
369
- elif command -v python3 >/dev/null 2>&1; then
370
- CHECKPOINT_SUMMARY=$(python3 - "$CHECKPOINT_FILE" <<'PY'
371
- import json
372
- import sys
373
-
374
- try:
375
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
376
- data = json.load(fh)
377
- stage = data.get("stage", "none")
378
- status = data.get("status", "unknown")
379
- run_id = data.get("runId", "none")
380
- ts = data.get("timestamp", "unknown")
381
- print(f"Checkpoint: stage={stage}, status={status}, run={run_id}, at={ts}")
382
- except Exception:
383
- print("")
384
- PY
385
- )
386
- fi
387
- fi
388
-
389
- SESSION_DIGEST=""
390
- DIGEST_PATH="$ROOT/${RUNTIME_ROOT}/state/session-digest.md"
391
- if [ -f "$DIGEST_PATH" ]; then
392
- SESSION_DIGEST=$(cat "$DIGEST_PATH" 2>/dev/null || echo "")
393
- fi
394
-
395
- # --- Recent stage activity summary ---
396
- ACTIVITY_SUMMARY=""
397
- if [ -f "$ACTIVITY_FILE" ] && [ -s "$ACTIVITY_FILE" ]; then
398
- if command -v jq >/dev/null 2>&1; then
399
- ACTIVITY_SUMMARY=$(tail -n 5 "$ACTIVITY_FILE" 2>/dev/null | jq -r -s '
400
- map(select(type=="object"))
401
- | map("- " + (.ts // "unknown") + " [" + (.phase // "unknown") + "] " + (.tool // "unknown") + " (stage=" + (.stage // "unknown") + ", run=" + (.runId // "none") + ")")
402
- | join("\\n")
403
- ' 2>/dev/null || echo "")
404
- else
405
- ACTIVITY_SUMMARY=$(tail -n 3 "$ACTIVITY_FILE" 2>/dev/null || echo "")
406
- fi
407
- fi
408
-
409
- # --- Latest context warning ---
410
- CONTEXT_WARNING=""
411
- if [ -f "$CONTEXT_WARNINGS_FILE" ] && [ -s "$CONTEXT_WARNINGS_FILE" ]; then
412
- if command -v jq >/dev/null 2>&1; then
413
- CONTEXT_WARNING=$(tail -n 1 "$CONTEXT_WARNINGS_FILE" 2>/dev/null | jq -r '.note // ""' 2>/dev/null || echo "")
414
- else
415
- CONTEXT_WARNING=$(tail -n 1 "$CONTEXT_WARNINGS_FILE" 2>/dev/null || echo "")
416
- fi
417
- fi
418
-
419
- # --- Proactive stage suggestion memory (persistent opt-out) ---
420
- SUGGESTIONS_ENABLED="true"
421
- STAGE_MUTED="false"
422
- if [ -f "$SUGGESTION_MEMORY_FILE" ]; then
423
- if command -v jq >/dev/null 2>&1; then
424
- SUGGESTIONS_ENABLED=$(jq -r 'if (.enabled // true) then "true" else "false" end' "$SUGGESTION_MEMORY_FILE" 2>/dev/null || echo "true")
425
- STAGE_MUTED=$(jq -r --arg stage "$STAGE" 'if ((.mutedStages // []) | index($stage)) == null then "false" else "true" end' "$SUGGESTION_MEMORY_FILE" 2>/dev/null || echo "false")
426
- elif command -v python3 >/dev/null 2>&1; then
427
- SUGGESTIONS_ENABLED=$(python3 - "$SUGGESTION_MEMORY_FILE" <<'PY'
428
- import json
429
- import sys
430
- try:
431
- with open(sys.argv[1], "r", encoding="utf-8") as handle:
432
- value = json.load(handle)
433
- enabled = value.get("enabled", True)
434
- print("true" if enabled else "false")
435
- except Exception:
436
- print("true")
437
- PY
438
- )
439
- STAGE_MUTED=$(python3 - "$SUGGESTION_MEMORY_FILE" "$STAGE" <<'PY'
440
- import json
441
- import sys
442
- try:
443
- with open(sys.argv[1], "r", encoding="utf-8") as handle:
444
- value = json.load(handle)
445
- muted = value.get("mutedStages", [])
446
- print("true" if isinstance(muted, list) and sys.argv[2] in muted else "false")
447
- except Exception:
448
- print("false")
449
- PY
450
- )
451
- fi
452
- fi
453
-
454
- STAGE_SUGGESTION=""
455
- if [ "$SUGGESTIONS_ENABLED" = "true" ] && [ "$STAGE_MUTED" != "true" ]; then
456
- case "$STAGE" in
457
- brainstorm) STAGE_SUGGESTION="Suggestion: list 2-3 alternatives and ask a single focused clarifying question before direction lock." ;;
458
- scope) STAGE_SUGGESTION="Suggestion: lock explicit in-scope/out-of-scope boundaries and choose one scope mode." ;;
459
- design) STAGE_SUGGESTION="Suggestion: map failure modes per new codepath and confirm architecture boundaries before moving forward." ;;
460
- spec) STAGE_SUGGESTION="Suggestion: ensure every acceptance criterion is measurable and mapped to a concrete test." ;;
461
- plan) STAGE_SUGGESTION="Suggestion: group tasks into dependency batches and keep WAIT_FOR_CONFIRM pending until approval." ;;
462
- tdd) STAGE_SUGGESTION="Suggestion: execute RED → GREEN → REFACTOR for each selected slice and capture evidence per cycle." ;;
463
- review) STAGE_SUGGESTION="Suggestion: run Layer 1 before Layer 2 and reconcile findings into 07-review-army.json." ;;
464
- ship) STAGE_SUGGESTION="Suggestion: verify preflight + rollback plan before selecting exactly one finalization mode." ;;
465
- *) STAGE_SUGGESTION="" ;;
466
- esac
467
- fi
468
-
469
- if [ -n "$STAGE_SUGGESTION" ]; then
470
- NOW_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
471
- TMP_SUGGESTION_FILE="$SUGGESTION_MEMORY_FILE.tmp.$$"
472
- if command -v jq >/dev/null 2>&1 && [ -f "$SUGGESTION_MEMORY_FILE" ]; then
473
- jq --arg stage "$STAGE" --arg ts "$NOW_TS" '
474
- .lastSuggestedStage = $stage
475
- | .lastSuggestedAt = $ts
476
- | .enabled = (.enabled // true)
477
- | .mutedStages = (if (.mutedStages | type) == "array" then .mutedStages else [] end)
478
- ' "$SUGGESTION_MEMORY_FILE" > "$TMP_SUGGESTION_FILE" 2>/dev/null || true
479
- elif command -v python3 >/dev/null 2>&1; then
480
- python3 - "$SUGGESTION_MEMORY_FILE" "$TMP_SUGGESTION_FILE" "$STAGE" "$NOW_TS" <<'PY'
481
- import json
482
- import sys
483
- from pathlib import Path
484
-
485
- source = Path(sys.argv[1])
486
- target = Path(sys.argv[2])
487
- stage = sys.argv[3]
488
- ts = sys.argv[4]
489
- payload = {"enabled": True, "mutedStages": []}
490
- if source.exists():
491
- try:
492
- parsed = json.loads(source.read_text(encoding="utf-8"))
493
- if isinstance(parsed, dict):
494
- payload.update(parsed)
495
- except Exception:
496
- pass
497
- if not isinstance(payload.get("mutedStages"), list):
498
- payload["mutedStages"] = []
499
- if not isinstance(payload.get("enabled"), bool):
500
- payload["enabled"] = True
501
- payload["lastSuggestedStage"] = stage
502
- payload["lastSuggestedAt"] = ts
503
- target.write_text(json.dumps(payload, indent=2) + "\\n", encoding="utf-8")
504
- PY
505
- fi
506
- if [ -s "$TMP_SUGGESTION_FILE" ]; then
507
- mv "$TMP_SUGGESTION_FILE" "$SUGGESTION_MEMORY_FILE" 2>/dev/null || rm -f "$TMP_SUGGESTION_FILE" 2>/dev/null || true
508
- fi
509
- fi
510
-
511
- # --- Read meta-skill (full file) ---
512
- META_CONTENT=""
513
- if [ -f "$META_SKILL" ]; then
514
- META_CONTENT=$(cat "$META_SKILL" 2>/dev/null || echo "")
515
- fi
516
-
517
- # --- Build compact knowledge digest (stage + branch + diff aware) ---
518
- KNOWLEDGE_DIGEST=""
519
- LEARNINGS_COUNT=0
520
- if [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
521
- LEARNINGS_COUNT=$(grep -c '^{' "$KNOWLEDGE_FILE" 2>/dev/null || echo "0")
522
- fi
523
-
524
- if command -v cclaw >/dev/null 2>&1 && [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
525
- BRANCH_NAME=""
526
- if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
527
- BRANCH_NAME=$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
528
- fi
529
- DIFF_FILES_CSV=""
530
- if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
531
- DIFF_FILES_CSV=$(git -C "$ROOT" diff --name-only HEAD~5..HEAD 2>/dev/null | head -n 20 | tr '\n' ',' | sed 's/,$//' || echo "")
532
- fi
533
- OPEN_GATES_CSV=""
534
- if [ -f "$STATE_FILE" ] && command -v jq >/dev/null 2>&1; then
535
- OPEN_GATES_CSV=$(jq -r --arg stage "$STAGE" '
536
- (.stageGateCatalog[$stage].required // [])
537
- - (.stageGateCatalog[$stage].passed // [])
538
- | join(",")
539
- ' "$STATE_FILE" 2>/dev/null || echo "")
540
- fi
541
- DIGEST_CMD=(cclaw internal knowledge-digest --stage="$STAGE" --limit=8)
542
- if [ -n "$BRANCH_NAME" ]; then
543
- DIGEST_CMD+=("--branch=$BRANCH_NAME")
544
- fi
545
- if [ -n "$DIFF_FILES_CSV" ]; then
546
- DIGEST_CMD+=("--diff-files=$DIFF_FILES_CSV")
547
- fi
548
- if [ -n "$OPEN_GATES_CSV" ]; then
549
- DIGEST_CMD+=("--open-gates=$OPEN_GATES_CSV")
550
- fi
551
- KNOWLEDGE_DIGEST=$("\${DIGEST_CMD[@]}" 2>/dev/null || echo "")
552
- fi
553
-
554
- if [ -z "$KNOWLEDGE_DIGEST" ] && [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
555
- if command -v jq >/dev/null 2>&1; then
556
- KNOWLEDGE_DIGEST=$(tail -n 120 "$KNOWLEDGE_FILE" 2>/dev/null | jq -Rsc --arg stage "$STAGE" '
557
- split("\\n")
558
- | map(select(length > 0))
559
- | map(try fromjson catch null)
560
- | map(select(type == "object"))
561
- | map(select((.stage // null) == $stage or (.stage // null) == null))
562
- | reverse
563
- | .[0:6]
564
- | map("- [" + ((.confidence // "unknown")|tostring) + " • " + ((.stage // "global")|tostring) + " • " + ((.domain // "general")|tostring) + "] " + ((.trigger // "trigger")|tostring) + " -> " + ((.action // "action")|tostring))
565
- | join("\\n")
566
- ' 2>/dev/null || echo "")
567
- else
568
- KNOWLEDGE_DIGEST=$(tail -n 6 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
569
- fi
570
- fi
571
-
572
- if [ -n "$KNOWLEDGE_DIGEST" ]; then
573
- printf '# Knowledge digest (auto-generated)\\n\\n%s\\n' "$KNOWLEDGE_DIGEST" > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
574
- elif [ -f "$KNOWLEDGE_DIGEST_FILE" ]; then
575
- printf '# Knowledge digest (auto-generated)\\n\\n(no matching entries for current stage)\\n' > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
576
- fi
577
-
578
- IRON_LAWS_SUMMARY=""
579
- if [ -f "$IRON_LAWS_FILE" ]; then
580
- if command -v jq >/dev/null 2>&1; then
581
- IRON_LAWS_SUMMARY=$(jq -r '
582
- (.laws // [])
583
- | map("- [" + (if (.strict // false) then "strict" else "advisory" end) + "] " + ((.id // "law")|tostring) + " -> " + ((.rule // "")|tostring))
584
- | .[0:6]
585
- | join("\\n")
586
- ' "$IRON_LAWS_FILE" 2>/dev/null || echo "")
587
- elif command -v python3 >/dev/null 2>&1; then
588
- IRON_LAWS_SUMMARY=$(python3 - "$IRON_LAWS_FILE" <<'PY'
589
- import json
590
- import sys
591
- out = []
592
- try:
593
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
594
- parsed = json.load(fh)
595
- for row in (parsed.get("laws") or [])[:6]:
596
- if not isinstance(row, dict):
597
- continue
598
- strict = "strict" if row.get("strict") else "advisory"
599
- law_id = str(row.get("id") or "law")
600
- rule = str(row.get("rule") or "")
601
- out.append(f"- [{strict}] {law_id} -> {rule}")
602
- except Exception:
603
- out = []
604
- print("\\n".join(out))
605
- PY
606
- )
607
- fi
608
- fi
609
-
610
- # --- Installed cclaw-cli version vs. project's recorded version (one-block
611
- # upgrade-check, gstack-style). Purely informational — we never block. ---
612
- VERSION_NOTE=""
613
- INSTALLED_VERSION=""
614
- PROJECT_VERSION=""
615
- # Version lookup is skipped by default — spawning the cli on every session
616
- # start adds ~10s on Node-based installs. Opt-in via CCLAW_HOOK_VERSION_CHECK=1.
617
- if [ "\${CCLAW_HOOK_VERSION_CHECK:-0}" = "1" ] && command -v cclaw >/dev/null 2>&1; then
618
- INSTALLED_VERSION=$(cclaw --version 2>/dev/null | head -1 | awk '{print $NF}' || echo "")
619
- fi
620
- CONFIG_FILE="$ROOT/${RUNTIME_ROOT}/config.json"
621
- if [ -f "$CONFIG_FILE" ]; then
622
- if command -v jq >/dev/null 2>&1; then
623
- PROJECT_VERSION=$(jq -r '.version // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
624
- else
625
- PROJECT_VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "")
626
- fi
627
- fi
628
- if [ -n "$INSTALLED_VERSION" ] && [ -n "$PROJECT_VERSION" ] && [ "$INSTALLED_VERSION" != "$PROJECT_VERSION" ]; then
629
- VERSION_NOTE="cclaw-cli $INSTALLED_VERSION installed; project recorded $PROJECT_VERSION — run 'cclaw sync' to realign."
630
- fi
631
-
632
- # --- Routing-check: AGENTS.md / CLAUDE.md must contain the cclaw block. ---
633
- ROUTING_NOTE=""
634
- ROUTING_MISSING=""
635
- for routing_file in "$ROOT/AGENTS.md" "$ROOT/CLAUDE.md"; do
636
- if [ -f "$routing_file" ]; then
637
- if ! grep -q "cclaw-start" "$routing_file" 2>/dev/null; then
638
- ROUTING_MISSING="$ROUTING_MISSING $(basename "$routing_file")"
639
- fi
640
- fi
641
- done
642
- if [ -n "$ROUTING_MISSING" ]; then
643
- ROUTING_NOTE="Routing block missing from:\${ROUTING_MISSING}. Run 'cclaw sync' to re-inject."
644
- fi
645
-
646
- # --- Build context message ---
647
- CTX="cclaw loaded. Flow: stage=$STAGE ($COMPLETED/8 completed, run=$ACTIVE_RUN, feature=$ACTIVE_FEATURE). Active artifacts: ${RUNTIME_ROOT}/artifacts/. Feature registry: ${RUNTIME_ROOT}/state/worktrees.json (managed roots: ${RUNTIME_ROOT}/worktrees/). Learnings: $LEARNINGS_COUNT entries."
648
- if [ -n "$VERSION_NOTE" ]; then
649
- CTX="$CTX
650
- $VERSION_NOTE"
651
- fi
652
- if [ -n "$ROUTING_NOTE" ]; then
653
- CTX="$CTX
654
- $ROUTING_NOTE"
655
- fi
656
- if [ -n "$CONTEXT_MODE_NOTE" ]; then
657
- CTX="$CTX
658
- $CONTEXT_MODE_NOTE"
659
- fi
660
- if [ -n "$CHECKPOINT_SUMMARY" ]; then
661
- CTX="$CTX
662
- $CHECKPOINT_SUMMARY"
663
- fi
664
- if [ -n "$SESSION_DIGEST" ]; then
665
- CTX="$CTX
666
- Last session:
667
- $SESSION_DIGEST"
668
- fi
669
- if [ -n "$ACTIVITY_SUMMARY" ]; then
670
- CTX="$CTX
671
- Recent stage activity:
672
- $ACTIVITY_SUMMARY"
673
- fi
674
- if [ -n "$CONTEXT_WARNING" ]; then
675
- CTX="$CTX
676
- Latest context warning:
677
- $CONTEXT_WARNING"
678
- fi
679
- if [ -n "$STAGE_SUGGESTION" ]; then
680
- CTX="$CTX
681
- $STAGE_SUGGESTION
682
- To disable suggestions persistently set ${RUNTIME_ROOT}/state/suggestion-memory.json -> enabled=false."
683
- fi
684
- if [ -n "$STALE_STAGES" ]; then
685
- CTX="$CTX
686
- Stale stages pending acknowledgement: $STALE_STAGES (use /cc-ops rewind --ack <stage> after redo)."
687
- fi
688
- if [ -n "$KNOWLEDGE_DIGEST" ]; then
689
- CTX="$CTX
690
- Knowledge digest (top relevant entries):
691
- $KNOWLEDGE_DIGEST"
692
- fi
693
- if [ -n "$IRON_LAWS_SUMMARY" ]; then
694
- CTX="$CTX
695
- Iron laws (enforced policy highlights):
696
- $IRON_LAWS_SUMMARY"
697
- fi
698
- if [ -n "$META_CONTENT" ]; then
699
- CTX="$CTX
700
-
701
- $META_CONTENT"
702
- fi
703
-
704
- # --- Escape for JSON ---
705
- ${ESCAPE_FN}
706
- MSG=$(escape_json "$CTX")
707
-
708
- # --- Output harness-specific JSON ---
709
- case "$HARNESS" in
710
- claude)
711
- printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$MSG"
712
- ;;
713
- cursor)
714
- printf '{"additional_context":"%s"}\\n' "$MSG"
715
- ;;
716
- codex)
717
- printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$MSG"
718
- ;;
719
- *)
720
- printf '{"additional_context":"%s"}\\n' "$MSG"
721
- ;;
722
- esac
723
- `;
724
- }
725
- export function stopCheckpointScript() {
726
- return `#!/usr/bin/env bash
727
- # cclaw stop hook — generated by cclaw sync
728
- # Writes checkpoint state and reminds agent about flow/session consistency.
729
- set -euo pipefail
730
-
731
- ${RUNTIME_SHELL_DETECT_ROOT}
732
-
733
- INPUT=$(cat 2>/dev/null || echo '{}')
734
-
735
- STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
736
- STATE_FILE="$STATE_DIR/flow-state.json"
737
- CHECKPOINT_FILE="$STATE_DIR/checkpoint.json"
738
- CHECKPOINT_TMP="$STATE_DIR/checkpoint.json.tmp.$$"
739
- CHECKPOINT_LOCK_DIR="$STATE_DIR/.checkpoint.lock"
740
- IRON_LAWS_FILE="$STATE_DIR/iron-laws.json"
741
- STAGE="none"
742
- ACTIVE_RUN="none"
743
- LOOP_COUNT=""
744
-
745
- if command -v jq >/dev/null 2>&1; then
746
- LOOP_COUNT=$(echo "$INPUT" | jq -r '.loop_count // 0' 2>/dev/null || echo "")
747
- elif command -v python3 >/dev/null 2>&1; then
748
- LOOP_COUNT=$(INPUT_JSON="$INPUT" python3 - <<'PY'
749
- import json
750
- import os
751
-
752
- try:
753
- data = json.loads(os.environ.get("INPUT_JSON", "{}"))
754
- print(data.get("loop_count", 0))
755
- except Exception:
756
- print("")
757
- PY
758
- )
759
- fi
760
- [ -n "$LOOP_COUNT" ] || LOOP_COUNT="0"
761
-
762
- if [ -f "$STATE_FILE" ]; then
763
- if command -v jq >/dev/null 2>&1; then
764
- STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
765
- ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
766
- elif command -v python3 >/dev/null 2>&1; then
767
- STAGE=$(python3 - "$STATE_FILE" <<'PY'
768
- import json
769
- import sys
770
-
771
- stage = "none"
772
- try:
773
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
774
- data = json.load(fh)
775
- value = data.get("currentStage")
776
- if isinstance(value, str) and value:
777
- stage = value
778
- except Exception:
779
- pass
780
- print(stage)
781
- PY
782
- )
783
- ACTIVE_RUN=$(python3 - "$STATE_FILE" <<'PY'
784
- import json
785
- import sys
786
-
787
- run = "none"
788
- try:
789
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
790
- data = json.load(fh)
791
- value = data.get("activeRunId")
792
- if isinstance(value, str) and value:
793
- run = value
794
- except Exception:
795
- pass
796
- print(run)
797
- PY
798
- )
799
- fi
800
- fi
801
-
802
- DIRTY_STATE="unknown"
803
- if command -v git >/dev/null 2>&1; then
804
- if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
805
- if [ -n "$(git -C "$ROOT" status --porcelain 2>/dev/null)" ]; then
806
- DIRTY_STATE="dirty"
807
- else
808
- DIRTY_STATE="clean"
809
- fi
810
- fi
811
- fi
812
-
813
- STRICT_STOP_DIRTY="false"
814
- if [ -f "$IRON_LAWS_FILE" ]; then
815
- if command -v jq >/dev/null 2>&1; then
816
- STRICT_STOP_DIRTY=$(jq -r '
817
- if (.mode // "advisory") == "strict" then "true"
818
- elif ((.laws // []) | any(.id == "stop-clean-or-checkpointed" and .strict == true)) then "true"
819
- else "false"
820
- end
821
- ' "$IRON_LAWS_FILE" 2>/dev/null || echo "false")
822
- elif command -v python3 >/dev/null 2>&1; then
823
- STRICT_STOP_DIRTY=$(python3 - "$IRON_LAWS_FILE" <<'PY'
824
- import json
825
- import sys
826
- value = "false"
827
- try:
828
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
829
- parsed = json.load(fh)
830
- if str(parsed.get("mode", "advisory")) == "strict":
831
- value = "true"
832
- else:
833
- for row in parsed.get("laws", []):
834
- if isinstance(row, dict) and row.get("id") == "stop-clean-or-checkpointed" and row.get("strict") is True:
835
- value = "true"
836
- break
837
- except Exception:
838
- value = "false"
839
- print(value)
840
- PY
841
- )
842
- fi
843
- fi
844
-
845
- TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
846
- mkdir -p "$STATE_DIR" 2>/dev/null || true
847
- CHECKPOINT_WRITTEN=0
848
-
849
- cleanup_checkpoint_tmp() {
850
- rm -f "$CHECKPOINT_TMP" 2>/dev/null || true
851
- }
852
-
853
- acquire_checkpoint_lock() {
854
- local attempt=0
855
- while ! mkdir "$CHECKPOINT_LOCK_DIR" 2>/dev/null; do
856
- attempt=$((attempt + 1))
857
- if [ "$attempt" -ge 200 ]; then
858
- return 1
859
- fi
860
- sleep 0.02
861
- done
862
- return 0
863
- }
864
-
865
- release_checkpoint_lock() {
866
- rmdir "$CHECKPOINT_LOCK_DIR" 2>/dev/null || true
867
- }
868
-
869
- cleanup_checkpoint_state() {
870
- cleanup_checkpoint_tmp
871
- release_checkpoint_lock
872
- }
873
- trap cleanup_checkpoint_state EXIT INT TERM
874
-
875
- acquire_checkpoint_lock || exit 0
876
-
877
- if command -v jq >/dev/null 2>&1; then
878
- EXISTING_JSON="{}"
879
- if [ -f "$CHECKPOINT_FILE" ]; then
880
- EXISTING_JSON=$(jq -c '.' "$CHECKPOINT_FILE" 2>/dev/null || echo "{}")
881
- fi
882
-
883
- if jq -n \
884
- --argjson existing "$EXISTING_JSON" \
885
- --arg stage "$STAGE" \
886
- --arg run "$ACTIVE_RUN" \
887
- --arg ts "$TS" \
888
- --arg dirty "$DIRTY_STATE" \
889
- --arg harness "$HARNESS" \
890
- '
891
- ($existing | if type == "object" then . else {} end) as $base
892
- | $base + {
893
- stage: $stage,
894
- runId: $run,
895
- status: (if (($base.status // "") | tostring | length) > 0 then ($base.status | tostring) else "in_progress" end),
896
- dirtyState: $dirty,
897
- lastCompletedStep: (if ($base.lastCompletedStep | type) == "string" then $base.lastCompletedStep else "" end),
898
- remainingSteps: (if ($base.remainingSteps | type) == "array" then $base.remainingSteps else [] end),
899
- blockers: (if ($base.blockers | type) == "array" then $base.blockers else [] end),
900
- harness: $harness,
901
- timestamp: $ts
902
- }
903
- ' > "$CHECKPOINT_TMP" 2>/dev/null; then
904
- if mv "$CHECKPOINT_TMP" "$CHECKPOINT_FILE" 2>/dev/null; then
905
- CHECKPOINT_WRITTEN=1
906
- fi
907
- fi
908
- fi
909
-
910
- if [ "$CHECKPOINT_WRITTEN" -eq 0 ] && command -v python3 >/dev/null 2>&1; then
911
- if python3 - "$CHECKPOINT_FILE" "$CHECKPOINT_TMP" "$STAGE" "$ACTIVE_RUN" "$DIRTY_STATE" "$HARNESS" "$TS" <<'PY'
912
- import json
913
- import sys
914
- from pathlib import Path
915
-
916
- checkpoint_path, checkpoint_tmp_path, stage, run_id, dirty_state, harness, ts = sys.argv[1:8]
917
- checkpoint_file = Path(checkpoint_path)
918
- checkpoint_tmp_file = Path(checkpoint_tmp_path)
919
- payload = {}
920
- if checkpoint_file.exists():
921
- try:
922
- current = json.loads(checkpoint_file.read_text(encoding="utf-8"))
923
- if isinstance(current, dict):
924
- payload = dict(current)
925
- except Exception:
926
- payload = {}
927
-
928
- payload["stage"] = stage
929
- payload["runId"] = run_id
930
- payload["dirtyState"] = dirty_state
931
- payload["harness"] = harness
932
- payload["timestamp"] = ts
933
- if not isinstance(payload.get("status"), str) or not payload["status"].strip():
934
- payload["status"] = "in_progress"
935
- if not isinstance(payload.get("lastCompletedStep"), str):
936
- payload["lastCompletedStep"] = ""
937
- if not isinstance(payload.get("remainingSteps"), list):
938
- payload["remainingSteps"] = []
939
- if not isinstance(payload.get("blockers"), list):
940
- payload["blockers"] = []
941
-
942
- try:
943
- checkpoint_tmp_file.write_text(json.dumps(payload, indent=2) + "\\n", encoding="utf-8")
944
- except Exception:
945
- raise SystemExit(1)
946
- PY
947
- then
948
- if mv "$CHECKPOINT_TMP" "$CHECKPOINT_FILE" 2>/dev/null; then
949
- CHECKPOINT_WRITTEN=1
950
- fi
951
- fi
952
- fi
953
-
954
- if [ "$CHECKPOINT_WRITTEN" -eq 0 ]; then
955
- printf '{\n "stage": "%s",\n "runId": "%s",\n "status": "in_progress",\n "dirtyState": "%s",\n "lastCompletedStep": "",\n "remainingSteps": [],\n "blockers": [],\n "harness": "%s",\n "timestamp": "%s"\n}\n' \
956
- "$STAGE" "$ACTIVE_RUN" "$DIRTY_STATE" "$HARNESS" "$TS" > "$CHECKPOINT_TMP" 2>/dev/null || true
957
- if [ -s "$CHECKPOINT_TMP" ] && mv "$CHECKPOINT_TMP" "$CHECKPOINT_FILE" 2>/dev/null; then
958
- CHECKPOINT_WRITTEN=1
959
- fi
960
- fi
961
-
962
- cleanup_checkpoint_state
963
- trap - EXIT INT TERM
964
-
965
- CHECKPOINT_NOTE="Checkpoint updated at ${RUNTIME_ROOT}/state/checkpoint.json."
966
- if [ "$CHECKPOINT_WRITTEN" -eq 0 ]; then
967
- CHECKPOINT_NOTE="Checkpoint update failed. Review ${RUNTIME_ROOT}/state/checkpoint.json manually."
968
- fi
969
-
970
- if [ "$DIRTY_STATE" = "dirty" ] && [ "$STRICT_STOP_DIRTY" = "true" ]; then
971
- printf '[cclaw] Stop blocked by iron law "stop-clean-or-checkpointed": working tree is dirty. Commit/revert changes or update checkpoint blockers before ending the session.\\n' >&2
972
- exit 1
973
- fi
974
-
975
- RUN_SYNC_NOTE="Run metadata sync removed; active artifacts stay in ${RUNTIME_ROOT}/artifacts until /cc-ops archive (or cclaw archive runtime)."
976
-
977
- # --- Escape for JSON ---
978
- ${ESCAPE_FN}
979
- 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 current feature intent, (3) if you discovered a non-obvious rule/pattern, append one strict-schema JSON line to ${RUNTIME_ROOT}/knowledge.jsonl, (4) commit or revert pending changes.")
980
-
981
- # --- Output harness-specific JSON ---
982
- case "$HARNESS" in
983
- claude)
984
- printf '{"systemMessage":"%s"}\\n' "$MSG"
985
- ;;
986
- cursor)
987
- if [ "\${LOOP_COUNT:-0}" -eq 0 ]; then
988
- printf '{"followup_message":"%s"}\\n' "$MSG"
989
- else
990
- printf '{}\\n'
991
- fi
992
- ;;
993
- codex)
994
- printf '{"systemMessage":"%s"}\\n' "$MSG"
995
- ;;
996
- *)
997
- printf '{"systemMessage":"%s"}\\n' "$MSG"
998
- ;;
999
- esac
1000
- `;
1001
- }
1002
- export function runHookDispatcherScript() {
1003
- return `#!/usr/bin/env bash
1004
- # cclaw hook dispatcher — generated by cclaw sync
1005
- # Single entrypoint used by harness hook JSON wiring.
1006
- set -euo pipefail
1007
-
1008
- ${RUNTIME_SHELL_DETECT_ROOT}
1009
-
1010
- if [ "$#" -lt 1 ]; then
1011
- printf 'Usage: bash ${RUNTIME_ROOT}/hooks/run-hook.cmd <session-start|stop-checkpoint|pre-compact|prompt-guard|workflow-guard|context-monitor>\\n' >&2
1012
- exit 1
1013
- fi
1014
-
1015
- HOOK_NAME="$1"
1016
- shift || true
1017
-
1018
- case "$HOOK_NAME" in
1019
- session-start|session-start.sh)
1020
- HOOK_FILE="session-start.sh"
1021
- ;;
1022
- stop-checkpoint|stop-checkpoint.sh)
1023
- HOOK_FILE="stop-checkpoint.sh"
1024
- ;;
1025
- pre-compact|pre-compact.sh)
1026
- HOOK_FILE="pre-compact.sh"
1027
- ;;
1028
- prompt-guard|prompt-guard.sh)
1029
- HOOK_FILE="prompt-guard.sh"
1030
- ;;
1031
- workflow-guard|workflow-guard.sh)
1032
- HOOK_FILE="workflow-guard.sh"
1033
- ;;
1034
- context-monitor|context-monitor.sh)
1035
- HOOK_FILE="context-monitor.sh"
1036
- ;;
1037
- *)
1038
- printf '[cclaw] run-hook: unsupported hook "%s".\\n' "$HOOK_NAME" >&2
1039
- exit 1
1040
- ;;
1041
- esac
1042
-
1043
- HOOK_PATH="$ROOT/${RUNTIME_ROOT}/hooks/$HOOK_FILE"
1044
- if [ ! -f "$HOOK_PATH" ]; then
1045
- printf '[cclaw] run-hook: hook script not found at %s\\n' "$HOOK_PATH" >&2
1046
- exit 1
1047
- fi
1048
-
1049
- exec bash "$HOOK_PATH" "$@"
1050
- `;
1051
- }
1052
2
  export function stageCompleteScript() {
1053
- return `#!/usr/bin/env bash
1054
- # cclaw stage-complete helper — generated by cclaw sync
1055
- # Canonical helper for stage closeout: delegates validation + flow-state
1056
- # mutation to \`cclaw internal advance-stage\`.
1057
- set -euo pipefail
1058
-
1059
- ${RUNTIME_SHELL_DETECT_ROOT}
1060
-
1061
- if [ "$#" -lt 1 ]; then
1062
- printf 'Usage: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...]\\n' >&2
1063
- exit 1
1064
- fi
1065
-
1066
- if [ ! -d "$ROOT/${RUNTIME_ROOT}" ]; then
1067
- printf '[cclaw] stage-complete: runtime root not found at %s\\n' "$ROOT/${RUNTIME_ROOT}" >&2
1068
- exit 1
1069
- fi
1070
-
1071
- STAGE="$1"
1072
- shift || true
1073
-
1074
- if command -v cclaw >/dev/null 2>&1; then
1075
- exec cclaw internal advance-stage "$STAGE" "$@"
1076
- fi
1077
-
1078
- printf '[cclaw] stage-complete: cclaw binary not found in PATH. Install cclaw CLI and rerun stage completion.\\n' >&2
1079
- exit 1
1080
- `;
3
+ return `#!/usr/bin/env node
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { spawn, spawnSync } from "node:child_process";
8
+
9
+ const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
10
+
11
+ async function detectRoot() {
12
+ const candidates = [
13
+ process.env.CCLAW_PROJECT_ROOT,
14
+ process.env.CLAUDE_PROJECT_DIR,
15
+ process.env.CURSOR_PROJECT_DIR,
16
+ process.env.CURSOR_PROJECT_ROOT,
17
+ process.env.OPENCODE_PROJECT_DIR,
18
+ process.env.OPENCODE_PROJECT_ROOT,
19
+ process.cwd()
20
+ ].filter((value) => typeof value === "string" && value.length > 0);
21
+
22
+ for (const candidate of candidates) {
23
+ try {
24
+ const runtimePath = path.join(candidate, RUNTIME_ROOT);
25
+ const stat = await fs.stat(runtimePath);
26
+ if (stat.isDirectory()) return candidate;
27
+ } catch {
28
+ // continue
29
+ }
30
+ }
31
+ return candidates[0] || process.cwd();
1081
32
  }
1082
- export function preCompactScript() {
1083
- return `#!/usr/bin/env bash
1084
- # cclaw pre-compact hook — generated by cclaw sync
1085
- # Persists a session digest before the harness compacts/clears context, so the
1086
- # next session-start hook can restore the most important state without the agent
1087
- # having to re-derive it from scratch.
1088
- set -uo pipefail
1089
-
1090
- ${RUNTIME_SHELL_DETECT_ROOT}
1091
-
1092
- STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
1093
- STATE_FILE="$STATE_DIR/flow-state.json"
1094
- DELEGATION_FILE="$STATE_DIR/delegation-log.json"
1095
- KNOWLEDGE_FILE="$ROOT/${RUNTIME_ROOT}/knowledge.jsonl"
1096
- DIGEST_FILE="$STATE_DIR/session-digest.md"
1097
- DIGEST_TMP="$STATE_DIR/session-digest.md.tmp.$$"
1098
33
 
1099
- mkdir -p "$STATE_DIR" 2>/dev/null || true
1100
-
1101
- cleanup_digest_tmp() {
1102
- rm -f "$DIGEST_TMP" 2>/dev/null || true
34
+ function printUsage() {
35
+ process.stderr.write(
36
+ "Usage: node " +
37
+ RUNTIME_ROOT +
38
+ "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...]\\n"
39
+ );
1103
40
  }
1104
- trap cleanup_digest_tmp EXIT INT TERM
1105
-
1106
- STAGE="none"
1107
- TRACK="standard"
1108
- COMPLETED="0"
1109
- SKIPPED=""
1110
- ACTIVE_RUN="none"
1111
- PASSED_GATES=""
1112
- BLOCKED_GATES=""
1113
41
 
1114
- if [ -f "$STATE_FILE" ]; then
1115
- if command -v jq >/dev/null 2>&1; then
1116
- STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
1117
- TRACK=$(jq -r '.track // "standard"' "$STATE_FILE" 2>/dev/null || echo "standard")
1118
- COMPLETED=$(jq -r '(.completedStages // []) | length' "$STATE_FILE" 2>/dev/null || echo "0")
1119
- SKIPPED=$(jq -r '(.skippedStages // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
1120
- ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
1121
- PASSED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGateCatalog[$stage].passed // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
1122
- BLOCKED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGateCatalog[$stage].blocked // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
1123
- elif command -v python3 >/dev/null 2>&1; then
1124
- OUTPUT=$(python3 - "$STATE_FILE" <<'PY'
1125
- import json, sys
1126
- try:
1127
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
1128
- data = json.load(fh)
1129
- except Exception:
1130
- data = {}
1131
- stage = data.get("currentStage") or "none"
1132
- track = data.get("track") or "standard"
1133
- completed = data.get("completedStages") or []
1134
- skipped = data.get("skippedStages") or []
1135
- run = data.get("activeRunId") or "none"
1136
- gates = (data.get("stageGateCatalog") or {}).get(stage) or {}
1137
- passed = gates.get("passed") or []
1138
- blocked = gates.get("blocked") or []
1139
- print(stage)
1140
- print(track)
1141
- print(len(completed) if isinstance(completed, list) else 0)
1142
- print(",".join(skipped) if isinstance(skipped, list) else "")
1143
- print(run)
1144
- print(",".join(passed) if isinstance(passed, list) else "")
1145
- print(",".join(blocked) if isinstance(blocked, list) else "")
1146
- PY
1147
- )
42
+ async function main() {
43
+ const [, , stage, ...flags] = process.argv;
44
+ if (!stage || stage.trim().length === 0) {
45
+ printUsage();
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+
50
+ const root = await detectRoot();
51
+ const runtimePath = path.join(root, RUNTIME_ROOT);
52
+ try {
53
+ const stat = await fs.stat(runtimePath);
54
+ if (!stat.isDirectory()) throw new Error("not-dir");
55
+ } catch {
56
+ process.stderr.write("[cclaw] stage-complete: runtime root not found at " + runtimePath + "\\n");
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+
61
+ if (process.platform === "win32") {
62
+ const probe = spawnSync("where", ["cclaw"], {
63
+ cwd: root,
64
+ env: process.env,
65
+ stdio: "ignore"
66
+ });
67
+ if ((probe.status ?? 1) !== 0) {
68
+ process.stderr.write(
69
+ "[cclaw] stage-complete: cclaw binary not found in PATH. Install cclaw CLI and rerun stage completion.\\n"
70
+ );
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+ }
75
+
76
+ const isWindows = process.platform === "win32";
77
+ const child = spawn(
78
+ isWindows ? "cmd.exe" : "cclaw",
79
+ isWindows
80
+ ? ["/d", "/s", "/c", "cclaw", "internal", "advance-stage", stage, ...flags]
81
+ : ["internal", "advance-stage", stage, ...flags],
1148
82
  {
1149
- IFS= read -r STAGE
1150
- IFS= read -r TRACK
1151
- IFS= read -r COMPLETED
1152
- IFS= read -r SKIPPED
1153
- IFS= read -r ACTIVE_RUN
1154
- IFS= read -r PASSED_GATES
1155
- IFS= read -r BLOCKED_GATES
1156
- } <<EOF
1157
- $OUTPUT
1158
- EOF
1159
- fi
1160
- fi
1161
-
1162
- DELEGATION_PENDING=""
1163
- if [ -f "$DELEGATION_FILE" ] && command -v jq >/dev/null 2>&1; then
1164
- DELEGATION_PENDING=$(jq -r --arg stage "$STAGE" '
1165
- (.entries // [])
1166
- | map(select((.stage // "") == $stage and (.status // "") != "completed" and (.status // "") != "waived"))
1167
- | map(.agent // "unknown")
1168
- | unique
1169
- | join(",")
1170
- ' "$DELEGATION_FILE" 2>/dev/null || echo "")
1171
- fi
1172
-
1173
- KNOWLEDGE_TAIL=""
1174
- if [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
1175
- KNOWLEDGE_TAIL=$(tail -n 12 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
1176
- fi
1177
-
1178
- GIT_HEAD=""
1179
- GIT_BRANCH=""
1180
- GIT_DIRTY="unknown"
1181
- if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
1182
- GIT_HEAD=$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo "")
1183
- GIT_BRANCH=$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
1184
- if [ -n "$(git -C "$ROOT" status --porcelain 2>/dev/null)" ]; then
1185
- GIT_DIRTY="dirty"
1186
- else
1187
- GIT_DIRTY="clean"
1188
- fi
1189
- fi
1190
-
1191
- TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
1192
-
1193
- {
1194
- printf '# Session Digest\n'
1195
- printf '_Generated by pre-compact hook at %s_\n\n' "$TS"
1196
- printf '## Flow snapshot\n'
1197
- printf -- '- track: %s\n' "$TRACK"
1198
- printf -- '- current stage: %s\n' "$STAGE"
1199
- printf -- '- completed: %s stages\n' "$COMPLETED"
1200
- printf -- '- skipped: %s\n' "\${SKIPPED:-(none)}"
1201
- printf -- '- run: %s\n\n' "$ACTIVE_RUN"
1202
- printf '## Gates (current stage)\n'
1203
- printf -- '- passed: %s\n' "\${PASSED_GATES:-(none)}"
1204
- printf -- '- blocked: %s\n\n' "\${BLOCKED_GATES:-(none)}"
1205
- printf '## Outstanding delegations\n'
1206
- printf -- '- pending: %s\n\n' "\${DELEGATION_PENDING:-(none)}"
1207
- printf '## Git\n'
1208
- printf -- '- branch: %s\n' "\${GIT_BRANCH:-(unknown)}"
1209
- printf -- '- head: %s\n' "\${GIT_HEAD:-(unknown)}"
1210
- printf -- '- worktree: %s\n\n' "$GIT_DIRTY"
1211
- if [ -n "$KNOWLEDGE_TAIL" ]; then
1212
- printf '## Knowledge tail\n'
1213
- printf '%s\n' "$KNOWLEDGE_TAIL"
1214
- fi
1215
- } > "$DIGEST_TMP" 2>/dev/null || true
1216
-
1217
- if [ -s "$DIGEST_TMP" ]; then
1218
- mv "$DIGEST_TMP" "$DIGEST_FILE" 2>/dev/null || rm -f "$DIGEST_TMP" 2>/dev/null || true
1219
- fi
83
+ cwd: root,
84
+ env: process.env,
85
+ stdio: "inherit"
86
+ }
87
+ );
88
+ let spawnErrored = false;
89
+
90
+ child.on("error", (error) => {
91
+ spawnErrored = true;
92
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
93
+ if (code === "ENOENT") {
94
+ process.stderr.write(
95
+ "[cclaw] stage-complete: cclaw binary not found in PATH. Install cclaw CLI and rerun stage completion.\\n"
96
+ );
97
+ } else {
98
+ process.stderr.write(
99
+ "[cclaw] stage-complete: failed to invoke cclaw internal advance-stage (" +
100
+ (error instanceof Error ? error.message : String(error)) +
101
+ ").\\n"
102
+ );
103
+ }
104
+ process.exitCode = 1;
105
+ });
106
+
107
+ child.on("close", (code, signal) => {
108
+ if (spawnErrored) {
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ if (signal) {
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ process.exitCode = typeof code === "number" && code >= 0 ? code : 1;
117
+ });
118
+ }
1220
119
 
1221
- trap - EXIT INT TERM
1222
- exit 0
120
+ void main();
1223
121
  `;
1224
122
  }
1225
- // ---------------------------------------------------------------------------
1226
- // hooks.json generators are defined in observe.ts (shared across harnesses).
1227
- // ---------------------------------------------------------------------------
1228
123
  export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
1229
124
  export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
1230
125
  export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
1231
126
  export { nodeHookRuntimeScript } from "./node-hooks.js";
1232
- // ---------------------------------------------------------------------------
1233
- // OpenCode plugin — JS module
1234
- // ---------------------------------------------------------------------------
1235
127
  export { opencodePluginJs } from "./opencode-plugin.js";