agentxchain 2.155.72 → 2.156.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/README.md +4 -8
- package/bin/agentxchain.js +22 -0
- package/dashboard/app.js +54 -0
- package/dashboard/components/org-audit-trail.js +161 -0
- package/dashboard/components/org-history.js +140 -0
- package/dashboard/components/org-overview.js +145 -0
- package/dashboard/components/org-runs.js +168 -0
- package/dashboard/index.html +4 -0
- package/package.json +4 -5
- package/scripts/migrate-node-test-to-vitest.mjs +98 -0
- package/scripts/release-postflight.sh +1 -1
- package/scripts/release-preflight.sh +5 -5
- package/scripts/verify-post-publish.sh +1 -1
- package/src/commands/ci-report.js +80 -0
- package/src/commands/doctor.js +22 -1
- package/src/commands/intake-approve.js +1 -0
- package/src/commands/replay.js +1 -0
- package/src/commands/run.js +50 -0
- package/src/commands/serve.js +64 -0
- package/src/commands/step.js +63 -1
- package/src/commands/verify.js +1 -0
- package/src/lib/adapters/local-cli-adapter.js +326 -2
- package/src/lib/api/execution-worker.js +192 -0
- package/src/lib/api/hosted-runner.js +494 -0
- package/src/lib/api/job-queue.js +152 -0
- package/src/lib/api/org-state-aggregator.js +428 -0
- package/src/lib/api/project-registry.js +148 -0
- package/src/lib/api/protocol-bridge.js +476 -0
- package/src/lib/approval-policy.js +12 -0
- package/src/lib/ci-reporter.js +188 -0
- package/src/lib/claude-local-auth.js +89 -1
- package/src/lib/connector-probe.js +21 -0
- package/src/lib/continuous-run.js +51 -3
- package/src/lib/dashboard/bridge-server.js +10 -5
- package/src/lib/dispatch-bundle.js +7 -3
- package/src/lib/dispatch-progress.js +9 -0
- package/src/lib/governed-state.js +5 -4
- package/src/lib/intake.js +32 -6
- package/src/lib/normalized-config.js +4 -0
- package/src/lib/recovery-classification.js +158 -0
- package/src/lib/report.js +91 -0
- package/src/lib/run-events.js +7 -1
- package/src/lib/schemas/agentxchain-config.schema.json +10 -0
- package/src/lib/scope-overlap.js +214 -0
- package/src/lib/turn-checkpoint.js +33 -3
- package/src/lib/turn-result-validator.js +47 -6
- package/src/lib/validation.js +11 -3
- package/src/lib/verification-replay.js +125 -4
- package/src/lib/vision-reader.js +16 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Registry — file-backed registry mapping project IDs to root directories.
|
|
3
|
+
*
|
|
4
|
+
* Persists to <primaryRoot>/.agentxchain/org-registry.json.
|
|
5
|
+
* The primary project is always registered and cannot be unregistered.
|
|
6
|
+
*
|
|
7
|
+
* @module project-registry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
11
|
+
import { join, resolve, basename } from 'node:path';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Derive a deterministic project ID from a normalized absolute path.
|
|
16
|
+
* Uses first 12 hex chars of SHA-256.
|
|
17
|
+
*/
|
|
18
|
+
function deriveProjectId(absolutePath) {
|
|
19
|
+
const normalized = resolve(absolutePath);
|
|
20
|
+
return createHash('sha256').update(normalized).digest('hex').slice(0, 12);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a project registry persisted in the primary project's .agentxchain/ directory.
|
|
25
|
+
* @param {string} primaryRoot - primary project root (always registered)
|
|
26
|
+
* @returns {ProjectRegistry}
|
|
27
|
+
*/
|
|
28
|
+
export function createProjectRegistry(primaryRoot) {
|
|
29
|
+
const normalizedPrimary = resolve(primaryRoot);
|
|
30
|
+
const axDir = join(normalizedPrimary, '.agentxchain');
|
|
31
|
+
const registryPath = join(axDir, 'org-registry.json');
|
|
32
|
+
|
|
33
|
+
/** @type {Map<string, RegistryEntry>} */
|
|
34
|
+
const entries = new Map();
|
|
35
|
+
|
|
36
|
+
// Always register the primary project
|
|
37
|
+
const primaryId = deriveProjectId(normalizedPrimary);
|
|
38
|
+
entries.set(primaryId, {
|
|
39
|
+
id: primaryId,
|
|
40
|
+
name: basename(normalizedPrimary),
|
|
41
|
+
root: normalizedPrimary,
|
|
42
|
+
is_primary: true,
|
|
43
|
+
registered_at: Date.now(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Try to load existing registry from disk
|
|
47
|
+
load();
|
|
48
|
+
|
|
49
|
+
function load() {
|
|
50
|
+
try {
|
|
51
|
+
if (!existsSync(registryPath)) return;
|
|
52
|
+
const raw = readFileSync(registryPath, 'utf8').trim();
|
|
53
|
+
if (!raw) return;
|
|
54
|
+
const data = JSON.parse(raw);
|
|
55
|
+
if (data.version !== 1 || !Array.isArray(data.projects)) return;
|
|
56
|
+
for (const proj of data.projects) {
|
|
57
|
+
if (!proj.id || !proj.root) continue;
|
|
58
|
+
entries.set(proj.id, {
|
|
59
|
+
id: proj.id,
|
|
60
|
+
name: proj.name || basename(proj.root),
|
|
61
|
+
root: proj.root,
|
|
62
|
+
is_primary: proj.id === primaryId,
|
|
63
|
+
registered_at: proj.registered_at || Date.now(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// Ensure primary is always present
|
|
67
|
+
if (!entries.has(primaryId)) {
|
|
68
|
+
entries.set(primaryId, {
|
|
69
|
+
id: primaryId,
|
|
70
|
+
name: basename(normalizedPrimary),
|
|
71
|
+
root: normalizedPrimary,
|
|
72
|
+
is_primary: true,
|
|
73
|
+
registered_at: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Graceful: corrupt file → keep primary-only registry
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function save() {
|
|
82
|
+
try {
|
|
83
|
+
if (!existsSync(axDir)) {
|
|
84
|
+
mkdirSync(axDir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
const data = {
|
|
87
|
+
version: 1,
|
|
88
|
+
projects: Array.from(entries.values()),
|
|
89
|
+
};
|
|
90
|
+
writeFileSync(registryPath, JSON.stringify(data, null, 2));
|
|
91
|
+
} catch {
|
|
92
|
+
// Best-effort persistence
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function register(root, name) {
|
|
97
|
+
const normalizedRoot = resolve(root);
|
|
98
|
+
|
|
99
|
+
// Validate: must be a governed project
|
|
100
|
+
if (!existsSync(join(normalizedRoot, 'agentxchain.json'))) {
|
|
101
|
+
throw new Error(`no agentxchain.json found at ${normalizedRoot}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const id = deriveProjectId(normalizedRoot);
|
|
105
|
+
const existing = entries.get(id);
|
|
106
|
+
|
|
107
|
+
if (existing) {
|
|
108
|
+
// Idempotent: update name only
|
|
109
|
+
if (name) existing.name = name;
|
|
110
|
+
save();
|
|
111
|
+
return { ...existing };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const entry = {
|
|
115
|
+
id,
|
|
116
|
+
name: name || basename(normalizedRoot),
|
|
117
|
+
root: normalizedRoot,
|
|
118
|
+
is_primary: id === primaryId,
|
|
119
|
+
registered_at: Date.now(),
|
|
120
|
+
};
|
|
121
|
+
entries.set(id, entry);
|
|
122
|
+
save();
|
|
123
|
+
return { ...entry };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function unregister(projectId) {
|
|
127
|
+
const entry = entries.get(projectId);
|
|
128
|
+
if (!entry) return false;
|
|
129
|
+
// Primary project cannot be unregistered
|
|
130
|
+
if (entry.is_primary) return false;
|
|
131
|
+
entries.delete(projectId);
|
|
132
|
+
save();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function list() {
|
|
137
|
+
return Array.from(entries.values())
|
|
138
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
139
|
+
.map(e => ({ ...e }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function get(projectId) {
|
|
143
|
+
const entry = entries.get(projectId);
|
|
144
|
+
return entry ? { ...entry } : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { register, unregister, list, get, save, load };
|
|
148
|
+
}
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol Bridge — wraps runner-interface.js primitives for HTTP consumption.
|
|
3
|
+
*
|
|
4
|
+
* Design principles:
|
|
5
|
+
* 1. No HTTP objects (no req/res) — pure protocol-to-protocol adapter
|
|
6
|
+
* 2. Typed error classification for deterministic HTTP status mapping
|
|
7
|
+
* 3. @state-provider JSDoc on every filesystem operation
|
|
8
|
+
* 4. No side effects beyond protocol state mutations
|
|
9
|
+
*
|
|
10
|
+
* This module proves the agentxchain protocol is composable by an HTTP server
|
|
11
|
+
* without modifying the protocol layer. The server maps bridge errors to HTTP
|
|
12
|
+
* status codes and bridge results to JSON responses.
|
|
13
|
+
*
|
|
14
|
+
* Note: restartFromCheckpoint is defined in the OpenAPI spec but not exported
|
|
15
|
+
* here because the protocol layer (turn-checkpoint.js) does not yet expose a
|
|
16
|
+
* restart primitive. The bridge will add it when the protocol does.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
initRun,
|
|
24
|
+
loadState,
|
|
25
|
+
acceptTurn,
|
|
26
|
+
rejectTurn,
|
|
27
|
+
approvePhaseGate,
|
|
28
|
+
markRunBlocked,
|
|
29
|
+
reissueTurn,
|
|
30
|
+
acquireLock,
|
|
31
|
+
releaseLock,
|
|
32
|
+
getTurnStagingResultPath,
|
|
33
|
+
} from '../runner-interface.js';
|
|
34
|
+
|
|
35
|
+
import { checkpointAcceptedTurn } from '../turn-checkpoint.js';
|
|
36
|
+
import { queryRunHistory } from '../run-history.js';
|
|
37
|
+
import { readRunEvents } from '../run-events.js';
|
|
38
|
+
import { evaluatePhaseExit } from '../gate-evaluator.js';
|
|
39
|
+
import { buildRunExport } from '../export.js';
|
|
40
|
+
|
|
41
|
+
// ── Error Classes ──────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export class ProtocolError extends Error {
|
|
44
|
+
constructor(code, message, details) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = 'ProtocolError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.details = details || null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class NotFoundError extends Error {
|
|
53
|
+
constructor(target, id) {
|
|
54
|
+
super(`${target} "${id}" not found`);
|
|
55
|
+
this.name = 'NotFoundError';
|
|
56
|
+
this.code = 'not_found';
|
|
57
|
+
this.target = target;
|
|
58
|
+
this.target_id = id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class ValidationError extends Error {
|
|
63
|
+
constructor(message, details) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = 'ValidationError';
|
|
66
|
+
this.code = 'invalid_request';
|
|
67
|
+
this.details = details || null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class AuthorizationError extends Error {
|
|
72
|
+
constructor(requiredRole, actualRole) {
|
|
73
|
+
super(`Requires role "${requiredRole}", got "${actualRole}"`);
|
|
74
|
+
this.name = 'AuthorizationError';
|
|
75
|
+
this.code = 'forbidden';
|
|
76
|
+
this.required_role = requiredRole;
|
|
77
|
+
this.actual_role = actualRole;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class ConflictError extends Error {
|
|
82
|
+
constructor(message, details) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = 'ConflictError';
|
|
85
|
+
this.code = 'lock_conflict';
|
|
86
|
+
this.details = details || null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const AGENTXCHAIN_DIR = '.agentxchain';
|
|
93
|
+
const HISTORY_FILE = 'history.jsonl';
|
|
94
|
+
const DECISION_LEDGER_FILE = 'decision-ledger.jsonl';
|
|
95
|
+
|
|
96
|
+
function requireState(root) {
|
|
97
|
+
const statePath = join(root, AGENTXCHAIN_DIR, 'state.json');
|
|
98
|
+
if (!existsSync(statePath)) {
|
|
99
|
+
throw new NotFoundError('run state', root);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readJsonl(filePath) {
|
|
104
|
+
if (!existsSync(filePath)) return [];
|
|
105
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
106
|
+
return raw.split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function paginate(items, cursor, limit = 25) {
|
|
110
|
+
const safeLimit = Math.max(1, Math.min(100, limit));
|
|
111
|
+
let startIndex = 0;
|
|
112
|
+
if (cursor) {
|
|
113
|
+
const cursorIndex = parseInt(cursor, 10);
|
|
114
|
+
if (!Number.isNaN(cursorIndex) && cursorIndex >= 0) {
|
|
115
|
+
startIndex = cursorIndex;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const slice = items.slice(startIndex, startIndex + safeLimit);
|
|
119
|
+
const hasMore = startIndex + safeLimit < items.length;
|
|
120
|
+
return {
|
|
121
|
+
data: slice,
|
|
122
|
+
cursor: hasMore ? String(startIndex + safeLimit) : null,
|
|
123
|
+
has_more: hasMore,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Bridge Functions ───────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a new governed run.
|
|
131
|
+
*
|
|
132
|
+
* @state-provider writeState
|
|
133
|
+
* Writes: .agentxchain/state.json
|
|
134
|
+
* Cloud replacement: state store PUT (create new run document)
|
|
135
|
+
*
|
|
136
|
+
* @param {string} root - project root
|
|
137
|
+
* @param {object} config - normalized config
|
|
138
|
+
* @param {object} [opts] - options forwarded to initializeGovernedRun
|
|
139
|
+
* @returns {object} created run state
|
|
140
|
+
*/
|
|
141
|
+
export function createRun(root, config, opts = {}) {
|
|
142
|
+
const result = initRun(root, config, opts);
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
throw new ProtocolError('invalid_state', result.error);
|
|
145
|
+
}
|
|
146
|
+
return result.state;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get current run state.
|
|
151
|
+
*
|
|
152
|
+
* @state-provider readState
|
|
153
|
+
* Reads: .agentxchain/state.json
|
|
154
|
+
* Cloud replacement: state store GET by run_id
|
|
155
|
+
*
|
|
156
|
+
* @param {string} root - project root
|
|
157
|
+
* @param {object} config - normalized config
|
|
158
|
+
* @returns {object} run state
|
|
159
|
+
*/
|
|
160
|
+
export function getRunState(root, config) {
|
|
161
|
+
requireState(root);
|
|
162
|
+
const state = loadState(root, config);
|
|
163
|
+
if (!state) {
|
|
164
|
+
throw new NotFoundError('run', root);
|
|
165
|
+
}
|
|
166
|
+
return state;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* List runs from run history.
|
|
171
|
+
*
|
|
172
|
+
* @state-provider readHistory
|
|
173
|
+
* Reads: .agentxchain/run-history.jsonl
|
|
174
|
+
* Cloud replacement: run history query by project_id with cursor
|
|
175
|
+
*
|
|
176
|
+
* @param {string} root - project root
|
|
177
|
+
* @param {string|null} cursor - pagination cursor
|
|
178
|
+
* @param {number} [limit=25] - page size
|
|
179
|
+
* @returns {{ data: object[], cursor: string|null, has_more: boolean }}
|
|
180
|
+
*/
|
|
181
|
+
export function listRuns(root, cursor, limit = 25) {
|
|
182
|
+
const entries = queryRunHistory(root);
|
|
183
|
+
return paginate(entries, cursor, limit);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Cancel (block) a run.
|
|
188
|
+
*
|
|
189
|
+
* @state-provider writeState
|
|
190
|
+
* Writes: .agentxchain/state.json
|
|
191
|
+
* Cloud replacement: state store PUT (update run status)
|
|
192
|
+
*
|
|
193
|
+
* @param {string} root - project root
|
|
194
|
+
* @param {string} reason - cancellation reason
|
|
195
|
+
* @returns {object} updated run state
|
|
196
|
+
*/
|
|
197
|
+
export function cancelRun(root, reason) {
|
|
198
|
+
requireState(root);
|
|
199
|
+
if (!reason) {
|
|
200
|
+
throw new ValidationError('Cancellation reason is required');
|
|
201
|
+
}
|
|
202
|
+
const result = markRunBlocked(root, {
|
|
203
|
+
category: 'operator_cancelled',
|
|
204
|
+
reason,
|
|
205
|
+
});
|
|
206
|
+
if (result && !result.ok) {
|
|
207
|
+
throw new ProtocolError('invalid_state', result.error || 'Failed to cancel run');
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* List turns from history.
|
|
214
|
+
*
|
|
215
|
+
* @state-provider readHistory
|
|
216
|
+
* Reads: .agentxchain/history.jsonl
|
|
217
|
+
* Cloud replacement: turn history query by run_id with cursor
|
|
218
|
+
*
|
|
219
|
+
* @param {string} root - project root
|
|
220
|
+
* @param {string|null} cursor - pagination cursor
|
|
221
|
+
* @param {number} [limit=25] - page size
|
|
222
|
+
* @returns {{ data: object[], cursor: string|null, has_more: boolean }}
|
|
223
|
+
*/
|
|
224
|
+
export function getTurns(root, cursor, limit = 25) {
|
|
225
|
+
const historyPath = join(root, AGENTXCHAIN_DIR, HISTORY_FILE);
|
|
226
|
+
const entries = readJsonl(historyPath);
|
|
227
|
+
return paginate(entries, cursor, limit);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get a specific turn by ID.
|
|
232
|
+
*
|
|
233
|
+
* @state-provider readHistory
|
|
234
|
+
* Reads: .agentxchain/history.jsonl, .agentxchain/staging/<turn_id>/turn-result.json
|
|
235
|
+
* Cloud replacement: turn store GET by turn_id
|
|
236
|
+
*
|
|
237
|
+
* @param {string} root - project root
|
|
238
|
+
* @param {string} turnId - turn identifier
|
|
239
|
+
* @returns {object} turn detail
|
|
240
|
+
*/
|
|
241
|
+
export function getTurn(root, turnId) {
|
|
242
|
+
if (!turnId) {
|
|
243
|
+
throw new ValidationError('turn_id is required');
|
|
244
|
+
}
|
|
245
|
+
const historyPath = join(root, AGENTXCHAIN_DIR, HISTORY_FILE);
|
|
246
|
+
const entries = readJsonl(historyPath);
|
|
247
|
+
const entry = entries.find(e => e.turn_id === turnId);
|
|
248
|
+
if (entry) return entry;
|
|
249
|
+
|
|
250
|
+
const stagingPath = join(root, getTurnStagingResultPath(turnId));
|
|
251
|
+
if (existsSync(stagingPath)) {
|
|
252
|
+
return JSON.parse(readFileSync(stagingPath, 'utf8'));
|
|
253
|
+
}
|
|
254
|
+
throw new NotFoundError('turn', turnId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Accept a turn result.
|
|
259
|
+
*
|
|
260
|
+
* @state-provider writeState
|
|
261
|
+
* Writes: .agentxchain/state.json
|
|
262
|
+
* Cloud replacement: state store PUT (compare-and-swap)
|
|
263
|
+
*
|
|
264
|
+
* @state-provider acquireLock
|
|
265
|
+
* Reads/writes: .agentxchain/lock.json
|
|
266
|
+
* Cloud replacement: distributed lock (lease-based, workspace-scoped)
|
|
267
|
+
*
|
|
268
|
+
* @param {string} root - project root
|
|
269
|
+
* @param {object} config - normalized config
|
|
270
|
+
* @param {string} turnId - target turn ID
|
|
271
|
+
* @param {object} [opts] - additional options
|
|
272
|
+
* @returns {object} acceptance result with updated state
|
|
273
|
+
*/
|
|
274
|
+
export function acceptTurnResult(root, config, turnId, opts = {}) {
|
|
275
|
+
requireState(root);
|
|
276
|
+
const result = acceptTurn(root, config, { ...opts, turnId });
|
|
277
|
+
if (!result.ok) {
|
|
278
|
+
if (result.error_code === 'lock_conflict' || (result.error && result.error.includes('lock'))) {
|
|
279
|
+
throw new ConflictError(result.error);
|
|
280
|
+
}
|
|
281
|
+
throw new ProtocolError(
|
|
282
|
+
result.error_code || 'invalid_state',
|
|
283
|
+
result.error || 'Turn acceptance failed',
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Reject a turn result.
|
|
291
|
+
*
|
|
292
|
+
* @state-provider writeState
|
|
293
|
+
* Writes: .agentxchain/state.json
|
|
294
|
+
* Cloud replacement: state store PUT (compare-and-swap)
|
|
295
|
+
*
|
|
296
|
+
* @state-provider acquireLock
|
|
297
|
+
* Reads/writes: .agentxchain/lock.json
|
|
298
|
+
* Cloud replacement: distributed lock (lease-based, workspace-scoped)
|
|
299
|
+
*
|
|
300
|
+
* @param {string} root - project root
|
|
301
|
+
* @param {object} config - normalized config
|
|
302
|
+
* @param {string} turnId - target turn ID
|
|
303
|
+
* @param {string} reason - rejection reason
|
|
304
|
+
* @param {object} [opts] - additional options
|
|
305
|
+
* @returns {object} rejection result with updated state
|
|
306
|
+
*/
|
|
307
|
+
export function rejectTurnResult(root, config, turnId, reason, opts = {}) {
|
|
308
|
+
requireState(root);
|
|
309
|
+
if (!reason) {
|
|
310
|
+
throw new ValidationError('Rejection reason is required');
|
|
311
|
+
}
|
|
312
|
+
const result = rejectTurn(root, config, null, { ...opts, turnId, reason });
|
|
313
|
+
if (!result.ok) {
|
|
314
|
+
if (result.error_code === 'lock_conflict' || (result.error && result.error.includes('lock'))) {
|
|
315
|
+
throw new ConflictError(result.error);
|
|
316
|
+
}
|
|
317
|
+
throw new ProtocolError(
|
|
318
|
+
result.error_code || 'invalid_state',
|
|
319
|
+
result.error || 'Turn rejection failed',
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Approve a pending phase transition.
|
|
327
|
+
*
|
|
328
|
+
* @state-provider writeState
|
|
329
|
+
* Writes: .agentxchain/state.json
|
|
330
|
+
* Cloud replacement: state store PUT (update phase)
|
|
331
|
+
*
|
|
332
|
+
* @param {string} root - project root
|
|
333
|
+
* @param {object} config - normalized config
|
|
334
|
+
* @param {object} [opts] - additional options
|
|
335
|
+
* @returns {object} approval result with transition details
|
|
336
|
+
*/
|
|
337
|
+
export function approveTransition(root, config, opts = {}) {
|
|
338
|
+
requireState(root);
|
|
339
|
+
const result = approvePhaseGate(root, config, opts);
|
|
340
|
+
if (!result.ok) {
|
|
341
|
+
throw new ProtocolError(
|
|
342
|
+
result.error_code || 'gate_not_satisfied',
|
|
343
|
+
result.error || 'Phase transition approval failed',
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Checkpoint an accepted turn.
|
|
351
|
+
*
|
|
352
|
+
* @state-provider writeState
|
|
353
|
+
* Writes: git commit (checkpoint)
|
|
354
|
+
* Cloud replacement: snapshot store PUT by checkpoint_id
|
|
355
|
+
*
|
|
356
|
+
* @param {string} root - project root
|
|
357
|
+
* @param {string} turnId - turn to checkpoint
|
|
358
|
+
* @returns {object} checkpoint result
|
|
359
|
+
*/
|
|
360
|
+
export function checkpointTurn(root, turnId) {
|
|
361
|
+
requireState(root);
|
|
362
|
+
if (!turnId) {
|
|
363
|
+
throw new ValidationError('turn_id is required');
|
|
364
|
+
}
|
|
365
|
+
const result = checkpointAcceptedTurn(root, { turnId });
|
|
366
|
+
if (result && !result.ok) {
|
|
367
|
+
throw new ProtocolError(
|
|
368
|
+
'checkpoint_not_found',
|
|
369
|
+
result.error || 'Checkpoint failed',
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Retry (reissue) a failed turn.
|
|
377
|
+
*
|
|
378
|
+
* @state-provider writeState
|
|
379
|
+
* Writes: .agentxchain/state.json
|
|
380
|
+
* Cloud replacement: state store PUT (reissue turn)
|
|
381
|
+
*
|
|
382
|
+
* @param {string} root - project root
|
|
383
|
+
* @param {object} config - normalized config
|
|
384
|
+
* @param {string} turnId - turn to retry
|
|
385
|
+
* @returns {object} reissue result
|
|
386
|
+
*/
|
|
387
|
+
export function retryTurn(root, config, turnId) {
|
|
388
|
+
requireState(root);
|
|
389
|
+
if (!turnId) {
|
|
390
|
+
throw new ValidationError('turn_id is required');
|
|
391
|
+
}
|
|
392
|
+
const result = reissueTurn(root, config, { turnId });
|
|
393
|
+
if (!result.ok) {
|
|
394
|
+
throw new ProtocolError(
|
|
395
|
+
result.error_code || 'invalid_state',
|
|
396
|
+
result.error || 'Turn retry failed',
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get run events.
|
|
404
|
+
*
|
|
405
|
+
* @state-provider readEvents
|
|
406
|
+
* Reads: .agentxchain/events.jsonl
|
|
407
|
+
* Cloud replacement: event store query by run_id with cursor
|
|
408
|
+
*
|
|
409
|
+
* @param {string} root - project root
|
|
410
|
+
* @param {string|null} cursor - pagination cursor
|
|
411
|
+
* @param {number} [limit=25] - page size
|
|
412
|
+
* @returns {{ data: object[], cursor: string|null, has_more: boolean }}
|
|
413
|
+
*/
|
|
414
|
+
export function getEvents(root, cursor, limit = 25) {
|
|
415
|
+
const events = readRunEvents(root);
|
|
416
|
+
return paginate(events, cursor, limit);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get decision ledger.
|
|
421
|
+
*
|
|
422
|
+
* @state-provider readDecisions
|
|
423
|
+
* Reads: .agentxchain/decision-ledger.jsonl
|
|
424
|
+
* Cloud replacement: decision ledger query by run_id
|
|
425
|
+
*
|
|
426
|
+
* @param {string} root - project root
|
|
427
|
+
* @returns {{ data: object[] }}
|
|
428
|
+
*/
|
|
429
|
+
export function getDecisions(root) {
|
|
430
|
+
const ledgerPath = join(root, AGENTXCHAIN_DIR, DECISION_LEDGER_FILE);
|
|
431
|
+
const entries = readJsonl(ledgerPath);
|
|
432
|
+
return { data: entries };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get gate states for the current phase.
|
|
437
|
+
*
|
|
438
|
+
* @state-provider readState
|
|
439
|
+
* Reads: .agentxchain/state.json + gate artifacts
|
|
440
|
+
* Cloud replacement: gate state query by run_id
|
|
441
|
+
*
|
|
442
|
+
* @param {string} root - project root
|
|
443
|
+
* @param {object} config - normalized config
|
|
444
|
+
* @returns {{ data: object[] }}
|
|
445
|
+
*/
|
|
446
|
+
export function getGates(root, config) {
|
|
447
|
+
requireState(root);
|
|
448
|
+
const state = loadState(root, config);
|
|
449
|
+
if (!state) {
|
|
450
|
+
throw new NotFoundError('run', root);
|
|
451
|
+
}
|
|
452
|
+
const gates = state.gates || {};
|
|
453
|
+
const gateList = Object.entries(gates).map(([name, gate]) => ({
|
|
454
|
+
name,
|
|
455
|
+
...gate,
|
|
456
|
+
}));
|
|
457
|
+
return { data: gateList };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Export run as repo-native artifact bundle.
|
|
462
|
+
*
|
|
463
|
+
* @state-provider readState
|
|
464
|
+
* Reads: .agentxchain/ (all state files)
|
|
465
|
+
* Cloud replacement: export service builds bundle from service stores
|
|
466
|
+
*
|
|
467
|
+
* @param {string} root - project root
|
|
468
|
+
* @returns {object} export result with { ok, export }
|
|
469
|
+
*/
|
|
470
|
+
export function exportRun(root) {
|
|
471
|
+
const result = buildRunExport(root);
|
|
472
|
+
if (!result.ok) {
|
|
473
|
+
throw new ProtocolError('internal_error', result.error || 'Export failed');
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
@@ -48,6 +48,18 @@ function isCredentialedGate(config, gateId) {
|
|
|
48
48
|
return config?.gates?.[gateId]?.credentialed === true;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// BUG-59 follow-up — lights-out without blind trust. The --auto-approve /
|
|
52
|
+
// continuous gate path (run.js approveGate) must apply the same credentialed
|
|
53
|
+
// hard-stop as the policy path. Resolve the exit gate for a phase and report
|
|
54
|
+
// whether it is credentialed, so a credentialed / irreversible gate is never
|
|
55
|
+
// auto-approved by the flag path either — it falls through to gate_held and
|
|
56
|
+
// requires a human (approve-transition / approve-completion).
|
|
57
|
+
export function isCredentialedExitGate(config, phase) {
|
|
58
|
+
if (!phase) return false;
|
|
59
|
+
const gateId = config?.routing?.[phase]?.exit_gate;
|
|
60
|
+
return isCredentialedGate(config, gateId);
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
function evaluateRunCompletionPolicy({ gateResult, state, config, policy }) {
|
|
52
64
|
if (isCredentialedGate(config, gateResult?.gate_id)) {
|
|
53
65
|
return {
|