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 +1 -1
- package/src/commands/decisions.js +29 -3
- package/src/commands/role.js +24 -10
- package/src/lib/dispatch-bundle.js +1 -1
- package/src/lib/export-verifier.js +4 -23
- package/src/lib/export.js +4 -14
- package/src/lib/governed-state.js +3 -1
- package/src/lib/normalized-config.js +5 -0
- package/src/lib/repo-decisions.js +163 -3
- package/src/lib/report.js +40 -5
package/package.json
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/commands/role.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => [
|
|
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
|
|