nubos-pilot 0.7.0 → 0.7.2

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 (37) hide show
  1. package/agents/np-executor.md +32 -0
  2. package/agents/np-planner.md +28 -0
  3. package/agents/np-researcher.md +28 -0
  4. package/agents/np-verifier.md +15 -0
  5. package/bin/np-tools/_commands.cjs +10 -0
  6. package/bin/np-tools/dashboard.cjs +30 -0
  7. package/bin/np-tools/doctor.cjs +38 -6
  8. package/bin/np-tools/doctor.test.cjs +29 -0
  9. package/bin/np-tools/handoff-list.cjs +27 -0
  10. package/bin/np-tools/handoff-read.cjs +20 -0
  11. package/bin/np-tools/handoff-status.cjs +26 -0
  12. package/bin/np-tools/handoff-write.cjs +59 -0
  13. package/bin/np-tools/plan-milestone.cjs +14 -0
  14. package/bin/np-tools/render-todo.cjs +24 -0
  15. package/bin/np-tools/reset-slice.cjs +31 -2
  16. package/bin/np-tools/resume-work.cjs +42 -0
  17. package/bin/np-tools/worktree-create.cjs +24 -0
  18. package/bin/np-tools/worktree-ff-merge.cjs +33 -0
  19. package/bin/np-tools/worktree-list.cjs +14 -0
  20. package/bin/np-tools/worktree-remove.cjs +38 -0
  21. package/docs/adr/0008-worktree-isolation-per-slice.md +140 -0
  22. package/docs/adr/0009-tui-framework-for-dashboard.md +95 -0
  23. package/lib/config-defaults.cjs +1 -0
  24. package/lib/dashboard.cjs +145 -0
  25. package/lib/dashboard.test.cjs +179 -0
  26. package/lib/git.cjs +21 -0
  27. package/lib/handoff.cjs +277 -0
  28. package/lib/handoff.test.cjs +227 -0
  29. package/lib/tasks.cjs +13 -2
  30. package/lib/todo.cjs +128 -0
  31. package/lib/todo.test.cjs +179 -0
  32. package/lib/worktree.cjs +304 -0
  33. package/lib/worktree.test.cjs +228 -0
  34. package/np-tools.cjs +10 -0
  35. package/package.json +1 -1
  36. package/workflows/dashboard.md +49 -0
  37. package/workflows/execute-phase.md +33 -0
@@ -112,6 +112,38 @@ into the `task(…)` commit. If `workflow.commit_docs=true`, the
112
112
  - Auto-discover files via `git status` — the plan declares scope, not the filesystem.
113
113
  </scope_guardrail>
114
114
 
115
+ ## Handoff Protocol
116
+
117
+ Agent handoffs are persistent notes between phase invocations — context that doesn't belong in commit messages or frontmatter. They survive across spawns and let downstream agents see non-obvious signals you discovered during execution.
118
+
119
+ **At start, check handoffs addressed to you:**
120
+
121
+ ```bash
122
+ node .nubos-pilot/bin/np-tools.cjs handoff-list --for np-executor --milestone M<NNN> --status open
123
+ ```
124
+
125
+ For each relevant entry:
126
+ 1. `node .nubos-pilot/bin/np-tools.cjs handoff-read <id>` — read body
127
+ 2. Apply the context to your work
128
+ 3. `node .nubos-pilot/bin/np-tools.cjs handoff-status <id> acted`
129
+
130
+ **At end, write a handoff ONLY for genuine cross-phase signals:**
131
+
132
+ - Non-obvious compromise the verifier must know about → `--to np-verifier`
133
+ - Plan flaw the next planner run should address → `--to np-planner`
134
+ - Trap in shared code that applies broadly → `--to "*"` (broadcast)
135
+
136
+ ```bash
137
+ node .nubos-pilot/bin/np-tools.cjs handoff-write \
138
+ --from np-executor \
139
+ --to np-verifier \
140
+ --topic "Short subject" \
141
+ --milestone M<NNN> --slice M<NNN>-S<NNN> --task M<NNN>-S<NNN>-T<NNNN> \
142
+ --body "What downstream needs to know"
143
+ ```
144
+
145
+ Do NOT write handoffs for routine work. One handoff per genuine signal; noise trains future agents to ignore the channel.
146
+
115
147
  ## Stop Conditions
116
148
 
117
149
  Hard-stop (report to orchestrator, do not attempt recovery):
