@timurproko/a1 0.2.7-dev.794 → 0.2.7-dev.801

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 (38) hide show
  1. package/dist/app/session-shell/clipboard-diagnostics.js +1 -1
  2. package/dist/app/session-shell/response-copy-helper.js +21 -6
  3. package/dist/app/session-shell/response-copy-protocol.d.ts +1 -1
  4. package/dist/app/session-shell/response-copy-transport.d.ts +3 -6
  5. package/dist/app/session-shell/response-copy-transport.js +60 -17
  6. package/dist/app/session-shell/session-shell-root.js +6 -1
  7. package/dist/app/session-shell/session-shell.js +12 -19
  8. package/dist/app/session-shell/shell-host.js +11 -0
  9. package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
  10. package/dist/foundation/resident-tabs/fixture-control.js +35 -0
  11. package/dist/foundation/resident-tabs/progress-observation.d.ts +7 -0
  12. package/dist/foundation/resident-tabs/progress-observation.js +119 -0
  13. package/dist/integrations/pi/engine/session-runtime.js +5 -2
  14. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
  15. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +30 -1
  16. package/dist/native/darwin-arm64/manifest.json +1 -1
  17. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  18. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  19. package/dist/native/darwin-x64/manifest.json +1 -1
  20. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  21. package/dist/native/darwin-x64/terminal-host.node +0 -0
  22. package/dist/native/linux-x64/manifest.json +1 -1
  23. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  24. package/dist/native/linux-x64/terminal-host.node +0 -0
  25. package/dist/native/win32-x64/manifest.json +2 -2
  26. package/dist/native/win32-x64/process-guardian.node +0 -0
  27. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  28. package/dist/native/win32-x64/terminal-host.node +0 -0
  29. package/dist/product-identity.d.ts +2 -2
  30. package/dist/product-identity.js +2 -2
  31. package/dist/product-identity.json +6 -0
  32. package/docs/architecture/resident-tabs.md +10 -6
  33. package/docs/architecture/terminal-host-provenance.md +1 -1
  34. package/docs/architecture/toolchain.md +3 -1
  35. package/docs/ci-release-runbook.md +1 -1
  36. package/docs/openspec-archive-automation.md +20 -6
  37. package/docs/validation.md +1 -1
  38. package/package.json +6 -3
@@ -39,7 +39,7 @@ export class ClipboardDiagnosticCapture {
39
39
  // Security: project only declared scalar fields. Never serialize the supplied event object or raw errors.
40
40
  this.#records.push({ source, phase: event.phase, atMs: scalar(event.atMs), request: event.request !== undefined && event.request < 0 ? -scalar(-event.request) : scalar(event.request), pending: scalar(event.pending), bytes: scalar(event.bytes), elapsedMs: scalar(event.elapsedMs),
41
41
  sourceUnits: scalar(event.sourceUnits), pendingBytes,
42
- transport: ["native", "terminal", "provided", "injected"].includes(event.transport ?? "") ? event.transport : "none",
42
+ transport: ["native", "terminal", "platform", "provided", "injected"].includes(event.transport ?? "") ? event.transport : "none",
43
43
  outcome: ["ready", "failed", "canceled", "delivered", "submitted-unverified", "timed-out", "superseded"].includes(event.outcome ?? "") ? event.outcome : "none" });
