gsd-pi 3.0.0-dev.2e8b124f7 → 3.0.0-dev.6c9a50fd0
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +2 -3
- package/dist/resources/extensions/gsd/auto/orchestrator.js +2 -2
- package/dist/resources/extensions/gsd/auto/phases.js +12 -4
- package/dist/resources/extensions/gsd/auto-dispatch.js +34 -4
- package/dist/resources/extensions/gsd/auto-recovery.js +1 -0
- package/dist/resources/extensions/gsd/auto.js +27 -11
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +35 -4
- package/dist/resources/extensions/gsd/crash-recovery.js +4 -1
- package/dist/resources/extensions/gsd/db/auto-workers.js +21 -0
- package/dist/resources/extensions/gsd/preferences.js +4 -0
- package/dist/resources/extensions/gsd/repo-identity.js +39 -22
- package/dist/resources/extensions/gsd/session-lock.js +15 -2
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +9 -1
- package/dist/resources/extensions/gsd/tools/complete-slice.js +50 -2
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +66 -40
- package/dist/resources/extensions/gsd/worktree-safety.js +10 -3
- package/dist/resources/extensions/shared/next-action-ui.js +13 -5
- 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 +6 -6
- 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 +6 -6
- 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/src/resources/extensions/gsd/auto/contracts.ts +2 -0
- package/src/resources/extensions/gsd/auto/loop.ts +2 -2
- package/src/resources/extensions/gsd/auto/orchestrator.ts +2 -2
- package/src/resources/extensions/gsd/auto/phases.ts +14 -4
- package/src/resources/extensions/gsd/auto-dispatch.ts +52 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +1 -0
- package/src/resources/extensions/gsd/auto.ts +63 -18
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +25 -4
- package/src/resources/extensions/gsd/crash-recovery.ts +3 -0
- package/src/resources/extensions/gsd/db/auto-workers.ts +25 -0
- package/src/resources/extensions/gsd/preferences.ts +4 -0
- package/src/resources/extensions/gsd/repo-identity.ts +45 -25
- package/src/resources/extensions/gsd/session-lock.ts +15 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +135 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +64 -35
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +17 -15
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +51 -1
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +111 -1
- package/src/resources/extensions/gsd/tests/integration/state-machine-live-validation.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +28 -1
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +44 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +10 -0
- package/src/resources/extensions/gsd/tools/complete-slice.ts +51 -2
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +31 -17
- package/src/resources/extensions/gsd/worktree-safety.ts +12 -4
- package/src/resources/extensions/shared/next-action-ui.ts +11 -5
- package/src/resources/extensions/shared/tests/next-action-ui-hasui.test.ts +32 -0
- /package/dist/web/standalone/.next/static/{zCegwxH2e6vLp1vEZLLuZ → 8wipfz6TDZ6YWoaQjgqYD}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{zCegwxH2e6vLp1vEZLLuZ → 8wipfz6TDZ6YWoaQjgqYD}/_ssgManifest.js +0 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
closeDatabase,
|
|
11
11
|
_getAdapter,
|
|
12
12
|
insertGateRow,
|
|
13
|
+
insertAssessment,
|
|
13
14
|
upsertRequirement,
|
|
14
15
|
getAllMilestones,
|
|
15
16
|
} from "../gsd-db.ts";
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
executeTaskComplete,
|
|
27
28
|
executeMilestoneStatus,
|
|
28
29
|
executeSliceComplete,
|
|
30
|
+
executeSliceReopen,
|
|
29
31
|
executeValidateMilestone,
|
|
30
32
|
} from "../tools/workflow-tool-executors.ts";
|
|
31
33
|
|
|
@@ -146,6 +148,117 @@ test("executeTaskComplete coerces string verificationEvidence entries", async ()
|
|
|
146
148
|
}
|
|
147
149
|
});
|
|
148
150
|
|
|
151
|
+
test("executeSliceComplete preserves omitted optional requirement arrays", async () => {
|
|
152
|
+
const base = makeTmpBase();
|
|
153
|
+
try {
|
|
154
|
+
openTestDb(base);
|
|
155
|
+
await inProjectDir(base, () => executePlanMilestone({
|
|
156
|
+
milestoneId: "M001",
|
|
157
|
+
title: "Requirement preservation",
|
|
158
|
+
vision: "Ensure omitted arrays are not coerced to empties.",
|
|
159
|
+
slices: [
|
|
160
|
+
{
|
|
161
|
+
sliceId: "S01",
|
|
162
|
+
title: "Slice",
|
|
163
|
+
risk: "medium",
|
|
164
|
+
depends: [],
|
|
165
|
+
demo: "demo",
|
|
166
|
+
goal: "goal",
|
|
167
|
+
successCriteria: "done",
|
|
168
|
+
proofLevel: "integration",
|
|
169
|
+
integrationClosure: "closed",
|
|
170
|
+
observabilityImpact: "covered",
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
}, base));
|
|
174
|
+
await inProjectDir(base, () => executePlanSlice({
|
|
175
|
+
milestoneId: "M001",
|
|
176
|
+
sliceId: "S01",
|
|
177
|
+
goal: "goal",
|
|
178
|
+
tasks: [
|
|
179
|
+
{
|
|
180
|
+
taskId: "T01",
|
|
181
|
+
title: "Task",
|
|
182
|
+
description: "desc",
|
|
183
|
+
estimate: "5m",
|
|
184
|
+
files: ["src/a.ts"],
|
|
185
|
+
verify: "node --test",
|
|
186
|
+
inputs: ["in"],
|
|
187
|
+
expectedOutput: ["out"],
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
}, base));
|
|
191
|
+
await inProjectDir(base, () => executeTaskComplete({
|
|
192
|
+
milestoneId: "M001",
|
|
193
|
+
sliceId: "S01",
|
|
194
|
+
taskId: "T01",
|
|
195
|
+
oneLiner: "done",
|
|
196
|
+
narrative: "done",
|
|
197
|
+
verification: "ok",
|
|
198
|
+
}, base));
|
|
199
|
+
|
|
200
|
+
const result = await inProjectDir(base, () => executeSliceComplete({
|
|
201
|
+
milestoneId: "M001",
|
|
202
|
+
sliceId: "S01",
|
|
203
|
+
sliceTitle: "Slice",
|
|
204
|
+
oneLiner: "done",
|
|
205
|
+
narrative: "done",
|
|
206
|
+
verification: "ok",
|
|
207
|
+
uatContent: "ok",
|
|
208
|
+
requirementsAdvanced: [{ id: "R010", how: "advanced" }],
|
|
209
|
+
requirementsValidated: [{ id: "R010", proof: "validated" }],
|
|
210
|
+
}, base));
|
|
211
|
+
|
|
212
|
+
assert.equal(result.details.operation, "complete_slice");
|
|
213
|
+
const summaryPath = String(result.details.summaryPath);
|
|
214
|
+
const summary = readFileSync(summaryPath, "utf-8");
|
|
215
|
+
assert.match(summary, /R010 — advanced/);
|
|
216
|
+
assert.match(summary, /R010 — validated/);
|
|
217
|
+
|
|
218
|
+
const reopenResult = await inProjectDir(base, () => executeSliceReopen({
|
|
219
|
+
milestoneId: "M001",
|
|
220
|
+
sliceId: "S01",
|
|
221
|
+
reason: "validate idempotent overwrite behavior",
|
|
222
|
+
}, base));
|
|
223
|
+
assert.equal(reopenResult.details.operation, "reopen_slice");
|
|
224
|
+
await inProjectDir(base, () => executeTaskComplete({
|
|
225
|
+
milestoneId: "M001",
|
|
226
|
+
sliceId: "S01",
|
|
227
|
+
taskId: "T01",
|
|
228
|
+
oneLiner: "done (updated)",
|
|
229
|
+
narrative: "done (updated)",
|
|
230
|
+
verification: "ok",
|
|
231
|
+
}, base));
|
|
232
|
+
|
|
233
|
+
const recallResult = await inProjectDir(base, () => executeSliceComplete({
|
|
234
|
+
milestoneId: "M001",
|
|
235
|
+
sliceId: "S01",
|
|
236
|
+
sliceTitle: "Slice",
|
|
237
|
+
oneLiner: "done (updated)",
|
|
238
|
+
narrative: "done (updated)",
|
|
239
|
+
verification: "ok",
|
|
240
|
+
uatContent: "ok",
|
|
241
|
+
}, base));
|
|
242
|
+
|
|
243
|
+
assert.equal(recallResult.details.operation, "complete_slice");
|
|
244
|
+
const recallSummaryPath = String(recallResult.details.summaryPath);
|
|
245
|
+
const recallSummary = readFileSync(recallSummaryPath, "utf-8");
|
|
246
|
+
assert.match(
|
|
247
|
+
recallSummary,
|
|
248
|
+
/R010 — advanced/,
|
|
249
|
+
"requirementsAdvanced should be preserved from first call",
|
|
250
|
+
);
|
|
251
|
+
assert.match(
|
|
252
|
+
recallSummary,
|
|
253
|
+
/R010 — validated/,
|
|
254
|
+
"requirementsValidated should be preserved from first call",
|
|
255
|
+
);
|
|
256
|
+
} finally {
|
|
257
|
+
closeDatabase();
|
|
258
|
+
cleanup(base);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
149
262
|
test("executeMilestoneStatus returns milestone metadata and slice counts", async () => {
|
|
150
263
|
const base = makeTmpBase();
|
|
151
264
|
try {
|
|
@@ -371,6 +484,13 @@ test("executeCompleteMilestone sanitizes raw params and writes milestone summary
|
|
|
371
484
|
db!.prepare(
|
|
372
485
|
"INSERT OR REPLACE INTO tasks (milestone_id, slice_id, id, title, status) VALUES (?, ?, ?, ?, ?)",
|
|
373
486
|
).run("M003", "S03", "T03", "Task T03", "complete");
|
|
487
|
+
insertAssessment({
|
|
488
|
+
path: join(".gsd", "milestones", "M003", "M003-VALIDATION.md"),
|
|
489
|
+
milestoneId: "M003",
|
|
490
|
+
status: "pass",
|
|
491
|
+
scope: "milestone-validation",
|
|
492
|
+
fullContent: "---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nValidated.",
|
|
493
|
+
});
|
|
374
494
|
|
|
375
495
|
const rawParams = {
|
|
376
496
|
milestoneId: "M003",
|
|
@@ -129,6 +129,50 @@ describe("Worktree Safety module", () => {
|
|
|
129
129
|
assert.equal(result.details?.expectedRoot, unitRoot);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test("accepts project root for source-writing Unit when isolation mode is none", () => {
|
|
133
|
+
const safety = createWorktreeSafetyModule({
|
|
134
|
+
existsSync: () => true,
|
|
135
|
+
lstatSync: () => ({ isFile: () => true }),
|
|
136
|
+
listRegisteredWorktrees: () => [{ path: projectRoot, branch: "main" }],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = safety.validateUnitRoot({
|
|
140
|
+
unitType: "execute-task",
|
|
141
|
+
unitId: "M001/S01/T01",
|
|
142
|
+
writeScope: "source-writing",
|
|
143
|
+
projectRoot,
|
|
144
|
+
unitRoot: projectRoot,
|
|
145
|
+
milestoneId: "M001",
|
|
146
|
+
isolationMode: "none",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
assert.equal(result.ok, true);
|
|
150
|
+
assert.equal(result.kind, "safe");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("rejects non-project root for source-writing Unit when isolation mode is none", () => {
|
|
154
|
+
const safety = createWorktreeSafetyModule({
|
|
155
|
+
existsSync: () => true,
|
|
156
|
+
lstatSync: () => ({ isFile: () => true }),
|
|
157
|
+
listRegisteredWorktrees: () => [{ path: unitRoot, branch: "main" }],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = safety.validateUnitRoot({
|
|
161
|
+
unitType: "execute-task",
|
|
162
|
+
unitId: "M001/S01/T01",
|
|
163
|
+
writeScope: "source-writing",
|
|
164
|
+
projectRoot,
|
|
165
|
+
unitRoot,
|
|
166
|
+
milestoneId: "M001",
|
|
167
|
+
isolationMode: "none",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
assert.equal(result.ok, false);
|
|
171
|
+
assert.equal(result.kind, "invalid-root");
|
|
172
|
+
assert.equal(result.details?.expectedRoot, projectRoot);
|
|
173
|
+
assert.equal(result.details?.unitRoot, unitRoot);
|
|
174
|
+
});
|
|
175
|
+
|
|
132
176
|
test("rejects a standalone repository masquerading as a worktree", () => {
|
|
133
177
|
unlinkSync(join(unitRoot, ".git"));
|
|
134
178
|
mkdirSync(join(unitRoot, ".git"), { recursive: true });
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getMilestone,
|
|
16
16
|
getMilestoneSlices,
|
|
17
17
|
getSliceTasks,
|
|
18
|
+
getLatestAssessmentByScope,
|
|
18
19
|
updateMilestoneStatus,
|
|
19
20
|
} from "../gsd-db.js";
|
|
20
21
|
import { resolveMilestonePath, clearPathCache } from "../paths.js";
|
|
@@ -156,6 +157,15 @@ export async function handleCompleteMilestone(
|
|
|
156
157
|
return;
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
// Defense-in-depth: only a passing milestone validation permits closeout.
|
|
161
|
+
const validation = getLatestAssessmentByScope(params.milestoneId, "milestone-validation");
|
|
162
|
+
if (validation?.status !== "pass") {
|
|
163
|
+
guardError =
|
|
164
|
+
`Refusing to complete ${params.milestoneId}: latest milestone-validation verdict is ` +
|
|
165
|
+
`"${validation?.status ?? "absent"}". Only verdict=pass permits closeout.`;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
159
169
|
// Verify all slices are complete
|
|
160
170
|
const slices = getMilestoneSlices(params.milestoneId);
|
|
161
171
|
if (slices.length === 0) {
|
|
@@ -232,6 +232,34 @@ ${params.uatContent}
|
|
|
232
232
|
`;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
function parseRequirementSection(
|
|
236
|
+
summaryMd: string,
|
|
237
|
+
heading: "Requirements Advanced" | "Requirements Validated" | "Requirements Invalidated or Re-scoped",
|
|
238
|
+
field: "how" | "proof" | "what",
|
|
239
|
+
): Array<{ id: string; how?: string; proof?: string; what?: string }> {
|
|
240
|
+
const headingLine = `## ${heading}\n\n`;
|
|
241
|
+
const start = summaryMd.indexOf(headingLine);
|
|
242
|
+
if (start === -1) return [];
|
|
243
|
+
const contentStart = start + headingLine.length;
|
|
244
|
+
const nextHeading = summaryMd.indexOf("\n\n## ", contentStart);
|
|
245
|
+
const content = nextHeading === -1
|
|
246
|
+
? summaryMd.slice(contentStart)
|
|
247
|
+
: summaryMd.slice(contentStart, nextHeading);
|
|
248
|
+
return content
|
|
249
|
+
.split("\n")
|
|
250
|
+
.map((line) => line.trim())
|
|
251
|
+
.filter((line) => line.startsWith("- "))
|
|
252
|
+
.map((line) => line.slice(2).trim())
|
|
253
|
+
.map((line) => {
|
|
254
|
+
const pair = line.match(/^(.+?)\s*(?:—|-)\s+(.+)$/);
|
|
255
|
+
const id = pair ? pair[1].trim() : line.trim();
|
|
256
|
+
const detail = pair ? pair[2].trim() : "";
|
|
257
|
+
if (!id || !detail) return null;
|
|
258
|
+
return { id, [field]: detail };
|
|
259
|
+
})
|
|
260
|
+
.filter((entry): entry is { id: string; how?: string; proof?: string; what?: string } => entry !== null);
|
|
261
|
+
}
|
|
262
|
+
|
|
235
263
|
/**
|
|
236
264
|
* Handle the complete_slice operation end-to-end.
|
|
237
265
|
*
|
|
@@ -277,6 +305,7 @@ export async function handleCompleteSlice(
|
|
|
277
305
|
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
278
306
|
const completedAt = new Date().toISOString();
|
|
279
307
|
let guardError: string | null = null;
|
|
308
|
+
let existingSummaryMd = "";
|
|
280
309
|
|
|
281
310
|
transaction(() => {
|
|
282
311
|
// State machine preconditions (inside txn for atomicity).
|
|
@@ -289,6 +318,7 @@ export async function handleCompleteSlice(
|
|
|
289
318
|
}
|
|
290
319
|
|
|
291
320
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
321
|
+
existingSummaryMd = slice?.full_summary_md?.trim() ?? "";
|
|
292
322
|
if (slice && isClosedStatus(slice.status)) {
|
|
293
323
|
if (isStaleWrite("complete-slice")) {
|
|
294
324
|
guardError = "__stale_duplicate__";
|
|
@@ -347,8 +377,27 @@ export async function handleCompleteSlice(
|
|
|
347
377
|
return { error: guardError };
|
|
348
378
|
}
|
|
349
379
|
|
|
380
|
+
const effectiveParams: CompleteSliceParams = { ...params };
|
|
381
|
+
if (existingSummaryMd) {
|
|
382
|
+
// Keep these heading names in lock-step with renderSliceSummaryMarkdown's
|
|
383
|
+
// section titles so omitted CompleteSliceParams requirement fields can be
|
|
384
|
+
// backfilled from previously rendered summary markdown.
|
|
385
|
+
if (effectiveParams.requirementsAdvanced === undefined) {
|
|
386
|
+
const parsed = parseRequirementSection(existingSummaryMd, "Requirements Advanced", "how");
|
|
387
|
+
if (parsed.length > 0) effectiveParams.requirementsAdvanced = parsed as Array<{ id: string; how: string }>;
|
|
388
|
+
}
|
|
389
|
+
if (effectiveParams.requirementsValidated === undefined) {
|
|
390
|
+
const parsed = parseRequirementSection(existingSummaryMd, "Requirements Validated", "proof");
|
|
391
|
+
if (parsed.length > 0) effectiveParams.requirementsValidated = parsed as Array<{ id: string; proof: string }>;
|
|
392
|
+
}
|
|
393
|
+
if (effectiveParams.requirementsInvalidated === undefined) {
|
|
394
|
+
const parsed = parseRequirementSection(existingSummaryMd, "Requirements Invalidated or Re-scoped", "what");
|
|
395
|
+
if (parsed.length > 0) effectiveParams.requirementsInvalidated = parsed as Array<{ id: string; what: string }>;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
350
399
|
// Render summary markdown
|
|
351
|
-
const summaryMd = renderSliceSummaryMarkdown(
|
|
400
|
+
const summaryMd = renderSliceSummaryMarkdown(effectiveParams);
|
|
352
401
|
|
|
353
402
|
// Resolve and write summary to disk
|
|
354
403
|
let summaryPath: string;
|
|
@@ -363,7 +412,7 @@ export async function handleCompleteSlice(
|
|
|
363
412
|
summaryPath = join(manualSliceDir, `${params.sliceId}-SUMMARY.md`);
|
|
364
413
|
}
|
|
365
414
|
|
|
366
|
-
const uatMd = renderUatMarkdown(
|
|
415
|
+
const uatMd = renderUatMarkdown(effectiveParams);
|
|
367
416
|
const uatPath = summaryPath.replace(/-SUMMARY\.md$/, "-UAT.md");
|
|
368
417
|
setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd);
|
|
369
418
|
let projectionStale = false;
|
|
@@ -523,39 +523,53 @@ export async function executeSliceComplete(
|
|
|
523
523
|
const m = s.match(/^(.+?)\s*(?:—|-)\s+(.+)$/);
|
|
524
524
|
return m ? [m[1].trim(), m[2].trim()] : [s.trim(), ""];
|
|
525
525
|
};
|
|
526
|
-
const
|
|
527
|
-
v == null ?
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
coerced.
|
|
533
|
-
|
|
534
|
-
coerced.
|
|
535
|
-
|
|
536
|
-
coerced.
|
|
537
|
-
|
|
538
|
-
coerced.
|
|
526
|
+
const wrapOptionalArray = (v: unknown): unknown[] | undefined =>
|
|
527
|
+
v == null ? undefined : Array.isArray(v) ? v : [v];
|
|
528
|
+
const coerced = Object.fromEntries(
|
|
529
|
+
Object.entries(params).filter(([, value]) => value !== undefined && value !== null),
|
|
530
|
+
) as CompleteSliceParams & Record<string, unknown>;
|
|
531
|
+
const provides = wrapOptionalArray(params.provides);
|
|
532
|
+
if (provides !== undefined) coerced.provides = provides as string[];
|
|
533
|
+
const keyFiles = wrapOptionalArray(params.keyFiles);
|
|
534
|
+
if (keyFiles !== undefined) coerced.keyFiles = keyFiles as string[];
|
|
535
|
+
const keyDecisions = wrapOptionalArray(params.keyDecisions);
|
|
536
|
+
if (keyDecisions !== undefined) coerced.keyDecisions = keyDecisions as string[];
|
|
537
|
+
const patternsEstablished = wrapOptionalArray(params.patternsEstablished);
|
|
538
|
+
if (patternsEstablished !== undefined) coerced.patternsEstablished = patternsEstablished as string[];
|
|
539
|
+
const observabilitySurfaces = wrapOptionalArray(params.observabilitySurfaces);
|
|
540
|
+
if (observabilitySurfaces !== undefined) coerced.observabilitySurfaces = observabilitySurfaces as string[];
|
|
541
|
+
const requirementsSurfaced = wrapOptionalArray(params.requirementsSurfaced);
|
|
542
|
+
if (requirementsSurfaced !== undefined) coerced.requirementsSurfaced = requirementsSurfaced as string[];
|
|
543
|
+
const drillDownPaths = wrapOptionalArray(params.drillDownPaths);
|
|
544
|
+
if (drillDownPaths !== undefined) coerced.drillDownPaths = drillDownPaths as string[];
|
|
545
|
+
const affects = wrapOptionalArray(params.affects);
|
|
546
|
+
if (affects !== undefined) coerced.affects = affects as string[];
|
|
547
|
+
const filesModified = wrapOptionalArray(params.filesModified);
|
|
548
|
+
if (filesModified !== undefined) coerced.filesModified = filesModified.map((f) => {
|
|
539
549
|
if (typeof f !== "string") return f;
|
|
540
550
|
const [path, description] = splitPair(f);
|
|
541
551
|
return { path, description };
|
|
542
552
|
}) as Array<{ path: string; description: string }>;
|
|
543
|
-
|
|
553
|
+
const requires = wrapOptionalArray(params.requires);
|
|
554
|
+
if (requires !== undefined) coerced.requires = requires.map((r) => {
|
|
544
555
|
if (typeof r !== "string") return r;
|
|
545
556
|
const [slice, provides] = splitPair(r);
|
|
546
557
|
return { slice, provides };
|
|
547
558
|
}) as Array<{ slice: string; provides: string }>;
|
|
548
|
-
|
|
559
|
+
const requirementsAdvanced = wrapOptionalArray(params.requirementsAdvanced);
|
|
560
|
+
if (requirementsAdvanced !== undefined) coerced.requirementsAdvanced = requirementsAdvanced.map((r) => {
|
|
549
561
|
if (typeof r !== "string") return r;
|
|
550
562
|
const [id, how] = splitPair(r);
|
|
551
563
|
return { id, how };
|
|
552
564
|
}) as Array<{ id: string; how: string }>;
|
|
553
|
-
|
|
565
|
+
const requirementsValidated = wrapOptionalArray(params.requirementsValidated);
|
|
566
|
+
if (requirementsValidated !== undefined) coerced.requirementsValidated = requirementsValidated.map((r) => {
|
|
554
567
|
if (typeof r !== "string") return r;
|
|
555
568
|
const [id, proof] = splitPair(r);
|
|
556
569
|
return { id, proof };
|
|
557
570
|
}) as Array<{ id: string; proof: string }>;
|
|
558
|
-
|
|
571
|
+
const requirementsInvalidated = wrapOptionalArray(params.requirementsInvalidated);
|
|
572
|
+
if (requirementsInvalidated !== undefined) coerced.requirementsInvalidated = requirementsInvalidated.map((r) => {
|
|
559
573
|
if (typeof r !== "string") return r;
|
|
560
574
|
const [id, what] = splitPair(r);
|
|
561
575
|
return { id, what };
|
|
@@ -53,6 +53,7 @@ export interface WorktreeSafetyInput {
|
|
|
53
53
|
projectRoot: string;
|
|
54
54
|
unitRoot: string;
|
|
55
55
|
milestoneId?: string | null;
|
|
56
|
+
isolationMode?: "none" | "branch" | "worktree";
|
|
56
57
|
expectedBranch?: string | null;
|
|
57
58
|
emptyWorktreeWithProjectContent?: boolean;
|
|
58
59
|
lease?: {
|
|
@@ -156,12 +157,19 @@ export function createWorktreeSafetyModule(
|
|
|
156
157
|
|
|
157
158
|
const projectRoot = resolve(input.projectRoot);
|
|
158
159
|
const unitRoot = resolve(input.unitRoot);
|
|
159
|
-
const
|
|
160
|
+
const isolationMode = input.isolationMode ?? "worktree";
|
|
161
|
+
const expectedRoot = isolationMode === "worktree"
|
|
162
|
+
? join(projectRoot, ".gsd", "worktrees", milestoneId)
|
|
163
|
+
: projectRoot;
|
|
160
164
|
if (!samePath(unitRoot, expectedRoot)) {
|
|
161
165
|
return failure(
|
|
162
166
|
"invalid-root",
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
isolationMode === "worktree"
|
|
168
|
+
? `Unit root ${unitRoot} is not the expected worktree root for ${milestoneId}.`
|
|
169
|
+
: `Unit root ${unitRoot} is not the project root while isolation mode is ${isolationMode}.`,
|
|
170
|
+
isolationMode === "worktree"
|
|
171
|
+
? "Prepare the Unit in its canonical milestone worktree before allowing source writes."
|
|
172
|
+
: "Run the Unit from the project root when worktree isolation is disabled.",
|
|
165
173
|
{ expectedRoot, unitRoot },
|
|
166
174
|
);
|
|
167
175
|
}
|
|
@@ -197,7 +205,7 @@ export function createWorktreeSafetyModule(
|
|
|
197
205
|
);
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
if (!gitMarkerStat.isFile()) {
|
|
208
|
+
if (isolationMode === "worktree" && !gitMarkerStat.isFile()) {
|
|
201
209
|
return failure(
|
|
202
210
|
"worktree-git-marker-not-file",
|
|
203
211
|
`Worktree root ${unitRoot} has a .git directory, not a registered worktree .git file.`,
|
|
@@ -118,11 +118,9 @@ export async function showNextAction(
|
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
// Headless guard:
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
// reaching a deterministic "not_yet". Lockup #5125 root protection.
|
|
125
|
-
if (!ctx.hasUI) {
|
|
121
|
+
// Headless/non-interactive guard: avoid emitting interactive select requests
|
|
122
|
+
// in contexts where no human can answer (no UI, RPC/headless shims).
|
|
123
|
+
if (!isInteractiveUIContext(ctx)) {
|
|
126
124
|
return "not_yet";
|
|
127
125
|
}
|
|
128
126
|
|
|
@@ -218,3 +216,11 @@ export async function showNextAction(
|
|
|
218
216
|
|
|
219
217
|
return result;
|
|
220
218
|
}
|
|
219
|
+
|
|
220
|
+
function isInteractiveUIContext(ctx: ExtensionCommandContext): boolean {
|
|
221
|
+
if (!ctx.hasUI) return false;
|
|
222
|
+
if (process.env.GSD_HEADLESS === "1") return false;
|
|
223
|
+
const uiMode = (ctx.ui as { mode?: string } | undefined)?.mode;
|
|
224
|
+
if (uiMode === "rpc" || uiMode === "headless") return false;
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
@@ -81,6 +81,38 @@ describe("showNextAction ctx.hasUI guard (#5125 lockup root protection)", () =>
|
|
|
81
81
|
assert.equal(result, "alpha", "fallback should map the picked label back to the chosen action id");
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it("returns 'not_yet' immediately when UI mode is rpc even if ctx.hasUI is true", async () => {
|
|
85
|
+
let customCalled = 0;
|
|
86
|
+
let selectCalled = 0;
|
|
87
|
+
|
|
88
|
+
const ctx = {
|
|
89
|
+
hasUI: true,
|
|
90
|
+
ui: {
|
|
91
|
+
mode: "rpc",
|
|
92
|
+
custom: async () => {
|
|
93
|
+
customCalled++;
|
|
94
|
+
return undefined as never;
|
|
95
|
+
},
|
|
96
|
+
select: async () => {
|
|
97
|
+
selectCalled++;
|
|
98
|
+
return undefined;
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const result = await showNextAction(ctx as any, {
|
|
104
|
+
title: "GSD — test",
|
|
105
|
+
actions: [
|
|
106
|
+
{ id: "alpha", label: "Alpha", description: "first", recommended: true },
|
|
107
|
+
{ id: "beta", label: "Beta", description: "second" },
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.equal(result, "not_yet", "rpc-backed UI is non-interactive for next-action");
|
|
112
|
+
assert.equal(customCalled, 0, "ctx.ui.custom must not be called in rpc mode");
|
|
113
|
+
assert.equal(selectCalled, 0, "ctx.ui.select must not be called in rpc mode");
|
|
114
|
+
});
|
|
115
|
+
|
|
84
116
|
it("returns the resolved id when ctx.ui.custom completes normally", async () => {
|
|
85
117
|
let selectCalled = 0;
|
|
86
118
|
|
|
File without changes
|
|
File without changes
|