@ludecker/aaac 1.1.5 → 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 (104) hide show
  1. package/README.md +27 -12
  2. package/package.json +9 -9
  3. package/src/cli.mjs +19 -7
  4. package/src/generators/generate-commands.mjs +25 -1
  5. package/src/generators/generate-graph.mjs +9 -1
  6. package/src/lib/install.mjs +13 -1
  7. package/src/lib/sweep-project-docs.mjs +348 -0
  8. package/src/run-engine/advance-phase.mjs +23 -0
  9. package/src/run-engine/debug-run.mjs +0 -0
  10. package/src/run-engine/gate-write.mjs +13 -0
  11. package/src/run-engine/lib.mjs +153 -5
  12. package/src/run-engine/log-dump.mjs +0 -0
  13. package/src/run-engine/log-trace.mjs +0 -0
  14. package/templates/cursor/aaac/enforcement.json +96 -5
  15. package/templates/cursor/aaac/graph.project.yaml +44 -5
  16. package/templates/cursor/aaac/lifecycle/lifecycle.json +26 -0
  17. package/templates/cursor/aaac/lifecycle/phases.json +9 -1
  18. package/templates/cursor/aaac/ontology.json +1 -0
  19. package/templates/cursor/aaac/project.config.json +36 -0
  20. package/templates/cursor/aaac/scripts/remediation/auto-check-swarm-synthesis.mjs +75 -0
  21. package/templates/cursor/aaac/scripts/remediation/auto-dispatch-queue-from-health.mjs +78 -0
  22. package/templates/cursor/aaac/scripts/remediation/bootstrap-autonomous.mjs +113 -0
  23. package/templates/cursor/aaac/scripts/remediation/capture-verify-baseline.mjs +66 -0
  24. package/templates/cursor/aaac/scripts/remediation/capture-wave-snapshot.mjs +79 -0
  25. package/templates/cursor/aaac/scripts/remediation/check-swarm-raw.template.json +26 -0
  26. package/templates/cursor/aaac/scripts/remediation/classify-fallow-issues.mjs +77 -0
  27. package/templates/cursor/aaac/scripts/remediation/classify-verify-failure.mjs +176 -0
  28. package/templates/cursor/aaac/scripts/remediation/compute-satisfaction.mjs +344 -0
  29. package/templates/cursor/aaac/scripts/remediation/debt-sweep-gate.mjs +202 -0
  30. package/templates/cursor/aaac/scripts/remediation/dispatch-rules.json +44 -0
  31. package/templates/cursor/aaac/scripts/remediation/fallow-fp-rules.json +87 -0
  32. package/templates/cursor/aaac/scripts/remediation/fallow-scan.mjs +219 -0
  33. package/templates/cursor/aaac/scripts/remediation/handle-yield.mjs +240 -0
  34. package/templates/cursor/aaac/scripts/remediation/init-campaign.mjs +211 -0
  35. package/templates/cursor/aaac/scripts/remediation/lib/autonomous-mode.mjs +63 -0
  36. package/templates/cursor/aaac/scripts/remediation/lib/campaign-focus.mjs +87 -0
  37. package/templates/cursor/aaac/scripts/remediation/lib/fallow-classifier.mjs +190 -0
  38. package/templates/cursor/aaac/scripts/remediation/lib/fallow-health-targets.mjs +56 -0
  39. package/templates/cursor/aaac/scripts/remediation/lib/fallow-metrics.mjs +119 -0
  40. package/templates/cursor/aaac/scripts/remediation/lib/invoke-cursor-agent.mjs +51 -0
  41. package/templates/cursor/aaac/scripts/remediation/lib/reconcile-run-manifest.mjs +41 -0
  42. package/templates/cursor/aaac/scripts/remediation/lib/regression-analysis.mjs +55 -0
  43. package/templates/cursor/aaac/scripts/remediation/lib/remediation-config.mjs +69 -0
  44. package/templates/cursor/aaac/scripts/remediation/lib/remediation-progress.mjs +58 -0
  45. package/templates/cursor/aaac/scripts/remediation/lib/remediation-watch-loop.mjs +168 -0
  46. package/templates/cursor/aaac/scripts/remediation/lib/runner-exec.mjs +156 -0
  47. package/templates/cursor/aaac/scripts/remediation/lib/runner-state.mjs +145 -0
  48. package/templates/cursor/aaac/scripts/remediation/lib/verify-metrics.mjs +205 -0
  49. package/templates/cursor/aaac/scripts/remediation/merge-check-swarm.mjs +257 -0
  50. package/templates/cursor/aaac/scripts/remediation/plan-waves-from-queue.mjs +85 -0
  51. package/templates/cursor/aaac/scripts/remediation/prepare-check-context.mjs +148 -0
  52. package/templates/cursor/aaac/scripts/remediation/record-fallow-fp.mjs +107 -0
  53. package/templates/cursor/aaac/scripts/remediation/record-iteration-step.mjs +56 -0
  54. package/templates/cursor/aaac/scripts/remediation/remediation-cli.mjs +157 -0
  55. package/templates/cursor/aaac/scripts/remediation/remediation-cursor-watch.sh +10 -0
  56. package/templates/cursor/aaac/scripts/remediation/remediation-runner-daemon.sh +13 -0
  57. package/templates/cursor/aaac/scripts/remediation/remediation-runner.mjs +748 -0
  58. package/templates/cursor/aaac/scripts/remediation/remediation-yield-watcher.mjs +40 -0
  59. package/templates/cursor/aaac/scripts/remediation/remediator-gate.mjs +405 -0
  60. package/templates/cursor/aaac/scripts/remediation/repair-fallow-start-baseline.mjs +118 -0
  61. package/templates/cursor/aaac/scripts/remediation/runner-health-check.mjs +164 -0
  62. package/templates/cursor/aaac/scripts/remediation/satisfaction-loop-gate.mjs +286 -0
  63. package/templates/cursor/aaac/scripts/remediation/validate-campaign-complete.mjs +191 -0
  64. package/templates/cursor/aaac/scripts/remediation/verify-remediation-iteration.mjs +112 -0
  65. package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +23 -0
  66. package/templates/cursor/aaac/scripts/run-engine/debug-run.mjs +0 -0
  67. package/templates/cursor/aaac/scripts/run-engine/gate-write.mjs +13 -0
  68. package/templates/cursor/aaac/scripts/run-engine/lib.mjs +153 -5
  69. package/templates/cursor/aaac/scripts/run-engine/log-dump.mjs +0 -0
  70. package/templates/cursor/aaac/scripts/run-engine/log-trace.mjs +0 -0
  71. package/templates/cursor/agents/doc-conformance.md +25 -0
  72. package/templates/cursor/agents/implementation-review.md +21 -0
  73. package/templates/cursor/agents/remediation-check-app-inventory.md +32 -0
  74. package/templates/cursor/agents/remediation-check-app-ssot.md +24 -0
  75. package/templates/cursor/agents/remediation-check-app-trace.md +29 -0
  76. package/templates/cursor/agents/remediation-check-architecture-boundaries.md +21 -0
  77. package/templates/cursor/agents/remediation-check-architecture-decomposition.md +25 -0
  78. package/templates/cursor/agents/remediation-check-architecture-deps.md +23 -0
  79. package/templates/cursor/agents/remediation-check-risk.md +37 -0
  80. package/templates/cursor/agents/remediation-e2e-gate.md +30 -0
  81. package/templates/cursor/agents/remediation-remediator.md +69 -0
  82. package/templates/cursor/agents/test-author.md +27 -0
  83. package/templates/cursor/commands/remediate-app.md +212 -0
  84. package/templates/cursor/hooks/aaac-before-submit.sh +0 -0
  85. package/templates/cursor/hooks/aaac-pre-tool.sh +0 -0
  86. package/templates/cursor/hooks/aaac-stop.sh +0 -0
  87. package/templates/cursor/hooks/aaac-subagent-start.sh +0 -0
  88. package/templates/cursor/rules/aaac-enforcement.mdc +10 -3
  89. package/templates/cursor/skills/shared/execution/SKILL.md +7 -3
  90. package/templates/cursor/skills/shared/governance/implementation/SKILL.md +396 -28
  91. package/templates/cursor/skills/shared/implementation-review/SKILL.md +49 -0
  92. package/templates/cursor/skills/shared/planning/SKILL.md +5 -0
  93. package/templates/cursor/skills/shared/remediation/SKILL.md +51 -0
  94. package/templates/cursor/skills/shared/remediation/babysit/SKILL.md +223 -0
  95. package/templates/cursor/skills/shared/remediation/check-swarm/SKILL.md +114 -0
  96. package/templates/cursor/skills/shared/remediation/orchestrator/SKILL.md +275 -0
  97. package/templates/cursor/skills/shared/remediation/orchestrator/contract.yaml +116 -0
  98. package/templates/cursor/skills/shared/test-authoring/SKILL.md +58 -0
  99. package/templates/cursor/skills/shared/testing/SKILL.md +6 -0
  100. package/templates/cursor/skills/shared/verbs/create/orchestrator/SKILL.md +5 -3
  101. package/templates/cursor/skills/shared/verbs/fix/orchestrator/SKILL.md +5 -3
  102. package/templates/cursor/skills/shared/verbs/update/orchestrator/SKILL.md +5 -3
  103. package/templates/cursor/skills/shared/verification/SKILL.md +5 -3
  104. package/templates/docs/agentic_architecture.md +169 -97
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run full Fallow scan suite for remediation campaigns:
4
+ * dead-code, dupes, health (whole-repo JS/TS via frontend root).
5
+ *
6
+ * Usage:
7
+ * node fallow-scan.mjs --campaign-id <id> --iteration <n> [--save-baseline]
8
+ */
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { spawnSync } from "child_process";
12
+ import { fileURLToPath } from "url";
13
+ import { REPO_ROOT, isoNow, readJson, writeJson } from "../run-engine/lib.mjs";
14
+ import {
15
+ FRONTEND_ROOT,
16
+ runFallow,
17
+ summarizeDeadCode,
18
+ summarizeDupes,
19
+ summarizeHealth,
20
+ } from "./lib/fallow-metrics.mjs";
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
24
+
25
+ const SCAN_TARGETS = [
26
+ {
27
+ id: "dead-code",
28
+ subcommand: "dead-code",
29
+ extraArgs: [],
30
+ outFile: "fallow-scan.json",
31
+ summarize: summarizeDeadCode,
32
+ baselineFile: "fallow-start-baseline.json",
33
+ baselineField: "fallow_total_issues",
34
+ iterBaselineField: "fallow_total_issues",
35
+ },
36
+ {
37
+ id: "dupes",
38
+ subcommand: "dupes",
39
+ extraArgs: [],
40
+ outFile: "fallow-dupes.json",
41
+ summarize: summarizeDupes,
42
+ baselineFile: "fallow-start-dupes-baseline.json",
43
+ baselineField: "clone_groups",
44
+ iterBaselineField: "fallow_dupes_clone_groups",
45
+ },
46
+ {
47
+ id: "health",
48
+ subcommand: "health",
49
+ extraArgs: ["--score", "--hotspots", "--top", "20"],
50
+ outFile: "fallow-health.json",
51
+ summarize: summarizeHealth,
52
+ baselineFile: "fallow-start-health-baseline.json",
53
+ baselineField: "health_score",
54
+ iterBaselineField: "fallow_health_score",
55
+ },
56
+ ];
57
+
58
+ function parseArgs(argv) {
59
+ const out = { campaignId: null, iteration: 0, saveBaseline: false };
60
+ for (let i = 0; i < argv.length; i++) {
61
+ const a = argv[i];
62
+ if (a === "--campaign-id") out.campaignId = argv[++i];
63
+ else if (a === "--iteration") out.iteration = Number(argv[++i]);
64
+ else if (a === "--save-baseline") out.saveBaseline = true;
65
+ }
66
+ return out;
67
+ }
68
+
69
+ function writeImmutableBaseline(campaignDir, target, summary, scanPath) {
70
+ const baselinePath = path.join(campaignDir, target.baselineFile);
71
+ const value = summary[target.baselineField] ?? summary.clone_groups ?? summary.health_score ?? null;
72
+ const baseline = {
73
+ [target.baselineField]: value,
74
+ fallow_scan_path: scanPath,
75
+ recorded_at: isoNow(),
76
+ immutable: true,
77
+ source: "fallow-scan",
78
+ layer: target.id,
79
+ summary,
80
+ };
81
+ writeJson(baselinePath, baseline);
82
+ return baseline;
83
+ }
84
+
85
+ const args = parseArgs(process.argv.slice(2));
86
+ if (!args.campaignId) {
87
+ console.error("fallow-scan: --campaign-id required");
88
+ process.exit(2);
89
+ }
90
+
91
+ const campaignDir = path.join(CAMPAIGNS_ROOT, args.campaignId);
92
+ const iterDir = path.join(campaignDir, "iterations", String(args.iteration));
93
+ fs.mkdirSync(iterDir, { recursive: true });
94
+
95
+ const layers = {};
96
+ let anyRuntimeError = false;
97
+
98
+ for (const target of SCAN_TARGETS) {
99
+ const outPath = path.join(iterDir, target.outFile);
100
+ const run = runFallow(target.subcommand, target.extraArgs);
101
+ const payload = run.payload ?? { error: true, message: "no payload" };
102
+ const summary = target.summarize(payload);
103
+
104
+ if (!run.ok) anyRuntimeError = true;
105
+
106
+ const remediationMeta = {
107
+ layer: target.id,
108
+ scanned_at: isoNow(),
109
+ root: FRONTEND_ROOT,
110
+ exit_code: run.exit_code,
111
+ summary,
112
+ raw_path: outPath,
113
+ };
114
+
115
+ writeJson(outPath, { ...payload, _remediation: remediationMeta });
116
+ layers[target.id] = { summary, exit_code: run.exit_code, path: outPath, ok: run.ok };
117
+ }
118
+
119
+ const bundlePath = path.join(iterDir, "fallow-scan-bundle.json");
120
+ const bundle = {
121
+ scanned_at: isoNow(),
122
+ root: FRONTEND_ROOT,
123
+ scope_note:
124
+ "Fallow v2 suite from frontend/ (dead-code + dupes + health). Backend Go and python/ verified via go test / pytest in verify gate.",
125
+ layers: {
126
+ dead_code: layers["dead-code"],
127
+ dupes: layers.dupes,
128
+ health: layers.health,
129
+ },
130
+ };
131
+ writeJson(bundlePath, bundle);
132
+
133
+ const deadSummary = layers["dead-code"].summary;
134
+ const dupesSummary = layers.dupes.summary;
135
+ const healthSummary = layers.health.summary;
136
+
137
+ const campaign = readJson(path.join(campaignDir, "campaign.json"));
138
+ const startBaselinePath = path.join(campaignDir, "fallow-start-baseline.json");
139
+
140
+ if (campaign) {
141
+ campaign.current = campaign.current ?? {};
142
+ campaign.current.fallow_total_issues = deadSummary.total_issues;
143
+ campaign.current.fallow_dupes_clone_groups = dupesSummary.clone_groups;
144
+ campaign.current.fallow_health_score = healthSummary.health_score;
145
+ campaign.updated_at = isoNow();
146
+
147
+ const iterBaselinePath = path.join(iterDir, "iteration-baseline.json");
148
+ if (args.saveBaseline) {
149
+ writeJson(iterBaselinePath, {
150
+ iteration: args.iteration,
151
+ fallow_total_issues: deadSummary.total_issues,
152
+ fallow_dupes_clone_groups: dupesSummary.clone_groups,
153
+ fallow_dupes_duplication_percentage: dupesSummary.duplication_percentage,
154
+ fallow_health_score: healthSummary.health_score,
155
+ fallow_health_functions_above_threshold: healthSummary.functions_above_threshold,
156
+ fallow_scan_path: path.join(iterDir, "fallow-scan.json"),
157
+ fallow_dupes_path: path.join(iterDir, "fallow-dupes.json"),
158
+ fallow_health_path: path.join(iterDir, "fallow-health.json"),
159
+ recorded_at: isoNow(),
160
+ });
161
+ }
162
+
163
+ if (args.saveBaseline && !fs.existsSync(startBaselinePath)) {
164
+ const baseline = writeImmutableBaseline(
165
+ campaignDir,
166
+ SCAN_TARGETS[0],
167
+ deadSummary,
168
+ path.join(iterDir, "fallow-scan.json"),
169
+ );
170
+ campaign.baseline = { ...campaign.baseline, ...baseline };
171
+ fs.appendFileSync(
172
+ path.join(campaignDir, "journal.md"),
173
+ `\n- **Fallow start baseline captured** (immutable) — dead-code total=${deadSummary.total_issues}\n`,
174
+ );
175
+ } else if (fs.existsSync(startBaselinePath)) {
176
+ campaign.baseline = { ...campaign.baseline, ...readJson(startBaselinePath, {}) };
177
+ }
178
+
179
+ for (const target of SCAN_TARGETS.slice(1)) {
180
+ const baselinePath = path.join(campaignDir, target.baselineFile);
181
+ if (!fs.existsSync(baselinePath)) {
182
+ const summary = layers[target.id].summary;
183
+ const baseline = writeImmutableBaseline(
184
+ campaignDir,
185
+ target,
186
+ summary,
187
+ path.join(iterDir, target.outFile),
188
+ );
189
+ campaign.baseline = { ...campaign.baseline, ...baseline };
190
+ fs.appendFileSync(
191
+ path.join(campaignDir, "journal.md"),
192
+ `- **Fallow ${target.id} baseline backfilled** — ${target.baselineField}=${baseline[target.baselineField]}\n`,
193
+ );
194
+ }
195
+ }
196
+
197
+ writeJson(path.join(campaignDir, "campaign.json"), campaign);
198
+ }
199
+
200
+ const classifyArgs = [
201
+ path.join(__dirname, "classify-fallow-issues.mjs"),
202
+ "--campaign-id",
203
+ args.campaignId,
204
+ "--iteration",
205
+ String(args.iteration),
206
+ ];
207
+ if (args.saveBaseline) classifyArgs.push("--save-actionable-baseline");
208
+ spawnSync(process.execPath, classifyArgs, { encoding: "utf8" });
209
+
210
+ console.log(
211
+ JSON.stringify({
212
+ ok: !anyRuntimeError,
213
+ summary: deadSummary,
214
+ dupes: dupesSummary,
215
+ health: healthSummary,
216
+ path: path.join(iterDir, "fallow-scan.json"),
217
+ bundle_path: bundlePath,
218
+ }),
219
+ );
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Handle a runner yield — scriptable steps + Cursor agent for code waves.
4
+ */
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { spawnSync } from "child_process";
8
+ import { REPO_ROOT, readJson, writeJson, isoNow } from "../run-engine/lib.mjs";
9
+ import {
10
+ campaignDir,
11
+ iterDir,
12
+ loadYield,
13
+ runArtifactsDir,
14
+ } from "./lib/runner-state.mjs";
15
+ import { loadCampaignContext } from "./lib/campaign-focus.mjs";
16
+ import { invokeCursorAgent } from "./lib/invoke-cursor-agent.mjs";
17
+ import { runNode, parseDispatchQueueYaml, journal } from "./lib/runner-exec.mjs";
18
+
19
+ function parseArgs(argv) {
20
+ const out = { runId: null, campaignId: null };
21
+ for (let i = 0; i < argv.length; i++) {
22
+ if (argv[i] === "--run-id") out.runId = argv[++i];
23
+ else if (argv[i] === "--campaign-id") out.campaignId = argv[++i];
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function readWave(campaignId, runId, waveIndex) {
29
+ const queuePath = path.join(campaignDir(campaignId), "dispatch-queue.yaml");
30
+ if (fs.existsSync(queuePath)) {
31
+ const waves = parseDispatchQueueYaml(fs.readFileSync(queuePath, "utf8"));
32
+ if (waves[waveIndex]) return waves[waveIndex];
33
+ }
34
+ const planPath = path.join(runArtifactsDir(runId), "plan_waves.yaml");
35
+ if (fs.existsSync(planPath)) {
36
+ const waves = parseDispatchQueueYaml(fs.readFileSync(planPath, "utf8"));
37
+ if (waves[waveIndex]) return waves[waveIndex];
38
+ }
39
+ return { index: waveIndex, command: "fix-module", intent: `Health wave ${waveIndex}`, risk: "low" };
40
+ }
41
+
42
+ function recordExecuteWave(campaignId, iteration, waveIndex, wave, status = "completed") {
43
+ const artifact = path.join(campaignDir(campaignId), "artifacts", "execute_waves.json");
44
+ const data = readJson(artifact, { campaign_id: campaignId, iteration, waves: [] });
45
+ data.iteration = iteration;
46
+ data.waves = (data.waves ?? []).filter((w) => w.index !== waveIndex);
47
+ data.waves.push({
48
+ index: waveIndex,
49
+ priority: wave.priority ?? waveIndex + 1,
50
+ command: wave.command ?? "fix-module",
51
+ status,
52
+ risk: wave.risk ?? "low",
53
+ intent: wave.intent ?? "",
54
+ completed_at: isoNow(),
55
+ });
56
+ writeJson(artifact, data);
57
+ }
58
+
59
+ function buildWavePrompt(ctx, yieldPayload, wave) {
60
+ const protectedList = ctx.protected_paths.map((p) => `- ${p}`).join("\n");
61
+ return [
62
+ "You are executing a remediation campaign wave. Follow instructions exactly.",
63
+ "",
64
+ `Campaign intent: ${ctx.campaign.intent}`,
65
+ `Focus: reduce functions over ${ctx.focus.max_function_loc} LOC (health decomposition)`,
66
+ `Iteration: ${yieldPayload.iteration}`,
67
+ `Wave ${yieldPayload.wave_index + 1} of ${yieldPayload.wave_total ?? "?"}`,
68
+ "",
69
+ "Wave intent:",
70
+ wave.intent || yieldPayload.intent,
71
+ "",
72
+ "Protected paths — NEVER modify, delete, or split these files:",
73
+ protectedList,
74
+ "",
75
+ "Rules:",
76
+ "- Work in frontend/ when paths start with src/",
77
+ "- Split large functions into focused modules; each function should be <= 60 LOC where practical",
78
+ "- Preserve exports and public API; composition roots stay thin",
79
+ "- Match existing code conventions",
80
+ "- Run: cd frontend && pnpm exec tsc --noEmit — fix any errors you introduce",
81
+ "- Do not commit",
82
+ ].join("\n");
83
+ }
84
+
85
+ function buildRemediatorPrompt(ctx, yieldPayload, handoff) {
86
+ const logHint = handoff.log_path ? `Read full log: ${handoff.log_path}` : "";
87
+ return [
88
+ "Fix remediation verify/debt-sweep failures introduced by the last wave.",
89
+ "",
90
+ `Campaign intent: ${ctx.campaign.intent}`,
91
+ `Mode: ${handoff.mode ?? yieldPayload.phase}`,
92
+ `Layer: ${handoff.layer ?? "unknown"}`,
93
+ "",
94
+ handoff.intent ?? handoff.message ?? "",
95
+ logHint,
96
+ "",
97
+ "Fix only what is required to pass verification. Run tsc and relevant tests.",
98
+ ].join("\n");
99
+ }
100
+
101
+ function handleCheckSwarm(args, ctx, yieldPayload) {
102
+ const n = yieldPayload.iteration;
103
+ const synth = runNode("auto-check-swarm-synthesis.mjs", [
104
+ "--campaign-id", args.campaignId, "--iteration", String(n),
105
+ ]);
106
+ if (!synth.ok) throw new Error(`auto-check-swarm-synthesis failed: ${synth.stderr}`);
107
+
108
+ const merge = runNode("merge-check-swarm.mjs", [
109
+ "--campaign-id", args.campaignId, "--iteration", String(n), "--run-id", args.runId,
110
+ ]);
111
+ if (!merge.ok) throw new Error(`merge-check-swarm failed: ${merge.stderr}`);
112
+
113
+ const dispatch = runNode("auto-dispatch-queue-from-health.mjs", [
114
+ "--campaign-id", args.campaignId, "--iteration", String(n),
115
+ ]);
116
+ if (!dispatch.ok) throw new Error(`auto-dispatch-queue failed: ${dispatch.stderr}`);
117
+
118
+ const synthesis = `# Auto check synthesis iter ${n}\n\nFocus: ${ctx.focus.health_functions_above_60_loc ? "Health Functions >60 LOC" : "general"}\n`;
119
+ fs.writeFileSync(path.join(campaignDir(args.campaignId), "artifacts", "check_synthesis.md"), synthesis);
120
+ journal(args.campaignId, `- **Yield handler** check_swarm iter ${n} (auto)`);
121
+ return "check_swarm";
122
+ }
123
+
124
+ function handleDispatchQueue(args, ctx, yieldPayload) {
125
+ const n = yieldPayload.iteration;
126
+ const dispatch = runNode("auto-dispatch-queue-from-health.mjs", [
127
+ "--campaign-id", args.campaignId, "--iteration", String(n),
128
+ ]);
129
+ if (!dispatch.ok) throw new Error(`auto-dispatch-queue failed: ${dispatch.stderr}`);
130
+ journal(args.campaignId, `- **Yield handler** dispatch_queue iter ${n}`);
131
+ return "dispatch_queue";
132
+ }
133
+
134
+ function runTsc(scope) {
135
+ const cwd = scope === "frontend" ? path.join(REPO_ROOT, "frontend") : REPO_ROOT;
136
+ return spawnSync("pnpm", ["exec", "tsc", "--noEmit"], { cwd, encoding: "utf8" });
137
+ }
138
+
139
+ function handleExecuteWave(args, ctx, yieldPayload) {
140
+ const waveIndex = yieldPayload.wave_index ?? 0;
141
+ const wave = readWave(args.campaignId, args.runId, waveIndex);
142
+ const logPath = path.join(
143
+ iterDir(args.campaignId, yieldPayload.iteration),
144
+ "verify-logs",
145
+ `yield-agent-wave-${waveIndex}.log`,
146
+ );
147
+ const prompt = buildWavePrompt(ctx, yieldPayload, wave);
148
+ const agent = invokeCursorAgent(prompt, { cwd: REPO_ROOT, logPath, timeoutMs: 1_200_000 });
149
+ if (!agent.ok) {
150
+ recordExecuteWave(args.campaignId, yieldPayload.iteration, waveIndex, wave, "degraded");
151
+ throw new Error(`cursor agent failed (${agent.status}): ${agent.stderr.slice(0, 500)}`);
152
+ }
153
+ const tsc = runTsc(ctx.scope);
154
+ if (tsc.status !== 0) {
155
+ recordExecuteWave(args.campaignId, yieldPayload.iteration, waveIndex, wave, "degraded");
156
+ throw new Error(`tsc failed after wave: ${(tsc.stderr || tsc.stdout).slice(0, 500)}`);
157
+ }
158
+ recordExecuteWave(args.campaignId, yieldPayload.iteration, waveIndex, wave, "completed");
159
+ journal(args.campaignId, `- **Yield handler** execute_wave ${waveIndex} iter ${yieldPayload.iteration}`);
160
+ return "execute_wave";
161
+ }
162
+
163
+ function handleRemediator(args, ctx, yieldPayload) {
164
+ const iterDirPath = iterDir(args.campaignId, yieldPayload.iteration);
165
+ const handoffFiles = fs.existsSync(iterDirPath)
166
+ ? fs.readdirSync(iterDirPath).filter((f) => f.startsWith("remediator-handoff-attempt-"))
167
+ : [];
168
+ handoffFiles.sort();
169
+ const latest = handoffFiles[handoffFiles.length - 1];
170
+ const handoff = latest ? readJson(path.join(iterDirPath, latest), {}) : {};
171
+ const logPath = path.join(iterDirPath, "verify-logs", `yield-agent-remediator-${yieldPayload.attempt ?? 1}.log`);
172
+ const prompt = buildRemediatorPrompt(ctx, yieldPayload, handoff);
173
+ const agent = invokeCursorAgent(prompt, { cwd: REPO_ROOT, logPath, timeoutMs: 900_000 });
174
+ if (!agent.ok) throw new Error(`remediator agent failed: ${agent.stderr.slice(0, 500)}`);
175
+ journal(args.campaignId, `- **Yield handler** remediator iter ${yieldPayload.iteration}`);
176
+ return "remediator";
177
+ }
178
+
179
+ function handleReport(args, ctx) {
180
+ const reportPath = path.join(campaignDir(args.campaignId), "artifacts", "report.md");
181
+ const history = path.join(campaignDir(args.campaignId), "satisfaction-history.yaml");
182
+ const body = [
183
+ "# Remediation report",
184
+ "",
185
+ `Campaign: ${args.campaignId}`,
186
+ `Intent: ${ctx.campaign.intent}`,
187
+ `Completed: ${isoNow()}`,
188
+ "",
189
+ fs.existsSync(history) ? fs.readFileSync(history, "utf8") : "",
190
+ ].join("\n");
191
+ fs.writeFileSync(reportPath, body);
192
+ journal(args.campaignId, "- **Yield handler** report written");
193
+ return "report";
194
+ }
195
+
196
+ const args = parseArgs(process.argv.slice(2));
197
+ if (!args.runId || !args.campaignId) {
198
+ console.error("Usage: handle-yield.mjs --run-id <id> --campaign-id <id>");
199
+ process.exit(2);
200
+ }
201
+
202
+ const yieldPayload = loadYield(args.campaignId);
203
+ if (!yieldPayload?.type) {
204
+ console.error("No pending yield");
205
+ process.exit(2);
206
+ }
207
+
208
+ const ctx = loadCampaignContext(args.campaignId);
209
+ if (!ctx) {
210
+ console.error("Campaign not found");
211
+ process.exit(2);
212
+ }
213
+
214
+ let ackType;
215
+ try {
216
+ switch (yieldPayload.type) {
217
+ case "check_swarm":
218
+ ackType = handleCheckSwarm(args, ctx, yieldPayload);
219
+ break;
220
+ case "dispatch_queue":
221
+ ackType = handleDispatchQueue(args, ctx, yieldPayload);
222
+ break;
223
+ case "execute_wave":
224
+ ackType = handleExecuteWave(args, ctx, yieldPayload);
225
+ break;
226
+ case "remediator":
227
+ ackType = handleRemediator(args, ctx, yieldPayload);
228
+ break;
229
+ case "report":
230
+ ackType = handleReport(args, ctx);
231
+ break;
232
+ default:
233
+ throw new Error(`Unknown yield type: ${yieldPayload.type}`);
234
+ }
235
+ } catch (err) {
236
+ console.error(JSON.stringify({ ok: false, error: String(err?.message ?? err), yield: yieldPayload.type }));
237
+ process.exit(1);
238
+ }
239
+
240
+ console.log(JSON.stringify({ ok: true, ack_type: ackType, yield: yieldPayload.type }));
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Initialize or resume a remediation campaign for /remediate-app.
4
+ *
5
+ * Usage:
6
+ * node init-campaign.mjs --run-id <run_id> [--campaign-id <id>] [--resume <id>]
7
+ */
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { spawnSync } from "child_process";
11
+ import { fileURLToPath } from "url";
12
+ import {
13
+ REPO_ROOT,
14
+ isoNow,
15
+ readJson,
16
+ runDir,
17
+ slugify,
18
+ writeJson,
19
+ } from "../run-engine/lib.mjs";
20
+ import { applyAutonomousToConfig } from "./lib/autonomous-mode.mjs";
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const BOOTSTRAP_SCRIPT = path.join(__dirname, "bootstrap-autonomous.mjs");
24
+ const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
25
+
26
+ function parseArgs(argv) {
27
+ const out = {
28
+ runId: null,
29
+ campaignId: null,
30
+ resume: null,
31
+ scope: "whole-repo",
32
+ intent: "",
33
+ };
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const a = argv[i];
36
+ if (a === "--run-id") out.runId = argv[++i];
37
+ else if (a === "--campaign-id") out.campaignId = argv[++i];
38
+ else if (a === "--resume") out.resume = argv[++i];
39
+ else if (a === "--scope") out.scope = argv[++i];
40
+ else if (a === "--intent") out.intent = argv[++i];
41
+ }
42
+ return out;
43
+ }
44
+
45
+ function parseIntentConfig(intent) {
46
+ const base = {
47
+ max_iterations: 5,
48
+ max_waves_per_iteration: 3,
49
+ max_remediator_attempts_per_wave: 3,
50
+ max_remediator_attempts_per_iteration: 3,
51
+ max_remediator_attempts_per_debt_round: 3,
52
+ max_debt_sweep_rounds: 10,
53
+ wave_gate_mode: "regression",
54
+ debt_gate_mode: "strict",
55
+ satisfaction_threshold: 85,
56
+ rollback_on_verify_fail: true,
57
+ };
58
+ if (!intent) return applyAutonomousToConfig(base, intent);
59
+ const maxIter = intent.match(/max_iterations\s*=\s*(\d+)/i);
60
+ if (maxIter) base.max_iterations = Number(maxIter[1]);
61
+ const maxWaves = intent.match(/max_waves_per_iteration\s*=\s*(\d+)/i);
62
+ if (maxWaves) base.max_waves_per_iteration = Number(maxWaves[1]);
63
+ const maxRemWave = intent.match(/max_remediator_attempts_per_wave\s*=\s*(\d+)/i);
64
+ if (maxRemWave) base.max_remediator_attempts_per_wave = Number(maxRemWave[1]);
65
+ const maxRemIter = intent.match(/max_remediator_attempts_per_iteration\s*=\s*(\d+)/i);
66
+ if (maxRemIter) base.max_remediator_attempts_per_iteration = Number(maxRemIter[1]);
67
+ const maxDebtRounds = intent.match(/max_debt_sweep_rounds\s*=\s*(\d+)/i);
68
+ if (maxDebtRounds) base.max_debt_sweep_rounds = Number(maxDebtRounds[1]);
69
+ const maxDebtAttempts = intent.match(/max_remediator_attempts_per_debt_round\s*=\s*(\d+)/i);
70
+ if (maxDebtAttempts) base.max_remediator_attempts_per_debt_round = Number(maxDebtAttempts[1]);
71
+ const threshold = intent.match(/satisfaction_threshold\s*=\s*(\d+)/i);
72
+ if (threshold) base.satisfaction_threshold = Number(threshold[1]);
73
+ return applyAutonomousToConfig(base, intent);
74
+ }
75
+
76
+ function campaignDir(campaignId) {
77
+ return path.join(CAMPAIGNS_ROOT, campaignId);
78
+ }
79
+
80
+ function appendJournal(campaignId, line) {
81
+ const journalPath = path.join(campaignDir(campaignId), "journal.md");
82
+ const header =
83
+ fs.existsSync(journalPath) ? "" : "# Remediation campaign journal\n\n";
84
+ fs.appendFileSync(journalPath, `${header}${line}\n`);
85
+ }
86
+
87
+ const args = parseArgs(process.argv.slice(2));
88
+ if (!args.runId) {
89
+ console.error("init-campaign: --run-id required");
90
+ process.exit(2);
91
+ }
92
+
93
+ const manifest = readJson(path.join(runDir(args.runId), "run.json"), {});
94
+ const intent = args.intent || manifest.intent || "";
95
+ const config = parseIntentConfig(intent);
96
+
97
+ let campaignId = args.resume || args.campaignId;
98
+ const now = isoNow();
99
+ const date = now.slice(0, 10).replace(/-/g, "");
100
+
101
+ if (!campaignId) {
102
+ const resumeMatch = intent.match(/resume\s+(campaign_[a-z0-9_-]+)/i);
103
+ campaignId = resumeMatch?.[1] ?? `campaign_${date}_${slugify(manifest.command ?? "remediate")}`;
104
+ }
105
+
106
+ const dir = campaignDir(campaignId);
107
+ fs.mkdirSync(path.join(dir, "iterations"), { recursive: true });
108
+
109
+ const isResume = Boolean(args.resume || intent.match(/resume/i));
110
+ let campaign = readJson(path.join(dir, "campaign.json"), null);
111
+ if (campaign && !isResume) {
112
+ campaign = null;
113
+ }
114
+
115
+ if (!campaign) {
116
+ campaign = {
117
+ campaign_id: campaignId,
118
+ run_id: args.runId,
119
+ conversation_id: manifest.conversation_id ?? null,
120
+ scope: args.scope,
121
+ status: "running",
122
+ intent,
123
+ config,
124
+ iteration: 0,
125
+ waves_completed_total: 0,
126
+ baseline: {
127
+ fallow_total_issues: null,
128
+ fallow_scan_path: null,
129
+ clone_groups: null,
130
+ health_score: null,
131
+ recorded_at: null,
132
+ },
133
+ current: {
134
+ fallow_total_issues: null,
135
+ fallow_dupes_clone_groups: null,
136
+ fallow_health_score: null,
137
+ satisfaction_score: null,
138
+ satisfaction_rate: null,
139
+ e2e_pass: null,
140
+ verify_status: null,
141
+ },
142
+ dispatch_queue_path: "dispatch-queue.yaml",
143
+ debt_sweep: { status: "pending", iteration: null },
144
+ verify_baseline: null,
145
+ created_at: now,
146
+ updated_at: now,
147
+ };
148
+ writeJson(path.join(dir, "campaign.json"), campaign);
149
+ writeJson(path.join(dir, "satisfaction-history.yaml"), { entries: [] });
150
+ appendJournal(
151
+ campaignId,
152
+ `## ${now} — Campaign started\n\n- **Run:** \`${args.runId}\`\n- **Scope:** ${args.scope}\n- **Config:** max_iterations=${config.max_iterations}, threshold=${config.satisfaction_threshold}, autonomous=${config.autonomous}\n`,
153
+ );
154
+ } else {
155
+ campaign.run_id = args.runId;
156
+ campaign.intent = intent || campaign.intent;
157
+ campaign.config = { ...campaign.config, ...config };
158
+ campaign.updated_at = now;
159
+ campaign.status = "running";
160
+ writeJson(path.join(dir, "campaign.json"), campaign);
161
+ appendJournal(
162
+ campaignId,
163
+ `## ${now} — Campaign resumed\n\n- **Run:** \`${args.runId}\`\n- **Iteration:** ${campaign.iteration}\n- **Autonomous:** ${campaign.config.autonomous} (${campaign.config.autonomous_reason})\n`,
164
+ );
165
+ }
166
+
167
+ const artifactPath = path.join(runDir(args.runId), "artifacts/campaign.json");
168
+ writeJson(artifactPath, {
169
+ campaign_id: campaignId,
170
+ campaign_dir: dir,
171
+ config: campaign.config,
172
+ status: campaign.status,
173
+ iteration: campaign.iteration,
174
+ autonomous: campaign.config.autonomous,
175
+ });
176
+
177
+ let bootstrap = null;
178
+ if (campaign.config.autonomous) {
179
+ const boot = spawnSync(
180
+ process.execPath,
181
+ [BOOTSTRAP_SCRIPT, "--run-id", args.runId, "--campaign-id", campaignId],
182
+ { encoding: "utf8" },
183
+ );
184
+ try {
185
+ bootstrap = JSON.parse(boot.stdout.trim().split("\n").pop());
186
+ } catch {
187
+ bootstrap = { ok: false, error: boot.stderr || "bootstrap parse failed" };
188
+ }
189
+ }
190
+
191
+ const out = {
192
+ ok: true,
193
+ campaign_id: campaignId,
194
+ campaign_dir: dir,
195
+ iteration: campaign.iteration,
196
+ config: campaign.config,
197
+ autonomous: campaign.config.autonomous,
198
+ autonomous_reason: campaign.config.autonomous_reason,
199
+ orchestrator_mode: campaign.config.runner_mode,
200
+ bootstrap,
201
+ };
202
+
203
+ if (campaign.config.autonomous && bootstrap?.next_action) {
204
+ out.orchestrator_must = {
205
+ read_skill: bootstrap.skill_required,
206
+ next_action: bootstrap.next_action,
207
+ must_not_end_turn_until: "remediation-runner exit 0 or blocked",
208
+ };
209
+ }
210
+
211
+ console.log(JSON.stringify(out));