@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,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Continuous remediation loop (machine sentinel logs).
4
+ * Prefer remediation-cli.mjs watch for human / Cursor terminal monitoring.
5
+ */
6
+ import { isoNow } from "../run-engine/lib.mjs";
7
+ import { runRemediationWatchLoop } from "./lib/remediation-watch-loop.mjs";
8
+
9
+ const SENTINEL = "AGENT_REMEDIATION_WATCHER";
10
+
11
+ function parseArgs(argv) {
12
+ const out = { runId: null, campaignId: null, pollMs: 5000, maxRetries: 5 };
13
+ for (let i = 0; i < argv.length; i++) {
14
+ const a = argv[i];
15
+ if (a === "--run-id") out.runId = argv[++i];
16
+ else if (a === "--campaign-id") out.campaignId = argv[++i];
17
+ else if (a === "--poll-ms") out.pollMs = Number(argv[++i]);
18
+ else if (a === "--max-retries") out.maxRetries = Number(argv[++i]);
19
+ }
20
+ return out;
21
+ }
22
+
23
+ function log(event, detail = {}) {
24
+ const line = JSON.stringify({ at: isoNow(), event, ...detail });
25
+ console.log(`${SENTINEL} ${line}`);
26
+ }
27
+
28
+ const args = parseArgs(process.argv.slice(2));
29
+ if (!args.runId || !args.campaignId) {
30
+ console.error("Usage: remediation-yield-watcher.mjs --run-id <id> --campaign-id <id>");
31
+ process.exit(2);
32
+ }
33
+
34
+ runRemediationWatchLoop({
35
+ ...args,
36
+ reporter: { onEvent: (event, detail) => log(event, detail) },
37
+ }).then((code) => process.exit(code)).catch((err) => {
38
+ log("fatal", { message: String(err?.message ?? err) });
39
+ process.exit(2);
40
+ });
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agentic OS remediator sub-loop with two-tier validation:
4
+ * wave — regression gate (pre-existing debt does not block wave promotion)
5
+ * debt — strict gate (all layers must pass; used by debt_sweep phase)
6
+ * iteration — strict (legacy alias for debt within an iteration)
7
+ *
8
+ * Exit codes:
9
+ * 0 — promote (wave regression-clean OR strict pass OR wave deferred to debt_sweep)
10
+ * 1 — blocked (debt/infra exhausted — campaign cannot satisfy)
11
+ * 2 — runtime error
12
+ * 3 — remediate required (agent MUST fix and re-run; never treat as stop)
13
+ *
14
+ * Usage:
15
+ * node remediator-gate.mjs --campaign-id <id> --iteration <n> --mode wave|debt|iteration \
16
+ * [--wave-index <w>] [--run-id <run_id>] [--attempt <n>] [--skip-verify]
17
+ */
18
+ import fs from "fs";
19
+ import path from "path";
20
+ import { spawnSync } from "child_process";
21
+ import { fileURLToPath } from "url";
22
+ import { REPO_ROOT, isoNow, readJson, writeJson, runDir } from "../run-engine/lib.mjs";
23
+ import { analyzeRegression } from "./lib/regression-analysis.mjs";
24
+
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+ const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
27
+ const VERIFY_SCRIPT = path.join(__dirname, "verify-remediation-iteration.mjs");
28
+ const CLASSIFY_SCRIPT = path.join(__dirname, "classify-verify-failure.mjs");
29
+ const RULES = readJson(path.join(__dirname, "dispatch-rules.json"), {});
30
+
31
+ function parseArgs(argv) {
32
+ const out = {
33
+ campaignId: null,
34
+ iteration: 0,
35
+ mode: "wave",
36
+ waveIndex: null,
37
+ runId: null,
38
+ attempt: 1,
39
+ skipVerify: false,
40
+ };
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const a = argv[i];
43
+ if (a === "--campaign-id") out.campaignId = argv[++i];
44
+ else if (a === "--iteration") out.iteration = Number(argv[++i]);
45
+ else if (a === "--mode") out.mode = argv[++i];
46
+ else if (a === "--wave-index") out.waveIndex = Number(argv[++i]);
47
+ else if (a === "--run-id") out.runId = argv[++i];
48
+ else if (a === "--attempt") out.attempt = Number(argv[++i]);
49
+ else if (a === "--skip-verify") out.skipVerify = true;
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function campaignDir(id) {
55
+ return path.join(CAMPAIGNS_ROOT, id);
56
+ }
57
+
58
+ function iterDir(campaignId, iteration) {
59
+ return path.join(campaignDir(campaignId), "iterations", String(iteration));
60
+ }
61
+
62
+ function remediatorStatePath(campaignId, iteration, mode, waveIndex) {
63
+ const base = iterDir(campaignId, iteration);
64
+ const key = mode === "wave" && waveIndex != null ? `wave-${waveIndex}` : mode;
65
+ return path.join(base, `remediator-loop-${key}.json`);
66
+ }
67
+
68
+ function appendJournal(campaignId, text) {
69
+ fs.appendFileSync(path.join(campaignDir(campaignId), "journal.md"), text);
70
+ }
71
+
72
+ function gateMode(campaign, mode) {
73
+ if (mode === "wave") {
74
+ return campaign?.config?.wave_gate_mode ?? "regression";
75
+ }
76
+ return "strict";
77
+ }
78
+
79
+ function maxAttempts(campaign, mode) {
80
+ const cfg = campaign?.config ?? {};
81
+ if (mode === "debt" || mode === "iteration") {
82
+ return cfg.max_remediator_attempts_per_debt_round ?? cfg.max_remediator_attempts_per_iteration ?? RULES.defaults?.max_remediator_attempts_per_iteration ?? 3;
83
+ }
84
+ return cfg.max_remediator_attempts_per_wave ?? RULES.defaults?.max_remediator_attempts_per_wave ?? 3;
85
+ }
86
+
87
+ function verifyFileForMode(dir, mode) {
88
+ if (mode === "wave") return path.join(dir, "verify-wave.json");
89
+ if (mode === "debt") return path.join(dir, "verify-debt.json");
90
+ return path.join(dir, "verify-iteration.json");
91
+ }
92
+
93
+ function runVerify(args) {
94
+ const verifyMode = args.mode === "iteration" ? "debt" : args.mode;
95
+ const verifyArgs = [
96
+ VERIFY_SCRIPT,
97
+ "--campaign-id",
98
+ args.campaignId,
99
+ "--iteration",
100
+ String(args.iteration),
101
+ "--mode",
102
+ verifyMode,
103
+ ];
104
+ if (args.runId) verifyArgs.push("--run-id", args.runId);
105
+ const result = spawnSync(process.execPath, verifyArgs, { encoding: "utf8" });
106
+ let parsed = null;
107
+ try {
108
+ parsed = JSON.parse(result.stdout.trim().split("\n").pop());
109
+ } catch {
110
+ parsed = { ok: false, parse_error: true };
111
+ }
112
+ return { exitCode: result.status, parsed, stdout: result.stdout, stderr: result.stderr };
113
+ }
114
+
115
+ function classify(reportPath, ctx, layersFilter = null) {
116
+ const classifyArgs = [
117
+ CLASSIFY_SCRIPT,
118
+ "--report",
119
+ reportPath,
120
+ "--campaign-id",
121
+ ctx.campaignId,
122
+ "--iteration",
123
+ String(ctx.iteration),
124
+ "--attempt",
125
+ String(ctx.attempt),
126
+ ];
127
+ if (ctx.waveIndex != null) classifyArgs.push("--wave-index", String(ctx.waveIndex));
128
+ const result = spawnSync(process.execPath, classifyArgs, { encoding: "utf8" });
129
+ try {
130
+ const out = JSON.parse(result.stdout.trim());
131
+ if (layersFilter?.length && out.classification) {
132
+ out.classification.handoffs = (out.classification.handoffs ?? []).filter((h) =>
133
+ layersFilter.includes(h.layer),
134
+ );
135
+ out.classification.failed_layers = (out.classification.failed_layers ?? []).filter((l) =>
136
+ layersFilter.includes(l),
137
+ );
138
+ out.classification.primary =
139
+ out.classification.handoffs.find((h) => h.command) ?? out.classification.handoffs[0] ?? null;
140
+ out.classification.status = out.classification.handoffs.length ? "fail" : "pass";
141
+ }
142
+ return out;
143
+ } catch {
144
+ return { ok: false, error: "classify parse failed", stderr: result.stderr };
145
+ }
146
+ }
147
+
148
+ function writeHandoffArtifact(dir, attempt, payload) {
149
+ const handoffPath = path.join(dir, `remediator-handoff-attempt-${attempt}.json`);
150
+ writeJson(handoffPath, payload);
151
+ return handoffPath;
152
+ }
153
+
154
+ function loadRegressionContext(campaignId, iteration, waveIndex) {
155
+ const dir = iterDir(campaignId, iteration);
156
+ const campaignRoot = campaignDir(campaignId);
157
+ const preWave = waveIndex != null ? readJson(path.join(dir, `wave-${waveIndex}-pre.json`), null) : null;
158
+ const campaignBaseline = readJson(path.join(campaignRoot, "verify-baseline.json"), null);
159
+ return { preWave, campaignBaseline };
160
+ }
161
+
162
+ function promotePayload({ args, verifyFile, statePath, loopState, extra }) {
163
+ loopState.status = "promoted";
164
+ loopState.promoted_at = isoNow();
165
+ loopState.final_attempt = args.attempt;
166
+ writeJson(statePath, loopState);
167
+
168
+ const output = {
169
+ action: extra?.action ?? "promote",
170
+ status: "pass",
171
+ attempt: args.attempt,
172
+ verify_path: verifyFile,
173
+ loop_state_path: statePath,
174
+ campaign_must_continue: extra?.campaign_must_continue ?? false,
175
+ ...extra,
176
+ };
177
+ if (args.runId) {
178
+ writeJson(path.join(runDir(args.runId), "artifacts", `remediator_gate_${args.mode}.json`), output);
179
+ }
180
+ return output;
181
+ }
182
+
183
+ const args = parseArgs(process.argv.slice(2));
184
+ if (!args.campaignId) {
185
+ console.error("remediator-gate: --campaign-id required");
186
+ process.exit(2);
187
+ }
188
+
189
+ const campaign = readJson(path.join(campaignDir(args.campaignId), "campaign.json"), {});
190
+ const mode = args.mode === "iteration" ? "debt" : args.mode;
191
+ const gMode = gateMode(campaign, mode === "wave" ? "wave" : "debt");
192
+ const max = maxAttempts(campaign, mode);
193
+ const dir = iterDir(args.campaignId, args.iteration);
194
+ fs.mkdirSync(dir, { recursive: true });
195
+
196
+ const verifyFile = verifyFileForMode(dir, mode);
197
+
198
+ if (!args.skipVerify) {
199
+ runVerify({ ...args, mode });
200
+ }
201
+
202
+ if (!fs.existsSync(verifyFile)) {
203
+ console.error("remediator-gate: verify report missing after run");
204
+ process.exit(2);
205
+ }
206
+
207
+ const report = readJson(verifyFile, {});
208
+ const statePath = remediatorStatePath(args.campaignId, args.iteration, mode, args.waveIndex);
209
+ let loopState = readJson(statePath, {
210
+ mode,
211
+ wave_index: args.waveIndex,
212
+ gate_mode: gMode,
213
+ attempts: [],
214
+ status: "running",
215
+ });
216
+
217
+ const { preWave, campaignBaseline } = loadRegressionContext(
218
+ args.campaignId,
219
+ args.iteration,
220
+ args.waveIndex,
221
+ );
222
+ const regression = analyzeRegression({
223
+ current: report,
224
+ preWave: preWave ?? campaignBaseline,
225
+ campaignBaseline,
226
+ });
227
+
228
+ const strictPass = regression.strict_pass;
229
+ const isWaveRegression = mode === "wave" && gMode === "regression";
230
+
231
+ if (strictPass || (isWaveRegression && !regression.introduced_regression)) {
232
+ const debtRemaining = regression.debt_remaining;
233
+ const action = isWaveRegression && debtRemaining ? "promote_wave" : "promote";
234
+ const output = promotePayload({
235
+ args: { ...args, mode },
236
+ verifyFile,
237
+ statePath,
238
+ loopState,
239
+ extra: {
240
+ action,
241
+ gate_mode: gMode,
242
+ introduced_regression: regression.introduced_regression,
243
+ debt_remaining: debtRemaining,
244
+ campaign_must_continue: debtRemaining || isWaveRegression,
245
+ regression_analysis: regression,
246
+ message: isWaveRegression && debtRemaining
247
+ ? "Wave promoted — no new regression; pre-existing debt deferred to debt_sweep"
248
+ : "All verification layers pass",
249
+ },
250
+ });
251
+
252
+ appendJournal(
253
+ args.campaignId,
254
+ `- Remediator gate **${action}** ${mode} iter ${args.iteration}${args.waveIndex != null ? ` wave ${args.waveIndex}` : ""} (attempt ${args.attempt}, debt_remaining=${debtRemaining})\n`,
255
+ );
256
+ console.log(JSON.stringify(output));
257
+ process.exit(0);
258
+ }
259
+
260
+ const layersFilter =
261
+ isWaveRegression && regression.introduced_layers?.length
262
+ ? regression.introduced_layers
263
+ : null;
264
+
265
+ const classificationResult = classify(verifyFile, args, layersFilter);
266
+ const classification = classificationResult.classification ?? { handoffs: [], primary: null };
267
+
268
+ const infra = classification.handoffs?.find((h) => h.level === "infrastructure");
269
+ if (infra) {
270
+ const payload = {
271
+ action: "infrastructure",
272
+ status: "blocked",
273
+ attempt: args.attempt,
274
+ handoff: infra.handoff,
275
+ layer: infra.layer,
276
+ verify_path: verifyFile,
277
+ campaign_must_continue: false,
278
+ message: "Infrastructure prerequisite failed — run handoff then retry",
279
+ };
280
+ writeHandoffArtifact(dir, args.attempt, payload);
281
+ appendJournal(args.campaignId, `- Remediator gate: **INFRA** ${infra.handoff}\n`);
282
+ console.log(JSON.stringify(payload));
283
+ process.exit(1);
284
+ }
285
+
286
+ const attemptRecord = {
287
+ attempt: args.attempt,
288
+ at: isoNow(),
289
+ gate_mode: gMode,
290
+ introduced_regression: regression.introduced_regression,
291
+ failed_layers: classification.failed_layers ?? [],
292
+ primary: classification.primary,
293
+ verify_path: verifyFile,
294
+ regression_analysis: regression,
295
+ };
296
+ loopState.attempts.push(attemptRecord);
297
+ loopState.updated_at = isoNow();
298
+ writeJson(statePath, loopState);
299
+
300
+ if (args.attempt >= max) {
301
+ if (isWaveRegression) {
302
+ loopState.status = "deferred_to_debt_sweep";
303
+ loopState.deferred_at = isoNow();
304
+ loopState.deferred_reason = "max_remediator_attempts_wave_regression";
305
+ writeJson(statePath, loopState);
306
+
307
+ const payload = promotePayload({
308
+ args: { ...args, mode },
309
+ verifyFile,
310
+ statePath,
311
+ loopState,
312
+ extra: {
313
+ action: "defer_to_debt_sweep",
314
+ gate_mode: gMode,
315
+ reason: "max_remediator_attempts",
316
+ attempt: args.attempt,
317
+ max_attempts: max,
318
+ classification,
319
+ campaign_must_continue: true,
320
+ message: "Wave regression not fixed in max attempts — deferred to debt_sweep; continue remaining waves",
321
+ },
322
+ });
323
+ writeHandoffArtifact(dir, args.attempt, payload);
324
+ appendJournal(
325
+ args.campaignId,
326
+ `- Remediator wave **DEFERRED** to debt_sweep after ${args.attempt}/${max} attempts\n`,
327
+ );
328
+ console.log(JSON.stringify(payload));
329
+ process.exit(0);
330
+ }
331
+
332
+ loopState.status = "blocked";
333
+ loopState.blocked_at = isoNow();
334
+ loopState.blocked_reason = "max_remediator_attempts";
335
+ writeJson(statePath, loopState);
336
+
337
+ const payload = {
338
+ action: "block",
339
+ status: "fail",
340
+ reason: "max_remediator_attempts",
341
+ attempt: args.attempt,
342
+ max_attempts: max,
343
+ gate_mode: gMode,
344
+ classification,
345
+ verify_path: verifyFile,
346
+ loop_state_path: statePath,
347
+ campaign_must_continue: false,
348
+ manual_handoff: classification.primary?.slash_command ?? null,
349
+ };
350
+ writeHandoffArtifact(dir, args.attempt, payload);
351
+ appendJournal(
352
+ args.campaignId,
353
+ `- Remediator gate **BLOCKED** after ${args.attempt}/${max} attempts (${(classification.failed_layers ?? []).join(", ")})\n`,
354
+ );
355
+ if (args.runId) {
356
+ writeJson(path.join(runDir(args.runId), "artifacts", `remediator_gate_${mode}.json`), payload);
357
+ }
358
+ console.log(JSON.stringify(payload));
359
+ process.exit(1);
360
+ }
361
+
362
+ const primary = classification.primary;
363
+ const payload = {
364
+ action: "remediate",
365
+ status: "fail",
366
+ attempt: args.attempt,
367
+ max_attempts: max,
368
+ next_attempt: args.attempt + 1,
369
+ gate_mode: gMode,
370
+ introduced_regression: regression.introduced_regression,
371
+ introduced_layers: regression.introduced_layers,
372
+ failed_layers: classification.failed_layers,
373
+ campaign_must_continue: true,
374
+ orchestrator_must_not_stop: true,
375
+ orchestrator_must_not_set_blocked: true,
376
+ handoff: primary
377
+ ? {
378
+ command: primary.command,
379
+ domain: primary.domain,
380
+ intent: primary.intent,
381
+ layer: primary.layer,
382
+ level: primary.level,
383
+ file_paths: primary.file_paths,
384
+ log_path: report[primary.layer]?.log_path ?? null,
385
+ }
386
+ : null,
387
+ all_handoffs: classification.handoffs,
388
+ verify_path: verifyFile,
389
+ loop_state_path: statePath,
390
+ regression_analysis: regression,
391
+ retry_command: `node .cursor/aaac/scripts/remediation/remediator-gate.mjs --campaign-id ${args.campaignId} --iteration ${args.iteration} --mode ${mode}${args.waveIndex != null ? ` --wave-index ${args.waveIndex}` : ""}${args.runId ? ` --run-id ${args.runId}` : ""} --attempt ${args.attempt + 1}`,
392
+ };
393
+
394
+ const handoffPath = writeHandoffArtifact(dir, args.attempt, payload);
395
+ appendJournal(
396
+ args.campaignId,
397
+ `- Remediator attempt ${args.attempt}/${max}: **${primary?.command ?? "unknown"}** (${primary?.layer}) — handoff \`${handoffPath}\` — **CONTINUE** (exit 3 ≠ stop)\n`,
398
+ );
399
+
400
+ if (args.runId) {
401
+ writeJson(path.join(runDir(args.runId), "artifacts", `remediator_handoff_attempt_${args.attempt}.json`), payload);
402
+ }
403
+
404
+ console.log(JSON.stringify(payload));
405
+ process.exit(3);
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One-time repair for campaigns whose Fallow start baselines were overwritten or missing layers.
4
+ *
5
+ * Usage:
6
+ * node repair-fallow-start-baseline.mjs --campaign-id <id> --total <n> [--recorded-at <iso>]
7
+ * node repair-fallow-start-baseline.mjs --campaign-id <id> --dupes-clone-groups <n>
8
+ * node repair-fallow-start-baseline.mjs --campaign-id <id> --health-score <n>
9
+ */
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { REPO_ROOT, isoNow, readJson, writeJson } from "../run-engine/lib.mjs";
13
+
14
+ const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
15
+
16
+ function parseArgs(argv) {
17
+ const out = {
18
+ campaignId: null,
19
+ total: null,
20
+ dupesCloneGroups: null,
21
+ healthScore: null,
22
+ recordedAt: null,
23
+ };
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === "--campaign-id") out.campaignId = argv[++i];
27
+ else if (a === "--total") out.total = Number(argv[++i]);
28
+ else if (a === "--dupes-clone-groups") out.dupesCloneGroups = Number(argv[++i]);
29
+ else if (a === "--health-score") out.healthScore = Number(argv[++i]);
30
+ else if (a === "--recorded-at") out.recordedAt = argv[++i];
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function repairLayer(cDir, filename, field, value, layer) {
36
+ const startPath = path.join(cDir, filename);
37
+ const existing = readJson(startPath, null);
38
+ if (existing?.immutable && existing[field] != null) {
39
+ return { skipped: true, reason: `${filename} already immutable`, baseline: existing };
40
+ }
41
+ const baseline = {
42
+ [field]: value,
43
+ fallow_scan_path: existing?.fallow_scan_path ?? null,
44
+ recorded_at: args.recordedAt ?? existing?.recorded_at ?? isoNow(),
45
+ immutable: true,
46
+ source: "repair-fallow-start-baseline",
47
+ layer,
48
+ repaired_at: isoNow(),
49
+ };
50
+ writeJson(startPath, baseline);
51
+ return { skipped: false, baseline };
52
+ }
53
+
54
+ const args = parseArgs(process.argv.slice(2));
55
+ if (!args.campaignId) {
56
+ console.error(
57
+ "repair-fallow-start-baseline: --campaign-id required; pass at least one of --total, --dupes-clone-groups, --health-score",
58
+ );
59
+ process.exit(2);
60
+ }
61
+
62
+ if (
63
+ args.total == null &&
64
+ args.dupesCloneGroups == null &&
65
+ args.healthScore == null
66
+ ) {
67
+ console.error("repair-fallow-start-baseline: at least one metric required");
68
+ process.exit(2);
69
+ }
70
+
71
+ const cDir = path.join(CAMPAIGNS_ROOT, args.campaignId);
72
+ const results = {};
73
+
74
+ if (args.total != null && !Number.isNaN(args.total)) {
75
+ results.dead_code = repairLayer(
76
+ cDir,
77
+ "fallow-start-baseline.json",
78
+ "fallow_total_issues",
79
+ args.total,
80
+ "dead-code",
81
+ );
82
+ }
83
+
84
+ if (args.dupesCloneGroups != null && !Number.isNaN(args.dupesCloneGroups)) {
85
+ results.dupes = repairLayer(
86
+ cDir,
87
+ "fallow-start-dupes-baseline.json",
88
+ "clone_groups",
89
+ args.dupesCloneGroups,
90
+ "dupes",
91
+ );
92
+ }
93
+
94
+ if (args.healthScore != null && !Number.isNaN(args.healthScore)) {
95
+ results.health = repairLayer(
96
+ cDir,
97
+ "fallow-start-health-baseline.json",
98
+ "health_score",
99
+ args.healthScore,
100
+ "health",
101
+ );
102
+ }
103
+
104
+ const campaignPath = path.join(cDir, "campaign.json");
105
+ const campaign = readJson(campaignPath, null);
106
+ if (campaign) {
107
+ campaign.baseline = { ...campaign.baseline };
108
+ for (const r of Object.values(results)) {
109
+ if (!r.skipped) Object.assign(campaign.baseline, r.baseline);
110
+ }
111
+ campaign.updated_at = isoNow();
112
+ writeJson(campaignPath, campaign);
113
+ }
114
+
115
+ const journal = `\n- **Fallow baselines repaired** — ${JSON.stringify(results)}\n`;
116
+ fs.appendFileSync(path.join(cDir, "journal.md"), journal);
117
+
118
+ console.log(JSON.stringify({ ok: true, results }));