@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/dist/types/async/job-manager.d.ts +46 -7
  3. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
  4. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  5. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
  6. package/dist/types/modes/interactive-mode.d.ts +2 -2
  7. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +1 -0
  8. package/dist/types/modes/types.d.ts +2 -2
  9. package/dist/types/sdk/broker/ensure.d.ts +9 -0
  10. package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
  11. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -0
  12. package/dist/types/tools/subagent-render.d.ts +1 -0
  13. package/dist/types/tools/subagent.d.ts +6 -1
  14. package/package.json +8 -8
  15. package/scripts/build-sdk-package-smoke.ts +5 -0
  16. package/src/async/job-manager.ts +406 -63
  17. package/src/gjc-runtime/ultragoal-guard.ts +55 -8
  18. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
  19. package/src/gjc-runtime/ultragoal-runtime.ts +72 -8
  20. package/src/internal-urls/docs-index.generated.ts +2 -2
  21. package/src/modes/controllers/command-controller.ts +9 -15
  22. package/src/modes/controllers/extension-ui-controller.ts +48 -24
  23. package/src/modes/interactive-mode.ts +28 -16
  24. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +7 -3
  25. package/src/modes/types.ts +2 -2
  26. package/src/sdk/broker/discovery.ts +20 -1
  27. package/src/sdk/broker/ensure.ts +82 -24
  28. package/src/sdk/bus/chat-effect-journal.ts +6 -4
  29. package/src/sdk/bus/discord-daemon.ts +25 -8
  30. package/src/sdk/bus/index.ts +57 -21
  31. package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
  32. package/src/sdk/bus/telegram-daemon.ts +102 -10
  33. package/src/session/agent-session.ts +176 -29
  34. package/src/task/index.ts +26 -16
  35. package/src/tools/subagent-render.ts +19 -8
  36. 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 { validateDeferredMemberReceiptFresh, validateReceiptFreshBase } from "./ultragoal-receipt-freshness";
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 = [...requiredGoals(plan)]
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 = [...requiredGoals(plan)]
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
- return diagnostic ? null : finalReceipt;
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
- const existingFinalAggregateGoal = requiredGoals.find(
583
- item => item.id !== goal.id && item.completionVerification?.receiptKind === "final-aggregate",
584
- );
585
- if (existingFinalAggregateGoal) return "per-goal";
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 matchingIdempotentEvent = ledgerBefore.find(
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
- if (goal.status === input.status && goal.evidence === evidence && matchingIdempotentEvent) {
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({