@zq-silk/yui 0.6.13 → 0.6.14

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 (90) hide show
  1. package/README.md +34 -5
  2. package/dist/cli/commandCatalog.js +173 -57
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +99 -11
  5. package/dist/commands/configCommands.js +521 -171
  6. package/dist/commands/deliveryGuardPreflight.js +2 -2
  7. package/dist/commands/executionAuditCommands.js +56 -3
  8. package/dist/commands/projectCommands.js +504 -2
  9. package/dist/commands/releaseCommands.js +0 -1
  10. package/dist/commands/taskActor.js +17 -0
  11. package/dist/commands/taskBaseCommands.js +29 -0
  12. package/dist/commands/taskCommands.js +577 -126
  13. package/dist/commands/taskContextCommand.js +36 -2
  14. package/dist/commands/taskNextActionCommand.js +48 -3
  15. package/dist/commands/taskPublicationCommands.js +319 -0
  16. package/dist/commands/taskRoleRuntimeStatus.js +83 -59
  17. package/dist/commands/telemetryCommands.js +14 -13
  18. package/dist/config/yuiConfig.js +161 -8
  19. package/dist/context/sessionContextBudget.js +68 -0
  20. package/dist/context/wakeNotification.js +65 -0
  21. package/dist/controller/clientRuntime.js +2 -1
  22. package/dist/controller/ephemeralResourceReaper.js +2 -1
  23. package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
  24. package/dist/controller/jobSupervisor.js +5 -4
  25. package/dist/controller/resourceCleanupLinux.js +6 -6
  26. package/dist/controller/resourceInventoryLinux.js +3 -3
  27. package/dist/controller/runtime.js +88 -10
  28. package/dist/controller/updateReconciliation.js +4 -3
  29. package/dist/doctor/doctor.js +26 -7
  30. package/dist/executor/agentConfigurationCatalog.js +18 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +3 -3
  32. package/dist/lifecycle/contextBudgetRollover.js +81 -0
  33. package/dist/lifecycle/exactRunTerminalization.js +11 -1
  34. package/dist/lifecycle/providerErrorClass.js +33 -12
  35. package/dist/observability/executionAudit.js +214 -6
  36. package/dist/output/table.js +18 -0
  37. package/dist/repository/gitWorkspace.js +92 -0
  38. package/dist/repository/project.js +218 -4
  39. package/dist/repository/taskBaseFreshness.js +318 -0
  40. package/dist/repository/taskWorkspacePreparer.js +16 -2
  41. package/dist/review/deltaRecheck.js +232 -0
  42. package/dist/review/reviewConfig.js +31 -0
  43. package/dist/review/reviewFindingLedger.js +5 -1
  44. package/dist/review/reviewRound.js +156 -1
  45. package/dist/run/providerRetry.js +21 -3
  46. package/dist/run/providerRetryConfig.js +13 -60
  47. package/dist/run/recoveryProjection.js +199 -0
  48. package/dist/runtime/builtinAgentDrivers.js +3 -0
  49. package/dist/runtime/builtinTranscriptUsage.js +76 -32
  50. package/dist/runtime/continuationManager.js +17 -0
  51. package/dist/runtime/index.js +2 -0
  52. package/dist/runtime/launchDiagnostics.js +154 -0
  53. package/dist/runtime/lifecycleReservation.js +13 -0
  54. package/dist/runtime/providerContinuation.js +38 -0
  55. package/dist/runtime/providerContinuationReconciliationService.js +1 -0
  56. package/dist/runtime/providerErrorCodes.js +278 -0
  57. package/dist/runtime/runtimeHealthPolicy.js +20 -0
  58. package/dist/runtime/runtimeObservation.js +1 -0
  59. package/dist/runtime/runtimeProjection.js +115 -23
  60. package/dist/runtime/tmuxAdapters.js +242 -48
  61. package/dist/scheduler/activeRoleRunDelivery.js +139 -3
  62. package/dist/scheduler/activeTaskProgress.js +4 -3
  63. package/dist/scheduler/leaderWakeupProcessor.js +105 -15
  64. package/dist/scheduler/roleRunLiveness.js +2 -1
  65. package/dist/scheduler/roleRunStall.js +3 -2
  66. package/dist/scheduler/taskWake.js +72 -0
  67. package/dist/scheduler/wakeReason.js +64 -0
  68. package/dist/scheduler/wakeupQueue.js +2 -1
  69. package/dist/setup/setupCommand.js +1 -1
  70. package/dist/storage/migration/productionRegistry.js +325 -1
  71. package/dist/storage/sqliteSchema.js +61 -2
  72. package/dist/storage/sqliteStore.js +129 -2
  73. package/dist/storage/storeRpc.js +1 -0
  74. package/dist/storage/taskStore.js +262 -5
  75. package/dist/storage/upgrade/recordVersions.js +6 -1
  76. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  77. package/dist/task/completionReadiness.js +282 -0
  78. package/dist/task/publicationReference.js +123 -0
  79. package/dist/task/taskRecordReference.js +3 -1
  80. package/dist/telemetry/telemetryConfig.js +23 -18
  81. package/dist/telemetry/telemetryWiring.js +8 -8
  82. package/dist/tmux/tmuxManager.js +50 -9
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/view.js +18 -0
  85. package/dist/web/webSnapshot.js +100 -10
  86. package/i18n/README.zh-CN.md +5 -5
  87. package/package.json +1 -1
  88. package/skills/yui-leader/SKILL.md +49 -10
  89. package/skills/yui-operator/SKILL.md +13 -3
  90. package/skills/yui-worker/SKILL.md +8 -0