@@ -14,6 +14,34 @@ Spawned by:
14
14
  - `/np:plan-phase <N> --gaps` — gap closure from verification failures
15
15
  - `/np:plan-phase <N>` in revision mode — updating plans based on plan-checker feedback
16
16
 
17
+ ## Handoff Protocol
18
+
19
+ Agent handoffs are persistent notes between phase invocations. Before planning, check handoffs addressed to `np-planner` for this milestone:
20
+
21
+ ```bash
22
+ node .nubos-pilot/bin/np-tools.cjs handoff-list --for np-planner --milestone M<NNN> --status open
23
+ ```
24
+
25
+ For each entry:
26
+ 1. `node .nubos-pilot/bin/np-tools.cjs handoff-read <id>` — read body
27
+ 2. Integrate the signal into your plan, OR reject it with a return handoff explaining why (executors often flag plan-flaws this way; honor them or refute them — never silently ignore).
28
+ 3. `node .nubos-pilot/bin/np-tools.cjs handoff-status <id> acted`
29
+
30
+ **Write a handoff ONLY for cross-phase signals downstream needs:**
31
+
32
+ - Scope nuance that doesn't fit cleanly in the slice `PLAN.md` → `--to np-executor`
33
+ - SC interpretation that matters at verification time → `--to np-verifier`
34
+
35
+ ```bash
36
+ node .nubos-pilot/bin/np-tools.cjs handoff-write \
37
+ --from np-planner --to <target> \
38
+ --topic "Short subject" \
39
+ --milestone M<NNN> \
40
+ --body "What downstream needs to know"
41
+ ```
42
+
43
+ Do NOT write handoffs for information already captured in PLAN/ROADMAP/CONTEXT.
44
+
17
45
  ## Layout (MANDATORY)
18
46
 
19
47
  Every artifact you write MUST land at exactly these paths. The orchestrator provides the absolute paths in the `<files_to_write>` block — use them verbatim.
@@ -24,6 +24,34 @@ anchor points for your research — do not propose replacements without
24
24
  explicit justification. If `INDEX.md` is absent, report and stop —
25
25
  `np:scan-codebase` must run first.
26
26
 
27
+ ## Handoff Protocol
28
+
29
+ Agent handoffs are persistent notes between phase invocations. Before researching, check handoffs addressed to `np-researcher`:
30
+
31
+ ```bash
32
+ node .nubos-pilot/bin/np-tools.cjs handoff-list --for np-researcher --milestone M<NNN> --status open
33
+ ```
34
+
35
+ For each entry:
36
+ 1. `node .nubos-pilot/bin/np-tools.cjs handoff-read <id>` — read body
37
+ 2. Let the signal shape your research focus (e.g. a verifier-flagged uncertain SC steers deeper investigation in that area)
38
+ 3. `node .nubos-pilot/bin/np-tools.cjs handoff-status <id> acted`
39
+
40
+ **Write a handoff when findings apply beyond this single RESEARCH.md:**
41
+
42
+ - Evidence hint for a known-hard SC → `--to np-verifier`
43
+ - Cross-milestone trap future planners must see → `--to np-planner` without `--milestone` (global scope)
44
+
45
+ ```bash
46
+ node .nubos-pilot/bin/np-tools.cjs handoff-write \
47
+ --from np-researcher --to <target> \
48
+ --topic "Short subject" \
49
+ [--milestone M<NNN>] \
50
+ --body "What downstream needs to know"
51
+ ```
52
+
53
+ Do NOT use handoffs as a replacement for RESEARCH.md content — they are for signals that transcend this milestone's research doc.
54
+
27
55
  ## Tool Availability Detection
28
56
 
29
57
  On startup, before doing any research work, probe the web + MCP surface:
@@ -32,6 +32,21 @@ The orchestrator provides these in your prompt context. Read every path it hands
32
32
  | success_criteria (from init payload) | The list of SC strings to classify. | provided inline in prompt |
33
33
  | Task commits | `git log --grep='^task(M<NNN>-'` → audit trail. | git history |
34
34
 
