@zq-silk/yui 0.4.2 → 0.5.1

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.
@@ -1,10 +1,17 @@
1
1
  /** The one production transition registry and its baseline-to-current delivery gate. */
2
+ import { generateHomeIdentity } from "../../repository/homeIdentity.js";
2
3
  import { latestStorageVersionState } from "../upgrade/recordVersions.js";
3
4
  import { assertBaselineConsistency, baselineStorageVersionState } from "./baseline.js";
4
5
  import { planMigration } from "./planner.js";
5
6
  import { MigrationRegistry } from "./registry.js";
6
7
  const FINAL_REVIEW_AGGREGATE_FROM_VERSION = 16;
7
8
  const FINAL_REVIEW_AGGREGATE_TO_VERSION = 17;
9
+ const HOME_IDENTITY_AGGREGATE_FROM_VERSION = 17;
10
+ const HOME_IDENTITY_AGGREGATE_TO_VERSION = 18;
11
+ const PROJECT_FROM_VERSION = 2;
12
+ const PROJECT_TO_VERSION = 3;
13
+ const TASK_FROM_VERSION = 3;
14
+ const TASK_TO_VERSION = 4;
8
15
  const WORK_ITEM_FROM_VERSION = 6;
9
16
  const WORK_ITEM_TO_VERSION = 7;
10
17
  const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
@@ -38,6 +45,16 @@ export function createProductionStorageRegistry() {
38
45
  transform: migrateAggregateV16ToV17,
39
46
  declaredEffects: []
40
47
  })
48
+ .registerOfflineMigration({
49
+ axis: "aggregate",
50
+ fromVersion: HOME_IDENTITY_AGGREGATE_FROM_VERSION,
51
+ toVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION,
52
+ preconditions: requireAggregateV17Snapshot,
53
+ transform: migrateAggregateV17ToV18,
54
+ declaredEffects: []
55
+ })
56
+ .registerOfflineMigration(projectOwnershipStep())
57
+ .registerOfflineMigration(taskWorkspaceIdentityStep())
41
58
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
42
59
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
43
60
  .registerOfflineMigration(workItemExecutionGroupHistoryStep())
@@ -470,6 +487,169 @@ function requireAggregateV16Snapshot(snapshot) {
470
487
  throw new Error("Aggregate 16->17 migration requires state.json schemaVersion 16 to match schema.json.");
471
488
  }
472
489
  }
