@ludecker/aaac 1.1.0 → 1.1.2

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 (40) hide show
  1. package/package.json +1 -1
  2. package/src/generators/generate-commands.mjs +17 -9
  3. package/src/run-engine/advance-phase.mjs +152 -1
  4. package/src/run-engine/capability-evidence.mjs +460 -0
  5. package/src/run-engine/gate-write.mjs +14 -2
  6. package/src/run-engine/init-run.mjs +92 -1
  7. package/src/run-engine/lib.mjs +38 -0
  8. package/src/run-engine/record-task.mjs +7 -1
  9. package/src/run-engine/stop-check.mjs +7 -1
  10. package/src/run-engine/verify-website-build.mjs +185 -0
  11. package/templates/cursor/aaac/capabilities/promotion-rules.json +64 -0
  12. package/templates/cursor/aaac/capabilities/registry.json +11 -11
  13. package/templates/cursor/aaac/dispatch.md +2 -2
  14. package/templates/cursor/aaac/enforcement.json +6 -3
  15. package/templates/cursor/aaac/governance/gates.json +3 -1
  16. package/templates/cursor/aaac/graph.project.yaml +4 -204
  17. package/templates/cursor/aaac/layers.md +3 -0
  18. package/templates/cursor/aaac/observability/telemetry.yaml +3 -0
  19. package/templates/cursor/aaac/ontology.md +17 -32
  20. package/templates/cursor/aaac/project.config.json +4 -1
  21. package/templates/cursor/aaac/run/schema.json +5 -1
  22. package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +152 -1
  23. package/templates/cursor/aaac/scripts/run-engine/capability-evidence.mjs +460 -0
  24. package/templates/cursor/aaac/scripts/run-engine/gate-write.mjs +14 -2
  25. package/templates/cursor/aaac/scripts/run-engine/init-run.mjs +92 -1
  26. package/templates/cursor/aaac/scripts/run-engine/lib.mjs +38 -0
  27. package/templates/cursor/aaac/scripts/run-engine/record-task.mjs +7 -1
  28. package/templates/cursor/aaac/scripts/run-engine/stop-check.mjs +7 -1
  29. package/templates/cursor/aaac/scripts/run-engine/verify-website-build.mjs +185 -0
  30. package/templates/cursor/aaac/state/capability-stats.json +5 -0
  31. package/templates/cursor/agents/playwright-check-run.md +8 -26
  32. package/templates/cursor/agents/release-git.md +2 -2
  33. package/templates/cursor/agents/unit-test-run.md +3 -7
  34. package/templates/cursor/skills/shared/governance/implementation/SKILL.md +25 -396
  35. package/templates/cursor/skills/shared/platform-release/SKILL.md +22 -19
  36. package/templates/cursor/skills/shared/platform-release/orchestrator/contract.yaml +27 -7
  37. package/templates/cursor/skills/shared/testing/SKILL.md +5 -0
  38. package/templates/cursor/skills/shared/verbs/check/orchestrator/SKILL.md +1 -1
  39. package/templates/cursor/skills/shared/verification/SKILL.md +2 -1
  40. package/templates/docs/agentic_architecture.md +163 -60
