cleargate 0.8.2 → 0.11.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 (122) hide show
  1. package/CHANGELOG.md +210 -0
  2. package/README.md +22 -1
  3. package/dist/MANIFEST.json +276 -31
  4. package/dist/chunk-HZPJ5QX4.js +459 -0
  5. package/dist/chunk-HZPJ5QX4.js.map +1 -0
  6. package/dist/{chunk-OM4FAEA7.js → chunk-Q3BTSXCK.js} +69 -3
  7. package/dist/chunk-Q3BTSXCK.js.map +1 -0
  8. package/dist/cli.cjs +2888 -598
  9. package/dist/cli.cjs.map +1 -1
  10. package/dist/cli.js +2481 -619
  11. package/dist/cli.js.map +1 -1
  12. package/dist/lib/ledger.cjs +120 -0
  13. package/dist/lib/ledger.cjs.map +1 -0
  14. package/dist/lib/ledger.d.cts +64 -0
  15. package/dist/lib/ledger.d.ts +64 -0
  16. package/dist/lib/ledger.js +96 -0
  17. package/dist/lib/ledger.js.map +1 -0
  18. package/dist/lib/lifecycle-reconcile.cjs +497 -0
  19. package/dist/lib/lifecycle-reconcile.cjs.map +1 -0
  20. package/dist/lib/lifecycle-reconcile.d.cts +136 -0
  21. package/dist/lib/lifecycle-reconcile.d.ts +136 -0
  22. package/dist/lib/lifecycle-reconcile.js +20 -0
  23. package/dist/lib/lifecycle-reconcile.js.map +1 -0
  24. package/dist/templates/cleargate-planning/.claude/agents/architect.md +65 -10
  25. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
  26. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
  27. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
  28. package/dist/templates/cleargate-planning/.claude/agents/developer.md +51 -2
  29. package/dist/templates/cleargate-planning/.claude/agents/devops.md +249 -0
  30. package/dist/templates/cleargate-planning/.claude/agents/qa.md +91 -1
  31. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +72 -14
  32. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
  33. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
  34. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
  35. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
  36. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +334 -96
  37. package/dist/templates/cleargate-planning/.claude/settings.json +4 -0
  38. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +644 -0
  39. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
  40. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
  41. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
  42. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
  43. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +72 -9
  44. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
  45. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
  46. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +471 -29
  47. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
  48. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
  49. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
  50. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
  51. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
  52. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
  53. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
  54. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
  55. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +483 -13
  56. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
  57. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
  58. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +136 -0
  59. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +27 -1
  60. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +35 -1
  61. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
  62. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +40 -3
  63. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +53 -0
  64. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
  65. package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +17 -4
  66. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
  67. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
  68. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +58 -3
  69. package/dist/templates/cleargate-planning/CLAUDE.md +30 -10
  70. package/dist/templates/cleargate-planning/MANIFEST.json +276 -31
  71. package/dist/{whoami-CX7CXJD5.js → whoami-W4U6DPVG.js} +17 -17
  72. package/dist/whoami-W4U6DPVG.js.map +1 -0
  73. package/package.json +20 -6
  74. package/templates/cleargate-planning/.claude/agents/architect.md +65 -10
  75. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
  76. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
  77. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
  78. package/templates/cleargate-planning/.claude/agents/developer.md +51 -2
  79. package/templates/cleargate-planning/.claude/agents/devops.md +249 -0
  80. package/templates/cleargate-planning/.claude/agents/qa.md +91 -1
  81. package/templates/cleargate-planning/.claude/agents/reporter.md +72 -14
  82. package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
  83. package/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
  84. package/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
  85. package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
  86. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +334 -96
  87. package/templates/cleargate-planning/.claude/settings.json +4 -0
  88. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +644 -0
  89. package/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
  90. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
  91. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
  92. package/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
  93. package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +72 -9
  94. package/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
  95. package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
  96. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +471 -29
  97. package/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
  98. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
  99. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
  100. package/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
  101. package/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
  102. package/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
  103. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
  104. package/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
  105. package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +483 -13
  106. package/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
  107. package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
  108. package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +136 -0
  109. package/templates/cleargate-planning/.cleargate/templates/Bug.md +27 -1
  110. package/templates/cleargate-planning/.cleargate/templates/CR.md +35 -1
  111. package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
  112. package/templates/cleargate-planning/.cleargate/templates/epic.md +40 -3
  113. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +53 -0
  114. package/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
  115. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
  116. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
  117. package/templates/cleargate-planning/.cleargate/templates/story.md +58 -3
  118. package/templates/cleargate-planning/CLAUDE.md +30 -10
  119. package/templates/cleargate-planning/MANIFEST.json +276 -31
  120. package/dist/chunk-OM4FAEA7.js.map +0 -1
  121. package/dist/whoami-CX7CXJD5.js.map +0 -1
  122. 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
