@gajae-code/coding-agent 0.12.0 → 0.12.2

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 (247) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +56 -2
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  16. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
  17. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  18. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  19. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  20. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  21. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
  22. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  23. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  24. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  25. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  26. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  28. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  29. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  32. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  34. package/dist/types/modes/interactive-mode.d.ts +9 -0
  35. package/dist/types/modes/theme/theme.d.ts +3 -1
  36. package/dist/types/modes/types.d.ts +32 -0
  37. package/dist/types/reminders/star-reminder.d.ts +3 -13
  38. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  39. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  40. package/dist/types/sdk/broker/transport.d.ts +2 -0
  41. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  43. package/dist/types/sdk/bus/config.d.ts +6 -2
  44. package/dist/types/sdk/bus/index.d.ts +29 -0
  45. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  46. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  47. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  48. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  49. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  50. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  51. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  52. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
  53. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  54. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  55. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  56. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  57. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  58. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  59. package/dist/types/sdk/prompt-status.d.ts +23 -3
  60. package/dist/types/sdk/session.d.ts +6 -0
  61. package/dist/types/session/agent-session.d.ts +40 -2
  62. package/dist/types/session/artifacts.d.ts +8 -0
  63. package/dist/types/session/blob-store.d.ts +57 -4
  64. package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
  65. package/dist/types/session/session-manager.d.ts +23 -6
  66. package/dist/types/session/session-storage.d.ts +4 -0
  67. package/dist/types/session/streaming-output.d.ts +36 -7
  68. package/dist/types/setup/credential-import.d.ts +12 -0
  69. package/dist/types/skill-state/active-state.d.ts +12 -0
  70. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  71. package/dist/types/task/executor.d.ts +3 -2
  72. package/dist/types/task/index.d.ts +7 -1
  73. package/dist/types/task/output-manager.d.ts +1 -0
  74. package/dist/types/task/provider-retry-status.d.ts +8 -0
  75. package/dist/types/task/receipt.d.ts +9 -1
  76. package/dist/types/task/types.d.ts +23 -0
  77. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  78. package/dist/types/tools/bash-interactive.d.ts +4 -1
  79. package/dist/types/tools/bash.d.ts +3 -1
  80. package/dist/types/tools/image-gen.d.ts +4 -0
  81. package/dist/types/tools/index.d.ts +6 -0
  82. package/dist/types/tools/output-meta.d.ts +23 -2
  83. package/dist/types/tools/path-utils.d.ts +1 -0
  84. package/dist/types/tools/read.d.ts +0 -6
  85. package/dist/types/tools/review.d.ts +5 -7
  86. package/dist/types/tools/subagent.d.ts +2 -0
  87. package/dist/types/utils/gh.d.ts +13 -0
  88. package/dist/types/utils/image-resize.d.ts +1 -0
  89. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  90. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  91. package/dist/types/web/search/providers/xai.d.ts +2 -0
  92. package/package.json +10 -8
  93. package/scripts/acp-conformance-agent.ts +218 -0
  94. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  95. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  96. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  97. package/scripts/resident-memory-bench.ts +1341 -0
  98. package/scripts/run-acp-conformance.ts +290 -0
  99. package/src/async/job-manager.ts +99 -39
  100. package/src/cli/fast-help.ts +1 -1
  101. package/src/cli/plugin-cli.ts +176 -18
  102. package/src/commands/team.ts +19 -1
  103. package/src/config/file-lock.ts +11 -2
  104. package/src/config/settings-schema.ts +53 -2
  105. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  106. package/src/coordinator-mcp/server.ts +5 -12
  107. package/src/debug/index.ts +42 -17
  108. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  109. package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
  110. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  111. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
  112. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  113. package/src/defaults/gjc-defaults.ts +4 -4
  114. package/src/exa/mcp-client.ts +12 -3
  115. package/src/exec/bash-executor.ts +144 -27
  116. package/src/extensibility/extensions/runner.ts +15 -0
  117. package/src/extensibility/extensions/types.ts +18 -1
  118. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  119. package/src/extensibility/gjc-plugins/index.ts +19 -2
  120. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  121. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  122. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  123. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  124. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  125. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  126. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  127. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  128. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  129. package/src/extensibility/gjc-plugins/types.ts +141 -0
  130. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  131. package/src/gjc-runtime/launch-tmux.ts +25 -6
  132. package/src/gjc-runtime/launch-worktree.ts +103 -1
  133. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  134. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  135. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  136. package/src/gjc-runtime/ralplan-runtime.ts +491 -27
  137. package/src/gjc-runtime/team-launch.ts +8 -0
  138. package/src/gjc-runtime/team-runtime.ts +140 -32
  139. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  140. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  141. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  142. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  143. package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
  144. package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
  145. package/src/gjc-runtime/workflow-manifest.ts +21 -70
  146. package/src/internal-urls/docs-index.generated.ts +16 -16
  147. package/src/modes/DESIGN.md +104 -0
  148. package/src/modes/acp/acp-agent.ts +298 -186
  149. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  150. package/src/modes/components/irc-sidebar.ts +30 -1
  151. package/src/modes/components/notifications-settings-editor.ts +12 -1
  152. package/src/modes/components/oauth-selector.ts +68 -30
  153. package/src/modes/components/session-observer-overlay.ts +305 -86
  154. package/src/modes/components/settings-selector.ts +56 -5
  155. package/src/modes/components/status-line/gh.ts +16 -0
  156. package/src/modes/components/tool-status-header.ts +3 -15
  157. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  158. package/src/modes/controllers/command-controller.ts +42 -34
  159. package/src/modes/controllers/event-controller.ts +27 -16
  160. package/src/modes/controllers/extension-ui-controller.ts +193 -34
  161. package/src/modes/controllers/selector-controller.ts +160 -22
  162. package/src/modes/interactive-mode.ts +189 -62
  163. package/src/modes/jobs-observer.ts +1 -1
  164. package/src/modes/runtime-init.ts +3 -0
  165. package/src/modes/theme/theme.ts +11 -9
  166. package/src/modes/types.ts +88 -0
  167. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  168. package/src/prompts/system/eager-todo.md +8 -4
  169. package/src/prompts/system/system-prompt.md +11 -4
  170. package/src/prompts/tools/bash.md +5 -5
  171. package/src/prompts/tools/search-tool-bm25.md +5 -0
  172. package/src/prompts/tools/task.md +1 -1
  173. package/src/reminders/star-reminder.ts +4 -42
  174. package/src/runtime-mcp/manager.ts +19 -4
  175. package/src/sdk/acp/adapter.ts +22 -6
  176. package/src/sdk/broker/lifecycle.ts +18 -13
  177. package/src/sdk/broker/transport.ts +35 -3
  178. package/src/sdk/bus/attachment-registry.ts +5 -1
  179. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  180. package/src/sdk/bus/chat-daemon-control.ts +7 -3
  181. package/src/sdk/bus/config.ts +18 -2
  182. package/src/sdk/bus/index.ts +661 -126
  183. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  184. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  185. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  186. package/src/sdk/bus/reconciliation-store.ts +95 -16
  187. package/src/sdk/bus/rich-draft.ts +1 -1
  188. package/src/sdk/bus/rich-render.ts +2 -2
  189. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  190. package/src/sdk/bus/telegram-cli.ts +33 -8
  191. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  192. package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
  193. package/src/sdk/bus/telegram-daemon.ts +916 -23
  194. package/src/sdk/bus/telegram-reference.ts +40 -3
  195. package/src/sdk/bus/threaded-render.ts +48 -3
  196. package/src/sdk/bus/topic-registry.ts +20 -0
  197. package/src/sdk/host/query/revision-store.ts +36 -2
  198. package/src/sdk/host/reverse-leases.ts +44 -7
  199. package/src/sdk/prompt-status.ts +16 -3
  200. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  201. package/src/sdk/session.ts +89 -9
  202. package/src/session/agent-session.ts +844 -173
  203. package/src/session/artifacts.ts +22 -0
  204. package/src/session/auth-broker-config.ts +10 -3
  205. package/src/session/blob-store.ts +915 -24
  206. package/src/session/internal/managed-session-scope.ts +200 -111
  207. package/src/session/internal/managed-session-storage.ts +492 -62
  208. package/src/session/session-manager.ts +1614 -435
  209. package/src/session/session-storage.ts +38 -45
  210. package/src/session/streaming-output.ts +238 -68
  211. package/src/setup/credential-import.ts +75 -13
  212. package/src/skill-state/active-state.ts +32 -0
  213. package/src/skill-state/workflow-hud.ts +17 -0
  214. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  215. package/src/task/executor.ts +122 -39
  216. package/src/task/index.ts +280 -45
  217. package/src/task/output-manager.ts +24 -14
  218. package/src/task/provider-retry-status.ts +16 -0
  219. package/src/task/receipt.ts +34 -8
  220. package/src/task/render.ts +6 -2
  221. package/src/task/types.ts +108 -0
  222. package/src/tools/ask-answer-registry.ts +12 -6
  223. package/src/tools/ask.ts +86 -55
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash-interactive.ts +8 -3
  227. package/src/tools/bash.ts +503 -60
  228. package/src/tools/find.ts +1 -0
  229. package/src/tools/image-gen.ts +29 -5
  230. package/src/tools/index.ts +6 -0
  231. package/src/tools/output-meta.ts +108 -14
  232. package/src/tools/path-utils.ts +3 -2
  233. package/src/tools/read.ts +18 -0
  234. package/src/tools/review.ts +5 -7
  235. package/src/tools/search.ts +1 -0
  236. package/src/tools/subagent-render.ts +6 -2
  237. package/src/tools/subagent.ts +6 -0
  238. package/src/tools/telegram-send.ts +6 -3
  239. package/src/utils/gh.ts +39 -0
  240. package/src/utils/image-resize.ts +3 -0
  241. package/src/web/search/providers/anthropic.ts +4 -4
  242. package/src/web/search/providers/kimi.ts +15 -5
  243. package/src/web/search/providers/searxng.ts +47 -4
  244. package/src/web/search/providers/xai.ts +7 -2
  245. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  246. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  247. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -4,6 +4,7 @@ import * as path from "node:path";
