@moretea-labs/forge 1.7.0 → 1.7.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 (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +85 -89
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +1 -1
  166. package/src/runtime/root/package-runtime-service.ts +33 -2
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -12,6 +12,7 @@ import {
12
12
  writeControlPlaneRecordWithinTransaction,
13
13
  } from '../persistence/sqlite-store';
14
14
  import { assertWorkAdmissionAllowed } from './work-admission-policy';
15
+ import { phaseIndex, suggestedActionsForStatus, transitionPhaseEvidence, validateWorkSemanticTransition, validateWorkSemantics } from './work-state-machine';
15
16
  import {
16
17
  type EvidenceRef,
17
18
  type CompletionOutcome,
@@ -32,7 +33,6 @@ import {
32
33
  type WorkPhaseEvidenceState,
33
34
  type WorkContractStore,
34
35
  isDirectEditWorkCompletionReceipt,
35
- isRemoteEffectCompletionReceipt,
36
36
  isRepositoryCompletionReceipt,
37
37
  isTerminalWorkContractStatus,
38
38
  } from './types';
@@ -173,10 +173,6 @@ function inferredEvidenceState(status: WorkContractStatus): EvidenceState {
173
173
  return 'none';
174
174
  }
175
175
 
176
- function phaseIndex(phase: WorkPhase): number {
177
- return ['implementation', 'verification', 'delivery', 'cleanup'].indexOf(phase);
178
- }
179
-
180
176
  function legacyPhaseEvidence(
181
177
  contract: Pick<WorkContract, 'phase' | 'status' | 'evidenceRefs' | 'completionReceipt' | 'updatedAt'>,
182
178
  source: WorkPhaseEvidence['source'] = 'legacy_inferred',
@@ -207,210 +203,6 @@ function legacyPhaseEvidence(
207
203
  })) as WorkPhaseEvidenceMap;
208
204
  }
209
205
 
