@ludecker/aaac 1.0.0 → 1.1.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.
Files changed (82) hide show
  1. package/README.md +4 -3
  2. package/package.json +13 -1
  3. package/src/cli.mjs +39 -5
  4. package/src/generators/generate-commands.mjs +120 -3
  5. package/src/generators/generate-graph.mjs +17 -0
  6. package/src/lib/install.mjs +1 -0
  7. package/src/lib/run-engine-paths.mjs +33 -0
  8. package/src/run-engine/advance-phase.mjs +192 -0
  9. package/src/run-engine/debug-run.mjs +38 -0
  10. package/src/run-engine/gate-write.mjs +95 -0
  11. package/src/run-engine/init-run.mjs +165 -0
  12. package/src/run-engine/lib.mjs +136 -0
  13. package/src/run-engine/log-dump.mjs +76 -0
  14. package/src/run-engine/log-trace.mjs +18 -0
  15. package/src/run-engine/log.mjs +343 -0
  16. package/src/run-engine/record-task.mjs +56 -0
  17. package/src/run-engine/stop-check.mjs +55 -0
  18. package/templates/cursor/aaac/complexity.yaml +98 -0
  19. package/templates/cursor/aaac/contracts/commands/fix-bug.yaml +10 -3
  20. package/templates/cursor/aaac/contracts/commands/fix-module.yaml +41 -0
  21. package/templates/cursor/aaac/contracts/skills/investigation.yaml +22 -1
  22. package/templates/cursor/aaac/contracts/skills/planning.yaml +17 -0
  23. package/templates/cursor/aaac/contracts/skills/validation.yaml +9 -1
  24. package/templates/cursor/aaac/dispatch.md +30 -5
  25. package/templates/cursor/aaac/enforcement.json +22 -0
  26. package/templates/cursor/aaac/fitness-functions.yaml +8 -0
  27. package/templates/cursor/aaac/governance/gates.json +3 -1
  28. package/templates/cursor/aaac/graph.project.yaml +237 -5
  29. package/templates/cursor/aaac/layers.md +3 -1
  30. package/templates/cursor/aaac/lifecycle/lifecycle.json +41 -1
  31. package/templates/cursor/aaac/lifecycle/phases.json +1 -0
  32. package/templates/cursor/aaac/observability/telemetry.yaml +60 -0
  33. package/templates/cursor/aaac/observability/verb-debug.yaml +170 -0
  34. package/templates/cursor/aaac/ontology.json +10 -1
  35. package/templates/cursor/aaac/run/RUN.md +2 -0
  36. package/templates/cursor/aaac/run/schema.json +9 -0
  37. package/templates/cursor/aaac/scripts/generate-runtime-registry.mjs +115 -0
  38. package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +192 -0
  39. package/templates/cursor/aaac/scripts/run-engine/debug-run.mjs +38 -0
  40. package/templates/cursor/aaac/scripts/run-engine/gate-write.mjs +95 -0
  41. package/templates/cursor/aaac/scripts/run-engine/init-run.mjs +165 -0
  42. package/templates/cursor/aaac/scripts/run-engine/lib.mjs +136 -0
  43. package/templates/cursor/aaac/scripts/run-engine/log-dump.mjs +76 -0
  44. package/templates/cursor/aaac/scripts/run-engine/log-trace.mjs +18 -0
  45. package/templates/cursor/aaac/scripts/run-engine/log.mjs +343 -0
  46. package/templates/cursor/aaac/scripts/run-engine/record-task.mjs +56 -0
  47. package/templates/cursor/aaac/scripts/run-engine/stop-check.mjs +55 -0
  48. package/templates/cursor/agents/aaac-log-debug.md +72 -0
  49. package/templates/cursor/agents/fix-code-path.md +27 -0
  50. package/templates/cursor/agents/fix-hypothesis-validate.md +26 -0
  51. package/templates/cursor/agents/fix-inventory-confirm.md +22 -0
  52. package/templates/cursor/agents/fix-recent-changes.md +22 -0
  53. package/templates/cursor/agents/fix-regression-scope.md +27 -0
  54. package/templates/cursor/agents/fix-repro-verify.md +21 -0
  55. package/templates/cursor/agents/fix-repro.md +29 -0
  56. package/templates/cursor/agents/fix-runtime-evidence.md +22 -0
  57. package/templates/cursor/agents/fix-test-failures.md +23 -0
  58. package/templates/cursor/agents/playwright-check-run.md +44 -0
  59. package/templates/cursor/hooks/aaac-before-submit.sh +3 -0
  60. package/templates/cursor/hooks/aaac-pre-tool.sh +4 -0
  61. package/templates/cursor/hooks/aaac-stop.sh +3 -0
  62. package/templates/cursor/hooks/aaac-subagent-start.sh +3 -0
  63. package/templates/cursor/hooks.json +30 -0
  64. package/templates/cursor/policies/minimal-complexity.md +101 -0
  65. package/templates/cursor/rules/aaac-enforcement.mdc +42 -0
  66. package/templates/cursor/skills/shared/execution/SKILL.md +1 -1
  67. package/templates/cursor/skills/shared/fitness-functions/SKILL.md +23 -7
  68. package/templates/cursor/skills/shared/investigation/SKILL.md +91 -18
  69. package/templates/cursor/skills/shared/investigation/orchestrator/SKILL.md +12 -4
  70. package/templates/cursor/skills/shared/planning/SKILL.md +74 -8
  71. package/templates/cursor/skills/shared/reporting/SKILL.md +2 -1
  72. package/templates/cursor/skills/shared/root-cause/SKILL.md +14 -3
  73. package/templates/cursor/skills/shared/testing/SKILL.md +26 -5
  74. package/templates/cursor/skills/shared/validation/SKILL.md +48 -13
  75. package/templates/cursor/skills/shared/verbs/_dispatch-utils.md +20 -1
  76. package/templates/cursor/skills/shared/verbs/_lifecycle.md +3 -2
  77. package/templates/cursor/skills/shared/verbs/check/orchestrator/SKILL.md +4 -1
  78. package/templates/cursor/skills/shared/verbs/create/orchestrator/SKILL.md +2 -2
  79. package/templates/cursor/skills/shared/verbs/fix/orchestrator/SKILL.md +21 -11
  80. package/templates/cursor/skills/shared/verbs/fix/orchestrator/contract.yaml +19 -4
  81. package/templates/cursor/skills/shared/verbs/update/orchestrator/SKILL.md +2 -2
  82. package/templates/cursor/skills/shared/verification/SKILL.md +2 -0
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /** preToolUse — deny code edits outside execute phase for THIS chat only. */
3
+ import path from "path";
4
+ import {
5
+ loadActiveRun,
6
+ loadRunManifest,
7
+ loadEnforcement,
8
+ isEditPhase,
9
+ isArtifactPath,
10
+ conversationIdFromHook,
11
+ runDir,
12
+ writeJson,
13
+ isoNow,
14
+ } from "./lib.mjs";
15
+ import { recordLog } from "./log.mjs";
16
+
17
+ let input = "";
18
+ process.stdin.setEncoding("utf8");
19
+ process.stdin.on("data", (c) => (input += c));
20
+ process.stdin.on("end", () => {
21
+ const deny = (userMessage, agentMessage, detail) => {
22
+ console.log(JSON.stringify({ permission: "deny", user_message: userMessage, agent_message: agentMessage }));
23
+ process.exit(0);
24
+ };
25
+ const allow = () => {
26
+ console.log(JSON.stringify({ permission: "allow" }));
27
+ process.exit(0);
28
+ };
29
+
30
+ const persistEditEvent = (manifest, runId, event, detail) => {
31
+ recordLog(manifest, {
32
+ event,
33
+ phase: manifest.phase,
34
+ phase_kind: manifest.phase_kind,
35
+ detail,
36
+ level: "debug",
37
+ });
38
+ manifest.updated_at = isoNow();
39
+ writeJson(path.join(runDir(runId), "run.json"), manifest);
40
+ };
41
+
42
+ let hook;
43
+ try {
44
+ hook = JSON.parse(input || "{}");
45
+ } catch {
46
+ allow();
47
+ }
48
+
49
+ const toolName = hook.tool_name ?? hook.toolName ?? "";
50
+ if (!/^(Write|StrReplace|Delete|EditNotebook|ApplyPatch)$/i.test(toolName)) allow();
51
+
52
+ const conversationId = conversationIdFromHook(hook);
53
+ if (!conversationId) allow();
54
+
55
+ const active = loadActiveRun(conversationId);
56
+ if (!active?.run_id || active.status === "completed") allow();
57
+
58
+ const manifest = loadRunManifest(active.run_id);
59
+ if (!manifest || manifest.status === "completed") allow();
60
+ if (manifest.conversation_id && manifest.conversation_id !== conversationId) allow();
61
+
62
+ const enforcement = loadEnforcement();
63
+ const filePath =
64
+ hook.tool_input?.path ?? hook.toolInput?.path ?? hook.tool_input?.file_path ?? hook.arguments?.path ?? "";
65
+
66
+ if (filePath && isArtifactPath(filePath, enforcement)) {
67
+ persistEditEvent(manifest, active.run_id, "edit_allowed", `artifact path: ${filePath}`);
68
+ allow();
69
+ }
70
+
71
+ if (manifest.awaiting_approval || manifest.status === "blocked") {
72
+ persistEditEvent(manifest, active.run_id, "edit_denied", `blocked at gate: ${manifest.blocked_reason ?? "approval"}`);
73
+ deny(`AAAC Run ${active.run_id} blocked at gate.`, `Run blocked. Run: ${active.run_id}`);
74
+ }
75
+
76
+ if (isEditPhase(manifest.phase, enforcement)) {
77
+ persistEditEvent(manifest, active.run_id, "edit_allowed", `${toolName} in phase ${manifest.phase}`);
78
+ allow();
79
+ }
80
+ if (enforcement.artifact_write_phases?.includes(manifest.phase) && filePath) {
81
+ persistEditEvent(manifest, active.run_id, "edit_allowed", `artifact_write phase ${manifest.phase}`);
82
+ allow();
83
+ }
84
+
85
+ persistEditEvent(
86
+ manifest,
87
+ active.run_id,
88
+ "edit_denied",
89
+ `${toolName} blocked in phase ${manifest.phase}${filePath ? `: ${filePath}` : ""}`,
90
+ );
91
+ deny(
92
+ `AAAC: edits blocked in phase "${manifest.phase}" (this chat). Run: ${active.run_id}`,
93
+ `Cannot ${toolName} during "${manifest.phase}". Chat ${conversationId}. Advance phase first.`,
94
+ );
95
+ });
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ /** Create AAAC Run — stdin: Cursor beforeSubmitPrompt hook JSON (conversation-scoped). */
3
+ import fs from "fs";
4
+ import {
5
+ loadRegistry,
6
+ parseAaacPrompt,
7
+ resolvePending,
8
+ runDir,
9
+ slugify,
10
+ isoNow,
11
+ phaseKind,
12
+ writeJson,
13
+ saveActiveRun,
14
+ conversationIdFromHook,
15
+ promptFromHook,
16
+ } from "./lib.mjs";
17
+ import { recordLog, recordDecision } from "./log.mjs";
18
+
19
+ async function readStdin() {
20
+ return new Promise((resolve) => {
21
+ let data = "";
22
+ process.stdin.setEncoding("utf8");
23
+ process.stdin.on("data", (chunk) => (data += chunk));
24
+ process.stdin.on("end", () => resolve(data));
25
+ if (process.stdin.isTTY) resolve("");
26
+ });
27
+ }
28
+
29
+ const stdin = await readStdin();
30
+ let hook = {};
31
+ try {
32
+ hook = stdin ? JSON.parse(stdin) : {};
33
+ } catch {
34
+ hook = {};
35
+ }
36
+
37
+ const prompt = process.argv[2] ?? promptFromHook(hook);
38
+ const conversationId = conversationIdFromHook(hook);
39
+ const parsed = parseAaacPrompt(prompt);
40
+
41
+ if (!parsed) {
42
+ console.log(JSON.stringify({ ok: true, aaac: false }));
43
+ process.exit(0);
44
+ }
45
+
46
+ if (!conversationId) {
47
+ console.log(
48
+ JSON.stringify({
49
+ ok: false,
50
+ aaac: true,
51
+ error: "missing conversation_id — cannot scope Run to this chat",
52
+ }),
53
+ );
54
+ process.exit(0);
55
+ }
56
+
57
+ const registry = loadRegistry();
58
+ const pending = resolvePending(parsed.command, registry);
59
+ const now = isoNow();
60
+ const date = now.slice(0, 10).replace(/-/g, "");
61
+ const convShort = conversationId.slice(0, 8);
62
+ const runId = `run_${date}_${slugify(parsed.command + (parsed.domain ? `-${parsed.domain}` : ""))}-${convShort}`;
63
+
64
+ const entry = registry.commands[parsed.command];
65
+ fs.mkdirSync(runDir(runId), { recursive: true });
66
+
67
+ const manifest = {
68
+ run_id: runId,
69
+ conversation_id: conversationId,
70
+ command: parsed.command,
71
+ verb: entry.verb ?? parsed.command.split("-")[0],
72
+ object: entry.object ?? null,
73
+ domain: parsed.domain,
74
+ intent: parsed.intent,
75
+ orchestrator: entry.orchestrator ?? null,
76
+ status: "running",
77
+ phase: pending[0],
78
+ phase_kind: phaseKind(pending[0], registry),
79
+ awaiting_approval: false,
80
+ blocked_reason: null,
81
+ completed: [],
82
+ pending: pending.slice(1),
83
+ decisions: [],
84
+ artifacts: {},
85
+ checkpoints: [],
86
+ log: [],
87
+ capabilities_resolved: {},
88
+ confidence: { architecture: null, requirements: null, scope: null },
89
+ gates: { stack: entry.gate_stack ?? null, results: {} },
90
+ swarm: { task_launches_this_phase: 0, phase: pending[0] },
91
+ enforcement: { edit_allowed: false, hook_version: 2 },
92
+ created_at: now,
93
+ updated_at: now,
94
+ };
95
+
96
+ recordLog(manifest, {
97
+ event: "command_parsed",
98
+ phase: "dispatch",
99
+ phase_kind: "work",
100
+ detail: parsed.raw,
101
+ level: "info",
102
+ });
103
+
104
+ recordDecision(manifest, {
105
+ phase: "dispatch",
106
+ decision: "command_parsed",
107
+ reason: `Parsed /${parsed.command}`,
108
+ evidence: parsed.raw,
109
+ });
110
+
111
+ recordLog(manifest, {
112
+ event: "graph_resolved",
113
+ phase: "dispatch",
114
+ phase_kind: "work",
115
+ detail: `orchestrator=${entry.orchestrator ?? "null"} pending=${pending.length} phases`,
116
+ level: "debug",
117
+ });
118
+
119
+ recordLog(manifest, {
120
+ event: "run_created",
121
+ phase: pending[0],
122
+ phase_kind: phaseKind(pending[0], registry),
123
+ detail: `Run for /${parsed.command} chat=${convShort}`,
124
+ level: "info",
125
+ });
126
+
127
+ recordDecision(manifest, {
128
+ phase: "dispatch",
129
+ decision: "run_created",
130
+ reason: "Hook-initiated Run (conversation-scoped)",
131
+ evidence: parsed.raw,
132
+ });
133
+
134
+ recordLog(manifest, {
135
+ event: "phase_start",
136
+ phase: pending[0],
137
+ phase_kind: phaseKind(pending[0], registry),
138
+ detail: `Run for /${parsed.command} chat=${convShort}`,
139
+ level: "info",
140
+ });
141
+
142
+ manifest.updated_at = now;
143
+ writeJson(`${runDir(runId)}/run.json`, manifest);
144
+ saveActiveRun(conversationId, {
145
+ run_id: runId,
146
+ conversation_id: conversationId,
147
+ command: parsed.command,
148
+ phase: pending[0],
149
+ status: "running",
150
+ task_launches_this_phase: 0,
151
+ edit_allowed: false,
152
+ started_at: now,
153
+ });
154
+
155
+ console.log(
156
+ JSON.stringify({
157
+ ok: true,
158
+ aaac: true,
159
+ run_id: runId,
160
+ conversation_id: conversationId,
161
+ command: parsed.command,
162
+ phase: pending[0],
163
+ message: `AAAC Run ${runId} created for this chat only.`,
164
+ }),
165
+ );
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ export const CURSOR_ROOT = path.resolve(__dirname, "../../..");
8
+ export const AAAC_ROOT = path.join(CURSOR_ROOT, "aaac");
9
+ export const STATE_ROOT = path.join(AAAC_ROOT, "state");
10
+ export const RUNS_ROOT = path.join(STATE_ROOT, "runs");
11
+ export const ACTIVE_RUN_PATH = path.join(STATE_ROOT, "active-run.json");
12
+ export const ACTIVE_RUNS_DIR = path.join(STATE_ROOT, "active-runs");
13
+ export const REGISTRY_PATH = path.join(AAAC_ROOT, "runtime-registry.json");
14
+ export const ENFORCEMENT_PATH = path.join(AAAC_ROOT, "enforcement.json");
15
+
16
+ export function readJson(filePath, fallback = null) {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
19
+ } catch {
20
+ return fallback;
21
+ }
22
+ }
23
+
24
+ export function writeJson(filePath, data) {
25
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
26
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
27
+ }
28
+
29
+ export function loadRegistry() {
30
+ const reg = readJson(REGISTRY_PATH);
31
+ if (!reg?.commands) {
32
+ throw new Error(`Missing ${REGISTRY_PATH}. Run: node .cursor/aaac/generate-graph.mjs`);
33
+ }
34
+ return reg;
35
+ }
36
+
37
+ export function loadEnforcement() {
38
+ return readJson(ENFORCEMENT_PATH, {
39
+ edit_phases: ["execute", "sync_inventory", "persist", "write"],
40
+ swarm_min_agents: {},
41
+ allowed_path_prefixes: {},
42
+ });
43
+ }
44
+
45
+ export function conversationIdFromHook(hook) {
46
+ return hook?.conversation_id ?? hook?.conversationId ?? hook?.session_id ?? hook?.sessionId ?? null;
47
+ }
48
+
49
+ export function activeRunPath(conversationId) {
50
+ if (!conversationId) return ACTIVE_RUN_PATH;
51
+ const safe = conversationId.replace(/[^a-zA-Z0-9-]/g, "_");
52
+ return path.join(ACTIVE_RUNS_DIR, `${safe}.json`);
53
+ }
54
+
55
+ export function loadActiveRun(conversationId = null) {
56
+ if (conversationId) return readJson(activeRunPath(conversationId));
57
+ return readJson(ACTIVE_RUN_PATH);
58
+ }
59
+
60
+ export function saveActiveRun(conversationId, data) {
61
+ if (!conversationId) {
62
+ writeJson(ACTIVE_RUN_PATH, data);
63
+ return;
64
+ }
65
+ writeJson(activeRunPath(conversationId), data);
66
+ }
67
+
68
+ export function loadRunManifest(runId) {
69
+ return readJson(path.join(RUNS_ROOT, runId, "run.json"));
70
+ }
71
+
72
+ export function runDir(runId) {
73
+ return path.join(RUNS_ROOT, runId);
74
+ }
75
+
76
+ export function parseAaacPrompt(text) {
77
+ if (!text || typeof text !== "string") return null;
78
+ const registry = loadRegistry();
79
+ const trimmed = text.trim();
80
+ const match = trimmed.match(/^\/([a-z][a-z0-9-]*)(?:\s+(\S+))?(?:\s+"([^"]*)"|(?:\s+([\s\S]+))?)?$/);
81
+ if (!match) return null;
82
+ let command = match[1];
83
+ const aliasTarget = registry.aliases?.[command];
84
+ if (aliasTarget) command = aliasTarget;
85
+ if (!registry.commands[command]) return null;
86
+ let domain = match[2] ?? null;
87
+ let intent = (match[3] ?? match[4] ?? "").trim();
88
+ if (domain && (domain.includes(" ") || domain.startsWith('"'))) {
89
+ intent = `${domain} ${intent}`.trim();
90
+ domain = null;
91
+ }
92
+ if (domain && !intent && !domain.match(/^[a-z]+$/)) {
93
+ intent = domain;
94
+ domain = null;
95
+ }
96
+ return { command, domain, intent, raw: trimmed };
97
+ }
98
+
99
+ export function resolvePending(command, registry) {
100
+ const entry = registry.commands[command];
101
+ if (!entry?.pending?.length) throw new Error(`No pending phases for command: ${command}`);
102
+ return entry.pending;
103
+ }
104
+
105
+ export function slugify(s) {
106
+ return (s || "run").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
107
+ }
108
+
109
+ export function isoNow() {
110
+ return new Date().toISOString();
111
+ }
112
+
113
+ export function isGatePhase(phase, registry) {
114
+ return Boolean(registry.gate_phases?.[phase]);
115
+ }
116
+
117
+ export function isEditPhase(phase, enforcement) {
118
+ return enforcement.edit_phases.includes(phase);
119
+ }
120
+
121
+ export function isArtifactPath(filePath, enforcement) {
122
+ const normalized = filePath.replace(/\\/g, "/");
123
+ const prefixes = [
124
+ ...(enforcement.allowed_path_prefixes?.run_artifacts ?? []),
125
+ ...(enforcement.allowed_path_prefixes?.write_article ?? []),
126
+ ];
127
+ return prefixes.some((p) => normalized.includes(p.replace(/^\.\//, "")));
128
+ }
129
+
130
+ export function phaseKind(phase, registry) {
131
+ return isGatePhase(phase, registry) ? "gate" : "work";
132
+ }
133
+
134
+ export function promptFromHook(hook) {
135
+ return hook?.prompt ?? hook?.text ?? hook?.content ?? "";
136
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dump Run manifest log + decisions.
4
+ * Usage: log-dump.mjs <run_id> [--level debug] [--format timeline|json|pretty]
5
+ */
6
+ import { loadRunManifest } from "./lib.mjs";
7
+ import { filterLogByLevel, formatTimeline } from "./log.mjs";
8
+
9
+ function parseArgs(argv) {
10
+ const args = { runId: null, level: "debug", format: "timeline" };
11
+ const positional = [];
12
+ for (let i = 2; i < argv.length; i++) {
13
+ const arg = argv[i];
14
+ if (arg === "--level" && argv[i + 1]) {
15
+ args.level = argv[++i];
16
+ } else if (arg === "--format" && argv[i + 1]) {
17
+ args.format = argv[++i];
18
+ } else if (!arg.startsWith("-")) {
19
+ positional.push(arg);
20
+ }
21
+ }
22
+ args.runId = positional[0];
23
+ return args;
24
+ }
25
+
26
+ const args = parseArgs(process.argv);
27
+
28
+ if (!args.runId) {
29
+ console.error("Usage: log-dump.mjs <run_id> [--level debug] [--format timeline|json|pretty]");
30
+ process.exit(1);
31
+ }
32
+
33
+ const manifest = loadRunManifest(args.runId);
34
+ if (!manifest) {
35
+ console.error(`Run not found: ${args.runId}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ const filteredLog = filterLogByLevel(manifest.log ?? [], args.level);
40
+
41
+ if (args.format === "json") {
42
+ console.log(
43
+ JSON.stringify(
44
+ {
45
+ run_id: manifest.run_id,
46
+ command: manifest.command,
47
+ verb: manifest.verb,
48
+ status: manifest.status,
49
+ phase: manifest.phase,
50
+ log: filteredLog,
51
+ decisions: manifest.decisions ?? [],
52
+ },
53
+ null,
54
+ 2,
55
+ ),
56
+ );
57
+ process.exit(0);
58
+ }
59
+
60
+ if (args.format === "pretty") {
61
+ console.log(`Run: ${manifest.run_id} /${manifest.command} status=${manifest.status} phase=${manifest.phase}`);
62
+ console.log(`Log entries (level>=${args.level}): ${filteredLog.length}`);
63
+ console.log("--- log ---");
64
+ for (const e of filteredLog) {
65
+ const skill = e.skill ? ` skill=${e.skill}` : "";
66
+ console.log(`${e.at} [${e.level ?? "info"}] ${e.phase} :: ${e.event}${skill} — ${e.detail}`);
67
+ }
68
+ console.log("--- decisions ---");
69
+ for (const d of manifest.decisions ?? []) {
70
+ console.log(`${d.at} ${d.phase} :: ${d.decision} — ${d.reason}`);
71
+ if (d.evidence) console.log(` evidence: ${d.evidence}`);
72
+ }
73
+ process.exit(0);
74
+ }
75
+
76
+ console.log(formatTimeline({ ...manifest, log: filteredLog }, { minLevel: args.level }));
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /** Reconstruct chronological trace for a Run. Usage: log-trace.mjs <run_id> */
3
+ import { loadRunManifest } from "./lib.mjs";
4
+ import { buildTrace } from "./log.mjs";
5
+
6
+ const runId = process.argv[2];
7
+ if (!runId) {
8
+ console.error("Usage: log-trace.mjs <run_id>");
9
+ process.exit(1);
10
+ }
11
+
12
+ const manifest = loadRunManifest(runId);
13
+ if (!manifest) {
14
+ console.error(`Run not found: ${runId}`);
15
+ process.exit(1);
16
+ }
17
+
18
+ console.log(buildTrace(manifest));