@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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
writeControlPlaneRecordWithinTransaction,
|
|
13
13
|
} from '../persistence/sqlite-store';
|
|
14
14
|
import { assertWorkAdmissionAllowed } from './work-admission-policy';
|
|
15
|
+
import { phaseIndex, suggestedActionsForStatus, transitionPhaseEvidence, validateWorkSemanticTransition, validateWorkSemantics } from './work-state-machine';
|
|
15
16
|
import {
|
|
16
17
|
type EvidenceRef,
|
|
17
18
|
type CompletionOutcome,
|
|
@@ -32,7 +33,6 @@ import {
|
|
|
32
33
|
type WorkPhaseEvidenceState,
|
|
33
34
|
type WorkContractStore,
|
|
34
35
|
isDirectEditWorkCompletionReceipt,
|
|
35
|
-
isRemoteEffectCompletionReceipt,
|
|
36
36
|
isRepositoryCompletionReceipt,
|
|
37
37
|
isTerminalWorkContractStatus,
|
|
38
38
|
} from './types';
|
|
@@ -173,10 +173,6 @@ function inferredEvidenceState(status: WorkContractStatus): EvidenceState {
|
|
|
173
173
|
return 'none';
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
function phaseIndex(phase: WorkPhase): number {
|
|
177
|
-
return ['implementation', 'verification', 'delivery', 'cleanup'].indexOf(phase);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
176
|
function legacyPhaseEvidence(
|
|
181
177
|
contract: Pick<WorkContract, 'phase' | 'status' | 'evidenceRefs' | 'completionReceipt' | 'updatedAt'>,
|
|
182
178
|
source: WorkPhaseEvidence['source'] = 'legacy_inferred',
|
|
@@ -207,210 +203,6 @@ function legacyPhaseEvidence(
|
|
|
207
203
|
})) as WorkPhaseEvidenceMap;
|
|
208
204
|
}
|
|
209
205
|
|
|
210
|
-
function transitionPhaseEvidence(
|
|
211
|
-
current: Pick<WorkContract, 'phaseEvidence' | 'evidenceRefs'>,
|
|
212
|
-
targetPhase: WorkPhase,
|
|
213
|
-
input: {
|
|
214
|
-
status: WorkContractStatus;
|
|
215
|
-
summary: string;
|
|
216
|
-
evidenceRefs?: EvidenceRef[];
|
|
217
|
-
recordedAt: string;
|
|
218
|
-
source?: WorkPhaseEvidence['source'];
|
|
219
|
-
},
|
|
220
|
-
): WorkPhaseEvidenceMap {
|
|
221
|
-
const targetIndex = phaseIndex(targetPhase);
|
|
222
|
-
const evidenceRefs = (input.evidenceRefs ?? current.evidenceRefs).slice(0, 20);
|
|
223
|
-
const source = input.source ?? 'recorded';
|
|
224
|
-
const phaseEvidence = { ...current.phaseEvidence };
|
|
225
|
-
for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
|
|
226
|
-
const index = phaseIndex(phase);
|
|
227
|
-
if (index < targetIndex) {
|
|
228
|
-
phaseEvidence[phase] = {
|
|
229
|
-
state: ['satisfied', 'skipped'].includes(phaseEvidence[phase].state) ? phaseEvidence[phase].state : 'satisfied',
|
|
230
|
-
source,
|
|
231
|
-
summary: `Advanced to ${targetPhase}: ${input.summary}`.slice(0, 1_000),
|
|
232
|
-
evidenceRefs,
|
|
233
|
-
recordedAt: input.recordedAt,
|
|
234
|
-
};
|
|
235
|
-
} else if (index > targetIndex) {
|
|
236
|
-
phaseEvidence[phase] = {
|
|
237
|
-
state: 'pending',
|
|
238
|
-
source,
|
|
239
|
-
summary: `Waiting for Work phase ${phase}.`,
|
|
240
|
-
evidenceRefs: [],
|
|
241
|
-
recordedAt: input.recordedAt,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
phaseEvidence[targetPhase] = {
|
|
246
|
-
state: input.status === 'failed'
|
|
247
|
-
? 'failed'
|
|
248
|
-
: input.status === 'cancelled'
|
|
249
|
-
? 'skipped'
|
|
250
|
-
: input.status === 'blocked' || input.status === 'ready'
|
|
251
|
-
? 'blocked'
|
|
252
|
-
: 'active',
|
|
253
|
-
source,
|
|
254
|
-
summary: input.summary.trim().slice(0, 1_000) || `Work entered ${targetPhase}.`,
|
|
255
|
-
evidenceRefs,
|
|
256
|
-
recordedAt: input.recordedAt,
|
|
257
|
-
};
|
|
258
|
-
return phaseEvidence;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function validateWorkSemantics(contract: WorkContract): WorkContract {
|
|
262
|
-
if (!contract.objective.trim()) throw new Error('WORK_OBJECTIVE_REQUIRED');
|
|
263
|
-
if (contract.status === 'completed' && !contract.completionReceipt) {
|
|
264
|
-
throw new Error('WORK_COMPLETION_RECEIPT_REQUIRED');
|
|
265
|
-
}
|
|
266
|
-
const currentPhaseIndex = phaseIndex(contract.phase);
|
|
267
|
-
for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
|
|
268
|
-
const checkpoint = contract.phaseEvidence?.[phase];
|
|
269
|
-
if (!checkpoint) throw new Error(`WORK_PHASE_EVIDENCE_REQUIRED: ${phase}`);
|
|
270
|
-
const index = phaseIndex(phase);
|
|
271
|
-
if (index < currentPhaseIndex && !['satisfied', 'skipped'].includes(checkpoint.state)) {
|
|
272
|
-
throw new Error(`WORK_PHASE_EVIDENCE_PREVIOUS_NOT_SATISFIED: ${phase}`);
|
|
273
|
-
}
|
|
274
|
-
if (index === currentPhaseIndex && checkpoint.state === 'pending') {
|
|
275
|
-
throw new Error(`WORK_PHASE_EVIDENCE_REQUIRED: ${phase}`);
|
|
276
|
-
}
|
|
277
|
-
if (index > currentPhaseIndex && checkpoint.state !== 'pending') {
|
|
278
|
-
throw new Error(`WORK_PHASE_EVIDENCE_FUTURE_NOT_PENDING: ${phase}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (contract.completionReceipt) {
|
|
282
|
-
const receipt = contract.completionReceipt;
|
|
283
|
-
if (receipt.workId !== contract.workId) throw new Error('WORK_COMPLETION_RECEIPT_IDENTITY_MISMATCH');
|
|
284
|
-
if (isRepositoryCompletionReceipt(receipt)) {
|
|
285
|
-
if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
|
|
286
|
-
if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
|
|
287
|
-
if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
|
|
288
|
-
} else if (isDirectEditWorkCompletionReceipt(receipt)) {
|
|
289
|
-
const directEditAuthorityCount = Number(Boolean(receipt.editSessionId?.trim())) + Number(Boolean(receipt.reconciliationId?.trim()));
|
|
290
|
-
if (directEditAuthorityCount !== 1) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_AUTHORITY_REQUIRED');
|
|
291
|
-
if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
|
|
292
|
-
if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
|
|
293
|
-
if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
|
|
294
|
-
if (!['repository_change', 'completed_no_change'].includes(contract.workKind)) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_KIND_REQUIRED');
|
|
295
|
-
const expectedOutcome = contract.workKind === 'completed_no_change' ? 'completed_no_change' : 'completed_changed';
|
|
296
|
-
if (contract.completionOutcome !== expectedOutcome) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_OUTCOME_REQUIRED');
|
|
297
|
-
} else if (isRemoteEffectCompletionReceipt(receipt)) {
|
|
298
|
-
if (contract.workKind !== 'remote_effect') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_KIND_REQUIRED');
|
|
299
|
-
if (contract.completionOutcome !== 'completed_remote') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_OUTCOME_REQUIRED');
|
|
300
|
-
if (!receipt.pluginId.trim() || !receipt.actionId.trim() || !receipt.requestId.trim() || !receipt.semanticKey.trim() || !receipt.resultDigest.trim()) {
|
|
301
|
-
throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_IDENTITY_REQUIRED');
|
|
302
|
-
}
|
|
303
|
-
} else {
|
|
304
|
-
if (contract.workKind !== 'local_effect') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_KIND_REQUIRED');
|
|
305
|
-
if (contract.completionOutcome !== 'completed_local') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_OUTCOME_REQUIRED');
|
|
306
|
-
if (!receipt.operation.trim() || !receipt.target.id.trim()) throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_TARGET_REQUIRED');
|
|
307
|
-
}
|
|
308
|
-
if (contract.status !== 'completed') throw new Error('WORK_COMPLETION_RECEIPT_REQUIRES_COMPLETED_WORK');
|
|
309
|
-
for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
|
|
310
|
-
if (!['satisfied', 'skipped'].includes(contract.phaseEvidence[phase].state)) {
|
|
311
|
-
throw new Error(`WORK_COMPLETION_PHASE_NOT_SATISFIED: ${phase}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
for (const phase of ['delivery', 'cleanup'] as WorkPhase[]) {
|
|
315
|
-
if (contract.phaseEvidence[phase].receiptId !== receipt.receiptId) {
|
|
316
|
-
throw new Error(`WORK_COMPLETION_PHASE_RECEIPT_MISMATCH: ${phase}`);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
const outcome = contract.completionOutcome;
|
|
321
|
-
if (!outcome) return contract;
|
|
322
|
-
if (contract.status !== 'completed' || !contract.completionReceipt) {
|
|
323
|
-
throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires a completed Work receipt`);
|
|
324
|
-
}
|
|
325
|
-
if (contract.dispatchState !== 'terminal') {
|
|
326
|
-
throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires terminal dispatch`);
|
|
327
|
-
}
|
|
328
|
-
if (contract.evidenceState !== 'valid') {
|
|
329
|
-
throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires valid evidence`);
|
|
330
|
-
}
|
|
331
|
-
if (outcome === 'completed_changed' && contract.workKind !== 'repository_change') {
|
|
332
|
-
throw new Error('WORK_SEMANTICS_INVALID: completed_changed requires repository_change WorkKind');
|
|
333
|
-
}
|
|
334
|
-
if (outcome === 'completed_no_change' && contract.workKind !== 'completed_no_change') {
|
|
335
|
-
throw new Error('WORK_SEMANTICS_INVALID: completed_no_change requires completed_no_change WorkKind');
|
|
336
|
-
}
|
|
337
|
-
if (outcome === 'completed_local' && contract.workKind !== 'local_effect') {
|
|
338
|
-
throw new Error('WORK_SEMANTICS_INVALID: completed_local requires local_effect WorkKind');
|
|
339
|
-
}
|
|
340
|
-
if (outcome === 'completed_local' && contract.completionReceipt?.source !== 'local_effect') {
|
|
341
|
-
throw new Error('WORK_SEMANTICS_INVALID: completed_local requires a local_effect completion receipt');
|
|
342
|
-
}
|
|
343
|
-
if (outcome === 'completed_remote' && contract.workKind !== 'remote_effect') {
|
|
344
|
-
throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires remote_effect WorkKind');
|
|
345
|
-
}
|
|
346
|
-
if (outcome === 'completed_remote' && contract.completionReceipt?.source !== 'remote_effect') {
|
|
347
|
-
throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires a remote_effect completion receipt');
|
|
348
|
-
}
|
|
349
|
-
if (outcome === 'superseded' && contract.workKind !== 'superseded') {
|
|
350
|
-
throw new Error('WORK_SEMANTICS_INVALID: superseded requires superseded WorkKind');
|
|
351
|
-
}
|
|
352
|
-
return contract;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const DISPATCH_TRANSITIONS: Readonly<Record<DispatchState, readonly DispatchState[]>> = {
|
|
356
|
-
not_dispatched: ['not_dispatched', 'claimed', 'launching', 'running', 'blocked', 'terminal'],
|
|
357
|
-
claimed: ['claimed', 'launching', 'running', 'blocked', 'terminal'],
|
|
358
|
-
launching: ['launching', 'running', 'blocked', 'terminal'],
|
|
359
|
-
running: ['running', 'blocked', 'terminal'],
|
|
360
|
-
blocked: ['blocked', 'claimed', 'launching', 'running', 'terminal'],
|
|
361
|
-
terminal: ['terminal'],
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
const EVIDENCE_TRANSITIONS: Readonly<Record<EvidenceState, readonly EvidenceState[]>> = {
|
|
365
|
-
none: ['none', 'partial', 'valid', 'failed'],
|
|
366
|
-
partial: ['partial', 'valid', 'stale', 'contradictory', 'failed'],
|
|
367
|
-
valid: ['valid', 'stale', 'contradictory', 'failed'],
|
|
368
|
-
stale: ['stale', 'partial', 'valid', 'contradictory', 'failed'],
|
|
369
|
-
contradictory: ['contradictory', 'partial', 'valid', 'failed'],
|
|
370
|
-
failed: ['failed', 'partial', 'valid', 'contradictory'],
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
function validateWorkSemanticTransition(
|
|
374
|
-
current: WorkContract,
|
|
375
|
-
next: WorkContract,
|
|
376
|
-
options: { allowRetainedCancelledResume?: boolean } = {},
|
|
377
|
-
): WorkContract {
|
|
378
|
-
const retryingFailedWork = current.status === 'failed'
|
|
379
|
-
&& !current.completionOutcome
|
|
380
|
-
&& ['claimed', 'launching', 'running', 'blocked'].includes(next.dispatchState);
|
|
381
|
-
const resumingRetainedCancelledWork = options.allowRetainedCancelledResume === true
|
|
382
|
-
&& current.status === 'cancelled'
|
|
383
|
-
&& current.dispatchState === 'terminal'
|
|
384
|
-
&& current.phase === 'cleanup'
|
|
385
|
-
&& current.phaseEvidence.cleanup.state === 'skipped'
|
|
386
|
-
&& !current.completionReceipt
|
|
387
|
-
&& !current.completionOutcome
|
|
388
|
-
&& next.status === 'running'
|
|
389
|
-
&& next.dispatchState === 'running'
|
|
390
|
-
&& next.phase === 'implementation';
|
|
391
|
-
if (!retryingFailedWork && !resumingRetainedCancelledWork && !DISPATCH_TRANSITIONS[current.dispatchState].includes(next.dispatchState)) {
|
|
392
|
-
throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: dispatch ${current.dispatchState} -> ${next.dispatchState}`);
|
|
393
|
-
}
|
|
394
|
-
if (!EVIDENCE_TRANSITIONS[current.evidenceState].includes(next.evidenceState)) {
|
|
395
|
-
throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: evidence ${current.evidenceState} -> ${next.evidenceState}`);
|
|
396
|
-
}
|
|
397
|
-
if (current.completionOutcome && current.completionOutcome !== next.completionOutcome) {
|
|
398
|
-
throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: completion outcome ${current.completionOutcome} is immutable`);
|
|
399
|
-
}
|
|
400
|
-
if ((current.lifecycleRole ?? 'primary') !== (next.lifecycleRole ?? 'primary')) {
|
|
401
|
-
throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: lifecycleRole is immutable');
|
|
402
|
-
}
|
|
403
|
-
if (current.parentWorkId !== next.parentWorkId) {
|
|
404
|
-
throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: parentWorkId is immutable');
|
|
405
|
-
}
|
|
406
|
-
return next;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function suggestedActionsForStatus(status: WorkContractStatus, actions: readonly SuggestedNextAction[]): SuggestedNextAction[] {
|
|
410
|
-
if (status === 'completed' || status === 'cancelled') return [];
|
|
411
|
-
return actions.slice(0, 8);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
206
|
function sqliteBacked(options: WorkContractStoreOptions): options is WorkContractStoreOptions & { controllerHome: string; repoId: string } {
|
|
415
207
|
// A caller-provided root is a test/portable compatibility store. Runtime
|
|
416
208
|
// controller state always carries controllerHome + repoId and is SQLite.
|
|
@@ -805,6 +597,8 @@ function wasPausedByRuntimeReconciliation(contract: WorkContract): boolean {
|
|
|
805
597
|
|
|
806
598
|
function hasReviewableWorkOutput(contract: WorkContract): boolean {
|
|
807
599
|
if (contract.checkRefs.length > 0 || contract.handoffRefs.length > 0) return true;
|
|
600
|
+
if ((contract.scopeEvidence?.inspectedPaths.length ?? 0) > 0) return true;
|
|
601
|
+
if ((contract.readOnlyReviewEvidence?.inspectedPaths.length ?? 0) > 0 || (contract.readOnlyReviewEvidence?.findings.length ?? 0) > 0) return true;
|
|
808
602
|
return contract.evidenceRefs.some((evidence) =>
|
|
809
603
|
Boolean(evidence.artifactId || evidence.evidenceId)
|
|
810
604
|
|| !SYSTEM_ONLY_EVIDENCE_TITLES.has(evidence.title));
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDirectEditWorkCompletionReceipt,
|
|
3
|
+
isReadOnlyReviewCompletionReceipt,
|
|
4
|
+
isRemoteEffectCompletionReceipt,
|
|
5
|
+
isRepositoryCompletionReceipt,
|
|
6
|
+
type DispatchState,
|
|
7
|
+
type EvidenceRef,
|
|
8
|
+
type EvidenceState,
|
|
9
|
+
type SuggestedNextAction,
|
|
10
|
+
type WorkContract,
|
|
11
|
+
type WorkContractStatus,
|
|
12
|
+
type WorkPhase,
|
|
13
|
+
type WorkPhaseEvidence,
|
|
14
|
+
type WorkPhaseEvidenceMap,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
export function phaseIndex(phase: WorkPhase): number {
|
|
18
|
+
return ['implementation', 'verification', 'delivery', 'cleanup'].indexOf(phase);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function transitionPhaseEvidence(
|
|
22
|
+
current: Pick<WorkContract, 'phaseEvidence' | 'evidenceRefs'>,
|
|
23
|
+
targetPhase: WorkPhase,
|
|
24
|
+
input: {
|
|
25
|
+
status: WorkContractStatus;
|
|
26
|
+
summary: string;
|
|
27
|
+
evidenceRefs?: EvidenceRef[];
|
|
28
|
+
recordedAt: string;
|
|
29
|
+
source?: WorkPhaseEvidence['source'];
|
|
30
|
+
},
|
|
31
|
+
): WorkPhaseEvidenceMap {
|
|
32
|
+
const targetIndex = phaseIndex(targetPhase);
|
|
33
|
+
const evidenceRefs = (input.evidenceRefs ?? current.evidenceRefs).slice(0, 20);
|
|
34
|
+
const source = input.source ?? 'recorded';
|
|
35
|
+
const phaseEvidence = { ...current.phaseEvidence };
|
|
36
|
+
for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
|
|
37
|
+
const index = phaseIndex(phase);
|
|
38
|
+
if (index < targetIndex) {
|
|
39
|
+
phaseEvidence[phase] = {
|
|
40
|
+
state: ['satisfied', 'skipped'].includes(phaseEvidence[phase].state) ? phaseEvidence[phase].state : 'satisfied',
|
|
41
|
+
source,
|
|
42
|
+
summary: `Advanced to ${targetPhase}: ${input.summary}`.slice(0, 1_000),
|
|
43
|
+
evidenceRefs,
|
|
44
|
+
recordedAt: input.recordedAt,
|
|
45
|
+
};
|
|
46
|
+
} else if (index > targetIndex) {
|
|
47
|
+
phaseEvidence[phase] = {
|
|
48
|
+
state: 'pending',
|
|
49
|
+
source,
|
|
50
|
+
summary: `Waiting for Work phase ${phase}.`,
|
|
51
|
+
evidenceRefs: [],
|
|
52
|
+
recordedAt: input.recordedAt,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
phaseEvidence[targetPhase] = {
|
|
57
|
+
state: input.status === 'failed'
|
|
58
|
+
? 'failed'
|
|
59
|
+
: input.status === 'cancelled'
|
|
60
|
+
? 'skipped'
|
|
61
|
+
: input.status === 'blocked' || input.status === 'ready'
|
|
62
|
+
? 'blocked'
|
|
63
|
+
: 'active',
|
|
64
|
+
source,
|
|
65
|
+
summary: input.summary.trim().slice(0, 1_000) || `Work entered ${targetPhase}.`,
|
|
66
|
+
evidenceRefs,
|
|
67
|
+
recordedAt: input.recordedAt,
|
|
68
|
+
};
|
|
69
|
+
return phaseEvidence;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validateWorkSemantics(contract: WorkContract): WorkContract {
|
|
73
|
+
if (!contract.objective.trim()) throw new Error('WORK_OBJECTIVE_REQUIRED');
|
|
74
|
+
if (contract.status === 'completed' && !contract.completionReceipt) {
|
|
75
|
+
throw new Error('WORK_COMPLETION_RECEIPT_REQUIRED');
|
|
76
|
+
}
|
|
77
|
+
const currentPhaseIndex = phaseIndex(contract.phase);
|
|
78
|
+
for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
|
|
79
|
+
const checkpoint = contract.phaseEvidence?.[phase];
|
|
80
|
+
if (!checkpoint) throw new Error(`WORK_PHASE_EVIDENCE_REQUIRED: ${phase}`);
|
|
81
|
+
const index = phaseIndex(phase);
|
|
82
|
+
if (index < currentPhaseIndex && !['satisfied', 'skipped'].includes(checkpoint.state)) {
|
|
83
|
+
throw new Error(`WORK_PHASE_EVIDENCE_PREVIOUS_NOT_SATISFIED: ${phase}`);
|
|
84
|
+
}
|
|
85
|
+
if (index === currentPhaseIndex && checkpoint.state === 'pending') {
|
|
86
|
+
throw new Error(`WORK_PHASE_EVIDENCE_REQUIRED: ${phase}`);
|
|
87
|
+
}
|
|
88
|
+
if (index > currentPhaseIndex && checkpoint.state !== 'pending') {
|
|
89
|
+
throw new Error(`WORK_PHASE_EVIDENCE_FUTURE_NOT_PENDING: ${phase}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (contract.completionReceipt) {
|
|
93
|
+
const receipt = contract.completionReceipt;
|
|
94
|
+
if (receipt.workId !== contract.workId) throw new Error('WORK_COMPLETION_RECEIPT_IDENTITY_MISMATCH');
|
|
95
|
+
if (isRepositoryCompletionReceipt(receipt)) {
|
|
96
|
+
if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
|
|
97
|
+
if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
|
|
98
|
+
if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
|
|
99
|
+
} else if (isDirectEditWorkCompletionReceipt(receipt)) {
|
|
100
|
+
const directEditAuthorityCount = Number(Boolean(receipt.editSessionId?.trim())) + Number(Boolean(receipt.reconciliationId?.trim()));
|
|
101
|
+
if (directEditAuthorityCount !== 1) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_AUTHORITY_REQUIRED');
|
|
102
|
+
if (!receipt.targetBranch.trim() || !receipt.targetRevision.trim()) throw new Error('WORK_COMPLETION_RECEIPT_TARGET_REQUIRED');
|
|
103
|
+
if (receipt.delivery.status !== 'integrated' || !receipt.delivery.reachable) throw new Error('WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN');
|
|
104
|
+
if (!['complete', 'maintenance_warning'].includes(receipt.cleanup.status) || receipt.cleanup.blockers.length > 0) throw new Error('WORK_COMPLETION_RECEIPT_CLEANUP_NOT_PROVEN');
|
|
105
|
+
if (!['repository_change', 'completed_no_change'].includes(contract.workKind)) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_KIND_REQUIRED');
|
|
106
|
+
const expectedOutcome = contract.workKind === 'completed_no_change' ? 'completed_no_change' : 'completed_changed';
|
|
107
|
+
if (contract.completionOutcome !== expectedOutcome) throw new Error('WORK_COMPLETION_RECEIPT_DIRECT_EDIT_OUTCOME_REQUIRED');
|
|
108
|
+
} else if (isReadOnlyReviewCompletionReceipt(receipt)) {
|
|
109
|
+
if (contract.workKind !== 'read_only_review') throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_KIND_REQUIRED');
|
|
110
|
+
if (contract.completionOutcome !== 'completed_no_change') throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_OUTCOME_REQUIRED');
|
|
111
|
+
if (!receipt.baseRevision.trim() || receipt.baseRevision !== receipt.sourceRevision) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_SOURCE_REQUIRED');
|
|
112
|
+
if (contract.baseRevision && receipt.baseRevision !== contract.baseRevision) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_BASE_MISMATCH');
|
|
113
|
+
if (receipt.workspaceChangedPaths.length !== 0) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_WORKSPACE_CHANGED');
|
|
114
|
+
const reviewEvidence = contract.readOnlyReviewEvidence;
|
|
115
|
+
if (!reviewEvidence || reviewEvidence.sourceRevision !== receipt.sourceRevision) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_EVIDENCE_REQUIRED');
|
|
116
|
+
if (reviewEvidence.findings.length !== 0 || receipt.findingCount !== 0 || receipt.inspectedPaths.length === 0) throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_CLEAN_SCOPE_REQUIRED');
|
|
117
|
+
const evidencePaths = [...new Set(reviewEvidence.inspectedPaths)].sort();
|
|
118
|
+
const receiptPaths = [...new Set(receipt.inspectedPaths)].sort();
|
|
119
|
+
if (evidencePaths.length !== receiptPaths.length || evidencePaths.some((path, index) => path !== receiptPaths[index])) {
|
|
120
|
+
throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_SCOPE_MISMATCH');
|
|
121
|
+
}
|
|
122
|
+
if (reviewEvidence.workspaceFingerprint && receipt.workspaceFingerprint && reviewEvidence.workspaceFingerprint !== receipt.workspaceFingerprint) {
|
|
123
|
+
throw new Error('WORK_COMPLETION_RECEIPT_READ_ONLY_REVIEW_WORKSPACE_MISMATCH');
|
|
124
|
+
}
|
|
125
|
+
} else if (isRemoteEffectCompletionReceipt(receipt)) {
|
|
126
|
+
if (contract.workKind !== 'remote_effect') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_KIND_REQUIRED');
|
|
127
|
+
if (contract.completionOutcome !== 'completed_remote') throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_OUTCOME_REQUIRED');
|
|
128
|
+
const authority = receipt.authority ?? 'plugin_action';
|
|
129
|
+
if (!receipt.actionId.trim() || !receipt.requestId.trim() || !receipt.semanticKey.trim() || !receipt.resultDigest.trim()) {
|
|
130
|
+
throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_IDENTITY_REQUIRED');
|
|
131
|
+
}
|
|
132
|
+
if (authority === 'plugin_action' && !receipt.pluginId?.trim()) {
|
|
133
|
+
throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_PLUGIN_IDENTITY_REQUIRED');
|
|
134
|
+
}
|
|
135
|
+
if (authority === 'repository_process' && !receipt.processId?.trim()) {
|
|
136
|
+
throw new Error('WORK_COMPLETION_RECEIPT_REMOTE_EFFECT_PROCESS_IDENTITY_REQUIRED');
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
if (contract.workKind !== 'local_effect') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_KIND_REQUIRED');
|
|
140
|
+
if (contract.completionOutcome !== 'completed_local') throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_OUTCOME_REQUIRED');
|
|
141
|
+
if (!receipt.operation.trim() || !receipt.target.id.trim()) throw new Error('WORK_COMPLETION_RECEIPT_LOCAL_EFFECT_TARGET_REQUIRED');
|
|
142
|
+
}
|
|
143
|
+
if (contract.status !== 'completed') throw new Error('WORK_COMPLETION_RECEIPT_REQUIRES_COMPLETED_WORK');
|
|
144
|
+
for (const phase of ['implementation', 'verification', 'delivery', 'cleanup'] as WorkPhase[]) {
|
|
145
|
+
if (!['satisfied', 'skipped'].includes(contract.phaseEvidence[phase].state)) {
|
|
146
|
+
throw new Error(`WORK_COMPLETION_PHASE_NOT_SATISFIED: ${phase}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const phase of ['delivery', 'cleanup'] as WorkPhase[]) {
|
|
150
|
+
if (contract.phaseEvidence[phase].receiptId !== receipt.receiptId) {
|
|
151
|
+
throw new Error(`WORK_COMPLETION_PHASE_RECEIPT_MISMATCH: ${phase}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const outcome = contract.completionOutcome;
|
|
156
|
+
if (!outcome) return contract;
|
|
157
|
+
if (contract.status !== 'completed' || !contract.completionReceipt) {
|
|
158
|
+
throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires a completed Work receipt`);
|
|
159
|
+
}
|
|
160
|
+
if (contract.dispatchState !== 'terminal') {
|
|
161
|
+
throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires terminal dispatch`);
|
|
162
|
+
}
|
|
163
|
+
if (contract.evidenceState !== 'valid') {
|
|
164
|
+
throw new Error(`WORK_SEMANTICS_INVALID: ${outcome} requires valid evidence`);
|
|
165
|
+
}
|
|
166
|
+
if (outcome === 'completed_changed' && contract.workKind !== 'repository_change') {
|
|
167
|
+
throw new Error('WORK_SEMANTICS_INVALID: completed_changed requires repository_change WorkKind');
|
|
168
|
+
}
|
|
169
|
+
if (outcome === 'completed_no_change' && !['completed_no_change', 'read_only_review'].includes(contract.workKind)) {
|
|
170
|
+
throw new Error('WORK_SEMANTICS_INVALID: completed_no_change requires completed_no_change or read_only_review WorkKind');
|
|
171
|
+
}
|
|
172
|
+
if (outcome === 'completed_no_change' && contract.workKind === 'read_only_review' && contract.completionReceipt?.source !== 'read_only_review') {
|
|
173
|
+
throw new Error('WORK_SEMANTICS_INVALID: read_only_review completed_no_change requires a read_only_review completion receipt');
|
|
174
|
+
}
|
|
175
|
+
if (outcome === 'completed_local' && contract.workKind !== 'local_effect') {
|
|
176
|
+
throw new Error('WORK_SEMANTICS_INVALID: completed_local requires local_effect WorkKind');
|
|
177
|
+
}
|
|
178
|
+
if (outcome === 'completed_local' && contract.completionReceipt?.source !== 'local_effect') {
|
|
179
|
+
throw new Error('WORK_SEMANTICS_INVALID: completed_local requires a local_effect completion receipt');
|
|
180
|
+
}
|
|
181
|
+
if (outcome === 'completed_remote' && contract.workKind !== 'remote_effect') {
|
|
182
|
+
throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires remote_effect WorkKind');
|
|
183
|
+
}
|
|
184
|
+
if (outcome === 'completed_remote' && contract.completionReceipt?.source !== 'remote_effect') {
|
|
185
|
+
throw new Error('WORK_SEMANTICS_INVALID: completed_remote requires a remote_effect completion receipt');
|
|
186
|
+
}
|
|
187
|
+
if (outcome === 'superseded' && contract.workKind !== 'superseded') {
|
|
188
|
+
throw new Error('WORK_SEMANTICS_INVALID: superseded requires superseded WorkKind');
|
|
189
|
+
}
|
|
190
|
+
return contract;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const DISPATCH_TRANSITIONS: Readonly<Record<DispatchState, readonly DispatchState[]>> = {
|
|
194
|
+
not_dispatched: ['not_dispatched', 'claimed', 'launching', 'running', 'blocked', 'terminal'],
|
|
195
|
+
claimed: ['claimed', 'launching', 'running', 'blocked', 'terminal'],
|
|
196
|
+
launching: ['launching', 'running', 'blocked', 'terminal'],
|
|
197
|
+
running: ['running', 'blocked', 'terminal'],
|
|
198
|
+
blocked: ['blocked', 'claimed', 'launching', 'running', 'terminal'],
|
|
199
|
+
terminal: ['terminal'],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const EVIDENCE_TRANSITIONS: Readonly<Record<EvidenceState, readonly EvidenceState[]>> = {
|
|
203
|
+
none: ['none', 'partial', 'valid', 'failed'],
|
|
204
|
+
partial: ['partial', 'valid', 'stale', 'contradictory', 'failed'],
|
|
205
|
+
valid: ['valid', 'stale', 'contradictory', 'failed'],
|
|
206
|
+
stale: ['stale', 'partial', 'valid', 'contradictory', 'failed'],
|
|
207
|
+
contradictory: ['contradictory', 'partial', 'valid', 'failed'],
|
|
208
|
+
failed: ['failed', 'partial', 'valid', 'contradictory'],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export function validateWorkSemanticTransition(
|
|
212
|
+
current: WorkContract,
|
|
213
|
+
next: WorkContract,
|
|
214
|
+
options: { allowRetainedCancelledResume?: boolean } = {},
|
|
215
|
+
): WorkContract {
|
|
216
|
+
const retryingFailedWork = current.status === 'failed'
|
|
217
|
+
&& !current.completionOutcome
|
|
218
|
+
&& ['claimed', 'launching', 'running', 'blocked'].includes(next.dispatchState);
|
|
219
|
+
const resumingRetainedCancelledWork = options.allowRetainedCancelledResume === true
|
|
220
|
+
&& current.status === 'cancelled'
|
|
221
|
+
&& current.dispatchState === 'terminal'
|
|
222
|
+
&& current.phase === 'cleanup'
|
|
223
|
+
&& current.phaseEvidence.cleanup.state === 'skipped'
|
|
224
|
+
&& !current.completionReceipt
|
|
225
|
+
&& !current.completionOutcome
|
|
226
|
+
&& next.status === 'running'
|
|
227
|
+
&& next.dispatchState === 'running'
|
|
228
|
+
&& next.phase === 'implementation';
|
|
229
|
+
if (!retryingFailedWork && !resumingRetainedCancelledWork && !DISPATCH_TRANSITIONS[current.dispatchState].includes(next.dispatchState)) {
|
|
230
|
+
throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: dispatch ${current.dispatchState} -> ${next.dispatchState}`);
|
|
231
|
+
}
|
|
232
|
+
if (!EVIDENCE_TRANSITIONS[current.evidenceState].includes(next.evidenceState)) {
|
|
233
|
+
throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: evidence ${current.evidenceState} -> ${next.evidenceState}`);
|
|
234
|
+
}
|
|
235
|
+
if (current.completionOutcome && current.completionOutcome !== next.completionOutcome) {
|
|
236
|
+
throw new Error(`WORK_SEMANTICS_TRANSITION_INVALID: completion outcome ${current.completionOutcome} is immutable`);
|
|
237
|
+
}
|
|
238
|
+
if ((current.lifecycleRole ?? 'primary') !== (next.lifecycleRole ?? 'primary')) {
|
|
239
|
+
throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: lifecycleRole is immutable');
|
|
240
|
+
}
|
|
241
|
+
if (current.parentWorkId !== next.parentWorkId) {
|
|
242
|
+
throw new Error('WORK_SEMANTICS_TRANSITION_INVALID: parentWorkId is immutable');
|
|
243
|
+
}
|
|
244
|
+
return next;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function suggestedActionsForStatus(status: WorkContractStatus, actions: readonly SuggestedNextAction[]): SuggestedNextAction[] {
|
|
248
|
+
if (status === 'completed' || status === 'cancelled') return [];
|
|
249
|
+
return actions.slice(0, 8);
|
|
250
|
+
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
finishControllerRoundRelayDispatch,
|
|
12
12
|
} from '../facade/controller-round-relay';
|
|
13
13
|
import { assertAutomatedOperationAllowed } from '../governance/external-effects';
|
|
14
|
-
import {
|
|
14
|
+
import { runWorkChatgptContinuation } from '../launcher/chatgpt-work-continuation';
|
|
15
15
|
|
|
16
16
|
export async function runSchedulerPeriodicCleanup(input: {
|
|
17
17
|
controllerHome: string;
|
|
@@ -83,13 +83,13 @@ export async function runSchedulerValidationReconciliation(input: {
|
|
|
83
83
|
export async function runSchedulerControllerRoundRecovery(input: {
|
|
84
84
|
controllerHome: string;
|
|
85
85
|
nowMs: number;
|
|
86
|
-
repositories: readonly Pick<RepositoryRecord, 'repoId'>[];
|
|
86
|
+
repositories: readonly Pick<RepositoryRecord, 'repoId' | 'canonicalRoot' | 'localRoot'>[];
|
|
87
87
|
graceMs?: number;
|
|
88
88
|
maxRecoveries?: number;
|
|
89
|
-
dispatchPrompt?: typeof
|
|
89
|
+
dispatchPrompt?: typeof runWorkChatgptContinuation;
|
|
90
90
|
authorizeWake?: typeof assertAutomatedOperationAllowed;
|
|
91
91
|
}): Promise<{ claimed: number; dispatched: number; failed: number }> {
|
|
92
|
-
const dispatchPrompt = input.dispatchPrompt ??
|
|
92
|
+
const dispatchPrompt = input.dispatchPrompt ?? runWorkChatgptContinuation;
|
|
93
93
|
const authorizeWake = input.authorizeWake ?? assertAutomatedOperationAllowed;
|
|
94
94
|
const maxRecoveries = Math.max(1, Math.min(Math.trunc(input.maxRecoveries ?? 2), 8));
|
|
95
95
|
let claimed = 0;
|
|
@@ -116,8 +116,9 @@ export async function runSchedulerControllerRoundRecovery(input: {
|
|
|
116
116
|
const result = await dispatchPrompt({
|
|
117
117
|
controllerHome: input.controllerHome,
|
|
118
118
|
repoId: repository.repoId,
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
repoRoot: repository.canonicalRoot ?? repository.localRoot,
|
|
120
|
+
workId: record.originWorkId,
|
|
121
|
+
prompt: buildControllerRoundRelayPrompt(store, record, { exactOriginWork: true }),
|
|
121
122
|
browserSessionId: record.browserSessionId,
|
|
122
123
|
conversationUrl: record.conversationUrl,
|
|
123
124
|
model: 'gpt-5.6',
|
|
@@ -135,7 +136,13 @@ export async function runSchedulerControllerRoundRecovery(input: {
|
|
|
135
136
|
dispatched += 1;
|
|
136
137
|
} catch (error) {
|
|
137
138
|
const reason = error instanceof Error ? error.message : String(error);
|
|
138
|
-
finishControllerRoundRelayDispatch(store, {
|
|
139
|
+
finishControllerRoundRelayDispatch(store, {
|
|
140
|
+
workId: record.originWorkId,
|
|
141
|
+
ok: false,
|
|
142
|
+
error: reason,
|
|
143
|
+
recovery: true,
|
|
144
|
+
nowMs: input.nowMs,
|
|
145
|
+
});
|
|
139
146
|
failed += 1;
|
|
140
147
|
console.error(`[forge controller relay] stalled round recovery failed for ${record.relayScopeId}:`, reason);
|
|
141
148
|
}
|
|
@@ -199,8 +199,12 @@ export class GlobalScheduler {
|
|
|
199
199
|
) {
|
|
200
200
|
this.controllerHome = controllerHome;
|
|
201
201
|
this.controllerPid = runtime.controllerPid ?? process.pid;
|
|
202
|
-
this.actors = new RepoActorRegistry(controllerHome);
|
|
203
202
|
this.config = normalizeSchedulerConfig(config);
|
|
203
|
+
// The per-repository mailbox must not impose a hidden concurrency ceiling
|
|
204
|
+
// below the scheduler's global worker budget. Resource leases still serialize
|
|
205
|
+
// conflicting workspace/path/integration claims, while independent jobs in a
|
|
206
|
+
// busy single repository may use the available global worker capacity.
|
|
207
|
+
this.actors = new RepoActorRegistry(controllerHome, { maxConcurrentWorkers: this.config.maxWorkers });
|
|
204
208
|
this.runtimeSourceRoot = runtime.runtimeSourceRoot ? resolve(runtime.runtimeSourceRoot) : undefined;
|
|
205
209
|
this.workerEntrypoint = runtime.workerEntrypoint ? resolve(runtime.workerEntrypoint) : undefined;
|
|
206
210
|
this.fatalOnTickError = runtime.fatalOnTickError === true;
|
|
@@ -47,6 +47,19 @@ export function parseChatgptConversationIdentity(value: string): { conversationU
|
|
|
47
47
|
return { conversationUrl: url.toString(), conversationId };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function hasChatgptConversationIdentity(value: string): boolean {
|
|
51
|
+
try {
|
|
52
|
+
parseChatgptConversationIdentity(value);
|
|
53
|
+
return true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// ChatGPT root and Project URLs are valid launch seeds, but they are not
|
|
56
|
+
// durable conversation identities. Persist only the /c/<id> URL observed
|
|
57
|
+
// after the browser confirms submission.
|
|
58
|
+
if (error instanceof Error && error.message === 'CHATGPT_WORK_CONVERSATION_ID_MISSING') return false;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
function record(options: ChatgptWorkBindingStoreOptions, workId: string) {
|
|
51
64
|
return readControlPlaneRecord<ChatgptWorkConversationBinding>(
|
|
52
65
|
options.controllerHome,
|
|
@@ -5,6 +5,7 @@ import { getWorkContract } from '../facade/work-contract-store';
|
|
|
5
5
|
import {
|
|
6
6
|
bindChatgptWorkConversation,
|
|
7
7
|
getChatgptWorkConversationBinding,
|
|
8
|
+
hasChatgptConversationIdentity,
|
|
8
9
|
parseChatgptConversationIdentity,
|
|
9
10
|
rebindChatgptWorkConversation,
|
|
10
11
|
type ChatgptWorkConversationBinding,
|
|
@@ -256,7 +257,7 @@ async function findChatgptIntelligenceControl(
|
|
|
256
257
|
// Prefer the composer/main region so sidebar history cannot crowd the
|
|
257
258
|
// reasoning control out of a bounded query. Keep a larger global fallback
|
|
258
259
|
// for ChatGPT layouts that place the control outside <main>.
|
|
259
|
-
limit: selector
|
|
260
|
+
limit: chatgptAutomationControlQueryLimit(selector),
|
|
260
261
|
timeout_ms: timeoutMs ?? 60_000,
|
|
261
262
|
}, timeoutMs);
|
|
262
263
|
const match = queryMatches(result).find((candidate) => {
|
|
@@ -268,12 +269,19 @@ async function findChatgptIntelligenceControl(
|
|
|
268
269
|
return undefined;
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
export function chatgptAutomationControlQueryLimit(selector: string): number {
|
|
273
|
+
// Long tool-heavy conversations can contribute well over 80 buttons before
|
|
274
|
+
// the composer controls. Keep the preferred <main> scan bounded but large
|
|
275
|
+
// enough that the current reasoning control is not crowded out by history.
|
|
276
|
+
return selector.startsWith('main ') ? 160 : 320;
|
|
277
|
+
}
|
|
278
|
+
|
|
271
279
|
export function chatgptAutomationControlWaitBudgets(timeoutMs?: number): { waitBudgetMs: number; probeTimeoutMs: number } {
|
|
272
280
|
const waitBudgetMs = Math.min(Math.max(timeoutMs ?? 30_000, 1_000), 30_000);
|
|
273
|
-
//
|
|
274
|
-
// Keep each
|
|
275
|
-
//
|
|
276
|
-
return { waitBudgetMs, probeTimeoutMs: Math.min(waitBudgetMs,
|
|
281
|
+
// Native Chrome attachment has a few seconds of Apple Events/DOM latency on
|
|
282
|
+
// long conversations. Keep each probe bounded while allowing a real query to
|
|
283
|
+
// complete; the total readiness window remains capped at 30 seconds.
|
|
284
|
+
return { waitBudgetMs, probeTimeoutMs: Math.min(waitBudgetMs, 5_000) };
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
async function waitForChatgptIntelligenceControl(
|
|
@@ -433,6 +441,7 @@ export function chatgptAutomationNavigationRequiresReplacement(error: unknown):
|
|
|
433
441
|
'BROWSER_AUTOMATION_BACKGROUND_NAVIGATION_REQUIRES_REPLACEMENT',
|
|
434
442
|
'PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
435
443
|
'PLUGIN_SESSION_NOT_FOUND',
|
|
444
|
+
'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN',
|
|
436
445
|
].some((marker) => error.message.includes(marker));
|
|
437
446
|
}
|
|
438
447
|
|
|
@@ -685,7 +694,7 @@ export async function runWorkChatgptContinuation(input: WorkChatgptContinuationI
|
|
|
685
694
|
throw new Error(`CHATGPT_WORK_CONTRACT_NOT_FOUND: ${input.repoId}:${input.workId}`);
|
|
686
695
|
}
|
|
687
696
|
await ensureControllerChatgptBrowser(input.controllerHome, input.workId);
|
|
688
|
-
if (seedUrl && !binding) {
|
|
697
|
+
if (seedUrl && !binding && hasChatgptConversationIdentity(seedUrl)) {
|
|
689
698
|
binding = bindChatgptWorkConversation(store, {
|
|
690
699
|
workId: input.workId,
|
|
691
700
|
conversationUrl: seedUrl,
|