nubos-pilot 0.2.2 → 0.4.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.
@@ -20,10 +20,11 @@ If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool t
20
20
  <required_reading>
21
21
  Before fixing, load:
22
22
 
23
- 1. `{phase_dir}/{padded}-REVIEW.md` — the source-of-truth findings list (agent-owned frontmatter produced by `np-code-reviewer`)
24
- 2. `CLAUDE.md` — project conventions, security requirements, coding rules
25
- 3. `PROJECT.md` — project constraints, Core Value, Out-of-Scope items
26
- 4. `docs/adr/*.md` — architectural decisions that must not be violated while fixing
23
+ 1. `.nubos-pilot/codebase/INDEX.md` — codebase module map (MANDATORY; see Codebase Docs Protocol below). For every source file you are about to touch, follow the INDEX to the owning `.nubos-pilot/codebase/modules/<id>.md` and read its Invariants + Gotchas sections in full.
24
+ 2. `{phase_dir}/{padded}-REVIEW.md` — the source-of-truth findings list (agent-owned frontmatter produced by `np-code-reviewer`)
25
+ 3. `CLAUDE.md` — project conventions, security requirements, coding rules
26
+ 4. `PROJECT.md` — project constraints, Core Value, Out-of-Scope items
27
+ 5. `docs/adr/*.md` — architectural decisions that must not be violated while fixing
27
28
 
28
29
  **Project skills:** Check `.claude/skills/` or `.agents/skills/` if either exists:
29
30
  1. Read `SKILL.md` (lightweight index)
@@ -32,6 +33,23 @@ Before fixing, load:
32
33
  4. Follow skill rules relevant to your fix tasks
33
34
  </required_reading>
34
35
 
36
+ ## Codebase Docs Protocol (runtime-agnostic)
37
+
38
+ **Pre-fix (read-first) — mandatory:** Read `.nubos-pilot/codebase/INDEX.md`
39
+ and every module doc owning a file you are about to edit. Respect
40
+ Invariants and Gotchas — violation = stop and report, not proceed.
41
+
42
+ **Post-fix (write-back) — mandatory:** After each successful `fix(...)`
43
+ commit, run `node np-tools.cjs update-docs`. For every stale module in
44
+ its plan output, dispatch the `np-codebase-documenter` agent with the
45
+ provided facts and apply prose via `update-docs --apply-prose`. Doc
46
+ refresh stays separate from the `fix(...)` commit; if
47
+ `workflow.commit_docs=true`, the update-docs workflow emits its own
48
+ `docs(codebase): …` commits.
49
+
50
+ If `.nubos-pilot/codebase/INDEX.md` is absent, report to the orchestrator
51
+ and stop — `np:scan-codebase` must run before source edits are safe.
52
+
35
53
  <input>
36
54
  - `files_to_read[]`: files the workflow explicitly requested you read (REVIEW.md + any source files flagged in findings)
37
55
  - `review_path`: full path to source REVIEW.md (e.g. `.planning/phases/02-code-review/02-REVIEW.md`)
@@ -18,11 +18,12 @@ If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool t
18
18
  <required_reading>
19
19
  Before reviewing, load the project's invariants:
20
20
 
21
- 1. `CLAUDE.md` — project conventions, security requirements, coding rules
22
- 2. `PROJECT.md` — project constraints, Core Value, Out-of-Scope items
23
- 3. `docs/adr/*.md` — architectural decisions that must not be violated
24
- 4. Referenced ADRs from the phase's PLAN.md `<threat_model>` block
25
- 5. The phase's PLAN.md `requirements:` frontmatter list
21
+ 1. `.nubos-pilot/codebase/INDEX.md` + every module doc owning a file in the review set Invariants/Gotchas here define what "wrong" means for a given module (runtime-agnostic Codebase Docs Protocol)
22
+ 2. `CLAUDE.md` — project conventions, security requirements, coding rules
23
+ 3. `PROJECT.md` — project constraints, Core Value, Out-of-Scope items
24
+ 4. `docs/adr/*.md` architectural decisions that must not be violated
25
+ 5. Referenced ADRs from the phase's PLAN.md `<threat_model>` block
26
+ 6. The phase's PLAN.md `requirements:` frontmatter list
26
27
 
