@ludecker/aaac 1.1.4 → 1.1.6

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 (44) hide show
  1. package/README.md +27 -12
  2. package/package.json +1 -1
  3. package/src/cli.mjs +19 -7
  4. package/src/generators/generate-commands.mjs +25 -1
  5. package/src/generators/generate-graph.mjs +9 -1
  6. package/src/lib/install.mjs +13 -1
  7. package/src/lib/sweep-project-docs.mjs +348 -0
  8. package/src/run-engine/advance-phase.mjs +25 -5
  9. package/src/run-engine/debug-run.mjs +6 -0
  10. package/src/run-engine/gate-write.mjs +13 -0
  11. package/src/run-engine/lib.mjs +165 -0
  12. package/src/run-engine/log.mjs +1 -1
  13. package/src/run-engine/record-task.mjs +25 -4
  14. package/templates/cursor/aaac/enforcement.json +20 -4
  15. package/templates/cursor/aaac/graph.project.yaml +16 -5
  16. package/templates/cursor/aaac/lifecycle/lifecycle.json +12 -0
  17. package/templates/cursor/aaac/lifecycle/phases.json +2 -0
  18. package/templates/cursor/aaac/run/schema.json +5 -0
  19. package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +25 -5
  20. package/templates/cursor/aaac/scripts/run-engine/debug-run.mjs +6 -0
  21. package/templates/cursor/aaac/scripts/run-engine/gate-write.mjs +13 -0
  22. package/templates/cursor/aaac/scripts/run-engine/lib.mjs +165 -0
  23. package/templates/cursor/aaac/scripts/run-engine/log.mjs +1 -1
  24. package/templates/cursor/aaac/scripts/run-engine/record-task.mjs +25 -4
  25. package/templates/cursor/agents/doc-conformance.md +25 -0
  26. package/templates/cursor/agents/implementation-review.md +21 -0
  27. package/templates/cursor/agents/test-author.md +27 -0
  28. package/templates/cursor/rules/aaac-enforcement.mdc +18 -6
  29. package/templates/cursor/skills/shared/_task-prompt-policy.md +18 -0
  30. package/templates/cursor/skills/shared/check/SKILL.md +4 -0
  31. package/templates/cursor/skills/shared/discovery/SKILL.md +4 -0
  32. package/templates/cursor/skills/shared/execution/SKILL.md +7 -3
  33. package/templates/cursor/skills/shared/governance/implementation/SKILL.md +396 -28
  34. package/templates/cursor/skills/shared/implementation-review/SKILL.md +49 -0
  35. package/templates/cursor/skills/shared/investigation/SKILL.md +1 -1
  36. package/templates/cursor/skills/shared/investigation-lite/SKILL.md +2 -0
  37. package/templates/cursor/skills/shared/planning/SKILL.md +5 -0
  38. package/templates/cursor/skills/shared/test-authoring/SKILL.md +58 -0
  39. package/templates/cursor/skills/shared/testing/SKILL.md +9 -3
  40. package/templates/cursor/skills/shared/verbs/create/orchestrator/SKILL.md +5 -3
  41. package/templates/cursor/skills/shared/verbs/fix/orchestrator/SKILL.md +5 -3
  42. package/templates/cursor/skills/shared/verbs/update/orchestrator/SKILL.md +5 -3
  43. package/templates/cursor/skills/shared/verification/SKILL.md +5 -3
  44. package/templates/docs/agentic_architecture.md +168 -97
package/README.md CHANGED
@@ -4,40 +4,55 @@
4
4
 
5
5
  > Commands are the public API. Skills, agents, and orchestrators are private implementation.
6
6
 
7
- ## Install
7
+ ## Quick start
8
8
 
9
- No `npm` CLI required. Use `npx` or `pnpm dlx`:
9
+ **1. Install into your repo** (no global `npm` CLI required):
10
10
 
11
11
  ```bash
12
12
  npx @ludecker/aaac@latest init
13
13
  ```
14
14
 
15
15
  ```bash
16
- pnpm dlx @ludecker/aaac@latest init
16
+ pnpm dlx @ludecker/aaac@latest init --yes
17
17
  ```
18
18
 
19
- Non-interactive:
19
+ Non-interactive with a target path:
20
20
 
