nubos-pilot 1.1.3 → 1.2.1

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.
Files changed (137) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +30 -1
  4. package/SECURITY.md +60 -0
  5. package/agents/np-executor.md +20 -0
  6. package/agents/np-security-reviewer.md +49 -3
  7. package/bin/install.js +111 -41
  8. package/bin/np-tools/_args.cjs +8 -2
  9. package/bin/np-tools/_commands.cjs +1 -0
  10. package/bin/np-tools/_memory-resolve.cjs +4 -4
  11. package/bin/np-tools/checkpoint.cjs +1 -1
  12. package/bin/np-tools/close-project.cjs +3 -29
  13. package/bin/np-tools/commit-task.cjs +31 -35
  14. package/bin/np-tools/commit.cjs +0 -3
  15. package/bin/np-tools/config.cjs +4 -13
  16. package/bin/np-tools/discuss-phase.cjs +4 -27
  17. package/bin/np-tools/doctor.cjs +76 -16
  18. package/bin/np-tools/doctor.test.cjs +14 -0
  19. package/bin/np-tools/execute-milestone.cjs +6 -27
  20. package/bin/np-tools/handoff-write.cjs +16 -2
  21. package/bin/np-tools/init-dispatch.test.cjs +21 -0
  22. package/bin/np-tools/knowledge-search.cjs +0 -3
  23. package/bin/np-tools/learning-list.cjs +0 -2
  24. package/bin/np-tools/learning-log.cjs +1 -7
  25. package/bin/np-tools/loop-audit-tool-use.cjs +1 -11
  26. package/bin/np-tools/loop-run-round.cjs +51 -148
  27. package/bin/np-tools/loop-state-read.cjs +1 -5
  28. package/bin/np-tools/loop-state-record.cjs +1 -27
  29. package/bin/np-tools/loop-stuck.cjs +1 -8
  30. package/bin/np-tools/messages-send.cjs +16 -2
  31. package/bin/np-tools/metrics.test.cjs +4 -4
  32. package/bin/np-tools/new-milestone.cjs +14 -3
  33. package/bin/np-tools/new-project.cjs +4 -2
  34. package/bin/np-tools/new-project.test.cjs +12 -0
  35. package/bin/np-tools/park.cjs +2 -1
  36. package/bin/np-tools/plan-lint.cjs +0 -19
  37. package/bin/np-tools/plan-milestone.cjs +8 -29
  38. package/bin/np-tools/propose-milestones.cjs +14 -3
  39. package/bin/np-tools/propose-milestones.test.cjs +27 -0
  40. package/bin/np-tools/research-phase.cjs +7 -37
  41. package/bin/np-tools/researcher-reconcile.cjs +3 -21
  42. package/bin/np-tools/reset-slice.cjs +10 -16
  43. package/bin/np-tools/resolve-model.cjs +21 -26
  44. package/bin/np-tools/resolve-model.test.cjs +15 -5
  45. package/bin/np-tools/resume-work.cjs +1 -5
  46. package/bin/np-tools/security.cjs +177 -0
  47. package/bin/np-tools/security.test.cjs +82 -0
  48. package/bin/np-tools/skip.cjs +2 -1
  49. package/bin/np-tools/spawn-headless.cjs +138 -19
  50. package/bin/np-tools/spawn-headless.test.cjs +310 -0
  51. package/bin/np-tools/state.cjs +0 -1
  52. package/bin/np-tools/undo-task.cjs +2 -1
  53. package/bin/np-tools/undo.cjs +5 -3
  54. package/bin/np-tools/unpark.cjs +2 -1
  55. package/bin/np-tools/verify-work.cjs +82 -25
  56. package/bin/np-tools/verify-work.test.cjs +211 -1
  57. package/bin/researcher-merge.cjs +2 -1
  58. package/bin/researcher-merge.test.cjs +14 -0
  59. package/lib/agents-registry.cjs +32 -0
  60. package/lib/agents.cjs +14 -6
  61. package/lib/agents.test.cjs +44 -0
  62. package/lib/archive.cjs +102 -36
  63. package/lib/archive.test.cjs +115 -5
  64. package/lib/checkpoint.cjs +43 -23
  65. package/lib/checkpoint.test.cjs +67 -6
  66. package/lib/commit-policy.cjs +3 -1
  67. package/lib/commit-policy.test.cjs +6 -0
  68. package/lib/config-defaults.cjs +28 -1
  69. package/lib/config-defaults.test.cjs +86 -0
  70. package/lib/config-schema.cjs +223 -0
  71. package/lib/config-schema.test.cjs +206 -0
  72. package/lib/config.cjs +168 -14
  73. package/lib/config.test.cjs +234 -0
  74. package/lib/core.cjs +226 -52
  75. package/lib/core.test.cjs +193 -10
  76. package/lib/dashboard.cjs +0 -12
  77. package/lib/frontmatter.cjs +5 -0
  78. package/lib/git.cjs +34 -27
  79. package/lib/git.test.cjs +11 -3
  80. package/lib/handoff.cjs +16 -14
  81. package/lib/handoff.test.cjs +24 -0
  82. package/lib/ids.cjs +6 -0
  83. package/lib/init-emit.cjs +33 -0
  84. package/lib/install/claude-hooks.cjs +145 -31
  85. package/lib/install/claude-hooks.test.cjs +160 -0
  86. package/lib/install/manifest.cjs +19 -0
  87. package/lib/install/manifest.test.cjs +107 -0
  88. package/lib/knowledge-adapter.cjs +3 -49
  89. package/lib/learnings.cjs +3 -108
  90. package/lib/logger.cjs +157 -0
  91. package/lib/logger.test.cjs +159 -0
  92. package/lib/memory-index-usearch.cjs +9 -12
  93. package/lib/memory-provider-local.cjs +8 -0
  94. package/lib/memory.cjs +86 -27
  95. package/lib/memory.test.cjs +135 -0
  96. package/lib/messaging.cjs +155 -83
  97. package/lib/metrics-aggregate.cjs +26 -27
  98. package/lib/metrics.cjs +7 -3
  99. package/lib/metrics.test.cjs +6 -5
  100. package/lib/migrations.cjs +89 -0
  101. package/lib/migrations.test.cjs +82 -0
  102. package/lib/milestone-meta.cjs +70 -0
  103. package/lib/nubosloop-audit.cjs +41 -141
  104. package/lib/nubosloop.cjs +45 -149
  105. package/lib/plan-lint.cjs +0 -67
  106. package/lib/researcher-swarm.cjs +1 -62
  107. package/lib/roadmap-render.cjs +107 -33
  108. package/lib/roadmap-schema.cjs +42 -0
  109. package/lib/roadmap.cjs +93 -20
  110. package/lib/roadmap.test.cjs +215 -0
  111. package/lib/run-context.cjs +54 -0
  112. package/lib/run-context.test.cjs +53 -0
  113. package/lib/runtime/index.cjs +5 -10
  114. package/lib/runtime/index.test.cjs +8 -1
  115. package/lib/safe-path.cjs +156 -0
  116. package/lib/safe-path.test.cjs +164 -0
  117. package/lib/security/ledger.cjs +203 -0
  118. package/lib/security/ledger.test.cjs +139 -0
  119. package/lib/security/patterns.cjs +119 -0
  120. package/lib/security/review.cjs +220 -0
  121. package/lib/security/review.test.cjs +143 -0
  122. package/lib/security/scan.cjs +180 -0
  123. package/lib/security/scan.test.cjs +137 -0
  124. package/lib/state.cjs +28 -10
  125. package/lib/state.test.cjs +72 -22
  126. package/lib/tasks.cjs +92 -14
  127. package/lib/tasks.test.cjs +65 -0
  128. package/lib/todo.cjs +7 -5
  129. package/lib/verify.cjs +44 -3
  130. package/lib/worktree.cjs +2 -2
  131. package/lib/yaml.cjs +44 -0
  132. package/lib/yaml.test.cjs +65 -0
  133. package/np-tools.cjs +26 -23
  134. package/package.json +5 -2
  135. package/templates/claude/payload/hooks/np-security-hook.cjs +50 -0
  136. package/workflows/execute-phase.md +11 -1
  137. 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`).
@@ -49,6 +49,25 @@ The orchestrator provides these in your prompt context. Read every path it hands
49
49
  | Task summary (write on completion) | You fill this after the commit lands — describes changes, verification, follow-ups. | `.nubos-pilot/milestones/M<NNN>/slices/S<NNN>/tasks/T<NNNN>/T<NNNN>-SUMMARY.md` |
50
50
  | Checkpoint file (managed) | Write-through state transitions via `np-tools.cjs checkpoint transition`. Do NOT read/write directly. | `.nubos-pilot/checkpoints/<task-full-id>.json` |
51
51
 
52
+ ## Write against the success_criteria
53
+
54
+ When the orchestrator includes a `<success_criteria>` block in your prompt, those criteria are the
55
+ milestone's **acceptance target** — what "done right" means. Use them as your north star while you
56
+ implement, not just the `verify` command. `verify` proves the code runs; the criteria prove it does
57
+ the *right* thing. Aim for both green.
58
+
59
+ - **Intent, not a build spec (ADR-0019).** Criteria say *what* must be true, never *how* to build it
60
+ (no schema/filename/style is implied). Don't treat a criterion as a licence to add structure the
61
+ task plan didn't ask for.
62
+ - **Stay in scope.** A criterion is **never** a reason to edit a path outside `files_modified`. If
63
+ satisfying it would require touching another file, that is a planner-scope bug — emit the
64
+ `## SCOPE EXPANSION REQUEST` block (step 4a) and hand back; do not expand scope.
65
+ - **Self-check before commit.** Before `commit-task`, re-read your diff against each criterion your
66
+ task contributes to (cross-reference the slice `S<NNN>-UAT.md`). If your in-scope change leaves a
67
+ criterion it should satisfy unmet, fix it within `files_modified` before committing — don't ship a
68
+ known gap for the critic to bounce back.
69
+ - Criteria outside your task's scope are context, not your responsibility — do not chase them.
70
+
52
71
  ## Codebase Docs Protocol (runtime-agnostic)