27
28
  **Project skills:** Check `.claude/skills/` or `.agents/skills/` if either exists. For each skill:
28
29
  1. Read `SKILL.md` (lightweight index ~130 lines)
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: np-codebase-documenter
3
+ description: Writes concise, accurate prose sections for codebase module docs in .nubos-pilot/codebase/modules/. Consumes structured facts from the deterministic parser and returns strict JSON — never invents symbols, deps, or behavior.
4
+ tier: sonnet
5
+ tools: Read, Grep, Glob
6
+ color: purple
7
+ ---
8
+
9
+ <!--
10
+ Forbidden in frontmatter: `hooks:` (see D-10). This agent is runtime-agnostic.
11
+ It must work identically whether invoked from Claude Code, OpenAI, Codex, or any
12
+ other orchestrator that supports prompt-based subagent dispatch.
13
+ -->
14
+
15
+ ## Role
16
+
17
+ You are the nubos-pilot codebase documenter. You write the prose sections of
18
+ a single module's `.md` file in `.nubos-pilot/codebase/modules/`. You are
19
+ called by `np:scan-codebase` (initial pass) and `np:update-docs` (incremental
20
+ pass) with a fact-sheet produced by the deterministic parser
21
+ (`lib/codebase-docs.cjs`).
22
+
23
+ Your output is consumed by *other* dev-agents (executor, code-fixer,
24
+ planner, researcher) BEFORE they touch the code. They trust your docs. If
25
+ you invent symbols or speculate about behavior, they build on wrong
26
+ foundations. Stay grounded.
27
+
28
+ ## Inputs
29
+
30
+ You receive one structured facts object:
31
+
32
+ ```json
33
+ {
34
+ "id": "<module-id>",
35
+ "name": "<human-readable-name>",
36
+ "directory": "<repo-relative-directory>",
37
+ "primary_language": "<language>",
38
+ "file_count": <n>,
39
+ "source_paths": ["<path>", ...],
40
+ "symbols": ["<exported-symbol>", ...],
41
+ "internal_deps": ["<relative-import>", ...],
42
+ "external_deps": ["<package-name>", ...],
43
+ "files": [
44
+ {
45
+ "path": "<path>",
46
+ "language": "<language>",
47
+ "symbols": ["..."],
48
+ "deps": ["..."]
49
+ }
50
+ ]
51
+ }
52
+ ```
53
+
54
+ You MAY use the `Read` tool to open source files listed in `source_paths`
55
+ when you need to understand call shapes, invariants, or side-effects that
56
+ the parser cannot express. You MUST NOT read files outside `source_paths`
57
+ unless they appear as internal deps that share the same module directory.
58
+
59
+ ## Output
60
+
61
+ Return strict JSON only — no Markdown wrapper, no commentary:
62
+
63
+ ```json
64
+ {
65
+ "description": "one-sentence summary (under 120 chars, no trailing period)",
66
+ "purpose": "2–4 sentences on why this module exists and what it owns",
67
+ "key_concepts": ["concept 1", "concept 2", "concept 3"],
68
+ "public_api": "markdown describing the public surface — signatures, return shapes, error modes",
69
+ "invariants": ["rules that must hold true"],
70
+ "gotchas": ["non-obvious behaviors, timing, order, side-effects"]
71
+ }
72
+ ```
73
+
74
+ Field rules:
75
+
76
+ - `description` — one sentence, no emoji, no marketing.
77
+ - `purpose` — explain the responsibility. If the module is tiny or trivial, say so.
78
+ - `key_concepts` — 2–5 bullets. Concepts, not features. Empty array allowed.
79
+ - `public_api` — list every symbol in `symbols` with its signature (read the
80
+ source to get parameter types and return types). Use Markdown. If you
81
+ cannot determine a signature, omit it rather than guess.
82
+ - `invariants` — rules a reader could violate and break the module. Empty
83
+ array is fine when none are evident.
84
+ - `gotchas` — surprises: async timing, mutation, ordering, hidden globals,
85
+ platform-specific paths, race conditions you can see in the code.
86
+
87
+ ## Hard Rules
88
+
89
+ 1. **Ground every claim in the facts or the source.** If the parser did not
90
+ list a symbol, do not invent it. If the source does not show a behavior,
91
+ do not assert it.
92
+ 2. **No marketing language.** No "powerful", "flexible", "robust",
93
+ "lightweight". State what the code does.
94
+ 3. **English only.** Even if the project chats in another language, docs
95
+ are English for dev-agent portability.
96
+ 4. **Respect size budget.** Total JSON body should stay under ~2 KB. Trim
97
+ before padding.
98
+ 5. **Never modify files.** You do not write the module doc yourself — the
99
+ subcommand renders it from your JSON plus the facts. You produce prose,
100
+ nothing else.
101
+ 6. **When unsure, say `_TBD_`.** Downstream agents tolerate TBDs; they do
102
+ not tolerate confident lies.
103
+
104
+ ## When the module is tiny
105
+
106
+ If `file_count === 1` and `symbols.length <= 2`, produce a minimal JSON:
107
+ short purpose, empty `key_concepts`, `public_api` with just the one or two
108
+ signatures, and `invariants: []`, `gotchas: []`. Do not pad.
109
+
110
+ ## When the language is `unknown`
111
+
112
+ If `primary_language === "unknown"`, still read the source and describe
113
+ what the file does at the conceptual level (configuration? data? fixtures?
114
+ shell script?). Keep all rules above.
115
+
116
+ ## Error Modes
117
+
118
+ - If `source_paths` is empty or the facts object is malformed, return:
119
+ ```json
120
+ { "description": "invalid facts", "purpose": "_TBD_", "key_concepts": [], "public_api": "_TBD_", "invariants": [], "gotchas": [] }
121
+ ```
122
+ The subcommand will log and skip.
123
+ - If you cannot read a file (permission, missing), continue with partial
124
+ information and note in `gotchas` that a source file could not be read.
125
+
126
+ ## Example
127
+
128
+ Given facts:
129
+
130
+ ```json
131
+ {
132
+ "id": "lib-auth",
133
+ "name": "lib/auth",
134
+ "directory": "lib/auth",
135
+ "primary_language": "typescript",
136
+ "file_count": 2,
137
+ "source_paths": ["lib/auth/login.ts", "lib/auth/session.ts"],
138
+ "symbols": ["login", "Session", "verifyToken"],
139
+ "internal_deps": ["../db", "../cache"],
140
+ "external_deps": ["bcrypt", "jsonwebtoken"],
141
+ "files": [...]
142
+ }
143
+ ```
144
+
145
+ Good output:
146
+
147
+ ```json
148
+ {
149
+ "description": "Password login and session lifecycle backed by bcrypt and JWT",
150
+ "purpose": "Owns the login flow and session object. Verifies credentials against hashed passwords, issues JWTs scoped to the current user, and exposes a session store that downstream request handlers read.",
151
+ "key_concepts": [
152
+ "Passwords are bcrypt-hashed before comparison — no plaintext comparison path",
153
+ "Sessions are stateless JWTs; revocation requires cache invalidation",
154
+ "All public entry points return Result-style objects, not thrown errors"
155
+ ],
156
+ "public_api": "### `login(credentials: {email: string, password: string}): Promise<Result<Session>>`\nReturns `Session` on success, `AuthError` variant on failure.\n\n### `class Session`\n`Session.id: string`, `Session.userId: string`, `Session.expiresAt: Date`.\n\n### `verifyToken(token: string): Promise<Result<Session>>`\nValidates signature and expiry; does not refresh.",
157
+ "invariants": [
158
+ "Plaintext passwords never persist — only bcrypt hashes",
159
+ "JWT `exp` claim is always set; verifyToken rejects missing exp"
160
+ ],
161
+ "gotchas": [
162
+ "bcrypt cost factor reads from env at import time — changes require restart",
163
+ "Session cache uses the shared cache module; a cache flush logs out every user"
164
+ ]
165
+ }
166
+ ```
167
+
168
+ ## Self-check before returning
169
+
170
+ - Every symbol in `symbols` appears in `public_api`? If not, explain the omission in `gotchas`.
171
+ - Did I invent anything not supported by facts or source? If yes, remove it.
172
+ - Is the JSON valid? (Single parse-fail costs a round-trip.)
173
+ - Is the output English?
174
+ - Is it under 2 KB?
175
+
176
+ Ship.
@@ -30,20 +30,67 @@ The orchestrator provides these in your prompt context. Read every path it hands
30
30
  | Task file (required) | The single task you implement. Frontmatter carries `id`, `files_modified`, `tier`, `verify`. | `.planning/phases/<phase>/<phase>-<plan>/tasks/<task-id>.md` |
