cclaw-cli 0.48.9 → 0.48.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/content/doctor-references.js +11 -7
- package/dist/content/harness-doc.js +1 -1
- package/dist/content/harness-playbooks.js +7 -7
- package/dist/content/hook-events.js +19 -19
- package/dist/content/hooks.d.ts +0 -16
- package/dist/content/hooks.js +112 -1220
- package/dist/content/learnings.js +2 -2
- package/dist/content/next-command.js +2 -2
- package/dist/content/node-hooks.js +31 -15
- package/dist/content/observe.d.ts +0 -38
- package/dist/content/observe.js +31 -1718
- package/dist/content/opencode-plugin.js +10 -7
- package/dist/content/protocols.js +5 -9
- package/dist/content/skills.js +1 -1
- package/dist/content/stage-common-guidance.js +1 -1
- package/dist/content/stages/design.js +1 -1
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/scope.js +1 -1
- package/dist/doctor-registry.js +0 -9
- package/dist/doctor.js +58 -66
- package/dist/install.js +134 -54
- package/dist/policy.js +13 -13
- package/package.json +2 -3
package/dist/content/hooks.js
CHANGED
|
@@ -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
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
${
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
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";
|