@moretea-labs/forge 1.5.1 → 1.6.0

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 (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/DESIGN.md +7 -2
  3. package/README.md +4 -4
  4. package/README.zh-CN.md +2 -2
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/assets/hooks/pre-edit-guard.sh +8 -39
  7. package/assets/hooks/prompt-guard.sh +2 -2
  8. package/assets/plugin-registry.v1.json +54 -3
  9. package/assets/reference-configs/hook-operations.md +1 -1
  10. package/assets/reference-configs/sprint-contracts.md +2 -2
  11. package/assets/skill-version.json +2 -2
  12. package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
  13. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  14. package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
  15. package/docs/architecture/current/codegraph-context-provider.md +15 -3
  16. package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
  17. package/docs/architecture/current/entity-model.md +20 -0
  18. package/docs/architecture/current/external-plugin-governance.md +3 -3
  19. package/docs/architecture/current/system-overview.md +26 -1
  20. package/docs/architecture/current/thin-harness-v1.md +10 -0
  21. package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
  22. package/docs/operations/controller-browser-plugin.md +12 -16
  23. package/docs/operations/features.md +1 -1
  24. package/docs/operations/homebrew.md +2 -2
  25. package/docs/tutorials/01-install-and-start.md +1 -1
  26. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  27. package/docs/wiki/Installation.md +1 -1
  28. package/docs/wiki/Quick-Start.md +1 -1
  29. package/package.json +1 -1
  30. package/scripts/check-public-docs.sh +14 -1
  31. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  32. package/scripts/sprint-backlog.sh +1 -1
  33. package/src/cli/commands/chatgpt.ts +31 -2
  34. package/src/cli/commands/plugin.ts +36 -9
  35. package/src/cli/controller/context-pack.ts +158 -6
  36. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  37. package/src/cli/hook/prompt-intents.ts +1 -1
  38. package/src/cli/local-bridge/console-automations.ts +205 -23
  39. package/src/cli/local-bridge/console-view-models.ts +2 -0
  40. package/src/cli/local-bridge/facade-api.ts +2 -0
  41. package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
  42. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
  43. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
  44. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
  45. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  46. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  47. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
  48. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  49. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
  50. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  51. package/src/cli/local-bridge/frontend/src/types.ts +6 -2
  52. package/src/cli/local-bridge/server.ts +29 -19
  53. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  54. package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
  55. package/src/cli/mcp/paths.ts +8 -2
  56. package/src/cli/mcp/repository-tools.ts +124 -9
  57. package/src/cli/mcp/server.ts +164 -19
  58. package/src/cli/mcp/transports/http.ts +25 -2
  59. package/src/cli/mcp/transports/session-registry.ts +24 -0
  60. package/src/cli/repositories/command-classifier.ts +113 -0
  61. package/src/cli/repositories/safe-patch.ts +6 -4
  62. package/src/cli/repositories/structured-git.ts +49 -7
  63. package/src/cli/repository/inspector.ts +16 -3
  64. package/src/runtime/assistant/intent.ts +3 -3
  65. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
  66. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  67. package/src/runtime/control-plane/execution/validation.ts +15 -2
  68. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
  69. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
  70. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
  71. package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
  72. package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
  73. package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
  74. package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
  75. package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
  76. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  77. package/src/runtime/control-plane/facade/types.ts +33 -6
  78. package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
  79. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
  80. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  81. package/src/runtime/diagnostics/mcp-timing.ts +6 -0
  82. package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
  83. package/src/runtime/execution/process-runtime/check-result.ts +10 -2
  84. package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
  85. package/src/runtime/execution/process-runtime/gc.ts +64 -10
  86. package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
  87. package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
  88. package/src/runtime/execution/process-runtime/runtime.ts +16 -1
  89. package/src/runtime/execution/process-runtime/store.ts +6 -0
  90. package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
  91. package/src/runtime/gateway/mcp/router.ts +1 -1
  92. package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
  93. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  94. package/src/runtime/plugins/browser-adapter.ts +19 -72
  95. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  96. package/src/runtime/plugins/browser-handoff.ts +0 -3
  97. package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
  98. package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
  99. package/src/runtime/plugins/external-registration.ts +84 -5
  100. package/src/runtime/plugins/first-party-registry.ts +2 -2
  101. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  102. package/src/runtime/plugins/local-system-adapter.ts +2 -2
  103. package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
  104. package/src/runtime/plugins/store.ts +107 -5
  105. package/src/runtime/plugins/types.ts +4 -0
  106. package/src/runtime/recovery/actions.ts +0 -10
  107. package/src/runtime/recovery/classifier.ts +0 -9
  108. package/src/runtime/recovery/maintenance-executor.ts +43 -11
  109. package/src/runtime/recovery/probe.ts +0 -1
  110. package/src/runtime/recovery/types.ts +0 -1
  111. package/src/runtime/root/release-materialize.ts +143 -19
  112. package/src/runtime/root/service.ts +6 -4
  113. package/src/runtime/safe-tooling/index.ts +0 -1
  114. package/src/runtime/safe-tooling/types.ts +0 -30
  115. package/src/runtime/standalone-recovery/core.ts +2 -2
  116. package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
  117. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  118. package/src/runtime/workflow/schedules/engine.ts +357 -64
  119. package/src/runtime/workflow/schedules/types.ts +5 -0
  120. package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
  121. package/bin/forge-desktop-helper.mjs +0 -204
  122. package/docs/operations/controller-desktop-plugin.md +0 -112
  123. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  124. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
@@ -1,8 +1,9 @@
1
1
  import { createHash } from 'crypto';
2
- import { existsSync } from 'fs';
3
- import { basename } from 'path';
2
+ import { existsSync, readFileSync, statSync } from 'fs';
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'path';
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
6
+ import type { CallToolResult as SdkCallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
7
  import { collectRuntimePerformanceDiagnostics, inferLocalControllerProcess } from '../../diagnostics/performance';
7
8
  import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tools';
8
9
  import type { MultiRepositoryMcpToolContext } from '../../../cli/mcp/multi-repository';
@@ -14,11 +15,11 @@ import { repositoryControllerRoot } from '../../../cli/repositories/controller-h
14
15
  import { cancelExecutionJob, findExecutionJob, getExecutionJob, getExecutionJobByRequestId, listExecutionJobs } from '../../execution/jobs/store';
15
16
  import { waitForExecutionJob } from '../../execution/jobs/wait';
16
17
  import type { ExecutionJob } from '../../execution/jobs/types';
17
- import { getProcessHandle, getProcessRecord, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
18
+ import { getProcessHandle, getProcessRecord, isManagedProcessActive, listProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
18
19
  import { buildJobOperationDigest } from '../../control-plane/facade/operation-digest';
19
- import { readWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
20
+ import { readWorkHandle, transitionWorkHandle, writeWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
20
21
  import { executionIdentityForRepository } from '../../control-plane/execution/execution-identity';
21
- import { reconcileFinalizedDirectEditWorksAfterCommit } from '../../control-plane/execution/direct-edit-work-completion';
22
+ import { acceptReviewedDirectEditWorkReconciliation, reconcileFinalizedDirectEditWorksAfterCommit } from '../../control-plane/execution/direct-edit-work-completion';
22
23
  import { readJobEvents } from '../../evidence/event-ledger';
23
24
  import { readExecutionArtifact } from '../../evidence/artifact-store';
24
25
  import { readExecutionEvidence } from '../../evidence/evidence-store';
@@ -44,11 +45,6 @@ import { applyScheduleDedupe, buildScheduleDedupeReport, createSchedule, getSche
44
45
  import { evaluateSchedule } from '../../workflow/schedules/engine';
45
46
  import {
46
47
  createWorkContinuationSchedule,
47
- getWorkContinuationSchedule,
48
- listWorkContinuationSchedules,
49
- pauseWorkContinuationSchedule,
50
- resumeWorkContinuationSchedule,
51
- triggerWorkContinuationSchedule,
52
48
  type ContinuationControllerType,
53
49
  } from '../../workflow/schedules/work-continuation';
54
50
  import { createPortfolioWorkflow, getPortfolioWorkflow, listPortfolioWorkflows } from '../../workflow/portfolio/store';
@@ -109,10 +105,6 @@ import { resolveLocalBridgeSurface, summarizeRecentJobs } from '../../shared/loc
109
105
  import { assistantPluginScope, controllerPluginRepository, executeAssistantPluginReadDirect, getAssistantPluginManifest, isDirectPluginReadAction, listAssistantPluginManifests, submitAssistantPluginAction } from '../../plugins/store';
110
106
  import { mcpPluginExecutionOrigin } from '../../plugins/execution-origin';
111
107
  import {
112
- listWebTargets,
113
- mergeAllowedDomains,
114
- previewBrowserDomainAccess,
115
- resolveWebTargetUrl,
116
108
  summarizeExecutionJobForMcp,
117
109
  summarizeJobResultForLowInterception,
118
110
  summarizePluginForLowInterception,
@@ -205,7 +197,9 @@ import {
205
197
  releaseControllerSession,
206
198
  resumeControllerSession,
207
199
  } from '../../control-plane/facade';
208
- import { currentControllerInstanceId, startExecutionSession, updateExecutionSession } from '../../control-plane/execution/session-store';
200
+ import { currentControllerInstanceId, readExecutionSession, startExecutionSession, updateExecutionSession } from '../../control-plane/execution/session-store';
201
+ import { ensureManagedWorkspace } from '../../execution/managed-workspace';
202
+ import { currentPermissionSnapshotVersion } from '../../control-plane/execution/validation';
209
203
  import { observeRuntimeStatus } from '../../root/status';
210
204
  import { reconcileWorkValidation } from './work-validation-reconciler';
211
205
  import { callExecutionTool } from './execution-tools';
@@ -296,9 +290,15 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
296
290
  }),
297
291
  definition('rh_work', 'Preferred ChatGPT facade: bounded planning, direct control, controller ownership, and external SuperController launch.', {
298
292
  repo_id: repoId,
299
- operation: { type: 'string', enum: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'], description: 'Defaults to start. Complex starts require plan_id and plan_step_id. schedule_* manages bounded external-controller continuation for existing Work.' },
293
+ operation: { type: 'string', enum: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'], description: 'Defaults to start. Complex starts require plan_id and plan_step_id. schedule_* manages generic recurring ChatGPT workflow schedules plus Work-bound continuation/browser subtypes.' },
300
294
  objective: { type: 'string' },
301
295
  work_id: { type: 'string' },
296
+ related_work_id: { type: 'string', description: 'Optional explicit existing Work ownership target for continue/extend semantics. Unrelated active Work is never inferred as a semantic target.' },
297
+ work_relation: { type: 'string', enum: ['continue', 'extend', 'parallel', 'new_goal'], description: 'Optional semantic relation to an explicitly related Work. This field never selects Direct/Process/Durable execution depth; checkout conflicts are resolved by placement.' },
298
+ related_plan_id: { type: 'string', description: 'Existing active Plan selected when resolving another slice under the same Requirement.' },
299
+ plan_relation: { type: 'string', enum: ['extend', 'parallel'], description: 'Relationship to another active Plan under the same Requirement. Omit to receive candidates first. parallel explicitly permits a distinct Plan slice; extend reuses the existing Plan authority and requires replanning/supersession instead of creating another draft.' },
300
+ requested_by: { type: 'string', enum: ['chatgpt', 'user', 'system', 'scheduler'], description: 'Origin of the request. Scheduler-origin starts are continuation-only and cannot create new durable Work.' },
301
+ requirement_id: { type: 'string', description: 'Stable Requirement authority used to find related active Work before creation.' },
302
302
  handoff_id: { type: 'string', description: 'Optional existing HandoffItem used to resume controller context.' },
303
303
  controller_id: { type: 'string', description: 'Stable external SuperController identity for controller_claim/release.' },
304
304
  controller_type: { type: 'string', enum: ['chatgpt', 'codex', 'grok', 'claude', 'human'], description: 'Controller kind for claims and launcher_start.' },
@@ -309,10 +309,23 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
309
309
  launch_args: { type: 'array', items: { type: 'string' }, description: 'Provider-specific arguments for launcher_start.' },
310
310
  browser_session_id: { type: 'string', description: 'Saved Forge ChatGPT browser session to continue when controller_type=chatgpt.' },
311
311
  conversation_url: { type: 'string', description: 'Explicit https://chatgpt.com conversation URL used when no saved Forge browser session exists.' },
312
- continuation_prompt: { type: 'string', description: 'Additional bounded continuation instruction appended to the Work/Handoff prompt.' },
312
+ continuation_prompt: { type: 'string', description: 'Bounded Work continuation instruction, or the recurring ChatGPT prompt when schedule_mode=workflow and objective is omitted.' },
313
313
  schedule_id: { type: 'string', description: 'Schedule identity for schedule_get/pause/resume/trigger.' },
314
- schedule_name: { type: 'string', description: 'Human-readable schedule name; defaults to Continue Work <work_id>.' },
314
+ schedule_name: { type: 'string', description: 'Human-readable schedule name; generic workflow schedules use the objective/prompt when omitted.' },
315
315
  schedule_request_id: { type: 'string', description: 'Stable idempotency key for schedule_create.' },
316
+ schedule_mode: { type: 'string', enum: ['workflow', 'continuation', 'browser_watch', 'browser_keepalive'], description: 'Defaults to workflow when work_id is omitted and continuation when work_id is present. workflow dispatches one bounded ChatGPT prompt per finite Occurrence without requiring Work; continuation/browser modes remain Work-bound.' },
317
+ probe_url: { type: 'string', description: 'Allowed URL to navigate or refresh for browser_watch/browser_keepalive.' },
318
+ probe_browser_session_id: { type: 'string', description: 'Existing Forge browser session used as the external observation target.' },
319
+ probe_selector: { type: 'string', description: 'Optional selector limiting text extraction for browser_watch.' },
320
+ probe_max_chars: { type: 'number', description: 'Bounded extracted text size for browser_watch.' },
321
+ probe_timeout_ms: { type: 'number', description: 'Bounded browser probe timeout.' },
322
+ include_terms: { type: 'array', items: { type: 'string' }, description: 'Optional case-insensitive terms; only matching normalized lines contribute to the observation fingerprint.' },
323
+ ignore_patterns: { type: 'array', items: { type: 'string' }, description: 'Optional regex patterns stripped before fingerprinting volatile text such as relative timestamps.' },
324
+ login_url_terms: { type: 'array', items: { type: 'string' }, description: 'Configured URL markers that classify the browser observation as authentication-required.' },
325
+ login_text_terms: { type: 'array', items: { type: 'string' }, description: 'Configured page-text markers that classify the browser observation as authentication-required.' },
326
+ wake_on_first_observation: { type: 'boolean', description: 'Defaults false so the first browser_watch occurrence records a silent baseline.' },
327
+ wake_on_auth_required: { type: 'boolean', description: 'Defaults true for Work-bound browser schedules.' },
328
+ auth_required_prompt: { type: 'string', description: 'Optional ChatGPT continuation instruction when the probe detects a login marker.' },
316
329
  trigger_type: { type: 'string', enum: ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'] },
317
330
  every_minutes: { type: 'number', description: 'Interval trigger cadence in minutes.' },
318
331
  cron_expression: { type: 'string' },
@@ -383,12 +396,18 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
383
396
  no_ff: { type: 'boolean' },
384
397
  completion_outcome: { type: 'string', enum: ['completed_changed', 'completed_no_change'] },
385
398
  no_change_evidence: { type: 'string', description: 'Optional explicit proof for a no-change completion; otherwise the lifecycle coordinator derives proof only for an unchanged validated HEAD.' },
399
+ reconcile_historical_delivery: { type: 'boolean', description: 'Explicitly request reviewed reconciliation for a historically delivered Direct Edit Work whose original Work binding/receipt is missing.' },
400
+ reconcile_target_revision: { type: 'string', description: 'Exact already-integrated commit accepted by the reviewer.' },
401
+ reconcile_compared_paths: { type: 'array', items: { type: 'string' }, description: 'Complete exact path set changed from the Work base revision to reconcile_target_revision.' },
402
+ reconcile_rationale: { type: 'string', description: 'Bounded reviewer rationale for accepting the historical delivery.' },
403
+ reconcile_cleanup_proof: { type: 'string', description: 'Concrete proof that no owned cleanup remains or that cleanup is complete.' },
386
404
  detail_level: { type: 'string', enum: ['summary', 'detail'] },
387
405
  reason: { type: 'string', description: 'Bounded reason for a work stop or repair record.' },
388
406
  }, [], false),
389
407
  definition('work_submit', 'Submit one durable repository operation and return a resumable Work handle.', {
390
408
  repo_id: repoId,
391
- request_id: { type: 'string', description: 'Stable idempotency key used to resume the same Work after reconnecting.' },
409
+ request_id: { type: 'string', description: 'Stable idempotency key used to resume the same execution-child Work after reconnecting.' },
410
+ parent_work_id: { type: 'string', description: 'Optional active primary Work that owns this low-level execution child. work_submit never creates a second business-workflow authority.' },
392
411
  operation: { type: 'string', description: 'Existing durable controller operation, for example run_check, dispatch_task, or create_issue.' },
393
412
  arguments: { type: 'object', description: 'Arguments passed to the durable operation.' },
394
413
  timeout_ms: { type: 'number' },
@@ -718,6 +737,7 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
718
737
  plugin_id: { type: 'string' },
719
738
  action_id: { type: 'string' },
720
739
  request_id: { type: 'string' },
740
+ work_id: { type: 'string', description: 'Optional existing remote-effect Work that should own the successful typed plugin receipt.' },
721
741
  arguments: { type: 'object' },
722
742
  timeout_ms: { type: 'number' },
723
743
  confirm_authorization: { type: 'boolean' },
@@ -727,34 +747,6 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
727
747
  repo_id: repoId,
728
748
  plugin_id: { type: 'string' },
729
749
  }, ['plugin_id']),
730
- definition('web_targets_list', 'List pre-allowed web targets as domain keys. This avoids arbitrary URL parameters.', {
731
- repo_id: repoId,
732
- }),
733
- definition('web_target_snapshot', 'Create a read-only browser snapshot for a pre-allowed web target by target_key and path. Does not accept arbitrary URLs.', {
734
- repo_id: repoId,
735
- target_key: { type: 'string' },
736
- path: { type: 'string' },
737
- query: { type: 'object' },
738
- capture: { type: 'string', enum: ['title', 'text', 'screenshot'] },
739
- wait_until: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] },
740
- max_chars: { type: 'number' },
741
- full_page: { type: 'boolean' },
742
- request_id: { type: 'string' },
743
- timeout_ms: { type: 'number' },
744
- }, ['target_key', 'request_id'], false),
745
- definition('web_domain_access_preview', 'Preview a browser domain access request without writing plugin config or accepting arbitrary URLs.', {
746
- repo_id: repoId,
747
- domain: { type: 'string' },
748
- reason: { type: 'string' },
749
- }, ['domain']),
750
- definition('web_domain_access_apply', 'Apply a previously previewed browser domain access request through browser configuration with explicit authorization.', {
751
- repo_id: repoId,
752
- domain: { type: 'string' },
753
- reason: { type: 'string' },
754
- preview_ticket_id: { type: 'string' },
755
- request_id: { type: 'string' },
756
- confirm_authorization: { type: 'boolean' },
757
- }, ['domain', 'request_id', 'confirm_authorization'], false),
758
750
  definition('work_result_summary', 'Return a redacted job result summary with failure class and suggested next actions. Does not return raw stdout or stderr.', {
759
751
  repo_id: repoId,
760
752
  job_id: { type: 'string' },
@@ -1123,6 +1115,77 @@ function result(value: Record<string, unknown>, isError = false): CallToolResult
1123
1115
  return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value, ...(isError ? { isError: true } : {}) };
1124
1116
  }
1125
1117
 
1118
+ const MAX_INLINE_PLUGIN_IMAGE_BYTES = 8 * 1024 * 1024;
1119
+ const MAX_INLINE_PLUGIN_IMAGES = 4;
1120
+ const INLINE_PLUGIN_IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
1121
+
1122
+ type McpImageContent = Extract<SdkCallToolResult['content'][number], { type: 'image' }>;
1123
+
1124
+ export function boundedPluginArtifactImageContent(
1125
+ controllerHome: string,
1126
+ repoId: string,
1127
+ pluginResult: Record<string, unknown> | undefined,
1128
+ ): McpImageContent[] {
1129
+ if (!pluginResult) return [];
1130
+ const nestedResult = pluginResult.result && typeof pluginResult.result === 'object' && !Array.isArray(pluginResult.result)
1131
+ ? pluginResult.result as Record<string, unknown>
1132
+ : undefined;
1133
+ const candidates = [
1134
+ ...(Array.isArray(pluginResult.artifactCandidates) ? pluginResult.artifactCandidates : []),
1135
+ ...(nestedResult && Array.isArray(nestedResult.artifactCandidates) ? nestedResult.artifactCandidates : []),
1136
+ ];
1137
+ if (candidates.length === 0) return [];
1138
+ const allowedRoot = resolve(repositoryControllerRoot(controllerHome, repoId));
1139
+ const images: McpImageContent[] = [];
1140
+ const seenPaths = new Set<string>();
1141
+
1142
+ for (const candidate of candidates) {
1143
+ if (images.length >= MAX_INLINE_PLUGIN_IMAGES) break;
1144
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
1145
+ const record = candidate as Record<string, unknown>;
1146
+ const mediaType = typeof record.mediaType === 'string' ? record.mediaType : '';
1147
+ const path = typeof record.path === 'string' ? record.path : '';
1148
+ if (!INLINE_PLUGIN_IMAGE_MEDIA_TYPES.has(mediaType) || !path) continue;
1149
+
1150
+ const resolvedPath = resolve(path);
1151
+ if (seenPaths.has(resolvedPath)) continue;
1152
+ const rel = relative(allowedRoot, resolvedPath);
1153
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
1154
+ if (!existsSync(resolvedPath)) continue;
1155
+
1156
+ try {
1157
+ const stat = statSync(resolvedPath);
1158
+ if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_INLINE_PLUGIN_IMAGE_BYTES) continue;
1159
+ images.push({
1160
+ type: 'image',
1161
+ data: readFileSync(resolvedPath).toString('base64'),
1162
+ mimeType: mediaType,
1163
+ });
1164
+ seenPaths.add(resolvedPath);
1165
+ } catch {
1166
+ // The structured plugin result remains authoritative when an artifact cannot be inlined.
1167
+ }
1168
+ }
1169
+
1170
+ return images;
1171
+ }
1172
+
1173
+ function resultWithPluginArtifactImages(
1174
+ value: Record<string, unknown>,
1175
+ controllerHome: string,
1176
+ repoId: string,
1177
+ pluginResult: Record<string, unknown> | undefined,
1178
+ ): CallToolResult {
1179
+ const images = boundedPluginArtifactImageContent(controllerHome, repoId, pluginResult);
1180
+ const richResult: SdkCallToolResult = {
1181
+ content: [{ type: 'text', text: JSON.stringify(value) }, ...images],
1182
+ structuredContent: value,
1183
+ };
1184
+ // The legacy internal facade intentionally types content as text-first for existing callers.
1185
+ // The MCP wire contract supports additional SDK image blocks after that stable first text block.
1186
+ return richResult as unknown as CallToolResult;
1187
+ }
1188
+
1126
1189
  function repositoryRootForRepoId(controllerHome: string, repoId: string): string | undefined {
1127
1190
  return listRepositories(controllerHome).find((repository) => repository.repoId === repoId)?.canonicalRoot;
1128
1191
  }
@@ -1543,6 +1606,64 @@ function authenticatedFacadeControllerIdentity(
1543
1606
  };
1544
1607
  }
1545
1608
 
1609
+ function ensureFacadeWorkHandle(
1610
+ ctx: MultiRepositoryMcpToolContext,
1611
+ repository: ReturnType<typeof selected>,
1612
+ workId: string,
1613
+ args: Record<string, unknown>,
1614
+ ): WorkHandleState | undefined {
1615
+ const existing = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
1616
+ if (existing) return existing;
1617
+ const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
1618
+ if (!contract || contract.workKind !== 'repository_change' || contract.mode !== 'goal_workloop' || !contract.checkoutId) return undefined;
1619
+ const executionRepository = resolveRepositorySelection({ repoId: repository.repoId, checkoutId: contract.checkoutId, controllerHome: ctx.controllerHome, allowSoleRepository: false });
1620
+ const checkout = selectRepositoryCheckout(executionRepository, contract.checkoutId, { allowArchived: true });
1621
+ const status = repositoryGitStatus(checkout);
1622
+ if (!status.branch) throw new Error(`WORKTREE_DETACHED: ${contract.checkoutId} has no branch`);
1623
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
1624
+ const at = new Date().toISOString();
1625
+ const managedWorktree = Boolean(contract.worktreeRef) && resolve(contract.worktreeRef!) === resolve(checkout.canonicalRoot) && resolve(checkout.canonicalRoot) !== resolve(repository.canonicalRoot);
1626
+ return writeWorkHandle(ctx.controllerHome, {
1627
+ schemaVersion: 1,
1628
+ workId,
1629
+ sessionId: identity.sessionId,
1630
+ principalId: identity.principalId,
1631
+ repositoryId: repository.repoId,
1632
+ checkoutId: contract.checkoutId,
1633
+ worktreePath: checkout.canonicalRoot,
1634
+ branch: status.branch,
1635
+ sourceCheckoutId: repository.activeCheckoutId,
1636
+ managedWorktree,
1637
+ workContractId: contract.workId,
1638
+ baseCommit: contract.baseRevision ?? status.head ?? undefined,
1639
+ expectedHead: status.head ?? contract.baseRevision,
1640
+ permissionSnapshotVersion: currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId),
1641
+ state: 'prepared',
1642
+ createdAt: at,
1643
+ updatedAt: at,
1644
+ finalization: { validation: 'pending', commit: 'pending', merge: 'pending', branchCleanup: 'pending', worktreeCleanup: 'pending' },
1645
+ cleanupResponsibility: { owner: 'work_finalizer', registeredAt: at },
1646
+ });
1647
+ }
1648
+
1649
+ function claimNewFacadeWork(
1650
+ ctx: MultiRepositoryMcpToolContext,
1651
+ repository: ReturnType<typeof selected>,
1652
+ workId: string,
1653
+ args: Record<string, unknown>,
1654
+ ) {
1655
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
1656
+ return resumeControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
1657
+ workId,
1658
+ controllerId: identity.controllerId,
1659
+ controllerType: identity.controllerType,
1660
+ sessionId: identity.sessionId,
1661
+ principalId: identity.principalId,
1662
+ controllerInstanceId: identity.controllerInstanceId,
1663
+ leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
1664
+ });
1665
+ }
1666
+
1546
1667
  function bindFacadeExecutionSession(
1547
1668
  ctx: MultiRepositoryMcpToolContext,
1548
1669
  repository: ReturnType<typeof selected>,
@@ -1623,14 +1744,7 @@ async function finalizeFacadeWorkHandle(
1623
1744
  ...(noChangeEvidence ? { no_change_evidence: noChangeEvidence } : {}),
1624
1745
  };
1625
1746
 
1626
- let physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
1627
- let payload = contextRecord(physical?.structuredContent);
1628
- if (
1629
- physical
1630
- && physical.isError !== true
1631
- && typeof payload.continuation === 'string'
1632
- && payload.continuation.startsWith('WORK_COMMITTED_REVALIDATION_REQUIRED')
1633
- ) {
1747
+ const validateExactWorkspace = async (): Promise<CallToolResult | undefined> => {
1634
1748
  const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
1635
1749
  const validation = await callExecutionTool(ctx, 'work_validate', {
1636
1750
  session_id: session.sessionId,
@@ -1640,13 +1754,38 @@ async function finalizeFacadeWorkHandle(
1640
1754
  });
1641
1755
  if (!validation || validation.isError === true) return validation;
1642
1756
  const validationPayload = contextRecord(validation.structuredContent);
1643
- if (contextRecord(validationPayload.validation).passed !== true) return validation;
1757
+ return contextRecord(validationPayload.validation).passed === true ? undefined : validation;
1758
+ };
1759
+
1760
+ let physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
1761
+ let payload = contextRecord(physical?.structuredContent);
1762
+ if (physical?.isError === true && contextRecord(payload.error).code === 'WORK_VALIDATION_REQUIRED') {
1763
+ const validationFailure = await validateExactWorkspace();
1764
+ if (validationFailure) return validationFailure;
1644
1765
  physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
1645
1766
  payload = contextRecord(physical?.structuredContent);
1646
1767
  }
1768
+ if (
1769
+ physical
1770
+ && physical.isError !== true
1771
+ && typeof payload.continuation === 'string'
1772
+ && payload.continuation.startsWith('WORK_COMMITTED_REVALIDATION_REQUIRED')
1773
+ ) {
1774
+ const validationFailure = await validateExactWorkspace();
1775
+ if (validationFailure) return validationFailure;
1776
+ physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
1777
+ }
1647
1778
  return physical;
1648
1779
  }
1649
1780
 
1781
+ function structuralIndexRoot(repository: ReturnType<typeof resolveRepositorySelection>): string | undefined {
1782
+ if (existsSync(join(repository.canonicalRoot, '.codegraph', 'codegraph.db'))) return repository.canonicalRoot;
1783
+ return repository.checkouts
1784
+ .filter((checkout) => checkout.checkoutId !== repository.activeCheckoutId && checkout.worktree !== true)
1785
+ .map((checkout) => checkout.canonicalRoot)
1786
+ .find((root) => existsSync(join(root, '.codegraph', 'codegraph.db')));
1787
+ }
1788
+
1650
1789
  function selected(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>) {
1651
1790
  return resolveRepositorySelection({
1652
1791
  repoId: typeof args.repo_id === 'string' ? args.repo_id : undefined,
@@ -2569,15 +2708,18 @@ async function runFacadeVerify(
2569
2708
  const requestId = typeof args.request_id === 'string' && args.request_id.trim()
2570
2709
  ? args.request_id.trim()
2571
2710
  : undefined;
2572
- const observedGitHead = repositoryGitStatus(repository).head;
2573
2711
  const workContract = workId ? getWorkContract(store, workId) : undefined;
2574
2712
  if (workId && !workContract) throw new Error(`WORK_NOT_FOUND: ${workId}`);
2713
+ const verificationRepository = workContract?.checkoutId
2714
+ ? selectRepositoryCheckout(repository, workContract.checkoutId, { allowArchived: true })
2715
+ : repository;
2716
+ const observedGitHead = repositoryGitStatus(verificationRepository).head;
2575
2717
  const executed = await runPersistedCheckViaProcessRuntime({
2576
2718
  controllerHome: ctx.controllerHome,
2577
- repoId: repository.repoId,
2578
- checkoutId: repository.activeCheckoutId,
2579
- repoRoot: repository.canonicalRoot,
2580
- executionIdentity: executionIdentityForRepository(repository, workId ? { workId } : {}),
2719
+ repoId: verificationRepository.repoId,
2720
+ checkoutId: verificationRepository.activeCheckoutId,
2721
+ repoRoot: verificationRepository.canonicalRoot,
2722
+ executionIdentity: executionIdentityForRepository(verificationRepository, workId ? { workId } : {}),
2581
2723
  checkId: normalizedCheckId,
2582
2724
  timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
2583
2725
  // rh_work.verify is a control-plane continuation primitive: callers should
@@ -2620,7 +2762,7 @@ async function runFacadeVerify(
2620
2762
 
2621
2763
  const handle = executed.process;
2622
2764
  if (!handle) throw new Error(`PROCESS_CHECK_HANDLE_MISSING: ${normalizedCheckId}`);
2623
- const record = getProcessRecord(ctx.controllerHome, repository.repoId, handle.processId);
2765
+ const record = getProcessRecord(ctx.controllerHome, verificationRepository.repoId, handle.processId);
2624
2766
  const checkContentRevision = record?.checkExecution?.revision;
2625
2767
 
2626
2768
  if (!handle.completed) {
@@ -2651,11 +2793,11 @@ async function runFacadeVerify(
2651
2793
 
2652
2794
  if (!record) throw new Error(`PROCESS_CHECK_RECORD_MISSING: ${handle.processId}`);
2653
2795
  const receipt = processCheckCompletionReceipt(record, {
2654
- repoId: repository.repoId,
2796
+ repoId: verificationRepository.repoId,
2655
2797
  checkId: normalizedCheckId,
2656
2798
  processId: handle.processId,
2657
2799
  ...(record.checkExecution ? {
2658
- checkoutId: repository.activeCheckoutId,
2800
+ checkoutId: verificationRepository.activeCheckoutId,
2659
2801
  workId: workId || undefined,
2660
2802
  requestId,
2661
2803
  checkExecution: {
@@ -2677,7 +2819,7 @@ async function runFacadeVerify(
2677
2819
  );
2678
2820
  const legacyEvidence = record.origin?.checkResultReceiptPath
2679
2821
  ? undefined
2680
- : readLatestControllerCheckEvidence(repository.canonicalRoot, normalizedCheckId);
2822
+ : readLatestControllerCheckEvidence(verificationRepository.canonicalRoot, normalizedCheckId);
2681
2823
  const legacyEvidenceMatchesProcess = Boolean(
2682
2824
  legacyEvidence?.cacheKey
2683
2825
  && record.checkExecution?.cacheKey
@@ -2922,7 +3064,14 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
2922
3064
  });
2923
3065
  const capabilities = listCapabilityDescriptors(manifests);
2924
3066
  const pendingHandoffs = listHandoffItems({ ...store, status: 'pending', limit: 20 });
2925
- const activeWork = listWorkContracts({ ...store, status: 'active', limit: 20 });
3067
+ const activeContracts = listWorkContracts({ ...store, status: 'active', limit: 200 });
3068
+ const activePrimaryWork = activeContracts.filter((contract) => (contract.lifecycleRole ?? 'primary') === 'primary');
3069
+ const activeExecutionChildren = activeContracts.filter((contract) => contract.lifecycleRole === 'execution_child');
3070
+ const activeRuntimeInstanceId = currentControllerInstanceId();
3071
+ const activeProcessRecords = listProcessRecords(ctx.controllerHome, repository.repoId, 500).filter((process) => isManagedProcessActive(process) && (!process.runtimeInstanceId || process.runtimeInstanceId === activeRuntimeInstanceId));
3072
+ const activeProcessWorkIds = new Set(activeProcessRecords.map((process) => process.workId).filter((workId): workId is string => Boolean(workId)));
3073
+ const activeControllerWorkIds = new Set(activePrimaryWork.filter((contract) => Boolean(getControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, contract.workId))).map((contract) => contract.workId));
3074
+ const executingPrimaryWorkIds = new Set([...activeProcessWorkIds, ...activeControllerWorkIds].filter((workId) => activePrimaryWork.some((contract) => contract.workId === workId)));
2926
3075
  const preferredFacadeTools = ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'] as const;
2927
3076
  const facade = buildFacadeResult({
2928
3077
  status: effectiveReady ? 'ok' : 'blocked',
@@ -2951,7 +3100,15 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
2951
3100
  dynamicDomainSchemaLoadingSupported: false,
2952
3101
  },
2953
3102
  pendingHandoffCount: pendingHandoffs.length,
2954
- activeWorkCount: activeWork.length,
3103
+ // User-facing Work count means objective-level primary lanes. Low-level
3104
+ // resumable operation handles are reported separately.
3105
+ activeWorkCount: activePrimaryWork.length,
3106
+ activePrimaryWorkCount: activePrimaryWork.length,
3107
+ activeExecutionChildCount: activeExecutionChildren.length,
3108
+ activeProcessCount: activeProcessRecords.length,
3109
+ executingPrimaryWorkCount: executingPrimaryWorkIds.size,
3110
+ waitingPrimaryWorkCount: Math.max(0, activePrimaryWork.length - executingPrimaryWorkIds.size),
3111
+ activeContractCount: activeContracts.length,
2955
3112
  // Summary keeps the stable facade surface only; detail expands to the full registered schema.
2956
3113
  toolSurface: detailLevel === 'detail'
2957
3114
  ? exposure.actualToolNames
@@ -3099,6 +3256,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3099
3256
  maxFiles: typeof args.max_files === 'number' ? args.max_files : undefined,
3100
3257
  maxSnippets: typeof args.max_snippets === 'number' ? args.max_snippets : undefined,
3101
3258
  structuralContext,
3259
+ structuralIndexRoot: structuralContext === 'off' ? undefined : structuralIndexRoot(repository),
3102
3260
  retrievalMode,
3103
3261
  impactDomains,
3104
3262
  });
@@ -3116,6 +3274,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3116
3274
  goal: pack.goal,
3117
3275
  search: pack.search,
3118
3276
  structuralContext: pack.structuralContext,
3277
+ impactContext: pack.impactContext,
3119
3278
  files: pack.files,
3120
3279
  deniedPaths: pack.deniedPaths,
3121
3280
  omitted: pack.omitted,
@@ -3167,6 +3326,11 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3167
3326
  .filter((job) => timestampIsCurrent(job.updatedAt, currentCutoffMs))
3168
3327
  .slice(0, 5)
3169
3328
  : [];
3329
+ const processScan = listProcessRecords(ctx.controllerHome, repository.repoId, workId ? 100 : 50);
3330
+ const relevantProcesses = processScan.filter((process) => workId ? process.workId === workId : timestampIsCurrent(process.updatedAt, currentCutoffMs));
3331
+ const activeRuntimeInstanceId = currentControllerInstanceId();
3332
+ const activeProcesses = relevantProcesses.filter((process) => isManagedProcessActive(process) && (!process.runtimeInstanceId || process.runtimeInstanceId === activeRuntimeInstanceId));
3333
+ const workController = work ? getControllerSession(store, work.workId) : undefined;
3170
3334
  const repositoryManifests = listAssistantPluginManifests(ctx.controllerHome, repository, {
3171
3335
  preferStored: true,
3172
3336
  });
@@ -3204,6 +3368,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3204
3368
  || timestampIsCurrent(item.updatedAt, currentCutoffMs)
3205
3369
  ))
3206
3370
  : pendingAttention;
3371
+ const workAttention = work ? currentAttentionScan.find((item) => item.workId === work.workId) : undefined;
3207
3372
  const currentAttentionItems = isSummary
3208
3373
  ? currentAttentionScan.slice(0, 3)
3209
3374
  : pendingAttention;
@@ -3259,6 +3424,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3259
3424
  }
3260
3425
  : undefined,
3261
3426
  executionJob: executionJob ? summarizeWorkListItem(executionJob) : undefined,
3427
+ executionState: work ? (workAttention ? 'blocked' : activeProcesses.length > 0 ? 'executing' : workController ? 'controller_active' : 'waiting_trigger') : undefined,
3428
+ activeController: workController ? { controllerType: workController.controllerType, sessionId: workController.sessionId, leaseExpiresAt: workController.leaseExpiresAt } : undefined,
3429
+ activeProcesses: activeProcesses.slice(0, 3).map((process) => ({ processId: process.processId, workId: process.workId, status: process.status, route: process.route, startedAt: process.startedAt, updatedAt: process.updatedAt })),
3430
+ recentProcesses: relevantProcesses.slice(0, 5).map((process) => ({ processId: process.processId, workId: process.workId, status: process.status, route: process.route, startedAt: process.startedAt, updatedAt: process.updatedAt })),
3262
3431
  activeWork: activeContracts.map((entry) => ({
3263
3432
  workId: entry.workId,
3264
3433
  status: entry.status,
@@ -3281,6 +3450,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3281
3450
  historicalNonTerminalWork: Math.max(0, activeContractScan.length - currentContractScan.length),
3282
3451
  currentAttention: currentAttentionScan.length,
3283
3452
  currentAttentionShown: attention.length,
3453
+ activeProcesses: activeProcesses.length,
3454
+ recentProcesses: relevantProcesses.length,
3284
3455
  pendingAttentionScanned: pendingAttention.length,
3285
3456
  historicalPendingAttention: Math.max(0, pendingAttention.length - currentAttentionScan.length),
3286
3457
  omittedCurrentAttention: Math.max(0, currentAttentionScan.length - attention.length),
@@ -3318,6 +3489,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3318
3489
  },
3319
3490
  work: work ? { ...work, continuation: buildWorkContinuationSnapshot(work) } : undefined,
3320
3491
  executionJob: executionJob ? summarizeWorkListItem(executionJob) : undefined,
3492
+ executionState: work ? (workAttention ? 'blocked' : activeProcesses.length > 0 ? 'executing' : workController ? 'controller_active' : 'waiting_trigger') : undefined,
3493
+ activeController: workController,
3494
+ activeProcesses: activeProcesses.slice(0, 10),
3495
+ recentProcesses: relevantProcesses.slice(0, 20),
3321
3496
  activeWork: activeContracts.map((entry) => ({
3322
3497
  workId: entry.workId,
3323
3498
  status: entry.status,
@@ -3333,6 +3508,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3333
3508
  capabilities: capabilities.length,
3334
3509
  activeWork: activeContracts.length,
3335
3510
  recentExecutionJobs: recentJobs.length,
3511
+ activeProcesses: activeProcesses.length,
3512
+ recentProcesses: relevantProcesses.length,
3336
3513
  activeAttention: attention.length,
3337
3514
  },
3338
3515
  bounded: true,
@@ -3385,8 +3562,59 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3385
3562
  const triggerType = ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'].includes(triggerTypeRaw)
3386
3563
  ? triggerTypeRaw as 'interval' | 'cron' | 'calendar' | 'condition' | 'repository-event' | 'dependency-checkpoint' | 'manual'
3387
3564
  : undefined;
3565
+ const scheduleModeRaw = String(args.schedule_mode ?? (workId ? 'continuation' : 'workflow')).trim();
3566
+ if (!['workflow', 'continuation', 'browser_watch', 'browser_keepalive'].includes(scheduleModeRaw)) throw new Error('SCHEDULE_MODE_INVALID');
3567
+ if (scheduleModeRaw === 'workflow') {
3568
+ if (controllerType !== 'chatgpt') throw new Error('WORKFLOW_SCHEDULE_CONTROLLER_TYPE_UNSUPPORTED');
3569
+ const prompt = (typeof args.objective === 'string' ? args.objective : typeof args.continuation_prompt === 'string' ? args.continuation_prompt : '').trim();
3570
+ if (!prompt) throw new Error('WORKFLOW_SCHEDULE_PROMPT_REQUIRED');
3571
+ const trigger = {
3572
+ type: triggerType ?? (typeof args.every_minutes === 'number' ? 'interval' : 'manual'),
3573
+ everyMinutes: typeof args.every_minutes === 'number' ? Math.max(1, Math.trunc(args.every_minutes)) : undefined,
3574
+ cronExpression: typeof args.cron_expression === 'string' ? args.cron_expression : undefined,
3575
+ timezone: typeof args.schedule_timezone === 'string' ? args.schedule_timezone : undefined,
3576
+ catchUpMinutes: typeof args.catch_up_minutes === 'number' ? Math.max(0, Math.trunc(args.catch_up_minutes)) : undefined,
3577
+ calendarAt: typeof args.calendar_at === 'string' ? args.calendar_at : undefined,
3578
+ condition: args.condition && typeof args.condition === 'object' && !Array.isArray(args.condition) ? args.condition as never : undefined,
3579
+ eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
3580
+ dependencyJobIds: Array.isArray(args.dependency_job_ids) ? args.dependency_job_ids.map(String) : undefined,
3581
+ };
3582
+ const policy = {
3583
+ maxActiveOccurrences: 1,
3584
+ maxFailures: typeof args.max_failures === 'number' ? Math.max(1, Math.trunc(args.max_failures)) : 3,
3585
+ cooldownMinutes: typeof args.cooldown_minutes === 'number' ? Math.max(0, Math.trunc(args.cooldown_minutes)) : 120,
3586
+ dailyBudgetMinutes: typeof args.daily_budget_minutes === 'number' ? Math.max(1, Math.trunc(args.daily_budget_minutes)) : 180,
3587
+ shadowMode: args.shadow_mode !== false,
3588
+ backoffBaseMinutes: typeof args.backoff_base_minutes === 'number' ? Math.max(1, Math.trunc(args.backoff_base_minutes)) : 5,
3589
+ backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? Math.max(1, Math.trunc(args.backoff_max_minutes)) : 24 * 60,
3590
+ };
3591
+ const actionArguments = {
3592
+ prompt,
3593
+ ...(typeof args.browser_session_id === 'string' && args.browser_session_id.trim() ? { browser_session_id: args.browser_session_id.trim() } : {}),
3594
+ ...(typeof args.conversation_url === 'string' && args.conversation_url.trim() ? { conversation_url: args.conversation_url.trim() } : {}),
3595
+ };
3596
+ assertAutomatedOperationAllowed('chatgpt_browser_prompt', actionArguments);
3597
+ const name = typeof args.schedule_name === 'string' && args.schedule_name.trim() ? args.schedule_name.trim() : `Workflow: ${prompt.slice(0, 80)}`;
3598
+ const stopConditions = Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : [];
3599
+ const semantic = JSON.stringify({ repoId: repository.repoId, name, trigger, policy, actionArguments, stopConditions });
3600
+ const requestId = typeof args.schedule_request_id === 'string' && args.schedule_request_id.trim()
3601
+ ? args.schedule_request_id.trim()
3602
+ : `workflow-schedule:${repository.repoId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 20)}`;
3603
+ const schedule = createSchedule(ctx.controllerHome, {
3604
+ requestId,
3605
+ repoId: repository.repoId,
3606
+ name,
3607
+ enabled: true,
3608
+ trigger,
3609
+ policy,
3610
+ action: { operation: 'chatgpt_browser_prompt', target: 'runtime', arguments: actionArguments, resourceClaims: [] },
3611
+ stopConditions,
3612
+ });
3613
+ return result(buildFacadeResult({ summary: `Workflow schedule ${schedule.scheduleId} is configured without a durable Work.`, data: { schedule } }) as unknown as Record<string, unknown>);
3614
+ }
3388
3615
  const created = createWorkContinuationSchedule(ctx.controllerHome, repository.repoId, {
3389
3616
  workId,
3617
+ scheduleMode: scheduleModeRaw as 'continuation' | 'browser_watch' | 'browser_keepalive',
3390
3618
  controllerType: controllerType as ContinuationControllerType,
3391
3619
  executable: typeof args.executable === 'string' ? args.executable : undefined,
3392
3620
  launchArgs: Array.isArray(args.launch_args) ? args.launch_args.map(String) : undefined,
@@ -3395,6 +3623,18 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3395
3623
  browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
3396
3624
  conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
3397
3625
  continuationPrompt: typeof args.continuation_prompt === 'string' ? args.continuation_prompt : undefined,
3626
+ probeUrl: typeof args.probe_url === 'string' ? args.probe_url : undefined,
3627
+ probeBrowserSessionId: typeof args.probe_browser_session_id === 'string' ? args.probe_browser_session_id : undefined,
3628
+ probeSelector: typeof args.probe_selector === 'string' ? args.probe_selector : undefined,
3629
+ probeMaxChars: typeof args.probe_max_chars === 'number' ? args.probe_max_chars : undefined,
3630
+ probeTimeoutMs: typeof args.probe_timeout_ms === 'number' ? args.probe_timeout_ms : undefined,
3631
+ includeTerms: Array.isArray(args.include_terms) ? args.include_terms.map(String) : undefined,
3632
+ ignorePatterns: Array.isArray(args.ignore_patterns) ? args.ignore_patterns.map(String) : undefined,
3633
+ loginUrlTerms: Array.isArray(args.login_url_terms) ? args.login_url_terms.map(String) : undefined,
3634
+ loginTextTerms: Array.isArray(args.login_text_terms) ? args.login_text_terms.map(String) : undefined,
3635
+ wakeOnFirstObservation: args.wake_on_first_observation === true,
3636
+ wakeOnAuthRequired: args.wake_on_auth_required !== false,
3637
+ authRequiredPrompt: typeof args.auth_required_prompt === 'string' ? args.auth_required_prompt : undefined,
3398
3638
  scheduleName: typeof args.schedule_name === 'string' ? args.schedule_name : undefined,
3399
3639
  requestId: typeof args.schedule_request_id === 'string' ? args.schedule_request_id : undefined,
3400
3640
  triggerType,
@@ -3414,33 +3654,41 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3414
3654
  backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? args.backoff_max_minutes : undefined,
3415
3655
  stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : undefined,
3416
3656
  });
3417
- return result(buildFacadeResult({ summary: `Continuation schedule ${created.schedule.scheduleId} is configured for Work ${workId}.`, data: { schedule: created.schedule, work: buildWorkContinuationSnapshot(created.work) } }) as unknown as Record<string, unknown>);
3657
+ return result(buildFacadeResult({ summary: `Work schedule ${created.schedule.scheduleId} is configured for Work ${workId}.`, data: { schedule: created.schedule, work: buildWorkContinuationSnapshot(created.work) } }) as unknown as Record<string, unknown>);
3418
3658
  }
3419
3659
  if (operation === 'schedule_list') {
3420
- const listed = listWorkContinuationSchedules(ctx.controllerHome, repository.repoId, { workId: workId || undefined, includeOccurrences: args.include_occurrences === true });
3421
- return result(buildFacadeResult({ summary: `Found ${listed.schedules.length} Work continuation schedule(s).`, data: listed }) as unknown as Record<string, unknown>);
3660
+ const schedules = listSchedules(ctx.controllerHome, repository.repoId)
3661
+ .filter((entry) => !workId || entry.action.arguments?.work_id === workId);
3662
+ const data = args.include_occurrences === true
3663
+ ? { schedules, occurrences: listOccurrences(ctx.controllerHome, repository.repoId, undefined, 100).filter((entry) => schedules.some((schedule) => schedule.scheduleId === entry.scheduleId)) }
3664
+ : { schedules };
3665
+ return result(buildFacadeResult({ summary: `Found ${schedules.length} schedule(s).`, data }) as unknown as Record<string, unknown>);
3422
3666
  }
3423
3667
  if (!scheduleId) throw new Error('SCHEDULE_ID_REQUIRED');
3424
3668
  if (operation === 'schedule_get') {
3425
- const fetched = getWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, args.include_occurrences === true);
3426
- return result(buildFacadeResult({ summary: `Continuation schedule ${scheduleId}.`, data: fetched }) as unknown as Record<string, unknown>);
3669
+ const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3670
+ const data = args.include_occurrences === true ? { schedule, occurrences: listOccurrences(ctx.controllerHome, repository.repoId, scheduleId, 50) } : { schedule };
3671
+ return result(buildFacadeResult({ summary: `Schedule ${scheduleId}.`, data }) as unknown as Record<string, unknown>);
3427
3672
  }
3428
3673
  if (operation === 'schedule_pause') {
3429
- const schedule = pauseWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, typeof args.reason === 'string' ? args.reason : undefined);
3430
- return result(buildFacadeResult({ summary: `Continuation schedule ${scheduleId} is paused.`, data: { schedule } }) as unknown as Record<string, unknown>);
3674
+ const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3675
+ const saved = saveSchedule(ctx.controllerHome, { ...schedule, enabled: false, pausedReason: typeof args.reason === 'string' && args.reason.trim() ? args.reason.trim() : 'Paused by ChatGPT.' });
3676
+ return result(buildFacadeResult({ summary: `Schedule ${scheduleId} is paused.`, data: { schedule: saved } }) as unknown as Record<string, unknown>);
3431
3677
  }
3432
3678
  if (operation === 'schedule_resume') {
3433
- const schedule = resumeWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3434
- return result(buildFacadeResult({ summary: `Continuation schedule ${scheduleId} is resumed.`, data: { schedule } }) as unknown as Record<string, unknown>);
3679
+ const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3680
+ const saved = saveSchedule(ctx.controllerHome, { ...schedule, enabled: true, pausedReason: undefined, consecutiveFailures: 0, nextEligibleAt: undefined });
3681
+ return result(buildFacadeResult({ summary: `Schedule ${scheduleId} is resumed.`, data: { schedule: saved } }) as unknown as Record<string, unknown>);
3435
3682
  }
3436
3683
  if (operation === 'schedule_trigger') {
3437
- const occurrence = await triggerWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, {
3684
+ const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3685
+ const occurrence = await evaluateSchedule(ctx.controllerHome, schedule, true, {
3438
3686
  source: typeof args.event_name === 'string' && args.event_name.trim() ? 'repository-event' : 'manual',
3439
3687
  eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
3440
3688
  eventId: typeof args.event_id === 'string' ? args.event_id : undefined,
3441
3689
  data: args.event_data && typeof args.event_data === 'object' && !Array.isArray(args.event_data) ? args.event_data as Record<string, unknown> : undefined,
3442
3690
  });
3443
- return result(buildFacadeResult({ summary: occurrence ? `Continuation schedule ${scheduleId} produced ${occurrence.decision}.` : `Continuation schedule ${scheduleId} produced no occurrence.`, data: { occurrence } }) as unknown as Record<string, unknown>);
3691
+ return result(buildFacadeResult({ summary: occurrence ? `Schedule ${scheduleId} produced ${occurrence.decision}.` : `Schedule ${scheduleId} produced no occurrence.`, data: { occurrence } }) as unknown as Record<string, unknown>);
3444
3692
  }
3445
3693
  } catch (error) {
3446
3694
  return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Schedule operation failed.', data: {} }) as unknown as Record<string, unknown>, true);
@@ -3453,7 +3701,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3453
3701
  if (operation === 'controller_claim') {
3454
3702
  try {
3455
3703
  const workId = String(args.work_id ?? '').trim();
3456
- if (!getWorkContract(store, workId)) throw new Error(`WORK_NOT_FOUND: ${workId}`);
3704
+ const work = getWorkContract(store, workId);
3705
+ if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
3457
3706
  const identity = authenticatedFacadeControllerIdentity(ctx, args);
3458
3707
  const session = resumeControllerSession(store, {
3459
3708
  workId,
@@ -3464,6 +3713,24 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3464
3713
  controllerInstanceId: identity.controllerInstanceId,
3465
3714
  leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
3466
3715
  });
3716
+ const permissionSnapshotVersion = currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId);
3717
+ const executionSession = startExecutionSession(ctx.controllerHome, {
3718
+ sessionId: identity.sessionId,
3719
+ principalId: identity.principalId,
3720
+ controllerInstanceId: identity.controllerInstanceId,
3721
+ permissionSnapshotVersion,
3722
+ });
3723
+ updateExecutionSession(ctx.controllerHome, {
3724
+ sessionId: executionSession.sessionId,
3725
+ principalId: executionSession.principalId,
3726
+ controllerInstanceId: executionSession.controllerInstanceId,
3727
+ }, {
3728
+ activeRepositoryId: repository.repoId,
3729
+ activeCheckoutId: work.checkoutId || repository.activeCheckoutId,
3730
+ activeWorkId: work.workId,
3731
+ permissionSnapshotVersion,
3732
+ lastValidatedAt: new Date().toISOString(),
3733
+ });
3467
3734
  return result(buildFacadeResult({ summary: `Controller ${session.controllerId} claimed ${session.workId}.`, data: { session } }) as unknown as Record<string, unknown>);
3468
3735
  } catch (error) {
3469
3736
  return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller claim failed.', data: {} }) as unknown as Record<string, unknown>, true);
@@ -3478,6 +3745,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3478
3745
  throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId} is owned by ${owner.controllerId}`);
3479
3746
  }
3480
3747
  releaseControllerSession(store, workId, identity.controllerId);
3748
+ const executionSession = readExecutionSession(ctx.controllerHome, identity);
3749
+ if (executionSession?.activeWorkId === workId) {
3750
+ updateExecutionSession(ctx.controllerHome, identity, { activeWorkId: undefined, lastValidatedAt: new Date().toISOString() });
3751
+ }
3481
3752
  return result(buildFacadeResult({ summary: 'Controller lease released.', data: {} }) as unknown as Record<string, unknown>);
3482
3753
  } catch (error) {
3483
3754
  return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller release failed.', data: {} }) as unknown as Record<string, unknown>, true);
@@ -3515,15 +3786,81 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3515
3786
  controllerInstanceId: ctx.controllerInstanceId,
3516
3787
  availableChecks: checks,
3517
3788
  sourceRevision: gitSnapshot(repository.canonicalRoot).head ?? undefined,
3789
+ materializeIsolatedWorkspace: ({ workId, title, baseRef }: { workId: string; title: string; baseRef?: string }) => {
3790
+ const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, { requestId: workId, title, baseRef });
3791
+ if (!workspace.managed || !workspace.checkoutId || !workspace.root) throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
3792
+ return { checkoutId: workspace.checkoutId, root: workspace.root, baseRevision: workspace.baseRevision, managed: true as const };
3793
+ },
3518
3794
  };
3519
3795
 
3520
3796
  if (operation.startsWith('plan_')) {
3521
3797
  try {
3522
3798
  if (operation === 'plan_create') {
3523
3799
  const rawSteps = Array.isArray(args.plan_steps) ? args.plan_steps : [];
3800
+ const requestedRequirementId = typeof args.requirement_id === 'string' && args.requirement_id.trim() ? args.requirement_id.trim() : undefined;
3801
+ const requestedScopeKey = String(args.scope_key ?? '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 160);
3802
+ const activePlans = listPlanContracts({ ...store, status: 'active', limit: 100 });
3803
+ const exactScopeAuthority = requestedScopeKey
3804
+ ? activePlans.find((candidate) => candidate.scopeKey === requestedScopeKey)
3805
+ : undefined;
3806
+ if (exactScopeAuthority) {
3807
+ const facade = buildFacadeResult({
3808
+ summary: `PLAN_AUTHORITY_REUSED: active Plan ${exactScopeAuthority.planId} already owns scope ${requestedScopeKey}; no duplicate draft was created.`,
3809
+ data: {
3810
+ plan: summarizePlanContract(exactScopeAuthority),
3811
+ executionStarted: false,
3812
+ planContractCreated: false,
3813
+ admissionDecision: 'reuse_existing',
3814
+ resolutionRequired: false,
3815
+ },
3816
+ suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: exactScopeAuthority.planId }, risk: 'readonly', confidence: 'high' }],
3817
+ });
3818
+ return result(facade as unknown as Record<string, unknown>);
3819
+ }
3820
+ const requirementPlans = requestedRequirementId
3821
+ ? activePlans.filter((candidate) => candidate.requirementId === requestedRequirementId)
3822
+ : [];
3823
+ const requestedPlanRelation = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
3824
+ ? args.plan_relation
3825
+ : undefined;
3826
+ const relatedPlanId = typeof args.related_plan_id === 'string' && args.related_plan_id.trim() ? args.related_plan_id.trim() : undefined;
3827
+ const relatedPlan = relatedPlanId
3828
+ ? requirementPlans.find((candidate) => candidate.planId === relatedPlanId)
3829
+ : requirementPlans.length === 1 ? requirementPlans[0] : undefined;
3830
+ if (requirementPlans.length > 0 && !requestedPlanRelation) {
3831
+ const facade = buildFacadeResult({
3832
+ summary: `PLAN_RELATION_RESOLUTION_REQUIRED: Requirement ${requestedRequirementId} already has ${requirementPlans.length} active Plan slice(s). Decide whether this scope extends one of them or is an intentional parallel slice before creating a draft.`,
3833
+ data: {
3834
+ executionStarted: false,
3835
+ planContractCreated: false,
3836
+ admissionDecision: 'resolution_required',
3837
+ resolutionRequired: true,
3838
+ candidates: requirementPlans.slice(0, 8).map(summarizePlanContract),
3839
+ allowedPlanRelations: ['extend', 'parallel'],
3840
+ },
3841
+ suggestedNextActions: requirementPlans.slice(0, 3).map((candidate) => ({ label: `Read ${candidate.planId}`, tool: 'rh_work', operation: 'plan_get', payload: { plan_id: candidate.planId }, risk: 'readonly' as const, confidence: 'high' as const })),
3842
+ });
3843
+ return result(facade as unknown as Record<string, unknown>);
3844
+ }
3845
+ if (requirementPlans.length > 0 && requestedPlanRelation === 'extend') {
3846
+ if (!relatedPlan) {
3847
+ const facade = buildFacadeResult({
3848
+ summary: `PLAN_EXTENSION_TARGET_REQUIRED: select related_plan_id from the active Plan slices for Requirement ${requestedRequirementId}.`,
3849
+ data: { executionStarted: false, planContractCreated: false, admissionDecision: 'resolution_required', resolutionRequired: true, candidates: requirementPlans.slice(0, 8).map(summarizePlanContract) },
3850
+ });
3851
+ return result(facade as unknown as Record<string, unknown>);
3852
+ }
3853
+ const facade = buildFacadeResult({
3854
+ summary: `PLAN_EXTENSION_REQUIRES_REPLAN: ${relatedPlan.planId} remains the serial authority. Extend/replan and supersede that authority rather than creating a second active Plan draft.`,
3855
+ data: { plan: summarizePlanContract(relatedPlan), executionStarted: false, planContractCreated: false, admissionDecision: 'extend_existing', resolutionRequired: false },
3856
+ suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: relatedPlan.planId }, risk: 'readonly', confidence: 'high' }],
3857
+ });
3858
+ return result(facade as unknown as Record<string, unknown>);
3859
+ }
3524
3860
  const plan = createPlanContract(store, {
3525
3861
  planId: String(args.plan_id ?? ''),
3526
3862
  repoId: repository.repoId,
3863
+ requirementId: requestedRequirementId,
3527
3864
  scopeKey: String(args.scope_key ?? ''),
3528
3865
  sourceRevision: String(args.source_revision ?? ''),
3529
3866
  goal: String(args.objective ?? ''),
@@ -3545,8 +3882,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3545
3882
  })),
3546
3883
  });
3547
3884
  const facade = buildFacadeResult({
3548
- summary: `PlanContract ${plan.planId} created as draft; no execution was started.`,
3549
- data: { plan: summarizePlanContract(plan), executionStarted: false },
3885
+ summary: `PlanContract ${plan.planId} created as draft after authority preflight; no execution was started.`,
3886
+ data: { plan: summarizePlanContract(plan), executionStarted: false, planContractCreated: true, admissionDecision: 'create_new' },
3550
3887
  suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: plan.planId }, risk: 'workspace_write', confidence: 'medium' }],
3551
3888
  });
3552
3889
  return result(facade as unknown as Record<string, unknown>);
@@ -3652,7 +3989,43 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3652
3989
 
3653
3990
  if (operation === 'finalize') {
3654
3991
  const workId = String(args.work_id ?? '').trim();
3655
- const before = workId ? getWorkContract(store, workId) : undefined;
3992
+ let before = workId ? getWorkContract(store, workId) : undefined;
3993
+ if (before && !before.completionReceipt && args.reconcile_historical_delivery === true) {
3994
+ try {
3995
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
3996
+ const owner = getControllerSession(store, workId);
3997
+ if (!owner || (owner.principalId?.trim() || owner.controllerId) !== identity.principalId) {
3998
+ throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
3999
+ }
4000
+ const historicalHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
4001
+ if (historicalHandle?.managedWorktree) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
4002
+ const reconciliation = acceptReviewedDirectEditWorkReconciliation({
4003
+ controllerHome: ctx.controllerHome,
4004
+ repoId: repository.repoId,
4005
+ checkoutId: before.checkoutId ?? repository.activeCheckoutId,
4006
+ repoRoot: repository.canonicalRoot,
4007
+ workId,
4008
+ targetBranch: typeof args.target_branch === 'string' && args.target_branch.trim() ? args.target_branch.trim() : repository.defaultBranch || 'main',
4009
+ targetRevision: String(args.reconcile_target_revision ?? ''),
4010
+ comparedPaths: Array.isArray(args.reconcile_compared_paths) ? args.reconcile_compared_paths.map(String) : [],
4011
+ reviewer: identity.principalId,
4012
+ rationale: String(args.reconcile_rationale ?? ''),
4013
+ cleanupOwnershipProof: String(args.reconcile_cleanup_proof ?? ''),
4014
+ });
4015
+ if (historicalHandle && historicalHandle.state !== 'cleaned') {
4016
+ const finalization: WorkHandleState['finalization'] = { validation: 'done', commit: 'done', merge: 'skipped', branchCleanup: 'skipped', worktreeCleanup: 'skipped' };
4017
+ const delivered = historicalHandle.state === 'committed' || historicalHandle.state === 'merged' || historicalHandle.state === 'failed_terminal_cleanup'
4018
+ ? historicalHandle
4019
+ : transitionWorkHandle(ctx.controllerHome, historicalHandle, 'committed', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
4020
+ transitionWorkHandle(ctx.controllerHome, delivered, 'cleaned', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
4021
+ }
4022
+ releaseControllerSession(store, workId, owner.controllerId);
4023
+ before = getWorkContract(store, workId);
4024
+ } catch (error) {
4025
+ const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Historical Work delivery reconciliation failed.', data: { workId, lifecycleClosed: false } });
4026
+ return result(blocked as unknown as Record<string, unknown>, true);
4027
+ }
4028
+ }
3656
4029
  if (before && !before.completionReceipt) {
3657
4030
  try {
3658
4031
  const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'finalize');
@@ -3708,12 +4081,28 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3708
4081
  }
3709
4082
 
3710
4083
  const facade = runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, operation as 'start' | 'continue', args);
4084
+ const facadeData = facade.data && typeof facade.data === 'object' ? facade.data as Record<string, unknown> : {};
4085
+ const facadeWorkId = contextText(contextRecord(facadeData.work).workId, 200);
4086
+ if (facade.status === 'ok' && facadeWorkId) {
4087
+ try {
4088
+ const handle = ensureFacadeWorkHandle(ctx, repository, facadeWorkId, args);
4089
+ if (handle) facadeData.executionHandle = { workId: handle.workId, checkoutId: handle.checkoutId, managedWorktree: handle.managedWorktree, state: handle.state };
4090
+ if (facadeData.workContractCreated === true) {
4091
+ const owner = claimNewFacadeWork(ctx, repository, facadeWorkId, args);
4092
+ facadeData.controllerSession = owner;
4093
+ facadeData.ownershipClaimed = true;
4094
+ }
4095
+ } catch (error) {
4096
+ const blocked = buildFacadeResult({ status: 'blocked', summary: `WORK_HANDLE_MATERIALIZATION_FAILED: ${error instanceof Error ? error.message : String(error)}`, data: { ...facadeData, workId: facadeWorkId, executionStarted: false } });
4097
+ return result(blocked as unknown as Record<string, unknown>, true);
4098
+ }
4099
+ }
3711
4100
  const response = resumedControllerSession
3712
4101
  ? {
3713
4102
  ...facade,
3714
4103
  summary: `Controller ownership resumed for ${resumedControllerSession.workId}. ${facade.summary}`,
3715
4104
  data: {
3716
- ...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
4105
+ ...facadeData,
3717
4106
  ownershipResumed: true,
3718
4107
  controllerSession: resumedControllerSession,
3719
4108
  },
@@ -3781,6 +4170,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3781
4170
  const accepted = acceptSubmittedWorkContract(ctx.controllerHome, {
3782
4171
  requestId,
3783
4172
  repoId: repository.repoId,
4173
+ parentWorkId: typeof args.parent_work_id === 'string' && args.parent_work_id.trim() ? args.parent_work_id.trim() : undefined,
3784
4174
  semanticKey,
3785
4175
  operation: {
3786
4176
  name: operation,
@@ -5254,9 +5644,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5254
5644
  case 'recovery.workspace_auth_login_prepare':
5255
5645
  payload = { skipped: true, nextTool: 'workspace_auth_login_prepare', reason: 'Auth login is a non-secret handoff and should be prepared through the dedicated typed tool.' };
5256
5646
  break;
5257
- case 'recovery.browser_domain_access_preview':
5258
- payload = { skipped: true, nextTool: 'web_domain_access_preview', reason: 'Browser access must be granted by domain key before snapshot or interaction.' };
5259
- break;
5260
5647
  case 'recovery.external_filesystem_grant_preview':
5261
5648
  payload = { skipped: true, nextTool: 'external_filesystem_grant_preview', reason: 'External filesystem access must be converted into a named read-only target first.' };
5262
5649
  break;
@@ -5489,6 +5876,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5489
5876
  pluginId,
5490
5877
  actionId,
5491
5878
  requestId,
5879
+ workId: typeof args.work_id === 'string' && args.work_id.trim() ? args.work_id.trim() : undefined,
5492
5880
  args: actionArguments,
5493
5881
  timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
5494
5882
  signal: ctx.signal,
@@ -5500,7 +5888,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5500
5888
  const action = manifest.actions.find((entry) => entry.actionId === actionId);
5501
5889
  if (action && isDirectPluginReadAction(action)) {
5502
5890
  const direct = await executeAssistantPluginReadDirect(ctx.controllerHome, repository, request);
5503
- return result({
5891
+ const value = {
5504
5892
  accepted: true,
5505
5893
  direct: true,
5506
5894
  durable: false,
@@ -5521,10 +5909,11 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5521
5909
  },
5522
5910
  },
5523
5911
  next: 'Continue with the returned bounded result; use rh_context capability detail only when the typed action schema/policy is needed.',
5524
- });
5912
+ };
5913
+ return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, direct.result);
5525
5914
  }
5526
5915
  const submitted = await submitAssistantPluginAction(ctx.controllerHome, repository, request);
5527
- return result({
5916
+ const value = {
5528
5917
  accepted: true,
5529
5918
  deduplicated: submitted.deduplicated,
5530
5919
  direct: true,
@@ -5539,6 +5928,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5539
5928
  scope: repository.repoId === '__controller__' ? 'controller' : 'repository',
5540
5929
  receiptId: submitted.receipt.receiptId,
5541
5930
  requestId: submitted.receipt.requestId,
5931
+ ...(submitted.receipt.workId ? { workId: submitted.receipt.workId } : {}),
5542
5932
  result: submitted.result,
5543
5933
  detail: {
5544
5934
  tool: 'rh_context',
@@ -5549,7 +5939,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5549
5939
  },
5550
5940
  },
5551
5941
  next: 'Continue with the returned bounded plugin result; use rh_context capability detail only when the typed action schema/policy is needed.',
5552
- });
5942
+ };
5943
+ return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, submitted.result);
5553
5944
  }
5554
5945
  case 'toolchain_plugin_summary': {
5555
5946
  const pluginId = String(args.plugin_id ?? '').trim();
@@ -5559,97 +5950,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5559
5950
  plugin: summarizePluginForLowInterception(manifest),
5560
5951
  nonOpaque: true,
5561
5952
  next: manifest.pluginId === 'browser'
5562
- ? 'Use web_targets_list, web_domain_access_preview, or web_target_snapshot instead of raw browser action names.'
5953
+ ? 'Use rh_context for browser capability schemas and plugin_action_execute for typed HTTP(S) browser actions.'
5563
5954
  : undefined,
5564
5955
  });
5565
5956
  }
5566
- case 'web_targets_list': {
5567
- const repository = selected(ctx, args);
5568
- const manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'browser');
5569
- return result({
5570
- pluginId: 'browser',
5571
- enabled: manifest.enabled,
5572
- healthState: manifest.health.state,
5573
- ready: manifest.health.ready,
5574
- targets: listWebTargets(repository.canonicalRoot, manifest),
5575
- safety: {
5576
- arbitraryUrlAccepted: false,
5577
- returnsRawConfig: false,
5578
- nextTool: 'web_target_snapshot',
5579
- },
5580
- });
5581
- }
5582
- case 'web_target_snapshot': {
5583
- const repository = selected(ctx, args);
5584
- const manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'browser');
5585
- const url = resolveWebTargetUrl(repository.canonicalRoot, String(args.target_key ?? '').trim(), args.path, args.query, manifest);
5586
- const capture = args.capture === 'screenshot' ? 'screenshot' : args.capture === 'text' ? 'text' : 'title';
5587
- const actionId = capture === 'screenshot' ? 'screenshot' : 'open_page';
5588
- const actionArguments: Record<string, unknown> = {
5589
- url,
5590
- wait_until: typeof args.wait_until === 'string' ? args.wait_until : 'domcontentloaded',
5591
- timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
5592
- ...(capture === 'text' ? { extract_text: true, max_chars: typeof args.max_chars === 'number' ? args.max_chars : 4000 } : {}),
5593
- ...(capture === 'screenshot' ? { full_page: args.full_page === true } : {}),
5594
- };
5595
- const requestId = String(args.request_id ?? '').trim();
5596
- if (!requestId) throw new Error('REQUEST_ID_REQUIRED: web_target_snapshot requires request_id');
5597
- const submitted = await submitAssistantPluginAction(ctx.controllerHome, repository, {
5598
- pluginId: 'browser',
5599
- actionId,
5600
- requestId,
5601
- args: actionArguments,
5602
- timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
5603
- origin: { surface: 'mcp', actor: 'web_target_snapshot', correlationId: requestId },
5604
- });
5605
- return result({
5606
- accepted: true,
5607
- deduplicated: submitted.deduplicated,
5608
- webTarget: {
5609
- targetKey: String(args.target_key ?? '').trim(),
5610
- capture,
5611
- path: typeof args.path === 'string' ? args.path : '/',
5612
- arbitraryUrlAccepted: false,
5613
- },
5614
- job: summarizeWork(submitted.job, repository.canonicalRoot),
5615
- next: `Call work_status_digest with work_ref ${submitted.job.jobId}.`,
5616
- });
5617
- }
5618
- case 'web_domain_access_preview': {
5619
- const repository = selected(ctx, args);
5620
- const manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'browser');
5621
- return result({
5622
- preview: previewBrowserDomainAccess(repository.canonicalRoot, args.domain, args.reason, manifest),
5623
- next: 'After human review, call web_domain_access_apply with confirm_authorization=true.',
5624
- });
5625
- }
5626
- case 'web_domain_access_apply': {
5627
- const repository = selected(ctx, args);
5628
- if (args.confirm_authorization !== true) throw new Error('CONFIRM_AUTHORIZATION_REQUIRED: web_domain_access_apply requires confirm_authorization=true');
5629
- const manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'browser');
5630
- const preview = previewBrowserDomainAccess(repository.canonicalRoot, args.domain, args.reason, manifest);
5631
- const providedTicket = typeof args.preview_ticket_id === 'string' ? args.preview_ticket_id.trim() : '';
5632
- if (providedTicket && providedTicket !== preview.ticketId) throw new Error('DOMAIN_ACCESS_TICKET_MISMATCH');
5633
- const requestId = String(args.request_id ?? '').trim();
5634
- if (!requestId) throw new Error('REQUEST_ID_REQUIRED: web_domain_access_apply requires request_id');
5635
- const allowedDomains = mergeAllowedDomains(repository.canonicalRoot, args.domain, manifest);
5636
- const submitted = await submitAssistantPluginAction(ctx.controllerHome, repository, {
5637
- pluginId: 'browser',
5638
- actionId: 'configure',
5639
- requestId,
5640
- args: { enabled: true, allowed_domains: allowedDomains },
5641
- confirmAuthorization: true,
5642
- origin: { surface: 'mcp', actor: 'web_domain_access_apply', correlationId: requestId },
5643
- });
5644
- return result({
5645
- accepted: true,
5646
- deduplicated: submitted.deduplicated,
5647
- preview,
5648
- allowedDomainCount: allowedDomains.length,
5649
- job: summarizeWork(submitted.job, repository.canonicalRoot),
5650
- next: `Call work_status_digest with work_ref ${submitted.job.jobId}.`,
5651
- });
5652
- }
5653
5957
  case 'work_result_summary': {
5654
5958
  const repository = selected(ctx, args);
5655
5959
  const jobId = String(args.job_id ?? '').trim();