21
21
  ```bash
22
22
  npx @ludecker/aaac@latest init --yes --dir /path/to/your/repo
23
23
  ```
24
24
 
25
- ## What you get
25
+ **2. Open the project in Cursor** and **enable Hooks once**: Settings → Hooks, then restart Cursor.
26
+
27
+ After that, slash commands work — no domain overlay, resolvers, or manual `generate` step required. `init` already generates `graph.yaml` and all commands.
28
+
29
+ **Install report:** `init` writes `.cursor/aaac/state/install-sweep-report.md` — a read-only inventory of docs, Cursor rules, and AAAC framework markdown in your repo, with recommendations (no merges or aliases).
30
+
31
+ **Install report:** `init` writes `.cursor/aaac/state/install-sweep-report.md` — a read-only inventory of docs, Cursor rules, and AAAC framework markdown in your repo, with recommendations (no merges or aliases).
26
32
 
27
- Works **out of the box** after `init` — open in Cursor and run commands. No post-install setup.
33
+ ## Example commands
34
+
35
+ ```text
36
+ /create-module api "Add health check endpoint"
37
+ /fix-bug auth "Session expires too soon"
38
+ /check-module payments "Validate webhook idempotency"
39
+ /review-architecture system "Check layer boundaries"
40
+ ```
41
+
42
+ ## What you get
28
43
 
29
- - `.cursor/hooks.json` — Run lifecycle and edit enforcement (installed with the project)
44
+ - `.cursor/hooks.json` — Run lifecycle and edit enforcement
30
45
  - `.cursor/aaac/` — ontology, graph, lifecycle, run model, enforcement
31
46
  - `.cursor/skills/shared/` — full pipeline (discovery → execute → verify → report)
32
- - `.cursor/agents/` — 13 generic subagent specs
47
+ - `.cursor/agents/` — generic subagent specs
33
48
  - `.cursor/commands/` — ~130 generated slash commands
34
- - `docs/` — ready-to-use `master_rules.md`, `ui_design.md`, `project_context.md`, `architecture.md`, and `agentic_architecture.md`
49
+ - `docs/` — `master_rules.md`, `ui_design.md`, `project_context.md`, `architecture.md`, `agentic_architecture.md`
35
50
 
36
- Optional later: add **domains** under `.cursor/domains/<slug>/` (see maintainer appendix in `agentic_architecture.md`).
51
+ **Optional later:** add **domains** under `.cursor/domains/<slug>/` and resolvers in `graph.project.yaml` for slug routing (e.g. `/update-module cms "…"`). See `docs/agentic_architecture.md` Part 2.
37
52
 
38
53
  ## Regenerate
39
54
 
40
- After editing `ontology.json` or `graph.project.yaml`:
55
+ Only needed after you edit `ontology.json` or `graph.project.yaml`:
41
56
 
