nubos-pilot 1.3.3 → 1.3.5
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-task-architect.md +95 -0
- package/agents/np-test-writer.md +89 -0
- package/bin/install.js +73 -16
- package/bin/np-tools/commit-task.cjs +80 -6
- package/bin/np-tools/commit-task.test.cjs +133 -0
- package/bin/np-tools/loop-commands.test.cjs +121 -2
- package/bin/np-tools/loop-run-round.cjs +121 -5
- package/lib/agents-registry.cjs +10 -0
- package/lib/agents.test.cjs +2 -0
- package/lib/config-defaults.cjs +8 -0
- package/lib/config-schema.cjs +2 -0
- package/lib/git.cjs +6 -2
- package/lib/git.test.cjs +28 -0
- package/lib/template.cjs +20 -15
- package/lib/template.test.cjs +35 -0
- package/package.json +1 -1
- package/templates/RULES.md +36 -1
- package/workflows/execute-phase.md +138 -1
- package/workflows/plan-phase.md +17 -2
|
@@ -426,6 +426,125 @@ function _seedSpawnEvidence(taskId, round, agents, cwd) {
|
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
test('LCLI-RR-2a: phase=post-architect refuses without np-task-architect spawn audit', async () => {
|
|
430
|
+
const r = _mkRoot();
|
|
431
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
432
|
+
require('../../lib/nubosloop.cjs').recordLoopState('M001-S001-T0001', { round: 1 }, r);
|
|
433
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
434
|
+
await assert.rejects(
|
|
435
|
+
() => loopRunRound.run(
|
|
436
|
+
['M001-S001-T0001', '--phase', 'post-architect'],
|
|
437
|
+
{ cwd: r, stdout: _cap().stub },
|
|
438
|
+
),
|
|
439
|
+
(err) => err && err.code === 'loop-post-architect-missing-spawn-audit',
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('LCLI-RR-2b: phase=post-architect with stamp → spawn-test-writer, no round bump', async () => {
|
|
444
|
+
const r = _mkRoot();
|
|
445
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
446
|
+
_seedSpawnEvidence('M001-S001-T0001', 1, ['np-task-architect'], r);
|
|
447
|
+
const cap = _cap();
|
|
448
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
449
|
+
await loopRunRound.run(['M001-S001-T0001', '--phase', 'post-architect'], { cwd: r, stdout: cap.stub });
|
|
450
|
+
const out = JSON.parse(cap.get());
|
|
451
|
+
assert.equal(out.phase, 'post-architect');
|
|
452
|
+
assert.equal(out.next_action, 'spawn-test-writer');
|
|
453
|
+
const cp = checkpoint.readCheckpoint('M001-S001-T0001', r);
|
|
454
|
+
assert.equal(cp.nubosloop.last_phase, 'post-architect');
|
|
455
|
+
assert.equal(cp.nubosloop.round, 1, 'prep step must not bump the round counter');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('LCLI-RR-2c: phase=post-architect --force-post-architect bypasses the audit gate', async () => {
|
|
459
|
+
const r = _mkRoot();
|
|
460
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
461
|
+
require('../../lib/nubosloop.cjs').recordLoopState('M001-S001-T0001', { round: 1 }, r);
|
|
462
|
+
const cap = _cap();
|
|
463
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
464
|
+
await loopRunRound.run(
|
|
465
|
+
['M001-S001-T0001', '--phase', 'post-architect', '--force-post-architect'],
|
|
466
|
+
{ cwd: r, stdout: cap.stub },
|
|
467
|
+
);
|
|
468
|
+
const out = JSON.parse(cap.get());
|
|
469
|
+
assert.equal(out.forced, true);
|
|
470
|
+
assert.equal(out.next_action, 'spawn-test-writer');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('LCLI-RR-2d: phase=post-test-writer refuses without np-test-writer spawn audit', async () => {
|
|
474
|
+
const r = _mkRoot();
|
|
475
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
476
|
+
require('../../lib/nubosloop.cjs').recordLoopState('M001-S001-T0001', { round: 1 }, r);
|
|
477
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
478
|
+
await assert.rejects(
|
|
479
|
+
() => loopRunRound.run(
|
|
480
|
+
['M001-S001-T0001', '--phase', 'post-test-writer'],
|
|
481
|
+
{ cwd: r, stdout: _cap().stub },
|
|
482
|
+
),
|
|
483
|
+
(err) => err && err.code === 'loop-post-test-writer-missing-spawn-audit',
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('LCLI-RR-2e: phase=post-test-writer with stamp → spawn-executor, no round bump', async () => {
|
|
488
|
+
const r = _mkRoot();
|
|
489
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
490
|
+
_seedSpawnEvidence('M001-S001-T0001', 1, ['np-test-writer'], r);
|
|
491
|
+
const cap = _cap();
|
|
492
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
493
|
+
await loopRunRound.run(['M001-S001-T0001', '--phase', 'post-test-writer'], { cwd: r, stdout: cap.stub });
|
|
494
|
+
const out = JSON.parse(cap.get());
|
|
495
|
+
assert.equal(out.phase, 'post-test-writer');
|
|
496
|
+
assert.equal(out.next_action, 'spawn-executor');
|
|
497
|
+
const cp = checkpoint.readCheckpoint('M001-S001-T0001', r);
|
|
498
|
+
assert.equal(cp.nubosloop.last_phase, 'post-test-writer');
|
|
499
|
+
assert.equal(cp.nubosloop.round, 1);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('LCLI-RR-2f: phase=post-test-writer --tests records the written paths in nubosloop.tdd_tests', async () => {
|
|
503
|
+
const r = _mkRoot();
|
|
504
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
505
|
+
_seedSpawnEvidence('M001-S001-T0001', 1, ['np-test-writer'], r);
|
|
506
|
+
const cap = _cap();
|
|
507
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
508
|
+
await loopRunRound.run(
|
|
509
|
+
['M001-S001-T0001', '--phase', 'post-test-writer', '--tests', 'tests/a.test.ts, tests/b.test.ts'],
|
|
510
|
+
{ cwd: r, stdout: cap.stub },
|
|
511
|
+
);
|
|
512
|
+
const out = JSON.parse(cap.get());
|
|
513
|
+
assert.deepEqual(out.tdd_tests, ['tests/a.test.ts', 'tests/b.test.ts']);
|
|
514
|
+
const cp = checkpoint.readCheckpoint('M001-S001-T0001', r);
|
|
515
|
+
assert.deepEqual(cp.nubosloop.tdd_tests, ['tests/a.test.ts', 'tests/b.test.ts']);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('LCLI-RR-2g: post-researcher next_action skips disabled prep steps (architect off → test-writer)', async () => {
|
|
519
|
+
const r = _mkRoot();
|
|
520
|
+
fs.writeFileSync(
|
|
521
|
+
path.join(r, '.nubos-pilot', 'config.json'),
|
|
522
|
+
JSON.stringify({ agents: { architect: false } }),
|
|
523
|
+
'utf-8',
|
|
524
|
+
);
|
|
525
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
526
|
+
_seedSpawnEvidence('M001-S001-T0001', 1, ['np-researcher', 'np-researcher', 'np-researcher'], r);
|
|
527
|
+
const cap = _cap();
|
|
528
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
529
|
+
await loopRunRound.run(['M001-S001-T0001', '--phase', 'post-researcher'], { cwd: r, stdout: cap.stub });
|
|
530
|
+
assert.equal(JSON.parse(cap.get()).next_action, 'spawn-test-writer');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('LCLI-RR-2h: post-researcher next_action → executor when both prep steps are off', async () => {
|
|
534
|
+
const r = _mkRoot();
|
|
535
|
+
fs.writeFileSync(
|
|
536
|
+
path.join(r, '.nubos-pilot', 'config.json'),
|
|
537
|
+
JSON.stringify({ agents: { architect: false, test_writer: false } }),
|
|
538
|
+
'utf-8',
|
|
539
|
+
);
|
|
540
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
541
|
+
_seedSpawnEvidence('M001-S001-T0001', 1, ['np-researcher', 'np-researcher', 'np-researcher'], r);
|
|
542
|
+
const cap = _cap();
|
|
543
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
544
|
+
await loopRunRound.run(['M001-S001-T0001', '--phase', 'post-researcher'], { cwd: r, stdout: cap.stub });
|
|
545
|
+
assert.equal(JSON.parse(cap.get()).next_action, 'spawn-executor');
|
|
546
|
+
});
|
|
547
|
+
|
|
429
548
|
test('LCLI-RR-3: loop-run-round phase=post-executor with verify-green → spawn-critic-schwarm', async () => {
|
|
430
549
|
const r = _mkRoot();
|
|
431
550
|
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
@@ -1184,7 +1303,7 @@ test('LCLI-RR-35: post-researcher accepts when k=3 np-researcher audits exist (T
|
|
|
1184
1303
|
{ cwd: r, stdout: cap.stub },
|
|
1185
1304
|
);
|
|
1186
1305
|
const out = JSON.parse(cap.get());
|
|
1187
|
-
assert.equal(out.next_action, 'spawn-
|
|
1306
|
+
assert.equal(out.next_action, 'spawn-architect');
|
|
1188
1307
|
assert.equal(out.forced, false);
|
|
1189
1308
|
assert.equal(out.expected_researcher_count, 3);
|
|
1190
1309
|
});
|
|
@@ -1207,7 +1326,7 @@ test('LCLI-RR-35b: post-researcher k-gate honors swarm.research.k config (Gap #6
|
|
|
1207
1326
|
);
|
|
1208
1327
|
const out = JSON.parse(cap.get());
|
|
1209
1328
|
assert.equal(out.expected_researcher_count, 1);
|
|
1210
|
-
assert.equal(out.next_action, 'spawn-
|
|
1329
|
+
assert.equal(out.next_action, 'spawn-architect');
|
|
1211
1330
|
});
|
|
1212
1331
|
|
|
1213
1332
|
test('LCLI-RR-36: --force-post-researcher bypasses Layer-C gate + stamps flag (T2)', async () => {
|
|
@@ -7,6 +7,7 @@ const { NubosPilotError, safeAssign } = require('../../lib/core.cjs');
|
|
|
7
7
|
const safePath = require('../../lib/safe-path.cjs');
|
|
8
8
|
|
|
9
9
|
const checkpoint = require('../../lib/checkpoint.cjs');
|
|
10
|
+
const config = require('../../lib/config.cjs');
|
|
10
11
|
const nubosloop = require('../../lib/nubosloop.cjs');
|
|
11
12
|
const messaging = require('../../lib/messaging.cjs');
|
|
12
13
|
const compress = require('../../lib/compress.cjs');
|
|
@@ -40,6 +41,8 @@ function _verifyExcerpt(verifyOutput, cwd) {
|
|
|
40
41
|
const VALID_PHASES = new Set([
|
|
41
42
|
'preflight',
|
|
42
43
|
'post-researcher',
|
|
44
|
+
'post-architect',
|
|
45
|
+
'post-test-writer',
|
|
43
46
|
'post-executor',
|
|
44
47
|
'post-critics',
|
|
45
48
|
'commit',
|
|
@@ -153,13 +156,124 @@ function _runPostResearcher(taskId, list, cwd) {
|
|
|
153
156
|
);
|
|
154
157
|
return {
|
|
155
158
|
phase: 'post-researcher',
|
|
156
|
-
next_action:
|
|
159
|
+
next_action: _nextAfterResearcher(cwd),
|
|
157
160
|
forced: force,
|
|
158
161
|
expected_researcher_count: expectedK,
|
|
159
162
|
round: merged.nubosloop ? merged.nubosloop.round : null,
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
// Round-1 preparation steps (architect, test-writer) that run between the
|
|
167
|
+
// researcher swarm and the executor. Each verifies its spawn was stamped via
|
|
168
|
+
// `loop-audit-tool-use` (Layer-C SKIP-GUARD) and records last_phase. They never
|
|
169
|
+
// bump the round counter — TDD writes tests once; build-fixer rounds iterate.
|
|
170
|
+
// Config-gated in the orchestrator: when agents.architect / agents.test_writer
|
|
171
|
+
// is off the step (and this phase) is simply never invoked.
|
|
172
|
+
function _checkPrepSpawnAudited(taskId, list, cwd, agent, forceFlag) {
|
|
173
|
+
const force = list.includes(forceFlag);
|
|
174
|
+
const cur = checkpoint.readCheckpoint(taskId, cwd) || {};
|
|
175
|
+
const round = Number((cur.nubosloop && cur.nubosloop.round)) || 1;
|
|
176
|
+
const satisfied = force
|
|
177
|
+
? true
|
|
178
|
+
: nubosloop.assertSpawnsAuditedForRound(taskId, [agent], round, cwd).satisfied;
|
|
179
|
+
return { force, round, satisfied };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _stampPrepPhase(taskId, cwd, phase, lastAction, forcedKey, force, extraFn) {
|
|
183
|
+
return checkpoint.mergeCheckpoint(
|
|
184
|
+
taskId,
|
|
185
|
+
(curCp) => {
|
|
186
|
+
const prev = (curCp && curCp.nubosloop) || {};
|
|
187
|
+
const partial = { last_phase: phase, last_action: lastAction };
|
|
188
|
+
if (force) partial[forcedKey] = true;
|
|
189
|
+
if (typeof extraFn === 'function') safeAssign(partial, extraFn(prev));
|
|
190
|
+
return { nubosloop: safeAssign({}, prev, partial) };
|
|
191
|
+
},
|
|
192
|
+
cwd,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The round-1 prep steps are config-gated, so the emitted next_action hint must
|
|
197
|
+
// reflect which downstream steps are actually enabled — a consumer driving off
|
|
198
|
+
// the JSON hint (rather than the ACTION CONTRACT prose) must not skip an enabled
|
|
199
|
+
// architect/test-writer or stall on a disabled one.
|
|
200
|
+
function _nextEnabled(cwd, steps) {
|
|
201
|
+
for (const [path, action] of steps) {
|
|
202
|
+
if (config.tryReadConfigPath(cwd, path, true, { onWarn() {} })) return action;
|
|
203
|
+
}
|
|
204
|
+
return 'spawn-executor';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function _nextAfterResearcher(cwd) {
|
|
208
|
+
return _nextEnabled(cwd, [['agents.architect', 'spawn-architect'], ['agents.test_writer', 'spawn-test-writer']]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function _nextAfterArchitect(cwd) {
|
|
212
|
+
return _nextEnabled(cwd, [['agents.test_writer', 'spawn-test-writer']]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// `--tests "a, b, c"` → ['a','b','c']; the post-test-writer phase records these
|
|
216
|
+
// (the paths np-test-writer wrote) so commit-task can fold them into the commit.
|
|
217
|
+
function _parseTestPaths(raw) {
|
|
218
|
+
if (typeof raw !== 'string' || raw.trim() === '') return [];
|
|
219
|
+
return raw.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function _runPostArchitect(taskId, list, cwd) {
|
|
223
|
+
const g = _checkPrepSpawnAudited(taskId, list, cwd, 'np-task-architect', '--force-post-architect');
|
|
224
|
+
if (!g.satisfied) {
|
|
225
|
+
throw new NubosPilotError(
|
|
226
|
+
'loop-post-architect-missing-spawn-audit',
|
|
227
|
+
'phase=post-architect refused: no `loop-audit-tool-use` record for round=' + g.round +
|
|
228
|
+
', agent=np-task-architect on ' + taskId + '. ' +
|
|
229
|
+
'Spawn np-task-architect and call `loop-audit-tool-use ' + taskId +
|
|
230
|
+
' --agent np-task-architect --tool-use-log <json>` after the spawn, ' +
|
|
231
|
+
'or pass --force-post-architect for an explicit override.',
|
|
232
|
+
{ taskId, round: g.round, missing: ['np-task-architect'], required: ['np-task-architect'] },
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const merged = _stampPrepPhase(taskId, cwd, 'post-architect', 'architect-spawned', 'forced_post_architect', g.force);
|
|
236
|
+
return {
|
|
237
|
+
phase: 'post-architect',
|
|
238
|
+
next_action: _nextAfterArchitect(cwd),
|
|
239
|
+
forced: g.force,
|
|
240
|
+
round: merged.nubosloop ? merged.nubosloop.round : g.round,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _runPostTestWriter(taskId, list, cwd) {
|
|
245
|
+
const g = _checkPrepSpawnAudited(taskId, list, cwd, 'np-test-writer', '--force-post-test-writer');
|
|
246
|
+
if (!g.satisfied) {
|
|
247
|
+
throw new NubosPilotError(
|
|
248
|
+
'loop-post-test-writer-missing-spawn-audit',
|
|
249
|
+
'phase=post-test-writer refused: no `loop-audit-tool-use` record for round=' + g.round +
|
|
250
|
+
', agent=np-test-writer on ' + taskId + '. ' +
|
|
251
|
+
'Spawn np-test-writer and call `loop-audit-tool-use ' + taskId +
|
|
252
|
+
' --agent np-test-writer --tool-use-log <json>` after the spawn, ' +
|
|
253
|
+
'or pass --force-post-test-writer for an explicit override.',
|
|
254
|
+
{ taskId, round: g.round, missing: ['np-test-writer'], required: ['np-test-writer'] },
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const testPaths = _parseTestPaths(args.getFlag(list, '--tests'));
|
|
258
|
+
const merged = _stampPrepPhase(
|
|
259
|
+
taskId, cwd, 'post-test-writer', 'test-writer-spawned', 'forced_post_test_writer', g.force,
|
|
260
|
+
(prev) => {
|
|
261
|
+
const prior = Array.isArray(prev.tdd_tests) ? prev.tdd_tests : [];
|
|
262
|
+
const seen = new Set(prior);
|
|
263
|
+
const tdd_tests = prior.slice();
|
|
264
|
+
for (const p of testPaths) { if (!seen.has(p)) { seen.add(p); tdd_tests.push(p); } }
|
|
265
|
+
return { tdd_tests };
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
return {
|
|
269
|
+
phase: 'post-test-writer',
|
|
270
|
+
next_action: 'spawn-executor',
|
|
271
|
+
forced: g.force,
|
|
272
|
+
tdd_tests: merged.nubosloop && Array.isArray(merged.nubosloop.tdd_tests) ? merged.nubosloop.tdd_tests : [],
|
|
273
|
+
round: merged.nubosloop ? merged.nubosloop.round : g.round,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
163
277
|
function _runPostExecutor(taskId, list, cwd) {
|
|
164
278
|
const verifyExitCode = args.getFlag(list, '--verify-exit-code');
|
|
165
279
|
if (verifyExitCode === undefined) {
|
|
@@ -557,7 +671,7 @@ async function run(argv, ctx) {
|
|
|
557
671
|
if (!phase) {
|
|
558
672
|
throw new NubosPilotError(
|
|
559
673
|
'loop-run-round-missing-phase',
|
|
560
|
-
'loop-run-round requires --phase <preflight|post-researcher|post-executor|post-critics|commit|stuck>',
|
|
674
|
+
'loop-run-round requires --phase <preflight|post-researcher|post-architect|post-test-writer|post-executor|post-critics|commit|stuck>',
|
|
561
675
|
{ hint: 'each phase corresponds to a non-LLM transition between LLM spawns' },
|
|
562
676
|
);
|
|
563
677
|
}
|
|
@@ -572,9 +686,11 @@ async function run(argv, ctx) {
|
|
|
572
686
|
const tail = list.slice(1);
|
|
573
687
|
let result;
|
|
574
688
|
switch (phase) {
|
|
575
|
-
case 'preflight':
|
|
576
|
-
case 'post-researcher':
|
|
577
|
-
case 'post-
|
|
689
|
+
case 'preflight': result = await _runPreflight(taskId, tail, cwd); break;
|
|
690
|
+
case 'post-researcher': result = _runPostResearcher(taskId, tail, cwd); break;
|
|
691
|
+
case 'post-architect': result = _runPostArchitect(taskId, tail, cwd); break;
|
|
692
|
+
case 'post-test-writer': result = _runPostTestWriter(taskId, tail, cwd); break;
|
|
693
|
+
case 'post-executor': result = _runPostExecutor(taskId, tail, cwd); break;
|
|
578
694
|
case 'post-critics': result = _runPostCritics(taskId, tail, cwd); break;
|
|
579
695
|
case 'commit': result = _runCommit(taskId, tail, cwd); break;
|
|
580
696
|
case 'stuck': result = _runStuck(taskId, tail, cwd); break;
|
package/lib/agents-registry.cjs
CHANGED
|
@@ -15,6 +15,14 @@ const BUILD_FIXER_AGENT = 'np-build-fixer';
|
|
|
15
15
|
|
|
16
16
|
const RESEARCHER_AGENT = 'np-researcher';
|
|
17
17
|
|
|
18
|
+
// Round-1 preparation agents that run between the researcher swarm and the
|
|
19
|
+
// executor. Config-gated (agents.architect / agents.test_writer). They get
|
|
20
|
+
// Layer-C spawn-evidence stamps but are NOT Rule-9 audited (not in
|
|
21
|
+
// AUDITED_AGENTS) — the architect is advisory/read-only and the test-writer's
|
|
22
|
+
// quality is enforced downstream by the np-critic-tests axis.
|
|
23
|
+
const TASK_ARCHITECT_AGENT = 'np-task-architect';
|
|
24
|
+
const TEST_WRITER_AGENT = 'np-test-writer';
|
|
25
|
+
|
|
18
26
|
const AUDITED_AGENTS = Object.freeze([
|
|
19
27
|
EXECUTOR_AGENT,
|
|
20
28
|
BUILD_FIXER_AGENT,
|
|
@@ -29,5 +37,7 @@ module.exports = {
|
|
|
29
37
|
EXECUTOR_AGENT,
|
|
30
38
|
BUILD_FIXER_AGENT,
|
|
31
39
|
RESEARCHER_AGENT,
|
|
40
|
+
TASK_ARCHITECT_AGENT,
|
|
41
|
+
TEST_WRITER_AGENT,
|
|
32
42
|
AUDITED_AGENTS,
|
|
33
43
|
};
|
package/lib/agents.test.cjs
CHANGED
|
@@ -242,6 +242,8 @@ const NP_AGENTS = [
|
|
|
242
242
|
{ file: 'np-researcher-reconciler', expected_tier: 'sonnet' },
|
|
243
243
|
{ file: 'np-codebase-documenter', expected_tier: 'sonnet' },
|
|
244
244
|
{ file: 'np-architect', expected_tier: 'sonnet' },
|
|
245
|
+
{ file: 'np-task-architect', expected_tier: 'sonnet' },
|
|
246
|
+
{ file: 'np-test-writer', expected_tier: 'sonnet' },
|
|
245
247
|
{ file: 'np-build-fixer', expected_tier: 'sonnet' },
|
|
246
248
|
{ file: 'np-security-reviewer', expected_tier: 'sonnet' },
|
|
247
249
|
{ file: 'np-nyquist-auditor', expected_tier: 'haiku' },
|
package/lib/config-defaults.cjs
CHANGED
|
@@ -18,6 +18,14 @@ const DEFAULT_AGENTS = Object.freeze({
|
|
|
18
18
|
research: true,
|
|
19
19
|
plan_checker: true,
|
|
20
20
|
verifier: true,
|
|
21
|
+
// Per-task architecture step in the Nubosloop (np-task-architect). Runs in
|
|
22
|
+
// round 1 after the researcher swarm, before the test-writer and executor.
|
|
23
|
+
// Backfilled to true on install/update when absent (see bin/install.js).
|
|
24
|
+
architect: true,
|
|
25
|
+
// Per-task TDD step (np-test-writer). Runs in round 1 after the architect,
|
|
26
|
+
// before the executor — writes the tests the executor must make green.
|
|
27
|
+
// Backfilled to true on install/update when absent (see bin/install.js).
|
|
28
|
+
test_writer: true,
|
|
21
29
|
// Economy axis level (off|lite|full|ultra). Default `lite` = prevention-first:
|
|
22
30
|
// the climb-the-ladder discipline is on, the in-loop critic is opt-in (full/ultra).
|
|
23
31
|
// Resolved via lib/economy-mode.cjs; legacy `economy_critic` bool still honoured.
|
package/lib/config-schema.cjs
CHANGED
|
@@ -33,6 +33,8 @@ const SCHEMA = Object.freeze({
|
|
|
33
33
|
research: { type: 'boolean', optional: true },
|
|
34
34
|
plan_checker: { type: 'boolean', optional: true },
|
|
35
35
|
verifier: { type: 'boolean', optional: true },
|
|
36
|
+
architect: { type: 'boolean', optional: true },
|
|
37
|
+
test_writer: { type: 'boolean', optional: true },
|
|
36
38
|
economy: { type: 'enum', values: VALID_ECONOMY_MODES, optional: true },
|
|
37
39
|
economy_critic: { type: 'boolean', optional: true },
|
|
38
40
|
},
|
package/lib/git.cjs
CHANGED
|
@@ -58,7 +58,7 @@ function assertCommittablePaths(paths, opts) {
|
|
|
58
58
|
return committable;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function commitTask(taskId, files, message) {
|
|
61
|
+
function commitTask(taskId, files, message, body) {
|
|
62
62
|
const { committable, ignored } = classifyCommittablePaths(files);
|
|
63
63
|
if (committable.length === 0) {
|
|
64
64
|
if (ignored.length > 0) {
|
|
@@ -84,7 +84,11 @@ function commitTask(taskId, files, message) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
execFileSync('git', ['add', '--', ...committable], { stdio: 'pipe', timeout: GIT_TIMEOUT_MS });
|
|
87
|
-
|
|
87
|
+
const commitArgs = ['commit', '-m', message];
|
|
88
|
+
if (typeof body === 'string' && body.trim().length > 0) {
|
|
89
|
+
commitArgs.push('-m', body);
|
|
90
|
+
}
|
|
91
|
+
execFileSync('git', [...commitArgs, '--', ...committable], { stdio: 'pipe', timeout: GIT_TIMEOUT_MS });
|
|
88
92
|
return {
|
|
89
93
|
committed: true,
|
|
90
94
|
files_committed: committable.slice(),
|
package/lib/git.test.cjs
CHANGED
|
@@ -199,6 +199,34 @@ test('GIT-5: commitTask creates a single commit containing exactly the supplied
|
|
|
199
199
|
});
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
test('GIT-5b: commitTask attaches a multi-line body via a second -m when body is supplied', () => {
|
|
203
|
+
const root = makeRepo();
|
|
204
|
+
inRepo(root, () => {
|
|
205
|
+
writeFile(root, 'lib/git.cjs', '// stub');
|
|
206
|
+
git.commitTask(
|
|
207
|
+
'M006-S001-T0001',
|
|
208
|
+
['lib/git.cjs'],
|
|
209
|
+
'task(M006-S001-T0001): add git helper',
|
|
210
|
+
'Implements the git helper.\n\nTask: M006-S001-T0001',
|
|
211
|
+
);
|
|
212
|
+
const subject = execFileSync('git', ['log', '-n', '1', '--format=%s'], { encoding: 'utf-8' }).trim();
|
|
213
|
+
const fullBody = execFileSync('git', ['log', '-n', '1', '--format=%b'], { encoding: 'utf-8' });
|
|
214
|
+
assert.equal(subject, 'task(M006-S001-T0001): add git helper');
|
|
215
|
+
assert.match(fullBody, /Implements the git helper\./);
|
|
216
|
+
assert.match(fullBody, /Task: M006-S001-T0001/);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('GIT-5c: commitTask omits the body -m when body is empty/whitespace (backward-compatible)', () => {
|
|
221
|
+
const root = makeRepo();
|
|
222
|
+
inRepo(root, () => {
|
|
223
|
+
writeFile(root, 'lib/git.cjs', '// stub');
|
|
224
|
+
git.commitTask('M006-S001-T0001', ['lib/git.cjs'], 'task(M006-S001-T0001): add git helper', ' ');
|
|
225
|
+
const fullBody = execFileSync('git', ['log', '-n', '1', '--format=%b'], { encoding: 'utf-8' }).trim();
|
|
226
|
+
assert.equal(fullBody, '');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
202
230
|
test('GIT-6: findCommitByTaskId returns 40-char SHA for known task commit', () => {
|
|
203
231
|
const root = makeRepo();
|
|
204
232
|
inRepo(root, () => {
|
package/lib/template.cjs
CHANGED
|
@@ -3,15 +3,14 @@ const path = require('node:path');
|
|
|
3
3
|
const { projectStateDir, NubosPilotError } = require('./core.cjs');
|
|
4
4
|
|
|
5
5
|
const PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
|
6
|
+
const PACKAGE_TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates');
|
|
6
7
|
|
|
7
8
|
function templatesDir(cwd) {
|
|
8
9
|
return path.join(projectStateDir(cwd), 'templates');
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
function
|
|
12
|
-
const dir = templatesDir(cwd);
|
|
12
|
+
function resolveInDir(dir, name) {
|
|
13
13
|
const filePath = path.resolve(dir, name + '.md');
|
|
14
|
-
|
|
15
14
|
const dirWithSep = dir.endsWith(path.sep) ? dir : dir + path.sep;
|
|
16
15
|
if (!filePath.startsWith(dirWithSep)) {
|
|
17
16
|
throw new NubosPilotError(
|
|
@@ -20,21 +19,27 @@ function loadTemplate(name, vars, cwd = process.cwd()) {
|
|
|
20
19
|
{ template: name, path: filePath },
|
|
21
20
|
);
|
|
22
21
|
}
|
|
22
|
+
return filePath;
|
|
23
|
+
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
function loadTemplate(name, vars, cwd = process.cwd()) {
|
|
26
|
+
const localPath = resolveInDir(templatesDir(cwd), name);
|
|
27
|
+
const packagePath = resolveInDir(PACKAGE_TEMPLATES_DIR, name);
|
|
28
|
+
|
|
29
|
+
let filePath = null;
|
|
30
|
+
if (fs.existsSync(localPath)) filePath = localPath;
|
|
31
|
+
else if (fs.existsSync(packagePath)) filePath = packagePath;
|
|
32
|
+
|
|
33
|
+
if (filePath === null) {
|
|
34
|
+
throw new NubosPilotError(
|
|
35
|
+
'template-not-found',
|
|
36
|
+
`Template "${name}" not found in project overlay (${localPath}) or package templates (${packagePath})`,
|
|
37
|
+
{ template: name, path: localPath, packagePath },
|
|
38
|
+
);
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
42
|
+
|
|
38
43
|
return raw.replace(PLACEHOLDER_RE, (_match, key) => {
|
|
39
44
|
if (!(key in vars)) {
|
|
40
45
|
throw new NubosPilotError(
|
package/lib/template.test.cjs
CHANGED
|
@@ -144,6 +144,41 @@ test('TPL-12: listTemplates on sandbox without templates dir returns []', () =>
|
|
|
144
144
|
assert.deepEqual(list, []);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
test('TPL-14: name absent in project overlay falls back to package template', () => {
|
|
148
|
+
const cwd = makeSandbox({});
|
|
149
|
+
const anyVars = new Proxy({}, { has: () => true, get: () => 'X' });
|
|
150
|
+
let out = null;
|
|
151
|
+
let thrown = null;
|
|
152
|
+
try {
|
|
153
|
+
out = tpl.loadTemplate('milestone/CONTEXT', anyVars, cwd);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
thrown = err;
|
|
156
|
+
}
|
|
157
|
+
assert.equal(thrown, null, 'package fallback should resolve, not throw');
|
|
158
|
+
assert.equal(typeof out, 'string');
|
|
159
|
+
assert.ok(out.length > 0, 'package template rendered to non-empty output');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('TPL-15: project overlay wins over package template of the same name', () => {
|
|
163
|
+
const cwd = makeSandbox({ RULES: 'LOCAL-OVERLAY {{x}}' });
|
|
164
|
+
const out = tpl.loadTemplate('RULES', { x: '1' }, cwd);
|
|
165
|
+
assert.equal(out, 'LOCAL-OVERLAY 1');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('TPL-16: name absent in BOTH overlay and package → template-not-found with both paths', () => {
|
|
169
|
+
const cwd = makeSandbox({});
|
|
170
|
+
let thrown = null;
|
|
171
|
+
try {
|
|
172
|
+
tpl.loadTemplate('definitely-missing-xyz', {}, cwd);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
thrown = err;
|
|
175
|
+
}
|
|
176
|
+
assert.ok(thrown, 'expected throw');
|
|
177
|
+
assert.equal(thrown.code, 'template-not-found');
|
|
178
|
+
assert.ok(thrown.details.path.endsWith(path.join('templates', 'definitely-missing-xyz.md')));
|
|
179
|
+
assert.ok(thrown.details.packagePath.endsWith(path.join('templates', 'definitely-missing-xyz.md')));
|
|
180
|
+
});
|
|
181
|
+
|
|
147
182
|
test('TPL-13: cwd with no .nubos-pilot ancestor → projectStateDir throws not-in-project', () => {
|
|
148
183
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'nubos-pilot-tpl-noroot-'));
|
|
149
184
|
_sandboxes.push(root);
|
package/package.json
CHANGED
package/templates/RULES.md
CHANGED
|
@@ -79,7 +79,33 @@ Examples:
|
|
|
79
79
|
-->
|
|
80
80
|
- _TBD — fill with logging policy._
|
|
81
81
|
|
|
82
|
-
##
|
|
82
|
+
## Conventions
|
|
83
|
+
|
|
84
|
+
> **How your code must be built.** These rules bind the architect (`np-task-architect`),
|
|
85
|
+
> the test-writer (`np-test-writer`), the executor, and the style critic
|
|
86
|
+
> (`np-critic-style`). They are read on every task. Each subsection is **MUST FILL** —
|
|
87
|
+
> use `- _none — <reason>_` only when a subsection genuinely does not apply.
|
|
88
|
+
|
|
89
|
+
### Class / Module Structure
|
|
90
|
+
|
|
91
|
+
<!-- How classes, modules, and units are shaped. Examples:
|
|
92
|
+
- One public class per file; file name matches the class name.
|
|
93
|
+
- Constructor injection only — no service-locator / static singletons.
|
|
94
|
+
- Business logic lives in Services/Actions; controllers stay thin (no DB access).
|
|
95
|
+
- Public surface ≤ 5 methods; split when it grows past that.
|
|
96
|
+
-->
|
|
97
|
+
- _TBD — fill with class/module construction rules._
|
|
98
|
+
|
|
99
|
+
### Naming
|
|
100
|
+
|
|
101
|
+
<!-- Identifier conventions. Examples:
|
|
102
|
+
- Classes PascalCase, methods camelCase, constants UPPER_SNAKE.
|
|
103
|
+
- Booleans read as predicates (`isActive`, `hasAccess`), never `flag`/`status`.
|
|
104
|
+
- Table names follow the framework's pluralization — never override.
|
|
105
|
+
-->
|
|
106
|
+
- _TBD — fill with naming conventions._
|
|
107
|
+
|
|
108
|
+
### Code Style
|
|
83
109
|
|
|
84
110
|
<!-- Format/lint/comment policy. Examples:
|
|
85
111
|
- No comments inside source — names + tests carry intent.
|
|
@@ -88,6 +114,15 @@ Examples:
|
|
|
88
114
|
-->
|
|
89
115
|
- _TBD — fill with style policy._
|
|
90
116
|
|
|
117
|
+
### Patterns & Paradigms
|
|
118
|
+
|
|
119
|
+
<!-- Architectural patterns that are required or banned. Examples:
|
|
120
|
+
- Required: Repository pattern for all persistence; Result objects over exceptions for control flow.
|
|
121
|
+
- Banned: anemic domain models; inheritance for code reuse (prefer composition).
|
|
122
|
+
- Errors are typed and surfaced — never swallowed or stringly-typed.
|
|
123
|
+
-->
|
|
124
|
+
- _TBD — fill with required/banned patterns._
|
|
125
|
+
|
|
91
126
|
## Out-of-Scope (Forever)
|
|
92
127
|
|
|
93
128
|
<!-- Things this project explicitly will never do. Distinct from deferred ideas.
|