cleargate 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/MANIFEST.json +72 -16
  3. package/dist/admin-api/index.cjs +0 -1
  4. package/dist/admin-api/index.js +1 -2
  5. package/dist/auth/factory.cjs +0 -1
  6. package/dist/auth/factory.js +2 -3
  7. package/dist/auth/require-token.cjs +0 -1
  8. package/dist/auth/require-token.js +1 -2
  9. package/dist/auth/token-store.cjs +0 -1
  10. package/dist/auth/token-store.js +1 -2
  11. package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
  12. package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
  13. package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
  14. package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
  15. package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
  16. package/dist/cli.cjs +1564 -1414
  17. package/dist/cli.js +1514 -1364
  18. package/dist/lib/ledger.cjs +0 -1
  19. package/dist/lib/ledger.js +1 -2
  20. package/dist/lib/lifecycle-reconcile.cjs +0 -1
  21. package/dist/lib/lifecycle-reconcile.js +2 -3
  22. package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
  23. package/package.json +4 -3
  24. package/templates/cleargate-planning/.claude/agents/architect-synth.md +2 -0
  25. package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
  26. package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
  27. package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
  28. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
  29. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
  30. package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
  31. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
  32. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
  33. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
  34. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
  35. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
  36. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
  37. package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
  38. package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
  39. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
  40. package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
  41. package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
  42. package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
  44. package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
  45. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
  46. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +222 -0
  47. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
  48. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
  49. package/templates/cleargate-planning/MANIFEST.json +72 -16
  50. package/dist/admin-api/index.cjs.map +0 -1
  51. package/dist/admin-api/index.js.map +0 -1
  52. package/dist/auth/factory.cjs.map +0 -1
  53. package/dist/auth/factory.js.map +0 -1
  54. package/dist/auth/require-token.cjs.map +0 -1
  55. package/dist/auth/require-token.js.map +0 -1
  56. package/dist/auth/token-store.cjs.map +0 -1
  57. package/dist/auth/token-store.js.map +0 -1
  58. package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
  59. package/dist/chunk-5DI2Z3C2.js.map +0 -1
  60. package/dist/chunk-BTSZOEWC.js.map +0 -1
  61. package/dist/chunk-E3X7IE5E.js.map +0 -1
  62. package/dist/chunk-PDE37WFQ.js.map +0 -1
  63. package/dist/cli.cjs.map +0 -1
  64. package/dist/cli.js.map +0 -1
  65. package/dist/lib/ledger.cjs.map +0 -1
  66. package/dist/lib/ledger.js.map +0 -1
  67. package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
  68. package/dist/lib/lifecycle-reconcile.js.map +0 -1
  69. package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
  70. package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
  71. package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
  72. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
  73. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
  74. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
  75. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
  76. package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
  77. package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
  78. package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
  79. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
  80. package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
  81. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
  82. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
  83. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
  84. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
  85. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
  86. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
  87. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
  88. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
  89. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
  90. package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
  91. package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
  92. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
  93. package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
  94. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
  95. package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
  96. package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
  97. package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
  98. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
  99. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
  100. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
  101. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
  102. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
  103. package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
  104. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
  105. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
  106. package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
  107. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
  108. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
  109. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
  110. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
  111. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
  112. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
  113. package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
  114. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
  115. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
  116. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
  117. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
  118. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
  119. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
  120. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
  121. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
  122. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
  123. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
  124. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
  125. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
  126. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
  127. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
  128. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
  129. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
  130. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
  131. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
  132. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
  133. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
  134. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
  135. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
  136. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
  137. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
  138. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
  139. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
  140. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
  141. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
  142. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
  143. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
  144. package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
  145. package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
  146. package/dist/templates/synthesis/active-sprint.md +0 -30
  147. package/dist/templates/synthesis/open-gates.md +0 -38
  148. package/dist/templates/synthesis/product-state.md +0 -31
  149. package/dist/templates/synthesis/roadmap.md +0 -63
  150. package/dist/whoami-EANGN46Z.js.map +0 -1
@@ -63,6 +63,15 @@
63
63
  * CLEARGATE_SKIP_BUNDLE_CHECK=1 — skip Step 3.5 bundle generation + size check entirely
64
64
  * (CR-036 test seam; analogous to CLEARGATE_SKIP_MERGE_CHECK).
65
65
  * Never use in production — Step 3.5 is v2-fatal in production.
