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.
@@ -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
- - **Infra failure:** emit `## INFRA BLOCKER` block listing the missing dependency; do NOT edit.
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
 
@@ -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 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
+ });
@@ -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
- if (config == null) {
79
- if (!raw) stdout.write('\n');
80
- return 0;
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
+ });
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.9.2",
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": {
@@ -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