cleargate 0.14.0 → 0.15.1

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 (150) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/MANIFEST.json +72 -16
  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-synth.md +2 -0
  25. package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
  26. package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
  27. package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
  28. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
  29. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
  30. package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
  31. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
  32. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
  33. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
  34. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
  35. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
  36. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
  37. package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
  38. package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
  39. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
  40. package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
  41. package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
  42. package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
  44. package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
  45. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
  46. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +222 -0
  47. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
  48. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
  49. package/templates/cleargate-planning/MANIFEST.json +72 -16
  50. package/dist/admin-api/index.cjs.map +0 -1
  51. package/dist/admin-api/index.js.map +0 -1
  52. package/dist/auth/factory.cjs.map +0 -1
  53. package/dist/auth/factory.js.map +0 -1
  54. package/dist/auth/require-token.cjs.map +0 -1
  55. package/dist/auth/require-token.js.map +0 -1
  56. package/dist/auth/token-store.cjs.map +0 -1
  57. package/dist/auth/token-store.js.map +0 -1
  58. package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
  59. package/dist/chunk-5DI2Z3C2.js.map +0 -1
  60. package/dist/chunk-BTSZOEWC.js.map +0 -1
  61. package/dist/chunk-E3X7IE5E.js.map +0 -1
  62. package/dist/chunk-PDE37WFQ.js.map +0 -1
  63. package/dist/cli.cjs.map +0 -1
  64. package/dist/cli.js.map +0 -1
  65. package/dist/lib/ledger.cjs.map +0 -1
  66. package/dist/lib/ledger.js.map +0 -1
  67. package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
  68. package/dist/lib/lifecycle-reconcile.js.map +0 -1
  69. package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
  70. package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
  71. package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
  72. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
  73. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
  74. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
  75. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
  76. package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
  77. package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
  78. package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
  79. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
  80. package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
  81. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
  82. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
  83. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
  84. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
  85. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
  86. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
  87. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
  88. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
  89. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
  90. package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
  91. package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
  92. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
  93. package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
  94. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
  95. package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
  96. package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
  97. package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
  98. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
  99. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
  100. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
  101. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
  102. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
  103. package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
  104. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
  105. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
  106. package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
  107. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
  108. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
  109. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
  110. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
  111. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
  112. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
  113. package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
  114. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
  115. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
  116. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
  117. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
  118. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
  119. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
  120. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
  121. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
  122. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
  123. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
  124. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
  125. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
  126. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
  127. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
  128. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
  129. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
  130. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
  131. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
  132. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
  133. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
  134. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
  135. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
  136. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
  137. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
  138. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
  139. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
  140. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
  141. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
  142. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
  143. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
  144. package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
  145. package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
  146. package/dist/templates/synthesis/active-sprint.md +0 -30
  147. package/dist/templates/synthesis/open-gates.md +0 -38
  148. package/dist/templates/synthesis/product-state.md +0 -31
  149. package/dist/templates/synthesis/roadmap.md +0 -63
  150. 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
- }