nubos-pilot 0.9.3 → 0.9.4

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.
@@ -9,6 +9,32 @@ const git = require('../../lib/git.cjs');
9
9
  const { commitTask, findCommitByTaskId } = git;
10
10
  const { deleteCheckpoint, readCheckpoint } = require('../../lib/checkpoint.cjs');
11
11
 
12
+ const BYPASS_FLAG = '--bypass-nubosloop';
13
+
14
+ function _assertLoopGate(taskId, cwd, bypass, stderr) {
15
+ const cp = readCheckpoint(taskId, cwd);
16
+ const last = cp && cp.nubosloop && cp.nubosloop.last_phase;
17
+ if (last === 'commit') return { bypassed: false, last_phase: last };
18
+ const reason = !cp ? 'no-checkpoint' : 'last-phase-mismatch';
19
+ const observed = last || (cp ? 'none' : 'no-checkpoint');
20
+ if (bypass) {
21
+ stderr.write(
22
+ '[nubos-pilot] WARNING: commit-task ' + taskId +
23
+ ' bypassing Nubosloop gate (' + BYPASS_FLAG + '; observed=' + observed +
24
+ '). Single-pass commit, no critic review enforced.\n',
25
+ );
26
+ return { bypassed: true, last_phase: last || null };
27
+ }
28
+ throw new NubosPilotError(
29
+ 'commit-task-loop-bypass-violation',
30
+ 'commit-task refused: Nubosloop did not reach phase=commit for ' + taskId +
31
+ ' (observed nubosloop.last_phase=' + observed + '). ' +
32
+ 'Run `loop-run-round ' + taskId + ' --phase commit` first, or pass ' + BYPASS_FLAG +
33
+ ' for an explicit single-pass override.',
34
+ { taskId, reason, last_phase: last || null },
35
+ );
36
+ }
37
+
12
38
  function _resolveTaskFile(taskId, cwd) {
13
39
  const parsed = layout.parseTaskFullId(taskId);
14
40
  const filePath = layout.taskPlanPath(parsed.milestone, parsed.slice, parsed.task, cwd);
@@ -49,8 +75,11 @@ function run(args, ctx) {
49
75
  const context = ctx || {};
50
76
  const cwd = context.cwd || process.cwd();
51
77
  const stdout = context.stdout || process.stdout;
78
+ const stderr = context.stderr || process.stderr;
52
79
  const list = Array.isArray(args) ? args : [];
53
- const taskId = list[0];
80
+ const bypass = list.includes(BYPASS_FLAG);
81
+ const positional = list.filter((a) => !String(a).startsWith('--'));
82
+ const taskId = positional[0];
54
83
 
55
84
  if (!taskId) {
56
85
  throw new NubosPilotError(
@@ -67,6 +96,8 @@ function run(args, ctx) {
67
96
  );
68
97
  }
69
98
 
99
+ const gate = _assertLoopGate(taskId, cwd, bypass, stderr);
100
+
70
101
  const { filePath } = _resolveTaskFile(taskId, cwd);
71
102
  const raw = fs.readFileSync(filePath, 'utf-8');
72
103
  const { frontmatter, body } = extractFrontmatter(raw);
@@ -93,7 +124,7 @@ function run(args, ctx) {
93
124
  const name = _extractName(frontmatter, body);
94
125
  const message = 'task(' + taskId + '): ' + name;
95
126
 
96
-
127
+
97
128
 
98
129
  commitTask(taskId, safeFiles, message);
99
130
  const sha = findCommitByTaskId(taskId);
@@ -103,7 +134,14 @@ function run(args, ctx) {
103
134
  process.stderr.write('[nubos-pilot warn] setTaskStatus failed for ' + taskId + ': ' + (err && err.message) + '\n');
104
135
  }
105
136
 
106
- const payload = { ok: true, task_id: taskId, sha, files: safeFiles, files_source: filesSource };
137
+ const payload = {
138
+ ok: true,
139
+ task_id: taskId,
140
+ sha,
141
+ files: safeFiles,
142
+ files_source: filesSource,
143
+ nubosloop_bypassed: gate.bypassed,
144
+ };
107
145
  stdout.write(JSON.stringify(payload));
108
146
  return payload;
109
147
  }
@@ -82,6 +82,27 @@ function _capture() {
82
82
  return { stub, get: () => buf };
83
83
  }
84
84
 
85
+ // Seed a checkpoint that satisfies the Nubosloop gate (last_phase=commit) so
86
+ // commit-task accepts it. Tests that exercise the gate explicitly bypass this
87
+ // helper. Optional `extra` overrides any field on the envelope.
88
+ function seedLoopReadyCheckpoint(root, taskId, extra) {
89
+ const cpPath = path.join(root, '.nubos-pilot', 'checkpoints', taskId + '.json');
90
+ fs.mkdirSync(path.dirname(cpPath), { recursive: true });
91
+ const base = {
92
+ schema_version: 1,
93
+ task_id: taskId,
94
+ status: 'pre-commit',
95
+ files_touched: [],
96
+ nubosloop: {
97
+ last_phase: 'commit',
98
+ last_action: 'commit',
99
+ committed_at: '2026-05-04T12:00:00Z',
100
+ },
101
+ };
102
+ fs.writeFileSync(cpPath, JSON.stringify(Object.assign(base, extra || {})), 'utf-8');
103
+ return cpPath;
104
+ }
105
+
85
106
  after(() => {
86
107
  while (_repos.length) {
87
108
  const r = _repos.pop();
@@ -110,6 +131,7 @@ test('CT-2: commit-task rejects invalid TASK_ID format (defense-in-depth)', () =
110
131
  test('CT-3: commit-task emits JSON with sha + files on success', () => {
111
132
  const root = makeRepo();
112
133
  seedPlanAndTask(root, '06-01', 'M006-S001-T0001', ['src/a.ts']);
134
+ seedLoopReadyCheckpoint(root, 'M006-S001-T0001');
113
135
 
114
136
  fs.mkdirSync(path.join(root, 'src'), { recursive: true });
115
137
  fs.writeFileSync(path.join(root, 'src', 'a.ts'), 'export const a = 1;\n', 'utf-8');
@@ -126,6 +148,7 @@ test('CT-3: commit-task emits JSON with sha + files on success', () => {
126
148
  assert.equal(payload.task_id, 'M006-S001-T0001');
127
149
  assert.ok(/^[0-9a-f]{40}$/.test(payload.sha));
128
150
  assert.deepEqual(payload.files, ['src/a.ts']);
151
+ assert.equal(payload.nubosloop_bypassed, false);
129
152
 
130
153
  const subject = execFileSync('git', ['-C', root, 'log', '-n', '1', '--format=%s'], { encoding: 'utf-8' }).trim();
131
154
  assert.ok(subject.startsWith('task(M006-S001-T0001):'), 'subject: ' + subject);
@@ -134,6 +157,7 @@ test('CT-3: commit-task emits JSON with sha + files on success', () => {
134
157
  test('CT-4: commit-task LOUD-FAILS when every files_modified entry is gitignored (D-25)', () => {
135
158
  const root = makeRepo();
136
159
  seedPlanAndTask(root, '06-01', 'M006-S001-T0002', ['build/out.js']);
160
+ seedLoopReadyCheckpoint(root, 'M006-S001-T0002');
137
161
  fs.writeFileSync(path.join(root, '.gitignore'), 'build/\n', 'utf-8');
138
162
  fs.mkdirSync(path.join(root, 'build'), { recursive: true });
139
163
  fs.writeFileSync(path.join(root, 'build', 'out.js'), 'noise', 'utf-8');
@@ -152,9 +176,13 @@ test('CT-4: commit-task LOUD-FAILS when every files_modified entry is gitignored
152
176
 
153
177
  test('CT-5: commit-task unknown task id → task-not-found', () => {
154
178
  const root = makeRepo();
179
+ // Loop gate runs BEFORE task lookup (no checkpoint seeded), so we expect
180
+ // the bypass-violation here, not the task-not-found error. Using --bypass
181
+ // so we exercise the unknown-task path instead.
155
182
  const cap = _capture();
183
+ const stderr = _capture();
156
184
  assert.throws(
157
- () => subcmd.run(['M006-S099-T0099'], { cwd: root, stdout: cap.stub }),
185
+ () => subcmd.run(['M006-S099-T0099', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
158
186
  (err) => err && err.code === 'commit-task-not-found',
159
187
  );
160
188
  });
@@ -164,14 +192,8 @@ test('CT-6: empty files_modified falls back to checkpoint.files_touched', () =>
164
192
  seedPlanAndTask(root, '06-01', 'M006-S001-T0010', []);
165
193
  fs.mkdirSync(path.join(root, 'src'), { recursive: true });
166
194
  fs.writeFileSync(path.join(root, 'src', 'b.ts'), 'export const b = 2;\n', 'utf-8');
167
- const cpPath = path.join(root, '.nubos-pilot', 'checkpoints', 'M006-S001-T0010.json');
168
- fs.mkdirSync(path.dirname(cpPath), { recursive: true });
169
- fs.writeFileSync(cpPath, JSON.stringify({
170
- schema_version: 1,
171
- task_id: 'M006-S001-T0010',
172
- status: 'pre-commit',
173
- files_touched: ['src/b.ts'],
174
- }), 'utf-8');
195
+ // Checkpoint must satisfy the loop gate AND carry files_touched.
196
+ seedLoopReadyCheckpoint(root, 'M006-S001-T0010', { files_touched: ['src/b.ts'] });
175
197
  const prev = process.cwd();
176
198
  process.chdir(root);
177
199
  const cap = _capture();
@@ -189,9 +211,88 @@ test('CT-6: empty files_modified falls back to checkpoint.files_touched', () =>
189
211
  test('CT-7: empty files_modified AND no checkpoint → commit-task-no-files', () => {
190
212
  const root = makeRepo();
191
213
  seedPlanAndTask(root, '06-01', 'M006-S001-T0011', []);
214
+ // No checkpoint → gate would normally refuse first; bypass to reach the
215
+ // no-files path that this test exercises.
192
216
  const cap = _capture();
217
+ const stderr = _capture();
193
218
  assert.throws(
194
- () => subcmd.run(['M006-S001-T0011'], { cwd: root, stdout: cap.stub }),
219
+ () => subcmd.run(['M006-S001-T0011', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
195
220
  (err) => err && err.code === 'commit-task-no-files',
196
221
  );
197
222
  });
223
+
224
+ test('CT-8: refuse commit when no checkpoint exists (Nubosloop gate)', () => {
225
+ const root = makeRepo();
226
+ seedPlanAndTask(root, '06-01', 'M006-S001-T0020', ['src/c.ts']);
227
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
228
+ fs.writeFileSync(path.join(root, 'src', 'c.ts'), 'export const c = 3;\n', 'utf-8');
229
+ const cap = _capture();
230
+ const stderr = _capture();
231
+ assert.throws(
232
+ () => subcmd.run(['M006-S001-T0020'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
233
+ (err) => err && err.code === 'commit-task-loop-bypass-violation' && err.details && err.details.reason === 'no-checkpoint',
234
+ );
235
+ });
236
+
237
+ test('CT-9: refuse commit when nubosloop.last_phase ≠ commit', () => {
238
+ const root = makeRepo();
239
+ seedPlanAndTask(root, '06-01', 'M006-S001-T0021', ['src/d.ts']);
240
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
241
+ fs.writeFileSync(path.join(root, 'src', 'd.ts'), 'export const d = 4;\n', 'utf-8');
242
+ // Checkpoint exists but loop only made it to verifying — gate must refuse.
243
+ seedLoopReadyCheckpoint(root, 'M006-S001-T0021', {
244
+ nubosloop: { last_phase: 'verifying', last_action: 'verify-green' },
245
+ });
246
+ const cap = _capture();
247
+ const stderr = _capture();
248
+ assert.throws(
249
+ () => subcmd.run(['M006-S001-T0021'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
250
+ (err) => err && err.code === 'commit-task-loop-bypass-violation'
251
+ && err.details && err.details.reason === 'last-phase-mismatch'
252
+ && err.details.last_phase === 'verifying',
253
+ );
254
+ });
255
+
256
+ test('CT-10: --bypass-nubosloop allows single-pass commit and warns on stderr', () => {
257
+ const root = makeRepo();
258
+ seedPlanAndTask(root, '06-01', 'M006-S001-T0022', ['src/e.ts']);
259
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
260
+ fs.writeFileSync(path.join(root, 'src', 'e.ts'), 'export const e = 5;\n', 'utf-8');
261
+ const prev = process.cwd();
262
+ process.chdir(root);
263
+ const cap = _capture();
264
+ const stderr = _capture();
265
+ try {
266
+ subcmd.run(['M006-S001-T0022', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub });
267
+ } finally {
268
+ process.chdir(prev);
269
+ }
270
+ const payload = JSON.parse(cap.get());
271
+ assert.equal(payload.ok, true);
272
+ assert.equal(payload.nubosloop_bypassed, true);
273
+ assert.match(stderr.get(), /WARNING: commit-task M006-S001-T0022 bypassing Nubosloop gate/);
274
+ assert.match(stderr.get(), /observed=no-checkpoint/);
275
+ });
276
+
277
+ test('CT-11: --bypass-nubosloop on partial loop state stamps the bypass reason', () => {
278
+ const root = makeRepo();
279
+ seedPlanAndTask(root, '06-01', 'M006-S001-T0023', ['src/f.ts']);
280
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
281
+ fs.writeFileSync(path.join(root, 'src', 'f.ts'), 'export const f = 6;\n', 'utf-8');
282
+ seedLoopReadyCheckpoint(root, 'M006-S001-T0023', {
283
+ nubosloop: { last_phase: 'post-critics', last_action: 'executor' },
284
+ });
285
+ const prev = process.cwd();
286
+ process.chdir(root);
287
+ const cap = _capture();
288
+ const stderr = _capture();
289
+ try {
290
+ subcmd.run(['M006-S001-T0023', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub });
291
+ } finally {
292
+ process.chdir(prev);
293
+ }
294
+ const payload = JSON.parse(cap.get());
295
+ assert.equal(payload.ok, true);
296
+ assert.equal(payload.nubosloop_bypassed, true);
297
+ assert.match(stderr.get(), /observed=post-critics/);
298
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "AI-driven planning and execution tool for code projects",
5
5
  "homepage": "https://github.com/Nubos-AI/nubos-pilot",
6
6
  "repository": {