nubos-pilot 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -9,6 +9,58 @@ 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
|
+
// Evidence-based gate: a complete Nubosloop run accumulates fields on the
|
|
15
|
+
// checkpoint envelope (cache_hit from preflight, verify_exit_code from
|
|
16
|
+
// post-executor, findings from post-critics, committed_at from commit). A
|
|
17
|
+
// gamed run that only invokes `loop-run-round --phase commit` directly leaves
|
|
18
|
+
// verify_exit_code and findings undefined. Checking last_phase alone is not
|
|
19
|
+
// enough — we require the cumulative signature.
|
|
20
|
+
function _assertLoopGate(taskId, cwd, bypass, stderr) {
|
|
21
|
+
const cp = readCheckpoint(taskId, cwd);
|
|
22
|
+
const np = (cp && cp.nubosloop) || null;
|
|
23
|
+
const last = np && np.last_phase;
|
|
24
|
+
const checks = [
|
|
25
|
+
{ ok: !!cp, reason: 'no-checkpoint', missing: 'checkpoint', observed: 'no-checkpoint' },
|
|
26
|
+
{ ok: last === 'commit', reason: 'last-phase-mismatch', missing: 'last_phase=commit', observed: last || 'none' },
|
|
27
|
+
{ ok: np && np.verify_exit_code === 0, reason: 'post-executor-not-green', missing: 'verify_exit_code=0', observed: np && np.verify_exit_code !== undefined ? String(np.verify_exit_code) : 'undefined' },
|
|
28
|
+
{ ok: np && Array.isArray(np.findings), reason: 'post-critics-missing', missing: 'findings (array)', observed: np && np.findings !== undefined ? JSON.stringify(np.findings).slice(0, 60) : 'undefined' },
|
|
29
|
+
{ ok: np && !!np.committed_at, reason: 'commit-phase-not-stamped', missing: 'committed_at', observed: (np && np.committed_at) || 'undefined' },
|
|
30
|
+
];
|
|
31
|
+
const failed = checks.find((c) => !c.ok);
|
|
32
|
+
if (!failed) {
|
|
33
|
+
return { bypassed: false, last_phase: last, forced_commit_phase: !!(np && np.forced_commit_phase) };
|
|
34
|
+
}
|
|
35
|
+
if (bypass) {
|
|
36
|
+
stderr.write(
|
|
37
|
+
'[nubos-pilot] WARNING: commit-task ' + taskId +
|
|
38
|
+
' bypassing Nubosloop gate (' + BYPASS_FLAG +
|
|
39
|
+
'; reason=' + failed.reason + '; missing=' + failed.missing +
|
|
40
|
+
'; observed=' + failed.observed +
|
|
41
|
+
'). Single-pass commit, no critic review enforced.\n',
|
|
42
|
+
);
|
|
43
|
+
return { bypassed: true, last_phase: last || null, forced_commit_phase: !!(np && np.forced_commit_phase) };
|
|
44
|
+
}
|
|
45
|
+
throw new NubosPilotError(
|
|
46
|
+
'commit-task-loop-bypass-violation',
|
|
47
|
+
'commit-task refused: Nubosloop sequence incomplete for ' + taskId +
|
|
48
|
+
' (reason=' + failed.reason + '; missing=' + failed.missing +
|
|
49
|
+
'; observed=' + failed.observed + '). ' +
|
|
50
|
+
'Run the full loop (preflight → post-executor verify-green → post-critics → commit) first, or pass ' + BYPASS_FLAG +
|
|
51
|
+
' for an explicit single-pass override.',
|
|
52
|
+
{
|
|
53
|
+
taskId,
|
|
54
|
+
reason: failed.reason,
|
|
55
|
+
missing: failed.missing,
|
|
56
|
+
observed_last_phase: last || null,
|
|
57
|
+
observed_verify_exit_code: np && np.verify_exit_code !== undefined ? np.verify_exit_code : null,
|
|
58
|
+
observed_findings_is_array: !!(np && Array.isArray(np.findings)),
|
|
59
|
+
observed_committed_at: (np && np.committed_at) || null,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
12
64
|
function _resolveTaskFile(taskId, cwd) {
|
|
13
65
|
const parsed = layout.parseTaskFullId(taskId);
|
|
14
66
|
const filePath = layout.taskPlanPath(parsed.milestone, parsed.slice, parsed.task, cwd);
|
|
@@ -49,8 +101,11 @@ function run(args, ctx) {
|
|
|
49
101
|
const context = ctx || {};
|
|
50
102
|
const cwd = context.cwd || process.cwd();
|
|
51
103
|
const stdout = context.stdout || process.stdout;
|
|
104
|
+
const stderr = context.stderr || process.stderr;
|
|
52
105
|
const list = Array.isArray(args) ? args : [];
|
|
53
|
-
const
|
|
106
|
+
const bypass = list.includes(BYPASS_FLAG);
|
|
107
|
+
const positional = list.filter((a) => !String(a).startsWith('--'));
|
|
108
|
+
const taskId = positional[0];
|
|
54
109
|
|
|
55
110
|
if (!taskId) {
|
|
56
111
|
throw new NubosPilotError(
|
|
@@ -67,6 +122,8 @@ function run(args, ctx) {
|
|
|
67
122
|
);
|
|
68
123
|
}
|
|
69
124
|
|
|
125
|
+
const gate = _assertLoopGate(taskId, cwd, bypass, stderr);
|
|
126
|
+
|
|
70
127
|
const { filePath } = _resolveTaskFile(taskId, cwd);
|
|
71
128
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
72
129
|
const { frontmatter, body } = extractFrontmatter(raw);
|
|
@@ -93,7 +150,7 @@ function run(args, ctx) {
|
|
|
93
150
|
const name = _extractName(frontmatter, body);
|
|
94
151
|
const message = 'task(' + taskId + '): ' + name;
|
|
95
152
|
|
|
96
|
-
|
|
153
|
+
|
|
97
154
|
|
|
98
155
|
commitTask(taskId, safeFiles, message);
|
|
99
156
|
const sha = findCommitByTaskId(taskId);
|
|
@@ -103,7 +160,15 @@ function run(args, ctx) {
|
|
|
103
160
|
process.stderr.write('[nubos-pilot warn] setTaskStatus failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
104
161
|
}
|
|
105
162
|
|
|
106
|
-
const payload = {
|
|
163
|
+
const payload = {
|
|
164
|
+
ok: true,
|
|
165
|
+
task_id: taskId,
|
|
166
|
+
sha,
|
|
167
|
+
files: safeFiles,
|
|
168
|
+
files_source: filesSource,
|
|
169
|
+
nubosloop_bypassed: gate.bypassed,
|
|
170
|
+
nubosloop_forced_commit_phase: !!gate.forced_commit_phase,
|
|
171
|
+
};
|
|
107
172
|
stdout.write(JSON.stringify(payload));
|
|
108
173
|
return payload;
|
|
109
174
|
}
|
|
@@ -82,6 +82,37 @@ function _capture() {
|
|
|
82
82
|
return { stub, get: () => buf };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Seed a checkpoint that satisfies the full Nubosloop gate (sequence-integrity).
|
|
86
|
+
// A real loop accumulates evidence on the envelope; the gate refuses unless
|
|
87
|
+
// every required marker is present. Tests that exercise game-paths build their
|
|
88
|
+
// own partial fixtures.
|
|
89
|
+
function seedLoopReadyCheckpoint(root, taskId, extra) {
|
|
90
|
+
const cpPath = path.join(root, '.nubos-pilot', 'checkpoints', taskId + '.json');
|
|
91
|
+
fs.mkdirSync(path.dirname(cpPath), { recursive: true });
|
|
92
|
+
const base = {
|
|
93
|
+
schema_version: 1,
|
|
94
|
+
task_id: taskId,
|
|
95
|
+
status: 'pre-commit',
|
|
96
|
+
files_touched: [],
|
|
97
|
+
nubosloop: {
|
|
98
|
+
round: 1,
|
|
99
|
+
cache_hit: false,
|
|
100
|
+
last_phase: 'commit',
|
|
101
|
+
last_action: 'commit',
|
|
102
|
+
verify_exit_code: 0,
|
|
103
|
+
findings: [],
|
|
104
|
+
committed_at: '2026-05-04T12:00:00Z',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
// Allow the test to override individual fields (incl. nubosloop sub-fields).
|
|
108
|
+
const merged = Object.assign({}, base, extra || {});
|
|
109
|
+
if (extra && extra.nubosloop) {
|
|
110
|
+
merged.nubosloop = Object.assign({}, base.nubosloop, extra.nubosloop);
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(cpPath, JSON.stringify(merged), 'utf-8');
|
|
113
|
+
return cpPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
85
116
|
after(() => {
|
|
86
117
|
while (_repos.length) {
|
|
87
118
|
const r = _repos.pop();
|
|
@@ -110,6 +141,7 @@ test('CT-2: commit-task rejects invalid TASK_ID format (defense-in-depth)', () =
|
|
|
110
141
|
test('CT-3: commit-task emits JSON with sha + files on success', () => {
|
|
111
142
|
const root = makeRepo();
|
|
112
143
|
seedPlanAndTask(root, '06-01', 'M006-S001-T0001', ['src/a.ts']);
|
|
144
|
+
seedLoopReadyCheckpoint(root, 'M006-S001-T0001');
|
|
113
145
|
|
|
114
146
|
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
115
147
|
fs.writeFileSync(path.join(root, 'src', 'a.ts'), 'export const a = 1;\n', 'utf-8');
|
|
@@ -126,6 +158,7 @@ test('CT-3: commit-task emits JSON with sha + files on success', () => {
|
|
|
126
158
|
assert.equal(payload.task_id, 'M006-S001-T0001');
|
|
127
159
|
assert.ok(/^[0-9a-f]{40}$/.test(payload.sha));
|
|
128
160
|
assert.deepEqual(payload.files, ['src/a.ts']);
|
|
161
|
+
assert.equal(payload.nubosloop_bypassed, false);
|
|
129
162
|
|
|
130
163
|
const subject = execFileSync('git', ['-C', root, 'log', '-n', '1', '--format=%s'], { encoding: 'utf-8' }).trim();
|
|
131
164
|
assert.ok(subject.startsWith('task(M006-S001-T0001):'), 'subject: ' + subject);
|
|
@@ -134,6 +167,7 @@ test('CT-3: commit-task emits JSON with sha + files on success', () => {
|
|
|
134
167
|
test('CT-4: commit-task LOUD-FAILS when every files_modified entry is gitignored (D-25)', () => {
|
|
135
168
|
const root = makeRepo();
|
|
136
169
|
seedPlanAndTask(root, '06-01', 'M006-S001-T0002', ['build/out.js']);
|
|
170
|
+
seedLoopReadyCheckpoint(root, 'M006-S001-T0002');
|
|
137
171
|
fs.writeFileSync(path.join(root, '.gitignore'), 'build/\n', 'utf-8');
|
|
138
172
|
fs.mkdirSync(path.join(root, 'build'), { recursive: true });
|
|
139
173
|
fs.writeFileSync(path.join(root, 'build', 'out.js'), 'noise', 'utf-8');
|
|
@@ -152,9 +186,13 @@ test('CT-4: commit-task LOUD-FAILS when every files_modified entry is gitignored
|
|
|
152
186
|
|
|
153
187
|
test('CT-5: commit-task unknown task id → task-not-found', () => {
|
|
154
188
|
const root = makeRepo();
|
|
189
|
+
// Loop gate runs BEFORE task lookup (no checkpoint seeded), so we expect
|
|
190
|
+
// the bypass-violation here, not the task-not-found error. Using --bypass
|
|
191
|
+
// so we exercise the unknown-task path instead.
|
|
155
192
|
const cap = _capture();
|
|
193
|
+
const stderr = _capture();
|
|
156
194
|
assert.throws(
|
|
157
|
-
() => subcmd.run(['M006-S099-T0099'], { cwd: root, stdout: cap.stub }),
|
|
195
|
+
() => subcmd.run(['M006-S099-T0099', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
|
|
158
196
|
(err) => err && err.code === 'commit-task-not-found',
|
|
159
197
|
);
|
|
160
198
|
});
|
|
@@ -164,14 +202,8 @@ test('CT-6: empty files_modified falls back to checkpoint.files_touched', () =>
|
|
|
164
202
|
seedPlanAndTask(root, '06-01', 'M006-S001-T0010', []);
|
|
165
203
|
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
166
204
|
fs.writeFileSync(path.join(root, 'src', 'b.ts'), 'export const b = 2;\n', 'utf-8');
|
|
167
|
-
|
|
168
|
-
|
|
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');
|
|
205
|
+
// Checkpoint must satisfy the loop gate AND carry files_touched.
|
|
206
|
+
seedLoopReadyCheckpoint(root, 'M006-S001-T0010', { files_touched: ['src/b.ts'] });
|
|
175
207
|
const prev = process.cwd();
|
|
176
208
|
process.chdir(root);
|
|
177
209
|
const cap = _capture();
|
|
@@ -189,9 +221,190 @@ test('CT-6: empty files_modified falls back to checkpoint.files_touched', () =>
|
|
|
189
221
|
test('CT-7: empty files_modified AND no checkpoint → commit-task-no-files', () => {
|
|
190
222
|
const root = makeRepo();
|
|
191
223
|
seedPlanAndTask(root, '06-01', 'M006-S001-T0011', []);
|
|
224
|
+
// No checkpoint → gate would normally refuse first; bypass to reach the
|
|
225
|
+
// no-files path that this test exercises.
|
|
192
226
|
const cap = _capture();
|
|
227
|
+
const stderr = _capture();
|
|
193
228
|
assert.throws(
|
|
194
|
-
() => subcmd.run(['M006-S001-T0011'], { cwd: root, stdout: cap.stub }),
|
|
229
|
+
() => subcmd.run(['M006-S001-T0011', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
|
|
195
230
|
(err) => err && err.code === 'commit-task-no-files',
|
|
196
231
|
);
|
|
197
232
|
});
|
|
233
|
+
|
|
234
|
+
test('CT-8: refuse commit when no checkpoint exists (Nubosloop gate)', () => {
|
|
235
|
+
const root = makeRepo();
|
|
236
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0020', ['src/c.ts']);
|
|
237
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
238
|
+
fs.writeFileSync(path.join(root, 'src', 'c.ts'), 'export const c = 3;\n', 'utf-8');
|
|
239
|
+
const cap = _capture();
|
|
240
|
+
const stderr = _capture();
|
|
241
|
+
assert.throws(
|
|
242
|
+
() => subcmd.run(['M006-S001-T0020'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
|
|
243
|
+
(err) => err && err.code === 'commit-task-loop-bypass-violation' && err.details && err.details.reason === 'no-checkpoint',
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('CT-9: refuse commit when nubosloop.last_phase ≠ commit', () => {
|
|
248
|
+
const root = makeRepo();
|
|
249
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0021', ['src/d.ts']);
|
|
250
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
251
|
+
fs.writeFileSync(path.join(root, 'src', 'd.ts'), 'export const d = 4;\n', 'utf-8');
|
|
252
|
+
// Checkpoint exists but loop only made it to verifying — gate must refuse.
|
|
253
|
+
seedLoopReadyCheckpoint(root, 'M006-S001-T0021', {
|
|
254
|
+
nubosloop: { last_phase: 'verifying', last_action: 'verify-green' },
|
|
255
|
+
});
|
|
256
|
+
const cap = _capture();
|
|
257
|
+
const stderr = _capture();
|
|
258
|
+
assert.throws(
|
|
259
|
+
() => subcmd.run(['M006-S001-T0021'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
|
|
260
|
+
(err) => err && err.code === 'commit-task-loop-bypass-violation'
|
|
261
|
+
&& err.details && err.details.reason === 'last-phase-mismatch'
|
|
262
|
+
&& err.details.observed_last_phase === 'verifying',
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('CT-12: refuse gamed commit (last_phase=commit but no verify_exit_code)', () => {
|
|
267
|
+
const root = makeRepo();
|
|
268
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0030', ['src/g.ts']);
|
|
269
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
270
|
+
fs.writeFileSync(path.join(root, 'src', 'g.ts'), 'export const g = 7;\n', 'utf-8');
|
|
271
|
+
// Simulates an agent that ran ONLY `loop-run-round --phase commit` to game
|
|
272
|
+
// the gate, without going through preflight/post-executor/post-critics.
|
|
273
|
+
// verify_exit_code is undefined → post-executor never ran.
|
|
274
|
+
const cpPath = path.join(root, '.nubos-pilot', 'checkpoints', 'M006-S001-T0030.json');
|
|
275
|
+
fs.mkdirSync(path.dirname(cpPath), { recursive: true });
|
|
276
|
+
fs.writeFileSync(cpPath, JSON.stringify({
|
|
277
|
+
schema_version: 1,
|
|
278
|
+
task_id: 'M006-S001-T0030',
|
|
279
|
+
status: 'pre-commit',
|
|
280
|
+
files_touched: [],
|
|
281
|
+
nubosloop: { last_phase: 'commit', last_action: 'commit', committed_at: '2026-05-04T12:00:00Z' },
|
|
282
|
+
}), 'utf-8');
|
|
283
|
+
const cap = _capture();
|
|
284
|
+
const stderr = _capture();
|
|
285
|
+
assert.throws(
|
|
286
|
+
() => subcmd.run(['M006-S001-T0030'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
|
|
287
|
+
(err) => err && err.code === 'commit-task-loop-bypass-violation'
|
|
288
|
+
&& err.details && err.details.reason === 'post-executor-not-green',
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('CT-13: refuse gamed commit when verify ran but post-critics findings missing', () => {
|
|
293
|
+
const root = makeRepo();
|
|
294
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0031', ['src/h.ts']);
|
|
295
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
296
|
+
fs.writeFileSync(path.join(root, 'src', 'h.ts'), 'export const h = 8;\n', 'utf-8');
|
|
297
|
+
// verify ran (exit_code=0) but critics never produced findings — agent
|
|
298
|
+
// skipped the critic-schwarm step.
|
|
299
|
+
const cpPath = path.join(root, '.nubos-pilot', 'checkpoints', 'M006-S001-T0031.json');
|
|
300
|
+
fs.mkdirSync(path.dirname(cpPath), { recursive: true });
|
|
301
|
+
fs.writeFileSync(cpPath, JSON.stringify({
|
|
302
|
+
schema_version: 1,
|
|
303
|
+
task_id: 'M006-S001-T0031',
|
|
304
|
+
status: 'pre-commit',
|
|
305
|
+
files_touched: [],
|
|
306
|
+
nubosloop: {
|
|
307
|
+
last_phase: 'commit', last_action: 'commit',
|
|
308
|
+
verify_exit_code: 0, // post-executor ran
|
|
309
|
+
committed_at: '2026-05-04T12:00:00Z',
|
|
310
|
+
// findings: missing → post-critics never ran
|
|
311
|
+
},
|
|
312
|
+
}), 'utf-8');
|
|
313
|
+
const cap = _capture();
|
|
314
|
+
const stderr = _capture();
|
|
315
|
+
assert.throws(
|
|
316
|
+
() => subcmd.run(['M006-S001-T0031'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
|
|
317
|
+
(err) => err && err.code === 'commit-task-loop-bypass-violation'
|
|
318
|
+
&& err.details && err.details.reason === 'post-critics-missing',
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('CT-14: refuse when verify-red was recorded (post-executor failed)', () => {
|
|
323
|
+
const root = makeRepo();
|
|
324
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0032', ['src/i.ts']);
|
|
325
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
326
|
+
fs.writeFileSync(path.join(root, 'src', 'i.ts'), 'export const i = 9;\n', 'utf-8');
|
|
327
|
+
// Loop reached commit-stamp somehow but verify was red — must refuse.
|
|
328
|
+
seedLoopReadyCheckpoint(root, 'M006-S001-T0032', {
|
|
329
|
+
nubosloop: { verify_exit_code: 1 },
|
|
330
|
+
});
|
|
331
|
+
const cap = _capture();
|
|
332
|
+
const stderr = _capture();
|
|
333
|
+
assert.throws(
|
|
334
|
+
() => subcmd.run(['M006-S001-T0032'], { cwd: root, stdout: cap.stub, stderr: stderr.stub }),
|
|
335
|
+
(err) => err && err.code === 'commit-task-loop-bypass-violation'
|
|
336
|
+
&& err.details && err.details.reason === 'post-executor-not-green'
|
|
337
|
+
&& err.details.observed_verify_exit_code === 1,
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('CT-15: bypass on gamed commit logs precise reason in stderr', () => {
|
|
342
|
+
const root = makeRepo();
|
|
343
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0033', ['src/j.ts']);
|
|
344
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
345
|
+
fs.writeFileSync(path.join(root, 'src', 'j.ts'), 'export const j = 10;\n', 'utf-8');
|
|
346
|
+
const cpPath = path.join(root, '.nubos-pilot', 'checkpoints', 'M006-S001-T0033.json');
|
|
347
|
+
fs.mkdirSync(path.dirname(cpPath), { recursive: true });
|
|
348
|
+
fs.writeFileSync(cpPath, JSON.stringify({
|
|
349
|
+
schema_version: 1, task_id: 'M006-S001-T0033', status: 'pre-commit', files_touched: [],
|
|
350
|
+
nubosloop: { last_phase: 'commit', last_action: 'commit', committed_at: 'z' },
|
|
351
|
+
}), 'utf-8');
|
|
352
|
+
const prev = process.cwd();
|
|
353
|
+
process.chdir(root);
|
|
354
|
+
const cap = _capture();
|
|
355
|
+
const stderr = _capture();
|
|
356
|
+
try {
|
|
357
|
+
subcmd.run(['M006-S001-T0033', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub });
|
|
358
|
+
} finally {
|
|
359
|
+
process.chdir(prev);
|
|
360
|
+
}
|
|
361
|
+
const payload = JSON.parse(cap.get());
|
|
362
|
+
assert.equal(payload.ok, true);
|
|
363
|
+
assert.equal(payload.nubosloop_bypassed, true);
|
|
364
|
+
assert.match(stderr.get(), /reason=post-executor-not-green/);
|
|
365
|
+
assert.match(stderr.get(), /missing=verify_exit_code=0/);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('CT-10: --bypass-nubosloop allows single-pass commit and warns on stderr', () => {
|
|
369
|
+
const root = makeRepo();
|
|
370
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0022', ['src/e.ts']);
|
|
371
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
372
|
+
fs.writeFileSync(path.join(root, 'src', 'e.ts'), 'export const e = 5;\n', 'utf-8');
|
|
373
|
+
const prev = process.cwd();
|
|
374
|
+
process.chdir(root);
|
|
375
|
+
const cap = _capture();
|
|
376
|
+
const stderr = _capture();
|
|
377
|
+
try {
|
|
378
|
+
subcmd.run(['M006-S001-T0022', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub });
|
|
379
|
+
} finally {
|
|
380
|
+
process.chdir(prev);
|
|
381
|
+
}
|
|
382
|
+
const payload = JSON.parse(cap.get());
|
|
383
|
+
assert.equal(payload.ok, true);
|
|
384
|
+
assert.equal(payload.nubosloop_bypassed, true);
|
|
385
|
+
assert.match(stderr.get(), /WARNING: commit-task M006-S001-T0022 bypassing Nubosloop gate/);
|
|
386
|
+
assert.match(stderr.get(), /observed=no-checkpoint/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('CT-11: --bypass-nubosloop on partial loop state stamps the bypass reason', () => {
|
|
390
|
+
const root = makeRepo();
|
|
391
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0023', ['src/f.ts']);
|
|
392
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
393
|
+
fs.writeFileSync(path.join(root, 'src', 'f.ts'), 'export const f = 6;\n', 'utf-8');
|
|
394
|
+
seedLoopReadyCheckpoint(root, 'M006-S001-T0023', {
|
|
395
|
+
nubosloop: { last_phase: 'post-critics', last_action: 'executor' },
|
|
396
|
+
});
|
|
397
|
+
const prev = process.cwd();
|
|
398
|
+
process.chdir(root);
|
|
399
|
+
const cap = _capture();
|
|
400
|
+
const stderr = _capture();
|
|
401
|
+
try {
|
|
402
|
+
subcmd.run(['M006-S001-T0023', '--bypass-nubosloop'], { cwd: root, stdout: cap.stub, stderr: stderr.stub });
|
|
403
|
+
} finally {
|
|
404
|
+
process.chdir(prev);
|
|
405
|
+
}
|
|
406
|
+
const payload = JSON.parse(cap.get());
|
|
407
|
+
assert.equal(payload.ok, true);
|
|
408
|
+
assert.equal(payload.nubosloop_bypassed, true);
|
|
409
|
+
assert.match(stderr.get(), /observed=post-critics/);
|
|
410
|
+
});
|
|
@@ -467,6 +467,79 @@ test('LCLI-RR-7: loop-run-round rejects unknown --phase', () => {
|
|
|
467
467
|
);
|
|
468
468
|
});
|
|
469
469
|
|
|
470
|
+
test('LCLI-RR-8: phase=commit refuses without verify_exit_code (post-executor never ran)', () => {
|
|
471
|
+
const r = _mkRoot();
|
|
472
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
473
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
474
|
+
assert.throws(
|
|
475
|
+
() => loopRunRound.run(
|
|
476
|
+
['M001-S001-T0001', '--phase', 'commit'],
|
|
477
|
+
{ cwd: r, stdout: _cap().stub },
|
|
478
|
+
),
|
|
479
|
+
(err) => err && err.code === 'loop-commit-precondition-missing'
|
|
480
|
+
&& err.details && err.details.missing === 'verify_exit_code',
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('LCLI-RR-9: phase=commit refuses without findings (post-critics never ran)', () => {
|
|
485
|
+
const r = _mkRoot();
|
|
486
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
487
|
+
const nubosloop = require('../../lib/nubosloop.cjs');
|
|
488
|
+
// post-executor ran (verify-green) but critics never produced findings.
|
|
489
|
+
nubosloop.recordLoopState('M001-S001-T0001', { round: 1 }, r);
|
|
490
|
+
checkpoint.mergeCheckpoint('M001-S001-T0001', (cur) => ({
|
|
491
|
+
nubosloop: Object.assign({}, (cur && cur.nubosloop) || {}, { verify_exit_code: 0 }),
|
|
492
|
+
}), r);
|
|
493
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
494
|
+
assert.throws(
|
|
495
|
+
() => loopRunRound.run(
|
|
496
|
+
['M001-S001-T0001', '--phase', 'commit'],
|
|
497
|
+
{ cwd: r, stdout: _cap().stub },
|
|
498
|
+
),
|
|
499
|
+
(err) => err && err.code === 'loop-commit-precondition-missing'
|
|
500
|
+
&& err.details && err.details.missing === 'findings',
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test('LCLI-RR-10: phase=commit accepts complete loop state (verify-green + findings array)', () => {
|
|
505
|
+
const r = _mkRoot();
|
|
506
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
507
|
+
const nubosloop = require('../../lib/nubosloop.cjs');
|
|
508
|
+
nubosloop.recordLoopState('M001-S001-T0001', { round: 1 }, r);
|
|
509
|
+
checkpoint.mergeCheckpoint('M001-S001-T0001', (cur) => ({
|
|
510
|
+
nubosloop: Object.assign({}, (cur && cur.nubosloop) || {}, {
|
|
511
|
+
verify_exit_code: 0,
|
|
512
|
+
findings: [],
|
|
513
|
+
}),
|
|
514
|
+
}), r);
|
|
515
|
+
const cap = _cap();
|
|
516
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
517
|
+
loopRunRound.run(['M001-S001-T0001', '--phase', 'commit'], { cwd: r, stdout: cap.stub });
|
|
518
|
+
const out = JSON.parse(cap.get());
|
|
519
|
+
assert.equal(out.next_action, 'commit-task');
|
|
520
|
+
assert.equal(out.forced, false);
|
|
521
|
+
const cp = checkpoint.readCheckpoint('M001-S001-T0001', r);
|
|
522
|
+
assert.equal(cp.nubosloop.last_phase, 'commit');
|
|
523
|
+
assert.equal(cp.nubosloop.forced_commit_phase, false);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('LCLI-RR-11: phase=commit --force-commit-phase bypasses preconditions and stamps the override', () => {
|
|
527
|
+
const r = _mkRoot();
|
|
528
|
+
checkpoint.startTask({ id: 'M001-S001-T0001' }, r);
|
|
529
|
+
// Empty checkpoint — no verify, no findings. Force should still allow.
|
|
530
|
+
const cap = _cap();
|
|
531
|
+
const loopRunRound = require('./loop-run-round.cjs');
|
|
532
|
+
loopRunRound.run(
|
|
533
|
+
['M001-S001-T0001', '--phase', 'commit', '--force-commit-phase'],
|
|
534
|
+
{ cwd: r, stdout: cap.stub },
|
|
535
|
+
);
|
|
536
|
+
const out = JSON.parse(cap.get());
|
|
537
|
+
assert.equal(out.next_action, 'commit-task');
|
|
538
|
+
assert.equal(out.forced, true);
|
|
539
|
+
const cp = checkpoint.readCheckpoint('M001-S001-T0001', r);
|
|
540
|
+
assert.equal(cp.nubosloop.forced_commit_phase, true);
|
|
541
|
+
});
|
|
542
|
+
|
|
470
543
|
test('LCLI-22: learning-match queries the local store', () => {
|
|
471
544
|
const r = _mkRoot();
|
|
472
545
|
const lr = require('../../lib/learnings.cjs');
|
|
@@ -164,6 +164,43 @@ function _runPostCritics(taskId, list, cwd) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
function _runCommit(taskId, list, cwd) {
|
|
167
|
+
// Sequence-integrity guard: the commit phase MUST follow a complete loop run.
|
|
168
|
+
// Stamping last_phase='commit' is what unlocks commit-task, so without this
|
|
169
|
+
// check an agent could shell out `loop-run-round --phase commit` directly,
|
|
170
|
+
// skip preflight/executor/critics, and bypass the entire Nubosloop. The
|
|
171
|
+
// commit-task gate then sees a satisfied last_phase and lets the commit
|
|
172
|
+
// through. Defense-in-depth: refuse here AND in commit-task.
|
|
173
|
+
//
|
|
174
|
+
// Required evidence on the checkpoint envelope:
|
|
175
|
+
// - verify_exit_code === 0 → post-executor ran AND verify was green
|
|
176
|
+
// - findings is an array → post-critics ran (empty array = passed)
|
|
177
|
+
//
|
|
178
|
+
// Bypass for legitimate test fixtures / migration: --force-commit-phase.
|
|
179
|
+
const force = list.includes('--force-commit-phase');
|
|
180
|
+
if (!force) {
|
|
181
|
+
const cur = checkpoint.readCheckpoint(taskId, cwd) || {};
|
|
182
|
+
const np = (cur && cur.nubosloop) || {};
|
|
183
|
+
if (np.verify_exit_code !== 0) {
|
|
184
|
+
throw new NubosPilotError(
|
|
185
|
+
'loop-commit-precondition-missing',
|
|
186
|
+
'phase=commit refused: post-executor did not record a verify-green run for ' + taskId +
|
|
187
|
+
' (observed verify_exit_code=' + (np.verify_exit_code === undefined ? 'undefined' : np.verify_exit_code) + '). ' +
|
|
188
|
+
'Run `loop-run-round ' + taskId + ' --phase post-executor --verify-exit-code 0 --verify-output-path ...` first, ' +
|
|
189
|
+
'or pass --force-commit-phase for an explicit override.',
|
|
190
|
+
{ taskId, missing: 'verify_exit_code', observed: np.verify_exit_code === undefined ? null : np.verify_exit_code },
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(np.findings)) {
|
|
194
|
+
throw new NubosPilotError(
|
|
195
|
+
'loop-commit-precondition-missing',
|
|
196
|
+
'phase=commit refused: post-critics did not produce a findings array for ' + taskId +
|
|
197
|
+
' (observed findings=' + (np.findings === undefined ? 'undefined' : JSON.stringify(np.findings)) + '). ' +
|
|
198
|
+
'Run `loop-run-round ' + taskId + ' --phase post-critics --critic-outputs <json>` first, ' +
|
|
199
|
+
'or pass --force-commit-phase for an explicit override.',
|
|
200
|
+
{ taskId, missing: 'findings', observed: np.findings === undefined ? null : np.findings },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
167
204
|
const pattern = args.getFlag(list, '--learning-pattern') || null;
|
|
168
205
|
const outcome = args.getFlag(list, '--learning-outcome') || 'verified';
|
|
169
206
|
let logged = null;
|
|
@@ -184,6 +221,7 @@ function _runCommit(taskId, list, cwd) {
|
|
|
184
221
|
last_phase: 'commit',
|
|
185
222
|
last_action: 'commit',
|
|
186
223
|
committed_at: new Date().toISOString(),
|
|
224
|
+
forced_commit_phase: force ? true : (cur && cur.nubosloop && cur.nubosloop.forced_commit_phase) || false,
|
|
187
225
|
}),
|
|
188
226
|
}),
|
|
189
227
|
cwd,
|
|
@@ -192,6 +230,7 @@ function _runCommit(taskId, list, cwd) {
|
|
|
192
230
|
phase: 'commit',
|
|
193
231
|
next_action: 'commit-task',
|
|
194
232
|
learning_logged: logged,
|
|
233
|
+
forced: force,
|
|
195
234
|
};
|
|
196
235
|
}
|
|
197
236
|
|