@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.
- package/README.md +27 -12
- package/package.json +1 -1
- package/src/cli.mjs +19 -7
- package/src/generators/generate-commands.mjs +25 -1
- package/src/generators/generate-graph.mjs +9 -1
- package/src/lib/install.mjs +13 -1
- package/src/lib/sweep-project-docs.mjs +348 -0
- package/src/run-engine/advance-phase.mjs +25 -5
- package/src/run-engine/debug-run.mjs +6 -0
- package/src/run-engine/gate-write.mjs +13 -0
- package/src/run-engine/lib.mjs +165 -0
- package/src/run-engine/log.mjs +1 -1
- package/src/run-engine/record-task.mjs +25 -4
- package/templates/cursor/aaac/enforcement.json +20 -4
- package/templates/cursor/aaac/graph.project.yaml +16 -5
- package/templates/cursor/aaac/lifecycle/lifecycle.json +12 -0
- package/templates/cursor/aaac/lifecycle/phases.json +2 -0
- package/templates/cursor/aaac/run/schema.json +5 -0
- package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +25 -5
- package/templates/cursor/aaac/scripts/run-engine/debug-run.mjs +6 -0
- package/templates/cursor/aaac/scripts/run-engine/gate-write.mjs +13 -0
- package/templates/cursor/aaac/scripts/run-engine/lib.mjs +165 -0
- package/templates/cursor/aaac/scripts/run-engine/log.mjs +1 -1
- package/templates/cursor/aaac/scripts/run-engine/record-task.mjs +25 -4
- package/templates/cursor/agents/doc-conformance.md +25 -0
- package/templates/cursor/agents/implementation-review.md +21 -0
- package/templates/cursor/agents/test-author.md +27 -0
- package/templates/cursor/rules/aaac-enforcement.mdc +18 -6
- package/templates/cursor/skills/shared/_task-prompt-policy.md +18 -0
- package/templates/cursor/skills/shared/check/SKILL.md +4 -0
- package/templates/cursor/skills/shared/discovery/SKILL.md +4 -0
- package/templates/cursor/skills/shared/execution/SKILL.md +7 -3
- package/templates/cursor/skills/shared/governance/implementation/SKILL.md +396 -28
- package/templates/cursor/skills/shared/implementation-review/SKILL.md +49 -0
- package/templates/cursor/skills/shared/investigation/SKILL.md +1 -1
- package/templates/cursor/skills/shared/investigation-lite/SKILL.md +2 -0
- package/templates/cursor/skills/shared/planning/SKILL.md +5 -0
- package/templates/cursor/skills/shared/test-authoring/SKILL.md +58 -0
- package/templates/cursor/skills/shared/testing/SKILL.md +9 -3
- package/templates/cursor/skills/shared/verbs/create/orchestrator/SKILL.md +5 -3
- package/templates/cursor/skills/shared/verbs/fix/orchestrator/SKILL.md +5 -3
- package/templates/cursor/skills/shared/verbs/update/orchestrator/SKILL.md +5 -3
- package/templates/cursor/skills/shared/verification/SKILL.md +5 -3
- 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
|
-
##
|
|
7
|
+
## Quick start
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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/` —
|
|
47
|
+
- `.cursor/agents/` — generic subagent specs
|
|
33
48
|
- `.cursor/commands/` — ~130 generated slash commands
|
|
34
|
-
- `docs/` —
|
|
49
|
+
- `docs/` — `master_rules.md`, `ui_design.md`, `project_context.md`, `architecture.md`, `agentic_architecture.md`
|
|
35
50
|
|
|
36
|
-
Optional later
|
|
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
|
-
|
|
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
package/src/cli.mjs
CHANGED
|
@@ -15,11 +15,16 @@ Usage:
|
|
|
15
15
|
aaac generate [options]
|
|
16
16
|
|
|
17
17
|
Commands:
|
|
18
|
-
init
|
|
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
|
-
|
|
106
|
+
Then use any command, for example:
|
|
95
107
|
|
|
96
108
|
/create-module api "Add health check endpoint"
|
|
97
|
-
/fix-
|
|
109
|
+
/fix-bug auth "Session expires too soon"
|
|
98
110
|
/review-architecture system "Check structure"
|
|
99
111
|
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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 {
|
package/src/lib/install.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|