nubos-pilot 0.5.6 → 0.5.8
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/np-tools/commit.cjs +6 -0
- package/bin/np-tools/commit.test.cjs +44 -0
- package/bin/np-tools/execute-milestone.cjs +92 -0
- package/bin/np-tools/execute-milestone.test.cjs +95 -0
- package/lib/agents.cjs +12 -6
- package/lib/commit-policy.cjs +60 -0
- package/lib/commit-policy.test.cjs +74 -0
- package/package.json +1 -1
- package/workflows/add-tests.md +1 -1
- package/workflows/execute-phase.md +9 -0
- package/workflows/plan-phase.md +5 -2
- package/workflows/validate-phase.md +1 -1
- package/workflows/verify-work.md +1 -1
package/bin/np-tools/commit.cjs
CHANGED
|
@@ -2,6 +2,7 @@ const { execFileSync } = require('node:child_process');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
4
|
const { assertCommittablePaths } = require('../../lib/git.cjs');
|
|
5
|
+
const { resolveCommitArtifacts } = require('../../lib/commit-policy.cjs');
|
|
5
6
|
|
|
6
7
|
const MAX_MSG = 2000;
|
|
7
8
|
|
|
@@ -65,6 +66,7 @@ function _validateFiles(files) {
|
|
|
65
66
|
|
|
66
67
|
function run(argv, ctx) {
|
|
67
68
|
const context = ctx || {};
|
|
69
|
+
const cwd = context.cwd || process.cwd();
|
|
68
70
|
const stdout = context.stdout || process.stdout;
|
|
69
71
|
const stderr = context.stderr || process.stderr;
|
|
70
72
|
try {
|
|
@@ -81,6 +83,10 @@ function run(argv, ctx) {
|
|
|
81
83
|
return 1;
|
|
82
84
|
}
|
|
83
85
|
_validateFiles(files);
|
|
86
|
+
if (resolveCommitArtifacts(cwd) === false) {
|
|
87
|
+
stdout.write(JSON.stringify({ committed: false, reason: 'commit_artifacts=false', files }) + '\n');
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
84
90
|
const committable = assertCommittablePaths(files);
|
|
85
91
|
if (committable.length === 0) {
|
|
86
92
|
throw new NubosPilotError('commit-no-paths', 'commit invoked with no committable paths', { files });
|
|
@@ -91,3 +91,47 @@ test('COMMIT-4: overlong message exceeds limit → commit-message-too-long', ()
|
|
|
91
91
|
assert.equal(code, 1);
|
|
92
92
|
assert.match(stderr.toString(), /"code":\s*"commit-message-too-long"/);
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
test('COMMIT-5: workflow.commit_artifacts=false skips commit silently with exit 0', () => {
|
|
96
|
+
const sb = makeSandbox();
|
|
97
|
+
initGit(sb);
|
|
98
|
+
fs.writeFileSync(path.join(sb, 'note.md'), 'x');
|
|
99
|
+
fs.writeFileSync(
|
|
100
|
+
path.join(sb, '.nubos-pilot', 'config.json'),
|
|
101
|
+
JSON.stringify({ workflow: { commit_artifacts: false } }),
|
|
102
|
+
);
|
|
103
|
+
const stdout = makeSink();
|
|
104
|
+
const stderr = makeSink();
|
|
105
|
+
const code = commitCli.run(['docs: note', '--files', 'note.md'], { cwd: sb, stdout, stderr });
|
|
106
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
107
|
+
const out = stdout.toString();
|
|
108
|
+
assert.match(out, /"committed":\s*false/);
|
|
109
|
+
assert.match(out, /"reason":\s*"commit_artifacts=false"/);
|
|
110
|
+
let logOut = '';
|
|
111
|
+
try {
|
|
112
|
+
logOut = execFileSync('git', ['log', '--oneline'], { cwd: sb, encoding: 'utf-8' }).trim();
|
|
113
|
+
} catch { logOut = ''; }
|
|
114
|
+
assert.equal(logOut, '', 'expected no commits to be created');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('COMMIT-6: workflow.commit_artifacts=true still commits normally', () => {
|
|
118
|
+
const sb = makeSandbox();
|
|
119
|
+
initGit(sb);
|
|
120
|
+
fs.writeFileSync(path.join(sb, 'note.md'), 'x');
|
|
121
|
+
fs.writeFileSync(
|
|
122
|
+
path.join(sb, '.nubos-pilot', 'config.json'),
|
|
123
|
+
JSON.stringify({ workflow: { commit_artifacts: true } }),
|
|
124
|
+
);
|
|
125
|
+
const stdout = makeSink();
|
|
126
|
+
const stderr = makeSink();
|
|
127
|
+
const origCwd = process.cwd();
|
|
128
|
+
process.chdir(sb);
|
|
129
|
+
let code;
|
|
130
|
+
try {
|
|
131
|
+
code = commitCli.run(['docs: note', '--files', 'note.md'], { stdout, stderr });
|
|
132
|
+
} finally {
|
|
133
|
+
process.chdir(origCwd);
|
|
134
|
+
}
|
|
135
|
+
assert.equal(code, 0, 'stderr=' + stderr.toString());
|
|
136
|
+
assert.match(stdout.toString(), /"committed":\s*true/);
|
|
137
|
+
});
|
|
@@ -8,6 +8,7 @@ const crypto = require('node:crypto');
|
|
|
8
8
|
const {
|
|
9
9
|
NubosPilotError,
|
|
10
10
|
projectStateDir,
|
|
11
|
+
atomicWriteFileSync,
|
|
11
12
|
} = require('../../lib/core.cjs');
|
|
12
13
|
const layout = require('../../lib/layout.cjs');
|
|
13
14
|
const { getPhase } = require('../../lib/roadmap.cjs');
|
|
@@ -141,6 +142,84 @@ function _initPayload(mNum, cwd) {
|
|
|
141
142
|
};
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
function _readTaskSummaryBody(summaryPath) {
|
|
146
|
+
if (!fs.existsSync(summaryPath)) return null;
|
|
147
|
+
const raw = fs.readFileSync(summaryPath, 'utf-8');
|
|
148
|
+
const { body } = extractFrontmatter(raw);
|
|
149
|
+
return String(body || '').trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _finalizeSlice(mNum, sNum, cwd) {
|
|
153
|
+
const slicePath = layout.findSliceDir(mNum, sNum, cwd);
|
|
154
|
+
if (!slicePath) {
|
|
155
|
+
throw new NubosPilotError(
|
|
156
|
+
'finalize-slice-not-found',
|
|
157
|
+
'Slice ' + layout.sliceFullId(mNum, sNum) + ' does not exist',
|
|
158
|
+
{ milestone: mNum, slice: sNum },
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const summaryPath = layout.sliceSummaryPath(mNum, sNum, cwd);
|
|
162
|
+
const tasks = _sliceTasksSorted(mNum, sNum, cwd);
|
|
163
|
+
const doneTasks = tasks.filter((t) => t.status === 'done');
|
|
164
|
+
const pendingTasks = tasks.filter((t) => t.status !== 'done');
|
|
165
|
+
|
|
166
|
+
const lines = [
|
|
167
|
+
'---',
|
|
168
|
+
'slice: ' + JSON.stringify(layout.sliceFullId(mNum, sNum)),
|
|
169
|
+
'milestone: ' + JSON.stringify(layout.mId(mNum)),
|
|
170
|
+
'type: slice-summary',
|
|
171
|
+
'task_count: ' + tasks.length,
|
|
172
|
+
'tasks_done: ' + doneTasks.length,
|
|
173
|
+
'tasks_pending: ' + pendingTasks.length,
|
|
174
|
+
'generated_at: ' + JSON.stringify(new Date().toISOString()),
|
|
175
|
+
'---',
|
|
176
|
+
'',
|
|
177
|
+
'# ' + layout.sliceFullId(mNum, sNum) + ' — SUMMARY',
|
|
178
|
+
'',
|
|
179
|
+
'_Auto-aggregated from task summaries by `execute-milestone finalize-slice`._',
|
|
180
|
+
'',
|
|
181
|
+
'## Task Roll-Up',
|
|
182
|
+
'',
|
|
183
|
+
'| Task | Status | Name |',
|
|
184
|
+
'|------|--------|------|',
|
|
185
|
+
];
|
|
186
|
+
for (const t of tasks) {
|
|
187
|
+
lines.push('| ' + t.id + ' | ' + t.status + ' | ' + (t.name || '').replace(/\|/g, '\\|') + ' |');
|
|
188
|
+
}
|
|
189
|
+
lines.push('', '## Task Summaries', '');
|
|
190
|
+
for (const t of tasks) {
|
|
191
|
+
lines.push('### ' + t.id + ' — ' + (t.name || ''));
|
|
192
|
+
lines.push('');
|
|
193
|
+
const body = _readTaskSummaryBody(t.summary_path);
|
|
194
|
+
if (body) {
|
|
195
|
+
lines.push(body);
|
|
196
|
+
} else {
|
|
197
|
+
lines.push('_No T<NNNN>-SUMMARY.md file present._');
|
|
198
|
+
}
|
|
199
|
+
lines.push('');
|
|
200
|
+
}
|
|
201
|
+
atomicWriteFileSync(summaryPath, lines.join('\n'));
|
|
202
|
+
return {
|
|
203
|
+
slice: layout.sliceFullId(mNum, sNum),
|
|
204
|
+
summary_path: summaryPath,
|
|
205
|
+
task_count: tasks.length,
|
|
206
|
+
tasks_done: doneTasks.length,
|
|
207
|
+
tasks_pending: pendingTasks.length,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function _finalizeMilestone(mNum, cwd) {
|
|
212
|
+
const slices = layout.listSlices(mNum, cwd);
|
|
213
|
+
if (slices.length === 0) {
|
|
214
|
+
return { milestone: layout.mId(mNum), finalized: [], reason: 'no-slices' };
|
|
215
|
+
}
|
|
216
|
+
const finalized = [];
|
|
217
|
+
for (const s of slices) {
|
|
218
|
+
finalized.push(_finalizeSlice(mNum, s.number, cwd));
|
|
219
|
+
}
|
|
220
|
+
return { milestone: layout.mId(mNum), finalized, reason: 'ok' };
|
|
221
|
+
}
|
|
222
|
+
|
|
144
223
|
function _findTaskByFullId(mNum, taskFullId, cwd) {
|
|
145
224
|
let parsed;
|
|
146
225
|
try {
|
|
@@ -218,6 +297,19 @@ function run(args, ctx) {
|
|
|
218
297
|
_emit(payload, stdout, cwd);
|
|
219
298
|
return payload;
|
|
220
299
|
}
|
|
300
|
+
case 'finalize-slice': {
|
|
301
|
+
const mNum = _validateMilestoneArg(list[1]);
|
|
302
|
+
const sNum = _validateMilestoneArg(list[2]);
|
|
303
|
+
const payload = _finalizeSlice(mNum, sNum, cwd);
|
|
304
|
+
_emit(payload, stdout, cwd);
|
|
305
|
+
return payload;
|
|
306
|
+
}
|
|
307
|
+
case 'finalize-milestone': {
|
|
308
|
+
const mNum = _validateMilestoneArg(list[1]);
|
|
309
|
+
const payload = _finalizeMilestone(mNum, cwd);
|
|
310
|
+
_emit(payload, stdout, cwd);
|
|
311
|
+
return payload;
|
|
312
|
+
}
|
|
221
313
|
default:
|
|
222
314
|
throw new NubosPilotError(
|
|
223
315
|
'execute-milestone-unknown-verb',
|
|
@@ -152,3 +152,98 @@ test('EM-6: unknown verb throws', () => {
|
|
|
152
152
|
(err) => err && err.code === 'execute-milestone-unknown-verb',
|
|
153
153
|
);
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
function _writeTaskSummary(sandbox, mNum, sNum, tNum, body) {
|
|
157
|
+
const mId = 'M' + String(mNum).padStart(3, '0');
|
|
158
|
+
const sId = 'S' + String(sNum).padStart(3, '0');
|
|
159
|
+
const tId = 'T' + String(tNum).padStart(4, '0');
|
|
160
|
+
const fullId = mId + '-' + sId + '-' + tId;
|
|
161
|
+
const dir = path.join(sandbox, '.nubos-pilot', 'milestones', mId, 'slices', sId, 'tasks', tId);
|
|
162
|
+
const content = [
|
|
163
|
+
'---',
|
|
164
|
+
'id: ' + JSON.stringify(fullId),
|
|
165
|
+
'status: done',
|
|
166
|
+
'---',
|
|
167
|
+
'',
|
|
168
|
+
body,
|
|
169
|
+
'',
|
|
170
|
+
].join('\n');
|
|
171
|
+
fs.writeFileSync(path.join(dir, tId + '-SUMMARY.md'), content);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
test('EM-7: finalize-slice writes S<NNN>-SUMMARY.md aggregating task summaries', () => {
|
|
175
|
+
const sandbox = makeSandbox();
|
|
176
|
+
seedRoadmapYaml(sandbox, _roadmap());
|
|
177
|
+
seedMilestoneDir(sandbox, 1, {});
|
|
178
|
+
seedSliceDir(sandbox, 1, 1, {});
|
|
179
|
+
_seedTask(sandbox, 1, 1, 1, ['src/a.ts']);
|
|
180
|
+
_seedTask(sandbox, 1, 1, 2, ['src/b.ts']);
|
|
181
|
+
_writeTaskSummary(sandbox, 1, 1, 1, '## Changes\n- Added src/a.ts');
|
|
182
|
+
_writeTaskSummary(sandbox, 1, 1, 2, '## Changes\n- Added src/b.ts');
|
|
183
|
+
|
|
184
|
+
const cap = _capture();
|
|
185
|
+
subcmd.run(['finalize-slice', '1', '1'], { cwd: sandbox, stdout: cap.stub });
|
|
186
|
+
const out = JSON.parse(cap.get());
|
|
187
|
+
assert.equal(out.slice, 'M001-S001');
|
|
188
|
+
assert.equal(out.task_count, 2);
|
|
189
|
+
|
|
190
|
+
const summaryPath = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'S001-SUMMARY.md');
|
|
191
|
+
assert.ok(fs.existsSync(summaryPath));
|
|
192
|
+
const body = fs.readFileSync(summaryPath, 'utf-8');
|
|
193
|
+
assert.match(body, /slice: "M001-S001"/);
|
|
194
|
+
assert.match(body, /type: slice-summary/);
|
|
195
|
+
assert.match(body, /### M001-S001-T0001/);
|
|
196
|
+
assert.match(body, /### M001-S001-T0002/);
|
|
197
|
+
assert.match(body, /Added src\/a.ts/);
|
|
198
|
+
assert.match(body, /Added src\/b.ts/);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('EM-8: finalize-slice fails when slice directory does not exist', () => {
|
|
202
|
+
const sandbox = makeSandbox();
|
|
203
|
+
seedRoadmapYaml(sandbox, _roadmap());
|
|
204
|
+
seedMilestoneDir(sandbox, 1, {});
|
|
205
|
+
const cap = _capture();
|
|
206
|
+
assert.throws(
|
|
207
|
+
() => subcmd.run(['finalize-slice', '1', '9'], { cwd: sandbox, stdout: cap.stub }),
|
|
208
|
+
(err) => err && err.code === 'finalize-slice-not-found',
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('EM-9: finalize-milestone iterates every slice and produces one summary per slice', () => {
|
|
213
|
+
const sandbox = makeSandbox();
|
|
214
|
+
seedRoadmapYaml(sandbox, _roadmap());
|
|
215
|
+
seedMilestoneDir(sandbox, 1, {});
|
|
216
|
+
seedSliceDir(sandbox, 1, 1, {});
|
|
217
|
+
seedSliceDir(sandbox, 1, 2, {});
|
|
218
|
+
_seedTask(sandbox, 1, 1, 1, ['src/a.ts']);
|
|
219
|
+
_seedTask(sandbox, 1, 2, 1, ['src/c.ts']);
|
|
220
|
+
|
|
221
|
+
const cap = _capture();
|
|
222
|
+
subcmd.run(['finalize-milestone', '1'], { cwd: sandbox, stdout: cap.stub });
|
|
223
|
+
const out = JSON.parse(cap.get());
|
|
224
|
+
assert.equal(out.milestone, 'M001');
|
|
225
|
+
assert.equal(out.finalized.length, 2);
|
|
226
|
+
assert.equal(out.reason, 'ok');
|
|
227
|
+
|
|
228
|
+
const s1 = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'S001-SUMMARY.md');
|
|
229
|
+
const s2 = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S002', 'S002-SUMMARY.md');
|
|
230
|
+
assert.ok(fs.existsSync(s1));
|
|
231
|
+
assert.ok(fs.existsSync(s2));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('EM-10: finalize-slice marks tasks without SUMMARY.md but does not fail', () => {
|
|
235
|
+
const sandbox = makeSandbox();
|
|
236
|
+
seedRoadmapYaml(sandbox, _roadmap());
|
|
237
|
+
seedMilestoneDir(sandbox, 1, {});
|
|
238
|
+
seedSliceDir(sandbox, 1, 1, {});
|
|
239
|
+
_seedTask(sandbox, 1, 1, 1, ['src/a.ts']);
|
|
240
|
+
const dir = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'tasks', 'T0001');
|
|
241
|
+
fs.rmSync(path.join(dir, 'T0001-SUMMARY.md'));
|
|
242
|
+
|
|
243
|
+
const cap = _capture();
|
|
244
|
+
subcmd.run(['finalize-slice', '1', '1'], { cwd: sandbox, stdout: cap.stub });
|
|
245
|
+
const out = JSON.parse(cap.get());
|
|
246
|
+
assert.equal(out.task_count, 1);
|
|
247
|
+
const body = fs.readFileSync(out.summary_path, 'utf-8');
|
|
248
|
+
assert.match(body, /No T<NNNN>-SUMMARY.md file present/);
|
|
249
|
+
});
|
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
|
+
});
|
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[]`,
|
|
@@ -136,7 +136,16 @@ for WAVE_INDEX in 0 1 2 ...; do
|
|
|
136
136
|
fi
|
|
137
137
|
done
|
|
138
138
|
# wait for all parallel executors in this wave to finish before next wave
|
|
139
|
+
|
|
140
|
+
# After every task in the slice committed: aggregate per-task summaries into
|
|
141
|
+
# the slice-level S<NNN>-SUMMARY.md so /np:validate-phase can audit it.
|
|
142
|
+
SLICE_NUM=$(echo "$WAVE" | node -e "process.stdin.on('data', d => console.log(JSON.parse(d).wave))")
|
|
143
|
+
node .nubos-pilot/bin/np-tools.cjs init execute-milestone finalize-slice "$PHASE" "$SLICE_NUM" >/dev/null
|
|
139
144
|
done
|
|
145
|
+
|
|
146
|
+
# Milestone done — regenerate every slice summary so retroactive / resumed
|
|
147
|
+
# runs also end with a complete audit surface.
|
|
148
|
+
node .nubos-pilot/bin/np-tools.cjs init execute-milestone finalize-milestone "$PHASE" >/dev/null
|
|
140
149
|
```
|
|
141
150
|
|
|
142
151
|
After every slice completes, point the operator at `/np:validate-phase $PHASE` to run the UAT per slice.
|
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
|
```
|
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
|
```
|