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.
- package/CHANGELOG.md +16 -0
- package/dist/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs +0 -1
- package/dist/admin-api/index.js +1 -2
- package/dist/auth/factory.cjs +0 -1
- package/dist/auth/factory.js +2 -3
- package/dist/auth/require-token.cjs +0 -1
- package/dist/auth/require-token.js +1 -2
- package/dist/auth/token-store.cjs +0 -1
- package/dist/auth/token-store.js +1 -2
- package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
- package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
- package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
- package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
- package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
- package/dist/cli.cjs +1564 -1414
- package/dist/cli.js +1514 -1364
- package/dist/lib/ledger.cjs +0 -1
- package/dist/lib/ledger.js +1 -2
- package/dist/lib/lifecycle-reconcile.cjs +0 -1
- package/dist/lib/lifecycle-reconcile.js +2 -3
- package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
- package/package.json +4 -3
- package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
- package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
- package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
- package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
- package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +27 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
- package/templates/cleargate-planning/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs.map +0 -1
- package/dist/admin-api/index.js.map +0 -1
- package/dist/auth/factory.cjs.map +0 -1
- package/dist/auth/factory.js.map +0 -1
- package/dist/auth/require-token.cjs.map +0 -1
- package/dist/auth/require-token.js.map +0 -1
- package/dist/auth/token-store.cjs.map +0 -1
- package/dist/auth/token-store.js.map +0 -1
- package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
- package/dist/chunk-5DI2Z3C2.js.map +0 -1
- package/dist/chunk-BTSZOEWC.js.map +0 -1
- package/dist/chunk-E3X7IE5E.js.map +0 -1
- package/dist/chunk-PDE37WFQ.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/lib/ledger.cjs.map +0 -1
- package/dist/lib/ledger.js.map +0 -1
- package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
- package/dist/lib/lifecycle-reconcile.js.map +0 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
- package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
- package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
- package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
- package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
- package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
- package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
- package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
- package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
- package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
- package/dist/templates/synthesis/active-sprint.md +0 -30
- package/dist/templates/synthesis/open-gates.md +0 -38
- package/dist/templates/synthesis/product-state.md +0 -31
- package/dist/templates/synthesis/roadmap.md +0 -63
- 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
|
-
}
|