@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
@@ -16,6 +16,8 @@ const SQLITE_LAYOUT_FROM_VERSION = 6;
16
16
  const SQLITE_LAYOUT_TO_VERSION = 7;
17
17
  const PROJECT_FROM_VERSION = 2;
18
18
  const PROJECT_TO_VERSION = 3;
19
+ const PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION = 3;
20
+ const PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION = 4;
19
21
  const TASK_FROM_VERSION = 3;
20
22
  const TASK_TO_VERSION = 4;
21
23
  const WORK_ITEM_FROM_VERSION = 6;
@@ -57,6 +59,12 @@ const STORED_TASK_DURABLE_JOBS_FROM_VERSION = 14;
57
59
  const STORED_TASK_DURABLE_JOBS_TO_VERSION = 15;
58
60
  const STORED_TASK_JOB_CALLER_KEY_HASHES_FROM_VERSION = 15;
59
61
  const STORED_TASK_JOB_CALLER_KEY_HASHES_TO_VERSION = 16;
62
+ const STORED_TASK_WAKES_FROM_VERSION = 16;
63
+ const STORED_TASK_WAKES_TO_VERSION = 17;
64
+ const TASK_WAKE_FROM_VERSION = 0;
65
+ const TASK_WAKE_TO_VERSION = 1;
66
+ const STORED_TASK_PUBLICATION_REFERENCES_FROM_VERSION = 16;
67
+ const STORED_TASK_PUBLICATION_REFERENCES_TO_VERSION = 17;
60
68
  const CAPABILITY_GRANT_FROM_VERSION = 0;
61
69
  const CAPABILITY_GRANT_TO_VERSION = 1;
62
70
  const RELEASE_WORKFLOW_FROM_VERSION = 0;
@@ -65,6 +73,8 @@ const WORK_MAILBOX_FROM_VERSION = 1;
65
73
  const WORK_MAILBOX_TO_VERSION = 2;
66
74
  const TASK_ROLE_SESSION_SET_FROM_VERSION = 4;
67
75
  const TASK_ROLE_SESSION_SET_TO_VERSION = 5;
