cleargate 0.14.0 → 0.15.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/MANIFEST.json +71 -15
  3. package/dist/admin-api/index.cjs +0 -1
  4. package/dist/admin-api/index.js +1 -2
  5. package/dist/auth/factory.cjs +0 -1
  6. package/dist/auth/factory.js +2 -3
  7. package/dist/auth/require-token.cjs +0 -1
  8. package/dist/auth/require-token.js +1 -2
  9. package/dist/auth/token-store.cjs +0 -1
  10. package/dist/auth/token-store.js +1 -2
  11. package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
  12. package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
  13. package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
  14. package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
  15. package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
  16. package/dist/cli.cjs +1564 -1414
  17. package/dist/cli.js +1514 -1364
  18. package/dist/lib/ledger.cjs +0 -1
  19. package/dist/lib/ledger.js +1 -2
  20. package/dist/lib/lifecycle-reconcile.cjs +0 -1
  21. package/dist/lib/lifecycle-reconcile.js +2 -3
  22. package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
  23. package/package.json +4 -3
  24. package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
  25. package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
  26. package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
  27. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
  28. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
  29. package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
  30. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
  31. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
  32. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
  33. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
  34. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
  35. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
  36. package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
  37. package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
  38. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
  39. package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
  40. package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
  41. package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
  42. package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
  44. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
  45. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +27 -0
  46. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
  47. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
  48. package/templates/cleargate-planning/MANIFEST.json +71 -15
  49. package/dist/admin-api/index.cjs.map +0 -1
  50. package/dist/admin-api/index.js.map +0 -1
  51. package/dist/auth/factory.cjs.map +0 -1
  52. package/dist/auth/factory.js.map +0 -1
  53. package/dist/auth/require-token.cjs.map +0 -1
  54. package/dist/auth/require-token.js.map +0 -1
  55. package/dist/auth/token-store.cjs.map +0 -1
  56. package/dist/auth/token-store.js.map +0 -1
  57. package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
  58. package/dist/chunk-5DI2Z3C2.js.map +0 -1
  59. package/dist/chunk-BTSZOEWC.js.map +0 -1
  60. package/dist/chunk-E3X7IE5E.js.map +0 -1
  61. package/dist/chunk-PDE37WFQ.js.map +0 -1
  62. package/dist/cli.cjs.map +0 -1
  63. package/dist/cli.js.map +0 -1
  64. package/dist/lib/ledger.cjs.map +0 -1
  65. package/dist/lib/ledger.js.map +0 -1
  66. package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
  67. package/dist/lib/lifecycle-reconcile.js.map +0 -1
  68. package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
  69. package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
  70. package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
  71. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
  72. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
  73. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
  74. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
  75. package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
  76. package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
  77. package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
  78. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
  79. package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
  80. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
  81. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
  82. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
  83. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
  84. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
  85. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
  86. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
  87. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
  88. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
  89. package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
  90. package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
  91. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
  92. package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
  93. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
  94. package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
  95. package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
  96. package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
  97. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
  98. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
  99. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
  100. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
  101. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
  102. package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
  103. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
  104. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
  105. package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
  106. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
  107. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
  108. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
  109. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
  110. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
  111. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
  112. package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
  113. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
  114. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
  115. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
  116. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
  117. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
  118. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
  119. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
  120. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
  121. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
  122. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
  123. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
  124. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
  125. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
  126. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
  127. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
  128. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
  129. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
  130. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
  131. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
  132. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
  133. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
  134. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
  135. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
  136. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
  137. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
  138. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
  139. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
  140. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
  141. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
  142. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
  143. package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
  144. package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
  145. package/dist/templates/synthesis/active-sprint.md +0 -30
  146. package/dist/templates/synthesis/open-gates.md +0 -38
  147. package/dist/templates/synthesis/product-state.md +0 -31
  148. package/dist/templates/synthesis/roadmap.md +0 -63
  149. package/dist/whoami-EANGN46Z.js.map +0 -1
