@timurproko/a1 0.2.7-dev.1244 → 0.2.7-dev.1249

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 (49) hide show
  1. package/dist/features/owned-ui/settings-app.js +2 -2
  2. package/dist/foundation/process-containment/addon.d.ts +5 -0
  3. package/dist/foundation/release/bootstrap.js +34 -31
  4. package/dist/foundation/release/cohort-selection.d.ts +0 -6
  5. package/dist/foundation/release/cohort-selection.js +5 -13
  6. package/dist/foundation/release/cohort-state.d.ts +2 -0
  7. package/dist/foundation/release/endpoint-lock.d.ts +2 -0
  8. package/dist/foundation/release/endpoint-lock.js +37 -0
  9. package/dist/foundation/release/endpoint-recovery.d.ts +10 -0
  10. package/dist/foundation/release/endpoint-recovery.js +45 -0
  11. package/dist/foundation/release/endpoints.d.ts +10 -2
  12. package/dist/foundation/release/endpoints.js +42 -24
  13. package/dist/foundation/release/index.d.ts +2 -0
  14. package/dist/foundation/release/index.js +2 -0
  15. package/dist/foundation/release/process-cleanup.js +12 -2
  16. package/dist/foundation/release/supervisor-identity.d.ts +15 -0
  17. package/dist/foundation/release/supervisor-identity.js +63 -0
  18. package/dist/foundation/release/update.js +2 -2
  19. package/dist/foundation/storage/control-store.d.ts +5 -3
  20. package/dist/foundation/storage/control-store.js +8 -7
  21. package/dist/foundation/supervision/main.js +3 -1
  22. package/dist/foundation/supervision/server.js +80 -53
  23. package/dist/integrations/pi/engine/compaction-progress.js +13 -5
  24. package/dist/native/darwin-arm64/manifest.json +3 -3
  25. package/dist/native/darwin-arm64/process-guardian.node +0 -0
  26. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  27. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  28. package/dist/native/darwin-x64/manifest.json +3 -3
  29. package/dist/native/darwin-x64/process-guardian.node +0 -0
  30. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  31. package/dist/native/darwin-x64/terminal-host.node +0 -0
  32. package/dist/native/linux-x64/manifest.json +3 -3
  33. package/dist/native/linux-x64/process-guardian.node +0 -0
  34. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  35. package/dist/native/linux-x64/terminal-host.node +0 -0
  36. package/dist/native/win32-x64/manifest.json +3 -3
  37. package/dist/native/win32-x64/process-guardian.node +0 -0
  38. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  39. package/dist/native/win32-x64/terminal-host.node +0 -0
  40. package/dist/ui/settings/declarations.d.ts +1 -1
  41. package/dist/ui/settings/declarations.js +1 -1
  42. package/docs/architecture/project-structure.md +24 -16
  43. package/docs/architecture/toolchain.md +1 -1
  44. package/docs/ci-release-runbook.md +2 -0
  45. package/docs/launch-recovery.md +23 -0
  46. package/docs/local-worktree-cleanup.md +4 -0
  47. package/docs/openspec-archive-automation.md +11 -3
  48. package/docs/validation.md +3 -1
  49. package/package.json +3 -3
@@ -30,7 +30,7 @@ const LAUNCH_INSTANCES_TABLE = `
30
30
  export class ControlStore {
31
31
  database;
32
32
  bootNonce;
33
- constructor(path, bootNonce = null) {
33
+ constructor(path, bootNonce = null, options = {}) {
34
34
  this.bootNonce = bootNonce;
35
35
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
36
36
  this.database = new DatabaseSync(path);
@@ -38,7 +38,7 @@ export class ControlStore {
38
38
  this.database.exec("PRAGMA journal_mode = WAL");
39
39
  this.database.exec("PRAGMA foreign_keys = ON");
40
40
  this.migrate();
41
- if (bootNonce !== null)
41
+ if (bootNonce !== null && options.reconcileOnOpen !== false)
42
42
  this.reconcilePriorBootLaunchInstances(bootNonce);
43
43
  }
44
44
  catch (error) {
@@ -284,16 +284,16 @@ export class ControlStore {
284
284
  .run(terminalState, completedAt, JSON.stringify(outcome), instanceId, ownerClientId, this.bootNonce);
285
285
  return Number(result.changes) === 1;
286
286
  }
287
- loadActiveLaunchInstances() {
287
+ loadActiveLaunchInstances(bootNonce) {
288
288
  return this.database.prepare(`SELECT * FROM launch_instances
289
- WHERE state IN ('requested', 'active', 'stopping') ORDER BY created_at, id`).all()
289
+ WHERE state IN ('requested', 'active', 'stopping') ${bootNonce === undefined ? "" : "AND owner_boot_nonce = ?"} ORDER BY created_at, id`).all(...(bootNonce === undefined ? [] : [bootNonce]))
290
290
  .map(launchInstanceFromRow);
291
291
  }
292
292
  loadLaunchInstance(instanceId) {
293
293
  const row = this.database.prepare("SELECT * FROM launch_instances WHERE id = ?").get(instanceId);
294
294
  return row ? launchInstanceFromRow(row) : null;
295
295
  }
