@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.
- package/CHANGELOG.md +197 -0
- package/README.md +34 -2
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +61 -3
- package/agents/dgs-planner.md +51 -8
- package/bin/install.js +44 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +4 -3
- package/commands/dgs/capture-principle.md +11 -11
- package/commands/dgs/cleanup.md +2 -2
- package/commands/dgs/complete-milestone.md +11 -11
- package/commands/dgs/complete-quick.md +28 -0
- package/commands/dgs/create-milestone-job.md +2 -2
- package/commands/dgs/debug.md +3 -3
- package/commands/dgs/develop-idea.md +1 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/fast.md +3 -1
- package/commands/dgs/health.md +1 -1
- package/commands/dgs/map-codebase.md +6 -6
- package/commands/dgs/new-milestone.md +5 -5
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/plan-milestone-gaps.md +1 -1
- package/commands/dgs/progress.md +3 -3
- package/commands/dgs/quick-abandon.md +8 -0
- package/commands/dgs/quick-complete.md +8 -0
- package/commands/dgs/quick.md +10 -3
- package/commands/dgs/research-idea.md +3 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +14 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
- package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
- package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
- package/deliver-great-systems/bin/lib/commands.cjs +626 -46
- package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +80 -6
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +35 -14
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
- package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
- package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
- package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
- package/deliver-great-systems/bin/lib/governance.cjs +211 -0
- package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
- package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
- package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
- package/deliver-great-systems/bin/lib/init.cjs +357 -61
- package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
- package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
- package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
- package/deliver-great-systems/bin/lib/migration.cjs +409 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
- package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
- package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
- package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
- package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
- package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
- package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
- package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
- package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
- package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
- package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
- package/deliver-great-systems/bin/lib/phase.cjs +146 -3
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +65 -10
- package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
- package/deliver-great-systems/bin/lib/quick.cjs +739 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
- package/deliver-great-systems/bin/lib/repos.cjs +37 -13
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
- package/deliver-great-systems/bin/lib/specs.cjs +3 -81
- package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
- package/deliver-great-systems/bin/lib/state.cjs +147 -55
- package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
- package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
- package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
- package/deliver-great-systems/bin/lib/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +198 -7
- package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
- package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
- package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
- package/deliver-great-systems/references/agent-step-reliability.md +60 -0
- package/deliver-great-systems/references/conflict-resolution.md +4 -0
- package/deliver-great-systems/references/context-tiers.md +4 -0
- package/deliver-great-systems/references/package-scan-config.md +151 -0
- package/deliver-great-systems/references/questioning.md +0 -30
- package/deliver-great-systems/references/spec-review-loop.md +1 -2
- package/deliver-great-systems/references/workflow-conventions.md +29 -0
- package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
- package/deliver-great-systems/templates/REVIEW.md +35 -0
- package/deliver-great-systems/templates/VALIDATION.md +1 -1
- package/deliver-great-systems/templates/claude-md.md +27 -0
- package/deliver-great-systems/templates/package-scan-report.md +108 -0
- package/deliver-great-systems/templates/project.md +6 -170
- package/deliver-great-systems/templates/summary.md +3 -1
- package/deliver-great-systems/workflows/abandon-quick.md +89 -0
- package/deliver-great-systems/workflows/add-idea.md +3 -3
- package/deliver-great-systems/workflows/add-phase.md +5 -0
- package/deliver-great-systems/workflows/add-tests.md +14 -0
- package/deliver-great-systems/workflows/add-todo.md +1 -0
- package/deliver-great-systems/workflows/approve-spec.md +25 -4
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/audit-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +2 -2
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +218 -24
- package/deliver-great-systems/workflows/complete-quick.md +106 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
- package/deliver-great-systems/workflows/develop-idea.md +11 -11
- package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
- package/deliver-great-systems/workflows/discuss-idea.md +1 -1
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +209 -33
- package/deliver-great-systems/workflows/execute-plan.md +22 -22
- package/deliver-great-systems/workflows/help.md +53 -20
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +45 -167
- package/deliver-great-systems/workflows/new-milestone.md +140 -33
- package/deliver-great-systems/workflows/new-project.md +60 -331
- package/deliver-great-systems/workflows/package-scan.md +59 -0
- package/deliver-great-systems/workflows/plan-phase.md +79 -1
- package/deliver-great-systems/workflows/progress-all.md +133 -0
- package/deliver-great-systems/workflows/quick-abandon.md +89 -0
- package/deliver-great-systems/workflows/quick-complete.md +106 -0
- package/deliver-great-systems/workflows/quick.md +328 -26
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +77 -139
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +29 -43
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/validate-phase.md +39 -1
- package/deliver-great-systems/workflows/verify-work.md +14 -0
- package/deliver-great-systems/workflows/write-spec.md +11 -13
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- 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;
|