cleargate 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +11 -1
  3. package/dist/MANIFEST.json +40 -26
  4. package/dist/chunk-HZPJ5QX4.js +459 -0
  5. package/dist/chunk-HZPJ5QX4.js.map +1 -0
  6. package/dist/cli.cjs +421 -204
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +389 -515
  9. package/dist/cli.js.map +1 -1
  10. package/dist/lib/lifecycle-reconcile.cjs +497 -0
  11. package/dist/lib/lifecycle-reconcile.cjs.map +1 -0
  12. package/dist/lib/lifecycle-reconcile.d.cts +136 -0
  13. package/dist/lib/lifecycle-reconcile.d.ts +136 -0
  14. package/dist/lib/lifecycle-reconcile.js +20 -0
  15. package/dist/lib/lifecycle-reconcile.js.map +1 -0
  16. package/dist/templates/cleargate-planning/.claude/agents/architect.md +55 -2
  17. package/dist/templates/cleargate-planning/.claude/agents/developer.md +22 -0
  18. package/dist/templates/cleargate-planning/.claude/agents/devops.md +249 -0
  19. package/dist/templates/cleargate-planning/.claude/agents/qa.md +41 -0
  20. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +44 -8
  21. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
  22. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
  23. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +21 -1
  24. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +200 -29
  25. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
  26. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +41 -9
  27. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +98 -16
  28. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
  29. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
  30. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
  31. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +150 -22
  32. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
  33. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
  34. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +12 -1
  35. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +3 -0
  36. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +3 -0
  37. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +3 -0
  38. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +3 -0
  39. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
  40. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
  41. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +3 -0
  42. package/dist/templates/cleargate-planning/CLAUDE.md +3 -1
  43. package/dist/templates/cleargate-planning/MANIFEST.json +40 -26
  44. package/package.json +8 -5
  45. package/templates/cleargate-planning/.claude/agents/architect.md +55 -2
  46. package/templates/cleargate-planning/.claude/agents/developer.md +22 -0
  47. package/templates/cleargate-planning/.claude/agents/devops.md +249 -0
  48. package/templates/cleargate-planning/.claude/agents/qa.md +41 -0
  49. package/templates/cleargate-planning/.claude/agents/reporter.md +44 -8
  50. package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
  51. package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
  52. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +21 -1
  53. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +200 -29
  54. package/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
  55. package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +41 -9
  56. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +98 -16
  57. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
  58. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
  59. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
  60. package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +150 -22
  61. package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
  62. package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
  63. package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +12 -1
  64. package/templates/cleargate-planning/.cleargate/templates/Bug.md +3 -0
  65. package/templates/cleargate-planning/.cleargate/templates/CR.md +3 -0
  66. package/templates/cleargate-planning/.cleargate/templates/epic.md +3 -0
  67. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +3 -0
  68. package/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
  69. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
  70. package/templates/cleargate-planning/.cleargate/templates/story.md +3 -0
  71. package/templates/cleargate-planning/CLAUDE.md +3 -1
  72. package/templates/cleargate-planning/MANIFEST.json +40 -26
@@ -6,7 +6,7 @@ This file is the single source of truth for ClearGate's machine-checkable readin
6
6
 
7
7
  ## Predicate Vocabulary
8
8
 
9
- There are exactly **6 predicate shapes**. No other shapes are recognized; a check string that does not match one of these forms throws a parse error at evaluation time.
9
+ There are exactly **7 predicate shapes**. No other shapes are recognized; a check string that does not match one of these forms throws a parse error at evaluation time.
10
10
 
11
11
  **1. `frontmatter(<ref>).<field> <op> <value>`**
12
12
  Reads a frontmatter field from a document. `<ref>` is either `.` (the document being evaluated) or a frontmatter key whose value is a relative path to another document (e.g. `context_source`). `<op>` is one of `==`, `!=`, `>=`, `<=`. `<value>` is a literal string, number, or boolean. Example: `frontmatter(context_source).approved == true` reads the file named by the evaluated document's `context_source` key and asserts its `approved` field equals `true`.
