@gajae-code/coding-agent 0.11.0 → 0.11.1
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/CHANGELOG.md +9 -1
- package/dist/types/async/job-manager.d.ts +46 -7
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +1 -0
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/sdk/broker/ensure.d.ts +9 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -0
- package/dist/types/tools/subagent-render.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +6 -1
- package/package.json +8 -8
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/src/async/job-manager.ts +406 -63
- package/src/gjc-runtime/ultragoal-guard.ts +55 -8
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +72 -8
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/controllers/command-controller.ts +9 -15
- package/src/modes/controllers/extension-ui-controller.ts +48 -24
- package/src/modes/interactive-mode.ts +28 -16
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +7 -3
- package/src/modes/types.ts +2 -2
- package/src/sdk/broker/discovery.ts +20 -1
- package/src/sdk/broker/ensure.ts +82 -24
- package/src/sdk/bus/chat-effect-journal.ts +6 -4
- package/src/sdk/bus/discord-daemon.ts +25 -8
- package/src/sdk/bus/index.ts +57 -21
- package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +102 -10
- package/src/session/agent-session.ts +176 -29
- package/src/task/index.ts +26 -16
- package/src/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +84 -32
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
3
3
|
import { resolveGjcSessionForRead, SessionResolutionError } from "./session-resolution";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
validateDeferredMemberReceiptFresh,
|
|
6
|
+
validateReceiptFreshBase,
|
|
7
|
+
validateSupersededFinalAggregateReceipt,
|
|
8
|
+
} from "./ultragoal-receipt-freshness";
|
|
5
9
|
import {
|
|
6
10
|
getUltragoalPaths,
|
|
7
11
|
getUltragoalRunCompletionState,
|
|
@@ -156,8 +160,33 @@ function requiredGoals(plan: UltragoalPlan): UltragoalGoal[] {
|
|
|
156
160
|
return plan.goals.filter(goal => goal.status !== "superseded");
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Select the goal whose final-aggregate receipt should represent the run.
|
|
165
|
+
* Prefer a receipt that still validates fresh; several goals can hold
|
|
166
|
+
* final-aggregate receipts once plan growth (e.g. `steer add_subgoal`) stales
|
|
167
|
+
* an earlier one and a later checkpoint re-mints. Fall back to the newest
|
|
168
|
+
* holder (array-last) purely for diagnostics when none validates.
|
|
169
|
+
*/
|
|
170
|
+
function findFinalAggregateReceiptGoal(
|
|
171
|
+
plan: UltragoalPlan,
|
|
172
|
+
ledger: readonly UltragoalLedgerEvent[],
|
|
173
|
+
): UltragoalGoal | null {
|
|
174
|
+
const candidates = [...requiredGoals(plan)]
|
|
175
|
+
.reverse()
|
|
176
|
+
.filter(goal => goal.completionVerification?.receiptKind === "final-aggregate");
|
|
177
|
+
if (candidates.length === 0) return null;
|
|
178
|
+
return (
|
|
179
|
+
candidates.find(
|
|
180
|
+
goal =>
|
|
181
|
+
validateCompletionReceipt({ plan, ledger, goal, receiptKind: "final-aggregate" }).state ===
|
|
182
|
+
"active_verified_complete",
|
|
183
|
+
) ?? candidates[0]!
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
function findReceiptGoal(
|
|
160
188
|
plan: UltragoalPlan,
|
|
189
|
+
ledger: readonly UltragoalLedgerEvent[],
|
|
161
190
|
currentObjective: string,
|
|
162
191
|
): { goal: UltragoalGoal; receiptKind: UltragoalReceiptKind } | null {
|
|
163
192
|
if (
|
|
@@ -165,9 +194,7 @@ function findReceiptGoal(
|
|
|
165
194
|
currentObjective === DEFAULT_ULTRAGOAL_OBJECTIVE ||
|
|
166
195
|
plan.gjcObjectiveAliases?.some(alias => alias === currentObjective)
|
|
167
196
|
) {
|
|
168
|
-
const finalGoal =
|
|
169
|
-
.reverse()
|
|
170
|
-
.find(goal => goal.completionVerification?.receiptKind === "final-aggregate");
|
|
197
|
+
const finalGoal = findFinalAggregateReceiptGoal(plan, ledger);
|
|
171
198
|
return finalGoal ? { goal: finalGoal, receiptKind: "final-aggregate" } : null;
|
|
172
199
|
}
|
|
173
200
|
const storyGoal = plan.goals.find(goal => goal.objective === currentObjective);
|
|
@@ -300,6 +327,28 @@ export function validateCompletionReceipt(input: {
|
|
|
300
327
|
goalId: input.goal.id,
|
|
301
328
|
};
|
|
302
329
|
}
|
|
330
|
+
if (
|
|
331
|
+
priorGoal.completionVerification.validationBatch?.role !== "deferred-member" &&
|
|
332
|
+
priorGoal.completionVerification.receiptKind === "final-aggregate"
|
|
333
|
+
) {
|
|
334
|
+
// A prior goal may hold the run's previous final-aggregate receipt
|
|
335
|
+
// when plan growth staled it and a later checkpoint re-minted the
|
|
336
|
+
// aggregate receipt. Accept it as historical evidence for its own
|
|
337
|
+
// goal instead of demanding an impossible per-goal receipt.
|
|
338
|
+
const supersededDiagnostic = validateSupersededFinalAggregateReceipt({
|
|
339
|
+
ledger: input.ledger,
|
|
340
|
+
goal: priorGoal,
|
|
341
|
+
receipt: priorGoal.completionVerification,
|
|
342
|
+
});
|
|
343
|
+
if (supersededDiagnostic) {
|
|
344
|
+
return {
|
|
345
|
+
state: supersededDiagnostic.state,
|
|
346
|
+
message: `Ultragoal final receipt requires valid historical evidence for ${priorGoal.id}: ${supersededDiagnostic.message}`,
|
|
347
|
+
goalId: input.goal.id,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
303
352
|
const priorDiagnostic =
|
|
304
353
|
priorGoal.completionVerification.validationBatch?.role === "deferred-member"
|
|
305
354
|
? validateDeferredMemberReceiptFresh({
|
|
@@ -381,7 +430,7 @@ export async function readUltragoalVerificationState(input: {
|
|
|
381
430
|
message: "Ultragoal has blocked or failed goals; record blockers or rerun verification.",
|
|
382
431
|
};
|
|
383
432
|
}
|
|
384
|
-
const receiptTarget = findReceiptGoal(plan, currentObjective);
|
|
433
|
+
const receiptTarget = findReceiptGoal(plan, ledger, currentObjective);
|
|
385
434
|
if (!receiptTarget) {
|
|
386
435
|
// When earlier required goals are already complete but later ones remain, name the
|
|
387
436
|
// specific blocking goals (a final-aggregate receipt cannot exist yet anyway). Only
|
|
@@ -623,9 +672,7 @@ export async function isUltragoalAskBlocked(
|
|
|
623
672
|
});
|
|
624
673
|
}
|
|
625
674
|
|
|
626
|
-
const finalReceiptGoal =
|
|
627
|
-
.reverse()
|
|
628
|
-
.find(goal => goal.completionVerification?.receiptKind === "final-aggregate");
|
|
675
|
+
const finalReceiptGoal = findFinalAggregateReceiptGoal(plan, ledger);
|
|
629
676
|
if (!finalReceiptGoal) {
|
|
630
677
|
return activeAskDiagnostic({
|
|
631
678
|
reason: "Ultragoal aggregate completion is missing a final aggregate receipt.",
|
|
@@ -244,6 +244,93 @@ export function validateReceiptFreshBase(input: {
|
|
|
244
244
|
return null;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Validate a receipt whose plan-generation freshness was legitimately staled
|
|
249
|
+
* by later plan growth as a historical attestation for its own goal:
|
|
250
|
+
* internally consistent, anchored to a ledger checkpoint event that carries a
|
|
251
|
+
* byte-identical receipt, quality gate untampered, and the goal row untouched
|
|
252
|
+
* since verification. Plan-generation freshness against the CURRENT plan is
|
|
253
|
+
* intentionally not required; that is exactly what later plan growth
|
|
254
|
+
* invalidates by design. Returns null when the receipt stands as valid
|
|
255
|
+
* historical evidence.
|
|
256
|
+
*/
|
|
257
|
+
function validateLedgerAnchoredHistoricalReceipt(input: {
|
|
258
|
+
ledger: readonly UltragoalLedgerEvent[];
|
|
259
|
+
goal: UltragoalGoal;
|
|
260
|
+
receipt: UltragoalCompletionVerification;
|
|
261
|
+
label: string;
|
|
262
|
+
}): UltragoalReceiptFreshnessDiagnostic | null {
|
|
263
|
+
if (
|
|
264
|
+
input.receipt.schemaVersion !== 1 ||
|
|
265
|
+
input.receipt.goalId !== input.goal.id ||
|
|
266
|
+
!input.receipt.planGeneration ||
|
|
267
|
+
!input.receipt.checkpointLedgerEventId ||
|
|
268
|
+
hashStructuredValue(input.receipt.basis) !== input.receipt.planGeneration
|
|
269
|
+
) {
|
|
270
|
+
return {
|
|
271
|
+
state: "active_stale_receipt",
|
|
272
|
+
message: `Ultragoal ${input.goal.id} ${input.label} is malformed.`,
|
|
273
|
+
goalId: input.goal.id,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const event = findLedgerReceiptEvent(input.ledger, input.receipt);
|
|
277
|
+
if (!event)
|
|
278
|
+
return {
|
|
279
|
+
state: "active_stale_receipt",
|
|
280
|
+
message: `Ultragoal ${input.goal.id} ${input.label} ledger event is missing.`,
|
|
281
|
+
goalId: input.goal.id,
|
|
282
|
+
};
|
|
283
|
+
// Bind the goals.json receipt to the FULL receipt recorded on the ledger
|
|
284
|
+
// event. Field-selective matching would accept a coordinated edit of the
|
|
285
|
+
// goals-row basis/generation paired with only the event generation.
|
|
286
|
+
const eventReceipt = event.completionVerification as UltragoalCompletionVerification | undefined;
|
|
287
|
+
if (!eventReceipt || hashStructuredValue(eventReceipt) !== hashStructuredValue(input.receipt))
|
|
288
|
+
return {
|
|
289
|
+
state: "active_stale_receipt",
|
|
290
|
+
message: `Ultragoal ${input.goal.id} ${input.label} does not match its ledger event receipt.`,
|
|
291
|
+
goalId: input.goal.id,
|
|
292
|
+
};
|
|
293
|
+
if (hashStructuredValue(event.qualityGateJson) !== input.receipt.qualityGateHash)
|
|
294
|
+
return {
|
|
295
|
+
state: "active_dirty_quality_gate",
|
|
296
|
+
message: `Ultragoal ${input.goal.id} ${input.label} quality-gate hash does not match ledger.`,
|
|
297
|
+
goalId: input.goal.id,
|
|
298
|
+
};
|
|
299
|
+
if (input.goal.updatedAt !== input.receipt.verifiedAt)
|
|
300
|
+
return {
|
|
301
|
+
state: "active_stale_receipt",
|
|
302
|
+
message: `Ultragoal ${input.goal.id} changed after its ${input.label} was verified.`,
|
|
303
|
+
goalId: input.goal.id,
|
|
304
|
+
};
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Validate a final-aggregate receipt that was legitimately superseded — its
|
|
310
|
+
* aggregate claim staled by later plan growth (e.g. `steer add_subgoal`
|
|
311
|
+
* appending goals after a terminal run) — as historical evidence for its own
|
|
312
|
+
* goal. Returns null when the receipt stands.
|
|
313
|
+
*/
|
|
314
|
+
export function validateSupersededFinalAggregateReceipt(input: {
|
|
315
|
+
ledger: readonly UltragoalLedgerEvent[];
|
|
316
|
+
goal: UltragoalGoal;
|
|
317
|
+
receipt: UltragoalCompletionVerification;
|
|
318
|
+
}): UltragoalReceiptFreshnessDiagnostic | null {
|
|
319
|
+
if (input.receipt.receiptKind !== "final-aggregate") {
|
|
320
|
+
return {
|
|
321
|
+
state: "active_stale_receipt",
|
|
322
|
+
message: `Ultragoal ${input.goal.id} superseded final-aggregate receipt is malformed.`,
|
|
323
|
+
goalId: input.goal.id,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return validateLedgerAnchoredHistoricalReceipt({
|
|
327
|
+
ledger: input.ledger,
|
|
328
|
+
goal: input.goal,
|
|
329
|
+
receipt: input.receipt,
|
|
330
|
+
label: "superseded final-aggregate receipt",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
247
334
|
export function findFreshBatchCloseReceipt(input: {
|
|
248
335
|
plan: UltragoalPlan;
|
|
249
336
|
ledger: readonly UltragoalLedgerEvent[];
|
|
@@ -267,7 +354,21 @@ export function findFreshBatchCloseReceipt(input: {
|
|
|
267
354
|
receipt: finalReceipt,
|
|
268
355
|
receiptKind: finalReceipt.receiptKind,
|
|
269
356
|
});
|
|
270
|
-
|
|
357
|
+
if (!diagnostic) return finalReceipt;
|
|
358
|
+
// A batch close staled only by later plan growth (e.g. `steer add_subgoal`
|
|
359
|
+
// after the batch completed) still proves the batch was validly closed:
|
|
360
|
+
// the member receipts are freshness-checked separately, the close is
|
|
361
|
+
// anchored to its ledger event, and the final goal's row is untouched.
|
|
362
|
+
// Without this, completing a goal appended after a closed batch can never
|
|
363
|
+
// validate, because the old close can never be fresh against the grown
|
|
364
|
+
// plan and identical-evidence replays of fresh receipts are no-ops.
|
|
365
|
+
const historicalDiagnostic = validateLedgerAnchoredHistoricalReceipt({
|
|
366
|
+
ledger: input.ledger,
|
|
367
|
+
goal: finalGoal,
|
|
368
|
+
receipt: finalReceipt,
|
|
369
|
+
label: "superseded batch-close receipt",
|
|
370
|
+
});
|
|
371
|
+
return historicalDiagnostic ? null : finalReceipt;
|
|
271
372
|
}
|
|
272
373
|
|
|
273
374
|
export function validateDeferredMemberReceiptFresh(input: {
|
|
@@ -573,16 +573,32 @@ async function writePlan(cwd: string, plan: UltragoalPlan, sessionId?: string |
|
|
|
573
573
|
|
|
574
574
|
function chooseReceiptKind(
|
|
575
575
|
plan: UltragoalPlan,
|
|
576
|
+
ledger: readonly UltragoalLedgerEvent[],
|
|
576
577
|
goal: UltragoalGoal,
|
|
577
578
|
status: UltragoalGoalStatus,
|
|
578
579
|
): UltragoalReceiptKind {
|
|
579
580
|
if (plan.gjcGoalMode === "per-story") return "per-goal";
|
|
580
581
|
if (status !== "complete") return "per-goal";
|
|
582
|
+
// A non-final validation-batch member must always carry a per-goal
|
|
583
|
+
// deferred receipt; only the batch's final goal may close the batch and
|
|
584
|
+
// (in aggregate mode) carry the final-aggregate receipt. Without this, a
|
|
585
|
+
// context-stale re-verification replay of a member could mint an invalid
|
|
586
|
+
// final-aggregate receipt with validationBatch.role "deferred-member".
|
|
587
|
+
if (goal.validationBatch && goal.validationBatch.finalGoalId !== goal.id) return "per-goal";
|
|
581
588
|
const requiredGoals = requiredUltragoalGoals(plan);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
)
|
|
585
|
-
|
|
589
|
+
// Only a still-fresh final-aggregate receipt on another goal defers this
|
|
590
|
+
// checkpoint to per-goal. A stale one (e.g. staled by `steer add_subgoal`
|
|
591
|
+
// appending goals after a terminal run) must not suppress re-minting,
|
|
592
|
+
// otherwise the run can never regain a verifiable final-aggregate receipt:
|
|
593
|
+
// the completion guard demands a fresh final-aggregate receipt while this
|
|
594
|
+
// gate would keep answering per-goal forever.
|
|
595
|
+
const existingFreshFinalAggregateGoal = requiredGoals.find(item => {
|
|
596
|
+
if (item.id === goal.id) return false;
|
|
597
|
+
const receipt = item.completionVerification;
|
|
598
|
+
if (receipt?.receiptKind !== "final-aggregate") return false;
|
|
599
|
+
return validateReceiptFreshBase({ plan, ledger, goal: item, receipt, receiptKind: "final-aggregate" }) === null;
|
|
600
|
+
});
|
|
601
|
+
if (existingFreshFinalAggregateGoal) return "per-goal";
|
|
586
602
|
const unfinishedRequiredGoals = requiredGoals.filter(
|
|
587
603
|
item => item.id !== goal.id && !TERMINAL_OR_SKIPPED_STATUSES.has(item.status),
|
|
588
604
|
);
|
|
@@ -3964,13 +3980,21 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
3964
3980
|
const evidence = input.evidence.trim();
|
|
3965
3981
|
if (!evidence) throw new Error("checkpoint evidence is required");
|
|
3966
3982
|
const ledgerBefore = await readUltragoalLedger(input.cwd);
|
|
3967
|
-
const
|
|
3983
|
+
const matchingIdempotentEvents = ledgerBefore.filter(
|
|
3968
3984
|
event =>
|
|
3969
3985
|
event.event === "goal_checkpointed" &&
|
|
3970
3986
|
event.goalId === goal.id &&
|
|
3971
3987
|
event.status === input.status &&
|
|
3972
3988
|
event.evidence === evidence,
|
|
3973
3989
|
);
|
|
3990
|
+
// Re-verification replays legitimately append repeated same-status,
|
|
3991
|
+
// same-evidence checkpoint events for one goal. The recorded receipt must
|
|
3992
|
+
// be compared against ITS OWN checkpoint event — resolved by the receipt's
|
|
3993
|
+
// checkpointLedgerEventId, falling back to the latest match — never the
|
|
3994
|
+
// oldest duplicate, which would wrongly report the current receipt stale.
|
|
3995
|
+
const matchingIdempotentEvent =
|
|
3996
|
+
matchingIdempotentEvents.find(event => event.eventId === goal.completionVerification?.checkpointLedgerEventId) ??
|
|
3997
|
+
matchingIdempotentEvents.at(-1);
|
|
3974
3998
|
const batchMetadata = input.status === "complete" ? requireFreshValidationBatchMetadata(goal) : undefined;
|
|
3975
3999
|
if (batchMetadata && goal.completionVerification?.validationBatch) {
|
|
3976
4000
|
const receiptBatch = goal.completionVerification.validationBatch;
|
|
@@ -3987,7 +4011,35 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
3987
4011
|
if (input.status === "complete" && goal.completionVerification?.validationBatch && !batchMetadata) {
|
|
3988
4012
|
throw new Error(`Goal ${goal.id} has stale validation batch completion receipt`);
|
|
3989
4013
|
}
|
|
3990
|
-
|
|
4014
|
+
// An identical-evidence complete replay is only a no-op while the recorded
|
|
4015
|
+
// receipt still validates fresh. When the goal row itself is untouched
|
|
4016
|
+
// (updatedAt still matches the receipt's verifiedAt) but later goal-tagged
|
|
4017
|
+
// ledger events (e.g. blocker classifications) or plan growth staled the
|
|
4018
|
+
// receipt, the replay is a genuine re-verification: it must run the full
|
|
4019
|
+
// quality gate and mint a fresh receipt, otherwise a completed goal with a
|
|
4020
|
+
// context-staled receipt can never be repaired (different evidence is
|
|
4021
|
+
// rejected on complete goals by design). A mutated goal row keeps the
|
|
4022
|
+
// fail-loud tamper handling in the idempotent branch below.
|
|
4023
|
+
const staleCompleteReceiptReplay =
|
|
4024
|
+
input.status === "complete" &&
|
|
4025
|
+
goal.status === "complete" &&
|
|
4026
|
+
goal.evidence === evidence &&
|
|
4027
|
+
Boolean(matchingIdempotentEvent) &&
|
|
4028
|
+
(!goal.completionVerification ||
|
|
4029
|
+
(goal.completionVerification.verifiedAt === goal.updatedAt &&
|
|
4030
|
+
validateReceiptFreshBase({
|
|
4031
|
+
plan,
|
|
4032
|
+
ledger: ledgerBefore,
|
|
4033
|
+
goal,
|
|
4034
|
+
receipt: goal.completionVerification,
|
|
4035
|
+
receiptKind: goal.completionVerification.receiptKind,
|
|
4036
|
+
}) !== null));
|
|
4037
|
+
if (
|
|
4038
|
+
goal.status === input.status &&
|
|
4039
|
+
goal.evidence === evidence &&
|
|
4040
|
+
matchingIdempotentEvent &&
|
|
4041
|
+
!staleCompleteReceiptReplay
|
|
4042
|
+
) {
|
|
3991
4043
|
if (batchMetadata) {
|
|
3992
4044
|
const receipt = goal.completionVerification;
|
|
3993
4045
|
const receiptBatch = receipt?.validationBatch;
|
|
@@ -4009,6 +4061,18 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
4009
4061
|
)
|
|
4010
4062
|
throw new Error(`Goal ${goal.id} has stale validation batch metadata hash in close receipt`);
|
|
4011
4063
|
}
|
|
4064
|
+
// A complete goal whose row changed after its receipt was verified is
|
|
4065
|
+
// neither a clean no-op nor a repairable context-stale replay: fail loud
|
|
4066
|
+
// instead of silently laundering a tampered/inconsistent durable row.
|
|
4067
|
+
if (
|
|
4068
|
+
input.status === "complete" &&
|
|
4069
|
+
goal.completionVerification &&
|
|
4070
|
+
goal.completionVerification.verifiedAt !== goal.updatedAt
|
|
4071
|
+
) {
|
|
4072
|
+
throw new Error(
|
|
4073
|
+
`Goal ${goal.id} changed after its completion receipt was verified; refusing idempotent replay. Investigate the durable goals.json row before re-checkpointing.`,
|
|
4074
|
+
);
|
|
4075
|
+
}
|
|
4012
4076
|
// Idempotent re-checkpoint: this goal is already recorded in the target status with the same
|
|
4013
4077
|
// evidence, so skip the plan rewrite and ledger append to avoid duplicate goal_checkpointed
|
|
4014
4078
|
// events. The ledger is the dedup source of truth because it is exactly what a duplicate write
|
|
@@ -4020,7 +4084,7 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
4020
4084
|
const changeSet = input.status === "complete" ? await computeCheckpointChangeSet(input.cwd) : undefined;
|
|
4021
4085
|
if (input.status === "complete") {
|
|
4022
4086
|
validatePipelineCheckpointSafety(plan, goal, changeSet);
|
|
4023
|
-
validateCompleteCheckpointTargetGoal(goal);
|
|
4087
|
+
if (!staleCompleteReceiptReplay) validateCompleteCheckpointTargetGoal(goal);
|
|
4024
4088
|
}
|
|
4025
4089
|
const qualityGateJson =
|
|
4026
4090
|
input.status === "complete"
|
|
@@ -4047,7 +4111,7 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
4047
4111
|
blockedGoal.updatedAt = now;
|
|
4048
4112
|
}
|
|
4049
4113
|
}
|
|
4050
|
-
const receiptKind = input.status === "complete" ? chooseReceiptKind(plan, goal, input.status) : null;
|
|
4114
|
+
const receiptKind = input.status === "complete" ? chooseReceiptKind(plan, ledgerBefore, goal, input.status) : null;
|
|
4051
4115
|
const pendingCheckpointEventId = crypto.randomUUID();
|
|
4052
4116
|
if (input.status === "complete" && receiptKind && qualityGateJson && !Array.isArray(qualityGateJson)) {
|
|
4053
4117
|
goal.completionVerification = buildCompletionReceipt({
|