gsd-pi 2.80.0-dev.2848d9009 → 2.80.0-dev.2be7caf18
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 +3 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +32 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
- package/dist/resources/extensions/gsd/auto.js +7 -2
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
- package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
- package/dist/resources/extensions/gsd/guided-flow.js +33 -6
- package/dist/resources/extensions/gsd/migrate/command.js +40 -1
- package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
- package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/quick.js +34 -2
- package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
- package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
- package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
- package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +22 -17
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +66 -0
- package/packages/mcp-server/src/workflow-tools.ts +30 -16
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
- package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
- package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
- package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +18 -8
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +20 -8
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +50 -8
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
- package/src/resources/extensions/gsd/auto.ts +11 -2
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
- package/src/resources/extensions/gsd/guided-flow.ts +39 -6
- package/src/resources/extensions/gsd/migrate/command.ts +47 -1
- package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
- package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/quick.ts +37 -2
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
- package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
- package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
- package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
- /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → 3EDDd9ULaybSupPoA_vf-}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → 3EDDd9ULaybSupPoA_vf-}/_ssgManifest.js +0 -0
|
@@ -38,6 +38,7 @@ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
|
|
|
38
38
|
import { gsdRoot, normalizeRealPath } from "./paths.js";
|
|
39
39
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
40
40
|
import { effectiveLockFile } from "./session-lock.js";
|
|
41
|
+
import { isInFlightRuntimePhase, listUnitRuntimeRecords, type AutoUnitRuntimeRecord } from "./unit-runtime.js";
|
|
41
42
|
|
|
42
43
|
export interface LockData {
|
|
43
44
|
pid: number;
|
|
@@ -103,10 +104,40 @@ function getLatestDispatchForWorker(workerId: string):
|
|
|
103
104
|
return row ?? null;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
function
|
|
107
|
+
function latestInFlightRuntimeRecord(basePath: string): AutoUnitRuntimeRecord | null {
|
|
108
|
+
const records = listUnitRuntimeRecords(basePath).filter((record) =>
|
|
109
|
+
isInFlightRuntimePhase(record.phase),
|
|
110
|
+
);
|
|
111
|
+
if (records.length === 0) return null;
|
|
112
|
+
return records.sort((a, b) => {
|
|
113
|
+
const bTime = b.updatedAt || b.startedAt || 0;
|
|
114
|
+
const aTime = a.updatedAt || a.startedAt || 0;
|
|
115
|
+
return bTime - aTime;
|
|
116
|
+
})[0] ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function runtimeRecordToLockData(worker: AutoWorkerRow, record: AutoUnitRuntimeRecord, sessionFile?: string): LockData {
|
|
120
|
+
const startedAt = Number.isFinite(record.startedAt)
|
|
121
|
+
? new Date(record.startedAt).toISOString()
|
|
122
|
+
: worker.started_at;
|
|
123
|
+
return {
|
|
124
|
+
pid: worker.pid,
|
|
125
|
+
startedAt: worker.started_at,
|
|
126
|
+
unitType: record.unitType,
|
|
127
|
+
unitId: record.unitId,
|
|
128
|
+
unitStartedAt: startedAt,
|
|
129
|
+
sessionFile,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function workerToLockData(basePath: string, worker: AutoWorkerRow): LockData {
|
|
107
134
|
const dispatch = getLatestDispatchForWorker(worker.worker_id);
|
|
108
135
|
const sessionFile =
|
|
109
136
|
getRuntimeKv<string>("worker", worker.worker_id, SESSION_FILE_KV_KEY) ?? undefined;
|
|
137
|
+
if (!dispatch) {
|
|
138
|
+
const runtimeRecord = latestInFlightRuntimeRecord(basePath);
|
|
139
|
+
if (runtimeRecord) return runtimeRecordToLockData(worker, runtimeRecord, sessionFile);
|
|
140
|
+
}
|
|
110
141
|
return {
|
|
111
142
|
pid: worker.pid,
|
|
112
143
|
startedAt: worker.started_at,
|
|
@@ -204,7 +235,7 @@ export function readCrashLock(basePath: string): LockData | null {
|
|
|
204
235
|
try {
|
|
205
236
|
const projectRoot = normalizeRealPath(basePath);
|
|
206
237
|
const stale = findStaleWorkerForProject(projectRoot);
|
|
207
|
-
if (stale) return workerToLockData(stale);
|
|
238
|
+
if (stale) return workerToLockData(basePath, stale);
|
|
208
239
|
} catch {
|
|
209
240
|
// Fall through to the legacy lock-file compatibility path.
|
|
210
241
|
}
|
|
@@ -260,25 +291,48 @@ export function formatCrashInfo(lock: LockData): string {
|
|
|
260
291
|
*/
|
|
261
292
|
export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): void {
|
|
262
293
|
if (!lock.unitType || !lock.unitId || lock.unitType === "starting") return;
|
|
294
|
+
emitOpenUnitEndForUnit(basePath, lock.unitType, lock.unitId, "crash-recovered");
|
|
295
|
+
}
|
|
263
296
|
|
|
297
|
+
export function emitOpenUnitEndForUnit(
|
|
298
|
+
basePath: string,
|
|
299
|
+
unitType: string,
|
|
300
|
+
unitId: string,
|
|
301
|
+
status: string,
|
|
302
|
+
errorContext?: { message: string; category: string; stopReason?: string; isTransient?: boolean; retryAfterMs?: number },
|
|
303
|
+
): boolean {
|
|
264
304
|
try {
|
|
265
305
|
const all = queryJournal(basePath);
|
|
266
306
|
|
|
267
307
|
const starts = all.filter(
|
|
268
|
-
(e) =>
|
|
308
|
+
(e) =>
|
|
309
|
+
e.eventType === "unit-start" &&
|
|
310
|
+
e.data?.unitType === unitType &&
|
|
311
|
+
e.data?.unitId === unitId,
|
|
269
312
|
);
|
|
270
|
-
if (starts.length === 0) return;
|
|
313
|
+
if (starts.length === 0) return false;
|
|
271
314
|
|
|
272
|
-
const lastStart =
|
|
315
|
+
const lastStart = [...starts].reverse().find((start) => {
|
|
316
|
+
return !all.some(
|
|
317
|
+
(e) =>
|
|
318
|
+
e.eventType === "unit-end" &&
|
|
319
|
+
e.data?.unitType === unitType &&
|
|
320
|
+
e.data?.unitId === unitId &&
|
|
321
|
+
e.causedBy?.flowId === start.flowId &&
|
|
322
|
+
e.causedBy?.seq === start.seq,
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
if (!lastStart) return false;
|
|
273
326
|
|
|
274
327
|
const alreadyClosed = all.some(
|
|
275
328
|
(e) =>
|
|
276
329
|
e.eventType === "unit-end" &&
|
|
277
|
-
e.data?.
|
|
330
|
+
e.data?.unitType === unitType &&
|
|
331
|
+
e.data?.unitId === unitId &&
|
|
278
332
|
e.causedBy?.flowId === lastStart.flowId &&
|
|
279
333
|
e.causedBy?.seq === lastStart.seq,
|
|
280
334
|
);
|
|
281
|
-
if (alreadyClosed) return;
|
|
335
|
+
if (alreadyClosed) return false;
|
|
282
336
|
|
|
283
337
|
const maxSeq = all
|
|
284
338
|
.filter((e) => e.flowId === lastStart.flowId)
|
|
@@ -290,15 +344,18 @@ export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): voi
|
|
|
290
344
|
seq: maxSeq + 1,
|
|
291
345
|
eventType: "unit-end",
|
|
292
346
|
data: {
|
|
293
|
-
unitType
|
|
294
|
-
unitId
|
|
295
|
-
status
|
|
347
|
+
unitType,
|
|
348
|
+
unitId,
|
|
349
|
+
status,
|
|
296
350
|
artifactVerified: false,
|
|
351
|
+
...(errorContext ? { errorContext } : {}),
|
|
297
352
|
},
|
|
298
353
|
causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
|
|
299
354
|
});
|
|
355
|
+
return true;
|
|
300
356
|
} catch {
|
|
301
357
|
// Never throw from crash recovery path.
|
|
358
|
+
return false;
|
|
302
359
|
}
|
|
303
360
|
}
|
|
304
361
|
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
formatInterruptedSessionRunningMessage,
|
|
31
31
|
formatInterruptedSessionSummary,
|
|
32
32
|
} from "./interrupted-session.js";
|
|
33
|
-
import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
33
|
+
import { listUnitRuntimeRecords, clearUnitRuntimeRecord, isInFlightRuntimePhase } from "./unit-runtime.js";
|
|
34
34
|
import { resolveExpectedArtifactPath } from "./auto.js";
|
|
35
35
|
import { gsdHome } from "./gsd-home.js";
|
|
36
36
|
import {
|
|
@@ -118,6 +118,22 @@ export function resolveExpectedArtifactPathForScope(
|
|
|
118
118
|
return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
async function runQuickTaskChoice(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
122
|
+
if (!ctx.hasUI) {
|
|
123
|
+
ctx.ui.notify("Run /gsd quick <task> for small bounded work, or /gsd do <task> for natural-language routing.", "info");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const task = (await ctx.ui.input("Quick task", "Describe the small task to run with /gsd quick"))?.trim();
|
|
128
|
+
if (!task) {
|
|
129
|
+
ctx.ui.notify("Quick task cancelled.", "info");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { handleQuick } = await import("./quick.js");
|
|
134
|
+
await handleQuick(task, ctx, pi);
|
|
135
|
+
}
|
|
136
|
+
|
|
121
137
|
/**
|
|
122
138
|
* Scope-based overload of isGhostMilestone.
|
|
123
139
|
* Binds basePath and milestoneId from the scope, ensuring path resolution
|
|
@@ -1786,8 +1802,8 @@ function selfHealRuntimeRecords(basePath: string, ctx: ExtensionContext): { clea
|
|
|
1786
1802
|
cleared++;
|
|
1787
1803
|
continue;
|
|
1788
1804
|
}
|
|
1789
|
-
// Clear records stuck in
|
|
1790
|
-
if (phase
|
|
1805
|
+
// Clear records stuck in an in-flight phase (process died mid-unit).
|
|
1806
|
+
if (isInFlightRuntimePhase(phase)) {
|
|
1791
1807
|
clearUnitRuntimeRecord(basePath, unitType, unitId);
|
|
1792
1808
|
cleared++;
|
|
1793
1809
|
}
|
|
@@ -2042,6 +2058,23 @@ export async function showSmartEntry(
|
|
|
2042
2058
|
}
|
|
2043
2059
|
}
|
|
2044
2060
|
|
|
2061
|
+
if (interrupted.classification !== "recoverable") {
|
|
2062
|
+
try {
|
|
2063
|
+
const { autoImportMarkdownHierarchyIfDbMismatch } = await import("./migration-auto-check.js");
|
|
2064
|
+
const result = await autoImportMarkdownHierarchyIfDbMismatch(basePath);
|
|
2065
|
+
if (result.action === "imported") {
|
|
2066
|
+
ctx.ui.notify(
|
|
2067
|
+
`Recovered migrated planning state into gsd.db (${result.reason}): ${result.afterDb.milestones} milestone(s), ${result.afterDb.slices} slice(s), ${result.afterDb.tasks} task(s).`,
|
|
2068
|
+
"info",
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
} catch (err) {
|
|
2072
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2073
|
+
ctx.ui.notify(`GSD could not auto-import existing planning state into gsd.db: ${message}`, "warning");
|
|
2074
|
+
logWarning("guided", `planning state auto-import failed: ${message}`, { file: "guided-flow.ts" });
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2045
2078
|
// Always derive from the project root — the assessment may have derived
|
|
2046
2079
|
// state from a worktree path that was cleaned up in the stale branch above.
|
|
2047
2080
|
const state = await deriveState(basePath);
|
|
@@ -2150,7 +2183,7 @@ export async function showSmartEntry(
|
|
|
2150
2183
|
});
|
|
2151
2184
|
|
|
2152
2185
|
if (choice === "quick_task") {
|
|
2153
|
-
ctx
|
|
2186
|
+
await runQuickTaskChoice(ctx, pi);
|
|
2154
2187
|
} else if (choice === "new_milestone") {
|
|
2155
2188
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
|
|
2156
2189
|
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
@@ -2209,7 +2242,7 @@ export async function showSmartEntry(
|
|
|
2209
2242
|
});
|
|
2210
2243
|
|
|
2211
2244
|
if (choice === "quick_task") {
|
|
2212
|
-
ctx
|
|
2245
|
+
await runQuickTaskChoice(ctx, pi);
|
|
2213
2246
|
} else if (choice === "new_milestone") {
|
|
2214
2247
|
const milestoneIds = findMilestoneIds(basePath);
|
|
2215
2248
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
@@ -2352,7 +2385,7 @@ export async function showSmartEntry(
|
|
|
2352
2385
|
});
|
|
2353
2386
|
|
|
2354
2387
|
if (choice === "quick_task") {
|
|
2355
|
-
ctx
|
|
2388
|
+
await runQuickTaskChoice(ctx, pi);
|
|
2356
2389
|
} else if (choice === "plan") {
|
|
2357
2390
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
|
|
2358
2391
|
await dispatchWorkflow(
|
|
@@ -25,6 +25,50 @@ import {
|
|
|
25
25
|
|
|
26
26
|
import type { MigrationPreview } from "./writer.js";
|
|
27
27
|
import { homedir } from "node:os";
|
|
28
|
+
import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
|
|
29
|
+
import { clearEngineHierarchy, transaction } from "../gsd-db.js";
|
|
30
|
+
import { migrateFromMarkdown } from "../md-importer.js";
|
|
31
|
+
import { invalidateStateCache } from "../state.js";
|
|
32
|
+
|
|
33
|
+
export type MigrationImportCounts = ReturnType<typeof migrateFromMarkdown>;
|
|
34
|
+
|
|
35
|
+
function assertMigrationImportMatchesPreview(imported: MigrationImportCounts, preview: MigrationPreview): void {
|
|
36
|
+
const mismatches: string[] = [];
|
|
37
|
+
if (imported.hierarchy.milestones !== preview.milestoneCount) {
|
|
38
|
+
mismatches.push(`milestones ${imported.hierarchy.milestones}/${preview.milestoneCount}`);
|
|
39
|
+
}
|
|
40
|
+
if (imported.hierarchy.slices !== preview.totalSlices) {
|
|
41
|
+
mismatches.push(`slices ${imported.hierarchy.slices}/${preview.totalSlices}`);
|
|
42
|
+
}
|
|
43
|
+
if (imported.hierarchy.tasks !== preview.totalTasks) {
|
|
44
|
+
mismatches.push(`tasks ${imported.hierarchy.tasks}/${preview.totalTasks}`);
|
|
45
|
+
}
|
|
46
|
+
if (imported.requirements !== preview.requirements.total) {
|
|
47
|
+
mismatches.push(`requirements ${imported.requirements}/${preview.requirements.total}`);
|
|
48
|
+
}
|
|
49
|
+
if (mismatches.length > 0) {
|
|
50
|
+
throw new Error(`migration DB import verification failed: ${mismatches.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function importWrittenMigrationToDb(
|
|
55
|
+
basePath: string,
|
|
56
|
+
preview?: MigrationPreview,
|
|
57
|
+
): Promise<MigrationImportCounts> {
|
|
58
|
+
const opened = await ensureDbOpen(basePath);
|
|
59
|
+
if (!opened) {
|
|
60
|
+
throw new Error(`failed to open or create the GSD database at ${basePath}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const counts = transaction(() => {
|
|
64
|
+
clearEngineHierarchy();
|
|
65
|
+
const imported = migrateFromMarkdown(basePath);
|
|
66
|
+
if (preview) assertMigrationImportMatchesPreview(imported, preview);
|
|
67
|
+
return imported;
|
|
68
|
+
});
|
|
69
|
+
invalidateStateCache();
|
|
70
|
+
return counts;
|
|
71
|
+
}
|
|
28
72
|
|
|
29
73
|
/** Format preview stats for embedding in the review prompt. */
|
|
30
74
|
function formatPreviewStats(preview: MigrationPreview): string {
|
|
@@ -182,9 +226,10 @@ export async function handleMigrate(
|
|
|
182
226
|
|
|
183
227
|
const result = await writeGSDDirectory(project, process.cwd());
|
|
184
228
|
const gsdPath = gsdRoot(process.cwd());
|
|
229
|
+
const imported = await importWrittenMigrationToDb(process.cwd(), preview);
|
|
185
230
|
|
|
186
231
|
ctx.ui.notify(
|
|
187
|
-
`✓ Migration complete — ${result.paths.length} file(s) written to .gsd
|
|
232
|
+
`✓ Migration complete — ${result.paths.length} file(s) written to .gsd/ and ${imported.hierarchy.milestones}M/${imported.hierarchy.slices}S/${imported.hierarchy.tasks}T imported to the database`,
|
|
188
233
|
"info",
|
|
189
234
|
);
|
|
190
235
|
|
|
@@ -193,6 +238,7 @@ export async function handleMigrate(
|
|
|
193
238
|
title: "Migration written",
|
|
194
239
|
summary: [
|
|
195
240
|
`${result.paths.length} files written to .gsd/`,
|
|
241
|
+
`${imported.hierarchy.milestones} milestone(s), ${imported.hierarchy.slices} slice(s), and ${imported.hierarchy.tasks} task(s) imported to gsd.db`,
|
|
196
242
|
"",
|
|
197
243
|
"The agent can now review the migrated output against GSD-2 standards —",
|
|
198
244
|
"checking structure, content quality, deriveState() round-trip, and",
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
|
|
4
|
+
import {
|
|
5
|
+
clearEngineHierarchy,
|
|
6
|
+
getAllMilestones,
|
|
7
|
+
getMilestoneSlices,
|
|
8
|
+
getSliceTasks,
|
|
9
|
+
isDbAvailable,
|
|
10
|
+
transaction,
|
|
11
|
+
} from "./gsd-db.js";
|
|
12
|
+
import { migrateHierarchyToDb } from "./md-importer.js";
|
|
13
|
+
import { parsePlan, parseRoadmap } from "./parsers-legacy.js";
|
|
14
|
+
import {
|
|
15
|
+
milestonesDir,
|
|
16
|
+
resolveMilestoneFile,
|
|
17
|
+
resolveSliceFile,
|
|
18
|
+
} from "./paths.js";
|
|
19
|
+
import { invalidateStateCache } from "./state.js";
|
|
20
|
+
|
|
21
|
+
export interface HierarchyCounts {
|
|
22
|
+
milestones: number;
|
|
23
|
+
slices: number;
|
|
24
|
+
tasks: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MigrationAutoCheckResult {
|
|
28
|
+
action: "none" | "imported";
|
|
29
|
+
reason: "no-markdown" | "in-sync" | "db-empty" | "count-mismatch";
|
|
30
|
+
markdown: HierarchyCounts;
|
|
31
|
+
beforeDb: HierarchyCounts;
|
|
32
|
+
afterDb: HierarchyCounts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function zeroCounts(): HierarchyCounts {
|
|
36
|
+
return { milestones: 0, slices: 0, tasks: 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sameCounts(a: HierarchyCounts, b: HierarchyCounts): boolean {
|
|
40
|
+
return a.milestones === b.milestones && a.slices === b.slices && a.tasks === b.tasks;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function countMarkdownHierarchy(basePath: string): HierarchyCounts {
|
|
44
|
+
const root = milestonesDir(basePath);
|
|
45
|
+
if (!existsSync(root)) return zeroCounts();
|
|
46
|
+
|
|
47
|
+
const counts = zeroCounts();
|
|
48
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
49
|
+
if (!entry.isDirectory() || !/^M\d+/.test(entry.name)) continue;
|
|
50
|
+
counts.milestones++;
|
|
51
|
+
|
|
52
|
+
const roadmapPath = resolveMilestoneFile(basePath, entry.name, "ROADMAP");
|
|
53
|
+
if (!roadmapPath || !existsSync(roadmapPath)) continue;
|
|
54
|
+
|
|
55
|
+
const roadmap = parseRoadmap(readFileSync(roadmapPath, "utf-8"));
|
|
56
|
+
counts.slices += roadmap.slices.length;
|
|
57
|
+
|
|
58
|
+
for (const slice of roadmap.slices) {
|
|
59
|
+
const planPath = resolveSliceFile(basePath, entry.name, slice.id, "PLAN");
|
|
60
|
+
if (!planPath || !existsSync(planPath)) continue;
|
|
61
|
+
const plan = parsePlan(readFileSync(planPath, "utf-8"));
|
|
62
|
+
counts.tasks += plan.tasks.length;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return counts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function countDbHierarchy(): HierarchyCounts {
|
|
70
|
+
if (!isDbAvailable()) return zeroCounts();
|
|
71
|
+
const counts = zeroCounts();
|
|
72
|
+
const milestones = getAllMilestones();
|
|
73
|
+
counts.milestones = milestones.length;
|
|
74
|
+
|
|
75
|
+
for (const milestone of milestones) {
|
|
76
|
+
const slices = getMilestoneSlices(milestone.id);
|
|
77
|
+
counts.slices += slices.length;
|
|
78
|
+
for (const slice of slices) {
|
|
79
|
+
counts.tasks += getSliceTasks(milestone.id, slice.id).length;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return counts;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function autoImportMarkdownHierarchyIfDbMismatch(
|
|
87
|
+
basePath: string,
|
|
88
|
+
): Promise<MigrationAutoCheckResult> {
|
|
89
|
+
const markdown = countMarkdownHierarchy(basePath);
|
|
90
|
+
if (sameCounts(markdown, zeroCounts())) {
|
|
91
|
+
return {
|
|
92
|
+
action: "none",
|
|
93
|
+
reason: "no-markdown",
|
|
94
|
+
markdown,
|
|
95
|
+
beforeDb: zeroCounts(),
|
|
96
|
+
afterDb: zeroCounts(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const opened = await ensureDbOpen(basePath);
|
|
101
|
+
if (!opened || !isDbAvailable()) {
|
|
102
|
+
throw new Error(`failed to open or create the GSD database at ${basePath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const beforeDb = countDbHierarchy();
|
|
106
|
+
if (sameCounts(markdown, beforeDb)) {
|
|
107
|
+
return { action: "none", reason: "in-sync", markdown, beforeDb, afterDb: beforeDb };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const reason = sameCounts(beforeDb, zeroCounts()) ? "db-empty" : "count-mismatch";
|
|
111
|
+
const imported = transaction(() => {
|
|
112
|
+
clearEngineHierarchy();
|
|
113
|
+
return migrateHierarchyToDb(basePath);
|
|
114
|
+
});
|
|
115
|
+
invalidateStateCache();
|
|
116
|
+
|
|
117
|
+
const afterDb = {
|
|
118
|
+
milestones: imported.milestones,
|
|
119
|
+
slices: imported.slices,
|
|
120
|
+
tasks: imported.tasks,
|
|
121
|
+
};
|
|
122
|
+
if (!sameCounts(markdown, afterDb)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`migration auto-import verification failed: markdown ${markdown.milestones}M/${markdown.slices}S/${markdown.tasks}T, db ${afterDb.milestones}M/${afterDb.slices}S/${afterDb.tasks}T`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { action: "imported", reason, markdown, beforeDb, afterDb };
|
|
129
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import { normalizePlannedFileReference } from "./files.js";
|
|
3
|
+
|
|
4
|
+
export interface PlanningPathScopeField {
|
|
5
|
+
field: string;
|
|
6
|
+
values: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isInsideBase(basePath: string, candidate: string): boolean {
|
|
10
|
+
const base = resolve(basePath);
|
|
11
|
+
const abs = resolve(candidate);
|
|
12
|
+
const rel = relative(base, abs);
|
|
13
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Planning IO fields are execution contracts. Absolute paths are only safe when
|
|
18
|
+
* they stay inside the active working directory; in worktree mode, an absolute
|
|
19
|
+
* path to the original checkout makes executors edit the wrong tree.
|
|
20
|
+
*/
|
|
21
|
+
export function validatePlanningPathScope(
|
|
22
|
+
basePath: string,
|
|
23
|
+
fields: PlanningPathScopeField[],
|
|
24
|
+
): string | null {
|
|
25
|
+
for (const { field, values } of fields) {
|
|
26
|
+
for (const raw of values) {
|
|
27
|
+
const candidate = normalizePlannedFileReference(raw);
|
|
28
|
+
if (!isAbsolute(candidate)) continue;
|
|
29
|
+
if (isInsideBase(basePath, candidate)) continue;
|
|
30
|
+
return `${field} contains absolute path outside working directory: ${candidate}. Use a path relative to ${basePath}.`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -32,7 +32,7 @@ export interface ContextManagementConfig {
|
|
|
32
32
|
/**
|
|
33
33
|
* Opt-in tool-output sandboxing for sub-sessions. When enabled, the gsd_exec
|
|
34
34
|
* MCP tool runs scripts in an isolated subprocess and returns only a short
|
|
35
|
-
* digest to the calling agent's context window;
|
|
35
|
+
* digest to the calling agent's context window; capped stdout/stderr persist
|
|
36
36
|
* in the project memory store and can be retrieved by id later.
|
|
37
37
|
*
|
|
38
38
|
* Inspired by mksglu/context-mode (Elastic License 2.0). This is an
|
|
@@ -16,7 +16,7 @@ Dispatch ALL slices simultaneously using the `subagent` tool in **parallel mode*
|
|
|
16
16
|
|
|
17
17
|
1. Call `subagent` with `tasks: [...]` containing one entry per slice below
|
|
18
18
|
2. Wait for ALL subagents to complete
|
|
19
|
-
3. Verify each slice's RESEARCH file was written (check
|
|
19
|
+
3. Verify each slice's RESEARCH file was written (check `.gsd/milestones/{{mid}}/slices/<slice-id>/`)
|
|
20
20
|
4. If a subagent failed to write its RESEARCH file, retry it **once** individually
|
|
21
21
|
5. If it fails a second time, write a partial RESEARCH file for that slice with a `## BLOCKER` section explaining the failure — do NOT retry again
|
|
22
22
|
6. Report which slices completed research and which (if any) needed a blocker note
|
|
@@ -39,7 +39,7 @@ If slice research is inlined, trust it. Explore enough code to confirm paths, bo
|
|
|
39
39
|
5. Define slice verification before tasks. Non-trivial slices need real tests or executable assertions; boundary contracts need contract-exercising checks. Tests must not read .gitignore/gitignored paths such as `.gsd/`, `.planning/`, or `.audits/`.
|
|
40
40
|
6. Include Threat Surface (Q3), Requirement Impact (Q4), proof level, observability, integration closure, Failure Modes (Q5), Load Profile (Q6), and Negative Tests (Q7) only where applicable.
|
|
41
41
|
7. Right-size tasks. Simple slices can be one task; split only when context, ownership, or verification boundaries justify it.
|
|
42
|
-
8. Each task needs a concrete title, Why / Files / Do / Verify / Done when, plus task-plan description, steps, must-haves, verification, inputs, and expected output. Inputs and Expected Output must include concrete backtick-wrapped paths; each task needs at least one output path.
|
|
42
|
+
8. Each task needs a concrete title, Why / Files / Do / Verify / Done when, plus task-plan description, steps, must-haves, verification, inputs, and expected output. Inputs and Expected Output must include concrete backtick-wrapped paths; each task needs at least one output path. Use paths relative to `{{workingDirectory}}`; do not put absolute paths to the original checkout or any directory outside `{{workingDirectory}}` in `files`, `inputs`, `expectedOutput`, or verification commands.
|
|
43
43
|
9. Persist with `gsd_plan_slice` using goal, successCriteria, optional proofLevel/integrationClosure/observabilityImpact, and tasks. `gsd_plan_slice` handles task persistence transactionally and renders `{{outputPath}}` plus task plans; do not call `gsd_plan_task`. The DB-backed tool is the canonical write path. Do **not** rely on direct `PLAN.md` writes as the source of truth.
|
|
44
44
|
10. Self-audit before finishing: goal/demo closure, requirement coverage, locked decisions, concrete paths, dependency order, wiring, scope size, proof truthfulness, feature completeness, and quality gates. Quality gates: non-trivial slices/tasks include specific Q3-Q7 coverage where applicable.
|
|
45
45
|
11. If planning creates structural decisions, append them to `.gsd/DECISIONS.md`.
|
|
@@ -17,11 +17,7 @@ You are executing a GSD quick task — a lightweight, focused unit of work outsi
|
|
|
17
17
|
5. Verify your work:
|
|
18
18
|
- Run tests if applicable.
|
|
19
19
|
- Verify both happy path and failure modes for non-trivial changes.
|
|
20
|
-
6.
|
|
21
|
-
- Use conventional commit messages (feat:, fix:, refactor:, etc.)
|
|
22
|
-
- Stage only relevant files — never commit secrets or runtime files.
|
|
23
|
-
- Commit logical units separately if the task involves distinct changes.
|
|
24
|
-
- Quick tasks run outside the auto-mode lifecycle — there is no system auto-commit, so commit directly here.
|
|
20
|
+
6. {{commitInstruction}}
|
|
25
21
|
7. Write a brief summary to `{{summaryPath}}`:
|
|
26
22
|
- Quick tasks operate outside the milestone/slice/task DB structure, so `gsd_summary_save` (which requires a `milestone_id`) cannot be used here. Write the file directly.
|
|
27
23
|
|
|
@@ -27,13 +27,13 @@ Roadmap, slice summaries, assessments, requirements, decisions, and project cont
|
|
|
27
27
|
Call `subagent` with `tasks: [...]` containing ALL THREE reviewers simultaneously:
|
|
28
28
|
|
|
29
29
|
**Reviewer A - Requirements Coverage**
|
|
30
|
-
Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Read `.gsd/
|
|
30
|
+
Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Read `.gsd/REQUIREMENTS.md` or use the inlined requirements context. For each requirement, check slice SUMMARY files under `.gsd/milestones/{{milestoneId}}/slices/` and mark COVERED, PARTIAL, or MISSING. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
|
|
31
31
|
|
|
32
32
|
**Reviewer B - Cross-Slice Integration**
|
|
33
33
|
Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Read `{{roadmapPath}}` and find the boundary map (produces/consumes contracts). For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
|
|
34
34
|
|
|
35
35
|
**Reviewer C - Assessment & Acceptance Criteria**
|
|
36
|
-
Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Read `.gsd/{{milestoneId}}/CONTEXT.md` for criteria. Check slice
|
|
36
|
+
Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Read `.gsd/milestones/{{milestoneId}}/{{milestoneId}}-CONTEXT.md` for criteria. Check slice SUMMARY/UAT files under `.gsd/milestones/{{milestoneId}}/slices/`. Verify each criterion maps to passing evidence. Then review inlined milestone verification classes. For each non-empty planned class, output table: Class | Planned Check | Evidence | Verdict. Use the exact class names `Contract`, `Integration`, `Operational`, and `UAT` whenever those classes are present. If no verification classes were planned, say that explicitly. Output sections `Acceptance Criteria` with checklist `[ ] Criterion | Evidence`, and `Verification Classes` with the table. End with one-line verdict: PASS if all criteria and classes are covered, NEEDS-ATTENTION if gaps exist."
|
|
37
37
|
|
|
38
38
|
### Step 2 - Synthesize Findings
|
|
39
39
|
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { join } from "node:path";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
15
15
|
import { loadPrompt } from "./prompt-loader.js";
|
|
16
16
|
import { gsdRoot } from "./paths.js";
|
|
17
17
|
import { GitServiceImpl, runGit } from "./git-service.js";
|
|
@@ -77,6 +77,40 @@ function ensureQuickDir(basePath: string, taskNum: number, slug: string): string
|
|
|
77
77
|
return taskDir;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function isPathInside(parent: string, child: string): boolean {
|
|
81
|
+
const rel = relative(parent, child);
|
|
82
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isExternalGsdRoot(basePath: string, root: string): boolean {
|
|
86
|
+
try {
|
|
87
|
+
return !isPathInside(realpathSync(basePath), realpathSync(root));
|
|
88
|
+
} catch {
|
|
89
|
+
return !isPathInside(basePath, root);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildQuickCommitInstruction(basePath: string, root: string): string {
|
|
94
|
+
const externalState = isExternalGsdRoot(basePath, root);
|
|
95
|
+
if (externalState) {
|
|
96
|
+
return [
|
|
97
|
+
"Commit repo changes atomically, but do not stage or commit `.gsd/quick/...`:",
|
|
98
|
+
" - `.gsd/` resolves outside this git repository, so Git cannot stage quick-task summary files from the project repo.",
|
|
99
|
+
" - Write the quick summary file directly at the requested path; that file is persisted by GSD external state.",
|
|
100
|
+
" - Stage and commit only implementation/test/docs files that live inside the repository.",
|
|
101
|
+
" - If the task only writes quick-task research/summary files and no repository files changed, do not run `git commit`; report that there was nothing in the project repo to commit.",
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
"Commit your changes atomically:",
|
|
107
|
+
" - Use conventional commit messages (feat:, fix:, refactor:, etc.)",
|
|
108
|
+
" - Stage only relevant files — never commit secrets or runtime files.",
|
|
109
|
+
" - Commit logical units separately if the task involves distinct changes.",
|
|
110
|
+
" - Quick tasks run outside the auto-mode lifecycle — there is no system auto-commit, so commit directly here.",
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
80
114
|
function quickReturnStatePath(basePath: string): string {
|
|
81
115
|
return join(gsdRoot(basePath), "runtime", "quick-return.json");
|
|
82
116
|
}
|
|
@@ -246,6 +280,7 @@ export async function handleQuick(
|
|
|
246
280
|
taskDir: taskDirRel,
|
|
247
281
|
branch: actualBranch,
|
|
248
282
|
summaryPath,
|
|
283
|
+
commitInstruction: buildQuickCommitInstruction(basePath, root),
|
|
249
284
|
date,
|
|
250
285
|
taskNum: String(taskNum),
|
|
251
286
|
slug,
|
|
@@ -121,3 +121,14 @@ test('executeResume: reports friendly empty state when no snapshot exists', () =
|
|
|
121
121
|
cleanup(base);
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
test('executeResume: returns disabled error when context_mode.enabled=false', () => {
|
|
126
|
+
const base = freshBase();
|
|
127
|
+
try {
|
|
128
|
+
const result = executeResume({}, { baseDir: base, preferences: { context_mode: { enabled: false } } });
|
|
129
|
+
assert.equal(result.isError, true);
|
|
130
|
+
assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
|
|
131
|
+
} finally {
|
|
132
|
+
cleanup(base);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
@@ -232,4 +232,59 @@ describe('emitCrashRecoveredUnitEnd (#3348)', () => {
|
|
|
232
232
|
rmSync(base, { recursive: true, force: true });
|
|
233
233
|
}
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
test('emitOpenUnitEndForUnit closes the latest open start with error context', async () => {
|
|
237
|
+
const base = makeTmpBase();
|
|
238
|
+
try {
|
|
239
|
+
const { emitJournalEvent, queryJournal } = await import('../journal.ts');
|
|
240
|
+
const { emitOpenUnitEndForUnit } = await import('../crash-recovery.ts');
|
|
241
|
+
|
|
242
|
+
const firstFlowId = randomUUID();
|
|
243
|
+
const secondFlowId = randomUUID();
|
|
244
|
+
emitJournalEvent(base, {
|
|
245
|
+
ts: new Date().toISOString(),
|
|
246
|
+
flowId: firstFlowId,
|
|
247
|
+
seq: 1,
|
|
248
|
+
eventType: 'unit-start',
|
|
249
|
+
data: { unitType: 'execute-task', unitId: 'M008/S04/T02' },
|
|
250
|
+
});
|
|
251
|
+
emitJournalEvent(base, {
|
|
252
|
+
ts: new Date().toISOString(),
|
|
253
|
+
flowId: firstFlowId,
|
|
254
|
+
seq: 2,
|
|
255
|
+
eventType: 'unit-end',
|
|
256
|
+
data: { unitType: 'execute-task', unitId: 'M008/S04/T02', status: 'completed' },
|
|
257
|
+
causedBy: { flowId: firstFlowId, seq: 1 },
|
|
258
|
+
});
|
|
259
|
+
emitJournalEvent(base, {
|
|
260
|
+
ts: new Date().toISOString(),
|
|
261
|
+
flowId: secondFlowId,
|
|
262
|
+
seq: 3,
|
|
263
|
+
eventType: 'unit-start',
|
|
264
|
+
data: { unitType: 'execute-task', unitId: 'M008/S04/T02' },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const emitted = emitOpenUnitEndForUnit(
|
|
268
|
+
base,
|
|
269
|
+
'execute-task',
|
|
270
|
+
'M008/S04/T02',
|
|
271
|
+
'cancelled',
|
|
272
|
+
{ message: 'runUnitPhase exploded', category: 'unit-exception', isTransient: false },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
assert.equal(emitted, true, 'open unit should be closed');
|
|
276
|
+
const ends = queryJournal(base).filter((e) => e.eventType === 'unit-end');
|
|
277
|
+
assert.equal(ends.length, 2, 'should preserve existing end and add one new end');
|
|
278
|
+
const newEnd = ends.find((e) => e.causedBy?.flowId === secondFlowId);
|
|
279
|
+
assert.ok(newEnd, 'new end should close the latest open start');
|
|
280
|
+
assert.equal(newEnd!.data?.status, 'cancelled');
|
|
281
|
+
assert.deepEqual(newEnd!.data?.errorContext, {
|
|
282
|
+
message: 'runUnitPhase exploded',
|
|
283
|
+
category: 'unit-exception',
|
|
284
|
+
isTransient: false,
|
|
285
|
+
});
|
|
286
|
+
} finally {
|
|
287
|
+
rmSync(base, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
235
290
|
});
|