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.
Files changed (110) hide show
  1. package/CHANGELOG.md +323 -0
  2. package/README.md +6 -3
  3. package/VERSION +1 -1
  4. package/adopt.sh +66 -6
  5. package/bin/cli.js +3 -2
  6. package/kit/AGENT_TEMPLATE.md +2 -1
  7. package/kit/CLAUDE.md +15 -14
  8. package/kit/agents/crew-commit-agent.md +11 -4
  9. package/kit/agents/crew-docs-agent.md +71 -0
  10. package/kit/agents/crew-planner.md +2 -0
  11. package/kit/agents/crew-refuter.md +77 -0
  12. package/kit/agents/crew-review-agent.md +2 -0
  13. package/kit/agents/crew-security-expert.md +3 -1
  14. package/kit/agents/crew-session-manager.md +11 -9
  15. package/kit/eval/doctor.sh +128 -6
  16. package/kit/eval/gate-report.sh +2 -0
  17. package/kit/eval/golden-routing.txt +9 -0
  18. package/kit/eval/lib/crew-map/crew-map.sh +26 -0
  19. package/kit/eval/lib/crew-map/detect.mjs +238 -0
  20. package/kit/eval/lib/crew-map/narrative.mjs +132 -0
  21. package/kit/eval/lib/crew-map/parse.mjs +305 -0
  22. package/kit/eval/lib/crew-map/project.mjs +164 -0
  23. package/kit/eval/lib/crew-map/rules/names.json +38 -0
  24. package/kit/eval/lib/crew-map/rules/packages.json +142 -0
  25. package/kit/eval/lib/crew-map/rules/python-stdlib.json +197 -0
  26. package/kit/eval/lib/crew-map/rules/tech.json +35 -0
  27. package/kit/eval/lib/crew-map/scan.mjs +266 -0
  28. package/kit/eval/lib/crew-map/stacks.mjs +547 -0
  29. package/kit/eval/lib/crew-map/surface.mjs +535 -0
  30. package/kit/eval/lib/findings/findings.mjs +410 -0
  31. package/kit/eval/lib/findings/findings.schema.json +122 -0
  32. package/kit/eval/lib/findings/findings.sh +29 -0
  33. package/kit/eval/lib/gate-log.sh +52 -0
  34. package/kit/eval/lib/json-string.awk +27 -0
  35. package/kit/eval/lib/preset.sh +109 -0
  36. package/kit/eval/lib/presets/default.json +7 -0
  37. package/kit/eval/lib/presets/solo.json +7 -0
  38. package/kit/eval/lib/presets/strict.json +11 -0
  39. package/kit/eval/lib/preview.sh +150 -0
  40. package/kit/eval/lib/review-bench/bench.mjs +273 -0
  41. package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
  42. package/kit/eval/lib/review-prepass/prepass.sh +23 -0
  43. package/kit/eval/lib/settings-default.sh +36 -0
  44. package/kit/eval/lib/settings-json.awk +19 -4
  45. package/kit/eval/lib/star.sh +23 -7
  46. package/kit/eval/lib/statusline.settings.json +6 -0
  47. package/kit/eval/lib/statusline.sh +100 -0
  48. package/kit/eval/lib/verify-cmd.sh +17 -0
  49. package/kit/eval/lib/version.sh +41 -0
  50. package/kit/eval/review-bench.sh +26 -0
  51. package/kit/eval/smoke-test.sh +878 -53
  52. package/kit/hooks/agent-outcome.sh +44 -12
  53. package/kit/hooks/board-sync.sh +2 -23
  54. package/kit/hooks/guard-agent-model.sh +177 -24
  55. package/kit/hooks/guard-bash.sh +295 -204
  56. package/kit/hooks/guard-commit-scan.sh +10 -2
  57. package/kit/hooks/guard-question.sh +73 -0
  58. package/kit/hooks/guard-write.sh +24 -7
  59. package/kit/hooks/precompact-snapshot.sh +75 -0
  60. package/kit/hooks/prompt-approval.sh +389 -103
  61. package/kit/hooks/refuter-ledger.sh +80 -0
  62. package/kit/hooks/session-rehydrate.sh +25 -2
  63. package/kit/hooks/session-update-check.sh +9 -8
  64. package/kit/hooks/trace-blocklist.txt +9 -0
  65. package/kit/owned-blobs.tsv +4 -4
  66. package/kit/settings.json +51 -0
  67. package/kit/skills/crew-approve/SKILL.md +7 -0
  68. package/kit/skills/crew-code-review/SKILL.md +16 -0
  69. package/kit/skills/crew-doctor/SKILL.md +5 -1
  70. package/kit/skills/crew-handoff/SKILL.md +3 -2
  71. package/kit/skills/crew-map/SKILL.md +55 -0
  72. package/kit/skills/crew-review/SKILL.md +4 -1
  73. package/kit/skills/crew-ship/SKILL.md +3 -1
  74. package/kit/skills/crew-update/SKILL.md +1 -1
  75. package/kit/skills/incident-runbook/SKILL.md +2 -2
  76. package/kit/skills/security-scan/references/verify.md +6 -3
  77. package/kit/studio/README.md +25 -2
  78. package/kit/studio/server/index.js +187 -21
  79. package/kit/studio/server/lib/code-stamp.js +35 -0
  80. package/kit/studio/server/lib/crew-map.js +113 -0
  81. package/kit/studio/server/lib/graph.js +9 -1
  82. package/kit/studio/server/lib/kit-telemetry.js +30 -5
  83. package/kit/studio/server/lib/lan.js +87 -0
  84. package/kit/studio/server/lib/model-outcomes.js +130 -15
  85. package/kit/studio/server/lib/owned-stream.js +56 -0
  86. package/kit/studio/server/lib/pricing.js +3 -0
  87. package/kit/studio/server/lib/projects.js +11 -4
  88. package/kit/studio/server/lib/redact.js +60 -0
  89. package/kit/studio/server/lib/remote.js +185 -0
  90. package/kit/studio/server/lib/session.js +1 -1
  91. package/kit/studio/server/lib/terminal.js +51 -13
  92. package/kit/studio/server/lib/transcript.js +32 -0
  93. package/kit/studio/server/lib/usage.js +74 -14
  94. package/kit/studio/web/app.js +287 -17
  95. package/kit/studio/web/canvas.js +58 -122
  96. package/kit/studio/web/chat.js +114 -24
  97. package/kit/studio/web/graph-plan.js +13 -6
  98. package/kit/studio/web/index.html +10 -5
  99. package/kit/studio/web/liveness.js +14 -0
  100. package/kit/studio/web/map-plan.js +410 -0
  101. package/kit/studio/web/map.js +596 -0
  102. package/kit/studio/web/models-plan.js +61 -7
  103. package/kit/studio/web/models.js +19 -3
  104. package/kit/studio/web/qr.js +200 -0
  105. package/kit/studio/web/style.css +165 -0
  106. package/kit/studio/web/tokens.css +6 -0
  107. package/kit/studio/web/usage.js +12 -0
  108. package/kit/studio/web/viewport.js +160 -0
  109. package/package.json +6 -5
  110. 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
- if [ "$FAILED" = 0 ] && [ -x "$DIR/commit-msg" ]; then
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"
@@ -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"; unset _crew_d
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="${CREW_GATE_LOG:-}"
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