@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.
Files changed (79) hide show
  1. package/package.json +9 -9
  2. package/src/cli.mjs +0 -0
  3. package/src/run-engine/debug-run.mjs +0 -0
  4. package/src/run-engine/log-dump.mjs +0 -0
  5. package/src/run-engine/log-trace.mjs +0 -0
  6. package/templates/cursor/aaac/enforcement.json +84 -3
  7. package/templates/cursor/aaac/graph.project.yaml +28 -0
  8. package/templates/cursor/aaac/lifecycle/lifecycle.json +14 -0
  9. package/templates/cursor/aaac/lifecycle/phases.json +7 -1
  10. package/templates/cursor/aaac/ontology.json +1 -0
  11. package/templates/cursor/aaac/project.config.json +36 -0
  12. package/templates/cursor/aaac/scripts/remediation/auto-check-swarm-synthesis.mjs +75 -0
  13. package/templates/cursor/aaac/scripts/remediation/auto-dispatch-queue-from-health.mjs +78 -0
  14. package/templates/cursor/aaac/scripts/remediation/bootstrap-autonomous.mjs +113 -0
  15. package/templates/cursor/aaac/scripts/remediation/capture-verify-baseline.mjs +66 -0
  16. package/templates/cursor/aaac/scripts/remediation/capture-wave-snapshot.mjs +79 -0
  17. package/templates/cursor/aaac/scripts/remediation/check-swarm-raw.template.json +26 -0
  18. package/templates/cursor/aaac/scripts/remediation/classify-fallow-issues.mjs +77 -0
  19. package/templates/cursor/aaac/scripts/remediation/classify-verify-failure.mjs +176 -0
  20. package/templates/cursor/aaac/scripts/remediation/compute-satisfaction.mjs +344 -0
  21. package/templates/cursor/aaac/scripts/remediation/debt-sweep-gate.mjs +202 -0
  22. package/templates/cursor/aaac/scripts/remediation/dispatch-rules.json +44 -0
  23. package/templates/cursor/aaac/scripts/remediation/fallow-fp-rules.json +87 -0
  24. package/templates/cursor/aaac/scripts/remediation/fallow-scan.mjs +219 -0
  25. package/templates/cursor/aaac/scripts/remediation/handle-yield.mjs +240 -0
  26. package/templates/cursor/aaac/scripts/remediation/init-campaign.mjs +211 -0
  27. package/templates/cursor/aaac/scripts/remediation/lib/autonomous-mode.mjs +63 -0
  28. package/templates/cursor/aaac/scripts/remediation/lib/campaign-focus.mjs +87 -0
  29. package/templates/cursor/aaac/scripts/remediation/lib/fallow-classifier.mjs +190 -0
  30. package/templates/cursor/aaac/scripts/remediation/lib/fallow-health-targets.mjs +56 -0
  31. package/templates/cursor/aaac/scripts/remediation/lib/fallow-metrics.mjs +119 -0
  32. package/templates/cursor/aaac/scripts/remediation/lib/invoke-cursor-agent.mjs +51 -0
  33. package/templates/cursor/aaac/scripts/remediation/lib/reconcile-run-manifest.mjs +41 -0
  34. package/templates/cursor/aaac/scripts/remediation/lib/regression-analysis.mjs +55 -0
  35. package/templates/cursor/aaac/scripts/remediation/lib/remediation-config.mjs +69 -0
  36. package/templates/cursor/aaac/scripts/remediation/lib/remediation-progress.mjs +58 -0
  37. package/templates/cursor/aaac/scripts/remediation/lib/remediation-watch-loop.mjs +168 -0
  38. package/templates/cursor/aaac/scripts/remediation/lib/runner-exec.mjs +156 -0
  39. package/templates/cursor/aaac/scripts/remediation/lib/runner-state.mjs +145 -0
  40. package/templates/cursor/aaac/scripts/remediation/lib/verify-metrics.mjs +205 -0
  41. package/templates/cursor/aaac/scripts/remediation/merge-check-swarm.mjs +257 -0
  42. package/templates/cursor/aaac/scripts/remediation/plan-waves-from-queue.mjs +85 -0
  43. package/templates/cursor/aaac/scripts/remediation/prepare-check-context.mjs +148 -0
  44. package/templates/cursor/aaac/scripts/remediation/record-fallow-fp.mjs +107 -0
  45. package/templates/cursor/aaac/scripts/remediation/record-iteration-step.mjs +56 -0
  46. package/templates/cursor/aaac/scripts/remediation/remediation-cli.mjs +157 -0
  47. package/templates/cursor/aaac/scripts/remediation/remediation-cursor-watch.sh +10 -0
  48. package/templates/cursor/aaac/scripts/remediation/remediation-runner-daemon.sh +13 -0
  49. package/templates/cursor/aaac/scripts/remediation/remediation-runner.mjs +748 -0
  50. package/templates/cursor/aaac/scripts/remediation/remediation-yield-watcher.mjs +40 -0
  51. package/templates/cursor/aaac/scripts/remediation/remediator-gate.mjs +405 -0
  52. package/templates/cursor/aaac/scripts/remediation/repair-fallow-start-baseline.mjs +118 -0
  53. package/templates/cursor/aaac/scripts/remediation/runner-health-check.mjs +164 -0
  54. package/templates/cursor/aaac/scripts/remediation/satisfaction-loop-gate.mjs +286 -0
  55. package/templates/cursor/aaac/scripts/remediation/validate-campaign-complete.mjs +191 -0
  56. package/templates/cursor/aaac/scripts/remediation/verify-remediation-iteration.mjs +112 -0
  57. package/templates/cursor/aaac/scripts/run-engine/debug-run.mjs +0 -0
  58. package/templates/cursor/aaac/scripts/run-engine/log-dump.mjs +0 -0
  59. package/templates/cursor/aaac/scripts/run-engine/log-trace.mjs +0 -0
  60. package/templates/cursor/agents/remediation-check-app-inventory.md +32 -0
  61. package/templates/cursor/agents/remediation-check-app-ssot.md +24 -0
  62. package/templates/cursor/agents/remediation-check-app-trace.md +29 -0
  63. package/templates/cursor/agents/remediation-check-architecture-boundaries.md +21 -0
  64. package/templates/cursor/agents/remediation-check-architecture-decomposition.md +25 -0
  65. package/templates/cursor/agents/remediation-check-architecture-deps.md +23 -0
  66. package/templates/cursor/agents/remediation-check-risk.md +37 -0
  67. package/templates/cursor/agents/remediation-e2e-gate.md +30 -0
  68. package/templates/cursor/agents/remediation-remediator.md +69 -0
  69. package/templates/cursor/commands/remediate-app.md +212 -0
  70. package/templates/cursor/hooks/aaac-before-submit.sh +0 -0
  71. package/templates/cursor/hooks/aaac-pre-tool.sh +0 -0
  72. package/templates/cursor/hooks/aaac-stop.sh +0 -0
  73. package/templates/cursor/hooks/aaac-subagent-start.sh +0 -0
  74. package/templates/cursor/skills/shared/remediation/SKILL.md +51 -0
  75. package/templates/cursor/skills/shared/remediation/babysit/SKILL.md +223 -0
  76. package/templates/cursor/skills/shared/remediation/check-swarm/SKILL.md +114 -0
  77. package/templates/cursor/skills/shared/remediation/orchestrator/SKILL.md +275 -0
  78. package/templates/cursor/skills/shared/remediation/orchestrator/contract.yaml +116 -0
  79. package/templates/docs/agentic_architecture.md +1 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Autonomous mode resolution for /remediate-app.
