@ludecker/aaac 1.1.6 → 1.2.0
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/package.json +9 -9
- package/src/cli.mjs +0 -0
- package/src/run-engine/debug-run.mjs +0 -0
- package/src/run-engine/log-dump.mjs +0 -0
- package/src/run-engine/log-trace.mjs +0 -0
- package/templates/cursor/aaac/enforcement.json +84 -3
- package/templates/cursor/aaac/graph.project.yaml +28 -0
- package/templates/cursor/aaac/lifecycle/lifecycle.json +14 -0
- package/templates/cursor/aaac/lifecycle/phases.json +7 -1
- package/templates/cursor/aaac/ontology.json +1 -0
- package/templates/cursor/aaac/project.config.json +36 -0
- package/templates/cursor/aaac/scripts/remediation/auto-check-swarm-synthesis.mjs +75 -0
- package/templates/cursor/aaac/scripts/remediation/auto-dispatch-queue-from-health.mjs +78 -0
- package/templates/cursor/aaac/scripts/remediation/bootstrap-autonomous.mjs +113 -0
- package/templates/cursor/aaac/scripts/remediation/capture-verify-baseline.mjs +66 -0
- package/templates/cursor/aaac/scripts/remediation/capture-wave-snapshot.mjs +79 -0
- package/templates/cursor/aaac/scripts/remediation/check-swarm-raw.template.json +26 -0
- package/templates/cursor/aaac/scripts/remediation/classify-fallow-issues.mjs +77 -0
- package/templates/cursor/aaac/scripts/remediation/classify-verify-failure.mjs +176 -0
- package/templates/cursor/aaac/scripts/remediation/compute-satisfaction.mjs +344 -0
- package/templates/cursor/aaac/scripts/remediation/debt-sweep-gate.mjs +202 -0
- package/templates/cursor/aaac/scripts/remediation/dispatch-rules.json +44 -0
- package/templates/cursor/aaac/scripts/remediation/fallow-fp-rules.json +87 -0
- package/templates/cursor/aaac/scripts/remediation/fallow-scan.mjs +219 -0
- package/templates/cursor/aaac/scripts/remediation/handle-yield.mjs +240 -0
- package/templates/cursor/aaac/scripts/remediation/init-campaign.mjs +211 -0
- package/templates/cursor/aaac/scripts/remediation/lib/autonomous-mode.mjs +63 -0
- package/templates/cursor/aaac/scripts/remediation/lib/campaign-focus.mjs +87 -0
- package/templates/cursor/aaac/scripts/remediation/lib/fallow-classifier.mjs +190 -0
- package/templates/cursor/aaac/scripts/remediation/lib/fallow-health-targets.mjs +56 -0
- package/templates/cursor/aaac/scripts/remediation/lib/fallow-metrics.mjs +119 -0
- package/templates/cursor/aaac/scripts/remediation/lib/invoke-cursor-agent.mjs +51 -0
- package/templates/cursor/aaac/scripts/remediation/lib/reconcile-run-manifest.mjs +41 -0
- package/templates/cursor/aaac/scripts/remediation/lib/regression-analysis.mjs +55 -0
- package/templates/cursor/aaac/scripts/remediation/lib/remediation-config.mjs +69 -0
- package/templates/cursor/aaac/scripts/remediation/lib/remediation-progress.mjs +58 -0
- package/templates/cursor/aaac/scripts/remediation/lib/remediation-watch-loop.mjs +168 -0
- package/templates/cursor/aaac/scripts/remediation/lib/runner-exec.mjs +156 -0
- package/templates/cursor/aaac/scripts/remediation/lib/runner-state.mjs +145 -0
- package/templates/cursor/aaac/scripts/remediation/lib/verify-metrics.mjs +205 -0
- package/templates/cursor/aaac/scripts/remediation/merge-check-swarm.mjs +257 -0
- package/templates/cursor/aaac/scripts/remediation/plan-waves-from-queue.mjs +85 -0
- package/templates/cursor/aaac/scripts/remediation/prepare-check-context.mjs +148 -0
- package/templates/cursor/aaac/scripts/remediation/record-fallow-fp.mjs +107 -0
- package/templates/cursor/aaac/scripts/remediation/record-iteration-step.mjs +56 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-cli.mjs +157 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-cursor-watch.sh +10 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-runner-daemon.sh +13 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-runner.mjs +748 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-yield-watcher.mjs +40 -0
- package/templates/cursor/aaac/scripts/remediation/remediator-gate.mjs +405 -0
- package/templates/cursor/aaac/scripts/remediation/repair-fallow-start-baseline.mjs +118 -0
- package/templates/cursor/aaac/scripts/remediation/runner-health-check.mjs +164 -0
- package/templates/cursor/aaac/scripts/remediation/satisfaction-loop-gate.mjs +286 -0
- package/templates/cursor/aaac/scripts/remediation/validate-campaign-complete.mjs +191 -0
- package/templates/cursor/aaac/scripts/remediation/verify-remediation-iteration.mjs +112 -0
- package/templates/cursor/aaac/scripts/run-engine/debug-run.mjs +0 -0
- package/templates/cursor/aaac/scripts/run-engine/log-dump.mjs +0 -0
- package/templates/cursor/aaac/scripts/run-engine/log-trace.mjs +0 -0
- package/templates/cursor/agents/remediation-check-app-inventory.md +32 -0
- package/templates/cursor/agents/remediation-check-app-ssot.md +24 -0
- package/templates/cursor/agents/remediation-check-app-trace.md +29 -0
- package/templates/cursor/agents/remediation-check-architecture-boundaries.md +21 -0
- package/templates/cursor/agents/remediation-check-architecture-decomposition.md +25 -0
- package/templates/cursor/agents/remediation-check-architecture-deps.md +23 -0
- package/templates/cursor/agents/remediation-check-risk.md +37 -0
- package/templates/cursor/agents/remediation-e2e-gate.md +30 -0
- package/templates/cursor/agents/remediation-remediator.md +69 -0
- package/templates/cursor/commands/remediate-app.md +212 -0
- package/templates/cursor/hooks/aaac-before-submit.sh +0 -0
- package/templates/cursor/hooks/aaac-pre-tool.sh +0 -0
- package/templates/cursor/hooks/aaac-stop.sh +0 -0
- package/templates/cursor/hooks/aaac-subagent-start.sh +0 -0
- package/templates/cursor/skills/shared/remediation/SKILL.md +51 -0
- package/templates/cursor/skills/shared/remediation/babysit/SKILL.md +223 -0
- package/templates/cursor/skills/shared/remediation/check-swarm/SKILL.md +114 -0
- package/templates/cursor/skills/shared/remediation/orchestrator/SKILL.md +275 -0
- package/templates/cursor/skills/shared/remediation/orchestrator/contract.yaml +116 -0
- package/templates/docs/agentic_architecture.md +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared verify execution + metric extraction for remediation campaigns.
|
|
4
|
+
* Layer commands come from project.config.json → remediation.verify.layers.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { spawnSync } from "child_process";
|
|
9
|
+
import { REPO_ROOT, isoNow } from "../../run-engine/lib.mjs";
|
|
10
|
+
import { loadRemediationConfig, resolveLayerKeys } from "./remediation-config.mjs";
|
|
11
|
+
|
|
12
|
+
export function countTypeScriptErrors(text) {
|
|
13
|
+
const matches = text.match(/error TS\d+:/g);
|
|
14
|
+
return matches ? matches.length : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function countVitestFailures(text) {
|
|
18
|
+
const summary = text.match(/Tests\s+\d+\s+failed\s+\((\d+)\)/);
|
|
19
|
+
if (summary) return Number(summary[1]);
|
|
20
|
+
const failedBlocks = text.match(/^\s*FAIL\s+/gm);
|
|
21
|
+
return failedBlocks ? failedBlocks.length : text.includes("FAIL") ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function countGoTestFailures(text) {
|
|
25
|
+
const failLines = text.match(/^--- FAIL:/gm);
|
|
26
|
+
if (failLines) return failLines.length;
|
|
27
|
+
return text.includes("FAIL") && text.includes("go test") ? 1 : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function layerErrorCount(layer, step) {
|
|
31
|
+
if (!step || step.status === "skipped") return 0;
|
|
32
|
+
if (step.status === "pass") return 0;
|
|
33
|
+
const combined = `${step.stdout_full ?? step.stdout_tail ?? ""}\n${step.stderr_full ?? step.stderr_tail ?? ""}\n${step.detail ?? ""}`;
|
|
34
|
+
if (layer === "typecheck") return countTypeScriptErrors(combined);
|
|
35
|
+
if (layer === "vitest") return countVitestFailures(combined);
|
|
36
|
+
if (layer === "go_test") return countGoTestFailures(combined);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function runStep(name, cmd, cmdArgs, cwd, optional = false) {
|
|
41
|
+
const started = isoNow();
|
|
42
|
+
const result = spawnSync(cmd, cmdArgs, {
|
|
43
|
+
cwd: path.resolve(REPO_ROOT, cwd),
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
46
|
+
env: process.env,
|
|
47
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
48
|
+
});
|
|
49
|
+
const status =
|
|
50
|
+
result.status === 0 ? "pass" : optional && result.status !== 0 ? "skipped" : "fail";
|
|
51
|
+
return {
|
|
52
|
+
name,
|
|
53
|
+
status,
|
|
54
|
+
exit_code: result.status,
|
|
55
|
+
started_at: started,
|
|
56
|
+
completed_at: isoNow(),
|
|
57
|
+
stdout_full: result.stdout || "",
|
|
58
|
+
stderr_full: result.stderr || "",
|
|
59
|
+
stdout_tail: (result.stdout || "").slice(-4000),
|
|
60
|
+
stderr_tail: (result.stderr || "").slice(-4000),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function waitForDevServer(url, maxWaitSec = 30) {
|
|
65
|
+
for (let i = 0; i < maxWaitSec; i++) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
68
|
+
if (res.ok) return true;
|
|
69
|
+
} catch {
|
|
70
|
+
/* retry */
|
|
71
|
+
}
|
|
72
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runConfiguredLayer(layerDef) {
|
|
78
|
+
const name = layerDef.name ?? layerDef.id;
|
|
79
|
+
return runStep(
|
|
80
|
+
name,
|
|
81
|
+
layerDef.command,
|
|
82
|
+
layerDef.args ?? [],
|
|
83
|
+
layerDef.cwd ?? ".",
|
|
84
|
+
Boolean(layerDef.optional),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function runPlaywrightLayer(config, mode) {
|
|
89
|
+
const pw = config.verify.playwright;
|
|
90
|
+
if (!pw?.enabled) {
|
|
91
|
+
return {
|
|
92
|
+
name: "playwright_remediation",
|
|
93
|
+
status: "skipped",
|
|
94
|
+
reason: "playwright disabled in project.config.json",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!config.verify.strict_modes.includes(mode)) {
|
|
98
|
+
return {
|
|
99
|
+
name: "playwright_remediation",
|
|
100
|
+
status: "skipped",
|
|
101
|
+
reason: `playwright not run for mode ${mode}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pwCwd = path.resolve(REPO_ROOT, pw.cwd ?? ".");
|
|
106
|
+
const configPath = pw.config ? path.resolve(REPO_ROOT, pw.config) : null;
|
|
107
|
+
if (configPath && !fs.existsSync(configPath)) {
|
|
108
|
+
return {
|
|
109
|
+
name: "playwright_remediation",
|
|
110
|
+
status: "fail",
|
|
111
|
+
detail: `playwright config missing: ${pw.config}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const devUrl = config.verify.dev_server?.url;
|
|
116
|
+
const serverUp = devUrl ? await waitForDevServer(devUrl) : true;
|
|
117
|
+
if (!serverUp) {
|
|
118
|
+
return {
|
|
119
|
+
name: "playwright_remediation",
|
|
120
|
+
status: "fail",
|
|
121
|
+
detail: `dev server not reachable at ${devUrl} — ${config.verify.dev_server?.launch_hint ?? "start dev server"}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const playwrightCli = path.join(pwCwd, "node_modules", "@playwright", "test", "cli.js");
|
|
126
|
+
const args = configPath
|
|
127
|
+
? [playwrightCli, "test", "-c", configPath]
|
|
128
|
+
: [playwrightCli, "test"];
|
|
129
|
+
const step = runStep("playwright_remediation", process.execPath, args, pw.cwd ?? ".");
|
|
130
|
+
step.base_url = devUrl ?? null;
|
|
131
|
+
return step;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function runVerifySteps(mode) {
|
|
135
|
+
const config = loadRemediationConfig();
|
|
136
|
+
const layerKeys = resolveLayerKeys(config);
|
|
137
|
+
const report = {
|
|
138
|
+
status: "pass",
|
|
139
|
+
mode,
|
|
140
|
+
checked_at: isoNow(),
|
|
141
|
+
metrics: {},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
for (const layerDef of config.verify.layers) {
|
|
145
|
+
report[layerDef.id] = runConfiguredLayer(layerDef);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (config.verify.playwright?.enabled) {
|
|
149
|
+
report.playwright = await runPlaywrightLayer(config, mode);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const key of layerKeys) {
|
|
153
|
+
const step = report[key];
|
|
154
|
+
if (step && step.status === "fail") report.status = "fail";
|
|
155
|
+
report.metrics[key] = {
|
|
156
|
+
status: step?.status ?? "skipped",
|
|
157
|
+
error_count: layerErrorCount(key, step),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
report.metrics.total_errors = layerKeys.reduce(
|
|
162
|
+
(sum, k) => sum + (report.metrics[k]?.error_count ?? 0),
|
|
163
|
+
0,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return report;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function writeVerifyLogs(report, logDir, prefix) {
|
|
170
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
171
|
+
const keys = Object.keys(report).filter(
|
|
172
|
+
(k) => k !== "status" && k !== "mode" && k !== "checked_at" && k !== "metrics",
|
|
173
|
+
);
|
|
174
|
+
for (const key of keys) {
|
|
175
|
+
const step = report[key];
|
|
176
|
+
if (!step || typeof step !== "object") continue;
|
|
177
|
+
const logPath = path.join(logDir, `${prefix}-${key}.log`);
|
|
178
|
+
const body = [
|
|
179
|
+
`# ${key} exit=${step.exit_code} status=${step.status}`,
|
|
180
|
+
"## stdout",
|
|
181
|
+
step.stdout_full ?? step.stdout_tail ?? "",
|
|
182
|
+
"## stderr",
|
|
183
|
+
step.stderr_full ?? step.stderr_tail ?? "",
|
|
184
|
+
].join("\n");
|
|
185
|
+
fs.writeFileSync(logPath, body);
|
|
186
|
+
step.log_path = logPath;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function summarizeMetrics(report) {
|
|
191
|
+
const keys = Object.keys(report.metrics ?? {}).filter((k) => k !== "total_errors");
|
|
192
|
+
return {
|
|
193
|
+
status: report.status,
|
|
194
|
+
total_errors: report.metrics?.total_errors ?? 0,
|
|
195
|
+
layers: Object.fromEntries(
|
|
196
|
+
keys.map((k) => [
|
|
197
|
+
k,
|
|
198
|
+
{
|
|
199
|
+
status: report.metrics?.[k]?.status ?? report[k]?.status,
|
|
200
|
+
error_count: report.metrics?.[k]?.error_count ?? 0,
|
|
201
|
+
},
|
|
202
|
+
]),
|
|
203
|
+
),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Merge remediation check_swarm agent outputs → FP registry, reclassify, guardrail artifacts.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node merge-check-swarm.mjs --campaign-id <id> --iteration <n> [--run-id <run_id>]
|
|
7
|
+
*
|
|
8
|
+
* Reads iterations/{n}/check-swarm-raw.json (parent-collected agent JSON blocks).
|
|
9
|
+
*/
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { spawnSync } from "child_process";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
import { REPO_ROOT, isoNow, readJson, writeJson, runDir } from "../run-engine/lib.mjs";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const out = { campaignId: null, iteration: 0, runId: null, rawPath: null };
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
if (a === "--campaign-id") out.campaignId = argv[++i];
|
|
24
|
+
else if (a === "--iteration") out.iteration = Number(argv[++i]);
|
|
25
|
+
else if (a === "--run-id") out.runId = argv[++i];
|
|
26
|
+
else if (a === "--raw") out.rawPath = argv[++i];
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizePath(p) {
|
|
32
|
+
return (p ?? "").replace(/^frontend\//, "").replace(/^\//, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function uniqByPath(items) {
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
const key = `${normalizePath(item.path)}:${item.export_name ?? ""}`;
|
|
40
|
+
if (seen.has(key)) continue;
|
|
41
|
+
seen.add(key);
|
|
42
|
+
out.push({ ...item, path: normalizePath(item.path) });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function yamlList(items, indent = 2) {
|
|
48
|
+
const pad = " ".repeat(indent);
|
|
49
|
+
if (!items.length) return `${pad}[]\n`;
|
|
50
|
+
return items.map((i) => `${pad}- ${JSON.stringify(i)}`).join("\n") + "\n";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const args = parseArgs(process.argv.slice(2));
|
|
54
|
+
if (!args.campaignId) {
|
|
55
|
+
console.error("merge-check-swarm: --campaign-id required");
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const campaignDir = path.join(CAMPAIGNS_ROOT, args.campaignId);
|
|
60
|
+
const iterDir = path.join(campaignDir, "iterations", String(args.iteration));
|
|
61
|
+
const rawPath =
|
|
62
|
+
args.rawPath ?? path.join(iterDir, "check-swarm-raw.json");
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(rawPath)) {
|
|
65
|
+
console.error(`merge-check-swarm: missing ${rawPath}`);
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const raw = readJson(rawPath, { agents: [] });
|
|
70
|
+
const agents = Array.isArray(raw.agents) ? raw.agents : Array.isArray(raw) ? raw : [];
|
|
71
|
+
|
|
72
|
+
const falsePositives = [];
|
|
73
|
+
const protectedPaths = [];
|
|
74
|
+
const doNotDelete = [];
|
|
75
|
+
const safeToFix = [];
|
|
76
|
+
const findings = [];
|
|
77
|
+
const gaps = [];
|
|
78
|
+
|
|
79
|
+
for (const agent of agents) {
|
|
80
|
+
for (const fp of agent.false_positives ?? []) {
|
|
81
|
+
falsePositives.push({
|
|
82
|
+
...fp,
|
|
83
|
+
source: agent.agent_id ?? agent.command_mirror ?? "check-swarm",
|
|
84
|
+
iteration: args.iteration,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const p of agent.protected_paths ?? []) {
|
|
88
|
+
protectedPaths.push(normalizePath(p));
|
|
89
|
+
}
|
|
90
|
+
for (const d of agent.do_not_delete ?? []) {
|
|
91
|
+
doNotDelete.push({ path: normalizePath(d.path ?? d), reason: d.reason ?? "protected" });
|
|
92
|
+
}
|
|
93
|
+
for (const s of agent.safe_to_fix ?? []) {
|
|
94
|
+
safeToFix.push({
|
|
95
|
+
path: normalizePath(s.path),
|
|
96
|
+
category: s.category ?? null,
|
|
97
|
+
export_name: s.export_name ?? null,
|
|
98
|
+
evidence: s.evidence ?? null,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(agent.findings)) findings.push(...agent.findings.map((f) => `[${agent.agent_id}] ${f}`));
|
|
102
|
+
if (Array.isArray(agent.gaps)) gaps.push(...agent.gaps.map((g) => `[${agent.agent_id}] ${g}`));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fpDeduped = uniqByPath(falsePositives);
|
|
106
|
+
const protectedDeduped = [...new Set(protectedPaths)];
|
|
107
|
+
const doNotDeleteDeduped = uniqByPath(
|
|
108
|
+
doNotDelete.map((d) => ({ path: d.path, export_name: null, reason: d.reason })),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (fpDeduped.length) {
|
|
112
|
+
const batchPath = path.join(iterDir, "check-swarm-fp-batch.json");
|
|
113
|
+
writeJson(batchPath, { entries: fpDeduped });
|
|
114
|
+
spawnSync(
|
|
115
|
+
process.execPath,
|
|
116
|
+
[
|
|
117
|
+
path.join(__dirname, "record-fallow-fp.mjs"),
|
|
118
|
+
"--campaign-id",
|
|
119
|
+
args.campaignId,
|
|
120
|
+
"--from-json",
|
|
121
|
+
batchPath,
|
|
122
|
+
],
|
|
123
|
+
{ encoding: "utf8" },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
spawnSync(
|
|
128
|
+
process.execPath,
|
|
129
|
+
[
|
|
130
|
+
path.join(__dirname, "classify-fallow-issues.mjs"),
|
|
131
|
+
"--campaign-id",
|
|
132
|
+
args.campaignId,
|
|
133
|
+
"--iteration",
|
|
134
|
+
String(args.iteration),
|
|
135
|
+
],
|
|
136
|
+
{ encoding: "utf8" },
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const classification = readJson(path.join(iterDir, "fallow-classification.json"), {});
|
|
140
|
+
const context = readJson(path.join(iterDir, "check-context.json"), {});
|
|
141
|
+
|
|
142
|
+
const merge = {
|
|
143
|
+
merged_at: isoNow(),
|
|
144
|
+
campaign_id: args.campaignId,
|
|
145
|
+
iteration: args.iteration,
|
|
146
|
+
agents_reported: agents.length,
|
|
147
|
+
false_positives_recorded: fpDeduped.length,
|
|
148
|
+
protected_paths: protectedDeduped,
|
|
149
|
+
do_not_delete: doNotDeleteDeduped,
|
|
150
|
+
safe_to_fix: safeToFix,
|
|
151
|
+
classification_after_merge: classification.summary ?? null,
|
|
152
|
+
findings,
|
|
153
|
+
gaps,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
writeJson(path.join(iterDir, "check-swarm-merge.json"), merge);
|
|
157
|
+
|
|
158
|
+
const checkAppAgents = agents.filter((a) => a.command_mirror === "check-app" || (a.agent_id ?? "").includes("check-app"));
|
|
159
|
+
const archAgents = agents.filter(
|
|
160
|
+
(a) => a.command_mirror === "check-architecture" || (a.agent_id ?? "").includes("check-architecture"),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const checkAppValidate = {
|
|
164
|
+
run_id: args.runId ?? null,
|
|
165
|
+
phase: "validate",
|
|
166
|
+
command: "check-app",
|
|
167
|
+
command_mirror: true,
|
|
168
|
+
campaign_id: args.campaignId,
|
|
169
|
+
iteration: args.iteration,
|
|
170
|
+
intent: context.questions?.check_app ?? "Fallow false-positive triage for app runtime surfaces",
|
|
171
|
+
answer: checkAppAgents.some((a) => a.answer === "partial")
|
|
172
|
+
? "partial"
|
|
173
|
+
: checkAppAgents.every((a) => a.answer === "yes")
|
|
174
|
+
? "yes"
|
|
175
|
+
: "partial",
|
|
176
|
+
confidence: {
|
|
177
|
+
overall: checkAppAgents.length >= 3 ? 0.85 : 0.6,
|
|
178
|
+
},
|
|
179
|
+
requirements_met: [
|
|
180
|
+
"Traced Fallow review/actionable paths against app entry points and workers",
|
|
181
|
+
`Recorded ${fpDeduped.length} false-positive paths for satisfaction scoring`,
|
|
182
|
+
`Protected ${protectedDeduped.length} paths from wave deletion`,
|
|
183
|
+
],
|
|
184
|
+
requirements_not_met: gaps.filter((g) => g.includes("check-app")),
|
|
185
|
+
protected_paths: protectedDeduped,
|
|
186
|
+
safe_to_fix_count: safeToFix.length,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const checkArchFitness = {
|
|
190
|
+
run_id: args.runId ?? null,
|
|
191
|
+
phase: "fitness_functions",
|
|
192
|
+
command: "check-architecture",
|
|
193
|
+
command_mirror: true,
|
|
194
|
+
campaign_id: args.campaignId,
|
|
195
|
+
iteration: args.iteration,
|
|
196
|
+
intent: context.questions?.check_architecture ?? "Fallow deletion impact on architecture",
|
|
197
|
+
answer: archAgents.some((a) => a.answer === "no") ? "no" : "partial",
|
|
198
|
+
criteria: [
|
|
199
|
+
"No wave may delete protected_paths or false_positive registry entries",
|
|
200
|
+
"Dupes consolidation must preserve main/worker SSOT pairs until shared extract exists",
|
|
201
|
+
"Boundary violations are fix targets; barrel facades default to review not delete",
|
|
202
|
+
],
|
|
203
|
+
violations: findings.filter((f) => f.includes("boundary") || f.includes("check-architecture")),
|
|
204
|
+
protected_paths: protectedDeduped,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const protectedYaml = {
|
|
208
|
+
campaign_id: args.campaignId,
|
|
209
|
+
iteration: args.iteration,
|
|
210
|
+
updated_at: isoNow(),
|
|
211
|
+
protected_paths: protectedDeduped,
|
|
212
|
+
do_not_delete: doNotDeleteDeduped.map((d) => d.path),
|
|
213
|
+
false_positive_paths: fpDeduped.map((f) => f.path),
|
|
214
|
+
wave_exclude_block: protectedDeduped,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
function writeArtifact(runId, name, data) {
|
|
218
|
+
if (!runId) return;
|
|
219
|
+
const dir = path.join(runDir(runId), "artifacts");
|
|
220
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
221
|
+
const ext = name.endsWith(".yaml") ? ".yaml" : ".json";
|
|
222
|
+
if (ext === ".json") {
|
|
223
|
+
writeJson(path.join(dir, name), data);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const yaml = `campaign_id: ${args.campaignId}\niteration: ${args.iteration}\nupdated_at: ${isoNow()}\n\nprotected_paths:\n${protectedDeduped.map((p) => ` - ${p}`).join("\n") || " []"}\n\ndo_not_delete:\n${doNotDeleteDeduped.map((d) => ` - path: ${d.path}\n reason: ${JSON.stringify(d.reason)}`).join("\n") || " []"}\n`;
|
|
227
|
+
fs.writeFileSync(path.join(dir, name), yaml);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
writeJson(path.join(iterDir, "protected-paths.json"), protectedYaml);
|
|
231
|
+
writeJson(path.join(iterDir, "check-app-validate.json"), checkAppValidate);
|
|
232
|
+
writeJson(path.join(iterDir, "check-architecture-fitness.json"), checkArchFitness);
|
|
233
|
+
|
|
234
|
+
if (args.runId) {
|
|
235
|
+
const artDir = path.join(runDir(args.runId), "artifacts");
|
|
236
|
+
fs.mkdirSync(artDir, { recursive: true });
|
|
237
|
+
writeJson(path.join(artDir, "check_app_validate.yaml"), checkAppValidate);
|
|
238
|
+
writeJson(path.join(artDir, "check_architecture_fitness.yaml"), checkArchFitness);
|
|
239
|
+
writeJson(path.join(artDir, "check_swarm_merge.json"), merge);
|
|
240
|
+
const yaml = `campaign_id: ${args.campaignId}\niteration: ${args.iteration}\nupdated_at: ${isoNow()}\n\nprotected_paths:\n${protectedDeduped.map((p) => ` - ${p}`).join("\n") || " []"}\n\ndo_not_delete:\n${doNotDeleteDeduped.map((d) => ` - path: ${d.path}\n reason: ${JSON.stringify(d.reason)}`).join("\n") || " []"}\n`;
|
|
241
|
+
fs.writeFileSync(path.join(artDir, "protected_paths.yaml"), yaml);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fs.appendFileSync(
|
|
245
|
+
path.join(campaignDir, "journal.md"),
|
|
246
|
+
`\n- **Check swarm merged** iter ${args.iteration}: agents=${agents.length}, fp_recorded=${fpDeduped.length}, protected=${protectedDeduped.length}, actionable_after=${classification.summary?.actionable_total ?? "?"}\n`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
console.log(
|
|
250
|
+
JSON.stringify({
|
|
251
|
+
ok: true,
|
|
252
|
+
merge_path: path.join(iterDir, "check-swarm-merge.json"),
|
|
253
|
+
false_positives_recorded: fpDeduped.length,
|
|
254
|
+
protected_paths: protectedDeduped.length,
|
|
255
|
+
actionable_total: classification.summary?.actionable_total,
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build plan_waves.yaml from dispatch-queue.yaml (scriptable plan_waves phase).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node plan-waves-from-queue.mjs --campaign-id <id> --run-id <run_id> [--iteration <n>]
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import {
|
|
11
|
+
campaignDir,
|
|
12
|
+
loadCampaign,
|
|
13
|
+
runArtifactsDir,
|
|
14
|
+
} from "./lib/runner-state.mjs";
|
|
15
|
+
import {
|
|
16
|
+
buildPlanWavesYaml,
|
|
17
|
+
copyFileIfExists,
|
|
18
|
+
journal,
|
|
19
|
+
parseDispatchQueueYaml,
|
|
20
|
+
writeRunArtifact,
|
|
21
|
+
} from "./lib/runner-exec.mjs";
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const out = { campaignId: null, runId: null, iteration: null };
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const a = argv[i];
|
|
27
|
+
if (a === "--campaign-id") out.campaignId = argv[++i];
|
|
28
|
+
else if (a === "--run-id") out.runId = argv[++i];
|
|
29
|
+
else if (a === "--iteration") out.iteration = Number(argv[++i]);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const args = parseArgs(process.argv.slice(2));
|
|
35
|
+
if (!args.campaignId || !args.runId) {
|
|
36
|
+
console.error("plan-waves-from-queue: --campaign-id and --run-id required");
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const campaign = loadCampaign(args.campaignId);
|
|
41
|
+
if (!campaign) {
|
|
42
|
+
console.error(`Campaign not found: ${args.campaignId}`);
|
|
43
|
+
process.exit(2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const iteration = args.iteration ?? campaign.iteration;
|
|
47
|
+
const queuePath = path.join(campaignDir(args.campaignId), "dispatch-queue.yaml");
|
|
48
|
+
if (!fs.existsSync(queuePath)) {
|
|
49
|
+
console.error(`Missing dispatch-queue.yaml at ${queuePath}`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const queueText = fs.readFileSync(queuePath, "utf8");
|
|
54
|
+
let waves = parseDispatchQueueYaml(queueText);
|
|
55
|
+
const maxWaves = campaign.config?.max_waves_per_iteration ?? 3;
|
|
56
|
+
waves = waves.slice(0, maxWaves);
|
|
57
|
+
|
|
58
|
+
if (waves.length === 0) {
|
|
59
|
+
console.error("dispatch-queue.yaml has no waves");
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const yaml = buildPlanWavesYaml({ campaign: { ...campaign, iteration }, waves });
|
|
64
|
+
const campaignPlan = path.join(campaignDir(args.campaignId), "artifacts", "plan_waves.yaml");
|
|
65
|
+
const runPlan = path.join(runArtifactsDir(args.runId), "plan_waves.yaml");
|
|
66
|
+
fs.mkdirSync(path.dirname(campaignPlan), { recursive: true });
|
|
67
|
+
fs.writeFileSync(campaignPlan, yaml);
|
|
68
|
+
fs.mkdirSync(path.dirname(runPlan), { recursive: true });
|
|
69
|
+
fs.writeFileSync(runPlan, yaml);
|
|
70
|
+
|
|
71
|
+
journal(
|
|
72
|
+
args.campaignId,
|
|
73
|
+
`- **Plan waves** iter ${iteration}: ${waves.length} wave(s) from dispatch-queue.yaml`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
console.log(
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
ok: true,
|
|
79
|
+
campaign_id: args.campaignId,
|
|
80
|
+
iteration,
|
|
81
|
+
wave_count: waves.length,
|
|
82
|
+
plan_path: runPlan,
|
|
83
|
+
waves: waves.map((w) => ({ index: w.index, command: w.command, priority: w.priority })),
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build readonly check_swarm context from Fallow scan + classification.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node prepare-check-context.mjs --campaign-id <id> --iteration <n> [--run-id <run_id>]
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { REPO_ROOT, isoNow, readJson, writeJson, runDir } from "../run-engine/lib.mjs";
|
|
11
|
+
import {
|
|
12
|
+
summarizeDeadCode,
|
|
13
|
+
summarizeDupes,
|
|
14
|
+
summarizeHealth,
|
|
15
|
+
} from "./lib/fallow-metrics.mjs";
|
|
16
|
+
|
|
17
|
+
const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
|
|
18
|
+
const FRONTEND_ROOT = path.join(REPO_ROOT, "frontend");
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const out = { campaignId: null, iteration: 0, runId: null };
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
if (a === "--campaign-id") out.campaignId = argv[++i];
|
|
25
|
+
else if (a === "--iteration") out.iteration = Number(argv[++i]);
|
|
26
|
+
else if (a === "--run-id") out.runId = argv[++i];
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadLayer(iterDir, file, summarize) {
|
|
32
|
+
const p = path.join(iterDir, file);
|
|
33
|
+
if (!fs.existsSync(p)) return null;
|
|
34
|
+
const payload = readJson(p, {});
|
|
35
|
+
return payload._remediation?.summary ?? summarize(payload);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function topCloneGroups(dupesPayload, limit = 15) {
|
|
39
|
+
const groups = dupesPayload?.clone_groups;
|
|
40
|
+
if (!Array.isArray(groups)) return [];
|
|
41
|
+
return [...groups]
|
|
42
|
+
.sort((a, b) => (b.token_count ?? 0) - (a.token_count ?? 0))
|
|
43
|
+
.slice(0, limit)
|
|
44
|
+
.map((g) => ({
|
|
45
|
+
token_count: g.token_count,
|
|
46
|
+
line_count: g.line_count,
|
|
47
|
+
instances: (g.instances ?? []).map((i) => ({
|
|
48
|
+
file: i.file,
|
|
49
|
+
start_line: i.start_line,
|
|
50
|
+
end_line: i.end_line,
|
|
51
|
+
})),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function inventoryByClassification(classification) {
|
|
56
|
+
const buckets = { true_positive: [], review: [], false_positive: [] };
|
|
57
|
+
for (const item of classification?.inventory ?? []) {
|
|
58
|
+
const bucket = buckets[item.classification] ?? buckets.review;
|
|
59
|
+
bucket.push({
|
|
60
|
+
id: item.id,
|
|
61
|
+
category: item.category,
|
|
62
|
+
path: item.path,
|
|
63
|
+
export_name: item.export_name,
|
|
64
|
+
reason: item.reason,
|
|
65
|
+
rule_id: item.rule_id,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return buckets;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const args = parseArgs(process.argv.slice(2));
|
|
72
|
+
if (!args.campaignId) {
|
|
73
|
+
console.error("prepare-check-context: --campaign-id required");
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const campaignDir = path.join(CAMPAIGNS_ROOT, args.campaignId);
|
|
78
|
+
const iterDir = path.join(campaignDir, "iterations", String(args.iteration));
|
|
79
|
+
const campaign = readJson(path.join(campaignDir, "campaign.json"), {});
|
|
80
|
+
|
|
81
|
+
const deadScan = readJson(path.join(iterDir, "fallow-scan.json"), null);
|
|
82
|
+
const dupesScan = readJson(path.join(iterDir, "fallow-dupes.json"), null);
|
|
83
|
+
const healthScan = readJson(path.join(iterDir, "fallow-health.json"), null);
|
|
84
|
+
const classification = readJson(path.join(iterDir, "fallow-classification.json"), null);
|
|
85
|
+
const registry = readJson(path.join(campaignDir, "fallow-false-positives.json"), { entries: [] });
|
|
86
|
+
|
|
87
|
+
if (!deadScan) {
|
|
88
|
+
console.error("prepare-check-context: fallow-scan.json missing — run fallow-scan.mjs first");
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const inventory = inventoryByClassification(classification);
|
|
93
|
+
const context = {
|
|
94
|
+
prepared_at: isoNow(),
|
|
95
|
+
campaign_id: args.campaignId,
|
|
96
|
+
iteration: args.iteration,
|
|
97
|
+
scope: campaign.scope ?? "whole-repo",
|
|
98
|
+
root: FRONTEND_ROOT,
|
|
99
|
+
questions: {
|
|
100
|
+
check_app:
|
|
101
|
+
"Which Fallow unused files, exports, and class members are live runtime surfaces (workers, hooks, barrels, lazy routes, provider interfaces) and must NOT be deleted?",
|
|
102
|
+
check_architecture:
|
|
103
|
+
"Which dead-code removals or dupes consolidations would break layer boundaries, SSOT, import graphs, or main/worker pairs?",
|
|
104
|
+
},
|
|
105
|
+
fallow: {
|
|
106
|
+
dead_code: loadLayer(iterDir, "fallow-scan.json", summarizeDeadCode),
|
|
107
|
+
dupes: loadLayer(iterDir, "fallow-dupes.json", summarizeDupes),
|
|
108
|
+
health: loadLayer(iterDir, "fallow-health.json", summarizeHealth),
|
|
109
|
+
classification_summary: classification?.summary ?? null,
|
|
110
|
+
inventory,
|
|
111
|
+
top_review_for_trace: inventory.review.slice(0, 25),
|
|
112
|
+
top_actionable: inventory.true_positive.slice(0, 40),
|
|
113
|
+
},
|
|
114
|
+
dupes_top_groups: topCloneGroups(dupesScan),
|
|
115
|
+
registry_entry_count: registry.entries?.length ?? 0,
|
|
116
|
+
registry_paths: (registry.entries ?? []).map((e) => e.path),
|
|
117
|
+
trace_commands: {
|
|
118
|
+
trace_file: "fallow dead-code --format json --quiet --trace-file <path>",
|
|
119
|
+
trace_export: "fallow dead-code --format json --quiet --trace <path>:<export>",
|
|
120
|
+
trace_clone: "fallow dupes --format json --quiet --trace <path>:<line>",
|
|
121
|
+
},
|
|
122
|
+
agent_specs: {
|
|
123
|
+
check_app: [
|
|
124
|
+
".cursor/agents/remediation-check-app-inventory.md",
|
|
125
|
+
".cursor/agents/remediation-check-app-ssot.md",
|
|
126
|
+
".cursor/agents/remediation-check-app-trace.md",
|
|
127
|
+
],
|
|
128
|
+
check_architecture: [
|
|
129
|
+
".cursor/agents/remediation-check-architecture-boundaries.md",
|
|
130
|
+
".cursor/agents/remediation-check-architecture-deps.md",
|
|
131
|
+
".cursor/agents/remediation-check-architecture-decomposition.md",
|
|
132
|
+
],
|
|
133
|
+
check_risk: ".cursor/agents/remediation-check-risk.md",
|
|
134
|
+
},
|
|
135
|
+
merge_script:
|
|
136
|
+
"node .cursor/aaac/scripts/remediation/merge-check-swarm.mjs --campaign-id <id> --iteration <n>",
|
|
137
|
+
record_fp_script:
|
|
138
|
+
"node .cursor/aaac/scripts/remediation/record-fallow-fp.mjs --campaign-id <id> --from-json <file>",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const outPath = path.join(iterDir, "check-context.json");
|
|
142
|
+
writeJson(outPath, context);
|
|
143
|
+
|
|
144
|
+
if (args.runId) {
|
|
145
|
+
writeJson(path.join(runDir(args.runId), "artifacts", "check_context.json"), context);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(JSON.stringify({ ok: true, path: outPath, actionable: inventory.true_positive.length, review: inventory.review.length }));
|