agentxchain 2.158.0 → 2.159.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.
@@ -971,6 +971,12 @@ roleCmd
971
971
  .option('-j, --json', 'Output as JSON')
972
972
  .action((roleId, opts) => roleCommand('show', roleId, opts));
973
973
 
974
+ roleCmd
975
+ .command('validate [role_id]')
976
+ .description('Validate role charter well-formedness against the four VISION:123–128 invariants (exit 1 if any role incomplete)')
977
+ .option('-j, --json', 'Output as JSON')
978
+ .action((roleId, opts) => roleCommand('validate', roleId, opts));
979
+
974
980
  const turnCmd = program
975
981
  .command('turn')
976
982
  .description('Inspect active governed turn dispatch bundles');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.158.0",
3
+ "version": "2.159.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,10 @@ import {
4
4
  getRoleRuntimeCapabilityContract,
5
5
  summarizeRuntimeCapabilityContract,
6
6
  } from '../lib/runtime-capabilities.js';
7
+ import {
8
+ evaluateRoleCharter,
9
+ evaluateAllRoleCharters,
10
+ } from '../lib/role-charter.js';
7
11
 
8
12
  export function roleCommand(subcommand, roleId, opts) {
9
13
  const context = loadProjectContext();
@@ -12,7 +16,7 @@ export function roleCommand(subcommand, roleId, opts) {
12
16
  process.exit(1);
13
17
  }
14
18
 
15
- const { config, version } = context;
19
+ const { config, rawConfig, version } = context;
16
20
 
17
21
  if (version !== 4) {
18
22
  console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
@@ -27,6 +31,10 @@ export function roleCommand(subcommand, roleId, opts) {
27
31
  return showRole(roleId, roles, runtimes, roleIds, opts);
28
32
  }
29
33
 
34
+ if (subcommand === 'validate') {
35
+ return validateRoles(roleId, config, rawConfig, roleIds, opts);
36
+ }
37
+
30
38
  // Default: list
31
39
  return listRoles(roles, runtimes, roleIds, opts);
32
40
  }
@@ -155,3 +163,63 @@ function showRole(roleId, roles, runtimes, roleIds, opts) {
155
163
  }
156
164
  console.log('');
157
165
  }
166
+
167
+ function renderCharterReport(report) {
168
+ const mark = report.overall === 'well_formed' ? chalk.green('✓') : chalk.red('✗');
169
+ if (report.overall === 'well_formed') {
170
+ console.log(` ${mark} ${chalk.cyan(report.role_id)} well-formed (4/4 invariants)`);
171
+ return;
172
+ }
173
+ console.log(` ${mark} ${chalk.cyan(report.role_id)} incomplete — missing: ${report.missing.join(', ')}`);
174
+ for (const inv of report.invariants) {
175
+ if (!inv.satisfied && inv.fix_hint) {
176
+ console.log(chalk.dim(` → ${inv.fix_hint}`));
177
+ }
178
+ }
179
+ }
180
+
181
+ function validateRoles(roleId, config, rawConfig, roleIds, opts) {
182
+ // Single-role validation
183
+ if (roleId) {
184
+ if (!config.roles?.[roleId]) {
185
+ console.log(chalk.red(` Unknown role: ${roleId}`));
186
+ console.log(chalk.dim(` Available: ${roleIds.join(', ')}`));
187
+ process.exit(1);
188
+ }
189
+ const report = evaluateRoleCharter(config, rawConfig, roleId);
190
+ if (opts.json) {
191
+ console.log(JSON.stringify(report, null, 2));
192
+ } else {
193
+ console.log('');
194
+ renderCharterReport(report);
195
+ console.log('');
196
+ }
197
+ process.exit(report.overall === 'well_formed' ? 0 : 1);
198
+ }
199
+
200
+ // All-roles validation
201
+ const all = evaluateAllRoleCharters(config, rawConfig);
202
+ if (opts.json) {
203
+ console.log(JSON.stringify(all, null, 2));
204
+ process.exit(all.incomplete === 0 ? 0 : 1);
205
+ }
206
+
207
+ if (all.total === 0) {
208
+ console.log(' No roles defined.');
209
+ process.exit(0);
210
+ }
211
+
212
+ if (all.incomplete === 0) {
213
+ console.log(chalk.bold(`\n Role charter validation (${all.total} roles): all well-formed (4/4 invariants each).\n`));
214
+ process.exit(0);
215
+ }
216
+
217
+ console.log(chalk.bold(`\n Role charter validation (${all.total} roles):\n`));
218
+ for (const report of all.roles) {
219
+ renderCharterReport(report);
220
+ }
221
+ console.log('');
222
+ console.log(` ${all.incomplete} of ${all.total} roles incomplete.`);
223
+ console.log('');
224
+ process.exit(1);
225
+ }
package/src/lib/report.js CHANGED
@@ -15,6 +15,7 @@ import { extractGateActionDigest } from './gate-actions.js';
15
15
  import { buildRecoveryClassificationReport } from './recovery-classification.js';
