@ludecker/aaac 1.1.5 → 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 (32) 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 +23 -0
  9. package/src/run-engine/gate-write.mjs +13 -0
  10. package/src/run-engine/lib.mjs +153 -5
  11. package/templates/cursor/aaac/enforcement.json +14 -4
  12. package/templates/cursor/aaac/graph.project.yaml +16 -5
  13. package/templates/cursor/aaac/lifecycle/lifecycle.json +12 -0
  14. package/templates/cursor/aaac/lifecycle/phases.json +2 -0
  15. package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +23 -0
  16. package/templates/cursor/aaac/scripts/run-engine/gate-write.mjs +13 -0
  17. package/templates/cursor/aaac/scripts/run-engine/lib.mjs +153 -5
  18. package/templates/cursor/agents/doc-conformance.md +25 -0
  19. package/templates/cursor/agents/implementation-review.md +21 -0
  20. package/templates/cursor/agents/test-author.md +27 -0
  21. package/templates/cursor/rules/aaac-enforcement.mdc +10 -3
  22. package/templates/cursor/skills/shared/execution/SKILL.md +7 -3
  23. package/templates/cursor/skills/shared/governance/implementation/SKILL.md +396 -28
  24. package/templates/cursor/skills/shared/implementation-review/SKILL.md +49 -0
  25. package/templates/cursor/skills/shared/planning/SKILL.md +5 -0
  26. package/templates/cursor/skills/shared/test-authoring/SKILL.md +58 -0
  27. package/templates/cursor/skills/shared/testing/SKILL.md +6 -0
  28. package/templates/cursor/skills/shared/verbs/create/orchestrator/SKILL.md +5 -3
  29. package/templates/cursor/skills/shared/verbs/fix/orchestrator/SKILL.md +5 -3
  30. package/templates/cursor/skills/shared/verbs/update/orchestrator/SKILL.md +5 -3
  31. package/templates/cursor/skills/shared/verification/SKILL.md +5 -3
  32. 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 = [
@@ -138,11 +160,22 @@ export function phaseKind(phase, registry) {
138
160
 
139
161
  /** Swarm minimum for completed phase — check verb uses check_swarm on discover. */
140
162
  export function resolveSwarmMinimum(completedPhase, manifest, enforcement) {
141
- if (
142
- completedPhase === "verify" &&
143
- (enforcement.fix_commands?.includes(manifest.command) || manifest.verb === "fix")
144
- ) {
145
- return enforcement.swarm_min_agents?.verify_fix;
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;
146
179
  }
147
180
  if (completedPhase === "discover" && manifest.verb === "check") {
148
181
  return (
@@ -189,3 +222,118 @@ export function clearActiveRun(conversationId) {
189
222
  // already cleared
190
223
  }
191
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
+ }
@@ -1,15 +1,23 @@
1
1
  {
2
- "version": 2,
2
+ "version": 3,
3
3
  "description": "AAAC runtime enforcement — SSOT for hooks and run engine",
4
- "edit_phases": ["execute", "sync_inventory", "persist", "write"],
5
- "artifact_write_phases": ["plan", "report", "verify"],
4
+ "edit_phases": ["execute", "test_execute", "sync_inventory", "persist", "write"],
5
+ "artifact_write_phases": ["plan", "report"],
6
+ "mutating_verbs": ["create", "update", "fix"],
7
+ "phase_edit_scopes": {
8
+ "execute": { "deny_test_paths": true },
9
+ "test_execute": { "test_paths_only": true }
10
+ },
6
11
  "verify_verbs": ["create", "update", "fix"],
7
12
  "swarm_min_agents": {
8
13
  "discover": 4,
9
14
  "check_swarm": 3,
10
15
  "investigate_swarm": 7,
11
16
  "research_swarm": 6,
12
- "verify_fix": 3
17
+ "test_execute": 1,
18
+ "verify": 3,
19
+ "verify_fix": 3,
20
+ "review_swarm": 3
13
21
  },
14
22
  "phase_artifacts": {
15
23
  "investigate_swarm": ["artifacts/investigation.md"],
@@ -20,7 +28,9 @@
20
28
  "dependency_graph": ["artifacts/dependency_graph.yaml"],
21
29
  "fitness_functions": ["artifacts/fitness.yaml"],
22
30
  "rollback": ["artifacts/rollback.yaml"],
31
+ "test_execute": ["artifacts/test_plan.yaml"],
23
32
  "verify": ["artifacts/verify.yaml"],
33
+ "review_swarm": ["artifacts/review.yaml"],
24
34
  "report": ["artifacts/report.md"]
25
35
  },
26
36
  "allowed_path_prefixes": {
@@ -1,5 +1,6 @@
1
- # Generic AAAC project overlay — verb orchestrators + exception commands only.
2
- # Add domain resolvers, orchestrators, and project skills in your repo after init.
1
+ # Generic AAAC project overlay — copied into consumer repos by `aaac init`.
2
+ # Add `resolvers:` and domain orchestrators when you create `.cursor/domains/<slug>/`.
3
+ # Reference implementation (Lüdecker cms/ui/database): ludecker repo `.cursor/aaac/graph.project.yaml`
3
4
 
4
5
  orchestrators:
5
6
  update-doc:
@@ -41,7 +42,7 @@ orchestrators:
41
42
  verb-fix:
42
43
  path: skills/shared/verbs/fix/orchestrator
43
44
  requires: [discovery, investigation, root-cause, planning, validation, impact-analysis, dependency-graph, fitness-functions, rollback, execution, testing, verification, reporting]
44
- phases: [load_inventory, discover, investigate_swarm, root_cause, plan, validate, impact_analysis, dependency_graph, fitness_functions, rollback, execute, verify, sync_inventory, report]
45
+ phases: [load_inventory, discover, investigate_swarm, root_cause, plan, validate, impact_analysis, dependency_graph, fitness_functions, rollback, execute, test_execute, verify, review_swarm, sync_inventory, report]
45
46
 
46
47
  verb-review:
47
48
  path: skills/shared/verbs/review/orchestrator
@@ -72,11 +73,17 @@ skills:
72
73
  execution:
73
74
  path: skills/shared/execution
74
75
  depends: [governance/implementation]
76
+ test-authoring:
77
+ path: skills/shared/test-authoring
78
+ agents: [test-author]
75
79
  testing:
76
80
  path: skills/shared/testing
77
81
  agents: [unit-test-run, fallow-check-changed, fix-repro-verify]
78
82
  verification:
79
83
  path: skills/shared/verification
84
+ implementation-review:
85
+ path: skills/shared/implementation-review
86
+ agents: [boundary-review, doc-conformance, implementation-review]
80
87
  reporting:
81
88
  path: skills/shared/reporting
82
89
  architecture:
@@ -182,6 +189,12 @@ agents:
182
189
  path: agents/fix-repro-verify.md
183
190
  fix-hypothesis-validate:
184
191
  path: agents/fix-hypothesis-validate.md
192
+ test-author:
193
+ path: agents/test-author.md
194
+ doc-conformance:
195
+ path: agents/doc-conformance.md
196
+ implementation-review:
197
+ path: agents/implementation-review.md
185
198
  release-git:
186
199
  path: agents/release-git.md
187
200
  wave: 1
@@ -189,7 +202,5 @@ agents:
189
202
 
190
203
  policies:
191
204
  - policies/master-rules.md
192
- - policies/project-context.md
193
- - policies/ui-design.md
194
205
  - policies/implementation.md
195
206
  - policies/mcp-and-deploy.md
@@ -8,7 +8,9 @@
8
8
  "investigate_lite",
9
9
  "plan",
10
10
  "execute",
11
+ "test_execute",
11
12
  "verify",
13
+ "review_swarm",
12
14
  "report"
13
15
  ],
14
16
  "gate_stack": "pre_execute"
@@ -19,7 +21,9 @@
19
21
  "investigate_lite",
20
22
  "plan",
21
23
  "execute",
24
+ "test_execute",
22
25
  "verify",
26
+ "review_swarm",
23
27
  "report"
24
28
  ],
25
29
  "gate_stack": "pre_execute"
@@ -31,7 +35,9 @@
31
35
  "root_cause",
32
36
  "plan",
33
37
  "execute",
38
+ "test_execute",
34
39
  "verify",
40
+ "review_swarm",
35
41
  "report"
36
42
  ],
37
43
  "gate_stack": "pre_execute"
@@ -58,7 +64,9 @@
58
64
  "investigate_lite",
59
65
  "plan",
60
66
  "execute",
67
+ "test_execute",
61
68
  "verify",
69
+ "review_swarm",
62
70
  "report"
63
71
  ],
64
72
  "gate_stack": "pre_execute"
@@ -96,7 +104,9 @@
96
104
  "root_cause",
97
105
  "plan",
98
106
  "execute",
107
+ "test_execute",
99
108
  "verify",
109
+ "review_swarm",
100
110
  "report"
101
111
  ],
102
112
  "gate_stack": "pre_execute"
@@ -109,7 +119,9 @@
109
119
  "root_cause",
110
120
  "plan",
111
121
  "execute",
122
+ "test_execute",
112
123
  "verify",
124
+ "review_swarm",
113
125
  "report"
114
126
  ],
