@mnemom/mnemom 0.7.1 → 0.8.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.
@@ -1,28 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import { spawnSync } from "node:child_process";
3
5
  import yaml from "js-yaml";
4
- import { configExists, loadConfig, requireAgent } from "../lib/config.js";
5
- import { getCard, updateCard, reverifyAgent, } from "../lib/api.js";
6
+ import { getAlignmentCard, putAlignmentCard, resolveAgentId, } from "../lib/api.js";
6
7
  import { requireAuth } from "../lib/auth.js";
7
8
  import { fmt } from "../lib/format.js";
8
9
  import { askYesNo, isInteractive } from "../lib/prompt.js";
9
- // Standard AAP values that do not require custom definitions
10
- const STANDARD_VALUES = new Set([
11
- "transparency",
12
- "honesty",
13
- "safety",
14
- "privacy",
15
- "fairness",
16
- "accountability",
17
- "beneficence",
18
- "non-maleficence",
19
- "autonomy",
20
- "justice",
21
- "reliability",
22
- "security",
23
- "human_oversight",
24
- "explainability",
25
- ]);
10
+ import { evaluatePolicy, } from "@mnemom/policy-engine";
26
11
  function detectFormat(filePath) {
27
12
  const ext = path.extname(filePath).toLowerCase();
28
13
  if (ext === ".yaml" || ext === ".yml")
@@ -37,37 +22,48 @@ export function parseCardFile(filePath) {
37
22
  if (!parsed || typeof parsed !== "object") {
38
23
  throw new Error("YAML did not produce a valid object");
39
24
  }
40
- return { format, parsed };
25
+ return { format, parsed, raw };
41
26
  }
42
- return { format, parsed: JSON.parse(raw) };
27
+ return { format, parsed: JSON.parse(raw), raw };
43
28
  }