53
72
 
54
73
  nubos-pilot maintains a skill-style code documentation layer at
@@ -131,6 +150,7 @@ into the `task(…)` commit. If `workflow.commit_docs=true`, the
131
150
  <scope_guardrail>
132
151
  **Do:**
133
152
  - Edit only files enumerated in `files_modified`.
153
+ - Treat any `<success_criteria>` in your prompt as the acceptance target; self-check your diff against it before commit (see "Write against the success_criteria").
134
154
  - Commit via `node np-tools.cjs commit-task <task-id>`.
135
155
  - Write checkpoint state transitions via the wrapper.
136
156
  - Stay within the task's declared scope even if you spot tangential issues — log them, do not fix them.
@@ -1,15 +1,19 @@
1
1
  ---
2
2
  name: np-security-reviewer
3
- description: Read-only post-execution security audit for a milestone. Spawned by /np:validate-phase (or on demand) once all tasks of a milestone are committed. Scans every files_modified path against OWASP-aligned categories, emits M<NNN>-SECURITY.md draft with Pass/Risk/Defer per finding. Detection-only — never edits source.
3
+ description: Read-only security auditor with two input modes. Modus A (milestone): spawned by /np:validate-phase once a milestone's tasks are committed scans every files_modified path against OWASP-aligned categories and emits an M<NNN>-SECURITY.md draft with Pass/Risk/Defer per finding. Modus B (session/diff): spawned headlessly by the ADR-0020 in-session security hooks against a single turn-diff or commit — returns a JSON findings envelope as its final message. Detection-only in both modes — never edits source.
4
4
  tier: sonnet
