@cassiomc1/forgeloop 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +46 -12
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +418 -9
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +78 -52
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +22 -4
- package/completions/forgeloop.bash +40 -4
- package/completions/forgeloop.fish +130 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +392 -10
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -12
- package/docs/GETTING_STARTED.md +59 -0
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +60 -19
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +32 -0
- package/docs/RELEASE_CHECKLIST.md +66 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +298 -3
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +1397 -0
- package/docs/protocol-requirements.json +101 -0
- package/package.json +46 -4
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/routing-input.schema.json +1 -1
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +40 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/next.js +19 -7
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/config/guides.json +44 -0
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +249 -1
- package/src/core/command-executors.js +115 -3
- package/src/core/command-input.js +212 -102
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-presets.js +82 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +281 -3
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/filesystem.js +1 -10
- package/src/core/gate-provenance.js +124 -0
- package/src/core/go-project.js +206 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/java-project.js +403 -0
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/multi-language-project.js +117 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/next-explanation.js +63 -0
- package/src/core/phase.js +128 -10
- package/src/core/php-project.js +85 -0
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/project-detection.js +1760 -52
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +132 -53
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +223 -4
- package/src/core/runtime-context.js +118 -61
- package/src/core/rust-project.js +400 -0
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
|
@@ -4,6 +4,8 @@ import { PUBLIC_ERROR_REGISTRY } from "./error-codes.js";
|
|
|
4
4
|
import { GUIDE_REGISTRY } from "./guide-registry.js";
|
|
5
5
|
import { PROTOCOL_VERSION, WORK_PHASES, WORK_TRANSITIONS } from "./protocol.js";
|
|
6
6
|
import { VERIFICATION_ISOLATION_MODES } from "./verification-execution.js";
|
|
7
|
+
import { PROVIDER_KINDS } from "../providers/capabilities.js";
|
|
8
|
+
import { DECISION_DEFAULT_POLICY, DECISION_ENGINE_ID, PINNED_JEV_MODEL } from "./decision/constants.js";
|
|
7
9
|
|
|
8
10
|
export const SCHEMA_COMPATIBILITY_POLICY = Object.freeze({
|
|
9
11
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -36,6 +38,42 @@ export function protocolInfo({ packageVersion = null } = {}) {
|
|
|
36
38
|
writesSchemaVersions: schemaVersions,
|
|
37
39
|
compatibility: SCHEMA_COMPATIBILITY_POLICY,
|
|
38
40
|
features: {
|
|
41
|
+
semanticDecisionPlane: {
|
|
42
|
+
version: 1,
|
|
43
|
+
supported: true,
|
|
44
|
+
requiredForNewSemanticDecisions: true,
|
|
45
|
+
engine: DECISION_ENGINE_ID,
|
|
46
|
+
model: PINNED_JEV_MODEL,
|
|
47
|
+
policyVersion: DECISION_DEFAULT_POLICY.policyVersion,
|
|
48
|
+
authority: "SEMANTIC_DECISION",
|
|
49
|
+
evidenceAuthority: "NONE",
|
|
50
|
+
lifecycleAuthority: false,
|
|
51
|
+
completionAuthority: false,
|
|
52
|
+
ownershipAuthority: false,
|
|
53
|
+
installationAuthority: false,
|
|
54
|
+
commands: ["decision-status", "decision-show", "context-plan", "model-route", "semantic-plan", "test-inventory", "test-utility", "test-prune-plan", "test-prune-probe"],
|
|
55
|
+
resource: "task/decisions",
|
|
56
|
+
contextPlanResource: "task/context-plan",
|
|
57
|
+
modelRouteResource: "task/model-route",
|
|
58
|
+
testUtilityResource: "task/test-utility",
|
|
59
|
+
testPruneResource: "task/test-utility",
|
|
60
|
+
offlineInspection: true,
|
|
61
|
+
completePerformsLiveRequest: false,
|
|
62
|
+
modelRouting: {
|
|
63
|
+
version: 1,
|
|
64
|
+
tiers: ["NONE", "FAST", "STANDARD", "PRIMARY"],
|
|
65
|
+
deterministicFloor: true,
|
|
66
|
+
advisoryEscalationOnly: true,
|
|
67
|
+
vendorSelection: false,
|
|
68
|
+
},
|
|
69
|
+
semanticPlanning: {
|
|
70
|
+
version: 1,
|
|
71
|
+
questionSets: ["intake-v1", "contract-v1", "route-v1", "context-v1", "model-route-v1", "failure-v1", "diagnosis-v1", "review-v1", "task-overlap-v1", "test-utility-v1", "test-prune-v1"],
|
|
72
|
+
authority: "SEMANTIC_DECISION",
|
|
73
|
+
evidenceAuthority: "NONE",
|
|
74
|
+
lifecycleAuthority: false,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
39
77
|
taskClaimRecovery: {
|
|
40
78
|
version: 1,
|
|
41
79
|
durableRecoveryState: true,
|
|
@@ -153,6 +191,18 @@ export function protocolInfo({ packageVersion = null } = {}) {
|
|
|
153
191
|
commands: ["next", "task-show"],
|
|
154
192
|
preservesDefaultOutput: true,
|
|
155
193
|
},
|
|
194
|
+
auditUx: {
|
|
195
|
+
version: 1,
|
|
196
|
+
supported: true,
|
|
197
|
+
readOnly: true,
|
|
198
|
+
resource: "task/audit-view",
|
|
199
|
+
timeline: true,
|
|
200
|
+
lifecycleAuthority: false,
|
|
201
|
+
evidenceAuthority: false,
|
|
202
|
+
completionAuthority: false,
|
|
203
|
+
mutationAuthority: false,
|
|
204
|
+
externalExecution: false,
|
|
205
|
+
},
|
|
156
206
|
usageTelemetry: {
|
|
157
207
|
version: 1,
|
|
158
208
|
supported: true,
|
|
@@ -205,6 +255,21 @@ export function protocolInfo({ packageVersion = null } = {}) {
|
|
|
205
255
|
evidenceAuthority: false,
|
|
206
256
|
executable: false,
|
|
207
257
|
},
|
|
258
|
+
providerExtensions: {
|
|
259
|
+
version: 1,
|
|
260
|
+
supported: true,
|
|
261
|
+
providerNeutral: true,
|
|
262
|
+
maturity: "experimental",
|
|
263
|
+
publicRegistryApi: false,
|
|
264
|
+
packageSubpathExported: false,
|
|
265
|
+
autoInstall: false,
|
|
266
|
+
lifecycleAuthority: false,
|
|
267
|
+
completionAuthority: false,
|
|
268
|
+
evidenceAuthority: false,
|
|
269
|
+
providerKinds: [...PROVIDER_KINDS],
|
|
270
|
+
resultBoundary: "STRICT_JSON_SNAPSHOT",
|
|
271
|
+
cancellation: "COOPERATIVE_ABORT_SIGNAL",
|
|
272
|
+
},
|
|
208
273
|
responsibilityConstraints: {
|
|
209
274
|
version: 1,
|
|
210
275
|
supported: true,
|
package/src/core/protocol.js
CHANGED
|
@@ -30,6 +30,9 @@ export const FAILURE_CODES = Object.freeze([
|
|
|
30
30
|
"E_GATE_REQUIRED",
|
|
31
31
|
"E_GATE_UNVERIFIED",
|
|
32
32
|
"E_GATE_STALE",
|
|
33
|
+
"E_GATE_NOT_REQUIRED",
|
|
34
|
+
"E_GATE_INVALID",
|
|
35
|
+
"E_PHASE_FREEZE",
|
|
33
36
|
"E_PHASE_TRANSITION_INVALID",
|
|
34
37
|
"E_PHASE_PREREQUISITE_MISSING",
|
|
35
38
|
"E_PHASE_CHRONOLOGY_INVALID",
|
|
@@ -113,6 +116,23 @@ export const FAILURE_CODES = Object.freeze([
|
|
|
113
116
|
"E_INTERVENTION_HYPOTHESIS_MISSING",
|
|
114
117
|
"E_INTERVENTION_REFERENCE_INVALID",
|
|
115
118
|
"E_STRATEGY_OSCILLATION",
|
|
119
|
+
"E_DECISION_ENGINE_UNAVAILABLE",
|
|
120
|
+
"E_DECISION_ENGINE_AUTH_REQUIRED",
|
|
121
|
+
"E_DECISION_ENGINE_AUTH_INVALID",
|
|
122
|
+
"E_DECISION_MODEL_UNSUPPORTED",
|
|
123
|
+
"E_DECISION_REQUEST_INVALID",
|
|
124
|
+
"E_DECISION_RESULT_INVALID",
|
|
125
|
+
"E_DECISION_TIMEOUT",
|
|
126
|
+
"E_DECISION_RATE_LIMITED",
|
|
127
|
+
"E_DECISION_STATE_UNSAFE",
|
|
128
|
+
"E_DECISION_STATE_LIMIT",
|
|
129
|
+
"E_DECISION_LOW_CONFIDENCE",
|
|
130
|
+
"E_DECISION_STALE",
|
|
131
|
+
"E_DECISION_POLICY_INVALID",
|
|
132
|
+
"E_DECISION_CACHE_INVALID",
|
|
133
|
+
"E_DECISION_QUESTION_SET_UNKNOWN",
|
|
134
|
+
"E_DECISION_QUESTION_SET_STALE",
|
|
135
|
+
"E_DECISION_REQUIRED",
|
|
116
136
|
"E_DECISION_CRITERION_INVALID",
|
|
117
137
|
"E_DECISION_NOT_UNRESOLVED",
|
|
118
138
|
]);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { readContract } from "./contract.js";
|
|
2
2
|
import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
|
-
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
3
|
+
import { appendProtocolEvent, validateCompletionRecoveryAuthorization, validateEventLedger } from "./events.js";
|
|
4
4
|
import { authorizeCompletionRecoveryOrRebind } from "./completion-recovery-rebind.js";
|
|
5
5
|
import { runCommandExecution } from "./execution.js";
|
|
6
6
|
import { createReceipt } from "./receipt.js";
|
|
7
7
|
import { currentRepositoryFingerprint } from "./repository.js";
|
|
8
8
|
import { taskArtifactPath } from "./task-paths.js";
|
|
9
|
+
import { resolveTaskClaimState } from "./task-claim-state.js";
|
|
9
10
|
import { classifyLoadedWorkState, readWorkState, mutateWorkState } from "./work-state.js";
|
|
11
|
+
import { classifyRequirement } from "./evidence-readiness.js";
|
|
10
12
|
|
|
11
13
|
export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
|
|
12
14
|
|
|
13
|
-
const RECONCILABLE_DRIFT = new Set(["REPOSITORY_CHANGED"]);
|
|
14
|
-
|
|
15
15
|
const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "REVIEWING"]);
|
|
16
16
|
|
|
17
17
|
function reconcileError(code, message, artifacts = []) {
|
|
@@ -21,14 +21,126 @@ function reconcileError(code, message, artifacts = []) {
|
|
|
21
21
|
return error;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function assertRepositoryOnlyFreshness(freshness, stateRel, contractRel) {
|
|
25
|
+
if (freshness.status !== "REVALIDATION_REQUIRED") {
|
|
26
|
+
throw reconcileError(
|
|
27
|
+
"E_RECONCILE_NOT_STALE",
|
|
28
|
+
`work-state checkpoint is ${freshness.status === "FRESH" ? "fresh" : "not revalidation-required"}; no reconciliation required`,
|
|
29
|
+
[stateRel, contractRel],
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (freshness.reasons.length === 1 && freshness.reasons[0] === "REPOSITORY_CHANGED") return;
|
|
33
|
+
throw reconcileError(
|
|
34
|
+
"E_RECONCILE_UNSUPPORTED_DRIFT",
|
|
35
|
+
`reconcile-closure only reconciles repository fingerprint drift; unresolved drift: ${freshness.reasons.join(", ")}`,
|
|
36
|
+
[stateRel, contractRel],
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function assertFreshReviewingRecoveryIsAuthorized({
|
|
41
|
+
target,
|
|
42
|
+
packageRoot,
|
|
43
|
+
taskId,
|
|
44
|
+
state,
|
|
45
|
+
eventsRel,
|
|
46
|
+
receiptRel,
|
|
47
|
+
}) {
|
|
48
|
+
if (state.phase !== "REVIEWING" || !Object.prototype.hasOwnProperty.call(state, "lastCompletionAttempt")) return;
|
|
49
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId });
|
|
50
|
+
if (!ledger.valid) return;
|
|
51
|
+
let receipt = null;
|
|
52
|
+
try {
|
|
53
|
+
receipt = (await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot)).value;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
56
|
+
}
|
|
57
|
+
const recoveryAuth = validateCompletionRecoveryAuthorization({ state, receipt, events: ledger.events });
|
|
58
|
+
if (!recoveryAuth.authorized) {
|
|
59
|
+
const first = recoveryAuth.errors?.[0] ?? {};
|
|
60
|
+
throw reconcileError(
|
|
61
|
+
first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
|
|
62
|
+
`REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
|
|
63
|
+
[eventsRel, receiptRel],
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function validateReconciliationCheckpoint({
|
|
69
|
+
target,
|
|
70
|
+
packageRoot,
|
|
71
|
+
taskId,
|
|
72
|
+
state,
|
|
73
|
+
stateRel,
|
|
74
|
+
contractRel,
|
|
75
|
+
eventsRel,
|
|
76
|
+
receiptRel,
|
|
77
|
+
authorityContext,
|
|
78
|
+
runtimeContext,
|
|
79
|
+
}) {
|
|
80
|
+
const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
|
|
81
|
+
if (freshness.status !== "REVALIDATION_REQUIRED") {
|
|
82
|
+
await assertFreshReviewingRecoveryIsAuthorized({
|
|
83
|
+
target,
|
|
84
|
+
packageRoot,
|
|
85
|
+
taskId,
|
|
86
|
+
state,
|
|
87
|
+
eventsRel,
|
|
88
|
+
receiptRel,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
assertRepositoryOnlyFreshness(freshness, stateRel, contractRel);
|
|
92
|
+
|
|
93
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId });
|
|
94
|
+
if (!ledger.valid) {
|
|
95
|
+
const first = ledger.errors[0];
|
|
96
|
+
throw reconcileError(
|
|
97
|
+
"E_RECONCILE_LEDGER_INVALID",
|
|
98
|
+
`append-only event ledger must be valid before reconciliation: ${first?.message ?? "invalid ledger"}`,
|
|
99
|
+
[eventsRel],
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
|
|
104
|
+
if (!ownership.ownershipValid || !ownership.mutationAllowed || ownership.claimState !== "ACTIVE") {
|
|
105
|
+
const first = ownership.ownershipErrors?.[0] ?? ownership.errors?.[0] ?? {};
|
|
106
|
+
throw reconcileError(
|
|
107
|
+
first.code ?? "E_TASK_CLAIM_OWNERSHIP_INCONSISTENT",
|
|
108
|
+
`reconcile-closure requires active, valid task claim ownership: ${first.message ?? ownership.claimState}`,
|
|
109
|
+
[stateRel, eventsRel],
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (state.phase !== "REVIEWING" || !Object.prototype.hasOwnProperty.call(state, "lastCompletionAttempt")) {
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const recovery = await authorizeCompletionRecoveryOrRebind({
|
|
118
|
+
target,
|
|
119
|
+
packageRoot,
|
|
120
|
+
taskId,
|
|
121
|
+
authorityContext,
|
|
122
|
+
runtimeContext,
|
|
123
|
+
});
|
|
124
|
+
if (!recovery.recoveryAuth.authorized) {
|
|
125
|
+
const first = recovery.recoveryAuth.errors?.[0] ?? {};
|
|
126
|
+
throw reconcileError(
|
|
127
|
+
first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
|
|
128
|
+
`REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
|
|
129
|
+
[stateRel, receiptRel],
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return recovery.rebound ? recovery.state : state;
|
|
133
|
+
}
|
|
134
|
+
|
|
24
135
|
/**
|
|
25
136
|
* Canonical recovery for an EXECUTING, VERIFYING, or REVIEWING task whose
|
|
26
137
|
* objective is already satisfied in the current repository but whose
|
|
27
138
|
* work-state checkpoint is stale because the repository fingerprint moved.
|
|
28
139
|
*
|
|
29
140
|
* The command refreshes the checkpoint repository fingerprint only after:
|
|
30
|
-
* - the task is EXECUTING, VERIFYING, or (with
|
|
31
|
-
*
|
|
141
|
+
* - the task is EXECUTING, VERIFYING, or REVIEWING (with either a
|
|
142
|
+
* persisted completion rejection or the narrow repository-only bootstrap
|
|
143
|
+
* path),
|
|
32
144
|
* - classification requires revalidation and the only drift is
|
|
33
145
|
* REPOSITORY_CHANGED,
|
|
34
146
|
* - the append-only event ledger is valid,
|
|
@@ -80,60 +192,27 @@ export async function runReconcileClosure({
|
|
|
80
192
|
[stateRel],
|
|
81
193
|
);
|
|
82
194
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
|
|
96
|
-
[stateRel, receiptRel],
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (recovery.rebound) {
|
|
100
|
-
state = recovery.state;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
|
|
105
|
-
if (freshness.status !== "REVALIDATION_REQUIRED" || !freshness.reasons.includes("REPOSITORY_CHANGED")) {
|
|
106
|
-
throw reconcileError(
|
|
107
|
-
"E_RECONCILE_NOT_STALE",
|
|
108
|
-
`work-state checkpoint is ${freshness.status === "FRESH" ? "fresh" : "not revalidation-required"}; no reconciliation required`,
|
|
109
|
-
[stateRel, contractRel],
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
const unsupported = freshness.reasons.filter((reason) => !RECONCILABLE_DRIFT.has(reason));
|
|
113
|
-
if (unsupported.length > 0) {
|
|
114
|
-
throw reconcileError(
|
|
115
|
-
"E_RECONCILE_UNSUPPORTED_DRIFT",
|
|
116
|
-
`reconcile-closure only reconciles repository fingerprint drift; unresolved drift: ${unsupported.join(", ")}`,
|
|
117
|
-
[stateRel, contractRel],
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const ledger = await validateEventLedger(target, packageRoot, { taskId });
|
|
122
|
-
if (!ledger.valid) {
|
|
123
|
-
const first = ledger.errors[0];
|
|
124
|
-
throw reconcileError(
|
|
125
|
-
"E_RECONCILE_LEDGER_INVALID",
|
|
126
|
-
`append-only event ledger must be valid before reconciliation: ${first?.message ?? "invalid ledger"}`,
|
|
127
|
-
[eventsRel],
|
|
128
|
-
);
|
|
129
|
-
}
|
|
195
|
+
state = await validateReconciliationCheckpoint({
|
|
196
|
+
target,
|
|
197
|
+
packageRoot,
|
|
198
|
+
taskId,
|
|
199
|
+
state,
|
|
200
|
+
stateRel,
|
|
201
|
+
contractRel,
|
|
202
|
+
eventsRel,
|
|
203
|
+
receiptRel,
|
|
204
|
+
authorityContext,
|
|
205
|
+
runtimeContext,
|
|
206
|
+
});
|
|
130
207
|
|
|
131
208
|
const contract = await readContract(target, packageRoot, { taskId });
|
|
132
209
|
const verificationItem = (contract.value.verification ?? []).find((item) => {
|
|
133
210
|
if (typeof item === "string") {
|
|
134
211
|
return item === requirement;
|
|
135
212
|
}
|
|
136
|
-
return item.type === "VERIFICATION"
|
|
213
|
+
return classifyRequirement(item).type === "VERIFICATION"
|
|
214
|
+
&& item.id === checkId
|
|
215
|
+
&& item.text === requirement;
|
|
137
216
|
});
|
|
138
217
|
if (!verificationItem) {
|
|
139
218
|
throw reconcileError(
|
package/src/core/resumability.js
CHANGED
|
@@ -4,6 +4,12 @@ import { createWorkState, initializeWorkState, readWorkState, mutateWorkState }
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_PENDING_STEPS = ["planning", "implementation", "verification"];
|
|
6
6
|
|
|
7
|
+
const CURRENT_ROUTE_CHECKPOINT_PHASES = new Set(["ROUTED"]);
|
|
8
|
+
|
|
9
|
+
export function routeCheckpointMustMatchCurrentRoute(phase) {
|
|
10
|
+
return CURRENT_ROUTE_CHECKPOINT_PHASES.has(phase);
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Resume phase derived from the highest lifecycle milestone already recorded in
|
|
9
15
|
* a validated ledger. Recreating a checkpoint at ROUTED for a task whose ledger
|
|
@@ -16,9 +22,88 @@ const RESUME_PHASE_BY_MILESTONE = Object.freeze({
|
|
|
16
22
|
EXECUTION_STARTED: "EXECUTING",
|
|
17
23
|
VERIFICATION_STARTED: "VERIFYING",
|
|
18
24
|
VERIFICATION_RECORDED: "VERIFYING",
|
|
25
|
+
REVIEW_STARTED: "REVIEWING",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const RESUME_STEPS_BY_PHASE = Object.freeze({
|
|
29
|
+
CONTRACT_READY: {
|
|
30
|
+
completedSteps: ["contract"],
|
|
31
|
+
pendingSteps: ["route", ...DEFAULT_PENDING_STEPS],
|
|
32
|
+
},
|
|
33
|
+
ROUTED: {
|
|
34
|
+
completedSteps: ["contract", "route"],
|
|
35
|
+
pendingSteps: [...DEFAULT_PENDING_STEPS],
|
|
36
|
+
},
|
|
37
|
+
PLANNED: {
|
|
38
|
+
completedSteps: ["contract", "route", "planning"],
|
|
39
|
+
pendingSteps: [...DEFAULT_PENDING_STEPS.filter((step) => step !== "planning")],
|
|
40
|
+
},
|
|
41
|
+
EXECUTING: {
|
|
42
|
+
completedSteps: ["contract", "route", "planning", "implementation"],
|
|
43
|
+
pendingSteps: ["verification"],
|
|
44
|
+
},
|
|
45
|
+
VERIFYING: {
|
|
46
|
+
completedSteps: ["contract", "route", "planning", "implementation"],
|
|
47
|
+
pendingSteps: ["verification"],
|
|
48
|
+
},
|
|
49
|
+
REVIEWING: {
|
|
50
|
+
completedSteps: ["contract", "route", "planning", "implementation", "verification"],
|
|
51
|
+
pendingSteps: [],
|
|
52
|
+
},
|
|
19
53
|
});
|
|
20
54
|
|
|
21
|
-
|
|
55
|
+
function cycleEvents(events) {
|
|
56
|
+
const CYCLE_EVENT_NAMES = new Set([
|
|
57
|
+
"VERIFICATION_STARTED",
|
|
58
|
+
"VERIFICATION_RECORDED",
|
|
59
|
+
"DIAGNOSIS_RECORDED",
|
|
60
|
+
"DIAGNOSTIC_CASE_RECORDED",
|
|
61
|
+
]);
|
|
62
|
+
const cycles = events
|
|
63
|
+
.filter((event) => CYCLE_EVENT_NAMES.has(event.event))
|
|
64
|
+
.map((event) => event.details?.verificationCycle)
|
|
65
|
+
.filter((cycle) => Number.isInteger(cycle) && cycle >= 1);
|
|
66
|
+
return cycles.at(-1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Canonical reconstruction projection: for a resumed phase derived from a
|
|
71
|
+
* validated ledger, returns the single source of truth for completedSteps and
|
|
72
|
+
* pendingSteps. Unknown phases fall back to the ROUTED projection, matching the
|
|
73
|
+
* historical default resume checkpoint.
|
|
74
|
+
*/
|
|
75
|
+
export function resumeStepsForPhase(phase) {
|
|
76
|
+
return RESUME_STEPS_BY_PHASE[phase] ?? RESUME_STEPS_BY_PHASE.ROUTED;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Canonical verification cycle derived from the ledger when history proves
|
|
81
|
+
* verification has started; undefined when the ledger has no cycle metadata.
|
|
82
|
+
*/
|
|
83
|
+
export function resumeVerificationCycleForPhase(events) {
|
|
84
|
+
return cycleEvents(events);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Canonical work-state identity fields for reconstruction from a validated
|
|
89
|
+
* ledger: resumed phase, completedSteps, pendingSteps, and verificationCycle.
|
|
90
|
+
* This is the one projection shared by ensureResumableState and
|
|
91
|
+
* contract-create reconstruction so later phases cannot be reconstructed with
|
|
92
|
+
* contradictory steps.
|
|
93
|
+
*/
|
|
94
|
+
export function buildResumableWorkStateFields({ events, resumedPhase }) {
|
|
95
|
+
const steps = resumeStepsForPhase(resumedPhase);
|
|
96
|
+
return {
|
|
97
|
+
phase: resumedPhase,
|
|
98
|
+
completedSteps: [...steps.completedSteps],
|
|
99
|
+
pendingSteps: [...steps.pendingSteps],
|
|
100
|
+
...(events && events.length > 0 && cycleEvents(events) !== undefined
|
|
101
|
+
? { verificationCycle: cycleEvents(events) }
|
|
102
|
+
: {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
|
|
22
107
|
let ledger;
|
|
23
108
|
try {
|
|
24
109
|
ledger = await validateEventLedger(target, packageRoot, { taskId });
|
|
@@ -32,71 +117,37 @@ async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
|
|
|
32
117
|
let derived = null;
|
|
33
118
|
for (const event of scoped) {
|
|
34
119
|
const phase = positions[event.event];
|
|
35
|
-
if (
|
|
36
|
-
if (!derived) {
|
|
37
|
-
derived = phase;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (phase === "VERIFYING") derived = "VERIFYING";
|
|
120
|
+
if (phase) derived = phase;
|
|
41
121
|
}
|
|
42
122
|
return derived;
|
|
43
123
|
}
|
|
44
124
|
|
|
45
|
-
function deriveVerificationCycleFromLedger(events) {
|
|
46
|
-
const cycleEvents = new Set([
|
|
47
|
-
"VERIFICATION_STARTED",
|
|
48
|
-
"VERIFICATION_RECORDED",
|
|
49
|
-
"DIAGNOSIS_RECORDED",
|
|
50
|
-
"DIAGNOSTIC_CASE_RECORDED",
|
|
51
|
-
]);
|
|
52
|
-
const cycles = events
|
|
53
|
-
.filter((event) => cycleEvents.has(event.event))
|
|
54
|
-
.map((event) => event.details?.verificationCycle)
|
|
55
|
-
.filter((cycle) => Number.isInteger(cycle) && cycle >= 1);
|
|
56
|
-
return cycles.at(-1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function resumeSteps(phase) {
|
|
60
|
-
if (phase === "EXECUTING" || phase === "VERIFYING") {
|
|
61
|
-
return {
|
|
62
|
-
completedSteps: ["contract", "route", "planning", "implementation"],
|
|
63
|
-
pendingSteps: ["verification"],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
completedSteps: ["contract", "route"],
|
|
68
|
-
pendingSteps: [...DEFAULT_PENDING_STEPS],
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
125
|
export async function ensureResumableState({ target, packageRoot, contract, route, taskId, statePath }) {
|
|
73
126
|
if (!contract || !route) return null;
|
|
74
127
|
const existing = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
75
128
|
if (existing) return existing;
|
|
76
129
|
|
|
77
130
|
const resumedPhase = await deriveResumePhaseFromLedger(target, packageRoot, taskId) ?? "ROUTED";
|
|
78
|
-
let
|
|
131
|
+
let events = [];
|
|
79
132
|
try {
|
|
80
133
|
const ledger = await validateEventLedger(target, packageRoot, { taskId });
|
|
81
134
|
if (ledger.valid) {
|
|
82
|
-
|
|
83
|
-
(ledger.events ?? []).filter((event) => !taskId || event.taskId === taskId),
|
|
84
|
-
);
|
|
135
|
+
events = (ledger.events ?? []).filter((event) => !taskId || event.taskId === taskId);
|
|
85
136
|
}
|
|
86
137
|
} catch {
|
|
87
|
-
|
|
138
|
+
events = [];
|
|
88
139
|
}
|
|
89
|
-
const
|
|
140
|
+
const projection = buildResumableWorkStateFields({ events, resumedPhase });
|
|
90
141
|
const state = createWorkState({
|
|
91
142
|
taskId: contract.value.taskId,
|
|
92
143
|
contractFingerprint: contract.fingerprint,
|
|
93
144
|
routeFingerprint: route.fingerprint,
|
|
94
145
|
repositoryFingerprint: await currentRepositoryFingerprint(target),
|
|
95
|
-
phase:
|
|
146
|
+
phase: projection.phase,
|
|
96
147
|
selectedGuides: route.value.guides,
|
|
97
|
-
completedSteps:
|
|
98
|
-
pendingSteps:
|
|
99
|
-
...(verificationCycle !== undefined ? { verificationCycle } : {}),
|
|
148
|
+
completedSteps: projection.completedSteps,
|
|
149
|
+
pendingSteps: projection.pendingSteps,
|
|
150
|
+
...(projection.verificationCycle !== undefined ? { verificationCycle: projection.verificationCycle } : {}),
|
|
100
151
|
checks: [],
|
|
101
152
|
failures: [],
|
|
102
153
|
blockers: [],
|
|
@@ -105,6 +156,65 @@ export async function ensureResumableState({ target, packageRoot, contract, rout
|
|
|
105
156
|
return initializeWorkState(target, state, { packageRoot, taskId, statePath });
|
|
106
157
|
}
|
|
107
158
|
|
|
159
|
+
function routeSyncError(code, message) {
|
|
160
|
+
const error = new Error(message);
|
|
161
|
+
error.code = code;
|
|
162
|
+
return error;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sameStringList(left, right) {
|
|
166
|
+
const a = [...(left ?? [])].sort();
|
|
167
|
+
const b = [...(right ?? [])].sort();
|
|
168
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Rebinds an existing ROUTED checkpoint to an already-persisted route.
|
|
173
|
+
*
|
|
174
|
+
* Only the route-bound identity fields are updated; contract, repository,
|
|
175
|
+
* steps, checks, evidence, and phase are preserved. Same-identity reruns are
|
|
176
|
+
* a no-op. Every other situation fails closed.
|
|
177
|
+
*/
|
|
178
|
+
export async function synchronizePersistedRouteState({ target, packageRoot, taskId, route, contract = null, statePath } = {}) {
|
|
179
|
+
if (!route || !route.value || typeof route.fingerprint !== "string") {
|
|
180
|
+
throw routeSyncError("E_ROUTE_STALE", "A persisted route artifact is required to synchronize checkpoint identity");
|
|
181
|
+
}
|
|
182
|
+
const state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
183
|
+
if (!state) return null;
|
|
184
|
+
if (state.phase !== "ROUTED") {
|
|
185
|
+
throw routeSyncError(
|
|
186
|
+
"E_ROUTE_PHASE_UNSUPPORTED",
|
|
187
|
+
`Route checkpoint synchronization supports phase ROUTED, found ${state.phase}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (taskId && state.taskId !== taskId) {
|
|
191
|
+
throw routeSyncError("E_ROUTE_STALE", "Work state does not belong to the current route task");
|
|
192
|
+
}
|
|
193
|
+
if (contract && state.contractFingerprint !== contract.fingerprint) {
|
|
194
|
+
// Contract evolution with regenerated routing is a pre-existing flow whose
|
|
195
|
+
// stale checkpoint is recovered through the sanctioned clear-state and
|
|
196
|
+
// preflight recreation path. Never rebind route identity across a contract
|
|
197
|
+
// boundary here; leave the checkpoint untouched.
|
|
198
|
+
return state;
|
|
199
|
+
}
|
|
200
|
+
if (route.value.contractFingerprint !== undefined && state.contractFingerprint !== route.value.contractFingerprint) {
|
|
201
|
+
return state;
|
|
202
|
+
}
|
|
203
|
+
if (state.routeFingerprint === route.fingerprint && sameStringList(state.selectedGuides, route.value.guides)) {
|
|
204
|
+
return state;
|
|
205
|
+
}
|
|
206
|
+
return mutateWorkState(target, {
|
|
207
|
+
expectedRevision: state.revision ?? 0,
|
|
208
|
+
packageRoot,
|
|
209
|
+
taskId,
|
|
210
|
+
statePath,
|
|
211
|
+
}, () => ({
|
|
212
|
+
...state,
|
|
213
|
+
routeFingerprint: route.fingerprint,
|
|
214
|
+
selectedGuides: [...route.value.guides],
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
108
218
|
export async function synchronizePreflightState({
|
|
109
219
|
target,
|
|
110
220
|
packageRoot,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { assertRouteInvariants } from "./router.js";
|
|
2
2
|
import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
3
|
import { readContract } from "./contract.js";
|
|
4
|
-
import { ensureResumableState } from "./resumability.js";
|
|
4
|
+
import { ensureResumableState, synchronizePersistedRouteState } from "./resumability.js";
|
|
5
|
+
import { readWorkState } from "./work-state.js";
|
|
5
6
|
import { taskArtifactPath } from "./task-paths.js";
|
|
6
7
|
|
|
8
|
+
|
|
9
|
+
|
|
7
10
|
export async function persistRoute(target, route, packageRoot, options = {}) {
|
|
8
11
|
assertRouteInvariants(route);
|
|
9
12
|
const { contractFingerprint, ...writeOptions } = options;
|
|
@@ -21,6 +24,14 @@ export async function persistRoute(target, route, packageRoot, options = {}) {
|
|
|
21
24
|
: { ...route, contractFingerprint };
|
|
22
25
|
assertRouteInvariants(value);
|
|
23
26
|
const taskId = options.taskId ?? contractArtifact?.value?.taskId ?? null;
|
|
27
|
+
let existingState = null;
|
|
28
|
+
if (taskId) {
|
|
29
|
+
try {
|
|
30
|
+
existingState = await readWorkState(target, { packageRoot, taskId });
|
|
31
|
+
} catch {
|
|
32
|
+
existingState = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
const relPath = options.routePath ?? options.routeFile ?? options.relativePath ?? (taskId ? taskArtifactPath(taskId, "route") : ARTIFACT_PATHS.route);
|
|
25
36
|
const artifact = await writeJsonArtifact(
|
|
26
37
|
target,
|
|
@@ -33,6 +44,9 @@ export async function persistRoute(target, route, packageRoot, options = {}) {
|
|
|
33
44
|
if (contractArtifact && contractArtifact.fingerprint === artifact.value.contractFingerprint) {
|
|
34
45
|
await ensureResumableState({ target, packageRoot, contract: contractArtifact, route: artifact, taskId });
|
|
35
46
|
}
|
|
47
|
+
if (existingState?.phase === "ROUTED") {
|
|
48
|
+
await synchronizePersistedRouteState({ target, packageRoot, taskId, route: artifact, contract: contractArtifact });
|
|
49
|
+
}
|
|
36
50
|
return artifact;
|
|
37
51
|
}
|
|
38
52
|
|