agentxchain 2.91.0 → 2.93.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 +12 -2
- package/package.json +1 -1
- package/src/commands/audit.js +7 -1
- package/src/commands/decisions.js +94 -0
- package/src/commands/doctor.js +19 -1
- package/src/commands/report.js +7 -1
- package/src/commands/status.js +14 -0
- package/src/lib/adapters/local-cli-adapter.js +1 -1
- package/src/lib/dispatch-bundle.js +9 -0
- package/src/lib/export.js +60 -0
- package/src/lib/governed-state.js +135 -3
- package/src/lib/repo-decisions.js +100 -0
- package/src/lib/repo-observer.js +39 -5
- package/src/lib/report.js +618 -0
- package/src/lib/run-loop.js +16 -0
- package/src/lib/schemas/turn-result.schema.json +10 -0
- package/src/lib/turn-result-validator.js +13 -0
package/bin/agentxchain.js
CHANGED
|
@@ -113,6 +113,7 @@ import { intakeResolveCommand } from '../src/commands/intake-resolve.js';
|
|
|
113
113
|
import { intakeStatusCommand } from '../src/commands/intake-status.js';
|
|
114
114
|
import { demoCommand } from '../src/commands/demo.js';
|
|
115
115
|
import { historyCommand } from '../src/commands/history.js';
|
|
116
|
+
import { decisionsCommand } from '../src/commands/decisions.js';
|
|
116
117
|
import { diffCommand } from '../src/commands/diff.js';
|
|
117
118
|
import { eventsCommand } from '../src/commands/events.js';
|
|
118
119
|
import { connectorCheckCommand } from '../src/commands/connector.js';
|
|
@@ -157,7 +158,7 @@ program
|
|
|
157
158
|
program
|
|
158
159
|
.command('audit')
|
|
159
160
|
.description('Render a governance audit directly from the current governed project or coordinator workspace')
|
|
160
|
-
.option('--format <format>', 'Output format: text, json, or
|
|
161
|
+
.option('--format <format>', 'Output format: text, json, markdown, or html', 'text')
|
|
161
162
|
.action(auditCommand);
|
|
162
163
|
|
|
163
164
|
program
|
|
@@ -176,7 +177,7 @@ program
|
|
|
176
177
|
.command('report')
|
|
177
178
|
.description('Render a human-readable governance summary from an export artifact')
|
|
178
179
|
.option('--input <path>', 'Export artifact path, or "-" for stdin', '-')
|
|
179
|
-
.option('--format <format>', 'Output format: text, json, or
|
|
180
|
+
.option('--format <format>', 'Output format: text, json, markdown, or html', 'text')
|
|
180
181
|
.action(reportCommand);
|
|
181
182
|
|
|
182
183
|
program
|
|
@@ -333,6 +334,15 @@ program
|
|
|
333
334
|
.option('-d, --dir <path>', 'Project directory')
|
|
334
335
|
.action(historyCommand);
|
|
335
336
|
|
|
337
|
+
program
|
|
338
|
+
.command('decisions')
|
|
339
|
+
.description('Show repo-level decisions that persist across governed runs')
|
|
340
|
+
.option('-j, --json', 'Output as JSON')
|
|
341
|
+
.option('-a, --all', 'Include overridden decisions')
|
|
342
|
+
.option('-s, --show <id>', 'Show details for a specific decision (e.g. DEC-042)')
|
|
343
|
+
.option('-d, --dir <path>', 'Project directory')
|
|
344
|
+
.action(decisionsCommand);
|
|
345
|
+
|
|
336
346
|
program
|
|
337
347
|
.command('diff <left_run_id> <right_run_id>')
|
|
338
348
|
.description('Compare two recorded governed runs from run-history')
|
package/package.json
CHANGED
package/src/commands/audit.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { buildCoordinatorExport, buildRunExport } from '../lib/export.js';
|
|
4
4
|
import {
|
|
5
5
|
buildGovernanceReport,
|
|
6
|
+
formatGovernanceReportHtml,
|
|
6
7
|
formatGovernanceReportMarkdown,
|
|
7
8
|
formatGovernanceReportText,
|
|
8
9
|
} from '../lib/report.js';
|
|
@@ -43,6 +44,11 @@ function printAndExit(report, format, exitCode) {
|
|
|
43
44
|
process.exit(exitCode);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
if (format === 'html') {
|
|
48
|
+
console.log(formatGovernanceReportHtml(report));
|
|
49
|
+
process.exit(exitCode);
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
if (format === 'text') {
|
|
47
53
|
if (report.overall === 'error' || report.overall === 'fail') {
|
|
48
54
|
console.log(chalk.red(formatGovernanceReportText(report)));
|
|
@@ -52,7 +58,7 @@ function printAndExit(report, format, exitCode) {
|
|
|
52
58
|
process.exit(exitCode);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
console.error(`Unsupported audit format "${format}". Use "text", "json", or "
|
|
61
|
+
console.error(`Unsupported audit format "${format}". Use "text", "json", "markdown", or "html".`);
|
|
56
62
|
process.exit(2);
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentxchain decisions — cross-run decision carryover surface.
|
|
3
|
+
*
|
|
4
|
+
* Shows repo-level decisions that persist across governed runs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { readRepoDecisions, getActiveRepoDecisions, getRepoDecisionById } from '../lib/repo-decisions.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} opts - { json?: boolean, all?: boolean, show?: string, dir?: string }
|
|
14
|
+
*/
|
|
15
|
+
export async function decisionsCommand(opts) {
|
|
16
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
17
|
+
if (!root) {
|
|
18
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Show single decision ───────────────────────────────────────────────
|
|
23
|
+
if (opts.show) {
|
|
24
|
+
const dec = getRepoDecisionById(root, opts.show);
|
|
25
|
+
if (!dec) {
|
|
26
|
+
console.error(chalk.red(`Decision ${opts.show} not found in repo decisions.`));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
console.log(JSON.stringify(dec, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(chalk.bold(`Decision ${dec.id}`));
|
|
34
|
+
console.log(` Category: ${dec.category}`);
|
|
35
|
+
console.log(` Statement: ${dec.statement}`);
|
|
36
|
+
console.log(` Rationale: ${dec.rationale}`);
|
|
37
|
+
console.log(` Status: ${formatStatus(dec.status)}`);
|
|
38
|
+
console.log(` Role: ${dec.role || '—'}`);
|
|
39
|
+
console.log(` Phase: ${dec.phase || '—'}`);
|
|
40
|
+
console.log(` Run: ${(dec.run_id || '—').slice(0, 16)}`);
|
|
41
|
+
console.log(` Turn: ${(dec.turn_id || '—').slice(0, 16)}`);
|
|
42
|
+
console.log(` Created: ${dec.created_at || '—'}`);
|
|
43
|
+
if (dec.overridden_by) {
|
|
44
|
+
console.log(` Overridden: ${chalk.yellow(dec.overridden_by)}`);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── List decisions ─────────────────────────────────────────────────────
|
|
50
|
+
const decisions = opts.all ? readRepoDecisions(root) : getActiveRepoDecisions(root);
|
|
51
|
+
|
|
52
|
+
if (opts.json) {
|
|
53
|
+
console.log(JSON.stringify(decisions, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (decisions.length === 0) {
|
|
58
|
+
console.log(chalk.dim('No repo-level decisions found.'));
|
|
59
|
+
if (!opts.all) {
|
|
60
|
+
console.log(chalk.dim('Use --all to include overridden decisions.'));
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const label = opts.all ? 'Repo Decisions (all)' : 'Active Repo Decisions';
|
|
66
|
+
console.log(chalk.bold(`${label}: ${decisions.length}`));
|
|
67
|
+
console.log('');
|
|
68
|
+
|
|
69
|
+
for (const dec of decisions) {
|
|
70
|
+
const status = formatStatus(dec.status);
|
|
71
|
+
const runShort = (dec.run_id || '').slice(0, 12);
|
|
72
|
+
const override = dec.overridden_by ? chalk.dim(` → ${dec.overridden_by}`) : '';
|
|
73
|
+
console.log(` ${chalk.cyan(dec.id)} ${status} ${chalk.dim(dec.category)} ${dec.statement}${override}`);
|
|
74
|
+
console.log(` ${chalk.dim(`by ${dec.role || '?'} in ${runShort || '?'}`)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatStatus(status) {
|
|
79
|
+
if (status === 'active') return chalk.green('active');
|
|
80
|
+
if (status === 'overridden') return chalk.yellow('overridden');
|
|
81
|
+
return chalk.dim(status || '—');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function findProjectRoot(dir) {
|
|
85
|
+
let current = resolve(dir);
|
|
86
|
+
while (current !== '/') {
|
|
87
|
+
if (existsSync(resolve(current, 'agentxchain.json'))) return current;
|
|
88
|
+
if (existsSync(resolve(current, '.agentxchain'))) return current;
|
|
89
|
+
const parent = resolve(current, '..');
|
|
90
|
+
if (parent === current) break;
|
|
91
|
+
current = parent;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { loadConfig, loadLock, findProjectRoot } from '../lib/config.js';
|
|
6
6
|
import { validateProject } from '../lib/validation.js';
|
|
7
7
|
import { getWatchPid } from './watch.js';
|
|
8
|
+
import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
8
9
|
import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-config.js';
|
|
9
10
|
import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
|
|
10
11
|
import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
|
|
@@ -219,6 +220,21 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
// 9. Dashboard session health (unconditional — dashboard is a general operator surface)
|
|
224
|
+
{
|
|
225
|
+
const dashPid = getDashboardPid(root);
|
|
226
|
+
const dashSession = getDashboardSession(root);
|
|
227
|
+
if (dashPid && dashSession) {
|
|
228
|
+
checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'pass', detail: `Dashboard running at ${dashSession.url} (PID: ${dashPid})` });
|
|
229
|
+
} else if (dashPid && !dashSession) {
|
|
230
|
+
checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'warn', detail: `Dashboard PID ${dashPid} alive but session file missing` });
|
|
231
|
+
} else if (!dashPid && dashSession) {
|
|
232
|
+
checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'warn', detail: `Stale dashboard session files (PID ${dashSession.pid || '?'} not running)` });
|
|
233
|
+
} else {
|
|
234
|
+
checks.push({ id: 'dashboard_session', name: 'Dashboard session', level: 'info', detail: 'No dashboard session' });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
// Compute summary
|
|
223
239
|
const failCount = checks.filter(c => c.level === 'fail').length;
|
|
224
240
|
const warnCount = checks.filter(c => c.level === 'warn').length;
|
|
@@ -253,7 +269,9 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
253
269
|
? chalk.green('PASS')
|
|
254
270
|
: c.level === 'warn'
|
|
255
271
|
? chalk.yellow('WARN')
|
|
256
|
-
:
|
|
272
|
+
: c.level === 'info'
|
|
273
|
+
? chalk.dim('INFO')
|
|
274
|
+
: chalk.red('FAIL');
|
|
257
275
|
console.log(` ${badge} ${c.name.padEnd(24)} ${chalk.dim(c.detail)}`);
|
|
258
276
|
}
|
|
259
277
|
|
package/src/commands/report.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { loadExportArtifact } from '../lib/export-verifier.js';
|
|
4
4
|
import {
|
|
5
5
|
buildGovernanceReport,
|
|
6
|
+
formatGovernanceReportHtml,
|
|
6
7
|
formatGovernanceReportMarkdown,
|
|
7
8
|
formatGovernanceReportText,
|
|
8
9
|
} from '../lib/report.js';
|
|
@@ -18,6 +19,11 @@ function printAndExit(report, format, exitCode) {
|
|
|
18
19
|
process.exit(exitCode);
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
if (format === 'html') {
|
|
23
|
+
console.log(formatGovernanceReportHtml(report));
|
|
24
|
+
process.exit(exitCode);
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
if (format === 'text') {
|
|
22
28
|
if (report.overall === 'error') {
|
|
23
29
|
console.log(chalk.red(formatGovernanceReportText(report)));
|
|
@@ -29,7 +35,7 @@ function printAndExit(report, format, exitCode) {
|
|
|
29
35
|
process.exit(exitCode);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
console.error(`Unsupported report format "${format}". Use "text", "json", or "
|
|
38
|
+
console.error(`Unsupported report format "${format}". Use "text", "json", "markdown", or "html".`);
|
|
33
39
|
process.exit(1);
|
|
34
40
|
}
|
|
35
41
|
|
package/src/commands/status.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getConnectorHealth } from '../lib/connector-health.js';
|
|
|
9
9
|
import { deriveWorkflowKitArtifacts } from '../lib/workflow-kit-artifacts.js';
|
|
10
10
|
import { evaluateTimeouts } from '../lib/timeout-evaluator.js';
|
|
11
11
|
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
12
|
+
import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
12
13
|
|
|
13
14
|
export async function statusCommand(opts) {
|
|
14
15
|
const context = loadProjectContext();
|
|
@@ -87,6 +88,14 @@ function renderGovernedStatus(context, opts) {
|
|
|
87
88
|
const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
|
|
88
89
|
|
|
89
90
|
if (opts.json) {
|
|
91
|
+
const dashPid = getDashboardPid(root);
|
|
92
|
+
const dashSession = getDashboardSession(root);
|
|
93
|
+
const dashboardSessionObj = dashPid
|
|
94
|
+
? { status: 'running', pid: dashPid, url: dashSession?.url || null, started_at: dashSession?.started_at || null }
|
|
95
|
+
: dashSession
|
|
96
|
+
? { status: 'stale', pid: dashSession.pid || null, url: dashSession.url || null, started_at: dashSession.started_at || null }
|
|
97
|
+
: { status: 'not_running', pid: null, url: null, started_at: null };
|
|
98
|
+
|
|
90
99
|
console.log(JSON.stringify({
|
|
91
100
|
version,
|
|
92
101
|
protocol_mode: config.protocol_mode,
|
|
@@ -96,9 +105,11 @@ function renderGovernedStatus(context, opts) {
|
|
|
96
105
|
state,
|
|
97
106
|
provenance: state?.provenance || null,
|
|
98
107
|
inherited_context: state?.inherited_context || null,
|
|
108
|
+
repo_decisions: state?.repo_decisions || null,
|
|
99
109
|
continuity,
|
|
100
110
|
connector_health: connectorHealth,
|
|
101
111
|
workflow_kit_artifacts: workflowKitArtifacts,
|
|
112
|
+
dashboard_session: dashboardSessionObj,
|
|
102
113
|
}, null, 2));
|
|
103
114
|
return;
|
|
104
115
|
}
|
|
@@ -123,6 +134,9 @@ function renderGovernedStatus(context, opts) {
|
|
|
123
134
|
if (state?.inherited_context?.parent_run_id) {
|
|
124
135
|
console.log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`)}`);
|
|
125
136
|
}
|
|
137
|
+
if (state?.repo_decisions?.length > 0) {
|
|
138
|
+
console.log(` ${chalk.dim('Repo decisions:')} ${chalk.yellow(`${state.repo_decisions.length} active`)}`);
|
|
139
|
+
}
|
|
126
140
|
if (state?.accepted_integration_ref) {
|
|
127
141
|
console.log(` ${chalk.dim('Accepted:')} ${state.accepted_integration_ref}`);
|
|
128
142
|
}
|
|
@@ -115,7 +115,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
115
115
|
child = spawn(command, args, {
|
|
116
116
|
cwd: runtime.cwd ? join(root, runtime.cwd) : root,
|
|
117
117
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
|
-
env: { ...process.env },
|
|
118
|
+
env: { ...process.env, AGENTXCHAIN_TURN_ID: turn.turn_id },
|
|
119
119
|
});
|
|
120
120
|
} catch (err) {
|
|
121
121
|
resolve({ ok: false, error: `Failed to spawn "${command}": ${err.message}`, logs });
|
|
@@ -18,6 +18,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync
|
|
|
18
18
|
import { join } from 'path';
|
|
19
19
|
import { getActiveTurn, getActiveTurns } from './governed-state.js';
|
|
20
20
|
import { renderInheritedContextMarkdown } from './run-context-inheritance.js';
|
|
21
|
+
import { renderRepoDecisionsMarkdown } from './repo-decisions.js';
|
|
21
22
|
import {
|
|
22
23
|
DISPATCH_INDEX_PATH,
|
|
23
24
|
getDispatchAssignmentPath,
|
|
@@ -605,6 +606,14 @@ function renderContext(state, config, root, turn, role) {
|
|
|
605
606
|
lines.push('');
|
|
606
607
|
}
|
|
607
608
|
|
|
609
|
+
// Repo-level decisions that persist across runs
|
|
610
|
+
if (state.repo_decisions && state.repo_decisions.length > 0) {
|
|
611
|
+
const repoDecMd = renderRepoDecisionsMarkdown(state.repo_decisions);
|
|
612
|
+
if (repoDecMd) {
|
|
613
|
+
lines.push(repoDecMd);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
608
617
|
// Inherited context from parent run (when --inherit-context was used)
|
|
609
618
|
if (state.inherited_context) {
|
|
610
619
|
// First turn gets the full rendering; subsequent turns get compact
|
package/src/lib/export.js
CHANGED
|
@@ -7,6 +7,8 @@ import { loadProjectContext, loadProjectState } from './config.js';
|
|
|
7
7
|
import { loadCoordinatorConfig, COORDINATOR_CONFIG_FILE } from './coordinator-config.js';
|
|
8
8
|
import { loadCoordinatorState } from './coordinator-state.js';
|
|
9
9
|
import { normalizeRunProvenance } from './run-provenance.js';
|
|
10
|
+
import { getDashboardPid, getDashboardSession } from '../commands/dashboard.js';
|
|
11
|
+
import { readRepoDecisions } from './repo-decisions.js';
|
|
10
12
|
|
|
11
13
|
const EXPORT_SCHEMA_VERSION = '0.3';
|
|
12
14
|
|
|
@@ -23,10 +25,13 @@ const COORDINATOR_INCLUDED_ROOTS = [
|
|
|
23
25
|
export const RUN_EXPORT_INCLUDED_ROOTS = [
|
|
24
26
|
'agentxchain.json',
|
|
25
27
|
'TALK.md',
|
|
28
|
+
'.agentxchain-dashboard.pid',
|
|
29
|
+
'.agentxchain-dashboard.json',
|
|
26
30
|
'.agentxchain/state.json',
|
|
27
31
|
'.agentxchain/session.json',
|
|
28
32
|
'.agentxchain/history.jsonl',
|
|
29
33
|
'.agentxchain/decision-ledger.jsonl',
|
|
34
|
+
'.agentxchain/repo-decisions.jsonl',
|
|
30
35
|
'.agentxchain/hook-audit.jsonl',
|
|
31
36
|
'.agentxchain/hook-annotations.jsonl',
|
|
32
37
|
'.agentxchain/notification-audit.jsonl',
|
|
@@ -166,6 +171,59 @@ function countDirectoryFiles(files, prefix) {
|
|
|
166
171
|
return Object.keys(files).filter((path) => path.startsWith(`${prefix}/`)).length;
|
|
167
172
|
}
|
|
168
173
|
|
|
174
|
+
function buildDashboardSessionSummary(root) {
|
|
175
|
+
const dashPid = getDashboardPid(root);
|
|
176
|
+
const dashSession = getDashboardSession(root);
|
|
177
|
+
|
|
178
|
+
if (dashPid && dashSession) {
|
|
179
|
+
return {
|
|
180
|
+
status: 'running',
|
|
181
|
+
pid: dashPid,
|
|
182
|
+
url: dashSession.url || null,
|
|
183
|
+
started_at: dashSession.started_at || null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (dashPid && !dashSession) {
|
|
188
|
+
return {
|
|
189
|
+
status: 'pid_only',
|
|
190
|
+
pid: dashPid,
|
|
191
|
+
url: null,
|
|
192
|
+
started_at: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!dashPid && dashSession) {
|
|
197
|
+
return {
|
|
198
|
+
status: 'stale',
|
|
199
|
+
pid: dashSession.pid || null,
|
|
200
|
+
url: dashSession.url || null,
|
|
201
|
+
started_at: dashSession.started_at || null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
status: 'not_running',
|
|
207
|
+
pid: null,
|
|
208
|
+
url: null,
|
|
209
|
+
started_at: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function buildRepoDecisionsSummary(root) {
|
|
214
|
+
const all = readRepoDecisions(root);
|
|
215
|
+
if (!all || all.length === 0) return null;
|
|
216
|
+
const active = all.filter(d => d.status === 'active');
|
|
217
|
+
const overridden = all.filter(d => d.status === 'overridden');
|
|
218
|
+
return {
|
|
219
|
+
total: all.length,
|
|
220
|
+
active_count: active.length,
|
|
221
|
+
overridden_count: overridden.length,
|
|
222
|
+
active: active.map(d => ({ id: d.id, category: d.category, statement: d.statement, role: d.role, run_id: d.run_id })),
|
|
223
|
+
overridden: overridden.map(d => ({ id: d.id, overridden_by: d.overridden_by, statement: d.statement })),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
169
227
|
export function buildDelegationSummary(files) {
|
|
170
228
|
const historyData = files['.agentxchain/history.jsonl']?.data;
|
|
171
229
|
if (!Array.isArray(historyData)) {
|
|
@@ -404,7 +462,9 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
404
462
|
staging_artifact_files: countDirectoryFiles(files, '.agentxchain/staging'),
|
|
405
463
|
intake_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/intake/')),
|
|
406
464
|
coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
|
|
465
|
+
dashboard_session: buildDashboardSessionSummary(root),
|
|
407
466
|
delegation_summary: buildDelegationSummary(files),
|
|
467
|
+
repo_decisions: buildRepoDecisionsSummary(root),
|
|
408
468
|
},
|
|
409
469
|
workspace: buildRunWorkspaceMetadata(root),
|
|
410
470
|
files,
|
|
@@ -44,6 +44,12 @@ import { emitRunEvent } from './run-events.js';
|
|
|
44
44
|
import { writeSessionCheckpoint } from './session-checkpoint.js';
|
|
45
45
|
import { recordRunHistory } from './run-history.js';
|
|
46
46
|
import { buildDefaultRunProvenance } from './run-provenance.js';
|
|
47
|
+
import {
|
|
48
|
+
getActiveRepoDecisions,
|
|
49
|
+
appendRepoDecision,
|
|
50
|
+
overrideRepoDecision,
|
|
51
|
+
validateOverride,
|
|
52
|
+
} from './repo-decisions.js';
|
|
47
53
|
import {
|
|
48
54
|
replayVerificationMachineEvidence,
|
|
49
55
|
summarizeVerificationReplay,
|
|
@@ -799,6 +805,61 @@ function readJsonlEntries(root, relPath) {
|
|
|
799
805
|
.filter(Boolean);
|
|
800
806
|
}
|
|
801
807
|
|
|
808
|
+
function collectPendingConcurrentSiblingDeclarations(root, state, currentTurn, historyEntries = []) {
|
|
809
|
+
const concurrentIds = new Set(
|
|
810
|
+
Array.isArray(currentTurn?.concurrent_with)
|
|
811
|
+
? currentTurn.concurrent_with.filter((id) => typeof id === 'string' && id.length > 0)
|
|
812
|
+
: [],
|
|
813
|
+
);
|
|
814
|
+
const activeTurns = getActiveTurns(state);
|
|
815
|
+
for (const turn of Object.values(activeTurns)) {
|
|
816
|
+
if (
|
|
817
|
+
turn?.turn_id !== currentTurn?.turn_id
|
|
818
|
+
&& Array.isArray(turn?.concurrent_with)
|
|
819
|
+
&& turn.concurrent_with.includes(currentTurn?.turn_id)
|
|
820
|
+
) {
|
|
821
|
+
concurrentIds.add(turn.turn_id);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (concurrentIds.size === 0) {
|
|
825
|
+
return [];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const acceptedIds = new Set(
|
|
829
|
+
(Array.isArray(historyEntries) ? historyEntries : [])
|
|
830
|
+
.map((entry) => entry?.turn_id)
|
|
831
|
+
.filter((turnId) => typeof turnId === 'string' && turnId.length > 0),
|
|
832
|
+
);
|
|
833
|
+
const declarations = [];
|
|
834
|
+
|
|
835
|
+
for (const siblingTurnId of concurrentIds) {
|
|
836
|
+
if (siblingTurnId === currentTurn?.turn_id || acceptedIds.has(siblingTurnId) || !activeTurns[siblingTurnId]) {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const stagedSibling = loadHookStagedTurn(root, getTurnStagingResultPath(siblingTurnId));
|
|
841
|
+
const siblingResult = stagedSibling.turnResult;
|
|
842
|
+
if (!siblingResult || siblingResult.turn_id !== siblingTurnId) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const siblingFiles = [...new Set(
|
|
847
|
+
(Array.isArray(siblingResult.files_changed) ? siblingResult.files_changed : [])
|
|
848
|
+
.filter((filePath) => typeof filePath === 'string' && filePath.length > 0),
|
|
849
|
+
)];
|
|
850
|
+
if (siblingFiles.length === 0) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
declarations.push({
|
|
855
|
+
turn_id: siblingTurnId,
|
|
856
|
+
files_changed: siblingFiles,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return declarations;
|
|
861
|
+
}
|
|
862
|
+
|
|
802
863
|
function getObservedFiles(entry) {
|
|
803
864
|
if (Array.isArray(entry?.observed_artifact?.files_changed)) {
|
|
804
865
|
return entry.observed_artifact.files_changed;
|
|
@@ -1853,6 +1914,7 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
1853
1914
|
const runId = generateId('run');
|
|
1854
1915
|
const now = new Date().toISOString();
|
|
1855
1916
|
const provenance = buildDefaultRunProvenance(options.provenance);
|
|
1917
|
+
const repoDecisions = getActiveRepoDecisions(root);
|
|
1856
1918
|
const updatedState = {
|
|
1857
1919
|
...state,
|
|
1858
1920
|
run_id: runId,
|
|
@@ -1867,6 +1929,7 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
1867
1929
|
},
|
|
1868
1930
|
provenance,
|
|
1869
1931
|
inherited_context: options.inherited_context || null,
|
|
1932
|
+
repo_decisions: repoDecisions.length > 0 ? repoDecisions : null,
|
|
1870
1933
|
};
|
|
1871
1934
|
|
|
1872
1935
|
writeState(root, updatedState);
|
|
@@ -2368,12 +2431,39 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2368
2431
|
}
|
|
2369
2432
|
|
|
2370
2433
|
const turnResult = validation.turnResult;
|
|
2434
|
+
|
|
2435
|
+
// Validate cross-run decision overrides against repo-decisions.jsonl
|
|
2436
|
+
if (turnResult.decisions && turnResult.decisions.length > 0) {
|
|
2437
|
+
for (const dec of turnResult.decisions) {
|
|
2438
|
+
if (dec.overrides) {
|
|
2439
|
+
const overrideCheck = validateOverride(root, dec);
|
|
2440
|
+
if (!overrideCheck.ok) {
|
|
2441
|
+
return {
|
|
2442
|
+
ok: false,
|
|
2443
|
+
error: `Override validation failed: ${overrideCheck.error}`,
|
|
2444
|
+
error_code: 'override_validation_failed',
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2371
2451
|
const stagingFile = join(root, resolvedStagingPath);
|
|
2372
2452
|
const now = new Date().toISOString();
|
|
2373
2453
|
const baseline = currentTurn.baseline || null;
|
|
2374
2454
|
const rawObservation = observeChanges(root, baseline);
|
|
2375
2455
|
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
2376
|
-
const
|
|
2456
|
+
const pendingConcurrentSiblingDeclarations = collectPendingConcurrentSiblingDeclarations(
|
|
2457
|
+
root,
|
|
2458
|
+
state,
|
|
2459
|
+
currentTurn,
|
|
2460
|
+
historyEntries,
|
|
2461
|
+
);
|
|
2462
|
+
const observation = attributeObservedChangesToTurn(rawObservation, currentTurn, historyEntries, {
|
|
2463
|
+
currentDeclaredFiles: turnResult.files_changed || [],
|
|
2464
|
+
concurrentSiblingIds: pendingConcurrentSiblingDeclarations.map((entry) => entry.turn_id),
|
|
2465
|
+
pendingConcurrentSiblingDeclarations,
|
|
2466
|
+
});
|
|
2377
2467
|
const role = config.roles?.[turnResult.role];
|
|
2378
2468
|
const runtimeId = turnResult.runtime_id;
|
|
2379
2469
|
const runtime = config.runtimes?.[runtimeId];
|
|
@@ -2381,11 +2471,28 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2381
2471
|
materializeDerivedReviewArtifact(root, turnResult, state, runtimeType, baseline);
|
|
2382
2472
|
materializeDerivedProposalArtifact(root, turnResult, state, runtimeType);
|
|
2383
2473
|
const writeAuthority = role?.write_authority || 'review_only';
|
|
2474
|
+
|
|
2475
|
+
// When concurrent siblings exist but have not yet been accepted, the
|
|
2476
|
+
// observation includes their file changes too. The attribution system
|
|
2477
|
+
// (attributeObservedChangesToTurn) only removes sibling files for
|
|
2478
|
+
// *later*-accepted turns. For the *first*-accepted concurrent turn,
|
|
2479
|
+
// undeclared files are expected noise from concurrency — downgrade to
|
|
2480
|
+
// warnings so the governance contract is not broken by turn-acceptance
|
|
2481
|
+
// ordering.
|
|
2482
|
+
const concurrentIds = new Set(
|
|
2483
|
+
Array.isArray(currentTurn.concurrent_with) ? currentTurn.concurrent_with : [],
|
|
2484
|
+
);
|
|
2485
|
+
const acceptedTurnIds = new Set(historyEntries.map(h => h.turn_id));
|
|
2486
|
+
const hasUnacceptedConcurrentSiblings = [...concurrentIds].some(id => !acceptedTurnIds.has(id));
|
|
2487
|
+
|
|
2384
2488
|
const diffComparison = compareDeclaredVsObserved(
|
|
2385
2489
|
turnResult.files_changed || [],
|
|
2386
2490
|
observation.files_changed,
|
|
2387
2491
|
writeAuthority,
|
|
2388
|
-
{
|
|
2492
|
+
{
|
|
2493
|
+
observation_available: observation.observation_available,
|
|
2494
|
+
has_unaccepted_concurrent_siblings: hasUnacceptedConcurrentSiblings,
|
|
2495
|
+
},
|
|
2389
2496
|
);
|
|
2390
2497
|
if (diffComparison.errors.length > 0) {
|
|
2391
2498
|
return {
|
|
@@ -3214,11 +3321,36 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3214
3321
|
};
|
|
3215
3322
|
writeAcceptanceJournal(root, journal);
|
|
3216
3323
|
|
|
3217
|
-
// ── Commit order: history → ledger → talk → state → cleanup → journal ─
|
|
3324
|
+
// ── Commit order: history → ledger → repo-decisions → talk → state → cleanup → journal ─
|
|
3218
3325
|
appendJsonl(root, HISTORY_PATH, historyEntry);
|
|
3219
3326
|
for (const entry of ledgerEntries) {
|
|
3220
3327
|
appendJsonl(root, LEDGER_PATH, entry);
|
|
3221
3328
|
}
|
|
3329
|
+
// Persist repo-durable decisions and process overrides
|
|
3330
|
+
if (turnResult.decisions && turnResult.decisions.length > 0) {
|
|
3331
|
+
for (const dec of turnResult.decisions) {
|
|
3332
|
+
// Process override first (marks target as overridden in repo-decisions.jsonl)
|
|
3333
|
+
if (dec.overrides) {
|
|
3334
|
+
overrideRepoDecision(root, dec.overrides, dec.id);
|
|
3335
|
+
}
|
|
3336
|
+
// Write to repo-decisions.jsonl if repo-durable or overriding a repo decision
|
|
3337
|
+
if ((dec.durability || 'run') === 'repo' || dec.overrides) {
|
|
3338
|
+
appendRepoDecision(root, {
|
|
3339
|
+
id: dec.id,
|
|
3340
|
+
run_id: state.run_id,
|
|
3341
|
+
turn_id: turnResult.turn_id,
|
|
3342
|
+
role: turnResult.role,
|
|
3343
|
+
phase: state.phase,
|
|
3344
|
+
category: dec.category,
|
|
3345
|
+
statement: dec.statement,
|
|
3346
|
+
rationale: dec.rationale,
|
|
3347
|
+
status: 'active',
|
|
3348
|
+
overridden_by: null,
|
|
3349
|
+
created_at: now,
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3222
3354
|
appendTalk(root, talkSection);
|
|
3223
3355
|
writeState(root, updatedState);
|
|
3224
3356
|
|