gsd-pi 2.22.0 → 2.23.0
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/README.md +25 -1
- package/dist/cli.js +62 -4
- package/dist/headless.d.ts +21 -0
- package/dist/headless.js +346 -0
- package/dist/help-text.js +32 -0
- package/dist/mcp-server.d.ts +20 -3
- package/dist/mcp-server.js +21 -1
- package/dist/models-resolver.d.ts +32 -0
- package/dist/models-resolver.js +50 -0
- package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/dist/resources/extensions/bg-shell/types.ts +33 -1
- package/dist/resources/extensions/browser-tools/capture.ts +18 -16
- package/dist/resources/extensions/browser-tools/index.ts +20 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +10 -0
- package/dist/resources/extensions/gsd/auto.ts +437 -11
- package/dist/resources/extensions/gsd/captures.ts +49 -0
- package/dist/resources/extensions/gsd/commands.ts +20 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +16 -2
- package/dist/resources/extensions/gsd/diff-context.ts +73 -80
- package/dist/resources/extensions/gsd/doctor.ts +20 -1
- package/dist/resources/extensions/gsd/forensics.ts +95 -52
- package/dist/resources/extensions/gsd/guided-flow.ts +10 -5
- package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
- package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/src/resources/extensions/bg-shell/types.ts +33 -1
- package/src/resources/extensions/browser-tools/capture.ts +18 -16
- package/src/resources/extensions/browser-tools/index.ts +20 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
- package/src/resources/extensions/gsd/auto.ts +437 -11
- package/src/resources/extensions/gsd/captures.ts +49 -0
- package/src/resources/extensions/gsd/commands.ts +20 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +16 -2
- package/src/resources/extensions/gsd/diff-context.ts +73 -80
- package/src/resources/extensions/gsd/doctor.ts +20 -1
- package/src/resources/extensions/gsd/forensics.ts +95 -52
- package/src/resources/extensions/gsd/guided-flow.ts +10 -5
- package/src/resources/extensions/gsd/mcp-server.ts +33 -12
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/src/resources/extensions/gsd/session-forensics.ts +36 -2
- package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/src/resources/extensions/gsd/workspace-index.ts +34 -6
|
@@ -222,3 +222,123 @@ test("dashboard: overlay labels triage-captures and quick-task unit types", () =
|
|
|
222
222
|
"unitLabel should handle quick-task",
|
|
223
223
|
);
|
|
224
224
|
});
|
|
225
|
+
|
|
226
|
+
// ─── Post-triage resolution execution ─────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
test("dispatch: post-triage resolution executor fires after triage-captures unit", () => {
|
|
229
|
+
const triageCompletionBlock = autoSrc.slice(
|
|
230
|
+
autoSrc.indexOf("Post-triage: execute actionable resolutions"),
|
|
231
|
+
autoSrc.indexOf("Path A fix: verify artifact"),
|
|
232
|
+
);
|
|
233
|
+
assert.ok(
|
|
234
|
+
triageCompletionBlock.includes('currentUnit.type === "triage-captures"'),
|
|
235
|
+
"should check for triage-captures unit completion",
|
|
236
|
+
);
|
|
237
|
+
assert.ok(
|
|
238
|
+
triageCompletionBlock.includes("executeTriageResolutions"),
|
|
239
|
+
"should call executeTriageResolutions",
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("dispatch: post-triage executor handles inject results", () => {
|
|
244
|
+
const triageCompletionBlock = autoSrc.slice(
|
|
245
|
+
autoSrc.indexOf("Post-triage: execute actionable resolutions"),
|
|
246
|
+
autoSrc.indexOf("Path A fix: verify artifact"),
|
|
247
|
+
);
|
|
248
|
+
assert.ok(
|
|
249
|
+
triageCompletionBlock.includes("triageResult.injected"),
|
|
250
|
+
"should check injected count",
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("dispatch: post-triage executor handles replan results", () => {
|
|
255
|
+
const triageCompletionBlock = autoSrc.slice(
|
|
256
|
+
autoSrc.indexOf("Post-triage: execute actionable resolutions"),
|
|
257
|
+
autoSrc.indexOf("Path A fix: verify artifact"),
|
|
258
|
+
);
|
|
259
|
+
assert.ok(
|
|
260
|
+
triageCompletionBlock.includes("triageResult.replanned"),
|
|
261
|
+
"should check replanned count",
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("dispatch: post-triage executor queues quick-tasks", () => {
|
|
266
|
+
const triageCompletionBlock = autoSrc.slice(
|
|
267
|
+
autoSrc.indexOf("Post-triage: execute actionable resolutions"),
|
|
268
|
+
autoSrc.indexOf("Path A fix: verify artifact"),
|
|
269
|
+
);
|
|
270
|
+
assert.ok(
|
|
271
|
+
triageCompletionBlock.includes("pendingQuickTasks"),
|
|
272
|
+
"should push quick-tasks to pendingQuickTasks queue",
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ─── Quick-task dispatch ──────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
test("dispatch: quick-task dispatch block exists after triage check", () => {
|
|
279
|
+
const quickTaskBlock = autoSrc.indexOf("Quick-task dispatch: execute queued quick-tasks");
|
|
280
|
+
const triageBlock = autoSrc.indexOf("Triage check: dispatch triage unit");
|
|
281
|
+
const stepModeBlock = autoSrc.indexOf("In step mode, pause and show a wizard");
|
|
282
|
+
|
|
283
|
+
assert.ok(quickTaskBlock > 0, "quick-task dispatch block should exist");
|
|
284
|
+
assert.ok(
|
|
285
|
+
quickTaskBlock > triageBlock,
|
|
286
|
+
"quick-task dispatch should come after triage check",
|
|
287
|
+
);
|
|
288
|
+
assert.ok(
|
|
289
|
+
quickTaskBlock < stepModeBlock,
|
|
290
|
+
"quick-task dispatch should come before step mode check",
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("dispatch: quick-task dispatch uses buildQuickTaskPrompt", () => {
|
|
295
|
+
const quickTaskSection = autoSrc.slice(
|
|
296
|
+
autoSrc.indexOf("Quick-task dispatch: execute queued quick-tasks"),
|
|
297
|
+
autoSrc.indexOf("In step mode, pause and show a wizard"),
|
|
298
|
+
);
|
|
299
|
+
assert.ok(
|
|
300
|
+
quickTaskSection.includes("buildQuickTaskPrompt"),
|
|
301
|
+
"should call buildQuickTaskPrompt for quick-task dispatch",
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("dispatch: quick-task dispatch marks capture as executed", () => {
|
|
306
|
+
const quickTaskSection = autoSrc.slice(
|
|
307
|
+
autoSrc.indexOf("Quick-task dispatch: execute queued quick-tasks"),
|
|
308
|
+
autoSrc.indexOf("In step mode, pause and show a wizard"),
|
|
309
|
+
);
|
|
310
|
+
assert.ok(
|
|
311
|
+
quickTaskSection.includes("markCaptureExecuted"),
|
|
312
|
+
"should mark capture as executed after dispatch",
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("dispatch: quick-task dispatch uses early-return pattern", () => {
|
|
317
|
+
const quickTaskSection = autoSrc.slice(
|
|
318
|
+
autoSrc.indexOf("Quick-task dispatch: execute queued quick-tasks"),
|
|
319
|
+
autoSrc.indexOf("In step mode, pause and show a wizard"),
|
|
320
|
+
);
|
|
321
|
+
assert.ok(
|
|
322
|
+
quickTaskSection.includes("return; // handleAgentEnd will fire again when quick-task session completes"),
|
|
323
|
+
"quick-task dispatch should return after sending message",
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ─── Post-unit hook exclusion for quick-task ──────────────────────────────────
|
|
328
|
+
|
|
329
|
+
test("dispatch: quick-task excluded from post-unit hook triggering", () => {
|
|
330
|
+
assert.ok(
|
|
331
|
+
hooksSrc.includes('"quick-task"'),
|
|
332
|
+
"post-unit-hooks.ts should reference quick-task",
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ─── pendingQuickTasks queue lifecycle ────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
test("dispatch: pendingQuickTasks queue is reset on auto-mode start/stop", () => {
|
|
339
|
+
const resetMatches = autoSrc.match(/pendingQuickTasks = \[\]/g);
|
|
340
|
+
assert.ok(
|
|
341
|
+
resetMatches && resetMatches.length >= 3,
|
|
342
|
+
"pendingQuickTasks should be reset in at least 3 places (start, stop, manual hook)",
|
|
343
|
+
);
|
|
344
|
+
});
|
|
@@ -7,10 +7,10 @@ import assert from "node:assert/strict";
|
|
|
7
7
|
import { mkdirSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
|
-
import { appendCapture, markCaptureResolved, loadAllCaptures } from "../captures.ts";
|
|
10
|
+
import { appendCapture, markCaptureResolved, markCaptureExecuted, loadAllCaptures, loadActionableCaptures } from "../captures.ts";
|
|
11
11
|
// Import only the functions that don't depend on @gsd/pi-coding-agent
|
|
12
12
|
// (triage-ui.ts imports next-action-ui.ts which imports the unavailable package)
|
|
13
|
-
import { executeInject, executeReplan, detectFileOverlap, loadDeferredCaptures, loadReplanCaptures, buildQuickTaskPrompt } from "../triage-resolution.ts";
|
|
13
|
+
import { executeInject, executeReplan, detectFileOverlap, loadDeferredCaptures, loadReplanCaptures, buildQuickTaskPrompt, executeTriageResolutions } from "../triage-resolution.ts";
|
|
14
14
|
|
|
15
15
|
function makeTempDir(prefix: string): string {
|
|
16
16
|
const dir = join(
|
|
@@ -213,3 +213,204 @@ test("resolution: buildQuickTaskPrompt includes capture text and ID", () => {
|
|
|
213
213
|
assert.ok(prompt.includes("Quick Task"), "should have Quick Task header");
|
|
214
214
|
assert.ok(prompt.includes("Do NOT modify"), "should warn about plan files");
|
|
215
215
|
});
|
|
216
|
+
|
|
217
|
+
// ─── markCaptureExecuted ─────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
test("resolution: markCaptureExecuted adds Executed field to capture", () => {
|
|
220
|
+
const tmp = makeTempDir("res-executed");
|
|
221
|
+
try {
|
|
222
|
+
const id = appendCapture(tmp, "fix the button");
|
|
223
|
+
markCaptureResolved(tmp, id, "quick-task", "execute as quick-task", "small fix");
|
|
224
|
+
|
|
225
|
+
markCaptureExecuted(tmp, id);
|
|
226
|
+
|
|
227
|
+
const all = loadAllCaptures(tmp);
|
|
228
|
+
assert.strictEqual(all.length, 1);
|
|
229
|
+
assert.strictEqual(all[0].executed, true, "should be marked as executed");
|
|
230
|
+
} finally {
|
|
231
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("resolution: markCaptureExecuted is idempotent", () => {
|
|
236
|
+
const tmp = makeTempDir("res-executed-idem");
|
|
237
|
+
try {
|
|
238
|
+
const id = appendCapture(tmp, "fix something");
|
|
239
|
+
markCaptureResolved(tmp, id, "inject", "inject task", "needed");
|
|
240
|
+
|
|
241
|
+
markCaptureExecuted(tmp, id);
|
|
242
|
+
markCaptureExecuted(tmp, id); // call again — should not duplicate
|
|
243
|
+
|
|
244
|
+
const filePath = join(tmp, ".gsd", "CAPTURES.md");
|
|
245
|
+
const content = readFileSync(filePath, "utf-8");
|
|
246
|
+
const executedMatches = content.match(/\*\*Executed:\*\*/g);
|
|
247
|
+
assert.strictEqual(executedMatches?.length, 1, "should have exactly one Executed field");
|
|
248
|
+
} finally {
|
|
249
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ─── loadActionableCaptures ──────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
test("resolution: loadActionableCaptures returns only unexecuted actionable captures", () => {
|
|
256
|
+
const tmp = makeTempDir("res-actionable");
|
|
257
|
+
try {
|
|
258
|
+
const id1 = appendCapture(tmp, "inject this task");
|
|
259
|
+
const id2 = appendCapture(tmp, "quick fix");
|
|
260
|
+
const id3 = appendCapture(tmp, "just a note");
|
|
261
|
+
const id4 = appendCapture(tmp, "replan needed");
|
|
262
|
+
const id5 = appendCapture(tmp, "already executed inject");
|
|
263
|
+
|
|
264
|
+
markCaptureResolved(tmp, id1, "inject", "add task", "needed");
|
|
265
|
+
markCaptureResolved(tmp, id2, "quick-task", "quick fix", "small");
|
|
266
|
+
markCaptureResolved(tmp, id3, "note", "acknowledged", "info");
|
|
267
|
+
markCaptureResolved(tmp, id4, "replan", "replan triggered", "approach changed");
|
|
268
|
+
markCaptureResolved(tmp, id5, "inject", "add task", "needed");
|
|
269
|
+
markCaptureExecuted(tmp, id5); // mark as executed
|
|
270
|
+
|
|
271
|
+
const actionable = loadActionableCaptures(tmp);
|
|
272
|
+
assert.strictEqual(actionable.length, 3, "should have 3 actionable captures");
|
|
273
|
+
assert.deepStrictEqual(
|
|
274
|
+
actionable.map(c => c.id),
|
|
275
|
+
[id1, id2, id4],
|
|
276
|
+
"should include inject, quick-task, replan but not note or executed inject",
|
|
277
|
+
);
|
|
278
|
+
} finally {
|
|
279
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ─── executeTriageResolutions ────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
test("resolution: executeTriageResolutions executes inject captures", () => {
|
|
286
|
+
const tmp = makeTempDir("res-exec-inject");
|
|
287
|
+
try {
|
|
288
|
+
setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
|
|
289
|
+
const id1 = appendCapture(tmp, "add error handling");
|
|
290
|
+
const id2 = appendCapture(tmp, "add retry logic");
|
|
291
|
+
markCaptureResolved(tmp, id1, "inject", "add task", "needed");
|
|
292
|
+
markCaptureResolved(tmp, id2, "inject", "add task", "also needed");
|
|
293
|
+
|
|
294
|
+
const result = executeTriageResolutions(tmp, "M001", "S01");
|
|
295
|
+
|
|
296
|
+
assert.strictEqual(result.injected, 2, "should inject 2 tasks");
|
|
297
|
+
assert.strictEqual(result.replanned, 0);
|
|
298
|
+
assert.strictEqual(result.quickTasks.length, 0);
|
|
299
|
+
|
|
300
|
+
// Verify tasks were added to plan
|
|
301
|
+
const planPath = join(tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
|
|
302
|
+
const planContent = readFileSync(planPath, "utf-8");
|
|
303
|
+
assert.ok(planContent.includes("**T04:"), "should have T04");
|
|
304
|
+
assert.ok(planContent.includes("**T05:"), "should have T05");
|
|
305
|
+
|
|
306
|
+
// Verify captures marked as executed
|
|
307
|
+
const all = loadAllCaptures(tmp);
|
|
308
|
+
assert.strictEqual(all[0].executed, true, "first capture should be executed");
|
|
309
|
+
assert.strictEqual(all[1].executed, true, "second capture should be executed");
|
|
310
|
+
} finally {
|
|
311
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("resolution: executeTriageResolutions executes replan captures", () => {
|
|
316
|
+
const tmp = makeTempDir("res-exec-replan");
|
|
317
|
+
try {
|
|
318
|
+
setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
|
|
319
|
+
const id = appendCapture(tmp, "approach is wrong");
|
|
320
|
+
markCaptureResolved(tmp, id, "replan", "replan triggered", "wrong approach");
|
|
321
|
+
|
|
322
|
+
const result = executeTriageResolutions(tmp, "M001", "S01");
|
|
323
|
+
|
|
324
|
+
assert.strictEqual(result.injected, 0);
|
|
325
|
+
assert.strictEqual(result.replanned, 1, "should trigger 1 replan");
|
|
326
|
+
assert.strictEqual(result.quickTasks.length, 0);
|
|
327
|
+
|
|
328
|
+
// Verify trigger file was written
|
|
329
|
+
const triggerPath = join(
|
|
330
|
+
tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-REPLAN-TRIGGER.md",
|
|
331
|
+
);
|
|
332
|
+
assert.ok(existsSync(triggerPath), "replan trigger should exist");
|
|
333
|
+
|
|
334
|
+
// Verify capture marked as executed
|
|
335
|
+
const all = loadAllCaptures(tmp);
|
|
336
|
+
assert.strictEqual(all[0].executed, true, "capture should be executed");
|
|
337
|
+
} finally {
|
|
338
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("resolution: executeTriageResolutions queues quick-tasks without executing inline", () => {
|
|
343
|
+
const tmp = makeTempDir("res-exec-qt");
|
|
344
|
+
try {
|
|
345
|
+
const id = appendCapture(tmp, "fix typo in readme");
|
|
346
|
+
markCaptureResolved(tmp, id, "quick-task", "execute as quick-task", "small fix");
|
|
347
|
+
|
|
348
|
+
const result = executeTriageResolutions(tmp, "M001", "S01");
|
|
349
|
+
|
|
350
|
+
assert.strictEqual(result.injected, 0);
|
|
351
|
+
assert.strictEqual(result.replanned, 0);
|
|
352
|
+
assert.strictEqual(result.quickTasks.length, 1, "should queue 1 quick-task");
|
|
353
|
+
assert.strictEqual(result.quickTasks[0].id, id);
|
|
354
|
+
|
|
355
|
+
// Quick-tasks should NOT be marked as executed yet (caller marks after dispatch)
|
|
356
|
+
const all = loadAllCaptures(tmp);
|
|
357
|
+
assert.ok(!all[0].executed, "quick-task should not be executed yet");
|
|
358
|
+
} finally {
|
|
359
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("resolution: executeTriageResolutions handles mixed classifications", () => {
|
|
364
|
+
const tmp = makeTempDir("res-exec-mixed");
|
|
365
|
+
try {
|
|
366
|
+
setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
|
|
367
|
+
const id1 = appendCapture(tmp, "inject a task");
|
|
368
|
+
const id2 = appendCapture(tmp, "quick fix typo");
|
|
369
|
+
const id3 = appendCapture(tmp, "just a note");
|
|
370
|
+
const id4 = appendCapture(tmp, "defer to later");
|
|
371
|
+
|
|
372
|
+
markCaptureResolved(tmp, id1, "inject", "add task", "needed");
|
|
373
|
+
markCaptureResolved(tmp, id2, "quick-task", "quick fix", "small");
|
|
374
|
+
markCaptureResolved(tmp, id3, "note", "acknowledged", "info");
|
|
375
|
+
markCaptureResolved(tmp, id4, "defer", "deferred", "later");
|
|
376
|
+
|
|
377
|
+
const result = executeTriageResolutions(tmp, "M001", "S01");
|
|
378
|
+
|
|
379
|
+
assert.strictEqual(result.injected, 1, "should inject 1 task");
|
|
380
|
+
assert.strictEqual(result.replanned, 0);
|
|
381
|
+
assert.strictEqual(result.quickTasks.length, 1, "should queue 1 quick-task");
|
|
382
|
+
assert.strictEqual(result.actions.length, 2, "should have 2 action entries (note/defer excluded)");
|
|
383
|
+
} finally {
|
|
384
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("resolution: executeTriageResolutions skips already-executed captures", () => {
|
|
389
|
+
const tmp = makeTempDir("res-exec-skip");
|
|
390
|
+
try {
|
|
391
|
+
setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
|
|
392
|
+
const id = appendCapture(tmp, "already done");
|
|
393
|
+
markCaptureResolved(tmp, id, "inject", "add task", "needed");
|
|
394
|
+
markCaptureExecuted(tmp, id); // already executed
|
|
395
|
+
|
|
396
|
+
const result = executeTriageResolutions(tmp, "M001", "S01");
|
|
397
|
+
|
|
398
|
+
assert.strictEqual(result.injected, 0, "should not inject again");
|
|
399
|
+
assert.strictEqual(result.actions.length, 0, "should have no actions");
|
|
400
|
+
} finally {
|
|
401
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("resolution: executeTriageResolutions returns empty result when no actionable captures", () => {
|
|
406
|
+
const tmp = makeTempDir("res-exec-empty");
|
|
407
|
+
try {
|
|
408
|
+
const result = executeTriageResolutions(tmp, "M001", "S01");
|
|
409
|
+
assert.strictEqual(result.injected, 0);
|
|
410
|
+
assert.strictEqual(result.replanned, 0);
|
|
411
|
+
assert.strictEqual(result.quickTasks.length, 0);
|
|
412
|
+
assert.strictEqual(result.actions.length, 0);
|
|
413
|
+
} finally {
|
|
414
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Tests for GSD visualizer overlay.
|
|
2
|
-
// Verifies filter mode, tab switching, and export key handling.
|
|
2
|
+
// Verifies filter mode, tab switching, including reverse tab navigation, and export key handling.
|
|
3
3
|
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { join, dirname } from "node:path";
|
|
@@ -81,6 +81,11 @@ assertTrue(
|
|
|
81
81
|
"tab key wraps around TAB_COUNT",
|
|
82
82
|
);
|
|
83
83
|
|
|
84
|
+
assertTrue(
|
|
85
|
+
overlaySrc.includes('Key.shift("tab")') || overlaySrc.includes("Key.shift('tab')"),
|
|
86
|
+
"supports Shift+Tab for reverse tab switching",
|
|
87
|
+
);
|
|
88
|
+
|
|
84
89
|
console.log("\n=== Overlay: Export Key Interception ===");
|
|
85
90
|
|
|
86
91
|
assertTrue(
|
|
@@ -101,8 +106,8 @@ assertTrue(
|
|
|
101
106
|
console.log("\n=== Overlay: Footer ===");
|
|
102
107
|
|
|
103
108
|
assertTrue(
|
|
104
|
-
overlaySrc.includes("Tab/1-7"),
|
|
105
|
-
"footer hint shows 1-7 tab range",
|
|
109
|
+
overlaySrc.includes("Tab/Shift+Tab/1-7"),
|
|
110
|
+
"footer hint shows Tab, Shift+Tab, and 1-7 tab range",
|
|
106
111
|
);
|
|
107
112
|
|
|
108
113
|
assertTrue(
|
|
@@ -16,7 +16,9 @@ import type { Classification, CaptureEntry } from "./captures.js";
|
|
|
16
16
|
import {
|
|
17
17
|
loadPendingCaptures,
|
|
18
18
|
loadAllCaptures,
|
|
19
|
+
loadActionableCaptures,
|
|
19
20
|
markCaptureResolved,
|
|
21
|
+
markCaptureExecuted,
|
|
20
22
|
} from "./captures.js";
|
|
21
23
|
|
|
22
24
|
// ─── Resolution Executors ─────────────────────────────────────────────────────
|
|
@@ -198,3 +200,84 @@ export function buildQuickTaskPrompt(capture: CaptureEntry): string {
|
|
|
198
200
|
`5. When done, say: "Quick task complete."`,
|
|
199
201
|
].join("\n");
|
|
200
202
|
}
|
|
203
|
+
|
|
204
|
+
// ─── Post-Triage Resolution Executor ─────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Result of executing triage resolutions after a triage-captures unit completes.
|
|
208
|
+
*/
|
|
209
|
+
export interface TriageExecutionResult {
|
|
210
|
+
/** Number of inject resolutions executed (tasks added to plan) */
|
|
211
|
+
injected: number;
|
|
212
|
+
/** Number of replan triggers written */
|
|
213
|
+
replanned: number;
|
|
214
|
+
/** Captures classified as quick-task that need dispatch */
|
|
215
|
+
quickTasks: CaptureEntry[];
|
|
216
|
+
/** Details of each action taken, for logging */
|
|
217
|
+
actions: string[];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Execute pending triage resolutions.
|
|
222
|
+
*
|
|
223
|
+
* Called after a triage-captures unit completes. Reads CAPTURES.md for
|
|
224
|
+
* resolved captures that have actionable classifications (inject, replan,
|
|
225
|
+
* quick-task) but haven't been executed yet, then:
|
|
226
|
+
*
|
|
227
|
+
* - inject: calls executeInject() to add a task to the current slice plan
|
|
228
|
+
* - replan: calls executeReplan() to write the REPLAN-TRIGGER.md marker
|
|
229
|
+
* - quick-task: collects for dispatch (caller handles dispatching quick-task units)
|
|
230
|
+
*
|
|
231
|
+
* Each capture is marked as executed after its resolution action succeeds,
|
|
232
|
+
* preventing double-execution on retries or restarts.
|
|
233
|
+
*/
|
|
234
|
+
export function executeTriageResolutions(
|
|
235
|
+
basePath: string,
|
|
236
|
+
mid: string,
|
|
237
|
+
sid: string,
|
|
238
|
+
): TriageExecutionResult {
|
|
239
|
+
const result: TriageExecutionResult = {
|
|
240
|
+
injected: 0,
|
|
241
|
+
replanned: 0,
|
|
242
|
+
quickTasks: [],
|
|
243
|
+
actions: [],
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const actionable = loadActionableCaptures(basePath);
|
|
247
|
+
if (actionable.length === 0) return result;
|
|
248
|
+
|
|
249
|
+
for (const capture of actionable) {
|
|
250
|
+
switch (capture.classification) {
|
|
251
|
+
case "inject": {
|
|
252
|
+
const newTaskId = executeInject(basePath, mid, sid, capture);
|
|
253
|
+
if (newTaskId) {
|
|
254
|
+
markCaptureExecuted(basePath, capture.id);
|
|
255
|
+
result.injected++;
|
|
256
|
+
result.actions.push(`Injected ${newTaskId} from ${capture.id}: "${capture.text}"`);
|
|
257
|
+
} else {
|
|
258
|
+
result.actions.push(`Failed to inject ${capture.id}: "${capture.text}" (no plan file or parse error)`);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "replan": {
|
|
263
|
+
const success = executeReplan(basePath, mid, sid, capture);
|
|
264
|
+
if (success) {
|
|
265
|
+
markCaptureExecuted(basePath, capture.id);
|
|
266
|
+
result.replanned++;
|
|
267
|
+
result.actions.push(`Replan triggered from ${capture.id}: "${capture.text}"`);
|
|
268
|
+
} else {
|
|
269
|
+
result.actions.push(`Failed to trigger replan from ${capture.id}: "${capture.text}"`);
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case "quick-task": {
|
|
274
|
+
// Quick-tasks are collected for dispatch, not executed inline
|
|
275
|
+
result.quickTasks.push(capture);
|
|
276
|
+
result.actions.push(`Quick-task queued from ${capture.id}: "${capture.text}"`);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
@@ -112,6 +112,13 @@ export class GSDVisualizerOverlay {
|
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
if (matchesKey(data, Key.shift("tab"))) {
|
|
116
|
+
this.activeTab = (this.activeTab - 1 + TAB_COUNT) % TAB_COUNT;
|
|
117
|
+
this.invalidate();
|
|
118
|
+
this.tui.requestRender();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
if (matchesKey(data, Key.tab)) {
|
|
116
123
|
this.activeTab = (this.activeTab + 1) % TAB_COUNT;
|
|
117
124
|
this.invalidate();
|
|
@@ -300,7 +307,7 @@ export class GSDVisualizerOverlay {
|
|
|
300
307
|
const lines = this.wrapInBox(visibleContent, width);
|
|
301
308
|
|
|
302
309
|
// Footer hint
|
|
303
|
-
const hint = th.fg("dim", "Tab/1-7 switch · / filter · ↑↓ scroll · g/G top/end · esc close");
|
|
310
|
+
const hint = th.fg("dim", "Tab/Shift+Tab/1-7 switch · / filter · ↑↓ scroll · g/G top/end · esc close");
|
|
304
311
|
const hintVis = visibleWidth(hint);
|
|
305
312
|
const hintPad = Math.max(0, Math.floor((width - hintVis) / 2));
|
|
306
313
|
lines.push(" ".repeat(hintPad) + hint);
|
|
@@ -103,10 +103,21 @@ async function indexSlice(basePath: string, milestoneId: string, sliceId: string
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
export
|
|
106
|
+
export interface IndexWorkspaceOptions {
|
|
107
|
+
/**
|
|
108
|
+
* When true, run validatePlanBoundary and validateCompleteBoundary for each slice.
|
|
109
|
+
* Skipped by default — validation is expensive (content analysis) and only needed
|
|
110
|
+
* for explicit doctor/audit flows. The /gsd status dashboard and scope pickers
|
|
111
|
+
* don't need the full issue list.
|
|
112
|
+
*/
|
|
113
|
+
validate?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function indexWorkspace(basePath: string, opts: IndexWorkspaceOptions = {}): Promise<GSDWorkspaceIndex> {
|
|
107
117
|
const milestoneIds = findMilestoneIds(basePath);
|
|
108
118
|
const milestones: WorkspaceMilestoneTarget[] = [];
|
|
109
119
|
const validationIssues: ValidationIssue[] = [];
|
|
120
|
+
const runValidation = opts.validate === true;
|
|
110
121
|
|
|
111
122
|
for (const milestoneId of milestoneIds) {
|
|
112
123
|
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP") ?? undefined;
|
|
@@ -118,11 +129,27 @@ export async function indexWorkspace(basePath: string): Promise<GSDWorkspaceInde
|
|
|
118
129
|
if (roadmapContent) {
|
|
119
130
|
const roadmap = parseRoadmap(roadmapContent);
|
|
120
131
|
title = titleFromRoadmapHeader(roadmapContent, milestoneId);
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
|
|
133
|
+
// Parallelise all per-slice I/O: indexSlice + (optional) validation calls run concurrently.
|
|
134
|
+
// Order is preserved via Promise.all on an array built from roadmap.slices.
|
|
135
|
+
const sliceResults = await Promise.all(
|
|
136
|
+
roadmap.slices.map(async (slice) => {
|
|
137
|
+
if (runValidation) {
|
|
138
|
+
const [indexedSlice, planIssues, completeIssues] = await Promise.all([
|
|
139
|
+
indexSlice(basePath, milestoneId, slice.id, slice.title, slice.done),
|
|
140
|
+
validatePlanBoundary(basePath, milestoneId, slice.id),
|
|
141
|
+
validateCompleteBoundary(basePath, milestoneId, slice.id),
|
|
142
|
+
]);
|
|
143
|
+
return { indexedSlice, issues: [...planIssues, ...completeIssues] };
|
|
144
|
+
}
|
|
145
|
+
const indexedSlice = await indexSlice(basePath, milestoneId, slice.id, slice.title, slice.done);
|
|
146
|
+
return { indexedSlice, issues: [] as ValidationIssue[] };
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
for (const { indexedSlice, issues } of sliceResults) {
|
|
123
151
|
slices.push(indexedSlice);
|
|
124
|
-
validationIssues.push(...
|
|
125
|
-
validationIssues.push(...await validateCompleteBoundary(basePath, milestoneId, slice.id));
|
|
152
|
+
validationIssues.push(...issues);
|
|
126
153
|
}
|
|
127
154
|
}
|
|
128
155
|
}
|
|
@@ -173,7 +200,8 @@ export async function listDoctorScopeSuggestions(basePath: string): Promise<Arra
|
|
|
173
200
|
}
|
|
174
201
|
|
|
175
202
|
export async function getSuggestedNextCommands(basePath: string): Promise<string[]> {
|
|
176
|
-
|
|
203
|
+
// Run validation here since we surface a /gsd doctor audit hint when issues exist.
|
|
204
|
+
const index = await indexWorkspace(basePath, { validate: true });
|
|
177
205
|
const scope = index.active.milestoneId && index.active.sliceId
|
|
178
206
|
? `${index.active.milestoneId}/${index.active.sliceId}`
|
|
179
207
|
: index.active.milestoneId;
|