nubos-pilot 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +30 -1
  4. package/SECURITY.md +60 -0
  5. package/bin/install.js +104 -39
  6. package/bin/np-tools/_args.cjs +8 -2
  7. package/bin/np-tools/_memory-resolve.cjs +4 -4
  8. package/bin/np-tools/checkpoint.cjs +1 -1
  9. package/bin/np-tools/close-project.cjs +3 -29
  10. package/bin/np-tools/commit-task.cjs +31 -35
  11. package/bin/np-tools/commit.cjs +0 -3
  12. package/bin/np-tools/config.cjs +4 -13
  13. package/bin/np-tools/discuss-phase.cjs +4 -27
  14. package/bin/np-tools/doctor.cjs +76 -16
  15. package/bin/np-tools/doctor.test.cjs +14 -0
  16. package/bin/np-tools/execute-milestone.cjs +6 -27
  17. package/bin/np-tools/handoff-write.cjs +16 -2
  18. package/bin/np-tools/init-dispatch.test.cjs +21 -0
  19. package/bin/np-tools/knowledge-search.cjs +0 -3
  20. package/bin/np-tools/learning-list.cjs +0 -2
  21. package/bin/np-tools/learning-log.cjs +1 -7
  22. package/bin/np-tools/loop-audit-tool-use.cjs +1 -11
  23. package/bin/np-tools/loop-run-round.cjs +51 -148
  24. package/bin/np-tools/loop-state-read.cjs +1 -5
  25. package/bin/np-tools/loop-state-record.cjs +1 -27
  26. package/bin/np-tools/loop-stuck.cjs +1 -8
  27. package/bin/np-tools/messages-send.cjs +16 -2
  28. package/bin/np-tools/metrics.test.cjs +4 -4
  29. package/bin/np-tools/new-milestone.cjs +14 -3
  30. package/bin/np-tools/new-project.cjs +4 -2
  31. package/bin/np-tools/new-project.test.cjs +12 -0
  32. package/bin/np-tools/park.cjs +2 -1
  33. package/bin/np-tools/plan-lint.cjs +0 -19
  34. package/bin/np-tools/plan-milestone.cjs +8 -29
  35. package/bin/np-tools/propose-milestones.cjs +14 -3
  36. package/bin/np-tools/propose-milestones.test.cjs +27 -0
  37. package/bin/np-tools/research-phase.cjs +7 -37
  38. package/bin/np-tools/researcher-reconcile.cjs +3 -21
  39. package/bin/np-tools/reset-slice.cjs +10 -16
  40. package/bin/np-tools/resolve-model.cjs +21 -26
  41. package/bin/np-tools/resolve-model.test.cjs +15 -5
  42. package/bin/np-tools/resume-work.cjs +1 -5
  43. package/bin/np-tools/skip.cjs +2 -1
  44. package/bin/np-tools/spawn-headless.cjs +138 -19
  45. package/bin/np-tools/spawn-headless.test.cjs +310 -0
  46. package/bin/np-tools/state.cjs +0 -1
  47. package/bin/np-tools/undo-task.cjs +2 -1
  48. package/bin/np-tools/undo.cjs +5 -3
  49. package/bin/np-tools/unpark.cjs +2 -1
  50. package/bin/np-tools/verify-work.cjs +82 -25
  51. package/bin/np-tools/verify-work.test.cjs +211 -1
  52. package/bin/researcher-merge.cjs +2 -1
  53. package/bin/researcher-merge.test.cjs +14 -0
  54. package/lib/agents-registry.cjs +32 -0
  55. package/lib/agents.cjs +14 -6
  56. package/lib/agents.test.cjs +44 -0
  57. package/lib/archive.cjs +102 -36
  58. package/lib/archive.test.cjs +115 -5
  59. package/lib/checkpoint.cjs +43 -23
  60. package/lib/checkpoint.test.cjs +67 -6
  61. package/lib/commit-policy.cjs +3 -1
  62. package/lib/commit-policy.test.cjs +6 -0
  63. package/lib/config-defaults.cjs +5 -1
  64. package/lib/config-defaults.test.cjs +71 -0
  65. package/lib/config-schema.cjs +204 -0
  66. package/lib/config-schema.test.cjs +148 -0
  67. package/lib/config.cjs +168 -14
  68. package/lib/config.test.cjs +234 -0
  69. package/lib/core.cjs +226 -52
  70. package/lib/core.test.cjs +193 -10
  71. package/lib/dashboard.cjs +0 -12
  72. package/lib/frontmatter.cjs +5 -0
  73. package/lib/git.cjs +34 -27
  74. package/lib/git.test.cjs +11 -3
  75. package/lib/handoff.cjs +16 -14
  76. package/lib/handoff.test.cjs +24 -0
  77. package/lib/ids.cjs +6 -0
  78. package/lib/init-emit.cjs +33 -0
  79. package/lib/install/claude-hooks.cjs +46 -25
  80. package/lib/install/claude-hooks.test.cjs +64 -0
  81. package/lib/install/manifest.cjs +19 -0
  82. package/lib/install/manifest.test.cjs +107 -0
  83. package/lib/knowledge-adapter.cjs +3 -49
  84. package/lib/learnings.cjs +3 -108
  85. package/lib/logger.cjs +157 -0
  86. package/lib/logger.test.cjs +159 -0
  87. package/lib/memory-index-usearch.cjs +9 -12
  88. package/lib/memory-provider-local.cjs +8 -0
  89. package/lib/memory.cjs +86 -27
  90. package/lib/memory.test.cjs +135 -0
  91. package/lib/messaging.cjs +155 -83
  92. package/lib/metrics-aggregate.cjs +26 -27
  93. package/lib/metrics.cjs +7 -3
  94. package/lib/metrics.test.cjs +6 -5
  95. package/lib/migrations.cjs +89 -0
  96. package/lib/migrations.test.cjs +82 -0
  97. package/lib/milestone-meta.cjs +70 -0
  98. package/lib/nubosloop-audit.cjs +41 -141
  99. package/lib/nubosloop.cjs +45 -149
  100. package/lib/plan-lint.cjs +0 -67
  101. package/lib/researcher-swarm.cjs +1 -62
  102. package/lib/roadmap-render.cjs +107 -33
  103. package/lib/roadmap-schema.cjs +42 -0
  104. package/lib/roadmap.cjs +93 -20
  105. package/lib/roadmap.test.cjs +215 -0
  106. package/lib/run-context.cjs +54 -0
  107. package/lib/run-context.test.cjs +53 -0
  108. package/lib/runtime/index.cjs +5 -10
  109. package/lib/runtime/index.test.cjs +8 -1
  110. package/lib/safe-path.cjs +156 -0
  111. package/lib/safe-path.test.cjs +164 -0
  112. package/lib/state.cjs +28 -10
  113. package/lib/state.test.cjs +72 -22
  114. package/lib/tasks.cjs +92 -14
  115. package/lib/tasks.test.cjs +65 -0
  116. package/lib/todo.cjs +7 -5
  117. package/lib/verify.cjs +44 -3
  118. package/lib/worktree.cjs +2 -2
  119. package/lib/yaml.cjs +44 -0
  120. package/lib/yaml.test.cjs +65 -0
  121. package/np-tools.cjs +25 -23
  122. package/package.json +5 -2
  123. package/workflows/research-phase.md +1 -1