@@ -15,7 +15,13 @@ Reads a frontmatter field from a document. `<ref>` is either `.` (the document b
15
15
  Performs a case-sensitive substring search on the document body (everything after the frontmatter block). The negated form `body does not contain` passes when the string is absent. Example: `body does not contain 'TBD'` fails if the literal string `TBD` appears anywhere in the body.
16
16
 
17
17
  **3. `section(<N>) has <count> <item-type>`**
18
- Splits the document body on `## ` heading boundaries (1-indexed) and counts items of a given type within section N. `<count>` is an expression like `≥1`, `≥3`, or `0` (exact zero). `<item-type>` is one of `checked-checkbox` (lines matching `- [x]`), `unchecked-checkbox` (lines matching `- [ ]`), or `listed-item` (lines matching `- ` regardless of checkbox state). Example: `section(2) has ≥1 checked-checkbox` asserts that the second `##` section contains at least one checked markdown checkbox.
18
+ Splits the document body on `## ` heading boundaries (1-indexed) and counts items of a given type within section N. `<count>` is an expression like `≥1`, `≥3`, or `0` (exact zero). `<item-type>` is one of:
19
+ - `checked-checkbox` — lines matching `- [x]`
20
+ - `unchecked-checkbox` — lines matching `- [ ]`
21
+ - `listed-item` — lines matching `- ` regardless of checkbox state (bullet-precise; use when checkbox/task-list semantics are required, e.g. DoD)
22
+ - `declared-item` — any line that declares a structured item: bullet lines (`- ...`), table data rows (`| ... |` lines following a `|---|`-style separator within the section), or definition-list terms (lines matching `**Item:**`, `Item:`, `*Item*:` etc.). Use `declared-item` when the gate cares only that the author declared at least N entries in section N, regardless of presentation format (table vs bullet vs def-list).
23
+
24
+ Example: `section(2) has ≥1 checked-checkbox` asserts that the second `##` section contains at least one checked markdown checkbox. Example: `section(3) has ≥1 declared-item` passes when §3 contains at least one bullet, table data row, or definition-list term.
19
25
 
20
26
  **4. `file-exists(<path>)`**
21
27
  Asserts that a file exists on disk at the given path, resolved relative to the project root. Example: `file-exists(.cleargate/knowledge/cleargate-protocol.md)` passes when that file is present in the working tree.
@@ -26,6 +32,9 @@ Reads `.cleargate/wiki/index.md` and asserts that the wiki index contains a refe
26
32
  **6. `status-of(<[[ID]]>) == <value>`**
27
33
  Resolves the given ID via the wiki index, reads that page's compiled frontmatter `status:` field, and compares it to `<value>`. Status values in the live corpus are textual strings (`Draft`, `Ready`, `Active`, `Done`) — not emoji. Example: `status-of([[EPIC-008]]) == Active` passes when EPIC-008's wiki page has `status: Active`. Note: this predicate returns `unknown` (evaluates to fail) when the wiki index is stale and the item is not yet compiled. Run `cleargate wiki build` before relying on `status-of` predicates.
28
34
 
35
+ **7. `existing-surfaces-verified`**
36
+ Closed-set predicate (no parameters). Locates the `## Existing Surfaces` section in the document body, extracts path-shaped substrings via regex, asserts each cited path exists on disk relative to the project root. Passes when section is absent (defers to `reuse-audit-recorded`) OR all cited paths exist OR section contains a "no overlap found" / "no existing surface" / "no prior implementation" / "audit returned empty" sentinel. Sandbox-rejected paths (escaping project root) are treated as missing. Example: `existing-surfaces-verified` against an Epic body whose `## Existing Surfaces` cites `cleargate-cli/src/lib/work-item-type.ts:detectWorkItemTypeFromFm` passes when that path exists.
37
+
29
38
  ---
30
39
 
31
40
  ## Severity Model
@@ -60,20 +69,26 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
60
69
  transition: ready-for-decomposition
61
70
  severity: enforcing
62
71
  criteria:
63
- - id: proposal-approved
72
+ - id: parent-approved-proposal
64
73
  check: "frontmatter(context_source).approved == true"
74
+ or_group: parent-approved
75
+ - id: parent-approved-initiative
76
+ check: "frontmatter(context_source).status == 'Triaged'"
77
+ or_group: parent-approved
65
78
  - id: no-tbds
66
79
  check: "body does not contain marker 'TBD'"
67
80
  - id: scope-in-populated
68
- check: "section(2) has ≥1 listed-item"
81
+ check: "section(3) has ≥1 declared-item"
69
82
  - id: affected-files-declared
70
- check: "section(4) has ≥1 listed-item"
83
+ check: "section(5) has ≥1 declared-item"
71
84
  - id: interrogation-resolved
72
85
  check: "body does not contain 'Unresolved'"
73
86
  - id: discovery-checked
74
87
  check: "frontmatter(.).context_source != null"
75
88
  - id: reuse-audit-recorded
76
89
  check: "body contains '## Existing Surfaces'"
90
+ - id: existing-surfaces-verified
91
+ check: "existing-surfaces-verified"
77
92
  - id: simplest-form-justified
78
93
  check: "body contains '## Why not simpler?'"
79
94
  ```
@@ -107,7 +122,7 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
107
122
  - id: no-tbds
108
123
  check: "body does not contain marker 'TBD'"
109
124
  - id: implementation-files-declared
110
- check: "section(3) has ≥1 listed-item"
125
+ check: "section(3) has ≥1 declared-item"
111
126
  - id: dod-declared
112
127
  check: "section(4) has ≥1 listed-item"
113
128
  - id: gherkin-present
@@ -116,6 +131,8 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
116
131
  check: "frontmatter(.).context_source != null"
117
132
  - id: reuse-audit-recorded
118
133
  check: "body contains '## Existing Surfaces'"
134
+ - id: existing-surfaces-verified
135
+ check: "existing-surfaces-verified"
119
136
  - id: simplest-form-justified
120
137
  check: "body contains '## Why not simpler?'"
121
138
  ```
@@ -126,15 +143,17 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
126
143
  severity: enforcing
127
144
  criteria:
128
145
  - id: blast-radius-populated
129
- check: "section(2) has ≥1 listed-item"
146
+ check: "section(2) has ≥1 declared-item"
130
147
  - id: no-tbds
131
148
  check: "body does not contain marker 'TBD'"
132
149
  - id: sandbox-paths-declared
133
- check: "section(2) has ≥1 listed-item"
150
+ check: "section(3) has ≥1 declared-item"
134
151
  - id: discovery-checked
135
152
  check: "frontmatter(.).context_source != null"
136
153
  - id: reuse-audit-recorded
137
154
  check: "body contains '## Existing Surfaces'"
155
+ - id: existing-surfaces-verified
156
+ check: "existing-surfaces-verified"
138
157
  ```
139
158
 
140
159
  ```yaml
@@ -143,7 +162,7 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
143
162
  severity: enforcing
144
163
  criteria:
145
164
  - id: repro-steps-deterministic
146
- check: "section(2) has ≥3 listed-item"
165
+ check: "section(2) has ≥3 declared-item"
147
166
  - id: severity-set
148
167
  check: "frontmatter(.).severity != null"
149
168
  - id: no-tbds
@@ -162,3 +181,16 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
162
181
  - id: discovery-checked
163
182
  check: "frontmatter(.).context_source != null"
164
183
  ```
184
+
185
+ ```yaml
186
+ - work_item_type: initiative
187
+ transition: ready-for-decomposition
188
+ severity: advisory
189
+ criteria:
190
+ - id: no-tbds
191
+ check: "body does not contain marker 'TBD'"
192
+ - id: user-flow-populated
193
+ check: "section(1) has ≥1 listed-item"
194
+ - id: success-criteria-populated
195
+ check: "section(5) has ≥1 listed-item"
196
+ ```
@@ -34,9 +34,10 @@
34
34
  * atomicWrite pattern from update_state.mjs
35
35
  *
36
36
  * Test seams (CR-022 M1):
37
- * CLEARGATE_SKIP_LIFECYCLE_CHECK=1 — skip Step 2.6 lifecycle reconciliation entirely
38
- * (test environments where the CLI binary is present
39
- * but real git history would produce drift false-positives).
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).
40
41
  * CLEARGATE_SKIP_WORKTREE_CHECK=1 — skip Step 2.7 entirely (test environments that cannot
41
42
  * run git worktree list from a real git root).
42
43
  * CLEARGATE_FORCE_WORKTREE_PATHS=p1,p2 — comma-separated fake worktree paths injected into
@@ -59,6 +60,9 @@
59
60
  * --flashcard-cleanup scan in suggest_improvements.mjs.
60
61
  * CLEARGATE_FLASHCARD_LOOKBACK=<N> — override 3-sprint default lookback for
61
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.
62
66
  */
63
67
 
64
68
  import fs from 'node:fs';
@@ -153,7 +157,7 @@ function invokeScript(scriptName, scriptArgs, env) {
153
157
  });
154
158
  }
155
159
 
156
- function main() {
160
+ async function main() {
157
161
  const args = process.argv.slice(2);
158
162
 
159
163
  if (args.length < 1) usage();
@@ -348,6 +352,61 @@ function main() {
348
352
  }
349
353
  }
350
354
 
355
+ // ── Step 2.6b: Cross-Sprint Orphan Drift Check (CR-048) ─────────────────────
356
+ // Detect items in pending-sync/ with non-terminal status whose state.json entry
357
+ // in any closed sprint shows Done — i.e., completed but never archived.
358
+ // v2: drift > 0 blocks close. v1: warn-only.
359
+ // Test seam: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 also skips this step.
360
+ process.stdout.write('Step 2.6b: checking for cross-sprint orphan drift...\n');
361
+ if (process.env.CLEARGATE_SKIP_LIFECYCLE_CHECK !== '1') {
362
+ try {
363
+ const cliBin26b = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
364
+ if (fs.existsSync(cliBin26b)) {
365
+ // Dynamic import the compiled reconciler function
366
+ const reconcilerMod = await import(
367
+ path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'lib', 'lifecycle-reconcile.js')
368
+ ).catch(() => null);
369
+
370
+ if (reconcilerMod && typeof reconcilerMod.reconcileCrossSprintOrphans === 'function') {
371
+ const deliveryRoot = path.join(REPO_ROOT, '.cleargate', 'delivery');
372
+ const sprintRunsRoot = path.join(REPO_ROOT, '.cleargate', 'sprint-runs');
373
+ const orphanResult = reconcilerMod.reconcileCrossSprintOrphans({ deliveryRoot, sprintRunsRoot });
374
+
375
+ if (orphanResult.drift.length > 0) {
376
+ process.stderr.write(
377
+ `Step 2.6b: ${orphanResult.drift.length} cross-sprint orphan(s) detected:\n`
378
+ );
379
+ for (const item of orphanResult.drift) {
380
+ process.stderr.write(
381
+ ` ${item.id} — status: ${item.pending_sync_status} in pending-sync, ` +
382
+ `state: ${item.state_json_state} in ${item.state_json_sprint}\n`
383
+ );
384
+ }
385
+ if (isV2) {
386
+ process.stderr.write(
387
+ 'close_sprint: Step 2.6b FAILED — orphan drift blocks sprint close under v2.\n' +
388
+ ' Archive the listed items and re-run close_sprint.mjs.\n'
389
+ );
390
+ process.exit(1);
391
+ } else {
392
+ process.stdout.write('Step 2.6b warning (v1): orphan drift detected above — remediate before next sprint.\n');
393
+ }
394
+ } else {
395
+ process.stdout.write('Step 2.6b passed: no cross-sprint orphan drift.\n');
396
+ }
397
+ } else {
398
+ process.stdout.write('Step 2.6b skipped: reconcileCrossSprintOrphans not available in built CLI.\n');
399
+ }
400
+ } else {
401
+ process.stdout.write('Step 2.6b skipped: CLI binary not found (non-fatal).\n');
402
+ }
403
+ } catch (step26bErr) {
404
+ process.stderr.write(`Step 2.6b warning: orphan check unavailable: ${step26bErr.message}\n`);
405
+ }
406
+ } else {
407
+ process.stdout.write('Step 2.6b skipped: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 set (test seam).\n');
408
+ }
409
+
351
410
  // ── Step 2.7: Worktree-Closed Check (CR-022 M1) ──────────────────────────
352
411
  // Block close if any .worktrees/STORY-* path is present.
353
412
  // v2 enforcing (exit 1); v1 advisory (warn + continue).
@@ -511,17 +570,40 @@ function main() {
511
570
  }
512
571
 
513
572
  // ── Step 3.5: Build curated Reporter context bundle ───────────────────────
514
- process.stdout.write('Step 3.5: building Reporter context bundle...\n');
515
- try {
516
- invokeScript('prep_reporter_context.mjs', [sprintId], {
517
- CLEARGATE_STATE_FILE: stateFile,
518
- CLEARGATE_SPRINT_DIR: sprintDir,
519
- });
520
- process.stdout.write(`Step 3.5 passed: ${sprintDir}/.reporter-context.md ready.\n`);
521
- } catch (err) {
522
- // Non-fatal — Reporter falls back to source files
523
- process.stderr.write(`Step 3.5 warning: prep_reporter_context.mjs failed: ${/** @type {Error} */ (err).message}\n`);
524
- process.stderr.write('Reporter will fall back to broad-fetch context loading.\n');
573
+ const bundlePath = path.join(sprintDir, '.reporter-context.md');
574
+ const isEnforcingV2 = isV2 && state.execution_mode === 'v2';
575
+ const MIN_BUNDLE_BYTES = 2048;
576
+ if (process.env.CLEARGATE_SKIP_BUNDLE_CHECK === '1') {
577
+ process.stdout.write('Step 3.5 skipped: CLEARGATE_SKIP_BUNDLE_CHECK=1 set (test seam).\n');
578
+ } else {
579
+ process.stdout.write('Step 3.5: building Reporter context bundle...\n');
580
+ try {
581
+ invokeScript('prep_reporter_context.mjs', [sprintId], {
582
+ CLEARGATE_STATE_FILE: stateFile,
583
+ CLEARGATE_SPRINT_DIR: sprintDir,
584
+ });
585
+ if (!fs.existsSync(bundlePath)) {
586
+ throw new Error(`bundle not written at ${bundlePath}`);
587
+ }
588
+ const bundleSize = fs.statSync(bundlePath).size;
589
+ if (bundleSize < MIN_BUNDLE_BYTES) {
590
+ throw new Error(`bundle too small (${bundleSize}B < ${MIN_BUNDLE_BYTES}B): ${bundlePath}`);
591
+ }
592
+ process.stdout.write(`Step 3.5 passed: ${bundlePath} ready (${Math.round(bundleSize / 1024)}KB).\n`);
593
+ } catch (err) {
594
+ const msg = /** @type {Error} */ (err).message;
595
+ if (isEnforcingV2) {
596
+ process.stderr.write(
597
+ `close_sprint: Step 3.5 FAILED (v2 hard-block): ${msg}\n` +
598
+ ` Cannot dispatch Reporter without bundle. Fix prep_reporter_context.mjs or run with execution_mode: v1.\n` +
599
+ ` Diagnostic: node .cleargate/scripts/prep_reporter_context.mjs ${sprintId}\n`
600
+ );
601
+ process.exit(1);
602
+ } else {
603
+ process.stderr.write(`Step 3.5 warning (v1 advisory): ${msg}\n`);
604
+ process.stderr.write('Reporter will fall back to broad-fetch context loading.\n');
605
+ }
606
+ }
525
607
  }
526
608
 
527
609
  // ── Step 4: Orchestrator spawns Reporter separately ───────────────────────
@@ -700,4 +782,4 @@ function main() {
700
782
  }
701
783
  }
702
784
 
703
- main();
785
+ await main();
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "qa": {
4
- "typecheck": "npm run typecheck",
4
+ "typecheck": "cd cleargate-cli && npm run typecheck",
5
5
  "debug_patterns": ["console.log", "console.debug", "debugger"],
6
6
  "todo_patterns": ["TODO", "FIXME", "XXX"],
7
- "test": "npm test"
7
+ "test": "cd cleargate-cli && npm test"
8
8
  },
9
9
  "arch": {
10
- "typecheck": "npm run typecheck",
10
+ "typecheck": "cd cleargate-cli && npm run typecheck",
11
11
  "new_deps_check": true,
12
12
  "stray_env_files": [".env", ".env.local", ".env.production"],
13
13
  "file_count_report": true
@@ -2,7 +2,12 @@
2
2
  /**
3
3
  * init_sprint.mjs — Initialize a sprint state.json
4
4
  *
5
- * Usage: node init_sprint.mjs <sprint-id> --stories ID1,ID2,... [--force]
5
+ * Usage: node init_sprint.mjs <sprint-id> --stories ID1,ID2,... [--force] [--preserve-bounces]
6
+ *
7
+ * --preserve-bounces requires --force; reads existing state.json and carries
8
+ * forward qa_bounces / arch_bounces / state / lane / worktree per matching
9
+ * story-id. Items not in the new --stories list are dropped. Useful when
10
+ * mid-sprint dogfood / rerun must not lose kickback history.
6
11
  *
7
12
  * Creates .cleargate/sprint-runs/<sprint-id>/state.json with initial state
8
13
  * "Ready to Bounce" for each story. Refuses if state.json already exists
@@ -117,6 +122,7 @@ function main() {
117
122
  }
118
123
 
119
124
  const force = args.includes('--force');
125
+ const preserveBounces = args.includes('--preserve-bounces');
120
126
 
121
127
  const sprintDir = path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
122
128
  const stateFile = path.join(sprintDir, 'state.json');
@@ -128,6 +134,20 @@ function main() {
128
134
  process.exit(1);
129
135
  }
130
136
 
137
+ // --- CR-049-followup: Preserve bounce counters when --force re-inits an in-flight sprint ---
138
+ // Default --force overwrites everything (initial design). With --preserve-bounces,
139
+ // qa_bounces / arch_bounces / state are read from the existing state.json and merged
140
+ // back per-story. Only Ready-to-Bounce default fields get reset.
141
+ let preserved = {};
142
+ if (force && preserveBounces && fs.existsSync(stateFile)) {
143
+ try {
144
+ const existing = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
145
+ preserved = existing.stories || {};
146
+ } catch (err) {
147
+ process.stderr.write(`WARN: --preserve-bounces could not read existing state.json: ${err.message}\n`);
148
+ }
149
+ }
150
+
131
151
  // --- Read execution_mode from sprint frontmatter ---
132
152
  const sprintFilePath = findSprintFile(REPO_ROOT, sprintId);
133
153
  const executionMode = sprintFilePath ? readExecutionMode(sprintFilePath) : 'v1';
@@ -159,17 +179,18 @@ function main() {
159
179
  const now = new Date().toISOString();
160
180
  const stories = {};
161
181
  for (const id of storyIds) {
182
+ const carry = preserved[id] || {};
162
183
  stories[id] = {
163
- state: 'Ready to Bounce',
164
- qa_bounces: 0,
165
- arch_bounces: 0,
166
- worktree: null,
184
+ state: carry.state ?? 'Ready to Bounce',
185
+ qa_bounces: carry.qa_bounces ?? 0,
186
+ arch_bounces: carry.arch_bounces ?? 0,
187
+ worktree: carry.worktree ?? null,
167
188
  updated_at: now,
168
- notes: '',
169
- lane: 'standard',
170
- lane_assigned_by: 'migration-default',
171
- lane_demoted_at: null,
172
- lane_demotion_reason: null,
189
+ notes: carry.notes ?? '',
190
+ lane: carry.lane ?? 'standard',
191
+ lane_assigned_by: carry.lane_assigned_by ?? 'migration-default',
192
+ lane_demoted_at: carry.lane_demoted_at ?? null,
193
+ lane_demotion_reason: carry.lane_demotion_reason ?? null,
173
194
  };
174
195
  }
175
196
 
@@ -189,6 +210,61 @@ function main() {
189
210
  fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2) + '\n', 'utf8');
190
211
  fs.renameSync(tmpFile, stateFile);
191
212
 
213
+ // --- CR-045: Write sprint-context.md from template ---
214
+ // Reads .cleargate/templates/sprint_context.md, substitutes frontmatter fields,
215
+ // optionally splices the sprint goal from the sprint plan §0, and writes atomically.
216
+ // Skip if file already exists and --force was not passed (idempotency-safe re-init).
217
+ const ctxTemplate = path.join(REPO_ROOT, '.cleargate', 'templates', 'sprint_context.md');
218
+ const ctxOut = path.join(sprintDir, 'sprint-context.md');
219
+
220
+ if (!fs.existsSync(ctxOut) || force) {
221
+ let ctxContent;
222
+ try {
223
+ ctxContent = fs.readFileSync(ctxTemplate, 'utf8');
224
+ } catch {
225
+ // Template absent — log warning and continue (non-fatal)
226
+ process.stderr.write(
227
+ `WARN: sprint-context.md template not found at ${ctxTemplate}; skipping sprint-context.md write.\n`
228
+ );
229
+ ctxContent = null;
230
+ }
231
+
232
+ if (ctxContent !== null) {
233
+ // Substitute frontmatter placeholders
234
+ ctxContent = ctxContent.replace(/sprint_id:\s*["']?S-NN["']?/, `sprint_id: "${sprintId}"`);
235
+ ctxContent = ctxContent.replace(/created_at:\s*["']?YYYY-MM-DDTHH:MM:SSZ["']?/, `created_at: "${now}"`);
236
+ ctxContent = ctxContent.replace(/last_updated:\s*["']?YYYY-MM-DDTHH:MM:SSZ["']?/, `last_updated: "${now}"`);
237
+
238
+ // Optionally extract sprint goal from sprint plan §0.
239
+ // Regex matches `- **Sprint Goal:** <text>` within first 200 lines (after H1).
240
+ // Falls back to placeholder if absent — non-fatal.
241
+ if (sprintFilePath) {
242
+ try {
243
+ const planContent = fs.readFileSync(sprintFilePath, 'utf8');
244
+ const planLines = planContent.split('\n').slice(0, 200);
245
+ const goalLine = planLines.find((l) => /^- \*\*Sprint Goal:\*\* (.+)$/.test(l.trim()));
246
+ if (goalLine) {
247
+ const goalMatch = goalLine.trim().match(/^- \*\*Sprint Goal:\*\* (.+)$/);
248
+ if (goalMatch) {
249
+ const goalText = goalMatch[1].trim();
250
+ ctxContent = ctxContent.replace(
251
+ '_(populated by orchestrator from sprint plan §0 at kickoff)_',
252
+ goalText
253
+ );
254
+ }
255
+ }
256
+ } catch {
257
+ // Goal extraction failed — leave placeholder; non-fatal
258
+ }
259
+ }
260
+
261
+ // Write atomically via tmpFile + renameSync (mirrors state.json pattern)
262
+ const ctxTmp = `${ctxOut}.tmp.${process.pid}`;
263
+ fs.writeFileSync(ctxTmp, ctxContent, 'utf8');
264
+ fs.renameSync(ctxTmp, ctxOut);
265
+ }
266
+ }
267
+
192
268
  process.stdout.write(`Initialized state.json for sprint ${sprintId} with ${storyIds.length} stories\n`);
193
269
  }
194
270