agentxchain 0.8.8 → 2.2.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 +136 -136
- package/bin/agentxchain.js +186 -5
- package/dashboard/app.js +305 -0
- package/dashboard/components/blocked.js +145 -0
- package/dashboard/components/cross-repo.js +126 -0
- package/dashboard/components/gate.js +311 -0
- package/dashboard/components/hooks.js +177 -0
- package/dashboard/components/initiative.js +147 -0
- package/dashboard/components/ledger.js +165 -0
- package/dashboard/components/timeline.js +222 -0
- package/dashboard/index.html +352 -0
- package/package.json +14 -6
- package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
- package/scripts/publish-from-tag.sh +88 -0
- package/scripts/release-postflight.sh +231 -0
- package/scripts/release-preflight.sh +167 -0
- package/src/commands/accept-turn.js +160 -0
- package/src/commands/approve-completion.js +80 -0
- package/src/commands/approve-transition.js +85 -0
- package/src/commands/dashboard.js +70 -0
- package/src/commands/init.js +516 -0
- package/src/commands/migrate.js +348 -0
- package/src/commands/multi.js +549 -0
- package/src/commands/plugin.js +157 -0
- package/src/commands/reject-turn.js +204 -0
- package/src/commands/resume.js +389 -0
- package/src/commands/status.js +196 -3
- package/src/commands/step.js +947 -0
- package/src/commands/template-list.js +33 -0
- package/src/commands/template-set.js +279 -0
- package/src/commands/validate.js +20 -11
- package/src/commands/verify.js +71 -0
- package/src/lib/adapters/api-proxy-adapter.js +1076 -0
- package/src/lib/adapters/local-cli-adapter.js +337 -0
- package/src/lib/adapters/manual-adapter.js +169 -0
- package/src/lib/blocked-state.js +94 -0
- package/src/lib/config.js +97 -1
- package/src/lib/context-compressor.js +121 -0
- package/src/lib/context-section-parser.js +220 -0
- package/src/lib/coordinator-acceptance.js +428 -0
- package/src/lib/coordinator-config.js +461 -0
- package/src/lib/coordinator-dispatch.js +276 -0
- package/src/lib/coordinator-gates.js +487 -0
- package/src/lib/coordinator-hooks.js +239 -0
- package/src/lib/coordinator-recovery.js +523 -0
- package/src/lib/coordinator-state.js +365 -0
- package/src/lib/cross-repo-context.js +247 -0
- package/src/lib/dashboard/bridge-server.js +284 -0
- package/src/lib/dashboard/file-watcher.js +93 -0
- package/src/lib/dashboard/state-reader.js +96 -0
- package/src/lib/dispatch-bundle.js +568 -0
- package/src/lib/dispatch-manifest.js +252 -0
- package/src/lib/gate-evaluator.js +285 -0
- package/src/lib/governed-state.js +2139 -0
- package/src/lib/governed-templates.js +145 -0
- package/src/lib/hook-runner.js +788 -0
- package/src/lib/normalized-config.js +539 -0
- package/src/lib/plugin-config-schema.js +192 -0
- package/src/lib/plugins.js +692 -0
- package/src/lib/protocol-conformance.js +291 -0
- package/src/lib/reference-conformance-adapter.js +858 -0
- package/src/lib/repo-observer.js +597 -0
- package/src/lib/repo.js +0 -31
- package/src/lib/schema.js +121 -0
- package/src/lib/schemas/turn-result.schema.json +205 -0
- package/src/lib/token-budget.js +206 -0
- package/src/lib/token-counter.js +27 -0
- package/src/lib/turn-paths.js +67 -0
- package/src/lib/turn-result-validator.js +496 -0
- package/src/lib/validation.js +137 -0
- package/src/templates/governed/api-service.json +31 -0
- package/src/templates/governed/cli-tool.json +30 -0
- package/src/templates/governed/generic.json +10 -0
- package/src/templates/governed/web-app.json +30 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { appendFileSync, copyFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { loadProjectContext, loadProjectState } from './config.js';
|
|
4
|
+
import { assignGovernedTurn, getActiveTurnCount } from './governed-state.js';
|
|
5
|
+
import { writeDispatchBundle } from './dispatch-bundle.js';
|
|
6
|
+
import { getDispatchTurnDir } from './turn-paths.js';
|
|
7
|
+
import { readBarriers, readCoordinatorHistory } from './coordinator-state.js';
|
|
8
|
+
import { generateCrossRepoContext } from './cross-repo-context.js';
|
|
9
|
+
|
|
10
|
+
function loadRepoRuntime(repoPath) {
|
|
11
|
+
const context = loadProjectContext(repoPath);
|
|
12
|
+
if (!context) {
|
|
13
|
+
return { ok: false, reason: 'repo_context_invalid', detail: `Repo at "${repoPath}" is not a loadable governed project` };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const state = loadProjectState(context.root, context.config);
|
|
17
|
+
if (!state) {
|
|
18
|
+
return { ok: false, reason: 'repo_state_missing', detail: `Repo at "${repoPath}" has no governed state` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
root: context.root,
|
|
24
|
+
config: context.config,
|
|
25
|
+
state,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getWorkstreamCandidates(state, config) {
|
|
30
|
+
const ordered = Array.isArray(config.workstream_order) ? [...config.workstream_order] : Object.keys(config.workstreams || {});
|
|
31
|
+
const currentPhase = state.phase;
|
|
32
|
+
const phaseCandidates = ordered.filter((workstreamId) => config.workstreams?.[workstreamId]?.phase === currentPhase);
|
|
33
|
+
const entryWorkstream = config.routing?.[currentPhase]?.entry_workstream;
|
|
34
|
+
|
|
35
|
+
if (!entryWorkstream || !phaseCandidates.includes(entryWorkstream)) {
|
|
36
|
+
return phaseCandidates;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [entryWorkstream, ...phaseCandidates.filter((workstreamId) => workstreamId !== entryWorkstream)];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getAcceptedRepoIds(history, workstreamId) {
|
|
43
|
+
return new Set(
|
|
44
|
+
history
|
|
45
|
+
.filter((entry) => entry?.type === 'acceptance_projection' && entry.workstream_id === workstreamId)
|
|
46
|
+
.map((entry) => entry.repo_id),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function barrierBlocksRepo(barrier, repoId, roleId) {
|
|
51
|
+
if (!barrier || barrier.status === 'satisfied') {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(barrier.blocked_assignments)) {
|
|
56
|
+
for (const assignment of barrier.blocked_assignments) {
|
|
57
|
+
if (assignment === repoId || assignment === `${repoId}:${roleId}` || assignment === `${repoId}:*`) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (Array.isArray(barrier.downstream_repos) && barrier.downstream_repos.includes(repoId)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (barrier.type === 'shared_human_gate' && Array.isArray(barrier.required_repos) && barrier.required_repos.includes(repoId)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getDependencyBarrierId(workstreamId) {
|
|
75
|
+
return `${workstreamId}_completion`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveRecommendedRole(repoState, repoConfig, workstreamPhase) {
|
|
79
|
+
const routing = repoConfig.routing?.[workstreamPhase];
|
|
80
|
+
const recommendedRole = repoState.next_recommended_role;
|
|
81
|
+
if (
|
|
82
|
+
recommendedRole
|
|
83
|
+
&& repoConfig.roles?.[recommendedRole]
|
|
84
|
+
&& (!routing?.allowed_next_roles || routing.allowed_next_roles.includes(recommendedRole))
|
|
85
|
+
) {
|
|
86
|
+
return recommendedRole;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (routing?.entry_role) {
|
|
90
|
+
return routing.entry_role;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Object.keys(repoConfig.roles || {})[0] || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function evaluateWorkstream(workspacePath, state, config, workstreamId, history, barriers) {
|
|
97
|
+
const workstream = config.workstreams?.[workstreamId];
|
|
98
|
+
if (!workstream) {
|
|
99
|
+
return { ok: false, reason: 'workstream_missing', detail: `Unknown workstream "${workstreamId}"` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const dependencyId of workstream.depends_on || []) {
|
|
103
|
+
const dependencyBarrier = barriers[getDependencyBarrierId(dependencyId)];
|
|
104
|
+
if (!dependencyBarrier || dependencyBarrier.status !== 'satisfied') {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: 'dependency_pending',
|
|
108
|
+
detail: `Dependency "${dependencyId}" is not satisfied`,
|
|
109
|
+
workstream_id: workstreamId,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const acceptedRepoIds = getAcceptedRepoIds(history, workstreamId);
|
|
115
|
+
const candidateRepos = acceptedRepoIds.size === 0
|
|
116
|
+
? [workstream.entry_repo]
|
|
117
|
+
: workstream.repos.filter((repoId) => !acceptedRepoIds.has(repoId));
|
|
118
|
+
|
|
119
|
+
if (candidateRepos.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: 'workstream_complete',
|
|
123
|
+
detail: `Workstream "${workstreamId}" already has accepted projections for all repos`,
|
|
124
|
+
workstream_id: workstreamId,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const repoId = candidateRepos[0];
|
|
129
|
+
const repo = config.repos?.[repoId];
|
|
130
|
+
const runtime = loadRepoRuntime(repo.resolved_path);
|
|
131
|
+
if (!runtime.ok) {
|
|
132
|
+
return { ok: false, reason: runtime.reason, detail: runtime.detail, workstream_id: workstreamId, repo_id: repoId };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (runtime.state.status === 'blocked') {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: 'repo_blocked',
|
|
139
|
+
detail: `Repo "${repoId}" is blocked`,
|
|
140
|
+
workstream_id: workstreamId,
|
|
141
|
+
repo_id: repoId,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (getActiveTurnCount(runtime.state) > 0) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: 'repo_busy',
|
|
149
|
+
detail: `Repo "${repoId}" already has an active turn`,
|
|
150
|
+
workstream_id: workstreamId,
|
|
151
|
+
repo_id: repoId,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const role = resolveRecommendedRole(runtime.state, runtime.config, workstream.phase);
|
|
156
|
+
if (!role) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: 'role_unresolved',
|
|
160
|
+
detail: `Repo "${repoId}" has no routing role for phase "${workstream.phase}"`,
|
|
161
|
+
workstream_id: workstreamId,
|
|
162
|
+
repo_id: repoId,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const barrier of Object.values(barriers)) {
|
|
167
|
+
if (barrier?.workstream_id !== workstreamId) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (barrier.type === 'all_repos_accepted') {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (barrierBlocksRepo(barrier, repoId, role)) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
reason: 'barrier_blocked',
|
|
177
|
+
detail: `Barrier "${barrier.barrier_id || 'unknown'}" blocks repo "${repoId}"`,
|
|
178
|
+
workstream_id: workstreamId,
|
|
179
|
+
repo_id: repoId,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
workstream_id: workstreamId,
|
|
187
|
+
repo_id: repoId,
|
|
188
|
+
role,
|
|
189
|
+
reason: acceptedRepoIds.size === 0 ? 'entry_repo' : 'next_unaccepted_repo',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function appendCoordinatorHistory(workspacePath, entry) {
|
|
194
|
+
appendFileSync(join(workspacePath, '.agentxchain/multirepo/history.jsonl'), JSON.stringify(entry) + '\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function selectNextAssignment(workspacePath, state, config) {
|
|
198
|
+
const history = readCoordinatorHistory(workspacePath);
|
|
199
|
+
const barriers = readBarriers(workspacePath);
|
|
200
|
+
const candidates = getWorkstreamCandidates(state, config);
|
|
201
|
+
let firstFailure = null;
|
|
202
|
+
|
|
203
|
+
for (const workstreamId of candidates) {
|
|
204
|
+
const evaluation = evaluateWorkstream(workspacePath, state, config, workstreamId, history, barriers);
|
|
205
|
+
if (evaluation.ok) {
|
|
206
|
+
return evaluation;
|
|
207
|
+
}
|
|
208
|
+
if (!firstFailure) {
|
|
209
|
+
firstFailure = evaluation;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return firstFailure || { ok: false, reason: 'no_assignable_workstream', detail: 'No workstream is assignable in the current phase' };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function dispatchCoordinatorTurn(workspacePath, state, config, assignment) {
|
|
217
|
+
if (!assignment?.ok) {
|
|
218
|
+
return { ok: false, error: 'Assignment is required before dispatch' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const repo = config.repos?.[assignment.repo_id];
|
|
222
|
+
if (!repo) {
|
|
223
|
+
return { ok: false, error: `Unknown repo "${assignment.repo_id}"` };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const runtime = loadRepoRuntime(repo.resolved_path);
|
|
227
|
+
if (!runtime.ok) {
|
|
228
|
+
return { ok: false, error: runtime.detail };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const assignResult = assignGovernedTurn(runtime.root, runtime.config, assignment.role);
|
|
232
|
+
if (!assignResult.ok) {
|
|
233
|
+
return { ok: false, error: assignResult.error };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const bundleResult = writeDispatchBundle(runtime.root, assignResult.state, runtime.config);
|
|
237
|
+
if (!bundleResult.ok) {
|
|
238
|
+
return { ok: false, error: bundleResult.error };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const turn = assignResult.state.current_turn;
|
|
242
|
+
const contextResult = generateCrossRepoContext(
|
|
243
|
+
workspacePath,
|
|
244
|
+
state,
|
|
245
|
+
config,
|
|
246
|
+
assignment.repo_id,
|
|
247
|
+
assignment.workstream_id,
|
|
248
|
+
);
|
|
249
|
+
if (!contextResult.ok) {
|
|
250
|
+
return { ok: false, error: contextResult.error };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const bundleDir = join(runtime.root, getDispatchTurnDir(turn.turn_id));
|
|
254
|
+
copyFileSync(contextResult.jsonPath, join(bundleDir, 'COORDINATOR_CONTEXT.json'));
|
|
255
|
+
copyFileSync(contextResult.mdPath, join(bundleDir, 'COORDINATOR_CONTEXT.md'));
|
|
256
|
+
|
|
257
|
+
appendCoordinatorHistory(workspacePath, {
|
|
258
|
+
type: 'turn_dispatched',
|
|
259
|
+
timestamp: new Date().toISOString(),
|
|
260
|
+
super_run_id: state.super_run_id,
|
|
261
|
+
workstream_id: assignment.workstream_id,
|
|
262
|
+
repo_id: assignment.repo_id,
|
|
263
|
+
repo_run_id: runtime.state.run_id,
|
|
264
|
+
repo_turn_id: turn.turn_id,
|
|
265
|
+
role: assignment.role,
|
|
266
|
+
context_ref: contextResult.contextRef,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
ok: true,
|
|
271
|
+
repo_id: assignment.repo_id,
|
|
272
|
+
turn_id: turn.turn_id,
|
|
273
|
+
bundle_path: bundleResult.bundlePath,
|
|
274
|
+
context_ref: contextResult.contextRef,
|
|
275
|
+
};
|
|
276
|
+
}
|