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,179 @@
1
+ 'use strict';
2
+
3
+ const { test } = 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
+
9
+ const { renderTodoMd, todoPath, STATUS_CHECKBOX } = require('./todo.cjs');
10
+ const { setTaskStatus } = require('./tasks.cjs');
11
+
12
+ function _writeTask(root, mNum, sNum, tNum, status, name) {
13
+ const mId = 'M' + String(mNum).padStart(3, '0');
14
+ const sId = 'S' + String(sNum).padStart(3, '0');
15
+ const tId = 'T' + String(tNum).padStart(4, '0');
16
+ const fullId = mId + '-' + sId + '-' + tId;
17
+ const dir = path.join(root, '.nubos-pilot', 'milestones', mId, 'slices', sId, 'tasks', tId);
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ const fm = [
20
+ '---',
21
+ 'id: "' + fullId + '"',
22
+ 'slice: "' + mId + '-' + sId + '"',
23
+ 'milestone: "' + mId + '"',
24
+ 'type: execute',
25
+ 'status: ' + status,
26
+ 'tier: sonnet',
27
+ 'owner: np-executor',
28
+ 'wave: 1',
29
+ 'depends_on: []',
30
+ 'files_modified: []',
31
+ 'autonomous: true',
32
+ 'must_haves: {}',
33
+ '---',
34
+ '',
35
+ '# ' + fullId + ' — ' + name,
36
+ '',
37
+ ].join('\n');
38
+ const file = path.join(dir, tId + '-PLAN.md');
39
+ fs.writeFileSync(file, fm, 'utf-8');
40
+ return { fullId, sliceFullId: mId + '-' + sId, file };
41
+ }
42
+
43
+ function _sandbox() {
44
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-todo-'));
45
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
46
+ return root;
47
+ }
48
+
49
+ test('TD-1: renderTodoMd writes TODO.md under slice dir', () => {
50
+ const root = _sandbox();
51
+ try {
52
+ const { sliceFullId } = _writeTask(root, 1, 1, 1, 'pending', 'First task');
53
+ const target = renderTodoMd(sliceFullId, root);
54
+ assert.equal(target, path.join(root, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'TODO.md'));
55
+ assert.ok(fs.existsSync(target), 'TODO.md must exist');
56
+ } finally {
57
+ fs.rmSync(root, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ test('TD-2: frontmatter contains correct counts', () => {
62
+ const root = _sandbox();
63
+ try {
64
+ _writeTask(root, 1, 1, 1, 'pending', 'A');
65
+ _writeTask(root, 1, 1, 2, 'in-progress', 'B');
66
+ _writeTask(root, 1, 1, 3, 'done', 'C');
67
+ _writeTask(root, 1, 1, 4, 'done', 'D');
68
+ _writeTask(root, 1, 1, 5, 'skipped', 'E');
69
+ _writeTask(root, 1, 1, 6, 'parked', 'F');
70
+ renderTodoMd('M001-S001', root);
71
+ const raw = fs.readFileSync(todoPath('M001-S001', root), 'utf-8');
72
+ assert.match(raw, /total:\s*6/);
73
+ assert.match(raw, /pending:\s*1/);
74
+ assert.match(raw, /in_progress:\s*1/);
75
+ assert.match(raw, /done:\s*2/);
76
+ assert.match(raw, /skipped:\s*1/);
77
+ assert.match(raw, /parked:\s*1/);
78
+ } finally {
79
+ fs.rmSync(root, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ test('TD-3: each status renders its correct checkbox', () => {
84
+ const root = _sandbox();
85
+ try {
86
+ _writeTask(root, 2, 1, 1, 'pending', 'P');
87
+ _writeTask(root, 2, 1, 2, 'in-progress', 'IP');
88
+ _writeTask(root, 2, 1, 3, 'done', 'D');
89
+ _writeTask(root, 2, 1, 4, 'skipped', 'S');
90
+ _writeTask(root, 2, 1, 5, 'parked', 'K');
91
+ renderTodoMd('M002-S001', root);
92
+ const raw = fs.readFileSync(todoPath('M002-S001', root), 'utf-8');
93
+ assert.match(raw, /- \[ \] \*\*M002-S001-T0001\*\* — P/);
94
+ assert.match(raw, /- \[~\] \*\*M002-S001-T0002\*\* — IP/);
95
+ assert.match(raw, /- \[x\] \*\*M002-S001-T0003\*\* — D/);
96
+ assert.match(raw, /- \[-\] \*\*M002-S001-T0004\*\* — S/);
97
+ assert.match(raw, /- \[!\] \*\*M002-S001-T0005\*\* — K/);
98
+ } finally {
99
+ fs.rmSync(root, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ test('TD-4: empty slice renders "No tasks yet." placeholder', () => {
104
+ const root = _sandbox();
105
+ try {
106
+ fs.mkdirSync(path.join(root, '.nubos-pilot', 'milestones', 'M003', 'slices', 'S001', 'tasks'), { recursive: true });
107
+ renderTodoMd('M003-S001', root);
108
+ const raw = fs.readFileSync(todoPath('M003-S001', root), 'utf-8');
109
+ assert.match(raw, /_No tasks yet\._/);
110
+ assert.match(raw, /total:\s*0/);
111
+ } finally {
112
+ fs.rmSync(root, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ test('TD-5: task name extracted from H1 after em-dash', () => {
117
+ const root = _sandbox();
118
+ try {
119
+ _writeTask(root, 4, 1, 1, 'pending', 'Implement feature X');
120
+ renderTodoMd('M004-S001', root);
121
+ const raw = fs.readFileSync(todoPath('M004-S001', root), 'utf-8');
122
+ assert.match(raw, /\*\*M004-S001-T0001\*\* — Implement feature X/);
123
+ } finally {
124
+ fs.rmSync(root, { recursive: true, force: true });
125
+ }
126
+ });
127
+
128
+ test('TD-6: setTaskStatus triggers auto-render of TODO.md', () => {
129
+ const root = _sandbox();
130
+ try {
131
+ const { fullId, sliceFullId } = _writeTask(root, 5, 1, 1, 'pending', 'Task one');
132
+ setTaskStatus(fullId, 'in-progress', root);
133
+ const raw = fs.readFileSync(todoPath(sliceFullId, root), 'utf-8');
134
+ assert.match(raw, /- \[~\] \*\*M005-S001-T0001\*\* — Task one/);
135
+ assert.match(raw, /in_progress:\s*1/);
136
+ assert.match(raw, /pending:\s*0/);
137
+ } finally {
138
+ fs.rmSync(root, { recursive: true, force: true });
139
+ }
140
+ });
141
+
142
+ test('TD-7: renderTodoMd rejects malformed slice full-id', () => {
143
+ assert.throws(
144
+ () => renderTodoMd('not-a-slice-id', '/tmp'),
145
+ (err) => err.name === 'NubosPilotError' && err.code === 'layout-invalid-id',
146
+ );
147
+ });
148
+
149
+ test('TD-8: renderTodoMd rejects missing sliceFullId', () => {
150
+ assert.throws(
151
+ () => renderTodoMd(null, '/tmp'),
152
+ (err) => err.name === 'NubosPilotError' && err.code === 'todo-missing-slice-id',
153
+ );
154
+ });
155
+
156
+ test('TD-9: STATUS_CHECKBOX covers all task status enum values', () => {
157
+ const expected = ['pending', 'in-progress', 'done', 'skipped', 'parked'];
158
+ for (const s of expected) {
159
+ assert.ok(STATUS_CHECKBOX[s], 'missing checkbox mapping for status: ' + s);
160
+ }
161
+ });
162
+
163
+ test('TD-10: re-rendering overwrites stale counts', () => {
164
+ const root = _sandbox();
165
+ try {
166
+ const { fullId } = _writeTask(root, 6, 1, 1, 'pending', 'Alpha');
167
+ renderTodoMd('M006-S001', root);
168
+ let raw = fs.readFileSync(todoPath('M006-S001', root), 'utf-8');
169
+ assert.match(raw, /pending:\s*1/);
170
+ assert.match(raw, /done:\s*0/);
171
+ setTaskStatus(fullId, 'done', root);
172
+ raw = fs.readFileSync(todoPath('M006-S001', root), 'utf-8');
173
+ assert.match(raw, /pending:\s*0/);
174
+ assert.match(raw, /done:\s*1/);
175
+ assert.match(raw, /- \[x\] \*\*M006-S001-T0001\*\* — Alpha/);
176
+ } finally {
177
+ fs.rmSync(root, { recursive: true, force: true });
178
+ }
179
+ });
@@ -0,0 +1,304 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { NubosPilotError, projectStateDir } = require('./core.cjs');
7
+ const { parseSliceFullId, mId, sId, sliceFullId: buildSliceFullId } = require('./layout.cjs');
8
+ const { runGit: _spawnGit } = require('./git.cjs');
9
+
10
+ const BRANCH_PREFIX = 'np/';
11
+ const WORKTREES_DIRNAME = 'worktrees';
12
+ const CONFIG_FLAG = 'worktree_isolation';
13
+
14
+ function _assertGitRepo(cwd) {
15
+ const r = _spawnGit(['rev-parse', '--git-dir'], { cwd });
16
+ if (!r.ok) {
17
+ throw new NubosPilotError(
18
+ 'worktree-not-git-repo',
19
+ 'not inside a git repository: ' + cwd,
20
+ { cwd, stderr: r.stderr },
21
+ );
22
+ }
23
+ }
24
+
25
+ function sliceBranchName(sliceFullIdStr) {
26
+ parseSliceFullId(sliceFullIdStr);
27
+ return BRANCH_PREFIX + sliceFullIdStr;
28
+ }
29
+
30
+ function parseSliceBranchName(branch) {
31
+ if (typeof branch !== 'string' || !branch.startsWith(BRANCH_PREFIX)) return null;
32
+ const rest = branch.slice(BRANCH_PREFIX.length);
33
+ try {
34
+ const { milestone, slice } = parseSliceFullId(rest);
35
+ return { sliceFullId: rest, milestone, slice };
36
+ } catch { return null; }
37
+ }
38
+
39
+ function sliceWorktreePath(sliceFullIdStr, cwd) {
40
+ const { milestone, slice } = parseSliceFullId(sliceFullIdStr);
41
+ const base = projectStateDir(cwd || process.cwd());
42
+ return path.join(base, WORKTREES_DIRNAME, mId(milestone), sId(slice));
43
+ }
44
+
45
+ function worktreeIsolationEnabled(cwd) {
46
+ const cfgPath = path.join(projectStateDir(cwd || process.cwd()), 'config.json');
47
+ if (!fs.existsSync(cfgPath)) return false;
48
+ try {
49
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
50
+ const wf = cfg && cfg.workflow;
51
+ if (!wf || typeof wf !== 'object') return false;
52
+ return Boolean(wf[CONFIG_FLAG]);
53
+ } catch { return false; }
54
+ }
55
+
56
+ function _currentHeadSha(cwd) {
57
+ const r = _spawnGit(['rev-parse', 'HEAD'], { cwd });
58
+ if (!r.ok) {
59
+ throw new NubosPilotError(
60
+ 'worktree-rev-parse-failed',
61
+ 'git rev-parse HEAD failed: ' + (r.stderr || '').trim(),
62
+ { cwd, stderr: r.stderr },
63
+ );
64
+ }
65
+ return r.stdout.trim();
66
+ }
67
+
68
+ function _currentBranchName(cwd) {
69
+ const r = _spawnGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
70
+ if (!r.ok) return null;
71
+ const name = r.stdout.trim();
72
+ return name === 'HEAD' ? null : name;
73
+ }
74
+
75
+ function _listRawWorktrees(cwd) {
76
+ const r = _spawnGit(['worktree', 'list', '--porcelain'], { cwd });
77
+ if (!r.ok) {
78
+ throw new NubosPilotError(
79
+ 'worktree-list-failed',
80
+ 'git worktree list failed: ' + (r.stderr || '').trim(),
81
+ { cwd, stderr: r.stderr },
82
+ );
83
+ }
84
+ const entries = [];
85
+ let current = null;
86
+ const lines = r.stdout.split(/\r?\n/);
87
+ for (const line of lines) {
88
+ if (line === '') {
89
+ if (current) { entries.push(current); current = null; }
90
+ continue;
91
+ }
92
+ if (!current) current = {};
93
+ if (line.startsWith('worktree ')) current.worktree = line.slice(9);
94
+ else if (line.startsWith('HEAD ')) current.head = line.slice(5);
95
+ else if (line.startsWith('branch ')) current.branch = line.slice(7);
96
+ else if (line === 'bare') current.bare = true;
97
+ else if (line === 'detached') current.detached = true;
98
+ }
99
+ if (current) entries.push(current);
100
+ return entries;
101
+ }
102
+
103
+ function listSliceWorktrees(cwd) {
104
+ _assertGitRepo(cwd);
105
+ const raw = _listRawWorktrees(cwd);
106
+ const out = [];
107
+ for (const w of raw) {
108
+ if (!w.branch) continue;
109
+ const short = w.branch.startsWith('refs/heads/') ? w.branch.slice(11) : w.branch;
110
+ const parsed = parseSliceBranchName(short);
111
+ if (!parsed) continue;
112
+ out.push({
113
+ slice_full_id: parsed.sliceFullId,
114
+ milestone: parsed.milestone,
115
+ slice: parsed.slice,
116
+ branch: short,
117
+ path: w.worktree,
118
+ head: w.head || null,
119
+ });
120
+ }
121
+ out.sort((a, b) => a.slice_full_id.localeCompare(b.slice_full_id));
122
+ return out;
123
+ }
124
+
125
+ function hasSliceWorktree(sliceFullIdStr, cwd) {
126
+ const target = sliceWorktreePath(sliceFullIdStr, cwd);
127
+ for (const w of listSliceWorktrees(cwd)) {
128
+ if (path.resolve(w.path) === path.resolve(target)) return true;
129
+ if (w.slice_full_id === sliceFullIdStr) return true;
130
+ }
131
+ return false;
132
+ }
133
+
134
+ function _assertWorktreesGitignored(cwd) {
135
+ const stateDir = projectStateDir(cwd || process.cwd());
136
+ const worktreesDir = path.join(stateDir, WORKTREES_DIRNAME);
137
+ const rel = path.relative(cwd || process.cwd(), worktreesDir) + path.sep + '.placeholder';
138
+ const r = _spawnGit(['check-ignore', '--quiet', '--', rel], { cwd });
139
+ if (r.ok) return true;
140
+ if (r.status === 1) {
141
+ throw new NubosPilotError(
142
+ 'worktree-not-gitignored',
143
+ 'safety: ' + rel + ' must be gitignored before worktrees can be created. Add `.nubos-pilot/worktrees/` to your .gitignore.',
144
+ { path: rel },
145
+ );
146
+ }
147
+ throw new NubosPilotError(
148
+ 'worktree-gitignore-check-failed',
149
+ 'git check-ignore failed: ' + (r.stderr || '').trim(),
150
+ { stderr: r.stderr },
151
+ );
152
+ }
153
+
154
+ function createSliceWorktree(sliceFullIdStr, cwd) {
155
+ _assertGitRepo(cwd);
156
+ parseSliceFullId(sliceFullIdStr);
157
+ _assertWorktreesGitignored(cwd);
158
+
159
+ const branch = sliceBranchName(sliceFullIdStr);
160
+ const targetPath = sliceWorktreePath(sliceFullIdStr, cwd);
161
+
162
+ if (fs.existsSync(targetPath)) {
163
+ throw new NubosPilotError(
164
+ 'worktree-already-exists',
165
+ 'worktree path already exists: ' + targetPath,
166
+ { slice_full_id: sliceFullIdStr, path: targetPath },
167
+ );
168
+ }
169
+
170
+ const branchCheck = _spawnGit(['rev-parse', '--verify', '--quiet', 'refs/heads/' + branch], { cwd });
171
+ if (branchCheck.ok) {
172
+ throw new NubosPilotError(
173
+ 'worktree-branch-conflict',
174
+ 'branch already exists: ' + branch,
175
+ { slice_full_id: sliceFullIdStr, branch },
176
+ );
177
+ }
178
+
179
+ const baseSha = _currentHeadSha(cwd);
180
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
181
+
182
+ const add = _spawnGit(
183
+ ['worktree', 'add', '-b', branch, targetPath, baseSha],
184
+ { cwd },
185
+ );
186
+ if (!add.ok) {
187
+ throw new NubosPilotError(
188
+ 'worktree-add-failed',
189
+ 'git worktree add failed: ' + (add.stderr || '').trim(),
190
+ { slice_full_id: sliceFullIdStr, branch, path: targetPath, stderr: add.stderr },
191
+ );
192
+ }
193
+
194
+ return { slice_full_id: sliceFullIdStr, branch, path: targetPath, base_sha: baseSha };
195
+ }
196
+
197
+ function removeSliceWorktree(sliceFullIdStr, cwd, opts) {
198
+ _assertGitRepo(cwd);
199
+ const o = opts || {};
200
+ const force = Boolean(o.force);
201
+ const deleteBranch = o.deleteBranch !== false;
202
+
203
+ const targetPath = sliceWorktreePath(sliceFullIdStr, cwd);
204
+ const branch = sliceBranchName(sliceFullIdStr);
205
+
206
+ const removeArgs = ['worktree', 'remove'];
207
+ if (force) removeArgs.push('--force');
208
+ removeArgs.push(targetPath);
209
+
210
+ const r = _spawnGit(removeArgs, { cwd });
211
+ if (!r.ok) {
212
+ if (fs.existsSync(targetPath)) {
213
+ throw new NubosPilotError(
214
+ 'worktree-remove-failed',
215
+ 'git worktree remove failed: ' + (r.stderr || '').trim(),
216
+ { slice_full_id: sliceFullIdStr, path: targetPath, stderr: r.stderr, force },
217
+ );
218
+ }
219
+ }
220
+
221
+ if (deleteBranch) {
222
+ const exists = _spawnGit(['rev-parse', '--verify', '--quiet', 'refs/heads/' + branch], { cwd });
223
+ if (exists.ok) {
224
+ const del = _spawnGit(['branch', '-D', branch], { cwd });
225
+ if (!del.ok) {
226
+ throw new NubosPilotError(
227
+ 'worktree-branch-delete-failed',
228
+ 'git branch -D ' + branch + ' failed: ' + (del.stderr || '').trim(),
229
+ { slice_full_id: sliceFullIdStr, branch, stderr: del.stderr },
230
+ );
231
+ }
232
+ }
233
+ }
234
+
235
+ _spawnGit(['worktree', 'prune'], { cwd });
236
+
237
+ return { slice_full_id: sliceFullIdStr, removed: true, path: targetPath, branch };
238
+ }
239
+
240
+ function ffMergeSliceWorktree(sliceFullIdStr, targetBranch, cwd) {
241
+ _assertGitRepo(cwd);
242
+ parseSliceFullId(sliceFullIdStr);
243
+
244
+ const branch = sliceBranchName(sliceFullIdStr);
245
+
246
+ const existCheck = _spawnGit(['rev-parse', '--verify', '--quiet', 'refs/heads/' + branch], { cwd });
247
+ if (!existCheck.ok) {
248
+ throw new NubosPilotError(
249
+ 'worktree-branch-missing',
250
+ 'slice branch not found: ' + branch,
251
+ { slice_full_id: sliceFullIdStr, branch },
252
+ );
253
+ }
254
+
255
+ const currentBranch = _currentBranchName(cwd);
256
+ if (targetBranch && currentBranch && currentBranch !== targetBranch) {
257
+ throw new NubosPilotError(
258
+ 'worktree-ff-wrong-branch',
259
+ 'ff-merge requires HEAD on ' + targetBranch + ' but is on ' + currentBranch,
260
+ { slice_full_id: sliceFullIdStr, expected: targetBranch, actual: currentBranch },
261
+ );
262
+ }
263
+
264
+ const merge = _spawnGit(['merge', '--ff-only', branch], { cwd });
265
+ if (!merge.ok) {
266
+ throw new NubosPilotError(
267
+ 'worktree-ff-not-possible',
268
+ 'git merge --ff-only failed for ' + branch + ': ' + (merge.stderr || '').trim(),
269
+ { slice_full_id: sliceFullIdStr, branch, target_branch: targetBranch || currentBranch, stderr: merge.stderr },
270
+ );
271
+ }
272
+
273
+ const mergedSha = _currentHeadSha(cwd);
274
+ return { slice_full_id: sliceFullIdStr, branch, target_branch: targetBranch || currentBranch, merged_sha: mergedSha };
275
+ }
276
+
277
+ function pruneSliceWorktrees(cwd) {
278
+ _assertGitRepo(cwd);
279
+ const r = _spawnGit(['worktree', 'prune'], { cwd });
280
+ if (!r.ok) {
281
+ throw new NubosPilotError(
282
+ 'worktree-prune-failed',
283
+ 'git worktree prune failed: ' + (r.stderr || '').trim(),
284
+ { cwd, stderr: r.stderr },
285
+ );
286
+ }
287
+ return { pruned: true };
288
+ }
289
+
290
+ module.exports = {
291
+ BRANCH_PREFIX,
292
+ WORKTREES_DIRNAME,
293
+ CONFIG_FLAG,
294
+ sliceBranchName,
295
+ parseSliceBranchName,
296
+ sliceWorktreePath,
297
+ worktreeIsolationEnabled,
298
+ listSliceWorktrees,
299
+ hasSliceWorktree,
300
+ createSliceWorktree,
301
+ removeSliceWorktree,
302
+ ffMergeSliceWorktree,
303
+ pruneSliceWorktrees,
304
+ };