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
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ const { test, after } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { execFileSync } = require('node:child_process');
9
+
10
+ const worktree = require('./worktree.cjs');
11
+
12
+ const _repos = [];
13
+
14
+ after(() => {
15
+ for (const r of _repos) {
16
+ try { fs.rmSync(r, { recursive: true, force: true }); } catch {}
17
+ }
18
+ });
19
+
20
+ function makeRepo(opts) {
21
+ const o = opts || {};
22
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-worktree-'));
23
+ execFileSync('git', ['init', '-q', '-b', 'main', root], { stdio: 'pipe' });
24
+ execFileSync('git', ['-C', root, 'config', 'user.email', 'test@nubos-pilot.local']);
25
+ execFileSync('git', ['-C', root, 'config', 'user.name', 'nubos-test']);
26
+ execFileSync('git', ['-C', root, 'config', 'commit.gpgsign', 'false']);
27
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
28
+ if (o.gitignored !== false) {
29
+ fs.writeFileSync(path.join(root, '.gitignore'), '.nubos-pilot/worktrees/\n', 'utf-8');
30
+ execFileSync('git', ['-C', root, 'add', '.gitignore'], { stdio: 'pipe' });
31
+ }
32
+ execFileSync('git', ['-C', root, 'commit', '--allow-empty', '-q', '-m', 'chore: init'], { stdio: 'pipe' });
33
+ _repos.push(root);
34
+ return root;
35
+ }
36
+
37
+ function writeConfig(root, cfg) {
38
+ fs.writeFileSync(
39
+ path.join(root, '.nubos-pilot', 'config.json'),
40
+ JSON.stringify(cfg, null, 2),
41
+ 'utf-8',
42
+ );
43
+ }
44
+
45
+ function commitFile(root, rel, body, message) {
46
+ const abs = path.join(root, rel);
47
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
48
+ fs.writeFileSync(abs, body, 'utf-8');
49
+ execFileSync('git', ['-C', root, 'add', rel], { stdio: 'pipe' });
50
+ execFileSync('git', ['-C', root, 'commit', '-q', '-m', message], { stdio: 'pipe' });
51
+ }
52
+
53
+ test('WT-1: sliceBranchName builds np/<mid>-<sid> for a valid slice id', () => {
54
+ assert.equal(worktree.sliceBranchName('M001-S001'), 'np/M001-S001');
55
+ assert.equal(worktree.sliceBranchName('M042-S099'), 'np/M042-S099');
56
+ });
57
+
58
+ test('WT-2: sliceBranchName rejects malformed ids', () => {
59
+ assert.throws(
60
+ () => worktree.sliceBranchName('not-a-slice-id'),
61
+ (err) => err.name === 'NubosPilotError' && err.code === 'layout-invalid-id',
62
+ );
63
+ });
64
+
65
+ test('WT-3: parseSliceBranchName round-trips sliceBranchName', () => {
66
+ const parsed = worktree.parseSliceBranchName('np/M003-S007');
67
+ assert.deepEqual(parsed, { sliceFullId: 'M003-S007', milestone: 3, slice: 7 });
68
+ });
69
+
70
+ test('WT-4: parseSliceBranchName returns null for non-np branches', () => {
71
+ assert.equal(worktree.parseSliceBranchName('main'), null);
72
+ assert.equal(worktree.parseSliceBranchName('feature/foo'), null);
73
+ assert.equal(worktree.parseSliceBranchName('np/not-valid'), null);
74
+ });
75
+
76
+ test('WT-5: sliceWorktreePath lives under .nubos-pilot/worktrees/<mid>/<sid>', () => {
77
+ const root = makeRepo();
78
+ const p = worktree.sliceWorktreePath('M001-S002', root);
79
+ assert.equal(p, path.join(root, '.nubos-pilot', 'worktrees', 'M001', 'S002'));
80
+ });
81
+
82
+ test('WT-6: worktreeIsolationEnabled reads workflow.worktree_isolation flag', () => {
83
+ const root = makeRepo();
84
+ assert.equal(worktree.worktreeIsolationEnabled(root), false);
85
+ writeConfig(root, { workflow: { worktree_isolation: true } });
86
+ assert.equal(worktree.worktreeIsolationEnabled(root), true);
87
+ writeConfig(root, { workflow: { worktree_isolation: false } });
88
+ assert.equal(worktree.worktreeIsolationEnabled(root), false);
89
+ });
90
+
91
+ test('WT-7: createSliceWorktree creates a worktree and branch on current HEAD', () => {
92
+ const root = makeRepo();
93
+ const res = worktree.createSliceWorktree('M001-S001', root);
94
+
95
+ assert.equal(res.slice_full_id, 'M001-S001');
96
+ assert.equal(res.branch, 'np/M001-S001');
97
+ assert.equal(res.path, path.join(root, '.nubos-pilot', 'worktrees', 'M001', 'S001'));
98
+ assert.match(res.base_sha, /^[a-f0-9]{40}$/);
99
+
100
+ assert.ok(fs.existsSync(res.path), 'worktree path must exist');
101
+ const branchCheck = execFileSync('git', ['-C', root, 'rev-parse', '--verify', '--quiet', 'refs/heads/np/M001-S001']);
102
+ assert.ok(branchCheck.toString().trim().length === 40);
103
+ });
104
+
105
+ test('WT-8: createSliceWorktree refuses to recreate an existing worktree', () => {
106
+ const root = makeRepo();
107
+ worktree.createSliceWorktree('M001-S001', root);
108
+ assert.throws(
109
+ () => worktree.createSliceWorktree('M001-S001', root),
110
+ (err) => err.name === 'NubosPilotError' && err.code === 'worktree-already-exists',
111
+ );
112
+ });
113
+
114
+ test('WT-9: createSliceWorktree refuses to reuse an existing branch', () => {
115
+ const root = makeRepo();
116
+ execFileSync('git', ['-C', root, 'branch', 'np/M002-S001'], { stdio: 'pipe' });
117
+ assert.throws(
118
+ () => worktree.createSliceWorktree('M002-S001', root),
119
+ (err) => err.name === 'NubosPilotError' && err.code === 'worktree-branch-conflict',
120
+ );
121
+ });
122
+
123
+ test('WT-10: listSliceWorktrees returns only np/ worktrees, not main', () => {
124
+ const root = makeRepo();
125
+ worktree.createSliceWorktree('M001-S001', root);
126
+ worktree.createSliceWorktree('M001-S002', root);
127
+ const list = worktree.listSliceWorktrees(root);
128
+ assert.equal(list.length, 2);
129
+ const ids = list.map((w) => w.slice_full_id).sort();
130
+ assert.deepEqual(ids, ['M001-S001', 'M001-S002']);
131
+ assert.ok(list[0].branch.startsWith('np/'));
132
+ });
133
+
134
+ test('WT-11: hasSliceWorktree reports correctly before and after creation', () => {
135
+ const root = makeRepo();
136
+ assert.equal(worktree.hasSliceWorktree('M001-S001', root), false);
137
+ worktree.createSliceWorktree('M001-S001', root);
138
+ assert.equal(worktree.hasSliceWorktree('M001-S001', root), true);
139
+ assert.equal(worktree.hasSliceWorktree('M001-S002', root), false);
140
+ });
141
+
142
+ test('WT-12: removeSliceWorktree deletes the worktree and its branch', () => {
143
+ const root = makeRepo();
144
+ const res = worktree.createSliceWorktree('M001-S001', root);
145
+ worktree.removeSliceWorktree('M001-S001', root);
146
+ assert.equal(fs.existsSync(res.path), false);
147
+ const branchCheck = execFileSync('git', ['-C', root, 'branch', '--list', 'np/M001-S001']).toString().trim();
148
+ assert.equal(branchCheck, '');
149
+ });
150
+
151
+ test('WT-13: removeSliceWorktree with deleteBranch=false keeps the branch', () => {
152
+ const root = makeRepo();
153
+ worktree.createSliceWorktree('M001-S001', root);
154
+ worktree.removeSliceWorktree('M001-S001', root, { deleteBranch: false });
155
+ const branchCheck = execFileSync('git', ['-C', root, 'branch', '--list', 'np/M001-S001']).toString().trim();
156
+ assert.match(branchCheck, /np\/M001-S001/);
157
+ });
158
+
159
+ test('WT-14: ffMergeSliceWorktree ff-merges a slice branch back to main', () => {
160
+ const root = makeRepo();
161
+ const created = worktree.createSliceWorktree('M001-S001', root);
162
+ commitFile(created.path, 'src/foo.txt', 'hello', 'task: add foo');
163
+ const res = worktree.ffMergeSliceWorktree('M001-S001', 'main', root);
164
+ assert.match(res.merged_sha, /^[a-f0-9]{40}$/);
165
+
166
+ const head = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD']).toString().trim();
167
+ assert.equal(head, res.merged_sha);
168
+ assert.ok(fs.existsSync(path.join(root, 'src/foo.txt')));
169
+ });
170
+
171
+ test('WT-15: ffMergeSliceWorktree rejects a non-ff merge (main moved ahead)', () => {
172
+ const root = makeRepo();
173
+ worktree.createSliceWorktree('M001-S001', root);
174
+ commitFile(path.join(root, '.nubos-pilot', 'worktrees', 'M001', 'S001'), 'in-slice.txt', 'x', 'task: slice work');
175
+ commitFile(root, 'on-main.txt', 'y', 'chore: advance main');
176
+
177
+ assert.throws(
178
+ () => worktree.ffMergeSliceWorktree('M001-S001', 'main', root),
179
+ (err) => err.name === 'NubosPilotError' && err.code === 'worktree-ff-not-possible',
180
+ );
181
+ });
182
+
183
+ test('WT-16: ffMergeSliceWorktree rejects when HEAD is on wrong branch', () => {
184
+ const root = makeRepo();
185
+ worktree.createSliceWorktree('M001-S001', root);
186
+ execFileSync('git', ['-C', root, 'checkout', '-q', '-b', 'other'], { stdio: 'pipe' });
187
+ assert.throws(
188
+ () => worktree.ffMergeSliceWorktree('M001-S001', 'main', root),
189
+ (err) => err.name === 'NubosPilotError' && err.code === 'worktree-ff-wrong-branch',
190
+ );
191
+ });
192
+
193
+ test('WT-17: ffMergeSliceWorktree fails if the slice branch does not exist', () => {
194
+ const root = makeRepo();
195
+ assert.throws(
196
+ () => worktree.ffMergeSliceWorktree('M001-S099', 'main', root),
197
+ (err) => err.name === 'NubosPilotError' && err.code === 'worktree-branch-missing',
198
+ );
199
+ });
200
+
201
+ test('WT-18: _assertGitRepo fails outside a git repo', () => {
202
+ const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'np-nonrepo-'));
203
+ _repos.push(nonRepo);
204
+ assert.throws(
205
+ () => worktree.listSliceWorktrees(nonRepo),
206
+ (err) => err.name === 'NubosPilotError' && err.code === 'worktree-not-git-repo',
207
+ );
208
+ });
209
+
210
+ test('WT-19: createSliceWorktree nests parent dirs under .nubos-pilot/worktrees/', () => {
211
+ const root = makeRepo();
212
+ const res = worktree.createSliceWorktree('M042-S013', root);
213
+ assert.ok(res.path.endsWith(path.join('.nubos-pilot', 'worktrees', 'M042', 'S013')));
214
+ assert.ok(fs.existsSync(path.join(root, '.nubos-pilot', 'worktrees', 'M042')));
215
+ });
216
+
217
+ test('WT-20: pruneSliceWorktrees does not throw on a clean repo', () => {
218
+ const root = makeRepo();
219
+ assert.doesNotThrow(() => worktree.pruneSliceWorktrees(root));
220
+ });
221
+
222
+ test('WT-21: createSliceWorktree refuses when .nubos-pilot/worktrees/ is NOT gitignored', () => {
223
+ const root = makeRepo({ gitignored: false });
224
+ assert.throws(
225
+ () => worktree.createSliceWorktree('M001-S001', root),
226
+ (err) => err.name === 'NubosPilotError' && err.code === 'worktree-not-gitignored',
227
+ );
228
+ });
package/np-tools.cjs CHANGED
@@ -54,6 +54,16 @@ const topLevelCommands = {
54
54
  'phase-meta': require('./bin/np-tools/phase-meta.cjs'),
55
55
  'state-dir': require('./bin/np-tools/state-dir.cjs'),
56
56
  'render-template': require('./bin/np-tools/render-template.cjs'),
57
+ 'render-todo': require('./bin/np-tools/render-todo.cjs'),
58
+ 'handoff-write': require('./bin/np-tools/handoff-write.cjs'),
59
+ 'handoff-read': require('./bin/np-tools/handoff-read.cjs'),
60
+ 'handoff-list': require('./bin/np-tools/handoff-list.cjs'),
61
+ 'handoff-status': require('./bin/np-tools/handoff-status.cjs'),
62
+ 'worktree-create': require('./bin/np-tools/worktree-create.cjs'),
63
+ 'worktree-remove': require('./bin/np-tools/worktree-remove.cjs'),
64
+ 'worktree-list': require('./bin/np-tools/worktree-list.cjs'),
65
+ 'worktree-ff-merge': require('./bin/np-tools/worktree-ff-merge.cjs'),
66
+ 'dashboard': require('./bin/np-tools/dashboard.cjs'),
57
67
  'thread-resume': require('./bin/np-tools/thread-resume.cjs'),
58
68
  'state-incr': require('./bin/np-tools/state-incr.cjs'),
59
69
  'session-aggregate': require('./bin/np-tools/session-aggregate.cjs'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "AI-driven planning and execution tool for code projects",
5
5
  "homepage": "https://github.com/Nubos-AI/nubos-pilot",
6
6
  "repository": {
@@ -0,0 +1,49 @@
1
+ ---
2
+ command: np:dashboard
3
+ description: One-shot console dashboard of milestones, slices, and tasks. Read-only — no files written, no state mutated, no git commit.
4
+ ---
5
+
6
+ # np:dashboard
7
+
8
+ Read-only snapshot of milestones, slices, and task statuses for the project.
9
+
10
+ ```bash
11
+ node .nubos-pilot/bin/np-tools.cjs dashboard
12
+ ```
13
+
14
+ That is the entire workflow. The CLI prints the snapshot to stdout — milestone-by-milestone, with one row per slice showing per-status counts plus a checkbox row of all tasks in the slice (`[ ]` pending, `[~]` in-progress, `[x]` done, `[-]` skipped, `[!]` parked).
15
+
16
+ ## No Commit
17
+
18
+ Read-only. No files are written, no state is mutated, no git commit is made.
19
+
20
+ ## Scope Guardrail
21
+
22
+ <scope_guardrail>
23
+ **Do:**
24
+ - Run the CLI with no arguments for the formatted view, `--json` for the raw snapshot, or `--no-color` for plain text.
25
+ - Treat the output as a render — re-run the workflow when you want a fresh view.
26
+
27
+ **Don't:**
28
+ - Add a long-running watch loop here — single-shot only, by design (ADR-0001).
29
+ - Mutate any state from this workflow — strictly read-only.
30
+ - Add additional sections beyond milestones / slices / tasks. Drill-down into handoffs / checkpoints / worktrees uses their dedicated commands.
31
+ </scope_guardrail>
32
+
33
+ ## Output
34
+
35
+ Stdout only — formatted milestone overview with checkbox rows per slice. No files created. No state mutated. No git commit.
36
+
37
+ ## Success Criteria
38
+
39
+ - [ ] Empty project renders the "No milestones yet" placeholder line.
40
+ - [ ] Every milestone in `roadmap.yaml` appears with its name and status.
41
+ - [ ] Every slice's checkbox row reflects current task frontmatter `status` values.
42
+ - [ ] `--json` emits the snapshot shape `{ milestones: [{ id, number, name, status, slices: [{ id, full_id, counts, task_statuses }] }] }`.
43
+ - [ ] `--no-color` emits no ANSI escape sequences.
44
+ - [ ] Zero file writes, zero state mutations, zero commits.
45
+
46
+ ## Related Workflows
47
+
48
+ - **`/np:stats`** — phases-table + metrics aggregation (commit-history view).
49
+ - **`/np:state`** — current STATE.md frontmatter snapshot.
@@ -21,6 +21,7 @@ INIT=$(node .nubos-pilot/bin/np-tools.cjs init execute-milestone init "$PHASE")
21
21
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
22
22
  AGENT_SKILLS_EXECUTOR=$(node .nubos-pilot/bin/np-tools.cjs agent-skills executor 2>/dev/null)
23
23
  RUNTIME=$(node .nubos-pilot/bin/np-tools.cjs detect-runtime)
24
+ WORKTREE_ISOLATION=$(node .nubos-pilot/bin/np-tools.cjs config-get workflow.worktree_isolation 2>/dev/null || echo "false")
24
25
  ```
25
26
 
26
27
  **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
@@ -95,6 +96,18 @@ for WAVE_INDEX in 0 1 2 ...; do
95
96
 
96
97
  echo "=== Wave $((WAVE_INDEX+1)): $SLICE_FULL_ID — tasks: $TASK_IDS ===" >&2
97
98
 
99
+ # Worktree-Isolation (ADR-0008): when workflow.worktree_isolation=true,
100
+ # create an isolated git worktree for this slice before spawning executors.
101
+ # Executors run inside the worktree (cwd = worktree path), commits land on
102
+ # the slice branch np/<slice-full-id>, and the slice is fast-forward merged
103
+ # back on success. On failure: worktree stays in place for inspection.
104
+ SLICE_CWD="$PWD"
105
+ if [ "$WORKTREE_ISOLATION" = "true" ]; then
106
+ WT_CREATE=$(node .nubos-pilot/bin/np-tools.cjs worktree-create "$SLICE_FULL_ID")
107
+ SLICE_CWD=$(echo "$WT_CREATE" | node -e "process.stdin.on('data', d => console.log(JSON.parse(d).path))")
108
+ echo "[np:execute-phase] worktree created at $SLICE_CWD (branch np/$SLICE_FULL_ID)" >&2
109
+ fi
110
+
98
111
  # For each task id in TASK_IDS, spawn an executor IN PARALLEL.
99
112
  # The orchestrator's parallel primitive dispatches all of them in a single
100
113
  # message (multiple Agent tool use blocks in one send).
@@ -131,6 +144,9 @@ for WAVE_INDEX in 0 1 2 ...; do
131
144
 
132
145
  if [ "$COMMIT_STATUS" -ne 0 ]; then
133
146
  echo "[np:execute-phase] commit-task failed for $TASK_ID — aborting wave $SLICE_FULL_ID." >&2
147
+ if [ "$WORKTREE_ISOLATION" = "true" ]; then
148
+ echo " Worktree $SLICE_CWD left in place for inspection. Clean up with: /np:reset-slice $TASK_ID" >&2
149
+ fi
134
150
  exit "$COMMIT_STATUS"
135
151
  fi
136
152
  done
@@ -140,6 +156,23 @@ for WAVE_INDEX in 0 1 2 ...; do
140
156
  # the slice-level S<NNN>-SUMMARY.md so /np:validate-phase can audit it.
141
157
  SLICE_NUM=$(echo "$WAVE" | node -e "process.stdin.on('data', d => console.log(JSON.parse(d).wave))")
142
158
  node .nubos-pilot/bin/np-tools.cjs init execute-milestone finalize-slice "$PHASE" "$SLICE_NUM" >/dev/null
159
+
160
+ # Worktree merge-back (ADR-0008 D-8.7): fast-forward-only merge the slice
161
+ # branch back onto the invoking workspace's current branch. Non-FF (e.g.
162
+ # because the base branch advanced during execution) fails hard — that
163
+ # surfaces the drift to the user rather than silently rewriting task SHAs.
164
+ if [ "$WORKTREE_ISOLATION" = "true" ]; then
165
+ FF_RESULT=$(node .nubos-pilot/bin/np-tools.cjs worktree-ff-merge "$SLICE_FULL_ID" 2>&1)
166
+ FF_STATUS=$?
167
+ if [ "$FF_STATUS" -ne 0 ]; then
168
+ echo "[np:execute-phase] ff-merge for $SLICE_FULL_ID failed — worktree left in place for inspection:" >&2
169
+ echo " $FF_RESULT" >&2
170
+ echo " To resolve: cd into $SLICE_CWD, rebase onto current base, then re-run this workflow." >&2
171
+ exit "$FF_STATUS"
172
+ fi
173
+ node .nubos-pilot/bin/np-tools.cjs worktree-remove "$SLICE_FULL_ID" >/dev/null
174
+ echo "[np:execute-phase] worktree $SLICE_FULL_ID merged + removed." >&2
175
+ fi
143
176
  done
144
177
 
145
178
  # Milestone done — regenerate every slice summary so retroactive / resumed