agentxchain 2.155.73 → 2.157.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/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 +2 -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 +47 -0
- package/src/commands/serve.js +64 -0
- package/src/commands/step.js +16 -0
- package/src/commands/verify.js +1 -0
- package/src/lib/adapters/local-cli-adapter.js +184 -0
- 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 +200 -0
- package/src/lib/claude-local-auth.js +75 -1
- package/src/lib/connector-probe.js +21 -0
- package/src/lib/continuous-run.js +75 -4
- package/src/lib/dashboard/bridge-server.js +10 -5
- package/src/lib/governed-state.js +1 -1
- package/src/lib/intake.js +32 -6
- package/src/lib/normalized-config.js +2 -0
- package/src/lib/scope-overlap.js +214 -0
- package/src/lib/stream-json-cost-parser.js +169 -0
- package/src/lib/turn-checkpoint.js +21 -0
- package/src/lib/turn-result-validator.js +25 -0
- package/src/lib/validation.js +11 -3
- package/src/lib/verification-replay.js +125 -4
- package/src/lib/vision-reader.js +7 -2
|
@@ -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.157.0",
|
|
4
4
|
"description": "CLI for AgentXchain — governed multi-agent software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"node": ">=18.17.0 || >=20.5.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
+
"js-yaml": "^4.1.1",
|
|
78
79
|
"vitest": "^4.1.2"
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -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';
|
|
@@ -365,6 +366,12 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
365
366
|
onStartupHeartbeat: ({ elapsed_since_spawn_ms }) => {
|
|
366
367
|
tracker.heartbeat(`Adapter keepalive (${Math.round((elapsed_since_spawn_ms || 0) / 1000)}s since spawn)`);
|
|
367
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
|
+
},
|
|
368
375
|
};
|
|
369
376
|
|
|
370
377
|
const recordOutputActivity = (stream, text) => {
|
|
@@ -438,6 +445,37 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
438
445
|
const transport = runtime ? resolvePromptTransport(runtime) : 'dispatch_bundle_only';
|
|
439
446
|
log(chalk.dim(` Dispatching to local CLI: ${runtime?.command || '(default)'} transport: ${transport}`));
|
|
440
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
|
+
}
|
|
441
479
|
} else if (runtimeType === 'remote_agent') {
|
|
442
480
|
ensureStartingState(null);
|
|
443
481
|
ensureRunningState('request');
|
|
@@ -578,6 +616,15 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
578
616
|
|
|
579
617
|
async approveGate(gateType, state) {
|
|
580
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
|
+
}
|
|
581
628
|
log(chalk.yellow(` Auto-approved ${gateType} gate`));
|
|
582
629
|
return true;
|
|
583
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
|
+
}
|
package/src/commands/step.js
CHANGED
|
@@ -77,6 +77,7 @@ import { consumeNextApprovedIntent } from '../lib/intake.js';
|
|
|
77
77
|
import { failTurnStartup, reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
|
|
78
78
|
import { isKnownTurnRunningProofStream } from '../lib/dispatch-streams.js';
|
|
79
79
|
import { getDispatchProgressRelativePath } from '../lib/dispatch-progress.js';
|
|
80
|
+
import { checkpointAcceptedTurn } from '../lib/turn-checkpoint.js';
|
|
80
81
|
|
|
81
82
|
export async function stepCommand(opts) {
|
|
82
83
|
const context = loadProjectContext();
|
|
@@ -1002,6 +1003,21 @@ export async function stepCommand(opts) {
|
|
|
1002
1003
|
}
|
|
1003
1004
|
|
|
1004
1005
|
printAcceptSummary(acceptResult, config);
|
|
1006
|
+
|
|
1007
|
+
// Auto-checkpoint accepted turn so workspace is clean for next assignment
|
|
1008
|
+
if (!opts.noCheckpoint && existsSync(join(root, '.git'))) {
|
|
1009
|
+
const checkpoint = checkpointAcceptedTurn(root, { turnId: turn.turn_id });
|
|
1010
|
+
if (!checkpoint.ok) {
|
|
1011
|
+
console.log(` ${chalk.yellow('Checkpoint:')} accepted but checkpoint failed`);
|
|
1012
|
+
console.log(` ${chalk.dim('Error:')} ${checkpoint.error}`);
|
|
1013
|
+
console.log(` ${chalk.dim('Retry:')} agentxchain checkpoint-turn --turn ${turn.turn_id}`);
|
|
1014
|
+
console.log('');
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
if (!checkpoint.skipped) {
|
|
1018
|
+
console.log(` ${chalk.dim('Checkpoint:')} ${checkpoint.checkpoint_sha}`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1005
1021
|
} else {
|
|
1006
1022
|
// Reject and potentially retry
|
|
1007
1023
|
console.log(chalk.yellow('Validation failed:'));
|