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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org Runs component — renders cross-project run list as a filterable data table.
|
|
3
|
+
*
|
|
4
|
+
* Pure render function: takes data, returns HTML string.
|
|
5
|
+
* Follows the same pattern as ledger.js filter bar and run-history.js table.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function esc(str) {
|
|
9
|
+
if (str == null) return '';
|
|
10
|
+
return String(str)
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function badge(label, color = 'var(--text-dim)') {
|
|
19
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function statusBadge(status) {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'active': return badge('active', 'var(--green)');
|
|
25
|
+
case 'blocked': return badge('blocked', 'var(--yellow)');
|
|
26
|
+
case 'completed': return badge('completed', 'var(--green)');
|
|
27
|
+
case 'failed': return badge('failed', 'var(--red)');
|
|
28
|
+
default: return badge(status || 'unknown', 'var(--text-dim)');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function phaseBadge(phase) {
|
|
33
|
+
switch (phase) {
|
|
34
|
+
case 'planning': return badge('planning', 'var(--accent)');
|
|
35
|
+
case 'implementation': return badge('implementation', '#38bdf8');
|
|
36
|
+
case 'qa': return badge('qa', 'var(--yellow)');
|
|
37
|
+
default: return badge(phase || 'unknown', 'var(--text-dim)');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatUsd(amount) {
|
|
42
|
+
return '$' + (amount || 0).toFixed(2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function relativeTime(isoString) {
|
|
46
|
+
if (!isoString) return '—';
|
|
47
|
+
try {
|
|
48
|
+
const dt = new Date(isoString);
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const diffMs = now - dt.getTime();
|
|
51
|
+
if (diffMs < 0) return esc(isoString);
|
|
52
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
53
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
54
|
+
const minutes = Math.floor(seconds / 60);
|
|
55
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
56
|
+
const hours = Math.floor(minutes / 60);
|
|
57
|
+
if (hours < 24) return `${hours}h ago`;
|
|
58
|
+
const days = Math.floor(hours / 24);
|
|
59
|
+
return `${days}d ago`;
|
|
60
|
+
} catch {
|
|
61
|
+
return esc(isoString);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function truncateRunId(runId) {
|
|
66
|
+
if (!runId) return '—';
|
|
67
|
+
return runId.length > 20 ? runId.slice(0, 20) + '...' : runId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderFilterBar(runs, filter) {
|
|
71
|
+
const projectSet = new Set(runs.map(r => r.project_name));
|
|
72
|
+
const projectOptions = ['all', ...Array.from(projectSet).sort()];
|
|
73
|
+
const phaseOptions = ['all', 'planning', 'implementation', 'qa'];
|
|
74
|
+
const statusOptions = ['all', 'active', 'blocked', 'completed', 'failed'];
|
|
75
|
+
|
|
76
|
+
function options(list, selected) {
|
|
77
|
+
return list.map(v =>
|
|
78
|
+
`<option value="${esc(v)}"${v === selected ? ' selected' : ''}>${esc(v)}</option>`
|
|
79
|
+
).join('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return `<div class="filter-bar">
|
|
83
|
+
<label class="filter-control">
|
|
84
|
+
Project
|
|
85
|
+
<select data-view-control="org-runs-project">
|
|
86
|
+
${options(projectOptions, filter?.project || 'all')}
|
|
87
|
+
</select>
|
|
88
|
+
</label>
|
|
89
|
+
<label class="filter-control">
|
|
90
|
+
Phase
|
|
91
|
+
<select data-view-control="org-runs-phase">
|
|
92
|
+
${options(phaseOptions, filter?.phase || 'all')}
|
|
93
|
+
</select>
|
|
94
|
+
</label>
|
|
95
|
+
<label class="filter-control">
|
|
96
|
+
Status
|
|
97
|
+
<select data-view-control="org-runs-status">
|
|
98
|
+
${options(statusOptions, filter?.status || 'all')}
|
|
99
|
+
</select>
|
|
100
|
+
</label>
|
|
101
|
+
</div>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function applyFilter(runs, filter) {
|
|
105
|
+
let filtered = runs;
|
|
106
|
+
if (filter?.project && filter.project !== 'all') {
|
|
107
|
+
filtered = filtered.filter(r => r.project_name === filter.project);
|
|
108
|
+
}
|
|
109
|
+
if (filter?.phase && filter.phase !== 'all') {
|
|
110
|
+
filtered = filtered.filter(r => r.phase === filter.phase);
|
|
111
|
+
}
|
|
112
|
+
if (filter?.status && filter.status !== 'all') {
|
|
113
|
+
filtered = filtered.filter(r => r.status === filter.status);
|
|
114
|
+
}
|
|
115
|
+
return filtered;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {{ orgRuns: object, liveMeta: object, filter?: object }} data
|
|
120
|
+
* @returns {string} HTML string
|
|
121
|
+
*/
|
|
122
|
+
export function render(data) {
|
|
123
|
+
const runsData = data?.orgRuns?.data || data?.orgRuns || null;
|
|
124
|
+
const runs = Array.isArray(runsData) ? runsData : (runsData?.data || []);
|
|
125
|
+
const filter = data?.filter || {};
|
|
126
|
+
|
|
127
|
+
if (!runs.length && !filter.project && !filter.phase && !filter.status) {
|
|
128
|
+
return `<div class="placeholder">
|
|
129
|
+
<h2>Org Runs</h2>
|
|
130
|
+
<p>No cross-project runs available. Register projects and start governed runs.</p>
|
|
131
|
+
</div>`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const filtered = applyFilter(runs, filter);
|
|
135
|
+
|
|
136
|
+
const tableRows = filtered.map(r => `<tr>
|
|
137
|
+
<td>${esc(r.project_name)}</td>
|
|
138
|
+
<td class="mono">${esc(truncateRunId(r.run_id))}</td>
|
|
139
|
+
<td>${phaseBadge(r.phase)}</td>
|
|
140
|
+
<td>${statusBadge(r.status)}</td>
|
|
141
|
+
<td>${r.turns_completed || 0}</td>
|
|
142
|
+
<td>${formatUsd(r.cost_usd)}</td>
|
|
143
|
+
<td>${relativeTime(r.started_at)}</td>
|
|
144
|
+
<td>${relativeTime(r.updated_at)}</td>
|
|
145
|
+
</tr>`).join('');
|
|
146
|
+
|
|
147
|
+
return `<div class="section">
|
|
148
|
+
<h3>Org Runs (${filtered.length})</h3>
|
|
149
|
+
${renderFilterBar(runs, filter)}
|
|
150
|
+
<table class="data-table">
|
|
151
|
+
<thead>
|
|
152
|
+
<tr>
|
|
153
|
+
<th>Project</th>
|
|
154
|
+
<th>Run ID</th>
|
|
155
|
+
<th>Phase</th>
|
|
156
|
+
<th>Status</th>
|
|
157
|
+
<th>Turns</th>
|
|
158
|
+
<th>Cost</th>
|
|
159
|
+
<th>Started</th>
|
|
160
|
+
<th>Updated</th>
|
|
161
|
+
</tr>
|
|
162
|
+
</thead>
|
|
163
|
+
<tbody>
|
|
164
|
+
${tableRows || '<tr><td colspan="8" style="text-align:center;color:var(--text-dim)">No runs match filters</td></tr>'}
|
|
165
|
+
</tbody>
|
|
166
|
+
</table>
|
|
167
|
+
</div>`;
|
|
168
|
+
}
|
package/dashboard/index.html
CHANGED
|
@@ -395,6 +395,10 @@
|
|
|
395
395
|
</header>
|
|
396
396
|
<div class="action-banner" id="action-banner"></div>
|
|
397
397
|
<nav>
|
|
398
|
+
<a href="#org-overview">Org Overview</a>
|
|
399
|
+
<a href="#org-runs">Org Runs</a>
|
|
400
|
+
<a href="#org-history">Org History</a>
|
|
401
|
+
<a href="#org-audit-trail">Audit Trail</a>
|
|
398
402
|
<a href="#initiative">Initiative</a>
|
|
399
403
|
<a href="#cross-repo">Cross-Repo</a>
|
|
400
404
|
<a href="#timeline" class="active">Timeline</a>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxchain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.156.0",
|
|
4
4
|
"description": "CLI for AgentXchain — governed multi-agent software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,10 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
28
|
"dev": "node bin/agentxchain.js",
|
|
29
|
-
"test": "
|
|
30
|
-
"test:
|
|
31
|
-
"test:beta": "node --test test/beta-tester-scenarios/*.test.js",
|
|
32
|
-
"test:node": "node --test --test-timeout=60000 --test-concurrency=4 test/*.test.js test/beta-tester-scenarios/*.test.js",
|
|
29
|
+
"test": "vitest run --reporter=verbose",
|
|
30
|
+
"test:watch": "vitest --reporter=verbose",
|
|
33
31
|
"preflight:release": "bash scripts/release-preflight.sh",
|
|
34
32
|
"preflight:release:strict": "bash scripts/release-preflight.sh --strict",
|
|
35
33
|
"check:release-alignment": "node scripts/check-release-alignment.mjs",
|
|
@@ -77,6 +75,7 @@
|
|
|
77
75
|
"node": ">=18.17.0 || >=20.5.0"
|
|
78
76
|
},
|
|
79
77
|
"devDependencies": {
|
|
78
|
+
"js-yaml": "^4.1.1",
|
|
80
79
|
"vitest": "^4.1.2"
|
|
81
80
|
}
|
|
82
81
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, relative, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
7
|
+
const CLI_ROOT = join(SCRIPT_DIR, '..');
|
|
8
|
+
const TEST_ROOT = process.argv[2] ? resolve(process.argv[2]) : join(CLI_ROOT, 'test');
|
|
9
|
+
|
|
10
|
+
function walk(dir) {
|
|
11
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
12
|
+
const files = [];
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
const absolute = join(dir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
files.push(...walk(absolute));
|
|
17
|
+
} else if (entry.isFile() && entry.name.endsWith('.test.js')) {
|
|
18
|
+
files.push(absolute);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseNamedSpecifiers(specifiers) {
|
|
25
|
+
return specifiers
|
|
26
|
+
.split(',')
|
|
27
|
+
.map((part) => part.trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function migrateNamedSpecifiers(specifiers) {
|
|
32
|
+
let importsBefore = false;
|
|
33
|
+
let importsAfter = false;
|
|
34
|
+
const migrated = parseNamedSpecifiers(specifiers).map((specifier) => {
|
|
35
|
+
if (specifier === 'before') {
|
|
36
|
+
importsBefore = true;
|
|
37
|
+
return 'beforeAll';
|
|
38
|
+
}
|
|
39
|
+
if (specifier === 'after') {
|
|
40
|
+
importsAfter = true;
|
|
41
|
+
return 'afterAll';
|
|
42
|
+
}
|
|
43
|
+
return specifier;
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
importsBefore,
|
|
47
|
+
importsAfter,
|
|
48
|
+
importLine: `import { ${migrated.join(', ')} } from 'vitest';`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function migrateSource(source) {
|
|
53
|
+
let importsBefore = false;
|
|
54
|
+
let importsAfter = false;
|
|
55
|
+
let changed = false;
|
|
56
|
+
|
|
57
|
+
let next = source.replace(
|
|
58
|
+
/^import\s+test\s+from\s+['"]node:test['"];$/gm,
|
|
59
|
+
() => {
|
|
60
|
+
changed = true;
|
|
61
|
+
return "import { test } from 'vitest';";
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
next = next.replace(
|
|
66
|
+
/^import\s+\{([^}]+)\}\s+from\s+['"]node:test['"];$/gm,
|
|
67
|
+
(_match, specifiers) => {
|
|
68
|
+
const migrated = migrateNamedSpecifiers(specifiers);
|
|
69
|
+
importsBefore ||= migrated.importsBefore;
|
|
70
|
+
importsAfter ||= migrated.importsAfter;
|
|
71
|
+
changed = true;
|
|
72
|
+
return migrated.importLine;
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (importsBefore) {
|
|
77
|
+
next = next.replace(/\bbefore(?=\s*\()/g, 'beforeAll');
|
|
78
|
+
}
|
|
79
|
+
if (importsAfter) {
|
|
80
|
+
next = next.replace(/\bafter(?=\s*\()/g, 'afterAll');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { changed, source: next };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const changedFiles = [];
|
|
87
|
+
for (const file of walk(TEST_ROOT)) {
|
|
88
|
+
const source = readFileSync(file, 'utf8');
|
|
89
|
+
const migrated = migrateSource(source);
|
|
90
|
+
if (!migrated.changed || migrated.source === source) continue;
|
|
91
|
+
writeFileSync(file, migrated.source);
|
|
92
|
+
changedFiles.push(relative(CLI_ROOT, file));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`Migrated ${changedFiles.length} test files from node:test to vitest.`);
|
|
96
|
+
for (const file of changedFiles) {
|
|
97
|
+
console.log(file);
|
|
98
|
+
}
|
|
@@ -78,7 +78,7 @@ FAIL=0
|
|
|
78
78
|
TARBALL_URL=""
|
|
79
79
|
REGISTRY_CHECKSUM=""
|
|
80
80
|
PACKAGE_NAME="$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json', 'utf8')).name)")"
|
|
81
|
-
PACKAGE_BIN_NAME="$(node -e "const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf8')); if (typeof pkg.bin === 'string') { console.log(pkg.name); process.exit(0); } const
|
|
81
|
+
PACKAGE_BIN_NAME="$(node -e "const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf8')); if (typeof pkg.bin === 'string') { console.log(pkg.name); process.exit(0); } const bins = pkg.bin || {}; if (bins[pkg.name]) { console.log(pkg.name); process.exit(0); } const names = Object.keys(bins); if (names.length === 1) { console.log(names[0]); process.exit(0); } console.error('package.json bin must declare the primary package bin'); process.exit(1);")"
|
|
82
82
|
RUNNER_INTERFACE_VERSION_EXPECTED="$(node --input-type=module -e "import('./src/lib/runner-interface.js').then((mod) => { console.log(mod.RUNNER_INTERFACE_VERSION); }).catch((error) => { console.error(error.message); process.exit(1); });")"
|
|
83
83
|
ADAPTER_INTERFACE_VERSION_EXPECTED="$(node --input-type=module -e "import('./src/lib/adapter-interface.js').then((mod) => { console.log(mod.ADAPTER_INTERFACE_VERSION); }).catch((error) => { console.error(error.message); process.exit(1); });")"
|
|
84
84
|
|
|
@@ -179,15 +179,15 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
|
|
|
179
179
|
fail "No beta-tester scenario tests found for release-gate verification"
|
|
180
180
|
TEST_OUTPUT=""
|
|
181
181
|
TEST_STATUS=1
|
|
182
|
-
elif run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1
|
|
182
|
+
elif run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test -- "${GATE_TEST_ARGS[@]}"; then
|
|
183
183
|
TEST_STATUS=0
|
|
184
184
|
else
|
|
185
185
|
TEST_STATUS=$?
|
|
186
186
|
fi
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if [ "$TEST_STATUS" -eq 0 ] && [ "${
|
|
190
|
-
pass "${
|
|
187
|
+
VITEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed/ { for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+$/) { print $i; exit } }')"
|
|
188
|
+
VITEST_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+failed/ { for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+$/) { print $i; exit } }')"
|
|
189
|
+
if [ "$TEST_STATUS" -eq 0 ] && [ "${VITEST_FAIL:-0}" = "0" ]; then
|
|
190
|
+
pass "${VITEST_PASS:-?} release-gate tests passed, 0 failures"
|
|
191
191
|
else
|
|
192
192
|
fail "Release-gate tests failed"
|
|
193
193
|
printf '%s\n' "$TEST_OUTPUT" | tail -20
|
|
@@ -21,7 +21,7 @@ TARGET_VERSION=""
|
|
|
21
21
|
|
|
22
22
|
FORMULA_PATH="${CLI_DIR}/homebrew/agentxchain.rb"
|
|
23
23
|
PACKAGE_NAME="$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).name)")"
|
|
24
|
-
PACKAGE_BIN_NAME="$(node -e "const pkg = JSON.parse(require('fs').readFileSync('package.json','utf8')); if (typeof pkg.bin === 'string') { console.log(pkg.name); process.exit(0); } const
|
|
24
|
+
PACKAGE_BIN_NAME="$(node -e "const pkg = JSON.parse(require('fs').readFileSync('package.json','utf8')); if (typeof pkg.bin === 'string') { console.log(pkg.name); process.exit(0); } const bins = pkg.bin || {}; if (bins[pkg.name]) { console.log(pkg.name); process.exit(0); } const names = Object.keys(bins); if (names.length === 1) { console.log(names[0]); process.exit(0); } console.error('package.json bin must declare the primary package bin'); process.exit(1);")"
|
|
25
25
|
|
|
26
26
|
formula_url() {
|
|
27
27
|
local formula_path="$1"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { buildRunExport } from '../lib/export.js';
|
|
2
|
+
import { loadExportArtifact } from '../lib/export-verifier.js';
|
|
3
|
+
import { buildGovernanceReport } from '../lib/report.js';
|
|
4
|
+
import {
|
|
5
|
+
deriveCIExitCode,
|
|
6
|
+
detectCIEnvironment,
|
|
7
|
+
formatGitHubAnnotations,
|
|
8
|
+
formatJUnitXml,
|
|
9
|
+
writeGitHubOutputVars,
|
|
10
|
+
} from '../lib/ci-reporter.js';
|
|
11
|
+
|
|
12
|
+
export async function ciReportCommand(options) {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const format = options.format || 'auto';
|
|
15
|
+
|
|
16
|
+
// Step 1: Build or load the governance report
|
|
17
|
+
let report;
|
|
18
|
+
if (options.input && options.input !== '-') {
|
|
19
|
+
const loaded = loadExportArtifact(options.input, cwd);
|
|
20
|
+
if (!loaded.ok) {
|
|
21
|
+
console.error(loaded.error);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const result = buildGovernanceReport(loaded.artifact, { input: loaded.input });
|
|
26
|
+
report = result.report;
|
|
27
|
+
} else {
|
|
28
|
+
// Build from current project
|
|
29
|
+
const exportResult = buildRunExport(cwd);
|
|
30
|
+
if (!exportResult.ok) {
|
|
31
|
+
console.error(exportResult.error);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const result = buildGovernanceReport(exportResult.export, { input: 'current-project' });
|
|
36
|
+
report = result.report;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Step 2: Detect CI environment and resolve format
|
|
40
|
+
const ci = detectCIEnvironment();
|
|
41
|
+
let resolvedFormat = format;
|
|
42
|
+
if (resolvedFormat === 'auto') {
|
|
43
|
+
if (ci?.provider === 'github_actions') {
|
|
44
|
+
resolvedFormat = 'github-actions';
|
|
45
|
+
} else {
|
|
46
|
+
resolvedFormat = 'json';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Step 3: Emit formatted output
|
|
51
|
+
switch (resolvedFormat) {
|
|
52
|
+
case 'github-actions': {
|
|
53
|
+
const annotations = formatGitHubAnnotations(report);
|
|
54
|
+
console.log(annotations);
|
|
55
|
+
|
|
56
|
+
// Write output variables if $GITHUB_OUTPUT is set
|
|
57
|
+
const ghOutput = process.env.GITHUB_OUTPUT;
|
|
58
|
+
if (ghOutput) {
|
|
59
|
+
writeGitHubOutputVars(report, ghOutput);
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'junit-xml': {
|
|
64
|
+
const xml = formatJUnitXml(report);
|
|
65
|
+
console.log(xml);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'json': {
|
|
69
|
+
console.log(JSON.stringify(report, null, 2));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
console.error(`Unsupported ci-report format "${resolvedFormat}". Use "github-actions", "junit-xml", "json", or "auto".`);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 4: Exit with CI-appropriate code
|
|
79
|
+
process.exitCode = deriveCIExitCode(report);
|
|
80
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -21,7 +21,7 @@ import { detectActiveTurnBindingDrift, detectStateBundleDesync } from '../lib/go
|
|
|
21
21
|
import { findPendingApprovedIntents } from '../lib/intake.js';
|
|
22
22
|
import { checkCleanBaseline } from '../lib/repo-observer.js';
|
|
23
23
|
import { probeRuntimeSpawnContext } from '../lib/runtime-spawn-context.js';
|
|
24
|
-
import { getClaudeSubprocessAuthIssue } from '../lib/claude-local-auth.js';
|
|
24
|
+
import { getClaudeSubprocessAuthIssue, isCursorLocalCliRuntime, isWindsurfLocalCliRuntime, isOpenCodeLocalCliRuntime } from '../lib/claude-local-auth.js';
|
|
25
25
|
|
|
26
26
|
export async function doctorCommand(opts = {}) {
|
|
27
27
|
const root = findProjectRoot(process.cwd());
|
|
@@ -502,6 +502,27 @@ async function checkRuntimeReachable(root, rtId, rt, boundRoleEntries = []) {
|
|
|
502
502
|
case 'local_cli': {
|
|
503
503
|
const probe = probeRuntimeSpawnContext(root, rt, { runtimeId: rtId });
|
|
504
504
|
if (probe.ok) {
|
|
505
|
+
if (isCursorLocalCliRuntime(rt)) {
|
|
506
|
+
return attachRuntimeContract({
|
|
507
|
+
...base,
|
|
508
|
+
level: 'pass',
|
|
509
|
+
detail: `${probe.detail} (Cursor IDE local_cli connector)`,
|
|
510
|
+
}, rtId, rt, boundRoleEntries);
|
|
511
|
+
}
|
|
512
|
+
if (isWindsurfLocalCliRuntime(rt)) {
|
|
513
|
+
return attachRuntimeContract({
|
|
514
|
+
...base,
|
|
515
|
+
level: 'pass',
|
|
516
|
+
detail: `${probe.detail} (Windsurf IDE local_cli connector)`,
|
|
517
|
+
}, rtId, rt, boundRoleEntries);
|
|
518
|
+
}
|
|
519
|
+
if (isOpenCodeLocalCliRuntime(rt)) {
|
|
520
|
+
return attachRuntimeContract({
|
|
521
|
+
...base,
|
|
522
|
+
level: 'pass',
|
|
523
|
+
detail: `${probe.detail} (OpenCode local_cli connector)`,
|
|
524
|
+
}, rtId, rt, boundRoleEntries);
|
|
525
|
+
}
|
|
505
526
|
const claudeAuthIssue = await getClaudeSubprocessAuthIssue(rt);
|
|
506
527
|
if (claudeAuthIssue) {
|
|
507
528
|
return attachRuntimeContract({
|
package/src/commands/replay.js
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -18,6 +18,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
|
18
18
|
import { join } from 'path';
|
|
19
19
|
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
20
20
|
import { runLoop } from '../lib/run-loop.js';
|
|
21
|
+
import { isCredentialedExitGate } from '../lib/approval-policy.js';
|
|
21
22
|
import { transitionActiveTurnLifecycle } from '../lib/runner-interface.js';
|
|
22
23
|
import { buildRunExport } from '../lib/export.js';
|
|
23
24
|
import { buildGovernanceReport, formatGovernanceReportMarkdown } from '../lib/report.js';
|
|
@@ -362,6 +363,15 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
362
363
|
turnId: turn.turn_id,
|
|
363
364
|
onSpawnAttached: ({ pid, at }) => ensureStartingState(pid, at),
|
|
364
365
|
onFirstOutput: ({ at, stream }) => ensureRunningState(stream, at),
|
|
366
|
+
onStartupHeartbeat: ({ elapsed_since_spawn_ms }) => {
|
|
367
|
+
tracker.heartbeat(`Adapter keepalive (${Math.round((elapsed_since_spawn_ms || 0) / 1000)}s since spawn)`);
|
|
368
|
+
},
|
|
369
|
+
onLivenessHeartbeat: ({ elapsed_since_spawn_ms }) => {
|
|
370
|
+
// RB-6: refresh last_activity_at while the subprocess is alive but
|
|
371
|
+
// output-silent, so the stale-turn watchdog does not misfire during
|
|
372
|
+
// long quiet operations (e.g. running a test suite inside the turn).
|
|
373
|
+
tracker.heartbeat(`Subprocess alive (${Math.round((elapsed_since_spawn_ms || 0) / 1000)}s since spawn)`);
|
|
374
|
+
},
|
|
365
375
|
};
|
|
366
376
|
|
|
367
377
|
const recordOutputActivity = (stream, text) => {
|
|
@@ -435,6 +445,37 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
435
445
|
const transport = runtime ? resolvePromptTransport(runtime) : 'dispatch_bundle_only';
|
|
436
446
|
log(chalk.dim(` Dispatching to local CLI: ${runtime?.command || '(default)'} transport: ${transport}`));
|
|
437
447
|
adapterResult = await dispatchLocalCli(projectRoot, state, cfg, adapterOpts);
|
|
448
|
+
|
|
449
|
+
// Rate-limit backoff: if adapter classified as rate_limited + retryable,
|
|
450
|
+
// apply exponential backoff and re-dispatch. Does NOT consume max_turn_retries.
|
|
451
|
+
const RATE_LIMIT_MAX_RETRIES = 5;
|
|
452
|
+
const RATE_LIMIT_BASE_DELAY_MS = 2000;
|
|
453
|
+
const RATE_LIMIT_MAX_DELAY_MS = 120_000;
|
|
454
|
+
const RATE_LIMIT_BACKOFF_MULTIPLIER = 2;
|
|
455
|
+
|
|
456
|
+
let rateLimitAttempt = 0;
|
|
457
|
+
while (
|
|
458
|
+
adapterResult?.classified?.error_class === 'rate_limited' &&
|
|
459
|
+
adapterResult?.classified?.retryable === true &&
|
|
460
|
+
rateLimitAttempt < RATE_LIMIT_MAX_RETRIES
|
|
461
|
+
) {
|
|
462
|
+
rateLimitAttempt++;
|
|
463
|
+
const resetMs = adapterResult.classified.reset_ms;
|
|
464
|
+
const exponentialDelay = Math.min(
|
|
465
|
+
RATE_LIMIT_MAX_DELAY_MS,
|
|
466
|
+
RATE_LIMIT_BASE_DELAY_MS * RATE_LIMIT_BACKOFF_MULTIPLIER ** (rateLimitAttempt - 1),
|
|
467
|
+
);
|
|
468
|
+
const delayMs = resetMs != null ? Math.max(resetMs, RATE_LIMIT_BASE_DELAY_MS) : exponentialDelay;
|
|
469
|
+
// Add jitter: 0-25% of delay
|
|
470
|
+
const jitter = Math.floor(Math.random() * delayMs * 0.25);
|
|
471
|
+
const totalDelay = delayMs + jitter;
|
|
472
|
+
|
|
473
|
+
log(chalk.yellow(` Rate-limited by provider (attempt ${rateLimitAttempt}/${RATE_LIMIT_MAX_RETRIES}). Retrying in ${Math.ceil(totalDelay / 1000)}s...`));
|
|
474
|
+
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
475
|
+
|
|
476
|
+
log(chalk.dim(` Re-dispatching after rate-limit backoff...`));
|
|
477
|
+
adapterResult = await dispatchLocalCli(projectRoot, state, cfg, adapterOpts);
|
|
478
|
+
}
|
|
438
479
|
} else if (runtimeType === 'remote_agent') {
|
|
439
480
|
ensureStartingState(null);
|
|
440
481
|
ensureRunningState('request');
|
|
@@ -575,6 +616,15 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
575
616
|
|
|
576
617
|
async approveGate(gateType, state) {
|
|
577
618
|
if (autoApprove) {
|
|
619
|
+
// Lights-out without blind trust: a credentialed gate (protecting an
|
|
620
|
+
// irreversible, external, or operator-owned action) is NEVER auto-approved,
|
|
621
|
+
// even under --auto-approve / continuous mode. It falls through to gate_held,
|
|
622
|
+
// so a human must approve it (agentxchain approve-transition / approve-completion).
|
|
623
|
+
if (isCredentialedExitGate(config, state?.phase)) {
|
|
624
|
+
const gateId = config?.routing?.[state?.phase]?.exit_gate;
|
|
625
|
+
log(chalk.red(` Gate "${gateId}" is credentialed — auto-approval refused. A human must approve (lights-out without blind trust).`));
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
578
628
|
log(chalk.yellow(` Auto-approved ${gateType} gate`));
|
|
579
629
|
return true;
|
|
580
630
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentxchain serve — start the hosted runner HTTP server.
|
|
3
|
+
*
|
|
4
|
+
* Exposes control plane API routes for remote run management and dispatches
|
|
5
|
+
* governed turns to cloud agent APIs via the execution worker.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* agentxchain serve [--port 4100] [--host 127.0.0.1] [--project <path>]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { findProjectRoot, loadProjectContext } from '../lib/config.js';
|
|
13
|
+
import { createHostedRunner } from '../lib/api/hosted-runner.js';
|
|
14
|
+
|
|
15
|
+
export async function serveCommand(opts) {
|
|
16
|
+
const rootHint = opts.project || process.cwd();
|
|
17
|
+
const root = findProjectRoot(resolve(rootHint));
|
|
18
|
+
|
|
19
|
+
if (!root) {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
`[agentxchain serve] Error: no agentxchain.json found at or above ${rootHint}\n`
|
|
22
|
+
);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ctx = loadProjectContext(root);
|
|
28
|
+
if (!ctx || !ctx.config) {
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
`[agentxchain serve] Error: failed to load project configuration from ${root}\n`
|
|
31
|
+
);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const port = parseInt(opts.port || '4100', 10);
|
|
37
|
+
const host = opts.host || '127.0.0.1';
|
|
38
|
+
|
|
39
|
+
const additionalProjects = (opts.projects || '')
|
|
40
|
+
.split(',')
|
|
41
|
+
.map(p => p.trim())
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.map(p => resolve(p));
|
|
44
|
+
|
|
45
|
+
const runner = createHostedRunner({
|
|
46
|
+
root,
|
|
47
|
+
config: ctx.config,
|
|
48
|
+
port,
|
|
49
|
+
host,
|
|
50
|
+
projects: additionalProjects,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await runner.start();
|
|
54
|
+
process.stderr.write(`[agentxchain serve] Hosted runner listening on http://${host}:${port}\n`);
|
|
55
|
+
|
|
56
|
+
const shutdown = async () => {
|
|
57
|
+
process.stderr.write('[agentxchain serve] Shutting down...\n');
|
|
58
|
+
await runner.stop();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
process.on('SIGINT', shutdown);
|
|
63
|
+
process.on('SIGTERM', shutdown);
|
|
64
|
+
}
|