66
+ * CLEARGATE_SKIP_DEFERRED_VERIFY_CHECK=1 — skip Step 2.9 entirely (test environments where
67
+ * deferred-verification state is irrelevant; mirrors
68
+ * CLEARGATE_SKIP_WORKTREE_CHECK).
69
+ * CLEARGATE_FORCE_DEFERRED_VERIFY=<json> — inject a JSON map of deferred-verify state without
70
+ * reading real story files or result artifacts (mirrors
71
+ * CLEARGATE_FORCE_WORKTREE_PATHS). Shape:
72
+ * { "<STORY-ID>": { declared:[{command,blocks}],
73
+ * result:"green"|"red"|"unrun"|null } }
74
+ * Used to exercise FAIL/PASS/no-op paths in the node:test.
66
75
  */
67
76
 
68
77
  import fs from 'node:fs';
@@ -700,6 +709,224 @@ async function main() {
700
709
  }
701
710
  }
702
711
 
712
+ // ── Step 2.9: Deferred-Verification Close Gate (CR-082) ─────────────────────
713
+ // Block close if any story in this sprint declares a deferred_verification entry
714
+ // with blocks: close and does NOT have a matching green result artifact.
715
+ // NO-OP when no story declares any deferred_verification (the common case — SPRINT-34's path).
716
+ // Fail-open if story-file scan throws (delivery dir absent) — print skip, continue.
717
+ // Test seams: CLEARGATE_SKIP_DEFERRED_VERIFY_CHECK=1 bypasses entirely;
718
+ // CLEARGATE_FORCE_DEFERRED_VERIFY=<json> injects state without reading files.
719
+ process.stdout.write('Step 2.9: checking deferred verifications...\n');
720
+ {
721
+ if (process.env.CLEARGATE_SKIP_DEFERRED_VERIFY_CHECK === '1') {
722
+ process.stdout.write('Step 2.9 skipped: CLEARGATE_SKIP_DEFERRED_VERIFY_CHECK=1 set (test seam).\n');
723
+ } else {
724
+ /**
725
+ * Parse minimal frontmatter from a markdown file: returns an object of
726
+ * key: value pairs from the YAML block between the first two `---` lines.
727
+ * List fields (e.g. deferred_verification: [...]) are returned as raw strings.
728
+ * @param {string} raw
729
+ * @returns {Record<string,string>}
730
+ */
731
+ function parseFrontmatterFields(raw) {
732
+ const fm = raw.match(/^---\n([\s\S]*?)\n---/);
733
+ if (!fm) return {};
734
+ const fields = {};
735
+ for (const line of fm[1].split('\n')) {
736
+ const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)/);
737
+ if (m) fields[m[1]] = m[2].trim();
738
+ }
739
+ return fields;
740
+ }
741
+
742
+ /**
743
+ * Extract the deferred_verification list from raw frontmatter text.
744
+ * Looks for the multi-line YAML list following `deferred_verification:`.
745
+ * Returns an array of { description?, command, blocks } objects, or [].
746
+ * @param {string} raw
747
+ * @returns {Array<{description?: string, command: string, blocks: string}>}
748
+ */
749
+ function parseDeferredVerifications(raw) {
750
+ const fm = raw.match(/^---\n([\s\S]*?)\n---/);
751
+ if (!fm) return [];
752
+ const block = fm[1];
753
+ // Find the deferred_verification key and its value
754
+ const keyIdx = block.indexOf('deferred_verification:');
755
+ if (keyIdx === -1) return [];
756
+ const afterKey = block.slice(keyIdx + 'deferred_verification:'.length);
757
+ const inlineVal = afterKey.match(/^\s*\[([^\]]*)\]/);
758
+ if (inlineVal) {
759
+ const inner = inlineVal[1].trim();
760
+ if (!inner) return []; // empty list []
761
+ }
762
+ // Parse YAML-list items: lines starting with "- " at same or deeper indent
763
+ // For simplicity: if the inline value is empty or the list is multi-line,
764
+ // scan for list entries. A minimal deferred_verification entry has a `command:` field.
765
+ const lines = afterKey.split('\n');
766
+ const entries = [];
767
+ let currentEntry = null;
768
+ for (const line of lines) {
769
+ const listItem = line.match(/^(\s*)-\s+(.*)/);
770
+ const keyVal = line.match(/^\s+([a-zA-Z_]+)\s*:\s*(.*)/);
771
+ if (listItem) {
772
+ if (currentEntry) entries.push(currentEntry);
773
+ currentEntry = {};
774
+ const rest = listItem[2].trim();
775
+ if (rest.startsWith('{')) {
776
+ // Inline object: { description: ..., command: ..., blocks: ... }
777
+ const descM = rest.match(/description\s*:\s*([^,}]+)/);
778
+ const cmdM = rest.match(/command\s*:\s*([^,}]+)/);
779
+ const blkM = rest.match(/blocks\s*:\s*([^,}]+)/);
780
+ if (descM) currentEntry.description = descM[1].trim().replace(/['"]/g, '');
781
+ if (cmdM) currentEntry.command = cmdM[1].trim().replace(/['"]/g, '');
782
+ if (blkM) currentEntry.blocks = blkM[1].trim().replace(/['"]/g, '');
783
+ }
784
+ } else if (keyVal && currentEntry !== null) {
785
+ const k = keyVal[1].trim();
786
+ const v = keyVal[2].trim().replace(/['"]/g, '');
787
+ currentEntry[k] = v;
788
+ } else if (line.match(/^[a-zA-Z_]/) && !line.match(/^\s/)) {
789
+ // Hit a top-level key — end of list
790
+ break;
791
+ }
792
+ }
793
+ if (currentEntry) entries.push(currentEntry);
794
+ // Filter to only entries that have a command (valid entries)
795
+ return entries.filter((e) => e && (e.command || e.blocks));
796
+ }
797
+
798
+ // Determine the declared deferred-verify entries per story
799
+ /** @type {Record<string, {declared: Array<{command:string,blocks:string}>, result: string|null}>} */
800
+ let deferredMap = {};
801
+ let storyFileScanAvailable = true;
802
+
803
+ if (process.env.CLEARGATE_FORCE_DEFERRED_VERIFY) {
804
+ // Test seam: inject state without reading real story files
805
+ try {
806
+ deferredMap = JSON.parse(process.env.CLEARGATE_FORCE_DEFERRED_VERIFY);
807
+ } catch (parseErr) {
808
+ process.stderr.write(
809
+ `Step 2.9 warning: CLEARGATE_FORCE_DEFERRED_VERIFY is not valid JSON: ${/** @type {Error} */ (parseErr).message}\n`
810
+ );
811
+ storyFileScanAvailable = false;
812
+ }
813
+ } else {
814
+ // Scan delivery/pending-sync/ + delivery/archive/ for sprint's story files
815
+ try {
816
+ const deliveryBase = path.join(REPO_ROOT, '.cleargate', 'delivery');
817
+ const dirsToScan = [
818
+ path.join(deliveryBase, 'pending-sync'),
819
+ path.join(deliveryBase, 'archive'),
820
+ ];
821
+ for (const dir of dirsToScan) {
822
+ if (!fs.existsSync(dir)) continue;
823
+ for (const fname of fs.readdirSync(dir)) {
824
+ if (!fname.endsWith('.md')) continue;
825
+ const fpath = path.join(dir, fname);
826
+ let raw;
827
+ try {
828
+ raw = fs.readFileSync(fpath, 'utf8');
829
+ } catch {
830
+ continue;
831
+ }
832
+ const fields = parseFrontmatterFields(raw);
833
+ if (fields['sprint_cleargate_id'] !== sprintId) continue;
834
+ // This file belongs to the sprint — check for deferred_verification
835
+ const entries = parseDeferredVerifications(raw);
836
+ const storyId = fields['story_id'] || fields['cr_id'] || fname.replace('.md', '');
837
+ if (entries.length > 0) {
838
+ deferredMap[storyId] = { declared: entries, result: null };
839
+ }
840
+ }
841
+ }
842
+ } catch (scanErr) {
843
+ storyFileScanAvailable = false;
844
+ process.stdout.write(
845
+ `Step 2.9 skipped: story-file scan unavailable (non-fatal): ${/** @type {Error} */ (scanErr).message}\n`
846
+ );
847
+ }
848
+ }
849
+
850
+ if (!storyFileScanAvailable) {
851
+ // fail-open: scan threw, already printed message above
852
+ } else {
853
+ // Check: any stories with deferred_verification declared?
854
+ const storiesWithDeferred = Object.keys(deferredMap);
855
+ if (storiesWithDeferred.length === 0) {
856
+ // NO-OP: no deferred verifications — silent pass (SPRINT-34's own close path)
857
+ process.stdout.write('Step 2.9 passed: no deferred verifications declared.\n');
858
+ } else {
859
+ const deferred_verify_dir = path.join(sprintDir, 'deferred-verify');
860
+ const failures = [];
861
+
862
+ for (const storyId of storiesWithDeferred) {
863
+ const { declared, result: forcedResult } = deferredMap[storyId];
864
+ const closeEntries = declared.filter(
865
+ (e) => !e.blocks || e.blocks === 'close'
866
+ );
867
+ if (closeEntries.length === 0) continue;
868
+
869
+ if (forcedResult !== undefined && forcedResult !== null) {
870
+ // Test seam: result injected via CLEARGATE_FORCE_DEFERRED_VERIFY
871
+ if (forcedResult !== 'green') {
872
+ for (const entry of closeEntries) {
873
+ failures.push({ storyId, command: entry.command, reason: forcedResult ?? 'unrun' });
874
+ }
875
+ }
876
+ } else {
877
+ // Read result artifact from deferred-verify/<STORY-ID>.json
878
+ const resultFile = path.join(deferred_verify_dir, `${storyId}.json`);
879
+ if (!fs.existsSync(resultFile)) {
880
+ for (const entry of closeEntries) {
881
+ failures.push({ storyId, command: entry.command, reason: 'no result file' });
882
+ }
883
+ } else {
884
+ let resultJson;
885
+ try {
886
+ resultJson = JSON.parse(fs.readFileSync(resultFile, 'utf8'));
887
+ } catch {
888
+ for (const entry of closeEntries) {
889
+ failures.push({ storyId, command: entry.command, reason: 'unreadable result file' });
890
+ }
891
+ continue;
892
+ }
893
+ if (resultJson.status !== 'green' || resultJson.exit_code !== 0) {
894
+ for (const entry of closeEntries) {
895
+ failures.push({
896
+ storyId,
897
+ command: entry.command,
898
+ reason: `status=${resultJson.status ?? 'unknown'} exit_code=${resultJson.exit_code ?? 'unknown'}`,
899
+ });
900
+ }
901
+ }
902
+ }
903
+ }
904
+ }
905
+
906
+ if (failures.length > 0) {
907
+ process.stderr.write(
908
+ `Step 2.9 failed: ${failures.length} deferred verification(s) not green:\n`
909
+ );
910
+ for (const f of failures) {
911
+ process.stderr.write(
912
+ ` - ${f.storyId}: command="${f.command}" reason=${f.reason}\n`
913
+ );
914
+ }
915
+ process.stderr.write(
916
+ ` Run each command, write the result to ${deferred_verify_dir}/<STORY-ID>.json,\n` +
917
+ ` then re-run close_sprint.mjs.\n`
918
+ );
919
+ process.exit(1);
920
+ } else {
921
+ process.stdout.write(
922
+ `Step 2.9 passed: all ${storiesWithDeferred.length} story/stories' deferred verifications are green.\n`
923
+ );
924
+ }
925
+ }
926
+ }
927
+ }
928
+ }
929
+
703
930
  // ── Step 3: Invoke prefill_report.mjs ─────────────────────────────────────
704
931
  process.stdout.write('Step 3: running prefill_report.mjs...\n');
705
932
  try {
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "qa": {
4
- "typecheck": "cd cleargate-cli && npm run typecheck",
4
+ "typecheck": "",
5
5
  "debug_patterns": ["console.log", "console.debug", "debugger"],
6
6
  "todo_patterns": ["TODO", "FIXME", "XXX"],
7
- "test": "cd cleargate-cli && npm test"
7
+ "test": ""
8
8
  },
9
9
  "arch": {
10
- "typecheck": "cd cleargate-cli && npm run typecheck",
10
+ "typecheck": "",
11
11
  "new_deps_check": true,
12
12
  "stray_env_files": [".env", ".env.local", ".env.production"],
13
- "file_count_report": true
13
+ "file_count_report": true,
14
+ "qa_red_lint": true
14
15
  }
15
16
  }
@@ -147,9 +147,62 @@ function main() {
147
147
  }
148
148
 
149
149
  const now = new Date().toISOString();
150
+
151
+ // --- CR-078 F2: Ingest SDR lane assignments ---
152
+ // Read lane assignments from plans/waves.json (canonical machine-readable source).
153
+ // Falls back to parsing the Sprint Plan §2.4 Lane Audit table when waves.json is absent.
154
+ // Emits a WARN (not a hard fail) when neither source declares any lane.
155
+ const wavesJsonPath = path.join(sprintDir, 'plans', 'waves.json');
156
+ let laneAssignments = {};
157
+ let laneSourceFound = false;
158
+
159
+ // Try waves.json first
160
+ if (fs.existsSync(wavesJsonPath)) {
161
+ try {
162
+ const wavesData = JSON.parse(fs.readFileSync(wavesJsonPath, 'utf8'));
163
+ // waves.json may carry a top-level `lane_assignments: { "STORY-ID": "fast"|"standard" }` map
164
+ if (wavesData.lane_assignments && typeof wavesData.lane_assignments === 'object') {
165
+ laneAssignments = wavesData.lane_assignments;
166
+ laneSourceFound = Object.keys(laneAssignments).length > 0;
167
+ }
168
+ } catch (err) {
169
+ process.stderr.write(`WARN: could not parse plans/waves.json: ${err.message}\n`);
170
+ }
171
+ }
172
+
173
+ // Fallback: parse Sprint Plan §2.4 Lane Audit table
174
+ if (!laneSourceFound && sprintFilePath) {
175
+ try {
176
+ const planContent = fs.readFileSync(sprintFilePath, 'utf8');
177
+ // Match the §2.4 Lane Audit table rows: `| Story-ID | lane | rationale |`
178
+ // Accepts both backtick-wrapped and bare story IDs in the first column.
179
+ const tableRowRe = /^\|\s*`?([A-Z][-A-Z0-9]*(?:-\d+)?(?:-\d+)?)`?\s*\|\s*(fast|standard)\s*\|/gm;
180
+ let match;
181
+ while ((match = tableRowRe.exec(planContent)) !== null) {
182
+ const storyId = match[1].trim();
183
+ const lane = match[2].trim();
184
+ laneAssignments[storyId] = lane;
185
+ laneSourceFound = true;
186
+ }
187
+ } catch (err) {
188
+ process.stderr.write(`WARN: could not read sprint plan for lane audit: ${err.message}\n`);
189
+ }
190
+ }
191
+
192
+ if (!laneSourceFound) {
193
+ process.stderr.write(
194
+ `WARN: no lane assignments found (no plans/waves.json lane_assignments and no §2.4 Lane Audit table) — all stories default to standard\n`
195
+ );
196
+ }
197
+
150
198
  const stories = {};
151
199
  for (const id of storyIds) {
152
200
  const carry = preserved[id] || {};
201
+ // Apply SDR lane assignment when available; carry-over lanes take precedence over
202
+ // sdr-lane-audit (they were already explicitly set in the previous sprint).
203
+ const sdrLane = laneAssignments[id];
204
+ const assignedLane = carry.lane ?? sdrLane ?? 'standard';
205
+ const assignedBy = carry.lane_assigned_by ?? (sdrLane ? 'sdr-lane-audit' : 'migration-default');
153
206
  stories[id] = {
154
207
  state: carry.state ?? 'Ready to Bounce',
155
208
  qa_bounces: carry.qa_bounces ?? 0,
@@ -157,8 +210,8 @@ function main() {
157
210
  worktree: carry.worktree ?? null,
158
211
  updated_at: now,
159
212
  notes: carry.notes ?? '',
160
- lane: carry.lane ?? 'standard',
161
- lane_assigned_by: carry.lane_assigned_by ?? 'migration-default',
213
+ lane: assignedLane,
214
+ lane_assigned_by: assignedBy,
162
215
  lane_demoted_at: carry.lane_demoted_at ?? null,
163
216
  lane_demotion_reason: carry.lane_demotion_reason ?? null,
164
217
  };
@@ -234,6 +287,26 @@ function main() {
234
287
  }
235
288
  }
236
289
 
290
+ // --- CR-078 F1: Write .active sentinel (FINAL step) ---
291
+ // Atomically set <projectRoot>/.cleargate/sprint-runs/.active to the sprint ID,
292
+ // mirroring the state.json atomic-write idiom (tmp + rename).
293
+ // Emits a one-line WARN when the prior .active is non-empty and differs from this sprint.
294
+ const activeFile = path.join(REPO_ROOT, '.cleargate', 'sprint-runs', '.active');
295
+ let priorActive = '';
296
+ try {
297
+ priorActive = fs.readFileSync(activeFile, 'utf8').trim();
298
+ } catch {
299
+ // File absent or unreadable — treat as empty; non-fatal
300
+ }
301
+ if (priorActive && priorActive !== sprintId) {
302
+ process.stderr.write(
303
+ `WARN: .active was ${priorActive}, overwriting with ${sprintId} — prior sprint may not have been closed\n`
304
+ );
305
+ }
306
+ const activeTmp = `${activeFile}.tmp.${process.pid}`;
307
+ fs.writeFileSync(activeTmp, sprintId + '\n', 'utf8');
308
+ fs.renameSync(activeTmp, activeFile);
309
+
237
310
  process.stdout.write(`Initialized state.json for sprint ${sprintId} with ${storyIds.length} stories\n`);
238
311
  }
239
312
 
@@ -178,6 +178,54 @@ append_ld_event() {
178
178
  fi
179
179
  }
180
180
 
181
+ # ---------------------------------------------------------------------------
182
+ # read_provision_config <config_yml_path>
183
+ # Reads the worktree.provision_config list from config.yml (CR-079).
184
+ # Returns one entry per line. Defaults to ".env" if absent/unset.
185
+ # Single source of truth: both provision_worktree_config.sh and
186
+ # pre_gate_runner.sh source this function so provisioning and exemption
187
+ # always use the same list.
188
+ # YAML read strategy: awk extracts lines indented under
189
+ # "provision_config:" until the next non-list line or EOF.
190
+ # Node is available but the awk approach keeps it dependency-light and
191
+ # avoids a full YAML parse.
192
+ # ---------------------------------------------------------------------------
193
+ read_provision_config() {
194
+ local config_yml="${1:-}"
195
+ if [[ -z "${config_yml}" || ! -f "${config_yml}" ]]; then
196
+ # No config.yml — return the default list
197
+ printf '.env\n'
198
+ return
199
+ fi
200
+
201
+ # Extract the sequence items under "provision_config:" using awk.
202
+ # Handles YAML like:
203
+ # worktree:
204
+ # provision_config:
205
+ # - .env
206
+ # - .env.local
207
+ # Stops on the first line that is NOT an indented " - <value>" after
208
+ # the provision_config: key.
209
+ local extracted
210
+ extracted="$(awk '
211
+ /^[[:space:]]*provision_config:/ { in_list=1; next }
212
+ in_list && /^[[:space:]]*-[[:space:]]+/ {
213
+ sub(/^[[:space:]]*-[[:space:]]+/, "")
214
+ sub(/[[:space:]]*$/, "")
215
+ print
216
+ next
217
+ }
218
+ in_list { in_list=0 }
219
+ ' "${config_yml}" 2>/dev/null || true)"
220
+
221
+ if [[ -z "${extracted}" ]]; then
222
+ # Key absent or empty — default to .env
223
+ printf '.env\n'
224
+ else
225
+ printf '%s\n' "${extracted}"
226
+ fi
227
+ }
228
+
181
229
  # ---------------------------------------------------------------------------
182
230
  # diff_package_json <worktree_path> <branch>
183
231
  # Prints new runtime deps (non-dev) introduced vs <branch>^.
@@ -41,6 +41,11 @@ if [[ ! -d "$WORKTREE" ]]; then
41
41
  exit 2
42
42
  fi
43
43
 
44
+ # F5 — normalize WORKTREE to an absolute path so that REPORT_FILE and every
45
+ # downstream non-subshell `cd "$WORKTREE"` resolve correctly regardless of
46
+ # whether the caller passed a relative or absolute path (CR-080).
47
+ WORKTREE="$(cd "$WORKTREE" && pwd)"
48
+
44
49
  # ---------------------------------------------------------------------------
45
50
  # Locate gate-checks.json — auto-seed if missing
46
51
  # ---------------------------------------------------------------------------
@@ -255,11 +260,34 @@ run_arch() {
255
260
  try { JSON.parse('${stray_env_json}').forEach(p => console.log(p)); } catch(e) {}
256
261
  " 2>/dev/null)
257
262
 
263
+ # Read the provisioned-config exemption list (CR-079 single source).
264
+ # config.yml lives two directories above SCRIPT_DIR (.cleargate/scripts/../..).
265
+ local CONFIG_YML
266
+ CONFIG_YML="$(cd "${SCRIPT_DIR}/../.." && pwd)/.cleargate/config.yml"
267
+ local provisioned_config=()
268
+ while IFS= read -r _pline; do
269
+ [[ -z "$_pline" ]] || provisioned_config+=("$_pline")
270
+ done < <(read_provision_config "${CONFIG_YML}")
271
+
272
+ # Helper: returns 0 (true) if a pattern is in the provisioned-config list.
273
+ is_provisioned() {
274
+ local _pat="$1"
275
+ local _p
276
+ for _p in "${provisioned_config[@]:-}"; do
277
+ [[ "$_p" = "$_pat" ]] && return 0
278
+ done
279
+ return 1
280
+ }
281
+
258
282
  local stray_found=0
259
283
  local stray_details=""
260
284
  for pat in "${stray_patterns[@]:-}"; do
261
285
  [[ -z "$pat" ]] && continue
262
- if [[ -f "${WORKTREE}/${pat}" ]]; then
286
+ if [[ -f "${WORKTREE}/${pat}" || -L "${WORKTREE}/${pat}" ]]; then
287
+ # Skip patterns that are in the provisioned-config list (CR-079 exemption).
288
+ if is_provisioned "${pat}"; then
289
+ continue
290
+ fi
263
291
  stray_details+="${pat}"$'\n'
264
292
  stray_found=1
265
293
  fi
@@ -284,6 +312,34 @@ run_arch() {
284
312
  echo " ${dir}: ${count} files" >> "$REPORT_FILE"
285
313
  done
286
314
  fi
315
+
316
+ # 5. QA-Red semantic fixture lint (CR-081)
317
+ # Glob red test files under the worktree, run qa_red_lint.mjs.
318
+ # Gate behind arch.qa_red_lint config key (default true).
319
+ # Uses ABSOLUTE paths — no cd (avoids cwd-leak per FLASHCARD #pre-gate #cwd-leak).
320
+ # Uses grep -q (not grep -c || echo 0 — avoids double-count hazard per FLASHCARD #test-harness #bash).
321
+ local qa_red_lint_enabled
322
+ qa_red_lint_enabled="$(read_config_field "arch.qa_red_lint" "$CONFIG_FILE")"
323
+ # Default to enabled when key is absent (empty string → treat as true)
324
+ if [[ -z "$qa_red_lint_enabled" || "$qa_red_lint_enabled" == "true" ]]; then
325
+ local lint_script="${SCRIPT_DIR}/qa_red_lint.mjs"
326
+ if [[ -f "$lint_script" ]]; then
327
+ local lint_out lint_exit
328
+ lint_exit=0
329
+ lint_out="$(node "${lint_script}" "${WORKTREE}" 2>&1)" || lint_exit=$?
330
+ if [[ $lint_exit -eq 0 ]]; then
331
+ record_result "$REPORT_FILE" "qa_red_lint" "PASS" "no semantic fixture issues"
332
+ else
333
+ record_result "$REPORT_FILE" "qa_red_lint" "FAIL" "$(echo "$lint_out" | head -5 | tr '\n' '|')"
334
+ echo "$lint_out" >> "$REPORT_FILE"
335
+ OVERALL_EXIT=1
336
+ fi
337
+ else
338
+ record_result "$REPORT_FILE" "qa_red_lint" "INFO" "skipped (qa_red_lint.mjs not found at ${lint_script})"
339
+ fi
340
+ else
341
+ record_result "$REPORT_FILE" "qa_red_lint" "INFO" "skipped (arch.qa_red_lint=false in config)"
342
+ fi
287
343
  }
288
344
 
289
345
  # ---------------------------------------------------------------------------
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bash
2
+ # provision_worktree_config.sh — Provision configured gitignored config into a story worktree.
3
+ # Usage: provision_worktree_config.sh <worktree-path> [--mode symlink|copy]
4
+ #
5
+ # Provisions the roots listed in config.yml worktree.provision_config (default [".env"])
6
+ # into the given worktree path. Symlink mode (default) creates an absolute symlink so
7
+ # the target's build/tests load their config in-worktree without a manual step.
8
+ # Copy mode copies the file (use when the worktree must hold a divergent config).
9
+ #
10
+ # Idempotent: skips roots already present in the worktree.
11
+ # No-op (INFO, exit 0): skips roots absent at the repo root (not an error).
12
+ #
13
+ # Absolute-path safety: repo root resolved via `git rev-parse --show-toplevel`
14
+ # from the SCRIPT's own directory — NOT $PWD — to avoid the doubled-cwd hazard
15
+ # (cf. FLASHCARD #pre-gate #cwd-leak: cwd in a worktree differs from repo root).
16
+ #
17
+ # Part of CR-079: F4 fix (provision gitignored config) + F7 fix (the provisioned
18
+ # .env is exempted from the stray_env_files scan; see pre_gate_runner.sh + the
19
+ # read_provision_config() single-source helper in pre_gate_common.sh).
20
+ #
21
+ # macOS bash 3.2 portable.
22
+ set -euo pipefail
23
+
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+
26
+ # Source shared helpers (read_provision_config lives here).
27
+ # shellcheck source=pre_gate_common.sh
28
+ source "${SCRIPT_DIR}/pre_gate_common.sh"
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Argument parsing
32
+ # ---------------------------------------------------------------------------
33
+ if [[ $# -lt 1 ]]; then
34
+ echo "Usage: provision_worktree_config.sh <worktree-path> [--mode symlink|copy]" >&2
35
+ exit 2
36
+ fi
37
+
38
+ WORKTREE_PATH="$1"
39
+ shift
40
+
41
+ # Default mode from config.yml worktree.provision_mode; flag overrides.
42
+ PROVISION_MODE=""
43
+ while [[ $# -gt 0 ]]; do
44
+ case "$1" in
45
+ --mode)
46
+ if [[ $# -lt 2 ]]; then
47
+ echo "provision_worktree_config.sh: --mode requires a value (symlink|copy)" >&2
48
+ exit 2
49
+ fi
50
+ PROVISION_MODE="$2"
51
+ shift 2
52
+ ;;
53
+ *)
54
+ echo "provision_worktree_config.sh: unknown argument '$1'" >&2
55
+ exit 2
56
+ ;;
57
+ esac
58
+ done
59
+
60
+ if [[ -n "${PROVISION_MODE}" && "${PROVISION_MODE}" != "symlink" && "${PROVISION_MODE}" != "copy" ]]; then
61
+ echo "provision_worktree_config.sh: --mode must be 'symlink' or 'copy', got '${PROVISION_MODE}'" >&2
62
+ exit 2
63
+ fi
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Validate worktree path
67
+ # ---------------------------------------------------------------------------
68
+ if [[ ! -d "${WORKTREE_PATH}" ]]; then
69
+ echo "provision_worktree_config.sh: worktree path does not exist: ${WORKTREE_PATH}" >&2
70
+ exit 2
71
+ fi
72
+
73
+ # Resolve ABSOLUTE worktree path (guard against relative paths passed by caller).
74
+ WORKTREE_ABS="$(cd "${WORKTREE_PATH}" && pwd)"
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Resolve repo root ABSOLUTELY from the script's own directory.
78
+ # This is critical: $PWD in a worktree context points to the worktree, not
79
+ # the repo root, causing doubled-cwd hazard if used for resolution.
80
+ # ---------------------------------------------------------------------------
81
+ REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel 2>/dev/null || true)"
82
+ if [[ -z "${REPO_ROOT}" ]]; then
83
+ echo "provision_worktree_config.sh: cannot resolve repo root from ${SCRIPT_DIR}" >&2
84
+ exit 2
85
+ fi
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Read config.yml: provision_mode (if not set via flag)
89
+ # ---------------------------------------------------------------------------
90
+ CONFIG_YML="${REPO_ROOT}/.cleargate/config.yml"
91
+
92
+ if [[ -z "${PROVISION_MODE}" ]]; then
93
+ if [[ -f "${CONFIG_YML}" ]]; then
94
+ # Extract provision_mode from config.yml using awk.
95
+ # YAML line looks like: provision_mode: symlink
96
+ _raw_mode="$(awk '/^[[:space:]]*provision_mode:[[:space:]]/ {
97
+ sub(/^[[:space:]]*provision_mode:[[:space:]]*/, "")
98
+ sub(/[[:space:]]*#.*$/, "")
99
+ sub(/[[:space:]]*$/, "")
100
+ print; exit
101
+ }' "${CONFIG_YML}" 2>/dev/null || true)"
102
+ if [[ "${_raw_mode}" = "copy" ]]; then
103
+ PROVISION_MODE="copy"
104
+ else
105
+ PROVISION_MODE="symlink"
106
+ fi
107
+ else
108
+ PROVISION_MODE="symlink"
109
+ fi
110
+ fi
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Read the provisioned-config list (single source: read_provision_config).
114
+ # ---------------------------------------------------------------------------
115
+ provision_roots=()
116
+ while IFS= read -r _root; do
117
+ [[ -z "$_root" ]] || provision_roots+=("$_root")
118
+ done < <(read_provision_config "${CONFIG_YML}")
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Provision each root
122
+ # ---------------------------------------------------------------------------
123
+ for root in "${provision_roots[@]:-}"; do
124
+ [[ -z "$root" ]] && continue
125
+
126
+ src="${REPO_ROOT}/${root}"
127
+ dst="${WORKTREE_ABS}/${root}"
128
+
129
+ # No-op if source does not exist at repo root.
130
+ if [[ ! -e "${src}" ]]; then
131
+ echo "[INFO] provision_worktree_config: source '${root}' absent at repo root — skipping"
132
+ continue
133
+ fi
134
+
135
+ # Idempotent: skip if already provisioned in the worktree.
136
+ if [[ -e "${dst}" || -L "${dst}" ]]; then
137
+ echo "[INFO] provision_worktree_config: '${root}' already present in worktree — skipping"
138
+ continue
139
+ fi
140
+
141
+ case "${PROVISION_MODE}" in
142
+ symlink)
143
+ # Symlink target MUST be absolute so the link resolves correctly
144
+ # from inside the worktree's nested directory depth (F5 absolute-path rule).
145
+ ln -s "${src}" "${dst}"
146
+ echo "[INFO] provision_worktree_config: symlinked '${dst}' -> '${src}'"
147
+ ;;
148
+ copy)
149
+ cp "${src}" "${dst}"
150
+ echo "[INFO] provision_worktree_config: copied '${src}' -> '${dst}'"
151
+ ;;
152
+ esac
153
+ done
154
+
155
+ exit 0