5
5
  tools: Read, Bash, Grep, Glob
6
6
  color: red
7
7
  ---
8
8
 
9
9
  <role>
10
- You are the nubos-pilot security reviewer. Post-execution twin of `np-verifier` for the security surface. Spawned once a milestone's task commits are in place. You emit a `M<NNN>-SECURITY.md` draft with one block per finding, classified as `Pass` (no risk), `Risk` (concrete vulnerability), or `Defer` (needs user decision / out-of-scope).
10
+ You are the nubos-pilot security reviewer. Post-execution twin of `np-verifier` for the security surface. You run in one of two modes, decided by the prompt.
11
11
 
12
- You DO NOT propose patches. You DO NOT edit source. You report.
12
+ **Modus A milestone audit (default).** Spawned once a milestone's task commits are in place. You emit a `M<NNN>-SECURITY.md` draft with one block per finding, classified as `Pass` (no risk), `Risk` (concrete vulnerability), or `Defer` (needs user decision / out-of-scope).
13
+
14
+ **Modus B — session/diff (ADR-0020).** If the prompt contains a `<security_scan mode="…">` block, you operate in in-session mode: you review ONLY the supplied turn-diff (and, in `mode="commit"`, the surrounding code you reach via `Read`/`Grep`) and return a single JSON findings envelope as your **final message** — you do NOT write `M<NNN>-SECURITY.md`, do NOT use a milestone number, and do NOT read milestone files. See "## Session/Diff Mode (Modus B)" below for the exact contract.
15
+
16
+ You DO NOT propose patches. You DO NOT edit source. You report — in both modes.
13
17
 
