@moretea-labs/forge 1.5.0-rc.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/AGENTS.md +9 -11
  2. package/CHANGELOG.md +27 -1
  3. package/CLAUDE.md +8 -10
  4. package/DESIGN.md +278 -0
  5. package/README.md +70 -90
  6. package/README.zh-CN.md +52 -55
  7. package/SKILL.md +8 -11
  8. package/THIRD_PARTY_NOTICES.md +9 -3
  9. package/assets/branding/README.md +4 -2
  10. package/assets/hooks/lib/workflow-state.sh +19 -18
  11. package/assets/hooks/post-edit-guard.sh +6 -75
  12. package/assets/hooks/prompt-guard.sh +24 -12
  13. package/assets/hooks/session-start-context.sh +17 -17
  14. package/assets/hooks/stop-orchestrator.sh +2 -2
  15. package/assets/partials/04-project-structure.partial.md +6 -6
  16. package/assets/partials/05-workflow.partial.md +6 -6
  17. package/assets/partials/07-footer.partial.md +1 -1
  18. package/assets/partials/08-orchestration.partial.md +2 -2
  19. package/assets/partials-agents/02-operating-mode.partial.md +5 -5
  20. package/assets/partials-agents/03-orchestration.partial.md +3 -3
  21. package/assets/partials-agents/04-task-protocol.partial.md +3 -3
  22. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  23. package/assets/reference-configs/agentic-development-flow.md +49 -86
  24. package/assets/reference-configs/external-tooling.md +21 -61
  25. package/assets/reference-configs/handoff-protocol.md +25 -20
  26. package/assets/reference-configs/harness-overview.md +8 -8
  27. package/assets/skill-version.json +2 -2
  28. package/assets/templates/helpers/archive-workflow.sh +0 -1
  29. package/assets/templates/helpers/capture-plan.sh +3 -3
  30. package/assets/templates/helpers/check-task-workflow.sh +2 -2
  31. package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
  32. package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
  33. package/assets/templates/helpers/new-plan.sh +1 -1
  34. package/assets/templates/helpers/plan-to-todo.sh +2 -2
  35. package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
  36. package/assets/templates/helpers/prepare-handoff.sh +9 -9
  37. package/assets/templates/helpers/refresh-current-status.sh +2 -2
  38. package/assets/templates/helpers/ship-worktrees.sh +1 -1
  39. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  40. package/assets/templates/helpers/verify-sprint.sh +7 -7
  41. package/assets/templates/helpers/workstream-sync.sh +1 -1
  42. package/assets/templates/plan.template.md +1 -1
  43. package/assets/workflow-contract.v1.json +6 -3
  44. package/docs/architecture/current/README.md +10 -9
  45. package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
  46. package/docs/architecture/current/architecture-invariants.md +2 -2
  47. package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
  48. package/docs/architecture/current/codegraph-context-provider.md +2 -2
  49. package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
  50. package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
  51. package/docs/architecture/current/entity-model.md +0 -27
  52. package/docs/architecture/current/failure-recovery.md +0 -24
  53. package/docs/architecture/current/governance.md +1 -1
  54. package/docs/architecture/current/implementation-status.md +1 -1
  55. package/docs/architecture/current/legacy-governance.md +58 -0
  56. package/docs/architecture/current/multi-repository-execution.md +1 -1
  57. package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
  58. package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
  59. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
  60. package/docs/architecture/current/target-requirements-traceability.md +2 -2
  61. package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
  62. package/docs/architecture/current/thin-harness-v1.md +9 -11
  63. package/docs/architecture/domains/runtime-harness.md +1 -1
  64. package/docs/forge-autonomous-goal-loop.md +3 -1
  65. package/docs/forge-chatgpt-mcp-setup.md +67 -176
  66. package/docs/images/forge-banner-cn.svg +26 -1
  67. package/docs/images/forge-banner.svg +26 -1
  68. package/docs/operations/features.md +1 -1
  69. package/docs/operations/homebrew.md +3 -3
  70. package/docs/operations/mcp-tool-exposure.md +5 -5
  71. package/docs/operations/platform-support.md +45 -37
  72. package/docs/operations/platform-support.zh-CN.md +40 -38
  73. package/docs/operations/releasing.md +3 -3
  74. package/docs/operations/releasing.zh-CN.md +3 -3
  75. package/docs/operations/troubleshooting.md +11 -6
  76. package/docs/operations/troubleshooting.zh-CN.md +11 -6
  77. package/docs/public-usage-guide.md +28 -21
  78. package/docs/public-usage-guide.zh-CN.md +28 -21
  79. package/docs/tutorials/01-install-and-start.md +58 -35
  80. package/docs/tutorials/01-install-and-start.zh-CN.md +58 -35
  81. package/docs/tutorials/02-connect-chatgpt.md +118 -46
  82. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +119 -45
  83. package/docs/wiki/Architecture.md +1 -1
  84. package/docs/wiki/Installation.md +2 -2
  85. package/docs/wiki/Quick-Start.md +1 -1
  86. package/docs/wiki/Work-Lifecycle.md +1 -1
  87. package/install.ps1 +4 -5
  88. package/install.sh +4 -6
  89. package/package.json +15 -5
  90. package/scripts/archive-workflow.sh +0 -1
  91. package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
  92. package/scripts/build-controller-ui.ts +28 -0
  93. package/scripts/capture-plan.sh +4 -4
  94. package/scripts/check-agent-tooling.sh +12 -1
  95. package/scripts/check-platform-support.mjs +2 -2
  96. package/scripts/check-public-docs.sh +16 -5
  97. package/scripts/check-release-readiness.sh +5 -11
  98. package/scripts/check-runtime-architecture.mjs +14 -22
  99. package/scripts/check-tarball-install-smoke.sh +3 -2
  100. package/scripts/check-task-workflow.sh +2 -2
  101. package/scripts/codex-handoff-resume.sh +5 -5
  102. package/scripts/create-project-dirs.sh +3 -3
  103. package/scripts/ensure-task-workflow.sh +45 -35
  104. package/scripts/init-project.sh +4 -4
  105. package/scripts/install-desktop-operator-registration.ts +2 -2
  106. package/scripts/lib/project-init-lib.sh +46 -44
  107. package/scripts/migrate-project-template.sh +7 -4
  108. package/scripts/new-plan.sh +1 -1
  109. package/scripts/plan-to-todo.sh +2 -2
  110. package/scripts/prepare-codex-handoff.sh +2 -2
  111. package/scripts/prepare-handoff.sh +9 -9
  112. package/scripts/refresh-current-status.sh +2 -2
  113. package/scripts/setup-plugins.sh +3 -3
  114. package/scripts/ship-worktrees.sh +1 -1
  115. package/scripts/sprint-backlog.sh +5 -5
  116. package/scripts/sync-codex-installed-copies.sh +2 -3
  117. package/scripts/verify-sprint.sh +7 -7
  118. package/scripts/workstream-sync.sh +1 -1
  119. package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
  120. package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
  121. package/src/cli/chatgpt-browser/file-policy.ts +3 -1
  122. package/src/cli/commands/chatgpt.ts +51 -0
  123. package/src/cli/commands/doctor.ts +1 -1
  124. package/src/cli/commands/init-hook.ts +213 -70
  125. package/src/cli/commands/init.ts +5 -6
  126. package/src/cli/commands/mcp.ts +31 -5
  127. package/src/cli/commands/runtime.ts +61 -8
  128. package/src/cli/commands/setup-profile.ts +472 -0
  129. package/src/cli/controller/check-runner.ts +20 -3
  130. package/src/cli/controller/codex-continuation.ts +4 -7
  131. package/src/cli/controller/context-pack.ts +118 -14
  132. package/src/cli/controller/operational-plan.ts +4 -4
  133. package/src/cli/controller/task-ledger.ts +10 -10
  134. package/src/cli/controller/work-mode.ts +6 -13
  135. package/src/cli/index.ts +14 -35
  136. package/src/cli/local-bridge/connector-freshness.ts +1 -1
  137. package/src/cli/local-bridge/console-assets.ts +17 -0
  138. package/src/cli/local-bridge/console-automations.ts +160 -0
  139. package/src/cli/local-bridge/dashboard.ts +8 -1303
  140. package/src/cli/local-bridge/facade-api.ts +16 -7
  141. package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
  142. package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
  143. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
  144. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
  145. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
  146. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
  147. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
  148. package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
  149. package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
  150. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
  151. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
  152. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
  153. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
  154. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
  155. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
  156. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
  157. package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
  158. package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
  159. package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
  160. package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
  161. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
  162. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
  163. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
  164. package/src/cli/local-bridge/frontend/src/styles.css +12 -0
  165. package/src/cli/local-bridge/frontend/src/types.ts +14 -0
  166. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
  167. package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
  168. package/src/cli/local-bridge/server.ts +45 -0
  169. package/src/cli/local-bridge/ui-dist/app.css +1 -0
  170. package/src/cli/local-bridge/ui-dist/app.js +11094 -0
  171. package/src/cli/mcp/auth.ts +2 -0
  172. package/src/cli/mcp/instructions.ts +3 -3
  173. package/src/cli/mcp/legacy-tool-service.ts +27 -23
  174. package/src/cli/mcp/multi-repository.ts +2 -0
  175. package/src/cli/mcp/policy.ts +7 -5
  176. package/src/cli/mcp/repository-tools.ts +53 -14
  177. package/src/cli/mcp/server.ts +5 -10
  178. package/src/cli/mcp/setup.ts +220 -276
  179. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  180. package/src/cli/mcp/toolset-names.ts +1 -1
  181. package/src/cli/mcp/toolset.ts +0 -1
  182. package/src/cli/mcp/transports/http.ts +51 -4
  183. package/src/cli/repositories/branch-name-policy.ts +0 -7
  184. package/src/cli/repositories/controller-home.ts +110 -3
  185. package/src/cli/repositories/safe-patch.ts +1 -1
  186. package/src/cli/repositories/selected-path-actions.ts +26 -24
  187. package/src/core/adoption/gitignore-plan.ts +2 -3
  188. package/src/core/adoption/plan.ts +4 -1
  189. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
  190. package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
  191. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
  192. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
  193. package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
  194. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
  195. package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
  196. package/src/runtime/control-plane/facade/types.ts +0 -1
  197. package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
  198. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
  199. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
  200. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
  201. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
  202. package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
  203. package/src/runtime/control-plane/routing/route-policy.ts +14 -14
  204. package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
  205. package/src/runtime/evidence/event-ledger.ts +1 -1
  206. package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
  207. package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
  208. package/src/runtime/execution/process-runtime/check-result.ts +45 -0
  209. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
  210. package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
  211. package/src/runtime/execution/process-runtime/gc.ts +1 -1
  212. package/src/runtime/execution/process-runtime/index.ts +3 -0
  213. package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
  214. package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
  215. package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
  216. package/src/runtime/execution/process-runtime/runtime.ts +18 -1
  217. package/src/runtime/execution/process-runtime/store.ts +1 -19
  218. package/src/runtime/execution/process-runtime/types.ts +4 -0
  219. package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
  220. package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
  221. package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
  222. package/src/runtime/execution/thin-harness/types.ts +0 -1
  223. package/src/runtime/execution/workers/executor.ts +1 -1
  224. package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
  225. package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
  226. package/src/runtime/gateway/mcp/process-tools.ts +16 -3
  227. package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
  228. package/src/runtime/gateway/mcp/router.ts +117 -182
  229. package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
  230. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
  231. package/src/runtime/plugins/browser-adapter.ts +5 -0
  232. package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
  233. package/src/runtime/plugins/store.ts +14 -6
  234. package/src/runtime/plugins/types.ts +1 -1
  235. package/src/runtime/projections/materialized-view.ts +1 -23
  236. package/src/runtime/recovery/maintenance-executor.ts +9 -0
  237. package/src/runtime/resources/ownership.ts +0 -1
  238. package/src/runtime/root/gateway-adapter.ts +2 -0
  239. package/src/runtime/root/mcp-transport.ts +10 -2
  240. package/src/runtime/root/package-connector-service.ts +136 -0
  241. package/src/runtime/root/package-runtime-release.ts +161 -0
  242. package/src/runtime/root/package-runtime-service.ts +185 -0
  243. package/src/runtime/root/release-materialize.ts +19 -0
  244. package/src/runtime/root/runtime.ts +2 -1
  245. package/src/runtime/root/scheduler.ts +5 -0
  246. package/src/runtime/root/service.ts +18 -5
  247. package/src/runtime/standalone-recovery/core.ts +65 -16
  248. package/src/runtime/workflow/findings/store.ts +145 -106
  249. package/src/runtime/workflow/findings/types.ts +22 -0
  250. package/src/runtime/workflow/schedules/engine.ts +132 -2
  251. package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
  252. package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
  253. package/docs/CHANGELOG.md +0 -978
  254. package/docs/README.zh-CN.md +0 -58
  255. package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
  256. package/src/cli/commands/controller.ts +0 -680
  257. package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
  258. package/src/runtime/workflow/campaigns/engine.ts +0 -112
  259. package/src/runtime/workflow/campaigns/normalize.ts +0 -100
  260. package/src/runtime/workflow/campaigns/review.ts +0 -315
  261. package/src/runtime/workflow/campaigns/store.ts +0 -526
  262. package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
  263. package/src/runtime/workflow/campaigns/types.ts +0 -290
  264. /package/src/runtime/{workflow/campaigns → execution}/workspace-agent.ts +0 -0
