cleargate 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/MANIFEST.json +71 -15
  3. package/dist/admin-api/index.cjs +0 -1
  4. package/dist/admin-api/index.js +1 -2
  5. package/dist/auth/factory.cjs +0 -1
  6. package/dist/auth/factory.js +2 -3
  7. package/dist/auth/require-token.cjs +0 -1
  8. package/dist/auth/require-token.js +1 -2
  9. package/dist/auth/token-store.cjs +0 -1
  10. package/dist/auth/token-store.js +1 -2
  11. package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
  12. package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
  13. package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
  14. package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
  15. package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
  16. package/dist/cli.cjs +1564 -1414
  17. package/dist/cli.js +1514 -1364
  18. package/dist/lib/ledger.cjs +0 -1
  19. package/dist/lib/ledger.js +1 -2
  20. package/dist/lib/lifecycle-reconcile.cjs +0 -1
  21. package/dist/lib/lifecycle-reconcile.js +2 -3
  22. package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
  23. package/package.json +4 -3
  24. package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
  25. package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
  26. package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
  27. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
  28. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
  29. package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
  30. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
  31. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
  32. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
  33. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
  34. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
  35. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
  36. package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
  37. package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
  38. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
  39. package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
  40. package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
  41. package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
  42. package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
  44. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
  45. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +27 -0
  46. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
  47. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
  48. package/templates/cleargate-planning/MANIFEST.json +71 -15
  49. package/dist/admin-api/index.cjs.map +0 -1
  50. package/dist/admin-api/index.js.map +0 -1
  51. package/dist/auth/factory.cjs.map +0 -1
  52. package/dist/auth/factory.js.map +0 -1
  53. package/dist/auth/require-token.cjs.map +0 -1
  54. package/dist/auth/require-token.js.map +0 -1
  55. package/dist/auth/token-store.cjs.map +0 -1
  56. package/dist/auth/token-store.js.map +0 -1
  57. package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
  58. package/dist/chunk-5DI2Z3C2.js.map +0 -1
  59. package/dist/chunk-BTSZOEWC.js.map +0 -1
  60. package/dist/chunk-E3X7IE5E.js.map +0 -1
  61. package/dist/chunk-PDE37WFQ.js.map +0 -1
  62. package/dist/cli.cjs.map +0 -1
  63. package/dist/cli.js.map +0 -1
  64. package/dist/lib/ledger.cjs.map +0 -1
  65. package/dist/lib/ledger.js.map +0 -1
  66. package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
  67. package/dist/lib/lifecycle-reconcile.js.map +0 -1
  68. package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
  69. package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
  70. package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
  71. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
  72. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
  73. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
  74. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
  75. package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
  76. package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
  77. package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
  78. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
  79. package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
  80. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
  81. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
  82. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
  83. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
  84. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
  85. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
  86. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
  87. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
  88. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
  89. package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
  90. package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
  91. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
  92. package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
  93. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
  94. package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
  95. package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
  96. package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
  97. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
  98. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
  99. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
  100. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
  101. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
  102. package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
  103. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
  104. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
  105. package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
  106. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
  107. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
  108. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
  109. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
  110. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
  111. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
  112. package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
  113. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
  114. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
  115. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
  116. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
  117. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
  118. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
  119. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
  120. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
  121. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
  122. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
  123. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
  124. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
  125. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
  126. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
  127. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
  128. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
  129. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
  130. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
  131. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
  132. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
  133. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
  134. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
  135. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
  136. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
  137. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
  138. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
  139. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
  140. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
  141. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
  142. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
  143. package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
  144. package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
  145. package/dist/templates/synthesis/active-sprint.md +0 -30
  146. package/dist/templates/synthesis/open-gates.md +0 -38
  147. package/dist/templates/synthesis/product-state.md +0 -31
  148. package/dist/templates/synthesis/roadmap.md +0 -63
  149. package/dist/whoami-EANGN46Z.js.map +0 -1
