@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/src/run-engine/lib.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/run-engine/log.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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, {
|
|
78
|
+
saveActiveRun(conversationId, {
|
|
79
|
+
...active,
|
|
80
|
+
task_launches_this_phase: manifest.swarm.task_launches_this_phase,
|
|
81
|
+
});
|
|
61
82
|
allow();
|
|
62
83
|
});
|
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
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"
|
|
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,
|
|
14
|
+
"check_swarm": 3,
|
|
9
15
|
"investigate_swarm": 7,
|
|
10
16
|
"research_swarm": 6,
|
|
11
|
-
"
|
|
17
|
+
"test_execute": 1,
|
|
18
|
+
"verify": 3,
|
|
19
|
+
"verify_fix": 3,
|
|
20
|
+
"review_swarm": 3
|
|
12
21
|
},
|
|
13
22
|
"phase_artifacts": {
|
|
14
23
|
"investigate_swarm": ["artifacts/investigation.md"],
|
|
15
24
|
"root_cause": ["artifacts/root_cause.yaml"],
|
|
16
25
|
"plan": ["artifacts/plan.yaml"],
|
|
26
|
+
"validate": ["artifacts/validate.yaml"],
|
|
27
|
+
"impact_analysis": ["artifacts/impact.yaml"],
|
|
28
|
+
"dependency_graph": ["artifacts/dependency_graph.yaml"],
|
|
29
|
+
"fitness_functions": ["artifacts/fitness.yaml"],
|
|
30
|
+
"rollback": ["artifacts/rollback.yaml"],
|
|
31
|
+
"test_execute": ["artifacts/test_plan.yaml"],
|
|
17
32
|
"verify": ["artifacts/verify.yaml"],
|
|
33
|
+
"review_swarm": ["artifacts/review.yaml"],
|
|
18
34
|
"report": ["artifacts/report.md"]
|
|
19
35
|
},
|
|
20
36
|
"allowed_path_prefixes": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# Generic AAAC project overlay —
|
|
2
|
-
# Add
|
|
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
|
}
|
|
@@ -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
|
}
|