cclaw-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/dist/cli.d.ts +10 -0
  4. package/dist/cli.js +101 -0
  5. package/dist/config.d.ts +5 -0
  6. package/dist/config.js +70 -0
  7. package/dist/constants.d.ts +12 -0
  8. package/dist/constants.js +50 -0
  9. package/dist/content/agents.d.ts +39 -0
  10. package/dist/content/agents.js +244 -0
  11. package/dist/content/autoplan.d.ts +7 -0
  12. package/dist/content/autoplan.js +297 -0
  13. package/dist/content/contracts.d.ts +2 -0
  14. package/dist/content/contracts.js +50 -0
  15. package/dist/content/examples.d.ts +2 -0
  16. package/dist/content/examples.js +327 -0
  17. package/dist/content/hooks.d.ts +16 -0
  18. package/dist/content/hooks.js +753 -0
  19. package/dist/content/learnings.d.ts +5 -0
  20. package/dist/content/learnings.js +265 -0
  21. package/dist/content/meta-skill.d.ts +10 -0
  22. package/dist/content/meta-skill.js +137 -0
  23. package/dist/content/observe.d.ts +21 -0
  24. package/dist/content/observe.js +1110 -0
  25. package/dist/content/session-hooks.d.ts +7 -0
  26. package/dist/content/session-hooks.js +137 -0
  27. package/dist/content/skills.d.ts +3 -0
  28. package/dist/content/skills.js +257 -0
  29. package/dist/content/stage-schema.d.ts +78 -0
  30. package/dist/content/stage-schema.js +1453 -0
  31. package/dist/content/subagents.d.ts +13 -0
  32. package/dist/content/subagents.js +616 -0
  33. package/dist/content/templates.d.ts +3 -0
  34. package/dist/content/templates.js +272 -0
  35. package/dist/content/utility-skills.d.ts +12 -0
  36. package/dist/content/utility-skills.js +467 -0
  37. package/dist/doctor.d.ts +7 -0
  38. package/dist/doctor.js +610 -0
  39. package/dist/flow-state.d.ts +19 -0
  40. package/dist/flow-state.js +41 -0
  41. package/dist/fs-utils.d.ts +5 -0
  42. package/dist/fs-utils.js +28 -0
  43. package/dist/gitignore.d.ts +3 -0
  44. package/dist/gitignore.js +43 -0
  45. package/dist/harness-adapters.d.ts +12 -0
  46. package/dist/harness-adapters.js +175 -0
  47. package/dist/install.d.ts +9 -0
  48. package/dist/install.js +562 -0
  49. package/dist/learnings-summarizer.d.ts +25 -0
  50. package/dist/learnings-summarizer.js +201 -0
  51. package/dist/logger.d.ts +3 -0
  52. package/dist/logger.js +6 -0
  53. package/dist/policy.d.ts +6 -0
  54. package/dist/policy.js +179 -0
  55. package/dist/runs.d.ts +18 -0
  56. package/dist/runs.js +446 -0
  57. package/dist/types.d.ts +19 -0
  58. package/dist/types.js +12 -0
  59. package/package.json +47 -0
