@moretea-labs/forge 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -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 +125 -101
  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 +69 -29
  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 +150 -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 +815 -1015
  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 +3 -2
  166. package/src/runtime/root/package-runtime-service.ts +48 -3
  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,7 +12,6 @@ import {
12
12
  createWorkContract,
13
13
  getWorkContract,
14
14
  listWorkContracts,
15
- recordWorkCompletionReceipt,
16
15
  recordWorkScopeEvidence,
17
16
  summarizeWorkContract,
18
17
  transitionWorkContractPhase,
@@ -26,6 +25,8 @@ import {
26
25
  type PlanContractStoreOptions,
27
26
  } from './plan-contract-store';
28
27
  import { withPrimaryWorkAdmissionLock } from './semantic-admission';
28
+ import { completeWorkWithReceipt } from '../execution/work-completion-authority';
29
+ import { evaluateReadOnlyReviewSourceIdentity, evaluateWorkCompletionEvidence, evaluateWorkImplementationEvidence, verificationRecordAppliesToCurrentWorkspace } from '../execution/work-evidence-policy';
29
30
  import { readRequirement } from '../persistence/requirement-store';
30
31
  import {
31
32
  classifyVerificationOutcome,
@@ -51,6 +52,7 @@ import type {
51
52
  WorkRisk,
52
53
  } from './types';
53
54
  import { selectExecutionMode } from './types';
55
+ import { resolveWorkspaceAdmissionConstraint } from '../routing/workspace-admission';
54
56
 
55
57
  export type GoalWorkloopOperation = 'start' | 'continue' | 'verify' | 'finalize' | 'stop';
56
58
 
@@ -67,6 +69,8 @@ export interface GoalWorkloopContext {
67
69
  workspaceFingerprint?: string;
68
70
  /** Current net repository paths changed from the Work base plus dirty checkout paths. */
69
71
  workspaceChangedPaths?: readonly string[];
72
+ /** Durable terminal repository Process ids explicitly bound to this Work + checkout. */
73
+ workBoundProcessEvidenceIds?: readonly string[];
70
74
  now?: () => string;
71
75
  /** True only while a Gateway caller holds the cross-process primary Work admission lock. */
72
76
  semanticAdmissionLocked?: boolean;
@@ -97,7 +101,13 @@ export interface GoalWorkloopStartInput {
97
101
  planId?: string;
98
102
  planStepId?: string;
99
103
  /** Explicit technical evidence shape chosen by the semantic Controller. Never inferred from objective/check text. */
100
- workKind?: Extract<WorkKind, 'repository_change' | 'completed_no_change' | 'investigation' | 'local_effect' | 'remote_effect' | 'reconciliation'>;
104
+ workKind?: Extract<WorkKind, 'repository_change' | 'completed_no_change' | 'read_only_review' | 'investigation' | 'local_effect' | 'remote_effect' | 'reconciliation'>;
105
+ }
106
+
107
+ export interface GoalWorkloopAcceptanceEvidenceBinding {
108
+ criterion: string;
109
+ evidenceIds: string[];
110
+ rationale: string;
101
111
  }
102
112
 
103
113
  export interface GoalWorkloopContinueInput {
@@ -108,6 +118,10 @@ export interface GoalWorkloopContinueInput {
108
118
  checks?: string[];
109
119
  additionalLikelyPaths?: string[];
110
120
  inspectedPaths?: string[];
121
+ /** Concise semantic findings discovered by a first-class read-only review. */
122
+ reviewFindings?: string[];
123
+ /** Explicit Controller-reviewed bindings from exact declared criteria to durable Work evidence. */
124
+ acceptanceEvidence?: GoalWorkloopAcceptanceEvidenceBinding[];
111
125
  /** Explicit Controller decision after deterministic acceptance failure; never inferred from note text. */
112
126
  acceptanceFailureDecision?: 'repair' | 'rescope';
113
127
  }
@@ -163,6 +177,7 @@ function handoffIdFor(prefix: string): string {
163
177
  }
164
178
 
165
179
  function workRiskFor(input: GoalWorkloopStartInput): WorkRisk {
180
+ if (input.workKind === 'read_only_review') return 'readonly';
166
181
  const risk = input.modeInput.risk;
167
182
  if (input.modeInput.secretAccess === true || input.modeInput.destructive === true) return 'destructive';
168
183
  if (input.modeInput.remoteWrite === true) return 'high';
@@ -217,126 +232,6 @@ function initialEvidence(objective: string): EvidenceRef {
217
232
  };
218
233
  }
219
234
 
220
- type ReconciledVerificationHistory = ReturnType<typeof reconcileVerificationHistory>;
221
-
222
- interface WorkCompletionEvidenceEvaluation {
223
- status: 'complete' | 'failed' | 'incomplete';
224
- history: ReconciledVerificationHistory;
225
- missingChecks: string[];
226
- durableResultEvidence: boolean;
227
- reasons: string[];
228
- }
229
-
230
- function verificationRecordAppliesToCurrentWorkspace(
231
- record: VerificationRecord,
232
- currentRevision?: string,
233
- currentWorkspaceFingerprint?: string,
234
- ): boolean {
235
- if (currentRevision && record.sourceRevision && record.sourceRevision !== currentRevision) return false;
236
- if (
237
- currentWorkspaceFingerprint
238
- && record.workspaceFingerprint
239
- && record.workspaceFingerprint !== currentWorkspaceFingerprint
240
- ) return false;
241
- return true;
242
- }
243
-
244
- function isAuthoritativeCurrentWorkVerification(
245
- work: WorkContract,
246
- record: VerificationRecord,
247
- currentRevision?: string,
248
- ): boolean {
249
- const receipt = record.receipt;
250
- return Boolean(
251
- currentRevision
252
- && record.outcome === 'valid_pass'
253
- && record.sourceRevision === currentRevision
254
- && receipt
255
- && receipt.repoId === work.repoId
256
- && receipt.workId === work.workId
257
- && receipt.checkId === record.checkId
258
- && receipt.ok === true
259
- && receipt.timedOut === false
260
- && receipt.cancelled === false,
261
- );
262
- }
263
-
264
- interface WorkImplementationEvidenceEvaluation {
265
- status: 'complete' | 'incomplete';
266
- changedPaths: string[];
267
- reasons: string[];
268
- }
269
-
270
- function evaluateWorkImplementationEvidence(
271
- work: WorkContract,
272
- currentWorkspaceChangedPaths: readonly string[] = [],
273
- ): WorkImplementationEvidenceEvaluation {
274
- if (work.workKind !== 'repository_change') {
275
- return { status: 'complete', changedPaths: [], reasons: [] };
276
- }
277
-
278
- const changedPaths = [...new Set(currentWorkspaceChangedPaths.map((path) => path.trim()).filter(Boolean))].sort();
279
- if (changedPaths.length > 0) {
280
- return { status: 'complete', changedPaths, reasons: [] };
281
- }
282
-
283
- return {
284
- status: 'incomplete',
285
- changedPaths: [],
286
- reasons: ['Repository-change Work has no current net source changes relative to its base revision. Verification evidence cannot substitute for implementation evidence.'],
287
- };
288
- }
289
-
290
- function evaluateWorkCompletionEvidence(
291
- work: WorkContract,
292
- currentRevision?: string,
293
- currentWorkspaceFingerprint?: string,
294
- ): WorkCompletionEvidenceEvaluation {
295
- const applicableCheckRefs = work.checkRefs.filter((record) =>
296
- verificationRecordAppliesToCurrentWorkspace(record, currentRevision, currentWorkspaceFingerprint));
297
- const history = reconcileVerificationHistory(
298
- applicableCheckRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
299
- );
300
- const missingChecks = work.checks.filter((checkId) => !history.validPasses.includes(checkId));
301
- const durableResultEvidence = work.evidenceRefs.some((evidence) => Boolean(evidence.evidenceId || evidence.artifactId))
302
- || applicableCheckRefs.some((record) => isAuthoritativeCurrentWorkVerification(work, record, currentRevision));
303
- const reasons: string[] = [];
304
-
305
- if (history.acceptanceFailures.length > 0) {
306
- reasons.push(`Acceptance checks failed: ${history.acceptanceFailures.join(', ')}.`);
307
- return { status: 'failed', history, missingChecks, durableResultEvidence, reasons };
308
- }
309
- if (history.infrastructureIssues.length > 0) {
310
- reasons.push(`Infrastructure issues remain: ${history.infrastructureIssues.join(', ')}.`);
311
- }
312
- if (history.invalidCheckIds.length > 0) {
313
- reasons.push(`Invalid check ids remain: ${history.invalidCheckIds.join(', ')}.`);
314
- }
315
- if (missingChecks.length > 0) {
316
- reasons.push(`Declared checks are missing valid_pass evidence: ${missingChecks.join(', ')}.`);
317
- }
318
- if (work.checks.length === 0 && !durableResultEvidence) {
319
- const staleWorkVerification = work.checkRefs.some((record) =>
320
- Boolean(record.receipt)
321
- && !verificationRecordAppliesToCurrentWorkspace(record, currentRevision, currentWorkspaceFingerprint));
322
- reasons.push(staleWorkVerification
323
- ? 'Work-bound verification evidence is stale for the current source/workspace identity.'
324
- : 'No durable result evidence (evidenceId, artifactId, or current Work-bound verification receipt) was recorded for this no-check WorkContract.');
325
- }
326
-
327
- const complete = history.infrastructureIssues.length === 0
328
- && history.invalidCheckIds.length === 0
329
- && missingChecks.length === 0
330
- && (work.checks.length > 0 || durableResultEvidence);
331
- return {
332
- status: complete ? 'complete' : 'incomplete',
333
- history,
334
- missingChecks,
335
- durableResultEvidence,
336
- reasons,
337
- };
338
- }
339
-
340
235
  /**
341
236
  * Mode selection for facade routing. Direct control never creates a WorkContract.
342
237
  * Goal workloop creates one. Handoff-only creates a handoff and stops.
@@ -345,14 +240,74 @@ export function routeWorkStart(
345
240
  ctx: GoalWorkloopContext,
346
241
  input: GoalWorkloopStartInput,
347
242
  ): FacadeResult {
243
+ const placementResolution = resolveWorkspaceAdmissionConstraint(input.constraints);
244
+ if (placementResolution.ok === false) {
245
+ return buildFacadeResult({
246
+ status: 'blocked',
247
+ summary: `${placementResolution.code}: ${placementResolution.message}`,
248
+ data: { executionStarted: false, workContractCreated: false, placementConstraintConflict: true },
249
+ rawAvailable: false,
250
+ });
251
+ }
252
+ const placementConstraint = placementResolution.constraint;
253
+ const canonicalConstraints: WorkContract['constraints'] = {
254
+ ...(input.constraints ?? {}),
255
+ workspaceMode: placementConstraint.workspaceMode,
256
+ requireWorktree: placementConstraint.requireWorktree,
257
+ directMainProhibited: placementConstraint.directMainProhibited,
258
+ };
348
259
  const strategyConflictRequiresApproval = input.constraints?.architectureStrategyChange === true
349
260
  || input.constraints?.conflictsWithThinHarnessPolicy === true
350
261
  || input.constraints?.changesDefaultExecutionStrategy === true;
262
+ const relatedLifecycleSource = input.relatedWorkId ? getWorkContract(ctx.workStore, input.relatedWorkId) : undefined;
263
+ const inheritedReadOnlyReview = Boolean(
264
+ input.workKind === undefined
265
+ && input.workRelation === 'new_goal'
266
+ && relatedLifecycleSource?.workKind === 'read_only_review'
267
+ && (relatedLifecycleSource.status === 'cancelled' || relatedLifecycleSource.status === 'completed' || relatedLifecycleSource.status === 'failed'),
268
+ );
269
+ if (
270
+ inheritedReadOnlyReview
271
+ && relatedLifecycleSource?.baseRevision
272
+ && ctx.sourceRevision
273
+ && relatedLifecycleSource.baseRevision !== ctx.sourceRevision
274
+ ) {
275
+ return buildFacadeResult({
276
+ status: 'blocked',
277
+ summary: `READ_ONLY_REVIEW_REPLACEMENT_SOURCE_DRIFT: replacement source ${ctx.sourceRevision} does not match frozen review base ${relatedLifecycleSource.baseRevision}.`,
278
+ data: { executionStarted: false, workContractCreated: false, relatedWorkId: relatedLifecycleSource.workId, sourceDrift: true },
279
+ });
280
+ }
281
+ const requestedWorkKind = input.workKind ?? (inheritedReadOnlyReview ? 'read_only_review' : undefined);
282
+ const typedReadOnlyReviewRequested = requestedWorkKind === undefined
283
+ && (input.modeInput.mutation === false || (input.modeInput.mutation === undefined && input.modeInput.risk === 'readonly'))
284
+ && input.modeInput.requiresInvestigation === true
285
+ && input.modeInput.requiresRecovery === true;
286
+ const effectiveWorkKind = requestedWorkKind ?? (typedReadOnlyReviewRequested ? 'read_only_review' : undefined);
287
+ const readOnlyReviewRequested = effectiveWorkKind === 'read_only_review';
288
+ const readOnlyMutationConflict = readOnlyReviewRequested && (
289
+ input.modeInput.mutation === true
290
+ || (input.modeInput.expectedFiles ?? 0) > 0
291
+ || (input.modeInput.expectedChangedLines ?? 0) > 0
292
+ || input.modeInput.requiresExternalEffect === true
293
+ || input.modeInput.remoteWrite === true
294
+ || input.modeInput.destructive === true
295
+ );
296
+ if (readOnlyMutationConflict) {
297
+ return buildFacadeResult({
298
+ status: 'blocked',
299
+ summary: 'READ_ONLY_REVIEW_MUTATION_CONFLICT: read_only_review cannot request repository mutation, external effects, remote writes, or destructive execution.',
300
+ data: { executionStarted: false, workContractCreated: false, workKind: effectiveWorkKind },
301
+ });
302
+ }
351
303
  const effectiveModeInput: ExecutionModeSelectionInput = {
352
304
  ...input.modeInput,
353
305
  objective: input.objective,
354
306
  knownPaths: input.allowedPaths,
355
- mutation: input.modeInput.mutation ?? input.modeInput.risk !== 'readonly',
307
+ workspacePlacement: placementConstraint.workspaceMode,
308
+ directMainProhibited: placementConstraint.directMainProhibited,
309
+ mutation: readOnlyReviewRequested ? false : input.modeInput.mutation ?? input.modeInput.risk !== 'readonly',
310
+ risk: readOnlyReviewRequested ? 'readonly' : input.modeInput.risk,
356
311
  approvalConfirmed: input.approvalConfirmed === true,
357
312
  requiresUserApproval: input.approvalConfirmed === true
358
313
  ? false
@@ -376,7 +331,7 @@ export function routeWorkStart(
376
331
  : selected;
377
332
  const evaluateAccessPolicy = (selected: ReturnType<typeof selectExecutionMode>) => evaluatePolicyGate({
378
333
  capabilityId: selected.mode === 'direct_control' ? 'repository.direct_edit' : selected.mode === 'goal_workloop' ? 'controller.goal_workloop' : 'controller.handoff_inbox',
379
- risk: input.modeInput.risk
334
+ risk: effectiveModeInput.risk
380
335
  ?? (input.modeInput.secretAccess === true ? 'raw_secret_config'
381
336
  : input.modeInput.destructive === true ? 'destructive'
382
337
  : input.modeInput.remoteWrite === true ? 'remote_write'
@@ -396,12 +351,24 @@ export function routeWorkStart(
396
351
  });
397
352
 
398
353
  let selectedMode = selectExecutionMode(effectiveModeInput);
354
+ if (input.forceMode === 'direct_control' && selectedMode.routeDecision.requiresIsolation) {
355
+ return buildFacadeResult({
356
+ status: 'blocked',
357
+ summary: 'WORKSPACE_PLACEMENT_DIRECT_CONTROL_FORBIDDEN: typed placement requires isolation, so force_mode=direct_control cannot override admission policy.',
358
+ data: { executionStarted: false, workContractCreated: false, routeDecision: selectedMode.routeDecision },
359
+ rawAvailable: false,
360
+ });
361
+ }
399
362
  let policy = evaluateAccessPolicy(selectedMode);
400
363
  if (policy.decision === 'allowed' && effectiveModeInput.requiresUserApproval !== true) {
401
364
  selectedMode = selectExecutionMode({ ...effectiveModeInput, approvalConfirmed: true });
402
365
  policy = evaluateAccessPolicy(selectedMode);
403
366
  }
404
367
  let mode = applyForcedMode(selectedMode);
368
+ const routeApprovalRequired = mode.routeDecision.requiresApproval
369
+ && mode.routeDecision.waitForUser
370
+ && mode.routeDecision.approvalState !== 'blocked_by_policy';
371
+ const approvalRequired = policy.decision === 'approval_required' || routeApprovalRequired;
405
372
 
406
373
  // Route Policy is the sole execution-depth authority. Existing Work is
407
374
  // ownership context, not a reason to upgrade an otherwise Direct operation
@@ -488,7 +455,7 @@ export function routeWorkStart(
488
455
  severity: policy.decision === 'denied' ? 'blocked' : 'needs_review',
489
456
  creationReason: !input.modeInput.scopeClear
490
457
  ? 'invalid_objective'
491
- : policy.decision === 'approval_required'
458
+ : approvalRequired
492
459
  ? (input.modeInput.destructive ? 'destructive_action_requires_confirmation' : 'policy_approval_required')
493
460
  : 'missing_authorization',
494
461
  reason: mode.reason,
@@ -501,13 +468,13 @@ export function routeWorkStart(
501
468
  },
502
469
  attemptedActions: ['route_execution_mode'],
503
470
  evidenceRefs: [],
504
- blockingDecision: policy.decision === 'approval_required'
471
+ blockingDecision: approvalRequired
505
472
  ? 'Approve side effects or restate a safer objective.'
506
473
  : 'Clarify objective, scope, and acceptance criteria.',
507
474
  recommendedDecision: 'Provide a clear objective and authorization, or cancel the request.',
508
475
  recommendedPrompt: `Resolve handoff and restate work for repo ${ctx.repoId}.`,
509
476
  recommendedContinuationPrompt: `After approval, start the approved work for ${ctx.repoId}.`,
510
- approvalAction: policy.decision === 'approval_required'
477
+ approvalAction: approvalRequired
511
478
  ? {
512
479
  operation: 'start',
513
480
  label: 'Approve and start work',
@@ -528,6 +495,9 @@ export function routeWorkStart(
528
495
  requiresApproval: input.modeInput.requiresApproval === true || input.modeInput.requiresUserApproval === true,
529
496
  destructive: input.modeInput.destructive === true,
530
497
  accessMode: input.constraints?.accessMode,
498
+ workspaceMode: placementConstraint.workspaceMode,
499
+ requireWorktree: placementConstraint.requireWorktree,
500
+ directMainProhibited: placementConstraint.directMainProhibited,
531
501
  approvalConfirmed: true,
532
502
  forceMode: 'goal_workloop',
533
503
  },
@@ -545,7 +515,7 @@ export function routeWorkStart(
545
515
  });
546
516
 
547
517
  return buildFacadeResult({
548
- status: policy.decision === 'denied' ? 'blocked' : policy.decision === 'approval_required' ? 'approval_required' : 'blocked',
518
+ status: policy.decision === 'denied' ? 'blocked' : approvalRequired ? 'approval_required' : 'blocked',
549
519
  summary: `Handoff-only: no WorkContract created and no execution started. ${mode.reason}`,
550
520
  data: {
551
521
  mode,
@@ -575,7 +545,12 @@ export function routeWorkStart(
575
545
  });
576
546
  }
577
547
 
578
- return startGoalWorkloop(ctx, input, policy, 'goal_workloop', mode.routeDecision);
548
+ return startGoalWorkloop(ctx, {
549
+ ...input,
550
+ constraints: canonicalConstraints,
551
+ modeInput: effectiveModeInput,
552
+ workKind: effectiveWorkKind,
553
+ }, policy, 'goal_workloop', mode.routeDecision);
579
554
  }
580
555
 
581
556
  export function startGoalWorkloop(
@@ -585,6 +560,22 @@ export function startGoalWorkloop(
585
560
  executionMode: 'direct_control' | 'goal_workloop' = 'goal_workloop',
586
561
  routeDecision = selectExecutionMode({ ...input.modeInput, objective: input.objective, knownPaths: input.allowedPaths }).routeDecision,
587
562
  ): FacadeResult {
563
+ const placementResolution = resolveWorkspaceAdmissionConstraint(input.constraints);
564
+ if (placementResolution.ok === false) {
565
+ return buildFacadeResult({
566
+ status: 'blocked',
567
+ summary: `${placementResolution.code}: ${placementResolution.message}`,
568
+ data: { executionStarted: false, workContractCreated: false, placementConstraintConflict: true },
569
+ rawAvailable: false,
570
+ });
571
+ }
572
+ const placementConstraint = placementResolution.constraint;
573
+ const canonicalConstraints: WorkContract['constraints'] = {
574
+ ...(input.constraints ?? {}),
575
+ workspaceMode: placementConstraint.workspaceMode,
576
+ requireWorktree: placementConstraint.requireWorktree,
577
+ directMainProhibited: placementConstraint.directMainProhibited,
578
+ };
588
579
  const hasStrongAdmissionBinding = Boolean(input.planId || input.planStepId || input.requirementId || input.relatedWorkId || input.workRelation);
589
580
  if (hasStrongAdmissionBinding && !ctx.semanticAdmissionLocked) {
590
581
  return withPrimaryWorkAdmissionLock(ctx.workStore, () => startGoalWorkloop(
@@ -597,7 +588,7 @@ export function startGoalWorkloop(
597
588
  }
598
589
  const at = nowIso(ctx);
599
590
  const available = ctx.availableChecks ?? [];
600
- const workspaceMode = input.constraints?.workspaceMode ?? 'auto';
591
+ const workspaceMode = placementConstraint.workspaceMode;
601
592
  const activeWorks = listWorkContracts({ ...ctx.workStore, status: 'active', limit: 100 })
602
593
  .filter((candidate) => (candidate.lifecycleRole ?? 'primary') === 'primary');
603
594
  const workspaceOwner = ctx.checkoutId
@@ -803,11 +794,10 @@ export function startGoalWorkloop(
803
794
  }
804
795
 
805
796
  const placementConflict = Boolean(workspaceOwner);
806
- const needsWorktree = executionMode === 'goal_workloop' && (input.constraints?.requireWorktree
807
- ?? (workspaceMode === 'isolated'
808
- || placementConflict
809
- || input.modeInput.requiresParallelism === true
810
- || (workspaceMode === 'auto' && routeDecision.requiresIsolation === true)));
797
+ const needsWorktree = executionMode === 'goal_workloop' && (placementConstraint.requireWorktree
798
+ || placementConflict
799
+ || input.modeInput.requiresParallelism === true
800
+ || routeDecision.requiresIsolation === true);
811
801
  if (!needsWorktree && workspaceOwner) {
812
802
  return buildFacadeResult({
813
803
  status: 'blocked',
@@ -863,11 +853,17 @@ export function startGoalWorkloop(
863
853
  const repositoryChangeIntent = input.workKind === 'repository_change'
864
854
  || (input.modeInput.expectedFiles ?? 0) > 0
865
855
  || (input.modeInput.expectedChangedLines ?? 0) > 0;
856
+ const typedRecoverableReadOnlyReview = input.workKind === undefined
857
+ && input.modeInput.mutation === false
858
+ && input.modeInput.requiresInvestigation === true
859
+ && input.modeInput.requiresRecovery === true;
866
860
  const resolvedWorkKind = input.workKind ?? (
867
- input.modeInput.requiresExternalEffect === true
868
- && !repositoryChangeIntent
869
- ? (input.modeInput.remoteWrite === true ? 'remote_effect' : 'local_effect')
870
- : 'repository_change'
861
+ typedRecoverableReadOnlyReview
862
+ ? 'read_only_review'
863
+ : input.modeInput.requiresExternalEffect === true
864
+ && !repositoryChangeIntent
865
+ ? (input.modeInput.remoteWrite === true ? 'remote_effect' : 'local_effect')
866
+ : 'repository_change'
871
867
  );
872
868
  const remoteDeliveryRequired = resolvedWorkKind === 'repository_change'
873
869
  && input.modeInput.requiresExternalEffect === true
@@ -886,7 +882,7 @@ export function startGoalWorkloop(
886
882
  objective: input.objective,
887
883
  acceptanceCriteria: effectiveAcceptanceCriteria,
888
884
  constraints: {
889
- ...(input.constraints ?? { requireHandoffOnAmbiguity: true }),
885
+ ...canonicalConstraints,
890
886
  ...(remoteDeliveryRequired ? { remoteDeliveryRequired: true } : {}),
891
887
  },
892
888
  risk: workRiskFor(input),
@@ -1030,6 +1026,24 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1030
1026
  });
1031
1027
  }
1032
1028
 
1029
+ if ((input.reviewFindings?.length ?? 0) > 0 && work.workKind !== 'read_only_review') {
1030
+ return buildFacadeResult({
1031
+ status: 'blocked',
1032
+ summary: `READ_ONLY_REVIEW_FINDINGS_KIND_REQUIRED: ${work.workId} is ${work.workKind}, not read_only_review.`,
1033
+ data: { work: summarizeWorkContract(work), reviewFindingsRecorded: false },
1034
+ });
1035
+ }
1036
+ if (work.workKind === 'read_only_review') {
1037
+ const sourceIdentity = evaluateReadOnlyReviewSourceIdentity(work, ctx.sourceRevision, ctx.workspaceChangedPaths);
1038
+ if (sourceIdentity.status !== 'complete') {
1039
+ return buildFacadeResult({
1040
+ status: 'blocked',
1041
+ summary: `READ_ONLY_REVIEW_SOURCE_IDENTITY_REQUIRED: ${sourceIdentity.reasons.join(' ')}`,
1042
+ data: { work: summarizeWorkContract(work), sourceIdentityProven: false },
1043
+ });
1044
+ }
1045
+ }
1046
+
1033
1047
  const explicitPolicyScope = input.allowedPaths !== undefined
1034
1048
  || input.forbiddenPaths !== undefined
1035
1049
  || input.checks !== undefined;
@@ -1072,6 +1086,104 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1072
1086
  inspectedPaths: input.inspectedPaths,
1073
1087
  });
1074
1088
  }
1089
+ if ((input.acceptanceEvidence?.length ?? 0) > 0) {
1090
+ if (work.workKind !== 'local_effect') {
1091
+ return buildFacadeResult({
1092
+ status: 'blocked',
1093
+ summary: `WORK_SEMANTIC_ACCEPTANCE_LOCAL_EFFECT_REQUIRED: ${work.workId} is ${work.workKind}.`,
1094
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1095
+ });
1096
+ }
1097
+ const declaredCriteria = new Map(work.acceptanceCriteria.map((criterion) => [criterion.trim(), criterion]));
1098
+ const availableEvidenceIds = new Set([
1099
+ ...work.evidenceRefs.flatMap((evidence) => [evidence.evidenceId, evidence.artifactId])
1100
+ .map((value) => value?.trim())
1101
+ .filter((value): value is string => Boolean(value)),
1102
+ ...(ctx.workBoundProcessEvidenceIds ?? []).map((value) => value.trim()).filter(Boolean),
1103
+ ]);
1104
+ const normalizedBindings: Array<{ criterion: string; evidenceIds: string[]; rationale: string }> = [];
1105
+ for (const binding of input.acceptanceEvidence ?? []) {
1106
+ const requestedCriterion = binding.criterion.trim();
1107
+ const criterion = declaredCriteria.get(requestedCriterion);
1108
+ if (!criterion) {
1109
+ return buildFacadeResult({
1110
+ status: 'blocked',
1111
+ summary: `WORK_SEMANTIC_ACCEPTANCE_CRITERION_UNKNOWN: ${requestedCriterion || '<empty>'}.`,
1112
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1113
+ });
1114
+ }
1115
+ const evidenceIds = [...new Set(binding.evidenceIds.map((value) => value.trim()).filter(Boolean))];
1116
+ if (evidenceIds.length === 0) {
1117
+ return buildFacadeResult({
1118
+ status: 'blocked',
1119
+ summary: `WORK_SEMANTIC_ACCEPTANCE_EVIDENCE_REQUIRED: ${criterion}.`,
1120
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1121
+ });
1122
+ }
1123
+ const unknownEvidenceIds = evidenceIds.filter((evidenceId) => !availableEvidenceIds.has(evidenceId));
1124
+ if (unknownEvidenceIds.length > 0) {
1125
+ return buildFacadeResult({
1126
+ status: 'blocked',
1127
+ summary: `WORK_SEMANTIC_ACCEPTANCE_EVIDENCE_UNKNOWN: ${unknownEvidenceIds.join(', ')}.`,
1128
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1129
+ });
1130
+ }
1131
+ const rationale = binding.rationale.trim();
1132
+ if (!rationale) {
1133
+ return buildFacadeResult({
1134
+ status: 'blocked',
1135
+ summary: `WORK_SEMANTIC_ACCEPTANCE_RATIONALE_REQUIRED: ${criterion}.`,
1136
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1137
+ });
1138
+ }
1139
+ normalizedBindings.push({ criterion, evidenceIds, rationale: rationale.slice(0, 1_000) });
1140
+ }
1141
+ const recordedAt = nowIso(ctx);
1142
+ const byCriterion = new Map((work.semanticAcceptanceEvidence ?? []).map((review) => [review.criterion, review]));
1143
+ for (const binding of normalizedBindings) {
1144
+ const previous = byCriterion.get(binding.criterion);
1145
+ byCriterion.set(binding.criterion, {
1146
+ criterion: binding.criterion,
1147
+ evidenceIds: [...new Set([...(previous?.evidenceIds ?? []), ...binding.evidenceIds])].slice(0, 50),
1148
+ rationale: binding.rationale,
1149
+ recordedAt,
1150
+ });
1151
+ }
1152
+ work = updateWorkContract(ctx.workStore, work.workId, {
1153
+ semanticAcceptanceEvidence: [...byCriterion.values()].slice(0, 20),
1154
+ });
1155
+ work = appendWorkEvidence(ctx.workStore, work.workId, {
1156
+ title: 'Controller semantic acceptance review',
1157
+ summary: `Controller reviewed ${normalizedBindings.length} exact acceptance criterion binding(s) against durable Work evidence.`,
1158
+ detailLevel: 'summary',
1159
+ });
1160
+ }
1161
+
1162
+ if (work.workKind === 'read_only_review' && ((input.inspectedPaths?.length ?? 0) > 0 || input.reviewFindings !== undefined)) {
1163
+ const findings = [...new Set([
1164
+ ...(work.readOnlyReviewEvidence?.findings ?? []),
1165
+ ...(input.reviewFindings ?? []).map((finding) => finding.trim()).filter(Boolean),
1166
+ ])].slice(0, 50);
1167
+ const inspectedPaths = [...new Set(work.scopeEvidence?.inspectedPaths ?? [])].slice(0, 200);
1168
+ work = updateWorkContract(ctx.workStore, work.workId, {
1169
+ readOnlyReviewEvidence: {
1170
+ sourceRevision: ctx.sourceRevision!,
1171
+ ...(ctx.workspaceFingerprint ? { workspaceFingerprint: ctx.workspaceFingerprint } : {}),
1172
+ inspectedPaths,
1173
+ findings,
1174
+ recordedAt: nowIso(ctx),
1175
+ },
1176
+ });
1177
+ for (const finding of input.reviewFindings ?? []) {
1178
+ const summary = finding.trim();
1179
+ if (!summary) continue;
1180
+ work = appendWorkEvidence(ctx.workStore, work.workId, {
1181
+ title: 'read-only review finding',
1182
+ summary: summary.slice(0, 1_500),
1183
+ detailLevel: 'summary',
1184
+ });
1185
+ }
1186
+ }
1075
1187
 
1076
1188
  const currentCheckRefs = work.checkRefs.filter((record) =>
1077
1189
  verificationRecordAppliesToCurrentWorkspace(record, ctx.sourceRevision, ctx.workspaceFingerprint));
@@ -1104,7 +1216,7 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1104
1216
  acceptanceFailures: history.acceptanceFailures,
1105
1217
  infrastructureIssues: history.infrastructureIssues,
1106
1218
  backgroundCompleted: false,
1107
- nextStep: 'execute',
1219
+ nextStep: work.workKind === 'read_only_review' ? 'review' : 'execute',
1108
1220
  acceptanceFailureDecision: decision,
1109
1221
  },
1110
1222
  evidenceRefs: work.evidenceRefs.slice(0, 5),
@@ -1368,7 +1480,13 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1368
1480
  });
1369
1481
  }
1370
1482
 
1371
- const completionEvidence = evaluateWorkCompletionEvidence(work, ctx.sourceRevision, ctx.workspaceFingerprint);
1483
+ const completionEvidence = evaluateWorkCompletionEvidence(
1484
+ work,
1485
+ ctx.sourceRevision,
1486
+ ctx.workspaceFingerprint,
1487
+ ctx.workBoundProcessEvidenceIds,
1488
+ ctx.workspaceChangedPaths,
1489
+ );
1372
1490
  if (completionEvidence.status !== 'complete') {
1373
1491
  const suggested = validateSuggestedNextActions([
1374
1492
  {
@@ -1383,9 +1501,11 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1383
1501
  ]).actions;
1384
1502
  transitionWorkContractPhase(ctx.workStore, work.workId, {
1385
1503
  status: 'running',
1386
- phase: 'implementation',
1504
+ phase: work.workKind === 'read_only_review' ? 'verification' : 'implementation',
1387
1505
  state: 'active',
1388
- summary: `Meaningful implementation evidence is still missing: ${completionEvidence.reasons.join(' ')}`,
1506
+ summary: work.workKind === 'read_only_review'
1507
+ ? `Read-only review completion evidence is incomplete: ${completionEvidence.reasons.join(' ')}`
1508
+ : `Meaningful implementation evidence is still missing: ${completionEvidence.reasons.join(' ')}`,
1389
1509
  evidenceRefs: work.evidenceRefs,
1390
1510
  });
1391
1511
  const updated = updateWorkContract(ctx.workStore, work.workId, {
@@ -1398,7 +1518,7 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1398
1518
  work: summarizeWorkContract(updated),
1399
1519
  backgroundCompleted: false,
1400
1520
  nextStep: 'execute',
1401
- executionEvidencePresent: false,
1521
+ executionEvidencePresent: Boolean(ctx.workBoundProcessEvidenceIds?.length),
1402
1522
  missingChecks: completionEvidence.missingChecks,
1403
1523
  durableResultEvidence: completionEvidence.durableResultEvidence,
1404
1524
  ignoredWeakReferences: {
@@ -1424,7 +1544,9 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1424
1544
  status: 'running',
1425
1545
  phase: 'delivery',
1426
1546
  state: 'active',
1427
- summary: 'Implementation and verification evidence are sufficient; exact delivery and cleanup receipt is next.',
1547
+ summary: work.workKind === 'read_only_review'
1548
+ ? 'Read-only review evidence is source-bound and clean; semantic no-change finalization is next.'
1549
+ : 'Implementation and verification evidence are sufficient; exact delivery and cleanup receipt is next.',
1428
1550
  evidenceRefs: work.evidenceRefs,
1429
1551
  });
1430
1552
  const updated = updateWorkContract(ctx.workStore, work.workId, {
@@ -1642,7 +1764,13 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1642
1764
  });
1643
1765
  }
1644
1766
 
1645
- const completionEvidence = evaluateWorkCompletionEvidence(work, ctx.sourceRevision, ctx.workspaceFingerprint);
1767
+ const completionEvidence = evaluateWorkCompletionEvidence(
1768
+ work,
1769
+ ctx.sourceRevision,
1770
+ ctx.workspaceFingerprint,
1771
+ ctx.workBoundProcessEvidenceIds,
1772
+ ctx.workspaceChangedPaths,
1773
+ );
1646
1774
  const history = completionEvidence.history;
1647
1775
 
1648
1776
  if (input.forceFailed || completionEvidence.status === 'failed') {
@@ -1717,6 +1845,50 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1717
1845
  });
1718
1846
  }
1719
1847
 
1848
+ // Read-only review completion is semantic no-change authority, not Git delivery.
1849
+ // The exact source/workspace fence is re-evaluated above at finalize time; only
1850
+ // a clean review with persisted inspected paths and zero findings can close.
1851
+ if (work.workKind === 'read_only_review' && !work.completionReceipt) {
1852
+ const reviewEvidence = work.readOnlyReviewEvidence!;
1853
+ const recordedAt = nowIso(ctx);
1854
+ const completed = completeWorkWithReceipt(
1855
+ ctx.workStore,
1856
+ work.workId,
1857
+ {
1858
+ schemaVersion: 1,
1859
+ receiptId: `ROR-WORK-${randomUUID()}`,
1860
+ source: 'read_only_review',
1861
+ workId: work.workId,
1862
+ baseRevision: work.baseRevision!,
1863
+ sourceRevision: ctx.sourceRevision!,
1864
+ ...(ctx.workspaceFingerprint ? { workspaceFingerprint: ctx.workspaceFingerprint } : {}),
1865
+ workspaceChangedPaths: [],
1866
+ inspectedPaths: reviewEvidence.inspectedPaths,
1867
+ findingCount: 0,
1868
+ recordedAt,
1869
+ },
1870
+ 'completed_no_change',
1871
+ 'read_only_review',
1872
+ );
1873
+ if (completed.planId && completed.planStepId && ctx.planStore) {
1874
+ completePlanStepForWork(ctx.planStore, { planId: completed.planId, stepId: completed.planStepId, work: completed });
1875
+ }
1876
+ return buildFacadeResult({
1877
+ status: 'ok',
1878
+ summary: `Finalize result: clean read-only review completed with no source change for ${work.workId}.`,
1879
+ data: {
1880
+ work: summarizeWorkContract(completed),
1881
+ finalStatus: 'completed',
1882
+ completionOutcome: 'completed_no_change',
1883
+ completionReceipt: completed.completionReceipt,
1884
+ inspectedPathCount: reviewEvidence.inspectedPaths.length,
1885
+ hiddenFailure: false,
1886
+ },
1887
+ evidenceRefs: completed.evidenceRefs.slice(0, 5),
1888
+ suggestedNextActions: [{ label: 'Read controller status', tool: 'rh_status', operation: 'get', risk: 'readonly' }],
1889
+ });
1890
+ }
1891
+
1720
1892
  // For a controller-local effect Work, the semantic Controller's explicit
1721
1893
  // finalize call is the terminalization decision, but it is admissible only
1722
1894
  // after durable result evidence exists. Record one canonical Work receipt here
@@ -1724,7 +1896,7 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1724
1896
  // remain bound to their durable plugin action receipt in the plugin layer.
1725
1897
  if (work.workKind === 'local_effect' && !work.completionReceipt && completionEvidence.durableResultEvidence) {
1726
1898
  const recordedAt = nowIso(ctx);
1727
- const completed = recordWorkCompletionReceipt(
1899
+ const completed = completeWorkWithReceipt(
1728
1900
  ctx.workStore,
1729
1901
  work.workId,
1730
1902
  {
@@ -1947,6 +2119,7 @@ export function runGoalWorkloop(
1947
2119
  planStepId: typeof args.plan_step_id === 'string' ? args.plan_step_id : undefined,
1948
2120
  workKind: args.work_kind === 'repository_change'
1949
2121
  || args.work_kind === 'completed_no_change'
2122
+ || args.work_kind === 'read_only_review'
1950
2123
  || args.work_kind === 'investigation'
1951
2124
  || args.work_kind === 'local_effect'
1952
2125
  || args.work_kind === 'remote_effect'
@@ -1963,6 +2136,14 @@ export function runGoalWorkloop(
1963
2136
  checks: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
1964
2137
  additionalLikelyPaths: Array.isArray(args.additional_likely_paths) ? args.additional_likely_paths.map(String) : undefined,
1965
2138
  inspectedPaths: Array.isArray(args.inspected_paths) ? args.inspected_paths.map(String) : undefined,
2139
+ reviewFindings: Array.isArray(args.review_findings) ? args.review_findings.map(String) : undefined,
2140
+ acceptanceEvidence: Array.isArray(args.acceptance_evidence)
2141
+ ? args.acceptance_evidence.filter((value): value is Record<string, unknown> => Boolean(value) && typeof value === 'object').map((value) => ({
2142
+ criterion: typeof value.criterion === 'string' ? value.criterion : '',
2143
+ evidenceIds: Array.isArray(value.evidence_ids) ? value.evidence_ids.map(String) : [],
2144
+ rationale: typeof value.rationale === 'string' ? value.rationale : '',
2145
+ }))
2146
+ : undefined,
1966
2147
  acceptanceFailureDecision: args.acceptance_failure_decision === 'repair' || args.acceptance_failure_decision === 'rescope'
1967
2148
  ? args.acceptance_failure_decision
1968
2149
  : undefined,