@@ -1,219 +1 @@
1
- import {
2
- getWorkContract,
3
- transitionWorkContractPhase,
4
- updateWorkContract,
5
- } from '../../control-plane/facade/work-contract-store';
6
- import {
7
- transitionWorkHandle,
8
- type WorkHandleState,
9
- } from '../../control-plane/execution/work-handle-store';
10
- import { processCheckCompletionReceipt } from '../../execution/process-runtime/check-receipt';
11
- import { processCheckSemanticScopeKey } from '../../execution/process-runtime/check-facade';
12
- import { getProcessRecord } from '../../execution/process-runtime/store';
13
- import { controllerCheckExecutionIdentity } from '../../../cli/controller/check-runner';
14
-
15
- export type WorkValidationOutcome =
16
- | 'not_validating'
17
- | 'running'
18
- | 'passed'
19
- | 'failed'
20
- | 'infrastructure_failure';
21
-
22
- export interface WorkValidationReconciliation {
23
- handle: WorkHandleState;
24
- changed: boolean;
25
- outcome: WorkValidationOutcome;
26
- summary?: string;
27
- }
28
-
29
- export function hasCurrentWorkValidationAuthority(input: {
30
- finalizationValidation: WorkHandleState['finalization']['validation'];
31
- validatedInputFingerprint?: string;
32
- evidenceState: string;
33
- expectedFingerprint: string;
34
- }): boolean {
35
- return input.finalizationValidation === 'done'
36
- && input.evidenceState === 'valid'
37
- && input.validatedInputFingerprint === input.expectedFingerprint;
38
- }
39
-
40
- /**
41
- * A new validation run invalidates delivery authority without deleting or
42
- * rewriting the prior receipts. `stale` is the only valid transition from an
43
- * already-valid evidence set; all other retryable states converge through
44
- * `partial`.
45
- */
46
- export function markWorkValidationPending(controllerHome: string, handle: WorkHandleState): void {
47
- const contractId = handle.workContractId;
48
- if (!contractId) return;
49
- const options = { controllerHome, repoId: handle.repositoryId };
50
- const contract = getWorkContract(options, contractId);
51
- if (!contract || contract.completionReceipt) return;
52
- const evidenceState = contract.evidenceState === 'valid' || contract.evidenceState === 'stale'
53
- ? 'stale'
54
- : 'partial';
55
- if (contract.evidenceState !== evidenceState) updateWorkContract(options, contractId, { evidenceState });
56
- }
57
-
58
- /**
59
- * Project durable validation evidence into the Work-owned contract lifecycle.
60
- * Process and Check records contribute evidence only; they never become a
61
- * second completion or lifecycle authority.
62
- */
63
- export function projectWorkValidationOutcome(
64
- controllerHome: string,
65
- handle: WorkHandleState,
66
- outcome: Exclude<WorkValidationOutcome, 'not_validating' | 'running'>,
67
- summary?: string,
68
- ): void {
69
- const contractId = handle.workContractId;
70
- if (!contractId) return;
71
- const options = { controllerHome, repoId: handle.repositoryId };
72
- const contract = getWorkContract(options, contractId);
73
- if (!contract || contract.completionReceipt) return;
74
-
75
- if (outcome === 'passed') {
76
- transitionWorkContractPhase(options, contractId, {
77
- phase: 'delivery',
78
- status: 'running',
79
- state: 'active',
80
- summary: summary ?? 'All requested validation receipts passed; delivery is next.',
81
- evidenceRefs: contract.evidenceRefs,
82
- });
83
- updateWorkContract(options, contractId, { evidenceState: 'valid' });
84
- return;
85
- }
86
-
87
- if (outcome === 'failed') {
88
- transitionWorkContractPhase(options, contractId, {
89
- phase: 'cleanup',
90
- status: 'failed',
91
- state: 'failed',
92
- summary: summary ?? 'A requested validation check failed; terminal cleanup is next.',
93
- evidenceRefs: contract.evidenceRefs,
94
- });
95
- updateWorkContract(options, contractId, { evidenceState: 'failed' });
96
- return;
97
- }
98
-
99
- transitionWorkContractPhase(options, contractId, {
100
- phase: 'cleanup',
101
- status: 'failed',
102
- state: 'failed',
103
- summary: summary ?? 'Validation infrastructure failed; terminal cleanup is required without treating this as an acceptance failure.',
104
- evidenceRefs: contract.evidenceRefs,
105
- });
106
- updateWorkContract(options, contractId, {
107
- evidenceState: contract.evidenceState === 'valid' || contract.evidenceState === 'stale' ? 'stale' : 'partial',
108
- });
109
- }
110
-
111
- function settleInfrastructureFailure(
112
- controllerHome: string,
113
- handle: WorkHandleState,
114
- summary: string,
115
- ): WorkValidationReconciliation {
116
- const next = transitionWorkHandle(controllerHome, handle, 'failed', {
117
- finalization: { ...handle.finalization, validation: 'failed', lastError: summary },
118
- validationRun: undefined,
119
- failureReason: summary,
120
- });
121
- projectWorkValidationOutcome(controllerHome, next, 'infrastructure_failure', summary);
122
- return { handle: next, changed: true, outcome: 'infrastructure_failure', summary };
123
- }
124
-
125
- /**
126
- * Converge a persisted Work validation run from durable Process receipts.
127
- *
128
- * No command is launched here. Missing/running bindings stay pending; timeout,
129
- * cancellation, missing process state, or malformed receipt makes the Work
130
- * terminally failed for cleanup without classifying it as an acceptance failure.
131
- */
132
- export function reconcileWorkValidation(
133
- controllerHome: string,
134
- handle: WorkHandleState,
135
- ): WorkValidationReconciliation {
136
- const run = handle.validationRun;
137
- if (handle.state !== 'validating' || !run) {
138
- return { handle, changed: false, outcome: 'not_validating' };
139
- }
140
-
141
- if (run.requestedChecks.length === 0) {
142
- const next = transitionWorkHandle(controllerHome, handle, run.resumeState, {
143
- finalization: { ...handle.finalization, validation: 'done', lastError: undefined },
144
- validationRun: undefined,
145
- validatedInputFingerprint: run.fingerprint,
146
- failureReason: undefined,
147
- });
148
- projectWorkValidationOutcome(controllerHome, next, 'passed', 'No validation checks were required.');
149
- return { handle: next, changed: true, outcome: 'passed' };
150
- }
151
-
152
- for (const checkId of run.requestedChecks) {
153
- const binding = run.processes[checkId];
154
- if (!binding) return { handle, changed: false, outcome: 'running' };
155
- const record = getProcessRecord(controllerHome, handle.repositoryId, binding.processId);
156
- if (!record) {
157
- return settleInfrastructureFailure(
158
- controllerHome,
159
- handle,
160
- `Validation process record is unavailable: ${binding.processId}`,
161
- );
162
- }
163
- if (record.status === 'running' || record.status === 'starting' || record.status === 'running_recovered') {
164
- return { handle, changed: false, outcome: 'running' };
165
- }
166
-
167
- try {
168
- const currentIdentity = record.checkExecution
169
- ? controllerCheckExecutionIdentity(handle.worktreePath, checkId, record.checkExecution.timeoutMs)
170
- : undefined;
171
- const receipt = processCheckCompletionReceipt(record, {
172
- repoId: handle.repositoryId,
173
- checkoutId: handle.checkoutId,
174
- workId: handle.workId,
175
- executionSessionId: handle.sessionId,
176
- checkId,
177
- processId: binding.processId,
178
- ...(currentIdentity ? {
179
- checkExecution: {
180
- cacheKey: currentIdentity.cacheKey,
181
- revision: currentIdentity.revision,
182
- definitionDigest: currentIdentity.definitionDigest,
183
- environmentFingerprint: currentIdentity.environmentFingerprint,
184
- timeoutMs: currentIdentity.timeoutMs,
185
- scopeKey: processCheckSemanticScopeKey({
186
- checkoutId: handle.checkoutId,
187
- workId: handle.workId,
188
- verificationBinding: { executionSessionId: handle.sessionId },
189
- }, currentIdentity.reuseScope),
190
- },
191
- } : {}),
192
- });
193
- if (!receipt.ok) {
194
- if (receipt.status === 'timed_out' || receipt.status === 'cancelled') {
195
- return settleInfrastructureFailure(controllerHome, handle, receipt.summary);
196
- }
197
- const next = transitionWorkHandle(controllerHome, handle, 'failed', {
198
- finalization: { ...handle.finalization, validation: 'failed', lastError: receipt.summary },
199
- validationRun: undefined,
200
- failureReason: receipt.summary,
201
- });
202
- projectWorkValidationOutcome(controllerHome, next, 'failed', receipt.summary);
203
- return { handle: next, changed: true, outcome: 'failed', summary: receipt.summary };
204
- }
205
- } catch (error) {
206
- const summary = error instanceof Error ? error.message : String(error);
207
- return settleInfrastructureFailure(controllerHome, handle, summary);
208
- }
209
- }
210
-
211
- const next = transitionWorkHandle(controllerHome, handle, run.resumeState, {
212
- finalization: { ...handle.finalization, validation: 'done', lastError: undefined },
213
- validationRun: undefined,
214
- validatedInputFingerprint: run.fingerprint,
215
- failureReason: undefined,
216
- });
217
- projectWorkValidationOutcome(controllerHome, next, 'passed', 'All requested validation receipts passed.');
218
- return { handle: next, changed: true, outcome: 'passed' };
219
- }
1
+ export * from '../../control-plane/execution/work-validation-reconciler';
@@ -383,6 +383,11 @@ function loadConfig(repoRoot: string): BrowserPluginConfig {
383
383
  return normalizeConfig(readJson<Partial<BrowserPluginConfig>>(configPath(repoRoot)) ?? {});
384
384
  }
