@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
|
@@ -178,6 +178,84 @@ export function _roadmapHasParseableSlicesForTest(roadmapContent) {
|
|
|
178
178
|
return false;
|
|
179
179
|
return parseRoadmapSlices(roadmapContent).length > 0;
|
|
180
180
|
}
|
|
181
|
+
function hasExecutablePlanForHandoff(milestoneId, roadmapFile) {
|
|
182
|
+
if (isDbAvailable()) {
|
|
183
|
+
return getMilestoneSlices(milestoneId).length > 0;
|
|
184
|
+
}
|
|
185
|
+
if (!roadmapFile)
|
|
186
|
+
return false;
|
|
187
|
+
try {
|
|
188
|
+
return parseRoadmapSlices(readFileSync(roadmapFile, "utf-8")).length > 0;
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
logWarning("guided", `failed to parse roadmap slices for ${milestoneId}: ${e.message}`);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function formatAcceptedDiscussHandoffMessage(milestoneId, contextFile, hasExecutablePlan) {
|
|
196
|
+
if (hasExecutablePlan)
|
|
197
|
+
return `Milestone ${milestoneId} ready.`;
|
|
198
|
+
if (contextFile)
|
|
199
|
+
return `Milestone ${milestoneId} context captured. Continuing the planning pipeline.`;
|
|
200
|
+
return `Milestone ${milestoneId} planning artifacts captured. Continuing the planning pipeline.`;
|
|
201
|
+
}
|
|
202
|
+
function manifestContainsMilestone(basePath, milestoneId) {
|
|
203
|
+
try {
|
|
204
|
+
const manifest = readManifest(basePath);
|
|
205
|
+
return (Array.isArray(manifest?.milestones) &&
|
|
206
|
+
manifest.milestones.some(m => m.id === milestoneId));
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
logWarning("guided", `R3b: failed to read state manifest: ${e.message}`);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function notifyDbRowRecoveryFailed(entry) {
|
|
214
|
+
entry.ctx.ui.notify(`Milestone ${entry.milestoneId}: DB row recovery failed ${entry.r3bRecoveryCount} times. ` +
|
|
215
|
+
`Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`, "error");
|
|
216
|
+
}
|
|
217
|
+
function noteDbRowRecoveryMiss(entry) {
|
|
218
|
+
entry.r3bRecoveryCount += 1;
|
|
219
|
+
if (entry.r3bRecoveryCount >= MAX_DB_ROW_RECOVERIES) {
|
|
220
|
+
notifyDbRowRecoveryFailed(entry);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function ensureMilestoneRowForAcceptedHandoff(entry, contextFile) {
|
|
224
|
+
if (!isDbAvailable())
|
|
225
|
+
return true;
|
|
226
|
+
const { basePath, milestoneId } = entry;
|
|
227
|
+
const milestoneRow = getMilestone(milestoneId);
|
|
228
|
+
if (milestoneRow)
|
|
229
|
+
return true;
|
|
230
|
+
if (manifestContainsMilestone(basePath, milestoneId)) {
|
|
231
|
+
logWarning("guided", `R3b: getMilestone(${milestoneId}) returned null but manifest has the row — treating as stale read`);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (!contextFile) {
|
|
235
|
+
entry.ctx.ui.notify(`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
|
|
236
|
+
`PROJECT.md may have failed to register milestones. ` +
|
|
237
|
+
`Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
|
|
238
|
+
`then re-run /gsd to recover.`, "error");
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (entry.r3bRecoveryCount >= MAX_DB_ROW_RECOVERIES) {
|
|
242
|
+
logWarning("guided", `R3b: milestone ${milestoneId} DB-row recovery limit reached ` +
|
|
243
|
+
`(${entry.r3bRecoveryCount}/${MAX_DB_ROW_RECOVERIES}); user already notified`);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
logWarning("guided", `R3b: ${milestoneId} has CONTEXT.md but no DB row — inserting placeholder "queued" row ` +
|
|
247
|
+
`(attempt ${entry.r3bRecoveryCount + 1}/${MAX_DB_ROW_RECOVERIES})`);
|
|
248
|
+
try {
|
|
249
|
+
insertMilestone({ id: milestoneId, title: milestoneId, status: "queued" });
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
logWarning("guided", `R3b: insertMilestone failed: ${e.message}`);
|
|
253
|
+
}
|
|
254
|
+
if (getMilestone(milestoneId))
|
|
255
|
+
return true;
|
|
256
|
+
noteDbRowRecoveryMiss(entry);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
181
259
|
// ─── Commit Instruction Helpers ──────────────────────────────────────────────
|
|
182
260
|
/** Build commit instruction for planning prompts. .gsd/ is managed externally and always gitignored. */
|
|
183
261
|
function buildDocsCommitInstruction(_message) {
|
|
@@ -186,10 +264,8 @@ function buildDocsCommitInstruction(_message) {
|
|
|
186
264
|
// #4573: cap for how many times we nudge the LLM after a premature ready
|
|
187
265
|
// phrase before giving up and asking the user to re-run /gsd.
|
|
188
266
|
const MAX_READY_REJECTS = 2;
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
// to investigate manually.
|
|
192
|
-
const MAX_PLAN_BLOCKED_RECOVERIES = 3;
|
|
267
|
+
// Cap failed in-flight DB row repair attempts before escalating to the user.
|
|
268
|
+
const MAX_DB_ROW_RECOVERIES = 3;
|
|
193
269
|
// #4573: matches the canonical ready phrase the discuss prompt asks the LLM
|
|
194
270
|
// to emit. Accepts any M-prefixed milestone ID (three digits + optional
|
|
195
271
|
// suffix) with optional trailing punctuation.
|
|
@@ -420,56 +496,12 @@ export function checkAutoStartAfterDiscuss(lookupBasePath) {
|
|
|
420
496
|
return false;
|
|
421
497
|
}
|
|
422
498
|
}
|
|
423
|
-
// Gate 1b:
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
//
|
|
429
|
-
if (isDbAvailable()) {
|
|
430
|
-
const dbRow = getMilestone(milestoneId);
|
|
431
|
-
if (dbRow?.status === "queued" && contextFile) {
|
|
432
|
-
if (entry.planBlockedRecoveryCount >= MAX_PLAN_BLOCKED_RECOVERIES) {
|
|
433
|
-
// H1: recovery loop cap reached — stop triggering new turns, escalate to user.
|
|
434
|
-
logWarning("guided", `Gate 1b: milestone ${milestoneId} plan-blocked recovery limit reached ` +
|
|
435
|
-
`(${entry.planBlockedRecoveryCount}/${MAX_PLAN_BLOCKED_RECOVERIES}); escalating to user`);
|
|
436
|
-
ctx.ui.notify(`Milestone ${milestoneId} plan_milestone has been blocked ${entry.planBlockedRecoveryCount} times. ` +
|
|
437
|
-
`Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`, "error");
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
logWarning("guided", `Gate 1b: milestone ${milestoneId} queued with CONTEXT.md present — ` +
|
|
441
|
-
`plan_milestone was blocked; emitting recovery hint ` +
|
|
442
|
-
`(attempt ${entry.planBlockedRecoveryCount + 1}/${MAX_PLAN_BLOCKED_RECOVERIES})`);
|
|
443
|
-
ctx.ui.notify(`Milestone ${milestoneId}: context file exists but milestone is still queued. ` +
|
|
444
|
-
`Retrying gsd_plan_milestone to complete the blocked planning step.`, "warning");
|
|
445
|
-
try {
|
|
446
|
-
pi.sendMessage({
|
|
447
|
-
customType: "gsd-plan-milestone-blocked-recovery",
|
|
448
|
-
content: `Milestone ${milestoneId} has ${contextFile} on disk but its DB row is still ` +
|
|
449
|
-
`"queued". The gsd_plan_milestone tool was previously blocked by the ` +
|
|
450
|
-
`depth-verification gate. Call gsd_plan_milestone now to complete the ` +
|
|
451
|
-
`planning phase.`,
|
|
452
|
-
display: false,
|
|
453
|
-
}, { triggerTurn: true });
|
|
454
|
-
// Increment only after a successful dispatch so transient sendMessage
|
|
455
|
-
// failures do not consume recovery budget.
|
|
456
|
-
entry.planBlockedRecoveryCount += 1;
|
|
457
|
-
}
|
|
458
|
-
catch (e) {
|
|
459
|
-
logWarning("guided", `Gate 1b recovery sendMessage failed: ${e.message}`);
|
|
460
|
-
}
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// Gate 2: STATE.md must exist — written as the last step in the discuss
|
|
465
|
-
// output phase. This prevents auto-start from firing during Phase 3
|
|
466
|
-
// (sequential readiness gates for remaining milestones) in multi-milestone
|
|
467
|
-
// discussions, where M001-CONTEXT.md exists but M002/M003 haven't been
|
|
468
|
-
// processed yet.
|
|
469
|
-
const stateFilePath = entry.scope.stateFile();
|
|
470
|
-
if (!existsSync(stateFilePath))
|
|
471
|
-
return false; // discussion not finalized yet
|
|
472
|
-
// Gate 3: Multi-milestone completeness warning
|
|
499
|
+
// Gate 1b: accept the in-flight discuss handoff. A queued DB row with pinned
|
|
500
|
+
// CONTEXT.md is Discussion Complete, Planning Pending, not a plan-blocked
|
|
501
|
+
// failure. If the row is missing, only this pending handoff may repair it.
|
|
502
|
+
if (!ensureMilestoneRowForAcceptedHandoff(entry, contextFile))
|
|
503
|
+
return false;
|
|
504
|
+
// Gate 2: Multi-milestone completeness warning
|
|
473
505
|
// Parse PROJECT.md for milestone sequence, warn if any are missing context.
|
|
474
506
|
// Don't block — milestones can be intentionally queued without context.
|
|
475
507
|
const projectFile = resolveGsdRootFile(basePath, "PROJECT");
|
|
@@ -495,7 +527,7 @@ export function checkAutoStartAfterDiscuss(lookupBasePath) {
|
|
|
495
527
|
logWarning("guided", `PROJECT.md parsing failed: ${e.message}`);
|
|
496
528
|
}
|
|
497
529
|
}
|
|
498
|
-
// Gate
|
|
530
|
+
// Gate 3: Discussion manifest process verification (multi-milestone only)
|
|
499
531
|
// The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
|
|
500
532
|
// When it exists, validate it before auto-starting. Project history alone is
|
|
501
533
|
// not a reliable signal for the current discussion mode.
|
|
@@ -541,59 +573,9 @@ export function checkAutoStartAfterDiscuss(lookupBasePath) {
|
|
|
541
573
|
logWarning("guided", `manifest unlink failed: ${e.message}`);
|
|
542
574
|
}
|
|
543
575
|
}
|
|
544
|
-
// R3b: belt-and-suspenders for silent registration failure. The discuss flow
|
|
545
|
-
// finished and STATE.md exists, but the milestone may never have landed in
|
|
546
|
-
// the DB. Without this guard, the user sees "Milestone M001 ready." and then
|
|
547
|
-
// /gsd reports "No Active Milestone".
|
|
548
|
-
if (isDbAvailable()) {
|
|
549
|
-
const milestoneRow = getMilestone(milestoneId);
|
|
550
|
-
if (!milestoneRow) {
|
|
551
|
-
let manifestHasMilestone = false;
|
|
552
|
-
try {
|
|
553
|
-
const manifest = readManifest(basePath);
|
|
554
|
-
manifestHasMilestone = Array.isArray(manifest?.milestones) && manifest.milestones.some(m => m.id === milestoneId);
|
|
555
|
-
}
|
|
556
|
-
catch (e) {
|
|
557
|
-
logWarning("guided", `R3b: failed to read state manifest: ${e.message}`);
|
|
558
|
-
}
|
|
559
|
-
if (manifestHasMilestone) {
|
|
560
|
-
logWarning("guided", `R3b: getMilestone(${milestoneId}) returned null but manifest has the row — treating as stale read`);
|
|
561
|
-
}
|
|
562
|
-
else if (contextFile) {
|
|
563
|
-
// R3b-recovery: CONTEXT.md is on disk but gsd_plan_milestone was never called
|
|
564
|
-
// (likely blocked by the depth-verification gate re-firing on post-verification
|
|
565
|
-
// text). Auto-register as "queued" so Gate 1b can pick it up and retry
|
|
566
|
-
// gsd_plan_milestone on the next checkAutoStartAfterDiscuss call.
|
|
567
|
-
if (entry.r3bRecoveryCount >= MAX_PLAN_BLOCKED_RECOVERIES) {
|
|
568
|
-
logWarning("guided", `R3b: milestone ${milestoneId} DB-row recovery limit reached ` +
|
|
569
|
-
`(${entry.r3bRecoveryCount}/${MAX_PLAN_BLOCKED_RECOVERIES}); escalating to user`);
|
|
570
|
-
ctx.ui.notify(`Milestone ${milestoneId}: DB row recovery failed ${entry.r3bRecoveryCount} times. ` +
|
|
571
|
-
`Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`, "error");
|
|
572
|
-
return false;
|
|
573
|
-
}
|
|
574
|
-
logWarning("guided", `R3b: ${milestoneId} has CONTEXT.md but no DB row — inserting placeholder "queued" row ` +
|
|
575
|
-
`for Gate 1b recovery (attempt ${entry.r3bRecoveryCount + 1}/${MAX_PLAN_BLOCKED_RECOVERIES})`);
|
|
576
|
-
try {
|
|
577
|
-
insertMilestone({ id: milestoneId, title: milestoneId, status: "queued" });
|
|
578
|
-
}
|
|
579
|
-
catch (e) {
|
|
580
|
-
logWarning("guided", `R3b: insertMilestone failed: ${e.message}`);
|
|
581
|
-
}
|
|
582
|
-
entry.r3bRecoveryCount += 1;
|
|
583
|
-
ctx.ui.notify(`Milestone ${milestoneId}: context file exists but DB row was missing — recovering. Retrying gsd_plan_milestone.`, "warning");
|
|
584
|
-
return false;
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
ctx.ui.notify(`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
|
|
588
|
-
`PROJECT.md may have failed to register milestones. ` +
|
|
589
|
-
`Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
|
|
590
|
-
`then re-run /gsd to recover.`, "error");
|
|
591
|
-
return false;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
576
|
deletePendingAutoStart(basePath);
|
|
596
|
-
|
|
577
|
+
const hasExecutablePlan = hasExecutablePlanForHandoff(milestoneId, roadmapFile);
|
|
578
|
+
ctx.ui.notify(formatAcceptedDiscussHandoffMessage(milestoneId, contextFile, hasExecutablePlan), "success");
|
|
597
579
|
if (entry.startAuto !== false) {
|
|
598
580
|
scheduleAutoStartAfterIdle(ctx, pi, basePath, false, { step: step ?? true });
|
|
599
581
|
}
|
|
@@ -15,6 +15,7 @@ import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js";
|
|
|
15
15
|
import { logWarning } from "./workflow-logger.js";
|
|
16
16
|
import { hasImplementationArtifacts } from "./milestone-implementation-evidence.js";
|
|
17
17
|
import { buildCompleteMilestonePrompt } from "./auto-prompts.js";
|
|
18
|
+
import { checkCloseoutConsistencyGate } from "./closeout-consistency-gate.js";
|
|
18
19
|
import { commitPendingMilestoneCloseoutChanges, findMissingSummaries, isVerificationNotApplicable, readUatGateVerdict, } from "./auto-dispatch.js";
|
|
19
20
|
const COMPLETE_MILESTONE_DB_SETTLE_MS = 1500;
|
|
20
21
|
const COMPLETE_MILESTONE_DB_SETTLE_POLL_MS = 100;
|
|
@@ -28,7 +29,8 @@ export async function isMilestoneCloseoutSettled(mid, basePath) {
|
|
|
28
29
|
if (isDbAvailable()) {
|
|
29
30
|
const milestone = getMilestone(mid);
|
|
30
31
|
if (milestone && isClosedStatus(milestone.status)) {
|
|
31
|
-
|
|
32
|
+
const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
|
|
33
|
+
if (closeoutGate.ok && verifyExpectedArtifact("complete-milestone", mid, basePath)) {
|
|
32
34
|
return true;
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -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:
|
|
@@ -19,6 +19,14 @@ export function classifyFailure(input) {
|
|
|
19
19
|
exitReason: "tool-schema",
|
|
20
20
|
remediation: "Fix the Unit Tool Contract or tool schema before retrying.",
|
|
21
21
|
};
|
|
22
|
+
case "tool-contract":
|
|
23
|
+
return {
|
|
24
|
+
failureKind,
|
|
25
|
+
action: "stop",
|
|
26
|
+
reason: `Tool Contract failure${unitSuffix(input)}: ${message}`,
|
|
27
|
+
exitReason: "tool-contract",
|
|
28
|
+
remediation: "Fix the Unit Tool Contract or prompt so the Unit is only asked to use tools owned by its phase.",
|
|
29
|
+
};
|
|
22
30
|
case "deterministic-policy":
|
|
23
31
|
return {
|
|
24
32
|
failureKind,
|
|
@@ -27,6 +35,14 @@ export function classifyFailure(input) {
|
|
|
27
35
|
exitReason: "deterministic-policy",
|
|
28
36
|
remediation: "Resolve the policy blocker; retrying the same Unit will repeat the failure.",
|
|
29
37
|
};
|
|
38
|
+
case "lifecycle-progression":
|
|
39
|
+
return {
|
|
40
|
+
failureKind,
|
|
41
|
+
action: "stop",
|
|
42
|
+
reason: `Lifecycle progression failure${unitSuffix(input)}: ${message}`,
|
|
43
|
+
exitReason: "lifecycle-progression",
|
|
44
|
+
remediation: "Route to the required owning Unit or restore the missing artifact before advancing lifecycle state.",
|
|
45
|
+
};
|
|
30
46
|
case "stale-worker":
|
|
31
47
|
return {
|
|
32
48
|
failureKind,
|
|
@@ -83,6 +99,10 @@ export function classifyFailure(input) {
|
|
|
83
99
|
}
|
|
84
100
|
}
|
|
85
101
|
function inferFailureKind(message) {
|
|
102
|
+
if (/tool contract|auto-unit tool scope|phase-boundary gate|not permitted.*own/i.test(message))
|
|
103
|
+
return "tool-contract";
|
|
104
|
+
if (/lifecycle progression|required artifact|missing .*assessment|missing .*closeout|cannot legally (?:advance|progress)/i.test(message))
|
|
105
|
+
return "lifecycle-progression";
|
|
86
106
|
if (/schema|invalid.*tool|tool.*invalid|enum/i.test(message))
|
|
87
107
|
return "tool-schema";
|
|
88
108
|
if (/deterministic policy|policy rejection|write gate|blocked by policy/i.test(message))
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// File Purpose: ADR-015 Tool Contract module for Unit prompt, policy, and tool parity.
|
|
3
3
|
import { resolveManifest, } from "./unit-context-manifest.js";
|
|
4
4
|
import { getRequiredWorkflowToolsForAutoUnit } from "./workflow-mcp.js";
|
|
5
|
+
import { getUnitToolSurfaceContract } from "./unit-tool-contracts.js";
|
|
5
6
|
export function compileUnitToolContract(unitType) {
|
|
6
7
|
const manifest = resolveManifest(unitType);
|
|
8
|
+
const surfaceContract = getUnitToolSurfaceContract(unitType);
|
|
7
9
|
if (!manifest) {
|
|
8
10
|
return {
|
|
9
11
|
ok: false,
|
|
@@ -12,6 +14,8 @@ export function compileUnitToolContract(unitType) {
|
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
const requiredWorkflowTools = getRequiredWorkflowToolsForAutoUnit(unitType);
|
|
17
|
+
const forbiddenWorkflowTools = Object.entries(surfaceContract?.forbiddenGsdTools ?? {})
|
|
18
|
+
.map(([name, reason]) => ({ name, reason }));
|
|
15
19
|
const closeoutTools = requiredWorkflowTools.filter((tool) => /^gsd_(?:task|slice|milestone|complete|validate|save|summary)/.test(tool));
|
|
16
20
|
if (requiresCloseoutTool(unitType) && closeoutTools.length === 0) {
|
|
17
21
|
return {
|
|
@@ -27,6 +31,7 @@ export function compileUnitToolContract(unitType) {
|
|
|
27
31
|
contextMode: manifest.contextMode,
|
|
28
32
|
toolsPolicy: manifest.tools,
|
|
29
33
|
requiredWorkflowTools,
|
|
34
|
+
forbiddenWorkflowTools,
|
|
30
35
|
promptObligations: [
|
|
31
36
|
`context-mode:${manifest.contextMode}`,
|
|
32
37
|
`tools-policy:${manifest.tools.mode}`,
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Resolve phase-aware tool surfaces for GSD model presentations.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"gsd_uat_result_save",
|
|
6
|
-
"gsd_resume",
|
|
7
|
-
"gsd_milestone_status",
|
|
8
|
-
"gsd_journal_query",
|
|
9
|
-
];
|
|
3
|
+
import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_TOOL_PRESENTATION_PLAN_ID, RUN_UAT_WORKFLOW_TOOL_NAMES, } from "./unit-tool-contracts.js";
|
|
4
|
+
export { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_TOOL_PRESENTATION_PLAN_ID, RUN_UAT_WORKFLOW_TOOL_NAMES, } from "./unit-tool-contracts.js";
|
|
10
5
|
export const RUN_UAT_FORBIDDEN_TOOL_NAMES = [
|
|
11
6
|
"edit",
|
|
12
7
|
"write",
|
|
@@ -74,9 +69,24 @@ function addBlockedTool(blocked, name, reason) {
|
|
|
74
69
|
export function buildRunUatCanonicalToolNames(options = {}) {
|
|
75
70
|
return dedupe([
|
|
76
71
|
...RUN_UAT_WORKFLOW_TOOL_NAMES,
|
|
72
|
+
...RUN_UAT_READ_ONLY_TOOL_NAMES,
|
|
77
73
|
...(options.includeBrowserTools ?? []),
|
|
78
74
|
]);
|
|
79
75
|
}
|
|
76
|
+
export function buildRunUatResultPresentation(options = {}) {
|
|
77
|
+
const presentedTools = options.presentedTools
|
|
78
|
+
? dedupe(options.presentedTools)
|
|
79
|
+
: buildRunUatCanonicalToolNames({ includeBrowserTools: options.includeBrowserTools });
|
|
80
|
+
const blockedTools = RUN_UAT_FORBIDDEN_TOOL_NAMES
|
|
81
|
+
.filter((toolName) => !toolName.includes("*"))
|
|
82
|
+
.map((name) => ({ name, reason: "forbidden during run-uat" }));
|
|
83
|
+
return {
|
|
84
|
+
surface: options.surface ?? "mcp",
|
|
85
|
+
presentedTools,
|
|
86
|
+
blockedTools,
|
|
87
|
+
toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
80
90
|
export function resolveToolPresentationPlan(options) {
|
|
81
91
|
const requested = options.requestedToolNames ?? (options.phase === "run-uat"
|
|
82
92
|
? buildRunUatCanonicalToolNames({ includeBrowserTools: options.includeBrowserTools })
|
|
@@ -26,7 +26,7 @@ import { invalidateStateCache } from "../state.js";
|
|
|
26
26
|
import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
27
27
|
import { parseProject } from "../schemas/parsers.js";
|
|
28
28
|
import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
|
|
29
|
-
import { canonicalWorkflowToolName, parseMcpToolName, RUN_UAT_FORBIDDEN_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES, } from "../tool-presentation-plan.js";
|
|
29
|
+
import { buildRunUatResultPresentation, canonicalWorkflowToolName, parseMcpToolName, RUN_UAT_FORBIDDEN_TOOL_NAMES, RUN_UAT_TOOL_PRESENTATION_PLAN_ID, RUN_UAT_WORKFLOW_TOOL_NAMES, } from "../tool-presentation-plan.js";
|
|
30
30
|
export const SUPPORTED_SUMMARY_ARTIFACT_TYPES = [
|
|
31
31
|
"SUMMARY",
|
|
32
32
|
"RESEARCH",
|
|
@@ -53,7 +53,7 @@ function blockIfWrongAutoUnit(requiredUnitType, operation) {
|
|
|
53
53
|
return null;
|
|
54
54
|
if (snapshot.currentUnit.type === requiredUnitType)
|
|
55
55
|
return null;
|
|
56
|
-
const error = `HARD BLOCK: ${operation} may only run from ${requiredUnitType}; active unit is ${snapshot.currentUnit.type}. The orchestrator owns phase transitions.`;
|
|
56
|
+
const error = `HARD BLOCK: Tool Contract failure: ${operation} may only run from ${requiredUnitType}; active unit is ${snapshot.currentUnit.type}. Fix unit-tool-contracts.ts or the active Unit prompt. The orchestrator owns phase transitions.`;
|
|
57
57
|
return {
|
|
58
58
|
content: [{ type: "text", text: error }],
|
|
59
59
|
details: { operation, error },
|
|
@@ -121,7 +121,11 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
|
|
|
121
121
|
if (rootArtifactGuard.block) {
|
|
122
122
|
return {
|
|
123
123
|
content: [{ type: "text", text: `Error saving artifact: ${rootArtifactGuard.reason ?? "root artifact write blocked"}` }],
|
|
124
|
-
details: {
|
|
124
|
+
details: {
|
|
125
|
+
operation: "save_summary",
|
|
126
|
+
error: "root_artifact_write_blocked",
|
|
127
|
+
displayReason: "Approval confirmation required before saving final project setup artifacts.",
|
|
128
|
+
},
|
|
125
129
|
isError: true,
|
|
126
130
|
};
|
|
127
131
|
}
|
|
@@ -129,7 +133,11 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
|
|
|
129
133
|
if (contextGuard.block) {
|
|
130
134
|
return {
|
|
131
135
|
content: [{ type: "text", text: `Error saving artifact: ${contextGuard.reason ?? "context write blocked"}` }],
|
|
132
|
-
details: {
|
|
136
|
+
details: {
|
|
137
|
+
operation: "save_summary",
|
|
138
|
+
error: "context_write_blocked",
|
|
139
|
+
displayReason: "Depth check required before writing milestone context.",
|
|
140
|
+
},
|
|
133
141
|
isError: true,
|
|
134
142
|
};
|
|
135
143
|
}
|
|
@@ -808,6 +816,52 @@ function errorResult(operation, message, error) {
|
|
|
808
816
|
function isNonEmptyString(value) {
|
|
809
817
|
return typeof value === "string" && value.trim().length > 0;
|
|
810
818
|
}
|
|
819
|
+
function mergeBlockedTools(current, canonical) {
|
|
820
|
+
const merged = new Map();
|
|
821
|
+
for (const entry of [...(current ?? []), ...canonical]) {
|
|
822
|
+
merged.set(canonicalWorkflowToolName(parseMcpToolName(entry.name)?.tool ?? entry.name), entry);
|
|
823
|
+
}
|
|
824
|
+
return [...merged.values()];
|
|
825
|
+
}
|
|
826
|
+
function mergePresentedTools(current, canonical) {
|
|
827
|
+
return [...new Set([...(current ?? []), ...canonical])];
|
|
828
|
+
}
|
|
829
|
+
function normalizeUatVerdict(params) {
|
|
830
|
+
const raw = params;
|
|
831
|
+
if (typeof raw.verdict === "string") {
|
|
832
|
+
return { ...params, verdict: raw.verdict.toUpperCase() };
|
|
833
|
+
}
|
|
834
|
+
return params;
|
|
835
|
+
}
|
|
836
|
+
function supplyDefaultPresentation(params) {
|
|
837
|
+
const raw = params;
|
|
838
|
+
if (!raw.presentation) {
|
|
839
|
+
return { ...params, presentation: buildRunUatResultPresentation() };
|
|
840
|
+
}
|
|
841
|
+
return params;
|
|
842
|
+
}
|
|
843
|
+
function mergeCanonicalPresentation(params) {
|
|
844
|
+
const canonicalPresentation = buildRunUatResultPresentation();
|
|
845
|
+
const providedPresentation = params.presentation;
|
|
846
|
+
return {
|
|
847
|
+
...params,
|
|
848
|
+
presentation: {
|
|
849
|
+
...providedPresentation,
|
|
850
|
+
surface: providedPresentation.surface ?? canonicalPresentation.surface,
|
|
851
|
+
presentedTools: mergePresentedTools(providedPresentation.presentedTools, canonicalPresentation.presentedTools),
|
|
852
|
+
blockedTools: mergeBlockedTools(providedPresentation.blockedTools, canonicalPresentation.blockedTools),
|
|
853
|
+
toolPresentationPlanId: RUN_UAT_TOOL_PRESENTATION_PLAN_ID,
|
|
854
|
+
},
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
const VALID_UAT_TYPES = [
|
|
858
|
+
"artifact-driven",
|
|
859
|
+
"browser-executable",
|
|
860
|
+
"runtime-executable",
|
|
861
|
+
"live-runtime",
|
|
862
|
+
"mixed",
|
|
863
|
+
"human-experience",
|
|
864
|
+
];
|
|
811
865
|
function ensureUatRequiredFields(params) {
|
|
812
866
|
if (!isNonEmptyString(params.milestoneId))
|
|
813
867
|
return "milestoneId is required";
|
|
@@ -815,6 +869,9 @@ function ensureUatRequiredFields(params) {
|
|
|
815
869
|
return "sliceId is required";
|
|
816
870
|
if (!isNonEmptyString(params.uatType))
|
|
817
871
|
return "uatType is required";
|
|
872
|
+
if (!VALID_UAT_TYPES.includes(params.uatType)) {
|
|
873
|
+
return `uatType must be one of: ${VALID_UAT_TYPES.join(", ")}`;
|
|
874
|
+
}
|
|
818
875
|
if (!["PASS", "FAIL", "PARTIAL"].includes(params.verdict))
|
|
819
876
|
return "verdict must be PASS, FAIL, or PARTIAL";
|
|
820
877
|
if (!Array.isArray(params.checks) || params.checks.length === 0)
|
|
@@ -951,6 +1008,12 @@ function validateUatChecks(basePath, params) {
|
|
|
951
1008
|
}
|
|
952
1009
|
return null;
|
|
953
1010
|
}
|
|
1011
|
+
function validateFreshUatOwnedEvidence(params) {
|
|
1012
|
+
const hasFreshUatEvidence = params.checks.some((check) => (check.evidence ?? []).some((evidence) => evidence.kind === "gsd_uat_exec"));
|
|
1013
|
+
return hasFreshUatEvidence
|
|
1014
|
+
? null
|
|
1015
|
+
: "UAT Assessment requires at least one fresh gsd_uat_exec evidence reference from run-uat";
|
|
1016
|
+
}
|
|
954
1017
|
function validateUatMode(params) {
|
|
955
1018
|
const modes = new Set(params.checks.map((check) => check.mode));
|
|
956
1019
|
const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
|
|
@@ -1087,18 +1150,32 @@ async function saveUatAttemptArtifact(basePath, params, attempt) {
|
|
|
1087
1150
|
return relativePath;
|
|
1088
1151
|
}
|
|
1089
1152
|
export async function executeUatResultSave(params, basePath = process.cwd()) {
|
|
1153
|
+
const unitGuard = blockIfWrongAutoUnit("run-uat", "save_uat_result");
|
|
1154
|
+
if (unitGuard)
|
|
1155
|
+
return unitGuard;
|
|
1156
|
+
// Phase 1: normalize verdict and supply the canonical presentation when none was provided.
|
|
1157
|
+
params = normalizeUatVerdict(params);
|
|
1158
|
+
params = supplyDefaultPresentation(params);
|
|
1090
1159
|
const dbAvailable = await ensureDbOpen(basePath);
|
|
1091
1160
|
if (!dbAvailable)
|
|
1092
1161
|
return errorResult("save_uat_result", "GSD database is not available.", "db_unavailable");
|
|
1162
|
+
// Phase 2: validate the submitted presentation before the canonical merge so that
|
|
1163
|
+
// presentations missing required workflow tools are rejected rather than silently patched.
|
|
1093
1164
|
const requiredError = ensureUatRequiredFields(params);
|
|
1094
1165
|
if (requiredError)
|
|
1095
1166
|
return errorResult("save_uat_result", requiredError, "invalid_params");
|
|
1096
1167
|
const presentationError = validateCanonicalPresentation(params);
|
|
1097
1168
|
if (presentationError)
|
|
1098
1169
|
return errorResult("save_uat_result", presentationError, "alias_tool_name");
|
|
1170
|
+
// Phase 3: merge in the canonical plan ID and read-only audit tools so the persisted
|
|
1171
|
+
// artifact always carries the full audit surface even when the provider omitted them.
|
|
1172
|
+
params = mergeCanonicalPresentation(params);
|
|
1099
1173
|
const checkError = validateUatChecks(basePath, params);
|
|
1100
1174
|
if (checkError)
|
|
1101
1175
|
return errorResult("save_uat_result", checkError, "invalid_evidence");
|
|
1176
|
+
const freshEvidenceError = validateFreshUatOwnedEvidence(params);
|
|
1177
|
+
if (freshEvidenceError)
|
|
1178
|
+
return errorResult("save_uat_result", freshEvidenceError, "missing_fresh_uat_evidence");
|
|
1102
1179
|
const modeError = validateUatMode(params);
|
|
1103
1180
|
if (modeError)
|
|
1104
1181
|
return errorResult("save_uat_result", modeError, "uat_mode_mismatch");
|