@@ -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
+ }
@@ -53,10 +53,22 @@ process.stdin.on("end", () => {
53
53
  if (!conversationId) allow();
54
54
 
55
55
  const active = loadActiveRun(conversationId);
56
- if (!active?.run_id || active.status === "completed") allow();
56
+ if (
57
+ !active?.run_id ||
58
+ active.status === "completed" ||
59
+ active.status === "cancelled"
60
+ ) {
61
+ allow();
62
+ }
57
63
 
58
64
  const manifest = loadRunManifest(active.run_id);
59
- if (!manifest || manifest.status === "completed") allow();
65
+ if (
66
+ !manifest ||
67
+ manifest.status === "completed" ||
68
+ manifest.status === "cancelled"
69
+ ) {
70
+ allow();
71
+ }
60
72
  if (manifest.conversation_id && manifest.conversation_id !== conversationId) allow();
61
73
 
62
74
  const enforcement = loadEnforcement();
@@ -11,10 +11,20 @@ import {
11
11
  phaseKind,
12
12
  writeJson,
13
13
  saveActiveRun,
14
+ loadActiveRun,
15
+ loadRunManifest,
16
+ clearActiveRun,
17
+ cancelRunManifest,
18
+ isUserStopIntent,
14
19
  conversationIdFromHook,
15
20
  promptFromHook,
16
21
  } from "./lib.mjs";
17
22
  import { recordLog, recordDecision } from "./log.mjs";
23
+ import {
24
+ resolveCapabilitiesWithRuntime,
25
+ evaluateCapabilityRuntimePolicy,
26
+ loadObjectMaturity,
27
+ } from "./capability-evidence.mjs";
18
28
 
19
29
  async function readStdin() {
20
30
  return new Promise((resolve) => {
@@ -36,6 +46,42 @@ try {
36
46
 
37
47
  const prompt = process.argv[2] ?? promptFromHook(hook);
38
48
  const conversationId = conversationIdFromHook(hook);
49
+
50
+ if (isUserStopIntent(prompt) && conversationId) {
51
+ const active = loadActiveRun(conversationId);
52
+ let cancelledRunId = null;
53
+ if (active?.run_id) {
54
+ const existing = loadRunManifest(active.run_id);
55
+ if (
56
+ existing &&
57
+ existing.status !== "completed" &&
58
+ existing.status !== "cancelled"
59
+ ) {
60
+ cancelRunManifest(existing, prompt.trim());
61
+ recordLog(existing, {
62
+ event: "run_cancelled",
63
+ phase: existing.phase,
64
+ phase_kind: existing.phase_kind,
65
+ detail: `user stop: ${prompt.trim()}`,
66
+ level: "info",
67
+ });
68
+ recordDecision(existing, {
69
+ phase: existing.phase ?? "dispatch",
70
+ decision: "user_stop",
71
+ reason: "User requested stop",
72
+ evidence: prompt.trim(),
73
+ });
74
+ writeJson(`${runDir(active.run_id)}/run.json`, existing);
75
+ cancelledRunId = active.run_id;
76
+ }
77
+ clearActiveRun(conversationId);
78
+ }
79
+ console.log(
80
+ JSON.stringify({ ok: true, aaac: false, cancelled: cancelledRunId }),
81
+ );
82
+ process.exit(0);
83
+ }
84
+
39
85
  const parsed = parseAaacPrompt(prompt);
40
86
 
41
87
  if (!parsed) {
@@ -64,6 +110,14 @@ const runId = `run_${date}_${slugify(parsed.command + (parsed.domain ? `-${parse
64
110
  const entry = registry.commands[parsed.command];
65
111
  fs.mkdirSync(runDir(runId), { recursive: true });
66
112
 
113
+ const runObject = entry.object ?? null;
114
+ const runVerb = entry.verb ?? parsed.command.split("-")[0];
115
+ const objectMaturity = loadObjectMaturity(runObject);
116
+ const capabilitiesResolved = resolveCapabilitiesWithRuntime(runObject, runVerb);
117
+ const capabilityRuntimePolicy = evaluateCapabilityRuntimePolicy(capabilitiesResolved, {
118
+ object_maturity: objectMaturity,
119
+ });
120
+
67
121
  const manifest = {
68
122
  run_id: runId,
69
123
  conversation_id: conversationId,
@@ -84,7 +138,9 @@ const manifest = {
84
138
  artifacts: {},
85
139
  checkpoints: [],
86
140
  log: [],
87
- capabilities_resolved: {},
141
+ capabilities_resolved: capabilitiesResolved,
142
+ capability_runtime: capabilityRuntimePolicy,
143
+ capability_runtime_approved: false,
88
144
  confidence: { architecture: null, requirements: null, scope: null },
89
145
  gates: { stack: entry.gate_stack ?? null, results: {} },
90
146
  swarm: { task_launches_this_phase: 0, phase: pending[0] },
@@ -131,6 +187,41 @@ recordDecision(manifest, {
131
187
  evidence: parsed.raw,
132
188
  });
133
189
 
190
+ for (const [capabilityId, resolution] of Object.entries(manifest.capabilities_resolved)) {
191
+ recordLog(manifest, {
192
+ event: "capability_resolved",
193
+ phase: "dispatch",
194
+ phase_kind: "work",
195
+ detail: `${capabilityId}:${(resolution.providers ?? []).map((p) => p.id).join(",")} state=${resolution.runtime?.state ?? "experimental"}`,
196
+ level: "debug",
197
+ });
198
+ }
199
+
200
+ recordLog(manifest, {
201
+ event: "capability_runtime_evaluated",
202
+ phase: "dispatch",
203
+ phase_kind: "work",
204
+ detail: `action=${capabilityRuntimePolicy.action} maturity=${objectMaturity}`,
205
+ level: "info",
206
+ });
207
+
208
+ if (capabilityRuntimePolicy.action === "warn") {
209
+ recordLog(manifest, {
210
+ event: "capability_runtime_warn",
211
+ phase: "dispatch",
212
+ phase_kind: "work",
213
+ detail: capabilityRuntimePolicy.reasons.join("; "),
214
+ level: "warn",
215
+ });
216
+ }
217
+
218
+ recordDecision(manifest, {
219
+ phase: "dispatch",
220
+ decision: "capability_runtime",
221
+ reason: capabilityRuntimePolicy.action,
222
+ evidence: capabilityRuntimePolicy.reasons.join("; ") || "allow",
223
+ });
224
+
134
225
  recordLog(manifest, {
135
226
  event: "phase_start",
136
227
  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 {
@@ -134,3 +139,36 @@ export function phaseKind(phase, registry) {
134
139
  export function promptFromHook(hook) {
135
140
  return hook?.prompt ?? hook?.text ?? hook?.content ?? "";
136
141
  }
142
+
143
+ /** User explicitly asked to halt the current Run (short prompts only). */
144
+ export function isUserStopIntent(text) {
145
+ if (!text || typeof text !== "string") return false;
146
+ const trimmed = text.trim();
147
+ if (trimmed.length > 60) return false;
148
+ return (
149
+ /^(stop|cancel|abort)([.!?]*)$/i.test(trimmed) ||
150
+ /^(please\s+)?(stop|cancel|abort)([.!?]*)$/i.test(trimmed) ||
151
+ /^(stop|cancel|abort)\s+(the\s+)?run([.!?]*)$/i.test(trimmed)
152
+ );
153
+ }
154
+
155
+ export function cancelRunManifest(manifest, evidence = "user_stop") {
156
+ manifest.status = "cancelled";
157
+ manifest.awaiting_approval = false;
158
+ manifest.blocked_reason = null;
159
+ manifest.updated_at = isoNow();
160
+ if (manifest.enforcement) {
161
+ manifest.enforcement.edit_allowed = true;
162
+ }
163
+ return manifest;
164
+ }
165
+
166
+ export function clearActiveRun(conversationId) {
167
+ if (!conversationId) return;
168
+ const filePath = activeRunPath(conversationId);
169
+ try {
170
+ fs.unlinkSync(filePath);
171
+ } catch {
172
+ // already cleared
173
+ }
174
+ }
@@ -34,7 +34,13 @@ process.stdin.on("end", () => {
34
34
  if (!active?.run_id) allow();
35
35
 
36
36
  const manifest = loadRunManifest(active.run_id);
37
- if (!manifest || manifest.status === "completed") allow();
37
+ if (
38
+ !manifest ||
39
+ manifest.status === "completed" ||
40
+ manifest.status === "cancelled"
41
+ ) {
42
+ allow();
43
+ }
38
44
  if (manifest.conversation_id && manifest.conversation_id !== conversationId) allow();
39
45
 
40
46
  manifest.swarm = manifest.swarm ?? {};
@@ -28,7 +28,13 @@ process.stdin.on("end", () => {
28
28
  if (!active?.run_id) process.exit(0);
29
29
 
30
30
  const manifest = loadRunManifest(active.run_id);
31
- if (!manifest || manifest.status === "completed") process.exit(0);
31
+ if (
32
+ !manifest ||
33
+ manifest.status === "completed" ||
34
+ manifest.status === "cancelled"
35
+ ) {
36
+ process.exit(0);
37
+ }
32
38
 
33
39
  const remaining = [manifest.phase, ...(manifest.pending ?? [])].filter(Boolean);
34
40