@moretea-labs/forge 1.5.0-rc.1 → 1.5.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 +9 -11
- package/CHANGELOG.md +27 -1
- package/CLAUDE.md +8 -10
- package/DESIGN.md +278 -0
- package/README.md +70 -90
- package/README.zh-CN.md +52 -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/skill-version.json +2 -2
- 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 +67 -176
- 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 +58 -35
- package/docs/tutorials/01-install-and-start.zh-CN.md +58 -35
- package/docs/tutorials/02-connect-chatgpt.md +118 -46
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +119 -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 +213 -70
- package/src/cli/commands/init.ts +5 -6
- package/src/cli/commands/mcp.ts +31 -5
- package/src/cli/commands/runtime.ts +61 -8
- package/src/cli/commands/setup-profile.ts +472 -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-assets.ts +17 -0
- 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 +45 -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/auth.ts +2 -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 +220 -276
- 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 +51 -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-connector-service.ts +136 -0
- package/src/runtime/root/package-runtime-release.ts +161 -0
- package/src/runtime/root/package-runtime-service.ts +185 -0
- package/src/runtime/root/release-materialize.ts +19 -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,55 +1,84 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
listControlPlaneRecords,
|
|
7
|
+
mutateControlPlaneRecord,
|
|
8
|
+
readOrImportControlPlaneRecord,
|
|
9
|
+
} from '../../control-plane/persistence/sqlite-store';
|
|
6
10
|
import { appendRuntimeEvent } from '../../evidence/event-ledger';
|
|
7
|
-
import { readJsonFile, sanitizeFileComponent
|
|
11
|
+
import { readJsonFile, sanitizeFileComponent } from '../../shared/json-files';
|
|
8
12
|
import type { CandidateFinding, RecordCandidateFindingInput } from './types';
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
recent: Array<{ findingId: string; semanticKey: string; status: CandidateFinding['status']; lastSeenAt: string }>;
|
|
15
|
-
}
|
|
14
|
+
const FINDING_NAMESPACE = 'candidate_finding';
|
|
15
|
+
const FINDING_SCHEMA_VERSION = 1;
|
|
16
|
+
const MAX_EVIDENCE = 100;
|
|
17
|
+
const MAX_FINDINGS = 5_000;
|
|
16
18
|
|
|
17
|
-
function
|
|
19
|
+
function legacyRoot(controllerHome: string, repoId: string): string {
|
|
18
20
|
return join(repositoryControllerRoot(controllerHome, repoId), 'candidate-findings');
|
|
19
21
|
}
|
|
20
|
-
function
|
|
21
|
-
return join(
|
|
22
|
+
function legacyRecordPath(controllerHome: string, repoId: string, findingId: string): string {
|
|
23
|
+
return join(legacyRoot(controllerHome, repoId), 'records', `${sanitizeFileComponent(findingId)}.json`);
|
|
24
|
+
}
|
|
25
|
+
function legacySemanticPath(controllerHome: string, repoId: string, semanticKey: string): string {
|
|
26
|
+
return join(legacyRoot(controllerHome, repoId), 'indexes', 'semantic', `${findingKey(semanticKey)}.json`);
|
|
27
|
+
}
|
|
28
|
+
function findingKey(semanticKey: string): string {
|
|
29
|
+
return createHash('sha256').update(semanticKey).digest('hex');
|
|
22
30
|
}
|
|
23
|
-
function
|
|
24
|
-
return
|
|
31
|
+
function legacyFinding(controllerHome: string, repoId: string, findingId: string): CandidateFinding | undefined {
|
|
32
|
+
try { return readJsonFile<CandidateFinding>(legacyRecordPath(controllerHome, repoId, findingId)); } catch { return undefined; }
|
|
25
33
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
34
|
+
function legacyFindingForSemanticKey(controllerHome: string, repoId: string, semanticKey: string): CandidateFinding | undefined {
|
|
35
|
+
try {
|
|
36
|
+
const reference = readJsonFile<{ findingId?: string }>(legacySemanticPath(controllerHome, repoId, semanticKey));
|
|
37
|
+
return typeof reference.findingId === 'string' ? legacyFinding(controllerHome, repoId, reference.findingId) : undefined;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
28
41
|
}
|
|
29
|
-
function
|
|
30
|
-
return
|
|
42
|
+
function records(controllerHome: string, repoId: string): CandidateFinding[] {
|
|
43
|
+
return listControlPlaneRecords<CandidateFinding>(controllerHome, {
|
|
44
|
+
namespace: FINDING_NAMESPACE,
|
|
45
|
+
scope: repoId,
|
|
46
|
+
limit: MAX_FINDINGS,
|
|
47
|
+
}).map((record) => record.value);
|
|
31
48
|
}
|
|
32
|
-
function
|
|
33
|
-
return
|
|
49
|
+
function importLegacyFinding(controllerHome: string, repoId: string, finding: CandidateFinding): CandidateFinding {
|
|
50
|
+
return readOrImportControlPlaneRecord<CandidateFinding>(controllerHome, {
|
|
51
|
+
namespace: FINDING_NAMESPACE,
|
|
52
|
+
scope: repoId,
|
|
53
|
+
key: findingKey(finding.semanticKey),
|
|
54
|
+
schemaVersion: FINDING_SCHEMA_VERSION,
|
|
55
|
+
readLegacy: () => finding,
|
|
56
|
+
})!.value;
|
|
34
57
|
}
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
function importLegacyFindings(controllerHome: string, repoId: string): CandidateFinding[] {
|
|
59
|
+
const directory = join(legacyRoot(controllerHome, repoId), 'records');
|
|
60
|
+
try {
|
|
61
|
+
for (const name of readdirSync(directory).filter((entry) => entry.endsWith('.json')).slice(0, MAX_FINDINGS)) {
|
|
62
|
+
const finding = legacyFinding(controllerHome, repoId, name.slice(0, -'.json'.length));
|
|
63
|
+
if (finding?.repoId === repoId && finding.semanticKey) importLegacyFinding(controllerHome, repoId, finding);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// No legacy records is the normal v2 path.
|
|
41
67
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
return records(controllerHome, repoId);
|
|
69
|
+
}
|
|
70
|
+
function existingFinding(controllerHome: string, repoId: string, findingId: string): CandidateFinding | undefined {
|
|
71
|
+
return records(controllerHome, repoId).find((finding) => finding.findingId === findingId)
|
|
72
|
+
?? (() => {
|
|
73
|
+
const legacy = legacyFinding(controllerHome, repoId, findingId);
|
|
74
|
+
return legacy?.repoId === repoId ? importLegacyFinding(controllerHome, repoId, legacy) : undefined;
|
|
75
|
+
})();
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
export function getCandidateFinding(controllerHome: string, repoId: string, findingId: string): CandidateFinding {
|
|
52
|
-
|
|
79
|
+
const finding = existingFinding(controllerHome, repoId, findingId);
|
|
80
|
+
if (!finding) throw new Error(`CANDIDATE_FINDING_NOT_FOUND: ${findingId}`);
|
|
81
|
+
return finding;
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
export function recordCandidateFinding(controllerHome: string, input: RecordCandidateFindingInput): CandidateFinding {
|
|
@@ -59,31 +88,38 @@ export function recordCandidateFinding(controllerHome: string, input: RecordCand
|
|
|
59
88
|
if (!semanticKey) throw new Error('CANDIDATE_SEMANTIC_KEY_REQUIRED');
|
|
60
89
|
if (!title) throw new Error('CANDIDATE_TITLE_REQUIRED');
|
|
61
90
|
if (!requestId) throw new Error('CANDIDATE_REQUEST_ID_REQUIRED');
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
const timestamp = input.evidence?.observedAt ?? new Date().toISOString();
|
|
92
|
+
const record = mutateControlPlaneRecord<CandidateFinding>(controllerHome, {
|
|
93
|
+
namespace: FINDING_NAMESPACE,
|
|
94
|
+
scope: input.repoId,
|
|
95
|
+
key: findingKey(semanticKey),
|
|
96
|
+
schemaVersion: FINDING_SCHEMA_VERSION,
|
|
97
|
+
action: 'candidate_finding_observed',
|
|
98
|
+
readLegacy: () => legacyFindingForSemanticKey(controllerHome, input.repoId, semanticKey),
|
|
99
|
+
mutate: (current) => {
|
|
100
|
+
const previous = current?.value;
|
|
70
101
|
const evidence = input.evidence
|
|
71
|
-
? [...
|
|
72
|
-
:
|
|
73
|
-
|
|
74
|
-
...
|
|
75
|
-
revision:
|
|
102
|
+
? [...(previous?.evidence ?? []), { ...input.evidence, observedAt: timestamp }].slice(-MAX_EVIDENCE)
|
|
103
|
+
: previous?.evidence ?? [];
|
|
104
|
+
return previous ? {
|
|
105
|
+
...previous,
|
|
106
|
+
revision: previous.revision + 1,
|
|
76
107
|
title,
|
|
77
|
-
summary: input.summary ??
|
|
78
|
-
severity: input.severity ??
|
|
79
|
-
status:
|
|
80
|
-
observationCount:
|
|
108
|
+
summary: input.summary ?? previous.summary,
|
|
109
|
+
severity: input.severity ?? previous.severity,
|
|
110
|
+
status: previous.status === 'dismissed' ? 'candidate' : previous.status,
|
|
111
|
+
observationCount: previous.observationCount + 1,
|
|
81
112
|
evidence,
|
|
82
113
|
lastSeenAt: timestamp,
|
|
83
|
-
dismissedReason:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
dismissedReason: previous.status === 'dismissed' ? undefined : previous.dismissedReason,
|
|
115
|
+
requirementId: input.requirementId ?? previous.requirementId,
|
|
116
|
+
kind: input.kind ?? previous.kind,
|
|
117
|
+
sourceRepoId: input.sourceRepoId ?? previous.sourceRepoId,
|
|
118
|
+
sourceWorkId: input.sourceWorkId ?? previous.sourceWorkId,
|
|
119
|
+
sourceProcessId: input.sourceProcessId ?? previous.sourceProcessId,
|
|
120
|
+
sourcePluginId: input.sourcePluginId ?? previous.sourcePluginId,
|
|
121
|
+
externalRefs: input.externalRefs ?? previous.externalRefs,
|
|
122
|
+
} : {
|
|
87
123
|
schemaVersion: 1,
|
|
88
124
|
revision: 1,
|
|
89
125
|
findingId: `FIND-${Date.now()}-${randomUUID().slice(0, 8)}`,
|
|
@@ -94,25 +130,30 @@ export function recordCandidateFinding(controllerHome: string, input: RecordCand
|
|
|
94
130
|
severity: input.severity ?? 'medium',
|
|
95
131
|
status: 'candidate',
|
|
96
132
|
observationCount: 1,
|
|
97
|
-
evidence
|
|
133
|
+
evidence,
|
|
98
134
|
firstSeenAt: timestamp,
|
|
99
135
|
lastSeenAt: timestamp,
|
|
136
|
+
requirementId: input.requirementId,
|
|
137
|
+
kind: input.kind,
|
|
138
|
+
sourceRepoId: input.sourceRepoId,
|
|
139
|
+
sourceWorkId: input.sourceWorkId,
|
|
140
|
+
sourceProcessId: input.sourceProcessId,
|
|
141
|
+
sourcePluginId: input.sourcePluginId,
|
|
142
|
+
externalRefs: input.externalRefs,
|
|
100
143
|
};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return finding;
|
|
115
|
-
}, 10_000);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const finding = record.value;
|
|
147
|
+
appendRuntimeEvent(controllerHome, {
|
|
148
|
+
repoId: finding.repoId,
|
|
149
|
+
entityType: 'candidate-finding',
|
|
150
|
+
entityId: finding.findingId,
|
|
151
|
+
eventType: finding.observationCount === 1 ? 'candidate_finding_created' : 'candidate_finding_observed',
|
|
152
|
+
requestId,
|
|
153
|
+
revision: finding.revision,
|
|
154
|
+
data: { semanticKey, observationCount: finding.observationCount, severity: finding.severity },
|
|
155
|
+
});
|
|
156
|
+
return finding;
|
|
116
157
|
}
|
|
117
158
|
|
|
118
159
|
export function updateCandidateFinding(
|
|
@@ -123,43 +164,41 @@ export function updateCandidateFinding(
|
|
|
123
164
|
requestId: string,
|
|
124
165
|
eventType: string,
|
|
125
166
|
): CandidateFinding {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
167
|
+
const existing = getCandidateFinding(controllerHome, repoId, findingId);
|
|
168
|
+
const record = mutateControlPlaneRecord<CandidateFinding>(controllerHome, {
|
|
169
|
+
namespace: FINDING_NAMESPACE,
|
|
170
|
+
scope: repoId,
|
|
171
|
+
key: findingKey(existing.semanticKey),
|
|
172
|
+
schemaVersion: FINDING_SCHEMA_VERSION,
|
|
173
|
+
action: eventType,
|
|
174
|
+
mutate: (current) => {
|
|
175
|
+
const previous = current?.value ?? existing;
|
|
176
|
+
const next = updater(structuredClone(previous));
|
|
177
|
+
if (next.findingId !== previous.findingId || next.repoId !== previous.repoId || next.semanticKey !== previous.semanticKey) {
|
|
178
|
+
throw new Error('CANDIDATE_IDENTITY_IMMUTABLE');
|
|
179
|
+
}
|
|
180
|
+
return { ...next, revision: previous.revision + 1, lastSeenAt: new Date().toISOString() };
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const finding = record.value;
|
|
184
|
+
appendRuntimeEvent(controllerHome, {
|
|
185
|
+
repoId,
|
|
186
|
+
entityType: 'candidate-finding',
|
|
187
|
+
entityId: findingId,
|
|
188
|
+
eventType,
|
|
189
|
+
requestId,
|
|
190
|
+
revision: finding.revision,
|
|
191
|
+
data: { status: finding.status, promotedJobId: finding.promotedJobId },
|
|
192
|
+
});
|
|
193
|
+
return finding;
|
|
147
194
|
}
|
|
148
195
|
|
|
149
196
|
export function listCandidateFindings(controllerHome: string, repoId: string, options: { includeTerminal?: boolean; limit?: number } = {}): CandidateFinding[] {
|
|
150
197
|
const bounded = Math.max(1, Math.min(options.limit ?? 100, 1000));
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
return readdirSync(join(root(controllerHome, repoId), 'records'))
|
|
159
|
-
.filter((name) => name.endsWith('.json'))
|
|
160
|
-
.map((name) => readJsonFile<CandidateFinding>(join(root(controllerHome, repoId), 'records', name)))
|
|
161
|
-
.filter((finding) => options.includeTerminal || finding.status === 'candidate')
|
|
162
|
-
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))
|
|
163
|
-
.slice(0, bounded);
|
|
164
|
-
} catch { return []; }
|
|
198
|
+
const stored = records(controllerHome, repoId);
|
|
199
|
+
const all = stored.length > 0 ? stored : importLegacyFindings(controllerHome, repoId);
|
|
200
|
+
return all
|
|
201
|
+
.filter((finding) => options.includeTerminal || finding.status === 'candidate')
|
|
202
|
+
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))
|
|
203
|
+
.slice(0, bounded);
|
|
165
204
|
}
|
|
@@ -7,6 +7,13 @@ export interface CandidateFindingEvidence {
|
|
|
7
7
|
details?: Record<string, unknown>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface CandidateFindingExternalRef {
|
|
11
|
+
kind: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
number?: number;
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export interface CandidateFinding {
|
|
11
18
|
schemaVersion: 1;
|
|
12
19
|
revision: number;
|
|
@@ -23,6 +30,14 @@ export interface CandidateFinding {
|
|
|
23
30
|
lastSeenAt: string;
|
|
24
31
|
promotedJobId?: string;
|
|
25
32
|
dismissedReason?: string;
|
|
33
|
+
/** Optional Requirement/operational provenance; repoId remains the owner. */
|
|
34
|
+
requirementId?: string;
|
|
35
|
+
kind?: 'defect' | 'performance' | 'reliability' | 'usability';
|
|
36
|
+
sourceRepoId?: string;
|
|
37
|
+
sourceWorkId?: string;
|
|
38
|
+
sourceProcessId?: string;
|
|
39
|
+
sourcePluginId?: string;
|
|
40
|
+
externalRefs?: CandidateFindingExternalRef[];
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
export interface RecordCandidateFindingInput {
|
|
@@ -32,5 +47,12 @@ export interface RecordCandidateFindingInput {
|
|
|
32
47
|
summary?: string;
|
|
33
48
|
severity?: CandidateFinding['severity'];
|
|
34
49
|
evidence?: Omit<CandidateFindingEvidence, 'observedAt'> & { observedAt?: string };
|
|
50
|
+
requirementId?: string;
|
|
51
|
+
kind?: CandidateFinding['kind'];
|
|
52
|
+
sourceRepoId?: string;
|
|
53
|
+
sourceWorkId?: string;
|
|
54
|
+
sourceProcessId?: string;
|
|
55
|
+
sourcePluginId?: string;
|
|
56
|
+
externalRefs?: CandidateFindingExternalRef[];
|
|
35
57
|
requestId: string;
|
|
36
58
|
}
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
type RuntimeMaintenanceActionId,
|
|
11
11
|
} from '../../recovery/maintenance-executor';
|
|
12
12
|
import { listCandidateFindings } from '../findings/store';
|
|
13
|
+
import { getControllerSession, getWorkContract, isTerminalWorkContractStatus, listHandoffItems } from '../../control-plane/facade';
|
|
14
|
+
import { launchSuperController } from '../../control-plane/launcher/thin-launcher';
|
|
15
|
+
import { getExternalControllerLaunchReservation } from '../../control-plane/launcher/launch-reservation-store';
|
|
13
16
|
import {
|
|
14
17
|
getOccurrence,
|
|
15
18
|
getSchedule,
|
|
@@ -31,11 +34,13 @@ import type {
|
|
|
31
34
|
|
|
32
35
|
const execFileAsync = promisify(execFile);
|
|
33
36
|
const LIVE_MAINTENANCE_OPERATION = 'runtime_maintenance_apply';
|
|
37
|
+
const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
|
|
34
38
|
|
|
35
39
|
function isDeterministicSchedule(schedule: RepositorySchedule): boolean {
|
|
36
40
|
// Kernel may execute only fully deterministic, allowlisted schedule operations.
|
|
37
41
|
// Semantic / model-backed schedules remain external-controller handoffs.
|
|
38
|
-
return schedule.action.operation === LIVE_MAINTENANCE_OPERATION
|
|
42
|
+
return schedule.action.operation === LIVE_MAINTENANCE_OPERATION
|
|
43
|
+
|| schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
function normalizedWindow(minutes: number, at = Date.now()): string {
|
|
@@ -229,9 +234,34 @@ async function triggerDue(
|
|
|
229
234
|
}
|
|
230
235
|
}
|
|
231
236
|
|
|
237
|
+
function continuationWorkId(schedule: RepositorySchedule): string | undefined {
|
|
238
|
+
if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION) return undefined;
|
|
239
|
+
const value = schedule.action.arguments?.work_id;
|
|
240
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function executionJobBelongsToWork(job: ReturnType<typeof listExecutionJobs>[number], workId: string): boolean {
|
|
244
|
+
if (job.resourceClaims.some((claim) => claim.workId === workId)) return true;
|
|
245
|
+
const payload = job.payload as Record<string, unknown>;
|
|
246
|
+
const args = payload.arguments && typeof payload.arguments === 'object' && !Array.isArray(payload.arguments)
|
|
247
|
+
? payload.arguments as Record<string, unknown>
|
|
248
|
+
: undefined;
|
|
249
|
+
return [payload.workId, payload.work_id, args?.workId, args?.work_id].some((value) => value === workId);
|
|
250
|
+
}
|
|
251
|
+
|
|
232
252
|
async function stopReason(controllerHome: string, schedule: RepositorySchedule): Promise<string | undefined> {
|
|
233
253
|
const projection = readRepositoryProjection(controllerHome, schedule.repoId);
|
|
234
|
-
|
|
254
|
+
const workId = continuationWorkId(schedule);
|
|
255
|
+
const work = workId ? getWorkContract({ controllerHome, repoId: schedule.repoId }, workId) : undefined;
|
|
256
|
+
if (schedule.stopConditions.includes('human_review_required')) {
|
|
257
|
+
if (workId) {
|
|
258
|
+
const activeHandoff = listHandoffItems({ controllerHome, repoId: schedule.repoId, status: 'active', limit: 100 })
|
|
259
|
+
.find((item) => item.workId === workId);
|
|
260
|
+
if (activeHandoff) return `Work ${workId} has active Handoff ${activeHandoff.id} requiring review.`;
|
|
261
|
+
} else if (projection.currentAttention.length > 0) {
|
|
262
|
+
return 'Repository has jobs requiring human attention.';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
235
265
|
if (schedule.stopConditions.includes('release_ready') && projection.releaseFrozen) return 'Repository is in release freeze.';
|
|
236
266
|
if (schedule.stopConditions.includes('external_blocker')) {
|
|
237
267
|
try {
|
|
@@ -242,6 +272,10 @@ async function stopReason(controllerHome: string, schedule: RepositorySchedule):
|
|
|
242
272
|
}
|
|
243
273
|
const recentInfrastructureFailure = listExecutionJobs(controllerHome, schedule.repoId, 20).find((job) => {
|
|
244
274
|
if (!['failed', 'timed_out', 'orphaned'].includes(job.status) || !job.error) return false;
|
|
275
|
+
if (workId) {
|
|
276
|
+
if (!work || Date.parse(job.createdAt) < Date.parse(work.createdAt)) return false;
|
|
277
|
+
if (!executionJobBelongsToWork(job, workId)) return false;
|
|
278
|
+
}
|
|
245
279
|
if (job.error.retryable) return true;
|
|
246
280
|
return /(?:network|connection|upstream|external|remote|github|tunnel|502|503|timeout)/i.test(`${job.error.code} ${job.error.message}`);
|
|
247
281
|
});
|
|
@@ -513,6 +547,102 @@ export async function evaluateSchedule(
|
|
|
513
547
|
}
|
|
514
548
|
|
|
515
549
|
const args = schedule.action.arguments ?? {};
|
|
550
|
+
if (schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION) {
|
|
551
|
+
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
552
|
+
const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
|
|
553
|
+
if (!workId) {
|
|
554
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_WORK_ID_REQUIRED');
|
|
555
|
+
}
|
|
556
|
+
if (!['chatgpt', 'codex', 'claude', 'grok'].includes(controllerType)) {
|
|
557
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', 'EXTERNAL_CONTROLLER_WAKE_TYPE_INVALID');
|
|
558
|
+
}
|
|
559
|
+
const workStore = { controllerHome, repoId: schedule.repoId };
|
|
560
|
+
const work = getWorkContract(workStore, workId);
|
|
561
|
+
if (!work) {
|
|
562
|
+
saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} no longer exists.`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
563
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'operation_blocked', 'skipped', `EXTERNAL_CONTROLLER_WAKE_WORK_NOT_FOUND:${workId}`);
|
|
564
|
+
}
|
|
565
|
+
if (isTerminalWorkContractStatus(work.status)) {
|
|
566
|
+
saveSchedule(controllerHome, { ...schedule, enabled: false, pausedReason: `Work ${workId} is terminal (${work.status}).`, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
567
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} is terminal (${work.status}); automatic continuation stopped.`);
|
|
568
|
+
}
|
|
569
|
+
const existingOwner = getControllerSession(workStore, workId);
|
|
570
|
+
if (existingOwner) {
|
|
571
|
+
saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
572
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has an active Controller ${existingOwner.controllerId}.`);
|
|
573
|
+
}
|
|
574
|
+
const launchReservation = getExternalControllerLaunchReservation(workStore, workId);
|
|
575
|
+
if (launchReservation) {
|
|
576
|
+
saveSchedule(controllerHome, { ...schedule, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
577
|
+
return decideOccurrence(controllerHome, schedule, occurrence, 'nothing_to_do', 'skipped', `Work ${workId} already has a pending external Controller launch ${launchReservation.reservationId}.`);
|
|
578
|
+
}
|
|
579
|
+
const wakeDecision = decideOccurrence(
|
|
580
|
+
controllerHome,
|
|
581
|
+
schedule,
|
|
582
|
+
occurrence,
|
|
583
|
+
'execute',
|
|
584
|
+
'running',
|
|
585
|
+
'Deterministic external Controller wake is starting through Thin Launcher.',
|
|
586
|
+
occurrenceDecisionEvidence({ operation: schedule.action.operation, workId, controllerType }),
|
|
587
|
+
);
|
|
588
|
+
try {
|
|
589
|
+
const repository = getRepository(schedule.repoId, controllerHome, { includeRemoved: true });
|
|
590
|
+
const launched = launchSuperController({
|
|
591
|
+
work: { controllerHome, repoId: schedule.repoId },
|
|
592
|
+
handoff: { controllerHome, repoId: schedule.repoId },
|
|
593
|
+
}, {
|
|
594
|
+
controllerType: controllerType as 'chatgpt' | 'codex' | 'claude' | 'grok',
|
|
595
|
+
executable: typeof args.executable === 'string' ? args.executable : undefined,
|
|
596
|
+
args: Array.isArray(args.launch_args) ? args.launch_args.map(String) : [],
|
|
597
|
+
workId,
|
|
598
|
+
handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
|
|
599
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
600
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
601
|
+
launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
602
|
+
continuationPrompt: typeof args.continuation_prompt === 'string'
|
|
603
|
+
? args.continuation_prompt
|
|
604
|
+
: `Scheduled continuation ${occurrence.occurrenceId} from ${schedule.scheduleId}. Read current Forge state and continue only the bounded Work objective.`,
|
|
605
|
+
cwd: repository.canonicalRoot,
|
|
606
|
+
});
|
|
607
|
+
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
608
|
+
saveSchedule(controllerHome, {
|
|
609
|
+
...latest,
|
|
610
|
+
lastTriggeredAt: timestamp,
|
|
611
|
+
lastOccurrenceId: occurrenceId,
|
|
612
|
+
consecutiveFailures: 0,
|
|
613
|
+
nextEligibleAt: undefined,
|
|
614
|
+
pausedReason: undefined,
|
|
615
|
+
});
|
|
616
|
+
return saveOccurrence(controllerHome, {
|
|
617
|
+
...wakeDecision,
|
|
618
|
+
status: 'succeeded',
|
|
619
|
+
reason: `External Controller wake started ${launched.controllerType} pid=${String(launched.pid ?? 'unknown')} for Work ${workId}.`,
|
|
620
|
+
});
|
|
621
|
+
} catch (error) {
|
|
622
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
623
|
+
const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
624
|
+
outcome: 'failed',
|
|
625
|
+
decision: 'execute',
|
|
626
|
+
reason,
|
|
627
|
+
handoff: {
|
|
628
|
+
title: `External Controller wake ${occurrence.occurrenceId} failed`,
|
|
629
|
+
summary: 'Forge recorded the schedule trigger but could not wake the configured external Controller.',
|
|
630
|
+
reason,
|
|
631
|
+
creationReason: 'repeated_infrastructure_failure',
|
|
632
|
+
blockingDecision: 'Repair the external Controller/browser launch path or update the saved session reference.',
|
|
633
|
+
recommendedDecision: 'Inspect launcher/browser readiness, then retrigger this bounded continuation.',
|
|
634
|
+
recommendedPrompt: `Resume Work ${workId} manually, inspect failed wake occurrence ${occurrence.occurrenceId}, and repair the configured external Controller launch path before unattended continuation resumes.`,
|
|
635
|
+
statusSummary: 'Scheduled external Controller wake failed.',
|
|
636
|
+
blockedBy: ['external_controller_wake_failed'],
|
|
637
|
+
attemptedActions: [`schedule:${schedule.scheduleId}`, `work:${workId}`, `controller:${controllerType}`],
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
const latest = getSchedule(controllerHome, schedule.repoId, schedule.scheduleId);
|
|
641
|
+
saveSchedule(controllerHome, { ...latest, lastTriggeredAt: timestamp, lastOccurrenceId: occurrenceId });
|
|
642
|
+
return failed.occurrence ?? saveOccurrence(controllerHome, { ...wakeDecision, status: 'failed', reason });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
516
646
|
const actionIdRaw = typeof args.action_id === 'string'
|
|
517
647
|
? args.action_id
|
|
518
648
|
: typeof args.actionId === 'string'
|