@@ -1,246 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * update_state.mjs — Atomic state/counter update for a story in state.json
4
- *
5
- * Usage:
6
- * node update_state.mjs <STORY-ID> <new-state> — transition to a new state
7
- * node update_state.mjs <STORY-ID> --qa-bounce — increment qa_bounces counter
8
- * node update_state.mjs <STORY-ID> --arch-bounce — increment arch_bounces counter
9
- * node update_state.mjs <STORY-ID> --lane <standard|fast> — set lane for a story
10
- * node update_state.mjs <STORY-ID> --lane-demote <reason> — demote story from fast lane
11
- *
12
- * Atomic write: write to .tmp.<pid> file, then rename to final path.
13
- * Idempotent: if new state equals current (for state transitions) and
14
- * no counter change, exit 0 without rewriting the file.
15
- *
16
- * Auto-escalation: when qa_bounces or arch_bounces reaches BOUNCE_CAP (3),
17
- * state is automatically set to "Escalated".
18
- *
19
- * Migration: reads v1 state.json transparently; upgrades to v2 on first touch
20
- * (injects lane fields with defaults; emits one stderr log line).
21
- */
22
-
23
- import fs from 'node:fs';
24
- import path from 'node:path';
25
- import { fileURLToPath } from 'node:url';
26
- import { SCHEMA_VERSION, VALID_STATES, TERMINAL_STATES, BOUNCE_CAP } from './constants.mjs';
27
- import { validateState, validateShapeIgnoringVersion } from './validate_state.mjs';
28
- import { migrateStateToV3 } from './_migrate-schema-v3.mjs';
29
-
30
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
32
-
33
- function usage() {
34
- process.stderr.write(
35
- 'Usage:\n' +
36
- ' node update_state.mjs <STORY-ID> <new-state>\n' +
37
- ' node update_state.mjs <STORY-ID> --qa-bounce\n' +
38
- ' node update_state.mjs <STORY-ID> --arch-bounce\n' +
39
- ' node update_state.mjs <STORY-ID> --lane <standard|fast>\n' +
40
- ' node update_state.mjs <STORY-ID> --lane-demote <reason>\n'
41
- );
42
- process.exit(2);
43
- }
44
-
45
- /**
46
- * Migrate a v1 state.json to v2 by injecting lane fields with defaults.
47
- * Mutates the state object in-place and returns it.
48
- * Emits a single stderr log line describing the migration.
49
- * @param {object} state - Parsed v1 state object
50
- * @returns {object} - The mutated (now v2) state object
51
- */
52
- export function migrateV1ToV2(state) {
53
- state.schema_version = 2;
54
- const storyIds = Object.keys(state.stories || {});
55
- for (const id of storyIds) {
56
- const story = state.stories[id];
57
- if (story.lane == null) story.lane = 'standard';
58
- if (story.lane_assigned_by == null) story.lane_assigned_by = 'migration-default';
59
- if (story.lane_demoted_at === undefined) story.lane_demoted_at = null;
60
- if (story.lane_demotion_reason === undefined) story.lane_demotion_reason = null;
61
- }
62
- process.stderr.write(
63
- `migration: schema_version 1 → 2 for sprint ${state.sprint_id} (${storyIds.length} stories defaulted to lane: standard)\n`
64
- );
65
- return state;
66
- }
67
-
68
- function resolveStateFile() {
69
- const envFile = process.env.CLEARGATE_STATE_FILE;
70
- if (envFile) return path.resolve(envFile);
71
- throw new Error(
72
- 'CLEARGATE_STATE_FILE env var not set; cannot resolve state.json'
73
- );
74
- }
75
-
76
- function atomicWrite(stateFile, state) {
77
- const tmpFile = `${stateFile}.tmp.${process.pid}`;
78
- fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2) + '\n', 'utf8');
79
- fs.renameSync(tmpFile, stateFile);
80
- }
81
-
82
- function main() {
83
- const args = process.argv.slice(2);
84
-
85
- if (args.length < 2) usage();
86
-
87
- const storyId = args[0];
88
- const action = args[1];
89
-
90
- const stateFile = resolveStateFile();
91
-
92
- if (!fs.existsSync(stateFile)) {
93
- process.stderr.write(`Error: state.json not found at ${stateFile}\n`);
94
- process.exit(1);
95
- }
96
-
97
- let state;
98
- try {
99
- state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
100
- } catch (err) {
101
- process.stderr.write(`Error: failed to parse state.json: ${err.message}\n`);
102
- process.exit(1);
103
- }
104
-
105
- // Pre-migration: validate shape (ignoring version) before potentially migrating
106
- const preCheck = validateShapeIgnoringVersion(state);
107
- if (!preCheck.valid) {
108
- process.stderr.write(`Error: state.json is invalid:\n`);
109
- for (const e of preCheck.errors) process.stderr.write(` - ${e}\n`);
110
- process.exit(1);
111
- }
112
-
113
- // Migrate v1 → v2 if needed; write atomically so subsequent reads see v2
114
- if (state.schema_version === 1) {
115
- state = migrateV1ToV2(state);
116
- atomicWrite(stateFile, state);
117
- }
118
-
119
- // Migrate v2 → v3: strip execution_mode (STORY-070-01)
120
- const { changed: v3Changed } = migrateStateToV3(state, stateFile);
121
- if (v3Changed) {
122
- atomicWrite(stateFile, state);
123
- }
124
-
125
- // Post-migration strict validation
126
- const { valid, errors } = validateState(state);
127
- if (!valid) {
128
- process.stderr.write(`Error: state.json is invalid after migration:\n`);
129
- for (const e of errors) process.stderr.write(` - ${e}\n`);
130
- process.exit(1);
131
- }
132
-
133
- if (!state.stories[storyId]) {
134
- process.stderr.write(`Error: story ${storyId} not found in state.json\n`);
135
- process.exit(1);
136
- }
137
-
138
- const story = state.stories[storyId];
139
-
140
- if (action === '--lane') {
141
- const laneValue = args[2];
142
- if (!laneValue || !['standard', 'fast'].includes(laneValue)) {
143
- process.stderr.write(
144
- `Error: --lane requires a value of "standard" or "fast"\n`
145
- );
146
- process.exit(2);
147
- }
148
- // TODO(STORY-022-04): cross-read sprint plan to enforce rubric §6 contradiction check
149
- // (expected_bounce_exposure: med|high + lane: fast is a contradiction per PROPOSAL-013 §2.3 #6)
150
- story.lane = laneValue;
151
- story.lane_assigned_by = 'human-override';
152
- story.updated_at = new Date().toISOString();
153
- state.last_action = `lane-set ${storyId}: lane=${laneValue} (human-override)`;
154
- state.updated_at = story.updated_at;
155
- atomicWrite(stateFile, state);
156
- process.stdout.write(
157
- `Updated ${storyId}: lane="${laneValue}", lane_assigned_by="human-override"\n`
158
- );
159
-
160
- } else if (action === '--lane-demote') {
161
- const reason = args[2];
162
- if (!reason) {
163
- process.stderr.write(
164
- `Error: --lane-demote requires a reason string\n`
165
- );
166
- process.exit(2);
167
- }
168
- story.lane = 'standard';
169
- story.lane_demoted_at = new Date().toISOString();
170
- story.lane_demotion_reason = reason;
171
- story.qa_bounces = 0;
172
- story.arch_bounces = 0;
173
- story.updated_at = story.lane_demoted_at;
174
- state.last_action = `lane-demote ${storyId}: "${reason}"`;
175
- state.updated_at = story.updated_at;
176
- atomicWrite(stateFile, state);
177
- process.stdout.write(
178
- `Updated ${storyId}: lane="standard", lane_demoted_at="${story.lane_demoted_at}", qa_bounces=0, arch_bounces=0\n`
179
- );
180
-
181
- } else if (action === '--qa-bounce') {
182
- if (story.state === 'Escalated') {
183
- process.stderr.write(`Error: story ${storyId} is already Escalated\n`);
184
- process.exit(1);
185
- }
186
- story.qa_bounces += 1;
187
- if (story.qa_bounces >= BOUNCE_CAP) {
188
- story.state = 'Escalated';
189
- }
190
- story.updated_at = new Date().toISOString();
191
- state.last_action = `qa-bounce ${storyId}: qa_bounces=${story.qa_bounces}`;
192
- state.updated_at = story.updated_at;
193
- atomicWrite(stateFile, state);
194
- process.stdout.write(
195
- `Updated ${storyId}: qa_bounces=${story.qa_bounces}, state=${story.state}\n`
196
- );
197
-
198
- } else if (action === '--arch-bounce') {
199
- if (story.state === 'Escalated') {
200
- process.stderr.write(`Error: story ${storyId} is already Escalated\n`);
201
- process.exit(1);
202
- }
203
- story.arch_bounces += 1;
204
- if (story.arch_bounces >= BOUNCE_CAP) {
205
- story.state = 'Escalated';
206
- }
207
- story.updated_at = new Date().toISOString();
208
- state.last_action = `arch-bounce ${storyId}: arch_bounces=${story.arch_bounces}`;
209
- state.updated_at = story.updated_at;
210
- atomicWrite(stateFile, state);
211
- process.stdout.write(
212
- `Updated ${storyId}: arch_bounces=${story.arch_bounces}, state=${story.state}\n`
213
- );
214
-
215
- } else {
216
- // State transition
217
- const newState = action;
218
-
219
- if (!VALID_STATES.includes(newState)) {
220
- process.stderr.write(
221
- `Error: invalid state "${newState}"; valid states: ${VALID_STATES.join(', ')}\n`
222
- );
223
- process.exit(1);
224
- }
225
-
226
- // Idempotency: if state is already the target, no-op
227
- if (story.state === newState) {
228
- process.stdout.write(`No-op: ${storyId} is already in state "${newState}"\n`);
229
- process.exit(0);
230
- }
231
-
232
- // Reset worktree to null on Done
233
- if (newState === 'Done') {
234
- story.worktree = null;
235
- }
236
-
237
- story.state = newState;
238
- story.updated_at = new Date().toISOString();
239
- state.last_action = `transition ${storyId} → ${newState}`;
240
- state.updated_at = story.updated_at;
241
- atomicWrite(stateFile, state);
242
- process.stdout.write(`Updated ${storyId}: state="${newState}"\n`);
243
- }
244
- }
245
-
246
- main();
@@ -1,111 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * validate_bounce_readiness.mjs — Pre-bounce gate check for a story
4
- *
5
- * Usage: node validate_bounce_readiness.mjs <STORY-ID>
6
- *
7
- * Checks:
8
- * (a) state.json exists and is valid
9
- * (b) story is present in state.json
10
- * (c) story state is "Ready to Bounce"
11
- * (d) git working tree is clean (git status --porcelain returns empty)
12
- *
13
- * Exits non-zero on any failure, with detail on stderr.
14
- * Exits 0 if all checks pass.
15
- */
16
-
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
- import { fileURLToPath } from 'node:url';
20
- import { execSync } from 'node:child_process';
21
- import { validateState } from './validate_state.mjs';
22
-
23
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
25
-
26
- function usage() {
27
- process.stderr.write('Usage: node validate_bounce_readiness.mjs <STORY-ID>\n');
28
- process.exit(2);
29
- }
30
-
31
- function main() {
32
- const args = process.argv.slice(2);
33
- if (args.length < 1 || args[0].startsWith('--')) usage();
34
-
35
- const storyId = args[0];
36
-
37
- const stateFile = process.env.CLEARGATE_STATE_FILE
38
- ? path.resolve(process.env.CLEARGATE_STATE_FILE)
39
- : null;
40
-
41
- if (!stateFile) {
42
- process.stderr.write(
43
- 'Error: CLEARGATE_STATE_FILE env var not set; cannot resolve state.json\n'
44
- );
45
- process.exit(1);
46
- }
47
-
48
- // (a) state.json exists
49
- if (!fs.existsSync(stateFile)) {
50
- process.stderr.write(`Error: state.json not found at ${stateFile}\n`);
51
- process.exit(1);
52
- }
53
-
54
- let state;
55
- try {
56
- state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
57
- } catch (err) {
58
- process.stderr.write(`Error: failed to parse state.json: ${err.message}\n`);
59
- process.exit(1);
60
- }
61
-
62
- // (a) schema valid
63
- const { valid, errors } = validateState(state);
64
- if (!valid) {
65
- process.stderr.write('Error: state.json is invalid:\n');
66
- for (const e of errors) process.stderr.write(` - ${e}\n`);
67
- process.exit(1);
68
- }
69
-
70
- // (b) story present
71
- if (!state.stories[storyId]) {
72
- process.stderr.write(`Error: story ${storyId} not found in state.json\n`);
73
- process.exit(1);
74
- }
75
-
76
- const story = state.stories[storyId];
77
-
78
- // (c) state is "Ready to Bounce"
79
- if (story.state !== 'Ready to Bounce') {
80
- process.stderr.write(
81
- `Error: story ${storyId} state is "${story.state}", expected "Ready to Bounce"\n`
82
- );
83
- process.exit(1);
84
- }
85
-
86
- // (d) git working tree is clean
87
- let gitOutput;
88
- try {
89
- gitOutput = execSync('git status --porcelain', {
90
- cwd: REPO_ROOT,
91
- encoding: 'utf8',
92
- });
93
- } catch (err) {
94
- process.stderr.write(`Error: failed to run git status: ${err.message}\n`);
95
- process.exit(1);
96
- }
97
-
98
- if (gitOutput.trim().length > 0) {
99
- process.stderr.write(
100
- `Error: git working tree is dirty. Uncommitted changes:\n${gitOutput}`
101
- );
102
- process.exit(1);
103
- }
104
-
105
- process.stdout.write(
106
- `Bounce readiness check passed for ${storyId} (state="Ready to Bounce", clean tree)\n`
107
- );
108
- process.exit(0);
109
- }
110
-
111
- main();
@@ -1,184 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * validate_state.mjs — Validate state.json schema and invariants
4
- *
5
- * Usage: node validate_state.mjs [--state-file <path>]
6
- *
7
- * Reads .cleargate/sprint-runs/<sprint-id>/state.json (or a specified path),
8
- * confirms schema version, and reports invariant violations.
9
- *
10
- * Exports validateState(state) for use by other scripts.
11
- */
12
-
13
- import fs from 'node:fs';
14
- import path from 'node:path';
15
- import { fileURLToPath } from 'node:url';
16
- import { SCHEMA_VERSION, VALID_STATES, BOUNCE_CAP } from './constants.mjs';
17
-
18
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
20
-
21
- /**
22
- * Validate a parsed state object, ignoring schema_version check.
23
- * Used PRE-MIGRATION so that v1 files pass shape validation before being upgraded.
24
- * @param {object} state - Parsed state.json content
25
- * @returns {{ valid: boolean, errors: string[] }}
26
- */
27
- export function validateShapeIgnoringVersion(state) {
28
- const errors = [];
29
-
30
- if (typeof state !== 'object' || state === null) {
31
- errors.push('state is not an object');
32
- return { valid: false, errors };
33
- }
34
-
35
- if (!state.sprint_id) {
36
- errors.push('missing required field: sprint_id');
37
- }
38
-
39
- if (!state.sprint_status) {
40
- errors.push('missing required field: sprint_status');
41
- }
42
-
43
- if (typeof state.stories !== 'object' || state.stories === null) {
44
- errors.push('stories field must be an object');
45
- return { valid: false, errors };
46
- }
47
-
48
- for (const [storyId, story] of Object.entries(state.stories)) {
49
- if (typeof story !== 'object' || story === null) {
50
- errors.push(`story ${storyId}: not an object`);
51
- continue;
52
- }
53
-
54
- if (!VALID_STATES.includes(story.state)) {
55
- errors.push(
56
- `story ${storyId}: invalid state "${story.state}"; expected one of: ${VALID_STATES.join(', ')}`
57
- );
58
- }
59
-
60
- if (typeof story.qa_bounces !== 'number') {
61
- errors.push(`story ${storyId}: qa_bounces must be a number`);
62
- } else if (story.qa_bounces > BOUNCE_CAP) {
63
- errors.push(
64
- `invariant violation: story ${storyId} qa_bounces=${story.qa_bounces} exceeds BOUNCE_CAP (${BOUNCE_CAP})`
65
- );
66
- } else if (story.qa_bounces < 0) {
67
- errors.push(`story ${storyId}: qa_bounces must be >= 0`);
68
- }
69
-
70
- if (typeof story.arch_bounces !== 'number') {
71
- errors.push(`story ${storyId}: arch_bounces must be a number`);
72
- } else if (story.arch_bounces > BOUNCE_CAP) {
73
- errors.push(
74
- `invariant violation: story ${storyId} arch_bounces=${story.arch_bounces} exceeds BOUNCE_CAP (${BOUNCE_CAP})`
75
- );
76
- } else if (story.arch_bounces < 0) {
77
- errors.push(`story ${storyId}: arch_bounces must be >= 0`);
78
- }
79
-
80
- if (!story.updated_at) {
81
- errors.push(`story ${storyId}: missing required field: updated_at`);
82
- }
83
- }
84
-
85
- if (!state.updated_at) {
86
- errors.push('missing required top-level field: updated_at');
87
- }
88
-
89
- return { valid: errors.length === 0, errors };
90
- }
91
-
92
- /**
93
- * Validate a parsed state object (strict — includes schema_version check).
94
- * Call this AFTER migration to assert the file is fully v2-compliant.
95
- * @param {object} state - Parsed state.json content
96
- * @returns {{ valid: boolean, errors: string[] }}
97
- */
98
- export function validateState(state) {
99
- const errors = [];
100
-
101
- if (typeof state !== 'object' || state === null) {
102
- errors.push('state is not an object');
103
- return { valid: false, errors };
104
- }
105
-
106
- if (state.schema_version !== SCHEMA_VERSION) {
107
- errors.push(
108
- `schema_version mismatch: expected ${SCHEMA_VERSION}, got ${state.schema_version}`
109
- );
110
- }
111
-
112
- // Delegate shape validation (everything except version check)
113
- const shapeResult = validateShapeIgnoringVersion(state);
114
- for (const e of shapeResult.errors) {
115
- errors.push(e);
116
- }
117
-
118
- return { valid: errors.length === 0, errors };
119
- }
120
-
121
- // CLI mode
122
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
123
- const args = process.argv.slice(2);
124
- const fileIdx = args.indexOf('--state-file');
125
- let stateFile;
126
-
127
- if (fileIdx !== -1 && args[fileIdx + 1]) {
128
- stateFile = path.resolve(args[fileIdx + 1]);
129
- } else {
130
- // Attempt to discover via environment or fallback
131
- const envStateFile = process.env.CLEARGATE_STATE_FILE;
132
- if (envStateFile) {
133
- stateFile = path.resolve(envStateFile);
134
- } else {
135
- // Look for state.json in sprint-runs/
136
- const sprintRunsDir = path.join(REPO_ROOT, '.cleargate', 'sprint-runs');
137
- if (!fs.existsSync(sprintRunsDir)) {
138
- process.stderr.write(`Error: sprint-runs directory not found at ${sprintRunsDir}\n`);
139
- process.exit(1);
140
- }
141
- const entries = fs.readdirSync(sprintRunsDir);
142
- const found = entries
143
- .map((e) => path.join(sprintRunsDir, e, 'state.json'))
144
- .filter((p) => fs.existsSync(p));
145
- if (found.length === 0) {
146
- process.stderr.write('Error: no state.json found in sprint-runs/\n');
147
- process.exit(1);
148
- }
149
- if (found.length > 1) {
150
- process.stderr.write(
151
- `Multiple state.json files found; specify --state-file:\n${found.join('\n')}\n`
152
- );
153
- process.exit(1);
154
- }
155
- stateFile = found[0];
156
- }
157
- }
158
-
159
- if (!fs.existsSync(stateFile)) {
160
- process.stderr.write(`Error: state.json not found at ${stateFile}\n`);
161
- process.exit(1);
162
- }
163
-
164
- let state;
165
- try {
166
- state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
167
- } catch (err) {
168
- process.stderr.write(`Error: failed to parse state.json: ${err.message}\n`);
169
- process.exit(1);
170
- }
171
-
172
- const { valid, errors } = validateState(state);
173
-
174
- if (valid) {
175
- process.stdout.write(`state.json at ${stateFile} is valid (schema_version=${state.schema_version})\n`);
176
- process.exit(0);
177
- } else {
178
- process.stderr.write(`Validation failed for ${stateFile}:\n`);
179
- for (const err of errors) {
180
- process.stderr.write(` - ${err}\n`);
181
- }
182
- process.exit(1);
183
- }
184
- }