cleargate 0.8.2 → 0.10.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 +190 -0
- package/README.md +11 -0
- package/dist/MANIFEST.json +259 -28
- package/dist/{chunk-OM4FAEA7.js → chunk-Q3BTSXCK.js} +69 -3
- package/dist/chunk-Q3BTSXCK.js.map +1 -0
- package/dist/cli.cjs +2621 -548
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2548 -560
- package/dist/cli.js.map +1 -1
- package/dist/lib/ledger.cjs +120 -0
- package/dist/lib/ledger.cjs.map +1 -0
- package/dist/lib/ledger.d.cts +64 -0
- package/dist/lib/ledger.d.ts +64 -0
- package/dist/lib/ledger.js +96 -0
- package/dist/lib/ledger.js.map +1 -0
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +10 -8
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +29 -2
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +50 -1
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
- package/dist/templates/cleargate-planning/.claude/settings.json +4 -0
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +24 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +32 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +37 -3
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +50 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
- package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +17 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +55 -3
- package/dist/templates/cleargate-planning/CLAUDE.md +28 -10
- package/dist/templates/cleargate-planning/MANIFEST.json +259 -28
- package/dist/{whoami-CX7CXJD5.js → whoami-W4U6DPVG.js} +17 -17
- package/dist/whoami-W4U6DPVG.js.map +1 -0
- package/package.json +13 -2
- package/templates/cleargate-planning/.claude/agents/architect.md +10 -8
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
- package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
- package/templates/cleargate-planning/.claude/agents/developer.md +29 -2
- package/templates/cleargate-planning/.claude/agents/qa.md +50 -1
- package/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
- package/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
- package/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
- package/templates/cleargate-planning/.claude/settings.json +4 -0
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
- package/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
- package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
- package/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
- package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
- package/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
- package/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
- package/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
- package/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
- package/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
- package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +24 -1
- package/templates/cleargate-planning/.cleargate/templates/CR.md +32 -1
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
- package/templates/cleargate-planning/.cleargate/templates/epic.md +37 -3
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +50 -0
- package/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
- package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
- package/templates/cleargate-planning/.cleargate/templates/story.md +55 -3
- package/templates/cleargate-planning/CLAUDE.md +28 -10
- package/templates/cleargate-planning/MANIFEST.json +259 -28
- package/dist/chunk-OM4FAEA7.js.map +0 -1
- package/dist/whoami-CX7CXJD5.js.map +0 -1
- package/templates/cleargate-planning/.cleargate/templates/proposal.md +0 -61
|
@@ -5,31 +5,46 @@
|
|
|
5
5
|
# Output: appends to .cleargate/sprint-runs/<sprint-id>/token-ledger.jsonl
|
|
6
6
|
# Cost computation is deferred to the Reporter agent (prices change; keep raw).
|
|
7
7
|
#
|
|
8
|
-
#
|
|
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
|
+
#
|
|
9
32
|
# Primary : .cleargate/sprint-runs/.active sentinel file (one line: "SPRINT-NN")
|
|
10
33
|
# Orchestrator writes this at sprint kickoff, removes/updates at close.
|
|
11
34
|
# Fallback : .cleargate/sprint-runs/_off-sprint/token-ledger.jsonl
|
|
12
35
|
# When no .active sentinel exists, writes still get captured but
|
|
13
|
-
# tagged
|
|
36
|
+
# tagged _off-sprint instead of misrouting to a stale sprint dir.
|
|
14
37
|
# (Removed): the old `ls -td sprint-runs/*/ | head -1` mtime heuristic — it
|
|
15
38
|
# misrouted SPRINT-04 firings to SPRINT-03 because ledger appends
|
|
16
39
|
# themselves bumped SPRINT-03's mtime. See REPORT.md SPRINT-04.
|
|
17
40
|
#
|
|
18
|
-
# Work-item ID detection (
|
|
41
|
+
# ── Work-item ID detection (legacy / no-sentinel path) ────────────────────────
|
|
42
|
+
#
|
|
19
43
|
# Primary : first user message in the transcript — that's the orchestrator's
|
|
20
44
|
# dispatch prompt, which by agent convention starts with
|
|
21
45
|
# `STORY=NNN-NN`, `PROPOSAL-NNN`, `EPIC-NNN`, `CR-NNN`, or `BUG-NNN`.
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# reminder that lists work-item IDs in prose/bullets — those lines start
|
|
25
|
-
# with "- " or whitespace, never with the marker prefix. Scanning the full
|
|
26
|
-
# content caused BUG-010: all SPRINT-14 rows tagged BUG-002 (first item in
|
|
27
|
-
# the reminder list). Fix: join content with \n, split, filter line-starts.
|
|
28
|
-
# Pattern : ^(STORY|PROPOSAL|PROP|EPIC|CR|BUG)[-=][0-9]...
|
|
29
|
-
# PROP-NNN is normalised to PROPOSAL-NNN after match (BUG-009, 2026-04-26)
|
|
30
|
-
# Tie-break: when multiple dispatch-marker lines appear in the first user message,
|
|
31
|
-
# the FIRST line wins (deterministic; orchestrator puts the marker first).
|
|
32
|
-
# Fallback : grep first line-start match anywhere in the transcript.
|
|
46
|
+
# Pattern : (STORY|PROPOSAL|EPIC|CR|BUG)[-=]?[0-9]+(-[0-9]+)?
|
|
47
|
+
# Fallback : grep first match anywhere in the transcript.
|
|
33
48
|
# story_id : populated only when the match is a STORY-* (backward compat).
|
|
34
49
|
# work_item_id: always populated when detection succeeds; equals story_id for STORY items.
|
|
35
50
|
# (Removed): grep-first-anywhere as PRIMARY — it picked up SPRINT-05 mentions
|
|
@@ -41,6 +56,11 @@
|
|
|
41
56
|
|
|
42
57
|
set -u
|
|
43
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
|
+
|
|
44
64
|
REPO_ROOT="${ORCHESTRATOR_PROJECT_DIR:-${CLAUDE_PROJECT_DIR}}"
|
|
45
65
|
LOG_DIR="${REPO_ROOT}/.cleargate/hook-log"
|
|
46
66
|
mkdir -p "${LOG_DIR}"
|
|
@@ -78,85 +98,187 @@ ACTIVE_SENTINEL="${REPO_ROOT}/.cleargate/sprint-runs/.active"
|
|
|
78
98
|
fi
|
|
79
99
|
LEDGER="${SPRINT_DIR}/token-ledger.jsonl"
|
|
80
100
|
|
|
81
|
-
# ---
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
# --- dispatch-marker attribution (CR-016 + CR-026, 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
|
+
# CR-026 Defect 1 fix — path-B (newest-file lookup):
|
|
109
|
+
# The old lookup keyed on the subagent's session_id from the SubagentStop payload,
|
|
110
|
+
# which is the *subagent's* session ID, never the orchestrator's. This caused 100%
|
|
111
|
+
# lookup failure since SPRINT-15 (BUG-024 §3.1 Defect 1). We now use newest-file
|
|
112
|
+
# lookup (ls -t) — atomic with the existing mv→.processed-$$ rename pattern.
|
|
113
|
+
# See CR-026 M3 plan §"Open Question Q1 resolution — path-A vs path-B".
|
|
114
|
+
#
|
|
115
|
+
# Atomicity: rename to .processed-$$ before reading, then delete post-row-write.
|
|
116
|
+
# This prevents stale dispatch files from leaking attribution to a later subagent.
|
|
117
|
+
SENTINEL_AGENT_TYPE=""
|
|
118
|
+
SENTINEL_WORK_ITEM_ID=""
|
|
119
|
+
|
|
120
|
+
# CR-026: newest-file lookup (path-B) — replaces session-id-keyed lookup.
|
|
121
|
+
DISPATCH_FILE="$(ls -t "${SPRINT_DIR}"/.dispatch-*.json 2>/dev/null | head -1)"
|
|
122
|
+
if [[ -n "${DISPATCH_FILE}" && -f "${DISPATCH_FILE}" ]]; then
|
|
123
|
+
DISPATCH_PROCESSED="${DISPATCH_FILE%.json}.processed-$$"
|
|
124
|
+
if mv "${DISPATCH_FILE}" "${DISPATCH_PROCESSED}" 2>/dev/null; then
|
|
125
|
+
DISPATCH_JSON="$(cat "${DISPATCH_PROCESSED}" 2>/dev/null)"
|
|
126
|
+
DISPATCH_AGENT="$(printf '%s' "${DISPATCH_JSON}" | jq -r '.agent_type // empty' 2>/dev/null)"
|
|
127
|
+
DISPATCH_WORK_ITEM="$(printf '%s' "${DISPATCH_JSON}" | jq -r '.work_item_id // empty' 2>/dev/null)"
|
|
128
|
+
if [[ -n "${DISPATCH_AGENT}" && -n "${DISPATCH_WORK_ITEM}" ]]; then
|
|
129
|
+
SENTINEL_AGENT_TYPE="${DISPATCH_AGENT}"
|
|
130
|
+
SENTINEL_WORK_ITEM_ID="${DISPATCH_WORK_ITEM}"
|
|
131
|
+
printf '[%s] dispatch-marker: session=%s work_item=%s agent=%s\n' \
|
|
132
|
+
"$(date -u +%FT%TZ)" "${SESSION_ID}" "${SENTINEL_WORK_ITEM_ID}" "${SENTINEL_AGENT_TYPE}" >> "${HOOK_LOG}"
|
|
133
|
+
else
|
|
134
|
+
printf '[%s] warn: dispatch file malformed or missing fields, falling back: %s\n' \
|
|
135
|
+
"$(date -u +%FT%TZ)" "${DISPATCH_PROCESSED}" >> "${HOOK_LOG}"
|
|
136
|
+
fi
|
|
137
|
+
else
|
|
138
|
+
printf '[%s] warn: could not rename dispatch file %s (race?), skipping\n' \
|
|
139
|
+
"$(date -u +%FT%TZ)" "${DISPATCH_FILE}" >> "${HOOK_LOG}"
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
# DISPATCH_PROCESSED is deleted after row-write (see "delete processed dispatch" block below).
|
|
143
|
+
|
|
144
|
+
# --- per-task sentinel: find newest .pending-task-*.json in sprint dir ---
|
|
145
|
+
# Second-priority fallback when dispatch-marker is absent or malformed.
|
|
146
|
+
# When dispatch-marker already populated SENTINEL_AGENT_TYPE + SENTINEL_WORK_ITEM_ID,
|
|
147
|
+
# still read the pending-task sentinel to get turn_index + started_at for delta accounting.
|
|
148
|
+
SENTINEL_FILE=""
|
|
149
|
+
SENTINEL_TURN_INDEX=0
|
|
150
|
+
SENTINEL_STARTED_AT=""
|
|
151
|
+
# Preserve SENTINEL_AGENT_TYPE / SENTINEL_WORK_ITEM_ID from dispatch block above.
|
|
152
|
+
|
|
153
|
+
if [[ -d "${SPRINT_DIR}" ]]; then
|
|
154
|
+
# Find newest pending-task sentinel (ls -t sorts newest first)
|
|
155
|
+
SENTINEL_FILE="$(ls -t "${SPRINT_DIR}"/.pending-task-*.json 2>/dev/null | head -1)"
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [[ -n "${SENTINEL_FILE}" && -f "${SENTINEL_FILE}" ]]; then
|
|
159
|
+
SENTINEL_JSON="$(cat "${SENTINEL_FILE}" 2>/dev/null)"
|
|
160
|
+
# Only use attribution from pending-task if dispatch-marker did not already provide it.
|
|
161
|
+
if [[ -z "${SENTINEL_AGENT_TYPE}" ]]; then
|
|
162
|
+
SENTINEL_AGENT_TYPE="$(printf '%s' "${SENTINEL_JSON}" | jq -r '.agent_type // empty' 2>/dev/null)"
|
|
163
|
+
fi
|
|
164
|
+
if [[ -z "${SENTINEL_WORK_ITEM_ID}" ]]; then
|
|
165
|
+
SENTINEL_WORK_ITEM_ID="$(printf '%s' "${SENTINEL_JSON}" | jq -r '.work_item_id // empty' 2>/dev/null)"
|
|
166
|
+
fi
|
|
167
|
+
SENTINEL_TURN_INDEX="$(printf '%s' "${SENTINEL_JSON}" | jq -r '.turn_index // 0' 2>/dev/null)"
|
|
168
|
+
SENTINEL_STARTED_AT="$(printf '%s' "${SENTINEL_JSON}" | jq -r '.started_at // empty' 2>/dev/null)"
|
|
169
|
+
printf '[%s] found sentinel=%s agent=%s work_item=%s turn_index=%s\n' \
|
|
170
|
+
"$(date -u +%FT%TZ)" "${SENTINEL_FILE}" "${SENTINEL_AGENT_TYPE}" "${SENTINEL_WORK_ITEM_ID}" "${SENTINEL_TURN_INDEX}" >> "${HOOK_LOG}"
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# --- compute token usage ---
|
|
174
|
+
# DELTA MODEL (per-task sentinel path): slice transcript from turn_index forward.
|
|
175
|
+
# CUMULATIVE GUARD (no-sentinel path): use entire transcript — single-fire so no double-count.
|
|
176
|
+
#
|
|
177
|
+
# turn_index counts assistant turns (0-based). We select assistant turns, skip the first
|
|
178
|
+
# turn_index of them, then sum the remainder. This isolates tokens attributable to this subagent run.
|
|
179
|
+
if [[ -n "${SENTINEL_FILE}" && -f "${SENTINEL_FILE}" && -n "${SENTINEL_AGENT_TYPE}" ]]; then
|
|
180
|
+
# Delta model: slice from turn_index
|
|
181
|
+
USAGE_JSON="$(jq -cs --argjson idx "${SENTINEL_TURN_INDEX}" '
|
|
182
|
+
[ .[] | select(.type == "assistant" and .message.usage) ]
|
|
183
|
+
| .[$idx:]
|
|
184
|
+
| (map(.message.usage.input_tokens // 0) | add // 0) as $in
|
|
185
|
+
| (map(.message.usage.output_tokens // 0) | add // 0) as $out
|
|
186
|
+
| (map(.message.usage.cache_creation_input_tokens // 0) | add // 0) as $cc
|
|
187
|
+
| (map(.message.usage.cache_read_input_tokens // 0) | add // 0) as $cr
|
|
188
|
+
| (map(.message.model) | unique | map(select(. != null)) | join(",")) as $models
|
|
189
|
+
| (length) as $turns
|
|
190
|
+
| {input: $in, output: $out, cache_creation: $cc, cache_read: $cr, model: $models, turns: $turns}
|
|
191
|
+
' "${TRANSCRIPT_PATH}" 2>/dev/null)"
|
|
192
|
+
else
|
|
193
|
+
# No sentinel: sum all assistant turns (full transcript — first fire, no prior rows to double-count)
|
|
194
|
+
USAGE_JSON="$(jq -cs '
|
|
195
|
+
map(select(.type == "assistant" and .message.usage))
|
|
196
|
+
| (map(.message.usage.input_tokens // 0) | add // 0) as $in
|
|
197
|
+
| (map(.message.usage.output_tokens // 0) | add // 0) as $out
|
|
198
|
+
| (map(.message.usage.cache_creation_input_tokens // 0) | add // 0) as $cc
|
|
199
|
+
| (map(.message.usage.cache_read_input_tokens // 0) | add // 0) as $cr
|
|
200
|
+
| (map(.message.model) | unique | map(select(. != null)) | join(",")) as $models
|
|
201
|
+
| (length) as $turns
|
|
202
|
+
| {input: $in, output: $out, cache_creation: $cc, cache_read: $cr, model: $models, turns: $turns}
|
|
203
|
+
' "${TRANSCRIPT_PATH}" 2>/dev/null)"
|
|
204
|
+
fi
|
|
92
205
|
|
|
93
206
|
if [[ -z "${USAGE_JSON}" || "${USAGE_JSON}" == "null" ]]; then
|
|
94
207
|
printf '[%s] could not parse usage from %s\n' "$(date -u +%FT%TZ)" "${TRANSCRIPT_PATH}" >> "${HOOK_LOG}"
|
|
95
208
|
exit 0
|
|
96
209
|
fi
|
|
97
210
|
|
|
98
|
-
# ---
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
211
|
+
# --- resolve agent_type and work_item_id ---
|
|
212
|
+
# Sentinel takes precedence; fall back to legacy transcript-grep when no sentinel.
|
|
213
|
+
AGENT_TYPE="${SENTINEL_AGENT_TYPE}"
|
|
214
|
+
WORK_ITEM_ID="${SENTINEL_WORK_ITEM_ID}"
|
|
215
|
+
|
|
216
|
+
if [[ -z "${AGENT_TYPE}" ]]; then
|
|
217
|
+
# Legacy: grep subagent_type from user messages, then role markers
|
|
218
|
+
AGENT_TYPE="$(jq -rs '
|
|
219
|
+
[.[] | select(.type == "user") | .message.content]
|
|
220
|
+
| tostring
|
|
221
|
+
| capture("subagent_type[\"\\s:=]+(?<t>[a-zA-Z0-9_-]+)"; "g")?.t
|
|
222
|
+
// "unknown"
|
|
223
|
+
' "${TRANSCRIPT_PATH}" 2>/dev/null)"
|
|
224
|
+
[[ -z "${AGENT_TYPE}" || "${AGENT_TYPE}" == "null" ]] && AGENT_TYPE="unknown"
|
|
225
|
+
|
|
226
|
+
if [[ "${AGENT_TYPE}" == "unknown" ]]; then
|
|
227
|
+
for role in architect developer qa reporter cleargate-wiki-contradict; do
|
|
228
|
+
if grep -qiE "\\b${role}\\b agent|role: ${role}|you are the ${role}" "${TRANSCRIPT_PATH}" 2>/dev/null; then
|
|
229
|
+
AGENT_TYPE="${role}"
|
|
230
|
+
break
|
|
231
|
+
fi
|
|
232
|
+
done
|
|
233
|
+
fi
|
|
116
234
|
fi
|
|
117
235
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if
|
|
156
|
-
|
|
236
|
+
if [[ -z "${WORK_ITEM_ID}" ]]; then
|
|
237
|
+
# Legacy: detect work_item_id (PRIMARY: first user message; FALLBACK: anywhere-grep)
|
|
238
|
+
#
|
|
239
|
+
# CR-026: banner-skip applied before jq scan (BUG-024 §3.1 Defect 2).
|
|
240
|
+
# The SessionStart hook emits a banner line of the form:
|
|
241
|
+
# "N items blocked: BUG-004: ..."
|
|
242
|
+
# This line poisons transcript-grep by matching the work-item regex first.
|
|
243
|
+
# We skip it via select(. | test(BANNER_SKIP_RE) | not) in the jq pipeline.
|
|
244
|
+
# BANNER_SKIP_RE is defined near the top of this script.
|
|
245
|
+
WORK_ITEM_RAW="$(jq -rs --arg banner_re "${BANNER_SKIP_RE}" '
|
|
246
|
+
[.[] | select(.type == "user")]
|
|
247
|
+
| [.[] | select(
|
|
248
|
+
(.message.content | if type == "array"
|
|
249
|
+
then map(.text? // "") | join(" ")
|
|
250
|
+
else (. // "") end
|
|
251
|
+
) | test($banner_re) | not
|
|
252
|
+
)]
|
|
253
|
+
| .[0].message.content
|
|
254
|
+
| if type == "array" then map(.text? // "") | join(" ") else (. // "") end
|
|
255
|
+
| tostring
|
|
256
|
+
| scan("(STORY|PROPOSAL|EPIC|CR|BUG|HOTFIX)[-=]?([0-9]+(-[0-9]+)?)") | .[0:2] | join("-")
|
|
257
|
+
' "${TRANSCRIPT_PATH}" 2>/dev/null | head -1)"
|
|
258
|
+
|
|
259
|
+
if [[ -n "${WORK_ITEM_RAW}" && "${WORK_ITEM_RAW}" != "null" && "${WORK_ITEM_RAW}" != "-" ]]; then
|
|
260
|
+
WORK_ITEM_ID="$(printf '%s' "${WORK_ITEM_RAW}" | sed 's/=/-/g')"
|
|
261
|
+
else
|
|
262
|
+
# CR-026: fallback grep also applies banner-skip via sed filter.
|
|
263
|
+
WORK_ITEM_ID="$(sed -E "/${BANNER_SKIP_RE}/d" "${TRANSCRIPT_PATH}" 2>/dev/null \
|
|
264
|
+
| grep -oE '(STORY|PROPOSAL|EPIC|CR|BUG|HOTFIX)[-=]?[0-9]+(-[0-9]+)?' \
|
|
265
|
+
| head -1 \
|
|
266
|
+
| sed 's/=/-/g')"
|
|
267
|
+
if [[ -n "${WORK_ITEM_ID}" ]]; then
|
|
268
|
+
printf '[%s] work_item_id fallback grep: %s\n' "$(date -u +%FT%TZ)" "${WORK_ITEM_ID}" >> "${HOOK_LOG}"
|
|
269
|
+
fi
|
|
270
|
+
fi
|
|
271
|
+
[[ -z "${WORK_ITEM_ID}" ]] && WORK_ITEM_ID=""
|
|
272
|
+
|
|
273
|
+
# Legacy fallback: if no work_item_id found at all, fall back to old grep for story_id only
|
|
274
|
+
if [[ -z "${WORK_ITEM_ID}" ]]; then
|
|
275
|
+
STORY_ID_LEGACY="$(grep -oE 'STORY[-=]?[0-9]{3}-[0-9]{2}' "${TRANSCRIPT_PATH}" 2>/dev/null \
|
|
276
|
+
| head -1 \
|
|
277
|
+
| sed -E 's/STORY[-=]?([0-9]{3}-[0-9]{2})/STORY-\1/')"
|
|
278
|
+
[[ -z "${STORY_ID_LEGACY}" ]] && STORY_ID_LEGACY="none"
|
|
279
|
+
WORK_ITEM_ID="${STORY_ID_LEGACY}"
|
|
157
280
|
fi
|
|
158
281
|
fi
|
|
159
|
-
[[ -z "${WORK_ITEM_ID}" ]] && WORK_ITEM_ID=""
|
|
160
282
|
|
|
161
283
|
# story_id is populated only when the work item is a STORY-* (backward compat)
|
|
162
284
|
STORY_ID=""
|
|
@@ -164,17 +286,97 @@ ACTIVE_SENTINEL="${REPO_ROOT}/.cleargate/sprint-runs/.active"
|
|
|
164
286
|
STORY_ID="${WORK_ITEM_ID}"
|
|
165
287
|
fi
|
|
166
288
|
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
289
|
+
# --- atomic sentinel deletion (mv → append → rm) ---
|
|
290
|
+
PROCESSED_FILE=""
|
|
291
|
+
if [[ -n "${SENTINEL_FILE}" && -f "${SENTINEL_FILE}" ]]; then
|
|
292
|
+
PROCESSED_FILE="${SENTINEL_FILE%.json}.processed-$$"
|
|
293
|
+
if ! mv "${SENTINEL_FILE}" "${PROCESSED_FILE}" 2>/dev/null; then
|
|
294
|
+
# mv failed (race condition or permissions): proceed without deletion
|
|
295
|
+
PROCESSED_FILE=""
|
|
296
|
+
printf '[%s] warn: could not mv sentinel %s\n' "$(date -u +%FT%TZ)" "${SENTINEL_FILE}" >> "${HOOK_LOG}"
|
|
297
|
+
fi
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# --- per-turn delta math (CR-018) ---
|
|
301
|
+
# USAGE_JSON (computed above) is the intra-fire cumulative session total from the transcript.
|
|
302
|
+
# We maintain .session-totals.json keyed by session_id with the last-known cumulative totals.
|
|
303
|
+
# Cross-fire delta = current_session_total − prior_session_total.
|
|
304
|
+
# For the first fire of a session, delta == session_total.
|
|
305
|
+
#
|
|
306
|
+
# Atomic update via mktemp + mv (POSIX rename is atomic on the same filesystem).
|
|
307
|
+
# Concurrent-fire safety: two worktree SubagentStop fires may race. The atomic rename
|
|
308
|
+
# ensures one writer wins; the other sees the updated file on its next read.
|
|
309
|
+
SESSION_TOTALS_FILE="${SPRINT_DIR}/.session-totals.json"
|
|
310
|
+
|
|
311
|
+
# Read current session totals (or empty object if file absent)
|
|
312
|
+
PRIOR_SESSION_JSON="{}"
|
|
313
|
+
if [[ -f "${SESSION_TOTALS_FILE}" ]]; then
|
|
314
|
+
PRIOR_SESSION_JSON="$(cat "${SESSION_TOTALS_FILE}" 2>/dev/null || printf '{}')"
|
|
315
|
+
[[ -z "${PRIOR_SESSION_JSON}" ]] && PRIOR_SESSION_JSON="{}"
|
|
174
316
|
fi
|
|
175
317
|
|
|
176
|
-
#
|
|
318
|
+
# Compute current intra-fire totals from USAGE_JSON
|
|
319
|
+
CURRENT_IN="$(printf '%s' "${USAGE_JSON}" | jq -r '.input // 0')"
|
|
320
|
+
CURRENT_OUT="$(printf '%s' "${USAGE_JSON}" | jq -r '.output // 0')"
|
|
321
|
+
CURRENT_CC="$(printf '%s' "${USAGE_JSON}" | jq -r '.cache_creation // 0')"
|
|
322
|
+
CURRENT_CR="$(printf '%s' "${USAGE_JSON}" | jq -r '.cache_read // 0')"
|
|
323
|
+
CURRENT_MODEL="$(printf '%s' "${USAGE_JSON}" | jq -r '.model // ""')"
|
|
324
|
+
CURRENT_TURNS="$(printf '%s' "${USAGE_JSON}" | jq -r '.turns // 0')"
|
|
325
|
+
|
|
326
|
+
# Look up prior totals for this session_id
|
|
327
|
+
PRIOR_IN="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -r --arg sid "${SESSION_ID}" '.[$sid].input // 0' 2>/dev/null || printf '0')"
|
|
328
|
+
PRIOR_OUT="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -r --arg sid "${SESSION_ID}" '.[$sid].output // 0' 2>/dev/null || printf '0')"
|
|
329
|
+
PRIOR_CC="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -r --arg sid "${SESSION_ID}" '.[$sid].cache_creation // 0' 2>/dev/null || printf '0')"
|
|
330
|
+
PRIOR_CR="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -r --arg sid "${SESSION_ID}" '.[$sid].cache_read // 0' 2>/dev/null || printf '0')"
|
|
331
|
+
|
|
332
|
+
# Compute delta = current − prior (floor at 0 to guard against transcript-reread edge cases)
|
|
333
|
+
DELTA_IN=$(( CURRENT_IN - PRIOR_IN ))
|
|
334
|
+
DELTA_OUT=$(( CURRENT_OUT - PRIOR_OUT ))
|
|
335
|
+
DELTA_CC=$(( CURRENT_CC - PRIOR_CC ))
|
|
336
|
+
DELTA_CR=$(( CURRENT_CR - PRIOR_CR ))
|
|
337
|
+
[[ "${DELTA_IN}" -lt 0 ]] && DELTA_IN=0
|
|
338
|
+
[[ "${DELTA_OUT}" -lt 0 ]] && DELTA_OUT=0
|
|
339
|
+
[[ "${DELTA_CC}" -lt 0 ]] && DELTA_CC=0
|
|
340
|
+
[[ "${DELTA_CR}" -lt 0 ]] && DELTA_CR=0
|
|
341
|
+
|
|
342
|
+
# Build delta and session_total JSON blocks
|
|
343
|
+
DELTA_JSON="$(jq -cn \
|
|
344
|
+
--argjson in "${DELTA_IN}" \
|
|
345
|
+
--argjson out "${DELTA_OUT}" \
|
|
346
|
+
--argjson cc "${DELTA_CC}" \
|
|
347
|
+
--argjson cr "${DELTA_CR}" \
|
|
348
|
+
'{input: $in, output: $out, cache_creation: $cc, cache_read: $cr}')"
|
|
349
|
+
|
|
350
|
+
SESSION_TOTAL_JSON="$(jq -cn \
|
|
351
|
+
--argjson in "${CURRENT_IN}" \
|
|
352
|
+
--argjson out "${CURRENT_OUT}" \
|
|
353
|
+
--argjson cc "${CURRENT_CC}" \
|
|
354
|
+
--argjson cr "${CURRENT_CR}" \
|
|
355
|
+
'{input: $in, output: $out, cache_creation: $cc, cache_read: $cr}')"
|
|
356
|
+
|
|
357
|
+
# Atomically update .session-totals.json with new totals for this session_id
|
|
177
358
|
TS="$(date -u +%FT%TZ)"
|
|
359
|
+
NEW_SESSION_TOTALS="$(printf '%s' "${PRIOR_SESSION_JSON}" | jq -c \
|
|
360
|
+
--arg sid "${SESSION_ID}" \
|
|
361
|
+
--argjson in "${CURRENT_IN}" \
|
|
362
|
+
--argjson out "${CURRENT_OUT}" \
|
|
363
|
+
--argjson cc "${CURRENT_CC}" \
|
|
364
|
+
--argjson cr "${CURRENT_CR}" \
|
|
365
|
+
--arg ts "${TS}" \
|
|
366
|
+
--argjson ti "${SENTINEL_TURN_INDEX}" \
|
|
367
|
+
'.[$sid] = {input: $in, output: $out, cache_creation: $cc, cache_read: $cr, last_ts: $ts, last_turn_index: $ti}' \
|
|
368
|
+
2>/dev/null)"
|
|
369
|
+
|
|
370
|
+
if [[ -n "${NEW_SESSION_TOTALS}" ]]; then
|
|
371
|
+
SESSION_TOTALS_TMP="$(mktemp "${SESSION_TOTALS_FILE}.tmp.XXXXXX" 2>/dev/null)"
|
|
372
|
+
if [[ -n "${SESSION_TOTALS_TMP}" ]]; then
|
|
373
|
+
printf '%s\n' "${NEW_SESSION_TOTALS}" > "${SESSION_TOTALS_TMP}"
|
|
374
|
+
mv "${SESSION_TOTALS_TMP}" "${SESSION_TOTALS_FILE}" 2>/dev/null || \
|
|
375
|
+
printf '[%s] warn: could not atomic-rename session-totals\n' "${TS}" >> "${HOOK_LOG}"
|
|
376
|
+
fi
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
# --- assemble ledger row (v2 schema: delta + session_total replace flat input/output/cache_*) ---
|
|
178
380
|
ROW="$(jq -cn \
|
|
179
381
|
--arg ts "${TS}" \
|
|
180
382
|
--arg agent "${AGENT_TYPE}" \
|
|
@@ -183,15 +385,31 @@ ACTIVE_SENTINEL="${REPO_ROOT}/.cleargate/sprint-runs/.active"
|
|
|
183
385
|
--arg session "${SESSION_ID}" \
|
|
184
386
|
--arg transcript "${TRANSCRIPT_PATH}" \
|
|
185
387
|
--arg sprint "${SPRINT_ID}" \
|
|
186
|
-
--
|
|
187
|
-
|
|
388
|
+
--arg sentinel_started_at "${SENTINEL_STARTED_AT}" \
|
|
389
|
+
--argjson turn_index "${SENTINEL_TURN_INDEX}" \
|
|
390
|
+
--argjson delta "${DELTA_JSON}" \
|
|
391
|
+
--argjson session_total "${SESSION_TOTAL_JSON}" \
|
|
392
|
+
--arg model "${CURRENT_MODEL}" \
|
|
393
|
+
--argjson turns "${CURRENT_TURNS}" \
|
|
394
|
+
'{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}')"
|
|
188
395
|
|
|
189
396
|
printf '%s\n' "${ROW}" >> "${LEDGER}"
|
|
190
|
-
printf '[%s] wrote row: sprint=%s agent=%s work_item=%s story=%s
|
|
397
|
+
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' \
|
|
191
398
|
"${TS}" "${SPRINT_ID}" "${AGENT_TYPE}" "${WORK_ITEM_ID}" "${STORY_ID}" \
|
|
192
|
-
"$
|
|
193
|
-
"$
|
|
399
|
+
"${DELTA_IN}" "${DELTA_OUT}" \
|
|
400
|
+
"${CURRENT_IN}" "${CURRENT_OUT}" \
|
|
401
|
+
"${SENTINEL_TURN_INDEX}" \
|
|
194
402
|
>> "${HOOK_LOG}"
|
|
403
|
+
|
|
404
|
+
# --- delete processed sentinel ---
|
|
405
|
+
if [[ -n "${PROCESSED_FILE}" && -f "${PROCESSED_FILE}" ]]; then
|
|
406
|
+
rm -f "${PROCESSED_FILE}"
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
# --- delete processed dispatch file ---
|
|
410
|
+
if [[ -n "${DISPATCH_PROCESSED:-}" && -f "${DISPATCH_PROCESSED}" ]]; then
|
|
411
|
+
rm -f "${DISPATCH_PROCESSED}"
|
|
412
|
+
fi
|
|
195
413
|
} 2>> "${HOOK_LOG}"
|
|
196
414
|
|
|
197
415
|
exit 0
|