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.
- package/agents/np-executor.md +32 -0
- package/agents/np-planner.md +28 -0
- package/agents/np-researcher.md +28 -0
- package/agents/np-verifier.md +15 -0
- package/bin/np-tools/_commands.cjs +10 -0
- package/bin/np-tools/dashboard.cjs +30 -0
- package/bin/np-tools/doctor.cjs +38 -6
- package/bin/np-tools/doctor.test.cjs +29 -0
- package/bin/np-tools/handoff-list.cjs +27 -0
- package/bin/np-tools/handoff-read.cjs +20 -0
- package/bin/np-tools/handoff-status.cjs +26 -0
- package/bin/np-tools/handoff-write.cjs +59 -0
- package/bin/np-tools/plan-milestone.cjs +14 -0
- package/bin/np-tools/render-todo.cjs +24 -0
- package/bin/np-tools/reset-slice.cjs +31 -2
- package/bin/np-tools/resume-work.cjs +42 -0
- package/bin/np-tools/worktree-create.cjs +24 -0
- package/bin/np-tools/worktree-ff-merge.cjs +33 -0
- package/bin/np-tools/worktree-list.cjs +14 -0
- package/bin/np-tools/worktree-remove.cjs +38 -0
- package/docs/adr/0008-worktree-isolation-per-slice.md +140 -0
- package/docs/adr/0009-tui-framework-for-dashboard.md +95 -0
- package/lib/config-defaults.cjs +1 -0
- package/lib/dashboard.cjs +145 -0
- package/lib/dashboard.test.cjs +179 -0
- package/lib/git.cjs +21 -0
- package/lib/handoff.cjs +277 -0
- package/lib/handoff.test.cjs +227 -0
- package/lib/tasks.cjs +13 -2
- package/lib/todo.cjs +128 -0
- package/lib/todo.test.cjs +179 -0
- package/lib/worktree.cjs +304 -0
- package/lib/worktree.test.cjs +228 -0
- package/np-tools.cjs +10 -0
- package/package.json +1 -1
- package/workflows/dashboard.md +49 -0
- 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
|
@@ -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
|