@moretea-labs/forge 1.8.1 → 1.8.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 (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -1,162 +1,35 @@
1
1
  import type { CallToolResult, McpToolDefinition } from '../../../packages/protocols/mcp/tool-contract';
2
2
  export type { CallToolResult, McpToolDefinition } from '../../../packages/protocols/mcp/tool-contract';
3
3
  import { createHash } from "crypto";
4
- import {
5
- existsSync,
6
- mkdirSync,
7
- readdirSync,
8
- readFileSync,
9
- statSync,
10
- writeFileSync,
11
- appendFileSync,
12
- } from "fs";
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, appendFileSync } from "fs";
13
5
  import { basename, dirname, join } from "path";
14
- import { runProcess } from "../../../src/effects/process-runner";
15
- import {
16
- beginEditSession,
17
- applyEditOperations,
18
- createEditSavepoint,
19
- EditSessionPatchError,
20
- finalizeEditSession,
21
- getEditSession,
22
- getEditSessionDiff,
23
- listEditSessions,
24
- PREFERRED_EDIT_PATCH_BATCH_OPERATIONS,
25
- rollbackEditSession,
26
- verifyEditSession,
27
- } from "../../../src/cli/editing/edit-session";
28
- import {
29
- EDIT_OPERATION_INPUT_SCHEMA,
30
- normalizeEditOperations,
31
- } from "../../../src/cli/editing/edit-operation-contract";
32
- import {
33
- cancelAgentJob,
34
- getAgentJob,
35
- getAgentJobEvents,
36
- getAgentJobLog,
37
- listAgentJobs,
38
- } from "../../../src/cli/agent-jobs/job-manager";
39
- import {
40
- classifyGitHubCopilotPreflight,
41
- classifyLocalExecutorHealth,
42
- executorHealthCode,
43
- isExecutorHealthError,
44
- type ExecutorHealth,
45
- } from "../../../src/cli/agent-jobs/executor-health";
46
- import { integrateAgentJob, taskRunDiff } from "../../../src/cli/agent-jobs/integration";
47
- import {
48
- appendTask,
49
- archiveIssue,
50
- createIssue,
51
- getIssue,
52
- getIssueEffectiveView,
53
- getIssueReadView,
54
- inspectIssueReadiness,
55
- inspectTaskReadiness,
56
- acceptVerifiedTask,
57
- listIssueEffectiveViews,
58
- listIssues,
59
- projectIssueEffectiveView,
60
- planIssue,
61
- projectBoard,
62
- recordTaskVerification,
63
- restoreIssue,
64
- setTaskDependencies,
65
- splitTask,
66
- supersedeTask,
67
- updateIssue,
68
- updateTask,
69
- } from "../../../src/cli/controller/issue-store";
70
- import { applyMigratedIssueDecision } from '../../../src/cli/controller/migrated-issue-decision';
71
- import {
72
- listControllerChecks,
73
- } from "../../../src/cli/controller/check-runner";
74
- import { normalizeCheckIds } from '../../../src/runtime/control-plane/facade/check-normalization';
6
+ import { beginEditSession, applyEditOperations, createEditSavepoint, EditSessionPatchError, finalizeEditSession, getEditSession, getEditSessionDiff, listEditSessions, PREFERRED_EDIT_PATCH_BATCH_OPERATIONS, rollbackEditSession } from "../../../src/cli/editing/edit-session";
7
+ import { EDIT_OPERATION_INPUT_SCHEMA, normalizeEditOperations } from "../../../src/cli/editing/edit-operation-contract";
8
+ import { listAgentJobs } from "../../../src/cli/agent-jobs/job-manager";
9
+ import { listControllerChecks } from "../../../src/cli/controller/check-runner";
75
10
  import { buildExecutionDiagnostics, buildRequirementBoard } from '../../../src/runtime/control-plane/facade/requirement-board';
76
11
  import { getWorkContract } from '../../../packages/kernel/work/api';
77
12
  import { admitDirectEditWorkContract } from '../../../src/runtime/control-plane/facade/repository-work-admission';
78
13
  import { listCapabilityDescriptors, summarizeCapabilityGroups } from '../../../src/runtime/control-plane/facade/capability-registry';
79
- import {
80
- exportControllerWorklog,
81
- listControllerWorklogEvents,
82
- parseWorklogCategory,
83
- } from "../../../src/cli/controller/worklog";
84
- import { inspectProjectGovernance, reconcileProjectGovernance } from "../../../src/cli/controller/governance";
85
- import { taskExecutionPolicy } from "../../../src/cli/controller/execution-policy";
86
- import { finishEditSession, finishTaskRun } from "../../../src/cli/controller/completion-orchestrator";
87
- import { buildControllerTaskLedgerProjection } from "../../../src/cli/controller/task-ledger";
88
- import { loadControllerProjectState, saveControllerProjectState } from "../../../src/cli/controller/project-state";
89
- import {
90
- FORGE_MCP_SCHEMA_VERSION,
91
- FORGE_TOOL_SURFACE,
92
- FORGE_VERSION,
93
- forgeToolSurfaceFingerprint,
94
- MIN_AGENT_TIMEOUT_MS,
95
- formatDurationMs,
96
- normalizeAgentTimeoutMs,
97
- } from "../../../src/cli/controller/runtime-config";
98
- import { DEFAULT_CONTROLLER_TOOL_NAMES, PREFERRED_FACADE_TOOL_NAMES } from "../toolset-names";
14
+ import { exportControllerWorklog, listControllerWorklogEvents, parseWorklogCategory } from "../../../src/cli/controller/worklog";
15
+ import { FORGE_MCP_SCHEMA_VERSION, FORGE_TOOL_SURFACE, FORGE_VERSION, forgeToolSurfaceFingerprint, MIN_AGENT_TIMEOUT_MS, formatDurationMs, normalizeAgentTimeoutMs } from "../../../src/cli/controller/runtime-config";
16
+ import { PREFERRED_FACADE_TOOL_NAMES } from "../toolset-names";
99
17
  import { buildLocalConnectorStatusForRepo } from "../../../src/cli/local-bridge/connector-freshness";
100
- import type {
101
- ControllerAgent,
102
- ControllerTask,
103
- IssueKind,
104
- IssueStatus,
105
- TaskDraft,
106
- TaskRisk,
107
- TaskCommandEvidence,
108
- TaskStatus,
109
- TaskVerification,
110
- } from "../../../src/cli/controller/types";
111
- import {
112
- gitDiff,
113
- gitSnapshot,
114
- searchRepository,
115
- } from "../../../src/cli/repository/inspector";
116
- import {
117
- closeGitHubIssue,
118
- getGitHubStatus,
119
- publishIssueToGitHub,
120
- refreshGitHubIssue,
121
- } from "../../../src/cli/github/github";
122
- import {
123
- getGitHubPluginStatus,
124
- saveGitHubPluginConfig,
125
- } from "../../../src/cli/github/plugin";
126
- import {
127
- getLocalBridgeJobEvents,
128
- getLocalBridgeJobSnapshot,
129
- listLocalBridgeJobs,
130
- readLocalBridgeJobOutput,
131
- reconcileLocalBridgeJobs,
132
- } from "../../../src/cli/local-bridge/job-store";
18
+ import { gitDiff, gitSnapshot, searchRepository } from "../../../src/cli/repository/inspector";
19
+ import { getGitHubStatus } from "../../../src/cli/github/github";
20
+ import { getGitHubPluginStatus, saveGitHubPluginConfig } from "../../../src/cli/github/plugin";
21
+ import { getLocalBridgeJobEvents, getLocalBridgeJobSnapshot, listLocalBridgeJobs, readLocalBridgeJobOutput, reconcileLocalBridgeJobs } from "../../../src/cli/local-bridge/job-store";
133
22
  import { runHelper } from "../../../src/cli/runtime/helper-runner";
134
- import {
135
- listSessions,
136
- openSession,
137
- readSession,
138
- runBrowserConsult,
139
- runBrowserFollowup,
140
- } from "../../../src/cli/chatgpt-browser/engine";
141
- import type {
142
- BrowserProviderName,
143
- NativeBrowserChannel,
144
- ThinkingLevel,
145
- } from "../../../src/cli/chatgpt-browser/types";
23
+ import { listSessions, openSession, readSession, runBrowserConsult, runBrowserFollowup } from "../../../src/cli/chatgpt-browser/engine";
24
+ import type { BrowserProviderName, NativeBrowserChannel, ThinkingLevel } from "../../../src/cli/chatgpt-browser/types";
146
25
  import { hashMcpInput, tryWriteMcpAuditEntry } from "../audit";
147
- import { loadMcpRuntimeState } from "../auth";
148
26
  import { resolveLocalBridgeSurface, summarizeRecentJobs } from "../../../src/runtime/shared/local-bridge-surface";
149
27
  import { resolveRepoPreferredControllerHome } from "../../../src/cli/repositories/controller-home";
150
28
  import { resolveRepositoryCheckStorage } from '../../../src/runtime/execution/process-runtime/check-storage';
151
29
  import { resolveRepositorySelection } from '../../../src/cli/repositories/registry';
152
30
  import { ensureRepositoryRuntimeStorageBinding } from '../../../src/cli/repositories/runtime-storage';
153
- import { executionIdentityForRepository } from '../../../src/runtime/control-plane/execution/execution-identity';
154
- import { runPersistedCheckViaProcessRuntime } from '../runtime-gateway/persisted-check-process';
155
- import { getProcessRecord } from '../../../src/runtime/execution/process-runtime/store';
156
- import { processCheckCompletionReceipt } from '../../../src/runtime/execution/process-runtime/check-receipt';
157
- import { normalizeMcpRelativePath, resolveMcpPath } from "../paths";
31
+ import { resolveMcpPath } from "../paths";
158
32
  import { currentGitBranch, isForgeAdopted } from "../repo";
159
- import { repositoryToolNames } from "./repository-tools";
160
33
  import { redactMcpText } from "../redaction";
161
34
  import type { McpPolicy } from "../types";
162
35
 
@@ -178,6 +51,8 @@ export interface McpToolContext {
178
51
  /** Bound repository identity for session cache keys. */
179
52
  repoId?: string;
180
53
  checkoutId?: string;
54
+ /** Canonical tools/list projection supplied by the MCP registry; never authored by the client. */
55
+ publishedToolNames?: readonly string[];
181
56
  /** Request-scoped MCP cancellation; never persisted or reused across calls. */
182
57
  signal?: AbortSignal;
183
58
  }
@@ -214,30 +89,6 @@ function errorResult(
214
89
  return result;
215
90
  }
216
91
 
217
- function dispatchErrorResult(
218
- code: string,
219
- message: string,
220
- options: {
221
- requestId?: string;
222
- retryable?: boolean;
223
- details?: unknown;
224
- } = {},
225
- ): CallToolResult {
226
- const payload = {
227
- accepted: false,
228
- code,
229
- message: redactMcpText(message).text,
230
- retryable: options.retryable === true,
231
- requestId: options.requestId,
232
- details: options.details,
233
- };
234
- const result = textResult({
235
- error: payload,
236
- ...payload,
237
- });
238
- result.isError = true;
239
- return result;
240
- }
241
92
 