42
57
  ```bash
43
58
  npx @ludecker/aaac@latest generate
@@ -48,7 +63,7 @@ pnpm dlx @ludecker/aaac@latest generate
48
63
 
49
64
  - [Install guide](https://ludecker.com/guide/install-aaac)
50
65
  - [Package on npm](https://www.npmjs.com/package/@ludecker/aaac)
51
- - [Lüdecker](https://ludecker.com) — reference implementation
66
+ - [Lüdecker](https://ludecker.com) — reference implementation (domain overlays, production deploy)
52
67
 
53
68
  ## Publish (maintainers)
54
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ludecker/aaac",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Agentic Architecture as Code (AAAC) — installable Cursor agent framework",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.mjs CHANGED
@@ -15,11 +15,16 @@ Usage:
15
15
  aaac generate [options]
16
16
 
17
17
  Commands:
18
- init Copy AAAC kernel into .cursor/ and docs/ (default)
18
+ init Copy AAAC kernel into .cursor/ and docs/ (default)
19
19
  generate Regenerate graph.yaml and commands from ontology
20
20
  log-dump Print Run manifest log + decisions
21
21
  debug-run One-shot Run status summary
22
22
 
23
+ Quick start:
24
+ 1. npx @ludecker/aaac@latest init (or: pnpm dlx @ludecker/aaac@latest init --yes)
25
+ 2. Open project in Cursor → Settings → Hooks → enable → restart Cursor
26
+ 3. Use any slash command (graph + commands generated during init)
27
+
23
28
  Options:
24
29
  --dir <path> Target project directory (default: cwd)
25
30
  --yes, -y Non-interactive defaults
@@ -78,7 +83,7 @@ async function cmdInit(args) {
78
83
  const options = await promptInitOptions(args);
79
84
  console.log(`\nInstalling AAAC into ${options.targetDir}...\n`);
80
85
 
81
- const { cursorDest, docsDest } = installAaac({
86
+ const { cursorDest, docsDest, sweepReportPath } = installAaac({
82
87
  targetDir: options.targetDir,
83
88
  projectName: options.projectName,
84
89
  docsRoot: options.docsRoot,
@@ -90,17 +95,24 @@ Agentic OS is ready.
90
95
 
91
96
  .cursor/ → ${cursorDest}
92
97
  docs/ → ${docsDest}
98
+ sweep report → ${sweepReportPath}
99
+
100
+ Next steps:
101
+
102
+ 1. Open this project in Cursor
103
+ 2. Settings → Hooks → enable Hooks, then restart Cursor
104
+ 3. Read the install sweep report (docs/rules/framework inventory + recommendations)
93
105
 
94
- Open this project in Cursor and use any command, for example:
106
+ Then use any command, for example:
95
107
 
96
108
  /create-module api "Add health check endpoint"
97
- /fix-module auth "Session expires too soon"
109
+ /fix-bug auth "Session expires too soon"
98
110
  /review-architecture system "Check structure"
99
111
 
100
- No setup required after install. Rules and architecture docs are already in place.
101
- To go deeper later, read ${options.docsRoot}/agentic_architecture.md (Part 2 — optional).
112
+ Graph and commands were generated during init no manual setup required.
113
+ Optional: add domain resolvers later (see ${options.docsRoot}/agentic_architecture.md Part 2).
102
114
 
103
- Regenerate commands after ontology edits:
115
+ Regenerate only after ontology or graph.project.yaml edits:
104
116
  npx @ludecker/aaac@latest generate
105
117
  `);
106
118
  }
@@ -96,7 +96,7 @@ Contract: [${canonical}.yaml](../aaac/contracts/commands/${canonical}.yaml)
96
96
  /${cmd} payments "Webhook handler drops events on retry"
97
97
  /${cmd} api "Auth middleware returns 500 on expired token"
