nubos-pilot 0.5.6 → 0.5.7

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.
@@ -8,6 +8,7 @@ const crypto = require('node:crypto');
8
8
  const {
9
9
  NubosPilotError,
10
10
  projectStateDir,
11
+ atomicWriteFileSync,
11
12
  } = require('../../lib/core.cjs');
12
13
  const layout = require('../../lib/layout.cjs');
13
14
  const { getPhase } = require('../../lib/roadmap.cjs');
@@ -141,6 +142,84 @@ function _initPayload(mNum, cwd) {
141
142
  };
142
143
  }
143
144
 
145
+ function _readTaskSummaryBody(summaryPath) {
146
+ if (!fs.existsSync(summaryPath)) return null;
147
+ const raw = fs.readFileSync(summaryPath, 'utf-8');
148
+ const { body } = extractFrontmatter(raw);
149
+ return String(body || '').trim();
150
+ }
151
+
152
+ function _finalizeSlice(mNum, sNum, cwd) {
153
+ const slicePath = layout.findSliceDir(mNum, sNum, cwd);
154
+ if (!slicePath) {
155
+ throw new NubosPilotError(
156
+ 'finalize-slice-not-found',
157
+ 'Slice ' + layout.sliceFullId(mNum, sNum) + ' does not exist',
158
+ { milestone: mNum, slice: sNum },
159
+ );
160
+ }
161
+ const summaryPath = layout.sliceSummaryPath(mNum, sNum, cwd);
162
+ const tasks = _sliceTasksSorted(mNum, sNum, cwd);
163
+ const doneTasks = tasks.filter((t) => t.status === 'done');
164
+ const pendingTasks = tasks.filter((t) => t.status !== 'done');
165
+
166
+ const lines = [
167
+ '---',
168
+ 'slice: ' + JSON.stringify(layout.sliceFullId(mNum, sNum)),
169
+ 'milestone: ' + JSON.stringify(layout.mId(mNum)),
170
+ 'type: slice-summary',
171
+ 'task_count: ' + tasks.length,
172
+ 'tasks_done: ' + doneTasks.length,
173
+ 'tasks_pending: ' + pendingTasks.length,
174
+ 'generated_at: ' + JSON.stringify(new Date().toISOString()),
175
+ '---',
176
+ '',
177
+ '# ' + layout.sliceFullId(mNum, sNum) + ' — SUMMARY',
178
+ '',
179
+ '_Auto-aggregated from task summaries by `execute-milestone finalize-slice`._',
180
+ '',
181
+ '## Task Roll-Up',
182
+ '',
183
+ '| Task | Status | Name |',
184
+ '|------|--------|------|',
185
+ ];
186
+ for (const t of tasks) {
187
+ lines.push('| ' + t.id + ' | ' + t.status + ' | ' + (t.name || '').replace(/\|/g, '\\|') + ' |');
188
+ }
189
+ lines.push('', '## Task Summaries', '');
190
+ for (const t of tasks) {
191
+ lines.push('### ' + t.id + ' — ' + (t.name || ''));
192
+ lines.push('');
193
+ const body = _readTaskSummaryBody(t.summary_path);
194
+ if (body) {
195
+ lines.push(body);
196
+ } else {
197
+ lines.push('_No T<NNNN>-SUMMARY.md file present._');
198
+ }
199
+ lines.push('');
200
+ }
201
+ atomicWriteFileSync(summaryPath, lines.join('\n'));
202
+ return {
203
+ slice: layout.sliceFullId(mNum, sNum),
204
+ summary_path: summaryPath,
205
+ task_count: tasks.length,
206
+ tasks_done: doneTasks.length,
207
+ tasks_pending: pendingTasks.length,
208
+ };
209
+ }
210
+
211
+ function _finalizeMilestone(mNum, cwd) {
212
+ const slices = layout.listSlices(mNum, cwd);
213
+ if (slices.length === 0) {
214
+ return { milestone: layout.mId(mNum), finalized: [], reason: 'no-slices' };
215
+ }
216
+ const finalized = [];
217
+ for (const s of slices) {
218
+ finalized.push(_finalizeSlice(mNum, s.number, cwd));
219
+ }
220
+ return { milestone: layout.mId(mNum), finalized, reason: 'ok' };
221
+ }
222
+
144
223
  function _findTaskByFullId(mNum, taskFullId, cwd) {
145
224
  let parsed;
146
225
  try {
@@ -218,6 +297,19 @@ function run(args, ctx) {
218
297
  _emit(payload, stdout, cwd);
219
298
  return payload;
220
299
  }
300
+ case 'finalize-slice': {
301
+ const mNum = _validateMilestoneArg(list[1]);
302
+ const sNum = _validateMilestoneArg(list[2]);
303
+ const payload = _finalizeSlice(mNum, sNum, cwd);
304
+ _emit(payload, stdout, cwd);
305
+ return payload;
306
+ }
307
+ case 'finalize-milestone': {
308
+ const mNum = _validateMilestoneArg(list[1]);
309
+ const payload = _finalizeMilestone(mNum, cwd);
310
+ _emit(payload, stdout, cwd);
311
+ return payload;
312
+ }
221
313
  default:
222
314
  throw new NubosPilotError(
223
315
  'execute-milestone-unknown-verb',
@@ -152,3 +152,98 @@ test('EM-6: unknown verb throws', () => {
152
152
  (err) => err && err.code === 'execute-milestone-unknown-verb',
153
153
  );
154
154
  });
155
+
156
+ function _writeTaskSummary(sandbox, mNum, sNum, tNum, body) {
157
+ const mId = 'M' + String(mNum).padStart(3, '0');
158
+ const sId = 'S' + String(sNum).padStart(3, '0');
159
+ const tId = 'T' + String(tNum).padStart(4, '0');
160
+ const fullId = mId + '-' + sId + '-' + tId;
161
+ const dir = path.join(sandbox, '.nubos-pilot', 'milestones', mId, 'slices', sId, 'tasks', tId);
162
+ const content = [
163
+ '---',
164
+ 'id: ' + JSON.stringify(fullId),
165
+ 'status: done',
166
+ '---',
167
+ '',
168
+ body,
169
+ '',
170
+ ].join('\n');
171
+ fs.writeFileSync(path.join(dir, tId + '-SUMMARY.md'), content);
172
+ }
173
+
174
+ test('EM-7: finalize-slice writes S<NNN>-SUMMARY.md aggregating task summaries', () => {
175
+ const sandbox = makeSandbox();
176
+ seedRoadmapYaml(sandbox, _roadmap());
177
+ seedMilestoneDir(sandbox, 1, {});
178
+ seedSliceDir(sandbox, 1, 1, {});
179
+ _seedTask(sandbox, 1, 1, 1, ['src/a.ts']);
180
+ _seedTask(sandbox, 1, 1, 2, ['src/b.ts']);
181
+ _writeTaskSummary(sandbox, 1, 1, 1, '## Changes\n- Added src/a.ts');
182
+ _writeTaskSummary(sandbox, 1, 1, 2, '## Changes\n- Added src/b.ts');
183
+
184
+ const cap = _capture();
185
+ subcmd.run(['finalize-slice', '1', '1'], { cwd: sandbox, stdout: cap.stub });
186
+ const out = JSON.parse(cap.get());
187
+ assert.equal(out.slice, 'M001-S001');
188
+ assert.equal(out.task_count, 2);
189
+
190
+ const summaryPath = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'S001-SUMMARY.md');
191
+ assert.ok(fs.existsSync(summaryPath));
192
+ const body = fs.readFileSync(summaryPath, 'utf-8');
193
+ assert.match(body, /slice: "M001-S001"/);
194
+ assert.match(body, /type: slice-summary/);
195
+ assert.match(body, /### M001-S001-T0001/);
196
+ assert.match(body, /### M001-S001-T0002/);
197
+ assert.match(body, /Added src\/a.ts/);
198
+ assert.match(body, /Added src\/b.ts/);
199
+ });
200
+
201
+ test('EM-8: finalize-slice fails when slice directory does not exist', () => {
202
+ const sandbox = makeSandbox();
203
+ seedRoadmapYaml(sandbox, _roadmap());
204
+ seedMilestoneDir(sandbox, 1, {});
205
+ const cap = _capture();
206
+ assert.throws(
207
+ () => subcmd.run(['finalize-slice', '1', '9'], { cwd: sandbox, stdout: cap.stub }),
208
+ (err) => err && err.code === 'finalize-slice-not-found',
209
+ );
210
+ });
211
+
212
+ test('EM-9: finalize-milestone iterates every slice and produces one summary per slice', () => {
213
+ const sandbox = makeSandbox();
214
+ seedRoadmapYaml(sandbox, _roadmap());
215
+ seedMilestoneDir(sandbox, 1, {});
216
+ seedSliceDir(sandbox, 1, 1, {});
217
+ seedSliceDir(sandbox, 1, 2, {});
218
+ _seedTask(sandbox, 1, 1, 1, ['src/a.ts']);
219
+ _seedTask(sandbox, 1, 2, 1, ['src/c.ts']);
220
+
221
+ const cap = _capture();
222
+ subcmd.run(['finalize-milestone', '1'], { cwd: sandbox, stdout: cap.stub });
223
+ const out = JSON.parse(cap.get());
224
+ assert.equal(out.milestone, 'M001');
225
+ assert.equal(out.finalized.length, 2);
226
+ assert.equal(out.reason, 'ok');
227
+
228
+ const s1 = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'S001-SUMMARY.md');
229
+ const s2 = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S002', 'S002-SUMMARY.md');
230
+ assert.ok(fs.existsSync(s1));
231
+ assert.ok(fs.existsSync(s2));
232
+ });
233
+
234
+ test('EM-10: finalize-slice marks tasks without SUMMARY.md but does not fail', () => {
235
+ const sandbox = makeSandbox();
236
+ seedRoadmapYaml(sandbox, _roadmap());
237
+ seedMilestoneDir(sandbox, 1, {});
238
+ seedSliceDir(sandbox, 1, 1, {});
239
+ _seedTask(sandbox, 1, 1, 1, ['src/a.ts']);
240
+ const dir = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'tasks', 'T0001');
241
+ fs.rmSync(path.join(dir, 'T0001-SUMMARY.md'));
242
+
243
+ const cap = _capture();
244
+ subcmd.run(['finalize-slice', '1', '1'], { cwd: sandbox, stdout: cap.stub });
245
+ const out = JSON.parse(cap.get());
246
+ assert.equal(out.task_count, 1);
247
+ const body = fs.readFileSync(out.summary_path, 'utf-8');
248
+ assert.match(body, /No T<NNNN>-SUMMARY.md file present/);
249
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
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": {
@@ -136,7 +136,16 @@ for WAVE_INDEX in 0 1 2 ...; do
136
136
  fi
137
137
  done
138
138
  # wait for all parallel executors in this wave to finish before next wave
139
+
140
+ # After every task in the slice committed: aggregate per-task summaries into
141
+ # the slice-level S<NNN>-SUMMARY.md so /np:validate-phase can audit it.
142
+ SLICE_NUM=$(echo "$WAVE" | node -e "process.stdin.on('data', d => console.log(JSON.parse(d).wave))")
143
+ node .nubos-pilot/bin/np-tools.cjs init execute-milestone finalize-slice "$PHASE" "$SLICE_NUM" >/dev/null
139
144
  done
145
+
146
+ # Milestone done — regenerate every slice summary so retroactive / resumed
147
+ # runs also end with a complete audit surface.
148
+ node .nubos-pilot/bin/np-tools.cjs init execute-milestone finalize-milestone "$PHASE" >/dev/null
140
149
  ```
141
150
 
142
151
  After every slice completes, point the operator at `/np:validate-phase $PHASE` to run the UAT per slice.