44
44
  if (this.#records.length > 128)
45
45
  this.#records.shift();
@@ -1,5 +1,6 @@
1
1
  import { MAX_COPY_ROWS, MAX_COPY_SOURCE_UNITS, selectionCopyRowText } from "../../ui/components/index.js";
2
2
  import { COPY_CHUNK_UNITS, MAX_COPY_BYTES, MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
3
+ const NATIVE_RETRY_DELAYS_MS = [20, 60];
3
4
  // Security: this isolated executor never inherits the UI terminal and never logs clipboard contents.
4
5
  let header;
5
6
  let fragments = [];
@@ -24,6 +25,14 @@ function nextOutput() {
24
25
  send({ kind: "data", text });
25
26
  }
26
27
  function send(message) { process.send?.(message); }
28
+ function transientClipboardError(error) {
29
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ return /^(?:EAGAIN|EBUSY)$/iu.test(code) || /(?:busy|locked|contention|temporar|try again)/iu.test(message);
32
+ }
33
+ function delay(milliseconds) {
34
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
35
+ }
27
36
  function finish(result, control) {
28
37
  if (finished)
29
38
  return;
@@ -97,13 +106,19 @@ async function receive(message) {
97
106
  return;
98
107
  }
99
108
  send({ kind: "phase", phase: "submitting", bytes });
100
- try {
101
- await native.setText(text);
102
- finish({ outcome: "delivered" });
103
- }
104
- catch {
105
- finish({ outcome: "failed", failure: "denied" });
109
+ for (let attempt = 0; attempt <= NATIVE_RETRY_DELAYS_MS.length; attempt++) {
110
+ try {
111
+ await native.setText(text);
112
+ finish({ outcome: "delivered" });
113
+ return;
114
+ }
115
+ catch (error) {
116
+ if (!transientClipboardError(error) || attempt === NATIVE_RETRY_DELAYS_MS.length)
117
+ break;
118
+ await delay(NATIVE_RETRY_DELAYS_MS[attempt]);
119
+ }
106
120
  }
121
+ finish({ outcome: "failed", failure: "denied" });
107
122
  return;
108
123
  }
109
124
  else
@@ -19,7 +19,7 @@ export interface ResponseCopyEvent {
19
19
  readonly pending: number;
20
20
  readonly sourceUnits: number;
21
21
  readonly bytes?: number;
22
- readonly transport?: "native" | "terminal" | "injected";
22
+ readonly transport?: "native" | "terminal" | "platform" | "injected";
23
23
  readonly outcome?: CopyOutcome;
24
24
  }
25
25
  /** One request per helper lifetime. Each source fragment is acknowledged before the next is sent. */
@@ -7,7 +7,7 @@ export interface ResponseCopyJob {
7
7
  readonly stopped: Promise<void>;
8
8
  cancel(): void;
9
9
  }
10
- export type CopyPhaseObserver = (phase: "extracted" | "encoded" | "submitting", bytes: number, transport: "native" | "terminal" | "injected") => void;
10
+ export type CopyPhaseObserver = (phase: "extracted" | "encoded" | "submitting", bytes: number, transport: "native" | "terminal" | "platform" | "injected") => void;
11
11
  export type ResponseCopyExecutor = (snapshot: SelectionCopySnapshot, phase: CopyPhaseObserver) => ResponseCopyJob;
12
12
  /** The production executor also holds a spare helper; the owner warms it at start and disposes it at shutdown unless the pool is shared. */
13
13
  export interface OwnedResponseCopyExecutor extends ResponseCopyExecutor {
@@ -22,17 +22,14 @@ export interface ResponseCopyTerminal {
22
22
  }
23
23
  /** Never substitute a remote host's native clipboard for the terminal user's clipboard. */
24
24
  export declare function responseCopyDestination(env: NodeJS.ProcessEnv): "native" | "terminal";
25
- /** Node documents synchronous POSIX TTY and Windows pipe writes; do not put clipboard traffic there. */
26
- export declare function hasAsyncClipboardOutput(platform?: NodeJS.Platform, stdout?: {
27
- readonly isTTY?: boolean | undefined;
28
- readonly fd?: number | undefined;
29
- }): boolean;
30
25
  /** Isolates preparation and native clipboard access; only the owner may submit terminal controls. */
31
26
  export declare function createResponseCopyExecutor(options?: {
32
27
  readonly terminal?: ResponseCopyTerminal;
33
28
  readonly destination?: "native" | "terminal";
34
29
  /** Explicit host seam: must be non-blocking and honor cancellation, or its unresolved lifetime is quarantined. */
35
30
  readonly writeText?: (text: string, signal: AbortSignal) => Promise<void>;
31
+ /** Platform/fault-test seam; production writes through the supported operating-system command. */
32
+ readonly platformWriteText?: (text: string, signal: AbortSignal) => Promise<boolean>;
36
33
  /** Package/fault-test seam: the production helper is always an emitted sibling. */
37
34
  readonly helper?: URL;
38
35
  /** Idle bound for the spare helper; production keeps the default. */
@@ -1,25 +1,58 @@
1
- import { fork } from "node:child_process";
2
- import { fstatSync } from "node:fs";
1
+ import { execFile, fork } from "node:child_process";
3
2
  import { HelperPool } from "./helper-pool.js";
3
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
4
  import { COPY_CHUNK_UNITS, COPY_CLEANUP_MS, MAX_COPY_BYTES, MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
5
+ const PLATFORM_WRITE_TIMEOUT_MS = 1_500;
5
6
  /** Never substitute a remote host's native clipboard for the terminal user's clipboard. */
6
7
  export function responseCopyDestination(env) {
7
- return env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY || env.TMUX || env.STY || env.WSL_DISTRO_NAME
8
+ return env[PRODUCT_IDENTITY.environment.tabId] || env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY || env.TMUX || env.STY || env.WSL_DISTRO_NAME
8
9
  ? "terminal" : "native";
9
10
  }
10
- /** Node documents synchronous POSIX TTY and Windows pipe writes; do not put clipboard traffic there. */
11
- export function hasAsyncClipboardOutput(platform = process.platform, stdout = process.stdout) {
12
- if (stdout.isTTY)
13
- return platform === "win32";
14
- if (platform === "win32")
15
- return false;
16
- try {
17
- const stat = fstatSync(stdout.fd ?? -1);
18
- return stat.isFIFO() || stat.isSocket();
19
- }
20
- catch {
21
- return false;
11
+ function platformClipboardCommands() {
12
+ if (process.platform === "win32")
13
+ return [{
14
+ command: "powershell.exe",
15
+ args: ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "$value=[Console]::In.ReadToEnd(); Set-Clipboard -Value $value"],
16
+ }];
17
+ if (process.platform === "darwin")
18
+ return [{ command: "pbcopy", args: [] }];
19
+ return [
20
+ { command: "wl-copy", args: ["--type", "text/plain;charset=utf-8"] },
21
+ { command: "xclip", args: ["-selection", "clipboard", "-in"] },
22
+ ];
23
+ }
24
+ async function writePlatformClipboardText(text, signal) {
25
+ for (const route of platformClipboardCommands()) {
26
+ if (signal.aborted)
27
+ return false;
28
+ const delivered = await new Promise(resolve => {
29
+ let settled = false;
30
+ const done = (value) => {
31
+ if (settled)
32
+ return;
33
+ settled = true;
34
+ resolve(value);
35
+ };
36
+ const child = execFile(route.command, [...route.args], {
37
+ signal,
38
+ windowsHide: true,
39
+ timeout: PLATFORM_WRITE_TIMEOUT_MS,
40
+ killSignal: "SIGKILL",
41
+ maxBuffer: 1_024,
42
+ }, error => done(error === null));
43
+ child.once("error", () => done(false));
44
+ if (child.stdin === null) {
45
+ child.kill("SIGKILL");
46
+ done(false);
47
+ return;
48
+ }
49
+ child.stdin.once("error", () => { child.kill("SIGKILL"); done(false); });
50
+ child.stdin.end(text);
51
+ });
52
+ if (delivered)
53
+ return true;
22
54
  }
55
+ return false;
23
56
  }
24
57
  /** Isolates preparation and native clipboard access; only the owner may submit terminal controls. */
25
58
  export function createResponseCopyExecutor(options = {}) {
@@ -43,8 +76,18 @@ export function createResponseCopyExecutor(options = {}) {
43
76
  };
44
77
  let outcome = await execute(destination);
45
78
  // Concurrency: helper results settle only after exit; fallback cannot race an older native write.
46
- if (!controller.signal.aborted && destination === "native" && outcome.result.failure === "unavailable" && options.terminal !== undefined) {
47
- outcome = await execute("terminal");
79
+ if (!controller.signal.aborted && destination === "native" && outcome.result.outcome === "failed") {
80
+ const prepared = await execute("prepare");
81
+ if (prepared.result.outcome === "delivered" && prepared.text !== undefined) {
82
+ phase("submitting", prepared.bytes ?? 0, "platform");
83
+ const delivered = await (options.platformWriteText ?? writePlatformClipboardText)(prepared.text, controller.signal);
84
+ outcome = { kind: "result", result: delivered ? { outcome: "delivered" } : { outcome: "failed", failure: "unavailable" } };
85
+ }
86
+ else
87
+ outcome = { kind: "result", result: { outcome: "failed", failure: "transport" } };
88
+ if (!controller.signal.aborted && outcome.result.outcome === "failed" && options.terminal !== undefined) {
89
+ outcome = await execute("terminal");
90
+ }
48
91
  }
49
92
  if (controller.signal.aborted)
50
93
  return { outcome: "canceled" };
@@ -438,6 +438,7 @@ export class OwnedUiSessionShellRoot {
438
438
  ? Math.max(0, dock.inputRows - 2)
439
439
  : 0;
440
440
  const copyableDockRows = [
441
+ ...dock.copyableMessageRows,
441
442
  ...Array.from({ length: promptCopyRows }, (_value, index) => ({
442
443
  row: editorOffset + index + 1,
443
444
  prompt: index === 0 ? "first" : "continuation",
@@ -620,7 +621,10 @@ export class OwnedUiSessionShellRoot {
620
621
  #renderDockLayout(width) {
621
622
  const queued = this.#customViewport ? [] : this.#renderQueued(width);
622
623
  const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
623
- const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
624
+ const noticeRows = this.#renderDockNotice(width);
625
+ const transientRows = [...queued, ...statusRows, ...noticeRows];
626
+ const messageStart = queued.length;
627
+ const copyableMessageRows = Array.from({ length: statusRows.length + noticeRows.length }, (_value, index) => ({ row: messageStart + index }));
624
628
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
625
629
  const input = this.#inputSurface.render(width);
626
630
  const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
@@ -634,6 +638,7 @@ export class OwnedUiSessionShellRoot {
634
638
  editorOffset: inputOffset + (body?.rowOffset ?? 0),
635
639
  inputRows: body?.rowCount ?? input.length,
636
640
  footerRows: footer.length,
641
+ copyableMessageRows,
637
642
  };
638
643
  }
639
644
  #renderCopyAcknowledgement(width) {
@@ -1,5 +1,5 @@
1
1
  import { ResponseCopyCoordinator } from "./response-copy-coordinator.js";
2
- import { createResponseCopyExecutor, hasAsyncClipboardOutput, responseCopyDestination } from "./response-copy-transport.js";
2
+ import { createResponseCopyExecutor, responseCopyDestination } from "./response-copy-transport.js";
3
3
  import { MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
4
4
  import { PromptHistoryController } from "./prompt-history-controller.js";
5
5
  import { PRODUCT_TEXT } from "../../product-identity.js";
@@ -129,31 +129,24 @@ export class OwnedUiSessionPresenter {
129
129
  // throw releases them in reverse, so a shell that never finished constructing holds nothing.
130
130
  const constructed = [() => host.disconnect(this)];
131
131
  try {
132
- const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
133
132
  this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
134
133
  execute: responseCopy?.execute ?? (this.#copyExecutor = createResponseCopyExecutor({
135
134
  ...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.copy }),
136
135
  ...(terminal === undefined && clipboard === undefined ? {} : { destination: "terminal" }),
137
136
  ...(clipboard?.writeText === undefined ? {} : { writeText: (text, signal) => clipboard.writeText(text, signal) }),
138
- ...(terminalCopy ? { terminal: { submit: async (control, signal) => {
139
- if (signal.aborted || this.#disposed || !this.#terminal.running)
140
- throw new Error("Copy canceled");
141
- // Performance: never grow the real terminal's pending buffer with another clipboard payload.
142
- if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
143
- throw new Error("Clipboard terminal is busy");
144
- }
145
- this.#terminal.writeControl(control);
146
- } } } : {}),
137
+ // Invariant: the owned host is the route contract. Resident and pseudoterminal children must not infer
138
+ // clipboard reachability from Node's view of stdout, and inactive presenters are fenced here.
139
+ terminal: { submit: async (control, signal) => {
140
+ if (signal.aborted || this.#disposed || !this.#terminal.running || !this.#terminal.attached)
141
+ throw new Error("Copy canceled");
142
+ // Performance: never grow the real terminal's pending buffer with another clipboard payload.
143
+ if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
144
+ throw new Error("Clipboard terminal is busy");
145
+ }
146
+ this.#terminal.writeControl(control);
147
+ } },
147
148
  })),
148
149
  ...(responseCopy?.onEvent === undefined ? {} : { onEvent: responseCopy.onEvent }),
149
- onFailure: result => {
150
- if (this.#disposed || !this.#terminal.running)
151
- return;
152
- this.root.appendWorkflowStatus(result.outcome === "timed-out" ? "Copy timed out; the clipboard did not respond."
153
- : result.failure === "size" ? "Selection exceeds this clipboard route's size limit."
154
- : "Could not copy the selection; the clipboard is unavailable.");
155
- this.#terminal.requestRender();
156
- },
157
150
  }) : null;
158
151
  constructed.push(() => { this.#responseCopy?.dispose(); this.#copyExecutor?.dispose(); });
159
152
  const readClipboard = async (signal) => {
@@ -1,6 +1,8 @@
1
1
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../ui/components/mouse.js";
2
2
  import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
3
3
  import { piCanvasBackgroundAnsi } from "../../integrations/pi/components/theme.js";
4
+ import { residentFixtureControl } from "../../foundation/resident-tabs/fixture-control.js";
5
+ import { residentProgressObserver } from "../../foundation/resident-tabs/progress-observation.js";
4
6
  import { DamageAwareTerminalAdapter, } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
5
7
  import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
6
8
  import { classifyPiTuiInput } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
@@ -47,6 +49,8 @@ export class OwnedUiTerminalHost {
47
49
  this.#customViewport = options.layout === "custom-viewport";
48
50
  this.#quitOutro = options.quitOutro;
49
51
  const input = options.input;
52
+ const fixtureControl = residentFixtureControl();
53
+ const progressObserver = residentProgressObserver();
50
54
  let damageTerminal;
51
55
  const surface = {
52
56
  render: width => this.#active?.render(width) ?? [],
@@ -72,6 +76,13 @@ export class OwnedUiTerminalHost {
72
76
  inspectHyperlinks: readVisibleHyperlinks,
73
77
  onResize: () => this.#active?.noteImmediatePresentation(),
74
78
  onHyperlinkCleanupRequired: () => this.runtime?.requestRender(true),
79
+ ...(fixtureControl === null ? {} : {
80
+ allowWrite: () => !fixtureControl.stopped("child-render-write"),
81
+ writeChunks: data => fixtureControl.fragmentChildWrite(data),
82
+ }),
83
+ ...(progressObserver === null ? {} : {
84
+ onWrite: ({ bytes, fragments, transaction, frameId, cause }) => progressObserver.childRenderWrite(bytes, fragments, transaction, frameId, cause),
85
+ }),
75
86
  });
76
87
  return damageTerminal;
77
88
  },
@@ -0,0 +1,7 @@
1
+ export type ResidentFixtureStoppedStage = "child-render-write";
2
+ export interface ResidentFixtureControlPort {
3
+ stopped(stage: ResidentFixtureStoppedStage): boolean;
4
+ fragmentChildWrite(data: string): readonly string[];
5
+ }
6
+ /** Returns the exact-artifact fixture control, or null for every ordinary resident child. */
7
+ export declare function residentFixtureControl(environment?: NodeJS.ProcessEnv): ResidentFixtureControlPort | null;
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
+ import { residentProgressObserver } from "./progress-observation.js";
5
+ /** Returns the exact-artifact fixture control, or null for every ordinary resident child. */
6
+ export function residentFixtureControl(environment = process.env) {
7
+ const directory = environment[PRODUCT_IDENTITY.environment.residentFixtureControlDir];
8
+ if (environment === process.env)
9
+ delete process.env[PRODUCT_IDENTITY.environment.residentFixtureControlDir];
10
+ // Invariant: launch erases the bridge tab identity; the observer retains the observed one.
11
+ const tabId = residentProgressObserver(environment)?.tabId;
12
+ if (!directory || !tabId || !/^[a-z0-9-]{1,128}$/iu.test(tabId))
13
+ return null;
14
+ return {
15
+ stopped: stage => existsSync(join(directory, `${tabId}.${stage}.stop`)),
16
+ fragmentChildWrite: data => existsSync(join(directory, `${tabId}.child-render-fragments.enable`))
17
+ ? boundedFragments(data)
18
+ : [data],
19
+ };
20
+ }
21
+ function boundedFragments(data) {
22
+ const points = [...data];
23
+ if (points.length <= 1)
24
+ return [data];
25
+ if (points.length <= 16)
26
+ return points;
27
+ const fragments = points.slice(0, 8);
28
+ const middle = points.slice(8, -8);
29
+ const chunkSize = Math.max(1, Math.ceil(middle.length / 48));
30
+ for (let index = 0; index < middle.length; index += chunkSize) {
31
+ fragments.push(middle.slice(index, index + chunkSize).join(""));
32
+ }
33
+ fragments.push(...points.slice(-8));
34
+ return fragments;
35
+ }
@@ -0,0 +1,7 @@
1
+ export interface ResidentProgressObserverPort {
2
+ readonly tabId: string;
3
+ engineDelivery(event: unknown): void;
4
+ childRenderWrite(bytes: number, fragments: number, transaction: "synchronized-output" | "ordinary", frameId?: number, renderCause?: string): void;
5
+ }
6
+ /** Returns the opt-in payload-free observer for a resident child, or null on ordinary launches. */
7
+ export declare function residentProgressObserver(environment?: NodeJS.ProcessEnv): ResidentProgressObserverPort | null;
@@ -0,0 +1,119 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { appendFile, mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { performance } from "node:perf_hooks";
5
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
6
+ const OBSERVATION_DIRECTORY_ENV = PRODUCT_IDENTITY.environment.residentProgressObservationDir;
7
+ const OBSERVATION_RUN_ENV = PRODUCT_IDENTITY.environment.residentProgressRunId;
8
+ const MAX_PENDING_BYTES = 256 * 1024;
9
+ class ResidentProgressObserver {
10
+ #directory;
11
+ #runId;
12
+ #tabId;
13
+ #incarnation;
14
+ #processInstance;
15
+ #path;
16
+ #sequence = 0;
17
+ #engineSequence = 0;
18
+ #pendingBytes = 0;
19
+ #lost = 0;
20
+ #writing = Promise.resolve();
21
+ #failed = false;
22
+ get tabId() { return this.#tabId; }
23
+ constructor(directory, runId, tabId, incarnation) {
24
+ this.#directory = directory;
25
+ this.#runId = runId;
26
+ this.#tabId = tabId;
27
+ this.#incarnation = incarnation;
28
+ this.#processInstance = `${process.pid}-${randomUUID()}`;
29
+ this.#path = join(directory, `child-${this.#processInstance}.jsonl`);
30
+ }
31
+ engineDelivery(event) {
32
+ this.#engineSequence += 1;
33
+ this.#observe("engine-delivery", {
34
+ eventSequence: this.#engineSequence,
35
+ eventKind: eventKind(event),
36
+ count: 1,
37
+ });
38
+ }
39
+ childRenderWrite(bytes, fragments, transaction, frameId, renderCause) {
40
+ this.#observe("child-render-write", {
41
+ eventSequence: this.#engineSequence,
42
+ bytes,
43
+ count: 1,
44
+ fragmentCount: fragments,
45
+ transactionKind: transaction,
46
+ ...(frameId === undefined ? {} : { frameId }),
47
+ ...(renderCause === undefined ? {} : { renderCause }),
48
+ });
49
+ }
50
+ #observe(stage, fields) {
51
+ if (this.#failed)
52
+ return;
53
+ const lostBefore = this.#lost;
54
+ const record = {
55
+ schema: "a1-resident-progress-observation-v1",
56
+ runId: this.#runId,
57
+ role: "child",
58
+ stage,
59
+ pid: process.pid,
60
+ processInstance: this.#processInstance,
61
+ timestampUnixUs: Math.round((performance.timeOrigin + performance.now()) * 1_000),
62
+ // Rationale: timeOrigin is calibrated to the Unix epoch; retain a conservative cross-process error.
63
+ clockErrorUs: 1_000,
64
+ sequence: ++this.#sequence,
65
+ tabId: this.#tabId,
66
+ incarnation: this.#incarnation,
67
+ lostBefore,
68
+ ...fields,
69
+ };
70
+ const line = `${JSON.stringify(record)}\n`;
71
+ const length = Buffer.byteLength(line);
72
+ if (this.#pendingBytes + length > MAX_PENDING_BYTES) {
73
+ this.#lost = lostBefore + 1;
74
+ return;
75
+ }
76
+ this.#lost = 0;
77
+ this.#pendingBytes += length;
78
+ this.#writing = this.#writing.then(async () => {
79
+ await mkdir(this.#directory, { recursive: true });
80
+ await appendFile(this.#path, line, "utf8");
81
+ }).catch(() => {
82
+ this.#failed = true;
83
+ }).finally(() => {
84
+ this.#pendingBytes -= length;
85
+ });
86
+ }
87
+ }
88
+ let observer;
89
+ /** Returns the opt-in payload-free observer for a resident child, or null on ordinary launches. */
90
+ export function residentProgressObserver(environment = process.env) {
91
+ if (environment === process.env && observer !== undefined)
92
+ return observer;
93
+ const directory = environment[OBSERVATION_DIRECTORY_ENV];
94
+ const runId = environment[OBSERVATION_RUN_ENV];
95
+ const tabId = environment[PRODUCT_IDENTITY.environment.residentProgressTabId];
96
+ const incarnation = Number(environment[PRODUCT_IDENTITY.environment.residentProgressTabIncarnation]);
97
+ const value = directory && runId && runId.length <= 128 && tabId && Number.isSafeInteger(incarnation) && incarnation >= 0
98
+ ? new ResidentProgressObserver(directory, runId, tabId, incarnation)
99
+ : null;
100
+ if (environment === process.env) {
101
+ observer = value;
102
+ // Security: test evidence paths and tab identity are not child/tool configuration. Retain them only in
103
+ // this observer instance so extensions and spawned tools cannot append to the fixture trace.
104
+ for (const name of [
105
+ OBSERVATION_DIRECTORY_ENV,
106
+ OBSERVATION_RUN_ENV,
107
+ PRODUCT_IDENTITY.environment.residentProgressTabId,
108
+ PRODUCT_IDENTITY.environment.residentProgressTabIncarnation,
109
+ ])
110
+ delete process.env[name];
111
+ }
112
+ return value;
113
+ }
114
+ function eventKind(event) {
115
+ if (typeof event !== "object" || event === null)
116
+ return "unknown";
117
+ const value = event.type;
118
+ return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/iu.test(value) ? value : "unknown";
119
+ }
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { promisify } from "node:util";
4
4
  import { PRODUCT_IDENTITY } from "../../../product-identity.js";
5
+ import { residentProgressObserver } from "../../../foundation/resident-tabs/progress-observation.js";
5
6
  import { observeCompactionProgress } from "./compaction-progress.js";
6
7
  import { createPiRuntimeIntegration } from "./runtime-integration.js";
7
8
  import { PiSessionCommandIntegration } from "./session-integration.js";
@@ -369,8 +370,10 @@ export class PiEngineRuntime {
369
370
  return;
370
371
  const generation = this.#sessionGeneration;
371
372
  this.#unsubscribe = session.subscribe(event => {
372
- if (generation === this.#sessionGeneration && !this.#ports.disposed())
373
- this.#ports.event(event);
373
+ if (generation !== this.#sessionGeneration || this.#ports.disposed())
374
+ return;
375
+ residentProgressObserver()?.engineDelivery(event);
376
+ this.#ports.event(event);
374
377
  });
375
378
  }
376
379
  /**
@@ -52,6 +52,17 @@ export interface DamageAwareTerminalOptions {
52
52
  };
53
53
  readonly onResize?: () => void;
54
54
  readonly onHyperlinkCleanupRequired?: () => void;
55
+ /** Exact-artifact fault controls evaluated before physical writes; absent in ordinary sessions. */
56
+ readonly allowWrite?: () => boolean;
57
+ readonly writeChunks?: (data: string) => readonly string[];
58
+ /** Optional payload-free observation after a terminal write returns. */
59
+ readonly onWrite?: (metadata: {
60
+ readonly bytes: number;
61
+ readonly fragments: number;
62
+ readonly transaction: "synchronized-output" | "ordinary";
63
+ readonly frameId?: number;
64
+ readonly cause?: string;
65
+ }) => void;
55
66
  }
56
67
  /**
57
68
  * A fail-closed adapter for one pinned Pi fullscreen write grammar. Semantic
@@ -166,7 +166,36 @@ export class DamageAwareTerminalAdapter {
166
166
  const forwarded = this.#canvasBackgroundAnsi === null
167
167
  ? output
168
168
  : paintTerminalCanvasFrame(output, this.#canvasBackgroundAnsi);
169
- this.inner.write(forwarded);
169
+ let writeAllowed = true;
170
+ try {
171
+ writeAllowed = this.options.allowWrite?.() ?? true;
172
+ }
173
+ catch { /* Invariant: fixture controls fail open. */ }
174
+ if (!writeAllowed)
175
+ return;
176
+ let chunks = [forwarded];
177
+ try {
178
+ const controlled = this.options.writeChunks?.(forwarded);
179
+ if (controlled !== undefined && controlled.length > 0 && controlled.length <= 4_096
180
+ && controlled.join("") === forwarded)
181
+ chunks = controlled;
182
+ }
183
+ catch { /* Invariant: fixture controls fail open without corrupting the renderer write. */ }
184
+ for (const chunk of chunks)
185
+ this.inner.write(chunk);
186
+ try {
187
+ this.options.onWrite?.({
188
+ bytes: Buffer.byteLength(forwarded),
189
+ fragments: chunks.length,
190
+ transaction: forwarded.startsWith(BEGIN_SYNCHRONIZED_OUTPUT) && forwarded.endsWith(END_SYNCHRONIZED_OUTPUT)
191
+ ? "synchronized-output" : "ordinary",
192
+ ...(armed === undefined ? {} : {
193
+ frameId: armed.descriptor.frameId,
194
+ cause: armed.descriptor.cause,
195
+ }),
196
+ });
197
+ }
198
+ catch { /* Invariant: observation never changes rendering or write completion. */ }
170
199
  if (epoch !== this.#epoch
171
200
  || (armed !== undefined && this.#lastConsumedFrameId !== armed.descriptor.frameId))
172
201
  return;
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "arm64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-18T08:41:09.288Z",
10
+ "builtAt": "2026-10-19T06:24:29.067Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "a87eaaffcd986b624c8b044b756446cc1bdaf4334a1775b97820542fe34f9fb7",
@@ -9,11 +9,11 @@
9
9
  "architecture": "arm64",
10
10
  "target": "aarch64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-18T08:43:27.368Z",
12
+ "builtAt": "2026-10-19T06:27:25.061Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "af7b9fee2efa8478c728465031e73fb316b6b7012d11de0364130b0acd1b6e62",
16
- "size": 5173904
15
+ "sha256": "54b4a6c6f4b8abf76afd0a65053dd9916653dcf7990639c716bdd0d0ee735ad2",
16
+ "size": 5710496
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-18T08:41:16.359Z",
10
+ "builtAt": "2026-10-19T06:24:37.379Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "783a65f0de287a1c02cd5429dbe97bc8633f4cc8a7b92e3e961180b9bdcff814",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-18T08:45:43.907Z",
12
+ "builtAt": "2026-10-19T06:30:00.884Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "1a5465ea12ecfe76d5f0c8fd5c11dee99597b4206a7e9ece01b37fc5c477c211",
16
- "size": 5656912
15
+ "sha256": "c4c070744e7dd382532d11759533d5cc93acd53befd999e993b85ad17e848cd4",
16
+ "size": 6226880
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,7 +7,7 @@
7
7
  "platform": "linux",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-18T08:40:48.343Z",
10
+ "builtAt": "2026-10-19T06:24:11.933Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "118e5954e3864672bd8482ab7edf419cb27dcdf2533720316979b7f5cc5a53a5",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-unknown-linux-gnu",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-18T08:43:45.076Z",
12
+ "builtAt": "2026-10-19T06:27:20.231Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "843b10ab29eddf859a74a97875ba646a90917d9ec78445474895bb64f6e0772a",
16
- "size": 6353496
15
+ "sha256": "1e2d6d37ee740ee716aa67f5d37aaac6c193739eeae14ce19320fc8b34939d16",
16
+ "size": 6981672
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,10 +7,10 @@
7
7
  "platform": "win32",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-18T08:41:31.607Z",
10
+ "builtAt": "2026-10-19T06:24:34.859Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
- "sha256": "6299956800f1ab62f2e5bb4a1e4d61dbd45ce70a7711cbb9aca8f23033bdbbaf",
13
+ "sha256": "24685c41544bb8e90fb5162f9ab9545cb698204530cf131644aaf6ee97159c6e",
14
14
  "size": 402432
15
15
  },
16
16
  "provenance": {
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-pc-windows-msvc",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-18T08:44:54.428Z",
12
+ "builtAt": "2026-10-19T06:28:49.237Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "de82227898a90a6ec6bda0c8b503148388d3769ef4ab7f1176e56c8472b76232",
16
- "size": 5617664
15
+ "sha256": "a4d34ff27229603c8f354607c2bff377506d34ac72f90886ffe6abea37fd2f88",
16
+ "size": 6212608
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -1,10 +1,10 @@
1
- declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath", "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId"];
1
+ declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath", "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "residentFixtureControlDir", "residentProgressObservationDir", "residentProgressRunId", "residentProgressTabId", "residentProgressTabIncarnation", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId"];
2
2
  declare const FILESYSTEM_KEYS: readonly ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
3
3
  declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile", "sessionRepositoryDirectory", "sessionWorktreeClaimDirectory"];
4
4
  declare const ENDPOINT_KEYS: readonly ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
5
5
  declare const MANIFEST_KEYS: readonly ["releaseFilename", "packageFilename"];
6
6
  declare const PROTOCOL_KEYS: readonly ["namespace", "controlEnvelope", "supervisorSchema", "nativeHostSchema", "structuredAgentSchema", "controlStoreSchema", "promptHistorySchema", "releaseCohortSchema", "updateJournalSchema"];
7
- declare const EVIDENCE_KEYS: readonly ["nativeSpikeSchema", "terminalProvenanceSchema", "terminalProofSchema", "stableReleaseSchema", "previewReleaseSchema", "releaseCertificationSchema", "previewPlatformVerdictSchema", "piSourceLedgerSchema", "piComponentParitySchema", "piEventFrameParitySchema", "startupTraceSchema", "dependencyLayerSchema", "dependencyLayerCertificationSchema", "runtimePayloadSchema"];
7
+ declare const EVIDENCE_KEYS: readonly ["nativeSpikeSchema", "terminalProvenanceSchema", "terminalProofSchema", "terminalTemporalSnapshotSchema", "stableReleaseSchema", "previewReleaseSchema", "releaseCertificationSchema", "previewPlatformVerdictSchema", "piSourceLedgerSchema", "piComponentParitySchema", "piEventFrameParitySchema", "startupTraceSchema", "dependencyLayerSchema", "dependencyLayerCertificationSchema", "runtimePayloadSchema"];
8
8
  declare const ARTIFACT_KEYS: readonly ["cliEntry", "supervisorEntry", "guardianEntry", "uiEntry", "terminalHostAddon", "terminalHostEntry", "nativeCrate", "processGuardianAddon", "releaseTarballStem", "diagnosticStem"];
9
9
  type StringRecord<Keys extends readonly string[]> = {
10
10
  readonly [Key in Keys[number]]: string;
@@ -1,14 +1,14 @@
1
1
  import rawIdentity from "./product-identity.json" with { type: "json" };
2
2
  const ENVIRONMENT_KEYS = [
3
3
  "clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath",
4
- "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId",
4
+ "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "residentFixtureControlDir", "residentProgressObservationDir", "residentProgressRunId", "residentProgressTabId", "residentProgressTabIncarnation", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId",
5
5
  ];
6
6
  const FILESYSTEM_KEYS = ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
7
7
  const STATE_KEYS = ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile", "sessionRepositoryDirectory", "sessionWorktreeClaimDirectory"];
8
8
  const ENDPOINT_KEYS = ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
9
9
  const MANIFEST_KEYS = ["releaseFilename", "packageFilename"];
10
10
  const PROTOCOL_KEYS = ["namespace", "controlEnvelope", "supervisorSchema", "nativeHostSchema", "structuredAgentSchema", "controlStoreSchema", "promptHistorySchema", "releaseCohortSchema", "updateJournalSchema"];
11
- const EVIDENCE_KEYS = ["nativeSpikeSchema", "terminalProvenanceSchema", "terminalProofSchema", "stableReleaseSchema", "previewReleaseSchema", "releaseCertificationSchema", "previewPlatformVerdictSchema", "piSourceLedgerSchema", "piComponentParitySchema", "piEventFrameParitySchema", "startupTraceSchema", "dependencyLayerSchema", "dependencyLayerCertificationSchema", "runtimePayloadSchema"];
11
+ const EVIDENCE_KEYS = ["nativeSpikeSchema", "terminalProvenanceSchema", "terminalProofSchema", "terminalTemporalSnapshotSchema", "stableReleaseSchema", "previewReleaseSchema", "releaseCertificationSchema", "previewPlatformVerdictSchema", "piSourceLedgerSchema", "piComponentParitySchema", "piEventFrameParitySchema", "startupTraceSchema", "dependencyLayerSchema", "dependencyLayerCertificationSchema", "runtimePayloadSchema"];
12
12
  const ARTIFACT_KEYS = ["cliEntry", "supervisorEntry", "guardianEntry", "uiEntry", "terminalHostAddon", "terminalHostEntry", "nativeCrate", "processGuardianAddon", "releaseTarballStem", "diagnosticStem"];
13
13
  const ROOT_KEYS = ["schema", "displayName", "commandName", "packageName", "filesystem", "environment", "state", "endpoint", "manifest", "protocol", "evidence", "artifacts"];
14
14
  export function validateProductIdentity(value) {
@@ -23,6 +23,11 @@
23
23
  "processGuardianPath": "A1_PROCESS_GUARDIAN_PATH",
24
24
  "prFooterPreview": "A1_PR_FOOTER_PREVIEW",
25
25
  "profileHome": "A1_PROFILE_HOME",
26
+ "residentFixtureControlDir": "A1_RESIDENT_FIXTURE_CONTROL_DIR",
27
+ "residentProgressObservationDir": "A1_RESIDENT_PROGRESS_OBSERVATION_DIR",
28
+ "residentProgressRunId": "A1_RESIDENT_PROGRESS_RUN_ID",
29
+ "residentProgressTabId": "A1_RESIDENT_PROGRESS_TAB_ID",
30
+ "residentProgressTabIncarnation": "A1_RESIDENT_PROGRESS_TAB_INCARNATION",
26
31
  "runtimeDir": "A1_RUNTIME_DIR",
27
32
  "sessionRuntimeId": "A1_SESSION_RUNTIME_ID",
28
33
  "skipVersionCheck": "A1_SKIP_VERSION_CHECK",
@@ -70,6 +75,7 @@
70
75
  "nativeSpikeSchema": "a1-native-host-spike-evidence-v1",
71
76
  "terminalProvenanceSchema": "a1-terminal-host-provenance-v1",
72
77
  "terminalProofSchema": "a1-terminal-host-proof-v1",
78
+ "terminalTemporalSnapshotSchema": "a1-terminal-temporal-snapshot-v1",
73
79
  "stableReleaseSchema": "a1-stable-release-v1",
74
80
  "previewReleaseSchema": "a1-preview-release-v1",
75
81
  "releaseCertificationSchema": "a1-release-certification-v1",
@@ -223,13 +223,13 @@ The channels carry bounded generation-1 frames: a u32 little-endian length follo
223
223
  - The server sends `bridge.visibility{visible}`, so a hidden A1 throttles animation, and `bridge.rename`, so a strip rename sets the Pi session name.
224
224
  - The bridge is advisory. A missing or broken bridge degrades that tab to process-level status and never breaks the tab.
225
225
  - **Backpressure:**
226
- - Each client connection has a reliable, bounded control lane and a **single-slot render lane**. A newer full current surface replaces an unsent one, so a client that drains never depends on a dropped intermediate revision.
227
- - The server never blocks a holder, another client, or registry writes on a slow client.
226
+ - Each client connection has a reliable, bounded control lane and a **single-slot render lane**. A newer full current surface replaces an unsent one, so a client that drains never depends on a dropped intermediate revision. Structured surfaces are immutable `Arc` values while routed; encoding happens outside the routing mutex, decoded structured state is capped at 8 MiB, and a superseded holder socket cannot publish into a newer incarnation.
227
+ - The server never blocks a holder, another client, or registry writes on a slow client. Attach and holder hot loops yield after bounded frame, byte, and two-millisecond work quanta; completed outer writes, rather than offers, advance the presentation baseline.
228
228
  - Holders parse their PTY continuously, whether or not the tab is viewed, so a PTY is never throttled and a child never blocks on output.
229
229
 
230
230
  ### 6. Rendering, input, and outer-terminal fidelity
231
231
 
232
- - **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Composition reuses the proof's damage-aware composer and synchronized-output support, and falls back to a full repaint only when the layout changes.
232
+ - **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Ghostty remains the sole child-terminal parser. Its stream parser reports synchronized-output begin/end/reset boundaries to the holder, which keeps mutable parser state separate from an immutable committed surface and never publishes an open transaction. A non-renewable 250 ms deadline publishes one typed recovery commit for a malformed transaction. The outer composer owns the only synchronized envelope and falls back to a full repaint only when identity, geometry generation, dimensions, or the structured base changes.
233
233
  - **Outer modes.** The client negotiates the outer terminal's modes: raw mode, alternate screen, bracketed paste, focus events, SGR mouse, kitty keyboard protocol where available, and synchronized output.
234
234
  - Child-requested modes are tracked in the holder's model. Input is re-encoded per child mode by libghostty-vt, which the proof already does.
235
235
  - Mouse rows are offset by the strip. Row 0 belongs to the client.
@@ -249,8 +249,8 @@ Direct `a1 solo` is the behavioural oracle for every viewed resident child. Only
249
249
  - **Typed effects.** The holder registers libghostty's clipboard-write callback, which normalizes OSC 52 and OSC 1337. A write from the viewed tab crosses as one bounded `terminal.effect`; attach writes one OSC 52 for it. A hidden tab's write is dropped, never replayed into a later view. Payloads are never logged.
250
250
  - **Modes and cursor.** Each surface carries the child's cursor cell and modes. Attach mirrors a cursor shape the child chose and resets it on detach or failure; a default block cursor is left to the outer terminal, as direct A1 leaves it.
251
251
  - **Shortcut projection.** The owned child projects the effective A1 keybindings plus `keybindings.json` into one assignment per tab action: a key an A1 action holds never becomes a tab shortcut, and the earlier declaration keeps a duplicated key. It publishes the projection as `bridge.shortcuts`; the server relays it as `tabs.shortcuts`, and attach dispatches and labels exactly that projection. `/hotkeys` renders the same projection. A shared case file is replayed by both resolvers.
252
- - **Committed presentation.** A requested view commits only with a topology that lists it and a complete target surface. Surfaces of the committed view apply in revision order, and revisions rise across holder incarnations. A retained re-offer is marked stale and parks for at most 120 ms while the fresh frame arrives. One compositor writes strip, body, overlay, and final cursor inside a single synchronized update, and refuses clear-only, blank, and cursor-less strip-only frames. Clock, hover, and title-animation damage repaints only the strip and restores the child's cursor.
253
- - **Evidence.** One interaction matrix drives three layers: an owned-shell oracle that records content-free observations, role fixtures that drive real server, holder, and attach processes with a certified parity child, and an exact-artifact harness, `verify-interaction-parity.mjs`, that compares the bytes the same child observes directly and through the built host on each platform. Observations carry digests and lengths, never content. Each platform's verdict stands alone.
252
+ - **Committed presentation.** A requested view commits only with a topology that lists it and a complete target surface. Retained rows, palette/default colors, modes, and cursor are captured atomically; equal captures do not invent a revision. Surfaces of the committed view apply in revision order, and revisions rise across holder incarnations. A retained re-offer is marked stale from the request start and parks for at most 120 ms while the fresh frame arrives. One compositor writes strip, body, overlay, modes, and the viewport-clipped final cursor inside a single synchronized update. It erases suffix cells when a complete replacement row becomes shorter and restores the newest underlying surface when an overlay leaves. Clock, hover, and title-animation damage repaints only the strip and restores the child's cursor.
253
+ - **Evidence.** One interaction matrix drives three layers: an owned-shell oracle that records content-free observations, role fixtures that drive real server, holder, and attach processes with a certified parity child, and exact-artifact harnesses. `verify-interaction-parity.mjs` compares the bytes the same child observes directly and through the built host. `verify-resident-parallel-progress.mjs` configures the real engine against a bounded loopback streaming provider and crosses all six first-response orders with all three initial views while every turn remains unfinished behind a completion barrier. At matching 100×29 child geometry, the same real-A1 prompt also runs through isolated direct-solo and resident profiles; the native Ghostty oracle compares a content-free digest of the intermediate fixture cells and their resolved styles together with normalized cursor and mode state. It advances only at parser-reported synchronized-output boundaries, rejects unbalanced transactions or disappearing intermediate cells, and excludes only the resident strip row rather than normalizing terminal semantics. Opt-in per-process JSONL observations identify engine delivery, completed child writes, PTY ingestion, distinct holder commits, attach receipt, and completed outer paint by run/tab/incarnation/process-instance/sequence without recording terminal or conversation content; queue loss, clock faults, duplicates, stale order, and missing stages fail the report. Named pressure variants keep all turns unfinished while exercising hidden progress, repeated switching, navigation input, rename overlays, tab-local resize, a bounded 35 ms attach-receipt delay, and a 900 ms paused outer sink; every tab must retain partial animation/progress before settlement and then present its final output. A detached/reconnect variant releases all three unfinished turns after terminating the authenticated attach and exact self-reported server identity; child writes, PTY ingestion, and holder commits must continue before a replacement server and attach present every retained final output. Separate isolated exact-artifact control markers stop one target tab at engine delivery, physical child rendering, holder commit, or outer paint; each control requires its upstream stages and the other tabs to advance, then removes the stop and requires final output from all three tabs. A fifth control fragments a real owned-shell synchronized write into at most 64 physical writes without changing its bytes, records the DEC private mode 2026 transaction classification, and requires ordinary complete holder commits without deadline recovery before final output. This characterizes the built PTY/ConPTY coherence path; a passing fragmented run does not by itself assign the historical stall to that path without a failing live reproduction. The marker path is absent in ordinary sessions and is scrubbed before extensions or tools inherit the child environment, while observation callbacks remain passive and cannot select a control. CI runs the vendored Ghostty VT tests and both exact-artifact harnesses on Windows x64, Linux x64, and macOS arm64. Each platform's verdict stands alone.
254
254
 
255
255
  ### 7. Status and attention come from the tab bridge
256
256
 
@@ -391,7 +391,9 @@ Every queue, buffer, file, and collection the resident roles keep has a declared
391
391
  | Bound | Cap | Enforced in | Test |
392
392
  |---|---|---|---|
393
393
  | Protocol frame | 2 MiB, nesting 64, 65,536 collection items | `native/terminal-host/src/protocol.rs`, `src/foundation/resident-tabs/protocol.ts` | `refuses_oversized_header_before_payload`; `refuses a frame header past 2 MiB before buffering its payload`, `refuses nesting past 64 levels`, `refuses a collection past 65,536 items` |
394
+ | Structured complete surface | 512 × 256 geometry, 65,536 cells, 4,096 styles, 4,096 links, and 8 MiB decoded state | `protocol.rs` `StructuredSurface::validate` | `structured_surfaces_fail_closed_on_dimensions_indexes_and_control_injection` |
394
395
  | Input per message and holder input queue | 1 MiB | `protocol.rs`, `core/holder.rs`, `roles/holder.rs` | `over_capacity_paste_delivers_no_prefix` |
396
+ | Holder PTY ingress and work round | 64 × 16 KiB lossless chunks; at most 256 KiB or 2 ms before yielding | `roles/holder.rs` | `verify-resident-parallel-progress.mjs` concurrent and stopped-stage cases |
395
397
  | Per-client control lane | 1,024 frames and 4 MiB | `roles/server.rs` | `control_lane_is_bounded_by_frames_and_bytes` |
396
398
  | Per-client render slot | one pending full surface; a newer one replaces it | `roles/server.rs` `Peer::offer_surface` | `stalled_client_gets_one_full_current_surface` |
397
399
  | Concurrent endpoint connections | 128 | `roles/server.rs` `admit_connection` | `connections_beyond_the_cap_are_refused_and_released_slots_are_reused` |
@@ -407,7 +409,9 @@ Every queue, buffer, file, and collection the resident roles keep has a declared
407
409
  | Session reconciliation read | 64 MiB | `src/foundation/resident-tabs/recovery.ts` | `refuses to scan a session past the 64 MiB reconciliation cap` |
408
410
  | Release-hold report | 32 uncertainty reasons; 1 MiB read | `roles/release_holds.rs`, `release-holds.ts`, `bounded-host-command.ts` | `uncertainty_reasons_stay_within_their_cap`; `fails a release-hold report that pushes past its byte cap without returning a prefix` |
409
411
  | Maintenance exchange | 5 s reads, 60 s stops, 4 MiB report | `roles/maintain.rs`, `maintenance.ts`, `bounded-host-command.ts` | `fails a maintenance report that pushes past its byte cap without returning a prefix`, `kills and rejects a command that outlives its deadline` |
410
- | Attach client | no outbound queue: input is written synchronously in chunks of at most 1 MiB; inbound frames at most 2 MiB; one coalesced wake | `roles/attach.rs` | `refuses_oversized_header_before_payload`, `over_capacity_paste_delivers_no_prefix` |
412
+ | Attach server ingress | reader queue 64 frames/4 MiB; execution queue 8 frames/4 MiB; at most 64 KiB or 2 ms before yielding | `roles/attach.rs` | `verify-resident-parallel-progress.mjs` slow-receipt and switching cases |
413
+ | Attach input | 1,024 parsed events; semantic input is emitted in at most 1 MiB chunks and target-bound while a view/create is pending | `roles/attach.rs` | `deferred_input_is_atomic_bounded_and_bound_to_the_requested_tab` |
414
+ | Outer presentation writer | one replaceable complete desired state, 8 control/effect frames, and 4 MiB effect bytes | `roles/attach.rs` `OuterWriter` | `presentation_is_one_ordered_synchronized_write`; `verify-resident-parallel-progress.mjs` paused-sink case |
411
415
  | Doctor bundle | 25 MiB, oldest log generations dropped first | `src/foundation/resident-tabs/doctor.ts` | `drops the oldest log generations first to stay within its cap` |
412
416
 
413
417
  ### 13. Packaging and certification
@@ -76,6 +76,6 @@ Unverified, mismatched, or unsigned/unattested artifacts are not eligible for co
76
76
 
77
77
  ## Release artifact
78
78
 
79
- `npm run build:terminal-host` (`scripts/development/build-terminal-host.mjs`) runs only on release and native validation runners. It builds the library once with `cargo build --release --locked --lib --features node-addon`, copies the cdylib as `terminal-host.node`, loads the addon in Node, and checks its `version()` report: protocol, generation, crate version, pinned `libghostty-vt` commit, and the absence of `fault-injection` and `resident-fixtures`. The macOS x64 cross build may use the recorded Mach-O architecture check when the arm64 runner cannot load the addon.
79
+ `npm run build:terminal-host` (`scripts/development/build-terminal-host.mjs`) builds the library once with `cargo build --release --locked --lib --features node-addon`, copies the cdylib as `terminal-host.node`, loads the addon in Node, and checks its `version()` report: protocol, generation, crate version, pinned `libghostty-vt` commit, and the absence of `fault-injection` and `resident-fixtures`. Release and native validation runners invoke it with their pinned Zig toolchain. The normal developer `npm run build` invokes the same release-profile builder for the current platform; on Windows it links a manifest-verified terminal library exported by trusted CI and records the repository-pinned Zig producer version without invoking Zig on the workstation. The macOS x64 cross build may use the recorded Mach-O architecture check when the arm64 runner cannot load the addon.
80
80
 
81
81
  The generated `terminal-host.manifest.json` uses schema `a1-terminal-host-artifact-v2` and records `kind: "node-addon"`, Node-API 9, the addon's SHA-256 and size, the release-relative role-entry digest, the notices digest, target triple, Rust and Zig versions, and `Cargo.lock` digest. The role entry passes only the fixed role grammar and records `{ executable: process.execPath, entry, artifact: addonPath }` once in the addon; the explicit loaded artifact identity makes release-hold reporting independent of development environment propagation. Detached and WMI resident starts use the recorded Node-and-entry command and never resolve an A1-built executable. The publication `package` job refuses to pack unless every platform's process-guardian and terminal-host addons match their manifests, and A1 verifies the addon, entry, and notices before loading the host.
@@ -38,7 +38,9 @@ It contains no executable built from A1 sources, separate session-lock addon, br
38
38
 
39
39
  Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
40
40
 
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. Preview and stable candidate workflows build the addon on isolated Windows, Linux, and macOS arm64 runners, cross-compile the macOS x64 addon on the arm64 runner with `--target x86_64-apple-darwin`, and assemble all targets before packing. The same release runners compile TypeScript and run `npm run build:terminal-host`, which builds one Node-API 9 `terminal-host.node` containing the fixed roles and the session-writer lock. Its v2 manifest binds the addon, `dist/foundation/resident-tabs/host-entry.js`, and notices. The local `npm run build` does not build that addon because the Zig fetch for `libghostty-vt` is quarantined by workstation antivirus on Windows; local native iteration uses a verified terminal library downloaded from a CI lane (see [`terminal-host-provenance.md`](terminal-host-provenance.md) and [`resident-tabs.md`](resident-tabs.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).
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
+
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).
42
44
 
43
45
  ## Gates
44
46
 
@@ -193,7 +193,7 @@ validation; malformed, mixed, renamed, stale, or unavailable inputs fail closed.
193
193
  A legitimate generated baseline update remains outside the allowlist and follows the
194
194
  manually accepted mixed/code path.
195
195
 
196
- A new implementation-bound specification starts as OpenSpec-only artifacts in one normally named draft PR. Explicit approval to implement continues in that same worktree, branch, history, and PR; the plan does not merge first. Approved refinements reconcile planning before code. After implementation, add one to ten plain implementation-specific bullets under `## Acceptance` and mark the PR ready; the trusted `OpenSpec finalization` workflow conservatively synchronizes deltas, stages the dated archive plus conditional acceptance manifest, and commits them to the same branch, and exact-head CI validates that head. Auto-merge remains disabled: an authorized maintainer's manual merge accepts the listed scenarios and atomically integrates implementation, specs, and archive. No acceptance or archive follow-up PR is created. Closing an unmerged draft integrates nothing; cleanup still needs separate approval. See [delivery and archive handoff](openspec-archive-automation.md) for commands and legacy compatibility.
196
+ A new implementation-bound specification starts as OpenSpec-only artifacts in one normally named draft PR. Explicit approval to implement continues in that same worktree, branch, history, and PR; the plan does not merge first. Approved refinements reconcile planning before code. After implementation, add one to ten plain implementation-specific bullets under `## Acceptance` and mark the PR ready; the trusted `OpenSpec finalization` workflow conservatively synchronizes deltas, stages the dated archive plus conditional acceptance manifest, and commits them to the same branch, and exact-head CI validates that head. Automation-owned auto-merge remains disabled: an authorized maintainer's direct merge, personal exact-head auto-merge arm, or explicit request for the active agent to expected-head squash-merge the currently eligible candidate accepts the listed scenarios and atomically integrates implementation, specs, and archive. No acceptance or archive follow-up PR is created. Closing an unmerged draft integrates nothing; cleanup still needs separate approval. See [delivery and archive handoff](openspec-archive-automation.md) for commands and legacy compatibility.
197
197
 
198
198
  ## Numbered development previews
199
199
 
@@ -1,6 +1,6 @@
1
1
  # Atomic OpenSpec delivery and legacy archival
2
2
 
3
- New OpenSpec deliveries use one ordinary development pull request from draft planning through implementation, validation, acceptance, specification synchronization, and archival. An authorized maintainer chooses integration by manually merging the exact validated head or personally enabling GitHub native auto-merge for that unchanged head. Agents, Apps, bots, merge queue, and repository automation never arm or invoke implementation integration and never create a version-3 acceptance or archive follow-up PR.
3
+ New OpenSpec deliveries use one ordinary development pull request from draft planning through implementation, validation, acceptance, specification synchronization, and archival. An authorized maintainer chooses integration by manually merging the exact validated head, personally enabling GitHub native auto-merge for that unchanged head, or explicitly asking the active agent to merge the identified currently eligible head. Agents never infer or independently choose integration; Apps, bots, merge queue, workflows, and repository automation receive no implementation authority, and no version-3 acceptance or archive follow-up PR is created.
4
4
 
5
5
  Unrelated standalone documentation remains separate: a non-draft unassociated PR changing only `openspec/**`, `docs/**`, and/or root `README.md` retains the existing CI-gated automatic route. A new active change, implementation association, malformed lifecycle data, or mixed path remains outside documentation-owned integration. Before finalization or without valid human provenance, any arm is disabled; a finalized version-3 human arm remains a separate maintainer-owned route.
6
6
 
@@ -14,9 +14,23 @@ Version-1 and version-2 deliveries and their existing comments, acceptance PRs,
14
14
  4. **Plain acceptance list:** before checking the final item, keep the body phase-free and add final `## Acceptance` with one to ten concise implementation-specific behavior-and-result bullets. Do not use checkboxes, generic review/CI/approval/archive statements, angle-bracket placeholders or HTML, URLs, mentions, or automated-test inventory.
15
15
  5. **Automatic readiness and finalization:** reconcile and push current `origin/develop`, run the [delivery body check](#delivery-body-check) on the intended final body, then check the final item. That checkbox update completes the agent-owned implementation job; agents never mark implementation PRs ready themselves. Trusted automation revalidates the exact fully complete version-3 task-list snapshot and all pre-finalization inputs, marks the PR ready, and then conservatively synchronizes all deltas, moves the active change into its dated archive, stages the conditional acceptance manifest, commits that to the same branch with the archive App identity, and writes the emitted paths into the body's implementation fence. It never checks tasks or infers completion. Running the [finalization command](#finalization-command) locally first is optional and yields the same bytes.
16
16
  6. **Validate:** one normal exact-head workflow validates the finalized head: implementation, synchronized specs, archive, manifest, tasks/evidence, exact PR-body list, and every selected product/governance scope before emitting the stable protected aggregate. A new commit or acceptance-list change re-finalizes automatically when needed and requires full renewed validation; no lifecycle body edit or second workflow run is required.
17
- 7. **Maintainer integration accepts:** after reviewing the finalized candidate, an authorized human either manually merges the exact validated head after the stable protected aggregate succeeds or personally enables GitHub native auto-merge for that unchanged head so protected integration waits for the aggregate. That human action means the listed scenarios are accepted. Agents, Apps, bots, merge queue, automation-owned implementation arms, and documentation integration remain forbidden. A changed head, acceptance list, or finalization invalidates the earlier arm and requires the maintainer to choose again.
17
+ 7. **Maintainer integration accepts:** after reviewing the finalized candidate, an authorized human manually merges the exact validated head, personally enables GitHub native auto-merge for that unchanged head, or explicitly asks the active agent to merge that identified head while it is currently eligible. The agent route re-reads authoritative state, requires the exact finalized head and acceptance list, current `develop`, successful `Development validation required`, positive mergeability, and the maintainer's authenticated human `User`, then issues one protected squash merge with expected-head enforcement. It never waits, arms auto-merge, retries later, uses automation credentials or merge queue, or bypasses protection. Each route means the listed scenarios are accepted. A candidate change invalidates the decision and requires the maintainer to choose again.
18
18
  8. **Verify and clean:** trusted post-merge policy derives `Archived` and reports `accepted-and-archived` from committed bytes and immutable GitHub provenance without editing the accepted PR body. It publishes no lifecycle branch or PR. Shared exact-head remote cleanup may delete the unchanged topic ref; local cleanup remains separately ownership-controlled: the agent parks the worktree with `handoff` at step 7, and the next session's `sweep` removes it once the merge, archive, and remote-ref evidence verify (see [local cleanup](local-worktree-cleanup.md)).
19
19
 
20
+ ### Maintainer-requested agent merge
21
+
22
+ The permission is one-shot and candidate-bound. A same-session instruction such as “merge it” is sufficient only when the conversation identifies one handed-off PR and head unambiguously; implementation approval, green CI, an earlier request, or a standing preference is not merge authority. If the PR is not currently eligible, report the blocker and stop. Do not poll, watch, arm native auto-merge, or retain the instruction for later.
23
+
24
+ Immediately before mutation, refresh the PR and authenticated principal. Require an open non-draft same-repository PR into `develop`, the requested exact finalized head and acceptance list, `MERGEABLE`/`CLEAN` state, successful `Development validation required` for that head, and a human `User` with write, maintain, or admin repository permission. Then use the ordinary protected route with no `--admin` or `--auto`:
25
+
26
+ ```bash
27
+ gh pr merge <pr> --repo <owner>/<repository> --squash --match-head-commit <exact-head-sha>
28
+ ```
29
+
30
+ GitHub branch protection remains authoritative. A refusal, changed head, advanced target, stale finalization, failed or pending check, ambiguous identity, or unavailable read ends the attempt and needs a fresh maintainer request. After success, use the ordinary read-only accepted/archive verification and cleanup path.
31
+
32
+ The policy PR introducing this route must integrate through the previously deployed maintainer-owned route. Only a later candidate may use a requested agent merge after this policy is present on `develop`.
33
+
20
34
  ### Complete-regression evidence ordering
21
35
 
22
36
  PR-attached Full regression is selected only for a repair that the trusted triage created from a failed Full regression: base policy verifies the immutable `openspec-ci` App author, generated candidate identity, and `regression-provenance.json`. Human-authored lookalikes, publishing or validation-authority changes, labels, Publish-only repairs, and successful-run persistent-overrun candidates retain bounded ordinary validation. Generated repair drafts run no Development test suites, including after implementation begins. Finish substantive implementation tasks, focused local evidence, pre-finalization observations, and known-gap disposition before checking the final Implementation item. The ready active head defers testing while trusted finalization publishes the candidate; the finalized head then receives all four native lanes as new full evidence, required by `Development validation required` alongside the existing gates.
@@ -153,7 +167,7 @@ node scripts/governance/check-delivery-body.mjs --body-file .artifacts/agent/pr-
153
167
 
154
168
  `--final` validates the body as if every Implementation item were checked, so it can run before the last marker changes. `--pr <n>` reads the current pull request body instead. A defect exits 1 and prints the same `draft-<code>` reason the finalization run would report. The check covers association, Implementation, layout, and Acceptance only; detailed tasks, evidence, and target state remain covered by the [finalization command](#finalization-command) inspection mode.
155
169
 
156
- The committed conditional manifest contains the same ordered text. Trusted policy validates membership but never checks or edits it and never claims that the scenarios passed. The authorized maintainer's manual merge or personal native auto-merge arm for the unchanged exact head is the acceptance decision; no acceptance comment, checkbox edit, review-approval requirement, JSON edit, acceptance PR, archive PR, or later command is needed.
170
+ The committed conditional manifest contains the same ordered text. Trusted policy validates membership but never checks or edits it and never claims that the scenarios passed. The authorized maintainer's direct merge, personal native auto-merge arm, or explicit request for the active agent to merge the unchanged currently eligible exact head is the acceptance decision; no acceptance comment, checkbox edit, review-approval requirement, JSON edit, acceptance PR, archive PR, or later command is needed.
157
171
 
158
172
  If the body list changes, candidate validation reruns and compares it with the committed manifest. If the head changes, all prior exact-head CI is stale. If only the body changes to disagree with the manifest, integration remains blocked until the list and committed candidate agree again. No lifecycle body edit is needed after green CI.
159
173
 
@@ -214,7 +228,7 @@ Before a first finalization, canonical specs must still equal the selected targe
214
228
 
215
229
  ## Conditional acceptance and derived receipt
216
230
 
217
- `acceptance.md` truthfully says its verdict activates only when an authorized human manually merges the containing exact head after required current-head checks or personally arms native auto-merge for that unchanged head and GitHub integrates it after those checks. Its `openspec-delivery` block records stable pre-merge facts:
231
+ `acceptance.md` truthfully says its verdict activates only when an authorized human directly merges the containing exact head after required current-head checks, explicitly asks the active agent to execute that protected exact-head merge while it is eligible, or personally arms native auto-merge for the unchanged head and GitHub integrates it after those checks. Its `openspec-delivery` block records stable pre-merge facts:
218
232
 
219
233
  - schema version, repository, change, and source PR;
220
234
  - archive and manifest paths and finalization date;
@@ -223,7 +237,7 @@ Before a first finalization, canonical specs must still equal the selected targe
223
237
  - archive, canonical-spec, task, and evidence digests; and
224
238
  - explicit known-gap dispositions.
225
239
 
226
- It does not predict source head, merge commit, merger, or merge time. The PR head contains the manifest, so required CI binds those bytes without a recursive self-hash. After merge, the shared reader combines the manifest with immutable GitHub source head, successful required run, authorized human actor and permission, integration route, merge commit/time, and `develop` ancestry. Direct manual integration requires no active automatic authority. Human-enabled native auto-merge additionally requires a matching final-head enable event and `auto_merge.enabled_by`, with no later candidate commit or disable event. Queue, App/Bot, automation-owned, stale, malformed, or differently authored authority remains invalid. A human enable followed by disable before a valid manual merge is an abandoned attempt, not contradictory provenance. That shared result is the durable version-3 receipt used by audit and cleanup.
240
+ It does not predict source head, merge commit, merger, or merge time. The PR head contains the manifest, so required CI binds those bytes without a recursive self-hash. After merge, the shared reader combines the manifest with immutable GitHub source head, successful required run, authorized human actor and permission, integration route, merge commit/time, and `develop` ancestry. Direct integration, including an explicitly requested agent invocation through the maintainer's credential, requires no active automatic authority and retains the same authenticated-human merge provenance. Human-enabled native auto-merge additionally requires a matching final-head enable event and `auto_merge.enabled_by`, with no later candidate commit or disable event. Queue, App/Bot, automation-owned, stale, malformed, or differently authored authority remains invalid. A human enable followed by disable before a valid direct merge is an abandoned attempt, not contradictory provenance. That shared result is the durable version-3 receipt used by audit and cleanup.
227
241
 
228
242
  Unavailable, stale, automatic, unauthorized, conflicting, or contradictory provenance produces `invalid-provenance`/`blocked`, not acceptance. Post-merge code does not repair it by pushing `develop` or publishing another PR.
229
243
 
@@ -277,7 +291,7 @@ This bootstrap policy itself finishes under deployed version-2 authority. The fi
277
291
  - draft planning remaining unmerged;
278
292
  - explicit plan approval and same-PR implementation;
279
293
  - automated current-target finalization and exact-head CI;
280
- - authorized human integration, including manual merge and personally enabled exact-head native auto-merge, with plain acceptance scenarios;
294
+ - authorized human integration, including direct merge, an explicit exact-head agent-merge request, and personally enabled exact-head native auto-merge, with plain acceptance scenarios;
281
295
  - integrated canonical specs/archive and no generated acceptance/archive PR;
282
296
  - read-only `accepted-and-archived` verification and exact-head branch cleanup;
283
297
  - stale head/body/base/arm, incomplete tasks/evidence, malformed manifest, and automated or unauthorized auto-merge refusal fixtures; and
@@ -126,7 +126,7 @@ Download these artifacts from the exact workflow run:
126
126
 
127
127
  For selected implementation candidates, record focused and pre-finalization full observations before finalization. Keep final head/run/selection identity in Actions and handoff rather than adding a post-success commit that invalidates that evidence. A successful selected PR run replaces an additional mandatory dispatch; standalone runs remain diagnostic, and numbered-package nightly recovery remains independent.
128
128
 
129
- Draft pull requests run no Development test suites. A ready version-3 PR whose body still identifies an active change defers testing and emits no protected aggregate while `OpenSpec finalization` publishes the candidate. A ready unassociated PR also fails closed when immutable base/head evidence shows that it introduces or restores active delivery content, or combines active-change edits with code or operational paths; it does not fall through to ordinary validation merely because its body fence is absent. Existing active-change documentation-only revisions and ordinary code PRs without active delivery paths retain their established routes. After finalization records the archive and acceptance manifest, the ordinary exact-head workflow runs selected product/governance lanes and `Finalized delivery validation` in parallel, then emits the stable protected `Development validation required` aggregate only when both authorities succeed. Green CI enables maintainer review and manual merge but does not claim human acceptance or merge automatically. The finalization fence update is the validation trigger, so no pre-finalization test run or later lifecycle body edit is required. Any subsequent eligible implementation commit changes the head and reruns applicable validation; any body change reruns readiness and finalized-record validation and must continue to match the committed manifest. Legacy acceptance-record-only PRs retain their separate trusted `Acceptance record validation` route. Queue availability remains explicitly unavailable inside a runner and is calculated from the Actions API during final run analysis rather than guessed.
129
+ Draft pull requests run no Development test suites. A ready version-3 PR whose body still identifies an active change defers testing and emits no protected aggregate while `OpenSpec finalization` publishes the candidate. A ready unassociated PR also fails closed when immutable base/head evidence shows that it introduces or restores active delivery content, or combines active-change edits with code or operational paths; it does not fall through to ordinary validation merely because its body fence is absent. Existing active-change documentation-only revisions and ordinary code PRs without active delivery paths retain their established routes. After finalization records the archive and acceptance manifest, the ordinary exact-head workflow runs selected product/governance lanes and `Finalized delivery validation` in parallel, then emits the stable protected `Development validation required` aggregate only when both authorities succeed. Green CI enables maintainer review and an explicit integration decision but does not claim human acceptance or merge automatically. After green exact-head validation, the maintainer may directly merge, personally arm native auto-merge, or explicitly ask the active agent to perform one expected-head protected squash merge for that currently eligible candidate. The finalization fence update is the validation trigger, so no pre-finalization test run or later lifecycle body edit is required. Any subsequent eligible implementation commit changes the head and reruns applicable validation; any body change reruns readiness and finalized-record validation and must continue to match the committed manifest. Legacy acceptance-record-only PRs retain their separate trusted `Acceptance record validation` route. Queue availability remains explicitly unavailable inside a runner and is calculated from the Actions API during final run analysis rather than guessed.
130
130
 
131
131
  ## Rollback
132
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.2.7-dev.794",
3
+ "version": "0.2.7-dev.801",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",
@@ -23,7 +23,9 @@
23
23
  },
24
24
  "scripts": {
25
25
  "clean": "node scripts/clean.mjs",
26
- "build": "node scripts/development/check-environment.mjs && npm run clean && tsc -p tsconfig.build.json && node scripts/release/generate-release-notes-resource.mjs && node scripts/pi/build-pi-settings-metadata.mjs && node scripts/pi/build-startup-public.mjs && node scripts/development/build-process-guardian.mjs && node scripts/release/generate-runtime-payload-inventory.mjs",
26
+ "build": "node scripts/development/check-environment.mjs && npm run build:managed && node scripts/development/prepare-local-terminal-host.mjs && node scripts/release/generate-runtime-payload-inventory.mjs",
27
+ "build:core": "node scripts/development/check-environment.mjs && npm run build:managed && node scripts/release/generate-runtime-payload-inventory.mjs",
28
+ "build:managed": "npm run clean && tsc -p tsconfig.build.json && node scripts/release/generate-release-notes-resource.mjs && node scripts/pi/build-pi-settings-metadata.mjs && node scripts/pi/build-startup-public.mjs && node scripts/development/build-process-guardian.mjs",
27
29
  "build:process-guardian": "node scripts/development/build-process-guardian.mjs",
28
30
  "build:terminal-host": "node scripts/development/build-terminal-host.mjs",
29
31
  "typecheck": "tsgo -p tsconfig.json --noEmit && tsgo -p tsconfig.bin.json",
@@ -51,6 +53,7 @@
51
53
  "test:full": "node scripts/release/run-validation-tier.mjs full-release",
52
54
  "test:release": "node scripts/release/run-release-gates.mjs",
53
55
  "test:terminal-host": "node scripts/development/run-terminal-host-probe.mjs",
56
+ "test:resident-parallel-progress": "node scripts/development/verify-resident-parallel-progress.mjs",
54
57
  "test:resident-chaos": "node scripts/development/run-resident-chaos.mjs --build",
55
58
  "test:pi-engine-conformance": "vitest run test/integrations/pi/engine/conformance.test.ts test/integrations/pi/engine/runtime-integration.test.ts test/integrations/pi/engine/session-integration.test.ts",
56
59
  "report:pi-engine-conformance": "npm run build --silent && node scripts/pi/run-pi-engine-conformance.mjs",
@@ -65,7 +68,7 @@
65
68
  "start:pi": "npm run build && node scripts/development/dev-launch.mjs pi",
66
69
  "prepack": "node scripts/release/validate-packaged-release-note.mjs && node scripts/release/prepack-gate.mjs",
67
70
  "prepublishOnly": "npm run test:release",
68
- "prepare": "npm run build",
71
+ "prepare": "npm run build:core",
69
72
  "typecheck:bin": "tsgo -p tsconfig.bin.json"
70
73
  },
71
74
  "dependencies": {