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.
- package/CHANGELOG.md +21 -0
- package/dist/MANIFEST.json +72 -16
- 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-synth.md +2 -0
- 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 +222 -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 +72 -16
- 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,1012 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* close_sprint.mjs — Eight-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
|
-
* 3.5 Build curated Reporter context bundle via prep_reporter_context.mjs (non-fatal)
|
|
13
|
-
* 4. Orchestrator spawns Reporter separately (script validates preconditions only)
|
|
14
|
-
* 5. On Reporter success + user ack (or --assume-ack flag), flip sprint_status -> "Completed"
|
|
15
|
-
* 6. Invoke suggest_improvements.mjs unconditionally
|
|
16
|
-
* 7. Auto-push per-artifact status updates to MCP via cleargate sync work-items (non-fatal)
|
|
17
|
-
* 8. Verbose post-close handoff list (6-item next-steps block to stdout)
|
|
18
|
-
*
|
|
19
|
-
* Report filename: SPRINT-<#>_REPORT.md for new sprints (SPRINT-18+).
|
|
20
|
-
* Backwards-compat: if SPRINT-<#>_REPORT.md is absent but REPORT.md exists (legacy
|
|
21
|
-
* SPRINT-01..17), fall back to REPORT.md for read operations. New writes always
|
|
22
|
-
* use SPRINT-<#>_REPORT.md when the sprint-id carries a numeric portion.
|
|
23
|
-
* If the sprint-id has no numeric portion (e.g. SPRINT-TEST), plain REPORT.md is used.
|
|
24
|
-
*
|
|
25
|
-
* Stdin fallback (STORY-014-10): when `--report-body-stdin` is passed, the script
|
|
26
|
-
* reads the full SPRINT-<#>_REPORT.md body from stdin and writes it atomically in lieu of
|
|
27
|
-
* waiting for a Reporter-produced file. Replaces the Step-4 gate; implies ack.
|
|
28
|
-
* Refuses empty stdin or pre-existing report file.
|
|
29
|
-
*
|
|
30
|
-
* Does NOT archive the sprint file (pending-sync -> archive stays human per EPIC-013 §4.5 step 7).
|
|
31
|
-
*
|
|
32
|
-
* Reuse: TERMINAL_STATES, VALID_STATES from constants.mjs
|
|
33
|
-
* validateState from validate_state.mjs
|
|
34
|
-
* atomicWrite pattern from update_state.mjs
|
|
35
|
-
*
|
|
36
|
-
* Test seams (CR-022 M1):
|
|
37
|
-
* CLEARGATE_SKIP_LIFECYCLE_CHECK=1 — skip Step 2.6 lifecycle reconciliation AND Step 2.6b
|
|
38
|
-
* cross-sprint orphan drift check entirely (test
|
|
39
|
-
* environments where the CLI binary is present but
|
|
40
|
-
* real git history would produce drift false-positives).
|
|
41
|
-
* CLEARGATE_SKIP_WORKTREE_CHECK=1 — skip Step 2.7 entirely (test environments that cannot
|
|
42
|
-
* run git worktree list from a real git root).
|
|
43
|
-
* CLEARGATE_FORCE_WORKTREE_PATHS=p1,p2 — comma-separated fake worktree paths injected into
|
|
44
|
-
* Step 2.7 instead of running git worktree list.
|
|
45
|
-
* Used to exercise the v2 block / v1 advisory paths
|
|
46
|
-
* without a real .worktrees/STORY-* directory.
|
|
47
|
-
* CLEARGATE_SKIP_MERGE_CHECK=1 — skip Step 2.8 entirely (test environments where git
|
|
48
|
-
* refs are absent or merge state is irrelevant).
|
|
49
|
-
* CLEARGATE_FORCE_MERGE_STATUS=merged|unmerged — inject merge status for Step 2.8 without
|
|
50
|
-
* running git merge-base. Used to exercise
|
|
51
|
-
* the v2 block / v1 advisory paths.
|
|
52
|
-
* CLEARGATE_REPO_ROOT=<path> — override REPO_ROOT for Step 2.8 git commands
|
|
53
|
-
* (used in tests that need a controlled git repo).
|
|
54
|
-
* CLEARGATE_SKIP_SPRINT_TRENDS=1 — skip Step 6.5 entirely (test environments).
|
|
55
|
-
* CLEARGATE_SKIP_SKILL_CANDIDATES=1 — skip Step 6.6 entirely (test environments).
|
|
56
|
-
* CLEARGATE_SKIP_FLASHCARD_CLEANUP=1 — skip Step 6.7 entirely (test environments).
|
|
57
|
-
* CLEARGATE_SPRINT_RUNS_DIR=<path> — override .cleargate/sprint-runs/ root for
|
|
58
|
-
* sibling-sprint counting in sprint_trends.mjs.
|
|
59
|
-
* CLEARGATE_FLASHCARD_PATH=<path> — override .cleargate/FLASHCARD.md path for
|
|
60
|
-
* --flashcard-cleanup scan in suggest_improvements.mjs.
|
|
61
|
-
* CLEARGATE_FLASHCARD_LOOKBACK=<N> — override 3-sprint default lookback for
|
|
62
|
-
* --flashcard-cleanup scan.
|
|
63
|
-
* CLEARGATE_SKIP_BUNDLE_CHECK=1 — skip Step 3.5 bundle generation + size check entirely
|
|
64
|
-
* (CR-036 test seam; analogous to CLEARGATE_SKIP_MERGE_CHECK).
|
|
65
|
-
* Never use in production — Step 3.5 is v2-fatal in production.
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
import fs from 'node:fs';
|
|
69
|
-
import path from 'node:path';
|
|
70
|
-
import { fileURLToPath } from 'node:url';
|
|
71
|
-
import { execSync } from 'node:child_process';
|
|
72
|
-
import { TERMINAL_STATES } from './constants.mjs';
|
|
73
|
-
import { validateState } from './validate_state.mjs';
|
|
74
|
-
import { migrateStateToV3 } from './_migrate-schema-v3.mjs';
|
|
75
|
-
import { reportFilename } from './lib/report-filename.mjs';
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Migrate a v1 state.json to v2 by injecting lane fields with defaults.
|
|
79
|
-
* Inlined from update_state.mjs:migrateV1ToV2 to avoid triggering that
|
|
80
|
-
* script's CLI main() on import (update_state.mjs has no module guard).
|
|
81
|
-
* @param {object} state - Parsed v1 state object
|
|
82
|
-
* @returns {object} - The mutated (now v2) state object
|
|
83
|
-
*/
|
|
84
|
-
function migrateV1ToV2(state) {
|
|
85
|
-
state.schema_version = 2;
|
|
86
|
-
const storyIds = Object.keys(state.stories || {});
|
|
87
|
-
for (const id of storyIds) {
|
|
88
|
-
const story = state.stories[id];
|
|
89
|
-
if (story.lane == null) story.lane = 'standard';
|
|
90
|
-
if (story.lane_assigned_by == null) story.lane_assigned_by = 'migration-default';
|
|
91
|
-
if (story.lane_demoted_at === undefined) story.lane_demoted_at = null;
|
|
92
|
-
if (story.lane_demotion_reason === undefined) story.lane_demotion_reason = null;
|
|
93
|
-
}
|
|
94
|
-
process.stderr.write(
|
|
95
|
-
`migration: schema_version 1 → 2 for sprint ${state.sprint_id} (${storyIds.length} stories defaulted to lane: standard)\n`
|
|
96
|
-
);
|
|
97
|
-
return state;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
101
|
-
const REPO_ROOT = process.env.CLEARGATE_REPO_ROOT
|
|
102
|
-
? path.resolve(process.env.CLEARGATE_REPO_ROOT)
|
|
103
|
-
: path.resolve(__dirname, '..', '..');
|
|
104
|
-
const SCRIPTS_DIR = __dirname;
|
|
105
|
-
|
|
106
|
-
function usage() {
|
|
107
|
-
process.stderr.write(
|
|
108
|
-
'Usage: node close_sprint.mjs <sprint-id> [--assume-ack | --report-body-stdin]\n' +
|
|
109
|
-
'\n' +
|
|
110
|
-
'Options:\n' +
|
|
111
|
-
' --assume-ack Skip user acknowledgement prompt (automated tests ONLY — conversational orchestrators MUST NOT pass this)\n' +
|
|
112
|
-
' --report-body-stdin Read SPRINT-<#>_REPORT.md body from stdin; implies ack (STORY-014-10)\n'
|
|
113
|
-
);
|
|
114
|
-
process.exit(2);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Atomic write using tmp+rename pattern (per M1 update_state.mjs convention).
|
|
120
|
-
* @param {string} filePath
|
|
121
|
-
* @param {object} data
|
|
122
|
-
*/
|
|
123
|
-
function atomicWrite(filePath, data) {
|
|
124
|
-
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
125
|
-
fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
126
|
-
fs.renameSync(tmpFile, filePath);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Atomic write for a string body. Separate from atomicWrite() so we don't
|
|
131
|
-
* accidentally JSON.stringify a markdown body.
|
|
132
|
-
* @param {string} filePath
|
|
133
|
-
* @param {string} body
|
|
134
|
-
*/
|
|
135
|
-
function atomicWriteString(filePath, body) {
|
|
136
|
-
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
137
|
-
fs.writeFileSync(tmpFile, body, 'utf8');
|
|
138
|
-
fs.renameSync(tmpFile, filePath);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Invoke a script via node (for .mjs scripts in the same directory).
|
|
143
|
-
* Throws on non-zero exit.
|
|
144
|
-
* @param {string} scriptName
|
|
145
|
-
* @param {string[]} scriptArgs
|
|
146
|
-
* @param {object} env
|
|
147
|
-
*/
|
|
148
|
-
function invokeScript(scriptName, scriptArgs, env) {
|
|
149
|
-
const scriptPath = path.join(SCRIPTS_DIR, scriptName);
|
|
150
|
-
if (!fs.existsSync(scriptPath)) {
|
|
151
|
-
throw new Error(`Script not found: ${scriptPath}`);
|
|
152
|
-
}
|
|
153
|
-
const argStr = scriptArgs.map(a => JSON.stringify(a)).join(' ');
|
|
154
|
-
const cmd = `node ${JSON.stringify(scriptPath)} ${argStr}`;
|
|
155
|
-
execSync(cmd, {
|
|
156
|
-
stdio: 'inherit',
|
|
157
|
-
env: Object.assign({}, process.env, env || {}),
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function main() {
|
|
162
|
-
const args = process.argv.slice(2);
|
|
163
|
-
|
|
164
|
-
if (args.length < 1) usage();
|
|
165
|
-
|
|
166
|
-
const sprintId = args[0];
|
|
167
|
-
const reportBodyStdin = args.includes('--report-body-stdin');
|
|
168
|
-
const assumeAck = args.includes('--assume-ack') || reportBodyStdin;
|
|
169
|
-
|
|
170
|
-
const sprintDir = process.env.CLEARGATE_SPRINT_DIR
|
|
171
|
-
? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
|
|
172
|
-
: path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
|
|
173
|
-
|
|
174
|
-
if (!fs.existsSync(sprintDir)) {
|
|
175
|
-
process.stderr.write(`Error: sprint directory not found: ${sprintDir}\n`);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const stateFile = process.env.CLEARGATE_STATE_FILE
|
|
180
|
-
? path.resolve(process.env.CLEARGATE_STATE_FILE)
|
|
181
|
-
: path.join(sprintDir, 'state.json');
|
|
182
|
-
|
|
183
|
-
// ── Step 1: Load and validate state.json ──────────────────────────────────
|
|
184
|
-
if (!fs.existsSync(stateFile)) {
|
|
185
|
-
process.stderr.write(
|
|
186
|
-
`Error: state.json not found at ${stateFile}\n` +
|
|
187
|
-
`Hint: run init_sprint.mjs ${sprintId} --stories <ids> first\n`
|
|
188
|
-
);
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let state;
|
|
193
|
-
try {
|
|
194
|
-
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
195
|
-
} catch (err) {
|
|
196
|
-
process.stderr.write(`Error: failed to parse state.json: ${err.message}\n`);
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Migrate v1 → v2 if needed before strict validation
|
|
201
|
-
if (state.schema_version === 1) {
|
|
202
|
-
state = migrateV1ToV2(state);
|
|
203
|
-
atomicWrite(stateFile, state);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Migrate v2 → v3: strip execution_mode (STORY-070-01)
|
|
207
|
-
const { changed: v3Changed } = migrateStateToV3(state, stateFile);
|
|
208
|
-
if (v3Changed) {
|
|
209
|
-
atomicWrite(stateFile, state);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const { valid, errors } = validateState(state);
|
|
213
|
-
if (!valid) {
|
|
214
|
-
process.stderr.write('Error: state.json validation failed:\n');
|
|
215
|
-
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ── Step 2: Refuse if any story not in TERMINAL_STATES ────────────────────
|
|
220
|
-
const nonTerminal = [];
|
|
221
|
-
for (const [storyId, story] of Object.entries(state.stories || {})) {
|
|
222
|
-
if (!TERMINAL_STATES.includes(story.state)) {
|
|
223
|
-
nonTerminal.push(`${storyId}: ${story.state} — not terminal`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (nonTerminal.length > 0) {
|
|
228
|
-
process.stderr.write('Error: sprint cannot close — non-terminal stories:\n');
|
|
229
|
-
for (const msg of nonTerminal) {
|
|
230
|
-
process.stderr.write(` ${msg}\n`);
|
|
231
|
-
}
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
process.stdout.write(`Step 1-2 passed: all ${Object.keys(state.stories || {}).length} stories are terminal.\n`);
|
|
236
|
-
|
|
237
|
-
// ── Step 2.5: v2.1 validation — activation-gated ──────────────────────────
|
|
238
|
-
// Activation gate: at least one story has lane: 'fast'
|
|
239
|
-
// (schema_version is always >= 3 post-STORY-070-01 migrator)
|
|
240
|
-
const hasFastLane = Object.values(state.stories || {}).some(
|
|
241
|
-
(s) => /** @type {any} */ (s).lane === 'fast'
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
if (hasFastLane) {
|
|
245
|
-
// Naming convention: sprint dir must match ^SPRINT-\d{2,3}$
|
|
246
|
-
const sprintDirName = path.basename(sprintDir);
|
|
247
|
-
if (!/^SPRINT-\d{2,3}$/.test(sprintDirName)) {
|
|
248
|
-
process.stderr.write(
|
|
249
|
-
`close_sprint: sprint dir "${sprintDirName}" does not match ^SPRINT-\\d{2,3}$\n` +
|
|
250
|
-
` Expected format: SPRINT-NN or SPRINT-NNN (e.g. SPRINT-14)\n` +
|
|
251
|
-
` Got: "${sprintDirName}" at path: ${sprintDir}\n`
|
|
252
|
-
);
|
|
253
|
-
process.exit(1);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Read SPRINT-<#>_REPORT.md (with legacy REPORT.md fallback for pre-CR-021 sprints)
|
|
257
|
-
const reportFile2 = reportFilename(sprintDir, sprintId, { forRead: true });
|
|
258
|
-
if (!fs.existsSync(reportFile2)) {
|
|
259
|
-
process.stderr.write(
|
|
260
|
-
`close_sprint: v2.1 validation requires ${path.basename(reportFile2)} at ${reportFile2}\n` +
|
|
261
|
-
' Run the Reporter agent first, then re-run close_sprint.mjs.\n'
|
|
262
|
-
);
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
const report = fs.readFileSync(reportFile2, 'utf8');
|
|
266
|
-
|
|
267
|
-
// Check required §3 metric rows
|
|
268
|
-
const requiredMetricRows = [
|
|
269
|
-
/Fast-Track Ratio/,
|
|
270
|
-
/Fast-Track Demotion Rate/,
|
|
271
|
-
/Hotfix Count/,
|
|
272
|
-
/Hotfix-to-Story Ratio/,
|
|
273
|
-
/Hotfix Cap Breaches/,
|
|
274
|
-
/LD events/,
|
|
275
|
-
];
|
|
276
|
-
const missingMetrics = requiredMetricRows.filter((rx) => !rx.test(report));
|
|
277
|
-
if (missingMetrics.length > 0) {
|
|
278
|
-
process.stderr.write(
|
|
279
|
-
`close_sprint: §3 missing rows: ${missingMetrics.map((rx) => rx.source).join(', ')}\n`
|
|
280
|
-
);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Check required §5 sections
|
|
285
|
-
const requiredSections = [
|
|
286
|
-
/Lane Audit/,
|
|
287
|
-
/Hotfix Audit/,
|
|
288
|
-
/Hotfix Trend/,
|
|
289
|
-
];
|
|
290
|
-
const missingSections = requiredSections.filter((rx) => !rx.test(report));
|
|
291
|
-
if (missingSections.length > 0) {
|
|
292
|
-
process.stderr.write(
|
|
293
|
-
`close_sprint: §5 missing: ${missingSections.map((rx) => rx.source).join(', ')}\n`
|
|
294
|
-
);
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
process.stdout.write('Step 2.5 passed: v2.1 validation — all required §3 metrics and §5 sections present.\n');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ── WS8(e) Dist Fail-Closed Assertion ────────────────────────────────────
|
|
302
|
-
// Verify that cleargate-cli/dist/cli.js is present BEFORE attempting the
|
|
303
|
-
// Step 2.6 cascade. Absent dist means the build is stale and the lifecycle/
|
|
304
|
-
// orphan/parent-rollup/backsync/merge gates will silently no-op — that is
|
|
305
|
-
// worse than failing loudly. Exit 1 early so the operator fixes the build.
|
|
306
|
-
//
|
|
307
|
-
// Test seam bypass: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 skips this assertion
|
|
308
|
-
// (the deliberate skip-env path for test environments where the CLI binary
|
|
309
|
-
// is intentionally absent — e.g. sandbox-only sprint dirs).
|
|
310
|
-
if (process.env.CLEARGATE_SKIP_LIFECYCLE_CHECK !== '1') {
|
|
311
|
-
const cliBinEarly = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
|
|
312
|
-
if (!fs.existsSync(cliBinEarly)) {
|
|
313
|
-
process.stderr.write(
|
|
314
|
-
`dist not built — run \`npm run build\` in cleargate-cli/\n` +
|
|
315
|
-
` Expected: ${cliBinEarly}\n` +
|
|
316
|
-
` The lifecycle/orphan/parent-rollup/backsync/merge gates require a built CLI dist.\n`
|
|
317
|
-
);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ── Step 2.6: Lifecycle Reconciliation (CR-017) ──────────────────────────
|
|
323
|
-
// Block close if any artifact referenced in this sprint's commits is still
|
|
324
|
-
// non-terminal in pending-sync (excluding carry_over: true).
|
|
325
|
-
// Invokes `cleargate sprint reconcile-lifecycle <sprint-id>` CLI wrapper.
|
|
326
|
-
// Fail-open if CLI binary is unavailable (non-blocking for test environments).
|
|
327
|
-
// Test seam: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 skips this step entirely (non-fatal).
|
|
328
|
-
process.stdout.write('Step 2.6: running lifecycle reconciliation...\n');
|
|
329
|
-
if (process.env.CLEARGATE_SKIP_LIFECYCLE_CHECK === '1') {
|
|
330
|
-
process.stdout.write('Step 2.6 skipped: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 set (test seam).\n');
|
|
331
|
-
} else {
|
|
332
|
-
try {
|
|
333
|
-
// Resolve CLI binary: prefer local dist/
|
|
334
|
-
const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
|
|
335
|
-
|
|
336
|
-
if (fs.existsSync(cliBin)) {
|
|
337
|
-
// Read sprint start_date from frontmatter for the --since arg
|
|
338
|
-
let sinceArg = '';
|
|
339
|
-
try {
|
|
340
|
-
const pendingDir = path.join(REPO_ROOT, '.cleargate', 'delivery', 'pending-sync');
|
|
341
|
-
if (fs.existsSync(pendingDir)) {
|
|
342
|
-
const entries = fs.readdirSync(pendingDir);
|
|
343
|
-
const sprintFile = entries.find(
|
|
344
|
-
(e) => (e.startsWith(`${sprintId}_`) || e === `${sprintId}.md`) && e.endsWith('.md')
|
|
345
|
-
);
|
|
346
|
-
if (sprintFile) {
|
|
347
|
-
const raw = fs.readFileSync(path.join(pendingDir, sprintFile), 'utf8');
|
|
348
|
-
const startDateMatch = /^start_date:\s*(.+)$/m.exec(raw);
|
|
349
|
-
if (startDateMatch && startDateMatch[1]) {
|
|
350
|
-
sinceArg = `--since ${startDateMatch[1].trim()}`;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
} catch { /* ignore */ }
|
|
355
|
-
|
|
356
|
-
const reconcileArgs = [
|
|
357
|
-
'node', JSON.stringify(cliBin), 'sprint', 'reconcile-lifecycle', JSON.stringify(sprintId),
|
|
358
|
-
];
|
|
359
|
-
if (sinceArg) reconcileArgs.push(sinceArg);
|
|
360
|
-
const reconcileCmd = reconcileArgs.join(' ');
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
execSync(reconcileCmd, { stdio: 'inherit', env: process.env });
|
|
364
|
-
process.stdout.write('Step 2.6 passed: lifecycle reconciliation clean.\n');
|
|
365
|
-
} catch (_reconcileErr) {
|
|
366
|
-
// Exit code 1 from reconcile-lifecycle means drift found
|
|
367
|
-
process.stderr.write(
|
|
368
|
-
'close_sprint: Step 2.6 FAILED — lifecycle drift blocks sprint close.\n' +
|
|
369
|
-
' Remediate the listed artifacts and re-run close_sprint.mjs.\n' +
|
|
370
|
-
' To carry over an artifact: set carry_over: true in its frontmatter.\n'
|
|
371
|
-
);
|
|
372
|
-
process.exit(1);
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
process.stdout.write('Step 2.6 skipped: CLI binary not found at cleargate-cli/dist/cli.js (non-fatal).\n');
|
|
376
|
-
}
|
|
377
|
-
} catch (step26Err) {
|
|
378
|
-
// Unexpected error — fail-open (log but do not block)
|
|
379
|
-
process.stderr.write(`Step 2.6 warning: lifecycle reconciliation unavailable: ${step26Err.message}\n`);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ── Step 2.6b: Cross-Sprint Orphan Drift Check (CR-048) ─────────────────────
|
|
384
|
-
// Detect items in pending-sync/ with non-terminal status whose state.json entry
|
|
385
|
-
// in any closed sprint shows Done — i.e., completed but never archived.
|
|
386
|
-
// Always enforced (STORY-070-01: execution_mode retired).
|
|
387
|
-
// Test seam: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 also skips this step.
|
|
388
|
-
process.stdout.write('Step 2.6b: checking for cross-sprint orphan drift...\n');
|
|
389
|
-
if (process.env.CLEARGATE_SKIP_LIFECYCLE_CHECK !== '1') {
|
|
390
|
-
try {
|
|
391
|
-
const cliBin26b = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
|
|
392
|
-
if (fs.existsSync(cliBin26b)) {
|
|
393
|
-
// Dynamic import the compiled reconciler function
|
|
394
|
-
const reconcilerMod = await import(
|
|
395
|
-
path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'lib', 'lifecycle-reconcile.js')
|
|
396
|
-
).catch(() => null);
|
|
397
|
-
|
|
398
|
-
if (reconcilerMod && typeof reconcilerMod.reconcileCrossSprintOrphans === 'function') {
|
|
399
|
-
const deliveryRoot = path.join(REPO_ROOT, '.cleargate', 'delivery');
|
|
400
|
-
const sprintRunsRoot = path.join(REPO_ROOT, '.cleargate', 'sprint-runs');
|
|
401
|
-
const orphanResult = reconcilerMod.reconcileCrossSprintOrphans({ deliveryRoot, sprintRunsRoot });
|
|
402
|
-
|
|
403
|
-
if (orphanResult.drift.length > 0) {
|
|
404
|
-
process.stderr.write(
|
|
405
|
-
`Step 2.6b: ${orphanResult.drift.length} cross-sprint orphan(s) detected:\n`
|
|
406
|
-
);
|
|
407
|
-
for (const item of orphanResult.drift) {
|
|
408
|
-
process.stderr.write(
|
|
409
|
-
` ${item.id} — status: ${item.pending_sync_status} in pending-sync, ` +
|
|
410
|
-
`state: ${item.state_json_state} in ${item.state_json_sprint}\n`
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
// Always enforced (STORY-070-01: execution_mode retired)
|
|
414
|
-
process.stderr.write(
|
|
415
|
-
'close_sprint: Step 2.6b FAILED — orphan drift blocks sprint close.\n' +
|
|
416
|
-
' Archive the listed items and re-run close_sprint.mjs.\n'
|
|
417
|
-
);
|
|
418
|
-
process.exit(1);
|
|
419
|
-
} else {
|
|
420
|
-
process.stdout.write('Step 2.6b passed: no cross-sprint orphan drift.\n');
|
|
421
|
-
}
|
|
422
|
-
} else {
|
|
423
|
-
process.stdout.write('Step 2.6b skipped: reconcileCrossSprintOrphans not available in built CLI.\n');
|
|
424
|
-
}
|
|
425
|
-
} else {
|
|
426
|
-
process.stdout.write('Step 2.6b skipped: CLI binary not found (non-fatal).\n');
|
|
427
|
-
}
|
|
428
|
-
} catch (step26bErr) {
|
|
429
|
-
process.stderr.write(`Step 2.6b warning: orphan check unavailable: ${step26bErr.message}\n`);
|
|
430
|
-
}
|
|
431
|
-
} else {
|
|
432
|
-
process.stdout.write('Step 2.6b skipped: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 set (test seam).\n');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// ── Step 2.6c: Parent (Epic/Sprint) Rollup (CR-066) ──────────────────────
|
|
436
|
-
// Runs unconditionally (not gated by CLEARGATE_SKIP_LIFECYCLE_CHECK).
|
|
437
|
-
// For each active parent in delivery/pending-sync/, checks children coverage:
|
|
438
|
-
// auto-flip → rewrite status: Completed atomically, log one line.
|
|
439
|
-
// halt-partial → collect into haltList; exit 1 after processing all.
|
|
440
|
-
// halt-zero-children → collect into haltList; exit 1 after processing all.
|
|
441
|
-
// no-op / skip-deferred → silent.
|
|
442
|
-
// Defensive guard: if walkActiveParents is not a function (stale dist/), skip with warning.
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Atomic in-place status rewrite (raw-bytes regex-replace).
|
|
446
|
-
* Follows FLASHCARD 2026-04-24 #frontmatter #write-back pattern.
|
|
447
|
-
* @param {string} filePath
|
|
448
|
-
* @param {string} newStatus
|
|
449
|
-
*/
|
|
450
|
-
function setFrontmatterStatusAtomic(filePath, newStatus) {
|
|
451
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
452
|
-
const fm = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
453
|
-
if (!fm) throw new Error(`No frontmatter in ${filePath}`);
|
|
454
|
-
const newFm = fm[1].replace(/^status:.*$/m, `status: ${newStatus}`);
|
|
455
|
-
const newRaw = raw.replace(fm[1], newFm);
|
|
456
|
-
const tmp = filePath + '.tmp.' + process.pid;
|
|
457
|
-
fs.writeFileSync(tmp, newRaw, 'utf8');
|
|
458
|
-
fs.renameSync(tmp, filePath);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// ── Step 2.6c test seam ──────────────────────────────────────────────────────
|
|
462
|
-
if (process.env.CLEARGATE_SKIP_PARENT_ROLLUP === '1') {
|
|
463
|
-
process.stdout.write('Step 2.6c skipped: CLEARGATE_SKIP_PARENT_ROLLUP=1 set (test seam).\n');
|
|
464
|
-
} else {
|
|
465
|
-
process.stdout.write('Step 2.6c: rolling up parent statuses...\n');
|
|
466
|
-
try {
|
|
467
|
-
// Use __dirname-relative path so the import finds the ACTUAL built dist,
|
|
468
|
-
// not a fixture tmpdir override (CLEARGATE_REPO_ROOT may point elsewhere in tests).
|
|
469
|
-
const scriptRepoRoot26c = path.resolve(SCRIPTS_DIR, '..', '..');
|
|
470
|
-
const reconcilerMod26c = await import(
|
|
471
|
-
path.join(scriptRepoRoot26c, 'cleargate-cli', 'dist', 'lib', 'lifecycle-reconcile.js')
|
|
472
|
-
).catch(() => null);
|
|
473
|
-
|
|
474
|
-
if (!reconcilerMod26c || typeof reconcilerMod26c.walkActiveParents !== 'function') {
|
|
475
|
-
process.stdout.write('Step 2.6c skipped: walkActiveParents not in built CLI — rebuild cleargate-cli/.\n');
|
|
476
|
-
} else {
|
|
477
|
-
// Delivery paths come from REPO_ROOT (may be fixture tmpdir in tests)
|
|
478
|
-
const deliveryRoot26c = path.join(REPO_ROOT, '.cleargate', 'delivery');
|
|
479
|
-
const archiveRoot26c = path.join(deliveryRoot26c, 'archive');
|
|
480
|
-
const results26c = await reconcilerMod26c.walkActiveParents({
|
|
481
|
-
deliveryRoot: deliveryRoot26c,
|
|
482
|
-
archiveRoot: archiveRoot26c,
|
|
483
|
-
});
|
|
484
|
-
const flips26c = results26c.filter((r) => r.verdict === 'auto-flip');
|
|
485
|
-
const halts26c = results26c.filter(
|
|
486
|
-
(r) => r.verdict === 'halt-partial' || r.verdict === 'halt-zero-children',
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
for (const f of flips26c) {
|
|
490
|
-
setFrontmatterStatusAtomic(f.parent_path, 'Completed');
|
|
491
|
-
process.stdout.write(
|
|
492
|
-
`Step 2.6c: ${f.parent_id} status ${f.current_status} → Completed` +
|
|
493
|
-
` (${f.terminal_children.length}/${f.terminal_children.length} children Completed:` +
|
|
494
|
-
` ${f.terminal_children.join(', ')})\n`,
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (halts26c.length > 0) {
|
|
499
|
-
process.stderr.write(`Step 2.6c HALT: ${halts26c.length} parent(s) require manual ack:\n`);
|
|
500
|
-
for (const h of halts26c) {
|
|
501
|
-
process.stderr.write(` - [${h.verdict}] ${h.halt_reason}\n`);
|
|
502
|
-
}
|
|
503
|
-
process.exit(1);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
process.stdout.write(`Step 2.6c passed: ${flips26c.length} parent(s) auto-flipped; no halts.\n`);
|
|
507
|
-
}
|
|
508
|
-
} catch (e26c) {
|
|
509
|
-
process.stderr.write(`Step 2.6c warning: parent rollup unavailable: ${e26c.message}\n`);
|
|
510
|
-
}
|
|
511
|
-
} // end CLEARGATE_SKIP_PARENT_ROLLUP else block
|
|
512
|
-
|
|
513
|
-
// ── Step 2.6d: Same-Sprint Story Backsync (BUG-032) ─────────────────────────
|
|
514
|
-
// Flip all Done-state stories in the closing sprint from their current
|
|
515
|
-
// frontmatter status to `status: Completed, approved: true`, then move
|
|
516
|
-
// the file from pending-sync/ to archive/.
|
|
517
|
-
//
|
|
518
|
-
// Root cause: reconcileCrossSprintOrphans explicitly SKIPS the active sprint
|
|
519
|
-
// (reads .active sentinel). No prior step flipped same-sprint story frontmatter.
|
|
520
|
-
// This step fills that gap.
|
|
521
|
-
//
|
|
522
|
-
// Idempotence: stories already at a terminal status are silently skipped.
|
|
523
|
-
// Runs unconditionally (no CLEARGATE_SKIP_* seam) — the function is pure FS,
|
|
524
|
-
// no git calls, no CLI binary required. CLEARGATE_REPO_ROOT overrides delivery paths.
|
|
525
|
-
process.stdout.write('Step 2.6d: back-syncing same-sprint story frontmatter...\n');
|
|
526
|
-
try {
|
|
527
|
-
const reconcilerMod26d = await import(
|
|
528
|
-
path.join(SCRIPTS_DIR, '..', '..', 'cleargate-cli', 'dist', 'lib', 'lifecycle-reconcile.js')
|
|
529
|
-
).catch(() => null);
|
|
530
|
-
|
|
531
|
-
if (!reconcilerMod26d || typeof reconcilerMod26d.reconcileCurrentSprintStories !== 'function') {
|
|
532
|
-
process.stdout.write(
|
|
533
|
-
'Step 2.6d skipped: reconcileCurrentSprintStories not in built CLI — rebuild cleargate-cli/.\n',
|
|
534
|
-
);
|
|
535
|
-
} else {
|
|
536
|
-
const deliveryRoot26d = path.join(REPO_ROOT, '.cleargate', 'delivery');
|
|
537
|
-
const sprintRunsRoot26d = path.join(REPO_ROOT, '.cleargate', 'sprint-runs');
|
|
538
|
-
|
|
539
|
-
const backsyncResult = reconcilerMod26d.reconcileCurrentSprintStories({
|
|
540
|
-
deliveryRoot: deliveryRoot26d,
|
|
541
|
-
sprintRunsRoot: sprintRunsRoot26d,
|
|
542
|
-
sprintId,
|
|
543
|
-
retroactive: false,
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
if (backsyncResult.flipped.length > 0) {
|
|
547
|
-
for (const item of backsyncResult.flipped) {
|
|
548
|
-
process.stdout.write(
|
|
549
|
-
`Step 2.6d: ${item.id} status ${item.old_status} → Completed` +
|
|
550
|
-
` (state.json: Done) → archived at ${item.file_path}\n`,
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
process.stdout.write(
|
|
554
|
-
`Step 2.6d passed: ${backsyncResult.flipped.length} story/stories back-synced and archived.\n`,
|
|
555
|
-
);
|
|
556
|
-
} else {
|
|
557
|
-
process.stdout.write(
|
|
558
|
-
`Step 2.6d passed: no same-sprint story backsync needed` +
|
|
559
|
-
` (${backsyncResult.skipped_already_terminal} already terminal, ` +
|
|
560
|
-
`${backsyncResult.skipped_not_done} pending non-Done).\n`,
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
} catch (e26d) {
|
|
565
|
-
process.stderr.write(`Step 2.6d warning: same-sprint backsync unavailable: ${e26d.message}\n`);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// ── Step 2.7: Worktree-Closed Check (CR-022 M1) ──────────────────────────
|
|
569
|
-
// Block close if any .worktrees/STORY-* path is present.
|
|
570
|
-
// Always enforced (STORY-070-01: execution_mode retired).
|
|
571
|
-
// Skip if git worktree list is unavailable (non-fatal — tests run against tmpdirs).
|
|
572
|
-
// Test seams: CLEARGATE_SKIP_WORKTREE_CHECK=1 bypasses entirely;
|
|
573
|
-
// CLEARGATE_FORCE_WORKTREE_PATHS=p1,p2 injects fake paths (no git call).
|
|
574
|
-
process.stdout.write('Step 2.7: checking for leftover worktrees...\n');
|
|
575
|
-
{
|
|
576
|
-
if (process.env.CLEARGATE_SKIP_WORKTREE_CHECK === '1') {
|
|
577
|
-
process.stdout.write('Step 2.7 skipped: CLEARGATE_SKIP_WORKTREE_CHECK=1 set (test seam).\n');
|
|
578
|
-
} else {
|
|
579
|
-
let leftoverWorktrees = [];
|
|
580
|
-
let worktreeListAvailable = true;
|
|
581
|
-
|
|
582
|
-
if (process.env.CLEARGATE_FORCE_WORKTREE_PATHS) {
|
|
583
|
-
// Test seam: inject fake worktree paths without running git
|
|
584
|
-
leftoverWorktrees = process.env.CLEARGATE_FORCE_WORKTREE_PATHS
|
|
585
|
-
.split(',')
|
|
586
|
-
.map((p) => p.trim())
|
|
587
|
-
.filter(Boolean);
|
|
588
|
-
} else {
|
|
589
|
-
try {
|
|
590
|
-
const output = execSync('git worktree list --porcelain', {
|
|
591
|
-
cwd: REPO_ROOT,
|
|
592
|
-
encoding: 'utf8',
|
|
593
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
594
|
-
});
|
|
595
|
-
for (const line of output.split('\n')) {
|
|
596
|
-
const trimmed = line.trim();
|
|
597
|
-
if (!trimmed.startsWith('worktree ')) continue;
|
|
598
|
-
const wtPath = trimmed.slice('worktree '.length);
|
|
599
|
-
if (/[/\\]\.worktrees[/\\]STORY-/.test(wtPath)) {
|
|
600
|
-
const m = /(\.(worktrees)[/\\]STORY-.+)$/.exec(wtPath);
|
|
601
|
-
leftoverWorktrees.push(m ? m[1] : wtPath);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
} catch {
|
|
605
|
-
worktreeListAvailable = false;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Step 2.7 always enforced (STORY-070-01: execution_mode retired).
|
|
610
|
-
|
|
611
|
-
if (!worktreeListAvailable) {
|
|
612
|
-
process.stdout.write('Step 2.7 skipped: git worktree list unavailable (non-fatal).\n');
|
|
613
|
-
} else if (leftoverWorktrees.length === 0) {
|
|
614
|
-
process.stdout.write('Step 2.7 passed: no leftover worktrees.\n');
|
|
615
|
-
} else {
|
|
616
|
-
// Always block close on leftover worktrees
|
|
617
|
-
process.stderr.write(
|
|
618
|
-
`close_sprint: Step 2.7 failed: leftover worktree at ${leftoverWorktrees[0]}\n` +
|
|
619
|
-
` ${leftoverWorktrees.length === 1 ? '' : `(plus ${leftoverWorktrees.length - 1} more)\n `}` +
|
|
620
|
-
`Run \`git worktree remove ${leftoverWorktrees[0]}\` if abandoned, or merge the work in progress.\n` +
|
|
621
|
-
` All worktrees must be closed before sprint close.\n`
|
|
622
|
-
);
|
|
623
|
-
process.exit(1);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// ── Step 2.8: Sprint branch merged to main (verify-only, NO auto-merge) ──────
|
|
629
|
-
// CR-022 §1: verify-only — script asserts merge ancestry, does NOT run the merge.
|
|
630
|
-
// On miss: list unmerged commits + exit 1 (v2 enforcing); warn + continue (v1 advisory).
|
|
631
|
-
// Skip when sprintId has no numeric portion (e.g. SPRINT-TEST fixture).
|
|
632
|
-
// Test seams: CLEARGATE_SKIP_MERGE_CHECK=1 bypasses entirely;
|
|
633
|
-
// CLEARGATE_FORCE_MERGE_STATUS=merged|unmerged injects status without git call.
|
|
634
|
-
{
|
|
635
|
-
if (process.env.CLEARGATE_SKIP_MERGE_CHECK === '1') {
|
|
636
|
-
process.stdout.write('Step 2.8 skipped: CLEARGATE_SKIP_MERGE_CHECK=1 set (test seam).\n');
|
|
637
|
-
} else {
|
|
638
|
-
const sprintNumMatch = /^SPRINT-(\d{2,3})$/.exec(sprintId);
|
|
639
|
-
if (!sprintNumMatch) {
|
|
640
|
-
process.stdout.write(`Step 2.8 skipped: sprint-id "${sprintId}" has no numeric portion.\n`);
|
|
641
|
-
} else {
|
|
642
|
-
const sprintBranch = `refs/heads/sprint/S-${sprintNumMatch[1]}`;
|
|
643
|
-
const mainBranch = 'refs/heads/main';
|
|
644
|
-
process.stdout.write(`Step 2.8: verifying ${sprintBranch} merged to ${mainBranch}...\n`);
|
|
645
|
-
|
|
646
|
-
// Step 2.8 always enforced (STORY-070-01: execution_mode retired).
|
|
647
|
-
const forcedStatus = process.env.CLEARGATE_FORCE_MERGE_STATUS;
|
|
648
|
-
let isMerged = false;
|
|
649
|
-
let mergeCheckAvailable = true;
|
|
650
|
-
|
|
651
|
-
if (forcedStatus === 'merged') {
|
|
652
|
-
isMerged = true;
|
|
653
|
-
} else if (forcedStatus === 'unmerged') {
|
|
654
|
-
isMerged = false;
|
|
655
|
-
} else {
|
|
656
|
-
try {
|
|
657
|
-
execSync(
|
|
658
|
-
`git merge-base --is-ancestor ${sprintBranch} ${mainBranch}`,
|
|
659
|
-
{ stdio: 'pipe', cwd: REPO_ROOT, env: process.env }
|
|
660
|
-
);
|
|
661
|
-
isMerged = true;
|
|
662
|
-
} catch (mergeErr) {
|
|
663
|
-
const exitStatus = /** @type {any} */ (mergeErr).status;
|
|
664
|
-
if (exitStatus === 1) {
|
|
665
|
-
isMerged = false;
|
|
666
|
-
} else {
|
|
667
|
-
// exit 128: refs missing or other git failure — fail-open with warning
|
|
668
|
-
mergeCheckAvailable = false;
|
|
669
|
-
process.stderr.write(
|
|
670
|
-
`Step 2.8 warning: git merge-base check unavailable (${/** @type {Error} */ (mergeErr).message}). ` +
|
|
671
|
-
`Skipping merge verification.\n`
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (!mergeCheckAvailable) {
|
|
678
|
-
// fail-open: refs missing or git unavailable — continue to Step 3
|
|
679
|
-
} else if (isMerged) {
|
|
680
|
-
process.stdout.write(`Step 2.8 passed: ${sprintBranch} is merged to ${mainBranch}.\n`);
|
|
681
|
-
} else {
|
|
682
|
-
// Always enforced (STORY-070-01: execution_mode retired)
|
|
683
|
-
let unmergedLog = '';
|
|
684
|
-
if (!forcedStatus) {
|
|
685
|
-
try {
|
|
686
|
-
unmergedLog = execSync(
|
|
687
|
-
`git log ${mainBranch}..${sprintBranch} --oneline`,
|
|
688
|
-
{ encoding: 'utf8', cwd: REPO_ROOT, env: process.env }
|
|
689
|
-
);
|
|
690
|
-
} catch { /* unmerged-log fetch failed — proceed without */ }
|
|
691
|
-
}
|
|
692
|
-
process.stderr.write(
|
|
693
|
-
`Step 2.8 failed: sprint/S-${sprintNumMatch[1]} not merged to main.\n` +
|
|
694
|
-
(unmergedLog ? ` Unmerged commits:\n${unmergedLog}` : '') +
|
|
695
|
-
` Resolve: merge sprint/S-${sprintNumMatch[1]} → main, then re-run close_sprint.mjs.\n`
|
|
696
|
-
);
|
|
697
|
-
process.exit(1);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// ── Step 3: Invoke prefill_report.mjs ─────────────────────────────────────
|
|
704
|
-
process.stdout.write('Step 3: running prefill_report.mjs...\n');
|
|
705
|
-
try {
|
|
706
|
-
invokeScript('prefill_report.mjs', [sprintId], {
|
|
707
|
-
CLEARGATE_STATE_FILE: stateFile,
|
|
708
|
-
CLEARGATE_SPRINT_DIR: sprintDir,
|
|
709
|
-
});
|
|
710
|
-
} catch (err) {
|
|
711
|
-
process.stderr.write(`Error: prefill_report.mjs failed: ${err.message}\n`);
|
|
712
|
-
process.exit(1);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// ── Step 3.5: Build curated Reporter context bundle ───────────────────────
|
|
716
|
-
const bundlePath = path.join(sprintDir, '.reporter-context.md');
|
|
717
|
-
const MIN_BUNDLE_BYTES = 2048;
|
|
718
|
-
if (process.env.CLEARGATE_SKIP_BUNDLE_CHECK === '1') {
|
|
719
|
-
process.stdout.write('Step 3.5 skipped: CLEARGATE_SKIP_BUNDLE_CHECK=1 set (test seam).\n');
|
|
720
|
-
} else {
|
|
721
|
-
process.stdout.write('Step 3.5: building Reporter context bundle...\n');
|
|
722
|
-
try {
|
|
723
|
-
invokeScript('prep_reporter_context.mjs', [sprintId], {
|
|
724
|
-
CLEARGATE_STATE_FILE: stateFile,
|
|
725
|
-
CLEARGATE_SPRINT_DIR: sprintDir,
|
|
726
|
-
});
|
|
727
|
-
if (!fs.existsSync(bundlePath)) {
|
|
728
|
-
throw new Error(`bundle not written at ${bundlePath}`);
|
|
729
|
-
}
|
|
730
|
-
const bundleSize = fs.statSync(bundlePath).size;
|
|
731
|
-
if (bundleSize < MIN_BUNDLE_BYTES) {
|
|
732
|
-
throw new Error(`bundle too small (${bundleSize}B < ${MIN_BUNDLE_BYTES}B): ${bundlePath}`);
|
|
733
|
-
}
|
|
734
|
-
process.stdout.write(`Step 3.5 passed: ${bundlePath} ready (${Math.round(bundleSize / 1024)}KB).\n`);
|
|
735
|
-
} catch (err) {
|
|
736
|
-
// Always enforced (STORY-070-01: execution_mode retired)
|
|
737
|
-
const msg = /** @type {Error} */ (err).message;
|
|
738
|
-
process.stderr.write(
|
|
739
|
-
`close_sprint: Step 3.5 FAILED: ${msg}\n` +
|
|
740
|
-
` Cannot dispatch Reporter without bundle. Fix prep_reporter_context.mjs.\n` +
|
|
741
|
-
` Diagnostic: node .cleargate/scripts/prep_reporter_context.mjs ${sprintId}\n`
|
|
742
|
-
);
|
|
743
|
-
process.exit(1);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// ── Step 4: Orchestrator spawns Reporter separately ───────────────────────
|
|
748
|
-
// This script only validates preconditions; it does NOT fork the Reporter agent.
|
|
749
|
-
const reportFile = reportFilename(sprintDir, sprintId);
|
|
750
|
-
const reportBasename = path.basename(reportFile);
|
|
751
|
-
process.stdout.write(
|
|
752
|
-
'Step 4: preconditions satisfied — orchestrator should now spawn the Reporter agent.\n' +
|
|
753
|
-
` The Reporter writes ${reportBasename} using the sprint_report.md template.\n` +
|
|
754
|
-
` Expected output: ${reportFile}\n`
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
// ── Step 4.5 (STORY-014-10): --report-body-stdin fallback ────────────────
|
|
758
|
-
// Orchestrator pipes the Reporter's markdown body here when the Reporter's
|
|
759
|
-
// Write tool is blocked. Refuses empty stdin + pre-existing report file.
|
|
760
|
-
if (reportBodyStdin) {
|
|
761
|
-
if (fs.existsSync(reportFile)) {
|
|
762
|
-
process.stderr.write(
|
|
763
|
-
`Error: ${reportBasename} already exists at ${reportFile}\n` +
|
|
764
|
-
'Delete it or skip --report-body-stdin mode to use the primary Reporter-write path.\n'
|
|
765
|
-
);
|
|
766
|
-
process.exit(1);
|
|
767
|
-
}
|
|
768
|
-
let body;
|
|
769
|
-
try {
|
|
770
|
-
body = fs.readFileSync(0, 'utf8');
|
|
771
|
-
} catch (err) {
|
|
772
|
-
process.stderr.write(`Error: failed to read stdin: ${/** @type {Error} */ (err).message}\n`);
|
|
773
|
-
process.exit(1);
|
|
774
|
-
}
|
|
775
|
-
if (!body || body.trim().length === 0) {
|
|
776
|
-
process.stderr.write('Error: empty report body — refusing to write.\n');
|
|
777
|
-
process.exit(1);
|
|
778
|
-
}
|
|
779
|
-
atomicWriteString(reportFile, body);
|
|
780
|
-
process.stdout.write(
|
|
781
|
-
`Step 4.5 (stdin mode): ${reportBasename} written (${body.length} bytes) at ${reportFile}\n`
|
|
782
|
-
);
|
|
783
|
-
// Fall through to Step 5 + 6 + 7 unconditionally — stdin mode implies ack.
|
|
784
|
-
} else if (!assumeAck) {
|
|
785
|
-
// Apply read-fallback for legacy sprints (e.g. SPRINT-15 with plain REPORT.md)
|
|
786
|
-
const reportFileForCheck = reportFilename(sprintDir, sprintId, { forRead: true });
|
|
787
|
-
if (!fs.existsSync(reportFileForCheck)) {
|
|
788
|
-
process.stdout.write(
|
|
789
|
-
`\nWaiting for Reporter to produce ${reportBasename}...\n` +
|
|
790
|
-
'After Reporter succeeds, re-run with --assume-ack to complete the close.\n'
|
|
791
|
-
);
|
|
792
|
-
process.exit(0);
|
|
793
|
-
}
|
|
794
|
-
// In non-assume-ack mode with existing report, prompt user
|
|
795
|
-
process.stdout.write(
|
|
796
|
-
`\n${reportBasename} found at ${reportFileForCheck}\n` +
|
|
797
|
-
'Review the report, then confirm close by re-running with --assume-ack\n'
|
|
798
|
-
);
|
|
799
|
-
process.exit(0);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// ── Step 5: Flip sprint_status to "Completed" ────────────────────────────
|
|
803
|
-
process.stdout.write('Step 5: flipping sprint_status to "Completed"...\n');
|
|
804
|
-
const now = new Date().toISOString();
|
|
805
|
-
state.sprint_status = 'Completed';
|
|
806
|
-
state.last_action = `close_sprint: sprint ${sprintId} completed`;
|
|
807
|
-
state.updated_at = now;
|
|
808
|
-
atomicWrite(stateFile, state);
|
|
809
|
-
process.stdout.write(`sprint_status flipped to "Completed" at ${now}\n`);
|
|
810
|
-
|
|
811
|
-
// ── Step 6: Invoke suggest_improvements.mjs unconditionally ───────────────
|
|
812
|
-
process.stdout.write('Step 6: running suggest_improvements.mjs...\n');
|
|
813
|
-
try {
|
|
814
|
-
invokeScript('suggest_improvements.mjs', [sprintId], {
|
|
815
|
-
CLEARGATE_STATE_FILE: stateFile,
|
|
816
|
-
CLEARGATE_SPRINT_DIR: sprintDir,
|
|
817
|
-
});
|
|
818
|
-
} catch (err) {
|
|
819
|
-
// suggest_improvements failure is non-fatal — log but do not abort
|
|
820
|
-
process.stderr.write(`Warning: suggest_improvements.mjs failed: ${/** @type {Error} */ (err).message}\n`);
|
|
821
|
-
process.stderr.write('Sprint is still marked Completed; improvement suggestions may be incomplete.\n');
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// ── Step 6.5: Run sprint_trends.mjs (stub — full impl deferred to CR-027) ──
|
|
825
|
-
if (process.env.CLEARGATE_SKIP_SPRINT_TRENDS !== '1') {
|
|
826
|
-
process.stdout.write('Step 6.5: running sprint_trends.mjs (stub)...\n');
|
|
827
|
-
try {
|
|
828
|
-
invokeScript('sprint_trends.mjs', [sprintId], {
|
|
829
|
-
CLEARGATE_STATE_FILE: stateFile,
|
|
830
|
-
CLEARGATE_SPRINT_DIR: sprintDir,
|
|
831
|
-
});
|
|
832
|
-
} catch (err) {
|
|
833
|
-
// Non-fatal — sprint stays Completed; trends are advisory only.
|
|
834
|
-
process.stderr.write(`Step 6.5 warning: sprint_trends.mjs failed: ${/** @type {Error} */ (err).message}\n`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// ── Step 6.6: Skill-candidate detection (folds into suggest_improvements.mjs) ──
|
|
839
|
-
if (process.env.CLEARGATE_SKIP_SKILL_CANDIDATES !== '1') {
|
|
840
|
-
process.stdout.write('Step 6.6: scanning for skill candidates...\n');
|
|
841
|
-
try {
|
|
842
|
-
invokeScript('suggest_improvements.mjs', [sprintId, '--skill-candidates'], {
|
|
843
|
-
CLEARGATE_STATE_FILE: stateFile,
|
|
844
|
-
CLEARGATE_SPRINT_DIR: sprintDir,
|
|
845
|
-
});
|
|
846
|
-
} catch (err) {
|
|
847
|
-
process.stderr.write(`Step 6.6 warning: skill-candidate scan failed: ${/** @type {Error} */ (err).message}\n`);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// ── Step 6.7: FLASHCARD cleanup pass (folds into suggest_improvements.mjs) ──
|
|
852
|
-
if (process.env.CLEARGATE_SKIP_FLASHCARD_CLEANUP !== '1') {
|
|
853
|
-
process.stdout.write('Step 6.7: scanning FLASHCARD.md for cleanup candidates...\n');
|
|
854
|
-
try {
|
|
855
|
-
invokeScript('suggest_improvements.mjs', [sprintId, '--flashcard-cleanup'], {
|
|
856
|
-
CLEARGATE_STATE_FILE: stateFile,
|
|
857
|
-
CLEARGATE_SPRINT_DIR: sprintDir,
|
|
858
|
-
});
|
|
859
|
-
} catch (err) {
|
|
860
|
-
process.stderr.write(`Step 6.7 warning: FLASHCARD cleanup scan failed: ${/** @type {Error} */ (err).message}\n`);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// ── Step 7: Auto-push per-artifact status updates to MCP ─────────────────
|
|
865
|
-
// Runs after Gate 4 ack succeeds. Non-fatal: sprint stays Completed on failure.
|
|
866
|
-
process.stdout.write('Step 7: pushing per-artifact status updates to MCP...\n');
|
|
867
|
-
try {
|
|
868
|
-
const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
|
|
869
|
-
if (fs.existsSync(cliBin)) {
|
|
870
|
-
// cleargate sync work-items takes ZERO positional args (verified cli.ts:592-598).
|
|
871
|
-
// CR-021 §3.2.3 spec shows a sprint-id arg — that is spec drift; drop it.
|
|
872
|
-
execSync(`node ${JSON.stringify(cliBin)} sync work-items`, {
|
|
873
|
-
stdio: 'inherit',
|
|
874
|
-
env: process.env,
|
|
875
|
-
timeout: 30000,
|
|
876
|
-
});
|
|
877
|
-
process.stdout.write('Step 7 passed: work-item statuses synced.\n');
|
|
878
|
-
} else {
|
|
879
|
-
process.stdout.write('Step 7 skipped: CLI binary not found (non-fatal).\n');
|
|
880
|
-
}
|
|
881
|
-
} catch (err) {
|
|
882
|
-
// Non-fatal — sprint stays Completed; sync can be retried manually
|
|
883
|
-
process.stderr.write(`Step 7 warning: sync work-items failed: ${/** @type {Error} */ (err).message}\n`);
|
|
884
|
-
process.stderr.write('Run `cleargate sync work-items` manually to retry.\n');
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// ── Step 7.4: MCP push of sprint plan + report ───────────────────────────────
|
|
888
|
-
// CR-064: mcp push sprint plan + report
|
|
889
|
-
// Runs after Step 7 (work-item sync) and BEFORE Step 7.5 (CR-063 wiki ingest).
|
|
890
|
-
// Per SDR-locked ordering: MCP push first, wiki ingest second (CR-064 §0.5 Q4 accepted).
|
|
891
|
-
// Non-fatal on failure: sprint stays Completed. MCP push can be retried manually.
|
|
892
|
-
process.stdout.write('Step 7.4: pushing sprint plan + report to MCP...\n');
|
|
893
|
-
try {
|
|
894
|
-
const cliBin74 = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
|
|
895
|
-
if (fs.existsSync(cliBin74)) {
|
|
896
|
-
// Resolve sprint plan path: prefer archive/, fall back to pending-sync/
|
|
897
|
-
const deliveryBase = path.join(REPO_ROOT, '.cleargate', 'delivery');
|
|
898
|
-
const archiveDir = path.join(deliveryBase, 'archive');
|
|
899
|
-
const pendingDir = path.join(deliveryBase, 'pending-sync');
|
|
900
|
-
let planPath = null;
|
|
901
|
-
for (const dir of [archiveDir, pendingDir]) {
|
|
902
|
-
if (!fs.existsSync(dir)) continue;
|
|
903
|
-
const match = fs.readdirSync(dir).find(
|
|
904
|
-
(f) => f.startsWith(sprintId + '_') && f.endsWith('.md'),
|
|
905
|
-
);
|
|
906
|
-
if (match) { planPath = path.join(dir, match); break; }
|
|
907
|
-
}
|
|
908
|
-
if (planPath && fs.existsSync(planPath)) {
|
|
909
|
-
try {
|
|
910
|
-
execSync(`node ${JSON.stringify(cliBin74)} push ${JSON.stringify(planPath)}`, {
|
|
911
|
-
stdio: 'inherit',
|
|
912
|
-
env: process.env,
|
|
913
|
-
timeout: 60000,
|
|
914
|
-
});
|
|
915
|
-
process.stdout.write('Step 7.4a passed: sprint plan pushed to MCP.\n');
|
|
916
|
-
} catch (err74a) {
|
|
917
|
-
process.stderr.write(`Step 7.4a warning: sprint plan push failed: ${/** @type {Error} */ (err74a).message}\n`);
|
|
918
|
-
}
|
|
919
|
-
} else {
|
|
920
|
-
process.stdout.write('Step 7.4a skipped: sprint plan file not found.\n');
|
|
921
|
-
}
|
|
922
|
-
// Resolve sprint report path: reuses reportFile resolved via legacy-fallback in Step 4
|
|
923
|
-
if (reportFile && fs.existsSync(reportFile)) {
|
|
924
|
-
try {
|
|
925
|
-
execSync(`node ${JSON.stringify(cliBin74)} push ${JSON.stringify(reportFile)}`, {
|
|
926
|
-
stdio: 'inherit',
|
|
927
|
-
env: process.env,
|
|
928
|
-
timeout: 60000,
|
|
929
|
-
});
|
|
930
|
-
process.stdout.write('Step 7.4b passed: sprint report pushed to MCP.\n');
|
|
931
|
-
} catch (err74b) {
|
|
932
|
-
process.stderr.write(`Step 7.4b warning: sprint report push failed: ${/** @type {Error} */ (err74b).message}\n`);
|
|
933
|
-
}
|
|
934
|
-
} else {
|
|
935
|
-
process.stdout.write('Step 7.4b skipped: sprint report file not found (legacy sprint or report not yet written).\n');
|
|
936
|
-
}
|
|
937
|
-
} else {
|
|
938
|
-
process.stdout.write('Step 7.4 skipped: CLI binary not found (non-fatal).\n');
|
|
939
|
-
}
|
|
940
|
-
} catch (err) {
|
|
941
|
-
// Non-fatal — sprint stays Completed; MCP push can be retried manually
|
|
942
|
-
process.stderr.write(`Step 7.4 warning: MCP push failed: ${/** @type {Error} */ (err).message}\n`);
|
|
943
|
-
process.stderr.write('Run `cleargate push <plan-or-report-path>` manually to retry.\n');
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// ── Step 7.5: wiki ingest sprint report ──────────────────────────────────
|
|
947
|
-
// CR-063: wiki ingest sprint report
|
|
948
|
-
// Runs after Step 7 (MCP sync). Non-fatal: sprint stays Completed on failure.
|
|
949
|
-
// CR-064 (Wave 3) inserts its MCP-push step at Step 7.4, immediately before this anchor.
|
|
950
|
-
process.stdout.write('Step 7.5: ingesting sprint report into wiki...\n');
|
|
951
|
-
try {
|
|
952
|
-
const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
|
|
953
|
-
if (fs.existsSync(cliBin)) {
|
|
954
|
-
// Resolve report path using the same legacy-fallback rule as Step 4
|
|
955
|
-
// (SPRINT-NN_REPORT.md preferred; fall back to REPORT.md for legacy sprints)
|
|
956
|
-
const reportPath = reportFile; // reportFile is already resolved via legacy-fallback in Step 4
|
|
957
|
-
if (fs.existsSync(reportPath)) {
|
|
958
|
-
execSync(`node ${JSON.stringify(cliBin)} wiki ingest ${JSON.stringify(reportPath)}`, {
|
|
959
|
-
stdio: 'inherit',
|
|
960
|
-
env: process.env,
|
|
961
|
-
timeout: 60000,
|
|
962
|
-
});
|
|
963
|
-
process.stdout.write('Step 7.5 passed: sprint report ingested into wiki.\n');
|
|
964
|
-
} else {
|
|
965
|
-
process.stdout.write('Step 7.5 skipped: report file not found (legacy sprint or report not yet written).\n');
|
|
966
|
-
}
|
|
967
|
-
} else {
|
|
968
|
-
process.stdout.write('Step 7.5 skipped: CLI binary not found (non-fatal).\n');
|
|
969
|
-
}
|
|
970
|
-
} catch (err) {
|
|
971
|
-
// Non-fatal — sprint stays Completed; ingest can be retried manually
|
|
972
|
-
process.stderr.write(`Step 7.5 warning: wiki ingest sprint report failed: ${/** @type {Error} */ (err).message}\n`);
|
|
973
|
-
process.stderr.write('Run `cleargate wiki ingest <report-path>` manually to retry.\n');
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// ── Step 8: Verbose post-close handoff list ───────────────────────────────
|
|
977
|
-
// Prints 6 explicit next-step items to stdout (CR-022 §3 M4).
|
|
978
|
-
{
|
|
979
|
-
const sprintNumMatch = /^SPRINT-(\d{2,3})$/.exec(sprintId);
|
|
980
|
-
const nextSprintNum = sprintNumMatch
|
|
981
|
-
? String(parseInt(sprintNumMatch[1], 10) + 1).padStart(sprintNumMatch[1].length, '0')
|
|
982
|
-
: null;
|
|
983
|
-
const nextSprintId = nextSprintNum ? `SPRINT-${nextSprintNum}` : '<next-sprint-id>';
|
|
984
|
-
const reportBasename = path.basename(reportFile);
|
|
985
|
-
const suggestionsPath = path.join(sprintDir, 'improvement-suggestions.md');
|
|
986
|
-
|
|
987
|
-
process.stdout.write(`\n${sprintId} closed. Next steps:\n`);
|
|
988
|
-
process.stdout.write(` 1. Review ${reportBasename}\n`);
|
|
989
|
-
process.stdout.write(
|
|
990
|
-
` 2. Review improvement-suggestions.md (sections: Suggestions / Skill Candidates / FLASHCARD Cleanup)\n`,
|
|
991
|
-
);
|
|
992
|
-
process.stdout.write(
|
|
993
|
-
` 3. Approve or reject Skill Candidates → run /improve or cleargate skill create <name>\n`,
|
|
994
|
-
);
|
|
995
|
-
process.stdout.write(
|
|
996
|
-
` 4. Approve or reject FLASHCARD cleanup entries → run /improve or cleargate flashcard prune\n`,
|
|
997
|
-
);
|
|
998
|
-
process.stdout.write(
|
|
999
|
-
` 5. Push approved status changes to MCP if Step 7 warned (\`cleargate sync work-items\`)\n`,
|
|
1000
|
-
);
|
|
1001
|
-
process.stdout.write(
|
|
1002
|
-
` 6. Initialize next sprint: \`cleargate sprint init ${nextSprintId} --stories <ids>\`\n`,
|
|
1003
|
-
);
|
|
1004
|
-
|
|
1005
|
-
// Surface artifact paths for convenience
|
|
1006
|
-
process.stdout.write(`\nArtifacts:\n`);
|
|
1007
|
-
process.stdout.write(` report: ${reportFile}\n`);
|
|
1008
|
-
process.stdout.write(` improvement-suggestions: ${suggestionsPath}\n`);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
await main();
|