14
18
  **CRITICAL: Mandatory Initial Read**
15
19
  If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
@@ -104,6 +108,48 @@ Milestone Status resolution:
104
108
  - Else any `Defer` → `deferred`.
105
109
  - Else → `clean`.
106
110
 
111
+ ## Session/Diff Mode (Modus B) — ADR-0020
112
+
113
+ Triggered when the prompt contains a `<security_scan mode="stop|commit">` block. This is the in-session
114
+ review spawned by the security hooks. It is independent by construction: you receive only the diff and a
115
+ fresh context — you never graded the code you are reviewing.
116
+
117
+ **Inputs (all inside the `<security_scan>` block):**
118
+ - The list of changed files and the diff under review.
119
+ - `mode="stop"` — review only what the turn changed; start from the diff, do not hunt outside it.
120
+ - `mode="commit"` — a deeper pass: use `Read`/`Grep`/`Glob` to inspect surrounding code (callers,
121
+ sanitizers, related files) before deciding a finding is real, to keep false positives low.
122
+ - An optional project guidance block. It is **additive** — it adds checks on top of the built-in OWASP
123
+ categories and never disables them. `RULES.md`/`CONTEXT.md` (if referenced) still authorize/neutralize
124
+ a finding the same way as Modus A.
125
+
126
+ **Behaviour:**
127
+ - Apply the same OWASP-aligned categories as Modus A.
128
+ - Report ONLY concrete `Risk` findings. Omit `Pass`/no-risk entries entirely.
129
+ - Do NOT write any file. Do NOT edit source. Do NOT spawn agents. Do NOT use a milestone number.
130
+
131
+ **Output contract — your FINAL message MUST be exactly one JSON object, no prose, no code fence:**
132
+
133
+ ```json
134
+ {
135
+ "status": "clean | risks-found",
136
+ "findings": [
137
+ {
138
+ "category": "Injection | Auth & Session | Access Control | Crypto | SSRF / Open Redirect | Deserialization | File / Path | Secrets | Logging | Dependencies",
139
+ "severity": "high | medium | low",
140
+ "file": "relative/path.ext",
141
+ "line": 42,
142
+ "title": "short finding title",
143
+ "evidence": "the matched line / why it is exploitable",
144
+ "mitigation_hint": "the real fix (a pointer, not a patch)"
145
+ }
146
+ ]
147
+ }
148
+ ```
149
+
150
+ If you find nothing, return `{"status":"clean","findings":[]}`. The orchestrator surfaces and fixes these
151
+ findings as a follow-up in the same conversation — it never blocks the write or commit.
152
+
107
153
  ## Handoff Protocol
108
154
 
109
155
  Before reviewing, check handoffs addressed to `np-security-reviewer`:
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 _readExistingScope(projectRoot) {
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
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
205
- return cfg && cfg.scope ? cfg.scope : null;
206
- } catch { return null; }
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 cfgPath = path.join(_stateDirFor(projectRoot), 'config.json');
211
- if (!fs.existsSync(cfgPath)) return null;
212
- try {
213
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
214
- if (Array.isArray(cfg.runtimes) && cfg.runtimes.length) return cfg.runtimes.slice();
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
- try { fs.unlinkSync(path.join(payloadDir, rel)); } catch {}
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
  }
