nubos-pilot 0.9.2 → 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.
- package/agents/np-build-fixer.md +4 -1
- package/agents/np-critic-acceptance.md +1 -0
- package/agents/np-executor.md +3 -0
- package/bin/np-tools/commit-task.cjs +41 -3
- package/bin/np-tools/commit-task.test.cjs +111 -10
- package/bin/np-tools/config.cjs +4 -4
- package/bin/np-tools/config.test.cjs +61 -0
- package/lib/config-defaults.cjs +12 -0
- package/package.json +1 -1
- package/workflows/execute-phase.md +2 -0
package/agents/np-build-fixer.md
CHANGED
|
@@ -90,7 +90,7 @@ node .nubos-pilot/bin/np-tools.cjs handoff-write \
|
|
|
90
90
|
- **Success:** verify command exits 0; no extra files written; control returned to executor.
|
|
91
91
|
- **Stuck after 3 attempts:** write `T<NNNN>-FIX-NOTES.md` next to the task plan; emit `## FIX FAILED` block listing attempts + suspected cause.
|
|
92
92
|
- **Out-of-scope failure:** emit `## SCOPE EXPANSION REQUEST` block listing the out-of-scope path + the symbol involved; do NOT edit.
|
|
93
|
-
- **
|
|
93
|
+
- **Infrastructure mismatch (container down, wrong runtime version, missing service):** this is NOT a fix-target. Emit a finding tagged `information-missing` with the specific mismatch (e.g., `composer requires php ^8.5, container runs 8.4`) so `loop-evaluate` routes to the researcher swarm or plan-checker, not back to you. Do NOT edit Dockerfiles, compose configs, or other infra paths to "make verify green" — that's outside any task's `files_modified`.
|
|
94
94
|
|
|
95
95
|
<scope_guardrail>
|
|
96
96
|
**Do:**
|
|
@@ -98,6 +98,7 @@ node .nubos-pilot/bin/np-tools.cjs handoff-write \
|
|
|
98
98
|
- Run the task's verify command via Bash.
|
|
99
99
|
- Use `knowledge-search` for unfamiliar symbols.
|
|
100
100
|
- Stop after 3 failed attempts and document.
|
|
101
|
+
- Distinguish code failures (your job) from infrastructure failures (route via finding).
|
|
101
102
|
|
|
102
103
|
**Don't:**
|
|
103
104
|
- Expand `files_modified` — that's the planner's job; emit a SCOPE EXPANSION REQUEST instead.
|
|
@@ -106,4 +107,6 @@ node .nubos-pilot/bin/np-tools.cjs handoff-write \
|
|
|
106
107
|
- Silence failures with empty catches, skipped tests, or commented-out assertions.
|
|
107
108
|
- Re-litigate locked decisions in `M<NNN>-CONTEXT.md` or `RULES.md`.
|
|
108
109
|
- Spawn other agents.
|
|
110
|
+
- Edit infrastructure (Dockerfile, docker-compose, k8s, CI configs) to fix verify-red — those paths are out of scope for any task; surface the mismatch as an `information-missing` finding instead.
|
|
111
|
+
- Treat container-down / runtime-version-skew as a code bug. It's an environment routing signal, not a code-fixable failure.
|
|
109
112
|
</scope_guardrail>
|
|
@@ -48,6 +48,7 @@ The orchestrator provides these paths in your prompt context. Read every path it
|
|
|
48
48
|
2. **Locked-decision conformance** — the diff does not violate any locked decision in `M<NNN>-CONTEXT.md`. Violations are findings of category `locked-decision-violation`.
|
|
49
49
|
3. **Scope creep** — the diff does not edit files outside `files_modified`. Out-of-scope edits are findings of category `scope-creep`.
|
|
50
50
|
4. **Stuck-marker check** — if the task is on round 3 with no progress between rounds, you flag `stuck-detected` so the orchestrator escalates.
|
|
51
|
+
5. **Infrastructure-mismatch detection** — if the verify output indicates an infrastructure failure (container exited, runtime version skew, missing service: `php -v` mismatch, `docker exec` errors, port-not-bound, DB-unreachable), do NOT downgrade affected criteria to `Unsatisfied` or `Satisfied`. Mark them `Information-Missing` with a finding of category `information-missing` whose `remediation` names the specific environment delta (e.g., `composer requires php ^8.5, container runs 8.4 — Dockerfile bump required outside this milestone`). The orchestrator routes that to researcher / plan-checker, not back to executor — the code is not at fault.
|
|
51
52
|
|
|
52
53
|
## Output Schema
|
|
53
54
|
|
package/agents/np-executor.md
CHANGED
|
@@ -117,6 +117,7 @@ into the `task(…)` commit. If `workflow.commit_docs=true`, the
|
|
|
117
117
|
- Commit via `node np-tools.cjs commit-task <task-id>`.
|
|
118
118
|
- Write checkpoint state transitions via the wrapper.
|
|
119
119
|
- Stay within the task's declared scope even if you spot tangential issues — log them, do not fix them.
|
|
120
|
+
- Run the task's `<verify>` command and capture its exit code + output. If it fails because the runtime environment is wrong (container exited, wrong PHP/Node version, missing service), surface that in the verify output verbatim — the Nubosloop's `loop-run-round --phase post-executor` reads the exit code and routes accordingly. The infra issue is a routing signal, not your decision.
|
|
120
121
|
|
|
121
122
|
**Don't:**
|
|
122
123
|
- Add files to the commit beyond `files_modified` (D-04 authoritative).
|
|
@@ -124,6 +125,8 @@ into the `task(…)` commit. If `workflow.commit_docs=true`, the
|
|
|
124
125
|
- Bypass the checkpoint wrapper.
|
|
125
126
|
- Use `--no-verify`, `--force`, `git reset --hard`, `git clean`, `git restore .`, or any destructive git flag.
|
|
126
127
|
- Auto-discover files via `git status` — the plan declares scope, not the filesystem.
|
|
128
|
+
- **Pre-validate the runtime environment** (`docker ps`, `php -v`, `node -v`, container-status checks, DB connectivity probes). The orchestrator's pre-flight phase covers what needs to be checked; you do code edits and run verify. If the container is down or the runtime is wrong, the verify command will fail and the loop routes that — never declare a "hard blocker" or abort the spawn over environment state.
|
|
129
|
+
- **Refuse to spawn / halt before editing because of infra mismatch** (PHP version skew, missing image, etc.). Tasks edit code, not infrastructure. Run your edits, run verify, let the result speak.
|
|
127
130
|
</scope_guardrail>
|
|
128
131
|
|
|
129
132
|
## Handoff Protocol
|
|
@@ -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
|
|
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 = {
|
|
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
|
-
|
|
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');
|
|
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/bin/np-tools/config.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const { findProjectRoot, NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { DEFAULT_CONFIG_TREE } = require('../../lib/config-defaults.cjs');
|
|
4
5
|
|
|
5
6
|
const SEGMENT_RE = /^[a-zA-Z0-9_-]+$/;
|
|
6
7
|
const BLOCKED_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
@@ -75,11 +76,10 @@ function run(argv, ctx) {
|
|
|
75
76
|
try {
|
|
76
77
|
_validateSegments(segments);
|
|
77
78
|
const config = _readConfig(cwd);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
let value = config == null ? undefined : _walkPath(config, segments);
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
value = _walkPath(DEFAULT_CONFIG_TREE, segments);
|
|
81
82
|
}
|
|
82
|
-
const value = _walkPath(config, segments);
|
|
83
83
|
if (value === undefined) {
|
|
84
84
|
if (!raw) stdout.write('\n');
|
|
85
85
|
return 0;
|
|
@@ -69,3 +69,64 @@ test('CONFIG-4: object value serialized as JSON', () => {
|
|
|
69
69
|
assert.equal(code, 0);
|
|
70
70
|
assert.equal(stdout.toString(), '{"k":"v"}');
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
test('CONFIG-5: returns DEFAULT_CONFIG_TREE value when key absent from user config', () => {
|
|
74
|
+
const sb = makeSandbox({ runtime: 'claude' });
|
|
75
|
+
const stdout = makeSink();
|
|
76
|
+
const code = configCli.run(['loop.maxRounds'], { cwd: sb, stdout, stderr: makeSink() });
|
|
77
|
+
assert.equal(code, 0);
|
|
78
|
+
assert.equal(stdout.toString(), '3\n');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('CONFIG-6: defaults walk into nested swarm.research.* keys', () => {
|
|
82
|
+
const sb = makeSandbox({});
|
|
83
|
+
const out1 = makeSink(); configCli.run(['swarm.research.k'], { cwd: sb, stdout: out1, stderr: makeSink() });
|
|
84
|
+
const out2 = makeSink(); configCli.run(['swarm.research.threshold'], { cwd: sb, stdout: out2, stderr: makeSink() });
|
|
85
|
+
const out3 = makeSink(); configCli.run(['swarm.research.minOccurrence'], { cwd: sb, stdout: out3, stderr: makeSink() });
|
|
86
|
+
assert.equal(out1.toString(), '3\n');
|
|
87
|
+
assert.equal(out2.toString(), '0.9\n');
|
|
88
|
+
assert.equal(out3.toString(), '3\n');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('CONFIG-7: user-set value wins over default', () => {
|
|
92
|
+
const sb = makeSandbox({ loop: { maxRounds: 5 } });
|
|
93
|
+
const stdout = makeSink();
|
|
94
|
+
configCli.run(['loop.maxRounds'], { cwd: sb, stdout, stderr: makeSink() });
|
|
95
|
+
assert.equal(stdout.toString(), '5\n');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('CONFIG-8: partial user override falls through to defaults for sibling keys', () => {
|
|
99
|
+
const sb = makeSandbox({ swarm: { research: { k: 7 } } });
|
|
100
|
+
const k = makeSink(); configCli.run(['swarm.research.k'], { cwd: sb, stdout: k, stderr: makeSink() });
|
|
101
|
+
const t = makeSink(); configCli.run(['swarm.research.threshold'], { cwd: sb, stdout: t, stderr: makeSink() });
|
|
102
|
+
assert.equal(k.toString(), '7\n');
|
|
103
|
+
assert.equal(t.toString(), '0.9\n');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('CONFIG-9: unknown key without a default still returns empty', () => {
|
|
107
|
+
const sb = makeSandbox({});
|
|
108
|
+
const stdout = makeSink();
|
|
109
|
+
configCli.run(['really.not.a.thing'], { cwd: sb, stdout, stderr: makeSink() });
|
|
110
|
+
assert.equal(stdout.toString(), '\n');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('CONFIG-10: defaults resolve even without config.json present', () => {
|
|
114
|
+
const sb = makeSandbox(); // no config.json
|
|
115
|
+
const stdout = makeSink();
|
|
116
|
+
configCli.run(['loop.maxRounds'], { cwd: sb, stdout, stderr: makeSink() });
|
|
117
|
+
assert.equal(stdout.toString(), '3\n');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('CONFIG-11: explicit user false wins over default true (boolean handling)', () => {
|
|
121
|
+
const sb = makeSandbox({ auto_log_learning: false });
|
|
122
|
+
const stdout = makeSink();
|
|
123
|
+
configCli.run(['auto_log_learning'], { cwd: sb, stdout, stderr: makeSink() });
|
|
124
|
+
assert.equal(stdout.toString(), 'false\n');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('CONFIG-12: --raw mode resolves defaults without trailing newline', () => {
|
|
128
|
+
const sb = makeSandbox({});
|
|
129
|
+
const stdout = makeSink();
|
|
130
|
+
configCli.run(['loop.maxRounds', '--raw'], { cwd: sb, stdout, stderr: makeSink() });
|
|
131
|
+
assert.equal(stdout.toString(), '3');
|
|
132
|
+
});
|
package/lib/config-defaults.cjs
CHANGED
|
@@ -47,6 +47,17 @@ const DEFAULT_MODEL_PROFILE = 'frontier';
|
|
|
47
47
|
const DEFAULT_SCOPE = 'local';
|
|
48
48
|
const DEFAULT_RESPONSE_LANGUAGE = 'en';
|
|
49
49
|
|
|
50
|
+
const DEFAULT_CONFIG_TREE = Object.freeze({
|
|
51
|
+
scope: DEFAULT_SCOPE,
|
|
52
|
+
model_profile: DEFAULT_MODEL_PROFILE,
|
|
53
|
+
response_language: DEFAULT_RESPONSE_LANGUAGE,
|
|
54
|
+
workflow: DEFAULT_WORKFLOW,
|
|
55
|
+
agents: DEFAULT_AGENTS,
|
|
56
|
+
loop: DEFAULT_LOOP,
|
|
57
|
+
swarm: DEFAULT_SWARM,
|
|
58
|
+
auto_log_learning: DEFAULT_AUTO_LOG_LEARNING,
|
|
59
|
+
});
|
|
60
|
+
|
|
50
61
|
function buildInstallConfig(answers) {
|
|
51
62
|
const a = answers || {};
|
|
52
63
|
return {
|
|
@@ -80,5 +91,6 @@ module.exports = {
|
|
|
80
91
|
DEFAULT_MODEL_PROFILE,
|
|
81
92
|
DEFAULT_SCOPE,
|
|
82
93
|
DEFAULT_RESPONSE_LANGUAGE,
|
|
94
|
+
DEFAULT_CONFIG_TREE,
|
|
83
95
|
buildInstallConfig,
|
|
84
96
|
};
|
package/package.json
CHANGED
|
@@ -363,6 +363,8 @@ After every slice completes, point the operator at `/np:validate-phase $PHASE` t
|
|
|
363
363
|
- Bundle two tasks into one commit (ADR-0004 atomicity).
|
|
364
364
|
- Skip the checkpoint start step — it's the crash-safety primitive `resume-work` depends on.
|
|
365
365
|
- Pass `--no-verify` or `--force` anywhere in the pipeline.
|
|
366
|
+
- **Introduce ad-hoc pre-flight checks beyond the two sanctioned guards** (orphan-checkpoint, empty-milestone). Container-status (`docker ps`), runtime-version probes (`php -v`, `node -v`), DB-connectivity, port-binding — none of these belong in the orchestrator's pre-flight. Tasks edit code; environment failures surface inside the Nubosloop as `verify-red` (→ `spawn-build-fixer`) or as `np-critic-acceptance` `information-missing` findings (→ researcher / plan-checker). They are **never** workflow-level halts.
|
|
367
|
+
- **Declare a "hard blocker" because of infrastructure state.** Container down, PHP version skew, missing image, exited service — all of these are routing signals inside the loop, not reasons to abort the wave. The wave only halts on `commit-task` non-zero, `stuck` after `loop.maxRounds`, or `plan-checker` (locked-decision-violation). Infrastructure mismatch routes via critic findings to researcher/plan-checker; if it's truly out-of-scope for any task in the milestone, the operator handles it separately and re-runs the workflow.
|
|
366
368
|
<!-- /scope_guardrail -->
|
|
367
369
|
|
|
368
370
|
## Output
|