@@ -1,590 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SubagentStop hook: append one JSONL row per subagent completion to the active sprint's token ledger.
3
- #
4
- # Input: JSON on stdin from Claude Code with fields session_id, transcript_path, cwd, hook_event_name.
5
- # Output: appends to .cleargate/sprint-runs/<sprint-id>/token-ledger.jsonl
6
- # Cost computation is deferred to the Reporter agent (prices change; keep raw).
7
- #
8
- # ── Per-task sentinel contract (ADDED 2026-04-20) ─────────────────────────────
9
- #
10
- # Before each Task (Agent) dispatch the orchestrator writes:
11
- # .cleargate/sprint-runs/<sprint>/.pending-task-<N>.json
12
- # with shape:
13
- # { "agent_type": "developer"|"architect"|"qa"|"reporter",
14
- # "work_item_id": "STORY-006-01",
15
- # "turn_index": <int — count of assistant turns in orchestrator transcript at dispatch>,
16
- # "started_at": "<ISO-8601>" }
17
- #
18
- # On SubagentStop fire the hook:
19
- # 1. Finds the newest .pending-task-*.json in the active sprint dir.
20
- # 2. Reads agent_type, work_item_id, turn_index from it.
21
- # 3. Computes token delta = sum of assistant turns at index >= turn_index in the transcript
22
- # (NOT the cumulative whole-file sum — that was the SPRINT-05 double-count bug).
23
- # 4. Writes one JSONL row to token-ledger.jsonl with sentinel attribution.
24
- # 5. Deletes the sentinel atomically (mv → append → rm .processed-*).
25
- #
26
- # If no sentinel exists: falls back to legacy transcript-grep for agent_type/work_item_id
27
- # and computes the full-transcript delta (single-fire: no prior rows to double-count).
28
- # Fail-silent on missing sentinel — never block a subagent stop (exit 0 always).
29
- #
30
- # ── Active sprint detection ────────────────────────────────────────────────────
31
- #
32
- # Primary : .cleargate/sprint-runs/.active sentinel file (one line: "SPRINT-NN")
33
- # Orchestrator writes this at sprint kickoff, removes/updates at close.
34
- # Fallback : .cleargate/sprint-runs/_off-sprint/token-ledger.jsonl
35
- # When no .active sentinel exists, writes still get captured but
36
- # tagged _off-sprint instead of misrouting to a stale sprint dir.
37
- # (Removed): the old `ls -td sprint-runs/*/ | head -1` mtime heuristic — it
38
- # misrouted SPRINT-04 firings to SPRINT-03 because ledger appends
39
- # themselves bumped SPRINT-03's mtime. See REPORT.md SPRINT-04.
40
- #
41
- # ── Work-item ID detection (legacy / no-sentinel path) ────────────────────────
42
- #
43
- # Primary : first user message in the transcript — that's the orchestrator's
44
- # dispatch prompt, which by agent convention starts with
45
- # `STORY=NNN-NN`, `PROPOSAL-NNN`, `EPIC-NNN`, `CR-NNN`, or `BUG-NNN`.
46
- # Pattern : (STORY|PROPOSAL|EPIC|CR|BUG)[-=]?[0-9]+(-[0-9]+)?
47
- # Fallback : grep first match anywhere in the transcript.
48
- # story_id : populated only when the match is a STORY-* (backward compat).
49
- # work_item_id: always populated when detection succeeds; equals story_id for STORY items.
50
- # (Removed): grep-first-anywhere as PRIMARY — it picked up SPRINT-05 mentions
51
- # from architect plans being read by the subagent and mistagged
52
- # every SPRINT-04 firing as STORY-006-01.
53
- #
54
- # Robustness: never exits non-zero on parse failure (never block a subagent stop). Errors go to a
55
- # sibling hook.log so you can diagnose without fighting the runtime.
56
-
57
- set -u
58
-
59
- # CR-026: SessionStart blocked-items banner poisons transcript-grep (BUG-024 §3.1 Defect 2).
60
- # Skip-pattern: any line starting with "<N> items? blocked: " is the SessionStart banner.
61
- # Applied in the legacy work-item ID resolution block below via sed filtering.
62
- BANNER_SKIP_RE='^[0-9]+ items? blocked: '
63
-
64
- REPO_ROOT="${ORCHESTRATOR_PROJECT_DIR:-${CLAUDE_PROJECT_DIR}}"
65
- LOG_DIR="${REPO_ROOT}/.cleargate/hook-log"
66
- mkdir -p "${LOG_DIR}"
67
- HOOK_LOG="${LOG_DIR}/token-ledger.log"
68
- ACTIVE_SENTINEL="${REPO_ROOT}/.cleargate/sprint-runs/.active"
69
-
70
- {
71
- INPUT="$(cat)"
72
-
73
- # --- parse hook payload ---
74
- TRANSCRIPT_PATH="$(printf '%s' "${INPUT}" | jq -r '.transcript_path // empty')"
75
- SESSION_ID="$(printf '%s' "${INPUT}" | jq -r '.session_id // empty')"
76
-
77
- if [[ -z "${TRANSCRIPT_PATH}" || ! -f "${TRANSCRIPT_PATH}" ]]; then
78
- printf '[%s] no transcript_path (session=%s)\n' "$(date -u +%FT%TZ)" "${SESSION_ID}" >> "${HOOK_LOG}"
79
- exit 0
80
- fi
81
-
82
- # --- determine active sprint via sentinel ---
83
- SPRINT_ID=""
84
- if [[ -f "${ACTIVE_SENTINEL}" ]]; then
85
- SPRINT_ID="$(tr -d '[:space:]' < "${ACTIVE_SENTINEL}")"
86
- fi
87
-
88
- if [[ -n "${SPRINT_ID}" ]]; then
89
- SPRINT_DIR="${REPO_ROOT}/.cleargate/sprint-runs/${SPRINT_ID}"
90
- mkdir -p "${SPRINT_DIR}"
91
- printf '[%s] routing to sprint=%s (sentinel)\n' "$(date -u +%FT%TZ)" "${SPRINT_ID}" >> "${HOOK_LOG}"
92
- else
93
- # No active sprint: capture the row in an off-sprint ledger so we don't lose data.
94
- SPRINT_ID="_off-sprint"
95
- SPRINT_DIR="${REPO_ROOT}/.cleargate/sprint-runs/_off-sprint"
96
- mkdir -p "${SPRINT_DIR}"
97
- printf '[%s] no .active sentinel — bucketing as _off-sprint\n' "$(date -u +%FT%TZ)" >> "${HOOK_LOG}"
98
- fi
99
- LEDGER="${SPRINT_DIR}/token-ledger.jsonl"
100
-
101
- # --- dispatch-marker attribution (CR-016 + CR-026 + BUG-029, highest priority) ---
102
- # The PreToolUse:Task hook (pre-tool-use-task.sh, CR-026) auto-writes:
103
- # .cleargate/sprint-runs/<sprint>/.dispatch-<ts>-<pid>-<rand>.json
104
- # with { work_item_id, agent_type, spawned_at, session_id, writer }.
105
- # Reading this file (if present) gives accurate attribution; falls back to the
106
- # per-task pending-task sentinel (second priority) and transcript-scan (third).
107
- #
108
- # BUG-029 fix — tuple-match replaces newest-file lookup:
109
- # When two parallel Task() spawns write two distinct .dispatch-*.json files,
110
- # the old `ls -t | head -1` (newest-file) lookup grabs whichever was written
111
- # last — mis-attributing the ledger row to the wrong story. The fix: extract
112
- # the work_item_id from the SubagentStop transcript (first user message) and
113
- # match it against the work_item_id field inside each .dispatch-*.json.
114
- # Fallback: if no tuple match, fall back to newest-file lookup with a warning.
115
- #
116
- # Atomicity: rename to .processed-$$ before reading, then delete post-row-write.
117
- # This prevents stale dispatch files from leaking attribution to a later subagent.
118
- SENTINEL_AGENT_TYPE=""
119
- SENTINEL_WORK_ITEM_ID=""
120
- DISPATCH_PROCESSED=""
121
- DISPATCH_RUN_ID=""
122
-
123
- # BUG-029: extract work_item_id from the SubagentStop transcript (first user message).
124
- # This is the orchestrator's dispatch prompt — by convention it starts with the
125
- # work_item_id (e.g. "STORY=NNN-NN" or "STORY-NNN-NN") or contains it prominently.
126
- # We use non-capturing groups (no capture groups → scan returns full match string)
127
- # and a broad alphanumeric suffix to also match letter-suffix IDs like STORY-A, STORY-B
128
- # used in tests and fast-lane items (not just digit-keyed like the legacy path).
129
- TRANSCRIPT_WORK_ITEM=""
130
- if [[ -f "${TRANSCRIPT_PATH}" ]]; then
131
- # Primary: first user message, scan for work-item reference (TYPE[-=]ID).
132
- # scan("(?:...)+") with no capture groups returns the full match string.
133
- TRANSCRIPT_WORK_ITEM="$(jq -rs --arg banner_re "${BANNER_SKIP_RE}" '
134
- [.[] | select(.type == "user")]
135
- | [.[] | select(
136
- (.message.content | if type == "array"
137
- then map(.text? // "") | join(" ")
138
- else (. // "") end
139
- ) | test($banner_re) | not
140
- )]
141
- | .[0].message.content
142
- | if type == "array" then map(.text? // "") | join(" ") else (. // "") end
143
- | tostring
144
- | [scan("(?:STORY|PROPOSAL|EPIC|CR|BUG|HOTFIX)[-=][A-Za-z0-9]+(?:-[A-Za-z0-9]+)?")]
145
- | .[0] // ""
146
- ' "${TRANSCRIPT_PATH}" 2>/dev/null | head -1 | sed 's/=/-/g')"
147
- # Normalize: replace = with - (STORY=NNN-NN → STORY-NNN-NN)
148
- TRANSCRIPT_WORK_ITEM="$(printf '%s' "${TRANSCRIPT_WORK_ITEM}" | sed 's/=/-/g')"
149
- [[ "${TRANSCRIPT_WORK_ITEM}" == "" || "${TRANSCRIPT_WORK_ITEM}" == "null" ]] && TRANSCRIPT_WORK_ITEM=""
150
- fi
151
-
152
- # BUG-029: tuple-match — iterate dispatch files, find one whose work_item_id
153
- # matches TRANSCRIPT_WORK_ITEM. If exactly one matches, consume it; otherwise
154
- # fall back to newest-file (legacy CR-026 path) with a warning logged.
155
- DISPATCH_FILE=""
156
- if [[ -n "${TRANSCRIPT_WORK_ITEM}" ]]; then
157
- # Search all dispatch files for a content match on work_item_id.
158
- MATCHED_FILE=""
159
- MATCH_COUNT=0
160
- for CANDIDATE in "${SPRINT_DIR}"/.dispatch-*.json; do
161
- [[ -f "${CANDIDATE}" ]] || continue
162
- CANDIDATE_WORK_ITEM="$(jq -r '.work_item_id // empty' "${CANDIDATE}" 2>/dev/null)"
163
- if [[ "${CANDIDATE_WORK_ITEM}" == "${TRANSCRIPT_WORK_ITEM}" ]]; then
164
- MATCHED_FILE="${CANDIDATE}"
165
- MATCH_COUNT=$(( MATCH_COUNT + 1 ))
166
- fi
167
- done
168
- if [[ "${MATCH_COUNT}" -eq 1 ]]; then
169
- DISPATCH_FILE="${MATCHED_FILE}"
170
- printf '[%s] dispatch-marker tuple-match: transcript_work_item=%s → %s\n' \
171
- "$(date -u +%FT%TZ)" "${TRANSCRIPT_WORK_ITEM}" "${DISPATCH_FILE}" >> "${HOOK_LOG}"
172
- elif [[ "${MATCH_COUNT}" -gt 1 ]]; then
173
- printf '[%s] warn: %d dispatch files matched work_item=%s — falling back to newest-file\n' \
174
- "$(date -u +%FT%TZ)" "${MATCH_COUNT}" "${TRANSCRIPT_WORK_ITEM}" >> "${HOOK_LOG}"
175
- fi
176
- fi
177
-
178
- # Fallback: newest-file lookup (CR-026 path-B) when tuple-match found nothing.
179
- if [[ -z "${DISPATCH_FILE}" ]]; then
180
- if [[ -n "${TRANSCRIPT_WORK_ITEM}" ]]; then
181
- printf '[%s] warn: no tuple-match for work_item=%s — falling back to newest-file lookup\n' \
182
- "$(date -u +%FT%TZ)" "${TRANSCRIPT_WORK_ITEM}" >> "${HOOK_LOG}"
183
- fi
184
- DISPATCH_FILE="$(ls -t "${SPRINT_DIR}"/.dispatch-*.json 2>/dev/null | head -1)"
185
- fi
186
-
187
- if [[ -n "${DISPATCH_FILE}" && -f "${DISPATCH_FILE}" ]]; then
188
- DISPATCH_PROCESSED="${DISPATCH_FILE%.json}.processed-$$"
189
- if mv "${DISPATCH_FILE}" "${DISPATCH_PROCESSED}" 2>/dev/null; then
190
- DISPATCH_JSON="$(cat "${DISPATCH_PROCESSED}" 2>/dev/null)"
191
- DISPATCH_AGENT="$(printf '%s' "${DISPATCH_JSON}" | jq -r '.agent_type // empty' 2>/dev/null)"
192
- DISPATCH_WORK_ITEM="$(printf '%s' "${DISPATCH_JSON}" | jq -r '.work_item_id // empty' 2>/dev/null)"
193
- DISPATCH_RUN_ID="$(printf '%s' "${DISPATCH_JSON}" | jq -r '.run_id // empty' 2>/dev/null)"
194
- if [[ -n "${DISPATCH_AGENT}" && -n "${DISPATCH_WORK_ITEM}" ]]; then
195
- SENTINEL_AGENT_TYPE="${DISPATCH_AGENT}"
196
- SENTINEL_WORK_ITEM_ID="${DISPATCH_WORK_ITEM}"
197
- printf '[%s] dispatch-marker: session=%s work_item=%s agent=%s\n' \
198
- "$(date -u +%FT%TZ)" "${SESSION_ID}" "${SENTINEL_WORK_ITEM_ID}" "${SENTINEL_AGENT_TYPE}" >> "${HOOK_LOG}"
199
- else
200
- printf '[%s] warn: dispatch file malformed or missing fields, falling back: %s\n' \
201
- "$(date -u +%FT%TZ)" "${DISPATCH_PROCESSED}" >> "${HOOK_LOG}"
202
- fi
203
- else
204
- printf '[%s] warn: could not rename dispatch file %s (race?), skipping\n' \
205
- "$(date -u +%FT%TZ)" "${DISPATCH_FILE}" >> "${HOOK_LOG}"
206
- fi
207
- fi
208
- # DISPATCH_PROCESSED is deleted after row-write (see "delete processed dispatch" block below).
209
-
210
- # --- per-task sentinel: find newest .pending-task-*.json in sprint dir ---
211
- # Second-priority fallback when dispatch-marker is absent or malformed.
212
- # When dispatch-marker already populated SENTINEL_AGENT_TYPE + SENTINEL_WORK_ITEM_ID,
213
- # still read the pending-task sentinel to get turn_index + started_at for delta accounting.
214
- SENTINEL_FILE=""
215
- SENTINEL_TURN_INDEX=0
216
- SENTINEL_STARTED_AT=""
217
- # Preserve SENTINEL_AGENT_TYPE / SENTINEL_WORK_ITEM_ID from dispatch block above.
218
-
219
- if [[ -d "${SPRINT_DIR}" ]]; then
220
- # Find newest pending-task sentinel (ls -t sorts newest first)
221
- SENTINEL_FILE="$(ls -t "${SPRINT_DIR}"/.pending-task-*.json 2>/dev/null | head -1)"
222
- fi
223
-
224
- if [[ -n "${SENTINEL_FILE}" && -f "${SENTINEL_FILE}" ]]; then
225
- SENTINEL_JSON="$(cat "${SENTINEL_FILE}" 2>/dev/null)"
226
- # Only use attribution from pending-task if dispatch-marker did not already provide it.
227
- if [[ -z "${SENTINEL_AGENT_TYPE}" ]]; then
228
- SENTINEL_AGENT_TYPE="$(printf '%s' "${SENTINEL_JSON}" | jq -r '.agent_type // empty' 2>/dev/null)"
229
- fi
230
- if [[ -z "${SENTINEL_WORK_ITEM_ID}" ]]; then
231
- SENTINEL_WORK_ITEM_ID="$(printf '%s' "${SENTINEL_JSON}" | jq -r '.work_item_id // empty' 2>/dev/null)"
232
- fi
233
- SENTINEL_TURN_INDEX="$(printf '%s' "${SENTINEL_JSON}" | jq -r '.turn_index // 0' 2>/dev/null)"
234
- SENTINEL_STARTED_AT="$(printf '%s' "${SENTINEL_JSON}" | jq -r '.started_at // empty' 2>/dev/null)"
235
- printf '[%s] found sentinel=%s agent=%s work_item=%s turn_index=%s\n' \
236
- "$(date -u +%FT%TZ)" "${SENTINEL_FILE}" "${SENTINEL_AGENT_TYPE}" "${SENTINEL_WORK_ITEM_ID}" "${SENTINEL_TURN_INDEX}" >> "${HOOK_LOG}"
237
- fi
238
-
239
- # --- compute token usage ---
240
- # DELTA MODEL (per-task sentinel path): slice transcript from turn_index forward.
241
- # CUMULATIVE GUARD (no-sentinel path): use entire transcript — single-fire so no double-count.
242
- #
243
- # turn_index counts assistant turns (0-based). We select assistant turns, skip the first
244
- # turn_index of them, then sum the remainder. This isolates tokens attributable to this subagent run.
245
- if [[ -n "${SENTINEL_FILE}" && -f "${SENTINEL_FILE}" && -n "${SENTINEL_AGENT_TYPE}" ]]; then
246
- # Delta model: slice from turn_index
247
- USAGE_JSON="$(jq -cs --argjson idx "${SENTINEL_TURN_INDEX}" '
248
- [ .[] | select(.type == "assistant" and .message.usage) ]
249
- | .[$idx:]
250
- | (map(.message.usage.input_tokens // 0) | add // 0) as $in
251
- | (map(.message.usage.output_tokens // 0) | add // 0) as $out
252
- | (map(.message.usage.cache_creation_input_tokens // 0) | add // 0) as $cc
253
- | (map(.message.usage.cache_read_input_tokens // 0) | add // 0) as $cr
254
- | (map(.message.model) | unique | map(select(. != null)) | join(",")) as $models
255
- | (length) as $turns
256
- | {input: $in, output: $out, cache_creation: $cc, cache_read: $cr, model: $models, turns: $turns}
257
- ' "${TRANSCRIPT_PATH}" 2>/dev/null)"
258
- else
259
- # No sentinel: sum all assistant turns (full transcript — first fire, no prior rows to double-count)
260
- USAGE_JSON="$(jq -cs '
261
- map(select(.type == "assistant" and .message.usage))
262
- | (map(.message.usage.input_tokens // 0) | add // 0) as $in
263
- | (map(.message.usage.output_tokens // 0) | add // 0) as $out
264
- | (map(.message.usage.cache_creation_input_tokens // 0) | add // 0) as $cc
265
- | (map(.message.usage.cache_read_input_tokens // 0) | add // 0) as $cr
266
- | (map(.message.model) | unique | map(select(. != null)) | join(",")) as $models
267
- | (length) as $turns
268
- | {input: $in, output: $out, cache_creation: $cc, cache_read: $cr, model: $models, turns: $turns}
269
- ' "${TRANSCRIPT_PATH}" 2>/dev/null)"
270
- fi
271
-
272
- if [[ -z "${USAGE_JSON}" || "${USAGE_JSON}" == "null" ]]; then
273
- printf '[%s] could not parse usage from %s\n' "$(date -u +%FT%TZ)" "${TRANSCRIPT_PATH}" >> "${HOOK_LOG}"
274
- exit 0
275
- fi
276
-
277
- # --- resolve agent_type and work_item_id ---
278
- # Sentinel takes precedence; fall back to legacy transcript-grep when no sentinel.
279
- AGENT_TYPE="${SENTINEL_AGENT_TYPE}"
280
- WORK_ITEM_ID="${SENTINEL_WORK_ITEM_ID}"
281
-
282
- if [[ -z "${AGENT_TYPE}" ]]; then
283
- # Legacy: grep subagent_type from user messages, then role markers
284
- AGENT_TYPE="$(jq -rs '
285
- [.[] | select(.type == "user") | .message.content]
286
- | tostring
287
- | capture("subagent_type[\"\\s:=]+(?<t>[a-zA-Z0-9_-]+)"; "g")?.t
288
- // "unknown"
289
- ' "${TRANSCRIPT_PATH}" 2>/dev/null)"
290
- [[ -z "${AGENT_TYPE}" || "${AGENT_TYPE}" == "null" ]] && AGENT_TYPE="unknown"
291
-
292
- if [[ "${AGENT_TYPE}" == "unknown" ]]; then
293
- for role in architect developer qa reporter devops cleargate-wiki-contradict; do
294
- if grep -qiE "\\b${role}\\b agent|role: ${role}|you are the ${role}" "${TRANSCRIPT_PATH}" 2>/dev/null; then
295
- AGENT_TYPE="${role}"
296
- break
297
- fi
298
- done
299
- fi
300
- fi
301
-
302
- if [[ -z "${WORK_ITEM_ID}" ]]; then
303
- # BUG-027: Before falling to transcript grep, attempt sentinel-aware lookups.
304
- #
305
- # Resolution order (cheapest/most-accurate first):
306
- # Step 1 — Prior ledger row (Option A, M1 open decision):
307
- # Read the most-recent row from ${LEDGER} (the file this hook appends to).
308
- # Orchestrator-architect coordination calls happen AFTER a subagent dispatch that
309
- # correctly tagged the active epic; reusing the last row's work_item_id is both
310
- # cheap and accurate. This step is the primary fix for the 12 EPIC-001
311
- # misattributions observed during the SPRINT-02 dogfood (BUG-027 context_source).
312
- # Step 2 — Most-recent dispatch-marker log line:
313
- # The hook emits "dispatch-marker: session=... work_item=... agent=..." to HOOK_LOG
314
- # on every successful dispatch-file consumption. Reading the last such line gives
315
- # accurate attribution for the same class of coordination calls.
316
- # Step 3 (legacy) — First user message transcript scan (CR-026 banner-skip).
317
- # Step 4 (last resort) — Anywhere-grep in transcript (CR-026 banner-skip).
318
- #
319
- # Steps 3+4 are kept as final fallbacks; the transcript grep is now the last resort,
320
- # not the primary path, which eliminates the EPIC-001 lexical-first misattribution.
321
-
322
- # Step 1: Read most-recent prior ledger row's work_item_id.
323
- PRIOR_LEDGER_WORK_ITEM=""
324
- if [[ -f "${LEDGER}" ]]; then
325
- PRIOR_LEDGER_WORK_ITEM="$(tail -1 "${LEDGER}" 2>/dev/null \
326
- | jq -r '.work_item_id // empty' 2>/dev/null)"
327
- # Only accept non-empty, non-"none", non-"unknown" values.
328
- if [[ -n "${PRIOR_LEDGER_WORK_ITEM}" && \
329
- "${PRIOR_LEDGER_WORK_ITEM}" != "none" && \
330
- "${PRIOR_LEDGER_WORK_ITEM}" != "unknown" && \
331
- "${PRIOR_LEDGER_WORK_ITEM}" != "null" ]]; then
332
- WORK_ITEM_ID="${PRIOR_LEDGER_WORK_ITEM}"
333
- printf '[%s] work_item_id from prior ledger row: %s\n' "$(date -u +%FT%TZ)" "${WORK_ITEM_ID}" >> "${HOOK_LOG}"
334
- fi
335
- fi
336
-
337
- # Step 2: Read most-recent dispatch-marker log line (if Step 1 did not resolve).
338
- if [[ -z "${WORK_ITEM_ID}" && -f "${HOOK_LOG}" ]]; then
339
- DISPATCH_MARKER_WORK_ITEM="$(grep -E '^\[.+\] dispatch-marker: ' "${HOOK_LOG}" 2>/dev/null \
340
- | tail -1 \
341
- | grep -oE 'work_item=[^ ]+' \
342
- | head -1 \
343
- | sed 's/work_item=//')"
344
- if [[ -n "${DISPATCH_MARKER_WORK_ITEM}" && \
345
- "${DISPATCH_MARKER_WORK_ITEM}" != "none" && \
346
- "${DISPATCH_MARKER_WORK_ITEM}" != "unknown" ]]; then
347
- WORK_ITEM_ID="${DISPATCH_MARKER_WORK_ITEM}"
348
- printf '[%s] work_item_id from dispatch-marker log: %s\n' "$(date -u +%FT%TZ)" "${WORK_ITEM_ID}" >> "${HOOK_LOG}"
349
- fi
350
- fi
351
-
352
- # Step 3: Legacy transcript scan — first user message (CR-026 banner-skip applied).
353
- # Only runs when Steps 1+2 did not resolve.
354
- if [[ -z "${WORK_ITEM_ID}" ]]; then
355
- # CR-026: banner-skip applied before jq scan (BUG-024 §3.1 Defect 2).
356
- # The SessionStart hook emits a banner line of the form:
357
- # "N items blocked: BUG-004: ..."
358
- # This line poisons transcript-grep by matching the work-item regex first.
359
- # We skip it via select(. | test(BANNER_SKIP_RE) | not) in the jq pipeline.
360
- # BANNER_SKIP_RE is defined near the top of this script.
361
- WORK_ITEM_RAW="$(jq -rs --arg banner_re "${BANNER_SKIP_RE}" '
362
- [.[] | select(.type == "user")]
363
- | [.[] | select(
364
- (.message.content | if type == "array"
365
- then map(.text? // "") | join(" ")
366
- else (. // "") end
367
- ) | test($banner_re) | not
368
- )]
369
- | .[0].message.content
370
- | if type == "array" then map(.text? // "") | join(" ") else (. // "") end
371
- | tostring
372
- | scan("(STORY|PROPOSAL|EPIC|CR|BUG|HOTFIX)[-=]?([0-9]+(-[0-9]+)?)") | .[0:2] | join("-")
373
- ' "${TRANSCRIPT_PATH}" 2>/dev/null | head -1)"
374
-
375
- if [[ -n "${WORK_ITEM_RAW}" && "${WORK_ITEM_RAW}" != "null" && "${WORK_ITEM_RAW}" != "-" ]]; then
376
- WORK_ITEM_ID="$(printf '%s' "${WORK_ITEM_RAW}" | sed 's/=/-/g')"
377
- else
378
- # Step 4 (last resort): CR-026: fallback grep also applies banner-skip via sed filter.
379
- # This is the path that was misattributing EPIC-001 (BUG-027). Now reached only when
380
- # Steps 1+2+3 all fail to resolve a work_item_id.
381
- WORK_ITEM_ID="$(sed -E "/${BANNER_SKIP_RE}/d" "${TRANSCRIPT_PATH}" 2>/dev/null \
382
- | grep -oE '(STORY|PROPOSAL|EPIC|CR|BUG|HOTFIX)[-=]?[0-9]+(-[0-9]+)?' \
383
- | head -1 \
384
- | sed 's/=/-/g')"
385
- if [[ -n "${WORK_ITEM_ID}" ]]; then
386
- printf '[%s] work_item_id fallback grep: %s\n' "$(date -u +%FT%TZ)" "${WORK_ITEM_ID}" >> "${HOOK_LOG}"
387
- fi
388
- fi
389
- [[ -z "${WORK_ITEM_ID}" ]] && WORK_ITEM_ID=""
390
-
391
- # Legacy fallback: if no work_item_id found at all, fall back to old grep for story_id only
392
- if [[ -z "${WORK_ITEM_ID}" ]]; then
393
- STORY_ID_LEGACY="$(grep -oE 'STORY[-=]?[0-9]{3}-[0-9]{2}' "${TRANSCRIPT_PATH}" 2>/dev/null \
394
- | head -1 \
395
- | sed -E 's/STORY[-=]?([0-9]{3}-[0-9]{2})/STORY-\1/')"
396
- [[ -z "${STORY_ID_LEGACY}" ]] && STORY_ID_LEGACY="none"
397
- WORK_ITEM_ID="${STORY_ID_LEGACY}"
398
- fi
399
- fi
400
- fi
401
-
402
- # story_id is populated only when the work item is a STORY-* (backward compat)
403
- STORY_ID=""
404
- if [[ "${WORK_ITEM_ID}" == STORY-* ]]; then
405
- STORY_ID="${WORK_ITEM_ID}"
406
- fi
407
-
408
- # --- atomic sentinel deletion (mv → append → rm) ---
409
- PROCESSED_FILE=""
410
- if [[ -n "${SENTINEL_FILE}" && -f "${SENTINEL_FILE}" ]]; then
411
- PROCESSED_FILE="${SENTINEL_FILE%.json}.processed-$$"
412
- if ! mv "${SENTINEL_FILE}" "${PROCESSED_FILE}" 2>/dev/null; then
413
- # mv failed (race condition or permissions): proceed without deletion
414
- PROCESSED_FILE=""
415
- printf '[%s] warn: could not mv sentinel %s\n' "$(date -u +%FT%TZ)" "${SENTINEL_FILE}" >> "${HOOK_LOG}"
416
- fi
417
- fi
418
-
419
- # --- per-turn delta math (CR-018) ---
420
- # USAGE_JSON (computed above) is the intra-fire cumulative session total from the transcript.
421
- # We maintain .session-totals.json keyed by session_id with the last-known cumulative totals.
422
- # Cross-fire delta = current_session_total − prior_session_total.
423
- # For the first fire of a session, delta == session_total.
424
- #
425
- # Atomic update via mktemp + mv (POSIX rename is atomic on the same filesystem).
426
- # Concurrent-fire safety: two worktree SubagentStop fires may race. The atomic rename
427
- # ensures one writer wins; the other sees the updated file on its next read.
428
- SESSION_TOTALS_FILE="${SPRINT_DIR}/.session-totals.json"
429
-
430
- # Read current session totals (or empty object if file absent)
431
- PRIOR_SESSION_JSON="{}"
432
- if [[ -f "${SESSION_TOTALS_FILE}" ]]; then
433
- PRIOR_SESSION_JSON="$(cat "${SESSION_TOTALS_FILE}" 2>/dev/null || printf '{}')"
434
- [[ -z "${PRIOR_SESSION_JSON}" ]] && PRIOR_SESSION_JSON="{}"
435
- fi
436
-
437
- # Compute current intra-fire totals from USAGE_JSON
438
- CURRENT_IN="$(printf '%s' "${USAGE_JSON}" | jq -r '.input // 0')"
439
- CURRENT_OUT="$(printf '%s' "${USAGE_JSON}" | jq -r '.output // 0')"
440
- CURRENT_CC="$(printf '%s' "${USAGE_JSON}" | jq -r '.cache_creation // 0')"
441
- CURRENT_CR="$(printf '%s' "${USAGE_JSON}" | jq -r '.cache_read // 0')"
442
- CURRENT_MODEL="$(printf '%s' "${USAGE_JSON}" | jq -r '.model // ""')"
443
- CURRENT_TURNS="$(printf '%s' "${USAGE_JSON}" | jq -r '.turns // 0')"
444
-
445
- # Look up prior totals for this session_id
446
- PRIOR_IN="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -r --arg sid "${SESSION_ID}" '.[$sid].input // 0' 2>/dev/null || printf '0')"
447
- PRIOR_OUT="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -r --arg sid "${SESSION_ID}" '.[$sid].output // 0' 2>/dev/null || printf '0')"
448
- PRIOR_CC="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -r --arg sid "${SESSION_ID}" '.[$sid].cache_creation // 0' 2>/dev/null || printf '0')"
449
- PRIOR_CR="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -r --arg sid "${SESSION_ID}" '.[$sid].cache_read // 0' 2>/dev/null || printf '0')"
450
-
451
- # Compute delta = current − prior (floor at 0 to guard against transcript-reread edge cases)
452
- DELTA_IN=$(( CURRENT_IN - PRIOR_IN ))
453
- DELTA_OUT=$(( CURRENT_OUT - PRIOR_OUT ))
454
- DELTA_CC=$(( CURRENT_CC - PRIOR_CC ))
455
- DELTA_CR=$(( CURRENT_CR - PRIOR_CR ))
456
- [[ "${DELTA_IN}" -lt 0 ]] && DELTA_IN=0
457
- [[ "${DELTA_OUT}" -lt 0 ]] && DELTA_OUT=0
458
- [[ "${DELTA_CC}" -lt 0 ]] && DELTA_CC=0
459
- [[ "${DELTA_CR}" -lt 0 ]] && DELTA_CR=0
460
-
461
- # Build delta and session_total JSON blocks
462
- DELTA_JSON="$(jq -cn \
463
- --argjson in "${DELTA_IN}" \
464
- --argjson out "${DELTA_OUT}" \
465
- --argjson cc "${DELTA_CC}" \
466
- --argjson cr "${DELTA_CR}" \
467
- '{input: $in, output: $out, cache_creation: $cc, cache_read: $cr}')"
468
-
469
- SESSION_TOTAL_JSON="$(jq -cn \
470
- --argjson in "${CURRENT_IN}" \
471
- --argjson out "${CURRENT_OUT}" \
472
- --argjson cc "${CURRENT_CC}" \
473
- --argjson cr "${CURRENT_CR}" \
474
- '{input: $in, output: $out, cache_creation: $cc, cache_read: $cr}')"
475
-
476
- # Atomically update .session-totals.json with new totals for this session_id
477
- # STORY-033-02: also key by RUN_ID when dispatch marker carries one (parallel-wave attribution).
478
- TS="$(date -u +%FT%TZ)"
479
- NEW_SESSION_TOTALS="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -c \
480
- --arg sid "${SESSION_ID}" \
481
- --arg run_id "${DISPATCH_RUN_ID:-}" \
482
- --argjson in "${CURRENT_IN}" \
483
- --argjson out "${CURRENT_OUT}" \
484
- --argjson cc "${CURRENT_CC}" \
485
- --argjson cr "${CURRENT_CR}" \
486
- --arg ts "${TS}" \
487
- --argjson ti "${SENTINEL_TURN_INDEX}" \
488
- '
489
- .[$sid] = {input: $in, output: $out, cache_creation: $cc, cache_read: $cr, last_ts: $ts, last_turn_index: $ti}
490
- | if $run_id == "" then . else .[$run_id] = {input: $in, output: $out, cache_creation: $cc, cache_read: $cr, last_ts: $ts, last_turn_index: $ti} end
491
- ' \
492
- 2>/dev/null)"
493
-
494
- if [[ -n "${NEW_SESSION_TOTALS}" ]]; then
495
- SESSION_TOTALS_TMP="$(mktemp "${SESSION_TOTALS_FILE}.tmp.XXXXXX" 2>/dev/null)"
496
- if [[ -n "${SESSION_TOTALS_TMP}" ]]; then
497
- printf '%s\n' "${NEW_SESSION_TOTALS}" > "${SESSION_TOTALS_TMP}"
498
- mv "${SESSION_TOTALS_TMP}" "${SESSION_TOTALS_FILE}" 2>/dev/null || \
499
- printf '[%s] warn: could not atomic-rename session-totals\n' "${TS}" >> "${HOOK_LOG}"
500
- fi
501
- fi
502
-
503
- # --- STORY-033-02: RUN_ID no-op guard (barrier-written row already exists) ---
504
- # When the dispatch marker carries a run_id, check whether a ledger row for this
505
- # work_item_id + run_id already exists (written by the barrier). If so, exit 0 without
506
- # appending a second row — this neutralises late SubagentStop mis-attribution.
507
- if [[ -n "${DISPATCH_RUN_ID}" && -f "${LEDGER}" ]]; then
508
- if grep -q "\"run_id\":\"${DISPATCH_RUN_ID}\"" "${LEDGER}" 2>/dev/null; then
509
- printf '[%s] barrier row already present — skip (work_item=%s run_id=%s)\n' \
510
- "${TS}" "${WORK_ITEM_ID}" "${DISPATCH_RUN_ID}" >> "${HOOK_LOG}"
511
- # Clean up sentinels/dispatch before exit
512
- if [[ -n "${PROCESSED_FILE}" && -f "${PROCESSED_FILE}" ]]; then rm -f "${PROCESSED_FILE}"; fi
513
- if [[ -n "${DISPATCH_PROCESSED:-}" && -f "${DISPATCH_PROCESSED}" ]]; then rm -f "${DISPATCH_PROCESSED}"; fi
514
- exit 0
515
- fi
516
- fi
517
-
518
- # --- STORY-033-02: ESCALATED guard — when RUN_ID is set but transcript has no tokens ---
519
- # A segment whose verdict carries no tokens is recorded as ESCALATED; no row is written.
520
- if [[ -n "${DISPATCH_RUN_ID}" ]]; then
521
- if [[ "${CURRENT_IN}" -eq 0 && "${CURRENT_OUT}" -eq 0 ]]; then
522
- printf '[%s] ESCALATED: run_id=%s work_item=%s — no tokens in transcript, skipping row\n' \
523
- "${TS}" "${DISPATCH_RUN_ID}" "${WORK_ITEM_ID}" >> "${HOOK_LOG}"
524
- # Clean up sentinels/dispatch before exit
525
- if [[ -n "${PROCESSED_FILE}" && -f "${PROCESSED_FILE}" ]]; then rm -f "${PROCESSED_FILE}"; fi
526
- if [[ -n "${DISPATCH_PROCESSED:-}" && -f "${DISPATCH_PROCESSED}" ]]; then rm -f "${DISPATCH_PROCESSED}"; fi
527
- exit 0
528
- fi
529
- fi
530
-
531
- # --- assemble ledger row (v2 schema: delta + session_total replace flat input/output/cache_*) ---
532
- # STORY-033-02: include run_id field when dispatch marker carries one.
533
- ROW="$(jq -cn \
534
- --arg ts "${TS}" \
535
- --arg agent "${AGENT_TYPE}" \
536
- --arg story "${STORY_ID}" \
537
- --arg work_item "${WORK_ITEM_ID}" \
538
- --arg session "${SESSION_ID}" \
539
- --arg transcript "${TRANSCRIPT_PATH}" \
540
- --arg sprint "${SPRINT_ID}" \
541
- --arg sentinel_started_at "${SENTINEL_STARTED_AT}" \
542
- --argjson turn_index "${SENTINEL_TURN_INDEX}" \
543
- --argjson delta "${DELTA_JSON}" \
544
- --argjson session_total "${SESSION_TOTAL_JSON}" \
545
- --arg model "${CURRENT_MODEL}" \
546
- --argjson turns "${CURRENT_TURNS}" \
547
- --arg run_id "${DISPATCH_RUN_ID:-}" \
548
- '{ts: $ts, sprint_id: $sprint, story_id: $story, work_item_id: $work_item, agent_type: $agent, session_id: $session, transcript: $transcript, sentinel_started_at: $sentinel_started_at, delta_from_turn: $turn_index, delta: $delta, session_total: $session_total, model: $model, turns: $turns}
549
- | if $run_id == "" then . else .run_id = $run_id end')"
550
-
551
- printf '%s\n' "${ROW}" >> "${LEDGER}"
552
- printf '[%s] wrote row: sprint=%s agent=%s work_item=%s story=%s delta=in:%s/out:%s session_total=in:%s/out:%s delta_from=%s\n' \
553
- "${TS}" "${SPRINT_ID}" "${AGENT_TYPE}" "${WORK_ITEM_ID}" "${STORY_ID}" \
554
- "${DELTA_IN}" "${DELTA_OUT}" \
555
- "${CURRENT_IN}" "${CURRENT_OUT}" \
556
- "${SENTINEL_TURN_INDEX}" \
557
- >> "${HOOK_LOG}"
558
-
559
- # --- CR-036: Reporter token-budget warning (chat-injection per CR-032 pattern) ---
560
- if [[ "${AGENT_TYPE}" == "reporter" ]]; then
561
- REPORTER_TOTAL=$(( DELTA_IN + DELTA_OUT + DELTA_CC + DELTA_CR ))
562
- REPORTER_BUDGET_SOFT=200000
563
- REPORTER_BUDGET_HARD=500000
564
- if [[ "${REPORTER_TOTAL}" -gt "${REPORTER_BUDGET_HARD}" ]]; then
565
- printf '\n⚠️ Reporter token budget exceeded: %s > %s (HARD advisory)\n' \
566
- "${REPORTER_TOTAL}" "${REPORTER_BUDGET_HARD}"
567
- # Auto-flashcard via cleargate CLI (best-effort; never block)
568
- if command -v cleargate >/dev/null 2>&1; then
569
- cleargate flashcard record \
570
- "Reporter dispatch exceeded 500k tokens — investigate prompt or bundle" \
571
- >/dev/null 2>&1 || true
572
- fi
573
- elif [[ "${REPORTER_TOTAL}" -gt "${REPORTER_BUDGET_SOFT}" ]]; then
574
- printf '\n⚠️ Reporter token budget exceeded: %s > %s (soft warn)\n' \
575
- "${REPORTER_TOTAL}" "${REPORTER_BUDGET_SOFT}"
576
- fi
577
- fi
578
-
579
- # --- delete processed sentinel ---
580
- if [[ -n "${PROCESSED_FILE}" && -f "${PROCESSED_FILE}" ]]; then
581
- rm -f "${PROCESSED_FILE}"
582
- fi
583
-
584
- # --- delete processed dispatch file ---
585
- if [[ -n "${DISPATCH_PROCESSED:-}" && -f "${DISPATCH_PROCESSED}" ]]; then
586
- rm -f "${DISPATCH_PROCESSED}"
587
- fi
588
- } 2>> "${HOOK_LOG}"
589
-
590
- exit 0
@@ -1,68 +0,0 @@
1
- {
2
- "hooks": {
3
- "SubagentStop": [
4
- {
5
- "hooks": [
6
- {
7
- "type": "command",
8
- "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/token-ledger.sh"
9
- }
10
- ]
11
- }
12
- ],
13
- "PreToolUse": [
14
- {
15
- "matcher": "Task",
16
- "hooks": [
17
- {
18
- "type": "command",
19
- "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pending-task-sentinel.sh"
20
- },
21
- {
22
- "type": "command",
23
- "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-tool-use-task.sh"
24
- }
25
- ]
26
- },
27
- {
28
- "matcher": "Edit|Write",
29
- "hooks": [
30
- {
31
- "type": "command",
32
- "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-edit-gate.sh"
33
- }
34
- ]
35
- },
36
- {
37
- "matcher": "AskUserQuestion",
38
- "hooks": [
39
- {
40
- "type": "command",
41
- "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-tool-use-autonomy.sh"
42
- }
43
- ]
44
- }
45
- ],
46
- "PostToolUse": [
47
- {
48
- "matcher": "Edit|Write",
49
- "hooks": [
50
- {
51
- "type": "command",
52
- "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/stamp-and-gate.sh"
53
- }
54
- ]
55
- }
56
- ],
57
- "SessionStart": [
58
- {
59
- "hooks": [
60
- {
61
- "type": "command",
62
- "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-start.sh"
63
- }
64
- ]
65
- }
66
- ]
67
- }
68
- }