@@ -1,17 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('node:fs');
4
- const path = require('node:path');
5
- const os = require('node:os');
6
- const crypto = require('node:crypto');
7
4
 
8
- const { NubosPilotError, projectStateDir } = require('../../lib/core.cjs');
5
+ const { NubosPilotError } = require('../../lib/core.cjs');
6
+ const { emitInitPayload } = require('../../lib/init-emit.cjs');
9
7
  const { getPhase } = require('../../lib/roadmap.cjs');
10
8
  const layout = require('../../lib/layout.cjs');
11
9
  const textMode = require('../../lib/text-mode.cjs');
12
10
 
13
- const INLINE_THRESHOLD_BYTES = 16 * 1024;
14
-
15
11
  function _parseArgs(args) {
16
12
  const rest = [];
17
13
  const flags = { assumptions: false };
@@ -51,25 +47,6 @@ function _agentSkills() {
51
47
  return { planner: null };
52
48
  }
53
49
 
54
- function _emit(payload, stdout, cwd) {
55
- const json = JSON.stringify(payload, null, 2);
56
- if (Buffer.byteLength(json, 'utf-8') <= INLINE_THRESHOLD_BYTES) {
57
- stdout.write(json);
58
- return;
59
- }
60
- let tmpDir;
61
- try {
62
- tmpDir = path.join(projectStateDir(cwd), '.tmp');
63
- fs.mkdirSync(tmpDir, { recursive: true });
64
- } catch (_err) {
65
- tmpDir = os.tmpdir();
66
- }
67
- const suffix = process.pid + '-' + crypto.randomBytes(4).toString('hex');
68
- const tmpPath = path.join(tmpDir, 'init-discuss-phase-' + suffix + '.json');
69
- fs.writeFileSync(tmpPath, json, 'utf-8');
70
- stdout.write('@file:' + tmpPath);
71
- }
72
-
73
50
  function run(args, ctx) {
74
51
  const context = ctx || {};
75
52
  const cwd = context.cwd || process.cwd();
@@ -117,8 +94,8 @@ function run(args, ctx) {
117
94
  agent_skills: _agentSkills(),
118
95
  };
119
96
 
120
- _emit(payload, stdout, cwd);
97
+ emitInitPayload(payload, stdout, cwd, 'discuss-phase');
121
98
  return payload;
122
99
  }
123
100
 
124
- module.exports = { run, _parseArgs, _validateMilestoneArg, INLINE_THRESHOLD_BYTES };
101
+ module.exports = { run, _parseArgs, _validateMilestoneArg };
@@ -5,6 +5,7 @@ const os = require('node:os');
5
5
  const path = require('node:path');
6
6
 
7
7
  const { NubosPilotError, atomicWriteFileSync } = require('../../lib/core.cjs');
8
+ const { safeYamlParse } = require('../../lib/yaml.cjs');
8
9
  const manifestMod = require('../../lib/install/manifest.cjs');
9
10
  const codexTomlMod = require('../../lib/install/codex-toml.cjs');
10
11
  const runtimeAssetsMod = require('../../lib/install/runtime-assets.cjs');
@@ -20,14 +21,16 @@ const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
20
21
  const OPENCODE_LOCAL_PREFIX = '.opencode/nubos-pilot/';
21
22
 
22
23
  function _readScope(projectRoot) {
23
- const cfgPath = path.join(projectRoot, STATE_SUBPATH, 'config.json');
24
- if (!fs.existsSync(cfgPath)) return 'local';
24
+ const { readConfig, _CONFIG_PARSE_CODES } = require('../../lib/config.cjs');
25
+ let cfg;
25
26
  try {
26
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
27
- return cfg && cfg.scope === 'global' ? 'global' : 'local';
28
- } catch {
29
- return 'local';
27
+ cfg = readConfig(projectRoot);
28
+ } catch (err) {
29
+ if (err && err.code === 'not-in-project') return 'local';
30
+ if (err && _CONFIG_PARSE_CODES.has(err.code)) return 'local';
31
+ throw err;
30
32
  }
33
+ return cfg && cfg.scope === 'global' ? 'global' : 'local';
31
34
  }
32
35
 
33
36
  function _payloadBaseFor(projectRoot, scope) {
@@ -281,8 +284,7 @@ function _checkMilestoneLayout(projectRoot) {
281
284
  if (!fs.existsSync(roadmapPath)) return [];
282
285
  let doc;
283
286
  try {
284
- const YAML = require('yaml');
285
- doc = YAML.parse(fs.readFileSync(roadmapPath, 'utf-8'));
287
+ doc = safeYamlParse(fs.readFileSync(roadmapPath, 'utf-8'), { kind: 'doctor-roadmap' });
286
288
  } catch {
287
289
  return [{
288
290
  id: 'roadmap-unreadable',
@@ -321,7 +323,6 @@ function _checkMilestoneLayout(projectRoot) {
321
323
  }
322
324
  }
323
325
 
324
- // Flag old .nubos-pilot/phases/ dir as stale if still present
325
326
  const phasesDir = path.join(stateDir, 'phases');
326
327
  if (fs.existsSync(phasesDir)) {
327
328
  issues.push({
@@ -337,10 +338,6 @@ function _checkMilestoneLayout(projectRoot) {
337
338
  return issues;
338
339
  }
339
340
 
340
- // Single-critic revision (ADR-0010 §Single-Critic Revision 2026-05-05): one
341
- // np-critic spawned per round, with three audit-surface modules loaded as
342
- // <files_to_read>. The doctor checks that all four files are present —
343
- // missing the spawnable critic OR any of the three modules breaks the loop.
344
341
  const NUBOSLOOP_CRITICS = [
345
342
  'np-critic', // spawnable (sonnet)
346
343
  'np-critic-style', // axis module (Style)
@@ -426,10 +423,28 @@ function _checkNubosloopKnowledgeStore(projectRoot) {
426
423
 
427
424
  function _checkNubosloopConfig(projectRoot) {
428
425
  const issues = [];
429
- const cfgPath = path.join(projectRoot, '.nubos-pilot', 'config.json');
430
- if (!fs.existsSync(cfgPath)) return issues;
426
+ const { readConfig, _CONFIG_PARSE_CODES } = require('../../lib/config.cjs');
431
427
  let cfg;
432
- try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')); } catch { return issues; }
428
+ try {
429
+ cfg = readConfig(projectRoot);
430
+ } catch (err) {
431
+ if (err && err.code === 'not-in-project') return issues;
432
+ if (err && _CONFIG_PARSE_CODES.has(err.code)) {
433
+ issues.push({
434
+ id: 'config-json-corrupt',
435
+ severity: 'error',
436
+ fixable: 'manual',
437
+ details: {
438
+ code: err.code,
439
+ message: err.message,
440
+ file: err.details && err.details.file,
441
+ hint: 'Repair or delete .nubos-pilot/config.json — the file is unparseable or has the wrong shape.',
442
+ },
443
+ });
444
+ return issues;
445
+ }
446
+ throw err;
447
+ }
433
448
  const swarm = cfg && cfg.swarm;
434
449
  const adapter = swarm && swarm.knowledge_adapter;
435
450
  if (adapter && adapter !== 'local') {
@@ -488,6 +503,50 @@ function _checkOrphanTmpFiles(projectRoot) {
488
503
  return issues;
489
504
  }
490
505
 
506
+ function _checkOrphanCheckpoints(projectRoot) {
507
+ const issues = [];
508
+ const stateDir = path.join(projectRoot, STATE_SUBPATH);
509
+ const cpDir = path.join(stateDir, 'checkpoints');
510
+ if (!fs.existsSync(cpDir)) return issues;
511
+ let entries;
512
+ try { entries = fs.readdirSync(cpDir); } catch { return issues; }
513
+
514
+ let currentTask = null;
515
+ try {
516
+ const statePath = path.join(stateDir, 'STATE.md');
517
+ if (fs.existsSync(statePath)) {
518
+ const { readState } = require('../../lib/state.cjs');
519
+ const s = readState(projectRoot);
520
+ currentTask = (s && s.frontmatter && s.frontmatter.current_task) || null;
521
+ }
522
+ } catch { /* state unreadable — treat as null current_task */ }
523
+
524
+ for (const name of entries) {
525
+ if (!name.endsWith('.json')) continue;
526
+ const taskId = name.slice(0, -5);
527
+ const cpPath = path.join(cpDir, name);
528
+ let cp;
529
+ try { cp = JSON.parse(fs.readFileSync(cpPath, 'utf-8')); }
530
+ catch { continue; }
531
+ if (!cp || cp.status !== 'in-progress') continue;
532
+ if (currentTask === taskId) continue;
533
+ issues.push({
534
+ id: 'orphan-checkpoint',
535
+ severity: 'warn',
536
+ fixable: 'manual',
537
+ details: {
538
+ task_id: taskId,
539
+ checkpoint: path.relative(projectRoot, cpPath),
540
+ current_task: currentTask,
541
+ hint: 'Checkpoint marks task as in-progress but STATE.md.current_task does not match. '
542
+ + 'Likely a crash during finishTask between STATE-clear and checkpoint-unlink. '
543
+ + 'Run `np-tools undo-task ' + taskId + '` to clean up, or delete manually after verifying the task is genuinely done.',
544
+ },
545
+ });
546
+ }
547
+ return issues;
548
+ }
549
+
491
550
  function _checkOutputSchemas(projectRoot) {
492
551
  const issues = [];
493
552
  const milestonesRoot = path.join(projectRoot, STATE_SUBPATH, 'milestones');
@@ -551,6 +610,7 @@ function _audit(projectRoot) {
551
610
  issues.push(..._checkNubosloopKnowledgeStore(projectRoot));
552
611
  issues.push(..._checkNubosloopConfig(projectRoot));
553
612
  issues.push(..._checkOrphanTmpFiles(projectRoot));
613
+ issues.push(..._checkOrphanCheckpoints(projectRoot));
554
614
  issues.push(..._checkOutputSchemas(projectRoot));
555
615
  return { issues, _codexContent: codex.content };
556
616
  }
@@ -169,6 +169,20 @@ test('DOC-8: flags nubosloop-knowledge-adapter-invalid for unsupported adapter',
169
169
  assert.ok(ids.includes('nubosloop-knowledge-adapter-invalid'));
170
170
  });
171
171
 
172
+ test('DOC-9b: flags config-json-corrupt when config.json is unparseable', async () => {
173
+ const root = makeSandbox();
174
+ fs.writeFileSync(path.join(root, 'src.js'), 'export function a(){}');
175
+ scanCodebase.run([], { cwd: root, stdout: captureStdout().stub });
176
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'config.json'), '{ not json');
177
+ const cap = captureStdout();
178
+ await doctor.run([], { cwd: root, stdout: cap.stub, stderr: cap.stub, askUser: async () => ({ value: false }) });
179
+ const out = cap.json();
180
+ const issue = out.issues.find((i) => i.id === 'config-json-corrupt');
181
+ assert.ok(issue, 'expected config-json-corrupt issue');
182
+ assert.equal(issue.severity, 'error');
183
+ assert.match(issue.details.hint, /Repair or delete/);
184
+ });
185
+
172
186
  test('DOC-9: flags nubosloop-maxRounds-out-of-range when value > 10', async () => {
173
187
  const root = makeSandbox();
174
188
  fs.writeFileSync(path.join(root, 'src.js'), 'export function a(){}');
@@ -2,22 +2,18 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const os = require('node:os');
6
- const crypto = require('node:crypto');
7
5
 
8
6
  const {
9
7
  NubosPilotError,
10
- projectStateDir,
11
8
  atomicWriteFileSync,
12
9
  } = require('../../lib/core.cjs');
10
+ const { emitInitPayload } = require('../../lib/init-emit.cjs');
13
11
  const layout = require('../../lib/layout.cjs');
14
12
  const { getPhase } = require('../../lib/roadmap.cjs');
15
13
  const { extractFrontmatter } = require('../../lib/frontmatter.cjs');
16
14
  const { getAgentSkills } = require('../../lib/agents.cjs');
17
15
  const textMode = require('../../lib/text-mode.cjs');
18
16
 
19
- const INLINE_THRESHOLD_BYTES = 16 * 1024;
20
-
21
17
  function _hasVerifyWorkFlag(list) {
22
18
  return Array.isArray(list) && list.some((a) => a === '--verify-work');
23
19
  }
@@ -45,23 +41,6 @@ function _safeSkills(name, cwd) {
45
41
  try { return getAgentSkills(name, cwd); } catch { return []; }
46
42
  }
47
43
 
48
- function _emit(payload, stdout, cwd) {
49
- const json = JSON.stringify(payload, null, 2);
50
- if (Buffer.byteLength(json, 'utf-8') <= INLINE_THRESHOLD_BYTES) {
51
- stdout.write(json);
52
- return;
53
- }
54
- let tmpDir;
55
- try {
56
- tmpDir = path.join(projectStateDir(cwd), '.tmp');
57
- fs.mkdirSync(tmpDir, { recursive: true });
58
- } catch { tmpDir = os.tmpdir(); }
59
- const suffix = process.pid + '-' + crypto.randomBytes(4).toString('hex');
60
- const tmpPath = path.join(tmpDir, 'init-execute-milestone-' + suffix + '.json');
61
- fs.writeFileSync(tmpPath, json, 'utf-8');
62
- stdout.write('@file:' + tmpPath);
63
- }
64
-
65
44
  function _readTaskPayload(taskPlanPath) {
66
45
  const raw = fs.readFileSync(taskPlanPath, 'utf-8');
67
46
  const { frontmatter, body } = extractFrontmatter(raw);
@@ -288,7 +267,7 @@ function run(args, ctx) {
288
267
  const mNum = _validateMilestoneArg(list[1]);
289
268
  const autoVerify = _hasVerifyWorkFlag(list.slice(2));
290
269
  const payload = _initPayload(mNum, cwd, { auto_verify: autoVerify });
291
- _emit(payload, stdout, cwd);
270
+ emitInitPayload(payload, stdout, cwd, 'execute-milestone');
292
271
  return payload;
293
272
  }
294
273
  case 'execute-task': {
@@ -302,20 +281,20 @@ function run(args, ctx) {
302
281
  );
303
282
  }
304
283
  const payload = _findTaskByFullId(mNum, taskId, cwd);
305
- _emit(payload, stdout, cwd);
284
+ emitInitPayload(payload, stdout, cwd, 'execute-milestone');
306
285
  return payload;
307
286
  }
308
287
  case 'finalize-slice': {
309
288
  const mNum = _validateMilestoneArg(list[1]);
310
289
  const sNum = _validateMilestoneArg(list[2]);
311
290
  const payload = _finalizeSlice(mNum, sNum, cwd);
312
- _emit(payload, stdout, cwd);
291
+ emitInitPayload(payload, stdout, cwd, 'execute-milestone');
313
292
  return payload;
314
293
  }
315
294
  case 'finalize-milestone': {
316
295
  const mNum = _validateMilestoneArg(list[1]);
317
296
  const payload = _finalizeMilestone(mNum, cwd);
318
- _emit(payload, stdout, cwd);
297
+ emitInitPayload(payload, stdout, cwd, 'execute-milestone');
319
298
  return payload;
320
299
  }
321
300
  default:
@@ -327,4 +306,4 @@ function run(args, ctx) {
327
306
  }
328
307
  }
329
308
 
330
- module.exports = { run, INLINE_THRESHOLD_BYTES };
309
+ module.exports = { run };
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('node:fs');
4
- const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { NubosPilotError, projectStateDir } = require('../../lib/core.cjs');
5
5
  const { writeHandoff } = require('../../lib/handoff.cjs');
6
+ const safePath = require('../../lib/safe-path.cjs');
6
7
 
7
8
  function _parseArgs(args) {
8
9
  const out = {
@@ -32,7 +33,20 @@ function run(args, opts) {
32
33
 
33
34
  let body = parsed.body || '';
34
35
  if (parsed.bodyFile) {
35
- try { body = fs.readFileSync(parsed.bodyFile, 'utf-8'); }
36
+ let resolved;
37
+ try {
38
+ resolved = safePath.assertInsideAnyOf([cwd, projectStateDir(cwd)], parsed.bodyFile, 'body-file');
39
+ } catch (err) {
40
+ if (err && (err.code === 'safe-path-outside-base' || err.code === 'safe-path-invalid-input' || err.code === 'safe-path-base-missing')) {
41
+ throw new NubosPilotError(
42
+ 'handoff-body-file-not-allowed',
43
+ '--body-file must reside inside the project or .nubos-pilot/: ' + parsed.bodyFile,
44
+ { path: parsed.bodyFile, cause: err.code },
45
+ );
46
+ }
47
+ throw err;
48
+ }
49
+ try { body = fs.readFileSync(resolved, 'utf-8'); }
36
50
  catch (err) {
37
51
  throw new NubosPilotError(
38
52
  'handoff-body-file-read-failed',
@@ -33,6 +33,27 @@ test('TD-1: topLevelCommands routes metrics/resolve-model/plan-diff and siblings
33
33
  }
34
34
  });
35
35
 
36
+ test('TD-1b: verify-work is reachable as top-level command (not only via init)', () => {
37
+ const np = require('../../np-tools.cjs');
38
+ assert.ok(np.topLevelCommands['verify-work'], 'verify-work must be exposed at top level for agents/workflows');
39
+ assert.equal(typeof np.topLevelCommands['verify-work'].run, 'function');
40
+ });
41
+
42
+ test('TD-1c: every initWorkflow is also exposed top-level (registry-parity invariant)', () => {
43
+ const np = require('../../np-tools.cjs');
44
+ for (const key of Object.keys(np.initWorkflows)) {
45
+ assert.ok(
46
+ np.topLevelCommands[key],
47
+ 'initWorkflow "' + key + '" missing from topLevelCommands — both registries must list every agent-callable verb',
48
+ );
49
+ assert.strictEqual(
50
+ np.topLevelCommands[key],
51
+ np.initWorkflows[key],
52
+ 'topLevelCommands["' + key + '"] must be the SAME module as initWorkflows["' + key + '"]',
53
+ );
54
+ }
55
+ });
56
+
36
57
  test('TD-2: initWorkflows exposes plan-milestone + execute-milestone entries', () => {
37
58
  const np = require('../../np-tools.cjs');
38
59
  assert.ok(np.initWorkflows && typeof np.initWorkflows === 'object');
@@ -31,9 +31,6 @@ function run(args, ctx) {
31
31
  }
32
32
  const result = search(parsed.query, cwd, { limit: parsed.limit });
33
33
  stdout.write(JSON.stringify(result));
34
- // When run inside a Nubosloop task (--task), record Rule 9 evidence so the
35
- // tool-use audit can verify the search actually happened. Best-effort:
36
- // ledger failure must never fail the search itself.
37
34
  if (parsed.task) {
38
35
  try {
39
36
  require('../../lib/nubosloop.cjs').recordSearchEvidence(parsed.task, parsed.query, cwd);
@@ -13,14 +13,12 @@ function run(argv, ctx) {
13
13
 
14
14
  const adapter = knowledgeAdapter.getAdapter(cwd);
15
15
  const all = adapter.list();
16
- // Sort by occurrence desc, last_seen desc — most-used first.
17
16
  const sorted = all.slice().sort((a, b) => {
18
17
  const oc = (b.occurrence || 0) - (a.occurrence || 0);
19
18
  if (oc !== 0) return oc;
20
19
  return String(b.last_seen || '').localeCompare(String(a.last_seen || ''));
21
20
  });
22
21
  const truncated = sorted.slice(0, cap);
23
- // Strip the bulky tokens[] array from the listing — it's an internal cache.
24
22
  const projected = truncated.map((l) => {
25
23
  const { tokens, ...rest } = l;
26
24
  return rest;
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { NubosPilotError } = require('../../lib/core.cjs');
4
- const { TASK_ID_RE } = require('../../lib/checkpoint.cjs');
5
- const { MILESTONE_ID_RE } = require('../../lib/learnings.cjs');
4
+ const { TASK_ID_RE, MILESTONE_ID_RE } = require('../../lib/ids.cjs');
6
5
  const knowledgeAdapter = require('../../lib/knowledge-adapter.cjs');
7
6
  const { getFlag } = require('./_args.cjs');
8
7
 
@@ -52,11 +51,6 @@ function run(args, ctx) {
52
51
  const learnings = require('../../lib/learnings.cjs');
53
52
  const fingerprint = learnings._fingerprint(pattern);
54
53
  const adapter = knowledgeAdapter.getAdapter(cwd);
55
- // R5/risk from fifth review: the prior pre-scan-then-write approach raced
56
- // a concurrent writer that landed the same fingerprint between the scan
57
- // and the log() call. We now derive `was_new` from the post-write
58
- // occurrence: logLearning is locked, so occurrence === 1 is determinative
59
- // — exactly one log() call inside the lock window made the entry new.
60
54
  const result = adapter.log({ pattern, outcome, task_id: taskId, milestone_id: milestoneId });
61
55
  let occurrence = 1;
62
56
  if (Array.isArray(result && result.learnings)) {
@@ -4,8 +4,7 @@ const checkpoint = require('../../lib/checkpoint.cjs');
4
4
  const nubosloop = require('../../lib/nubosloop.cjs');
5
5
  const agentsLib = require('../../lib/agents.cjs');
6
6
  const args = require('./_args.cjs');
7
-
8
- const TASK_ID_RE = checkpoint.TASK_ID_RE;
7
+ const { TASK_ID_RE } = require('../../lib/ids.cjs');
9
8
 
10
9
  function run(argv, ctx) {
11
10
  const context = ctx || {};
@@ -16,14 +15,12 @@ function run(argv, ctx) {
16
15
  args.assertMatch(taskId, TASK_ID_RE, 'loop-audit-invalid-task-id', 'taskId');
17
16
  const tail = list.slice(1);
18
17
 
19
- // Read mode: just dump the audit log.
20
18
  if (tail.includes('--read')) {
21
19
  const log = nubosloop.readToolUseAudit(taskId, cwd) || [];
22
20
  stdout.write(JSON.stringify({ task_id: taskId, audit: log }) + '\n');
23
21
  return { task_id: taskId, audit: log };
24
22
  }
25
23
 
26
- // Append mode: agent + tool_use_log.
27
24
  const agent = args.getFlag(tail, '--agent');
28
25
  if (!agent) {
29
26
  throw new (require('../../lib/core.cjs').NubosPilotError)(
@@ -43,15 +40,8 @@ function run(argv, ctx) {
43
40
  } catch (err) {
44
41
  if (!err) throw err;
45
42
  if (err.code === 'loop-audit-agent-is-module') throw err;
46
- // Any other error (agent-not-found, agent-not-a-module) means the name
47
- // is not a known module — fall through and accept the audit.
48
43
  }
49
44
  }
50
- // --tool-use-log is required for AUDITED_AGENTS (Rule 9 enforcement reads
51
- // the tool list to verify search-knowledge / match-existing-learning calls).
52
- // For non-audited spawns (critics, plan-checker, etc.) the orchestrator may
53
- // omit it — we still record the spawn for Layer-C audit-trail evidence with
54
- // an empty log. Explicit empty-array is also accepted.
55
45
  const isAuditedAgent = nubosloop.AUDITED_AGENTS.includes(agent);
56
46
  let log;
57
47
  if (tail.includes('--tool-use-log')) {