@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.
- package/dist/features/owned-ui/settings-app.js +2 -2
- package/dist/foundation/process-containment/addon.d.ts +5 -0
- package/dist/foundation/release/bootstrap.js +34 -31
- package/dist/foundation/release/cohort-selection.d.ts +0 -6
- package/dist/foundation/release/cohort-selection.js +5 -13
- package/dist/foundation/release/cohort-state.d.ts +2 -0
- package/dist/foundation/release/endpoint-lock.d.ts +2 -0
- package/dist/foundation/release/endpoint-lock.js +37 -0
- package/dist/foundation/release/endpoint-recovery.d.ts +10 -0
- package/dist/foundation/release/endpoint-recovery.js +45 -0
- package/dist/foundation/release/endpoints.d.ts +10 -2
- package/dist/foundation/release/endpoints.js +42 -24
- package/dist/foundation/release/index.d.ts +2 -0
- package/dist/foundation/release/index.js +2 -0
- package/dist/foundation/release/process-cleanup.js +12 -2
- package/dist/foundation/release/supervisor-identity.d.ts +15 -0
- package/dist/foundation/release/supervisor-identity.js +63 -0
- package/dist/foundation/release/update.js +2 -2
- package/dist/foundation/storage/control-store.d.ts +5 -3
- package/dist/foundation/storage/control-store.js +8 -7
- package/dist/foundation/supervision/main.js +3 -1
- package/dist/foundation/supervision/server.js +80 -53
- package/dist/integrations/pi/engine/compaction-progress.js +13 -5
- package/dist/native/darwin-arm64/manifest.json +3 -3
- package/dist/native/darwin-arm64/process-guardian.node +0 -0
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +3 -3
- package/dist/native/darwin-x64/process-guardian.node +0 -0
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +3 -3
- package/dist/native/linux-x64/process-guardian.node +0 -0
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +3 -3
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/ui/settings/declarations.d.ts +1 -1
- package/dist/ui/settings/declarations.js +1 -1
- package/docs/architecture/project-structure.md +24 -16
- package/docs/architecture/toolchain.md +1 -1
- package/docs/ci-release-runbook.md +2 -0
- package/docs/launch-recovery.md +23 -0
- package/docs/local-worktree-cleanup.md +4 -0
- package/docs/openspec-archive-automation.md +11 -3
- package/docs/validation.md +3 -1
- 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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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-
|
|
10
|
+
"builtAt": "2026-11-06T18:31:39.019Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
|
-
"sha256": "
|
|
14
|
-
"size":
|
|
13
|
+
"sha256": "9ab579426034c752ef8ed53a789566f5b7cfe102149d30c28f4bdb60e73107c5",
|
|
14
|
+
"size": 456272
|
|
15
15
|
},
|
|
16
16
|
"provenance": {
|
|
17
17
|
"language": "Rust",
|
|
Binary file
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "arm64",
|
|
10
10
|
"target": "aarch64-apple-darwin",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-11-
|
|
12
|
+
"builtAt": "2026-11-06T18:34:18.531Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "7dcfd045769d22ffd59f11974a7d5d490199ee67eec288e92224be4c049857d7",
|
|
16
|
+
"size": 6645216
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-11-
|
|
10
|
+
"builtAt": "2026-11-06T18:31:46.232Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
|
-
"sha256": "
|
|
14
|
-
"size":
|
|
13
|
+
"sha256": "65f3a469a6faa9b578bc095e7559012775ee85e5b93cbc9451c62604f769146e",
|
|
14
|
+
"size": 452648
|
|
15
15
|
},
|
|
16
16
|
"provenance": {
|
|
17
17
|
"language": "Rust",
|
|
Binary file
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-apple-darwin",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-11-
|
|
12
|
+
"builtAt": "2026-11-06T18:37:01.604Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "691f821e9a4d2da71866dbe09742909d8f8c46fc330bdb917e8b81de2fa83450",
|
|
16
|
+
"size": 7249568
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"platform": "linux",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-11-
|
|
10
|
+
"builtAt": "2026-11-06T18:26:24.402Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
|
-
"sha256": "
|
|
14
|
-
"size":
|
|
13
|
+
"sha256": "64232d4c827bb7f69c14612986f9b6780a0593d8842cd039a411b6bfe29cd5da",
|
|
14
|
+
"size": 500264
|
|
15
15
|
},
|
|
16
16
|
"provenance": {
|
|
17
17
|
"language": "Rust",
|
|
Binary file
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-unknown-linux-gnu",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-11-
|
|
12
|
+
"builtAt": "2026-11-06T18:28:48.745Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "70f658de12b148666d0414e8a0fd0d8a10844d7a0ee6e2b11c2013477f4db91f",
|
|
16
|
+
"size": 8033296
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"platform": "win32",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-11-
|
|
10
|
+
"builtAt": "2026-11-06T18:27:06.362Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
|
-
"sha256": "
|
|
14
|
-
"size":
|
|
13
|
+
"sha256": "e8f4a87276da9375c998d9cdb28c2ed5d4ed857089001c338a689bb045d75dad",
|
|
14
|
+
"size": 414208
|
|
15
15
|
},
|
|
16
16
|
"provenance": {
|
|
17
17
|
"language": "Rust",
|
|
Binary file
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-pc-windows-msvc",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-11-
|
|
12
|
+
"builtAt": "2026-11-06T18:29:48.572Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "712c6ed93b1d9fa307a6e12045222c1cbf34be1044360d1a0070d694bdfe2f55",
|
|
16
|
+
"size": 7155712
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -177,7 +177,7 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
|
|
|
177
177
|
}),
|
|
178
178
|
tabBarRightItems: Object.freeze({
|
|
179
179
|
id: "tabBarRightItems",
|
|
180
|
-
label: "
|
|
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
|
-
|
|
98
|
+
### Common Git workflow
|
|
97
99
|
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
109
|
-
git -C .worktrees
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|