agentxchain 2.92.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/report.js +7 -1
- package/src/commands/status.js +4 -0
- package/src/lib/dispatch-bundle.js +9 -0
- package/src/lib/export.js +17 -0
- package/src/lib/governed-state.js +51 -1
- package/src/lib/repo-decisions.js +100 -0
- package/src/lib/report.js +578 -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/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
|
@@ -105,6 +105,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
105
105
|
state,
|
|
106
106
|
provenance: state?.provenance || null,
|
|
107
107
|
inherited_context: state?.inherited_context || null,
|
|
108
|
+
repo_decisions: state?.repo_decisions || null,
|
|
108
109
|
continuity,
|
|
109
110
|
connector_health: connectorHealth,
|
|
110
111
|
workflow_kit_artifacts: workflowKitArtifacts,
|
|
@@ -133,6 +134,9 @@ function renderGovernedStatus(context, opts) {
|
|
|
133
134
|
if (state?.inherited_context?.parent_run_id) {
|
|
134
135
|
console.log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`)}`);
|
|
135
136
|
}
|
|
137
|
+
if (state?.repo_decisions?.length > 0) {
|
|
138
|
+
console.log(` ${chalk.dim('Repo decisions:')} ${chalk.yellow(`${state.repo_decisions.length} active`)}`);
|
|
139
|
+
}
|
|
136
140
|
if (state?.accepted_integration_ref) {
|
|
137
141
|
console.log(` ${chalk.dim('Accepted:')} ${state.accepted_integration_ref}`);
|
|
138
142
|
}
|
|
@@ -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
|
@@ -8,6 +8,7 @@ import { loadCoordinatorConfig, COORDINATOR_CONFIG_FILE } from './coordinator-co
|
|
|
8
8
|
import { loadCoordinatorState } from './coordinator-state.js';
|
|
9
9
|
import { normalizeRunProvenance } from './run-provenance.js';
|
|
10
10
|
import { getDashboardPid, getDashboardSession } from '../commands/dashboard.js';
|
|
11
|
+
import { readRepoDecisions } from './repo-decisions.js';
|
|
11
12
|
|
|
12
13
|
const EXPORT_SCHEMA_VERSION = '0.3';
|
|
13
14
|
|
|
@@ -30,6 +31,7 @@ export const RUN_EXPORT_INCLUDED_ROOTS = [
|
|
|
30
31
|
'.agentxchain/session.json',
|
|
31
32
|
'.agentxchain/history.jsonl',
|
|
32
33
|
'.agentxchain/decision-ledger.jsonl',
|
|
34
|
+
'.agentxchain/repo-decisions.jsonl',
|
|
33
35
|
'.agentxchain/hook-audit.jsonl',
|
|
34
36
|
'.agentxchain/hook-annotations.jsonl',
|
|
35
37
|
'.agentxchain/notification-audit.jsonl',
|
|
@@ -208,6 +210,20 @@ function buildDashboardSessionSummary(root) {
|
|
|
208
210
|
};
|
|
209
211
|
}
|
|
210
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
|
+
|
|
211
227
|
export function buildDelegationSummary(files) {
|
|
212
228
|
const historyData = files['.agentxchain/history.jsonl']?.data;
|
|
213
229
|
if (!Array.isArray(historyData)) {
|
|
@@ -448,6 +464,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
448
464
|
coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
|
|
449
465
|
dashboard_session: buildDashboardSessionSummary(root),
|
|
450
466
|
delegation_summary: buildDelegationSummary(files),
|
|
467
|
+
repo_decisions: buildRepoDecisionsSummary(root),
|
|
451
468
|
},
|
|
452
469
|
workspace: buildRunWorkspaceMetadata(root),
|
|
453
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,
|
|
@@ -1908,6 +1914,7 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
1908
1914
|
const runId = generateId('run');
|
|
1909
1915
|
const now = new Date().toISOString();
|
|
1910
1916
|
const provenance = buildDefaultRunProvenance(options.provenance);
|
|
1917
|
+
const repoDecisions = getActiveRepoDecisions(root);
|
|
1911
1918
|
const updatedState = {
|
|
1912
1919
|
...state,
|
|
1913
1920
|
run_id: runId,
|
|
@@ -1922,6 +1929,7 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
1922
1929
|
},
|
|
1923
1930
|
provenance,
|
|
1924
1931
|
inherited_context: options.inherited_context || null,
|
|
1932
|
+
repo_decisions: repoDecisions.length > 0 ? repoDecisions : null,
|
|
1925
1933
|
};
|
|
1926
1934
|
|
|
1927
1935
|
writeState(root, updatedState);
|
|
@@ -2423,6 +2431,23 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2423
2431
|
}
|
|
2424
2432
|
|
|
2425
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
|
+
|
|
2426
2451
|
const stagingFile = join(root, resolvedStagingPath);
|
|
2427
2452
|
const now = new Date().toISOString();
|
|
2428
2453
|
const baseline = currentTurn.baseline || null;
|
|
@@ -3296,11 +3321,36 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3296
3321
|
};
|
|
3297
3322
|
writeAcceptanceJournal(root, journal);
|
|
3298
3323
|
|
|
3299
|
-
// ── Commit order: history → ledger → talk → state → cleanup → journal ─
|
|
3324
|
+
// ── Commit order: history → ledger → repo-decisions → talk → state → cleanup → journal ─
|
|
3300
3325
|
appendJsonl(root, HISTORY_PATH, historyEntry);
|
|
3301
3326
|
for (const entry of ledgerEntries) {
|
|
3302
3327
|
appendJsonl(root, LEDGER_PATH, entry);
|
|
3303
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
|
+
}
|
|
3304
3354
|
appendTalk(root, talkSection);
|
|
3305
3355
|
writeState(root, updatedState);
|
|
3306
3356
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Decisions — cross-run decision carryover.
|
|
3
|
+
*
|
|
4
|
+
* Decisions with durability: "repo" persist in `.agentxchain/repo-decisions.jsonl`
|
|
5
|
+
* across governed runs. They act as binding constraints: agents in future runs
|
|
6
|
+
* must comply with active repo decisions or explicitly override them.
|
|
7
|
+
*
|
|
8
|
+
* DEC-SPEC: .planning/CROSS_RUN_DECISION_CARRYOVER_SPEC.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, appendFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
|
|
14
|
+
const REPO_DECISIONS_PATH = '.agentxchain/repo-decisions.jsonl';
|
|
15
|
+
|
|
16
|
+
// ── Read ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export function readRepoDecisions(root) {
|
|
19
|
+
const filePath = join(root, REPO_DECISIONS_PATH);
|
|
20
|
+
if (!existsSync(filePath)) return [];
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(filePath, 'utf8').trim();
|
|
23
|
+
if (!content) return [];
|
|
24
|
+
return content.split('\n').filter(Boolean).map(line => {
|
|
25
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
26
|
+
}).filter(Boolean);
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getActiveRepoDecisions(root) {
|
|
33
|
+
return readRepoDecisions(root).filter(d => d.status === 'active');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getRepoDecisionById(root, decisionId) {
|
|
37
|
+
return readRepoDecisions(root).find(d => d.id === decisionId) || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Write ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export function appendRepoDecision(root, entry) {
|
|
43
|
+
const filePath = join(root, REPO_DECISIONS_PATH);
|
|
44
|
+
const dir = dirname(filePath);
|
|
45
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
46
|
+
appendFileSync(filePath, JSON.stringify(entry) + '\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function overrideRepoDecision(root, targetId, overridingId) {
|
|
50
|
+
const all = readRepoDecisions(root);
|
|
51
|
+
const updated = all.map(d => {
|
|
52
|
+
if (d.id === targetId) {
|
|
53
|
+
return { ...d, status: 'overridden', overridden_by: overridingId };
|
|
54
|
+
}
|
|
55
|
+
return d;
|
|
56
|
+
});
|
|
57
|
+
const filePath = join(root, REPO_DECISIONS_PATH);
|
|
58
|
+
const dir = dirname(filePath);
|
|
59
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
60
|
+
writeFileSync(filePath, updated.map(d => JSON.stringify(d)).join('\n') + '\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Validate Override ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export function validateOverride(root, decision) {
|
|
66
|
+
if (!decision.overrides) return { ok: true };
|
|
67
|
+
const targetId = decision.overrides;
|
|
68
|
+
const target = getRepoDecisionById(root, targetId);
|
|
69
|
+
if (!target) {
|
|
70
|
+
return { ok: false, error: `decisions: overrides references ${targetId} which does not exist in repo decisions.` };
|
|
71
|
+
}
|
|
72
|
+
if (target.status === 'overridden') {
|
|
73
|
+
return { ok: false, error: `decisions: ${targetId} is already overridden by ${target.overridden_by}.` };
|
|
74
|
+
}
|
|
75
|
+
if (target.status !== 'active') {
|
|
76
|
+
return { ok: false, error: `decisions: ${targetId} has status "${target.status}", only active repo decisions can be overridden.` };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Render ──────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export function renderRepoDecisionsMarkdown(activeDecisions) {
|
|
84
|
+
if (!activeDecisions || activeDecisions.length === 0) return '';
|
|
85
|
+
const lines = [
|
|
86
|
+
'## Active Repo Decisions',
|
|
87
|
+
'',
|
|
88
|
+
'These decisions persist from prior governed runs. Comply or explicitly override with rationale.',
|
|
89
|
+
'',
|
|
90
|
+
];
|
|
91
|
+
for (const d of activeDecisions) {
|
|
92
|
+
lines.push(`- **${d.id}** (${d.category}): ${d.statement}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push('');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export { REPO_DECISIONS_PATH };
|
package/src/lib/report.js
CHANGED
|
@@ -992,6 +992,7 @@ function buildRunSubject(artifact) {
|
|
|
992
992
|
recovery_summary: recoverySummary,
|
|
993
993
|
continuity,
|
|
994
994
|
workflow_kit_artifacts: extractWorkflowKitArtifacts(artifact),
|
|
995
|
+
repo_decisions: artifact.summary?.repo_decisions || null,
|
|
995
996
|
},
|
|
996
997
|
artifacts: {
|
|
997
998
|
history_entries: artifact.summary?.history_entries || 0,
|
|
@@ -1293,6 +1294,14 @@ export function formatGovernanceReportText(report) {
|
|
|
1293
1294
|
}
|
|
1294
1295
|
}
|
|
1295
1296
|
|
|
1297
|
+
if (run.repo_decisions?.active?.length > 0) {
|
|
1298
|
+
lines.push('', 'Repo Decisions:');
|
|
1299
|
+
lines.push(` Active: ${run.repo_decisions.active_count} Overridden: ${run.repo_decisions.overridden_count}`);
|
|
1300
|
+
for (const d of run.repo_decisions.active) {
|
|
1301
|
+
lines.push(` - ${d.id} (${d.category}): ${d.statement}`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1296
1305
|
if (run.turns && run.turns.length > 0) {
|
|
1297
1306
|
lines.push('', 'Turn Timeline:');
|
|
1298
1307
|
for (let i = 0; i < run.turns.length; i++) {
|
|
@@ -1778,6 +1787,16 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1778
1787
|
}
|
|
1779
1788
|
}
|
|
1780
1789
|
|
|
1790
|
+
if (run.repo_decisions?.active?.length > 0) {
|
|
1791
|
+
lines.push('', '## Repo Decisions', '');
|
|
1792
|
+
lines.push(`Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}`, '');
|
|
1793
|
+
lines.push('| ID | Category | Statement | Role | Run |', '|----|----------|-----------|------|-----|');
|
|
1794
|
+
for (const d of run.repo_decisions.active) {
|
|
1795
|
+
const stmt = (d.statement || '').replace(/\|/g, '\\|');
|
|
1796
|
+
lines.push(`| ${d.id} | ${d.category} | ${stmt} | ${d.role || '—'} | \`${(d.run_id || '').slice(0, 12)}\` |`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1781
1800
|
if (run.turns && run.turns.length > 0) {
|
|
1782
1801
|
lines.push('', '## Turn Timeline', '', '| # | Role | Phase | Summary | Files | Cost | Time |', '|---|------|-------|---------|-------|------|------|');
|
|
1783
1802
|
for (let i = 0; i < run.turns.length; i++) {
|
|
@@ -2157,3 +2176,562 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
2157
2176
|
}));
|
|
2158
2177
|
return mdLines.join('\n');
|
|
2159
2178
|
}
|
|
2179
|
+
|
|
2180
|
+
// --- HTML governance report formatter ---
|
|
2181
|
+
|
|
2182
|
+
function esc(str) {
|
|
2183
|
+
if (typeof str !== 'string') return '';
|
|
2184
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
function badge(status) {
|
|
2188
|
+
const colors = {
|
|
2189
|
+
pass: '#22c55e', running: '#3b82f6', completed: '#22c55e',
|
|
2190
|
+
failed: '#ef4444', error: '#ef4444', fail: '#ef4444',
|
|
2191
|
+
blocked: '#f59e0b', pending: '#a855f7', mixed: '#f59e0b',
|
|
2192
|
+
paused: '#6b7280', not_running: '#6b7280', stale: '#f59e0b',
|
|
2193
|
+
pid_only: '#f59e0b',
|
|
2194
|
+
};
|
|
2195
|
+
const color = colors[status] || '#6b7280';
|
|
2196
|
+
return `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.85em;font-weight:600;color:#fff;background:${color}">${esc(String(status))}</span>`;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
function htmlTable(headers, rows) {
|
|
2200
|
+
const lines = ['<table>', '<thead><tr>'];
|
|
2201
|
+
for (const h of headers) lines.push(`<th>${esc(h)}</th>`);
|
|
2202
|
+
lines.push('</tr></thead>', '<tbody>');
|
|
2203
|
+
for (const row of rows) {
|
|
2204
|
+
lines.push('<tr>');
|
|
2205
|
+
for (const cell of row) lines.push(`<td>${cell}</td>`);
|
|
2206
|
+
lines.push('</tr>');
|
|
2207
|
+
}
|
|
2208
|
+
lines.push('</tbody>', '</table>');
|
|
2209
|
+
return lines.join('');
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
function htmlSection(title, content, level = 2) {
|
|
2213
|
+
const tag = `h${level}`;
|
|
2214
|
+
return `<${tag}>${esc(title)}</${tag}>\n${content}`;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
function htmlDl(pairs) {
|
|
2218
|
+
const lines = ['<dl>'];
|
|
2219
|
+
for (const [label, value] of pairs) {
|
|
2220
|
+
lines.push(`<dt>${esc(label)}</dt><dd>${value}</dd>`);
|
|
2221
|
+
}
|
|
2222
|
+
lines.push('</dl>');
|
|
2223
|
+
return lines.join('');
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
const HTML_STYLES = `
|
|
2227
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2228
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.6;color:#1a1a2e;background:#f8fafc;padding:2rem;max-width:1100px;margin:0 auto}
|
|
2229
|
+
h1{font-size:1.6rem;margin-bottom:0.5rem;border-bottom:2px solid #e2e8f0;padding-bottom:0.5rem}
|
|
2230
|
+
h2{font-size:1.2rem;margin-top:2rem;margin-bottom:0.75rem;color:#334155}
|
|
2231
|
+
h3{font-size:1.05rem;margin-top:1.5rem;margin-bottom:0.5rem;color:#475569}
|
|
2232
|
+
h4{font-size:0.95rem;margin-top:1rem;margin-bottom:0.4rem;color:#64748b}
|
|
2233
|
+
dl{display:grid;grid-template-columns:max-content 1fr;gap:0.3rem 1rem;margin-bottom:1rem}
|
|
2234
|
+
dt{font-weight:600;color:#475569;white-space:nowrap}
|
|
2235
|
+
dd{color:#1e293b}
|
|
2236
|
+
table{width:100%;border-collapse:collapse;margin:0.75rem 0 1.5rem;font-size:0.9rem}
|
|
2237
|
+
th{background:#f1f5f9;font-weight:600;text-align:left;padding:0.5rem 0.75rem;border-bottom:2px solid #cbd5e1;color:#334155}
|
|
2238
|
+
td{padding:0.4rem 0.75rem;border-bottom:1px solid #e2e8f0}
|
|
2239
|
+
tr:hover td{background:#f8fafc}
|
|
2240
|
+
code{font-family:"SF Mono",Menlo,monospace;font-size:0.85em;background:#f1f5f9;padding:1px 4px;border-radius:3px}
|
|
2241
|
+
.header{display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem}
|
|
2242
|
+
.header-brand{font-size:0.85rem;color:#64748b}
|
|
2243
|
+
.meta{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
|
|
2244
|
+
.section{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1rem}
|
|
2245
|
+
ul{margin:0.5rem 0;padding-left:1.5rem}
|
|
2246
|
+
li{margin-bottom:0.25rem}
|
|
2247
|
+
.warn{color:#d97706;font-weight:600}
|
|
2248
|
+
@media(prefers-color-scheme:dark){
|
|
2249
|
+
body{background:#0f172a;color:#e2e8f0}
|
|
2250
|
+
h1{border-bottom-color:#334155}
|
|
2251
|
+
h2,h3,h4{color:#94a3b8}
|
|
2252
|
+
dt{color:#94a3b8}dd{color:#e2e8f0}
|
|
2253
|
+
th{background:#1e293b;border-bottom-color:#475569;color:#cbd5e1}
|
|
2254
|
+
td{border-bottom-color:#334155}
|
|
2255
|
+
tr:hover td{background:#1e293b}
|
|
2256
|
+
code{background:#1e293b}
|
|
2257
|
+
.meta,.section{background:#1e293b;border-color:#334155}
|
|
2258
|
+
.header-brand{color:#94a3b8}
|
|
2259
|
+
}
|
|
2260
|
+
@media print{
|
|
2261
|
+
body{background:#fff;color:#000;padding:1rem}
|
|
2262
|
+
.meta,.section{border:1px solid #ccc}
|
|
2263
|
+
th{background:#eee}
|
|
2264
|
+
}
|
|
2265
|
+
`;
|
|
2266
|
+
|
|
2267
|
+
function wrapHtml(title, bodyContent) {
|
|
2268
|
+
return `<!DOCTYPE html>
|
|
2269
|
+
<html lang="en">
|
|
2270
|
+
<head>
|
|
2271
|
+
<meta charset="utf-8">
|
|
2272
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
2273
|
+
<title>${esc(title)}</title>
|
|
2274
|
+
<style>${HTML_STYLES}</style>
|
|
2275
|
+
</head>
|
|
2276
|
+
<body>
|
|
2277
|
+
<div class="header">
|
|
2278
|
+
<h1>AgentXchain Governance Report</h1>
|
|
2279
|
+
<span class="header-brand">agentxchain.dev</span>
|
|
2280
|
+
</div>
|
|
2281
|
+
${bodyContent}
|
|
2282
|
+
</body>
|
|
2283
|
+
</html>`;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
function renderHtmlGovEventDetail(evt) {
|
|
2287
|
+
const parts = [];
|
|
2288
|
+
switch (evt.type) {
|
|
2289
|
+
case 'policy_escalation':
|
|
2290
|
+
for (const v of evt.violations || []) {
|
|
2291
|
+
parts.push(`<li>Violation: <code>${esc(v.policy_id || '?')}</code> / <code>${esc(v.rule || '?')}</code> — ${esc(v.message || 'n/a')}</li>`);
|
|
2292
|
+
}
|
|
2293
|
+
break;
|
|
2294
|
+
case 'conflict_detected':
|
|
2295
|
+
if (evt.conflicting_files?.length > 0) parts.push(`<li>Files: ${evt.conflicting_files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</li>`);
|
|
2296
|
+
if (evt.overlap_ratio != null) parts.push(`<li>Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%</li>`);
|
|
2297
|
+
break;
|
|
2298
|
+
case 'operator_escalated':
|
|
2299
|
+
if (evt.reason) parts.push(`<li>Reason: ${esc(evt.reason)}</li>`);
|
|
2300
|
+
if (evt.blocked_on) parts.push(`<li>Blocked on: <code>${esc(evt.blocked_on)}</code></li>`);
|
|
2301
|
+
break;
|
|
2302
|
+
case 'escalation_resolved':
|
|
2303
|
+
if (evt.resolved_via) parts.push(`<li>Resolved via: <code>${esc(evt.resolved_via)}</code></li>`);
|
|
2304
|
+
break;
|
|
2305
|
+
}
|
|
2306
|
+
return parts.length ? `<ul>${parts.join('')}</ul>` : '';
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
function renderRunHtml(report) {
|
|
2310
|
+
const { project, run, artifacts } = report.subject;
|
|
2311
|
+
const sections = [];
|
|
2312
|
+
|
|
2313
|
+
// Meta section
|
|
2314
|
+
const metaPairs = [
|
|
2315
|
+
['Input', `<code>${esc(report.input)}</code>`],
|
|
2316
|
+
['Export kind', `<code>${esc(report.export_kind)}</code>`],
|
|
2317
|
+
['Verification', badge('pass')],
|
|
2318
|
+
['Project', `${esc(project.name || 'unknown')} (<code>${esc(project.id || 'unknown')}</code>)`],
|
|
2319
|
+
];
|
|
2320
|
+
if (project.goal) metaPairs.push(['Goal', esc(project.goal)]);
|
|
2321
|
+
metaPairs.push(
|
|
2322
|
+
['Template', `<code>${esc(project.template)}</code>`],
|
|
2323
|
+
['Protocol', `<code>${esc(project.protocol_mode || 'unknown')}</code> (schema <code>${esc(project.schema_version || 'unknown')}</code>)`],
|
|
2324
|
+
['Run ID', `<code>${esc(run.run_id || 'none')}</code>`],
|
|
2325
|
+
['Status', badge(run.status || 'unknown')],
|
|
2326
|
+
['Phase', `<code>${esc(run.phase || 'unknown')}</code>`],
|
|
2327
|
+
['Blocked on', `<code>${esc(summarizeBlockedState(run))}</code>`],
|
|
2328
|
+
['Active turns', `${run.active_turn_count}${run.active_turn_ids.length ? ` (${run.active_turn_ids.map((id) => `<code>${esc(id)}</code>`).join(', ')})` : ''}`],
|
|
2329
|
+
['Retained turns', `${run.retained_turn_count}${run.retained_turn_ids.length ? ` (${run.retained_turn_ids.map((id) => `<code>${esc(id)}</code>`).join(', ')})` : ''}`],
|
|
2330
|
+
['Active roles', run.active_roles.length ? run.active_roles.map((r) => `<code>${esc(r)}</code>`).join(', ') : '<code>none</code>'],
|
|
2331
|
+
);
|
|
2332
|
+
|
|
2333
|
+
if (run.budget_status) {
|
|
2334
|
+
const warnTag = run.budget_status.warn_mode ? ' <span class="warn">[OVER BUDGET]</span>' : '';
|
|
2335
|
+
metaPairs.push(['Budget', `spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}${warnTag}`]);
|
|
2336
|
+
}
|
|
2337
|
+
if (run.created_at) metaPairs.push(['Started', `<code>${esc(run.created_at)}</code>`]);
|
|
2338
|
+
if (run.completed_at) metaPairs.push(['Completed', `<code>${esc(run.completed_at)}</code>`]);
|
|
2339
|
+
if (run.duration_seconds != null) metaPairs.push(['Duration', `<code>${run.duration_seconds}s</code>`]);
|
|
2340
|
+
if (summarizeRunProvenance(run.provenance)) metaPairs.push(['Provenance', `<code>${esc(summarizeRunProvenance(run.provenance))}</code>`]);
|
|
2341
|
+
if (run.inherited_context?.parent_run_id) metaPairs.push(['Inherited from', `<code>${esc(run.inherited_context.parent_run_id)}</code> (${esc(run.inherited_context.parent_status || 'unknown')})`]);
|
|
2342
|
+
if (run.dashboard_session) metaPairs.push(['Dashboard', `<code>${esc(formatDashboardSessionLine(run.dashboard_session))}</code>`]);
|
|
2343
|
+
|
|
2344
|
+
metaPairs.push(
|
|
2345
|
+
['History entries', String(artifacts.history_entries)],
|
|
2346
|
+
['Decision entries', String(artifacts.decision_entries)],
|
|
2347
|
+
['Hook audit entries', String(artifacts.hook_audit_entries)],
|
|
2348
|
+
['Notification entries', String(artifacts.notification_audit_entries)],
|
|
2349
|
+
['Dispatch files', String(artifacts.dispatch_artifact_files)],
|
|
2350
|
+
['Staging files', String(artifacts.staging_artifact_files)],
|
|
2351
|
+
['Intake artifacts', artifacts.intake_present ? 'yes' : 'no'],
|
|
2352
|
+
['Coordinator artifacts', artifacts.coordinator_present ? 'yes' : 'no'],
|
|
2353
|
+
);
|
|
2354
|
+
|
|
2355
|
+
sections.push(`<div class="meta">${htmlDl(metaPairs)}</div>`);
|
|
2356
|
+
|
|
2357
|
+
// Cost Summary
|
|
2358
|
+
if (run.cost_summary) {
|
|
2359
|
+
const cs = run.cost_summary;
|
|
2360
|
+
let costHtml = `<p><strong>Total:</strong> ${formatUsd(cs.total_usd)} across ${cs.turn_count} turn${cs.turn_count !== 1 ? 's' : ''} (${cs.costed_turn_count} with cost data)</p>`;
|
|
2361
|
+
if (cs.total_input_tokens != null || cs.total_output_tokens != null) {
|
|
2362
|
+
costHtml += `<p><strong>Tokens:</strong> ${formatTokenCount(cs.total_input_tokens)} input / ${formatTokenCount(cs.total_output_tokens)} output</p>`;
|
|
2363
|
+
}
|
|
2364
|
+
if (cs.by_role.length > 0) {
|
|
2365
|
+
costHtml += htmlTable(
|
|
2366
|
+
['Role', 'Cost', 'Turns', 'Input Tokens', 'Output Tokens'],
|
|
2367
|
+
cs.by_role.map((r) => [esc(r.role), formatUsd(r.usd), String(r.turns), formatTokenCount(r.input_tokens), formatTokenCount(r.output_tokens)]),
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
if (cs.by_phase.length > 0) {
|
|
2371
|
+
costHtml += htmlTable(
|
|
2372
|
+
['Phase', 'Cost', 'Turns'],
|
|
2373
|
+
cs.by_phase.map((p) => [esc(p.phase), formatUsd(p.usd), String(p.turns)]),
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
sections.push(`<div class="section">${htmlSection('Cost Summary', costHtml)}</div>`);
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// Delegation Summary
|
|
2380
|
+
if (run.delegation_summary?.delegation_chains?.length > 0) {
|
|
2381
|
+
const ds = run.delegation_summary;
|
|
2382
|
+
let delHtml = `<p>Total delegations issued: ${ds.total_delegations_issued}</p>`;
|
|
2383
|
+
const rows = [];
|
|
2384
|
+
for (const chain of ds.delegation_chains) {
|
|
2385
|
+
for (let i = 0; i < chain.delegations.length; i++) {
|
|
2386
|
+
const d = chain.delegations[i];
|
|
2387
|
+
rows.push([
|
|
2388
|
+
i === 0 ? esc(chain.parent_role) : '',
|
|
2389
|
+
i === 0 ? `<code>${esc(chain.parent_turn_id)}</code>` : '',
|
|
2390
|
+
i === 0 ? badge(chain.outcome) : '',
|
|
2391
|
+
i === 0 ? `<code>${esc(chain.review_turn_id || 'pending')}</code>` : '',
|
|
2392
|
+
`<code>${esc(d.delegation_id)}</code> → <code>${esc(d.to_role)}</code>`,
|
|
2393
|
+
`<code>${esc(d.child_turn_id || 'pending')}</code>`,
|
|
2394
|
+
badge(d.status),
|
|
2395
|
+
esc(d.charter),
|
|
2396
|
+
]);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
delHtml += htmlTable(['Parent Role', 'Parent Turn', 'Outcome', 'Review Turn', 'Delegation', 'Child Turn', 'Status', 'Charter'], rows);
|
|
2400
|
+
sections.push(`<div class="section">${htmlSection('Delegation Summary', delHtml)}</div>`);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// Repo Decisions
|
|
2404
|
+
if (run.repo_decisions?.active?.length > 0) {
|
|
2405
|
+
let rdHtml = `<p>Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}</p>`;
|
|
2406
|
+
rdHtml += htmlTable(
|
|
2407
|
+
['ID', 'Category', 'Statement', 'Role', 'Run'],
|
|
2408
|
+
run.repo_decisions.active.map((d) => [esc(d.id), esc(d.category), esc(d.statement || ''), esc(d.role || '\u2014'), `<code>${esc((d.run_id || '').slice(0, 12))}</code>`]),
|
|
2409
|
+
);
|
|
2410
|
+
sections.push(`<div class="section">${htmlSection('Repo Decisions', rdHtml)}</div>`);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// Turn Timeline
|
|
2414
|
+
if (run.turns && run.turns.length > 0) {
|
|
2415
|
+
const turnRows = run.turns.map((t, i) => {
|
|
2416
|
+
const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
|
|
2417
|
+
const phase = t.phase_transition ? `${esc(t.phase || '?')} → ${esc(t.phase_transition)}` : esc(t.phase || '?');
|
|
2418
|
+
const sibNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling)` : '';
|
|
2419
|
+
return [String(i + 1), esc(t.role), phase, esc(t.summary || '(no summary)'), `${t.files_changed_count}${sibNote}`, cost, esc(formatTurnTimelineTime(t))];
|
|
2420
|
+
});
|
|
2421
|
+
sections.push(`<div class="section">${htmlSection('Turn Timeline', htmlTable(['#', 'Role', 'Phase', 'Summary', 'Files', 'Cost', 'Time'], turnRows))}</div>`);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// Decisions
|
|
2425
|
+
if (run.decisions && run.decisions.length > 0) {
|
|
2426
|
+
const decList = run.decisions.map((d) => `<li><strong>${esc(d.id)}</strong> (${esc(d.role || '?')}, ${esc(d.phase || '?')} phase): ${esc(d.statement)}</li>`).join('');
|
|
2427
|
+
sections.push(`<div class="section">${htmlSection('Decisions', `<ul>${decList}</ul>`)}</div>`);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// Gate Outcomes
|
|
2431
|
+
if (run.gate_summary && run.gate_summary.length > 0) {
|
|
2432
|
+
const gateList = run.gate_summary.map((g) => `<li><code>${esc(g.gate_id)}</code>: ${badge(g.status)}</li>`).join('');
|
|
2433
|
+
sections.push(`<div class="section">${htmlSection('Gate Outcomes', `<ul>${gateList}</ul>`)}</div>`);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// Gate Failures
|
|
2437
|
+
if (run.gate_failures && run.gate_failures.length > 0) {
|
|
2438
|
+
let gfHtml = '<ul>';
|
|
2439
|
+
for (const failure of run.gate_failures) {
|
|
2440
|
+
const request = failure.gate_type === 'run_completion' ? 'run completion' : `${esc(failure.from_phase || failure.phase || '?')} → ${esc(failure.to_phase || '?')}`;
|
|
2441
|
+
gfHtml += `<li><code>${esc(failure.gate_id || 'unknown')}</code> (${esc(failure.gate_type || 'unknown')}) at <code>${esc(failure.failed_at || 'n/a')}</code>: ${request}`;
|
|
2442
|
+
if (failure.reasons?.length) {
|
|
2443
|
+
gfHtml += '<ul>' + failure.reasons.map((r) => `<li>${esc(r)}</li>`).join('') + '</ul>';
|
|
2444
|
+
}
|
|
2445
|
+
gfHtml += '</li>';
|
|
2446
|
+
}
|
|
2447
|
+
gfHtml += '</ul>';
|
|
2448
|
+
sections.push(`<div class="section">${htmlSection('Gate Failures', gfHtml)}</div>`);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// Approval Policy
|
|
2452
|
+
if (run.approval_policy_events && run.approval_policy_events.length > 0) {
|
|
2453
|
+
let apHtml = '<ul>';
|
|
2454
|
+
for (const evt of run.approval_policy_events) {
|
|
2455
|
+
const transition = evt.gate_type === 'run_completion' ? 'run completion' : `${esc(evt.from_phase || '?')} → ${esc(evt.to_phase || '?')}`;
|
|
2456
|
+
apHtml += `<li><strong>${esc(evt.action || 'unknown')}</strong> (${esc(evt.gate_type || 'unknown')}) ${transition} at <code>${esc(evt.timestamp || 'n/a')}</code>`;
|
|
2457
|
+
if (evt.reason) apHtml += `<br>${esc(evt.reason)}`;
|
|
2458
|
+
apHtml += '</li>';
|
|
2459
|
+
}
|
|
2460
|
+
apHtml += '</ul>';
|
|
2461
|
+
sections.push(`<div class="section">${htmlSection('Approval Policy', apHtml)}</div>`);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// Governance Events
|
|
2465
|
+
if (run.governance_events && run.governance_events.length > 0) {
|
|
2466
|
+
let geHtml = '<ul>';
|
|
2467
|
+
for (const evt of run.governance_events) {
|
|
2468
|
+
geHtml += `<li><strong>${esc(evt.type)}</strong> (<code>${esc(evt.role || '?')}</code>, <code>${esc(evt.phase || '?')}</code> phase) at <code>${esc(evt.timestamp || 'n/a')}</code>${renderHtmlGovEventDetail(evt)}</li>`;
|
|
2469
|
+
}
|
|
2470
|
+
geHtml += '</ul>';
|
|
2471
|
+
sections.push(`<div class="section">${htmlSection('Governance Events', geHtml)}</div>`);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// Timeout Events
|
|
2475
|
+
if (run.timeout_events && run.timeout_events.length > 0) {
|
|
2476
|
+
let teHtml = '<ul>';
|
|
2477
|
+
for (const evt of run.timeout_events) {
|
|
2478
|
+
const label = evt.type === 'timeout_warning' ? 'Warning' : evt.type === 'timeout_skip' ? 'Skip' : evt.type === 'timeout_skip_failed' ? 'Skip Failed' : 'Escalation';
|
|
2479
|
+
const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
|
|
2480
|
+
const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
|
|
2481
|
+
const exceeded = evt.exceeded_by_minutes != null ? ` (+${evt.exceeded_by_minutes}m)` : '';
|
|
2482
|
+
teHtml += `<li><strong>${label}</strong> (<code>${esc(evt.scope || '?')}</code> scope) \u2014 ${elapsed}/${limit}${exceeded}, action: <code>${esc(evt.action || 'n/a')}</code>, phase: <code>${esc(evt.phase || 'n/a')}</code> at <code>${esc(evt.timestamp || 'n/a')}</code></li>`;
|
|
2483
|
+
}
|
|
2484
|
+
teHtml += '</ul>';
|
|
2485
|
+
sections.push(`<div class="section">${htmlSection('Timeout Events', teHtml)}</div>`);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// Intake Linkage
|
|
2489
|
+
if (run.intake_links && run.intake_links.length > 0) {
|
|
2490
|
+
const ilRows = run.intake_links.map((intake) => [
|
|
2491
|
+
`<code>${esc(intake.intent_id)}</code>`,
|
|
2492
|
+
badge(intake.status || 'unknown'),
|
|
2493
|
+
`<code>${esc(intake.event_id || 'n/a')}</code>`,
|
|
2494
|
+
`<code>${esc(intake.target_turn || 'n/a')}</code>`,
|
|
2495
|
+
`<code>${esc(intake.started_at || 'n/a')}</code>`,
|
|
2496
|
+
]);
|
|
2497
|
+
sections.push(`<div class="section">${htmlSection('Intake Linkage', htmlTable(['Intent', 'Status', 'Event', 'Target Turn', 'Started'], ilRows))}</div>`);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// Hook Activity
|
|
2501
|
+
if (run.hook_summary) {
|
|
2502
|
+
const eventList = Object.entries(run.hook_summary.events).sort(([a], [b]) => a.localeCompare(b, 'en')).map(([e, c]) => `${esc(e)}(${c})`).join(', ');
|
|
2503
|
+
const hookHtml = htmlDl([
|
|
2504
|
+
['Total executions', String(run.hook_summary.total)],
|
|
2505
|
+
['Blocked', String(run.hook_summary.blocked)],
|
|
2506
|
+
...(eventList ? [['Events', eventList]] : []),
|
|
2507
|
+
]);
|
|
2508
|
+
sections.push(`<div class="section">${htmlSection('Hook Activity', hookHtml)}</div>`);
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// Recovery
|
|
2512
|
+
if (run.recovery_summary) {
|
|
2513
|
+
const rs = run.recovery_summary;
|
|
2514
|
+
sections.push(`<div class="section">${htmlSection('Recovery', htmlDl([
|
|
2515
|
+
['Category', `<code>${esc(rs.category || 'unknown')}</code>`],
|
|
2516
|
+
['Typed reason', `<code>${esc(rs.typed_reason || 'unknown')}</code>`],
|
|
2517
|
+
['Owner', `<code>${esc(rs.owner || 'unknown')}</code>`],
|
|
2518
|
+
['Action', `<code>${esc(rs.recovery_action || 'n/a')}</code>`],
|
|
2519
|
+
['Detail', esc(rs.detail || 'n/a')],
|
|
2520
|
+
['Turn retained', rs.turn_retained == null ? 'n/a' : (rs.turn_retained ? 'yes' : 'no')],
|
|
2521
|
+
]))}</div>`);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// Continuity
|
|
2525
|
+
if (run.continuity) {
|
|
2526
|
+
const pairs = [
|
|
2527
|
+
['Session', `<code>${esc(run.continuity.session_id || 'unknown')}</code>`],
|
|
2528
|
+
['Checkpoint', `<code>${esc(run.continuity.checkpoint_reason || 'unknown')}</code> at <code>${esc(run.continuity.last_checkpoint_at || 'n/a')}</code>`],
|
|
2529
|
+
['Last turn', `<code>${esc(run.continuity.last_turn_id || 'none')}</code>`],
|
|
2530
|
+
['Last role', `<code>${esc(run.continuity.last_role || 'unknown')}</code>`],
|
|
2531
|
+
['Last phase', `<code>${esc(run.continuity.last_phase || 'unknown')}</code>`],
|
|
2532
|
+
];
|
|
2533
|
+
if (run.continuity.stale_checkpoint) {
|
|
2534
|
+
pairs.push(['Warning', `<span class="warn">checkpoint tracks run <code>${esc(run.continuity.run_id)}</code>, but export tracks <code>${esc(run.run_id)}</code></span>`]);
|
|
2535
|
+
}
|
|
2536
|
+
sections.push(`<div class="section">${htmlSection('Continuity', htmlDl(pairs))}</div>`);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// Workflow Artifacts
|
|
2540
|
+
if (Array.isArray(run.workflow_kit_artifacts) && run.workflow_kit_artifacts.length > 0) {
|
|
2541
|
+
let waHtml = `<p>Phase: <code>${esc(run.phase || 'unknown')}</code></p>`;
|
|
2542
|
+
waHtml += htmlTable(
|
|
2543
|
+
['Artifact', 'Required', 'Semantics', 'Owner', 'Resolution', 'Status'],
|
|
2544
|
+
run.workflow_kit_artifacts.map((art) => [
|
|
2545
|
+
`<code>${esc(art.path)}</code>`,
|
|
2546
|
+
art.required ? 'yes' : 'no',
|
|
2547
|
+
art.semantics ? `<code>${esc(art.semantics)}</code>` : 'none',
|
|
2548
|
+
art.owned_by ? `<code>${esc(art.owned_by)}</code>` : 'none',
|
|
2549
|
+
esc(art.owner_resolution),
|
|
2550
|
+
art.exists ? 'exists' : '<strong class="warn">missing</strong>',
|
|
2551
|
+
]),
|
|
2552
|
+
);
|
|
2553
|
+
sections.push(`<div class="section">${htmlSection('Workflow Artifacts', waHtml)}</div>`);
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
return wrapHtml('AgentXchain Governance Report', sections.join('\n'));
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
function renderCoordinatorHtml(report) {
|
|
2560
|
+
const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest, approval_policy_events, governance_events, timeout_events, recovery_report: coordRecoveryReport } = report.subject;
|
|
2561
|
+
const sections = [];
|
|
2562
|
+
|
|
2563
|
+
const metaPairs = [
|
|
2564
|
+
['Input', `<code>${esc(report.input)}</code>`],
|
|
2565
|
+
['Export kind', `<code>${esc(report.export_kind)}</code>`],
|
|
2566
|
+
['Verification', badge('pass')],
|
|
2567
|
+
['Workspace', `${esc(coordinator.project_name || 'unknown')} (<code>${esc(coordinator.project_id || 'unknown')}</code>)`],
|
|
2568
|
+
['Schema', `<code>${esc(coordinator.schema_version || 'unknown')}</code>`],
|
|
2569
|
+
['Super run', `<code>${esc(run.super_run_id || 'none')}</code>`],
|
|
2570
|
+
['Status', badge(run.status || 'unknown')],
|
|
2571
|
+
['Phase', `<code>${esc(run.phase || 'unknown')}</code>`],
|
|
2572
|
+
['Blocked reason', `<code>${esc(run.blocked_reason || 'none')}</code>`],
|
|
2573
|
+
];
|
|
2574
|
+
|
|
2575
|
+
if (run.run_id_mismatches?.length > 0) {
|
|
2576
|
+
metaPairs.push(['Run ID mismatches', `<strong class="warn">${run.run_id_mismatches.length}</strong>`]);
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
metaPairs.push(
|
|
2580
|
+
['Started', `<code>${esc(run.created_at || 'n/a')}</code>`],
|
|
2581
|
+
['Repos', `${coordinator.repo_count} total, ${run.repo_ok_count} exported, ${run.repo_error_count} failed`],
|
|
2582
|
+
['Workstreams', String(coordinator.workstream_count)],
|
|
2583
|
+
['Barriers', String(run.barrier_count)],
|
|
2584
|
+
['Repo statuses', formatStatusCounts(run.repo_status_counts)],
|
|
2585
|
+
['History entries', String(artifacts.history_entries)],
|
|
2586
|
+
['Decision entries', String(artifacts.decision_entries)],
|
|
2587
|
+
);
|
|
2588
|
+
if (run.completed_at) metaPairs.push(['Completed', `<code>${esc(run.completed_at)}</code>`]);
|
|
2589
|
+
if (run.duration_seconds != null) metaPairs.push(['Duration', `<code>${run.duration_seconds}s</code>`]);
|
|
2590
|
+
if (run.pending_gate) metaPairs.push(['Pending gate', `<code>${esc(run.pending_gate.gate)}</code> (<code>${esc(run.pending_gate.gate_type)}</code>)`]);
|
|
2591
|
+
|
|
2592
|
+
sections.push(`<div class="meta">${htmlDl(metaPairs)}</div>`);
|
|
2593
|
+
|
|
2594
|
+
// Next Actions
|
|
2595
|
+
if (run.next_actions?.length > 0) {
|
|
2596
|
+
const naHtml = '<ol>' + run.next_actions.map((a) => `<li><code>${esc(a.command)}</code>: ${esc(a.reason)}</li>`).join('') + '</ol>';
|
|
2597
|
+
sections.push(`<div class="section">${htmlSection('Next Actions', naHtml)}</div>`);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// Coordinator Timeline
|
|
2601
|
+
if (coordinator_timeline?.length > 0) {
|
|
2602
|
+
const tlRows = coordinator_timeline.map((ev, i) => [String(i + 1), `<code>${esc(ev.type)}</code>`, `<code>${esc(ev.timestamp || 'n/a')}</code>`, esc(ev.summary)]);
|
|
2603
|
+
sections.push(`<div class="section">${htmlSection('Coordinator Timeline', htmlTable(['#', 'Type', 'Time', 'Summary'], tlRows))}</div>`);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// Barrier Summary
|
|
2607
|
+
if (barrier_summary?.length > 0) {
|
|
2608
|
+
const bRows = barrier_summary.map((b) => [
|
|
2609
|
+
`<code>${esc(b.barrier_id)}</code>`,
|
|
2610
|
+
`<code>${esc(b.workstream_id || 'unknown')}</code>`,
|
|
2611
|
+
`<code>${esc(b.type)}</code>`,
|
|
2612
|
+
badge(b.status),
|
|
2613
|
+
`${b.satisfied_repos.length}/${b.required_repos.length} repos`,
|
|
2614
|
+
]);
|
|
2615
|
+
sections.push(`<div class="section">${htmlSection('Barrier Summary', htmlTable(['Barrier', 'Workstream', 'Type', 'Status', 'Satisfied'], bRows))}</div>`);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// Barrier Transitions
|
|
2619
|
+
if (barrier_ledger_timeline?.length > 0) {
|
|
2620
|
+
const btRows = barrier_ledger_timeline.map((t, i) => [String(i + 1), `<code>${esc(t.timestamp || 'n/a')}</code>`, `<code>${esc(t.barrier_id)}</code>`, `<code>${esc(t.previous_status)}</code>`, `<code>${esc(t.new_status)}</code>`, esc(t.summary)]);
|
|
2621
|
+
sections.push(`<div class="section">${htmlSection('Barrier Transitions', htmlTable(['#', 'Time', 'Barrier', 'From', 'To', 'Summary'], btRows))}</div>`);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// Coordinator Decisions
|
|
2625
|
+
if (decision_digest?.length > 0) {
|
|
2626
|
+
const ddList = decision_digest.map((d) => `<li><strong>${esc(d.id)}</strong> (${esc(d.role || '?')}, ${esc(d.phase || '?')} phase): ${esc(d.statement)}</li>`).join('');
|
|
2627
|
+
sections.push(`<div class="section">${htmlSection('Coordinator Decisions', `<ul>${ddList}</ul>`)}</div>`);
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// Approval Policy
|
|
2631
|
+
if (approval_policy_events?.length > 0) {
|
|
2632
|
+
let apHtml = '<ul>';
|
|
2633
|
+
for (const evt of approval_policy_events) {
|
|
2634
|
+
const transition = evt.gate_type === 'run_completion' ? 'run completion' : `${esc(evt.from_phase || '?')} → ${esc(evt.to_phase || '?')}`;
|
|
2635
|
+
apHtml += `<li><strong>${esc(evt.action || 'unknown')}</strong> (${esc(evt.gate_type || 'unknown')}) ${transition} at <code>${esc(evt.timestamp || 'n/a')}</code>`;
|
|
2636
|
+
if (evt.reason) apHtml += `<br>${esc(evt.reason)}`;
|
|
2637
|
+
apHtml += '</li>';
|
|
2638
|
+
}
|
|
2639
|
+
apHtml += '</ul>';
|
|
2640
|
+
sections.push(`<div class="section">${htmlSection('Approval Policy', apHtml)}</div>`);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// Governance Events
|
|
2644
|
+
if (governance_events?.length > 0) {
|
|
2645
|
+
let geHtml = '<ul>';
|
|
2646
|
+
for (const evt of governance_events) {
|
|
2647
|
+
geHtml += `<li><strong>${esc(evt.type)}</strong> (<code>${esc(evt.role || '?')}</code>, <code>${esc(evt.phase || '?')}</code> phase) at <code>${esc(evt.timestamp || 'n/a')}</code>${renderHtmlGovEventDetail(evt)}</li>`;
|
|
2648
|
+
}
|
|
2649
|
+
geHtml += '</ul>';
|
|
2650
|
+
sections.push(`<div class="section">${htmlSection('Governance Events', geHtml)}</div>`);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// Timeout Events
|
|
2654
|
+
if (timeout_events?.length > 0) {
|
|
2655
|
+
let teHtml = '<ul>';
|
|
2656
|
+
for (const evt of timeout_events) {
|
|
2657
|
+
const label = evt.type === 'timeout_warning' ? 'Warning' : evt.type === 'timeout_skip' ? 'Skip' : 'Escalation';
|
|
2658
|
+
const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
|
|
2659
|
+
const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
|
|
2660
|
+
teHtml += `<li><strong>${label}</strong> (<code>${esc(evt.scope || '?')}</code>) \u2014 ${elapsed}/${limit}, action: <code>${esc(evt.action || 'n/a')}</code> at <code>${esc(evt.timestamp || 'n/a')}</code></li>`;
|
|
2661
|
+
}
|
|
2662
|
+
teHtml += '</ul>';
|
|
2663
|
+
sections.push(`<div class="section">${htmlSection('Timeout Events', teHtml)}</div>`);
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// Recovery Report
|
|
2667
|
+
if (coordRecoveryReport) {
|
|
2668
|
+
sections.push(`<div class="section">${htmlSection('Recovery Report', htmlDl([
|
|
2669
|
+
['Trigger', esc(coordRecoveryReport.trigger || 'n/a')],
|
|
2670
|
+
['Impact', esc(coordRecoveryReport.impact || 'n/a')],
|
|
2671
|
+
['Mitigation', esc(coordRecoveryReport.mitigation || 'n/a')],
|
|
2672
|
+
['Owner', esc(coordRecoveryReport.owner || 'n/a')],
|
|
2673
|
+
['Exit Condition', esc(coordRecoveryReport.exit_condition || 'n/a')],
|
|
2674
|
+
]))}</div>`);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// Repo Details
|
|
2678
|
+
if (repos?.length > 0) {
|
|
2679
|
+
let repoHtml = '';
|
|
2680
|
+
for (const repo of repos) {
|
|
2681
|
+
if (!repo.ok) {
|
|
2682
|
+
repoHtml += `<h3>${esc(repo.repo_id)}</h3><p>Failed export: ${esc(repo.error || 'unknown error')}, path <code>${esc(repo.path || 'unknown')}</code></p>`;
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
const repoPairs = [
|
|
2686
|
+
['Status', badge(repo.status || 'unknown')],
|
|
2687
|
+
['Run', `<code>${esc(repo.run_id || 'none')}</code>`],
|
|
2688
|
+
['Phase', `<code>${esc(repo.phase || 'unknown')}</code>`],
|
|
2689
|
+
['Path', `<code>${esc(repo.path || 'unknown')}</code>`],
|
|
2690
|
+
];
|
|
2691
|
+
if (repo.blocked_on) repoPairs.push(['Blocked on', `<code>${esc(summarizeBlockedOn(repo.blocked_on))}</code>`]);
|
|
2692
|
+
repoHtml += `<h3>${esc(repo.repo_id)}</h3>${htmlDl(repoPairs)}`;
|
|
2693
|
+
|
|
2694
|
+
if (repo.turns?.length > 0) {
|
|
2695
|
+
const turnRows = repo.turns.map((t, i) => {
|
|
2696
|
+
const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
|
|
2697
|
+
const phase = t.phase_transition ? `${esc(t.phase || '?')} → ${esc(t.phase_transition)}` : esc(t.phase || '?');
|
|
2698
|
+
return [String(i + 1), esc(t.role), phase, esc(t.summary || '(no summary)'), String(t.files_changed_count), cost, esc(formatTurnTimelineTime(t))];
|
|
2699
|
+
});
|
|
2700
|
+
repoHtml += htmlSection('Turn Timeline', htmlTable(['#', 'Role', 'Phase', 'Summary', 'Files', 'Cost', 'Time'], turnRows), 4);
|
|
2701
|
+
}
|
|
2702
|
+
if (repo.decisions?.length > 0) {
|
|
2703
|
+
repoHtml += htmlSection('Decisions', '<ul>' + repo.decisions.map((d) => `<li><strong>${esc(d.id)}</strong> (${esc(d.role || '?')}, ${esc(d.phase || '?')} phase): ${esc(d.statement)}</li>`).join('') + '</ul>', 4);
|
|
2704
|
+
}
|
|
2705
|
+
if (repo.gate_summary?.length > 0) {
|
|
2706
|
+
repoHtml += htmlSection('Gate Outcomes', '<ul>' + repo.gate_summary.map((g) => `<li><code>${esc(g.gate_id)}</code>: ${badge(g.status)}</li>`).join('') + '</ul>', 4);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
sections.push(`<div class="section">${htmlSection('Repo Details', repoHtml)}</div>`);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
return wrapHtml('AgentXchain Governance Report — Coordinator', sections.join('\n'));
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
export function formatGovernanceReportHtml(report) {
|
|
2716
|
+
if (report.overall === 'error') {
|
|
2717
|
+
return wrapHtml('AgentXchain Governance Report — Error', `
|
|
2718
|
+
<div class="meta">
|
|
2719
|
+
${htmlDl([['Input', `<code>${esc(report.input)}</code>`], ['Status', badge('error')], ['Message', esc(report.message)]])}
|
|
2720
|
+
</div>`);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
if (report.overall === 'fail') {
|
|
2724
|
+
const errorList = (report.verification?.errors || []).map((e) => `<li>${esc(e)}</li>`).join('');
|
|
2725
|
+
return wrapHtml('AgentXchain Governance Report — Fail', `
|
|
2726
|
+
<div class="meta">
|
|
2727
|
+
${htmlDl([['Input', `<code>${esc(report.input)}</code>`], ['Verification', badge('fail')], ['Message', esc(report.message)]])}
|
|
2728
|
+
</div>
|
|
2729
|
+
${errorList ? `<div class="section"><h2>Verification Errors</h2><ul>${errorList}</ul></div>` : ''}`);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
if (report.subject?.kind === 'governed_run') {
|
|
2733
|
+
return renderRunHtml(report);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
return renderCoordinatorHtml(report);
|
|
2737
|
+
}
|
|
@@ -76,6 +76,16 @@
|
|
|
76
76
|
"rationale": {
|
|
77
77
|
"type": "string",
|
|
78
78
|
"minLength": 1
|
|
79
|
+
},
|
|
80
|
+
"durability": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"enum": ["run", "repo"],
|
|
83
|
+
"description": "Decision persistence scope. 'run' (default) dies with the run; 'repo' persists across runs as a binding constraint."
|
|
84
|
+
},
|
|
85
|
+
"overrides": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"pattern": "^DEC-\\d+$",
|
|
88
|
+
"description": "ID of an active repo-durable decision this decision supersedes."
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
91
|
}
|
|
@@ -325,6 +325,8 @@ function checkPlaceholder(errors, fieldPath, value) {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
const VALID_DURABILITIES = ['run', 'repo'];
|
|
329
|
+
|
|
328
330
|
function validateDecision(dec, index) {
|
|
329
331
|
const errors = [];
|
|
330
332
|
const prefix = `decisions[${index}]`;
|
|
@@ -344,6 +346,17 @@ function validateDecision(dec, index) {
|
|
|
344
346
|
if (typeof dec.rationale !== 'string' || !dec.rationale.trim()) {
|
|
345
347
|
errors.push(`${prefix}.rationale must be a non-empty string.`);
|
|
346
348
|
}
|
|
349
|
+
if (dec.durability !== undefined && !VALID_DURABILITIES.includes(dec.durability)) {
|
|
350
|
+
errors.push(`${prefix}.durability must be one of: ${VALID_DURABILITIES.join(', ')}.`);
|
|
351
|
+
}
|
|
352
|
+
if (dec.overrides !== undefined) {
|
|
353
|
+
if (typeof dec.overrides !== 'string' || !/^DEC-\d+$/.test(dec.overrides)) {
|
|
354
|
+
errors.push(`${prefix}.overrides must match pattern DEC-NNN.`);
|
|
355
|
+
}
|
|
356
|
+
if (dec.overrides === dec.id) {
|
|
357
|
+
errors.push(`${prefix}.overrides cannot reference itself.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
347
360
|
return errors;
|
|
348
361
|
}
|
|
349
362
|
|