98
98
  \`\`\`
99
- `;
99
+ ${testExecuteAppendix()}`;
100
100
  fs.writeFileSync(path.join(commandsDir, `${cmd}.md`), body);
101
101
  }
102
102
 
@@ -203,6 +203,29 @@ function domainLine(cmd) {
203
203
  return "Domain slug recommended.";
204
204
  }
205
205
 
206
+ const MUTATING_VERBS = new Set(["create", "update", "fix"]);
207
+
208
+ function testExecuteAppendix() {
209
+ return `
210
+
211
+ ## Execute vs test_execute (mandatory)
212
+
213
+ | Phase | Allowed edits | Blocked |
214
+ |-------|---------------|---------|
215
+ | **execute** | Production/source files | \`*.test.*\`, \`*.spec.*\`, \`__tests__/\` |
216
+ | **test_execute** | Test files only | Production/source paths |
217
+
218
+ **Rules:**
219
+
220
+ 1. \`artifacts/plan.yaml\` must include **\`tests_to_add\`** (behaviors to cover, or \`tests_to_add: []\` when truly none).
221
+ 2. In **test_execute**, launch **1** [test-author](../../agents/test-author.md) Task agent — parent must not author tests in execute.
222
+ 3. \`artifacts/test_plan.yaml\` must list **\`files_written\`** when \`tests_to_add\` is non-empty, or **\`skipped_reason\`** when \`tests_to_add: []\`.
223
+ 4. **\`status: deferred\` is invalid** — hooks block test writes in execute; deferral is not a substitute for the test_execute phase.
224
+
225
+ If execute hits \`phase cannot edit this path\` for a test file, **advance to test_execute** and author tests there — do not bypass with a hollow \`test_plan.yaml\`.
226
+ `;
227
+ }
228
+
206
229
  function writeCmd(cmd, entry = null) {
207
230
  if (isContentWriter(cmd, entry)) {
208
231
  writeContentWriterCmd(cmd, entry);
@@ -242,6 +265,7 @@ ${layerLine}**${cap(vDesc)}** a **${object}**${aliasNote} — ${description}.
242
265
  3. ${orchestratorHint(cmd, entry ?? {})}
243
266
 
244
267
  ${domainLine(cmd)}
268
+ ${MUTATING_VERBS.has(verb) ? testExecuteAppendix() : ""}
245
269
  `;
246
270
  fs.writeFileSync(path.join(commandsDir, `${cmd}.md`), body);
247
271
  }
@@ -13,6 +13,14 @@ import {
13
13
 
14
14
  const INJECT_MARKER = "{{INJECT_OBJECT_BLOCKS}}";
15
15
 
16
+ /** Drop `#` comment lines from graph.project.yaml — comments are not valid in merged graph.yaml tail. */
17
+ function stripProjectOverlayComments(text) {
18
+ return text
19
+ .split("\n")
20
+ .filter((line) => !/^\s*#/.test(line))
21
+ .join("\n");
22
+ }
23
+
16
24
  const args = parseArgs(process.argv);
17
25
  const cursorRoot = resolveCursorRoot(args.root);
18
26
  const aaac = aaacDir(cursorRoot);
@@ -242,7 +250,7 @@ contracts: aaac/contracts/
242
250
 
243
251
  `;
244
252
 
245
- let tail = projectTail.trim();
253
+ let tail = stripProjectOverlayComments(projectTail.trim());
246
254
  if (tail.includes(INJECT_MARKER)) {
247
255
  tail = tail.replace(INJECT_MARKER, injectBlock.trim());
248
256
  } else {
@@ -7,6 +7,10 @@ import {
7
7
  packageGeneratorsDir,
8
8
  packageTemplatesDir,
9
9
  } from "./paths.mjs";
10
+ import {
11
+ runInstallSweep,
12
+ snapshotProjectDocs,
13
+ } from "./sweep-project-docs.mjs";
10
14
 
11
15
  export function runGenerators(cursorRoot) {
12
16
  const generatorsDir = packageGeneratorsDir();
@@ -45,6 +49,8 @@ export function installAaac({
45
49
  );
46
50
  }
47
51
 
52
+ const beforeSweep = snapshotProjectDocs(resolvedTarget, { docsRoot });
53
+
48
54
  if (fs.existsSync(cursorDest) && force) {
49
55
  const backup = `${cursorDest}.aaac-backup-${Date.now()}`;
50
56
  fs.renameSync(cursorDest, backup);
@@ -73,5 +79,11 @@ export function installAaac({
73
79
 
74
80
  runGenerators(cursorDest);
75
81
 
76
- return { cursorDest, docsDest };
82
+ const sweep = runInstallSweep(resolvedTarget, {
83
+ docsRoot,
84
+ projectName,
85
+ before: beforeSweep,
86
+ });
87
+
88
+ return { cursorDest, docsDest, sweepReportPath: sweep.reportPath };
77
89
  }
@@ -0,0 +1,348 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ /** @typedef {'docs' | 'rules' | 'framework'} SweepCategory */
5
+
6
+ const SKIP_DIR_NAMES = new Set([
7
+ "node_modules",
8
+ ".git",
9
+ "dist",
10
+ "build",
11
+ ".next",
12
+ "coverage",
13
+ "vendor",
14
+ ".turbo",
15
+ ".cache",
16
+ ]);
17
+
18
+ const DOC_DIR_NAMES = new Set(["docs", "doc", "documentation"]);
19
+
20
+ const ROOT_DOC_FILES = new Set([
21
+ "README.md",
22
+ "CONTRIBUTING.md",
23
+ "ARCHITECTURE.md",
24
+ "CHANGELOG.md",
25
+ "CODE_OF_CONDUCT.md",
26
+ "SECURITY.md",
27
+ "AGENTS.md",
28
+ "CLAUDE.md",
29
+ "GEMINI.md",
30
+ ]);
31
+
32
+ const AAAC_TEMPLATE_DOCS = new Set([
33
+ "master_rules.md",
34
+ "project_context.md",
35
+ "ui_design.md",
36
+ "architecture.md",
37
+ "agentic_architecture.md",
38
+ ]);
39
+
40
+ const FRAMEWORK_PREFIXES = [
41
+ ".cursor/aaac/",
42
+ ".cursor/skills/",
43
+ ".cursor/agents/",
44
+ ".cursor/policies/",
45
+ ".cursor/commands/",
46
+ ];
47
+
48
+ const RULES_PREFIXES = [".cursor/rules/"];
49
+
50
+ const MARKDOWN_EXT = /\.(md|mdc)$/i;
51
+
52
+ /**
53
+ * @param {string} root
54
+ * @param {{ docsRoot?: string; includeFramework?: boolean }} [options]
55
+ */
56
+ export function snapshotProjectDocs(root, options = {}) {
57
+ return sweepProjectDocs(root, { ...options, includeFramework: false });
58
+ }
59
+
60
+ /**
61
+ * @param {string} root
62
+ * @param {{ docsRoot?: string; includeFramework?: boolean }} [options]
63
+ */
64
+ export function sweepProjectDocs(root, options = {}) {
65
+ const resolvedRoot = path.resolve(root);
66
+ const docsRoot = normalizeRel(options.docsRoot ?? "docs");
67
+ const includeFramework = options.includeFramework !== false;
68
+ /** @type {Record<SweepCategory, string[]>} */
69
+ const inventory = { docs: [], rules: [], framework: [] };
70
+
71
+ walk(resolvedRoot, "", (rel, isDir) => {
72
+ if (isDir) {
73
+ if (SKIP_DIR_NAMES.has(path.basename(rel))) return "skip";
74
+ if (rel.includes(".cursor/aaac/state/runs")) return "skip";
75
+ return;
76
+ }
77
+ if (!MARKDOWN_EXT.test(rel)) return;
78
+
79
+ const category = classify(rel, docsRoot, includeFramework);
80
+ if (category) inventory[category].push(rel);
81
+ });
82
+
83
+ for (const key of Object.keys(inventory)) {
84
+ inventory[key].sort();
85
+ }
86
+ return inventory;
87
+ }
88
+
89
+ /**
90
+ * @param {{
91
+ * before: Record<SweepCategory, string[]>;
92
+ * after: Record<SweepCategory, string[]>;
93
+ * docsRoot: string;
94
+ * projectName?: string;
95
+ * installedAt?: string;
96
+ * }} ctx
97
+ */
98
+ export function buildRecommendations(ctx) {
99
+ const { before, after, docsRoot } = ctx;
100
+ const recs = [];
101
+ const docsRootNorm = normalizeRel(docsRoot);
102
+
103
+ const overwritten = before.docs.filter((rel) => {
104
+ const base = path.basename(rel);
105
+ return (
106
+ AAAC_TEMPLATE_DOCS.has(base) &&
107
+ after.docs.includes(`${docsRootNorm}/${base}`)
108
+ );
109
+ });
110
+
111
+ if (overwritten.length > 0) {
112
+ recs.push({
113
+ kind: "review_overwrite",
114
+ message: `These doc paths existed before init and were replaced by AAAC templates: ${overwritten.join(", ")}. Review content if you had custom rules there (restore from backup or merge manually).`,
115
+ });
116
+ }
117
+
118
+ const extraDocDirs = new Set();
119
+ for (const rel of after.docs) {
120
+ const top = rel.split("/")[0];
121
+ if (
122
+ DOC_DIR_NAMES.has(top) &&
123
+ top !== docsRootNorm &&
124
+ !rel.startsWith(`${docsRootNorm}/`)
125
+ ) {
126
+ extraDocDirs.add(top);
127
+ }
128
+ }
129
+ if (extraDocDirs.size > 0) {
130
+ recs.push({
131
+ kind: "extra_doc_dirs",
132
+ message: `Additional documentation directories found: ${[...extraDocDirs].join(", ")}. AAAC policies point at ${docsRootNorm}/ — add SSOT anchors in ${docsRootNorm}/project_context.md if those dirs hold project truth.`,
133
+ });
134
+ }
135
+
136
+ if (after.rules.some((r) => r === "AGENTS.md" || r.endsWith("/AGENTS.md"))) {
137
+ recs.push({
138
+ kind: "agents_md",
139
+ message:
140
+ "AGENTS.md is present. Cursor loads it alongside AAAC policies — keep both aligned or document division of responsibility in project_context.md.",
141
+ });
142
+ }
143
+
144
+ const customRules = after.rules.filter((r) =>
145
+ r.startsWith(".cursor/rules/"),
146
+ );
147
+ const aaacRule = customRules.find((r) =>
148
+ r.includes("aaac-enforcement"),
149
+ );
150
+ if (customRules.length > 1) {
151
+ recs.push({
152
+ kind: "cursor_rules",
153
+ message: `${customRules.length} Cursor rule files under .cursor/rules/. Review for overlap with docs/master_rules.md${aaacRule ? " and aaac-enforcement" : ""}.`,
154
+ });
155
+ }
156
+
157
+ const projectContext = `${docsRootNorm}/project_context.md`;
158
+ if (after.docs.includes(projectContext)) {
159
+ recs.push({
160
+ kind: "fill_project_context",
161
+ message: `Fill in ${projectContext} with your repo paths and SSOT anchors when ready — until then agents use generic master rules only.`,
162
+ });
163
+ }
164
+
165
+ const archPaths = after.docs.filter((rel) =>
166
+ /architecture\.md$/i.test(rel),
167
+ );
168
+ if (archPaths.length > 1) {
169
+ recs.push({
170
+ kind: "multiple_architecture",
171
+ message: `Multiple architecture docs: ${archPaths.join(", ")}. Pick one SSOT for structure or cross-link from ${docsRootNorm}/architecture.md.`,
172
+ });
173
+ }
174
+
175
+ if (before.docs.length === 0 && before.rules.length === 0) {
176
+ recs.push({
177
+ kind: "greenfield",
178
+ message:
179
+ "No pre-existing project docs or Cursor rules detected — AAAC template docs are your starting SSOT.",
180
+ });
181
+ } else {
182
+ recs.push({
183
+ kind: "no_auto_merge",
184
+ message:
185
+ "Init did not merge or alias any existing files. This report is informational only — wire existing docs into project_context.md manually if needed.",
186
+ });
187
+ }
188
+
189
+ return recs;
190
+ }
191
+
192
+ /**
193
+ * @param {Parameters<typeof buildRecommendations>[0]} ctx
194
+ */
195
+ export function formatInstallSweepReport(ctx) {
196
+ const { after, docsRoot, projectName = "project", installedAt } = ctx;
197
+ const recommendations = buildRecommendations(ctx);
198
+ const lines = [
199
+ "# AAAC install — docs / rules / framework inventory",
200
+ "",
201
+ `Project: **${projectName}**`,
202
+ installedAt ? `Generated: ${installedAt}` : "",
203
+ "",
204
+ "Read-only sweep after `aaac init`. No merges or aliases were applied.",
205
+ "",
206
+ "## Summary",
207
+ "",
208
+ `| Category | Files |`,
209
+ `|----------|------:|`,
210
+ `| Docs | ${after.docs.length} |`,
211
+ `| Rules | ${after.rules.length} |`,
212
+ `| Framework (AAAC) | ${after.framework.length} |`,
213
+ "",
214
+ "## Docs",
215
+ "",
216
+ ];
217
+
218
+ lines.push(
219
+ ...(after.docs.length
220
+ ? after.docs.map((f) => `- \`${f}\``)
221
+ : ["_None found._"]),
222
+ );
223
+ lines.push("", "## Rules", "");
224
+ lines.push(
225
+ ...(after.rules.length
226
+ ? after.rules.map((f) => `- \`${f}\``)
227
+ : ["_None found._"]),
228
+ );
229
+ lines.push("", "## Framework (AAAC markdown)", "");
230
+ lines.push(
231
+ ...(after.framework.length
232
+ ? after.framework.map((f) => `- \`${f}\``)
233
+ : ["_None found._"]),
234
+ );
235
+
236
+ lines.push("", "## Recommendations", "");
237
+ for (const [i, rec] of recommendations.entries()) {
238
+ lines.push(`${i + 1}. **${rec.kind}** — ${rec.message}`);
239
+ }
240
+ lines.push(
241
+ "",
242
+ "## Next steps",
243
+ "",
244
+ `- Edit \`${normalizeRel(docsRoot)}/project_context.md\` for project-specific paths.`,
245
+ "- Enable Cursor Hooks (Settings → Hooks) and restart Cursor.",
246
+ "- Optional: add domain resolvers in `.cursor/aaac/graph.project.yaml`.",
247
+ "",
248
+ );
249
+
250
+ return `${lines.filter((l) => l !== undefined).join("\n")}\n`;
251
+ }
252
+
253
+ /**
254
+ * @param {string} targetDir
255
+ * @param {string} markdown
256
+ * @returns {string} absolute report path
257
+ */
258
+ export function writeInstallSweepReport(targetDir, markdown) {
259
+ const reportDir = path.join(targetDir, ".cursor", "aaac", "state");
260
+ fs.mkdirSync(reportDir, { recursive: true });
261
+ const reportPath = path.join(reportDir, "install-sweep-report.md");
262
+ fs.writeFileSync(reportPath, markdown);
263
+ return reportPath;
264
+ }
265
+
266
+ /**
267
+ * @param {string} root
268
+ * @param {{ docsRoot: string; projectName: string; before?: ReturnType<typeof snapshotProjectDocs> }} params
269
+ */
270
+ export function runInstallSweep(root, params) {
271
+ const after = sweepProjectDocs(root, {
272
+ docsRoot: params.docsRoot,
273
+ includeFramework: true,
274
+ });
275
+ const before =
276
+ params.before ??
277
+ snapshotProjectDocs(root, { docsRoot: params.docsRoot });
278
+ const markdown = formatInstallSweepReport({
279
+ before,
280
+ after,
281
+ docsRoot: params.docsRoot,
282
+ projectName: params.projectName,
283
+ installedAt: new Date().toISOString(),
284
+ });
285
+ const reportPath = writeInstallSweepReport(root, markdown);
286
+ return { after, before, reportPath, markdown };
287
+ }
288
+
289
+ function normalizeRel(rel) {
290
+ return rel.replace(/\\/g, "/").replace(/\/$/, "") || "docs";
291
+ }
292
+
293
+ /**
294
+ * @param {string} rel POSIX relative path
295
+ * @param {string} docsRoot
296
+ * @param {boolean} includeFramework
297
+ * @returns {SweepCategory | null}
298
+ */
299
+ function classify(rel, docsRoot, includeFramework) {
300
+ const norm = rel.replace(/\\/g, "/");
301
+
302
+ if (RULES_PREFIXES.some((p) => norm.startsWith(p))) return "rules";
303
+ if (norm === "AGENTS.md" || norm === "CLAUDE.md" || norm === "GEMINI.md") {
304
+ return "rules";
305
+ }
306
+ if (norm.startsWith(".cursor/policies/")) return "rules";
307
+
308
+ if (includeFramework && FRAMEWORK_PREFIXES.some((p) => norm.startsWith(p))) {
309
+ if (norm.includes(".cursor/aaac/state/")) return null;
310
+ return "framework";
311
+ }
312
+
313
+ if (norm.startsWith(`${docsRoot}/`)) return "docs";
314
+ const top = norm.split("/")[0];
315
+ if (DOC_DIR_NAMES.has(top)) return "docs";
316
+ if (!norm.includes("/") && ROOT_DOC_FILES.has(norm)) return "docs";
317
+ if (/^(README|ARCHITECTURE|CONTRIBUTING|SECURITY|CHANGELOG)/i.test(norm)) {
318
+ return "docs";
319
+ }
320
+
321
+ return null;
322
+ }
323
+
324
+ /**
325
+ * @param {string} root
326
+ * @param {string} dirRel
327
+ * @param {(rel: string, isDir: boolean) => 'skip' | void} visit
328
+ */
329
+ function walk(root, dirRel, visit) {
330
+ const abs = dirRel === "" ? root : path.join(root, dirRel);
331
+ let entries;
332
+ try {
333
+ entries = fs.readdirSync(abs, { withFileTypes: true });
334
+ } catch {
335
+ return;
336
+ }
337
+
338
+ for (const entry of entries) {
339
+ const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
340
+ if (entry.isDirectory()) {
341
+ const action = visit(rel, true);
342
+ if (action === "skip") continue;
343
+ walk(root, rel, visit);
344
+ } else {
345
+ visit(rel, false);
346
+ }
347
+ }
348
+ }
@@ -16,6 +16,8 @@ import {
16
16
  phaseKind,
17
17
  isEditPhase,
18
18
  isGatePhase,
19
+ resolveSwarmMinimum,
20
+ validatePhaseArtifactContent,
19
21
  writeJson,
20
22
  saveActiveRun,
21
23
  } from "./lib.mjs";