115
127
  "gate_stack": "pre_execute"
@@ -14,7 +14,9 @@
14
14
  "fitness_functions": { "skill": "fitness-functions", "gate": true },
15
15
  "rollback": { "skill": "rollback", "gate": true },
16
16
  "execute": { "skill": "execution" },
17
+ "test_execute": { "skill": "test-authoring" },
17
18
  "verify": { "skills": ["testing", "verification"] },
19
+ "review_swarm": { "skill": "implementation-review", "readonly": true },
18
20
  "report": { "skill": "reporting" }
19
21
  }
20
22
  }
@@ -17,6 +17,7 @@ import {
17
17
  isEditPhase,
18
18
  isGatePhase,
19
19
  resolveSwarmMinimum,
20
+ validatePhaseArtifactContent,
20
21
  writeJson,
21
22
  saveActiveRun,
22
23
  } from "./lib.mjs";
@@ -132,6 +133,28 @@ for (const rel of requiredArtifacts) {
132
133
  }
133
134
  }
134
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
+
135
158
  const now = isoNow();
136
159
  const completedIsGate = isGatePhase(completedPhase, registry);
137
160
 
@@ -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
  }
@@ -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 = [
@@ -138,11 +160,22 @@ export function phaseKind(phase, registry) {
138
160
 
139
161
  /** Swarm minimum for completed phase — check verb uses check_swarm on discover. */
140
162
  export function resolveSwarmMinimum(completedPhase, manifest, enforcement) {
141
- if (
142
- completedPhase === "verify" &&
143
- (enforcement.fix_commands?.includes(manifest.command) || manifest.verb === "fix")
144
- ) {
145
- return enforcement.swarm_min_agents?.verify_fix;
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;
146
179
  }
147
180
  if (completedPhase === "discover" && manifest.verb === "check") {
148
181
  return (
@@ -189,3 +222,118 @@ export function clearActiveRun(conversationId) {
189
222
  // already cleared
190
223
  }
191
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
+ }
@@ -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