3
+ * When true, orchestrator MUST use remediation-runner + babysit (not manual chat phases).
4
+ */
5
+
6
+ export const AUTONOMOUS_DEFAULTS = {
7
+ max_iterations_auto: 10,
8
+ satisfaction_threshold_auto: 100,
9
+ };
10
+
11
+ /**
12
+ * @param {string} intent
13
+ * @param {object} config - parsed intent config (threshold, max_iterations, …)
14
+ * @returns {{ autonomous: boolean, reason: string }}
15
+ */
16
+ export function resolveAutonomousMode(intent, config) {
17
+ const text = (intent ?? "").toLowerCase();
18
+
19
+ if (/\bautonomous\b/.test(text) || /\bauto[-_]?babysit\b/.test(text)) {
20
+ return { autonomous: true, reason: "intent_token_autonomous" };
21
+ }
22
+ if (/\bmanual\b/.test(text) || /\bno[-_]?autonomous\b/.test(text)) {
23
+ return { autonomous: false, reason: "intent_token_manual" };
24
+ }
25
+ if (config.satisfaction_threshold >= AUTONOMOUS_DEFAULTS.satisfaction_threshold_auto) {
26
+ return {
27
+ autonomous: true,
28
+ reason: `satisfaction_threshold_${config.satisfaction_threshold}`,
29
+ };
30
+ }
31
+ if (config.max_iterations >= AUTONOMOUS_DEFAULTS.max_iterations_auto) {
32
+ return {
33
+ autonomous: true,
34
+ reason: `max_iterations_${config.max_iterations}`,
35
+ };
36
+ }
37
+ return { autonomous: false, reason: "default_manual_orchestrator" };
38
+ }
39
+
40
+ export function applyAutonomousToConfig(config, intent) {
41
+ const { autonomous, reason } = resolveAutonomousMode(intent, config);
42
+ return {
43
+ ...config,
44
+ autonomous,
45
+ autonomous_reason: reason,
46
+ runner_mode: autonomous ? "shell_runner_yield_watcher" : "chat_orchestrator",
47
+ };
48
+ }
49
+
50
+ export const BABYSIT_SKILL = ".cursor/skills/shared/remediation/babysit/SKILL.md";
51
+
52
+ export function autonomousBootstrapCommands(runId, campaignId) {
53
+ return {
54
+ health: `node .cursor/aaac/scripts/remediation/runner-health-check.mjs --campaign-id ${campaignId}`,
55
+ cli_watch: `node .cursor/aaac/scripts/remediation/remediation-cli.mjs watch --run-id ${runId} --campaign-id ${campaignId}`,
56
+ cli_cursor: `node .cursor/aaac/scripts/remediation/remediation-cli.mjs cursor --run-id ${runId} --campaign-id ${campaignId}`,
57
+ cli_status: `node .cursor/aaac/scripts/remediation/remediation-cli.mjs status --run-id ${runId} --campaign-id ${campaignId}`,
58
+ yield_watcher: `node .cursor/aaac/scripts/remediation/remediation-yield-watcher.mjs --run-id ${runId} --campaign-id ${campaignId}`,
59
+ runner_until_yield: `node .cursor/aaac/scripts/remediation/remediation-runner.mjs --run-id ${runId} --campaign-id ${campaignId} --until-yield`,
60
+ runner_status: `node .cursor/aaac/scripts/remediation/remediation-runner.mjs --run-id ${runId} --campaign-id ${campaignId} --status`,
61
+ skill: BABYSIT_SKILL,
62
+ };
63
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Parse campaign intent into remediation focus constraints.
3
+ */
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { campaignDir, loadCampaign } from "./runner-state.mjs";
7
+
8
+ const DEFAULT_PROTECTED = [
9
+ "src/lib/views/hooks/useView.ts",
10
+ "src/lib/views/hooks/useWidgetOperations.ts",
11
+ "src/lib/views/hooks/useViewActionCallbacks.ts",
12
+ "src/lib/views/utils/LayoutSaveQueue.ts",
13
+ "src/operations/formula/evaluator.ts",
14
+ "src/operations/formula/dsl.ts",
15
+ ];
16
+
17
+ export function normalizeRepoPath(p) {
18
+ return (p ?? "").replace(/^frontend\//, "").replace(/^\//, "").trim();
19
+ }
20
+
21
+ export function parseCampaignFocus(intent = "") {
22
+ const text = (intent ?? "").toLowerCase();
23
+ const healthFocus =
24
+ /health\s*functions?\s*>\s*60\s*loc/.test(text) ||
25
+ /functions?\s*>\s*60\s*loc/.test(text) ||
26
+ /focus:\s*health/.test(text) ||
27
+ text.includes("health functions");
28
+
29
+ return {
30
+ intent_raw: intent,
31
+ health_functions_above_60_loc: healthFocus,
32
+ primary_metric: healthFocus ? "functions_above_threshold" : "health_score",
33
+ wave_command: "fix-module",
34
+ defer_high_fan_in: true,
35
+ max_function_loc: 60,
36
+ };
37
+ }
38
+
39
+ export function loadProtectedPaths(campaignId) {
40
+ const paths = new Set(DEFAULT_PROTECTED.map(normalizeRepoPath));
41
+
42
+ for (const rel of ["artifacts/protected_paths.yaml", "dispatch-queue.yaml"]) {
43
+ const filePath = path.join(campaignDir(campaignId), rel);
44
+ if (!fs.existsSync(filePath)) continue;
45
+ let inProtected = false;
46
+ for (const line of fs.readFileSync(filePath, "utf8").split("\n")) {
47
+ if (line.trim() === "protected_paths:") {
48
+ inProtected = true;
49
+ continue;
50
+ }
51
+ const m = line.match(/^\s*-\s+(.+)/);
52
+ if (inProtected && m) paths.add(normalizeRepoPath(m[1].trim()));
53
+ if (inProtected && line.trim() && !line.startsWith(" ") && !m) inProtected = false;
54
+ }
55
+ }
56
+
57
+ return [...paths];
58
+ }
59
+
60
+ export function loadCampaignContext(campaignId) {
61
+ const campaign = loadCampaign(campaignId);
62
+ if (!campaign) return null;
63
+ return {
64
+ campaign,
65
+ focus: parseCampaignFocus(campaign.intent),
66
+ protected_paths: loadProtectedPaths(campaignId),
67
+ threshold: campaign.config?.satisfaction_threshold ?? 85,
68
+ max_iterations: campaign.config?.max_iterations ?? 5,
69
+ scope: campaign.scope ?? "frontend",
70
+ };
71
+ }
72
+
73
+ export function isGoalAchieved(campaign, satisfaction = null) {
74
+ const threshold = campaign.config?.satisfaction_threshold ?? 85;
75
+ const score = satisfaction?.score ?? campaign.current?.satisfaction_score ?? 0;
76
+ if (score < threshold) return false;
77
+ if (campaign.status === "complete" || campaign.status === "satisfied") return true;
78
+ if (satisfaction) {
79
+ const verifyOk =
80
+ satisfaction.e2e_pass !== false &&
81
+ satisfaction.vitest_pass !== false &&
82
+ satisfaction.typecheck_pass !== false &&
83
+ satisfaction.build_pass !== false;
84
+ if (!verifyOk) return false;
85
+ }
86
+ return score >= threshold;
87
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Classify Fallow scan issues: true_positive | false_positive | review
3
+ * Bridges swarm/check-risk knowledge into remediation metrics (SSOT).
4
+ */
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { REPO_ROOT, readJson } from "../../run-engine/lib.mjs";
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const FRONTEND_ROOT = path.join(REPO_ROOT, "frontend");
12
+ const RULES_PATH = path.join(__dirname, "..", "fallow-fp-rules.json");
13
+
14
+ const ISSUE_ARRAYS = [
15
+ "unused_files",
16
+ "unused_exports",
17
+ "unused_types",
18
+ "unused_dependencies",
19
+ "unused_enum_members",
20
+ "unused_class_members",
21
+ "unresolved_imports",
22
+ "duplicate_exports",
23
+ "circular_dependencies",
24
+ "boundary_violations",
25
+ ];
26
+
27
+ export function loadFpRules() {
28
+ return readJson(RULES_PATH, { path_globs: [], path_regex: [], issue_heuristics: {}, scoring: {} });
29
+ }
30
+
31
+ function loadFallowrcGlobs() {
32
+ const rc = readJson(path.join(FRONTEND_ROOT, ".fallowrc.json"), {});
33
+ return Array.isArray(rc.dynamicallyLoaded) ? rc.dynamicallyLoaded : [];
34
+ }
35
+
36
+ function globToRegex(glob) {
37
+ const normalized = glob.replace(/^frontend\//, "").replace(/^\//, "");
38
+ const escaped = normalized
39
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
40
+ .replace(/\*\*/g, "§§")
41
+ .replace(/\*/g, "[^/]*")
42
+ .replace(/§§/g, ".*");
43
+ return new RegExp(`^${escaped}$`);
44
+ }
45
+
46
+ function normalizePath(p) {
47
+ return (p ?? "").replace(/^frontend\//, "").replace(/^\//, "");
48
+ }
49
+
50
+ function pathMatchesGlob(filePath, glob) {
51
+ const norm = normalizePath(filePath);
52
+ const g = glob.replace(/^frontend\//, "");
53
+ if (g.includes("*")) return globToRegex(g).test(norm);
54
+ return norm === g || norm.endsWith(`/${g}`);
55
+ }
56
+
57
+ function pathMatchesAnyGlob(filePath, globs) {
58
+ return globs.some((g) => pathMatchesGlob(filePath, g));
59
+ }
60
+
61
+ function issueKey(category, issue) {
62
+ const p = normalizePath(issue.path ?? issue.file ?? "");
63
+ const name = issue.export_name ?? issue.name ?? issue.dependency ?? issue.symbol ?? "";
64
+ const line = issue.line ?? "";
65
+ return `${category}:${p}:${name}:${line}`;
66
+ }
67
+
68
+ function loadCampaignRegistry(campaignDir) {
69
+ const jsonPath = path.join(campaignDir, "fallow-false-positives.json");
70
+ if (fs.existsSync(jsonPath)) {
71
+ return readJson(jsonPath, { entries: [] });
72
+ }
73
+ return { version: 1, entries: [] };
74
+ }
75
+
76
+ function registryLookup(registry, key, filePath, exportName) {
77
+ for (const entry of registry.entries ?? []) {
78
+ if (entry.id === key) return entry;
79
+ if (entry.path && normalizePath(entry.path) === normalizePath(filePath)) {
80
+ if (!entry.export_name || entry.export_name === exportName) return entry;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ function classifyByRules(category, issue, rules, dynamicGlobs) {
87
+ const filePath = normalizePath(issue.path ?? issue.file ?? "");
88
+
89
+ for (const rule of rules.path_globs ?? []) {
90
+ const globs =
91
+ rule.glob === "from_fallowrc_dynamicallyLoaded" ? dynamicGlobs : [rule.glob];
92
+ if (pathMatchesAnyGlob(filePath, globs)) {
93
+ return { classification: rule.classification, reason: rule.reason, rule_id: rule.id };
94
+ }
95
+ }
96
+
97
+ for (const rule of rules.path_regex ?? []) {
98
+ if (new RegExp(rule.pattern).test(filePath)) {
99
+ return { classification: rule.classification, reason: rule.reason, rule_id: rule.id };
100
+ }
101
+ }
102
+
103
+ const heuristics = rules.issue_heuristics?.[category] ?? [];
104
+ for (const h of heuristics) {
105
+ if (h.path_suffix && !filePath.endsWith(h.path_suffix.replace(/^\//, ""))) continue;
106
+ if (h.is_re_export != null && issue.is_re_export !== h.is_re_export) continue;
107
+ if (h.action_note_contains) {
108
+ const notes = (issue.actions ?? []).map((a) => a.note ?? "").join(" ");
109
+ if (!notes.includes(h.action_note_contains)) continue;
110
+ }
111
+ return { classification: h.classification, reason: h.reason, rule_id: h.id };
112
+ }
113
+
114
+ return { classification: "true_positive", reason: "fallow_reported_unused", rule_id: null };
115
+ }
116
+
117
+ export function classifyFallowScan({ scan, campaignDir, rules = loadFpRules() }) {
118
+ const dynamicGlobs = loadFallowrcGlobs();
119
+ const registry = campaignDir ? loadCampaignRegistry(campaignDir) : { entries: [] };
120
+ const inventory = [];
121
+ const counts = { true_positive: 0, false_positive: 0, review: 0 };
122
+
123
+ for (const category of ISSUE_ARRAYS) {
124
+ const items = scan[category];
125
+ if (!Array.isArray(items)) continue;
126
+
127
+ for (const issue of items) {
128
+ const key = issueKey(category, issue);
129
+ const filePath = normalizePath(issue.path ?? issue.file ?? "");
130
+ const exportName = issue.export_name ?? null;
131
+ const manual = registryLookup(registry, key, filePath, exportName);
132
+
133
+ let result;
134
+ if (manual) {
135
+ result = {
136
+ classification: manual.classification ?? "false_positive",
137
+ reason: manual.reason ?? "campaign_registry",
138
+ rule_id: manual.source ?? "manual",
139
+ source: "campaign_registry",
140
+ };
141
+ } else {
142
+ const ruled = classifyByRules(category, issue, rules, dynamicGlobs);
143
+ result = { ...ruled, source: "fallow-fp-rules" };
144
+ }
145
+
146
+ counts[result.classification] = (counts[result.classification] ?? 0) + 1;
147
+ inventory.push({
148
+ id: key,
149
+ category,
150
+ path: filePath,
151
+ export_name: exportName,
152
+ classification: result.classification,
153
+ reason: result.reason,
154
+ rule_id: result.rule_id,
155
+ source: result.source,
156
+ is_re_export: issue.is_re_export ?? null,
157
+ });
158
+ }
159
+ }
160
+
161
+ const rawTotal = scan.total_issues ?? scan.summary?.total_issues ?? inventory.length;
162
+ const actionable = (counts.true_positive ?? 0) + (counts.review ?? 0);
163
+ const excluded = counts.false_positive ?? 0;
164
+
165
+ return {
166
+ classified_at: new Date().toISOString(),
167
+ raw_total: rawTotal,
168
+ inventory_count: inventory.length,
169
+ counts,
170
+ actionable_total: actionable,
171
+ excluded_false_positives: excluded,
172
+ actionable_classifications: rules.scoring?.actionable_classifications ?? [
173
+ "true_positive",
174
+ "review",
175
+ ],
176
+ inventory,
177
+ summary: {
178
+ raw_total: rawTotal,
179
+ actionable_total: actionable,
180
+ false_positive_total: excluded,
181
+ review_total: counts.review ?? 0,
182
+ true_positive_total: counts.true_positive ?? 0,
183
+ },
184
+ };
185
+ }
186
+
187
+ export function resolveActionableBaseline(campaignDir) {
188
+ const p = path.join(campaignDir, "fallow-start-actionable-baseline.json");
189
+ return readJson(p, null);
190
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Fetch ranked Fallow health decomposition targets for wave planning.
3
+ */
4
+ import { spawnSync } from "child_process";
5
+ import path from "path";
6
+ import { REPO_ROOT } from "../../run-engine/lib.mjs";
7
+ import { normalizeRepoPath } from "./campaign-focus.mjs";
8
+
9
+ const HIGH_FAN_IN_DEFER = new Set([
10
+ "src/operations/formula/evaluator.ts",
11
+ "src/operations/formula/dsl.ts",
12
+ ]);
13
+
14
+ export function fetchHealthTargets({ scope = "frontend", limit = 12 } = {}) {
15
+ const cwd = scope === "frontend" ? path.join(REPO_ROOT, "frontend") : REPO_ROOT;
16
+ const npx = spawnSync("npx", ["fallow", "health", "--score", "--targets", "--format", "json", "--quiet"], {
17
+ cwd,
18
+ encoding: "utf8",
19
+ });
20
+
21
+ try {
22
+ const parsed = JSON.parse(npx.stdout || "{}");
23
+ const targets = (parsed.targets ?? []).map((t) => ({
24
+ ...t,
25
+ path: normalizeRepoPath(t.path),
26
+ }));
27
+ return {
28
+ score: parsed.health_score?.score ?? parsed.score ?? null,
29
+ targets: targets.slice(0, limit),
30
+ };
31
+ } catch {
32
+ return { score: null, targets: [] };
33
+ }
34
+ }
35
+
36
+ export function filterTargetsForWaves(targets, { protected_paths = [], defer_high_fan_in = true } = {}) {
37
+ const protectedSet = new Set(protected_paths.map(normalizeRepoPath));
38
+ return targets.filter((t) => {
39
+ const p = normalizeRepoPath(t.path);
40
+ if (protectedSet.has(p)) return false;
41
+ if (defer_high_fan_in && HIGH_FAN_IN_DEFER.has(p)) return false;
42
+ const fanIn = (t.factors ?? []).find((f) => f.metric === "fan_in")?.value;
43
+ if (defer_high_fan_in && fanIn != null && fanIn >= 10) return false;
44
+ return true;
45
+ });
46
+ }
47
+
48
+ export function targetToWaveIntent(target) {
49
+ const rec = target.recommendation ?? target.actions?.[0]?.description ?? "";
50
+ const filePath = normalizeRepoPath(target.path);
51
+ const fn = target.evidence?.complex_functions?.[0]?.name;
52
+ if (fn) {
53
+ return `Extract ${fn} from ${filePath} into focused modules; keep every function under 60 LOC; preserve exports`;
54
+ }
55
+ return `Health decompose ${filePath}: ${rec}`.slice(0, 260);
56
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Shared Fallow command runners and metric summarizers for remediation campaigns.
3
+ */
4
+ import { spawnSync } from "child_process";
5
+ import path from "path";
6
+ import { REPO_ROOT, readJson } from "../../run-engine/lib.mjs";
7
+ import { loadRemediationConfig } from "./remediation-config.mjs";
8
+
9
+ export function getScanRoot() {
10
+ const config = loadRemediationConfig();
11
+ return path.resolve(REPO_ROOT, config.scan_root ?? config.fallow_cwd ?? ".");
12
+ }
13
+
14
+ /** @deprecated use getScanRoot() */
15
+ export const FRONTEND_ROOT = getScanRoot();
16
+ export const FALLOW_BUFFER = 100 * 1024 * 1024;
17
+
18
+ export function runFallow(subcommand, extraArgs = [], cwd = getScanRoot()) {
19
+ const args = [subcommand, "--format", "json", "--quiet", "--explain", ...extraArgs];
20
+ const result = spawnSync("fallow", args, {
21
+ cwd,
22
+ encoding: "utf8",
23
+ maxBuffer: FALLOW_BUFFER,
24
+ });
25
+
26
+ let payload;
27
+ if (result.stdout?.trim()) {
28
+ try {
29
+ payload = JSON.parse(result.stdout);
30
+ } catch (e) {
31
+ return {
32
+ ok: false,
33
+ exit_code: 2,
34
+ error: true,
35
+ message: `invalid JSON from fallow ${subcommand}: ${e.message}`,
36
+ payload: null,
37
+ };
38
+ }
39
+ } else {
40
+ payload = {
41
+ error: true,
42
+ message: result.stderr || `fallow ${subcommand} produced no output`,
43
+ exit_code: result.status,
44
+ };
45
+ }
46
+
47
+ return {
48
+ ok: result.status !== 2,
49
+ exit_code: result.status,
50
+ payload,
51
+ };
52
+ }
53
+
54
+ export function summarizeDeadCode(payload) {
55
+ const s = payload?.summary ?? payload?.totals ?? payload ?? {};
56
+ return {
57
+ total_issues: s.total_issues ?? payload?.total_issues ?? 0,
58
+ unused_files: s.unused_files ?? (payload?.unused_files?.length ?? 0),
59
+ unused_exports: s.unused_exports ?? (payload?.unused_exports?.length ?? 0),
60
+ unused_dependencies: s.unused_dependencies ?? (payload?.unused_dependencies?.length ?? 0),
61
+ circular_dependencies: s.circular_dependencies ?? (payload?.circular_dependencies?.length ?? 0),
62
+ unresolved_imports: s.unresolved_imports ?? (payload?.unresolved_imports?.length ?? 0),
63
+ duplicate_exports: s.duplicate_exports ?? (payload?.duplicate_exports?.length ?? 0),
64
+ boundary_violations: s.boundary_violations ?? (payload?.boundary_violations?.length ?? 0),
65
+ elapsed_ms: payload?.elapsed_ms ?? null,
66
+ };
67
+ }
68
+
69
+ export function summarizeDupes(payload) {
70
+ const stats = payload?.stats ?? {};
71
+ return {
72
+ clone_groups: stats.clone_groups ?? (payload?.clone_groups?.length ?? 0),
73
+ clone_instances: stats.clone_instances ?? 0,
74
+ files_with_clones: stats.files_with_clones ?? 0,
75
+ total_files: stats.total_files ?? 0,
76
+ duplicated_lines: stats.duplicated_lines ?? 0,
77
+ duplicated_tokens: stats.duplicated_tokens ?? 0,
78
+ duplication_percentage: stats.duplication_percentage ?? 0,
79
+ elapsed_ms: payload?.elapsed_ms ?? null,
80
+ };
81
+ }
82
+
83
+ export function summarizeHealth(payload) {
84
+ const summary = payload?.summary ?? {};
85
+ const healthScore = payload?.health_score ?? {};
86
+ return {
87
+ health_score: healthScore.score ?? null,
88
+ health_grade: healthScore.grade ?? null,
89
+ functions_analyzed: summary.functions_analyzed ?? 0,
90
+ functions_above_threshold: summary.functions_above_threshold ?? 0,
91
+ severity_critical_count: summary.severity_critical_count ?? 0,
92
+ severity_high_count: summary.severity_high_count ?? 0,
93
+ severity_moderate_count: summary.severity_moderate_count ?? 0,
94
+ findings_count: Array.isArray(payload?.findings) ? payload.findings.length : 0,
95
+ hotspots_count: Array.isArray(payload?.hotspots) ? payload.hotspots.length : 0,
96
+ elapsed_ms: payload?.elapsed_ms ?? null,
97
+ };
98
+ }
99
+
100
+ /** Lower-is-better metric: fraction reduced vs baseline (0–1). */
101
+ export function reductionRate(baseline, current) {
102
+ if (baseline == null || current == null) return null;
103
+ if (baseline <= 0) return current <= 0 ? 1 : 0;
104
+ return Math.max(0, Math.min(1, (baseline - current) / baseline));
105
+ }
106
+
107
+ /** Higher-is-better score on 0–100 scale: fraction of remaining headroom gained. */
108
+ export function improvementRate(baselineScore, currentScore, ceiling = 100) {
109
+ if (baselineScore == null || currentScore == null) return null;
110
+ const headroom = ceiling - baselineScore;
111
+ if (headroom <= 0) return currentScore >= ceiling ? 1 : 0;
112
+ return Math.max(0, Math.min(1, (currentScore - baselineScore) / headroom));
113
+ }
114
+
115
+ export function readStartBaseline(campaignDir, filename, field) {
116
+ const data = readJson(path.join(campaignDir, filename), null);
117
+ if (data?.[field] != null) return { value: data[field], source: filename, record: data };
118
+ return { value: null, source: "none", record: data };
119
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Invoke Cursor Agent non-interactively for yield handling.
3
+ */
4
+ import { spawnSync } from "child_process";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { REPO_ROOT, isoNow } from "../../run-engine/lib.mjs";
8
+
9
+ const DEFAULT_CURSOR_BIN = "/Applications/Cursor.app/Contents/Resources/app/bin/cursor";
10
+
11
+ export function resolveCursorBin() {
12
+ if (process.env.CURSOR_AGENT_BIN && fs.existsSync(process.env.CURSOR_AGENT_BIN)) {
13
+ return process.env.CURSOR_AGENT_BIN;
14
+ }
15
+ if (fs.existsSync(DEFAULT_CURSOR_BIN)) return DEFAULT_CURSOR_BIN;
16
+ const which = spawnSync("which", ["cursor"], { encoding: "utf8" });
17
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
18
+ return null;
19
+ }
20
+
21
+ export function invokeCursorAgent(prompt, opts = {}) {
22
+ const bin = resolveCursorBin();
23
+ if (!bin) {
24
+ return { ok: false, status: 127, stdout: "", stderr: "cursor agent binary not found" };
25
+ }
26
+
27
+ const cwd = opts.cwd ?? REPO_ROOT;
28
+ const timeoutMs = opts.timeoutMs ?? 900_000;
29
+ const result = spawnSync(bin, ["agent", "-p", "-f", "--approve-mcps", "--output-format", "text", prompt], {
30
+ cwd,
31
+ encoding: "utf8",
32
+ timeout: timeoutMs,
33
+ env: { ...process.env, CI: process.env.CI ?? "1" },
34
+ maxBuffer: 20 * 1024 * 1024,
35
+ });
36
+
37
+ if (opts.logPath) {
38
+ fs.mkdirSync(path.dirname(opts.logPath), { recursive: true });
39
+ fs.writeFileSync(
40
+ opts.logPath,
41
+ `# Cursor agent ${isoNow()}\n\n## Prompt\n${prompt}\n\n## Exit ${result.status}\n\n## Stdout\n${result.stdout ?? ""}\n\n## Stderr\n${result.stderr ?? ""}\n`,
42
+ );
43
+ }
44
+
45
+ return {
46
+ ok: (result.status ?? 1) === 0,
47
+ status: result.status ?? 1,
48
+ stdout: result.stdout ?? "",
49
+ stderr: result.stderr ?? "",
50
+ };
51
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Reconcile AAAC run manifest pending queue with remediation runner state.
3
+ * Prevents advance-phase from jumping to report when pending was drained by a prior chat session.
4
+ */
5
+ import path from "path";
6
+ import { isoNow, loadRunManifest, runDir, writeJson } from "../../run-engine/lib.mjs";
7
+ import { PHASES } from "./runner-state.mjs";
8
+
9
+ export function phasesFrom(phase) {
10
+ const idx = PHASES.indexOf(phase);
11
+ if (idx < 0) return [...PHASES];
12
+ return PHASES.slice(idx);
13
+ }
14
+
15
+ export function reconcileRemediationRun(runId, runnerState) {
16
+ const manifestPath = path.join(runDir(runId), "run.json");
17
+ const manifest = loadRunManifest(runId);
18
+ if (!manifest || manifest.command !== "remediate-app") {
19
+ return { ok: false, reason: "not_remediate_run" };
20
+ }
21
+
22
+ const targetPhase = runnerState?.phase ?? manifest.phase ?? "scan";
23
+ const pending = phasesFrom(targetPhase);
24
+
25
+ manifest.phase = targetPhase;
26
+ manifest.pending = pending.slice(1);
27
+ manifest.status = "running";
28
+ manifest.campaign_iteration = runnerState?.iteration ?? manifest.campaign_iteration;
29
+ manifest.updated_at = isoNow();
30
+ manifest.swarm = manifest.swarm ?? {};
31
+ manifest.swarm.task_launches_this_phase = 0;
32
+ manifest.swarm.phase = targetPhase;
33
+
34
+ writeJson(manifestPath, manifest);
35
+ return {
36
+ ok: true,
37
+ phase: targetPhase,
38
+ pending: manifest.pending,
39
+ iteration: manifest.campaign_iteration,
40
+ };
41
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Compare verify metrics: campaign baseline vs pre-wave vs current.
4
+ */
5
+ import { readJson } from "../../run-engine/lib.mjs";
6
+
7
+ const LAYER_KEYS = ["typecheck", "vitest", "go_test", "build", "playwright"];
8
+
9
+ function layerCount(snapshot, layer) {
10
+ if (!snapshot) return 0;
11
+ return (
12
+ snapshot.metrics?.[layer]?.error_count ??
13
+ (snapshot.layers?.[layer]?.error_count ?? 0)
14
+ );
15
+ }
16
+
17
+ function layerStatus(snapshot, layer) {
18
+ if (!snapshot) return "pass";
19
+ return snapshot.metrics?.[layer]?.status ?? snapshot.layers?.[layer]?.status ?? "pass";
20
+ }
21
+
22
+ export function analyzeRegression({ current, preWave, campaignBaseline }) {
23
+ const deltas = {};
24
+ const introduced = {};
25
+ let introducedRegression = false;
26
+
27
+ for (const layer of LAYER_KEYS) {
28
+ const cur = layerCount(current, layer);
29
+ const pre = layerCount(preWave, layer);
30
+ const base = layerCount(campaignBaseline, layer);
31
+ const deltaPre = cur - pre;
32
+ const deltaBase = cur - base;
33
+ deltas[layer] = { current: cur, pre_wave: pre, campaign_baseline: base, delta_pre_wave: deltaPre, delta_baseline: deltaBase };
34
+ introduced[layer] = deltaPre > 0 || (layerStatus(current, layer) === "fail" && layerStatus(preWave, layer) === "pass" && cur > 0);
35
+ if (introduced[layer]) introducedRegression = true;
36
+ }
37
+
38
+ const debtRemaining = LAYER_KEYS.some((l) => layerCount(current, l) > 0 || layerStatus(current, l) === "fail");
39
+ const strictPass = current?.status === "pass" && (current?.metrics?.total_errors ?? 0) === 0;
40
+
41
+ return {
42
+ introduced_regression: introducedRegression,
43
+ introduced_layers: Object.entries(introduced).filter(([, v]) => v).map(([k]) => k),
44
+ deltas,
45
+ debt_remaining: debtRemaining,
46
+ strict_pass: strictPass,
47
+ total_errors: current?.metrics?.total_errors ?? 0,
48
+ total_errors_baseline: campaignBaseline?.metrics?.total_errors ?? layerCount(campaignBaseline, "typecheck"),
49
+ };
50
+ }
51
+
52
+ export function loadSnapshot(path) {
53
+ if (!path) return null;
54
+ return readJson(path, null);
55
+ }