@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
@@ -0,0 +1,96 @@
1
+ import { resolve } from 'path';
2
+ import type { RepositoryRecord } from '../../../cli/repositories/types';
3
+ import { resolveRepositorySelection, selectRepositoryCheckout } from '../../../cli/repositories/registry';
4
+ import { repositoryGitStatus } from '../../../cli/repositories/structured-git';
5
+ import { getWorkContract } from '../facade/work-contract-store';
6
+ import { currentPermissionSnapshotVersion } from './validation';
7
+ import { readWorkHandle, writeWorkHandle, type WorkHandleState } from './work-handle-store';
8
+
9
+ export interface RepositoryWorkHandleControllerIdentity {
10
+ sessionId: string;
11
+ principalId: string;
12
+ }
13
+
14
+ /**
15
+ * Canonical compatibility repair for a goal-workloop Work whose durable
16
+ * WorkContract exists but whose WorkHandle has not yet been materialized.
17
+ * Transport layers provide only the authenticated Controller identity.
18
+ */
19
+ export function ensureRepositoryWorkHandle(input: {
20
+ controllerHome: string;
21
+ repository: RepositoryRecord;
22
+ workId: string;
23
+ identity: RepositoryWorkHandleControllerIdentity;
24
+ }): WorkHandleState | undefined {
25
+ const existing = readWorkHandle(input.controllerHome, input.repository.repoId, input.workId);
26
+ if (existing) return existing;
27
+ const contract = getWorkContract(
28
+ { controllerHome: input.controllerHome, repoId: input.repository.repoId },
29
+ input.workId,
30
+ );
31
+ if (!contract || contract.workKind !== 'repository_change' || contract.mode !== 'goal_workloop' || !contract.checkoutId) {
32
+ return undefined;
33
+ }
34
+ const executionRepository = resolveRepositorySelection({
35
+ repoId: input.repository.repoId,
36
+ checkoutId: contract.checkoutId,
37
+ controllerHome: input.controllerHome,
38
+ allowSoleRepository: false,
39
+ });
40
+ const checkout = selectRepositoryCheckout(executionRepository, contract.checkoutId, { allowArchived: true });
41
+ const status = repositoryGitStatus(checkout);
42
+ if (!status.branch) throw new Error(`WORKTREE_DETACHED: ${contract.checkoutId} has no branch`);
43
+ const at = new Date().toISOString();
44
+ const managedWorktree = Boolean(contract.worktreeRef)
45
+ && resolve(contract.worktreeRef!) === resolve(checkout.canonicalRoot)
46
+ && resolve(checkout.canonicalRoot) !== resolve(input.repository.canonicalRoot);
47
+ return writeWorkHandle(input.controllerHome, {
48
+ schemaVersion: 1,
49
+ workId: input.workId,
50
+ sessionId: input.identity.sessionId,
51
+ principalId: input.identity.principalId,
52
+ repositoryId: input.repository.repoId,
53
+ checkoutId: contract.checkoutId,
54
+ worktreePath: checkout.canonicalRoot,
55
+ branch: status.branch,
56
+ sourceCheckoutId: input.repository.activeCheckoutId,
57
+ managedWorktree,
58
+ workContractId: contract.workId,
59
+ baseCommit: contract.baseRevision ?? status.head ?? undefined,
60
+ deliveryBaseCommit: contract.baseRevision ?? status.head ?? undefined,
61
+ expectedHead: status.head ?? contract.baseRevision,
62
+ permissionSnapshotVersion: currentPermissionSnapshotVersion(input.controllerHome, input.repository.repoId),
63
+ state: 'prepared',
64
+ createdAt: at,
65
+ updatedAt: at,
66
+ finalization: {
67
+ validation: 'pending',
68
+ commit: 'pending',
69
+ merge: 'pending',
70
+ branchCleanup: 'pending',
71
+ worktreeCleanup: 'pending',
72
+ },
73
+ cleanupResponsibility: { owner: 'work_finalizer', registeredAt: at },
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Rebinds the durable WorkHandle to the Controller session that has already
79
+ * been admitted by the ControllerSession authority. The compare-and-swap
80
+ * revision on the read handle preserves fail-closed concurrent ownership.
81
+ */
82
+ export function rebindRepositoryWorkHandleControllerIdentity(input: {
83
+ controllerHome: string;
84
+ repositoryId: string;
85
+ workId: string;
86
+ identity: RepositoryWorkHandleControllerIdentity;
87
+ }): WorkHandleState | undefined {
88
+ const existing = readWorkHandle(input.controllerHome, input.repositoryId, input.workId);
89
+ if (!existing) return undefined;
90
+ if (existing.principalId === input.identity.principalId && existing.sessionId === input.identity.sessionId) return existing;
91
+ return writeWorkHandle(input.controllerHome, {
92
+ ...existing,
93
+ principalId: input.identity.principalId,
94
+ sessionId: input.identity.sessionId,
95
+ });
96
+ }
@@ -3,7 +3,7 @@ import { existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
5
5
  import { readJsonFile, sanitizeFileComponent } from '../../shared/json-files';
6
- import type { ProcessCheckExecutionIdentity } from '../../execution/process-runtime/types';
6
+ import type { ProcessCheckExecutionIdentity } from '../../evidence/process-check-execution';
7
7
  import { listControlPlaneRecords, mutateControlPlaneRecord, readOrImportControlPlaneRecord, writeControlPlaneRecord } from '../persistence/sqlite-store';
8
8
 
9
9
  export const WORK_HANDLE_STATES = [
@@ -0,0 +1,468 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { isAbsolute, relative, resolve } from 'path';
3
+ import type { MultiRepositoryMcpToolContext } from '../../../cli/mcp/multi-repository';
4
+ import { repositoryGitStatus, repositoryGitDiff } from '../../../cli/repositories/structured-git';
5
+ import { classifyRepositoryCommand } from '../../../cli/repositories/command-classifier';
6
+ import { listControllerChecks } from '../../../cli/controller/check-runner';
7
+ import { readRepositoryAccessPolicy } from '../governance/access-policy';
8
+ import { appendVerificationRecord } from '../facade/work-contract-store';
9
+ import { validateWorkHandle } from './validation';
10
+ import { commandFingerprint, effectiveVerificationEvidence, verificationInputFingerprint, workspaceValidationFingerprint, workValidationInputFingerprint } from './verification-evidence';
11
+ import { executionIdentityForWork } from './execution-identity';
12
+ import { markWorkHandleFailed, transitionWorkHandle, type WorkHandleState } from './work-handle-store';
13
+ import { assertResolvedAuthorization, decideAuthorization, type AuthorizationDecision, type AuthorizationRiskClass } from '../governance/authorization';
14
+ import { recordMcpTiming } from '../../diagnostics/mcp-timing';
15
+ import { commandValue, normalizeRepositoryCommand, type RepositoryCommandValue } from '../../../cli/repositories/command-normalization';
16
+ import { executeRepositoryCommandViaProcessRuntime } from '../../execution/process-runtime/command-facade';
17
+ import { getCheckProcessHandle } from '../../execution/process-runtime/check-facade';
18
+ import { processCheckCompletionReceipt } from '../../execution/process-runtime/check-receipt';
19
+ import { classifyPersistedCheckTerminalEvidence } from '../../execution/process-runtime/check-result';
20
+ import { claimProcessInvocation, getProcessRecord } from '../../execution/process-runtime/store';
21
+ import { runPersistedCheckViaProcessRuntime } from '../../execution/process-runtime/persisted-check';
22
+ import { markWorkValidationPending, projectWorkValidationOutcome } from './work-validation-reconciler';
23
+ import { assertWorkControllerOwnership, compactHandle, contractFor, gitHead, identityFor, makeBoundedWorkResult, reconcileTerminalCleanup, requireSession, terminalCleanupOutcome, workForSession } from './work-execution-support';
24
+
25
+ function commandInputs(args: Record<string, unknown>): Array<Record<string, unknown>> {
26
+ if (Array.isArray(args.commands)) return args.commands.filter((value): value is Record<string, unknown> => Boolean(value && typeof value === 'object' && !Array.isArray(value)));
27
+ if (args.command !== undefined) return [{ command: args.command, cwd: args.cwd, approval_token: args.approval_token, timeout_ms: args.timeout_ms, max_output_bytes: args.max_output_bytes }];
28
+ throw new Error('COMMAND_REQUIRED: provide command or commands');
29
+ }
30
+
31
+ function authorizationRisk(command: RepositoryCommandValue, classification: ReturnType<typeof classifyRepositoryCommand>): AuthorizationRiskClass {
32
+ if (classification.risk === 'readonly') return 'readonly';
33
+ if (classification.risk === 'remote_write') return 'remote_write';
34
+ if (classification.risk === 'destructive') return 'destructive';
35
+ const executable = typeof command === 'string' ? command : command[0] ?? '';
36
+ if (typeof command === 'string' && /\b(?:npm|bun|pnpm|yarn)\s+(?:install|add|remove|update)\b/i.test(command)) return 'dependency_change';
37
+ if (/^\s*(?:git|.*[\\/]git)(?:\s|$)/i.test(executable)) return 'local_git';
38
+ return 'workspace_write';
39
+ }
40
+
41
+ export async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
42
+ const session = requireSession(ctx, args);
43
+ const handle = workForSession(ctx, session, args);
44
+ assertWorkControllerOwnership(ctx, session, handle, args);
45
+ const commands = commandInputs(args);
46
+ if (commands.length > 16) throw new Error('COMMAND_BATCH_TOO_LARGE: at most 16 commands per work_execute');
47
+ const cheap = validateWorkHandle(ctx.controllerHome, handle, identityFor(ctx, args), 'cheap', 'execute');
48
+ const inputs = commands.map((entry) => ({
49
+ command: commandValue(normalizeRepositoryCommand(entry.command)),
50
+ cwd: typeof entry.cwd === 'string' ? entry.cwd : undefined,
51
+ approvalToken: typeof entry.approval_token === 'string' ? entry.approval_token : undefined,
52
+ }));
53
+ const classifications = inputs.map((entry) => classifyRepositoryCommand(entry.command, cheap.repository.defaultBranch));
54
+ const requiresFull = classifications.some((classification) => classification.risk !== 'readonly');
55
+ if (requiresFull) validateWorkHandle(ctx.controllerHome, handle, identityFor(ctx, args), 'full', 'execute');
56
+ const decisions: AuthorizationDecision[] = [];
57
+ const approvalRequestId = typeof args.approval_request_id === 'string' ? args.approval_request_id.trim() : '';
58
+ const resolvedRequest = approvalRequestId
59
+ ? assertResolvedAuthorization({ controllerHome: ctx.controllerHome, repositoryId: handle.repositoryId, approvalRequestId, sessionId: session.sessionId, principalId: session.principalId, workId: handle.workId, permissionSnapshotVersion: handle.permissionSnapshotVersion, command: inputs[0]?.command })
60
+ : undefined;
61
+ for (const [index, entry] of inputs.entries()) {
62
+ const classification = classifications[index]!;
63
+ const outsideCwd = Boolean(entry.cwd && (isAbsolute(entry.cwd) || (() => {
64
+ const rel = relative(resolve(handle.worktreePath), resolve(handle.worktreePath, entry.cwd));
65
+ return rel === '..' || rel.startsWith('../') || rel.startsWith('..\\');
66
+ })()));
67
+ const risk = outsideCwd ? 'outside_repository' : authorizationRisk(entry.command, classification);
68
+ const decision = resolvedRequest
69
+ ? { decision: 'allow', source: 'user_confirmation', reason: 'The user resolved the exact approval request for this command.' } as const
70
+ : decideAuthorization({
71
+ controllerHome: ctx.controllerHome,
72
+ accessMode: readRepositoryAccessPolicy(ctx.controllerHome, handle.repositoryId).mode,
73
+ risk,
74
+ repositoryId: handle.repositoryId,
75
+ currentRepositoryId: handle.repositoryId,
76
+ workId: handle.workId,
77
+ boundWorkId: handle.workId,
78
+ goalId: handle.goalId,
79
+ boundGoalId: handle.goalId,
80
+ sessionId: session.sessionId,
81
+ principalId: session.principalId,
82
+ permissionSnapshotVersion: handle.permissionSnapshotVersion,
83
+ delegation: session.goalDelegation,
84
+ worktreePath: handle.worktreePath,
85
+ cwd: entry.cwd,
86
+ command: entry.command,
87
+ approvedByUser: Boolean(resolvedRequest),
88
+ });
89
+ decisions.push(decision);
90
+ if (decision.decision !== 'allow') return { authorization: decision, work: compactHandle(handle), command: entry.command };
91
+ }
92
+ const invocationId = typeof args.request_id === 'string' && args.request_id.trim()
93
+ ? args.request_id.trim()
94
+ : `invoke-${session.sessionId}-${handle.workId}-${Date.now()}-${randomUUID().slice(0, 8)}`;
95
+ const invocationFingerprint = createHash('sha256')
96
+ .update(JSON.stringify({
97
+ tool: 'work_execute',
98
+ workId: handle.workId,
99
+ commands: inputs.map((entry) => ({ command: entry.command, cwd: entry.cwd ?? null })),
100
+ timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : null,
101
+ maxOutputBytes: typeof args.max_output_bytes === 'number' ? args.max_output_bytes : null,
102
+ }))
103
+ .digest('hex');
104
+ claimProcessInvocation({
105
+ controllerHome: ctx.controllerHome,
106
+ repoId: handle.repositoryId,
107
+ checkoutId: handle.checkoutId,
108
+ requestId: invocationId,
109
+ invocationFingerprint,
110
+ });
111
+ const run = async (entry: typeof inputs[number], index: number) => {
112
+ const commandId = `${invocationId}:command:${index + 1}`;
113
+ const execution = await executeRepositoryCommandViaProcessRuntime({
114
+ controllerHome: ctx.controllerHome,
115
+ repository: cheap.worktreeRepository,
116
+ executionIdentity: executionIdentityForWork(cheap.worktreeRepository, handle),
117
+ command: entry.command,
118
+ cwd: entry.cwd,
119
+ workId: handle.workId,
120
+ commandId,
121
+ requestId: commandId,
122
+ timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
123
+ maxOutputBytes: typeof args.max_output_bytes === 'number' ? args.max_output_bytes : undefined,
124
+ });
125
+ const process = execution.process;
126
+ const ok = process ? process.ok === true : execution.ok === true;
127
+ const status = process
128
+ ? (process.completed
129
+ ? (process.cancelled ? 'cancelled' : process.timedOut ? 'timed_out' : ok ? 'executed' : 'failed')
130
+ : 'running')
131
+ : execution.route === 'process_direct'
132
+ ? (ok ? 'executed' : 'failed')
133
+ : execution.route === 'durable'
134
+ ? 'deferred_durable'
135
+ : 'rejected';
136
+ return {
137
+ processId: process?.processId,
138
+ commandId,
139
+ requestId: commandId,
140
+ status,
141
+ ok,
142
+ exitCode: process?.exitCode ?? execution.exitCode,
143
+ timedOut: process?.timedOut === true,
144
+ cancelled: process?.cancelled === true,
145
+ startedAt: process?.startedAt,
146
+ finishedAt: process?.completed ? process.startedAt : undefined,
147
+ stdout: process?.stdout ?? execution.stdout,
148
+ stderr: process?.stderr ?? execution.stderr,
149
+ logArtifact: process?.processId ? { processId: process.processId, kind: 'process_logs' } : undefined,
150
+ route: execution.route,
151
+ reason: execution.reason,
152
+ authorizationDecision: decisions[index],
153
+ approvalRequestId: resolvedRequest?.approvalRequestId,
154
+ authorization: decisions[index]?.decision === 'allow'
155
+ ? (resolvedRequest ? 'confirmed_plan' : decisions[index]?.source)
156
+ : 'explicit_user_request',
157
+ durableSideEffects: execution.durableSideEffects,
158
+ process,
159
+ };
160
+ };
161
+ const started = performance.now();
162
+ const executions = classifications.every((classification) => classification.risk === 'readonly')
163
+ ? await Promise.all(inputs.map((entry, index) => run(entry, index)))
164
+ : await (async () => {
165
+ const ordered: Awaited<ReturnType<typeof run>>[] = [];
166
+ for (const [index, entry] of inputs.entries()) {
167
+ const execution = await run(entry, index);
168
+ ordered.push(execution);
169
+ // Do not launch another mutating command while a Work-owned Lightweight
170
+ // process is still changing the workspace. This is sequencing, not a
171
+ // durable Lease boundary; the caller resumes through process_wait.
172
+ if (execution.process && !execution.process.completed) break;
173
+ }
174
+ return ordered;
175
+ })();
176
+ const branch = repositoryGitStatus(cheap.worktreeRepository).branch;
177
+ const head = gitHead(cheap.worktreeRepository.canonicalRoot);
178
+ let nextHandle = handle;
179
+ if (branch !== handle.branch) nextHandle = markWorkHandleFailed(ctx.controllerHome, handle, `command changed the bound branch to ${branch ?? 'detached'}`);
180
+ else nextHandle = transitionWorkHandle(ctx.controllerHome, handle, 'editing', { expectedHead: head, failureReason: undefined });
181
+ const value = {
182
+ work: compactHandle(nextHandle),
183
+ commands: executions,
184
+ executedCount: executions.filter((entry) => entry.status === 'executed' && entry.ok === true).length,
185
+ managedProcessCount: executions.filter((entry) => entry.process && !entry.process.completed).length,
186
+ deferredCommandCount: Math.max(0, inputs.length - executions.length),
187
+ authorization: decisions[0],
188
+ requestId: invocationId,
189
+ };
190
+ const response = makeBoundedWorkResult(ctx, session, handle.repositoryId, handle.workId, 'command', value);
191
+ recordMcpTiming(ctx.controllerHome, { tool: 'work_execute', workHandleValidationMs: 0, commandExecutionMs: Math.round((performance.now() - started) * 100) / 100, totalToolDurationMs: Math.round((performance.now() - started) * 100) / 100, sessionId: session.sessionId, repoId: handle.repositoryId, workId: handle.workId });
192
+ return response;
193
+ }
194
+
195
+ export function selectDefaultWorkValidationChecks(
196
+ contract: ReturnType<typeof contractFor>,
197
+ changedPaths: string[],
198
+ ): string[] {
199
+ if (!contract || changedPaths.length === 0) return [];
200
+ if (contract.risk === 'medium' || contract.risk === 'high' || contract.risk === 'destructive') {
201
+ return [...contract.checks];
202
+ }
203
+ const sourceTypeScriptChanged = changedPaths.some((path) =>
204
+ /\.(?:ts|tsx|mts|cts)$/.test(path)
205
+ && !/(?:^|\/)(?:tests?|__tests__|fixtures)(?:\/|$)/.test(path));
206
+ return contract.checks.filter((checkId) => {
207
+ const normalized = checkId.toLowerCase();
208
+ if (normalized.includes('package:test') || normalized.includes('full') || normalized.includes('architecture') || normalized.includes('release') || normalized.includes('runtime')) {
209
+ return false;
210
+ }
211
+ if (normalized.includes('type')) return sourceTypeScriptChanged;
212
+ return normalized.includes('focused') || normalized.includes('unit') || normalized.includes('changed') || normalized.includes('target');
213
+ });
214
+ }
215
+
216
+ export async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
217
+ const session = requireSession(ctx, args);
218
+ const handle = workForSession(ctx, session, args, {
219
+ reconcileValidation: false,
220
+ allowClaimedTerminalCleanup: args.cleanup !== false,
221
+ });
222
+ const terminalOutcome = terminalCleanupOutcome(ctx, handle);
223
+ if (terminalOutcome && args.cleanup !== false) {
224
+ return await reconcileTerminalCleanup(ctx, session, handle, args, terminalOutcome);
225
+ }
226
+ const validated = validateWorkHandle(ctx.controllerHome, handle, identityFor(ctx, args), 'full', 'validate');
227
+ const contract = contractFor(ctx, handle);
228
+ const changed = repositoryGitDiff(validated.worktreeRepository, { maxBytes: 64 * 1024 });
229
+ const changedPaths = Array.isArray(changed.nameOnly) ? changed.nameOnly.map(String) : [];
230
+ const requestedChecks = Array.isArray(args.check_ids)
231
+ ? args.check_ids.map(String).filter(Boolean)
232
+ : selectDefaultWorkValidationChecks(contract, changedPaths);
233
+ const validationInvocationId = typeof args.request_id === 'string' && args.request_id.trim()
234
+ ? args.request_id.trim()
235
+ : `validate-${session.sessionId}-${handle.workId}-${Date.now()}-${randomUUID().slice(0, 8)}`;
236
+ const validationStatus = repositoryGitStatus(validated.worktreeRepository);
237
+ const validationHead = validationStatus.head
238
+ ?? handle.expectedHead
239
+ ?? handle.baseCommit
240
+ ?? 'unknown';
241
+ const workspaceFingerprint = workspaceValidationFingerprint(
242
+ validated.worktreeRepository.canonicalRoot,
243
+ validationStatus,
244
+ );
245
+ const validationFingerprint = workValidationInputFingerprint(
246
+ validationHead,
247
+ workspaceFingerprint,
248
+ requestedChecks,
249
+ );
250
+ const previousRun = handle.validationRun?.fingerprint === validationFingerprint
251
+ ? handle.validationRun
252
+ : undefined;
253
+ let validationRun: NonNullable<WorkHandleState['validationRun']> = previousRun ?? {
254
+ fingerprint: validationFingerprint,
255
+ head: validationHead,
256
+ workspaceFingerprint,
257
+ requestedChecks,
258
+ resumeState: handle.state === 'committed' || handle.state === 'merged' ? handle.state : 'editing',
259
+ processes: {},
260
+ };
261
+ let current = transitionWorkHandle(ctx.controllerHome, handle, 'validating', {
262
+ finalization: { ...handle.finalization, validation: 'pending', lastError: undefined },
263
+ validationRun,
264
+ });
265
+ markWorkValidationPending(ctx.controllerHome, current);
266
+ const available = new Set(listControllerChecks(validated.worktreeRepository.canonicalRoot).map((check) => check.id));
267
+ const checks: Array<Record<string, unknown>> = [];
268
+ for (const [index, checkId] of requestedChecks.entries()) {
269
+ if (!available.has(checkId)) {
270
+ checks.push({ checkId, ok: false, status: 'missing', summary: `Check not found: ${checkId}` });
271
+ break;
272
+ }
273
+ const reusablePass = contract
274
+ ? [...effectiveVerificationEvidence(contract.checkRefs, {
275
+ sourceRevision: validationHead,
276
+ workspaceFingerprint,
277
+ checkId,
278
+ requestedChecks,
279
+ })].reverse().find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))
280
+ : undefined;
281
+ if (reusablePass) {
282
+ checks.push({
283
+ checkId,
284
+ ok: true,
285
+ status: 'passed',
286
+ reusedEvidence: true,
287
+ receipt: reusablePass.record.receipt,
288
+ });
289
+ continue;
290
+ }
291
+ const existingBinding = validationRun.processes[checkId];
292
+ let process = existingBinding
293
+ ? getCheckProcessHandle(ctx.controllerHome, handle.repositoryId, existingBinding.processId)
294
+ : undefined;
295
+ if (existingBinding && !process) {
296
+ checks.push({
297
+ checkId,
298
+ ok: false,
299
+ status: 'infrastructure_failure',
300
+ summary: `Validation process record is unavailable: ${existingBinding.processId}`,
301
+ });
302
+ break;
303
+ }
304
+ if (!process) {
305
+ const processRequestId = `${validationInvocationId}:check:${index + 1}`;
306
+ const executed = await runPersistedCheckViaProcessRuntime({
307
+ controllerHome: ctx.controllerHome,
308
+ repoId: handle.repositoryId,
309
+ checkoutId: handle.checkoutId,
310
+ repoRoot: validated.worktreeRepository.canonicalRoot,
311
+ executionIdentity: Object.freeze({
312
+ ...executionIdentityForWork(validated.worktreeRepository, handle),
313
+ expectedHead: validationHead === 'unknown' ? handle.expectedHead : validationHead,
314
+ }),
315
+ checkId,
316
+ requestId: processRequestId,
317
+ workId: handle.workId,
318
+ commandId: processRequestId,
319
+ verificationBinding: { executionSessionId: session.sessionId },
320
+ verificationSnapshot: contract && contract.allowedPaths.length > 0 ? {
321
+ workId: contract.workId,
322
+ allowedPaths: contract.allowedPaths,
323
+ forbiddenPaths: contract.forbiddenPaths,
324
+ } : undefined,
325
+ });
326
+ if (executed.mode === 'durable') {
327
+ checks.push({ checkId, ok: undefined, status: 'deferred', summary: executed.durable?.reason, durable: executed.durable });
328
+ break;
329
+ }
330
+ process = executed.process!;
331
+ const launchedRecord = getProcessRecord(ctx.controllerHome, handle.repositoryId, process.processId);
332
+ validationRun = {
333
+ ...validationRun,
334
+ processes: {
335
+ ...validationRun.processes,
336
+ [checkId]: {
337
+ processId: process.processId,
338
+ requestId: processRequestId,
339
+ ...(launchedRecord?.checkExecution ? { checkExecution: { ...launchedRecord.checkExecution } } : {}),
340
+ },
341
+ },
342
+ };
343
+ current = transitionWorkHandle(ctx.controllerHome, current, 'validating', { validationRun });
344
+ }
345
+ if (!process.completed) {
346
+ checks.push({ checkId, ok: undefined, status: 'running', process });
347
+ break;
348
+ }
349
+ const record = getProcessRecord(ctx.controllerHome, handle.repositoryId, process.processId);
350
+ if (!record) {
351
+ checks.push({ checkId, ok: false, status: 'infrastructure_failure', summary: `Validation process record is unavailable: ${process.processId}` });
352
+ break;
353
+ }
354
+ const boundCheckExecution = validationRun.processes[checkId]?.checkExecution;
355
+ const receipt = processCheckCompletionReceipt(record, {
356
+ repoId: handle.repositoryId,
357
+ checkoutId: handle.checkoutId,
358
+ workId: handle.workId,
359
+ checkId,
360
+ processId: process.processId,
361
+ ...(boundCheckExecution ? {
362
+ checkExecution: {
363
+ cacheKey: boundCheckExecution.cacheKey,
364
+ revision: boundCheckExecution.revision,
365
+ definitionDigest: boundCheckExecution.definitionDigest,
366
+ environmentFingerprint: boundCheckExecution.environmentFingerprint,
367
+ timeoutMs: boundCheckExecution.timeoutMs,
368
+ scopeKey: boundCheckExecution.scopeKey,
369
+ },
370
+ } : {}),
371
+ });
372
+ const terminalEvidence = classifyPersistedCheckTerminalEvidence(record, checkId);
373
+ const infrastructureFailed = receipt.timedOut
374
+ || receipt.cancelled
375
+ || terminalEvidence.state !== 'matched'
376
+ || (!receipt.ok && terminalEvidence.failureClass !== 'acceptance_failure');
377
+ appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, handle.workId, {
378
+ checkId,
379
+ outcome: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'valid_pass' : 'valid_fail',
380
+ summary: receipt.summary,
381
+ recordedAt: receipt.finishedAt,
382
+ sourceRevision: validationHead,
383
+ workspaceFingerprint,
384
+ verificationInputFingerprint: verificationInputFingerprint({
385
+ sourceRevision: validationHead,
386
+ workspaceFingerprint,
387
+ checkId,
388
+ requestedChecks,
389
+ }),
390
+ commandFingerprint: commandFingerprint(checkId, receipt.commandId),
391
+ resultArtifactId: receipt.receiptId,
392
+ startedAt: receipt.startedAt,
393
+ completedAt: receipt.finishedAt,
394
+ evidenceRef: { title: checkId, summary: `${receipt.receiptId}; artifact=${receipt.artifactPath}`, detailLevel: 'summary' },
395
+ receipt,
396
+ });
397
+ checks.push({
398
+ checkId,
399
+ ok: receipt.ok,
400
+ status: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'passed' : 'failed',
401
+ process,
402
+ receipt,
403
+ ...(terminalEvidence.warning ? { warning: terminalEvidence.warning } : {}),
404
+ });
405
+ if (!receipt.ok) break;
406
+ }
407
+ const infrastructureFailure = checks.find((check) => (
408
+ check.status === 'missing'
409
+ || check.status === 'infrastructure_failure'
410
+ || check.status === 'deferred'
411
+ ));
412
+ const acceptedFailure = checks.find((check) => check.status === 'failed');
413
+ const allObserved = checks.length === requestedChecks.length && checks.every((check) => check.ok !== undefined);
414
+ const completed = Boolean(infrastructureFailure || acceptedFailure || allObserved);
415
+ const passed = completed
416
+ && !infrastructureFailure
417
+ && !acceptedFailure
418
+ && checks.every((check) => check.ok === true);
419
+ const failureSummary = infrastructureFailure
420
+ ? String(infrastructureFailure.summary ?? 'validation infrastructure failure')
421
+ : acceptedFailure
422
+ ? String(acceptedFailure.summary ?? 'targeted validation failed')
423
+ : undefined;
424
+ const nextState = !completed
425
+ ? 'validating'
426
+ : passed
427
+ ? validationRun.resumeState
428
+ : 'failed';
429
+ const validation = !completed ? 'pending' : passed ? 'done' : 'failed';
430
+ const next = transitionWorkHandle(ctx.controllerHome, current, nextState, {
431
+ finalization: {
432
+ ...current.finalization,
433
+ validation,
434
+ lastError: failureSummary,
435
+ },
436
+ validationRun: completed ? undefined : validationRun,
437
+ ...(passed ? { validatedInputFingerprint: validationFingerprint } : {}),
438
+ ...(failureSummary ? { failureReason: failureSummary } : { failureReason: undefined }),
439
+ });
440
+ if (completed) {
441
+ projectWorkValidationOutcome(
442
+ ctx.controllerHome,
443
+ next,
444
+ infrastructureFailure ? 'infrastructure_failure' : passed ? 'passed' : 'failed',
445
+ failureSummary,
446
+ );
447
+ }
448
+ if (completed && acceptedFailure) {
449
+ const cleanup = await reconcileTerminalCleanup(ctx, session, next, args, 'validation_failed');
450
+ const value = {
451
+ ...cleanup,
452
+ validation: { passed, completed, checks, targeted: true, changedPaths, cleanupTriggered: true },
453
+ };
454
+ return makeBoundedWorkResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
455
+ }
456
+ const value = {
457
+ work: compactHandle(next),
458
+ validation: {
459
+ passed,
460
+ completed,
461
+ checks,
462
+ targeted: true,
463
+ changedPaths,
464
+ ...(infrastructureFailure ? { retryable: true, cleanupTriggered: false } : {}),
465
+ },
466
+ };
467
+ return makeBoundedWorkResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
468
+ }
@@ -0,0 +1,37 @@
1
+ import { globMatches } from '../../../cli/mcp/paths';
2
+ import type { WorkContract } from '../facade/types';
3
+
4
+ export interface WorkPathScopeViolation {
5
+ kind: 'forbidden' | 'out_of_scope';
6
+ path: string;
7
+ pattern?: string;
8
+ }
9
+
10
+ /**
11
+ * WorkContract path scope is the durable mutation authority. Edit-session or
12
+ * caller-local path filters may narrow this scope, but they can never widen it.
13
+ */
14
+ export function findWorkPathScopeViolation(
15
+ work: Pick<WorkContract, 'allowedPaths' | 'forbiddenPaths'>,
16
+ paths: readonly string[],
17
+ ): WorkPathScopeViolation | undefined {
18
+ for (const path of [...new Set(paths.map((value) => value.trim()).filter(Boolean))]) {
19
+ const forbidden = work.forbiddenPaths.find((pattern) => globMatches(pattern, path));
20
+ if (forbidden) return { kind: 'forbidden', path, pattern: forbidden };
21
+ if (work.allowedPaths.length > 0 && !work.allowedPaths.some((pattern) => globMatches(pattern, path))) {
22
+ return { kind: 'out_of_scope', path };
23
+ }
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ export function assertWorkPathsWithinScope(
29
+ work: Pick<WorkContract, 'allowedPaths' | 'forbiddenPaths'>,
30
+ paths: readonly string[],
31
+ codes: { forbidden: string; outOfScope: string },
32
+ ): void {
33
+ const violation = findWorkPathScopeViolation(work, paths);
34
+ if (!violation) return;
35
+ if (violation.kind === 'forbidden') throw new Error(`${codes.forbidden}: ${violation.path}`);
36
+ throw new Error(`${codes.outOfScope}: ${violation.path}`);
37
+ }