agent-composer 0.3.1 → 0.4.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/README.md +495 -180
- package/composer.config.schema.json +206 -2
- package/dist/cli/cleanup.d.ts +24 -0
- package/dist/cli/cleanup.js +151 -0
- package/dist/cli/cleanup.js.map +1 -0
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +244 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/goal.d.ts +28 -0
- package/dist/cli/goal.js +251 -0
- package/dist/cli/goal.js.map +1 -0
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.js +31 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +116 -21
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/initArgs.d.ts +16 -0
- package/dist/cli/initArgs.js +19 -0
- package/dist/cli/initArgs.js.map +1 -0
- package/dist/cli/installGitHook.d.ts +7 -0
- package/dist/cli/installGitHook.js +61 -0
- package/dist/cli/installGitHook.js.map +1 -0
- package/dist/cli/mode.d.ts +6 -0
- package/dist/cli/mode.js +25 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/cli/status.d.ts +105 -0
- package/dist/cli/status.js +400 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/config/env.d.ts +1 -1
- package/dist/config/modes.d.ts +10 -0
- package/dist/config/modes.js +26 -0
- package/dist/config/modes.js.map +1 -0
- package/dist/config/oracleRole.d.ts +10 -0
- package/dist/config/oracleRole.js +11 -0
- package/dist/config/oracleRole.js.map +1 -0
- package/dist/config/schema.d.ts +246 -0
- package/dist/config/schema.js +127 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/evolve/reflection.d.ts +9 -0
- package/dist/evolve/reflection.js +14 -0
- package/dist/evolve/reflection.js.map +1 -1
- package/dist/evolve/runner.d.ts +2 -1
- package/dist/evolve/runner.js +2 -1
- package/dist/evolve/runner.js.map +1 -1
- package/dist/index.js +115 -6
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicCompatibleProvider.d.ts +13 -1
- package/dist/providers/AnthropicCompatibleProvider.js +115 -9
- package/dist/providers/AnthropicCompatibleProvider.js.map +1 -1
- package/dist/providers/CLIProvider.d.ts +18 -0
- package/dist/providers/CLIProvider.js +265 -62
- package/dist/providers/CLIProvider.js.map +1 -1
- package/dist/providers/IProvider.d.ts +12 -0
- package/dist/providers/SpendGuardProvider.d.ts +32 -0
- package/dist/providers/SpendGuardProvider.js +98 -0
- package/dist/providers/SpendGuardProvider.js.map +1 -0
- package/dist/registry.d.ts +5 -2
- package/dist/registry.js +17 -2
- package/dist/registry.js.map +1 -1
- package/dist/server/activeRuns.d.ts +17 -0
- package/dist/server/activeRuns.js +114 -0
- package/dist/server/activeRuns.js.map +1 -0
- package/dist/server/codexLifecycleRunner.d.ts +29 -0
- package/dist/server/codexLifecycleRunner.js +188 -0
- package/dist/server/codexLifecycleRunner.js.map +1 -0
- package/dist/server/configMutation.d.ts +22 -0
- package/dist/server/configMutation.js +121 -0
- package/dist/server/configMutation.js.map +1 -0
- package/dist/server/handoffContext.d.ts +1 -0
- package/dist/server/handoffContext.js +12 -0
- package/dist/server/handoffContext.js.map +1 -0
- package/dist/server/progress.d.ts +24 -0
- package/dist/server/progress.js +109 -0
- package/dist/server/progress.js.map +1 -0
- package/dist/server/toolDescriptions.d.ts +60 -0
- package/dist/server/toolDescriptions.js +134 -0
- package/dist/server/toolDescriptions.js.map +1 -0
- package/dist/server.d.ts +19 -25
- package/dist/server.js +87 -377
- package/dist/server.js.map +1 -1
- package/dist/tools/audit.d.ts +2 -0
- package/dist/tools/audit.js +66 -0
- package/dist/tools/audit.js.map +1 -0
- package/dist/tools/code.d.ts +2 -0
- package/dist/tools/code.js +160 -0
- package/dist/tools/code.js.map +1 -0
- package/dist/tools/codexLifecycle.d.ts +2 -0
- package/dist/tools/codexLifecycle.js +206 -0
- package/dist/tools/codexLifecycle.js.map +1 -0
- package/dist/tools/config.d.ts +2 -0
- package/dist/tools/config.js +183 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/context.d.ts +31 -0
- package/dist/tools/context.js +2 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/goal.d.ts +2 -0
- package/dist/tools/goal.js +159 -0
- package/dist/tools/goal.js.map +1 -0
- package/dist/tools/handoff.d.ts +2 -0
- package/dist/tools/handoff.js +57 -0
- package/dist/tools/handoff.js.map +1 -0
- package/dist/tools/oracle.d.ts +2 -0
- package/dist/tools/oracle.js +248 -0
- package/dist/tools/oracle.js.map +1 -0
- package/dist/tools/research.d.ts +2 -0
- package/dist/tools/research.js +51 -0
- package/dist/tools/research.js.map +1 -0
- package/dist/tools/review.d.ts +2 -0
- package/dist/tools/review.js +233 -0
- package/dist/tools/review.js.map +1 -0
- package/dist/tools/route.d.ts +2 -0
- package/dist/tools/route.js +69 -0
- package/dist/tools/route.js.map +1 -0
- package/dist/tools/session.d.ts +2 -0
- package/dist/tools/session.js +37 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/status.d.ts +2 -0
- package/dist/tools/status.js +34 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/workflow.d.ts +2 -0
- package/dist/tools/workflow.js +27 -0
- package/dist/tools/workflow.js.map +1 -0
- package/dist/util/applyFileBlocks.d.ts +18 -0
- package/dist/util/applyFileBlocks.js +163 -0
- package/dist/util/applyFileBlocks.js.map +1 -0
- package/dist/util/asyncControl.d.ts +14 -0
- package/dist/util/asyncControl.js +106 -0
- package/dist/util/asyncControl.js.map +1 -0
- package/dist/util/auditLog.d.ts +56 -0
- package/dist/util/auditLog.js +232 -0
- package/dist/util/auditLog.js.map +1 -0
- package/dist/util/codexLifecycle.d.ts +55 -0
- package/dist/util/codexLifecycle.js +102 -0
- package/dist/util/codexLifecycle.js.map +1 -0
- package/dist/util/codexLifecycleJob.d.ts +209 -0
- package/dist/util/codexLifecycleJob.js +360 -0
- package/dist/util/codexLifecycleJob.js.map +1 -0
- package/dist/util/composerDisabled.d.ts +6 -0
- package/dist/util/composerDisabled.js +27 -0
- package/dist/util/composerDisabled.js.map +1 -0
- package/dist/util/dispatchHint.d.ts +5 -3
- package/dist/util/dispatchHint.js +62 -2
- package/dist/util/dispatchHint.js.map +1 -1
- package/dist/util/goal.d.ts +132 -0
- package/dist/util/goal.js +616 -0
- package/dist/util/goal.js.map +1 -0
- package/dist/util/goalReport.d.ts +51 -0
- package/dist/util/goalReport.js +164 -0
- package/dist/util/goalReport.js.map +1 -0
- package/dist/util/jobPolling.d.ts +9 -0
- package/dist/util/jobPolling.js +17 -0
- package/dist/util/jobPolling.js.map +1 -0
- package/dist/util/oracleJob.d.ts +66 -0
- package/dist/util/oracleJob.js +295 -0
- package/dist/util/oracleJob.js.map +1 -0
- package/dist/util/oracleLock.d.ts +38 -0
- package/dist/util/oracleLock.js +182 -0
- package/dist/util/oracleLock.js.map +1 -0
- package/dist/util/reviewDiff.d.ts +8 -0
- package/dist/util/reviewDiff.js +29 -0
- package/dist/util/reviewDiff.js.map +1 -0
- package/dist/util/reviewJob.d.ts +57 -0
- package/dist/util/reviewJob.js +207 -0
- package/dist/util/reviewJob.js.map +1 -0
- package/dist/util/workflowPlan.d.ts +24 -0
- package/dist/util/workflowPlan.js +49 -0
- package/dist/util/workflowPlan.js.map +1 -0
- package/package.json +8 -1
- package/plugin/composer-mastermind/commands/evolve.md +4 -0
- package/plugin/composer-mastermind/hooks/boundary_guard.sh +43 -2
- package/plugin/composer-mastermind/hooks/codex_warm_review.sh +161 -9
- package/plugin/composer-mastermind/hooks/learn.sh +172 -32
- package/plugin/composer-mastermind/hooks/precommit_codex_review.sh +430 -62
- package/plugin/composer-mastermind/skills/composer-mastermind/SKILL.md +184 -4
- package/scripts/composer-oracle-router-safe.sh +47 -0
- package/scripts/composer-statusline-segment.mjs +40 -0
- package/scripts/oracle-codex-handoff-safe.sh +49 -0
- package/scripts/oracle-plan-mcp.sh +66 -0
- package/scripts/oracle-pro-safe.sh +471 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-composer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-agent orchestration MCP server. Claude orchestrates; GLM, Codex, and agy do the work.",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/",
|
|
11
11
|
"plugin/",
|
|
12
|
+
"scripts/oracle-pro-safe.sh",
|
|
13
|
+
"scripts/oracle-plan-mcp.sh",
|
|
14
|
+
"scripts/composer-oracle-router-safe.sh",
|
|
15
|
+
"scripts/oracle-codex-handoff-safe.sh",
|
|
16
|
+
"scripts/composer-statusline-segment.mjs",
|
|
12
17
|
"composer.config.schema.json",
|
|
13
18
|
"README.md"
|
|
14
19
|
],
|
|
@@ -32,6 +37,8 @@
|
|
|
32
37
|
"test:hooks": "bash tests/hooks/run.sh",
|
|
33
38
|
"test:scripts": "bash tests/scripts/run.sh",
|
|
34
39
|
"test:all": "npm run test && npm run test:hooks && npm run test:scripts",
|
|
40
|
+
"ci": "npm run typecheck && npm run test && npm run test:hooks && npm run test:scripts && npm run schema:lint",
|
|
41
|
+
"bench:speed": "node --import tsx scripts/bench-composer.mjs",
|
|
35
42
|
"eval:routes": "tsx evals/scripts/route-compare.ts",
|
|
36
43
|
"usage": "npx -y ccusage daily",
|
|
37
44
|
"schema:lint": "ajv compile --strict=true -c ajv-formats -s composer.config.schema.json && ajv validate --strict=false -c ajv-formats -s composer.config.schema.json -d composer.config.json && ajv compile --strict=true -s plugin.schema.json && ajv validate --strict=true -s plugin.schema.json -d 'plugin/*/plugin.json'",
|
|
@@ -52,6 +52,10 @@ The summary prints to stdout:
|
|
|
52
52
|
- Task descriptions in `evals/tasks.jsonl` are passed verbatim to the reflection provider — keep that file under version control to retain a clear trust boundary.
|
|
53
53
|
- **SKILL.md edits are safe during a real-mode run.** Each task eval runs in a throwaway `git worktree` at `/tmp/composer-eval-<pid>-<taskId>`. The candidate skill is written only into the worktree copy; the real repo's SKILL.md is never touched during evaluation. You can freely edit SKILL.md in your editor while `/evolve --eval-mode real` is running.
|
|
54
54
|
|
|
55
|
+
## Evidence
|
|
56
|
+
|
|
57
|
+
The reflection mutator now receives recent route/audit failures from the durable audit trail (`composer_audit_record` → `readAuditFailures`). Up to 20 recent failures (events with `status=failed` or `userCorrection=true`) are extracted from the audit log and injected into the reflection prompt before the current-ecosystem section. This means proposed skill rewrites are driven by real routing and outcome failures — wrong route choice, unnecessary Oracle use, issues a review caught after the fact, user-corrected routes — not just synthetic scorer signals.
|
|
58
|
+
|
|
55
59
|
## v1 caveat
|
|
56
60
|
|
|
57
61
|
The v1 scorer is synthetic (heuristic-based):
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
# exit: always 0; semantics carried by JSON
|
|
7
7
|
# Fail-closed: any unexpected condition (missing jq / empty stdin /
|
|
8
8
|
# malformed JSON / absent tool_name) MUST emit a deny payload.
|
|
9
|
+
# Scope: GLOBAL. When Composer is enabled, this hook denies main-thread
|
|
10
|
+
# file-mutation tools in every repo and every path. Enforcement is controlled
|
|
11
|
+
# only by kill switches such as ~/.claude/composer.disabled or /composer disable.
|
|
9
12
|
|
|
10
13
|
set -u
|
|
11
14
|
|
|
@@ -51,7 +54,11 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
51
54
|
fi
|
|
52
55
|
|
|
53
56
|
# 2. Read tool-call JSON from stdin.
|
|
54
|
-
|
|
57
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
58
|
+
INPUT="$(timeout 5 cat 2>/dev/null || true)"
|
|
59
|
+
else
|
|
60
|
+
INPUT="$(cat || true)"
|
|
61
|
+
fi
|
|
55
62
|
if [[ -z "$INPUT" ]]; then
|
|
56
63
|
emit_deny "boundary_guard: empty stdin, failing closed"
|
|
57
64
|
fi
|
|
@@ -101,6 +108,40 @@ if [[ "$TRANSCRIPT" == */subagents/* ]] \
|
|
|
101
108
|
exit 0
|
|
102
109
|
fi
|
|
103
110
|
|
|
111
|
+
# 3.8. Brain housekeeping carve-out.
|
|
112
|
+
# Claude (the orchestrator / "brain") must persist its OWN state — e.g.
|
|
113
|
+
# cross-session memory — even while enforcement is ON. These paths are NOT
|
|
114
|
+
# project code, so routing them through the executor adds no safety. Allow a
|
|
115
|
+
# main-thread mutation whose target path is under an approved brain-state
|
|
116
|
+
# root. Default: the Claude memory store. Extra roots may be added via
|
|
117
|
+
# COMPOSER_GUARD_ALLOW_GLOBS (colon-separated case globs). Paths containing a
|
|
118
|
+
# ".." traversal segment are NEVER allow-listed, so an allowed prefix cannot
|
|
119
|
+
# be used to escape into ~/.claude/hooks or similar. Tools without a file
|
|
120
|
+
# path (Bash, mcp exec/bash) have no FILE and fall through to the block list.
|
|
121
|
+
FILE="$(jq -r '.tool_input.file_path // .tool_input.path // .tool_input.notebook_path // empty' <<<"$INPUT" 2>/dev/null)"
|
|
122
|
+
if [[ -n "$FILE" && "$FILE" != *"/../"* && "$FILE" != *"/.." ]]; then
|
|
123
|
+
brain_globs="$HOME/.claude/projects/*/memory/*"
|
|
124
|
+
if [[ -n "${COMPOSER_GUARD_ALLOW_GLOBS:-}" ]]; then
|
|
125
|
+
brain_globs="$brain_globs:$COMPOSER_GUARD_ALLOW_GLOBS"
|
|
126
|
+
fi
|
|
127
|
+
set -f
|
|
128
|
+
brain_old_ifs="$IFS"
|
|
129
|
+
IFS=':'
|
|
130
|
+
for glob in $brain_globs; do
|
|
131
|
+
[[ -z "$glob" ]] && continue
|
|
132
|
+
# shellcheck disable=SC2254
|
|
133
|
+
case "$FILE" in
|
|
134
|
+
$glob)
|
|
135
|
+
IFS="$brain_old_ifs"
|
|
136
|
+
set +f
|
|
137
|
+
exit 0
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
done
|
|
141
|
+
IFS="$brain_old_ifs"
|
|
142
|
+
set +f
|
|
143
|
+
fi
|
|
144
|
+
|
|
104
145
|
# 4. Block list — native dangerous file-mutating tools + MCP-prefixed variants.
|
|
105
146
|
# Native Bash is allowed on the main thread for inspection and verification;
|
|
106
147
|
# the orchestrator skill still forbids using Bash to author code or perform
|
|
@@ -110,7 +151,7 @@ case "$TOOL" in
|
|
|
110
151
|
Edit|Update|Write|NotebookEdit \
|
|
111
152
|
| mcp__*__write_file | mcp__*__edit_file | mcp__*__bash \
|
|
112
153
|
| mcp__*__write | mcp__*__edit | mcp__*__exec)
|
|
113
|
-
emit_deny "
|
|
154
|
+
emit_deny "Composer is handling file edits. Route this change through composer_code_cli (or composer_code_chain), or run /composer disable to edit directly. Bash inspection stays available."
|
|
114
155
|
;;
|
|
115
156
|
esac
|
|
116
157
|
|
|
@@ -300,19 +300,171 @@ if [[ "$status" -ne 0 || -z "$output" ]] || ! jq -e . >/dev/null 2>&1 <<<"$outpu
|
|
|
300
300
|
append_run_log "skip" "$duration_ms" 0
|
|
301
301
|
exit 0
|
|
302
302
|
fi
|
|
303
|
-
|
|
303
|
+
parse_error_message="$(jq -r '
|
|
304
|
+
def parsed_json($value):
|
|
305
|
+
if ($value | type) == "string" then (($value | fromjson?) // {}) else {} end;
|
|
306
|
+
def first_error($items):
|
|
307
|
+
[
|
|
308
|
+
$items[]
|
|
309
|
+
| select(. != null and . != false and ((. | tostring) | length) > 0)
|
|
310
|
+
]
|
|
311
|
+
| first // "";
|
|
312
|
+
|
|
313
|
+
(parsed_json(.rawOutput? // null)) as $rawOutputJson
|
|
314
|
+
| (parsed_json(.codex.stdout? // null)) as $codexStdoutJson
|
|
315
|
+
| first_error([
|
|
316
|
+
.parseError?,
|
|
317
|
+
.result.parseError?,
|
|
318
|
+
.review?.parseError?,
|
|
319
|
+
.review?.result?.parseError?,
|
|
320
|
+
.data?.parseError?,
|
|
321
|
+
.data?.result?.parseError?,
|
|
322
|
+
.output?.parseError?,
|
|
323
|
+
.output?.result?.parseError?,
|
|
324
|
+
.response?.parseError?,
|
|
325
|
+
.response?.result?.parseError?,
|
|
326
|
+
.payload?.parseError?,
|
|
327
|
+
.payload?.result?.parseError?,
|
|
328
|
+
$rawOutputJson.parseError?,
|
|
329
|
+
$rawOutputJson.result.parseError?,
|
|
330
|
+
$codexStdoutJson.parseError?,
|
|
331
|
+
$codexStdoutJson.result.parseError?
|
|
332
|
+
])
|
|
333
|
+
' <<<"$output" 2>/dev/null || true)"
|
|
334
|
+
if [[ -n "$parse_error_message" ]]; then
|
|
304
335
|
append_run_log "skip" "$duration_ms" 0
|
|
305
336
|
exit 0
|
|
306
337
|
fi
|
|
307
|
-
normalized="$(jq -c
|
|
308
|
-
def
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
338
|
+
normalized="$(jq -c '
|
|
339
|
+
def parsed_json($value):
|
|
340
|
+
if ($value | type) == "string" then (($value | fromjson?) // {}) else {} end;
|
|
341
|
+
def first_value($items):
|
|
342
|
+
[
|
|
343
|
+
$items[]
|
|
344
|
+
| select(. != null and . != "")
|
|
345
|
+
]
|
|
346
|
+
| first // null;
|
|
347
|
+
def first_text($items):
|
|
348
|
+
[
|
|
349
|
+
$items[]
|
|
350
|
+
| select((. | type) == "string" and length > 0)
|
|
351
|
+
]
|
|
352
|
+
| first // "";
|
|
353
|
+
def first_array($items):
|
|
354
|
+
[
|
|
355
|
+
$items[]
|
|
356
|
+
| select((. | type) == "array")
|
|
357
|
+
]
|
|
358
|
+
| first // [];
|
|
359
|
+
|
|
360
|
+
(parsed_json(.rawOutput? // null)) as $rawOutputJson
|
|
361
|
+
| (parsed_json(.codex.stdout? // null)) as $codexStdoutJson
|
|
362
|
+
| (parsed_json(.stdout? // null)) as $stdoutJson
|
|
363
|
+
| {
|
|
364
|
+
verdict: first_value([
|
|
365
|
+
.result.verdict?,
|
|
366
|
+
.verdict?,
|
|
367
|
+
.review?.result?.verdict?,
|
|
368
|
+
.review?.verdict?,
|
|
369
|
+
.data?.result?.verdict?,
|
|
370
|
+
.data?.verdict?,
|
|
371
|
+
.output?.result?.verdict?,
|
|
372
|
+
.output?.verdict?,
|
|
373
|
+
.response?.result?.verdict?,
|
|
374
|
+
.response?.verdict?,
|
|
375
|
+
.payload?.result?.verdict?,
|
|
376
|
+
.payload?.verdict?,
|
|
377
|
+
$rawOutputJson.result.verdict?,
|
|
378
|
+
$rawOutputJson.verdict?,
|
|
379
|
+
$rawOutputJson.review?.result?.verdict?,
|
|
380
|
+
$rawOutputJson.review?.verdict?,
|
|
381
|
+
$codexStdoutJson.result.verdict?,
|
|
382
|
+
$codexStdoutJson.verdict?,
|
|
383
|
+
$codexStdoutJson.review?.result?.verdict?,
|
|
384
|
+
$codexStdoutJson.review?.verdict?,
|
|
385
|
+
$stdoutJson.result.verdict?,
|
|
386
|
+
$stdoutJson.verdict?
|
|
387
|
+
]),
|
|
388
|
+
summary: (first_text([
|
|
389
|
+
.result.summary?,
|
|
390
|
+
.summary?,
|
|
391
|
+
.review?.result?.summary?,
|
|
392
|
+
.review?.summary?,
|
|
393
|
+
.data?.result?.summary?,
|
|
394
|
+
.data?.summary?,
|
|
395
|
+
.output?.result?.summary?,
|
|
396
|
+
.output?.summary?,
|
|
397
|
+
.response?.result?.summary?,
|
|
398
|
+
.response?.summary?,
|
|
399
|
+
.payload?.result?.summary?,
|
|
400
|
+
.payload?.summary?,
|
|
401
|
+
$rawOutputJson.result.summary?,
|
|
402
|
+
$rawOutputJson.summary?,
|
|
403
|
+
$rawOutputJson.review?.result?.summary?,
|
|
404
|
+
$rawOutputJson.review?.summary?,
|
|
405
|
+
$codexStdoutJson.result.summary?,
|
|
406
|
+
$codexStdoutJson.summary?,
|
|
407
|
+
$codexStdoutJson.review?.result?.summary?,
|
|
408
|
+
$codexStdoutJson.review?.summary?,
|
|
409
|
+
$stdoutJson.result.summary?,
|
|
410
|
+
$stdoutJson.summary?
|
|
411
|
+
]) // ""),
|
|
412
|
+
findings: first_array([
|
|
413
|
+
.result.findings?,
|
|
414
|
+
.findings?,
|
|
415
|
+
.review?.result?.findings?,
|
|
416
|
+
.review?.findings?,
|
|
417
|
+
.data?.result?.findings?,
|
|
418
|
+
.data?.findings?,
|
|
419
|
+
.output?.result?.findings?,
|
|
420
|
+
.output?.findings?,
|
|
421
|
+
.response?.result?.findings?,
|
|
422
|
+
.response?.findings?,
|
|
423
|
+
.payload?.result?.findings?,
|
|
424
|
+
.payload?.findings?,
|
|
425
|
+
$rawOutputJson.result.findings?,
|
|
426
|
+
$rawOutputJson.findings?,
|
|
427
|
+
$rawOutputJson.review?.result?.findings?,
|
|
428
|
+
$rawOutputJson.review?.findings?,
|
|
429
|
+
$codexStdoutJson.result.findings?,
|
|
430
|
+
$codexStdoutJson.findings?,
|
|
431
|
+
$codexStdoutJson.review?.result?.findings?,
|
|
432
|
+
$codexStdoutJson.review?.findings?,
|
|
433
|
+
$stdoutJson.result.findings?,
|
|
434
|
+
$stdoutJson.findings?
|
|
435
|
+
]),
|
|
436
|
+
next_steps: first_array([
|
|
437
|
+
.result.next_steps?,
|
|
438
|
+
.next_steps?,
|
|
439
|
+
.review?.result?.next_steps?,
|
|
440
|
+
.review?.next_steps?,
|
|
441
|
+
.data?.result?.next_steps?,
|
|
442
|
+
.data?.next_steps?,
|
|
443
|
+
.output?.result?.next_steps?,
|
|
444
|
+
.output?.next_steps?,
|
|
445
|
+
.response?.result?.next_steps?,
|
|
446
|
+
.response?.next_steps?,
|
|
447
|
+
.payload?.result?.next_steps?,
|
|
448
|
+
.payload?.next_steps?,
|
|
449
|
+
$rawOutputJson.result.next_steps?,
|
|
450
|
+
$rawOutputJson.next_steps?,
|
|
451
|
+
$rawOutputJson.review?.result?.next_steps?,
|
|
452
|
+
$rawOutputJson.review?.next_steps?,
|
|
453
|
+
$codexStdoutJson.result.next_steps?,
|
|
454
|
+
$codexStdoutJson.next_steps?,
|
|
455
|
+
$codexStdoutJson.review?.result?.next_steps?,
|
|
456
|
+
$codexStdoutJson.review?.next_steps?,
|
|
457
|
+
$stdoutJson.result.next_steps?,
|
|
458
|
+
$stdoutJson.next_steps?
|
|
459
|
+
]),
|
|
460
|
+
raw_text: first_text([
|
|
461
|
+
.codex.stdout?,
|
|
462
|
+
.rawOutput?,
|
|
463
|
+
.stdout?,
|
|
464
|
+
.review?
|
|
465
|
+
])
|
|
314
466
|
}
|
|
315
|
-
|
|
467
|
+
' <<<"$output" 2>/dev/null || true)"
|
|
316
468
|
if [[ -z "$normalized" ]] || ! jq -e . >/dev/null 2>&1 <<<"$normalized"; then
|
|
317
469
|
append_run_log "skip" "$duration_ms" 0
|
|
318
470
|
exit 0
|
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
set -u
|
|
10
10
|
|
|
11
|
+
LEARN_DEFAULT_TIMEOUT_MS=5000
|
|
12
|
+
LEARN_MAX_TIMEOUT_MS=30000
|
|
13
|
+
LEARN_LOG="${COMPOSER_LEARN_LOG:-/tmp/composer-learn-log.jsonl}"
|
|
14
|
+
LEARN_TEMP_FILES=()
|
|
15
|
+
|
|
16
|
+
cleanup_learn_temp() {
|
|
17
|
+
if ((${#LEARN_TEMP_FILES[@]} > 0)); then
|
|
18
|
+
rm -f "${LEARN_TEMP_FILES[@]}" 2>/dev/null || true
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
trap cleanup_learn_temp EXIT
|
|
23
|
+
|
|
11
24
|
composer_disabled() {
|
|
12
25
|
case "${COMPOSER_ENABLED:-}" in
|
|
13
26
|
0|false|FALSE|off|OFF|no|NO) return 0 ;;
|
|
@@ -31,13 +44,120 @@ if composer_disabled; then
|
|
|
31
44
|
exit 0
|
|
32
45
|
fi
|
|
33
46
|
|
|
47
|
+
resolve_learn_timeout_ms() {
|
|
48
|
+
local configured="${COMPOSER_LEARN_HOOK_TIMEOUT_MS:-$LEARN_DEFAULT_TIMEOUT_MS}"
|
|
49
|
+
case "$configured" in
|
|
50
|
+
''|*[!0-9]*) configured="$LEARN_DEFAULT_TIMEOUT_MS" ;;
|
|
51
|
+
esac
|
|
52
|
+
if [[ "$configured" -lt 1 ]]; then
|
|
53
|
+
configured="$LEARN_DEFAULT_TIMEOUT_MS"
|
|
54
|
+
elif [[ "$configured" -gt "$LEARN_MAX_TIMEOUT_MS" ]]; then
|
|
55
|
+
configured="$LEARN_MAX_TIMEOUT_MS"
|
|
56
|
+
fi
|
|
57
|
+
printf '%s\n' "$configured"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
timeout_ms_to_seconds() {
|
|
61
|
+
local timeout_ms="$1"
|
|
62
|
+
local seconds=$(( (timeout_ms + 999) / 1000 ))
|
|
63
|
+
[[ "$seconds" -gt 0 ]] || seconds=1
|
|
64
|
+
printf '%s\n' "$seconds"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
remaining_learn_seconds() {
|
|
68
|
+
local started="$1"
|
|
69
|
+
local total="$2"
|
|
70
|
+
local now elapsed remaining
|
|
71
|
+
now="$(date +%s)"
|
|
72
|
+
elapsed=$(( now - started ))
|
|
73
|
+
remaining=$(( total - elapsed ))
|
|
74
|
+
if [[ "$remaining" -le 0 ]]; then
|
|
75
|
+
printf '0\n'
|
|
76
|
+
else
|
|
77
|
+
printf '%s\n' "$remaining"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
learn_mktemp() {
|
|
82
|
+
local path
|
|
83
|
+
path="$(mktemp -t composer_learnings.XXXXXX)" || return 1
|
|
84
|
+
LEARN_TEMP_FILES+=("$path")
|
|
85
|
+
printf '%s\n' "$path"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
log_learn_timeout() {
|
|
89
|
+
local elapsed_ms="${1:-0}"
|
|
90
|
+
printf '{"ts":"%s","reason_code":"hook_timeout","stage":"learn_stop","elapsed_wall_ms":%s}\n' \
|
|
91
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$elapsed_ms" >> "$LEARN_LOG" 2>/dev/null || true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
kill_tree() {
|
|
95
|
+
local sig="$1" root="$2" child
|
|
96
|
+
kill -STOP "$root" 2>/dev/null || true
|
|
97
|
+
for child in $(pgrep -P "$root" 2>/dev/null); do
|
|
98
|
+
kill_tree "$sig" "$child"
|
|
99
|
+
done
|
|
100
|
+
kill -"$sig" "$root" 2>/dev/null || true
|
|
101
|
+
kill -CONT "$root" 2>/dev/null || true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
run_with_timeout() {
|
|
105
|
+
local timeout_seconds="$1"
|
|
106
|
+
shift
|
|
107
|
+
local pid watchdog status marker start end
|
|
108
|
+
marker="${TMPDIR:-/tmp}/composer-learn-timeout.$$.$RANDOM"
|
|
109
|
+
start="$(date +%s)"
|
|
110
|
+
( "$@" ) &
|
|
111
|
+
pid=$!
|
|
112
|
+
(
|
|
113
|
+
sleeper=""
|
|
114
|
+
trap '[[ -n "$sleeper" ]] && kill "$sleeper" 2>/dev/null || true; exit 0' TERM INT
|
|
115
|
+
sleep "$timeout_seconds" &
|
|
116
|
+
sleeper=$!
|
|
117
|
+
wait "$sleeper" 2>/dev/null || exit 0
|
|
118
|
+
printf '1' >"$marker" 2>/dev/null || true
|
|
119
|
+
kill_tree TERM "$pid"
|
|
120
|
+
sleep 1
|
|
121
|
+
kill_tree KILL "$pid"
|
|
122
|
+
) &
|
|
123
|
+
watchdog=$!
|
|
124
|
+
wait "$pid"
|
|
125
|
+
status=$?
|
|
126
|
+
kill "$watchdog" 2>/dev/null || true
|
|
127
|
+
wait "$watchdog" 2>/dev/null || true
|
|
128
|
+
if [[ -f "$marker" ]]; then
|
|
129
|
+
rm -f "$marker" 2>/dev/null || true
|
|
130
|
+
end="$(date +%s)"
|
|
131
|
+
log_learn_timeout "$(( (end - start) * 1000 ))"
|
|
132
|
+
return 124
|
|
133
|
+
fi
|
|
134
|
+
rm -f "$marker" 2>/dev/null || true
|
|
135
|
+
return "$status"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
run_capture_with_timeout() {
|
|
139
|
+
local timeout_seconds="$1"
|
|
140
|
+
local stdin_file="$2"
|
|
141
|
+
shift 2
|
|
142
|
+
local output_file status
|
|
143
|
+
output_file="$(learn_mktemp)" || return 1
|
|
144
|
+
run_with_timeout "$timeout_seconds" bash -c 'exec "$@"' _ "$@" < "$stdin_file" > "$output_file" 2>/dev/null
|
|
145
|
+
status=$?
|
|
146
|
+
cat "$output_file" 2>/dev/null || true
|
|
147
|
+
return "$status"
|
|
148
|
+
}
|
|
149
|
+
|
|
34
150
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
35
151
|
LEARN_DIR="$PROJECT_DIR/.claude/learnings"
|
|
36
152
|
MONTH="$(date +%Y-%m)"
|
|
37
153
|
OUT="$LEARN_DIR/${MONTH}.md"
|
|
38
154
|
|
|
39
155
|
# Read JSON envelope from stdin (Anthropic Stop hook contract).
|
|
40
|
-
|
|
156
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
157
|
+
INPUT="$(timeout 5 cat 2>/dev/null || true)"
|
|
158
|
+
else
|
|
159
|
+
INPUT="$(cat || true)"
|
|
160
|
+
fi
|
|
41
161
|
if [[ -z "$INPUT" ]]; then
|
|
42
162
|
exit 0
|
|
43
163
|
fi
|
|
@@ -46,7 +166,24 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
46
166
|
exit 0
|
|
47
167
|
fi
|
|
48
168
|
|
|
49
|
-
|
|
169
|
+
LEARN_TIMEOUT_MS="$(resolve_learn_timeout_ms)"
|
|
170
|
+
LEARN_TIMEOUT_SECONDS="$(timeout_ms_to_seconds "$LEARN_TIMEOUT_MS")"
|
|
171
|
+
LEARN_STARTED="$(date +%s)"
|
|
172
|
+
INPUT_FILE="$(learn_mktemp)" || exit 0
|
|
173
|
+
printf '%s' "$INPUT" > "$INPUT_FILE" 2>/dev/null || exit 0
|
|
174
|
+
REMAINING_SECONDS="$(remaining_learn_seconds "$LEARN_STARTED" "$LEARN_TIMEOUT_SECONDS")"
|
|
175
|
+
if [[ "$REMAINING_SECONDS" -le 0 ]]; then
|
|
176
|
+
log_learn_timeout "$LEARN_TIMEOUT_MS"
|
|
177
|
+
exit 0
|
|
178
|
+
fi
|
|
179
|
+
TRANSCRIPT_PATH="$(run_capture_with_timeout "$REMAINING_SECONDS" "$INPUT_FILE" jq -r '.transcript_path // empty')"
|
|
180
|
+
TRANSCRIPT_STATUS=$?
|
|
181
|
+
if [[ "$TRANSCRIPT_STATUS" -eq 124 ]]; then
|
|
182
|
+
exit 0
|
|
183
|
+
fi
|
|
184
|
+
if [[ "$TRANSCRIPT_STATUS" -ne 0 ]]; then
|
|
185
|
+
exit 0
|
|
186
|
+
fi
|
|
50
187
|
if [[ -z "$TRANSCRIPT_PATH" || ! -f "$TRANSCRIPT_PATH" ]]; then
|
|
51
188
|
exit 0
|
|
52
189
|
fi
|
|
@@ -57,39 +194,42 @@ mkdir -p "$LEARN_DIR" 2>/dev/null || exit 0
|
|
|
57
194
|
# Conservative regex; expand from real data over time.
|
|
58
195
|
TRIGGER='(?i)\b(no|don.t|do not|wrong|stop|actually|instead|never|please don.t)\b'
|
|
59
196
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
197
|
+
process_transcript() {
|
|
198
|
+
local matches="" new_matches=""
|
|
199
|
+
trap 'rm -f "$matches" "$new_matches" 2>/dev/null || true' EXIT TERM INT
|
|
200
|
+
# Anthropic transcripts are JSONL (one event per line). Filter user-role
|
|
201
|
+
# events whose content matches the trigger regex, then append new short
|
|
202
|
+
# bullets only. Truncate to 400 chars to keep the log scannable.
|
|
203
|
+
matches="$(mktemp -t composer_learnings.XXXXXX)" || exit 0
|
|
204
|
+
jq -r --arg trig "$TRIGGER" '
|
|
205
|
+
select(.role == "user" or .type == "user")
|
|
206
|
+
| (.content // .message // "") as $raw
|
|
207
|
+
| (if ($raw | type) == "array" then ($raw | map(.text // "") | join(" ")) else ($raw | tostring) end) as $text
|
|
208
|
+
| select($text | test($trig))
|
|
209
|
+
| "- " + ($text | gsub("\\s+"; " ") | .[0:400])
|
|
210
|
+
' "$TRANSCRIPT_PATH" 2>/dev/null > "$matches" || exit 0
|
|
74
211
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
212
|
+
new_matches="$(mktemp -t composer_learnings_new.XXXXXX)" || exit 0
|
|
213
|
+
while IFS= read -r line; do
|
|
214
|
+
[[ -n "$line" ]] || continue
|
|
215
|
+
if [[ ! -f "$OUT" ]] || ! grep -Fxq -- "$line" "$OUT" 2>/dev/null; then
|
|
216
|
+
printf '%s\n' "$line" >> "$new_matches"
|
|
217
|
+
fi
|
|
218
|
+
done < "$matches"
|
|
219
|
+
|
|
220
|
+
if [[ -s "$new_matches" ]]; then
|
|
221
|
+
{
|
|
222
|
+
printf '\n## Session ended %s\n\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
223
|
+
cat "$new_matches"
|
|
224
|
+
} >> "$OUT" 2>/dev/null || true
|
|
83
225
|
fi
|
|
84
|
-
|
|
226
|
+
}
|
|
85
227
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} >> "$OUT" 2>/dev/null || true
|
|
228
|
+
REMAINING_SECONDS="$(remaining_learn_seconds "$LEARN_STARTED" "$LEARN_TIMEOUT_SECONDS")"
|
|
229
|
+
if [[ "$REMAINING_SECONDS" -le 0 ]]; then
|
|
230
|
+
log_learn_timeout "$LEARN_TIMEOUT_MS"
|
|
231
|
+
exit 0
|
|
91
232
|
fi
|
|
92
|
-
|
|
93
|
-
rm -f "$MATCHES" "$NEW_MATCHES"
|
|
233
|
+
run_with_timeout "$REMAINING_SECONDS" process_transcript >/dev/null 2>&1 || true
|
|
94
234
|
|
|
95
235
|
exit 0
|