490
+ /**
491
+ * Introduce the durable Home identity. A v18 aggregate always carries one; a
492
+ * Home with no state.json yet gets its identity when its first state is
493
+ * written, so this step only mints one when state.json already exists.
494
+ */
495
+ function migrateAggregateV17ToV18(snapshot) {
496
+ const schemaManifest = {
497
+ ...snapshot.schemaManifest,
498
+ aggregateSchemaVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION
499
+ };
500
+ if (snapshot.state === null)
501
+ return { schemaManifest, state: null };
502
+ const state = snapshot.state;
503
+ if (state.homeIdentity !== undefined) {
504
+ throw new Error("Aggregate 17->18 migration found an unexpected homeIdentity.");
505
+ }
506
+ return {
507
+ schemaManifest,
508
+ state: {
509
+ ...state,
510
+ schemaVersion: HOME_IDENTITY_AGGREGATE_TO_VERSION,
511
+ homeIdentity: generateHomeIdentity(new Date())
512
+ }
513
+ };
514
+ }
515
+ function requireAggregateV17Snapshot(snapshot) {
516
+ if (snapshot.schemaManifest.aggregateSchemaVersion
517
+ !== HOME_IDENTITY_AGGREGATE_FROM_VERSION) {
518
+ throw new Error("Aggregate 17->18 migration requires schema.json aggregateSchemaVersion 17.");
519
+ }
520
+ if (snapshot.state !== null
521
+ && snapshot.state.schemaVersion !== HOME_IDENTITY_AGGREGATE_FROM_VERSION) {
522
+ throw new Error("Aggregate 17->18 migration requires state.json schemaVersion 17 to match schema.json.");
523
+ }
524
+ }
525
+ /**
526
+ * Project ownership is a new required field. Every pre-v3 Project is a
527
+ * user-registered checkout, so the historical binding is `external`; a managed
528
+ * Home-owned repository is only ever created explicitly by the new binding
529
+ * path. The version transition is durable and centralized.
530
+ */
531
+ function projectOwnershipStep() {
532
+ return {
533
+ axis: "record",
534
+ recordKind: "project",
535
+ fromVersion: PROJECT_FROM_VERSION,
536
+ toVersion: PROJECT_TO_VERSION,
537
+ preconditions: requireProjectV2Family,
538
+ transform: migrateProjectV2ToV3,
539
+ declaredEffects: []
540
+ };
541
+ }
542
+ function requireProjectV2Family(snapshot) {
543
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
544
+ if (manifestVersions.project !== PROJECT_FROM_VERSION) {
545
+ throw new Error(`Record project migration requires manifest version ${PROJECT_FROM_VERSION}.`);
546
+ }
547
+ if (snapshot.state === null)
548
+ return;
549
+ const projects = snapshot.state.projects;
550
+ if (projects === undefined)
551
+ return;
552
+ const map = asObject(projects, "project map");
553
+ for (const [projectId, rawProject] of Object.entries(map)) {
554
+ const project = asObject(rawProject, `Project ${projectId}`);
555
+ if (project.schemaVersion !== PROJECT_FROM_VERSION) {
556
+ throw new Error(`Project ${projectId} must use schemaVersion ${PROJECT_FROM_VERSION}.`);
557
+ }
558
+ }
559
+ }
560
+ function migrateProjectV2ToV3(snapshot) {
561
+ requireProjectV2Family(snapshot);
562
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
563
+ const schemaManifest = {
564
+ ...snapshot.schemaManifest,
565
+ recordVersions: { ...manifestVersions, project: PROJECT_TO_VERSION }
566
+ };
567
+ if (snapshot.state === null)
568
+ return { schemaManifest, state: null };
569
+ const projects = snapshot.state.projects;
570
+ if (projects === undefined) {
571
+ return { schemaManifest, state: { ...snapshot.state } };
572
+ }
573
+ const map = asObject(projects, "project map");
574
+ const nextProjects = {};
575
+ for (const [projectId, rawProject] of Object.entries(map)) {
576
+ const project = asObject(rawProject, `Project ${projectId}`);
577
+ nextProjects[projectId] = {
578
+ ...project,
579
+ schemaVersion: PROJECT_TO_VERSION,
580
+ ownership: "external"
581
+ };
582
+ }
583
+ return {
584
+ schemaManifest,
585
+ state: { ...snapshot.state, projects: nextProjects }
586
+ };
587
+ }
588
+ /**
589
+ * Task v4 adds the optional durable workspace identity. Historical Tasks have
590
+ * no identity; they keep working against their existing (legacy) refs until the
591
+ * controlled rebuild mints one. This adjacent step performs no field rewrite,
592
+ * preserving old records while keeping a pre-v4 Task out of the strict current
593
+ * parser.
594
+ */
595
+ function taskWorkspaceIdentityStep() {
596
+ return {
597
+ axis: "record",
598
+ recordKind: "task",
599
+ fromVersion: TASK_FROM_VERSION,
600
+ toVersion: TASK_TO_VERSION,
601
+ preconditions: requireTaskV3Family,
602
+ transform: migrateTaskV3ToV4,
603
+ declaredEffects: []
604
+ };
605
+ }
606
+ function requireTaskV3Family(snapshot) {
607
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
608
+ if (manifestVersions.task !== TASK_FROM_VERSION) {
609
+ throw new Error(`Record task migration requires manifest version ${TASK_FROM_VERSION}.`);
610
+ }
611
+ if (snapshot.state === null)
612
+ return;
613
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
614
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
615
+ const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
616
+ const task = aggregate.task;
617
+ if (task === undefined)
618
+ continue;
619
+ const record = asObject(task, `Task ${taskId}`);
620
+ if (record.schemaVersion !== TASK_FROM_VERSION) {
621
+ throw new Error(`Task ${taskId} must use schemaVersion ${TASK_FROM_VERSION}.`);
622
+ }
623
+ }
624
+ }
625
+ function migrateTaskV3ToV4(snapshot) {
626
+ requireTaskV3Family(snapshot);
627
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
628
+ const schemaManifest = {
629
+ ...snapshot.schemaManifest,
630
+ recordVersions: { ...manifestVersions, task: TASK_TO_VERSION }
631
+ };
632
+ if (snapshot.state === null)
633
+ return { schemaManifest, state: null };
634
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
635
+ const nextTasks = {};
636
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
637
+ const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
638
+ if (aggregate.task === undefined) {
639
+ nextTasks[taskId] = { ...aggregate };
640
+ continue;
641
+ }
642
+ const task = asObject(aggregate.task, `Task ${taskId}`);
643
+ nextTasks[taskId] = {
644
+ ...aggregate,
645
+ task: { ...task, schemaVersion: TASK_TO_VERSION }
646
+ };
647
+ }
648
+ return {
649
+ schemaManifest,
650
+ state: { ...snapshot.state, tasks: nextTasks }
651
+ };
652
+ }
473
653
  /**
474
654
  * A version bump is deliverable only when the shared planner resolves the full
475
655
  * adjacent path. This also covers target-only record families as explicit 0->1
@@ -8,4 +8,4 @@
8
8
  /** Version of the on-disk layout (`schema.json`, root `state.json`, and locks). */
