nubos-pilot 0.1.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 (273) hide show
  1. package/agents/np-ai-researcher.md +140 -0
  2. package/agents/np-code-fixer.md +363 -0
  3. package/agents/np-code-reviewer.md +351 -0
  4. package/agents/np-domain-researcher.md +136 -0
  5. package/agents/np-eval-auditor.md +167 -0
  6. package/agents/np-eval-planner.md +153 -0
  7. package/agents/np-executor.md +72 -0
  8. package/agents/np-framework-selector.md +171 -0
  9. package/agents/np-nyquist-auditor.md +185 -0
  10. package/agents/np-plan-checker.md +165 -0
  11. package/agents/np-planner.md +199 -0
  12. package/agents/np-researcher.md +150 -0
  13. package/agents/np-security-auditor.md +206 -0
  14. package/agents/np-ui-auditor.md +369 -0
  15. package/agents/np-ui-checker.md +192 -0
  16. package/agents/np-ui-researcher.md +324 -0
  17. package/agents/np-verifier.md +79 -0
  18. package/bin/check-coverage.cjs +40 -0
  19. package/bin/check-workflows.cjs +171 -0
  20. package/bin/check-workflows.test.cjs +208 -0
  21. package/bin/install.js +500 -0
  22. package/bin/np-tools/_commands.cjs +70 -0
  23. package/bin/np-tools/add-tests.cjs +171 -0
  24. package/bin/np-tools/add-tests.test.cjs +122 -0
  25. package/bin/np-tools/add-todo.cjs +108 -0
  26. package/bin/np-tools/add-todo.test.cjs +112 -0
  27. package/bin/np-tools/agent-skills.cjs +14 -0
  28. package/bin/np-tools/agent-skills.test.cjs +42 -0
  29. package/bin/np-tools/ai-integration-phase.cjs +109 -0
  30. package/bin/np-tools/ai-integration-phase.test.cjs +123 -0
  31. package/bin/np-tools/askuser.cjs +53 -0
  32. package/bin/np-tools/askuser.test.cjs +49 -0
  33. package/bin/np-tools/autonomous.cjs +69 -0
  34. package/bin/np-tools/autonomous.test.cjs +74 -0
  35. package/bin/np-tools/checkpoint.cjs +101 -0
  36. package/bin/np-tools/checkpoint.test.cjs +119 -0
  37. package/bin/np-tools/code-review.cjs +133 -0
  38. package/bin/np-tools/code-review.test.cjs +96 -0
  39. package/bin/np-tools/commit-task.cjs +120 -0
  40. package/bin/np-tools/commit-task.test.cjs +160 -0
  41. package/bin/np-tools/commit.cjs +103 -0
  42. package/bin/np-tools/commit.test.cjs +93 -0
  43. package/bin/np-tools/config.cjs +101 -0
  44. package/bin/np-tools/config.test.cjs +71 -0
  45. package/bin/np-tools/discuss-phase-power.cjs +265 -0
  46. package/bin/np-tools/discuss-phase-power.test.cjs +242 -0
  47. package/bin/np-tools/discuss-phase.cjs +132 -0
  48. package/bin/np-tools/discuss-phase.test.cjs +148 -0
  49. package/bin/np-tools/dispatch.cjs +116 -0
  50. package/bin/np-tools/doctor.cjs +242 -0
  51. package/bin/np-tools/eval-review.cjs +116 -0
  52. package/bin/np-tools/eval-review.test.cjs +123 -0
  53. package/bin/np-tools/execute-phase.cjs +182 -0
  54. package/bin/np-tools/execute-phase.test.cjs +116 -0
  55. package/bin/np-tools/execute-plan.cjs +124 -0
  56. package/bin/np-tools/execute-plan.test.cjs +82 -0
  57. package/bin/np-tools/help.cjs +28 -0
  58. package/bin/np-tools/help.test.cjs +29 -0
  59. package/bin/np-tools/init-dispatch.test.cjs +91 -0
  60. package/bin/np-tools/metrics.cjs +97 -0
  61. package/bin/np-tools/metrics.test.cjs +188 -0
  62. package/bin/np-tools/new-milestone.cjs +288 -0
  63. package/bin/np-tools/new-milestone.test.cjs +166 -0
  64. package/bin/np-tools/new-project.cjs +284 -0
  65. package/bin/np-tools/new-project.test.cjs +165 -0
  66. package/bin/np-tools/next.cjs +7 -0
  67. package/bin/np-tools/next.test.cjs +30 -0
  68. package/bin/np-tools/park.cjs +48 -0
  69. package/bin/np-tools/park.test.cjs +50 -0
  70. package/bin/np-tools/pause-work.cjs +24 -0
  71. package/bin/np-tools/pause-work.test.cjs +74 -0
  72. package/bin/np-tools/phase.cjs +71 -0
  73. package/bin/np-tools/phase.test.cjs +81 -0
  74. package/bin/np-tools/plan-diff.cjs +57 -0
  75. package/bin/np-tools/plan-diff.test.cjs +134 -0
  76. package/bin/np-tools/plan-milestone-gaps.cjs +115 -0
  77. package/bin/np-tools/plan-milestone-gaps.test.cjs +122 -0
  78. package/bin/np-tools/plan-phase.cjs +350 -0
  79. package/bin/np-tools/plan-phase.test.cjs +263 -0
  80. package/bin/np-tools/progress.cjs +7 -0
  81. package/bin/np-tools/progress.test.cjs +44 -0
  82. package/bin/np-tools/queue.cjs +213 -0
  83. package/bin/np-tools/research-phase.cjs +144 -0
  84. package/bin/np-tools/research-phase.test.cjs +154 -0
  85. package/bin/np-tools/reset-slice.cjs +17 -0
  86. package/bin/np-tools/reset-slice.test.cjs +96 -0
  87. package/bin/np-tools/resolve-model.cjs +110 -0
  88. package/bin/np-tools/resolve-model.test.cjs +200 -0
  89. package/bin/np-tools/resume-work.cjs +76 -0
  90. package/bin/np-tools/resume-work.test.cjs +91 -0
  91. package/bin/np-tools/skip.cjs +48 -0
  92. package/bin/np-tools/skip.test.cjs +66 -0
  93. package/bin/np-tools/slug.cjs +34 -0
  94. package/bin/np-tools/slug.test.cjs +46 -0
  95. package/bin/np-tools/state.cjs +16 -0
  96. package/bin/np-tools/state.test.cjs +40 -0
  97. package/bin/np-tools/stats.cjs +151 -0
  98. package/bin/np-tools/stats.test.cjs +118 -0
  99. package/bin/np-tools/triage.cjs +128 -0
  100. package/bin/np-tools/ui-phase.cjs +108 -0
  101. package/bin/np-tools/ui-phase.test.cjs +121 -0
  102. package/bin/np-tools/ui-review.cjs +108 -0
  103. package/bin/np-tools/ui-review.test.cjs +120 -0
  104. package/bin/np-tools/undo-task.cjs +31 -0
  105. package/bin/np-tools/undo-task.test.cjs +117 -0
  106. package/bin/np-tools/undo.cjs +43 -0
  107. package/bin/np-tools/undo.test.cjs +120 -0
  108. package/bin/np-tools/unpark.cjs +48 -0
  109. package/bin/np-tools/unpark.test.cjs +50 -0
  110. package/bin/np-tools/verify-work.cjs +186 -0
  111. package/bin/np-tools/verify-work.test.cjs +97 -0
  112. package/docs/adr/0001-no-daemon-invariant.md +82 -0
  113. package/docs/adr/0002-zero-runtime-dependencies.md +90 -0
  114. package/docs/adr/0003-max-six-unit-types.md +85 -0
  115. package/docs/adr/0004-atomic-commit-per-unit.md +102 -0
  116. package/docs/adr/0005-three-orthogonal-file-trees.md +98 -0
  117. package/docs/adr/0006-yaml-dependency-amendment.md +60 -0
  118. package/docs/adr/README.md +27 -0
  119. package/docs/agent-frontmatter-schema.md +84 -0
  120. package/docs/phase-artifact-schemas.md +292 -0
  121. package/docs/phase-directory-layout.md +82 -0
  122. package/lib/__tests__/README.md +1 -0
  123. package/lib/agents.cjs +98 -0
  124. package/lib/agents.test.cjs +286 -0
  125. package/lib/askuser.cjs +36 -0
  126. package/lib/askuser.test.cjs +310 -0
  127. package/lib/checkpoint.cjs +135 -0
  128. package/lib/checkpoint.test.cjs +184 -0
  129. package/lib/core.cjs +165 -0
  130. package/lib/core.test.cjs +405 -0
  131. package/lib/fixtures/README.md +1 -0
  132. package/lib/fixtures/phase-tree/README.md +1 -0
  133. package/lib/fixtures/plans/cycle/PLAN.md +16 -0
  134. package/lib/fixtures/plans/cycle/tasks/T-01.md +20 -0
  135. package/lib/fixtures/plans/cycle/tasks/T-02.md +20 -0
  136. package/lib/fixtures/plans/cycle/tasks/T-03.md +20 -0
  137. package/lib/fixtures/plans/linear/PLAN.md +16 -0
  138. package/lib/fixtures/plans/linear/tasks/T-01.md +20 -0
  139. package/lib/fixtures/plans/linear/tasks/T-02.md +20 -0
  140. package/lib/fixtures/plans/linear/tasks/T-03.md +20 -0
  141. package/lib/fixtures/plans/parallel/PLAN.md +16 -0
  142. package/lib/fixtures/plans/parallel/tasks/T-01.md +20 -0
  143. package/lib/fixtures/plans/parallel/tasks/T-02.md +20 -0
  144. package/lib/fixtures/plans/parallel/tasks/T-03.md +20 -0
  145. package/lib/fixtures/plans/wave-conflict/PLAN.md +16 -0
  146. package/lib/fixtures/plans/wave-conflict/tasks/T-01.md +20 -0
  147. package/lib/fixtures/plans/wave-conflict/tasks/T-02.md +20 -0
  148. package/lib/fixtures/roadmap/ROADMAP-malformed.md +3 -0
  149. package/lib/fixtures/roadmap/ROADMAP-minimal.md +51 -0
  150. package/lib/fixtures/roadmap/roadmap-malformed.yaml +7 -0
  151. package/lib/fixtures/roadmap/roadmap-minimal.yaml +40 -0
  152. package/lib/fixtures/roadmap/roadmap-ten-phases.yaml +101 -0
  153. package/lib/fixtures/templates/phase-context.md +6 -0
  154. package/lib/fixtures/templates/plan-skeleton.md +6 -0
  155. package/lib/frontmatter.cjs +251 -0
  156. package/lib/frontmatter.test.cjs +177 -0
  157. package/lib/gaps.cjs +197 -0
  158. package/lib/gaps.test.cjs +200 -0
  159. package/lib/git.cjs +207 -0
  160. package/lib/git.test.cjs +305 -0
  161. package/lib/install/agents-md.cjs +77 -0
  162. package/lib/install/backup.cjs +70 -0
  163. package/lib/install/codex-toml.cjs +440 -0
  164. package/lib/install/managed-block.cjs +30 -0
  165. package/lib/install/manifest.cjs +148 -0
  166. package/lib/install/mcp-writer.cjs +127 -0
  167. package/lib/install/runtime-detect.cjs +44 -0
  168. package/lib/install/staging.cjs +149 -0
  169. package/lib/metrics-aggregate.cjs +229 -0
  170. package/lib/metrics-aggregate.test.cjs +192 -0
  171. package/lib/metrics.cjs +120 -0
  172. package/lib/metrics.test.cjs +182 -0
  173. package/lib/model-aliases.regression.test.cjs +16 -0
  174. package/lib/model-profiles.cjs +42 -0
  175. package/lib/model-profiles.test.cjs +61 -0
  176. package/lib/next.cjs +236 -0
  177. package/lib/next.test.cjs +194 -0
  178. package/lib/phase.cjs +95 -0
  179. package/lib/phase.test.cjs +189 -0
  180. package/lib/plan-checker-contract.test.cjs +72 -0
  181. package/lib/plan-diff.cjs +173 -0
  182. package/lib/plan-diff.test.cjs +217 -0
  183. package/lib/plan.cjs +85 -0
  184. package/lib/plan.test.cjs +263 -0
  185. package/lib/progress.cjs +95 -0
  186. package/lib/progress.test.cjs +116 -0
  187. package/lib/researcher-contract.test.cjs +61 -0
  188. package/lib/roadmap-render.cjs +206 -0
  189. package/lib/roadmap-render.test.cjs +121 -0
  190. package/lib/roadmap.cjs +416 -0
  191. package/lib/roadmap.test.cjs +371 -0
  192. package/lib/runtime/_contract.test.cjs +61 -0
  193. package/lib/runtime/_readline.cjs +119 -0
  194. package/lib/runtime/_readline.test.cjs +126 -0
  195. package/lib/runtime/claude.cjs +48 -0
  196. package/lib/runtime/claude.test.cjs +101 -0
  197. package/lib/runtime/codex.cjs +35 -0
  198. package/lib/runtime/codex.test.cjs +114 -0
  199. package/lib/runtime/gemini.cjs +35 -0
  200. package/lib/runtime/gemini.test.cjs +109 -0
  201. package/lib/runtime/index.cjs +49 -0
  202. package/lib/runtime/index.test.cjs +181 -0
  203. package/lib/runtime/opencode.cjs +35 -0
  204. package/lib/runtime/opencode.test.cjs +124 -0
  205. package/lib/state.cjs +205 -0
  206. package/lib/state.test.cjs +264 -0
  207. package/lib/surface-audit.test.cjs +46 -0
  208. package/lib/tasks.cjs +327 -0
  209. package/lib/tasks.test.cjs +389 -0
  210. package/lib/template.cjs +66 -0
  211. package/lib/template.test.cjs +159 -0
  212. package/lib/undo.cjs +179 -0
  213. package/lib/undo.test.cjs +261 -0
  214. package/lib/verify.cjs +116 -0
  215. package/lib/verify.test.cjs +187 -0
  216. package/np-tools.cjs +303 -0
  217. package/package.json +39 -0
  218. package/templates/AI-SPEC.md +90 -0
  219. package/templates/CONTEXT.md +32 -0
  220. package/templates/PLAN.md +69 -0
  221. package/templates/PROJECT.md +60 -0
  222. package/templates/REQUIREMENTS.md +38 -0
  223. package/templates/SECURITY.md +61 -0
  224. package/templates/UI-SPEC.md +64 -0
  225. package/templates/VALIDATION.md +76 -0
  226. package/templates/claude/payload/README.md +11 -0
  227. package/templates/opencode/opencode.json +6 -0
  228. package/templates/opencode/payload/AGENTS.md +9 -0
  229. package/workflows/add-backlog.md +212 -0
  230. package/workflows/add-tests.md +69 -0
  231. package/workflows/add-todo.md +222 -0
  232. package/workflows/ai-integration-phase.md +230 -0
  233. package/workflows/autonomous.md +94 -0
  234. package/workflows/cleanup.md +325 -0
  235. package/workflows/code-review-fix.md +435 -0
  236. package/workflows/code-review.md +447 -0
  237. package/workflows/discuss-phase-assumptions.md +269 -0
  238. package/workflows/discuss-phase-power.md +139 -0
  239. package/workflows/discuss-phase.md +386 -0
  240. package/workflows/dispatch.md +9 -0
  241. package/workflows/doctor.md +10 -0
  242. package/workflows/eval-review.md +243 -0
  243. package/workflows/execute-phase.md +142 -0
  244. package/workflows/execute-plan.md +82 -0
  245. package/workflows/help.md +8 -0
  246. package/workflows/new-milestone.md +166 -0
  247. package/workflows/new-project.md +213 -0
  248. package/workflows/next.md +8 -0
  249. package/workflows/note.md +244 -0
  250. package/workflows/park.md +29 -0
  251. package/workflows/pause-work.md +34 -0
  252. package/workflows/plan-milestone-gaps.md +233 -0
  253. package/workflows/plan-phase.md +351 -0
  254. package/workflows/progress.md +8 -0
  255. package/workflows/queue.md +9 -0
  256. package/workflows/research-phase.md +327 -0
  257. package/workflows/reset-slice.md +39 -0
  258. package/workflows/resume-work.md +79 -0
  259. package/workflows/review.md +489 -0
  260. package/workflows/secure-phase.md +209 -0
  261. package/workflows/session-report.md +243 -0
  262. package/workflows/skip.md +29 -0
  263. package/workflows/state.md +7 -0
  264. package/workflows/stats.md +170 -0
  265. package/workflows/thread.md +214 -0
  266. package/workflows/triage.md +9 -0
  267. package/workflows/ui-phase.md +246 -0
  268. package/workflows/ui-review.md +222 -0
  269. package/workflows/undo-task.md +42 -0
  270. package/workflows/undo.md +55 -0
  271. package/workflows/unpark.md +29 -0
  272. package/workflows/validate-phase.md +231 -0
  273. package/workflows/verify-work.md +83 -0
