@zq-silk/yui 0.6.12 → 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 (93) 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/sqliteFileSet.js +50 -0
  77. package/dist/storage/upgrade/sqliteMigrationTarget.js +6 -4
  78. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +6 -4
  79. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  80. package/dist/task/completionReadiness.js +282 -0
  81. package/dist/task/publicationReference.js +123 -0
  82. package/dist/task/taskRecordReference.js +3 -1
  83. package/dist/telemetry/telemetryConfig.js +23 -18
  84. package/dist/telemetry/telemetryWiring.js +8 -8
  85. package/dist/tmux/tmuxManager.js +50 -9
  86. package/dist/web/assets/client/i18n.js +4 -0
  87. package/dist/web/assets/client/view.js +18 -0
  88. package/dist/web/webSnapshot.js +100 -10
  89. package/i18n/README.zh-CN.md +5 -5
  90. package/package.json +1 -1
  91. package/skills/yui-leader/SKILL.md +49 -10
  92. package/skills/yui-operator/SKILL.md +13 -3
  93. 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")
@@ -0,0 +1,50 @@
1
+ import { existsSync, renameSync, rmSync } from "node:fs";
2
+ const SQLITE_FILE_SUFFIXES = ["", "-wal", "-shm"];
3
+ /**
4
+ * Move a SQLite database together with any live WAL/SHM sidecars.
5
+ *
6
+ * A WAL database is one logical file set. Moving only the main file can leave
7
+ * the old WAL under the promoted database name, causing SQLite to replay it
8
+ * against an unrelated database. The migration fence guarantees there are no
9
+ * writers while this bounded rename runs.
10
+ */
11
+ export function moveSqliteFileSet(sourcePath, targetPath, rename = renameSync) {
12
+ const suffixes = SQLITE_FILE_SUFFIXES.filter((suffix) => existsSync(`${sourcePath}${suffix}`));
13
+ for (const suffix of suffixes) {
14
+ if (existsSync(`${targetPath}${suffix}`)) {
15
+ throw new Error(`Refusing to overwrite an existing SQLite switch target: ${targetPath}${suffix}.`);
16
+ }
17
+ }
18
+ const moved = [];
19
+ try {
20
+ for (const suffix of suffixes) {
21
+ rename(`${sourcePath}${suffix}`, `${targetPath}${suffix}`);
22
+ moved.push(suffix);
23
+ }
24
+ }
25
+ catch (error) {
26
+ let rollbackError;
27
+ for (const suffix of moved.reverse()) {
28
+ try {
29
+ rename(`${targetPath}${suffix}`, `${sourcePath}${suffix}`);
30
+ }
31
+ catch (candidate) {
32
+ rollbackError ??= candidate;
33
+ }
34
+ }
35
+ if (rollbackError !== undefined) {
36
+ throw new Error(`SQLite file-set move failed and rollback was incomplete: ${messageOf(error)}; `
37
+ + `rollback failed: ${messageOf(rollbackError)}`);
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+ /** Remove a promoted SQLite file set before restoring its timestamped backup. */
43
+ export function removeSqliteFileSet(databasePath) {
44
+ for (const suffix of SQLITE_FILE_SUFFIXES) {
45
+ rmSync(`${databasePath}${suffix}`, { force: true });
46
+ }
47
+ }
48
+ function messageOf(error) {
49
+ return error instanceof Error ? error.message : String(error);
50
+ }
@@ -42,6 +42,7 @@ import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspect
42
42
  import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState } from "./sqliteStateMigration.js";
43
43
  import { latestStateBackupPath } from "./pseudoLayoutRepair.js";
44
44
  import { migrationReceiptPath, writeMigrationReceipt } from "./migrationReceipt.js";
45
+ import { moveSqliteFileSet, removeSqliteFileSet } from "./sqliteFileSet.js";
45
46
  /** Build the SQLite-backed migration target. */
46
47
  export function createSqliteMigrationTarget(options) {
47
48
  const home = options.home;
@@ -155,7 +156,7 @@ export function createSqliteMigrationTarget(options) {
155
156
  if (existsSync(backupPath)) {
156
157
  throw new Error(`Refusing to overwrite an existing database backup: ${backupPath}.`);
157
158
  }
158
- renameSync(committedDbPath, backupPath);
159
+ moveSqliteFileSet(committedDbPath, backupPath);
159
160
  }
160
161
  renameSync(stagedDbPath, committedDbPath);
161
162
  // The staged connection may leave empty WAL/SHM sidecars behind
@@ -167,7 +168,7 @@ export function createSqliteMigrationTarget(options) {
167
168
  // Pre-promotion failure: restore the original database if we moved it.
168
169
  if (backupPath !== undefined && existsSync(backupPath)) {
169
170
  try {
170
- renameSync(backupPath, committedDbPath);
171
+ moveSqliteFileSet(backupPath, committedDbPath);
171
172
  }
172
173
  catch {
173
174
  throw new AmbiguousSwitchError({
@@ -208,10 +209,11 @@ export function createSqliteMigrationTarget(options) {
208
209
  // is ambiguous and must be recovered manually.
209
210
  try {
210
211
  if (backupPath !== undefined && existsSync(backupPath)) {
211
- renameSync(backupPath, committedDbPath);
212
+ removeSqliteFileSet(committedDbPath);
213
+ moveSqliteFileSet(backupPath, committedDbPath);
212
214
  }
213
215
  else {
214
- rmSync(committedDbPath, { force: true });
216
+ removeSqliteFileSet(committedDbPath);
215
217
  }
216
218
  }
217
219
  catch {
@@ -37,6 +37,7 @@ import { planMigration } from "../migration/planner.js";
37
37
  import { AmbiguousSwitchError } from "../migration/index.js";
38
38
  import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState, inspectSnapshotVersionState } from "./homeMigrationTarget.js";
39
39
  import { writeSwitchProgress } from "./switchProgress.js";
40
+ import { moveSqliteFileSet, removeSqliteFileSet } from "./sqliteFileSet.js";
40
41
  import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readStateFromSqlite } from "./sqliteStateMigration.js";
41
42
  /** Build the SQLite-backed record-migration target. */
42
43
  export function createSqliteRecordMigrationTarget(options) {
@@ -135,7 +136,7 @@ export function createSqliteRecordMigrationTarget(options) {
135
136
  if (existsSync(backupPath)) {
136
137
  throw new Error(`Refusing to overwrite an existing database backup: ${backupPath}.`);
137
138
  }
138
- renameSync(committedDbPath, backupPath);
139
+ moveSqliteFileSet(committedDbPath, backupPath);
139
140
  }
140
141
  promoteRename(stagedDbPath, committedDbPath);
141
142
  // The staged connection may leave empty WAL/SHM sidecars behind
@@ -147,7 +148,7 @@ export function createSqliteRecordMigrationTarget(options) {
147
148
  // Pre-promotion failure: restore the original database if we moved it.
148
149
  if (backupPath !== undefined && existsSync(backupPath)) {
149
150
  try {
150
- promoteRename(backupPath, committedDbPath);
151
+ moveSqliteFileSet(backupPath, committedDbPath, promoteRename);
151
152
  }
152
153
  catch {
153
154
  writeInterruptedMarker(home, backupPath, stagedDbPath, now);
@@ -175,10 +176,11 @@ export function createSqliteRecordMigrationTarget(options) {
175
176
  // is ambiguous and must be recovered manually.
176
177
  try {
177
178
  if (backupPath !== undefined && existsSync(backupPath)) {
178
- promoteRename(backupPath, committedDbPath);
179
+ removeSqliteFileSet(committedDbPath);
180
+ moveSqliteFileSet(backupPath, committedDbPath, promoteRename);
179
181
  }
180
182
  else {
181
- rmSync(committedDbPath, { force: true });
183
+ removeSqliteFileSet(committedDbPath);
182
184
  }
183
185
  }
184
186
  catch {