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.
- package/CHANGELOG.md +16 -0
- package/dist/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs +0 -1
- package/dist/admin-api/index.js +1 -2
- package/dist/auth/factory.cjs +0 -1
- package/dist/auth/factory.js +2 -3
- package/dist/auth/require-token.cjs +0 -1
- package/dist/auth/require-token.js +1 -2
- package/dist/auth/token-store.cjs +0 -1
- package/dist/auth/token-store.js +1 -2
- package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
- package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
- package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
- package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
- package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
- package/dist/cli.cjs +1564 -1414
- package/dist/cli.js +1514 -1364
- package/dist/lib/ledger.cjs +0 -1
- package/dist/lib/ledger.js +1 -2
- package/dist/lib/lifecycle-reconcile.cjs +0 -1
- package/dist/lib/lifecycle-reconcile.js +2 -3
- package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
- package/package.json +4 -3
- package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
- package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
- package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
- package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
- package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +27 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
- package/templates/cleargate-planning/MANIFEST.json +71 -15
- package/dist/admin-api/index.cjs.map +0 -1
- package/dist/admin-api/index.js.map +0 -1
- package/dist/auth/factory.cjs.map +0 -1
- package/dist/auth/factory.js.map +0 -1
- package/dist/auth/require-token.cjs.map +0 -1
- package/dist/auth/require-token.js.map +0 -1
- package/dist/auth/token-store.cjs.map +0 -1
- package/dist/auth/token-store.js.map +0 -1
- package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
- package/dist/chunk-5DI2Z3C2.js.map +0 -1
- package/dist/chunk-BTSZOEWC.js.map +0 -1
- package/dist/chunk-E3X7IE5E.js.map +0 -1
- package/dist/chunk-PDE37WFQ.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/lib/ledger.cjs.map +0 -1
- package/dist/lib/ledger.js.map +0 -1
- package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
- package/dist/lib/lifecycle-reconcile.js.map +0 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
- package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
- package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
- package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
- package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
- package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
- package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
- package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
- package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
- package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
- package/dist/templates/synthesis/active-sprint.md +0 -30
- package/dist/templates/synthesis/open-gates.md +0 -38
- package/dist/templates/synthesis/product-state.md +0 -31
- package/dist/templates/synthesis/roadmap.md +0 -63
- 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
|
-
}
|