@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
@@ -123,6 +123,28 @@ export function isEditPhase(phase, enforcement) {
123
123
  return enforcement.edit_phases.includes(phase);
124
124
  }
125
125
 
126
+ /** Test/spec file paths — used for writer vs tester phase scoping. */
127
+ export function isTestPath(filePath) {
128
+ if (!filePath) return false;
129
+ const normalized = filePath.replace(/\\/g, "/");
130
+ return (
131
+ /\.(test|spec)\.(mjs|cjs|js|ts|tsx)$/.test(normalized) ||
132
+ /(?:^|\/)__tests__(?:\/|$)/.test(normalized) ||
133
+ /(?:^|\/)tests\/(?:unit|integration|e2e|fixtures)\//.test(normalized)
134
+ );
135
+ }
136
+
137
+ /** Phase-scoped edit rules from enforcement.phase_edit_scopes (v3+). */
138
+ export function isPathAllowedForPhase(filePath, phase, enforcement) {
139
+ if (!filePath) return true;
140
+ const scopes = enforcement.phase_edit_scopes?.[phase];
141
+ if (!scopes) return true;
142
+ const isTest = isTestPath(filePath);
143
+ if (scopes.deny_test_paths && isTest) return false;
144
+ if (scopes.test_paths_only && !isTest) return false;
145
+ return true;
146
+ }
147
+
126
148
  export function isArtifactPath(filePath, enforcement) {
127
149
  const normalized = filePath.replace(/\\/g, "/");
128
150
  const prefixes = [
@@ -136,6 +158,34 @@ export function phaseKind(phase, registry) {
136
158
  return isGatePhase(phase, registry) ? "gate" : "work";
137
159
  }
138
160
 
161
+ /** Swarm minimum for completed phase — check verb uses check_swarm on discover. */
162
+ export function resolveSwarmMinimum(completedPhase, manifest, enforcement) {
163
+ const mutating = enforcement.mutating_verbs ?? ["create", "update", "fix"];
164
+ const isMutating =
165
+ mutating.includes(manifest.verb) ||
166
+ enforcement.fix_commands?.includes(manifest.command);
167
+
168
+ if (completedPhase === "verify" && isMutating) {
169
+ return (
170
+ enforcement.swarm_min_agents?.verify ??
171
+ enforcement.swarm_min_agents?.verify_fix
172
+ );
173
+ }
174
+ if (completedPhase === "test_execute" && isMutating) {
175
+ return enforcement.swarm_min_agents?.test_execute;
176
+ }
177
+ if (completedPhase === "review_swarm" && isMutating) {
178
+ return enforcement.swarm_min_agents?.review_swarm;
179
+ }
180
+ if (completedPhase === "discover" && manifest.verb === "check") {
181
+ return (
182
+ enforcement.swarm_min_agents?.check_swarm ??
183
+ enforcement.swarm_min_agents?.discover
184
+ );
185
+ }
186
+ return enforcement.swarm_min_agents?.[completedPhase];
187
+ }
188
+
139
189
  export function promptFromHook(hook) {
140
190
  return hook?.prompt ?? hook?.text ?? hook?.content ?? "";
141
191
  }
@@ -172,3 +222,118 @@ export function clearActiveRun(conversationId) {
172
222
  // already cleared
173
223
  }
174
224
  }
225
+
226
+ export function isMutatingVerb(manifest, enforcement) {
227
+ const mutating = enforcement.mutating_verbs ?? ["create", "update", "fix"];
228
+ return (
229
+ mutating.includes(manifest.verb) ||
230
+ (enforcement.fix_commands ?? []).includes(manifest.command)
231
+ );
232
+ }
233
+
234
+ /** List items under a YAML field (lines starting with `-` before next top-level key). */
235
+ export function readYamlListField(content, fieldName) {
236
+ if (!content) return [];
237
+ const lines = content.split("\n");
238
+ const start = lines.findIndex((line) => line.startsWith(`${fieldName}:`));
239
+ if (start < 0) return [];
240
+
241
+ const inline = lines[start].slice(`${fieldName}:`.length).trim();
242
+ if (inline === "[]") return [];
243
+ if (inline && !inline.startsWith("-")) return [inline];
244
+
245
+ const items = [];
246
+ for (let i = start + 1; i < lines.length; i += 1) {
247
+ const line = lines[i];
248
+ if (/^\S/.test(line) && line.trim()) break;
249
+ const itemMatch = line.match(/^\s+-\s+(.*)$/);
250
+ if (itemMatch) items.push(itemMatch[1].trim());
251
+ }
252
+ return items;
253
+ }
254
+
255
+ export function readYamlScalarField(content, fieldName) {
256
+ if (!content) return null;
257
+ const match = content.match(new RegExp(`^${fieldName}:\\s*(.+)$`, "m"));
258
+ if (!match) return null;
259
+ return match[1].trim().replace(/^["']|["']$/g, "");
260
+ }
261
+
262
+ export function hasYamlField(content, fieldName) {
263
+ if (!content) return false;
264
+ return new RegExp(`^${fieldName}:`, "m").test(content);
265
+ }
266
+
267
+ export function planRequiresTests(planContent) {
268
+ if (!planContent) return false;
269
+ if (hasYamlField(planContent, "tests_to_add")) {
270
+ return readYamlListField(planContent, "tests_to_add").length > 0;
271
+ }
272
+ return /^\s*create:[\s\S]*?^\s+-\s+path:.*\/lib\//m.test(planContent);
273
+ }
274
+
275
+ export function validatePhaseArtifactContent(runId, completedPhase, manifest, enforcement) {
276
+ if (!isMutatingVerb(manifest, enforcement)) {
277
+ return { ok: true };
278
+ }
279
+
280
+ const planPath = path.join(runDir(runId), "artifacts/plan.yaml");
281
+ const planContent = fs.existsSync(planPath)
282
+ ? fs.readFileSync(planPath, "utf8")
283
+ : "";
284
+
285
+ if (completedPhase === "plan") {
286
+ if (!hasYamlField(planContent, "tests_to_add")) {
287
+ return {
288
+ ok: false,
289
+ reason:
290
+ "plan.yaml must include tests_to_add (behaviors to cover, or tests_to_add: [] when no tests are needed)",
291
+ };
292
+ }
293
+ return { ok: true };
294
+ }
295
+
296
+ if (completedPhase === "test_execute") {
297
+ const testPlanPath = path.join(runDir(runId), "artifacts/test_plan.yaml");
298
+ const testPlanContent = fs.existsSync(testPlanPath)
299
+ ? fs.readFileSync(testPlanPath, "utf8")
300
+ : "";
301
+
302
+ const filesWritten = readYamlListField(testPlanContent, "files_written");
303
+ const skippedReason = readYamlScalarField(testPlanContent, "skipped_reason");
304
+ const testsRequired = planRequiresTests(planContent);
305
+
306
+ if (/status:\s*deferred/i.test(testPlanContent) && filesWritten.length === 0) {
307
+ return {
308
+ ok: false,
309
+ reason:
310
+ "test_plan.yaml cannot defer tests — author test files in test_execute (files_written required)",
311
+ };
312
+ }
313
+
314
+ if (testsRequired && filesWritten.length === 0) {
315
+ return {
316
+ ok: false,
317
+ reason:
318
+ "plan.yaml tests_to_add requires non-empty test_plan.files_written — launch test-author Task in test_execute",
319
+ };
320
+ }
321
+
322
+ if (
323
+ hasYamlField(planContent, "tests_to_add") &&
324
+ /tests_to_add:\s*\[\]/m.test(planContent) &&
325
+ filesWritten.length === 0 &&
326
+ !skippedReason
327
+ ) {
328
+ return {
329
+ ok: false,
330
+ reason:
331
+ "tests_to_add is empty — test_plan.yaml must include skipped_reason explaining why no tests were authored",
332
+ };
333
+ }
334
+
335
+ return { ok: true };
336
+ }
337
+
338
+ return { ok: true };
339
+ }
@@ -334,7 +334,7 @@ export function debugRunSummary(manifest) {
334
334
  awaiting_approval: manifest.awaiting_approval,
335
335
  completed: manifest.completed ?? [],
336
336
  pending: manifest.pending ?? [],
337
- swarm: { phase: swarmPhase, task_launches_this_phase: swarmCount },
337
+ swarm: { phase: swarmPhase, task_launches_this_phase: swarmCount, agents: manifest.swarm?.agents ?? [] },
338
338
  edit_allowed: manifest.enforcement?.edit_allowed ?? false,
339
339
  last_log_entries: log.slice(-10),
340
340
  decisions_count: (manifest.decisions ?? []).length,
@@ -44,19 +44,40 @@ process.stdin.on("end", () => {
44
44
  if (manifest.conversation_id && manifest.conversation_id !== conversationId) allow();
45
45
 
46
46
  manifest.swarm = manifest.swarm ?? {};
47
- manifest.swarm.task_launches_this_phase = (manifest.swarm.task_launches_this_phase ?? 0) + 1;
47
+ const launchIndex = (manifest.swarm.task_launches_this_phase ?? 0) + 1;
48
+ manifest.swarm.task_launches_this_phase = launchIndex;
48
49
  manifest.swarm.phase = manifest.phase;
49
- manifest.updated_at = isoNow();
50
+
51
+ const agentEntry = {
52
+ at: isoNow(),
53
+ index: launchIndex,
54
+ phase: manifest.phase,
55
+ subagent_type: hook.subagent_type ?? hook.subagentType ?? null,
56
+ description: hook.description ?? hook.subagent_description ?? null,
57
+ model: hook.model ?? null,
58
+ readonly: hook.readonly ?? null,
59
+ };
60
+ manifest.swarm.agents = manifest.swarm.agents ?? [];
61
+ manifest.swarm.agents.push(agentEntry);
62
+
63
+ const telemetryDetail = JSON.stringify({
64
+ count: launchIndex,
65
+ subagent_type: agentEntry.subagent_type,
66
+ index: launchIndex,
67
+ });
50
68
 
51
69
  recordLog(manifest, {
52
70
  event: "agent_spawned",
53
71
  phase: manifest.phase,
54
72
  phase_kind: manifest.phase_kind,
55
- detail: `count=${manifest.swarm.task_launches_this_phase}`,
73
+ detail: telemetryDetail,
56
74
  level: "debug",
57
75
  });
58
76
 
59
77
  writeJson(path.join(runDir(active.run_id), "run.json"), manifest);
60
- saveActiveRun(conversationId, { ...active, task_launches_this_phase: manifest.swarm.task_launches_this_phase });
78
+ saveActiveRun(conversationId, {
79
+ ...active,
80
+ task_launches_this_phase: manifest.swarm.task_launches_this_phase,
81
+ });
61
82
  allow();
62
83
  });
@@ -0,0 +1,25 @@
1
+ # Agent: doc-conformance
2
+
3
+ **Readonly.**
4
+
5
+ ## Role
6
+
7
+ Compare implementation diff against supporting docs and policies — not layer boundaries (see boundary-review).
8
+
9
+ ## Sources (read before judging)
10
+
11
+ - [docs/master_rules.md](../../docs/master_rules.md)
12
+ - [docs/architecture.md](../../docs/architecture.md) when present
13
+ - Domain inventory under `.cursor/domains/<slug>/update/inventory/` when available
14
+ - [.cursor/policies/](../../.cursor/policies/)
15
+
16
+ ## Check
17
+
18
+ - SSOT violations (duplicated constants, mirrored state)
19
+ - Undocumented exceptions to master rules
20
+ - Plan `requirement_map` entries satisfied in code
21
+ - Missing validation at boundaries when plan promised schemas
22
+
23
+ ## Return
24
+
25
+ Findings, Evidence (`path:line`), Severity (critical | suggestion), Confidence.
@@ -0,0 +1,21 @@
1
+ # Agent: implementation-review
2
+
3
+ **Readonly.**
4
+
5
+ ## Role
6
+
7
+ Independent post-execute review of the diff — **not** the agent that wrote the code. Spot-check that the change matches plan and does not introduce obvious defects.
8
+
9
+ ## Check
10
+
11
+ - Plan `paths_to_touch` vs actual diff scope
12
+ - No drive-by refactors outside plan
13
+ - Error paths logged, not swallowed
14
+ - Async flows use explicit state machines where plan required
15
+ - Size budgets not violated on touched files (flag if file grew past 80% budget)
16
+
17
+ ## Return
18
+
19
+ Findings, Evidence (`path:line`), Severity (critical | suggestion), Confidence.
20
+
21
+ **Blocking:** any **critical** finding must be fixed before `report` on mutating verbs.
@@ -0,0 +1,27 @@
1
+ # Agent: test-author
2
+
3
+ **Phase:** `test_execute` only. Parent orchestrator must **not** write test files — this agent does.
4
+
5
+ ## Role
6
+
7
+ Author behavioral tests for changes made in `execute`. Read plan `tests_to_add[]`, implementation diff, and domain inventory test conventions.
8
+
9
+ ## Must
10
+
11
+ - Write only `*.test.*`, `*.spec.*`, or paths under `__tests__/` / `tests/`
12
+ - Cover behaviors from `requirement_map`, not implementation details
13
+ - Match existing test framework (vitest, playwright) in the touched package
14
+ - Include [_task-prompt-policy.md](../skills/shared/_task-prompt-policy.md) policies
15
+
16
+ ## Must not
17
+
18
+ - Edit production/source files (non-test paths)
19
+ - Weaken assertions to make tests pass
20
+ - Duplicate tests that already cover the behavior
21
+
22
+ ## Return
23
+
24
+ - Files created/modified (paths only)
25
+ - Behaviors covered (one line each)
26
+ - Gaps — behaviors still untested
27
+ - Confidence: high | medium | low
@@ -11,15 +11,15 @@ Every AAAC slash command (`/fix-module`, `/update-module`, `/write-article`, …
11
11
 
12
12
  ## Prerequisites
13
13
 
14
- 1. **Project opened in Cursor** — `.cursor/hooks.json` is installed by `init`; hooks run when the project is open
15
- 2. **Registry current** — after ontology edits: `npx @ludecker/aaac@latest generate`
14
+ 1. **Cursor Hooks enabled** — Settings → Hooks; restart Cursor after `.cursor/hooks.json` changes
15
+ 2. **Registry current** — `node .cursor/aaac/generate-graph.mjs`
16
16
 
17
17
  ## Hook behavior (automatic)
18
18
 
19
19
  | Hook | Effect |
20
20
  |------|--------|
21
21
  | `beforeSubmitPrompt` | Detects `/command` → creates Run scoped to **`conversation_id`** (this chat only) |
22
- | `preToolUse` | **Denies** Write/StrReplace/Delete for **this chat only** until execute phase |
22
+ | `preToolUse` | **Denies** edits outside allowed phases; **phase-scoped paths** — `execute` = prod only, `test_execute` = tests only |
23
23
  | `subagentStart` | Counts Task launches for swarm phase validation |
24
24
  | `stop` | Follow-up if Run not `completed` |
25
25
 
@@ -31,11 +31,23 @@ Every AAAC slash command (`/fix-module`, `/update-module`, `/write-article`, …
31
31
  node .cursor/aaac/scripts/run-engine/advance-phase.mjs <run_id> <phase>
32
32
  ```
33
33
  3. **Swarm minimums** (enforced by advance-phase):
34
- - `discover`: 4 Task agents
34
+ - `discover`: 4 Task agents (check verbs: `check_swarm` 3)
35
35
  - `investigate_swarm`: 7 Task agents
36
36
  - `research_swarm`: 6 Task agents
37
- 4. **Code edits only in `execute`** (hook-enforced). Before execute: artifacts only under `.cursor/aaac/state/runs/`.
38
- 5. **Complete the Run** advance through `report`, set status completed.
37
+ - `test_execute`: 1 test-author Task agent (mutating verbs)
38
+ - `verify`: 3 Task agents (all create/update/fix not fix-only)
39
+ - `review_swarm`: 3 readonly reviewers (mutating verbs)
40
+ 4. **Agent separation (mutating verbs):**
41
+ - **Writer** — parent in `execute` only (no test files)
42
+ - **Tester** — test-author subagent in `test_execute` only
43
+ - **Reviewer** — readonly swarm in `review_swarm` (not the execute agent)
44
+ 5. **Verify gate (create / update / fix):** before advancing past `verify`, run:
45
+ ```bash
46
+ node .cursor/aaac/scripts/run-engine/verify-website-build.mjs --run-id <run_id>
47
+ ```
48
+ `advance-phase.mjs verify` runs this automatically and blocks on missing static assets or failed `vite build` (catches favicon/path regressions).
49
+ 5. **Edits:** prod code in `execute`; test files in `test_execute` only. Run artifacts under `.cursor/aaac/state/runs/` anytime.
50
+ 6. **Complete the Run** — advance through `report`, set status completed.
39
51
 
40
52
  ## If edit is denied
41
53
 
@@ -0,0 +1,18 @@
1
+ # Task prompt policy excerpt (mandatory)
2
+
3
+ Append this block to **every** Task sub-agent prompt the orchestrator sends.
4
+
5
+ ## Policies (mandatory)
6
+
7
+ - **Readonly** unless the agent spec explicitly allows test runs or shell commands.
8
+ - **Evidence:** every claim needs `path:line` citations the parent can spot-check.
9
+ - **SSOT:** do not invent constants, routes, or file paths — read the repo.
10
+ - **Prime directive** (master rules): clarity beats cleverness; predictability beats shortcuts; one truth beats convenience.
11
+ - **Layer boundaries:** `packages/ui` must not import `apps/website`; `packages/types` and `packages/utils` stay runtime-free.
12
+ - **Errors:** never silent — state gaps explicitly in the return block.
13
+
14
+ Full policy chain: [.cursor/policies/master-rules.md](../../policies/master-rules.md) → [docs/master_rules.md](../../../docs/master_rules.md)
15
+
16
+ ## Return shape
17
+
18
+ Follow the agent spec (`Findings`, `Evidence`, `Gaps`, `Confidence`). Do not edit files unless the spec allows it.
@@ -34,6 +34,10 @@ Launch **3** parallel `Task` subagents (`explore`, `readonly: true`) in **one me
34
34
 
35
35
  Optional **4th** agent (second wave, only if intent names external system): `discovery-boundaries.md` for integration edges.
36
36
 
37
+ ## Task prompt (mandatory)
38
+
39
+ Every Task prompt **must** include the policy excerpt from [_task-prompt-policy.md](../_task-prompt-policy.md) plus: question, scope, agent spec path, and inventory path when available.
40
+
37
41
  ## Merge
38
42
 
39
43
  Parent synthesizes one brief:
@@ -24,6 +24,10 @@ Launch **4–6** parallel `Task` subagents (`explore`, `readonly: true`) in **on
24
24
 
25
25
  Add domain-specific angles from inventory skill. Max **8** agents total; second wave ≤2 for critical gaps.
26
26
 
27
+ ## Task prompt (mandatory)
28
+
29
+ Every Task prompt **must** include the policy excerpt from [_task-prompt-policy.md](../_task-prompt-policy.md) plus: intent, domain, inventory constraints, and the linked agent spec path.
30
+
27
31
  ## Output
28
32
 
29
33
  Merged brief for `planning`: findings, evidence, gaps, confidence. Parent spot-checks `path:line` claims.
@@ -15,18 +15,22 @@ Orchestrator phase `execute` after approved plan.
15
15
  ## Mandatory
16
16
 
17
17
  1. Read [governance/implementation/SKILL.md](../governance/implementation/SKILL.md)
18
- 2. Read domain inventory when present (`domains/<slug>/update/inventory/`)
18
+ 2. Read domain [inventory](../../../domains/) constraints
19
19
  3. Read [policies/](../../../policies/)
20
20
 
21
21
  ## Actions
22
22
 
23
- - Edit files per plan and implementation skill
24
- - Apply database migrations via configured MCP when your project uses one (see [mcp-and-deploy.md](../../../policies/mcp-and-deploy.md) and `{{DOCS_ROOT}}/project_context.md`)
23
+ - Edit **production/source** files per plan and implementation skill
24
+ - **Do not** create or edit test files (`*.test.*`, `*.spec.*`, `__tests__/`) deferred to `test_execute` / [test-authoring](../test-authoring/SKILL.md)
25
+ - `apply_migration` for new/changed `supabase/migrations/` (project `anseivwusnyiwopihnqu` — see [supabase-mcp.mdc](../../../rules/supabase-mcp.mdc))
26
+ - `track()` for user-facing mutations
25
27
  - Structured logging on server async paths
26
28
 
27
29
  ## Must not
28
30
 
29
31
  - Invent plan during execution
32
+ - Write or edit test files (hooks block in `execute`; use `test_execute`)
33
+ - Self-review implementation (use [implementation-review](../implementation-review/SKILL.md) in `review_swarm`)
30
34
  - Race guards or useEffect-driven mutations (implementation ban)
31
35
  - Skip schema validation at boundaries
32
36