385
385
 
386
+ export function readBrowserPluginConfiguration(repoRoot: string): { enabled: boolean; allowedDomains: string[] } {
387
+ const config = loadConfig(repoRoot);
388
+ return { enabled: config.enabled, allowedDomains: [...(config.allowedDomains ?? [])] };
389
+ }
390
+
386
391
  function saveConfig(repoRoot: string, patch: Partial<BrowserPluginConfig>): BrowserPluginConfig {
387
392
  const next = normalizeConfig({ ...loadConfig(repoRoot), ...patch });
388
393
  writeJson(configPath(repoRoot), next);
@@ -384,7 +384,7 @@ export function createDesktopOperatorRegistrationInput(
384
384
  providerPluginId: 'desktop_operator',
385
385
  displayName: 'Forge Desktop Operator',
386
386
  provider: 'local-macos',
387
- pluginVersion: options.pluginVersion ?? '0.1.0',
387
+ pluginVersion: options.pluginVersion ?? '0.2.1',
388
388
  protocolVersion: options.protocolVersion ?? '1.0',
389
389
  scope: 'controller',
390
390
  enabled: options.enabled !== false,
@@ -265,13 +265,21 @@ function writeRegistry(controllerHome: string, repoId: string, manifests: Assist
265
265
  return index;
266
266
  }
267
267
 
268
- function mapResourceClaims(action: AssistantPluginActionDescriptor, repository: RepositoryRecord): ResourceClaimSpec[] {
268
+ export function claimsForAssistantPluginAction(
269
+ action: AssistantPluginActionDescriptor,
270
+ repository: RepositoryRecord,
271
+ trustedPluginId: string,
272
+ ): ResourceClaimSpec[] {
269
273
  const controllerScoped = repository.repoId === CONTROLLER_SCOPE_REPO_ID;
274
+ const pluginId = trustedPluginId.trim().replace(/[^a-zA-Z0-9._-]+/g, '-');
275
+ if (!pluginId) throw new Error('PLUGIN_ID_REQUIRED_FOR_RESOURCE_CLAIMS');
270
276
  return action.resourceClaims.map((claim) => ({
271
- resourceKey: controllerScoped
272
- ? `controller-system:${claim.resource}`
273
- : claim.resource === 'remote'
274
- ? `remote:${repository.repoId}`
277
+ resourceKey: claim.resource === 'remote' || claim.resource === 'provider-state'
278
+ // The manifest/plugin id was resolved from the trusted registration; a
279
+ // provider never supplies arbitrary resource-key text.
280
+ ? `provider-state:${pluginId}`
281
+ : controllerScoped
282
+ ? `controller-system:${claim.resource}`
275
283
  : claim.resource === 'workspace'
276
284
  ? `workspace:${repository.activeCheckoutId}`
277
285
  : claim.resource === 'git-refs'
@@ -764,7 +772,7 @@ export async function submitAssistantPluginAction(
764
772
  mode: 'mutating',
765
773
  idempotent: action.idempotent,
766
774
  replayable: action.idempotent,
767
- resourceClaims: mapResourceClaims(action, repository),
775
+ resourceClaims: claimsForAssistantPluginAction(action, repository, manifest.pluginId),
768
776
  },
769
777
  objective: `Execute bounded controller-local effect ${request.pluginId}/${request.actionId}`,
770
778
  mode: 'direct_control',
@@ -4,7 +4,7 @@ export type AssistantPluginLifecycleState = 'enabled' | 'disabled' | 'degraded'
4
4
  export type AssistantPluginHealthState = 'ready' | 'disabled' | 'degraded' | 'error';
5
5
  export type AssistantPluginActionRisk = 'readonly' | 'workspace_write' | 'remote_write' | 'destructive';
6
6
  export type AssistantPluginActionConfirmation = 'none' | 'authorization' | 'strong_confirmation';
7
- export type AssistantPluginResource = 'repo-state' | 'workspace' | 'remote' | 'git-refs';
7
+ export type AssistantPluginResource = 'repo-state' | 'workspace' | 'remote' | 'git-refs' | 'provider-state';
8
8
 
9
9
  export interface AssistantPluginPermissionScope {
10
10
  scope: string;
@@ -22,7 +22,6 @@ import {
22
22
  type ProjectionRefreshOwner,
23
23
  } from './invalidation';
24
24
  import { readRepositoryGitStatusSample } from './git-status-sampler';
25
- import { listCampaigns } from '../workflow/campaigns/store';
26
25
  import { listAssistantPluginManifests } from '../plugins/store';
27
26
  import type { ProjectionObservation, ProjectionSourceReconciliation } from '../health';
28
27
  import { RUNTIME_HEALTH_THRESHOLDS } from '../health/evaluator';
@@ -60,12 +59,6 @@ export interface RepositoryRuntimeProjection {
60
59
  degraded: number;
61
60
  error: number;
62
61
  };
63
- campaigns?: {
64
- active: number;
65
- waitingForSupervisor: number;
66
- pendingReviews: number;
67
- readyForHumanAcceptance: number;
68
- };
69
62
  }
70
63
 
71
64
  interface DirtyProjectionReadCacheEntry {
@@ -112,12 +105,6 @@ function emptyProjection(repoId: string, reason?: string): RepositoryRuntimeProj
112
105
  degraded: 0,
113
106
  error: 0,
114
107
  },
115
- campaigns: {
116
- active: 0,
117
- waitingForSupervisor: 0,
118
- pendingReviews: 0,
119
- readyForHumanAcceptance: 0,
120
- },
121
108
  };
122
109
  }
123
110
 
@@ -191,7 +178,7 @@ function projectionWithExecutionIndexOverlay(
191
178
  }
192
179
 
193
180
  function dirtyReasonImpliesActiveRisk(reason: string | undefined): boolean {
194
- return Boolean(reason && /^(job:|leases-|campaign:|schedule:|worker:|process:|cleanup:)/.test(reason));
181
+ return Boolean(reason && /^(job:|leases-|schedule:|worker:|process:|cleanup:)/.test(reason));
195
182
  }
196
183
 
197
184
  function buildRepositoryProjection(
@@ -210,8 +197,6 @@ function buildRepositoryProjection(
210
197
  // Terminal attention records remain in history for diagnosis and audit, but only
211
198
  // active/unresolved records should influence "current readiness" decisions.
212
199
  const currentAttentionJobs = attentionJobs.filter((job) => !job.finishedAt || activeJobIds.has(job.jobId));
213
- const campaigns = listCampaigns(controllerHome, repoId, 1_000);
214
- const reviewableCampaigns = campaigns.filter((campaign) => ['active', 'waiting_for_supervisor'].includes(campaign.status));
215
200
  const repository = listRepositories(controllerHome).find((entry) => entry.repoId === repoId);
216
201
  const plugins = repository ? listAssistantPluginManifests(controllerHome, repository, {
217
202
  preferStored: true,
@@ -251,12 +236,6 @@ function buildRepositoryProjection(
251
236
  degraded: plugins.filter((plugin) => plugin.health.state === 'degraded').length,
252
237
  error: plugins.filter((plugin) => plugin.health.state === 'error').length,
253
238
  },
254
- campaigns: {
255
- active: campaigns.filter((campaign) => campaign.status === 'active').length,
256
- waitingForSupervisor: campaigns.filter((campaign) => campaign.status === 'waiting_for_supervisor').length,
257
- pendingReviews: reviewableCampaigns.reduce((count, campaign) => count + campaign.checkpoints.filter((checkpoint) => checkpoint.status === 'open').length, 0),
258
- readyForHumanAcceptance: campaigns.filter((campaign) => campaign.status === 'ready_for_human_acceptance').length,
259
- },
260
239
  };
261
240
  projection.metadata = {
262
241
  ...projection.metadata!,
@@ -270,7 +249,6 @@ function buildRepositoryProjection(
270
249
  currentAttention: projection.currentAttention,
271
250
  attention: projection.attention,
272
251
  plugins: projection.plugins,
273
- campaigns: projection.campaigns,
274
252
  })).digest('hex'),
275
253
  };
276
254
  return projection;
@@ -19,6 +19,7 @@ import {
19
19
  type RuntimeStorageRepairApplyResult,
20
20
  type RuntimeStorageRepairPreview,
21
21
  } from './local-jobs-repair';
22
+ import { gcTerminalProcesses, type ProcessGcResult } from '../execution/process-runtime/gc';
22
23
 
23
24
  export type RuntimeMaintenanceActionId =
24
25
  | 'local_jobs_reconcile'
@@ -118,6 +119,8 @@ export interface RuntimeMaintenanceApplyResult extends Omit<RuntimeMaintenanceSt
118
119
  actionId: RuntimeMaintenanceActionId;
119
120
  applied: Array<RuntimeMaintenanceCandidate & { applied: boolean; result?: string; error?: string }>;
120
121
  runtimeStorageRepairApply?: RuntimeStorageRepairApplyResult;
122
+ /** Existing bounded terminal-process retention, run only during explicit full maintenance. */
123
+ processGc?: ProcessGcResult;
121
124
  projection?: unknown;
122
125
  }
123
126
 
@@ -801,6 +804,11 @@ export function applyRuntimeMaintenance(
801
804
  return { ...candidate, applied: false, error: error instanceof Error ? error.message : String(error) };
802
805
  }
803
806
  });
807
+ // Reuse the existing Process GC within the explicit full-maintenance cadence.
808
+ // No scheduler, per-tool-call cleanup, or database compaction is introduced.
809
+ const processGc = options.actionId === 'full_maintenance_pass'
810
+ ? gcTerminalProcesses({ controllerHome, repoId: repository.repoId })
811
+ : undefined;
804
812
 
805
813
  if (options.actionId === 'runtime_storage_finalize_relocation' || options.actionId === 'full_maintenance_pass' || applied.some((candidate) => candidate.applied) || runtimeStorageRepairApply) {
806
814
  rebuildActiveIndex(repository.canonicalRoot);
@@ -817,6 +825,7 @@ export function applyRuntimeMaintenance(
817
825
  runtimeStorageError: after.runtimeStorageError ?? storage.error,
818
826
  applied,
819
827
  runtimeStorageRepairApply,
828
+ processGc,
820
829
  projection,
821
830
  };
822
831
  }
@@ -1,7 +1,6 @@
1
1
  export type ResourceOwnerKind =
2
2
  | 'execution_job'
3
3
  | 'agent_run'
4
- | 'campaign'
5
4
  | 'work_contract'
6
5
  | 'edit_session'
7
6
  | 'controller_generation'
@@ -8,6 +8,7 @@ export interface RuntimeGatewayServerOptions {
8
8
  runtimeInstanceId: string;
9
9
  runtimeSourceRoot: string;
10
10
  sessionId?: string;
11
+ controllerType?: 'chatgpt' | 'codex' | 'claude' | 'grok';
11
12
  }
12
13
 
13
14
  /**
@@ -28,6 +29,7 @@ export function createRuntimeGatewayServer(
28
29
  profile: 'controller',
29
30
  principalId,
30
31
  sessionId: options.sessionId,
32
+ controllerType: options.controllerType,
31
33
  controllerInstanceId: options.runtimeInstanceId,
32
34
  runtimeSourceRoot: options.runtimeSourceRoot,
33
35
  });
@@ -17,12 +17,14 @@ export interface RuntimeMcpTransportHandle {
17
17
  close(): Promise<void>;
18
18
  }
19
19
 
20
+ export type ForwardedControllerType = 'chatgpt' | 'codex' | 'claude' | 'grok';
21
+
20
22
  export interface StartRuntimeMcpTransportOptions {
21
23
  host: string;
22
24
  port: number;
23
25
  authToken: string;
24
26
  readiness: () => RuntimeReadiness;
25
- createServer: (principalId: string, sessionId?: string) => Server;
27
+ createServer: (principalId: string, sessionId?: string, controllerType?: ForwardedControllerType) => Server;
26
28
  onFatal?: (error: Error) => void;
27
29
  }
28
30
 
@@ -99,8 +101,14 @@ export async function startRuntimeMcpTransport(
99
101
  const forwardedSessionId = typeof req.headers['x-forge-forwarded-session-id'] === 'string'
100
102
  ? req.headers['x-forge-forwarded-session-id'].trim().slice(0, 512)
101
103
  : '';
104
+ const forwardedControllerTypeRaw = typeof req.headers['x-forge-forwarded-controller-type'] === 'string'
105
+ ? req.headers['x-forge-forwarded-controller-type'].trim().toLowerCase()
106
+ : '';
107
+ const forwardedControllerType = ['chatgpt', 'codex', 'claude', 'grok'].includes(forwardedControllerTypeRaw)
108
+ ? forwardedControllerTypeRaw as ForwardedControllerType
109
+ : undefined;
102
110
  const principal = forwardedPrincipalId || principalId(options.authToken);
103
- const server = options.createServer(principal, forwardedSessionId || undefined);
111
+ const server = options.createServer(principal, forwardedSessionId || undefined, forwardedControllerType);
104
112
  transport = new StreamableHTTPServerTransport({
105
113
  sessionIdGenerator: () => randomUUID(),
106
114
  onsessioninitialized: (createdSessionId) => {
@@ -0,0 +1,136 @@
1
+ import { createHash } from 'crypto';
2
+ import { closeSync, mkdirSync, openSync, renameSync, writeFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { basename, dirname, join, resolve } from 'path';
5
+ import { spawn, spawnSync } from 'child_process';
6
+ import { bootstrapLaunchAgentWithRetryV2, installLaunchAgent, launchAgentPath } from '../../cli/controller/launch-agents';
7
+ import type { PackageRuntimeRelease } from './package-runtime-release';
8
+
9
+ export interface PackageConnectorServicePaths {
10
+ label: string;
11
+ serviceRoot: string;
12
+ sourcePlistPath: string;
13
+ installedPlistPath: string;
14
+ stdoutPath: string;
15
+ stderrPath: string;
16
+ }
17
+
18
+ export interface PackageConnectorServiceResult {
19
+ endpoint: string;
20
+ mode: 'launchd' | 'systemd-user' | 'portable';
21
+ persistent: boolean;
22
+ servicePath?: string;
23
+ pid?: number;
24
+ }
25
+
26
+ function xml(value: string): string {
27
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
28
+ }
29
+
30
+ function atomicWrite(path: string, content: string, mode = 0o600): void {
31
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
32
+ const temporary = `${path}.${process.pid}.tmp`;
33
+ writeFileSync(temporary, content, { encoding: 'utf8', mode });
34
+ renameSync(temporary, path);
35
+ }
36
+
37
+ export function packageConnectorServicePaths(controllerHome: string, accountHome = process.env.HOME ?? homedir()): PackageConnectorServicePaths {
38
+ const home = resolve(controllerHome);
39
+ const suffix = createHash('sha256').update(home).digest('hex').slice(0, 12);
40
+ const label = `com.moretea.forge.mcp-gateway.${suffix}`;
41
+ const serviceRoot = join(home, 'runtime', 'connector-service');
42
+ return {
43
+ label,
44
+ serviceRoot,
45
+ sourcePlistPath: join(serviceRoot, `${label}.plist`),
46
+ installedPlistPath: launchAgentPath(label, accountHome),
47
+ stdoutPath: join(serviceRoot, 'logs', 'stdout.log'),
48
+ stderrPath: join(serviceRoot, 'logs', 'stderr.log'),
49
+ };
50
+ }
51
+
52
+ export function packageConnectorLaunchSpec(input: { release: PackageRuntimeRelease; controllerHome: string; endpoint: string; executable?: string }): { executable: string; args: string[]; environment: Record<string, string>; port: number } {
53
+ const parsed = new URL(input.endpoint);
54
+ const port = Number(parsed.port);
55
+ if (parsed.protocol !== 'http:' || parsed.hostname !== '127.0.0.1' || parsed.pathname !== '/mcp' || !Number.isInteger(port) || port < 1 || port > 65_535) {
56
+ throw new Error('FORGE_PACKAGE_CONNECTOR_ENDPOINT_INVALID');
57
+ }
58
+ const executable = resolve(input.executable ?? process.execPath);
59
+ const packageRoot = resolve(input.release.packageRoot);
60
+ const cliEntry = join(packageRoot, 'src', 'cli', 'index.ts');
61
+ const nodeLoader = join(packageRoot, 'src', 'runtime', 'shared', 'node-ts-loader.mjs');
62
+ const isBun = Boolean(process.versions.bun) || /(?:^|[/\\-])bun(?:$|[/\\]|\.exe$)/i.test(basename(executable));
63
+ const cliArgs = [
64
+ 'mcp', 'serve', '--repo', packageRoot, '--controller-home', resolve(input.controllerHome),
65
+ '--transport', 'http', '--host', '127.0.0.1', '--port', String(port), '--profile', 'controller', '--auth', 'oauth',
66
+ ];
67
+ return {
68
+ executable,
69
+ args: isBun ? [cliEntry, ...cliArgs] : ['--loader', nodeLoader, cliEntry, ...cliArgs],
70
+ environment: { FORGE_CONTROLLER_HOME: resolve(input.controllerHome), FORGE_CONTROLLER_LIFECYCLE_OWNER: '1' },
71
+ port,
72
+ };
73
+ }
74
+
75
+ export function renderPackageConnectorLaunchAgent(input: { paths: PackageConnectorServicePaths; launch: ReturnType<typeof packageConnectorLaunchSpec> }): string {
76
+ const environmentXml = Object.entries(input.launch.environment).sort(([a], [b]) => a.localeCompare(b))
77
+ .map(([key, value]) => ` <key>${xml(key)}</key>\n <string>${xml(value)}</string>`).join('\n');
78
+ const args = [input.launch.executable, ...input.launch.args];
79
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key>\n <string>${xml(input.paths.label)}</string>\n <key>ProgramArguments</key>\n <array>\n${args.map((arg) => ` <string>${xml(arg)}</string>`).join('\n')}\n </array>\n <key>EnvironmentVariables</key>\n <dict>\n${environmentXml}\n </dict>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <dict><key>SuccessfulExit</key><false/></dict>\n <key>ThrottleInterval</key><integer>5</integer>\n <key>StandardOutPath</key><string>${xml(input.paths.stdoutPath)}</string>\n <key>StandardErrorPath</key><string>${xml(input.paths.stderrPath)}</string>\n</dict>\n</plist>\n`;
80
+ }
81
+
82
+ export function renderPackageConnectorSystemdUserUnit(input: { launch: ReturnType<typeof packageConnectorLaunchSpec> }): string {
83
+ const quote = (value: string) => `"${value.replaceAll('%', '%%').replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
84
+ const env = Object.entries(input.launch.environment).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `Environment=${quote(`${k}=${v}`)}`);
85
+ return ['[Unit]', 'Description=Forge ChatGPT OAuth Gateway', 'After=network-online.target', '', '[Service]', 'Type=simple', `ExecStart=${[input.launch.executable, ...input.launch.args].map(quote).join(' ')}`, ...env, 'Restart=on-failure', 'RestartSec=5', '', '[Install]', 'WantedBy=default.target', ''].join('\n');
86
+ }
87
+
88
+ function installSystemd(paths: PackageConnectorServicePaths, launch: ReturnType<typeof packageConnectorLaunchSpec>, env: NodeJS.ProcessEnv): string {
89
+ const unitName = `${paths.label}.service`;
90
+ const unitPath = join(env.HOME ?? homedir(), '.config', 'systemd', 'user', unitName);
91
+ atomicWrite(unitPath, renderPackageConnectorSystemdUserUnit({ launch }), 0o644);
92
+ for (const args of [['--user', 'daemon-reload'], ['--user', 'enable', '--now', unitName]]) {
93
+ const result = spawnSync('systemctl', args, { encoding: 'utf8', env, timeout: 30_000 });
94
+ if (result.status !== 0) throw new Error(`FORGE_PACKAGE_CONNECTOR_SYSTEMD_INSTALL_FAILED: ${(result.stderr || result.stdout || '').trim()}`);
95
+ }
96
+ return unitPath;
97
+ }
98
+
99
+ function startPortable(paths: PackageConnectorServicePaths, launch: ReturnType<typeof packageConnectorLaunchSpec>, env: NodeJS.ProcessEnv): number {
100
+ mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
101
+ const stdout = openSync(paths.stdoutPath, 'a', 0o600), stderr = openSync(paths.stderrPath, 'a', 0o600);
102
+ // A connector is not the Canonical Runtime writer. Never let a transient
103
+ // installer/worker write claim escape into this long-lived process; the claim
104
+ // would become stale as soon as its parent exits. Keep only the explicit
105
+ // Controller Home and lifecycle-role marker from launch.environment.
106
+ const childEnv = { ...env };
107
+ for (const key of [
108
+ 'FORGE_RUNTIME_INSTANCE_ID', 'FORGE_RUNTIME_OWNER_PID', 'FORGE_RELEASE_AUTHORITY_REVISION',
109
+ 'FORGE_RELEASE_FENCING_TOKEN', 'FORGE_RELEASE_ID', 'FORGE_ARTIFACT_IDENTITY', 'FORGE_WORKER_PROTOCOL_VERSION',
110
+ ]) delete childEnv[key];
111
+ try {
112
+ const child = spawn(launch.executable, launch.args, { detached: true, stdio: ['ignore', stdout, stderr], env: { ...childEnv, ...launch.environment } });
113
+ if (!child.pid) throw new Error('FORGE_PACKAGE_CONNECTOR_START_FAILED');
114
+ child.unref();
115
+ return child.pid;
116
+ } finally { closeSync(stdout); closeSync(stderr); }
117
+ }
118
+
119
+ export async function installPackageConnectorService(input: { release: PackageRuntimeRelease; controllerHome: string; endpoint: string; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; forcePortable?: boolean }): Promise<PackageConnectorServiceResult> {
120
+ const paths = packageConnectorServicePaths(input.controllerHome, input.env?.HOME);
121
+ mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
122
+ const launch = packageConnectorLaunchSpec({ release: input.release, controllerHome: input.controllerHome, endpoint: input.endpoint });
123
+ const platform = input.platform ?? process.platform, env = input.env ?? process.env;
124
+ if (!input.forcePortable && platform === 'darwin') {
125
+ atomicWrite(paths.sourcePlistPath, renderPackageConnectorLaunchAgent({ paths, launch }), 0o600);
126
+ installLaunchAgent(paths.sourcePlistPath, paths.label);
127
+ const result = await bootstrapLaunchAgentWithRetryV2({ label: paths.label, plistPath: paths.installedPlistPath });
128
+ if (!result.ok) throw new Error(`FORGE_PACKAGE_CONNECTOR_LAUNCHD_INSTALL_FAILED: ${result.diagnostics.join('; ')}`);
129
+ return { endpoint: input.endpoint, mode: 'launchd', persistent: true, servicePath: paths.installedPlistPath };
130
+ }
131
+ if (!input.forcePortable && platform === 'linux') {
132
+ const probe = spawnSync('systemctl', ['--user', 'show-environment'], { encoding: 'utf8', env, timeout: 30_000 });
133
+ if (probe.status === 0) return { endpoint: input.endpoint, mode: 'systemd-user', persistent: true, servicePath: installSystemd(paths, launch, env) };
134
+ }
135
+ return { endpoint: input.endpoint, mode: 'portable', persistent: false, pid: startPortable(paths, launch, env) };
136
+ }