@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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/docs/architecture/CURRENT.md +12 -4
- package/docs/forge-plugin-management.md +1 -1
- package/docs/operations/controller-performance-and-502.md +19 -0
- package/docs/operations/homebrew.md +2 -2
- package/package.json +5 -2
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-work-efficiency.ts +0 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1531
- package/scripts/check-architecture-sync.sh +2 -302
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-runtime-architecture.mjs +243 -16
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +0 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -702
- package/scripts/ensure-task-workflow.sh +1 -1192
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/lib/project-init-lib.sh +7 -2
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -33
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -771
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +3 -0
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -531
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -593
- package/scripts/verify-sprint.sh +1 -500
- package/scripts/workflow-contract.ts +4 -201
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/controller/check-runner.ts +7 -2
- package/src/cli/controller/context-pack.ts +46 -21
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +2 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/local-bridge/facade-api.ts +37 -47
- package/src/cli/local-bridge/server.ts +2 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -33
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +85 -89
- package/src/cli/mcp/server.ts +57 -13
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset.ts +2 -2
- package/src/cli/mcp/transports/http.ts +23 -0
- package/src/cli/repositories/command-classifier.ts +2 -2
- package/src/cli/repositories/command-executor.ts +24 -6
- package/src/cli/repositories/repository-snapshot.ts +5 -2
- package/src/cli/repositories/structured-git.ts +57 -3
- package/src/cli/repository/inspector.ts +11 -3
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +9 -61
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
- package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
- package/src/runtime/control-plane/facade/index.ts +2 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/types.ts +68 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
- package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +18 -6
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/process-runtime/check-result.ts +53 -18
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
- package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
- package/src/runtime/execution/process-runtime/runtime.ts +10 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -12
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -13
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/router.ts +2 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
- package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
- package/src/runtime/plugins/browser-adapter.ts +256 -1083
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +18 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +10 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
- package/src/runtime/plugins/external-adapter.ts +92 -58
- package/src/runtime/plugins/macos-capability-broker.ts +0 -8
- package/src/runtime/plugins/store.ts +66 -9
- package/src/runtime/plugins/types.ts +3 -1
- package/src/runtime/recovery/maintenance-executor.ts +61 -25
- package/src/runtime/root/package-connector-service.ts +46 -4
- package/src/runtime/root/package-runtime-release.ts +1 -1
- package/src/runtime/root/package-runtime-service.ts +33 -2
- package/src/runtime/root/release-manifest.ts +137 -16
- package/src/runtime/root/release-materialize.ts +109 -35
- package/src/runtime/root/types.ts +10 -0
- package/src/runtime/standalone-recovery/core.ts +425 -194
- package/src/runtime/standalone-recovery/entry.ts +1 -1
- package/src/runtime/standalone-recovery/installer.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +113 -87
- package/src/runtime/workflow/schedules/settlement.ts +26 -23
- package/src/runtime/workflow/schedules/store.ts +79 -10
|
@@ -2,9 +2,9 @@ import { bindRepositoryEntities } from '../repositories/entity-migration';
|
|
|
2
2
|
import { bootstrapLocalProject, diagnoseLatestLocalProjectSource } from '../repositories/local-project-onboarding';
|
|
3
3
|
import { resolveEphemeralWorkspaceTarget } from '../repositories/ephemeral-workspace';
|
|
4
4
|
import type { ResolvedExecutionIdentity } from '../../runtime/control-plane/execution/execution-identity';
|
|
5
|
-
import {
|
|
5
|
+
import { assertNoBoundExecutionSessionMutation, resolveClaimedRepositoryWorkId, resolveExplicitClaimedRepositoryWork, type RepositoryWorkAttributionCaller } from '../../runtime/control-plane/execution/repository-work-attribution';
|
|
6
6
|
import { getWorkContract } from '../../runtime/control-plane/facade/work-contract-store';
|
|
7
|
-
import {
|
|
7
|
+
import { assertWorkPathsWithinScope } from '../../runtime/control-plane/execution/work-path-scope';
|
|
8
8
|
import { isTerminalWorkContractStatus } from '../../runtime/control-plane/facade/types';
|
|
9
9
|
import { executeRepositoryCommand, previewRepositoryCommandExecution } from '../repositories/command-executor';
|
|
10
10
|
import { withControllerLock } from '../repositories/locks';
|
|
@@ -53,11 +53,12 @@ import {
|
|
|
53
53
|
executeRepositoryCommandViaProcessRuntime,
|
|
54
54
|
} from '../../runtime/execution/process-runtime/command-facade';
|
|
55
55
|
import { assessWorkMode, parseExplicitTaskMode } from '../controller/work-mode';
|
|
56
|
+
import { classifyRepositoryCommand } from '../repositories/command-classifier';
|
|
56
57
|
import { normalizeRepositoryCommand } from '../repositories/command-normalization';
|
|
57
58
|
import { readRepositoryRange } from '../repository/inspector';
|
|
58
59
|
import { getMcpPolicy } from './policy';
|
|
59
60
|
import { redactMcpText } from './redaction';
|
|
60
|
-
import type { CallToolResult, McpToolDefinition } from './
|
|
61
|
+
import type { CallToolResult, McpToolDefinition } from './tool-contract';
|
|
61
62
|
import {
|
|
62
63
|
boundUtf8,
|
|
63
64
|
compactCommandOutput,
|
|
@@ -287,87 +288,7 @@ function withResponseMeta(payload: Record<string, unknown>, startedAt: number):
|
|
|
287
288
|
return response;
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
export
|
|
291
|
-
sessionId?: string;
|
|
292
|
-
principalId?: string;
|
|
293
|
-
controllerInstanceId?: string;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function claimedExplicitWork(
|
|
297
|
-
controllerHome: string,
|
|
298
|
-
repoId: string,
|
|
299
|
-
caller: RepositoryToolCallerContext | undefined,
|
|
300
|
-
requestedWorkId: string,
|
|
301
|
-
) {
|
|
302
|
-
if (!caller?.principalId?.trim()) return undefined;
|
|
303
|
-
const work = getWorkContract({ controllerHome, repoId }, requestedWorkId);
|
|
304
|
-
if (!work || isTerminalWorkContractStatus(work.status)) throw new Error(`WORK_ATTRIBUTION_INVALID: ${requestedWorkId}`);
|
|
305
|
-
const owner = getControllerSession({ controllerHome, repoId }, requestedWorkId);
|
|
306
|
-
if (!owner) throw new Error(`WORK_CONTROLLER_CLAIM_REQUIRED: ${requestedWorkId}`);
|
|
307
|
-
if ((owner.principalId?.trim() || owner.controllerId) !== caller.principalId.trim()) throw new Error(`WORK_CONTROLLER_OWNERSHIP_MISMATCH: ${requestedWorkId}`);
|
|
308
|
-
return work;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function assertNoBoundExecutionSessionMutation(
|
|
312
|
-
controllerHome: string,
|
|
313
|
-
repository: ReturnType<typeof resolveRepositorySelection>,
|
|
314
|
-
caller?: RepositoryToolCallerContext,
|
|
315
|
-
): void {
|
|
316
|
-
if (!caller?.principalId?.trim() || !caller.sessionId?.trim()) return;
|
|
317
|
-
const executionSession = readExecutionSession(controllerHome, {
|
|
318
|
-
sessionId: caller.sessionId,
|
|
319
|
-
principalId: caller.principalId,
|
|
320
|
-
controllerInstanceId: caller.controllerInstanceId,
|
|
321
|
-
});
|
|
322
|
-
const workId = executionSession?.activeWorkId?.trim();
|
|
323
|
-
if (!workId || executionSession?.activeRepositoryId !== repository.repoId) return;
|
|
324
|
-
const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
|
|
325
|
-
if (!work) throw new Error(`WORK_ATTRIBUTION_INVALID: ${workId}`);
|
|
326
|
-
if (isTerminalWorkContractStatus(work.status)) {
|
|
327
|
-
throw new Error(`WORK_ATTRIBUTION_TERMINAL: ${work.workId}:${work.status}`);
|
|
328
|
-
}
|
|
329
|
-
throw new Error(`WORK_ATTRIBUTION_REQUIRED: ${work.workId}; active execution session mutations must pass work_id explicitly`);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function claimedSessionWorkId(
|
|
333
|
-
controllerHome: string,
|
|
334
|
-
repository: ReturnType<typeof resolveRepositorySelection>,
|
|
335
|
-
caller?: RepositoryToolCallerContext,
|
|
336
|
-
explicitWorkId?: unknown,
|
|
337
|
-
): string | undefined {
|
|
338
|
-
const requestedWorkId = typeof explicitWorkId === 'string' ? explicitWorkId.trim() : '';
|
|
339
|
-
if (requestedWorkId) {
|
|
340
|
-
const work = claimedExplicitWork(controllerHome, repository.repoId, caller, requestedWorkId);
|
|
341
|
-
if (!work) return undefined;
|
|
342
|
-
if (work.checkoutId && work.checkoutId !== repository.activeCheckoutId) throw new Error(`WORK_CHECKOUT_MISMATCH: ${requestedWorkId}:${repository.activeCheckoutId}`);
|
|
343
|
-
return requestedWorkId;
|
|
344
|
-
}
|
|
345
|
-
if (!caller?.principalId?.trim()) return undefined;
|
|
346
|
-
if (caller.sessionId?.trim()) {
|
|
347
|
-
const executionSession = readExecutionSession(controllerHome, {
|
|
348
|
-
sessionId: caller.sessionId,
|
|
349
|
-
principalId: caller.principalId,
|
|
350
|
-
controllerInstanceId: caller.controllerInstanceId,
|
|
351
|
-
});
|
|
352
|
-
const workId = executionSession?.activeWorkId?.trim();
|
|
353
|
-
if (workId && executionSession?.activeRepositoryId === repository.repoId && (!executionSession.activeCheckoutId || executionSession.activeCheckoutId === repository.activeCheckoutId)) {
|
|
354
|
-
const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
|
|
355
|
-
if (work && isTerminalWorkContractStatus(work.status)) {
|
|
356
|
-
throw new Error(`WORK_ATTRIBUTION_TERMINAL: ${work.workId}:${work.status}`);
|
|
357
|
-
}
|
|
358
|
-
const owner = getControllerSession({ controllerHome, repoId: repository.repoId }, workId);
|
|
359
|
-
if (work && owner?.sessionId === caller.sessionId && (owner.principalId?.trim() || owner.controllerId) === caller.principalId.trim()) return workId;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
const principal = caller.principalId.trim();
|
|
363
|
-
const candidates = listControllerSessions({ controllerHome, repoId: repository.repoId })
|
|
364
|
-
.filter((owner) => (owner.principalId?.trim() || owner.controllerId) === principal)
|
|
365
|
-
.map((owner) => ({ owner, work: getWorkContract({ controllerHome, repoId: repository.repoId }, owner.workId) }))
|
|
366
|
-
.filter((entry): entry is { owner: ReturnType<typeof listControllerSessions>[number]; work: NonNullable<ReturnType<typeof getWorkContract>> } => Boolean(entry.work && !isTerminalWorkContractStatus(entry.work.status) && (!entry.work.checkoutId || entry.work.checkoutId === repository.activeCheckoutId)));
|
|
367
|
-
if (candidates.length === 1) return candidates[0].work.workId;
|
|
368
|
-
if (candidates.length > 1) throw new Error(`WORK_ATTRIBUTION_AMBIGUOUS: principal ${principal} owns ${candidates.length} active Works on checkout ${repository.activeCheckoutId}`);
|
|
369
|
-
return undefined;
|
|
370
|
-
}
|
|
291
|
+
export type RepositoryToolCallerContext = RepositoryWorkAttributionCaller;
|
|
371
292
|
|
|
372
293
|
export function claimedSessionEditBinding(
|
|
373
294
|
controllerHome: string,
|
|
@@ -375,7 +296,7 @@ export function claimedSessionEditBinding(
|
|
|
375
296
|
caller?: RepositoryToolCallerContext,
|
|
376
297
|
explicitWorkId?: unknown,
|
|
377
298
|
): EditSessionBinding | undefined {
|
|
378
|
-
const workId =
|
|
299
|
+
const workId = resolveClaimedRepositoryWorkId(controllerHome, repository, caller, explicitWorkId);
|
|
379
300
|
if (!workId || !caller?.principalId?.trim()) return undefined;
|
|
380
301
|
const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
|
|
381
302
|
if (!work) return undefined;
|
|
@@ -423,7 +344,7 @@ function resolveRepositorySelectionForClaimedWork(
|
|
|
423
344
|
allowSoleRepository: true,
|
|
424
345
|
});
|
|
425
346
|
if (!explicitWorkId || checkoutId) return repository;
|
|
426
|
-
const work =
|
|
347
|
+
const work = resolveExplicitClaimedRepositoryWork(controllerHome, repository, caller, explicitWorkId);
|
|
427
348
|
if (!work?.checkoutId || work.checkoutId === repository.activeCheckoutId) return repository;
|
|
428
349
|
return resolveRepositorySelection({
|
|
429
350
|
repoId: repository.repoId,
|
|
@@ -445,6 +366,32 @@ function rawDefaultBranchMergeCommand(repository: ReturnType<typeof resolveRepos
|
|
|
445
366
|
return /(?:^|[;&|]\s*)git\s+merge\b/.test(normalized.shellCommand ?? '');
|
|
446
367
|
}
|
|
447
368
|
|
|
369
|
+
interface HistoricalReadOnlyWorkContext {
|
|
370
|
+
workId: string;
|
|
371
|
+
status: string;
|
|
372
|
+
mode: 'historical_read_only_observation';
|
|
373
|
+
attribution: 'not_active_work';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function historicalReadOnlyWorkContext(
|
|
377
|
+
controllerHome: string,
|
|
378
|
+
repository: ReturnType<typeof resolveRepositorySelection>,
|
|
379
|
+
args: Record<string, unknown>,
|
|
380
|
+
): HistoricalReadOnlyWorkContext | undefined {
|
|
381
|
+
const requestedWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
382
|
+
if (!requestedWorkId) return undefined;
|
|
383
|
+
const work = getWorkContract({ controllerHome, repoId: repository.repoId }, requestedWorkId);
|
|
384
|
+
if (!work || !isTerminalWorkContractStatus(work.status)) return undefined;
|
|
385
|
+
const classification = classifyRepositoryCommand(args.command as string | string[], repository.defaultBranch);
|
|
386
|
+
if (classification.risk !== 'readonly') return undefined;
|
|
387
|
+
return {
|
|
388
|
+
workId: work.workId,
|
|
389
|
+
status: work.status,
|
|
390
|
+
mode: 'historical_read_only_observation',
|
|
391
|
+
attribution: 'not_active_work',
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
448
395
|
function resolveRepositoryCommandTarget(
|
|
449
396
|
controllerHome: string,
|
|
450
397
|
args: Record<string, unknown>,
|
|
@@ -454,6 +401,7 @@ function resolveRepositoryCommandTarget(
|
|
|
454
401
|
repository: ReturnType<typeof resolveRepositorySelection>;
|
|
455
402
|
executionIdentity: ResolvedExecutionIdentity;
|
|
456
403
|
workspace?: { workspaceId: string; root: string; registered: false };
|
|
404
|
+
historicalWorkContext?: HistoricalReadOnlyWorkContext;
|
|
457
405
|
} {
|
|
458
406
|
const workspaceRoot = typeof args.workspace_root === 'string' ? args.workspace_root.trim() : '';
|
|
459
407
|
const checkoutId = typeof args.checkout_id === 'string' ? args.checkout_id.trim() : '';
|
|
@@ -474,10 +422,37 @@ function resolveRepositoryCommandTarget(
|
|
|
474
422
|
workspace: { workspaceId: target.workspaceId, root: target.canonicalRoot, registered: false },
|
|
475
423
|
};
|
|
476
424
|
}
|
|
425
|
+
// A terminal Work no longer owns mutation or checkout authority. An exact
|
|
426
|
+
// terminal work_id may still accompany a read-only observation immediately
|
|
427
|
+
// after finalize, so preserve it as explicit historical context while
|
|
428
|
+
// executing against the caller-selected/current repository checkout. Any
|
|
429
|
+
// non-read-only command continues through claimed Work resolution and fails
|
|
430
|
+
// closed for terminal attribution.
|
|
431
|
+
const selectedRepository = resolveRepositorySelection({
|
|
432
|
+
repoId: repoIdValue || undefined,
|
|
433
|
+
checkoutId: checkoutId || undefined,
|
|
434
|
+
controllerHome,
|
|
435
|
+
allowSoleRepository: true,
|
|
436
|
+
});
|
|
437
|
+
const historicalWorkContext = historicalReadOnlyWorkContext(controllerHome, selectedRepository, args);
|
|
438
|
+
if (historicalWorkContext) {
|
|
439
|
+
return {
|
|
440
|
+
repository: selectedRepository,
|
|
441
|
+
executionIdentity: {
|
|
442
|
+
schemaVersion: 1,
|
|
443
|
+
authority: 'repository',
|
|
444
|
+
repositoryId: selectedRepository.repoId,
|
|
445
|
+
checkoutId: selectedRepository.activeCheckoutId,
|
|
446
|
+
canonicalRoot: selectedRepository.canonicalRoot,
|
|
447
|
+
},
|
|
448
|
+
historicalWorkContext,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
477
452
|
const repository = resolveRepositorySelectionForClaimedWork(controllerHome, args, repoIdValue, caller);
|
|
478
453
|
const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
479
454
|
const workId = explicitWorkId
|
|
480
|
-
?
|
|
455
|
+
? resolveClaimedRepositoryWorkId(controllerHome, repository, caller, explicitWorkId)
|
|
481
456
|
: undefined;
|
|
482
457
|
return {
|
|
483
458
|
repository,
|
|
@@ -615,6 +590,7 @@ function compactProcessCommandPayload(input: {
|
|
|
615
590
|
repoId: string;
|
|
616
591
|
checkoutId?: string;
|
|
617
592
|
workspace?: { workspaceId: string; root: string; registered: false };
|
|
593
|
+
historicalWorkContext?: HistoricalReadOnlyWorkContext;
|
|
618
594
|
processId?: string;
|
|
619
595
|
process?: unknown;
|
|
620
596
|
completed?: boolean;
|
|
@@ -653,6 +629,7 @@ function compactProcessCommandPayload(input: {
|
|
|
653
629
|
repoId: input.repoId,
|
|
654
630
|
...(input.checkoutId ? { checkoutId: input.checkoutId } : {}),
|
|
655
631
|
...(input.workspace ? { workspace: input.workspace } : {}),
|
|
632
|
+
...(input.historicalWorkContext ? { historicalWorkContext: input.historicalWorkContext } : {}),
|
|
656
633
|
...(input.processId ? { processId: input.processId } : {}),
|
|
657
634
|
...(completed ? { exitCode: input.exitCode ?? (ok ? 0 : 1) } : {}),
|
|
658
635
|
// A running default response otherwise loses its only dependency-bound
|
|
@@ -693,6 +670,7 @@ function compactProcessCommandPayload(input: {
|
|
|
693
670
|
repoId: input.repoId,
|
|
694
671
|
...(input.checkoutId ? { checkoutId: input.checkoutId } : {}),
|
|
695
672
|
...(input.workspace ? { workspace: input.workspace } : {}),
|
|
673
|
+
...(input.historicalWorkContext ? { historicalWorkContext: input.historicalWorkContext } : {}),
|
|
696
674
|
...(input.processId ? { processId: input.processId } : {}),
|
|
697
675
|
ok: input.ok,
|
|
698
676
|
exitCode: input.exitCode,
|
|
@@ -998,6 +976,16 @@ export async function callRepositoryTool(
|
|
|
998
976
|
case 'repository_safe_patch_apply': {
|
|
999
977
|
const repository = resolveRepositorySelectionForClaimedWork(controllerHome, args, repoIdValue, caller);
|
|
1000
978
|
const binding = safePatchEditBinding(controllerHome, repository, caller, args);
|
|
979
|
+
if (binding?.workId) {
|
|
980
|
+
const work = getWorkContract({ controllerHome, repoId: repository.repoId }, binding.workId);
|
|
981
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${binding.workId}`);
|
|
982
|
+
const requestedPaths = buildSafePatchPlan(repository, { operations: args.operations, chunkSize: args.chunk_size })
|
|
983
|
+
.chunks.flatMap((chunk) => chunk.paths);
|
|
984
|
+
assertWorkPathsWithinScope(work, requestedPaths, {
|
|
985
|
+
forbidden: 'WORK_MUTATION_FORBIDDEN_PATH',
|
|
986
|
+
outOfScope: 'WORK_MUTATION_PATH_OUT_OF_SCOPE',
|
|
987
|
+
});
|
|
988
|
+
}
|
|
1001
989
|
const applied = withControllerLock(
|
|
1002
990
|
controllerHome,
|
|
1003
991
|
{ scope: 'repository', repoId: repository.repoId },
|
|
@@ -1098,7 +1086,7 @@ export async function callRepositoryTool(
|
|
|
1098
1086
|
case 'repository_command_execute': {
|
|
1099
1087
|
const explorationGuidance = fragmentedRepositoryExplorationGuidance(args.command);
|
|
1100
1088
|
const target = resolveRepositoryCommandTarget(controllerHome, args, repoIdValue, caller);
|
|
1101
|
-
const { repository, executionIdentity } = target;
|
|
1089
|
+
const { repository, executionIdentity, historicalWorkContext } = target;
|
|
1102
1090
|
const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
1103
1091
|
if (!explicitWorkId) assertNoBoundExecutionSessionMutation(controllerHome, repository, caller);
|
|
1104
1092
|
const deliveryWorkId = rawDefaultBranchMergeCommand(repository, args.command)
|
|
@@ -1206,6 +1194,13 @@ export async function callRepositoryTool(
|
|
|
1206
1194
|
});
|
|
1207
1195
|
}
|
|
1208
1196
|
if (routeClass.route === 'process_direct' || routeClass.route === 'process_managed' || routeClass.route === 'durable') {
|
|
1197
|
+
const processRequestId = typeof args.request_id === 'string' ? args.request_id.trim() : '';
|
|
1198
|
+
if (executionIdentity.workId && !processRequestId) {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
`WORK_PROCESS_REQUEST_ID_REQUIRED: ${executionIdentity.workId}; `
|
|
1201
|
+
+ 'Work-attributed Process Runtime commands require a stable request_id so reconnect/retry can resume the original Process without duplicate execution.',
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1209
1204
|
const processResult = await executeRepositoryCommandViaProcessRuntime({
|
|
1210
1205
|
controllerHome,
|
|
1211
1206
|
repository,
|
|
@@ -1217,7 +1212,7 @@ export async function callRepositoryTool(
|
|
|
1217
1212
|
: typeof args.interactive_wait_ms === 'number' ? args.interactive_wait_ms : undefined,
|
|
1218
1213
|
maxOutputBytes,
|
|
1219
1214
|
returnHandleImmediately,
|
|
1220
|
-
requestId:
|
|
1215
|
+
requestId: processRequestId || undefined,
|
|
1221
1216
|
workId: executionIdentity.workId,
|
|
1222
1217
|
executionIdentity,
|
|
1223
1218
|
allowNonGitWorkspace: target.workspace !== undefined,
|
|
@@ -1272,6 +1267,7 @@ export async function callRepositoryTool(
|
|
|
1272
1267
|
repoId: repository.repoId,
|
|
1273
1268
|
checkoutId: repository.activeCheckoutId,
|
|
1274
1269
|
workspace: target.workspace,
|
|
1270
|
+
historicalWorkContext,
|
|
1275
1271
|
processId: handle?.processId,
|
|
1276
1272
|
process: detailLevel === 'detail' ? handle : undefined,
|
|
1277
1273
|
completed,
|
package/src/cli/mcp/server.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type MultiRepositoryMcpToolContext,
|
|
14
14
|
} from './multi-repository';
|
|
15
15
|
import { callAccessTool } from './access-tools';
|
|
16
|
-
import {
|
|
16
|
+
import { callRepositoryToolWithPostFinalizeAttribution } from './post-finalize-work-attribution';
|
|
17
17
|
import { callRuntimeTool } from '../../runtime/gateway/mcp/runtime-tools';
|
|
18
18
|
import { callExecutionTool } from '../../runtime/gateway/mcp/execution-tools';
|
|
19
19
|
import { callProcessTool } from '../../runtime/gateway/mcp/process-tools';
|
|
@@ -450,6 +450,36 @@ export function sameCanonicalRuntimeProxyIdentity(
|
|
|
450
450
|
&& left.runtimeInstanceId === right.runtimeInstanceId;
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
export function canonicalRuntimeToolCallIsReplaySafe(name: string, args: Record<string, unknown>): boolean {
|
|
454
|
+
if (name !== 'repository_command_execute') return false;
|
|
455
|
+
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
456
|
+
const requestId = typeof args.request_id === 'string' ? args.request_id.trim() : '';
|
|
457
|
+
return Boolean(workId && requestId);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function canonicalRuntimeToolCallFailureIsTransient(error: unknown): boolean {
|
|
461
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error ?? '');
|
|
462
|
+
return /Connection closed|socket[^\n]*closed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|UND_ERR_SOCKET|fetch failed|MCP_REQUEST_FAILED/i.test(message);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function callCanonicalRuntimeToolWithReplay<T>(input: {
|
|
466
|
+
name: string;
|
|
467
|
+
args: Record<string, unknown>;
|
|
468
|
+
call: () => Promise<T>;
|
|
469
|
+
reconnect: () => Promise<void>;
|
|
470
|
+
}): Promise<T> {
|
|
471
|
+
try {
|
|
472
|
+
return await input.call();
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (!canonicalRuntimeToolCallIsReplaySafe(input.name, input.args)
|
|
475
|
+
|| !canonicalRuntimeToolCallFailureIsTransient(error)) {
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
await input.reconnect();
|
|
479
|
+
return await input.call();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
453
483
|
export interface CanonicalRuntimeProxy {
|
|
454
484
|
listTools(): Promise<{ tools: Tool[] }>;
|
|
455
485
|
callTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>, timing?: MutableGatewayProxyTiming): Promise<CallToolResult>;
|
|
@@ -743,22 +773,36 @@ export function createCanonicalRuntimeProxy(ctx: MultiRepositoryMcpToolContext):
|
|
|
743
773
|
}
|
|
744
774
|
},
|
|
745
775
|
async callTool(callerContext, name, args, timing = {}) {
|
|
746
|
-
const
|
|
776
|
+
const prepared = await prepareLane(timing, name === 'process_wait' ? 'wait' : 'interactive');
|
|
777
|
+
const laneId = prepared.laneId;
|
|
778
|
+
let activeClient = prepared.client;
|
|
747
779
|
const gatewayCallStartedAtMs = Date.now();
|
|
748
780
|
const callStarted = performance.now();
|
|
781
|
+
const forwardedRequest = {
|
|
782
|
+
name,
|
|
783
|
+
arguments: args,
|
|
784
|
+
_meta: canonicalRuntimeForwardingMeta(callerContext),
|
|
785
|
+
} as Parameters<Client['callTool']>[0];
|
|
749
786
|
try {
|
|
750
787
|
// The outer Connector session is already bound to the Canonical Runtime
|
|
751
788
|
// schema and fenced by the Runtime-published fingerprint before dispatch.
|
|
752
789
|
// Caller identity stays request-scoped while the inner MCP lane is reused.
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
790
|
+
// Only keyed Work-attributed Process Runtime commands may replay after a
|
|
791
|
+
// transient inner disconnect: the stable request_id resolves to the
|
|
792
|
+
// original Process instead of spawning the side effect twice.
|
|
793
|
+
const response = await callCanonicalRuntimeToolWithReplay({
|
|
794
|
+
name,
|
|
795
|
+
args,
|
|
796
|
+
call: async () => await activeClient.callTool(
|
|
797
|
+
forwardedRequest,
|
|
798
|
+
undefined,
|
|
799
|
+
{ timeout: CANONICAL_RUNTIME_TOOL_CALL_TIMEOUT_MS },
|
|
800
|
+
),
|
|
801
|
+
reconnect: async () => {
|
|
802
|
+
await closeLane(laneId, activeClient);
|
|
803
|
+
activeClient = await clientForCurrentRuntime(laneId, timing);
|
|
804
|
+
},
|
|
805
|
+
});
|
|
762
806
|
const result = response as unknown as CallToolResult;
|
|
763
807
|
timing.gatewayProxyCallMs = roundedMs(performance.now() - callStarted);
|
|
764
808
|
Object.assign(timing, deriveCanonicalForwardingTiming({
|
|
@@ -771,7 +815,7 @@ export function createCanonicalRuntimeProxy(ctx: MultiRepositoryMcpToolContext):
|
|
|
771
815
|
timing.gatewayProxyCallMs = roundedMs(performance.now() - callStarted);
|
|
772
816
|
// Poison only the failed lane. Other hot lanes remain available so one
|
|
773
817
|
// Runtime/session failure cannot collapse the whole Gateway proxy pool.
|
|
774
|
-
await closeLane(laneId,
|
|
818
|
+
await closeLane(laneId, activeClient);
|
|
775
819
|
throw error;
|
|
776
820
|
} finally {
|
|
777
821
|
scheduler.release(laneId);
|
|
@@ -892,7 +936,7 @@ export function createForgeMcpServerFromContext(
|
|
|
892
936
|
if (runtimeResult) return runtimeResult;
|
|
893
937
|
const durableResult = await routeDurableMcpCall(ctx, name, args);
|
|
894
938
|
if (durableResult) return durableResult;
|
|
895
|
-
const repositoryResult = await
|
|
939
|
+
const repositoryResult = await callRepositoryToolWithPostFinalizeAttribution(ctx.controllerHome, name, args, ctx);
|
|
896
940
|
if (repositoryResult) return repositoryResult;
|
|
897
941
|
return callMultiRepositoryTool(ctx, name, args);
|
|
898
942
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CallToolResult as SdkCallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
|
|
3
|
+
/** Transport-neutral MCP tool schema used by legacy, repository and Runtime surfaces. */
|
|
4
|
+
export interface McpToolDefinition {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: Record<string, unknown>;
|
|
8
|
+
annotations?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Bounded text-first result contract shared by Forge MCP adapters. */
|
|
12
|
+
export type CallToolResult = Omit<SdkCallToolResult, 'content'> & {
|
|
13
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
14
|
+
};
|
package/src/cli/mcp/toolset.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { McpToolDefinition } from './
|
|
1
|
+
import type { McpToolDefinition } from './tool-contract';
|
|
2
2
|
import type { MultiRepositoryMcpToolContext } from './multi-repository';
|
|
3
3
|
import { buildMultiRepositoryToolDefinitions } from './multi-repository';
|
|
4
4
|
import { accessToolDefinitions } from './access-tools';
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ControllerAccessState,
|
|
8
8
|
} from './access-mode';
|
|
9
9
|
import { repositoryToolDefinitions } from './repository-tools';
|
|
10
|
-
import { runtimeToolDefinitions } from '../../runtime/gateway/mcp/runtime-
|
|
10
|
+
import { runtimeToolDefinitions } from '../../runtime/gateway/mcp/runtime-tool-definitions';
|
|
11
11
|
import { executionToolDefinitions } from '../../runtime/gateway/mcp/execution-tools';
|
|
12
12
|
import { processToolDefinitions } from '../../runtime/gateway/mcp/process-tools';
|
|
13
13
|
import { injectDurableCommandFields } from '../../runtime/gateway/mcp/router';
|
|
@@ -1125,6 +1125,29 @@ export async function startMcpHttp(opts: McpHttpOptions): Promise<void> {
|
|
|
1125
1125
|
});
|
|
1126
1126
|
});
|
|
1127
1127
|
|
|
1128
|
+
// Recovery probes this endpoint every few seconds. Keep it strictly in-memory
|
|
1129
|
+
// and transport-scoped: whole-control-plane readiness below may traverse every
|
|
1130
|
+
// repository projection and probe the local bridge, which must never become a
|
|
1131
|
+
// periodic event-loop load generator for the public MCP Connector itself.
|
|
1132
|
+
app.get('/transport-ready', (_req, res) => {
|
|
1133
|
+
const sessionSnapshot = sessionRegistry.snapshot();
|
|
1134
|
+
const sessionCapacityReady = sessionSnapshot.acceptingNewSessions
|
|
1135
|
+
&& runtimeStats.initializing < MAX_INITIALIZING_SESSIONS
|
|
1136
|
+
&& runtimeStats.activePosts < MAX_ACTIVE_POSTS;
|
|
1137
|
+
res.status(sessionCapacityReady ? 200 : 503).json({
|
|
1138
|
+
ready: sessionCapacityReady,
|
|
1139
|
+
profile: toolContext.policy.profile,
|
|
1140
|
+
gateway: sessionCapacityReady ? 'ready' : 'saturated',
|
|
1141
|
+
sessionCapacity: sessionSnapshot,
|
|
1142
|
+
runtimeCapacity: {
|
|
1143
|
+
initializing: runtimeStats.initializing,
|
|
1144
|
+
maximumInitializing: MAX_INITIALIZING_SESSIONS,
|
|
1145
|
+
activePosts: runtimeStats.activePosts,
|
|
1146
|
+
maximumActivePosts: MAX_ACTIVE_POSTS,
|
|
1147
|
+
},
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1128
1151
|
app.get('/ready', async (_req, res) => {
|
|
1129
1152
|
const runtimeGeneration = currentRuntimeGeneration();
|
|
1130
1153
|
const sessionSnapshot = sessionRegistry.snapshot();
|
|
@@ -42,7 +42,7 @@ const READ_ONLY_GIT_SUBCOMMANDS = new Set([
|
|
|
42
42
|
'count-objects', 'verify-pack', 'check-ignore',
|
|
43
43
|
]);
|
|
44
44
|
|
|
45
|
-
function shellSegments(command: string): string[] {
|
|
45
|
+
export function shellSegments(command: string): string[] {
|
|
46
46
|
const segments: string[] = [];
|
|
47
47
|
let current = '';
|
|
48
48
|
let quote: "'" | '"' | undefined;
|
|
@@ -87,7 +87,7 @@ function shellSegments(command: string): string[] {
|
|
|
87
87
|
return segments;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function shellWordsPreservingQuotes(segment: string): string[] {
|
|
90
|
+
export function shellWordsPreservingQuotes(segment: string): string[] {
|
|
91
91
|
const words: string[] = [];
|
|
92
92
|
let current = '';
|
|
93
93
|
let quote: "'" | '"' | undefined;
|
|
@@ -72,6 +72,8 @@ export interface ExecuteRepositoryCommandInput {
|
|
|
72
72
|
allowNonGitWorkspace?: boolean;
|
|
73
73
|
/** Internal: command-facade already classified this as a bounded local script; retain policy/snapshot/audit without durable Process state. */
|
|
74
74
|
allowOpaqueLocalScript?: boolean;
|
|
75
|
+
/** Internal/test seam: bound dirty-path fingerprint enrichment independently from child execution lifetime. */
|
|
76
|
+
snapshotFingerprintTimeoutMs?: number;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
|
|
@@ -98,6 +100,8 @@ export interface RepositoryCommandExecution {
|
|
|
98
100
|
authorizationDecision?: AuthorizationDecision;
|
|
99
101
|
approvalRequestId?: string;
|
|
100
102
|
infrastructureError?: { code: string; message: string };
|
|
103
|
+
/** Child execution completed, but post-execution repository evidence could not be proven. Mutation state is unknown until a later authoritative inspection. */
|
|
104
|
+
evidenceError?: { code: string; message: string };
|
|
101
105
|
externalPathUsages?: RepositoryCommandExternalPathUsage[];
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -307,6 +311,7 @@ async function prepareRepositoryCommandExecutionAsync(
|
|
|
307
311
|
? emptyWorkspaceSnapshot()
|
|
308
312
|
: await repositorySnapshotAsync(root, input.signal, {
|
|
309
313
|
pathFingerprints: !(classification.risk === 'readonly' && controllerHome === undefined),
|
|
314
|
+
fingerprintTimeoutMs: input.snapshotFingerprintTimeoutMs,
|
|
310
315
|
}));
|
|
311
316
|
return finalizePreparedExecution(
|
|
312
317
|
repository,
|
|
@@ -565,9 +570,19 @@ export async function executeRepositoryCommandAsync(
|
|
|
565
570
|
...hooks,
|
|
566
571
|
signal,
|
|
567
572
|
});
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
573
|
+
let after: RepositoryCommandSnapshot | undefined = input.allowNonGitWorkspace ? before : undefined;
|
|
574
|
+
let evidenceError: RepositoryCommandExecution['evidenceError'];
|
|
575
|
+
if (!input.allowNonGitWorkspace) {
|
|
576
|
+
try {
|
|
577
|
+
after = await repositorySnapshotAsync(root, signal?.aborted ? undefined : signal, {
|
|
578
|
+
fingerprintTimeoutMs: input.snapshotFingerprintTimeoutMs,
|
|
579
|
+
});
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
582
|
+
const code = message.includes(':') ? message.slice(0, message.indexOf(':')) : 'POST_EXECUTION_EVIDENCE_FAILED';
|
|
583
|
+
evidenceError = { code, message };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
571
586
|
const execution: RepositoryCommandExecution = {
|
|
572
587
|
...base,
|
|
573
588
|
status: 'executed',
|
|
@@ -578,8 +593,9 @@ export async function executeRepositoryCommandAsync(
|
|
|
578
593
|
stdout: result.stdout,
|
|
579
594
|
stderr: result.stderr,
|
|
580
595
|
after,
|
|
581
|
-
repositoryChanged: snapshotChanged(before, after),
|
|
582
|
-
changedPaths: changedSnapshotPaths(before, after),
|
|
596
|
+
repositoryChanged: after ? snapshotChanged(before, after) : undefined,
|
|
597
|
+
changedPaths: after ? changedSnapshotPaths(before, after) : undefined,
|
|
598
|
+
evidenceError,
|
|
583
599
|
policyDecision: 'allowed',
|
|
584
600
|
externalPathUsages: externalPathUsages.length > 0 ? externalPathUsages : undefined,
|
|
585
601
|
infrastructureError: result.timedOut
|
|
@@ -594,7 +610,9 @@ export async function executeRepositoryCommandAsync(
|
|
|
594
610
|
}
|
|
595
611
|
: undefined,
|
|
596
612
|
};
|
|
597
|
-
|
|
613
|
+
// Unknown post-execution mutation evidence is conservatively cache-invalidating;
|
|
614
|
+
// later reads must re-observe the repository rather than reuse a false unchanged state.
|
|
615
|
+
if (execution.repositoryChanged === true || execution.evidenceError) invalidateRepositoryReadCaches(root);
|
|
598
616
|
auditCommand(controllerHome, repository, execution);
|
|
599
617
|
return execution;
|
|
600
618
|
}
|
|
@@ -84,7 +84,7 @@ const MAX_DIRTY_PATHS = 200;
|
|
|
84
84
|
export async function repositorySnapshotAsync(
|
|
85
85
|
root: string,
|
|
86
86
|
signal?: AbortSignal,
|
|
87
|
-
options: { pathFingerprints?: boolean } = {},
|
|
87
|
+
options: { pathFingerprints?: boolean; fingerprintTimeoutMs?: number } = {},
|
|
88
88
|
): Promise<RepositoryCommandSnapshot> {
|
|
89
89
|
const [headResult, branchResult, statusResult, refsResult] = await Promise.all([
|
|
90
90
|
gitAsync(root, ['rev-parse', '--verify', 'HEAD'], signal), gitAsync(root, ['branch', '--show-current'], signal),
|
|
@@ -107,7 +107,10 @@ export async function repositorySnapshotAsync(
|
|
|
107
107
|
let pathFingerprints: Record<string, string> = {};
|
|
108
108
|
if (includePathFingerprints) {
|
|
109
109
|
const { computePathFingerprintsAsync } = await import('../../runtime/execution/thin-harness/fingerprint-worker');
|
|
110
|
-
const
|
|
110
|
+
const fingerprintTimeoutMs = typeof options.fingerprintTimeoutMs === 'number' && Number.isFinite(options.fingerprintTimeoutMs)
|
|
111
|
+
? Math.max(1, Math.trunc(options.fingerprintTimeoutMs))
|
|
112
|
+
: 5_000;
|
|
113
|
+
const fingerprint = await computePathFingerprintsAsync({ root, paths, statusByPath: Object.fromEntries(byPath), maxFileBytes: 256 * 1024, maxTotalBytes: 8 * 1024 * 1024, maxPaths: MAX_DIRTY_PATHS }, { signal, timeoutMs: fingerprintTimeoutMs });
|
|
111
114
|
pathFingerprints = fingerprint.pathFingerprints;
|
|
112
115
|
}
|
|
113
116
|
return { head, branch: branchResult.ok ? branchResult.stdout || null : null, status: statusResult.stdout, dirty: paths.length > 0, refsHash: createHash('sha256').update(refs).digest('hex'), paths, pathFingerprints };
|