@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
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getWorkContract,
|
|
3
|
+
transitionWorkContractPhase,
|
|
4
|
+
updateWorkContract,
|
|
5
|
+
} from '../facade/work-contract-store';
|
|
6
|
+
import {
|
|
7
|
+
listValidatingWorkHandles,
|
|
8
|
+
transitionWorkHandle,
|
|
9
|
+
type WorkHandleState,
|
|
10
|
+
} from './work-handle-store';
|
|
11
|
+
import { processCheckCompletionReceipt } from '../../execution/process-runtime/check-receipt';
|
|
12
|
+
import { processCheckSemanticScopeKey } from '../../execution/process-runtime/check-facade';
|
|
13
|
+
import { getProcessRecord } from '../../execution/process-runtime/store';
|
|
14
|
+
import { controllerCheckExecutionIdentity } from '../../../cli/controller/check-runner';
|
|
15
|
+
|
|
16
|
+
export type WorkValidationOutcome =
|
|
17
|
+
| 'not_validating'
|
|
18
|
+
| 'running'
|
|
19
|
+
| 'passed'
|
|
20
|
+
| 'failed'
|
|
21
|
+
| 'infrastructure_failure';
|
|
22
|
+
|
|
23
|
+
export interface WorkValidationReconciliation {
|
|
24
|
+
handle: WorkHandleState;
|
|
25
|
+
changed: boolean;
|
|
26
|
+
outcome: WorkValidationOutcome;
|
|
27
|
+
summary?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hasCurrentWorkValidationAuthority(input: {
|
|
31
|
+
finalizationValidation: WorkHandleState['finalization']['validation'];
|
|
32
|
+
validatedInputFingerprint?: string;
|
|
33
|
+
evidenceState: string;
|
|
34
|
+
expectedFingerprint: string;
|
|
35
|
+
}): boolean {
|
|
36
|
+
return input.finalizationValidation === 'done'
|
|
37
|
+
&& input.evidenceState === 'valid'
|
|
38
|
+
&& input.validatedInputFingerprint === input.expectedFingerprint;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A new validation run invalidates delivery authority without deleting or
|
|
43
|
+
* rewriting the prior receipts. `stale` is the only valid transition from an
|
|
44
|
+
* already-valid evidence set; all other retryable states converge through
|
|
45
|
+
* `partial`.
|
|
46
|
+
*/
|
|
47
|
+
export function markWorkValidationPending(controllerHome: string, handle: WorkHandleState): void {
|
|
48
|
+
const contractId = handle.workContractId;
|
|
49
|
+
if (!contractId) return;
|
|
50
|
+
const options = { controllerHome, repoId: handle.repositoryId };
|
|
51
|
+
const contract = getWorkContract(options, contractId);
|
|
52
|
+
if (!contract || contract.completionReceipt) return;
|
|
53
|
+
const evidenceState = contract.evidenceState === 'valid' || contract.evidenceState === 'stale'
|
|
54
|
+
? 'stale'
|
|
55
|
+
: 'partial';
|
|
56
|
+
if (contract.evidenceState !== evidenceState) updateWorkContract(options, contractId, { evidenceState });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Project durable validation evidence into the Work-owned contract lifecycle.
|
|
61
|
+
* Process and Check records contribute evidence only; they never become a
|
|
62
|
+
* second completion or lifecycle authority.
|
|
63
|
+
*/
|
|
64
|
+
export function projectWorkValidationOutcome(
|
|
65
|
+
controllerHome: string,
|
|
66
|
+
handle: WorkHandleState,
|
|
67
|
+
outcome: Exclude<WorkValidationOutcome, 'not_validating' | 'running'>,
|
|
68
|
+
summary?: string,
|
|
69
|
+
): void {
|
|
70
|
+
const contractId = handle.workContractId;
|
|
71
|
+
if (!contractId) return;
|
|
72
|
+
const options = { controllerHome, repoId: handle.repositoryId };
|
|
73
|
+
const contract = getWorkContract(options, contractId);
|
|
74
|
+
if (!contract || contract.completionReceipt) return;
|
|
75
|
+
|
|
76
|
+
if (outcome === 'passed') {
|
|
77
|
+
transitionWorkContractPhase(options, contractId, {
|
|
78
|
+
phase: 'delivery',
|
|
79
|
+
status: 'running',
|
|
80
|
+
state: 'active',
|
|
81
|
+
summary: summary ?? 'All requested validation receipts passed; delivery is next.',
|
|
82
|
+
evidenceRefs: contract.evidenceRefs,
|
|
83
|
+
});
|
|
84
|
+
updateWorkContract(options, contractId, { evidenceState: 'valid' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (outcome === 'failed') {
|
|
89
|
+
transitionWorkContractPhase(options, contractId, {
|
|
90
|
+
phase: 'cleanup',
|
|
91
|
+
status: 'failed',
|
|
92
|
+
state: 'failed',
|
|
93
|
+
summary: summary ?? 'A requested validation check failed; terminal cleanup is next.',
|
|
94
|
+
evidenceRefs: contract.evidenceRefs,
|
|
95
|
+
});
|
|
96
|
+
updateWorkContract(options, contractId, { evidenceState: 'failed' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
transitionWorkContractPhase(options, contractId, {
|
|
101
|
+
phase: 'cleanup',
|
|
102
|
+
status: 'failed',
|
|
103
|
+
state: 'failed',
|
|
104
|
+
summary: summary ?? 'Validation infrastructure failed; terminal cleanup is required without treating this as an acceptance failure.',
|
|
105
|
+
evidenceRefs: contract.evidenceRefs,
|
|
106
|
+
});
|
|
107
|
+
updateWorkContract(options, contractId, {
|
|
108
|
+
evidenceState: contract.evidenceState === 'valid' || contract.evidenceState === 'stale' ? 'stale' : 'partial',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function settleInfrastructureFailure(
|
|
113
|
+
controllerHome: string,
|
|
114
|
+
handle: WorkHandleState,
|
|
115
|
+
summary: string,
|
|
116
|
+
): WorkValidationReconciliation {
|
|
117
|
+
const next = transitionWorkHandle(controllerHome, handle, 'failed', {
|
|
118
|
+
finalization: { ...handle.finalization, validation: 'failed', lastError: summary },
|
|
119
|
+
validationRun: undefined,
|
|
120
|
+
failureReason: summary,
|
|
121
|
+
});
|
|
122
|
+
projectWorkValidationOutcome(controllerHome, next, 'infrastructure_failure', summary);
|
|
123
|
+
return { handle: next, changed: true, outcome: 'infrastructure_failure', summary };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Converge a persisted Work validation run from durable Process receipts.
|
|
128
|
+
*
|
|
129
|
+
* No command is launched here. Missing/running bindings stay pending; timeout,
|
|
130
|
+
* cancellation, missing process state, or malformed receipt makes the Work
|
|
131
|
+
* terminally failed for cleanup without classifying it as an acceptance failure.
|
|
132
|
+
*/
|
|
133
|
+
export function reconcileWorkValidation(
|
|
134
|
+
controllerHome: string,
|
|
135
|
+
handle: WorkHandleState,
|
|
136
|
+
): WorkValidationReconciliation {
|
|
137
|
+
const run = handle.validationRun;
|
|
138
|
+
if (handle.state !== 'validating' || !run) {
|
|
139
|
+
return { handle, changed: false, outcome: 'not_validating' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (run.requestedChecks.length === 0) {
|
|
143
|
+
const next = transitionWorkHandle(controllerHome, handle, run.resumeState, {
|
|
144
|
+
finalization: { ...handle.finalization, validation: 'done', lastError: undefined },
|
|
145
|
+
validationRun: undefined,
|
|
146
|
+
validatedInputFingerprint: run.fingerprint,
|
|
147
|
+
failureReason: undefined,
|
|
148
|
+
});
|
|
149
|
+
projectWorkValidationOutcome(controllerHome, next, 'passed', 'No validation checks were required.');
|
|
150
|
+
return { handle: next, changed: true, outcome: 'passed' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const checkId of run.requestedChecks) {
|
|
154
|
+
const binding = run.processes[checkId];
|
|
155
|
+
if (!binding) return { handle, changed: false, outcome: 'running' };
|
|
156
|
+
const record = getProcessRecord(controllerHome, handle.repositoryId, binding.processId);
|
|
157
|
+
if (!record) {
|
|
158
|
+
return settleInfrastructureFailure(
|
|
159
|
+
controllerHome,
|
|
160
|
+
handle,
|
|
161
|
+
`Validation process record is unavailable: ${binding.processId}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (record.status === 'running' || record.status === 'starting' || record.status === 'running_recovered') {
|
|
165
|
+
return { handle, changed: false, outcome: 'running' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const currentIdentity = record.checkExecution
|
|
170
|
+
? controllerCheckExecutionIdentity(handle.worktreePath, checkId, record.checkExecution.timeoutMs)
|
|
171
|
+
: undefined;
|
|
172
|
+
const receipt = processCheckCompletionReceipt(record, {
|
|
173
|
+
repoId: handle.repositoryId,
|
|
174
|
+
checkoutId: handle.checkoutId,
|
|
175
|
+
workId: handle.workId,
|
|
176
|
+
executionSessionId: handle.sessionId,
|
|
177
|
+
checkId,
|
|
178
|
+
processId: binding.processId,
|
|
179
|
+
...(currentIdentity ? {
|
|
180
|
+
checkExecution: {
|
|
181
|
+
cacheKey: currentIdentity.cacheKey,
|
|
182
|
+
revision: currentIdentity.revision,
|
|
183
|
+
definitionDigest: currentIdentity.definitionDigest,
|
|
184
|
+
environmentFingerprint: currentIdentity.environmentFingerprint,
|
|
185
|
+
timeoutMs: currentIdentity.timeoutMs,
|
|
186
|
+
scopeKey: processCheckSemanticScopeKey({
|
|
187
|
+
checkoutId: handle.checkoutId,
|
|
188
|
+
workId: handle.workId,
|
|
189
|
+
verificationBinding: { executionSessionId: handle.sessionId },
|
|
190
|
+
}, currentIdentity.reuseScope),
|
|
191
|
+
},
|
|
192
|
+
} : {}),
|
|
193
|
+
});
|
|
194
|
+
if (!receipt.ok) {
|
|
195
|
+
if (receipt.status === 'timed_out' || receipt.status === 'cancelled') {
|
|
196
|
+
return settleInfrastructureFailure(controllerHome, handle, receipt.summary);
|
|
197
|
+
}
|
|
198
|
+
const next = transitionWorkHandle(controllerHome, handle, 'failed', {
|
|
199
|
+
finalization: { ...handle.finalization, validation: 'failed', lastError: receipt.summary },
|
|
200
|
+
validationRun: undefined,
|
|
201
|
+
failureReason: receipt.summary,
|
|
202
|
+
});
|
|
203
|
+
projectWorkValidationOutcome(controllerHome, next, 'failed', receipt.summary);
|
|
204
|
+
return { handle: next, changed: true, outcome: 'failed', summary: receipt.summary };
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const summary = error instanceof Error ? error.message : String(error);
|
|
208
|
+
return settleInfrastructureFailure(controllerHome, handle, summary);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const next = transitionWorkHandle(controllerHome, handle, run.resumeState, {
|
|
213
|
+
finalization: { ...handle.finalization, validation: 'done', lastError: undefined },
|
|
214
|
+
validationRun: undefined,
|
|
215
|
+
validatedInputFingerprint: run.fingerprint,
|
|
216
|
+
failureReason: undefined,
|
|
217
|
+
});
|
|
218
|
+
projectWorkValidationOutcome(controllerHome, next, 'passed', 'All requested validation receipts passed.');
|
|
219
|
+
return { handle: next, changed: true, outcome: 'passed' };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface PendingWorkValidationReconciliationSummary {
|
|
223
|
+
repositoryId: string;
|
|
224
|
+
examined: number;
|
|
225
|
+
validating: number;
|
|
226
|
+
changed: number;
|
|
227
|
+
running: number;
|
|
228
|
+
passed: number;
|
|
229
|
+
failed: number;
|
|
230
|
+
infrastructureFailure: number;
|
|
231
|
+
errors: Array<{ workId: string; error: string }>;
|
|
232
|
+
truncated: boolean;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Scheduler-safe convergence for validation Processes that were already
|
|
237
|
+
* launched by a Work. This function never launches commands and never waits
|
|
238
|
+
* for a Process. It only projects terminal receipts into Work state, allowing
|
|
239
|
+
* long checks to finish without ChatGPT polling.
|
|
240
|
+
*/
|
|
241
|
+
export function reconcilePendingWorkValidations(
|
|
242
|
+
controllerHome: string,
|
|
243
|
+
repositoryId: string,
|
|
244
|
+
limit = 500,
|
|
245
|
+
): PendingWorkValidationReconciliationSummary {
|
|
246
|
+
const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 5_000));
|
|
247
|
+
const validating = listValidatingWorkHandles(controllerHome, repositoryId, boundedLimit);
|
|
248
|
+
const handles = validating;
|
|
249
|
+
const summary: PendingWorkValidationReconciliationSummary = {
|
|
250
|
+
repositoryId,
|
|
251
|
+
examined: handles.length,
|
|
252
|
+
validating: validating.length,
|
|
253
|
+
changed: 0,
|
|
254
|
+
running: 0,
|
|
255
|
+
passed: 0,
|
|
256
|
+
failed: 0,
|
|
257
|
+
infrastructureFailure: 0,
|
|
258
|
+
errors: [],
|
|
259
|
+
truncated: handles.length >= boundedLimit,
|
|
260
|
+
};
|
|
261
|
+
for (const handle of validating) {
|
|
262
|
+
try {
|
|
263
|
+
const reconciled = reconcileWorkValidation(controllerHome, handle);
|
|
264
|
+
if (reconciled.changed) summary.changed += 1;
|
|
265
|
+
if (reconciled.outcome === 'running') summary.running += 1;
|
|
266
|
+
else if (reconciled.outcome === 'passed') summary.passed += 1;
|
|
267
|
+
else if (reconciled.outcome === 'failed') summary.failed += 1;
|
|
268
|
+
else if (reconciled.outcome === 'infrastructure_failure') summary.infrastructureFailure += 1;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
summary.errors.push({
|
|
271
|
+
workId: handle.workId,
|
|
272
|
+
error: error instanceof Error ? error.message : String(error),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return summary;
|
|
277
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
symlinkSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'fs';
|
|
14
|
+
import { dirname, join, resolve } from 'path';
|
|
15
|
+
import { spawnSync } from 'child_process';
|
|
16
|
+
import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
|
|
17
|
+
import { globMatches, normalizeMcpRelativePath } from '../../../cli/mcp/paths';
|
|
18
|
+
import { sanitizeFileComponent } from '../../shared/json-files';
|
|
19
|
+
|
|
20
|
+
const STALE_SNAPSHOT_MS = 6 * 60 * 60_000;
|
|
21
|
+
const SNAPSHOT_MARKER = '.forge-work-verification-snapshot.json';
|
|
22
|
+
|
|
23
|
+
export interface WorkVerificationSnapshotScope {
|
|
24
|
+
workId: string;
|
|
25
|
+
allowedPaths: readonly string[];
|
|
26
|
+
forbiddenPaths: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkVerificationSnapshot {
|
|
30
|
+
root: string;
|
|
31
|
+
sourceHead: string;
|
|
32
|
+
includedPaths: string[];
|
|
33
|
+
excludedPaths: string[];
|
|
34
|
+
ownershipDigest: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function git(root: string, args: string[], maxBytes = 2 * 1024 * 1024): Buffer {
|
|
38
|
+
const result = spawnSync('git', ['-C', root, ...args], {
|
|
39
|
+
encoding: 'buffer',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
+
timeout: 120_000,
|
|
42
|
+
maxBuffer: maxBytes,
|
|
43
|
+
});
|
|
44
|
+
if (result.status !== 0 || result.error) {
|
|
45
|
+
const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString('utf8') : String(result.stderr ?? '');
|
|
46
|
+
throw new Error(`WORK_VERIFICATION_SNAPSHOT_GIT_FAILED: git ${args.join(' ')}: ${stderr || result.error?.message || `exit ${result.status}`}`);
|
|
47
|
+
}
|
|
48
|
+
return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nulPaths(value: Buffer): string[] {
|
|
52
|
+
return value.toString('utf8').split('\0').filter(Boolean).map((raw) => {
|
|
53
|
+
const normalized = normalizeMcpRelativePath(raw);
|
|
54
|
+
if (!normalized.ok || !normalized.relativePath) {
|
|
55
|
+
throw new Error(`WORK_VERIFICATION_SNAPSHOT_PATH_INVALID: ${raw}`);
|
|
56
|
+
}
|
|
57
|
+
return normalized.relativePath;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function matchesAny(patterns: readonly string[], path: string): boolean {
|
|
62
|
+
return patterns.some((pattern) => globMatches(pattern, path));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function snapshotRoot(controllerHome: string, repoId: string, workId: string): string {
|
|
66
|
+
const root = join(repositoryControllerRoot(controllerHome, repoId), 'verification-snapshots');
|
|
67
|
+
mkdirSync(root, { recursive: true });
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
70
|
+
if (!entry.isDirectory() || !entry.name.startsWith('snapshot-')) continue;
|
|
71
|
+
const path = join(root, entry.name);
|
|
72
|
+
try {
|
|
73
|
+
if (now - statSync(path).mtimeMs > STALE_SNAPSHOT_MS) rmSync(path, { recursive: true, force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
// Best-effort stale cleanup; a fresh snapshot must not fail because an old
|
|
76
|
+
// directory disappeared concurrently.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return mkdtempSync(join(root, `snapshot-${sanitizeFileComponent(workId).slice(0, 48)}-`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cloneHead(sourceRoot: string, targetRoot: string, head: string): void {
|
|
83
|
+
const clone = spawnSync('git', ['clone', '--shared', '--no-checkout', '--quiet', '--', sourceRoot, targetRoot], {
|
|
84
|
+
encoding: 'utf8',
|
|
85
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
86
|
+
timeout: 120_000,
|
|
87
|
+
});
|
|
88
|
+
if (clone.status !== 0 || clone.error) {
|
|
89
|
+
throw new Error(`WORK_VERIFICATION_SNAPSHOT_CLONE_FAILED: ${clone.stderr || clone.error?.message || `exit ${clone.status}`}`);
|
|
90
|
+
}
|
|
91
|
+
git(targetRoot, ['checkout', '--detach', '--quiet', head]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function overlayPath(sourceRoot: string, targetRoot: string, relativePath: string): void {
|
|
95
|
+
const source = resolve(sourceRoot, relativePath);
|
|
96
|
+
const target = resolve(targetRoot, relativePath);
|
|
97
|
+
rmSync(target, { recursive: true, force: true });
|
|
98
|
+
if (!existsSync(source)) return;
|
|
99
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
100
|
+
const stat = lstatSync(source);
|
|
101
|
+
if (stat.isSymbolicLink()) {
|
|
102
|
+
cpSync(source, target, { recursive: false, dereference: false, force: true });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
cpSync(source, target, { recursive: stat.isDirectory(), dereference: false, force: true, preserveTimestamps: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function linkIgnoredNodeModules(sourceRoot: string, targetRoot: string): void {
|
|
109
|
+
const source = join(sourceRoot, 'node_modules');
|
|
110
|
+
const target = join(targetRoot, 'node_modules');
|
|
111
|
+
if (!existsSync(source) || existsSync(target)) return;
|
|
112
|
+
const ignored = spawnSync('git', ['-C', sourceRoot, 'check-ignore', '--quiet', '--', 'node_modules'], { stdio: 'ignore', timeout: 10_000 });
|
|
113
|
+
if (ignored.status !== 0) return;
|
|
114
|
+
try {
|
|
115
|
+
symlinkSync(source, target, process.platform === 'win32' ? 'junction' : 'dir');
|
|
116
|
+
} catch {
|
|
117
|
+
// Dependency linking is an optimization. If it is unavailable the check may
|
|
118
|
+
// still succeed using its own toolchain resolution; never copy a huge cache.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function materializeWorkVerificationSnapshot(input: {
|
|
123
|
+
controllerHome: string;
|
|
124
|
+
repoId: string;
|
|
125
|
+
sourceRoot: string;
|
|
126
|
+
scope: WorkVerificationSnapshotScope;
|
|
127
|
+
}): WorkVerificationSnapshot {
|
|
128
|
+
const sourceHead = git(input.sourceRoot, ['rev-parse', '--verify', 'HEAD']).toString('utf8').trim();
|
|
129
|
+
const tracked = nulPaths(git(input.sourceRoot, ['diff', '--name-only', '-z', 'HEAD', '--']));
|
|
130
|
+
const untracked = nulPaths(git(input.sourceRoot, ['ls-files', '--others', '--exclude-standard', '-z']));
|
|
131
|
+
const dirtyPaths = [...new Set([...tracked, ...untracked])].sort();
|
|
132
|
+
const includedPaths: string[] = [];
|
|
133
|
+
const excludedPaths: string[] = [];
|
|
134
|
+
for (const path of dirtyPaths) {
|
|
135
|
+
if (matchesAny(input.scope.forbiddenPaths, path)) {
|
|
136
|
+
excludedPaths.push(path);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (input.scope.allowedPaths.length > 0 && matchesAny(input.scope.allowedPaths, path)) {
|
|
140
|
+
includedPaths.push(path);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`WORK_VERIFICATION_PATH_OWNERSHIP_AMBIGUOUS: ${path} is dirty but is neither allowed nor forbidden for ${input.scope.workId}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const root = snapshotRoot(input.controllerHome, input.repoId, input.scope.workId);
|
|
147
|
+
try {
|
|
148
|
+
cloneHead(input.sourceRoot, root, sourceHead);
|
|
149
|
+
for (const path of includedPaths) overlayPath(input.sourceRoot, root, path);
|
|
150
|
+
linkIgnoredNodeModules(input.sourceRoot, root);
|
|
151
|
+
const ownershipDigest = createHash('sha256').update(JSON.stringify({
|
|
152
|
+
sourceHead,
|
|
153
|
+
workId: input.scope.workId,
|
|
154
|
+
includedPaths,
|
|
155
|
+
excludedPaths,
|
|
156
|
+
allowedPaths: [...input.scope.allowedPaths].sort(),
|
|
157
|
+
forbiddenPaths: [...input.scope.forbiddenPaths].sort(),
|
|
158
|
+
})).digest('hex');
|
|
159
|
+
writeFileSync(join(root, SNAPSHOT_MARKER), `${JSON.stringify({ schemaVersion: 1, ...input.scope, sourceHead, includedPaths, excludedPaths, ownershipDigest }, null, 2)}\n`);
|
|
160
|
+
return { root, sourceHead, includedPaths, excludedPaths, ownershipDigest };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
rmSync(root, { recursive: true, force: true });
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function cleanupWorkVerificationSnapshot(snapshotRoot: string): void {
|
|
168
|
+
rmSync(snapshotRoot, { recursive: true, force: true });
|
|
169
|
+
}
|
|
@@ -142,16 +142,6 @@ const CORE_CAPABILITIES: CapabilityDescriptor[] = [
|
|
|
142
142
|
schemaExposure: 'stable_static',
|
|
143
143
|
summary: 'Plan, dispatch, review, verify, retry, and accept durable Issue and Task work through existing typed handlers.',
|
|
144
144
|
},
|
|
145
|
-
{
|
|
146
|
-
capabilityId: 'workflow.campaign',
|
|
147
|
-
domain: 'controller',
|
|
148
|
-
group: 'campaign',
|
|
149
|
-
operationClass: 'execute',
|
|
150
|
-
risk: 'workspace_write',
|
|
151
|
-
exposedVia: 'rh_work',
|
|
152
|
-
schemaExposure: 'stable_static',
|
|
153
|
-
summary: 'Coordinate multi-step campaign DAGs and review checkpoints while keeping integration and final acceptance explicit.',
|
|
154
|
-
},
|
|
155
145
|
{
|
|
156
146
|
capabilityId: 'plugin.browser',
|
|
157
147
|
domain: 'plugin',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
|
|
4
|
+
import { withControllerLock } from '../../../cli/repositories/locks';
|
|
4
5
|
import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
|
|
5
6
|
import {
|
|
6
7
|
type HandoffCreationReason,
|
|
@@ -65,6 +66,20 @@ function nowIso(options: HandoffInboxStoreOptions): string {
|
|
|
65
66
|
return options.now?.() ?? new Date().toISOString();
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
function withHandoffInboxWriteLock<T>(
|
|
70
|
+
options: HandoffInboxStoreOptions,
|
|
71
|
+
owner: string,
|
|
72
|
+
operation: () => T,
|
|
73
|
+
): T {
|
|
74
|
+
if (!options.controllerHome || !options.repoId || options.root) return operation();
|
|
75
|
+
return withControllerLock(
|
|
76
|
+
options.controllerHome,
|
|
77
|
+
{ scope: 'task', repoId: options.repoId, taskId: 'handoff-inbox' },
|
|
78
|
+
owner,
|
|
79
|
+
operation,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
export function handoffInboxRoot(location: HandoffInboxStoreLocation): string {
|
|
69
84
|
if (location.root) {
|
|
70
85
|
mkdirSync(location.root, { recursive: true });
|
|
@@ -117,17 +132,19 @@ export function createHandoffItem(options: HandoffInboxStoreOptions, input: Crea
|
|
|
117
132
|
createdAt: at,
|
|
118
133
|
updatedAt: input.updatedAt ?? at,
|
|
119
134
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
return withHandoffInboxWriteLock(options, `create-handoff:${item.id}`, () => {
|
|
136
|
+
const store = readHandoffInboxStore(options);
|
|
137
|
+
if (store.items.some((existing) => existing.id === item.id)) {
|
|
138
|
+
throw new Error(`handoff already exists: ${item.id}`);
|
|
139
|
+
}
|
|
140
|
+
const nextStore: HandoffInboxStore = {
|
|
141
|
+
schemaVersion: 1,
|
|
142
|
+
updatedAt: item.updatedAt,
|
|
143
|
+
items: [item, ...store.items],
|
|
144
|
+
};
|
|
145
|
+
writeHandoffInboxStore(options, nextStore);
|
|
146
|
+
return item;
|
|
147
|
+
});
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
export function listHandoffItems(options: ListHandoffOptions): HandoffItem[] {
|
|
@@ -171,20 +188,22 @@ function setHandoffStatus(
|
|
|
171
188
|
patch: Partial<Pick<HandoffItem, 'decision' | 'resolver'>> = {},
|
|
172
189
|
): HandoffItem {
|
|
173
190
|
const sanitizedId = sanitizeFileComponent(id);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
return withHandoffInboxWriteLock(options, `update-handoff:${sanitizedId}`, () => {
|
|
192
|
+
const store = readHandoffInboxStore(options);
|
|
193
|
+
const index = store.items.findIndex((item) => item.id === sanitizedId);
|
|
194
|
+
if (index < 0) throw new Error(`handoff not found: ${sanitizedId}`);
|
|
195
|
+
const at = nowIso(options);
|
|
196
|
+
const item: HandoffItem = {
|
|
197
|
+
...store.items[index],
|
|
198
|
+
...patch,
|
|
199
|
+
status,
|
|
200
|
+
updatedAt: at,
|
|
201
|
+
};
|
|
202
|
+
const items = [...store.items];
|
|
203
|
+
items[index] = item;
|
|
204
|
+
writeHandoffInboxStore(options, { schemaVersion: 1, updatedAt: at, items });
|
|
205
|
+
return item;
|
|
206
|
+
});
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
export function acknowledgeHandoffItem(options: HandoffInboxStoreOptions, id: string): HandoffItem {
|
|
@@ -12,8 +12,14 @@ function expectedActionRisk(action: SuggestedNextAction): SuggestedNextAction['r
|
|
|
12
12
|
case 'controller_claim':
|
|
13
13
|
case 'controller_release':
|
|
14
14
|
case 'launcher_start':
|
|
15
|
+
case 'schedule_create':
|
|
16
|
+
case 'schedule_pause':
|
|
17
|
+
case 'schedule_resume':
|
|
18
|
+
case 'schedule_trigger':
|
|
15
19
|
return 'workspace_write';
|
|
16
20
|
case 'controller_get_owner':
|
|
21
|
+
case 'schedule_list':
|
|
22
|
+
case 'schedule_get':
|
|
17
23
|
return 'readonly';
|
|
18
24
|
case 'plan_create':
|
|
19
25
|
case 'plan_get':
|
|
@@ -34,7 +40,7 @@ const ALLOWED_FACADE_OPERATIONS: Record<FacadeTool, readonly string[]> = {
|
|
|
34
40
|
rh_status: ['list', 'get', 'repair'],
|
|
35
41
|
rh_inbox: ['list', 'get', 'ack', 'accept', 'resolve', 'dismiss', 'create'],
|
|
36
42
|
rh_context: ['list', 'get', 'search'],
|
|
37
|
-
rh_work: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede'],
|
|
43
|
+
rh_work: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'],
|
|
38
44
|
};
|
|
39
45
|
|
|
40
46
|
export interface SuggestedActionValidationOptions {
|