@ludecker/aaac 1.1.0 → 1.1.1

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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/src/run-engine/advance-phase.mjs +152 -1
  3. package/src/run-engine/capability-evidence.mjs +460 -0
  4. package/src/run-engine/init-run.mjs +51 -1
  5. package/src/run-engine/lib.mjs +5 -0
  6. package/src/run-engine/verify-website-build.mjs +148 -0
  7. package/templates/cursor/aaac/capabilities/promotion-rules.json +64 -0
  8. package/templates/cursor/aaac/capabilities/registry.json +17 -15
  9. package/templates/cursor/aaac/dispatch.md +2 -2
  10. package/templates/cursor/aaac/enforcement.json +6 -3
  11. package/templates/cursor/aaac/governance/gates.json +3 -1
  12. package/templates/cursor/aaac/layers.md +3 -0
  13. package/templates/cursor/aaac/observability/telemetry.yaml +3 -0
  14. package/templates/cursor/aaac/run/schema.json +2 -0
  15. package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +152 -1
  16. package/templates/cursor/aaac/scripts/run-engine/capability-evidence.mjs +460 -0
  17. package/templates/cursor/aaac/scripts/run-engine/init-run.mjs +51 -1
  18. package/templates/cursor/aaac/scripts/run-engine/lib.mjs +5 -0
  19. package/templates/cursor/aaac/scripts/run-engine/verify-website-build.mjs +148 -0
  20. package/templates/cursor/aaac/state/capability-stats.json +5 -0
  21. package/templates/cursor/skills/shared/platform-release/SKILL.md +22 -19
  22. package/templates/cursor/skills/shared/platform-release/orchestrator/contract.yaml +27 -7
  23. package/templates/cursor/skills/shared/testing/SKILL.md +5 -0
  24. package/templates/cursor/skills/shared/verification/SKILL.md +1 -0
  25. package/templates/docs/agentic_architecture.md +236 -53
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ludecker/aaac",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Agentic Architecture as Code (AAAC) — installable Cursor agent framework",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import fs from "fs";
7
7
  import path from "path";
