@gajae-code/coding-agent 0.16.7 → 0.17.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 (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -0,0 +1,514 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { type BigIntStats, constants } from "node:fs";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import {
6
+ exactReplacePath,
7
+ exactSwapManagedLink,
8
+ getDoctorLinkProtocolVersion,
9
+ type NativeExactFileIdentity,
10
+ verifyOwnerOnlyFdSecurity,
11
+ } from "@gajae-code/natives";
12
+ import { type DoctorFileObservation, readDoctorFile } from "./files";
13
+ import { candidateLinkId, linkTargetId, resolveDoctorRoot } from "./ids";
14
+ import { DoctorJournal, DoctorJournalCreateError } from "./journal";
15
+
16
+ export type ManagedLinkKind = "source" | "wrapper" | "binary";
17
+ export interface ManagedLinkCandidate {
18
+ readonly id: string;
19
+ readonly kind: ManagedLinkKind;
20
+ readonly path: string;
21
+ readonly exists: boolean;
22
+ readonly provenance: "workspace" | "unknown";
23
+ }
24
+ export interface ManagedLinkDescriptor {
25
+ readonly schemaVersion: 1;
26
+ readonly targetId: string;
27
+ readonly alias: string;
28
+ readonly targetPath: string;
29
+ readonly parentPath: string;
30
+ readonly status: "healthy" | "broken" | "foreign" | "unknown";
31
+ readonly receiptTrusted: boolean;
32
+ readonly candidates: readonly ManagedLinkCandidate[];
33
+ readonly reasonCode?: string;
34
+ }
35
+ export interface ManagedLinkRepairRequest {
36
+ readonly targetPath: string;
37
+ readonly alias: string;
38
+ readonly root: string;
39
+ readonly ref: string;
40
+ readonly mode: "dry-run" | "fix";
41
+ readonly allowRisks: readonly string[];
42
+ readonly yes: boolean;
43
+ readonly journalRoot?: string;
44
+ readonly runId?: string;
45
+ }
46
+ export interface ManagedLinkRepairResult {
47
+ readonly state:
48
+ | "planned"
49
+ | "verified"
50
+ | "not_needed"
51
+ | "blocked"
52
+ | "rolled_back"
53
+ | "rollback_conflict"
54
+ | "unsupported"
55
+ | "failed"
56
+ | "uncertain";
57
+ readonly changed: boolean;
58
+ readonly sideEffectStarted: boolean;
59
+ readonly reasonCode?: string;
60
+ }
61
+ interface ReceiptBody {
62
+ readonly version: 1;
63
+ readonly alias: string;
64
+ readonly target: string;
65
+ readonly root: string;
66
+ readonly source: string;
67
+ readonly parent: string;
68
+ readonly identity: { readonly dev: string; readonly ino: string };
69
+ }
70
+ interface LinkSnapshot {
71
+ readonly stat: BigIntStats;
72
+ readonly parent: BigIntStats;
73
+ readonly target: string;
74
+ }
75
+ interface CandidateSnapshot {
76
+ readonly stat: BigIntStats;
77
+ readonly candidate: ManagedLinkCandidate;
78
+ }
79
+ interface LinkAuthority {
80
+ readonly root: string;
81
+ readonly targetPath: string;
82
+ readonly alias: string;
83
+ /** Absent only when the managed alias name is unoccupied and the receipt still proves ownership. */
84
+ readonly link?: LinkSnapshot;
85
+ readonly parent: BigIntStats;
86
+ readonly receipt: Extract<DoctorFileObservation, { status: "read" }>;
87
+ readonly body: ReceiptBody;
88
+ readonly candidates: readonly CandidateSnapshot[];
89
+ }
90
+ const authorities = new WeakMap<ManagedLinkDescriptor, LinkAuthority>();
91
+ const REPAIR_ID = "install.repair-managed-link";
92
+ function digest(body: ReceiptBody): string {
93
+ return createHash("sha256").update(JSON.stringify(body)).digest("hex");
94
+ }
95
+ function receiptPath(target: string): string {
96
+ return `${target}.gjc-managed.json`;
97
+ }
98
+ function sameStat(a: BigIntStats, b: BigIntStats): boolean {
99
+ return (
100
+ a.dev === b.dev &&
101
+ a.ino === b.ino &&
102
+ a.nlink === b.nlink &&
103
+ a.size === b.size &&
104
+ a.mtimeNs === b.mtimeNs &&
105
+ a.mode === b.mode &&
106
+ a.uid === b.uid
107
+ );
108
+ }
109
+ async function linkSnapshot(target: string): Promise<LinkSnapshot | undefined> {
110
+ try {
111
+ const before = await fs.lstat(target, { bigint: true });
112
+ if (!before.isSymbolicLink()) return undefined;
113
+ const parent = await fs.lstat(path.dirname(target), { bigint: true });
114
+ if (!parent.isDirectory() || parent.isSymbolicLink()) return undefined;
115
+ const value = await fs.readlink(target);
116
+ const after = await fs.lstat(target, { bigint: true });
117
+ const parentAfter = await fs.lstat(path.dirname(target), { bigint: true });
118
+ if (
119
+ !sameStat(before, after) ||
120
+ before.ctimeNs !== after.ctimeNs ||
121
+ parent.dev !== parentAfter.dev ||
122
+ parent.ino !== parentAfter.ino
123
+ )
124
+ return undefined;
125
+ return { stat: after, parent, target: value };
126
+ } catch (error) {
127
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
128
+ throw error;
129
+ }
130
+ }
131
+ function parseReceipt(
132
+ observation: DoctorFileObservation,
133
+ target: string,
134
+ root: string,
135
+ alias: string,
136
+ ): ReceiptBody | undefined {
137
+ if (observation.status !== "read") return undefined;
138
+ let value: unknown;
139
+ try {
140
+ value = JSON.parse(observation.text);
141
+ } catch {
142
+ return undefined;
143
+ }
144
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
145
+ const record = value as Partial<ReceiptBody> & { auth?: unknown };
146
+ if (
147
+ record.version !== 1 ||
148
+ record.alias !== alias ||
149
+ record.target !== target ||
150
+ record.root !== root ||
151
+ record.parent !== path.dirname(target) ||
152
+ typeof record.source !== "string" ||
153
+ !record.identity ||
154
+ typeof record.identity.dev !== "string" ||
155
+ typeof record.identity.ino !== "string"
156
+ )
157
+ return undefined;
158
+ const body: ReceiptBody = {
159
+ version: 1,
160
+ alias,
161
+ target,
162
+ root,
163
+ source: record.source,
164
+ parent: record.parent,
165
+ identity: record.identity,
166
+ };
167
+ return record.auth === digest(body) ? body : undefined;
168
+ }
169
+ function ownedReceipt(observation: DoctorFileObservation): boolean {
170
+ return (
171
+ observation.status === "read" &&
172
+ (observation.identity.mode & 0o077) === 0 &&
173
+ observation.identity.links === 1 &&
174
+ (process.getuid === undefined || observation.identity.owner === process.getuid())
175
+ );
176
+ }
177
+
178
+ /** Public values describe candidates; the private map retains the original filesystem authority. */
179
+ export async function describeManagedLink(
180
+ targetPath: string,
181
+ root: string,
182
+ alias = "gjc",
183
+ ): Promise<ManagedLinkDescriptor> {
184
+ const target = path.resolve(targetPath);
185
+ const workspace = path.resolve(root);
186
+ const parentPath = path.dirname(target);
187
+ const rootId = resolveDoctorRoot("install", parentPath).rootId;
188
+ const candidates: CandidateSnapshot[] = [];
189
+ const publicCandidates: ManagedLinkCandidate[] = [];
190
+ for (const [kind, relative] of [
191
+ ["source", "packages/coding-agent/src/cli.ts"],
192
+ ["wrapper", "packages/coding-agent/bin/gjc.js"],
193
+ ["binary", "packages/coding-agent/dist/gjc"],
194
+ ] as const) {
195
+ const candidatePath = path.join(workspace, relative);
196
+ let stat: BigIntStats | undefined;
197
+ try {
198
+ stat = await fs.lstat(candidatePath, { bigint: true });
199
+ } catch (error) {
200
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
201
+ }
202
+ const exists = stat?.isFile() === true && !stat.isSymbolicLink() && stat.nlink === 1n;
203
+ const candidate: ManagedLinkCandidate = {
204
+ id: candidateLinkId(rootId, kind, candidatePath),
205
+ kind,
206
+ path: candidatePath,
207
+ exists,
208
+ provenance: exists ? "workspace" : "unknown",
209
+ };
210
+ publicCandidates.push(candidate);
211
+ if (exists && stat) candidates.push({ stat, candidate });
212
+ }
213
+ const receipt = await readDoctorFile(receiptPath(target), 16 * 1024);
214
+ const body = parseReceipt(receipt, target, workspace, alias);
215
+ const link = await linkSnapshot(target);
216
+ const occupant = await fs.lstat(target).catch(() => undefined);
217
+ const parent = await fs.lstat(parentPath, { bigint: true }).catch(() => undefined);
218
+ const trustedReceipt =
219
+ !!body && ownedReceipt(receipt) && publicCandidates.some(candidate => candidate.path === body.source);
220
+ const identityMatches =
221
+ !!body &&
222
+ !!link &&
223
+ body.identity.dev === link.stat.dev.toString() &&
224
+ body.identity.ino === link.stat.ino.toString();
225
+ const owned = trustedReceipt && (link ? identityMatches : occupant === undefined);
226
+ const recordedCandidate = body && publicCandidates.find(candidate => candidate.path === body.source);
227
+ let status: ManagedLinkDescriptor["status"] = "unknown";
228
+ let reasonCode: string | undefined = "ownership_receipt_untrusted";
229
+ if (owned && body) {
230
+ if (!link) {
231
+ status = "broken";
232
+ reasonCode = "target_missing";
233
+ } else if (path.resolve(parentPath, link.target) !== body.source) {
234
+ status = "broken";
235
+ reasonCode = "target_drifted_from_receipt";
236
+ } else if (!recordedCandidate?.exists) {
237
+ status = "broken";
238
+ reasonCode = "recorded_source_missing";
239
+ } else {
240
+ status = "healthy";
241
+ reasonCode = undefined;
242
+ }
243
+ } else if (occupant) status = "foreign";
244
+ else if (link) status = "foreign";
245
+ const descriptor: ManagedLinkDescriptor = {
246
+ schemaVersion: 1,
247
+ targetId: linkTargetId(rootId, alias),
248
+ alias,
249
+ targetPath: target,
250
+ parentPath,
251
+ status,
252
+ receiptTrusted: owned,
253
+ candidates: publicCandidates,
254
+ ...(reasonCode ? { reasonCode } : {}),
255
+ };
256
+ if (owned && body && receipt.status === "read" && parent)
257
+ authorities.set(descriptor, {
258
+ root: workspace,
259
+ targetPath: target,
260
+ alias,
261
+ link,
262
+ parent,
263
+ receipt,
264
+ body,
265
+ candidates,
266
+ });
267
+ return descriptor;
268
+ }
269
+
270
+ async function refreshReceipt(
271
+ authority: LinkAuthority,
272
+ parent: BigIntStats,
273
+ source: string,
274
+ published: LinkSnapshot,
275
+ ): Promise<void> {
276
+ const body: ReceiptBody = {
277
+ ...authority.body,
278
+ source,
279
+ identity: { dev: published.stat.dev.toString(), ino: published.stat.ino.toString() },
280
+ };
281
+ const text = `${JSON.stringify({ ...body, auth: digest(body) })}\n`;
282
+ const temporary = `${receiptPath(authority.targetPath)}.${randomUUID()}.tmp`;
283
+ const handle = await fs.open(
284
+ temporary,
285
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
286
+ 0o600,
287
+ );
288
+ let expected: NativeExactFileIdentity;
289
+ try {
290
+ await handle.writeFile(text, "utf8");
291
+ await handle.sync();
292
+ const stat = await handle.stat({ bigint: true });
293
+ expected = {
294
+ dev: stat.dev,
295
+ ino: stat.ino,
296
+ nlink: stat.nlink,
297
+ size: stat.size,
298
+ mtimeNs: stat.mtimeNs,
299
+ parentDev: parent.dev,
300
+ parentIno: parent.ino,
301
+ sha256: createHash("sha256").update(text).digest("hex"),
302
+ };
303
+ } finally {
304
+ await handle.close();
305
+ }
306
+ if (!exactReplacePath(temporary, receiptPath(authority.targetPath), expected, authority.receipt.exactIdentity).ok)
307
+ throw new Error("receipt_changed");
308
+ }
309
+
310
+ async function syncParent(parentPath: string, expected: BigIntStats): Promise<void> {
311
+ const parent = await fs.open(
312
+ parentPath,
313
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
314
+ );
315
+ try {
316
+ const stat = await parent.stat({ bigint: true });
317
+ if (stat.dev !== expected.dev || stat.ino !== expected.ino) throw new Error("parent_changed");
318
+ await parent.sync();
319
+ } finally {
320
+ await parent.close();
321
+ }
322
+ }
323
+
324
+ export async function repairManagedLink(
325
+ request: ManagedLinkRepairRequest,
326
+ descriptor: ManagedLinkDescriptor,
327
+ ): Promise<ManagedLinkRepairResult> {
328
+ const refuse = (reasonCode: string): ManagedLinkRepairResult => ({
329
+ state: "blocked",
330
+ changed: false,
331
+ sideEffectStarted: false,
332
+ reasonCode,
333
+ });
334
+ if (!request.allowRisks.includes("install-replace") || !request.yes) return refuse("authorization_missing");
335
+ if (!request.ref) return refuse("candidate_selection_missing");
336
+ const known = descriptor.candidates.find(candidate => candidate.id === request.ref);
337
+ if (!known) return refuse("candidate_selection_missing");
338
+ const authority = authorities.get(descriptor);
339
+ if (
340
+ !authority ||
341
+ path.resolve(request.targetPath) !== authority.targetPath ||
342
+ path.resolve(request.root) !== authority.root ||
343
+ request.alias !== authority.alias
344
+ )
345
+ return refuse(descriptor.reasonCode ?? "original_authority_missing");
346
+ const selected = authority.candidates.find(item => item.candidate.id === request.ref);
347
+ if (!selected) return refuse("candidate_unresolved");
348
+ if (request.mode === "dry-run") return { state: "planned", changed: false, sideEffectStarted: false };
349
+ const current = await linkSnapshot(authority.targetPath);
350
+ const receipt = await readDoctorFile(receiptPath(authority.targetPath), 16 * 1024);
351
+ const parent = await fs.lstat(descriptor.parentPath, { bigint: true }).catch(() => undefined);
352
+ const candidateNow = await fs.lstat(selected.candidate.path, { bigint: true }).catch(() => undefined);
353
+ const linkUnchanged = authority.link
354
+ ? !!current && sameStat(current.stat, authority.link.stat) && current.target === authority.link.target
355
+ : current === undefined && (await fs.lstat(authority.targetPath).catch(() => undefined)) === undefined;
356
+ if (
357
+ !linkUnchanged ||
358
+ !parent ||
359
+ parent.dev !== authority.parent.dev ||
360
+ parent.ino !== authority.parent.ino ||
361
+ receipt.status !== "read" ||
362
+ receipt.text !== authority.receipt.text ||
363
+ receipt.exactIdentity.dev !== authority.receipt.exactIdentity.dev ||
364
+ receipt.exactIdentity.ino !== authority.receipt.exactIdentity.ino ||
365
+ !candidateNow ||
366
+ !sameStat(candidateNow, selected.stat)
367
+ )
368
+ return refuse("identity_recheck_required");
369
+ if (getDoctorLinkProtocolVersion() !== 2) return refuse("native_boundary_unavailable");
370
+ const receiptHandle = await fs.open(
371
+ receiptPath(authority.targetPath),
372
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
373
+ );
374
+ try {
375
+ if (!verifyOwnerOnlyFdSecurity(receiptPath(authority.targetPath), "file", receiptHandle.fd).ok)
376
+ return refuse("receipt_security_unproven");
377
+ } finally {
378
+ await receiptHandle.close();
379
+ }
380
+ if (current && path.resolve(descriptor.parentPath, current.target) === selected.candidate.path)
381
+ return { state: "not_needed", changed: false, sideEffectStarted: false };
382
+ if (!request.journalRoot || !request.runId) return refuse("journal_identity_missing");
383
+ let journal: DoctorJournal;
384
+ try {
385
+ journal = await DoctorJournal.create(request.journalRoot, request.runId);
386
+ } catch (error) {
387
+ const started = error instanceof DoctorJournalCreateError ? error.sideEffectStarted : true;
388
+ return {
389
+ state: started ? "uncertain" : "blocked",
390
+ changed: false,
391
+ sideEffectStarted: started,
392
+ reasonCode: "journal_unavailable",
393
+ };
394
+ }
395
+ const staged = `${authority.targetPath}.doctor-stage-${request.runId}`;
396
+ const quarantine = `${authority.targetPath}.doctor-old-${request.runId}`;
397
+ let published: LinkSnapshot | undefined;
398
+ let retained = staged;
399
+ try {
400
+ await journal.append({
401
+ repairId: REPAIR_ID,
402
+ targetId: descriptor.targetId,
403
+ phase: "before",
404
+ before: current
405
+ ? {
406
+ exists: true,
407
+ dev: current.stat.dev.toString(),
408
+ ino: current.stat.ino.toString(),
409
+ parentDev: parent.dev.toString(),
410
+ parentIno: parent.ino.toString(),
411
+ quarantineName: path.basename(quarantine),
412
+ }
413
+ : { exists: false, parentDev: parent.dev.toString(), parentIno: parent.ino.toString() },
414
+ });
415
+ await journal.append({ repairId: REPAIR_ID, targetId: descriptor.targetId, phase: "applying" });
416
+ if (!current) {
417
+ // The managed name is unoccupied: exclusive creation is the whole mutation.
418
+ await fs.symlink(selected.candidate.path, authority.targetPath);
419
+ published = await linkSnapshot(authority.targetPath);
420
+ if (!published || published.target !== selected.candidate.path) throw new Error("link_postcheck_failed");
421
+ retained = authority.targetPath;
422
+ } else {
423
+ await fs.symlink(selected.candidate.path, staged);
424
+ const stage = await linkSnapshot(staged);
425
+ if (!stage) throw new Error("stage_unverified");
426
+ const result = exactSwapManagedLink(
427
+ staged,
428
+ authority.targetPath,
429
+ quarantine,
430
+ parent.dev.toString(),
431
+ parent.ino.toString(),
432
+ current.stat.dev.toString(),
433
+ current.stat.ino.toString(),
434
+ current.target,
435
+ stage.stat.dev.toString(),
436
+ stage.stat.ino.toString(),
437
+ selected.candidate.path,
438
+ );
439
+ if (!result.verified || result.status !== "verified") throw new Error("swap_unverified");
440
+ retained = result.code === "old_artifact_retained_at_quarantine" ? quarantine : staged;
441
+ published = await linkSnapshot(authority.targetPath);
442
+ const old = await linkSnapshot(retained);
443
+ if (
444
+ !published ||
445
+ !old ||
446
+ published.stat.dev !== stage.stat.dev ||
447
+ published.stat.ino !== stage.stat.ino ||
448
+ published.target !== selected.candidate.path ||
449
+ old.stat.dev !== current.stat.dev ||
450
+ old.stat.ino !== current.stat.ino ||
451
+ old.target !== current.target
452
+ )
453
+ throw new Error("link_postcheck_failed");
454
+ }
455
+ await refreshReceipt(authority, parent, selected.candidate.path, published);
456
+ await syncParent(descriptor.parentPath, parent);
457
+ const after = await describeManagedLink(authority.targetPath, authority.root, authority.alias);
458
+ if (!after.receiptTrusted || after.status !== "healthy") throw new Error("receipt_postcheck_failed");
459
+ await journal.append({
460
+ repairId: REPAIR_ID,
461
+ targetId: descriptor.targetId,
462
+ phase: "verified",
463
+ after: {
464
+ exists: true,
465
+ dev: published.stat.dev.toString(),
466
+ ino: published.stat.ino.toString(),
467
+ parentDev: parent.dev.toString(),
468
+ parentIno: parent.ino.toString(),
469
+ },
470
+ outcome: "verified",
471
+ });
472
+ return { state: "verified", changed: true, sideEffectStarted: true };
473
+ } catch {
474
+ let state: ManagedLinkRepairResult["state"] = "uncertain";
475
+ if (published && current) {
476
+ try {
477
+ const result = exactSwapManagedLink(
478
+ retained,
479
+ authority.targetPath,
480
+ `${authority.targetPath}.doctor-failed-${request.runId}`,
481
+ parent.dev.toString(),
482
+ parent.ino.toString(),
483
+ published.stat.dev.toString(),
484
+ published.stat.ino.toString(),
485
+ published.target,
486
+ current.stat.dev.toString(),
487
+ current.stat.ino.toString(),
488
+ current.target,
489
+ );
490
+ state = result.verified ? "rolled_back" : "rollback_conflict";
491
+ } catch {
492
+ state = "rollback_conflict";
493
+ }
494
+ }
495
+ try {
496
+ await journal.append({
497
+ repairId: REPAIR_ID,
498
+ targetId: descriptor.targetId,
499
+ phase: state === "rolled_back" ? "failed" : "uncertain",
500
+ outcome: state,
501
+ });
502
+ } catch {
503
+ state = "uncertain";
504
+ }
505
+ return {
506
+ state,
507
+ changed: published !== undefined,
508
+ sideEffectStarted: true,
509
+ reasonCode: "managed_link_transaction_unverified",
510
+ };
511
+ } finally {
512
+ journal.close();
513
+ }
514
+ }
@@ -0,0 +1,13 @@
1
+ import { runNativeSmokeTest } from "../native-smoke";
2
+ import type { DoctorProbeReceipt } from "./probe-types";
3
+
4
+ export async function collectNativeProbe(): Promise<DoctorProbeReceipt> {
5
+ await runNativeSmokeTest();
6
+ return {
7
+ schemaVersion: 1,
8
+ kind: "native",
9
+ status: "completed",
10
+ reasonCode: "native_exports_verified",
11
+ checks: [],
12
+ };
13
+ }