@zq-silk/yui 0.16.1 → 1.0.0-alpha

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 (193) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +9 -4
  4. package/dist/agent/agent.js +4 -9
  5. package/dist/agent/executionComponents.js +4 -4
  6. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  7. package/dist/agentRun/agentRun.js +13 -23
  8. package/dist/artifacts/managedGit.js +3 -56
  9. package/dist/brief/taskBrief.js +3 -3
  10. package/dist/cli/agentConfigurationPicker.js +24 -21
  11. package/dist/cli/commandCatalog.js +18 -10
  12. package/dist/cli/roleWizard.js +41 -30
  13. package/dist/cli/updateCommand.js +35 -13
  14. package/dist/cli/updateOrchestrator.js +73 -44
  15. package/dist/cli/updatePorts.js +76 -86
  16. package/dist/cli/upgradeCommand.js +4 -8
  17. package/dist/cli.js +39 -33
  18. package/dist/commands/controllerCommands.js +2 -2
  19. package/dist/commands/executionAuditCommands.js +2 -2
  20. package/dist/commands/projectCommands.js +1 -1
  21. package/dist/commands/releaseCommands.js +9 -38
  22. package/dist/commands/taskCommands.js +91 -81
  23. package/dist/commands/taskCompletionGate.js +0 -117
  24. package/dist/commands/taskRoleRuntimeStatus.js +2 -2
  25. package/dist/commands/taskUpstreamCommands.js +93 -54
  26. package/dist/context/runContextPack.js +30 -54
  27. package/dist/context/runInputContract.js +9 -0
  28. package/dist/context/sessionBootstrapManifest.js +11 -19
  29. package/dist/controller/agentHostObservation.js +4 -2
  30. package/dist/controller/agentRuntimeObserver.js +4 -4
  31. package/dist/controller/clientRuntime.js +19 -44
  32. package/dist/controller/controller.js +24 -10
  33. package/dist/controller/fileSchedulerStoreAdapter.js +91 -24
  34. package/dist/controller/globalInputDelivery.js +31 -8
  35. package/dist/controller/globalRuntimeAttention.js +34 -0
  36. package/dist/controller/jobSupervisor.js +3 -3
  37. package/dist/controller/operatorNotification.js +31 -0
  38. package/dist/controller/providerRetryAdmission.js +3 -1
  39. package/dist/controller/providerRetryDelivery.js +136 -119
  40. package/dist/controller/runtime.js +2 -28
  41. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  42. package/dist/controller/structuredProviderObservation.js +28 -1
  43. package/dist/controller/updateReconciliation.js +72 -20
  44. package/dist/coordination/workMailbox.js +4 -4
  45. package/dist/core/controllerIdentity.js +25 -0
  46. package/dist/core/controllerProcessIdentity.js +2 -2
  47. package/dist/core/controllerServer.js +4 -2
  48. package/dist/core/protocol.js +1 -1
  49. package/dist/doctor/doctor.js +2 -1
  50. package/dist/domain/validation.js +6 -0
  51. package/dist/event/taskEvent.js +3 -3
  52. package/dist/execution/workItemExecution.js +2 -2
  53. package/dist/executor/agentAdapter.js +28 -96
  54. package/dist/executor/agentConfigurationCatalog.js +13 -68
  55. package/dist/executor/agentConfigurationFields.js +120 -0
  56. package/dist/executor/agentConfigurationProbe.js +55 -80
  57. package/dist/executor/agentExecutor.js +11 -8
  58. package/dist/executor/effectiveLaunch.js +3 -3
  59. package/dist/executor/executorRegistry.js +0 -9
  60. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  61. package/dist/grant/capabilityGrant.js +2 -2
  62. package/dist/input/inputRequest.js +4 -4
  63. package/dist/integration/changeSet.js +3 -3
  64. package/dist/integration/integrationAttempt.js +3 -3
  65. package/dist/integration/integrationSourceApplication.js +1 -1
  66. package/dist/interaction/operatorPresentation.js +2 -1
  67. package/dist/job/durableJob.js +1 -1
  68. package/dist/job/jobRunner.js +2 -2
  69. package/dist/message/message.js +8 -6
  70. package/dist/message/messageContinuation.js +8 -9
  71. package/dist/milestone/milestone.js +3 -3
  72. package/dist/observability/executionAudit.js +1 -10
  73. package/dist/observability/runtimeIdentity.js +0 -23
  74. package/dist/output/agentConfigurationPresentation.js +4 -2
  75. package/dist/profile/agentProfile.js +3 -3
  76. package/dist/release/releaseHandover.js +2 -2
  77. package/dist/release/releaseIdempotencyStore.js +0 -23
  78. package/dist/release/releaseWorkflowPorts.js +8 -4
  79. package/dist/release/runtimeRelease.js +9 -7
  80. package/dist/repository/gitWorkspace.js +66 -19
  81. package/dist/repository/project.js +3 -3
  82. package/dist/repository/taskWorkspacePreparer.js +75 -68
  83. package/dist/resources/liveReferences.js +1 -1
  84. package/dist/resources/resourceRegistry.js +90 -44
  85. package/dist/resources/sqliteResourceRegistry.js +5 -7
  86. package/dist/review/reviewRound.js +5 -5
  87. package/dist/role/role.js +7 -8
  88. package/dist/runtime/acpProtocol.js +2 -3
  89. package/dist/runtime/acpSession.js +21 -5
  90. package/dist/runtime/agentDriverObservation.js +1 -1
  91. package/dist/runtime/agentEndpoint.js +4 -1
  92. package/dist/runtime/agentHost.js +25 -65
  93. package/dist/runtime/agentHostCleanup.js +85 -0
  94. package/dist/runtime/agentHostProtocol.js +3 -3
  95. package/dist/runtime/builtinAgentDrivers.js +46 -119
  96. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  97. package/dist/runtime/codexAppServerRuntime.js +55 -89
  98. package/dist/runtime/codexInteractiveHost.js +1 -1
  99. package/dist/runtime/jsonLineChannel.js +35 -7
  100. package/dist/runtime/launchBroker.js +1 -1
  101. package/dist/runtime/processExitObservation.js +1 -1
  102. package/dist/runtime/providerContinuationReconciliationService.js +86 -38
  103. package/dist/runtime/providerRetry.js +15 -0
  104. package/dist/runtime/providerRuntimeIdentity.js +3 -3
  105. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  106. package/dist/runtime/runtimeCoherence.js +7 -3
  107. package/dist/runtime/runtimeObservation.js +3 -3
  108. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  109. package/dist/runtime/structuredProviderHost.js +106 -81
  110. package/dist/runtime/taskRuntimeIsolation.js +3 -3
  111. package/dist/runtime/tmuxAdapters.js +3 -3
  112. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  113. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  114. package/dist/scheduler/taskWake.js +1 -1
  115. package/dist/storage/baselineSchema.js +606 -0
  116. package/dist/storage/homeLayout.js +5 -16
  117. package/dist/storage/recordValidation.js +16 -4
  118. package/dist/storage/sqliteSchema.js +106 -1741
  119. package/dist/storage/sqliteStore.js +16 -14
  120. package/dist/storage/storageSchema.js +8 -7
  121. package/dist/storage/storageVersions.js +23 -16
  122. package/dist/storage/taskStore.js +3 -29
  123. package/dist/storage/upgrade/upgradeOrchestrator.js +14 -112
  124. package/dist/task/nextAction.js +33 -23
  125. package/dist/task/task.js +17 -8
  126. package/dist/task/taskActivation.js +5 -4
  127. package/dist/telemetry/sqliteTelemetryStore.js +2 -2
  128. package/dist/verification/gateArtifact.js +5 -3
  129. package/dist/verification/verificationPlan.js +6 -7
  130. package/dist/web/assets/assetManifest.js +2 -0
  131. package/dist/web/assets/client/app.js +72 -3
  132. package/dist/web/assets/client/components.js +2 -1
  133. package/dist/web/assets/client/i18n.js +2 -0
  134. package/dist/web/assets/client/taskSummary.js +345 -0
  135. package/dist/web/assets/client/taskSurface.js +44 -26
  136. package/dist/web/assets/client/view.js +33 -0
  137. package/dist/web/assets/shell.js +1 -0
  138. package/dist/web/assets/styles/cards.js +21 -0
  139. package/dist/web/webServer.js +39 -5
  140. package/dist/web/webSessions.js +165 -0
  141. package/dist/web/webSnapshot.js +23 -0
  142. package/dist/web/webTaskSurface.js +23 -0
  143. package/dist/workItem/workItem.js +6 -6
  144. package/dist/workspace/cleanupInspection.js +1 -9
  145. package/dist/worktree/managedWorkspace.js +3 -3
  146. package/docs/managed-turn-and-session-runtime.md +50 -7
  147. package/docs/managed-turn-and-session-runtime.zh-CN.md +36 -5
  148. package/docs/observability/README.md +47 -0
  149. package/docs/observability/README.zh-CN.md +37 -0
  150. package/docs/project-refresh.md +9 -0
  151. package/docs/project-refresh.zh-CN.md +8 -0
  152. package/docs/provider-retry.md +35 -0
  153. package/docs/release-workflow.md +70 -214
  154. package/docs/release-workflow.zh-CN.md +53 -157
  155. package/docs/roles-and-configuration.md +30 -0
  156. package/docs/roles-and-configuration.zh-CN.md +20 -0
  157. package/docs/sqlite-control-plane-design.md +48 -289
  158. package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
  159. package/docs/storage-baseline.md +132 -0
  160. package/docs/storage-baseline.zh-CN.md +106 -0
  161. package/docs/task-delivery.md +110 -2
  162. package/docs/task-delivery.zh-CN.md +82 -2
  163. package/docs/task-discovery.md +9 -0
  164. package/docs/task-discovery.zh-CN.md +6 -0
  165. package/docs/testing/verification-levels.md +42 -164
  166. package/docs/testing/verification-levels.zh-CN.md +29 -115
  167. package/i18n/README.zh-CN.md +7 -4
  168. package/package.json +1 -1
  169. package/skills/yui-leader/SKILL.md +11 -0
  170. package/skills/yui-leader/references/execution.md +22 -0
  171. package/skills/yui-leader/references/planning.md +6 -2
  172. package/skills/yui-operator/SKILL.md +45 -13
  173. package/skills/yui-operator/references/task-delivery.md +99 -0
  174. package/skills/yui-runtime/SKILL.md +5 -3
  175. package/skills/yui-runtime/references/publication.md +56 -5
  176. package/skills/yui-runtime/references/recovery.md +12 -2
  177. package/dist/runtime/agentHostCompatibility.js +0 -127
  178. package/dist/storage/migrations/agentFailureContext.js +0 -22
  179. package/dist/storage/migrations/agentRunContract.js +0 -159
  180. package/dist/storage/migrations/artifactsToGit.js +0 -338
  181. package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
  182. package/dist/storage/migrations/currentInputContract.js +0 -86
  183. package/dist/storage/migrations/currentRuntimeContract.js +0 -228
  184. package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
  185. package/dist/storage/migrations/integrationContinuation.js +0 -105
  186. package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
  187. package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
  188. package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
  189. package/dist/storage/migrations/submitIntent.js +0 -126
  190. package/dist/storage/migrations/unifyHomeLayout.js +0 -925
  191. package/dist/storage/migrations/verificationPlanV1.js +0 -162
  192. package/dist/storage/migrations/verificationPolicy.js +0 -74
  193. package/dist/storage/migrations/workItemHistory.js +0 -46
