nubos-pilot 0.5.7 → 0.5.9
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/bin/install.js +3 -7
- package/bin/np-tools/_commands.cjs +1 -0
- package/bin/np-tools/commit.cjs +38 -14
- package/bin/np-tools/commit.test.cjs +74 -3
- package/bin/np-tools/template-path.cjs +61 -0
- package/bin/np-tools/template-path.test.cjs +86 -0
- package/lib/agents.cjs +12 -6
- package/lib/commit-policy.cjs +60 -0
- package/lib/commit-policy.test.cjs +74 -0
- package/lib/config-defaults.cjs +40 -0
- package/lib/git.cjs +11 -7
- package/np-tools.cjs +1 -0
- package/package.json +1 -1
- package/workflows/add-tests.md +1 -1
- package/workflows/discuss-phase.md +4 -3
- package/workflows/plan-phase.md +5 -2
- package/workflows/validate-phase.md +5 -5
- package/workflows/verify-work.md +1 -1
package/bin/install.js
CHANGED
|
@@ -17,6 +17,7 @@ const backupMod = require('../lib/install/backup.cjs');
|
|
|
17
17
|
const registryMod = require('../lib/install/runtimes-registry.cjs');
|
|
18
18
|
const runtimeAssetsMod = require('../lib/install/runtime-assets.cjs');
|
|
19
19
|
const languageMod = require('../lib/language.cjs');
|
|
20
|
+
const configDefaults = require('../lib/config-defaults.cjs');
|
|
20
21
|
|
|
21
22
|
const cyan = '\x1b[36m', green = '\x1b[32m', yellow = '\x1b[33m',
|
|
22
23
|
red = '\x1b[31m', blue = '\x1b[38;5;33m',
|
|
@@ -248,16 +249,11 @@ async function _runInitQuestions(detectedRuntime, askUser, flags) {
|
|
|
248
249
|
const model_profile = (await askUser({ type: 'select', question: 'Model-Profile?',
|
|
249
250
|
options: ['frontier', 'quality', 'balanced', 'budget', 'inherit'], default: 'frontier' })).value;
|
|
250
251
|
const response_language = (await askUser({ type: 'input', question: 'Response language (ISO-639 code)?', default: 'en' })).value;
|
|
251
|
-
return {
|
|
252
|
+
return configDefaults.buildInstallConfig({
|
|
252
253
|
runtime, runtimes, scope, mcp: !!f.mcp,
|
|
253
254
|
model_profile,
|
|
254
255
|
response_language,
|
|
255
|
-
|
|
256
|
-
parallelization: true,
|
|
257
|
-
research: true,
|
|
258
|
-
plan_checker: true,
|
|
259
|
-
verifier: true,
|
|
260
|
-
};
|
|
256
|
+
});
|
|
261
257
|
}
|
|
262
258
|
|
|
263
259
|
function _repairCodexConfig() {
|
|
@@ -48,6 +48,7 @@ const COMMANDS = [
|
|
|
48
48
|
{ name: 'generate-slug', category: 'Utility', description: 'Slugify text via lib/layout.cjs.slugify' },
|
|
49
49
|
{ name: 'stats', category: 'Utility', description: 'Aggregated project stats (roadmap + STATE + git + metrics JSON shape)' },
|
|
50
50
|
{ name: 'detect-runtime', category: 'Utility', description: 'Print detected runtime id (claude, codex, gemini, …) — reads config.json ∨ env ∨ default' },
|
|
51
|
+
{ name: 'template-path', category: 'Utility', description: 'Print absolute path to a package-shipped template by name (e.g. VALIDATION, milestone/CONTEXT)' },
|
|
51
52
|
|
|
52
53
|
{ name: 'thread', category: 'Utility', description: 'Cross-session thread CRUD (create/resume under .nubos-pilot/threads/)' },
|
|
53
54
|
{ name: 'session-report', category: 'Utility', description: 'Generate session report from metrics since .last-session pointer' },
|
package/bin/np-tools/commit.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const { execFileSync } = require('node:child_process');
|
|
2
|
+
const fs = require('node:fs');
|
|
2
3
|
const path = require('node:path');
|
|
3
|
-
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { NubosPilotError, findProjectRoot } = require('../../lib/core.cjs');
|
|
4
5
|
const { assertCommittablePaths } = require('../../lib/git.cjs');
|
|
6
|
+
const { resolveCommitArtifacts } = require('../../lib/commit-policy.cjs');
|
|
5
7
|
|
|
6
8
|
const MAX_MSG = 2000;
|
|
7
9
|
|
|
@@ -46,25 +48,41 @@ function _parseArgs(argv) {
|
|
|
46
48
|
return { msg, files };
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
+
function _realpathOrResolve(p) {
|
|
52
|
+
try { return fs.realpathSync(p); } catch { return path.resolve(p); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _normalizeFiles(files, cwd, root) {
|
|
56
|
+
const realRoot = _realpathOrResolve(root);
|
|
57
|
+
return files.map((f) => {
|
|
51
58
|
if (typeof f !== 'string' || f.length === 0) {
|
|
52
59
|
throw new NubosPilotError('commit-invalid-path', 'commit path must be non-empty string', { path: f });
|
|
53
60
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const abs = path.isAbsolute(f) ? path.resolve(f) : path.resolve(cwd, f);
|
|
62
|
+
const realAbs = _realpathOrResolve(abs);
|
|
63
|
+
const rel = path.relative(realRoot, realAbs);
|
|
64
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
65
|
+
throw new NubosPilotError(
|
|
66
|
+
'commit-path-outside-project',
|
|
67
|
+
'commit path must resolve inside project root',
|
|
68
|
+
{ path: f, root },
|
|
69
|
+
);
|
|
59
70
|
}
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
return rel;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _validateFiles(files) {
|
|
76
|
+
for (const f of files) {
|
|
77
|
+
if (typeof f !== 'string' || f.length === 0) {
|
|
78
|
+
throw new NubosPilotError('commit-invalid-path', 'commit path must be non-empty string', { path: f });
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
function run(argv, ctx) {
|
|
67
84
|
const context = ctx || {};
|
|
85
|
+
const cwd = context.cwd || process.cwd();
|
|
68
86
|
const stdout = context.stdout || process.stdout;
|
|
69
87
|
const stderr = context.stderr || process.stderr;
|
|
70
88
|
try {
|
|
@@ -81,13 +99,19 @@ function run(argv, ctx) {
|
|
|
81
99
|
return 1;
|
|
82
100
|
}
|
|
83
101
|
_validateFiles(files);
|
|
84
|
-
|
|
102
|
+
if (resolveCommitArtifacts(cwd) === false) {
|
|
103
|
+
stdout.write(JSON.stringify({ committed: false, reason: 'commit_artifacts=false', files }) + '\n');
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
const root = findProjectRoot(cwd);
|
|
107
|
+
const normalized = _normalizeFiles(files, cwd, root);
|
|
108
|
+
const committable = assertCommittablePaths(normalized, { cwd: root });
|
|
85
109
|
if (committable.length === 0) {
|
|
86
110
|
throw new NubosPilotError('commit-no-paths', 'commit invoked with no committable paths', { files });
|
|
87
111
|
}
|
|
88
|
-
execFileSync('git', ['add', '--', ...committable], { stdio: 'pipe' });
|
|
89
|
-
execFileSync('git', ['commit', '-m', msg, '--', ...committable], { stdio: 'pipe' });
|
|
90
|
-
const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf-8' }).trim();
|
|
112
|
+
execFileSync('git', ['add', '--', ...committable], { cwd: root, stdio: 'pipe' });
|
|
113
|
+
execFileSync('git', ['commit', '-m', msg, '--', ...committable], { cwd: root, stdio: 'pipe' });
|
|
114
|
+
const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, encoding: 'utf-8' }).trim();
|
|
91
115
|
stdout.write(JSON.stringify({ committed: true, sha, files: committable }) + '\n');
|
|
92
116
|
return 0;
|
|
93
117
|
} catch (err) {
|
|
@@ -57,12 +57,39 @@ test('COMMIT-1: happy path commits a single file and prints sha JSON', () => {
|
|
|
57
57
|
assert.match(stdout.toString(), /"committed":\s*true/);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
test('COMMIT-2: path
|
|
60
|
+
test('COMMIT-2: path resolving outside project root rejected with commit-path-outside-project', () => {
|
|
61
|
+
const sb = makeSandbox();
|
|
62
|
+
initGit(sb);
|
|
63
|
+
const stdout = makeSink();
|
|
64
|
+
const stderr = makeSink();
|
|
65
|
+
const code = commitCli.run(['feat: x', '--files', '../outside.txt'], { cwd: sb, stdout, stderr });
|
|
66
|
+
assert.equal(code, 1);
|
|
67
|
+
assert.match(stderr.toString(), /"code":\s*"commit-path-outside-project"/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('COMMIT-2b: absolute path inside project root is accepted and normalized', () => {
|
|
71
|
+
const sb = makeSandbox();
|
|
72
|
+
initGit(sb);
|
|
73
|
+
const absFile = path.join(sb, 'note.md');
|
|
74
|
+
fs.writeFileSync(absFile, 'hi\n');
|
|
75
|
+
const stdout = makeSink();
|
|
76
|
+
const stderr = makeSink();
|
|
77
|
+
const code = commitCli.run(['docs: note', '--files', absFile], { cwd: sb, stdout, stderr });
|
|
78
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
79
|
+
assert.match(stdout.toString(), /"committed":\s*true/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('COMMIT-2c: absolute path outside project root rejected', () => {
|
|
83
|
+
const sb = makeSandbox();
|
|
84
|
+
initGit(sb);
|
|
85
|
+
const outside = path.join(os.tmpdir(), 'np-commit-outside-' + Date.now() + '.txt');
|
|
86
|
+
fs.writeFileSync(outside, 'x');
|
|
61
87
|
const stdout = makeSink();
|
|
62
88
|
const stderr = makeSink();
|
|
63
|
-
const code = commitCli.run(['
|
|
89
|
+
const code = commitCli.run(['docs: x', '--files', outside], { cwd: sb, stdout, stderr });
|
|
90
|
+
try { fs.unlinkSync(outside); } catch {}
|
|
64
91
|
assert.equal(code, 1);
|
|
65
|
-
assert.match(stderr.toString(), /"code":\s*"commit-path-
|
|
92
|
+
assert.match(stderr.toString(), /"code":\s*"commit-path-outside-project"/);
|
|
66
93
|
});
|
|
67
94
|
|
|
68
95
|
test('COMMIT-3: empty message prints usage and exits 1', () => {
|
|
@@ -91,3 +118,47 @@ test('COMMIT-4: overlong message exceeds limit → commit-message-too-long', ()
|
|
|
91
118
|
assert.equal(code, 1);
|
|
92
119
|
assert.match(stderr.toString(), /"code":\s*"commit-message-too-long"/);
|
|
93
120
|
});
|
|
121
|
+
|
|
122
|
+
test('COMMIT-5: workflow.commit_artifacts=false skips commit silently with exit 0', () => {
|
|
123
|
+
const sb = makeSandbox();
|
|
124
|
+
initGit(sb);
|
|
125
|
+
fs.writeFileSync(path.join(sb, 'note.md'), 'x');
|
|
126
|
+
fs.writeFileSync(
|
|
127
|
+
path.join(sb, '.nubos-pilot', 'config.json'),
|
|
128
|
+
JSON.stringify({ workflow: { commit_artifacts: false } }),
|
|
129
|
+
);
|
|
130
|
+
const stdout = makeSink();
|
|
131
|
+
const stderr = makeSink();
|
|
132
|
+
const code = commitCli.run(['docs: note', '--files', 'note.md'], { cwd: sb, stdout, stderr });
|
|
133
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
134
|
+
const out = stdout.toString();
|
|
135
|
+
assert.match(out, /"committed":\s*false/);
|
|
136
|
+
assert.match(out, /"reason":\s*"commit_artifacts=false"/);
|
|
137
|
+
let logOut = '';
|
|
138
|
+
try {
|
|
139
|
+
logOut = execFileSync('git', ['log', '--oneline'], { cwd: sb, encoding: 'utf-8' }).trim();
|
|
140
|
+
} catch { logOut = ''; }
|
|
141
|
+
assert.equal(logOut, '', 'expected no commits to be created');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('COMMIT-6: workflow.commit_artifacts=true still commits normally', () => {
|
|
145
|
+
const sb = makeSandbox();
|
|
146
|
+
initGit(sb);
|
|
147
|
+
fs.writeFileSync(path.join(sb, 'note.md'), 'x');
|
|
148
|
+
fs.writeFileSync(
|
|
149
|
+
path.join(sb, '.nubos-pilot', 'config.json'),
|
|
150
|
+
JSON.stringify({ workflow: { commit_artifacts: true } }),
|
|
151
|
+
);
|
|
152
|
+
const stdout = makeSink();
|
|
153
|
+
const stderr = makeSink();
|
|
154
|
+
const origCwd = process.cwd();
|
|
155
|
+
process.chdir(sb);
|
|
156
|
+
let code;
|
|
157
|
+
try {
|
|
158
|
+
code = commitCli.run(['docs: note', '--files', 'note.md'], { stdout, stderr });
|
|
159
|
+
} finally {
|
|
160
|
+
process.chdir(origCwd);
|
|
161
|
+
}
|
|
162
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
163
|
+
assert.match(stdout.toString(), /"committed":\s*true/);
|
|
164
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
6
|
+
|
|
7
|
+
const PACKAGE_TEMPLATES_DIR = path.resolve(__dirname, '..', '..', 'templates');
|
|
8
|
+
|
|
9
|
+
function _emitError(err, stderr) {
|
|
10
|
+
const code = err && err.name === 'NubosPilotError' ? err.code : 'template-path-internal-error';
|
|
11
|
+
const message = (err && err.message) || String(err);
|
|
12
|
+
const details = (err && err.details) || null;
|
|
13
|
+
stderr.write(JSON.stringify({ code, message, details }) + '\n');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveTemplatePath(name) {
|
|
17
|
+
if (!name || typeof name !== 'string') {
|
|
18
|
+
throw new NubosPilotError('template-invalid-name', 'template name must be a non-empty string', { name });
|
|
19
|
+
}
|
|
20
|
+
const segments = name.split(/[/\\]/);
|
|
21
|
+
for (const seg of segments) {
|
|
22
|
+
if (seg === '' || seg === '..' || seg === '.') {
|
|
23
|
+
throw new NubosPilotError('template-invalid-name', 'template name segment invalid: ' + seg, { name, segment: seg });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const withExt = /\.[a-z0-9]+$/i.test(name) ? name : name + '.md';
|
|
27
|
+
const full = path.resolve(PACKAGE_TEMPLATES_DIR, withExt);
|
|
28
|
+
const guard = PACKAGE_TEMPLATES_DIR + path.sep;
|
|
29
|
+
if (!full.startsWith(guard)) {
|
|
30
|
+
throw new NubosPilotError('template-path-traversal', 'template name escapes templates directory', { name, resolved: full });
|
|
31
|
+
}
|
|
32
|
+
if (!fs.existsSync(full)) {
|
|
33
|
+
throw new NubosPilotError('template-not-found', 'template not found: ' + name, { name, path: full });
|
|
34
|
+
}
|
|
35
|
+
return full;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run(argv, ctx) {
|
|
39
|
+
const context = ctx || {};
|
|
40
|
+
const stdout = context.stdout || process.stdout;
|
|
41
|
+
const stderr = context.stderr || process.stderr;
|
|
42
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
43
|
+
if (args.length === 0 || args[0] === '--help') {
|
|
44
|
+
stderr.write('Usage: np-tools.cjs template-path <name>\n');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const out = resolveTemplatePath(args[0]);
|
|
49
|
+
stdout.write(out);
|
|
50
|
+
return 0;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
_emitError(err, stderr);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { run, resolveTemplatePath, PACKAGE_TEMPLATES_DIR };
|
|
58
|
+
|
|
59
|
+
if (require.main === module) {
|
|
60
|
+
process.exit(run(process.argv.slice(2)));
|
|
61
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { test } = require('node:test');
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const { Writable } = require('node:stream');
|
|
8
|
+
|
|
9
|
+
const cli = require('./template-path.cjs');
|
|
10
|
+
const { resolveTemplatePath, PACKAGE_TEMPLATES_DIR } = cli;
|
|
11
|
+
|
|
12
|
+
function makeSink() {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
const w = new Writable({ write(chunk, _enc, cb) { chunks.push(chunk); cb(); } });
|
|
15
|
+
w.toString = () => Buffer.concat(chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(String(c)))).toString('utf-8');
|
|
16
|
+
return w;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('TPL-1: resolves VALIDATION to absolute path in package templates dir', () => {
|
|
20
|
+
const out = resolveTemplatePath('VALIDATION');
|
|
21
|
+
assert.ok(path.isAbsolute(out));
|
|
22
|
+
assert.ok(out.startsWith(PACKAGE_TEMPLATES_DIR + path.sep));
|
|
23
|
+
assert.ok(fs.existsSync(out));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('TPL-2: accepts nested name like milestone/CONTEXT', () => {
|
|
27
|
+
const out = resolveTemplatePath('milestone/CONTEXT');
|
|
28
|
+
assert.ok(fs.existsSync(out));
|
|
29
|
+
assert.match(out, /templates[/\\]milestone[/\\]CONTEXT\.md$/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('TPL-3: appends .md when extension absent', () => {
|
|
33
|
+
const out = resolveTemplatePath('VALIDATION');
|
|
34
|
+
assert.ok(out.endsWith('.md'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('TPL-4: keeps explicit extension', () => {
|
|
38
|
+
const out = resolveTemplatePath('VALIDATION.md');
|
|
39
|
+
assert.ok(out.endsWith('VALIDATION.md'));
|
|
40
|
+
assert.doesNotMatch(out, /\.md\.md$/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('TPL-5: rejects traversal ..', () => {
|
|
44
|
+
assert.throws(
|
|
45
|
+
() => resolveTemplatePath('../etc/passwd'),
|
|
46
|
+
(err) => err && err.code === 'template-invalid-name',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('TPL-6: rejects empty segments', () => {
|
|
51
|
+
assert.throws(
|
|
52
|
+
() => resolveTemplatePath('milestone//CONTEXT'),
|
|
53
|
+
(err) => err && err.code === 'template-invalid-name',
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('TPL-7: rejects non-existent template with template-not-found', () => {
|
|
58
|
+
assert.throws(
|
|
59
|
+
() => resolveTemplatePath('NONEXISTENT'),
|
|
60
|
+
(err) => err && err.code === 'template-not-found',
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('TPL-8: CLI prints path and exits 0 for valid template', () => {
|
|
65
|
+
const stdout = makeSink();
|
|
66
|
+
const stderr = makeSink();
|
|
67
|
+
const code = cli.run(['VALIDATION'], { stdout, stderr });
|
|
68
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
69
|
+
assert.ok(fs.existsSync(stdout.toString()));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('TPL-9: CLI emits error JSON and exits 1 for missing template', () => {
|
|
73
|
+
const stdout = makeSink();
|
|
74
|
+
const stderr = makeSink();
|
|
75
|
+
const code = cli.run(['NOPE'], { stdout, stderr });
|
|
76
|
+
assert.equal(code, 1);
|
|
77
|
+
assert.match(stderr.toString(), /"code":\s*"template-not-found"/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('TPL-10: CLI with no args prints usage to stderr and exits 1', () => {
|
|
81
|
+
const stdout = makeSink();
|
|
82
|
+
const stderr = makeSink();
|
|
83
|
+
const code = cli.run([], { stdout, stderr });
|
|
84
|
+
assert.equal(code, 1);
|
|
85
|
+
assert.match(stderr.toString(), /Usage:/);
|
|
86
|
+
});
|
package/lib/agents.cjs
CHANGED
|
@@ -50,16 +50,22 @@ function validateAgentFrontmatter(fm, agentName) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function loadAgent(name, cwd) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const candidates = [];
|
|
54
|
+
try {
|
|
55
|
+
const root = findProjectRoot(cwd || process.cwd());
|
|
56
|
+
candidates.push(path.join(root, 'agents', name + '.md'));
|
|
57
|
+
} catch {}
|
|
58
|
+
candidates.push(path.resolve(__dirname, '..', 'agents', name + '.md'));
|
|
59
|
+
|
|
60
|
+
const found = candidates.find((p) => fs.existsSync(p));
|
|
61
|
+
if (!found) {
|
|
56
62
|
throw new NubosPilotError(
|
|
57
63
|
'agent-not-found',
|
|
58
|
-
'Agent "' + name + '" not found at ' +
|
|
59
|
-
{ name, path:
|
|
64
|
+
'Agent "' + name + '" not found at ' + candidates[0],
|
|
65
|
+
{ name, path: candidates[0], tried: candidates },
|
|
60
66
|
);
|
|
61
67
|
}
|
|
62
|
-
const { frontmatter } = extractFrontmatter(fs.readFileSync(
|
|
68
|
+
const { frontmatter } = extractFrontmatter(fs.readFileSync(found, 'utf-8'));
|
|
63
69
|
return validateAgentFrontmatter(frontmatter, name);
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { findProjectRoot, NubosPilotError } = require('./core.cjs');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_COMMIT_ARTIFACTS = true;
|
|
8
|
+
|
|
9
|
+
function _coerceBool(raw) {
|
|
10
|
+
if (raw === true || raw === false) return raw;
|
|
11
|
+
if (raw == null) return null;
|
|
12
|
+
const s = String(raw).trim().toLowerCase();
|
|
13
|
+
if (s === 'true' || s === '1' || s === 'yes' || s === 'on') return true;
|
|
14
|
+
if (s === 'false' || s === '0' || s === 'no' || s === 'off') return false;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readConfigCommitArtifacts(cwd) {
|
|
19
|
+
let root;
|
|
20
|
+
try {
|
|
21
|
+
root = findProjectRoot(cwd || process.cwd());
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err && err.code === 'not-in-project') return null;
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
const p = path.join(root, '.nubos-pilot', 'config.json');
|
|
27
|
+
if (!fs.existsSync(p)) return null;
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw new NubosPilotError('commit-policy-config-parse-error', 'config.json invalid JSON', { cause: err && err.message });
|
|
33
|
+
}
|
|
34
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
35
|
+
const workflow = parsed.workflow;
|
|
36
|
+
if (!workflow || typeof workflow !== 'object') return null;
|
|
37
|
+
if (!Object.prototype.hasOwnProperty.call(workflow, 'commit_artifacts')) return null;
|
|
38
|
+
return _coerceBool(workflow.commit_artifacts);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveCommitArtifacts(cwd) {
|
|
42
|
+
const fromConfig = readConfigCommitArtifacts(cwd);
|
|
43
|
+
if (fromConfig !== null) return fromConfig;
|
|
44
|
+
return DEFAULT_COMMIT_ARTIFACTS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveCommitArtifactsDetail(cwd) {
|
|
48
|
+
const fromConfig = readConfigCommitArtifacts(cwd);
|
|
49
|
+
if (fromConfig !== null) {
|
|
50
|
+
return { enabled: fromConfig, source: 'config' };
|
|
51
|
+
}
|
|
52
|
+
return { enabled: DEFAULT_COMMIT_ARTIFACTS, source: 'default' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
DEFAULT_COMMIT_ARTIFACTS,
|
|
57
|
+
readConfigCommitArtifacts,
|
|
58
|
+
resolveCommitArtifacts,
|
|
59
|
+
resolveCommitArtifactsDetail,
|
|
60
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { test } = require('node:test');
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
DEFAULT_COMMIT_ARTIFACTS,
|
|
11
|
+
readConfigCommitArtifacts,
|
|
12
|
+
resolveCommitArtifacts,
|
|
13
|
+
resolveCommitArtifactsDetail,
|
|
14
|
+
} = require('./commit-policy.cjs');
|
|
15
|
+
|
|
16
|
+
const _sandboxes = [];
|
|
17
|
+
|
|
18
|
+
function makeSandbox(config) {
|
|
19
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-commit-policy-'));
|
|
20
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
21
|
+
if (config !== undefined) {
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(root, '.nubos-pilot', 'config.json'),
|
|
24
|
+
typeof config === 'string' ? config : JSON.stringify(config),
|
|
25
|
+
);
|
|
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('CP-1: default is true when config absent', () => {
|
|
38
|
+
const sb = makeSandbox();
|
|
39
|
+
assert.equal(resolveCommitArtifacts(sb), true);
|
|
40
|
+
assert.equal(DEFAULT_COMMIT_ARTIFACTS, true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('CP-2: workflow.commit_artifacts=false is respected', () => {
|
|
44
|
+
const sb = makeSandbox({ workflow: { commit_artifacts: false } });
|
|
45
|
+
assert.equal(resolveCommitArtifacts(sb), false);
|
|
46
|
+
assert.deepEqual(resolveCommitArtifactsDetail(sb), { enabled: false, source: 'config' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('CP-3: workflow.commit_artifacts=true is respected', () => {
|
|
50
|
+
const sb = makeSandbox({ workflow: { commit_artifacts: true } });
|
|
51
|
+
assert.equal(resolveCommitArtifacts(sb), true);
|
|
52
|
+
assert.deepEqual(resolveCommitArtifactsDetail(sb), { enabled: true, source: 'config' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('CP-4: missing workflow.commit_artifacts key falls back to default', () => {
|
|
56
|
+
const sb = makeSandbox({ workflow: { text_mode: true } });
|
|
57
|
+
assert.equal(resolveCommitArtifacts(sb), true);
|
|
58
|
+
assert.deepEqual(resolveCommitArtifactsDetail(sb), { enabled: true, source: 'default' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('CP-5: invalid JSON surfaces as commit-policy-config-parse-error', () => {
|
|
62
|
+
const sb = makeSandbox('{ "workflow": { "commit_artifacts": false ');
|
|
63
|
+
assert.throws(
|
|
64
|
+
() => readConfigCommitArtifacts(sb),
|
|
65
|
+
(err) => err && err.code === 'commit-policy-config-parse-error',
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('CP-6: string "false" / "off" / "0" coerce to false', () => {
|
|
70
|
+
for (const val of ['false', 'off', '0', 'no']) {
|
|
71
|
+
const sb = makeSandbox({ workflow: { commit_artifacts: val } });
|
|
72
|
+
assert.equal(resolveCommitArtifacts(sb), false, 'expected ' + val + ' → false');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_WORKFLOW = Object.freeze({
|
|
4
|
+
commit_docs: true,
|
|
5
|
+
commit_artifacts: true,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const DEFAULT_AGENTS = Object.freeze({
|
|
9
|
+
parallelization: true,
|
|
10
|
+
research: true,
|
|
11
|
+
plan_checker: true,
|
|
12
|
+
verifier: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MODEL_PROFILE = 'frontier';
|
|
16
|
+
const DEFAULT_SCOPE = 'local';
|
|
17
|
+
const DEFAULT_RESPONSE_LANGUAGE = 'en';
|
|
18
|
+
|
|
19
|
+
function buildInstallConfig(answers) {
|
|
20
|
+
const a = answers || {};
|
|
21
|
+
return {
|
|
22
|
+
runtime: a.runtime || null,
|
|
23
|
+
runtimes: Array.isArray(a.runtimes) ? a.runtimes.slice() : (a.runtime ? [a.runtime] : []),
|
|
24
|
+
scope: a.scope || DEFAULT_SCOPE,
|
|
25
|
+
mcp: !!a.mcp,
|
|
26
|
+
model_profile: a.model_profile || DEFAULT_MODEL_PROFILE,
|
|
27
|
+
response_language: a.response_language || DEFAULT_RESPONSE_LANGUAGE,
|
|
28
|
+
workflow: { ...DEFAULT_WORKFLOW },
|
|
29
|
+
agents: { ...DEFAULT_AGENTS },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
DEFAULT_WORKFLOW,
|
|
35
|
+
DEFAULT_AGENTS,
|
|
36
|
+
DEFAULT_MODEL_PROFILE,
|
|
37
|
+
DEFAULT_SCOPE,
|
|
38
|
+
DEFAULT_RESPONSE_LANGUAGE,
|
|
39
|
+
buildInstallConfig,
|
|
40
|
+
};
|
package/lib/git.cjs
CHANGED
|
@@ -8,12 +8,14 @@ function _isFatalCheckIgnore(err) {
|
|
|
8
8
|
return err && err.status !== 1;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function isPathIgnored(p) {
|
|
11
|
+
function isPathIgnored(p, opts) {
|
|
12
|
+
const spawnOpts = { stdio: 'pipe' };
|
|
13
|
+
if (opts && opts.cwd) spawnOpts.cwd = opts.cwd;
|
|
12
14
|
try {
|
|
13
|
-
execFileSync('git', ['check-ignore', '--quiet', '--', p],
|
|
14
|
-
return true;
|
|
15
|
+
execFileSync('git', ['check-ignore', '--quiet', '--', p], spawnOpts);
|
|
16
|
+
return true;
|
|
15
17
|
} catch (err) {
|
|
16
|
-
if (err && err.status === 1) return false;
|
|
18
|
+
if (err && err.status === 1) return false;
|
|
17
19
|
if (err && err.status === 128) {
|
|
18
20
|
|
|
19
21
|
throw err;
|
|
@@ -22,7 +24,7 @@ function isPathIgnored(p) {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
function assertCommittablePaths(paths) {
|
|
27
|
+
function assertCommittablePaths(paths, opts) {
|
|
26
28
|
if (!Array.isArray(paths)) {
|
|
27
29
|
throw new NubosPilotError(
|
|
28
30
|
'commit-paths-invalid',
|
|
@@ -30,11 +32,13 @@ function assertCommittablePaths(paths) {
|
|
|
30
32
|
{ got: typeof paths },
|
|
31
33
|
);
|
|
32
34
|
}
|
|
35
|
+
const spawnOpts = { stdio: 'pipe' };
|
|
36
|
+
if (opts && opts.cwd) spawnOpts.cwd = opts.cwd;
|
|
33
37
|
const ignored = [];
|
|
34
38
|
for (const p of paths) {
|
|
35
39
|
try {
|
|
36
|
-
execFileSync('git', ['check-ignore', '--quiet', '--', p],
|
|
37
|
-
ignored.push(p);
|
|
40
|
+
execFileSync('git', ['check-ignore', '--quiet', '--', p], spawnOpts);
|
|
41
|
+
ignored.push(p);
|
|
38
42
|
} catch (err) {
|
|
39
43
|
if (_isFatalCheckIgnore(err)) {
|
|
40
44
|
if (err.status === 128) throw err;
|
package/np-tools.cjs
CHANGED
|
@@ -48,6 +48,7 @@ const topLevelCommands = {
|
|
|
48
48
|
'lang-directive': require('./bin/np-tools/lang-directive.cjs'),
|
|
49
49
|
'text-mode': require('./bin/np-tools/text-mode.cjs'),
|
|
50
50
|
'detect-runtime': require('./bin/np-tools/detect-runtime.cjs'),
|
|
51
|
+
'template-path': require('./bin/np-tools/template-path.cjs'),
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
const THRESHOLD = 16 * 1024;
|
package/package.json
CHANGED
package/workflows/add-tests.md
CHANGED
|
@@ -17,7 +17,7 @@ sentinels survive regeneration.
|
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
PHASE="$1"
|
|
20
|
-
INIT=$(node .nubos-pilot/bin/np-tools.cjs init add-tests "$PHASE")
|
|
20
|
+
INIT=$(node .nubos-pilot/bin/np-tools.cjs init add-tests init "$PHASE")
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Parse: `phase`, `target_path`, `verification_path`, `pass_cases[]`,
|
|
@@ -356,13 +356,14 @@ CONTEXT_PATH=$(echo "$INIT" | node -e 'let d="";process.stdin.on("data",c=>d+=c)
|
|
|
356
356
|
mkdir -p "$MILESTONE_DIR"
|
|
357
357
|
mkdir -p "$MILESTONE_DIR/slices"
|
|
358
358
|
|
|
359
|
+
TPL_PATH=$(node .nubos-pilot/bin/np-tools.cjs template-path milestone/CONTEXT)
|
|
359
360
|
node -e '
|
|
360
361
|
const { render } = require("./lib/template.cjs");
|
|
361
362
|
const fs = require("node:fs");
|
|
362
|
-
const tpl = fs.readFileSync(
|
|
363
|
-
const vars = JSON.parse(process.argv[
|
|
363
|
+
const tpl = fs.readFileSync(process.argv[1], "utf-8");
|
|
364
|
+
const vars = JSON.parse(process.argv[2]);
|
|
364
365
|
process.stdout.write(render(tpl, vars));
|
|
365
|
-
' "$VARS_JSON" > "$CONTEXT_PATH"
|
|
366
|
+
' "$TPL_PATH" "$VARS_JSON" > "$CONTEXT_PATH"
|
|
366
367
|
```
|
|
367
368
|
|
|
368
369
|
`$VARS_JSON` is the JSON-serialised accumulator from Steps 2–5 (keys map to
|
package/workflows/plan-phase.md
CHANGED
|
@@ -277,8 +277,11 @@ The scaffolder:
|
|
|
277
277
|
## Commit
|
|
278
278
|
|
|
279
279
|
```bash
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
COMMIT_ARTIFACTS=$(node .nubos-pilot/bin/np-tools.cjs config-get workflow.commit_artifacts 2>/dev/null || echo "true")
|
|
281
|
+
if [[ "$COMMIT_ARTIFACTS" != "false" ]]; then
|
|
282
|
+
git add "$milestone_dir"
|
|
283
|
+
git commit -m "docs(${milestone_id}): milestone plan ready for execute"
|
|
284
|
+
fi
|
|
282
285
|
```
|
|
283
286
|
|
|
284
287
|
Commits include: all milestone-level artefacts (CONTEXT/ROADMAP/META), every slice's ASSESSMENT/PLAN/UAT, and every scaffolded task file.
|
|
@@ -20,7 +20,7 @@ if [[ -z "$PHASE" ]]; then
|
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
22
|
LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
|
|
23
|
-
INIT=$(node .nubos-pilot/bin/np-tools.cjs init verify-work "$PHASE")
|
|
23
|
+
INIT=$(node .nubos-pilot/bin/np-tools.cjs init verify-work init "$PHASE")
|
|
24
24
|
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
|
25
25
|
RUNTIME=$(node .nubos-pilot/bin/np-tools.cjs detect-runtime)
|
|
26
26
|
```
|
|
@@ -42,7 +42,7 @@ the main chat. Auto-enabled in Claude Code (CLAUDECODE=1); opt-in via
|
|
|
42
42
|
MILESTONE_ID=$(echo "$INIT" | jq -r '.milestone_id')
|
|
43
43
|
MILESTONE_DIR=$(echo "$INIT" | jq -r '.milestone_dir')
|
|
44
44
|
VALIDATION_PATH="${MILESTONE_DIR}/${MILESTONE_ID}-VALIDATION.md"
|
|
45
|
-
TEMPLATE_PATH
|
|
45
|
+
TEMPLATE_PATH=$(node .nubos-pilot/bin/np-tools.cjs template-path VALIDATION)
|
|
46
46
|
REQS_PATH=".nubos-pilot/REQUIREMENTS.md"
|
|
47
47
|
PLAN_ID="${MILESTONE_ID}-validate"
|
|
48
48
|
TASK_ID="${MILESTONE_ID}-validate"
|
|
@@ -93,9 +93,9 @@ fi
|
|
|
93
93
|
### Gate 3 — Template present
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
if [[ ! -f "$TEMPLATE_PATH" ]]; then
|
|
97
|
-
echo "Error:
|
|
98
|
-
echo "Re-run 'npx nubos-pilot install' or
|
|
96
|
+
if [[ -z "$TEMPLATE_PATH" || ! -f "$TEMPLATE_PATH" ]]; then
|
|
97
|
+
echo "Error: VALIDATION template not resolvable via np-tools.cjs template-path." >&2
|
|
98
|
+
echo "Re-run 'npx nubos-pilot install' or check the package's templates/ dir." >&2
|
|
99
99
|
exit 1
|
|
100
100
|
fi
|
|
101
101
|
```
|
package/workflows/verify-work.md
CHANGED
|
@@ -17,7 +17,7 @@ Slice-level acceptance (UAT) is validated separately by `/np:validate-phase <N>`
|
|
|
17
17
|
```bash
|
|
18
18
|
PHASE="$1"
|
|
19
19
|
LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
|
|
20
|
-
INIT=$(node .nubos-pilot/bin/np-tools.cjs init verify-work "$PHASE")
|
|
20
|
+
INIT=$(node .nubos-pilot/bin/np-tools.cjs init verify-work init "$PHASE")
|
|
21
21
|
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
|
22
22
|
AGENT_SKILLS_VERIFIER=$(node .nubos-pilot/bin/np-tools.cjs agent-skills verifier 2>/dev/null)
|
|
23
23
|
```
|