9
9
  export const CURRENT_STORAGE_LAYOUT_VERSION = 6;
10
10
  /** Version of the authoritative aggregate stored in `state.json`. */
11
- export const CURRENT_AGGREGATE_SCHEMA_VERSION = 17;
11
+ export const CURRENT_AGGREGATE_SCHEMA_VERSION = 18;
@@ -14,6 +14,7 @@ import { validateReviewConfig } from "../review/reviewConfig.js";
14
14
  import { validateReviewRound } from "../review/reviewRound.js";
15
15
  import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
16
16
  import { assertProjectCatalog, validateProject } from "../repository/project.js";
17
+ import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
17
18
  import { validateAgentProfile } from "../profile/agentProfile.js";
18
19
  import { validateChangeSet } from "../integration/changeSet.js";
19
20
  import { validateIntegrationAttempt } from "../integration/integrationAttempt.js";
@@ -32,6 +33,7 @@ export const STORAGE_STATE_FILE = "state.json";
32
33
  /** The root StorageState schema is the persisted aggregate document version. */
33
34
  export const CURRENT_STORAGE_STATE_SCHEMA_VERSION = CURRENT_AGGREGATE_SCHEMA_VERSION;
34
35
  export const CURRENT_CONFIG_SCHEMA_VERSION = 1;
36
+ export const CURRENT_HOME_IDENTITY_SCHEMA_VERSION = 1;
35
37
  export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
36
38
  /**
37
39
  * Persisted StorageState/StoredTask family versions owned by this boundary.
@@ -42,11 +44,11 @@ export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
42
44
  * below, but are not independent record-axis families.
43
45
  */
44
46
  export const CURRENT_CONFIGURED_AGENT_SCHEMA_VERSION = 2;
45
- export const CURRENT_PROJECT_SCHEMA_VERSION = 2;
47
+ export const CURRENT_PROJECT_SCHEMA_VERSION = 3;
46
48
  export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
47
49
  export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
48
50
  export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
49
- export const CURRENT_TASK_SCHEMA_VERSION = 3;
51
+ export const CURRENT_TASK_SCHEMA_VERSION = 4;
50
52
  export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
51
53
  export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 3;
52
54
  export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
@@ -134,6 +136,7 @@ export class FileTaskStore {
134
136
  });
135
137
  }
