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