35
+ ## Handoff Protocol (read-only)
36
+
37
+ Agent handoffs are persistent notes between phase invocations. Before classifying, check handoffs addressed to `np-verifier` for this milestone:
38
+
39
+ ```bash
40
+ node .nubos-pilot/bin/np-tools.cjs handoff-list --for np-verifier --milestone M<NNN> --status open
41
+ ```
42
+
43
+ For each entry:
44
+ 1. `node .nubos-pilot/bin/np-tools.cjs handoff-read <id>` — read body
45
+ 2. Fold the context into your evidence gathering (executors often flag compromises that would otherwise read as `Fail` — the handoff explains the compromise and may move the SC to `Pass` or `Defer`).
46
+ 3. `node .nubos-pilot/bin/np-tools.cjs handoff-status <id> acted`
47
+
48
+ **You do NOT write handoffs.** Verifier is detection-only — your findings land in `VERIFICATION.md`, never in the handoff channel. If you have no Write tool, writing handoffs is impossible anyway.
49
+
35
50
  ## Workflow
36
51
 
37
52
  1. **Parse success_criteria:** read the prompt-provided SC list (from `np-tools.cjs init verify-work <N>`).
@@ -54,6 +54,16 @@ const COMMANDS = [
54
54
  { name: 'phase-meta', category: 'Planning', description: 'Read roadmap.yaml phase fields as JSON (supports --field NAME and --length for arrays)' },
55
55
  { name: 'state-dir', category: 'Utility', description: 'Print project-state directory (.nubos-pilot) or a validated subdir via --subdir NAME' },
56
56
  { name: 'render-template', category: 'Utility', description: 'Render a shipped template by name with --vars JSON (or --vars-file PATH)' },
57
+ { name: 'render-todo', category: 'Utility', description: 'Render slice TODO.md rollup (checkbox view of task statuses) for a slice full-id' },
58
+ { name: 'handoff-write', category: 'Capture', description: 'Write an agent-to-agent handoff note (milestone-scoped by default, global without --milestone)' },
59
+ { name: 'handoff-read', category: 'Capture', description: 'Read a single handoff by id (returns frontmatter + body as JSON)' },
60
+ { name: 'handoff-list', category: 'Capture', description: 'List handoffs (JSON array); filter with --for AGENT, --milestone M<NNN>, --status STATUS, --global' },
61
+ { name: 'handoff-status', category: 'Capture', description: 'Update a handoff status (open|read|acted|archived)' },
62
+ { name: 'worktree-create', category: 'Execution', description: 'Create an isolated git worktree for a slice (branch np/<mid>-<sid> off current HEAD) under .nubos-pilot/worktrees/' },
63
+ { name: 'worktree-remove', category: 'Execution', description: 'Remove a slice worktree + delete its branch (--force / --keep-branch)' },
64
+ { name: 'worktree-list', category: 'Execution', description: 'List all nubos-pilot-managed slice worktrees (np/<mid>-<sid> only) as JSON' },
65
+ { name: 'worktree-ff-merge', category: 'Execution', description: 'Fast-forward merge a slice branch back to its base (fails hard on non-FF)' },
66
+ { name: 'dashboard', category: 'Utility', description: 'One-shot console dashboard of milestones, slices, and tasks. Read-only; flags: --json, --no-color' },
57
67
  { name: 'thread-resume', category: 'Utility', description: 'Bump a thread markdown on resume (status OPEN→IN_PROGRESS, refresh last_resumed) via atomic write' },
58
68
  { name: 'state-incr', category: 'Capture', description: 'Increment a whitelisted STATE.md counter (e.g. pending_todos) under withFileLock' },
59
69
 
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const { collectSnapshot, renderSnapshot } = require('../../lib/dashboard.cjs');
4
+
5
+ function _parseArgs(args) {
6
+ const out = { json: false, noColor: false };
7
+ for (const a of args) {
8
+ if (a === '--json') out.json = true;
9
+ else if (a === '--no-color') out.noColor = true;
10
+ }
11
+ return out;
12
+ }
13
+
14
+ function run(args, opts) {
15
+ const o = opts || {};
16
+ const cwd = o.cwd || process.cwd();
17
+ const stdout = o.stdout || process.stdout;
18
+ const parsed = _parseArgs(Array.isArray(args) ? args : []);
19
+
20
+ const snap = collectSnapshot(cwd);
21
+ if (parsed.json) {
22
+ stdout.write(JSON.stringify(snap, null, 2) + '\n');
23
+ return 0;
24
+ }
25
+ const useColor = !parsed.noColor && Boolean(stdout.isTTY);
26
+ stdout.write(renderSnapshot(snap, { color: useColor }) + '\n');
27
+ return 0;
28
+ }
29
+
30
+ module.exports = { run, _parseArgs };
@@ -7,15 +7,45 @@ const path = require('node:path');
7
7
  const { NubosPilotError, atomicWriteFileSync } = require('../../lib/core.cjs');
8
8
  const manifestMod = require('../../lib/install/manifest.cjs');
9
9
  const codexTomlMod = require('../../lib/install/codex-toml.cjs');
10
+ const runtimeAssetsMod = require('../../lib/install/runtime-assets.cjs');
10
11
  const askuserMod = require('../../lib/askuser.cjs');
11
12
  const codebaseManifest = require('../../lib/codebase-manifest.cjs');
12
13
  const { scan: workspaceScan } = require('../../lib/workspace-scan.cjs');
13
14
 
14
15
  const PAYLOAD_SUBPATH = path.join('.claude', 'nubos-pilot');
16
+ const STATE_SUBPATH = '.nubos-pilot';
15
17
  const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
18
+ const OPENCODE_LOCAL_PREFIX = '.opencode/nubos-pilot/';
19
+ const OPENCODE_GLOBAL_PREFIX = '~/.config/opencode/nubos-pilot/';
16
20
 
17
- function _payloadDirFor(projectRoot) {
18
- return path.join(projectRoot, PAYLOAD_SUBPATH);
21
+ function _readScope(projectRoot) {
22
+ const cfgPath = path.join(projectRoot, STATE_SUBPATH, 'config.json');
23
+ if (!fs.existsSync(cfgPath)) return 'local';
24
+ try {
25
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
26
+ return cfg && cfg.scope === 'global' ? 'global' : 'local';
27
+ } catch {
28
+ return 'local';
29
+ }
30
+ }
31
+
32
+ function _payloadBaseFor(projectRoot, scope) {
33
+ return scope === 'global' ? os.homedir() : projectRoot;
34
+ }
35
+
36
+ function _payloadDirFor(projectRoot, scope) {
37
+ return path.join(_payloadBaseFor(projectRoot, scope), PAYLOAD_SUBPATH);
38
+ }
39
+
40
+ function _resolveManifestEntry(rel, projectRoot, scope) {
41
+ if (rel.startsWith('~/')) {
42
+ return path.join(os.homedir(), rel.slice(2));
43
+ }
44
+ const base = _payloadBaseFor(projectRoot, scope);
45
+ if (runtimeAssetsMod.isAssetManifestKey(rel) || rel.startsWith(OPENCODE_LOCAL_PREFIX)) {
46
+ return path.join(base, rel);
47
+ }
48
+ return path.join(_payloadDirFor(projectRoot, scope), rel);
19
49
  }
20
50
 
21
51
  function _pkgVersion() {
@@ -26,8 +56,9 @@ function _pkgVersion() {
26
56
  }
27
57
  }
28
58
 
29
- function _checkManifestIntegrity(payloadDir) {
59
+ function _checkManifestIntegrity(projectRoot, scope) {
30
60
  const issues = [];
61
+ const payloadDir = _payloadDirFor(projectRoot, scope);
31
62
  let manifest = null;
32
63
  try {
33
64
  manifest = manifestMod.readManifest(payloadDir);
@@ -51,7 +82,7 @@ function _checkManifestIntegrity(payloadDir) {
51
82
  }
52
83
  const files = (manifest.files && typeof manifest.files === 'object') ? manifest.files : {};
53
84
  for (const rel of Object.keys(files)) {
54
- const full = path.join(payloadDir, rel);
85
+ const full = _resolveManifestEntry(rel, projectRoot, scope);
55
86
  if (!fs.existsSync(full)) {
56
87
  issues.push({
57
88
  id: 'payload-missing',
@@ -306,9 +337,10 @@ function _checkMilestoneLayout(projectRoot) {
306
337
  }
307
338
 
308
339
  function _audit(projectRoot) {
309
- const payloadDir = _payloadDirFor(projectRoot);
340
+ const scope = _readScope(projectRoot);
341
+ const payloadDir = _payloadDirFor(projectRoot, scope);
310
342
  const issues = [];
311
- const { manifest, issues: manifestIssues } = _checkManifestIntegrity(payloadDir);
343
+ const { manifest, issues: manifestIssues } = _checkManifestIntegrity(projectRoot, scope);
312
344
  issues.push(...manifestIssues);
313
345
  issues.push(..._checkVersionMismatch(manifest));
314
346
  issues.push(..._checkHooksMissing(manifest, payloadDir));
@@ -85,6 +85,35 @@ test('DOC-4: flags codebase-tbd-docs for modules with _TBD Purpose', async () =>
85
85
  assert.ok(tbd.details.count >= 1);
86
86
  });
87
87
 
88
+ test('DOC-6: asset manifest keys resolve to project root, not payloadDir', async () => {
89
+ const root = makeSandbox();
90
+
91
+ const payloadDir = path.join(root, '.claude', 'nubos-pilot');
92
+ fs.mkdirSync(payloadDir, { recursive: true });
93
+ fs.writeFileSync(path.join(payloadDir, '.manifest.json'), JSON.stringify({
94
+ version: '0.0.0',
95
+ timestamp: new Date().toISOString(),
96
+ files: {
97
+ '.claude/commands/np/foo.md': 'deadbeef',
98
+ '.claude/agents/np-bar.md': 'deadbeef',
99
+ },
100
+ }));
101
+ const cmdDir = path.join(root, '.claude', 'commands', 'np');
102
+ const agentsDir = path.join(root, '.claude', 'agents');
103
+ fs.mkdirSync(cmdDir, { recursive: true });
104
+ fs.mkdirSync(agentsDir, { recursive: true });
105
+ fs.writeFileSync(path.join(cmdDir, 'foo.md'), 'x');
106
+ fs.writeFileSync(path.join(agentsDir, 'np-bar.md'), 'y');
107
+
108
+ const cap = captureStdout();
109
+ await doctor.run([], { cwd: root, stdout: cap.stub, stderr: cap.stub, askUser: async () => ({ value: false }) });
110
+ const out = cap.json();
111
+ const missing = out.issues.filter((i) => i.id === 'payload-missing');
112
+ assert.equal(missing.length, 0,
113
+ 'asset keys must resolve to project-root paths (found ' +
114
+ missing.map((m) => m.file).join(', ') + ')');
115
+ });
116
+
88
117
  test('DOC-5: no tbd flag after prose applied', async () => {
89
118
  const root = makeSandbox();
90
119
  fs.mkdirSync(path.join(root, 'src'), { recursive: true });
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const { listHandoffs } = require('../../lib/handoff.cjs');
4
+
5
+ function _parseArgs(args) {
6
+ const out = { for: null, milestone: null, status: null, global: false };
7
+ for (let i = 0; i < args.length; i++) {
8
+ const a = args[i];
9
+ if (a === '--for') { out.for = args[++i] || null; continue; }
10
+ if (a === '--milestone') { out.milestone = args[++i] || null; continue; }
11
+ if (a === '--status') { out.status = args[++i] || null; continue; }
12
+ if (a === '--global') { out.global = true; continue; }
13
+ }
14
+ return out;
15
+ }
16
+
17
+ function run(args, opts) {
18
+ const o = opts || {};
19
+ const cwd = o.cwd || process.cwd();
20
+ const stdout = o.stdout || process.stdout;
21
+ const parsed = _parseArgs(Array.isArray(args) ? args : []);
22
+ const list = listHandoffs(parsed, cwd);
23
+ stdout.write(JSON.stringify(list) + '\n');
24
+ return 0;
25
+ }
26
+
27
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { readHandoff } = require('../../lib/handoff.cjs');
5
+
6
+ function run(args, opts) {
7
+ const o = opts || {};
8
+ const cwd = o.cwd || process.cwd();
9
+ const stdout = o.stdout || process.stdout;
10
+ const list = Array.isArray(args) ? args : [];
11
+ const id = list.find((a) => !a.startsWith('-'));
12
+ if (!id) {
13
+ throw new NubosPilotError('handoff-read-missing-id', 'handoff id required', {});
14
+ }
15
+ const rec = readHandoff(id, cwd);
16
+ stdout.write(JSON.stringify(rec) + '\n');
17
+ return 0;
18
+ }
19
+
20
+ module.exports = { run };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { setHandoffStatus } = require('../../lib/handoff.cjs');
5
+
6
+ function run(args, opts) {
7
+ const o = opts || {};
8
+ const cwd = o.cwd || process.cwd();
9
+ const stdout = o.stdout || process.stdout;
10
+ const list = Array.isArray(args) ? args : [];
11
+ const positional = list.filter((a) => !a.startsWith('-'));
12
+ const id = positional[0];
13
+ const newStatus = positional[1];
14
+ if (!id || !newStatus) {
15
+ throw new NubosPilotError(
16
+ 'handoff-status-missing-args',
17
+ 'usage: handoff-status <id> <new-status>',
18
+ { got: { id, newStatus } },
19
+ );
20
+ }
21
+ const result = setHandoffStatus(id, newStatus, cwd);
22
+ stdout.write(JSON.stringify({ id, status: result }) + '\n');
23
+ return 0;
24
+ }
25
+
26
+ module.exports = { run };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const { NubosPilotError } = require('../../lib/core.cjs');
5
+ const { writeHandoff } = require('../../lib/handoff.cjs');
6
+
7
+ function _parseArgs(args) {
8
+ const out = {
9
+ from: null, to: null, topic: null,
10
+ milestone: null, slice: null, task: null,
11
+ body: null, bodyFile: null,
12
+ };
13
+ for (let i = 0; i < args.length; i++) {
14
+ const a = args[i];
15
+ if (a === '--from') { out.from = args[++i] || null; continue; }
16
+ if (a === '--to') { out.to = args[++i] || null; continue; }
17
+ if (a === '--topic') { out.topic = args[++i] || null; continue; }
18
+ if (a === '--milestone') { out.milestone = args[++i] || null; continue; }
19
+ if (a === '--slice') { out.slice = args[++i] || null; continue; }
20
+ if (a === '--task') { out.task = args[++i] || null; continue; }
21
+ if (a === '--body') { out.body = args[++i] || null; continue; }
22
+ if (a === '--body-file') { out.bodyFile = args[++i] || null; continue; }
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function run(args, opts) {
28
+ const o = opts || {};
29
+ const cwd = o.cwd || process.cwd();
30
+ const stdout = o.stdout || process.stdout;
31
+ const parsed = _parseArgs(Array.isArray(args) ? args : []);
32
+
33
+ let body = parsed.body || '';
34
+ if (parsed.bodyFile) {
35
+ try { body = fs.readFileSync(parsed.bodyFile, 'utf-8'); }
36
+ catch (err) {
37
+ throw new NubosPilotError(
38
+ 'handoff-body-file-read-failed',
39
+ 'failed to read --body-file: ' + (err && err.message),
40
+ { path: parsed.bodyFile },
41
+ );
42
+ }
43
+ }
44
+
45
+ const result = writeHandoff({
46
+ from: parsed.from,
47
+ to: parsed.to,
48
+ topic: parsed.topic,
49
+ milestone: parsed.milestone,
50
+ slice: parsed.slice,
51
+ task: parsed.task,
52
+ body,
53
+ }, cwd);
54
+
55
+ stdout.write(JSON.stringify(result) + '\n');
56
+ return 0;
57
+ }
58
+
59
+ module.exports = { run, _parseArgs };
@@ -357,6 +357,19 @@ function _scaffoldAllTasks(mNum, cwd) {
357
357
  for (const s of slices) {
358
358
  per.push(_scaffoldSliceTasks(mNum, s.number, cwd));
359
359
  }
360
+
361
+ const { renderTodoMd } = require('../../lib/todo.cjs');
362
+ const todos = [];
363
+ for (const s of slices) {
364
+ try {
365
+ todos.push(renderTodoMd(s.full_id, cwd));
366
+ } catch (err) {
367
+ process.stderr.write(
368
+ '[nubos-pilot warn] TODO.md render failed for ' + s.full_id + ': ' + ((err && err.message) || err) + '\n',
369
+ );
370
+ }
371
+ }
372
+
360
373
  const total = per.reduce((acc, p) => acc + (p.task_count || 0), 0);
361
374
  return {
362
375
  scaffolded: per,
@@ -364,6 +377,7 @@ function _scaffoldAllTasks(mNum, cwd) {
364
377
  milestone: layout.mId(mNum),
365
378
  total_tasks: total,
366
379
  normalized_ids: normalized.changed ? normalized.remap : {},
380
+ todos_rendered: todos,
367
381
  };
368
382
  }
369
383
 
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { renderTodoMd } = require('../../lib/todo.cjs');
5
+
6
+ function run(args, opts) {
7
+ const o = opts || {};
8
+ const cwd = o.cwd || process.cwd();
9
+ const stdout = o.stdout || process.stdout;
10
+ const list = Array.isArray(args) ? args : [];
11
+ const sliceFullId = list.find((a) => !a.startsWith('-'));
12
+ if (!sliceFullId) {
13
+ throw new NubosPilotError(
14
+ 'render-todo-missing-slice',
15
+ 'slice full-id required (e.g. M001-S001)',
16
+ {},
17
+ );
18
+ }
19
+ const target = renderTodoMd(sliceFullId, cwd);
20
+ stdout.write(target + '\n');
21
+ return 0;
22
+ }
23
+
24
+ module.exports = { run };
@@ -8,6 +8,11 @@ const { restoreFiles } = require('../../lib/git.cjs');
8
8
  const { deleteCheckpoint, listCheckpoints } = require('../../lib/checkpoint.cjs');
9
9
  const layout = require('../../lib/layout.cjs');
10
10
  const { extractFrontmatter } = require('../../lib/frontmatter.cjs');
11
+ const {
12
+ hasSliceWorktree,
13
+ removeSliceWorktree,
14
+ worktreeIsolationEnabled,
15
+ } = require('../../lib/worktree.cjs');
11
16
 
12
17
  function _resolveTaskId(explicit, cwd) {
13
18
  if (explicit) {
@@ -44,13 +49,33 @@ function _readTaskFiles(taskId, cwd) {
44
49
  return Array.isArray(frontmatter.files_modified) ? frontmatter.files_modified : [];
45
50
  }
46
51
 
52
+ function _maybeRemoveWorktreeForTask(taskId, cwd) {
53
+ if (!worktreeIsolationEnabled(cwd)) return null;
54
+ let parsed;
55
+ try { parsed = layout.parseTaskFullId(taskId); } catch { return null; }
56
+ const sliceFullId = layout.sliceFullId(parsed.milestone, parsed.slice);
57
+ let exists = false;
58
+ try { exists = hasSliceWorktree(sliceFullId, cwd); } catch { exists = false; }
59
+ if (!exists) return null;
60
+ try {
61
+ return removeSliceWorktree(sliceFullId, cwd, { force: true });
62
+ } catch (err) {
63
+ process.stderr.write(
64
+ '[nubos-pilot warn] removeSliceWorktree failed for ' + sliceFullId + ': ' + ((err && err.message) || err) + '\n',
65
+ );
66
+ return null;
67
+ }
68
+ }
69
+
47
70
  function run(args, ctx) {
48
71
  const context = ctx || {};
49
72
  const cwd = context.cwd || process.cwd();
50
73
  const stdout = context.stdout || process.stdout;
51
74
  const list = Array.isArray(args) ? args : [];
52
75
 
53
- const explicit = list[0] && !list[0].startsWith('--') ? list[0] : null;
76
+ const keepWorktree = list.includes('--keep-worktree');
77
+ const positional = list.filter((a) => a && !a.startsWith('--'));
78
+ const explicit = positional[0] || null;
54
79
  const taskId = _resolveTaskId(explicit, cwd);
55
80
 
56
81
  if (!taskId) {
@@ -91,12 +116,16 @@ function run(args, ctx) {
91
116
  return { frontmatter: fm, body: state.body };
92
117
  }, cwd);
93
118
 
119
+ const worktreeRemoved = keepWorktree ? null : _maybeRemoveWorktreeForTask(taskId, cwd);
120
+
94
121
  const payload = {
95
122
  ok: true,
96
123
  task_id: taskId,
97
124
  restored_files: files,
98
125
  deleted_checkpoints: [taskId],
99
- message: 'in-flight task discarded; working tree restored to HEAD',
126
+ worktree_removed: worktreeRemoved,
127
+ message: 'in-flight task discarded; working tree restored to HEAD'
128
+ + (worktreeRemoved ? '; worktree ' + worktreeRemoved.branch + ' removed' : ''),
100
129
  };
101
130
  stdout.write(JSON.stringify(payload));
102
131
  return payload;
@@ -5,6 +5,29 @@ const { readState } = require('../../lib/state.cjs');
5
5
  const { readCheckpoint, listCheckpoints } = require('../../lib/checkpoint.cjs');
6
6
  const { TASK_ID_RE } = require('../../lib/tasks.cjs');
7
7
  const textMode = require('../../lib/text-mode.cjs');
8
+ const layout = require('../../lib/layout.cjs');
9
+ const {
10
+ hasSliceWorktree,
11
+ sliceWorktreePath,
12
+ sliceBranchName,
13
+ worktreeIsolationEnabled,
14
+ listSliceWorktrees,
15
+ } = require('../../lib/worktree.cjs');
16
+
17
+ function _worktreeInfoForTask(taskId, cwd) {
18
+ if (!taskId || !worktreeIsolationEnabled(cwd)) return null;
19
+ let parsed;
20
+ try { parsed = layout.parseTaskFullId(taskId); } catch { return null; }
21
+ const sliceFullId = layout.sliceFullId(parsed.milestone, parsed.slice);
22
+ let exists = false;
23
+ try { exists = hasSliceWorktree(sliceFullId, cwd); } catch { exists = false; }
24
+ if (!exists) return null;
25
+ return {
26
+ slice_full_id: sliceFullId,
27
+ branch: sliceBranchName(sliceFullId),
28
+ path: sliceWorktreePath(sliceFullId, cwd),
29
+ };
30
+ }
8
31
 
9
32
  function _safeReadState(cwd) {
10
33
  try { return readState(cwd); } catch { return null; }
@@ -74,6 +97,25 @@ function run(_args, ctx) {
74
97
  payload.text_mode = tmDetail.enabled;
75
98
  payload.text_mode_source = tmDetail.source;
76
99
 
100
+ const wtInfo = _worktreeInfoForTask(currentTask, cwd);
101
+ if (wtInfo) payload.worktree = wtInfo;
102
+ payload.worktree_isolation = worktreeIsolationEnabled(cwd);
103
+ let stale = [];
104
+ try {
105
+ stale = listSliceWorktrees(cwd).filter((w) => {
106
+ const b = w.branch;
107
+ const activeBranch = wtInfo && wtInfo.branch;
108
+ return b !== activeBranch;
109
+ });
110
+ } catch { stale = []; }
111
+ if (stale.length > 0) {
112
+ payload.stale_worktrees = stale.map((w) => ({
113
+ slice_full_id: w.slice_full_id,
114
+ branch: w.branch,
115
+ path: w.path,
116
+ }));
117
+ }
118
+
77
119
  stdout.write(JSON.stringify(payload));
78
120
  return payload;
79
121
  }
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { createSliceWorktree } = require('../../lib/worktree.cjs');
5
+
6
+ function run(args, opts) {
7
+ const o = opts || {};
8
+ const cwd = o.cwd || process.cwd();
9
+ const stdout = o.stdout || process.stdout;
10
+ const list = Array.isArray(args) ? args : [];
11
+ const sliceFullId = list.find((a) => !a.startsWith('-'));
12
+ if (!sliceFullId) {
13
+ throw new NubosPilotError(
14
+ 'worktree-create-missing-slice',
15
+ 'slice full-id required (e.g. M001-S001)',
16
+ {},
17
+ );
18
+ }
19
+ const result = createSliceWorktree(sliceFullId, cwd);
20
+ stdout.write(JSON.stringify(result) + '\n');
21
+ return 0;
22
+ }
23
+
24
+ module.exports = { run };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { ffMergeSliceWorktree } = require('../../lib/worktree.cjs');
5
+
6
+ function _parseArgs(args) {
7
+ const out = { sliceFullId: null, target: null };
8
+ for (let i = 0; i < args.length; i++) {
9
+ const a = args[i];
10
+ if (a === '--target') { out.target = args[++i] || null; continue; }
11
+ if (!a.startsWith('-') && !out.sliceFullId) out.sliceFullId = a;
12
+ }
13
+ return out;
14
+ }
15
+
16
+ function run(args, opts) {
17
+ const o = opts || {};
18
+ const cwd = o.cwd || process.cwd();
19
+ const stdout = o.stdout || process.stdout;
20
+ const parsed = _parseArgs(Array.isArray(args) ? args : []);
21
+ if (!parsed.sliceFullId) {
22
+ throw new NubosPilotError(
23
+ 'worktree-ff-merge-missing-slice',
24
+ 'slice full-id required (e.g. M001-S001)',
25
+ {},
26
+ );
27
+ }
28
+ const result = ffMergeSliceWorktree(parsed.sliceFullId, parsed.target, cwd);
29
+ stdout.write(JSON.stringify(result) + '\n');
30
+ return 0;
31
+ }
32
+
33
+ module.exports = { run, _parseArgs };