@@ -5,6 +5,7 @@ import { isDeepStrictEqual } from "node:util";
5
5
  import { validateConfiguredAgent } from "../agent/agent.js";
6
6
  import { validateCapabilityGrant } from "../grant/capabilityGrant.js";
7
7
  import { validateReleaseWorkflow } from "../release/releaseWorkflow.js";
8
+ import { publicationExternalKey, validatePublicationReference } from "../task/publicationReference.js";
8
9
  import { reconciliationIntervalMilliseconds, resolveLeaderNextActionMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode } from "../config/yuiConfig.js";
9
10
  import { resolveTimeZone } from "../output/timePresentation.js";
10
11
  import { mailboxBatches, consumePendingBatch, mailboxHasWork, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
@@ -16,6 +17,7 @@ import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from
16
17
  import { FileSessionOwnerRegistry } from "../runtime/sessionOwnerRegistry.js";
17
18
  import { validateReviewConfig } from "../review/reviewConfig.js";
18
19
  import { validateReviewRound } from "../review/reviewRound.js";
20
+ import { reviewFindingLedgerMode } from "../review/reviewFindingLedger.js";
19
21
  import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
20
22
  import { assertProjectCatalog, validateProject } from "../repository/project.js";
21
23
  import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
@@ -27,6 +29,7 @@ import { CURRENT_DURABLE_JOB_SCHEMA_VERSION, validDurableJobTransition, validate
27
29
  import { validateGlobalRole, validateTaskRole } from "../role/role.js";
28
30
  import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../scheduler/leaderFailure.js";
29
31
  import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../scheduler/operatorNotification.js";
32
+ import { CURRENT_TASK_WAKE_SCHEMA_VERSION, validateTaskWake } from "../scheduler/taskWake.js";
30
33
  import { validateTask } from "../task/task.js";
31
34
  import { formatAgentRunReceiptId, TASK_RECORD_ID_PREFIXES, validateTaskRecordReference } from "../task/taskRecordReference.js";
32
35
  import { workItemExecutionGroupById, validateWorkItem } from "../workItem/workItem.js";
@@ -51,7 +54,7 @@ export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
51
54
  * below, but are not independent record-axis families.
52
55
  */
53
56
  export const CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION = 2;
54
- export const CURRENT_PROJECT_SCHEMA_VERSION = 3;
57
+ export const CURRENT_PROJECT_SCHEMA_VERSION = 4;
55
58
  export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
56
59
  export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
57
60
  export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
@@ -71,6 +74,7 @@ export const CURRENT_EVENT_SCHEMA_VERSION = 2;
71
74
  export const CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION = 1;
72
75
  export const CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION = 1;
73
76
  export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 2;
77
+ export const CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION = 1;
74
78
  export const CURRENT_ROLE_AGENT_SESSION_SCHEMA_VERSION = 3;
75
79
  export const CURRENT_PENDING_WAKEUP_SCHEMA_VERSION = 1;
76
80
  const STORAGE_LOCK_DIRECTORY = ".state.lock";
@@ -104,7 +108,7 @@ export function executionLaneActiveRunKeyParts(key) {
104
108
  }
105
109
  }
106
110
  /** The schema version of each persisted `state.json#/tasks/*` aggregate. */
107
- export const CURRENT_STORED_TASK_SCHEMA_VERSION = 16;
111
+ export const CURRENT_STORED_TASK_SCHEMA_VERSION = 17;
108
112
  /**
109
113
  * Persisted nested-record versions consumed by this store's strict parser.
110
114
  * Keep these named at the storage boundary so the upgrade record-axis map can
@@ -406,7 +410,8 @@ export class FileTaskStore {
406
410
  task: {
407
411
  id: aggregate.task.id,
408
412
  status: aggregate.task.status,
409
- projectBindings: aggregate.task.projectBindings
413
+ projectBindings: aggregate.task.projectBindings,
414
+ requireIntegration: aggregate.task.requireIntegration
410
415
  },
411
416
  workItems: values(aggregate.workItems, "id"),
412
417
  changeSets: values(aggregate.changeSets, "id"),
@@ -419,6 +424,31 @@ export class FileTaskStore {
419
424
  leaderRuns: agentRuns.filter((run) => run.roleName === "leader")
420
425
  };
421
426
  }
427
+ readCompletionReadinessFacts(taskId) {
428
+ const base = this.readNextActionFacts(taskId);
429
+ if (base === null)
430
+ return null;
431
+ const aggregate = this.#state().tasks[taskId];
432
+ const ledgerMode = reviewFindingLedgerMode(this.#state().config);
433
+ // The file-rollback backend does not persist the finding ledger. Under
434
+ // `enforce` the completion gate must fail closed with the same bounded
435
+ // diagnosis as `listReviewFindings` instead of silently treating
436
+ // "unsupported" as "no open findings".
437
+ if (ledgerMode === "enforce") {
438
+ throw new StorageRecordError(`Review findings require the SQLite backend (yui.db); migrate this Home with \`yui update\` before using the finding ledger on Task ${taskId}.`);
439
+ }
440
+ return {
441
+ ...base,
442
+ managedWorkspaces: values(aggregate.managedWorkspaces, (workspace) => managedWorkspaceKey(workspace.owner)),
443
+ durableJobs: values(aggregate.durableJobs, "id"),
444
+ integrationQueueEntries: values(aggregate.integrationQueue, "id"),
445
+ // The SQLite backend supplies real findings; the file backend never
446
+ // reaches this point under `enforce` (throw above).
447
+ reviewFindings: [],
448
+ reviewFindingLedgerMode: ledgerMode,
449
+ events: values(aggregate.events, "id")
450
+ };
451
+ }
422
452
  getReviewConfig() {
423
453
  return optional(this.#state().config.review);
424
454
  }
@@ -1246,6 +1276,28 @@ export class FileTaskStore {
1246
1276
  });
1247
1277
  }
1248
1278
  listEvents(taskId) { return values(this.#requireTask(taskId).events, "id"); }
1279
+ nextTaskWakeId(taskId) {
1280
+ return this.#nextTaskRecordId(taskId, "taskWake");
1281
+ }
1282
+ peekNextTaskWakeId(taskId) {
1283
+ return this.#peekTaskRecordId(taskId, "taskWake");
1284
+ }
1285
+ saveTaskWake(taskId, wake) {
1286
+ if (wake.taskId !== taskId) {
1287
+ throw new StorageRecordError(`Task wake belongs to another Task: ${wake.taskId}`);
1288
+ }
1289
+ this.#mutate((state) => {
1290
+ const aggregate = requireTaskFromState(state, taskId);
1291
+ observeTaskRecordId(aggregate, "taskWake", wake.id);
1292
+ aggregate.wakes[wake.id] = wake;
1293
+ });
1294
+ }
1295
+ getTaskWake(taskId, wakeId) {
1296
+ return this.#state().tasks[taskId]?.wakes[wakeId] ?? null;
1297
+ }
1298
+ listTaskWakes(taskId) {
1299
+ return values(this.#requireTask(taskId).wakes, "id");
1300
+ }
1249
1301
  removeEvents(taskId, eventIds) {
1250
1302
  if (eventIds.length === 0)
1251
1303
  return 0;
@@ -1317,6 +1369,62 @@ export class FileTaskStore {
1317
1369
  getReleaseWorkflow(taskId, workflowId) {
1318
1370
  return this.#requireTask(taskId).releaseWorkflows[workflowId] ?? null;
1319
1371
  }
1372
+ nextPublicationReferenceId(taskId) {
1373
+ return this.#nextTaskRecordId(taskId, "publicationReference");
1374
+ }
1375
+ savePublicationReference(taskId, reference) {
1376
+ const stored = storedPublicationReference(reference);
1377
+ if (stored.taskId !== taskId) {
1378
+ throw new StorageRecordError(`Publication reference belongs to another Task: ${stored.taskId}`);
1379
+ }
1380
+ this.#mutate((state) => {
1381
+ const aggregate = requireTaskFromState(state, taskId);
1382
+ const existing = aggregate.publicationReferences[stored.id];
1383
+ if (existing !== undefined) {
1384
+ if (!isDeepStrictEqual(existing, stored)) {
1385
+ throw new StorageRecordError(`Publication reference cannot be overwritten: ${taskId}/${stored.id}`);
1386
+ }
1387
+ }
1388
+ else {
1389
+ const key = publicationExternalKey(stored);
1390
+ const sameExternal = [];
1391
+ for (const [otherTaskId, otherAggregate] of Object.entries(state.tasks)) {
1392
+ for (const candidate of Object.values(otherAggregate.publicationReferences)) {
1393
+ if (publicationExternalKey(candidate) === key) {
1394
+ if (otherTaskId !== taskId) {
1395
+ throw new StorageRecordError(`Publication external identity already belongs to Task ${otherTaskId}: ${key}.`);
1396
+ }
1397
+ sameExternal.push(candidate);
1398
+ }
1399
+ }
1400
+ }
1401
+ if (sameExternal.length > 0) {
1402
+ const latest = latestPublicationReference(sameExternal);
1403
+ if (stored.supersedes !== latest.id) {
1404
+ throw new StorageRecordError(`Publication external identity conflicts with ${latest.id}; `
1405
+ + `explicitly supersede that record: ${key}.`);
1406
+ }
1407
+ }
1408
+ else if (stored.supersedes !== undefined) {
1409
+ throw new StorageRecordError(`Publication supersedes target has a different external identity: ${stored.supersedes}.`);
1410
+ }
1411
+ observeTaskRecordId(aggregate, "publicationReference", stored.id);
1412
+ }
1413
+ aggregate.publicationReferences[stored.id] = stored;
1414
+ });
1415
+ }
1416
+ listPublicationReferences(taskId) {
1417
+ return values(this.#requireTask(taskId).publicationReferences, "id");
1418
+ }
1419
+ getPublicationReference(taskId, referenceId) {
1420
+ return this.#requireTask(taskId).publicationReferences[referenceId] ?? null;
1421
+ }
1422
+ findPublicationReferenceByExternalKey(externalKey) {
1423
+ const matches = Object.values(this.#state().tasks)
1424
+ .flatMap((aggregate) => Object.values(aggregate.publicationReferences))
1425
+ .filter((reference) => publicationExternalKey(reference) === externalKey);
1426
+ return matches.length === 0 ? null : latestPublicationReference(matches);
1427
+ }
1320
1428
  // -- Gate artifacts (Issue 08) ---------------------------------------------
1321
1429
  // FileTaskStore delegates to the file-backed artifact namespace under
1322
1430
  // `<home>/artifacts/gates/`. This is the transitional path for Homes that
@@ -1820,8 +1928,10 @@ function emptyStoredTask(task) {
1820
1928
  decisions: {},
1821
1929
  milestones: {},
1822
1930
  events: {},
1931
+ wakes: {},
1823
1932
  capabilityGrants: {},
1824
1933
  releaseWorkflows: {},
1934
+ publicationReferences: {},
1825
1935
  leaderFailure: null,
1826
1936
  operatorNotification: null
1827
1937
  };
@@ -1841,8 +1951,10 @@ function emptyTaskIdHighWaterMarks() {
1841
1951
  decision: 0,
1842
1952
  milestone: 0,
1843
1953
  event: 0,
1954
+ taskWake: 0,
1844
1955
  capabilityGrant: 0,
1845
- releaseWorkflow: 0
1956
+ releaseWorkflow: 0,
1957
+ publicationReference: 0
1846
1958
  };
1847
1959
  }
1848
1960
  function nextTaskRecordSequence(aggregate, taskId, kind) {
@@ -2033,8 +2145,10 @@ function parseStoredTask(value, taskId) {
2033
2145
  "decisions",
2034
2146
  "milestones",
2035
2147
  "events",
2148
+ "wakes",
2036
2149
  "capabilityGrants",
2037
2150
  "releaseWorkflows",
2151
+ "publicationReferences",
2038
2152
  "leaderFailure",
2039
2153
  "operatorNotification"
2040
2154
  ], `Task aggregate ${taskId}`);
@@ -2190,6 +2304,14 @@ function parseStoredTask(value, taskId) {
2190
2304
  }
2191
2305
  return event;
2192
2306
  }, "events");
2307
+ parseMap(aggregate.wakes, (record, key) => {
2308
+ const wake = identified(record, CURRENT_TASK_WAKE_SCHEMA_VERSION, "id", key, "Task wake");
2309
+ if (wake.taskId !== taskId) {
2310
+ throw new StorageRecordError(`Task wake belongs to another Task: ${wake.taskId}`);
2311
+ }
2312
+ validateTaskWake(wake);
2313
+ return wake;
2314
+ }, "wakes");
2193
2315
  parseMap(aggregate.capabilityGrants, (record, key) => {
2194
2316
  const grant = storedCapabilityGrant(record);
2195
2317
  if (grant.id !== key) {
@@ -2210,6 +2332,16 @@ function parseStoredTask(value, taskId) {
2210
2332
  }
2211
2333
  return workflow;
2212
2334
  }, "releaseWorkflows");
2335
+ parseMap(aggregate.publicationReferences, (record, key) => {
2336
+ const reference = storedPublicationReference(record);
2337
+ if (reference.id !== key) {
2338
+ throw new StorageRecordError(`Publication reference identity is inconsistent: ${key}.`);
2339
+ }
2340
+ if (reference.taskId !== taskId) {
2341
+ throw new StorageRecordError(`Publication reference belongs to another Task: ${reference.taskId}`);
2342
+ }
2343
+ return reference;
2344
+ }, "publicationReferences");
2213
2345
  for (const [key, label] of [["leaderFailure", "Leader failure"], ["operatorNotification", "Operator notification"]]) {
2214
2346
  const record = aggregate[key];
2215
2347
  if (record !== null) {
@@ -2239,8 +2371,10 @@ function validateTaskIdHighWaterCoverage(aggregate, taskId) {
2239
2371
  decision: aggregate.decisions,
2240
2372
  milestone: aggregate.milestones,
2241
2373
  event: aggregate.events,
2374
+ taskWake: aggregate.wakes,
2242
2375
  capabilityGrant: aggregate.capabilityGrants,
2243
- releaseWorkflow: aggregate.releaseWorkflows
2376
+ releaseWorkflow: aggregate.releaseWorkflows,
2377
+ publicationReference: aggregate.publicationReferences
2244
2378
  };
2245
2379
  for (const kind of Object.keys(TASK_RECORD_ID_PREFIXES)) {
2246
2380
  const pattern = new RegExp(`^${TASK_RECORD_ID_PREFIXES[kind]}-(\\d+)$`);
@@ -2649,6 +2783,101 @@ export function storedReleaseWorkflow(value) {
2649
2783
  throw new StorageRecordError(error instanceof Error ? error.message : String(error));
2650
2784
  }
2651
2785
  }
2786
+ export function storedPublicationReference(value) {
2787
+ const reference = versioned(value, CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, "Publication reference");
2788
+ const fields = [
2789
+ "schemaVersion", "id", "taskId", "projectId", "provider", "repository",
2790
+ "externalKind", "externalId", "state", "verification", "recordedBy",
2791
+ "source", "createdAt"
2792
+ ];
2793
+ if (reference.externalUrl !== undefined)
2794
+ fields.push("externalUrl");
2795
+ if (reference.title !== undefined)
2796
+ fields.push("title");
2797
+ if (reference.sourceBranch !== undefined)
2798
+ fields.push("sourceBranch");
2799
+ if (reference.targetBranch !== undefined)
2800
+ fields.push("targetBranch");
2801
+ if (reference.localCommit !== undefined)
2802
+ fields.push("localCommit");
2803
+ if (reference.remoteCommit !== undefined)
2804
+ fields.push("remoteCommit");
2805
+ if (reference.evidence !== undefined)
2806
+ fields.push("evidence");
2807
+ if (reference.supersedes !== undefined)
2808
+ fields.push("supersedes");
2809
+ if (reference.mergedAt !== undefined)
2810
+ fields.push("mergedAt");
2811
+ exact(reference, fields, "Publication reference");
2812
+ requireRecordIdentity(reference.id, "Publication reference id");
2813
+ requireRecordIdentity(reference.taskId, "Publication reference Task id");
2814
+ validateTaskRecordReference({ taskId: reference.taskId, localId: reference.id }, "publicationReference");
2815
+ requireRecordIdentity(reference.projectId, "Publication reference Project id");
2816
+ requireNormalizedText(reference.provider, "Publication reference provider");
2817
+ requireNormalizedText(reference.repository, "Publication reference repository");
2818
+ requireNormalizedText(reference.externalKind, "Publication reference external kind");
2819
+ requireNormalizedText(reference.externalId, "Publication reference external id");
2820
+ if (reference.externalUrl !== undefined) {
2821
+ requireNormalizedText(reference.externalUrl, "Publication reference external URL");
2822
+ }
2823
+ if (reference.title !== undefined) {
2824
+ requireNormalizedText(reference.title, "Publication reference title");
2825
+ }
2826
+ if (reference.sourceBranch !== undefined) {
2827
+ requireNormalizedText(reference.sourceBranch, "Publication reference source branch");
2828
+ }
2829
+ if (reference.targetBranch !== undefined) {
2830
+ requireNormalizedText(reference.targetBranch, "Publication reference target branch");
2831
+ }
2832
+ if (reference.localCommit !== undefined) {
2833
+ requireNormalizedText(reference.localCommit, "Publication reference local commit");
2834
+ }
2835
+ if (reference.remoteCommit !== undefined) {
2836
+ requireNormalizedText(reference.remoteCommit, "Publication reference remote commit");
2837
+ }
2838
+ requireNormalizedText(reference.state, "Publication reference state");
2839
+ requireNormalizedText(reference.verification, "Publication reference verification");
2840
+ if (reference.evidence !== undefined) {
2841
+ requireNormalizedText(reference.evidence, "Publication reference evidence");
2842
+ }
2843
+ if (reference.supersedes !== undefined) {
2844
+ requireNormalizedText(reference.supersedes, "Publication reference supersedes id");
2845
+ }
2846
+ requireNormalizedText(reference.recordedBy, "Publication reference recordedBy");
2847
+ requireNormalizedText(reference.source, "Publication reference source");
2848
+ if (reference.mergedAt !== undefined) {
2849
+ requireTimestamp(reference.mergedAt, "Publication reference mergedAt");
2850
+ }
2851
+ requireTimestamp(reference.createdAt, "Publication reference createdAt");
2852
+ try {
2853
+ return validatePublicationReference(reference);
2854
+ }
2855
+ catch (error) {
2856
+ throw new StorageRecordError(error instanceof Error ? error.message : String(error));
2857
+ }
2858
+ }
2859
+ function latestPublicationReference(references) {
2860
+ const byId = new Map(references.map((reference) => [reference.id, reference]));
2861
+ const superseded = new Set(references
2862
+ .map((reference) => reference.supersedes)
2863
+ .filter((id) => id !== undefined));
2864
+ const roots = references.filter((reference) => !superseded.has(reference.id));
2865
+ const latest = roots.length === 1 ? roots[0] : [...byId.values()].sort((left, right) => (right.id.localeCompare(left.id, undefined, { numeric: true })))[0];
2866
+ let current = latest;
2867
+ const seen = new Set();
2868
+ while (current.supersedes !== undefined) {
2869
+ if (seen.has(current.id)) {
2870
+ throw new StorageRecordError(`Publication supersession cycle at ${current.id}.`);
2871
+ }
2872
+ seen.add(current.id);
2873
+ const previous = byId.get(current.supersedes);
2874
+ if (previous === undefined) {
2875
+ throw new StorageRecordError(`Publication supersession target not found: ${current.id}/${current.supersedes}.`);
2876
+ }
2877
+ current = previous;
2878
+ }
2879
+ return latest;
2880
+ }
2652
2881
  function storedReleaseWorkflowSource(source) {
2653
2882
  const value = object(source, "Release workflow source");
2654
2883
  const sourceFields = ["repository", "commit"];
@@ -3097,10 +3326,38 @@ function validateCanonicalTaskReferences(state, aggregate) {
3097
3326
  }
3098
3327
  }
3099
3328
  }
3329
+ for (const reference of Object.values(aggregate.publicationReferences)) {
3330
+ if (!boundProjects.has(reference.projectId)) {
3331
+ throw new StorageRecordError(`Publication reference Project does not match Task: ${reference.id}.`);
3332
+ }
3333
+ if (reference.supersedes !== undefined) {
3334
+ const target = aggregate.publicationReferences[reference.supersedes];
3335
+ if (target === undefined) {
3336
+ throw new StorageRecordError(`Publication reference supersedes target not found: ${reference.id}/${reference.supersedes}.`);
3337
+ }
3338
+ if (publicationExternalKey(target) !== publicationExternalKey(reference)) {
3339
+ throw new StorageRecordError(`Publication reference supersedes a different external identity: ${reference.id}/${reference.supersedes}.`);
3340
+ }
3341
+ }
3342
+ }
3343
+ assertPublicationExternalIdentityUniqueness(state);
3100
3344
  for (const workspace of Object.values(aggregate.managedWorkspaces)) {
3101
3345
  assertManagedWorkspaceReferences(aggregate, workspace, "Managed workspace");
3102
3346
  }
3103
3347
  }
3348
+ function assertPublicationExternalIdentityUniqueness(state) {
3349
+ const owners = new Map();
3350
+ for (const aggregate of Object.values(state.tasks)) {
3351
+ for (const reference of Object.values(aggregate.publicationReferences)) {
3352
+ const key = publicationExternalKey(reference);
3353
+ const owner = owners.get(key);
3354
+ if (owner !== undefined && owner !== aggregate.task.id) {
3355
+ throw new StorageRecordError(`Publication external identity belongs to multiple Tasks: ${key}.`);
3356
+ }
3357
+ owners.set(key, aggregate.task.id);
3358
+ }
3359
+ }
3360
+ }
3104
3361
  /**
3105
3362
  * Enforce the owner-specific scope at the storage boundary. The map key and
3106
3363
  * foreign-record checks prevent dangling identities; these checks prevent a
@@ -16,9 +16,10 @@
16
16
  * transform in the production migration graph.
17
17
  */
18
18
  import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
19
- import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_STORED_TASK_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
19
+ import { CURRENT_AGENT_RUN_SCHEMA_VERSION, CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION, CURRENT_AGENT_PROFILE_SCHEMA_VERSION, CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION, CURRENT_CHANGE_SET_SCHEMA_VERSION, CURRENT_DECISION_SCHEMA_VERSION, CURRENT_EVENT_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SCHEMA_VERSION, CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_INPUT_REQUEST_SCHEMA_VERSION, CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION, CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION, CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION, CURRENT_MESSAGE_SCHEMA_VERSION, CURRENT_MILESTONE_SCHEMA_VERSION, CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, CURRENT_PROJECT_SCHEMA_VERSION, CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, CURRENT_REVIEW_ROUND_SCHEMA_VERSION, CURRENT_STORED_TASK_SCHEMA_VERSION, CURRENT_TASK_BRIEF_SCHEMA_VERSION, CURRENT_TASK_ROLE_SCHEMA_VERSION, CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION, CURRENT_TASK_SCHEMA_VERSION, CURRENT_WORK_ITEM_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION } from "../taskStore.js";
20
20
  import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../../scheduler/leaderFailure.js";
21
21
  import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../../scheduler/operatorNotification.js";
22
+ import { CURRENT_TASK_WAKE_SCHEMA_VERSION } from "../../scheduler/taskWake.js";
22
23
  import { CURRENT_DURABLE_JOB_SCHEMA_VERSION } from "../../job/durableJob.js";
23
24
  function descriptor(version, path) {
24
25
  return Object.freeze({ version, path });
@@ -55,8 +56,10 @@ const EXPECTED_DIRECT_RECORD_LOCATORS = Object.freeze({
55
56
  decision: "state.json#/tasks/*/decisions",
56
57
  milestone: "state.json#/tasks/*/milestones",
57
58
  event: "state.json#/tasks/*/events",
59
+ taskWake: "state.json#/tasks/*/wakes",
58
60
  capabilityGrant: "state.json#/tasks/*/capabilityGrants",
59
61
  releaseWorkflow: "state.json#/tasks/*/releaseWorkflows",
62
+ publicationReference: "state.json#/tasks/*/publicationReferences",
60
63
  leaderFailure: "state.json#/tasks/*/leaderFailure",
61
64
  operatorNotification: "state.json#/tasks/*/operatorNotification",
62
65
  workMailbox: "state.json#/mailboxes"
@@ -108,8 +111,10 @@ function getCurrentRecordDescriptors() {
108
111
  decision: descriptor(CURRENT_DECISION_SCHEMA_VERSION, "state.json#/tasks/*/decisions"),
109
112
  milestone: descriptor(CURRENT_MILESTONE_SCHEMA_VERSION, "state.json#/tasks/*/milestones"),
110
113
  event: descriptor(CURRENT_EVENT_SCHEMA_VERSION, "state.json#/tasks/*/events"),
114
+ taskWake: descriptor(CURRENT_TASK_WAKE_SCHEMA_VERSION, "state.json#/tasks/*/wakes"),
111
115
  capabilityGrant: descriptor(CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION, "state.json#/tasks/*/capabilityGrants"),
112
116
  releaseWorkflow: descriptor(CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION, "state.json#/tasks/*/releaseWorkflows"),
117
+ publicationReference: descriptor(CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION, "state.json#/tasks/*/publicationReferences"),
113
118
  leaderFailure: descriptor(CURRENT_LEADER_FAILURE_SCHEMA_VERSION, "state.json#/tasks/*/leaderFailure"),
114
119
  operatorNotification: descriptor(CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION, "state.json#/tasks/*/operatorNotification"),
115
120
  workMailbox: descriptor(CURRENT_WORK_MAILBOX_SCHEMA_VERSION, "state.json#/mailboxes")
@@ -189,11 +189,13 @@ function asStoredTask(value) {
189
189
  decisions: asObjectMap(record.decisions),
190
190
  milestones: asObjectMap(record.milestones),
191
191
  events: asObjectMap(record.events),
192
+ wakes: asObjectMap(record.wakes),
192
193
  leaderFailure: asNullableObject(record.leaderFailure),
193
194
  operatorNotification: asNullableObject(record.operatorNotification),
194
195
  idHighWaterMarks: asObject(record.idHighWaterMarks),
195
196
  capabilityGrants: asObjectMap(record.capabilityGrants),
196
- releaseWorkflows: asObjectMap(record.releaseWorkflows)
197
+ releaseWorkflows: asObjectMap(record.releaseWorkflows),
198
+ publicationReferences: asObjectMap(record.publicationReferences)
197
199
  };
198
200
  }
199
201
  function tasksOf(state) {
@@ -335,6 +337,9 @@ export function populateSqliteFromState(home, state, databaseFilename) {
335
337
  for (const event of Object.values(stored.events)) {
336
338
  store.saveEvent(taskId, event);
337
339
  }
340
+ for (const wake of Object.values(stored.wakes)) {
341
+ store.saveTaskWake(taskId, wake);
342
+ }
338
343
  if (stored.leaderFailure !== null) {
339
344
  store.saveLeaderFailure(stored.leaderFailure);
340
345
  }
@@ -354,6 +359,9 @@ export function populateSqliteFromState(home, state, databaseFilename) {
354
359
  for (const workflow of Object.values(stored.releaseWorkflows)) {
355
360
  store.saveReleaseWorkflow(taskId, workflow);
356
361
  }
362
+ for (const reference of Object.values(stored.publicationReferences)) {
363
+ store.savePublicationReference(taskId, reference);
364
+ }
357
365
  }
358
366
  // Work mailboxes.
359
367
  for (const mailbox of Object.values(asObjectMap(state.mailboxes))) {
@@ -456,10 +464,12 @@ export function computeStateFamilyChecksums(state) {
456
464
  const decisions = [];
457
465
  const milestones = [];
458
466
  const events = [];
467
+ const wakes = [];
459
468
  const leaderFailures = [];
460
469
  const operatorNotifications = [];
461
470
  const capabilityGrants = [];
462
471
  const releaseWorkflows = [];
472
+ const publicationReferences = [];
463
473
  for (const stored of Object.values(tasks)) {
464
474
  taskRecords.push(stored.task);
465
475
  if (stored.brief !== null)
@@ -492,12 +502,14 @@ export function computeStateFamilyChecksums(state) {
492
502
  decisions.push(...Object.values(stored.decisions));
493
503
  milestones.push(...Object.values(stored.milestones));
494
504
  events.push(...Object.values(stored.events));
505
+ wakes.push(...Object.values(stored.wakes));
495
506
  if (stored.leaderFailure !== null)
496
507
  leaderFailures.push(stored.leaderFailure);
497
508
  if (stored.operatorNotification !== null)
498
509
  operatorNotifications.push(stored.operatorNotification);
499
510
  capabilityGrants.push(...Object.values(stored.capabilityGrants));
500
511
  releaseWorkflows.push(...Object.values(stored.releaseWorkflows));
512
+ publicationReferences.push(...Object.values(stored.publicationReferences));
501
513
  }
502
514
  checksums.task = hashRecords(taskRecords);
503
515
  checksums.taskBrief = hashRecords(briefs);
@@ -518,10 +530,12 @@ export function computeStateFamilyChecksums(state) {
518
530
  checksums.decision = hashRecords(decisions);
519
531
  checksums.milestone = hashRecords(milestones);
520
532
  checksums.event = hashRecords(events);
533
+ checksums.taskWake = hashRecords(wakes);
521
534
  checksums.leaderFailure = hashRecords(leaderFailures);
522
535
  checksums.operatorNotification = hashRecords(operatorNotifications);
523
536
  checksums.capabilityGrant = hashRecords(capabilityGrants);
524
537
  checksums.releaseWorkflow = hashRecords(releaseWorkflows);
538
+ checksums.publicationReference = hashRecords(publicationReferences);
525
539
  checksums.workMailbox = hashRecords(Object.values(asObjectMap(state.mailboxes)));
526
540
  return checksums;
527
541
  }
@@ -609,10 +623,12 @@ export function computeDbFamilyChecksums(home, databaseFilename) {
609
623
  checksums.decision = hashPayloadTable(db, "SELECT payload FROM decisions");
610
624
  checksums.milestone = hashPayloadTable(db, "SELECT payload FROM milestones");
611
625
  checksums.event = hashPayloadTable(db, "SELECT payload FROM events");
626
+ checksums.taskWake = hashPayloadTable(db, "SELECT payload FROM task_wakes");
612
627
  checksums.leaderFailure = hashPayloadTable(db, "SELECT payload FROM task_projections WHERE kind = 'leader-failure' AND payload IS NOT NULL");
613
628
  checksums.operatorNotification = hashPayloadTable(db, "SELECT payload FROM task_projections WHERE kind = 'operator-notification' AND payload IS NOT NULL");
614
629
  checksums.capabilityGrant = hashPayloadTable(db, "SELECT payload FROM capability_grants");
615
630
  checksums.releaseWorkflow = hashPayloadTable(db, "SELECT payload FROM release_workflows");
631
+ checksums.publicationReference = hashPayloadTable(db, "SELECT payload FROM publication_references");
616
632
  // Mailboxes are reconstructed from typed columns (no payload column).
617
633
  const mailboxRows = readWorkMailboxRows(db);
618
634
  checksums.workMailbox = hashRecords(mailboxRows.map(rowToMailbox));
@@ -730,11 +746,13 @@ export function readStateFromSqlite(home) {
730
746
  decisions: {},
731
747
  milestones: {},
732
748
  events: {},
749
+ wakes: {},
733
750
  leaderFailure: null,
734
751
  operatorNotification: null,
735
752
  idHighWaterMarks: {},
736
753
  capabilityGrants: {},
737
- releaseWorkflows: {}
754
+ releaseWorkflows: {},
755
+ publicationReferences: {}
738
756
  };
739
757
  }
740
758
  // Per-task record families, keyed by their state.json map keys.
@@ -753,8 +771,10 @@ export function readStateFromSqlite(home) {
753
771
  loadTaskPayloadMap(db, "decisions", tasks, "decisions", (record) => record.id);
754
772
  loadTaskPayloadMap(db, "milestones", tasks, "milestones", (record) => record.id);
755
773
  loadTaskPayloadMap(db, "events", tasks, "events", (record) => record.id);
774
+ loadTaskPayloadMap(db, "task_wakes", tasks, "wakes", (record) => record.id);
756
775
  loadTaskPayloadMap(db, "capability_grants", tasks, "capabilityGrants", (record) => record.id);
757
776
  loadTaskPayloadMap(db, "release_workflows", tasks, "releaseWorkflows", (record) => record.id);
777
+ loadTaskPayloadMap(db, "publication_references", tasks, "publicationReferences", (record) => record.id);
758
778
  // Job caller key hashes: keyed by `${roleName}\0${agentId}`.
759
779
  const callerHashRows = db.prepare("SELECT task_id, role_name, agent_id, hash FROM job_caller_key_hashes").all();
760
780
  for (const row of callerHashRows) {