gsd-pi 2.79.0-dev.5c910bb05 → 2.79.0-dev.ece5fd8ba
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/phases.js +6 -1
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -8
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
- package/dist/resources/extensions/gsd/guided-flow.js +40 -0
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +45 -4
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- 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 +15 -15
- 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 +15 -15
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +13 -2
- package/src/resources/extensions/gsd/auto/phases.ts +6 -1
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +11 -8
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
- package/src/resources/extensions/gsd/guided-flow.ts +47 -0
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +36 -7
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +47 -4
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → TzEVJ1Lh8vbez4n4Q9TqQ}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → TzEVJ1Lh8vbez4n4Q9TqQ}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
76e1a0d39c0a27d8
|
|
@@ -1089,7 +1089,12 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1089
1089
|
s.lastGitActionFailure = null;
|
|
1090
1090
|
s.lastGitActionStatus = null;
|
|
1091
1091
|
s.lastUnitAgentEndMessages = null;
|
|
1092
|
-
setCurrentPhase(unitType
|
|
1092
|
+
setCurrentPhase(unitType, {
|
|
1093
|
+
basePath: s.basePath,
|
|
1094
|
+
traceId: ic.flowId,
|
|
1095
|
+
turnId: `iter-${ic.iteration}`,
|
|
1096
|
+
causedBy: "unit-start",
|
|
1097
|
+
});
|
|
1093
1098
|
s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
|
|
1094
1099
|
const unitStartSeq = ic.nextSeq();
|
|
1095
1100
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
|
|
@@ -128,9 +128,9 @@ export function diagnoseExpectedArtifact(unitType, unitId, base) {
|
|
|
128
128
|
}
|
|
129
129
|
return `${relSliceFile(base, mid, sid, "RESEARCH")} (slice research)`;
|
|
130
130
|
case "plan-slice":
|
|
131
|
-
return `${relSliceFile(base, mid, sid, "PLAN")} (slice plan)`;
|
|
131
|
+
return `${relSliceFile(base, mid, sid, "PLAN")} plus tasks/T##-PLAN.md files (slice plan and task plans)`;
|
|
132
132
|
case "refine-slice":
|
|
133
|
-
return `${relSliceFile(base, mid, sid, "PLAN")} (refined slice plan
|
|
133
|
+
return `${relSliceFile(base, mid, sid, "PLAN")} plus tasks/T##-PLAN.md files (refined slice plan and task plans)`;
|
|
134
134
|
case "execute-task": {
|
|
135
135
|
return `Task ${tid} marked [x] in ${relSliceFile(base, mid, sid, "PLAN")} + summary written`;
|
|
136
136
|
}
|
|
@@ -559,6 +559,8 @@ export const DISPATCH_RULES = [
|
|
|
559
559
|
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
560
560
|
if (hasContext)
|
|
561
561
|
return null; // fall through to next rule
|
|
562
|
+
if (prefs?.planning_depth === "deep")
|
|
563
|
+
return null;
|
|
562
564
|
// H6 fix (#4973): keep the non-deep auto-mode bypass, but do not
|
|
563
565
|
// pre-verify deep planning's user-facing milestone approval gate.
|
|
564
566
|
if (shouldBypassMilestoneDepthGateInAuto(prefs)) {
|
|
@@ -257,7 +257,7 @@ function scanGsdTaggedCommits(basePath, milestoneId, gitArgs) {
|
|
|
257
257
|
if (!commitMessageHasGsdTrailer(message))
|
|
258
258
|
continue;
|
|
259
259
|
const commitFiles = getChangedFilesForCommit(basePath, hash);
|
|
260
|
-
if (!commitMatchesMilestone(message, milestoneId, commitFiles))
|
|
260
|
+
if (!commitMatchesMilestone(basePath, message, milestoneId, commitFiles))
|
|
261
261
|
continue;
|
|
262
262
|
matched = true;
|
|
263
263
|
for (const file of commitFiles) {
|
|
@@ -278,22 +278,37 @@ function getChangedFilesForCommit(basePath, hash) {
|
|
|
278
278
|
function commitMessageHasGsdTrailer(message) {
|
|
279
279
|
return /^GSD-(?:Task|Unit):\s*\S+/m.test(message);
|
|
280
280
|
}
|
|
281
|
-
function commitMatchesMilestone(message, milestoneId, files) {
|
|
281
|
+
function commitMatchesMilestone(basePath, message, milestoneId, files) {
|
|
282
282
|
if (commitTrailerStartsWithMilestone(message, milestoneId))
|
|
283
283
|
return true;
|
|
284
284
|
// Meaningful execute-task commits currently store task scope as Sxx/Tyy
|
|
285
285
|
// rather than Mxx/Sxx/Tyy. Bind those commits back to the milestone when
|
|
286
286
|
// either the commit touched this milestone's artifacts, or — for projects
|
|
287
287
|
// where .gsd/ is gitignored/external (#5033) — the message explicitly
|
|
288
|
-
// names the milestone.
|
|
288
|
+
// names the milestone or local GSD state proves the task belongs here.
|
|
289
289
|
if (/^GSD-Task:\s*S[^/\s]+\/T\S+/m.test(message)) {
|
|
290
290
|
if (files.some((file) => isMilestoneArtifactPath(file, milestoneId)))
|
|
291
291
|
return true;
|
|
292
292
|
if (commitMessageMentionsMilestone(message, milestoneId))
|
|
293
293
|
return true;
|
|
294
|
+
if (commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId))
|
|
295
|
+
return true;
|
|
294
296
|
}
|
|
295
297
|
return false;
|
|
296
298
|
}
|
|
299
|
+
function commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId) {
|
|
300
|
+
const match = message.match(/^GSD-Task:\s*(S[^/\s]+)\/(T[^\s]+)/m);
|
|
301
|
+
if (!match)
|
|
302
|
+
return false;
|
|
303
|
+
const [, sliceId, taskId] = match;
|
|
304
|
+
if (getTask(milestoneId, sliceId, taskId))
|
|
305
|
+
return true;
|
|
306
|
+
const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
|
|
307
|
+
if (!tasksDir)
|
|
308
|
+
return false;
|
|
309
|
+
return existsSync(join(tasksDir, `${taskId}-PLAN.md`))
|
|
310
|
+
|| existsSync(join(tasksDir, `${taskId}-SUMMARY.md`));
|
|
311
|
+
}
|
|
297
312
|
function commitMessageMentionsMilestone(message, milestoneId) {
|
|
298
313
|
if (!MILESTONE_ID_RE.test(milestoneId))
|
|
299
314
|
return false;
|
|
@@ -486,8 +486,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
486
486
|
// Clear survivor flag — finalization is done
|
|
487
487
|
hasSurvivorBranch = false;
|
|
488
488
|
}
|
|
489
|
+
const effectivePrefs = loadEffectiveGSDPreferences(base)?.preferences;
|
|
489
490
|
const deepProjectStagePending = !hasSurvivorBranch
|
|
490
|
-
? (await import("./auto-dispatch.js")).hasPendingDeepStage(
|
|
491
|
+
? (await import("./auto-dispatch.js")).hasPendingDeepStage(effectivePrefs, base)
|
|
491
492
|
: false;
|
|
492
493
|
if (deepProjectStagePending) {
|
|
493
494
|
// Deep project-level setup runs before the first milestone exists. Let
|
|
@@ -526,7 +527,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
526
527
|
const mid = state.activeMilestone.id;
|
|
527
528
|
const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
528
529
|
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
529
|
-
if (!hasContext) {
|
|
530
|
+
if (!hasContext && effectivePrefs?.planning_depth !== "deep") {
|
|
530
531
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
531
532
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
532
533
|
// showSmartEntry dispatches via pi.sendMessage() which is fire-and-forget:
|
|
@@ -166,7 +166,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
166
166
|
const { getEcosystemReadyPromise } = await import("../ecosystem/loader.js");
|
|
167
167
|
await getEcosystemReadyPromise();
|
|
168
168
|
const beforeAgentBasePath = process.cwd();
|
|
169
|
-
const pendingApprovalGate = getPendingGate();
|
|
169
|
+
const pendingApprovalGate = getPendingGate(beforeAgentBasePath);
|
|
170
170
|
if (pendingApprovalGate && isExplicitApprovalResponse(event.prompt, pendingApprovalGate)) {
|
|
171
171
|
markApprovalGateVerified(pendingApprovalGate, beforeAgentBasePath);
|
|
172
172
|
const milestoneId = extractDepthVerificationMilestoneId(pendingApprovalGate);
|
|
@@ -363,15 +363,15 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
363
363
|
// ── Discussion gate enforcement: block tool calls while gate is pending ──
|
|
364
364
|
// If ask_user_questions was called with a gate ID but hasn't been confirmed,
|
|
365
365
|
// block all non-read-only tool calls to prevent the model from skipping gates.
|
|
366
|
-
if (getPendingGate()) {
|
|
366
|
+
if (getPendingGate(discussionBasePath)) {
|
|
367
367
|
const milestoneId = await getDiscussionMilestoneIdFor(discussionBasePath);
|
|
368
368
|
if (isToolCallEventType("bash", event)) {
|
|
369
|
-
const bashGuard = shouldBlockPendingGateBash(event.input.command, milestoneId, isQueuePhaseActive());
|
|
369
|
+
const bashGuard = shouldBlockPendingGateBash(event.input.command, milestoneId, isQueuePhaseActive(discussionBasePath), discussionBasePath);
|
|
370
370
|
if (bashGuard.block)
|
|
371
371
|
return bashGuard;
|
|
372
372
|
}
|
|
373
373
|
else {
|
|
374
|
-
const gateGuard = shouldBlockPendingGate(toolName, milestoneId, isQueuePhaseActive());
|
|
374
|
+
const gateGuard = shouldBlockPendingGate(toolName, milestoneId, isQueuePhaseActive(discussionBasePath), discussionBasePath);
|
|
375
375
|
if (gateGuard.block)
|
|
376
376
|
return gateGuard;
|
|
377
377
|
}
|
|
@@ -380,7 +380,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
380
380
|
// When /gsd queue is active, the agent should only create milestones,
|
|
381
381
|
// not execute work. Block write/edit to non-.gsd/ paths and bash commands
|
|
382
382
|
// that would modify files.
|
|
383
|
-
if (isQueuePhaseActive()) {
|
|
383
|
+
if (isQueuePhaseActive(discussionBasePath)) {
|
|
384
384
|
let queueInput = "";
|
|
385
385
|
if (isToolCallEventType("write", event)) {
|
|
386
386
|
queueInput = event.input.path;
|
|
@@ -445,7 +445,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
445
445
|
}
|
|
446
446
|
if (!isToolCallEventType("write", event))
|
|
447
447
|
return;
|
|
448
|
-
const result = shouldBlockContextWrite(event.toolName, event.input.path, await getDiscussionMilestoneIdFor(discussionBasePath), isQueuePhaseActive());
|
|
448
|
+
const result = shouldBlockContextWrite(event.toolName, event.input.path, await getDiscussionMilestoneIdFor(discussionBasePath), isQueuePhaseActive(discussionBasePath), discussionBasePath);
|
|
449
449
|
if (result.block)
|
|
450
450
|
return result;
|
|
451
451
|
});
|
|
@@ -500,7 +500,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
500
500
|
return;
|
|
501
501
|
const basePath = process.cwd();
|
|
502
502
|
const milestoneId = await getDiscussionMilestoneIdFor(basePath);
|
|
503
|
-
const queueActive = isQueuePhaseActive();
|
|
503
|
+
const queueActive = isQueuePhaseActive(basePath);
|
|
504
504
|
const details = event.details;
|
|
505
505
|
// ── Discussion gate enforcement: handle gate question responses ──
|
|
506
506
|
// If the result is cancelled or has no response, the pending gate stays active
|
|
@@ -508,7 +508,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
508
508
|
// If the user responded at all (even "needs adjustment"), clear the pending gate
|
|
509
509
|
// because the user engaged — the prompt handles the re-ask-after-adjustment flow.
|
|
510
510
|
const questions = event.input?.questions ?? [];
|
|
511
|
-
const currentPendingGate = getPendingGate();
|
|
511
|
+
const currentPendingGate = getPendingGate(basePath);
|
|
512
512
|
if (currentPendingGate) {
|
|
513
513
|
if (details?.cancelled || !details?.response) {
|
|
514
514
|
// Gate stays pending. Direct the agent to the most reliable recovery
|
|
@@ -298,8 +298,8 @@ export function getPendingGate(basePath = process.cwd()) {
|
|
|
298
298
|
* Returns { block: true, reason } if the tool should be blocked.
|
|
299
299
|
* ask_user_questions itself is allowed so the model can re-ask the gate.
|
|
300
300
|
*/
|
|
301
|
-
export function shouldBlockPendingGate(toolName, milestoneId, queuePhaseActive) {
|
|
302
|
-
return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(), toolName, milestoneId, queuePhaseActive);
|
|
301
|
+
export function shouldBlockPendingGate(toolName, milestoneId, queuePhaseActive, basePath = process.cwd()) {
|
|
302
|
+
return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(basePath), toolName, milestoneId, queuePhaseActive);
|
|
303
303
|
}
|
|
304
304
|
export function shouldBlockPendingGateInSnapshot(snapshot, toolName, _milestoneId, _queuePhaseActive) {
|
|
305
305
|
if (!snapshot.pendingGateId)
|
|
@@ -322,8 +322,8 @@ export function shouldBlockPendingGateInSnapshot(snapshot, toolName, _milestoneI
|
|
|
322
322
|
* Check whether a bash command should be blocked because a discussion gate is pending.
|
|
323
323
|
* All bash is blocked while waiting for confirmation so the question stays visible.
|
|
324
324
|
*/
|
|
325
|
-
export function shouldBlockPendingGateBash(command, milestoneId, queuePhaseActive) {
|
|
326
|
-
return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(), command, milestoneId, queuePhaseActive);
|
|
325
|
+
export function shouldBlockPendingGateBash(command, milestoneId, queuePhaseActive, basePath = process.cwd()) {
|
|
326
|
+
return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(basePath), command, milestoneId, queuePhaseActive);
|
|
327
327
|
}
|
|
328
328
|
export function shouldBlockPendingGateBashInSnapshot(snapshot, command, _milestoneId, _queuePhaseActive) {
|
|
329
329
|
if (!snapshot.pendingGateId)
|
|
@@ -363,7 +363,7 @@ export function isDepthConfirmationAnswer(selected, options) {
|
|
|
363
363
|
// Returning false prevents any free-form string from unlocking the gate.
|
|
364
364
|
return false;
|
|
365
365
|
}
|
|
366
|
-
export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queuePhaseActive) {
|
|
366
|
+
export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queuePhaseActive, basePath = process.cwd()) {
|
|
367
367
|
if (toolName !== "write")
|
|
368
368
|
return { block: false };
|
|
369
369
|
if (!MILESTONE_CONTEXT_RE.test(inputPath))
|
|
@@ -379,7 +379,7 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queue
|
|
|
379
379
|
].join(" "),
|
|
380
380
|
};
|
|
381
381
|
}
|
|
382
|
-
if (isMilestoneDepthVerified(targetMilestoneId))
|
|
382
|
+
if (isMilestoneDepthVerified(targetMilestoneId, basePath))
|
|
383
383
|
return { block: false };
|
|
384
384
|
return {
|
|
385
385
|
block: true,
|
|
@@ -397,8 +397,8 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queue
|
|
|
397
397
|
* Slice-level CONTEXT artifacts are allowed; milestone-level CONTEXT writes
|
|
398
398
|
* require the milestone to be depth-verified first.
|
|
399
399
|
*/
|
|
400
|
-
export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceId) {
|
|
401
|
-
return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(), artifactType, milestoneId, sliceId);
|
|
400
|
+
export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceId, basePath = process.cwd()) {
|
|
401
|
+
return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(basePath), artifactType, milestoneId, sliceId);
|
|
402
402
|
}
|
|
403
403
|
export function shouldBlockContextArtifactSaveInSnapshot(snapshot, artifactType, milestoneId, sliceId) {
|
|
404
404
|
if (artifactType !== "CONTEXT")
|
|
@@ -44,6 +44,7 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForGuidedUnit
|
|
|
44
44
|
import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, } from "./preparation.js";
|
|
45
45
|
import { verifyExpectedArtifact } from "./auto-recovery.js";
|
|
46
46
|
import { createWorkspace, scopeMilestone } from "./workspace.js";
|
|
47
|
+
import { getPendingGate, extractDepthVerificationMilestoneId } from "./bootstrap/write-gate.js";
|
|
47
48
|
// ─── Re-exports (preserve public API for existing importers) ────────────────
|
|
48
49
|
export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, reserveMilestoneId, claimReservedId, getReservedMilestoneIds, clearReservedMilestoneIds, } from "./milestone-ids.js";
|
|
49
50
|
export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
|
|
@@ -295,6 +296,14 @@ export async function checkDeepProjectSetupAfterTurn(_event, ctx, basePath) {
|
|
|
295
296
|
return false;
|
|
296
297
|
}
|
|
297
298
|
}
|
|
299
|
+
// R2: a depth-verification gate is still pending — the LLM emitted the
|
|
300
|
+
// confirmation question (via ask_user_questions or plain chat) but the user
|
|
301
|
+
// has not approved yet. Returning false keeps the entry in the
|
|
302
|
+
// pendingDeepProjectSetupMap so the next user message can resume.
|
|
303
|
+
const pendingGateId = getPendingGate(entry.basePath);
|
|
304
|
+
if (pendingGateId) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
298
307
|
return dispatchNextDeepProjectSetupStage(entry);
|
|
299
308
|
}
|
|
300
309
|
async function dispatchNextDeepProjectSetupStage(entry) {
|
|
@@ -368,6 +377,23 @@ export function checkAutoStartAfterDiscuss() {
|
|
|
368
377
|
const roadmapFile = existsSync(roadmapFilePath) ? roadmapFilePath : null;
|
|
369
378
|
if (!contextFile && !roadmapFile)
|
|
370
379
|
return false; // neither artifact yet — keep waiting
|
|
380
|
+
// Gate 1a: a depth-verification gate is still pending for THIS milestone — the
|
|
381
|
+
// LLM emitted the confirmation question (via ask_user_questions or plain chat)
|
|
382
|
+
// but the user has not answered yet. Advancing now would skip the gate and
|
|
383
|
+
// race ahead with unverified context.
|
|
384
|
+
const basePathForGate = entry.scope.workspace.projectRoot;
|
|
385
|
+
const pendingGateId = getPendingGate(basePathForGate);
|
|
386
|
+
if (pendingGateId) {
|
|
387
|
+
const pendingMilestoneId = extractDepthVerificationMilestoneId(pendingGateId);
|
|
388
|
+
// Block advancement if the gate is for THIS milestone, OR if it's a
|
|
389
|
+
// project/requirements gate (no milestone id encoded) for the deep setup flow.
|
|
390
|
+
const isProjectGate = pendingGateId === "depth_verification_project_confirm" ||
|
|
391
|
+
pendingGateId === "depth_verification_requirements_confirm" ||
|
|
392
|
+
pendingGateId === "depth_verification_research_decision_confirm";
|
|
393
|
+
if (pendingMilestoneId === milestoneId || isProjectGate) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
371
397
|
// Gate 1b: Discriminate plan-blocked from discuss-incomplete when the DB row is queued.
|
|
372
398
|
// If the DB is available and the row is still "queued" but CONTEXT.md already exists on
|
|
373
399
|
// disk, the discuss phase completed but gsd_plan_milestone was hard-blocked by the
|
|
@@ -489,6 +515,20 @@ export function checkAutoStartAfterDiscuss() {
|
|
|
489
515
|
logWarning("guided", `manifest unlink failed: ${e.message}`);
|
|
490
516
|
}
|
|
491
517
|
}
|
|
518
|
+
// R3b: belt-and-suspenders for silent registration failure. The discuss flow
|
|
519
|
+
// finished and STATE.md exists, but the milestone may never have landed in
|
|
520
|
+
// the DB. Without this guard, the user sees "Milestone M001 ready." and then
|
|
521
|
+
// /gsd reports "No Active Milestone".
|
|
522
|
+
if (isDbAvailable()) {
|
|
523
|
+
const milestoneRow = getMilestone(milestoneId);
|
|
524
|
+
if (!milestoneRow) {
|
|
525
|
+
ctx.ui.notify(`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
|
|
526
|
+
`PROJECT.md may have failed to register milestones. ` +
|
|
527
|
+
`Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
|
|
528
|
+
`then re-run /gsd to recover.`, "error");
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
492
532
|
pendingAutoStartMap.delete(basePath);
|
|
493
533
|
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
|
|
494
534
|
startAutoDetached(ctx, pi, basePath, false, { step });
|
|
@@ -177,8 +177,12 @@ export function resolveDir(parentDir, idPrefix) {
|
|
|
177
177
|
const exact = entries.find(e => e.isDirectory() && e.name === idPrefix);
|
|
178
178
|
if (exact)
|
|
179
179
|
return exact.name;
|
|
180
|
+
const idLower = idPrefix.toLowerCase();
|
|
181
|
+
const exactCaseInsensitive = entries.find(e => e.isDirectory() && e.name.toLowerCase() === idLower);
|
|
182
|
+
if (exactCaseInsensitive)
|
|
183
|
+
return exactCaseInsensitive.name;
|
|
180
184
|
// Prefix match for legacy descriptor dirs: M001-SOMETHING
|
|
181
|
-
const prefixed = entries.find(e => e.isDirectory() && e.name.startsWith(
|
|
185
|
+
const prefixed = entries.find(e => e.isDirectory() && e.name.toLowerCase().startsWith(idLower + "-"));
|
|
182
186
|
return prefixed ? prefixed.name : null;
|
|
183
187
|
}
|
|
184
188
|
catch {
|
|
@@ -141,7 +141,6 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
|
|
|
141
141
|
task_id: isRootArtifact ? undefined : params.task_id,
|
|
142
142
|
}, basePath);
|
|
143
143
|
let registeredMilestones = [];
|
|
144
|
-
let registrationWarning;
|
|
145
144
|
if (params.artifact_type === "PROJECT") {
|
|
146
145
|
try {
|
|
147
146
|
registeredMilestones = registerProjectMilestoneSequence(contentToSave);
|
|
@@ -150,12 +149,55 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
|
|
|
150
149
|
}
|
|
151
150
|
catch (err) {
|
|
152
151
|
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
-
|
|
154
|
-
logWarning("tool", registrationWarning, {
|
|
152
|
+
logError("tool", `gsd_summary_save: PROJECT artifact persisted but milestone registration threw: ${msg}`, {
|
|
155
153
|
tool: "gsd_summary_save",
|
|
156
154
|
error: String(err),
|
|
157
155
|
stack: err instanceof Error ? err.stack ?? "" : "",
|
|
158
156
|
});
|
|
157
|
+
// PROJECT.md was persisted by saveArtifactToDb above; the artifacts row
|
|
158
|
+
// changed even though no milestones registered. Invalidate so subsequent
|
|
159
|
+
// /gsd reads see the persisted artifact instead of the pre-save cache.
|
|
160
|
+
invalidateStateCache();
|
|
161
|
+
return {
|
|
162
|
+
content: [{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: `Error: PROJECT.md was saved to ${relativePath} but milestone registration failed: ${msg}. ` +
|
|
165
|
+
`The DB has no milestone rows for this project, so /gsd will report "No Active Milestone". ` +
|
|
166
|
+
`Re-call gsd_summary_save(PROJECT) once the underlying error is resolved — INSERT OR IGNORE makes registration idempotent.`,
|
|
167
|
+
}],
|
|
168
|
+
details: {
|
|
169
|
+
operation: "save_summary",
|
|
170
|
+
path: relativePath,
|
|
171
|
+
artifact_type: params.artifact_type,
|
|
172
|
+
error: "milestone_registration_threw",
|
|
173
|
+
registration_error: msg,
|
|
174
|
+
},
|
|
175
|
+
isError: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (registeredMilestones.length === 0) {
|
|
179
|
+
logError("tool", `gsd_summary_save: PROJECT.md saved to ${relativePath} but parsed zero milestones — registration produced no DB rows`, {
|
|
180
|
+
tool: "gsd_summary_save",
|
|
181
|
+
});
|
|
182
|
+
// PROJECT.md was persisted; invalidate so subsequent reads see the new
|
|
183
|
+
// artifacts row even though no milestones registered.
|
|
184
|
+
invalidateStateCache();
|
|
185
|
+
return {
|
|
186
|
+
content: [{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: `Error: PROJECT.md was saved to ${relativePath} but contains zero parseable milestone lines, ` +
|
|
189
|
+
`so no milestones were registered in the DB. /gsd will report "No Active Milestone". ` +
|
|
190
|
+
`Rewrite PROJECT.md so the "Milestone Sequence" section uses canonical lines: ` +
|
|
191
|
+
`\`- [ ] M001: <Title> — <One-liner>\` (em-dash, double-dash \`--\`, or single-dash \`-\` separator), then re-call gsd_summary_save(PROJECT).`,
|
|
192
|
+
}],
|
|
193
|
+
details: {
|
|
194
|
+
operation: "save_summary",
|
|
195
|
+
path: relativePath,
|
|
196
|
+
artifact_type: params.artifact_type,
|
|
197
|
+
error: "milestone_registration_empty_parse",
|
|
198
|
+
},
|
|
199
|
+
isError: true,
|
|
200
|
+
};
|
|
159
201
|
}
|
|
160
202
|
}
|
|
161
203
|
if (params.artifact_type === "CONTEXT" && !params.task_id) {
|
|
@@ -178,7 +220,6 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
|
|
|
178
220
|
artifact_type: params.artifact_type,
|
|
179
221
|
content_source: contentSource,
|
|
180
222
|
...(registeredMilestones.length > 0 ? { registeredMilestones } : {}),
|
|
181
|
-
...(registrationWarning ? { warning: registrationWarning } : {}),
|
|
182
223
|
},
|
|
183
224
|
};
|
|
184
225
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD2 UOK Audit Events and DB-First Projection Writes
|
|
1
2
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -5,6 +6,7 @@ import { isStaleWrite } from "../auto/turn-epoch.js";
|
|
|
5
6
|
import { withFileLockSync } from "../file-lock.js";
|
|
6
7
|
import { gsdRoot } from "../paths.js";
|
|
7
8
|
import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
|
|
9
|
+
import { CURRENT_UOK_CONTRACT_VERSION, validateAuditEvent } from "./contracts.js";
|
|
8
10
|
function auditLogPath(basePath) {
|
|
9
11
|
return join(gsdRoot(basePath), "audit", "events.jsonl");
|
|
10
12
|
}
|
|
@@ -13,6 +15,7 @@ function ensureAuditDir(basePath) {
|
|
|
13
15
|
}
|
|
14
16
|
export function buildAuditEnvelope(args) {
|
|
15
17
|
return {
|
|
18
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
16
19
|
eventId: randomUUID(),
|
|
17
20
|
traceId: args.traceId,
|
|
18
21
|
turnId: args.turnId,
|
|
@@ -27,6 +30,25 @@ export function emitUokAuditEvent(basePath, event) {
|
|
|
27
30
|
// Drop writes from a turn superseded by timeout recovery / cancellation.
|
|
28
31
|
if (isStaleWrite("uok-audit"))
|
|
29
32
|
return;
|
|
33
|
+
const validation = validateAuditEvent(event);
|
|
34
|
+
if (!validation.ok) {
|
|
35
|
+
throw new Error(`Invalid UOK audit event: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
36
|
+
}
|
|
37
|
+
const canonical = validation.value;
|
|
38
|
+
if (isDbAvailable()) {
|
|
39
|
+
try {
|
|
40
|
+
insertAuditEvent({
|
|
41
|
+
...canonical,
|
|
42
|
+
payload: {
|
|
43
|
+
...canonical.payload,
|
|
44
|
+
contractVersion: canonical.version ?? CURRENT_UOK_CONTRACT_VERSION,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw new Error(`DB authoritative audit write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
30
52
|
try {
|
|
31
53
|
ensureAuditDir(basePath);
|
|
32
54
|
const path = auditLogPath(basePath);
|
|
@@ -39,18 +61,10 @@ export function emitUokAuditEvent(basePath, event) {
|
|
|
39
61
|
// POSIX O_APPEND atomicity still protects small line writes, so skipping
|
|
40
62
|
// the lock rather than stalling orchestration is the correct tradeoff.
|
|
41
63
|
withFileLockSync(path, () => {
|
|
42
|
-
appendFileSync(path, `${JSON.stringify(
|
|
64
|
+
appendFileSync(path, `${JSON.stringify(canonical)}\n`, "utf-8");
|
|
43
65
|
}, { onLocked: "skip" });
|
|
44
66
|
}
|
|
45
67
|
catch {
|
|
46
68
|
// Best-effort: audit writes must never break orchestration.
|
|
47
69
|
}
|
|
48
|
-
if (!isDbAvailable())
|
|
49
|
-
return;
|
|
50
|
-
try {
|
|
51
|
-
insertAuditEvent(event);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Projection failures are non-fatal while legacy readers are still active.
|
|
55
|
-
}
|
|
56
70
|
}
|
|
@@ -1 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
// GSD2 UOK Contract Types and Versioning
|
|
2
|
+
export const CURRENT_UOK_CONTRACT_VERSION = "1";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function normalizeVersion(value) {
|
|
7
|
+
return value === CURRENT_UOK_CONTRACT_VERSION ? CURRENT_UOK_CONTRACT_VERSION : "0";
|
|
8
|
+
}
|
|
9
|
+
function requireString(value, key, issues) {
|
|
10
|
+
if (typeof value[key] !== "string" || value[key] === "") {
|
|
11
|
+
issues.push({ path: key, message: `${key} must be a non-empty string` });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function requireRecord(value, key, issues) {
|
|
15
|
+
if (!isRecord(value[key])) {
|
|
16
|
+
issues.push({ path: key, message: `${key} must be an object` });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function normalizeTurnResult(value) {
|
|
20
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
21
|
+
}
|
|
22
|
+
export function normalizeDispatchEnvelope(value) {
|
|
23
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
24
|
+
}
|
|
25
|
+
export function normalizeAuditEvent(value) {
|
|
26
|
+
return { ...value, version: normalizeVersion(value.version) };
|
|
27
|
+
}
|
|
28
|
+
export function validateTurnResult(value) {
|
|
29
|
+
const normalized = normalizeTurnResult(value);
|
|
30
|
+
const record = normalized;
|
|
31
|
+
const issues = [];
|
|
32
|
+
requireString(record, "traceId", issues);
|
|
33
|
+
requireString(record, "turnId", issues);
|
|
34
|
+
if (!Number.isInteger(record.iteration)) {
|
|
35
|
+
issues.push({ path: "iteration", message: "iteration must be an integer" });
|
|
36
|
+
}
|
|
37
|
+
requireString(record, "status", issues);
|
|
38
|
+
requireString(record, "failureClass", issues);
|
|
39
|
+
if (!Array.isArray(record.phaseResults)) {
|
|
40
|
+
issues.push({ path: "phaseResults", message: "phaseResults must be an array" });
|
|
41
|
+
}
|
|
42
|
+
requireString(record, "startedAt", issues);
|
|
43
|
+
requireString(record, "finishedAt", issues);
|
|
44
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
45
|
+
}
|
|
46
|
+
export function validateDispatchEnvelope(value) {
|
|
47
|
+
const normalized = normalizeDispatchEnvelope(value);
|
|
48
|
+
const record = normalized;
|
|
49
|
+
const issues = [];
|
|
50
|
+
requireString(record, "action", issues);
|
|
51
|
+
requireRecord(record, "reason", issues);
|
|
52
|
+
if (isRecord(record.reason)) {
|
|
53
|
+
requireString(record.reason, "reasonCode", issues);
|
|
54
|
+
requireString(record.reason, "summary", issues);
|
|
55
|
+
}
|
|
56
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
57
|
+
}
|
|
58
|
+
export function validateAuditEvent(value) {
|
|
59
|
+
const normalized = normalizeAuditEvent(value);
|
|
60
|
+
const record = normalized;
|
|
61
|
+
const issues = [];
|
|
62
|
+
requireString(record, "eventId", issues);
|
|
63
|
+
requireString(record, "traceId", issues);
|
|
64
|
+
requireString(record, "category", issues);
|
|
65
|
+
requireString(record, "type", issues);
|
|
66
|
+
requireString(record, "ts", issues);
|
|
67
|
+
requireRecord(record, "payload", issues);
|
|
68
|
+
return { ok: issues.length === 0, value: normalized, issues };
|
|
69
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
// GSD2 UOK Dispatch Envelope Builder
|
|
2
|
+
import { CURRENT_UOK_CONTRACT_VERSION } from "./contracts.js";
|
|
1
3
|
export function buildDispatchEnvelope(input) {
|
|
2
4
|
return {
|
|
5
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
3
6
|
action: input.action,
|
|
4
7
|
nodeKind: input.node?.kind,
|
|
5
8
|
unitType: input.unitType,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// GSD2 UOK Turn Observer and DB-Backed Lifecycle Emission
|
|
2
|
+
import { CURRENT_UOK_CONTRACT_VERSION, validateTurnResult } from "./contracts.js";
|
|
1
3
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
|
|
2
4
|
import { writeTurnCloseoutGitRecord, writeTurnGitTransaction } from "./gitops.js";
|
|
3
5
|
import { acquireWriterToken, nextWriteRecord, releaseWriterToken } from "./writer.js";
|
|
@@ -115,46 +117,59 @@ export function createTurnObserver(options) {
|
|
|
115
117
|
}
|
|
116
118
|
},
|
|
117
119
|
onTurnResult(result) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
const cleanup = () => {
|
|
121
|
+
if (writerToken) {
|
|
122
|
+
releaseWriterToken(options.basePath, writerToken);
|
|
123
|
+
}
|
|
124
|
+
writerToken = null;
|
|
125
|
+
current = null;
|
|
126
|
+
phaseResults.length = 0;
|
|
121
127
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
try {
|
|
129
|
+
const merged = {
|
|
130
|
+
...result,
|
|
131
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
132
|
+
phaseResults: Array.isArray(result.phaseResults) && result.phaseResults.length > 0 ? result.phaseResults : [...phaseResults],
|
|
133
|
+
};
|
|
134
|
+
const validation = validateTurnResult(merged);
|
|
135
|
+
if (!validation.ok) {
|
|
136
|
+
throw new Error(`Invalid UOK turn result: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
137
|
+
}
|
|
138
|
+
if (options.enableAudit) {
|
|
139
|
+
emitUokAuditEvent(options.basePath, buildAuditEnvelope({
|
|
140
|
+
traceId: validation.value.traceId,
|
|
141
|
+
turnId: validation.value.turnId,
|
|
142
|
+
category: "orchestration",
|
|
143
|
+
type: "turn-result",
|
|
144
|
+
payload: nextSequenceMetadata("audit", "append", {
|
|
145
|
+
contractVersion: validation.value.version,
|
|
146
|
+
unitType: validation.value.unitType,
|
|
147
|
+
unitId: validation.value.unitId,
|
|
148
|
+
status: validation.value.status,
|
|
149
|
+
failureClass: validation.value.failureClass,
|
|
150
|
+
error: validation.value.error,
|
|
151
|
+
phaseCount: validation.value.phaseResults.length,
|
|
152
|
+
}),
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
if (options.enableGitops) {
|
|
156
|
+
const closeout = merged.closeout ?? {
|
|
157
|
+
traceId: merged.traceId,
|
|
158
|
+
turnId: merged.turnId,
|
|
129
159
|
unitType: merged.unitType,
|
|
130
160
|
unitId: merged.unitId,
|
|
131
161
|
status: merged.status,
|
|
132
162
|
failureClass: merged.failureClass,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
gitAction: options.gitAction,
|
|
164
|
+
gitPushed: options.gitPush,
|
|
165
|
+
finishedAt: merged.finishedAt,
|
|
166
|
+
};
|
|
167
|
+
writeTurnCloseoutGitRecord(options.basePath, closeout, nextSequenceMetadata("gitops", "update", { action: "record" }));
|
|
168
|
+
}
|
|
137
169
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
traceId: merged.traceId,
|
|
141
|
-
turnId: merged.turnId,
|
|
142
|
-
unitType: merged.unitType,
|
|
143
|
-
unitId: merged.unitId,
|
|
144
|
-
status: merged.status,
|
|
145
|
-
failureClass: merged.failureClass,
|
|
146
|
-
gitAction: options.gitAction,
|
|
147
|
-
gitPushed: options.gitPush,
|
|
148
|
-
finishedAt: merged.finishedAt,
|
|
149
|
-
};
|
|
150
|
-
writeTurnCloseoutGitRecord(options.basePath, closeout, nextSequenceMetadata("gitops", "update", { action: "record" }));
|
|
170
|
+
finally {
|
|
171
|
+
cleanup();
|
|
151
172
|
}
|
|
152
|
-
if (writerToken) {
|
|
153
|
-
releaseWriterToken(options.basePath, writerToken);
|
|
154
|
-
}
|
|
155
|
-
writerToken = null;
|
|
156
|
-
current = null;
|
|
157
|
-
phaseResults.length = 0;
|
|
158
173
|
},
|
|
159
174
|
};
|
|
160
175
|
}
|