nubos-pilot 0.5.5 → 0.5.6
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-planner.md
CHANGED
|
@@ -223,13 +223,15 @@ Inside each `S<NNN>-PLAN.md`, every `<task>` tag MUST have these four attributes
|
|
|
223
223
|
|
|
224
224
|
The scaffolder (`_extractTasksFromSlicePlan` in `bin/np-tools/plan-milestone.cjs`) reads ONLY these opening-tag attributes. Without them, zero task files are scaffolded and execute-phase has nothing to dispatch.
|
|
225
225
|
|
|
226
|
+
Inside the body, every `<task>` MUST also contain a `<files_modified>` block listing the files the executor will touch (one per line or comma-separated). An empty or missing `<files_modified>` block produces `files_modified: []` in task frontmatter, which causes `commit-task` to fail (`commit-task-no-files`) unless the executor reported touched files via `checkpoint touch` as a runtime fallback. Plans MUST declare intent up-front; relying on the checkpoint fallback is a last-resort safety net.
|
|
227
|
+
|
|
226
228
|
Correct example for `slices/S001/S001-PLAN.md`:
|
|
227
229
|
|
|
228
230
|
```
|
|
229
231
|
<tasks>
|
|
230
232
|
<task id="M001-S001-T0001" depends_on="" wave="1" tier="sonnet">
|
|
231
233
|
<name>Seed login form</name>
|
|
232
|
-
<
|
|
234
|
+
<files_modified>src/auth/LoginForm.tsx</files_modified>
|
|
233
235
|
<read_first>
|
|
234
236
|
- src/auth/AuthProvider.tsx
|
|
235
237
|
</read_first>
|
|
@@ -7,7 +7,7 @@ const { TASK_ID_RE, setTaskStatus } = require('../../lib/tasks.cjs');
|
|
|
7
7
|
const layout = require('../../lib/layout.cjs');
|
|
8
8
|
const git = require('../../lib/git.cjs');
|
|
9
9
|
const { commitTask, findCommitByTaskId } = git;
|
|
10
|
-
const { deleteCheckpoint } = require('../../lib/checkpoint.cjs');
|
|
10
|
+
const { deleteCheckpoint, readCheckpoint } = require('../../lib/checkpoint.cjs');
|
|
11
11
|
|
|
12
12
|
function _resolveTaskFile(taskId, cwd) {
|
|
13
13
|
const parsed = layout.parseTaskFullId(taskId);
|
|
@@ -70,11 +70,21 @@ function run(args, ctx) {
|
|
|
70
70
|
const { filePath } = _resolveTaskFile(taskId, cwd);
|
|
71
71
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
72
72
|
const { frontmatter, body } = extractFrontmatter(raw);
|
|
73
|
-
const
|
|
73
|
+
const declared = Array.isArray(frontmatter.files_modified) ? frontmatter.files_modified : [];
|
|
74
|
+
let files = declared.slice();
|
|
75
|
+
let filesSource = 'frontmatter';
|
|
76
|
+
if (files.length === 0) {
|
|
77
|
+
const cp = readCheckpoint(taskId, cwd);
|
|
78
|
+
const touched = cp && Array.isArray(cp.files_touched) ? cp.files_touched : [];
|
|
79
|
+
if (touched.length > 0) {
|
|
80
|
+
files = touched.slice();
|
|
81
|
+
filesSource = 'checkpoint';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
74
84
|
if (files.length === 0) {
|
|
75
85
|
throw new NubosPilotError(
|
|
76
86
|
'commit-task-no-files',
|
|
77
|
-
'Task ' + taskId + ' has empty files_modified',
|
|
87
|
+
'Task ' + taskId + ' has empty files_modified and no files_touched in checkpoint',
|
|
78
88
|
{ taskId },
|
|
79
89
|
);
|
|
80
90
|
}
|
|
@@ -93,7 +103,7 @@ function run(args, ctx) {
|
|
|
93
103
|
process.stderr.write('[nubos-pilot warn] setTaskStatus failed for ' + taskId + ': ' + (err && err.message) + '\n');
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
const payload = { ok: true, task_id: taskId, sha, files: safeFiles };
|
|
106
|
+
const payload = { ok: true, task_id: taskId, sha, files: safeFiles, files_source: filesSource };
|
|
97
107
|
stdout.write(JSON.stringify(payload));
|
|
98
108
|
return payload;
|
|
99
109
|
}
|
|
@@ -158,3 +158,40 @@ test('CT-5: commit-task unknown task id → task-not-found', () => {
|
|
|
158
158
|
(err) => err && err.code === 'commit-task-not-found',
|
|
159
159
|
);
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
test('CT-6: empty files_modified falls back to checkpoint.files_touched', () => {
|
|
163
|
+
const root = makeRepo();
|
|
164
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0010', []);
|
|
165
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
166
|
+
fs.writeFileSync(path.join(root, 'src', 'b.ts'), 'export const b = 2;\n', 'utf-8');
|
|
167
|
+
const cpPath = path.join(root, '.nubos-pilot', 'checkpoints', 'M006-S001-T0010.json');
|
|
168
|
+
fs.mkdirSync(path.dirname(cpPath), { recursive: true });
|
|
169
|
+
fs.writeFileSync(cpPath, JSON.stringify({
|
|
170
|
+
schema_version: 1,
|
|
171
|
+
task_id: 'M006-S001-T0010',
|
|
172
|
+
status: 'pre-commit',
|
|
173
|
+
files_touched: ['src/b.ts'],
|
|
174
|
+
}), 'utf-8');
|
|
175
|
+
const prev = process.cwd();
|
|
176
|
+
process.chdir(root);
|
|
177
|
+
const cap = _capture();
|
|
178
|
+
try {
|
|
179
|
+
subcmd.run(['M006-S001-T0010'], { cwd: root, stdout: cap.stub });
|
|
180
|
+
} finally {
|
|
181
|
+
process.chdir(prev);
|
|
182
|
+
}
|
|
183
|
+
const payload = JSON.parse(cap.get());
|
|
184
|
+
assert.equal(payload.ok, true);
|
|
185
|
+
assert.equal(payload.files_source, 'checkpoint');
|
|
186
|
+
assert.deepEqual(payload.files, ['src/b.ts']);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('CT-7: empty files_modified AND no checkpoint → commit-task-no-files', () => {
|
|
190
|
+
const root = makeRepo();
|
|
191
|
+
seedPlanAndTask(root, '06-01', 'M006-S001-T0011', []);
|
|
192
|
+
const cap = _capture();
|
|
193
|
+
assert.throws(
|
|
194
|
+
() => subcmd.run(['M006-S001-T0011'], { cwd: root, stdout: cap.stub }),
|
|
195
|
+
(err) => err && err.code === 'commit-task-no-files',
|
|
196
|
+
);
|
|
197
|
+
});
|
|
@@ -163,12 +163,65 @@ function _extractTasksFromSlicePlan(planPath) {
|
|
|
163
163
|
return out;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
function _escapeRegex(s) {
|
|
167
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function _buildMilestoneRemap(mNum, cwd) {
|
|
171
|
+
const slices = layout.listSlices(mNum, cwd);
|
|
172
|
+
const remap = new Map();
|
|
173
|
+
for (const s of slices) {
|
|
174
|
+
const planPath = layout.slicePlanPath(mNum, s.number, cwd);
|
|
175
|
+
if (!fs.existsSync(planPath)) continue;
|
|
176
|
+
const tasks = _extractTasksFromSlicePlan(planPath);
|
|
177
|
+
tasks.forEach((t, idx) => {
|
|
178
|
+
const oldId = t.id;
|
|
179
|
+
if (!oldId) return;
|
|
180
|
+
const newId = layout.taskFullId(mNum, s.number, idx + 1);
|
|
181
|
+
if (oldId !== newId) remap.set(oldId, newId);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return remap;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _rewriteSlicePlanIds(planPath, remap) {
|
|
188
|
+
if (remap.size === 0) return false;
|
|
189
|
+
const raw = fs.readFileSync(planPath, 'utf-8');
|
|
190
|
+
const keys = [...remap.keys()].sort((a, b) => b.length - a.length);
|
|
191
|
+
const re = new RegExp('\\b(' + keys.map(_escapeRegex).join('|') + ')\\b', 'g');
|
|
192
|
+
const next = raw.replace(re, (m) => remap.get(m) || m);
|
|
193
|
+
if (next === raw) return false;
|
|
194
|
+
atomicWriteFileSync(planPath, next);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _normalizeMilestoneTaskIds(mNum, cwd) {
|
|
199
|
+
const remap = _buildMilestoneRemap(mNum, cwd);
|
|
200
|
+
if (remap.size === 0) return { changed: false, remap: {} };
|
|
201
|
+
const slices = layout.listSlices(mNum, cwd);
|
|
202
|
+
for (const s of slices) {
|
|
203
|
+
const planPath = layout.slicePlanPath(mNum, s.number, cwd);
|
|
204
|
+
if (!fs.existsSync(planPath)) continue;
|
|
205
|
+
_rewriteSlicePlanIds(planPath, remap);
|
|
206
|
+
}
|
|
207
|
+
return { changed: true, remap: Object.fromEntries(remap) };
|
|
208
|
+
}
|
|
209
|
+
|
|
166
210
|
function _extractInnerTag(body, tag) {
|
|
167
211
|
const re = new RegExp('<' + tag + '(?:\\s[^>]*)?>([\\s\\S]*?)</' + tag + '>', 'i');
|
|
168
212
|
const m = body.match(re);
|
|
169
213
|
return m ? m[1].trim() : '';
|
|
170
214
|
}
|
|
171
215
|
|
|
216
|
+
function _parseFilesList(body) {
|
|
217
|
+
const raw = _extractInnerTag(body, 'files_modified') || _extractInnerTag(body, 'files');
|
|
218
|
+
if (!raw) return [];
|
|
219
|
+
return raw
|
|
220
|
+
.split(/[,\n]/)
|
|
221
|
+
.map((s) => s.trim().replace(/^[-*]\s+/, '').trim())
|
|
222
|
+
.filter(Boolean);
|
|
223
|
+
}
|
|
224
|
+
|
|
172
225
|
function _filesYaml(files) {
|
|
173
226
|
if (!files.length) return '[]';
|
|
174
227
|
return '\n' + files.map((f) => ' - ' + JSON.stringify(f)).join('\n');
|
|
@@ -184,8 +237,7 @@ function _renderTaskPlanMd(task, mNum, sNum) {
|
|
|
184
237
|
const parsed = fullId.match(/T(\d{4,})$/);
|
|
185
238
|
const taskNum = parsed ? Number(parsed[1]) : 0;
|
|
186
239
|
const name = _extractInnerTag(task.body, 'name') || fullId;
|
|
187
|
-
const
|
|
188
|
-
const filesList = files ? files.split(/[,\n]/).map((s) => s.trim()).filter(Boolean) : [];
|
|
240
|
+
const filesList = _parseFilesList(task.body);
|
|
189
241
|
const readFirst = _extractInnerTag(task.body, 'read_first');
|
|
190
242
|
const action = _extractInnerTag(task.body, 'action');
|
|
191
243
|
const verify = _extractInnerTag(task.body, 'verify');
|
|
@@ -258,6 +310,13 @@ function _scaffoldSliceTasks(mNum, sNum, cwd) {
|
|
|
258
310
|
if (!fs.existsSync(planPath)) {
|
|
259
311
|
return { scaffolded: [], reason: 'no-slice-plan', slice: layout.sliceFullId(mNum, sNum) };
|
|
260
312
|
}
|
|
313
|
+
const sliceRemap = new Map();
|
|
314
|
+
const preTasks = _extractTasksFromSlicePlan(planPath);
|
|
315
|
+
preTasks.forEach((t, idx) => {
|
|
316
|
+
const newId = layout.taskFullId(mNum, sNum, idx + 1);
|
|
317
|
+
if (t.id && t.id !== newId) sliceRemap.set(t.id, newId);
|
|
318
|
+
});
|
|
319
|
+
_rewriteSlicePlanIds(planPath, sliceRemap);
|
|
261
320
|
const tasks = _extractTasksFromSlicePlan(planPath);
|
|
262
321
|
if (tasks.length === 0) {
|
|
263
322
|
return { scaffolded: [], reason: 'no-tasks-in-slice-plan', slice: layout.sliceFullId(mNum, sNum) };
|
|
@@ -293,12 +352,19 @@ function _scaffoldAllTasks(mNum, cwd) {
|
|
|
293
352
|
if (slices.length === 0) {
|
|
294
353
|
return { scaffolded: [], reason: 'no-slices', milestone: layout.mId(mNum) };
|
|
295
354
|
}
|
|
355
|
+
const normalized = _normalizeMilestoneTaskIds(mNum, cwd);
|
|
296
356
|
const per = [];
|
|
297
357
|
for (const s of slices) {
|
|
298
358
|
per.push(_scaffoldSliceTasks(mNum, s.number, cwd));
|
|
299
359
|
}
|
|
300
360
|
const total = per.reduce((acc, p) => acc + (p.task_count || 0), 0);
|
|
301
|
-
return {
|
|
361
|
+
return {
|
|
362
|
+
scaffolded: per,
|
|
363
|
+
reason: 'ok',
|
|
364
|
+
milestone: layout.mId(mNum),
|
|
365
|
+
total_tasks: total,
|
|
366
|
+
normalized_ids: normalized.changed ? normalized.remap : {},
|
|
367
|
+
};
|
|
302
368
|
}
|
|
303
369
|
|
|
304
370
|
function _createMilestoneDir(mNum, cwd) {
|
|
@@ -207,3 +207,149 @@ test('PM-9: unknown verb throws', () => {
|
|
|
207
207
|
(err) => err && err.code === 'plan-milestone-unknown-verb',
|
|
208
208
|
);
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
test('PM-10: scaffold-all-tasks renumbers task ids per slice when planner numbered globally', () => {
|
|
212
|
+
const sandbox = makeSandbox();
|
|
213
|
+
seedRoadmapYaml(sandbox, _roadmap());
|
|
214
|
+
seedMilestoneDir(sandbox, 1, {});
|
|
215
|
+
const plan1 = [
|
|
216
|
+
'---', 'slice: "M001-S001"', 'milestone: "M001"', '---', '',
|
|
217
|
+
'<task id="M001-S001-T0001" wave="1" tier="sonnet" depends_on=""><name>A</name><action>A</action><done>D</done></task>',
|
|
218
|
+
'<task id="M001-S001-T0002" wave="1" tier="sonnet" depends_on=""><name>B</name><action>A</action><done>D</done></task>',
|
|
219
|
+
'<task id="M001-S001-T0003" wave="1" tier="sonnet" depends_on=""><name>C</name><action>A</action><done>D</done></task>',
|
|
220
|
+
].join('\n');
|
|
221
|
+
const plan2 = [
|
|
222
|
+
'---', 'slice: "M001-S002"', 'milestone: "M001"', '---', '',
|
|
223
|
+
'<task id="M001-S002-T0004" wave="2" tier="sonnet" depends_on="M001-S001-T0003"><name>D</name><action>A</action><done>D</done></task>',
|
|
224
|
+
'<task id="M001-S002-T0005" wave="2" tier="sonnet" depends_on=""><name>E</name><action>A</action><done>D</done></task>',
|
|
225
|
+
].join('\n');
|
|
226
|
+
const plan3 = [
|
|
227
|
+
'---', 'slice: "M001-S003"', 'milestone: "M001"', '---', '',
|
|
228
|
+
'<task id="M001-S003-T0006" wave="3" tier="sonnet" depends_on="M001-S002-T0004"><name>F</name><action>A</action><done>D</done></task>',
|
|
229
|
+
'<task id="M001-S003-T0007" wave="3" tier="sonnet" depends_on="M001-S002-T0005,M001-S001-T0001"><name>G</name><action>A</action><done>D</done></task>',
|
|
230
|
+
].join('\n');
|
|
231
|
+
seedSliceDir(sandbox, 1, 1, { 'S001-PLAN.md': plan1 });
|
|
232
|
+
seedSliceDir(sandbox, 1, 2, { 'S002-PLAN.md': plan2 });
|
|
233
|
+
seedSliceDir(sandbox, 1, 3, { 'S003-PLAN.md': plan3 });
|
|
234
|
+
|
|
235
|
+
const cap = _capture();
|
|
236
|
+
subcmd.run(['scaffold-all-tasks', '1'], { cwd: sandbox, stdout: cap.stub });
|
|
237
|
+
const out = JSON.parse(cap.get().trim());
|
|
238
|
+
assert.equal(out.reason, 'ok');
|
|
239
|
+
assert.equal(out.total_tasks, 7);
|
|
240
|
+
|
|
241
|
+
const tasksBase = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices');
|
|
242
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'S001', 'tasks', 'T0001')));
|
|
243
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'S001', 'tasks', 'T0002')));
|
|
244
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'S001', 'tasks', 'T0003')));
|
|
245
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'S002', 'tasks', 'T0001')));
|
|
246
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'S002', 'tasks', 'T0002')));
|
|
247
|
+
assert.ok(!fs.existsSync(path.join(tasksBase, 'S002', 'tasks', 'T0004')));
|
|
248
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'S003', 'tasks', 'T0001')));
|
|
249
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'S003', 'tasks', 'T0002')));
|
|
250
|
+
assert.ok(!fs.existsSync(path.join(tasksBase, 'S003', 'tasks', 'T0006')));
|
|
251
|
+
|
|
252
|
+
const s2t1 = fs.readFileSync(path.join(tasksBase, 'S002', 'tasks', 'T0001', 'T0001-PLAN.md'), 'utf-8');
|
|
253
|
+
assert.match(s2t1, /id: "M001-S002-T0001"/);
|
|
254
|
+
assert.match(s2t1, /- "M001-S001-T0003"/);
|
|
255
|
+
|
|
256
|
+
const s3t1 = fs.readFileSync(path.join(tasksBase, 'S003', 'tasks', 'T0001', 'T0001-PLAN.md'), 'utf-8');
|
|
257
|
+
assert.match(s3t1, /id: "M001-S003-T0001"/);
|
|
258
|
+
assert.match(s3t1, /- "M001-S002-T0001"/);
|
|
259
|
+
|
|
260
|
+
const s3t2 = fs.readFileSync(path.join(tasksBase, 'S003', 'tasks', 'T0002', 'T0002-PLAN.md'), 'utf-8');
|
|
261
|
+
assert.match(s3t2, /id: "M001-S003-T0002"/);
|
|
262
|
+
assert.match(s3t2, /- "M001-S002-T0002"/);
|
|
263
|
+
assert.match(s3t2, /- "M001-S001-T0001"/);
|
|
264
|
+
|
|
265
|
+
const s2PlanRaw = fs.readFileSync(path.join(tasksBase, 'S002', 'S002-PLAN.md'), 'utf-8');
|
|
266
|
+
assert.match(s2PlanRaw, /id="M001-S002-T0001"/);
|
|
267
|
+
assert.match(s2PlanRaw, /id="M001-S002-T0002"/);
|
|
268
|
+
assert.doesNotMatch(s2PlanRaw, /T0004/);
|
|
269
|
+
assert.doesNotMatch(s2PlanRaw, /T0005/);
|
|
270
|
+
|
|
271
|
+
assert.deepEqual(out.normalized_ids['M001-S002-T0004'], 'M001-S002-T0001');
|
|
272
|
+
assert.deepEqual(out.normalized_ids['M001-S002-T0005'], 'M001-S002-T0002');
|
|
273
|
+
assert.deepEqual(out.normalized_ids['M001-S003-T0006'], 'M001-S003-T0001');
|
|
274
|
+
assert.deepEqual(out.normalized_ids['M001-S003-T0007'], 'M001-S003-T0002');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('PM-11: scaffold-all-tasks is idempotent — already-normalized ids stay unchanged', () => {
|
|
278
|
+
const sandbox = makeSandbox();
|
|
279
|
+
seedRoadmapYaml(sandbox, _roadmap());
|
|
280
|
+
seedMilestoneDir(sandbox, 1, {});
|
|
281
|
+
const plan1 = [
|
|
282
|
+
'---', 'slice: "M001-S001"', 'milestone: "M001"', '---', '',
|
|
283
|
+
'<task id="M001-S001-T0001" wave="1" tier="sonnet" depends_on=""><name>A</name><action>A</action><done>D</done></task>',
|
|
284
|
+
].join('\n');
|
|
285
|
+
const plan2 = [
|
|
286
|
+
'---', 'slice: "M001-S002"', 'milestone: "M001"', '---', '',
|
|
287
|
+
'<task id="M001-S002-T0001" wave="2" tier="sonnet" depends_on="M001-S001-T0001"><name>B</name><action>A</action><done>D</done></task>',
|
|
288
|
+
].join('\n');
|
|
289
|
+
seedSliceDir(sandbox, 1, 1, { 'S001-PLAN.md': plan1 });
|
|
290
|
+
seedSliceDir(sandbox, 1, 2, { 'S002-PLAN.md': plan2 });
|
|
291
|
+
|
|
292
|
+
subcmd.run(['scaffold-all-tasks', '1'], { cwd: sandbox, stdout: _capture().stub });
|
|
293
|
+
const cap = _capture();
|
|
294
|
+
subcmd.run(['scaffold-all-tasks', '1'], { cwd: sandbox, stdout: cap.stub });
|
|
295
|
+
const out = JSON.parse(cap.get().trim());
|
|
296
|
+
assert.deepEqual(out.normalized_ids, {});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('PM-13: scaffold accepts <files_modified> tag and strips bullet prefixes', () => {
|
|
300
|
+
const sandbox = makeSandbox();
|
|
301
|
+
seedRoadmapYaml(sandbox, _roadmap());
|
|
302
|
+
seedMilestoneDir(sandbox, 1, {});
|
|
303
|
+
const slicePlan = [
|
|
304
|
+
'---', 'slice: "M001-S001"', 'milestone: "M001"', '---', '',
|
|
305
|
+
'<task id="M001-S001-T0001" wave="1" tier="sonnet" depends_on="">',
|
|
306
|
+
' <name>Multi-file task</name>',
|
|
307
|
+
' <files_modified>',
|
|
308
|
+
' - src/a.ts',
|
|
309
|
+
' - src/b.ts',
|
|
310
|
+
' - src/c.ts',
|
|
311
|
+
' </files_modified>',
|
|
312
|
+
' <action>Touch three files.</action>',
|
|
313
|
+
' <done>Done.</done>',
|
|
314
|
+
'</task>',
|
|
315
|
+
].join('\n');
|
|
316
|
+
seedSliceDir(sandbox, 1, 1, { 'S001-PLAN.md': slicePlan });
|
|
317
|
+
subcmd.run(['scaffold-slice-tasks', '1', '1'], { cwd: sandbox, stdout: _capture().stub });
|
|
318
|
+
const body = fs.readFileSync(
|
|
319
|
+
path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'tasks', 'T0001', 'T0001-PLAN.md'),
|
|
320
|
+
'utf-8',
|
|
321
|
+
);
|
|
322
|
+
assert.match(body, /- "src\/a.ts"/);
|
|
323
|
+
assert.match(body, /- "src\/b.ts"/);
|
|
324
|
+
assert.match(body, /- "src\/c.ts"/);
|
|
325
|
+
assert.doesNotMatch(body, /- "- /);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('PM-12: scaffold-slice-tasks renumbers its own slice when ids do not start at T0001', () => {
|
|
329
|
+
const sandbox = makeSandbox();
|
|
330
|
+
seedRoadmapYaml(sandbox, _roadmap());
|
|
331
|
+
seedMilestoneDir(sandbox, 1, {});
|
|
332
|
+
const plan2 = [
|
|
333
|
+
'---', 'slice: "M001-S002"', 'milestone: "M001"', '---', '',
|
|
334
|
+
'<task id="M001-S002-T0004" wave="2" tier="sonnet" depends_on=""><name>A</name><action>A</action><done>D</done></task>',
|
|
335
|
+
'<task id="M001-S002-T0005" wave="2" tier="sonnet" depends_on=""><name>B</name><action>A</action><done>D</done></task>',
|
|
336
|
+
].join('\n');
|
|
337
|
+
seedSliceDir(sandbox, 1, 2, { 'S002-PLAN.md': plan2 });
|
|
338
|
+
|
|
339
|
+
const cap = _capture();
|
|
340
|
+
subcmd.run(['scaffold-slice-tasks', '1', '2'], { cwd: sandbox, stdout: cap.stub });
|
|
341
|
+
const out = JSON.parse(cap.get().trim());
|
|
342
|
+
assert.equal(out.task_count, 2);
|
|
343
|
+
|
|
344
|
+
const tasksBase = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S002', 'tasks');
|
|
345
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'T0001')));
|
|
346
|
+
assert.ok(fs.existsSync(path.join(tasksBase, 'T0002')));
|
|
347
|
+
assert.ok(!fs.existsSync(path.join(tasksBase, 'T0004')));
|
|
348
|
+
|
|
349
|
+
const planRaw = fs.readFileSync(
|
|
350
|
+
path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S002', 'S002-PLAN.md'),
|
|
351
|
+
'utf-8',
|
|
352
|
+
);
|
|
353
|
+
assert.match(planRaw, /id="M001-S002-T0001"/);
|
|
354
|
+
assert.doesNotMatch(planRaw, /T0004/);
|
|
355
|
+
});
|