@moretea-labs/forge 1.5.1 → 1.6.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/CHANGELOG.md +20 -0
- package/DESIGN.md +7 -2
- package/README.md +4 -4
- package/README.zh-CN.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +2 -2
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
- package/docs/architecture/current/codegraph-context-provider.md +15 -3
- package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
- package/docs/architecture/current/entity-model.md +20 -0
- package/docs/architecture/current/external-plugin-governance.md +3 -3
- package/docs/architecture/current/system-overview.md +26 -1
- package/docs/architecture/current/thin-harness-v1.md +10 -0
- package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
- package/docs/operations/controller-browser-plugin.md +12 -16
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Quick-Start.md +1 -1
- package/package.json +1 -1
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/sprint-backlog.sh +1 -1
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/plugin.ts +36 -9
- package/src/cli/controller/context-pack.ts +158 -6
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/local-bridge/console-automations.ts +205 -23
- package/src/cli/local-bridge/console-view-models.ts +2 -0
- package/src/cli/local-bridge/facade-api.ts +2 -0
- package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +6 -2
- package/src/cli/local-bridge/server.ts +29 -19
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +124 -9
- package/src/cli/mcp/server.ts +164 -19
- package/src/cli/mcp/transports/http.ts +25 -2
- package/src/cli/mcp/transports/session-registry.ts +24 -0
- package/src/cli/repositories/command-classifier.ts +113 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/structured-git.ts +49 -7
- package/src/cli/repository/inspector.ts +16 -3
- package/src/runtime/assistant/intent.ts +3 -3
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
- package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
- package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/types.ts +33 -6
- package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/diagnostics/mcp-timing.ts +6 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
- package/src/runtime/execution/process-runtime/check-result.ts +10 -2
- package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
- package/src/runtime/execution/process-runtime/gc.ts +64 -10
- package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
- package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
- package/src/runtime/execution/process-runtime/runtime.ts +16 -1
- package/src/runtime/execution/process-runtime/store.ts +6 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
- package/src/runtime/gateway/mcp/router.ts +1 -1
- package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/browser-adapter.ts +19 -72
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +0 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/first-party-registry.ts +2 -2
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/local-system-adapter.ts +2 -2
- package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
- package/src/runtime/plugins/store.ts +107 -5
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +43 -11
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/release-materialize.ts +143 -19
- package/src/runtime/root/service.ts +6 -4
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/standalone-recovery/core.ts +2 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +357 -64
- package/src/runtime/workflow/schedules/types.ts +5 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- package/src/runtime/safe-tooling/browser-safe.ts +0 -133
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* - GC failures must not throw into the controller main loop.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, readdirSync, unlinkSync, statSync } from 'fs';
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, statSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
import { ensureRepositoryControllerLayout, repositoryControllerRoot } from '../../../cli/repositories/controller-home';
|
|
12
|
-
import {
|
|
12
|
+
import { listActiveProcessIds } from './store';
|
|
13
13
|
import type { ProcessRuntimeStatus } from './types';
|
|
14
14
|
import { assertRuntimeMayWrite } from '../../root/write-fence';
|
|
15
15
|
import { isProcessAlive } from '../../shared/process-tree';
|
|
@@ -39,6 +39,8 @@ export interface ProcessGcResult {
|
|
|
39
39
|
removedRecords: number;
|
|
40
40
|
removedLogs: number;
|
|
41
41
|
skippedActive: number;
|
|
42
|
+
/** Legacy/corrupt records without enough terminal proof are preserved. */
|
|
43
|
+
skippedInvalid?: number;
|
|
42
44
|
error?: string;
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -50,6 +52,54 @@ function logDir(controllerHome: string, repoId: string): string {
|
|
|
50
52
|
return join(processesDir(controllerHome, repoId), 'logs');
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
interface ProcessGcMetadata {
|
|
56
|
+
status?: ProcessRuntimeStatus;
|
|
57
|
+
finishedAt?: string;
|
|
58
|
+
updatedAt?: string;
|
|
59
|
+
identity?: { pid: number; processStartTime: string };
|
|
60
|
+
identityUntrusted?: boolean;
|
|
61
|
+
reusableCheckEvidence: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* GC deliberately reads only the metadata needed to prove terminality. Historical
|
|
66
|
+
* Process records can predate today's required command descriptor; feeding those
|
|
67
|
+
* records through the normal recovery reader would either throw during redaction
|
|
68
|
+
* or, worse, require manufacturing a fake executable. A malformed/unknown record
|
|
69
|
+
* is skipped rather than deleted, preserving fail-closed recovery semantics.
|
|
70
|
+
*/
|
|
71
|
+
function readProcessGcMetadata(path: string): ProcessGcMetadata | undefined {
|
|
72
|
+
try {
|
|
73
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
|
|
74
|
+
if (raw.schemaVersion !== 1) return undefined;
|
|
75
|
+
const status = typeof raw.status === 'string' && TERMINAL.has(raw.status as ProcessRuntimeStatus)
|
|
76
|
+
? raw.status as ProcessRuntimeStatus
|
|
77
|
+
: typeof raw.status === 'string'
|
|
78
|
+
? raw.status as ProcessRuntimeStatus
|
|
79
|
+
: undefined;
|
|
80
|
+
const identityRaw = raw.identity && typeof raw.identity === 'object' && !Array.isArray(raw.identity)
|
|
81
|
+
? raw.identity as Record<string, unknown>
|
|
82
|
+
: undefined;
|
|
83
|
+
const pid = typeof identityRaw?.pid === 'number' && Number.isInteger(identityRaw.pid) && identityRaw.pid > 0
|
|
84
|
+
? identityRaw.pid
|
|
85
|
+
: undefined;
|
|
86
|
+
const processStartTime = typeof identityRaw?.processStartTime === 'string' ? identityRaw.processStartTime : undefined;
|
|
87
|
+
const checkExecution = raw.checkExecution && typeof raw.checkExecution === 'object' && !Array.isArray(raw.checkExecution)
|
|
88
|
+
? raw.checkExecution as Record<string, unknown>
|
|
89
|
+
: undefined;
|
|
90
|
+
return {
|
|
91
|
+
status,
|
|
92
|
+
finishedAt: typeof raw.finishedAt === 'string' ? raw.finishedAt : undefined,
|
|
93
|
+
updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : undefined,
|
|
94
|
+
...(pid && processStartTime ? { identity: { pid, processStartTime } } : {}),
|
|
95
|
+
identityUntrusted: raw.identityUntrusted === true,
|
|
96
|
+
reusableCheckEvidence: status === 'succeeded' && typeof checkExecution?.cacheKey === 'string' && checkExecution.cacheKey.length > 0,
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
53
103
|
function safeUnlink(path: string): boolean {
|
|
54
104
|
try {
|
|
55
105
|
if (existsSync(path)) {
|
|
@@ -89,6 +139,7 @@ export function gcTerminalProcesses(options: ProcessGcOptions): ProcessGcResult
|
|
|
89
139
|
const active = new Set(listActiveProcessIds(options.controllerHome, options.repoId));
|
|
90
140
|
const terminal: Array<{ processId: string; finishedAt: number; path: string; reusableCheckEvidence: boolean }> = [];
|
|
91
141
|
let skippedActive = 0;
|
|
142
|
+
let skippedInvalid = 0;
|
|
92
143
|
|
|
93
144
|
for (const name of readdirSync(root)) {
|
|
94
145
|
if (!name.endsWith('.json') || name === 'active-index.json') continue;
|
|
@@ -97,12 +148,15 @@ export function gcTerminalProcesses(options: ProcessGcOptions): ProcessGcResult
|
|
|
97
148
|
skippedActive += 1;
|
|
98
149
|
continue;
|
|
99
150
|
}
|
|
100
|
-
const
|
|
101
|
-
if (!
|
|
151
|
+
const metadata = readProcessGcMetadata(join(root, name));
|
|
152
|
+
if (!metadata?.status) {
|
|
153
|
+
skippedInvalid += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
102
156
|
// Skip still-alive identity matches even if status drifted.
|
|
103
|
-
if (
|
|
157
|
+
if (metadata.identity && !metadata.identityUntrusted && !metadata.identity.processStartTime.startsWith('untrusted:')) {
|
|
104
158
|
try {
|
|
105
|
-
if (isProcessAlive(
|
|
159
|
+
if (isProcessAlive(metadata.identity.pid)) {
|
|
106
160
|
skippedActive += 1;
|
|
107
161
|
continue;
|
|
108
162
|
}
|
|
@@ -110,15 +164,15 @@ export function gcTerminalProcesses(options: ProcessGcOptions): ProcessGcResult
|
|
|
110
164
|
/* ignore probe failures */
|
|
111
165
|
}
|
|
112
166
|
}
|
|
113
|
-
if (!TERMINAL.has(
|
|
167
|
+
if (!TERMINAL.has(metadata.status)) continue;
|
|
114
168
|
// Do not delete terminal evidence that has never been read when maxAge is not exceeded
|
|
115
169
|
// unless we are strictly over maxTerminalRecords budget (handled by sort below).
|
|
116
|
-
const finished = Date.parse(
|
|
170
|
+
const finished = Date.parse(metadata.finishedAt ?? metadata.updatedAt ?? '');
|
|
117
171
|
terminal.push({
|
|
118
172
|
processId,
|
|
119
173
|
finishedAt: Number.isFinite(finished) ? finished : 0,
|
|
120
174
|
path: join(root, name),
|
|
121
|
-
reusableCheckEvidence:
|
|
175
|
+
reusableCheckEvidence: metadata.reusableCheckEvidence,
|
|
122
176
|
});
|
|
123
177
|
}
|
|
124
178
|
|
|
@@ -140,7 +194,7 @@ export function gcTerminalProcesses(options: ProcessGcOptions): ProcessGcResult
|
|
|
140
194
|
}
|
|
141
195
|
}
|
|
142
196
|
|
|
143
|
-
return { ok: true, removedRecords, removedLogs, skippedActive };
|
|
197
|
+
return { ok: true, removedRecords, removedLogs, skippedActive, skippedInvalid };
|
|
144
198
|
} catch (error) {
|
|
145
199
|
return {
|
|
146
200
|
ok: false,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { dirname, resolve } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
import {
|
|
5
6
|
controllerCheckExecutionIdentity,
|
|
6
7
|
listControllerChecks,
|
|
@@ -37,7 +38,10 @@ export function resolvePersistedCheckCliInvocation(
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function resolveRuntimeCliTarget(controllerHome?: string): CliRuntimeTarget {
|
|
40
|
-
const
|
|
41
|
+
const moduleDir = typeof import.meta.dir === 'string' && import.meta.dir
|
|
42
|
+
? import.meta.dir
|
|
43
|
+
: dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const moduleSourceRoot = resolve(moduleDir, '..', '..', '..', '..');
|
|
41
45
|
const sourceEntry = resolve(moduleSourceRoot, 'src', 'cli', 'index.ts');
|
|
42
46
|
const source = resolveControllerRuntimeSourceRoot({
|
|
43
47
|
explicitRoot: existsSync(sourceEntry) ? moduleSourceRoot : undefined,
|
|
@@ -214,7 +218,11 @@ export async function runPersistedCheckViaProcessRuntime(
|
|
|
214
218
|
scopeKey: persistedCheckSemanticScopeKey(input, semanticCheck.reuseScope),
|
|
215
219
|
};
|
|
216
220
|
const cliTarget = resolveRuntimeCliTarget(input.controllerHome);
|
|
217
|
-
const checkResultReceiptPath = allocatePersistedCheckResultReceiptPath(
|
|
221
|
+
const checkResultReceiptPath = allocatePersistedCheckResultReceiptPath(
|
|
222
|
+
input.controllerHome,
|
|
223
|
+
input.repoId,
|
|
224
|
+
input.requestId,
|
|
225
|
+
);
|
|
218
226
|
const checkArgs = [
|
|
219
227
|
'--repo',
|
|
220
228
|
executionRoot,
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ResourceClaimSpec } from '../jobs/types';
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
7
9
|
import type { ControllerCheckEffects } from '../../../cli/controller/check-runner';
|
|
8
10
|
import { classifyRepositoryCommand, fixedShellWrapperCommand } from '../../../cli/repositories/command-classifier';
|
|
9
11
|
import { normalizeRepositoryCommand } from '../../../cli/repositories/command-normalization';
|
|
@@ -157,7 +159,89 @@ function iosSimulatorTestCommandClaims(
|
|
|
157
159
|
]);
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
function
|
|
162
|
+
function localHttpServerClaims(command: string | readonly string[]): ResourceClaimSpec[] | undefined {
|
|
163
|
+
const canonical = normalizeRepositoryCommand(command);
|
|
164
|
+
if (canonical.kind !== 'argv') return undefined;
|
|
165
|
+
const program = (canonical.executable ?? '').split(/[\\/]/).at(-1)?.toLowerCase();
|
|
166
|
+
if (program !== 'python' && program !== 'python3') return undefined;
|
|
167
|
+
const args = canonical.args ?? [];
|
|
168
|
+
if (args[0] !== '-m' || args[1] !== 'http.server' || args.includes('--cgi')) return undefined;
|
|
169
|
+
let bind = '0.0.0.0';
|
|
170
|
+
let port = '8000';
|
|
171
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
172
|
+
const arg = args[index]!;
|
|
173
|
+
if (arg === '--bind') { bind = args[index + 1] ?? bind; index += 1; continue; }
|
|
174
|
+
if (arg === '--directory' || arg === '-d' || arg === '--protocol') { index += 1; continue; }
|
|
175
|
+
if (!arg.startsWith('-') && /^\d{1,5}$/.test(arg)) port = arg;
|
|
176
|
+
}
|
|
177
|
+
return [claimHostService(`tcp-listen:${bind}:${port}`, 'write')];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function loopbackCurlClaims(command: string | readonly string[], repoId: string): ResourceClaimSpec[] | undefined {
|
|
181
|
+
const canonical = normalizeRepositoryCommand(command);
|
|
182
|
+
if (canonical.kind !== 'argv') return undefined;
|
|
183
|
+
const program = (canonical.executable ?? '').split(/[\\/]/).at(-1)?.toLowerCase();
|
|
184
|
+
if (program !== 'curl') return undefined;
|
|
185
|
+
const args = canonical.args ?? [];
|
|
186
|
+
if (args.some((arg) => /^(?:-X|--request|-d|--data|--data-raw|--data-binary|-F|--form|-T|--upload-file|-o|--output|-O|--remote-name)$/i.test(arg))) return undefined;
|
|
187
|
+
const explicitMethodIndex = args.findIndex((arg) => arg === '-X' || arg === '--request');
|
|
188
|
+
if (explicitMethodIndex >= 0 && !/^(?:GET|HEAD)$/i.test(args[explicitMethodIndex + 1] ?? '')) return undefined;
|
|
189
|
+
const urls = args.filter((arg) => /^https?:\/\//i.test(arg));
|
|
190
|
+
if (urls.length === 0 || urls.some((url) => {
|
|
191
|
+
try {
|
|
192
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
193
|
+
return !['127.0.0.1', 'localhost', '::1'].includes(host);
|
|
194
|
+
} catch { return true; }
|
|
195
|
+
})) return undefined;
|
|
196
|
+
return [claimNetwork(repoId, 'read')];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function simpleViteScript(script: string): boolean {
|
|
200
|
+
const normalized = script.trim();
|
|
201
|
+
if (!normalized || /[;&|`$<>\n\r]/.test(normalized)) return false;
|
|
202
|
+
return /^(?:(?:\.\/)?node_modules\/.bin\/)?vite(?:\s|$)/.test(normalized);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function viteServiceClaims(
|
|
206
|
+
command: string | readonly string[],
|
|
207
|
+
repoId: string,
|
|
208
|
+
checkoutId?: string,
|
|
209
|
+
repoRoot?: string,
|
|
210
|
+
): ResourceClaimSpec[] | undefined {
|
|
211
|
+
const canonical = normalizeRepositoryCommand(command);
|
|
212
|
+
if (canonical.kind !== 'argv') return undefined;
|
|
213
|
+
const program = (canonical.executable ?? '').split(/[\\/]/).at(-1)?.toLowerCase();
|
|
214
|
+
const args = canonical.args ?? [];
|
|
215
|
+
let vite = program === 'vite';
|
|
216
|
+
if (!vite && (program === 'npx' || program === 'bunx') && args[0]?.toLowerCase() === 'vite') vite = true;
|
|
217
|
+
if (!vite && program === 'bun' && args[0]?.toLowerCase() === 'x' && args[1]?.toLowerCase() === 'vite') vite = true;
|
|
218
|
+
if (!vite && repoRoot && ['npm', 'pnpm', 'yarn', 'bun'].includes(program ?? '')) {
|
|
219
|
+
const runIndex = args[0]?.toLowerCase() === 'run' ? 1 : program === 'yarn' ? 0 : -1;
|
|
220
|
+
const scriptName = runIndex >= 0 ? args[runIndex]?.toLowerCase() : undefined;
|
|
221
|
+
if (scriptName === 'dev') {
|
|
222
|
+
try {
|
|
223
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8')) as { scripts?: Record<string, unknown> };
|
|
224
|
+
const script = typeof pkg.scripts?.dev === 'string' ? pkg.scripts.dev : '';
|
|
225
|
+
vite = simpleViteScript(script);
|
|
226
|
+
} catch {
|
|
227
|
+
vite = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!vite) return undefined;
|
|
232
|
+
return normalizeClaims([
|
|
233
|
+
claimBuildCacheWrite(repoId),
|
|
234
|
+
claimHostService(`vite:${checkoutScope(checkoutId)}`, 'write'),
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function hostOnlyCommandClaims(command: string | readonly string[], repoId: string, checkoutId?: string, repoRoot?: string): ResourceClaimSpec[] | undefined {
|
|
239
|
+
const localServer = localHttpServerClaims(command);
|
|
240
|
+
if (localServer) return localServer;
|
|
241
|
+
const loopbackCurl = loopbackCurlClaims(command, repoId);
|
|
242
|
+
if (loopbackCurl) return loopbackCurl;
|
|
243
|
+
const vite = viteServiceClaims(command, repoId, checkoutId, repoRoot);
|
|
244
|
+
if (vite) return vite;
|
|
161
245
|
const canonical = normalizeRepositoryCommand(command);
|
|
162
246
|
if (canonical.kind === 'shell') {
|
|
163
247
|
return browserAppleScriptHeredocClaims(canonical.shellCommand ?? '');
|
|
@@ -347,13 +431,14 @@ export function claimsForRepositoryCommand(
|
|
|
347
431
|
repoId: string,
|
|
348
432
|
checkoutId?: string,
|
|
349
433
|
defaultBranch?: string,
|
|
434
|
+
repoRoot?: string,
|
|
350
435
|
): ResourceClaimSpec[] {
|
|
351
436
|
const classification = classifyRepositoryCommand(command, defaultBranch);
|
|
352
437
|
const canonical = normalizeRepositoryCommand(command);
|
|
353
438
|
const focused = isFocusedCheckCommand(command);
|
|
354
439
|
const iosSimulatorClaims = iosSimulatorTestCommandClaims(command, repoId, checkoutId);
|
|
355
440
|
if (iosSimulatorClaims) return iosSimulatorClaims;
|
|
356
|
-
const hostClaims = hostOnlyCommandClaims(command);
|
|
441
|
+
const hostClaims = hostOnlyCommandClaims(command, repoId, checkoutId, repoRoot);
|
|
357
442
|
if (hostClaims) return hostClaims;
|
|
358
443
|
|
|
359
444
|
if (classification.risk === 'readonly') {
|
|
@@ -258,8 +258,23 @@ function captureIdentity(pid: number | undefined): {
|
|
|
258
258
|
} {
|
|
259
259
|
if (!pid || pid <= 0) return {};
|
|
260
260
|
const probe = defaultProcessIdentityProbe;
|
|
261
|
-
|
|
261
|
+
// The combined inspect probe already proves the PID exists on the normal
|
|
262
|
+
// POSIX path. Avoid an immediately preceding ps(stat) child process; only
|
|
263
|
+
// fall back to the separate alive/command/start-time probes when the combined
|
|
264
|
+
// observation is unavailable or incomplete.
|
|
262
265
|
const inspected = probe.inspect?.(pid);
|
|
266
|
+
if (inspected?.command && inspected.startTime) {
|
|
267
|
+
return {
|
|
268
|
+
identity: {
|
|
269
|
+
pid,
|
|
270
|
+
processStartTime: inspected.startTime,
|
|
271
|
+
executableFingerprint: executableFingerprint(inspected.command),
|
|
272
|
+
processGroupId: process.platform !== 'win32' ? pid : undefined,
|
|
273
|
+
},
|
|
274
|
+
identityUntrusted: false,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (!probe.isAlive(pid)) return {};
|
|
263
278
|
const command = inspected?.command ?? probe.command(pid);
|
|
264
279
|
const startTime = inspected?.startTime ?? probe.startTime(pid);
|
|
265
280
|
if (!command || !startTime) {
|
|
@@ -308,6 +308,12 @@ function sanitizeProcessRecord(record: ManagedProcessRecord): { record: ManagedP
|
|
|
308
308
|
function readProcessRecord(path: string): ManagedProcessRecord | undefined {
|
|
309
309
|
const record = readJson<ManagedProcessRecord>(path);
|
|
310
310
|
if (!record || record.schemaVersion !== 1) return undefined;
|
|
311
|
+
// Legacy/partial Process files can predate the required command descriptor.
|
|
312
|
+
// They are not executable recovery authority and must never be repaired by
|
|
313
|
+
// inventing a command. Treat them as unavailable to normal Runtime readers;
|
|
314
|
+
// bounded GC has its own metadata-only parser so one such file cannot block
|
|
315
|
+
// cleanup of unrelated proven terminal records.
|
|
316
|
+
if (!record.command || typeof record.command !== 'object' || Array.isArray(record.command)) return undefined;
|
|
311
317
|
const sanitized = sanitizeProcessRecord(record);
|
|
312
318
|
if (sanitized.changed) atomicWrite(path, sanitized.record);
|
|
313
319
|
return sanitized.record;
|
|
@@ -659,7 +659,11 @@ function terminalCleanupOutcome(
|
|
|
659
659
|
if (contract?.status === 'cancelled') return 'cancelled';
|
|
660
660
|
const reason = `${handle.failureReason ?? ''} ${handle.finalization.lastError ?? ''}`.toLowerCase();
|
|
661
661
|
if (contract?.status === 'blocked' && reason.includes('terminal')) return 'blocked_terminal';
|
|
662
|
-
|
|
662
|
+
// `failed` is a retryable execution-handle state (its transition table allows
|
|
663
|
+
// failed -> validating/editing). Do not reinterpret it as terminal cleanup
|
|
664
|
+
// unless the durable Work itself is terminal. `failed_terminal_cleanup` is
|
|
665
|
+
// the explicit point of no return for resource cleanup reconciliation.
|
|
666
|
+
if (contract?.status === 'failed' || handle.state === 'failed_terminal_cleanup') {
|
|
663
667
|
if (reason.includes('infrastructure') || reason.includes('timed out') || reason.includes('unavailable')) {
|
|
664
668
|
return 'infrastructure_failed';
|
|
665
669
|
}
|
|
@@ -1202,12 +1206,20 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1202
1206
|
repoId: handle.repositoryId,
|
|
1203
1207
|
checkoutId: handle.checkoutId,
|
|
1204
1208
|
repoRoot: validated.worktreeRepository.canonicalRoot,
|
|
1205
|
-
executionIdentity:
|
|
1209
|
+
executionIdentity: Object.freeze({
|
|
1210
|
+
...executionIdentityForWork(validated.worktreeRepository, handle),
|
|
1211
|
+
expectedHead: validationHead === 'unknown' ? handle.expectedHead : validationHead,
|
|
1212
|
+
}),
|
|
1206
1213
|
checkId,
|
|
1207
1214
|
requestId: processRequestId,
|
|
1208
1215
|
workId: handle.workId,
|
|
1209
1216
|
commandId: processRequestId,
|
|
1210
1217
|
verificationBinding: { executionSessionId: session.sessionId },
|
|
1218
|
+
verificationSnapshot: contract && contract.allowedPaths.length > 0 ? {
|
|
1219
|
+
workId: contract.workId,
|
|
1220
|
+
allowedPaths: contract.allowedPaths,
|
|
1221
|
+
forbiddenPaths: contract.forbiddenPaths,
|
|
1222
|
+
} : undefined,
|
|
1211
1223
|
});
|
|
1212
1224
|
if (executed.mode === 'durable') {
|
|
1213
1225
|
checks.push({ checkId, ok: undefined, status: 'deferred', summary: executed.durable?.reason, durable: executed.durable });
|
|
@@ -1236,7 +1248,6 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1236
1248
|
repoId: handle.repositoryId,
|
|
1237
1249
|
checkoutId: handle.checkoutId,
|
|
1238
1250
|
workId: handle.workId,
|
|
1239
|
-
executionSessionId: session.sessionId,
|
|
1240
1251
|
checkId,
|
|
1241
1252
|
processId: process.processId,
|
|
1242
1253
|
});
|
|
@@ -1311,21 +1322,25 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1311
1322
|
failureSummary,
|
|
1312
1323
|
);
|
|
1313
1324
|
}
|
|
1314
|
-
if (completed &&
|
|
1315
|
-
const cleanup = await reconcileTerminalCleanup(
|
|
1316
|
-
ctx,
|
|
1317
|
-
session,
|
|
1318
|
-
next,
|
|
1319
|
-
args,
|
|
1320
|
-
infrastructureFailure ? 'infrastructure_failed' : 'validation_failed',
|
|
1321
|
-
);
|
|
1325
|
+
if (completed && acceptedFailure) {
|
|
1326
|
+
const cleanup = await reconcileTerminalCleanup(ctx, session, next, args, 'validation_failed');
|
|
1322
1327
|
const value = {
|
|
1323
1328
|
...cleanup,
|
|
1324
1329
|
validation: { passed, completed, checks, targeted: true, changedPaths, cleanupTriggered: true },
|
|
1325
1330
|
};
|
|
1326
1331
|
return makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
|
|
1327
1332
|
}
|
|
1328
|
-
const value = {
|
|
1333
|
+
const value = {
|
|
1334
|
+
work: compactHandle(next),
|
|
1335
|
+
validation: {
|
|
1336
|
+
passed,
|
|
1337
|
+
completed,
|
|
1338
|
+
checks,
|
|
1339
|
+
targeted: true,
|
|
1340
|
+
changedPaths,
|
|
1341
|
+
...(infrastructureFailure ? { retryable: true, cleanupTriggered: false } : {}),
|
|
1342
|
+
},
|
|
1343
|
+
};
|
|
1329
1344
|
return makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
|
|
1330
1345
|
}
|
|
1331
1346
|
|
|
@@ -898,7 +898,7 @@ export async function routeDurableMcpCall(
|
|
|
898
898
|
}, validation.isError === true);
|
|
899
899
|
}
|
|
900
900
|
|
|
901
|
-
const applied = await callRepositoryTool(ctx.controllerHome, name, args);
|
|
901
|
+
const applied = await callRepositoryTool(ctx.controllerHome, name, args, ctx);
|
|
902
902
|
if (!applied || applied.isError === true) return applied;
|
|
903
903
|
const patchPayload = (applied.structuredContent ?? {}) as Record<string, unknown>;
|
|
904
904
|
if (patchPayload.status !== 'applied') return applied;
|