210
- function transitionPhaseEvidence(
211
- current: Pick<WorkContract, 'phaseEvidence' | 'evidenceRefs'>,
212
- targetPhase: WorkPhase,
213
- input: {
214
- status: WorkContractStatus;
215
- summary: string;
216
- evidenceRefs?: EvidenceRef[];
217
- recordedAt: string;
218
- source?: WorkPhaseEvidence['source'];
219
- },
220
- ): WorkPhaseEvidenceMap {
221
- const targetIndex = phaseIndex(targetPhase);
222
- const evidenceRefs = (input.evidenceRefs ?? current.evidenceRefs).slice(0, 20);
223
- const source = input.source ?? 'recorded';
224
- const phaseEvidence = { ...current.phaseEvidence };
225
- for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
226
- const index = phaseIndex(phase);
227
- if (index < targetIndex) {
228
- phaseEvidence[phase] = {
229
- state: ['satisfied', 'skipped'].includes(phaseEvidence[phase].state) ? phaseEvidence[phase].state : 'satisfied',
230
- source,
231
- summary: `Advanced to ${targetPhase}: ${input.summary}`.slice(0, 1_000),
232
- evidenceRefs,
233
- recordedAt: input.recordedAt,
234
- };
235
- } else if (index > targetIndex) {
236
- phaseEvidence[phase] = {
237
- state: 'pending',
238
- source,
239
- summary: `Waiting for Work phase ${phase}.`,
240
- evidenceRefs: [],
241
- recordedAt: input.recordedAt,
242
- };
243
- }
244
- }
245
- phaseEvidence[targetPhase] = {
246
- state: input.status === 'failed'
247
- ? 'failed'
248
- : input.status === 'cancelled'
249
- ? 'skipped'
250
- : input.status === 'blocked' || input.status === 'ready'
251
- ? 'blocked'
252
- : 'active',
253
- source,
254
- summary: input.summary.trim().slice(0, 1_000) || `Work entered ${targetPhase}.`,
255
- evidenceRefs,
256
- recordedAt: input.recordedAt,
257
- };
258
- return phaseEvidence;
259
- }
260
-
261
- function validateWorkSemantics(contract: WorkContract): WorkContract {
262
- if (!contract.objective.trim()) throw new Error('WORK_OBJECTIVE_REQUIRED');
263
- if (contract.status === 'completed' && !contract.completionReceipt) {
264
- throw new Error('WORK_COMPLETION_RECEIPT_REQUIRED');
265
- }
266
- const currentPhaseIndex = phaseIndex(contract.phase);
267
- for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
268
- const checkpoint = contract.phaseEvidence?.[phase];
269
- if (!checkpoint) throw new Error(`WORK_PHASE_EVIDENCE_REQUIRED: ${phase}`);
270
- const index = phaseIndex(phase);
271
- if (index < currentPhaseIndex && !['satisfied', 'skipped'].includes(checkpoint.state)) {
272
- throw new Error(`WORK_PHASE_EVIDENCE_PREVIOUS_NOT_SATISFIED: ${phase}`);
273
- }
274
- if (index === currentPhaseIndex && checkpoint.state === 'pending') {
275
- throw new Error(`WORK_PHASE_EVIDENCE_REQUIRED: ${phase}`);
276
- }
277
- if (index > currentPhaseIndex && checkpoint.state !== 'pending') {
278
- throw new Error(`WORK_PHASE_EVIDENCE_FUTURE_NOT_PENDING: ${phase}`);
279
- }
280
- }
281
- if (contract.completionReceipt) {
282
- const receipt = contract.completionReceipt;
283
- if (receipt.workId !== contract.workId) throw new Error('WORK_COMPLETION_RECEIPT_IDENTITY_MISMATCH');
284
- if (isRepositoryCompletionReceipt(receipt)) {
285
- if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
286
- if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
287
- if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
288
- } else if (isDirectEditWorkCompletionReceipt(receipt)) {
289
- const directEditAuthorityCount = Number(Boolean(receipt.editSessionId?.trim())) + Number(Boolean(receipt.reconciliationId?.trim()));
290
- if (directEditAuthorityCount !== 1) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_AUTHORITY_REQUIRED');
291
- if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
292
- if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
293
- if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
294
- if (!['repository_change', 'completed_no_change'].includes(contract.workKind)) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_KIND_REQUIRED');
295
- const expectedOutcome = contract.workKind === 'completed_no_change' ? 'completed_no_change' : 'completed_changed';
296
- if (contract.completionOutcome !== expectedOutcome) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_OUTCOME_REQUIRED');
297
- } else if (isRemoteEffectCompletionReceipt(receipt)) {
298
- if (contract.workKind !== 'remote_effect') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_KIND_REQUIRED');
299
- if (contract.completionOutcome !== 'completed_remote') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_OUTCOME_REQUIRED');
300
- if (!receipt.pluginId.trim() || !receipt.actionId.trim() || !receipt.requestId.trim() || !receipt.semanticKey.trim() || !receipt.resultDigest.trim()) {
301
- throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_IDENTITY_REQUIRED');
302
- }
303
- } else {
304
- if (contract.workKind !== 'local_effect') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_KIND_REQUIRED');
305
- if (contract.completionOutcome !== 'completed_local') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_OUTCOME_REQUIRED');
306
- if (!receipt.operation.trim() || !receipt.target.id.trim()) throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_TARGET_REQUIRED');
307
- }
308
- if (contract.status !== 'completed') throw new Error('WORK_COMPLETION_RECEIPT_REQUIRES_COMPLETED_WORK');
309
- for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
310
- if (!['satisfied', 'skipped'].includes(contract.phaseEvidence[phase].state)) {
311
- throw new Error(`WORK_COMPLETION_PHASE_NOT_SATISFIED: ${phase}`);
312
- }
313
- }
314
- for (const phase of ['delivery', 'cleanup'] as WorkPhase[]) {
315
- if (contract.phaseEvidence[phase].receiptId !== receipt.receiptId) {
316
- throw new Error(`WORK_COMPLETION_PHASE_RECEIPT_MISMATCH: ${phase}`);
317
- }
318
- }
319
- }
320
- const outcome = contract.completionOutcome;
321
- if (!outcome) return contract;
322
- if (contract.status !== 'completed' || !contract.completionReceipt) {
323
- throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires a completed Work receipt`);
324
- }
325
- if (contract.dispatchState !== 'terminal') {
326
- throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires terminal dispatch`);
327
- }
328
- if (contract.evidenceState !== 'valid') {
329
- throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires valid evidence`);
330
- }
331
- if (outcome === 'completed_changed' && contract.workKind !== 'repository_change') {
332
- throw new Error('WORK_SEMANTICS_INVALID: completed_changed requires repository_change WorkKind');
333
- }
334
- if (outcome === 'completed_no_change' && contract.workKind !== 'completed_no_change') {
335
- throw new Error('WORK_SEMANTICS_INVALID: completed_no_change requires completed_no_change WorkKind');
336
- }
337
- if (outcome === 'completed_local' && contract.workKind !== 'local_effect') {
338
- throw new Error('WORK_SEMANTICS_INVALID: completed_local requires local_effect WorkKind');
339
- }
340
- if (outcome === 'completed_local' && contract.completionReceipt?.source !== 'local_effect') {
341
- throw new Error('WORK_SEMANTICS_INVALID: completed_local requires a local_effect completion receipt');
342
- }
343
- if (outcome === 'completed_remote' && contract.workKind !== 'remote_effect') {
344
- throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires remote_effect WorkKind');
345
- }
346
- if (outcome === 'completed_remote' && contract.completionReceipt?.source !== 'remote_effect') {
347
- throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires a remote_effect completion receipt');
348
- }
349
- if (outcome === 'superseded' && contract.workKind !== 'superseded') {
350
- throw new Error('WORK_SEMANTICS_INVALID: superseded requires superseded WorkKind');
351
- }
352
- return contract;
353
- }
354
-
355
- const DISPATCH_TRANSITIONS: Readonly<Record<DispatchState, readonly DispatchState[]>> = {
356
- not_dispatched: ['not_dispatched', 'claimed', 'launching', 'running', 'blocked', 'terminal'],
357
- claimed: ['claimed', 'launching', 'running', 'blocked', 'terminal'],
358
- launching: ['launching', 'running', 'blocked', 'terminal'],
359
- running: ['running', 'blocked', 'terminal'],
360
- blocked: ['blocked', 'claimed', 'launching', 'running', 'terminal'],
361
- terminal: ['terminal'],
362
- };
363
-
364
- const EVIDENCE_TRANSITIONS: Readonly<Record<EvidenceState, readonly EvidenceState[]>> = {
365
- none: ['none', 'partial', 'valid', 'failed'],
366
- partial: ['partial', 'valid', 'stale', 'contradictory', 'failed'],
367
- valid: ['valid', 'stale', 'contradictory', 'failed'],
368
- stale: ['stale', 'partial', 'valid', 'contradictory', 'failed'],
369
- contradictory: ['contradictory', 'partial', 'valid', 'failed'],
370
- failed: ['failed', 'partial', 'valid', 'contradictory'],
371
- };
372
-
373
- function validateWorkSemanticTransition(
374
- current: WorkContract,
375
- next: WorkContract,
376
- options: { allowRetainedCancelledResume?: boolean } = {},
377
- ): WorkContract {
378
- const retryingFailedWork = current.status === 'failed'
379
- && !current.completionOutcome
380
- && ['claimed', 'launching', 'running', 'blocked'].includes(next.dispatchState);
381
- const resumingRetainedCancelledWork = options.allowRetainedCancelledResume === true
382
- && current.status === 'cancelled'
383
- && current.dispatchState === 'terminal'
384
- && current.phase === 'cleanup'
385
- && current.phaseEvidence.cleanup.state === 'skipped'
386
- && !current.completionReceipt
387
- && !current.completionOutcome
388
- && next.status === 'running'
389
- && next.dispatchState === 'running'
390
- && next.phase === 'implementation';
391
- if (!retryingFailedWork && !resumingRetainedCancelledWork && !DISPATCH_TRANSITIONS[current.dispatchState].includes(next.dispatchState)) {
392
- throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: dispatch ${current.dispatchState} -> ${next.dispatchState}`);
393
- }
394
- if (!EVIDENCE_TRANSITIONS[current.evidenceState].includes(next.evidenceState)) {
395
- throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: evidence ${current.evidenceState} -> ${next.evidenceState}`);
396
- }
397
- if (current.completionOutcome && current.completionOutcome !== next.completionOutcome) {
398
- throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: completion outcome ${current.completionOutcome} is immutable`);
399
- }
400
- if ((current.lifecycleRole ?? 'primary') !== (next.lifecycleRole ?? 'primary')) {
401
- throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: lifecycleRole is immutable');
402
- }
403
- if (current.parentWorkId !== next.parentWorkId) {
404
- throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: parentWorkId is immutable');
405
- }
406
- return next;
407
- }
408
-
409
- function suggestedActionsForStatus(status: WorkContractStatus, actions: readonly SuggestedNextAction[]): SuggestedNextAction[] {
410
- if (status === 'completed' || status === 'cancelled') return [];
411
- return actions.slice(0, 8);
412
- }
413
-
414
206
  function sqliteBacked(options: WorkContractStoreOptions): options is WorkContractStoreOptions & { controllerHome: string; repoId: string } {
415
207
  // A caller-provided root is a test/portable compatibility store. Runtime
416
208
  // controller state always carries controllerHome + repoId and is SQLite.
@@ -805,6 +597,8 @@ function wasPausedByRuntimeReconciliation(contract: WorkContract): boolean {
805
597
 
806
598
  function hasReviewableWorkOutput(contract: WorkContract): boolean {
807
599
  if (contract.checkRefs.length > 0 || contract.handoffRefs.length > 0) return true;
600
+ if ((contract.scopeEvidence?.inspectedPaths.length ?? 0) > 0) return true;
601
+ if ((contract.readOnlyReviewEvidence?.inspectedPaths.length ?? 0) > 0 || (contract.readOnlyReviewEvidence?.findings.length ?? 0) > 0) return true;
808
602
  return contract.evidenceRefs.some((evidence) =>
809
603
  Boolean(evidence.artifactId || evidence.evidenceId)
810
604
  || !SYSTEM_ONLY_EVIDENCE_TITLES.has(evidence.title));
@@ -0,0 +1,250 @@
1
+ import {
2
+ isDirectEditWorkCompletionReceipt,
3
+ isReadOnlyReviewCompletionReceipt,
4
+ isRemoteEffectCompletionReceipt,
5
+ isRepositoryCompletionReceipt,
6
+ type DispatchState,
7
+ type EvidenceRef,
8
+ type EvidenceState,
9
+ type SuggestedNextAction,
10
+ type WorkContract,
11
+ type WorkContractStatus,
12
+ type WorkPhase,
13
+ type WorkPhaseEvidence,
14
+ type WorkPhaseEvidenceMap,
15
+ } from './types';
16
+
17
+ export function phaseIndex(phase: WorkPhase): number {
18
+ return ['implementation', 'verification', 'delivery', 'cleanup'].indexOf(phase);
19
+ }
20
+
21
+ export function transitionPhaseEvidence(
22
+ current: Pick<WorkContract, 'phaseEvidence' | 'evidenceRefs'>,
23
+ targetPhase: WorkPhase,
24
+ input: {
25
+ status: WorkContractStatus;
26
+ summary: string;
27
+ evidenceRefs?: EvidenceRef[];
28
+ recordedAt: string;
29
+ source?: WorkPhaseEvidence['source'];
30
+ },
31
+ ): WorkPhaseEvidenceMap {
32
+ const targetIndex = phaseIndex(targetPhase);
33
+ const evidenceRefs = (input.evidenceRefs ?? current.evidenceRefs).slice(0, 20);
34
+ const source = input.source ?? 'recorded';
35
+ const phaseEvidence = { ...current.phaseEvidence };
36
+ for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
37
+ const index = phaseIndex(phase);
38
+ if (index < targetIndex) {
39
+ phaseEvidence[phase] = {
40
+ state: ['satisfied', 'skipped'].includes(phaseEvidence[phase].state) ? phaseEvidence[phase].state : 'satisfied',
41
+ source,
42
+ summary: `Advanced to ${targetPhase}: ${input.summary}`.slice(0, 1_000),
43
+ evidenceRefs,
44
+ recordedAt: input.recordedAt,
45
+ };
46
+ } else if (index > targetIndex) {
47
+ phaseEvidence[phase] = {
48
+ state: 'pending',
49
+ source,
50
+ summary: `Waiting for Work phase ${phase}.`,
51
+ evidenceRefs: [],
52
+ recordedAt: input.recordedAt,
53
+ };
54
+ }
55
+ }
56
+ phaseEvidence[targetPhase] = {
57
+ state: input.status === 'failed'
58
+ ? 'failed'
59
+ : input.status === 'cancelled'
60
+ ? 'skipped'
61
+ : input.status === 'blocked' || input.status === 'ready'
62
+ ? 'blocked'
63
+ : 'active',
64
+ source,
65
+ summary: input.summary.trim().slice(0, 1_000) || `Work entered ${targetPhase}.`,
66
+ evidenceRefs,
67
+ recordedAt: input.recordedAt,
68
+ };
69
+ return phaseEvidence;
70
+ }
71
+
72
+ export function validateWorkSemantics(contract: WorkContract): WorkContract {
73
+ if (!contract.objective.trim()) throw new Error('WORK_OBJECTIVE_REQUIRED');
74
+ if (contract.status === 'completed' && !contract.completionReceipt) {
75
+ throw new Error('WORK_COMPLETION_RECEIPT_REQUIRED');
76
+ }
77
+ const currentPhaseIndex = phaseIndex(contract.phase);
78
+ for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
79
+ const checkpoint = contract.phaseEvidence?.[phase];
80
+ if (!checkpoint) throw new Error(`WORK_PHASE_EVIDENCE_REQUIRED: ${phase}`);
81
+ const index = phaseIndex(phase);
82
+ if (index < currentPhaseIndex && !['satisfied', 'skipped'].includes(checkpoint.state)) {
83
+ throw new Error(`WORK_PHASE_EVIDENCE_PREVIOUS_NOT_SATISFIED: ${phase}`);
84
+ }
85
+ if (index === currentPhaseIndex && checkpoint.state === 'pending') {
86
+ throw new Error(`WORK_PHASE_EVIDENCE_REQUIRED: ${phase}`);
87
+ }
88
+ if (index > currentPhaseIndex && checkpoint.state !== 'pending') {
89
+ throw new Error(`WORK_PHASE_EVIDENCE_FUTURE_NOT_PENDING: ${phase}`);
90
+ }
91
+ }
92
+ if (contract.completionReceipt) {
93
+ const receipt = contract.completionReceipt;
94
+ if (receipt.workId !== contract.workId) throw new Error('WORK_COMPLETION_RECEIPT_IDENTITY_MISMATCH');
95
+ if (isRepositoryCompletionReceipt(receipt)) {
96
+ if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
97
+ if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
98
+ if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
99
+ } else if (isDirectEditWorkCompletionReceipt(receipt)) {
100
+ const directEditAuthorityCount = Number(Boolean(receipt.editSessionId?.trim())) + Number(Boolean(receipt.reconciliationId?.trim()));
101
+ if (directEditAuthorityCount !== 1) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_AUTHORITY_REQUIRED');
102
+ if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
103
+ if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
104
+ if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
105
+ if (!['repository_change', 'completed_no_change'].includes(contract.workKind)) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_KIND_REQUIRED');
106
+ const expectedOutcome = contract.workKind === 'completed_no_change' ? 'completed_no_change' : 'completed_changed';
107
+ if (contract.completionOutcome !== expectedOutcome) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_OUTCOME_REQUIRED');
108
+ } else if (isReadOnlyReviewCompletionReceipt(receipt)) {
109
+ if (contract.workKind !== 'read_only_review') throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_KIND_REQUIRED');
110
+ if (contract.completionOutcome !== 'completed_no_change') throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_OUTCOME_REQUIRED');
111
+ if (!receipt.baseRevision.trim() || receipt.baseRevision !== receipt.sourceRevision) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_SOURCE_REQUIRED');
112
+ if (contract.baseRevision && receipt.baseRevision !== contract.baseRevision) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_BASE_MISMATCH');
113
+ if (receipt.workspaceChangedPaths.length !== 0) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_WORKSPACE_CHANGED');
114
+ const reviewEvidence = contract.readOnlyReviewEvidence;
115
+ if (!reviewEvidence || reviewEvidence.sourceRevision !== receipt.sourceRevision) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_EVIDENCE_REQUIRED');
116
+ if (reviewEvidence.findings.length !== 0 || receipt.findingCount !== 0 || receipt.inspectedPaths.length === 0) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_CLEAN_SCOPE_REQUIRED');
117
+ const evidencePaths = [...new Set(reviewEvidence.inspectedPaths)].sort();
118
+ const receiptPaths = [...new Set(receipt.inspectedPaths)].sort();
119
+ if (evidencePaths.length !== receiptPaths.length || evidencePaths.some((path, index) => path !== receiptPaths[index])) {
120
+ throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_SCOPE_MISMATCH');
121
+ }
122
+ if (reviewEvidence.workspaceFingerprint && receipt.workspaceFingerprint && reviewEvidence.workspaceFingerprint !== receipt.workspaceFingerprint) {
123
+ throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_WORKSPACE_MISMATCH');
124
+ }
125
+ } else if (isRemoteEffectCompletionReceipt(receipt)) {
126
+ if (contract.workKind !== 'remote_effect') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_KIND_REQUIRED');
127
+ if (contract.completionOutcome !== 'completed_remote') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_OUTCOME_REQUIRED');
128
+ const authority = receipt.authority ?? 'plugin_action';
129
+ if (!receipt.actionId.trim() || !receipt.requestId.trim() || !receipt.semanticKey.trim() || !receipt.resultDigest.trim()) {
130
+ throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_IDENTITY_REQUIRED');
131
+ }
132
+ if (authority === 'plugin_action' && !receipt.pluginId?.trim()) {
133
+ throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_PLUGIN_IDENTITY_REQUIRED');
134
+ }
135
+ if (authority === 'repository_process' && !receipt.processId?.trim()) {
136
+ throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_PROCESS_IDENTITY_REQUIRED');
137
+ }
138
+ } else {
139
+ if (contract.workKind !== 'local_effect') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_KIND_REQUIRED');
140
+ if (contract.completionOutcome !== 'completed_local') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_OUTCOME_REQUIRED');
141
+ if (!receipt.operation.trim() || !receipt.target.id.trim()) throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_TARGET_REQUIRED');
142
+ }
143
+ if (contract.status !== 'completed') throw new Error('WORK_COMPLETION_RECEIPT_REQUIRES_COMPLETED_WORK');
144
+ for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
145
+ if (!['satisfied', 'skipped'].includes(contract.phaseEvidence[phase].state)) {
146
+ throw new Error(`WORK_COMPLETION_PHASE_NOT_SATISFIED: ${phase}`);
147
+ }
148
+ }
149
+ for (const phase of ['delivery', 'cleanup'] as WorkPhase[]) {
150
+ if (contract.phaseEvidence[phase].receiptId !== receipt.receiptId) {
151
+ throw new Error(`WORK_COMPLETION_PHASE_RECEIPT_MISMATCH: ${phase}`);
152
+ }
153
+ }
154
+ }
155
+ const outcome = contract.completionOutcome;
156
+ if (!outcome) return contract;
157
+ if (contract.status !== 'completed' || !contract.completionReceipt) {
158
+ throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires a completed Work receipt`);
159
+ }
160
+ if (contract.dispatchState !== 'terminal') {
161
+ throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires terminal dispatch`);
162
+ }
163
+ if (contract.evidenceState !== 'valid') {
164
+ throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires valid evidence`);
165
+ }
166
+ if (outcome === 'completed_changed' && contract.workKind !== 'repository_change') {
167
+ throw new Error('WORK_SEMANTICS_INVALID: completed_changed requires repository_change WorkKind');
168
+ }
169
+ if (outcome === 'completed_no_change' && !['completed_no_change', 'read_only_review'].includes(contract.workKind)) {
170
+ throw new Error('WORK_SEMANTICS_INVALID: completed_no_change requires completed_no_change or read_only_review WorkKind');
171
+ }
172
+ if (outcome === 'completed_no_change' && contract.workKind === 'read_only_review' && contract.completionReceipt?.source !== 'read_only_review') {
173
+ throw new Error('WORK_SEMANTICS_INVALID: read_only_review completed_no_change requires a read_only_review completion receipt');
174
+ }
175
+ if (outcome === 'completed_local' && contract.workKind !== 'local_effect') {
176
+ throw new Error('WORK_SEMANTICS_INVALID: completed_local requires local_effect WorkKind');
177
+ }
178
+ if (outcome === 'completed_local' && contract.completionReceipt?.source !== 'local_effect') {
179
+ throw new Error('WORK_SEMANTICS_INVALID: completed_local requires a local_effect completion receipt');
180
+ }
181
+ if (outcome === 'completed_remote' && contract.workKind !== 'remote_effect') {
182
+ throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires remote_effect WorkKind');
183
+ }
184
+ if (outcome === 'completed_remote' && contract.completionReceipt?.source !== 'remote_effect') {
185
+ throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires a remote_effect completion receipt');
186
+ }
187
+ if (outcome === 'superseded' && contract.workKind !== 'superseded') {
188
+ throw new Error('WORK_SEMANTICS_INVALID: superseded requires superseded WorkKind');
189
+ }
190
+ return contract;
191
+ }
192
+
193
+ const DISPATCH_TRANSITIONS: Readonly<Record<DispatchState, readonly DispatchState[]>> = {
194
+ not_dispatched: ['not_dispatched', 'claimed', 'launching', 'running', 'blocked', 'terminal'],
195
+ claimed: ['claimed', 'launching', 'running', 'blocked', 'terminal'],
196
+ launching: ['launching', 'running', 'blocked', 'terminal'],
197
+ running: ['running', 'blocked', 'terminal'],
198
+ blocked: ['blocked', 'claimed', 'launching', 'running', 'terminal'],
199
+ terminal: ['terminal'],
200
+ };
201
+
202
+ const EVIDENCE_TRANSITIONS: Readonly<Record<EvidenceState, readonly EvidenceState[]>> = {
203
+ none: ['none', 'partial', 'valid', 'failed'],
204
+ partial: ['partial', 'valid', 'stale', 'contradictory', 'failed'],
205
+ valid: ['valid', 'stale', 'contradictory', 'failed'],
206
+ stale: ['stale', 'partial', 'valid', 'contradictory', 'failed'],
207
+ contradictory: ['contradictory', 'partial', 'valid', 'failed'],
208
+ failed: ['failed', 'partial', 'valid', 'contradictory'],
209
+ };
210
+
211
+ export function validateWorkSemanticTransition(
212
+ current: WorkContract,
213
+ next: WorkContract,
214
+ options: { allowRetainedCancelledResume?: boolean } = {},
215
+ ): WorkContract {
216
+ const retryingFailedWork = current.status === 'failed'
217
+ && !current.completionOutcome
218
+ && ['claimed', 'launching', 'running', 'blocked'].includes(next.dispatchState);
219
+ const resumingRetainedCancelledWork = options.allowRetainedCancelledResume === true
220
+ && current.status === 'cancelled'
221
+ && current.dispatchState === 'terminal'
222
+ && current.phase === 'cleanup'
223
+ && current.phaseEvidence.cleanup.state === 'skipped'
224
+ && !current.completionReceipt
225
+ && !current.completionOutcome
226
+ && next.status === 'running'
227
+ && next.dispatchState === 'running'
228
+ && next.phase === 'implementation';
229
+ if (!retryingFailedWork && !resumingRetainedCancelledWork && !DISPATCH_TRANSITIONS[current.dispatchState].includes(next.dispatchState)) {
230
+ throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: dispatch ${current.dispatchState} -> ${next.dispatchState}`);
231
+ }
232
+ if (!EVIDENCE_TRANSITIONS[current.evidenceState].includes(next.evidenceState)) {
233
+ throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: evidence ${current.evidenceState} -> ${next.evidenceState}`);
234
+ }
235
+ if (current.completionOutcome && current.completionOutcome !== next.completionOutcome) {
236
+ throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: completion outcome ${current.completionOutcome} is immutable`);
237
+ }
238
+ if ((current.lifecycleRole ?? 'primary') !== (next.lifecycleRole ?? 'primary')) {
239
+ throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: lifecycleRole is immutable');
240
+ }
241
+ if (current.parentWorkId !== next.parentWorkId) {
242
+ throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: parentWorkId is immutable');
243
+ }
244
+ return next;
245
+ }
246
+
247
+ export function suggestedActionsForStatus(status: WorkContractStatus, actions: readonly SuggestedNextAction[]): SuggestedNextAction[] {
248
+ if (status === 'completed' || status === 'cancelled') return [];
249
+ return actions.slice(0, 8);
250
+ }
@@ -11,7 +11,7 @@ import {
11
11
  finishControllerRoundRelayDispatch,
12
12
  } from '../facade/controller-round-relay';
