@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.
- package/README.md +27 -12
- package/package.json +9 -9
- package/src/cli.mjs +19 -7
- package/src/generators/generate-commands.mjs +25 -1
- package/src/generators/generate-graph.mjs +9 -1
- package/src/lib/install.mjs +13 -1
- package/src/lib/sweep-project-docs.mjs +348 -0
- package/src/run-engine/advance-phase.mjs +23 -0
- package/src/run-engine/debug-run.mjs +0 -0
- package/src/run-engine/gate-write.mjs +13 -0
- package/src/run-engine/lib.mjs +153 -5
- package/src/run-engine/log-dump.mjs +0 -0
- package/src/run-engine/log-trace.mjs +0 -0
- package/templates/cursor/aaac/enforcement.json +96 -5
- package/templates/cursor/aaac/graph.project.yaml +44 -5
- package/templates/cursor/aaac/lifecycle/lifecycle.json +26 -0
- package/templates/cursor/aaac/lifecycle/phases.json +9 -1
- package/templates/cursor/aaac/ontology.json +1 -0
- package/templates/cursor/aaac/project.config.json +36 -0
- package/templates/cursor/aaac/scripts/remediation/auto-check-swarm-synthesis.mjs +75 -0
- package/templates/cursor/aaac/scripts/remediation/auto-dispatch-queue-from-health.mjs +78 -0
- package/templates/cursor/aaac/scripts/remediation/bootstrap-autonomous.mjs +113 -0
- package/templates/cursor/aaac/scripts/remediation/capture-verify-baseline.mjs +66 -0
- package/templates/cursor/aaac/scripts/remediation/capture-wave-snapshot.mjs +79 -0
- package/templates/cursor/aaac/scripts/remediation/check-swarm-raw.template.json +26 -0
- package/templates/cursor/aaac/scripts/remediation/classify-fallow-issues.mjs +77 -0
- package/templates/cursor/aaac/scripts/remediation/classify-verify-failure.mjs +176 -0
- package/templates/cursor/aaac/scripts/remediation/compute-satisfaction.mjs +344 -0
- package/templates/cursor/aaac/scripts/remediation/debt-sweep-gate.mjs +202 -0
- package/templates/cursor/aaac/scripts/remediation/dispatch-rules.json +44 -0
- package/templates/cursor/aaac/scripts/remediation/fallow-fp-rules.json +87 -0
- package/templates/cursor/aaac/scripts/remediation/fallow-scan.mjs +219 -0
- package/templates/cursor/aaac/scripts/remediation/handle-yield.mjs +240 -0
- package/templates/cursor/aaac/scripts/remediation/init-campaign.mjs +211 -0
- package/templates/cursor/aaac/scripts/remediation/lib/autonomous-mode.mjs +63 -0
- package/templates/cursor/aaac/scripts/remediation/lib/campaign-focus.mjs +87 -0
- package/templates/cursor/aaac/scripts/remediation/lib/fallow-classifier.mjs +190 -0
- package/templates/cursor/aaac/scripts/remediation/lib/fallow-health-targets.mjs +56 -0
- package/templates/cursor/aaac/scripts/remediation/lib/fallow-metrics.mjs +119 -0
- package/templates/cursor/aaac/scripts/remediation/lib/invoke-cursor-agent.mjs +51 -0
- package/templates/cursor/aaac/scripts/remediation/lib/reconcile-run-manifest.mjs +41 -0
- package/templates/cursor/aaac/scripts/remediation/lib/regression-analysis.mjs +55 -0
- package/templates/cursor/aaac/scripts/remediation/lib/remediation-config.mjs +69 -0
- package/templates/cursor/aaac/scripts/remediation/lib/remediation-progress.mjs +58 -0
- package/templates/cursor/aaac/scripts/remediation/lib/remediation-watch-loop.mjs +168 -0
- package/templates/cursor/aaac/scripts/remediation/lib/runner-exec.mjs +156 -0
- package/templates/cursor/aaac/scripts/remediation/lib/runner-state.mjs +145 -0
- package/templates/cursor/aaac/scripts/remediation/lib/verify-metrics.mjs +205 -0
- package/templates/cursor/aaac/scripts/remediation/merge-check-swarm.mjs +257 -0
- package/templates/cursor/aaac/scripts/remediation/plan-waves-from-queue.mjs +85 -0
- package/templates/cursor/aaac/scripts/remediation/prepare-check-context.mjs +148 -0
- package/templates/cursor/aaac/scripts/remediation/record-fallow-fp.mjs +107 -0
- package/templates/cursor/aaac/scripts/remediation/record-iteration-step.mjs +56 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-cli.mjs +157 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-cursor-watch.sh +10 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-runner-daemon.sh +13 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-runner.mjs +748 -0
- package/templates/cursor/aaac/scripts/remediation/remediation-yield-watcher.mjs +40 -0
- package/templates/cursor/aaac/scripts/remediation/remediator-gate.mjs +405 -0
- package/templates/cursor/aaac/scripts/remediation/repair-fallow-start-baseline.mjs +118 -0
- package/templates/cursor/aaac/scripts/remediation/runner-health-check.mjs +164 -0
- package/templates/cursor/aaac/scripts/remediation/satisfaction-loop-gate.mjs +286 -0
- package/templates/cursor/aaac/scripts/remediation/validate-campaign-complete.mjs +191 -0
- package/templates/cursor/aaac/scripts/remediation/verify-remediation-iteration.mjs +112 -0
- package/templates/cursor/aaac/scripts/run-engine/advance-phase.mjs +23 -0
- package/templates/cursor/aaac/scripts/run-engine/debug-run.mjs +0 -0
- package/templates/cursor/aaac/scripts/run-engine/gate-write.mjs +13 -0
- package/templates/cursor/aaac/scripts/run-engine/lib.mjs +153 -5
- package/templates/cursor/aaac/scripts/run-engine/log-dump.mjs +0 -0
- package/templates/cursor/aaac/scripts/run-engine/log-trace.mjs +0 -0
- package/templates/cursor/agents/doc-conformance.md +25 -0
- package/templates/cursor/agents/implementation-review.md +21 -0
- package/templates/cursor/agents/remediation-check-app-inventory.md +32 -0
- package/templates/cursor/agents/remediation-check-app-ssot.md +24 -0
- package/templates/cursor/agents/remediation-check-app-trace.md +29 -0
- package/templates/cursor/agents/remediation-check-architecture-boundaries.md +21 -0
- package/templates/cursor/agents/remediation-check-architecture-decomposition.md +25 -0
- package/templates/cursor/agents/remediation-check-architecture-deps.md +23 -0
- package/templates/cursor/agents/remediation-check-risk.md +37 -0
- package/templates/cursor/agents/remediation-e2e-gate.md +30 -0
- package/templates/cursor/agents/remediation-remediator.md +69 -0
- package/templates/cursor/agents/test-author.md +27 -0
- package/templates/cursor/commands/remediate-app.md +212 -0
- package/templates/cursor/hooks/aaac-before-submit.sh +0 -0
- package/templates/cursor/hooks/aaac-pre-tool.sh +0 -0
- package/templates/cursor/hooks/aaac-stop.sh +0 -0
- package/templates/cursor/hooks/aaac-subagent-start.sh +0 -0
- package/templates/cursor/rules/aaac-enforcement.mdc +10 -3
- package/templates/cursor/skills/shared/execution/SKILL.md +7 -3
- package/templates/cursor/skills/shared/governance/implementation/SKILL.md +396 -28
- package/templates/cursor/skills/shared/implementation-review/SKILL.md +49 -0
- package/templates/cursor/skills/shared/planning/SKILL.md +5 -0
- package/templates/cursor/skills/shared/remediation/SKILL.md +51 -0
- package/templates/cursor/skills/shared/remediation/babysit/SKILL.md +223 -0
- package/templates/cursor/skills/shared/remediation/check-swarm/SKILL.md +114 -0
- package/templates/cursor/skills/shared/remediation/orchestrator/SKILL.md +275 -0
- package/templates/cursor/skills/shared/remediation/orchestrator/contract.yaml +116 -0
- package/templates/cursor/skills/shared/test-authoring/SKILL.md +58 -0
- package/templates/cursor/skills/shared/testing/SKILL.md +6 -0
- package/templates/cursor/skills/shared/verbs/create/orchestrator/SKILL.md +5 -3
- package/templates/cursor/skills/shared/verbs/fix/orchestrator/SKILL.md +5 -3
- package/templates/cursor/skills/shared/verbs/update/orchestrator/SKILL.md +5 -3
- package/templates/cursor/skills/shared/verification/SKILL.md +5 -3
- package/templates/docs/agentic_architecture.md +169 -97
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build dispatch-queue.yaml from Fallow health targets + campaign focus.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { campaignDir, loadCampaign } from "./lib/runner-state.mjs";
|
|
8
|
+
import { loadCampaignContext, normalizeRepoPath } from "./lib/campaign-focus.mjs";
|
|
9
|
+
import {
|
|
10
|
+
fetchHealthTargets,
|
|
11
|
+
filterTargetsForWaves,
|
|
12
|
+
targetToWaveIntent,
|
|
13
|
+
} from "./lib/fallow-health-targets.mjs";
|
|
14
|
+
import { journal } from "./lib/runner-exec.mjs";
|
|
15
|
+
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const out = { campaignId: null, iteration: null };
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
if (argv[i] === "--campaign-id") out.campaignId = argv[++i];
|
|
20
|
+
else if (argv[i] === "--iteration") out.iteration = Number(argv[++i]);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const args = parseArgs(process.argv.slice(2));
|
|
26
|
+
if (!args.campaignId) {
|
|
27
|
+
console.error("--campaign-id required");
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ctx = loadCampaignContext(args.campaignId);
|
|
32
|
+
const campaign = ctx.campaign;
|
|
33
|
+
const iteration = args.iteration ?? campaign.iteration ?? 0;
|
|
34
|
+
const maxWaves = campaign.config?.max_waves_per_iteration ?? 3;
|
|
35
|
+
|
|
36
|
+
const { targets } = fetchHealthTargets({ scope: ctx.scope, limit: 20 });
|
|
37
|
+
const filtered = filterTargetsForWaves(targets, {
|
|
38
|
+
protected_paths: ctx.protected_paths,
|
|
39
|
+
defer_high_fan_in: ctx.focus.defer_high_fan_in,
|
|
40
|
+
}).slice(0, maxWaves);
|
|
41
|
+
|
|
42
|
+
if (filtered.length === 0) {
|
|
43
|
+
console.error("No health targets available for waves");
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines = [
|
|
48
|
+
`# iteration ${iteration} health waves — auto from Fallow targets`,
|
|
49
|
+
`iteration: ${iteration}`,
|
|
50
|
+
`campaign_id: ${args.campaignId}`,
|
|
51
|
+
`scope: ${ctx.scope}`,
|
|
52
|
+
`intent_focus: health`,
|
|
53
|
+
"",
|
|
54
|
+
"protected_paths:",
|
|
55
|
+
...ctx.protected_paths.map((p) => ` - ${p}`),
|
|
56
|
+
"",
|
|
57
|
+
"waves:",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
61
|
+
const t = filtered[i];
|
|
62
|
+
const risk = t.effort === "high" ? "medium" : "low";
|
|
63
|
+
lines.push(`- priority: ${i + 1}`);
|
|
64
|
+
lines.push(` command: fix-module`);
|
|
65
|
+
lines.push(` intent: ${targetToWaveIntent(t)}`);
|
|
66
|
+
lines.push(` risk: ${risk}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const yaml = `${lines.join("\n")}\n`;
|
|
70
|
+
const dest = path.join(campaignDir(args.campaignId), "dispatch-queue.yaml");
|
|
71
|
+
const artifact = path.join(campaignDir(args.campaignId), "artifacts", "dispatch-queue.yaml");
|
|
72
|
+
fs.writeFileSync(dest, yaml);
|
|
73
|
+
fs.mkdirSync(path.dirname(artifact), { recursive: true });
|
|
74
|
+
fs.writeFileSync(artifact, yaml);
|
|
75
|
+
|
|
76
|
+
journal(args.campaignId, `- **Auto dispatch-queue** iter ${iteration}: ${filtered.length} health wave(s)`);
|
|
77
|
+
|
|
78
|
+
console.log(JSON.stringify({ ok: true, waves: filtered.length, paths: filtered.map((t) => t.path) }));
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Write run + campaign artifacts when autonomous mode is active.
|
|
4
|
+
* Called from init-campaign.mjs — orchestrator MUST follow bootstrap.next_action.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node bootstrap-autonomous.mjs --run-id <id> --campaign-id <id>
|
|
8
|
+
*/
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { REPO_ROOT, isoNow, readJson, runDir, writeJson } from "../run-engine/lib.mjs";
|
|
12
|
+
import {
|
|
13
|
+
autonomousBootstrapCommands,
|
|
14
|
+
BABYSIT_SKILL,
|
|
15
|
+
} from "./lib/autonomous-mode.mjs";
|
|
16
|
+
import { loadRunnerState, loadYield, runnerStatePath, yieldArtifactPath } from "./lib/runner-state.mjs";
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const out = { runId: null, campaignId: null };
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const a = argv[i];
|
|
22
|
+
if (a === "--run-id") out.runId = argv[++i];
|
|
23
|
+
else if (a === "--campaign-id") out.campaignId = argv[++i];
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const args = parseArgs(process.argv.slice(2));
|
|
29
|
+
if (!args.runId || !args.campaignId) {
|
|
30
|
+
console.error("bootstrap-autonomous: --run-id and --campaign-id required");
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const campaignPath = path.join(
|
|
35
|
+
REPO_ROOT,
|
|
36
|
+
".cursor/aaac/state/campaigns",
|
|
37
|
+
args.campaignId,
|
|
38
|
+
"campaign.json",
|
|
39
|
+
);
|
|
40
|
+
const campaign = readJson(campaignPath, null);
|
|
41
|
+
if (!campaign?.config?.autonomous) {
|
|
42
|
+
console.log(JSON.stringify({ ok: true, autonomous: false, skipped: true }));
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const commands = autonomousBootstrapCommands(args.runId, args.campaignId);
|
|
47
|
+
const runnerState = loadRunnerState(args.campaignId);
|
|
48
|
+
const yieldPayload = loadYield(args.campaignId);
|
|
49
|
+
|
|
50
|
+
const bootstrap = {
|
|
51
|
+
ok: true,
|
|
52
|
+
autonomous: true,
|
|
53
|
+
autonomous_reason: campaign.config.autonomous_reason,
|
|
54
|
+
campaign_id: args.campaignId,
|
|
55
|
+
run_id: args.runId,
|
|
56
|
+
iteration: campaign.iteration,
|
|
57
|
+
satisfaction_threshold: campaign.config.satisfaction_threshold,
|
|
58
|
+
orchestrator_mode: "shell_runner_yield_watcher",
|
|
59
|
+
skill_required: BABYSIT_SKILL,
|
|
60
|
+
orchestrator_must_not: [
|
|
61
|
+
"walk_phases_manually_in_chat",
|
|
62
|
+
"end_turn_before_runner_exit_0",
|
|
63
|
+
"report_when_satisfaction_below_threshold",
|
|
64
|
+
],
|
|
65
|
+
loop: [
|
|
66
|
+
"read_babysit_skill",
|
|
67
|
+
"runner_health_check",
|
|
68
|
+
"remediation_yield_watcher",
|
|
69
|
+
"if_exit_3_handle_yield_then_ack_yield",
|
|
70
|
+
"repeat_until_exit_0",
|
|
71
|
+
],
|
|
72
|
+
commands,
|
|
73
|
+
runner_state_path: runnerStatePath(args.campaignId),
|
|
74
|
+
yield_path: yieldArtifactPath(args.campaignId),
|
|
75
|
+
current_runner: runnerState,
|
|
76
|
+
pending_yield: yieldPayload,
|
|
77
|
+
next_action: yieldPayload
|
|
78
|
+
? {
|
|
79
|
+
type: "handle_yield",
|
|
80
|
+
yield_type: yieldPayload.type,
|
|
81
|
+
ack_command: `node .cursor/aaac/scripts/remediation/remediation-runner.mjs --run-id ${args.runId} --campaign-id ${args.campaignId} --ack-yield ${yieldPayload.type}`,
|
|
82
|
+
}
|
|
83
|
+
: {
|
|
84
|
+
type: "run_until_yield",
|
|
85
|
+
command: commands.runner_until_yield,
|
|
86
|
+
},
|
|
87
|
+
at: isoNow(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const runArtifact = path.join(runDir(args.runId), "artifacts", "autonomous_bootstrap.json");
|
|
91
|
+
writeJson(runArtifact, bootstrap);
|
|
92
|
+
|
|
93
|
+
const campaignArtifact = path.join(
|
|
94
|
+
REPO_ROOT,
|
|
95
|
+
".cursor/aaac/state/campaigns",
|
|
96
|
+
args.campaignId,
|
|
97
|
+
"autonomous-bootstrap.json",
|
|
98
|
+
);
|
|
99
|
+
writeJson(campaignArtifact, bootstrap);
|
|
100
|
+
|
|
101
|
+
appendJournal(args.campaignId, `- **Autonomous mode** — ${campaign.config.autonomous_reason}; bootstrap written`);
|
|
102
|
+
|
|
103
|
+
console.log(JSON.stringify(bootstrap));
|
|
104
|
+
|
|
105
|
+
function appendJournal(campaignId, line) {
|
|
106
|
+
const journalPath = path.join(
|
|
107
|
+
REPO_ROOT,
|
|
108
|
+
".cursor/aaac/state/campaigns",
|
|
109
|
+
campaignId,
|
|
110
|
+
"journal.md",
|
|
111
|
+
);
|
|
112
|
+
fs.appendFileSync(journalPath, `\n${line}\n`);
|
|
113
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Capture campaign verify baseline at start (before any waves).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node capture-verify-baseline.mjs --campaign-id <id> [--run-id <run_id>]
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { REPO_ROOT, isoNow, readJson, writeJson, runDir } from "../run-engine/lib.mjs";
|
|
11
|
+
import {
|
|
12
|
+
runVerifySteps,
|
|
13
|
+
writeVerifyLogs,
|
|
14
|
+
summarizeMetrics,
|
|
15
|
+
} from "./lib/verify-metrics.mjs";
|
|
16
|
+
|
|
17
|
+
const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const out = { campaignId: null, runId: null };
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
if (argv[i] === "--campaign-id") out.campaignId = argv[++i];
|
|
23
|
+
else if (argv[i] === "--run-id") out.runId = argv[++i];
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const args = parseArgs(process.argv.slice(2));
|
|
29
|
+
if (!args.campaignId) {
|
|
30
|
+
console.error("capture-verify-baseline: --campaign-id required");
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const campaignDir = path.join(CAMPAIGNS_ROOT, args.campaignId);
|
|
35
|
+
const baselineDir = path.join(campaignDir, "baseline");
|
|
36
|
+
fs.mkdirSync(baselineDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
const report = await runVerifySteps("wave");
|
|
39
|
+
writeVerifyLogs(report, baselineDir, "baseline");
|
|
40
|
+
|
|
41
|
+
const snapshot = {
|
|
42
|
+
captured_at: isoNow(),
|
|
43
|
+
kind: "campaign_verify_baseline",
|
|
44
|
+
summary: summarizeMetrics(report),
|
|
45
|
+
metrics: report.metrics,
|
|
46
|
+
report_path: path.join(baselineDir, "verify-baseline.json"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
writeJson(snapshot.report_path, report);
|
|
50
|
+
writeJson(path.join(campaignDir, "verify-baseline.json"), snapshot);
|
|
51
|
+
|
|
52
|
+
const campaign = readJson(path.join(campaignDir, "campaign.json"), {});
|
|
53
|
+
if (campaign) {
|
|
54
|
+
campaign.verify_baseline = snapshot.summary;
|
|
55
|
+
campaign.updated_at = isoNow();
|
|
56
|
+
writeJson(path.join(campaignDir, "campaign.json"), campaign);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (args.runId) {
|
|
60
|
+
writeJson(path.join(runDir(args.runId), "artifacts", "verify_baseline.json"), snapshot);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const journal = `\n- **Verify baseline captured** — total_errors=${snapshot.summary.total_errors} (typecheck ${snapshot.summary.layers.typecheck.error_count}, vitest ${snapshot.summary.layers.vitest.error_count})\n`;
|
|
64
|
+
fs.appendFileSync(path.join(campaignDir, "journal.md"), journal);
|
|
65
|
+
|
|
66
|
+
console.log(JSON.stringify({ ok: true, baseline: snapshot }));
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot verify metrics immediately before a cleanup wave executes.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node capture-wave-snapshot.mjs --campaign-id <id> --iteration <n> --wave-index <w>
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { REPO_ROOT, isoNow, writeJson } from "../run-engine/lib.mjs";
|
|
13
|
+
import { summarizeMetrics } from "./lib/verify-metrics.mjs";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
|
|
17
|
+
const VERIFY_SCRIPT = path.join(__dirname, "verify-remediation-iteration.mjs");
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const out = { campaignId: null, iteration: 0, waveIndex: 0 };
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
if (argv[i] === "--campaign-id") out.campaignId = argv[++i];
|
|
23
|
+
else if (argv[i] === "--iteration") out.iteration = Number(argv[++i]);
|
|
24
|
+
else if (argv[i] === "--wave-index") out.waveIndex = Number(argv[++i]);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const args = parseArgs(process.argv.slice(2));
|
|
30
|
+
if (!args.campaignId) {
|
|
31
|
+
console.error("capture-wave-snapshot: --campaign-id required");
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
spawnSync(
|
|
36
|
+
process.execPath,
|
|
37
|
+
[
|
|
38
|
+
VERIFY_SCRIPT,
|
|
39
|
+
"--campaign-id",
|
|
40
|
+
args.campaignId,
|
|
41
|
+
"--iteration",
|
|
42
|
+
String(args.iteration),
|
|
43
|
+
"--mode",
|
|
44
|
+
"wave",
|
|
45
|
+
"--label",
|
|
46
|
+
`pre-wave-${args.waveIndex}`,
|
|
47
|
+
],
|
|
48
|
+
{ encoding: "utf8" },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const iterDir = path.join(
|
|
52
|
+
CAMPAIGNS_ROOT,
|
|
53
|
+
args.campaignId,
|
|
54
|
+
"iterations",
|
|
55
|
+
String(args.iteration),
|
|
56
|
+
);
|
|
57
|
+
const prePath = path.join(iterDir, `wave-${args.waveIndex}-pre.json`);
|
|
58
|
+
const verifyPath = path.join(iterDir, "verify-wave.json");
|
|
59
|
+
|
|
60
|
+
let report = null;
|
|
61
|
+
try {
|
|
62
|
+
report = JSON.parse(fs.readFileSync(verifyPath, "utf8"));
|
|
63
|
+
} catch {
|
|
64
|
+
console.error("capture-wave-snapshot: missing verify-wave.json");
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const snapshot = {
|
|
69
|
+
captured_at: isoNow(),
|
|
70
|
+
kind: "pre_wave",
|
|
71
|
+
wave_index: args.waveIndex,
|
|
72
|
+
iteration: args.iteration,
|
|
73
|
+
summary: summarizeMetrics(report),
|
|
74
|
+
metrics: report.metrics,
|
|
75
|
+
};
|
|
76
|
+
writeJson(prePath, snapshot);
|
|
77
|
+
fs.copyFileSync(verifyPath, path.join(iterDir, `wave-${args.waveIndex}-pre-verify.json`));
|
|
78
|
+
|
|
79
|
+
console.log(JSON.stringify({ ok: true, snapshot_path: prePath, summary: snapshot.summary }));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"campaign_id": "campaign_EXAMPLE",
|
|
3
|
+
"iteration": 0,
|
|
4
|
+
"collected_at": "ISO8601",
|
|
5
|
+
"agents": [
|
|
6
|
+
{
|
|
7
|
+
"agent_id": "remediation-check-app-inventory",
|
|
8
|
+
"command_mirror": "check-app",
|
|
9
|
+
"answer": "partial",
|
|
10
|
+
"confidence": "high",
|
|
11
|
+
"false_positives": [
|
|
12
|
+
{
|
|
13
|
+
"path": "src/hooks/useCryptoPriceWorker.ts",
|
|
14
|
+
"export_name": null,
|
|
15
|
+
"reason": "worker_hook_runtime",
|
|
16
|
+
"evidence": "src/hooks/useCryptoPriceWorker.ts:1"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"protected_paths": ["src/hooks/useCryptoPriceWorker.ts"],
|
|
20
|
+
"do_not_delete": [],
|
|
21
|
+
"safe_to_fix": [],
|
|
22
|
+
"findings": [],
|
|
23
|
+
"gaps": []
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Classify Fallow scan findings and persist actionable vs false-positive SSOT.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node classify-fallow-issues.mjs --campaign-id <id> --iteration <n> [--save-actionable-baseline]
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { REPO_ROOT, isoNow, readJson, writeJson } from "../run-engine/lib.mjs";
|
|
11
|
+
import { classifyFallowScan, loadFpRules } from "./lib/fallow-classifier.mjs";
|
|
12
|
+
|
|
13
|
+
const CAMPAIGNS_ROOT = path.join(REPO_ROOT, ".cursor/aaac/state/campaigns");
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const out = { campaignId: null, iteration: 0, saveActionableBaseline: false };
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a === "--campaign-id") out.campaignId = argv[++i];
|
|
20
|
+
else if (a === "--iteration") out.iteration = Number(argv[++i]);
|
|
21
|
+
else if (a === "--save-actionable-baseline") out.saveActionableBaseline = true;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const args = parseArgs(process.argv.slice(2));
|
|
27
|
+
if (!args.campaignId) {
|
|
28
|
+
console.error("classify-fallow-issues: --campaign-id required");
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const campaignDir = path.join(CAMPAIGNS_ROOT, args.campaignId);
|
|
33
|
+
const iterDir = path.join(campaignDir, "iterations", String(args.iteration));
|
|
34
|
+
const scanPath = path.join(iterDir, "fallow-scan.json");
|
|
35
|
+
const scan = readJson(scanPath, null);
|
|
36
|
+
|
|
37
|
+
if (!scan) {
|
|
38
|
+
console.error(`classify-fallow-issues: missing ${scanPath}`);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const classification = classifyFallowScan({
|
|
43
|
+
scan,
|
|
44
|
+
campaignDir,
|
|
45
|
+
rules: loadFpRules(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const outPath = path.join(iterDir, "fallow-classification.json");
|
|
49
|
+
writeJson(outPath, classification);
|
|
50
|
+
|
|
51
|
+
const actionableBaselinePath = path.join(campaignDir, "fallow-start-actionable-baseline.json");
|
|
52
|
+
if (args.saveActionableBaseline && !fs.existsSync(actionableBaselinePath)) {
|
|
53
|
+
writeJson(actionableBaselinePath, {
|
|
54
|
+
actionable_total: classification.summary.actionable_total,
|
|
55
|
+
raw_total: classification.summary.raw_total,
|
|
56
|
+
false_positive_total: classification.summary.false_positive_total,
|
|
57
|
+
recorded_at: isoNow(),
|
|
58
|
+
immutable: true,
|
|
59
|
+
source: "classify-fallow-issues",
|
|
60
|
+
iteration: args.iteration,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const campaign = readJson(path.join(campaignDir, "campaign.json"), null);
|
|
65
|
+
if (campaign) {
|
|
66
|
+
campaign.current = campaign.current ?? {};
|
|
67
|
+
campaign.current.fallow_raw_total = classification.summary.raw_total;
|
|
68
|
+
campaign.current.fallow_actionable_total = classification.summary.actionable_total;
|
|
69
|
+
campaign.current.fallow_false_positive_total = classification.summary.false_positive_total;
|
|
70
|
+
campaign.updated_at = isoNow();
|
|
71
|
+
writeJson(path.join(campaignDir, "campaign.json"), campaign);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const journal = `\n- **Fallow classified** iter ${args.iteration}: raw=${classification.summary.raw_total}, actionable=${classification.summary.actionable_total}, false_positive=${classification.summary.false_positive_total}, review=${classification.summary.review_total}\n`;
|
|
75
|
+
fs.appendFileSync(path.join(campaignDir, "journal.md"), journal);
|
|
76
|
+
|
|
77
|
+
console.log(JSON.stringify({ ok: true, classification: classification.summary, path: outPath }));
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Classify verify-remediation report failures → fix-module / fix-bug handoffs.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node classify-verify-failure.mjs --report <path> --campaign-id <id> [--iteration n] [--wave-index n] [--attempt n]
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { REPO_ROOT, readJson } from "../run-engine/lib.mjs";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const RULES_PATH = path.join(__dirname, "dispatch-rules.json");
|
|
15
|
+
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const out = {
|
|
18
|
+
reportPath: null,
|
|
19
|
+
campaignId: "",
|
|
20
|
+
iteration: 0,
|
|
21
|
+
waveIndex: null,
|
|
22
|
+
attempt: 1,
|
|
23
|
+
};
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === "--report") out.reportPath = argv[++i];
|
|
27
|
+
else if (a === "--campaign-id") out.campaignId = argv[++i];
|
|
28
|
+
else if (a === "--iteration") out.iteration = Number(argv[++i]);
|
|
29
|
+
else if (a === "--wave-index") out.waveIndex = Number(argv[++i]);
|
|
30
|
+
else if (a === "--attempt") out.attempt = Number(argv[++i]);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadRules() {
|
|
36
|
+
return readJson(RULES_PATH, { layers: {}, evidence_priority: [] });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readLogFile(logPath) {
|
|
40
|
+
if (!logPath || !fs.existsSync(logPath)) return "";
|
|
41
|
+
try {
|
|
42
|
+
return fs.readFileSync(logPath, "utf8");
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function evidenceText(step) {
|
|
49
|
+
if (!step) return "";
|
|
50
|
+
const fromLog = readLogFile(step.log_path);
|
|
51
|
+
if (fromLog) return fromLog;
|
|
52
|
+
return [
|
|
53
|
+
step.stdout_full,
|
|
54
|
+
step.stderr_full,
|
|
55
|
+
step.stderr_tail,
|
|
56
|
+
step.stdout_tail,
|
|
57
|
+
step.detail,
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractFilePaths(text) {
|
|
64
|
+
const paths = new Set();
|
|
65
|
+
const patterns = [
|
|
66
|
+
/(?:frontend|backend)\/[^\s:'"]+\.(?:tsx?|jsx?|go)/g,
|
|
67
|
+
/src\/[^\s:'"]+\.(?:tsx?|jsx?)/g,
|
|
68
|
+
/internal\/[^\s:'"]+\.go/g,
|
|
69
|
+
/[^\s('"]+\.(?:test|spec)\.(?:tsx?|jsx?)/g,
|
|
70
|
+
];
|
|
71
|
+
for (const re of patterns) {
|
|
72
|
+
for (const m of text.matchAll(re)) {
|
|
73
|
+
paths.add(m[0].replace(/^[('"]+/, ""));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...paths];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function inferDomain(layer, evidence, ruleDomain) {
|
|
80
|
+
const paths = extractFilePaths(evidence);
|
|
81
|
+
if (paths.some((p) => p.startsWith("backend/") || p.includes("/internal/") || p.endsWith(".go"))) {
|
|
82
|
+
return "backend";
|
|
83
|
+
}
|
|
84
|
+
if (paths.some((p) => p.startsWith("frontend/") || p.startsWith("src/"))) {
|
|
85
|
+
return "frontend";
|
|
86
|
+
}
|
|
87
|
+
return ruleDomain ?? "frontend";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function pickCommand(layer, rule, evidence) {
|
|
91
|
+
if (layer === "playwright" && /frontend not reachable|launch via/i.test(evidence)) {
|
|
92
|
+
return { command: null, level: "infrastructure", handoff: rule.layers?.playwright_infra?.handoff ?? "/launch-se100" };
|
|
93
|
+
}
|
|
94
|
+
if (layer === "playwright" && /does not provide an export|Failed to fetch dynamically imported module|Cannot find module/i.test(evidence)) {
|
|
95
|
+
return { command: rule.module_import_command ?? "fix-module", level: "code" };
|
|
96
|
+
}
|
|
97
|
+
if (layer === "vitest" && /\.(test|spec)\.(tsx?|jsx?)/.test(evidence) && !/src\/(?!.*\.test\.)/.test(evidence.split("\n")[0] ?? "")) {
|
|
98
|
+
return { command: rule.test_file_command ?? rule.command, level: "test" };
|
|
99
|
+
}
|
|
100
|
+
return { command: rule.command, level: rule.level ?? "code" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fillTemplate(template, vars) {
|
|
104
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? ""));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function classifyReport(report, ctx) {
|
|
108
|
+
const rules = loadRules();
|
|
109
|
+
const failedLayers = rules.evidence_priority.filter((layer) => report[layer]?.status === "fail");
|
|
110
|
+
const handoffs = [];
|
|
111
|
+
|
|
112
|
+
for (const layer of failedLayers) {
|
|
113
|
+
const rule = rules.layers[layer];
|
|
114
|
+
if (!rule) continue;
|
|
115
|
+
const evidence = evidenceText(report[layer]);
|
|
116
|
+
const picked = pickCommand(layer, rule, evidence);
|
|
117
|
+
if (picked.level === "infrastructure") {
|
|
118
|
+
handoffs.push({
|
|
119
|
+
layer,
|
|
120
|
+
level: "infrastructure",
|
|
121
|
+
command: null,
|
|
122
|
+
domain: null,
|
|
123
|
+
intent: null,
|
|
124
|
+
handoff: picked.handoff,
|
|
125
|
+
evidence: evidence.slice(0, 16000),
|
|
126
|
+
evidence_truncated: evidence.length > 16000,
|
|
127
|
+
log_path: report[layer]?.log_path ?? null,
|
|
128
|
+
file_paths: extractFilePaths(evidence),
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const domain = inferDomain(layer, evidence, rule.domain);
|
|
133
|
+
const intent = fillTemplate(rule.intent_template, {
|
|
134
|
+
campaign_id: ctx.campaignId,
|
|
135
|
+
iteration: ctx.iteration,
|
|
136
|
+
wave_index: ctx.waveIndex ?? "n/a",
|
|
137
|
+
attempt: ctx.attempt,
|
|
138
|
+
evidence: evidence.slice(0, 16000),
|
|
139
|
+
});
|
|
140
|
+
handoffs.push({
|
|
141
|
+
layer,
|
|
142
|
+
level: picked.level,
|
|
143
|
+
command: picked.command,
|
|
144
|
+
domain,
|
|
145
|
+
intent,
|
|
146
|
+
slash_command: `/${picked.command} ${domain} "${intent.replace(/"/g, '\\"').slice(0, 500)}…"`,
|
|
147
|
+
evidence: evidence.slice(0, 16000),
|
|
148
|
+
evidence_truncated: evidence.length > 16000,
|
|
149
|
+
log_path: report[layer]?.log_path ?? null,
|
|
150
|
+
file_paths: extractFilePaths(evidence),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
status: handoffs.length ? "fail" : "pass",
|
|
156
|
+
failed_layers: failedLayers,
|
|
157
|
+
handoffs,
|
|
158
|
+
primary: handoffs.find((h) => h.command) ?? handoffs[0] ?? null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const args = parseArgs(process.argv.slice(2));
|
|
163
|
+
if (!args.reportPath) {
|
|
164
|
+
console.error("classify-verify-failure: --report required");
|
|
165
|
+
process.exit(2);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const report = readJson(args.reportPath, null);
|
|
169
|
+
if (!report) {
|
|
170
|
+
console.error(`classify-verify-failure: cannot read report ${args.reportPath}`);
|
|
171
|
+
process.exit(2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const classification = classifyReport(report, args);
|
|
175
|
+
console.log(JSON.stringify({ ok: true, classification }));
|
|
176
|
+
process.exit(classification.status === "pass" ? 0 : 1);
|