@sayknow-cli/coding-agent 0.3.7 → 0.3.8

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 (248) hide show
  1. package/dist/types/append-only-mode.d.ts +31 -0
  2. package/dist/types/capability/rule.d.ts +8 -0
  3. package/dist/types/cli/completion-cli.d.ts +41 -0
  4. package/dist/types/cli/config-cli.d.ts +1 -0
  5. package/dist/types/cli/fixture-report.d.ts +20 -0
  6. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  7. package/dist/types/cli/nofile-limit.d.ts +11 -0
  8. package/dist/types/cli/notify-cli.d.ts +7 -0
  9. package/dist/types/cli/update-cli.d.ts +22 -0
  10. package/dist/types/cli.d.ts +124 -1
  11. package/dist/types/commands/completion.d.ts +29 -0
  12. package/dist/types/commands/config.d.ts +3 -0
  13. package/dist/types/commands/deep-interview.d.ts +3 -0
  14. package/dist/types/config/file-lock.d.ts +7 -6
  15. package/dist/types/config/keybindings.d.ts +2 -2
  16. package/dist/types/config/model-profile-activation.d.ts +6 -0
  17. package/dist/types/config/model-profiles.d.ts +1 -0
  18. package/dist/types/config/settings-schema.d.ts +105 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/goals/runtime.d.ts +1 -0
  26. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  27. package/dist/types/main.d.ts +4 -1
  28. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  29. package/dist/types/modes/components/model-selector.d.ts +4 -0
  30. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  31. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  32. package/dist/types/modes/components/status-line.d.ts +9 -0
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  35. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/print-mode.d.ts +13 -1
  38. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  39. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  40. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  41. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  42. package/dist/types/modes/types.d.ts +1 -0
  43. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  44. package/dist/types/notifications/config.d.ts +14 -1
  45. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  46. package/dist/types/notifications/index.d.ts +6 -3
  47. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  48. package/dist/types/notifications/telegram-daemon.d.ts +23 -3
  49. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  50. package/dist/types/notifications/threaded-render.d.ts +6 -0
  51. package/dist/types/runtime-api-key.d.ts +7 -0
  52. package/dist/types/sdk.d.ts +3 -0
  53. package/dist/types/session/agent-session.d.ts +61 -6
  54. package/dist/types/session/agent-storage.d.ts +1 -0
  55. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  56. package/dist/types/session/messages.d.ts +4 -0
  57. package/dist/types/session/session-manager.d.ts +17 -1
  58. package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
  59. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
  60. package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
  61. package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
  62. package/dist/types/skc-runtime/state-writer.d.ts +3 -0
  63. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  64. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
  65. package/dist/types/skill-state/active-state.d.ts +18 -1
  66. package/dist/types/system-prompt.d.ts +18 -2
  67. package/dist/types/task/executor.d.ts +3 -0
  68. package/dist/types/task/fork-context-budget.d.ts +7 -0
  69. package/dist/types/task/token-log.d.ts +30 -0
  70. package/dist/types/task/token-log.test.d.ts +1 -0
  71. package/dist/types/tools/bisect.d.ts +81 -0
  72. package/dist/types/tools/computer.d.ts +2 -2
  73. package/dist/types/tools/index.d.ts +1 -0
  74. package/dist/types/tools/search.d.ts +2 -2
  75. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  76. package/dist/types/utils/file-mentions.d.ts +14 -0
  77. package/dist/types/utils/git.d.ts +32 -0
  78. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  79. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  80. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  81. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  82. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  83. package/package.json +8 -8
  84. package/src/append-only-mode.ts +58 -0
  85. package/src/autoresearch/prompt.md +1 -1
  86. package/src/autoresearch/tools/log-experiment.ts +1 -1
  87. package/src/capability/rule.ts +9 -0
  88. package/src/cli/completion-cli.ts +234 -0
  89. package/src/cli/config-cli.ts +61 -9
  90. package/src/cli/fast-help.ts +1 -0
  91. package/src/cli/fixture-report.test.ts +230 -0
  92. package/src/cli/fixture-report.ts +171 -0
  93. package/src/cli/nofile-limit.ts +48 -0
  94. package/src/cli/notify-cli.ts +92 -8
  95. package/src/cli/telegram-cli.ts +1 -1
  96. package/src/cli/update-cli.ts +85 -9
  97. package/src/cli.ts +29 -1
  98. package/src/commands/completion.ts +84 -0
  99. package/src/commands/config.ts +2 -0
  100. package/src/commands/contribution-prep.ts +1 -1
  101. package/src/commands/deep-interview.ts +2 -1
  102. package/src/config/file-lock.ts +73 -34
  103. package/src/config/keybindings.ts +2 -2
  104. package/src/config/model-profile-activation.ts +65 -0
  105. package/src/config/model-profiles.ts +31 -2
  106. package/src/config/settings-schema.ts +68 -1
  107. package/src/config/settings.ts +22 -0
  108. package/src/defaults/skc/skills/team/SKILL.md +17 -14
  109. package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
  110. package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  111. package/src/defaults/skc-defaults.ts +9 -0
  112. package/src/discovery/helpers.ts +10 -0
  113. package/src/discovery/index.ts +1 -0
  114. package/src/export/html/template.generated.ts +1 -1
  115. package/src/export/html/template.js +60 -2
  116. package/src/export/ttsr.ts +96 -18
  117. package/src/extensibility/extensions/runner.ts +183 -219
  118. package/src/extensibility/extensions/types.ts +9 -0
  119. package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
  120. package/src/goals/runtime.ts +10 -2
  121. package/src/hindsight/backend.ts +1 -3
  122. package/src/hooks/native-skill-hook.ts +124 -4
  123. package/src/internal-urls/docs-index.generated.ts +14 -5
  124. package/src/main.ts +95 -43
  125. package/src/memories/index.ts +12 -1
  126. package/src/modes/components/assistant-message.ts +241 -82
  127. package/src/modes/components/custom-editor.ts +1 -0
  128. package/src/modes/components/model-selector.ts +54 -11
  129. package/src/modes/components/queued-message-selector.ts +92 -0
  130. package/src/modes/components/settings-selector.ts +4 -0
  131. package/src/modes/components/status-line.ts +167 -54
  132. package/src/modes/components/tool-execution.ts +44 -19
  133. package/src/modes/components/welcome.ts +181 -44
  134. package/src/modes/controllers/command-controller.ts +40 -4
  135. package/src/modes/controllers/event-controller.ts +66 -47
  136. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  137. package/src/modes/controllers/input-controller.ts +180 -18
  138. package/src/modes/controllers/selector-controller.ts +127 -24
  139. package/src/modes/interactive-mode.ts +51 -16
  140. package/src/modes/print-mode.ts +43 -4
  141. package/src/modes/prompt-action-autocomplete.ts +21 -20
  142. package/src/modes/rpc/rpc-mode.ts +29 -3
  143. package/src/modes/rpc/rpc-types.ts +9 -0
  144. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  145. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  146. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  147. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  148. package/src/modes/types.ts +1 -0
  149. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  150. package/src/modes/utils/ui-helpers.ts +20 -7
  151. package/src/notifications/config.ts +25 -5
  152. package/src/notifications/daemon-paths.ts +2 -0
  153. package/src/notifications/index.ts +117 -14
  154. package/src/notifications/lifecycle-commands.ts +37 -7
  155. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  156. package/src/notifications/telegram-daemon-control.ts +5 -3
  157. package/src/notifications/telegram-daemon.ts +302 -51
  158. package/src/notifications/telegram-reference.ts +0 -16
  159. package/src/notifications/threaded-render.ts +17 -1
  160. package/src/prompts/agents/architect.md +19 -10
  161. package/src/prompts/agents/critic.md +16 -18
  162. package/src/prompts/agents/executor.md +14 -11
  163. package/src/prompts/agents/planner.md +18 -13
  164. package/src/prompts/ci-green-request.md +3 -8
  165. package/src/prompts/goals/goal-continuation.md +0 -5
  166. package/src/prompts/goals/goal-mode-active.md +0 -5
  167. package/src/prompts/system/agent-creation-architect.md +2 -2
  168. package/src/prompts/system/plan-mode-active.md +1 -1
  169. package/src/prompts/system/plan-mode-subagent.md +3 -3
  170. package/src/prompts/system/project-prompt.md +0 -13
  171. package/src/prompts/system/subagent-system-prompt.md +3 -2
  172. package/src/prompts/system/system-prompt.md +29 -17
  173. package/src/prompts/system/volatile-project-context.md +10 -0
  174. package/src/prompts/system/web-search.md +4 -7
  175. package/src/prompts/tools/apply-patch.md +6 -0
  176. package/src/prompts/tools/ast-edit.md +1 -0
  177. package/src/prompts/tools/bash.md +1 -1
  178. package/src/prompts/tools/bisect.md +22 -0
  179. package/src/prompts/tools/browser.md +1 -1
  180. package/src/prompts/tools/computer.md +2 -2
  181. package/src/prompts/tools/cron.md +0 -2
  182. package/src/prompts/tools/eval.md +2 -2
  183. package/src/prompts/tools/find.md +1 -1
  184. package/src/prompts/tools/github.md +6 -6
  185. package/src/prompts/tools/goal.md +1 -3
  186. package/src/prompts/tools/image-gen.md +12 -5
  187. package/src/prompts/tools/irc.md +5 -4
  188. package/src/prompts/tools/job.md +2 -2
  189. package/src/prompts/tools/lsp.md +1 -1
  190. package/src/prompts/tools/monitor.md +2 -2
  191. package/src/prompts/tools/patch.md +2 -2
  192. package/src/prompts/tools/replace.md +2 -21
  193. package/src/prompts/tools/rewind.md +1 -1
  194. package/src/prompts/tools/search-tool-bm25.md +1 -11
  195. package/src/prompts/tools/search.md +1 -1
  196. package/src/prompts/tools/skill.md +3 -2
  197. package/src/prompts/tools/ssh.md +2 -1
  198. package/src/prompts/tools/subagent.md +2 -2
  199. package/src/prompts/tools/task.md +4 -5
  200. package/src/prompts/tools/todo-write.md +1 -1
  201. package/src/prompts/tools/vim.md +1 -0
  202. package/src/prompts/tools/web-search.md +2 -0
  203. package/src/prompts/tools/write.md +8 -2
  204. package/src/runtime-api-key.ts +16 -0
  205. package/src/sdk.ts +52 -28
  206. package/src/session/agent-session.ts +1011 -141
  207. package/src/session/agent-storage.ts +4 -0
  208. package/src/session/file-mention-pruning.ts +64 -0
  209. package/src/session/messages.ts +4 -0
  210. package/src/session/session-manager.ts +103 -4
  211. package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
  212. package/src/skc-runtime/deep-interview-recorder.ts +62 -8
  213. package/src/skc-runtime/deep-interview-state.ts +44 -2
  214. package/src/skc-runtime/session-state-sidecar.ts +263 -20
  215. package/src/skc-runtime/state-runtime.ts +12 -2
  216. package/src/skc-runtime/state-writer.ts +15 -0
  217. package/src/skc-runtime/ultragoal-guard.ts +55 -66
  218. package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
  219. package/src/skc-runtime/ultragoal-runtime.ts +1663 -159
  220. package/src/skc-runtime/workflow-manifest.generated.json +113 -2
  221. package/src/skc-runtime/workflow-manifest.ts +74 -1
  222. package/src/skill-state/active-state.ts +141 -5
  223. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  224. package/src/slash-commands/builtin-registry.ts +218 -71
  225. package/src/system-prompt.ts +57 -16
  226. package/src/task/executor.ts +132 -24
  227. package/src/task/fork-context-advisory.ts +2 -7
  228. package/src/task/fork-context-budget.ts +9 -0
  229. package/src/task/index.ts +13 -6
  230. package/src/task/output-manager.ts +20 -8
  231. package/src/task/receipt.ts +26 -7
  232. package/src/task/token-log.test.ts +188 -0
  233. package/src/task/token-log.ts +179 -0
  234. package/src/tools/bisect.ts +367 -0
  235. package/src/tools/computer.ts +246 -43
  236. package/src/tools/index.ts +3 -0
  237. package/src/tools/output-meta.ts +116 -4
  238. package/src/tools/search.ts +8 -2
  239. package/src/tools/skill.ts +15 -0
  240. package/src/tools/telegram-send.ts +6 -0
  241. package/src/tools/ultragoal-ask-guard.ts +39 -5
  242. package/src/utils/file-mentions.ts +39 -7
  243. package/src/utils/git.ts +73 -0
  244. package/src/web/scrapers/youtube.ts +24 -12
  245. package/src/workflow/workflow-intent-diff.ts +29 -6
  246. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  247. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  248. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -1,17 +1,15 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
