@opengsd/gsd-pi 1.1.1-dev.9bb7453 → 1.1.1-dev.a5a2de8
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-dispatch.js +11 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
- package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
- package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +19 -8
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
- package/dist/resources/extensions/gsd/guided-flow.js +89 -107
- package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
- package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +3 -17
- package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
- package/dist/resources/extensions/gsd/tool-contract.js +5 -0
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +17 -7
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +81 -4
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +169 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -75
- 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/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 +1 -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/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -3
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
- package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
- package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
- package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +3 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +6 -6
- package/packages/pi-ai/dist/models.generated.js +6 -6
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
- package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
- package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +23 -8
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
- package/src/resources/extensions/gsd/guided-flow.ts +124 -134
- package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
- package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
- package/src/resources/extensions/gsd/prompts/run-uat.md +3 -17
- package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
- package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
- package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
- package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +23 -5
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
- package/src/resources/extensions/gsd/tool-contract.ts +6 -0
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +38 -8
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +100 -5
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +186 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -75
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
- /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → 9y3LeeR2uGr2yRj9RjY3D}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → 9y3LeeR2uGr2yRj9RjY3D}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Shared DB-backed guard for milestone closeout finalization.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getDbPath,
|
|
6
|
+
getLatestAssessmentByScope,
|
|
7
|
+
getMilestone,
|
|
8
|
+
getMilestoneSlices,
|
|
9
|
+
getPendingGates,
|
|
10
|
+
getSliceTasks,
|
|
11
|
+
isDbAvailable,
|
|
12
|
+
refreshOpenDatabaseFromDisk,
|
|
13
|
+
} from "./gsd-db.js";
|
|
14
|
+
import { isClosedStatus } from "./status-guards.js";
|
|
15
|
+
|
|
16
|
+
export const CLOSEOUT_CONSISTENCY_BLOCKED_REASON = "closeout-consistency-blocked";
|
|
17
|
+
|
|
18
|
+
export type CloseoutConsistencyFailureReason =
|
|
19
|
+
| "db-unavailable"
|
|
20
|
+
| "db-refresh-failed"
|
|
21
|
+
| "milestone-missing"
|
|
22
|
+
| "milestone-open"
|
|
23
|
+
| "validation-not-pass"
|
|
24
|
+
| "slice-missing"
|
|
25
|
+
| "slice-open"
|
|
26
|
+
| "task-open"
|
|
27
|
+
| "quality-gate-pending";
|
|
28
|
+
|
|
29
|
+
export type CloseoutConsistencyResult =
|
|
30
|
+
| { ok: true }
|
|
31
|
+
| {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: CloseoutConsistencyFailureReason;
|
|
34
|
+
recoveryReason: typeof CLOSEOUT_CONSISTENCY_BLOCKED_REASON;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface CloseoutConsistencyOptions {
|
|
39
|
+
refreshFromDisk?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function blocked(reason: CloseoutConsistencyFailureReason, message: string): CloseoutConsistencyResult {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
reason,
|
|
46
|
+
recoveryReason: CLOSEOUT_CONSISTENCY_BLOCKED_REASON,
|
|
47
|
+
message,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isFileBackedDbPath(path: string | null): boolean {
|
|
52
|
+
return Boolean(path && path !== ":memory:");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function checkCloseoutConsistencyGate(
|
|
56
|
+
milestoneId: string,
|
|
57
|
+
options: CloseoutConsistencyOptions = {},
|
|
58
|
+
): CloseoutConsistencyResult {
|
|
59
|
+
if (!isDbAvailable()) {
|
|
60
|
+
return blocked(
|
|
61
|
+
"db-unavailable",
|
|
62
|
+
`Closeout consistency blocked for ${milestoneId}: canonical DB is unavailable.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options.refreshFromDisk && isFileBackedDbPath(getDbPath()) && !refreshOpenDatabaseFromDisk()) {
|
|
67
|
+
return blocked(
|
|
68
|
+
"db-refresh-failed",
|
|
69
|
+
`Closeout consistency blocked for ${milestoneId}: canonical DB refresh failed.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const milestone = getMilestone(milestoneId);
|
|
74
|
+
if (!milestone) {
|
|
75
|
+
return blocked(
|
|
76
|
+
"milestone-missing",
|
|
77
|
+
`Closeout consistency blocked for ${milestoneId}: milestone is missing from canonical DB.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (!isClosedStatus(milestone.status)) {
|
|
81
|
+
return blocked(
|
|
82
|
+
"milestone-open",
|
|
83
|
+
`Closeout consistency blocked for ${milestoneId}: canonical DB milestone status is "${milestone.status}".`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (milestone.status !== "skipped") {
|
|
88
|
+
const validation = getLatestAssessmentByScope(milestoneId, "milestone-validation");
|
|
89
|
+
if (validation?.status !== "pass") {
|
|
90
|
+
return blocked(
|
|
91
|
+
"validation-not-pass",
|
|
92
|
+
`Closeout consistency blocked for ${milestoneId}: latest milestone validation is "${validation?.status ?? "absent"}".`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const slices = getMilestoneSlices(milestoneId);
|
|
98
|
+
if (slices.length === 0 && milestone.status !== "skipped") {
|
|
99
|
+
return blocked(
|
|
100
|
+
"slice-missing",
|
|
101
|
+
`Closeout consistency blocked for ${milestoneId}: no slices exist in canonical DB.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const slice of slices) {
|
|
106
|
+
if (!isClosedStatus(slice.status)) {
|
|
107
|
+
return blocked(
|
|
108
|
+
"slice-open",
|
|
109
|
+
`Closeout consistency blocked for ${milestoneId}: slice ${slice.id} status is "${slice.status}".`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const task of getSliceTasks(milestoneId, slice.id)) {
|
|
114
|
+
if (!isClosedStatus(task.status)) {
|
|
115
|
+
return blocked(
|
|
116
|
+
"task-open",
|
|
117
|
+
`Closeout consistency blocked for ${milestoneId}: task ${slice.id}/${task.id} status is "${task.status}".`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const pendingGate = getPendingGates(milestoneId, slice.id)[0];
|
|
123
|
+
if (pendingGate) {
|
|
124
|
+
return blocked(
|
|
125
|
+
"quality-gate-pending",
|
|
126
|
+
`Closeout consistency blocked for ${milestoneId}: quality gate ${pendingGate.gate_id} is still pending for ${slice.id}.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function formatCloseoutConsistencyBlock(result: CloseoutConsistencyResult): string {
|
|
135
|
+
if (result.ok) return "";
|
|
136
|
+
return `${result.message} Recovery reason: ${result.recoveryReason}. Resolve the canonical DB state and run /gsd auto to retry.`;
|
|
137
|
+
}
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
getDiscussionMilestoneId,
|
|
99
99
|
hasPendingAutoStart,
|
|
100
100
|
setPendingAutoStart,
|
|
101
|
+
type PendingAutoStartEntry,
|
|
101
102
|
} from "./pending-auto-start.js";
|
|
102
103
|
import { clearGuidedUnitContext, setGuidedUnitContext } from "./guided-unit-context.js";
|
|
103
104
|
|
|
@@ -320,6 +321,116 @@ export function _roadmapHasParseableSlicesForTest(
|
|
|
320
321
|
return parseRoadmapSlices(roadmapContent).length > 0;
|
|
321
322
|
}
|
|
322
323
|
|
|
324
|
+
function hasExecutablePlanForHandoff(milestoneId: string, roadmapFile: string | null): boolean {
|
|
325
|
+
if (isDbAvailable()) {
|
|
326
|
+
return getMilestoneSlices(milestoneId).length > 0;
|
|
327
|
+
}
|
|
328
|
+
if (!roadmapFile) return false;
|
|
329
|
+
try {
|
|
330
|
+
return parseRoadmapSlices(readFileSync(roadmapFile, "utf-8")).length > 0;
|
|
331
|
+
} catch (e) {
|
|
332
|
+
logWarning(
|
|
333
|
+
"guided",
|
|
334
|
+
`failed to parse roadmap slices for ${milestoneId}: ${(e as Error).message}`,
|
|
335
|
+
);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function formatAcceptedDiscussHandoffMessage(
|
|
341
|
+
milestoneId: string,
|
|
342
|
+
contextFile: string | null,
|
|
343
|
+
hasExecutablePlan: boolean,
|
|
344
|
+
): string {
|
|
345
|
+
if (hasExecutablePlan) return `Milestone ${milestoneId} ready.`;
|
|
346
|
+
if (contextFile) return `Milestone ${milestoneId} context captured. Continuing the planning pipeline.`;
|
|
347
|
+
return `Milestone ${milestoneId} planning artifacts captured. Continuing the planning pipeline.`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function manifestContainsMilestone(basePath: string, milestoneId: string): boolean {
|
|
351
|
+
try {
|
|
352
|
+
const manifest = readManifest(basePath);
|
|
353
|
+
return (
|
|
354
|
+
Array.isArray(manifest?.milestones) &&
|
|
355
|
+
manifest.milestones.some(m => m.id === milestoneId)
|
|
356
|
+
);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
logWarning("guided", `R3b: failed to read state manifest: ${(e as Error).message}`);
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function notifyDbRowRecoveryFailed(entry: PendingAutoStartEntry): void {
|
|
364
|
+
entry.ctx.ui.notify(
|
|
365
|
+
`Milestone ${entry.milestoneId}: DB row recovery failed ${entry.r3bRecoveryCount} times. ` +
|
|
366
|
+
`Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`,
|
|
367
|
+
"error",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function noteDbRowRecoveryMiss(entry: PendingAutoStartEntry): void {
|
|
372
|
+
entry.r3bRecoveryCount += 1;
|
|
373
|
+
if (entry.r3bRecoveryCount >= MAX_DB_ROW_RECOVERIES) {
|
|
374
|
+
notifyDbRowRecoveryFailed(entry);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function ensureMilestoneRowForAcceptedHandoff(
|
|
379
|
+
entry: PendingAutoStartEntry,
|
|
380
|
+
contextFile: string | null,
|
|
381
|
+
): boolean {
|
|
382
|
+
if (!isDbAvailable()) return true;
|
|
383
|
+
|
|
384
|
+
const { basePath, milestoneId } = entry;
|
|
385
|
+
const milestoneRow = getMilestone(milestoneId);
|
|
386
|
+
if (milestoneRow) return true;
|
|
387
|
+
|
|
388
|
+
if (manifestContainsMilestone(basePath, milestoneId)) {
|
|
389
|
+
logWarning(
|
|
390
|
+
"guided",
|
|
391
|
+
`R3b: getMilestone(${milestoneId}) returned null but manifest has the row — treating as stale read`,
|
|
392
|
+
);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!contextFile) {
|
|
397
|
+
entry.ctx.ui.notify(
|
|
398
|
+
`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
|
|
399
|
+
`PROJECT.md may have failed to register milestones. ` +
|
|
400
|
+
`Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
|
|
401
|
+
`then re-run /gsd to recover.`,
|
|
402
|
+
"error",
|
|
403
|
+
);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (entry.r3bRecoveryCount >= MAX_DB_ROW_RECOVERIES) {
|
|
408
|
+
logWarning(
|
|
409
|
+
"guided",
|
|
410
|
+
`R3b: milestone ${milestoneId} DB-row recovery limit reached ` +
|
|
411
|
+
`(${entry.r3bRecoveryCount}/${MAX_DB_ROW_RECOVERIES}); user already notified`,
|
|
412
|
+
);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
logWarning(
|
|
417
|
+
"guided",
|
|
418
|
+
`R3b: ${milestoneId} has CONTEXT.md but no DB row — inserting placeholder "queued" row ` +
|
|
419
|
+
`(attempt ${entry.r3bRecoveryCount + 1}/${MAX_DB_ROW_RECOVERIES})`,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
insertMilestone({ id: milestoneId, title: milestoneId, status: "queued" });
|
|
424
|
+
} catch (e) {
|
|
425
|
+
logWarning("guided", `R3b: insertMilestone failed: ${(e as Error).message}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (getMilestone(milestoneId)) return true;
|
|
429
|
+
|
|
430
|
+
noteDbRowRecoveryMiss(entry);
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
323
434
|
// ─── Commit Instruction Helpers ──────────────────────────────────────────────
|
|
324
435
|
|
|
325
436
|
/** Build commit instruction for planning prompts. .gsd/ is managed externally and always gitignored. */
|
|
@@ -344,10 +455,8 @@ interface PendingDeepProjectSetupEntry {
|
|
|
344
455
|
// phrase before giving up and asking the user to re-run /gsd.
|
|
345
456
|
const MAX_READY_REJECTS = 2;
|
|
346
457
|
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
// to investigate manually.
|
|
350
|
-
const MAX_PLAN_BLOCKED_RECOVERIES = 3;
|
|
458
|
+
// Cap failed in-flight DB row repair attempts before escalating to the user.
|
|
459
|
+
const MAX_DB_ROW_RECOVERIES = 3;
|
|
351
460
|
|
|
352
461
|
// #4573: matches the canonical ready phrase the discuss prompt asks the LLM
|
|
353
462
|
// to emit. Accepts any M-prefixed milestone ID (three digits + optional
|
|
@@ -613,72 +722,12 @@ export function checkAutoStartAfterDiscuss(lookupBasePath?: string): boolean {
|
|
|
613
722
|
}
|
|
614
723
|
}
|
|
615
724
|
|
|
616
|
-
// Gate 1b:
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
// gsd_plan_milestone, then return false (keep blocking auto-start).
|
|
621
|
-
// If CONTEXT.md does not exist (discuss-incomplete), Gate 1 already blocked above.
|
|
622
|
-
if (isDbAvailable()) {
|
|
623
|
-
const dbRow = getMilestone(milestoneId);
|
|
624
|
-
if (dbRow?.status === "queued" && contextFile) {
|
|
625
|
-
if (entry.planBlockedRecoveryCount >= MAX_PLAN_BLOCKED_RECOVERIES) {
|
|
626
|
-
// H1: recovery loop cap reached — stop triggering new turns, escalate to user.
|
|
627
|
-
logWarning(
|
|
628
|
-
"guided",
|
|
629
|
-
`Gate 1b: milestone ${milestoneId} plan-blocked recovery limit reached ` +
|
|
630
|
-
`(${entry.planBlockedRecoveryCount}/${MAX_PLAN_BLOCKED_RECOVERIES}); escalating to user`,
|
|
631
|
-
);
|
|
632
|
-
ctx.ui.notify(
|
|
633
|
-
`Milestone ${milestoneId} plan_milestone has been blocked ${entry.planBlockedRecoveryCount} times. ` +
|
|
634
|
-
`Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`,
|
|
635
|
-
"error",
|
|
636
|
-
);
|
|
637
|
-
return false;
|
|
638
|
-
}
|
|
639
|
-
logWarning(
|
|
640
|
-
"guided",
|
|
641
|
-
`Gate 1b: milestone ${milestoneId} queued with CONTEXT.md present — ` +
|
|
642
|
-
`plan_milestone was blocked; emitting recovery hint ` +
|
|
643
|
-
`(attempt ${entry.planBlockedRecoveryCount + 1}/${MAX_PLAN_BLOCKED_RECOVERIES})`,
|
|
644
|
-
);
|
|
645
|
-
ctx.ui.notify(
|
|
646
|
-
`Milestone ${milestoneId}: context file exists but milestone is still queued. ` +
|
|
647
|
-
`Retrying gsd_plan_milestone to complete the blocked planning step.`,
|
|
648
|
-
"warning",
|
|
649
|
-
);
|
|
650
|
-
try {
|
|
651
|
-
pi.sendMessage(
|
|
652
|
-
{
|
|
653
|
-
customType: "gsd-plan-milestone-blocked-recovery",
|
|
654
|
-
content:
|
|
655
|
-
`Milestone ${milestoneId} has ${contextFile} on disk but its DB row is still ` +
|
|
656
|
-
`"queued". The gsd_plan_milestone tool was previously blocked by the ` +
|
|
657
|
-
`depth-verification gate. Call gsd_plan_milestone now to complete the ` +
|
|
658
|
-
`planning phase.`,
|
|
659
|
-
display: false,
|
|
660
|
-
},
|
|
661
|
-
{ triggerTurn: true },
|
|
662
|
-
);
|
|
663
|
-
// Increment only after a successful dispatch so transient sendMessage
|
|
664
|
-
// failures do not consume recovery budget.
|
|
665
|
-
entry.planBlockedRecoveryCount += 1;
|
|
666
|
-
} catch (e) {
|
|
667
|
-
logWarning("guided", `Gate 1b recovery sendMessage failed: ${(e as Error).message}`);
|
|
668
|
-
}
|
|
669
|
-
return false;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Gate 2: STATE.md must exist — written as the last step in the discuss
|
|
674
|
-
// output phase. This prevents auto-start from firing during Phase 3
|
|
675
|
-
// (sequential readiness gates for remaining milestones) in multi-milestone
|
|
676
|
-
// discussions, where M001-CONTEXT.md exists but M002/M003 haven't been
|
|
677
|
-
// processed yet.
|
|
678
|
-
const stateFilePath = entry.scope.stateFile();
|
|
679
|
-
if (!existsSync(stateFilePath)) return false; // discussion not finalized yet
|
|
725
|
+
// Gate 1b: accept the in-flight discuss handoff. A queued DB row with pinned
|
|
726
|
+
// CONTEXT.md is Discussion Complete, Planning Pending, not a plan-blocked
|
|
727
|
+
// failure. If the row is missing, only this pending handoff may repair it.
|
|
728
|
+
if (!ensureMilestoneRowForAcceptedHandoff(entry, contextFile)) return false;
|
|
680
729
|
|
|
681
|
-
// Gate
|
|
730
|
+
// Gate 2: Multi-milestone completeness warning
|
|
682
731
|
// Parse PROJECT.md for milestone sequence, warn if any are missing context.
|
|
683
732
|
// Don't block — milestones can be intentionally queued without context.
|
|
684
733
|
const projectFile = resolveGsdRootFile(basePath, "PROJECT");
|
|
@@ -705,7 +754,7 @@ export function checkAutoStartAfterDiscuss(lookupBasePath?: string): boolean {
|
|
|
705
754
|
} catch (e) { logWarning("guided", `PROJECT.md parsing failed: ${(e as Error).message}`); }
|
|
706
755
|
}
|
|
707
756
|
|
|
708
|
-
// Gate
|
|
757
|
+
// Gate 3: Discussion manifest process verification (multi-milestone only)
|
|
709
758
|
// The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
|
|
710
759
|
// When it exists, validate it before auto-starting. Project history alone is
|
|
711
760
|
// not a reliable signal for the current discussion mode.
|
|
@@ -747,71 +796,12 @@ export function checkAutoStartAfterDiscuss(lookupBasePath?: string): boolean {
|
|
|
747
796
|
try { unlinkSync(manifestPath); } catch (e) { logWarning("guided", `manifest unlink failed: ${(e as Error).message}`); }
|
|
748
797
|
}
|
|
749
798
|
|
|
750
|
-
// R3b: belt-and-suspenders for silent registration failure. The discuss flow
|
|
751
|
-
// finished and STATE.md exists, but the milestone may never have landed in
|
|
752
|
-
// the DB. Without this guard, the user sees "Milestone M001 ready." and then
|
|
753
|
-
// /gsd reports "No Active Milestone".
|
|
754
|
-
if (isDbAvailable()) {
|
|
755
|
-
const milestoneRow = getMilestone(milestoneId);
|
|
756
|
-
if (!milestoneRow) {
|
|
757
|
-
let manifestHasMilestone = false;
|
|
758
|
-
try {
|
|
759
|
-
const manifest = readManifest(basePath);
|
|
760
|
-
manifestHasMilestone = Array.isArray(manifest?.milestones) && manifest.milestones.some(m => m.id === milestoneId);
|
|
761
|
-
} catch (e) {
|
|
762
|
-
logWarning("guided", `R3b: failed to read state manifest: ${(e as Error).message}`);
|
|
763
|
-
}
|
|
764
|
-
if (manifestHasMilestone) {
|
|
765
|
-
logWarning("guided", `R3b: getMilestone(${milestoneId}) returned null but manifest has the row — treating as stale read`);
|
|
766
|
-
} else if (contextFile) {
|
|
767
|
-
// R3b-recovery: CONTEXT.md is on disk but gsd_plan_milestone was never called
|
|
768
|
-
// (likely blocked by the depth-verification gate re-firing on post-verification
|
|
769
|
-
// text). Auto-register as "queued" so Gate 1b can pick it up and retry
|
|
770
|
-
// gsd_plan_milestone on the next checkAutoStartAfterDiscuss call.
|
|
771
|
-
if (entry.r3bRecoveryCount >= MAX_PLAN_BLOCKED_RECOVERIES) {
|
|
772
|
-
logWarning(
|
|
773
|
-
"guided",
|
|
774
|
-
`R3b: milestone ${milestoneId} DB-row recovery limit reached ` +
|
|
775
|
-
`(${entry.r3bRecoveryCount}/${MAX_PLAN_BLOCKED_RECOVERIES}); escalating to user`,
|
|
776
|
-
);
|
|
777
|
-
ctx.ui.notify(
|
|
778
|
-
`Milestone ${milestoneId}: DB row recovery failed ${entry.r3bRecoveryCount} times. ` +
|
|
779
|
-
`Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`,
|
|
780
|
-
"error",
|
|
781
|
-
);
|
|
782
|
-
return false;
|
|
783
|
-
}
|
|
784
|
-
logWarning(
|
|
785
|
-
"guided",
|
|
786
|
-
`R3b: ${milestoneId} has CONTEXT.md but no DB row — inserting placeholder "queued" row ` +
|
|
787
|
-
`for Gate 1b recovery (attempt ${entry.r3bRecoveryCount + 1}/${MAX_PLAN_BLOCKED_RECOVERIES})`,
|
|
788
|
-
);
|
|
789
|
-
try {
|
|
790
|
-
insertMilestone({ id: milestoneId, title: milestoneId, status: "queued" });
|
|
791
|
-
} catch (e) {
|
|
792
|
-
logWarning("guided", `R3b: insertMilestone failed: ${(e as Error).message}`);
|
|
793
|
-
}
|
|
794
|
-
entry.r3bRecoveryCount += 1;
|
|
795
|
-
ctx.ui.notify(
|
|
796
|
-
`Milestone ${milestoneId}: context file exists but DB row was missing — recovering. Retrying gsd_plan_milestone.`,
|
|
797
|
-
"warning",
|
|
798
|
-
);
|
|
799
|
-
return false;
|
|
800
|
-
} else {
|
|
801
|
-
ctx.ui.notify(
|
|
802
|
-
`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
|
|
803
|
-
`PROJECT.md may have failed to register milestones. ` +
|
|
804
|
-
`Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
|
|
805
|
-
`then re-run /gsd to recover.`,
|
|
806
|
-
"error",
|
|
807
|
-
);
|
|
808
|
-
return false;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
799
|
deletePendingAutoStart(basePath);
|
|
814
|
-
|
|
800
|
+
const hasExecutablePlan = hasExecutablePlanForHandoff(milestoneId, roadmapFile);
|
|
801
|
+
ctx.ui.notify(
|
|
802
|
+
formatAcceptedDiscussHandoffMessage(milestoneId, contextFile, hasExecutablePlan),
|
|
803
|
+
"success",
|
|
804
|
+
);
|
|
815
805
|
if (entry.startAuto !== false) {
|
|
816
806
|
scheduleAutoStartAfterIdle(ctx, pi, basePath, false, { step: step ?? true });
|
|
817
807
|
}
|
|
@@ -16,6 +16,7 @@ import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js";
|
|
|
16
16
|
import { logWarning } from "./workflow-logger.js";
|
|
17
17
|
import { hasImplementationArtifacts } from "./milestone-implementation-evidence.js";
|
|
18
18
|
import { buildCompleteMilestonePrompt } from "./auto-prompts.js";
|
|
19
|
+
import { checkCloseoutConsistencyGate } from "./closeout-consistency-gate.js";
|
|
19
20
|
import type { DispatchAction, DispatchContext } from "./auto-dispatch.js";
|
|
20
21
|
import {
|
|
21
22
|
commitPendingMilestoneCloseoutChanges,
|
|
@@ -37,7 +38,8 @@ export async function isMilestoneCloseoutSettled(mid: string, basePath: string):
|
|
|
37
38
|
if (isDbAvailable()) {
|
|
38
39
|
const milestone = getMilestone(mid);
|
|
39
40
|
if (milestone && isClosedStatus(milestone.status)) {
|
|
40
|
-
|
|
41
|
+
const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
|
|
42
|
+
if (closeoutGate.ok && verifyExpectedArtifact("complete-milestone", mid, basePath)) {
|
|
41
43
|
return true;
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -14,7 +14,6 @@ export interface PendingAutoStartEntry {
|
|
|
14
14
|
createdAt: number;
|
|
15
15
|
readyRejectCount?: number;
|
|
16
16
|
scope: MilestoneScope;
|
|
17
|
-
planBlockedRecoveryCount: number;
|
|
18
17
|
r3bRecoveryCount: number;
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -51,7 +50,6 @@ export function setPendingAutoStart(basePath: string, entry: PendingAutoStartInp
|
|
|
51
50
|
const scope = scopeMilestone(ws, entry.milestoneId);
|
|
52
51
|
pendingAutoStartMap.set(basePath, {
|
|
53
52
|
createdAt: Date.now(),
|
|
54
|
-
planBlockedRecoveryCount: 0,
|
|
55
53
|
r3bRecoveryCount: 0,
|
|
56
54
|
...entry,
|
|
57
55
|
scope,
|
|
@@ -75,24 +75,10 @@ verdict: "PASS" | "FAIL" | "PARTIAL",
|
|
|
75
75
|
notes: "<one sentence overall verdict rationale>",
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
Use this
|
|
78
|
+
Use this canonical `presentation` object in the save call so the audit can verify the run-uat tool surface without retrying missing fields one by one. Keep `toolPresentationPlanId` as `{{toolPresentationPlanId}}`. If browser tools were actually presented for this run, add those concrete browser tool names to `presentedTools`; otherwise reuse this object exactly:
|
|
79
79
|
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
surface: "mcp",
|
|
83
|
-
presentedTools: [
|
|
84
|
-
"gsd_uat_exec",
|
|
85
|
-
"gsd_uat_result_save",
|
|
86
|
-
"gsd_resume",
|
|
87
|
-
"gsd_milestone_status",
|
|
88
|
-
"gsd_journal_query",
|
|
89
|
-
],
|
|
90
|
-
blockedTools: [
|
|
91
|
-
{ name: "gsd_exec", reason: "forbidden during run-uat" },
|
|
92
|
-
{ name: "gsd_summary_save", reason: "forbidden during run-uat" },
|
|
93
|
-
{ name: "gsd_save_gate_result", reason: "forbidden during run-uat" },
|
|
94
|
-
],
|
|
95
|
-
}
|
|
80
|
+
```json
|
|
81
|
+
{{canonicalPresentation}}
|
|
96
82
|
```
|
|
97
83
|
|
|
98
84
|
Pass `checks` with this logical shape:
|
|
@@ -6,7 +6,9 @@ import { ReconciliationFailedError } from "./state-reconciliation.js";
|
|
|
6
6
|
|
|
7
7
|
export type RecoveryFailureKind =
|
|
8
8
|
| "tool-schema"
|
|
9
|
+
| "tool-contract"
|
|
9
10
|
| "deterministic-policy"
|
|
11
|
+
| "lifecycle-progression"
|
|
10
12
|
| "stale-worker"
|
|
11
13
|
| "worktree-invalid"
|
|
12
14
|
| "verification-drift"
|
|
@@ -52,6 +54,14 @@ export function classifyFailure(input: RecoveryClassificationInput): RecoveryCla
|
|
|
52
54
|
exitReason: "tool-schema",
|
|
53
55
|
remediation: "Fix the Unit Tool Contract or tool schema before retrying.",
|
|
54
56
|
};
|
|
57
|
+
case "tool-contract":
|
|
58
|
+
return {
|
|
59
|
+
failureKind,
|
|
60
|
+
action: "stop",
|
|
61
|
+
reason: `Tool Contract failure${unitSuffix(input)}: ${message}`,
|
|
62
|
+
exitReason: "tool-contract",
|
|
63
|
+
remediation: "Fix the Unit Tool Contract or prompt so the Unit is only asked to use tools owned by its phase.",
|
|
64
|
+
};
|
|
55
65
|
case "deterministic-policy":
|
|
56
66
|
return {
|
|
57
67
|
failureKind,
|
|
@@ -60,6 +70,14 @@ export function classifyFailure(input: RecoveryClassificationInput): RecoveryCla
|
|
|
60
70
|
exitReason: "deterministic-policy",
|
|
61
71
|
remediation: "Resolve the policy blocker; retrying the same Unit will repeat the failure.",
|
|
62
72
|
};
|
|
73
|
+
case "lifecycle-progression":
|
|
74
|
+
return {
|
|
75
|
+
failureKind,
|
|
76
|
+
action: "stop",
|
|
77
|
+
reason: `Lifecycle progression failure${unitSuffix(input)}: ${message}`,
|
|
78
|
+
exitReason: "lifecycle-progression",
|
|
79
|
+
remediation: "Route to the required owning Unit or restore the missing artifact before advancing lifecycle state.",
|
|
80
|
+
};
|
|
63
81
|
case "stale-worker":
|
|
64
82
|
return {
|
|
65
83
|
failureKind,
|
|
@@ -118,6 +136,8 @@ export function classifyFailure(input: RecoveryClassificationInput): RecoveryCla
|
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
function inferFailureKind(message: string): RecoveryFailureKind {
|
|
139
|
+
if (/tool contract|auto-unit tool scope|phase-boundary gate|not permitted.*own/i.test(message)) return "tool-contract";
|
|
140
|
+
if (/lifecycle progression|required artifact|missing .*assessment|missing .*closeout|cannot legally (?:advance|progress)/i.test(message)) return "lifecycle-progression";
|
|
121
141
|
if (/schema|invalid.*tool|tool.*invalid|enum/i.test(message)) return "tool-schema";
|
|
122
142
|
if (/deterministic policy|policy rejection|write gate|blocked by policy/i.test(message)) return "deterministic-policy";
|
|
123
143
|
if (/stale worker|stale lock|worker.*stale/i.test(message)) return "stale-worker";
|
|
@@ -1558,6 +1558,14 @@ test("verifyExpectedArtifact complete-milestone passes when DB milestone is comp
|
|
|
1558
1558
|
|
|
1559
1559
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
1560
1560
|
insertMilestone({ id: "M001", title: "Milestone One", status: "complete" });
|
|
1561
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Done Slice", status: "complete" });
|
|
1562
|
+
insertAssessment({
|
|
1563
|
+
path: "milestones/M001/M001-VALIDATION.md",
|
|
1564
|
+
milestoneId: "M001",
|
|
1565
|
+
status: "pass",
|
|
1566
|
+
scope: "milestone-validation",
|
|
1567
|
+
fullContent: "verdict: pass",
|
|
1568
|
+
});
|
|
1561
1569
|
|
|
1562
1570
|
const result = verifyExpectedArtifact("complete-milestone", "M001", base);
|
|
1563
1571
|
assert.equal(result, true, "complete-milestone should pass when DB status is complete");
|
|
@@ -1566,7 +1574,7 @@ test("verifyExpectedArtifact complete-milestone passes when DB milestone is comp
|
|
|
1566
1574
|
}
|
|
1567
1575
|
});
|
|
1568
1576
|
|
|
1569
|
-
test("verifyExpectedArtifact complete-milestone
|
|
1577
|
+
test("verifyExpectedArtifact complete-milestone rejects success SUMMARY when DB milestone is still open (#4658)", () => {
|
|
1570
1578
|
const base = makeGitBase();
|
|
1571
1579
|
try {
|
|
1572
1580
|
execFileSync("git", ["checkout", "-b", "feat/ms-db-lag-success"], { cwd: base, stdio: "ignore" });
|
|
@@ -1591,7 +1599,7 @@ test("verifyExpectedArtifact complete-milestone tolerates transient DB lag when
|
|
|
1591
1599
|
insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
|
|
1592
1600
|
|
|
1593
1601
|
const result = verifyExpectedArtifact("complete-milestone", "M001", base);
|
|
1594
|
-
assert.equal(result,
|
|
1602
|
+
assert.equal(result, false, "success SUMMARY must not overrule an open DB milestone");
|
|
1595
1603
|
} finally {
|
|
1596
1604
|
cleanup(base);
|
|
1597
1605
|
}
|
|
@@ -30,6 +30,7 @@ test("checkAutoStartAfterDiscuss waits until discussion artifacts exist before r
|
|
|
30
30
|
setPendingAutoStart(base, {
|
|
31
31
|
basePath: base,
|
|
32
32
|
milestoneId: "M001",
|
|
33
|
+
startAuto: false,
|
|
33
34
|
ctx: { ui: { notify: (message: string) => notifications.push(message) } } as any,
|
|
34
35
|
pi: { sendMessage: () => {} } as any,
|
|
35
36
|
});
|
|
@@ -41,5 +42,7 @@ test("checkAutoStartAfterDiscuss waits until discussion artifacts exist before r
|
|
|
41
42
|
writeFileSync(join(base, ".gsd", "STATE.md"), "# State\n", "utf-8");
|
|
42
43
|
|
|
43
44
|
assert.equal(checkAutoStartAfterDiscuss(), true);
|
|
44
|
-
assert.deepEqual(notifications, [
|
|
45
|
+
assert.deepEqual(notifications, [
|
|
46
|
+
"Milestone M001 context captured. Continuing the planning pipeline.",
|
|
47
|
+
]);
|
|
45
48
|
});
|
|
@@ -92,13 +92,23 @@ test("checkAutoStartAfterDiscuss completes when discussion manifest is absent",
|
|
|
92
92
|
setPendingAutoStart(base, {
|
|
93
93
|
basePath: base,
|
|
94
94
|
milestoneId: "M001",
|
|
95
|
-
|
|
95
|
+
startAuto: false,
|
|
96
|
+
ctx: {
|
|
97
|
+
ui: {
|
|
98
|
+
notify: (message: string, level: string) => notifications.push({ message, level }),
|
|
99
|
+
},
|
|
100
|
+
} as any,
|
|
96
101
|
pi: { sendMessage: () => { scheduled = true; } } as any,
|
|
97
102
|
});
|
|
98
103
|
|
|
99
104
|
assert.equal(checkAutoStartAfterDiscuss(), true);
|
|
100
105
|
assert.equal(scheduled, false);
|
|
101
|
-
assert.deepEqual(notifications, [
|
|
106
|
+
assert.deepEqual(notifications, [
|
|
107
|
+
{
|
|
108
|
+
message: "Milestone M001 context captured. Continuing the planning pipeline.",
|
|
109
|
+
level: "success",
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
102
112
|
} finally {
|
|
103
113
|
clearPendingAutoStart(base);
|
|
104
114
|
rmSync(base, { recursive: true, force: true });
|