@@ -0,0 +1,753 @@
1
+ /**
2
+ * Hook generators for all supported harnesses.
3
+ *
4
+ * SessionStart: injects using-cclaw + flow state + learnings + checkpoint/activity summary.
5
+ * Stop: writes checkpoint.json and reminds about flow consistency.
6
+ * PreToolUse/PostToolUse and summarize chain are generated in observe.ts.
7
+ */
8
+ 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 DETECT_ROOT = `HARNESS="codex"
19
+ if [ -n "\${CLAUDE_PROJECT_DIR:-}" ]; then
20
+ HARNESS="claude"
21
+ elif [ -n "\${CURSOR_PROJECT_DIR:-}" ] || [ -n "\${CURSOR_PROJECT_ROOT:-}" ]; then
22
+ HARNESS="cursor"
23
+ elif [ -n "\${OPENCODE_PROJECT_DIR:-}" ] || [ -n "\${OPENCODE_PROJECT_ROOT:-}" ]; then
24
+ HARNESS="opencode"
25
+ fi
26
+
27
+ ROOT=""
28
+ for candidate in "\${CCLAW_PROJECT_ROOT:-}" "\${CLAUDE_PROJECT_DIR:-}" "\${CURSOR_PROJECT_DIR:-}" "\${CURSOR_PROJECT_ROOT:-}" "\${OPENCODE_PROJECT_DIR:-}" "\${OPENCODE_PROJECT_ROOT:-}" "\${PWD:-}"; do
29
+ if [ -n "$candidate" ] && [ -d "$candidate/${RUNTIME_ROOT}" ]; then
30
+ ROOT="$candidate"
31
+ break
32
+ fi
33
+ done
34
+ if [ -z "$ROOT" ]; then
35
+ ROOT="\${CCLAW_PROJECT_ROOT:-\${CLAUDE_PROJECT_DIR:-\${CURSOR_PROJECT_DIR:-\${CURSOR_PROJECT_ROOT:-\${OPENCODE_PROJECT_DIR:-\${OPENCODE_PROJECT_ROOT:-\${PWD}}}}}}}"
36
+ fi`;
37
+ /** Shared bash preamble for generated hook scripts. */
38
+ export const RUNTIME_SHELL_DETECT_ROOT = DETECT_ROOT;
39
+ export function sessionStartScript() {
40
+ return `#!/usr/bin/env bash
41
+ # cclaw session-start hook — generated by cclaw sync
42
+ # Injects using-cclaw + flow status + run pointer + top learnings + checkpoint/activity summary.
43
+ set -euo pipefail
44
+
45
+ ${DETECT_ROOT}
46
+
47
+ STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
48
+ CHECKPOINT_FILE="$ROOT/${RUNTIME_ROOT}/state/checkpoint.json"
49
+ ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
50
+ SUGGESTION_MEMORY_FILE="$ROOT/${RUNTIME_ROOT}/state/suggestion-memory.json"
51
+ CONTEXT_WARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/state/context-warnings.jsonl"
52
+ LEARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/learnings.jsonl"
53
+ META_SKILL="$ROOT/${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"
54
+
55
+ # --- Read flow state ---
56
+ STAGE="none"
57
+ COMPLETED="0"
58
+ ACTIVE_RUN="none"
59
+ if [ -f "$STATE_FILE" ]; then
60
+ if command -v jq >/dev/null 2>&1; then
61
+ STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
62
+ COMPLETED=$(jq -r '(.completedStages | length) // 0' "$STATE_FILE" 2>/dev/null || echo "0")
63
+ ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
64
+ else
65
+ if command -v python3 >/dev/null 2>&1; then
66
+ STAGE=$(python3 - "$STATE_FILE" <<'PY'
67
+ import json
68
+ import sys
69
+
70
+ stage = "none"
71
+ try:
72
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
73
+ data = json.load(fh)
74
+ value = data.get("currentStage")
75
+ if isinstance(value, str) and value:
76
+ stage = value
77
+ except Exception:
78
+ pass
79
+ print(stage)
80
+ PY
81
+ )
82
+ COMPLETED=$(python3 - "$STATE_FILE" <<'PY'
83
+ import json
84
+ import sys
85
+
86
+ count = 0
87
+ try:
88
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
89
+ data = json.load(fh)
90
+ value = data.get("completedStages")
91
+ if isinstance(value, list):
92
+ count = len(value)
93
+ except Exception:
94
+ pass
95
+ print(count)
96
+ PY
97
+ )
98
+ ACTIVE_RUN=$(python3 - "$STATE_FILE" <<'PY'
99
+ import json
100
+ import sys
101
+
102
+ run = "none"
103
+ try:
104
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
105
+ data = json.load(fh)
106
+ value = data.get("activeRunId")
107
+ if isinstance(value, str) and value:
108
+ run = value
109
+ except Exception:
110
+ pass
111
+ print(run)
112
+ PY
113
+ )
114
+ else
115
+ STAGE=$(grep -o '"currentStage"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
116
+ COMPLETED_RAW=$(grep -o '"completedStages"[[:space:]]*:[[:space:]]*\\[[^]]*\\]' "$STATE_FILE" 2>/dev/null | head -1 || echo "")
117
+ if [ -n "$COMPLETED_RAW" ]; then
118
+ COMPLETED=$(printf '%s' "$COMPLETED_RAW" | grep -o '"[^"]*"' | wc -l | tr -d ' ' 2>/dev/null || echo "0")
119
+ else
120
+ COMPLETED="0"
121
+ fi
122
+ ACTIVE_RUN=$(grep -o '"activeRunId"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
123
+ fi
124
+ fi
125
+ fi
126
+
127
+ # --- Checkpoint summary ---
128
+ CHECKPOINT_SUMMARY=""
129
+ if [ -f "$CHECKPOINT_FILE" ]; then
130
+ if command -v jq >/dev/null 2>&1; then
131
+ CHECKPOINT_SUMMARY=$(jq -r '"Checkpoint: stage=" + (.stage // "none") + ", status=" + (.status // "unknown") + ", run=" + (.runId // "none") + ", at=" + (.timestamp // "unknown")' "$CHECKPOINT_FILE" 2>/dev/null || echo "")
132
+ elif command -v python3 >/dev/null 2>&1; then
133
+ CHECKPOINT_SUMMARY=$(python3 - "$CHECKPOINT_FILE" <<'PY'
134
+ import json
135
+ import sys
136
+
137
+ try:
138
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
139
+ data = json.load(fh)
140
+ stage = data.get("stage", "none")
141
+ status = data.get("status", "unknown")
142
+ run_id = data.get("runId", "none")
143
+ ts = data.get("timestamp", "unknown")
144
+ print(f"Checkpoint: stage={stage}, status={status}, run={run_id}, at={ts}")
145
+ except Exception:
146
+ print("")
147
+ PY
148
+ )
149
+ fi
150
+ fi
151
+
152
+ # --- Recent stage activity summary ---
153
+ ACTIVITY_SUMMARY=""
154
+ if [ -f "$ACTIVITY_FILE" ] && [ -s "$ACTIVITY_FILE" ]; then
155
+ if command -v jq >/dev/null 2>&1; then
156
+ ACTIVITY_SUMMARY=$(tail -n 5 "$ACTIVITY_FILE" 2>/dev/null | jq -r -s '
157
+ map(select(type=="object"))
158
+ | map("- " + (.ts // "unknown") + " [" + (.phase // "unknown") + "] " + (.tool // "unknown") + " (stage=" + (.stage // "unknown") + ", run=" + (.runId // "none") + ")")
159
+ | join("\\n")
160
+ ' 2>/dev/null || echo "")
161
+ else
162
+ ACTIVITY_SUMMARY=$(tail -n 3 "$ACTIVITY_FILE" 2>/dev/null || echo "")
163
+ fi
164
+ fi
165
+
166
+ # --- Latest context warning ---
167
+ CONTEXT_WARNING=""
168
+ if [ -f "$CONTEXT_WARNINGS_FILE" ] && [ -s "$CONTEXT_WARNINGS_FILE" ]; then
169
+ if command -v jq >/dev/null 2>&1; then
170
+ CONTEXT_WARNING=$(tail -n 1 "$CONTEXT_WARNINGS_FILE" 2>/dev/null | jq -r '.note // ""' 2>/dev/null || echo "")
171
+ else
172
+ CONTEXT_WARNING=$(tail -n 1 "$CONTEXT_WARNINGS_FILE" 2>/dev/null || echo "")
173
+ fi
174
+ fi
175
+
176
+ # --- Proactive stage suggestion memory (persistent opt-out) ---
177
+ SUGGESTIONS_ENABLED="true"
178
+ STAGE_MUTED="false"
179
+ if [ -f "$SUGGESTION_MEMORY_FILE" ]; then
180
+ if command -v jq >/dev/null 2>&1; then
181
+ SUGGESTIONS_ENABLED=$(jq -r 'if (.enabled // true) then "true" else "false" end' "$SUGGESTION_MEMORY_FILE" 2>/dev/null || echo "true")
182
+ 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")
183
+ elif command -v python3 >/dev/null 2>&1; then
184
+ SUGGESTIONS_ENABLED=$(python3 - "$SUGGESTION_MEMORY_FILE" <<'PY'
185
+ import json
186
+ import sys
187
+ try:
188
+ with open(sys.argv[1], "r", encoding="utf-8") as handle:
189
+ value = json.load(handle)
190
+ enabled = value.get("enabled", True)
191
+ print("true" if enabled else "false")
192
+ except Exception:
193
+ print("true")
194
+ PY
195
+ )
196
+ STAGE_MUTED=$(python3 - "$SUGGESTION_MEMORY_FILE" "$STAGE" <<'PY'
197
+ import json
198
+ import sys
199
+ try:
200
+ with open(sys.argv[1], "r", encoding="utf-8") as handle:
201
+ value = json.load(handle)
202
+ muted = value.get("mutedStages", [])
203
+ print("true" if isinstance(muted, list) and sys.argv[2] in muted else "false")
204
+ except Exception:
205
+ print("false")
206
+ PY
207
+ )
208
+ fi
209
+ fi
210
+
211
+ STAGE_SUGGESTION=""
212
+ if [ "$SUGGESTIONS_ENABLED" = "true" ] && [ "$STAGE_MUTED" != "true" ]; then
213
+ case "$STAGE" in
214
+ brainstorm) STAGE_SUGGESTION="Suggestion: list 2-3 alternatives and ask a single focused clarifying question before direction lock." ;;
215
+ scope) STAGE_SUGGESTION="Suggestion: lock explicit in-scope/out-of-scope boundaries and choose one scope mode." ;;
216
+ design) STAGE_SUGGESTION="Suggestion: map failure modes per new codepath and confirm architecture boundaries before moving forward." ;;
217
+ spec) STAGE_SUGGESTION="Suggestion: ensure every acceptance criterion is measurable and mapped to a concrete test." ;;
218
+ plan) STAGE_SUGGESTION="Suggestion: group tasks into dependency waves and keep WAIT_FOR_CONFIRM pending until approval." ;;
219
+ test) STAGE_SUGGESTION="Suggestion: RED only in this stage — capture failing output for each selected slice." ;;
220
+ build) STAGE_SUGGESTION="Suggestion: apply minimal GREEN change, run full suite, then document REFACTOR notes." ;;
221
+ review) STAGE_SUGGESTION="Suggestion: run Layer 1 before Layer 2 and reconcile findings into 07-review-army.json." ;;
222
+ ship) STAGE_SUGGESTION="Suggestion: verify preflight + rollback plan before selecting exactly one finalization mode." ;;
223
+ *) STAGE_SUGGESTION="" ;;
224
+ esac
225
+ fi
226
+
227
+ if [ -n "$STAGE_SUGGESTION" ]; then
228
+ NOW_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
229
+ TMP_SUGGESTION_FILE="$SUGGESTION_MEMORY_FILE.tmp.$$"
230
+ if command -v jq >/dev/null 2>&1 && [ -f "$SUGGESTION_MEMORY_FILE" ]; then
231
+ jq --arg stage "$STAGE" --arg ts "$NOW_TS" '
232
+ .lastSuggestedStage = $stage
233
+ | .lastSuggestedAt = $ts
234
+ | .enabled = (.enabled // true)
235
+ | .mutedStages = (if (.mutedStages | type) == "array" then .mutedStages else [] end)
236
+ ' "$SUGGESTION_MEMORY_FILE" > "$TMP_SUGGESTION_FILE" 2>/dev/null || true
237
+ elif command -v python3 >/dev/null 2>&1; then
238
+ python3 - "$SUGGESTION_MEMORY_FILE" "$TMP_SUGGESTION_FILE" "$STAGE" "$NOW_TS" <<'PY'
239
+ import json
240
+ import sys
241
+ from pathlib import Path
242
+
243
+ source = Path(sys.argv[1])
244
+ target = Path(sys.argv[2])
245
+ stage = sys.argv[3]
246
+ ts = sys.argv[4]
247
+ payload = {"enabled": True, "mutedStages": []}
248
+ if source.exists():
249
+ try:
250
+ parsed = json.loads(source.read_text(encoding="utf-8"))
251
+ if isinstance(parsed, dict):
252
+ payload.update(parsed)
253
+ except Exception:
254
+ pass
255
+ if not isinstance(payload.get("mutedStages"), list):
256
+ payload["mutedStages"] = []
257
+ if not isinstance(payload.get("enabled"), bool):
258
+ payload["enabled"] = True
259
+ payload["lastSuggestedStage"] = stage
260
+ payload["lastSuggestedAt"] = ts
261
+ target.write_text(json.dumps(payload, indent=2) + "\\n", encoding="utf-8")
262
+ PY
263
+ fi
264
+ if [ -s "$TMP_SUGGESTION_FILE" ]; then
265
+ mv "$TMP_SUGGESTION_FILE" "$SUGGESTION_MEMORY_FILE" 2>/dev/null || rm -f "$TMP_SUGGESTION_FILE" 2>/dev/null || true
266
+ fi
267
+ fi
268
+
269
+ # --- Read meta-skill (full file) ---
270
+ META_CONTENT=""
271
+ if [ -f "$META_SKILL" ]; then
272
+ META_CONTENT=$(cat "$META_SKILL" 2>/dev/null || echo "")
273
+ fi
274
+
275
+ # --- Read top learnings with decay ---
276
+ LEARNINGS_SUMMARY=""
277
+ if [ -f "$LEARNINGS_FILE" ] && [ -s "$LEARNINGS_FILE" ]; then
278
+ if command -v jq >/dev/null 2>&1; then
279
+ NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
280
+ LEARNINGS_SUMMARY=$(tail -n 30 "$LEARNINGS_FILE" 2>/dev/null | jq -r -s --arg now "$NOW_EPOCH" '
281
+ [.[] | select(.key != null)]
282
+ | group_by([.key, .type])
283
+ | map(sort_by(.ts) | last)
284
+ | map(
285
+ . as $e |
286
+ (if $e.source == "user-stated" then 0
287
+ else (try ((($now|tonumber) - ($e.ts // "" | fromdateiso8601)) / 2592000 | floor) catch 0)
288
+ end) as $months |
289
+ ($e.confidence - $months) as $eff |
290
+ . + {effective_confidence: (if $eff < 0 then 0 else $eff end)}
291
+ )
292
+ | sort_by(-.effective_confidence)
293
+ | .[0:3]
294
+ | map("- " + .key + " (conf " + (.effective_confidence|tostring) + "): " + .insight)
295
+ | join("\\n")
296
+ ' 2>/dev/null || echo "")
297
+ else
298
+ LEARNINGS_SUMMARY=$(tail -n 3 "$LEARNINGS_FILE" 2>/dev/null || echo "")
299
+ fi
300
+ fi
301
+
302
+ # --- Build context message ---
303
+ CTX="cclaw loaded. Flow: stage=$STAGE ($COMPLETED/9 completed, run=$ACTIVE_RUN). Active run artifacts: ${RUNTIME_ROOT}/runs/$ACTIVE_RUN/artifacts/"
304
+ if [ -n "$CHECKPOINT_SUMMARY" ]; then
305
+ CTX="$CTX
306
+ $CHECKPOINT_SUMMARY"
307
+ fi
308
+ if [ -n "$ACTIVITY_SUMMARY" ]; then
309
+ CTX="$CTX
310
+ Recent stage activity:
311
+ $ACTIVITY_SUMMARY"
312
+ fi
313
+ if [ -n "$CONTEXT_WARNING" ]; then
314
+ CTX="$CTX
315
+ Latest context warning:
316
+ $CONTEXT_WARNING"
317
+ fi
318
+ if [ -n "$STAGE_SUGGESTION" ]; then
319
+ CTX="$CTX
320
+ $STAGE_SUGGESTION
321
+ To disable suggestions persistently set ${RUNTIME_ROOT}/state/suggestion-memory.json -> enabled=false."
322
+ fi
323
+ if [ -n "$LEARNINGS_SUMMARY" ]; then
324
+ CTX="$CTX
325
+ Top learnings:
326
+ $LEARNINGS_SUMMARY"
327
+ fi
328
+ if [ -n "$META_CONTENT" ]; then
329
+ CTX="$CTX
330
+
331
+ $META_CONTENT"
332
+ fi
333
+
334
+ # --- Escape for JSON ---
335
+ ${ESCAPE_FN}
336
+ MSG=$(escape_json "$CTX")
337
+
338
+ # --- Output harness-specific JSON ---
339
+ case "$HARNESS" in
340
+ claude)
341
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$MSG"
342
+ ;;
343
+ cursor)
344
+ printf '{"additional_context":"%s"}\\n' "$MSG"
345
+ ;;
346
+ codex)
347
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$MSG"
348
+ ;;
349
+ *)
350
+ printf '{"additional_context":"%s"}\\n' "$MSG"
351
+ ;;
352
+ esac
353
+ `;
354
+ }
355
+ export function stopCheckpointScript() {
356
+ return `#!/usr/bin/env bash
357
+ # cclaw stop hook — generated by cclaw sync
358
+ # Writes checkpoint state and reminds agent about flow/session consistency.
359
+ set -euo pipefail
360
+
361
+ ${DETECT_ROOT}
362
+
363
+ INPUT=$(cat 2>/dev/null || echo '{}')
364
+
365
+ STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
366
+ STATE_FILE="$STATE_DIR/flow-state.json"
367
+ CHECKPOINT_FILE="$STATE_DIR/checkpoint.json"
368
+ CHECKPOINT_TMP="$STATE_DIR/checkpoint.json.tmp.$$"
369
+ STAGE="none"
370
+ ACTIVE_RUN="none"
371
+ LOOP_COUNT=""
372
+
373
+ if command -v jq >/dev/null 2>&1; then
374
+ LOOP_COUNT=$(echo "$INPUT" | jq -r '.loop_count // 0' 2>/dev/null || echo "")
375
+ elif command -v python3 >/dev/null 2>&1; then
376
+ LOOP_COUNT=$(INPUT_JSON="$INPUT" python3 - <<'PY'
377
+ import json
378
+ import os
379
+
380
+ try:
381
+ data = json.loads(os.environ.get("INPUT_JSON", "{}"))
382
+ print(data.get("loop_count", 0))
383
+ except Exception:
384
+ print("")
385
+ PY
386
+ )
387
+ fi
388
+ [ -n "$LOOP_COUNT" ] || LOOP_COUNT="0"
389
+
390
+ if [ -f "$STATE_FILE" ]; then
391
+ if command -v jq >/dev/null 2>&1; then
392
+ STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
393
+ ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
394
+ elif command -v python3 >/dev/null 2>&1; then
395
+ STAGE=$(python3 - "$STATE_FILE" <<'PY'
396
+ import json
397
+ import sys
398
+
399
+ stage = "none"
400
+ try:
401
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
402
+ data = json.load(fh)
403
+ value = data.get("currentStage")
404
+ if isinstance(value, str) and value:
405
+ stage = value
406
+ except Exception:
407
+ pass
408
+ print(stage)
409
+ PY
410
+ )
411
+ ACTIVE_RUN=$(python3 - "$STATE_FILE" <<'PY'
412
+ import json
413
+ import sys
414
+
415
+ run = "none"
416
+ try:
417
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
418
+ data = json.load(fh)
419
+ value = data.get("activeRunId")
420
+ if isinstance(value, str) and value:
421
+ run = value
422
+ except Exception:
423
+ pass
424
+ print(run)
425
+ PY
426
+ )
427
+ fi
428
+ fi
429
+
430
+ DIRTY_STATE="unknown"
431
+ if command -v git >/dev/null 2>&1; then
432
+ if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
433
+ if [ -n "$(git -C "$ROOT" status --porcelain 2>/dev/null)" ]; then
434
+ DIRTY_STATE="dirty"
435
+ else
436
+ DIRTY_STATE="clean"
437
+ fi
438
+ fi
439
+ fi
440
+
441
+ TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
442
+ mkdir -p "$STATE_DIR" 2>/dev/null || true
443
+ CHECKPOINT_WRITTEN=0
444
+
445
+ cleanup_checkpoint_tmp() {
446
+ rm -f "$CHECKPOINT_TMP" 2>/dev/null || true
447
+ }
448
+ trap cleanup_checkpoint_tmp EXIT INT TERM
449
+
450
+ if command -v jq >/dev/null 2>&1; then
451
+ EXISTING_JSON="{}"
452
+ if [ -f "$CHECKPOINT_FILE" ]; then
453
+ EXISTING_JSON=$(jq -c '.' "$CHECKPOINT_FILE" 2>/dev/null || echo "{}")
454
+ fi
455
+
456
+ if jq -n \
457
+ --argjson existing "$EXISTING_JSON" \
458
+ --arg stage "$STAGE" \
459
+ --arg run "$ACTIVE_RUN" \
460
+ --arg ts "$TS" \
461
+ --arg dirty "$DIRTY_STATE" \
462
+ --arg harness "$HARNESS" \
463
+ '
464
+ ($existing | if type == "object" then . else {} end) as $base
465
+ | $base + {
466
+ stage: $stage,
467
+ runId: $run,
468
+ status: (if (($base.status // "") | tostring | length) > 0 then ($base.status | tostring) else "in_progress" end),
469
+ dirtyState: $dirty,
470
+ lastCompletedStep: (if ($base.lastCompletedStep | type) == "string" then $base.lastCompletedStep else "" end),
471
+ remainingSteps: (if ($base.remainingSteps | type) == "array" then $base.remainingSteps else [] end),
472
+ blockers: (if ($base.blockers | type) == "array" then $base.blockers else [] end),
473
+ harness: $harness,
474
+ timestamp: $ts
475
+ }
476
+ ' > "$CHECKPOINT_TMP" 2>/dev/null; then
477
+ if mv "$CHECKPOINT_TMP" "$CHECKPOINT_FILE" 2>/dev/null; then
478
+ CHECKPOINT_WRITTEN=1
479
+ fi
480
+ fi
481
+ fi
482
+
483
+ if [ "$CHECKPOINT_WRITTEN" -eq 0 ] && command -v python3 >/dev/null 2>&1; then
484
+ if python3 - "$CHECKPOINT_FILE" "$CHECKPOINT_TMP" "$STAGE" "$ACTIVE_RUN" "$DIRTY_STATE" "$HARNESS" "$TS" <<'PY'
485
+ import json
486
+ import sys
487
+ from pathlib import Path
488
+
489
+ checkpoint_path, checkpoint_tmp_path, stage, run_id, dirty_state, harness, ts = sys.argv[1:8]
490
+ checkpoint_file = Path(checkpoint_path)
491
+ checkpoint_tmp_file = Path(checkpoint_tmp_path)
492
+ payload = {}
493
+ if checkpoint_file.exists():
494
+ try:
495
+ current = json.loads(checkpoint_file.read_text(encoding="utf-8"))
496
+ if isinstance(current, dict):
497
+ payload = dict(current)
498
+ except Exception:
499
+ payload = {}
500
+
501
+ payload["stage"] = stage
502
+ payload["runId"] = run_id
503
+ payload["dirtyState"] = dirty_state
504
+ payload["harness"] = harness
505
+ payload["timestamp"] = ts
506
+ if not isinstance(payload.get("status"), str) or not payload["status"].strip():
507
+ payload["status"] = "in_progress"
508
+ if not isinstance(payload.get("lastCompletedStep"), str):
509
+ payload["lastCompletedStep"] = ""
510
+ if not isinstance(payload.get("remainingSteps"), list):
511
+ payload["remainingSteps"] = []
512
+ if not isinstance(payload.get("blockers"), list):
513
+ payload["blockers"] = []
514
+
515
+ try:
516
+ checkpoint_tmp_file.write_text(json.dumps(payload, indent=2) + "\\n", encoding="utf-8")
517
+ except Exception:
518
+ raise SystemExit(1)
519
+ PY
520
+ then
521
+ if mv "$CHECKPOINT_TMP" "$CHECKPOINT_FILE" 2>/dev/null; then
522
+ CHECKPOINT_WRITTEN=1
523
+ fi
524
+ fi
525
+ fi
526
+
527
+ if [ "$CHECKPOINT_WRITTEN" -eq 0 ]; then
528
+ 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' \
529
+ "$STAGE" "$ACTIVE_RUN" "$DIRTY_STATE" "$HARNESS" "$TS" > "$CHECKPOINT_TMP" 2>/dev/null || true
530
+ if [ -s "$CHECKPOINT_TMP" ] && mv "$CHECKPOINT_TMP" "$CHECKPOINT_FILE" 2>/dev/null; then
531
+ CHECKPOINT_WRITTEN=1
532
+ fi
533
+ fi
534
+
535
+ cleanup_checkpoint_tmp
536
+ trap - EXIT INT TERM
537
+
538
+ CHECKPOINT_NOTE="Checkpoint updated at ${RUNTIME_ROOT}/state/checkpoint.json."
539
+ if [ "$CHECKPOINT_WRITTEN" -eq 0 ]; then
540
+ CHECKPOINT_NOTE="Checkpoint update failed. Review ${RUNTIME_ROOT}/state/checkpoint.json manually."
541
+ fi
542
+
543
+ # --- Escape for JSON ---
544
+ ${ESCAPE_FN}
545
+ 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.")
546
+
547
+ # --- Output harness-specific JSON ---
548
+ case "$HARNESS" in
549
+ claude)
550
+ printf '{"systemMessage":"%s"}\\n' "$MSG"
551
+ ;;
552
+ cursor)
553
+ if [ "\${LOOP_COUNT:-0}" -eq 0 ]; then
554
+ printf '{"followup_message":"%s"}\\n' "$MSG"
555
+ else
556
+ printf '{}\\n'
557
+ fi
558
+ ;;
559
+ codex)
560
+ printf '{"systemMessage":"%s"}\\n' "$MSG"
561
+ ;;
562
+ *)
563
+ printf '{"systemMessage":"%s"}\\n' "$MSG"
564
+ ;;
565
+ esac
566
+ `;
567
+ }
568
+ // ---------------------------------------------------------------------------
569
+ // hooks.json generators — NOW use observe.ts versions with PreToolUse/PostToolUse
570
+ // These are kept as fallbacks for when observation is disabled.
571
+ // ---------------------------------------------------------------------------
572
+ export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
573
+ export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
574
+ export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
575
+ // ---------------------------------------------------------------------------
576
+ // OpenCode plugin — JS module
577
+ // ---------------------------------------------------------------------------
578
+ export function opencodePluginJs() {
579
+ return `// cclaw OpenCode plugin — generated by cclaw sync
580
+ import { closeSync, fstatSync, openSync, readFileSync, readSync } from "node:fs";
581
+ import { join } from "node:path";
582
+
583
+ export default function cclawPlugin(ctx) {
584
+ const root = ctx.directory || process.cwd();
585
+ const checkpointPath = join(root, "${RUNTIME_ROOT}/state/checkpoint.json");
586
+ const activityPath = join(root, "${RUNTIME_ROOT}/state/stage-activity.jsonl");
587
+
588
+ function readFlowState() {
589
+ try {
590
+ const raw = readFileSync(join(root, "${RUNTIME_ROOT}/state/flow-state.json"), "utf8");
591
+ const state = JSON.parse(raw);
592
+ return {
593
+ stage: state.currentStage || "none",
594
+ completed: (state.completedStages || []).length,
595
+ activeRunId: state.activeRunId || "none",
596
+ };
597
+ } catch {
598
+ return { stage: "none", completed: 0, activeRunId: "none" };
599
+ }
600
+ }
601
+
602
+ function readMetaSkill() {
603
+ try {
604
+ return readFileSync(join(root, "${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"), "utf8");
605
+ } catch {
606
+ return "";
607
+ }
608
+ }
609
+
610
+ function readTailText(filePath, maxBytes = 65536) {
611
+ let fd;
612
+ try {
613
+ fd = openSync(filePath, "r");
614
+ const size = fstatSync(fd).size;
615
+ if (!Number.isFinite(size) || size <= 0) return "";
616
+ const bytesToRead = Math.min(size, maxBytes);
617
+ const buffer = Buffer.allocUnsafe(bytesToRead);
618
+ readSync(fd, buffer, 0, bytesToRead, size - bytesToRead);
619
+ return buffer.toString("utf8");
620
+ } catch {
621
+ return "";
622
+ } finally {
623
+ if (fd !== undefined) {
624
+ try {
625
+ closeSync(fd);
626
+ } catch {
627
+ // ignore
628
+ }
629
+ }
630
+ }
631
+ }
632
+
633
+ function readTailLines(filePath, maxLines, maxBytes = 65536) {
634
+ const raw = readTailText(filePath, maxBytes).trim();
635
+ if (!raw) return [];
636
+ return raw.split(/\\r?\\n/).slice(-maxLines);
637
+ }
638
+
639
+ function readTopLearnings() {
640
+ try {
641
+ const lines = readTailLines(join(root, "${RUNTIME_ROOT}/learnings.jsonl"), 30, 65536);
642
+ if (lines.length === 0) return [];
643
+ const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
644
+ const deduped = new Map();
645
+ for (const e of entries) {
646
+ const key = e.key + ":" + e.type;
647
+ if (!deduped.has(key) || e.ts > deduped.get(key).ts) deduped.set(key, e);
648
+ }
649
+ const now = Date.now();
650
+ return [...deduped.values()]
651
+ .map(e => {
652
+ const months = e.source === "user-stated" ? 0 : Math.floor((now - new Date(e.ts).getTime()) / (30 * 86400000));
653
+ const eff = Math.max(0, e.confidence - months);
654
+ return { ...e, effective_confidence: eff };
655
+ })
656
+ .sort((a, b) => b.effective_confidence - a.effective_confidence)
657
+ .slice(0, 3);
658
+ } catch {
659
+ return [];
660
+ }
661
+ }
662
+
663
+ function readCheckpointSummary() {
664
+ try {
665
+ const raw = readFileSync(checkpointPath, "utf8");
666
+ const cp = JSON.parse(raw);
667
+ return \`Checkpoint: stage=\${cp.stage || "none"}, status=\${cp.status || "unknown"}, run=\${cp.runId || "none"}, at=\${cp.timestamp || "unknown"}\`;
668
+ } catch {
669
+ return "";
670
+ }
671
+ }
672
+
673
+ function readRecentActivity() {
674
+ try {
675
+ const lines = readTailLines(activityPath, 5, 32768);
676
+ if (lines.length === 0) return [];
677
+ return lines.map((line) => {
678
+ try {
679
+ return JSON.parse(line);
680
+ } catch {
681
+ return null;
682
+ }
683
+ }).filter(Boolean).map((entry) => \`- \${entry.ts || "unknown"} [\${entry.phase || "unknown"}] \${entry.tool || "unknown"} (stage=\${entry.stage || "unknown"}, run=\${entry.runId || "none"})\`);
684
+ } catch {
685
+ return [];
686
+ }
687
+ }
688
+
689
+ function buildBootstrap() {
690
+ const { stage, completed, activeRunId } = readFlowState();
691
+ const meta = readMetaSkill();
692
+ const learnings = readTopLearnings();
693
+ const checkpoint = readCheckpointSummary();
694
+ const activity = readRecentActivity();
695
+ const parts = [\`cclaw loaded. Flow: stage=\${stage} (\${completed}/9 completed, run=\${activeRunId}). Active run artifacts: ${RUNTIME_ROOT}/runs/\${activeRunId}/artifacts/\`];
696
+ if (checkpoint) parts.push(checkpoint);
697
+ if (activity.length > 0) parts.push("Recent stage activity:", ...activity);
698
+ if (learnings.length > 0) {
699
+ parts.push("Top learnings:");
700
+ for (const l of learnings) parts.push(\`- \${l.key} (conf \${l.effective_confidence}): \${l.insight}\`);
701
+ }
702
+ if (meta) parts.push("", meta);
703
+ return parts.join("\\n");
704
+ }
705
+
706
+ function emitBootstrap() {
707
+ console.log(buildBootstrap());
708
+ }
709
+
710
+ return {
711
+ "session.created": emitBootstrap,
712
+ "session.resumed": emitBootstrap,
713
+ "session.compacted": emitBootstrap,
714
+ "session.cleared": emitBootstrap,
715
+ "experimental.chat.system.transform": (payload) => {
716
+ const bootstrap = buildBootstrap();
717
+ if (typeof payload === "string") {
718
+ return payload.includes("cclaw loaded.") ? payload : \`\${payload}\\n\\n\${bootstrap}\`;
719
+ }
720
+ if (payload && typeof payload === "object" && typeof payload.system === "string") {
721
+ if (payload.system.includes("cclaw loaded.")) return payload;
722
+ return { ...payload, system: \`\${payload.system}\\n\\n\${bootstrap}\` };
723
+ }
724
+ return payload;
725
+ },
726
+ };
727
+ }
728
+ `;
729
+ }
730
+ // ---------------------------------------------------------------------------
731
+ // AGENTS.md block for hooks
732
+ // ---------------------------------------------------------------------------
733
+ export function hooksAgentsMdBlock() {
734
+ return `### Hooks (real lifecycle integration)
735
+
736
+ Cclaw generates real hook integrations across harnesses:
737
+ - **Claude/Cursor/Codex:** lifecycle rehydration + PreToolUse/PostToolUse + Stop
738
+ - **OpenCode:** session lifecycle + system transform rehydration in plugin
739
+
740
+ | Harness | Hook file | Events |
741
+ |---------|-----------|--------|
742
+ | Claude Code | \`.claude/hooks/hooks.json\` | SessionStart(startup/resume/clear/compact), PreToolUse, PostToolUse, Stop |
743
+ | Cursor | \`.cursor/hooks.json\` | sessionStart/sessionResume/sessionClear/sessionCompact, preToolUse, postToolUse, stop |
744
+ | Codex | \`.codex/hooks.json\` | SessionStart(startup/resume/clear/compact), PreToolUse, PostToolUse, Stop |
745
+ | OpenCode | \`${RUNTIME_ROOT}/hooks/opencode-plugin.mjs\` | session.created/resumed/compacted/cleared + system transform |
746
+
747
+ Hook state files:
748
+ - \`${RUNTIME_ROOT}/state/stage-activity.jsonl\`
749
+ - \`${RUNTIME_ROOT}/state/checkpoint.json\`
750
+
751
+ Disable observation: \`touch ${RUNTIME_ROOT}/.observe-disabled\`
752
+ `;
753
+ }