@@ -12,7 +12,7 @@
12
12
  * the write transaction; conflict ->
13
13
  * StorageConflictError (transactionWithRevisionCas).
14
14
  * - Atomic durable write ........ WAL + synchronous=FULL; COMMIT == fsync.
15
- * - Mailbox per-target ordering . WorkMailbox v2 sequence/cursors in one row.
15
+ * - Mailbox per-target ordering . WorkMailbox sequence/cursors in one row.
16
16
  * - Exactly-once terminal state . conditional updates + UNIQUE(request_id)
17
17
  * on the durable outbox.
18
18
  * - Crash recovery .............. WAL rollback of uncommitted transactions;
@@ -62,8 +62,9 @@ import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtif
62
62
  import { validateWorkItem } from "../workItem/workItem.js";
63
63
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
64
64
  import { contextInputReferences, queryContextRecords } from "./contextRecords.js";
65
- import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, TELEMETRY_KEEP_PER_RUN, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
65
+ import { initializeSqliteSchema, validateSqliteSchema, SqliteSchemaError, TELEMETRY_KEEP_PER_RUN, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
66
66
  import { StorageSchemaError } from "./storageSchema.js";
67
+ import { STORAGE_FORMAT } from "./storageVersions.js";
67
68
  import { queryTaskCatalog } from "./taskCatalog.js";
68
69
  import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, pendingWakeupProjection, StorageCancelledError, StorageConflictError, StorageRecordError, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, validateYuiConfig } from "./taskStore.js";