16
16
  import { buildShipStatusSummary } from './ship-status.js';
17
17
  import { buildHumanAttentionSummary } from './human-attention.js';
18
+ import { buildRoleCharterSummary } from './role-charter.js';
18
19
 
19
20
  export const GOVERNANCE_REPORT_VERSION = '0.1';
20
21
 
@@ -1081,6 +1082,7 @@ function buildRunSubject(artifact) {
1081
1082
  repo_decisions: artifact.summary?.repo_decisions || null,
1082
1083
  ship_status: buildShipStatusSummary(artifact),
1083
1084
  human_attention: buildHumanAttentionSummary(artifact),
1085
+ role_charters: buildRoleCharterSummary(artifact),
1084
1086
  },
1085
1087
  artifacts: {
1086
1088
  history_entries: artifact.summary?.history_entries || 0,
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Role Charter Well-Formedness — VISION.md:100–130 "Roles Are Open-Ended, Not Fixed".
3
+ *
4
+ * The vision fixes a precise, testable four-part invariant for *any* chartered
5
+ * role (VISION.md:123–128):
6
+ *
7
+ * 1. every role has a mandate
8
+ * 2. every role has authority boundaries
9
+ * 3. every role produces governed artifacts
10
+ * 4. every role participates in a structured workflow
11
+ *
12
+ * This module scores a role definition against those four invariants. It is
13
+ * **read-only** and **composes** existing primitives — it does not reimplement
14
+ * runtime-capability derivation (`runtime-capabilities.js`), gate-artifact
15
+ * resolution (`gate-evaluator.js`), or the manual-runtime / file-production
16
+ * logic that admission control (`admission-control.js`) already relies on.
17
+ *
18
+ * `agentxchain role` permits arbitrary roles; this module *governs* them.
19
+ */
20
+
21
+ import { getEffectiveGateArtifacts } from './gate-evaluator.js';
22
+ import {
23
+ getRoleRuntimeCapabilityContract,
24
+ canRoleParticipateInRequiredFileProduction,
25
+ canRoleSatisfyWorkflowArtifactOwnership,
26
+ } from './runtime-capabilities.js';
27
+
28
+ const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
29
+
30
+ // effective_write_path values that mean the authority binding is NOT coherent
31
+ // with the runtime — i.e. the role can never actually exercise its declared
32
+ // authority (e.g. review_only on a local_cli runtime that only writes directly).
33
+ const INCOHERENT_WRITE_PATHS = new Set([
34
+ 'none',
35
+ 'unknown',
36
+ 'invalid_review_only_binding',
37
+ 'invalid_authoritative_binding',
38
+ ]);
39
+
40
+ export const ROLE_CHARTER_INVARIANT_IDS = [
41
+ 'mandate',
42
+ 'authority_boundary',
43
+ 'produces_artifacts',
44
+ 'workflow_participation',
45
+ ];
46
+
47
+ /**
48
+ * Resolve the config surfaces this module needs, tolerating either a normalized
49
+ * config (roles carry `runtime_id`) or a raw agentxchain.json (roles carry
50
+ * `runtime`). Mirrors admission-control.js's `config || rawConfig` fallback so
51
+ * the same evaluator works from the CLI (normalized) and the governance report
52
+ * (raw export artifact config).
53
+ */
54
+ function resolveConfigSources(config, rawConfig) {
55
+ return {
56
+ roles: config?.roles || rawConfig?.roles || {},
57
+ runtimes: config?.runtimes || rawConfig?.runtimes || {},
58
+ routing: config?.routing || rawConfig?.routing || {},
59
+ gates: config?.gates || rawConfig?.gates || {},
60
+ // getEffectiveGateArtifacts only reads `config.workflow_kit`, so hand it a
61
+ // shim carrying whichever workflow_kit is available.
62
+ workflowKitConfig: { workflow_kit: config?.workflow_kit || rawConfig?.workflow_kit || null },
63
+ };
64
+ }
65
+
66
+ function getRoleRuntime(role, runtimes) {
67
+ const key = role?.runtime_id || role?.runtime;
68
+ return key ? runtimes?.[key] : undefined;
69
+ }
70
+
71
+ /** Phases whose routing references this role as entry_role or allowed_next_roles. */
72
+ function getRoutedPhases(routing, roleId) {
73
+ const phases = [];
74
+ for (const [phase, route] of Object.entries(routing || {})) {
75
+ const inEntry = route?.entry_role === roleId;
76
+ const inNext = Array.isArray(route?.allowed_next_roles) && route.allowed_next_roles.includes(roleId);
77
+ if (inEntry || inNext) phases.push(phase);
78
+ }
79
+ return phases;
80
+ }
81
+
82
+ // ── Invariant 1: Mandate (VISION.md:124) ────────────────────────────────────
83
+
84
+ function evaluateMandate(role, roleId) {
85
+ const mandate = typeof role?.mandate === 'string' ? role.mandate.trim() : '';
86
+ const satisfied = mandate.length > 0;
87
+ return {
88
+ id: 'mandate',
89
+ name: 'Has a mandate',
90
+ satisfied,
91
+ detail: satisfied
92
+ ? `Role "${roleId}" declares a non-empty mandate.`
93
+ : `Role "${roleId}" has no mandate text.`,
94
+ fix_hint: satisfied
95
+ ? null
96
+ : `Set a non-empty "mandate" for role "${roleId}" in agentxchain.json`,
97
+ };
98
+ }
99
+
100
+ // ── Invariant 2: Authority boundary (VISION.md:125) ─────────────────────────
101
+
102
+ function evaluateAuthorityBoundary(role, roleId, runtimes) {
103
+ const authority = role?.write_authority;
104
+ const runtimeKey = role?.runtime_id || role?.runtime || null;
105
+ const runtime = getRoleRuntime(role, runtimes);
106
+
107
+ if (!VALID_WRITE_AUTHORITIES.includes(authority)) {
108
+ return {
109
+ id: 'authority_boundary',
110
+ name: 'Has coherent authority boundaries',
111
+ satisfied: false,
112
+ detail: `Role "${roleId}" write_authority "${authority ?? '(unset)'}" is not one of ${VALID_WRITE_AUTHORITIES.join('/')}.`,
113
+ fix_hint: `Set role "${roleId}" write_authority to one of ${VALID_WRITE_AUTHORITIES.join('/')}`,
114
+ };
115
+ }
116
+
117
+ if (!runtime) {
118
+ return {
119
+ id: 'authority_boundary',
120
+ name: 'Has coherent authority boundaries',
121
+ satisfied: false,
122
+ detail: `Role "${roleId}" is bound to runtime "${runtimeKey ?? '(none)'}" which is not defined in runtimes.`,
123
+ fix_hint: `Bind role "${roleId}" to a defined runtime so its authority "${authority}" can resolve`,
124
+ };
125
+ }
126
+
127
+ const contract = getRoleRuntimeCapabilityContract(roleId, role, runtime);
128
+ const coherent = !INCOHERENT_WRITE_PATHS.has(contract.effective_write_path);
129
+
130
+ return {
131
+ id: 'authority_boundary',
132
+ name: 'Has coherent authority boundaries',
133
+ satisfied: coherent,
134
+ detail: coherent
135
+ ? `Role "${roleId}" write_authority "${authority}" on ${runtime.type} runtime resolves to write path "${contract.effective_write_path}".`
136
+ : `Role "${roleId}" write_authority "${authority}" on ${runtime.type} runtime resolves to "${contract.effective_write_path}".`,
137
+ fix_hint: coherent
138
+ ? null
139
+ : `Role "${roleId}" write_authority "${authority}" on ${runtime.type} runtime resolves to no usable write path; bind a runtime that supports it or change authority`,
140
+ };
141
+ }
142
+
143
+ // ── Invariant 3: Produces governed artifacts (VISION.md:126) ────────────────
144
+
145
+ function evaluateProducesArtifacts(role, roleId, sources) {
146
+ const { runtimes, routing, gates, workflowKitConfig } = sources;
147
+ const runtime = getRoleRuntime(role, runtimes);
148
+ const routedPhases = getRoutedPhases(routing, roleId);
149
+
150
+ // (a) The role can reach required-file production in at least one routed
151
+ // phase. canRoleParticipateInRequiredFileProduction already returns true for
152
+ // manual-runtime roles (humans satisfy required files outside the governed
153
+ // turn mechanism) — that is the manual-runtime carve-out admission control
154
+ // also relies on, so no special-casing is needed here.
155
+ let reachesFileProduction = false;
156
+ if (canRoleParticipateInRequiredFileProduction(role, runtime)) {
157
+ for (const phase of routedPhases) {
158
+ const gateId = routing[phase]?.exit_gate;
159
+ const gateDef = gateId ? gates?.[gateId] : null;
160
+ if (!gateDef) continue;
161
+ const artifacts = getEffectiveGateArtifacts(workflowKitConfig, gateDef, phase);
162
+ if (artifacts.some((a) => a.required)) {
163
+ reachesFileProduction = true;
164
+ break;
165
+ }
166
+ }
167
+ }
168
+
169
+ // (b) The role owns at least one workflow-kit artifact it can satisfy.
170
+ let ownsSatisfiableArtifact = false;
171
+ if (canRoleSatisfyWorkflowArtifactOwnership(role, runtime)) {
172
+ for (const phase of Object.keys(routing || {})) {
173
+ const gateId = routing[phase]?.exit_gate;
174
+ const gateDef = (gateId ? gates?.[gateId] : null) || {};
175
+ const artifacts = getEffectiveGateArtifacts(workflowKitConfig, gateDef, phase);
176
+ if (artifacts.some((a) => a.owned_by === roleId)) {
177
+ ownsSatisfiableArtifact = true;
178
+ break;
179
+ }
180
+ }
181
+ }
182
+
183
+ const satisfied = reachesFileProduction || ownsSatisfiableArtifact;
184
+ let detail;
185
+ if (reachesFileProduction && ownsSatisfiableArtifact) {
186
+ detail = `Role "${roleId}" reaches required-file production in a routed phase and owns a governed artifact.`;
187
+ } else if (reachesFileProduction) {
188
+ detail = `Role "${roleId}" reaches required-file production in a routed phase.`;
189
+ } else if (ownsSatisfiableArtifact) {
190
+ detail = `Role "${roleId}" owns a governed workflow-kit artifact it can satisfy.`;
191
+ } else {
192
+ detail = `Role "${roleId}" reaches no required-file-producing routed phase and owns no satisfiable artifact.`;
193
+ }
194
+
195
+ return {
196
+ id: 'produces_artifacts',
197
+ name: 'Produces governed artifacts',
198
+ satisfied,
199
+ detail,
200
+ fix_hint: satisfied
201
+ ? null
202
+ : `Role "${roleId}" produces no governed artifact: route it to a file-producing phase or give it owned_by on a workflow artifact`,
203
+ };
204
+ }
205
+
206
+ // ── Invariant 4: Participates in a structured workflow (VISION.md:127) ───────
207
+
208
+ function evaluateWorkflowParticipation(roleId, routing) {
209
+ const phases = getRoutedPhases(routing, roleId);
210
+ const satisfied = phases.length > 0;
211
+ return {
212
+ id: 'workflow_participation',
213
+ name: 'Participates in a structured workflow',
214
+ satisfied,
215
+ detail: satisfied
216
+ ? `Role "${roleId}" appears in routing for phase(s): ${phases.join(', ')}.`
217
+ : `Role "${roleId}" is not referenced in any phase routing.`,
218
+ fix_hint: satisfied
219
+ ? null
220
+ : `Role "${roleId}" is not in any phase routing; add it to entry_role or allowed_next_roles of a phase`,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Score a single role against the four VISION:123–128 charter invariants.
226
+ * Read-only. Each invariant is evaluated independently — one failing invariant
227
+ * never suppresses evaluation of the others.
228
+ *
229
+ * @returns {{ role_id, overall, invariants, missing, evidence_summary }}
230
+ */
231
+ export function evaluateRoleCharter(config, rawConfig, roleId) {
232
+ const sources = resolveConfigSources(config, rawConfig);
233
+ const role = sources.roles?.[roleId] || null;
234
+
235
+ const invariants = [
236
+ evaluateMandate(role, roleId),
237
+ evaluateAuthorityBoundary(role, roleId, sources.runtimes),
238
+ evaluateProducesArtifacts(role, roleId, sources),
239
+ evaluateWorkflowParticipation(roleId, sources.routing),
240
+ ];
241
+
242
+ const missing = invariants.filter((inv) => !inv.satisfied).map((inv) => inv.id);
243
+ const overall = missing.length === 0 ? 'well_formed' : 'incomplete';
244
+
245
+ return {
246
+ role_id: roleId,
247
+ overall,
248
+ invariants,
249
+ missing,
250
+ evidence_summary:
251
+ overall === 'well_formed'
252
+ ? `Role "${roleId}" is well-formed (4/4 charter invariants satisfied).`
253
+ : `Role "${roleId}" is incomplete — missing: ${missing.join(', ')}.`,
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Score every defined role against the four charter invariants. Roles are
259
+ * reported in stable order (sorted by id) so output and tests are deterministic.
260
+ *
261
+ * @returns {{ total, well_formed, incomplete, incomplete_role_ids, roles }}
262
+ */
263
+ export function evaluateAllRoleCharters(config, rawConfig) {
264
+ const sources = resolveConfigSources(config, rawConfig);
265
+ const roleIds = Object.keys(sources.roles || {}).sort();
266
+ const roles = roleIds.map((id) => evaluateRoleCharter(config, rawConfig, id));
267
+ const incomplete = roles.filter((r) => r.overall === 'incomplete');
268
+
269
+ return {
270
+ total: roles.length,
271
+ well_formed: roles.length - incomplete.length,
272
+ incomplete: incomplete.length,
273
+ incomplete_role_ids: incomplete.map((r) => r.role_id),
274
+ roles,
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Compact summary for the governance report (`report.role_charters`), mirroring
280
+ * buildHumanAttentionSummary / buildShipStatusSummary. The export artifact's
281
+ * `config` is the raw agentxchain.json, which evaluateAllRoleCharters tolerates.
282
+ */
283
+ export function buildRoleCharterSummary(artifact) {
284
+ if (!artifact) return null;
285
+ const config = artifact.config || null;
286
+ if (!config) return null;
287
+ const report = evaluateAllRoleCharters(config, config);
288
+ return {
289
+ total: report.total,
290
+ well_formed: report.well_formed,
291
+ incomplete: report.incomplete,
292
+ incomplete_role_ids: report.incomplete_role_ids,
293
+ };
294
+ }