nubos-pilot 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +30 -1
- package/SECURITY.md +60 -0
- package/bin/install.js +104 -39
- package/bin/np-tools/_args.cjs +8 -2
- package/bin/np-tools/_memory-resolve.cjs +4 -4
- package/bin/np-tools/checkpoint.cjs +1 -1
- package/bin/np-tools/close-project.cjs +3 -29
- package/bin/np-tools/commit-task.cjs +31 -35
- package/bin/np-tools/commit.cjs +0 -3
- package/bin/np-tools/config.cjs +4 -13
- package/bin/np-tools/discuss-phase.cjs +4 -27
- package/bin/np-tools/doctor.cjs +76 -16
- package/bin/np-tools/doctor.test.cjs +14 -0
- package/bin/np-tools/execute-milestone.cjs +6 -27
- package/bin/np-tools/handoff-write.cjs +16 -2
- package/bin/np-tools/init-dispatch.test.cjs +21 -0
- package/bin/np-tools/knowledge-search.cjs +0 -3
- package/bin/np-tools/learning-list.cjs +0 -2
- package/bin/np-tools/learning-log.cjs +1 -7
- package/bin/np-tools/loop-audit-tool-use.cjs +1 -11
- package/bin/np-tools/loop-run-round.cjs +51 -148
- package/bin/np-tools/loop-state-read.cjs +1 -5
- package/bin/np-tools/loop-state-record.cjs +1 -27
- package/bin/np-tools/loop-stuck.cjs +1 -8
- package/bin/np-tools/messages-send.cjs +16 -2
- package/bin/np-tools/metrics.test.cjs +4 -4
- package/bin/np-tools/new-milestone.cjs +14 -3
- package/bin/np-tools/new-project.cjs +4 -2
- package/bin/np-tools/new-project.test.cjs +12 -0
- package/bin/np-tools/park.cjs +2 -1
- package/bin/np-tools/plan-lint.cjs +0 -19
- package/bin/np-tools/plan-milestone.cjs +8 -29
- package/bin/np-tools/propose-milestones.cjs +14 -3
- package/bin/np-tools/propose-milestones.test.cjs +27 -0
- package/bin/np-tools/research-phase.cjs +7 -37
- package/bin/np-tools/researcher-reconcile.cjs +3 -21
- package/bin/np-tools/reset-slice.cjs +10 -16
- package/bin/np-tools/resolve-model.cjs +21 -26
- package/bin/np-tools/resolve-model.test.cjs +15 -5
- package/bin/np-tools/resume-work.cjs +1 -5
- package/bin/np-tools/skip.cjs +2 -1
- package/bin/np-tools/spawn-headless.cjs +138 -19
- package/bin/np-tools/spawn-headless.test.cjs +310 -0
- package/bin/np-tools/state.cjs +0 -1
- package/bin/np-tools/undo-task.cjs +2 -1
- package/bin/np-tools/undo.cjs +5 -3
- package/bin/np-tools/unpark.cjs +2 -1
- package/bin/np-tools/verify-work.cjs +82 -25
- package/bin/np-tools/verify-work.test.cjs +211 -1
- package/bin/researcher-merge.cjs +2 -1
- package/bin/researcher-merge.test.cjs +14 -0
- package/lib/agents-registry.cjs +32 -0
- package/lib/agents.cjs +14 -6
- package/lib/agents.test.cjs +44 -0
- package/lib/archive.cjs +102 -36
- package/lib/archive.test.cjs +115 -5
- package/lib/checkpoint.cjs +43 -23
- package/lib/checkpoint.test.cjs +67 -6
- package/lib/commit-policy.cjs +3 -1
- package/lib/commit-policy.test.cjs +6 -0
- package/lib/config-defaults.cjs +5 -1
- package/lib/config-defaults.test.cjs +71 -0
- package/lib/config-schema.cjs +204 -0
- package/lib/config-schema.test.cjs +148 -0
- package/lib/config.cjs +168 -14
- package/lib/config.test.cjs +234 -0
- package/lib/core.cjs +226 -52
- package/lib/core.test.cjs +193 -10
- package/lib/dashboard.cjs +0 -12
- package/lib/frontmatter.cjs +5 -0
- package/lib/git.cjs +34 -27
- package/lib/git.test.cjs +11 -3
- package/lib/handoff.cjs +16 -14
- package/lib/handoff.test.cjs +24 -0
- package/lib/ids.cjs +6 -0
- package/lib/init-emit.cjs +33 -0
- package/lib/install/claude-hooks.cjs +46 -25
- package/lib/install/claude-hooks.test.cjs +64 -0
- package/lib/install/manifest.cjs +19 -0
- package/lib/install/manifest.test.cjs +107 -0
- package/lib/knowledge-adapter.cjs +3 -49
- package/lib/learnings.cjs +3 -108
- package/lib/logger.cjs +157 -0
- package/lib/logger.test.cjs +159 -0
- package/lib/memory-index-usearch.cjs +9 -12
- package/lib/memory-provider-local.cjs +8 -0
- package/lib/memory.cjs +86 -27
- package/lib/memory.test.cjs +135 -0
- package/lib/messaging.cjs +155 -83
- package/lib/metrics-aggregate.cjs +26 -27
- package/lib/metrics.cjs +7 -3
- package/lib/metrics.test.cjs +6 -5
- package/lib/migrations.cjs +89 -0
- package/lib/migrations.test.cjs +82 -0
- package/lib/milestone-meta.cjs +70 -0
- package/lib/nubosloop-audit.cjs +41 -141
- package/lib/nubosloop.cjs +45 -149
- package/lib/plan-lint.cjs +0 -67
- package/lib/researcher-swarm.cjs +1 -62
- package/lib/roadmap-render.cjs +107 -33
- package/lib/roadmap-schema.cjs +42 -0
- package/lib/roadmap.cjs +93 -20
- package/lib/roadmap.test.cjs +215 -0
- package/lib/run-context.cjs +54 -0
- package/lib/run-context.test.cjs +53 -0
- package/lib/runtime/index.cjs +5 -10
- package/lib/runtime/index.test.cjs +8 -1
- package/lib/safe-path.cjs +156 -0
- package/lib/safe-path.test.cjs +164 -0
- package/lib/state.cjs +28 -10
- package/lib/state.test.cjs +72 -22
- package/lib/tasks.cjs +92 -14
- package/lib/tasks.test.cjs +65 -0
- package/lib/todo.cjs +7 -5
- package/lib/verify.cjs +44 -3
- package/lib/worktree.cjs +2 -2
- package/lib/yaml.cjs +44 -0
- package/lib/yaml.test.cjs +65 -0
- package/np-tools.cjs +25 -23
- package/package.json +5 -2
- package/workflows/research-phase.md +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to nubos-pilot are documented in this file. Format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
|
|
5
|
+
follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.1.4] — 2026-05-25
|
|
8
|
+
|
|
9
|
+
Public release.
|
|
10
|
+
|
|
11
|
+
- Plan, execute, and verify code changes through a researcher + critic
|
|
12
|
+
agent loop.
|
|
13
|
+
- Wave-based milestone execution; one atomic git commit per task.
|
|
14
|
+
- Multi-runtime install for 14 host CLIs (Claude Code, Codex, Gemini,
|
|
15
|
+
OpenCode, Cursor, and more) via `npx nubos-pilot`.
|
|
16
|
+
- Local vector memory for cross-task learnings.
|
|
17
|
+
- Inter-agent messages, handoffs, and project archive with crash-safe
|
|
18
|
+
resume.
|
|
19
|
+
- Hardened filesystem operations: symlink-rejecting locks, restricted
|
|
20
|
+
permissions on audit logs, path containment for file-input flags,
|
|
21
|
+
frontmatter sanitisation, and a memory-model allow-list.
|
|
22
|
+
|
|
23
|
+
Full documentation at <https://pilot.nubos.cloud>.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nubos AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -146,6 +146,35 @@ npm test # all unit tests via node:test
|
|
|
146
146
|
node bin/check-workflows.cjs # workflow linter
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for setup, code conventions, ADR
|
|
150
|
+
map and commit format.
|
|
151
|
+
|
|
152
|
+
## Architecture Decisions
|
|
153
|
+
|
|
154
|
+
ADRs live in the VitePress at
|
|
155
|
+
[`pilot.nubos.cloud/v1/adr/`](https://pilot.nubos.cloud/v1/adr/). The
|
|
156
|
+
load-bearing ones for users and contributors:
|
|
157
|
+
|
|
158
|
+
| ADR | What it pins |
|
|
159
|
+
|---|---|
|
|
160
|
+
| 0004 | `workflow.commit_artifacts` controls whether `.nubos-pilot/` is committed |
|
|
161
|
+
| 0010 | Nubosloop — researcher → executor → critic-schwarm is mandatory in `/np:execute-phase` |
|
|
162
|
+
| 0012 | Completeness doctrine (12 rules in `templates/COMPLETENESS.md`) |
|
|
163
|
+
| 0013 | Learnings-store schema evolution |
|
|
164
|
+
| 0017 | Strict output-schema enforcement |
|
|
165
|
+
| 0019 | Plan-side trust layer (`lib/plan-lint.cjs`) |
|
|
166
|
+
|
|
167
|
+
## Security
|
|
168
|
+
|
|
169
|
+
See [`SECURITY.md`](./SECURITY.md) for the vulnerability disclosure policy
|
|
170
|
+
and threat model.
|
|
171
|
+
|
|
172
|
+
## Support
|
|
173
|
+
|
|
174
|
+
- Bugs / features: [GitHub issues](https://github.com/Nubos-AI/nubos-pilot/issues)
|
|
175
|
+
- Security: `security@nubos.ai` (see [`SECURITY.md`](./SECURITY.md))
|
|
176
|
+
- Docs: <https://pilot.nubos.cloud>
|
|
177
|
+
|
|
149
178
|
## License
|
|
150
179
|
|
|
151
|
-
MIT
|
|
180
|
+
MIT — see [`LICENSE`](./LICENSE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
If you discover a security issue in nubos-pilot, **do not open a public issue**.
|
|
6
|
+
Email **security@nubos.ai** with:
|
|
7
|
+
|
|
8
|
+
- A description of the issue and its impact.
|
|
9
|
+
- Steps to reproduce (PoC if possible).
|
|
10
|
+
- The affected version (`npx nubos-pilot --version` or check `package.json`).
|
|
11
|
+
- Your preferred contact channel for follow-up.
|
|
12
|
+
|
|
13
|
+
We will acknowledge receipt within **3 business days** and provide a
|
|
14
|
+
resolution plan within **14 business days**. Fixes are released as patch
|
|
15
|
+
versions and announced in `CHANGELOG.md`.
|
|
16
|
+
|
|
17
|
+
## Supported Versions
|
|
18
|
+
|
|
19
|
+
| Version | Supported |
|
|
20
|
+
|---------|-----------|
|
|
21
|
+
| 0.2.x | ✅ active |
|
|
22
|
+
| < 0.2 | ❌ end of life |
|
|
23
|
+
|
|
24
|
+
Only the latest minor on the current major receives security patches until
|
|
25
|
+
1.0 is reached.
|
|
26
|
+
|
|
27
|
+
## Threat Model
|
|
28
|
+
|
|
29
|
+
nubos-pilot is a **local CLI** distributed via npm to developer workstations
|
|
30
|
+
and CI. It is **not** a hosted service. The threat surface and assumptions:
|
|
31
|
+
|
|
32
|
+
| What nubos-pilot reads | What it writes | What it executes |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `.nubos-pilot/`, project source for context | `.nubos-pilot/` state, `~/.codex/`, `~/.claude/` config (install only) | `git`, `claude`/`codex` headless via `child_process.spawn` |
|
|
35
|
+
|
|
36
|
+
**Trust boundaries:**
|
|
37
|
+
|
|
38
|
+
- **Project source code** — untrusted in the sense that agent-authored
|
|
39
|
+
files (`PLAN.md`, `RESEARCH.md` etc.) may contain hostile YAML. nubos-pilot
|
|
40
|
+
rejects prototype-pollution keys, refuses symlink-escape via `safe-path`,
|
|
41
|
+
caps message bodies, and whitelists ML model identifiers.
|
|
42
|
+
- **`.nubos-pilot/messages/`** — multi-agent inbox; entries are written
|
|
43
|
+
atomically with `O_CREAT|O_EXCL|O_NOFOLLOW` (POSIX) so a pre-planted
|
|
44
|
+
symlink cannot redirect writes.
|
|
45
|
+
- **Subprocess spawn** — `claude`/`codex` are invoked via `spawnSync` (no
|
|
46
|
+
shell). The binary path is overridable via `NUBOS_PILOT_CLAUDE_BIN` /
|
|
47
|
+
`NUBOS_PILOT_CODEX_BIN`; treat operators who can set those env vars as
|
|
48
|
+
trusted.
|
|
49
|
+
- **`workflow.commit_artifacts`** flag controls whether `.nubos-pilot/`
|
|
50
|
+
artifacts are committed to git. Default is `true`; downstream projects
|
|
51
|
+
that consider artifacts sensitive should set it to `false`.
|
|
52
|
+
|
|
53
|
+
## What is Out of Scope
|
|
54
|
+
|
|
55
|
+
- Vulnerabilities in `@huggingface/transformers`, `usearch`, or the
|
|
56
|
+
`yaml` package — report those upstream.
|
|
57
|
+
- Operator-controlled config (`config.json`) that the operator themselves
|
|
58
|
+
wrote — config is trusted input from the project owner.
|
|
59
|
+
- DoS from running nubos-pilot in obviously bad conditions
|
|
60
|
+
(no disk space, no Node 22+, broken `git`).
|
package/bin/install.js
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require('node:fs');
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
|
|
8
|
-
const { atomicWriteFileSync, withFileLock, NubosPilotError } = require('../lib/core.cjs');
|
|
8
|
+
const { atomicWriteFileSync, withFileLock, installSignalCleanup, NubosPilotError } = require('../lib/core.cjs');
|
|
9
9
|
const { askUser: defaultAskUser } = require('../lib/askuser.cjs');
|
|
10
10
|
const manifestMod = require('../lib/install/manifest.cjs');
|
|
11
11
|
const stagingMod = require('../lib/install/staging.cjs');
|
|
@@ -159,6 +159,10 @@ function _renderShim(target, mode) {
|
|
|
159
159
|
return '#!/usr/bin/env node\n'
|
|
160
160
|
+ "'use strict';\n"
|
|
161
161
|
+ 'const fs = require(\'node:fs\');\n'
|
|
162
|
+
+ 'if (Number(process.versions.node.split(\'.\')[0]) < 22) {\n'
|
|
163
|
+
+ ' process.stderr.write("nubos-pilot: requires Node >= 22 (running " + process.versions.node + ")\\n");\n'
|
|
164
|
+
+ ' process.exit(1);\n'
|
|
165
|
+
+ '}\n'
|
|
162
166
|
+ 'const TARGET = ' + JSON.stringify(target) + ';\n'
|
|
163
167
|
+ 'if (!fs.existsSync(TARGET)) {\n'
|
|
164
168
|
+ ' process.stderr.write("nubos-pilot: tool binary fehlt unter " + TARGET + "\\nFix: npx nubos-pilot@latest update\\n");\n'
|
|
@@ -170,12 +174,18 @@ function _renderShim(target, mode) {
|
|
|
170
174
|
+ "'use strict';\n"
|
|
171
175
|
+ 'const fs = require(\'node:fs\');\n'
|
|
172
176
|
+ 'const { spawn } = require(\'node:child_process\');\n'
|
|
177
|
+
+ 'if (Number(process.versions.node.split(\'.\')[0]) < 22) {\n'
|
|
178
|
+
+ ' process.stderr.write("nubos-pilot: requires Node >= 22 (running " + process.versions.node + ")\\n");\n'
|
|
179
|
+
+ ' process.exit(1);\n'
|
|
180
|
+
+ '}\n'
|
|
173
181
|
+ 'const TARGET = ' + JSON.stringify(target) + ';\n'
|
|
174
182
|
+ 'if (!fs.existsSync(TARGET)) {\n'
|
|
175
183
|
+ ' process.stderr.write("nubos-pilot: tool binary fehlt unter " + TARGET + "\\nFix: npx nubos-pilot@latest update\\n");\n'
|
|
176
184
|
+ ' process.exit(1);\n'
|
|
177
185
|
+ '}\n'
|
|
178
186
|
+ 'const child = spawn(process.execPath, [TARGET, ...process.argv.slice(2)], { stdio: \'inherit\' });\n'
|
|
187
|
+
+ 'child.on(\'error\', (err) => { process.stderr.write("nubos-pilot shim: " + (err && err.message ? err.message : String(err)) + "\\n"); process.exit(1); });\n'
|
|
188
|
+
+ 'for (const s of [\'SIGINT\', \'SIGTERM\', \'SIGHUP\']) { process.on(s, () => { try { child.kill(s); } catch {} }); }\n'
|
|
179
189
|
+ 'child.on(\'exit\', (code, sig) => { if (sig) process.kill(process.pid, sig); else process.exit(code == null ? 1 : code); });\n';
|
|
180
190
|
}
|
|
181
191
|
|
|
@@ -197,24 +207,38 @@ function _stateDirFor(projectRoot) {
|
|
|
197
207
|
return path.join(projectRoot, STATE_SUBPATH);
|
|
198
208
|
}
|
|
199
209
|
|
|
200
|
-
function
|
|
210
|
+
function _readInstallConfig(projectRoot) {
|
|
201
211
|
const cfgPath = path.join(_stateDirFor(projectRoot), 'config.json');
|
|
202
212
|
if (!fs.existsSync(cfgPath)) return null;
|
|
213
|
+
const { _CONFIG_PARSE_CODES, readConfig } = require('../lib/config.cjs');
|
|
214
|
+
const { NubosPilotError } = require('../lib/core.cjs');
|
|
203
215
|
try {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
216
|
+
return readConfig(projectRoot);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (err && err.code === 'not-in-project') return null;
|
|
219
|
+
if (err && _CONFIG_PARSE_CODES.has(err.code)) {
|
|
220
|
+
throw new NubosPilotError(
|
|
221
|
+
'install-config-unusable',
|
|
222
|
+
'install refused — .nubos-pilot/config.json is unusable (' + err.code
|
|
223
|
+
+ '). Repair or delete the file and re-run.',
|
|
224
|
+
{ cause: err.code },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _readExistingScope(projectRoot) {
|
|
232
|
+
const cfg = _readInstallConfig(projectRoot);
|
|
233
|
+
return cfg && cfg.scope ? cfg.scope : null;
|
|
207
234
|
}
|
|
208
235
|
|
|
209
236
|
function _readExistingRuntimes(projectRoot) {
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (cfg.runtime) return [cfg.runtime];
|
|
216
|
-
return null;
|
|
217
|
-
} catch { return null; }
|
|
237
|
+
const cfg = _readInstallConfig(projectRoot);
|
|
238
|
+
if (!cfg) return null;
|
|
239
|
+
if (Array.isArray(cfg.runtimes) && cfg.runtimes.length) return cfg.runtimes.slice();
|
|
240
|
+
if (cfg.runtime) return [cfg.runtime];
|
|
241
|
+
return null;
|
|
218
242
|
}
|
|
219
243
|
|
|
220
244
|
function detectMode(projectRoot, scope) {
|
|
@@ -277,10 +301,19 @@ async function _runInitQuestions(detectedRuntime, askUser, flags) {
|
|
|
277
301
|
const model_profile = (await askUser({ type: 'select', question: 'Model-Profile?',
|
|
278
302
|
options: ['frontier', 'quality', 'balanced', 'budget', 'inherit'], default: 'frontier' })).value;
|
|
279
303
|
const response_language = (await askUser({ type: 'input', question: 'Response language (ISO-639 code)?', default: 'en' })).value;
|
|
304
|
+
// Wizard / --yes default is intentionally `false` (safer-by-default per
|
|
305
|
+
// FIX-B2) even though the implicit code default lives at `true` in
|
|
306
|
+
// DEFAULT_WORKFLOW (ADR-0004). The two are NOT in drift: explicit answer
|
|
307
|
+
// overrides default; absent key falls back to ADR-0004 true. This is
|
|
308
|
+
// covered by tests/install/install-flags.test.cjs:85.
|
|
309
|
+
const commit_artifacts = (await askUser({ type: 'confirm',
|
|
310
|
+
question: 'Auto-commit nubos-pilot planning artefacts (.nubos-pilot/ — milestones, roadmap, learnings) into your git repo?',
|
|
311
|
+
default: false })).value;
|
|
280
312
|
return configDefaults.buildInstallConfig({
|
|
281
313
|
runtime, runtimes, scope,
|
|
282
314
|
model_profile,
|
|
283
315
|
response_language,
|
|
316
|
+
commit_artifacts,
|
|
284
317
|
});
|
|
285
318
|
}
|
|
286
319
|
|
|
@@ -476,8 +509,19 @@ async function _runInstallLocked(ctx) {
|
|
|
476
509
|
}
|
|
477
510
|
|
|
478
511
|
stagingMod.finalizeSwap(payloadBase);
|
|
512
|
+
const resolvedPayloadDir = path.resolve(payloadDir);
|
|
479
513
|
for (const rel of diff.stale) {
|
|
480
|
-
|
|
514
|
+
manifestMod.assertSafeManifestKey(rel, 'install-stale-cleanup');
|
|
515
|
+
const abs = path.join(payloadDir, rel);
|
|
516
|
+
const resolvedAbs = path.resolve(abs);
|
|
517
|
+
if (!(resolvedAbs === resolvedPayloadDir || resolvedAbs.startsWith(resolvedPayloadDir + path.sep))) {
|
|
518
|
+
throw new NubosPilotError(
|
|
519
|
+
'manifest-unlink-outside-base',
|
|
520
|
+
'Refusing unlink that escapes payloadDir',
|
|
521
|
+
{ rel, base: path.basename(payloadDir) },
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
try { fs.unlinkSync(abs); } catch {}
|
|
481
525
|
}
|
|
482
526
|
|
|
483
527
|
if (opencodeManifest) {
|
|
@@ -503,9 +547,20 @@ async function _runInstallLocked(ctx) {
|
|
|
503
547
|
const opencodeBase = resolvedScope === 'global' ? os.homedir() : projectRoot;
|
|
504
548
|
for (const rel of diff.stale) {
|
|
505
549
|
if (rel.startsWith(opencodeManifestPrefix)) {
|
|
550
|
+
manifestMod.assertSafeManifestKey(rel, 'install-opencode-stale');
|
|
506
551
|
const relFs = rel.startsWith('~/')
|
|
507
552
|
? path.join(os.homedir(), rel.slice(2))
|
|
508
553
|
: path.join(opencodeBase, rel);
|
|
554
|
+
const expectedBase = rel.startsWith('~/') ? os.homedir() : opencodeBase;
|
|
555
|
+
const resolvedRelFs = path.resolve(relFs);
|
|
556
|
+
const resolvedExpected = path.resolve(expectedBase);
|
|
557
|
+
if (!(resolvedRelFs === resolvedExpected || resolvedRelFs.startsWith(resolvedExpected + path.sep))) {
|
|
558
|
+
throw new NubosPilotError(
|
|
559
|
+
'manifest-unlink-outside-base',
|
|
560
|
+
'Refusing opencode unlink that escapes its base',
|
|
561
|
+
{ rel, base: path.basename(expectedBase) },
|
|
562
|
+
);
|
|
563
|
+
}
|
|
509
564
|
try { fs.unlinkSync(relFs); } catch {}
|
|
510
565
|
}
|
|
511
566
|
}
|
|
@@ -594,14 +649,11 @@ function _runUninstallLocked(projectRoot) {
|
|
|
594
649
|
return { uninstalled: false };
|
|
595
650
|
}
|
|
596
651
|
|
|
652
|
+
// Reuse the SAME validator as readManifest so a legitimate key like
|
|
653
|
+
// `..bar` (no traversal segment) isn't false-rejected here while passing
|
|
654
|
+
// validation upstream. Single source of truth lives in manifest.cjs.
|
|
597
655
|
for (const rel of Object.keys(manifest.files)) {
|
|
598
|
-
|
|
599
|
-
throw new NubosPilotError(
|
|
600
|
-
'manifest-path-traversal',
|
|
601
|
-
'Manifest contains suspicious path',
|
|
602
|
-
{ rel },
|
|
603
|
-
);
|
|
604
|
-
}
|
|
656
|
+
manifestMod.assertSafeManifestKey(rel, 'uninstall');
|
|
605
657
|
}
|
|
606
658
|
|
|
607
659
|
const payloadBase = scope === 'global' ? os.homedir() : projectRoot;
|
|
@@ -612,6 +664,20 @@ function _runUninstallLocked(projectRoot) {
|
|
|
612
664
|
const abs = rel.startsWith('~/')
|
|
613
665
|
? path.join(os.homedir(), rel.slice(2))
|
|
614
666
|
: isAsset ? path.join(payloadBase, rel) : path.join(payloadDir, rel);
|
|
667
|
+
// Defense-in-depth: even with the validator above, ensure the resolved
|
|
668
|
+
// path lives inside its expected base. A symlink or future-validator
|
|
669
|
+
// regression cannot escape this prefix check.
|
|
670
|
+
const expectedBase = rel.startsWith('~/') ? os.homedir()
|
|
671
|
+
: isAsset ? payloadBase : payloadDir;
|
|
672
|
+
const resolvedAbs = path.resolve(abs);
|
|
673
|
+
const resolvedBase = path.resolve(expectedBase);
|
|
674
|
+
if (!(resolvedAbs === resolvedBase || resolvedAbs.startsWith(resolvedBase + path.sep))) {
|
|
675
|
+
throw new NubosPilotError(
|
|
676
|
+
'manifest-unlink-outside-base',
|
|
677
|
+
'Refusing unlink that escapes its payload base',
|
|
678
|
+
{ rel, base: path.basename(expectedBase) },
|
|
679
|
+
);
|
|
680
|
+
}
|
|
615
681
|
try {
|
|
616
682
|
fs.unlinkSync(abs);
|
|
617
683
|
removed++;
|
|
@@ -639,12 +705,11 @@ function _runUninstallLocked(projectRoot) {
|
|
|
639
705
|
|
|
640
706
|
try { fs.rmdirSync(payloadDir); } catch {}
|
|
641
707
|
|
|
642
|
-
const cfgPath = path.join(_stateDirFor(projectRoot), 'config.json');
|
|
643
708
|
let installedRuntimes = [];
|
|
644
|
-
|
|
645
|
-
|
|
709
|
+
const cfg = _readInstallConfig(projectRoot);
|
|
710
|
+
if (cfg) {
|
|
646
711
|
installedRuntimes = cfg.runtimes || (cfg.runtime ? [cfg.runtime] : []);
|
|
647
|
-
}
|
|
712
|
+
}
|
|
648
713
|
|
|
649
714
|
const legacyFiles = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
|
|
650
715
|
const extraFiles = [];
|
|
@@ -793,21 +858,21 @@ async function runUninstallHooks(opts) {
|
|
|
793
858
|
}
|
|
794
859
|
|
|
795
860
|
if (require.main === module) {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
process.stderr.write(
|
|
799
|
-
JSON.stringify({
|
|
800
|
-
error: {
|
|
801
|
-
code: err.code,
|
|
802
|
-
message: err.message,
|
|
803
|
-
details: err.details || null,
|
|
804
|
-
},
|
|
805
|
-
}) + '\n',
|
|
806
|
-
);
|
|
807
|
-
} else {
|
|
808
|
-
process.stderr.write(((err && err.stack) || String(err)) + '\n');
|
|
809
|
-
}
|
|
861
|
+
if (Number(process.versions.node.split('.')[0]) < 22) {
|
|
862
|
+
process.stderr.write('nubos-pilot: requires Node >= 22 (running ' + process.versions.node + ')\n');
|
|
810
863
|
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
installSignalCleanup();
|
|
866
|
+
main().catch((err) => {
|
|
867
|
+
const payload = (err && err.code)
|
|
868
|
+
? JSON.stringify({ error: { code: err.code, message: err.message, details: err.details || null } }) + '\n'
|
|
869
|
+
: ((err && err.stack) || String(err)) + '\n';
|
|
870
|
+
// Drain stderr before exit. process.exit() can otherwise tear down the
|
|
871
|
+
// pipe mid-flush on busy CI, truncating the envelope. Set exitCode and
|
|
872
|
+
// let Node drain naturally; force-exit only as a last-resort fallback.
|
|
873
|
+
try { process.stderr.write(payload); } catch {}
|
|
874
|
+
process.exitCode = 1;
|
|
875
|
+
setTimeout(() => process.exit(1), 1000).unref();
|
|
811
876
|
});
|
|
812
877
|
}
|
|
813
878
|
|
package/bin/np-tools/_args.cjs
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
4
|
|
|
5
|
-
function getFlag(rest, name) {
|
|
5
|
+
function getFlag(rest, name, opts) {
|
|
6
6
|
const idx = rest.indexOf(name);
|
|
7
|
-
|
|
7
|
+
if (idx === -1) return undefined;
|
|
8
|
+
const next = rest[idx + 1];
|
|
9
|
+
const allowDash = opts && opts.allowDashValues === true;
|
|
10
|
+
if (!allowDash && typeof next === 'string' && next.startsWith('--')) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return next;
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
function getJsonFlag(rest, name, missingCode, hint) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
-
const {
|
|
4
|
+
const { tryReadConfigPath } = require('../../lib/config.cjs');
|
|
5
5
|
const { createMemory } = require('../../lib/memory.cjs');
|
|
6
6
|
|
|
7
7
|
function resolveMemory(opts) {
|
|
@@ -17,7 +17,7 @@ function resolveMemory(opts) {
|
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const enabled =
|
|
20
|
+
const enabled = tryReadConfigPath(cwd, 'memory.enabled', false);
|
|
21
21
|
if (!enabled) {
|
|
22
22
|
throw new NubosPilotError(
|
|
23
23
|
'memory-disabled',
|
|
@@ -26,8 +26,8 @@ function resolveMemory(opts) {
|
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const model =
|
|
30
|
-
const alpha =
|
|
29
|
+
const model = tryReadConfigPath(cwd, 'memory.model', 'Xenova/bge-small-en-v1.5');
|
|
30
|
+
const alpha = tryReadConfigPath(cwd, 'memory.alpha', 0.6);
|
|
31
31
|
|
|
32
32
|
const { createLocalProvider } = require('../../lib/memory-provider-local.cjs');
|
|
33
33
|
const { createUsearchIndex } = require('../../lib/memory-index-usearch.cjs');
|
|
@@ -1,36 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
const crypto = require('node:crypto');
|
|
7
|
-
|
|
8
|
-
const {
|
|
9
|
-
NubosPilotError,
|
|
10
|
-
projectStateDir,
|
|
11
|
-
} = require('../../lib/core.cjs');
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const { emitInitPayload } = require('../../lib/init-emit.cjs');
|
|
12
5
|
const archive = require('../../lib/archive.cjs');
|
|
13
6
|
const textMode = require('../../lib/text-mode.cjs');
|
|
14
7
|
|
|
15
|
-
const INLINE_THRESHOLD_BYTES = 16 * 1024;
|
|
16
|
-
|
|
17
|
-
function _emit(payload, stdout, cwd) {
|
|
18
|
-
const json = JSON.stringify(payload, null, 2);
|
|
19
|
-
if (Buffer.byteLength(json, 'utf-8') <= INLINE_THRESHOLD_BYTES) {
|
|
20
|
-
stdout.write(json);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
let tmpDir;
|
|
24
|
-
try {
|
|
25
|
-
tmpDir = path.join(projectStateDir(cwd), '.tmp');
|
|
26
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
27
|
-
} catch { tmpDir = os.tmpdir(); }
|
|
28
|
-
const suffix = process.pid + '-' + crypto.randomBytes(4).toString('hex');
|
|
29
|
-
const tmpPath = path.join(tmpDir, 'init-close-project-' + suffix + '.json');
|
|
30
|
-
fs.writeFileSync(tmpPath, json, 'utf-8');
|
|
31
|
-
stdout.write('@file:' + tmpPath);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
8
|
function _initPayload(cwd) {
|
|
35
9
|
const completion = archive.computeCompletionStatus(cwd);
|
|
36
10
|
const tmDetail = textMode.resolveTextModeDetail(cwd);
|
|
@@ -68,7 +42,7 @@ function run(args, ctx) {
|
|
|
68
42
|
case 'init':
|
|
69
43
|
case undefined: {
|
|
70
44
|
const payload = _initPayload(cwd);
|
|
71
|
-
|
|
45
|
+
emitInitPayload(payload, stdout, cwd, 'close-project');
|
|
72
46
|
return payload;
|
|
73
47
|
}
|
|
74
48
|
case 'check': {
|
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const safePath = require('../../lib/safe-path.cjs');
|
|
3
4
|
|
|
4
5
|
const { NubosPilotError, findProjectRoot } = require('../../lib/core.cjs');
|
|
5
6
|
const { extractFrontmatter } = require('../../lib/frontmatter.cjs');
|
|
6
|
-
const {
|
|
7
|
+
const { setTaskStatus } = require('../../lib/tasks.cjs');
|
|
8
|
+
const { TASK_ID_RE } = require('../../lib/ids.cjs');
|
|
7
9
|
const layout = require('../../lib/layout.cjs');
|
|
8
10
|
const git = require('../../lib/git.cjs');
|
|
9
11
|
const { commitTask, findCommitByTaskId } = git;
|
|
10
|
-
const {
|
|
12
|
+
const { finishTask, readCheckpoint, mergeCheckpoint } = require('../../lib/checkpoint.cjs');
|
|
11
13
|
|
|
12
14
|
const BYPASS_FLAG = '--bypass-nubosloop';
|
|
13
15
|
|
|
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
|
-
//
|
|
21
|
-
// `evaluateLoop` only routes `next_action='commit'` when `findings.length === 0`
|
|
22
|
-
// (see lib/nubosloop.cjs). The previous gate accepted `Array.isArray(findings)`
|
|
23
|
-
// alone — a critic that returned actual findings still satisfied the shape
|
|
24
|
-
// check, letting the commit slip through. Mirror the evaluator's invariant
|
|
25
|
-
// here so a non-empty findings array is a hard refuse, not an accident.
|
|
26
16
|
function _assertLoopGate(taskId, cwd, bypass, stderr) {
|
|
27
17
|
const cp = readCheckpoint(taskId, cwd);
|
|
28
18
|
const np = (cp && cp.nubosloop) || null;
|
|
@@ -85,24 +75,33 @@ function _resolveTaskFile(taskId, cwd) {
|
|
|
85
75
|
}
|
|
86
76
|
|
|
87
77
|
function _resolveSafe(root, p) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
78
|
+
try {
|
|
79
|
+
safePath.assertInsideBase(root, path.resolve(root, p), 'commit-files');
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err && (err.code === 'safe-path-outside-base' || err.code === 'safe-path-invalid-input' || err.code === 'safe-path-base-missing')) {
|
|
82
|
+
throw new NubosPilotError(
|
|
83
|
+
'path-not-in-project',
|
|
84
|
+
'files_modified entry escapes project root: ' + p,
|
|
85
|
+
{ path: p, root, cause: err.code },
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
96
89
|
}
|
|
97
90
|
return p;
|
|
98
91
|
}
|
|
99
92
|
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
const _COMMIT_NAME_MAX = 200;
|
|
94
|
+
function _sanitizeCommitName(s) {
|
|
95
|
+
return String(s == null ? '' : s).replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, _COMMIT_NAME_MAX);
|
|
96
|
+
}
|
|
102
97
|
|
|
98
|
+
function _extractName(frontmatter, body) {
|
|
99
|
+
if (typeof frontmatter.name === 'string' && frontmatter.name.length > 0) {
|
|
100
|
+
return _sanitizeCommitName(frontmatter.name);
|
|
101
|
+
}
|
|
103
102
|
const m = String(body || '').match(/^#\s+(?:Task:\s*)?(.+?)\s*$/m);
|
|
104
|
-
if (m) return m[1]
|
|
105
|
-
return frontmatter.id || 'task';
|
|
103
|
+
if (m) return _sanitizeCommitName(m[1]);
|
|
104
|
+
return _sanitizeCommitName(frontmatter.id || 'task');
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
function run(args, ctx) {
|
|
@@ -163,13 +162,6 @@ function run(args, ctx) {
|
|
|
163
162
|
const result = commitTask(taskId, safeFiles, message);
|
|
164
163
|
|
|
165
164
|
if (result.committed === false && result.reason === 'artifacts-gitignored') {
|
|
166
|
-
// Soft-skip: every files_modified entry is gitignored. The task ran the
|
|
167
|
-
// full Nubosloop (preflight → executor → critic), edits landed locally,
|
|
168
|
-
// and the workflow already stamped `committed_at` via loop-run-round.
|
|
169
|
-
// We mark the task done WITHOUT a git commit, record the skip reason on
|
|
170
|
-
// the checkpoint for audit, and let the wave continue. Symmetric to
|
|
171
|
-
// commit_artifacts=false (commit.cjs:102) and to feedback_no_container_blocker:
|
|
172
|
-
// gitignore is a routing signal, never a hard stop.
|
|
173
165
|
try {
|
|
174
166
|
mergeCheckpoint(taskId, (cur) => ({
|
|
175
167
|
nubosloop: Object.assign({}, (cur && cur.nubosloop) || {}, {
|
|
@@ -180,7 +172,9 @@ function run(args, ctx) {
|
|
|
180
172
|
} catch (err) {
|
|
181
173
|
process.stderr.write('[nubos-pilot warn] checkpoint stamp failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
182
174
|
}
|
|
183
|
-
try {
|
|
175
|
+
try { finishTask(taskId, cwd); } catch (err) {
|
|
176
|
+
process.stderr.write('[nubos-pilot warn] finishTask failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
177
|
+
}
|
|
184
178
|
try { setTaskStatus(taskId, 'done', cwd); } catch (err) {
|
|
185
179
|
process.stderr.write('[nubos-pilot warn] setTaskStatus failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
186
180
|
}
|
|
@@ -201,7 +195,9 @@ function run(args, ctx) {
|
|
|
201
195
|
|
|
202
196
|
const sha = findCommitByTaskId(taskId);
|
|
203
197
|
|
|
204
|
-
try {
|
|
198
|
+
try { finishTask(taskId, cwd); } catch (err) {
|
|
199
|
+
process.stderr.write('[nubos-pilot warn] finishTask failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
200
|
+
}
|
|
205
201
|
try { setTaskStatus(taskId, 'done', cwd); } catch (err) {
|
|
206
202
|
process.stderr.write('[nubos-pilot warn] setTaskStatus failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
207
203
|
}
|
package/bin/np-tools/commit.cjs
CHANGED
|
@@ -101,9 +101,6 @@ function run(argv, ctx) {
|
|
|
101
101
|
const normalized = _normalizeFiles(files, cwd, root);
|
|
102
102
|
const committable = assertCommittablePaths(normalized, { cwd: root });
|
|
103
103
|
if (committable.length === 0) {
|
|
104
|
-
// All paths gitignored → soft-skip with structured payload (symmetric to
|
|
105
|
-
// commit_artifacts=false above). The earlier `commit-no-paths` throw
|
|
106
|
-
// turned a routing signal into a hard error.
|
|
107
104
|
stdout.write(JSON.stringify({
|
|
108
105
|
committed: false,
|
|
109
106
|
reason: 'artifacts-gitignored',
|
package/bin/np-tools/config.cjs
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const { findProjectRoot, NubosPilotError } = require('../../lib/core.cjs');
|
|
1
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
2
|
const { DEFAULT_CONFIG_TREE } = require('../../lib/config-defaults.cjs');
|
|
5
3
|
const { emitErrorEnvelope } = require('./_args.cjs');
|
|
6
4
|
|
|
@@ -12,21 +10,14 @@ function _usage() {
|
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
function _readConfig(cwd) {
|
|
15
|
-
|
|
13
|
+
const { readConfig } = require('../../lib/config.cjs');
|
|
16
14
|
try {
|
|
17
|
-
|
|
15
|
+
const cfg = readConfig(cwd);
|
|
16
|
+
return cfg && Object.keys(cfg).length === 0 ? null : cfg;
|
|
18
17
|
} catch (err) {
|
|
19
18
|
if (err && err.code === 'not-in-project') return null;
|
|
20
19
|
throw err;
|
|
21
20
|
}
|
|
22
|
-
const p = path.join(root, '.nubos-pilot', 'config.json');
|
|
23
|
-
if (!fs.existsSync(p)) return null;
|
|
24
|
-
try {
|
|
25
|
-
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
26
|
-
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
27
|
-
} catch (err) {
|
|
28
|
-
throw new NubosPilotError('config-parse-error', 'config.json invalid JSON', { cause: err && err.message });
|
|
29
|
-
}
|
|
30
21
|
}
|
|
31
22
|
|
|
32
23
|
function _walkPath(obj, segments) {
|