@opengsd/gsd-pi 1.9.0-dev.5c1a3ff2 → 1.9.0-dev.97ef11e3
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/dist/headless.js +2 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +24 -4
- package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
- package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +12 -4
- package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
- package/dist/resources/extensions/gsd/auto-start.js +29 -9
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
- package/dist/resources/extensions/gsd/auto.js +15 -5
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -2
- package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
- package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
- package/dist/resources/extensions/gsd/db/engine.js +5 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
- package/dist/resources/extensions/gsd/error-classifier.js +12 -2
- package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
- package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
- package/dist/resources/extensions/gsd/gsd-db.js +75 -31
- package/dist/resources/extensions/gsd/guided-flow.js +2 -2
- package/dist/resources/extensions/gsd/markdown-renderer.js +3 -9
- package/dist/resources/extensions/gsd/paths.js +17 -7
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
- package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
- package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
- package/dist/resources/extensions/gsd/stop-notice.js +9 -0
- package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
- package/dist/resources/extensions/shared/interview-ui.js +43 -6
- 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/chunks/1991.js +2 -2
- package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- 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 +2 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +9 -3
- package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
- package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
- package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cli.js +121 -15
- package/packages/gsd-cloud/dist/cli.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
- package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-config.js +7 -1
- package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
- package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.js +272 -0
- package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
- package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
- package/packages/gsd-cloud/package.json +2 -2
- package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
- package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
- package/packages/mcp-server/dist/cli-runner.js +119 -33
- package/packages/mcp-server/dist/cli-runner.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +17 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +17 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +48 -4
- package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
- package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
- package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
- package/src/resources/extensions/gsd/auto-start.ts +31 -8
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
- package/src/resources/extensions/gsd/auto.ts +20 -7
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
- package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
- package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
- package/src/resources/extensions/gsd/db/engine.ts +5 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
- package/src/resources/extensions/gsd/error-classifier.ts +13 -2
- package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
- package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
- package/src/resources/extensions/gsd/gsd-db.ts +85 -33
- package/src/resources/extensions/gsd/guided-flow.ts +2 -2
- package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
- package/src/resources/extensions/gsd/paths.ts +17 -7
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
- package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
- package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
- package/src/resources/extensions/gsd/stop-notice.ts +11 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
- package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
- package/src/resources/extensions/shared/interview-ui.ts +47 -8
- package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
- /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → DXD9fZPnqtjZSlT5xdC3v}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → DXD9fZPnqtjZSlT5xdC3v}/_ssgManifest.js +0 -0
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// (drift to import). gsd-core is oblivious to this file and ignores it.
|
|
7
7
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { dirname, join, isAbsolute, normalize } from "node:path";
|
|
9
|
+
import { existsSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { dirname, join, isAbsolute, normalize, relative, resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
/** Current marker schema version. Bump on breaking format changes + migrate. */
|
|
13
13
|
export const COMPAT_MARKER_SCHEMA = 2;
|
|
@@ -54,6 +54,13 @@ export interface CompatMarker {
|
|
|
54
54
|
piVersion: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export interface ReadCompatMarkerOptions {
|
|
58
|
+
/** Rewrite invalid-but-derivable marker keys back to safe root-relative keys. */
|
|
59
|
+
healInvalidKeys?: boolean;
|
|
60
|
+
/** Quarantine malformed/unsafe markers. Disable only for read-only previews. */
|
|
61
|
+
quarantineInvalid?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
/** Marker returned when no marker exists yet (fresh project, first gsd-pi run). */
|
|
58
65
|
export const EMPTY_MARKER: CompatMarker = {
|
|
59
66
|
schema: COMPAT_MARKER_SCHEMA,
|
|
@@ -81,6 +88,37 @@ export function computeProjectionSha(content: string): string {
|
|
|
81
88
|
return createHash("sha256").update(normalizeForHash(content)).digest("hex").slice(0, 16);
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
function normalizeRealPath(p: string): string {
|
|
92
|
+
try {
|
|
93
|
+
return realpathSync.native(p);
|
|
94
|
+
} catch {
|
|
95
|
+
return resolve(p);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function rootRelativeKey(rootPath: string, absPath: string): string | null {
|
|
100
|
+
const root = normalizeRealPath(rootPath);
|
|
101
|
+
const abs = normalizeRealPath(absPath);
|
|
102
|
+
const rel = relative(root, abs);
|
|
103
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sepForRelative(rel)}`) || isAbsolute(rel)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return rel.replace(/\\/g, "/");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sepForRelative(rel: string): string {
|
|
110
|
+
return rel.includes("\\") ? "\\" : "/";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function deriveCompatProjectionKey(absPath: string, roots: readonly string[]): string {
|
|
114
|
+
for (const root of roots) {
|
|
115
|
+
const key = rootRelativeKey(root, absPath);
|
|
116
|
+
if (key) return key;
|
|
117
|
+
}
|
|
118
|
+
const fallbackRoot = roots[roots.length - 1] ?? dirname(absPath);
|
|
119
|
+
return relative(fallbackRoot, absPath).replace(/\\/g, "/");
|
|
120
|
+
}
|
|
121
|
+
|
|
84
122
|
/**
|
|
85
123
|
* Read & validate the marker. A missing marker → EMPTY_MARKER (treat every
|
|
86
124
|
* projection as external on next reconcile). A malformed marker is quarantined
|
|
@@ -88,7 +126,12 @@ export function computeProjectionSha(content: string): string {
|
|
|
88
126
|
* EMPTY_MARKER. A schema-mismatch returns EMPTY_MARKER (forward-compat: refuse
|
|
89
127
|
* to act on a future format we don't understand).
|
|
90
128
|
*/
|
|
91
|
-
export function readCompatMarker(
|
|
129
|
+
export function readCompatMarker(
|
|
130
|
+
basePath: string,
|
|
131
|
+
options: ReadCompatMarkerOptions = {},
|
|
132
|
+
): CompatMarker {
|
|
133
|
+
const healInvalidKeys = options.healInvalidKeys ?? true;
|
|
134
|
+
const quarantineInvalid = options.quarantineInvalid ?? true;
|
|
92
135
|
const path = compatMarkerPath(basePath);
|
|
93
136
|
if (!existsSync(path)) return emptyMarker();
|
|
94
137
|
|
|
@@ -103,12 +146,16 @@ export function readCompatMarker(basePath: string): CompatMarker {
|
|
|
103
146
|
try {
|
|
104
147
|
parsed = JSON.parse(raw);
|
|
105
148
|
} catch {
|
|
106
|
-
quarantine(basePath, raw);
|
|
149
|
+
if (quarantineInvalid) quarantine(basePath, raw);
|
|
107
150
|
return emptyMarker();
|
|
108
151
|
}
|
|
109
152
|
|
|
153
|
+
if (healInvalidKeys && healMarkerProjectionKeys(basePath, parsed) && isValidMarker(parsed)) {
|
|
154
|
+
writeCompatMarker(basePath, parsed);
|
|
155
|
+
}
|
|
156
|
+
|
|
110
157
|
if (!isValidMarker(parsed)) {
|
|
111
|
-
quarantine(basePath, raw);
|
|
158
|
+
if (quarantineInvalid) quarantine(basePath, raw);
|
|
112
159
|
return emptyMarker();
|
|
113
160
|
}
|
|
114
161
|
// Promote older markers by defaulting absent fields. Schema 1 → 2 only adds
|
|
@@ -245,6 +292,53 @@ function isValidProjectionMap(x: unknown): boolean {
|
|
|
245
292
|
return true;
|
|
246
293
|
}
|
|
247
294
|
|
|
295
|
+
function healMarkerProjectionKeys(basePath: string, marker: unknown): marker is CompatMarker {
|
|
296
|
+
if (typeof marker !== "object" || marker === null) return false;
|
|
297
|
+
const m = marker as Record<string, unknown>;
|
|
298
|
+
let changed = false;
|
|
299
|
+
|
|
300
|
+
if (typeof m.projections === "object" && m.projections !== null) {
|
|
301
|
+
changed = healProjectionMapKeys(basePath, ".gsd", m.projections as Record<string, ProjectionEntry>) || changed;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const planning = m.planning;
|
|
305
|
+
if (typeof planning === "object" && planning !== null) {
|
|
306
|
+
const p = planning as Record<string, unknown>;
|
|
307
|
+
if (typeof p.projections === "object" && p.projections !== null) {
|
|
308
|
+
changed = healProjectionMapKeys(basePath, ".planning", p.projections as Record<string, ProjectionEntry>) || changed;
|
|
309
|
+
}
|
|
310
|
+
if (typeof p.passthrough === "object" && p.passthrough !== null) {
|
|
311
|
+
changed = healProjectionMapKeys(basePath, ".planning", p.passthrough as Record<string, ProjectionEntry>) || changed;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return changed;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function healProjectionMapKeys(
|
|
319
|
+
basePath: string,
|
|
320
|
+
rootName: ".gsd" | ".planning",
|
|
321
|
+
map: Record<string, ProjectionEntry>,
|
|
322
|
+
): boolean {
|
|
323
|
+
let changed = false;
|
|
324
|
+
const root = join(basePath, rootName);
|
|
325
|
+
|
|
326
|
+
for (const key of Object.keys(map)) {
|
|
327
|
+
if (isSafeProjectionKey(key)) continue;
|
|
328
|
+
if (key.includes("\0") || isAbsolute(key) || /^[A-Za-z]:/.test(key)) continue;
|
|
329
|
+
const healedKey = rootRelativeKey(root, resolve(root, key));
|
|
330
|
+
if (!healedKey || !isSafeProjectionKey(healedKey)) continue;
|
|
331
|
+
|
|
332
|
+
if (!map[healedKey]) {
|
|
333
|
+
map[healedKey] = map[key]!;
|
|
334
|
+
}
|
|
335
|
+
delete map[key];
|
|
336
|
+
changed = true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return changed;
|
|
340
|
+
}
|
|
341
|
+
|
|
248
342
|
function isValidPlanningMarker(x: unknown): x is PlanningMarker {
|
|
249
343
|
if (typeof x !== "object" || x === null) return false;
|
|
250
344
|
const p = x as Record<string, unknown>;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// writer layer.
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
14
|
import { GSDError, GSD_STALE_STATE } from "../errors.js";
|
|
15
15
|
import type { GsdWorkspace, MilestoneScope } from "../workspace.js";
|
|
16
16
|
import { logError, logWarning } from "../workflow-logger.js";
|
|
@@ -71,7 +71,10 @@ let _gsdRequire: ReturnType<typeof createRequire> | null | undefined;
|
|
|
71
71
|
function getGsdRequire(): ReturnType<typeof createRequire> | null {
|
|
72
72
|
if (_gsdRequire !== undefined) return _gsdRequire;
|
|
73
73
|
try {
|
|
74
|
-
|
|
74
|
+
// Next.js may emit this module into a CommonJS chunk. Avoid ESM-only module
|
|
75
|
+
// metadata syntax here; it is a hard parse error there.
|
|
76
|
+
const packageRoot = process.env.GSD_WEB_PACKAGE_ROOT || process.env.GSD_PKG_ROOT || process.cwd();
|
|
77
|
+
_gsdRequire = createRequire(resolve(packageRoot, "package.json"));
|
|
75
78
|
} catch {
|
|
76
79
|
_gsdRequire = null;
|
|
77
80
|
}
|
|
@@ -95,6 +95,25 @@ function reportCheckboxDbStatusDivergence(
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function bareDuplicateMilestoneId(milestoneId: string): string | null {
|
|
99
|
+
const match = milestoneId.match(/^(M\d{3})-[a-z0-9]{6}$/i);
|
|
100
|
+
return match?.[1]?.toUpperCase() ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function checkboxDbStatusMilestoneIds(basePath: string, milestoneIds: string[]): string[] {
|
|
104
|
+
if (!hasFlatPhaseLayout(basePath)) return milestoneIds;
|
|
105
|
+
|
|
106
|
+
// Flat-phase directories are keyed by phase number. A restored
|
|
107
|
+
// M003-xxxxxx row alongside M003 is a stale duplicate for the same
|
|
108
|
+
// phases/03-* projection, so checking both can compare DB status against two
|
|
109
|
+
// competing PLAN files and report a persistent false divergence.
|
|
110
|
+
const allIds = new Set(milestoneIds.map((id) => id.toUpperCase()));
|
|
111
|
+
return milestoneIds.filter((milestoneId) => {
|
|
112
|
+
const bareId = bareDuplicateMilestoneId(milestoneId);
|
|
113
|
+
return !bareId || !allIds.has(bareId);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
98
117
|
function checkProjectionCheckboxDbStatus(basePath: string, milestoneIds: string[], issues: DoctorIssue[]): void {
|
|
99
118
|
for (const milestoneId of milestoneIds) {
|
|
100
119
|
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
@@ -434,7 +453,9 @@ function staleMilestonesArtifactRowFixable(basePath: string, row: ArtifactRow, a
|
|
|
434
453
|
|
|
435
454
|
function stalePhasesArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
|
|
436
455
|
const issuePath = artifactPathRelativeToGsd(row.path);
|
|
437
|
-
return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) &&
|
|
456
|
+
return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) &&
|
|
457
|
+
(hasPresentFlatPhaseReplacement(basePath, row, artifactRows) ||
|
|
458
|
+
hasPresentMilestonesReplacement(basePath, row, artifactRows));
|
|
438
459
|
}
|
|
439
460
|
|
|
440
461
|
function staleArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
|
|
@@ -807,7 +828,11 @@ export async function checkEngineHealth(
|
|
|
807
828
|
// block doctor.
|
|
808
829
|
try {
|
|
809
830
|
if (isDbAvailable()) {
|
|
810
|
-
checkProjectionCheckboxDbStatus(
|
|
831
|
+
checkProjectionCheckboxDbStatus(
|
|
832
|
+
basePath,
|
|
833
|
+
checkboxDbStatusMilestoneIds(basePath, getAllMilestones().map((milestone) => milestone.id)),
|
|
834
|
+
issues,
|
|
835
|
+
);
|
|
811
836
|
}
|
|
812
837
|
} catch {
|
|
813
838
|
// Non-fatal: checkbox-vs-DB divergence check must never block doctor
|
|
@@ -65,8 +65,12 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|web
|
|
|
65
65
|
// transient errors so auto-mode can retry with reduced budget or fall back to a larger-context model.
|
|
66
66
|
// See: https://github.com/open-gsd/gsd-pi/issues/4528
|
|
67
67
|
const SERVER_RE = /internal(?: server)?[ _-]?error|server[ _-]?error|500|502|503|overloaded|server_error|api_error|service.?unavailable|context (?:window|length) exceed|context window exceed/i;
|
|
68
|
+
// Provider adapters throw this when an upstream stop reason reaches Pi before
|
|
69
|
+
// the adapter knows how to map it. Keep the match scoped so plain "unknown"
|
|
70
|
+
// runtime errors still pause for inspection instead of being retried forever.
|
|
71
|
+
const PROVIDER_STOP_REASON_RE = /unhandled stop reason:\s*(?:abort|unknown)\b/i;
|
|
68
72
|
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
|
69
|
-
const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
73
|
+
const CONNECTION_RE = /terminated|\babort\b|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
70
74
|
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
|
|
71
75
|
// This eliminates the need to enumerate every error message variant individually.
|
|
72
76
|
const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
|
|
@@ -103,7 +107,7 @@ const UNSUPPORTED_MODEL_SCOPE_RE = /\b(?:account|plan|tier|subscription)\b/i;
|
|
|
103
107
|
* 3. Network (ECONNRESET, ETIMEDOUT, socket hang up, fetch failed, dns)
|
|
104
108
|
* 4. Stream truncation (malformed JSON from mid-stream cut)
|
|
105
109
|
* 5. Server (500/502/503, overloaded, server_error)
|
|
106
|
-
* 6. Connection (terminated, ECONNREFUSED, EPIPE, other side closed)
|
|
110
|
+
* 6. Connection (terminated, abort/unknown stop reason, ECONNREFUSED, EPIPE, other side closed)
|
|
107
111
|
* 7. Model error (400 invalid argument / payload rejected for this model)
|
|
108
112
|
* 8. Unknown
|
|
109
113
|
*/
|
|
@@ -163,6 +167,13 @@ export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorCla
|
|
|
163
167
|
return { kind: "rate-limit", retryAfterMs: delayMs };
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
// Provider stop-reason adapter gaps are usually request/stream failures from
|
|
171
|
+
// the current model. Treat the scoped adapter message as fallback-eligible,
|
|
172
|
+
// but keep generic unknown errors on the indefinite pause path.
|
|
173
|
+
if (PROVIDER_STOP_REASON_RE.test(errorMsg)) {
|
|
174
|
+
return { kind: "connection", retryAfterMs: retryAfterMs ?? 15_000 };
|
|
175
|
+
}
|
|
176
|
+
|
|
166
177
|
// 3. Network errors — same-model retry candidate
|
|
167
178
|
if (NETWORK_RE.test(errorMsg)) {
|
|
168
179
|
// Exclude if also matches permanent signals (already handled above for
|
|
@@ -155,6 +155,19 @@ function isInsideFlatProjectionBackup(backupDir: string, src: string): boolean {
|
|
|
155
155
|
return src === phaseBackupDir || src.startsWith(`${phaseBackupDir}/`) || src.startsWith(`${phaseBackupDir}\\`);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
function removeFlatProjectionBackup(backupDir: string): void {
|
|
159
|
+
const phaseBackupDir = join(backupDir, "__phases");
|
|
160
|
+
if (!existsSync(phaseBackupDir)) return;
|
|
161
|
+
try {
|
|
162
|
+
removePathWithRetries(phaseBackupDir);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logWarning(
|
|
165
|
+
"migration",
|
|
166
|
+
`flat-phase migration succeeded but could not remove temporary ${LAYOUT_SEGMENTS.level1}/ backup: ${(err as Error).message}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
158
171
|
function restoreFlatProjectionFromBackup(basePath: string, backupDir: string): void {
|
|
159
172
|
const phaseBackupDir = join(backupDir, "__phases");
|
|
160
173
|
if (!existsSync(phaseBackupDir)) return;
|
|
@@ -494,4 +507,5 @@ export async function migrateToFlatPhase(basePath: string): Promise<void> {
|
|
|
494
507
|
`flat-phase migration succeeded but could not remove ${LEGACY_MIGRATING_SEGMENT}: ${(err as Error).message}`,
|
|
495
508
|
);
|
|
496
509
|
}
|
|
510
|
+
removeFlatProjectionBackup(backupDir);
|
|
497
511
|
}
|
|
@@ -67,6 +67,13 @@ export type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
|
|
|
67
67
|
|
|
68
68
|
import { TERMINAL_STATUS_SQL } from "./db/sql-constants.js";
|
|
69
69
|
import { applyStatusTransition } from "./db/writers/status.js";
|
|
70
|
+
import {
|
|
71
|
+
LAYOUT_SEGMENTS,
|
|
72
|
+
derivePhaseSlug,
|
|
73
|
+
milestoneIdToPhaseNum,
|
|
74
|
+
milestoneIdUniqueSuffix,
|
|
75
|
+
phaseDirName,
|
|
76
|
+
} from "./layout-policy.js";
|
|
70
77
|
|
|
71
78
|
export function insertDecision(d: Omit<Decision, "seq">): void {
|
|
72
79
|
if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
@@ -206,6 +213,47 @@ export function insertArtifact(a: {
|
|
|
206
213
|
});
|
|
207
214
|
}
|
|
208
215
|
|
|
216
|
+
function canonicalPhaseDirNameForDb(milestoneId: string, title: string): string {
|
|
217
|
+
const phaseNum = milestoneIdToPhaseNum(milestoneId);
|
|
218
|
+
const slug = derivePhaseSlug(title || milestoneId);
|
|
219
|
+
const suffix = milestoneIdUniqueSuffix(milestoneId);
|
|
220
|
+
return phaseDirName(phaseNum, suffix ? `${suffix}-${slug}` : slug);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function reconcileMilestonePhaseArtifactPaths(milestoneId: string, title: string): void {
|
|
224
|
+
const db = getDbOrNull()!;
|
|
225
|
+
const phaseNumPrefix = String(milestoneIdToPhaseNum(milestoneId)).padStart(2, "0");
|
|
226
|
+
const canonicalDir = canonicalPhaseDirNameForDb(milestoneId, title);
|
|
227
|
+
const staleRows = db.prepare(
|
|
228
|
+
`SELECT path
|
|
229
|
+
FROM artifacts
|
|
230
|
+
WHERE milestone_id = :milestone_id
|
|
231
|
+
AND path LIKE :phase_prefix`,
|
|
232
|
+
).all({
|
|
233
|
+
":milestone_id": milestoneId,
|
|
234
|
+
":phase_prefix": `${LAYOUT_SEGMENTS.level1}/${phaseNumPrefix}-%/%`,
|
|
235
|
+
}) as Array<{ path: string }>;
|
|
236
|
+
|
|
237
|
+
const updatePath = db.prepare("UPDATE artifacts SET path = :new_path WHERE path = :old_path");
|
|
238
|
+
const deletePath = db.prepare("DELETE FROM artifacts WHERE path = :path");
|
|
239
|
+
const existingPath = db.prepare("SELECT 1 AS present FROM artifacts WHERE path = :path");
|
|
240
|
+
|
|
241
|
+
for (const row of staleRows) {
|
|
242
|
+
const parts = row.path.split("/");
|
|
243
|
+
if (parts.length < 3) continue;
|
|
244
|
+
if (parts[0] !== LAYOUT_SEGMENTS.level1) continue;
|
|
245
|
+
if (parts[1] === canonicalDir) continue;
|
|
246
|
+
if (!parts[1]?.startsWith(`${phaseNumPrefix}-`)) continue;
|
|
247
|
+
|
|
248
|
+
const newPath = [LAYOUT_SEGMENTS.level1, canonicalDir, ...parts.slice(2)].join("/");
|
|
249
|
+
if (existingPath.get({ ":path": newPath })) {
|
|
250
|
+
deletePath.run({ ":path": row.path });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
updatePath.run({ ":new_path": newPath, ":old_path": row.path });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
209
257
|
export interface MilestonePlanningRecord {
|
|
210
258
|
vision: string;
|
|
211
259
|
successCriteria: string[];
|
|
@@ -287,39 +335,43 @@ export function insertMilestone(m: {
|
|
|
287
335
|
|
|
288
336
|
export function upsertMilestonePlanning(milestoneId: string, planning: Partial<MilestonePlanningRecord> & { title?: string; status?: string; depends_on?: string[] }): void {
|
|
289
337
|
if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
338
|
+
transaction(() => {
|
|
339
|
+
getDbOrNull()!.prepare(
|
|
340
|
+
`UPDATE milestones SET
|
|
341
|
+
title = COALESCE(NULLIF(:title, ''), title),
|
|
342
|
+
status = COALESCE(NULLIF(:status, ''), status),
|
|
343
|
+
depends_on = COALESCE(:depends_on, depends_on),
|
|
344
|
+
vision = COALESCE(:vision, vision),
|
|
345
|
+
success_criteria = COALESCE(:success_criteria, success_criteria),
|
|
346
|
+
key_risks = COALESCE(:key_risks, key_risks),
|
|
347
|
+
proof_strategy = COALESCE(:proof_strategy, proof_strategy),
|
|
348
|
+
verification_contract = COALESCE(:verification_contract, verification_contract),
|
|
349
|
+
verification_integration = COALESCE(:verification_integration, verification_integration),
|
|
350
|
+
verification_operational = COALESCE(:verification_operational, verification_operational),
|
|
351
|
+
verification_uat = COALESCE(:verification_uat, verification_uat),
|
|
352
|
+
definition_of_done = COALESCE(:definition_of_done, definition_of_done),
|
|
353
|
+
requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
|
|
354
|
+
boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
|
|
355
|
+
WHERE id = :id`,
|
|
356
|
+
).run({
|
|
357
|
+
":id": milestoneId,
|
|
358
|
+
":title": planning.title ?? "",
|
|
359
|
+
":status": planning.status ?? "",
|
|
360
|
+
":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
|
|
361
|
+
":vision": planning.vision ?? null,
|
|
362
|
+
":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
|
|
363
|
+
":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
|
|
364
|
+
":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
|
|
365
|
+
":verification_contract": planning.verificationContract ?? null,
|
|
366
|
+
":verification_integration": planning.verificationIntegration ?? null,
|
|
367
|
+
":verification_operational": planning.verificationOperational ?? null,
|
|
368
|
+
":verification_uat": planning.verificationUat ?? null,
|
|
369
|
+
":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
|
|
370
|
+
":requirement_coverage": planning.requirementCoverage ?? null,
|
|
371
|
+
":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
|
|
372
|
+
});
|
|
373
|
+
const finalTitle = planning.title?.trim();
|
|
374
|
+
if (finalTitle) reconcileMilestonePhaseArtifactPaths(milestoneId, finalTitle);
|
|
323
375
|
});
|
|
324
376
|
}
|
|
325
377
|
|
|
@@ -1282,7 +1282,7 @@ export async function buildDiscussSlicePrompt(
|
|
|
1282
1282
|
: "";
|
|
1283
1283
|
|
|
1284
1284
|
const inlinedTemplates = inlineTemplate("slice-context", "Slice Context");
|
|
1285
|
-
return loadPrompt("guided-discuss-slice", {
|
|
1285
|
+
return prependLanguageDirective(base, loadPrompt("guided-discuss-slice", {
|
|
1286
1286
|
milestoneId: mid,
|
|
1287
1287
|
sliceId: sid,
|
|
1288
1288
|
sliceTitle: sTitle,
|
|
@@ -1293,7 +1293,7 @@ export async function buildDiscussSlicePrompt(
|
|
|
1293
1293
|
inlinedTemplates,
|
|
1294
1294
|
structuredQuestionsAvailable: options?.structuredQuestionsAvailable ?? "false",
|
|
1295
1295
|
commitInstruction: buildDocsCommitInstruction(`docs(${mid}/${sid}): slice context from discuss`),
|
|
1296
|
-
});
|
|
1296
|
+
}));
|
|
1297
1297
|
}
|
|
1298
1298
|
|
|
1299
1299
|
/**
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from "node:fs";
|
|
13
13
|
import { logWarning } from "./workflow-logger.js";
|
|
14
14
|
import { isClosedStatus } from "./status-guards.js";
|
|
15
|
-
import { dirname, join
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
16
|
import {
|
|
17
17
|
getAllMilestones,
|
|
18
18
|
getMilestone,
|
|
@@ -50,7 +50,7 @@ import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.j
|
|
|
50
50
|
import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
|
|
51
51
|
import { invalidateStateCache } from "./state.js";
|
|
52
52
|
import { clearPathCache, milestonesDir, legacyMilestonesDir, isLegacyMilestonesLayout, resolveMilestonePath, relSliceFile, canonicalPhaseDirName } from "./paths.js";
|
|
53
|
-
import { readCompatMarker, writeCompatMarker, computeProjectionSha } from "./compat/compat-marker.js";
|
|
53
|
+
import { readCompatMarker, writeCompatMarker, computeProjectionSha, deriveCompatProjectionKey } from "./compat/compat-marker.js";
|
|
54
54
|
import type { RiskLevel } from "./types.js";
|
|
55
55
|
import {
|
|
56
56
|
phaseDirName,
|
|
@@ -109,13 +109,7 @@ function flushProjectionWritesToMarker(): void {
|
|
|
109
109
|
*/
|
|
110
110
|
function toArtifactPath(absPath: string, basePath: string): string {
|
|
111
111
|
const projectionRoot = gsdProjectionRoot(basePath);
|
|
112
|
-
|
|
113
|
-
const root = projectionRel && !projectionRel.startsWith("..") && !projectionRel.startsWith("/")
|
|
114
|
-
? projectionRoot
|
|
115
|
-
: gsdRoot(basePath);
|
|
116
|
-
const rel = relative(root, absPath);
|
|
117
|
-
// Normalize to forward slashes for consistent DB keys
|
|
118
|
-
return rel.replace(/\\/g, "/");
|
|
112
|
+
return deriveCompatProjectionKey(absPath, [projectionRoot, gsdRoot(basePath)]);
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
/**
|
|
@@ -136,6 +136,14 @@ function cachedReaddir(dirPath: string): string[] {
|
|
|
136
136
|
return entries;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
function isExistingFile(path: string): boolean {
|
|
140
|
+
try {
|
|
141
|
+
return statSync(path).isFile();
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
139
147
|
/**
|
|
140
148
|
* Clear the volatile directory listing caches.
|
|
141
149
|
* Call after milestone transitions, file creation in planning directories,
|
|
@@ -284,7 +292,7 @@ export function resolveFile(dir: string, idPrefix: string, suffix: string): stri
|
|
|
284
292
|
if (!existsSync(dir)) return null;
|
|
285
293
|
const target = `${idPrefix}-${suffix}.md`.toUpperCase();
|
|
286
294
|
try {
|
|
287
|
-
const entries =
|
|
295
|
+
const entries = cachedReaddirWithTypes(dir).filter(e => e.isFile()).map(e => e.name);
|
|
288
296
|
// Direct match: ID-SUFFIX.md
|
|
289
297
|
const direct = entries.find(e => e.toUpperCase() === target);
|
|
290
298
|
if (direct) return direct;
|
|
@@ -311,7 +319,9 @@ export function resolveFile(dir: string, idPrefix: string, suffix: string): stri
|
|
|
311
319
|
export function resolveTaskFiles(tasksDir: string, suffix: string): string[] {
|
|
312
320
|
if (!existsSync(tasksDir)) return [];
|
|
313
321
|
try {
|
|
314
|
-
return
|
|
322
|
+
return cachedReaddirWithTypes(tasksDir)
|
|
323
|
+
.filter(e => e.isFile())
|
|
324
|
+
.map(e => e.name)
|
|
315
325
|
.filter(f => taskIdFromTaskFileName(f, suffix) !== null)
|
|
316
326
|
.sort();
|
|
317
327
|
} catch {
|
|
@@ -860,7 +870,7 @@ export function resolveMilestoneFile(
|
|
|
860
870
|
const prefix = `${String(phaseNum).padStart(2, "0")}`;
|
|
861
871
|
const flatName = `${prefix}-${suffix}.md`;
|
|
862
872
|
const flatPath = join(mDir, flatName);
|
|
863
|
-
if (
|
|
873
|
+
if (isExistingFile(flatPath)) return flatPath;
|
|
864
874
|
// Legacy fallback: M001-SUFFIX.md
|
|
865
875
|
const file = resolveFile(mDir, milestoneId, suffix);
|
|
866
876
|
return file ? join(mDir, file) : null;
|
|
@@ -895,11 +905,11 @@ export function resolveSliceFile(
|
|
|
895
905
|
const planNum = sliceIdToPlanNum(sliceId);
|
|
896
906
|
const flatName = planFileName(phaseNum, planNum, suffix);
|
|
897
907
|
const flatPath = join(phaseDir, flatName);
|
|
898
|
-
if (
|
|
908
|
+
if (isExistingFile(flatPath)) return flatPath;
|
|
899
909
|
// Also check plan-number-only format MM-SUFFIX.md (written by buildSliceFileName)
|
|
900
910
|
const planOnlyName = `${String(planNum).padStart(2, "0")}-${suffix}.md`;
|
|
901
911
|
const planOnlyPath = join(phaseDir, planOnlyName);
|
|
902
|
-
if (
|
|
912
|
+
if (isExistingFile(planOnlyPath)) return planOnlyPath;
|
|
903
913
|
// Try prefix match for the phase+plan number (handles suffix variations)
|
|
904
914
|
const planPrefix = `${String(phaseNum).padStart(2, "0")}-${String(planNum).padStart(2, "0")}-`;
|
|
905
915
|
try {
|
|
@@ -949,9 +959,9 @@ export function resolveTaskFile(
|
|
|
949
959
|
|
|
950
960
|
if (suffix !== "PLAN" && !isLegacy) {
|
|
951
961
|
const flatPath = join(phaseDir, buildFlatTaskFileName(sliceId, taskId, suffix));
|
|
952
|
-
if (
|
|
962
|
+
if (isExistingFile(flatPath)) return flatPath;
|
|
953
963
|
const legacyFlatPath = join(phaseDir, buildTaskFileName(taskId, suffix));
|
|
954
|
-
return
|
|
964
|
+
return isExistingFile(legacyFlatPath) ? legacyFlatPath : null;
|
|
955
965
|
}
|
|
956
966
|
|
|
957
967
|
const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
|
|
@@ -24,18 +24,18 @@ Roadmap, slice summaries, assessments, requirements, decisions, and project cont
|
|
|
24
24
|
|
|
25
25
|
### Step 1 - Dispatch Parallel Reviewers
|
|
26
26
|
|
|
27
|
-
The Inlined Context above already preloads the evidence reviewers need — the roadmap, per-slice SUMMARY/ASSESSMENT excerpts, requirements, and verification classes. **Embed the relevant preloaded evidence directly into each reviewer's task prompt** so reviewers work from it instead of re-reading the same artifacts from disk. Each reviewer should read a full file only when its excerpt is missing, truncated, or internally inconsistent — never as a routine first step. This avoids three reviewers independently re-surveying artifacts the orchestrator already holds.
|
|
27
|
+
The Inlined Context above already preloads the evidence reviewers need — the roadmap, per-slice SUMMARY/ASSESSMENT excerpts, requirements, and verification classes. **Embed the relevant preloaded evidence directly into each reviewer's task prompt** so reviewers work from it instead of re-reading the same artifacts from disk. Each reviewer should read a full file only when its excerpt is missing, truncated, or internally inconsistent — never as a routine first step. When a reviewer needs a full artifact, use only explicit file paths from the On-demand Validation Artifacts / On-demand Milestone Context blocks or discover files with `find .gsd -type f`; never pass a phase, slice, `tasks/`, or `slices/` directory to `read` or `readFileSync`. This avoids three reviewers independently re-surveying artifacts the orchestrator already holds.
|
|
28
28
|
|
|
29
29
|
Call `subagent` with `tasks: [...]` containing ALL THREE reviewers simultaneously:
|
|
30
30
|
|
|
31
31
|
**Reviewer A - Requirements Coverage**
|
|
32
|
-
Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Use the preloaded requirements and slice SUMMARY evidence embedded in this task — do not re-read them from disk. For each requirement, mark COVERED, PARTIAL, or MISSING against that evidence. Read a full SUMMARY
|
|
32
|
+
Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Use the preloaded requirements and slice SUMMARY evidence embedded in this task — do not re-read them from disk. For each requirement, mark COVERED, PARTIAL, or MISSING against that evidence. Read a full SUMMARY only from an explicit file path in the On-demand Validation Artifacts list, or discover candidates with `find .gsd -type f -name '*-SUMMARY.md'`; never read a directory path. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
|
|
33
33
|
|
|
34
34
|
**Reviewer B - Cross-Slice Integration**
|
|
35
|
-
Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Use the preloaded roadmap and slice SUMMARY evidence embedded in this task — do not re-read them from disk. Find the boundary map (produces/consumes contracts) in the roadmap. For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Read `{{roadmapPath}}` or a full SUMMARY only if the preloaded evidence is missing, truncated, or inconsistent. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
|
|
35
|
+
Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Use the preloaded roadmap and slice SUMMARY evidence embedded in this task — do not re-read them from disk. Find the boundary map (produces/consumes contracts) in the roadmap. For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Read `{{roadmapPath}}` or a full SUMMARY from the On-demand Validation Artifacts list only if the preloaded evidence is missing, truncated, or inconsistent; never read a directory path. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
|
|
36
36
|
|
|
37
37
|
**Reviewer C - Assessment & Acceptance Criteria**
|
|
38
|
-
Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Use the preloaded milestone context, slice SUMMARY, and ASSESSMENT evidence embedded in this task — do not re-read them from disk. Read
|
|
38
|
+
Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Use the preloaded milestone context, slice SUMMARY, and ASSESSMENT evidence embedded in this task — do not re-read them from disk. Read the milestone context from the On-demand Milestone Context file path, and read full SUMMARY or ASSESSMENT files only from explicit paths in the On-demand Validation Artifacts list, if the preloaded excerpt is missing, truncated, or inconsistent. UAT files are specs, not evidence. Verify each criterion maps to passing evidence. Then review the inlined `Verification Classes (from planning)` table. For every planned row in that table, output a `Verification Classes` table with columns `Class | Planned Check | Evidence | Verdict`. Preserve every planned non-empty class row; do not summarize, rename, combine, or omit planned classes. The first cell of each row must be exactly `Contract`, `Integration`, `Operational`, or `UAT` when that class is present in planning. If a planned class lacks evidence, still include its canonical row and mark the verdict NEEDS-ATTENTION or FAIL. If a planned browser/UAT class has no ASSESSMENT with browser/runtime actions and assertions, return NEEDS-ATTENTION. 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 by evidence, NEEDS-ATTENTION if gaps exist."
|
|
39
39
|
|
|
40
40
|
### Step 2 - Synthesize Findings
|
|
41
41
|
|
|
@@ -85,6 +85,6 @@ If verdict is `needs-remediation`:
|
|
|
85
85
|
|
|
86
86
|
**You MUST call `gsd_validate_milestone` before finishing. Do not manually write `{{validationPath}}`.**
|
|
87
87
|
|
|
88
|
-
**File system safety:** When scanning milestone directories, use `
|
|
88
|
+
**File system safety:** When scanning milestone directories, use `find .gsd -type f` first and read only files. In flat-phase projects, `.gsd/phases/<NN>-<slug>/` is a directory, not an artifact. Never pass a directory path such as a phase directory, `tasks/`, or `slices/` directly to `read`, `readFile`, or `readFileSync`.
|
|
89
89
|
|
|
90
90
|
When done, say: "Milestone {{milestoneId}} validation complete — verdict: <verdict>." Say this exactly once — if you already said it in a prior message, do not repeat it.
|