cleargate 0.8.1 → 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.
Files changed (98) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +11 -0
  3. package/dist/MANIFEST.json +259 -28
  4. package/dist/{chunk-OM4FAEA7.js → chunk-Q3BTSXCK.js} +69 -3
  5. package/dist/chunk-Q3BTSXCK.js.map +1 -0
  6. package/dist/cli.cjs +2621 -548
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +2548 -560
  9. package/dist/cli.js.map +1 -1
  10. package/dist/lib/ledger.cjs +120 -0
  11. package/dist/lib/ledger.cjs.map +1 -0
  12. package/dist/lib/ledger.d.cts +64 -0
  13. package/dist/lib/ledger.d.ts +64 -0
  14. package/dist/lib/ledger.js +96 -0
  15. package/dist/lib/ledger.js.map +1 -0
  16. package/dist/templates/cleargate-planning/.claude/agents/architect.md +10 -8
  17. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
  18. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
  19. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
  20. package/dist/templates/cleargate-planning/.claude/agents/developer.md +29 -2
  21. package/dist/templates/cleargate-planning/.claude/agents/qa.md +50 -1
  22. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
  23. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
  24. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
  25. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
  26. package/dist/templates/cleargate-planning/.claude/settings.json +4 -0
  27. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
  28. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
  29. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
  30. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
  31. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
  32. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
  33. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
  34. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
  35. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
  36. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
  37. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
  38. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
  39. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
  40. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
  41. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
  42. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
  43. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
  44. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +33 -10
  45. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +41 -10
  46. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
  47. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +46 -12
  48. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +51 -1
  49. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
  50. package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +26 -13
  51. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
  52. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +64 -12
  53. package/dist/templates/cleargate-planning/CLAUDE.md +28 -10
  54. package/dist/templates/cleargate-planning/MANIFEST.json +259 -28
  55. package/dist/{whoami-CX7CXJD5.js → whoami-W4U6DPVG.js} +17 -17
  56. package/dist/whoami-W4U6DPVG.js.map +1 -0
  57. package/package.json +13 -2
  58. package/templates/cleargate-planning/.claude/agents/architect.md +10 -8
  59. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
  60. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
  61. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
  62. package/templates/cleargate-planning/.claude/agents/developer.md +29 -2
  63. package/templates/cleargate-planning/.claude/agents/qa.md +50 -1
  64. package/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
  65. package/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
  66. package/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
  67. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
  68. package/templates/cleargate-planning/.claude/settings.json +4 -0
  69. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
  70. package/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
  71. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
  72. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
  73. package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
  74. package/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
  75. package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
  76. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
  77. package/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
  78. package/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
  79. package/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
  80. package/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
  81. package/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
  82. package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
  83. package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
  84. package/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
  85. package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
  86. package/templates/cleargate-planning/.cleargate/templates/Bug.md +33 -10
  87. package/templates/cleargate-planning/.cleargate/templates/CR.md +41 -10
  88. package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
  89. package/templates/cleargate-planning/.cleargate/templates/epic.md +46 -12
  90. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +51 -1
  91. package/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
  92. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
  93. package/templates/cleargate-planning/.cleargate/templates/story.md +64 -12
  94. package/templates/cleargate-planning/CLAUDE.md +28 -10
  95. package/templates/cleargate-planning/MANIFEST.json +259 -28
  96. package/dist/chunk-OM4FAEA7.js.map +0 -1
  97. package/dist/whoami-CX7CXJD5.js.map +0 -1
  98. package/templates/cleargate-planning/.cleargate/templates/proposal.md +0 -61
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env bash
2
+ # test_prep_qa_context.sh
3
+ # Tests for prep_qa_context.mjs — 6 Gherkin scenarios:
4
+ # 1. Happy path — all sections present, size ≤20KB, schema_version:1
5
+ # 2. Missing story file — degrades to one-liner, exit 0
6
+ # 3. Missing baseline cache — baseline_unavailable:true, exit 0
7
+ # 4. Bundle size cap warning — oversized fixture warns stderr, still writes
8
+ # 5. Legacy STATUS=done — format:legacy in JSON, SCHEMA_INCOMPLETE in prose
9
+ # 6. Usage error — no args → exit 2, stderr contains "Usage:"
10
+ #
11
+ # Fixtures use mktemp -d. CLEARGATE_SPRINT_DIR + CLEARGATE_PENDING_SYNC_DIR
12
+ # env overrides for test isolation (FLASHCARD 2026-04-21 #test-harness #scripts #env).
13
+ # macOS bash 3.2 portable: no mapfile/readarray (FLASHCARD 2026-04-21 #bash #macos #portability).
14
+ # Exit 0 = all pass; exit 1 = one or more failures.
15
+ set -uo pipefail
16
+
17
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
18
+ SCRIPTS_DIR="${REPO_ROOT}/.cleargate/scripts"
19
+ PREP_SCRIPT="${SCRIPTS_DIR}/prep_qa_context.mjs"
20
+
21
+ PASS=0
22
+ FAIL=0
23
+
24
+ pass() {
25
+ echo "PASS: $1"
26
+ PASS=$((PASS + 1))
27
+ }
28
+
29
+ fail() {
30
+ echo "FAIL: $1"
31
+ echo " detail: $2"
32
+ FAIL=$((FAIL + 1))
33
+ }
34
+
35
+ make_tmpdir() {
36
+ mktemp -d
37
+ }
38
+
39
+ # ── Fixture helpers ────────────────────────────────────────────────────────────
40
+
41
+ # Write a minimal valid state.json with one Active story
42
+ write_state_json() {
43
+ local dir="$1"
44
+ local story_id="${2:-STORY-AAA-01}"
45
+ local story_state="${3:-Bouncing}"
46
+ local lane="${4:-standard}"
47
+ cat > "${dir}/state.json" << EOF
48
+ {
49
+ "schema_version": 2,
50
+ "sprint_id": "SPRINT-fixture",
51
+ "execution_mode": "v2",
52
+ "sprint_status": "Active",
53
+ "stories": {
54
+ "${story_id}": {
55
+ "state": "${story_state}",
56
+ "qa_bounces": 0,
57
+ "arch_bounces": 0,
58
+ "worktree": null,
59
+ "updated_at": "2026-05-01T00:00:00Z",
60
+ "notes": "",
61
+ "lane": "${lane}",
62
+ "lane_assigned_by": "test",
63
+ "lane_demoted_at": null,
64
+ "lane_demotion_reason": null
65
+ }
66
+ },
67
+ "last_action": "test setup",
68
+ "updated_at": "2026-05-01T00:00:00Z"
69
+ }
70
+ EOF
71
+ }
72
+
73
+ # Write a minimal milestone plan
74
+ write_milestone_plan() {
75
+ local dir="$1"
76
+ local story_id="${2:-STORY-AAA-01}"
77
+ mkdir -p "${dir}/plans"
78
+ cat > "${dir}/plans/M1.md" << EOF
79
+ # Milestone M1 — test fixture
80
+
81
+ ### ${story_id} — test story
82
+
83
+ - This is a test blueprint.
84
+ - Some content here.
85
+
86
+ ## Another Section
87
+ EOF
88
+ }
89
+
90
+ # Write a minimal story file in the pendingsync dir
91
+ write_story_file() {
92
+ local dir="$1"
93
+ local story_id="${2:-STORY-AAA-01}"
94
+ cat > "${dir}/${story_id}_Test_Story.md" << EOF
95
+ ---
96
+ story_id: ${story_id}
97
+ status: Approved
98
+ ---
99
+
100
+ # ${story_id}: Test Story
101
+
102
+ ## Gherkin
103
+
104
+ Given a test fixture
105
+ When the script runs
106
+ Then it should pass
107
+ EOF
108
+ }
109
+
110
+ # Write a .baseline-failures.json file
111
+ write_baseline_failures() {
112
+ local dir="$1"
113
+ cat > "${dir}/.baseline-failures.json" << 'EOF'
114
+ [
115
+ {"file": "cleargate-cli/test/sprint.test.ts", "count": 1}
116
+ ]
117
+ EOF
118
+ }
119
+
120
+ # Create a fake git worktree in tmpdir
121
+ # Sets up .git as a file pointing to a real git repo for git -C to work
122
+ setup_fake_worktree() {
123
+ local worktree_dir="$1"
124
+ local story_id="${2:-STORY-AAA-01}"
125
+
126
+ # Use the real repo but pretend we're checking the story branch
127
+ # The worktree dir has a .git file (linked worktree style)
128
+ # For simplicity, we just create a real git repo with one commit
129
+
130
+ local git_dir="${worktree_dir}/.git_fake_$$"
131
+ mkdir -p "${git_dir}"
132
+ git -C "${worktree_dir}" init --quiet 2>/dev/null || true
133
+ git -C "${worktree_dir}" config user.email "test@test.com" 2>/dev/null || true
134
+ git -C "${worktree_dir}" config user.name "Test" 2>/dev/null || true
135
+
136
+ # Create a dummy file and commit
137
+ echo "test" > "${worktree_dir}/test-fixture.txt"
138
+ git -C "${worktree_dir}" add test-fixture.txt 2>/dev/null || true
139
+ git -C "${worktree_dir}" commit --quiet -m "test(fixture): initial commit
140
+
141
+ STATUS: done
142
+ COMMIT: abc1234
143
+ TYPECHECK: pass
144
+ TESTS: 3 passed, 0 failed
145
+ FILES_CHANGED: test-fixture.txt
146
+ NOTES: test fixture commit" 2>/dev/null || true
147
+
148
+ # Create a 'main' branch alias (local ref) so diff --name-only main..HEAD works
149
+ git -C "${worktree_dir}" branch -f main HEAD 2>/dev/null || true
150
+ }
151
+
152
+ # Create worktree with a legacy STATUS=done commit
153
+ setup_legacy_status_worktree() {
154
+ local worktree_dir="$1"
155
+
156
+ git -C "${worktree_dir}" init --quiet 2>/dev/null || true
157
+ git -C "${worktree_dir}" config user.email "test@test.com" 2>/dev/null || true
158
+ git -C "${worktree_dir}" config user.name "Test" 2>/dev/null || true
159
+
160
+ echo "test" > "${worktree_dir}/test-file.txt"
161
+ git -C "${worktree_dir}" add test-file.txt 2>/dev/null || true
162
+ git -C "${worktree_dir}" commit --quiet -m "feat(test): legacy commit
163
+
164
+ STATUS: done
165
+ COMMIT: def5678
166
+ TYPECHECK: pass
167
+ TESTS: 2 passed, 0 failed
168
+ FILES_CHANGED: test-file.txt
169
+ NOTES: legacy format without r_coverage or plan_deviations" 2>/dev/null || true
170
+
171
+ git -C "${worktree_dir}" branch -f main HEAD 2>/dev/null || true
172
+ }
173
+
174
+ # ─── Scenario 1: Happy path ───────────────────────────────────────────────────
175
+
176
+ run_scenario_1() {
177
+ local tmpdir pendingsync_dir worktree_dir output_path
178
+ tmpdir="$(make_tmpdir)"
179
+ pendingsync_dir="$(make_tmpdir)"
180
+ worktree_dir="$(make_tmpdir)"
181
+
182
+ write_state_json "${tmpdir}" "STORY-AAA-01" "Bouncing" "standard"
183
+ write_milestone_plan "${tmpdir}" "STORY-AAA-01"
184
+ write_story_file "${pendingsync_dir}" "STORY-AAA-01"
185
+ write_baseline_failures "${tmpdir}"
186
+ setup_fake_worktree "${worktree_dir}" "STORY-AAA-01"
187
+
188
+ output_path="${tmpdir}/.qa-context-STORY-AAA-01.md"
189
+
190
+ local exit_code stderr_out
191
+ stderr_out=$(CLEARGATE_SPRINT_DIR="${tmpdir}" CLEARGATE_PENDING_SYNC_DIR="${pendingsync_dir}" \
192
+ node "${PREP_SCRIPT}" STORY-AAA-01 "${worktree_dir}" --output "${output_path}" 2>&1 >/dev/null)
193
+ exit_code=$?
194
+
195
+ if [ "${exit_code}" -ne 0 ]; then
196
+ fail "Scenario 1: happy path — expected exit 0, got ${exit_code}" "${stderr_out}"
197
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
198
+ fi
199
+
200
+ if [ ! -f "${output_path}" ]; then
201
+ fail "Scenario 1: happy path — output file not written at ${output_path}" "file missing"
202
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
203
+ fi
204
+
205
+ # Check size ≤20KB (20480 bytes)
206
+ local bundle_size
207
+ bundle_size=$(wc -c < "${output_path}" | tr -d ' ')
208
+ if [ "${bundle_size}" -gt 20480 ]; then
209
+ fail "Scenario 1: happy path — bundle size ${bundle_size} bytes exceeds 20KB" "too large"
210
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
211
+ fi
212
+
213
+ # Check all 8 section headers
214
+ local section
215
+ for section in "Worktree + Commit" "Spec Sources" "Baseline" "Adjacent Files" "Cross-Story Map" "Flashcard Slice" "Lane" "Dev Handoff"; do
216
+ if ! grep -qF "${section}" "${output_path}"; then
217
+ fail "Scenario 1: happy path — missing section '${section}' in bundle" "$(head -30 "${output_path}")"
218
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
219
+ fi
220
+ done
221
+
222
+ # Check JSON block parses to schema_version: 1
223
+ local schema_version
224
+ schema_version=$(node -e "
225
+ const fs = require('fs');
226
+ const content = fs.readFileSync('${output_path}', 'utf8');
227
+ const m = content.match(/\`\`\`json\\n([\\s\\S]*?)\\n\`\`\`/);
228
+ if (!m) { process.stdout.write('NO_JSON\\n'); process.exit(0); }
229
+ const obj = JSON.parse(m[1]);
230
+ process.stdout.write(String(obj.schema_version) + '\\n');
231
+ " 2>/dev/null)
232
+
233
+ if [ "${schema_version}" != "1" ]; then
234
+ fail "Scenario 1: happy path — expected schema_version 1, got '${schema_version}'" "json parse issue"
235
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
236
+ fi
237
+
238
+ pass "Scenario 1: prep_qa_context happy path — all 8 sections, ≤20KB, schema_version:1"
239
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"
240
+ }
241
+
242
+ # ─── Scenario 2: Missing story file → one-liner ───────────────────────────────
243
+
244
+ run_scenario_2() {
245
+ local tmpdir pendingsync_dir worktree_dir output_path
246
+ tmpdir="$(make_tmpdir)"
247
+ pendingsync_dir="$(make_tmpdir)"
248
+ worktree_dir="$(make_tmpdir)"
249
+
250
+ write_state_json "${tmpdir}" "STORY-AAA-01" "Bouncing" "standard"
251
+ write_milestone_plan "${tmpdir}" "STORY-AAA-01"
252
+ # Deliberately NO story file in pendingsync_dir
253
+ write_baseline_failures "${tmpdir}"
254
+ setup_fake_worktree "${worktree_dir}" "STORY-AAA-01"
255
+
256
+ output_path="${tmpdir}/.qa-context-STORY-AAA-01.md"
257
+
258
+ local exit_code
259
+ CLEARGATE_SPRINT_DIR="${tmpdir}" CLEARGATE_PENDING_SYNC_DIR="${pendingsync_dir}" \
260
+ node "${PREP_SCRIPT}" STORY-AAA-01 "${worktree_dir}" --output "${output_path}" >/dev/null 2>&1
261
+ exit_code=$?
262
+
263
+ if [ "${exit_code}" -ne 0 ]; then
264
+ fail "Scenario 2: missing story — expected exit 0, got ${exit_code}" "exit code mismatch"
265
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
266
+ fi
267
+
268
+ if [ ! -f "${output_path}" ]; then
269
+ fail "Scenario 2: missing story — output file not written" "file missing"
270
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
271
+ fi
272
+
273
+ # Spec Sources section must contain the story-not-found one-liner
274
+ if ! grep -qF "Story file not found" "${output_path}"; then
275
+ fail "Scenario 2: missing story — expected 'Story file not found' one-liner in Spec Sources" \
276
+ "$(grep -A5 'Spec Sources' "${output_path}" | head -10)"
277
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
278
+ fi
279
+
280
+ # Other sections still present (not impacted)
281
+ if ! grep -qF "## Baseline" "${output_path}"; then
282
+ fail "Scenario 2: missing story — Baseline section missing (other sections impacted)" "missing section"
283
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
284
+ fi
285
+
286
+ pass "Scenario 2: missing story file degrades to one-liner, other sections unaffected"
287
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"
288
+ }
289
+
290
+ # ─── Scenario 3: Missing baseline cache → baseline_unavailable:true ──────────
291
+
292
+ run_scenario_3() {
293
+ local tmpdir pendingsync_dir worktree_dir output_path
294
+ tmpdir="$(make_tmpdir)"
295
+ pendingsync_dir="$(make_tmpdir)"
296
+ worktree_dir="$(make_tmpdir)"
297
+
298
+ write_state_json "${tmpdir}" "STORY-AAA-01" "Bouncing" "standard"
299
+ write_milestone_plan "${tmpdir}" "STORY-AAA-01"
300
+ write_story_file "${pendingsync_dir}" "STORY-AAA-01"
301
+ # Deliberately NO .baseline-failures.json
302
+ setup_fake_worktree "${worktree_dir}" "STORY-AAA-01"
303
+
304
+ output_path="${tmpdir}/.qa-context-STORY-AAA-01.md"
305
+
306
+ local exit_code
307
+ CLEARGATE_SPRINT_DIR="${tmpdir}" CLEARGATE_PENDING_SYNC_DIR="${pendingsync_dir}" \
308
+ node "${PREP_SCRIPT}" STORY-AAA-01 "${worktree_dir}" --output "${output_path}" >/dev/null 2>&1
309
+ exit_code=$?
310
+
311
+ if [ "${exit_code}" -ne 0 ]; then
312
+ fail "Scenario 3: missing baseline — expected exit 0, got ${exit_code}" "exit code mismatch"
313
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
314
+ fi
315
+
316
+ # Check JSON baseline_unavailable === true
317
+ local baseline_unavailable
318
+ baseline_unavailable=$(node -e "
319
+ const fs = require('fs');
320
+ const content = fs.readFileSync('${output_path}', 'utf8');
321
+ const m = content.match(/\`\`\`json\\n([\\s\\S]*?)\\n\`\`\`/);
322
+ if (!m) { process.stdout.write('NO_JSON\\n'); process.exit(0); }
323
+ const obj = JSON.parse(m[1]);
324
+ process.stdout.write(String(obj.baseline.baseline_unavailable) + '\\n');
325
+ " 2>/dev/null)
326
+
327
+ if [ "${baseline_unavailable}" != "true" ]; then
328
+ fail "Scenario 3: missing baseline — expected baseline_unavailable=true, got '${baseline_unavailable}'" "json check"
329
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
330
+ fi
331
+
332
+ # Prose should contain recompute one-liner
333
+ if ! grep -q "cleargate gate test" "${output_path}"; then
334
+ fail "Scenario 3: missing baseline — expected recompute one-liner in Baseline section" \
335
+ "$(grep -A5 '## Baseline' "${output_path}" | head -10)"
336
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
337
+ fi
338
+
339
+ pass "Scenario 3: missing baseline cache → baseline_unavailable:true, recompute one-liner present"
340
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"
341
+ }
342
+
343
+ # ─── Scenario 4: Bundle size cap warning ─────────────────────────────────────
344
+
345
+ run_scenario_4() {
346
+ local tmpdir pendingsync_dir worktree_dir output_path
347
+ tmpdir="$(make_tmpdir)"
348
+ pendingsync_dir="$(make_tmpdir)"
349
+ worktree_dir="$(make_tmpdir)"
350
+
351
+ write_state_json "${tmpdir}" "STORY-AAA-01" "Bouncing" "standard"
352
+ write_milestone_plan "${tmpdir}" "STORY-AAA-01"
353
+ write_story_file "${pendingsync_dir}" "STORY-AAA-01"
354
+
355
+ # Create an oversized fixture: write a very large baseline-failures.json
356
+ # with 500+ entries to bloat the JSON block past 20KB
357
+ local big_json="${tmpdir}/.baseline-failures.json"
358
+ printf '[' > "${big_json}"
359
+ local i=0
360
+ while [ $i -lt 400 ]; do
361
+ if [ $i -gt 0 ]; then printf ',' >> "${big_json}"; fi
362
+ printf '{"file":"cleargate-cli/test/very/long/path/to/some/test/file/number_%d_with_extra_padding_so_it_is_bigger/test.spec.ts","count":%d}' $i $i >> "${big_json}"
363
+ i=$((i + 1))
364
+ done
365
+ printf ']' >> "${big_json}"
366
+
367
+ setup_fake_worktree "${worktree_dir}" "STORY-AAA-01"
368
+
369
+ output_path="${tmpdir}/.qa-context-STORY-AAA-01.md"
370
+
371
+ local exit_code stderr_out
372
+ stderr_out=$(CLEARGATE_SPRINT_DIR="${tmpdir}" CLEARGATE_PENDING_SYNC_DIR="${pendingsync_dir}" \
373
+ node "${PREP_SCRIPT}" STORY-AAA-01 "${worktree_dir}" --output "${output_path}" 2>&1 >/dev/null)
374
+ exit_code=$?
375
+
376
+ # Must still exit 0 (R4: write anyway)
377
+ if [ "${exit_code}" -ne 0 ]; then
378
+ fail "Scenario 4: size cap — expected exit 0 even when oversized, got ${exit_code}" "${stderr_out}"
379
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
380
+ fi
381
+
382
+ # Bundle must still be written
383
+ if [ ! -f "${output_path}" ]; then
384
+ fail "Scenario 4: size cap — bundle not written even when oversized" "file missing"
385
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
386
+ fi
387
+
388
+ # Stderr must contain warning about exceeding 20KB
389
+ if ! echo "${stderr_out}" | grep -qi "exceeds 20KB"; then
390
+ fail "Scenario 4: size cap — expected stderr warning about 20KB target" "stderr: ${stderr_out}"
391
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
392
+ fi
393
+
394
+ pass "Scenario 4: oversized fixture → exit 0, bundle written, stderr warns about 20KB"
395
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"
396
+ }
397
+
398
+ # ─── Scenario 5: Legacy STATUS=done → format:legacy ──────────────────────────
399
+
400
+ run_scenario_5() {
401
+ local tmpdir pendingsync_dir worktree_dir output_path
402
+ tmpdir="$(make_tmpdir)"
403
+ pendingsync_dir="$(make_tmpdir)"
404
+ worktree_dir="$(make_tmpdir)"
405
+
406
+ write_state_json "${tmpdir}" "STORY-AAA-01" "Bouncing" "standard"
407
+ write_milestone_plan "${tmpdir}" "STORY-AAA-01"
408
+ write_story_file "${pendingsync_dir}" "STORY-AAA-01"
409
+ setup_legacy_status_worktree "${worktree_dir}"
410
+
411
+ output_path="${tmpdir}/.qa-context-STORY-AAA-01.md"
412
+
413
+ local exit_code
414
+ CLEARGATE_SPRINT_DIR="${tmpdir}" CLEARGATE_PENDING_SYNC_DIR="${pendingsync_dir}" \
415
+ node "${PREP_SCRIPT}" STORY-AAA-01 "${worktree_dir}" --output "${output_path}" >/dev/null 2>&1
416
+ exit_code=$?
417
+
418
+ if [ "${exit_code}" -ne 0 ]; then
419
+ fail "Scenario 5: legacy handoff — expected exit 0, got ${exit_code}" "exit code mismatch"
420
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
421
+ fi
422
+
423
+ # Check JSON dev_handoff.format === "legacy"
424
+ local handoff_format
425
+ handoff_format=$(node -e "
426
+ const fs = require('fs');
427
+ const content = fs.readFileSync('${output_path}', 'utf8');
428
+ const m = content.match(/\`\`\`json\\n([\\s\\S]*?)\\n\`\`\`/);
429
+ if (!m) { process.stdout.write('NO_JSON\\n'); process.exit(0); }
430
+ const obj = JSON.parse(m[1]);
431
+ process.stdout.write(String(obj.dev_handoff.format) + '\\n');
432
+ " 2>/dev/null)
433
+
434
+ if [ "${handoff_format}" != "legacy" ]; then
435
+ fail "Scenario 5: legacy handoff — expected dev_handoff.format=legacy, got '${handoff_format}'" "json check"
436
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
437
+ fi
438
+
439
+ # Prose must contain SCHEMA_INCOMPLETE warning
440
+ if ! grep -qF "SCHEMA_INCOMPLETE" "${output_path}"; then
441
+ fail "Scenario 5: legacy handoff — expected SCHEMA_INCOMPLETE in Dev Handoff section" \
442
+ "$(grep -A5 '## Dev Handoff' "${output_path}" | head -10)"
443
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"; return
444
+ fi
445
+
446
+ pass "Scenario 5: legacy STATUS=done → format:legacy in JSON, SCHEMA_INCOMPLETE in prose"
447
+ rm -rf "${tmpdir}" "${pendingsync_dir}" "${worktree_dir}"
448
+ }
449
+
450
+ # ─── Scenario 6: Usage error — no args ────────────────────────────────────────
451
+
452
+ run_scenario_6() {
453
+ local exit_code stderr_out
454
+
455
+ stderr_out=$(node "${PREP_SCRIPT}" 2>&1 >/dev/null)
456
+ exit_code=$?
457
+
458
+ if [ "${exit_code}" -ne 2 ]; then
459
+ fail "Scenario 6: usage error — expected exit 2, got ${exit_code}" "exit code mismatch"
460
+ return
461
+ fi
462
+
463
+ if ! echo "${stderr_out}" | grep -qi "Usage:"; then
464
+ fail "Scenario 6: usage error — stderr must contain 'Usage:'" "stderr: ${stderr_out}"
465
+ return
466
+ fi
467
+
468
+ pass "Scenario 6: no args → exit 2, stderr contains 'Usage:'"
469
+ }
470
+
471
+ # ─── Run all scenarios ────────────────────────────────────────────────────────
472
+
473
+ run_scenario_1
474
+ run_scenario_2
475
+ run_scenario_3
476
+ run_scenario_4
477
+ run_scenario_5
478
+ run_scenario_6
479
+
480
+ echo ""
481
+ echo "Results: ${PASS} passed, ${FAIL} failed"
482
+ [ "${FAIL}" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ # write_dispatch.sh — write a dispatch marker JSON file before each Task() spawn.
3
+ #
4
+ # The token-ledger.sh SubagentStop hook reads this file (keyed by session_id) to
5
+ # get explicit attribution (work_item_id + agent_type) rather than relying on
6
+ # transcript-grep heuristics.
7
+ #
8
+ # Usage:
9
+ # bash .cleargate/scripts/write_dispatch.sh <work_item_id> <agent_type>
10
+ #
11
+ # FALLBACK PATH (CR-026): The primary dispatch-marker path is the PreToolUse:Task hook
12
+ # at `.claude/hooks/pre-tool-use-task.sh`, which auto-writes the marker on every Task()
13
+ # spawn without manual orchestrator intervention. This script is retained for one-off
14
+ # Architect dispatches or spawns whose Task() prompt does not contain a parseable
15
+ # work-item marker. Use it only when the PreToolUse:Task hook cannot determine the
16
+ # work_item_id from the prompt (e.g., a generic Architect spawn not tied to a sprint item).
17
+ #
18
+ # Args:
19
+ # $1 work_item_id — e.g. STORY-020-02, CR-016, BUG-021
20
+ # $2 agent_type — one of: developer|architect|qa|reporter|cleargate-wiki-contradict
21
+ #
22
+ # Env (optional):
23
+ # CLAUDE_SESSION_ID — session UUID of the orchestrator session
24
+ # ORCHESTRATOR_PROJECT_DIR — override for repo root (cross-project routing)
25
+ #
26
+ # Exit codes:
27
+ # 0 success
28
+ # 1 missing required args
29
+ # 2 no .active sprint sentinel found
30
+ #
31
+ # Output: .cleargate/sprint-runs/<sprint>/.dispatch-<session-id>.json
32
+ # Log: .cleargate/hook-log/write_dispatch.log
33
+
34
+ set -u
35
+
36
+ # ─── Resolve repo root ──────────────────────────────────────────────────────
37
+ REPO_ROOT="${ORCHESTRATOR_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}"
38
+ LOG_DIR="${REPO_ROOT}/.cleargate/hook-log"
39
+ mkdir -p "${LOG_DIR}"
40
+ LOG="${LOG_DIR}/write_dispatch.log"
41
+
42
+ # ─── Validate args ──────────────────────────────────────────────────────────
43
+ if [[ $# -lt 2 || -z "${1:-}" || -z "${2:-}" ]]; then
44
+ printf '[%s] error: usage: write_dispatch.sh <work_item_id> <agent_type>\n' "$(date -u +%FT%TZ)" >> "${LOG}"
45
+ printf 'Usage: write_dispatch.sh <work_item_id> <agent_type>\n' >&2
46
+ exit 1
47
+ fi
48
+
49
+ WORK_ITEM_ID="${1}"
50
+ AGENT_TYPE="${2}"
51
+
52
+ # ─── Resolve active sprint ──────────────────────────────────────────────────
53
+ ACTIVE_SENTINEL="${REPO_ROOT}/.cleargate/sprint-runs/.active"
54
+ if [[ ! -f "${ACTIVE_SENTINEL}" ]]; then
55
+ printf '[%s] error: no .active sentinel at %s\n' "$(date -u +%FT%TZ)" "${ACTIVE_SENTINEL}" >> "${LOG}"
56
+ printf 'error: no active sprint sentinel at %s\n' "${ACTIVE_SENTINEL}" >&2
57
+ exit 2
58
+ fi
59
+
60
+ SPRINT_ID="$(tr -d '[:space:]' < "${ACTIVE_SENTINEL}")"
61
+ if [[ -z "${SPRINT_ID}" ]]; then
62
+ printf '[%s] error: .active sentinel is empty\n' "$(date -u +%FT%TZ)" >> "${LOG}"
63
+ printf 'error: .active sentinel is empty\n' >&2
64
+ exit 2
65
+ fi
66
+
67
+ SPRINT_DIR="${REPO_ROOT}/.cleargate/sprint-runs/${SPRINT_ID}"
68
+ mkdir -p "${SPRINT_DIR}"
69
+
70
+ # ─── Resolve session_id ─────────────────────────────────────────────────────
71
+ SESSION_ID="${CLAUDE_SESSION_ID:-}"
72
+
73
+ if [[ -z "${SESSION_ID}" ]]; then
74
+ # Fall back to scanning the most recent transcript filename (UUID) under
75
+ # ~/.claude/projects/-*-ClearGate/ pattern.
76
+ TRANSCRIPT_DIR="${HOME}/.claude/projects"
77
+ if [[ -d "${TRANSCRIPT_DIR}" ]]; then
78
+ SESSION_ID="$(find "${TRANSCRIPT_DIR}" -maxdepth 2 -name '*.jsonl' 2>/dev/null \
79
+ | sort -t '/' -k1 2>/dev/null | tail -1 \
80
+ | xargs -I{} basename {} .jsonl 2>/dev/null || true)"
81
+ fi
82
+ fi
83
+
84
+ if [[ -z "${SESSION_ID}" ]]; then
85
+ # Final fallback: generate a pseudo-id from timestamp
86
+ SESSION_ID="fallback-$(date -u +%s)"
87
+ printf '[%s] warn: no CLAUDE_SESSION_ID and no transcript found; using %s\n' "$(date -u +%FT%TZ)" "${SESSION_ID}" >> "${LOG}"
88
+ fi
89
+
90
+ # ─── Resolve cleargate version ───────────────────────────────────────────────
91
+ # Read from cleargate-cli/package.json if available; otherwise use "unknown"
92
+ PKG_JSON="${REPO_ROOT}/cleargate-cli/package.json"
93
+ CG_VERSION="unknown"
94
+ if [[ -f "${PKG_JSON}" ]]; then
95
+ CG_VERSION="$(jq -r '.version // "unknown"' "${PKG_JSON}" 2>/dev/null || echo "unknown")"
96
+ fi
97
+
98
+ # ─── Write dispatch file atomically ─────────────────────────────────────────
99
+ DISPATCH_TARGET="${SPRINT_DIR}/.dispatch-${SESSION_ID}.json"
100
+ SPAWNED_AT="$(date -u +%FT%TZ)"
101
+
102
+ DISPATCH_JSON="$(jq -cn \
103
+ --arg work_item_id "${WORK_ITEM_ID}" \
104
+ --arg agent_type "${AGENT_TYPE}" \
105
+ --arg spawned_at "${SPAWNED_AT}" \
106
+ --arg session_id "${SESSION_ID}" \
107
+ --arg writer "write_dispatch.sh@cleargate-${CG_VERSION}" \
108
+ '{
109
+ work_item_id: $work_item_id,
110
+ agent_type: $agent_type,
111
+ spawned_at: $spawned_at,
112
+ session_id: $session_id,
113
+ writer: $writer
114
+ }')"
115
+
116
+ # Atomic write via mktemp + mv (rename is atomic on POSIX same-fs)
117
+ TMP="$(mktemp "${SPRINT_DIR}/.dispatch-tmp-XXXXXX")"
118
+ printf '%s\n' "${DISPATCH_JSON}" > "${TMP}"
119
+ mv "${TMP}" "${DISPATCH_TARGET}"
120
+
121
+ printf '[%s] wrote dispatch: sprint=%s session=%s work_item=%s agent=%s\n' \
122
+ "${SPAWNED_AT}" "${SPRINT_ID}" "${SESSION_ID}" "${WORK_ITEM_ID}" "${AGENT_TYPE}" >> "${LOG}"
123
+
124
+ printf '%s\n' "${DISPATCH_TARGET}"
125
+ exit 0
@@ -8,13 +8,28 @@ YAML Frontmatter: Bug ID, Parent Ref, Status, Severity, Reporter, Approved gate.
8
8
  §5 Verification Protocol: The failing test that proves the bug exists and proves the fix resolves it.
9
9
  Output location: .cleargate/delivery/pending-sync/BUG-{ID}.md
10
10
 
11
- CRITICAL PHASE GATE: Do NOT invoke cleargate_push_item until reproduction steps are 100% deterministic and a failing test is attached.
11
+ POST-WRITE BRIEF
12
+ After Writing this document, render a Brief in chat with the following sections,
13
+ mechanically extracted from the document's own structure:
14
+
15
+ - Summary ← §1 The Anomaly (repro)
16
+ - Open Questions ← §0.5 Open Questions
17
+ - Edge Cases ← §2 Impact (edge conditions)
18
+ - Risks ← §2 Impact
19
+ - Ambiguity ← bottom-of-doc ambiguity gate block
20
+
21
+ Halt for human review. When ambiguity reaches 🟢, proceed to call cleargate_push_item.
22
+ Do NOT ask separately for push confirmation — Brief approval covers it.
23
+
12
24
  Do NOT output these instructions.
13
25
  </instructions>
14
26
 
15
27
  ---
16
28
  bug_id: "BUG-{ID}"
17
29
  parent_ref: "EPIC-{ID} | STORY-{ID}"
30
+ parent_cleargate_id: null # canonical cleargate-id of parent work item; null for top-level
31
+ sprint_cleargate_id: null # canonical cleargate-id of owning sprint; null for off-sprint items
32
+ carry_over: false # set true to skip lifecycle reconciliation at sprint close
18
33
  status: "Draft | Triaged | In Fix | Verified"
19
34
  severity: "P0-Critical | P1-High | P2-Medium | P3-Low"
20
35
  reporter: "{name}"
@@ -35,19 +50,27 @@ cached_gate_result:
35
50
  pass: null
36
51
  failing_criteria: []
37
52
  last_gate_check: null
38
- # Sync attribution (EPIC-010). Optional; stamped by `cleargate push` / `cleargate pull`.
39
- pushed_by: null # STORY-010-07 writer / STORY-010-04 reader
40
- pushed_at: null # STORY-010-07 writer / STORY-010-04 reader
41
- last_pulled_by: null # STORY-010-04 writer / STORY-010-03 reader
42
- last_pulled_at: null # STORY-010-04 writer / STORY-010-03 reader
43
- last_remote_update: null # STORY-010-02 writer (from MCP) / STORY-010-03 reader
44
- source: "local-authored" # STORY-010-05 flips to "remote-authored" on intake
45
- last_synced_status: null # STORY-010-04 writer; required for conflict-detector rule 6
46
- last_synced_body_sha: null # STORY-010-04 writer; sha256 of body at last sync
53
+ # Sync attribution. Optional; stamped by `cleargate push` / `cleargate pull`.
54
+ pushed_by: null # set by push: which user pushed
55
+ pushed_at: null # set by push: ISO-8601 timestamp
56
+ last_pulled_by: null # set by pull: which user pulled
57
+ last_pulled_at: null # set by pull: ISO-8601 timestamp
58
+ last_remote_update: null # set by pull: server's last-modified timestamp
59
+ source: "local-authored" # flips to "remote-authored" on intake
60
+ last_synced_status: null # required for conflict-detector; status at last sync
61
+ last_synced_body_sha: null # sha256 of body at last sync
47
62
  ---
48
63
 
49
64
  # BUG-{ID}: {Bug Name}
50
65
 
66
+ ## 0.5 Open Questions
67
+
68
+ > Populate during drafting. Resolve every entry before flipping ambiguity to 🟢.
69
+
70
+ - **Question:** {edge case, contradiction, or missing detail}
71
+ - **Recommended:** {agent's proposed answer}
72
+ - **Human decision:** {populated during Brief review}
73
+
51
74
  ## 1. The Anomaly (Expected vs. Actual)
52
75
  **Expected Behavior:** {What the system should do under normal conditions.}
53
76