nubos-pilot 0.1.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/agents/np-ai-researcher.md +140 -0
- package/agents/np-code-fixer.md +363 -0
- package/agents/np-code-reviewer.md +351 -0
- package/agents/np-domain-researcher.md +136 -0
- package/agents/np-eval-auditor.md +167 -0
- package/agents/np-eval-planner.md +153 -0
- package/agents/np-executor.md +72 -0
- package/agents/np-framework-selector.md +171 -0
- package/agents/np-nyquist-auditor.md +185 -0
- package/agents/np-plan-checker.md +165 -0
- package/agents/np-planner.md +199 -0
- package/agents/np-researcher.md +150 -0
- package/agents/np-security-auditor.md +206 -0
- package/agents/np-ui-auditor.md +369 -0
- package/agents/np-ui-checker.md +192 -0
- package/agents/np-ui-researcher.md +324 -0
- package/agents/np-verifier.md +79 -0
- package/bin/check-coverage.cjs +40 -0
- package/bin/check-workflows.cjs +171 -0
- package/bin/check-workflows.test.cjs +208 -0
- package/bin/install.js +500 -0
- package/bin/np-tools/_commands.cjs +70 -0
- package/bin/np-tools/add-tests.cjs +171 -0
- package/bin/np-tools/add-tests.test.cjs +122 -0
- package/bin/np-tools/add-todo.cjs +108 -0
- package/bin/np-tools/add-todo.test.cjs +112 -0
- package/bin/np-tools/agent-skills.cjs +14 -0
- package/bin/np-tools/agent-skills.test.cjs +42 -0
- package/bin/np-tools/ai-integration-phase.cjs +109 -0
- package/bin/np-tools/ai-integration-phase.test.cjs +123 -0
- package/bin/np-tools/askuser.cjs +53 -0
- package/bin/np-tools/askuser.test.cjs +49 -0
- package/bin/np-tools/autonomous.cjs +69 -0
- package/bin/np-tools/autonomous.test.cjs +74 -0
- package/bin/np-tools/checkpoint.cjs +101 -0
- package/bin/np-tools/checkpoint.test.cjs +119 -0
- package/bin/np-tools/code-review.cjs +133 -0
- package/bin/np-tools/code-review.test.cjs +96 -0
- package/bin/np-tools/commit-task.cjs +120 -0
- package/bin/np-tools/commit-task.test.cjs +160 -0
- package/bin/np-tools/commit.cjs +103 -0
- package/bin/np-tools/commit.test.cjs +93 -0
- package/bin/np-tools/config.cjs +101 -0
- package/bin/np-tools/config.test.cjs +71 -0
- package/bin/np-tools/discuss-phase-power.cjs +265 -0
- package/bin/np-tools/discuss-phase-power.test.cjs +242 -0
- package/bin/np-tools/discuss-phase.cjs +132 -0
- package/bin/np-tools/discuss-phase.test.cjs +148 -0
- package/bin/np-tools/dispatch.cjs +116 -0
- package/bin/np-tools/doctor.cjs +242 -0
- package/bin/np-tools/eval-review.cjs +116 -0
- package/bin/np-tools/eval-review.test.cjs +123 -0
- package/bin/np-tools/execute-phase.cjs +182 -0
- package/bin/np-tools/execute-phase.test.cjs +116 -0
- package/bin/np-tools/execute-plan.cjs +124 -0
- package/bin/np-tools/execute-plan.test.cjs +82 -0
- package/bin/np-tools/help.cjs +28 -0
- package/bin/np-tools/help.test.cjs +29 -0
- package/bin/np-tools/init-dispatch.test.cjs +91 -0
- package/bin/np-tools/metrics.cjs +97 -0
- package/bin/np-tools/metrics.test.cjs +188 -0
- package/bin/np-tools/new-milestone.cjs +288 -0
- package/bin/np-tools/new-milestone.test.cjs +166 -0
- package/bin/np-tools/new-project.cjs +284 -0
- package/bin/np-tools/new-project.test.cjs +165 -0
- package/bin/np-tools/next.cjs +7 -0
- package/bin/np-tools/next.test.cjs +30 -0
- package/bin/np-tools/park.cjs +48 -0
- package/bin/np-tools/park.test.cjs +50 -0
- package/bin/np-tools/pause-work.cjs +24 -0
- package/bin/np-tools/pause-work.test.cjs +74 -0
- package/bin/np-tools/phase.cjs +71 -0
- package/bin/np-tools/phase.test.cjs +81 -0
- package/bin/np-tools/plan-diff.cjs +57 -0
- package/bin/np-tools/plan-diff.test.cjs +134 -0
- package/bin/np-tools/plan-milestone-gaps.cjs +115 -0
- package/bin/np-tools/plan-milestone-gaps.test.cjs +122 -0
- package/bin/np-tools/plan-phase.cjs +350 -0
- package/bin/np-tools/plan-phase.test.cjs +263 -0
- package/bin/np-tools/progress.cjs +7 -0
- package/bin/np-tools/progress.test.cjs +44 -0
- package/bin/np-tools/queue.cjs +213 -0
- package/bin/np-tools/research-phase.cjs +144 -0
- package/bin/np-tools/research-phase.test.cjs +154 -0
- package/bin/np-tools/reset-slice.cjs +17 -0
- package/bin/np-tools/reset-slice.test.cjs +96 -0
- package/bin/np-tools/resolve-model.cjs +110 -0
- package/bin/np-tools/resolve-model.test.cjs +200 -0
- package/bin/np-tools/resume-work.cjs +76 -0
- package/bin/np-tools/resume-work.test.cjs +91 -0
- package/bin/np-tools/skip.cjs +48 -0
- package/bin/np-tools/skip.test.cjs +66 -0
- package/bin/np-tools/slug.cjs +34 -0
- package/bin/np-tools/slug.test.cjs +46 -0
- package/bin/np-tools/state.cjs +16 -0
- package/bin/np-tools/state.test.cjs +40 -0
- package/bin/np-tools/stats.cjs +151 -0
- package/bin/np-tools/stats.test.cjs +118 -0
- package/bin/np-tools/triage.cjs +128 -0
- package/bin/np-tools/ui-phase.cjs +108 -0
- package/bin/np-tools/ui-phase.test.cjs +121 -0
- package/bin/np-tools/ui-review.cjs +108 -0
- package/bin/np-tools/ui-review.test.cjs +120 -0
- package/bin/np-tools/undo-task.cjs +31 -0
- package/bin/np-tools/undo-task.test.cjs +117 -0
- package/bin/np-tools/undo.cjs +43 -0
- package/bin/np-tools/undo.test.cjs +120 -0
- package/bin/np-tools/unpark.cjs +48 -0
- package/bin/np-tools/unpark.test.cjs +50 -0
- package/bin/np-tools/verify-work.cjs +186 -0
- package/bin/np-tools/verify-work.test.cjs +97 -0
- package/docs/adr/0001-no-daemon-invariant.md +82 -0
- package/docs/adr/0002-zero-runtime-dependencies.md +90 -0
- package/docs/adr/0003-max-six-unit-types.md +85 -0
- package/docs/adr/0004-atomic-commit-per-unit.md +102 -0
- package/docs/adr/0005-three-orthogonal-file-trees.md +98 -0
- package/docs/adr/0006-yaml-dependency-amendment.md +60 -0
- package/docs/adr/README.md +27 -0
- package/docs/agent-frontmatter-schema.md +84 -0
- package/docs/phase-artifact-schemas.md +292 -0
- package/docs/phase-directory-layout.md +82 -0
- package/lib/__tests__/README.md +1 -0
- package/lib/agents.cjs +98 -0
- package/lib/agents.test.cjs +286 -0
- package/lib/askuser.cjs +36 -0
- package/lib/askuser.test.cjs +310 -0
- package/lib/checkpoint.cjs +135 -0
- package/lib/checkpoint.test.cjs +184 -0
- package/lib/core.cjs +165 -0
- package/lib/core.test.cjs +405 -0
- package/lib/fixtures/README.md +1 -0
- package/lib/fixtures/phase-tree/README.md +1 -0
- package/lib/fixtures/plans/cycle/PLAN.md +16 -0
- package/lib/fixtures/plans/cycle/tasks/T-01.md +20 -0
- package/lib/fixtures/plans/cycle/tasks/T-02.md +20 -0
- package/lib/fixtures/plans/cycle/tasks/T-03.md +20 -0
- package/lib/fixtures/plans/linear/PLAN.md +16 -0
- package/lib/fixtures/plans/linear/tasks/T-01.md +20 -0
- package/lib/fixtures/plans/linear/tasks/T-02.md +20 -0
- package/lib/fixtures/plans/linear/tasks/T-03.md +20 -0
- package/lib/fixtures/plans/parallel/PLAN.md +16 -0
- package/lib/fixtures/plans/parallel/tasks/T-01.md +20 -0
- package/lib/fixtures/plans/parallel/tasks/T-02.md +20 -0
- package/lib/fixtures/plans/parallel/tasks/T-03.md +20 -0
- package/lib/fixtures/plans/wave-conflict/PLAN.md +16 -0
- package/lib/fixtures/plans/wave-conflict/tasks/T-01.md +20 -0
- package/lib/fixtures/plans/wave-conflict/tasks/T-02.md +20 -0
- package/lib/fixtures/roadmap/ROADMAP-malformed.md +3 -0
- package/lib/fixtures/roadmap/ROADMAP-minimal.md +51 -0
- package/lib/fixtures/roadmap/roadmap-malformed.yaml +7 -0
- package/lib/fixtures/roadmap/roadmap-minimal.yaml +40 -0
- package/lib/fixtures/roadmap/roadmap-ten-phases.yaml +101 -0
- package/lib/fixtures/templates/phase-context.md +6 -0
- package/lib/fixtures/templates/plan-skeleton.md +6 -0
- package/lib/frontmatter.cjs +251 -0
- package/lib/frontmatter.test.cjs +177 -0
- package/lib/gaps.cjs +197 -0
- package/lib/gaps.test.cjs +200 -0
- package/lib/git.cjs +207 -0
- package/lib/git.test.cjs +305 -0
- package/lib/install/agents-md.cjs +77 -0
- package/lib/install/backup.cjs +70 -0
- package/lib/install/codex-toml.cjs +440 -0
- package/lib/install/managed-block.cjs +30 -0
- package/lib/install/manifest.cjs +148 -0
- package/lib/install/mcp-writer.cjs +127 -0
- package/lib/install/runtime-detect.cjs +44 -0
- package/lib/install/staging.cjs +149 -0
- package/lib/metrics-aggregate.cjs +229 -0
- package/lib/metrics-aggregate.test.cjs +192 -0
- package/lib/metrics.cjs +120 -0
- package/lib/metrics.test.cjs +182 -0
- package/lib/model-aliases.regression.test.cjs +16 -0
- package/lib/model-profiles.cjs +42 -0
- package/lib/model-profiles.test.cjs +61 -0
- package/lib/next.cjs +236 -0
- package/lib/next.test.cjs +194 -0
- package/lib/phase.cjs +95 -0
- package/lib/phase.test.cjs +189 -0
- package/lib/plan-checker-contract.test.cjs +72 -0
- package/lib/plan-diff.cjs +173 -0
- package/lib/plan-diff.test.cjs +217 -0
- package/lib/plan.cjs +85 -0
- package/lib/plan.test.cjs +263 -0
- package/lib/progress.cjs +95 -0
- package/lib/progress.test.cjs +116 -0
- package/lib/researcher-contract.test.cjs +61 -0
- package/lib/roadmap-render.cjs +206 -0
- package/lib/roadmap-render.test.cjs +121 -0
- package/lib/roadmap.cjs +416 -0
- package/lib/roadmap.test.cjs +371 -0
- package/lib/runtime/_contract.test.cjs +61 -0
- package/lib/runtime/_readline.cjs +119 -0
- package/lib/runtime/_readline.test.cjs +126 -0
- package/lib/runtime/claude.cjs +48 -0
- package/lib/runtime/claude.test.cjs +101 -0
- package/lib/runtime/codex.cjs +35 -0
- package/lib/runtime/codex.test.cjs +114 -0
- package/lib/runtime/gemini.cjs +35 -0
- package/lib/runtime/gemini.test.cjs +109 -0
- package/lib/runtime/index.cjs +49 -0
- package/lib/runtime/index.test.cjs +181 -0
- package/lib/runtime/opencode.cjs +35 -0
- package/lib/runtime/opencode.test.cjs +124 -0
- package/lib/state.cjs +205 -0
- package/lib/state.test.cjs +264 -0
- package/lib/surface-audit.test.cjs +46 -0
- package/lib/tasks.cjs +327 -0
- package/lib/tasks.test.cjs +389 -0
- package/lib/template.cjs +66 -0
- package/lib/template.test.cjs +159 -0
- package/lib/undo.cjs +179 -0
- package/lib/undo.test.cjs +261 -0
- package/lib/verify.cjs +116 -0
- package/lib/verify.test.cjs +187 -0
- package/np-tools.cjs +303 -0
- package/package.json +39 -0
- package/templates/AI-SPEC.md +90 -0
- package/templates/CONTEXT.md +32 -0
- package/templates/PLAN.md +69 -0
- package/templates/PROJECT.md +60 -0
- package/templates/REQUIREMENTS.md +38 -0
- package/templates/SECURITY.md +61 -0
- package/templates/UI-SPEC.md +64 -0
- package/templates/VALIDATION.md +76 -0
- package/templates/claude/payload/README.md +11 -0
- package/templates/opencode/opencode.json +6 -0
- package/templates/opencode/payload/AGENTS.md +9 -0
- package/workflows/add-backlog.md +212 -0
- package/workflows/add-tests.md +69 -0
- package/workflows/add-todo.md +222 -0
- package/workflows/ai-integration-phase.md +230 -0
- package/workflows/autonomous.md +94 -0
- package/workflows/cleanup.md +325 -0
- package/workflows/code-review-fix.md +435 -0
- package/workflows/code-review.md +447 -0
- package/workflows/discuss-phase-assumptions.md +269 -0
- package/workflows/discuss-phase-power.md +139 -0
- package/workflows/discuss-phase.md +386 -0
- package/workflows/dispatch.md +9 -0
- package/workflows/doctor.md +10 -0
- package/workflows/eval-review.md +243 -0
- package/workflows/execute-phase.md +142 -0
- package/workflows/execute-plan.md +82 -0
- package/workflows/help.md +8 -0
- package/workflows/new-milestone.md +166 -0
- package/workflows/new-project.md +213 -0
- package/workflows/next.md +8 -0
- package/workflows/note.md +244 -0
- package/workflows/park.md +29 -0
- package/workflows/pause-work.md +34 -0
- package/workflows/plan-milestone-gaps.md +233 -0
- package/workflows/plan-phase.md +351 -0
- package/workflows/progress.md +8 -0
- package/workflows/queue.md +9 -0
- package/workflows/research-phase.md +327 -0
- package/workflows/reset-slice.md +39 -0
- package/workflows/resume-work.md +79 -0
- package/workflows/review.md +489 -0
- package/workflows/secure-phase.md +209 -0
- package/workflows/session-report.md +243 -0
- package/workflows/skip.md +29 -0
- package/workflows/state.md +7 -0
- package/workflows/stats.md +170 -0
- package/workflows/thread.md +214 -0
- package/workflows/triage.md +9 -0
- package/workflows/ui-phase.md +246 -0
- package/workflows/ui-review.md +222 -0
- package/workflows/undo-task.md +42 -0
- package/workflows/undo.md +55 -0
- package/workflows/unpark.md +29 -0
- package/workflows/validate-phase.md +231 -0
- package/workflows/verify-work.md +83 -0
package/lib/template.cjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { projectStateDir, NubosPilotError } = require('./core.cjs');
|
|
4
|
+
|
|
5
|
+
const PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
|
6
|
+
|
|
7
|
+
function templatesDir(cwd) {
|
|
8
|
+
return path.join(projectStateDir(cwd), 'templates');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function loadTemplate(name, vars, cwd = process.cwd()) {
|
|
12
|
+
const dir = templatesDir(cwd);
|
|
13
|
+
const filePath = path.resolve(dir, name + '.md');
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const dirWithSep = dir.endsWith(path.sep) ? dir : dir + path.sep;
|
|
17
|
+
if (!filePath.startsWith(dirWithSep)) {
|
|
18
|
+
throw new NubosPilotError(
|
|
19
|
+
'template-not-found',
|
|
20
|
+
`Template name "${name}" escapes templates directory`,
|
|
21
|
+
{ template: name, path: filePath },
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = fs.readFileSync(filePath, 'utf-8');
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err && err.code === 'ENOENT') {
|
|
30
|
+
throw new NubosPilotError(
|
|
31
|
+
'template-not-found',
|
|
32
|
+
`Template "${name}" not found at ${filePath}`,
|
|
33
|
+
{ template: name, path: filePath },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return raw.replace(PLACEHOLDER_RE, (_match, key) => {
|
|
40
|
+
if (!(key in vars)) {
|
|
41
|
+
throw new NubosPilotError(
|
|
42
|
+
'template-unresolved-var',
|
|
43
|
+
`Undefined placeholder {{${key}}} in template "${name}"`,
|
|
44
|
+
{ template: name, variable: key, available: Object.keys(vars) },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return String(vars[key]);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function listTemplates(cwd = process.cwd()) {
|
|
52
|
+
const dir = templatesDir(cwd);
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = fs.readdirSync(dir);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err && err.code === 'ENOENT') return [];
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
return entries
|
|
61
|
+
.filter((f) => f.endsWith('.md'))
|
|
62
|
+
.map((f) => f.slice(0, -3))
|
|
63
|
+
.sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { loadTemplate, listTemplates };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const tpl = require('./template.cjs');
|
|
2
|
+
const { test, afterEach } = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
|
|
8
|
+
const _sandboxes = [];
|
|
9
|
+
|
|
10
|
+
function makeSandbox(templates) {
|
|
11
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'nubos-pilot-tpl-'));
|
|
12
|
+
if (templates) {
|
|
13
|
+
const dir = path.join(root, '.nubos-pilot', 'templates');
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
for (const [name, content] of Object.entries(templates)) {
|
|
16
|
+
fs.writeFileSync(path.join(dir, `${name}.md`), content, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
_sandboxes.push(root);
|
|
22
|
+
return root;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
while (_sandboxes.length) {
|
|
27
|
+
const root = _sandboxes.pop();
|
|
28
|
+
try { fs.rmSync(root, { recursive: true, force: true }); } catch {}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const PHASE_CONTEXT = fs.readFileSync(path.join(__dirname, 'fixtures/templates/phase-context.md'), 'utf-8');
|
|
33
|
+
const PLAN_SKELETON = fs.readFileSync(path.join(__dirname, 'fixtures/templates/plan-skeleton.md'), 'utf-8');
|
|
34
|
+
|
|
35
|
+
test('TPL-1: phase-context with full vars substitutes all placeholders', () => {
|
|
36
|
+
const cwd = makeSandbox({ 'phase-context': PHASE_CONTEXT });
|
|
37
|
+
const out = tpl.loadTemplate('phase-context', {
|
|
38
|
+
phase_number: '03',
|
|
39
|
+
phase_name: 'Core Lib',
|
|
40
|
+
goal: 'Build parsers',
|
|
41
|
+
requirements: 'LIB-03',
|
|
42
|
+
}, cwd);
|
|
43
|
+
assert.match(out, /Phase 03: Core Lib/);
|
|
44
|
+
assert.match(out, /Requirements: LIB-03/);
|
|
45
|
+
assert.ok(!out.includes('{{'), 'no unresolved placeholders');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('TPL-2: repeated {{phase_number}} substitutes in BOTH positions', () => {
|
|
49
|
+
const cwd = makeSandbox({ 'phase-context': PHASE_CONTEXT });
|
|
50
|
+
const out = tpl.loadTemplate('phase-context', {
|
|
51
|
+
phase_number: '03',
|
|
52
|
+
phase_name: 'X',
|
|
53
|
+
goal: 'Y',
|
|
54
|
+
requirements: 'Z',
|
|
55
|
+
}, cwd);
|
|
56
|
+
|
|
57
|
+
const occurrences = (out.match(/03/g) || []).length;
|
|
58
|
+
assert.ok(occurrences >= 2, `expected ≥2 occurrences of "03", got ${occurrences}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('TPL-3: numeric value (3) stringifies to "3"', () => {
|
|
62
|
+
const cwd = makeSandbox({ tiny: 'value={{n}}' });
|
|
63
|
+
const out = tpl.loadTemplate('tiny', { n: 3 }, cwd);
|
|
64
|
+
assert.equal(out, 'value=3');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('TPL-4: boolean true stringifies to "true"', () => {
|
|
68
|
+
const cwd = makeSandbox({ tiny: 'flag={{b}}' });
|
|
69
|
+
const out = tpl.loadTemplate('tiny', { b: true }, cwd);
|
|
70
|
+
assert.equal(out, 'flag=true');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('TPL-5: empty-string value substitutes empty, does NOT throw', () => {
|
|
74
|
+
const cwd = makeSandbox({ tiny: 'x={{e}}|y' });
|
|
75
|
+
const out = tpl.loadTemplate('tiny', { e: '' }, cwd);
|
|
76
|
+
assert.equal(out, 'x=|y');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('TPL-6: template with no placeholders returns content unchanged', () => {
|
|
80
|
+
const content = '# Static\n\nNo placeholders here.\n';
|
|
81
|
+
const cwd = makeSandbox({ static: content });
|
|
82
|
+
const out = tpl.loadTemplate('static', {}, cwd);
|
|
83
|
+
assert.equal(out, content);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('TPL-7: missing var → throws template-unresolved-var with available keys', () => {
|
|
87
|
+
const cwd = makeSandbox({ 'phase-context': PHASE_CONTEXT });
|
|
88
|
+
let thrown = null;
|
|
89
|
+
try {
|
|
90
|
+
tpl.loadTemplate('phase-context', { phase_number: '03' }, cwd);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
thrown = err;
|
|
93
|
+
}
|
|
94
|
+
assert.ok(thrown, 'expected throw');
|
|
95
|
+
assert.equal(thrown.name, 'NubosPilotError');
|
|
96
|
+
assert.equal(thrown.code, 'template-unresolved-var');
|
|
97
|
+
assert.ok(thrown.details, 'details object present');
|
|
98
|
+
assert.equal(thrown.details.template, 'phase-context');
|
|
99
|
+
assert.ok(['phase_name', 'goal', 'requirements'].includes(thrown.details.variable),
|
|
100
|
+
`variable should be one of the missing keys, got ${thrown.details.variable}`);
|
|
101
|
+
assert.deepEqual(thrown.details.available, ['phase_number']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('TPL-8: missing template file → throws template-not-found', () => {
|
|
105
|
+
const cwd = makeSandbox({});
|
|
106
|
+
let thrown = null;
|
|
107
|
+
try {
|
|
108
|
+
tpl.loadTemplate('missing-name', {}, cwd);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
thrown = err;
|
|
111
|
+
}
|
|
112
|
+
assert.ok(thrown, 'expected throw');
|
|
113
|
+
assert.equal(thrown.name, 'NubosPilotError');
|
|
114
|
+
assert.equal(thrown.code, 'template-not-found');
|
|
115
|
+
assert.equal(thrown.details.template, 'missing-name');
|
|
116
|
+
assert.ok(thrown.details.path.endsWith(path.join('templates', 'missing-name.md')),
|
|
117
|
+
`path should end with templates/missing-name.md, got ${thrown.details.path}`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('TPL-9: {{123}} (digit-leading, non-identifier) returns verbatim', () => {
|
|
121
|
+
const cwd = makeSandbox({ edge: 'before {{123}} after' });
|
|
122
|
+
const out = tpl.loadTemplate('edge', {}, cwd);
|
|
123
|
+
assert.equal(out, 'before {{123}} after');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('TPL-10: {{ spaced }} (whitespace around identifier) IS a placeholder', () => {
|
|
127
|
+
const cwd = makeSandbox({ edge: 'x={{ name }}' });
|
|
128
|
+
const out = tpl.loadTemplate('edge', { name: 'ok' }, cwd);
|
|
129
|
+
assert.equal(out, 'x=ok');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('TPL-11: listTemplates returns sorted basenames without .md', () => {
|
|
133
|
+
const cwd = makeSandbox({
|
|
134
|
+
'phase-context': PHASE_CONTEXT,
|
|
135
|
+
'plan-skeleton': PLAN_SKELETON,
|
|
136
|
+
});
|
|
137
|
+
const list = tpl.listTemplates(cwd);
|
|
138
|
+
assert.deepEqual(list, ['phase-context', 'plan-skeleton']);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('TPL-12: listTemplates on sandbox without templates dir returns []', () => {
|
|
142
|
+
const cwd = makeSandbox({});
|
|
143
|
+
const list = tpl.listTemplates(cwd);
|
|
144
|
+
assert.deepEqual(list, []);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('TPL-13: cwd with no .nubos-pilot ancestor → projectStateDir throws not-in-project', () => {
|
|
148
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'nubos-pilot-tpl-noroot-'));
|
|
149
|
+
_sandboxes.push(root);
|
|
150
|
+
let thrown = null;
|
|
151
|
+
try {
|
|
152
|
+
tpl.listTemplates(root);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
thrown = err;
|
|
155
|
+
}
|
|
156
|
+
assert.ok(thrown, 'expected throw');
|
|
157
|
+
assert.equal(thrown.name, 'NubosPilotError');
|
|
158
|
+
assert.equal(thrown.code, 'not-in-project');
|
|
159
|
+
});
|
package/lib/undo.cjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { NubosPilotError, withFileLocks, atomicWriteFileSync, projectStateDir } = require('./core.cjs');
|
|
5
|
+
const { parseState, serializeState } = require('./state.cjs');
|
|
6
|
+
const {
|
|
7
|
+
findCommitByTaskId,
|
|
8
|
+
revertCommit,
|
|
9
|
+
restoreFiles,
|
|
10
|
+
listTaskCommits,
|
|
11
|
+
} = require('./git.cjs');
|
|
12
|
+
const {
|
|
13
|
+
readCheckpoint,
|
|
14
|
+
deleteCheckpoint,
|
|
15
|
+
checkpointPath,
|
|
16
|
+
} = require('./checkpoint.cjs');
|
|
17
|
+
const { setTaskStatus, TASK_ID_RE } = require('./tasks.cjs');
|
|
18
|
+
const { paddedPhase } = require('./phase.cjs');
|
|
19
|
+
|
|
20
|
+
function _statePath(cwd) {
|
|
21
|
+
return path.join(projectStateDir(cwd), 'STATE.md');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _findPlanDirForTask(taskId, cwd) {
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
const phasesRoot = path.join(projectStateDir(cwd), 'phases');
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = fs.readdirSync(phasesRoot, { withFileTypes: true });
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (err && err.code === 'ENOENT') return null;
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
const padded = taskId.slice(0, 2);
|
|
36
|
+
for (const e of entries) {
|
|
37
|
+
if (!e.isDirectory()) continue;
|
|
38
|
+
if (!(e.name === padded || e.name.startsWith(padded + '-'))) continue;
|
|
39
|
+
const candidate = path.join(phasesRoot, e.name, 'tasks', taskId + '.md');
|
|
40
|
+
if (fs.existsSync(candidate)) return path.join(phasesRoot, e.name);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _extractTaskIdFromSubject(subject) {
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
const m = String(subject).match(/^task\(([^)]+)\):/);
|
|
49
|
+
if (!m) return null;
|
|
50
|
+
if (!TASK_ID_RE.test(m[1])) return null;
|
|
51
|
+
return m[1];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function undoTask(id, cwd = process.cwd()) {
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
const sha = findCommitByTaskId(id);
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
revertCommit(sha);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
throw new NubosPilotError(
|
|
65
|
+
'undo-revert-conflict',
|
|
66
|
+
'git revert failed for task ' + id + ' (sha ' + sha + ')',
|
|
67
|
+
{ taskId: id, sha, cause: err && err.message },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const planDir = _findPlanDirForTask(id, cwd);
|
|
71
|
+
if (planDir) {
|
|
72
|
+
try { setTaskStatus(id, 'pending', planDir); }
|
|
73
|
+
catch (err) {
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
process.stderr.write('[nubos-pilot warn] setTaskStatus pending failed for ' + id + ': ' + (err && err.message) + '\n');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { task_id: id, reverted_sha: sha };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resetSlice(cwd = process.cwd()) {
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
const statePath = _statePath(cwd);
|
|
87
|
+
const stateBody = fs.readFileSync(statePath, 'utf-8');
|
|
88
|
+
const state = parseState(stateBody);
|
|
89
|
+
const currentTask = state.frontmatter && state.frontmatter.current_task;
|
|
90
|
+
if (currentTask == null) {
|
|
91
|
+
throw new NubosPilotError(
|
|
92
|
+
'undo-dirty-tree',
|
|
93
|
+
'No current_task in STATE.md — nothing to reset',
|
|
94
|
+
{},
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const cp = readCheckpoint(currentTask, cwd);
|
|
98
|
+
if (!cp) {
|
|
99
|
+
throw new NubosPilotError(
|
|
100
|
+
'checkpoint-orphan',
|
|
101
|
+
'STATE.current_task=' + currentTask + ' but no checkpoint file',
|
|
102
|
+
{ current_task: currentTask },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const files = Array.isArray(cp.files_touched) ? cp.files_touched : [];
|
|
106
|
+
if (files.length > 0) restoreFiles(files);
|
|
107
|
+
|
|
108
|
+
const cpPath = checkpointPath(currentTask, cwd);
|
|
109
|
+
withFileLocks([statePath, cpPath], () => {
|
|
110
|
+
|
|
111
|
+
const fresh = parseState(fs.readFileSync(statePath, 'utf-8'));
|
|
112
|
+
fresh.frontmatter.current_task = null;
|
|
113
|
+
atomicWriteFileSync(statePath, serializeState(fresh));
|
|
114
|
+
try { fs.unlinkSync(cpPath); } catch (err) {
|
|
115
|
+
if (!err || err.code !== 'ENOENT') throw err;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const planDir = _findPlanDirForTask(currentTask, cwd);
|
|
120
|
+
if (planDir) {
|
|
121
|
+
try { setTaskStatus(currentTask, 'pending', planDir); }
|
|
122
|
+
catch (err) {
|
|
123
|
+
process.stderr.write('[nubos-pilot warn] setTaskStatus pending failed for ' + currentTask + ': ' + (err && err.message) + '\n');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { task_id: currentTask, restored_paths: files };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _undoCommitsBatch(prefix, cwd) {
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
const commits = listTaskCommits(prefix);
|
|
134
|
+
if (commits.length === 0) {
|
|
135
|
+
return { reverted: [], pending_count: 0, message: 'nothing to revert' };
|
|
136
|
+
}
|
|
137
|
+
const reverted = [];
|
|
138
|
+
const taskIds = new Set();
|
|
139
|
+
for (const c of commits) {
|
|
140
|
+
revertCommit(c.sha);
|
|
141
|
+
reverted.push({ sha: c.sha, subject: c.subject });
|
|
142
|
+
const tid = _extractTaskIdFromSubject(c.subject);
|
|
143
|
+
if (tid) taskIds.add(tid);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const tid of taskIds) {
|
|
147
|
+
const planDir = _findPlanDirForTask(tid, cwd);
|
|
148
|
+
if (!planDir) continue;
|
|
149
|
+
try { setTaskStatus(tid, 'pending', planDir); }
|
|
150
|
+
catch (err) {
|
|
151
|
+
process.stderr.write('[nubos-pilot warn] setTaskStatus pending failed for ' + tid + ': ' + (err && err.message) + '\n');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { reverted, pending_count: taskIds.size };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function undoPlan(planId, cwd = process.cwd()) {
|
|
158
|
+
if (typeof planId !== 'string' || !/^\d{2}-\d{2}$/.test(planId)) {
|
|
159
|
+
throw new NubosPilotError(
|
|
160
|
+
'undo-invalid-plan-id',
|
|
161
|
+
'undoPlan requires a plan id in NN-NN form, got: ' + planId,
|
|
162
|
+
{ planId },
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return _undoCommitsBatch(planId, cwd);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function undoPhase(n, cwd = process.cwd()) {
|
|
169
|
+
|
|
170
|
+
const padded = paddedPhase(n);
|
|
171
|
+
return _undoCommitsBatch(padded, cwd);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
undoTask,
|
|
176
|
+
undoPlan,
|
|
177
|
+
undoPhase,
|
|
178
|
+
resetSlice,
|
|
179
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const { test, after } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const { execFileSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const undo = require('./undo.cjs');
|
|
9
|
+
const git = require('./git.cjs');
|
|
10
|
+
const cp = require('./checkpoint.cjs');
|
|
11
|
+
|
|
12
|
+
const _repos = [];
|
|
13
|
+
|
|
14
|
+
function makeRepo() {
|
|
15
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-undo-'));
|
|
16
|
+
execFileSync('git', ['init', '-q', '-b', 'main', root], { stdio: 'pipe' });
|
|
17
|
+
execFileSync('git', ['-C', root, 'config', 'user.email', 'test@nubos.local']);
|
|
18
|
+
execFileSync('git', ['-C', root, 'config', 'user.name', 'nubos-test']);
|
|
19
|
+
execFileSync('git', ['-C', root, 'commit', '--allow-empty', '-q', '-m', 'chore: init'], { stdio: 'pipe' });
|
|
20
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), `---
|
|
22
|
+
schema_version: 2
|
|
23
|
+
milestone: m1
|
|
24
|
+
milestone_name: m1
|
|
25
|
+
current_phase: 6
|
|
26
|
+
current_plan: "06-01"
|
|
27
|
+
current_task: null
|
|
28
|
+
last_updated: "2026-04-15T00:00:00Z"
|
|
29
|
+
progress:
|
|
30
|
+
total_phases: 0
|
|
31
|
+
completed_phases: 0
|
|
32
|
+
total_plans: 0
|
|
33
|
+
completed_plans: 0
|
|
34
|
+
percent: 0
|
|
35
|
+
session:
|
|
36
|
+
stopped_at: null
|
|
37
|
+
resume_file: null
|
|
38
|
+
last_activity: null
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# State
|
|
42
|
+
`, 'utf-8');
|
|
43
|
+
_repos.push(root);
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function seedTask(root, planId, taskId, filesModified) {
|
|
48
|
+
const phase = planId.slice(0, 2);
|
|
49
|
+
const phaseDir = path.join(root, '.nubos-pilot', 'phases', phase + '-demo');
|
|
50
|
+
const tasksDir = path.join(phaseDir, 'tasks');
|
|
51
|
+
fs.mkdirSync(tasksDir, { recursive: true });
|
|
52
|
+
const fm = [
|
|
53
|
+
'---',
|
|
54
|
+
`id: ${taskId}`,
|
|
55
|
+
`phase: ${Number(phase)}`,
|
|
56
|
+
`plan: "${planId}"`,
|
|
57
|
+
'type: auto',
|
|
58
|
+
'status: done',
|
|
59
|
+
'tier: sonnet',
|
|
60
|
+
'owner: np-executor',
|
|
61
|
+
'wave: 1',
|
|
62
|
+
'depends_on: []',
|
|
63
|
+
'files_modified:',
|
|
64
|
+
...filesModified.map((f) => ` - ${f}`),
|
|
65
|
+
'autonomous: true',
|
|
66
|
+
'must_haves:',
|
|
67
|
+
' truths: []',
|
|
68
|
+
'---',
|
|
69
|
+
'',
|
|
70
|
+
'# Task: demo',
|
|
71
|
+
].join('\n');
|
|
72
|
+
fs.writeFileSync(path.join(tasksDir, taskId + '.md'), fm, 'utf-8');
|
|
73
|
+
return { phaseDir, planDir: phaseDir };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function commitFor(root, taskId, files) {
|
|
77
|
+
for (const f of files) {
|
|
78
|
+
const abs = path.join(root, f);
|
|
79
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
80
|
+
fs.writeFileSync(abs, 'content for ' + taskId + ' in ' + f, 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
const prev = process.cwd();
|
|
83
|
+
process.chdir(root);
|
|
84
|
+
try {
|
|
85
|
+
git.commitTask(taskId, files, 'task(' + taskId + '): demo');
|
|
86
|
+
} finally {
|
|
87
|
+
process.chdir(prev);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
after(() => {
|
|
92
|
+
while (_repos.length) {
|
|
93
|
+
const r = _repos.pop();
|
|
94
|
+
try { fs.rmSync(r, { recursive: true, force: true }); } catch {}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('UN-1: undoTask reverts the commit and resets task status to pending', () => {
|
|
99
|
+
const root = makeRepo();
|
|
100
|
+
seedTask(root, '06-01', '06-01-T01', ['src/a.ts']);
|
|
101
|
+
const prev = process.cwd();
|
|
102
|
+
process.chdir(root);
|
|
103
|
+
try {
|
|
104
|
+
commitFor(root, '06-01-T01', ['src/a.ts']);
|
|
105
|
+
const before = execFileSync('git', ['log', '--format=%H'], { encoding: 'utf-8' }).trim().split('\n').length;
|
|
106
|
+
const result = undo.undoTask('06-01-T01', root);
|
|
107
|
+
assert.equal(result.task_id, '06-01-T01');
|
|
108
|
+
assert.ok(/^[0-9a-f]{40}$/.test(result.reverted_sha));
|
|
109
|
+
const after = execFileSync('git', ['log', '--format=%H'], { encoding: 'utf-8' }).trim().split('\n').length;
|
|
110
|
+
assert.equal(after, before + 1, 'a new revert commit should exist');
|
|
111
|
+
|
|
112
|
+
const taskFile = path.join(root, '.nubos-pilot', 'phases', '06-demo', 'tasks', '06-01-T01.md');
|
|
113
|
+
assert.match(fs.readFileSync(taskFile, 'utf-8'), /^status: pending$/m);
|
|
114
|
+
} finally {
|
|
115
|
+
process.chdir(prev);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('UN-2: undoTask with malicious task-id format → task-commit-not-found (T-06-17)', () => {
|
|
120
|
+
const root = makeRepo();
|
|
121
|
+
const prev = process.cwd();
|
|
122
|
+
process.chdir(root);
|
|
123
|
+
try {
|
|
124
|
+
assert.throws(
|
|
125
|
+
() => undo.undoTask('06-99; rm -rf /', root),
|
|
126
|
+
(err) => err && err.code === 'task-commit-not-found',
|
|
127
|
+
);
|
|
128
|
+
} finally {
|
|
129
|
+
process.chdir(prev);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('UN-3: resetSlice restores files_touched, deletes checkpoint, resets task status', () => {
|
|
134
|
+
const root = makeRepo();
|
|
135
|
+
seedTask(root, '06-01', '06-01-T03', ['src/c.ts']);
|
|
136
|
+
|
|
137
|
+
const abs = path.join(root, 'src', 'c.ts');
|
|
138
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
139
|
+
fs.writeFileSync(abs, 'baseline\n', 'utf-8');
|
|
140
|
+
const prev = process.cwd();
|
|
141
|
+
process.chdir(root);
|
|
142
|
+
try {
|
|
143
|
+
execFileSync('git', ['add', 'src/c.ts'], { stdio: 'pipe' });
|
|
144
|
+
execFileSync('git', ['commit', '-m', 'baseline', '-q'], { stdio: 'pipe' });
|
|
145
|
+
|
|
146
|
+
cp.startTask({ id: '06-01-T03', phase: 6, plan: '06-01', wave: 1 }, root);
|
|
147
|
+
cp.writeCheckpoint('06-01-T03', { files_touched: ['src/c.ts'] }, root);
|
|
148
|
+
|
|
149
|
+
fs.writeFileSync(abs, 'dirty work\n', 'utf-8');
|
|
150
|
+
const result = undo.resetSlice(root);
|
|
151
|
+
assert.equal(result.task_id, '06-01-T03');
|
|
152
|
+
assert.deepEqual(result.restored_paths, ['src/c.ts']);
|
|
153
|
+
|
|
154
|
+
assert.equal(fs.readFileSync(abs, 'utf-8'), 'baseline\n');
|
|
155
|
+
|
|
156
|
+
assert.equal(cp.readCheckpoint('06-01-T03', root), null);
|
|
157
|
+
|
|
158
|
+
const stateBody = fs.readFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), 'utf-8');
|
|
159
|
+
assert.match(stateBody, /current_task: null/);
|
|
160
|
+
|
|
161
|
+
const taskFile = path.join(root, '.nubos-pilot', 'phases', '06-demo', 'tasks', '06-01-T03.md');
|
|
162
|
+
assert.match(fs.readFileSync(taskFile, 'utf-8'), /^status: pending$/m);
|
|
163
|
+
} finally {
|
|
164
|
+
process.chdir(prev);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('UN-4: resetSlice with no current_task → undo-dirty-tree', () => {
|
|
169
|
+
const root = makeRepo();
|
|
170
|
+
const prev = process.cwd();
|
|
171
|
+
process.chdir(root);
|
|
172
|
+
try {
|
|
173
|
+
assert.throws(
|
|
174
|
+
() => undo.resetSlice(root),
|
|
175
|
+
(err) => err && err.code === 'undo-dirty-tree',
|
|
176
|
+
);
|
|
177
|
+
} finally {
|
|
178
|
+
process.chdir(prev);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('UN-5: resetSlice with current_task but no checkpoint → checkpoint-orphan', () => {
|
|
183
|
+
const root = makeRepo();
|
|
184
|
+
|
|
185
|
+
const sp = path.join(root, '.nubos-pilot', 'STATE.md');
|
|
186
|
+
const body = fs.readFileSync(sp, 'utf-8').replace(/current_task: null/, 'current_task: "06-01-T07"');
|
|
187
|
+
fs.writeFileSync(sp, body, 'utf-8');
|
|
188
|
+
const prev = process.cwd();
|
|
189
|
+
process.chdir(root);
|
|
190
|
+
try {
|
|
191
|
+
assert.throws(
|
|
192
|
+
() => undo.resetSlice(root),
|
|
193
|
+
(err) => err && err.code === 'checkpoint-orphan',
|
|
194
|
+
);
|
|
195
|
+
} finally {
|
|
196
|
+
process.chdir(prev);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('UN-6: undoPlan reverts all task commits in reverse chronological order', () => {
|
|
201
|
+
const root = makeRepo();
|
|
202
|
+
seedTask(root, '06-01', '06-01-T01', ['src/p1.ts']);
|
|
203
|
+
seedTask(root, '06-01', '06-01-T02', ['src/p2.ts']);
|
|
204
|
+
seedTask(root, '06-01', '06-01-T03', ['src/p3.ts']);
|
|
205
|
+
const prev = process.cwd();
|
|
206
|
+
process.chdir(root);
|
|
207
|
+
try {
|
|
208
|
+
commitFor(root, '06-01-T01', ['src/p1.ts']);
|
|
209
|
+
commitFor(root, '06-01-T02', ['src/p2.ts']);
|
|
210
|
+
commitFor(root, '06-01-T03', ['src/p3.ts']);
|
|
211
|
+
const result = undo.undoPlan('06-01', root);
|
|
212
|
+
assert.equal(result.reverted.length, 3);
|
|
213
|
+
|
|
214
|
+
const revertCount = execFileSync('git', ['log', '--grep=^Revert', '--format=%H'], { encoding: 'utf-8' })
|
|
215
|
+
.trim().split('\n').filter(Boolean).length;
|
|
216
|
+
assert.equal(revertCount, 3);
|
|
217
|
+
|
|
218
|
+
for (const id of ['06-01-T01', '06-01-T02', '06-01-T03']) {
|
|
219
|
+
const tf = path.join(root, '.nubos-pilot', 'phases', '06-demo', 'tasks', id + '.md');
|
|
220
|
+
assert.match(fs.readFileSync(tf, 'utf-8'), /^status: pending$/m, id);
|
|
221
|
+
}
|
|
222
|
+
} finally {
|
|
223
|
+
process.chdir(prev);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('UN-7: undoPlan with nothing committed → emit nothing-to-revert', () => {
|
|
228
|
+
const root = makeRepo();
|
|
229
|
+
const prev = process.cwd();
|
|
230
|
+
process.chdir(root);
|
|
231
|
+
try {
|
|
232
|
+
const result = undo.undoPlan('06-99', root);
|
|
233
|
+
assert.deepEqual(result.reverted, []);
|
|
234
|
+
assert.equal(result.message, 'nothing to revert');
|
|
235
|
+
} finally {
|
|
236
|
+
process.chdir(prev);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('UN-8: undoPhase reverts all plans of the phase', () => {
|
|
241
|
+
const root = makeRepo();
|
|
242
|
+
seedTask(root, '06-01', '06-01-T01', ['src/q1.ts']);
|
|
243
|
+
seedTask(root, '06-02', '06-02-T01', ['src/q2.ts']);
|
|
244
|
+
const prev = process.cwd();
|
|
245
|
+
process.chdir(root);
|
|
246
|
+
try {
|
|
247
|
+
commitFor(root, '06-01-T01', ['src/q1.ts']);
|
|
248
|
+
commitFor(root, '06-02-T01', ['src/q2.ts']);
|
|
249
|
+
const result = undo.undoPhase(6, root);
|
|
250
|
+
assert.equal(result.reverted.length, 2);
|
|
251
|
+
} finally {
|
|
252
|
+
process.chdir(prev);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('UN-9: exports', () => {
|
|
257
|
+
assert.equal(typeof undo.undoTask, 'function');
|
|
258
|
+
assert.equal(typeof undo.undoPlan, 'function');
|
|
259
|
+
assert.equal(typeof undo.undoPhase, 'function');
|
|
260
|
+
assert.equal(typeof undo.resetSlice, 'function');
|
|
261
|
+
});
|