296
- reconcilePriorBootLaunchInstances(bootNonce, reconciledAt = new Date().toISOString()) {
296
+ reconcilePriorBootLaunchInstances(bootNonce, reconciledAt = new Date().toISOString(), protectedBoots = []) {
297
297
  const outcome = {
298
298
  kind: "interrupted",
299
299
  reason: "supervisor-disconnect",
@@ -301,8 +301,9 @@ export class ControlStore {
301
301
  };
302
302
  const result = this.database.prepare(`UPDATE launch_instances
303
303
  SET state = 'interrupted', completed_at = ?, outcome_json = ?
304
- WHERE state IN ('requested', 'active', 'stopping') AND owner_boot_nonce <> ?`)
305
- .run(reconciledAt, JSON.stringify(outcome), bootNonce);
304
+ WHERE state IN ('requested', 'active', 'stopping') AND owner_boot_nonce <> ?
305
+ ${protectedBoots.length === 0 ? "" : `AND owner_boot_nonce NOT IN (${protectedBoots.map(() => "?").join(",")})`}`)
306
+ .run(reconciledAt, JSON.stringify(outcome), bootNonce, ...protectedBoots);
306
307
  return Number(result.changes);
307
308
  }
308
309
  close() {
@@ -36,7 +36,9 @@ export async function runSupervisor(arguments_ = []) {
36
36
  paths = { ...productPaths, ...resolveCohortEndpoint(productPaths, release.releaseId) };
37
37
  stage = "storage";
38
38
  const bootNonce = randomUUID();
39
- const store = new ControlStore(paths.databasePath, bootNonce);
39
+ // Concurrency: opening SQLite does not establish endpoint authority. The winning listener
40
+ // reconciles old boots only after binding; losing startup attempts must not mutate live rows.
41
+ const store = new ControlStore(paths.databasePath, bootNonce, { reconcileOnOpen: false });
40
42
  const cohortState = new CohortStateStore(paths.dataDir);
41
43
  server = new SupervisorServer(store, paths, release, bootNonce, undefined, undefined, undefined, async () => {
42
44
  const transaction = await new UpdateTransactionStore(paths.dataDir).read();
@@ -5,7 +5,7 @@ import { platform } from "node:os";
5
5
  import { dirname } from "node:path";
6
6
  import { assertLaunchInstanceOutcome, assertNativeProcessIdentity, assertProcessContainmentIdentity, } from "../lifecycle/index.js";
7
7
  import { encodeFrame, isCommandMessage, isControlHello, LineFrameDecoder, localControlHello, MAX_CONTROL_FRAME_BYTES, negotiateControlFeatures } from "../protocol/index.js";
8
- import { processIsAlive } from "../release/index.js";
8
+ import { listRecordedEndpoints, readEndpointMetadata, supervisorProcessState, currentSupervisorStartIdentity, withEndpointLock, processIsAlive } from "../release/index.js";
9
9
  import { ControlStore } from "../storage/index.js";
10
10
  import { resolveProductPaths } from "./paths.js";
11
11
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
@@ -43,7 +43,7 @@ export async function commitEndpointMetadata(path, source, operations = {}) {
43
43
  export class SupervisorServer {
44
44
  id = randomUUID();
45
45
  bootNonce;
46
- pidStartIdentity = `${process.pid}:${Math.floor(Date.now() - process.uptime() * 1_000)}`;
46
+ pidStartIdentity = currentSupervisorStartIdentity();
47
47
  startedAt = new Date().toISOString();
48
48
  paths;
49
49
  #server = null;
@@ -84,7 +84,7 @@ export class SupervisorServer {
84
84
  this.supersededPollMs = supersededPollMs;
85
85
  this.bootNonce = bootNonce;
86
86
  this.paths = paths;
87
- for (const instance of store.loadActiveLaunchInstances())
87
+ for (const instance of store.loadActiveLaunchInstances(bootNonce))
88
88
  this.#instances.set(instance.id, instance);
89
89
  }
90
90
  snapshot() {
@@ -98,41 +98,60 @@ export class SupervisorServer {
98
98
  };
99
99
  }
100
100
  async listen() {
101
- if (this.#server)
102
- throw new Error("supervisor is already listening");
103
- await mkdir(this.paths.runtimeDir, { recursive: true, mode: 0o700 });
104
- await mkdir(this.paths.endpointsDir, { recursive: true, mode: 0o700 });
105
- if (platform() !== "win32") {
106
- if (this.paths.endpointDirectory)
107
- await ensureManagedEndpointDirectory(dirname(this.paths.endpoint));
108
- const existing = await lstat(this.paths.endpoint).catch(error => {
109
- if (error.code === "ENOENT")
110
- return null;
111
- throw error;
112
- });
113
- if (existing) {
114
- if (await endpointIsLive(this.paths.endpoint)) {
115
- throw Object.assign(new Error(PRODUCT_TEXT.diagnostic(`supervisor already owns ${this.paths.endpoint}`)), { code: "EADDRINUSE" });
101
+ await withEndpointLock(this.paths.endpointMetadataPath, async () => {
102
+ if (this.#server)
103
+ throw new Error("supervisor is already listening");
104
+ await mkdir(this.paths.runtimeDir, { recursive: true, mode: 0o700 });
105
+ await mkdir(this.paths.endpointsDir, { recursive: true, mode: 0o700 });
106
+ const recorded = await readEndpointMetadata(this.paths.endpointMetadataPath);
107
+ if (recorded && await supervisorProcessState(recorded) !== "gone") {
108
+ throw Object.assign(new Error("another supervisor still owns this endpoint"), { code: "EADDRINUSE" });
109
+ }
110
+ if (platform() !== "win32") {
111
+ if (this.paths.endpointDirectory)
112
+ await ensureManagedEndpointDirectory(dirname(this.paths.endpoint));
113
+ const existing = await lstat(this.paths.endpoint).catch(error => {
114
+ if (error.code === "ENOENT")
115
+ return null;
116
+ throw error;
117
+ });
118
+ if (existing) {
119
+ if (await endpointIsLive(this.paths.endpoint)) {
120
+ throw Object.assign(new Error(PRODUCT_TEXT.diagnostic(`supervisor already owns ${this.paths.endpoint}`)), { code: "EADDRINUSE" });
121
+ }
122
+ await rm(this.paths.endpoint, { force: true });
116
123
  }
117
- await rm(this.paths.endpoint, { force: true });
118
124
  }
119
- }
120
- const server = createServer(socket => this.#attach(socket));
121
- this.#server = server;
122
- await new Promise((resolve, reject) => {
123
- server.once("error", reject);
124
- server.listen(this.paths.endpoint, () => {
125
- this.#ownsEndpoint = true;
126
- server.off("error", reject);
127
- resolve();
125
+ const server = createServer(socket => this.#attach(socket));
126
+ this.#server = server;
127
+ await new Promise((resolve, reject) => {
128
+ server.once("error", reject);
129
+ server.listen(this.paths.endpoint, () => {
130
+ this.#ownsEndpoint = true;
131
+ server.off("error", reject);
132
+ resolve();
133
+ });
128
134
  });
135
+ // Concurrency: only the bound winner may reconcile shared control data. Other cohort boots
136
+ // remain protected even when temporarily unresponsive; their instances never become ours.
137
+ await withEndpointLock(this.paths.databasePath, async () => {
138
+ // Concurrency: all cohorts sharing SQLite serialize boot reconciliation plus publication, so a new
139
+ // cohort cannot insert instances between another cohort's inventory and stale-row update.
140
+ const protectedBoots = [];
141
+ for (const other of await listRecordedEndpoints(this.paths)) {
142
+ if (other.paths.endpointMetadataPath !== this.paths.endpointMetadataPath
143
+ && await supervisorProcessState(other.metadata) !== "gone")
144
+ protectedBoots.push(other.metadata.bootNonce);
145
+ }
146
+ this.store.reconcilePriorBootLaunchInstances(this.bootNonce, new Date().toISOString(), protectedBoots);
147
+ await this.#writeEndpointMetadata();
148
+ });
149
+ if (this.readActiveReleaseId) {
150
+ const poll = setInterval(() => void this.#refreshCohortRole(), this.supersededPollMs);
151
+ poll.unref();
152
+ this.#supersededPoll = poll;
153
+ }
129
154
  });
130
- await this.#writeEndpointMetadata();
131
- if (this.readActiveReleaseId) {
132
- const poll = setInterval(() => void this.#refreshCohortRole(), this.supersededPollMs);
133
- poll.unref();
134
- this.#supersededPoll = poll;
135
- }
136
155
  }
137
156
  // Invariant: superseding a cohort never interrupts its held instances; it retires only when empty.
138
157
  async #refreshCohortRole() {
@@ -167,27 +186,32 @@ export class SupervisorServer {
167
186
  this.#supersededPoll = null;
168
187
  for (const client of this.#clients)
169
188
  client.destroy();
170
- try {
171
- await this.#metadataWrites.catch(() => undefined);
172
- if (this.#ownsEndpoint) {
173
- // Concurrency: remove only our publication, before releasing the listening endpoint.
174
- // A contender that failed to bind must never erase the successful owner's discovery file.
175
- const metadata = await readFile(this.paths.endpointMetadataPath, "utf8").then(source => JSON.parse(source)).catch(() => null);
176
- if (metadata?.supervisorId === this.id && metadata.bootNonce === this.bootNonce) {
177
- await rm(this.paths.endpointMetadataPath, { force: true });
189
+ // Concurrency: disconnect reconciliation can still commit a terminal outcome after its
190
+ // bounded process wait. Settle it before retiring the endpoint and closing shared storage.
191
+ await Promise.allSettled(this.#reconciliations.values());
192
+ await withEndpointLock(this.paths.endpointMetadataPath, async () => {
193
+ try {
194
+ await this.#metadataWrites.catch(() => undefined);
195
+ if (this.#ownsEndpoint) {
196
+ // Concurrency: remove only our publication, before releasing the listening endpoint.
197
+ // A contender that failed to bind must never erase the successful owner's discovery file.
198
+ const metadata = await readFile(this.paths.endpointMetadataPath, "utf8").then(source => JSON.parse(source)).catch(() => null);
199
+ if (metadata?.supervisorId === this.id && metadata.bootNonce === this.bootNonce) {
200
+ await rm(this.paths.endpointMetadataPath, { force: true });
201
+ }
202
+ // Platform: Node unlinks its Unix socket when the server closes; unlinking it early would
203
+ // let a replacement bind before the old server finishes releasing that path.
178
204
  }
179
- // Platform: Node unlinks its Unix socket when the server closes; unlinking it early would
180
- // let a replacement bind before the old server finishes releasing that path.
181
205
  }
182
- }
183
- finally {
184
- // Platform: a metadata sharing error must not leave a bound but undiscoverable listener.
185
- if (this.#server)
186
- await new Promise(resolve => this.#server?.close(() => resolve()));
187
- this.#server = null;
188
- this.#ownsEndpoint = false;
189
- this.store.close();
190
- }
206
+ finally {
207
+ // Platform: a metadata sharing error must not leave a bound but undiscoverable listener.
208
+ if (this.#server)
209
+ await new Promise(resolve => this.#server?.close(() => resolve()));
210
+ this.#server = null;
211
+ this.#ownsEndpoint = false;
212
+ this.store.close();
213
+ }
214
+ });
191
215
  }
192
216
  async closeForReleaseReplacement(stopAgents) {
193
217
  await this.close(stopAgents);
@@ -434,6 +458,8 @@ export class SupervisorServer {
434
458
  const existing = this.#reconciliations.get(instanceId);
435
459
  if (existing)
436
460
  return existing;
461
+ if (this.#closing)
462
+ return Promise.resolve();
437
463
  const reconciliation = this.#performReconciliation(instanceId).finally(() => this.#reconciliations.delete(instanceId));
438
464
  this.#reconciliations.set(instanceId, reconciliation);
439
465
  return reconciliation;
@@ -518,6 +544,7 @@ export class SupervisorServer {
518
544
  supervisorId: this.id,
519
545
  pid: process.pid,
520
546
  pidStartIdentity: this.pidStartIdentity,
547
+ nativeStartIdentity: this.pidStartIdentity,
521
548
  bootNonce: this.bootNonce,
522
549
  endpoint: this.paths.endpoint,
523
550
  databasePath: this.paths.databasePath,
@@ -19,7 +19,7 @@ export function observeCompactionProgress(session, onProgress) {
19
19
  let expected = DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS;
20
20
  let reported = null;
21
21
  const report = (percent = Math.min(MAX_STREAMING_PERCENT, Math.floor((100 * streamed) / expected))) => {
22
- if (percent === reported)
22
+ if (reported !== null && percent <= reported)
23
23
  return;
24
24
  reported = percent;
25
25
  onProgress(percent);
@@ -29,6 +29,18 @@ export function observeCompactionProgress(session, onProgress) {
29
29
  if (!active || disposed)
30
30
  return stream;
31
31
  const observed = generation;
32
+ // Concurrency: Pi resolves result() before an independent iterator receives the terminal
33
+ // event, so register first to publish 100 before Pi can emit compaction_end.
34
+ try {
35
+ void stream.result().then(result => {
36
+ if (observed !== generation || !active || disposed || result.stopReason !== "stop")
37
+ return;
38
+ report(COMPLETE_PERCENT);
39
+ }, () => undefined);
40
+ }
41
+ catch {
42
+ // Invariant: optional observation never affects the stream returned to Pi.
43
+ }
32
44
  void (async () => {
33
45
  report();
34
46
  // Invariant: observation never affects the request; a stream that cannot be iterated ends observation only.
@@ -41,10 +53,6 @@ export function observeCompactionProgress(session, onProgress) {
41
53
  report();
42
54
  }
43
55
  }
44
- // Invariant: normal exhaustion is the only observed completion boundary. The real
45
- // compaction_end event owns work-state exit; errors and stale generations never claim 100%.
46
- if (observed === generation && active && !disposed)
47
- report(COMPLETE_PERCENT);
48
56
  }
49
57
  catch {
50
58
  return;
@@ -7,11 +7,11 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "arm64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-11-06T06:57:04.765Z",
10
+ "builtAt": "2026-11-06T18:31:39.019Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
- "sha256": "5a314f46b164ef006957528ea7b1743e104895449a1b5a9e3c8b5adbf7122d33",
14
- "size": 456080
13
+ "sha256": "9ab579426034c752ef8ed53a789566f5b7cfe102149d30c28f4bdb60e73107c5",
14
+ "size": 456272
15
15
  },
16
16
  "provenance": {
17
17
  "language": "Rust",
@@ -9,11 +9,11 @@
9
9
  "architecture": "arm64",
10
10
  "target": "aarch64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-11-06T07:00:26.658Z",
12
+ "builtAt": "2026-11-06T18:34:18.531Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "b2548f95b9660b11b6d28fb4b35f254cde9940d9d19233517bb70507ffcf76ab",
16
- "size": 6626528
15
+ "sha256": "7dcfd045769d22ffd59f11974a7d5d490199ee67eec288e92224be4c049857d7",
16
+ "size": 6645216
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,11 +7,11 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-11-06T06:57:12.911Z",
10
+ "builtAt": "2026-11-06T18:31:46.232Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
- "sha256": "b4d66ed5920c3b2d4ac1cb658fe87549cdad2f81948dbe8cfa698ff6d87162e1",
14
- "size": 444248
13
+ "sha256": "65f3a469a6faa9b578bc095e7559012775ee85e5b93cbc9451c62604f769146e",
14
+ "size": 452648
15
15
  },
16
16
  "provenance": {
17
17
  "language": "Rust",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-11-06T07:03:27.415Z",
12
+ "builtAt": "2026-11-06T18:37:01.604Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "cd1103472889d53e2e039cf7673b2fe6e9e0937234591b1202234d0c1c357d2a",
16
- "size": 7226904
15
+ "sha256": "691f821e9a4d2da71866dbe09742909d8f8c46fc330bdb917e8b81de2fa83450",
16
+ "size": 7249568
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,11 +7,11 @@
7
7
  "platform": "linux",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-11-06T06:56:45.470Z",
10
+ "builtAt": "2026-11-06T18:26:24.402Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
- "sha256": "18442075f91a02d22071548c0a2c882c69ee04b2e48e0b1130f23a86c5b258ca",
14
- "size": 491536
13
+ "sha256": "64232d4c827bb7f69c14612986f9b6780a0593d8842cd039a411b6bfe29cd5da",
14
+ "size": 500264
15
15
  },
16
16
  "provenance": {
17
17
  "language": "Rust",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-unknown-linux-gnu",
11
11
  "capability": "supported",
12
- "builtAt": "2026-11-06T06:59:02.808Z",
12
+ "builtAt": "2026-11-06T18:28:48.745Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "f5abfe9464780c9acdf85dfbf8a0f194bf0355188ab23feaa09cb7df84363088",
16
- "size": 8010960
15
+ "sha256": "70f658de12b148666d0414e8a0fd0d8a10844d7a0ee6e2b11c2013477f4db91f",
16
+ "size": 8033296
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,11 +7,11 @@
7
7
  "platform": "win32",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-11-06T06:57:32.942Z",
10
+ "builtAt": "2026-11-06T18:27:06.362Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
- "sha256": "eb4519e51e5c77a740b9c290e28def2293363675b103a21388532fa73d208893",
14
- "size": 402432
13
+ "sha256": "e8f4a87276da9375c998d9cdb28c2ed5d4ed857089001c338a689bb045d75dad",
14
+ "size": 414208
15
15
  },
16
16
  "provenance": {
17
17
  "language": "Rust",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-pc-windows-msvc",
11
11
  "capability": "supported",
12
- "builtAt": "2026-11-06T06:59:37.419Z",
12
+ "builtAt": "2026-11-06T18:29:48.572Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "148523e2ee5648749b4c5b84518e3d120aacf247cdf8620c3fa205df5c960dff",
16
- "size": 7131136
15
+ "sha256": "712c6ed93b1d9fa307a6e12045222c1cbf34be1044360d1a0070d694bdfe2f55",
16
+ "size": 7155712
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -230,7 +230,7 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
230
230
  }>;
231
231
  readonly tabBarRightItems: Readonly<{
232
232
  id: "tabBarRightItems";
233
- label: "Right side";
233
+ label: "Widgets";
234
234
  section: Readonly<{
235
235
  id: "tabs";
236
236
  title: "Tab Bar";
@@ -177,7 +177,7 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
177
177
  }),
178
178
  tabBarRightItems: Object.freeze({
179
179
  id: "tabBarRightItems",
180
- label: "Right side",
180
+ label: "Widgets",
181
181
  section: TABS_SECTION,
182
182
  description: "Choose whether the tab bar's right side shows the executing Version, Time, both, or neither.",
183
183
  application: "live",
@@ -47,6 +47,8 @@ Three layer boundaries hold regardless of the DAG: `src/contracts/*` import noth
47
47
 
48
48
  `src/cli` contains command policy but delegates runtime work. `src/composition` is the concrete dependency-injection boundary: it may know both neutral contracts and Pi implementations, while product features receive vendor-neutral ports. Foundation modules never import product features. Pi package knowledge remains inside the Pi adapter owners.
49
49
 
50
+ Release-owned endpoint observation and recovery coordinate with supervision-owned publication and storage-owned boot reconciliation through the process guardian's OS-released gates; see [startup recovery](../launch-recovery.md) for the identity, concurrency, and diagnostic boundaries.
51
+
50
52
  Shared code moves to foundation only when it is product-agnostic, has a precise contract, and has more than one real consumer. Do not create `core`, `common`, `utils`, or `misc` dumping grounds.
51
53
 
52
54
  ## Feature ownership
@@ -93,31 +95,37 @@ Build output mirrors the production namespaces directly under ignored `dist/`, w
93
95
 
94
96
  Every task worktree must be created at `{working-dir}/.worktrees/<task-id>`, where `{working-dir}` is the agent session's initial working directory. The `.worktrees` directory is inside that working directory, not beside it. For working directory `D:/Git/a1`, `D:/Git/a1/.worktrees/<task-id>` is correct and `D:/Git/a1-<task-id>` is forbidden.
95
97
 
96
- From the initial working directory, run `a1 session worktrees` to inspect cooperative live ownership before selecting an existing checkout. A `busy` or `unverifiable` worktree is unavailable. An `available` checkout may be reused only when its exact branch, OpenSpec change, and pull request match the requested stream and atomic `a1 session link-worktree <absolute-worktree>` succeeds; names, similar files, recency, cleanliness, and ancestry are not ownership evidence. Otherwise create a fresh task branch/worktree, link it from the owning A1 session, and continue to address it explicitly:
98
+ ### Common Git workflow
97
99
 
98
- ```sh
99
- a1 session worktrees
100
- git fetch origin develop
101
- git worktree add -b <type>/<short-description> .worktrees/<task-id> origin/develop
102
- a1 session link-worktree <absolute-worktree>
103
- git -C .worktrees/<task-id> status
104
- ```
100
+ External agents use ordinary Git without an A1 installation, runtime, or session link. Keep primary on `develop`, run the bounded cleanup sweep and relay its `lines`, then inspect `git worktree list --porcelain` from primary. Inventory is read-only and advisory; it is not atomic editing exclusion or permission to adopt a checkout.
105
101
 
106
- Claude Code sessions have no A1 shell-tool session, so they link with Claude Code's `EnterWorktree` tool by task name. The tracked `.claude/settings.json` wires a `WorktreeCreate` hook, `scripts/governance/claude-worktree-create.mjs`, that creates `.worktrees/<task-id>` detached at fresh `origin/develop`, or returns it when it is already registered, so Claude Code anchors the session there without an approval prompt. The agent then creates the task branch inside it:
102
+ For a new delivery, create a unique absent task path and a new topic branch from freshly fetched `origin/develop`, unless the maintainer selected another base. For example, with primary at `D:/Git/a1` and unused task/branch names:
107
103
 
108
- ```sh
109
- git -C .worktrees/<task-id> switch -c <type>/<short-description>
104
+ ```bash
105
+ cd /d/Git/a1 && git fetch origin develop && git worktree add -b feature/example-task .worktrees/example-task origin/develop && git -C .worktrees/example-task rev-parse --show-toplevel --git-common-dir && git -C .worktrees/example-task status --short --branch
110
106
  ```
111
107
 
112
- Passing an absolute `path` to `EnterWorktree` is reserved for a worktree created by hand, from the primary checkout, before any shell command in that session; Claude Code otherwise keeps its shell tools anchored to the primary checkout and refuses their commands. The matching `WorktreeRemove` hook declines removal, so sessions leave with `ExitWorktree` `keep` and worktrees are removed only by the cleanup tool.
108
+ Successful creation by this session establishes provenance for that fresh checkout. Before planning, implementation, test, or delivery-documentation edits, verify the exact canonical root, common Git repository, intended branch, and working-tree state. Relative Git directory results must resolve to the primary repository's common directory. A path/branch collision never permits force creation, overwriting, or adoption of the existing checkout. A context mismatch means stop edits and report the blocker; never fall back to task edits in primary.
109
+
110
+ ### Resume and ownership
111
+
112
+ Reuse an existing checkout only when its exact branch, OpenSpec change, and PR match the requested stream and continuing ownership or explicit handoff from the prior owner or maintainer is established without conflicting ownership evidence. Names, similar files, recency, cleanliness, and ancestry are not ownership evidence. Inspect Git worktree locks and the cleanup journal's `status`; claim any released cleanup registration through the [cleanup ownership protocol](../local-worktree-cleanup.md#resume-or-recover-ownership) before editing. An owned registration requires matching owner authority. Neither an unregistered path nor a successful cleanup claim proves that no other editor is using it.
113
+
114
+ Known live editing claims, foreign cleanup ownership, Git locks, and unverifiable handoffs block reuse. Stop edits and report the blocker, or use a safe fresh worktree without overriding, recovering, unlocking, unlinking, or waiting out another owner. Preserve the existing delivery's branch/history/PR: do not force a branch checked out elsewhere or create a replacement PR to evade ownership. If the same stream cannot safely continue, report that concrete blocker.
115
+
116
+ Repeat exact-context and ownership verification when resuming or switching streams. Handoffs name the absolute worktree, branch, change, PR, and whether editing ownership is being handed over. This is cooperative coordination, not enforcement against arbitrary external editors. Missing A1 integration alone never blocks external agents and is not evidence of a conflicting owner.
117
+
118
+ ### Environment-specific adapters
119
+
120
+ Inside an active A1 shell-tool session only, run `a1 session worktrees` and `a1 session link-worktree <absolute-worktree>` before edits to acquire the runtime's editing claim and footer association. An `available` result still needs atomic linking; a `busy` or `unverifiable` owner, failed association, lost race, or different returned path blocks edits there. A failed stream switch preserves the prior association. External agents use the common Git workflow instead of fabricating runtime identities or starting A1 just to satisfy repository policy.
113
121
 
114
- Inventory is read-only and advisory because another runtime may win a later race. A successful link response confirming the exact canonical worktree is required before planning, implementation, test, or delivery-documentation edits. If inventory or linking fails, reports a live/unverifiable owner, loses a race, or confirms another path, stop task edits and report the blocker; use a separate fresh worktree rather than overriding, unlinking, or recovering that owner. A session resuming an existing delivery or switching streams atomically claims and links the exact worktree before editing it.
122
+ A stale Git registration elsewhere whose directory is missing and whose row is `prunable` may be pruned before retrying inventory; this does not permit removing a live checkout. Mutation-gate corruption and transient A1 startup-registration failures have a separate [worktree-link recovery procedure](../worktree-link-recovery.md). Recovery never overrides a live or unverifiable editing claim.
115
123
 
116
- Mutation-gate corruption and transient startup-registration failures have a separate [worktree-link recovery procedure](../worktree-link-recovery.md). Automatic recovery never overrides a live or unverifiable editing claim; ambiguous legacy gates remain protected.
124
+ Claude Code uses its established `EnterWorktree` tool by task name. The tracked `.claude/settings.json` wires `scripts/governance/claude-worktree-create.mjs` to create `.worktrees/<task-id>` detached at fresh `origin/develop`, or return an already registered path. The agent must still establish ownership before reusing that path and create the topic branch in a new detached worktree before editing. Passing an absolute `path` to `EnterWorktree` is reserved for a worktree created by hand, from primary before any shell command in that session. The matching `WorktreeRemove` hook declines removal: leave with `ExitWorktree` `keep` and use only the guarded cleanup tool for removal. Other agents do not need these tools or hooks.
117
125
 
118
- The link acquires a cooperative live-session editing claim and switches bare A1's footer repository context and pull-request discovery. It does not change process or tool cwd, create a Git worktree lock, or register, release, or authorize cleanup. Clean unlink/disposal releases live editing authority; a crashed owner becomes reusable only after exact process death is verified. Repository commands therefore keep an explicit worktree path. The primary worktree stays on `develop` for integration and must not be used for task edits. Do not edit, adopt, move, or remove another session's worktree.
126
+ A1 linking acquires a cooperative live-session editing claim and switches footer repository/PR discovery; an existing draft PR is not required when linking first succeeds. Clean unlink/disposal releases that runtime claim; a crashed A1 owner becomes reusable only after exact process death is verified. Runtime association does not change process or tool cwd, create a Git lock, or register, release, or authorize cleanup. Context verification and editing handoff also grant no acceptance, finalization, merge, deletion, or cleanup authority. Repository commands therefore keep an explicit worktree path. The primary worktree stays on `develop` for integration and must not be used for task edits. Do not edit, adopt, move, or remove another session's worktree.
119
127
 
120
- An agent that directly chooses a transient file path for a PR/comment body, command payload, captured output, temporary patch/diff, or ad hoc log must place it beneath the linked worktree's exact `.artifacts/` root, never the primary checkout, another worktree, OS temp, home/desktop, or a sibling path. Such scratch remains ignored, unstaged, uncommitted, disposable, non-authoritative, and secret-free. This repository path rule does not relocate storage internally selected by tools, product runtime code, or hermetic tests.
128
+ An agent that directly chooses a transient file path for a PR/comment body, command payload, captured output, temporary patch/diff, or ad hoc log must place it beneath the verified task worktree's exact `.artifacts/` root, never the primary checkout, another worktree, OS temp, home/desktop, or a sibling path. Such scratch remains ignored, unstaged, uncommitted, disposable, non-authoritative, and secret-free. This repository path rule does not relocate storage internally selected by tools, product runtime code, or hermetic tests.
121
129
 
122
130
  ## Documentation and comments
123
131
 
@@ -40,7 +40,7 @@ Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the e
40
40
 
41
41
  `npm run build` compiles `native/process-guardian` as the host platform's Node-API 9 addon, places `process-guardian.node` under `dist/native/<platform>-<architecture>/`, loads it to observe its version identity, and writes the v2 integrity manifest. It also builds the current platform's release-profile `terminal-host.node`, whose v2 manifest binds the addon, `dist/foundation/resident-tabs/host-entry.js`, and notices, so the ordinary `npm run build && ./scripts/dev` handoff exercises resident tabs without deployment. On Windows the normal build reuses a compatible ignored terminal-library cache or downloads the newest compatible library from the current branch or a successful CI run belonging to a pull request already merged into `develop`, then links it under the crate's manifest verification; it never silently invokes the quarantined Zig package fetch. An unavailable or mismatched prerequisite fails the build instead of leaving a nominally successful single-agent-only tree. The npm `prepare` lifecycle and validation preflight use the explicit core build because selected native lanes assemble and certify terminal hosts separately.
42
42
 
43
- Preview and stable candidate workflows build the addons on isolated Windows, Linux, and macOS arm64 runners, cross-compile the macOS x64 addons on the arm64 runner with `--target x86_64-apple-darwin`, and assemble all targets before packing. Those release runners retain their explicit `npm run build:terminal-host` step and pinned Zig input rather than consuming a workstation cache. Cross-compiled artifacts record a Mach-O architecture check when they cannot be loaded on the producing runner. Supported native code is always loaded by Node; an absent, incompatible, or unsupported addon fails before runtime startup rather than falling back to PID-only cleanup. The process-guardian addon inherits terminal handles but transports no terminal bytes. Its separate Node-hosted exit watcher restores terminal state after guardian failure, subject to the documented node-wide-kill gap. See [`process-guardian-provenance.md`](process-guardian-provenance.md), [`terminal-host-provenance.md`](terminal-host-provenance.md), and [`resident-tabs.md`](resident-tabs.md).
43
+ Preview and stable candidate workflows build the addons on isolated Windows, Linux, and macOS arm64 runners, cross-compile the macOS x64 addons on the arm64 runner with `--target x86_64-apple-darwin`, and assemble all targets before packing. Those release runners retain their explicit `npm run build:terminal-host` step and pinned Zig input rather than consuming a workstation cache; Development validation lanes may instead restore a CI-built release addon from an exact-input cache and re-verify it (see [`../ci-release-runbook.md`](../ci-release-runbook.md)). Cross-compiled artifacts record a Mach-O architecture check when they cannot be loaded on the producing runner. Supported native code is always loaded by Node; an absent, incompatible, or unsupported addon fails before runtime startup rather than falling back to PID-only cleanup. The process-guardian addon inherits terminal handles but transports no terminal bytes. Its separate Node-hosted exit watcher restores terminal state after guardian failure, subject to the documented node-wide-kill gap. See [`process-guardian-provenance.md`](process-guardian-provenance.md), [`terminal-host-provenance.md`](terminal-host-provenance.md), and [`resident-tabs.md`](resident-tabs.md).
44
44
 
45
45
  ## Gates
46
46
 
@@ -103,6 +103,8 @@ The real three-release `update-predecessor` scenario is exhaustive because its f
103
103
 
104
104
  Development outcomes report each owner/scope invocation separately while sharing authenticated build/package preparation. The aggregate reports setup, scope, job, aggregate-processing, total runner, and runner-critical-path durations. The acceptance targets are at most eight minutes of runner critical path and five minutes for one PR-required scope; an over-target result remains unmet without retries, timeout increases, workload reduction, or mutable installation caches. Hosted queue time is reported separately when available and is not counted as test execution.
105
105
 
106
+ Candidate-packing Development lanes restore the release `terminal-host.node`, its manifest, and its third-party notices from an exact-input `actions/cache` entry keyed by the native tree, `rust-toolchain.toml`, the provenance configuration, the build scripts, the pinned Zig version, and the runner image. On a hit the lane skips the Rust toolchain, Zig, and `cargo` build steps, re-records the manifest's `host-entry.js` digest from the current TypeScript build (`build-terminal-host.mjs --reuse`), and still runs `verify-native-artifacts.mjs --local` before the build receipt; any tampered or mismatched restored file fails the lane instead of rebuilding. A miss builds as before and saves the result. This is build-output reuse with a complete input key, not a mutable installation cache, and publication lanes keep building in-run. Before this reuse, each `packs_candidate` lane spent 2.6 to 3.4 minutes on the release build, and a resident-tabs pull request repeated it in four lanes. Likewise the Linux terminal-host lane caches the `cargo-fuzz 0.13.2` binary by version and nightly date instead of compiling it each run, and the three terminal-host partition jobs run only their `role_integration` slice, so the crate's unit tests execute once per target per run rather than four times.
107
+
106
108
  Inspect local committed and worktree impact without running tests:
107
109
 
108
110
  ```sh
@@ -0,0 +1,23 @@
1
+ # Startup recovery after process termination
2
+
3
+ A1 reconciles recoverable runtime ownership automatically when the user starts it again after a process crash or forced termination. Users should not need to find PIDs, delete session or control databases, restart a supervisor, or reboot to clear a stale record.
4
+
5
+ ## Recovery boundary
6
+
7
+ Supervisor metadata carries an additive native process-creation token. Endpoint handshakes still use the existing supervisor, boot, release, and opaque start-identity fields for compatibility; new supervisors use their native token in that start field as well. A failed probe is retried; native process inspection distinguishes a terminated process object or reused PID from the recorded owner. Older records remain readable: an absent process is reclaimable, while a live legacy PID is not treated as dead merely because its timestamp format differs. Windows and macOS native creation times strictly later than a legacy publication, with a one-second margin, can disprove that recorded ownership. Tick-only Linux legacy evidence remains conservative until absence or a matching handshake establishes the outcome.
8
+
9
+ Endpoint retirement requires the same publication and a fresh dead-owner observation under a native file-descriptor gate. The gate is never deleted; the OS releases its lock on exit, including forced exit. Supervisor binding, Unix stale-socket removal, metadata publication, and closure use that gate. Losing startups cannot delete the winner's endpoint. A separate shared-database gate serializes boot reconciliation and publication across cohorts. Only the bound winner reconciles old rows, and recorded surviving cohort boots remain protected.
10
+
11
+ Successful recovery is silent and continues the original profile, launch mode, and session selection. Saved sessions are not deleted or rewritten by endpoint recovery. Resident holders and their session-writer locks retain their independent ownership; restarting a supervisor is not permission to stop a resident tab or create a second session writer.
12
+
13
+ ## Exceptional failures
14
+
15
+ An uninspectable or genuinely unresponsive live owner is preserved after bounded retries. Empty saved instance lists are not authority to terminate it. The command returns a short recovery message instead of an uncaught stack or the old internal uncertain-live-ownership paragraph. Missing native recovery support or inaccessible runtime storage remains an installation or permission failure, not proof that an owner is dead.
16
+
17
+ A recovery attempt may replace one bounded `<endpoint-metadata>.recovery.json` summary containing only a reason, attempt count, and timestamp. It is diagnostic only, contains no session data or credentials, and never grants cleanup authority. The `.authority` gate files are also not user cleanup targets.
18
+
19
+ This restores launch availability, not killed process memory. Killing every Node process also kills the terminal-restoration watcher; no alternative executable is started to evade that limitation. Real OS permission failures, damaged installations, and persistently live unresponsive services cannot be converted into safe success by ignoring ownership.
20
+
21
+ ## Regression boundaries
22
+
23
+ The focused fixtures use isolated runtime/data roots and recorded fixture process identities. They cover supervisor, guardian, root-runtime, recovery-gate, PID-reuse, concurrent-startup, and Windows all-recorded-Node loss, plus native resident server/holder/tab loss in the resident lane. They never terminate processes by image name. Another managed runtime, a separate cohort, saved session bytes, and an unrelated sentinel process establish the preservation boundaries.
@@ -184,6 +184,10 @@ After a low-level release, request a pass from the stable checkout if queue clea
184
184
 
185
185
  ## Resume or recover ownership
186
186
 
187
+ This protocol works for external agents without an A1 installation, runtime, or session link. First verify the exact delivery branch, change, and PR plus continuing editing ownership or explicit handoff under the [worktree protocol](architecture/project-structure.md#development-worktrees). Inspect Git locks and this tool's `status` from the primary checkout. A familiar path, clean status, or missing A1 runtime is not evidence of another editor's absence. Known live editing claims, foreign cleanup ownership, Git locks, and unverifiable handoffs block reuse.
188
+
189
+ For a worktree with no cleanup registration, this tool supplies no ownership claim: a fresh checkout created by this session or verified continuing ownership/explicit handoff establishes editing provenance. Do not register with fabricated PR identities or create a cleanup claim solely to simulate an A1 link. For a released registration, the atomic claim below is mandatory before editing; an already-owned registration requires matching owner authority. A failed claim never authorizes recovery or takeover. A successful cleanup claim prevents cleanup but does not prove exclusive editing ownership, and an A1 runtime link does not claim the cleanup journal. Both remain cooperative, independent safeguards.
190
+
187
191
  Before resuming a released worktree, acquire it:
188
192
 
189
193
  ```bash