@@ -555,10 +610,15 @@ async function _runInstallLocked(ctx) {
555
610
  try {
556
611
  const claudeHooks = require('../lib/install/claude-hooks.cjs');
557
612
  const res = claudeHooks.installClaudeHooks({
558
- projectRoot, scope: resolvedScope, which: 'both', force: false,
613
+ projectRoot, scope: resolvedScope, which: 'all', force: false,
559
614
  });
615
+ const secAction = res.results.security
616
+ ? Object.values(res.results.security).every((r) => r.action === 'installed') ? 'installed'
617
+ : Object.values(res.results.security).every((r) => r.action === 'updated') ? 'updated' : 'mixed'
618
+ : 'skipped';
560
619
  console.error(dim + ' [claude-hooks] statusline: ' + res.results.statusline.action
561
- + ', ctx-monitor: ' + res.results.ctxMonitor.action + reset);
620
+ + ', ctx-monitor: ' + res.results.ctxMonitor.action
621
+ + ', security: ' + secAction + reset);
562
622
  if (res.results.statusline.action === 'skipped-existing') {
563
623
  console.error(yellow + ' [claude-hooks] foreign statusLine preserved — re-run `install-hooks --force` to overwrite' + reset);
564
624
  }
@@ -594,14 +654,11 @@ function _runUninstallLocked(projectRoot) {
594
654
  return { uninstalled: false };
595
655
  }
596
656
 
657
+ // Reuse the SAME validator as readManifest so a legitimate key like
658
+ // `..bar` (no traversal segment) isn't false-rejected here while passing
659
+ // validation upstream. Single source of truth lives in manifest.cjs.
597
660
  for (const rel of Object.keys(manifest.files)) {
598
- if (rel.includes('..') || path.isAbsolute(rel)) {
599
- throw new NubosPilotError(
600
- 'manifest-path-traversal',
601
- 'Manifest contains suspicious path',
602
- { rel },
603
- );
604
- }
661
+ manifestMod.assertSafeManifestKey(rel, 'uninstall');
605
662
  }
606
663
 
607
664
  const payloadBase = scope === 'global' ? os.homedir() : projectRoot;
@@ -612,6 +669,20 @@ function _runUninstallLocked(projectRoot) {
612
669
  const abs = rel.startsWith('~/')
613
670
  ? path.join(os.homedir(), rel.slice(2))
614
671
  : isAsset ? path.join(payloadBase, rel) : path.join(payloadDir, rel);
672
+ // Defense-in-depth: even with the validator above, ensure the resolved
673
+ // path lives inside its expected base. A symlink or future-validator
674
+ // regression cannot escape this prefix check.
675
+ const expectedBase = rel.startsWith('~/') ? os.homedir()
676
+ : isAsset ? payloadBase : payloadDir;
677
+ const resolvedAbs = path.resolve(abs);
678
+ const resolvedBase = path.resolve(expectedBase);
679
+ if (!(resolvedAbs === resolvedBase || resolvedAbs.startsWith(resolvedBase + path.sep))) {
680
+ throw new NubosPilotError(
681
+ 'manifest-unlink-outside-base',
682
+ 'Refusing unlink that escapes its payload base',
683
+ { rel, base: path.basename(expectedBase) },
684
+ );
685
+ }
615
686
  try {
616
687
  fs.unlinkSync(abs);
617
688
  removed++;
@@ -639,12 +710,11 @@ function _runUninstallLocked(projectRoot) {
639
710
 
640
711
  try { fs.rmdirSync(payloadDir); } catch {}
641
712
 
642
- const cfgPath = path.join(_stateDirFor(projectRoot), 'config.json');
643
713
  let installedRuntimes = [];
644
- try {
645
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
714
+ const cfg = _readInstallConfig(projectRoot);
715
+ if (cfg) {
646
716
  installedRuntimes = cfg.runtimes || (cfg.runtime ? [cfg.runtime] : []);
647
- } catch {}
717
+ }
648
718
 
649
719
  const legacyFiles = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
650
720
  const extraFiles = [];
@@ -793,21 +863,21 @@ async function runUninstallHooks(opts) {
793
863
  }
794
864
 
795
865
  if (require.main === module) {
796
- main().catch((err) => {
797
- if (err && err.code) {
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
- }
866
+ if (Number(process.versions.node.split('.')[0]) < 22) {
867
+ process.stderr.write('nubos-pilot: requires Node >= 22 (running ' + process.versions.node + ')\n');
810
868
  process.exit(1);
869
+ }
870
+ installSignalCleanup();
871
+ main().catch((err) => {
872
+ const payload = (err && err.code)
873
+ ? JSON.stringify({ error: { code: err.code, message: err.message, details: err.details || null } }) + '\n'
874
+ : ((err && err.stack) || String(err)) + '\n';
875
+ // Drain stderr before exit. process.exit() can otherwise tear down the
876
+ // pipe mid-flush on busy CI, truncating the envelope. Set exitCode and
877
+ // let Node drain naturally; force-exit only as a last-resort fallback.
878
+ try { process.stderr.write(payload); } catch {}
879
+ process.exitCode = 1;
880
+ setTimeout(() => process.exit(1), 1000).unref();
811
881
  });
812
882
  }
813
883
 
@@ -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
- return idx !== -1 ? rest[idx + 1] : undefined;
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) {
@@ -96,6 +96,7 @@ const COMMANDS = [
96
96
  { name: 'loop-audit-tool-use', category: 'Execution', description: 'Record/read the tool-use audit per spawn (Completeness Rule 9 mechanical check)', description_de: 'Tool-use Audit pro Spawn schreiben/lesen (Completeness Rule 9 mechanische Prüfung)' },
97
97
  { name: 'loop-stuck', category: 'Execution', description: 'Mark a task as stuck (writes loop-state + flips checkpoint status to stuck)', description_de: 'Markiert Task als stuck (schreibt Loop-State + setzt Checkpoint-Status auf stuck)' },
98
98
  { name: 'spawn-headless', category: 'Execution', description: 'Spawn an agent as a headless `claude -p` subprocess (ADR-0010 §L6); writes stdout to --output-path and returns exit code', description_de: 'Spawnt einen Agent als headless `claude -p` Subprozess (ADR-0010 §L6); schreibt stdout nach --output-path und liefert Exit-Code' },
99
+ { name: 'security', category: 'Review', description: 'In-session security review hook backend (ADR-0020). Verbs: session-start | baseline | scan | review | commit | run-review. Reads the Claude Code hook payload via --stdin; non-blocking, report-once, independent reviewer spawn.', description_de: 'Backend für die In-Session-Security-Review-Hooks (ADR-0020). Verben: session-start | baseline | scan | review | commit | run-review. Liest die Claude-Code-Hook-Payload via --stdin; non-blocking, report-once, unabhängiger Reviewer-Spawn.' },
99
100
  { name: 'loop-metrics', category: 'Utility', description: 'Aggregate Nubosloop telemetry across all checkpoints (commits, stuck, route distribution)', description_de: 'Aggregiert Nubosloop-Telemetrie über alle Checkpoints (Commits, Stuck, Routing)' },
100
101
  { name: 'learning-log', category: 'Execution', description: 'Persist a learning to the local store (or MCP adapter when configured)', description_de: 'Persistiert ein Learning im lokalen Store (oder MCP-Adapter falls konfiguriert)' },
101
102
  { name: 'learning-match', category: 'Utility', description: 'Query the learnings store for cached patterns matching a free-text query', description_de: 'Fragt den Learnings-Store nach Cached-Patterns ab' },
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { NubosPilotError } = require('../../lib/core.cjs');
4
- const { readConfigPath } = require('../../lib/config.cjs');
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 = readConfigPath(cwd, 'memory.enabled', false);
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 = readConfigPath(cwd, 'memory.model', 'Xenova/bge-small-en-v1.5');
30
- const alpha = readConfigPath(cwd, 'memory.alpha', 0.6);
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,5 +1,5 @@
1
1
  const { NubosPilotError } = require('../../lib/core.cjs');
2
- const { TASK_ID_RE } = require('../../lib/tasks.cjs');
2
+ const { TASK_ID_RE } = require('../../lib/ids.cjs');
3
3
  const {
4
4
  startTask,
5
5
  writeCheckpoint,
@@ -1,36 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('node:fs');
4
- const path = require('node:path');
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
- _emit(payload, stdout, cwd);
45
+ emitInitPayload(payload, stdout, cwd, 'close-project');
72
46
  return payload;
73
47
  }
74
48
  case 'check': {