@gajae-code/coding-agent 0.11.11 → 0.12.1

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 (344) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -0,0 +1,626 @@
1
+ import * as nodeFs from "node:fs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { type GjcBundleTransactionDecision, resolveGjcBundleCandidate, runGjcBundleTransaction } from "./installer";
5
+ import {
6
+ activationFingerprint,
7
+ baselineFingerprint,
8
+ bundleIdentity,
9
+ candidateFingerprint,
10
+ decisionContextFingerprint,
11
+ diffSurfaceIds,
12
+ identityEquals,
13
+ reconcileEnablement,
14
+ surfaceIdsOf,
15
+ targetFingerprint,
16
+ } from "./lifecycle-reconciliation";
17
+ import { readRegistry, sortRegistryEntries, withRegistryLock, writeRegistryUnlocked } from "./registry";
18
+ import type {
19
+ GjcBundleIdentity,
20
+ GjcBundleSafeSource,
21
+ GjcBundleSummary,
22
+ GjcBundleSurfaceSummary,
23
+ GjcInstallResult,
24
+ GjcLifecycleError,
25
+ GjcLifecycleResult,
26
+ GjcPluginRegistryEntry,
27
+ GjcPluginRegistrySource,
28
+ GjcPluginScope,
29
+ GjcReviewedUpdateToken,
30
+ GjcToggleResult,
31
+ GjcUpdateApplyResult,
32
+ GjcUpdatePreview,
33
+ } from "./types";
34
+ import { GJC_PLUGIN_MANIFEST_FILENAME, GjcPluginLoadError } from "./types";
35
+
36
+ /**
37
+ * GJC bundle lifecycle service.
38
+ *
39
+ * This module is the ONLY policy and persistence writer for GJC bundles: fresh
40
+ * install, update preview/apply, bundle enable/disable, and surface
41
+ * enable/disable. Callers (CLI, Settings) never touch the registry writers or
42
+ * the installer transaction directly.
43
+ */
44
+
45
+ export interface GjcLifecycleContext {
46
+ cwd: string;
47
+ }
48
+
49
+ function fail(code: GjcLifecycleError["code"], message: string, recovery?: string): GjcLifecycleError {
50
+ return recovery ? { code, message, recovery } : { code, message };
51
+ }
52
+
53
+ const UNSUPPORTED_UPDATE_REASON: Partial<Record<GjcPluginRegistrySource["kind"], string>> = {};
54
+
55
+ /**
56
+ * Redact a stored locator to a display form: host + path only. No userinfo,
57
+ * query, fragment, credentials, or parent-directory segments reach output.
58
+ */
59
+ const SAFE_LOCATOR_SEGMENT = /^[A-Za-z0-9._-]+$/;
60
+ const SAFE_HOST = /^[A-Za-z0-9.-]+$/;
61
+ const SAFE_REF = /^[A-Za-z0-9._/-]{1,128}$/;
62
+ const SAFE_SHA = /^[A-Fa-f0-9]{1,128}$/;
63
+
64
+ function safePathSegments(value: string): string[] {
65
+ return value
66
+ .replace(/\\/g, "/")
67
+ .split("/")
68
+ .flatMap(segment => {
69
+ try {
70
+ return [decodeURIComponent(segment)];
71
+ } catch {
72
+ return [];
73
+ }
74
+ })
75
+ .filter(segment => segment !== "." && !segment.includes("..") && SAFE_LOCATOR_SEGMENT.test(segment));
76
+ }
77
+
78
+ function displayPath(segments: string[]): string {
79
+ return segments.join("/").replace(/\.git$/i, "");
80
+ }
81
+
82
+ function safeRef(value: string | undefined): string | undefined {
83
+ if (value === undefined || !SAFE_REF.test(value) || value.startsWith("/") || value.includes("..")) return undefined;
84
+ return value;
85
+ }
86
+
87
+ function safeSha(value: string | undefined): string | undefined {
88
+ return value !== undefined && SAFE_SHA.test(value) ? value : undefined;
89
+ }
90
+
91
+ function localPathDisplay(value: string, fallback: string): string {
92
+ const segments = safePathSegments(value);
93
+ return segments.at(-1) ?? fallback;
94
+ }
95
+
96
+ export function redactSourceLocator(source: GjcPluginRegistrySource): string {
97
+ const unc = /^(?:\\\\|\/\/)(.+)$/.exec(source.uri);
98
+ if (unc) {
99
+ const [host, ...segments] = safePathSegments(unc[1] ?? "");
100
+ if (host && SAFE_HOST.test(host)) {
101
+ const safePath = displayPath(segments);
102
+ return safePath ? `${host}/${safePath}` : host;
103
+ }
104
+ return source.kind;
105
+ }
106
+
107
+ if (/^[A-Za-z]:[\\/]/.test(source.uri)) return localPathDisplay(source.uri, source.kind);
108
+ if (source.kind === "path" || /^(?:\.{1,2}[\\/]|[\\/])/.test(source.uri)) {
109
+ return localPathDisplay(source.uri, source.kind);
110
+ }
111
+
112
+ try {
113
+ const url = new URL(source.uri);
114
+ if (!SAFE_HOST.test(url.hostname)) return source.kind;
115
+ const safePath = displayPath(safePathSegments(url.pathname));
116
+ return safePath ? `${url.hostname}/${safePath}` : url.hostname;
117
+ } catch {
118
+ const scp = /^[^@/:]+@([A-Za-z0-9.-]+):(.+)$/.exec(source.uri);
119
+ if (scp) {
120
+ const safePath = displayPath(safePathSegments(scp[2] ?? ""));
121
+ return safePath ? `${scp[1]}/${safePath}` : (scp[1] ?? source.kind);
122
+ }
123
+ return source.kind;
124
+ }
125
+ }
126
+
127
+ function toSafeSource(source: GjcPluginRegistrySource): GjcBundleSafeSource {
128
+ const unsupportedReason = UNSUPPORTED_UPDATE_REASON[source.kind];
129
+ const safe: GjcBundleSafeSource = {
130
+ kind: source.kind,
131
+ display: redactSourceLocator(source),
132
+ resolvedAt: source.resolvedAt,
133
+ updatable: unsupportedReason === undefined,
134
+ };
135
+ const ref = safeRef(source.ref);
136
+ const sha = safeSha(source.sha);
137
+ if (ref !== undefined) safe.ref = ref;
138
+ if (sha !== undefined) safe.sha = sha;
139
+ if (unsupportedReason !== undefined) safe.unsupportedReason = unsupportedReason;
140
+ return safe;
141
+ }
142
+
143
+ function surfaceSummaries(entry: GjcPluginRegistryEntry): GjcBundleSurfaceSummary[] {
144
+ const disabled = new Set(entry.disabledSurfaceIds);
145
+ const quarantined = new Map((entry.quarantine ?? []).map(q => [q.surfaceId, q.code]));
146
+ const rows: GjcBundleSurfaceSummary[] = [
147
+ ...entry.surfaces.subskills.map(s => ({ extensionId: s.extensionId, kind: "subskill" as const, name: s.name })),
148
+ ...entry.surfaces.tools.map(t => ({ extensionId: t.extensionId, kind: "tool" as const, name: t.name })),
149
+ ...entry.surfaces.hooks.map(h => ({ extensionId: h.extensionId, kind: "hook" as const, name: h.name })),
150
+ ...entry.surfaces.mcps.map(m => ({ extensionId: m.extensionId, kind: "mcp" as const, name: m.name })),
151
+ ...entry.surfaces.systemAppendices.map(a => ({
152
+ extensionId: a.extensionId,
153
+ kind: "system-appendix" as const,
154
+ name: a.name,
155
+ })),
156
+ ...entry.surfaces.agentAppendices.map(a => ({
157
+ extensionId: a.extensionId,
158
+ kind: "agent-appendix" as const,
159
+ name: a.name,
160
+ })),
161
+ ].map(row => {
162
+ const code = quarantined.get(row.extensionId);
163
+ const summary: GjcBundleSurfaceSummary = {
164
+ ...row,
165
+ enabled: !disabled.has(row.extensionId),
166
+ quarantined: code !== undefined,
167
+ };
168
+ if (code !== undefined) summary.quarantineCode = code;
169
+ return summary;
170
+ });
171
+ return rows.sort((a, b) => a.extensionId.localeCompare(b.extensionId));
172
+ }
173
+
174
+ /** Safe, redacted DTO for one installed bundle. */
175
+ export function toBundleSummary(entry: GjcPluginRegistryEntry): GjcBundleSummary {
176
+ const surfaces = surfaceSummaries(entry);
177
+ return {
178
+ identity: bundleIdentity(entry.scope, entry.name),
179
+ version: entry.version,
180
+ enabled: entry.enabled,
181
+ source: toSafeSource(entry.source),
182
+ installedAt: entry.installedAt,
183
+ updatedAt: entry.updatedAt,
184
+ manifestHash: entry.manifestHash,
185
+ targetFingerprint: targetFingerprint(entry),
186
+ surfaces,
187
+ quarantined: surfaces.some(s => s.quarantined),
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Rebuild the exact locator an update must re-resolve from. A git ref is stored
193
+ * separately from the URI, so re-resolving the bare URI would silently drop the
194
+ * reviewed branch or tag and update from the default branch instead.
195
+ */
196
+ function storedSourceLocator(source: GjcPluginRegistrySource): string {
197
+ return source.kind === "git" && source.ref ? `${source.uri}#${source.ref}` : source.uri;
198
+ }
199
+
200
+ /** Exposed for locator-reconstruction tests; not part of the lifecycle API. */
201
+ export const storedSourceLocatorForTest = storedSourceLocator;
202
+
203
+ /** Exposed so a test can pin parity with the installer's source predicates. */
204
+ export const isLocalDirectorySourceForTest = isLocalDirectorySource;
205
+
206
+ async function readEffective(cwd: string): Promise<GjcPluginRegistryEntry[]> {
207
+ const [user, project] = await Promise.all([readRegistry("user", cwd), readRegistry("project", cwd)]);
208
+ return sortRegistryEntries([...user.plugins, ...project.plugins]);
209
+ }
210
+
211
+ /** All installed bundles across both scopes, deterministically ordered. */
212
+ export async function listGjcBundles(ctx: GjcLifecycleContext): Promise<GjcBundleSummary[]> {
213
+ return (await readEffective(ctx.cwd)).map(toBundleSummary);
214
+ }
215
+
216
+ /** One bundle by exact (scope, name) identity. Opposite scope never matches. */
217
+ export async function getGjcBundle(
218
+ ctx: GjcLifecycleContext,
219
+ identity: GjcBundleIdentity,
220
+ ): Promise<GjcLifecycleResult<GjcBundleSummary>> {
221
+ const registry = await readRegistry(identity.scope, ctx.cwd);
222
+ const entry = registry.plugins.find(p => p.name === identity.name);
223
+ if (!entry) return { ok: false, error: notInstalled(identity) };
224
+ return { ok: true, value: toBundleSummary(entry) };
225
+ }
226
+
227
+ function notInstalled(identity: GjcBundleIdentity): GjcLifecycleError {
228
+ return fail(
229
+ "not_installed",
230
+ `GJC bundle "${identity.name}" is not installed in the ${identity.scope} scope`,
231
+ `gjc plugin install <source> --${identity.scope}`,
232
+ );
233
+ }
234
+
235
+ function alreadyInstalled(name: string, scope: GjcPluginScope): GjcLifecycleError {
236
+ return fail(
237
+ "already_installed_use_upgrade",
238
+ `GJC bundle "${name}" is already installed in the ${scope} scope`,
239
+ `gjc plugin upgrade ${name} --${scope}`,
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Run a source-resolving operation and convert a resolution failure into the
245
+ * typed `source_unavailable` result the lifecycle contract promises.
246
+ *
247
+ * Re-resolution reaches the network and the filesystem, so it can throw with a
248
+ * message carrying the stored locator. Letting that escape would both crash the
249
+ * CLI with an unhandled rejection and echo an absolute path, which is the exact
250
+ * leak class the safe DTOs exist to prevent.
251
+ */
252
+ async function withSourceAvailability<T>(
253
+ identity: GjcBundleIdentity,
254
+ run: () => Promise<GjcLifecycleResult<T>>,
255
+ ): Promise<GjcLifecycleResult<T>> {
256
+ try {
257
+ return await run();
258
+ } catch (error) {
259
+ // Only a source-resolution failure becomes `source_unavailable`. A
260
+ // programming bug, an out-of-memory, or a write/rollback fault must keep
261
+ // propagating: mislabelling those as an unreachable source would hide real
262
+ // failures behind a benign-looking, retryable error.
263
+ if (!(error instanceof GjcPluginLoadError)) throw error;
264
+ return {
265
+ ok: false,
266
+ error: fail(
267
+ "source_unavailable",
268
+ `The stored source for GJC bundle "${identity.name}" could not be resolved`,
269
+ `gjc plugin install <source> --${identity.scope}`,
270
+ ),
271
+ };
272
+ }
273
+ }
274
+
275
+ /**
276
+ * True only for strings the installer would treat as a local path rather than
277
+ * a remote locator. This deliberately mirrors the installer's own `looksLikeGit`
278
+ * predicate: if the two ever disagreed, a remote locator could be read as a
279
+ * relative path and shadowed by a local directory of the same shape.
280
+ */
281
+ function isLocalDirectorySource(source: string): boolean {
282
+ // Mirrors installer `looksLikeGit`.
283
+ if (/^(https?|ssh|git):\/\//i.test(source)) return false;
284
+ if (/^git@/.test(source)) return false;
285
+ if (source.startsWith("git:")) return false;
286
+ // Mirrors installer `isTarball`: a local archive is extracted, not read in
287
+ // place, so its manifest is not at `<source>/gajae-plugin.json`.
288
+ if (/\.(tgz|tar\.gz|tar)$/i.test(source)) return false;
289
+ // Any other scheme-qualified locator is likewise not a local directory.
290
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(source)) return false;
291
+ // scp-style `user@host:path` and bare `host:port/path` forms are remote.
292
+ if (/^[^@/\s]+@[^:/\s]+:/.test(source)) return false;
293
+ if (/^[^/\s:]+:\d+\//.test(source)) return false;
294
+ return true;
295
+ }
296
+
297
+ /** Largest manifest this preflight will read before giving up. */
298
+ const PREFLIGHT_MANIFEST_MAX_BYTES = 64 * 1024;
299
+
300
+ /**
301
+ * Bundle name declared by a local directory source, read without resolving or
302
+ * compiling it so a create-only refusal does not depend on the source being
303
+ * fetchable. Returns undefined for anything that is not a plain local
304
+ * directory, for symlinked or oversized manifests, and for unreadable or
305
+ * malformed content, in which case the caller falls through to the
306
+ * transaction's own pre-lock preflight.
307
+ *
308
+ * This can only cause a refusal, never a commit, and the locked decision
309
+ * re-derives the identity from the compiled bundle.
310
+ */
311
+ async function declaredBundleName(source: string): Promise<string | undefined> {
312
+ // Classify the locator BEFORE touching the filesystem. On POSIX a remote
313
+ // locator like `https://host/repo` is also a valid relative path
314
+ // (`https:/host/repo`), so statting first would let a locally created
315
+ // directory shadow a remote source and refuse an install that should have
316
+ // resolved remotely.
317
+ if (!isLocalDirectorySource(source)) return undefined;
318
+ let handle: fs.FileHandle | undefined;
319
+ try {
320
+ const dir = await fs.stat(source);
321
+ if (!dir.isDirectory()) return undefined;
322
+ const manifestPath = path.join(source, GJC_PLUGIN_MANIFEST_FILENAME);
323
+ // Open without following a final symlink, then stat the OPEN handle so a
324
+ // concurrent rename cannot swap in a symlink or an oversized file between
325
+ // the check and the read.
326
+ handle = await fs.open(manifestPath, nodeFs.constants.O_RDONLY | nodeFs.constants.O_NOFOLLOW);
327
+ const manifest = await handle.stat();
328
+ if (!manifest.isFile() || manifest.size > PREFLIGHT_MANIFEST_MAX_BYTES) return undefined;
329
+ const parsed: unknown = JSON.parse(await handle.readFile("utf8"));
330
+ const name = (parsed as { name?: unknown }).name;
331
+ return typeof name === "string" && name.length > 0 ? name : undefined;
332
+ } catch {
333
+ return undefined;
334
+ } finally {
335
+ await handle?.close().catch(() => {});
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Fresh install only. An existing target in the same scope is create-only and
341
+ * is refused identically with or without force; upgrading is a separate,
342
+ * scope-qualified operation.
343
+ */
344
+ export async function installGjcBundle(
345
+ ctx: GjcLifecycleContext,
346
+ scope: GjcPluginScope,
347
+ source: string,
348
+ ): Promise<GjcLifecycleResult<GjcInstallResult>> {
349
+ // A create-only refusal must not depend on the source being reachable, so
350
+ // identify the target before resolving anything. Only the declared manifest
351
+ // name can do that: it IS the canonical identity component.
352
+ //
353
+ // A stored-locator match deliberately does NOT qualify. One locator can
354
+ // resolve to different content over time, and the same URI can back two
355
+ // differently named bundles, so matching on it would refuse installs that
356
+ // should proceed. When the name cannot be read the transaction's own
357
+ // pre-lock preflight refuses after resolving.
358
+ const declared = await declaredBundleName(source);
359
+ if (declared) {
360
+ const registry = await readRegistry(scope, ctx.cwd);
361
+ const existing = registry.plugins.find(p => p.name === declared);
362
+ if (existing) return { ok: false, error: alreadyInstalled(existing.name, scope) };
363
+ }
364
+
365
+ const result = await runGjcBundleTransaction(source, {
366
+ scope,
367
+ cwd: ctx.cwd,
368
+ decide: async ({ existing, candidate }): Promise<GjcBundleTransactionDecision> => {
369
+ if (existing) {
370
+ return {
371
+ kind: "abort",
372
+ error: fail(
373
+ "already_installed_use_upgrade",
374
+ `GJC bundle "${existing.name}" is already installed in the ${scope} scope`,
375
+ `gjc plugin upgrade ${existing.name} --${scope}`,
376
+ ),
377
+ };
378
+ }
379
+ return { kind: "commit", entry: candidate };
380
+ },
381
+ });
382
+ if (result.status === "aborted") return { ok: false, error: result.error };
383
+ return { ok: true, value: { status: "installed", summary: toBundleSummary(result.entry) } };
384
+ }
385
+
386
+ /**
387
+ * Re-resolve the stored source descriptor and describe what an update would do.
388
+ * The returned token binds the candidate, the exact installed baseline, and the
389
+ * deterministic decision context; apply is a compare-and-swap on all three.
390
+ */
391
+ export async function previewGjcBundleUpdate(
392
+ ctx: GjcLifecycleContext,
393
+ identity: GjcBundleIdentity,
394
+ ): Promise<GjcLifecycleResult<GjcUpdatePreview>> {
395
+ const registry = await readRegistry(identity.scope, ctx.cwd);
396
+ const entry = registry.plugins.find(p => p.name === identity.name);
397
+ if (!entry) return { ok: false, error: notInstalled(identity) };
398
+ const safeSource = toSafeSource(entry.source);
399
+ if (!safeSource.updatable) {
400
+ return {
401
+ ok: false,
402
+ error: fail(
403
+ "source_unsupported",
404
+ `GJC bundle "${identity.name}" was installed from a ${entry.source.kind} source that cannot be re-resolved`,
405
+ ),
406
+ };
407
+ }
408
+
409
+ const effective = await readEffective(ctx.cwd);
410
+ // Re-resolution reaches the network and the filesystem, so it can throw with
411
+ // a cause carrying the raw locator. Convert that into the typed
412
+ // `source_unavailable` result the contract promises, rather than letting an
413
+ // exception escape and echo the stored path.
414
+ return await withSourceAvailability(identity, async () =>
415
+ resolveGjcBundleCandidate(storedSourceLocator(entry.source), async ({ bundle }) => {
416
+ if (bundle.name !== entry.name) {
417
+ return {
418
+ ok: false as const,
419
+ error: fail(
420
+ "identity_mismatch",
421
+ `Source now declares "${bundle.name}" but "${entry.name}" is installed; install the new bundle and uninstall the old one`,
422
+ `gjc plugin install <source> --${identity.scope}`,
423
+ ),
424
+ };
425
+ }
426
+ const candidateIds = surfaceIdsOf(bundle.surfaces);
427
+ const delta = diffSurfaceIds(surfaceIdsOf(entry.surfaces), candidateIds);
428
+ const candidateHash = candidateFingerprint(identity.scope, bundle);
429
+ const baselineHash = baselineFingerprint(entry);
430
+ const contextHash = decisionContextFingerprint(identity, effective);
431
+ const token: GjcReviewedUpdateToken = {
432
+ identity,
433
+ candidateFingerprint: candidateHash,
434
+ baselineFingerprint: baselineHash,
435
+ decisionContextFingerprint: contextHash,
436
+ reviewedAt: new Date().toISOString(),
437
+ };
438
+ return {
439
+ ok: true as const,
440
+ value: {
441
+ identity,
442
+ current: toBundleSummary(entry),
443
+ candidateVersion: bundle.version,
444
+ candidateManifestHash: bundle.manifestHash,
445
+ addedSurfaceIds: delta.addedSurfaceIds,
446
+ removedSurfaceIds: delta.removedSurfaceIds,
447
+ retainedSurfaceIds: delta.retainedSurfaceIds,
448
+ changed: candidateHash !== targetFingerprint(entry),
449
+ token,
450
+ },
451
+ };
452
+ }),
453
+ );
454
+ }
455
+
456
+ /**
457
+ * Apply a previously reviewed update. Any drift in the candidate bytes, the
458
+ * installed baseline, or the decision context returns a typed stale error with
459
+ * zero mutation.
460
+ */
461
+ export async function applyGjcBundleUpdate(
462
+ ctx: GjcLifecycleContext,
463
+ token: GjcReviewedUpdateToken,
464
+ ): Promise<GjcLifecycleResult<GjcUpdateApplyResult>> {
465
+ const identity = token.identity;
466
+ const registry = await readRegistry(identity.scope, ctx.cwd);
467
+ const entry = registry.plugins.find(p => p.name === identity.name);
468
+ if (!entry) return { ok: false, error: notInstalled(identity) };
469
+ if (!toSafeSource(entry.source).updatable) {
470
+ return {
471
+ ok: false,
472
+ error: fail(
473
+ "source_unsupported",
474
+ `GJC bundle "${identity.name}" was installed from a ${entry.source.kind} source that cannot be re-resolved`,
475
+ ),
476
+ };
477
+ }
478
+
479
+ return await withSourceAvailability<GjcUpdateApplyResult>(identity, async () => {
480
+ const result = await runGjcBundleTransaction(storedSourceLocator(entry.source), {
481
+ scope: identity.scope,
482
+ cwd: ctx.cwd,
483
+ decide: async ({ existing, effective, bundle, candidate }): Promise<GjcBundleTransactionDecision> => {
484
+ if (!existing) return { kind: "abort", error: notInstalled(identity) };
485
+ if (
486
+ bundle.name !== existing.name ||
487
+ !identityEquals(bundleIdentity(identity.scope, bundle.name), identity)
488
+ ) {
489
+ return {
490
+ kind: "abort",
491
+ error: fail(
492
+ "identity_mismatch",
493
+ `Source now declares "${bundle.name}" but "${existing.name}" is installed; install the new bundle and uninstall the old one`,
494
+ `gjc plugin install <source> --${identity.scope}`,
495
+ ),
496
+ };
497
+ }
498
+ const candidateHash = candidateFingerprint(identity.scope, bundle);
499
+ if (candidateHash !== token.candidateFingerprint) {
500
+ return {
501
+ kind: "abort",
502
+ error: fail("stale_candidate", "The source changed since it was reviewed; preview the update again"),
503
+ };
504
+ }
505
+ const baselineHash = baselineFingerprint(existing);
506
+ if (baselineHash !== token.baselineFingerprint) {
507
+ return {
508
+ kind: "abort",
509
+ error: fail(
510
+ "stale_baseline",
511
+ "The installed bundle changed since it was reviewed; preview the update again",
512
+ ),
513
+ };
514
+ }
515
+ const contextHash = decisionContextFingerprint(identity, effective);
516
+ if (contextHash !== token.decisionContextFingerprint) {
517
+ return {
518
+ kind: "abort",
519
+ error: fail(
520
+ "stale_decision_context",
521
+ "Installed bundles changed since the update was reviewed; preview the update again",
522
+ ),
523
+ };
524
+ }
525
+ if (candidateHash === targetFingerprint(existing)) return { kind: "noop", entry: existing };
526
+
527
+ // Quarantine is recomputed against the candidate, never carried forward,
528
+ // so a surface the update fixes is not left permanently blocked.
529
+ const reconciled = reconcileEnablement(existing.disabledSurfaceIds, surfaceIdsOf(bundle.surfaces));
530
+ const next: GjcPluginRegistryEntry = {
531
+ ...candidate,
532
+ enabled: existing.enabled,
533
+ installedAt: existing.installedAt,
534
+ disabledSurfaceIds: reconciled.disabledSurfaceIds,
535
+ };
536
+ if (reconciled.quarantine.length > 0) next.quarantine = reconciled.quarantine;
537
+ else delete next.quarantine;
538
+ return { kind: "commit", entry: next };
539
+ },
540
+ });
541
+
542
+ if (result.status === "aborted") return { ok: false, error: result.error };
543
+ if (result.status === "noop") {
544
+ return { ok: true, value: { status: "unchanged", summary: toBundleSummary(result.entry), remnantCount: 0 } };
545
+ }
546
+ return {
547
+ ok: true,
548
+ value: { status: "updated", summary: toBundleSummary(result.entry), remnantCount: result.remnants.length },
549
+ };
550
+ });
551
+ }
552
+
553
+ async function mutateEntry(
554
+ ctx: GjcLifecycleContext,
555
+ identity: GjcBundleIdentity,
556
+ mutate: (entry: GjcPluginRegistryEntry) => GjcLifecycleResult<GjcPluginRegistryEntry | null>,
557
+ ): Promise<GjcLifecycleResult<GjcToggleResult>> {
558
+ return await withRegistryLock(identity.scope, ctx.cwd, async () => {
559
+ const registry = await readRegistry(identity.scope, ctx.cwd);
560
+ const entry = registry.plugins.find(p => p.name === identity.name);
561
+ if (!entry) return { ok: false, error: notInstalled(identity) };
562
+ const outcome = mutate(entry);
563
+ if (!outcome.ok) return { ok: false, error: outcome.error };
564
+ if (outcome.value === null) return { ok: true, value: { summary: toBundleSummary(entry), mutated: false } };
565
+ const next = sortRegistryEntries([...registry.plugins.filter(p => p.name !== identity.name), outcome.value]);
566
+ await writeRegistryUnlocked({ version: 1, scope: identity.scope, plugins: next }, ctx.cwd);
567
+ return { ok: true, value: { summary: toBundleSummary(outcome.value), mutated: true } };
568
+ });
569
+ }
570
+
571
+ /**
572
+ * Enable or disable a whole bundle. Deterministic quarantine blocks enabling;
573
+ * disabling is always allowed so operators can always de-escalate.
574
+ */
575
+ export async function setGjcBundleEnabled(
576
+ ctx: GjcLifecycleContext,
577
+ identity: GjcBundleIdentity,
578
+ enabled: boolean,
579
+ ): Promise<GjcLifecycleResult<GjcToggleResult>> {
580
+ return await mutateEntry(ctx, identity, entry => {
581
+ if (enabled && (entry.quarantine?.length ?? 0) > 0) {
582
+ return {
583
+ ok: false,
584
+ error: fail("quarantined", `GJC bundle "${identity.name}" is quarantined and cannot be enabled`),
585
+ };
586
+ }
587
+ if (entry.enabled === enabled) return { ok: true, value: null };
588
+ return { ok: true, value: { ...entry, enabled, updatedAt: new Date().toISOString() } };
589
+ });
590
+ }
591
+
592
+ /** Enable or disable one surface of a bundle by its stable extension ID. */
593
+ export async function setGjcBundleSurfaceEnabled(
594
+ ctx: GjcLifecycleContext,
595
+ identity: GjcBundleIdentity,
596
+ surfaceId: string,
597
+ enabled: boolean,
598
+ ): Promise<GjcLifecycleResult<GjcToggleResult>> {
599
+ return await mutateEntry(ctx, identity, entry => {
600
+ if (!surfaceIdsOf(entry.surfaces).includes(surfaceId)) {
601
+ return {
602
+ ok: false,
603
+ error: fail("surface_unknown", `GJC bundle "${identity.name}" has no surface "${surfaceId}"`),
604
+ };
605
+ }
606
+ if (enabled && (entry.quarantine ?? []).some(q => q.surfaceId === surfaceId)) {
607
+ return {
608
+ ok: false,
609
+ error: fail("quarantined", `Surface "${surfaceId}" is quarantined and cannot be enabled`),
610
+ };
611
+ }
612
+ const disabled = new Set(entry.disabledSurfaceIds);
613
+ if (enabled ? !disabled.has(surfaceId) : disabled.has(surfaceId)) return { ok: true, value: null };
614
+ if (enabled) disabled.delete(surfaceId);
615
+ else disabled.add(surfaceId);
616
+ return {
617
+ ok: true,
618
+ value: { ...entry, disabledSurfaceIds: [...disabled].sort(), updatedAt: new Date().toISOString() },
619
+ };
620
+ });
621
+ }
622
+
623
+ /** Deterministic activation generation for the current persisted state. */
624
+ export async function currentActivationFingerprint(ctx: GjcLifecycleContext): Promise<string> {
625
+ return activationFingerprint(await readEffective(ctx.cwd));
626
+ }
@@ -33,8 +33,8 @@ function statusFor(
33
33
  quarantinedIds: Map<string, string>,
34
34
  ): { status: PluginSurfaceStatus; quarantineCode?: string } {
35
35
  const q =
36
- quarantinedIds.get(`${entry.name}\u0000${extensionId}`) ??
37
- quarantinedIds.get(`${entry.name}\u0000plugin:${entry.name}`);
36
+ quarantinedIds.get(`${entry.scope}\u0000${entry.name}\u0000${extensionId}`) ??
37
+ quarantinedIds.get(`${entry.scope}\u0000${entry.name}\u0000plugin:${entry.name}`);
38
38
  if (q) return { status: "quarantined", quarantineCode: q };
39
39
  if (!entry.enabled || entry.disabledSurfaceIds.includes(extensionId)) return { status: "disabled" };
40
40
  return { status: "enabled" };
@@ -76,7 +76,7 @@ export async function summarizeGjcPluginObservability(cwd: string): Promise<Plug
76
76
  }
77
77
  const { quarantine } = validateSessionBundles(effective, {}, preQuarantine);
78
78
  const quarantinedIds = new Map<string, string>();
79
- for (const q of quarantine) quarantinedIds.set(`${q.plugin}\u0000${q.surfaceId}`, q.code);
79
+ for (const q of quarantine) quarantinedIds.set(`${q.identity.scope}\u0000${q.plugin}\u0000${q.surfaceId}`, q.code);
80
80
 
81
81
  const surfaces: PluginSurfaceRow[] = [];
82
82
  for (const entry of effective) surfaces.push(...rowsForEntry(entry, quarantinedIds));
@@ -23,7 +23,11 @@ function sanitizeBody(text: string): string {
23
23
  // Strip control chars (except tab/newline), then XML-escape &, <, > so a
24
24
  // malicious body can NEVER emit a closing delimiter or fake <system>/
25
25
  // <developer>/<gjc-subskill> tag that escapes the lower-authority block.
26
- const stripped = text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, "");
26
+ //
27
+ // The set spans C0, DEL, and C1. Carriage return can rewrite a rendered line,
28
+ // and U+009B is a single-byte CSI that introduces an escape sequence without
29
+ // any preceding ESC, so omitting either leaves the same injection open.
30
+ const stripped = text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
27
31
  return stripped.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
28
32
  }
29
33