69
70
  /** Read the immutable Home identity without opening a writable Store connection. */
@@ -139,7 +140,8 @@ export class SqliteTaskStore {
139
140
  #dirty = false;
140
141
  constructor(rootDir, _options = {}) {
141
142
  this.#rootDir = rootDir;
142
- mkdirSync(rootDir, { recursive: true, mode: 0o700 });
143
+ if (!_options.readonly)
144
+ mkdirSync(rootDir, { recursive: true, mode: 0o700 });
143
145
  const filename = _options.databaseFilename ?? "yui.db";
144
146
  const databasePath = join(rootDir, filename);
145
147
  // Remember whether the database file existed before this connection opened
@@ -147,18 +149,17 @@ export class SqliteTaskStore {
147
149
  // may seed its singleton rows; a store that opens an existing file missing
148
150
  // those rows has found corruption, not a bootstrap opportunity.
149
151
  const databaseExisted = existsSync(databasePath);
150
- this.#db = new Database(databasePath);
152
+ this.#db = new Database(databasePath, _options.readonly ? { readonly: true, fileMustExist: true } : {});
151
153
  try {
154
+ const existingSchema = databaseExisted ? validateSqliteSchema(this.#db) : undefined;
152
155
  // §4.1 / §9: WAL, no fsync weakening, FKs on, busy timeout for CLI contention.
153
- this.#db.pragma("journal_mode = WAL");
156
+ if (!_options.readonly)
157
+ this.#db.pragma("journal_mode = WAL");
154
158
  this.#db.pragma("synchronous = FULL");
155
159
  this.#db.pragma("foreign_keys = ON");
156
160
  this.#db.pragma("busy_timeout = 5000");
157
161
  this.#db.pragma("wal_autocheckpoint = 1000");
158
- migrateSqliteSchema(this.#db, {
159
- mode: !databaseExisted ? "apply" : "validate"
160
- });
161
- const schema = inspectSqliteSchemaMigrations(this.#db);
162
+ const schema = existingSchema ?? initializeSqliteSchema(this.#db);
162
163
  this.#openedSchemaHead = {
163
164
  version: schema.currentVersion,
164
165
  checksum: schema.currentChecksum
@@ -309,12 +310,13 @@ export class SqliteTaskStore {
309
310
  }
310
311
  /** Verify the SQLite schema head at the single write boundary. */
311
312
  #prepareWrite() {
312
- const current = this.#db.prepare("SELECT version, checksum FROM schema_migrations ORDER BY version DESC LIMIT 1").get();
313
- if (current?.version !== this.#openedSchemaHead.version
313
+ const current = this.#db.prepare("SELECT format, major || '.' || minor AS version, checksum FROM storage_schema WHERE id=1").get();
314
+ if (current?.format !== STORAGE_FORMAT
315
+ || current.version !== this.#openedSchemaHead.version
314
316
  || current.checksum !== this.#openedSchemaHead.checksum) {
315
- throw new SqliteSchemaMigrationError(`open Store schema head ${this.#openedSchemaHead.version}/${this.#openedSchemaHead.checksum} `
317
+ throw new SqliteSchemaError(`open Store format ${STORAGE_FORMAT} found ${String(current?.format)}; schema head ${this.#openedSchemaHead.version}/${this.#openedSchemaHead.checksum} `
316
318
  + `changed to ${String(current?.version)}/${String(current?.checksum)}; `
317
- + "reopen the Store with the active Yui version before writing", "admission");
319
+ + "reopen the Store with the active Yui version before writing");
318
320
  }
319
321
  }
320
322
  #bumpRevision() {
@@ -1754,7 +1756,7 @@ export class SqliteTaskStore {
1754
1756
  // -- active turns ----------------------------------------------------------
1755
1757
  #saveActiveRun(taskId, pointer, runId) {
1756
1758
  this.#mutate(() => {
1757
- const payload = this.#json({ schemaVersion: 3, runId });
1759
+ const payload = this.#json({ schemaVersion: 1, runId });
1758
1760
  this.#db.prepare(`INSERT INTO active_turns (task_id, pointer, turn_id, payload, updated_at) VALUES (?, ?, ?, ?, ?)
1759
1761
  ON CONFLICT(task_id, pointer) DO UPDATE SET turn_id = excluded.turn_id, payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, pointer, runId, payload, this.#now());
1760
1762
  });
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import Database from "better-sqlite3";
4
- import { inspectSqliteSchemaMigrations, storageMigrationPlan } from "./sqliteSchema.js";
5
- import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
4
+ import { inspectSqliteSchema, storageMinorUpgradePlan, SqliteSchemaError } from "./sqliteSchema.js";
5
+ import { compareStorageVersions, CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
6
6
  export { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
7
7
  export const CURRENT_DATABASE_FILENAME = "yui.db";
8
8
  export class StorageSchemaError extends Error {
@@ -47,7 +47,7 @@ export function inspectStorageSchema(rootDir) {
47
47
  return invalid(databasePath, error);
48
48
  }
49
49
  try {
50
- const migration = inspectSqliteSchemaMigrations(database);
50
+ const migration = inspectSqliteSchema(database);
51
51
  const fields = {
52
52
  currentVersion: migration.currentVersion,
53
53
  latestVersion: CURRENT_STORAGE_VERSION,
@@ -57,13 +57,13 @@ export function inspectStorageSchema(rootDir) {
57
57
  if (migration.currentVersion === CURRENT_STORAGE_VERSION) {
58
58
  return { status: "current", ...fields };
59
59
  }
60
- if (migration.currentVersion > CURRENT_STORAGE_VERSION) {
60
+ if (compareStorageVersions(migration.currentVersion, CURRENT_STORAGE_VERSION) > 0) {
61
61
  return { status: "unsupported", direction: "newer", ...fields };
62
62
  }
63
- if (migration.currentVersion < MIN_SUPPORTED_STORAGE_VERSION) {
63
+ if (compareStorageVersions(migration.currentVersion, MIN_SUPPORTED_STORAGE_VERSION) < 0) {
64
64
  return { status: "unsupported", direction: "older", ...fields };
65
65
  }
66
- const plan = storageMigrationPlan(migration.currentVersion);
66
+ const plan = storageMinorUpgradePlan(migration.currentVersion);
67
67
  if (plan === null) {
68
68
  return {
69
69
  status: "invalid",
@@ -114,6 +114,7 @@ function invalid(databasePath, error) {
114
114
  latestVersion: CURRENT_STORAGE_VERSION,
115
115
  minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
116
116
  databasePath,
117
- detail: error instanceof Error ? error.message : String(error)
117
+ detail: error instanceof Error ? error.message : String(error),
118
+ reason: error instanceof SqliteSchemaError && error.code === "STORAGE_FORMAT_UNSUPPORTED" ? "format" : "corruption"
118
119
  };
119
120
  }
@@ -1,16 +1,23 @@
1
- /**
2
- * The one durable compatibility axis for a Yui Home.
3
- *
4
- * Version 1 is the clean baseline introduced by Yui 0.15.0: SQLite's migration
5
- * ledger is authoritative and the historical layout/aggregate/record-version
6
- * manifest is not part of the active Home. Pre-0.15.0 Homes are outside this
7
- * compatibility line and remain untouched.
8
- *
9
- * Runtime code reads only {@link CURRENT_STORAGE_VERSION}. The explicit
10
- * upgrade boundary carries every forward migration from
11
- * {@link MIN_SUPPORTED_STORAGE_VERSION} to the current version so one target
12
- * CLI can upgrade any valid Home in that interval without installing
13
- * intermediate Yui releases.
14
- */
15
- export const MIN_SUPPORTED_STORAGE_VERSION = 1;
16
- export const CURRENT_STORAGE_VERSION = 37;
1
+ export const CURRENT_STORAGE_VERSION = "1.0";
2
+ export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
+ export const STORAGE_FORMAT = "yui-home";
4
+ export function isStorageVersion(value) {
5
+ if (typeof value !== "string" || !/^[1-9]\d*\.(0|[1-9]\d*)$/u.test(value))
6
+ return false;
7
+ return value.split(".").every(part => Number.isSafeInteger(Number(part)));
8
+ }
9
+ export function storageVersionParts(value) {
10
+ if (!isStorageVersion(value))
11
+ throw new Error(`Invalid storage version: ${String(value)}.`);
12
+ const [major, minor] = value.split(".").map(Number);
13
+ return { major: major, minor: minor };
14
+ }
15
+ export function compareStorageVersions(left, right) {
16
+ const a = storageVersionParts(left), b = storageVersionParts(right);
17
+ return Math.sign(a.major - b.major || a.minor - b.minor);
18
+ }
19
+ /** Cross-major conversion is never an ordinary update, even if explicitly pinned. */
20
+ export function isMinorStorageUpgrade(from, to) {
21
+ const a = storageVersionParts(from), b = storageVersionParts(to);
22
+ return a.major === b.major && b.minor > a.minor;
23
+ }
@@ -9,30 +9,10 @@ import { validateReleaseWorkflow } from "../release/releaseWorkflow.js";
9
9
  import { validateReviewConfig } from "../review/reviewConfig.js";
10
10
  import { validatePublicationReference } from "../task/publicationReference.js";
11
11
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
12
- export const CURRENT_CONFIG_SCHEMA_VERSION = 6;
13
- /** Current SQLite payload-family versions owned by this storage boundary. */
14
- export const CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION = 2;
15
- export const CURRENT_PROJECT_SCHEMA_VERSION = 6;
16
- export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 3;
17
- export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
18
- export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 5;
19
- export const CURRENT_TASK_SCHEMA_VERSION = 7;
20
- export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
21
- export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
22
- export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 4;
23
- export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
24
- export const CURRENT_WORK_ITEM_SCHEMA_VERSION = 15;
25
- export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION = 8;
26
- export const CURRENT_CHANGE_SET_SCHEMA_VERSION = 4;
27
- export const CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION = 6;
28
- export const CURRENT_MESSAGE_SCHEMA_VERSION = 3;
29
- export const CURRENT_INPUT_REQUEST_SCHEMA_VERSION = 3;
30
- export const CURRENT_DECISION_SCHEMA_VERSION = 1;
31
- export const CURRENT_MILESTONE_SCHEMA_VERSION = 2;
32
- export const CURRENT_EVENT_SCHEMA_VERSION = 2;
33
- export const CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION = 2;
12
+ export const CURRENT_CONFIG_SCHEMA_VERSION = 1;
13
+ export const CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION = 1;
34
14
  export const CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION = 1;
35
- export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 5;
15
+ export const CURRENT_WORK_MAILBOX_SCHEMA_VERSION = 1;
36
16
  export const CURRENT_PUBLICATION_REFERENCE_SCHEMA_VERSION = 1;
37
17
  export const CURRENT_PENDING_WAKEUP_SCHEMA_VERSION = 1;
38
18
  export const COMPLETION_SHELLS = ["bash", "zsh", "fish"];
@@ -62,12 +42,6 @@ export function executionLaneActiveRunKeyParts(key) {
62
42
  return null;
63
43
  }
64
44
  }
65
- /**
66
- * Nested-record versions consumed by the current SQLite payload validators.
67
- */
68
- export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 12;
69
- export const CURRENT_RUN_SCHEMA_VERSION = 5;
70
- export const CURRENT_INTEGRATION_QUEUE_SCHEMA_VERSION = 1;
71
45
  export class StorageRecordError extends Error {
72
46
  constructor(message) { super(message); this.name = "StorageRecordError"; }
73
47
  }
@@ -1,22 +1,15 @@
1
1
  import { join } from "node:path";
2
2
  import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
3
3
  import Database from "better-sqlite3";
4
- import { inspectAgentHostCompatibility, describeAgentHostUpgradeBlockers } from "../../runtime/agentHostCompatibility.js";
5
4
  import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation } from "../../runtime/runtimeObservation.js";
6
5
  import { RUNTIME_PROCESS_EXIT_TASK_EVENT, validateRuntimeProcessExitObservation } from "../../runtime/processExitObservation.js";
7
6
  import { SqliteTaskStore } from "../sqliteStore.js";
8
7
  import { storageBackupRoot } from "../homeLayout.js";
9
- import { preflightUnifyHomeLayout } from "../migrations/unifyHomeLayout.js";
10
- import { preflightCollapseWorktreeLayout } from "../migrations/collapseWorktreeLayout.js";
11
- import { preflightNotificationOnlyWakes } from "../migrations/notificationOnlyWakes.js";
12
- import { preflightVerificationPolicy } from "../migrations/verificationPolicy.js";
13
- import { preflightCurrentInputContract } from "../migrations/currentInputContract.js";
14
- import { preflightCurrentRuntimeContract } from "../migrations/currentRuntimeContract.js";
15
- import { migrateSqliteSchema, storageMigrationPlan } from "../sqliteSchema.js";
8
+ import { applySqliteMinorUpgrades, storageMinorUpgradePlan } from "../sqliteSchema.js";
16
9
  import { CURRENT_DATABASE_FILENAME, inspectStorageSchema } from "../storageSchema.js";
17
10
  import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storageVersions.js";
18
11
  /**
19
- * Upgrade one valid Home through the complete linear migration chain.
12
+ * Upgrade only recognized minor versions within the current Home major.
20
13
  *
21
14
  * Supported earlier versions are readable only here. Ordinary stores still
22
15
  * admit exactly {@link CURRENT_STORAGE_VERSION}; no old-shape normalizer or
@@ -30,6 +23,13 @@ export async function runStorageUpgrade(options) {
30
23
  return blocked({ ...corruptedClassification("Storage is not initialized."), uninitialized: true }, "uninitialized", "Yui storage is not initialized for this Home.", "Run `yui setup` with a new Home.");
31
24
  }
32
25
  if (state.status === "invalid") {
26
+ if (state.reason === "format") {
27
+ return blocked({
28
+ classification: { verdict: "UNSUPPORTED_FORMAT", status: "unsupported", detail: state.detail },
29
+ currentStorageVersion: CURRENT_STORAGE_VERSION,
30
+ minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
31
+ }, "unsupported", state.detail, "Keep this Home unchanged and use its matching release or the independent explicit conversion tool. Ordinary upgrades never cross storage formats.");
32
+ }
33
33
  return blocked(corruptedClassification(state.detail), "corruption", `Storage is invalid: ${state.detail}`, "Preserve this Home for diagnosis and restore it from a known-good backup.");
34
34
  }
35
35
  if (state.status === "unsupported") {
@@ -40,15 +40,6 @@ export async function runStorageUpgrade(options) {
40
40
  ? classification.classification.blocker.action
41
41
  : "Use a compatible Yui release.");
42
42
  }
43
- const incompatibleHosts = await inspectAgentHostCompatibility(options.home);
44
- if (incompatibleHosts.length > 0) {
45
- return {
46
- ...blocked(state.status === "current" ? currentClassification() : migratableClassification(state.currentVersion), "host-compatibility", `Existing Agent Hosts cannot safely use the target Controller/storage:\n${describeAgentHostUpgradeBlockers(incompatibleHosts)}`, "Keep the current Controller, Home and Host processes unchanged. Inspect the named Sessions and original pending "
47
- + "inputs/results; let current work settle, then use explicitly authorized Session replacement/cleanup at a safe "
48
- + "boundary before retrying the upgrade. This target cannot hot-patch legacy Host memory."),
49
- hosts: incompatibleHosts
50
- };
51
- }
52
43
  if (state.status === "current") {
53
44
  try {
54
45
  validateCurrentStore(options.home);
@@ -78,27 +69,12 @@ export async function runStorageUpgrade(options) {
78
69
  }
79
70
  };
80
71
  }
81
- const plan = storageMigrationPlan(state.currentVersion);
72
+ const plan = storageMinorUpgradePlan(state.currentVersion);
82
73
  if (plan === null) {
83
74
  return blocked(corruptedClassification(`No complete migration path exists from ${state.currentVersion} `
84
- + `to ${CURRENT_STORAGE_VERSION}.`), "corruption", "The storage migration registry is incomplete.", "Install a Yui release that carries the complete migration chain.");
75
+ + `to ${CURRENT_STORAGE_VERSION}.`), "corruption", "The storage migration registry is incomplete.", "Use a release with an explicit same-major minor upgrade path; cross-major conversion is separate.");
85
76
  }
86
77
  const classification = migratableClassification(state.currentVersion);
87
- // Genuine pre-migration safety check, shared by ALL migrating modes. The
88
- // unify-home data migration (18->19) physically relocates on-disk trees and
89
- // rewrites live launch pointers, so before any mode proceeds we run its READ-
90
- // ONLY preflight and refuse up front on the same conditions the migration would
91
- // throw on inside the apply transaction: a queued/running Job bound under a
92
- // relocating root, or a conflicting relocation target. The migration is applied
93
- // offline (after the operator has stopped this Home's writers), so the preflight
94
- // does not scan for live processes. Surfacing these here — before the Controller
95
- // is stopped or a backup is taken — is what makes `--dry-run` and the updater's
96
- // `--update-preflight` a genuine readiness signal rather than a mere list of
97
- // schema steps, while the in-transaction checks remain the authoritative guard
98
- // against anything that starts after this point.
99
- const migrationBlocked = preflightMigrationBlockers(options.home, plan, classification);
100
- if (migrationBlocked !== null)
101
- return migrationBlocked;
102
78
  if (options.mode === "update-preflight") {
103
79
  return {
104
80
  outcome: "update-preflight",
@@ -144,7 +120,7 @@ export async function runStorageUpgrade(options) {
144
120
  database.pragma("synchronous = FULL");
145
121
  database.pragma("foreign_keys = ON");
146
122
  database.pragma("busy_timeout = 5000");
147
- migrateSqliteSchema(database, { mode: "apply" });
123
+ applySqliteMinorUpgrades(database);
148
124
  migrationCommitted = true;
149
125
  }
150
126
  finally {
@@ -201,7 +177,7 @@ export async function runStorageUpgrade(options) {
201
177
  };
202
178
  }
203
179
  function validateCurrentStore(home) {
204
- const store = new SqliteTaskStore(home);
180
+ const store = new SqliteTaskStore(home, { readonly: true });
205
181
  try {
206
182
  store.validateCurrentRecords();
207
183
  for (const taskId of store.listTasks().map(({ id }) => id)) {
@@ -284,7 +260,7 @@ function unsupportedClassification(state) {
284
260
  const action = future
285
261
  ? "Use a newer Yui release."
286
262
  : "Preserve this Home for use with its matching historical Yui release, "
287
- + "or initialize a new Home with Yui 0.15.0 or later.";
263
+ + "or use the independent explicit converter for a different storage major.";
288
264
  return {
289
265
  classification: {
290
266
  verdict: "NEEDS_NEW_VERSION",
@@ -320,80 +296,6 @@ function blocked(classification, stage, message, action) {
320
296
  sceneUnchanged: true
321
297
  };
322
298
  }
323
- /**
324
- * Run each data migration's registered READ-ONLY preflight against the
325
- * current (pre-upgrade) database and, when any finds a blocker, return a
326
- * `blocked` result the caller surfaces before touching the Controller or the
327
- * Home. Returns `null` when the plan carries no registered readiness check,
328
- * or when every preflight is clear.
329
- *
330
- * Both the 23->24 unify and the 24->25 collapse migrations physically relocate
331
- * managed worktrees and share the same blocker shape (in-flight Job, conflicting
332
- * relocation target). When a Home is upgraded across both in one run, their
333
- * blockers are aggregated so the operator sees their readiness problems together.
334
- * Notification retirement separately rejects unsettled execution references.
335
- *
336
- * The database is opened read-only so the checks cannot mutate the authoritative
337
- * store, and the handle is always closed. An unexpected failure to evaluate a
338
- * preflight is itself a fail-closed blocker: we must not advance to an
339
- * irreversible migration on an unverifiable readiness signal.
340
- */
341
- function preflightMigrationBlockers(home, plan, classification) {
342
- const relocatesUnify = plan.some((step) => step.name === "unify-home-layout");
343
- const relocatesCollapse = plan.some((step) => step.name === "collapse-worktree-layout");
344
- const retiresRunWakes = plan.some((step) => step.name === "notification-only-wakes");
345
- const changesVerification = plan.some(step => step.name === "advisory-and-verification-policy");
346
- const changesInput = plan.some(step => step.name === "current-input-contract");
347
- const retiresRuntime = plan.some(step => step.name === "current-verification-and-owner-contract");
348
- if (!relocatesUnify && !relocatesCollapse && !retiresRunWakes && !changesVerification && !changesInput && !retiresRuntime)
349
- return null;
350
- let blockers;
351
- try {
352
- const database = new Database(join(home, CURRENT_DATABASE_FILENAME), {
353
- readonly: true,
354
- fileMustExist: true
355
- });
356
- try {
357
- blockers = [];
358
- if (relocatesUnify)
359
- blockers.push(...preflightUnifyHomeLayout(database).blockers);
360
- if (relocatesCollapse)
361
- blockers.push(...preflightCollapseWorktreeLayout(database).blockers);
362
- if (retiresRunWakes)
363
- preflightNotificationOnlyWakes(database);
364
- if (changesVerification)
365
- preflightVerificationPolicy(database);
366
- if (changesInput)
367
- preflightCurrentInputContract(database);
368
- if (retiresRuntime)
369
- preflightCurrentRuntimeContract(database);
370
- }
371
- finally {
372
- database.close();
373
- }
374
- }
375
- catch (error) {
376
- return {
377
- ...blocked(classification, "in-flight", `The storage migration readiness check could not be completed: ${messageOf(error)}`, "Resolve the named execution or resource boundary, preserving original input and results. "
378
- + "Confirm the relevant Runs and Jobs are settled, then rerun the upgrade.")
379
- };
380
- }
381
- if (blockers.length === 0)
382
- return null;
383
- return {
384
- outcome: "blocked",
385
- stage: "in-flight",
386
- message: "Refusing to migrate: the Home is not safe to relocate under the current layout yet. "
387
- + blockers.map((entry) => entry.detail).join("; ") + ".",
388
- action: "Resolve the reported condition(s) — let a queued/running Job finish or cancel it, and "
389
- + "clear any conflicting relocation target — then rerun the upgrade. The migration is applied "
390
- + "offline, so first confirm the Controller and any execution writers for this Home are "
391
- + "stopped. The authoritative Home is unchanged.",
392
- blockers: blockers.map((entry) => ({ reason: entry.reason, detail: entry.detail })),
393
- classification,
394
- sceneUnchanged: true
395
- };
396
- }
397
299
  function messageOf(error) {
398
300
  return error instanceof Error ? error.message : String(error);
399
301
  }
@@ -91,24 +91,7 @@ export function projectNextAction(facts) {
91
91
  ref: refs[1]
92
92
  }
93
93
  ],
94
- recommendedCommand: `yui task work edit ${task.id}/${dependencyIssue.workItemId} --clear-dependencies`
95
- });
96
- }
97
- const draftWork = selectOpenWorkItem(facts.workItems);
98
- if (draftWork.kind === "blocked") {
99
- const refs = [
100
- ref("work-item", draftWork.itemId),
101
- ref("work-item", draftWork.blockedBy)
102
- ];
103
- return buildAction(facts, {
104
- kind: "repair-protocol-inconsistency",
105
- reason: `Draft Work Item ${draftWork.itemId} depends on ${draftWork.blockedBy}, which is missing, retired, or not completed. Edit the Draft before activation.`,
106
- refs,
107
- conflicts: refs,
108
- preconditions: [
109
- { fact: `Draft dependency ${draftWork.blockedBy} is valid`, satisfied: false, ref: refs[1] }
110
- ],
111
- recommendedCommand: `yui task work edit ${task.id}/${draftWork.itemId} --clear-dependencies`
94
+ ...dependencyJudgment(task.id, dependencyIssue.workItemId)
112
95
  });
113
96
  }
114
97
  return buildAction(facts, {
@@ -323,18 +306,24 @@ export function projectNextAction(facts) {
323
306
  }
324
307
  const openWork = selectOpenWorkItem(facts.workItems);
325
308
  if (openWork?.kind === "blocked") {
309
+ const issue = draftWorkItemDependencyIssue(facts.workItems);
310
+ const itemId = issue?.workItemId ?? openWork.itemId;
311
+ const dependencyId = issue?.dependencyId ?? openWork.blockedBy;
326
312
  const refs = [
327
- ref("work-item", openWork.itemId),
328
- ref("work-item", openWork.blockedBy)
313
+ ref("work-item", itemId),
314
+ ref("work-item", dependencyId)
329
315
  ];
330
316
  return buildAction(facts, {
331
317
  kind: "repair-protocol-inconsistency",
332
- reason: `Work Item ${openWork.itemId} depends on ${openWork.blockedBy}, which is not completed or available.`,
318
+ reason: issue?.kind === "cycle"
319
+ ? `Work Item dependency cycle includes ${itemId}/${dependencyId}.`
320
+ : `Work Item ${itemId} depends on ${dependencyId}, which is ${issue === undefined ? "not completed" : "missing or retired"}.`,
333
321
  refs,
334
322
  conflicts: refs,
335
323
  preconditions: [
336
- { fact: `Dependency ${openWork.blockedBy} is completed`, satisfied: false, ref: refs[1] }
337
- ]
324
+ { fact: `Dependency ${dependencyId} is valid and completed`, satisfied: false, ref: refs[1] }
325
+ ],
326
+ ...dependencyJudgment(task.id, itemId)
338
327
  });
339
328
  }
340
329
  if (openWork?.kind === "ready") {
@@ -769,6 +758,27 @@ function reviewRoundConflict(round, activeRuns) {
769
758
  }
770
759
  return null;
771
760
  }
761
+ function dependencyJudgment(taskId, workItemId) {
762
+ return {
763
+ recommendedCommand: `yui task work show ${taskId}/${workItemId}`,
764
+ alternatives: [
765
+ {
766
+ kind: "inspect-dependencies",
767
+ reason: "Read the related requirements and dependency records before choosing a repair.",
768
+ recommendedCommand: `yui task work list ${taskId}`
769
+ },
770
+ {
771
+ kind: "revise-dependencies",
772
+ reason: "If the intended dependency graph is wrong, edit the affected WorkItem with --after for the complete intended set, preserving unrelated valid dependencies."
773
+ },
774
+ {
775
+ kind: "retire-obsolete-work",
776
+ reason: "Only if the requirement is no longer needed, deliberately retire it and reconcile its dependents."
777
+ }
778
+ ],
779
+ judgmentRequired: "The records identify a blocked dependency, not which business requirement or edge is wrong. Determine the intended graph from Task requirements; do not infer permission to delete dependencies."
780
+ };
781
+ }
772
782
  function selectOpenWorkItem(workItems) {
773
783
  const byId = new Map(workItems.map((item) => [item.id, item]));
774
784
  const openItems = workItems.filter((item) => OPEN_WORK_ITEM_STATUSES.has(item.status));
package/dist/task/task.js CHANGED
@@ -3,7 +3,7 @@ import { cancelTaskActivationRequest, recordTaskActivationEvidence, validateTask
3
3
  export function createTask(id, title, now, metadata = {}) {
4
4
  const timestamp = now.toISOString();
5
5
  return {
6
- schemaVersion: 7,
6
+ schemaVersion: 1,
7
7
  id: requireSafeIdentity(id, "Task id"),
8
8
  title: requireText(title, "Task title"),
9
9
  ...cloneMetadata(metadata),
@@ -16,7 +16,7 @@ export function createTask(id, title, now, metadata = {}) {
16
16
  /**
17
17
  * Persist the Task's durable workspace identity. The identity is immutable on
18
18
  * the Task: a second binding is rejected so restart/reconcile/attach can never
19
- * silently adopt a different (foreign or legacy) ref namespace.
19
+ * silently adopt a different workspace ref namespace.
20
20
  */
21
21
  export function bindTaskWorkspaceIdentity(task, identity, now) {
22
22
  validateTask(task);
@@ -304,7 +304,8 @@ export function reopenTask(task, now) {
304
304
  }
305
305
  : {}),
306
306
  status: "active",
307
- executionGate: { state: "enabled" },
307
+ // Reopening intent does not lift an independent execution stop. Only the
308
+ // explicit start operation may do that after its authority/cleanup checks.
308
309
  updatedAt: now.toISOString()
309
310
  });
310
311
  }
@@ -408,8 +409,19 @@ export function startTaskExecution(task, now) {
408
409
  });
409
410
  }
410
411
  export function validateTask(task) {
411
- if (task.schemaVersion !== 7)
412
- throw new Error("Task must use schemaVersion 7.");
412
+ if (task.schemaVersion !== 1)
413
+ throw new Error("Task must use schemaVersion 1.");
414
+ const fields = [
415
+ "schemaVersion", "id", "title", "type", "description", "priority", "tags", "dueAt",
416
+ "projectBindings", "cwd", "workspaceIdentity", "status", "executionGate", "activationRequest",
417
+ "settledActivationRequests", "completedAt", "completedBy", "completionSummary", "completionArtifactRefs",
418
+ "retiredAt", "retiredBy", "retirementSummary", "retirementIsolation", "replacementTaskId",
419
+ "archivedAt", "archivedBy", "archiveReason", "archiveSummary", "createdAt", "updatedAt"
420
+ ];
421
+ for (const field of Object.keys(task)) {
422
+ if (!fields.includes(field))
423
+ throw new Error(`Task contains an unsupported field: ${field}.`);
424
+ }
413
425
  requireSafeIdentity(task.id, "Task id");
414
426
  requireText(task.title, "Task title");
415
427
  if (task.type !== undefined)
@@ -491,9 +503,6 @@ export function validateTask(task) {
491
503
  normalizeProjectBindings(task.projectBindings);
492
504
  if (task.cwd !== undefined)
493
505
  requireText(task.cwd, "Task workspace");
494
- if (Object.hasOwn(task, "legacyDeliveryPath")) {
495
- throw new Error("Task contains the removed delivery-path field.");
496
- }
497
506
  const completionFields = [task.completedAt, task.completedBy, task.completionSummary];
498
507
  const hasAnyCompletion = completionFields.some((value) => value !== undefined);
499
508
  const hasAllCompletion = completionFields.every((value) => value !== undefined);
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { requireIdentity, requireTimestamp } from "../domain/validation.js";
3
+ import { requireIdentity, requireTimestamp, requireKnownFields } from "../domain/validation.js";
4
4
  import { recordOperationEvidence, validateOperationFacts } from "../kernel/operationFacts.js";
5
5
  export const TASK_ACTIVATION_IMPLEMENTATION = Object.freeze({
6
6
  id: "yui:task-activation",
@@ -229,6 +229,10 @@ export function validateTaskActivationRequest(request) {
229
229
  if (request.schemaVersion !== 1) {
230
230
  throw new Error("Activation request must use schemaVersion 1.");
231
231
  }
232
+ requireKnownFields(request, [
233
+ "schemaVersion", "operation", "startMode", "afterPlanningRun", "environmentPlan", "disposition",
234
+ "preparationId", "outcome", "requestedAt", "updatedAt"
235
+ ], "Activation request");
232
236
  validateOperationFacts(request.operation);
233
237
  if (request.operation.capability !== TASK_ACTIVATION_CAPABILITY) {
234
238
  throw new Error("Activation request capability is invalid.");
@@ -236,9 +240,6 @@ export function validateTaskActivationRequest(request) {
236
240
  if (!["immediate", "after-planning-turn"].includes(request.startMode)) {
237
241
  throw new Error(`Activation start mode is invalid: ${String(request.startMode)}.`);
238
242
  }
239
- if (Object.hasOwn(request, "origin")) {
240
- throw new Error("Activation contains retired origin metadata; use the storage upgrade boundary.");
241
- }
242
243
  if ((request.startMode === "after-planning-turn")
243
244
  !== (request.afterPlanningRun !== undefined)) {
244
245
  throw new Error("Activation deferral must name exactly the planning Turn it waits for.");
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
5
- import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
5
+ import { validateSqliteSchema } from "../storage/sqliteSchema.js";
6
6
  import { AsyncTaskStoreClient } from "../storage/storeRpc.js";
7
7
  import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
8
8
  /**
@@ -251,13 +251,13 @@ export class SqliteTelemetryStore {
251
251
  throw new Error(`Telemetry database not found: ${this.#path}`);
252
252
  }
253
253
  const db = opening = new Database(this.#path);
254
+ validateSqliteSchema(db);
254
255
  db.pragma("journal_mode = WAL");
255
256
  db.pragma("synchronous = FULL");
256
257
  db.pragma("foreign_keys = ON");
257
258
  // Cold diagnostic reads/retention must not wait behind a semantic writer.
258
259
  db.pragma("busy_timeout = 0");
259
260
  db.pragma("wal_autocheckpoint = 1000");
260
- migrateSqliteSchema(db, { mode: "validate" });
261
261
  this.#db = db;
262
262
  return db;
263
263
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isAbsolute } from "node:path";
3
- import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
3
+ import { requireIdentity, requireKnownFields, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
4
4
  /**
5
5
  * Issue 08: the reusable exact-SHA gate evidence record.
6
6
  *
@@ -80,6 +80,10 @@ export function recordGateArtifactReuse(artifact, now) {
80
80
  });
81
81
  }
82
82
  export function validateGateArtifact(artifact) {
83
+ requireKnownFields(artifact, [
84
+ "schemaVersion", "key", "projectId", "level", "commit", "planId", "planVersion", "planDigest", "toolchainDigest",
85
+ "boundary", "steps", "generator", "status", "outcome", "reuseCount", "createdAt", "completedAt", "lastUsedAt"
86
+ ], "GateArtifact");
83
87
  if (artifact.schemaVersion !== GATE_ARTIFACT_SCHEMA_VERSION) {
84
88
  throw new Error(`GateArtifact must use schemaVersion ${GATE_ARTIFACT_SCHEMA_VERSION}.`);
85
89
  }
@@ -139,8 +143,6 @@ export function validateGateArtifact(artifact) {
139
143
  throw new Error(`GateArtifact step ${step.name} logBytes is invalid.`);
140
144
  }
141
145
  }
142
- if (Object.hasOwn(artifact, "potentialReuseCount"))
143
- throw new Error("GateArtifact contains retired shadow metrics.");
144
146
  requirePositiveInteger(artifact.reuseCount + 1, "GateArtifact reuseCount");
145
147
  requireTimestamp(artifact.createdAt, "GateArtifact createdAt");
146
148
  requireTimestamp(artifact.lastUsedAt, "GateArtifact lastUsedAt");