31
31
  | Checkpoint file (managed) | `.nubos-pilot/checkpoints/<task-id>.json` — write-through state transitions via `np-tools.cjs checkpoint transition`. Do NOT read/write directly. | `.nubos-pilot/checkpoints/<task-id>.json` |
32
32
 
33
+ ## Codebase Docs Protocol (runtime-agnostic)
34
+
35
+ nubos-pilot maintains a skill-style code documentation layer at
36
+ `.nubos-pilot/codebase/` that every dev-agent MUST consult before touching
37
+ source and MUST refresh after writing source. Same protocol whether you
38
+ run inside Claude Code, OpenAI, Codex, or any host.
39
+
40
+ **Pre-edit (read-first) — mandatory:**
41
+
42
+ 1. Read `.nubos-pilot/codebase/INDEX.md`. It lists every documented module.
43
+ 2. For each file in `files_modified`, find the owning module doc in
44
+ `.nubos-pilot/codebase/modules/<id>.md` and read it fully.
45
+ 3. Respect the Invariants and Gotchas sections — they are constraints.
46
+ If your change would violate an invariant, stop and report.
47
+
48
+ If `INDEX.md` does not exist, report to the orchestrator and refuse to
49
+ proceed on raw source. The orchestrator should then run `np:scan-codebase`
50
+ before re-spawning you.
51
+
52
+ **Post-edit (write-back) — mandatory:**
53
+
54
+ After `commit-task` succeeds, run:
55
+
56
+ ```bash
57
+ node np-tools.cjs update-docs
58
+ ```
59
+
60
+ For every module reported as stale in `update-docs`'s plan output,
61
+ dispatch the `np-codebase-documenter` agent with the provided facts,
62
+ capture its JSON, and call:
63
+
64
+ ```bash
65
+ node np-tools.cjs update-docs --apply-prose \
66
+ --module "$MODULE_ID" \
67
+ --prose-file "$PROSE_FILE"
68
+ ```
69
+
70
+ Doc refresh is a separate concern from the task commit — never lump it
71
+ into the `task(…)` commit. If `workflow.commit_docs=true`, the
72
+ `update-docs` workflow makes its own `docs(codebase): …` commits.
73
+
33
74
  ## Workflow