@@ -55,11 +57,7 @@ if (manifest.phase !== completedPhase) {
55
57
  process.exit(1);
56
58
  }
57
59
 
58
- const minAgents =
59
- completedPhase === "verify" &&
60
- (enforcement.fix_commands?.includes(manifest.command) || manifest.verb === "fix")
61
- ? enforcement.swarm_min_agents?.verify_fix
62
- : enforcement.swarm_min_agents?.[completedPhase];
60
+ const minAgents = resolveSwarmMinimum(completedPhase, manifest, enforcement);
63
61
  const launches = manifest.swarm?.task_launches_this_phase ?? 0;
64
62
  if (minAgents && launches < minAgents && !force) {
65
63
  recordLog(manifest, {
@@ -135,6 +133,28 @@ for (const rel of requiredArtifacts) {
135
133
  }
136
134
  }
137
135
 
136
+ if (!force) {
137
+ const contentGate = validatePhaseArtifactContent(
138
+ runId,
139
+ completedPhase,
140
+ manifest,
141
+ enforcement,
142
+ );
143
+ if (!contentGate.ok) {
144
+ recordLog(manifest, {
145
+ event: "gate_fail",
146
+ phase: completedPhase,
147
+ phase_kind: manifest.phase_kind,
148
+ detail: contentGate.reason,
149
+ level: "warn",
150
+ });
151
+ manifest.updated_at = isoNow();
152
+ writeJson(manifestPath, manifest);
153
+ console.error(contentGate.reason);
154
+ process.exit(2);
155
+ }
156
+ }
157
+
138
158
  const now = isoNow();
139
159
  const completedIsGate = isGatePhase(completedPhase, registry);
140
160
 
@@ -31,6 +31,12 @@ if (summary.blocked_reason) console.log(`Blocked: ${summary.blocked_reason}`);
31
31
  console.log(`Completed: ${summary.completed.join(" → ") || "(none)"}`);
32
32
  console.log(`Pending: ${summary.pending.join(" → ") || "(none)"}`);
33
33
  console.log(`Swarm: phase=${summary.swarm.phase} count=${summary.swarm.task_launches_this_phase}`);
34
+ if (summary.swarm.agents?.length) {
35
+ console.log(`Agents: ${summary.swarm.agents.length} recorded this phase`);
36
+ for (const a of summary.swarm.agents.slice(-5)) {
37
+ console.log(` #${a.index} ${a.subagent_type ?? "?"} ${a.description ?? ""} @ ${a.at}`);
38
+ }
39
+ }
34
40
  console.log(`Log: ${summary.log_count} entries Decisions: ${summary.decisions_count}`);
35
41
  console.log("--- last 10 log entries ---");
36
42
  for (const e of summary.last_log_entries) {
@@ -7,6 +7,7 @@ import {
7
7
  loadEnforcement,
8
8
  isEditPhase,
9
9
  isArtifactPath,
10
+ isPathAllowedForPhase,
10
11
  conversationIdFromHook,
11
12
  runDir,
12
13
  writeJson,
@@ -86,6 +87,18 @@ process.stdin.on("end", () => {
86
87
  }
87
88
 
88
89
  if (isEditPhase(manifest.phase, enforcement)) {
90
+ if (filePath && !isPathAllowedForPhase(filePath, manifest.phase, enforcement)) {
91
+ persistEditEvent(
92
+ manifest,
93
+ active.run_id,
94
+ "edit_denied",
95
+ `${toolName} path not allowed in phase ${manifest.phase}: ${filePath}`,
96
+ );
97
+ deny(
98
+ `AAAC: ${manifest.phase} phase cannot edit this path. Run: ${active.run_id}`,
99
+ `Phase "${manifest.phase}" scope violation${filePath ? `: ${filePath}` : ""}. Use test_execute for tests; execute for prod code only.`,
100
+ );
101
+ }
89
102
  persistEditEvent(manifest, active.run_id, "edit_allowed", `${toolName} in phase ${manifest.phase}`);
90
103
  allow();
91
104
  }