@@ -0,0 +1 @@
1
+ Shared test helpers — populated in later plans if needed.
package/lib/agents.cjs ADDED
@@ -0,0 +1,98 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { extractFrontmatter } = require('./frontmatter.cjs');
4
+ const { NubosPilotError, findProjectRoot } = require('./core.cjs');
5
+
6
+ const REQUIRED = ['name', 'description', 'tier', 'tools'];
7
+ const TIER_ENUM = ['haiku', 'sonnet', 'opus'];
8
+ const FORBIDDEN = ['model', 'model_profile', 'hooks'];
9
+
10
+ function _forbiddenHint(field) {
11
+ if (field === 'model') return 'Use "tier" instead.';
12
+ if (field === 'model_profile') return 'Use "tier" instead.';
13
+ return 'hooks are runtime-specific and deferred to Phase 7/8.';
14
+ }
15
+
16
+ function validateAgentFrontmatter(fm, agentName) {
17
+ for (const f of REQUIRED) {
18
+ if (!fm[f]) {
19
+ throw new NubosPilotError(
20
+ 'agent-invalid-frontmatter',
21
+ 'Agent "' + agentName + '" missing required frontmatter field: ' + f,
22
+ { field: f, agent: agentName },
23
+ );
24
+ }
25
+ }
26
+ for (const f of FORBIDDEN) {
27
+ if (fm[f] !== undefined) {
28
+ throw new NubosPilotError(
29
+ 'agent-forbidden-field',
30
+ 'Agent "' + agentName + '" uses forbidden frontmatter field: ' + f,
31
+ { field: f, agent: agentName, hint: _forbiddenHint(f) },
32
+ );
33
+ }
34
+ }
35
+ if (!TIER_ENUM.includes(fm.tier)) {
36
+ throw new NubosPilotError(
37
+ 'agent-invalid-tier',
38
+ 'Agent "' + agentName + '" has invalid tier: ' + fm.tier,
39
+ { agent: agentName, value: fm.tier, allowed: TIER_ENUM.slice() },
40
+ );
41
+ }
42
+ if (fm.name !== agentName) {
43
+ throw new NubosPilotError(
44
+ 'agent-invalid-frontmatter',
45
+ 'Agent filename "' + agentName + '" does not match frontmatter name "' + fm.name + '"',
46
+ { field: 'name', agent: agentName, expected: agentName, got: fm.name },
47
+ );
48
+ }
49
+ return fm;
50
+ }
51
+
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)) {
56
+ throw new NubosPilotError(
57
+ 'agent-not-found',
58
+ 'Agent "' + name + '" not found at ' + p,
59
+ { name, path: p },
60
+ );
61
+ }
62
+ const { frontmatter } = extractFrontmatter(fs.readFileSync(p, 'utf-8'));
63
+ return validateAgentFrontmatter(frontmatter, name);
64
+ }
65
+
66
+ function listAgents(cwd) {
67
+ const root = findProjectRoot(cwd || process.cwd());
68
+ const dir = path.join(root, 'agents');
69
+ if (!fs.existsSync(dir)) return [];
70
+ return fs.readdirSync(dir)
71
+ .filter((f) => f.endsWith('.md'))
72
+ .map((f) => f.replace(/\.md$/, ''))
73
+ .sort();
74
+ }
75
+
76
+ function getAgentSkills(name, cwd) {
77
+ const root = findProjectRoot(cwd || process.cwd());
78
+ const configPath = path.join(root, '.nubos-pilot', 'config.json');
79
+ if (!fs.existsSync(configPath)) return [];
80
+ let config;
81
+ try {
82
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
83
+ } catch {
84
+ return [];
85
+ }
86
+ const skills = config && config.agent_skills && config.agent_skills[name];
87
+ return Array.isArray(skills) ? skills : [];
88
+ }
89
+
90
+ module.exports = {
91
+ validateAgentFrontmatter,
92
+ loadAgent,
93
+ listAgents,
94
+ getAgentSkills,
95
+ TIER_ENUM,
96
+ REQUIRED,
97
+ FORBIDDEN,
98
+ };
@@ -0,0 +1,286 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { test } = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+
7
+ const agents = require('./agents.cjs');
8
+ const {
9
+ validateAgentFrontmatter,
10
+ loadAgent,
11
+ listAgents,
12
+ getAgentSkills,
13
+ TIER_ENUM,
14
+ REQUIRED,
15
+ FORBIDDEN,
16
+ } = agents;
17
+ const { extractFrontmatter } = require('./frontmatter.cjs');
18
+
19
+ const FIXTURE_DIR = path.join(__dirname, '..', 'tests', 'fixtures', 'agents');
20
+
21
+ const _sandboxes = [];
22
+
23
+ function makeAgentSandbox(seed) {
24
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-agents-'));
25
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
26
+ fs.mkdirSync(path.join(root, 'agents'), { recursive: true });
27
+ const payload = seed || {};
28
+ for (const [name, srcOrContent] of Object.entries(payload)) {
29
+ const target = path.join(root, 'agents', name + '.md');
30
+ if (srcOrContent.startsWith('fixture:')) {
31
+ const fixtureName = srcOrContent.slice('fixture:'.length);
32
+ const content = fs.readFileSync(path.join(FIXTURE_DIR, fixtureName + '.md'), 'utf-8');
33
+ fs.writeFileSync(target, content, 'utf-8');
34
+ } else {
35
+ fs.writeFileSync(target, srcOrContent, 'utf-8');
36
+ }
37
+ }
38
+ _sandboxes.push(root);
39
+ return root;
40
+ }
41
+
42
+ function cleanupAll() {
43
+ while (_sandboxes.length) {
44
+ const r = _sandboxes.pop();
45
+ try { fs.rmSync(r, { recursive: true, force: true }); } catch { }
46
+ }
47
+ }
48
+
49
+ test.afterEach(() => cleanupAll());
50
+
51
+ test('AG-1: valid frontmatter returns the fm object', () => {
52
+ const fm = { name: 'np-planner', description: 'd', tier: 'opus', tools: 'Read, Write, Bash' };
53
+ const out = validateAgentFrontmatter(fm, 'np-planner');
54
+ assert.equal(out, fm);
55
+ assert.equal(out.tier, 'opus');
56
+ });
57
+
58
+ test('AG-2: missing required field tier → agent-invalid-frontmatter with field=tier', () => {
59
+ const fm = { name: 'np-planner', description: 'd', tools: 'Read' };
60
+ let thrown = null;
61
+ try { validateAgentFrontmatter(fm, 'np-planner'); } catch (e) { thrown = e; }
62
+ assert.ok(thrown);
63
+ assert.equal(thrown.name, 'NubosPilotError');
64
+ assert.equal(thrown.code, 'agent-invalid-frontmatter');
65
+ assert.equal(thrown.details.field, 'tier');
66
+ assert.equal(thrown.details.agent, 'np-planner');
67
+ });
68
+
69
+ test('AG-3: invalid tier gpt-4 → agent-invalid-tier with allowed enum', () => {
70
+ const fm = { name: 'np-planner', description: 'd', tier: 'gpt-4', tools: 'Read' };
71
+ let thrown = null;
72
+ try { validateAgentFrontmatter(fm, 'np-planner'); } catch (e) { thrown = e; }
73
+ assert.ok(thrown);
74
+ assert.equal(thrown.code, 'agent-invalid-tier');
75
+ assert.deepEqual(thrown.details.allowed, ['haiku', 'sonnet', 'opus']);
76
+ assert.equal(thrown.details.value, 'gpt-4');
77
+ });
78
+
79
+ test('AG-4: forbidden model → agent-forbidden-field with hint containing "Use \\"tier\\" instead."', () => {
80
+ const fm = { name: 'np-planner', description: 'd', tier: 'opus', tools: 'Read', model: 'opus' };
81
+ let thrown = null;
82
+ try { validateAgentFrontmatter(fm, 'np-planner'); } catch (e) { thrown = e; }
83
+ assert.ok(thrown);
84
+ assert.equal(thrown.code, 'agent-forbidden-field');
85
+ assert.equal(thrown.details.field, 'model');
86
+ assert.ok(thrown.details.hint && thrown.details.hint.includes('Use "tier" instead.'));
87
+ });
88
+
89
+ test('AG-5: forbidden model_profile → agent-forbidden-field field=model_profile', () => {
90
+ const fm = { name: 'np-planner', description: 'd', tier: 'opus', tools: 'Read', model_profile: 'quality' };
91
+ let thrown = null;
92
+ try { validateAgentFrontmatter(fm, 'np-planner'); } catch (e) { thrown = e; }
93
+ assert.ok(thrown);
94
+ assert.equal(thrown.code, 'agent-forbidden-field');
95
+ assert.equal(thrown.details.field, 'model_profile');
96
+ });
97
+
98
+ test('AG-6: forbidden hooks → agent-forbidden-field field=hooks', () => {
99
+ const fm = { name: 'np-planner', description: 'd', tier: 'opus', tools: 'Read', hooks: 'anything' };
100
+ let thrown = null;
101
+ try { validateAgentFrontmatter(fm, 'np-planner'); } catch (e) { thrown = e; }
102
+ assert.ok(thrown);
103
+ assert.equal(thrown.code, 'agent-forbidden-field');
104
+ assert.equal(thrown.details.field, 'hooks');
105
+ });
106
+
107
+ test('AG-7: name mismatch → agent-invalid-frontmatter field=name with expected/got', () => {
108
+ const fm = { name: 'different', description: 'd', tier: 'opus', tools: 'Read' };
109
+ let thrown = null;
110
+ try { validateAgentFrontmatter(fm, 'np-planner'); } catch (e) { thrown = e; }
111
+ assert.ok(thrown);
112
+ assert.equal(thrown.code, 'agent-invalid-frontmatter');
113
+ assert.equal(thrown.details.field, 'name');
114
+ assert.equal(thrown.details.expected, 'np-planner');
115
+ assert.equal(thrown.details.got, 'different');
116
+ });
117
+
118
+ test('AG-8: loadAgent nonexistent → agent-not-found', () => {
119
+ const sb = makeAgentSandbox({});
120
+ let thrown = null;
121
+ try { loadAgent('nonexistent', sb); } catch (e) { thrown = e; }
122
+ assert.ok(thrown);
123
+ assert.equal(thrown.code, 'agent-not-found');
124
+ assert.equal(thrown.details.name, 'nonexistent');
125
+ });
126
+
127
+ test('AG-9: loadAgent planner returns fm with tier=opus', () => {
128
+ const sb = makeAgentSandbox({ 'np-planner': 'fixture:valid-planner' });
129
+ const fm = loadAgent('np-planner', sb);
130
+ assert.equal(fm.tier, 'opus');
131
+ assert.equal(fm.name, 'np-planner');
132
+ });
133
+
134
+ test('AG-10: listAgents returns sorted names (no .md suffix)', () => {
135
+ const sb = makeAgentSandbox({
136
+ 'np-planner': 'fixture:valid-planner',
137
+ 'np-researcher': '---\nname: np-researcher\ndescription: r\ntier: sonnet\ntools: Read\n---\nbody',
138
+ 'np-plan-checker': '---\nname: np-plan-checker\ndescription: pc\ntier: opus\ntools: Read\n---\nbody',
139
+ });
140
+ const out = listAgents(sb);
141
+ assert.deepEqual(out, ['np-plan-checker', 'np-planner', 'np-researcher']);
142
+ });
143
+
144
+ test('AG-11: listAgents on missing agents/ dir returns []', () => {
145
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-agents-empty-'));
146
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
147
+ _sandboxes.push(root);
148
+ const out = listAgents(root);
149
+ assert.deepEqual(out, []);
150
+ });
151
+
152
+ test('AG-12: getAgentSkills returns configured skills array when config present', () => {
153
+ const sb = makeAgentSandbox({ 'np-planner': 'fixture:valid-planner' });
154
+ const config = { agent_skills: { 'np-planner': ['Read', 'Grep'] } };
155
+ fs.writeFileSync(path.join(sb, '.nubos-pilot', 'config.json'), JSON.stringify(config), 'utf-8');
156
+ const out = getAgentSkills('np-planner', sb);
157
+ assert.deepEqual(out, ['Read', 'Grep']);
158
+ });
159
+
160
+ test('AG-13: getAgentSkills returns [] when config missing', () => {
161
+ const sb = makeAgentSandbox({ 'np-planner': 'fixture:valid-planner' });
162
+ const out = getAgentSkills('np-planner', sb);
163
+ assert.deepEqual(out, []);
164
+ });
165
+
166
+ test('AG-14: exported constants match spec', () => {
167
+ assert.deepEqual(TIER_ENUM, ['haiku', 'sonnet', 'opus']);
168
+ assert.deepEqual(FORBIDDEN, ['model', 'model_profile', 'hooks']);
169
+ assert.deepEqual(REQUIRED, ['name', 'description', 'tier', 'tools']);
170
+ });
171
+
172
+ test('AG-15: loadAgent round-trip through fixture with FORBIDDEN model field throws', () => {
173
+ const sb = makeAgentSandbox({ 'invalid-model': 'fixture:invalid-model-field' });
174
+ let thrown = null;
175
+ try { loadAgent('invalid-model', sb); } catch (e) { thrown = e; }
176
+ assert.ok(thrown);
177
+ assert.equal(thrown.code, 'agent-forbidden-field');
178
+ assert.equal(thrown.details.field, 'model');
179
+ });
180
+
181
+ function _toolsToArray(toolsField) {
182
+
183
+ return String(toolsField).split(',').map((s) => s.trim()).filter(Boolean);
184
+ }
185
+
186
+ const REPO_AGENTS_DIR = path.resolve(__dirname, '..', 'agents');
187
+
188
+ function _seedRealAgent(agentName) {
189
+
190
+ const src = fs.readFileSync(path.join(REPO_AGENTS_DIR, agentName + '.md'), 'utf-8');
191
+ return makeAgentSandbox({ [agentName]: src });
192
+ }
193
+
194
+ test('AG-16: loadAgent executor validates (tier sonnet, full toolset, no forbidden fields)', () => {
195
+ const sb = _seedRealAgent('np-executor');
196
+ const fm = loadAgent('np-executor', sb);
197
+ assert.equal(fm.name, 'np-executor');
198
+ assert.equal(fm.tier, 'sonnet');
199
+ const tools = _toolsToArray(fm.tools).sort();
200
+ assert.deepEqual(tools, ['Bash', 'Edit', 'Glob', 'Grep', 'Read', 'Write']);
201
+
202
+ assert.equal(fm.model, undefined);
203
+ assert.equal(fm.model_profile, undefined);
204
+ assert.equal(fm.hooks, undefined);
205
+ });
206
+
207
+ test('AG-17: loadAgent verifier is sonnet and read-only (no Write/Edit in tools)', () => {
208
+ const sb = _seedRealAgent('np-verifier');
209
+ const fm = loadAgent('np-verifier', sb);
210
+ assert.equal(fm.name, 'np-verifier');
211
+ assert.equal(fm.tier, 'sonnet');
212
+ const tools = _toolsToArray(fm.tools);
213
+
214
+ assert.ok(!tools.includes('Write'), 'verifier must not have Write tool');
215
+ assert.ok(!tools.includes('Edit'), 'verifier must not have Edit tool');
216
+
217
+ assert.deepEqual(tools.slice().sort(), ['Bash', 'Glob', 'Grep', 'Read']);
218
+ });
219
+
220
+ const NP_AGENTS_EXPECTED_TIERS = {
221
+ 'np-framework-selector': 'opus',
222
+ 'np-ai-researcher': 'sonnet',
223
+ 'np-domain-researcher': 'sonnet',
224
+ 'np-eval-planner': 'opus',
225
+ 'np-eval-auditor': 'haiku',
226
+ 'np-ui-researcher': 'sonnet',
227
+ 'np-ui-checker': 'haiku',
228
+ 'np-ui-auditor': 'haiku',
229
+ };
230
+
231
+ const _npAgentNames = Object.keys(NP_AGENTS_EXPECTED_TIERS);
232
+ for (let i = 0; i < _npAgentNames.length; i += 1) {
233
+ const agentName = _npAgentNames[i];
234
+ const expectedTier = NP_AGENTS_EXPECTED_TIERS[agentName];
235
+ const testId = 'AG-' + (18 + i);
236
+ test(testId + ': loadAgent ' + agentName + ' has tier=' + expectedTier + ' and no FORBIDDEN fields', () => {
237
+ const sb = _seedRealAgent(agentName);
238
+ const fm = loadAgent(agentName, sb);
239
+ assert.equal(fm.name, agentName);
240
+ assert.equal(fm.tier, expectedTier);
241
+ assert.equal(fm.model, undefined, agentName + ' has FORBIDDEN field model');
242
+ assert.equal(fm.model_profile, undefined, agentName + ' has FORBIDDEN field model_profile');
243
+ assert.equal(fm.hooks, undefined, agentName + ' has FORBIDDEN field hooks');
244
+ });
245
+ }
246
+
247
+ test('AG-26: bulk np-* iteration — all 8 Phase-9 agents exist in agents/ and pass validation', () => {
248
+
249
+ const seed = {};
250
+ for (const n of _npAgentNames) {
251
+ seed[n] = fs.readFileSync(path.join(REPO_AGENTS_DIR, n + '.md'), 'utf-8');
252
+ }
253
+ const sb = makeAgentSandbox(seed);
254
+ const names = listAgents(sb).filter((n) => n.startsWith('np-'));
255
+ assert.equal(names.length, 8, 'Expected exactly 8 np-*.md files; got ' + names.length);
256
+ for (const n of names) {
257
+ const fm = loadAgent(n, sb);
258
+ assert.ok(TIER_ENUM.includes(fm.tier), n + ' has invalid tier ' + fm.tier);
259
+ assert.equal(fm.name, n);
260
+ assert.equal(fm.model, undefined);
261
+ assert.equal(fm.model_profile, undefined);
262
+ assert.equal(fm.hooks, undefined);
263
+ }
264
+ });
265
+
266
+ const PHASE_10_AGENTS = [
267
+ { file: 'np-code-reviewer', expected_tier: 'opus' },
268
+ { file: 'np-code-fixer', expected_tier: 'sonnet' },
269
+ { file: 'np-security-auditor', expected_tier: 'opus' },
270
+ { file: 'np-nyquist-auditor', expected_tier: 'haiku' },
271
+ ];
272
+
273
+ for (let i = 0; i < PHASE_10_AGENTS.length; i += 1) {
274
+ const spec = PHASE_10_AGENTS[i];
275
+ const testId = 'AG-' + (30 + i);
276
+ test(testId + ': ' + spec.file + ' passes validateAgentFrontmatter with tier:' + spec.expected_tier, () => {
277
+ const src = fs.readFileSync(path.join(REPO_AGENTS_DIR, spec.file + '.md'), 'utf-8');
278
+ const fm = extractFrontmatter(src).frontmatter;
279
+ assert.doesNotThrow(() => validateAgentFrontmatter(fm, spec.file));
280
+ assert.equal(fm.tier, spec.expected_tier);
281
+ assert.equal(fm.name, spec.file);
282
+ assert.ok(!('model' in fm), spec.file + ' must not have model: key');
283
+ assert.ok(!('model_profile' in fm), spec.file + ' must not have model_profile: key');
284
+ assert.ok(!('hooks' in fm), spec.file + ' must not have hooks: key');
285
+ });
286
+ }
@@ -0,0 +1,36 @@
1
+ let _runtime = null;
2
+
3
+ function _detectRuntime() {
4
+ const env = process.env;
5
+ if (env.NUBOS_RUNTIME) return String(env.NUBOS_RUNTIME);
6
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return 'claude';
7
+ if (env.CODEX_HOME || env.CODEX_VERSION) return 'codex';
8
+ if (env.GEMINI_CLI || env.GEMINI_VERSION) return 'gemini';
9
+ if (env.OPENCODE || env.OPENCODE_VERSION) return 'opencode';
10
+ return 'generic-readline';
11
+ }
12
+
13
+ function getRuntime() {
14
+ if (_runtime === null || process.env.NUBOS_PILOT_REDETECT_RUNTIME === '1') {
15
+ _runtime = _detectRuntime();
16
+ }
17
+ return _runtime;
18
+ }
19
+
20
+ function _setReadlineImplForTests(impl) {
21
+ const rl = require('./runtime/_readline.cjs');
22
+ rl._setReadlineImplForTests(impl);
23
+ }
24
+
25
+ async function askUser(spec) {
26
+ const { getCurrent } = require('./runtime/index.cjs');
27
+ const adapter = getCurrent();
28
+ return adapter.askUser(spec);
29
+ }
30
+
31
+ module.exports = {
32
+ askUser,
33
+ getRuntime,
34
+ _detectRuntime,
35
+ _setReadlineImplForTests,
36
+ };