contextos-mcp 0.4.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/LICENSE +21 -0
- package/README.md +260 -0
- package/bin/mcp.mjs +61 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +192 -0
- package/dist/agents/aider.js.map +1 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +236 -0
- package/dist/agents/claude-code.js.map +1 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +203 -0
- package/dist/agents/codex.js.map +1 -0
- package/dist/agents/direct-llm.d.ts +22 -0
- package/dist/agents/direct-llm.js +254 -0
- package/dist/agents/direct-llm.js.map +1 -0
- package/dist/agents/mock.d.ts +10 -0
- package/dist/agents/mock.js +170 -0
- package/dist/agents/mock.js.map +1 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +631 -0
- package/dist/agents/opencode.js.map +1 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/agents/provider.js.map +1 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +268 -0
- package/dist/compression/compressor.js.map +1 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +172 -0
- package/dist/config.js.map +1 -0
- package/dist/contextos/loader.d.ts +46 -0
- package/dist/contextos/loader.js +120 -0
- package/dist/contextos/loader.js.map +1 -0
- package/dist/contextos/selector.d.ts +46 -0
- package/dist/contextos/selector.js +80 -0
- package/dist/contextos/selector.js.map +1 -0
- package/dist/core/action-dispatcher.d.ts +15 -0
- package/dist/core/action-dispatcher.js +28 -0
- package/dist/core/action-dispatcher.js.map +1 -0
- package/dist/core/action-schema.d.ts +72 -0
- package/dist/core/action-schema.js +86 -0
- package/dist/core/action-schema.js.map +1 -0
- package/dist/core/repl-interface.d.ts +48 -0
- package/dist/core/repl-interface.js +5 -0
- package/dist/core/repl-interface.js.map +1 -0
- package/dist/core/repl.d.ts +59 -0
- package/dist/core/repl.js +353 -0
- package/dist/core/repl.js.map +1 -0
- package/dist/core/rlm.d.ts +65 -0
- package/dist/core/rlm.js +564 -0
- package/dist/core/rlm.js.map +1 -0
- package/dist/core/runtime.py +470 -0
- package/dist/core/types.d.ts +265 -0
- package/dist/core/types.js +19 -0
- package/dist/core/types.js.map +1 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/env.js.map +1 -0
- package/dist/hooks/runner.d.ts +43 -0
- package/dist/hooks/runner.js +173 -0
- package/dist/hooks/runner.js.map +1 -0
- package/dist/labs/cli.d.ts +15 -0
- package/dist/labs/cli.js +295 -0
- package/dist/labs/cli.js.map +1 -0
- package/dist/labs/episodic-memory.d.ts +132 -0
- package/dist/labs/episodic-memory.js +391 -0
- package/dist/labs/episodic-memory.js.map +1 -0
- package/dist/labs/interactive-swarm.d.ts +20 -0
- package/dist/labs/interactive-swarm.js +1173 -0
- package/dist/labs/interactive-swarm.js.map +1 -0
- package/dist/labs/interactive.d.ts +10 -0
- package/dist/labs/interactive.js +2099 -0
- package/dist/labs/interactive.js.map +1 -0
- package/dist/labs/ts-analyzer.d.ts +74 -0
- package/dist/labs/ts-analyzer.js +379 -0
- package/dist/labs/ts-analyzer.js.map +1 -0
- package/dist/labs/viewer.d.ts +12 -0
- package/dist/labs/viewer.js +1284 -0
- package/dist/labs/viewer.js.map +1 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +247 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/server.d.ts +16 -0
- package/dist/mcp/server.js +192775 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +117 -0
- package/dist/mcp/session.js +450 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/state.d.ts +42 -0
- package/dist/mcp/state.js +537 -0
- package/dist/mcp/state.js.map +1 -0
- package/dist/mcp/tools/contextos.d.ts +17 -0
- package/dist/mcp/tools/contextos.js +583 -0
- package/dist/mcp/tools/contextos.js.map +1 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +393 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/orchestration/reviewer-gate.d.ts +43 -0
- package/dist/orchestration/reviewer-gate.js +280 -0
- package/dist/orchestration/reviewer-gate.js.map +1 -0
- package/dist/orchestration/reviewer-prompt.d.ts +18 -0
- package/dist/orchestration/reviewer-prompt.js +67 -0
- package/dist/orchestration/reviewer-prompt.js.map +1 -0
- package/dist/orchestration/verification-runner.d.ts +53 -0
- package/dist/orchestration/verification-runner.js +352 -0
- package/dist/orchestration/verification-runner.js.map +1 -0
- package/dist/plugins/adapter.d.ts +11 -0
- package/dist/plugins/adapter.js +11 -0
- package/dist/plugins/adapter.js.map +1 -0
- package/dist/plugins/core/archive.d.ts +9 -0
- package/dist/plugins/core/archive.js +128 -0
- package/dist/plugins/core/archive.js.map +1 -0
- package/dist/plugins/core/digest.d.ts +11 -0
- package/dist/plugins/core/digest.js +70 -0
- package/dist/plugins/core/digest.js.map +1 -0
- package/dist/plugins/core/grants.d.ts +22 -0
- package/dist/plugins/core/grants.js +90 -0
- package/dist/plugins/core/grants.js.map +1 -0
- package/dist/plugins/core/index.d.ts +10 -0
- package/dist/plugins/core/index.js +11 -0
- package/dist/plugins/core/index.js.map +1 -0
- package/dist/plugins/core/pinning.d.ts +15 -0
- package/dist/plugins/core/pinning.js +86 -0
- package/dist/plugins/core/pinning.js.map +1 -0
- package/dist/plugins/core/types.d.ts +105 -0
- package/dist/plugins/core/types.js +22 -0
- package/dist/plugins/core/types.js.map +1 -0
- package/dist/plugins/core/updater.d.ts +16 -0
- package/dist/plugins/core/updater.js +64 -0
- package/dist/plugins/core/updater.js.map +1 -0
- package/dist/plugins/loader.d.ts +65 -0
- package/dist/plugins/loader.js +110 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/prompts/orchestrator.d.ts +6 -0
- package/dist/prompts/orchestrator.js +114 -0
- package/dist/prompts/orchestrator.js.map +1 -0
- package/dist/routing/model-resolver.d.ts +27 -0
- package/dist/routing/model-resolver.js +265 -0
- package/dist/routing/model-resolver.js.map +1 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/routing/model-router.js.map +1 -0
- package/dist/runtime/attestations.cjs +260 -0
- package/dist/runtime/reviewer-pipeline.cjs +319 -0
- package/dist/runtime/sandbox.cjs +468 -0
- package/dist/runtime/state-machine.cjs +885 -0
- package/dist/runtime/thread-store.cjs +175 -0
- package/dist/runtime/transactional-git.cjs +315 -0
- package/dist/runtime/verification-pipeline.cjs +435 -0
- package/dist/sandbox/execution.d.ts +165 -0
- package/dist/sandbox/execution.js +497 -0
- package/dist/sandbox/execution.js.map +1 -0
- package/dist/security/file-policy.d.ts +22 -0
- package/dist/security/file-policy.js +53 -0
- package/dist/security/file-policy.js.map +1 -0
- package/dist/security/repository-boundary.d.ts +21 -0
- package/dist/security/repository-boundary.js +119 -0
- package/dist/security/repository-boundary.js.map +1 -0
- package/dist/security/secret-filter.d.ts +36 -0
- package/dist/security/secret-filter.js +149 -0
- package/dist/security/secret-filter.js.map +1 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +624 -0
- package/dist/swarm.js.map +1 -0
- package/dist/threads/cache.d.ts +64 -0
- package/dist/threads/cache.js +241 -0
- package/dist/threads/cache.js.map +1 -0
- package/dist/threads/manager.d.ts +94 -0
- package/dist/threads/manager.js +914 -0
- package/dist/threads/manager.js.map +1 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +135 -0
- package/dist/ui/dashboard.js.map +1 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/index.js +12 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/log.js.map +1 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +755 -0
- package/dist/ui/onboarding.js.map +1 -0
- package/dist/ui/run-log.d.ts +50 -0
- package/dist/ui/run-log.js +60 -0
- package/dist/ui/run-log.js.map +1 -0
- package/dist/ui/spinner.d.ts +41 -0
- package/dist/ui/spinner.js +172 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/streaming-feed.d.ts +72 -0
- package/dist/ui/streaming-feed.js +386 -0
- package/dist/ui/streaming-feed.js.map +1 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/summary.js.map +1 -0
- package/dist/ui/text-input.d.ts +19 -0
- package/dist/ui/text-input.js +266 -0
- package/dist/ui/text-input.js.map +1 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/utils/command-exists.d.ts +11 -0
- package/dist/utils/command-exists.js +41 -0
- package/dist/utils/command-exists.js.map +1 -0
- package/dist/utils/process-security.d.ts +19 -0
- package/dist/utils/process-security.js +90 -0
- package/dist/utils/process-security.js.map +1 -0
- package/dist/utils/rolling-buffer.d.ts +16 -0
- package/dist/utils/rolling-buffer.js +30 -0
- package/dist/utils/rolling-buffer.js.map +1 -0
- package/dist/utils/safe-spawn.d.ts +13 -0
- package/dist/utils/safe-spawn.js +49 -0
- package/dist/utils/safe-spawn.js.map +1 -0
- package/dist/worktree/manager.d.ts +69 -0
- package/dist/worktree/manager.js +593 -0
- package/dist/worktree/manager.js.map +1 -0
- package/dist/worktree/merge.d.ts +58 -0
- package/dist/worktree/merge.js +389 -0
- package/dist/worktree/merge.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/runtime/state-machine.cjs
|
|
3
|
+
* ContextOS — Independent Runtime State Machine & Transition Engine
|
|
4
|
+
*
|
|
5
|
+
* Implements Section 15 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
|
|
6
|
+
* - 4 independent orthogonal lifecycle statuses (Execution, Verification, Review, Merge)
|
|
7
|
+
* - Immutable state transitions with optimistic concurrency control (expectedRevision)
|
|
8
|
+
* - Formal transition matrices prohibiting illegal skips (e.g. FAILED -> READY is impossible)
|
|
9
|
+
* - Automatic candidate commit drift invalidation (transitions active attestations to STALE)
|
|
10
|
+
* - Fail-closed Merge Readiness evaluation (requires evidence-bearing verification & review)
|
|
11
|
+
* - Structured append-only audit trail logging
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
const fs = require("node:fs");
|
|
17
|
+
const path = require("node:path");
|
|
18
|
+
const { sha256, computeScopeSha256, computeDiffSha256, verifyAttestationSubject } = require("./attestations.cjs");
|
|
19
|
+
|
|
20
|
+
// ── Status Enums (Section 15.1) ──────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const EXECUTION_STATUS = Object.freeze({
|
|
23
|
+
QUEUED: "QUEUED",
|
|
24
|
+
PREPARING: "PREPARING",
|
|
25
|
+
RUNNING: "RUNNING",
|
|
26
|
+
SUCCEEDED: "SUCCEEDED",
|
|
27
|
+
FAILED: "FAILED",
|
|
28
|
+
TIMED_OUT: "TIMED_OUT",
|
|
29
|
+
CANCELLED: "CANCELLED",
|
|
30
|
+
INTERRUPTED: "INTERRUPTED",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const VERIFICATION_STATUS = Object.freeze({
|
|
34
|
+
NOT_CONFIGURED: "NOT_CONFIGURED",
|
|
35
|
+
NOT_APPLICABLE: "NOT_APPLICABLE",
|
|
36
|
+
PENDING: "PENDING",
|
|
37
|
+
RUNNING: "RUNNING",
|
|
38
|
+
PASS: "PASS",
|
|
39
|
+
FAIL: "FAIL",
|
|
40
|
+
ERROR: "ERROR",
|
|
41
|
+
TIMEOUT: "TIMEOUT",
|
|
42
|
+
STALE: "STALE",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const REVIEW_STATUS = Object.freeze({
|
|
46
|
+
NOT_CONFIGURED: "NOT_CONFIGURED",
|
|
47
|
+
PENDING: "PENDING",
|
|
48
|
+
RUNNING: "RUNNING",
|
|
49
|
+
PASS: "PASS",
|
|
50
|
+
FAIL: "FAIL",
|
|
51
|
+
ERROR: "ERROR",
|
|
52
|
+
TIMEOUT: "TIMEOUT",
|
|
53
|
+
UNAVAILABLE: "UNAVAILABLE",
|
|
54
|
+
MALFORMED: "MALFORMED",
|
|
55
|
+
STALE: "STALE",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const MERGE_STATUS = Object.freeze({
|
|
59
|
+
NOT_READY: "NOT_READY",
|
|
60
|
+
READY: "READY",
|
|
61
|
+
MERGING: "MERGING",
|
|
62
|
+
MERGED: "MERGED",
|
|
63
|
+
BLOCKED: "BLOCKED",
|
|
64
|
+
CONFLICT: "CONFLICT",
|
|
65
|
+
STALE_BASE: "STALE_BASE",
|
|
66
|
+
ERROR: "ERROR",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── Legal Transition Matrices ───────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const LEGAL_EXECUTION_TRANSITIONS = {
|
|
72
|
+
[EXECUTION_STATUS.QUEUED]: [EXECUTION_STATUS.PREPARING, EXECUTION_STATUS.CANCELLED, EXECUTION_STATUS.INTERRUPTED],
|
|
73
|
+
[EXECUTION_STATUS.PREPARING]: [
|
|
74
|
+
EXECUTION_STATUS.RUNNING,
|
|
75
|
+
EXECUTION_STATUS.FAILED,
|
|
76
|
+
EXECUTION_STATUS.CANCELLED,
|
|
77
|
+
EXECUTION_STATUS.INTERRUPTED,
|
|
78
|
+
],
|
|
79
|
+
[EXECUTION_STATUS.RUNNING]: [
|
|
80
|
+
EXECUTION_STATUS.SUCCEEDED,
|
|
81
|
+
EXECUTION_STATUS.FAILED,
|
|
82
|
+
EXECUTION_STATUS.TIMED_OUT,
|
|
83
|
+
EXECUTION_STATUS.CANCELLED,
|
|
84
|
+
EXECUTION_STATUS.INTERRUPTED,
|
|
85
|
+
],
|
|
86
|
+
// Terminal states (unless explicit retry re-queues)
|
|
87
|
+
[EXECUTION_STATUS.SUCCEEDED]: [],
|
|
88
|
+
[EXECUTION_STATUS.FAILED]: [EXECUTION_STATUS.QUEUED], // explicit retry
|
|
89
|
+
[EXECUTION_STATUS.TIMED_OUT]: [EXECUTION_STATUS.QUEUED],
|
|
90
|
+
[EXECUTION_STATUS.CANCELLED]: [],
|
|
91
|
+
[EXECUTION_STATUS.INTERRUPTED]: [EXECUTION_STATUS.QUEUED],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const LEGAL_VERIFICATION_TRANSITIONS = {
|
|
95
|
+
[VERIFICATION_STATUS.NOT_CONFIGURED]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.NOT_APPLICABLE],
|
|
96
|
+
[VERIFICATION_STATUS.NOT_APPLICABLE]: [VERIFICATION_STATUS.PENDING],
|
|
97
|
+
[VERIFICATION_STATUS.PENDING]: [
|
|
98
|
+
VERIFICATION_STATUS.RUNNING,
|
|
99
|
+
VERIFICATION_STATUS.NOT_APPLICABLE,
|
|
100
|
+
VERIFICATION_STATUS.STALE,
|
|
101
|
+
],
|
|
102
|
+
[VERIFICATION_STATUS.RUNNING]: [
|
|
103
|
+
VERIFICATION_STATUS.PASS,
|
|
104
|
+
VERIFICATION_STATUS.FAIL,
|
|
105
|
+
VERIFICATION_STATUS.ERROR,
|
|
106
|
+
VERIFICATION_STATUS.TIMEOUT,
|
|
107
|
+
VERIFICATION_STATUS.STALE,
|
|
108
|
+
],
|
|
109
|
+
[VERIFICATION_STATUS.PASS]: [
|
|
110
|
+
VERIFICATION_STATUS.STALE,
|
|
111
|
+
VERIFICATION_STATUS.PENDING, // re-verification
|
|
112
|
+
],
|
|
113
|
+
[VERIFICATION_STATUS.FAIL]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.STALE],
|
|
114
|
+
[VERIFICATION_STATUS.ERROR]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.STALE],
|
|
115
|
+
[VERIFICATION_STATUS.TIMEOUT]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.STALE],
|
|
116
|
+
[VERIFICATION_STATUS.STALE]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.RUNNING],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const LEGAL_REVIEW_TRANSITIONS = {
|
|
120
|
+
[REVIEW_STATUS.NOT_CONFIGURED]: [REVIEW_STATUS.PENDING],
|
|
121
|
+
[REVIEW_STATUS.PENDING]: [REVIEW_STATUS.RUNNING, REVIEW_STATUS.STALE],
|
|
122
|
+
[REVIEW_STATUS.RUNNING]: [
|
|
123
|
+
REVIEW_STATUS.PASS,
|
|
124
|
+
REVIEW_STATUS.FAIL,
|
|
125
|
+
REVIEW_STATUS.ERROR,
|
|
126
|
+
REVIEW_STATUS.TIMEOUT,
|
|
127
|
+
REVIEW_STATUS.UNAVAILABLE,
|
|
128
|
+
REVIEW_STATUS.MALFORMED,
|
|
129
|
+
REVIEW_STATUS.STALE,
|
|
130
|
+
],
|
|
131
|
+
[REVIEW_STATUS.PASS]: [REVIEW_STATUS.STALE, REVIEW_STATUS.PENDING],
|
|
132
|
+
[REVIEW_STATUS.FAIL]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
|
|
133
|
+
[REVIEW_STATUS.ERROR]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
|
|
134
|
+
[REVIEW_STATUS.TIMEOUT]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
|
|
135
|
+
[REVIEW_STATUS.UNAVAILABLE]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
|
|
136
|
+
[REVIEW_STATUS.MALFORMED]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
|
|
137
|
+
[REVIEW_STATUS.STALE]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.RUNNING],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const LEGAL_MERGE_TRANSITIONS = {
|
|
141
|
+
[MERGE_STATUS.NOT_READY]: [MERGE_STATUS.READY, MERGE_STATUS.BLOCKED, MERGE_STATUS.CONFLICT, MERGE_STATUS.STALE_BASE],
|
|
142
|
+
[MERGE_STATUS.READY]: [MERGE_STATUS.MERGING, MERGE_STATUS.NOT_READY, MERGE_STATUS.BLOCKED, MERGE_STATUS.STALE_BASE],
|
|
143
|
+
[MERGE_STATUS.MERGING]: [MERGE_STATUS.MERGED, MERGE_STATUS.CONFLICT, MERGE_STATUS.ERROR, MERGE_STATUS.BLOCKED],
|
|
144
|
+
[MERGE_STATUS.MERGED]: [],
|
|
145
|
+
[MERGE_STATUS.BLOCKED]: [MERGE_STATUS.NOT_READY, MERGE_STATUS.READY],
|
|
146
|
+
[MERGE_STATUS.CONFLICT]: [MERGE_STATUS.NOT_READY],
|
|
147
|
+
[MERGE_STATUS.STALE_BASE]: [MERGE_STATUS.NOT_READY],
|
|
148
|
+
[MERGE_STATUS.ERROR]: [MERGE_STATUS.NOT_READY],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ── Thread Initializer ───────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates a brand new immutable ThreadState object.
|
|
155
|
+
*/
|
|
156
|
+
function createThreadState(options = {}) {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const id = options.id || `th-${now}-${Math.random().toString(36).slice(2, 8)}`;
|
|
159
|
+
const config = options.config || { task: options.task || "Autonomous coding task" };
|
|
160
|
+
const baseSha = options.baseSha || "0000000000000000000000000000000000000000";
|
|
161
|
+
const repoFingerprint = options.repositoryFingerprint || sha256(process.cwd());
|
|
162
|
+
|
|
163
|
+
const thread = {
|
|
164
|
+
schemaVersion: 1,
|
|
165
|
+
id,
|
|
166
|
+
revision: 0,
|
|
167
|
+
config: {
|
|
168
|
+
task: config.task,
|
|
169
|
+
objective: config.objective || config.task,
|
|
170
|
+
writeScope: config.writeScope || [],
|
|
171
|
+
testCommand: config.testCommand || "",
|
|
172
|
+
agent: config.agent || "gemini",
|
|
173
|
+
model: config.model || "gemini-2.5-pro",
|
|
174
|
+
targetBase: config.targetBase || "main",
|
|
175
|
+
requiredSandbox: config.requiredSandbox || "none",
|
|
176
|
+
},
|
|
177
|
+
execution: {
|
|
178
|
+
status: EXECUTION_STATUS.QUEUED,
|
|
179
|
+
startedAt: null,
|
|
180
|
+
completedAt: null,
|
|
181
|
+
attempt: 1,
|
|
182
|
+
maxAttempts: config.maxAttempts || 3,
|
|
183
|
+
error: null,
|
|
184
|
+
executionId: null,
|
|
185
|
+
worktreePath: null,
|
|
186
|
+
branchName: null,
|
|
187
|
+
},
|
|
188
|
+
verification: {
|
|
189
|
+
status: config.testCommand ? VERIFICATION_STATUS.PENDING : VERIFICATION_STATUS.NOT_CONFIGURED,
|
|
190
|
+
attestation: null,
|
|
191
|
+
reason: null,
|
|
192
|
+
updatedAt: now,
|
|
193
|
+
},
|
|
194
|
+
review: {
|
|
195
|
+
status: REVIEW_STATUS.NOT_CONFIGURED,
|
|
196
|
+
attestation: null,
|
|
197
|
+
reason: null,
|
|
198
|
+
updatedAt: now,
|
|
199
|
+
},
|
|
200
|
+
merge: {
|
|
201
|
+
status: MERGE_STATUS.NOT_READY,
|
|
202
|
+
readiness: {
|
|
203
|
+
isReady: false,
|
|
204
|
+
checks: [],
|
|
205
|
+
evaluatedAt: now,
|
|
206
|
+
},
|
|
207
|
+
mergedAt: null,
|
|
208
|
+
targetBranch: config.targetBase || "main",
|
|
209
|
+
mergeCommitSha: null,
|
|
210
|
+
error: null,
|
|
211
|
+
},
|
|
212
|
+
subject: {
|
|
213
|
+
repositoryFingerprint: repoFingerprint,
|
|
214
|
+
baseSha,
|
|
215
|
+
headSha: baseSha,
|
|
216
|
+
diffSha256: sha256(""),
|
|
217
|
+
scopeSha256: computeScopeSha256(config.writeScope || []),
|
|
218
|
+
filesChanged: [],
|
|
219
|
+
},
|
|
220
|
+
createdAt: now,
|
|
221
|
+
updatedAt: now,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return Object.freeze(thread);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Merge Readiness Evaluator (Section 15.5) ────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Strictly evaluates whether a thread is eligible for automated merge into target base.
|
|
231
|
+
* Fail-closed: requires affirmative, evidence-bearing proof across all gates.
|
|
232
|
+
*
|
|
233
|
+
* @param {Object} thread - Current ThreadState
|
|
234
|
+
* @param {Object} [options] - Evaluation options (current repo state)
|
|
235
|
+
* @returns {{ isReady: boolean, checks: Array<{ name: string, passed: boolean, message: string, code?: string }> }}
|
|
236
|
+
*/
|
|
237
|
+
function evaluateMergeReadiness(thread, options = {}) {
|
|
238
|
+
const checks = [];
|
|
239
|
+
const evaluatedAt = Date.now();
|
|
240
|
+
|
|
241
|
+
if (!thread) {
|
|
242
|
+
return {
|
|
243
|
+
isReady: false,
|
|
244
|
+
checks: [{ name: "thread_exists", passed: false, message: "Thread does not exist", code: "CTX_MERGE_NO_THREAD" }],
|
|
245
|
+
evaluatedAt,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Gate 1: Execution must be SUCCEEDED
|
|
250
|
+
const execPassed = thread.execution?.status === EXECUTION_STATUS.SUCCEEDED;
|
|
251
|
+
checks.push({
|
|
252
|
+
name: "execution_status",
|
|
253
|
+
passed: execPassed,
|
|
254
|
+
message: execPassed
|
|
255
|
+
? `Execution SUCCEEDED (id: ${thread.execution.executionId || "unknown"})`
|
|
256
|
+
: `Execution is ${thread.execution?.status || "UNKNOWN"}, must be SUCCEEDED`,
|
|
257
|
+
code: execPassed ? "OK" : "CTX_MERGE_EXECUTION_NOT_SUCCEEDED",
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Gate 2: Scope check (no out-of-scope files modified)
|
|
261
|
+
const allowedScope = thread.config?.writeScope || [];
|
|
262
|
+
const filesChanged = thread.subject?.filesChanged || [];
|
|
263
|
+
let scopePassed = true;
|
|
264
|
+
let scopeViolationFile = null;
|
|
265
|
+
|
|
266
|
+
if (allowedScope.length > 0 && filesChanged.length > 0) {
|
|
267
|
+
for (const f of filesChanged) {
|
|
268
|
+
const normalized = f.replace(/\\/g, "/");
|
|
269
|
+
const inScope = allowedScope.some((scopePath) => {
|
|
270
|
+
const normScope = scopePath.replace(/\\/g, "/");
|
|
271
|
+
return normalized === normScope || normalized.startsWith(normScope.endsWith("/") ? normScope : `${normScope}/`);
|
|
272
|
+
});
|
|
273
|
+
if (!inScope) {
|
|
274
|
+
scopePassed = false;
|
|
275
|
+
scopeViolationFile = f;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
checks.push({
|
|
282
|
+
name: "scope_boundaries",
|
|
283
|
+
passed: scopePassed,
|
|
284
|
+
message: scopePassed
|
|
285
|
+
? "All modified files strictly within planned write scope"
|
|
286
|
+
: `Blast radius violation: modified file "${scopeViolationFile}" is outside writeScope`,
|
|
287
|
+
code: scopePassed ? "OK" : "CTX_MERGE_SCOPE_VIOLATION",
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Gate 3: Evidence-bearing Verification Attestation
|
|
291
|
+
const verAtt = thread.verification?.attestation;
|
|
292
|
+
const verStatus = thread.verification?.status;
|
|
293
|
+
let verPassed = false;
|
|
294
|
+
let verReason = "";
|
|
295
|
+
|
|
296
|
+
if (verStatus !== VERIFICATION_STATUS.PASS) {
|
|
297
|
+
verReason = `Verification status is ${verStatus}, expected PASS`;
|
|
298
|
+
} else if (!verAtt || !verAtt.evidence) {
|
|
299
|
+
verReason = "FAIL_CLOSED: Verification marked PASS but lacks verifiable evidence (LEGACY_UNVERIFIED)";
|
|
300
|
+
} else if (verAtt.status === VERIFICATION_STATUS.STALE) {
|
|
301
|
+
verReason = "Verification attestation is STALE (candidate commit changed)";
|
|
302
|
+
} else if (verAtt.evidence.exitCode !== 0) {
|
|
303
|
+
verReason = `Verification evidence recorded non-zero exit code: ${verAtt.evidence.exitCode}`;
|
|
304
|
+
} else {
|
|
305
|
+
// Subject integrity check
|
|
306
|
+
const subjCheck = verifyAttestationSubject(verAtt, thread.subject);
|
|
307
|
+
if (!subjCheck.valid) {
|
|
308
|
+
verReason = `Verification attestation subject mismatch: ${subjCheck.reason}`;
|
|
309
|
+
} else {
|
|
310
|
+
verPassed = true;
|
|
311
|
+
verReason = `Verification PASS verified with exitCode: 0, outputSha: ${verAtt.evidence.outputSha256.slice(0, 10)}`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
checks.push({
|
|
316
|
+
name: "verification_attestation",
|
|
317
|
+
passed: verPassed,
|
|
318
|
+
message: verReason,
|
|
319
|
+
code: verPassed ? "OK" : "CTX_MERGE_VERIFICATION_FAILED",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Gate 4: Review Attestation
|
|
323
|
+
const revAtt = thread.review?.attestation;
|
|
324
|
+
const revStatus = thread.review?.status;
|
|
325
|
+
let revPassed = false;
|
|
326
|
+
let revReason = "";
|
|
327
|
+
|
|
328
|
+
// If review is configured, it must PASS with an attestation
|
|
329
|
+
if (revStatus !== REVIEW_STATUS.PASS) {
|
|
330
|
+
revReason = `Review status is ${revStatus}, expected PASS`;
|
|
331
|
+
} else if (!revAtt) {
|
|
332
|
+
revReason = "Review marked PASS but lacks ReviewAttestation";
|
|
333
|
+
} else if (revAtt.status === REVIEW_STATUS.STALE) {
|
|
334
|
+
revReason = "Review attestation is STALE (candidate commit changed)";
|
|
335
|
+
} else {
|
|
336
|
+
const subjCheck = verifyAttestationSubject(revAtt, thread.subject);
|
|
337
|
+
if (!subjCheck.valid) {
|
|
338
|
+
revReason = `Review attestation subject mismatch: ${subjCheck.reason}`;
|
|
339
|
+
} else if (
|
|
340
|
+
!revAtt.llmVerdict ||
|
|
341
|
+
revAtt.llmVerdict.specCompliance !== "PASS" ||
|
|
342
|
+
revAtt.llmVerdict.codeQuality !== "PASS"
|
|
343
|
+
) {
|
|
344
|
+
revReason = "Review verdict does not confirm PASS for spec compliance and code quality";
|
|
345
|
+
} else {
|
|
346
|
+
revPassed = true;
|
|
347
|
+
revReason = `Review PASS confirmed by reviewer ${revAtt.reviewerExecutionId} (${revAtt.model})`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
checks.push({
|
|
352
|
+
name: "review_attestation",
|
|
353
|
+
passed: revPassed,
|
|
354
|
+
message: revReason,
|
|
355
|
+
code: revPassed ? "OK" : "CTX_MERGE_REVIEW_FAILED",
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Gate 5: Sandbox level validation
|
|
359
|
+
const requiredSandbox = thread.config?.requiredSandbox || "none";
|
|
360
|
+
let sandboxPassed = true;
|
|
361
|
+
let sandboxReason = "Sandbox level satisfies execution requirement";
|
|
362
|
+
|
|
363
|
+
if (requiredSandbox === "oci" || requiredSandbox === "strict-sandbox") {
|
|
364
|
+
if (verAtt && verAtt.runnerMode !== "oci") {
|
|
365
|
+
sandboxPassed = false;
|
|
366
|
+
sandboxReason = `Required sandbox is ${requiredSandbox}, but verification ran in ${verAtt.runnerMode}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
checks.push({
|
|
371
|
+
name: "sandbox_conformance",
|
|
372
|
+
passed: sandboxPassed,
|
|
373
|
+
message: sandboxReason,
|
|
374
|
+
code: sandboxPassed ? "OK" : "CTX_MERGE_SANDBOX_VIOLATION",
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Gate 6: Candidate Commit & Target Base Consistency
|
|
378
|
+
const currentBaseSha = options.currentBaseSha || thread.subject.baseSha;
|
|
379
|
+
let basePassed = true;
|
|
380
|
+
let baseReason = "Target base SHA matches thread base SHA";
|
|
381
|
+
|
|
382
|
+
if (currentBaseSha && currentBaseSha !== thread.subject.baseSha) {
|
|
383
|
+
basePassed = false;
|
|
384
|
+
baseReason = `Stale base detected: target base advanced to ${currentBaseSha.slice(0, 8)}, thread based on ${thread.subject.baseSha.slice(0, 8)}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
checks.push({
|
|
388
|
+
name: "base_commitment",
|
|
389
|
+
passed: basePassed,
|
|
390
|
+
message: baseReason,
|
|
391
|
+
code: basePassed ? "OK" : "CTX_MERGE_STALE_BASE",
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Gate 7: Unresolved Error Boundary
|
|
395
|
+
const noErrors = !thread.execution.error && !thread.merge.error;
|
|
396
|
+
checks.push({
|
|
397
|
+
name: "clean_error_boundary",
|
|
398
|
+
passed: noErrors,
|
|
399
|
+
message: noErrors
|
|
400
|
+
? "Zero unresolved exceptions or fatal error markers"
|
|
401
|
+
: `Unresolved error: ${thread.execution.error || thread.merge.error}`,
|
|
402
|
+
code: noErrors ? "OK" : "CTX_MERGE_UNRESOLVED_ERROR",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const isReady = checks.every((c) => c.passed);
|
|
406
|
+
return {
|
|
407
|
+
isReady,
|
|
408
|
+
checks,
|
|
409
|
+
evaluatedAt,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Stale Invalidation Helper ───────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Detects whether new candidate commits or diff modifications invalidate
|
|
417
|
+
* active verification and review attestations, marking them STALE.
|
|
418
|
+
*
|
|
419
|
+
* @param {Object} thread
|
|
420
|
+
* @param {Object} newSubject - { headSha, diffSha256, filesChanged }
|
|
421
|
+
* @returns {Object} Updated ThreadState if modified, or original
|
|
422
|
+
*/
|
|
423
|
+
function invalidateOnCandidateChange(thread, newSubject = {}) {
|
|
424
|
+
const newHead = newSubject.headSha || thread.subject.headSha;
|
|
425
|
+
const newDiff = newSubject.diffSha256 || thread.subject.diffSha256;
|
|
426
|
+
|
|
427
|
+
const isHeadChanged = newHead !== thread.subject.headSha;
|
|
428
|
+
const isDiffChanged = newDiff !== thread.subject.diffSha256;
|
|
429
|
+
|
|
430
|
+
if (!isHeadChanged && !isDiffChanged) {
|
|
431
|
+
return thread;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
const nextVerification = { ...thread.verification };
|
|
436
|
+
if (nextVerification.status === VERIFICATION_STATUS.PASS || nextVerification.status === VERIFICATION_STATUS.RUNNING) {
|
|
437
|
+
nextVerification.status = VERIFICATION_STATUS.STALE;
|
|
438
|
+
nextVerification.reason = "Candidate commit or diff changed; previous verification attestation invalidated";
|
|
439
|
+
nextVerification.updatedAt = now;
|
|
440
|
+
if (nextVerification.attestation) {
|
|
441
|
+
nextVerification.attestation = { ...nextVerification.attestation, status: VERIFICATION_STATUS.STALE };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const nextReview = { ...thread.review };
|
|
446
|
+
if (nextReview.status === REVIEW_STATUS.PASS || nextReview.status === REVIEW_STATUS.RUNNING) {
|
|
447
|
+
nextReview.status = REVIEW_STATUS.STALE;
|
|
448
|
+
nextReview.reason = "Candidate commit or diff changed; previous review attestation invalidated";
|
|
449
|
+
nextReview.updatedAt = now;
|
|
450
|
+
if (nextReview.attestation) {
|
|
451
|
+
nextReview.attestation = { ...nextReview.attestation, status: REVIEW_STATUS.STALE };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const nextSubject = {
|
|
456
|
+
...thread.subject,
|
|
457
|
+
headSha: newHead,
|
|
458
|
+
diffSha256: newDiff,
|
|
459
|
+
filesChanged: newSubject.filesChanged || thread.subject.filesChanged,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const nextMerge = { ...thread.merge, status: MERGE_STATUS.NOT_READY };
|
|
463
|
+
|
|
464
|
+
const updatedThread = {
|
|
465
|
+
...thread,
|
|
466
|
+
revision: thread.revision + 1,
|
|
467
|
+
verification: nextVerification,
|
|
468
|
+
review: nextReview,
|
|
469
|
+
merge: nextMerge,
|
|
470
|
+
subject: nextSubject,
|
|
471
|
+
updatedAt: now,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return Object.freeze(updatedThread);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── State Transition Engine (Section 15.4) ──────────────────────────────────
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Transitions a thread state strictly according to the legal transition matrices.
|
|
481
|
+
* Direct mutation (thread.status = ...) is forbidden.
|
|
482
|
+
*
|
|
483
|
+
* @param {Object} thread - Current immutable ThreadState
|
|
484
|
+
* @param {Object} event - Event object: { type, payload, expectedRevision }
|
|
485
|
+
* @param {Object} [options] - Options like auditFilePath
|
|
486
|
+
* @returns {Object} Immutable next ThreadState
|
|
487
|
+
*/
|
|
488
|
+
function transitionThread(thread, event = {}, options = {}) {
|
|
489
|
+
if (!thread) {
|
|
490
|
+
throw new Error("transitionThread requires a current thread state");
|
|
491
|
+
}
|
|
492
|
+
if (!event.type) {
|
|
493
|
+
throw new Error("transitionThread requires an event with a type");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 1. Optimistic Concurrency Check
|
|
497
|
+
if (typeof event.expectedRevision === "number" && event.expectedRevision !== thread.revision) {
|
|
498
|
+
const err = new Error(
|
|
499
|
+
`Optimistic concurrency conflict for thread ${thread.id}: expected revision ${event.expectedRevision}, but current revision is ${thread.revision}`,
|
|
500
|
+
);
|
|
501
|
+
err.code = "CTX_THREAD_CONCURRENCY_CONFLICT";
|
|
502
|
+
err.threadId = thread.id;
|
|
503
|
+
err.expectedRevision = event.expectedRevision;
|
|
504
|
+
err.currentRevision = thread.revision;
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const now = Date.now();
|
|
509
|
+
const nextThread = {
|
|
510
|
+
...thread,
|
|
511
|
+
config: { ...thread.config },
|
|
512
|
+
execution: { ...thread.execution },
|
|
513
|
+
verification: { ...thread.verification },
|
|
514
|
+
review: { ...thread.review },
|
|
515
|
+
merge: { ...thread.merge },
|
|
516
|
+
subject: { ...thread.subject },
|
|
517
|
+
revision: thread.revision + 1,
|
|
518
|
+
updatedAt: now,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const payload = event.payload || {};
|
|
522
|
+
|
|
523
|
+
// 2. Dispatch Event Type
|
|
524
|
+
switch (event.type) {
|
|
525
|
+
// ── Execution Events ──
|
|
526
|
+
case "EXECUTION_PREPARE": {
|
|
527
|
+
_assertLegalTransition(
|
|
528
|
+
"execution",
|
|
529
|
+
thread.execution.status,
|
|
530
|
+
EXECUTION_STATUS.PREPARING,
|
|
531
|
+
LEGAL_EXECUTION_TRANSITIONS,
|
|
532
|
+
);
|
|
533
|
+
nextThread.execution.status = EXECUTION_STATUS.PREPARING;
|
|
534
|
+
nextThread.execution.startedAt = now;
|
|
535
|
+
if (payload.worktreePath) nextThread.execution.worktreePath = payload.worktreePath;
|
|
536
|
+
if (payload.branchName) nextThread.execution.branchName = payload.branchName;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
case "EXECUTION_START": {
|
|
541
|
+
_assertLegalTransition(
|
|
542
|
+
"execution",
|
|
543
|
+
thread.execution.status,
|
|
544
|
+
EXECUTION_STATUS.RUNNING,
|
|
545
|
+
LEGAL_EXECUTION_TRANSITIONS,
|
|
546
|
+
);
|
|
547
|
+
nextThread.execution.status = EXECUTION_STATUS.RUNNING;
|
|
548
|
+
if (payload.executionId) nextThread.execution.executionId = payload.executionId;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
case "EXECUTION_SUCCEED": {
|
|
553
|
+
_assertLegalTransition(
|
|
554
|
+
"execution",
|
|
555
|
+
thread.execution.status,
|
|
556
|
+
EXECUTION_STATUS.SUCCEEDED,
|
|
557
|
+
LEGAL_EXECUTION_TRANSITIONS,
|
|
558
|
+
);
|
|
559
|
+
nextThread.execution.status = EXECUTION_STATUS.SUCCEEDED;
|
|
560
|
+
nextThread.execution.completedAt = now;
|
|
561
|
+
if (payload.headSha) nextThread.subject.headSha = payload.headSha;
|
|
562
|
+
if (payload.diffSha256) nextThread.subject.diffSha256 = payload.diffSha256;
|
|
563
|
+
if (payload.filesChanged) nextThread.subject.filesChanged = payload.filesChanged;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
case "EXECUTION_FAIL": {
|
|
568
|
+
_assertLegalTransition(
|
|
569
|
+
"execution",
|
|
570
|
+
thread.execution.status,
|
|
571
|
+
EXECUTION_STATUS.FAILED,
|
|
572
|
+
LEGAL_EXECUTION_TRANSITIONS,
|
|
573
|
+
);
|
|
574
|
+
nextThread.execution.status = EXECUTION_STATUS.FAILED;
|
|
575
|
+
nextThread.execution.completedAt = now;
|
|
576
|
+
nextThread.execution.error = payload.error || "Execution failed";
|
|
577
|
+
// Failed execution can never remain in READY merge status
|
|
578
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
case "EXECUTION_TIMEOUT": {
|
|
583
|
+
_assertLegalTransition(
|
|
584
|
+
"execution",
|
|
585
|
+
thread.execution.status,
|
|
586
|
+
EXECUTION_STATUS.TIMED_OUT,
|
|
587
|
+
LEGAL_EXECUTION_TRANSITIONS,
|
|
588
|
+
);
|
|
589
|
+
nextThread.execution.status = EXECUTION_STATUS.TIMED_OUT;
|
|
590
|
+
nextThread.execution.completedAt = now;
|
|
591
|
+
nextThread.execution.error = payload.error || "Execution timed out";
|
|
592
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
case "EXECUTION_CANCEL": {
|
|
597
|
+
_assertLegalTransition(
|
|
598
|
+
"execution",
|
|
599
|
+
thread.execution.status,
|
|
600
|
+
EXECUTION_STATUS.CANCELLED,
|
|
601
|
+
LEGAL_EXECUTION_TRANSITIONS,
|
|
602
|
+
);
|
|
603
|
+
nextThread.execution.status = EXECUTION_STATUS.CANCELLED;
|
|
604
|
+
nextThread.execution.completedAt = now;
|
|
605
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
case "EXECUTION_INTERRUPT": {
|
|
610
|
+
_assertLegalTransition(
|
|
611
|
+
"execution",
|
|
612
|
+
thread.execution.status,
|
|
613
|
+
EXECUTION_STATUS.INTERRUPTED,
|
|
614
|
+
LEGAL_EXECUTION_TRANSITIONS,
|
|
615
|
+
);
|
|
616
|
+
nextThread.execution.status = EXECUTION_STATUS.INTERRUPTED;
|
|
617
|
+
nextThread.execution.completedAt = now;
|
|
618
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
case "EXECUTION_RETRY": {
|
|
623
|
+
_assertLegalTransition(
|
|
624
|
+
"execution",
|
|
625
|
+
thread.execution.status,
|
|
626
|
+
EXECUTION_STATUS.QUEUED,
|
|
627
|
+
LEGAL_EXECUTION_TRANSITIONS,
|
|
628
|
+
);
|
|
629
|
+
nextThread.execution.status = EXECUTION_STATUS.QUEUED;
|
|
630
|
+
nextThread.execution.attempt += 1;
|
|
631
|
+
nextThread.execution.error = null;
|
|
632
|
+
nextThread.execution.startedAt = null;
|
|
633
|
+
nextThread.execution.completedAt = null;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── Verification Events ──
|
|
638
|
+
case "VERIFICATION_CONFIGURED": {
|
|
639
|
+
_assertLegalTransition(
|
|
640
|
+
"verification",
|
|
641
|
+
thread.verification.status,
|
|
642
|
+
VERIFICATION_STATUS.PENDING,
|
|
643
|
+
LEGAL_VERIFICATION_TRANSITIONS,
|
|
644
|
+
);
|
|
645
|
+
nextThread.verification.status = VERIFICATION_STATUS.PENDING;
|
|
646
|
+
nextThread.verification.updatedAt = now;
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
case "VERIFICATION_START": {
|
|
651
|
+
_assertLegalTransition(
|
|
652
|
+
"verification",
|
|
653
|
+
thread.verification.status,
|
|
654
|
+
VERIFICATION_STATUS.RUNNING,
|
|
655
|
+
LEGAL_VERIFICATION_TRANSITIONS,
|
|
656
|
+
);
|
|
657
|
+
nextThread.verification.status = VERIFICATION_STATUS.RUNNING;
|
|
658
|
+
nextThread.verification.updatedAt = now;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
case "VERIFICATION_PASS": {
|
|
663
|
+
_assertLegalTransition(
|
|
664
|
+
"verification",
|
|
665
|
+
thread.verification.status,
|
|
666
|
+
VERIFICATION_STATUS.PASS,
|
|
667
|
+
LEGAL_VERIFICATION_TRANSITIONS,
|
|
668
|
+
);
|
|
669
|
+
if (!payload.attestation) {
|
|
670
|
+
throw new Error("FAIL_CLOSED: VERIFICATION_PASS event requires an evidence-bearing attestation");
|
|
671
|
+
}
|
|
672
|
+
nextThread.verification.status = VERIFICATION_STATUS.PASS;
|
|
673
|
+
nextThread.verification.attestation = payload.attestation;
|
|
674
|
+
nextThread.verification.updatedAt = now;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
case "VERIFICATION_FAIL": {
|
|
679
|
+
_assertLegalTransition(
|
|
680
|
+
"verification",
|
|
681
|
+
thread.verification.status,
|
|
682
|
+
VERIFICATION_STATUS.FAIL,
|
|
683
|
+
LEGAL_VERIFICATION_TRANSITIONS,
|
|
684
|
+
);
|
|
685
|
+
nextThread.verification.status = VERIFICATION_STATUS.FAIL;
|
|
686
|
+
nextThread.verification.attestation = payload.attestation || null;
|
|
687
|
+
nextThread.verification.reason = payload.reason || "Tests failed";
|
|
688
|
+
nextThread.verification.updatedAt = now;
|
|
689
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
case "VERIFICATION_ERROR": {
|
|
694
|
+
_assertLegalTransition(
|
|
695
|
+
"verification",
|
|
696
|
+
thread.verification.status,
|
|
697
|
+
VERIFICATION_STATUS.ERROR,
|
|
698
|
+
LEGAL_VERIFICATION_TRANSITIONS,
|
|
699
|
+
);
|
|
700
|
+
nextThread.verification.status = VERIFICATION_STATUS.ERROR;
|
|
701
|
+
nextThread.verification.reason = payload.reason || "Verification process error";
|
|
702
|
+
nextThread.verification.updatedAt = now;
|
|
703
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
case "VERIFICATION_TIMEOUT": {
|
|
708
|
+
_assertLegalTransition(
|
|
709
|
+
"verification",
|
|
710
|
+
thread.verification.status,
|
|
711
|
+
VERIFICATION_STATUS.TIMEOUT,
|
|
712
|
+
LEGAL_VERIFICATION_TRANSITIONS,
|
|
713
|
+
);
|
|
714
|
+
nextThread.verification.status = VERIFICATION_STATUS.TIMEOUT;
|
|
715
|
+
nextThread.verification.reason = "Verification runner timed out";
|
|
716
|
+
nextThread.verification.updatedAt = now;
|
|
717
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ── Review Events ──
|
|
722
|
+
case "REVIEW_CONFIGURED": {
|
|
723
|
+
_assertLegalTransition("review", thread.review.status, REVIEW_STATUS.PENDING, LEGAL_REVIEW_TRANSITIONS);
|
|
724
|
+
nextThread.review.status = REVIEW_STATUS.PENDING;
|
|
725
|
+
nextThread.review.updatedAt = now;
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
case "REVIEW_START": {
|
|
730
|
+
_assertLegalTransition("review", thread.review.status, REVIEW_STATUS.RUNNING, LEGAL_REVIEW_TRANSITIONS);
|
|
731
|
+
nextThread.review.status = REVIEW_STATUS.RUNNING;
|
|
732
|
+
nextThread.review.updatedAt = now;
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
case "REVIEW_PASS": {
|
|
737
|
+
_assertLegalTransition("review", thread.review.status, REVIEW_STATUS.PASS, LEGAL_REVIEW_TRANSITIONS);
|
|
738
|
+
if (!payload.attestation) {
|
|
739
|
+
throw new Error("FAIL_CLOSED: REVIEW_PASS event requires ReviewAttestation");
|
|
740
|
+
}
|
|
741
|
+
nextThread.review.status = REVIEW_STATUS.PASS;
|
|
742
|
+
nextThread.review.attestation = payload.attestation;
|
|
743
|
+
nextThread.review.updatedAt = now;
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
case "REVIEW_FAIL": {
|
|
748
|
+
_assertLegalTransition("review", thread.review.status, REVIEW_STATUS.FAIL, LEGAL_REVIEW_TRANSITIONS);
|
|
749
|
+
nextThread.review.status = REVIEW_STATUS.FAIL;
|
|
750
|
+
nextThread.review.attestation = payload.attestation || null;
|
|
751
|
+
nextThread.review.reason = payload.reason || "Review rejected changes";
|
|
752
|
+
nextThread.review.updatedAt = now;
|
|
753
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
case "REVIEW_UNAVAILABLE": {
|
|
758
|
+
_assertLegalTransition("review", thread.review.status, REVIEW_STATUS.UNAVAILABLE, LEGAL_REVIEW_TRANSITIONS);
|
|
759
|
+
nextThread.review.status = REVIEW_STATUS.UNAVAILABLE;
|
|
760
|
+
nextThread.review.reason = payload.reason || "Reviewer model/provider unavailable";
|
|
761
|
+
nextThread.review.updatedAt = now;
|
|
762
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
case "REVIEW_MALFORMED": {
|
|
767
|
+
_assertLegalTransition("review", thread.review.status, REVIEW_STATUS.MALFORMED, LEGAL_REVIEW_TRANSITIONS);
|
|
768
|
+
nextThread.review.status = REVIEW_STATUS.MALFORMED;
|
|
769
|
+
nextThread.review.reason = payload.reason || "Reviewer output was malformed";
|
|
770
|
+
nextThread.review.updatedAt = now;
|
|
771
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ── Merge Events ──
|
|
776
|
+
case "EVALUATE_MERGE_READINESS": {
|
|
777
|
+
const readiness = evaluateMergeReadiness(nextThread, payload.options);
|
|
778
|
+
nextThread.merge.readiness = readiness;
|
|
779
|
+
if (readiness.isReady) {
|
|
780
|
+
_assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.READY, LEGAL_MERGE_TRANSITIONS);
|
|
781
|
+
nextThread.merge.status = MERGE_STATUS.READY;
|
|
782
|
+
} else {
|
|
783
|
+
nextThread.merge.status = MERGE_STATUS.NOT_READY;
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
case "MERGE_START": {
|
|
789
|
+
_assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.MERGING, LEGAL_MERGE_TRANSITIONS);
|
|
790
|
+
nextThread.merge.status = MERGE_STATUS.MERGING;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
case "MERGE_COMPLETE": {
|
|
795
|
+
_assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.MERGED, LEGAL_MERGE_TRANSITIONS);
|
|
796
|
+
nextThread.merge.status = MERGE_STATUS.MERGED;
|
|
797
|
+
nextThread.merge.mergedAt = now;
|
|
798
|
+
if (payload.mergeCommitSha) nextThread.merge.mergeCommitSha = payload.mergeCommitSha;
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
case "MERGE_CONFLICT": {
|
|
803
|
+
_assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.CONFLICT, LEGAL_MERGE_TRANSITIONS);
|
|
804
|
+
nextThread.merge.status = MERGE_STATUS.CONFLICT;
|
|
805
|
+
nextThread.merge.error = payload.error || "Git merge conflict encountered";
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
case "MERGE_BLOCK": {
|
|
810
|
+
_assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.BLOCKED, LEGAL_MERGE_TRANSITIONS);
|
|
811
|
+
nextThread.merge.status = MERGE_STATUS.BLOCKED;
|
|
812
|
+
nextThread.merge.error = payload.reason || "Merge blocked by policy";
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ── Candidate Commit Drift Event ──
|
|
817
|
+
case "CANDIDATE_COMMIT_UPDATE": {
|
|
818
|
+
return invalidateOnCandidateChange(thread, payload);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
default:
|
|
822
|
+
throw new Error(`Unknown state transition event type: "${event.type}"`);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// 3. Write Immutable Audit Trail Event
|
|
826
|
+
const auditEvent = {
|
|
827
|
+
timestamp: now,
|
|
828
|
+
threadId: thread.id,
|
|
829
|
+
fromRevision: thread.revision,
|
|
830
|
+
toRevision: nextThread.revision,
|
|
831
|
+
eventType: event.type,
|
|
832
|
+
fromStates: {
|
|
833
|
+
execution: thread.execution.status,
|
|
834
|
+
verification: thread.verification.status,
|
|
835
|
+
review: thread.review.status,
|
|
836
|
+
merge: thread.merge.status,
|
|
837
|
+
},
|
|
838
|
+
toStates: {
|
|
839
|
+
execution: nextThread.execution.status,
|
|
840
|
+
verification: nextThread.verification.status,
|
|
841
|
+
review: nextThread.review.status,
|
|
842
|
+
merge: nextThread.merge.status,
|
|
843
|
+
},
|
|
844
|
+
payload,
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
if (options.auditFilePath) {
|
|
848
|
+
try {
|
|
849
|
+
fs.mkdirSync(path.dirname(options.auditFilePath), { recursive: true });
|
|
850
|
+
fs.appendFileSync(options.auditFilePath, `${JSON.stringify(auditEvent)}\n`);
|
|
851
|
+
} catch {
|
|
852
|
+
// ignore
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return Object.freeze(nextThread);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Validates that fromStatus -> toStatus is legal in transition table.
|
|
861
|
+
*/
|
|
862
|
+
function _assertLegalTransition(facet, fromStatus, toStatus, legalTable) {
|
|
863
|
+
if (fromStatus === toStatus) return; // idempotent
|
|
864
|
+
|
|
865
|
+
const allowed = legalTable[fromStatus] || [];
|
|
866
|
+
if (!allowed.includes(toStatus)) {
|
|
867
|
+
const err = new Error(`Illegal ${facet} state transition: cannot transition from ${fromStatus} to ${toStatus}`);
|
|
868
|
+
err.code = "CTX_ILLEGAL_STATE_TRANSITION";
|
|
869
|
+
err.facet = facet;
|
|
870
|
+
err.fromStatus = fromStatus;
|
|
871
|
+
err.toStatus = toStatus;
|
|
872
|
+
throw err;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
module.exports = {
|
|
877
|
+
EXECUTION_STATUS,
|
|
878
|
+
VERIFICATION_STATUS,
|
|
879
|
+
REVIEW_STATUS,
|
|
880
|
+
MERGE_STATUS,
|
|
881
|
+
createThreadState,
|
|
882
|
+
transitionThread,
|
|
883
|
+
evaluateMergeReadiness,
|
|
884
|
+
invalidateOnCandidateChange,
|
|
885
|
+
};
|