76
+ const PUBLICATION_REFERENCE_FROM_VERSION = 0;
77
+ const PUBLICATION_REFERENCE_TO_VERSION = 1;
68
78
  /**
69
79
  * Build the authoritative production graph. Transition intent and executable
70
80
  * transforms are registered together here; compatible loading and offline
@@ -113,6 +123,7 @@ export function createProductionStorageRegistry() {
113
123
  declaredEffects: []
114
124
  })
115
125
  .registerOfflineMigration(projectOwnershipStep())
126
+ .registerCompatible(projectKnowledgeProposalsStep())
116
127
  .registerOfflineMigration(taskWorkspaceIdentityStep())
117
128
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
118
129
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
@@ -131,11 +142,14 @@ export function createProductionStorageRegistry() {
131
142
  .registerOfflineMigration(integrationQueueIntroductionStep())
132
143
  .registerCompatible(storedTaskDurableJobsStep())
133
144
  .registerCompatible(storedTaskJobCallerKeyHashesStep())
145
+ .registerCompatible(storedTaskWakesAndPublicationReferencesStep())
146
+ .registerCompatible(taskWakeIntroductionStep())
134
147
  .registerCompatible(durableJobIntroductionStep())
135
148
  .registerOfflineMigration(capabilityGrantIntroductionStep())
136
149
  .registerOfflineMigration(releaseWorkflowIntroductionStep())
137
150
  .registerOfflineMigration(workMailboxV2Step())
138
- .registerOfflineMigration(taskRoleSessionSetV5Step());
151
+ .registerOfflineMigration(taskRoleSessionSetV5Step())
152
+ .registerOfflineMigration(publicationReferenceIntroductionStep());
139
153
  assertRegistryCoversBaselineToCurrent(registry);
140
154
  return registry;
141
155
  }
@@ -939,6 +953,158 @@ function normalizeStoredTaskV15ToV16(snapshot) {
939
953
  state: { ...snapshot.state, tasks: nextTasks }
940
954
  };
941
955
  }
956
+ /**
957
+ * StoredTask v17 introduces both the durable TaskWake ledger (Issue 04
958
+ * long-term design) and the publicationReferences map (Issue 11). Every
959
+ * aggregate gains empty `wakes` and `publicationReferences` maps and zero
960
+ * high-water marks.
961
+ */
962
+ function storedTaskWakesAndPublicationReferencesStep() {
963
+ return {
964
+ kind: "compatible",
965
+ axis: "record",
966
+ recordKind: "storedTask",
967
+ fromVersion: STORED_TASK_WAKES_FROM_VERSION,
968
+ toVersion: STORED_TASK_WAKES_TO_VERSION,
969
+ defaults: [
970
+ "wakes defaults to an empty map on every Task aggregate",
971
+ "publicationReferences defaults to an empty map on every Task aggregate"
972
+ ],
973
+ validateSource: (snapshot) => requireStoredTaskV16Shape(snapshot),
974
+ normalize: (snapshot) => normalizeStoredTaskV16ToV17(snapshot)
975
+ };
976
+ }
977
+ const STORED_TASK_V16_FIELDS = [
978
+ "schemaVersion",
979
+ "task",
980
+ "idHighWaterMarks",
981
+ "brief",
982
+ "changeSets",
983
+ "integrationAttempts",
984
+ "integrationQueue",
985
+ "durableJobs",
986
+ "jobCallerKeyHashes",
987
+ // May already be present after the publicationReference introduction runs
988
+ // first; the 16->17 normalizer preserves the empty introduced map.
989
+ "publicationReferences",
990
+ "capabilityGrants",
991
+ "releaseWorkflows",
992
+ "roles",
993
+ "managedWorkspaces",
994
+ "roleSessionSets",
995
+ "workItems",
996
+ "agentRuns",
997
+ "reviewRounds",
998
+ "activeRuns",
999
+ "messages",
1000
+ "inputRequests",
1001
+ "decisions",
1002
+ "milestones",
1003
+ "events",
1004
+ "leaderFailure",
1005
+ "operatorNotification"
1006
+ ];
1007
+ function requireStoredTaskV16Shape(snapshot) {
1008
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1009
+ if (manifestVersions.storedTask !== STORED_TASK_WAKES_FROM_VERSION) {
1010
+ throw new Error(`Record storedTask compatible step requires manifest version ${STORED_TASK_WAKES_FROM_VERSION}.`);
1011
+ }
1012
+ if (snapshot.state === null)
1013
+ return;
1014
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1015
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1016
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1017
+ if (task.schemaVersion !== STORED_TASK_WAKES_FROM_VERSION) {
1018
+ throw new Error(`Task aggregate ${taskId} must use schemaVersion ${STORED_TASK_WAKES_FROM_VERSION}.`);
1019
+ }
1020
+ const allowed = new Set(STORED_TASK_V16_FIELDS);
1021
+ const unknown = Object.keys(task).find((key) => !allowed.has(key));
1022
+ if (unknown !== undefined) {
1023
+ throw new Error(`Task aggregate ${taskId} has an unknown v16 field: ${unknown}.`);
1024
+ }
1025
+ if (task.publicationReferences !== undefined) {
1026
+ const references = asObject(task.publicationReferences, `Task aggregate ${taskId} publicationReferences`);
1027
+ if (Object.keys(references).length > 0) {
1028
+ throw new Error(`Task aggregate ${taskId} already has publication references before v17.`);
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ function normalizeStoredTaskV16ToV17(snapshot) {
1034
+ requireStoredTaskV16Shape(snapshot);
1035
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1036
+ const schemaManifest = {
1037
+ ...snapshot.schemaManifest,
1038
+ recordVersions: {
1039
+ ...manifestVersions,
1040
+ storedTask: STORED_TASK_WAKES_TO_VERSION
1041
+ }
1042
+ };
1043
+ if (snapshot.state === null)
1044
+ return { schemaManifest, state: null };
1045
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1046
+ const nextTasks = {};
1047
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1048
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1049
+ const existingReferences = task.publicationReferences === undefined
1050
+ ? {}
1051
+ : asObject(task.publicationReferences, `Task aggregate ${taskId} publicationReferences`);
1052
+ const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1053
+ const existingMark = marks.publicationReference;
1054
+ const existingWakeMark = marks.taskWake;
1055
+ nextTasks[taskId] = {
1056
+ ...task,
1057
+ schemaVersion: STORED_TASK_WAKES_TO_VERSION,
1058
+ idHighWaterMarks: {
1059
+ ...marks,
1060
+ publicationReference: typeof existingMark === "number" ? existingMark : 0,
1061
+ taskWake: typeof existingWakeMark === "number" ? existingWakeMark : TASK_WAKE_FROM_VERSION
1062
+ },
1063
+ publicationReferences: existingReferences,
1064
+ wakes: {}
1065
+ };
1066
+ }
1067
+ return {
1068
+ schemaManifest,
1069
+ state: { ...snapshot.state, tasks: nextTasks }
1070
+ };
1071
+ }
1072
+ /**
1073
+ * The taskWake record family is introduced alongside StoredTask v17. The
1074
+ * compatible step supplies the empty map and high-water mark; this step only
1075
+ * advances the manifest record version so the planner can resolve the 0->1
1076
+ * boundary.
1077
+ */
1078
+ function taskWakeIntroductionStep() {
1079
+ return {
1080
+ kind: "compatible",
1081
+ axis: "record",
1082
+ recordKind: "taskWake",
1083
+ fromVersion: TASK_WAKE_FROM_VERSION,
1084
+ toVersion: TASK_WAKE_TO_VERSION,
1085
+ introduction: true,
1086
+ defaults: [
1087
+ "taskWake family starts at version 1 with an empty ledger"
1088
+ ],
1089
+ validateSource: (snapshot) => {
1090
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1091
+ const namedVersion = manifestVersions.taskWake;
1092
+ if (namedVersion !== undefined && namedVersion !== TASK_WAKE_FROM_VERSION) {
1093
+ throw new Error(`Record taskWake introduction requires an absent manifest version or ${TASK_WAKE_FROM_VERSION}.`);
1094
+ }
1095
+ },
1096
+ normalize: (snapshot) => ({
1097
+ schemaManifest: {
1098
+ ...snapshot.schemaManifest,
1099
+ recordVersions: {
1100
+ ...asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions"),
1101
+ taskWake: TASK_WAKE_TO_VERSION
1102
+ }
1103
+ },
1104
+ state: snapshot.state
1105
+ })
1106
+ };
1107
+ }
942
1108
  /**
943
1109
  * The durableJob record family is introduced alongside the StoredTask v15
944
1110
  * aggregate. A pre-baseline Home has no durableJobs map and no manifest entry;
@@ -1147,6 +1313,79 @@ function introduceReleaseWorkflowFamily(snapshot) {
1147
1313
  state: { ...snapshot.state, tasks: nextTasks }
1148
1314
  };
1149
1315
  }
1316
+ /**
1317
+ * Issue 11: introduce the task-scoped PublicationReference family. A
1318
+ * pre-introduction Home names no publicationReference manifest version and its
1319
+ * Task aggregates carry no `publicationReferences` map; this explicit 0->1
1320
+ * step adds the empty family to every Task (plus the matching high-water mark).
1321
+ */
1322
+ function publicationReferenceIntroductionStep() {
1323
+ return {
1324
+ axis: "record",
1325
+ recordKind: "publicationReference",
1326
+ fromVersion: PUBLICATION_REFERENCE_FROM_VERSION,
1327
+ toVersion: PUBLICATION_REFERENCE_TO_VERSION,
1328
+ introduction: true,
1329
+ preconditions: requirePublicationReferenceIntroduction,
1330
+ transform: introducePublicationReferenceFamily,
1331
+ declaredEffects: []
1332
+ };
1333
+ }
1334
+ function requirePublicationReferenceIntroduction(snapshot) {
1335
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1336
+ const namedVersion = manifestVersions.publicationReference;
1337
+ if (namedVersion !== undefined && namedVersion !== PUBLICATION_REFERENCE_FROM_VERSION) {
1338
+ throw new Error(`Record publicationReference introduction requires an absent manifest version or ${PUBLICATION_REFERENCE_FROM_VERSION}.`);
1339
+ }
1340
+ if (snapshot.state === null)
1341
+ return;
1342
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1343
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1344
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1345
+ if (task.publicationReferences !== undefined) {
1346
+ const references = asObject(task.publicationReferences, `publicationReference map ${taskId}`);
1347
+ if (Object.keys(references).length > 0) {
1348
+ throw new Error(`Publication reference introduction found existing records: ${taskId}.`);
1349
+ }
1350
+ }
1351
+ const marks = task.idHighWaterMarks;
1352
+ if (marks !== undefined) {
1353
+ const highWaterMarks = asObject(marks, `Task id high-water marks ${taskId}`);
1354
+ const mark = highWaterMarks.publicationReference;
1355
+ if (mark !== undefined && mark !== PUBLICATION_REFERENCE_FROM_VERSION) {
1356
+ throw new Error(`Publication reference introduction found a high-water mark: ${taskId}.`);
1357
+ }
1358
+ }
1359
+ }
1360
+ }
1361
+ function introducePublicationReferenceFamily(snapshot) {
1362
+ requirePublicationReferenceIntroduction(snapshot);
1363
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1364
+ const schemaManifest = {
1365
+ ...snapshot.schemaManifest,
1366
+ recordVersions: {
1367
+ ...manifestVersions,
1368
+ publicationReference: PUBLICATION_REFERENCE_TO_VERSION
1369
+ }
1370
+ };
1371
+ if (snapshot.state === null)
1372
+ return { schemaManifest, state: null };
1373
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1374
+ const nextTasks = {};
1375
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1376
+ const task = asObject(rawTask, `Task aggregate ${taskId}`);
1377
+ const marks = asObject(task.idHighWaterMarks, `Task id high-water marks ${taskId}`);
1378
+ nextTasks[taskId] = {
1379
+ ...task,
1380
+ idHighWaterMarks: { ...marks, publicationReference: PUBLICATION_REFERENCE_FROM_VERSION },
1381
+ publicationReferences: {}
1382
+ };
1383
+ }
1384
+ return {
1385
+ schemaManifest,
1386
+ state: { ...snapshot.state, tasks: nextTasks }
1387
+ };
1388
+ }
1150
1389
  function recordFamilyStep(recordKind, fromVersion, toVersion, taskMapKey) {
1151
1390
  return {
1152
1391
  axis: "record",
@@ -1911,6 +2150,91 @@ function migrateProjectV2ToV3(snapshot) {
1911
2150
  state: { ...snapshot.state, projects: nextProjects }
1912
2151
  };
1913
2152
  }
2153
+ /**
2154
+ * Project v4 adds the `knowledgeProposals` workflow list (Leader-proposed
2155
+ * Knowledge candidates awaiting an Operator decision). Every v3 Project
2156
+ * defaults to an empty list; the compatible normalizer adds it in place
2157
+ * without rewriting any other field.
2158
+ */
2159
+ function projectKnowledgeProposalsStep() {
2160
+ return {
2161
+ kind: "compatible",
2162
+ axis: "record",
2163
+ recordKind: "project",
2164
+ fromVersion: PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION,
2165
+ toVersion: PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION,
2166
+ defaults: [
2167
+ "knowledgeProposals defaults to an empty list on every Project"
2168
+ ],
2169
+ validateSource: (snapshot) => requireProjectV3Shape(snapshot),
2170
+ normalize: (snapshot) => normalizeProjectV3ToV4(snapshot)
2171
+ };
2172
+ }
2173
+ function requireProjectV3Shape(snapshot) {
2174
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2175
+ if (manifestVersions.project !== PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION) {
2176
+ throw new Error(`Record project compatible step requires manifest version ${PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION}.`);
2177
+ }
2178
+ if (snapshot.state === null)
2179
+ return;
2180
+ const projects = snapshot.state.projects;
2181
+ if (projects === undefined)
2182
+ return;
2183
+ const map = asObject(projects, "project map");
2184
+ const allowed = new Set(PROJECT_V3_FIELDS);
2185
+ for (const [projectId, rawProject] of Object.entries(map)) {
2186
+ const project = asObject(rawProject, `Project ${projectId}`);
2187
+ if (project.schemaVersion !== PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION) {
2188
+ throw new Error(`Project ${projectId} must use schemaVersion ${PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION}.`);
2189
+ }
2190
+ const unknown = Object.keys(project).find((key) => !allowed.has(key));
2191
+ if (unknown !== undefined) {
2192
+ throw new Error(`Project ${projectId} has an unknown v3 field: ${unknown}.`);
2193
+ }
2194
+ }
2195
+ }
2196
+ const PROJECT_V3_FIELDS = [
2197
+ "schemaVersion",
2198
+ "id",
2199
+ "name",
2200
+ "aliases",
2201
+ "path",
2202
+ "ownership",
2203
+ "remoteUrl",
2204
+ "stableBranch",
2205
+ "developmentBranch",
2206
+ "knowledge",
2207
+ "createdAt",
2208
+ "updatedAt"
2209
+ ];
2210
+ function normalizeProjectV3ToV4(snapshot) {
2211
+ requireProjectV3Shape(snapshot);
2212
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2213
+ const schemaManifest = {
2214
+ ...snapshot.schemaManifest,
2215
+ recordVersions: { ...manifestVersions, project: PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION }
2216
+ };
2217
+ if (snapshot.state === null)
2218
+ return { schemaManifest, state: null };
2219
+ const projects = snapshot.state.projects;
2220
+ if (projects === undefined) {
2221
+ return { schemaManifest, state: { ...snapshot.state } };
2222
+ }
2223
+ const map = asObject(projects, "project map");
2224
+ const nextProjects = {};
2225
+ for (const [projectId, rawProject] of Object.entries(map)) {
2226
+ const project = asObject(rawProject, `Project ${projectId}`);
2227
+ nextProjects[projectId] = {
2228
+ ...project,
2229
+ schemaVersion: PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION,
2230
+ knowledgeProposals: []
2231
+ };
2232
+ }
2233
+ return {
2234
+ schemaManifest,
2235
+ state: { ...snapshot.state, projects: nextProjects }
2236
+ };
2237
+ }
1914
2238
  /**
1915
2239
  * Task v4 adds the optional durable workspace identity. Historical Tasks have
1916
2240
  * no identity; they keep working against their existing (legacy) refs until the
@@ -27,7 +27,7 @@ export const SQLITE_LAYOUT_VERSION = 7;
27
27
  /** The aggregate version of the normalized SQLite schema. */
28
28
  export const SQLITE_AGGREGATE_VERSION = 1;
29
29
  /** The current schema migration version. */
30
- export const SQLITE_SCHEMA_VERSION = 14;
30
+ export const SQLITE_SCHEMA_VERSION = 16;
31
31
  /** Telemetry retention bounds (§4.4). Open question 3 in §11; defaults from the design. */
32
32
  export const TELEMETRY_KEEP_PER_GENERATION = 200;
33
33
  export const TELEMETRY_RUN_CAP = 50_000;
@@ -855,6 +855,62 @@ CREATE INDEX idx_mailboxes_ready
855
855
  DROP TABLE pending_runtime_turn_completions;
856
856
  DROP TABLE IF EXISTS mailbox_signals;
857
857
  `;
858
+ /**
859
+ * Migration 15: Issue 11 external publication evidence. Records are immutable;
860
+ * a corrected MR/PR state appends a superseding record with the same
861
+ * external_key, so only the unsuperseded root is globally unique.
862
+ */
863
+ const MIGRATION_15_SQL = `
864
+ CREATE TABLE IF NOT EXISTS publication_references (
865
+ task_id TEXT NOT NULL,
866
+ publication_id TEXT NOT NULL,
867
+ project_id TEXT NOT NULL,
868
+ provider TEXT NOT NULL,
869
+ repository TEXT NOT NULL,
870
+ external_kind TEXT NOT NULL,
871
+ external_id TEXT NOT NULL,
872
+ external_key TEXT NOT NULL,
873
+ state TEXT NOT NULL,
874
+ verification TEXT NOT NULL,
875
+ external_url TEXT,
876
+ title TEXT,
877
+ source_branch TEXT,
878
+ target_branch TEXT,
879
+ local_commit TEXT,
880
+ remote_commit TEXT,
881
+ supersedes TEXT,
882
+ payload TEXT NOT NULL,
883
+ merged_at TEXT,
884
+ created_at TEXT NOT NULL,
885
+ PRIMARY KEY (task_id, publication_id)
886
+ );
887
+ CREATE INDEX IF NOT EXISTS idx_publication_references_task
888
+ ON publication_references(task_id);
889
+ CREATE INDEX IF NOT EXISTS idx_publication_references_external
890
+ ON publication_references(external_key);
891
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_publication_references_external_root
892
+ ON publication_references(external_key) WHERE supersedes IS NULL;
893
+ `;
894
+ const MIGRATION_16_SQL = `
895
+ -- Durable Leader wake ledger (Issue 04 long-term design). A wake is a
896
+ -- notification envelope, not a context dump: the record holds the aggregated
897
+ -- reason tags and the delta window; the Agent reads delta content on demand.
898
+ CREATE TABLE IF NOT EXISTS task_wakes (
899
+ task_id TEXT NOT NULL,
900
+ wake_id TEXT NOT NULL,
901
+ seq INTEGER NOT NULL,
902
+ status TEXT NOT NULL CHECK (status IN ('dispatched','consumed')),
903
+ run_id TEXT,
904
+ from_cursor TEXT NOT NULL,
905
+ to_cursor TEXT NOT NULL,
906
+ reasons TEXT NOT NULL,
907
+ payload TEXT NOT NULL,
908
+ created_at TEXT NOT NULL,
909
+ consumed_at TEXT,
910
+ PRIMARY KEY (task_id, wake_id)
911
+ );
912
+ CREATE INDEX IF NOT EXISTS idx_task_wakes_seq ON task_wakes(task_id, seq);
913
+ `;
858
914
  const MIGRATIONS = [
859
915
  { version: 1, axis: "layout", sql: MIGRATION_1_SQL },
860
916
  { version: 2, axis: "record", recordKind: "durableJob+capability-grant+release-workflow", sql: MIGRATION_2_SQL },
@@ -869,7 +925,9 @@ const MIGRATIONS = [
869
925
  { version: 11, axis: "layout", sql: MIGRATION_11_SQL },
870
926
  { version: 12, axis: "layout", sql: MIGRATION_12_SQL },
871
927
  { version: 13, axis: "layout", sql: MIGRATION_13_SQL },
872
- { version: 14, axis: "record", recordKind: "workMailbox", sql: MIGRATION_14_SQL }
928
+ { version: 14, axis: "record", recordKind: "workMailbox", sql: MIGRATION_14_SQL },
929
+ { version: 15, axis: "record", recordKind: "publicationReference", sql: MIGRATION_15_SQL },
930
+ { version: 16, axis: "record", recordKind: "taskWake", sql: MIGRATION_16_SQL }
873
931
  ];
874
932
  /** Current hot-path indexes whose absence would invalidate a current Home. */
875
933
  const REQUIRED_SCHEMA_INDEXES = [
@@ -1127,6 +1185,7 @@ export const SQLITE_SCHEMA_TABLES = [
1127
1185
  "telemetry_aggregate",
1128
1186
  "capability_grants",
1129
1187
  "release_workflows",
1188
+ "publication_references",
1130
1189
  "session_owners",
1131
1190
  "runtime_session_candidates",
1132
1191
  "resource_registry",
@@ -41,13 +41,16 @@ import Database from "better-sqlite3";
41
41
  import { consumePendingBatch, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
42
42
  import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
43
43
  import { validateReviewFinding } from "../review/reviewFinding.js";
44
+ import { reviewFindingLedgerMode } from "../review/reviewFindingLedger.js";
44
45
  import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
45
46
  import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
46
47
  import { validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
48
+ import { validateTaskWake } from "../scheduler/taskWake.js";
47
49
  import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
48
50
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
49
51
  import { assertHomeWritable } from "./upgradeFence.js";
50
- import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, FileTaskStore, storedCapabilityGrant, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, validateYuiConfig } from "./taskStore.js";
52
+ import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, FileTaskStore, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, validateYuiConfig } from "./taskStore.js";
53
+ import { publicationExternalKey } from "../task/publicationReference.js";
51
54
  import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
52
55
  import { migrateSqliteSchema, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
53
56
  import { inspectStorageSchema } from "./storageSchema.js";
@@ -79,6 +82,30 @@ const DEFAULT_CONFIG = { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION };
79
82
  function numericCompare(left, right) {
80
83
  return left.localeCompare(right, undefined, { numeric: true });
81
84
  }
85
+ function latestSqlitePublicationReference(references) {
86
+ const byId = new Map(references.map((reference) => [reference.id, reference]));
87
+ const superseded = new Set(references
88
+ .map((reference) => reference.supersedes)
89
+ .filter((id) => id !== undefined));
90
+ const roots = references.filter((reference) => !superseded.has(reference.id));
91
+ const latest = roots.length === 1
92
+ ? roots[0]
93
+ : [...references].sort((left, right) => numericCompare(right.id, left.id))[0];
94
+ let current = latest;
95
+ const seen = new Set();
96
+ while (current.supersedes !== undefined) {
97
+ if (seen.has(current.id)) {
98
+ throw new StorageRecordError(`Publication supersession cycle at ${current.id}.`);
99
+ }
100
+ seen.add(current.id);
101
+ const previous = byId.get(current.supersedes);
102
+ if (previous === undefined) {
103
+ throw new StorageRecordError(`Publication supersession target not found: ${current.id}/${current.supersedes}.`);
104
+ }
105
+ current = previous;
106
+ }
107
+ return latest;
108
+ }
82
109
  /** True when the better-sqlite3 error is a UNIQUE/PRIMARY KEY constraint failure. */
83
110
  function isUniqueConstraint(error) {
84
111
  return error instanceof Error
@@ -685,7 +712,8 @@ export class SqliteTaskStore {
685
712
  task: {
686
713
  id: task.id,
687
714
  status: task.status,
688
- projectBindings: task.projectBindings
715
+ projectBindings: task.projectBindings,
716
+ requireIntegration: task.requireIntegration
689
717
  },
690
718
  workItems: this.#sortById(this.#listPayload("work_items", "task_id = ?", [taskId]), (item) => item.id),
691
719
  changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
@@ -697,6 +725,20 @@ export class SqliteTaskStore {
697
725
  leaderRuns: runs.filter((run) => run.roleName === "leader")
698
726
  };
699
727
  }
728
+ readCompletionReadinessFacts(taskId) {
729
+ const base = this.readNextActionFacts(taskId);
730
+ if (base === null)
731
+ return null;
732
+ return {
733
+ ...base,
734
+ managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
735
+ durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id),
736
+ integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
737
+ reviewFindings: this.#sortById(this.#listPayload("review_findings", "task_id = ?", [taskId]), (finding) => finding.id),
738
+ reviewFindingLedgerMode: reviewFindingLedgerMode(this.getConfig()),
739
+ events: this.#sortById(this.#listPayload("events", "task_id = ?", [taskId]), (event) => event.id)
740
+ };
741
+ }
700
742
  /** Task ids flagged active in the catalog projection (the global active index). */
701
743
  listActiveTaskIds() {
702
744
  const rows = this.#db.prepare("SELECT task_id FROM tasks_catalog WHERE is_active = 1 ORDER BY task_id").all();
@@ -1217,6 +1259,58 @@ export class SqliteTaskStore {
1217
1259
  getReleaseWorkflow(taskId, workflowId) {
1218
1260
  return this.#getPayload("release_workflows", "task_id = ? AND workflow_id = ?", [taskId, workflowId]);
1219
1261
  }
1262
+ // -- publication references -------------------------------------------------
1263
+ nextPublicationReferenceId(taskId) {
1264
+ return this.#nextTaskRecordId(taskId, "publicationReference");
1265
+ }
1266
+ savePublicationReference(taskId, reference) {
1267
+ const stored = storedPublicationReference(reference);
1268
+ if (stored.taskId !== taskId) {
1269
+ throw new StorageRecordError(`Publication reference belongs to another Task: ${stored.taskId}`);
1270
+ }
1271
+ this.#requireTask(taskId);
1272
+ this.#mutate(() => {
1273
+ const key = publicationExternalKey(stored);
1274
+ const existing = this.#getPayload("publication_references", "task_id = ? AND publication_id = ?", [taskId, stored.id]);
1275
+ if (existing !== null && !isDeepStrictEqual(existing, stored)) {
1276
+ throw new StorageRecordError(`Publication reference cannot be overwritten: ${taskId}/${stored.id}`);
1277
+ }
1278
+ if (existing === null) {
1279
+ const sameExternal = this.#listPayload("publication_references", "external_key = ?", [key]);
1280
+ const otherTask = sameExternal.find((candidate) => candidate.taskId !== taskId);
1281
+ if (otherTask !== undefined) {
1282
+ throw new StorageRecordError(`Publication external identity already belongs to Task ${otherTask.taskId}: ${key}.`);
1283
+ }
1284
+ if (sameExternal.length > 0) {
1285
+ const latest = latestSqlitePublicationReference(sameExternal);
1286
+ if (stored.supersedes !== latest.id) {
1287
+ throw new StorageRecordError(`Publication external identity conflicts with ${latest.id}; `
1288
+ + `explicitly supersede that record: ${key}.`);
1289
+ }
1290
+ }
1291
+ else if (stored.supersedes !== undefined) {
1292
+ throw new StorageRecordError(`Publication supersedes target has a different external identity: ${stored.supersedes}.`);
1293
+ }
1294
+ }
1295
+ this.#db.prepare(`INSERT INTO publication_references
1296
+ (task_id, publication_id, project_id, provider, repository, external_kind,
1297
+ external_id, external_key, state, verification, external_url, local_commit,
1298
+ remote_commit, supersedes, payload, merged_at, created_at,
1299
+ title, source_branch, target_branch)
1300
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1301
+ ON CONFLICT(task_id, publication_id) DO UPDATE SET payload = excluded.payload`).run(taskId, stored.id, stored.projectId, stored.provider, stored.repository, stored.externalKind, stored.externalId, key, stored.state, stored.verification, stored.externalUrl ?? null, stored.localCommit ?? null, stored.remoteCommit ?? null, stored.supersedes ?? null, this.#json(stored), stored.mergedAt ?? null, this.#now(), stored.title ?? null, stored.sourceBranch ?? null, stored.targetBranch ?? null);
1302
+ });
1303
+ }
1304
+ listPublicationReferences(taskId) {
1305
+ return this.#sortById(this.#listPayload("publication_references", "task_id = ?", [taskId]), (reference) => reference.id);
1306
+ }
1307
+ getPublicationReference(taskId, referenceId) {
1308
+ return this.#getPayload("publication_references", "task_id = ? AND publication_id = ?", [taskId, referenceId]);
1309
+ }
1310
+ findPublicationReferenceByExternalKey(externalKey) {
1311
+ const matches = this.#listPayload("publication_references", "external_key = ?", [externalKey]);
1312
+ return matches.length === 0 ? null : latestSqlitePublicationReference(matches);
1313
+ }
1220
1314
  // -- gate artifacts (Issue 08) ----------------------------------------------
1221
1315
  saveGateArtifact(artifact, logs) {
1222
1316
  validateGateArtifact(artifact);
@@ -1696,6 +1790,39 @@ export class SqliteTaskStore {
1696
1790
  return removed;
1697
1791
  });
1698
1792
  }
1793
+ nextTaskWakeId(taskId) { return this.#nextTaskRecordId(taskId, "taskWake"); }
1794
+ peekNextTaskWakeId(taskId) { return this.#peekTaskRecordId(taskId, "taskWake"); }
1795
+ saveTaskWake(taskId, wake) {
1796
+ if (wake.taskId !== taskId) {
1797
+ throw new StorageRecordError(`Task wake belongs to another Task: ${wake.taskId}`);
1798
+ }
1799
+ validateTaskWake(wake);
1800
+ this.#requireTask(taskId);
1801
+ this.#mutate(() => {
1802
+ const seq = this.#idSequence(wake.id, "taskWake");
1803
+ this.#db.prepare(`INSERT INTO task_wakes
1804
+ (task_id, wake_id, seq, status, run_id, from_cursor, to_cursor, reasons, payload, created_at, consumed_at)
1805
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1806
+ + ` ON CONFLICT(task_id, wake_id) DO UPDATE SET
1807
+ status = excluded.status,
1808
+ run_id = excluded.run_id,
1809
+ reasons = excluded.reasons,
1810
+ payload = excluded.payload,
1811
+ consumed_at = excluded.consumed_at`).run(taskId, wake.id, wake.seq, wake.status, wake.runId ?? null, wake.fromCursor, wake.toCursor, this.#json([...wake.reasons]), this.#json(wake), wake.createdAt, wake.consumedAt ?? null);
1812
+ this.#observeHighWater(taskId, "taskWake", seq);
1813
+ });
1814
+ }
1815
+ getTaskWake(taskId, wakeId) {
1816
+ const row = this.#db.prepare("SELECT payload FROM task_wakes WHERE task_id = ? AND wake_id = ?").get(taskId, wakeId);
1817
+ if (row === undefined)
1818
+ return null;
1819
+ const wake = this.#parse(row.payload);
1820
+ validateTaskWake(wake);
1821
+ return wake;
1822
+ }
1823
+ listTaskWakes(taskId) {
1824
+ return this.#sortById(this.#listPayload("task_wakes", "task_id = ?", [taskId]), (wake) => wake.id);
1825
+ }
1699
1826
  // -- high-water maintenance ---------------------------------------------------
1700
1827
  /** Extract the numeric suffix of a `<prefix>-<n>` record id. */
1701
1828
  #idSequence(id, kind) {
@@ -49,6 +49,7 @@ const READ_ONLY_STORE_METHODS = new Set([
49
49
  "listTasks",
50
50
  "getTask",
51
51
  "readNextActionFacts",
52
+ "readCompletionReadinessFacts",
52
53
  "listActiveTaskIds",
53
54
  "getTaskBrief",
54
55
  "listChangeSets",