8
+ import { spawnSync } from "child_process";
9
+ import { fileURLToPath } from "url";
8
10
  import {
9
11
  loadRegistry,
10
12
  loadEnforcement,
@@ -18,6 +20,14 @@ import {
18
20
  saveActiveRun,
19
21
  } from "./lib.mjs";
20
22
  import { recordLog } from "./log.mjs";
23
+ import {
24
+ processRunEvidence,
25
+ evaluateCapabilityRuntimePolicy,
26
+ resolveCapabilitiesWithRuntime,
27
+ loadObjectMaturity,
28
+ } from "./capability-evidence.mjs";
29
+
30
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
31
 
22
32
  const runId = process.argv[2];
23
33
  const completedPhase = process.argv[3];
@@ -67,6 +77,46 @@ if (minAgents && launches < minAgents && !force) {
67
77
  process.exit(2);
68
78
  }
69
79
 
80
+ const verifyVerbs = enforcement.verify_verbs ?? ["create", "update", "fix"];
81
+ if (
82
+ completedPhase === "verify" &&
83
+ verifyVerbs.includes(manifest.verb) &&
84
+ !force
85
+ ) {
86
+ const verifyScript = path.join(__dirname, "verify-website-build.mjs");
87
+ const verifyRun = spawnSync("node", [verifyScript, "--run-id", runId], {
88
+ encoding: "utf8",
89
+ });
90
+ if (verifyRun.status !== 0) {
91
+ const detail =
92
+ verifyRun.stderr?.trim() ||
93
+ verifyRun.stdout?.trim() ||
94
+ "verify-website-build failed";
95
+ recordLog(manifest, {
96
+ event: "gate_fail",
97
+ phase: completedPhase,
98
+ phase_kind: manifest.phase_kind,
99
+ detail: `website verify failed: ${detail.slice(0, 500)}`,
100
+ level: "warn",
101
+ });
102
+ manifest.updated_at = isoNow();
103
+ writeJson(manifestPath, manifest);
104
+ console.error(
105
+ "Website verify failed (static assets + vite build). Fix errors, then re-run:\n" +
106
+ ` node .cursor/aaac/scripts/run-engine/verify-website-build.mjs --run-id ${runId}\n` +
107
+ detail,
108
+ );
109
+ process.exit(2);
110
+ }
111
+ recordLog(manifest, {
112
+ event: "verify_website_pass",
113
+ phase: completedPhase,
114
+ phase_kind: manifest.phase_kind,
115
+ detail: "static assets + vite build",
116
+ level: "info",
117
+ });
118
+ }
119
+
70
120
  const requiredArtifacts = enforcement.phase_artifacts?.[completedPhase] ?? [];
71
121
  for (const rel of requiredArtifacts) {
72
122
  const artifactPath = path.join(runDir(runId), rel);
@@ -119,7 +169,68 @@ recordLog(manifest, {
119
169
  level: "info",
120
170
  });
121
171
 
122
- const nextPhase = manifest.pending.shift() ?? null;
172
+ let nextPhase = manifest.pending.shift() ?? null;
173
+
174
+ if (nextPhase === "execute" && !force) {
175
+ const resolved =
176
+ manifest.capabilities_resolved &&
177
+ Object.keys(manifest.capabilities_resolved).length > 0
178
+ ? manifest.capabilities_resolved
179
+ : resolveCapabilitiesWithRuntime(manifest.object, manifest.verb);
180
+ const policy = evaluateCapabilityRuntimePolicy(resolved, {
181
+ object_maturity: loadObjectMaturity(manifest.object),
182
+ });
183
+ manifest.capability_runtime = policy;
184
+
185
+ const needsBlock =
186
+ policy.action === "block" ||
187
+ (policy.action === "require_approval" && !manifest.capability_runtime_approved);
188
+
189
+ if (needsBlock) {
190
+ manifest.pending.unshift(nextPhase);
191
+ nextPhase = null;
192
+ manifest.status = "blocked";
193
+ manifest.awaiting_approval = policy.action === "require_approval";
194
+ manifest.blocked_reason = policy.reasons.join("; ") || "capability runtime policy";
195
+ recordLog(manifest, {
196
+ event: "gate_fail",
197
+ phase: completedPhase,
198
+ phase_kind: manifest.phase_kind,
199
+ detail: `capability runtime ${policy.action}: ${manifest.blocked_reason}`,
200
+ level: "warn",
201
+ });
202
+ manifest.updated_at = isoNow();
203
+ writeJson(manifestPath, manifest);
204
+ saveActiveRun(manifest.conversation_id ?? null, {
205
+ run_id: runId,
206
+ conversation_id: manifest.conversation_id ?? null,
207
+ command: manifest.command,
208
+ phase: manifest.phase,
209
+ status: manifest.status,
210
+ task_launches_this_phase: 0,
211
+ edit_allowed: false,
212
+ started_at: manifest.created_at,
213
+ });
214
+ console.error(
215
+ `Capability runtime ${policy.action}: ${manifest.blocked_reason}. ` +
216
+ (policy.action === "require_approval"
217
+ ? "User must approve in chat; set capability_runtime_approved on Run and retry."
218
+ : "Cannot proceed to execute."),
219
+ );
220
+ process.exit(2);
221
+ }
222
+
223
+ if (policy.action === "warn") {
224
+ recordLog(manifest, {
225
+ event: "capability_runtime_warn",
226
+ phase: completedPhase,
227
+ phase_kind: manifest.phase_kind,
228
+ detail: policy.reasons.join("; "),
229
+ level: "warn",
230
+ });
231
+ }
232
+ }
233
+
123
234
  if (!nextPhase) {
124
235
  manifest.status = "completed";
125
236
  manifest.phase = "report";
@@ -131,6 +242,46 @@ if (!nextPhase) {
131
242
  detail: "all phases completed",
132
243
  level: "info",
133
244
  });
245
+
246
+ try {
247
+ const evidenceResult = processRunEvidence(runId, { manifest, skipManifestWrite: true });
248
+ if (evidenceResult.ok && !evidenceResult.skipped) {
249
+ manifest.capability_evidence_processed = true;
250
+ manifest.capability_evidence_outcomes = evidenceResult.outcomes;
251
+ if (
252
+ !manifest.capabilities_resolved ||
253
+ !Object.keys(manifest.capabilities_resolved).length
254
+ ) {
255
+ manifest.capabilities_resolved = evidenceResult.resolved;
256
+ }
257
+ recordLog(manifest, {
258
+ event: "evidence_aggregated",
259
+ phase: "report",
260
+ phase_kind: "work",
261
+ detail: `capabilities=${(evidenceResult.capabilities ?? []).join(",")}`,
262
+ level: "info",
263
+ });
264
+ for (const outcome of evidenceResult.outcomes ?? []) {
265
+ if (outcome.previous_state !== outcome.new_state) {
266
+ recordLog(manifest, {
267
+ event: "capability_promoted",
268
+ phase: "report",
269
+ phase_kind: "work",
270
+ detail: `${outcome.capability_id}:${outcome.previous_state}→${outcome.new_state}`,
271
+ level: "info",
272
+ });
273
+ }
274
+ }
275
+ }
276
+ } catch (err) {
277
+ recordLog(manifest, {
278
+ event: "evidence_aggregation_failed",
279
+ phase: "report",
280
+ phase_kind: "work",
281
+ detail: String(err.message ?? err).slice(0, 300),
282
+ level: "warn",
283
+ });
284
+ }
134
285
  } else {
135
286
  manifest.phase = nextPhase;
136
287
  manifest.phase_kind = phaseKind(nextPhase, registry);
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Capability evidence loop — resolve capabilities, extract run evidence,
4
+ * aggregate cross-run stats, evaluate deterministic lifecycle state.
5
+ */
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ import {
10
+ AAAC_ROOT,
11
+ STATE_ROOT,
12
+ readJson,
13
+ writeJson,
14
+ runDir,
15
+ loadRunManifest,
16
+ isoNow,
17
+ } from "./lib.mjs";
18
+
19
+ export const ONTOLOGY_PATH = path.join(AAAC_ROOT, "ontology.json");
20
+ export const CAPABILITY_REGISTRY_PATH = path.join(AAAC_ROOT, "capabilities", "registry.json");
21
+ export const PROMOTION_RULES_PATH = path.join(AAAC_ROOT, "capabilities", "promotion-rules.json");
22
+ export const CAPABILITY_STATS_PATH = path.join(STATE_ROOT, "capability-stats.json");
23
+
24
+ const STATE_ORDER = ["experimental", "validated", "trusted", "canonical", "deprecated"];
25
+
26
+ export function loadOntology() {
27
+ return readJson(ONTOLOGY_PATH, { object_capabilities: {}, object_capability_verbs: {} });
28
+ }
29
+
30
+ export function loadCapabilityRegistry() {
31
+ return readJson(CAPABILITY_REGISTRY_PATH, { capabilities: {} });
32
+ }
33
+
34
+ export function loadPromotionRules() {
35
+ return readJson(PROMOTION_RULES_PATH, {
36
+ default_state: "experimental",
37
+ thresholds: {},
38
+ demotion: {},
39
+ fitness_scoring: { pass: 100, warning: 75, fail: 0 },
40
+ });
41
+ }
42
+
43
+ export function loadCapabilityStats() {
44
+ return readJson(CAPABILITY_STATS_PATH, {
45
+ version: 1,
46
+ updated_at: null,
47
+ capabilities: {},
48
+ });
49
+ }
50
+
51
+ export function saveCapabilityStats(stats) {
52
+ stats.updated_at = isoNow();
53
+ writeJson(CAPABILITY_STATS_PATH, stats);
54
+ }
55
+
56
+ export function capabilityIdsForObject(object, verb, ontology = loadOntology()) {
57
+ if (!object) return [];
58
+ const base = ontology.object_capabilities?.[object] ?? [];
59
+ const verbExtras = ontology.object_capability_verbs?.[object]?.[verb] ?? [];
60
+ return [...new Set([...base, ...verbExtras])];
61
+ }
62
+
63
+ export function loadObjectMaturity(object) {
64
+ if (!object) return "evolving";
65
+ const ontology = loadOntology();
66
+ return ontology.object_maturity?.[object] ?? "evolving";
67
+ }
68
+
69
+ export function resolveCapabilitiesForObject(object, verb) {
70
+ const ontology = loadOntology();
71
+ const registry = loadCapabilityRegistry();
72
+ const ids = capabilityIdsForObject(object, verb, ontology);
73
+ const resolved = {};
74
+
75
+ for (const capabilityId of ids) {
76
+ const entry = registry.capabilities?.[capabilityId];
77
+ resolved[capabilityId] = {
78
+ providers: entry?.providers ?? [],
79
+ source: `object ${object}${verb ? ` verb ${verb}` : ""}`,
80
+ };
81
+ }
82
+
83
+ return resolved;
84
+ }
85
+
86
+ export function resolveCapabilitiesWithRuntime(object, verb) {
87
+ const resolved = resolveCapabilitiesForObject(object, verb);
88
+ const store = loadCapabilityStats();
89
+ const rules = loadPromotionRules();
90
+ const defaultState = rules.default_state ?? "experimental";
91
+
92
+ for (const [capabilityId, resolution] of Object.entries(resolved)) {
93
+ const entry = store.capabilities[capabilityId];
94
+ const state = entry?.state ?? defaultState;
95
+ const rates = entry ? computeRates(entry.stats) : null;
96
+ resolved[capabilityId] = {
97
+ ...resolution,
98
+ runtime: {
99
+ state,
100
+ invocations: entry?.stats?.invocations ?? 0,
101
+ success_rate: rates?.success_rate ?? null,
102
+ avg_fitness: rates?.avg_fitness ?? null,
103
+ },
104
+ };
105
+ }
106
+
107
+ return resolved;
108
+ }
109
+
110
+ export function evaluateCapabilityRuntimePolicy(resolved, context = {}) {
111
+ const rules = loadPromotionRules();
112
+ const runtime = rules.runtime ?? {};
113
+ const byState = runtime.by_state ?? {};
114
+ const evidenceTriggers = runtime.evidence_triggers ?? [];
115
+ const objectMaturity = context.object_maturity ?? "evolving";
116
+
117
+ const reasons = [];
118
+ let action = "allow";
119
+
120
+ const rank = { allow: 0, warn: 1, require_approval: 2, block: 3 };
121
+ const raise = (next, reason) => {
122
+ if (rank[next] > rank[action]) action = next;
123
+ reasons.push(reason);
124
+ };
125
+
126
+ for (const [capabilityId, resolution] of Object.entries(resolved)) {
127
+ const state = resolution.runtime?.state ?? rules.default_state ?? "experimental";
128
+ const statePolicy = byState[state] ?? {};
129
+
130
+ if (statePolicy.block_execute) {
131
+ raise("block", `${capabilityId}: state ${state} blocks execute`);
132
+ }
133
+
134
+ const requireOn = statePolicy.require_approval_on ?? [];
135
+ if (requireOn.includes(objectMaturity) || requireOn.includes("all")) {
136
+ raise(
137
+ "require_approval",
138
+ `${capabilityId}: ${state} on ${objectMaturity} object requires approval`,
139
+ );
140
+ }
141
+
142
+ if (statePolicy.warn) {
143
+ raise("warn", `${capabilityId}: ${state} — proceed with caution`);
144
+ }
145
+
146
+ const invocations = resolution.runtime?.invocations ?? 0;
147
+ const successRate = resolution.runtime?.success_rate;
148
+ const avgFitness = resolution.runtime?.avg_fitness;
149
+
150
+ for (const trigger of evidenceTriggers) {
151
+ if (invocations < (trigger.min_invocations ?? 0)) continue;
152
+
153
+ if (
154
+ successRate != null &&
155
+ trigger.min_success_rate_below != null &&
156
+ successRate < trigger.min_success_rate_below
157
+ ) {
158
+ raise(
159
+ trigger.action === "block" ? "block" : "require_approval",
160
+ `${capabilityId}: success_rate ${(successRate * 100).toFixed(1)}% below threshold`,
161
+ );
162
+ }
163
+
164
+ if (
165
+ avgFitness != null &&
166
+ trigger.min_avg_fitness_below != null &&
167
+ avgFitness < trigger.min_avg_fitness_below
168
+ ) {
169
+ raise(
170
+ trigger.action === "block" ? "block" : "require_approval",
171
+ `${capabilityId}: avg_fitness ${avgFitness.toFixed(1)} below threshold`,
172
+ );
173
+ }
174
+ }
175
+ }
176
+
177
+ return { action, reasons, object_maturity: objectMaturity };
178
+ }
179
+
180
+ function parseSimpleYaml(content) {
181
+ const result = {};
182
+ let currentKey = null;
183
+ let currentIndent = 0;
184
+
185
+ for (const line of content.split("\n")) {
186
+ if (!line.trim() || line.trim().startsWith("#")) continue;
187
+ const indent = line.search(/\S/);
188
+ const trimmed = line.trim();
189
+
190
+ if (trimmed.endsWith(":") && !trimmed.includes(": ")) {
191
+ currentKey = trimmed.slice(0, -1);
192
+ currentIndent = indent;
193
+ result[currentKey] = {};
194
+ continue;
195
+ }
196
+
197
+ const colon = trimmed.indexOf(": ");
198
+ if (colon === -1) continue;
199
+
200
+ const key = trimmed.slice(0, colon).trim();
201
+ const value = trimmed.slice(colon + 2).trim().replace(/^["']|["']$/g, "");
202
+
203
+ if (currentKey && indent > currentIndent) {
204
+ result[currentKey][key] = value;
205
+ } else {
206
+ result[key] = value;
207
+ currentKey = null;
208
+ }
209
+ }
210
+
211
+ return result;
212
+ }
213
+
214
+ function fitnessScoreFromArtifact(artifactsDir, scoring) {
215
+ const fitnessPath = path.join(artifactsDir, "fitness-functions.yaml");
216
+ if (!fs.existsSync(fitnessPath)) return null;
217
+
218
+ const content = fs.readFileSync(fitnessPath, "utf8");
219
+ const scores = [];
220
+ const scoreSection = content.match(/^score:\n([\s\S]*?)(?:\n\w|$)/m);
221
+ if (!scoreSection) return null;
222
+
223
+ for (const line of scoreSection[1].split("\n")) {
224
+ const match = line.match(/^\s+(\w+):\s+(pass|warning|fail)/);
225
+ if (match) {
226
+ scores.push(scoring[match[2]] ?? 0);
227
+ }
228
+ }
229
+
230
+ if (!scores.length) return null;
231
+ return scores.reduce((a, b) => a + b, 0) / scores.length;
232
+ }
233
+
234
+ export function extractRunEvidence(manifest, artifactsDir) {
235
+ const log = manifest.log ?? [];
236
+ const gateFails = log.filter((e) => e.event === "gate_fail").length;
237
+ const gatePasses = log.filter((e) => e.event === "gate_pass").length;
238
+ const rules = loadPromotionRules();
239
+ const fitnessScore = fitnessScoreFromArtifact(artifactsDir, rules.fitness_scoring ?? {});
240
+
241
+ const success = manifest.status === "completed";
242
+ const failure = manifest.status === "failed";
243
+
244
+ return {
245
+ success,
246
+ failure,
247
+ rollback: failure,
248
+ gate_failures: gateFails,
249
+ gate_passes: gatePasses,
250
+ fitness_score: fitnessScore,
251
+ run_id: manifest.run_id,
252
+ command: manifest.command,
253
+ object: manifest.object,
254
+ verb: manifest.verb,
255
+ };
256
+ }
257
+
258
+ function emptyCapabilityEntry(state = "experimental") {
259
+ return {
260
+ state,
261
+ stats: {
262
+ invocations: 0,
263
+ successes: 0,
264
+ failures: 0,
265
+ rollbacks: 0,
266
+ gate_passes: 0,
267
+ gate_failures: 0,
268
+ avg_fitness_score: null,
269
+ fitness_samples: 0,
270
+ },
271
+ gate_history: { passed: 0, failed: 0 },
272
+ history: { promoted: [] },
273
+ overrides: null,
274
+ last_run_id: null,
275
+ };
276
+ }
277
+
278
+ export function computeRates(stats) {
279
+ const invocations = stats.invocations || 0;
280
+ if (!invocations) {
281
+ return {
282
+ success_rate: 0,
283
+ rollback_rate: 0,
284
+ gate_failure_rate: 0,
285
+ avg_fitness: stats.avg_fitness_score,
286
+ };
287
+ }
288
+
289
+ return {
290
+ success_rate: stats.successes / invocations,
291
+ rollback_rate: stats.rollbacks / invocations,
292
+ gate_failure_rate: stats.gate_failures / (stats.gate_passes + stats.gate_failures || 1),
293
+ avg_fitness: stats.avg_fitness_score,
294
+ };
295
+ }
296
+
297
+ function meetsThreshold(stats, rates, threshold) {
298
+ if (threshold.min_invocations != null && stats.invocations < threshold.min_invocations) {
299
+ return false;
300
+ }
301
+ if (threshold.min_success_rate != null && rates.success_rate < threshold.min_success_rate) {
302
+ return false;
303
+ }
304
+ if (threshold.max_rollback_rate != null && rates.rollback_rate > threshold.max_rollback_rate) {
305
+ return false;
306
+ }
307
+ if (
308
+ threshold.max_gate_failure_rate != null &&
309
+ rates.gate_failure_rate > threshold.max_gate_failure_rate
310
+ ) {
311
+ return false;
312
+ }
313
+ return true;
314
+ }
315
+
316
+ export function evaluateState(entry, rules = loadPromotionRules()) {
317
+ if (entry.overrides?.state) return entry.overrides.state;
318
+
319
+ const rates = computeRates(entry.stats);
320
+ const thresholds = rules.thresholds ?? {};
321
+ const demotion = rules.demotion?.from_trusted;
322
+
323
+ if (
324
+ entry.state === "trusted" &&
325
+ demotion &&
326
+ entry.stats.invocations >= (demotion.min_invocations ?? 0) &&
327
+ rates.success_rate < (demotion.min_success_rate_below ?? 0)
328
+ ) {
329
+ return "validated";
330
+ }
331
+
332
+ const candidates = ["canonical", "trusted", "validated"];
333
+ for (const state of candidates) {
334
+ const threshold = thresholds[state];
335
+ if (!threshold) continue;
336
+ if (threshold.manual_approval && !entry.overrides?.approved_by) continue;
337
+ if (meetsThreshold(entry.stats, rates, threshold)) return state;
338
+ }
339
+
340
+ return rules.default_state ?? "experimental";
341
+ }
342
+
343
+ export function updateCapabilityFromEvidence(store, capabilityId, evidence) {
344
+ const entry = store.capabilities[capabilityId] ?? emptyCapabilityEntry();
345
+ const stats = entry.stats;
346
+
347
+ stats.invocations += 1;
348
+ if (evidence.success) stats.successes += 1;
349
+ if (evidence.failure) stats.failures += 1;
350
+ if (evidence.rollback) stats.rollbacks += 1;
351
+ stats.gate_passes += evidence.gate_passes;
352
+ stats.gate_failures += evidence.gate_failures;
353
+ entry.gate_history.passed += evidence.gate_passes;
354
+ entry.gate_history.failed += evidence.gate_failures;
355
+
356
+ if (evidence.fitness_score != null) {
357
+ const n = stats.fitness_samples;
358
+ stats.avg_fitness_score =
359
+ stats.avg_fitness_score == null
360
+ ? evidence.fitness_score
361
+ : (stats.avg_fitness_score * n + evidence.fitness_score) / (n + 1);
362
+ stats.fitness_samples = n + 1;
363
+ }
364
+
365
+ const previousState = entry.state;
366
+ const rules = loadPromotionRules();
367
+ entry.state = evaluateState(entry, rules);
368
+ if (entry.state !== previousState && !entry.history.promoted.includes(entry.state)) {
369
+ entry.history.promoted.push(entry.state);
370
+ }
371
+
372
+ entry.last_run_id = evidence.run_id;
373
+ store.capabilities[capabilityId] = entry;
374
+ return { previousState, newState: entry.state, entry };
375
+ }
376
+
377
+ export function processRunEvidence(runId, options = {}) {
378
+ const manifest = options.manifest ?? loadRunManifest(runId);
379
+ if (!manifest) {
380
+ return { ok: false, error: `Run not found: ${runId}` };
381
+ }
382
+
383
+ if (manifest.capability_evidence_processed && !options.force) {
384
+ return { ok: true, skipped: true, reason: "already_processed" };
385
+ }
386
+
387
+ const artifactsDir = path.join(runDir(runId), "artifacts");
388
+ const evidence = extractRunEvidence(manifest, artifactsDir);
389
+
390
+ const resolved =
391
+ manifest.capabilities_resolved &&
392
+ Object.keys(manifest.capabilities_resolved).length > 0
393
+ ? manifest.capabilities_resolved
394
+ : resolveCapabilitiesForObject(manifest.object, manifest.verb);
395
+
396
+ const capabilityIds = Object.keys(resolved);
397
+ if (!capabilityIds.length) {
398
+ return { ok: true, skipped: true, reason: "no_capabilities" };
399
+ }
400
+
401
+ const store = loadCapabilityStats();
402
+ const outcomes = [];
403
+
404
+ for (const capabilityId of capabilityIds) {
405
+ const result = updateCapabilityFromEvidence(store, capabilityId, evidence);
406
+ outcomes.push({
407
+ capability_id: capabilityId,
408
+ previous_state: result.previousState,
409
+ new_state: result.newState,
410
+ stats: result.entry.stats,
411
+ });
412
+ }
413
+
414
+ saveCapabilityStats(store);
415
+
416
+ const result = {
417
+ ok: true,
418
+ run_id: runId,
419
+ outcomes,
420
+ capabilities: capabilityIds,
421
+ resolved,
422
+ evidence,
423
+ };
424
+
425
+ if (!options.skipManifestWrite) {
426
+ manifest.capability_evidence_processed = true;
427
+ manifest.capability_evidence_outcomes = outcomes;
428
+ if (!manifest.capabilities_resolved || !Object.keys(manifest.capabilities_resolved).length) {
429
+ manifest.capabilities_resolved = resolved;
430
+ }
431
+ manifest.updated_at = isoNow();
432
+ writeJson(path.join(runDir(runId), "run.json"), manifest);
433
+ }
434
+
435
+ return result;
436
+ }
437
+
438
+ function main() {
439
+ const args = process.argv.slice(2);
440
+ const runIdIdx = args.indexOf("--run-id");
441
+ const runId = runIdIdx >= 0 ? args[runIdIdx + 1] : args[0];
442
+ const force = args.includes("--force");
443
+
444
+ if (!runId) {
445
+ console.error("Usage: capability-evidence.mjs --run-id <run_id> [--force]");
446
+ process.exit(1);
447
+ }
448
+
449
+ const result = processRunEvidence(runId, { force });
450
+ console.log(JSON.stringify(result));
451
+ process.exit(result.ok ? 0 : 1);
452
+ }
453
+
454
+ const isMain =
455
+ process.argv[1] &&
456
+ path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
457
+
458
+ if (isMain) {
459
+ main();
460
+ }