@ktpartners/dgs-platform 2.9.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +34 -2
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +61 -3
  6. package/agents/dgs-planner.md +51 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/abandon-quick.md +28 -0
  9. package/commands/dgs/add-tests.md +2 -2
  10. package/commands/dgs/audit-milestone.md +4 -3
  11. package/commands/dgs/capture-principle.md +11 -11
  12. package/commands/dgs/cleanup.md +2 -2
  13. package/commands/dgs/complete-milestone.md +11 -11
  14. package/commands/dgs/complete-quick.md +28 -0
  15. package/commands/dgs/create-milestone-job.md +2 -2
  16. package/commands/dgs/debug.md +3 -3
  17. package/commands/dgs/develop-idea.md +1 -1
  18. package/commands/dgs/diff-report.md +124 -0
  19. package/commands/dgs/fast.md +3 -1
  20. package/commands/dgs/health.md +1 -1
  21. package/commands/dgs/map-codebase.md +6 -6
  22. package/commands/dgs/new-milestone.md +5 -5
  23. package/commands/dgs/new-project.md +8 -21
  24. package/commands/dgs/package-scan.md +43 -0
  25. package/commands/dgs/plan-milestone-gaps.md +1 -1
  26. package/commands/dgs/progress.md +3 -3
  27. package/commands/dgs/quick-abandon.md +8 -0
  28. package/commands/dgs/quick-complete.md +8 -0
  29. package/commands/dgs/quick.md +10 -3
  30. package/commands/dgs/research-idea.md +3 -2
  31. package/commands/dgs/research-phase.md +3 -3
  32. package/commands/dgs/switch-project.md +14 -1
  33. package/commands/dgs/write-spec.md +3 -3
  34. package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
  35. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  36. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  37. package/deliver-great-systems/bin/lib/commands.cjs +626 -46
  38. package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
  39. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  40. package/deliver-great-systems/bin/lib/config.cjs +80 -6
  41. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  42. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  43. package/deliver-great-systems/bin/lib/core.cjs +35 -14
  44. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  45. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  46. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  47. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  48. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  49. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  50. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  51. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  52. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  53. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  54. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  55. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  56. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  57. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  58. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  59. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  60. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  61. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  62. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  63. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  64. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  65. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  66. package/deliver-great-systems/bin/lib/init.cjs +357 -61
  67. package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
  68. package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
  69. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  70. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  71. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  72. package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
  73. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  74. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  75. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  76. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  77. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  78. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  79. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  80. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  81. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  82. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  83. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  84. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  85. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  86. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  87. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  88. package/deliver-great-systems/bin/lib/phase.cjs +146 -3
  89. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  90. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  91. package/deliver-great-systems/bin/lib/projects.cjs +65 -10
  92. package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
  93. package/deliver-great-systems/bin/lib/quick.cjs +739 -0
  94. package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
  95. package/deliver-great-systems/bin/lib/repos.cjs +37 -13
  96. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  97. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  98. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  99. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  100. package/deliver-great-systems/bin/lib/state.cjs +147 -55
  101. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  102. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  103. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  104. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  105. package/deliver-great-systems/bin/lib/verify.cjs +198 -7
  106. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  107. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  108. package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
  109. package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
  110. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  111. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  112. package/deliver-great-systems/references/context-tiers.md +4 -0
  113. package/deliver-great-systems/references/package-scan-config.md +151 -0
  114. package/deliver-great-systems/references/questioning.md +0 -30
  115. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  116. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  117. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  118. package/deliver-great-systems/templates/REVIEW.md +35 -0
  119. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  120. package/deliver-great-systems/templates/claude-md.md +27 -0
  121. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  122. package/deliver-great-systems/templates/project.md +6 -170
  123. package/deliver-great-systems/templates/summary.md +3 -1
  124. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  125. package/deliver-great-systems/workflows/add-idea.md +3 -3
  126. package/deliver-great-systems/workflows/add-phase.md +5 -0
  127. package/deliver-great-systems/workflows/add-tests.md +14 -0
  128. package/deliver-great-systems/workflows/add-todo.md +1 -0
  129. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  130. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  131. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  132. package/deliver-great-systems/workflows/cancel-job.md +2 -2
  133. package/deliver-great-systems/workflows/check-todos.md +2 -3
  134. package/deliver-great-systems/workflows/codereview.md +103 -9
  135. package/deliver-great-systems/workflows/complete-milestone.md +218 -24
  136. package/deliver-great-systems/workflows/complete-quick.md +106 -0
  137. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  138. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  139. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  140. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  141. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  142. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  143. package/deliver-great-systems/workflows/execute-phase.md +209 -33
  144. package/deliver-great-systems/workflows/execute-plan.md +22 -22
  145. package/deliver-great-systems/workflows/help.md +53 -20
  146. package/deliver-great-systems/workflows/import-spec.md +65 -7
  147. package/deliver-great-systems/workflows/init-product.md +45 -167
  148. package/deliver-great-systems/workflows/new-milestone.md +140 -33
  149. package/deliver-great-systems/workflows/new-project.md +60 -331
  150. package/deliver-great-systems/workflows/package-scan.md +59 -0
  151. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  152. package/deliver-great-systems/workflows/progress-all.md +133 -0
  153. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  154. package/deliver-great-systems/workflows/quick-complete.md +106 -0
  155. package/deliver-great-systems/workflows/quick.md +328 -26
  156. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  157. package/deliver-great-systems/workflows/research-idea.md +77 -139
  158. package/deliver-great-systems/workflows/resume-project.md +2 -2
  159. package/deliver-great-systems/workflows/run-job.md +29 -43
  160. package/deliver-great-systems/workflows/settings.md +13 -77
  161. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  162. package/deliver-great-systems/workflows/verify-work.md +14 -0
  163. package/deliver-great-systems/workflows/write-spec.md +11 -13
  164. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  165. package/package.json +1 -1
  166. package/scripts/build-hooks.js +1 -0
