crewforth 3.1.1 → 3.2.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +323 -0
- package/README.md +6 -3
- package/VERSION +1 -1
- package/adopt.sh +66 -6
- package/bin/cli.js +3 -2
- package/kit/AGENT_TEMPLATE.md +2 -1
- package/kit/CLAUDE.md +15 -14
- package/kit/agents/crew-commit-agent.md +11 -4
- package/kit/agents/crew-docs-agent.md +71 -0
- package/kit/agents/crew-planner.md +2 -0
- package/kit/agents/crew-refuter.md +77 -0
- package/kit/agents/crew-review-agent.md +2 -0
- package/kit/agents/crew-security-expert.md +3 -1
- package/kit/agents/crew-session-manager.md +11 -9
- package/kit/eval/doctor.sh +128 -6
- package/kit/eval/gate-report.sh +2 -0
- package/kit/eval/golden-routing.txt +9 -0
- package/kit/eval/lib/crew-map/crew-map.sh +26 -0
- package/kit/eval/lib/crew-map/detect.mjs +238 -0
- package/kit/eval/lib/crew-map/narrative.mjs +132 -0
- package/kit/eval/lib/crew-map/parse.mjs +305 -0
- package/kit/eval/lib/crew-map/project.mjs +164 -0
- package/kit/eval/lib/crew-map/rules/names.json +38 -0
- package/kit/eval/lib/crew-map/rules/packages.json +142 -0
- package/kit/eval/lib/crew-map/rules/python-stdlib.json +197 -0
- package/kit/eval/lib/crew-map/rules/tech.json +35 -0
- package/kit/eval/lib/crew-map/scan.mjs +266 -0
- package/kit/eval/lib/crew-map/stacks.mjs +547 -0
- package/kit/eval/lib/crew-map/surface.mjs +535 -0
- package/kit/eval/lib/findings/findings.mjs +410 -0
- package/kit/eval/lib/findings/findings.schema.json +122 -0
- package/kit/eval/lib/findings/findings.sh +29 -0
- package/kit/eval/lib/gate-log.sh +52 -0
- package/kit/eval/lib/json-string.awk +27 -0
- package/kit/eval/lib/preset.sh +109 -0
- package/kit/eval/lib/presets/default.json +7 -0
- package/kit/eval/lib/presets/solo.json +7 -0
- package/kit/eval/lib/presets/strict.json +11 -0
- package/kit/eval/lib/preview.sh +150 -0
- package/kit/eval/lib/review-bench/bench.mjs +273 -0
- package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
- package/kit/eval/lib/review-prepass/prepass.sh +23 -0
- package/kit/eval/lib/settings-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +19 -4
- package/kit/eval/lib/star.sh +23 -7
- package/kit/eval/lib/statusline.settings.json +6 -0
- package/kit/eval/lib/statusline.sh +100 -0
- package/kit/eval/lib/verify-cmd.sh +17 -0
- package/kit/eval/lib/version.sh +41 -0
- package/kit/eval/review-bench.sh +26 -0
- package/kit/eval/smoke-test.sh +878 -53
- package/kit/hooks/agent-outcome.sh +44 -12
- package/kit/hooks/board-sync.sh +2 -23
- package/kit/hooks/guard-agent-model.sh +177 -24
- package/kit/hooks/guard-bash.sh +295 -204
- package/kit/hooks/guard-commit-scan.sh +10 -2
- package/kit/hooks/guard-question.sh +73 -0
- package/kit/hooks/guard-write.sh +24 -7
- package/kit/hooks/precompact-snapshot.sh +75 -0
- package/kit/hooks/prompt-approval.sh +389 -103
- package/kit/hooks/refuter-ledger.sh +80 -0
- package/kit/hooks/session-rehydrate.sh +25 -2
- package/kit/hooks/session-update-check.sh +9 -8
- package/kit/hooks/trace-blocklist.txt +9 -0
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +51 -0
- package/kit/skills/crew-approve/SKILL.md +7 -0
- package/kit/skills/crew-code-review/SKILL.md +16 -0
- package/kit/skills/crew-doctor/SKILL.md +5 -1
- package/kit/skills/crew-handoff/SKILL.md +3 -2
- package/kit/skills/crew-map/SKILL.md +55 -0
- package/kit/skills/crew-review/SKILL.md +4 -1
- package/kit/skills/crew-ship/SKILL.md +3 -1
- package/kit/skills/crew-update/SKILL.md +1 -1
- package/kit/skills/incident-runbook/SKILL.md +2 -2
- package/kit/skills/security-scan/references/verify.md +6 -3
- package/kit/studio/README.md +25 -2
- package/kit/studio/server/index.js +187 -21
- package/kit/studio/server/lib/code-stamp.js +35 -0
- package/kit/studio/server/lib/crew-map.js +113 -0
- package/kit/studio/server/lib/graph.js +9 -1
- package/kit/studio/server/lib/kit-telemetry.js +30 -5
- package/kit/studio/server/lib/lan.js +87 -0
- package/kit/studio/server/lib/model-outcomes.js +130 -15
- package/kit/studio/server/lib/owned-stream.js +56 -0
- package/kit/studio/server/lib/pricing.js +3 -0
- package/kit/studio/server/lib/projects.js +11 -4
- package/kit/studio/server/lib/redact.js +60 -0
- package/kit/studio/server/lib/remote.js +185 -0
- package/kit/studio/server/lib/session.js +1 -1
- package/kit/studio/server/lib/terminal.js +51 -13
- package/kit/studio/server/lib/transcript.js +32 -0
- package/kit/studio/server/lib/usage.js +74 -14
- package/kit/studio/web/app.js +287 -17
- package/kit/studio/web/canvas.js +58 -122
- package/kit/studio/web/chat.js +114 -24
- package/kit/studio/web/graph-plan.js +13 -6
- package/kit/studio/web/index.html +10 -5
- package/kit/studio/web/liveness.js +14 -0
- package/kit/studio/web/map-plan.js +410 -0
- package/kit/studio/web/map.js +596 -0
- package/kit/studio/web/models-plan.js +61 -7
- package/kit/studio/web/models.js +19 -3
- package/kit/studio/web/qr.js +200 -0
- package/kit/studio/web/style.css +165 -0
- package/kit/studio/web/tokens.css +6 -0
- package/kit/studio/web/usage.js +12 -0
- package/kit/studio/web/viewport.js +160 -0
- package/package.json +6 -5
- package/start.sh +24 -1
|
@@ -588,8 +588,11 @@ fi
|
|
|
588
588
|
|
|
589
589
|
# The message carries its own trace risk (a co-author trailer lives there, not in the diff), and commit-msg is
|
|
590
590
|
# what scans it. Reuse it on the -m value when there is one; an editor-composed message is not visible here and
|
|
591
|
-
# stays the git hook's job.
|
|
592
|
-
|
|
591
|
+
# stays the git hook's job. It is scanned even when the staged content already failed: a refusal that names one
|
|
592
|
+
# problem gets the commit retried with the other still in it (measured on a real project, 2026-10-21: a silenced
|
|
593
|
+
# lint check was named, the session-link trailer in the same message was not, and the message was shown to the
|
|
594
|
+
# user again).
|
|
595
|
+
if [ -x "$DIR/commit-msg" ]; then
|
|
593
596
|
# The message is tokenized the way a shell does (crew_opt_values), which matters because a real commit message
|
|
594
597
|
# is MULTI-LINE: a line-oriented `sed` extraction found the subject and stopped, so a co-author trailer on line
|
|
595
598
|
# 3 — the single most likely §4.1 violation, and the one the bare arm of the eval actually produced — went
|
|
@@ -640,6 +643,7 @@ $(bash "$DIR/commit-msg" "$MF" 2>&1)" || FAILED=1
|
|
|
640
643
|
| sed -n 's/.*[[:space:]]--\{0,1\}[Ff]\(ile\)\{0,1\}[[:space:]=]\{1,\}\([^[:space:];&|]\{1,\}\).*/\2/p' | head -1)"
|
|
641
644
|
MFILE="${MFILE%\"}"; MFILE="${MFILE#\"}"; MFILE="${MFILE%\'}"; MFILE="${MFILE#\'}"
|
|
642
645
|
if _grep "$CMD" -qE '(^|[[:space:]])(-F|--file)([[:space:]=]|$)' && { [ -z "$MFILE" ] || [ ! -f "$MFILE" ]; }; then
|
|
646
|
+
[ "$FAILED" = 1 ] && printf '%s\n' "$OUT" >&2
|
|
643
647
|
echo "GUARD (§4.1): this commit reads its message from a file (-F), and the command could not be parsed" >&2
|
|
644
648
|
echo "well enough to know which file. Scan cannot run, so the commit is refused. Commit with a plain" >&2
|
|
645
649
|
echo "-F <path> (or -m) in a command of its own." >&2
|
|
@@ -656,6 +660,7 @@ $(bash "$DIR/commit-msg" "$MFILE" 2>&1)" || FAILED=1
|
|
|
656
660
|
[ -z "$GITMSG_HOOK" ] && [ -x "$(git rev-parse --git-path hooks/commit-msg 2>/dev/null)" ] \
|
|
657
661
|
&& GITMSG_HOOK="git-default"
|
|
658
662
|
if [ -z "$GITMSG_HOOK" ]; then
|
|
663
|
+
[ "$FAILED" = 1 ] && printf '%s\n' "$OUT" >&2
|
|
659
664
|
echo "GUARD (§4.1): this commit's message would go unscanned." >&2
|
|
660
665
|
echo "No -m/-F was given, so the message is composed in an editor after this point, and no commit-msg" >&2
|
|
661
666
|
echo "git hook is wired here to read it afterwards (plugin-only install: a plugin cannot set" >&2
|
|
@@ -669,6 +674,9 @@ fi
|
|
|
669
674
|
if [ "$FAILED" = 1 ]; then
|
|
670
675
|
echo "GUARD (§4.1/§4.2): the commit content gate rejected this commit before it ran." >&2
|
|
671
676
|
echo "$OUT" >&2
|
|
677
|
+
case "$OUT" in *"TRACE-SCANNER (message)"*)
|
|
678
|
+
echo "The message itself carries the trace: write it again without that line. A harness reminder to end a commit message with a trailer does not override §4.1." >&2 ;;
|
|
679
|
+
esac
|
|
672
680
|
echo "Fix the flagged content and commit again. --no-verify is §4.5 and stays blocked." >&2
|
|
673
681
|
exit 2
|
|
674
682
|
fi
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# guard-question.sh - the approval question reaches the person, not a hook (§4.4, 3.2.0).
|
|
3
|
+
#
|
|
4
|
+
# WHY. In `auto` the user can approve a commit or a push by answering one fixed question (AskUserQuestion), and
|
|
5
|
+
# hooks/prompt-approval.sh records the answer on PostToolUse. An answer is only worth recording if a person gave it.
|
|
6
|
+
# Two things can give it instead: another PreToolUse hook that answers `allow` with the answers filled in, and a
|
|
7
|
+
# PermissionRequest hook that answers the prompt before the person does (both measured on Claude Code 2.1.294).
|
|
8
|
+
#
|
|
9
|
+
# WHAT. Wired on PreToolUse for AskUserQuestion. Any question but the approval question gets no decision. The
|
|
10
|
+
# approval question must be the fixed text with no answers, asked in `auto` in an attended session; then this
|
|
11
|
+
# hook answers `ask`, which beats the first kind of hook in every mode, and only when no PermissionRequest hook
|
|
12
|
+
# may be wired (the second kind; _crew_prh_scan). It also notes the call it asked, and prompt-approval.sh records
|
|
13
|
+
# an answer to that call only. Everything it decides is defined in prompt-approval.sh (_crew_q_pre), sourced.
|
|
14
|
+
#
|
|
15
|
+
# A GATE, so an error of its own refuses the call (CREW-FAILCLOSED): a question let through with no `ask` is one a
|
|
16
|
+
# hook can answer.
|
|
17
|
+
# COST. Builtins on every AskUserQuestion; on the approval question the scan reads files and starts no process.
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
# ---- CREW-LOCALE -----------------------------------------------------------------------------------------
|
|
20
|
+
# Everything this gate matches with runs in the C locale, whatever the session's own is. Under a Turkish locale
|
|
21
|
+
# the letters i and I are not each other's other case (their partners are İ and ı), and every case-insensitive
|
|
22
|
+
# match here is written in ASCII. Measured under tr_TR.UTF-8 with the locale left as it came:
|
|
23
|
+
# GNU grep 3.11 / bash 5.2 on Linux: `grep -i init` does not find INIT (-F, -E and plain alike);
|
|
24
|
+
# bash's nocasematch does not match GIT against git; `[A-Za-z]` in a regex does not hold I; awk's tolower
|
|
25
|
+
# turns GIT into gıt. The suite, run whole under that locale, went from 4 failures to 30: a recursive
|
|
26
|
+
# Remove-Item, a write to a gate file, `git config --remove-section core`, a read of a nested .env and a
|
|
27
|
+
# staged key all passed.
|
|
28
|
+
# GNU grep 3.0 in Git Bash on Windows: `grep -iF` with an ASCII pattern aborts (exit 134), and the pre-commit
|
|
29
|
+
# scan for private strings read that as "no match".
|
|
30
|
+
# macOS (BSD grep, bash 3.2): none of it; tr_TR.UTF-8 folds i and I the ASCII way there.
|
|
31
|
+
# The cost: a letter outside ASCII has no other case in the C locale. The two git hooks that scan a user's own
|
|
32
|
+
# words look a second time under the session's locale for a pattern that holds such a letter (_CREW_LOCALE).
|
|
33
|
+
# Byte-identical in every gate; the suite pins it.
|
|
34
|
+
_CREW_LOCALE="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
|
|
35
|
+
case "$_CREW_LOCALE" in C|POSIX) _CREW_LOCALE="" ;; esac
|
|
36
|
+
export LC_ALL=C
|
|
37
|
+
# ---- /CREW-LOCALE
|
|
38
|
+
# ---- CREW-FAILCLOSED ---------------------------------------------------------------------------------------
|
|
39
|
+
# A gate that stops on an error of its own must refuse the call, not let it through. Claude Code blocks a tool call
|
|
40
|
+
# on exit 2 only; a hook that dies with 1, or runs on past the rule that broke, has allowed it. Both happened in
|
|
41
|
+
# one defect (measured, bash 5): a local array declared and not set is "unbound" under `set -u`, the rule that read
|
|
42
|
+
# it was abandoned in the middle, the script went on with the NEXT top-level command and left with 0, and
|
|
43
|
+
# `rm .claude/hooks/pre-commit` passed. bash does one of two things with such an error, by the kind of error:
|
|
44
|
+
# * it leaves the shell with a status that is neither 0 nor 2 -> the EXIT trap below turns that into a refusal;
|
|
45
|
+
# * it abandons the top-level command it was in and continues -> the whole gate IS one top-level command,
|
|
46
|
+
# _gate_main, which only ever ends by `exit`. If it RETURNS, it was abandoned, and the line after it refuses.
|
|
47
|
+
# Not covered, and said so: an error inside a command substitution (only that subshell ends), and a command
|
|
48
|
+
# that is not found (status 127, the script continues inside the same command).
|
|
49
|
+
# Byte-identical in every PreToolUse gate; the suite pins it. No process is started.
|
|
50
|
+
_crew_stop(){ # $1 = what happened
|
|
51
|
+
declare -F gatelog >/dev/null 2>&1 && gatelog BLOCK 4.5 "the gate stopped on an error of its own"
|
|
52
|
+
echo "GUARD (§4.5): this gate stopped on an error of its own ($1) before it finished judging the call, so the call is refused." >&2
|
|
53
|
+
echo "Nothing about the call itself was found. The error is the line above these two; it is a defect in Crewforth, not in what you ran. Run the doctor (bash .claude/eval/doctor.sh, Bash tool, not PowerShell) and report it." >&2
|
|
54
|
+
exit 2
|
|
55
|
+
}
|
|
56
|
+
trap '_crew_rc=$?; trap - EXIT; case "$_crew_rc" in 0|2) exit "$_crew_rc" ;; esac; _crew_stop "exit status $_crew_rc"' EXIT
|
|
57
|
+
# ---- /CREW-FAILCLOSED
|
|
58
|
+
_gate_main(){
|
|
59
|
+
local here="${BASH_SOURCE%/*}"
|
|
60
|
+
[ "$here" = "${BASH_SOURCE}" ] && here=.
|
|
61
|
+
IFS= read -r -d '' INPUT || true
|
|
62
|
+
case "$INPUT" in *'Crewforth approval'*) ;; *) exit 0 ;; esac # not the approval question: nothing to decide
|
|
63
|
+
if [ ! -f "$here/prompt-approval.sh" ]; then
|
|
64
|
+
echo "GUARD (§4.4): prompt-approval.sh is not beside this gate, so the approval question cannot be checked and is not asked. Update Crewforth; the user can type /crew-approve instead." >&2
|
|
65
|
+
exit 2
|
|
66
|
+
fi
|
|
67
|
+
. "$here/prompt-approval.sh"
|
|
68
|
+
declare -F _crew_q_pre >/dev/null 2>&1 || { echo "GUARD (§4.4): prompt-approval.sh beside this gate is an older one, so the approval question is not asked. Update Crewforth; the user can type /crew-approve instead." >&2; exit 2; }
|
|
69
|
+
_crew_q_pre
|
|
70
|
+
exit 0
|
|
71
|
+
}
|
|
72
|
+
_gate_main "$@"
|
|
73
|
+
_crew_stop "a command of the gate was abandoned"
|
package/kit/hooks/guard-write.sh
CHANGED
|
@@ -72,7 +72,8 @@ trap '_crew_rc=$?; trap - EXIT; case "$_crew_rc" in 0|2) exit "$_crew_rc" ;; esa
|
|
|
72
72
|
_gate_main(){
|
|
73
73
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
74
74
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
75
|
-
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
75
|
+
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
76
|
+
[ -f "$_crew_d/../eval/lib/gate-log.sh" ] && . "$_crew_d/../eval/lib/gate-log.sh"; unset _crew_d # where the gate log goes
|
|
76
77
|
# Read with the builtin, as guard-powershell.sh does: `INPUT="$(cat)"` is a subshell plus a process on EVERY call
|
|
77
78
|
# of this hook. `read` returns 1 at end of input; the text is read regardless.
|
|
78
79
|
IFS= read -r -d '' INPUT || true
|
|
@@ -365,12 +366,7 @@ block(){ # $1 = rule name for the log (must keep the `gate-file edit` prefix
|
|
|
365
366
|
# and with the same rule about the payload: the path is NOT recorded unless CREW_GATE_LOG_CMD=1, because
|
|
366
367
|
# /crew-gates reports rule names and counts and never the argument. Logged after the verdict; it cannot
|
|
367
368
|
# change it. CREW_GATE_LOG overrides the path; point it at /dev/null to turn recording off.
|
|
368
|
-
_GL="$
|
|
369
|
-
if [ -z "$_GL" ] && [ -d ".claude" ]; then
|
|
370
|
-
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1 || git check-ignore -q ".claude/gate-log.tsv" 2>/dev/null; then
|
|
371
|
-
_GL=".claude/gate-log.tsv"
|
|
372
|
-
fi
|
|
373
|
-
fi
|
|
369
|
+
_GL=""; declare -F crew_gate_log_write >/dev/null && { crew_gate_log_write; _GL="$_CREW_GL"; }
|
|
374
370
|
if [ -n "$_GL" ]; then
|
|
375
371
|
if [ "${CREW_GATE_LOG_CMD:-0}" = 1 ]; then
|
|
376
372
|
printf 'BLOCK\t§4.5\t%s\t%s\n' "$1" \
|
|
@@ -388,6 +384,7 @@ WHY_SCRIPT="This file is a gate script — rewriting it would disarm the trace/s
|
|
|
388
384
|
WHY_DISC="This file is Crewforth's discipline document — it IS the text of §4.1-§4.5, so editing it empties the rules the gates enforce."
|
|
389
385
|
WHY_LINK="A parent directory of this path is a symlink and it resolves into a gate directory, so the write would land on a gate file."
|
|
390
386
|
WHY_MODEL="This file is what model routing decides from: the user's rules, a class's floor, the critical paths, or the record of a call and its result. Only the user and Crewforth's own hooks write it, so a session cannot lower its own floor or erase its own failure."
|
|
387
|
+
WHY_REFUTER="This file is the refuter ledger: what crew-refuter concluded about a finding, written by Crewforth's hook from the agent's own task and answer. A findings document is held to it, so a session that writes it would verify its own findings."
|
|
391
388
|
WHY_APPR="This file records the user's own approval for a commit or a push (§4.4). Only the user's message writes it, so a session cannot approve its own commit."
|
|
392
389
|
WHY_GITCFG="This file holds core.hooksPath: writing it can switch the git hooks off without touching one of them. Settings go through git config, which the Bash guard reads."
|
|
393
390
|
WHY_LONG="The path in this payload is longer than any filesystem accepts. It is refused rather than parsed, because parsing it is the slow path an attacker would aim at."
|
|
@@ -545,6 +542,10 @@ _is_gate(){ # 0 = gate file; sets GATE_RULE and GATE_WHY
|
|
|
545
542
|
# Matched by name wherever it is: a linked worktree keeps its git directory elsewhere.
|
|
546
543
|
*/[Cc][Rr][Ee][Ww][Ff][Oo][Rr][Tt][Hh]-[Aa][Pp][Pp][Rr][Oo][Vv][Aa][Ll]|[Cc][Rr][Ee][Ww][Ff][Oo][Rr][Tt][Hh]-[Aa][Pp][Pp][Rr][Oo][Vv][Aa][Ll])
|
|
547
544
|
GATE_RULE="approval-record edit (Write/Edit tools)"; GATE_WHY="$WHY_APPR"; return 0 ;;
|
|
545
|
+
# The refuter ledger (hooks/refuter-ledger.sh): what crew-refuter concluded, as the hook read it from the agent's
|
|
546
|
+
# own task and answer. findings.mjs --ledger trusts it; a session that writes it verifies its own findings.
|
|
547
|
+
*/.[Cc][Ll][Aa][Uu][Dd][Ee]/[Ss][Tt][Aa][Tt][Ee]/[Cc][Rr][Ee][Ww]-[Rr][Ee][Ff][Uu][Tt][Ee][Rr].[Tt][Ss][Vv]|.[Cc][Ll][Aa][Uu][Dd][Ee]/[Ss][Tt][Aa][Tt][Ee]/[Cc][Rr][Ee][Ww]-[Rr][Ee][Ff][Uu][Tt][Ee][Rr].[Tt][Ss][Vv])
|
|
548
|
+
GATE_RULE="gate-file edit (refuter ledger)"; GATE_WHY="$WHY_REFUTER"; return 0 ;;
|
|
548
549
|
# What model routing decides from (hooks/guard-agent-model.sh, hooks/agent-outcome.sh): the user's own rules,
|
|
549
550
|
# the floors raised from the record of outcomes, the list of critical paths, and the records of calls and
|
|
550
551
|
# results. A session that writes one lowers its own floor or wipes its own failure.
|
|
@@ -631,6 +632,22 @@ case "$INPUT" in *'"agent_id"'*)
|
|
|
631
632
|
esac
|
|
632
633
|
fi
|
|
633
634
|
fi ;;
|
|
635
|
+
*)
|
|
636
|
+
# The session's own write to a critical path (no agent_id: the main thread). Not refused (K7: the user may be
|
|
637
|
+
# doing exactly this, and an agent is a choice, not a rule); recorded, so it is seen: a NOTE line in the gate log,
|
|
638
|
+
# which Studio's Gates tab and the status line read. The same definition of "critical path" the agents are held
|
|
639
|
+
# to; a file outside the project is not the project's critical path.
|
|
640
|
+
if [ "${CREW_MODEL_ROUTING:-}" != off ] && [ -n "${FP:-}" ]; then
|
|
641
|
+
_gwm="${BASH_SOURCE%/*}"; [ "$_gwm" = "${BASH_SOURCE}" ] && _gwm=.
|
|
642
|
+
if [ -f "$_gwm/guard-agent-model.sh" ]; then
|
|
643
|
+
. "$_gwm/guard-agent-model.sh"
|
|
644
|
+
if declare -F _am_crit_path >/dev/null 2>&1 && _am_crit_path "$FP"; then
|
|
645
|
+
case "$_AMRP" in /*|[A-Za-z]:*) ;; *)
|
|
646
|
+
declare -F _am_log >/dev/null 2>&1 && _am_log "note: the session wrote a critical path itself, not through an agent: $_AMRP ($_AMW)" ;;
|
|
647
|
+
esac
|
|
648
|
+
fi
|
|
649
|
+
fi
|
|
650
|
+
fi ;;
|
|
634
651
|
esac
|
|
635
652
|
|
|
636
653
|
# ---- team board: you may not start work nobody knows you started -----------------------------------------------
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreCompact hook — just before the context is compacted, write down the state of the work in a few lines, so the
|
|
3
|
+
# session that comes back after the compaction is told it (session-rehydrate.sh reads it on SessionStart, source
|
|
4
|
+
# compact). A compaction summary is the model's own retelling; this is what the files and the gates say.
|
|
5
|
+
#
|
|
6
|
+
# What it writes, at most ~2 KB: the branch, the uncommitted files (five named, the rest counted), the last refusal in
|
|
7
|
+
# the gate log, how many findings each document under .crewforth/ holds, and whether a handover
|
|
8
|
+
# (docs/SESSION_STATE.md) is there. With the session id and the trigger, so a snapshot is only read back into the
|
|
9
|
+
# session that wrote it.
|
|
10
|
+
#
|
|
11
|
+
# Where: inside the repository's git directory (crewforth-session-snapshot.json), so it is never committed even in a
|
|
12
|
+
# project that tracks .claude/; outside a repository, .claude/state/session-snapshot.json.
|
|
13
|
+
#
|
|
14
|
+
# NEVER BLOCKS. PreCompact exit 2 blocks the compaction (docs: hooks, PreCompact), and blocking an automatic one at
|
|
15
|
+
# the context limit fails the request that needed it. Every path here ends in exit 0, and nothing is printed: a
|
|
16
|
+
# PreCompact hook's output does not reach the summary.
|
|
17
|
+
set -u
|
|
18
|
+
IN="$(cat 2>/dev/null || true)"
|
|
19
|
+
_here="${BASH_SOURCE%[/\\]*}"; [ "$_here" = "${BASH_SOURCE}" ] && _here=.
|
|
20
|
+
LIB="$_here/../eval/lib"
|
|
21
|
+
|
|
22
|
+
ROOT="${CLAUDE_PROJECT_DIR:-}"
|
|
23
|
+
[ -n "$ROOT" ] || ROOT="$(printf '%s' "$IN" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
|
|
24
|
+
[ -n "$ROOT" ] || ROOT="$PWD"
|
|
25
|
+
ROOT="${ROOT//\\\\//}"; ROOT="${ROOT//\\//}"
|
|
26
|
+
cd "$ROOT" 2>/dev/null || exit 0
|
|
27
|
+
|
|
28
|
+
field(){ local v="${IN#*\"$1\"}"; [ "$v" = "$IN" ] && return 1; v="${v#*:}"; v="${v#"${v%%[![:space:]]*}"}"; case "$v" in \"*) v="${v#\"}"; printf '%s' "${v%%\"*}" ;; *) printf '%s' "${v%%[,\}]*}" ;; esac; }
|
|
29
|
+
SID="$(field session_id)" || SID=""
|
|
30
|
+
TRIG="$(field trigger)" || TRIG=""
|
|
31
|
+
case "$SID" in ''|*[!A-Za-z0-9._-]*) exit 0 ;; esac # a snapshot no session can claim is not written
|
|
32
|
+
|
|
33
|
+
# ---- what the files and the gates say ----
|
|
34
|
+
LINES=()
|
|
35
|
+
GD=""
|
|
36
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
37
|
+
GD="$(git rev-parse --absolute-git-dir 2>/dev/null)" || GD=""
|
|
38
|
+
BR="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || BR=""
|
|
39
|
+
[ -n "$BR" ] && LINES+=("branch $BR")
|
|
40
|
+
# quotePath off: a name in Turkish (or any non-ASCII) is given as written, not as octal escapes (seen on Windows).
|
|
41
|
+
CH="$(git -c core.quotePath=false status --porcelain 2>/dev/null)" || CH=""
|
|
42
|
+
if [ -n "$CH" ]; then
|
|
43
|
+
n=0; names=""
|
|
44
|
+
while IFS= read -r l; do n=$((n+1)); [ "$n" -le 5 ] && names="${names:+$names, }${l:3}"; done <<< "$CH"
|
|
45
|
+
LINES+=("$n uncommitted file(s): $names$([ "$n" -gt 5 ] && printf ', and %d more' $((n-5)))")
|
|
46
|
+
else LINES+=("nothing uncommitted"); fi
|
|
47
|
+
fi
|
|
48
|
+
if [ -f "$LIB/gate-log.sh" ]; then
|
|
49
|
+
. "$LIB/gate-log.sh"; crew_gate_log_read
|
|
50
|
+
if [ -n "${_CREW_GL:-}" ] && [ -f "$_CREW_GL" ]; then
|
|
51
|
+
last=""; while IFS= read -r l; do case "$l" in BLOCK$'\t'*) last="$l" ;; esac; done < "$_CREW_GL"
|
|
52
|
+
if [ -n "$last" ]; then IFS=$'\t' read -r _ rule why _ <<< "$last"; LINES+=("the last refusal in the gate log: ${rule} ${why:0:120}"); fi
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
fnd=""
|
|
56
|
+
for f in .crewforth/*.json; do
|
|
57
|
+
[ -f "$f" ] || continue
|
|
58
|
+
c=0; while IFS= read -r l; do case "$l" in *'"rule": '*) c=$((c+1)) ;; esac; done < "$f"
|
|
59
|
+
[ "$c" -gt 0 ] && { b="${f##*/}"; fnd="${fnd:+$fnd, }${b%.json} $c"; }
|
|
60
|
+
done
|
|
61
|
+
[ -n "$fnd" ] && LINES+=("findings on record: $fnd")
|
|
62
|
+
[ -s docs/SESSION_STATE.md ] && LINES+=("a handover is at docs/SESSION_STATE.md")
|
|
63
|
+
[ "${#LINES[@]}" -gt 0 ] || exit 0
|
|
64
|
+
|
|
65
|
+
SUM=""; for l in "${LINES[@]}"; do SUM="${SUM:+$SUM; }$l"; done
|
|
66
|
+
SUM="${SUM:0:1800}"
|
|
67
|
+
|
|
68
|
+
# ---- where it goes ----
|
|
69
|
+
if [ -n "$GD" ] && [ -d "$GD" ]; then OUT="$GD/crewforth-session-snapshot.json"
|
|
70
|
+
else mkdir -p .claude/state 2>/dev/null || exit 0; OUT=".claude/state/session-snapshot.json"; fi
|
|
71
|
+
if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v NOW '%(%Y-%m-%dT%H:%M:%SZ)T' -1; else NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"; fi
|
|
72
|
+
ESC="$(printf '%s\n' "$SUM" | LC_ALL=C awk -f "$LIB/json-string.awk" 2>/dev/null)" || exit 0
|
|
73
|
+
printf '{"session_id":"%s","trigger":"%s","at":"%s","summary":"%s"}\n' "$SID" "${TRIG//[!a-z]/}" "$NOW" "$ESC" > "$OUT.$$" 2>/dev/null && mv -f "$OUT.$$" "$OUT" 2>/dev/null
|
|
74
|
+
rm -f "$OUT.$$" 2>/dev/null
|
|
75
|
+
exit 0
|