agentxchain 2.102.0 → 2.103.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.102.0",
3
+ "version": "2.103.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,9 +5,9 @@
5
5
  */
6
6
 
7
7
  import { resolve } from 'path';
8
- import { existsSync } from 'fs';
8
+ import { existsSync, readFileSync } from 'fs';
9
9
  import chalk from 'chalk';
10
- import { readRepoDecisions, getActiveRepoDecisions, getRepoDecisionById } from '../lib/repo-decisions.js';
10
+ import { readRepoDecisions, getActiveRepoDecisions, getRepoDecisionById, resolveDecisionAuthority } from '../lib/repo-decisions.js';
11
11
 
12
12
  /**
13
13
  * @param {object} opts - { json?: boolean, all?: boolean, show?: string, dir?: string }
@@ -39,6 +39,18 @@ export async function decisionsCommand(opts) {
39
39
  console.log(` Phase: ${dec.phase || '—'}`);
40
40
  console.log(` Run: ${(dec.run_id || '—').slice(0, 16)}`);
41
41
  console.log(` Turn: ${(dec.turn_id || '—').slice(0, 16)}`);
42
+ console.log(` Durability: ${dec.durability || 'repo'}`);
43
+ // Show decision authority if config has it
44
+ const config = loadConfig(root);
45
+ if (config && dec.role) {
46
+ const auth = resolveDecisionAuthority(dec.role, config);
47
+ if (auth !== null && !(typeof auth === 'object' && auth.unknown)) {
48
+ console.log(` Authority: ${auth} (${dec.role})`);
49
+ }
50
+ }
51
+ if (dec.overrides) {
52
+ console.log(` Supersedes: ${chalk.yellow(dec.overrides)}`);
53
+ }
42
54
  console.log(` Created: ${dec.created_at || '—'}`);
43
55
  if (dec.overridden_by) {
44
56
  console.log(` Overridden: ${chalk.yellow(dec.overridden_by)}`);
@@ -69,7 +81,11 @@ export async function decisionsCommand(opts) {
69
81
  for (const dec of decisions) {
70
82
  const status = formatStatus(dec.status);
71
83
  const runShort = (dec.run_id || '').slice(0, 12);
72
- const override = dec.overridden_by ? chalk.dim(` → ${dec.overridden_by}`) : '';
84
+ const override = dec.overridden_by
85
+ ? chalk.dim(` → ${dec.overridden_by}`)
86
+ : dec.overrides
87
+ ? chalk.dim(` ← supersedes ${dec.overrides}`)
88
+ : '';
73
89
  console.log(` ${chalk.cyan(dec.id)} ${status} ${chalk.dim(dec.category)} ${dec.statement}${override}`);
74
90
  console.log(` ${chalk.dim(`by ${dec.role || '?'} in ${runShort || '?'}`)}`);
75
91
  }
@@ -92,3 +108,13 @@ function findProjectRoot(dir) {
92
108
  }
93
109
  return null;
94
110
  }
111
+
112
+ function loadConfig(root) {
113
+ const configPath = resolve(root, 'agentxchain.json');
114
+ if (!existsSync(configPath)) return null;
115
+ try {
116
+ return JSON.parse(readFileSync(configPath, 'utf8'));
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
@@ -37,13 +37,19 @@ function listRoles(roles, roleIds, opts) {
37
37
  }
38
38
 
39
39
  if (opts.json) {
40
- const output = roleIds.map((id) => ({
41
- id,
42
- title: roles[id].title,
43
- mandate: roles[id].mandate,
44
- write_authority: roles[id].write_authority,
45
- runtime: roles[id].runtime,
46
- }));
40
+ const output = roleIds.map((id) => {
41
+ const entry = {
42
+ id,
43
+ title: roles[id].title,
44
+ mandate: roles[id].mandate,
45
+ write_authority: roles[id].write_authority,
46
+ runtime: roles[id].runtime,
47
+ };
48
+ if (typeof roles[id].decision_authority === 'number') {
49
+ entry.decision_authority = roles[id].decision_authority;
50
+ }
51
+ return entry;
52
+ });
47
53
  console.log(JSON.stringify(output, null, 2));
48
54
  return;
49
55
  }
@@ -56,7 +62,8 @@ function listRoles(roles, roleIds, opts) {
56
62
  : r.write_authority === 'proposed'
57
63
  ? chalk.yellow(r.write_authority)
58
64
  : chalk.dim(r.write_authority);
59
- console.log(` ${chalk.cyan(id)} ${r.title} [${authority}] ${chalk.dim(r.runtime)}`);
65
+ const decAuth = typeof r.decision_authority === 'number' ? chalk.dim(` dec:${r.decision_authority}`) : '';
66
+ console.log(` ${chalk.cyan(id)} — ${r.title} [${authority}${decAuth}] → ${chalk.dim(r.runtime)}`);
60
67
  }
61
68
  console.log('');
62
69
  console.log(chalk.dim(' Usage: agentxchain role show <role_id>\n'));
@@ -81,13 +88,17 @@ function showRole(roleId, roles, roleIds, opts) {
81
88
  const r = roles[roleId];
82
89
 
83
90
  if (opts.json) {
84
- console.log(JSON.stringify({
91
+ const entry = {
85
92
  id: roleId,
86
93
  title: r.title,
87
94
  mandate: r.mandate,
88
95
  write_authority: r.write_authority,
89
96
  runtime: r.runtime,
90
- }, null, 2));
97
+ };
98
+ if (typeof r.decision_authority === 'number') {
99
+ entry.decision_authority = r.decision_authority;
100
+ }
101
+ console.log(JSON.stringify(entry, null, 2));
91
102
  return;
92
103
  }
93
104
 
@@ -101,6 +112,9 @@ function showRole(roleId, roles, roleIds, opts) {
101
112
  console.log(` Title: ${r.title}`);
102
113
  console.log(` Mandate: ${r.mandate}`);
103
114
  console.log(` Authority: ${authority}`);
115
+ if (typeof r.decision_authority === 'number') {
116
+ console.log(` Decision: ${r.decision_authority}`);
117
+ }
104
118
  console.log(` Runtime: ${chalk.dim(r.runtime)}`);
105
119
  console.log('');
106
120
  }
@@ -618,7 +618,7 @@ function renderContext(state, config, root, turn, role) {
618
618
 
619
619
  // Repo-level decisions that persist across runs
620
620
  if (state.repo_decisions && state.repo_decisions.length > 0) {
621
- const repoDecMd = renderRepoDecisionsMarkdown(state.repo_decisions);
621
+ const repoDecMd = renderRepoDecisionsMarkdown(state.repo_decisions, config);
622
622
  if (repoDecMd) {
623
623
  lines.push(repoDecMd);
624
624
  }
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { isDeepStrictEqual } from 'node:util';
5
+ import { summarizeRepoDecisions } from './repo-decisions.js';
5
6
 
6
7
  const SUPPORTED_EXPORT_SCHEMA_VERSIONS = new Set(['0.2', '0.3']);
7
8
  const VALID_FILE_FORMATS = new Set(['json', 'jsonl', 'text']);
@@ -369,38 +370,18 @@ function verifyDelegationSummary(artifact, errors) {
369
370
  }
370
371
  }
371
372
 
372
- function buildExpectedRepoDecisionsSummary(files) {
373
+ function buildExpectedRepoDecisionsSummary(files, config = null) {
373
374
  const repoDecisionsData = files?.['.agentxchain/repo-decisions.jsonl']?.data;
374
375
  if (!Array.isArray(repoDecisionsData) || repoDecisionsData.length === 0) {
375
376
  return null;
376
377
  }
377
-
378
- const active = repoDecisionsData.filter((d) => d.status === 'active');
379
- const overridden = repoDecisionsData.filter((d) => d.status === 'overridden');
380
-
381
- return {
382
- total: repoDecisionsData.length,
383
- active_count: active.length,
384
- overridden_count: overridden.length,
385
- active: active.map((d) => ({
386
- id: d.id,
387
- category: d.category,
388
- statement: d.statement,
389
- role: d.role,
390
- run_id: d.run_id,
391
- })),
392
- overridden: overridden.map((d) => ({
393
- id: d.id,
394
- overridden_by: d.overridden_by,
395
- statement: d.statement,
396
- })),
397
- };
378
+ return summarizeRepoDecisions(repoDecisionsData, config);
398
379
  }
399
380
 
400
381
  function verifyRepoDecisionsSummary(artifact, errors) {
401
382
  const summary = artifact.summary?.repo_decisions;
402
383
  const hasFile = '.agentxchain/repo-decisions.jsonl' in (artifact.files || {});
403
- const expected = buildExpectedRepoDecisionsSummary(artifact.files);
384
+ const expected = buildExpectedRepoDecisionsSummary(artifact.files, artifact.config || null);
404
385
 
405
386
  if (summary === null && expected === null) {
406
387
  return;
package/src/lib/export.js CHANGED
@@ -8,7 +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
+ import { readRepoDecisions, summarizeRepoDecisions } from './repo-decisions.js';
12
12
  import { RUN_EVENTS_PATH } from './run-events.js';
13
13
 
14
14
  const EXPORT_SCHEMA_VERSION = '0.3';
@@ -211,18 +211,8 @@ function buildDashboardSessionSummary(root) {
211
211
  };
212
212
  }
213
213
 
214
- export function buildRepoDecisionsSummary(root) {
215
- const all = readRepoDecisions(root);
216
- if (!all || all.length === 0) return null;
217
- const active = all.filter(d => d.status === 'active');
218
- const overridden = all.filter(d => d.status === 'overridden');
219
- return {
220
- total: all.length,
221
- active_count: active.length,
222
- overridden_count: overridden.length,
223
- active: active.map(d => ({ id: d.id, category: d.category, statement: d.statement, role: d.role, run_id: d.run_id })),
224
- overridden: overridden.map(d => ({ id: d.id, overridden_by: d.overridden_by, statement: d.statement })),
225
- };
214
+ export function buildRepoDecisionsSummary(root, config = null) {
215
+ return summarizeRepoDecisions(readRepoDecisions(root), config);
226
216
  }
227
217
 
228
218
  export function buildDelegationSummary(files) {
@@ -471,7 +461,7 @@ export function buildRunExport(startDir = process.cwd()) {
471
461
  coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
472
462
  dashboard_session: buildDashboardSessionSummary(root),
473
463
  delegation_summary: buildDelegationSummary(files),
474
- repo_decisions: buildRepoDecisionsSummary(root),
464
+ repo_decisions: buildRepoDecisionsSummary(root, rawConfig),
475
465
  },
476
466
  workspace: buildRunWorkspaceMetadata(root),
477
467
  files,
@@ -2437,7 +2437,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2437
2437
  if (turnResult.decisions && turnResult.decisions.length > 0) {
2438
2438
  for (const dec of turnResult.decisions) {
2439
2439
  if (dec.overrides) {
2440
- const overrideCheck = validateOverride(root, dec);
2440
+ const overrideCheck = validateOverride(root, { ...dec, role: dec.role || turnResult.role }, config);
2441
2441
  if (!overrideCheck.ok) {
2442
2442
  return {
2443
2443
  ok: false,
@@ -3359,6 +3359,8 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3359
3359
  category: dec.category,
3360
3360
  statement: dec.statement,
3361
3361
  rationale: dec.rationale,
3362
+ durability: dec.durability || 'repo',
3363
+ overrides: dec.overrides || null,
3362
3364
  status: 'active',
3363
3365
  overridden_by: null,
3364
3366
  created_at: now,
@@ -366,6 +366,11 @@ export function validateV4Config(data, projectRoot) {
366
366
  if (!VALID_WRITE_AUTHORITIES.includes(role.write_authority)) {
367
367
  errors.push(`Role "${id}": write_authority must be one of: ${VALID_WRITE_AUTHORITIES.join(', ')}`);
368
368
  }
369
+ if (role.decision_authority !== undefined && role.decision_authority !== null) {
370
+ if (!Number.isInteger(role.decision_authority) || role.decision_authority < 0 || role.decision_authority > 99) {
371
+ errors.push(`Role "${id}": decision_authority must be an integer between 0 and 99`);
372
+ }
373
+ }
369
374
  if (typeof role.runtime !== 'string' || !role.runtime.trim()) errors.push(`Role "${id}": runtime required`);
370
375
  }
371
376
  }
@@ -37,6 +37,74 @@ export function getRepoDecisionById(root, decisionId) {
37
37
  return readRepoDecisions(root).find(d => d.id === decisionId) || null;
38
38
  }
39
39
 
40
+ export function getDecisionAuthorityMetadata(roleId, config) {
41
+ const resolved = resolveDecisionAuthority(roleId, config);
42
+ if (resolved === null) return null;
43
+ if (typeof resolved === 'object' && resolved.unknown) {
44
+ return {
45
+ level: resolved.level,
46
+ source: 'unknown_role',
47
+ role: roleId || null,
48
+ };
49
+ }
50
+ if (roleId === 'human') {
51
+ const explicitHumanAuthority = typeof config?.roles?.human?.decision_authority === 'number';
52
+ return {
53
+ level: resolved,
54
+ source: explicitHumanAuthority ? 'configured' : 'human_default',
55
+ role: roleId,
56
+ };
57
+ }
58
+ return {
59
+ level: resolved,
60
+ source: 'configured',
61
+ role: roleId || null,
62
+ };
63
+ }
64
+
65
+ export function summarizeRepoDecisions(decisions, config) {
66
+ if (!Array.isArray(decisions) || decisions.length === 0) return null;
67
+ const active = decisions.filter((d) => d.status === 'active');
68
+ const overridden = decisions.filter((d) => d.status === 'overridden');
69
+ const addAuthority = (decision) => {
70
+ const authority = getDecisionAuthorityMetadata(decision.role, config);
71
+ return {
72
+ id: decision.id,
73
+ category: decision.category,
74
+ statement: decision.statement,
75
+ role: decision.role,
76
+ run_id: decision.run_id,
77
+ overrides: decision.overrides || null,
78
+ durability: decision.durability || 'repo',
79
+ authority_level: authority?.level ?? null,
80
+ authority_source: authority?.source || null,
81
+ };
82
+ };
83
+ return {
84
+ total: decisions.length,
85
+ active_count: active.length,
86
+ overridden_count: overridden.length,
87
+ active: active.map(addAuthority),
88
+ overridden: overridden.map((d) => {
89
+ const authority = getDecisionAuthorityMetadata(d.role, config);
90
+ return {
91
+ id: d.id,
92
+ overridden_by: d.overridden_by,
93
+ statement: d.statement,
94
+ overrides: d.overrides || null,
95
+ durability: d.durability || 'repo',
96
+ role: d.role || null,
97
+ authority_level: authority?.level ?? null,
98
+ authority_source: authority?.source || null,
99
+ };
100
+ }),
101
+ };
102
+ }
103
+
104
+ export function buildRepoDecisionsSummary(decisions) {
105
+ return summarizeRepoDecisions(decisions, null);
106
+ }
107
+
40
108
  // ── Write ───────────────────────────────────────────────────────────────────
41
109
 
42
110
  export function appendRepoDecision(root, entry) {
@@ -62,7 +130,16 @@ export function overrideRepoDecision(root, targetId, overridingId) {
62
130
 
63
131
  // ── Validate Override ───────────────────────────────────────────────────────
64
132
 
65
- export function validateOverride(root, decision) {
133
+ /**
134
+ * Validate that an override is allowed.
135
+ * @param {string} root - project root
136
+ * @param {object} decision - the overriding decision (must have .overrides, .id, optionally .role)
137
+ * @param {object} [config] - agentxchain config (used for authority enforcement)
138
+ * @returns {{ ok: boolean, error?: string, warning?: string }}
139
+ *
140
+ * DEC-SPEC: .planning/DECISION_AUTHORITY_SPEC.md
141
+ */
142
+ export function validateOverride(root, decision, config) {
66
143
  if (!decision.overrides) return { ok: true };
67
144
  const targetId = decision.overrides;
68
145
  const target = getRepoDecisionById(root, targetId);
@@ -75,21 +152,104 @@ export function validateOverride(root, decision) {
75
152
  if (target.status !== 'active') {
76
153
  return { ok: false, error: `decisions: ${targetId} has status "${target.status}", only active repo decisions can be overridden.` };
77
154
  }
155
+
156
+ // Authority enforcement (opt-in via decision_authority on roles)
157
+ const authorityResult = checkOverrideAuthority(decision, target, config);
158
+ if (!authorityResult.ok) return authorityResult;
159
+
160
+ return authorityResult.warning ? { ok: true, warning: authorityResult.warning } : { ok: true };
161
+ }
162
+
163
+ /**
164
+ * Resolve the decision_authority level for a role.
165
+ * - 'human' defaults to 100 unless explicitly configured.
166
+ * - Unknown roles default to 0 (with warning).
167
+ * - Null means opt-out (no enforcement).
168
+ */
169
+ export function resolveDecisionAuthority(roleId, config) {
170
+ if (!config || !config.roles) return null;
171
+ if (roleId === 'human') {
172
+ const humanRole = config.roles.human;
173
+ if (humanRole && typeof humanRole.decision_authority === 'number') {
174
+ return humanRole.decision_authority;
175
+ }
176
+ return 100; // human default
177
+ }
178
+ const role = config.roles[roleId];
179
+ if (!role) return { level: 0, unknown: true };
180
+ if (typeof role.decision_authority !== 'number') return null;
181
+ return role.decision_authority;
182
+ }
183
+
184
+ /**
185
+ * Check whether the overriding role has sufficient authority to override
186
+ * a decision made by the target role.
187
+ */
188
+ function checkOverrideAuthority(overridingDecision, targetDecision, config) {
189
+ if (!config || !config.roles) return { ok: true };
190
+
191
+ const overridingRole = overridingDecision.role;
192
+ const targetRole = targetDecision.role;
193
+
194
+ // Same-role override is always allowed
195
+ if (overridingRole && targetRole && overridingRole === targetRole) return { ok: true };
196
+
197
+ const targetAuth = resolveDecisionAuthority(targetRole, config);
198
+ const overridingAuth = resolveDecisionAuthority(overridingRole, config);
199
+
200
+ // Handle unknown target role
201
+ let warning;
202
+ if (targetAuth && typeof targetAuth === 'object' && targetAuth.unknown) {
203
+ warning = `decisions: target decision role '${targetRole}' not found in current config, treating as authority 0.`;
204
+ // targetAuth is effectively 0, allow override
205
+ return { ok: true, warning };
206
+ }
207
+
208
+ // Opt-in: if either side is null (not configured), allow
209
+ if (targetAuth === null || overridingAuth === null) return { ok: true };
210
+
211
+ // Handle unknown overriding role (shouldn't normally happen, but be safe)
212
+ const overridingLevel = (typeof overridingAuth === 'object' && overridingAuth.unknown) ? 0 : overridingAuth;
213
+ const targetLevel = (typeof targetAuth === 'object') ? 0 : targetAuth;
214
+
215
+ if (overridingLevel < targetLevel) {
216
+ return {
217
+ ok: false,
218
+ error: `decisions: role '${overridingRole}' (authority ${overridingLevel}) cannot override ${targetDecision.id} made by '${targetRole}' (authority ${targetLevel}). Override requires authority >= ${targetLevel}.`,
219
+ };
220
+ }
221
+
78
222
  return { ok: true };
79
223
  }
80
224
 
81
225
  // ── Render ──────────────────────────────────────────────────────────────────
82
226
 
83
- export function renderRepoDecisionsMarkdown(activeDecisions) {
227
+ export function renderRepoDecisionsMarkdown(activeDecisions, config) {
84
228
  if (!activeDecisions || activeDecisions.length === 0) return '';
229
+ const hasAuthorityPolicy = Object.values(config?.roles || {}).some((role) => (
230
+ role && typeof role.decision_authority === 'number'
231
+ ));
85
232
  const lines = [
86
233
  '## Active Repo Decisions',
87
234
  '',
88
235
  'These decisions persist from prior governed runs. Comply or explicitly override with rationale.',
89
236
  '',
90
237
  ];
238
+ if (hasAuthorityPolicy) {
239
+ lines.push('When both roles declare `decision_authority`, overrides require authority greater than or equal to the originating role.');
240
+ lines.push('');
241
+ }
91
242
  for (const d of activeDecisions) {
92
- lines.push(`- **${d.id}** (${d.category}): ${d.statement}`);
243
+ const authority = getDecisionAuthorityMetadata(d.role, config);
244
+ const authorityText = authority
245
+ ? authority.source === 'human_default'
246
+ ? ' authority 100 (human default)'
247
+ : authority.source === 'unknown_role'
248
+ ? ' authority 0 (role no longer in config)'
249
+ : ` authority ${authority.level}`
250
+ : '';
251
+ const supersedes = d.overrides ? ` Supersedes ${d.overrides}.` : '';
252
+ lines.push(`- **${d.id}** (${d.category}, by ${d.role || 'unknown'}${authorityText}): ${d.statement}${supersedes}`);
93
253
  }
94
254
  lines.push('');
95
255
  return lines.join('\n');
package/src/lib/report.js CHANGED
@@ -1320,7 +1320,13 @@ export function formatGovernanceReportText(report) {
1320
1320
  lines.push('', 'Repo Decisions:');
1321
1321
  lines.push(` Active: ${run.repo_decisions.active_count} Overridden: ${run.repo_decisions.overridden_count}`);
1322
1322
  for (const d of run.repo_decisions.active) {
1323
- lines.push(` - ${d.id} (${d.category}): ${d.statement}`);
1323
+ const supersedes = d.overrides ? ` | supersedes ${d.overrides}` : '';
1324
+ const authority = d.authority_level == null ? '' : ` | authority ${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
1325
+ lines.push(` - ${d.id} (${d.category}): ${d.statement}${supersedes}${authority}`);
1326
+ }
1327
+ for (const d of run.repo_decisions.overridden || []) {
1328
+ const authority = d.authority_level == null ? '' : ` | authority ${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
1329
+ lines.push(` - ${d.id} (overridden by ${d.overridden_by || 'unknown'}${authority})`);
1324
1330
  }
1325
1331
  }
1326
1332
 
@@ -1825,10 +1831,20 @@ export function formatGovernanceReportMarkdown(report) {
1825
1831
  if (run.repo_decisions?.active?.length > 0) {
1826
1832
  lines.push('', '## Repo Decisions', '');
1827
1833
  lines.push(`Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}`, '');
1828
- lines.push('| ID | Category | Statement | Role | Run |', '|----|----------|-----------|------|-----|');
1834
+ lines.push('| ID | Category | Statement | Role | Authority | Run | Supersedes |', '|----|----------|-----------|------|-----------|-----|------------|');
1829
1835
  for (const d of run.repo_decisions.active) {
1830
1836
  const stmt = (d.statement || '').replace(/\|/g, '\\|');
1831
- lines.push(`| ${d.id} | ${d.category} | ${stmt} | ${d.role || ''} | \`${(d.run_id || '').slice(0, 12)}\` |`);
1837
+ const authority = d.authority_level == null ? '—' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
1838
+ lines.push(`| ${d.id} | ${d.category} | ${stmt} | ${d.role || '—'} | ${authority} | \`${(d.run_id || '').slice(0, 12)}\` | ${d.overrides || '—'} |`);
1839
+ }
1840
+ if (run.repo_decisions.overridden?.length > 0) {
1841
+ lines.push('', 'Overridden decisions:', '');
1842
+ lines.push('| ID | Statement | Authority | Overridden By |', '|----|-----------|-----------|---------------|');
1843
+ for (const d of run.repo_decisions.overridden) {
1844
+ const stmt = (d.statement || '').replace(/\|/g, '\\|');
1845
+ const authority = d.authority_level == null ? '—' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
1846
+ lines.push(`| ${d.id} | ${stmt} | ${authority} | ${d.overridden_by || '—'} |`);
1847
+ }
1832
1848
  }
1833
1849
  }
1834
1850
 
@@ -2453,9 +2469,28 @@ function renderRunHtml(report) {
2453
2469
  if (run.repo_decisions?.active?.length > 0) {
2454
2470
  let rdHtml = `<p>Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}</p>`;
2455
2471
  rdHtml += htmlTable(
2456
- ['ID', 'Category', 'Statement', 'Role', 'Run'],
2457
- 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>`]),
2472
+ ['ID', 'Category', 'Statement', 'Role', 'Authority', 'Run', 'Supersedes'],
2473
+ run.repo_decisions.active.map((d) => [
2474
+ esc(d.id),
2475
+ esc(d.category),
2476
+ esc(d.statement || ''),
2477
+ esc(d.role || '\u2014'),
2478
+ esc(d.authority_level == null ? '\u2014' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`),
2479
+ `<code>${esc((d.run_id || '').slice(0, 12))}</code>`,
2480
+ esc(d.overrides || '\u2014'),
2481
+ ]),
2458
2482
  );
2483
+ if (run.repo_decisions.overridden?.length > 0) {
2484
+ rdHtml += htmlTable(
2485
+ ['ID', 'Statement', 'Authority', 'Overridden By'],
2486
+ run.repo_decisions.overridden.map((d) => [
2487
+ esc(d.id),
2488
+ esc(d.statement || ''),
2489
+ esc(d.authority_level == null ? '\u2014' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`),
2490
+ esc(d.overridden_by || '\u2014'),
2491
+ ]),
2492
+ );
2493
+ }
2459
2494
  sections.push(`<div class="section">${htmlSection('Repo Decisions', rdHtml)}</div>`);
2460
2495
  }
2461
2496