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 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
- commit_docs: true,
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' },
@@ -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 _validateFiles(files) {
50
- for (const f of files) {
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 segments = String(f).split(/[/\\]/);
55
- for (const seg of segments) {
56
- if (seg === '..') {
57
- throw new NubosPilotError('commit-path-traversal', 'commit path must not contain ".." segments', { path: f });
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
- if (path.isAbsolute(f)) {
61
- throw new NubosPilotError('commit-path-absolute', 'commit path must be relative', { path: f });
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
- const committable = assertCommittablePaths(files);
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 with ".." segment rejected with commit-path-traversal', () => {
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(['feat: x', '--files', '../outside.txt'], { stdout, stderr });
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-traversal"/);
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 root = findProjectRoot(cwd || process.cwd());
54
- const p = path.join(root, 'agents', name + '.md');
55
- if (!fs.existsSync(p)) {
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 ' + p,
59
- { name, path: p },
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(p, 'utf-8'));
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], { stdio: 'pipe' });
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], { stdio: 'pipe' });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "AI-driven planning and execution tool for code projects",
5
5
  "homepage": "https://github.com/Nubos-AI/nubos-pilot",
6
6
  "repository": {
@@ -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("templates/milestone/CONTEXT.md", "utf-8");
363
- const vars = JSON.parse(process.argv[1]);
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
@@ -277,8 +277,11 @@ The scaffolder:
277
277
  ## Commit
278
278
 
279
279
  ```bash
280
- git add "$milestone_dir"
281
- git commit -m "docs(${milestone_id}): milestone plan ready for execute"
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="templates/VALIDATION.md"
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: $TEMPLATE_PATH missing." >&2
98
- echo "Re-run 'npx nubos-pilot install' or restore templates/VALIDATION.md from source." >&2
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
  ```
@@ -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
  ```