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
package/lib/core.cjs ADDED
@@ -0,0 +1,165 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+ const crypto = require('node:crypto');
5
+
6
+ class NubosPilotError extends Error {
7
+ constructor(code, message, details) {
8
+ super(message);
9
+ this.name = 'NubosPilotError';
10
+ this.code = code;
11
+ this.details = details;
12
+ if (typeof Error.captureStackTrace === 'function') {
13
+ Error.captureStackTrace(this, NubosPilotError);
14
+ }
15
+ }
16
+ }
17
+
18
+ function atomicWriteFileSync(filePath, content, encoding = 'utf-8') {
19
+ const tmp = `${filePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
20
+ try {
21
+ fs.writeFileSync(tmp, content, encoding);
22
+ fs.renameSync(tmp, filePath);
23
+ } catch (err) {
24
+ try { fs.unlinkSync(tmp); } catch {}
25
+ fs.writeFileSync(filePath, content, encoding);
26
+ }
27
+ }
28
+
29
+ const _heldLocks = new Set();
30
+ let _exitHandlerRegistered = false;
31
+
32
+ function _ensureExitHandler() {
33
+ if (_exitHandlerRegistered) return;
34
+ _exitHandlerRegistered = true;
35
+ process.on('exit', () => {
36
+ for (const p of _heldLocks) {
37
+ try { fs.unlinkSync(p); } catch {}
38
+ }
39
+ });
40
+ }
41
+
42
+ function _isPidAlive(pid) {
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ } catch (err) {
47
+ if (err && err.code === 'ESRCH') return false;
48
+ if (err && err.code === 'EPERM') return true;
49
+ return true;
50
+ }
51
+ }
52
+
53
+ function _sleepSync(ms) {
54
+ try {
55
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
56
+ } catch {
57
+ const until = Date.now() + ms;
58
+ while (Date.now() < until) { }
59
+ }
60
+ }
61
+
62
+ function withFileLock(filePath, fn, opts) {
63
+ const { timeoutMs = 10000, pollMs = 50, staleMs = 30000 } = opts || {};
64
+ try { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } catch {}
65
+ const lockPath = `${filePath}.lock`;
66
+ const selfHost = os.hostname();
67
+ const startedAt = Date.now();
68
+ let lastMeta = null;
69
+
70
+ while (true) {
71
+ try {
72
+ const payload = JSON.stringify({
73
+ pid: process.pid,
74
+ hostname: selfHost,
75
+ acquiredAt: new Date().toISOString(),
76
+ });
77
+ fs.writeFileSync(lockPath, payload, { flag: 'wx' });
78
+ _ensureExitHandler();
79
+ _heldLocks.add(lockPath);
80
+ try {
81
+ return fn();
82
+ } finally {
83
+ _heldLocks.delete(lockPath);
84
+ try { fs.unlinkSync(lockPath); } catch {}
85
+ }
86
+ } catch (err) {
87
+ if (!err || err.code !== 'EEXIST') {
88
+ throw err;
89
+ }
90
+ let meta = null;
91
+ let stat = null;
92
+ try {
93
+ meta = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
94
+ } catch {}
95
+ try {
96
+ stat = fs.statSync(lockPath);
97
+ } catch {}
98
+ if (meta) lastMeta = meta;
99
+ if (stat && Date.now() - stat.mtimeMs > staleMs) {
100
+ if (meta && meta.hostname && meta.hostname !== selfHost) {
101
+
102
+ } else if (meta && !_isPidAlive(meta.pid)) {
103
+ try { fs.unlinkSync(lockPath); } catch {}
104
+ continue;
105
+ }
106
+ }
107
+ if (Date.now() - startedAt >= timeoutMs) {
108
+ throw new NubosPilotError(
109
+ 'lock-timeout',
110
+ `Could not acquire lock on ${filePath} within ${timeoutMs}ms`,
111
+ { lockPath, holder: lastMeta },
112
+ );
113
+ }
114
+ _sleepSync(pollMs);
115
+ if (Date.now() - startedAt >= timeoutMs) {
116
+ throw new NubosPilotError(
117
+ 'lock-timeout',
118
+ `Could not acquire lock on ${filePath} within ${timeoutMs}ms`,
119
+ { lockPath, holder: lastMeta },
120
+ );
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ function withFileLocks(paths, fn, opts) {
127
+ const sorted = [...paths].sort();
128
+ function acquire(idx) {
129
+ if (idx >= sorted.length) return fn();
130
+ return withFileLock(sorted[idx], () => acquire(idx + 1), opts);
131
+ }
132
+ return acquire(0);
133
+ }
134
+
135
+ function findProjectRoot(cwd = process.cwd()) {
136
+ let dir = path.resolve(cwd);
137
+ const root = path.parse(dir).root;
138
+ while (true) {
139
+ const candidate = path.join(dir, '.nubos-pilot');
140
+ try {
141
+ if (fs.statSync(candidate).isDirectory()) return dir;
142
+ } catch { }
143
+ if (dir === root) {
144
+ throw new NubosPilotError(
145
+ 'not-in-project',
146
+ `No .nubos-pilot/ ancestor of ${cwd}`,
147
+ { startedFrom: cwd },
148
+ );
149
+ }
150
+ dir = path.dirname(dir);
151
+ }
152
+ }
153
+
154
+ function projectStateDir(cwd = process.cwd()) {
155
+ return path.join(findProjectRoot(cwd), '.nubos-pilot');
156
+ }
157
+
158
+ module.exports = {
159
+ atomicWriteFileSync,
160
+ withFileLock,
161
+ withFileLocks,
162
+ findProjectRoot,
163
+ projectStateDir,
164
+ NubosPilotError,
165
+ };
@@ -0,0 +1,405 @@
1
+ const core = require('./core.cjs');
2
+
3
+ const { test, before, beforeEach, afterEach } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const os = require('node:os');
8
+ const crypto = require('node:crypto');
9
+
10
+ let DEAD_PID_AVAILABLE = true;
11
+ const DEAD_PID = 99999999;
12
+
13
+ before(() => {
14
+ try {
15
+ process.kill(DEAD_PID, 0);
16
+ DEAD_PID_AVAILABLE = false;
17
+ } catch (err) {
18
+ DEAD_PID_AVAILABLE = err.code === 'ESRCH';
19
+ }
20
+ });
21
+
22
+ function mkSandbox() {
23
+ const dir = path.join(os.tmpdir(), 'nubos-pilot-test-' + crypto.randomBytes(8).toString('hex'));
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ return dir;
26
+ }
27
+
28
+ function rmSandbox(dir) {
29
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
30
+ }
31
+
32
+ test('NubosPilotError sets code, message, details, name and is instanceof Error', () => {
33
+ const e = new core.NubosPilotError('lock-timeout', 'msg', { extra: 1 });
34
+ assert.equal(e.code, 'lock-timeout');
35
+ assert.equal(e.message, 'msg');
36
+ assert.deepEqual(e.details, { extra: 1 });
37
+ assert.equal(e.name, 'NubosPilotError');
38
+ assert.ok(e instanceof Error);
39
+ });
40
+
41
+ test('A1 atomicWriteFileSync writes content round-trip-identical', () => {
42
+ const dir = mkSandbox();
43
+ try {
44
+ const target = path.join(dir, 'a.txt');
45
+ const content = 'hello world ' + crypto.randomBytes(4).toString('hex');
46
+ core.atomicWriteFileSync(target, content);
47
+ const read = fs.readFileSync(target, 'utf-8');
48
+ assert.equal(read, content);
49
+ } finally {
50
+ rmSandbox(dir);
51
+ }
52
+ });
53
+
54
+ test('A2 atomicWriteFileSync leaves no *.tmp leftover after success', () => {
55
+ const dir = mkSandbox();
56
+ try {
57
+ const target = path.join(dir, 'b.txt');
58
+ core.atomicWriteFileSync(target, 'x');
59
+ const entries = fs.readdirSync(dir);
60
+ const tmps = entries.filter((n) => n.endsWith('.tmp'));
61
+ assert.deepEqual(tmps, []);
62
+ } finally {
63
+ rmSandbox(dir);
64
+ }
65
+ });
66
+
67
+ test('A3 atomicWriteFileSync overwrites existing file', () => {
68
+ const dir = mkSandbox();
69
+ try {
70
+ const target = path.join(dir, 'c.txt');
71
+ fs.writeFileSync(target, 'old');
72
+ core.atomicWriteFileSync(target, 'new');
73
+ assert.equal(fs.readFileSync(target, 'utf-8'), 'new');
74
+ } finally {
75
+ rmSandbox(dir);
76
+ }
77
+ });
78
+
79
+ test('A4 tmp filename pattern is <target>.<pid>.<12-hex>.tmp (observed via writeFileSync spy)', () => {
80
+ const dir = mkSandbox();
81
+ try {
82
+ const target = path.join(dir, 'd.txt');
83
+ const origWrite = fs.writeFileSync;
84
+ const seenPaths = [];
85
+ fs.writeFileSync = function (p, ...rest) {
86
+ seenPaths.push(String(p));
87
+ return origWrite.apply(this, [p, ...rest]);
88
+ };
89
+ try {
90
+ core.atomicWriteFileSync(target, 'payload');
91
+ } finally {
92
+ fs.writeFileSync = origWrite;
93
+ }
94
+ const tmpCandidates = seenPaths.filter((p) => p !== target && p.startsWith(target + '.') && p.endsWith('.tmp'));
95
+ assert.ok(tmpCandidates.length >= 1, 'expected at least one tmp write call; saw: ' + JSON.stringify(seenPaths));
96
+ const tmp = tmpCandidates[0];
97
+ const suffix = tmp.slice(target.length + 1);
98
+ const match = /^(\d+)\.([0-9a-f]{12})\.tmp$/.exec(suffix);
99
+ assert.ok(match, 'tmp suffix must be <pid>.<12-hex>.tmp, got: ' + suffix);
100
+ assert.equal(Number(match[1]), process.pid);
101
+ } finally {
102
+ rmSandbox(dir);
103
+ }
104
+ });
105
+
106
+ test('A5 two parallel atomicWriteFileSync calls to same target: no collision, final content is one of the payloads', async () => {
107
+ const dir = mkSandbox();
108
+ try {
109
+ const target = path.join(dir, 'e.txt');
110
+ const a = 'A'.repeat(256);
111
+ const b = 'B'.repeat(256);
112
+ await Promise.all([
113
+ Promise.resolve().then(() => core.atomicWriteFileSync(target, a)),
114
+ Promise.resolve().then(() => core.atomicWriteFileSync(target, b)),
115
+ ]);
116
+ const final = fs.readFileSync(target, 'utf-8');
117
+ assert.ok(final === a || final === b, 'final must equal one payload verbatim, got length ' + final.length);
118
+ const entries = fs.readdirSync(dir);
119
+ const tmps = entries.filter((n) => n.endsWith('.tmp'));
120
+ assert.deepEqual(tmps, [], 'no tmp leftovers after parallel writes');
121
+ } finally {
122
+ rmSandbox(dir);
123
+ }
124
+ });
125
+
126
+ test('L1 withFileLock creates lockfile during fn and removes it after', () => {
127
+ const dir = mkSandbox();
128
+ try {
129
+ const target = path.join(dir, 'f.txt');
130
+ fs.writeFileSync(target, '');
131
+ const lockPath = target + '.lock';
132
+ let sawLock = false;
133
+ core.withFileLock(target, () => {
134
+ sawLock = fs.existsSync(lockPath);
135
+ });
136
+ assert.equal(sawLock, true);
137
+ assert.equal(fs.existsSync(lockPath), false);
138
+ } finally {
139
+ rmSandbox(dir);
140
+ }
141
+ });
142
+
143
+ test('L2 lockfile content is valid JSON {pid, hostname, acquiredAt ISO-8601}', () => {
144
+ const dir = mkSandbox();
145
+ try {
146
+ const target = path.join(dir, 'g.txt');
147
+ fs.writeFileSync(target, '');
148
+ const lockPath = target + '.lock';
149
+ let meta;
150
+ core.withFileLock(target, () => {
151
+ meta = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
152
+ });
153
+ assert.equal(typeof meta.pid, 'number');
154
+ assert.equal(typeof meta.hostname, 'string');
155
+ assert.ok(/^\d{4}-/.test(meta.acquiredAt), 'acquiredAt must be ISO-8601, got ' + meta.acquiredAt);
156
+ } finally {
157
+ rmSandbox(dir);
158
+ }
159
+ });
160
+
161
+ test('L3 re-entrant withFileLock on same path sequences (second waits for first)', async () => {
162
+ const dir = mkSandbox();
163
+ try {
164
+ const target = path.join(dir, 'h.txt');
165
+ fs.writeFileSync(target, '');
166
+ const events = [];
167
+ const first = new Promise((resolve) => {
168
+ setImmediate(() => {
169
+ core.withFileLock(target, () => {
170
+ events.push('first-start');
171
+ const until = Date.now() + 80;
172
+ while (Date.now() < until) { }
173
+ events.push('first-end');
174
+ }, { timeoutMs: 2000, pollMs: 10 });
175
+ resolve();
176
+ });
177
+ });
178
+ const second = new Promise((resolve) => {
179
+ setImmediate(() => {
180
+ setTimeout(() => {
181
+ core.withFileLock(target, () => {
182
+ events.push('second-start');
183
+ events.push('second-end');
184
+ }, { timeoutMs: 2000, pollMs: 10 });
185
+ resolve();
186
+ }, 10);
187
+ });
188
+ });
189
+ await Promise.all([first, second]);
190
+ assert.deepEqual(events, ['first-start', 'first-end', 'second-start', 'second-end']);
191
+ } finally {
192
+ rmSandbox(dir);
193
+ }
194
+ });
195
+
196
+ test('L4 lock-timeout throws NubosPilotError with code lock-timeout when held by live local pid', () => {
197
+ const dir = mkSandbox();
198
+ try {
199
+ const target = path.join(dir, 'i.txt');
200
+ fs.writeFileSync(target, '');
201
+ const lockPath = target + '.lock';
202
+ fs.writeFileSync(lockPath, JSON.stringify({
203
+ pid: process.pid,
204
+ hostname: os.hostname(),
205
+ acquiredAt: new Date().toISOString(),
206
+ }), { flag: 'wx' });
207
+ try {
208
+ assert.throws(
209
+ () => core.withFileLock(target, () => {}, { timeoutMs: 200, pollMs: 20 }),
210
+ (err) => err instanceof core.NubosPilotError && err.code === 'lock-timeout',
211
+ );
212
+ } finally {
213
+ try { fs.unlinkSync(lockPath); } catch {}
214
+ }
215
+ } finally {
216
+ rmSandbox(dir);
217
+ }
218
+ });
219
+
220
+ test('L5 stale lock + dead PID + same hostname → force-acquire', (t) => {
221
+ if (!DEAD_PID_AVAILABLE) { t.skip('DEAD_PID unexpectedly alive on this host'); return; }
222
+ const dir = mkSandbox();
223
+ try {
224
+ const target = path.join(dir, 'j.txt');
225
+ fs.writeFileSync(target, '');
226
+ const lockPath = target + '.lock';
227
+ fs.writeFileSync(lockPath, JSON.stringify({
228
+ pid: DEAD_PID,
229
+ hostname: os.hostname(),
230
+ acquiredAt: new Date(Date.now() - 60000).toISOString(),
231
+ }));
232
+ const past = new Date(Date.now() - 60000);
233
+ fs.utimesSync(lockPath, past, past);
234
+ let ran = false;
235
+ core.withFileLock(target, () => { ran = true; }, { timeoutMs: 1000, pollMs: 20, staleMs: 30000 });
236
+ assert.equal(ran, true);
237
+ } finally {
238
+ rmSandbox(dir);
239
+ }
240
+ });
241
+
242
+ test('L6 stale lock + ALIVE PID + same hostname → never force; lock-timeout', () => {
243
+ const dir = mkSandbox();
244
+ try {
245
+ const target = path.join(dir, 'k.txt');
246
+ fs.writeFileSync(target, '');
247
+ const lockPath = target + '.lock';
248
+ fs.writeFileSync(lockPath, JSON.stringify({
249
+ pid: process.pid,
250
+ hostname: os.hostname(),
251
+ acquiredAt: new Date(Date.now() - 60000).toISOString(),
252
+ }));
253
+ const past = new Date(Date.now() - 60000);
254
+ fs.utimesSync(lockPath, past, past);
255
+ try {
256
+ assert.throws(
257
+ () => core.withFileLock(target, () => {}, { timeoutMs: 200, pollMs: 20, staleMs: 30000 }),
258
+ (err) => err instanceof core.NubosPilotError && err.code === 'lock-timeout',
259
+ );
260
+ } finally {
261
+ try { fs.unlinkSync(lockPath); } catch {}
262
+ }
263
+ } finally {
264
+ rmSandbox(dir);
265
+ }
266
+ });
267
+
268
+ test('L7 stale lock + REMOTE hostname → never force; lock-timeout', () => {
269
+ const dir = mkSandbox();
270
+ try {
271
+ const target = path.join(dir, 'l.txt');
272
+ fs.writeFileSync(target, '');
273
+ const lockPath = target + '.lock';
274
+ fs.writeFileSync(lockPath, JSON.stringify({
275
+ pid: DEAD_PID,
276
+ hostname: '__remote_test_host__',
277
+ acquiredAt: new Date(Date.now() - 60000).toISOString(),
278
+ }));
279
+ const past = new Date(Date.now() - 60000);
280
+ fs.utimesSync(lockPath, past, past);
281
+ try {
282
+ assert.throws(
283
+ () => core.withFileLock(target, () => {}, { timeoutMs: 200, pollMs: 20, staleMs: 30000 }),
284
+ (err) => err instanceof core.NubosPilotError && err.code === 'lock-timeout',
285
+ );
286
+ } finally {
287
+ try { fs.unlinkSync(lockPath); } catch {}
288
+ }
289
+ } finally {
290
+ rmSandbox(dir);
291
+ }
292
+ });
293
+
294
+ test('L8 exit-handler registration: process.listenerCount("exit") >= 1 after first withFileLock', () => {
295
+ const dir = mkSandbox();
296
+ try {
297
+ const target = path.join(dir, 'm.txt');
298
+ fs.writeFileSync(target, '');
299
+ core.withFileLock(target, () => {});
300
+ assert.ok(process.listenerCount('exit') >= 1);
301
+ } finally {
302
+ rmSandbox(dir);
303
+ }
304
+ });
305
+
306
+ test('M1 withFileLocks acquires in lexicographic sort order', () => {
307
+ const dir = mkSandbox();
308
+ try {
309
+ const a = path.join(dir, 'a.lock-target');
310
+ const z = path.join(dir, 'z.lock-target');
311
+ fs.writeFileSync(a, '');
312
+ fs.writeFileSync(z, '');
313
+ const origWrite = fs.writeFileSync;
314
+ const writes = [];
315
+ fs.writeFileSync = function (p, ...rest) {
316
+ writes.push(String(p));
317
+ return origWrite.apply(this, [p, ...rest]);
318
+ };
319
+ try {
320
+ core.withFileLocks([z, a], () => {});
321
+ } finally {
322
+ fs.writeFileSync = origWrite;
323
+ }
324
+ const lockWrites = writes.filter((p) => p.endsWith('.lock'));
325
+ assert.ok(lockWrites.length >= 2, 'expected ≥2 lock writes, got ' + JSON.stringify(lockWrites));
326
+ assert.ok(lockWrites[0].endsWith('a.lock-target.lock'), 'first lock must be sorted-first: ' + lockWrites[0]);
327
+ assert.ok(lockWrites[1].endsWith('z.lock-target.lock'), 'second lock must be sorted-second: ' + lockWrites[1]);
328
+ } finally {
329
+ rmSandbox(dir);
330
+ }
331
+ });
332
+
333
+ test('M2 withFileLocks releases in reverse order (lockfiles disappear in reverse)', () => {
334
+ const dir = mkSandbox();
335
+ try {
336
+ const a = path.join(dir, 'a.lock-target');
337
+ const z = path.join(dir, 'z.lock-target');
338
+ fs.writeFileSync(a, '');
339
+ fs.writeFileSync(z, '');
340
+ const origUnlink = fs.unlinkSync;
341
+ const unlinks = [];
342
+ fs.unlinkSync = function (p, ...rest) {
343
+ unlinks.push(String(p));
344
+ return origUnlink.apply(this, [p, ...rest]);
345
+ };
346
+ try {
347
+ core.withFileLocks([z, a], () => {});
348
+ } finally {
349
+ fs.unlinkSync = origUnlink;
350
+ }
351
+ const lockUnlinks = unlinks.filter((p) => p.endsWith('.lock'));
352
+ assert.ok(lockUnlinks.length >= 2);
353
+ assert.ok(lockUnlinks[0].endsWith('z.lock-target.lock'), 'first unlink must be reverse-sorted: ' + lockUnlinks[0]);
354
+ assert.ok(lockUnlinks[1].endsWith('a.lock-target.lock'), 'second unlink must be reverse-sorted: ' + lockUnlinks[1]);
355
+ } finally {
356
+ rmSandbox(dir);
357
+ }
358
+ });
359
+
360
+ test('P1 findProjectRoot returns dir whose child is .nubos-pilot/', () => {
361
+ const dir = mkSandbox();
362
+ try {
363
+ fs.mkdirSync(path.join(dir, '.nubos-pilot'));
364
+ const root = core.findProjectRoot(dir);
365
+ assert.equal(fs.realpathSync(root), fs.realpathSync(dir));
366
+ } finally {
367
+ rmSandbox(dir);
368
+ }
369
+ });
370
+
371
+ test('P2 findProjectRoot walks up from nested subdir', () => {
372
+ const dir = mkSandbox();
373
+ try {
374
+ fs.mkdirSync(path.join(dir, '.nubos-pilot'));
375
+ const nested = path.join(dir, 'a', 'b', 'c');
376
+ fs.mkdirSync(nested, { recursive: true });
377
+ const root = core.findProjectRoot(nested);
378
+ assert.equal(fs.realpathSync(root), fs.realpathSync(dir));
379
+ } finally {
380
+ rmSandbox(dir);
381
+ }
382
+ });
383
+
384
+ test('P3 findProjectRoot throws NubosPilotError code=not-in-project when no ancestor', () => {
385
+ const dir = mkSandbox();
386
+ try {
387
+ assert.throws(
388
+ () => core.findProjectRoot(dir),
389
+ (err) => err instanceof core.NubosPilotError && err.code === 'not-in-project',
390
+ );
391
+ } finally {
392
+ rmSandbox(dir);
393
+ }
394
+ });
395
+
396
+ test('P4 projectStateDir returns path.join(root, .nubos-pilot)', () => {
397
+ const dir = mkSandbox();
398
+ try {
399
+ fs.mkdirSync(path.join(dir, '.nubos-pilot'));
400
+ const stateDir = core.projectStateDir(dir);
401
+ assert.equal(fs.realpathSync(stateDir), fs.realpathSync(path.join(dir, '.nubos-pilot')));
402
+ } finally {
403
+ rmSandbox(dir);
404
+ }
405
+ });
@@ -0,0 +1 @@
1
+ Shared fixtures for Phase 3 parser tests — each plan adds its own subdirectory.
@@ -0,0 +1 @@
1
+ Fixtures for phase-directory tests are created in tmpdir per-test; this README exists only to keep the dir tracked.
@@ -0,0 +1,16 @@
1
+ ---
2
+ phase: 99
3
+ plan: 01
4
+ type: execute
5
+ wave: 1
6
+ depends_on: []
7
+ files_modified: []
8
+ autonomous: true
9
+ must_haves:
10
+ truths:
11
+ - "fixture stub"
12
+ artifacts: []
13
+ key_links: []
14
+ ---
15
+
16
+ Cycle fixture: T-01 -> T-03 -> T-02 -> T-01.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: 99-01-T01
3
+ status: pending
4
+ tier: sonnet
5
+ owner: np-executor
6
+ phase: 99
7
+ plan: 01
8
+ type: execute
9
+ wave: 1
10
+ depends_on: [T-03]
11
+ files_modified: []
12
+ autonomous: true
13
+ must_haves:
14
+ truths:
15
+ - "stub"
16
+ artifacts: []
17
+ key_links: []
18
+ ---
19
+
20
+ Task T-01 body.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: 99-01-T02
3
+ status: pending
4
+ tier: sonnet
5
+ owner: np-executor
6
+ phase: 99
7
+ plan: 01
8
+ type: execute
9
+ wave: 1
10
+ depends_on: [T-01]
11
+ files_modified: []
12
+ autonomous: true
13
+ must_haves:
14
+ truths:
15
+ - "stub"
16
+ artifacts: []
17
+ key_links: []
18
+ ---
19
+
20
+ Task T-02 body.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: 99-01-T03
3
+ status: pending
4
+ tier: sonnet
5
+ owner: np-executor
6
+ phase: 99
7
+ plan: 01
8
+ type: execute
9
+ wave: 1
10
+ depends_on: [T-02]
11
+ files_modified: []
12
+ autonomous: true
13
+ must_haves:
14
+ truths:
15
+ - "stub"
16
+ artifacts: []
17
+ key_links: []
18
+ ---
19
+
20
+ Task T-03 body.
@@ -0,0 +1,16 @@
1
+ ---
2
+ phase: 99
3
+ plan: 01
4
+ type: execute
5
+ wave: 1
6
+ depends_on: []
7
+ files_modified: []
8
+ autonomous: true
9
+ must_haves:
10
+ truths:
11
+ - "fixture stub"
12
+ artifacts: []
13
+ key_links: []
14
+ ---
15
+
16
+ Linear chain fixture: T-01 -> T-02 -> T-03.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: 99-01-T01
3
+ status: pending
4
+ tier: sonnet
5
+ owner: np-executor
6
+ phase: 99
7
+ plan: 01
8
+ type: execute
9
+ wave: 1
10
+ depends_on: []
11
+ files_modified: []
12
+ autonomous: true
13
+ must_haves:
14
+ truths:
15
+ - "stub"
16
+ artifacts: []
17
+ key_links: []
18
+ ---
19
+
20
+ Task T-01 body.