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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { execFileSync } = require('node:child_process');
|
|
5
|
+
const { test } = require('node:test');
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const { Writable } = require('node:stream');
|
|
8
|
+
|
|
9
|
+
const commitCli = require('./commit.cjs');
|
|
10
|
+
|
|
11
|
+
const _sandboxes = [];
|
|
12
|
+
|
|
13
|
+
function makeSink() {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
const w = new Writable({
|
|
16
|
+
write(chunk, _enc, cb) { chunks.push(chunk); cb(); },
|
|
17
|
+
});
|
|
18
|
+
w.toString = () => Buffer.concat(chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(String(c)))).toString('utf-8');
|
|
19
|
+
return w;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeSandbox() {
|
|
23
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-commit-'));
|
|
24
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
25
|
+
_sandboxes.push(root);
|
|
26
|
+
return root;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function initGit(root) {
|
|
30
|
+
execFileSync('git', ['init', '-q'], { cwd: root });
|
|
31
|
+
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: root });
|
|
32
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: root });
|
|
33
|
+
execFileSync('git', ['config', 'commit.gpgsign', 'false'], { cwd: root });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test.afterEach(() => {
|
|
37
|
+
while (_sandboxes.length) {
|
|
38
|
+
try { fs.rmSync(_sandboxes.pop(), { recursive: true, force: true }); } catch { }
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('COMMIT-1: happy path commits a single file and prints sha JSON', () => {
|
|
43
|
+
const sb = makeSandbox();
|
|
44
|
+
initGit(sb);
|
|
45
|
+
fs.writeFileSync(path.join(sb, 'hello.txt'), 'hi\n');
|
|
46
|
+
const stdout = makeSink();
|
|
47
|
+
const stderr = makeSink();
|
|
48
|
+
const origCwd = process.cwd();
|
|
49
|
+
process.chdir(sb);
|
|
50
|
+
let code;
|
|
51
|
+
try {
|
|
52
|
+
code = commitCli.run(['feat: hello', '--files', 'hello.txt'], { stdout, stderr });
|
|
53
|
+
} finally {
|
|
54
|
+
process.chdir(origCwd);
|
|
55
|
+
}
|
|
56
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
57
|
+
assert.match(stdout.toString(), /"committed":\s*true/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('COMMIT-2: path with ".." segment rejected with commit-path-traversal', () => {
|
|
61
|
+
const stdout = makeSink();
|
|
62
|
+
const stderr = makeSink();
|
|
63
|
+
const code = commitCli.run(['feat: x', '--files', '../outside.txt'], { stdout, stderr });
|
|
64
|
+
assert.equal(code, 1);
|
|
65
|
+
assert.match(stderr.toString(), /"code":\s*"commit-path-traversal"/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('COMMIT-3: empty message prints usage and exits 1', () => {
|
|
69
|
+
const stdout = makeSink();
|
|
70
|
+
const stderr = makeSink();
|
|
71
|
+
const code = commitCli.run([], { stdout, stderr });
|
|
72
|
+
assert.equal(code, 1);
|
|
73
|
+
assert.match(stderr.toString(), /Usage:/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('COMMIT-4: overlong message exceeds limit → commit-message-too-long', () => {
|
|
77
|
+
const sb = makeSandbox();
|
|
78
|
+
initGit(sb);
|
|
79
|
+
fs.writeFileSync(path.join(sb, 'a.txt'), 'a');
|
|
80
|
+
const stdout = makeSink();
|
|
81
|
+
const stderr = makeSink();
|
|
82
|
+
const origCwd = process.cwd();
|
|
83
|
+
process.chdir(sb);
|
|
84
|
+
let code;
|
|
85
|
+
try {
|
|
86
|
+
const huge = 'x'.repeat(3000);
|
|
87
|
+
code = commitCli.run([huge, '--files', 'a.txt'], { stdout, stderr });
|
|
88
|
+
} finally {
|
|
89
|
+
process.chdir(origCwd);
|
|
90
|
+
}
|
|
91
|
+
assert.equal(code, 1);
|
|
92
|
+
assert.match(stderr.toString(), /"code":\s*"commit-message-too-long"/);
|
|
93
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { findProjectRoot, NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
|
|
5
|
+
const SEGMENT_RE = /^[a-zA-Z0-9_-]+$/;
|
|
6
|
+
const BLOCKED_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
7
|
+
|
|
8
|
+
function _usage() {
|
|
9
|
+
return 'Usage:\n np-tools.cjs config-get <dotted.key> [--raw]';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function _emitError(err, stderr) {
|
|
13
|
+
const code = err && err.name === 'NubosPilotError' ? err.code : 'config-get-internal-error';
|
|
14
|
+
const message = (err && err.message) || String(err);
|
|
15
|
+
const details = (err && err.details) || null;
|
|
16
|
+
stderr.write(JSON.stringify({ code, message, details }) + '\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _readConfig(cwd) {
|
|
20
|
+
let root;
|
|
21
|
+
try {
|
|
22
|
+
root = findProjectRoot(cwd);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err && err.code === 'not-in-project') return null;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
const p = path.join(root, '.nubos-pilot', 'config.json');
|
|
28
|
+
if (!fs.existsSync(p)) return null;
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
31
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new NubosPilotError('config-parse-error', 'config.json invalid JSON', { cause: err && err.message });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _walkPath(obj, segments) {
|
|
38
|
+
let cursor = obj;
|
|
39
|
+
for (const seg of segments) {
|
|
40
|
+
if (cursor == null || typeof cursor !== 'object') return undefined;
|
|
41
|
+
if (!Object.prototype.hasOwnProperty.call(cursor, seg)) return undefined;
|
|
42
|
+
cursor = cursor[seg];
|
|
43
|
+
}
|
|
44
|
+
return cursor;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _validateSegments(segments) {
|
|
48
|
+
for (const seg of segments) {
|
|
49
|
+
if (BLOCKED_SEGMENTS.has(seg)) {
|
|
50
|
+
throw new NubosPilotError('config-forbidden-key', 'config key segment forbidden: ' + seg, { segment: seg });
|
|
51
|
+
}
|
|
52
|
+
if (!SEGMENT_RE.test(seg)) {
|
|
53
|
+
throw new NubosPilotError('config-invalid-key', 'config key segment must match /^[a-zA-Z0-9_-]+$/: ' + seg, { segment: seg });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function run(argv, ctx) {
|
|
59
|
+
const context = ctx || {};
|
|
60
|
+
const cwd = context.cwd || process.cwd();
|
|
61
|
+
const stdout = context.stdout || process.stdout;
|
|
62
|
+
const stderr = context.stderr || process.stderr;
|
|
63
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
64
|
+
if (args.length === 0) {
|
|
65
|
+
stderr.write(_usage() + '\n');
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
const raw = args.includes('--raw');
|
|
69
|
+
const key = args.find((a) => !String(a).startsWith('--'));
|
|
70
|
+
if (!key) {
|
|
71
|
+
stderr.write(_usage() + '\n');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const segments = String(key).split('.');
|
|
75
|
+
try {
|
|
76
|
+
_validateSegments(segments);
|
|
77
|
+
const config = _readConfig(cwd);
|
|
78
|
+
if (config == null) {
|
|
79
|
+
if (!raw) stdout.write('\n');
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
const value = _walkPath(config, segments);
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
if (!raw) stdout.write('\n');
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
const out = typeof value === 'string' ? value : JSON.stringify(value);
|
|
88
|
+
if (raw) stdout.write(out);
|
|
89
|
+
else stdout.write(out + '\n');
|
|
90
|
+
return 0;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
_emitError(err, stderr);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { run, _walkPath, _validateSegments };
|
|
98
|
+
|
|
99
|
+
if (require.main === module) {
|
|
100
|
+
process.exit(run(process.argv.slice(2)));
|
|
101
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { test } = require('node:test');
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const { Writable } = require('node:stream');
|
|
7
|
+
|
|
8
|
+
const configCli = require('./config.cjs');
|
|
9
|
+
|
|
10
|
+
const _sandboxes = [];
|
|
11
|
+
|
|
12
|
+
function makeSink() {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
const w = new Writable({
|
|
15
|
+
write(chunk, _enc, cb) { chunks.push(chunk); cb(); },
|
|
16
|
+
});
|
|
17
|
+
w.toString = () => Buffer.concat(chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(String(c)))).toString('utf-8');
|
|
18
|
+
return w;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeSandbox(config) {
|
|
22
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-config-'));
|
|
23
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
24
|
+
if (config !== undefined) {
|
|
25
|
+
fs.writeFileSync(path.join(root, '.nubos-pilot', 'config.json'), JSON.stringify(config));
|
|
26
|
+
}
|
|
27
|
+
_sandboxes.push(root);
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test.afterEach(() => {
|
|
32
|
+
while (_sandboxes.length) {
|
|
33
|
+
try { fs.rmSync(_sandboxes.pop(), { recursive: true, force: true }); } catch { }
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('CONFIG-1: reads a nested string value via dotted path', () => {
|
|
38
|
+
const sb = makeSandbox({ review: { models: { gemini: 'gemini-2.5-pro' } } });
|
|
39
|
+
const stdout = makeSink();
|
|
40
|
+
const stderr = makeSink();
|
|
41
|
+
const code = configCli.run(['review.models.gemini', '--raw'], { cwd: sb, stdout, stderr });
|
|
42
|
+
assert.equal(code, 0);
|
|
43
|
+
assert.equal(stdout.toString(), 'gemini-2.5-pro');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('CONFIG-2: missing key prints empty line and exits 0', () => {
|
|
47
|
+
const sb = makeSandbox({ workflow: {} });
|
|
48
|
+
const stdout = makeSink();
|
|
49
|
+
const stderr = makeSink();
|
|
50
|
+
const code = configCli.run(['workflow.nonexistent'], { cwd: sb, stdout, stderr });
|
|
51
|
+
assert.equal(code, 0);
|
|
52
|
+
assert.equal(stdout.toString(), '\n');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('CONFIG-3: __proto__ segment rejected with config-forbidden-key', () => {
|
|
56
|
+
const sb = makeSandbox({ a: 1 });
|
|
57
|
+
const stdout = makeSink();
|
|
58
|
+
const stderr = makeSink();
|
|
59
|
+
const code = configCli.run(['__proto__.polluted'], { cwd: sb, stdout, stderr });
|
|
60
|
+
assert.equal(code, 1);
|
|
61
|
+
assert.match(stderr.toString(), /"code":\s*"config-forbidden-key"/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('CONFIG-4: object value serialized as JSON', () => {
|
|
65
|
+
const sb = makeSandbox({ workflow: { nested: { k: 'v' } } });
|
|
66
|
+
const stdout = makeSink();
|
|
67
|
+
const stderr = makeSink();
|
|
68
|
+
const code = configCli.run(['workflow.nested', '--raw'], { cwd: sb, stdout, stderr });
|
|
69
|
+
assert.equal(code, 0);
|
|
70
|
+
assert.equal(stdout.toString(), '{"k":"v"}');
|
|
71
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
atomicWriteFileSync,
|
|
6
|
+
withFileLock,
|
|
7
|
+
NubosPilotError,
|
|
8
|
+
} = require('../../lib/core.cjs');
|
|
9
|
+
const { findPhaseDir, paddedPhase } = require('../../lib/phase.cjs');
|
|
10
|
+
const { loadTemplate } = require('../../lib/template.cjs');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_QUESTIONS = [
|
|
13
|
+
|
|
14
|
+
{ id: 'Q-01', area: 'domain', question: 'What is the phase boundary — what IS and what is NOT in scope?', explain: 'The phase boundary separates work that belongs in this phase from downstream phases. Narrow scope to prevent plan drift.' },
|
|
15
|
+
{ id: 'Q-02', area: 'domain', question: 'What problem does this phase solve for the end user?', explain: 'Surface the user-facing value so downstream planners remember the goal, not just the mechanism.' },
|
|
16
|
+
|
|
17
|
+
{ id: 'Q-03', area: 'decisions', question: 'What are the main implementation decisions (D-XX style)?', explain: 'Decisions lock architecture choices so downstream plans cannot re-litigate them. Number them D-XX for traceability.' },
|
|
18
|
+
{ id: 'Q-04', area: 'decisions', question: 'Which prior-art patterns are adopted, modified, or rejected here?', explain: 'Explicit adoption/rejection prevents silent drift when the planner reads prior-art.' },
|
|
19
|
+
|
|
20
|
+
{ id: 'Q-05', area: 'canonical_refs', question: 'Which prior-phase CONTEXT.md files MUST be read before planning?', explain: 'Pointing downstream agents at the right references is cheaper than re-deriving decisions.' },
|
|
21
|
+
{ id: 'Q-06', area: 'canonical_refs', question: 'Which ADRs / REQUIREMENTS entries are authoritative for this phase?', explain: 'List only the ADRs and requirement IDs that actually constrain this phase, not the whole catalogue.' },
|
|
22
|
+
|
|
23
|
+
{ id: 'Q-07', area: 'code_context', question: 'Which existing lib/*.cjs or bin/*.cjs modules are reused?', explain: 'Reuse is mandatory where the API exists. Planner should not re-implement what Phase 2/3 already ships.' },
|
|
24
|
+
{ id: 'Q-08', area: 'code_context', question: 'Which integration points / file-tree locations does this phase write to?', explain: 'File-tree boundaries matter for Git-atomic commits and parallel execution safety.' },
|
|
25
|
+
|
|
26
|
+
{ id: 'Q-09', area: 'specifics', question: 'What user-specific expectations or workflow UX must the output honor?', explain: 'Specifics capture preferences the user confirmed in discussion and which tests will verify.' },
|
|
27
|
+
{ id: 'Q-10', area: 'specifics', question: 'Are there concrete file paths, error codes, or CLI strings the user pinned?', explain: 'Pinned strings become acceptance-criterion grep targets for the plan-checker.' },
|
|
28
|
+
|
|
29
|
+
{ id: 'Q-11', area: 'deferred', question: 'What is explicitly DEFERRED to a later phase?', explain: 'Deferral entries protect scope and prevent revisit-creep during execution.' },
|
|
30
|
+
{ id: 'Q-12', area: 'deferred', question: 'Which ideas were actively rejected (not merely postponed)?', explain: 'Rejected ideas with rationale help future audits understand why the surface stays small.' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const AREAS = ['domain', 'decisions', 'canonical_refs', 'code_context', 'specifics', 'deferred'];
|
|
34
|
+
|
|
35
|
+
function _resolvePhaseDir(phaseArg, cwd) {
|
|
36
|
+
const dir = findPhaseDir(phaseArg, cwd);
|
|
37
|
+
if (!dir) {
|
|
38
|
+
throw new NubosPilotError(
|
|
39
|
+
'phase-not-found',
|
|
40
|
+
'No phase directory for phase ' + phaseArg,
|
|
41
|
+
{ phase: phaseArg }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _questionsPath(phaseDir, padded) {
|
|
48
|
+
return path.join(phaseDir, padded + '-QUESTIONS.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _contextPath(phaseDir, padded) {
|
|
52
|
+
return path.join(phaseDir, padded + '-CONTEXT.md');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _readQuestions(qpath) {
|
|
56
|
+
const raw = fs.readFileSync(qpath, 'utf-8');
|
|
57
|
+
return JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _writeQuestions(qpath, doc) {
|
|
61
|
+
atomicWriteFileSync(qpath, JSON.stringify(doc, null, 2) + '\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _verbInit(args, ctx) {
|
|
65
|
+
const phaseArg = args[1];
|
|
66
|
+
const padded = paddedPhase(phaseArg);
|
|
67
|
+
const phaseDir = _resolvePhaseDir(phaseArg, ctx.cwd);
|
|
68
|
+
const qpath = _questionsPath(phaseDir, padded);
|
|
69
|
+
if (fs.existsSync(qpath)) {
|
|
70
|
+
throw new NubosPilotError(
|
|
71
|
+
'power-questions-exist',
|
|
72
|
+
'QUESTIONS.json already exists for phase ' + phaseArg,
|
|
73
|
+
{ path: qpath }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const doc = {
|
|
77
|
+
phase: Number(phaseArg),
|
|
78
|
+
padded,
|
|
79
|
+
mode: 'power',
|
|
80
|
+
created: new Date().toISOString(),
|
|
81
|
+
questions: DEFAULT_QUESTIONS.map((q) => ({
|
|
82
|
+
id: q.id,
|
|
83
|
+
area: q.area,
|
|
84
|
+
question: q.question,
|
|
85
|
+
answer: null,
|
|
86
|
+
explain: q.explain,
|
|
87
|
+
})),
|
|
88
|
+
answers_status: 'pending',
|
|
89
|
+
};
|
|
90
|
+
withFileLock(qpath, () => {
|
|
91
|
+
|
|
92
|
+
if (fs.existsSync(qpath)) {
|
|
93
|
+
throw new NubosPilotError(
|
|
94
|
+
'power-questions-exist',
|
|
95
|
+
'QUESTIONS.json already exists for phase ' + phaseArg,
|
|
96
|
+
{ path: qpath }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
_writeQuestions(qpath, doc);
|
|
100
|
+
});
|
|
101
|
+
const payload = {
|
|
102
|
+
status: 'initialized',
|
|
103
|
+
path: qpath,
|
|
104
|
+
question_count: doc.questions.length,
|
|
105
|
+
padded,
|
|
106
|
+
};
|
|
107
|
+
ctx.stdout.write(JSON.stringify(payload, null, 2));
|
|
108
|
+
return payload;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _computeStats(doc) {
|
|
112
|
+
const areas = {};
|
|
113
|
+
for (const a of AREAS) areas[a] = { total: 0, answered: 0 };
|
|
114
|
+
let answered = 0;
|
|
115
|
+
for (const q of doc.questions) {
|
|
116
|
+
if (!areas[q.area]) areas[q.area] = { total: 0, answered: 0 };
|
|
117
|
+
areas[q.area].total += 1;
|
|
118
|
+
if (q.answer !== null && q.answer !== undefined && String(q.answer).trim() !== '') {
|
|
119
|
+
areas[q.area].answered += 1;
|
|
120
|
+
answered += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
total_questions: doc.questions.length,
|
|
125
|
+
answered,
|
|
126
|
+
pending: doc.questions.length - answered,
|
|
127
|
+
areas,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _verbRefresh(args, ctx) {
|
|
132
|
+
const phaseArg = args[1];
|
|
133
|
+
const padded = paddedPhase(phaseArg);
|
|
134
|
+
const phaseDir = _resolvePhaseDir(phaseArg, ctx.cwd);
|
|
135
|
+
const qpath = _questionsPath(phaseDir, padded);
|
|
136
|
+
const doc = withFileLock(qpath, () => _readQuestions(qpath));
|
|
137
|
+
const stats = _computeStats(doc);
|
|
138
|
+
|
|
139
|
+
ctx.stdout.write(JSON.stringify(stats, null, 2));
|
|
140
|
+
return stats;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _groupAnswersByArea(doc) {
|
|
144
|
+
const grouped = {};
|
|
145
|
+
for (const a of AREAS) grouped[a] = [];
|
|
146
|
+
for (const q of doc.questions) {
|
|
147
|
+
if (!grouped[q.area]) grouped[q.area] = [];
|
|
148
|
+
grouped[q.area].push('- **' + q.question + '** ' + String(q.answer));
|
|
149
|
+
}
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const a of AREAS) {
|
|
152
|
+
out[a + '_text'] = grouped[a].length ? grouped[a].join('\n') : '_(no entries)_';
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _verbFinalize(args, ctx) {
|
|
158
|
+
const phaseArg = args[1];
|
|
159
|
+
const padded = paddedPhase(phaseArg);
|
|
160
|
+
const phaseDir = _resolvePhaseDir(phaseArg, ctx.cwd);
|
|
161
|
+
const qpath = _questionsPath(phaseDir, padded);
|
|
162
|
+
|
|
163
|
+
return withFileLock(qpath, () => {
|
|
164
|
+
const doc = _readQuestions(qpath);
|
|
165
|
+
const pending = doc.questions
|
|
166
|
+
.filter((q) => q.answer === null || q.answer === undefined || String(q.answer).trim() === '')
|
|
167
|
+
.map((q) => q.id);
|
|
168
|
+
if (pending.length > 0) {
|
|
169
|
+
throw new NubosPilotError(
|
|
170
|
+
'power-finalize-incomplete',
|
|
171
|
+
'Cannot finalize: ' + pending.length + ' unanswered question(s)',
|
|
172
|
+
{ pending_ids: pending }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const grouped = _groupAnswersByArea(doc);
|
|
176
|
+
const vars = Object.assign(
|
|
177
|
+
{ phase: String(doc.phase), padded: doc.padded },
|
|
178
|
+
grouped,
|
|
179
|
+
);
|
|
180
|
+
const rendered = loadTemplate('CONTEXT', vars, ctx.cwd);
|
|
181
|
+
const ctxPath = _contextPath(phaseDir, padded);
|
|
182
|
+
atomicWriteFileSync(ctxPath, rendered);
|
|
183
|
+
|
|
184
|
+
doc.answers_status = 'finalized';
|
|
185
|
+
_writeQuestions(qpath, doc);
|
|
186
|
+
|
|
187
|
+
const payload = {
|
|
188
|
+
status: 'finalized',
|
|
189
|
+
context_path: ctxPath,
|
|
190
|
+
questions_path: qpath,
|
|
191
|
+
};
|
|
192
|
+
ctx.stdout.write(JSON.stringify(payload, null, 2));
|
|
193
|
+
return payload;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function _verbExplain(args, ctx) {
|
|
198
|
+
const phaseArg = args[1];
|
|
199
|
+
const qid = args[2];
|
|
200
|
+
if (!qid) {
|
|
201
|
+
throw new NubosPilotError(
|
|
202
|
+
'power-explain-missing-id',
|
|
203
|
+
'explain verb requires a question id',
|
|
204
|
+
{ got: qid }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const padded = paddedPhase(phaseArg);
|
|
208
|
+
const phaseDir = _resolvePhaseDir(phaseArg, ctx.cwd);
|
|
209
|
+
const qpath = _questionsPath(phaseDir, padded);
|
|
210
|
+
const doc = _readQuestions(qpath);
|
|
211
|
+
const q = doc.questions.find((x) => x.id === qid);
|
|
212
|
+
if (!q) {
|
|
213
|
+
throw new NubosPilotError(
|
|
214
|
+
'power-question-not-found',
|
|
215
|
+
'Question ' + qid + ' not found in QUESTIONS.json',
|
|
216
|
+
{ id: qid, path: qpath }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
ctx.stdout.write(JSON.stringify(q, null, 2));
|
|
220
|
+
return q;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function _verbExit(args, ctx) {
|
|
224
|
+
const phaseArg = args[1];
|
|
225
|
+
const padded = paddedPhase(phaseArg);
|
|
226
|
+
const phaseDir = _resolvePhaseDir(phaseArg, ctx.cwd);
|
|
227
|
+
const qpath = _questionsPath(phaseDir, padded);
|
|
228
|
+
const preserved = fs.existsSync(qpath);
|
|
229
|
+
const payload = {
|
|
230
|
+
status: 'exited',
|
|
231
|
+
questions_preserved: preserved,
|
|
232
|
+
path: preserved ? qpath : null,
|
|
233
|
+
};
|
|
234
|
+
ctx.stdout.write(JSON.stringify(payload, null, 2));
|
|
235
|
+
return payload;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function run(args, ctx) {
|
|
239
|
+
const context = {
|
|
240
|
+
cwd: (ctx && ctx.cwd) || process.cwd(),
|
|
241
|
+
stdout: (ctx && ctx.stdout) || process.stdout,
|
|
242
|
+
};
|
|
243
|
+
const verb = (args && args[0]) || '';
|
|
244
|
+
switch (verb) {
|
|
245
|
+
case 'init': return _verbInit(args, context);
|
|
246
|
+
case 'refresh': return _verbRefresh(args, context);
|
|
247
|
+
case 'finalize': return _verbFinalize(args, context);
|
|
248
|
+
case 'explain': return _verbExplain(args, context);
|
|
249
|
+
case 'exit': return _verbExit(args, context);
|
|
250
|
+
default:
|
|
251
|
+
throw new NubosPilotError(
|
|
252
|
+
'power-unknown-verb',
|
|
253
|
+
'Unknown verb: "' + verb + '" (expected one of init|refresh|finalize|explain|exit)',
|
|
254
|
+
{ got: verb }
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
run,
|
|
261
|
+
DEFAULT_QUESTIONS,
|
|
262
|
+
AREAS,
|
|
263
|
+
_computeStats,
|
|
264
|
+
_groupAnswersByArea,
|
|
265
|
+
};
|