@timurproko/a1 0.2.7-dev.794 → 0.2.7-dev.806
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/app/session-shell/clipboard-diagnostics.js +1 -1
- package/dist/app/session-shell/response-copy-helper.js +21 -6
- package/dist/app/session-shell/response-copy-protocol.d.ts +1 -1
- package/dist/app/session-shell/response-copy-transport.d.ts +3 -6
- package/dist/app/session-shell/response-copy-transport.js +60 -17
- package/dist/app/session-shell/session-shell-root.js +6 -1
- package/dist/app/session-shell/session-shell.js +12 -19
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +1 -1
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- 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/docs/architecture/terminal-host-provenance.md +1 -1
- package/docs/architecture/toolchain.md +3 -1
- package/docs/ci-release-runbook.md +1 -1
- package/docs/openspec-archive-automation.md +20 -6
- package/docs/validation.md +1 -1
- package/package.json +5 -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
47
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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) => {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "arm64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-19T04:50:42.711Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "a87eaaffcd986b624c8b044b756446cc1bdaf4334a1775b97820542fe34f9fb7",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"architecture": "arm64",
|
|
10
10
|
"target": "aarch64-apple-darwin",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-10-
|
|
12
|
+
"builtAt": "2026-10-19T04:52:31.538Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
15
|
"sha256": "af7b9fee2efa8478c728465031e73fb316b6b7012d11de0364130b0acd1b6e62",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-19T04:50:48.870Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "783a65f0de287a1c02cd5429dbe97bc8633f4cc8a7b92e3e961180b9bdcff814",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-apple-darwin",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-10-
|
|
12
|
+
"builtAt": "2026-10-19T04:54:11.822Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
15
|
"sha256": "1a5465ea12ecfe76d5f0c8fd5c11dee99597b4206a7e9ece01b37fc5c477c211",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "linux",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-19T04:50:20.956Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "118e5954e3864672bd8482ab7edf419cb27dcdf2533720316979b7f5cc5a53a5",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-unknown-linux-gnu",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-10-
|
|
12
|
+
"builtAt": "2026-10-19T04:53:24.471Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
15
|
"sha256": "843b10ab29eddf859a74a97875ba646a90917d9ec78445474895bb64f6e0772a",
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"platform": "win32",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-19T04:51:53.049Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "19eb07160f90033855f43c38008aaf90665e40bc68a87fac0c44585b362977cd",
|
|
14
14
|
"size": 402432
|
|
15
15
|
},
|
|
16
16
|
"provenance": {
|
|
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-10-
|
|
12
|
+
"builtAt": "2026-10-19T04:54:01.087Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "2e11d6f772aac6780a0d4d55a6a79169483b14b2b1e055f0f694702d38cab825",
|
|
16
|
+
"size": 5617152
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -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`)
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/docs/validation.md
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "0.2.7-dev.806",
|
|
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
|
|
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",
|
|
@@ -65,7 +67,7 @@
|
|
|
65
67
|
"start:pi": "npm run build && node scripts/development/dev-launch.mjs pi",
|
|
66
68
|
"prepack": "node scripts/release/validate-packaged-release-note.mjs && node scripts/release/prepack-gate.mjs",
|
|
67
69
|
"prepublishOnly": "npm run test:release",
|
|
68
|
-
"prepare": "npm run build",
|
|
70
|
+
"prepare": "npm run build:core",
|
|
69
71
|
"typecheck:bin": "tsgo -p tsconfig.bin.json"
|
|
70
72
|
},
|
|
71
73
|
"dependencies": {
|