gentle-pi 2.7.0 → 3.0.0

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 (122) hide show
  1. package/README.md +24 -6
  2. package/assets/agents/gentle-ai-worker.md +5 -1
  3. package/assets/agents/sdd-apply.md +9 -7
  4. package/assets/agents/sdd-archive.md +42 -23
  5. package/assets/agents/sdd-proposal.md +2 -2
  6. package/assets/agents/sdd-remediate.md +4 -4
  7. package/assets/agents/sdd-research.md +20 -48
  8. package/assets/agents/sdd-tasks.md +5 -5
  9. package/assets/agents/sdd-verify.md +6 -28
  10. package/assets/chains/sdd-full.chain.md +4 -22
  11. package/assets/chains/sdd-verify.chain.md +3 -12
  12. package/assets/orchestrator-delegation.md +33 -3
  13. package/assets/orchestrator-memory.md +20 -7
  14. package/assets/orchestrator.md +5 -3
  15. package/assets/sdd-orchestrator-workflow.md +25 -58
  16. package/assets/support/sdd-status-contract.md +9 -12
  17. package/docs/gentle-shell.md +14 -4
  18. package/docs/readme-reference.md +171 -32
  19. package/extensions/codegraph-tools.ts +2 -0
  20. package/extensions/gentle-agents.ts +281 -361
  21. package/extensions/gentle-ai.ts +588 -117
  22. package/extensions/gentle-shell.ts +74 -30
  23. package/extensions/pi-pretty.ts +63 -14
  24. package/extensions/quiet-tools.ts +1 -2
  25. package/extensions/startup-banner.ts +10 -9
  26. package/lib/agent-home.ts +8 -0
  27. package/lib/agent-profile-pin.ts +336 -0
  28. package/lib/agent-profiles.ts +28 -8
  29. package/lib/agents-config.ts +24 -2
  30. package/lib/agents-history.ts +3 -97
  31. package/lib/agents-keys.ts +27 -0
  32. package/lib/agents-protocol.ts +2 -15
  33. package/lib/agents-runner.ts +67 -111
  34. package/lib/agents-session-transport.ts +691 -0
  35. package/lib/command-palette-catalog.ts +87 -0
  36. package/lib/command-palette.ts +346 -0
  37. package/lib/native-choice-list.ts +5 -0
  38. package/lib/native-review-cli.ts +10 -97
  39. package/lib/review-publication-gate.ts +11 -1
  40. package/lib/review-repository.ts +1 -1
  41. package/lib/review-snapshot.ts +1 -0
  42. package/lib/review-transaction.ts +4 -2
  43. package/lib/sdd-preflight.ts +2 -1
  44. package/lib/sdd-research-capabilities.ts +18 -152
  45. package/lib/sdd-status.ts +7 -779
  46. package/lib/session-change-capture.ts +2 -1
  47. package/lib/session-changes.ts +8 -1
  48. package/lib/shell-bar.ts +21 -12
  49. package/lib/shell-card.ts +8 -12
  50. package/lib/shell-changes.ts +3 -2
  51. package/lib/shell-prompt.ts +25 -8
  52. package/lib/shell-sidebar-banner.ts +2 -2
  53. package/lib/shell-sidebar-layout.ts +5 -2
  54. package/lib/windows-session-transport.ts +877 -0
  55. package/package.json +3 -3
  56. package/runtime/native-review-cli.mjs +9 -96
  57. package/runtime/windows-session-transport.ps1 +791 -0
  58. package/scripts/test-packed-runner.mjs +1668 -20
  59. package/scripts/verify-package-files.mjs +0 -1
  60. package/tests/agent-home.test.ts +52 -0
  61. package/tests/agent-profiles.test.ts +30 -1
  62. package/tests/agents-config.test.ts +44 -0
  63. package/tests/agents-history.test.ts +12 -24
  64. package/tests/agents-runner.test.ts +307 -58
  65. package/tests/agents-session-transport-process.test.ts +249 -0
  66. package/tests/agents-session-transport.test.ts +823 -0
  67. package/tests/artifact-language.test.ts +10 -7
  68. package/tests/command-palette.test.ts +378 -0
  69. package/tests/delegated-key-learnings-contract.test.ts +2 -2
  70. package/tests/fixtures/agents-session-transport-process.mjs +108 -0
  71. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
  72. package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
  73. package/tests/fixtures/windows-session-compile.ps1 +110 -0
  74. package/tests/gentle-agents.test.ts +849 -356
  75. package/tests/gentle-ai.test.ts +472 -4
  76. package/tests/gentle-shell.test.ts +201 -8
  77. package/tests/native-choice-list.test.ts +13 -0
  78. package/tests/native-review-cli.test.ts +0 -33
  79. package/tests/odd-routing-contract.test.ts +208 -0
  80. package/tests/orchestrator-budget.test.ts +17 -2
  81. package/tests/package-manifest.test.ts +115 -27
  82. package/tests/persona-single-channel.test.ts +3 -3
  83. package/tests/pi-pretty.test.ts +45 -0
  84. package/tests/profile-pin.test.ts +370 -0
  85. package/tests/quiet-tool-rendering.test.ts +32 -5
  86. package/tests/review-contract-prompt.test.ts +9 -0
  87. package/tests/review-controller.test.ts +0 -44
  88. package/tests/review-session-standing-permission-ipc.test.ts +427 -13
  89. package/tests/runtime-harness.mjs +4 -4
  90. package/tests/sdd-agent-tools.test.ts +15 -36
  91. package/tests/sdd-archive-replay.test.ts +82 -0
  92. package/tests/sdd-classical-continuation.test.ts +74 -0
  93. package/tests/sdd-execution-routing-contract.test.ts +18 -2
  94. package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
  95. package/tests/sdd-native-managed-uptake.test.ts +11 -21
  96. package/tests/sdd-no-attempts-contract.test.ts +15 -0
  97. package/tests/sdd-odd-integration.test.ts +33 -0
  98. package/tests/sdd-optional-research.test.ts +124 -0
  99. package/tests/sdd-planning-routing-contract.test.ts +1 -1
  100. package/tests/sdd-preflight-rpc-input.test.ts +125 -0
  101. package/tests/sdd-preflight.test.ts +1 -1
  102. package/tests/sdd-research-capabilities.test.ts +20 -162
  103. package/tests/sdd-selection-transport.test.ts +180 -88
  104. package/tests/sdd-status.test.ts +5 -778
  105. package/tests/sdd-task-truth.test.ts +43 -0
  106. package/tests/session-change-capture.test.ts +20 -2
  107. package/tests/session-changes.test.ts +11 -0
  108. package/tests/shell-bar.test.ts +21 -0
  109. package/tests/shell-card.test.ts +8 -6
  110. package/tests/shell-changes.test.ts +8 -0
  111. package/tests/shell-prompt.test.ts +41 -7
  112. package/tests/shell-sidebar-banner.test.ts +4 -4
  113. package/tests/shell-sidebar-layout.test.ts +97 -13
  114. package/tests/startup-banner.test.ts +55 -2
  115. package/tests/windows-hidden-processes.test.ts +303 -0
  116. package/tests/windows-session-bootstrap.test.ts +1772 -0
  117. package/tests/windows-session-compile.test.ts +170 -0
  118. package/tests/windows-session-transport.test.ts +754 -0
  119. package/assets/agents/sdd-sync.md +0 -146
  120. package/lib/openspec-guardrails.ts +0 -99
  121. package/tests/native-sdd-attempt-authority.test.ts +0 -240
  122. package/tests/openspec-guardrails.test.ts +0 -71
@@ -1,21 +1,333 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import assert from "node:assert/strict";
4
- import { execFileSync } from "node:child_process";
4
+ import { execFileSync, spawn } from "node:child_process";
5
5
  import { createHash } from "node:crypto";
