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.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +101 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +70 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +50 -0
- package/dist/content/agents.d.ts +39 -0
- package/dist/content/agents.js +244 -0
- package/dist/content/autoplan.d.ts +7 -0
- package/dist/content/autoplan.js +297 -0
- package/dist/content/contracts.d.ts +2 -0
- package/dist/content/contracts.js +50 -0
- package/dist/content/examples.d.ts +2 -0
- package/dist/content/examples.js +327 -0
- package/dist/content/hooks.d.ts +16 -0
- package/dist/content/hooks.js +753 -0
- package/dist/content/learnings.d.ts +5 -0
- package/dist/content/learnings.js +265 -0
- package/dist/content/meta-skill.d.ts +10 -0
- package/dist/content/meta-skill.js +137 -0
- package/dist/content/observe.d.ts +21 -0
- package/dist/content/observe.js +1110 -0
- package/dist/content/session-hooks.d.ts +7 -0
- package/dist/content/session-hooks.js +137 -0
- package/dist/content/skills.d.ts +3 -0
- package/dist/content/skills.js +257 -0
- package/dist/content/stage-schema.d.ts +78 -0
- package/dist/content/stage-schema.js +1453 -0
- package/dist/content/subagents.d.ts +13 -0
- package/dist/content/subagents.js +616 -0
- package/dist/content/templates.d.ts +3 -0
- package/dist/content/templates.js +272 -0
- package/dist/content/utility-skills.d.ts +12 -0
- package/dist/content/utility-skills.js +467 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +610 -0
- package/dist/flow-state.d.ts +19 -0
- package/dist/flow-state.js +41 -0
- package/dist/fs-utils.d.ts +5 -0
- package/dist/fs-utils.js +28 -0
- package/dist/gitignore.d.ts +3 -0
- package/dist/gitignore.js +43 -0
- package/dist/harness-adapters.d.ts +12 -0
- package/dist/harness-adapters.js +175 -0
- package/dist/install.d.ts +9 -0
- package/dist/install.js +562 -0
- package/dist/learnings-summarizer.d.ts +25 -0
- package/dist/learnings-summarizer.js +201 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +6 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +179 -0
- package/dist/runs.d.ts +18 -0
- package/dist/runs.js +446 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +12 -0
- 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
|
+
}
|