4
4
  import { bindPluginMcpToPublicNetwork } from "../../runtime-mcp/plugin-network-boundary";
5
5
  import { loadCustomTools } from "../custom-tools/loader";
6
6
  import type { CustomTool } from "../custom-tools/types";
7
+ import { bundleIdentity } from "./lifecycle-reconciliation";
7
8
  import { loadEffectiveGjcPluginRegistry, registryPathForScope } from "./registry";
8
9
  import { type SessionQuarantine, type SessionValidationResult, validateSessionBundles } from "./session-validation";
9
10
  import type { GjcPluginRegistryEntry, GjcPluginScope } from "./types";
@@ -105,6 +106,7 @@ async function verifyEntryHashesCached(entry: GjcPluginRegistryEntry): Promise<S
105
106
  const snapshot = await snapshotExistingFile(abs);
106
107
  if (!snapshot) {
107
108
  return {
109
+ identity: bundleIdentity(entry.scope, entry.name),
108
110
  plugin: entry.name,
109
111
  surfaceId: `plugin:${entry.name}`,
110
112
  code: "runtime_mismatch",
@@ -113,6 +115,7 @@ async function verifyEntryHashesCached(entry: GjcPluginRegistryEntry): Promise<S
113
115
  }
114
116
  if ((await hashFile(snapshot)) !== file.sha256) {
115
117
  return {
118
+ identity: bundleIdentity(entry.scope, entry.name),
116
119
  plugin: entry.name,
117
120
  surfaceId: `plugin:${entry.name}`,
118
121
  code: "runtime_mismatch",
@@ -186,12 +189,16 @@ export async function loadAlwaysOnPluginTools(input: {
186
189
  );
187
190
 
188
191
  // Map declared (path -> name) for every active always-on tool surface.
189
- const declared = new Map<string, { name: string; plugin: string }>();
192
+ const declared = new Map<string, { name: string; plugin: string; scope: GjcPluginScope }>();
190
193
  for (const entry of active) {
191
194
  const disabled = new Set(entry.disabledSurfaceIds);
192
195
  for (const t of entry.surfaces.tools) {
193
196
  if (disabled.has(t.extensionId)) continue;
194
- declared.set(path.join(entry.pluginRoot, t.relativePath), { name: t.name, plugin: entry.name });
197
+ declared.set(path.join(entry.pluginRoot, t.relativePath), {
198
+ name: t.name,
199
+ plugin: entry.name,
200
+ scope: entry.scope,
201
+ });
195
202
  }
196
203
  }
197
204
  if (declared.size === 0) return { tools: [], quarantine };
@@ -218,6 +225,7 @@ export async function loadAlwaysOnPluginTools(input: {
218
225
  // Manifest is authoritative: exactly the one declared name must come back.
219
226
  if (returned.length !== 1 || returned[0] !== info.name) {
220
227
  quarantine.push({
228
+ identity: bundleIdentity(info.scope, info.plugin),
221
229
  plugin: info.plugin,
222
230
  surfaceId: `tool:${info.name}`,
223
231
  code: "runtime_mismatch",
@@ -228,6 +236,7 @@ export async function loadAlwaysOnPluginTools(input: {
228
236
  if (seenNames.has(info.name)) {
229
237
  // Defense in depth: never overwrite a reserved/earlier name.
230
238
  quarantine.push({
239
+ identity: bundleIdentity(info.scope, info.plugin),
231
240
  plugin: info.plugin,
232
241
  surfaceId: `tool:${info.name}`,
233
242
  code: "session_collision",
@@ -335,6 +344,7 @@ export async function buildPluginMcpConfigs(input: { cwd: string }): Promise<{
335
344
  }
336
345
  } catch (error) {
337
346
  quarantine.push({
347
+ identity: bundleIdentity(entry.scope, entry.name),
338
348
  plugin: entry.name,
339
349
  surfaceId: m.extensionId,
340
350
  code: "security_policy",
@@ -0,0 +1,121 @@
1
+ import { identityEquals, identityKey } from "./lifecycle-reconciliation";
2
+ import type { GjcBundleIdentity, GjcRuntimeFinding, GjcRuntimeSnapshot, GjcRuntimeSnapshotState } from "./types";
3
+
4
+ /**
5
+ * Deterministic numeric activation generation for an activation fingerprint.
6
+ *
7
+ * Equal activation inputs yield an equal generation, so a consumer holding a
8
+ * snapshot can tell whether it still describes the state it is rendering.
9
+ * Derived from the fingerprint's leading hex so it stays inside the safe
10
+ * integer range.
11
+ */
12
+ export function gjcActivationGenerationFor(activationFingerprint: string): number {
13
+ const parsed = Number.parseInt(activationFingerprint.slice(0, 13), 16);
14
+ return Number.isSafeInteger(parsed) ? parsed : 0;
15
+ }
16
+
17
+ /**
18
+ * Caller-owned accumulator for scope-qualified runtime evidence.
19
+ *
20
+ * Producers (loaders, adapters, validators) hand findings to an accumulator
21
+ * they were given; they never publish. Exactly one coordinator publishes a
22
+ * complete generation snapshot, and consumers merge it only when the identity
23
+ * and generation match.
24
+ */
25
+ export class GjcRuntimeFindingAccumulator {
26
+ private readonly findings: GjcRuntimeFinding[] = [];
27
+
28
+ constructor(readonly generation: number) {}
29
+
30
+ add(finding: GjcRuntimeFinding): void {
31
+ this.findings.push(finding);
32
+ }
33
+
34
+ addAll(findings: readonly GjcRuntimeFinding[]): void {
35
+ for (const finding of findings) this.add(finding);
36
+ }
37
+
38
+ /** Sorted, de-duplicated snapshot for the generation this accumulator owns. */
39
+ snapshot(): GjcRuntimeSnapshot {
40
+ const seen = new Set<string>();
41
+ const unique: GjcRuntimeFinding[] = [];
42
+ for (const finding of this.findings) {
43
+ const key = [identityKey(finding.identity), finding.surfaceId, finding.code, finding.message].join("\u0000");
44
+ if (seen.has(key)) continue;
45
+ seen.add(key);
46
+ unique.push(finding);
47
+ }
48
+ unique.sort((a, b) => {
49
+ const ka = `${identityKey(a.identity)}\u0000${a.surfaceId}\u0000${a.code}`;
50
+ const kb = `${identityKey(b.identity)}\u0000${b.surfaceId}\u0000${b.code}`;
51
+ return ka.localeCompare(kb);
52
+ });
53
+ return { generation: this.generation, findings: unique };
54
+ }
55
+ }
56
+
57
+ /** Read-only view of the most recently published complete generation. */
58
+ export interface GjcRuntimeSnapshotProvider {
59
+ current(): GjcRuntimeSnapshotState;
60
+ }
61
+
62
+ /**
63
+ * Single-writer publisher for runtime evidence.
64
+ *
65
+ * Passes can overlap: the session's prompt rebuild is re-entrant, so a second
66
+ * pass may start while a first is still awaiting its producers. Publication is
67
+ * therefore fenced by a monotonic epoch. A pass reserves an epoch when it
68
+ * begins, which immediately retires whatever was published before, and its
69
+ * later publish is accepted only if no newer pass has reserved since. A slow or
70
+ * failed older pass can never overwrite a newer one, and an incomplete pass
71
+ * simply never publishes, leaving consumers at `unavailable`.
72
+ */
73
+ export class GjcRuntimeSnapshotStore implements GjcRuntimeSnapshotProvider {
74
+ private state: GjcRuntimeSnapshotState = { status: "unavailable" };
75
+ private epoch = 0;
76
+
77
+ /**
78
+ * Begin a pass. Retires the current snapshot and returns the epoch token the
79
+ * caller must present to publish.
80
+ */
81
+ beginPass(): number {
82
+ this.epoch += 1;
83
+ this.state = { status: "unavailable" };
84
+ return this.epoch;
85
+ }
86
+
87
+ /** Publish only if `epoch` is still the newest reserved pass. */
88
+ publish(snapshot: GjcRuntimeSnapshot, epoch?: number): void {
89
+ if (epoch !== undefined && epoch !== this.epoch) return;
90
+ this.state = { status: "current", snapshot };
91
+ }
92
+
93
+ invalidate(): void {
94
+ this.state = { status: "unavailable" };
95
+ }
96
+
97
+ current(): GjcRuntimeSnapshotState {
98
+ return this.state;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Findings for one bundle, but only when the snapshot is current AND describes
104
+ * the exact generation the consumer is rendering. Missing provider, mismatched
105
+ * generation, or unavailable state resolve to `unavailable` — never to a
106
+ * silently empty "clear" result.
107
+ */
108
+ export function findingsForBundle(
109
+ provider: GjcRuntimeSnapshotProvider | undefined,
110
+ identity: GjcBundleIdentity,
111
+ expectedGeneration: number,
112
+ ): { status: "unavailable" } | { status: "current"; findings: GjcRuntimeFinding[] } {
113
+ if (!provider) return { status: "unavailable" };
114
+ const state = provider.current();
115
+ if (state.status !== "current") return { status: "unavailable" };
116
+ if (state.snapshot.generation !== expectedGeneration) return { status: "unavailable" };
117
+ return {
118
+ status: "current",
119
+ findings: state.snapshot.findings.filter(f => identityEquals(f.identity, identity)),
120
+ };
121
+ }
@@ -5,6 +5,7 @@ import {
5
5
  type GjcPluginAppendixManifestEntry,
6
6
  type GjcPluginHookManifestEntry,
7
7
  GjcPluginLoadError,
8
+ type GjcPluginLoadErrorCode,
8
9
  type GjcPluginManifest,
9
10
  type GjcPluginMcpManifestEntry,
10
11
  type GjcPluginMcpTransport,
@@ -53,6 +54,67 @@ function requireNonEmptyString(value: unknown, field: string, filePath: string):
53
54
  return value;
54
55
  }
55
56
 
57
+ /**
58
+ * A bundle name is echoed by the CLI, rendered in Settings, and used to derive
59
+ * a directory segment, so it is constrained at the parse boundary rather than
60
+ * sanitized at every display site. Anything outside this set — control or ANSI
61
+ * sequences, path separators, whitespace, or credential-looking text — is
62
+ * rejected before it can ever be stored.
63
+ */
64
+ function manifestSafeName(
65
+ value: unknown,
66
+ field: string,
67
+ manifestPath: string,
68
+ code: GjcPluginLoadErrorCode = "invalid_manifest",
69
+ ): string {
70
+ const name =
71
+ code === "invalid_frontmatter"
72
+ ? requireNonEmptyString(value, field, manifestPath)
73
+ : manifestString(value, field, manifestPath);
74
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(name)) {
75
+ throw new GjcPluginLoadError(
76
+ code,
77
+ `GJC plugin ${field} must be 1-128 characters of letters, digits, dot, underscore, or hyphen (${manifestPath})`,
78
+ );
79
+ }
80
+ return name;
81
+ }
82
+
83
+ /**
84
+ * Free-form prose that is rendered into prompts or UI. It is not constrained to
85
+ * the identifier grammar, but control and ANSI characters are rejected so a
86
+ * manifest cannot inject escape sequences into a rendered surface.
87
+ */
88
+ function manifestSafeProse(
89
+ value: unknown,
90
+ field: string,
91
+ manifestPath: string,
92
+ code: GjcPluginLoadErrorCode = "invalid_manifest",
93
+ ): string {
94
+ const text = requireNonEmptyString(value, field, manifestPath);
95
+ // C0, DEL, and the C1 block: a single-byte CSI (U+009B) is an escape
96
+ // introducer on its own, so rejecting only C0 leaves the same injection open.
97
+ if (/[\u0000-\u001f\u007f-\u009f]/.test(text)) {
98
+ throw new GjcPluginLoadError(code, `GJC plugin ${field} must not contain control characters (${manifestPath})`);
99
+ }
100
+ return text;
101
+ }
102
+
103
+ /**
104
+ * A version string is rendered next to the bundle name everywhere the bundle
105
+ * appears, so it is constrained to printable version-like characters.
106
+ */
107
+ function manifestSafeVersion(value: unknown, manifestPath: string): string {
108
+ const version = manifestString(value, "version", manifestPath);
109
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,63}$/.test(version)) {
110
+ throw new GjcPluginLoadError(
111
+ "invalid_manifest",
112
+ `GJC plugin version must be 1-64 characters of letters, digits, dot, plus, underscore, or hyphen (${manifestPath})`,
113
+ );
114
+ }
115
+ return version;
116
+ }
117
+
56
118
  function manifestString(value: unknown, field: string, manifestPath: string): string {
57
119
  if (typeof value !== "string" || value.trim().length === 0) {
58
120
  throw new GjcPluginLoadError(
@@ -109,12 +171,12 @@ function parseTools(value: unknown, manifestPath: string): GjcPluginToolManifest
109
171
  `Invalid GJC plugin manifest at ${manifestPath}: tools[${index}] must be a string or object`,
110
172
  );
111
173
  }
112
- const name = manifestString(entry.name, `tools[${index}].name`, manifestPath);
174
+ const name = manifestSafeName(entry.name, `tools[${index}].name`, manifestPath);
113
175
  const path = manifestString(entry.path, `tools[${index}].path`, manifestPath);
114
176
  const description =
115
177
  entry.description === undefined
116
178
  ? undefined
117
- : manifestString(entry.description, `tools[${index}].description`, manifestPath);
179
+ : manifestSafeProse(entry.description, `tools[${index}].description`, manifestPath);
118
180
  const sha256 =
119
181
  entry.sha256 === undefined ? undefined : manifestString(entry.sha256, `tools[${index}].sha256`, manifestPath);
120
182
  return { name, path, description, sha256, surface: "always-on" };
@@ -130,11 +192,15 @@ function parseHooks(value: unknown, manifestPath: string): GjcPluginHookManifest
130
192
  `Invalid GJC plugin manifest at ${manifestPath}: hooks[${index}] must be an object`,
131
193
  );
132
194
  }
133
- const name = manifestString(entry.name, `hooks[${index}].name`, manifestPath);
134
- const event = manifestString(entry.event, `hooks[${index}].event`, manifestPath);
195
+ const name = manifestSafeName(entry.name, `hooks[${index}].name`, manifestPath);
196
+ // event/target become part of the hook surface ID
197
+ // (`hook:<event>:<phase>:<target>:<name>`), which is rendered and printed.
198
+ const event = manifestSafeName(entry.event, `hooks[${index}].event`, manifestPath);
135
199
  const path = manifestString(entry.path, `hooks[${index}].path`, manifestPath);
136
200
  const target =
137
- entry.target === undefined ? undefined : manifestString(entry.target, `hooks[${index}].target`, manifestPath);
201
+ entry.target === undefined
202
+ ? undefined
203
+ : manifestSafeName(entry.target, `hooks[${index}].target`, manifestPath);
138
204
  let phase: "before" | "after" | undefined;
139
205
  if (entry.phase !== undefined) {
140
206
  if (entry.phase !== "before" && entry.phase !== "after") {
@@ -160,7 +226,7 @@ function parseMcps(value: unknown, manifestPath: string): GjcPluginMcpManifestEn
160
226
  `Invalid GJC plugin manifest at ${manifestPath}: mcps[${index}] must be an object`,
161
227
  );
162
228
  }
163
- const name = manifestString(entry.name, `mcps[${index}].name`, manifestPath);
229
+ const name = manifestSafeName(entry.name, `mcps[${index}].name`, manifestPath);
164
230
  const transport = entry.transport;
165
231
  if (typeof transport !== "string" || !MCP_TRANSPORTS.includes(transport as GjcPluginMcpTransport)) {
166
232
  throw new GjcPluginLoadError(
@@ -207,7 +273,7 @@ function parseAppendixEntry(entry: unknown, field: string, manifestPath: string)
207
273
  `Invalid GJC plugin manifest at ${manifestPath}: ${field} must be an object`,
208
274
  );
209
275
  }
210
- const name = manifestString(entry.name, `${field}.name`, manifestPath);
276
+ const name = manifestSafeName(entry.name, `${field}.name`, manifestPath);
211
277
  const path = entry.path === undefined ? undefined : manifestString(entry.path, `${field}.path`, manifestPath);
212
278
  // Content may be empty/whitespace here; the compiler enforces non-empty and
213
279
  // maps emptiness to invalid_appendix (not invalid_manifest).
@@ -282,8 +348,8 @@ export function parseManifest(raw: unknown, manifestPath: string): GjcPluginMani
282
348
  );
283
349
  }
284
350
 
285
- const name = manifestString(raw.name, "name", manifestPath);
286
- const version = manifestString(raw.version, "version", manifestPath);
351
+ const name = manifestSafeName(raw.name, "name", manifestPath);
352
+ const version = manifestSafeVersion(raw.version, manifestPath);
287
353
 
288
354
  return {
289
355
  name,
@@ -300,10 +366,16 @@ export function parseManifest(raw: unknown, manifestPath: string): GjcPluginMani
300
366
 
301
367
  export function parseSubskillFrontmatter(fm: Record<string, unknown>, filePath: string): SubskillFrontmatter {
302
368
  return {
303
- name: requireNonEmptyString(fm.name, "name", filePath),
369
+ // Name and activation_arg become part of the surface ID
370
+ // (`subskill:<parent>:<phase>:<arg>`) and are rendered, so they share the
371
+ // identifier grammar. binds_to and phase are separately checked against
372
+ // the known parent/phase sets. The description is prose and may not be
373
+ // constrained to that grammar, but must not carry control characters into
374
+ // a rendered prompt.
375
+ name: manifestSafeName(fm.name, "name", filePath, "invalid_frontmatter"),
304
376
  binds_to: requireNonEmptyString(fm.binds_to, "binds_to", filePath),
305
377
  phase: requireNonEmptyString(fm.phase, "phase", filePath),
306
- activation_arg: requireNonEmptyString(fm.activation_arg, "activation_arg", filePath),
307
- description: requireNonEmptyString(fm.description, "description", filePath),
378
+ activation_arg: manifestSafeName(fm.activation_arg, "activation_arg", filePath, "invalid_frontmatter"),
379
+ description: manifestSafeProse(fm.description, "description", filePath, "invalid_frontmatter"),
308
380
  };
309
381
  }
@@ -1,7 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import type { GjcPluginLoadErrorCode, GjcPluginRegistryEntry } from "./types";
4
+ import { bundleIdentity, identityKey } from "./lifecycle-reconciliation";
5
+ import type { GjcBundleIdentity, GjcPluginLoadErrorCode, GjcPluginRegistryEntry } from "./types";
5
6
 
6
7
  /**
7
8
  * Session-start validation: the registry is the collision authority. Capability
@@ -22,6 +23,8 @@ export interface SessionCapabilityEvidence {
22
23
  }
23
24
 
24
25
  export interface SessionQuarantine {
26
+ /** Scope-qualified canonical target this finding belongs to. */
27
+ identity: GjcBundleIdentity;
25
28
  plugin: string;
26
29
  surfaceId: string;
27
30
  code: GjcPluginLoadErrorCode;
@@ -52,6 +55,7 @@ export async function verifyEntryHashes(entry: GjcPluginRegistryEntry): Promise<
52
55
  buf = await fs.readFile(abs);
53
56
  } catch {
54
57
  return {
58
+ identity: bundleIdentity(entry.scope, entry.name),
55
59
  plugin: entry.name,
56
60
  surfaceId: `plugin:${entry.name}`,
57
61
  code: "runtime_mismatch",
@@ -60,6 +64,7 @@ export async function verifyEntryHashes(entry: GjcPluginRegistryEntry): Promise<
60
64
  }
61
65
  if (sha256(buf) !== file.sha256) {
62
66
  return {
67
+ identity: bundleIdentity(entry.scope, entry.name),
63
68
  plugin: entry.name,
64
69
  surfaceId: `plugin:${entry.name}`,
65
70
  code: "runtime_mismatch",
@@ -103,7 +108,7 @@ export function validateSessionBundles(
103
108
  preQuarantined: readonly SessionQuarantine[] = [],
104
109
  ): SessionValidationResult {
105
110
  const quarantine: SessionQuarantine[] = [...preQuarantined];
106
- const quarantinedPlugins = new Set(preQuarantined.map(q => q.plugin));
111
+ const quarantinedPlugins = new Set(preQuarantined.map(q => identityKey(q.identity)));
107
112
 
108
113
  const seenTools = new Set<string>(evidence.toolNames ?? []);
109
114
  const seenMcps = new Set<string>(evidence.mcpNames ?? []);
@@ -113,12 +118,13 @@ export function validateSessionBundles(
113
118
  const active: GjcPluginRegistryEntry[] = [];
114
119
  for (const entry of entries) {
115
120
  if (!entry.enabled) continue; // user-disabled, not an error
116
- if (quarantinedPlugins.has(entry.name)) continue;
121
+ if (quarantinedPlugins.has(identityKey(bundleIdentity(entry.scope, entry.name)))) continue;
117
122
  const surfaces = activeSurfaceIds(entry);
118
123
  let collided = false;
119
124
  const recordCollision = (surfaceId: string, what: string): void => {
120
125
  collided = true;
121
126
  quarantine.push({
127
+ identity: bundleIdentity(entry.scope, entry.name),
122
128
  plugin: entry.name,
123
129
  surfaceId,
124
130
  code: "session_collision",
@@ -291,3 +291,144 @@ export interface GjcPluginRegistry {
291
291
  * disabledSurfaceIds, and quarantine bookkeeping.
292
292
  */
293
293
  export type GjcPluginSurfaceExtensionId = string;
294
+
295
+ /** Canonical GJC bundle identity: kind is fixed, target is (scope, name). */
296
+ export const GJC_BUNDLE_KIND = "gjc-bundle";
297
+
298
+ export interface GjcBundleIdentity {
299
+ kind: typeof GJC_BUNDLE_KIND;
300
+ scope: GjcPluginScope;
301
+ name: string;
302
+ }
303
+
304
+ /** Source descriptor exposed to CLI/Settings: never carries raw locator secrets. */
305
+ export interface GjcBundleSafeSource {
306
+ kind: GjcPluginSourceKind;
307
+ /** Redacted display locator (host + path only; no userinfo/query/fragment). */
308
+ display: string;
309
+ /** Conservative safe git ref, omitted when the stored value is unsafe. */
310
+ ref?: string;
311
+ /** Hex-only revision identifier, omitted when the stored value is unsafe. */
312
+ sha?: string;
313
+ resolvedAt: string;
314
+ /** True when this source kind supports re-resolution during update. */
315
+ updatable: boolean;
316
+ /** Present only when updatable is false. */
317
+ unsupportedReason?: string;
318
+ }
319
+
320
+ export interface GjcBundleSurfaceSummary {
321
+ extensionId: string;
322
+ kind: "tool" | "hook" | "mcp" | "system-appendix" | "agent-appendix" | "subskill";
323
+ name: string;
324
+ /** Persisted user intent (registry disabledSurfaceIds). */
325
+ enabled: boolean;
326
+ /** Deterministic quarantine derived from persisted registry state only. */
327
+ quarantined: boolean;
328
+ quarantineCode?: GjcPluginLoadErrorCode;
329
+ }
330
+
331
+ /** Installed-bundle DTO shared by CLI and Settings. Contains no raw locators. */
332
+ export interface GjcBundleSummary {
333
+ identity: GjcBundleIdentity;
334
+ version: string;
335
+ description?: string;
336
+ enabled: boolean;
337
+ source: GjcBundleSafeSource;
338
+ installedAt: string;
339
+ updatedAt: string;
340
+ manifestHash: string;
341
+ /** Deterministic fingerprint of the exact installed target. */
342
+ targetFingerprint: string;
343
+ surfaces: GjcBundleSurfaceSummary[];
344
+ /** True when any deterministic quarantine blocks enablement. */
345
+ quarantined: boolean;
346
+ }
347
+
348
+ /**
349
+ * Host-derived token binding an update preview to the exact candidate, the
350
+ * exact installed baseline, and the deterministic decision context. Apply is a
351
+ * compare-and-swap against all three fingerprints.
352
+ */
353
+ export interface GjcReviewedUpdateToken {
354
+ identity: GjcBundleIdentity;
355
+ candidateFingerprint: string;
356
+ baselineFingerprint: string;
357
+ decisionContextFingerprint: string;
358
+ reviewedAt: string;
359
+ }
360
+
361
+ export type GjcLifecycleErrorCode =
362
+ | "already_installed_use_upgrade"
363
+ | "not_installed"
364
+ | "identity_mismatch"
365
+ | "stale_candidate"
366
+ | "stale_baseline"
367
+ | "stale_decision_context"
368
+ | "source_unsupported"
369
+ | "source_unavailable"
370
+ | "quarantined"
371
+ | "surface_unknown"
372
+ | "invalid_target";
373
+
374
+ export interface GjcLifecycleError {
375
+ code: GjcLifecycleErrorCode;
376
+ /** Sanitized operator-facing message; never contains raw locators or causes. */
377
+ message: string;
378
+ /** Safe scoped recovery hint (e.g. the exact command to run instead). */
379
+ recovery?: string;
380
+ }
381
+
382
+ export type GjcLifecycleResult<T> = { ok: true; value: T } | { ok: false; error: GjcLifecycleError };
383
+
384
+ export interface GjcUpdatePreview {
385
+ identity: GjcBundleIdentity;
386
+ current: GjcBundleSummary;
387
+ candidateVersion: string;
388
+ candidateManifestHash: string;
389
+ /** Surface IDs added, removed, or retained by this candidate. */
390
+ addedSurfaceIds: string[];
391
+ removedSurfaceIds: string[];
392
+ retainedSurfaceIds: string[];
393
+ changed: boolean;
394
+ token: GjcReviewedUpdateToken;
395
+ }
396
+
397
+ export type GjcUpdateApplyStatus = "updated" | "unchanged";
398
+
399
+ export interface GjcUpdateApplyResult {
400
+ status: GjcUpdateApplyStatus;
401
+ summary: GjcBundleSummary;
402
+ /** Number of filesystem remnants that could not be removed after a successful swap. */
403
+ remnantCount: number;
404
+ }
405
+
406
+ export interface GjcInstallResult {
407
+ status: "installed";
408
+ summary: GjcBundleSummary;
409
+ }
410
+
411
+ export interface GjcToggleResult {
412
+ summary: GjcBundleSummary;
413
+ /** False when the requested state already matched (no persisted mutation). */
414
+ mutated: boolean;
415
+ }
416
+
417
+ /**
418
+ * Scope-qualified runtime evidence emitted by producers. Producers never
419
+ * publish; the session coordinator accumulates one complete generation.
420
+ */
421
+ export interface GjcRuntimeFinding {
422
+ identity: GjcBundleIdentity;
423
+ surfaceId: string;
424
+ code: GjcPluginLoadErrorCode;
425
+ message: string;
426
+ }
427
+
428
+ export interface GjcRuntimeSnapshot {
429
+ /** Monotonic activation generation this snapshot describes. */
430
+ generation: number;
431
+ findings: GjcRuntimeFinding[];
432
+ }
433
+
434
+ export type GjcRuntimeSnapshotState = { status: "unavailable" } | { status: "current"; snapshot: GjcRuntimeSnapshot };
@@ -88,6 +88,15 @@ export function validateInstallPlan(
88
88
  bundle: NormalizedGjcPluginBundle,
89
89
  effectiveEntries: readonly GjcPluginRegistryEntry[],
90
90
  ): void {
91
+ // Collision universe: the caller passes the effective registry across BOTH
92
+ // scopes, because surface IDs derive from the SURFACE name
93
+ // (`tool:<toolName>`, `mcp:<mcpName>`), not the bundle name. A differently
94
+ // named bundle in the opposite scope can therefore claim the same ID.
95
+ //
96
+ // Entries sharing this bundle's name are excluded in every scope: they are
97
+ // the same logical bundle being installed or replaced, and installing one
98
+ // bundle into both scopes is supported, so its own surfaces must not count
99
+ // as collisions against itself.
91
100
  const others = effectiveEntries.filter(e => e.name !== bundle.name);
92
101
 
93
102
  const toolNames = new Set<string>();
@@ -172,7 +172,7 @@ export interface TmuxLaunchPlan {
172
172
  /** Native tmux session identity emitted atomically by `new-session -P -F`. */
173
173
  createdSessionId?: string;
174
174
  /** Safe server identity proven immediately after creation. */
175
- createdServerIdentity?: { pid: number; startTime: string };
175
+ createdServerIdentity?: { pid: number; startTime: string; pidProven?: boolean };
176
176
  isPsmux: boolean;
177
177
  platform: NodeJS.Platform;
178
178
  }
@@ -793,7 +793,12 @@ function cleanupCreatedTmuxSession(
793
793
  if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, options, probe))
794
794
  throw new Error("gjc_tmux_exact_cleanup_uncertain");
795
795
  const nativeSessionId = plan.createdSessionId!;
796
- const expectedPid = plan.createdServerIdentity!.pid;
796
+ // Emit the `#{pid}` clause only when the server proof proved a PID. Non-Linux
797
+ // probes report a placeholder PID, and pinning `#{pid}` to it yields a
798
+ // predicate no live tmux server can satisfy, which turns every guarded
799
+ // cleanup on those platforms into `gjc_tmux_exact_cleanup_uncertain`.
800
+ const createdServer = plan.createdServerIdentity!;
801
+ const serverPidPredicate = createdServer.pidProven === false ? "1" : `#{==:#{pid},${createdServer.pid}}`;
797
802
  const guarded = spawnSync(
798
803
  plan.tmuxCommand,
799
804
  [
@@ -801,7 +806,7 @@ function cleanupCreatedTmuxSession(
801
806
  "-t",
802
807
  nativeSessionId,
803
808
  "-F",
804
- `#{&&:#{==:#{pid},${expectedPid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_name},${plan.sessionName}}}}`,
809
+ `#{&&:${serverPidPredicate},#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_name},${plan.sessionName}}}}`,
805
810
  `kill-session -t ${nativeSessionId} \\; display-message -p __gjc_tmux_guarded_cleanup_ok__`,
806
811
  "display-message -p __gjc_tmux_guarded_cleanup_refused__",
807
812
  ],
@@ -1141,7 +1146,13 @@ function defaultOwnerIsolationProbe(
1141
1146
  const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
1142
1147
  const probeServer = (): TmuxServerProof => {
1143
1148
  if (plan.platform !== "linux") {
1144
- return { state: "safe", pid: 1, startTime: "not-applicable", cgroup: { classification: "not_applicable" } };
1149
+ return {
1150
+ state: "safe",
1151
+ pid: 1,
1152
+ startTime: "not-applicable",
1153
+ cgroup: { classification: "not_applicable" },
1154
+ pidProven: false,
1155
+ };
1145
1156
  }
1146
1157
  const probe = spawn(plan.tmuxCommand, ["display-message", "-p", "#{pid}"], {
1147
1158
  cwd: plan.cwd,
@@ -1268,7 +1279,11 @@ function createIsolatedTmuxSession(
1268
1279
  isCurrentGeneration: () => isOwnerGenerationBaselineCurrentSync(stateDir, sessionId, baseline),
1269
1280
  cleanupSpawned: ({ nativeSessionId, server }) => {
1270
1281
  plan.createdSessionId = nativeSessionId;
1271
- plan.createdServerIdentity = { pid: server.pid!, startTime: server.startTime! };
1282
+ plan.createdServerIdentity = {
1283
+ pid: server.pid!,
1284
+ startTime: server.startTime!,
1285
+ pidProven: server.pidProven,
1286
+ };
1272
1287
  cleanupCreatedTmuxSessionAfterFailure(plan, spawn, options, probe, new Error("owner_generation_stale"));
1273
1288
  },
1274
1289
  });
@@ -1281,7 +1296,11 @@ function createIsolatedTmuxSession(
1281
1296
  return { exitCode: 1, stderr: outcome.diagnostic };
1282
1297
  }
1283
1298
  if (!plan.isPsmux && outcome.native_session_id) plan.createdSessionId = outcome.native_session_id;
1284
- plan.createdServerIdentity = { pid: outcome.server_pid, startTime: outcome.server_start_time };
1299
+ plan.createdServerIdentity = {
1300
+ pid: outcome.server_pid,
1301
+ startTime: outcome.server_start_time,
1302
+ pidProven: plan.platform === "linux" ? undefined : false,
1303
+ };
1285
1304
  return executed ?? { exitCode: 1, stderr: "tmux owner isolation did not execute" };
1286
1305
  }
1287
1306