13
13
  import { assertAutomatedOperationAllowed } from '../governance/external-effects';
14
- import { runStandaloneChatgptPrompt } from '../launcher/chatgpt-work-continuation';
14
+ import { runWorkChatgptContinuation } from '../launcher/chatgpt-work-continuation';
15
15
 
16
16
  export async function runSchedulerPeriodicCleanup(input: {
17
17
  controllerHome: string;
@@ -83,13 +83,13 @@ export async function runSchedulerValidationReconciliation(input: {
83
83
  export async function runSchedulerControllerRoundRecovery(input: {
84
84
  controllerHome: string;
85
85
  nowMs: number;
86
- repositories: readonly Pick<RepositoryRecord, 'repoId'>[];
86
+ repositories: readonly Pick<RepositoryRecord, 'repoId' | 'canonicalRoot' | 'localRoot'>[];
87
87
  graceMs?: number;
88
88
  maxRecoveries?: number;
89
- dispatchPrompt?: typeof runStandaloneChatgptPrompt;
89
+ dispatchPrompt?: typeof runWorkChatgptContinuation;
90
90
  authorizeWake?: typeof assertAutomatedOperationAllowed;
91
91
  }): Promise<{ claimed: number; dispatched: number; failed: number }> {
92
- const dispatchPrompt = input.dispatchPrompt ?? runStandaloneChatgptPrompt;
92
+ const dispatchPrompt = input.dispatchPrompt ?? runWorkChatgptContinuation;
93
93
  const authorizeWake = input.authorizeWake ?? assertAutomatedOperationAllowed;
94
94
  const maxRecoveries = Math.max(1, Math.min(Math.trunc(input.maxRecoveries ?? 2), 8));
95
95
  let claimed = 0;
@@ -116,8 +116,9 @@ export async function runSchedulerControllerRoundRecovery(input: {
116
116
  const result = await dispatchPrompt({
117
117
  controllerHome: input.controllerHome,
118
118
  repoId: repository.repoId,
119
- scopeId: `controller-relay:${record.relayScopeId}`,
120
- prompt: buildControllerRoundRelayPrompt(store, record),
119
+ repoRoot: repository.canonicalRoot ?? repository.localRoot,
120
+ workId: record.originWorkId,
121
+ prompt: buildControllerRoundRelayPrompt(store, record, { exactOriginWork: true }),
121
122
  browserSessionId: record.browserSessionId,
122
123
  conversationUrl: record.conversationUrl,
123
124
  model: 'gpt-5.6',
@@ -135,7 +136,13 @@ export async function runSchedulerControllerRoundRecovery(input: {
135
136
  dispatched += 1;
136
137
  } catch (error) {
137
138
  const reason = error instanceof Error ? error.message : String(error);
138
- finishControllerRoundRelayDispatch(store, { workId: record.originWorkId, ok: false, error: reason });
139
+ finishControllerRoundRelayDispatch(store, {
140
+ workId: record.originWorkId,
141
+ ok: false,
142
+ error: reason,
143
+ recovery: true,
144
+ nowMs: input.nowMs,
145
+ });
139
146
  failed += 1;
140
147
  console.error(`[forge controller relay] stalled round recovery failed for ${record.relayScopeId}:`, reason);
141
148
  }
@@ -199,8 +199,12 @@ export class GlobalScheduler {
199
199
  ) {
200
200
  this.controllerHome = controllerHome;
201
201
  this.controllerPid = runtime.controllerPid ?? process.pid;
202
- this.actors = new RepoActorRegistry(controllerHome);
203
202
  this.config = normalizeSchedulerConfig(config);
203
+ // The per-repository mailbox must not impose a hidden concurrency ceiling
204
+ // below the scheduler's global worker budget. Resource leases still serialize
205
+ // conflicting workspace/path/integration claims, while independent jobs in a
206
+ // busy single repository may use the available global worker capacity.
207
+ this.actors = new RepoActorRegistry(controllerHome, { maxConcurrentWorkers: this.config.maxWorkers });
204
208
  this.runtimeSourceRoot = runtime.runtimeSourceRoot ? resolve(runtime.runtimeSourceRoot) : undefined;
205
209
  this.workerEntrypoint = runtime.workerEntrypoint ? resolve(runtime.workerEntrypoint) : undefined;
206
210
  this.fatalOnTickError = runtime.fatalOnTickError === true;
@@ -47,6 +47,19 @@ export function parseChatgptConversationIdentity(value: string): { conversationU
47
47
  return { conversationUrl: url.toString(), conversationId };
48
48
  }
49
49
 
50
+ export function hasChatgptConversationIdentity(value: string): boolean {
51
+ try {
52
+ parseChatgptConversationIdentity(value);
53
+ return true;
54
+ } catch (error) {
55
+ // ChatGPT root and Project URLs are valid launch seeds, but they are not
56
+ // durable conversation identities. Persist only the /c/<id> URL observed
57
+ // after the browser confirms submission.
58
+ if (error instanceof Error && error.message === 'CHATGPT_WORK_CONVERSATION_ID_MISSING') return false;
59
+ throw error;
60
+ }
61
+ }
62
+
50
63
  function record(options: ChatgptWorkBindingStoreOptions, workId: string) {
51
64
  return readControlPlaneRecord<ChatgptWorkConversationBinding>(
52
65
  options.controllerHome,
@@ -5,6 +5,7 @@ import { getWorkContract } from '../facade/work-contract-store';
5
5
  import {
6
6
  bindChatgptWorkConversation,
7
7
  getChatgptWorkConversationBinding,
8
+ hasChatgptConversationIdentity,
8
9
  parseChatgptConversationIdentity,
9
10
  rebindChatgptWorkConversation,
10
11
  type ChatgptWorkConversationBinding,
@@ -256,7 +257,7 @@ async function findChatgptIntelligenceControl(
256
257
  // Prefer the composer/main region so sidebar history cannot crowd the
257
258
  // reasoning control out of a bounded query. Keep a larger global fallback
258
259
  // for ChatGPT layouts that place the control outside <main>.
259
- limit: selector.startsWith('main ') ? 80 : 240,
260
+ limit: chatgptAutomationControlQueryLimit(selector),
260
261
  timeout_ms: timeoutMs ?? 60_000,
261
262
  }, timeoutMs);
262
263
  const match = queryMatches(result).find((candidate) => {
@@ -268,12 +269,19 @@ async function findChatgptIntelligenceControl(
268
269
  return undefined;
269
270
  }
270
271
 
272
+ export function chatgptAutomationControlQueryLimit(selector: string): number {
273
+ // Long tool-heavy conversations can contribute well over 80 buttons before
274
+ // the composer controls. Keep the preferred <main> scan bounded but large
275
+ // enough that the current reasoning control is not crowded out by history.
276
+ return selector.startsWith('main ') ? 160 : 320;
277
+ }
278
+
271
279
  export function chatgptAutomationControlWaitBudgets(timeoutMs?: number): { waitBudgetMs: number; probeTimeoutMs: number } {
272
280
  const waitBudgetMs = Math.min(Math.max(timeoutMs ?? 30_000, 1_000), 30_000);
273
- // One probe checks both the preferred <main> scope and the global fallback.
274
- // Keep each selector query short enough that one slow pre-hydration pass cannot
275
- // consume the whole readiness window before React exposes the reasoning control.
276
- return { waitBudgetMs, probeTimeoutMs: Math.min(waitBudgetMs, 2_500) };
281
+ // Native Chrome attachment has a few seconds of Apple Events/DOM latency on
282
+ // long conversations. Keep each probe bounded while allowing a real query to
283
+ // complete; the total readiness window remains capped at 30 seconds.
284
+ return { waitBudgetMs, probeTimeoutMs: Math.min(waitBudgetMs, 5_000) };
277
285
  }
278
286
 
279
287
  async function waitForChatgptIntelligenceControl(
@@ -433,6 +441,7 @@ export function chatgptAutomationNavigationRequiresReplacement(error: unknown):
433
441
  'BROWSER_AUTOMATION_BACKGROUND_NAVIGATION_REQUIRES_REPLACEMENT',
434
442
  'PLUGIN_BROWSER_SESSION_STATE_LOST',
435
443
  'PLUGIN_SESSION_NOT_FOUND',
444
+ 'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN',
436
445
  ].some((marker) => error.message.includes(marker));
437
446
  }
438
447
 
@@ -685,7 +694,7 @@ export async function runWorkChatgptContinuation(input: WorkChatgptContinuationI
685
694
  throw new Error(`CHATGPT_WORK_CONTRACT_NOT_FOUND: ${input.repoId}:${input.workId}`);
686
695
  }
687
696
  await ensureControllerChatgptBrowser(input.controllerHome, input.workId);
688
- if (seedUrl && !binding) {
697
+ if (seedUrl && !binding && hasChatgptConversationIdentity(seedUrl)) {
689
698
  binding = bindChatgptWorkConversation(store, {
690
699
  workId: input.workId,
691
700
  conversationUrl: seedUrl,