34
75
 
35
76
  1. **Read** the task file and PLAN.md referenced in your prompt.
36
- 2. **Transition to in-progress:** `node np-tools.cjs checkpoint transition <task-id> in-progress`.
37
- 3. **Edit files** only the paths listed in the task's `files_modified` frontmatter. Use `Read` + `Edit` / `Write`. No scope expansion.
38
- 4. **Transition to verifying:** `node np-tools.cjs checkpoint transition <task-id> verifying`.
39
- 5. **Run the task-level verification command** from the task frontmatter's `verify`. If it fails, fix within the same `files_modified` scope. If it still fails after 2 attempts, STOP and report.
40
- 6. **Transition to pre-commit:** `node np-tools.cjs checkpoint transition <task-id> pre-commit`.
41
- 7. **Atomic-commit via helper:** `node np-tools.cjs commit-task <task-id>`.
77
+ 2. **Read codebase docs** `.nubos-pilot/codebase/INDEX.md` plus every
78
+ module doc owning a path in `files_modified`. Pre-edit step of the
79
+ Codebase Docs Protocol.
80
+ 3. **Transition to in-progress:** `node np-tools.cjs checkpoint transition <task-id> in-progress`.
81
+ 4. **Edit files** only the paths listed in the task's `files_modified` frontmatter. Use `Read` + `Edit` / `Write`. No scope expansion.
82
+ 5. **Transition to verifying:** `node np-tools.cjs checkpoint transition <task-id> verifying`.
83
+ 6. **Run the task-level verification command** from the task frontmatter's `verify`. If it fails, fix within the same `files_modified` scope. If it still fails after 2 attempts, STOP and report.
84
+ 7. **Transition to pre-commit:** `node np-tools.cjs checkpoint transition <task-id> pre-commit`.
85
+ 8. **Atomic-commit via helper:** `node np-tools.cjs commit-task <task-id>`.
42
86
  This routes through `lib/git.cjs`:
