@moretea-labs/forge 1.5.0-rc.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -11
- package/CHANGELOG.md +19 -0
- package/CLAUDE.md +8 -10
- package/DESIGN.md +278 -0
- package/README.md +70 -90
- package/README.zh-CN.md +50 -55
- package/SKILL.md +8 -11
- package/THIRD_PARTY_NOTICES.md +9 -3
- package/assets/branding/README.md +4 -2
- package/assets/hooks/lib/workflow-state.sh +19 -18
- package/assets/hooks/post-edit-guard.sh +6 -75
- package/assets/hooks/prompt-guard.sh +24 -12
- package/assets/hooks/session-start-context.sh +17 -17
- package/assets/hooks/stop-orchestrator.sh +2 -2
- package/assets/partials/04-project-structure.partial.md +6 -6
- package/assets/partials/05-workflow.partial.md +6 -6
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials/08-orchestration.partial.md +2 -2
- package/assets/partials-agents/02-operating-mode.partial.md +5 -5
- package/assets/partials-agents/03-orchestration.partial.md +3 -3
- package/assets/partials-agents/04-task-protocol.partial.md +3 -3
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +49 -86
- package/assets/reference-configs/external-tooling.md +21 -61
- package/assets/reference-configs/handoff-protocol.md +25 -20
- package/assets/reference-configs/harness-overview.md +8 -8
- package/assets/templates/helpers/archive-workflow.sh +0 -1
- package/assets/templates/helpers/capture-plan.sh +3 -3
- package/assets/templates/helpers/check-task-workflow.sh +2 -2
- package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
- package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +2 -2
- package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
- package/assets/templates/helpers/prepare-handoff.sh +9 -9
- package/assets/templates/helpers/refresh-current-status.sh +2 -2
- package/assets/templates/helpers/ship-worktrees.sh +1 -1
- package/assets/templates/helpers/sprint-backlog.sh +1 -1
- package/assets/templates/helpers/verify-sprint.sh +7 -7
- package/assets/templates/helpers/workstream-sync.sh +1 -1
- package/assets/templates/plan.template.md +1 -1
- package/assets/workflow-contract.v1.json +6 -3
- package/docs/architecture/current/README.md +10 -9
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
- package/docs/architecture/current/architecture-invariants.md +2 -2
- package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
- package/docs/architecture/current/codegraph-context-provider.md +2 -2
- package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
- package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
- package/docs/architecture/current/entity-model.md +0 -27
- package/docs/architecture/current/failure-recovery.md +0 -24
- package/docs/architecture/current/governance.md +1 -1
- package/docs/architecture/current/implementation-status.md +1 -1
- package/docs/architecture/current/legacy-governance.md +58 -0
- package/docs/architecture/current/multi-repository-execution.md +1 -1
- package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
- package/docs/architecture/current/target-requirements-traceability.md +2 -2
- package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
- package/docs/architecture/current/thin-harness-v1.md +9 -11
- package/docs/architecture/domains/runtime-harness.md +1 -1
- package/docs/forge-autonomous-goal-loop.md +3 -1
- package/docs/forge-chatgpt-mcp-setup.md +51 -184
- package/docs/images/forge-banner-cn.svg +26 -1
- package/docs/images/forge-banner.svg +26 -1
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +3 -3
- package/docs/operations/mcp-tool-exposure.md +5 -5
- package/docs/operations/platform-support.md +45 -37
- package/docs/operations/platform-support.zh-CN.md +40 -38
- package/docs/operations/releasing.md +3 -3
- package/docs/operations/releasing.zh-CN.md +3 -3
- package/docs/operations/troubleshooting.md +11 -6
- package/docs/operations/troubleshooting.zh-CN.md +11 -6
- package/docs/public-usage-guide.md +28 -21
- package/docs/public-usage-guide.zh-CN.md +28 -21
- package/docs/tutorials/01-install-and-start.md +56 -35
- package/docs/tutorials/01-install-and-start.zh-CN.md +56 -35
- package/docs/tutorials/02-connect-chatgpt.md +112 -46
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +113 -45
- package/docs/wiki/Architecture.md +1 -1
- package/docs/wiki/Installation.md +2 -2
- package/docs/wiki/Quick-Start.md +1 -1
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/install.ps1 +4 -5
- package/install.sh +4 -6
- package/package.json +15 -5
- package/scripts/archive-workflow.sh +0 -1
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
- package/scripts/build-controller-ui.ts +28 -0
- package/scripts/capture-plan.sh +4 -4
- package/scripts/check-agent-tooling.sh +12 -1
- package/scripts/check-platform-support.mjs +2 -2
- package/scripts/check-public-docs.sh +16 -5
- package/scripts/check-release-readiness.sh +5 -11
- package/scripts/check-runtime-architecture.mjs +14 -22
- package/scripts/check-tarball-install-smoke.sh +3 -2
- package/scripts/check-task-workflow.sh +2 -2
- package/scripts/codex-handoff-resume.sh +5 -5
- package/scripts/create-project-dirs.sh +3 -3
- package/scripts/ensure-task-workflow.sh +45 -35
- package/scripts/init-project.sh +4 -4
- package/scripts/install-desktop-operator-registration.ts +2 -2
- package/scripts/lib/project-init-lib.sh +46 -44
- package/scripts/migrate-project-template.sh +7 -4
- package/scripts/new-plan.sh +1 -1
- package/scripts/plan-to-todo.sh +2 -2
- package/scripts/prepare-codex-handoff.sh +2 -2
- package/scripts/prepare-handoff.sh +9 -9
- package/scripts/refresh-current-status.sh +2 -2
- package/scripts/setup-plugins.sh +3 -3
- package/scripts/ship-worktrees.sh +1 -1
- package/scripts/sprint-backlog.sh +5 -5
- package/scripts/sync-codex-installed-copies.sh +2 -3
- package/scripts/verify-sprint.sh +7 -7
- package/scripts/workstream-sync.sh +1 -1
- package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
- package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
- package/src/cli/chatgpt-browser/file-policy.ts +3 -1
- package/src/cli/commands/chatgpt.ts +51 -0
- package/src/cli/commands/doctor.ts +1 -1
- package/src/cli/commands/init-hook.ts +211 -70
- package/src/cli/commands/init.ts +5 -6
- package/src/cli/commands/mcp.ts +29 -5
- package/src/cli/commands/runtime.ts +60 -8
- package/src/cli/commands/setup-profile.ts +441 -0
- package/src/cli/controller/check-runner.ts +20 -3
- package/src/cli/controller/codex-continuation.ts +4 -7
- package/src/cli/controller/context-pack.ts +118 -14
- package/src/cli/controller/operational-plan.ts +4 -4
- package/src/cli/controller/task-ledger.ts +10 -10
- package/src/cli/controller/work-mode.ts +6 -13
- package/src/cli/index.ts +14 -35
- package/src/cli/local-bridge/connector-freshness.ts +1 -1
- package/src/cli/local-bridge/console-automations.ts +160 -0
- package/src/cli/local-bridge/dashboard.ts +8 -1303
- package/src/cli/local-bridge/facade-api.ts +16 -7
- package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
- package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
- package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
- package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
- package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/styles.css +12 -0
- package/src/cli/local-bridge/frontend/src/types.ts +14 -0
- package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
- package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
- package/src/cli/local-bridge/server.ts +44 -0
- package/src/cli/local-bridge/ui-dist/app.css +1 -0
- package/src/cli/local-bridge/ui-dist/app.js +11094 -0
- package/src/cli/mcp/instructions.ts +3 -3
- package/src/cli/mcp/legacy-tool-service.ts +27 -23
- package/src/cli/mcp/multi-repository.ts +2 -0
- package/src/cli/mcp/policy.ts +7 -5
- package/src/cli/mcp/repository-tools.ts +53 -14
- package/src/cli/mcp/server.ts +5 -10
- package/src/cli/mcp/setup.ts +211 -275
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/toolset-names.ts +1 -1
- package/src/cli/mcp/toolset.ts +0 -1
- package/src/cli/mcp/transports/http.ts +24 -4
- package/src/cli/repositories/branch-name-policy.ts +0 -7
- package/src/cli/repositories/controller-home.ts +110 -3
- package/src/cli/repositories/safe-patch.ts +1 -1
- package/src/cli/repositories/selected-path-actions.ts +26 -24
- package/src/core/adoption/gitignore-plan.ts +2 -3
- package/src/core/adoption/plan.ts +4 -1
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
- package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
- package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
- package/src/runtime/control-plane/facade/types.ts +0 -1
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
- package/src/runtime/control-plane/routing/route-policy.ts +14 -14
- package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
- package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
- package/src/runtime/execution/process-runtime/check-result.ts +45 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
- package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
- package/src/runtime/execution/process-runtime/gc.ts +1 -1
- package/src/runtime/execution/process-runtime/index.ts +3 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
- package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
- package/src/runtime/execution/process-runtime/runtime.ts +18 -1
- package/src/runtime/execution/process-runtime/store.ts +1 -19
- package/src/runtime/execution/process-runtime/types.ts +4 -0
- package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
- package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
- package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
- package/src/runtime/execution/thin-harness/types.ts +0 -1
- package/src/runtime/execution/workers/executor.ts +1 -1
- package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
- package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
- package/src/runtime/gateway/mcp/process-tools.ts +16 -3
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
- package/src/runtime/gateway/mcp/router.ts +117 -182
- package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
- package/src/runtime/plugins/browser-adapter.ts +5 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
- package/src/runtime/plugins/store.ts +14 -6
- package/src/runtime/plugins/types.ts +1 -1
- package/src/runtime/projections/materialized-view.ts +1 -23
- package/src/runtime/recovery/maintenance-executor.ts +9 -0
- package/src/runtime/resources/ownership.ts +0 -1
- package/src/runtime/root/gateway-adapter.ts +2 -0
- package/src/runtime/root/mcp-transport.ts +10 -2
- package/src/runtime/root/package-runtime-release.ts +161 -0
- package/src/runtime/root/package-runtime-service.ts +172 -0
- package/src/runtime/root/runtime.ts +2 -1
- package/src/runtime/root/scheduler.ts +5 -0
- package/src/runtime/root/service.ts +18 -5
- package/src/runtime/standalone-recovery/core.ts +65 -16
- package/src/runtime/workflow/findings/store.ts +145 -106
- package/src/runtime/workflow/findings/types.ts +22 -0
- package/src/runtime/workflow/schedules/engine.ts +132 -2
- package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
- package/docs/CHANGELOG.md +0 -978
- package/docs/README.zh-CN.md +0 -58
- package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
- package/src/cli/commands/controller.ts +0 -680
- package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
- package/src/runtime/workflow/campaigns/engine.ts +0 -112
- package/src/runtime/workflow/campaigns/normalize.ts +0 -100
- package/src/runtime/workflow/campaigns/review.ts +0 -315
- package/src/runtime/workflow/campaigns/store.ts +0 -526
- package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
- package/src/runtime/workflow/campaigns/types.ts +0 -290
- /package/src/runtime/{workflow/campaigns → execution}/workspace-agent.ts +0 -0
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'child_process';
|
|
2
2
|
import {
|
|
3
|
-
claimControllerSession,
|
|
4
3
|
getHandoffItem,
|
|
5
4
|
getWorkContract,
|
|
6
|
-
releaseControllerSession,
|
|
7
5
|
type HandoffInboxStoreOptions,
|
|
8
6
|
type WorkContractStoreOptions,
|
|
9
7
|
} from '../facade';
|
|
8
|
+
import {
|
|
9
|
+
attachExternalControllerLaunchPid,
|
|
10
|
+
releaseExternalControllerLaunchReservation,
|
|
11
|
+
reserveExternalControllerLaunch,
|
|
12
|
+
} from './launch-reservation-store';
|
|
10
13
|
import type { ControllerType } from '../facade/types';
|
|
14
|
+
import { codexMcpConfigArgs, resolveProviderMcpBootstrap, type ProviderMcpBootstrap } from './provider-mcp-bootstrap';
|
|
15
|
+
import { getChatgptWorkConversationBinding } from './chatgpt-work-binding-store';
|
|
11
16
|
|
|
12
17
|
export interface ThinLauncherRequest {
|
|
13
18
|
controllerType: Exclude<ControllerType, 'human'>;
|
|
14
19
|
executable?: string;
|
|
15
20
|
args?: string[];
|
|
16
21
|
workId: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
leaseMs?: number;
|
|
22
|
+
/** Short reservation only prevents duplicate spawns; the external MCP session claims Work with its authenticated identity. */
|
|
23
|
+
launchReservationMs?: number;
|
|
20
24
|
handoffId?: string;
|
|
25
|
+
/** Saved Forge ChatGPT browser session to continue. */
|
|
26
|
+
browserSessionId?: string;
|
|
27
|
+
/** Explicit ChatGPT conversation URL used when no saved Forge browser session exists. */
|
|
28
|
+
conversationUrl?: string;
|
|
29
|
+
/** Additional bounded continuation instruction, for example a Schedule occurrence. */
|
|
30
|
+
continuationPrompt?: string;
|
|
21
31
|
cwd: string;
|
|
32
|
+
/** Internal launch context; populated by launchSuperController, not by callers. */
|
|
33
|
+
controllerHome?: string;
|
|
34
|
+
repoId?: string;
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
export interface ThinLauncherResult {
|
|
25
38
|
controllerType: ThinLauncherRequest['controllerType'];
|
|
26
|
-
|
|
27
|
-
sessionId: string;
|
|
39
|
+
reservationId: string;
|
|
28
40
|
pid: number | undefined;
|
|
29
41
|
prompt: string;
|
|
30
42
|
executable: string;
|
|
@@ -37,7 +49,9 @@ function resolveLauncherExecutable(request: ThinLauncherRequest): string {
|
|
|
37
49
|
? 'codex'
|
|
38
50
|
: request.controllerType === 'claude'
|
|
39
51
|
? 'claude'
|
|
40
|
-
: ''
|
|
52
|
+
: request.controllerType === 'chatgpt'
|
|
53
|
+
? process.env.FORGE_CLI_EXECUTABLE?.trim() || 'forge'
|
|
54
|
+
: '';
|
|
41
55
|
if (!executable) throw new Error(`LAUNCHER_EXECUTABLE_REQUIRED: ${request.controllerType} requires an external launcher executable`);
|
|
42
56
|
const probe = spawnSync(executable, ['--version'], {
|
|
43
57
|
cwd: request.cwd,
|
|
@@ -50,6 +64,71 @@ function resolveLauncherExecutable(request: ThinLauncherRequest): string {
|
|
|
50
64
|
return executable;
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
function assertChatgptConversationUrl(value: string | undefined): string | undefined {
|
|
68
|
+
const raw = value?.trim();
|
|
69
|
+
if (!raw) return undefined;
|
|
70
|
+
let url: URL;
|
|
71
|
+
try {
|
|
72
|
+
url = new URL(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error('LAUNCHER_CHATGPT_CONVERSATION_URL_INVALID');
|
|
75
|
+
}
|
|
76
|
+
if (url.protocol !== 'https:' || !['chatgpt.com', 'www.chatgpt.com'].includes(url.hostname)) {
|
|
77
|
+
throw new Error('LAUNCHER_CHATGPT_CONVERSATION_URL_INVALID');
|
|
78
|
+
}
|
|
79
|
+
return url.toString();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildSuperControllerInvocation(
|
|
83
|
+
request: ThinLauncherRequest,
|
|
84
|
+
executable: string,
|
|
85
|
+
prompt: string,
|
|
86
|
+
mcpBootstrap?: ProviderMcpBootstrap,
|
|
87
|
+
): { executable: string; args: string[] } {
|
|
88
|
+
if (request.controllerType === 'codex') {
|
|
89
|
+
if (!mcpBootstrap) throw new Error('LAUNCHER_CODEX_FORGE_MCP_REQUIRED');
|
|
90
|
+
return {
|
|
91
|
+
executable,
|
|
92
|
+
args: [
|
|
93
|
+
'--ask-for-approval', 'never',
|
|
94
|
+
...codexMcpConfigArgs(mcpBootstrap),
|
|
95
|
+
'exec', '--sandbox', 'workspace-write',
|
|
96
|
+
...(request.args ?? []),
|
|
97
|
+
prompt,
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (request.controllerType === 'claude') {
|
|
102
|
+
if (!(request.args ?? []).includes('--mcp-config')) {
|
|
103
|
+
throw new Error('LAUNCHER_CLAUDE_FORGE_MCP_CONFIG_REQUIRED');
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
executable,
|
|
107
|
+
args: ['--print', '--permission-mode', 'auto', ...(request.args ?? []), prompt],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (request.controllerType !== 'chatgpt') {
|
|
111
|
+
return { executable, args: [...(request.args ?? []), prompt] };
|
|
112
|
+
}
|
|
113
|
+
const browserSessionId = request.browserSessionId?.trim();
|
|
114
|
+
const conversationUrl = assertChatgptConversationUrl(request.conversationUrl);
|
|
115
|
+
if (!request.controllerHome || !request.repoId) throw new Error('LAUNCHER_CHATGPT_WORK_BINDING_CONTEXT_REQUIRED');
|
|
116
|
+
return {
|
|
117
|
+
executable,
|
|
118
|
+
args: [
|
|
119
|
+
'chatgpt', 'work-continue',
|
|
120
|
+
'--repo', request.cwd,
|
|
121
|
+
'--controller-home', request.controllerHome,
|
|
122
|
+
'--repo-id', request.repoId,
|
|
123
|
+
'--work-id', request.workId,
|
|
124
|
+
'--prompt', prompt,
|
|
125
|
+
...(browserSessionId ? ['--session', browserSessionId] : []),
|
|
126
|
+
...(conversationUrl ? ['--conversation-url', conversationUrl] : []),
|
|
127
|
+
...(request.args ?? []),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
53
132
|
/**
|
|
54
133
|
* Starts one external SuperController session. This module deliberately owns
|
|
55
134
|
* provider process selection and prompt construction; the execution Kernel
|
|
@@ -62,36 +141,50 @@ export function launchSuperController(
|
|
|
62
141
|
},
|
|
63
142
|
request: ThinLauncherRequest,
|
|
64
143
|
): ThinLauncherResult {
|
|
65
|
-
if (!request.controllerId.trim() || !request.sessionId.trim()) throw new Error('LAUNCHER_CONTROLLER_SESSION_REQUIRED');
|
|
66
144
|
const executable = resolveLauncherExecutable(request);
|
|
67
145
|
const work = getWorkContract(stores.work, request.workId);
|
|
68
146
|
if (!work) throw new Error(`WORK_NOT_FOUND: ${request.workId}`);
|
|
69
|
-
const
|
|
147
|
+
const reservation = reserveExternalControllerLaunch(stores.work, {
|
|
70
148
|
workId: work.workId,
|
|
71
|
-
controllerId: request.controllerId,
|
|
72
149
|
controllerType: request.controllerType,
|
|
73
|
-
|
|
74
|
-
leaseMs: request.leaseMs,
|
|
150
|
+
ttlMs: request.launchReservationMs,
|
|
75
151
|
});
|
|
76
152
|
const handoff = request.handoffId ? getHandoffItem(stores.handoff, request.handoffId) : undefined;
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
153
|
+
const chatgptBinding = request.controllerType === 'chatgpt'
|
|
154
|
+
? getChatgptWorkConversationBinding(stores.work, work.workId)
|
|
155
|
+
: undefined;
|
|
156
|
+
const prompt = (request.controllerType === 'chatgpt' && chatgptBinding)
|
|
157
|
+
? [
|
|
158
|
+
`Continue Forge Work ${work.workId} in repo ${work.repoId}.`,
|
|
159
|
+
handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
|
|
160
|
+
request.continuationPrompt?.trim() ? `Continuation: ${request.continuationPrompt.trim()}` : '',
|
|
161
|
+
`First call rh_work continue with repo_id=${work.repoId} and work_id=${work.workId}. Treat Forge Work/Plan/evidence as source of truth; do not invent new scope from chat history. If the claim fails, do not mutate. Continue the next safe action, finalize only when acceptance passes, and create a HandoffItem when judgement is required.`,
|
|
162
|
+
].filter(Boolean).join('\n')
|
|
163
|
+
: [
|
|
164
|
+
`Work: ${work.workId}`,
|
|
165
|
+
`Objective: ${work.objective}`,
|
|
166
|
+
`Acceptance: ${work.acceptanceCriteria.join('; ') || 'none declared'}`,
|
|
167
|
+
`Current status: ${work.status}`,
|
|
168
|
+
handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
|
|
169
|
+
request.continuationPrompt?.trim() ? `Continuation: ${request.continuationPrompt.trim()}` : '',
|
|
170
|
+
`No Controller ownership was preclaimed for you. First call rh_work continue with repo_id=${work.repoId} and work_id=${work.workId} through your authenticated MCP session; do not invent controller_id/session_id. If that claim does not succeed, do not mutate the repository: create no patch, command, commit, or test run until ownership is established. Then use the repository MCP facade, record verification evidence, finalize only when acceptance passes, and create a HandoffItem when judgement is required.`,
|
|
171
|
+
].filter(Boolean).join('\n');
|
|
85
172
|
try {
|
|
86
|
-
const
|
|
173
|
+
const mcpBootstrap = request.controllerType === 'codex'
|
|
174
|
+
? resolveProviderMcpBootstrap(stores.work.controllerHome, 'codex', reservation.reservationId)
|
|
175
|
+
: undefined;
|
|
176
|
+
const invocation = buildSuperControllerInvocation({ ...request, controllerHome: stores.work.controllerHome, repoId: work.repoId }, executable, prompt, mcpBootstrap);
|
|
177
|
+
const child = spawn(invocation.executable, invocation.args, {
|
|
87
178
|
cwd: request.cwd,
|
|
88
179
|
detached: true,
|
|
89
180
|
stdio: 'ignore',
|
|
181
|
+
env: mcpBootstrap?.env ?? process.env,
|
|
90
182
|
});
|
|
91
183
|
child.unref();
|
|
92
|
-
|
|
184
|
+
attachExternalControllerLaunchPid(stores.work, work.workId, reservation.reservationId, child.pid);
|
|
185
|
+
return { controllerType: request.controllerType, reservationId: reservation.reservationId, pid: child.pid, prompt, executable };
|
|
93
186
|
} catch (error) {
|
|
94
|
-
|
|
187
|
+
releaseExternalControllerLaunchReservation(stores.work, work.workId, reservation.reservationId, 'spawn_failed');
|
|
95
188
|
throw error;
|
|
96
189
|
}
|
|
97
190
|
}
|
|
@@ -3,11 +3,11 @@ import { createHash } from 'crypto';
|
|
|
3
3
|
export const ROUTE_POLICY_VERSION = 'route-policy-v3' as const;
|
|
4
4
|
|
|
5
5
|
export type RouteExecutionMode = 'direct_control' | 'goal_workloop' | 'handoff_only';
|
|
6
|
-
export type RouteWorkMode = 'direct_edit' | 'bounded_work' | 'quick_agent' | 'issue_task'
|
|
7
|
-
export type RouteExecutionPath = 'fast' | 'durable'
|
|
6
|
+
export type RouteWorkMode = 'direct_edit' | 'bounded_work' | 'quick_agent' | 'issue_task';
|
|
7
|
+
export type RouteExecutionPath = 'fast' | 'durable';
|
|
8
8
|
export type RouteExecutorKind = 'direct_edit' | 'local_cli' | 'remote_api' | 'cloud_agent' | 'external_controller' | 'handoff_only';
|
|
9
9
|
export type RouteApprovalState = 'approval_not_required' | 'normal_authorization_required' | 'strong_confirmation_required' | 'blocked_by_policy';
|
|
10
|
-
export type ExplicitTaskMode = 'direct' | 'plan' | 'debug' | 'review' | '
|
|
10
|
+
export type ExplicitTaskMode = 'direct' | 'plan' | 'debug' | 'review' | 'release' | 'scale';
|
|
11
11
|
|
|
12
12
|
export interface RouteReason {
|
|
13
13
|
code: string;
|
|
@@ -233,7 +233,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
|
|
|
233
233
|
const approvalConfirmed = input.policy.approvalConfirmed === true;
|
|
234
234
|
const protectedPath = paths.some((path) => PROTECTED_PATH.test(path));
|
|
235
235
|
const explicitMode = input.intent.explicitMode;
|
|
236
|
-
const explicitParallelMode = explicitMode === '
|
|
236
|
+
const explicitParallelMode = explicitMode === 'scale';
|
|
237
237
|
const requiresIsolation = explicitMode === 'direct'
|
|
238
238
|
? false
|
|
239
239
|
: input.recovery.isolationRequired === true || explicitParallelMode;
|
|
@@ -242,7 +242,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
|
|
|
242
242
|
|| explicitMode === 'debug'
|
|
243
243
|
|| explicitMode === 'release'
|
|
244
244
|
|| explicitMode === 'scale';
|
|
245
|
-
const
|
|
245
|
+
const coordinationRequired = explicitParallelMode || input.intent.requiresIndependentDeliverables === true
|
|
246
246
|
|| independentTaskCount >= 3;
|
|
247
247
|
|
|
248
248
|
if (input.policy.policyBlocked === true) {
|
|
@@ -304,7 +304,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
|
|
|
304
304
|
if (input.intent.requiresLongRunningChecks) reasons.push({ code: 'long_checks', message: 'Long-running checks require durable continuation.' });
|
|
305
305
|
if (input.intent.requiresInvestigation) reasons.push({ code: 'investigation', message: 'Investigation is required before or during implementation.' });
|
|
306
306
|
if (input.intent.needsDependencies) reasons.push({ code: 'dependencies', message: 'Dependency ordering requires durable Work.' });
|
|
307
|
-
if (
|
|
307
|
+
if (coordinationRequired) reasons.push({ code: 'independent_deliverables', message: 'Multiple independent deliverables require durable PlanContract/Work coordination.' });
|
|
308
308
|
|
|
309
309
|
const explicitBoundedMode = explicitMode === 'plan'
|
|
310
310
|
|| explicitMode === 'debug'
|
|
@@ -312,7 +312,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
|
|
|
312
312
|
|| explicitMode === 'release';
|
|
313
313
|
const complex = explicitMode === 'direct'
|
|
314
314
|
? false
|
|
315
|
-
:
|
|
315
|
+
: coordinationRequired
|
|
316
316
|
|| explicitBoundedMode
|
|
317
317
|
|| requiresRecovery
|
|
318
318
|
|| requiresIsolation
|
|
@@ -326,19 +326,19 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
|
|
|
326
326
|
|| secretAccess
|
|
327
327
|
|| (mutation && (expectedFiles > 4 || expectedChangedLines > 200));
|
|
328
328
|
const executionMode: RouteExecutionMode = complex ? 'goal_workloop' : 'direct_control';
|
|
329
|
-
// Work topology is independent from executor/provider choice.
|
|
330
|
-
//
|
|
331
|
-
//
|
|
329
|
+
// Work topology is independent from executor/provider choice. Independent or
|
|
330
|
+
// parallel deliverables stay in the durable Goal Workloop and are decomposed by
|
|
331
|
+
// PlanContract/Work rather than introducing another project-level lifecycle.
|
|
332
332
|
const workMode: RouteWorkMode = explicitMode === 'direct'
|
|
333
333
|
? 'direct_edit'
|
|
334
|
-
:
|
|
335
|
-
? '
|
|
334
|
+
: coordinationRequired
|
|
335
|
+
? 'bounded_work'
|
|
336
336
|
: input.intent.agentRequested
|
|
337
337
|
? expectedFiles > 10 || expectedChangedLines > 1_500 ? 'issue_task' : 'quick_agent'
|
|
338
338
|
: complex
|
|
339
339
|
? 'bounded_work'
|
|
340
340
|
: 'direct_edit';
|
|
341
|
-
const executionPath: RouteExecutionPath =
|
|
341
|
+
const executionPath: RouteExecutionPath = complex ? 'durable' : 'fast';
|
|
342
342
|
const providerSelection = selectProvider(input);
|
|
343
343
|
const providersWereSupplied = input.capabilities.providers !== undefined;
|
|
344
344
|
if (providersWereSupplied && !providerSelection.provider) {
|
|
@@ -364,7 +364,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
|
|
|
364
364
|
workMode,
|
|
365
365
|
executionPath,
|
|
366
366
|
// Direct Control is intentionally contract-free. Persistence belongs to
|
|
367
|
-
// Goal Workloop/
|
|
367
|
+
// Goal Workloop/Agent tiers; bounded direct edits rely on the
|
|
368
368
|
// existing permission, patch, Process, and evidence boundaries instead.
|
|
369
369
|
requiresWork: executionMode !== 'direct_control',
|
|
370
370
|
requiresApproval: approvalRequired,
|
|
@@ -18,7 +18,7 @@ export interface RuntimeEntityEvent {
|
|
|
18
18
|
eventId: string;
|
|
19
19
|
eventType: string;
|
|
20
20
|
repoId: string;
|
|
21
|
-
entityType: 'job' | 'plugin' | 'schedule' | 'occurrence' | 'portfolio' | '
|
|
21
|
+
entityType: 'job' | 'plugin' | 'schedule' | 'occurrence' | 'portfolio' | 'candidate-finding' | 'release' | 'lease' | 'schedule-decision' | 'assistant-action-proposal' | 'assistant-standing-grant';
|
|
22
22
|
entityId: string;
|
|
23
23
|
correlationId?: string;
|
|
24
24
|
causationId?: string;
|
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { existsSync, mkdirSync, realpathSync } from 'fs';
|
|
3
3
|
import { dirname, join, resolve } from 'path';
|
|
4
|
-
import { repositoryControllerRoot } from '
|
|
5
|
-
import type { RepositoryRecord } from '
|
|
4
|
+
import { repositoryControllerRoot } from '../../cli/repositories/controller-home';
|
|
5
|
+
import type { RepositoryRecord } from '../../cli/repositories/types';
|
|
6
6
|
import {
|
|
7
7
|
addRepositoryCheckout,
|
|
8
8
|
listRepositories,
|
|
9
9
|
repositoryCheckoutRootMatches,
|
|
10
10
|
selectRepositoryCheckout,
|
|
11
|
-
} from '
|
|
12
|
-
import { managedWorktreePath } from '
|
|
13
|
-
import {
|
|
14
|
-
import { withControllerLock } from '
|
|
15
|
-
import { resolveGitExecutable } from '
|
|
16
|
-
import { runProcess } from '
|
|
17
|
-
import { readJsonFile, writeJsonAtomic } from '
|
|
18
|
-
import type { CampaignWorkspace } from './types';
|
|
11
|
+
} from '../../cli/repositories/registry';
|
|
12
|
+
import { managedWorktreePath } from '../../cli/repositories/worktree-storage';
|
|
13
|
+
import { branchSlugSegment, validateBranchName } from '../../cli/repositories/branch-name-policy';
|
|
14
|
+
import { withControllerLock } from '../../cli/repositories/locks';
|
|
15
|
+
import { resolveGitExecutable } from '../../effects/git-executable';
|
|
16
|
+
import { runProcess } from '../../effects/process-runner';
|
|
17
|
+
import { readJsonFile, writeJsonAtomic } from '../shared/json-files';
|
|
19
18
|
|
|
20
|
-
export interface
|
|
19
|
+
export interface EnsureManagedWorkspaceInput {
|
|
21
20
|
requestId: string;
|
|
22
21
|
title: string;
|
|
23
22
|
baseRef?: string;
|
|
24
23
|
branchName?: string;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
export interface ManagedWorkspace {
|
|
27
|
+
mode: 'current' | 'isolated';
|
|
28
|
+
checkoutId?: string;
|
|
29
|
+
root?: string;
|
|
30
|
+
branch?: string | null;
|
|
31
|
+
baseRevision?: string | null;
|
|
32
|
+
managed: boolean;
|
|
33
|
+
}
|
|
32
34
|
|
|
33
|
-
interface
|
|
35
|
+
interface ManagedWorkspaceManifest {
|
|
34
36
|
schemaVersion: 1;
|
|
35
37
|
repoId: string;
|
|
36
38
|
requestId: string;
|
|
@@ -51,7 +53,7 @@ function git(root: string, args: string[], timeoutMs = 30_000): string {
|
|
|
51
53
|
});
|
|
52
54
|
if (!result.ok) {
|
|
53
55
|
const detail = result.stderr || result.error || `exit ${result.status}`;
|
|
54
|
-
throw new Error(`
|
|
56
|
+
throw new Error(`MANAGED_WORKSPACE_GIT_FAILED: git ${args.join(' ')}: ${detail}`);
|
|
55
57
|
}
|
|
56
58
|
return result.stdout.trim();
|
|
57
59
|
}
|
|
@@ -64,9 +66,9 @@ function gitSucceeds(root: string, args: string[]): boolean {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
function assertBranch(root: string, branch: string): string {
|
|
67
|
-
const normalized = validateBranchName(branch, { purpose: '
|
|
69
|
+
const normalized = validateBranchName(branch, { purpose: 'MANAGED_WORKSPACE_BRANCH' });
|
|
68
70
|
if (!gitSucceeds(root, ['check-ref-format', '--branch', normalized])) {
|
|
69
|
-
throw new Error(`
|
|
71
|
+
throw new Error(`MANAGED_WORKSPACE_BRANCH_INVALID: ${normalized}`);
|
|
70
72
|
}
|
|
71
73
|
return normalized;
|
|
72
74
|
}
|
|
@@ -77,22 +79,22 @@ function existingWorkspace(path: string, branch: string): boolean {
|
|
|
77
79
|
timeoutMs: 15_000,
|
|
78
80
|
maxOutputBytes: 8 * 1024,
|
|
79
81
|
});
|
|
80
|
-
if (!root.ok) throw new Error(`
|
|
82
|
+
if (!root.ok) throw new Error(`MANAGED_WORKSPACE_PATH_OCCUPIED: ${path}`);
|
|
81
83
|
if (realpathSync(root.stdout.trim()) !== realpathSync(path)) {
|
|
82
|
-
throw new Error(`
|
|
84
|
+
throw new Error(`MANAGED_WORKSPACE_PATH_MISMATCH: ${path}`);
|
|
83
85
|
}
|
|
84
86
|
const currentBranch = git(path, ['branch', '--show-current']);
|
|
85
87
|
if (currentBranch !== branch) {
|
|
86
|
-
throw new Error(`
|
|
88
|
+
throw new Error(`MANAGED_WORKSPACE_BRANCH_MISMATCH: expected ${branch}, found ${currentBranch || 'detached'}`);
|
|
87
89
|
}
|
|
88
90
|
return true;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
function manifestPath(controllerHome: string, repoId: string, identity: string): string {
|
|
92
|
-
return join(repositoryControllerRoot(controllerHome, repoId), '
|
|
94
|
+
return join(repositoryControllerRoot(controllerHome, repoId), 'managed-workspaces', `${identity}.json`);
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
export function
|
|
97
|
+
export function currentManagedWorkspace(repository: RepositoryRecord): ManagedWorkspace {
|
|
96
98
|
return {
|
|
97
99
|
mode: 'current',
|
|
98
100
|
checkoutId: repository.activeCheckoutId,
|
|
@@ -106,41 +108,40 @@ export function currentCampaignWorkspace(repository: RepositoryRecord): Campaign
|
|
|
106
108
|
export function ensureManagedWorkspace(
|
|
107
109
|
controllerHome: string,
|
|
108
110
|
repository: RepositoryRecord,
|
|
109
|
-
input:
|
|
110
|
-
):
|
|
111
|
+
input: EnsureManagedWorkspaceInput,
|
|
112
|
+
): ManagedWorkspace {
|
|
111
113
|
const requestId = input.requestId.trim();
|
|
112
|
-
if (!requestId) throw new Error('
|
|
114
|
+
if (!requestId) throw new Error('MANAGED_WORKSPACE_REQUEST_ID_REQUIRED');
|
|
113
115
|
const identity = suffix(repository.repoId, requestId);
|
|
114
116
|
const sourceRoot = repository.canonicalRoot;
|
|
115
117
|
const requestedBranch = assertBranch(
|
|
116
118
|
sourceRoot,
|
|
117
|
-
input.branchName?.trim() ||
|
|
119
|
+
input.branchName?.trim() || `work/${branchSlugSegment(input.title)}-${identity}`,
|
|
118
120
|
);
|
|
119
121
|
const requestedBaseRef = input.baseRef?.trim() || 'HEAD';
|
|
120
122
|
const requestedPath = managedWorktreePath(
|
|
121
123
|
controllerHome,
|
|
122
124
|
repository.repoId,
|
|
123
|
-
`
|
|
125
|
+
`work-${identity}`,
|
|
124
126
|
listRepositories(controllerHome, { includeRemoved: true }),
|
|
125
127
|
);
|
|
126
128
|
const statePath = manifestPath(controllerHome, repository.repoId, identity);
|
|
127
129
|
|
|
128
130
|
return withControllerLock(
|
|
129
131
|
controllerHome,
|
|
130
|
-
{ scope: 'worktree', repoId: repository.repoId, worktreeId: `
|
|
131
|
-
`ensure-
|
|
132
|
+
{ scope: 'worktree', repoId: repository.repoId, worktreeId: `work-${identity}` },
|
|
133
|
+
`ensure-managed-workspace:${requestId}`,
|
|
132
134
|
() => {
|
|
133
|
-
const manifest = existsSync(statePath) ? readJsonFile<
|
|
135
|
+
const manifest = existsSync(statePath) ? readJsonFile<ManagedWorkspaceManifest>(statePath) : undefined;
|
|
134
136
|
if (manifest && (
|
|
135
137
|
manifest.repoId !== repository.repoId
|
|
136
138
|
|| manifest.requestId !== requestId
|
|
137
139
|
|| manifest.branch !== requestedBranch
|
|
138
140
|
)) {
|
|
139
|
-
throw new Error(`
|
|
141
|
+
throw new Error(`MANAGED_WORKSPACE_REQUEST_CONFLICT: ${requestId}`);
|
|
140
142
|
}
|
|
141
143
|
const branch = manifest?.branch ?? requestedBranch;
|
|
142
|
-
//
|
|
143
|
-
// point at legacy nested worktrees; never move an active worktree in place.
|
|
144
|
+
// Persisted manifests are authoritative evidence; never move an active worktree in place.
|
|
144
145
|
const path = manifest?.path ? resolve(manifest.path) : requestedPath;
|
|
145
146
|
const baseRevision = manifest?.baseRevision ?? git(sourceRoot, ['rev-parse', '--verify', `${requestedBaseRef}^{commit}`]);
|
|
146
147
|
|
|
@@ -156,7 +157,7 @@ export function ensureManagedWorkspace(
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
if (!repositoryCheckoutRootMatches(repository, path)) {
|
|
159
|
-
throw new Error(`
|
|
160
|
+
throw new Error(`MANAGED_WORKSPACE_REPOSITORY_MISMATCH: ${path}`);
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
const record = addRepositoryCheckout({
|
|
@@ -170,7 +171,7 @@ export function ensureManagedWorkspace(
|
|
|
170
171
|
existsSync(candidate.canonicalRoot)
|
|
171
172
|
&& realpathSync(candidate.canonicalRoot) === canonicalWorkspacePath,
|
|
172
173
|
);
|
|
173
|
-
if (!checkout) throw new Error(`
|
|
174
|
+
if (!checkout) throw new Error(`MANAGED_WORKSPACE_CHECKOUT_NOT_REGISTERED: ${path}`);
|
|
174
175
|
const selected = selectRepositoryCheckout(record, checkout.checkoutId);
|
|
175
176
|
if (!manifest) {
|
|
176
177
|
writeJsonAtomic(statePath, {
|
|
@@ -181,7 +182,7 @@ export function ensureManagedWorkspace(
|
|
|
181
182
|
path: selected.canonicalRoot,
|
|
182
183
|
baseRevision,
|
|
183
184
|
createdAt: new Date().toISOString(),
|
|
184
|
-
} satisfies
|
|
185
|
+
} satisfies ManagedWorkspaceManifest);
|
|
185
186
|
}
|
|
186
187
|
return {
|
|
187
188
|
mode: 'isolated',
|
|
@@ -190,11 +191,8 @@ export function ensureManagedWorkspace(
|
|
|
190
191
|
branch,
|
|
191
192
|
baseRevision,
|
|
192
193
|
managed: true,
|
|
193
|
-
} satisfies
|
|
194
|
+
} satisfies ManagedWorkspace;
|
|
194
195
|
},
|
|
195
196
|
120_000,
|
|
196
197
|
);
|
|
197
198
|
}
|
|
198
|
-
|
|
199
|
-
/** @deprecated Campaign compatibility alias. New Work paths use ensureManagedWorkspace. */
|
|
200
|
-
export const ensureCampaignWorkspace = ensureManagedWorkspace;
|
|
@@ -52,6 +52,12 @@ export interface RunCheckFacadeInput {
|
|
|
52
52
|
issueId?: string;
|
|
53
53
|
taskId?: string;
|
|
54
54
|
};
|
|
55
|
+
/** Work-scoped verification runs against HEAD plus only scope-owned dirty paths. */
|
|
56
|
+
verificationSnapshot?: {
|
|
57
|
+
workId: string;
|
|
58
|
+
allowedPaths: readonly string[];
|
|
59
|
+
forbiddenPaths: readonly string[];
|
|
60
|
+
};
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export function processCheckSemanticScopeKey(
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { processLogDir } from './store';
|
|
5
|
+
|
|
6
|
+
export interface PersistedCheckResultReceipt {
|
|
7
|
+
schemaVersion: 1;
|
|
8
|
+
receiptId: string;
|
|
9
|
+
checkId: string;
|
|
10
|
+
cacheKey: string;
|
|
11
|
+
ok: boolean;
|
|
12
|
+
status: number;
|
|
13
|
+
timedOut: boolean;
|
|
14
|
+
failureClass?: 'acceptance_failure' | 'infrastructure_failure';
|
|
15
|
+
validatedRevision?: string;
|
|
16
|
+
executedAt: string;
|
|
17
|
+
originalExecutedAt?: string;
|
|
18
|
+
cacheHit?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function allocatePersistedCheckResultReceiptPath(controllerHome: string, repoId: string): string {
|
|
22
|
+
const root = join(processLogDir(controllerHome, repoId), 'check-results');
|
|
23
|
+
mkdirSync(root, { recursive: true });
|
|
24
|
+
return join(root, `${randomUUID()}.json`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function writePersistedCheckResultReceipt(path: string, input: Omit<PersistedCheckResultReceipt, 'schemaVersion' | 'receiptId'>): PersistedCheckResultReceipt {
|
|
28
|
+
const digest = createHash('sha256').update(JSON.stringify(input)).digest('hex');
|
|
29
|
+
const receipt: PersistedCheckResultReceipt = { schemaVersion: 1, receiptId: `check_result_${digest.slice(0, 24)}`, ...input };
|
|
30
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
31
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
32
|
+
writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
|
|
33
|
+
renameSync(temporary, path);
|
|
34
|
+
return receipt;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readPersistedCheckResultReceipt(path: string | undefined): PersistedCheckResultReceipt | undefined {
|
|
38
|
+
if (!path || !existsSync(path)) return undefined;
|
|
39
|
+
try {
|
|
40
|
+
const value = JSON.parse(readFileSync(path, 'utf8')) as PersistedCheckResultReceipt;
|
|
41
|
+
return value?.schemaVersion === 1 && typeof value.receiptId === 'string' && typeof value.cacheKey === 'string' ? value : undefined;
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
+
import { rmSync } from 'fs';
|
|
3
4
|
import { resolve } from 'path';
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
+
controllerCheckExecutionIdentity,
|
|
7
|
+
runControllerCheckAsync,
|
|
6
8
|
snapshotControllerCheck,
|
|
7
9
|
} from '../../../cli/controller/check-runner';
|
|
10
|
+
import { writePersistedCheckResultReceipt } from './check-result';
|
|
8
11
|
|
|
9
12
|
interface ParsedArgs {
|
|
10
13
|
repo: string;
|
|
11
14
|
checkId: string;
|
|
12
15
|
timeoutMs?: number;
|
|
13
16
|
expectedCheckFingerprint: string;
|
|
17
|
+
resultReceiptPath?: string;
|
|
18
|
+
cleanupRoot?: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
function requiredValue(argv: string[], flag: string): string {
|
|
@@ -31,30 +36,51 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
31
36
|
checkId: requiredValue(argv, '--check-id'),
|
|
32
37
|
timeoutMs,
|
|
33
38
|
expectedCheckFingerprint: requiredValue(argv, '--expected-check-fingerprint'),
|
|
39
|
+
resultReceiptPath: argv.includes('--result-receipt') ? requiredValue(argv, '--result-receipt') : undefined,
|
|
40
|
+
cleanupRoot: argv.includes('--cleanup-root') ? requiredValue(argv, '--cleanup-root') : undefined,
|
|
34
41
|
};
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
export function runPersistedCheckSidecar(argv = process.argv.slice(2)): number {
|
|
44
|
+
export async function runPersistedCheckSidecar(argv = process.argv.slice(2)): Promise<number> {
|
|
38
45
|
const args = parseArgs(argv);
|
|
39
46
|
const root = resolve(args.repo);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
try {
|
|
48
|
+
const snapshot = snapshotControllerCheck(root, args.checkId);
|
|
49
|
+
const actualFingerprint = createHash('sha256').update(JSON.stringify(snapshot)).digest('hex');
|
|
50
|
+
if (actualFingerprint !== args.expectedCheckFingerprint) {
|
|
51
|
+
throw new Error('CHECK_SNAPSHOT_CHANGED: registered check changed before Process Runtime execution');
|
|
52
|
+
}
|
|
53
|
+
const identity = controllerCheckExecutionIdentity(root, args.checkId, args.timeoutMs, snapshot);
|
|
54
|
+
const result = await runControllerCheckAsync(root, args.checkId, { requestedTimeoutMs: args.timeoutMs, snapshot });
|
|
55
|
+
if (args.resultReceiptPath) {
|
|
56
|
+
writePersistedCheckResultReceipt(args.resultReceiptPath, {
|
|
57
|
+
checkId: args.checkId,
|
|
58
|
+
cacheKey: identity.cacheKey,
|
|
59
|
+
ok: result.ok,
|
|
60
|
+
status: result.status,
|
|
61
|
+
timedOut: result.timedOut,
|
|
62
|
+
failureClass: result.failureClass,
|
|
63
|
+
validatedRevision: result.validatedRevision,
|
|
64
|
+
executedAt: result.executedAt,
|
|
65
|
+
originalExecutedAt: result.originalExecutedAt,
|
|
66
|
+
cacheHit: result.cacheHit,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
70
|
+
if (result.stderr) process.stderr.write(result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`);
|
|
71
|
+
return result.ok ? 0 : Math.max(1, result.status || 1);
|
|
72
|
+
} finally {
|
|
73
|
+
if (args.cleanupRoot) rmSync(resolve(args.cleanupRoot), { recursive: true, force: true });
|
|
44
74
|
}
|
|
45
|
-
const result = runControllerCheck(root, args.checkId, args.timeoutMs, snapshot);
|
|
46
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
47
|
-
if (result.stderr) process.stderr.write(result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`);
|
|
48
|
-
return result.ok ? 0 : Math.max(1, result.status || 1);
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
const direct = typeof process.argv[1] === 'string'
|
|
52
78
|
&& (process.argv[1].includes('check-runner-sidecar') || process.argv[1].endsWith('/forge-check-runner'));
|
|
53
79
|
if (direct) {
|
|
54
|
-
|
|
55
|
-
process.exitCode =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
void runPersistedCheckSidecar()
|
|
81
|
+
.then((code) => { process.exitCode = code; })
|
|
82
|
+
.catch((error) => {
|
|
83
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
});
|
|
60
86
|
}
|