@@ -0,0 +1,54 @@
1
+ // deliver-great-systems/bin/lib/summary-frontmatter.cjs
2
+ // REL-07: canonical SUMMARY-frontmatter writer.
3
+ // Reads PLAN.md frontmatter `requirements:` field and writes a SUMMARY.md
4
+ // frontmatter that ALWAYS populates the canonical YAML key (CANONICAL_KEY)
5
+ // with those exact values.
6
+ //
7
+ // Canonical key: requirements_completed (underscore — pinned by 157-Q2-FINDINGS.md).
8
+
9
+ const fs = require('node:fs');
10
+ const { extractFrontmatter } = require('./frontmatter.cjs');
11
+
12
+ const CANONICAL_KEY = 'requirements_completed';
13
+
14
+ function writeSummaryFrontmatter({ planPath, summaryPath, extra = {} }) {
15
+ if (!fs.existsSync(planPath)) {
16
+ throw new Error(`writeSummaryFrontmatter: plan not found at ${planPath}`);
17
+ }
18
+ const planContent = fs.readFileSync(planPath, 'utf-8');
19
+ const planFm = extractFrontmatter(planContent);
20
+ const requirements = Array.isArray(planFm.requirements) ? planFm.requirements : [];
21
+
22
+ // Build SUMMARY frontmatter with the canonical key populated verbatim.
23
+ const fm = {
24
+ phase: extra.phase || planFm.phase || 'unknown',
25
+ plan: extra.plan || planFm.plan || '01',
26
+ subsystem: extra.subsystem || 'unspecified',
27
+ tags: extra.tags || [],
28
+ duration: extra.duration || '0min',
29
+ completed: extra.completed || new Date().toISOString().slice(0, 10),
30
+ [CANONICAL_KEY]: requirements,
31
+ ...(extra.executed_by ? { executed_by: extra.executed_by } : {}),
32
+ };
33
+
34
+ // Serialise to YAML frontmatter. Minimal stable serialiser to avoid
35
+ // introducing a new dependency.
36
+ const lines = ['---'];
37
+ for (const [key, value] of Object.entries(fm)) {
38
+ if (Array.isArray(value)) {
39
+ if (value.length === 0) {
40
+ lines.push(`${key}: []`);
41
+ } else {
42
+ lines.push(`${key}: [${value.join(', ')}]`);
43
+ }
44
+ } else {
45
+ lines.push(`${key}: ${value}`);
46
+ }
47
+ }
48
+ lines.push('---', '', `# Phase ${fm.phase} Plan ${fm.plan}: Summary`, '');
49
+
50
+ fs.writeFileSync(summaryPath, lines.join('\n'));
51
+ return { summaryPath, canonicalKey: CANONICAL_KEY, requirementsCopied: requirements.length };
52
+ }
53
+
54
+ module.exports = { writeSummaryFrontmatter, CANONICAL_KEY };
@@ -0,0 +1,78 @@
1
+ // deliver-great-systems/bin/lib/summary-frontmatter.test.cjs
2
+ // REL-07 regression test scaffold — initially RED. Turns GREEN after plan 02
3
+ // pins the canonical YAML key and updates writer/template/agent/workflow/reader.
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+ const os = require('node:os');
10
+ const { extractFrontmatter } = require('./frontmatter.cjs');
11
+
12
+ // CANONICAL_KEY: pinned by 157-Q2-FINDINGS.md (underscore variant).
13
+ // Plan 01 leaves the placeholder; plan 02 confirms the writer/template/etc. agree.
14
+ const CANONICAL_KEY = 'requirements_completed';
15
+
16
+ test('REL-07: SUMMARY writer copies PLAN requirements verbatim into canonical key', () => {
17
+ // RED: this assertion fails because no canonical writer module exists yet.
18
+ let writer;
19
+ try {
20
+ writer = require('./summary-frontmatter.cjs');
21
+ } catch (err) {
22
+ assert.fail('REL-07 writer module not yet implemented: bin/lib/summary-frontmatter.cjs (plan 02 task)');
23
+ }
24
+
25
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rel07-'));
26
+ const planPath = path.join(tmpDir, 'test-PLAN.md');
27
+ const summaryPath = path.join(tmpDir, 'test-SUMMARY.md');
28
+ fs.writeFileSync(planPath, '---\nphase: test\nplan: 01\nrequirements:\n - TEST-01\n - TEST-02\n---\n');
29
+
30
+ writer.writeSummaryFrontmatter({ planPath, summaryPath });
31
+
32
+ const summary = fs.readFileSync(summaryPath, 'utf-8');
33
+ const fm = extractFrontmatter(summary);
34
+ assert.deepStrictEqual(fm[CANONICAL_KEY], ['TEST-01', 'TEST-02']);
35
+ });
36
+
37
+ test('REL-07: SUMMARY writer leaves canonical key empty when PLAN requirements is empty', () => {
38
+ let writer;
39
+ try {
40
+ writer = require('./summary-frontmatter.cjs');
41
+ } catch (err) {
42
+ assert.fail('REL-07 writer module not yet implemented: bin/lib/summary-frontmatter.cjs (plan 02 task)');
43
+ }
44
+
45
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rel07-empty-'));
46
+ const planPath = path.join(tmpDir, 'test-PLAN.md');
47
+ const summaryPath = path.join(tmpDir, 'test-SUMMARY.md');
48
+ fs.writeFileSync(planPath, '---\nphase: test\nplan: 01\nrequirements: []\n---\n');
49
+
50
+ writer.writeSummaryFrontmatter({ planPath, summaryPath });
51
+
52
+ const summary = fs.readFileSync(summaryPath, 'utf-8');
53
+ const fm = extractFrontmatter(summary);
54
+ assert.deepStrictEqual(fm[CANONICAL_KEY] || [], []);
55
+ });
56
+
57
+ test('REL-07: canonical YAML key is consistent across template, agent prompt, workflow, reader, schema', () => {
58
+ // Convergence assertion. RED until plan 02 lands all five edits.
59
+ // __dirname = .../deliver-great-systems/bin/lib
60
+ // bin/lib/.. = .../deliver-great-systems/bin
61
+ // bin/.. = .../deliver-great-systems
62
+ // deliver-great-systems/.. = repo root (which contains the top-level agents/ and the nested deliver-great-systems/)
63
+ const repoRoot = path.resolve(__dirname, '../../..');
64
+ const sources = {
65
+ template: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/templates/summary.md'), 'utf-8'),
66
+ agent: fs.readFileSync(path.join(repoRoot, 'agents/dgs-executor.md'), 'utf-8'),
67
+ workflow: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/workflows/execute-plan.md'), 'utf-8'),
68
+ reader: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/bin/lib/commands.cjs'), 'utf-8'),
69
+ schema: fs.readFileSync(path.join(repoRoot, 'deliver-great-systems/bin/lib/frontmatter.cjs'), 'utf-8'),
70
+ };
71
+
72
+ // Each file must reference the CANONICAL_KEY at least once.
73
+ for (const [name, content] of Object.entries(sources)) {
74
+ assert.ok(content.includes(CANONICAL_KEY), `${name} must reference canonical key ${CANONICAL_KEY}`);
75
+ }
76
+ });
77
+
78
+ // REL-07 sentinel — flag this file as a Wave-0 RED scaffold for plan 02.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * RED test scaffold for REL-02 (Phase 156 plan 03).
3
+ *
4
+ * The `computePhaseSweep` helper is implemented in plan 03; running this
5
+ * file before plan 03 lands MUST produce 6 failed tests with
6
+ * 'not yet implemented — REL-02' style messages.
7
+ *
8
+ * Behaviour under test:
9
+ * - union: commit list = git-discovered phase-dir paths UNION
10
+ * executor-reported modified_files (deduplicated)
11
+ * - underreport defence: empty modified_files still commits the
12
+ * git-discovered phase-dir paths
13
+ * - scope filter — sibling phase: dirty file in a different phase
14
+ * dir is NOT included
15
+ * - scope filter — out-of-scope dirs: dirty files in ideas/, specs/,
16
+ * and project root are NOT included
17
+ * - reported-path escape: a modified_files entry pointing outside the
18
+ * phase dir is filtered out (or surfaced via dropped_out_of_scope)
19
+ * - idempotency: running the helper twice on the same fixture returns
20
+ * identical results
21
+ *
22
+ * Conventions:
23
+ * - Uses node:test runner + node:assert (matches state-transition-gate.test.cjs)
24
+ * - Each test creates and tears down its own temp git repo via os.tmpdir()
25
+ * - Until plan 03 lands, every test fails with 'not yet implemented'
26
+ * so the file is RED in a controlled way (no parse errors).
27
+ */
28
+
29
+ const test = require('node:test');
30
+ const assert = require('node:assert');
31
+ const fs = require('fs');
32
+ const os = require('os');
33
+ const path = require('path');
34
+ const { execSync } = require('child_process');
35
+
36
+ const NOT_IMPL = 'computePhaseSweep not yet implemented — REL-02';
37
+
38
+ // ─── Helpers ──────────────────────────────────────────────────────────────
39
+
40
+ function tryRequireCommands() {
41
+ try {
42
+ // Reset the planning-root cache between tests so each
43
+ // makeTempPlanningRoot() sees a fresh root; getPlanningRoot caches
44
+ // per-process and would otherwise pin the first temp dir for the
45
+ // whole test run.
46
+ try { require('./paths.cjs').resetPaths(); } catch { /* ignore */ }
47
+ return require('./commands.cjs');
48
+ } catch (err) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function makeTempPlanningRoot() {
54
+ // Resolve real path to handle symlink wrap on macOS (/tmp -> /private/tmp).
55
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'sweep-scope-test-')));
56
+ execSync('git init --quiet', { cwd: dir });
57
+ execSync('git config user.email test@example.com', { cwd: dir });
58
+ execSync('git config user.name "Test User"', { cwd: dir });
59
+ // Seed an initial commit so HEAD exists
60
+ fs.writeFileSync(path.join(dir, 'README.md'), '# seed\n');
61
+ execSync('git add README.md', { cwd: dir });
62
+ execSync('git commit --quiet -m "seed"', { cwd: dir });
63
+ return dir;
64
+ }
65
+
66
+ function cleanupRoot(dir) {
67
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
68
+ }
69
+
70
+ function writeFile(root, rel, content) {
71
+ const abs = path.join(root, rel);
72
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
73
+ fs.writeFileSync(abs, content);
74
+ }
75
+
76
+ // ─── Test 1: union ────────────────────────────────────────────────────────
77
+
78
+ test('REL-02 union: commit list = git-discovered phase-dir paths UNION executor-reported modified_files', () => {
79
+ const cmds = tryRequireCommands();
80
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
81
+ assert.fail(NOT_IMPL);
82
+ return;
83
+ }
84
+ const root = makeTempPlanningRoot();
85
+ try {
86
+ const phasesDir = 'phases';
87
+ const phaseDir = '156-test';
88
+ // git-dirty: A and B exist in the phase dir, untracked
89
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
90
+ writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
91
+ // C is reported by the executor but does not exist on disk yet
92
+ writeFile(root, `${phasesDir}/${phaseDir}/C.md`, 'c\n');
93
+ const result = cmds.computePhaseSweep(root, {
94
+ phasesDir,
95
+ phaseDir,
96
+ modifiedFiles: [
97
+ `${phasesDir}/${phaseDir}/B.md`,
98
+ `${phasesDir}/${phaseDir}/C.md`,
99
+ ],
100
+ }, true);
101
+ const swept = (result && result.swept) || [];
102
+ const expected = [
103
+ `${phasesDir}/${phaseDir}/A.md`,
104
+ `${phasesDir}/${phaseDir}/B.md`,
105
+ `${phasesDir}/${phaseDir}/C.md`,
106
+ ].sort();
107
+ assert.deepStrictEqual(swept.slice().sort(), expected, 'union must equal A,B,C');
108
+ } finally {
109
+ cleanupRoot(root);
110
+ }
111
+ });
112
+
113
+ // ─── Test 2: underreport defence ──────────────────────────────────────────
114
+
115
+ test('REL-02 underreport: executor returns empty modified_files but git-discovered phase-dir paths still committed', () => {
116
+ const cmds = tryRequireCommands();
117
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
118
+ assert.fail(NOT_IMPL);
119
+ return;
120
+ }
121
+ const root = makeTempPlanningRoot();
122
+ try {
123
+ const phasesDir = 'phases';
124
+ const phaseDir = '156-test';
125
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
126
+ writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
127
+ const result = cmds.computePhaseSweep(root, {
128
+ phasesDir,
129
+ phaseDir,
130
+ modifiedFiles: [],
131
+ }, true);
132
+ const swept = (result && result.swept) || [];
133
+ assert.strictEqual(swept.length, 2, 'expected 2 git-dirty files');
134
+ assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
135
+ assert.ok(swept.includes(`${phasesDir}/${phaseDir}/B.md`));
136
+ } finally {
137
+ cleanupRoot(root);
138
+ }
139
+ });
140
+
141
+ // ─── Test 3: scope filter — sibling phase ─────────────────────────────────
142
+
143
+ test('REL-02 scope filter: dirty file in sibling phase dir is NOT included', () => {
144
+ const cmds = tryRequireCommands();
145
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
146
+ assert.fail(NOT_IMPL);
147
+ return;
148
+ }
149
+ const root = makeTempPlanningRoot();
150
+ try {
151
+ const phasesDir = 'phases';
152
+ const phaseDir = '156-test';
153
+ const sibling = '155-other';
154
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
155
+ writeFile(root, `${phasesDir}/${sibling}/X.md`, 'x\n');
156
+ const result = cmds.computePhaseSweep(root, {
157
+ phasesDir,
158
+ phaseDir,
159
+ modifiedFiles: [],
160
+ }, true);
161
+ const swept = (result && result.swept) || [];
162
+ assert.strictEqual(swept.length, 1);
163
+ assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
164
+ assert.ok(!swept.some(p => p.includes(sibling)),
165
+ 'sibling phase paths must NOT appear in swept');
166
+ } finally {
167
+ cleanupRoot(root);
168
+ }
169
+ });
170
+
171
+ // ─── Test 4: scope filter — ideas / specs / project root ──────────────────
172
+
173
+ test('REL-02 scope filter: dirty file in ideas/, specs/, project root is NOT included', () => {
174
+ const cmds = tryRequireCommands();
175
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
176
+ assert.fail(NOT_IMPL);
177
+ return;
178
+ }
179
+ const root = makeTempPlanningRoot();
180
+ try {
181
+ const phasesDir = 'phases';
182
+ const phaseDir = '156-test';
183
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
184
+ writeFile(root, 'ideas/foo.md', 'idea\n');
185
+ writeFile(root, 'specs/bar.md', 'spec\n');
186
+ writeFile(root, 'NOTES.md', 'top-level\n');
187
+ const result = cmds.computePhaseSweep(root, {
188
+ phasesDir,
189
+ phaseDir,
190
+ modifiedFiles: [],
191
+ }, true);
192
+ const swept = (result && result.swept) || [];
193
+ assert.strictEqual(swept.length, 1, 'only the in-scope phase file should be swept');
194
+ assert.ok(swept.includes(`${phasesDir}/${phaseDir}/A.md`));
195
+ for (const out of ['ideas/foo.md', 'specs/bar.md', 'NOTES.md']) {
196
+ assert.ok(!swept.includes(out), `${out} must NOT be swept`);
197
+ }
198
+ } finally {
199
+ cleanupRoot(root);
200
+ }
201
+ });
202
+
203
+ // ─── Test 5: reported-path escape ─────────────────────────────────────────
204
+
205
+ test('REL-02 reported-path escape: a modified_files entry that points outside the phase dir is filtered out', () => {
206
+ const cmds = tryRequireCommands();
207
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
208
+ assert.fail(NOT_IMPL);
209
+ return;
210
+ }
211
+ const root = makeTempPlanningRoot();
212
+ try {
213
+ const phasesDir = 'phases';
214
+ const phaseDir = '156-test';
215
+ const sibling = '155-other';
216
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
217
+ writeFile(root, `${phasesDir}/${sibling}/X.md`, 'x\n');
218
+ const result = cmds.computePhaseSweep(root, {
219
+ phasesDir,
220
+ phaseDir,
221
+ modifiedFiles: [
222
+ `${phasesDir}/${phaseDir}/A.md`,
223
+ `${phasesDir}/${sibling}/X.md`, // escape attempt
224
+ ],
225
+ }, true);
226
+ const swept = (result && result.swept) || [];
227
+ assert.strictEqual(swept.length, 1, 'only the in-scope path should be swept');
228
+ assert.ok(!swept.includes(`${phasesDir}/${sibling}/X.md`),
229
+ 'sibling phase path must be filtered out of swept');
230
+ // Implementations may or may not surface a dropped_out_of_scope array;
231
+ // the contract is only that the dropped path is NOT in `swept`.
232
+ } finally {
233
+ cleanupRoot(root);
234
+ }
235
+ });
236
+
237
+ // ─── Test 6: idempotency ──────────────────────────────────────────────────
238
+
239
+ test('REL-02 idempotency: running the sweep twice on the same fixture returns the same set', () => {
240
+ const cmds = tryRequireCommands();
241
+ if (!cmds || typeof cmds.computePhaseSweep !== 'function') {
242
+ assert.fail(NOT_IMPL);
243
+ return;
244
+ }
245
+ const root = makeTempPlanningRoot();
246
+ try {
247
+ const phasesDir = 'phases';
248
+ const phaseDir = '156-test';
249
+ writeFile(root, `${phasesDir}/${phaseDir}/A.md`, 'a\n');
250
+ writeFile(root, `${phasesDir}/${phaseDir}/B.md`, 'b\n');
251
+ const opts = {
252
+ phasesDir,
253
+ phaseDir,
254
+ modifiedFiles: [`${phasesDir}/${phaseDir}/B.md`],
255
+ };
256
+ const r1 = cmds.computePhaseSweep(root, opts, true);
257
+ const r2 = cmds.computePhaseSweep(root, opts, true);
258
+ assert.deepStrictEqual((r1 && r1.swept) || [], (r2 && r2.swept) || [],
259
+ 'two identical calls must return identical swept sets');
260
+ } finally {
261
+ cleanupRoot(root);
262
+ }
263
+ });
@@ -378,6 +378,75 @@ function pullAll(cwd, options = {}) {
378
378
  * @param {string[]|null} [options.repos=null] - Filter to push only specific repos by name
379
379
  * @returns {{ ok: boolean, results: Array<{ repo: string, path: string, status: string, commits: number|null, message: string }>, problems?: Array, summary: string }}
380
380
  */
381
+ /**
382
+ * Push active worktree branches to remote as backup. Best-effort — failures
383
+ * are appended to results but do not block other sync operations.
384
+ *
385
+ * Reads worktree entries from config.local.json for the current project.
386
+ * Skips stale entries where the worktree directory no longer exists.
387
+ * Push-only — no pull of worktree branches (deferred to future).
388
+ *
389
+ * @param {string} cwd - Planning root
390
+ * @param {Array} results - Results array to append push outcomes to
391
+ */
392
+ function _pushWorktreeBranches(cwd, results) {
393
+ const config = loadConfig(cwd);
394
+ const project = config.current_project;
395
+ if (!project) return;
396
+
397
+ let localConfig;
398
+ try {
399
+ const localPath = getLocalConfigPath(cwd);
400
+ if (!fs.existsSync(localPath)) return;
401
+ localConfig = JSON.parse(fs.readFileSync(localPath, 'utf-8'));
402
+ } catch {
403
+ return; // No config.local.json or invalid JSON — nothing to push
404
+ }
405
+
406
+ const worktrees = (localConfig.projects && localConfig.projects[project] && localConfig.projects[project].worktrees) || {};
407
+
408
+ for (const [slug, entry] of Object.entries(worktrees)) {
409
+ const type = entry.type; // 'milestone' or 'quick'
410
+ const repos = entry.repos || {};
411
+
412
+ for (const [repoName, worktreePath] of Object.entries(repos)) {
413
+ // Skip stale worktrees (directory doesn't exist)
414
+ if (!fs.existsSync(worktreePath)) continue;
415
+
416
+ // Determine branch name based on type
417
+ const branchName = type === 'milestone' ? 'milestone/' + slug : 'quick/' + slug;
418
+ const label = repoName + ' (worktree: ' + slug + ')';
419
+
420
+ try {
421
+ // Check if worktree has a remote
422
+ if (!hasRemote(worktreePath)) continue;
423
+
424
+ // Push the worktree branch from the worktree directory
425
+ const pushResult = execGitWithTimeout(worktreePath, ['push', '-u', 'origin', branchName], 30000);
426
+
427
+ results.push({
428
+ repo: label,
429
+ path: worktreePath,
430
+ status: pushResult.exitCode === 0 ? 'pushed' : 'failed',
431
+ commits: 0,
432
+ message: pushResult.exitCode === 0
433
+ ? 'Pushed worktree branch ' + branchName
434
+ : 'Failed to push ' + branchName + ': ' + (pushResult.stderr || '').trim(),
435
+ });
436
+ } catch (e) {
437
+ // Best-effort — failure does not block other operations
438
+ results.push({
439
+ repo: label,
440
+ path: worktreePath,
441
+ status: 'failed',
442
+ commits: 0,
443
+ message: 'Push failed: ' + (e.message || String(e)),
444
+ });
445
+ }
446
+ }
447
+ }
448
+ }
449
+
381
450
  function pushAll(cwd, options = {}) {
382
451
  const { dryRun = false, force = false, repos: repoFilter = null } = options;
383
452
  let allRepos = collectSyncRepos(cwd);
@@ -468,6 +537,12 @@ function pushAll(cwd, options = {}) {
468
537
  results.push({ repo: label, path: repo.path, status: 'pushed', commits: commitCount, message: `Pushed ${commitLabel}` });
469
538
  }
470
539
 
540
+ // --- Worktree Branch Push ---
541
+ // Push active worktree branches as remote backup (best-effort, non-blocking)
542
+ if (!dryRun) {
543
+ _pushWorktreeBranches(cwd, results);
544
+ }
545
+
471
546
  // Build summary
472
547
  const pushed = results.filter(r => r.status === 'pushed').length;
473
548
  const current = results.filter(r => r.status === 'current').length;