44
- /** @deprecated Use validateCard instead */
45
- export const validateCardJson = validateCard;
46
- export function validateCard(raw) {
29
+ // Standard AAP values that do not require custom definitions
30
+ const STANDARD_VALUES = new Set([
31
+ "transparency", "honesty", "safety", "privacy", "fairness",
32
+ "accountability", "beneficence", "non-maleficence", "autonomy",
33
+ "justice", "reliability", "security", "human_oversight", "explainability",
34
+ ]);
35
+ /**
36
+ * Validate a unified alignment card object against ADR-008 schema.
37
+ * Sections: principal, values, conscience, integrity, autonomy,
38
+ * capabilities, enforcement, audit, extensions
39
+ */
40
+ export function validateUnifiedCard(card) {
47
41
  const checks = [];
48
- // Check 1: Valid JSON
49
- let card;
50
- try {
51
- card = JSON.parse(raw);
52
- checks.push({ name: "Valid JSON", passed: true, message: "Parsed successfully" });
53
- }
54
- catch (e) {
55
- const msg = e instanceof Error ? e.message : String(e);
56
- checks.push({ name: "Valid JSON", passed: false, message: `Parse error: ${msg}` });
57
- return checks; // Can't continue without valid JSON
58
- }
59
- // Check 2: Required blocks
60
- const requiredBlocks = ["principal", "values", "autonomy_envelope", "audit_commitment"];
61
- for (const block of requiredBlocks) {
62
- if (card[block] && typeof card[block] === "object") {
63
- checks.push({ name: `Block: ${block}`, passed: true, message: "Present" });
42
+ // Required sections
43
+ const requiredSections = ["principal", "values", "autonomy"];
44
+ for (const section of requiredSections) {
45
+ if (card[section] && typeof card[section] === "object") {
46
+ checks.push({ name: `Section: ${section}`, passed: true, message: "Present" });
64
47
  }
65
48
  else {
66
- checks.push({ name: `Block: ${block}`, passed: false, message: "Missing or invalid" });
49
+ checks.push({ name: `Section: ${section}`, passed: false, message: "Missing or invalid" });
67
50
  }
68
51
  }
69
- // Check 3: values.declared is non-empty array
70
- const declared = card.values?.declared;
52
+ // Optional sections validate shape if present
53
+ const optionalSections = ["conscience", "integrity", "capabilities", "enforcement", "audit", "extensions"];
54
+ for (const section of optionalSections) {
55
+ if (card[section] !== undefined) {
56
+ if (typeof card[section] === "object" && card[section] !== null) {
57
+ checks.push({ name: `Section: ${section}`, passed: true, message: "Present" });
58
+ }
59
+ else {
60
+ checks.push({ name: `Section: ${section}`, passed: false, message: "Must be an object" });
61
+ }
62
+ }
63
+ }
64
+ // values.declared is non-empty array
65
+ const values = card.values;
66
+ const declared = values?.declared;
71
67
  if (Array.isArray(declared) && declared.length > 0) {
72
68
  checks.push({
73
69
  name: "values.declared",
@@ -75,16 +71,16 @@ export function validateCard(raw) {
75
71
  message: `${declared.length} value(s) declared`,
76
72
  });
77
73
  }
78
- else {
74
+ else if (values) {
79
75
  checks.push({
80
76
  name: "values.declared",
81
77
  passed: false,
82
78
  message: "Must be a non-empty array",
83
79
  });
84
80
  }
85
- // Check 4: Custom values have definitions
81
+ // Custom values need definitions
86
82
  if (Array.isArray(declared)) {
87
- const definitions = card.values?.definitions || {};
83
+ const definitions = (values?.definitions ?? {});
88
84
  const customValues = declared.filter((v) => !STANDARD_VALUES.has(v));
89
85
  const missingDefs = customValues.filter((v) => !definitions[v]);
90
86
  if (missingDefs.length === 0) {
@@ -104,220 +100,90 @@ export function validateCard(raw) {
104
100
  });
105
101
  }
106
102
  }
107
- // Check 5: bounded_actions is non-empty array
108
- const bounded = card.autonomy_envelope?.bounded_actions;
103
+ // autonomy.bounded_actions is non-empty
104
+ const autonomy = card.autonomy;
105
+ const bounded = autonomy?.bounded_actions;
109
106
  if (Array.isArray(bounded) && bounded.length > 0) {
110
107
  checks.push({
111
- name: "bounded_actions",
108
+ name: "autonomy.bounded_actions",
112
109
  passed: true,
113
110
  message: `${bounded.length} bounded action(s)`,
114
111
  });
115
112
  }
116
- else {
113
+ else if (autonomy) {
117
114
  checks.push({
118
- name: "bounded_actions",
115
+ name: "autonomy.bounded_actions",
119
116
  passed: false,
120
117
  message: "Must be a non-empty array",
121
118
  });
122
119
  }
123
- // Check 6: escalation_triggers conditions are evaluable
124
- // Accepts: simple identifiers, tool_matches('...') calls, and logical expressions
125
- // (comparisons with ==, !=, >, <, and/or, quoted strings, numbers)
126
- const triggers = card.autonomy_envelope?.escalation_triggers;
127
- if (Array.isArray(triggers) && triggers.length > 0) {
128
- const conditionPattern = /^[\w\s\(\)\*\.'",=!><&|]+$/;
129
- const invalidTriggers = triggers.filter((t) => !t.condition || !conditionPattern.test(t.condition));
130
- if (invalidTriggers.length === 0) {
131
- checks.push({
132
- name: "escalation_triggers",
133
- passed: true,
134
- message: `${triggers.length} trigger(s), all valid`,
135
- });
136
- }
137
- else {
138
- checks.push({
139
- name: "escalation_triggers",
140
- passed: false,
141
- message: `${invalidTriggers.length} trigger(s) with invalid conditions`,
142
- });
143
- }
144
- }
145
- else if (Array.isArray(triggers) && triggers.length === 0) {
146
- checks.push({
147
- name: "escalation_triggers",
148
- passed: true,
149
- message: "No triggers defined",
150
- });
151
- }
152
- // Check 7: expires_at not already expired
153
- if (card.expires_at) {
154
- const expiresDate = new Date(card.expires_at);
155
- if (isNaN(expiresDate.getTime())) {
156
- checks.push({
157
- name: "expires_at",
158
- passed: false,
159
- message: "Invalid date format",
160
- });
161
- }
162
- else if (expiresDate.getTime() < Date.now()) {
163
- checks.push({
164
- name: "expires_at",
165
- passed: false,
166
- message: `Already expired: ${card.expires_at}`,
167
- });
168
- }
169
- else {
170
- checks.push({
171
- name: "expires_at",
172
- passed: true,
173
- message: `Valid until ${card.expires_at}`,
174
- });
175
- }
176
- }
177
- return checks;
178
- }
179
- /**
180
- * Validate a pre-parsed card object (used for YAML cards).
181
- * Re-serializes to JSON so the same checks run identically.
182
- */
183
- export function validateCardObject(card) {
184
- return validateCard(JSON.stringify(card));
185
- }
186
- // ============================================================================
187
- // Card display
188
- // ============================================================================
189
- function displayCard(cardResponse) {
190
- const card = cardResponse.card_json;
191
- console.log(fmt.header("Alignment Card"));
192
- console.log();
193
- // Header info
194
- console.log(fmt.label(" Card ID: ", cardResponse.card_id));
195
- if (card.version) {
196
- console.log(fmt.label(" Version: ", card.version));
197
- }
198
- if (card.issued_at) {
199
- console.log(fmt.label(" Issued: ", card.issued_at));
200
- }
201
- if (card.expires_at) {
202
- console.log(fmt.label(" Expires: ", card.expires_at));
203
- }
204
- // Principal
205
- if (card.principal) {
206
- console.log(fmt.section("Principal"));
207
- console.log();
208
- if (card.principal.name) {
209
- console.log(fmt.label(" Name: ", card.principal.name));
210
- }
211
- if (card.principal.type) {
212
- console.log(fmt.label(" Type: ", card.principal.type));
213
- }
214
- if (card.principal.organization) {
215
- console.log(fmt.label(" Organization:", ` ${card.principal.organization}`));
216
- }
217
- }
218
- // Values
219
- if (card.values) {
220
- console.log(fmt.section("Values"));
221
- console.log();
222
- if (Array.isArray(card.values.declared) && card.values.declared.length > 0) {
223
- const badges = card.values.declared.map((v) => {
224
- if (STANDARD_VALUES.has(v)) {
225
- return fmt.badge(v, "cyan");
226
- }
227
- return fmt.badge(v, "magenta");
228
- });
229
- console.log(` ${badges.join(" ")}`);
230
- // Show definitions for custom values
231
- const definitions = card.values.definitions || {};
232
- const customValues = card.values.declared.filter((v) => !STANDARD_VALUES.has(v));
233
- if (customValues.length > 0) {
234
- console.log();
235
- for (const v of customValues) {
236
- if (definitions[v]) {
237
- console.log(fmt.label(` ${v}:`, ` ${definitions[v]}`));
238
- }
239
- }
120
+ // capabilities shape validation (if present)
121
+ const capabilities = card.capabilities;
122
+ if (capabilities && typeof capabilities === "object") {
123
+ for (const [name, mapping] of Object.entries(capabilities)) {
124
+ const m = mapping;
125
+ if (!Array.isArray(m?.tools)) {
126
+ checks.push({
127
+ name: `capabilities.${name}.tools`,
128
+ passed: false,
129
+ message: "Must be an array",
130
+ });
240
131
  }
241
- }
242
- }
243
- // Autonomy envelope
244
- if (card.autonomy_envelope) {
245
- console.log(fmt.section("Autonomy Envelope"));
246
- console.log();
247
- const bounded = card.autonomy_envelope.bounded_actions;
248
- if (Array.isArray(bounded) && bounded.length > 0) {
249
- console.log(" Bounded actions:");
250
- for (const action of bounded) {
251
- console.log(` ${fmt.success(action)}`);
252
- }
253
- }
254
- const forbidden = card.autonomy_envelope.forbidden_actions;
255
- if (Array.isArray(forbidden) && forbidden.length > 0) {
256
- console.log(" Forbidden actions:");
257
- for (const action of forbidden) {
258
- console.log(` ${fmt.error(action)}`);
259
- }
260
- }
261
- const triggers = card.autonomy_envelope.escalation_triggers;
262
- if (Array.isArray(triggers) && triggers.length > 0) {
263
- console.log(" Escalation triggers:");
264
- for (const trigger of triggers) {
265
- const action = trigger.action ? ` -> ${trigger.action}` : "";
266
- console.log(` ${fmt.warn(`${trigger.condition}${action}`)}`);
132
+ if (!Array.isArray(m?.required_actions)) {
133
+ checks.push({
134
+ name: `capabilities.${name}.required_actions`,
135
+ passed: false,
136
+ message: "Must be an array",
137
+ });
267
138
  }
268
139
  }
269
140
  }
270
- // Audit commitment
271
- if (card.audit_commitment) {
272
- console.log(fmt.section("Audit Commitment"));
273
- console.log();
274
- if (card.audit_commitment.log_level) {
275
- console.log(fmt.label(" Log level: ", card.audit_commitment.log_level));
276
- }
277
- if (card.audit_commitment.retention_days != null) {
278
- console.log(fmt.label(" Retention: ", `${card.audit_commitment.retention_days} days`));
279
- }
280
- if (card.audit_commitment.access_policy) {
281
- console.log(fmt.label(" Access policy: ", card.audit_commitment.access_policy));
141
+ // enforcement.forbidden_tools shape validation (if present)
142
+ const enforcement = card.enforcement;
143
+ const forbidden = enforcement?.forbidden_tools;
144
+ if (Array.isArray(forbidden)) {
145
+ for (let i = 0; i < forbidden.length; i++) {
146
+ const rule = forbidden[i];
147
+ if (!rule?.pattern || !rule?.reason) {
148
+ checks.push({
149
+ name: `enforcement.forbidden_tools[${i}]`,
150
+ passed: false,
151
+ message: "Must have 'pattern' and 'reason' fields",
152
+ });
153
+ }
282
154
  }
283
155
  }
284
- // Extensions
285
- if (card.extensions && Object.keys(card.extensions).length > 0) {
286
- console.log(fmt.section("Extensions"));
287
- console.log();
288
- console.log(fmt.json(card.extensions));
289
- }
290
- console.log();
156
+ return checks;
291
157
  }
158
+ /** @deprecated Use validateUnifiedCard instead */
159
+ export const validateCardJson = (raw) => validateUnifiedCard(JSON.parse(raw));
292
160
  // ============================================================================
293
161
  // Subcommands
294
162
  // ============================================================================
295
163
  export async function cardShowCommand(agentName) {
296
- if (!configExists()) {
297
- console.log("\n" + fmt.error("smoltbot is not initialized") + "\n");
298
- console.log("Run `smoltbot init` to get started.\n");
299
- process.exit(1);
300
- }
301
- const config = loadConfig();
302
- if (!config) {
303
- console.log("\n" + fmt.error("Failed to load configuration") + "\n");
304
- process.exit(1);
305
- }
306
- const agent = await requireAgent(agentName);
307
- if (!agent) {
308
- console.log("\n" + fmt.error(`Agent not found${agentName ? `: ${agentName}` : ""}`) + "\n");
309
- process.exit(1);
310
- }
164
+ const agentId = await resolveAgentId(agentName);
311
165
  console.log("\nFetching alignment card...\n");
312
166
  try {
313
- const cardResponse = await getCard(agent.agentId);
314
- if (!cardResponse) {
315
- console.log(fmt.warn("No custom card -- using default"));
316
- console.log("\nPublish a custom card with:\n");
317
- console.log(" smoltbot card publish <file.json>\n");
167
+ const { body, contentType } = await getAlignmentCard(agentId);
168
+ if (!body) {
169
+ console.log(fmt.warn("No alignment card found"));
170
+ console.log("\nPublish one with:\n");
171
+ console.log(" mnemom card publish <file.yaml>\n");
318
172
  return;
319
173
  }
320
- displayCard(cardResponse);
174
+ // If the API returned YAML, print directly; otherwise convert
175
+ if (contentType.includes("yaml") || contentType.includes("text/yaml")) {
176
+ console.log(fmt.header("Alignment Card"));
177
+ console.log();
178
+ console.log(body);
179
+ }
180
+ else {
181
+ // JSON response — convert to YAML for display
182
+ const parsed = JSON.parse(body);
183
+ console.log(fmt.header("Alignment Card"));
184
+ console.log();
185
+ console.log(yaml.dump(parsed, { lineWidth: 120, noRefs: true }));
186
+ }
321
187
  }
322
188
  catch (error) {
323
189
  const message = error instanceof Error ? error.message : String(error);
@@ -326,21 +192,7 @@ export async function cardShowCommand(agentName) {
326
192
  }
327
193
  }
328
194
  export async function cardPublishCommand(file, agentName) {
329
- if (!configExists()) {
330
- console.log("\n" + fmt.error("smoltbot is not initialized") + "\n");
331
- console.log("Run `smoltbot init` to get started.\n");
332
- process.exit(1);
333
- }
334
- const config = loadConfig();
335
- if (!config) {
336
- console.log("\n" + fmt.error("Failed to load configuration") + "\n");
337
- process.exit(1);
338
- }
339
- const agent = await requireAgent(agentName);
340
- if (!agent) {
341
- console.log("\n" + fmt.error(`Agent not found${agentName ? `: ${agentName}` : ""}`) + "\n");
342
- process.exit(1);
343
- }
195
+ const agentId = await resolveAgentId(agentName);
344
196
  // Resolve file path
345
197
  const filePath = path.resolve(file);
346
198
  if (!fs.existsSync(filePath)) {
@@ -349,11 +201,8 @@ export async function cardPublishCommand(file, agentName) {
349
201
  }
350
202
  // Parse file (JSON or YAML)
351
203
  let parsed;
352
- let format;
353
204
  try {
354
- const result = parseCardFile(filePath);
355
- parsed = result.parsed;
356
- format = result.format;
205
+ parsed = parseCardFile(filePath);
357
206
  }
358
207
  catch (e) {
359
208
  const msg = e instanceof Error ? e.message : String(e);
@@ -361,10 +210,10 @@ export async function cardPublishCommand(file, agentName) {
361
210
  process.exit(1);
362
211
  }
363
212
  // Validate locally
364
- const checks = validateCardObject(parsed);
213
+ const checks = validateUnifiedCard(parsed.parsed);
365
214
  const allPassed = checks.every((c) => c.passed);
366
215
  console.log(fmt.header("Card Validation"));
367
- console.log(fmt.label(" Format:", ` ${format.toUpperCase()}`));
216
+ console.log(fmt.label(" Format:", ` ${parsed.format.toUpperCase()}`));
368
217
  console.log();
369
218
  for (const check of checks) {
370
219
  if (check.passed) {
@@ -383,27 +232,24 @@ export async function cardPublishCommand(file, agentName) {
383
232
  await requireAuth();
384
233
  // Confirm with user
385
234
  if (isInteractive()) {
386
- const confirm = await askYesNo(`Publish this card for agent ${agent.agentId}?`, false);
235
+ const confirm = await askYesNo(`Publish this alignment card for agent ${agentId}?`, false);
387
236
  if (!confirm) {
388
237
  console.log("\nPublish cancelled.\n");
389
238
  return;
390
239
  }
391
240
  }
392
- // Publish (always sends JSON to API regardless of source format)
241
+ // Publish send in source format
393
242
  try {
394
- console.log("\nPublishing card...");
395
- const result = await updateCard(agent.agentId, parsed);
396
- console.log(fmt.success(`Card published successfully!`));
397
- console.log(fmt.label(" Card ID:", ` ${result.card_id}`) + "\n");
398
- // Trigger re-verification
399
- try {
400
- console.log("Triggering re-verification...");
401
- const reverifyResult = await reverifyAgent(agent.agentId);
402
- console.log(fmt.success(`Re-verification started (${reverifyResult.reverified} traces queued)`) + "\n");
403
- }
404
- catch {
405
- console.log(fmt.warn("Could not trigger re-verification (non-critical)") + "\n");
243
+ console.log("\nPublishing alignment card...");
244
+ const contentType = parsed.format === "yaml" ? "text/yaml" : "application/json";
245
+ const body = parsed.format === "yaml" ? parsed.raw : JSON.stringify(parsed.parsed);
246
+ const result = await putAlignmentCard(agentId, body, contentType);
247
+ console.log(fmt.success("Alignment card published!"));
248
+ console.log(fmt.label(" Card ID:", ` ${result.card_id}`));
249
+ if (result.composed) {
250
+ console.log(fmt.success("Canonical card recomposed"));
406
251
  }
252
+ console.log();
407
253
  }
408
254
  catch (error) {
409
255
  const message = error instanceof Error ? error.message : String(error);
@@ -420,11 +266,8 @@ export async function cardValidateCommand(file) {
420
266
  }
421
267
  // Parse file (JSON or YAML)
422
268
  let parsed;
423
- let format;
424
269
  try {
425
- const result = parseCardFile(filePath);
426
- parsed = result.parsed;
427
- format = result.format;
270
+ parsed = parseCardFile(filePath);
428
271
  }
429
272
  catch (e) {
430
273
  const msg = e instanceof Error ? e.message : String(e);
@@ -432,14 +275,14 @@ export async function cardValidateCommand(file) {
432
275
  process.exit(1);
433
276
  }
434
277
  // Validate the parsed object
435
- const checks = validateCardObject(parsed);
278
+ const checks = validateUnifiedCard(parsed.parsed);
436
279
  const allPassed = checks.every((c) => c.passed);
437
280
  const passCount = checks.filter((c) => c.passed).length;
438
281
  const failCount = checks.filter((c) => !c.passed).length;
439
282
  console.log(fmt.header("Card Validation Report"));
440
283
  console.log();
441
284
  console.log(fmt.label(" File:", ` ${filePath}`));
442
- console.log(fmt.label(" Format:", ` ${format.toUpperCase()}`));
285
+ console.log(fmt.label(" Format:", ` ${parsed.format.toUpperCase()}`));
443
286
  console.log();
444
287
  for (const check of checks) {
445
288
  if (check.passed) {
@@ -458,3 +301,213 @@ export async function cardValidateCommand(file) {
458
301
  process.exit(1);
459
302
  }
460
303
  }
304
+ export async function cardEditCommand(agentName) {
305
+ const agentId = await resolveAgentId(agentName);
306
+ await requireAuth();
307
+ // Fetch current card as YAML
308
+ console.log("\nFetching current alignment card...\n");
309
+ const { body: original } = await getAlignmentCard(agentId);
310
+ if (!original) {
311
+ console.log(fmt.warn("No alignment card found. Creating a template..."));
312
+ }
313
+ const cardYaml = original || yaml.dump({
314
+ principal: { name: "", type: "ai_agent", organization: "" },
315
+ values: { declared: ["transparency", "safety", "honesty"] },
316
+ autonomy: { bounded_actions: [], forbidden_actions: [], escalation_triggers: [] },
317
+ }, { lineWidth: 120, noRefs: true });
318
+ // Write to temp file
319
+ const tmpDir = os.tmpdir();
320
+ const tmpFile = path.join(tmpDir, `mnemom-card-${agentId}.yaml`);
321
+ fs.writeFileSync(tmpFile, cardYaml);
322
+ // Open in editor
323
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
324
+ console.log(`Opening ${editor}...`);
325
+ const result = spawnSync(editor, [tmpFile], { stdio: "inherit" });
326
+ if (result.status !== 0) {
327
+ console.log("\n" + fmt.error("Editor exited with an error") + "\n");
328
+ try {
329
+ fs.unlinkSync(tmpFile);
330
+ }
331
+ catch { /* ignore */ }
332
+ process.exit(1);
333
+ }
334
+ // Read back and compare
335
+ const edited = fs.readFileSync(tmpFile, "utf-8");
336
+ try {
337
+ fs.unlinkSync(tmpFile);
338
+ }
339
+ catch { /* ignore */ }
340
+ if (edited === cardYaml) {
341
+ console.log("\nNo changes made.\n");
342
+ return;
343
+ }
344
+ // Validate
345
+ let parsed;
346
+ try {
347
+ parsed = yaml.load(edited);
348
+ if (!parsed || typeof parsed !== "object")
349
+ throw new Error("Invalid YAML");
350
+ }
351
+ catch (e) {
352
+ const msg = e instanceof Error ? e.message : String(e);
353
+ console.log("\n" + fmt.error(`Invalid YAML: ${msg}`) + "\n");
354
+ process.exit(1);
355
+ }
356
+ const checks = validateUnifiedCard(parsed);
357
+ const allPassed = checks.every((c) => c.passed);
358
+ if (!allPassed) {
359
+ console.log(fmt.header("Validation Errors"));
360
+ console.log();
361
+ for (const check of checks.filter((c) => !c.passed)) {
362
+ console.log(fmt.error(`${check.name}: ${check.message}`));
363
+ }
364
+ console.log();
365
+ console.log(fmt.error("Validation failed. Card not published.") + "\n");
366
+ process.exit(1);
367
+ }
368
+ // Confirm and publish
369
+ if (isInteractive()) {
370
+ const confirm = await askYesNo("Publish updated alignment card?", true);
371
+ if (!confirm) {
372
+ console.log("\nPublish cancelled.\n");
373
+ return;
374
+ }
375
+ }
376
+ try {
377
+ console.log("\nPublishing alignment card...");
378
+ const putResult = await putAlignmentCard(agentId, edited, "text/yaml");
379
+ console.log(fmt.success("Alignment card published!"));
380
+ console.log(fmt.label(" Card ID:", ` ${putResult.card_id}`) + "\n");
381
+ }
382
+ catch (error) {
383
+ const message = error instanceof Error ? error.message : String(error);
384
+ console.log("\n" + fmt.error(`Failed to publish card: ${message}`) + "\n");
385
+ process.exit(1);
386
+ }
387
+ }
388
+ /**
389
+ * mnemom card evaluate <card-file> --tools <tools> -- local CI/CD evaluation
390
+ *
391
+ * Runs entirely locally using the embedded policy engine. No API key needed.
392
+ * The card IS the policy source -- capabilities and enforcement sections
393
+ * are extracted by @mnemom/policy-engine 0.3.0's evaluatePolicy().
394
+ */
395
+ export async function cardEvaluateCommand(file, options) {
396
+ // 1. Read + validate card file
397
+ const cardPath = path.resolve(file);
398
+ if (!fs.existsSync(cardPath)) {
399
+ console.log("\n" + fmt.error(`Card file not found: ${cardPath}`) + "\n");
400
+ process.exit(1);
401
+ }
402
+ let parsed;
403
+ try {
404
+ parsed = parseCardFile(cardPath);
405
+ }
406
+ catch (e) {
407
+ const msg = e instanceof Error ? e.message : String(e);
408
+ console.log("\n" + fmt.error(`Could not parse card file: ${msg}`) + "\n");
409
+ process.exit(1);
410
+ }
411
+ const checks = validateUnifiedCard(parsed.parsed);
412
+ const allPassed = checks.every((c) => c.passed);
413
+ if (!allPassed) {
414
+ console.log(fmt.error("Card validation failed:"));
415
+ for (const check of checks.filter((c) => !c.passed)) {
416
+ console.log(fmt.error(` ${check.name}: ${check.message}`));
417
+ }
418
+ console.log();
419
+ process.exit(1);
420
+ }
421
+ // 2. Parse tool list from --tools or --tool-manifest
422
+ let tools = [];
423
+ if (options.tools) {
424
+ tools = options.tools.split(",").map((t) => ({ name: t.trim() })).filter((t) => t.name);
425
+ }
426
+ else if (options.toolManifest) {
427
+ const manifestPath = path.resolve(options.toolManifest);
428
+ if (!fs.existsSync(manifestPath)) {
429
+ console.log("\n" + fmt.error(`Tool manifest file not found: ${manifestPath}`) + "\n");
430
+ process.exit(1);
431
+ }
432
+ try {
433
+ const manifestRaw = fs.readFileSync(manifestPath, "utf-8");
434
+ const manifest = JSON.parse(manifestRaw);
435
+ if (Array.isArray(manifest)) {
436
+ tools = manifest.map((t) => typeof t === "string" ? { name: t } : { name: t.name });
437
+ }
438
+ }
439
+ catch (e) {
440
+ const msg = e instanceof Error ? e.message : String(e);
441
+ console.log("\n" + fmt.error(`Could not read tool manifest: ${msg}`) + "\n");
442
+ process.exit(1);
443
+ }
444
+ }
445
+ if (tools.length === 0) {
446
+ console.log("\n" + fmt.error("No tools specified. Use --tools or --tool-manifest") + "\n");
447
+ process.exit(1);
448
+ }
449
+ // 3. Run evaluation -- card IS the policy source
450
+ const result = evaluatePolicy({
451
+ context: "cicd",
452
+ card: parsed.parsed,
453
+ tools,
454
+ });
455
+ // 4. Display results
456
+ console.log(fmt.header("Card Policy Evaluation"));
457
+ console.log();
458
+ const principal = parsed.parsed.principal;
459
+ console.log(fmt.label(" Card:", ` ${principal?.name ?? path.basename(cardPath)}`));
460
+ console.log(fmt.label(" Context:", " cicd"));
461
+ console.log(fmt.label(" Tools:", ` ${tools.length} (${tools.map((t) => t.name).join(", ")})`));
462
+ console.log();
463
+ // Verdict
464
+ if (result.verdict === "pass") {
465
+ console.log(fmt.success("PASS -- all tools comply with card policy"));
466
+ }
467
+ else if (result.verdict === "warn") {
468
+ console.log(fmt.warn("WARN -- policy warnings detected"));
469
+ }
470
+ else {
471
+ console.log(fmt.error("FAIL -- policy violations detected"));
472
+ }
473
+ console.log();
474
+ // Violations
475
+ if (result.violations.length > 0) {
476
+ console.log(fmt.section("Violations"));
477
+ console.log();
478
+ for (const v of result.violations) {
479
+ console.log(fmt.error(` ${v.tool} [${v.severity}] -- ${v.type}`));
480
+ console.log(` ${v.reason}`);
481
+ }
482
+ }
483
+ // Warnings
484
+ if (result.warnings.length > 0) {
485
+ console.log(fmt.section("Warnings"));
486
+ console.log();
487
+ for (const w of result.warnings) {
488
+ console.log(fmt.warn(` ${w.tool} -- ${w.type}`));
489
+ console.log(` ${w.reason}`);
490
+ }
491
+ }
492
+ // Coverage
493
+ const cov = result.coverage;
494
+ console.log(fmt.section("Coverage"));
495
+ console.log();
496
+ console.log(fmt.label(" Card actions:", ` ${cov.total_card_actions}`));
497
+ console.log(fmt.label(" Mapped: ", ` ${cov.mapped_card_actions.length}`));
498
+ console.log(fmt.label(" Unmapped: ", ` ${cov.unmapped_card_actions.length}`));
499
+ console.log(fmt.label(" Coverage: ", ` ${cov.coverage_pct.toFixed(1)}%`));
500
+ if (cov.unmapped_card_actions.length > 0) {
501
+ console.log(fmt.label(" Unmapped list:", ` ${cov.unmapped_card_actions.join(", ")}`));
502
+ }
503
+ console.log();
504
+ console.log(fmt.label(" Duration:", ` ${result.duration_ms}ms`));
505
+ console.log();
506
+ // 5. Exit code
507
+ if (result.verdict === "fail") {
508
+ process.exit(1);
509
+ }
510
+ if (options.strict && result.verdict === "warn") {
511
+ process.exit(1);
512
+ }
513
+ }