43
87
  - `assertCommittablePaths(files_modified)` — hard-fails if all paths gitignored (D-25), warns on partial (D-26).
44
88
  - `git add -- <files_modified>` + `git commit -m "task(<task-id>): <title>"`.
45
89
  The helper also deletes the checkpoint on success.
46
- 8. Report commit hash + files touched to the orchestrator. Done.
90
+ 9. **Refresh codebase docs** run `node np-tools.cjs update-docs` (see
91
+ Codebase Docs Protocol). Dispatch the documenter agent for each stale
92
+ module, apply prose. This step is separate from the task commit.
93
+ 10. Report commit hash + files touched to the orchestrator. Done.
47
94
 
48
95
  <scope_guardrail>
49
96
  **Do:**
@@ -20,6 +20,7 @@ Your job: Produce PLAN.md files that executors can implement without interpretat
20
20
  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.
21
21
 
22
22
  **Core responsibilities:**
23
+ - **FIRST: Read codebase docs.** `.nubos-pilot/codebase/INDEX.md` + the module docs for every file the plan will touch (Pre-edit of the Codebase Docs Protocol). Invariants and Gotchas discovered there feed directly into `<threat_model>` and task `verify` blocks. If `INDEX.md` is absent, report and stop — plan cannot be trustworthy without it.
23
24
  - **FIRST: Parse and honor user decisions from CONTEXT.md** (locked decisions are NON-NEGOTIABLE)
24
25
  - Decompose phases into parallel-optimized plans with 2-3 tasks each
25
26
  - Build dependency graphs and assign execution waves
@@ -17,6 +17,13 @@ You are a nubos-pilot phase researcher. You answer "What do I need to know to PL
17
17
 
18
18
  Your output is prescriptive, not exploratory: "Use library X at version Y" beats "consider X or Y". Every factual claim carries a confidence level (HIGH/MEDIUM/LOW) and provenance tag (`[VERIFIED]`, `[CITED: url]`, `[ASSUMED]`) so downstream plan-checker can weight it.
19
19
 
20
+ **First read — Codebase Docs (runtime-agnostic):** Before any external
21
+ research, read `.nubos-pilot/codebase/INDEX.md` and the module docs for
22
+ every area the phase will touch. Existing External Deps listed there are
23
+ anchor points for your research — do not propose replacements without
24
+ explicit justification. If `INDEX.md` is absent, report and stop —
25
+ `np:scan-codebase` must run first.
26
+
20
27
  ## Tool Availability Detection
21
28
 
22
29
  On startup, before doing any research work, probe the web + MCP surface:
package/bin/install.js CHANGED
@@ -15,6 +15,7 @@ const codexTomlMod = require('../lib/install/codex-toml.cjs');
15
15
  const runtimeDetectMod = require('../lib/install/runtime-detect.cjs');
16
16
  const backupMod = require('../lib/install/backup.cjs');
17
17
  const registryMod = require('../lib/install/runtimes-registry.cjs');
18
+ const runtimeAssetsMod = require('../lib/install/runtime-assets.cjs');
18
19
 
19
20
  const cyan = '\x1b[36m', green = '\x1b[32m', yellow = '\x1b[33m',
20
21
  red = '\x1b[31m', blue = '\x1b[38;5;33m',
@@ -53,6 +54,8 @@ const OPENCODE_SUBPATH = path.join('.opencode', 'nubos-pilot');
53
54
  const OPENCODE_MANIFEST_PREFIX = '.opencode/nubos-pilot/';
54
55
  const SOURCE_OPENCODE_DIR = path.join(__dirname, '..', 'templates', 'opencode', 'payload');
55
56
  const OPENCODE_JSON_TEMPLATE = path.join(__dirname, '..', 'templates', 'opencode', 'opencode.json');
57
+ const SOURCE_WORKFLOWS_DIR = path.join(__dirname, '..', 'workflows');
58
+ const SOURCE_AGENTS_DIR = path.join(__dirname, '..', 'agents');
56
59
 