3
3
  import { resolveSkcSessionForRead, SessionResolutionError } from "./session-resolution";
4
+ import { validateDeferredMemberReceiptFresh, validateReceiptFreshBase } from "./ultragoal-receipt-freshness";
4
5
  import {
5
- computeUltragoalPlanGeneration,
6
6
  getUltragoalPaths,
7
7
  getUltragoalRunCompletionState,
8
- hashStructuredValue,
9
8
  readUltragoalLedger,
10
9
  readUltragoalPlan,
11
10
  recordUltragoalNudgeIfBudgetRemaining,
12
11
  resolveUltragoalNudgeBudget,
13
12
  selectUltragoalNudgeTarget,
14
- type UltragoalCompletionVerification,
15
13
  type UltragoalGoal,
16
14
  type UltragoalLedgerEvent,
17
15
  type UltragoalNudgeSurface,
@@ -176,26 +174,6 @@ function findReceiptGoal(
176
174
  return storyGoal ? { goal: storyGoal, receiptKind: "per-goal" } : null;
177
175
  }
178
176
 
179
- function findLedgerReceiptEvent(
180
- ledger: readonly UltragoalLedgerEvent[],
181
- receipt: UltragoalCompletionVerification,
182
- ): UltragoalLedgerEvent | null {
183
- return (
184
- ledger.find(event => {
185
- if (event.eventId !== receipt.checkpointLedgerEventId) return false;
186
- if (event.event !== "goal_checkpointed") return false;
187
- if (event.goalId !== receipt.goalId) return false;
188
- const eventReceipt = event.completionVerification as UltragoalCompletionVerification | undefined;
189
- return (
190
- event.status === "complete" &&
191
- eventReceipt?.receiptId === receipt.receiptId &&
192
- eventReceipt.receiptKind === receipt.receiptKind &&
193
- eventReceipt.planGeneration === receipt.planGeneration
194
- );
195
- }) ?? null
196
- );
197
- }
198
-
199
177
  export function validateCompletionReceipt(input: {
200
178
  plan: UltragoalPlan;
201
179
  ledger: readonly UltragoalLedgerEvent[];
@@ -210,55 +188,66 @@ export function validateCompletionReceipt(input: {
210
188
  goalId: input.goal.id,
211
189
  };
212
190
  }
213
- if (
214
- receipt.schemaVersion !== 1 ||
215
- receipt.goalId !== input.goal.id ||
216
- receipt.receiptKind !== input.receiptKind ||
217
- !receipt.planGeneration ||
218
- !receipt.checkpointLedgerEventId
219
- ) {
220
- return {
221
- state: "active_stale_receipt",
222
- message: `Ultragoal ${input.goal.id} receipt is malformed or stale.`,
223
- goalId: input.goal.id,
224
- };
225
- }
226
- const event = findLedgerReceiptEvent(input.ledger, receipt);
227
- if (!event) {
228
- return {
229
- state: "active_stale_receipt",
230
- message: `Ultragoal ${input.goal.id} receipt ledger event is missing.`,
231
- goalId: input.goal.id,
232
- };
191
+ if (receipt.validationBatch?.role === "deferred-member") {
192
+ return validateDeferredMemberReceiptFresh({
193
+ plan: input.plan,
194
+ ledger: input.ledger,
195
+ goal: input.goal,
196
+ receipt,
197
+ receiptKind: input.receiptKind,
198
+ requireClose: true,
199
+ });
233
200
  }
234
- const generation = computeUltragoalPlanGeneration({
201
+ const baseDiagnostic = validateReceiptFreshBase({
235
202
  plan: input.plan,
236
203
  ledger: input.ledger,
237
204
  goal: input.goal,
205
+ receipt,
238
206
  receiptKind: input.receiptKind,
239
- beforeStatus: receipt.goalStatusBeforeCheckpoint,
240
- excludeEventId: receipt.checkpointLedgerEventId,
241
207
  });
242
- if (generation.planGeneration !== receipt.planGeneration) {
243
- return {
244
- state: "active_stale_receipt",
245
- message: `Ultragoal ${input.goal.id} receipt generation is stale.`,
246
- goalId: input.goal.id,
247
- };
248
- }
249
- if (hashStructuredValue(event.qualityGateJson) !== receipt.qualityGateHash) {
250
- return {
251
- state: "active_dirty_quality_gate",
252
- message: `Ultragoal ${input.goal.id} receipt quality-gate hash does not match ledger.`,
253
- goalId: input.goal.id,
254
- };
255
- }
256
- if (input.goal.updatedAt !== receipt.verifiedAt) {
257
- return {
258
- state: "active_stale_receipt",
259
- message: `Ultragoal ${input.goal.id} receipt target changed after verification.`,
260
- goalId: input.goal.id,
261
- };
208
+ if (baseDiagnostic) return baseDiagnostic;
209
+ if (receipt.validationBatch?.role === "batch-close") {
210
+ for (const memberId of receipt.validationBatch.memberIds) {
211
+ const member = input.plan.goals.find(goal => goal.id === memberId);
212
+ if (
213
+ !member?.validationBatch ||
214
+ member.validationBatch.metadataHash !== receipt.validationBatch.memberMetadataHashes[memberId]
215
+ ) {
216
+ return {
217
+ state: "active_stale_receipt",
218
+ message: `Ultragoal ${input.goal.id} batch-close receipt has stale member metadata for ${memberId}.`,
219
+ goalId: input.goal.id,
220
+ };
221
+ }
222
+ if (memberId === receipt.validationBatch.finalGoalId) continue;
223
+ const memberReceipt = member.completionVerification;
224
+ if (memberReceipt?.validationBatch?.role !== "deferred-member") {
225
+ return {
226
+ state: "active_missing_final_receipt",
227
+ message: `Ultragoal ${input.goal.id} batch-close receipt requires deferred member receipt for ${memberId}.`,
228
+ goalId: input.goal.id,
229
+ };
230
+ }
231
+ const memberDiagnostic = validateDeferredMemberReceiptFresh({
232
+ plan: input.plan,
233
+ ledger: input.ledger,
234
+ goal: member,
235
+ receipt: memberReceipt,
236
+ receiptKind: "per-goal",
237
+ requireClose: false,
238
+ });
239
+ if (memberDiagnostic.state !== "active_verified_complete") return memberDiagnostic;
240
+ if (
241
+ receipt.validationBatch.memberReceiptIds[memberId] !== memberReceipt.receiptId ||
242
+ receipt.validationBatch.memberChangeSetHashes[memberId] !== memberReceipt.validationBatch.changeSetHash
243
+ ) {
244
+ return {
245
+ state: "active_stale_receipt",
246
+ message: `Ultragoal ${input.goal.id} batch-close receipt is stale for deferred member ${memberId}.`,
247
+ goalId: input.goal.id,
248
+ };
249
+ }
250
+ }
262
251
  }
263
252
  if (input.receiptKind === "final-aggregate") {
264
253
  const incomplete = requiredGoals(input.plan).filter(goal => goal.status !== "complete");
@@ -0,0 +1,315 @@
1
+ import * as crypto from "node:crypto";
2
+ import type {
3
+ UltragoalCompletionVerification,
4
+ UltragoalGoal,
5
+ UltragoalGoalStatus,
6
+ UltragoalLedgerEvent,
7
+ UltragoalPlan,
8
+ UltragoalReceiptKind,
9
+ } from "./ultragoal-runtime";
10
+
11
+ export type UltragoalReceiptFreshnessDiagnostic = {
12
+ state:
13
+ | "inactive"
14
+ | "unrelated_goal"
15
+ | "active_verified_complete"
16
+ | "active_missing_receipt"
17
+ | "active_stale_receipt"
18
+ | "active_missing_final_receipt"
19
+ | "active_dirty_quality_gate"
20
+ | "active_review_blocked_unrecorded"
21
+ | "active_review_blocked_recorded"
22
+ | "unreadable_fail_closed";
23
+ message: string;
24
+ goalId?: string;
25
+ };
26
+ function stableStructuredValue(value: unknown): unknown {
27
+ if (Array.isArray(value)) return value.map(item => stableStructuredValue(item));
28
+ if (typeof value !== "object" || value === null) return value;
29
+ const record = value as Record<string, unknown>;
30
+ const sorted: Record<string, unknown> = {};
31
+ for (const key of Object.keys(record).sort()) {
32
+ const item = record[key];
33
+ if (item !== undefined) sorted[key] = stableStructuredValue(item);
34
+ }
35
+ return sorted;
36
+ }
37
+
38
+ function hashStructuredValue(value: unknown): string {
39
+ return crypto
40
+ .createHash("sha256")
41
+ .update(JSON.stringify(stableStructuredValue(value)))
42
+ .digest("hex");
43
+ }
44
+
45
+ export function requiredUltragoalGoals(plan: UltragoalPlan): UltragoalGoal[] {
46
+ return plan.goals.filter(goal => goal.status !== "superseded");
47
+ }
48
+
49
+ export function receiptRelevantGoals(
50
+ plan: UltragoalPlan,
51
+ goal: UltragoalGoal,
52
+ receiptKind: UltragoalReceiptKind,
53
+ ): UltragoalGoal[] {
54
+ if (goal.validationBatch?.finalGoalId === goal.id) {
55
+ return goal.validationBatch.memberIds.map(memberId => {
56
+ const member = plan.goals.find(item => item.id === memberId);
57
+ if (!member)
58
+ throw new Error(`validation batch ${goal.validationBatch?.batchId} references missing goal ${memberId}`);
59
+ return member;
60
+ });
61
+ }
62
+ return receiptKind === "final-aggregate" ? requiredUltragoalGoals(plan) : [goal];
63
+ }
64
+
65
+ function ledgerEventId(event: UltragoalLedgerEvent): string | null {
66
+ return typeof event.eventId === "string" && event.eventId.trim().length > 0 ? event.eventId : null;
67
+ }
68
+
69
+ function isReceiptFreshnessBookkeepingEvent(event: UltragoalLedgerEvent): boolean {
70
+ return event.event === "nudge";
71
+ }
72
+
73
+ function latestRelevantLedgerEventId(
74
+ ledger: readonly UltragoalLedgerEvent[],
75
+ relevantGoalIds: readonly string[],
76
+ excludeEventId?: string,
77
+ ): string | null {
78
+ const relevant = new Set(relevantGoalIds);
79
+ for (const event of [...ledger].reverse()) {
80
+ const eventId = ledgerEventId(event);
81
+ if (eventId && eventId === excludeEventId) continue;
82
+ if (isReceiptFreshnessBookkeepingEvent(event)) continue;
83
+ const goalId = typeof event.goalId === "string" ? event.goalId.trim() : "";
84
+ if (goalId && relevant.has(goalId)) return eventId;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function planSnapshotForReceipt(input: {
90
+ plan: UltragoalPlan;
91
+ goal: UltragoalGoal;
92
+ beforeStatus: UltragoalGoalStatus;
93
+ targetGoalUpdatedAt: string;
94
+ receiptKind: UltragoalReceiptKind;
95
+ }): unknown {
96
+ const targetGoalSnapshot = {
97
+ ...input.goal,
98
+ status: input.beforeStatus,
99
+ updatedAt: input.targetGoalUpdatedAt,
100
+ evidence: undefined,
101
+ completedAt: undefined,
102
+ completionVerification: undefined,
103
+ };
104
+ const goals =
105
+ input.receiptKind === "final-aggregate"
106
+ ? input.plan.goals.map(goal => ({
107
+ ...goal,
108
+ status: goal.id === input.goal.id ? input.beforeStatus : goal.status,
109
+ updatedAt: goal.id === input.goal.id ? input.targetGoalUpdatedAt : goal.updatedAt,
110
+ evidence: goal.id === input.goal.id ? undefined : goal.evidence,
111
+ completedAt: goal.id === input.goal.id ? undefined : goal.completedAt,
112
+ completionVerification: undefined,
113
+ }))
114
+ : [targetGoalSnapshot];
115
+ return {
116
+ version: input.plan.version,
117
+ brief: input.plan.brief,
118
+ skcGoalMode: input.plan.skcGoalMode,
119
+ skcObjective: input.plan.skcObjective,
120
+ skcObjectiveAliases: input.plan.skcObjectiveAliases,
121
+ createdAt: input.plan.createdAt,
122
+ goals,
123
+ };
124
+ }
125
+
126
+ export function computeUltragoalPlanGeneration(input: {
127
+ plan: UltragoalPlan;
128
+ ledger: readonly UltragoalLedgerEvent[];
129
+ goal: UltragoalGoal;
130
+ receiptKind: UltragoalReceiptKind;
131
+ beforeStatus: UltragoalGoalStatus;
132
+ excludeEventId?: string;
133
+ targetGoalUpdatedAt?: string;
134
+ }): {
135
+ planGeneration: string;
136
+ basis: UltragoalCompletionVerification["basis"];
137
+ } {
138
+ const relevantGoals = receiptRelevantGoals(input.plan, input.goal, input.receiptKind);
139
+ const relevantGoalIds = relevantGoals.map(goal => goal.id);
140
+ const targetGoalUpdatedAt = input.targetGoalUpdatedAt ?? input.goal.updatedAt;
141
+ const planHashBeforeCheckpoint = hashStructuredValue(
142
+ planSnapshotForReceipt({
143
+ plan: input.plan,
144
+ goal: input.goal,
145
+ beforeStatus: input.beforeStatus,
146
+ targetGoalUpdatedAt,
147
+ receiptKind: input.receiptKind,
148
+ }),
149
+ );
150
+ const requiredGoalSetHashBeforeCheckpoint = hashStructuredValue(
151
+ relevantGoals.map(goal => ({
152
+ id: goal.id,
153
+ status: goal.id === input.goal.id ? input.beforeStatus : goal.status,
154
+ updatedAt: goal.id === input.goal.id ? targetGoalUpdatedAt : goal.updatedAt,
155
+ })),
156
+ );
157
+ const basis: UltragoalCompletionVerification["basis"] = {
158
+ planHashBeforeCheckpoint,
159
+ latestRelevantLedgerEventIdBeforeCheckpoint: latestRelevantLedgerEventId(
160
+ input.ledger,
161
+ relevantGoalIds,
162
+ input.excludeEventId,
163
+ ),
164
+ goalUpdatedAtBeforeCheckpoint: targetGoalUpdatedAt,
165
+ relevantGoalIdsBeforeCheckpoint: relevantGoalIds,
166
+ requiredGoalSetHashBeforeCheckpoint,
167
+ };
168
+ return { planGeneration: hashStructuredValue(basis), basis };
169
+ }
170
+
171
+ export function findLedgerReceiptEvent(
172
+ ledger: readonly UltragoalLedgerEvent[],
173
+ receipt: UltragoalCompletionVerification,
174
+ ): UltragoalLedgerEvent | null {
175
+ return (
176
+ ledger.find(event => {
177
+ if (event.eventId !== receipt.checkpointLedgerEventId) return false;
178
+ if (event.event !== "goal_checkpointed") return false;
179
+ if (event.goalId !== receipt.goalId) return false;
180
+ const eventReceipt = event.completionVerification as UltragoalCompletionVerification | undefined;
181
+ return (
182
+ event.status === "complete" &&
183
+ eventReceipt?.receiptId === receipt.receiptId &&
184
+ eventReceipt.receiptKind === receipt.receiptKind &&
185
+ eventReceipt.planGeneration === receipt.planGeneration
186
+ );
187
+ }) ?? null
188
+ );
189
+ }
190
+
191
+ export function validateReceiptFreshBase(input: {
192
+ plan: UltragoalPlan;
193
+ ledger: readonly UltragoalLedgerEvent[];
194
+ goal: UltragoalGoal;
195
+ receipt: UltragoalCompletionVerification;
196
+ receiptKind: UltragoalReceiptKind;
197
+ }): UltragoalReceiptFreshnessDiagnostic | null {
198
+ if (
199
+ input.receipt.schemaVersion !== 1 ||
200
+ input.receipt.goalId !== input.goal.id ||
201
+ input.receipt.receiptKind !== input.receiptKind ||
202
+ !input.receipt.planGeneration ||
203
+ !input.receipt.checkpointLedgerEventId
204
+ ) {
205
+ return {
206
+ state: "active_stale_receipt",
207
+ message: `Ultragoal ${input.goal.id} receipt is malformed or stale.`,
208
+ goalId: input.goal.id,
209
+ };
210
+ }
211
+ const event = findLedgerReceiptEvent(input.ledger, input.receipt);
212
+ if (!event)
213
+ return {
214
+ state: "active_stale_receipt",
215
+ message: `Ultragoal ${input.goal.id} receipt ledger event is missing.`,
216
+ goalId: input.goal.id,
217
+ };
218
+ const generation = computeUltragoalPlanGeneration({
219
+ plan: input.plan,
220
+ ledger: input.ledger,
221
+ goal: input.goal,
222
+ receiptKind: input.receiptKind,
223
+ beforeStatus: input.receipt.goalStatusBeforeCheckpoint,
224
+ excludeEventId: input.receipt.checkpointLedgerEventId,
225
+ });
226
+ if (generation.planGeneration !== input.receipt.planGeneration)
227
+ return {
228
+ state: "active_stale_receipt",
229
+ message: `Ultragoal ${input.goal.id} receipt generation is stale.`,
230
+ goalId: input.goal.id,
231
+ };
232
+ if (hashStructuredValue(event.qualityGateJson) !== input.receipt.qualityGateHash)
233
+ return {
234
+ state: "active_dirty_quality_gate",
235
+ message: `Ultragoal ${input.goal.id} receipt quality-gate hash does not match ledger.`,
236
+ goalId: input.goal.id,
237
+ };
238
+ if (input.goal.updatedAt !== input.receipt.verifiedAt)
239
+ return {
240
+ state: "active_stale_receipt",
241
+ message: `Ultragoal ${input.goal.id} receipt target changed after verification.`,
242
+ goalId: input.goal.id,
243
+ };
244
+ return null;
245
+ }
246
+
247
+ export function findFreshBatchCloseReceipt(input: {
248
+ plan: UltragoalPlan;
249
+ ledger: readonly UltragoalLedgerEvent[];
250
+ deferredGoal: UltragoalGoal;
251
+ deferredReceipt: UltragoalCompletionVerification;
252
+ }): UltragoalCompletionVerification | null {
253
+ const batch = input.deferredReceipt.validationBatch;
254
+ if (batch?.role !== "deferred-member") return null;
255
+ const finalGoal = input.plan.goals.find(goal => goal.id === batch.finalGoalId);
256
+ const finalReceipt = finalGoal?.completionVerification;
257
+ if (!finalGoal || finalReceipt?.validationBatch?.role !== "batch-close") return null;
258
+ if (
259
+ finalReceipt.validationBatch.batchId !== batch.batchId ||
260
+ finalReceipt.validationBatch.memberReceiptIds[input.deferredGoal.id] !== input.deferredReceipt.receiptId
261
+ )
262
+ return null;
263
+ const diagnostic = validateReceiptFreshBase({
264
+ plan: input.plan,
265
+ ledger: input.ledger,
266
+ goal: finalGoal,
267
+ receipt: finalReceipt,
268
+ receiptKind: finalReceipt.receiptKind,
269
+ });
270
+ return diagnostic ? null : finalReceipt;
271
+ }
272
+
273
+ export function validateDeferredMemberReceiptFresh(input: {
274
+ plan: UltragoalPlan;
275
+ ledger: readonly UltragoalLedgerEvent[];
276
+ goal: UltragoalGoal;
277
+ receipt: UltragoalCompletionVerification;
278
+ receiptKind: UltragoalReceiptKind;
279
+ requireClose: boolean;
280
+ }): UltragoalReceiptFreshnessDiagnostic {
281
+ const batch = input.receipt.validationBatch;
282
+ if (
283
+ batch?.role !== "deferred-member" ||
284
+ input.goal.validationBatch?.metadataHash !== batch.metadataHash ||
285
+ input.goal.validationBatch.batchId !== batch.batchId
286
+ ) {
287
+ return {
288
+ state: "active_stale_receipt",
289
+ message: `Ultragoal ${input.goal.id} deferred receipt is malformed or stale.`,
290
+ goalId: input.goal.id,
291
+ };
292
+ }
293
+ const base = validateReceiptFreshBase(input);
294
+ if (base) return base;
295
+ if (
296
+ input.requireClose &&
297
+ !findFreshBatchCloseReceipt({
298
+ plan: input.plan,
299
+ ledger: input.ledger,
300
+ deferredGoal: input.goal,
301
+ deferredReceipt: input.receipt,
302
+ })
303
+ ) {
304
+ return {
305
+ state: "active_missing_final_receipt",
306
+ message: `Ultragoal ${input.goal.id} is deferred to validation batch ${batch.batchId} until final goal ${batch.finalGoalId} closes the batch`,
307
+ goalId: input.goal.id,
308
+ };
309
+ }
310
+ return {
311
+ state: "active_verified_complete",
312
+ message: `Ultragoal ${input.goal.id} has a fresh deferred validation batch receipt.`,
313
+ goalId: input.goal.id,
314
+ };
315
+ }