@@ -19,11 +19,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
19
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
20
20
 
21
21
  /**
22
- * Validate a parsed state object.
22
+ * Validate a parsed state object, ignoring schema_version check.
23
+ * Used PRE-MIGRATION so that v1 files pass shape validation before being upgraded.
23
24
  * @param {object} state - Parsed state.json content
24
25
  * @returns {{ valid: boolean, errors: string[] }}
25
26
  */
26
- export function validateState(state) {
27
+ export function validateShapeIgnoringVersion(state) {
27
28
  const errors = [];
28
29
 
29
30
  if (typeof state !== 'object' || state === null) {
@@ -31,12 +32,6 @@ export function validateState(state) {
31
32
  return { valid: false, errors };
32
33
  }
33
34
 
34
- if (state.schema_version !== SCHEMA_VERSION) {
35
- errors.push(
36
- `schema_version mismatch: expected ${SCHEMA_VERSION}, got ${state.schema_version}`
37
- );
38
- }
39
-
40
35
  if (!state.sprint_id) {
41
36
  errors.push('missing required field: sprint_id');
42
37
  }
@@ -98,6 +93,35 @@ export function validateState(state) {
98
93
  return { valid: errors.length === 0, errors };
99
94
  }
100
95
 
96
+ /**
97
+ * Validate a parsed state object (strict — includes schema_version check).
98
+ * Call this AFTER migration to assert the file is fully v2-compliant.
99
+ * @param {object} state - Parsed state.json content
100
+ * @returns {{ valid: boolean, errors: string[] }}
101
+ */
102
+ export function validateState(state) {
103
+ const errors = [];
104
+
105
+ if (typeof state !== 'object' || state === null) {
106
+ errors.push('state is not an object');
107
+ return { valid: false, errors };
108
+ }
109
+
110
+ if (state.schema_version !== SCHEMA_VERSION) {
111
+ errors.push(
112
+ `schema_version mismatch: expected ${SCHEMA_VERSION}, got ${state.schema_version}`
113
+ );
114
+ }
115
+
116
+ // Delegate shape validation (everything except version check)
117
+ const shapeResult = validateShapeIgnoringVersion(state);
118
+ for (const e of shapeResult.errors) {
119
+ errors.push(e);
120
+ }
121
+
122
+ return { valid: errors.length === 0, errors };
123
+ }
124
+
101
125
  // CLI mode
102
126
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
103
127
  const args = process.argv.slice(2);
@@ -0,0 +1,136 @@
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|devops|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
+ # ─── Validate agent_type ────────────────────────────────────────────────────
53
+ case "${AGENT_TYPE}" in
54
+ developer|architect|qa|reporter|devops|cleargate-wiki-contradict)
55
+ ;;
56
+ *)
57
+ printf '[%s] error: invalid agent_type: %s\n' "$(date -u +%FT%TZ)" "${AGENT_TYPE}" >> "${LOG}"
58
+ printf 'error: invalid agent_type: %s (expected developer|architect|qa|reporter|devops|cleargate-wiki-contradict)\n' "${AGENT_TYPE}" >&2
59
+ exit 3
60
+ ;;
61
+ esac
62
+
63
+ # ─── Resolve active sprint ──────────────────────────────────────────────────
64
+ ACTIVE_SENTINEL="${REPO_ROOT}/.cleargate/sprint-runs/.active"
65
+ if [[ ! -f "${ACTIVE_SENTINEL}" ]]; then
66
+ printf '[%s] error: no .active sentinel at %s\n' "$(date -u +%FT%TZ)" "${ACTIVE_SENTINEL}" >> "${LOG}"
67
+ printf 'error: no active sprint sentinel at %s\n' "${ACTIVE_SENTINEL}" >&2
68
+ exit 2
69
+ fi
70
+
71
+ SPRINT_ID="$(tr -d '[:space:]' < "${ACTIVE_SENTINEL}")"
72
+ if [[ -z "${SPRINT_ID}" ]]; then
73
+ printf '[%s] error: .active sentinel is empty\n' "$(date -u +%FT%TZ)" >> "${LOG}"
74
+ printf 'error: .active sentinel is empty\n' >&2
75
+ exit 2
76
+ fi
77
+
78
+ SPRINT_DIR="${REPO_ROOT}/.cleargate/sprint-runs/${SPRINT_ID}"
79
+ mkdir -p "${SPRINT_DIR}"
80
+
81
+ # ─── Resolve session_id ─────────────────────────────────────────────────────
82
+ SESSION_ID="${CLAUDE_SESSION_ID:-}"
83
+
84
+ if [[ -z "${SESSION_ID}" ]]; then
85
+ # Fall back to scanning the most recent transcript filename (UUID) under
86
+ # ~/.claude/projects/-*-ClearGate/ pattern.
87
+ TRANSCRIPT_DIR="${HOME}/.claude/projects"
88
+ if [[ -d "${TRANSCRIPT_DIR}" ]]; then
89
+ SESSION_ID="$(find "${TRANSCRIPT_DIR}" -maxdepth 2 -name '*.jsonl' 2>/dev/null \
90
+ | sort -t '/' -k1 2>/dev/null | tail -1 \
91
+ | xargs -I{} basename {} .jsonl 2>/dev/null || true)"
92
+ fi
93
+ fi
94
+
95
+ if [[ -z "${SESSION_ID}" ]]; then
96
+ # Final fallback: generate a pseudo-id from timestamp
97
+ SESSION_ID="fallback-$(date -u +%s)"
98
+ printf '[%s] warn: no CLAUDE_SESSION_ID and no transcript found; using %s\n' "$(date -u +%FT%TZ)" "${SESSION_ID}" >> "${LOG}"
99
+ fi
100
+
101
+ # ─── Resolve cleargate version ───────────────────────────────────────────────
102
+ # Read from cleargate-cli/package.json if available; otherwise use "unknown"
103
+ PKG_JSON="${REPO_ROOT}/cleargate-cli/package.json"
104
+ CG_VERSION="unknown"
105
+ if [[ -f "${PKG_JSON}" ]]; then
106
+ CG_VERSION="$(jq -r '.version // "unknown"' "${PKG_JSON}" 2>/dev/null || echo "unknown")"
107
+ fi
108
+
109
+ # ─── Write dispatch file atomically ─────────────────────────────────────────
110
+ DISPATCH_TARGET="${SPRINT_DIR}/.dispatch-${SESSION_ID}.json"
111
+ SPAWNED_AT="$(date -u +%FT%TZ)"
112
+
113
+ DISPATCH_JSON="$(jq -cn \
114
+ --arg work_item_id "${WORK_ITEM_ID}" \
115
+ --arg agent_type "${AGENT_TYPE}" \
116
+ --arg spawned_at "${SPAWNED_AT}" \
117
+ --arg session_id "${SESSION_ID}" \
118
+ --arg writer "write_dispatch.sh@cleargate-${CG_VERSION}" \
119
+ '{
120
+ work_item_id: $work_item_id,
121
+ agent_type: $agent_type,
122
+ spawned_at: $spawned_at,
123
+ session_id: $session_id,
124
+ writer: $writer
125
+ }')"
126
+
127
+ # Atomic write via mktemp + mv (rename is atomic on POSIX same-fs)
128
+ TMP="$(mktemp "${SPRINT_DIR}/.dispatch-tmp-XXXXXX")"
129
+ printf '%s\n' "${DISPATCH_JSON}" > "${TMP}"
130
+ mv "${TMP}" "${DISPATCH_TARGET}"
131
+
132
+ printf '[%s] wrote dispatch: sprint=%s session=%s work_item=%s agent=%s\n' \
133
+ "${SPAWNED_AT}" "${SPRINT_ID}" "${SESSION_ID}" "${WORK_ITEM_ID}" "${AGENT_TYPE}" >> "${LOG}"
134
+
135
+ printf '%s\n' "${DISPATCH_TARGET}"
136
+ exit 0