6
- import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
+ import { existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
- import { basename, dirname, join } from "node:path";
8
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
9
+ import { createRequire } from "node:module";
9
10
  import { fileURLToPath, pathToFileURL } from "node:url";
10
11
 
11
12
  const root = fileURLToPath(new URL("..", import.meta.url));
12
- const temporary = mkdtempSync(join(tmpdir(), "gentle-pi-packed-runner-"));
13
- const packDirectory = join(temporary, "pack");
14
- const installDirectory = join(temporary, "install");
15
- // Every child inherits only disposable Pi homes, never the operator's settings.
16
- const agentHome = join(temporary, "agent");
17
- const piAgentHome = join(temporary, "pi-agent");
18
- const isolatedEnv = { ...process.env, GENTLE_PI_AGENT_HOME: agentHome, PI_CODING_AGENT_DIR: piAgentHome };
13
+ const MAX_NPM_OUTPUT_BYTES = 1024 * 1024;
14
+ const MAX_UNHOOKED_REPORT_BYTES = 1024;
15
+ const MAX_SDK_LIFECYCLE_CHILD_REPORT_BYTES = 2048;
16
+ // Four sequential 30s imports precede the existing 240s lifecycle allowance: two
17
+ // 30s session factories, two 40s binds, one 40s registry, three 2s lists, two 15s
18
+ // runtime disposals, a 15s observer close, and 9s receipt/scheduling. This finite
19
+ // 360s watchdog bounds reporting, not blocked SDK cancellation.
20
+ const SDK_LIFECYCLE_PROBE_TIMEOUT_MS = 4 * 30_000 + 2 * 30_000 + 2 * 40_000 + 40_000 + 3 * 2_000 + 2 * 15_000 + 15_000 + 4_000 + 5_000;
21
+ const MAX_WINDOWS_STARTUP_TIMING_REPORT_BYTES = 1024;
22
+ const MAX_WINDOWS_STARTUP_TIMING_ENVIRONMENT_REPORT_BYTES = 2048;
23
+ const MAX_WINDOWS_STARTUP_TIMING_OUTPUT_BYTES = 32 * 1024;
24
+ const WINDOWS_STARTUP_TIMING_BUDGET_MS = 25_000;
25
+ const WINDOWS_STARTUP_TIMING_CLEANUP_GRACE_MS = 5_000;
26
+ const WINDOWS_STARTUP_TIMING_FORCE_CLOSE_GRACE_MS = 2_500;
27
+ const WINDOWS_STARTUP_TIMING_POWERSHELL = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
28
+ const WINDOWS_STARTUP_TIMING_START_REQUEST = '{"requestId":"start-1","operation":"start"}\n';
29
+ const WINDOWS_STARTUP_TIMING_ENVIRONMENT_CASE_NAMES = Object.freeze(["baseline", "windows-paths"]);
30
+ const WINDOWS_STARTUP_TIMING_ENVIRONMENT_RECEIPT_FLAGS = Object.freeze({
31
+ caseOrder: "baseline-first-fixed",
32
+ sharedState: "shared-owned-home-and-cache",
33
+ confounders: "baseline-first-order-and-cache-effects",
34
+ conclusion: "no-causal-attribution-not-product-ready",
35
+ });
36
+ export const WINDOWS_STARTUP_TIMING_WINDOWS_PATH_KEYS = Object.freeze([
37
+ "ProgramFiles", "ProgramFiles(x86)", "ProgramW6432",
38
+ "CommonProgramFiles", "CommonProgramFiles(x86)", "CommonProgramW6432", "PSModulePath",
39
+ ]);
40
+ // Write-Reply uses ConvertTo-Json -Compress on a three-key hashtable. Hashtable
41
+ // enumeration order is not an API guarantee, so admit only its six compact key orders.
42
+ const WINDOWS_STARTUP_TIMING_VALID_START_REPLIES = new Set([
43
+ '{"requestId":"start-1","ok":true,"result":{"state":"partial"}}',
44
+ '{"requestId":"start-1","result":{"state":"partial"},"ok":true}',
45
+ '{"ok":true,"requestId":"start-1","result":{"state":"partial"}}',
46
+ '{"ok":true,"result":{"state":"partial"},"requestId":"start-1"}',
47
+ '{"result":{"state":"partial"},"requestId":"start-1","ok":true}',
48
+ '{"result":{"state":"partial"},"ok":true,"requestId":"start-1"}',
49
+ ]);
50
+ const WINDOWS_STARTUP_TIMING_REJECTED_START_REPLY = /^\{(?:"requestId":"start-1","ok":false,"error":"(?:unavailable|unsafe|busy|not_found|invalid)"|"requestId":"start-1","error":"(?:unavailable|unsafe|busy|not_found|invalid)","ok":false|"ok":false,"requestId":"start-1","error":"(?:unavailable|unsafe|busy|not_found|invalid)"|"ok":false,"error":"(?:unavailable|unsafe|busy|not_found|invalid)","requestId":"start-1"|"error":"(?:unavailable|unsafe|busy|not_found|invalid)","requestId":"start-1","ok":false|"error":"(?:unavailable|unsafe|busy|not_found|invalid)","ok":false,"requestId":"start-1")\}$/;
51
+ const UNHOOKED_STAGES = new Set(["pack", "pack-result", "install", "artifact-check", "import-probe", "post-import-check", "cleanup"]);
52
+ const UNHOOKED_ERROR_CODES = new Set(["spawn-failed", "timed-out", "output-limit", "nonzero-exit", "invalid-result", "assertion-failed", "cleanup-failed", "unknown"]);
53
+ const SDK_LIFECYCLE_STAGES = new Set(["pack", "pack-result", "install", "artifact-check", "lifecycle-probe", "lifecycle-result", "cleanup"]);
54
+ const WINDOWS_STARTUP_TIMING_STAGES = new Set(["pack", "pack-result", "install", "artifact-check", "helper-start", "helper-result", "cleanup"]);
55
+ const WINDOWS_STARTUP_TIMING_ERROR_CODES = new Set(["spawn-failed", "timed-out", "invalid-output", "rejected", "stream-failed", "write-failed", "exited", "nonzero-exit", "cleanup-unconfirmed", "environment-invalid", "assertion-failed", "cleanup-failed", "unknown"]);
56
+ const WINDOWS_STARTUP_TIMING_OUTCOMES = new Set(["not-attempted", "valid-reply", "rejected", "timed-out", "invalid-output", "spawn-failed", "stream-failed", "write-failed", "exited"]);
57
+ const WINDOWS_STARTUP_TIMING_CLEANUP_OUTCOMES = new Set(["not-attempted", "close-observed", "close-unconfirmed", "blocked"]);
58
+ const SDK_LIFECYCLE_ERROR_CODES = new Set(["spawn-failed", "timed-out", "unconfirmed-close", "output-limit", "nonzero-exit", "invalid-result", "assertion-failed", "cleanup-failed", "unknown"]);
59
+ const SDK_LIFECYCLE_EXTENSION_ERROR_PHASES = new Set(["unobserved", "none", "startup", "shutdown"]);
60
+ const SDK_LIFECYCLE_WINDOWS_OBSERVATION_AVAILABILITY = new Set(["not-applicable", "unavailable", "observed"]);
61
+ const SDK_LIFECYCLE_WINDOWS_OBSERVATION_PROVENANCE = new Set(["not-applicable", "unavailable", "ambiguous", "owned-instance"]);
62
+ const SDK_LIFECYCLE_WINDOWS_OBSERVATION_RESTORATION = new Set(["not-applicable", "not-required", "not-attempted"]);
63
+ const SDK_LIFECYCLE_WINDOWS_OBSERVATION_PHASES = new Set(["start", "initialize", "cleanup"]);
64
+ const SDK_LIFECYCLE_WINDOWS_OBSERVATION_FAILURE_CLASSES = new Set(["timed-out", "rejected", "unknown"]);
65
+ const SDK_LIFECYCLE_WINDOWS_STARTUP_MARKERS = new Set(["script-entered", "native-ready"]);
66
+ // This is the complete source-defined diagnostic vocabulary. The receipt never accepts
67
+ // a native error code, message, property, or process output as rejection evidence.
68
+ const SDK_LIFECYCLE_WINDOWS_OBSERVATION_ERROR_CODES = new Set(["spawn", "stream", "process", "exit", "write", "deadline", "protocol", "start-reply", "stopped", "unwritable", "unknown"]);
69
+ const SDK_LIFECYCLE_CHILD_STAGES = new Set(["bootstrap", "sdk-load", "jiti-load", "agents-module-load", "model-runtime", "services", "extensions-validate", "model-availability", "session-create", "session-bind", "presence-two", "dispose-first", "presence-one", "dispose-second", "presence-none", "cleanup"]);
70
+ const SDK_LIFECYCLE_CHILD_CHECK_IDS = new Set(["bootstrap-builtins", "bootstrap-agent-home", "bootstrap-directories", "sdk-import", "sdk-exports", "jiti-import", "agents-module-import", "agents-module-export", "settings-untrusted", "settings-default-provider", "settings-default-model", "model-runtime-create", "services-create", "services-settings-manager", "services-project-trusted", "extensions-errors", "extensions-paths", "extensions-hooks", "services-diagnostics", "ambient-skills", "ambient-prompts", "ambient-themes", "ambient-context-files", "model-availability-empty", "session-create", "session-model-unbound", "session-model-invocation-guard", "session-bind", "session-bind-extension-errors", "session-model-bound", "session-ids-distinct", "presence-observer-create", "presence-two-records", "presence-first-model-unbound", "presence-second-model-unbound", "presence-two-extension-errors", "dispose-first", "dispose-first-extension-errors", "presence-one-record", "presence-one-model-unbound", "presence-one-extension-errors", "dispose-second", "dispose-second-extension-errors", "presence-no-records", "presence-none-extension-errors", "cleanup-runtime", "cleanup-observer", "cleanup-extension-errors"]);
71
+ const SDK_LIFECYCLE_CHILD_ERROR_CODES = new Set(["assertion-failed", "forbidden-model-invocation", "load-failed", "timed-out", "cleanup-failed", "observation-invalid", "unknown"]);
72
+ const SDK_LIFECYCLE_CHILD_CLEANUP_STATUSES = new Set(["complete", "failed"]);
73
+ const SDK_LIFECYCLE_CHECK_IDS = new Set([
74
+ "not-attempted", "runner-hosted", "runner-temp", "temporary-root", "project-sdk-version", "pack-command", "pack-metadata", "pack-integrity", "install-command", "packed-assets", "native-artifacts", "sdk-manifest", "sdk-version", "jiti-manifest-owned", "jiti-static-export", "jiti-entry-owned", "jiti-version", "lifecycle-probe-command", "lifecycle-probe-result", "sdk-lifecycle-complete", "cleanup-owned-root-removal",
75
+ ]);
76
+ const WINDOWS_STARTUP_TIMING_CHECK_IDS = new Set([
77
+ "not-attempted", "runner-hosted", "runner-temp", "temporary-root", "project-sdk-version", "pack-command", "pack-metadata", "pack-integrity", "install-command", "packed-assets", "native-artifacts", "installed-helper", "helper-start", "helper-result", "windows-helper-startup-measured", "windows-helper-startup-environment-measured", "cleanup-owned-root-removal",
78
+ ]);
79
+ const UNHOOKED_CHECK_IDS = new Set([
80
+ "not-attempted", "runner-temp", "temporary-root", "project-sdk-version", "pack-command", "pack-metadata", "pack-integrity", "install-command", "import-probe-command", "import-probe-result", "unhooked-imports-complete", "cleanup-owned-root-removal",
81
+ "asset-runtime-windows-session-transport-owned", "asset-runtime-windows-session-transport-hash",
82
+ "asset-lib-windows-session-transport-owned", "asset-lib-windows-session-transport-hash",
83
+ "asset-lib-agents-session-transport-owned", "asset-lib-agents-session-transport-hash",
84
+ "asset-extension-gentle-agents-owned", "asset-extension-gentle-agents-hash",
85
+ "asset-extension-gentle-ai-owned", "asset-extension-gentle-ai-hash",
86
+ "asset-native-review-cli-owned", "asset-native-review-cli-hash",
87
+ "asset-review-integration-v2-owned", "asset-review-integration-v2-hash",
88
+ "asset-installer-gentle-ai-owned", "asset-installer-gentle-ai-hash",
89
+ "asset-installer-tui-mode-setting-owned", "asset-installer-tui-mode-setting-hash",
90
+ "native-package-cache-absent", "native-command-absent", "sdk-manifest-owned", "sdk-version", "jiti-manifest-owned", "jiti-static-export", "jiti-entry-owned", "jiti-version",
91
+ "home-empty", "gentle-pi-agent-empty", "pi-coding-agent-empty", "gentle-pi-config-empty", "xdg-config-empty", "xdg-cache-empty", "xdg-data-empty", "appdata-empty", "local-appdata-empty",
92
+ ]);
93
+
94
+ function newUnhookedReceipt() {
95
+ return { checkId: "not-attempted", packVerified: false, installCompleted: false, cleanupCompleted: false };
96
+ }
97
+
98
+ function newSdkLifecycleReceipt() {
99
+ return {
100
+ checkId: "not-attempted", packVerified: false, installCompleted: false, childReceiptObserved: false, sdkLoaded: false,
101
+ extensionErrorCount: null, extensionErrorPhase: "unobserved", modelInvocationCount: null, agentStartEventCount: null, turnStartEventCount: null, sessionsStarted: 0,
102
+ presenceAfterStart: 0, presenceAfterFirstDispose: 0, presenceAfterSecondDispose: 0,
103
+ disposedSessions: 0, windowsRegistryObservation: null, cleanupCompleted: false,
104
+ };
105
+ }
106
+
107
+ function newWindowsStartupTimingReceipt() {
108
+ return {
109
+ checkId: "not-attempted", packVerified: false, installCompleted: false, budgetMs: WINDOWS_STARTUP_TIMING_BUDGET_MS,
110
+ helperStartOutcome: "not-attempted", startElapsedMs: null, lastStartupMarker: null,
111
+ cleanup: "not-attempted", physicalCloseObserved: false, cleanupCompleted: false,
112
+ };
113
+ }
114
+
115
+ function newWindowsStartupTimingEnvironmentCase(name, pathAdditionKeys) {
116
+ if (!WINDOWS_STARTUP_TIMING_ENVIRONMENT_CASE_NAMES.includes(name)) throw new Error("invalid Windows startup environment case");
117
+ return {
118
+ name, pathAdditionKeys, budgetMs: WINDOWS_STARTUP_TIMING_BUDGET_MS,
119
+ helperStartOutcome: "not-attempted", startElapsedMs: null, lastStartupMarker: null,
120
+ cleanup: "not-attempted", physicalCloseObserved: false, failureStage: null, failureCode: null,
121
+ };
122
+ }
123
+
124
+ function selectUnhookedCheck(receipt, checkId) {
125
+ if (!UNHOOKED_CHECK_IDS.has(checkId)) throw new Error("invalid unhooked check identifier");
126
+ receipt.checkId = checkId;
127
+ }
128
+
129
+ function selectSdkLifecycleCheck(receipt, checkId) {
130
+ if (!SDK_LIFECYCLE_CHECK_IDS.has(checkId)) throw new Error("invalid SDK lifecycle check identifier");
131
+ receipt.checkId = checkId;
132
+ }
133
+
134
+ function selectWindowsStartupTimingCheck(receipt, checkId) {
135
+ if (!WINDOWS_STARTUP_TIMING_CHECK_IDS.has(checkId)) throw new Error("invalid Windows startup timing check identifier");
136
+ receipt.checkId = checkId;
137
+ }
138
+
139
+ function isWithin(rootPath, candidatePath) {
140
+ const root = resolve(rootPath);
141
+ const candidate = resolve(candidatePath);
142
+ const remainder = relative(root, candidate);
143
+ return remainder !== "" && !isAbsolute(remainder) && !remainder.split(sep).includes("..");
144
+ }
145
+
146
+ function assertOwnedRegularFile(rootPath, relativePath) {
147
+ const ownedRoot = resolve(rootPath);
148
+ const rootEntry = lstatSync(ownedRoot);
149
+ if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) throw new Error(`refusing unowned package root: ${rootPath}`);
150
+ const path = resolve(ownedRoot, relativePath);
151
+ if (!isWithin(ownedRoot, path)) throw new Error(`path escapes owned root: ${relativePath}`);
152
+ const normalizedRelativePath = relative(ownedRoot, path);
153
+ let current = ownedRoot;
154
+ for (const segment of normalizedRelativePath.split(sep)) {
155
+ current = join(current, segment);
156
+ const entry = lstatSync(current);
157
+ if (entry.isSymbolicLink()) throw new Error(`refusing symbolic link in owned package: ${relativePath}`);
158
+ }
159
+ if (!lstatSync(path).isFile()) throw new Error(`expected regular file: ${relativePath}`);
160
+ return path;
161
+ }
162
+
163
+ function assertEmptyDirectory(path) {
164
+ const entries = readdirSync(path);
165
+ if (entries.length !== 0) throw new Error(`disposable home changed unexpectedly: ${path}`);
166
+ }
167
+
168
+ function safeJson(buffer, description) {
169
+ const text = Buffer.isBuffer(buffer) ? buffer.toString("utf8") : String(buffer);
170
+ if (Buffer.byteLength(text, "utf8") > MAX_NPM_OUTPUT_BYTES) throw new Error(`${description} exceeded 1 MiB`);
171
+ return JSON.parse(text);
172
+ }
173
+
174
+ function validExitStatus(value) {
175
+ return Number.isInteger(value) && value > 0 && value <= 255 ? value : undefined;
176
+ }
177
+
178
+ class UnhookedFailure extends Error {
179
+ constructor(stage, code, exitStatus) {
180
+ super("unhooked packed proof failed");
181
+ this.stage = UNHOOKED_STAGES.has(stage) ? stage : "cleanup";
182
+ this.code = UNHOOKED_ERROR_CODES.has(code) ? code : "unknown";
183
+ this.exitStatus = validExitStatus(exitStatus);
184
+ }
185
+ }
186
+
187
+ class WindowsStartupTimingFailure extends Error {
188
+ constructor(stage, code) {
189
+ super("Windows helper startup timing experiment failed");
190
+ this.stage = WINDOWS_STARTUP_TIMING_STAGES.has(stage) ? stage : "cleanup";
191
+ this.code = WINDOWS_STARTUP_TIMING_ERROR_CODES.has(code) ? code : "unknown";
192
+ }
193
+ }
194
+
195
+ class SdkLifecycleFailure extends Error {
196
+ constructor(stage, code, exitStatus, childReceipt) {
197
+ super("SDK lifecycle packed proof failed");
198
+ this.stage = SDK_LIFECYCLE_STAGES.has(stage) ? stage : "cleanup";
199
+ this.code = SDK_LIFECYCLE_ERROR_CODES.has(code) ? code : "unknown";
200
+ this.exitStatus = validExitStatus(exitStatus);
201
+ this.childStage = "unknown";
202
+ this.childCheckId = "unobserved";
203
+ this.childCleanupStatus = "unobserved";
204
+ this.childFailureCode = "unknown";
205
+ if (childReceipt !== undefined) {
206
+ this.childStage = childReceipt.stage;
207
+ this.childCheckId = childReceipt.checkId;
208
+ this.childCleanupStatus = childReceipt.cleanupStatus;
209
+ this.childFailureCode = childReceipt.childFailureCode;
210
+ }
211
+ }
212
+ }
213
+
214
+ function processFailure(stage, error) {
215
+ const details = error && typeof error === "object" ? error : {};
216
+ const exitStatus = validExitStatus(details.status);
217
+ if (details.code === "ETIMEDOUT") return new UnhookedFailure(stage, "timed-out", exitStatus);
218
+ if (details.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return new UnhookedFailure(stage, "output-limit", exitStatus);
219
+ return new UnhookedFailure(stage, exitStatus === undefined ? "spawn-failed" : "nonzero-exit", exitStatus);
220
+ }
221
+
222
+ function stageFailure(stage, error, invalidResult = false) {
223
+ if (error instanceof UnhookedFailure) return error;
224
+ return new UnhookedFailure(stage, invalidResult ? "invalid-result" : "assertion-failed");
225
+ }
226
+
227
+ function reportUnhookedReceipt(receipt, error) {
228
+ const failure = error instanceof UnhookedFailure ? error : undefined;
229
+ const report = {
230
+ mode: "unhooked-imports",
231
+ status: failure === undefined ? "complete" : "failed",
232
+ checkId: failure === undefined ? "unhooked-imports-complete" : receipt.checkId,
233
+ packVerified: receipt.packVerified,
234
+ installCompleted: receipt.installCompleted,
235
+ cleanupCompleted: receipt.cleanupCompleted,
236
+ ...(failure === undefined ? {} : { stage: failure.stage, code: failure.code, ...(failure.exitStatus === undefined ? {} : { exitStatus: failure.exitStatus }) }),
237
+ };
238
+ const line = JSON.stringify(report);
239
+ const boundedLine = Buffer.byteLength(line, "utf8") <= MAX_UNHOOKED_REPORT_BYTES
240
+ ? line
241
+ : '{"mode":"unhooked-imports","status":"failed","checkId":"not-attempted","packVerified":false,"installCompleted":false,"cleanupCompleted":false,"stage":"cleanup","code":"unknown"}';
242
+ try { (failure === undefined ? process.stdout : process.stderr).write(`${boundedLine}\n`); } catch { /* Reporting cannot expose a raw secondary error. */ }
243
+ if (failure !== undefined) process.exitCode = failure.exitStatus ?? 1;
244
+ }
245
+
246
+ function reportWindowsStartupTimingReceipt(receipt, error) {
247
+ const failure = error instanceof WindowsStartupTimingFailure ? error : undefined;
248
+ const report = {
249
+ mode: "windows-startup-timing",
250
+ status: failure === undefined ? "complete" : "failed",
251
+ checkId: failure === undefined ? "windows-helper-startup-measured" : receipt.checkId,
252
+ packVerified: receipt.packVerified,
253
+ installCompleted: receipt.installCompleted,
254
+ budgetMs: receipt.budgetMs,
255
+ helperStartOutcome: receipt.helperStartOutcome,
256
+ startElapsedMs: receipt.startElapsedMs,
257
+ lastStartupMarker: receipt.lastStartupMarker,
258
+ cleanup: receipt.cleanup,
259
+ physicalCloseObserved: receipt.physicalCloseObserved,
260
+ cleanupCompleted: receipt.cleanupCompleted,
261
+ ...(failure === undefined ? {} : { stage: failure.stage, code: failure.code }),
262
+ };
263
+ const line = JSON.stringify(report);
264
+ const boundedLine = Buffer.byteLength(line, "utf8") <= MAX_WINDOWS_STARTUP_TIMING_REPORT_BYTES
265
+ ? line
266
+ : '{"mode":"windows-startup-timing","status":"failed","checkId":"not-attempted","packVerified":false,"installCompleted":false,"budgetMs":25000,"helperStartOutcome":"not-attempted","startElapsedMs":null,"lastStartupMarker":null,"cleanup":"not-attempted","physicalCloseObserved":false,"cleanupCompleted":false,"stage":"cleanup","code":"unknown"}';
267
+ try { (failure === undefined ? process.stdout : process.stderr).write(`${boundedLine}\n`); } catch { /* Reporting cannot expose a raw secondary error. */ }
268
+ if (failure !== undefined) process.exitCode = 1;
269
+ }
270
+
271
+ function reportWindowsStartupTimingEnvironmentReceipt(receipt, error) {
272
+ const failure = error instanceof WindowsStartupTimingFailure ? error : undefined;
273
+ const report = {
274
+ mode: "windows-startup-timing-environment",
275
+ status: failure === undefined ? "complete" : "failed",
276
+ checkId: failure === undefined ? "windows-helper-startup-environment-measured" : receipt.checkId,
277
+ packVerified: receipt.packVerified,
278
+ installCompleted: receipt.installCompleted,
279
+ isolation: "shared-owned-homes-fresh-helper",
280
+ ...WINDOWS_STARTUP_TIMING_ENVIRONMENT_RECEIPT_FLAGS,
281
+ allowedPathAdditionKeys: WINDOWS_STARTUP_TIMING_WINDOWS_PATH_KEYS,
282
+ cases: receipt.cases,
283
+ cleanupCompleted: receipt.cleanupCompleted,
284
+ stage: failure?.stage ?? null,
285
+ code: failure?.code ?? null,
286
+ };
287
+ const line = JSON.stringify(report);
288
+ const fallback = JSON.stringify({
289
+ mode: "windows-startup-timing-environment", status: "failed", checkId: receipt.checkId,
290
+ packVerified: receipt.packVerified, installCompleted: receipt.installCompleted,
291
+ isolation: "shared-owned-homes-fresh-helper", ...WINDOWS_STARTUP_TIMING_ENVIRONMENT_RECEIPT_FLAGS,
292
+ allowedPathAdditionKeys: WINDOWS_STARTUP_TIMING_WINDOWS_PATH_KEYS, cases: receipt.cases,
293
+ cleanupCompleted: receipt.cleanupCompleted, stage: failure?.stage ?? "unknown", code: failure?.code ?? "unknown",
294
+ });
295
+ const boundedLine = Buffer.byteLength(line, "utf8") <= MAX_WINDOWS_STARTUP_TIMING_ENVIRONMENT_REPORT_BYTES ? line : fallback;
296
+ try { (failure === undefined ? process.stdout : process.stderr).write(`${boundedLine}\n`); } catch { /* Reporting cannot expose a raw secondary error. */ }
297
+ if (failure !== undefined) process.exitCode = 1;
298
+ }
299
+
300
+ function reportSdkLifecycleReceipt(receipt, error) {
301
+ const failure = error instanceof SdkLifecycleFailure ? error : undefined;
302
+ const report = {
303
+ mode: "sdk-lifecycle",
304
+ status: failure === undefined ? "complete" : "failed",
305
+ checkId: failure === undefined ? "sdk-lifecycle-complete" : receipt.checkId,
306
+ packVerified: receipt.packVerified,
307
+ installCompleted: receipt.installCompleted,
308
+ childReceiptObserved: receipt.childReceiptObserved,
309
+ sdkLoaded: receipt.sdkLoaded,
310
+ extensionErrorCount: receipt.extensionErrorCount,
311
+ extensionErrorPhase: receipt.extensionErrorPhase,
312
+ sessionsStarted: receipt.sessionsStarted,
313
+ presenceAfterStart: receipt.presenceAfterStart,
314
+ presenceAfterFirstDispose: receipt.presenceAfterFirstDispose,
315
+ presenceAfterSecondDispose: receipt.presenceAfterSecondDispose,
316
+ disposedSessions: receipt.disposedSessions,
317
+ modelInvocationCount: receipt.modelInvocationCount,
318
+ agentStartEventCount: receipt.agentStartEventCount,
319
+ turnStartEventCount: receipt.turnStartEventCount,
320
+ windowsRegistryObservation: receipt.windowsRegistryObservation,
321
+ cleanupCompleted: receipt.cleanupCompleted,
322
+ ...(failure === undefined ? {} : { stage: failure.stage, code: failure.code, childStage: failure.childStage, childCheckId: failure.childCheckId, childCleanupStatus: failure.childCleanupStatus, ...(failure.childFailureCode === undefined ? {} : { childFailureCode: failure.childFailureCode }), ...(failure.exitStatus === undefined ? {} : { exitStatus: failure.exitStatus }) }),
323
+ };
324
+ const line = JSON.stringify(report);
325
+ const boundedLine = Buffer.byteLength(line, "utf8") <= MAX_UNHOOKED_REPORT_BYTES
326
+ ? line
327
+ : '{"mode":"sdk-lifecycle","status":"failed","checkId":"not-attempted","packVerified":false,"installCompleted":false,"childReceiptObserved":false,"sdkLoaded":false,"extensionErrorCount":null,"extensionErrorPhase":"unobserved","sessionsStarted":0,"presenceAfterStart":0,"presenceAfterFirstDispose":0,"presenceAfterSecondDispose":0,"disposedSessions":0,"modelInvocationCount":null,"agentStartEventCount":null,"turnStartEventCount":null,"cleanupCompleted":false,"stage":"cleanup","code":"unknown","childStage":"unknown","childCheckId":"unobserved","childCleanupStatus":"unobserved","childFailureCode":"unknown"}';
328
+ try { (failure === undefined ? process.stdout : process.stderr).write(`${boundedLine}\n`); } catch { /* Reporting cannot expose a raw secondary error. */ }
329
+ if (failure !== undefined) process.exitCode = failure.exitStatus ?? 1;
330
+ }
19
331
 
20
332
  function windowsNpmInvocation() {
21
333
  const candidates = [];
@@ -24,7 +336,7 @@ function windowsNpmInvocation() {
24
336
  const installedCli = candidates.find((path) => existsSync(path));
25
337
  if (installedCli !== undefined) return { file: process.execPath, prefix: [installedCli] };
26
338
  let commandPaths = [];
27
- try { commandPaths = execFileSync("where.exe", ["npm"], { encoding: "utf8", windowsHide: true }).split(/\r?\n/).filter(Boolean); }
339
+ try { commandPaths = execFileSync("where.exe", ["npm"], { encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }).split(/\r?\n/).filter(Boolean); }
28
340
  catch { /* fall through to the explicit resolution error */ }
29
341
  for (const path of commandPaths) {
30
342
  if (basename(path).toLowerCase() === "npm.exe") return { file: path, prefix: [] };
@@ -34,16 +346,273 @@ function windowsNpmInvocation() {
34
346
  throw new Error("could not resolve npm-cli.js without a command shell");
35
347
  }
36
348
 
37
- function runNpm(arguments_, options) {
349
+ function runNpmWithEnv(arguments_, env, options) {
38
350
  const invocation = process.platform === "win32" ? windowsNpmInvocation() : { file: "npm", prefix: [] };
39
- return execFileSync(invocation.file, [...invocation.prefix, ...arguments_], { ...options, env: isolatedEnv });
351
+ return execFileSync(invocation.file, [...invocation.prefix, ...arguments_], { ...options, env });
40
352
  }
41
353
 
42
- try {
43
- mkdirSync(packDirectory);
44
- mkdirSync(installDirectory);
45
- mkdirSync(agentHome);
46
- mkdirSync(piAgentHome);
354
+ function runBoundedNpm(stage, arguments_, env, cwd) {
355
+ try {
356
+ return runNpmWithEnv(arguments_, env, {
357
+ cwd,
358
+ encoding: "utf8",
359
+ stdio: ["ignore", "pipe", "pipe"],
360
+ timeout: 120000,
361
+ maxBuffer: MAX_NPM_OUTPUT_BYTES,
362
+ });
363
+ } catch (error) {
364
+ throw processFailure(stage, error);
365
+ }
366
+ }
367
+
368
+ function runBoundedProbe(arguments_, env, cwd) {
369
+ try {
370
+ return execFileSync(process.execPath, arguments_, {
371
+ cwd,
372
+ encoding: "utf8",
373
+ stdio: ["ignore", "pipe", "pipe"],
374
+ env,
375
+ timeout: 120000,
376
+ maxBuffer: MAX_NPM_OUTPUT_BYTES,
377
+ });
378
+ } catch (error) {
379
+ throw processFailure("import-probe", error);
380
+ }
381
+ }
382
+
383
+ function runBoundedSdkLifecycleProbe(arguments_, env, cwd) {
384
+ return new Promise((resolveProbe, rejectProbe) => {
385
+ let child;
386
+ let settled = false;
387
+ let outputBytes = 0;
388
+ const stdout = [];
389
+ const stderr = [];
390
+ let timeout;
391
+ let forceKill;
392
+ let closeGrace;
393
+ let stopFailure;
394
+ const lateErrorSink = () => {};
395
+ let onStdout;
396
+ let onStderr;
397
+ let onChildError;
398
+ let onClose;
399
+ const detachOperationalListeners = () => {
400
+ if (!child) return;
401
+ child.stdout?.removeListener("data", onStdout);
402
+ child.stderr?.removeListener("data", onStderr);
403
+ child.removeListener("error", onChildError);
404
+ child.removeListener("close", onClose);
405
+ };
406
+ const releaseUnconfirmedClose = () => {
407
+ if (!child) return;
408
+ detachOperationalListeners();
409
+ const removeLateGuards = () => {
410
+ child?.removeListener("error", lateErrorSink);
411
+ child?.stdout?.removeListener("error", lateErrorSink);
412
+ child?.stderr?.removeListener("error", lateErrorSink);
413
+ };
414
+ child.once("close", removeLateGuards);
415
+ child.on("error", lateErrorSink);
416
+ child.stdout?.on("error", lateErrorSink);
417
+ child.stderr?.on("error", lateErrorSink);
418
+ try { child.stdin?.destroy(); } catch { /* The process is already in bounded forced cleanup. */ }
419
+ try { child.stdout?.destroy(); } catch { /* The process is already in bounded forced cleanup. */ }
420
+ try { child.stderr?.destroy(); } catch { /* The process is already in bounded forced cleanup. */ }
421
+ try { child.unref(); } catch { /* The final receipt reports that close was unconfirmed. */ }
422
+ };
423
+ const finish = (error, result) => {
424
+ if (settled) return;
425
+ settled = true;
426
+ if (timeout) clearTimeout(timeout);
427
+ if (forceKill) clearTimeout(forceKill);
428
+ if (closeGrace) clearTimeout(closeGrace);
429
+ if (error instanceof SdkLifecycleFailure && error.code === "unconfirmed-close") releaseUnconfirmedClose();
430
+ else detachOperationalListeners();
431
+ if (error) rejectProbe(error);
432
+ else resolveProbe(result);
433
+ };
434
+ const requestStop = (failure) => {
435
+ if (stopFailure !== undefined || settled) return;
436
+ stopFailure = failure;
437
+ try { child?.kill(); } catch { /* The close grace below reports an unconfirmed close honestly. */ }
438
+ forceKill = setTimeout(() => { try { child?.kill("SIGKILL"); } catch { /* The final grace reports an unconfirmed close honestly. */ } }, 2500);
439
+ closeGrace = setTimeout(() => finish(new SdkLifecycleFailure("lifecycle-probe", "unconfirmed-close")), 5000);
440
+ };
441
+ try {
442
+ child = spawn(process.execPath, arguments_, {
443
+ cwd,
444
+ env,
445
+ stdio: ["ignore", "pipe", "pipe"],
446
+ windowsHide: true,
447
+ });
448
+ } catch {
449
+ finish(new SdkLifecycleFailure("lifecycle-probe", "spawn-failed"));
450
+ return;
451
+ }
452
+ const collect = (chunk, destination) => {
453
+ outputBytes += Buffer.byteLength(chunk);
454
+ if (outputBytes > MAX_NPM_OUTPUT_BYTES) requestStop(new SdkLifecycleFailure("lifecycle-probe", "output-limit"));
455
+ else if (destination !== undefined) destination.push(Buffer.from(chunk));
456
+ };
457
+ onStdout = (chunk) => collect(chunk, stdout);
458
+ onStderr = (chunk) => collect(chunk, stderr);
459
+ onChildError = () => requestStop(new SdkLifecycleFailure("lifecycle-probe", "spawn-failed"));
460
+ onClose = (status) => {
461
+ if (settled) return;
462
+ if (stopFailure !== undefined) {
463
+ finish(stopFailure);
464
+ return;
465
+ }
466
+ finish(undefined, { status, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) });
467
+ };
468
+ child.stdout.on("data", onStdout);
469
+ child.stderr.on("data", onStderr);
470
+ child.once("error", onChildError);
471
+ child.once("close", onClose);
472
+ timeout = setTimeout(() => requestStop(new SdkLifecycleFailure("lifecycle-probe", "timed-out")), SDK_LIFECYCLE_PROBE_TIMEOUT_MS);
473
+ });
474
+ }
475
+
476
+ function runWindowsStartupTimingProbe(runtimeScript, env, cwd) {
477
+ return new Promise((resolveProbe) => {
478
+ const observation = { helperStartOutcome: "not-attempted", startElapsedMs: null, lastStartupMarker: null, cleanup: "not-attempted", physicalCloseObserved: false };
479
+ let child;
480
+ let startedAt;
481
+ let startTimer;
482
+ let cleanupTimer;
483
+ let forceCloseTimer;
484
+ let settled = false;
485
+ let startReplyObserved = false;
486
+ let markerOrdinal = 0;
487
+ let output = Buffer.alloc(0);
488
+ let outputBytes = 0;
489
+ let failure;
490
+ const clearTimers = () => {
491
+ if (startTimer) clearTimeout(startTimer);
492
+ if (cleanupTimer) clearTimeout(cleanupTimer);
493
+ if (forceCloseTimer) clearTimeout(forceCloseTimer);
494
+ };
495
+ const finish = () => {
496
+ if (settled) return;
497
+ settled = true;
498
+ clearTimers();
499
+ if (observation.cleanup === "close-unconfirmed") {
500
+ const lateErrorSink = () => {};
501
+ child?.on("error", lateErrorSink);
502
+ child?.stdin?.on("error", lateErrorSink);
503
+ child?.stdout?.on("error", lateErrorSink);
504
+ child?.stderr?.on("error", lateErrorSink);
505
+ try { child?.stdin?.destroy(); child?.stdout?.destroy(); child?.stderr?.destroy(); child?.unref(); } catch { /* The receipt records that physical closure was not observed. */ }
506
+ }
507
+ resolveProbe({ ...observation, failure });
508
+ };
509
+ const setFailure = (stage, code, startOutcome) => {
510
+ if (failure !== undefined) return;
511
+ if (!startReplyObserved && WINDOWS_STARTUP_TIMING_OUTCOMES.has(startOutcome)) observation.helperStartOutcome = startOutcome;
512
+ failure = { stage, code };
513
+ };
514
+ const beginBoundedCleanup = () => {
515
+ if (!child || observation.physicalCloseObserved || cleanupTimer || settled) return;
516
+ cleanupTimer = setTimeout(() => {
517
+ try { child.kill(); } catch { /* The final bounded grace records unconfirmed physical closure. */ }
518
+ forceCloseTimer = setTimeout(() => {
519
+ if (observation.physicalCloseObserved) return;
520
+ observation.cleanup = "close-unconfirmed";
521
+ setFailure("cleanup", "cleanup-unconfirmed", "unknown");
522
+ finish();
523
+ }, WINDOWS_STARTUP_TIMING_FORCE_CLOSE_GRACE_MS);
524
+ }, WINDOWS_STARTUP_TIMING_CLEANUP_GRACE_MS);
525
+ };
526
+ const failAndCleanup = (stage, code, startOutcome) => {
527
+ setFailure(stage, code, startOutcome);
528
+ beginBoundedCleanup();
529
+ };
530
+ const rejectOutput = () => failAndCleanup(startReplyObserved ? "helper-result" : "helper-start", "invalid-output", "invalid-output");
531
+ const observeControlLine = (rawLine) => {
532
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
533
+ if (line === '{"event":"startup-marker","marker":"script-entered"}') {
534
+ if (startReplyObserved || markerOrdinal !== 0) return rejectOutput();
535
+ markerOrdinal = 1; observation.lastStartupMarker = "script-entered"; return;
536
+ }
537
+ if (line === '{"event":"startup-marker","marker":"native-ready"}') {
538
+ if (startReplyObserved || markerOrdinal !== 1) return rejectOutput();
539
+ markerOrdinal = 2; observation.lastStartupMarker = "native-ready"; return;
540
+ }
541
+ if (startReplyObserved) return rejectOutput();
542
+ if (WINDOWS_STARTUP_TIMING_REJECTED_START_REPLY.test(line)) return failAndCleanup("helper-start", "rejected", "rejected");
543
+ if (!WINDOWS_STARTUP_TIMING_VALID_START_REPLIES.has(line) || markerOrdinal !== 2) return rejectOutput();
544
+ const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
545
+ if (!Number.isFinite(elapsedMs) || elapsedMs > WINDOWS_STARTUP_TIMING_BUDGET_MS) return failAndCleanup("helper-start", "timed-out", "timed-out");
546
+ startReplyObserved = true;
547
+ observation.helperStartOutcome = "valid-reply";
548
+ observation.startElapsedMs = elapsedMs;
549
+ if (startTimer) clearTimeout(startTimer);
550
+ // Markers are protocol progress only. This succeeds because the exact start reply is valid.
551
+ beginBoundedCleanup();
552
+ };
553
+ const onStdout = (chunk) => {
554
+ if (!Buffer.isBuffer(chunk) || settled) return failAndCleanup("helper-start", "stream-failed", "stream-failed");
555
+ outputBytes += chunk.length;
556
+ if (outputBytes > MAX_WINDOWS_STARTUP_TIMING_OUTPUT_BYTES) return rejectOutput();
557
+ const combined = output.length === 0 ? chunk : Buffer.concat([output, chunk]);
558
+ let offset = 0;
559
+ for (;;) {
560
+ const newline = combined.indexOf(10, offset);
561
+ if (newline < 0) {
562
+ output = combined.subarray(offset);
563
+ if (output.length > 16_384) rejectOutput();
564
+ return;
565
+ }
566
+ const line = combined.subarray(offset, newline);
567
+ offset = newline + 1;
568
+ if (line.length > 16_385 || (line.length >= 3 && line[0] === 0xef && line[1] === 0xbb && line[2] === 0xbf)) return rejectOutput();
569
+ try { observeControlLine(new TextDecoder("utf-8", { fatal: true }).decode(line)); } catch { return rejectOutput(); }
570
+ if (failure !== undefined) return;
571
+ }
572
+ };
573
+ const onClose = (status) => {
574
+ observation.physicalCloseObserved = true;
575
+ observation.cleanup = "close-observed";
576
+ if (output.length !== 0) setFailure(startReplyObserved ? "helper-result" : "helper-start", "invalid-output", "invalid-output");
577
+ if (!startReplyObserved) setFailure("helper-start", "exited", "exited");
578
+ else if (status !== 0) setFailure("helper-result", "nonzero-exit", "unknown");
579
+ finish();
580
+ };
581
+ startedAt = process.hrtime.bigint(); // The measurement begins immediately before the exact spawn invocation.
582
+ try {
583
+ child = spawn(WINDOWS_STARTUP_TIMING_POWERSHELL, ["-NoLogo", "-NoProfile", "-NonInteractive", "-File", runtimeScript], { cwd, env, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
584
+ } catch {
585
+ setFailure("helper-start", "spawn-failed", "spawn-failed");
586
+ finish();
587
+ return;
588
+ }
589
+ child.stdout.on("data", onStdout);
590
+ child.stdout.on("error", () => failAndCleanup(startReplyObserved ? "helper-result" : "helper-start", "stream-failed", "stream-failed"));
591
+ child.stdin.on("error", () => failAndCleanup(startReplyObserved ? "helper-result" : "helper-start", "write-failed", "write-failed"));
592
+ child.stderr.on("error", () => failAndCleanup(startReplyObserved ? "helper-result" : "helper-start", "stream-failed", "stream-failed"));
593
+ child.stderr.resume();
594
+ child.once("error", () => failAndCleanup(startReplyObserved ? "helper-result" : "helper-start", "stream-failed", "stream-failed"));
595
+ child.once("close", onClose);
596
+ startTimer = setTimeout(() => failAndCleanup("helper-start", "timed-out", "timed-out"), WINDOWS_STARTUP_TIMING_BUDGET_MS);
597
+ try { child.stdin.end(WINDOWS_STARTUP_TIMING_START_REQUEST, (error) => { if (error) failAndCleanup(startReplyObserved ? "helper-result" : "helper-start", "write-failed", "write-failed"); }); }
598
+ catch { failAndCleanup("helper-start", "write-failed", "write-failed"); }
599
+ });
600
+ }
601
+
602
+ async function testHookedPackedRunner() {
603
+ const temporary = mkdtempSync(join(tmpdir(), "gentle-pi-packed-runner-"));
604
+ const packDirectory = join(temporary, "pack");
605
+ const installDirectory = join(temporary, "install");
606
+ // Every child inherits only disposable Pi homes, never the operator's settings.
607
+ const agentHome = join(temporary, "agent");
608
+ const piAgentHome = join(temporary, "pi-agent");
609
+ const isolatedEnv = { ...process.env, GENTLE_PI_AGENT_HOME: agentHome, PI_CODING_AGENT_DIR: piAgentHome };
610
+ const runNpm = (arguments_, options) => runNpmWithEnv(arguments_, isolatedEnv, options);
611
+ try {
612
+ mkdirSync(packDirectory);
613
+ mkdirSync(installDirectory);
614
+ mkdirSync(agentHome);
615
+ mkdirSync(piAgentHome);
47
616
  const originalSettings = '{ "tuiMode": "regular", "theme": "packed-fixture" }\n';
48
617
  writeFileSync(join(agentHome, "settings.json"), originalSettings);
49
618
  const packed = JSON.parse(runNpm(["pack", "--ignore-scripts", "--json", "--pack-destination", packDirectory], {
@@ -107,7 +676,1086 @@ try {
107
676
  const decoded = decodeReviewCapabilitiesV2(capabilities, executableDigest);
108
677
  if (decoded.contract !== "gentle-ai.review-integration/v2" || decoded.packageVersion !== versions[0].name.slice(1)) throw new Error("package-local Gentle AI returned incompatible capabilities");
109
678
  const packageManifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
110
- process.stdout.write(`packed package E2E passed (gentle-pi ${packageManifest.version ?? "unknown"}; Gentle AI ${decoded.packageVersion ?? "unknown"})\n`);
679
+ process.stdout.write(`packed package E2E passed (gentle-pi ${packageManifest.version ?? "unknown"}; Gentle AI ${decoded.packageVersion ?? "unknown"})\n`);
680
+ } finally {
681
+ rmSync(temporary, { recursive: true, force: true });
682
+ }
683
+ }
684
+
685
+ function isolatedUnhookedEnvironment(temporary) {
686
+ const homes = join(temporary, "homes");
687
+ const home = join(homes, "home");
688
+ const agentHome = join(homes, "gentle-pi-agent");
689
+ const piAgentHome = join(homes, "pi-coding-agent");
690
+ const gentleConfigHome = join(homes, "gentle-pi-config");
691
+ const xdgConfigHome = join(homes, "xdg-config");
692
+ const xdgCacheHome = join(homes, "xdg-cache");
693
+ const xdgDataHome = join(homes, "xdg-data");
694
+ const appData = join(homes, "appdata");
695
+ const localAppData = join(homes, "local-appdata");
696
+ const npmCache = join(temporary, "npm-cache");
697
+ const npmUserConfig = join(temporary, "npm-userconfig");
698
+ const passthrough = ["PATH", "SystemRoot", "SYSTEMROOT", "WINDIR", "ComSpec", "PATHEXT", "OS", "CI", "NUMBER_OF_PROCESSORS", "PROCESSOR_ARCHITECTURE"];
699
+ const env = {};
700
+ for (const key of passthrough) if (typeof process.env[key] === "string") env[key] = process.env[key];
701
+ for (const directory of [homes, home, agentHome, piAgentHome, gentleConfigHome, xdgConfigHome, xdgCacheHome, xdgDataHome, appData, localAppData, npmCache]) mkdirSync(directory, { recursive: true });
702
+ Object.assign(env, {
703
+ HOME: home, USERPROFILE: home, APPDATA: appData, LOCALAPPDATA: localAppData,
704
+ XDG_CONFIG_HOME: xdgConfigHome, XDG_CACHE_HOME: xdgCacheHome, XDG_DATA_HOME: xdgDataHome,
705
+ TMPDIR: temporary, TMP: temporary, TEMP: temporary,
706
+ GENTLE_PI_AGENT_HOME: agentHome, GENTLE_PI_CONFIG_HOME: gentleConfigHome, PI_CODING_AGENT_DIR: piAgentHome,
707
+ NPM_CONFIG_CACHE: npmCache, npm_config_cache: npmCache, NPM_CONFIG_USERCONFIG: npmUserConfig, npm_config_userconfig: npmUserConfig,
708
+ NPM_CONFIG_TMP: temporary, npm_config_tmp: temporary, NPM_CONFIG_IGNORE_SCRIPTS: "true", npm_config_ignore_scripts: "true",
709
+ NPM_CONFIG_UPDATE_NOTIFIER: "false", npm_config_update_notifier: "false",
710
+ });
711
+ if (process.platform === "win32") {
712
+ env.HOMEDRIVE = home.slice(0, 2);
713
+ env.HOMEPATH = home.slice(2).replaceAll("/", "\\\\");
714
+ }
715
+ return {
716
+ env,
717
+ homes: [
718
+ { checkId: "home-empty", path: home },
719
+ { checkId: "gentle-pi-agent-empty", path: agentHome },
720
+ { checkId: "pi-coding-agent-empty", path: piAgentHome },
721
+ { checkId: "gentle-pi-config-empty", path: gentleConfigHome },
722
+ { checkId: "xdg-config-empty", path: xdgConfigHome },
723
+ { checkId: "xdg-cache-empty", path: xdgCacheHome },
724
+ { checkId: "xdg-data-empty", path: xdgDataHome },
725
+ { checkId: "appdata-empty", path: appData },
726
+ { checkId: "local-appdata-empty", path: localAppData },
727
+ ],
728
+ };
729
+ }
730
+
731
+ function isValidatedWindowsMachineRoot(value, leaf) {
732
+ if (typeof value !== "string" || /[\0-\x1f\x7f]/.test(value)) return false;
733
+ const match = new RegExp(`^[A-Za-z]:\\\\${leaf}$`, "i").exec(value);
734
+ return match?.[0] === value;
735
+ }
736
+
737
+ function readCaseInsensitiveEnvironmentValue(source, name) {
738
+ if (!source || typeof source !== "object") return undefined;
739
+ const matches = Object.entries(source).filter(([key]) => key.toLowerCase() === name.toLowerCase()).map(([, value]) => value);
740
+ if (matches.length === 0 || matches.some((value) => typeof value !== "string")) return undefined;
741
+ return matches.every((value) => value === matches[0]) ? matches[0] : undefined;
742
+ }
743
+
744
+ function sameWindowsCanonicalPath(actual, expected) {
745
+ return typeof actual === "string" && win32.normalize(actual).toLowerCase() === win32.normalize(expected).toLowerCase();
746
+ }
747
+
748
+ // This deliberately reads only the named machine roots. It never inherits PSModulePath
749
+ // or any profile-derived path, and it returns keys rather than values for receipts.
750
+ export function deriveWindowsStartupTimingPathDelta(source) {
751
+ const programFiles = readCaseInsensitiveEnvironmentValue(source, "ProgramFiles");
752
+ const programFilesX86 = readCaseInsensitiveEnvironmentValue(source, "ProgramFiles(x86)");
753
+ const programFilesW6432 = readCaseInsensitiveEnvironmentValue(source, "ProgramW6432");
754
+ const systemRoot = readCaseInsensitiveEnvironmentValue(source, "SystemRoot");
755
+ if (!isValidatedWindowsMachineRoot(programFiles, "Program Files")
756
+ || !isValidatedWindowsMachineRoot(programFilesX86, "Program Files \\(x86\\)")
757
+ || !isValidatedWindowsMachineRoot(programFilesW6432, "Program Files")
758
+ || !isValidatedWindowsMachineRoot(systemRoot, "Windows")) {
759
+ return Object.freeze({ ready: false, pathAdditionKeys: Object.freeze([]), values: Object.freeze({}) });
760
+ }
761
+ const values = Object.freeze({
762
+ ProgramFiles: programFiles,
763
+ "ProgramFiles(x86)": programFilesX86,
764
+ ProgramW6432: programFilesW6432,
765
+ CommonProgramFiles: win32.join(programFiles, "Common Files"),
766
+ "CommonProgramFiles(x86)": win32.join(programFilesX86, "Common Files"),
767
+ CommonProgramW6432: win32.join(programFilesW6432, "Common Files"),
768
+ PSModulePath: [
769
+ win32.join(programFiles, "WindowsPowerShell", "Modules"),
770
+ win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "Modules"),
771
+ ].join(";"),
772
+ });
773
+ return Object.freeze({ ready: true, pathAdditionKeys: WINDOWS_STARTUP_TIMING_WINDOWS_PATH_KEYS, values });
774
+ }
775
+
776
+ export function validateWindowsStartupTimingMachinePaths(delta, operations = { lstat: lstatSync, realpath: realpathSync.native }) {
777
+ if (!delta?.ready || delta.pathAdditionKeys !== WINDOWS_STARTUP_TIMING_WINDOWS_PATH_KEYS || Object.keys(delta.values).join("\0") !== WINDOWS_STARTUP_TIMING_WINDOWS_PATH_KEYS.join("\0")) return false;
778
+ const values = delta.values;
779
+ const modulePaths = typeof values.PSModulePath === "string" ? values.PSModulePath.split(";") : [];
780
+ const systemRoot = modulePaths.length === 2 ? win32.dirname(win32.dirname(win32.dirname(win32.dirname(modulePaths[1])))) : undefined;
781
+ const expected = [
782
+ values.ProgramFiles, values["ProgramFiles(x86)"], values.ProgramW6432, systemRoot,
783
+ values.CommonProgramFiles, values["CommonProgramFiles(x86)"], values.CommonProgramW6432, ...modulePaths,
784
+ ];
785
+ if (!isValidatedWindowsMachineRoot(values.ProgramFiles, "Program Files")
786
+ || !isValidatedWindowsMachineRoot(values["ProgramFiles(x86)"], "Program Files \\(x86\\)")
787
+ || !isValidatedWindowsMachineRoot(values.ProgramW6432, "Program Files")
788
+ || !isValidatedWindowsMachineRoot(systemRoot, "Windows")
789
+ || values.CommonProgramFiles !== win32.join(values.ProgramFiles, "Common Files")
790
+ || values["CommonProgramFiles(x86)"] !== win32.join(values["ProgramFiles(x86)"], "Common Files")
791
+ || values.CommonProgramW6432 !== win32.join(values.ProgramW6432, "Common Files")
792
+ || modulePaths[0] !== win32.join(values.ProgramFiles, "WindowsPowerShell", "Modules")
793
+ || modulePaths[1] !== win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "Modules")) return false;
794
+ try {
795
+ return expected.every((path) => {
796
+ const entry = operations.lstat(path);
797
+ return entry.isDirectory() && !entry.isSymbolicLink() && sameWindowsCanonicalPath(operations.realpath(path), path);
798
+ });
799
+ } catch { return false; }
800
+ }
801
+
802
+ function assertPackResult(packed, packDirectory, receipt) {
803
+ selectUnhookedCheck(receipt, "pack-metadata");
804
+ if (!Array.isArray(packed) || packed.length !== 1 || !packed[0] || typeof packed[0] !== "object") throw new Error("npm pack did not return exactly one package");
805
+ const entry = packed[0];
806
+ if (entry.name !== "gentle-pi" || typeof entry.filename !== "string" || entry.filename !== basename(entry.filename)) throw new Error("npm pack returned an unsafe package identity");
807
+ if (typeof entry.integrity !== "string" || !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(entry.integrity)) throw new Error("npm pack did not report a sha512 integrity");
808
+ const tarball = resolve(packDirectory, entry.filename);
809
+ if (!isWithin(packDirectory, tarball)) throw new Error("npm pack tarball escapes the owned pack directory");
810
+ selectUnhookedCheck(receipt, "pack-integrity");
811
+ assertOwnedRegularFile(packDirectory, entry.filename);
812
+ const actualIntegrity = `sha512-${createHash("sha512").update(readFileSync(tarball)).digest("base64")}`;
813
+ if (actualIntegrity !== entry.integrity) throw new Error("npm pack tarball integrity does not match its reported identity");
814
+ return { entry, tarball };
815
+ }
816
+
817
+ const HASHED_PACKED_ASSETS = [
818
+ { ownedCheckId: "asset-runtime-windows-session-transport-owned", hashCheckId: "asset-runtime-windows-session-transport-hash", relativePath: "runtime/windows-session-transport.ps1" },
819
+ { ownedCheckId: "asset-lib-windows-session-transport-owned", hashCheckId: "asset-lib-windows-session-transport-hash", relativePath: "lib/windows-session-transport.ts" },
820
+ { ownedCheckId: "asset-lib-agents-session-transport-owned", hashCheckId: "asset-lib-agents-session-transport-hash", relativePath: "lib/agents-session-transport.ts" },
821
+ { ownedCheckId: "asset-extension-gentle-agents-owned", hashCheckId: "asset-extension-gentle-agents-hash", relativePath: "extensions/gentle-agents.ts" },
822
+ { ownedCheckId: "asset-extension-gentle-ai-owned", hashCheckId: "asset-extension-gentle-ai-hash", relativePath: "extensions/gentle-ai.ts" },
823
+ { ownedCheckId: "asset-native-review-cli-owned", hashCheckId: "asset-native-review-cli-hash", relativePath: "runtime/native-review-cli.mjs" },
824
+ { ownedCheckId: "asset-review-integration-v2-owned", hashCheckId: "asset-review-integration-v2-hash", relativePath: "runtime/review-integration-v2.mjs" },
825
+ { ownedCheckId: "asset-installer-gentle-ai-owned", hashCheckId: "asset-installer-gentle-ai-hash", relativePath: "scripts/install-gentle-ai.mjs" },
826
+ { ownedCheckId: "asset-installer-tui-mode-setting-owned", hashCheckId: "asset-installer-tui-mode-setting-hash", relativePath: "scripts/install-tui-mode-setting.mjs" },
827
+ ];
828
+
829
+ function assertPackedAssets(packageRoot, receipt) {
830
+ for (const asset of HASHED_PACKED_ASSETS) {
831
+ selectUnhookedCheck(receipt, asset.ownedCheckId);
832
+ const installedPath = assertOwnedRegularFile(packageRoot, asset.relativePath);
833
+ selectUnhookedCheck(receipt, asset.hashCheckId);
834
+ const source = readFileSync(join(root, asset.relativePath));
835
+ const installed = readFileSync(installedPath);
836
+ if (createHash("sha256").update(source).digest("hex") !== createHash("sha256").update(installed).digest("hex")) throw new Error("packed asset bytes differ from this checkout");
837
+ }
838
+ }
839
+
840
+ function assertNoNativeInstallerArtifacts(packageRoot, consumerDirectory, receipt) {
841
+ selectUnhookedCheck(receipt, "native-package-cache-absent");
842
+ if (existsSync(join(packageRoot, ".gentle-ai"))) throw new Error("unhooked install unexpectedly contains a native Gentle AI artifact");
843
+ selectUnhookedCheck(receipt, "native-command-absent");
844
+ const nativeCommand = process.platform === "win32" ? "gentle-ai.cmd" : "gentle-ai";
845
+ if (existsSync(join(consumerDirectory, "node_modules", ".bin", nativeCommand))) throw new Error("unhooked install unexpectedly exposed a native Gentle AI executable");
846
+ }
847
+
848
+ function resolveInstalledJitiStaticEntry(consumerDirectory, sdkPackageJson, sdkVersion, selectCheck, checkIds) {
849
+ selectCheck(checkIds.sdkManifest);
850
+ const checkedSdkPackageJson = assertOwnedRegularFile(consumerDirectory, relative(consumerDirectory, sdkPackageJson));
851
+ const sdkRequire = createRequire(checkedSdkPackageJson);
852
+ selectCheck(checkIds.sdkVersion);
853
+ const installedSdk = safeJson(readFileSync(checkedSdkPackageJson), "installed Pi SDK manifest");
854
+ if (installedSdk.name !== "@earendil-works/pi-coding-agent" || installedSdk.version !== sdkVersion) throw new Error("consumer resolved an unexpected Pi SDK");
855
+ const declaredJiti = installedSdk?.dependencies?.jiti;
856
+ selectCheck(checkIds.jitiManifest);
857
+ const jitiManifest = sdkRequire.resolve("jiti/package.json");
858
+ const checkedJitiPackageJson = assertOwnedRegularFile(consumerDirectory, relative(consumerDirectory, jitiManifest));
859
+ const jitiPackage = safeJson(readFileSync(checkedJitiPackageJson), "jiti package manifest");
860
+ selectCheck(checkIds.jitiStaticExport);
861
+ const staticExport = jitiPackage?.exports?.["./static"];
862
+ if (staticExport === null || typeof staticExport !== "object" || Array.isArray(staticExport)
863
+ || Object.keys(staticExport).length !== 2 || typeof staticExport.types !== "string" || typeof staticExport.import !== "string") {
864
+ throw new Error("Jiti manifest does not declare the expected static ESM export");
865
+ }
866
+ const jitiPackageRoot = dirname(checkedJitiPackageJson);
867
+ selectCheck(checkIds.jitiEntry);
868
+ const jitiStaticEntry = assertOwnedRegularFile(jitiPackageRoot, relative(jitiPackageRoot, resolve(jitiPackageRoot, staticExport.import)));
869
+ selectCheck(checkIds.jitiVersion);
870
+ if (jitiPackage.name !== "jiti" || typeof declaredJiti !== "string" || jitiPackage.version !== declaredJiti) throw new Error("consumer Jiti does not match the installed Pi SDK runtime dependency");
871
+ return jitiStaticEntry;
872
+ }
873
+
874
+ function unhookedProbeSource(packageRoot, consumerPackageJson, jitiStaticEntry) {
875
+ return `
876
+ import assert from "node:assert/strict";
877
+ import { pathToFileURL } from "node:url";
878
+ const { createJiti } = await import(pathToFileURL(${JSON.stringify(jitiStaticEntry)}).href);
879
+ const jiti = createJiti(pathToFileURL(${JSON.stringify(consumerPackageJson)}).href, { moduleCache: false });
880
+ const registrations = { tools: [], commands: [], events: [] };
881
+ const events = new Proxy({}, { get(_target, property) { return (..._args) => { registrations.events.push(\`events.\${String(property)}\`); }; } });
882
+ const pi = {
883
+ events,
884
+ on(name, _handler) { registrations.events.push(String(name)); },
885
+ registerTool(definition) { registrations.tools.push(String(definition.name)); },
886
+ registerCommand(name, _definition) { registrations.commands.push(String(name)); },
887
+ registerShortcut() {}, registerMessageRenderer() {}, registerEntryRenderer() {}, registerMarkdownTransformer() {},
888
+ };
889
+ async function register(relativePath) {
890
+ const loaded = await jiti.import(new URL(relativePath, pathToFileURL(${JSON.stringify(`${packageRoot}/`)}).href).href, { default: true });
891
+ const factory = typeof loaded === "function" ? loaded : loaded?.default;
892
+ assert.equal(typeof factory, "function", \`missing default extension factory: \${relativePath}\`);
893
+ await factory(pi);
894
+ }
895
+ await register("extensions/gentle-agents.ts");
896
+ await register("extensions/gentle-ai.ts");
897
+ for (const name of ["subagent_list_agents", "subagent_run", "orchestrator_session_id", "orchestrator_list", "orchestrator_send_message", "gentle_review", "gentle_review_capture", "gentle_review_capture_group", "gentle_review_scope"]) assert.ok(registrations.tools.includes(name), \`missing registered tool: \${name}\`);
898
+ for (const name of ["gentle:agents", "gentle:status", "gentle:review-mode"]) assert.ok(registrations.commands.includes(name), \`missing registered command: \${name}\`);
899
+ assert.ok(registrations.events.includes("session_start"), "expected session_start registration");
900
+ assert.ok(registrations.events.includes("session_shutdown"), "expected session_shutdown registration");
901
+ process.stdout.write(JSON.stringify({ tools: registrations.tools.sort(), commands: registrations.commands.sort(), loader: "Jiti from @earendil-works/pi-coding-agent dependency" }));
902
+ `;
903
+ }
904
+
905
+ function isSdkLifecycleChildStageCheck(stage, checkId) {
906
+ const stageChecks = {
907
+ "bootstrap": ["bootstrap-builtins", "bootstrap-agent-home", "bootstrap-directories"],
908
+ "sdk-load": ["sdk-import", "sdk-exports"],
909
+ "jiti-load": ["jiti-import"],
910
+ "agents-module-load": ["agents-module-import", "agents-module-export"],
911
+ "model-runtime": ["model-runtime-create"],
912
+ "services": ["settings-untrusted", "settings-default-provider", "settings-default-model", "services-create", "services-settings-manager", "services-project-trusted"],
913
+ "extensions-validate": ["extensions-errors", "extensions-paths", "extensions-hooks", "services-diagnostics", "ambient-skills", "ambient-prompts", "ambient-themes", "ambient-context-files"],
914
+ "model-availability": ["model-availability-empty"],
915
+ "session-create": ["session-create", "session-model-unbound"],
916
+ "session-bind": ["session-model-unbound", "session-model-invocation-guard", "session-bind", "session-bind-extension-errors", "session-model-bound", "session-ids-distinct"],
917
+ "presence-two": ["presence-observer-create", "presence-two-records", "presence-first-model-unbound", "presence-second-model-unbound", "presence-two-extension-errors"],
918
+ "dispose-first": ["dispose-first", "dispose-first-extension-errors"],
919
+ "presence-one": ["presence-one-record", "presence-one-model-unbound", "presence-one-extension-errors"],
920
+ "dispose-second": ["dispose-second", "dispose-second-extension-errors"],
921
+ "presence-none": ["presence-no-records", "presence-none-extension-errors"],
922
+ "cleanup": ["cleanup-runtime", "cleanup-observer", "cleanup-extension-errors"],
923
+ };
924
+ return SDK_LIFECYCLE_CHILD_STAGES.has(stage) && SDK_LIFECYCLE_CHILD_CHECK_IDS.has(checkId) && stageChecks[stage].includes(checkId);
925
+ }
926
+
927
+ function parseWindowsRegistryObservation(value) {
928
+ if (value === null) return null;
929
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) return undefined;
930
+ const keys = ["availability", "provenance", "restoration", "startCalls", "initializeCalls", "cleanupCalls", "firstFailurePhase", "firstFailureClass", "firstFailureCode", "lastStartupMarker"];
931
+ if (Object.keys(value).length !== keys.length || keys.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) return undefined;
932
+ const observation = value;
933
+ if (observation.lastStartupMarker !== null && !SDK_LIFECYCLE_WINDOWS_STARTUP_MARKERS.has(observation.lastStartupMarker)) return undefined;
934
+ if (!SDK_LIFECYCLE_WINDOWS_OBSERVATION_AVAILABILITY.has(observation.availability) || !SDK_LIFECYCLE_WINDOWS_OBSERVATION_PROVENANCE.has(observation.provenance) || !SDK_LIFECYCLE_WINDOWS_OBSERVATION_RESTORATION.has(observation.restoration)) return undefined;
935
+ const nullDetails = ["startCalls", "initializeCalls", "cleanupCalls", "firstFailurePhase", "firstFailureClass", "firstFailureCode", "lastStartupMarker"];
936
+ if (observation.availability !== "observed") {
937
+ if ((observation.availability === "not-applicable" && (observation.provenance !== "not-applicable" || observation.restoration !== "not-applicable")) || (observation.availability === "unavailable" && (!["unavailable", "ambiguous"].includes(observation.provenance) || !["not-required", "not-attempted"].includes(observation.restoration))) || nullDetails.some((key) => observation[key] !== null)) return undefined;
938
+ return Object.freeze({ ...observation });
939
+ }
940
+ if (observation.provenance !== "owned-instance" || observation.restoration !== "not-required") return undefined;
941
+ for (const key of ["startCalls", "initializeCalls", "cleanupCalls"]) if (!Number.isInteger(observation[key]) || observation[key] < 0 || observation[key] > 1) return undefined;
942
+ if (observation.startCalls !== 1 || observation.cleanupCalls !== 1 || (observation.initializeCalls !== 0 && observation.initializeCalls !== 1)) return undefined;
943
+ const failureFields = [observation.firstFailurePhase, observation.firstFailureClass, observation.firstFailureCode];
944
+ if (failureFields.every((field) => field === null)) return observation.initializeCalls === 1 ? Object.freeze({ ...observation }) : undefined;
945
+ if (!SDK_LIFECYCLE_WINDOWS_OBSERVATION_PHASES.has(observation.firstFailurePhase) || !SDK_LIFECYCLE_WINDOWS_OBSERVATION_FAILURE_CLASSES.has(observation.firstFailureClass) || !SDK_LIFECYCLE_WINDOWS_OBSERVATION_ERROR_CODES.has(observation.firstFailureCode) || ((observation.firstFailureClass === "timed-out") !== (observation.firstFailureCode === "deadline"))) return undefined;
946
+ return Object.freeze({ ...observation });
947
+ }
948
+
949
+ function parseSdkLifecycleChildReceipt(stdout, stderr) {
950
+ if (!Buffer.isBuffer(stdout) || !Buffer.isBuffer(stderr) || stdout.length === 0 || stdout.length > MAX_SDK_LIFECYCLE_CHILD_REPORT_BYTES || stderr.length !== 0) return undefined;
951
+ let parsed;
952
+ try { parsed = JSON.parse(stdout.toString("utf8")); } catch { return undefined; }
953
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Object.getPrototypeOf(parsed) !== Object.prototype) return undefined;
954
+ const has = (key) => Object.prototype.hasOwnProperty.call(parsed, key);
955
+ const progressKeys = ["sdkLoaded", "extensionErrorCount", "extensionErrorPhase", "modelInvocationCount", "agentStartEventCount", "turnStartEventCount", "sessionsStarted", "presenceAfterStart", "presenceAfterFirstDispose", "presenceAfterSecondDispose", "disposedSessions", "windowsRegistryObservation"];
956
+ const baseKeys = ["status", "stage", "checkId", "cleanupStatus"];
957
+ if (!has("status") || !has("stage") || !has("checkId") || !has("cleanupStatus") || !SDK_LIFECYCLE_CHILD_STAGES.has(parsed.stage) || !SDK_LIFECYCLE_CHILD_CHECK_IDS.has(parsed.checkId) || !isSdkLifecycleChildStageCheck(parsed.stage, parsed.checkId) || !SDK_LIFECYCLE_CHILD_CLEANUP_STATUSES.has(parsed.cleanupStatus)) return undefined;
958
+ const allowed = new Set([...baseKeys, ...progressKeys, "code"]);
959
+ if (Object.keys(parsed).some((key) => !allowed.has(key))) return undefined;
960
+ if (parsed.status !== "complete" && parsed.status !== "failed") return undefined;
961
+ if (parsed.status === "complete") {
962
+ const completeKeys = new Set([...baseKeys, ...progressKeys]);
963
+ if (Object.keys(parsed).length !== completeKeys.size || Object.keys(parsed).some((key) => !completeKeys.has(key)) || parsed.stage !== "presence-none" || parsed.checkId !== "presence-none-extension-errors" || parsed.cleanupStatus !== "complete") return undefined;
964
+ } else if (!has("code") || !SDK_LIFECYCLE_CHILD_ERROR_CODES.has(parsed.code)) return undefined;
965
+ if (has("sdkLoaded") && parsed.sdkLoaded !== true) return undefined;
966
+ const extensionObserved = has("extensionErrorCount") || has("extensionErrorPhase");
967
+ if (extensionObserved && (!has("extensionErrorCount") || !has("extensionErrorPhase") || !Number.isInteger(parsed.extensionErrorCount) || parsed.extensionErrorCount < 0 || parsed.extensionErrorCount > 2 || !SDK_LIFECYCLE_EXTENSION_ERROR_PHASES.has(parsed.extensionErrorPhase) || parsed.extensionErrorPhase === "unobserved")) return undefined;
968
+ const invocationObserved = has("modelInvocationCount") || has("agentStartEventCount") || has("turnStartEventCount");
969
+ if (invocationObserved && (!has("modelInvocationCount") || !has("agentStartEventCount") || !has("turnStartEventCount") || !Number.isInteger(parsed.modelInvocationCount) || !Number.isInteger(parsed.agentStartEventCount) || !Number.isInteger(parsed.turnStartEventCount) || parsed.modelInvocationCount < 0 || parsed.modelInvocationCount > 2 || parsed.agentStartEventCount < 0 || parsed.agentStartEventCount > 2 || parsed.turnStartEventCount < 0 || parsed.turnStartEventCount > 2)) return undefined;
970
+ if (parsed.status === "failed" && parsed.code === "forbidden-model-invocation" && (!invocationObserved || parsed.modelInvocationCount === 0)) return undefined;
971
+ const windowsRegistryObservation = has("windowsRegistryObservation") ? parseWindowsRegistryObservation(parsed.windowsRegistryObservation) : undefined;
972
+ if (has("windowsRegistryObservation") && windowsRegistryObservation === undefined) return undefined;
973
+ if (parsed.status === "complete" && (windowsRegistryObservation === undefined || windowsRegistryObservation === null || !((windowsRegistryObservation.availability === "not-applicable" && windowsRegistryObservation.restoration === "not-applicable") || (windowsRegistryObservation.availability === "observed" && windowsRegistryObservation.provenance === "owned-instance" && windowsRegistryObservation.restoration === "not-required" && windowsRegistryObservation.startCalls === 1 && windowsRegistryObservation.initializeCalls === 1 && windowsRegistryObservation.cleanupCalls === 1 && windowsRegistryObservation.firstFailurePhase === null && windowsRegistryObservation.firstFailureClass === null && windowsRegistryObservation.firstFailureCode === null && windowsRegistryObservation.lastStartupMarker === "native-ready")))) return undefined;
974
+ for (const key of ["sessionsStarted", "presenceAfterStart", "presenceAfterFirstDispose", "presenceAfterSecondDispose", "disposedSessions"]) {
975
+ if (has(key) && (!Number.isInteger(parsed[key]) || parsed[key] < 0 || parsed[key] > 2)) return undefined;
976
+ }
977
+ if (parsed.status === "complete" && (parsed.sdkLoaded !== true || parsed.extensionErrorCount !== 0 || parsed.extensionErrorPhase !== "none" || parsed.modelInvocationCount !== 0 || parsed.agentStartEventCount !== 0 || parsed.turnStartEventCount !== 0 || parsed.sessionsStarted !== 2 || parsed.presenceAfterStart !== 2 || parsed.presenceAfterFirstDispose !== 1 || parsed.presenceAfterSecondDispose !== 0 || parsed.disposedSessions !== 2)) return undefined;
978
+ const receipt = { status: parsed.status, stage: parsed.stage, checkId: parsed.checkId, cleanupStatus: parsed.cleanupStatus };
979
+ if (parsed.status === "failed") receipt.childFailureCode = parsed.code;
980
+ if (has("sdkLoaded")) receipt.sdkLoaded = true;
981
+ if (extensionObserved) {
982
+ receipt.extensionErrorCount = parsed.extensionErrorCount;
983
+ receipt.extensionErrorPhase = parsed.extensionErrorPhase;
984
+ }
985
+ if (invocationObserved) {
986
+ receipt.modelInvocationCount = parsed.modelInvocationCount;
987
+ receipt.agentStartEventCount = parsed.agentStartEventCount;
988
+ receipt.turnStartEventCount = parsed.turnStartEventCount;
989
+ }
990
+ for (const key of ["sessionsStarted", "presenceAfterStart", "presenceAfterFirstDispose", "presenceAfterSecondDispose", "disposedSessions"]) if (has(key)) receipt[key] = parsed[key];
991
+ if (has("windowsRegistryObservation")) receipt.windowsRegistryObservation = windowsRegistryObservation;
992
+ return receipt;
993
+ }
994
+
995
+ function mergeSdkLifecycleChildProgress(receipt, childReceipt) {
996
+ receipt.childReceiptObserved = true;
997
+ if (childReceipt.sdkLoaded !== undefined) receipt.sdkLoaded = childReceipt.sdkLoaded;
998
+ if (childReceipt.extensionErrorCount !== undefined) {
999
+ receipt.extensionErrorCount = childReceipt.extensionErrorCount;
1000
+ receipt.extensionErrorPhase = childReceipt.extensionErrorPhase;
1001
+ }
1002
+ if (childReceipt.modelInvocationCount !== undefined) receipt.modelInvocationCount = childReceipt.modelInvocationCount;
1003
+ if (childReceipt.agentStartEventCount !== undefined) receipt.agentStartEventCount = childReceipt.agentStartEventCount;
1004
+ if (childReceipt.turnStartEventCount !== undefined) receipt.turnStartEventCount = childReceipt.turnStartEventCount;
1005
+ if (childReceipt.sessionsStarted !== undefined) receipt.sessionsStarted = childReceipt.sessionsStarted;
1006
+ if (childReceipt.presenceAfterStart !== undefined) receipt.presenceAfterStart = childReceipt.presenceAfterStart;
1007
+ if (childReceipt.presenceAfterFirstDispose !== undefined) receipt.presenceAfterFirstDispose = childReceipt.presenceAfterFirstDispose;
1008
+ if (childReceipt.presenceAfterSecondDispose !== undefined) receipt.presenceAfterSecondDispose = childReceipt.presenceAfterSecondDispose;
1009
+ if (childReceipt.disposedSessions !== undefined) receipt.disposedSessions = childReceipt.disposedSessions;
1010
+ if (childReceipt.windowsRegistryObservation !== undefined) receipt.windowsRegistryObservation = childReceipt.windowsRegistryObservation;
1011
+ }
1012
+
1013
+ function sdkLifecycleProbeSource(packageRoot, consumerPackageJson, jitiStaticEntry) {
1014
+ return `
1015
+ // One bind permits default start (30s), initialize (2s), and listen (2s); its
1016
+ // remaining 6s covers SDK lifecycle/error observation. Listen owns publication.
1017
+ const SDK_LIFECYCLE_WINDOWS_BIND_DEADLINE_MS = 30_000 + 2_000 + 2_000 + 6_000;
1018
+ // A registry failure may need default start/initialize, shutdown (2s), two 500ms
1019
+ // cleanup graces, and phase-event propagation before this verification watchdog expires.
1020
+ const SDK_LIFECYCLE_WINDOWS_REGISTRY_DEADLINE_MS = 30_000 + 2_000 + 2_000 + 2 * 500 + 5_000;
1021
+ const SDK_LIFECYCLE_WINDOWS_LIST_DEADLINE_MS = 2_000;
1022
+ const progress = {};
1023
+ let childStage = "bootstrap";
1024
+ let childCheckId = "bootstrap-builtins";
1025
+ let primaryFailure;
1026
+ let cleanupFailure;
1027
+ let completedCheckpoint;
1028
+ let cleanupStatus = "complete";
1029
+ let firstRuntime;
1030
+ let secondRuntime;
1031
+ let firstInvocationGuard;
1032
+ let secondInvocationGuard;
1033
+ let observer;
1034
+ let windowsRegistryObservation;
1035
+ let extensionErrorPhase = "startup";
1036
+ let extensionErrorCount = 0;
1037
+ let extensionErrorsActive = true;
1038
+ let extensionLifecycleObserved = false;
1039
+ let assert;
1040
+ let mkdirSync;
1041
+ let join;
1042
+ let pathToFileURL;
1043
+ let settingsManager;
1044
+ const checkpoint = (stage, checkId) => { childStage = stage; childCheckId = checkId; };
1045
+ const recordCleanupFailure = (checkId) => {
1046
+ cleanupStatus = "failed";
1047
+ if (cleanupFailure === undefined) cleanupFailure = { stage: "cleanup", checkId, code: "cleanup-failed" };
1048
+ };
1049
+ const FORBIDDEN_MODEL_INVOCATION = "forbidden-model-invocation";
1050
+ const OBSERVATION_INVALID = "observation-invalid";
1051
+ const failureCode = (error, fallback = "assertion-failed") => error?.childReceiptCode === FORBIDDEN_MODEL_INVOCATION ? FORBIDDEN_MODEL_INVOCATION : error?.childReceiptCode === OBSERVATION_INVALID ? OBSERVATION_INVALID : error?.childReceiptCode === "timed-out" ? "timed-out" : error?.childReceiptCode === "load-failed" ? "load-failed" : fallback;
1052
+ const load = async (operation) => {
1053
+ try { return await operation; } catch { throw { childReceiptCode: "load-failed" }; }
1054
+ };
1055
+ const within = async (operation, milliseconds) => {
1056
+ let timer;
1057
+ try {
1058
+ return await Promise.race([
1059
+ operation,
1060
+ new Promise((_, reject) => { timer = setTimeout(() => reject({ childReceiptCode: "timed-out" }), milliseconds); }),
1061
+ ]);
1062
+ } finally {
1063
+ if (timer) clearTimeout(timer);
1064
+ }
1065
+ };
1066
+ const newWindowsRegistryObservation = (availability, provenance, restoration) => ({ availability, provenance, restoration, startCalls: null, initializeCalls: null, cleanupCalls: null, firstFailurePhase: null, firstFailureClass: null, firstFailureCode: null, lastStartupMarker: null });
1067
+ const createWindowsRegistryObservation = (PhaseSequence) => {
1068
+ if (process.platform === "win32") return new PhaseSequence();
1069
+ return { observe() { return undefined; }, startupSucceeded: true, cleanupComplete: true, admitsFullSuccess: true, snapshot: () => newWindowsRegistryObservation("not-applicable", "not-applicable", "not-applicable") };
1070
+ };
1071
+ const recordExtensionError = () => {
1072
+ if (!extensionErrorsActive) return;
1073
+ extensionLifecycleObserved = true;
1074
+ extensionErrorCount = Math.min(2, extensionErrorCount + 1);
1075
+ progress.extensionErrorCount = extensionErrorCount;
1076
+ progress.extensionErrorPhase = extensionErrorPhase;
1077
+ };
1078
+ const assertNoExtensionErrors = () => assert.equal(extensionErrorCount, 0, "packaged extension lifecycle error");
1079
+ const confirmNoExtensionErrors = () => {
1080
+ assertNoExtensionErrors();
1081
+ if (!extensionLifecycleObserved) return;
1082
+ progress.extensionErrorCount = 0;
1083
+ progress.extensionErrorPhase = "none";
1084
+ };
1085
+ const disposeRuntime = async (runtime, settingsManager, invocationGuard, disposeStage, runtimeCheckId, extensionCheckId) => {
1086
+ if (!runtime) return;
1087
+ const stableDisposeStage = disposeStage;
1088
+ const stableRuntimeCheckId = runtimeCheckId;
1089
+ const stableExtensionCheckId = extensionCheckId;
1090
+ checkpoint(stableDisposeStage, stableRuntimeCheckId);
1091
+ extensionErrorPhase = "shutdown";
1092
+ let completedDisposal = false;
1093
+ try {
1094
+ assertNoConfiguredModel(runtime, settingsManager, "session must remain unconfigured through disposal");
1095
+ try {
1096
+ await within(runtime.dispose(), 15000);
1097
+ } catch (error) {
1098
+ invocationGuard.assertNotInvoked("session-bind", "session-model-invocation-guard");
1099
+ throw error;
1100
+ }
1101
+ checkpoint(stableDisposeStage, stableExtensionCheckId);
1102
+ confirmNoExtensionErrors();
1103
+ invocationGuard.assertNotInvoked("session-bind", "session-model-invocation-guard");
1104
+ invocationGuard.detachAfterSuccessfulDispose();
1105
+ progress.disposedSessions = (progress.disposedSessions ?? 0) + 1;
1106
+ completedDisposal = true;
1107
+ } finally {
1108
+ if (!completedDisposal) { /* Keep the deny stream guard and observer attached until process exit. */ }
1109
+ }
1110
+ };
1111
+ const INERT_DEFAULT_MODEL = {
1112
+ id: "unknown",
1113
+ name: "unknown",
1114
+ api: "unknown",
1115
+ provider: "unknown",
1116
+ baseUrl: "",
1117
+ reasoning: false,
1118
+ input: [],
1119
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1120
+ contextWindow: 0,
1121
+ maxTokens: 0,
1122
+ };
1123
+ const assertNeutralInitializedSessionHistory = (runtime, description) => {
1124
+ // getEntries excludes the SDK session header; a new session adds neutral initialization metadata after the pre-creation no-restored-history assertion.
1125
+ const entries = runtime.session.sessionManager.getEntries();
1126
+ assert.equal(entries.length, 1, description + ": session history after creation must contain only neutral SDK initialization metadata");
1127
+ const [entry] = entries;
1128
+ assert.equal(entry.type, "thinking_level_change", description + ": session history after creation must not contain restored messages, model changes, or custom entries");
1129
+ assert.equal(entry.thinkingLevel, "off", description + ": neutral SDK initialization metadata must set thinking level off");
1130
+ const context = runtime.session.sessionManager.buildSessionContext();
1131
+ assert.deepEqual(context.messages, [], description + ": neutral SDK initialization metadata must not add context messages");
1132
+ assert.equal(context.model, null, description + ": neutral SDK initialization metadata must not select a model");
1133
+ };
1134
+ const assertNoConfiguredModel = (runtime, settingsManager, description) => {
1135
+ assertNeutralInitializedSessionHistory(runtime, description);
1136
+ assert.equal(settingsManager.getDefaultProvider(), undefined, description + ": in-memory settings must have no default provider");
1137
+ assert.equal(settingsManager.getDefaultModel(), undefined, description + ": in-memory settings must have no default model");
1138
+ assert.equal(runtime.services.modelRuntime.getAvailableSnapshot().length, 0, description + ": owned empty auth and model paths must expose no available models");
1139
+ assert.deepEqual(runtime.session.model, INERT_DEFAULT_MODEL, description + ": session must expose the SDK inert no-configured-model sentinel");
1140
+ };
1141
+ const installForbiddenModelInvocationGuard = (runtime) => {
1142
+ const agent = runtime.session.agent;
1143
+ if (progress.modelInvocationCount === undefined) progress.modelInvocationCount = 0;
1144
+ if (progress.agentStartEventCount === undefined) progress.agentStartEventCount = 0;
1145
+ if (progress.turnStartEventCount === undefined) progress.turnStartEventCount = 0;
1146
+ const guardedStreamFunction = (..._args) => {
1147
+ progress.modelInvocationCount = Math.min(2, progress.modelInvocationCount + 1);
1148
+ throw { childReceiptCode: FORBIDDEN_MODEL_INVOCATION };
1149
+ };
1150
+ const unsubscribe = agent.subscribe((event) => {
1151
+ if (event.type === "agent_start") progress.agentStartEventCount = Math.min(2, progress.agentStartEventCount + 1);
1152
+ if (event.type === "turn_start") progress.turnStartEventCount = Math.min(2, progress.turnStartEventCount + 1);
1153
+ });
1154
+ agent.streamFunction = guardedStreamFunction;
1155
+ return {
1156
+ assertNotInvoked: (stage, checkId) => {
1157
+ if (progress.modelInvocationCount !== 0) {
1158
+ checkpoint(stage, checkId);
1159
+ throw { childReceiptCode: FORBIDDEN_MODEL_INVOCATION };
1160
+ }
1161
+ if (progress.agentStartEventCount !== 0 || progress.turnStartEventCount !== 0) {
1162
+ checkpoint(stage, checkId);
1163
+ throw { childReceiptCode: "assertion-failed" };
1164
+ }
1165
+ },
1166
+ detachAfterSuccessfulDispose: () => {
1167
+ unsubscribe();
1168
+ },
1169
+ };
1170
+ };
1171
+ const bindRuntime = async (runtime, settingsManager, invocationGuard) => {
1172
+ checkpoint("session-bind", "session-model-unbound");
1173
+ assertNoConfiguredModel(runtime, settingsManager, "session must not configure a model before binding");
1174
+ checkpoint("session-bind", "session-bind");
1175
+ extensionErrorPhase = "startup";
1176
+ extensionLifecycleObserved = true;
1177
+ await within(runtime.session.bindExtensions({ mode: "json", onError: recordExtensionError }), SDK_LIFECYCLE_WINDOWS_BIND_DEADLINE_MS);
1178
+ checkpoint("session-bind", "session-bind-extension-errors");
1179
+ invocationGuard.assertNotInvoked("session-bind", "session-model-invocation-guard");
1180
+ confirmNoExtensionErrors();
1181
+ checkpoint("session-bind", "session-model-bound");
1182
+ assertNoConfiguredModel(runtime, settingsManager, "session must remain unconfigured after JSON lifecycle startup");
1183
+ progress.sessionsStarted = (progress.sessionsStarted ?? 0) + 1;
1184
+ };
1185
+ try {
1186
+ checkpoint("bootstrap", "bootstrap-builtins");
1187
+ const [assertModule, fsModule, pathModule, urlModule] = await load(Promise.all([import("node:assert/strict"), import("node:fs"), import("node:path"), import("node:url")]));
1188
+ assert = assertModule.default;
1189
+ ({ mkdirSync } = fsModule);
1190
+ ({ join } = pathModule);
1191
+ ({ pathToFileURL } = urlModule);
1192
+ checkpoint("bootstrap", "bootstrap-agent-home");
1193
+ const agentHome = process.env.GENTLE_PI_AGENT_HOME;
1194
+ assert.equal(typeof agentHome, "string");
1195
+ checkpoint("bootstrap", "bootstrap-directories");
1196
+ const probeRoot = join(process.cwd(), "sdk-lifecycle-probe");
1197
+ const cwd = join(probeRoot, "cwd");
1198
+ const sessionRoot = join(probeRoot, "sessions");
1199
+ const modelRoot = join(probeRoot, "model-runtime");
1200
+ for (const path of [probeRoot, cwd, sessionRoot, modelRoot, agentHome, join(agentHome, "gentle-agents")]) mkdirSync(path, { recursive: true });
1201
+ checkpoint("sdk-load", "sdk-import");
1202
+ const sdk = await within(load(import("@earendil-works/pi-coding-agent")), 30000);
1203
+ progress.sdkLoaded = true;
1204
+ checkpoint("sdk-load", "sdk-exports");
1205
+ const { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, ModelRuntime, SessionManager, SettingsManager } = sdk;
1206
+ for (const value of [createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, ModelRuntime, SessionManager, SettingsManager]) assert.equal(typeof value, "function", "SDK lifecycle export is unavailable");
1207
+ checkpoint("jiti-load", "jiti-import");
1208
+ const { createJiti } = await within(load(import(pathToFileURL(${JSON.stringify(jitiStaticEntry)}).href)), 30000);
1209
+ assert.equal(typeof createJiti, "function", "Jiti static export is unavailable");
1210
+ const jiti = createJiti(pathToFileURL(${JSON.stringify(consumerPackageJson)}).href, { moduleCache: false });
1211
+ const agentsExtensionPath = join(${JSON.stringify(packageRoot)}, "extensions", "gentle-agents.ts");
1212
+ const gentleAiExtensionPath = join(${JSON.stringify(packageRoot)}, "extensions", "gentle-ai.ts");
1213
+ checkpoint("agents-module-load", "agents-module-import");
1214
+ const { createDefaultSessionTransport } = await within(load(jiti.import(pathToFileURL(agentsExtensionPath).href)), 30000);
1215
+ checkpoint("agents-module-load", "agents-module-export");
1216
+ assert.equal(typeof createDefaultSessionTransport, "function", "packaged agents transport export is unavailable");
1217
+ const { WindowsSessionRegistryPhaseSequence } = await within(load(jiti.import(pathToFileURL(join(${JSON.stringify(packageRoot)}, "lib", "windows-session-transport.ts")).href)), 30000);
1218
+ assert.equal(typeof WindowsSessionRegistryPhaseSequence, "function", "packaged Windows phase sequence export is unavailable");
1219
+ const expectedExtensionPaths = [agentsExtensionPath, gentleAiExtensionPath];
1220
+ checkpoint("services", "settings-untrusted");
1221
+ settingsManager = SettingsManager.inMemory({}, { projectTrusted: false });
1222
+ assert.equal(settingsManager.isProjectTrusted(), false, "in-memory settings must keep project discovery untrusted");
1223
+ checkpoint("services", "settings-default-provider");
1224
+ assert.equal(settingsManager.getDefaultProvider(), undefined, "in-memory settings must have no default provider");
1225
+ checkpoint("services", "settings-default-model");
1226
+ assert.equal(settingsManager.getDefaultModel(), undefined, "in-memory settings must have no default model");
1227
+ const createRuntime = (name) => async ({ cwd: runtimeCwd, sessionManager, sessionStartEvent }) => {
1228
+ checkpoint("session-create", "session-create");
1229
+ assert.equal(sessionManager.getEntries().length, 0, "new SDK session must not restore prior entries");
1230
+ checkpoint("model-runtime", "model-runtime-create");
1231
+ const runtimeRoot = join(modelRoot, name);
1232
+ mkdirSync(runtimeRoot, { recursive: true });
1233
+ const modelRuntime = await within(ModelRuntime.create({
1234
+ authPath: join(runtimeRoot, "auth.json"),
1235
+ modelsPath: join(runtimeRoot, "models.json"),
1236
+ modelsStorePath: join(runtimeRoot, "models-store.json"),
1237
+ refreshOnCreate: false,
1238
+ }), 30000);
1239
+ checkpoint("services", "services-create");
1240
+ const services = await within(createAgentSessionServices({
1241
+ cwd: runtimeCwd,
1242
+ agentDir: agentHome,
1243
+ modelRuntime,
1244
+ settingsManager,
1245
+ resourceLoaderOptions: {
1246
+ additionalExtensionPaths: expectedExtensionPaths,
1247
+ noExtensions: true,
1248
+ noSkills: true,
1249
+ noPromptTemplates: true,
1250
+ noThemes: true,
1251
+ noContextFiles: true,
1252
+ },
1253
+ }), 30000);
1254
+ checkpoint("services", "services-settings-manager");
1255
+ assert.strictEqual(services.settingsManager, settingsManager, "services must retain the shared untrusted settings manager");
1256
+ checkpoint("services", "services-project-trusted");
1257
+ assert.equal(services.settingsManager.isProjectTrusted(), false, "services must keep project discovery untrusted");
1258
+ checkpoint("extensions-validate", "extensions-errors");
1259
+ const loaded = services.resourceLoader.getExtensions();
1260
+ assert.equal(loaded.errors.length, 0, "packaged extension load must not report errors");
1261
+ checkpoint("extensions-validate", "extensions-paths");
1262
+ assert.deepEqual(loaded.extensions.map((extension) => extension.resolvedPath), expectedExtensionPaths, "only the two packaged extensions may load");
1263
+ checkpoint("extensions-validate", "extensions-hooks");
1264
+ assert.deepEqual(loaded.extensions.map((extension) => extension.handlers.has("session_start") && extension.handlers.has("session_shutdown")), [true, true], "both packaged extensions must expose lifecycle hooks");
1265
+ checkpoint("extensions-validate", "services-diagnostics");
1266
+ assert.equal(services.diagnostics.length, 0, "packaged extension service setup must not report errors");
1267
+ checkpoint("extensions-validate", "ambient-skills");
1268
+ assert.deepEqual(services.resourceLoader.getSkills().skills, [], "ambient skills must stay disabled");
1269
+ checkpoint("extensions-validate", "ambient-prompts");
1270
+ assert.deepEqual(services.resourceLoader.getPrompts().prompts, [], "ambient prompts must stay disabled");
1271
+ checkpoint("extensions-validate", "ambient-themes");
1272
+ assert.deepEqual(services.resourceLoader.getThemes().themes, [], "ambient themes must stay disabled");
1273
+ checkpoint("extensions-validate", "ambient-context-files");
1274
+ assert.deepEqual(services.resourceLoader.getAgentsFiles().agentsFiles, [], "ambient context files must stay disabled");
1275
+ checkpoint("model-availability", "model-availability-empty");
1276
+ assert.equal(modelRuntime.getAvailableSnapshot().length, 0, "owned empty auth and model paths must expose no available models");
1277
+ checkpoint("session-create", "session-create");
1278
+ const created = await within(createAgentSessionFromServices({ services, sessionManager, sessionStartEvent, noTools: "all" }), 30000);
1279
+ checkpoint("session-create", "session-model-unbound");
1280
+ const runtime = { ...created, services, diagnostics: services.diagnostics };
1281
+ assertNoConfiguredModel(runtime, settingsManager, "new SDK session");
1282
+ const invocationGuard = installForbiddenModelInvocationGuard(runtime);
1283
+ if (name === "first") firstInvocationGuard = invocationGuard;
1284
+ else secondInvocationGuard = invocationGuard;
1285
+ return runtime;
1286
+ };
1287
+ checkpoint("session-create", "session-create");
1288
+ firstRuntime = await within(createAgentSessionRuntime(createRuntime("first"), {
1289
+ cwd,
1290
+ agentDir: agentHome,
1291
+ sessionManager: SessionManager.create(cwd, join(sessionRoot, "first")),
1292
+ }), 30000);
1293
+ await bindRuntime(firstRuntime, settingsManager, firstInvocationGuard);
1294
+ checkpoint("session-create", "session-create");
1295
+ secondRuntime = await within(createAgentSessionRuntime(createRuntime("second"), {
1296
+ cwd,
1297
+ agentDir: agentHome,
1298
+ sessionManager: SessionManager.create(cwd, join(sessionRoot, "second")),
1299
+ }), 30000);
1300
+ await bindRuntime(secondRuntime, settingsManager, secondInvocationGuard);
1301
+ checkpoint("session-bind", "session-ids-distinct");
1302
+ const firstId = firstRuntime.session.sessionId;
1303
+ const secondId = secondRuntime.session.sessionId;
1304
+ assert.notEqual(firstId, secondId, "SDK must create distinct real session IDs");
1305
+ checkpoint("presence-two", "presence-observer-create");
1306
+ windowsRegistryObservation = createWindowsRegistryObservation(WindowsSessionRegistryPhaseSequence);
1307
+ let windowsRegistryFailure;
1308
+ try {
1309
+ observer = await within(createDefaultSessionTransport(process.platform).createRegistry(agentHome, (event) => windowsRegistryObservation.observe(event)), SDK_LIFECYCLE_WINDOWS_REGISTRY_DEADLINE_MS);
1310
+ } catch (error) {
1311
+ windowsRegistryFailure = error;
1312
+ } finally {
1313
+ progress.windowsRegistryObservation = windowsRegistryObservation.snapshot();
1314
+ }
1315
+ if (windowsRegistryFailure !== undefined) throw windowsRegistryFailure;
1316
+ if (!windowsRegistryObservation.startupSucceeded) throw { childReceiptCode: OBSERVATION_INVALID };
1317
+ checkpoint("presence-two", "presence-two-records");
1318
+ const started = await within(observer.listActivations(), SDK_LIFECYCLE_WINDOWS_LIST_DEADLINE_MS);
1319
+ assert.deepEqual(started.map((record) => record.sessionId).sort(), [firstId, secondId].sort(), "default transport must advertise both SDK sessions");
1320
+ progress.presenceAfterStart = started.length;
1321
+ checkpoint("presence-two", "presence-first-model-unbound");
1322
+ assertNoConfiguredModel(firstRuntime, settingsManager, "first session must remain unconfigured through presence lifecycle");
1323
+ firstInvocationGuard.assertNotInvoked("session-bind", "session-model-invocation-guard");
1324
+ checkpoint("presence-two", "presence-second-model-unbound");
1325
+ assertNoConfiguredModel(secondRuntime, settingsManager, "second session must remain unconfigured through presence lifecycle");
1326
+ secondInvocationGuard.assertNotInvoked("session-bind", "session-model-invocation-guard");
1327
+ checkpoint("presence-two", "presence-two-extension-errors");
1328
+ confirmNoExtensionErrors();
1329
+ checkpoint("dispose-first", "dispose-first");
1330
+ await disposeRuntime(firstRuntime, settingsManager, firstInvocationGuard, "dispose-first", "dispose-first", "dispose-first-extension-errors");
1331
+ firstRuntime = undefined;
1332
+ checkpoint("presence-one", "presence-one-record");
1333
+ const afterFirstDispose = await within(observer.listActivations(), SDK_LIFECYCLE_WINDOWS_LIST_DEADLINE_MS);
1334
+ assert.deepEqual(afterFirstDispose.map((record) => record.sessionId), [secondId], "disposing one runtime must withdraw only its presence");
1335
+ progress.presenceAfterFirstDispose = afterFirstDispose.length;
1336
+ checkpoint("presence-one", "presence-one-model-unbound");
1337
+ assertNoConfiguredModel(secondRuntime, settingsManager, "surviving session must remain unconfigured after peer withdrawal");
1338
+ secondInvocationGuard.assertNotInvoked("session-bind", "session-model-invocation-guard");
1339
+ checkpoint("presence-one", "presence-one-extension-errors");
1340
+ confirmNoExtensionErrors();
1341
+ checkpoint("dispose-second", "dispose-second");
1342
+ await disposeRuntime(secondRuntime, settingsManager, secondInvocationGuard, "dispose-second", "dispose-second", "dispose-second-extension-errors");
1343
+ secondRuntime = undefined;
1344
+ checkpoint("presence-none", "presence-no-records");
1345
+ const afterSecondDispose = await within(observer.listActivations(), SDK_LIFECYCLE_WINDOWS_LIST_DEADLINE_MS);
1346
+ assert.deepEqual(afterSecondDispose, [], "disposing both runtimes must withdraw both presence records");
1347
+ progress.presenceAfterSecondDispose = afterSecondDispose.length;
1348
+ checkpoint("presence-none", "presence-none-extension-errors");
1349
+ confirmNoExtensionErrors();
1350
+ completedCheckpoint = { stage: childStage, checkId: childCheckId };
1351
+ } catch (error) {
1352
+ primaryFailure = { stage: childStage, checkId: childCheckId, code: failureCode(error) };
111
1353
  } finally {
112
- rmSync(temporary, { recursive: true, force: true });
1354
+ checkpoint("cleanup", "cleanup-runtime");
1355
+ try { await disposeRuntime(firstRuntime, settingsManager, firstInvocationGuard, "cleanup", "cleanup-runtime", "cleanup-runtime"); } catch { recordCleanupFailure("cleanup-runtime"); }
1356
+ try { await disposeRuntime(secondRuntime, settingsManager, secondInvocationGuard, "cleanup", "cleanup-runtime", "cleanup-runtime"); } catch { recordCleanupFailure("cleanup-runtime"); }
1357
+ checkpoint("cleanup", "cleanup-observer");
1358
+ try { await within(Promise.resolve(observer?.close?.()), 15000); } catch { recordCleanupFailure("cleanup-observer"); }
1359
+ try {
1360
+ if (windowsRegistryObservation !== undefined) {
1361
+ progress.windowsRegistryObservation = windowsRegistryObservation.snapshot();
1362
+ if (!windowsRegistryObservation.cleanupComplete || (primaryFailure === undefined && !windowsRegistryObservation.admitsFullSuccess)) recordCleanupFailure("cleanup-observer");
1363
+ }
1364
+ } catch { recordCleanupFailure("cleanup-observer"); }
1365
+ checkpoint("cleanup", "cleanup-extension-errors");
1366
+ try { assertNoExtensionErrors(); } catch { recordCleanupFailure("cleanup-extension-errors"); }
1367
+ extensionErrorsActive = false;
1368
+ if (primaryFailure === undefined && cleanupFailure !== undefined) primaryFailure = cleanupFailure;
1369
+ }
1370
+ const report = {
1371
+ status: primaryFailure === undefined ? "complete" : "failed",
1372
+ stage: primaryFailure?.stage ?? completedCheckpoint?.stage ?? childStage,
1373
+ checkId: primaryFailure?.checkId ?? completedCheckpoint?.checkId ?? childCheckId,
1374
+ cleanupStatus,
1375
+ ...progress,
1376
+ ...(primaryFailure === undefined ? {} : { code: primaryFailure.code }),
1377
+ };
1378
+ const line = JSON.stringify(report);
1379
+ const boundedLine = Buffer.byteLength(line, "utf8") <= ${MAX_SDK_LIFECYCLE_CHILD_REPORT_BYTES}
1380
+ ? line
1381
+ : '{"status":"failed","stage":"cleanup","checkId":"cleanup-runtime","cleanupStatus":"failed","code":"unknown"}';
1382
+ try { process.stdout.write(boundedLine); } catch { process.exitCode = 1; }
1383
+ if (boundedLine !== line || primaryFailure !== undefined) process.exitCode = 1;
1384
+ `;
1385
+ }
1386
+
1387
+ async function testSdkLifecyclePackedSession() {
1388
+ const receipt = newSdkLifecycleReceipt();
1389
+ let temporary;
1390
+ let stage = "pack";
1391
+ let failure;
1392
+ try {
1393
+ selectSdkLifecycleCheck(receipt, "runner-hosted");
1394
+ if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") throw new SdkLifecycleFailure("pack", "assertion-failed");
1395
+ selectSdkLifecycleCheck(receipt, "runner-temp");
1396
+ const runnerTemp = process.env.RUNNER_TEMP;
1397
+ if (typeof runnerTemp !== "string" || runnerTemp.length === 0) throw new SdkLifecycleFailure("pack", "assertion-failed");
1398
+ selectSdkLifecycleCheck(receipt, "temporary-root");
1399
+ temporary = mkdtempSync(join(resolve(runnerTemp), "gentle-pi-sdk-lifecycle-"));
1400
+ const packDirectory = join(temporary, "pack");
1401
+ const consumerDirectory = join(temporary, "consumer");
1402
+ mkdirSync(packDirectory);
1403
+ mkdirSync(consumerDirectory);
1404
+ const { env } = isolatedUnhookedEnvironment(temporary);
1405
+ Object.assign(env, { GENTLE_PI_AGENTS: "1", PI_OFFLINE: "1" });
1406
+ selectSdkLifecycleCheck(receipt, "project-sdk-version");
1407
+ const manifest = safeJson(readFileSync(join(root, "package.json")), "project package manifest");
1408
+ const sdkVersion = manifest?.devDependencies?.["@earendil-works/pi-coding-agent"];
1409
+ if (sdkVersion !== "0.85.1") throw new Error("SDK lifecycle probe requires the project-pinned Pi SDK");
1410
+ selectSdkLifecycleCheck(receipt, "pack-command");
1411
+ const packed = safeJson(runBoundedNpm("pack", ["pack", "--ignore-scripts", "--json", "--pack-destination", packDirectory], env, root), "npm pack output");
1412
+ stage = "pack-result";
1413
+ selectSdkLifecycleCheck(receipt, "pack-metadata");
1414
+ const { tarball } = assertPackResult(packed, packDirectory, { checkId: "pack-metadata" });
1415
+ selectSdkLifecycleCheck(receipt, "pack-integrity");
1416
+ receipt.packVerified = true;
1417
+ stage = "install";
1418
+ selectSdkLifecycleCheck(receipt, "install-command");
1419
+ writeFileSync(join(consumerDirectory, "package.json"), JSON.stringify({ name: "gentle-pi-sdk-lifecycle-proof", private: true, dependencies: { "@earendil-works/pi-coding-agent": sdkVersion } }), "utf8");
1420
+ runBoundedNpm("install", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false", "--omit=dev", "--legacy-peer-deps", tarball, `@earendil-works/pi-coding-agent@${sdkVersion}`], env, consumerDirectory);
1421
+ receipt.installCompleted = true;
1422
+ stage = "artifact-check";
1423
+ const packageRoot = join(consumerDirectory, "node_modules", "gentle-pi");
1424
+ selectSdkLifecycleCheck(receipt, "packed-assets");
1425
+ assertPackedAssets(packageRoot, { checkId: "asset-runtime-windows-session-transport-owned" });
1426
+ selectSdkLifecycleCheck(receipt, "native-artifacts");
1427
+ assertNoNativeInstallerArtifacts(packageRoot, consumerDirectory, { checkId: "native-package-cache-absent" });
1428
+ const sdkPackageJson = join(consumerDirectory, "node_modules", "@earendil-works", "pi-coding-agent", "package.json");
1429
+ const jitiStaticEntry = resolveInstalledJitiStaticEntry(consumerDirectory, sdkPackageJson, sdkVersion, (checkId) => selectSdkLifecycleCheck(receipt, checkId), {
1430
+ sdkManifest: "sdk-manifest", sdkVersion: "sdk-version", jitiManifest: "jiti-manifest-owned",
1431
+ jitiStaticExport: "jiti-static-export", jitiEntry: "jiti-entry-owned", jitiVersion: "jiti-version",
1432
+ });
1433
+ stage = "lifecycle-probe";
1434
+ selectSdkLifecycleCheck(receipt, "lifecycle-probe-command");
1435
+ const probe = await runBoundedSdkLifecycleProbe(["--input-type=module", "--eval", sdkLifecycleProbeSource(packageRoot, join(consumerDirectory, "package.json"), jitiStaticEntry)], env, consumerDirectory);
1436
+ stage = "lifecycle-result";
1437
+ selectSdkLifecycleCheck(receipt, "lifecycle-probe-result");
1438
+ const lifecycle = parseSdkLifecycleChildReceipt(probe.stdout, probe.stderr);
1439
+ if (lifecycle === undefined) throw new Error("SDK lifecycle probe returned an invalid receipt");
1440
+ mergeSdkLifecycleChildProgress(receipt, lifecycle);
1441
+ if (probe.status !== 0) throw new SdkLifecycleFailure("lifecycle-probe", "nonzero-exit", probe.status, lifecycle);
1442
+ if (lifecycle.status !== "complete") throw new SdkLifecycleFailure("lifecycle-probe", "nonzero-exit", undefined, lifecycle);
1443
+ selectSdkLifecycleCheck(receipt, "sdk-lifecycle-complete");
1444
+ } catch (error) {
1445
+ if (error instanceof SdkLifecycleFailure) failure = error;
1446
+ else if (error instanceof UnhookedFailure) failure = new SdkLifecycleFailure(stage, error.code, error.exitStatus);
1447
+ else failure = new SdkLifecycleFailure(stage, stage === "lifecycle-result" ? "invalid-result" : "assertion-failed");
1448
+ }
1449
+ if (temporary !== undefined) {
1450
+ try {
1451
+ if (failure === undefined) selectSdkLifecycleCheck(receipt, "cleanup-owned-root-removal");
1452
+ rmSync(temporary, { recursive: true, force: true });
1453
+ receipt.cleanupCompleted = !existsSync(temporary);
1454
+ if (!receipt.cleanupCompleted) throw new Error("owned temporary root remains after cleanup");
1455
+ } catch {
1456
+ if (failure === undefined) failure = new SdkLifecycleFailure("cleanup", "cleanup-failed");
1457
+ }
1458
+ }
1459
+ return { receipt, failure };
1460
+ }
1461
+
1462
+ async function testWindowsStartupTimingPackedHelper() {
1463
+ const receipt = newWindowsStartupTimingReceipt();
1464
+ let temporary;
1465
+ let stage = "pack";
1466
+ let failure;
1467
+ try {
1468
+ selectWindowsStartupTimingCheck(receipt, "runner-hosted");
1469
+ if (process.env.RUNNER_ENVIRONMENT !== "github-hosted" || process.platform !== "win32") throw new WindowsStartupTimingFailure("pack", "assertion-failed");
1470
+ selectWindowsStartupTimingCheck(receipt, "runner-temp");
1471
+ const runnerTemp = process.env.RUNNER_TEMP;
1472
+ if (typeof runnerTemp !== "string" || runnerTemp.length === 0) throw new WindowsStartupTimingFailure("pack", "assertion-failed");
1473
+ selectWindowsStartupTimingCheck(receipt, "temporary-root");
1474
+ temporary = mkdtempSync(join(resolve(runnerTemp), "gentle-pi-windows-startup-timing-"));
1475
+ const packDirectory = join(temporary, "pack");
1476
+ const consumerDirectory = join(temporary, "consumer");
1477
+ mkdirSync(packDirectory);
1478
+ mkdirSync(consumerDirectory);
1479
+ const { env } = isolatedUnhookedEnvironment(temporary);
1480
+ Object.assign(env, { GENTLE_PI_AGENTS: "1", PI_OFFLINE: "1" });
1481
+ selectWindowsStartupTimingCheck(receipt, "project-sdk-version");
1482
+ const manifest = safeJson(readFileSync(join(root, "package.json")), "project package manifest");
1483
+ const sdkVersion = manifest?.devDependencies?.["@earendil-works/pi-coding-agent"];
1484
+ if (sdkVersion !== "0.85.1") throw new Error("Windows startup timing probe requires the project-pinned Pi SDK");
1485
+ selectWindowsStartupTimingCheck(receipt, "pack-command");
1486
+ const packed = safeJson(runBoundedNpm("pack", ["pack", "--ignore-scripts", "--json", "--pack-destination", packDirectory], env, root), "npm pack output");
1487
+ stage = "pack-result";
1488
+ selectWindowsStartupTimingCheck(receipt, "pack-metadata");
1489
+ const { tarball } = assertPackResult(packed, packDirectory, { checkId: "pack-metadata" });
1490
+ selectWindowsStartupTimingCheck(receipt, "pack-integrity");
1491
+ receipt.packVerified = true;
1492
+ stage = "install";
1493
+ selectWindowsStartupTimingCheck(receipt, "install-command");
1494
+ writeFileSync(join(consumerDirectory, "package.json"), JSON.stringify({ name: "gentle-pi-windows-startup-timing", private: true, dependencies: { "@earendil-works/pi-coding-agent": sdkVersion } }), "utf8");
1495
+ runBoundedNpm("install", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false", "--omit=dev", "--legacy-peer-deps", tarball, `@earendil-works/pi-coding-agent@${sdkVersion}`], env, consumerDirectory);
1496
+ receipt.installCompleted = true;
1497
+ stage = "artifact-check";
1498
+ const packageRoot = join(consumerDirectory, "node_modules", "gentle-pi");
1499
+ selectWindowsStartupTimingCheck(receipt, "packed-assets");
1500
+ assertPackedAssets(packageRoot, { checkId: "asset-runtime-windows-session-transport-owned" });
1501
+ selectWindowsStartupTimingCheck(receipt, "native-artifacts");
1502
+ assertNoNativeInstallerArtifacts(packageRoot, consumerDirectory, { checkId: "native-package-cache-absent" });
1503
+ selectWindowsStartupTimingCheck(receipt, "installed-helper");
1504
+ const installedHelper = assertOwnedRegularFile(packageRoot, "runtime/windows-session-transport.ps1");
1505
+ stage = "helper-start";
1506
+ selectWindowsStartupTimingCheck(receipt, "helper-start");
1507
+ const measured = await runWindowsStartupTimingProbe(installedHelper, env, consumerDirectory);
1508
+ receipt.helperStartOutcome = measured.helperStartOutcome;
1509
+ receipt.startElapsedMs = measured.startElapsedMs;
1510
+ receipt.lastStartupMarker = measured.lastStartupMarker;
1511
+ receipt.cleanup = measured.cleanup;
1512
+ receipt.physicalCloseObserved = measured.physicalCloseObserved;
1513
+ stage = "helper-result";
1514
+ selectWindowsStartupTimingCheck(receipt, "helper-result");
1515
+ if (measured.failure !== undefined) throw new WindowsStartupTimingFailure(measured.failure.stage, measured.failure.code);
1516
+ if (measured.helperStartOutcome !== "valid-reply" || measured.startElapsedMs === null || measured.lastStartupMarker !== "native-ready" || measured.cleanup !== "close-observed" || !measured.physicalCloseObserved) throw new WindowsStartupTimingFailure("helper-result", "assertion-failed");
1517
+ selectWindowsStartupTimingCheck(receipt, "windows-helper-startup-measured");
1518
+ } catch (error) {
1519
+ if (error instanceof WindowsStartupTimingFailure) failure = error;
1520
+ else if (error instanceof UnhookedFailure) failure = new WindowsStartupTimingFailure(stage, error.code);
1521
+ else failure = new WindowsStartupTimingFailure(stage, "assertion-failed");
1522
+ }
1523
+ if (temporary !== undefined) {
1524
+ try {
1525
+ if (failure === undefined) selectWindowsStartupTimingCheck(receipt, "cleanup-owned-root-removal");
1526
+ rmSync(temporary, { recursive: true, force: true });
1527
+ receipt.cleanupCompleted = !existsSync(temporary);
1528
+ if (!receipt.cleanupCompleted) throw new Error("owned temporary root remains after cleanup");
1529
+ } catch {
1530
+ if (failure === undefined) failure = new WindowsStartupTimingFailure("cleanup", "cleanup-failed");
1531
+ }
1532
+ }
1533
+ return { receipt, failure };
1534
+ }
1535
+
1536
+ async function testWindowsStartupTimingEnvironmentExperiment() {
1537
+ const receipt = { checkId: "not-attempted", packVerified: false, installCompleted: false, cases: [], cleanupCompleted: false };
1538
+ let temporary;
1539
+ let stage = "pack";
1540
+ let failure;
1541
+ try {
1542
+ selectWindowsStartupTimingCheck(receipt, "runner-hosted");
1543
+ if (process.env.RUNNER_ENVIRONMENT !== "github-hosted" || process.platform !== "win32") throw new WindowsStartupTimingFailure("pack", "assertion-failed");
1544
+ selectWindowsStartupTimingCheck(receipt, "runner-temp");
1545
+ const runnerTemp = process.env.RUNNER_TEMP;
1546
+ if (typeof runnerTemp !== "string" || runnerTemp.length === 0) throw new WindowsStartupTimingFailure("pack", "assertion-failed");
1547
+ selectWindowsStartupTimingCheck(receipt, "temporary-root");
1548
+ temporary = mkdtempSync(join(resolve(runnerTemp), "gentle-pi-windows-startup-environment-"));
1549
+ const packDirectory = join(temporary, "pack");
1550
+ const consumerDirectory = join(temporary, "consumer");
1551
+ mkdirSync(packDirectory);
1552
+ mkdirSync(consumerDirectory);
1553
+ // Both cases retain this exact existing minimal isolation environment; only
1554
+ // the treatment receives the validated, fixed Windows path-key delta.
1555
+ const { env: isolatedEnv } = isolatedUnhookedEnvironment(temporary);
1556
+ Object.assign(isolatedEnv, { GENTLE_PI_AGENTS: "1", PI_OFFLINE: "1" });
1557
+ const pathDelta = deriveWindowsStartupTimingPathDelta(process.env);
1558
+ const treatmentPathDelta = validateWindowsStartupTimingMachinePaths(pathDelta) ? pathDelta : undefined;
1559
+ selectWindowsStartupTimingCheck(receipt, "project-sdk-version");
1560
+ const manifest = safeJson(readFileSync(join(root, "package.json")), "project package manifest");
1561
+ const sdkVersion = manifest?.devDependencies?.["@earendil-works/pi-coding-agent"];
1562
+ if (sdkVersion !== "0.85.1") throw new Error("Windows startup environment experiment requires the project-pinned Pi SDK");
1563
+ selectWindowsStartupTimingCheck(receipt, "pack-command");
1564
+ const packed = safeJson(runBoundedNpm("pack", ["pack", "--ignore-scripts", "--json", "--pack-destination", packDirectory], isolatedEnv, root), "npm pack output");
1565
+ stage = "pack-result";
1566
+ selectWindowsStartupTimingCheck(receipt, "pack-metadata");
1567
+ const { tarball } = assertPackResult(packed, packDirectory, { checkId: "pack-metadata" });
1568
+ selectWindowsStartupTimingCheck(receipt, "pack-integrity");
1569
+ receipt.packVerified = true;
1570
+ stage = "install";
1571
+ selectWindowsStartupTimingCheck(receipt, "install-command");
1572
+ writeFileSync(join(consumerDirectory, "package.json"), JSON.stringify({ name: "gentle-pi-windows-startup-environment", private: true, dependencies: { "@earendil-works/pi-coding-agent": sdkVersion } }), "utf8");
1573
+ runBoundedNpm("install", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false", "--omit=dev", "--legacy-peer-deps", tarball, `@earendil-works/pi-coding-agent@${sdkVersion}`], isolatedEnv, consumerDirectory);
1574
+ receipt.installCompleted = true;
1575
+ stage = "artifact-check";
1576
+ const packageRoot = join(consumerDirectory, "node_modules", "gentle-pi");
1577
+ selectWindowsStartupTimingCheck(receipt, "packed-assets");
1578
+ assertPackedAssets(packageRoot, { checkId: "asset-runtime-windows-session-transport-owned" });
1579
+ selectWindowsStartupTimingCheck(receipt, "native-artifacts");
1580
+ assertNoNativeInstallerArtifacts(packageRoot, consumerDirectory, { checkId: "native-package-cache-absent" });
1581
+ selectWindowsStartupTimingCheck(receipt, "installed-helper");
1582
+ const installedHelper = assertOwnedRegularFile(packageRoot, "runtime/windows-session-transport.ps1");
1583
+ for (const name of WINDOWS_STARTUP_TIMING_ENVIRONMENT_CASE_NAMES) {
1584
+ const isTreatment = name === "windows-paths";
1585
+ const caseReceipt = newWindowsStartupTimingEnvironmentCase(name, isTreatment && treatmentPathDelta !== undefined ? treatmentPathDelta.pathAdditionKeys : []);
1586
+ receipt.cases.push(caseReceipt);
1587
+ if (isTreatment && receipt.cases[0].physicalCloseObserved !== true) {
1588
+ caseReceipt.cleanup = "blocked";
1589
+ caseReceipt.failureStage = "cleanup";
1590
+ caseReceipt.failureCode = "cleanup-unconfirmed";
1591
+ break;
1592
+ }
1593
+ if (isTreatment && treatmentPathDelta === undefined) {
1594
+ caseReceipt.failureStage = "helper-start";
1595
+ caseReceipt.failureCode = "environment-invalid";
1596
+ if (failure === undefined) failure = new WindowsStartupTimingFailure("helper-start", "environment-invalid");
1597
+ continue;
1598
+ }
1599
+ stage = "helper-start";
1600
+ selectWindowsStartupTimingCheck(receipt, "helper-start");
1601
+ const measured = await runWindowsStartupTimingProbe(installedHelper, isTreatment ? Object.assign({}, isolatedEnv, treatmentPathDelta.values) : isolatedEnv, consumerDirectory);
1602
+ caseReceipt.helperStartOutcome = measured.helperStartOutcome;
1603
+ caseReceipt.startElapsedMs = measured.startElapsedMs;
1604
+ caseReceipt.lastStartupMarker = measured.lastStartupMarker;
1605
+ caseReceipt.cleanup = measured.cleanup;
1606
+ caseReceipt.physicalCloseObserved = measured.physicalCloseObserved;
1607
+ stage = "helper-result";
1608
+ selectWindowsStartupTimingCheck(receipt, "helper-result");
1609
+ if (!WINDOWS_STARTUP_TIMING_CLEANUP_OUTCOMES.has(caseReceipt.cleanup)) throw new WindowsStartupTimingFailure("helper-result", "assertion-failed");
1610
+ if (measured.failure !== undefined) {
1611
+ caseReceipt.failureStage = measured.failure.stage;
1612
+ caseReceipt.failureCode = measured.failure.code;
1613
+ if (failure === undefined) failure = new WindowsStartupTimingFailure(measured.failure.stage, measured.failure.code);
1614
+ } else if (measured.helperStartOutcome !== "valid-reply" || measured.startElapsedMs === null || measured.lastStartupMarker !== "native-ready" || measured.cleanup !== "close-observed" || !measured.physicalCloseObserved) {
1615
+ caseReceipt.failureStage = "helper-result";
1616
+ caseReceipt.failureCode = "assertion-failed";
1617
+ if (failure === undefined) failure = new WindowsStartupTimingFailure("helper-result", "assertion-failed");
1618
+ }
1619
+ }
1620
+ if (receipt.cases.length !== 2) throw new WindowsStartupTimingFailure("helper-result", "assertion-failed");
1621
+ if (failure === undefined) selectWindowsStartupTimingCheck(receipt, "windows-helper-startup-environment-measured");
1622
+ } catch (error) {
1623
+ if (failure === undefined) {
1624
+ if (error instanceof WindowsStartupTimingFailure) failure = error;
1625
+ else if (error instanceof UnhookedFailure) failure = new WindowsStartupTimingFailure(stage, error.code);
1626
+ else failure = new WindowsStartupTimingFailure(stage, "assertion-failed");
1627
+ }
1628
+ }
1629
+ if (temporary !== undefined) {
1630
+ try {
1631
+ if (failure === undefined) selectWindowsStartupTimingCheck(receipt, "cleanup-owned-root-removal");
1632
+ rmSync(temporary, { recursive: true, force: true });
1633
+ receipt.cleanupCompleted = !existsSync(temporary);
1634
+ if (!receipt.cleanupCompleted) throw new Error("owned temporary root remains after cleanup");
1635
+ } catch {
1636
+ if (failure === undefined) failure = new WindowsStartupTimingFailure("cleanup", "cleanup-failed");
1637
+ }
1638
+ }
1639
+ return { receipt, failure };
1640
+ }
1641
+
1642
+ async function testUnhookedPackedImports() {
1643
+ const receipt = newUnhookedReceipt();
1644
+ let temporary;
1645
+ let stage = "pack";
1646
+ let failure;
1647
+ try {
1648
+ selectUnhookedCheck(receipt, "runner-temp");
1649
+ const runnerTemp = process.env.RUNNER_TEMP;
1650
+ if (typeof runnerTemp !== "string" || runnerTemp.length === 0) throw new Error("RUNNER_TEMP is required");
1651
+ selectUnhookedCheck(receipt, "temporary-root");
1652
+ temporary = mkdtempSync(join(resolve(runnerTemp), "gentle-pi-packed-unhooked-"));
1653
+ const packDirectory = join(temporary, "pack");
1654
+ const consumerDirectory = join(temporary, "consumer");
1655
+ mkdirSync(packDirectory);
1656
+ mkdirSync(consumerDirectory);
1657
+ const { env, homes } = isolatedUnhookedEnvironment(temporary);
1658
+ selectUnhookedCheck(receipt, "project-sdk-version");
1659
+ const manifest = safeJson(readFileSync(join(root, "package.json")), "project package manifest");
1660
+ const sdkVersion = manifest?.devDependencies?.["@earendil-works/pi-coding-agent"];
1661
+ if (sdkVersion !== "0.85.1") throw new Error("unhooked probe requires the project-pinned Pi SDK");
1662
+ selectUnhookedCheck(receipt, "pack-command");
1663
+ const packOutput = runBoundedNpm("pack", ["pack", "--ignore-scripts", "--json", "--pack-destination", packDirectory], env, root);
1664
+ stage = "pack-result";
1665
+ selectUnhookedCheck(receipt, "pack-metadata");
1666
+ const packed = safeJson(packOutput, "npm pack output");
1667
+ const { tarball } = assertPackResult(packed, packDirectory, receipt);
1668
+ receipt.packVerified = true;
1669
+ stage = "install";
1670
+ selectUnhookedCheck(receipt, "install-command");
1671
+ writeFileSync(join(consumerDirectory, "package.json"), JSON.stringify({ name: "gentle-pi-unhooked-import-proof", private: true, dependencies: { "@earendil-works/pi-coding-agent": sdkVersion } }), "utf8");
1672
+ runBoundedNpm("install", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false", "--omit=dev", "--legacy-peer-deps", tarball, `@earendil-works/pi-coding-agent@${sdkVersion}`], env, consumerDirectory);
1673
+ receipt.installCompleted = true;
1674
+ stage = "artifact-check";
1675
+ const packageRoot = join(consumerDirectory, "node_modules", "gentle-pi");
1676
+ assertPackedAssets(packageRoot, receipt);
1677
+ assertNoNativeInstallerArtifacts(packageRoot, consumerDirectory, receipt);
1678
+ const sdkPackageJson = join(consumerDirectory, "node_modules", "@earendil-works", "pi-coding-agent", "package.json");
1679
+ const jitiStaticEntry = resolveInstalledJitiStaticEntry(consumerDirectory, sdkPackageJson, sdkVersion, (checkId) => selectUnhookedCheck(receipt, checkId), {
1680
+ sdkManifest: "sdk-manifest-owned", sdkVersion: "sdk-version", jitiManifest: "jiti-manifest-owned",
1681
+ jitiStaticExport: "jiti-static-export", jitiEntry: "jiti-entry-owned", jitiVersion: "jiti-version",
1682
+ });
1683
+ // The child calls only default factories on this inert recorder; it never invokes registered tools or event handlers.
1684
+ for (const home of homes) {
1685
+ selectUnhookedCheck(receipt, home.checkId);
1686
+ assertEmptyDirectory(home.path);
1687
+ }
1688
+ stage = "import-probe";
1689
+ selectUnhookedCheck(receipt, "import-probe-command");
1690
+ const probe = runBoundedProbe(["--input-type=module", "--eval", unhookedProbeSource(packageRoot, join(consumerDirectory, "package.json"), jitiStaticEntry)], env, consumerDirectory);
1691
+ selectUnhookedCheck(receipt, "import-probe-result");
1692
+ safeJson(probe, "unhooked registration probe output");
1693
+ stage = "post-import-check";
1694
+ assertNoNativeInstallerArtifacts(packageRoot, consumerDirectory, receipt);
1695
+ for (const home of homes) {
1696
+ selectUnhookedCheck(receipt, home.checkId);
1697
+ assertEmptyDirectory(home.path);
1698
+ }
1699
+ } catch (error) {
1700
+ failure = stageFailure(stage, error, stage === "pack-result" || stage === "import-probe");
1701
+ }
1702
+ if (temporary !== undefined) {
1703
+ try {
1704
+ if (failure === undefined) selectUnhookedCheck(receipt, "cleanup-owned-root-removal");
1705
+ rmSync(temporary, { recursive: true, force: true });
1706
+ receipt.cleanupCompleted = !existsSync(temporary);
1707
+ if (!receipt.cleanupCompleted) throw new Error("owned temporary root remains after cleanup");
1708
+ } catch {
1709
+ if (failure === undefined) failure = new UnhookedFailure("cleanup", "cleanup-failed");
1710
+ }
1711
+ }
1712
+ return { receipt, failure };
1713
+ }
1714
+
1715
+ function sameEntrypointPath(left, right, platform) {
1716
+ return platform === "win32"
1717
+ ? win32.normalize(left).toLowerCase() === win32.normalize(right).toLowerCase()
1718
+ : left === right;
1719
+ }
1720
+
1721
+ export function decidePackedRunnerEntrypoint(scriptPath, argvEntry, platform, operations = { resolvePath: resolve, realpath: realpathSync.native }) {
1722
+ const importDecision = Object.freeze({ identity: "import", runModes: false, exitCode: null });
1723
+ if (typeof argvEntry !== "string" || argvEntry.length === 0) return importDecision;
1724
+ let resolvedScript;
1725
+ let resolvedEntry;
1726
+ try {
1727
+ resolvedScript = operations.resolvePath(scriptPath);
1728
+ resolvedEntry = operations.resolvePath(argvEntry);
1729
+ } catch { return importDecision; }
1730
+ try {
1731
+ const sameIdentity = sameEntrypointPath(operations.realpath(resolvedScript), operations.realpath(resolvedEntry), platform);
1732
+ return sameIdentity
1733
+ ? Object.freeze({ identity: "main", runModes: true, exitCode: null })
1734
+ : importDecision;
1735
+ } catch {
1736
+ // A lexical identity is enough to identify only the direct CLI itself; a
1737
+ // foreign importer with an unresolvable argv entry remains completely inert.
1738
+ return sameEntrypointPath(resolvedScript, resolvedEntry, platform)
1739
+ ? Object.freeze({ identity: "direct-unresolved", runModes: false, exitCode: 1 })
1740
+ : importDecision;
1741
+ }
1742
+ }
1743
+
1744
+ const entrypoint = decidePackedRunnerEntrypoint(fileURLToPath(import.meta.url), process.argv[1], process.platform);
1745
+ if (entrypoint.exitCode !== null) {
1746
+ process.exitCode = entrypoint.exitCode;
1747
+ } else if (entrypoint.runModes && process.argv.includes("--unhooked-imports")) {
1748
+ const { receipt, failure } = await testUnhookedPackedImports();
1749
+ reportUnhookedReceipt(receipt, failure);
1750
+ } else if (entrypoint.runModes && process.argv.includes("--sdk-lifecycle")) {
1751
+ const { receipt, failure } = await testSdkLifecyclePackedSession();
1752
+ reportSdkLifecycleReceipt(receipt, failure);
1753
+ } else if (entrypoint.runModes && process.argv.includes("--windows-startup-timing")) {
1754
+ const { receipt, failure } = await testWindowsStartupTimingPackedHelper();
1755
+ reportWindowsStartupTimingReceipt(receipt, failure);
1756
+ } else if (entrypoint.runModes && process.argv.includes("--windows-startup-timing-environment")) {
1757
+ const { receipt, failure } = await testWindowsStartupTimingEnvironmentExperiment();
1758
+ reportWindowsStartupTimingEnvironmentReceipt(receipt, failure);
1759
+ } else if (entrypoint.runModes) {
1760
+ await testHookedPackedRunner();
113
1761
  }