242
93
  function audit(
243
94
  ctx: McpToolContext,
@@ -319,429 +170,25 @@ function knownRunPathReplacements(
319
170
  .map(([from, to]) => ({ from, to }));
320
171
  }
321
172
 
322
- function redactRunText(
323
- text: string,
324
- repoRoot: string,
325
- run: Partial<{
326
- executionRoot: string;
327
- worktree: string;
328
- worktreePath: string;
329
- }>,
330
- maxChars: number,
331
- ): { text: string; truncated: boolean; redactions: Array<{ type: string; count: number }> } {
332
- let scrubbed = text;
333
- for (const entry of knownRunPathReplacements(repoRoot, run)) {
334
- scrubbed = scrubbed.split(entry.from).join(entry.to);
335
- }
336
- scrubbed = scrubbed
337
- .replace(/\/(?:private\/)?tmp\/[^\s"']+/g, "<abs-path>")
338
- .replace(/\/(?:private\/)?var\/folders\/[^\s"']+/g, "<abs-path>")
339
- .replace(/\/Users\/[^\s"']+/g, "<abs-path>")
340
- .replace(/[A-Za-z]:\\[^\s"']+/g, "<abs-path>");
341
- const redacted = redactMcpText(scrubbed);
342
- if (redacted.text.length <= maxChars) {
343
- return { text: redacted.text, truncated: false, redactions: redacted.redactions };
344
- }
345
- return {
346
- text: redacted.text.slice(-maxChars),
347
- truncated: true,
348
- redactions: redacted.redactions,
349
- };
350
- }
351
173
 
352
- function maybeTail(
353
- value: string | undefined,
354
- repoRoot: string,
355
- run: Partial<{
356
- executionRoot: string;
357
- worktree: string;
358
- worktreePath: string;
359
- }>,
360
- maxChars: number,
361
- ): { text: string; truncated: boolean; redactions: Array<{ type: string; count: number }> } | undefined {
362
- if (!value) return undefined;
363
- return redactRunText(value, repoRoot, run, maxChars);
364
- }
365
174
 
366
- function summarizeAgentRun(
367
- repoRoot: string,
368
- run: Record<string, unknown>,
369
- options: {
370
- includePaths?: boolean;
371
- includeOutput?: boolean;
372
- tailChars?: number;
373
- } = {},
374
- ): Record<string, unknown> {
375
- const tailChars = Math.min(Math.max(options.tailChars ?? 4096, 256), 16 * 1024);
376
- const base: Record<string, unknown> = {
377
- runId: run.runId,
378
- issueId: run.issueId,
379
- taskId: run.taskId,
380
- agent: run.agent,
381
- provider: run.provider,
382
- executionMode: run.executionMode,
383
- executionClass: run.executionClass,
384
- allowedPaths: run.allowedPaths,
385
- status: run.status,
386
- closureState: run.closureState,
387
- closureUpdatedAt: run.closureUpdatedAt,
388
- preservationReason: run.preservationReason,
389
- preservationDetails: typeof run.preservationDetails === "string"
390
- ? redactRunText(run.preservationDetails, repoRoot, {
391
- executionRoot: typeof run.executionRoot === "string" ? run.executionRoot : undefined,
392
- worktree: typeof run.worktree === "string" ? run.worktree : undefined,
393
- worktreePath: typeof run.worktreePath === "string" ? run.worktreePath : undefined,
394
- }, 1000).text
395
- : run.preservationDetails,
396
- branch: run.branch,
397
- baseRevision: run.baseRevision,
398
- changedFiles: Array.isArray(run.changedFiles) ? run.changedFiles.slice(0, 100) : run.changedFiles,
399
- exitCode: run.exitCode,
400
- error: typeof run.error === "string"
401
- ? redactRunText(run.error, repoRoot, {
402
- executionRoot: typeof run.executionRoot === "string" ? run.executionRoot : undefined,
403
- worktree: typeof run.worktree === "string" ? run.worktree : undefined,
404
- worktreePath: typeof run.worktreePath === "string" ? run.worktreePath : undefined,
405
- }, 1000).text
406
- : run.error,
407
- executorHealth: run.executorHealth,
408
- timeoutMs: run.timeoutMs,
409
- deadlineAt: run.deadlineAt,
410
- startupDeadlineAt: run.startupDeadlineAt,
411
- lastHeartbeatAt: run.lastHeartbeatAt,
412
- progress: run.progress,
413
- autoIntegrate: run.autoIntegrate,
414
- autoIntegrationError: typeof run.autoIntegrationError === "string"
415
- ? redactRunText(run.autoIntegrationError, repoRoot, {
416
- executionRoot: typeof run.executionRoot === "string" ? run.executionRoot : undefined,
417
- worktree: typeof run.worktree === "string" ? run.worktree : undefined,
418
- worktreePath: typeof run.worktreePath === "string" ? run.worktreePath : undefined,
419
- }, 1000).text
420
- : run.autoIntegrationError,
421
- worktreeCleanedAt: run.worktreeCleanedAt,
422
- cleanupBranchDeletedAt: run.cleanupBranchDeletedAt,
423
- diffArtifactPath: run.diffArtifactPath,
424
- terminationReason: run.terminationReason,
425
- createdAt: run.createdAt,
426
- startedAt: run.startedAt,
427
- finishedAt: run.finishedAt,
428
- integratedSessionId: run.integratedSessionId,
429
- integratedAt: run.integratedAt,
430
- timing: run.timing,
431
- github: run.github && typeof run.github === "object"
432
- ? {
433
- owner: (run.github as Record<string, unknown>).owner,
434
- repo: (run.github as Record<string, unknown>).repo,
435
- taskId: (run.github as Record<string, unknown>).taskId,
436
- state: (run.github as Record<string, unknown>).state,
437
- url: (run.github as Record<string, unknown>).url,
438
- pullRequestUrl: (run.github as Record<string, unknown>).pullRequestUrl,
439
- baseRef: (run.github as Record<string, unknown>).baseRef,
440
- model: (run.github as Record<string, unknown>).model,
441
- createPullRequest: (run.github as Record<string, unknown>).createPullRequest,
442
- }
443
- : run.github,
444
- detail: {
445
- pathsIncluded: options.includePaths === true,
446
- outputIncluded: options.includeOutput !== false,
447
- next: {
448
- log: "Call get_task_run_log for a larger bounded tail.",
449
- diff: "Call get_task_diff to inspect Git changes from an isolated worktree.",
450
- },
451
- },
452
- };
453
- if (options.includeOutput !== false) {
454
- const stdout = maybeTail(
455
- typeof run.stdoutTail === "string" ? run.stdoutTail : undefined,
456
- repoRoot,
457
- {
458
- executionRoot: typeof run.executionRoot === "string" ? run.executionRoot : undefined,
459
- worktree: typeof run.worktree === "string" ? run.worktree : undefined,
460
- worktreePath: typeof run.worktreePath === "string" ? run.worktreePath : undefined,
461
- },
462
- tailChars,
463
- );
464
- const stderr = maybeTail(
465
- typeof run.stderrTail === "string" ? run.stderrTail : undefined,
466
- repoRoot,
467
- {
468
- executionRoot: typeof run.executionRoot === "string" ? run.executionRoot : undefined,
469
- worktree: typeof run.worktree === "string" ? run.worktree : undefined,
470
- worktreePath: typeof run.worktreePath === "string" ? run.worktreePath : undefined,
471
- },
472
- tailChars,
473
- );
474
- if (stdout) {
475
- base.stdoutTail = stdout.text;
476
- base.stdoutTailTruncated = stdout.truncated;
477
- }
478
- if (stderr) {
479
- base.stderrTail = stderr.text;
480
- base.stderrTailTruncated = stderr.truncated;
481
- }
482
- const redactions = [...(stdout?.redactions ?? []), ...(stderr?.redactions ?? [])];
483
- if (redactions.length > 0) base.redactions = redactions;
484
- }
485
- if (options.includePaths === true) {
486
- Object.assign(base, {
487
- repoRoot: run.repoRoot,
488
- executionRoot: run.executionRoot,
489
- worktree: run.worktree,
490
- worktreePath: run.worktreePath,
491
- promptPath: run.promptPath,
492
- stdoutPath: run.stdoutPath,
493
- stderrPath: run.stderrPath,
494
- resultPath: run.resultPath,
495
- eventsPath: run.eventsPath,
496
- controllerPid: run.controllerPid,
497
- controllerEpoch: run.controllerEpoch,
498
- controllerEpochPath: run.controllerEpochPath,
499
- launchPid: run.launchPid,
500
- workerPid: run.workerPid,
501
- agentPid: run.agentPid,
502
- });
503
- }
504
- return base;
505
- }
506
175
 
507
- function summarizeRunList(
508
- repoRoot: string,
509
- runs: Array<Record<string, unknown>>,
510
- includePaths = false,
511
- ): Array<Record<string, unknown>> {
512
- return runs.map((run) => summarizeAgentRun(repoRoot, run, { includePaths, includeOutput: false }));
513
- }
514
176
 
515
- function compactDispatchRun(run: Record<string, unknown>): Record<string, unknown> {
516
- const runId = typeof run.runId === "string" ? run.runId : String(run.runId ?? "");
517
- const detail: Record<string, unknown> = runId
518
- ? {
519
- get_task_run: { run_id: runId },
520
- get_task_run_log: { run_id: runId },
521
- get_task_run_events: { run_id: runId },
522
- get_task_diff: { run_id: runId },
523
- }
524
- : {};
525
- return {
526
- runId: run.runId,
527
- issueId: run.issueId,
528
- taskId: run.taskId,
529
- status: run.status,
530
- agent: run.agent,
531
- provider: run.provider,
532
- executionMode: run.executionMode,
533
- executionClass: run.executionClass,
534
- timeoutMs: run.timeoutMs,
535
- startedAt: run.startedAt,
536
- deadlineAt: run.deadlineAt,
537
- finishedAt: run.finishedAt,
538
- integratedSessionId: run.integratedSessionId,
539
- autoIntegrationError: run.autoIntegrationError,
540
- worktreeCleanedAt: run.worktreeCleanedAt,
541
- accepted: run.accepted,
542
- reused: run.reused,
543
- github: run.github,
544
- error: run.error,
545
- detail,
546
- };
547
- }
548
177
 
549
- function compactTaskView(task: Record<string, unknown>): Record<string, unknown> {
550
- return {
551
- id: task.id,
552
- title: task.title,
553
- status: task.status,
554
- effectiveStatus: task.effectiveStatus,
555
- declaredStatus: task.declaredStatus,
556
- latestRunId: task.latestRunId,
557
- latestRunStatus: task.latestRunStatus,
558
- activeRunId: task.activeRunId,
559
- activeRunStatus: task.activeRunStatus,
560
- verificationStatus: task.verificationStatus,
561
- dispatchable: task.dispatchable,
562
- retryable: task.retryable,
563
- requiresExplicitRetry: task.requiresExplicitRetry,
564
- };
565
- }
566
178
 
567
- function compactIssueView(issue: Record<string, unknown>): Record<string, unknown> {
568
- const id = typeof issue.id === "string" ? issue.id : String(issue.id ?? "");
569
- const tasks = Array.isArray(issue.tasks) ? issue.tasks as Array<Record<string, unknown>> : [];
570
- return {
571
- id: issue.id,
572
- title: issue.title,
573
- status: issue.status,
574
- lifecycleStatus: issue.lifecycleStatus,
575
- github: issue.github,
576
- taskCount: tasks.length,
577
- tasks: tasks.map((task) => compactTaskView(task)),
578
- detail: id ? { get_issue: { issue_id: id } } : {},
579
- };
580
- }
581
179
 
582
180
  const PROJECT_BOARD_RECENT_ISSUE_LIMIT = 5;
583
181
  const PROJECT_BOARD_TASK_LIMIT = 8;
584
182
 
585
- function compactProjectBoardText(value: unknown, maxChars = 96): unknown {
586
- if (typeof value !== "string" || value.length <= maxChars) return value;
587
- return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
588
- }
589
183
 
590
- function compactProjectBoardTask(
591
- task: Record<string, unknown>,
592
- options: { includeLifecycle?: boolean } = {},
593
- ): Record<string, unknown> {
594
- const compact: Record<string, unknown> = {
595
- issueId: task.issueId,
596
- taskId: task.taskId,
597
- id: task.id,
598
- title: compactProjectBoardText(task.title),
599
- agent: task.agent,
600
- effectiveStatus: task.effectiveStatus,
601
- executionClass: task.executionClass,
602
- approval: task.approval,
603
- };
604
- if (options.includeLifecycle) {
605
- Object.assign(compact, {
606
- status: task.status,
607
- declaredStatus: task.declaredStatus,
608
- statusReason: task.statusReason,
609
- verificationStatus: task.verificationStatus,
610
- dispatchable: task.dispatchable,
611
- queueable: task.queueable,
612
- retryable: task.retryable,
613
- requiresExplicitRetry: task.requiresExplicitRetry,
614
- });
615
- }
616
- return compact;
617
- }
618
184
 
619
- function compactProjectBoardIssue(
620
- issue: Record<string, unknown>,
621
- options: { includeTasks?: boolean } = {},
622
- ): Record<string, unknown> {
623
- const tasks = Array.isArray(issue.tasks) ? issue.tasks as Array<Record<string, unknown>> : [];
624
- const taskCounts = tasks.reduce<Record<string, number>>((counts, task) => {
625
- const status = typeof task.effectiveStatus === "string" ? task.effectiveStatus : "unknown";
626
- counts[status] = (counts[status] ?? 0) + 1;
627
- return counts;
628
- }, {});
629
- const compact: Record<string, unknown> = {
630
- id: issue.id,
631
- title: compactProjectBoardText(issue.title),
632
- kind: issue.kind,
633
- status: issue.status,
634
- lifecycleStatus: issue.lifecycleStatus,
635
- updatedAt: issue.updatedAt,
636
- isCurrent: issue.isCurrent === true,
637
- taskCount: tasks.length,
638
- taskCounts,
639
- readyTaskCount: tasks.filter((task) => task.dispatchable === true).length,
640
- queueableTaskCount: tasks.filter((task) => task.queueable === true).length,
641
- };
642
- if (options.includeTasks) {
643
- compact.tasks = tasks
644
- .slice(0, PROJECT_BOARD_TASK_LIMIT)
645
- .map((task) => compactProjectBoardTask(task, { includeLifecycle: true }));
646
- compact.taskTruncatedCount = Math.max(0, tasks.length - PROJECT_BOARD_TASK_LIMIT);
647
- }
648
- return compact;
649
- }
650
185
 
651
- function summarizeProjectBoard(board: Record<string, unknown>): Record<string, unknown> {
652
- const issues = Array.isArray(board.issues) ? board.issues as Array<Record<string, unknown>> : [];
653
- const readyTasks = Array.isArray(board.readyTasks) ? board.readyTasks as Array<Record<string, unknown>> : [];
654
- const queueableTasks = Array.isArray(board.queueableTasks) ? board.queueableTasks as Array<Record<string, unknown>> : [];
655
- const currentIssue = issues.find((issue) => issue.isCurrent === true);
656
- const recentIssueCandidates = currentIssue
657
- ? issues.filter((issue) => issue !== currentIssue)
658
- : issues;
659
- const recentIssues = recentIssueCandidates.slice(0, PROJECT_BOARD_RECENT_ISSUE_LIMIT);
660
- return {
661
- detailLevel: "summary",
662
- issueCount: issues.length,
663
- archivedIssueCount: board.archivedIssueCount,
664
- counts: board.counts,
665
- declaredCounts: board.declaredCounts,
666
- archivedCounts: board.archivedCounts,
667
- currentIssueId: board.currentIssueId,
668
- currentIssue: currentIssue ? compactProjectBoardIssue(currentIssue, { includeTasks: true }) : undefined,
669
- recentIssues: recentIssues.map((issue) => compactProjectBoardIssue(issue)),
670
- recentIssueTruncatedCount: Math.max(0, recentIssueCandidates.length - recentIssues.length),
671
- readyTaskCount: readyTasks.length,
672
- readyTasks: readyTasks.slice(0, PROJECT_BOARD_TASK_LIMIT).map((task) => compactProjectBoardTask(task)),
673
- readyTaskTruncatedCount: Math.max(0, readyTasks.length - PROJECT_BOARD_TASK_LIMIT),
674
- queueableTaskCount: queueableTasks.length,
675
- queueableTasks: queueableTasks.slice(0, PROJECT_BOARD_TASK_LIMIT).map((task) => compactProjectBoardTask(task)),
676
- queueableTaskTruncatedCount: Math.max(0, queueableTasks.length - PROJECT_BOARD_TASK_LIMIT),
677
- detailPointer: {
678
- tool: "get_project_board",
679
- arguments: { detail_level: "detail" },
680
- },
681
- };
682
- }
683
186
 
684
187
  const UPDATE_TASK_SUMMARY_TASK_LIMIT = 12;
685
188
 
686
- function compactUpdatedTaskView(task: Record<string, unknown>): Record<string, unknown> {
687
- return {
688
- ...compactTaskView(task),
689
- title: compactProjectBoardText(task.title),
690
- };
691
- }
692
189
 
693
- function summarizeUpdatedIssue(
694
- issue: Record<string, unknown>,
695
- taskId: string,
696
- ): Record<string, unknown> {
697
- const tasks = Array.isArray(issue.tasks) ? issue.tasks as Array<Record<string, unknown>> : [];
698
- const updatedTask = tasks.find((task) => task.id === taskId);
699
- return {
700
- detailLevel: "summary",
701
- id: issue.id,
702
- title: compactProjectBoardText(issue.title),
703
- status: issue.status,
704
- lifecycleStatus: issue.lifecycleStatus,
705
- github: issue.github,
706
- taskCount: tasks.length,
707
- tasks: tasks.slice(0, UPDATE_TASK_SUMMARY_TASK_LIMIT).map(compactUpdatedTaskView),
708
- taskTruncatedCount: Math.max(0, tasks.length - UPDATE_TASK_SUMMARY_TASK_LIMIT),
709
- updatedTask: updatedTask ? compactUpdatedTaskView(updatedTask) : undefined,
710
- detailPointer: {
711
- tool: "get_issue",
712
- arguments: { issue_id: issue.id, detail_level: "full" },
713
- },
714
- };
715
- }
716
190
 
717
- function dispatchExecutorHealthResult(
718
- health: ExecutorHealth,
719
- options: { requestId?: string; retryable?: boolean } = {},
720
- ): CallToolResult {
721
- return dispatchErrorResult(executorHealthCode(health), health.message, {
722
- requestId: options.requestId,
723
- retryable: options.retryable,
724
- details: { executorHealth: health },
725
- });
726
- }
727
191
 
728
- function taskExecutorHealth(
729
- ctx: McpToolContext,
730
- task: ControllerTask,
731
- agent: ControllerAgent,
732
- githubRepo?: string,
733
- ): ExecutorHealth | null {
734
- if (agent === "github-copilot") {
735
- return classifyGitHubCopilotPreflight(getGitHubStatus(ctx.repoRoot, githubRepo));
736
- }
737
- return classifyLocalExecutorHealth(
738
- agent,
739
- {
740
- agentRunner: ctx.policy.execution.agentRunner,
741
- allowedAgents: ctx.policy.execution.allowedAgents,
742
- },
743
- );
744
- }
745
192
 
746
193
  function listFilesUnder(
747
194
  repoRoot: string,
@@ -820,28 +267,7 @@ function timestampPrefix(date = new Date()): string {
820
267
  return `${yyyy}${mm}${dd}-${hh}${min}`;
821
268
  }
822
269
 
823
- function parseControllerAgent(value: unknown): ControllerAgent | null {
824
- const normalized = String(value ?? "")
825
- .trim()
826
- .toLowerCase();
827
- return normalized === "codex" ||
828
- normalized === "claude" ||
829
- normalized === "github-copilot"
830
- ? normalized
831
- : null;
832
- }
833
270
 
834
- function resolveDispatchAgent(
835
- ctx: McpToolContext,
836
- task: ControllerTask,
837
- requested: unknown,
838
- ): ControllerAgent | null {
839
- const hasExplicitRequest = requested !== undefined && requested !== null && String(requested).trim() !== "";
840
- if (hasExplicitRequest) return parseControllerAgent(requested);
841
- if (task.recommendedAgent) return task.recommendedAgent;
842
- const firstLocal = ctx.policy.execution.allowedAgents[0];
843
- return firstLocal ?? task.recommendedAgent ?? null;
844
- }
845
271
 
846
272
  function runnerTimeoutMs(ctx: McpToolContext, value: unknown): number {
847
273
  return normalizeAgentTimeoutMs(value, {
@@ -991,50 +417,8 @@ interface TaskVerificationLeaseConflict {
991
417
  blockingProcessId?: string;
992
418
  }
993
419
 
994
- function taskVerificationLeaseConflict(record: {
995
- error?: { message?: unknown };
996
- }): TaskVerificationLeaseConflict | undefined {
997
- const message = typeof record.error?.message === "string" ? record.error.message.trim() : "";
998
- if (!message.startsWith("PROCESS_LEASE_CONFLICT:")) return undefined;
999
- const match = /^PROCESS_LEASE_CONFLICT:\s*(.+?)@process:([A-Za-z0-9._-]+)$/.exec(message);
1000
- return {
1001
- message,
1002
- resourceKey: match?.[1],
1003
- blockingProcessId: match?.[2],
1004
- };
1005
- }
1006
420
 
1007
- function taskVerificationRetryRequestId(
1008
- requestBase: string,
1009
- checkId: string,
1010
- conflictedProcessId: string,
1011
- ): string {
1012
- const stableBase = requestBase.replace(/:retry:[a-f0-9]{16}$/i, "");
1013
- const retryToken = createHash("sha256")
1014
- .update(`${stableBase}\0${checkId}\0${conflictedProcessId}`)
1015
- .digest("hex")
1016
- .slice(0, 16);
1017
- return `${stableBase}:retry:${retryToken}`;
1018
- }
1019
421
 
1020
- function controllerTaskDrafts(value: unknown): TaskDraft[] {
1021
- return taskObjects(value).map((task) => ({
1022
- title: String(task.title ?? "").trim(),
1023
- objective: String(task.objective ?? "").trim(),
1024
- dependsOn: stringList(task.depends_on ?? task.dependsOn),
1025
- allowedPaths: stringList(task.allowed_paths ?? task.allowedPaths),
1026
- forbiddenPaths: stringList(task.forbidden_paths ?? task.forbiddenPaths),
1027
- checks: stringList(task.checks),
1028
- acceptanceCriteria: stringList(
1029
- task.acceptance_criteria ?? task.acceptanceCriteria,
1030
- ),
1031
- risk: (["readonly", "low", "medium", "high", "destructive"].includes(String(task.risk))
1032
- ? String(task.risk)
1033
- : "medium") as TaskRisk,
1034
- recommendedAgent:
1035
- parseControllerAgent(task.agent ?? task.recommendedAgent) ?? undefined,
1036
- }));
1037
- }
1038
422
 
1039
423
  function renderPrdFromIdeaBody(args: Record<string, unknown>): string {
1040
424
  const title = String(args.title ?? "Untitled PRD").trim() || "Untitled PRD";
@@ -1750,78 +1134,6 @@ export function buildMcpToolDefinitions(
1750
1134
  inputSchema: EMPTY_SCHEMA,
1751
1135
  annotations: readOnly,
1752
1136
  },
1753
- {
1754
- name: "list_issues",
1755
- description: "List controller-managed issues and task summaries.",
1756
- inputSchema: EMPTY_SCHEMA,
1757
- annotations: readOnly,
1758
- },
1759
- {
1760
- name: "get_project_board",
1761
- description: "Return the bounded Requirement Board by default. Explicit detail/full mode returns bounded Execution Diagnostics without exposing the full legacy Task/Run/Process ledger.",
1762
- inputSchema: {
1763
- type: "object",
1764
- properties: {
1765
- detail_level: { type: "string", enum: ["summary", "detail", "full"] },
1766
- requirement_id: { type: "string" },
1767
- },
1768
- additionalProperties: false,
1769
- },
1770
- annotations: readOnly,
1771
- },
1772
- {
1773
- name: "get_project_governance",
1774
- description:
1775
- "Return the current execution focus, evidence-driven execution queue, dead dependencies, pending review/acceptance, duplicate active Issues, and closeout anomalies.",
1776
- inputSchema: EMPTY_SCHEMA,
1777
- annotations: readOnly,
1778
- },
1779
- {
1780
- name: "reconcile_project_governance",
1781
- description:
1782
- "Apply only safe governance repairs: replace superseded dependencies, return failed attempts to a retryable Task state, close fully completed Issues, and select the sole active Issue as focus.",
1783
- inputSchema: EMPTY_SCHEMA,
1784
- annotations: write,
1785
- },
1786
- {
1787
- name: "get_project_state",
1788
- description: "Return the current execution focus and Issue creation policy.",
1789
- inputSchema: EMPTY_SCHEMA,
1790
- annotations: readOnly,
1791
- },
1792
- {
1793
- name: "set_current_issue",
1794
- description: "Select the one active Issue that is allowed to drive the execution queue.",
1795
- inputSchema: {
1796
- type: "object",
1797
- properties: { issue_id: { type: "string" } },
1798
- required: ["issue_id"],
1799
- additionalProperties: false,
1800
- },
1801
- annotations: write,
1802
- },
1803
- {
1804
- name: "archive_issue",
1805
- description: "Move a done or cancelled Issue out of the current workspace while retaining its full evidence history.",
1806
- inputSchema: {
1807
- type: "object",
1808
- properties: { issue_id: { type: "string" } },
1809
- required: ["issue_id"],
1810
- additionalProperties: false,
1811
- },
1812
- annotations: write,
1813
- },
1814
- {
1815
- name: "restore_issue",
1816
- description: "Restore an archived Issue to the normal controller views.",
1817
- inputSchema: {
1818
- type: "object",
1819
- properties: { issue_id: { type: "string" } },
1820
- required: ["issue_id"],
1821
- additionalProperties: false,
1822
- },
1823
- annotations: write,
1824
- },
1825
1137
  {
1826
1138
  name: "get_worklog_timeline",
1827
1139
  description:
@@ -1887,20 +1199,6 @@ export function buildMcpToolDefinitions(
1887
1199
  },
1888
1200
  annotations: write,
1889
1201
  },
1890
- {
1891
- name: "get_issue",
1892
- description: "Read one controller issue by ID with bounded task summaries by default and full detail on demand.",
1893
- inputSchema: {
1894
- type: "object",
1895
- properties: {
1896
- issue_id: { type: "string" },
1897
- detail_level: { type: "string", enum: ["summary", "full"] },
1898
- },
1899
- required: ["issue_id"],
1900
- additionalProperties: false,
1901
- },
1902
- annotations: readOnly,
1903
- },
1904
1202
  {
1905
1203
  name: "github_status",
1906
1204
  description:
@@ -1913,362 +1211,167 @@ export function buildMcpToolDefinitions(
1913
1211
  annotations: readOnly,
1914
1212
  },
1915
1213
  {
1916
- name: "publish_issue_to_github",
1214
+ name: "begin_edit_session",
1917
1215
  description:
1918
- "Create or update the controller Issue in GitHub, optionally mirroring Tasks as sub-issues and adding all items to a GitHub Project.",
1216
+ "Open a bounded direct-edit session tied to a purpose and optional Issue/Task.",
1919
1217
  inputSchema: {
1920
1218
  type: "object",
1921
1219
  properties: {
1220
+ purpose: { type: "string" },
1221
+ work_id: { type: "string", description: "Optional existing WorkContract. Omit to create lightweight direct-control Work lineage." },
1922
1222
  issue_id: { type: "string" },
1923
- repo: { type: "string" },
1924
- labels: { type: "array", items: { type: "string" } },
1925
- include_tasks: { type: "boolean" },
1926
- project_owner: { type: "string" },
1927
- project_number: { type: "number" },
1223
+ task_id: { type: "string" },
1224
+ allowed_paths: { type: "array", items: { type: "string" } },
1225
+ max_files: { type: "number" },
1226
+ max_changed_lines: { type: "number" },
1227
+ checks: { type: "array", items: { type: "string" }, description: "Named checks to run before finalization." },
1928
1228
  },
1929
- required: ["issue_id"],
1930
- additionalProperties: false,
1931
- },
1932
- annotations: {
1933
- readOnlyHint: false,
1934
- openWorldHint: true,
1935
- destructiveHint: false,
1936
- },
1937
- },
1938
- {
1939
- name: "refresh_github_issue",
1940
- description:
1941
- "Refresh the linked GitHub Issue metadata and remote state.",
1942
- inputSchema: {
1943
- type: "object",
1944
- properties: { issue_id: { type: "string" } },
1945
- required: ["issue_id"],
1229
+ required: ["purpose"],
1946
1230
  additionalProperties: false,
1947
1231
  },
1948
- annotations: readOnly,
1232
+ annotations: write,
1949
1233
  },
1950
1234
  {
1951
- name: "close_github_issue",
1235
+ name: "apply_patch",
1952
1236
  description:
1953
- "Close the linked GitHub Issue after local controller acceptance.",
1237
+ "Append one bounded patch batch to an active edit session. Prefer batches of 100 operations or fewer and pass expected_revision to avoid stale revisions.",
1954
1238
  inputSchema: {
1955
1239
  type: "object",
1956
- properties: { issue_id: { type: "string" } },
1957
- required: ["issue_id"],
1240
+ properties: {
1241
+ session_id: { type: "string" },
1242
+ expected_revision: { type: "number", description: "Optional current revision precondition for the edit session." },
1243
+ operations: {
1244
+ type: "array",
1245
+ items: EDIT_OPERATION_INPUT_SCHEMA,
1246
+ },
1247
+ },
1248
+ required: ["session_id", "operations"],
1958
1249
  additionalProperties: false,
1959
1250
  },
1960
1251
  annotations: {
1961
1252
  readOnlyHint: false,
1962
- openWorldHint: true,
1963
- destructiveHint: true,
1253
+ openWorldHint: false,
1254
+ destructiveHint: false,
1964
1255
  },
1965
1256
  },
1966
1257
  {
1967
- name: "inspect_issue_readiness",
1968
- description:
1969
- "Score launch readiness, list blockers and warnings, and summarize executable Tasks. Agent selection happens at dispatch time.",
1258
+ name: "get_edit_session",
1259
+ description: "Read an edit session, revisions, savepoints, and operation manifest.",
1970
1260
  inputSchema: {
1971
1261
  type: "object",
1972
- properties: { issue_id: { type: "string" } },
1973
- required: ["issue_id"],
1262
+ properties: { session_id: { type: "string" } },
1263
+ required: ["session_id"],
1974
1264
  additionalProperties: false,
1975
1265
  },
1976
1266
  annotations: readOnly,
1977
1267
  },
1978
1268
  {
1979
- name: "inspect_task_readiness",
1980
- description:
1981
- "Evaluate launch readiness for one Task only. Other Issues, focus selection, unrelated Tasks, and missing named checks do not block it.",
1269
+ name: "list_edit_sessions",
1270
+ description: "List recent direct-edit sessions with revision, changed-file, and check summaries.",
1982
1271
  inputSchema: {
1983
1272
  type: "object",
1984
- properties: {
1985
- issue_id: { type: "string" },
1986
- task_id: { type: "string" },
1987
- retry_from_run_id: { type: "string" },
1988
- approve_destructive: { type: "boolean" },
1989
- },
1990
- required: ["issue_id", "task_id"],
1273
+ properties: { limit: { type: "number" } },
1991
1274
  additionalProperties: false,
1992
1275
  },
1993
1276
  annotations: readOnly,
1994
1277
  },
1995
1278
  {
1996
- name: "prepare_issue_launch",
1997
- description:
1998
- "Return an Issue Launcher preview without starting any agent session.",
1279
+ name: "get_edit_session_diff",
1280
+ description: "Read the current aggregate localized diff for one direct-edit session.",
1999
1281
  inputSchema: {
2000
1282
  type: "object",
2001
- properties: {
2002
- issue_id: { type: "string" },
2003
- max_parallel: { type: "number" },
2004
- },
2005
- required: ["issue_id"],
1283
+ properties: { session_id: { type: "string" } },
1284
+ required: ["session_id"],
2006
1285
  additionalProperties: false,
2007
1286
  },
2008
1287
  annotations: readOnly,
2009
1288
  },
2010
1289
  {
2011
- name: "create_issue",
2012
- description:
2013
- "Create a durable Issue only for broad, long-running, dependency-aware, parallel, protected-path, or high-risk work. File discovery by itself is not a reason to create an Issue or start an Agent; search first and use bounded direct edits when practical.",
1290
+ name: "create_edit_savepoint",
1291
+ description: "Name the current edit revision so later patch batches can be rolled back to it.",
2014
1292
  inputSchema: {
2015
1293
  type: "object",
2016
1294
  properties: {
2017
- title: { type: "string" },
2018
- kind: {
2019
- type: "string",
2020
- enum: ["bug", "feature", "governance", "investigation"],
2021
- },
2022
- summary: { type: "string" },
2023
- goals: { type: "array", items: { type: "string" } },
2024
- non_goals: { type: "array", items: { type: "string" } },
2025
- acceptance_criteria: { type: "array", items: { type: "string" } },
2026
- related_artifacts: { type: "array", items: { type: "string" } },
2027
- tasks: { type: "array", items: taskItemSchema },
2028
- allow_while_focused: { type: "boolean", description: "Explicitly allow a separate Issue while another Issue is the current execution focus." },
2029
- allow_duplicate: { type: "boolean", description: "Explicitly allow an active Issue with the same normalized title." },
2030
- allow_when_paused: { type: "boolean", description: "Explicitly override a paused Issue-creation policy." },
1295
+ session_id: { type: "string" },
1296
+ name: { type: "string" },
2031
1297
  },
2032
- required: ["title"],
1298
+ required: ["session_id", "name"],
2033
1299
  additionalProperties: false,
2034
1300
  },
2035
1301
  annotations: write,
2036
1302
  },
2037
1303
  {
2038
- name: "update_issue",
2039
- description: "Update issue intent, artifacts, or lifecycle status.",
1304
+ name: "verify_edit_session",
1305
+ description: "Verify the current edit revision through persisted Process Runtime checks. Retries with the same request_id reuse the original process and terminal receipts are attached to the exact revision.",
2040
1306
  inputSchema: {
2041
1307
  type: "object",
2042
1308
  properties: {
2043
- issue_id: { type: "string" },
2044
- title: { type: "string" },
2045
- summary: { type: "string" },
2046
- status: {
2047
- type: "string",
2048
- enum: [
2049
- "backlog",
2050
- "analysis",
2051
- "planned",
2052
- "launch_blocked",
2053
- "in_progress",
2054
- "review",
2055
- "done",
2056
- "cancelled",
2057
- ],
2058
- },
2059
- goals: { type: "array", items: { type: "string" } },
2060
- non_goals: { type: "array", items: { type: "string" } },
2061
- acceptance_criteria: { type: "array", items: { type: "string" } },
2062
- related_artifacts: { type: "array", items: { type: "string" } },
1309
+ session_id: { type: "string" },
1310
+ check_ids: { type: "array", items: { type: "string" } },
1311
+ request_id: { type: "string" },
1312
+ reviewer: { type: "string" },
1313
+ note: { type: "string" },
2063
1314
  },
2064
- required: ["issue_id"],
1315
+ required: ["session_id"],
2065
1316
  additionalProperties: false,
2066
1317
  },
2067
1318
  annotations: write,
2068
1319
  },
2069
1320
  {
2070
- name: "plan_issue",
1321
+ name: "rollback_edit_session",
2071
1322
  description:
2072
- "Replace the pre-execution task plan for an Issue with ordered dependency-aware tasks.",
1323
+ "Rollback an active edit session completely, to a prior revision, or to a named savepoint.",
2073
1324
  inputSchema: {
2074
1325
  type: "object",
2075
1326
  properties: {
2076
- issue_id: { type: "string" },
2077
- tasks: { type: "array", items: taskItemSchema },
1327
+ session_id: { type: "string" },
1328
+ to_revision: { type: "number", description: "Keep revisions up to this value; omit for full rollback." },
1329
+ savepoint: { type: "string", description: "Rollback to a named savepoint." },
2078
1330
  },
2079
- required: ["issue_id", "tasks"],
1331
+ required: ["session_id"],
2080
1332
  additionalProperties: false,
2081
1333
  },
2082
- annotations: write,
1334
+ annotations: {
1335
+ readOnlyHint: false,
1336
+ openWorldHint: false,
1337
+ destructiveHint: false,
1338
+ },
2083
1339
  },
2084
1340
  {
2085
- name: "append_task",
1341
+ name: "finalize_edit_session",
2086
1342
  description:
2087
- "Append a stable Task to an Issue after execution has already started.",
2088
- inputSchema: {
2089
- type: "object",
2090
- properties: { issue_id: { type: "string" }, task: taskItemSchema },
2091
- required: ["issue_id", "task"],
2092
- additionalProperties: false,
2093
- },
2094
- annotations: write,
2095
- },
2096
- {
2097
- name: "split_task",
2098
- description:
2099
- "Supersede one not-running Task with two or more smaller replacement Tasks while preserving downstream dependencies.",
2100
- inputSchema: {
2101
- type: "object",
2102
- properties: {
2103
- issue_id: { type: "string" },
2104
- task_id: { type: "string" },
2105
- tasks: { type: "array", minItems: 2, items: taskItemSchema },
2106
- },
2107
- required: ["issue_id", "task_id", "tasks"],
2108
- additionalProperties: false,
2109
- },
2110
- annotations: write,
2111
- },
2112
- {
2113
- name: "supersede_task",
2114
- description:
2115
- "Mark a Task as superseded and optionally link replacement Task IDs.",
2116
- inputSchema: {
2117
- type: "object",
2118
- properties: {
2119
- issue_id: { type: "string" },
2120
- task_id: { type: "string" },
2121
- replacement_task_ids: { type: "array", items: { type: "string" } },
2122
- },
2123
- required: ["issue_id", "task_id"],
2124
- additionalProperties: false,
2125
- },
2126
- annotations: write,
2127
- },
2128
- {
2129
- name: "set_task_dependencies",
2130
- description:
2131
- "Replace one Task dependency list and revalidate the Issue DAG.",
2132
- inputSchema: {
2133
- type: "object",
2134
- properties: {
2135
- issue_id: { type: "string" },
2136
- task_id: { type: "string" },
2137
- depends_on: { type: "array", items: { type: "string" } },
2138
- },
2139
- required: ["issue_id", "task_id", "depends_on"],
2140
- additionalProperties: false,
2141
- },
2142
- annotations: write,
2143
- },
2144
- {
2145
- name: "update_task",
2146
- description: "Update one task status or append a controller note.",
1343
+ "Close an active edit session. Configured checks must pass; sessions without configured checks finalize directly.",
2147
1344
  inputSchema: {
2148
1345
  type: "object",
2149
1346
  properties: {
2150
- issue_id: { type: "string" },
2151
- task_id: { type: "string" },
2152
- status: {
2153
- type: "string",
2154
- enum: [
2155
- "backlog",
2156
- "analysis",
2157
- "planned",
2158
- "ready",
2159
- "launch_blocked",
2160
- "running",
2161
- "blocked",
2162
- "review",
2163
- "integrated",
2164
- "verifying",
2165
- "changes_requested",
2166
- "verified",
2167
- "done",
2168
- "cancelled",
2169
- "superseded",
2170
- ],
2171
- },
1347
+ session_id: { type: "string" },
1348
+ reviewer: { type: "string" },
2172
1349
  note: { type: "string" },
2173
- detail_level: { type: "string", enum: ["summary", "detail", "full"] },
1350
+ confirm_authorization: { type: "boolean", description: "Required when post-diff assurance detects protected paths or high semantic risk." },
2174
1351
  },
2175
- required: ["issue_id", "task_id"],
1352
+ required: ["session_id"],
2176
1353
  additionalProperties: false,
2177
1354
  },
2178
1355
  annotations: write,
2179
1356
  },
1357
+ );
1358
+ }
1359
+
1360
+ if (policy.execution.fixedWorkflowCheck) {
1361
+ tools.push({
1362
+ name: "run_workflow_check",
1363
+ description: "Run the fixed forge strict workflow check.",
1364
+ inputSchema: EMPTY_SCHEMA,
1365
+ annotations: write,
1366
+ });
1367
+ }
1368
+ if (opts.enableChatgptBrowser === true) {
1369
+ tools.push(
2180
1370
  {
2181
- name: "dispatch_task",
2182
- description:
2183
- "Execute one Task with an agent selected at dispatch time. Task definitions remain executor-neutral; Codex, Claude, and GitHub Copilot are optional implementation tools.",
2184
- inputSchema: {
2185
- type: "object",
2186
- properties: {
2187
- issue_id: { type: "string" },
2188
- task_id: { type: "string" },
2189
- request_id: { type: "string" },
2190
- agent: {
2191
- type: "string",
2192
- enum: ["codex", "claude", "github-copilot"],
2193
- },
2194
- timeout_ms: agentTimeoutSchema,
2195
- isolate: {
2196
- type: "boolean",
2197
- description:
2198
- "Optional. Omit for automatic placement: use the current workspace when no local Run is active and a worktree only for concurrency. false forces the current workspace and is rejected during concurrency; true forces a worktree.",
2199
- },
2200
- github_repo: { type: "string" },
2201
- base_ref: { type: "string" },
2202
- model: { type: "string" },
2203
- create_pull_request: { type: "boolean" },
2204
- approve_destructive: { type: "boolean", description: "Explicit same-request authorization for destructive or irreversible execution." },
2205
- },
2206
- required: ["issue_id", "task_id"],
2207
- additionalProperties: false,
2208
- },
2209
- annotations: {
2210
- readOnlyHint: false,
2211
- openWorldHint: false,
2212
- destructiveHint: false,
2213
- },
2214
- },
2215
- {
2216
- name: "launch_issue",
2217
- description:
2218
- "Execute independent Tasks from one Issue with an optional runtime agent override. Task definitions do not permanently bind an agent.",
2219
- inputSchema: {
2220
- type: "object",
2221
- properties: {
2222
- issue_id: { type: "string" },
2223
- max_parallel: { type: "number" },
2224
- request_id: { type: "string" },
2225
- agent: { type: "string", enum: ["codex", "claude", "github-copilot"], description: "Optional runtime executor override for selected Tasks." },
2226
- timeout_ms: agentTimeoutSchema,
2227
- isolate: {
2228
- type: "boolean",
2229
- description:
2230
- "Optional. Omit for automatic placement: use the current workspace when no local Run is active and a worktree only for concurrency. false forces the current workspace and is rejected during concurrency; true forces a worktree.",
2231
- },
2232
- github_repo: { type: "string" },
2233
- base_ref: { type: "string" },
2234
- model: { type: "string" },
2235
- create_pull_request: { type: "boolean" },
2236
- approve_destructive: { type: "boolean", description: "Explicit same-request authorization for destructive or irreversible execution." },
2237
- },
2238
- required: ["issue_id"],
2239
- additionalProperties: false,
2240
- },
2241
- annotations: {
2242
- readOnlyHint: false,
2243
- openWorldHint: true,
2244
- destructiveHint: false,
2245
- },
2246
- },
2247
- {
2248
- name: "dispatch_ready_tasks",
1371
+ name: "run_chatgpt_browser_consult",
2249
1372
  description:
2250
- "Execute up to max_parallel independent Tasks with an optional runtime agent override.",
2251
- inputSchema: {
2252
- type: "object",
2253
- properties: {
2254
- issue_id: { type: "string" },
2255
- max_parallel: { type: "number" },
2256
- request_id: { type: "string" },
2257
- agent: { type: "string", enum: ["codex", "claude", "github-copilot"], description: "Optional runtime executor override for selected Tasks." },
2258
- timeout_ms: agentTimeoutSchema,
2259
- isolate: {
2260
- type: "boolean",
2261
- description:
2262
- "Optional. Omit for automatic placement: use the current workspace when no local Run is active and a worktree only for concurrency. false forces the current workspace and is rejected during concurrency; true forces a worktree.",
2263
- },
2264
- github_repo: { type: "string" },
2265
- base_ref: { type: "string" },
2266
- model: { type: "string" },
2267
- create_pull_request: { type: "boolean" },
2268
- approve_destructive: { type: "boolean", description: "Explicit same-request authorization for destructive or irreversible execution." },
2269
- },
2270
- additionalProperties: false,
2271
- },
1373
+ "Run a local ChatGPT Web browser consult through forge. This may create a real ChatGPT Web conversation unless dryRun is true.",
1374
+ inputSchema: browserRunSchema,
2272
1375
  annotations: {
2273
1376
  readOnlyHint: false,
2274
1377
  openWorldHint: true,
@@ -2276,503 +1379,54 @@ export function buildMcpToolDefinitions(
2276
1379
  },
2277
1380
  },
2278
1381
  {
2279
- name: "get_task_run",
2280
- description:
2281
- "Read one persistent agent run with current status, bounded output tails, and opt-in path details.",
2282
- inputSchema: {
2283
- type: "object",
2284
- properties: {
2285
- run_id: { type: "string" },
2286
- include_paths: { type: "boolean", description: "Opt in to full local filesystem paths and PIDs." },
2287
- include_output: { type: "boolean", description: "Return bounded stdout/stderr tails. Defaults to true." },
2288
- tail_chars: { type: "number", description: "Per-stream character budget for the bounded tails; capped at 16384." },
2289
- },
2290
- required: ["run_id"],
2291
- additionalProperties: false,
2292
- },
2293
- annotations: readOnly,
2294
- },
2295
- {
2296
- name: "get_task_run_events",
1382
+ name: "read_chatgpt_browser_session",
2297
1383
  description:
2298
- "Read structured lifecycle events for a local or GitHub cloud Task Run.",
2299
- inputSchema: {
2300
- type: "object",
2301
- properties: {
2302
- run_id: { type: "string" },
2303
- limit: { type: "number" },
2304
- since_event_index: { type: "number" },
2305
- include_heartbeats: { type: "boolean" },
2306
- },
2307
- required: ["run_id"],
2308
- additionalProperties: false,
2309
- },
1384
+ "Read a saved forge ChatGPT browser consult session.",
1385
+ inputSchema: browserSessionSchema,
2310
1386
  annotations: readOnly,
2311
1387
  },
2312
1388
  {
2313
- name: "get_task_run_log",
1389
+ name: "list_chatgpt_browser_sessions",
2314
1390
  description:
2315
- "Read a bounded, redacted tail of current local output or GitHub cloud-agent session logs. GitHub logs can be watched directly in the linked Agents UI.",
1391
+ "List saved forge ChatGPT browser consult sessions.",
2316
1392
  inputSchema: {
2317
1393
  type: "object",
2318
- properties: {
2319
- run_id: { type: "string" },
2320
- max_bytes: { type: "number", description: "Bounded log tail size; capped at 1 MiB." },
2321
- },
2322
- required: ["run_id"],
1394
+ properties: { limit: { type: "number" } },
2323
1395
  additionalProperties: false,
2324
1396
  },
2325
1397
  annotations: readOnly,
2326
1398
  },
2327
1399
  {
2328
- name: "get_task_diff",
1400
+ name: "open_chatgpt_browser_session",
2329
1401
  description:
2330
- "Inspect the Git status and diff from one isolated Task Run worktree.",
2331
- inputSchema: {
2332
- type: "object",
2333
- properties: {
2334
- run_id: { type: "string" },
2335
- max_bytes: { type: "number" },
2336
- },
2337
- required: ["run_id"],
2338
- additionalProperties: false,
2339
- },
1402
+ "Return the ChatGPT conversation URL for a saved browser session. The MCP tool does not launch the local browser.",
1403
+ inputSchema: browserSessionSchema,
2340
1404
  annotations: readOnly,
2341
1405
  },
2342
1406
  {
2343
- name: "integrate_task_run",
2344
- description:
2345
- "Explicitly integrate one completed isolated Task Run into this dispatch checkout through a rollback-capable edit session.",
2346
- inputSchema: {
2347
- type: "object",
2348
- properties: { run_id: { type: "string" } },
2349
- required: ["run_id"],
2350
- additionalProperties: false,
2351
- },
2352
- annotations: {
2353
- readOnlyHint: false,
2354
- openWorldHint: false,
2355
- destructiveHint: true,
2356
- },
2357
- },
2358
- {
2359
- name: "finish_task_run",
1407
+ name: "continue_chatgpt_browser_session",
2360
1408
  description:
2361
- "Finish one reviewed Run in a single policy-gated step: integrate if needed, verify, accept when allowed, and clean the isolated worktree. Use decision=auto for low/medium trusted work; use approve_and_finish only for explicit human approval.",
1409
+ "Create a follow-up ChatGPT browser consult record linked to an existing session.",
2362
1410
  inputSchema: {
2363
1411
  type: "object",
2364
1412
  properties: {
2365
- run_id: { type: "string" },
2366
- decision: {
2367
- type: "string",
2368
- enum: ["auto", "approve_and_finish", "request_changes", "discard"],
2369
- },
2370
- reviewer: { type: "string" },
2371
- note: { type: "string" },
2372
- cleanup: { type: "boolean" },
2373
- commit: { type: "boolean", description: "Create a selected-path Git commit after successful finish. Refuses to mix with pre-staged changes." },
1413
+ sessionId: { type: "string" },
1414
+ prompt: { type: "string" },
1415
+ dryRun: { type: "boolean" },
2374
1416
  },
2375
- required: ["run_id"],
1417
+ required: ["sessionId", "prompt"],
2376
1418
  additionalProperties: false,
2377
1419
  },
2378
1420
  annotations: {
2379
1421
  readOnlyHint: false,
2380
- openWorldHint: false,
1422
+ openWorldHint: true,
2381
1423
  destructiveHint: false,
2382
1424
  },
2383
1425
  },
2384
- {
2385
- name: "list_task_runs",
2386
- description: "List recent persistent agent runs.",
2387
- inputSchema: {
2388
- type: "object",
2389
- properties: {
2390
- limit: { type: "number" },
2391
- include_paths: { type: "boolean", description: "Opt in to full local filesystem paths and PIDs." },
2392
- },
2393
- additionalProperties: false,
2394
- },
2395
- annotations: readOnly,
2396
- },
2397
- {
2398
- name: "cancel_task_run",
2399
- description: "Cancel a queued or running local agent task.",
2400
- inputSchema: {
2401
- type: "object",
2402
- properties: { run_id: { type: "string" } },
2403
- required: ["run_id"],
2404
- additionalProperties: false,
2405
- },
2406
- annotations: {
2407
- readOnlyHint: false,
2408
- openWorldHint: false,
2409
- destructiveHint: true,
2410
- },
2411
- },
2412
- {
2413
- name: "retry_task_run",
2414
- description:
2415
- "Create a new run for a failed, blocked, cancelled, or change-requested task.",
2416
- inputSchema: {
2417
- type: "object",
2418
- properties: {
2419
- run_id: { type: "string" },
2420
- timeout_ms: agentTimeoutSchema,
2421
- isolate: {
2422
- type: "boolean",
2423
- description:
2424
- "Optional. Omit for automatic placement: use the current workspace when no local Run is active and a worktree only for concurrency. false forces the current workspace and is rejected during concurrency; true forces a worktree.",
2425
- },
2426
- },
2427
- required: ["run_id"],
2428
- additionalProperties: false,
2429
- },
2430
- annotations: write,
2431
- },
2432
- {
2433
- name: "verify_task",
2434
- description:
2435
- "Record task-local completion evidence. Declared checks execute through persisted Process Runtime receipts; retries reuse the same processes and reported command evidence can satisfy risk-adaptive verification.",
2436
- inputSchema: {
2437
- type: "object",
2438
- properties: {
2439
- issue_id: { type: "string" },
2440
- task_id: { type: "string" },
2441
- run_id: { type: "string" },
2442
- integrated_revision: { type: "string" },
2443
- reviewed_diff_hash: { type: "string" },
2444
- reviewer: { type: "string" },
2445
- request_id: { type: "string", description: "Stable idempotency key for Process Runtime checks; retries reuse the original processes." },
2446
- check_results: {
2447
- type: "array",
2448
- items: {
2449
- type: "object",
2450
- properties: {
2451
- check_id: { type: "string" },
2452
- ok: { type: "boolean", description: "Deprecated compatibility field. The Controller executes the named check and uses the actual outcome." },
2453
- summary: { type: "string" },
2454
- },
2455
- required: ["check_id"],
2456
- additionalProperties: false,
2457
- },
2458
- },
2459
- reported_commands: {
2460
- type: "array",
2461
- items: {
2462
- type: "object",
2463
- properties: {
2464
- command: { type: "array", items: { type: "string" }, minItems: 1 },
2465
- cwd: { type: "string" },
2466
- ok: { type: "boolean" },
2467
- exit_code: { type: "number" },
2468
- stdout: { type: "string" },
2469
- stderr: { type: "string" },
2470
- artifact_path: { type: "string" },
2471
- executed_at: { type: "string" },
2472
- },
2473
- required: ["command", "ok"],
2474
- additionalProperties: false,
2475
- },
2476
- },
2477
- acceptance_results: {
2478
- type: "array",
2479
- items: {
2480
- type: "object",
2481
- properties: {
2482
- criterion: { type: "string" },
2483
- ok: { type: "boolean" },
2484
- evidence: { type: "string" },
2485
- },
2486
- required: ["criterion", "ok"],
2487
- additionalProperties: false,
2488
- },
2489
- },
2490
- },
2491
- required: ["issue_id", "task_id", "reviewer"],
2492
- additionalProperties: false,
2493
- },
2494
- annotations: write,
2495
- },
2496
- {
2497
- name: "accept_task",
2498
- description:
2499
- "Accept a verified task, mark it done, and unlock dependent tasks.",
2500
- inputSchema: {
2501
- type: "object",
2502
- properties: {
2503
- issue_id: { type: "string" },
2504
- task_id: { type: "string" },
2505
- work_id: { type: "string" },
2506
- note: { type: "string" },
2507
- },
2508
- required: ["issue_id", "task_id"],
2509
- additionalProperties: false,
2510
- },
2511
- annotations: write,
2512
- },
2513
- {
2514
- name: "request_task_changes",
2515
- description:
2516
- "Return a reviewed task for another focused execution run.",
2517
- inputSchema: {
2518
- type: "object",
2519
- properties: {
2520
- issue_id: { type: "string" },
2521
- task_id: { type: "string" },
2522
- note: { type: "string" },
2523
- },
2524
- required: ["issue_id", "task_id", "note"],
2525
- additionalProperties: false,
2526
- },
2527
- annotations: write,
2528
- },
2529
- {
2530
- name: "begin_edit_session",
2531
- description:
2532
- "Open a bounded direct-edit session tied to a purpose and optional Issue/Task.",
2533
- inputSchema: {
2534
- type: "object",
2535
- properties: {
2536
- purpose: { type: "string" },
2537
- work_id: { type: "string", description: "Optional existing WorkContract. Omit to create lightweight direct-control Work lineage." },
2538
- issue_id: { type: "string" },
2539
- task_id: { type: "string" },
2540
- allowed_paths: { type: "array", items: { type: "string" } },
2541
- max_files: { type: "number" },
2542
- max_changed_lines: { type: "number" },
2543
- checks: { type: "array", items: { type: "string" }, description: "Named checks to run before finalization." },
2544
- },
2545
- required: ["purpose"],
2546
- additionalProperties: false,
2547
- },
2548
- annotations: write,
2549
- },
2550
- {
2551
- name: "apply_patch",
2552
- description:
2553
- "Append one bounded patch batch to an active edit session. Prefer batches of 100 operations or fewer and pass expected_revision to avoid stale revisions.",
2554
- inputSchema: {
2555
- type: "object",
2556
- properties: {
2557
- session_id: { type: "string" },
2558
- expected_revision: { type: "number", description: "Optional current revision precondition for the edit session." },
2559
- operations: {
2560
- type: "array",
2561
- items: EDIT_OPERATION_INPUT_SCHEMA,
2562
- },
2563
- },
2564
- required: ["session_id", "operations"],
2565
- additionalProperties: false,
2566
- },
2567
- annotations: {
2568
- readOnlyHint: false,
2569
- openWorldHint: false,
2570
- destructiveHint: false,
2571
- },
2572
- },
2573
- {
2574
- name: "get_edit_session",
2575
- description: "Read an edit session, revisions, savepoints, and operation manifest.",
2576
- inputSchema: {
2577
- type: "object",
2578
- properties: { session_id: { type: "string" } },
2579
- required: ["session_id"],
2580
- additionalProperties: false,
2581
- },
2582
- annotations: readOnly,
2583
- },
2584
- {
2585
- name: "list_edit_sessions",
2586
- description: "List recent direct-edit sessions with revision, changed-file, and check summaries.",
2587
- inputSchema: {
2588
- type: "object",
2589
- properties: { limit: { type: "number" } },
2590
- additionalProperties: false,
2591
- },
2592
- annotations: readOnly,
2593
- },
2594
- {
2595
- name: "get_edit_session_diff",
2596
- description: "Read the current aggregate localized diff for one direct-edit session.",
2597
- inputSchema: {
2598
- type: "object",
2599
- properties: { session_id: { type: "string" } },
2600
- required: ["session_id"],
2601
- additionalProperties: false,
2602
- },
2603
- annotations: readOnly,
2604
- },
2605
- {
2606
- name: "create_edit_savepoint",
2607
- description: "Name the current edit revision so later patch batches can be rolled back to it.",
2608
- inputSchema: {
2609
- type: "object",
2610
- properties: {
2611
- session_id: { type: "string" },
2612
- name: { type: "string" },
2613
- },
2614
- required: ["session_id", "name"],
2615
- additionalProperties: false,
2616
- },
2617
- annotations: write,
2618
- },
2619
- {
2620
- name: "verify_edit_session",
2621
- description: "Verify the current edit revision through persisted Process Runtime checks. Retries with the same request_id reuse the original process and terminal receipts are attached to the exact revision.",
2622
- inputSchema: {
2623
- type: "object",
2624
- properties: {
2625
- session_id: { type: "string" },
2626
- check_ids: { type: "array", items: { type: "string" } },
2627
- request_id: { type: "string" },
2628
- reviewer: { type: "string" },
2629
- note: { type: "string" },
2630
- },
2631
- required: ["session_id"],
2632
- additionalProperties: false,
2633
- },
2634
- annotations: write,
2635
- },
2636
- {
2637
- name: "rollback_edit_session",
2638
- description:
2639
- "Rollback an active edit session completely, to a prior revision, or to a named savepoint.",
2640
- inputSchema: {
2641
- type: "object",
2642
- properties: {
2643
- session_id: { type: "string" },
2644
- to_revision: { type: "number", description: "Keep revisions up to this value; omit for full rollback." },
2645
- savepoint: { type: "string", description: "Rollback to a named savepoint." },
2646
- },
2647
- required: ["session_id"],
2648
- additionalProperties: false,
2649
- },
2650
- annotations: {
2651
- readOnlyHint: false,
2652
- openWorldHint: false,
2653
- destructiveHint: false,
2654
- },
2655
- },
2656
- {
2657
- name: "finalize_edit_session",
2658
- description:
2659
- "Close an active edit session. Configured checks must pass; sessions without configured checks finalize directly.",
2660
- inputSchema: {
2661
- type: "object",
2662
- properties: {
2663
- session_id: { type: "string" },
2664
- reviewer: { type: "string" },
2665
- note: { type: "string" },
2666
- confirm_authorization: { type: "boolean", description: "Required when post-diff assurance detects protected paths or high semantic risk." },
2667
- },
2668
- required: ["session_id"],
2669
- additionalProperties: false,
2670
- },
2671
- annotations: write,
2672
- },
2673
- {
2674
- name: "finish_edit_session",
2675
- description:
2676
- "Verify, finalize, selected-path commit, receipt, and complete the Task bound to one Direct Edit session.",
2677
- inputSchema: {
2678
- type: "object",
2679
- properties: {
2680
- session_id: { type: "string" },
2681
- decision: { type: "string", enum: ["auto", "approve_and_finish", "request_changes", "discard"] },
2682
- reviewer: { type: "string" },
2683
- note: { type: "string" },
2684
- check_ids: { type: "array", items: { type: "string" } },
2685
- commit: { type: "boolean" },
2686
- },
2687
- required: ["session_id"],
2688
- additionalProperties: false,
2689
- },
2690
- annotations: write,
2691
- },
2692
1426
  );
2693
1427
  }
2694
-
2695
- if (policy.execution.fixedWorkflowCheck) {
2696
- tools.push({
2697
- name: "run_workflow_check",
2698
- description: "Run the fixed forge strict workflow check.",
2699
- inputSchema: EMPTY_SCHEMA,
2700
- annotations: write,
2701
- });
2702
- }
2703
- if (opts.enableChatgptBrowser === true) {
2704
- tools.push(
2705
- {
2706
- name: "run_chatgpt_browser_consult",
2707
- description:
2708
- "Run a local ChatGPT Web browser consult through forge. This may create a real ChatGPT Web conversation unless dryRun is true.",
2709
- inputSchema: browserRunSchema,
2710
- annotations: {
2711
- readOnlyHint: false,
2712
- openWorldHint: true,
2713
- destructiveHint: false,
2714
- },
2715
- },
2716
- {
2717
- name: "read_chatgpt_browser_session",
2718
- description:
2719
- "Read a saved forge ChatGPT browser consult session.",
2720
- inputSchema: browserSessionSchema,
2721
- annotations: readOnly,
2722
- },
2723
- {
2724
- name: "list_chatgpt_browser_sessions",
2725
- description:
2726
- "List saved forge ChatGPT browser consult sessions.",
2727
- inputSchema: {
2728
- type: "object",
2729
- properties: { limit: { type: "number" } },
2730
- additionalProperties: false,
2731
- },
2732
- annotations: readOnly,
2733
- },
2734
- {
2735
- name: "open_chatgpt_browser_session",
2736
- description:
2737
- "Return the ChatGPT conversation URL for a saved browser session. The MCP tool does not launch the local browser.",
2738
- inputSchema: browserSessionSchema,
2739
- annotations: readOnly,
2740
- },
2741
- {
2742
- name: "continue_chatgpt_browser_session",
2743
- description:
2744
- "Create a follow-up ChatGPT browser consult record linked to an existing session.",
2745
- inputSchema: {
2746
- type: "object",
2747
- properties: {
2748
- sessionId: { type: "string" },
2749
- prompt: { type: "string" },
2750
- dryRun: { type: "boolean" },
2751
- },
2752
- required: ["sessionId", "prompt"],
2753
- additionalProperties: false,
2754
- },
2755
- annotations: {
2756
- readOnlyHint: false,
2757
- openWorldHint: true,
2758
- destructiveHint: false,
2759
- },
2760
- },
2761
- );
2762
- }
2763
- return tools.filter((tool) => !RETIRED_LEGACY_PUBLIC_TOOLS.has(tool.name));
2764
- }
2765
-
2766
- export function controllerExpectedToolNames(
2767
- policy: McpPolicy,
2768
- opts: { enableChatgptBrowser?: boolean } = {},
2769
- ): string[] {
2770
- const names = buildMcpToolDefinitions(policy, opts).map((tool) => tool.name);
2771
- if (policy.profile !== "controller") return names;
2772
- // The Connector readiness expectation must match the bounded default schema
2773
- // served by toolset.ts. Full compatibility tools are intentionally omitted.
2774
- return [...DEFAULT_CONTROLLER_TOOL_NAMES];
2775
- }
1428
+ return tools.filter((tool) => !RETIRED_LEGACY_PUBLIC_TOOLS.has(tool.name));
1429
+ }
2776
1430
 
2777
1431
  const RETIRED_LEGACY_PUBLIC_TOOLS = new Set([
2778
1432
  "list_issues",
@@ -2830,1539 +1484,645 @@ export async function callMcpTool(
2830
1484
  }
2831
1485
  switch (name) {
2832
1486
  case "harness_status": {
2833
- const roots = [
2834
- "docs/spec.md",
2835
- "plans",
2836
- "tasks/current.md",
2837
- ".ai/harness/session",
2838
- ".ai/harness/controller/packets",
2839
- ".ai/harness/projections",
2840
- ".ai/harness/checks",
2841
- ];
2842
- audit(ctx, name, "ok", args);
2843
- return textResult({
2844
- repoRoot: ctx.repoRoot,
2845
- adopted: isForgeAdopted(ctx.repoRoot),
2846
- profile: ctx.policy.profile,
2847
- branch: currentGitBranch(ctx.repoRoot),
2848
- workflowRoots: roots.map((path) => ({
2849
- path,
2850
- exists: existsSync(join(ctx.repoRoot, path)),
2851
- })),
2852
- });
2853
- }
2854
- case "harness_doctor": {
2855
- const localConfig = existsSync(
2856
- join(ctx.repoRoot, ".forge", "mcp.local.json"),
2857
- );
2858
- const codexConfig = existsSync(
2859
- join(ctx.repoRoot, ".codex", "config.toml"),
2860
- );
2861
- audit(ctx, name, "ok", args);
2862
- return textResult({
2863
- status: isForgeAdopted(ctx.repoRoot)
2864
- ? "ready_local"
2865
- : "not_adopted",
2866
- repo: ctx.repoRoot,
2867
- profile: ctx.policy.profile,
2868
- mcp: {
2869
- localConfig,
2870
- policy: "builtin",
2871
- profile: ctx.policy.profile,
2872
- toolSurface:
2873
- ctx.policy.profile === "controller"
2874
- ? FORGE_TOOL_SURFACE
2875
- : `${ctx.policy.profile}-legacy-v1`,
2876
- toolCount: buildMcpToolDefinitions(ctx.policy, {
2877
- enableChatgptBrowser: ctx.enableChatgptBrowser === true,
2878
- }).length,
2879
- repositoryRead:
2880
- ctx.policy.profile === "controller"
2881
- ? "ordinary repository source and document files are readable; immutable secret and runtime paths remain denied"
2882
- : "workflow-scoped",
2883
- deniedPaths: ctx.policy.denyGlobs.length,
2884
- runner: {
2885
- enabled: ctx.policy.execution.agentRunner,
2886
- defaultTimeoutMs: ctx.policy.execution.runnerTimeoutMs,
2887
- maxTimeoutMs: ctx.policy.execution.runnerMaxTimeoutMs,
2888
- },
2889
- },
2890
- codex: {
2891
- configured: codexConfig,
2892
- fix: codexConfig
2893
- ? null
2894
- : "forge mcp setup codex --repo . --scope project",
2895
- },
2896
- chatgpt: {
2897
- localEndpoint: "http://127.0.0.1:8765/mcp",
2898
- manualStepsRequired: true,
2899
- guide: "docs/forge-chatgpt-mcp-setup.md",
2900
- },
2901
- });
2902
- }
2903
- case "controller_capabilities": {
2904
- if (ctx.policy.profile !== "controller")
2905
- return errorResult(
2906
- "TOOL_DISABLED",
2907
- "controller_capabilities requires the controller profile",
2908
- );
2909
- const expectedTools = controllerExpectedToolNames(ctx.policy, {
2910
- enableChatgptBrowser: ctx.enableChatgptBrowser === true,
2911
- });
2912
- const preferredTools: string[] = [...PREFERRED_FACADE_TOOL_NAMES];
2913
- const capabilityDescriptors = listCapabilityDescriptors();
2914
- const connectorSnapshot = Array.isArray(args.connector_tool_names)
2915
- ? args.connector_tool_names.map(String)
2916
- : Array.isArray(args.connectorToolNames)
2917
- ? args.connectorToolNames.map(String)
2918
- : undefined;
2919
- const connectorFreshness = await buildLocalConnectorStatusForRepo({
2920
- repoRoot: ctx.repoRoot,
2921
- expectedTools,
2922
- connectorToolNames: connectorSnapshot,
2923
- });
2924
- // staleConnector only when ChatGPT snapshot is supplied and missing rh_*.
2925
- const missingFacadeTools = connectorFreshness.missingConnectorTools;
2926
- const staleConnector = connectorFreshness.status === "chatgpt_snapshot_missing_facade";
2927
- const payload = {
2928
- schemaVersion: FORGE_MCP_SCHEMA_VERSION,
2929
- toolSurface: FORGE_TOOL_SURFACE,
2930
- version: FORGE_VERSION,
2931
- toolSurfaceFingerprint: forgeToolSurfaceFingerprint(expectedTools),
2932
- executionModel: "chatgpt-controller-execution-bridge",
2933
- profile: ctx.policy.profile,
2934
- capabilities: {
2935
- repositoryInspection: true,
2936
- issueLauncher: true,
2937
- githubIssuesAndProjects: true,
2938
- localAgentRuns: ctx.policy.execution.agentRunner,
2939
- githubCopilotSessions: true,
2940
- dynamicTaskGraph: true,
2941
- runtimeAgentSelection: true,
2942
- taskAgentBinding: false,
2943
- structuredRunEvents: true,
2944
- boundedDirectEdits: true,
2945
- multiRevisionDirectEdits: true,
2946
- editSavepoints: true,
2947
- partialEditRollback: true,
2948
- directEditFirstRouting: true,
2949
- persistedEditDiffs: true,
2950
- verifiedEditFinalization: true,
2951
- verificationGate: true,
2952
- localRunWatch: true,
2953
- localVisualController: true,
2954
- hierarchicalControllerUI: true,
2955
- localApprovalQueue: false,
2956
- localRiskApprovalGate: false,
2957
- quickAgentSessions: true,
2958
- persistentTimeouts: true,
2959
- timeoutProgress: true,
2960
- connectorSchemaDiagnostics: true,
2961
- projectProgressAggregation: true,
2962
- effectiveTaskStatus: true,
2963
- unifiedWorklogTimeline: true,
2964
- exportableWorklogReports: true,
2965
- githubPluginConfiguration: true,
2966
- serverSentEventDashboard: true,
2967
- singleExecutionFocus: false,
2968
- taskLocalReadiness: true,
2969
- riskAdaptiveVerification: true,
2970
- ephemeralQuickAgentSessions: true,
2971
- automaticRunContinuation: true,
2972
- reportedCommandEvidence: true,
2973
- governanceDiagnostics: true,
2974
- safeGovernanceReconciliation: true,
2975
- evidenceGateProgress: true,
2976
- directTaskActions: true,
2977
- issueArchiving: true,
2978
- retryableRunFailures: true,
2979
- issueCreationGuardrails: true,
2980
- persistedCheckEvidence: true,
2981
- persistedCheckReuse: true,
2982
- directVerification: true,
2983
- controllerContextAggregation: true,
2984
- taskLedgerProjection: true,
2985
- assistantPluginRuntime: true,
2986
- chatgptFacadeTools: true,
2987
- goalWorkloop: true,
2988
- handoffInbox: true,
2989
- codexGrokDelegation: true,
2990
- selfHealingRepair: true,
2991
- },
2992
- runner: {
2993
- enabled: ctx.policy.execution.agentRunner,
2994
- allowedAgents: ctx.policy.execution.allowedAgents,
2995
- defaultTimeoutMs: ctx.policy.execution.runnerTimeoutMs,
2996
- defaultTimeout: formatDurationMs(
2997
- ctx.policy.execution.runnerTimeoutMs,
2998
- ),
2999
- maxTimeoutMs: ctx.policy.execution.runnerMaxTimeoutMs,
3000
- maxTimeout: formatDurationMs(
3001
- ctx.policy.execution.runnerMaxTimeoutMs,
3002
- ),
3003
- note: "Requested timeout values are validated explicitly and are never silently reduced to the default.",
3004
- },
3005
- preferredTools,
3006
- capabilityGroups: summarizeCapabilityGroups(),
3007
- toolArchitecture: {
3008
- facadeTools: preferredTools,
3009
- atomicTypedToolsRetained: true,
3010
- internalHandlersRetained: true,
3011
- domainSchemaLoading: 'static_stable_surface',
3012
- dynamicDomainSchemaLoadingSupported: false,
3013
- capabilityDescriptorCount: capabilityDescriptors.length,
3014
- },
3015
- toolExposure: {
3016
- preferred: preferredTools,
3017
- note: "Prefer the five rh_* workflow facades for orchestration. Typed repository, Git, Issue/Task, plugin, browser, iOS, artifact, and recovery tools remain available because the current MCP transport cannot load domain schemas per conversation.",
3018
- advanced: expectedTools.filter((tool) => !preferredTools.includes(tool)).slice(0, 40),
3019
- compatibilityRetained: true,
3020
- },
3021
- expectedTools,
3022
- facadeToolsPresentInExpectedTools: preferredTools.every((tool: string) => expectedTools.includes(tool)),
3023
- staleConnector,
3024
- missingFacadeTools,
3025
- connectorFreshness: {
3026
- status: connectorFreshness.status,
3027
- severity: connectorFreshness.severity,
3028
- summary: connectorFreshness.summary,
3029
- missingLocalTools: connectorFreshness.missingLocalTools,
3030
- missingConnectorTools: connectorFreshness.missingConnectorTools,
3031
- expectedFacadeTools: connectorFreshness.expectedFacadeTools,
3032
- restartRecommended: connectorFreshness.restartRecommended,
3033
- reconnectRecommended: connectorFreshness.reconnectRecommended,
3034
- suggestedActions: connectorFreshness.suggestedActions,
3035
- },
3036
- docs: [
3037
- "docs/architecture/current/README.md",
3038
- "docs/architecture/history.md",
3039
- "docs/operations/controller-reliability-runbook.md",
3040
- "docs/forge-github-issue-launcher.md",
3041
- "docs/forge-chatgpt-mcp-setup.md",
3042
- ],
3043
- staleConnectorHint: staleConnector
3044
- ? `Stale ChatGPT connector tool snapshot is missing facade tools: ${missingFacadeTools.join(", ")}. Refresh/reconnect MCP so tools/list reloads rh_status, rh_inbox, rh_context, and rh_work.`
3045
- : connectorFreshness.status === "unable_to_verify_chatgpt_snapshot"
3046
- ? `Local MCP tool surface includes preferred facade tools. ChatGPT connector snapshot was not supplied, so freshness cannot be verified from this call. If ChatGPT tools/list is missing rh_status/rh_inbox/rh_context/rh_work, reconnect MCP. Pass connector_tool_names for an exact check.`
3047
- : connectorFreshness.status === "stale_fingerprint"
3048
- ? `Local MCP process fingerprint/version may be stale. Restart controller/MCP, then reconnect ChatGPT if rh_* are still missing.`
3049
- : connectorFreshness.status === "local_mcp_missing_facade"
3050
- ? `Local MCP tool surface is missing facade tools: ${connectorFreshness.missingLocalTools.join(", ")}. Restart controller/MCP.`
3051
- : `Connector clients must match ${FORGE_TOOL_SURFACE} version ${FORGE_VERSION} / schema ${FORGE_MCP_SCHEMA_VERSION}. Prefer rh_status/rh_inbox/rh_context/rh_work. Pass connector_tool_names to detect a stale ChatGPT tool snapshot precisely.`,
3052
- };
3053
- audit(ctx, name, "ok", args);
3054
- return textResult(payload);
3055
- }
3056
- case "local_bridge_status": {
3057
- if (ctx.policy.profile !== "controller")
3058
- return errorResult(
3059
- "TOOL_DISABLED",
3060
- "local_bridge_status requires the controller profile",
3061
- );
3062
- const detailLevel = args.detail_level === "detail" || args.detail === true
3063
- ? "detail"
3064
- : "summary";
3065
- const controllerHome = resolveRepoPreferredControllerHome(ctx.repoRoot);
3066
- const surface = resolveLocalBridgeSurface({
3067
- controllerHome,
3068
- repoRoot: ctx.repoRoot,
3069
- allowProcessScan: detailLevel === "detail",
3070
- });
3071
- const jobs = listLocalBridgeJobs(ctx.repoRoot, detailLevel === "detail" ? 12 : 20);
3072
- const { activeJobCount, recentJobSummary } = summarizeRecentJobs(jobs);
3073
- // Prefer authoritative surface; never invent a legacy 8766 endpoint.
3074
- const endpoint = surface.endpoint ?? null;
3075
- const running = surface.enabled
3076
- && (surface.processRunning === true
3077
- || (surface.endpointConfigured && surface.mode === "embedded"));
3078
- if (detailLevel === "summary") {
3079
- const payload = {
3080
- detailLevel: "summary",
3081
- repoId: ctx.repoId,
3082
- running,
3083
- ready: surface.enabled ? (surface.processRunning !== false) : true,
3084
- health: surface.enabled
3085
- ? (surface.processRunning === true || surface.mode === "embedded" ? "ready" : "warning")
3086
- : "disabled",
3087
- mode: surface.mode,
3088
- endpoint,
3089
- endpointConfigured: surface.endpointConfigured,
3090
- endpointReachable: null as boolean | null,
3091
- processRunning: surface.processRunning ?? null,
3092
- expectedSurface: surface.expectedSurface,
3093
- requiredForReadiness: surface.requiredForReadiness,
3094
- warnings: surface.error && surface.requiredForReadiness
3095
- ? [{ code: "LOCAL_BRIDGE_ERROR", message: surface.error.slice(0, 300) }]
3096
- : [],
3097
- activeJobCount,
3098
- recentJobSummary,
3099
- statusSource: surface.source,
3100
- nonBlocking: !surface.requiredForReadiness,
3101
- approvalQueue: false,
3102
- };
3103
- audit(ctx, name, "ok", args);
3104
- return textResult(payload);
3105
- }
3106
- const reconciliation = reconcileLocalBridgeJobs(ctx.repoRoot);
3107
- const payload = {
3108
- detailLevel: "detail",
3109
- endpoint,
3110
- endpointConfigured: surface.endpointConfigured,
3111
- running,
3112
- error: surface.error,
3113
- mode: surface.mode,
3114
- requiredForReadiness: surface.requiredForReadiness,
3115
- statusSource: surface.source,
3116
- counts: recentJobSummary,
3117
- activeJobCount,
3118
- recentJobSummary,
3119
- approvalQueue: false,
3120
- reconciliation,
3121
- recentJobs: jobs.map((job) => ({
3122
- jobId: job.jobId,
3123
- action: job.action,
3124
- status: job.status,
3125
- checkId: job.action === "run-check" ? (job.payload as { checkId?: string }).checkId : undefined,
3126
- runId: job.runId,
3127
- issueId: job.issueId,
3128
- taskId: job.taskId,
3129
- createdAt: job.createdAt,
3130
- updatedAt: job.updatedAt,
3131
- finishedAt: job.finishedAt,
3132
- revision: job.revision,
3133
- deadlineAt: job.deadlineAt,
3134
- error: job.error?.slice(0, 300),
3135
- })),
3136
- nonBlocking: !surface.requiredForReadiness,
3137
- fallback:
3138
- "Open the localhost Local Controller to launch work or inspect execution when a ChatGPT write action is unavailable.",
3139
- };
3140
- audit(ctx, name, "ok", args);
3141
- return textResult(payload);
3142
- }
3143
- case "controller_context": {
3144
- if (ctx.policy.profile !== "controller")
3145
- return errorResult(
3146
- "TOOL_DISABLED",
3147
- "controller_context requires the controller profile",
3148
- );
3149
- const controllerHome = resolveRepoPreferredControllerHome(ctx.repoRoot);
3150
- const requirementBoard = buildRequirementBoard({ controllerHome, repoId: ctx.repoId });
3151
- const executionDiagnostics = buildExecutionDiagnostics({
3152
- controllerHome,
3153
- repoId: ctx.repoId,
3154
- detailLevel: "detail",
3155
- });
3156
- const runs = listAgentJobs(ctx.repoRoot, 8);
3157
- const reconciliation = reconcileLocalBridgeJobs(ctx.repoRoot);
3158
- const jobs = listLocalBridgeJobs(ctx.repoRoot, 8);
3159
- const checks = listControllerChecks(ctx.repoRoot).map((check) => ({
3160
- id: check.id,
3161
- description: check.description,
3162
- timeoutMs: check.timeoutMs,
3163
- source: check.source,
3164
- }));
3165
- const payload = {
3166
- git: gitSnapshot(ctx.repoRoot),
3167
- requirementBoard,
3168
- executionDiagnostics,
3169
- legacyCompatibility: {
3170
- currentIssue: "deprecated_frozen",
3171
- issueTaskBoard: "deprecated_frozen",
3172
- authority: "controller-home-sqlite",
3173
- },
3174
- activeRuns: runs.map((run) => ({
3175
- runId: run.runId,
3176
- issueId: run.issueId,
3177
- taskId: run.taskId,
3178
- status: run.status,
3179
- agent: run.agent,
3180
- provider: run.provider,
3181
- executionMode: run.executionMode,
3182
- progress: run.progress,
3183
- lastHeartbeatAt: run.lastHeartbeatAt,
3184
- error: run.error?.slice(0, 300),
3185
- })),
3186
- localBridge: {
3187
- reconciliation,
3188
- recentJobs: jobs.map((job) => ({
3189
- jobId: job.jobId,
3190
- action: job.action,
3191
- status: job.status,
3192
- checkId: job.action === "run-check" ? (job.payload as { checkId?: string }).checkId : undefined,
3193
- issueId: job.issueId,
3194
- taskId: job.taskId,
3195
- updatedAt: job.updatedAt,
3196
- })),
3197
- },
3198
- checks,
3199
- };
3200
- audit(ctx, name, "ok", args);
3201
- return textResult(payload);
3202
- }
3203
- case "quick_agent_session":
3204
- case "submit_local_job": {
3205
- if (ctx.policy.profile !== "controller") {
3206
- return errorResult("TOOL_DISABLED", `${name} requires the controller profile`);
3207
- }
3208
- audit(ctx, name, "blocked", args);
3209
- return errorResult(
3210
- name === "quick_agent_session" ? "AGENT_RUN_DEPRECATED" : "LOCAL_BRIDGE_JOB_RETIRED",
3211
- "Kernel-managed Agent and Local Bridge Jobs are retired. Create or claim WorkContract and use Process Runtime or the Thin Launcher.",
3212
- );
3213
- }
3214
- case "list_local_jobs": {
3215
- if (ctx.policy.profile !== "controller")
3216
- return errorResult(
3217
- "TOOL_DISABLED",
3218
- "list_local_jobs requires the controller profile",
3219
- );
3220
- const limit = typeof args.limit === "number" ? args.limit : 50;
3221
- const jobs = listLocalBridgeJobs(ctx.repoRoot, limit);
3222
- audit(ctx, name, "ok", args);
3223
- return textResult({ jobs });
3224
- }
3225
- case "get_local_job": {
3226
- if (ctx.policy.profile !== "controller")
3227
- return errorResult(
3228
- "TOOL_DISABLED",
3229
- "get_local_job requires the controller profile",
3230
- );
3231
- const jobId = String(args.job_id ?? "").trim();
3232
- const includeEvents = args.include_events === true;
3233
- const includeOutput = args.include_output === true;
3234
- const snapshot = getLocalBridgeJobSnapshot(ctx.repoRoot, jobId);
3235
- const payload = {
3236
- job: snapshot.job,
3237
- ...(snapshot.status !== "ok" ? { lookup: snapshot } : {}),
3238
- ...(includeEvents && snapshot.status === "ok" ? { events: getLocalBridgeJobEvents(ctx.repoRoot, jobId) } : {}),
3239
- ...(includeOutput
3240
- ? {
3241
- output: readLocalBridgeJobOutput(ctx.repoRoot, jobId, {
3242
- stream: args.stream === "stderr" ? "stderr" : "stdout",
3243
- maxBytes: typeof args.max_bytes === "number" ? args.max_bytes : undefined,
3244
- }),
3245
- }
3246
- : {}),
3247
- };
3248
- audit(ctx, name, "ok", args);
3249
- return textResult(payload);
3250
- }
3251
- case "get_local_job_output": {
3252
- if (ctx.policy.profile !== "controller")
3253
- return errorResult(
3254
- "TOOL_DISABLED",
3255
- "get_local_job_output requires the controller profile",
3256
- );
3257
- const jobId = String(args.job_id ?? "").trim();
3258
- const payload = readLocalBridgeJobOutput(ctx.repoRoot, jobId, {
3259
- stream: args.stream === "stderr" ? "stderr" : "stdout",
3260
- maxBytes: typeof args.max_bytes === "number" ? args.max_bytes : undefined,
3261
- });
3262
- audit(ctx, name, "ok", args);
3263
- return textResult(payload);
3264
- }
3265
- case "project_snapshot": {
3266
- if (ctx.policy.profile !== "controller") return errorResult("TOOL_DISABLED", "project_snapshot requires the controller profile");
3267
- const markerPaths = [
3268
- ".ai/harness/active-plan",
3269
- ".ai/harness/active-sprint",
3270
- ".ai/harness/active-worktree",
3271
- "tasks/current.md",
3272
- ];
3273
- const markers = markerPaths.map((path) => {
3274
- const absolute = join(ctx.repoRoot, path);
3275
- if (!existsSync(absolute)) return { path, exists: false };
3276
- const raw = readFileSync(absolute, "utf-8");
3277
- return {
3278
- path,
3279
- exists: true,
3280
- bytes: Buffer.byteLength(raw),
3281
- lines: raw.split(/\r?\n/).length,
3282
- sha256: createHash("sha256").update(raw).digest("hex").slice(0, 16),
3283
- preview: redactMcpText(raw).text.slice(0, 400),
3284
- };
3285
- });
3286
- const controllerHome = resolveRepoPreferredControllerHome(ctx.repoRoot);
3287
- const requirementBoard = buildRequirementBoard({ controllerHome, repoId: ctx.repoId });
3288
- const executionDiagnostics = buildExecutionDiagnostics({
3289
- controllerHome,
3290
- repoId: ctx.repoId,
3291
- detailLevel: "detail",
3292
- });
3293
- const snapshotRuns = listAgentJobs(ctx.repoRoot, 11);
3294
- const runs = snapshotRuns.slice(0, 10).map((run) => ({
3295
- runId: run.runId,
3296
- issueId: run.issueId,
3297
- taskId: run.taskId,
3298
- agent: run.agent,
3299
- provider: run.provider,
3300
- executionMode: run.executionMode,
3301
- status: run.status,
3302
- progress: run.progress,
3303
- createdAt: run.createdAt,
3304
- finishedAt: run.finishedAt,
3305
- error: run.error?.slice(0, 300),
3306
- }));
3307
- const payload = {
3308
- git: gitSnapshot(ctx.repoRoot),
3309
- requirementBoard,
3310
- executionDiagnostics,
3311
- legacyCompatibility: {
3312
- currentIssue: "deprecated_frozen",
3313
- issueTaskBoard: "deprecated_frozen",
3314
- authority: "controller-home-sqlite",
3315
- },
3316
- runs,
3317
- markers,
3318
- truncated: {
3319
- runs: snapshotRuns.length > 10,
3320
- },
3321
- };
3322
- audit(ctx, name, "ok", args);
3323
- return textResult(payload);
3324
- }
3325
- case "search_repository": {
3326
- if (ctx.policy.profile !== "controller")
3327
- return errorResult(
3328
- "TOOL_DISABLED",
3329
- "search_repository requires the controller profile",
3330
- );
3331
- const session = ctx.sessionId && ctx.repoId
3332
- ? {
3333
- sessionId: ctx.sessionId,
3334
- repoId: ctx.repoId,
3335
- checkoutId: ctx.checkoutId ?? ctx.repoId,
3336
- }
3337
- : undefined;
3338
- const result = searchRepository(ctx.repoRoot, ctx.policy, {
3339
- query: String(args.query ?? ""),
3340
- includeGlobs: stringList(args.include_globs),
3341
- excludeGlobs: stringList(args.exclude_globs),
3342
- maxResults:
3343
- typeof args.max_results === "number" ? args.max_results : undefined,
3344
- maxFiles:
3345
- typeof args.max_files === "number" ? args.max_files : undefined,
3346
- caseSensitive: args.case_sensitive === true,
3347
- session,
3348
- });
3349
- audit(ctx, name, "ok", args);
3350
- return textResult(result);
3351
- }
3352
- case "get_git_diff": {
3353
- if (ctx.policy.profile !== "controller")
3354
- return errorResult(
3355
- "TOOL_DISABLED",
3356
- "get_git_diff requires the controller profile",
3357
- );
3358
- const path =
3359
- typeof args.path === "string" && args.path.trim()
3360
- ? args.path.trim()
3361
- : undefined;
3362
- if (path) {
3363
- const decision = resolveMcpPath(
3364
- ctx.repoRoot,
3365
- path,
3366
- ctx.policy,
3367
- "read",
3368
- );
3369
- if (!decision.ok)
3370
- return errorResult(
3371
- "POLICY_DENIED",
3372
- decision.reason ?? "path denied",
3373
- { path },
3374
- );
3375
- }
3376
- const maxBytes =
3377
- typeof args.max_bytes === "number"
3378
- ? Math.min(Math.max(Math.trunc(args.max_bytes), 1024), 512 * 1024)
3379
- : 128 * 1024;
3380
- const result = gitDiff(ctx.repoRoot, path, maxBytes);
3381
- const redacted = redactMcpText(result.diff);
3382
- audit(ctx, name, "ok", args, path);
3383
- return textResult({
3384
- ...result,
3385
- diff: redacted.text,
3386
- redactions: redacted.redactions,
3387
- });
3388
- }
3389
- case "list_checks": {
3390
- if (ctx.policy.profile !== "controller")
3391
- return errorResult(
3392
- "TOOL_DISABLED",
3393
- "list_checks requires the controller profile",
3394
- );
3395
- const checks = listControllerChecks(ctx.repoRoot);
3396
- audit(ctx, name, "ok", args);
3397
- return textResult({ checks });
3398
- }
3399
- case "list_issues": {
3400
- if (ctx.policy.profile !== "controller")
3401
- return errorResult(
3402
- "TOOL_DISABLED",
3403
- "list_issues requires the controller profile",
3404
- );
3405
- const projectState = loadControllerProjectState(ctx.repoRoot);
3406
- const issues = listIssueEffectiveViews(ctx.repoRoot).map((issue) => {
3407
- const readiness = inspectIssueReadiness(ctx.repoRoot, issue.id);
3408
- return {
3409
- id: issue.id,
3410
- title: issue.title,
3411
- kind: issue.kind,
3412
- status: issue.status,
3413
- lifecycleStatus: issue.lifecycleStatus,
3414
- archivedAt: issue.archivedAt,
3415
- isCurrent: projectState.currentIssueId === issue.id,
3416
- updatedAt: issue.updatedAt,
3417
- taskCount: issue.tasks.length,
3418
- taskCounts: issue.tasks.reduce<Record<string, number>>((counts, task) => {
3419
- counts[task.effectiveStatus] = (counts[task.effectiveStatus] ?? 0) + 1;
3420
- return counts;
3421
- }, {}),
3422
- declaredTaskCounts: issue.tasks.reduce<Record<string, number>>((counts, task) => {
3423
- counts[task.declaredStatus] = (counts[task.declaredStatus] ?? 0) + 1;
3424
- return counts;
3425
- }, {}),
3426
- readyTaskIds: readiness.readyTaskIds,
3427
- queueableTaskIds: readiness.queueableTaskIds,
3428
- approvalPendingTaskIds: readiness.approvalPendingTaskIds,
3429
- };
3430
- });
3431
- audit(ctx, name, "ok", args);
3432
- return textResult({ issues });
3433
- }
3434
- case "get_project_board": {
3435
- if (ctx.policy.profile !== "controller")
3436
- return errorResult(
3437
- "TOOL_DISABLED",
3438
- "get_project_board requires the controller profile",
3439
- );
3440
- const startedAt = performance.now();
3441
- const controllerHome = resolveRepoPreferredControllerHome(ctx.repoRoot);
3442
- const detailLevel = args.detail_level === "full" ? "full" : args.detail_level === "detail" ? "detail" : "summary";
3443
- const payload = detailLevel === "summary"
3444
- ? buildRequirementBoard({ controllerHome, repoId: ctx.repoId })
3445
- : buildExecutionDiagnostics({
3446
- controllerHome,
3447
- repoId: ctx.repoId,
3448
- detailLevel,
3449
- requirementId: typeof args.requirement_id === "string" ? args.requirement_id : undefined,
3450
- });
3451
- audit(ctx, name, "ok", args);
3452
- const response = {
3453
- ...payload,
3454
- responseMeta: {
3455
- serverDurationMs: Number((performance.now() - startedAt).toFixed(2)),
3456
- structuredPayloadBytes: 0,
3457
- },
3458
- };
3459
- response.responseMeta.structuredPayloadBytes = Buffer.byteLength(JSON.stringify(response), "utf8");
3460
- return textResult(response);
3461
- }
3462
- case "get_project_governance": {
3463
- if (ctx.policy.profile !== "controller")
3464
- return errorResult("TOOL_DISABLED", "get_project_governance requires the controller profile");
3465
- const governance = inspectProjectGovernance(ctx.repoRoot);
3466
- audit(ctx, name, "ok", args);
3467
- return textResult(governance);
3468
- }
3469
- case "reconcile_project_governance": {
3470
- if (ctx.policy.profile !== "controller")
3471
- return errorResult("TOOL_DISABLED", "reconcile_project_governance requires the controller profile");
3472
- const result = reconcileProjectGovernance(ctx.repoRoot);
3473
- audit(ctx, name, "ok", args, ".ai/harness/controller/project-state.json");
3474
- return textResult(result);
3475
- }
3476
- case "get_project_state": {
3477
- if (ctx.policy.profile !== "controller")
3478
- return errorResult("TOOL_DISABLED", "get_project_state requires the controller profile");
3479
- const state = loadControllerProjectState(ctx.repoRoot);
3480
- audit(ctx, name, "ok", args);
3481
- return textResult(state);
3482
- }
3483
- case "set_current_issue": {
3484
- if (ctx.policy.profile !== "controller")
3485
- return errorResult("TOOL_DISABLED", "set_current_issue requires the controller profile");
3486
- const issue = getIssue(ctx.repoRoot, String(args.issue_id ?? ""));
3487
- if (issue.archivedAt || ["done", "cancelled"].includes(issue.status))
3488
- return errorResult("ISSUE_NOT_ACTIVE", `only an active, non-archived Issue can be selected (current: ${issue.status})`);
3489
- const state = saveControllerProjectState(ctx.repoRoot, { currentIssueId: issue.id }, "chatgpt-controller");
3490
- audit(ctx, name, "ok", args, ".ai/harness/controller/project-state.json");
3491
- return textResult(state);
3492
- }
3493
- case "archive_issue": {
3494
- if (ctx.policy.profile !== "controller")
3495
- return errorResult("TOOL_DISABLED", "archive_issue requires the controller profile");
3496
- const issue = archiveIssue(ctx.repoRoot, String(args.issue_id ?? ""));
3497
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3498
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
3499
- }
3500
- case "restore_issue": {
3501
- if (ctx.policy.profile !== "controller")
3502
- return errorResult("TOOL_DISABLED", "restore_issue requires the controller profile");
3503
- const issue = restoreIssue(ctx.repoRoot, String(args.issue_id ?? ""));
3504
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3505
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
3506
- }
3507
- case "get_worklog_timeline": {
3508
- if (ctx.policy.profile !== "controller")
3509
- return errorResult("TOOL_DISABLED", "get_worklog_timeline requires the controller profile");
3510
- const events = listControllerWorklogEvents(ctx.repoRoot, {
3511
- category: parseWorklogCategory(args.category),
3512
- issueId: typeof args.issue_id === "string" ? args.issue_id : undefined,
3513
- taskId: typeof args.task_id === "string" ? args.task_id : undefined,
3514
- runId: typeof args.run_id === "string" ? args.run_id : undefined,
3515
- editSessionId: typeof args.edit_session_id === "string" ? args.edit_session_id : undefined,
3516
- since: typeof args.since === "string" ? args.since : undefined,
3517
- until: typeof args.until === "string" ? args.until : undefined,
3518
- limit: typeof args.limit === "number" ? Math.trunc(args.limit) : undefined,
3519
- });
3520
- audit(ctx, name, "ok", args);
3521
- return textResult({ events });
3522
- }
3523
- case "export_worklog": {
3524
- if (ctx.policy.profile !== "controller")
3525
- return errorResult("TOOL_DISABLED", "export_worklog requires the controller profile");
3526
- const result = exportControllerWorklog(ctx.repoRoot, {
3527
- format: args.format === "json" ? "json" : "markdown",
3528
- outputPath: typeof args.output_path === "string" ? args.output_path : undefined,
3529
- filter: {
3530
- issueId: typeof args.issue_id === "string" ? args.issue_id : undefined,
3531
- taskId: typeof args.task_id === "string" ? args.task_id : undefined,
3532
- runId: typeof args.run_id === "string" ? args.run_id : undefined,
3533
- editSessionId: typeof args.edit_session_id === "string" ? args.edit_session_id : undefined,
3534
- },
3535
- });
3536
- audit(ctx, name, "ok", args, result.path);
3537
- return textResult(result);
3538
- }
3539
- case "get_github_plugin_status": {
3540
- if (ctx.policy.profile !== "controller")
3541
- return errorResult("TOOL_DISABLED", "get_github_plugin_status requires the controller profile");
3542
- const status = getGitHubPluginStatus(ctx.repoRoot);
3543
- audit(ctx, name, "ok", args, status.repository);
3544
- return textResult(status);
3545
- }
3546
- case "configure_github_plugin": {
3547
- if (ctx.policy.profile !== "controller")
3548
- return errorResult("TOOL_DISABLED", "configure_github_plugin requires the controller profile");
3549
- const config = saveGitHubPluginConfig(ctx.repoRoot, {
3550
- enabled: typeof args.enabled === "boolean" ? args.enabled : undefined,
3551
- repository: args.clear_repository === true ? "" : typeof args.repository === "string" ? args.repository : undefined,
3552
- syncMode: args.sync_mode === "checkpoint" ? "checkpoint" : args.sync_mode === "manual" ? "manual" : undefined,
3553
- includeTasks: typeof args.include_tasks === "boolean" ? args.include_tasks : undefined,
3554
- projectOwner: args.clear_project === true ? "" : typeof args.project_owner === "string" ? args.project_owner : undefined,
3555
- projectNumber: args.clear_project === true ? null : typeof args.project_number === "number" ? args.project_number : undefined,
1487
+ const roots = [
1488
+ "docs/spec.md",
1489
+ "plans",
1490
+ "tasks/current.md",
1491
+ ".ai/harness/session",
1492
+ ".ai/harness/controller/packets",
1493
+ ".ai/harness/projections",
1494
+ ".ai/harness/checks",
1495
+ ];
1496
+ audit(ctx, name, "ok", args);
1497
+ return textResult({
1498
+ repoRoot: ctx.repoRoot,
1499
+ adopted: isForgeAdopted(ctx.repoRoot),
1500
+ profile: ctx.policy.profile,
1501
+ branch: currentGitBranch(ctx.repoRoot),
1502
+ workflowRoots: roots.map((path) => ({
1503
+ path,
1504
+ exists: existsSync(join(ctx.repoRoot, path)),
1505
+ })),
3556
1506
  });
3557
- audit(ctx, name, "ok", args, "repository-registry:github");
3558
- return textResult(config);
3559
- }
3560
- case "get_issue": {
3561
- if (ctx.policy.profile !== "controller")
3562
- return errorResult(
3563
- "TOOL_DISABLED",
3564
- "get_issue requires the controller profile",
3565
- );
3566
- const issue = getIssueReadView(
3567
- ctx.repoRoot,
3568
- String(args.issue_id ?? ""),
3569
- args.detail_level === "full" ? "full" : "summary",
3570
- );
3571
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3572
- return textResult(issue);
3573
- }
3574
- case "github_status": {
3575
- if (ctx.policy.profile !== "controller")
3576
- return errorResult(
3577
- "TOOL_DISABLED",
3578
- "github_status requires the controller profile",
3579
- );
3580
- const status = getGitHubStatus(
3581
- ctx.repoRoot,
3582
- typeof args.repo === "string" ? args.repo : undefined,
3583
- );
3584
- audit(
3585
- ctx,
3586
- name,
3587
- status.available && status.authenticated ? "ok" : "failed",
3588
- args,
3589
- status.repository?.nameWithOwner,
3590
- status.errors.join("; "),
3591
- );
3592
- return textResult(status);
3593
1507
  }
3594
- case "publish_issue_to_github": {
3595
- if (ctx.policy.profile !== "controller")
3596
- return errorResult(
3597
- "TOOL_DISABLED",
3598
- "publish_issue_to_github requires the controller profile",
3599
- );
3600
- const issue = publishIssueToGitHub(
3601
- ctx.repoRoot,
3602
- String(args.issue_id ?? ""),
3603
- {
3604
- repo: typeof args.repo === "string" ? args.repo : undefined,
3605
- labels: stringList(args.labels),
3606
- includeTasks: args.include_tasks === true,
3607
- projectOwner:
3608
- typeof args.project_owner === "string"
3609
- ? args.project_owner
3610
- : undefined,
3611
- projectNumber:
3612
- typeof args.project_number === "number"
3613
- ? Math.trunc(args.project_number)
3614
- : undefined,
3615
- },
1508
+ case "harness_doctor": {
1509
+ const localConfig = existsSync(
1510
+ join(ctx.repoRoot, ".forge", "mcp.local.json"),
3616
1511
  );
3617
- audit(ctx, name, "ok", args, issue.github?.url);
3618
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
3619
- }
3620
- case "refresh_github_issue": {
3621
- if (ctx.policy.profile !== "controller")
3622
- return errorResult(
3623
- "TOOL_DISABLED",
3624
- "refresh_github_issue requires the controller profile",
3625
- );
3626
- const result = refreshGitHubIssue(
3627
- ctx.repoRoot,
3628
- String(args.issue_id ?? ""),
1512
+ const codexConfig = existsSync(
1513
+ join(ctx.repoRoot, ".codex", "config.toml"),
3629
1514
  );
3630
- audit(ctx, name, "ok", args, result.issue.github?.url);
1515
+ audit(ctx, name, "ok", args);
3631
1516
  return textResult({
3632
- ...result,
3633
- issue: projectIssueEffectiveView(ctx.repoRoot, result.issue),
1517
+ status: isForgeAdopted(ctx.repoRoot)
1518
+ ? "ready_local"
1519
+ : "not_adopted",
1520
+ repo: ctx.repoRoot,
1521
+ profile: ctx.policy.profile,
1522
+ mcp: {
1523
+ localConfig,
1524
+ policy: "builtin",
1525
+ profile: ctx.policy.profile,
1526
+ toolSurface:
1527
+ ctx.policy.profile === "controller"
1528
+ ? FORGE_TOOL_SURFACE
1529
+ : `${ctx.policy.profile}-legacy-v1`,
1530
+ toolCount: buildMcpToolDefinitions(ctx.policy, {
1531
+ enableChatgptBrowser: ctx.enableChatgptBrowser === true,
1532
+ }).length,
1533
+ repositoryRead:
1534
+ ctx.policy.profile === "controller"
1535
+ ? "ordinary repository source and document files are readable; immutable secret and runtime paths remain denied"
1536
+ : "workflow-scoped",
1537
+ deniedPaths: ctx.policy.denyGlobs.length,
1538
+ runner: {
1539
+ enabled: ctx.policy.execution.agentRunner,
1540
+ defaultTimeoutMs: ctx.policy.execution.runnerTimeoutMs,
1541
+ maxTimeoutMs: ctx.policy.execution.runnerMaxTimeoutMs,
1542
+ },
1543
+ },
1544
+ codex: {
1545
+ configured: codexConfig,
1546
+ fix: codexConfig
1547
+ ? null
1548
+ : "forge mcp setup codex --repo . --scope project",
1549
+ },
1550
+ chatgpt: {
1551
+ localEndpoint: "http://127.0.0.1:8765/mcp",
1552
+ manualStepsRequired: true,
1553
+ guide: "docs/forge-chatgpt-mcp-setup.md",
1554
+ },
3634
1555
  });
3635
1556
  }
3636
- case "close_github_issue": {
3637
- if (ctx.policy.profile !== "controller")
3638
- return errorResult(
3639
- "TOOL_DISABLED",
3640
- "close_github_issue requires the controller profile",
3641
- );
3642
- const issue = closeGitHubIssue(
3643
- ctx.repoRoot,
3644
- String(args.issue_id ?? ""),
3645
- );
3646
- audit(ctx, name, "ok", args, issue.github?.url);
3647
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
3648
- }
3649
- case "inspect_issue_readiness": {
1557
+ case "controller_capabilities": {
3650
1558
  if (ctx.policy.profile !== "controller")
3651
1559
  return errorResult(
3652
1560
  "TOOL_DISABLED",
3653
- "inspect_issue_readiness requires the controller profile",
1561
+ "controller_capabilities requires the controller profile",
3654
1562
  );
3655
- const readiness = inspectIssueReadiness(
3656
- ctx.repoRoot,
3657
- String(args.issue_id ?? ""),
3658
- );
3659
- audit(
3660
- ctx,
3661
- name,
3662
- readiness.ready ? "ok" : "blocked",
3663
- args,
3664
- `tasks/issues/${readiness.issueId}`,
3665
- [...readiness.blockers, ...readiness.taskBlockers].map((item) => item.message).join("; "),
1563
+ const expectedTools = ctx.publishedToolNames;
1564
+ if (!expectedTools) return errorResult(
1565
+ 'MCP_TOOL_SURFACE_UNAVAILABLE',
1566
+ 'Controller capability diagnostics require the authoritative MCP registry projection.',
3666
1567
  );
3667
- return textResult(readiness);
3668
- }
3669
- case "inspect_task_readiness": {
3670
- if (ctx.policy.profile !== "controller")
3671
- return errorResult(
3672
- "TOOL_DISABLED",
3673
- "inspect_task_readiness requires the controller profile",
3674
- );
3675
- const readiness = inspectTaskReadiness(
3676
- ctx.repoRoot,
3677
- String(args.issue_id ?? ""),
3678
- String(args.task_id ?? ""),
3679
- {
3680
- retryFromRunId: typeof args.retry_from_run_id === "string" ? args.retry_from_run_id : undefined,
3681
- approveDestructive: args.approve_destructive === true,
1568
+ const preferredTools: string[] = [...PREFERRED_FACADE_TOOL_NAMES];
1569
+ const capabilityDescriptors = listCapabilityDescriptors();
1570
+ const connectorSnapshot = Array.isArray(args.connector_tool_names)
1571
+ ? args.connector_tool_names.map(String)
1572
+ : Array.isArray(args.connectorToolNames)
1573
+ ? args.connectorToolNames.map(String)
1574
+ : undefined;
1575
+ const connectorFreshness = await buildLocalConnectorStatusForRepo({
1576
+ repoRoot: ctx.repoRoot,
1577
+ expectedTools,
1578
+ connectorToolNames: connectorSnapshot,
1579
+ });
1580
+ // staleConnector only when ChatGPT snapshot is supplied and missing rh_*.
1581
+ const missingFacadeTools = connectorFreshness.missingConnectorTools;
1582
+ const staleConnector = connectorFreshness.status === "chatgpt_snapshot_missing_facade";
1583
+ const payload = {
1584
+ schemaVersion: FORGE_MCP_SCHEMA_VERSION,
1585
+ toolSurface: FORGE_TOOL_SURFACE,
1586
+ version: FORGE_VERSION,
1587
+ toolSurfaceFingerprint: forgeToolSurfaceFingerprint(expectedTools),
1588
+ executionModel: "chatgpt-controller-execution-bridge",
1589
+ profile: ctx.policy.profile,
1590
+ capabilities: {
1591
+ repositoryInspection: true,
1592
+ issueLauncher: true,
1593
+ githubIssuesAndProjects: true,
1594
+ localAgentRuns: ctx.policy.execution.agentRunner,
1595
+ githubCopilotSessions: true,
1596
+ dynamicTaskGraph: true,
1597
+ runtimeAgentSelection: true,
1598
+ taskAgentBinding: false,
1599
+ structuredRunEvents: true,
1600
+ boundedDirectEdits: true,
1601
+ multiRevisionDirectEdits: true,
1602
+ editSavepoints: true,
1603
+ partialEditRollback: true,
1604
+ directEditFirstRouting: true,
1605
+ persistedEditDiffs: true,
1606
+ verifiedEditFinalization: true,
1607
+ verificationGate: true,
1608
+ localRunWatch: true,
1609
+ localVisualController: true,
1610
+ hierarchicalControllerUI: true,
1611
+ localApprovalQueue: false,
1612
+ localRiskApprovalGate: false,
1613
+ quickAgentSessions: true,
1614
+ persistentTimeouts: true,
1615
+ timeoutProgress: true,
1616
+ connectorSchemaDiagnostics: true,
1617
+ projectProgressAggregation: true,
1618
+ effectiveTaskStatus: true,
1619
+ unifiedWorklogTimeline: true,
1620
+ exportableWorklogReports: true,
1621
+ githubPluginConfiguration: true,
1622
+ serverSentEventDashboard: true,
1623
+ singleExecutionFocus: false,
1624
+ taskLocalReadiness: true,
1625
+ riskAdaptiveVerification: true,
1626
+ ephemeralQuickAgentSessions: true,
1627
+ automaticRunContinuation: true,
1628
+ reportedCommandEvidence: true,
1629
+ governanceDiagnostics: true,
1630
+ safeGovernanceReconciliation: true,
1631
+ evidenceGateProgress: true,
1632
+ directTaskActions: true,
1633
+ issueArchiving: true,
1634
+ retryableRunFailures: true,
1635
+ issueCreationGuardrails: true,
1636
+ persistedCheckEvidence: true,
1637
+ persistedCheckReuse: true,
1638
+ directVerification: true,
1639
+ controllerContextAggregation: true,
1640
+ taskLedgerProjection: true,
1641
+ assistantPluginRuntime: true,
1642
+ chatgptFacadeTools: true,
1643
+ goalWorkloop: true,
1644
+ handoffInbox: true,
1645
+ codexGrokDelegation: true,
1646
+ selfHealingRepair: true,
3682
1647
  },
3683
- );
3684
- audit(
3685
- ctx,
3686
- name,
3687
- readiness.ready ? "ok" : "blocked",
3688
- args,
3689
- `tasks/issues/${readiness.issueId}`,
3690
- readiness.blockers.map((item) => item.message).join("; "),
3691
- );
3692
- return textResult(readiness);
3693
- }
3694
- case "prepare_issue_launch": {
3695
- if (ctx.policy.profile !== "controller")
3696
- return errorResult(
3697
- "TOOL_DISABLED",
3698
- "prepare_issue_launch requires the controller profile",
3699
- );
3700
- const issueId = String(args.issue_id ?? "");
3701
- const issue = getIssue(ctx.repoRoot, issueId);
3702
- const readiness = inspectIssueReadiness(ctx.repoRoot, issueId);
3703
- const maxParallel = Math.min(
3704
- Math.max(
3705
- typeof args.max_parallel === "number"
3706
- ? Math.trunc(args.max_parallel)
3707
- : readiness.suggestedMaxParallel,
3708
- 1,
3709
- ),
3710
- 4,
3711
- );
3712
- const tasks = issue.tasks
3713
- .filter((task) => readiness.queueableTaskIds.includes(task.id))
3714
- .map((task) => ({
3715
- id: task.id,
3716
- title: task.title,
3717
- agent: task.recommendedAgent ?? "runtime-selected",
3718
- risk: task.risk,
3719
- allowedPaths: task.allowedPaths,
3720
- checks: task.checks,
3721
- acceptanceCriteria: task.acceptanceCriteria,
3722
- github: task.github,
3723
- }));
3724
- const preview = {
3725
- issue: {
3726
- id: issue.id,
3727
- title: issue.title,
3728
- status: issue.status,
3729
- github: issue.github,
1648
+ runner: {
1649
+ enabled: ctx.policy.execution.agentRunner,
1650
+ allowedAgents: ctx.policy.execution.allowedAgents,
1651
+ defaultTimeoutMs: ctx.policy.execution.runnerTimeoutMs,
1652
+ defaultTimeout: formatDurationMs(
1653
+ ctx.policy.execution.runnerTimeoutMs,
1654
+ ),
1655
+ maxTimeoutMs: ctx.policy.execution.runnerMaxTimeoutMs,
1656
+ maxTimeout: formatDurationMs(
1657
+ ctx.policy.execution.runnerMaxTimeoutMs,
1658
+ ),
1659
+ note: "Requested timeout values are validated explicitly and are never silently reduced to the default.",
1660
+ },
1661
+ preferredTools,
1662
+ capabilityGroups: summarizeCapabilityGroups(),
1663
+ toolArchitecture: {
1664
+ facadeTools: preferredTools,
1665
+ atomicTypedToolsRetained: true,
1666
+ internalHandlersRetained: true,
1667
+ domainSchemaLoading: 'static_stable_surface',
1668
+ dynamicDomainSchemaLoadingSupported: false,
1669
+ capabilityDescriptorCount: capabilityDescriptors.length,
1670
+ },
1671
+ toolExposure: {
1672
+ preferred: preferredTools,
1673
+ note: "Prefer the five rh_* workflow facades for orchestration. Typed repository, Git, Issue/Task, plugin, browser, iOS, artifact, and recovery tools remain available because the current MCP transport cannot load domain schemas per conversation.",
1674
+ exposedToolCount: expectedTools.length,
1675
+ },
1676
+ expectedTools,
1677
+ facadeToolsPresentInExpectedTools: preferredTools.every((tool: string) => expectedTools.includes(tool)),
1678
+ staleConnector,
1679
+ missingFacadeTools,
1680
+ connectorFreshness: {
1681
+ status: connectorFreshness.status,
1682
+ severity: connectorFreshness.severity,
1683
+ summary: connectorFreshness.summary,
1684
+ missingLocalTools: connectorFreshness.missingLocalTools,
1685
+ missingConnectorTools: connectorFreshness.missingConnectorTools,
1686
+ expectedFacadeTools: connectorFreshness.expectedFacadeTools,
1687
+ restartRecommended: connectorFreshness.restartRecommended,
1688
+ reconnectRecommended: connectorFreshness.reconnectRecommended,
1689
+ suggestedActions: connectorFreshness.suggestedActions,
3730
1690
  },
3731
- readiness,
3732
- maxParallel,
3733
- tasks,
1691
+ docs: [
1692
+ "docs/architecture/current/README.md",
1693
+ "docs/architecture/history.md",
1694
+ "docs/operations/controller-reliability-runbook.md",
1695
+ "docs/forge-github-issue-launcher.md",
1696
+ "docs/forge-chatgpt-mcp-setup.md",
1697
+ ],
1698
+ staleConnectorHint: staleConnector
1699
+ ? `Stale ChatGPT connector tool snapshot is missing facade tools: ${missingFacadeTools.join(", ")}. Refresh/reconnect MCP so tools/list reloads rh_status, rh_inbox, rh_context, and rh_work.`
1700
+ : connectorFreshness.status === "unable_to_verify_chatgpt_snapshot"
1701
+ ? `Local MCP tool surface includes preferred facade tools. ChatGPT connector snapshot was not supplied, so freshness cannot be verified from this call. If ChatGPT tools/list is missing rh_status/rh_inbox/rh_context/rh_work, reconnect MCP. Pass connector_tool_names for an exact check.`
1702
+ : connectorFreshness.status === "stale_fingerprint"
1703
+ ? `Local MCP process fingerprint/version may be stale. Restart controller/MCP, then reconnect ChatGPT if rh_* are still missing.`
1704
+ : connectorFreshness.status === "local_mcp_missing_facade"
1705
+ ? `Local MCP tool surface is missing facade tools: ${connectorFreshness.missingLocalTools.join(", ")}. Restart controller/MCP.`
1706
+ : `Connector clients must match ${FORGE_TOOL_SURFACE} version ${FORGE_VERSION} / schema ${FORGE_MCP_SCHEMA_VERSION}. Prefer rh_status/rh_inbox/rh_context/rh_work. Pass connector_tool_names to detect a stale ChatGPT tool snapshot precisely.`,
3734
1707
  };
3735
- audit(
3736
- ctx,
3737
- name,
3738
- readiness.queueable ? "ok" : "blocked",
3739
- args,
3740
- `tasks/issues/${issue.id}`,
3741
- );
3742
- return textResult(preview);
3743
- }
3744
- case "create_issue": {
3745
- if (ctx.policy.profile !== "controller")
3746
- return errorResult(
3747
- "TOOL_DISABLED",
3748
- "create_issue requires the controller profile",
3749
- );
3750
- const issue = createIssue(ctx.repoRoot, {
3751
- title: String(args.title ?? ""),
3752
- kind: (args.kind ? String(args.kind) : "feature") as IssueKind,
3753
- summary: typeof args.summary === "string" ? args.summary : undefined,
3754
- goals: stringList(args.goals),
3755
- nonGoals: stringList(args.non_goals),
3756
- acceptanceCriteria: stringList(args.acceptance_criteria),
3757
- relatedArtifacts: stringList(args.related_artifacts),
3758
- tasks: controllerTaskDrafts(args.tasks),
3759
- allowWhileFocused: args.allow_while_focused === true,
3760
- allowDuplicate: args.allow_duplicate === true,
3761
- allowWhenPaused: args.allow_when_paused === true,
3762
- });
3763
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3764
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
1708
+ audit(ctx, name, "ok", args);
1709
+ return textResult(payload);
3765
1710
  }
3766
- case "update_issue": {
1711
+ case "local_bridge_status": {
3767
1712
  if (ctx.policy.profile !== "controller")
3768
1713
  return errorResult(
3769
1714
  "TOOL_DISABLED",
3770
- "update_issue requires the controller profile",
1715
+ "local_bridge_status requires the controller profile",
3771
1716
  );
3772
- const issueId = String(args.issue_id ?? "");
3773
- const patch = {
3774
- title: typeof args.title === "string" ? args.title : undefined,
3775
- status:
3776
- typeof args.status === "string"
3777
- ? (args.status as IssueStatus)
3778
- : undefined,
3779
- summary: typeof args.summary === "string" ? args.summary : undefined,
3780
- goals: Array.isArray(args.goals) ? stringList(args.goals) : undefined,
3781
- nonGoals: Array.isArray(args.non_goals)
3782
- ? stringList(args.non_goals)
3783
- : undefined,
3784
- acceptanceCriteria: Array.isArray(args.acceptance_criteria)
3785
- ? stringList(args.acceptance_criteria)
3786
- : undefined,
3787
- relatedArtifacts: Array.isArray(args.related_artifacts)
3788
- ? stringList(args.related_artifacts)
3789
- : undefined,
3790
- };
3791
- const migrated = applyMigratedIssueDecision(ctx.repoRoot, {
3792
- issueId,
3793
- ...patch,
1717
+ const detailLevel = args.detail_level === "detail" || args.detail === true
1718
+ ? "detail"
1719
+ : "summary";
1720
+ const controllerHome = resolveRepoPreferredControllerHome(ctx.repoRoot);
1721
+ const surface = resolveLocalBridgeSurface({
1722
+ controllerHome,
1723
+ repoRoot: ctx.repoRoot,
1724
+ allowProcessScan: detailLevel === "detail",
3794
1725
  });
3795
- if (migrated) {
3796
- audit(ctx, name, "ok", args, `requirements/${migrated.requirement.requirementId}`);
3797
- return textResult(migrated);
1726
+ const jobs = listLocalBridgeJobs(ctx.repoRoot, detailLevel === "detail" ? 12 : 20);
1727
+ const { activeJobCount, recentJobSummary } = summarizeRecentJobs(jobs);
1728
+ // Prefer authoritative surface; never invent a legacy 8766 endpoint.
1729
+ const endpoint = surface.endpoint ?? null;
1730
+ const running = surface.enabled
1731
+ && (surface.processRunning === true
1732
+ || (surface.endpointConfigured && surface.mode === "embedded"));
1733
+ if (detailLevel === "summary") {
1734
+ const payload = {
1735
+ detailLevel: "summary",
1736
+ repoId: ctx.repoId,
1737
+ running,
1738
+ ready: surface.enabled ? (surface.processRunning !== false) : true,
1739
+ health: surface.enabled
1740
+ ? (surface.processRunning === true || surface.mode === "embedded" ? "ready" : "warning")
1741
+ : "disabled",
1742
+ mode: surface.mode,
1743
+ endpoint,
1744
+ endpointConfigured: surface.endpointConfigured,
1745
+ endpointReachable: null as boolean | null,
1746
+ processRunning: surface.processRunning ?? null,
1747
+ expectedSurface: surface.expectedSurface,
1748
+ requiredForReadiness: surface.requiredForReadiness,
1749
+ warnings: surface.error && surface.requiredForReadiness
1750
+ ? [{ code: "LOCAL_BRIDGE_ERROR", message: surface.error.slice(0, 300) }]
1751
+ : [],
1752
+ activeJobCount,
1753
+ recentJobSummary,
1754
+ statusSource: surface.source,
1755
+ nonBlocking: !surface.requiredForReadiness,
1756
+ approvalQueue: false,
1757
+ };
1758
+ audit(ctx, name, "ok", args);
1759
+ return textResult(payload);
3798
1760
  }
3799
- const issue = updateIssue(ctx.repoRoot, issueId, patch);
3800
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3801
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
1761
+ const reconciliation = reconcileLocalBridgeJobs(ctx.repoRoot);
1762
+ const payload = {
1763
+ detailLevel: "detail",
1764
+ endpoint,
1765
+ endpointConfigured: surface.endpointConfigured,
1766
+ running,
1767
+ error: surface.error,
1768
+ mode: surface.mode,
1769
+ requiredForReadiness: surface.requiredForReadiness,
1770
+ statusSource: surface.source,
1771
+ counts: recentJobSummary,
1772
+ activeJobCount,
1773
+ recentJobSummary,
1774
+ approvalQueue: false,
1775
+ reconciliation,
1776
+ recentJobs: jobs.map((job) => ({
1777
+ jobId: job.jobId,
1778
+ action: job.action,
1779
+ status: job.status,
1780
+ checkId: job.action === "run-check" ? (job.payload as { checkId?: string }).checkId : undefined,
1781
+ runId: job.runId,
1782
+ issueId: job.issueId,
1783
+ taskId: job.taskId,
1784
+ createdAt: job.createdAt,
1785
+ updatedAt: job.updatedAt,
1786
+ finishedAt: job.finishedAt,
1787
+ revision: job.revision,
1788
+ deadlineAt: job.deadlineAt,
1789
+ error: job.error?.slice(0, 300),
1790
+ })),
1791
+ nonBlocking: !surface.requiredForReadiness,
1792
+ fallback:
1793
+ "Open the localhost Local Controller to launch work or inspect execution when a ChatGPT write action is unavailable.",
1794
+ };
1795
+ audit(ctx, name, "ok", args);
1796
+ return textResult(payload);
3802
1797
  }
3803
- case "plan_issue": {
1798
+ case "controller_context": {
3804
1799
  if (ctx.policy.profile !== "controller")
3805
1800
  return errorResult(
3806
1801
  "TOOL_DISABLED",
3807
- "plan_issue requires the controller profile",
1802
+ "controller_context requires the controller profile",
3808
1803
  );
3809
- const issue = planIssue(
3810
- ctx.repoRoot,
3811
- String(args.issue_id ?? ""),
3812
- controllerTaskDrafts(args.tasks),
3813
- );
3814
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3815
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
1804
+ const controllerHome = resolveRepoPreferredControllerHome(ctx.repoRoot);
1805
+ const requirementBoard = buildRequirementBoard({ controllerHome, repoId: ctx.repoId });
1806
+ const executionDiagnostics = buildExecutionDiagnostics({
1807
+ controllerHome,
1808
+ repoId: ctx.repoId,
1809
+ detailLevel: "detail",
1810
+ });
1811
+ const runs = listAgentJobs(ctx.repoRoot, 8);
1812
+ const reconciliation = reconcileLocalBridgeJobs(ctx.repoRoot);
1813
+ const jobs = listLocalBridgeJobs(ctx.repoRoot, 8);
1814
+ const checks = listControllerChecks(ctx.repoRoot).map((check) => ({
1815
+ id: check.id,
1816
+ description: check.description,
1817
+ timeoutMs: check.timeoutMs,
1818
+ source: check.source,
1819
+ }));
1820
+ const payload = {
1821
+ git: gitSnapshot(ctx.repoRoot),
1822
+ requirementBoard,
1823
+ executionDiagnostics,
1824
+ legacyCompatibility: {
1825
+ currentIssue: "deprecated_frozen",
1826
+ issueTaskBoard: "deprecated_frozen",
1827
+ authority: "controller-home-sqlite",
1828
+ },
1829
+ activeRuns: runs.map((run) => ({
1830
+ runId: run.runId,
1831
+ issueId: run.issueId,
1832
+ taskId: run.taskId,
1833
+ status: run.status,
1834
+ agent: run.agent,
1835
+ provider: run.provider,
1836
+ executionMode: run.executionMode,
1837
+ progress: run.progress,
1838
+ lastHeartbeatAt: run.lastHeartbeatAt,
1839
+ error: run.error?.slice(0, 300),
1840
+ })),
1841
+ localBridge: {
1842
+ reconciliation,
1843
+ recentJobs: jobs.map((job) => ({
1844
+ jobId: job.jobId,
1845
+ action: job.action,
1846
+ status: job.status,
1847
+ checkId: job.action === "run-check" ? (job.payload as { checkId?: string }).checkId : undefined,
1848
+ issueId: job.issueId,
1849
+ taskId: job.taskId,
1850
+ updatedAt: job.updatedAt,
1851
+ })),
1852
+ },
1853
+ checks,
1854
+ };
1855
+ audit(ctx, name, "ok", args);
1856
+ return textResult(payload);
3816
1857
  }
3817
- case "append_task": {
3818
- if (ctx.policy.profile !== "controller")
3819
- return errorResult(
3820
- "TOOL_DISABLED",
3821
- "append_task requires the controller profile",
3822
- );
3823
- const drafts = controllerTaskDrafts([args.task]);
3824
- if (drafts.length !== 1)
3825
- return errorResult(
3826
- "INVALID_TASK",
3827
- "append_task requires one valid task",
3828
- );
3829
- const issue = appendTask(
3830
- ctx.repoRoot,
3831
- String(args.issue_id ?? ""),
3832
- drafts[0],
1858
+ case "quick_agent_session":
1859
+ case "submit_local_job": {
1860
+ if (ctx.policy.profile !== "controller") {
1861
+ return errorResult("TOOL_DISABLED", `${name} requires the controller profile`);
1862
+ }
1863
+ audit(ctx, name, "blocked", args);
1864
+ return errorResult(
1865
+ name === "quick_agent_session" ? "AGENT_RUN_DEPRECATED" : "LOCAL_BRIDGE_JOB_RETIRED",
1866
+ "Kernel-managed Agent and Local Bridge Jobs are retired. Create or claim WorkContract and use Process Runtime or the Thin Launcher.",
3833
1867
  );
3834
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3835
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
3836
1868
  }
3837
- case "split_task": {
1869
+ case "list_local_jobs": {
3838
1870
  if (ctx.policy.profile !== "controller")
3839
1871
  return errorResult(
3840
1872
  "TOOL_DISABLED",
3841
- "split_task requires the controller profile",
1873
+ "list_local_jobs requires the controller profile",
3842
1874
  );
3843
- const issue = splitTask(
3844
- ctx.repoRoot,
3845
- String(args.issue_id ?? ""),
3846
- String(args.task_id ?? ""),
3847
- controllerTaskDrafts(args.tasks),
3848
- );
3849
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3850
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
1875
+ const limit = typeof args.limit === "number" ? args.limit : 50;
1876
+ const jobs = listLocalBridgeJobs(ctx.repoRoot, limit);
1877
+ audit(ctx, name, "ok", args);
1878
+ return textResult({ jobs });
3851
1879
  }
3852
- case "supersede_task": {
1880
+ case "get_local_job": {
3853
1881
  if (ctx.policy.profile !== "controller")
3854
1882
  return errorResult(
3855
1883
  "TOOL_DISABLED",
3856
- "supersede_task requires the controller profile",
1884
+ "get_local_job requires the controller profile",
3857
1885
  );
3858
- const issue = supersedeTask(
3859
- ctx.repoRoot,
3860
- String(args.issue_id ?? ""),
3861
- String(args.task_id ?? ""),
3862
- stringList(args.replacement_task_ids),
3863
- );
3864
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3865
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
1886
+ const jobId = String(args.job_id ?? "").trim();
1887
+ const includeEvents = args.include_events === true;
1888
+ const includeOutput = args.include_output === true;
1889
+ const snapshot = getLocalBridgeJobSnapshot(ctx.repoRoot, jobId);
1890
+ const payload = {
1891
+ job: snapshot.job,
1892
+ ...(snapshot.status !== "ok" ? { lookup: snapshot } : {}),
1893
+ ...(includeEvents && snapshot.status === "ok" ? { events: getLocalBridgeJobEvents(ctx.repoRoot, jobId) } : {}),
1894
+ ...(includeOutput
1895
+ ? {
1896
+ output: readLocalBridgeJobOutput(ctx.repoRoot, jobId, {
1897
+ stream: args.stream === "stderr" ? "stderr" : "stdout",
1898
+ maxBytes: typeof args.max_bytes === "number" ? args.max_bytes : undefined,
1899
+ }),
1900
+ }
1901
+ : {}),
1902
+ };
1903
+ audit(ctx, name, "ok", args);
1904
+ return textResult(payload);
3866
1905
  }
3867
- case "set_task_dependencies": {
1906
+ case "get_local_job_output": {
3868
1907
  if (ctx.policy.profile !== "controller")
3869
1908
  return errorResult(
3870
1909
  "TOOL_DISABLED",
3871
- "set_task_dependencies requires the controller profile",
1910
+ "get_local_job_output requires the controller profile",
3872
1911
  );
3873
- const issue = setTaskDependencies(
3874
- ctx.repoRoot,
3875
- String(args.issue_id ?? ""),
3876
- String(args.task_id ?? ""),
3877
- stringList(args.depends_on),
3878
- );
3879
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3880
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
1912
+ const jobId = String(args.job_id ?? "").trim();
1913
+ const payload = readLocalBridgeJobOutput(ctx.repoRoot, jobId, {
1914
+ stream: args.stream === "stderr" ? "stderr" : "stdout",
1915
+ maxBytes: typeof args.max_bytes === "number" ? args.max_bytes : undefined,
1916
+ });
1917
+ audit(ctx, name, "ok", args);
1918
+ return textResult(payload);
3881
1919
  }
3882
- case "update_task": {
3883
- if (ctx.policy.profile !== "controller")
3884
- return errorResult(
3885
- "TOOL_DISABLED",
3886
- "update_task requires the controller profile",
3887
- );
3888
- const startedAt = performance.now();
3889
- const taskId = String(args.task_id ?? "");
3890
- const issue = updateTask(
3891
- ctx.repoRoot,
3892
- String(args.issue_id ?? ""),
3893
- taskId,
3894
- {
3895
- status:
3896
- typeof args.status === "string"
3897
- ? (args.status as TaskStatus)
3898
- : undefined,
3899
- note: typeof args.note === "string" ? args.note : undefined,
3900
- },
3901
- );
3902
- const issueView = projectIssueEffectiveView(ctx.repoRoot, issue);
3903
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
3904
- if (args.detail_level === "detail" || args.detail_level === "full") return textResult(issueView);
1920
+ case "project_snapshot": {
1921
+ if (ctx.policy.profile !== "controller") return errorResult("TOOL_DISABLED", "project_snapshot requires the controller profile");
1922
+ const markerPaths = [
1923
+ ".ai/harness/active-plan",
1924
+ ".ai/harness/active-sprint",
1925
+ ".ai/harness/active-worktree",
1926
+ "tasks/current.md",
1927
+ ];
1928
+ const markers = markerPaths.map((path) => {
1929
+ const absolute = join(ctx.repoRoot, path);
1930
+ if (!existsSync(absolute)) return { path, exists: false };
1931
+ const raw = readFileSync(absolute, "utf-8");
1932
+ return {
1933
+ path,
1934
+ exists: true,
1935
+ bytes: Buffer.byteLength(raw),
1936
+ lines: raw.split(/\r?\n/).length,
1937
+ sha256: createHash("sha256").update(raw).digest("hex").slice(0, 16),
1938
+ preview: redactMcpText(raw).text.slice(0, 400),
1939
+ };
1940
+ });
1941
+ const controllerHome = resolveRepoPreferredControllerHome(ctx.repoRoot);
1942
+ const requirementBoard = buildRequirementBoard({ controllerHome, repoId: ctx.repoId });
1943
+ const executionDiagnostics = buildExecutionDiagnostics({
1944
+ controllerHome,
1945
+ repoId: ctx.repoId,
1946
+ detailLevel: "detail",
1947
+ });
1948
+ const snapshotRuns = listAgentJobs(ctx.repoRoot, 11);
1949
+ const runs = snapshotRuns.slice(0, 10).map((run) => ({
1950
+ runId: run.runId,
1951
+ issueId: run.issueId,
1952
+ taskId: run.taskId,
1953
+ agent: run.agent,
1954
+ provider: run.provider,
1955
+ executionMode: run.executionMode,
1956
+ status: run.status,
1957
+ progress: run.progress,
1958
+ createdAt: run.createdAt,
1959
+ finishedAt: run.finishedAt,
1960
+ error: run.error?.slice(0, 300),
1961
+ }));
3905
1962
  const payload = {
3906
- ...summarizeUpdatedIssue(issueView as unknown as Record<string, unknown>, taskId),
3907
- responseMeta: {
3908
- serverDurationMs: Number((performance.now() - startedAt).toFixed(2)),
3909
- structuredPayloadBytes: 0,
1963
+ git: gitSnapshot(ctx.repoRoot),
1964
+ requirementBoard,
1965
+ executionDiagnostics,
1966
+ legacyCompatibility: {
1967
+ currentIssue: "deprecated_frozen",
1968
+ issueTaskBoard: "deprecated_frozen",
1969
+ authority: "controller-home-sqlite",
1970
+ },
1971
+ runs,
1972
+ markers,
1973
+ truncated: {
1974
+ runs: snapshotRuns.length > 10,
3910
1975
  },
3911
1976
  };
3912
- payload.responseMeta.structuredPayloadBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
1977
+ audit(ctx, name, "ok", args);
3913
1978
  return textResult(payload);
3914
1979
  }
3915
- case "dispatch_task": {
3916
- if (ctx.policy.profile !== "controller") return errorResult("TOOL_DISABLED", "dispatch_task requires the controller profile");
3917
- audit(ctx, name, "blocked", args);
3918
- return errorResult(
3919
- "AGENT_RUN_DEPRECATED",
3920
- "Kernel-managed Agent Runs are retired. Create or claim a WorkContract through rh_work, then start an external SuperController session through the Thin Launcher.",
3921
- );
3922
- }
3923
- case "launch_issue": {
3924
- if (ctx.policy.profile !== "controller") return errorResult("TOOL_DISABLED", "launch_issue requires the controller profile");
3925
- audit(ctx, name, "blocked", args);
3926
- return errorResult(
3927
- "AGENT_RUN_DEPRECATED",
3928
- "Kernel-managed Agent Runs are retired. Convert the Issue tasks to WorkContracts and claim them through rh_work.",
3929
- );
3930
- }
3931
- case "dispatch_ready_tasks": {
3932
- if (ctx.policy.profile !== "controller") return errorResult("TOOL_DISABLED", "dispatch_ready_tasks requires the controller profile");
3933
- audit(ctx, name, "blocked", args);
3934
- return errorResult(
3935
- "AGENT_RUN_DEPRECATED",
3936
- "Kernel-managed batch dispatch is retired. Convert ready Tasks into WorkContracts, claim them explicitly, and launch peer external Controllers through the Thin Launcher.",
3937
- {
3938
- requestedIssue: typeof args.issue_id === "string" && args.issue_id.trim() ? args.issue_id.trim() : undefined,
3939
- currentFocus: loadControllerProjectState(ctx.repoRoot).currentIssueId,
3940
- focusIsInformational: true,
3941
- },
3942
- );
3943
- }
3944
- case "get_task_run": {
3945
- if (ctx.policy.profile !== "controller")
3946
- return errorResult(
3947
- "TOOL_DISABLED",
3948
- "get_task_run requires the controller profile",
3949
- );
3950
- const run = getAgentJob(ctx.repoRoot, String(args.run_id ?? ""));
3951
- audit(ctx, name, "ok", args, run.promptPath);
3952
- return textResult(summarizeAgentRun(ctx.repoRoot, run as unknown as Record<string, unknown>, {
3953
- includePaths: args.include_paths === true,
3954
- includeOutput: args.include_output !== false,
3955
- tailChars: typeof args.tail_chars === "number" ? Math.trunc(args.tail_chars) : undefined,
3956
- }));
3957
- }
3958
- case "get_task_run_events": {
1980
+ case "search_repository": {
3959
1981
  if (ctx.policy.profile !== "controller")
3960
1982
  return errorResult(
3961
1983
  "TOOL_DISABLED",
3962
- "get_task_run_events requires the controller profile",
1984
+ "search_repository requires the controller profile",
3963
1985
  );
3964
- const sinceEventIndex = typeof args.since_event_index === "number"
3965
- ? Math.trunc(args.since_event_index)
1986
+ const session = ctx.sessionId && ctx.repoId
1987
+ ? {
1988
+ sessionId: ctx.sessionId,
1989
+ repoId: ctx.repoId,
1990
+ checkoutId: ctx.checkoutId ?? ctx.repoId,
1991
+ }
3966
1992
  : undefined;
3967
- const events = getAgentJobEvents(
3968
- ctx.repoRoot,
3969
- String(args.run_id ?? ""),
3970
- typeof args.limit === "number" ? Math.trunc(args.limit) : 200,
3971
- {
3972
- sinceIndex: sinceEventIndex,
3973
- includeHeartbeats: args.include_heartbeats === true,
3974
- },
3975
- );
3976
- audit(ctx, name, "ok", args);
3977
- return textResult({
3978
- events,
3979
- // Keep an existing cursor stable when no new event is available. New events
3980
- // that race with a read are returned on the next call instead of being lost.
3981
- nextSinceEventIndex: events.at(-1)?.data?.eventIndex ?? sinceEventIndex,
3982
- heartbeatsCollapsed: args.include_heartbeats !== true,
1993
+ const result = searchRepository(ctx.repoRoot, ctx.policy, {
1994
+ query: String(args.query ?? ""),
1995
+ includeGlobs: stringList(args.include_globs),
1996
+ excludeGlobs: stringList(args.exclude_globs),
1997
+ maxResults:
1998
+ typeof args.max_results === "number" ? args.max_results : undefined,
1999
+ maxFiles:
2000
+ typeof args.max_files === "number" ? args.max_files : undefined,
2001
+ caseSensitive: args.case_sensitive === true,
2002
+ session,
3983
2003
  });
2004
+ audit(ctx, name, "ok", args);
2005
+ return textResult(result);
3984
2006
  }
3985
- case "get_task_run_log": {
2007
+ case "get_git_diff": {
3986
2008
  if (ctx.policy.profile !== "controller")
3987
2009
  return errorResult(
3988
2010
  "TOOL_DISABLED",
3989
- "get_task_run_log requires the controller profile",
2011
+ "get_git_diff requires the controller profile",
3990
2012
  );
3991
- const log = getAgentJobLog(
3992
- ctx.repoRoot,
3993
- String(args.run_id ?? ""),
3994
- false,
3995
- typeof args.max_bytes === "number" ? Math.trunc(args.max_bytes) : 32 * 1024,
3996
- );
3997
- audit(ctx, name, "ok", args, log.url);
3998
- const run = getAgentJob(ctx.repoRoot, String(args.run_id ?? ""));
3999
- const bounded = redactRunText(
4000
- log.log,
4001
- ctx.repoRoot,
4002
- {
4003
- executionRoot: run.executionRoot,
4004
- worktree: run.worktree,
4005
- worktreePath: run.worktreePath,
4006
- },
4007
- Math.max(
4008
- 4 * 1024,
4009
- Math.min(
4010
- typeof args.max_bytes === "number" ? Math.trunc(args.max_bytes) : 32 * 1024,
4011
- 256 * 1024,
4012
- ),
4013
- ),
4014
- );
2013
+ const path =
2014
+ typeof args.path === "string" && args.path.trim()
2015
+ ? args.path.trim()
2016
+ : undefined;
2017
+ if (path) {
2018
+ const decision = resolveMcpPath(
2019
+ ctx.repoRoot,
2020
+ path,
2021
+ ctx.policy,
2022
+ "read",
2023
+ );
2024
+ if (!decision.ok)
2025
+ return errorResult(
2026
+ "POLICY_DENIED",
2027
+ decision.reason ?? "path denied",
2028
+ { path },
2029
+ );
2030
+ }
2031
+ const maxBytes =
2032
+ typeof args.max_bytes === "number"
2033
+ ? Math.min(Math.max(Math.trunc(args.max_bytes), 1024), 512 * 1024)
2034
+ : 128 * 1024;
2035
+ const result = gitDiff(ctx.repoRoot, path, maxBytes);
2036
+ const redacted = redactMcpText(result.diff);
2037
+ audit(ctx, name, "ok", args, path);
4015
2038
  return textResult({
4016
- ...log,
4017
- log: bounded.text,
4018
- truncated: bounded.truncated,
4019
- redactions: bounded.redactions,
4020
- next: bounded.truncated
4021
- ? "Call get_task_run_log with a larger max_bytes value for more retained tail."
4022
- : undefined,
2039
+ ...result,
2040
+ diff: redacted.text,
2041
+ redactions: redacted.redactions,
4023
2042
  });
4024
2043
  }
4025
- case "get_task_diff": {
2044
+ case "list_checks": {
4026
2045
  if (ctx.policy.profile !== "controller")
4027
2046
  return errorResult(
4028
2047
  "TOOL_DISABLED",
4029
- "get_task_diff requires the controller profile",
2048
+ "list_checks requires the controller profile",
4030
2049
  );
4031
- const diff = taskRunDiff(
4032
- ctx.repoRoot,
4033
- String(args.run_id ?? ""),
4034
- typeof args.max_bytes === "number"
4035
- ? Math.min(Math.max(Math.trunc(args.max_bytes), 1024), 1024 * 1024)
4036
- : undefined,
4037
- );
4038
- audit(ctx, name, "ok", args, diff.worktree);
4039
- return textResult(diff);
2050
+ const checks = listControllerChecks(ctx.repoRoot);
2051
+ audit(ctx, name, "ok", args);
2052
+ return textResult({ checks });
4040
2053
  }
4041
- case "integrate_task_run": {
2054
+ case "get_worklog_timeline": {
4042
2055
  if (ctx.policy.profile !== "controller")
4043
- return errorResult(
4044
- "TOOL_DISABLED",
4045
- "integrate_task_run requires the controller profile",
4046
- );
4047
- const integrated = integrateAgentJob(
4048
- ctx.repoRoot,
4049
- ctx.policy,
4050
- String(args.run_id ?? ""),
4051
- );
4052
- audit(
4053
- ctx,
4054
- name,
4055
- "ok",
4056
- args,
4057
- `controller-home://repositories/${editExecutionIdentity(ctx).repoId}/edit-sessions/${integrated.session.sessionId}`,
4058
- );
4059
- return textResult(integrated);
2056
+ return errorResult("TOOL_DISABLED", "get_worklog_timeline requires the controller profile");
2057
+ const events = listControllerWorklogEvents(ctx.repoRoot, {
2058
+ category: parseWorklogCategory(args.category),
2059
+ issueId: typeof args.issue_id === "string" ? args.issue_id : undefined,
2060
+ taskId: typeof args.task_id === "string" ? args.task_id : undefined,
2061
+ runId: typeof args.run_id === "string" ? args.run_id : undefined,
2062
+ editSessionId: typeof args.edit_session_id === "string" ? args.edit_session_id : undefined,
2063
+ since: typeof args.since === "string" ? args.since : undefined,
2064
+ until: typeof args.until === "string" ? args.until : undefined,
2065
+ limit: typeof args.limit === "number" ? Math.trunc(args.limit) : undefined,
2066
+ });
2067
+ audit(ctx, name, "ok", args);
2068
+ return textResult({ events });
4060
2069
  }
4061
- case "list_task_runs": {
2070
+ case "export_worklog": {
4062
2071
  if (ctx.policy.profile !== "controller")
4063
- return errorResult(
4064
- "TOOL_DISABLED",
4065
- "list_task_runs requires the controller profile",
4066
- );
4067
- const runs = listAgentJobs(
4068
- ctx.repoRoot,
4069
- typeof args.limit === "number" ? args.limit : 50,
4070
- );
4071
- audit(ctx, name, "ok", args);
4072
- return textResult({
4073
- runs: summarizeRunList(
4074
- ctx.repoRoot,
4075
- runs as unknown as Array<Record<string, unknown>>,
4076
- args.include_paths === true,
4077
- ),
2072
+ return errorResult("TOOL_DISABLED", "export_worklog requires the controller profile");
2073
+ const result = exportControllerWorklog(ctx.repoRoot, {
2074
+ format: args.format === "json" ? "json" : "markdown",
2075
+ outputPath: typeof args.output_path === "string" ? args.output_path : undefined,
2076
+ filter: {
2077
+ issueId: typeof args.issue_id === "string" ? args.issue_id : undefined,
2078
+ taskId: typeof args.task_id === "string" ? args.task_id : undefined,
2079
+ runId: typeof args.run_id === "string" ? args.run_id : undefined,
2080
+ editSessionId: typeof args.edit_session_id === "string" ? args.edit_session_id : undefined,
2081
+ },
4078
2082
  });
2083
+ audit(ctx, name, "ok", args, result.path);
2084
+ return textResult(result);
4079
2085
  }
4080
- case "cancel_task_run": {
2086
+ case "get_github_plugin_status": {
4081
2087
  if (ctx.policy.profile !== "controller")
4082
- return errorResult(
4083
- "TOOL_DISABLED",
4084
- "cancel_task_run requires the controller profile",
4085
- );
4086
- const run = cancelAgentJob(ctx.repoRoot, String(args.run_id ?? ""));
4087
- audit(ctx, name, "ok", args, run.promptPath);
4088
- return textResult(run);
2088
+ return errorResult("TOOL_DISABLED", "get_github_plugin_status requires the controller profile");
2089
+ const status = getGitHubPluginStatus(ctx.repoRoot);
2090
+ audit(ctx, name, "ok", args, status.repository);
2091
+ return textResult(status);
4089
2092
  }
4090
- case "retry_task_run": {
2093
+ case "configure_github_plugin": {
4091
2094
  if (ctx.policy.profile !== "controller")
4092
- return errorResult(
4093
- "TOOL_DISABLED",
4094
- "retry_task_run requires the controller profile",
4095
- );
4096
- // Fail closed before Task/provider side effects. Historical Runs remain readable.
4097
- audit(ctx, name, "failed", args, undefined, "AGENT_RUN_RETIRED");
4098
- return errorResult(
4099
- "AGENT_RUN_RETIRED",
4100
- "Kernel-managed Agent Run retry is disabled. Create or claim a WorkContract and start an external SuperController through Thin Launcher.",
4101
- );
4102
- }
4103
- case "finish_task_run": {
4104
- if (ctx.policy.profile !== "controller") return errorResult("TOOL_DISABLED", "finish_task_run requires the controller profile");
4105
- const runId = String(args.run_id ?? "").trim();
4106
- if (!runId) return errorResult("RUN_ID_REQUIRED", "finish_task_run requires run_id");
4107
- const decision = typeof args.decision === "string" ? args.decision : "auto";
4108
- if (!["auto", "approve_and_finish", "request_changes", "discard"].includes(decision)) {
4109
- return errorResult("INVALID_DECISION", "decision must be auto, approve_and_finish, request_changes, or discard");
4110
- }
4111
- const finished = finishTaskRun(ctx.repoRoot, {
4112
- runId,
4113
- decision: decision as "auto" | "approve_and_finish" | "request_changes" | "discard",
4114
- reviewer: typeof args.reviewer === "string" ? args.reviewer : undefined,
4115
- note: typeof args.note === "string" ? args.note : undefined,
4116
- cleanup: typeof args.cleanup === "boolean" ? args.cleanup : undefined,
4117
- commit: typeof args.commit === "boolean" ? args.commit : undefined,
4118
- });
4119
- audit(ctx, name, ["blocked", "needs_decision"].includes(finished.action) ? "failed" : "ok", args, `tasks/issues/${finished.issueId}`, finished.reason);
4120
- return textResult(finished);
4121
- }
4122
- case "verify_task": {
4123
- if (ctx.policy.profile !== "controller") return errorResult("TOOL_DISABLED", "verify_task requires the controller profile");
4124
- const issueId = String(args.issue_id ?? "");
4125
- const taskId = String(args.task_id ?? "");
4126
- const current = getIssue(ctx.repoRoot, issueId);
4127
- const task = current.tasks.find((entry) => entry.id === taskId);
4128
- if (!task) return errorResult("TASK_NOT_FOUND", `task not found: ${issueId}/${taskId}`);
4129
- const policy = taskExecutionPolicy(task);
4130
- const evidenceRunId = typeof args.run_id === "string" ? args.run_id : task.runIds.at(-1);
4131
- let evidenceRun: ReturnType<typeof getAgentJob> | undefined;
4132
- if (evidenceRunId) {
4133
- evidenceRun = getAgentJob(ctx.repoRoot, evidenceRunId);
4134
- if (evidenceRun.issueId !== issueId || evidenceRun.taskId !== taskId) return errorResult("RUN_TASK_MISMATCH", `Run ${evidenceRunId} does not belong to ${issueId}/${taskId}`);
4135
- if (evidenceRun.status !== "succeeded") return errorResult("RUN_NOT_SUCCEEDED", `verification requires a succeeded Run (current: ${evidenceRun.status})`);
4136
- if (evidenceRun.provider === "local" && evidenceRun.worktree !== ctx.repoRoot && !evidenceRun.integratedSessionId) return errorResult("RUN_NOT_INTEGRATED", "integrate the isolated local Run before recording verification");
4137
- if (evidenceRun.provider === "github" && evidenceRun.github?.createPullRequest !== false && !evidenceRun.github?.pullRequestUrl) return errorResult("GITHUB_PR_NOT_READY", "GitHub cloud verification requires the linked pull request to be available");
4138
- }
4139
-
4140
- const requestedCheckInputs = taskObjects(args.check_results).map((entry) => ({
4141
- checkId: String(entry.check_id ?? "").trim(),
4142
- requestedSummary: typeof entry.summary === "string" ? entry.summary : undefined,
4143
- })).filter((entry) => entry.checkId);
4144
- const registryChecks = listControllerChecks(ctx.repoRoot);
4145
- const requestedCheckIds = task.checks.length > 0 ? task.checks : requestedCheckInputs.map((entry) => entry.checkId);
4146
- const normalizedChecks = normalizeCheckIds(requestedCheckIds, registryChecks);
4147
- const checkIds = normalizedChecks.validCheckIds;
4148
- const checkResults: TaskVerification['checkResults'] = [];
4149
- if (checkIds.length > 0) {
4150
- const controllerHome = resolveRepoPreferredControllerHome(ctx.repoRoot);
4151
- const repository = resolveRepositorySelection({
4152
- explicitPath: ctx.repoRoot,
4153
- controllerHome,
4154
- allowSoleRepository: true,
4155
- });
4156
- if (!repository) return errorResult('TASK_CHECK_REPOSITORY_UNRESOLVED', 'verify_task could not resolve the repository checkout');
4157
- const taskCheckRequestBase = typeof args.request_id === 'string' && args.request_id.trim()
4158
- ? args.request_id.trim()
4159
- : `verify-task:${issueId}:${taskId}:${createHash('sha256').update(JSON.stringify({ checkIds, revision: args.integrated_revision ?? evidenceRun?.integratedSessionId ?? 'current' })).digest('hex').slice(0, 16)}`;
4160
- for (const [index, checkId] of checkIds.entries()) {
4161
- const processRequestId = `${taskCheckRequestBase}:check:${index + 1}:${createHash('sha256').update(checkId).digest('hex').slice(0, 12)}`;
4162
- const facade = await runPersistedCheckViaProcessRuntime({
4163
- controllerHome,
4164
- repoId: repository.repoId,
4165
- checkoutId: repository.activeCheckoutId,
4166
- repoRoot: repository.canonicalRoot,
4167
- executionIdentity: executionIdentityForRepository(repository),
4168
- checkId,
4169
- requestId: processRequestId,
4170
- commandId: processRequestId,
4171
- verificationBinding: { issueId, taskId },
4172
- });
4173
- if (facade.mode === 'durable' || !facade.process) {
4174
- return errorResult('TASK_CHECK_DURABLE_REQUIRED', facade.durable?.reason ?? `check requires durable workflow: ${checkId}`);
4175
- }
4176
- if (!facade.process.completed) {
4177
- return textResult({
4178
- accepted: true,
4179
- status: 'verification_running',
4180
- issueId,
4181
- taskId,
4182
- checkId,
4183
- requestId: taskCheckRequestBase,
4184
- processId: facade.process.processId,
4185
- next: `Retry verify_task with request_id=${taskCheckRequestBase}; the original Process will be reused.`,
4186
- durableSideEffects: { executionJobCount: 0, localJobCount: 0, workerSpawnCount: 0, projectionUpdateCount: 0 },
4187
- });
4188
- }
4189
- const record = getProcessRecord(controllerHome, repository.repoId, facade.process.processId);
4190
- if (!record) return errorResult('TASK_CHECK_PROCESS_MISSING', `process record not found: ${facade.process.processId}`);
4191
- const leaseConflict = taskVerificationLeaseConflict(record);
4192
- if (leaseConflict) {
4193
- const retryRequestId = taskVerificationRetryRequestId(
4194
- taskCheckRequestBase,
4195
- checkId,
4196
- facade.process.processId,
4197
- );
4198
- return textResult({
4199
- accepted: true,
4200
- status: 'verification_deferred',
4201
- reason: 'repository_resource_busy',
4202
- issueId,
4203
- taskId,
4204
- checkId,
4205
- requestId: taskCheckRequestBase,
4206
- processId: facade.process.processId,
4207
- conflict: {
4208
- code: 'PROCESS_LEASE_CONFLICT',
4209
- resourceKey: leaseConflict.resourceKey,
4210
- blockingProcessId: leaseConflict.blockingProcessId,
4211
- },
4212
- retryRequestId,
4213
- next: `Retry verify_task with request_id=${retryRequestId} after the conflicting repository process settles.`,
4214
- durableSideEffects: { executionJobCount: 0, localJobCount: 0, workerSpawnCount: 0, projectionUpdateCount: 0 },
4215
- });
4216
- }
4217
- const receipt = processCheckCompletionReceipt(record, {
4218
- repoId: repository.repoId,
4219
- checkoutId: repository.activeCheckoutId,
4220
- issueId,
4221
- taskId,
4222
- checkId,
4223
- processId: facade.process.processId,
4224
- });
4225
- checkResults.push({
4226
- checkId,
4227
- ok: receipt.ok,
4228
- summary: receipt.summary,
4229
- receipt,
4230
- });
4231
- }
4232
- }
4233
-
4234
- const commandEvidence: TaskCommandEvidence[] = [];
4235
- for (const entry of taskObjects(args.reported_commands)) {
4236
- const command = Array.isArray(entry.command) ? entry.command.map(String).map((part) => part.trim()).filter(Boolean) : [];
4237
- if (command.length === 0) return errorResult("INVALID_COMMAND_EVIDENCE", "reported command evidence requires a non-empty command argv");
4238
- const cwdInput = typeof entry.cwd === "string" && entry.cwd.trim() ? entry.cwd.trim() : undefined;
4239
- if (cwdInput && cwdInput !== "." && cwdInput !== "./") {
4240
- const cwdDecision = resolveMcpPath(ctx.repoRoot, cwdInput, ctx.policy, "read");
4241
- if (!cwdDecision.ok) {
4242
- const normalizedCwd = normalizeMcpRelativePath(cwdInput);
4243
- const missingHarnessEvidence = normalizedCwd.ok &&
4244
- normalizedCwd.relativePath?.startsWith(".ai/harness/") &&
4245
- cwdDecision.reason?.startsWith("path does not exist:");
4246
- if (!missingHarnessEvidence) return errorResult("COMMAND_EVIDENCE_PATH_DENIED", cwdDecision.reason ?? "reported command cwd is denied");
4247
- }
4248
- }
4249
- const artifactInput = typeof entry.artifact_path === "string" && entry.artifact_path.trim() ? entry.artifact_path.trim() : undefined;
4250
- if (artifactInput) {
4251
- const artifactDecision = resolveMcpPath(ctx.repoRoot, artifactInput, ctx.policy, "read");
4252
- if (!artifactDecision.ok) {
4253
- const normalizedArtifact = normalizeMcpRelativePath(artifactInput);
4254
- const missingHarnessEvidence = normalizedArtifact.ok &&
4255
- normalizedArtifact.relativePath?.startsWith(".ai/harness/") &&
4256
- artifactDecision.reason?.startsWith("path does not exist:");
4257
- if (!missingHarnessEvidence) return errorResult("COMMAND_EVIDENCE_ARTIFACT_DENIED", artifactDecision.reason ?? "reported command artifact is denied");
4258
- }
4259
- }
4260
- commandEvidence.push({
4261
- command,
4262
- cwd: cwdInput,
4263
- ok: entry.ok === true,
4264
- exitCode: typeof entry.exit_code === "number" ? Math.trunc(entry.exit_code) : undefined,
4265
- stdout: typeof entry.stdout === "string" ? entry.stdout.slice(-32 * 1024) : undefined,
4266
- stderr: typeof entry.stderr === "string" ? entry.stderr.slice(-32 * 1024) : undefined,
4267
- artifactPath: artifactInput,
4268
- reportedBy: String(args.reviewer ?? "").trim(),
4269
- executedAt: typeof entry.executed_at === "string" ? entry.executed_at : new Date().toISOString(),
4270
- source: "reported",
4271
- });
4272
- }
4273
-
4274
- let acceptanceResults: TaskVerification["acceptanceResults"] = taskObjects(args.acceptance_results).map((entry) => ({
4275
- criterion: String(entry.criterion ?? ""),
4276
- ok: entry.ok === true,
4277
- outcome: entry.ok === true ? "passed" as const : "failed" as const,
4278
- source: "reported" as const,
4279
- evidence: typeof entry.evidence === "string" ? entry.evidence : undefined,
4280
- }));
4281
- if (acceptanceResults.length === 0 && evidenceRun && task.acceptanceCriteria.length > 0) {
4282
- acceptanceResults = task.acceptanceCriteria.map((criterion) => ({
4283
- criterion,
4284
- ok: false,
4285
- outcome: "not_evaluated" as const,
4286
- source: "run_completion" as const,
4287
- evidence: `Successful Run ${evidenceRun!.runId}; acceptance was not independently evaluated.`,
4288
- }));
4289
- }
4290
- const verification: TaskVerification = {
4291
- runId: evidenceRunId,
4292
- integratedRevision: typeof args.integrated_revision === "string" ? args.integrated_revision : evidenceRun?.integratedSessionId,
4293
- reviewedDiffHash: typeof args.reviewed_diff_hash === "string" ? args.reviewed_diff_hash : undefined,
4294
- reviewer: String(args.reviewer ?? ""),
4295
- checkResults,
4296
- commandEvidence,
4297
- acceptanceResults,
4298
- verifiedAt: new Date().toISOString(),
4299
- };
4300
- const issue = recordTaskVerification(ctx.repoRoot, issueId, taskId, verification);
4301
- const issueView = projectIssueEffectiveView(ctx.repoRoot, issue);
4302
- const verifiedTask = issueView.tasks.find((entry) => entry.id === taskId);
4303
- audit(ctx, name, ["verified", "done"].includes(verifiedTask?.status ?? "") ? "ok" : "failed", args, `tasks/issues/${issue.id}`);
4304
- return textResult({
4305
- issueId,
4306
- taskId,
4307
- policy,
4308
- checkNormalization: {
4309
- validCheckIds: normalizedChecks.validCheckIds,
4310
- invalidCheckIds: normalizedChecks.invalidCheckIds,
4311
- warnings: normalizedChecks.warnings,
4312
- },
4313
- task: verifiedTask ? compactTaskView(verifiedTask as unknown as Record<string, unknown>) : undefined,
4314
- tasks: issueView.tasks.map((entry) => compactTaskView(entry as unknown as Record<string, unknown>)),
4315
- issue: compactIssueView(issueView as unknown as Record<string, unknown>),
2095
+ return errorResult("TOOL_DISABLED", "configure_github_plugin requires the controller profile");
2096
+ const config = saveGitHubPluginConfig(ctx.repoRoot, {
2097
+ enabled: typeof args.enabled === "boolean" ? args.enabled : undefined,
2098
+ repository: args.clear_repository === true ? "" : typeof args.repository === "string" ? args.repository : undefined,
2099
+ syncMode: args.sync_mode === "checkpoint" ? "checkpoint" : args.sync_mode === "manual" ? "manual" : undefined,
2100
+ includeTasks: typeof args.include_tasks === "boolean" ? args.include_tasks : undefined,
2101
+ projectOwner: args.clear_project === true ? "" : typeof args.project_owner === "string" ? args.project_owner : undefined,
2102
+ projectNumber: args.clear_project === true ? null : typeof args.project_number === "number" ? args.project_number : undefined,
4316
2103
  });
2104
+ audit(ctx, name, "ok", args, "repository-registry:github");
2105
+ return textResult(config);
4317
2106
  }
4318
- case "accept_task": {
4319
- if (ctx.policy.profile !== "controller") return errorResult("TOOL_DISABLED", "accept_task requires the controller profile");
4320
- const issueId = String(args.issue_id ?? "");
4321
- const taskId = String(args.task_id ?? "");
4322
- const current = getIssue(ctx.repoRoot, issueId);
4323
- const task = current.tasks.find((entry) => entry.id === taskId);
4324
- if (!task) return errorResult("TASK_NOT_FOUND", `task not found: ${issueId}/${taskId}`);
4325
- if (task.status === "done") return textResult(projectIssueEffectiveView(ctx.repoRoot, current));
4326
- if (task.status !== "verified" || !task.verification) return errorResult("TASK_NOT_VERIFIED", `task has not passed its risk-adaptive verification requirements (current: ${task.status})`);
4327
- const issue = acceptVerifiedTask(
4328
- ctx.repoRoot,
4329
- issueId,
4330
- taskId,
4331
- typeof args.note === "string" ? args.note : "Accepted after required verification evidence.",
4332
- );
4333
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
4334
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
4335
- }
4336
- case "request_task_changes": {
2107
+ case "github_status": {
4337
2108
  if (ctx.policy.profile !== "controller")
4338
2109
  return errorResult(
4339
2110
  "TOOL_DISABLED",
4340
- "request_task_changes requires the controller profile",
4341
- );
4342
- const issueId = String(args.issue_id ?? "");
4343
- const taskId = String(args.task_id ?? "");
4344
- const current = getIssue(ctx.repoRoot, issueId);
4345
- const task = current.tasks.find((entry) => entry.id === taskId);
4346
- if (!task)
4347
- return errorResult(
4348
- "TASK_NOT_FOUND",
4349
- `task not found: ${issueId}/${taskId}`,
4350
- );
4351
- if (
4352
- !["review", "integrated", "verifying", "verified"].includes(
4353
- task.status,
4354
- )
4355
- )
4356
- return errorResult(
4357
- "TASK_NOT_REVIEWABLE",
4358
- `task must be in review, integrated, verifying, or verified before requesting changes (current: ${task.status})`,
2111
+ "github_status requires the controller profile",
4359
2112
  );
4360
- const issue = updateTask(ctx.repoRoot, issueId, taskId, {
4361
- status: "changes_requested",
4362
- note: String(args.note ?? ""),
4363
- });
4364
- audit(ctx, name, "ok", args, `tasks/issues/${issue.id}`);
4365
- return textResult(projectIssueEffectiveView(ctx.repoRoot, issue));
2113
+ const status = getGitHubStatus(
2114
+ ctx.repoRoot,
2115
+ typeof args.repo === "string" ? args.repo : undefined,
2116
+ );
2117
+ audit(
2118
+ ctx,
2119
+ name,
2120
+ status.available && status.authenticated ? "ok" : "failed",
2121
+ args,
2122
+ status.repository?.nameWithOwner,
2123
+ status.errors.join("; "),
2124
+ );
2125
+ return textResult(status);
4366
2126
  }
4367
2127
  case "begin_edit_session": {
4368
2128
  if (ctx.policy.profile !== "controller")
@@ -4592,29 +2352,6 @@ export async function callMcpTool(
4592
2352
  );
4593
2353
  return textResult(session);
4594
2354
  }
4595
- case "finish_edit_session": {
4596
- if (ctx.policy.profile !== "controller")
4597
- return errorResult(
4598
- "TOOL_DISABLED",
4599
- "finish_edit_session requires the controller profile",
4600
- );
4601
- const sessionId = String(args.session_id ?? "").trim();
4602
- if (!sessionId) return errorResult("SESSION_ID_REQUIRED", "finish_edit_session requires session_id");
4603
- const decision = typeof args.decision === "string" ? args.decision : "auto";
4604
- if (!["auto", "approve_and_finish", "request_changes", "discard"].includes(decision)) {
4605
- return errorResult("INVALID_DECISION", "decision must be auto, approve_and_finish, request_changes, or discard");
4606
- }
4607
- const finished = finishEditSession(ctx.repoRoot, {
4608
- sessionId,
4609
- decision: decision as "auto" | "approve_and_finish" | "request_changes" | "discard",
4610
- reviewer: typeof args.reviewer === "string" ? args.reviewer : undefined,
4611
- note: typeof args.note === "string" ? args.note : undefined,
4612
- checkIds: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
4613
- commit: typeof args.commit === "boolean" ? args.commit : undefined,
4614
- });
4615
- audit(ctx, name, ["blocked", "needs_decision"].includes(finished.action) ? "failed" : "ok", args, `tasks/issues/${finished.issueId}`, finished.reason);
4616
- return textResult(finished);
4617
- }
4618
2355
  case "list_workflow_files": {
4619
2356
  const files = workflowFileCandidates(ctx.repoRoot)
4620
2357
  .filter(