cleargate 0.2.0 → 0.3.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/LICENSE +21 -0
- package/dist/MANIFEST.json +59 -17
- package/dist/admin-api/index.cjs +88 -1
- package/dist/admin-api/index.cjs.map +1 -1
- package/dist/admin-api/index.d.cts +105 -1
- package/dist/admin-api/index.d.ts +105 -1
- package/dist/admin-api/index.js +77 -1
- package/dist/admin-api/index.js.map +1 -1
- package/dist/bootstrap-root-FGWDICDT.js +130 -0
- package/dist/bootstrap-root-FGWDICDT.js.map +1 -0
- package/dist/chunk-OM4FAEA7.js +184 -0
- package/dist/chunk-OM4FAEA7.js.map +1 -0
- package/dist/cli.cjs +7995 -3984
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4062 -561
- package/dist/cli.js.map +1 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +72 -0
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +45 -3
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +7 -3
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +72 -75
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
- package/dist/templates/cleargate-planning/.claude/settings.json +11 -0
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +31 -12
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +2 -1
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +29 -0
- package/dist/templates/cleargate-planning/CLAUDE.md +3 -0
- package/dist/templates/cleargate-planning/MANIFEST.json +59 -17
- package/dist/whoami-CX7CXJD5.js +76 -0
- package/dist/whoami-CX7CXJD5.js.map +1 -0
- package/package.json +6 -2
- package/templates/cleargate-planning/.claude/agents/architect.md +72 -0
- package/templates/cleargate-planning/.claude/agents/developer.md +45 -3
- package/templates/cleargate-planning/.claude/agents/qa.md +7 -3
- package/templates/cleargate-planning/.claude/agents/reporter.md +72 -75
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
- package/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
- package/templates/cleargate-planning/.claude/settings.json +11 -0
- package/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +31 -12
- package/templates/cleargate-planning/.cleargate/FLASHCARD.md +2 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
- package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
- package/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
- package/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
- package/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
- package/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
- package/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
- package/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
- package/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
- package/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
- package/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
- package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
- package/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +29 -0
- package/templates/cleargate-planning/CLAUDE.md +3 -0
- package/templates/cleargate-planning/MANIFEST.json +59 -17
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* close_sprint.mjs — Six-step sprint close pipeline
|
|
4
|
+
*
|
|
5
|
+
* Usage: node close_sprint.mjs <sprint-id> [--assume-ack]
|
|
6
|
+
* node close_sprint.mjs <sprint-id> --report-body-stdin (STORY-014-10)
|
|
7
|
+
*
|
|
8
|
+
* Steps:
|
|
9
|
+
* 1. Load and validate state.json via validateState
|
|
10
|
+
* 2. Refuse if any story state is not in TERMINAL_STATES (exit non-zero, list offenders)
|
|
11
|
+
* 3. Invoke prefill_report.mjs on all agent reports
|
|
12
|
+
* 4. Orchestrator spawns Reporter separately (script validates preconditions only)
|
|
13
|
+
* 5. On Reporter success + user ack (or --assume-ack flag), flip sprint_status -> "Completed"
|
|
14
|
+
* 6. Invoke suggest_improvements.mjs unconditionally
|
|
15
|
+
*
|
|
16
|
+
* Stdin fallback (STORY-014-10): when `--report-body-stdin` is passed, the script
|
|
17
|
+
* reads the full REPORT.md body from stdin and writes it atomically in lieu of
|
|
18
|
+
* waiting for a Reporter-produced file. Replaces the Step-4 gate; implies ack.
|
|
19
|
+
* Refuses empty stdin or pre-existing REPORT.md.
|
|
20
|
+
*
|
|
21
|
+
* Does NOT archive the sprint file (pending-sync -> archive stays human per EPIC-013 §4.5 step 7).
|
|
22
|
+
*
|
|
23
|
+
* Reuse: TERMINAL_STATES, VALID_STATES from constants.mjs
|
|
24
|
+
* validateState from validate_state.mjs
|
|
25
|
+
* atomicWrite pattern from update_state.mjs
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { execSync } from 'node:child_process';
|
|
32
|
+
import { TERMINAL_STATES } from './constants.mjs';
|
|
33
|
+
import { validateState } from './validate_state.mjs';
|
|
34
|
+
|
|
35
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
37
|
+
const SCRIPTS_DIR = __dirname;
|
|
38
|
+
|
|
39
|
+
function usage() {
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
'Usage: node close_sprint.mjs <sprint-id> [--assume-ack | --report-body-stdin]\n' +
|
|
42
|
+
'\n' +
|
|
43
|
+
'Options:\n' +
|
|
44
|
+
' --assume-ack Skip user acknowledgement prompt (for automated tests)\n' +
|
|
45
|
+
' --report-body-stdin Read REPORT.md body from stdin; implies ack (STORY-014-10)\n'
|
|
46
|
+
);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Atomic write using tmp+rename pattern (per M1 update_state.mjs convention).
|
|
52
|
+
* @param {string} filePath
|
|
53
|
+
* @param {object} data
|
|
54
|
+
*/
|
|
55
|
+
function atomicWrite(filePath, data) {
|
|
56
|
+
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
57
|
+
fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
58
|
+
fs.renameSync(tmpFile, filePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Atomic write for a string body. Separate from atomicWrite() so we don't
|
|
63
|
+
* accidentally JSON.stringify a markdown body.
|
|
64
|
+
* @param {string} filePath
|
|
65
|
+
* @param {string} body
|
|
66
|
+
*/
|
|
67
|
+
function atomicWriteString(filePath, body) {
|
|
68
|
+
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
69
|
+
fs.writeFileSync(tmpFile, body, 'utf8');
|
|
70
|
+
fs.renameSync(tmpFile, filePath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Invoke a script via node (for .mjs scripts in the same directory).
|
|
75
|
+
* Throws on non-zero exit.
|
|
76
|
+
* @param {string} scriptName
|
|
77
|
+
* @param {string[]} scriptArgs
|
|
78
|
+
* @param {object} env
|
|
79
|
+
*/
|
|
80
|
+
function invokeScript(scriptName, scriptArgs, env) {
|
|
81
|
+
const scriptPath = path.join(SCRIPTS_DIR, scriptName);
|
|
82
|
+
if (!fs.existsSync(scriptPath)) {
|
|
83
|
+
throw new Error(`Script not found: ${scriptPath}`);
|
|
84
|
+
}
|
|
85
|
+
const argStr = scriptArgs.map(a => JSON.stringify(a)).join(' ');
|
|
86
|
+
const cmd = `node ${JSON.stringify(scriptPath)} ${argStr}`;
|
|
87
|
+
execSync(cmd, {
|
|
88
|
+
stdio: 'inherit',
|
|
89
|
+
env: Object.assign({}, process.env, env || {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function main() {
|
|
94
|
+
const args = process.argv.slice(2);
|
|
95
|
+
|
|
96
|
+
if (args.length < 1) usage();
|
|
97
|
+
|
|
98
|
+
const sprintId = args[0];
|
|
99
|
+
const reportBodyStdin = args.includes('--report-body-stdin');
|
|
100
|
+
const assumeAck = args.includes('--assume-ack') || reportBodyStdin;
|
|
101
|
+
|
|
102
|
+
const sprintDir = process.env.CLEARGATE_SPRINT_DIR
|
|
103
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
|
|
104
|
+
: path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(sprintDir)) {
|
|
107
|
+
process.stderr.write(`Error: sprint directory not found: ${sprintDir}\n`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const stateFile = process.env.CLEARGATE_STATE_FILE
|
|
112
|
+
? path.resolve(process.env.CLEARGATE_STATE_FILE)
|
|
113
|
+
: path.join(sprintDir, 'state.json');
|
|
114
|
+
|
|
115
|
+
// ── Step 1: Load and validate state.json ──────────────────────────────────
|
|
116
|
+
if (!fs.existsSync(stateFile)) {
|
|
117
|
+
process.stderr.write(
|
|
118
|
+
`Error: state.json not found at ${stateFile}\n` +
|
|
119
|
+
`Hint: run init_sprint.mjs ${sprintId} --stories <ids> first\n`
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let state;
|
|
125
|
+
try {
|
|
126
|
+
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
127
|
+
} catch (err) {
|
|
128
|
+
process.stderr.write(`Error: failed to parse state.json: ${err.message}\n`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { valid, errors } = validateState(state);
|
|
133
|
+
if (!valid) {
|
|
134
|
+
process.stderr.write('Error: state.json validation failed:\n');
|
|
135
|
+
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Step 2: Refuse if any story not in TERMINAL_STATES ────────────────────
|
|
140
|
+
const nonTerminal = [];
|
|
141
|
+
for (const [storyId, story] of Object.entries(state.stories || {})) {
|
|
142
|
+
if (!TERMINAL_STATES.includes(story.state)) {
|
|
143
|
+
nonTerminal.push(`${storyId}: ${story.state} — not terminal`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (nonTerminal.length > 0) {
|
|
148
|
+
process.stderr.write('Error: sprint cannot close — non-terminal stories:\n');
|
|
149
|
+
for (const msg of nonTerminal) {
|
|
150
|
+
process.stderr.write(` ${msg}\n`);
|
|
151
|
+
}
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
process.stdout.write(`Step 1-2 passed: all ${Object.keys(state.stories || {}).length} stories are terminal.\n`);
|
|
156
|
+
|
|
157
|
+
// ── Step 3: Invoke prefill_report.mjs ─────────────────────────────────────
|
|
158
|
+
process.stdout.write('Step 3: running prefill_report.mjs...\n');
|
|
159
|
+
try {
|
|
160
|
+
invokeScript('prefill_report.mjs', [sprintId], {
|
|
161
|
+
CLEARGATE_STATE_FILE: stateFile,
|
|
162
|
+
CLEARGATE_SPRINT_DIR: sprintDir,
|
|
163
|
+
});
|
|
164
|
+
} catch (err) {
|
|
165
|
+
process.stderr.write(`Error: prefill_report.mjs failed: ${err.message}\n`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Step 4: Orchestrator spawns Reporter separately ───────────────────────
|
|
170
|
+
// This script only validates preconditions; it does NOT fork the Reporter agent.
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
'Step 4: preconditions satisfied — orchestrator should now spawn the Reporter agent.\n' +
|
|
173
|
+
' The Reporter writes REPORT.md using the sprint_report.md template.\n' +
|
|
174
|
+
` Expected output: ${path.join(sprintDir, 'REPORT.md')}\n`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Check if REPORT.md already exists (e.g., --assume-ack path in tests)
|
|
178
|
+
const reportFile = path.join(sprintDir, 'REPORT.md');
|
|
179
|
+
|
|
180
|
+
// ── Step 4.5 (STORY-014-10): --report-body-stdin fallback ────────────────
|
|
181
|
+
// Orchestrator pipes the Reporter's markdown body here when the Reporter's
|
|
182
|
+
// Write tool is blocked. Refuses empty stdin + pre-existing REPORT.md.
|
|
183
|
+
if (reportBodyStdin) {
|
|
184
|
+
if (fs.existsSync(reportFile)) {
|
|
185
|
+
process.stderr.write(
|
|
186
|
+
`Error: REPORT.md already exists at ${reportFile}\n` +
|
|
187
|
+
'Delete it or skip --report-body-stdin mode to use the primary Reporter-write path.\n'
|
|
188
|
+
);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
let body;
|
|
192
|
+
try {
|
|
193
|
+
body = fs.readFileSync(0, 'utf8');
|
|
194
|
+
} catch (err) {
|
|
195
|
+
process.stderr.write(`Error: failed to read stdin: ${err.message}\n`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
if (!body || body.trim().length === 0) {
|
|
199
|
+
process.stderr.write('Error: empty report body — refusing to write.\n');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
atomicWriteString(reportFile, body);
|
|
203
|
+
process.stdout.write(
|
|
204
|
+
`Step 4.5 (stdin mode): REPORT.md written (${body.length} bytes) at ${reportFile}\n`
|
|
205
|
+
);
|
|
206
|
+
// Fall through to Step 5 + 6 unconditionally — stdin mode implies ack.
|
|
207
|
+
} else if (!assumeAck) {
|
|
208
|
+
if (!fs.existsSync(reportFile)) {
|
|
209
|
+
process.stdout.write(
|
|
210
|
+
'\nWaiting for Reporter to produce REPORT.md...\n' +
|
|
211
|
+
'After Reporter succeeds, re-run with --assume-ack to complete the close.\n'
|
|
212
|
+
);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
// In non-assume-ack mode with existing REPORT.md, prompt user
|
|
216
|
+
process.stdout.write(
|
|
217
|
+
`\nREPORT.md found at ${reportFile}\n` +
|
|
218
|
+
'Review the report, then confirm close by re-running with --assume-ack\n'
|
|
219
|
+
);
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Step 5: Flip sprint_status to "Completed" ────────────────────────────
|
|
224
|
+
process.stdout.write('Step 5: flipping sprint_status to "Completed"...\n');
|
|
225
|
+
const now = new Date().toISOString();
|
|
226
|
+
state.sprint_status = 'Completed';
|
|
227
|
+
state.last_action = `close_sprint: sprint ${sprintId} completed`;
|
|
228
|
+
state.updated_at = now;
|
|
229
|
+
atomicWrite(stateFile, state);
|
|
230
|
+
process.stdout.write(`sprint_status flipped to "Completed" at ${now}\n`);
|
|
231
|
+
|
|
232
|
+
// ── Step 6: Invoke suggest_improvements.mjs unconditionally ───────────────
|
|
233
|
+
process.stdout.write('Step 6: running suggest_improvements.mjs...\n');
|
|
234
|
+
try {
|
|
235
|
+
invokeScript('suggest_improvements.mjs', [sprintId], {
|
|
236
|
+
CLEARGATE_STATE_FILE: stateFile,
|
|
237
|
+
CLEARGATE_SPRINT_DIR: sprintDir,
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
// suggest_improvements failure is non-fatal — log but do not abort
|
|
241
|
+
process.stderr.write(`Warning: suggest_improvements.mjs failed: ${err.message}\n`);
|
|
242
|
+
process.stderr.write('Sprint is still marked Completed; improvement suggestions may be incomplete.\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
process.stdout.write(`\nSprint ${sprintId} close pipeline complete.\n`);
|
|
246
|
+
process.stdout.write(` state.json: sprint_status = Completed\n`);
|
|
247
|
+
process.stdout.write(` improvement-suggestions.md: ${path.join(sprintDir, 'improvement-suggestions.md')}\n`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
main();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClearGate Execution Phase v2 — Constants
|
|
3
|
+
*
|
|
4
|
+
* state.json v1 Schema (LOCKED — any future field change must bump schema_version):
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "schema_version": 1, // integer, mandatory
|
|
8
|
+
* "sprint_id": "S-NN", // string
|
|
9
|
+
* "execution_mode": "v1"|"v2", // string
|
|
10
|
+
* "sprint_status": "Active", // string
|
|
11
|
+
* "stories": {
|
|
12
|
+
* "STORY-NNN-NN": {
|
|
13
|
+
* "state": "Ready to Bounce", // one of VALID_STATES
|
|
14
|
+
* "qa_bounces": 0, // integer 0..BOUNCE_CAP
|
|
15
|
+
* "arch_bounces": 0, // integer 0..BOUNCE_CAP
|
|
16
|
+
* "worktree": null, // string|null — path to worktree checkout
|
|
17
|
+
* "updated_at": "<ISO-8601>", // string
|
|
18
|
+
* "notes": "" // string
|
|
19
|
+
* }
|
|
20
|
+
* },
|
|
21
|
+
* "last_action": "<string>", // human-readable last operation
|
|
22
|
+
* "updated_at": "<ISO-8601>" // string
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const SCHEMA_VERSION = 1;
|
|
27
|
+
|
|
28
|
+
export const BOUNCE_CAP = 3;
|
|
29
|
+
|
|
30
|
+
export const VALID_STATES = [
|
|
31
|
+
'Ready to Bounce',
|
|
32
|
+
'Bouncing',
|
|
33
|
+
'QA Passed',
|
|
34
|
+
'Architect Passed',
|
|
35
|
+
'Sprint Review',
|
|
36
|
+
'Done',
|
|
37
|
+
'Escalated',
|
|
38
|
+
'Parking Lot',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const TERMINAL_STATES = ['Done', 'Escalated', 'Parking Lot'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Canonical state-machine transitions table.
|
|
45
|
+
* Key: current state. Value: array of allowed next states.
|
|
46
|
+
* Terminal states have empty arrays (no transitions out).
|
|
47
|
+
*/
|
|
48
|
+
export const STATE_TRANSITIONS = {
|
|
49
|
+
'Ready to Bounce': ['Bouncing', 'Parking Lot'],
|
|
50
|
+
'Bouncing': ['QA Passed', 'Ready to Bounce', 'Escalated', 'Parking Lot'],
|
|
51
|
+
'QA Passed': ['Architect Passed', 'Ready to Bounce', 'Escalated', 'Parking Lot'],
|
|
52
|
+
'Architect Passed': ['Sprint Review', 'Ready to Bounce', 'Escalated', 'Parking Lot'],
|
|
53
|
+
'Sprint Review': ['Done', 'Ready to Bounce', 'Escalated', 'Parking Lot'],
|
|
54
|
+
'Done': [],
|
|
55
|
+
'Escalated': [],
|
|
56
|
+
'Parking Lot': [],
|
|
57
|
+
};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# file_surface_diff.sh — Parse §3.1 of the active story file and compare
|
|
3
|
+
# declared file paths against `git diff --cached --name-only`.
|
|
4
|
+
#
|
|
5
|
+
# Usage: file_surface_diff.sh [--story-file <path>] [--whitelist <path>] [--v1]
|
|
6
|
+
#
|
|
7
|
+
# Environment:
|
|
8
|
+
# SKIP_SURFACE_GATE=1 — bypass the gate (exit 0 always)
|
|
9
|
+
# CLEARGATE_REPO_ROOT — override repo root (default: CWD git toplevel)
|
|
10
|
+
#
|
|
11
|
+
# Exit codes:
|
|
12
|
+
# 0 — all staged files are on-surface or whitelisted, or v1 mode
|
|
13
|
+
# 1 — off-surface files detected (v2 mode only)
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
# ---- Configuration ---------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
REPO_ROOT="${CLEARGATE_REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
20
|
+
WHITELIST_DEFAULT="${REPO_ROOT}/.cleargate/scripts/surface-whitelist.txt"
|
|
21
|
+
ACTIVE_SENTINEL="${REPO_ROOT}/.cleargate/sprint-runs/.active"
|
|
22
|
+
STATE_JSON_GLOB="${REPO_ROOT}/.cleargate/sprint-runs"
|
|
23
|
+
|
|
24
|
+
# Parse args
|
|
25
|
+
STORY_FILE=""
|
|
26
|
+
WHITELIST_FILE="${WHITELIST_DEFAULT}"
|
|
27
|
+
FORCE_V1=0
|
|
28
|
+
|
|
29
|
+
while [[ $# -gt 0 ]]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--story-file) STORY_FILE="$2"; shift 2 ;;
|
|
32
|
+
--whitelist) WHITELIST_FILE="$2"; shift 2 ;;
|
|
33
|
+
--v1) FORCE_V1=1; shift ;;
|
|
34
|
+
*) shift ;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
# ---- Bypass check ----------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
if [[ "${SKIP_SURFACE_GATE:-0}" == "1" ]]; then
|
|
41
|
+
echo "[surface-gate] SKIP_SURFACE_GATE=1 — bypassing gate" >&2
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# ---- Execution mode --------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
detect_execution_mode() {
|
|
48
|
+
local state_file="${STATE_JSON_GLOB}"
|
|
49
|
+
# Determine active sprint
|
|
50
|
+
local sprint_id=""
|
|
51
|
+
if [[ -f "${ACTIVE_SENTINEL}" ]]; then
|
|
52
|
+
sprint_id="$(tr -d '[:space:]' < "${ACTIVE_SENTINEL}")"
|
|
53
|
+
fi
|
|
54
|
+
if [[ -z "${sprint_id}" ]]; then
|
|
55
|
+
echo "v1"
|
|
56
|
+
return
|
|
57
|
+
fi
|
|
58
|
+
local state_json="${REPO_ROOT}/.cleargate/sprint-runs/${sprint_id}/state.json"
|
|
59
|
+
if [[ -f "${state_json}" ]]; then
|
|
60
|
+
local mode
|
|
61
|
+
mode="$(grep -oE '"execution_mode"\s*:\s*"v[12]"' "${state_json}" 2>/dev/null | grep -oE 'v[12]' | head -1 || true)"
|
|
62
|
+
if [[ -n "${mode}" ]]; then
|
|
63
|
+
echo "${mode}"
|
|
64
|
+
return
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
# Fallback: check sprint file frontmatter
|
|
68
|
+
local sprint_file
|
|
69
|
+
sprint_file="$(ls "${REPO_ROOT}/.cleargate/delivery/pending-sync/SPRINT-${sprint_id}_"*.md 2>/dev/null | head -1 || true)"
|
|
70
|
+
if [[ -z "${sprint_file}" ]]; then
|
|
71
|
+
sprint_file="$(ls "${REPO_ROOT}/.cleargate/delivery/archive/SPRINT-${sprint_id}_"*.md 2>/dev/null | head -1 || true)"
|
|
72
|
+
fi
|
|
73
|
+
if [[ -n "${sprint_file}" ]]; then
|
|
74
|
+
local mode
|
|
75
|
+
mode="$(grep -E '^execution_mode:' "${sprint_file}" 2>/dev/null | grep -oE 'v[12]' | head -1 || true)"
|
|
76
|
+
if [[ -n "${mode}" ]]; then
|
|
77
|
+
echo "${mode}"
|
|
78
|
+
return
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
echo "v1"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if [[ "${FORCE_V1}" == "1" ]]; then
|
|
85
|
+
EXECUTION_MODE="v1"
|
|
86
|
+
else
|
|
87
|
+
EXECUTION_MODE="$(detect_execution_mode)"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ---- Resolve active story file ---------------------------------------------
|
|
91
|
+
|
|
92
|
+
resolve_story_file() {
|
|
93
|
+
local sprint_id=""
|
|
94
|
+
if [[ -f "${ACTIVE_SENTINEL}" ]]; then
|
|
95
|
+
sprint_id="$(tr -d '[:space:]' < "${ACTIVE_SENTINEL}")"
|
|
96
|
+
fi
|
|
97
|
+
if [[ -z "${sprint_id}" ]]; then
|
|
98
|
+
echo ""
|
|
99
|
+
return
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
local state_json="${REPO_ROOT}/.cleargate/sprint-runs/${sprint_id}/state.json"
|
|
103
|
+
if [[ ! -f "${state_json}" ]]; then
|
|
104
|
+
echo ""
|
|
105
|
+
return
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Find first non-terminal story (In Progress or Ready)
|
|
109
|
+
local story_id=""
|
|
110
|
+
story_id="$(python3 -c "
|
|
111
|
+
import json, sys
|
|
112
|
+
data = json.load(open('${state_json}'))
|
|
113
|
+
stories = data.get('stories', {})
|
|
114
|
+
for sid, st in stories.items():
|
|
115
|
+
if st.get('state','') in ('In Progress','Ready','In Review'):
|
|
116
|
+
print(sid)
|
|
117
|
+
break
|
|
118
|
+
" 2>/dev/null || true)"
|
|
119
|
+
|
|
120
|
+
if [[ -z "${story_id}" ]]; then
|
|
121
|
+
# Fallback: most recently updated
|
|
122
|
+
story_id="$(python3 -c "
|
|
123
|
+
import json, sys
|
|
124
|
+
data = json.load(open('${state_json}'))
|
|
125
|
+
stories = data.get('stories', {})
|
|
126
|
+
latest = max(stories.items(), key=lambda kv: kv[1].get('updated_at',''), default=(None,None))
|
|
127
|
+
if latest[0]:
|
|
128
|
+
print(latest[0])
|
|
129
|
+
" 2>/dev/null || true)"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
if [[ -z "${story_id}" ]]; then
|
|
133
|
+
echo ""
|
|
134
|
+
return
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Convert e.g. STORY-014-01 -> find file
|
|
138
|
+
local story_num="${story_id#STORY-}"
|
|
139
|
+
local story_file
|
|
140
|
+
story_file="$(ls "${REPO_ROOT}/.cleargate/delivery/pending-sync/STORY-${story_num}_"*.md 2>/dev/null | head -1 || true)"
|
|
141
|
+
if [[ -z "${story_file}" ]]; then
|
|
142
|
+
story_file="$(ls "${REPO_ROOT}/.cleargate/delivery/archive/STORY-${story_num}_"*.md 2>/dev/null | head -1 || true)"
|
|
143
|
+
fi
|
|
144
|
+
echo "${story_file}"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if [[ -z "${STORY_FILE}" ]]; then
|
|
148
|
+
STORY_FILE="$(resolve_story_file)"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
if [[ -z "${STORY_FILE}" || ! -f "${STORY_FILE}" ]]; then
|
|
152
|
+
echo "[surface-gate] WARNING: No active story file found — skipping surface check" >&2
|
|
153
|
+
exit 0
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# ---- Parse §3.1 file surface table -----------------------------------------
|
|
157
|
+
|
|
158
|
+
parse_surface_paths() {
|
|
159
|
+
local story_file="$1"
|
|
160
|
+
# Extract rows between "### 3.1" and the next "### " header.
|
|
161
|
+
# Table rows: | Item | Value |
|
|
162
|
+
# Only rows where Value cell looks like a path (contains . or /)
|
|
163
|
+
# Strip backticks. Split on ", " for multiple paths in one cell.
|
|
164
|
+
awk '
|
|
165
|
+
/^### 3\.1/ { in_section=1; next }
|
|
166
|
+
in_section && /^### / { in_section=0; next }
|
|
167
|
+
in_section && /^\|/ {
|
|
168
|
+
# Remove leading/trailing pipe, split into fields
|
|
169
|
+
line=$0
|
|
170
|
+
gsub(/^\||\|$/, "", line)
|
|
171
|
+
n=split(line, cols, "|")
|
|
172
|
+
if (n < 2) next
|
|
173
|
+
val=cols[2]
|
|
174
|
+
# Trim whitespace
|
|
175
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
|
|
176
|
+
# Only process if value looks like a path (contains . or /)
|
|
177
|
+
if (val !~ /[.\/]/) next
|
|
178
|
+
# Strip backticks
|
|
179
|
+
gsub(/`/, "", val)
|
|
180
|
+
# Handle multiple paths separated by ", "
|
|
181
|
+
npaths=split(val, paths, ", ")
|
|
182
|
+
for (i=1; i<=npaths; i++) {
|
|
183
|
+
p=paths[i]
|
|
184
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", p)
|
|
185
|
+
if (p != "" && (p ~ /[.\/]/)) print p
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
' "${story_file}"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Collect declared paths into array (portable bash 3.2 compat — no mapfile)
|
|
192
|
+
declared_paths=()
|
|
193
|
+
while IFS= read -r p; do
|
|
194
|
+
declared_paths+=("$p")
|
|
195
|
+
done < <(parse_surface_paths "${STORY_FILE}")
|
|
196
|
+
|
|
197
|
+
if [[ ${#declared_paths[@]} -eq 0 ]]; then
|
|
198
|
+
echo "[surface-gate] WARNING: No file paths found in §3.1 of ${STORY_FILE} — skipping surface check" >&2
|
|
199
|
+
exit 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# ---- Get staged files -------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
staged_files=()
|
|
205
|
+
while IFS= read -r f; do
|
|
206
|
+
staged_files+=("$f")
|
|
207
|
+
done < <(git -C "${REPO_ROOT}" diff --cached --name-only 2>/dev/null || true)
|
|
208
|
+
|
|
209
|
+
if [[ ${#staged_files[@]} -eq 0 ]]; then
|
|
210
|
+
# Nothing staged — nothing to check
|
|
211
|
+
exit 0
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# ---- Load whitelist ---------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
whitelist_patterns=()
|
|
217
|
+
if [[ -f "${WHITELIST_FILE}" ]]; then
|
|
218
|
+
while IFS= read -r line; do
|
|
219
|
+
# Skip comments and blank lines
|
|
220
|
+
[[ -z "${line}" || "${line}" =~ ^# ]] && continue
|
|
221
|
+
whitelist_patterns+=("$line")
|
|
222
|
+
done < "${WHITELIST_FILE}"
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# ---- Helper: match file against whitelist -----------------------------------
|
|
226
|
+
|
|
227
|
+
is_whitelisted() {
|
|
228
|
+
local file="$1"
|
|
229
|
+
local pattern
|
|
230
|
+
for pattern in "${whitelist_patterns[@]+"${whitelist_patterns[@]}"}"; do
|
|
231
|
+
# Use bash glob matching — convert ** to * for simple matching
|
|
232
|
+
# Try direct fnmatch with case
|
|
233
|
+
if [[ "${file}" == ${pattern} ]]; then
|
|
234
|
+
return 0
|
|
235
|
+
fi
|
|
236
|
+
# Try matching basename
|
|
237
|
+
local basename="${file##*/}"
|
|
238
|
+
local pat_base="${pattern##*/}"
|
|
239
|
+
if [[ "${basename}" == ${pat_base} && "${pat_base}" == "${basename}" ]]; then
|
|
240
|
+
: # need full path match
|
|
241
|
+
fi
|
|
242
|
+
# Try: if pattern has **, match any path segment
|
|
243
|
+
local simple_pat="${pattern//\*\*\//*/}"
|
|
244
|
+
if [[ "${file}" == ${simple_pat} ]]; then
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
# Also: check if file path contains the pattern as suffix
|
|
248
|
+
if [[ "${file}" == *"${pattern}" ]]; then
|
|
249
|
+
return 0
|
|
250
|
+
fi
|
|
251
|
+
done
|
|
252
|
+
return 1
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# ---- Helper: normalize path for comparison ----------------------------------
|
|
256
|
+
|
|
257
|
+
normalize_path() {
|
|
258
|
+
local p="$1"
|
|
259
|
+
# Strip leading ./
|
|
260
|
+
p="${p#./}"
|
|
261
|
+
# If absolute path under REPO_ROOT, make it relative
|
|
262
|
+
if [[ "${p}" == "${REPO_ROOT}/"* ]]; then
|
|
263
|
+
p="${p#${REPO_ROOT}/}"
|
|
264
|
+
fi
|
|
265
|
+
echo "${p}"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# ---- Compare staged vs declared ---------------------------------------------
|
|
269
|
+
|
|
270
|
+
off_surface=()
|
|
271
|
+
for staged in "${staged_files[@]}"; do
|
|
272
|
+
staged_norm="$(normalize_path "${staged}")"
|
|
273
|
+
|
|
274
|
+
# Check whitelist first
|
|
275
|
+
if is_whitelisted "${staged_norm}"; then
|
|
276
|
+
continue
|
|
277
|
+
fi
|
|
278
|
+
# Also check absolute path against whitelist
|
|
279
|
+
if is_whitelisted "${REPO_ROOT}/${staged_norm}"; then
|
|
280
|
+
continue
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
# Check against declared surface
|
|
284
|
+
found=0
|
|
285
|
+
for declared in "${declared_paths[@]+"${declared_paths[@]}"}"; do
|
|
286
|
+
declared_norm="$(normalize_path "${declared}")"
|
|
287
|
+
if [[ "${staged_norm}" == "${declared_norm}" ]]; then
|
|
288
|
+
found=1
|
|
289
|
+
break
|
|
290
|
+
fi
|
|
291
|
+
done
|
|
292
|
+
|
|
293
|
+
if [[ "${found}" == "0" ]]; then
|
|
294
|
+
off_surface+=("${staged_norm}")
|
|
295
|
+
fi
|
|
296
|
+
done
|
|
297
|
+
|
|
298
|
+
# ---- Report -----------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
if [[ ${#off_surface[@]} -eq 0 ]]; then
|
|
301
|
+
exit 0
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# Off-surface files detected
|
|
305
|
+
if [[ "${EXECUTION_MODE}" == "v1" ]]; then
|
|
306
|
+
echo "[surface-gate] WARNING (v1 advisory): staged files outside declared §3.1 surface:" >&2
|
|
307
|
+
for f in "${off_surface[@]}"; do
|
|
308
|
+
echo " off-surface: ${f}" >&2
|
|
309
|
+
done
|
|
310
|
+
echo "[surface-gate] v1 mode — not blocking commit. Switch to v2 to enforce." >&2
|
|
311
|
+
exit 0
|
|
312
|
+
else
|
|
313
|
+
echo "[surface-gate] BLOCKED: staged files outside declared §3.1 surface:" >&2
|
|
314
|
+
for f in "${off_surface[@]}"; do
|
|
315
|
+
echo " off-surface: ${f}" >&2
|
|
316
|
+
done
|
|
317
|
+
echo "[surface-gate] Commit blocked. Declare these files in §3.1 or open a CR:scope-change." >&2
|
|
318
|
+
echo "[surface-gate] Set SKIP_SURFACE_GATE=1 to bypass (v2 mode — use sparingly)." >&2
|
|
319
|
+
exit 1
|
|
320
|
+
fi
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"qa": {
|
|
4
|
+
"typecheck": "npm run typecheck",
|
|
5
|
+
"debug_patterns": ["console.log", "console.debug", "debugger"],
|
|
6
|
+
"todo_patterns": ["TODO", "FIXME", "XXX"],
|
|
7
|
+
"test": "npm test"
|
|
8
|
+
},
|
|
9
|
+
"arch": {
|
|
10
|
+
"typecheck": "npm run typecheck",
|
|
11
|
+
"new_deps_check": true,
|
|
12
|
+
"stray_env_files": [".env", ".env.local", ".env.production"],
|
|
13
|
+
"file_count_report": true
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# init_gate_config.sh — Idempotent seeder for gate-checks.json
|
|
3
|
+
# Usage: init_gate_config.sh [--config-path <path>]
|
|
4
|
+
# If gate-checks.json already exists, exits 0 without overwriting.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
CONFIG_PATH="${SCRIPT_DIR}/gate-checks.json"
|
|
9
|
+
|
|
10
|
+
# Allow override via argument
|
|
11
|
+
if [[ "${1:-}" == "--config-path" && -n "${2:-}" ]]; then
|
|
12
|
+
CONFIG_PATH="$2"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if [[ -f "$CONFIG_PATH" ]]; then
|
|
16
|
+
echo "gate-checks.json already exists at ${CONFIG_PATH} — no-op." >&2
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
cat > "$CONFIG_PATH" << 'EOF'
|
|
21
|
+
{
|
|
22
|
+
"schema_version": 1,
|
|
23
|
+
"qa": {
|
|
24
|
+
"typecheck": "npm run typecheck",
|
|
25
|
+
"debug_patterns": ["console.log", "console.debug", "debugger"],
|
|
26
|
+
"todo_patterns": ["TODO", "FIXME", "XXX"],
|
|
27
|
+
"test": "npm test"
|
|
28
|
+
},
|
|
29
|
+
"arch": {
|
|
30
|
+
"typecheck": "npm run typecheck",
|
|
31
|
+
"new_deps_check": true,
|
|
32
|
+
"stray_env_files": [".env", ".env.local", ".env.production"],
|
|
33
|
+
"file_count_report": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
EOF
|
|
37
|
+
|
|
38
|
+
echo "gate-checks.json created at ${CONFIG_PATH}" >&2
|