@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.
- package/package.json +1 -1
- package/src/run-engine/advance-phase.mjs +152 -1
- package/src/run-engine/capability-evidence.mjs +460 -0
- package/src/run-engine/init-run.mjs +51 -1
- package/src/run-engine/lib.mjs +5 -0
- package/src/run-engine/verify-website-build.mjs +148 -0
- package/templates/cursor/aaac/capabilities/promotion-rules.json +64 -0
- package/templates/cursor/aaac/capabilities/registry.json +17 -15
- package/templates/cursor/aaac/dispatch.md +2 -2
- package/templates/cursor/aaac/enforcement.json +6 -3
- package/templates/cursor/aaac/governance/gates.json +3 -1
- package/templates/cursor/aaac/layers.md +3 -0
- package/templates/cursor/aaac/observability/telemetry.yaml +3 -0
- package/templates/cursor/aaac/run/schema.json +2 -0
- package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +152 -1
- package/templates/cursor/aaac/scripts/run-engine/capability-evidence.mjs +460 -0
- package/templates/cursor/aaac/scripts/run-engine/init-run.mjs +51 -1
- package/templates/cursor/aaac/scripts/run-engine/lib.mjs +5 -0
- package/templates/cursor/aaac/scripts/run-engine/verify-website-build.mjs +148 -0
- package/templates/cursor/aaac/state/capability-stats.json +5 -0
- package/templates/cursor/skills/shared/platform-release/SKILL.md +22 -19
- package/templates/cursor/skills/shared/platform-release/orchestrator/contract.yaml +27 -7
- package/templates/cursor/skills/shared/testing/SKILL.md +5 -0
- package/templates/cursor/skills/shared/verification/SKILL.md +1 -0
- package/templates/docs/agentic_architecture.md +236 -53
|
@@ -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
|
+
}
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
promptFromHook,
|
|
16
16
|
} from "./lib.mjs";
|
|
17
17
|
import { recordLog, recordDecision } from "./log.mjs";
|
|
18
|
+
import {
|
|
19
|
+
resolveCapabilitiesWithRuntime,
|
|
20
|
+
evaluateCapabilityRuntimePolicy,
|
|
21
|
+
loadObjectMaturity,
|
|
22
|
+
} from "./capability-evidence.mjs";
|
|
18
23
|
|
|
19
24
|
async function readStdin() {
|
|
20
25
|
return new Promise((resolve) => {
|
|
@@ -64,6 +69,14 @@ const runId = `run_${date}_${slugify(parsed.command + (parsed.domain ? `-${parse
|
|
|
64
69
|
const entry = registry.commands[parsed.command];
|
|
65
70
|
fs.mkdirSync(runDir(runId), { recursive: true });
|
|
66
71
|
|
|
72
|
+
const runObject = entry.object ?? null;
|
|
73
|
+
const runVerb = entry.verb ?? parsed.command.split("-")[0];
|
|
74
|
+
const objectMaturity = loadObjectMaturity(runObject);
|
|
75
|
+
const capabilitiesResolved = resolveCapabilitiesWithRuntime(runObject, runVerb);
|
|
76
|
+
const capabilityRuntimePolicy = evaluateCapabilityRuntimePolicy(capabilitiesResolved, {
|
|
77
|
+
object_maturity: objectMaturity,
|
|
78
|
+
});
|
|
79
|
+
|
|
67
80
|
const manifest = {
|
|
68
81
|
run_id: runId,
|
|
69
82
|
conversation_id: conversationId,
|
|
@@ -84,7 +97,9 @@ const manifest = {
|
|
|
84
97
|
artifacts: {},
|
|
85
98
|
checkpoints: [],
|
|
86
99
|
log: [],
|
|
87
|
-
capabilities_resolved:
|
|
100
|
+
capabilities_resolved: capabilitiesResolved,
|
|
101
|
+
capability_runtime: capabilityRuntimePolicy,
|
|
102
|
+
capability_runtime_approved: false,
|
|
88
103
|
confidence: { architecture: null, requirements: null, scope: null },
|
|
89
104
|
gates: { stack: entry.gate_stack ?? null, results: {} },
|
|
90
105
|
swarm: { task_launches_this_phase: 0, phase: pending[0] },
|
|
@@ -131,6 +146,41 @@ recordDecision(manifest, {
|
|
|
131
146
|
evidence: parsed.raw,
|
|
132
147
|
});
|
|
133
148
|
|
|
149
|
+
for (const [capabilityId, resolution] of Object.entries(manifest.capabilities_resolved)) {
|
|
150
|
+
recordLog(manifest, {
|
|
151
|
+
event: "capability_resolved",
|
|
152
|
+
phase: "dispatch",
|
|
153
|
+
phase_kind: "work",
|
|
154
|
+
detail: `${capabilityId}:${(resolution.providers ?? []).map((p) => p.id).join(",")} state=${resolution.runtime?.state ?? "experimental"}`,
|
|
155
|
+
level: "debug",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
recordLog(manifest, {
|
|
160
|
+
event: "capability_runtime_evaluated",
|
|
161
|
+
phase: "dispatch",
|
|
162
|
+
phase_kind: "work",
|
|
163
|
+
detail: `action=${capabilityRuntimePolicy.action} maturity=${objectMaturity}`,
|
|
164
|
+
level: "info",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (capabilityRuntimePolicy.action === "warn") {
|
|
168
|
+
recordLog(manifest, {
|
|
169
|
+
event: "capability_runtime_warn",
|
|
170
|
+
phase: "dispatch",
|
|
171
|
+
phase_kind: "work",
|
|
172
|
+
detail: capabilityRuntimePolicy.reasons.join("; "),
|
|
173
|
+
level: "warn",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
recordDecision(manifest, {
|
|
178
|
+
phase: "dispatch",
|
|
179
|
+
decision: "capability_runtime",
|
|
180
|
+
reason: capabilityRuntimePolicy.action,
|
|
181
|
+
evidence: capabilityRuntimePolicy.reasons.join("; ") || "allow",
|
|
182
|
+
});
|
|
183
|
+
|
|
134
184
|
recordLog(manifest, {
|
|
135
185
|
event: "phase_start",
|
|
136
186
|
phase: pending[0],
|
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
export const CURSOR_ROOT = path.resolve(__dirname, "../../..");
|
|
8
|
+
export const REPO_ROOT = path.resolve(CURSOR_ROOT, "..");
|
|
8
9
|
export const AAAC_ROOT = path.join(CURSOR_ROOT, "aaac");
|
|
9
10
|
export const STATE_ROOT = path.join(AAAC_ROOT, "state");
|
|
10
11
|
export const RUNS_ROOT = path.join(STATE_ROOT, "runs");
|
|
@@ -12,6 +13,10 @@ export const ACTIVE_RUN_PATH = path.join(STATE_ROOT, "active-run.json");
|
|
|
12
13
|
export const ACTIVE_RUNS_DIR = path.join(STATE_ROOT, "active-runs");
|
|
13
14
|
export const REGISTRY_PATH = path.join(AAAC_ROOT, "runtime-registry.json");
|
|
14
15
|
export const ENFORCEMENT_PATH = path.join(AAAC_ROOT, "enforcement.json");
|
|
16
|
+
export const ONTOLOGY_PATH = path.join(AAAC_ROOT, "ontology.json");
|
|
17
|
+
export const CAPABILITY_REGISTRY_PATH = path.join(AAAC_ROOT, "capabilities", "registry.json");
|
|
18
|
+
export const PROMOTION_RULES_PATH = path.join(AAAC_ROOT, "capabilities", "promotion-rules.json");
|
|
19
|
+
export const CAPABILITY_STATS_PATH = path.join(STATE_ROOT, "capability-stats.json");
|
|
15
20
|
|
|
16
21
|
export function readJson(filePath, fallback = null) {
|
|
17
22
|
try {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verify website static assets + production build.
|
|
4
|
+
* Used by advance-phase on create/update/fix verify completion.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node verify-website-build.mjs [--run-id <run_id>] [--skip-build]
|
|
8
|
+
*/
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { REPO_ROOT, runDir, isoNow, writeJson } from "./lib.mjs";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const WEBSITE_ROOT = path.join(REPO_ROOT, "apps/website");
|
|
17
|
+
const INDEX_HTML = path.join(WEBSITE_ROOT, "index.html");
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const runIdIdx = args.indexOf("--run-id");
|
|
21
|
+
const runId = runIdIdx >= 0 ? args[runIdIdx + 1] : null;
|
|
22
|
+
const skipBuild = args.includes("--skip-build");
|
|
23
|
+
|
|
24
|
+
const results = {
|
|
25
|
+
status: "pass",
|
|
26
|
+
checked_at: isoNow(),
|
|
27
|
+
static_assets: { status: "pass", missing: [] },
|
|
28
|
+
build: { status: skipBuild ? "skipped" : "pending", command: "pnpm --filter @ludecker/website build" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function fail(section, detail) {
|
|
32
|
+
results.status = "fail";
|
|
33
|
+
if (section === "static_assets") {
|
|
34
|
+
results.static_assets.status = "fail";
|
|
35
|
+
results.static_assets.missing.push(detail);
|
|
36
|
+
} else if (section === "build") {
|
|
37
|
+
results.build.status = "fail";
|
|
38
|
+
results.build.detail = detail;
|
|
39
|
+
}
|
|
40
|
+
console.error(`[verify-website-build] FAIL ${section}: ${detail}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveRootAsset(assetPath) {
|
|
44
|
+
const rel = assetPath.replace(/^\//, "");
|
|
45
|
+
const candidates = [
|
|
46
|
+
path.join(WEBSITE_ROOT, "public", rel),
|
|
47
|
+
path.join(WEBSITE_ROOT, rel),
|
|
48
|
+
];
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (fs.existsSync(candidate)) {
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkStaticAssets() {
|
|
58
|
+
if (!fs.existsSync(INDEX_HTML)) {
|
|
59
|
+
fail("static_assets", `missing index.html at ${INDEX_HTML}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const html = fs.readFileSync(INDEX_HTML, "utf8");
|
|
64
|
+
const rootRefs = [
|
|
65
|
+
...html.matchAll(/\b(?:href|src)="(\/[^"#?]+)"/g),
|
|
66
|
+
].map((match) => match[1]);
|
|
67
|
+
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const ref of rootRefs) {
|
|
70
|
+
if (seen.has(ref) || ref.startsWith("//")) continue;
|
|
71
|
+
seen.add(ref);
|
|
72
|
+
|
|
73
|
+
const resolved = resolveRootAsset(ref);
|
|
74
|
+
if (!resolved) {
|
|
75
|
+
fail(
|
|
76
|
+
"static_assets",
|
|
77
|
+
`${ref} not found under apps/website/public/ or apps/website/ (Vite dev resolves root paths to project root)`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runBuild() {
|
|
84
|
+
if (skipBuild) return;
|
|
85
|
+
|
|
86
|
+
const proc = spawnSync(
|
|
87
|
+
"pnpm",
|
|
88
|
+
["--filter", "@ludecker/website", "build"],
|
|
89
|
+
{
|
|
90
|
+
cwd: REPO_ROOT,
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
env: { ...process.env, CI: "1" },
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (proc.status !== 0) {
|
|
97
|
+
const detail = [proc.stderr, proc.stdout].filter(Boolean).join("\n").trim();
|
|
98
|
+
results.build.status = "fail";
|
|
99
|
+
results.build.exit_code = proc.status ?? 1;
|
|
100
|
+
fail("build", detail || `exit ${proc.status}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
results.build.status = "pass";
|
|
105
|
+
results.build.exit_code = 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeArtifact() {
|
|
109
|
+
if (!runId) return;
|
|
110
|
+
|
|
111
|
+
const artifactDir = path.join(runDir(runId), "artifacts");
|
|
112
|
+
fs.mkdirSync(artifactDir, { recursive: true });
|
|
113
|
+
|
|
114
|
+
const yaml = [
|
|
115
|
+
`status: ${results.status}`,
|
|
116
|
+
`checked_at: ${results.checked_at}`,
|
|
117
|
+
"static_assets:",
|
|
118
|
+
` status: ${results.static_assets.status}`,
|
|
119
|
+
` missing: ${JSON.stringify(results.static_assets.missing)}`,
|
|
120
|
+
"build:",
|
|
121
|
+
` status: ${results.build.status}`,
|
|
122
|
+
` command: ${JSON.stringify(results.build.command)}`,
|
|
123
|
+
results.build.exit_code != null ? ` exit_code: ${results.build.exit_code}` : null,
|
|
124
|
+
results.build.detail ? ` detail: ${JSON.stringify(results.build.detail)}` : null,
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join("\n");
|
|
128
|
+
|
|
129
|
+
fs.writeFileSync(path.join(artifactDir, "verify.yaml"), `${yaml}\n`);
|
|
130
|
+
|
|
131
|
+
const manifestPath = path.join(runDir(runId), "run.json");
|
|
132
|
+
try {
|
|
133
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
134
|
+
manifest.artifacts = manifest.artifacts ?? {};
|
|
135
|
+
manifest.artifacts.verify = results;
|
|
136
|
+
manifest.updated_at = isoNow();
|
|
137
|
+
writeJson(manifestPath, manifest);
|
|
138
|
+
} catch {
|
|
139
|
+
// run.json may not exist in standalone invocations
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
checkStaticAssets();
|
|
144
|
+
runBuild();
|
|
145
|
+
writeArtifact();
|
|
146
|
+
|
|
147
|
+
console.log(JSON.stringify({ ok: results.status === "pass", ...results }));
|
|
148
|
+
process.exit(results.status === "pass" ? 0 : 1);
|