@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,344 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Compute satisfaction score and remediation rate for a campaign iteration.
4
+ * Fallow components: dead-code (actionable), dupes (clone_groups), health (score).
5
+ *
6
+ * Usage:
7
+ * node compute-satisfaction.mjs --campaign-id <id> --iteration <n>
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 { resolveActionableBaseline } from "./lib/fallow-classifier.mjs";
15
+ import {
16
+ improvementRate,
17
+ readStartBaseline,
18
+ reductionRate,
19
+ summarizeDeadCode,
20
+ summarizeDupes,
21
+ summarizeHealth,
22
+ } from "./lib/fallow-metrics.mjs";
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
26
+
27
+ const WEIGHTS = {
28
+ fallow_dead_code: 0.25,
29
+ fallow_dupes: 0.1,
30
+ fallow_health: 0.05,
31
+ structural_clean: 0.15,
32
+ unit_tests: 0.15,
33
+ build: 0.1,
34
+ e2e: 0.2,
35
+ };
36
+
37
+ /** @deprecated — use sum of effective fallow weights */
38
+ const LEGACY_FALLOW_WEIGHT =
39
+ WEIGHTS.fallow_dead_code + WEIGHTS.fallow_dupes + WEIGHTS.fallow_health;
40
+
41
+ function parseArgs(argv) {
42
+ const out = { campaignId: null, iteration: 0 };
43
+ for (let i = 0; i < argv.length; i++) {
44
+ if (argv[i] === "--campaign-id") out.campaignId = argv[++i];
45
+ else if (argv[i] === "--iteration") out.iteration = Number(argv[++i]);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ function scoreComponent(pass, partial = 0) {
51
+ if (pass === true) return 1;
52
+ if (pass === false) return 0;
53
+ return partial;
54
+ }
55
+
56
+ function resolveFallowStartBaseline(campaignDir, campaign) {
57
+ const startPath = path.join(campaignDir, "fallow-start-baseline.json");
58
+ const start = readJson(startPath, null);
59
+ if (start?.fallow_total_issues != null) {
60
+ return { issues: start.fallow_total_issues, source: "fallow-start-baseline.json" };
61
+ }
62
+ if (campaign?.baseline?.immutable && campaign.baseline.fallow_total_issues != null) {
63
+ return { issues: campaign.baseline.fallow_total_issues, source: "campaign.baseline" };
64
+ }
65
+ if (campaign?.baseline?.fallow_total_issues != null) {
66
+ return { issues: campaign.baseline.fallow_total_issues, source: "campaign.baseline.legacy" };
67
+ }
68
+ return { issues: null, source: "none" };
69
+ }
70
+
71
+ function loadLayerSummary(iterDir, file, summarize) {
72
+ const fullPath = path.join(iterDir, file);
73
+ if (!fs.existsSync(fullPath)) return null;
74
+ const payload = readJson(fullPath, {});
75
+ if (payload._remediation?.summary) return payload._remediation.summary;
76
+ return summarize(payload);
77
+ }
78
+
79
+ function normalizeWeights(weights, skipKeys = []) {
80
+ const active = { ...weights };
81
+ for (const key of skipKeys) delete active[key];
82
+ const sum = Object.values(active).reduce((a, b) => a + b, 0);
83
+ if (sum <= 0) return weights;
84
+ const normalized = {};
85
+ for (const [key, value] of Object.entries(active)) {
86
+ normalized[key] = value / sum;
87
+ }
88
+ return normalized;
89
+ }
90
+
91
+ function resolveVerify(iterDir) {
92
+ const debt = readJson(path.join(iterDir, "verify-debt.json"), null);
93
+ if (debt) return { verify: debt, source: "verify-debt.json" };
94
+ const iteration = readJson(path.join(iterDir, "verify-iteration.json"), null);
95
+ if (iteration) return { verify: iteration, source: "verify-iteration.json" };
96
+ return { verify: {}, source: "missing" };
97
+ }
98
+
99
+ function loadClassification(iterDir, campaignId, iteration) {
100
+ const classPath = path.join(iterDir, "fallow-classification.json");
101
+ if (fs.existsSync(classPath)) {
102
+ return readJson(classPath, null);
103
+ }
104
+ spawnSync(
105
+ process.execPath,
106
+ [
107
+ path.join(__dirname, "classify-fallow-issues.mjs"),
108
+ "--campaign-id",
109
+ campaignId,
110
+ "--iteration",
111
+ String(iteration),
112
+ ],
113
+ { encoding: "utf8" },
114
+ );
115
+ return readJson(classPath, null);
116
+ }
117
+
118
+ const args = parseArgs(process.argv.slice(2));
119
+ if (!args.campaignId) {
120
+ console.error("compute-satisfaction: --campaign-id required");
121
+ process.exit(2);
122
+ }
123
+
124
+ const campaignDir = path.join(CAMPAIGNS_ROOT, args.campaignId);
125
+ const campaign = readJson(path.join(campaignDir, "campaign.json"));
126
+ const iterDir = path.join(campaignDir, "iterations", String(args.iteration));
127
+
128
+ const deadSummary = loadLayerSummary(iterDir, "fallow-scan.json", summarizeDeadCode) ?? {};
129
+ const dupesSummary = loadLayerSummary(iterDir, "fallow-dupes.json", summarizeDupes);
130
+ const healthSummary = loadLayerSummary(iterDir, "fallow-health.json", summarizeHealth);
131
+
132
+ const classification = loadClassification(iterDir, args.campaignId, args.iteration);
133
+ const { verify, source: verifySource } = resolveVerify(iterDir);
134
+
135
+ const rawTotalIssues = deadSummary.total_issues ?? 0;
136
+ const actionableTotal =
137
+ classification?.summary?.actionable_total ?? rawTotalIssues;
138
+ const falsePositiveTotal = classification?.summary?.false_positive_total ?? 0;
139
+ const reviewTotal = classification?.summary?.review_total ?? 0;
140
+
141
+ const { issues: rawBaselineIssues, source: baselineSource } = resolveFallowStartBaseline(
142
+ campaignDir,
143
+ campaign,
144
+ );
145
+ const actionableBaseline = resolveActionableBaseline(campaignDir);
146
+ const startActionable =
147
+ actionableBaseline?.actionable_total ??
148
+ (rawBaselineIssues != null && classification
149
+ ? rawBaselineIssues - (actionableBaseline?.false_positive_total ?? 0)
150
+ : rawBaselineIssues) ??
151
+ actionableTotal;
152
+
153
+ const effectiveBaseline = startActionable ?? actionableTotal;
154
+
155
+ const dupesBaseline = readStartBaseline(campaignDir, "fallow-start-dupes-baseline.json", "clone_groups");
156
+ const healthBaseline = readStartBaseline(
157
+ campaignDir,
158
+ "fallow-start-health-baseline.json",
159
+ "health_score",
160
+ );
161
+
162
+ const currentCloneGroups = dupesSummary?.clone_groups ?? null;
163
+ const currentHealthScore = healthSummary?.health_score ?? null;
164
+
165
+ const dupesStart =
166
+ dupesBaseline.value ??
167
+ readJson(path.join(iterDir, "iteration-baseline.json"), null)?.fallow_dupes_clone_groups ??
168
+ currentCloneGroups;
169
+
170
+ const healthStart =
171
+ healthBaseline.value ??
172
+ readJson(path.join(iterDir, "iteration-baseline.json"), null)?.fallow_health_score ??
173
+ currentHealthScore;
174
+
175
+ const skipWeightKeys = [];
176
+ if (dupesSummary == null || dupesStart == null || currentCloneGroups == null) {
177
+ skipWeightKeys.push("fallow_dupes");
178
+ }
179
+ if (healthSummary == null || healthStart == null || currentHealthScore == null) {
180
+ skipWeightKeys.push("fallow_health");
181
+ }
182
+ const effectiveWeights = normalizeWeights(WEIGHTS, skipWeightKeys);
183
+ const effectiveFallowWeight =
184
+ (effectiveWeights.fallow_dead_code ?? 0) +
185
+ (effectiveWeights.fallow_dupes ?? 0) +
186
+ (effectiveWeights.fallow_health ?? 0);
187
+
188
+ const prevEntry = readJson(path.join(campaignDir, "satisfaction-history.yaml"), { entries: [] });
189
+ const prev = prevEntry.entries.filter((e) => e.iteration < args.iteration).pop();
190
+
191
+ const iterBaseline = readJson(path.join(iterDir, "iteration-baseline.json"), null);
192
+ const prevActionable = prev?.fallow_actionable_total ?? prev?.fallow_total_issues;
193
+ const iterationStartActionable =
194
+ iterBaseline?.fallow_actionable_total ??
195
+ iterBaseline?.fallow_total_issues ??
196
+ prevActionable ??
197
+ effectiveBaseline;
198
+
199
+ const deadCodeRate =
200
+ effectiveBaseline > 0
201
+ ? Math.max(0, (effectiveBaseline - actionableTotal) / effectiveBaseline)
202
+ : 1;
203
+
204
+ const dupesRate =
205
+ dupesSummary != null && dupesStart != null && currentCloneGroups != null
206
+ ? (reductionRate(dupesStart, currentCloneGroups) ?? 0)
207
+ : null;
208
+ const healthRate =
209
+ healthSummary != null && healthStart != null && currentHealthScore != null
210
+ ? (improvementRate(healthStart, currentHealthScore) ?? 0)
211
+ : null;
212
+
213
+ const fallowCompositeRate =
214
+ effectiveFallowWeight > 0
215
+ ? ((deadCodeRate * (effectiveWeights.fallow_dead_code ?? 0)) +
216
+ (dupesRate ?? 0) * (effectiveWeights.fallow_dupes ?? 0) +
217
+ (healthRate ?? 0) * (effectiveWeights.fallow_health ?? 0)) /
218
+ effectiveFallowWeight
219
+ : deadCodeRate;
220
+
221
+ const iterationRate =
222
+ iterationStartActionable > 0
223
+ ? Math.max(0, (iterationStartActionable - actionableTotal) / iterationStartActionable)
224
+ : deadCodeRate;
225
+
226
+ const structuralClean =
227
+ (deadSummary.unresolved_imports ?? 0) === 0 && (deadSummary.circular_dependencies ?? 0) === 0;
228
+
229
+ const vitestPass = verify.vitest?.status === "pass";
230
+ const typecheckPass = verify.typecheck?.status === "pass";
231
+ const buildPass = verify.build?.status === "pass";
232
+ const goTestPass = verify.go_test?.status === "pass" || verify.go_test?.status === "skipped";
233
+ const e2ePass = verify.playwright?.status === "pass";
234
+
235
+ const components = {
236
+ fallow_dead_code: scoreComponent(null, deadCodeRate),
237
+ fallow_dupes: dupesRate == null ? null : scoreComponent(null, dupesRate),
238
+ fallow_health: healthRate == null ? null : scoreComponent(null, healthRate),
239
+ fallow_remediation: scoreComponent(null, fallowCompositeRate),
240
+ structural_clean: scoreComponent(structuralClean),
241
+ unit_tests: scoreComponent(vitestPass && typecheckPass),
242
+ build: scoreComponent(buildPass),
243
+ e2e: scoreComponent(e2ePass),
244
+ };
245
+
246
+ let score = 0;
247
+ for (const [key, weight] of Object.entries(effectiveWeights)) {
248
+ score += (components[key] ?? 0) * weight * 100;
249
+ }
250
+ score = Math.round(score * 10) / 10;
251
+
252
+ const entry = {
253
+ iteration: args.iteration,
254
+ at: isoNow(),
255
+ score,
256
+ rate: Math.round(fallowCompositeRate * 1000) / 1000,
257
+ dead_code_rate: Math.round(deadCodeRate * 1000) / 1000,
258
+ dupes_rate: dupesRate == null ? null : Math.round(dupesRate * 1000) / 1000,
259
+ health_rate: healthRate == null ? null : Math.round(healthRate * 1000) / 1000,
260
+ iteration_rate: Math.round(iterationRate * 1000) / 1000,
261
+ fallow_raw_total: rawTotalIssues,
262
+ fallow_actionable_total: actionableTotal,
263
+ fallow_false_positive_total: falsePositiveTotal,
264
+ fallow_review_total: reviewTotal,
265
+ fallow_dupes_clone_groups: currentCloneGroups,
266
+ fallow_dupes_duplication_percentage: dupesSummary?.duplication_percentage ?? null,
267
+ fallow_health_score: currentHealthScore,
268
+ fallow_health_functions_above_threshold: healthSummary?.functions_above_threshold ?? null,
269
+ fallow_start_baseline: effectiveBaseline,
270
+ fallow_start_baseline_raw: rawBaselineIssues,
271
+ fallow_dupes_start_baseline: dupesStart,
272
+ fallow_health_start_baseline: healthStart,
273
+ fallow_baseline_source: baselineSource,
274
+ fallow_dupes_baseline_source: dupesBaseline.source,
275
+ fallow_health_baseline_source: healthBaseline.source,
276
+ fallow_scoring_mode: "actionable_dead_code_plus_dupes_health",
277
+ delta_vs_baseline: actionableTotal - effectiveBaseline,
278
+ delta_vs_baseline_raw: rawTotalIssues - (rawBaselineIssues ?? rawTotalIssues),
279
+ delta_dupes_vs_baseline: currentCloneGroups - (dupesStart ?? currentCloneGroups),
280
+ delta_health_vs_baseline:
281
+ currentHealthScore != null && healthStart != null ? currentHealthScore - healthStart : null,
282
+ delta_vs_iteration_start: actionableTotal - iterationStartActionable,
283
+ delta_vs_previous: prev
284
+ ? actionableTotal - (prev.fallow_actionable_total ?? prev.fallow_total_issues)
285
+ : null,
286
+ e2e_pass: e2ePass,
287
+ vitest_pass: vitestPass,
288
+ typecheck_pass: typecheckPass,
289
+ build_pass: buildPass,
290
+ go_test_pass: goTestPass,
291
+ weights_applied: effectiveWeights,
292
+ weights_skipped_layers: skipWeightKeys,
293
+ components,
294
+ classification_path: path.join(iterDir, "fallow-classification.json"),
295
+ fallow_dupes_path: path.join(iterDir, "fallow-dupes.json"),
296
+ fallow_health_path: path.join(iterDir, "fallow-health.json"),
297
+ verify_path: path.join(
298
+ iterDir,
299
+ verifySource === "verify-debt.json" ? "verify-debt.json" : "verify-iteration.json",
300
+ ),
301
+ verify_source: verifySource,
302
+ /** @deprecated use fallow_actionable_total */
303
+ fallow_total_issues: actionableTotal,
304
+ };
305
+
306
+ writeJson(path.join(iterDir, "satisfaction.json"), entry);
307
+
308
+ const history = readJson(path.join(campaignDir, "satisfaction-history.yaml"), { entries: [] });
309
+ history.entries = history.entries.filter((e) => e.iteration !== args.iteration);
310
+ history.entries.push(entry);
311
+ history.entries.sort((a, b) => a.iteration - b.iteration);
312
+ writeJson(path.join(campaignDir, "satisfaction-history.yaml"), history);
313
+
314
+ if (campaign) {
315
+ campaign.current = {
316
+ fallow_total_issues: rawTotalIssues,
317
+ fallow_actionable_total: actionableTotal,
318
+ fallow_false_positive_total: falsePositiveTotal,
319
+ fallow_dupes_clone_groups: currentCloneGroups,
320
+ fallow_health_score: currentHealthScore,
321
+ satisfaction_score: score,
322
+ satisfaction_rate: entry.rate,
323
+ e2e_pass: e2ePass,
324
+ verify_status: verify.status ?? null,
325
+ };
326
+ campaign.updated_at = isoNow();
327
+ const threshold = campaign.config?.satisfaction_threshold ?? 85;
328
+ if (
329
+ score >= threshold &&
330
+ e2ePass &&
331
+ vitestPass &&
332
+ typecheckPass &&
333
+ buildPass &&
334
+ (verify.metrics?.total_errors ?? 0) === 0
335
+ ) {
336
+ campaign.status = "satisfied";
337
+ }
338
+ writeJson(path.join(campaignDir, "campaign.json"), campaign);
339
+ }
340
+
341
+ const journalLine = `- **Iteration ${args.iteration}** — satisfaction **${score}/100** (fallow composite ${(entry.rate * 100).toFixed(1)}%: dead-code ${(deadCodeRate * 100).toFixed(1)}%${dupesRate != null ? `, dupes ${(dupesRate * 100).toFixed(1)}%` : ""}${healthRate != null ? `, health ${(healthRate * 100).toFixed(1)}%` : ""}), fallow raw=${rawTotalIssues} actionable=${actionableTotal} (FP=${falsePositiveTotal})${currentCloneGroups != null ? `, dupes=${currentCloneGroups} groups` : ""}${currentHealthScore != null ? `, health=${currentHealthScore}` : ""}, E2E ${e2ePass ? "PASS" : "FAIL"}\n`;
342
+ fs.appendFileSync(path.join(campaignDir, "journal.md"), `\n### ${entry.at}\n${journalLine}`);
343
+
344
+ console.log(JSON.stringify({ ok: true, satisfaction: entry, satisfied: campaign?.status === "satisfied" }));
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Strict debt sweep — mandatory phase after cleanup waves.
4
+ * Loops remediator-gate --mode debt until all layers pass or max rounds exhausted.
5
+ *
6
+ * Exit codes:
7
+ * 0 — debt sweep complete (strict pass)
8
+ * 1 — blocked (max rounds exhausted)
9
+ * 3 — remediate required (agent must fix and re-run with --round N --attempt M+1)
10
+ *
11
+ * Usage:
12
+ * node debt-sweep-gate.mjs --campaign-id <id> --iteration <n> \
13
+ * [--run-id <run_id>] [--round <n>] [--attempt <n>]
14
+ */
15
+ import fs from "fs";
16
+ import path from "path";
17
+ import { spawnSync } from "child_process";
18
+ import { fileURLToPath } from "url";
19
+ import { REPO_ROOT, isoNow, readJson, writeJson, runDir } from "../run-engine/lib.mjs";
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
23
+ const REMEDIATOR_GATE = path.join(__dirname, "remediator-gate.mjs");
24
+
25
+ function parseArgs(argv) {
26
+ const out = { campaignId: null, iteration: 0, runId: null, round: 1, attempt: 1 };
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a === "--campaign-id") out.campaignId = argv[++i];
30
+ else if (a === "--iteration") out.iteration = Number(argv[++i]);
31
+ else if (a === "--run-id") out.runId = argv[++i];
32
+ else if (a === "--round") out.round = Number(argv[++i]);
33
+ else if (a === "--attempt") out.attempt = Number(argv[++i]);
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function campaignDir(id) {
39
+ return path.join(CAMPAIGNS_ROOT, id);
40
+ }
41
+
42
+ function appendJournal(campaignId, text) {
43
+ fs.appendFileSync(path.join(campaignDir(campaignId), "journal.md"), text);
44
+ }
45
+
46
+ const args = parseArgs(process.argv.slice(2));
47
+ if (!args.campaignId) {
48
+ console.error("debt-sweep-gate: --campaign-id required");
49
+ process.exit(2);
50
+ }
51
+
52
+ const campaignPath = path.join(campaignDir(args.campaignId), "campaign.json");
53
+ const campaign = readJson(campaignPath, {});
54
+ const maxRounds = campaign?.config?.max_debt_sweep_rounds ?? 10;
55
+ const iterDir = path.join(campaignDir(args.campaignId), "iterations", String(args.iteration));
56
+ fs.mkdirSync(iterDir, { recursive: true });
57
+
58
+ const statePath = path.join(iterDir, "debt-sweep-state.json");
59
+ let sweepState = readJson(statePath, {
60
+ status: "running",
61
+ round: args.round,
62
+ attempts_by_round: {},
63
+ started_at: isoNow(),
64
+ });
65
+
66
+ const gateArgs = [
67
+ REMEDIATOR_GATE,
68
+ "--campaign-id",
69
+ args.campaignId,
70
+ "--iteration",
71
+ String(args.iteration),
72
+ "--mode",
73
+ "debt",
74
+ "--attempt",
75
+ String(args.attempt),
76
+ ];
77
+ if (args.runId) gateArgs.push("--run-id", args.runId);
78
+
79
+ const result = spawnSync(process.execPath, gateArgs, { encoding: "utf8" });
80
+ let payload = null;
81
+ try {
82
+ payload = JSON.parse(result.stdout.trim().split("\n").pop());
83
+ } catch {
84
+ console.error("debt-sweep-gate: failed to parse remediator-gate output");
85
+ process.exit(2);
86
+ }
87
+
88
+ const roundKey = String(args.round);
89
+ sweepState.attempts_by_round[roundKey] = sweepState.attempts_by_round[roundKey] ?? [];
90
+ sweepState.attempts_by_round[roundKey].push({
91
+ attempt: args.attempt,
92
+ at: isoNow(),
93
+ exit_code: result.status,
94
+ action: payload.action,
95
+ });
96
+ sweepState.round = args.round;
97
+ sweepState.updated_at = isoNow();
98
+
99
+ if (result.status === 0 && (payload.action === "promote" || payload.action === "promote_wave")) {
100
+ sweepState.status = "complete";
101
+ sweepState.completed_at = isoNow();
102
+ writeJson(statePath, sweepState);
103
+
104
+ if (campaign) {
105
+ campaign.debt_sweep = {
106
+ status: "complete",
107
+ iteration: args.iteration,
108
+ round: args.round,
109
+ completed_at: isoNow(),
110
+ };
111
+ campaign.updated_at = isoNow();
112
+ writeJson(campaignPath, campaign);
113
+ }
114
+
115
+ appendJournal(args.campaignId, `- Debt sweep **COMPLETE** iter ${args.iteration} round ${args.round}\n`);
116
+
117
+ const output = {
118
+ action: "debt_sweep_complete",
119
+ status: "pass",
120
+ round: args.round,
121
+ attempt: args.attempt,
122
+ sweep_state_path: statePath,
123
+ verify_path: payload.verify_path,
124
+ };
125
+ if (args.runId) {
126
+ writeJson(path.join(runDir(args.runId), "artifacts", "debt_sweep.json"), output);
127
+ }
128
+ console.log(JSON.stringify(output));
129
+ process.exit(0);
130
+ }
131
+
132
+ if (result.status === 3) {
133
+ writeJson(statePath, sweepState);
134
+ const output = {
135
+ ...payload,
136
+ debt_sweep_round: args.round,
137
+ max_debt_sweep_rounds: maxRounds,
138
+ campaign_must_continue: true,
139
+ orchestrator_must_not_stop: true,
140
+ retry_command: `node .cursor/aaac/scripts/remediation/debt-sweep-gate.mjs --campaign-id ${args.campaignId} --iteration ${args.iteration} --round ${args.round} --attempt ${args.attempt + 1}${args.runId ? ` --run-id ${args.runId}` : ""}`,
141
+ };
142
+ appendJournal(
143
+ args.campaignId,
144
+ `- Debt sweep round ${args.round} attempt ${args.attempt}: **remediate** — continue loop\n`,
145
+ );
146
+ if (args.runId) {
147
+ writeJson(path.join(runDir(args.runId), "artifacts", "debt_sweep_handoff.json"), output);
148
+ }
149
+ console.log(JSON.stringify(output));
150
+ process.exit(3);
151
+ }
152
+
153
+ if (result.status === 1) {
154
+ if (args.round < maxRounds) {
155
+ sweepState.status = "running";
156
+ writeJson(statePath, sweepState);
157
+ const nextRound = args.round + 1;
158
+ const output = {
159
+ action: "debt_sweep_next_round",
160
+ status: "fail",
161
+ round: args.round,
162
+ next_round: nextRound,
163
+ max_debt_sweep_rounds: maxRounds,
164
+ campaign_must_continue: true,
165
+ message: `Round ${args.round} blocked — starting round ${nextRound}`,
166
+ retry_command: `node .cursor/aaac/scripts/remediation/debt-sweep-gate.mjs --campaign-id ${args.campaignId} --iteration ${args.iteration} --round ${nextRound} --attempt 1${args.runId ? ` --run-id ${args.runId}` : ""}`,
167
+ };
168
+ appendJournal(args.campaignId, `- Debt sweep round ${args.round} blocked — advancing to round ${nextRound}\n`);
169
+ console.log(JSON.stringify(output));
170
+ process.exit(3);
171
+ }
172
+
173
+ sweepState.status = "blocked";
174
+ sweepState.blocked_at = isoNow();
175
+ writeJson(statePath, sweepState);
176
+
177
+ if (campaign) {
178
+ campaign.debt_sweep = { status: "blocked", iteration: args.iteration, round: args.round };
179
+ campaign.status = "blocked";
180
+ campaign.updated_at = isoNow();
181
+ writeJson(campaignPath, campaign);
182
+ }
183
+
184
+ const output = {
185
+ action: "debt_sweep_blocked",
186
+ status: "fail",
187
+ reason: "max_debt_sweep_rounds_exhausted",
188
+ round: args.round,
189
+ max_debt_sweep_rounds: maxRounds,
190
+ campaign_must_continue: false,
191
+ payload,
192
+ };
193
+ appendJournal(args.campaignId, `- Debt sweep **BLOCKED** after ${maxRounds} rounds\n`);
194
+ if (args.runId) {
195
+ writeJson(path.join(runDir(args.runId), "artifacts", "debt_sweep.json"), output);
196
+ }
197
+ console.log(JSON.stringify(output));
198
+ process.exit(1);
199
+ }
200
+
201
+ console.error("debt-sweep-gate: unexpected remediator exit", result.status);
202
+ process.exit(2);
@@ -0,0 +1,44 @@
1
+ {
2
+ "version": 1,
3
+ "description": "Validator failure → remediator command dispatch (SSOT for /remediate-app)",
4
+ "defaults": {
5
+ "max_remediator_attempts_per_wave": 3,
6
+ "max_remediator_attempts_per_iteration": 3,
7
+ "max_remediator_attempts_per_debt_round": 3,
8
+ "max_debt_sweep_rounds": 10,
9
+ "wave_gate_mode": "regression"
10
+ },
11
+ "layers": {
12
+ "typecheck": {
13
+ "command": "fix-module",
14
+ "level": "code",
15
+ "intent_template": "TypeScript type-check failed during remediation campaign {campaign_id} iter {iteration} wave {wave_index} attempt {attempt}. Fix compiler errors without widening types unsafely. Output:\n{evidence}"
16
+ },
17
+ "vitest": {
18
+ "command": "fix-module",
19
+ "level": "test",
20
+ "intent_template": "Unit tests failed during remediation campaign {campaign_id} iter {iteration} wave {wave_index} attempt {attempt}. Fix production code or test expectations as appropriate. Output:\n{evidence}"
21
+ },
22
+ "go_test": {
23
+ "command": "fix-module",
24
+ "level": "code",
25
+ "intent_template": "Go tests failed during remediation campaign {campaign_id} iter {iteration} wave {wave_index} attempt {attempt}. Fix failing packages. Output:\n{evidence}"
26
+ },
27
+ "build": {
28
+ "command": "fix-module",
29
+ "level": "code",
30
+ "intent_template": "Production build failed during remediation campaign {campaign_id} iter {iteration} attempt {attempt}. Fix build errors and broken imports. Output:\n{evidence}"
31
+ },
32
+ "playwright": {
33
+ "command": "fix-bug",
34
+ "level": "code",
35
+ "intent_template": "Playwright E2E failed during campaign {campaign_id} iter {iteration} attempt {attempt}. Fix runtime/module/navigation regressions. Output:\n{evidence}"
36
+ },
37
+ "playwright_infra": {
38
+ "command": null,
39
+ "level": "infrastructure",
40
+ "handoff": "Start dev server (see project.config.json remediation.verify.dev_server) then re-run remediator-gate"
41
+ }
42
+ },
43
+ "evidence_priority": ["typecheck", "build", "vitest", "go_test", "playwright"]
44
+ }
@@ -0,0 +1,87 @@
1
+ {
2
+ "version": 1,
3
+ "description": "SSOT rules for classifying Fallow findings before remediation scoring and wave planning",
4
+ "sources": {
5
+ "fallowrc": "frontend/.fallowrc.json",
6
+ "campaign_registry": "fallow-false-positives.yaml"
7
+ },
8
+ "path_globs": [
9
+ {
10
+ "id": "fallowrc_dynamically_loaded",
11
+ "glob": "from_fallowrc_dynamicallyLoaded",
12
+ "classification": "false_positive",
13
+ "reason": "dynamically_loaded_entry_point"
14
+ },
15
+ {
16
+ "id": "worker_hooks",
17
+ "glob": "src/hooks/*Worker*.ts",
18
+ "classification": "false_positive",
19
+ "reason": "worker_hook_runtime"
20
+ },
21
+ {
22
+ "id": "overlay_renderer_barrels",
23
+ "glob": "src/overlays/renderers/*/index.ts",
24
+ "classification": "false_positive",
25
+ "reason": "overlay_renderer_barrel"
26
+ }
27
+ ],
28
+ "path_regex": [
29
+ {
30
+ "id": "overlay_integration",
31
+ "pattern": "^src/overlays/integration/",
32
+ "classification": "review",
33
+ "reason": "overlay_integration_runtime"
34
+ },
35
+ {
36
+ "id": "lazy_route",
37
+ "pattern": "\\.lazy\\.(tsx?|jsx?)$",
38
+ "classification": "review",
39
+ "reason": "lazy_route_entry"
40
+ }
41
+ ],
42
+ "issue_heuristics": {
43
+ "unused_file": [
44
+ {
45
+ "id": "runtime_visibility_note",
46
+ "action_note_contains": "runtime functionality not visible to static analysis",
47
+ "classification": "review",
48
+ "reason": "runtime_visibility_risk"
49
+ }
50
+ ],
51
+ "unused_export": [
52
+ {
53
+ "id": "barrel_index_reexport",
54
+ "path_suffix": "/index.ts",
55
+ "is_re_export": true,
56
+ "classification": "review",
57
+ "reason": "barrel_reexport_facade"
58
+ },
59
+ {
60
+ "id": "barrel_index_any",
61
+ "path_suffix": "/index.ts",
62
+ "classification": "review",
63
+ "reason": "barrel_public_api"
64
+ }
65
+ ],
66
+ "unused_types": [
67
+ {
68
+ "id": "type_in_barrel",
69
+ "path_suffix": "/index.ts",
70
+ "classification": "review",
71
+ "reason": "barrel_type_facade"
72
+ }
73
+ ],
74
+ "unused_class_members": [
75
+ {
76
+ "id": "ws_shared_base_public_api",
77
+ "path_suffix": "websocketService.shared.ts",
78
+ "classification": "false_positive",
79
+ "reason": "base_class_public_api_inherited_by_subclasses"
80
+ }
81
+ ]
82
+ },
83
+ "scoring": {
84
+ "actionable_classifications": ["true_positive", "review"],
85
+ "excluded_from_satisfaction": ["false_positive"]
86
+ }
87
+ }