@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
|
@@ -248,13 +248,23 @@ function desktopSession(status: Record<string, unknown>, interactionId: string):
|
|
|
248
248
|
return recordArray(status.sessions).find((session) => firstString(session, 'interactionId', 'interaction_id') === interactionId);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
});
|
|
251
|
+
async function independentlyEnsureExactFrontmostDesktopApplication(
|
|
252
|
+
bundleId: string,
|
|
253
|
+
appName: string,
|
|
254
|
+
dependencies: ExternalPluginAdapterDependencies,
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
const activateAndVerify = dependencies.activateAndVerifyFrontmostApplication ?? activateAndVerifyFrontmostApplication;
|
|
257
|
+
const proof = await activateAndVerify({ bundleId: bundleId || undefined, appName: appName || undefined });
|
|
258
|
+
const proofBundleId = typeof proof.bundleId === 'string' ? proof.bundleId.trim() : '';
|
|
259
|
+
const proofAppName = typeof proof.appName === 'string' ? proof.appName.trim() : '';
|
|
260
|
+
const proofMatches = bundleId ? proofBundleId === bundleId : proofAppName === appName;
|
|
261
|
+
if (proof.activated !== true || proof.verified !== true || !proofMatches) {
|
|
262
|
+
throw new AssistantPluginError(
|
|
263
|
+
'DESKTOP_ACTIVATION_FALLBACK_IDENTITY_MISMATCH',
|
|
264
|
+
`Forge did not prove the exact requested application ${bundleId || appName} as frontmost.`,
|
|
265
|
+
{ retryable: true, details: { target: { bundleId, appName }, observed: { bundleId: proofBundleId, appName: proofAppName } } },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
258
268
|
}
|
|
259
269
|
|
|
260
270
|
async function openVerifiedDesktopSession(
|
|
@@ -272,7 +282,7 @@ async function openVerifiedDesktopSession(
|
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
let result: Record<string, unknown>;
|
|
275
|
-
let
|
|
285
|
+
let providerActivationError: AssistantPluginError | undefined;
|
|
276
286
|
try {
|
|
277
287
|
result = await callProvider(
|
|
278
288
|
registration,
|
|
@@ -285,7 +295,7 @@ async function openVerifiedDesktopSession(
|
|
|
285
295
|
);
|
|
286
296
|
} catch (error) {
|
|
287
297
|
if (!(error instanceof AssistantPluginError) || error.code !== 'APP_ACTIVATION_FAILED') throw error;
|
|
288
|
-
|
|
298
|
+
providerActivationError = error;
|
|
289
299
|
result = await callProvider(
|
|
290
300
|
registration,
|
|
291
301
|
`${requestId}:fallback-session`,
|
|
@@ -295,59 +305,36 @@ async function openVerifiedDesktopSession(
|
|
|
295
305
|
signal,
|
|
296
306
|
dependencies,
|
|
297
307
|
);
|
|
298
|
-
|
|
299
|
-
const activateFallback = dependencies.activateAndVerifyFrontmostApplication ?? activateAndVerifyFrontmostApplication;
|
|
300
|
-
try {
|
|
301
|
-
const fallbackProof = await activateFallback({ bundleId: bundleId || undefined, appName: appName || undefined });
|
|
302
|
-
const proofBundleId = typeof fallbackProof.bundleId === 'string' ? fallbackProof.bundleId.trim() : '';
|
|
303
|
-
const proofAppName = typeof fallbackProof.appName === 'string' ? fallbackProof.appName.trim() : '';
|
|
304
|
-
const proofMatches = bundleId ? proofBundleId === bundleId : proofAppName === appName;
|
|
305
|
-
if (fallbackProof.activated !== true || fallbackProof.verified !== true || !proofMatches) {
|
|
306
|
-
throw new AssistantPluginError(
|
|
307
|
-
'DESKTOP_ACTIVATION_FALLBACK_IDENTITY_MISMATCH',
|
|
308
|
-
`Forge fallback did not prove the exact requested application ${bundleId || appName} as frontmost.`,
|
|
309
|
-
{ retryable: true, details: { target: { bundleId, appName }, observed: { bundleId: proofBundleId, appName: proofAppName } } },
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
fallbackVerified = true;
|
|
313
|
-
} catch (fallbackError) {
|
|
314
|
-
if (fallbackError instanceof AssistantPluginError) {
|
|
315
|
-
throw new AssistantPluginError(
|
|
316
|
-
'DESKTOP_ACTIVATION_FALLBACK_FAILED',
|
|
317
|
-
`Desktop Operator activation failed and Forge could not independently verify ${bundleId || appName} as frontmost.`,
|
|
318
|
-
{
|
|
319
|
-
retryable: fallbackError.retryable,
|
|
320
|
-
details: {
|
|
321
|
-
providerErrorCode: error.code,
|
|
322
|
-
fallbackErrorCode: fallbackError.code,
|
|
323
|
-
fallbackErrorMessage: fallbackError.message,
|
|
324
|
-
target: bundleId || appName,
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
throw new AssistantPluginError(
|
|
330
|
-
'DESKTOP_ACTIVATION_FALLBACK_FAILED',
|
|
331
|
-
`Desktop Operator activation failed and Forge fallback failed for ${bundleId || appName}.`,
|
|
332
|
-
{ retryable: true, details: { providerErrorCode: error.code, fallbackErrorMessage: String(fallbackError), target: bundleId || appName } },
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
308
|
}
|
|
336
309
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
{
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
310
|
+
try {
|
|
311
|
+
await independentlyEnsureExactFrontmostDesktopApplication(bundleId, appName, dependencies);
|
|
312
|
+
} catch (verificationError) {
|
|
313
|
+
const wrappedCode = providerActivationError ? 'DESKTOP_ACTIVATION_FALLBACK_FAILED' : 'DESKTOP_ACTIVATION_NOT_CONFIRMED';
|
|
314
|
+
const wrappedMessage = providerActivationError
|
|
315
|
+
? `Desktop Operator activation failed and Forge could not independently verify ${bundleId || appName} as frontmost.`
|
|
316
|
+
: `Desktop Operator returned an activated session, but Forge could not independently verify ${bundleId || appName} as frontmost.`;
|
|
317
|
+
if (verificationError instanceof AssistantPluginError) {
|
|
318
|
+
throw new AssistantPluginError(wrappedCode, wrappedMessage, {
|
|
319
|
+
retryable: verificationError.retryable,
|
|
320
|
+
details: {
|
|
321
|
+
...(providerActivationError ? { providerErrorCode: providerActivationError.code } : {}),
|
|
322
|
+
verificationErrorCode: verificationError.code,
|
|
323
|
+
verificationErrorMessage: verificationError.message,
|
|
324
|
+
target: bundleId || appName,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
349
327
|
}
|
|
328
|
+
throw new AssistantPluginError(wrappedCode, wrappedMessage, {
|
|
329
|
+
retryable: true,
|
|
330
|
+
details: {
|
|
331
|
+
...(providerActivationError ? { providerErrorCode: providerActivationError.code } : {}),
|
|
332
|
+
verificationErrorMessage: String(verificationError),
|
|
333
|
+
target: bundleId || appName,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
350
336
|
}
|
|
337
|
+
|
|
351
338
|
return result;
|
|
352
339
|
}
|
|
353
340
|
|
|
@@ -616,6 +603,53 @@ export function createExternalPluginAdapter(
|
|
|
616
603
|
};
|
|
617
604
|
}
|
|
618
605
|
|
|
606
|
+
if (registration.pluginId === 'desktop_operator' && input.actionId === 'desktop_key') {
|
|
607
|
+
const sourceInteractionId = typeof input.args.interaction_id === 'string' ? input.args.interaction_id.trim() : '';
|
|
608
|
+
const keys = Array.isArray(input.args.keys) ? input.args.keys : [];
|
|
609
|
+
if (!sourceInteractionId || keys.length === 0) {
|
|
610
|
+
throw providerError('DESKTOP_FOREGROUND_KEY_ARGUMENT_INVALID', 'desktop_key requires interaction_id and at least one key.');
|
|
611
|
+
}
|
|
612
|
+
const sourceStatus = await callProvider(
|
|
613
|
+
registration,
|
|
614
|
+
`${input.requestId}:source-status`,
|
|
615
|
+
'desktop_status',
|
|
616
|
+
{ limit: 500 },
|
|
617
|
+
input.timeoutMs,
|
|
618
|
+
input.signal,
|
|
619
|
+
dependencies,
|
|
620
|
+
);
|
|
621
|
+
const sourceSession = desktopSession(sourceStatus, sourceInteractionId);
|
|
622
|
+
if (!sourceSession) {
|
|
623
|
+
throw providerError('DESKTOP_FOREGROUND_KEY_SESSION_NOT_FOUND', `Desktop session ${sourceInteractionId} is no longer available.`, true);
|
|
624
|
+
}
|
|
625
|
+
const bundleId = firstString(sourceSession, 'bundleIdentifier', 'bundle_id');
|
|
626
|
+
const appName = firstString(sourceSession, 'appName', 'app_name');
|
|
627
|
+
if (!bundleId && !appName) {
|
|
628
|
+
throw providerError('DESKTOP_FOREGROUND_KEY_TARGET_UNAVAILABLE', `Desktop session ${sourceInteractionId} has no stable application identity.`);
|
|
629
|
+
}
|
|
630
|
+
const activation = await openVerifiedDesktopSession(
|
|
631
|
+
registration,
|
|
632
|
+
`${input.requestId}:activate`,
|
|
633
|
+
{ ...(bundleId ? { bundle_id: bundleId } : { app_name: appName }), launch: false, activate: true },
|
|
634
|
+
input.timeoutMs,
|
|
635
|
+
input.signal,
|
|
636
|
+
dependencies,
|
|
637
|
+
);
|
|
638
|
+
const activationInteractionId = firstString(activation, 'interactionId', 'interaction_id');
|
|
639
|
+
if (!activationInteractionId) {
|
|
640
|
+
throw providerError('DESKTOP_ACTIVATION_SESSION_MISSING', 'Desktop Operator activated the application without returning a bound interaction session.', true);
|
|
641
|
+
}
|
|
642
|
+
return await callProvider(
|
|
643
|
+
registration,
|
|
644
|
+
`${input.requestId}:key`,
|
|
645
|
+
'desktop_key',
|
|
646
|
+
{ interaction_id: activationInteractionId, keys },
|
|
647
|
+
input.timeoutMs,
|
|
648
|
+
input.signal,
|
|
649
|
+
dependencies,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
619
653
|
if (registration.pluginId === 'desktop_operator' && input.actionId === 'desktop_session_open' && input.args.activate === true) {
|
|
620
654
|
return await openVerifiedDesktopSession(
|
|
621
655
|
registration,
|
|
@@ -54,14 +54,6 @@ export function validateMacOsCapabilityBrokerHandshake(
|
|
|
54
54
|
? handshake.browserAutomationActions.filter((entry): entry is string => typeof entry === 'string')
|
|
55
55
|
: [];
|
|
56
56
|
const browserProtocolVersion = handshake.browserAutomationProtocolVersion;
|
|
57
|
-
const declaresBrowserMetadata = Array.isArray(handshake.internalCapabilities)
|
|
58
|
-
|| Array.isArray(handshake.browserAutomationActions)
|
|
59
|
-
|| browserProtocolVersion !== undefined;
|
|
60
|
-
// Desktop Operator v0.2.1 predates optional browser capability metadata in its handshake,
|
|
61
|
-
// but it implements the separately routed macos_browser_automation method. In that legacy
|
|
62
|
-
// shape, let the real method/action call prove support. Unsupported methods/actions still
|
|
63
|
-
// fail closed at the provider boundary.
|
|
64
|
-
if (!declaresBrowserMetadata) return;
|
|
65
57
|
if (!capabilities.includes(BROWSER_AUTOMATION_CAPABILITY)
|
|
66
58
|
|| browserProtocolVersion !== BROWSER_AUTOMATION_PROTOCOL_VERSION
|
|
67
59
|
|| (requestedAction && !actions.includes(requestedAction))) {
|
|
@@ -598,6 +598,8 @@ export interface PluginActionReceipt {
|
|
|
598
598
|
receiptId: string;
|
|
599
599
|
requestId: string;
|
|
600
600
|
repoId: string;
|
|
601
|
+
/** Work authority repository when it differs from controller/provider receipt scope. */
|
|
602
|
+
workRepoId?: string;
|
|
601
603
|
pluginId: string;
|
|
602
604
|
actionId: string;
|
|
603
605
|
semanticKey: string;
|
|
@@ -613,6 +615,7 @@ export interface PluginActionReceipt {
|
|
|
613
615
|
interface PluginActionRequestIndex {
|
|
614
616
|
requestId: string;
|
|
615
617
|
repoId: string;
|
|
618
|
+
workRepoId?: string;
|
|
616
619
|
receiptId: string;
|
|
617
620
|
semanticKey: string;
|
|
618
621
|
createdAt: string;
|
|
@@ -733,6 +736,15 @@ function localEffectTarget(
|
|
|
733
736
|
};
|
|
734
737
|
}
|
|
735
738
|
|
|
739
|
+
function workAttributionRepoId(repository: RepositoryRecord, request: AssistantPluginActionRequest): string {
|
|
740
|
+
const explicit = request.workRepoId?.trim();
|
|
741
|
+
if (!request.workId?.trim()) {
|
|
742
|
+
if (explicit) throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_WITHOUT_WORK: ${explicit}`);
|
|
743
|
+
return repository.repoId;
|
|
744
|
+
}
|
|
745
|
+
return explicit || repository.repoId;
|
|
746
|
+
}
|
|
747
|
+
|
|
736
748
|
function remoteEffectWorkForPluginAction(
|
|
737
749
|
controllerHome: string,
|
|
738
750
|
repository: RepositoryRecord,
|
|
@@ -745,7 +757,8 @@ function remoteEffectWorkForPluginAction(
|
|
|
745
757
|
if (action.risk !== 'remote_write') {
|
|
746
758
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_REQUIRES_REMOTE_WRITE: ${action.actionId}`);
|
|
747
759
|
}
|
|
748
|
-
const
|
|
760
|
+
const workRepoId = workAttributionRepoId(repository, request);
|
|
761
|
+
const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
|
|
749
762
|
if (!work) throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_NOT_FOUND: ${workId}`);
|
|
750
763
|
if (work.workKind !== 'remote_effect') {
|
|
751
764
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
|
|
@@ -772,14 +785,38 @@ function attributedWorkForPluginAction(
|
|
|
772
785
|
): WorkContract | undefined {
|
|
773
786
|
const workId = request.workId?.trim();
|
|
774
787
|
if (!workId || action.risk === 'remote_write') return undefined;
|
|
775
|
-
const
|
|
776
|
-
|
|
788
|
+
const workRepoId = workAttributionRepoId(repository, request);
|
|
789
|
+
const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
|
|
790
|
+
if (!work) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${workId}`);
|
|
777
791
|
if (isTerminalWorkContractStatus(work.status)) {
|
|
778
792
|
throw new Error(`WORK_PLUGIN_ATTRIBUTION_TERMINAL: ${workId} is ${work.status}`);
|
|
779
793
|
}
|
|
780
794
|
return work;
|
|
781
795
|
}
|
|
782
796
|
|
|
797
|
+
function bindLocalEffectReceiptToAttributedWork(
|
|
798
|
+
controllerHome: string,
|
|
799
|
+
repository: RepositoryRecord,
|
|
800
|
+
action: AssistantPluginActionDescriptor,
|
|
801
|
+
request: AssistantPluginActionRequest,
|
|
802
|
+
receipt: PluginActionReceipt,
|
|
803
|
+
work?: WorkContract,
|
|
804
|
+
): WorkContract | undefined {
|
|
805
|
+
if (!request.workId || receipt.status !== 'succeeded') return work;
|
|
806
|
+
const workRepoId = workAttributionRepoId(repository, request);
|
|
807
|
+
const attributed = work ?? getWorkContract({ controllerHome, repoId: workRepoId }, request.workId.trim());
|
|
808
|
+
if (!attributed) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${request.workId.trim()}`);
|
|
809
|
+
if (attributed.workKind !== 'local_effect' || action.readOnly || action.risk === 'readonly') return attributed;
|
|
810
|
+
if (attributed.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) return attributed;
|
|
811
|
+
if (isTerminalWorkContractStatus(attributed.status)) return attributed;
|
|
812
|
+
return appendWorkEvidence({ controllerHome, repoId: workRepoId }, attributed.workId, {
|
|
813
|
+
evidenceId: receipt.receiptId,
|
|
814
|
+
title: 'typed local plugin effect completed',
|
|
815
|
+
summary: `${receipt.pluginId}/${receipt.actionId} completed with durable receipt ${receipt.receiptId}.`,
|
|
816
|
+
detailLevel: 'summary',
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
783
820
|
export function finalizeRemoteEffectWorkFromActionReceipt(
|
|
784
821
|
controllerHome: string,
|
|
785
822
|
repoId: string,
|
|
@@ -795,7 +832,11 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
|
|
|
795
832
|
throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_TERMINAL: ${workId} is ${work.status}`);
|
|
796
833
|
}
|
|
797
834
|
const receipt = work.evidenceRefs
|
|
798
|
-
.map((evidence) =>
|
|
835
|
+
.map((evidence) => {
|
|
836
|
+
if (!evidence.evidenceId) return undefined;
|
|
837
|
+
return readPluginActionReceipt(controllerHome, repoId, evidence.evidenceId)
|
|
838
|
+
?? readPluginActionReceipt(controllerHome, CONTROLLER_SCOPE_REPO_ID, evidence.evidenceId);
|
|
839
|
+
})
|
|
799
840
|
.find((candidate) => candidate?.workId === workId && candidate.status === 'succeeded');
|
|
800
841
|
if (!receipt) {
|
|
801
842
|
throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_RECEIPT_REQUIRED: ${workId} has no successful durable plugin action receipt`);
|
|
@@ -833,9 +874,10 @@ function bindRemoteEffectReceiptToWork(
|
|
|
833
874
|
const work = remoteEffectWorkForPluginAction(controllerHome, repository, action, request, receipt.receiptId);
|
|
834
875
|
if (!work) return undefined;
|
|
835
876
|
if (work.status === 'completed') return work;
|
|
877
|
+
const workRepoId = workAttributionRepoId(repository, request);
|
|
836
878
|
let updated = work;
|
|
837
879
|
if (!work.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) {
|
|
838
|
-
updated = appendWorkEvidence({ controllerHome, repoId:
|
|
880
|
+
updated = appendWorkEvidence({ controllerHome, repoId: workRepoId }, work.workId, {
|
|
839
881
|
evidenceId: receipt.receiptId,
|
|
840
882
|
title: 'typed remote plugin effect completed',
|
|
841
883
|
summary: `${receipt.pluginId}/${receipt.actionId} completed with durable receipt ${receipt.receiptId}.`,
|
|
@@ -856,7 +898,7 @@ function bindRemoteEffectReceiptToWork(
|
|
|
856
898
|
recordedAt: new Date().toISOString(),
|
|
857
899
|
};
|
|
858
900
|
return recordWorkCompletionReceipt(
|
|
859
|
-
{ controllerHome, repoId:
|
|
901
|
+
{ controllerHome, repoId: workRepoId },
|
|
860
902
|
work.workId,
|
|
861
903
|
remoteReceipt,
|
|
862
904
|
'completed_remote',
|
|
@@ -940,7 +982,15 @@ export async function submitAssistantPluginAction(
|
|
|
940
982
|
if (request.workId && existing.workId !== request.workId) {
|
|
941
983
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CONFLICT: ${request.requestId} already belongs to Work ${existing.workId ?? 'none'}`);
|
|
942
984
|
}
|
|
943
|
-
|
|
985
|
+
const requestedWorkRepoId = request.workId ? workAttributionRepoId(repository, request) : undefined;
|
|
986
|
+
if (requestedWorkRepoId && (index.workRepoId ?? index.repoId) !== requestedWorkRepoId) {
|
|
987
|
+
throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_CONFLICT: ${request.requestId} already belongs to Work repository ${index.workRepoId ?? index.repoId}`);
|
|
988
|
+
}
|
|
989
|
+
if (request.workId && action.risk === 'remote_write') {
|
|
990
|
+
bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, existing);
|
|
991
|
+
} else if (request.workId) {
|
|
992
|
+
bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, existing);
|
|
993
|
+
}
|
|
944
994
|
return {
|
|
945
995
|
manifest,
|
|
946
996
|
action,
|
|
@@ -1181,6 +1231,7 @@ export async function submitAssistantPluginAction(
|
|
|
1181
1231
|
receiptId,
|
|
1182
1232
|
requestId: request.requestId,
|
|
1183
1233
|
repoId: repository.repoId,
|
|
1234
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
|
|
1184
1235
|
pluginId: request.pluginId,
|
|
1185
1236
|
actionId: request.actionId,
|
|
1186
1237
|
semanticKey: key,
|
|
@@ -1201,11 +1252,16 @@ export async function submitAssistantPluginAction(
|
|
|
1201
1252
|
writeJsonAtomic(requestPath, {
|
|
1202
1253
|
requestId: request.requestId,
|
|
1203
1254
|
repoId: repository.repoId,
|
|
1255
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
|
|
1204
1256
|
receiptId,
|
|
1205
1257
|
semanticKey: key,
|
|
1206
1258
|
createdAt,
|
|
1207
1259
|
} satisfies PluginActionRequestIndex);
|
|
1208
|
-
if (boundRemoteWork)
|
|
1260
|
+
if (boundRemoteWork) {
|
|
1261
|
+
bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, receipt);
|
|
1262
|
+
} else if (attributedWork) {
|
|
1263
|
+
bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, receipt, attributedWork);
|
|
1264
|
+
}
|
|
1209
1265
|
const nextManifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
|
|
1210
1266
|
return {
|
|
1211
1267
|
manifest: nextManifest,
|
|
@@ -1221,7 +1277,7 @@ export async function submitAssistantPluginAction(
|
|
|
1221
1277
|
const message = error instanceof Error ? error.message : String(error);
|
|
1222
1278
|
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'PLUGIN_ACTION_FAILED';
|
|
1223
1279
|
if (boundRemoteWork) {
|
|
1224
|
-
appendWorkEvidence({ controllerHome, repoId: repository
|
|
1280
|
+
appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(repository, request) }, boundRemoteWork.workId, {
|
|
1225
1281
|
title: 'typed remote plugin effect failed',
|
|
1226
1282
|
summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
|
|
1227
1283
|
detailLevel: 'summary',
|
|
@@ -1246,6 +1302,7 @@ export async function submitAssistantPluginAction(
|
|
|
1246
1302
|
receiptId,
|
|
1247
1303
|
requestId: request.requestId,
|
|
1248
1304
|
repoId: repository.repoId,
|
|
1305
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
|
|
1249
1306
|
pluginId: request.pluginId,
|
|
1250
1307
|
actionId: request.actionId,
|
|
1251
1308
|
semanticKey: key,
|
|
@@ -124,8 +124,10 @@ export interface AssistantPluginActionRequest {
|
|
|
124
124
|
pluginId: string;
|
|
125
125
|
actionId: string;
|
|
126
126
|
requestId: string;
|
|
127
|
-
/** Optional existing
|
|
127
|
+
/** Optional existing Work that owns this typed action receipt. */
|
|
128
128
|
workId?: string;
|
|
129
|
+
/** Durable repository authority for workId. May differ from provider scope for controller-scoped plugins. */
|
|
130
|
+
workRepoId?: string;
|
|
129
131
|
args: Record<string, unknown>;
|
|
130
132
|
timeoutMs?: number;
|
|
131
133
|
signal?: AbortSignal;
|
|
@@ -91,7 +91,7 @@ export interface RuntimeMaintenanceCandidate {
|
|
|
91
91
|
sourceControllerHome?: string;
|
|
92
92
|
objective?: string;
|
|
93
93
|
continuationPrompt?: string;
|
|
94
|
-
disposition?: 'resume_or_supersede_review' | 'completion_reconciliation' | 'ownership_reconciliation' | 'source_preservation_required';
|
|
94
|
+
disposition?: 'resume_or_supersede_review' | 'completion_reconciliation' | 'ownership_reconciliation' | 'source_preservation_required' | 'semantic_completion_required';
|
|
95
95
|
sourceState?: 'no_managed_source' | 'clean_integrated' | 'dirty_worktree' | 'unintegrated_commits' | 'source_state_unknown';
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -584,10 +584,20 @@ function inspectStaleWorkRepositorySource(
|
|
|
584
584
|
};
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
-
function
|
|
588
|
-
return
|
|
589
|
-
|
|
590
|
-
|
|
587
|
+
function staleWorkSemanticTerminalizationReady(contract: WorkContract): boolean {
|
|
588
|
+
if (contract.phase !== 'cleanup') return false;
|
|
589
|
+
return (['implementation', 'verification', 'delivery'] as const).every((phase) =>
|
|
590
|
+
['satisfied', 'skipped'].includes(contract.phaseEvidence[phase].state));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function staleWorkCandidateReason(source: StaleWorkSourceInspection, semanticReady: boolean): string {
|
|
594
|
+
if (!source.safeToCancel) {
|
|
595
|
+
return `Protected stale Work: no active lifecycle authority remains, but repository source preservation is required. ${source.detail} The Work must remain nonterminal until source is explicitly adopted, delivered, or cleanup is semantically authorized.`;
|
|
596
|
+
}
|
|
597
|
+
if (!semanticReady) {
|
|
598
|
+
return `Protected stale Work: repository source has no unique live changes, but the Work semantic lifecycle has not reached cleanup with implementation, verification, and delivery explicitly satisfied or skipped. ${source.detail} Maintenance may clean source debt, but it cannot infer that the objective or acceptance criteria are complete.`;
|
|
599
|
+
}
|
|
600
|
+
return `Cleanup-ready WorkContract exceeded the explicit maintenance age threshold and has no active Plan, Requirement, Schedule, or Controller authority. ${source.detail} Explicit full maintenance may finish cleanup while retaining durable evidence.`;
|
|
591
601
|
}
|
|
592
602
|
|
|
593
603
|
function scanRetainedWorktreeCandidates(
|
|
@@ -738,19 +748,26 @@ function scanStaleWorkContractCandidates(
|
|
|
738
748
|
})
|
|
739
749
|
.sort((left, right) => right.ageMinutes - left.ageMinutes)
|
|
740
750
|
.slice(0, options.maxCandidates)
|
|
741
|
-
.map(({ contract, ageMinutes, source }) =>
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
751
|
+
.map(({ contract, ageMinutes, source }) => {
|
|
752
|
+
const semanticReady = staleWorkSemanticTerminalizationReady(contract);
|
|
753
|
+
return {
|
|
754
|
+
kind: 'stale_work_contract' as const,
|
|
755
|
+
id: contract.workId,
|
|
756
|
+
path: source.path,
|
|
757
|
+
status: contract.status,
|
|
758
|
+
safe: source.safeToCancel && semanticReady,
|
|
759
|
+
reason: staleWorkCandidateReason(source, semanticReady),
|
|
760
|
+
ageMinutes,
|
|
761
|
+
suggestedAction: 'full_maintenance_pass' as const,
|
|
762
|
+
ownershipStatus: 'explicit' as const,
|
|
763
|
+
sourceState: source.state,
|
|
764
|
+
disposition: !source.safeToCancel
|
|
765
|
+
? ('source_preservation_required' as const)
|
|
766
|
+
: !semanticReady
|
|
767
|
+
? ('semantic_completion_required' as const)
|
|
768
|
+
: undefined,
|
|
769
|
+
};
|
|
770
|
+
});
|
|
754
771
|
}
|
|
755
772
|
|
|
756
773
|
export function applyStaleWorkContractMaintenanceCandidate(
|
|
@@ -794,25 +811,38 @@ export function applyStaleWorkContractMaintenanceCandidate(
|
|
|
794
811
|
...candidate,
|
|
795
812
|
path: source.path ?? candidate.path,
|
|
796
813
|
safe: false,
|
|
797
|
-
reason: staleWorkCandidateReason(source),
|
|
814
|
+
reason: staleWorkCandidateReason(source, staleWorkSemanticTerminalizationReady(current)),
|
|
798
815
|
sourceState: source.state,
|
|
799
816
|
disposition: 'source_preservation_required' as const,
|
|
800
817
|
applied: false,
|
|
801
818
|
result: `work_source_preserved:${source.state}`,
|
|
802
819
|
};
|
|
803
820
|
}
|
|
821
|
+
const semanticReady = staleWorkSemanticTerminalizationReady(current);
|
|
822
|
+
if (!semanticReady) {
|
|
823
|
+
return {
|
|
824
|
+
...candidate,
|
|
825
|
+
path: source.path ?? candidate.path,
|
|
826
|
+
safe: false,
|
|
827
|
+
reason: staleWorkCandidateReason(source, false),
|
|
828
|
+
sourceState: source.state,
|
|
829
|
+
disposition: 'semantic_completion_required' as const,
|
|
830
|
+
applied: false,
|
|
831
|
+
result: 'work_semantic_completion_required',
|
|
832
|
+
};
|
|
833
|
+
}
|
|
804
834
|
transitionWorkContractPhase({ controllerHome, repoId: repository.repoId }, current.workId, {
|
|
805
835
|
phase: 'cleanup',
|
|
806
836
|
status: 'cancelled',
|
|
807
837
|
state: 'skipped',
|
|
808
|
-
summary: 'Cancelled by explicit full maintenance
|
|
838
|
+
summary: 'Cancelled by explicit full maintenance after the Work had already reached cleanup with prior semantic phases satisfied and no unique live repository source remained; durable evidence retained.',
|
|
809
839
|
evidenceRefs: current.evidenceRefs,
|
|
810
840
|
});
|
|
811
841
|
return {
|
|
812
842
|
...candidate,
|
|
813
843
|
path: source.path ?? candidate.path,
|
|
814
844
|
safe: true,
|
|
815
|
-
reason: staleWorkCandidateReason(source),
|
|
845
|
+
reason: staleWorkCandidateReason(source, true),
|
|
816
846
|
sourceState: source.state,
|
|
817
847
|
disposition: undefined,
|
|
818
848
|
applied: true,
|
|
@@ -856,14 +886,20 @@ function scanStaleEditSessionCandidates(
|
|
|
856
886
|
.filter(({ ageMinutes }) => ageMinutes >= options.minAgeMinutes)
|
|
857
887
|
.sort((left, right) => right.ageMinutes - left.ageMinutes)
|
|
858
888
|
.slice(0, options.maxCandidates)
|
|
859
|
-
.
|
|
889
|
+
.flatMap(({ session, ageMinutes }) => {
|
|
860
890
|
const work = session.workId
|
|
861
891
|
? getWorkContract({ controllerHome, repoId: repository.repoId }, session.workId)
|
|
862
892
|
: undefined;
|
|
863
893
|
const terminalWork = Boolean(work && isTerminalWorkContractStatus(work.status));
|
|
894
|
+
if (work && !terminalWork && activeWorkAuthorityRefs(repository, controllerHome, work).length > 0) {
|
|
895
|
+
// The Edit Session inherits the live Work lifecycle authority. Reporting it
|
|
896
|
+
// as stale maintenance debt while that authority is active would duplicate
|
|
897
|
+
// ownership and can permanently block release readiness for valid long work.
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
864
900
|
const contractFreeDirect = !session.workId;
|
|
865
901
|
const safeToReconcile = terminalWork || contractFreeDirect;
|
|
866
|
-
return {
|
|
902
|
+
return [{
|
|
867
903
|
kind: 'stale_edit_session' as const,
|
|
868
904
|
id: session.sessionId,
|
|
869
905
|
status: session.status,
|
|
@@ -872,11 +908,11 @@ function scanStaleEditSessionCandidates(
|
|
|
872
908
|
? 'Edit Session belongs to a terminal WorkContract; cleanup may reconcile metadata only and must refuse unique uncommitted source changes.'
|
|
873
909
|
: contractFreeDirect
|
|
874
910
|
? 'Contract-free Direct Edit Session has no WorkContract by design; cleanup may reconcile metadata only and must refuse unique uncommitted source changes.'
|
|
875
|
-
: 'Edit Session WorkContract is missing or still nonterminal; active-session ownership must remain fenced.',
|
|
911
|
+
: 'Edit Session WorkContract is missing or still nonterminal without live lifecycle authority; active-session ownership must remain fenced.',
|
|
876
912
|
ageMinutes,
|
|
877
913
|
suggestedAction: 'full_maintenance_pass' as const,
|
|
878
914
|
ownershipStatus: safeToReconcile ? 'explicit' as const : 'unknown' as const,
|
|
879
|
-
};
|
|
915
|
+
}];
|
|
880
916
|
});
|
|
881
917
|
}
|
|
882
918
|
|
|
@@ -122,13 +122,40 @@ async function defaultConnectorEndpointProbe(endpoint: string): Promise<boolean>
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export async function waitForPackageConnectorEndpointReady(
|
|
126
|
+
endpoint: string,
|
|
127
|
+
options: {
|
|
128
|
+
timeoutMs?: number;
|
|
129
|
+
pollIntervalMs?: number;
|
|
130
|
+
probeEndpoint?: (endpoint: string) => Promise<boolean>;
|
|
131
|
+
wait?: (ms: number) => Promise<void>;
|
|
132
|
+
now?: () => number;
|
|
133
|
+
} = {},
|
|
134
|
+
): Promise<boolean> {
|
|
135
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? 15_000);
|
|
136
|
+
const pollIntervalMs = Math.max(10, options.pollIntervalMs ?? 100);
|
|
137
|
+
const probeEndpoint = options.probeEndpoint ?? defaultConnectorEndpointProbe;
|
|
138
|
+
const wait = options.wait ?? ((ms: number) => new Promise<void>((resolveWait) => setTimeout(resolveWait, ms)));
|
|
139
|
+
const now = options.now ?? Date.now;
|
|
140
|
+
const deadline = now() + timeoutMs;
|
|
141
|
+
do {
|
|
142
|
+
if (await probeEndpoint(endpoint)) return true;
|
|
143
|
+
if (now() >= deadline) return false;
|
|
144
|
+
await wait(Math.min(pollIntervalMs, Math.max(1, deadline - now())));
|
|
145
|
+
} while (now() <= deadline);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
125
149
|
export function packageConnectorLaunchSpec(input: { release: PackageConnectorReleaseBinding; controllerHome: string; endpoint: string; executable?: string }): { executable: string; args: string[]; environment: Record<string, string>; port: number } {
|
|
126
150
|
const parsed = new URL(input.endpoint);
|
|
127
151
|
const port = Number(parsed.port);
|
|
128
152
|
if (parsed.protocol !== 'http:' || parsed.hostname !== '127.0.0.1' || parsed.pathname !== '/mcp' || !Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
129
153
|
throw new Error('FORGE_PACKAGE_CONNECTOR_ENDPOINT_INVALID');
|
|
130
154
|
}
|
|
131
|
-
const executable = resolve(input.executable ?? process.execPath);
|
|
155
|
+
const executable = resolve(input.executable ?? process.env.FORGE_CONNECTOR_EXECUTABLE ?? process.execPath);
|
|
156
|
+
if (/^forge-recovery-(?:gateway|watchdog)$/i.test(basename(executable))) {
|
|
157
|
+
throw new Error('FORGE_PACKAGE_CONNECTOR_EXECUTABLE_INVALID');
|
|
158
|
+
}
|
|
132
159
|
const packageRoot = resolve(input.release.packageRoot);
|
|
133
160
|
const cliEntry = join(packageRoot, 'src', 'cli', 'index.ts');
|
|
134
161
|
const nodeLoader = join(packageRoot, 'src', 'runtime', 'shared', 'node-ts-loader.mjs');
|
|
@@ -168,6 +195,7 @@ export function packageConnectorServiceMatchesRelease(input: {
|
|
|
168
195
|
endpoint: string;
|
|
169
196
|
platform: NodeJS.Platform;
|
|
170
197
|
env?: NodeJS.ProcessEnv;
|
|
198
|
+
executable?: string;
|
|
171
199
|
}): boolean {
|
|
172
200
|
try {
|
|
173
201
|
if (
|
|
@@ -176,7 +204,12 @@ export function packageConnectorServiceMatchesRelease(input: {
|
|
|
176
204
|
|| resolve(input.authority.releaseRoot) !== resolve(input.release.releaseRoot)
|
|
177
205
|
|| resolve(input.authority.packageRoot) !== resolve(input.release.packageRoot)
|
|
178
206
|
) return false;
|
|
179
|
-
const launch = packageConnectorLaunchSpec({
|
|
207
|
+
const launch = packageConnectorLaunchSpec({
|
|
208
|
+
release: input.release,
|
|
209
|
+
controllerHome: input.controllerHome,
|
|
210
|
+
endpoint: input.endpoint,
|
|
211
|
+
executable: input.executable,
|
|
212
|
+
});
|
|
180
213
|
if (input.platform === 'darwin') {
|
|
181
214
|
const paths = packageConnectorServicePaths(input.controllerHome, input.env?.HOME);
|
|
182
215
|
if (input.authority.mode !== 'launchd' || !input.authority.servicePath) return false;
|
|
@@ -226,10 +259,15 @@ function startPortable(paths: PackageConnectorServicePaths, launch: ReturnType<t
|
|
|
226
259
|
} finally { closeSync(stdout); closeSync(stderr); }
|
|
227
260
|
}
|
|
228
261
|
|
|
229
|
-
export async function installPackageConnectorService(input: { release: PackageConnectorReleaseBinding; controllerHome: string; endpoint: string; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; forcePortable?: boolean }): Promise<PackageConnectorServiceResult> {
|
|
262
|
+
export async function installPackageConnectorService(input: { release: PackageConnectorReleaseBinding; controllerHome: string; endpoint: string; executable?: string; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; forcePortable?: boolean; probeEndpoint?: (endpoint: string) => Promise<boolean> }): Promise<PackageConnectorServiceResult> {
|
|
230
263
|
const paths = packageConnectorServicePaths(input.controllerHome, input.env?.HOME);
|
|
231
264
|
mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
|
|
232
|
-
const launch = packageConnectorLaunchSpec({
|
|
265
|
+
const launch = packageConnectorLaunchSpec({
|
|
266
|
+
release: input.release,
|
|
267
|
+
controllerHome: input.controllerHome,
|
|
268
|
+
endpoint: input.endpoint,
|
|
269
|
+
executable: input.executable,
|
|
270
|
+
});
|
|
233
271
|
const platform = input.platform ?? process.platform, env = input.env ?? process.env;
|
|
234
272
|
if (!input.forcePortable && platform === 'darwin') {
|
|
235
273
|
await retireConflictingForgeLaunchAgents({
|
|
@@ -243,6 +281,8 @@ export async function installPackageConnectorService(input: { release: PackageCo
|
|
|
243
281
|
installLaunchAgent(paths.sourcePlistPath, paths.label);
|
|
244
282
|
const result = await bootstrapLaunchAgentWithRetryV2({ label: paths.label, plistPath: paths.installedPlistPath });
|
|
245
283
|
if (!result.ok) throw new Error(`FORGE_PACKAGE_CONNECTOR_LAUNCHD_INSTALL_FAILED: ${result.diagnostics.join('; ')}`);
|
|
284
|
+
const endpointReady = await waitForPackageConnectorEndpointReady(input.endpoint, { probeEndpoint: input.probeEndpoint });
|
|
285
|
+
if (!endpointReady) throw new Error(`FORGE_PACKAGE_CONNECTOR_ENDPOINT_NOT_READY: ${input.endpoint}`);
|
|
246
286
|
const installed = { endpoint: input.endpoint, mode: 'launchd' as const, persistent: true, servicePath: paths.installedPlistPath, releaseId: input.release.releaseId, releaseRoot: input.release.releaseRoot };
|
|
247
287
|
writePackageConnectorServiceAuthority({ release: input.release, controllerHome: input.controllerHome, result: installed });
|
|
248
288
|
return installed;
|
|
@@ -264,6 +304,7 @@ export async function ensurePackageConnectorService(input: {
|
|
|
264
304
|
release: PackageConnectorReleaseBinding;
|
|
265
305
|
controllerHome: string;
|
|
266
306
|
endpoint: string;
|
|
307
|
+
executable?: string;
|
|
267
308
|
platform?: NodeJS.Platform;
|
|
268
309
|
env?: NodeJS.ProcessEnv;
|
|
269
310
|
forcePortable?: boolean;
|
|
@@ -296,6 +337,7 @@ export async function ensurePackageConnectorService(input: {
|
|
|
296
337
|
endpoint: input.endpoint,
|
|
297
338
|
platform,
|
|
298
339
|
env: input.env,
|
|
340
|
+
executable: input.executable,
|
|
299
341
|
})
|
|
300
342
|
&& await (input.probeEndpoint ?? defaultConnectorEndpointProbe)(input.endpoint)
|
|
301
343
|
) {
|