136
138
  getConfig() { return clone(this.#state().config); }
139
+ getHomeIdentity() { return clone(this.#state().homeIdentity); }
137
140
  saveConfig(config) {
138
141
  const stored = versioned(config, CURRENT_CONFIG_SCHEMA_VERSION, "Yui config");
139
142
  validateYuiConfig(stored);
@@ -1172,6 +1175,7 @@ function emptyState() {
1172
1175
  return {
1173
1176
  schemaVersion: CURRENT_STORAGE_STATE_SCHEMA_VERSION,
1174
1177
  revision: 0,
1178
+ homeIdentity: generateHomeIdentity(new Date()),
1175
1179
  config: { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION },
1176
1180
  configuredAgents: {},
1177
1181
  projects: {},
@@ -1249,6 +1253,7 @@ function parseState(raw) {
1249
1253
  exact(state, [
1250
1254
  "schemaVersion",
1251
1255
  "revision",
1256
+ "homeIdentity",
1252
1257
  "config",
1253
1258
  "configuredAgents",
1254
1259
  "projects",
@@ -1261,6 +1266,12 @@ function parseState(raw) {
1261
1266
  if (state.schemaVersion !== CURRENT_STORAGE_STATE_SCHEMA_VERSION || !Number.isInteger(state.revision) || state.revision < 0)
1262
1267
  throw new StorageRecordError("Storage state schemaVersion/revision is invalid.");
1263
1268
  const result = clone(state);
1269
+ try {
1270
+ result.homeIdentity = validateHomeIdentity(versioned(result.homeIdentity, CURRENT_HOME_IDENTITY_SCHEMA_VERSION, "Home identity"));
1271
+ }
1272
+ catch (error) {
1273
+ throw new StorageRecordError(error instanceof Error ? error.message : String(error));
1274
+ }
1264
1275
  result.config = versioned(result.config, CURRENT_CONFIG_SCHEMA_VERSION, "Yui config");
1265
1276
  validateYuiConfig(result.config);
1266
1277
  parseMap(result.configuredAgents, (value, key) => {
package/dist/task/task.js CHANGED
@@ -1,7 +1,8 @@
1
+ import { validateTaskWorkspaceIdentity } from "../repository/taskWorkspaceIdentity.js";
1
2
  export function createTask(id, title, now, metadata = {}) {
2
3
  const timestamp = now.toISOString();
3
4
  return {
4
- schemaVersion: 3,
5
+ schemaVersion: 4,
5
6
  id: requireSafeIdentity(id, "Task id"),
6
7
  title: requireText(title, "Task title"),
7
8
  ...cloneMetadata(metadata),
@@ -10,6 +11,32 @@ export function createTask(id, title, now, metadata = {}) {
10
11
  updatedAt: timestamp
11
12
  };
12
13
  }
14
+ /**
15
+ * Persist the Task's durable workspace identity. The identity is immutable on
16
+ * the Task: a second binding is rejected so restart/reconcile/attach can never
17
+ * silently adopt a different (foreign or legacy) ref namespace.
18
+ */
19
+ export function bindTaskWorkspaceIdentity(task, identity, now) {
20
+ validateTask(task);
21
+ const valid = validateTaskWorkspaceIdentity(identity);
22
+ if (valid.taskId !== task.id) {
23
+ throw new Error(`Task workspace identity belongs to another Task: ${valid.taskId}.`);
24
+ }
25
+ if (task.workspaceIdentity !== undefined) {
26
+ const existing = validateTaskWorkspaceIdentity(task.workspaceIdentity);
27
+ if (existing.token !== valid.token
28
+ || existing.generatedAt !== valid.generatedAt
29
+ || existing.homeId !== valid.homeId) {
30
+ throw new Error(`Task workspace identity is already bound and immutable: ${task.id}.`);
31
+ }
32
+ return task;
33
+ }
34
+ return validateTask({
35
+ ...task,
36
+ workspaceIdentity: valid,
37
+ updatedAt: now.toISOString()
38
+ });
39
+ }
13
40
  export function activateTask(task, now) {
14
41
  if (task.status === "archived")
15
42
  throw new Error(`Cannot activate archived Task: ${task.id}.`);
@@ -144,8 +171,8 @@ export function isTaskArchived(task) {
144
171
  return task.status === "archived";
145
172
  }
146
173
  export function validateTask(task) {
147
- if (task.schemaVersion !== 3)
148
- throw new Error("Task must use schemaVersion 3.");
174
+ if (task.schemaVersion !== 4)
175
+ throw new Error("Task must use schemaVersion 4.");
149
176
  requireSafeIdentity(task.id, "Task id");
150
177
  requireText(task.title, "Task title");
151
178
  if (!["draft", "active", "completed", "retired", "archived"].includes(task.status)) {
@@ -156,6 +183,12 @@ export function validateTask(task) {
156
183
  if (Date.parse(task.updatedAt) < Date.parse(task.createdAt)) {
157
184
  throw new Error("Task updatedAt cannot precede createdAt.");
158
185
  }
186
+ if (task.workspaceIdentity !== undefined) {
187
+ const identity = validateTaskWorkspaceIdentity(task.workspaceIdentity);
188
+ if (identity.taskId !== task.id) {
189
+ throw new Error(`Task workspace identity belongs to another Task: ${identity.taskId}.`);
190
+ }
191
+ }
159
192
  if (task.priority !== undefined
160
193
  && !["low", "medium", "high", "urgent"].includes(task.priority)) {
161
194
  throw new Error(`Task priority is invalid: ${String(task.priority)}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,