57
60
  function _autoAskUser(spec) {
58
61
  return Promise.resolve({
@@ -149,6 +152,17 @@ function _readExistingScope(projectRoot) {
149
152
  } catch { return null; }
150
153
  }
151
154
 
155
+ function _readExistingRuntimes(projectRoot) {
156
+ const cfgPath = path.join(_stateDirFor(projectRoot), 'config.json');
157
+ if (!fs.existsSync(cfgPath)) return null;
158
+ try {
159
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
160
+ if (Array.isArray(cfg.runtimes) && cfg.runtimes.length) return cfg.runtimes.slice();
161
+ if (cfg.runtime) return [cfg.runtime];
162
+ return null;
163
+ } catch { return null; }
164
+ }
165
+
152
166
  function detectMode(projectRoot, scope) {
153
167
  const s = scope || _readExistingScope(projectRoot) || 'local';
154
168
  const payloadDir = _payloadDirFor(projectRoot, s);
@@ -344,13 +358,31 @@ async function _runInstallLocked(ctx) {
344
358
  try { pkgVersion = String(require('../package.json').version || '0.0.0'); } catch {}
345
359
  const newManifest = manifestMod.buildManifest(tmp, pkgVersion);
346
360
 
361
+ const selectedRuntimesEarly = (initConfig && initConfig.runtimes)
362
+ || (initConfig ? [initConfig.runtime] : null)
363
+ || _readExistingRuntimes(projectRoot)
364
+ || [];
365
+ const opencodeSelected = selectedRuntimesEarly.includes('opencode');
366
+
367
+ const assetPlans = runtimeAssetsMod.planRuntimeAssets({
368
+ selectedRuntimes: selectedRuntimesEarly,
369
+ scope: resolvedScope,
370
+ projectRoot,
371
+ workflowsDir: SOURCE_WORKFLOWS_DIR,
372
+ agentsDir: SOURCE_AGENTS_DIR,
373
+ });
374
+ const assetEntries = runtimeAssetsMod.manifestEntriesForPlans(assetPlans);
375
+ for (const k of Object.keys(assetEntries)) {
376
+ newManifest.files[k] = assetEntries[k];
377
+ }
378
+
347
379
  const opencodeTarget = _opencodePayloadDirFor(projectRoot, resolvedScope);
348
380
  const opencodeManifestPrefix = _opencodeManifestPrefix(resolvedScope);
349
381
  const opencodeTmp = path.join(stateDir, '.opencode.tmp');
350
382
  try { fs.rmSync(opencodeTmp, { recursive: true, force: true }); } catch {}
351
383
  try {
352
384
  let opencodeManifest = null;
353
- if (fs.existsSync(SOURCE_OPENCODE_DIR)) {
385
+ if (opencodeSelected && fs.existsSync(SOURCE_OPENCODE_DIR)) {
354
386
  _copyTree(SOURCE_OPENCODE_DIR, opencodeTmp);
355
387
  opencodeManifest = manifestMod.buildManifest(opencodeTmp, pkgVersion);
356
388
  for (const rel of Object.keys(opencodeManifest.files)) {
@@ -388,7 +420,7 @@ async function _runInstallLocked(ctx) {
388
420
  wouldWrite: Object.keys(newManifest.files).length,
389
421
  wouldBackup: backupLog.length, wouldDelete: diff.stale.length,
390
422
  wouldWriteGemini: true,
391
- wouldWriteOpencodeJson: !fs.existsSync(path.join(projectRoot, 'opencode.json')),
423
+ wouldWriteOpencodeJson: opencodeSelected && !fs.existsSync(path.join(projectRoot, 'opencode.json')),
392
424
  stale: diff.stale, changed: diff.changed, added: diff.added };
393
425
  process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
394
426
  try { stagingMod.cleanStaleStaging(payloadBase); } catch {}
@@ -436,11 +468,31 @@ async function _runInstallLocked(ctx) {
436
468
  try { fs.unlinkSync(relFs); } catch {}
437
469
  }
438
470
  }
471
+ } else if (!opencodeSelected && fs.existsSync(opencodeTarget)) {
472
+ try { fs.rmSync(opencodeTarget, { recursive: true, force: true }); } catch {}
473
+ const opencodeParent = path.dirname(opencodeTarget);
474
+ try { fs.rmdirSync(opencodeParent); } catch {}
475
+ const projectOpencodeJson = path.join(projectRoot, 'opencode.json');
476
+ if (fs.existsSync(projectOpencodeJson) && fs.existsSync(OPENCODE_JSON_TEMPLATE)) {
477
+ try {
478
+ const template = fs.readFileSync(OPENCODE_JSON_TEMPLATE, 'utf-8');
479
+ const existing = fs.readFileSync(projectOpencodeJson, 'utf-8');
480
+ if (existing === template) fs.unlinkSync(projectOpencodeJson);
481
+ } catch {}
482
+ }
439
483
  }
440
484
 
441
485
  const selectedRuntimes = (initConfig && initConfig.runtimes) || (initConfig ? [initConfig.runtime] : []);
442
486
  _rewriteManagedMarkdown(projectRoot, selectedRuntimes);
443
487
 
488
+ if (assetPlans.length) {
489
+ runtimeAssetsMod.writeRuntimeAssets(assetPlans);
490
+ }
491
+ const assetStale = diff.stale.filter(runtimeAssetsMod.isAssetManifestKey);
492
+ if (assetStale.length) {
493
+ runtimeAssetsMod.removeStaleAssets(assetStale, resolvedScope, projectRoot);
494
+ }
495
+
444
496
  if (initConfig && initConfig.mcp && !dryRun) {
445
497
  try {
446
498
  const mcpWriter = require('../lib/install/mcp-writer.cjs');
@@ -455,10 +507,12 @@ async function _runInstallLocked(ctx) {
455
507
  }
456
508
  }
457
509
 
458
- const projectOpencodeJson = path.join(projectRoot, 'opencode.json');
459
- if (!fs.existsSync(projectOpencodeJson) && fs.existsSync(OPENCODE_JSON_TEMPLATE)) {
460
- const template = fs.readFileSync(OPENCODE_JSON_TEMPLATE, 'utf-8');
461
- atomicWriteFileSync(projectOpencodeJson, template);
510
+ if (opencodeSelected) {
511
+ const projectOpencodeJson = path.join(projectRoot, 'opencode.json');
512
+ if (!fs.existsSync(projectOpencodeJson) && fs.existsSync(OPENCODE_JSON_TEMPLATE)) {
513
+ const template = fs.readFileSync(OPENCODE_JSON_TEMPLATE, 'utf-8');
514
+ atomicWriteFileSync(projectOpencodeJson, template);
515
+ }
462
516
  }
463
517
 
464
518
  try { _repairCodexConfig(); } catch (err) {
@@ -503,15 +557,34 @@ function _runUninstallLocked(projectRoot) {
503
557
  }
504
558
  }
505
559
 
560
+ const payloadBase = scope === 'global' ? os.homedir() : projectRoot;
506
561
  let removed = 0;
562
+ const assetDirs = new Set();
507
563
  for (const rel of Object.keys(manifest.files)) {
508
- const abs = path.join(payloadDir, rel);
509
- try { fs.unlinkSync(abs); removed++; } catch (err) {
564
+ const isAsset = runtimeAssetsMod.isAssetManifestKey(rel);
565
+ const abs = isAsset ? path.join(payloadBase, rel) : path.join(payloadDir, rel);
566
+ try {
567
+ fs.unlinkSync(abs);
568
+ removed++;
569
+ if (isAsset) assetDirs.add(path.dirname(abs));
570
+ } catch (err) {
510
571
  if (err && err.code !== 'ENOENT') {
511
572
  console.error(yellow + ' [uninstall] ' + rel + ' not removed: ' + err.message + reset);
512
573
  }
513
574
  }
514
575
  }
576
+ const sortedDirs = Array.from(assetDirs).sort((a, b) => b.length - a.length);
577
+ for (const dir of sortedDirs) {
578
+ let cur = dir;
579
+ while (cur && cur.startsWith(payloadBase) && cur !== payloadBase) {
580
+ try {
581
+ const entries = fs.readdirSync(cur);
582
+ if (entries.length > 0) break;
583
+ fs.rmdirSync(cur);
584
+ } catch { break; }
585
+ cur = path.dirname(cur);
586
+ }
587
+ }
515
588
 
516
589
  try { fs.unlinkSync(path.join(payloadDir, '.manifest.json')); } catch {}
517
590