@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.3
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/LICENSE +21 -0
- package/README.md +141 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/changes-62ZIEQSN.js +1 -0
- package/public/assets/chunk-2PZVOKID.js +1 -0
- package/public/assets/chunk-4D5IENEB.js +1 -0
- package/public/assets/chunk-5SARP5MU.js +1 -0
- package/public/assets/chunk-63YLZ3PR.js +1 -0
- package/public/assets/chunk-7FUHKF2M.js +1 -0
- package/public/assets/chunk-APQEVJ2K.js +1 -0
- package/public/assets/chunk-CGOZQMEZ.js +2 -0
- package/public/assets/chunk-J4VYS3KV.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-SIHDWM2Y.js +154 -0
- package/public/assets/chunk-TEJVYIKC.js +1 -0
- package/public/assets/code-browser-VB2HGZSG.js +4 -0
- package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
- package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
- package/public/assets/join-project-6RTFHNCA.js +1 -0
- package/public/assets/main-65EW73SB.js +1 -0
- package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
- package/public/assets/settings-UB5Q4AQM.js +9 -0
- package/public/assets/styles-18792BB4.css +32 -0
- package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
- package/public/assets/terminal-IWAIM4DE.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +148 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +143 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +88 -0
- package/public/docs/installation.html +80 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +129 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +65 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +412 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +131 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/computer-use.swift +64 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agent-tools.js +28 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +242 -0
- package/src/adapters/agy-tools.js +45 -0
- package/src/adapters/agy.js +326 -0
- package/src/adapters/claude-stream.js +58 -0
- package/src/adapters/claude.js +282 -0
- package/src/adapters/codex-app-server.js +555 -0
- package/src/adapters/codex.js +311 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +433 -0
- package/src/adapters/team-tools.js +103 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-browser.js +688 -0
- package/src/agent-computer.js +88 -0
- package/src/agent-container.js +155 -0
- package/src/agent-team-definitions.js +131 -0
- package/src/agent-tool-client.mjs +79 -0
- package/src/agent-tools.js +296 -0
- package/src/approvals.js +134 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/browser-files.js +293 -0
- package/src/browser-ownership.js +150 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-family.js +25 -0
- package/src/chat-handoff.js +87 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/documentation-changes.js +20 -0
- package/src/engine.js +5258 -0
- package/src/env.js +44 -0
- package/src/execution-budget.js +8 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/frame-policy.js +66 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/local-app-url.js +19 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +239 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +230 -0
- package/src/patterns/auto-team.js +816 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +21 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +46 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/task-review.js +45 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +392 -0
- package/src/preview.js +230 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-budget.js +22 -0
- package/src/project-team-model.js +225 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +234 -0
- package/src/project-team-usage.js +24 -0
- package/src/project-teams-api.js +43 -0
- package/src/project-teams.js +644 -0
- package/src/projects.js +290 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +725 -0
- package/src/repo-tree.js +106 -0
- package/src/repository-identity.js +13 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +111 -0
- package/src/run-projection.js +77 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +56 -0
- package/src/serve.js +138 -0
- package/src/server.js +2871 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +381 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +133 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/task-team.js +164 -0
- package/src/team-evaluation.js +243 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-steering.js +21 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +235 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { getAdapter } from "../adapters/index.js";
|
|
2
|
+
import { failureFingerprint } from "../checks.js";
|
|
3
|
+
import { changedFiles, restorePaths } from "../git.js";
|
|
4
|
+
import { describeIssues, issuesKey } from "../integrity.js";
|
|
5
|
+
import { checkSection, preamble, proofLine, proofRule, section, WRITE } from "./prompts.js";
|
|
6
|
+
const loopPattern = {
|
|
7
|
+
id: "loop",
|
|
8
|
+
name: "Loop until green",
|
|
9
|
+
summary: "An agent keeps fixing until your check passes; another vendor takes over if it gets stuck.",
|
|
10
|
+
description: "Use when \u201Cdone\u201D is a command you can run: tests, a type check, a build. code-flow runs it after every change, so the loop ends on evidence, not on an agent saying it's done.",
|
|
11
|
+
requiresCheck: true,
|
|
12
|
+
roles: [
|
|
13
|
+
{
|
|
14
|
+
id: "workers",
|
|
15
|
+
label: "Workers",
|
|
16
|
+
description: "Take turns in this order; the next one takes over when the current one is stuck",
|
|
17
|
+
multiple: true,
|
|
18
|
+
default: ["codex", "claude"]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
options: [
|
|
22
|
+
{ id: "maxRounds", label: "Max rounds", type: "number", default: 6, min: 1, max: 30 },
|
|
23
|
+
{ id: "stuckAfter", label: "Hand off after stuck rounds", type: "number", default: 2, min: 1, max: 10 }
|
|
24
|
+
],
|
|
25
|
+
async run(ctx) {
|
|
26
|
+
const workers = ctx.participants("workers");
|
|
27
|
+
if (!workers.length) throw new Error("Pick at least one worker");
|
|
28
|
+
const maxRounds = ctx.option("maxRounds", 6);
|
|
29
|
+
const stuckAfter = ctx.option("stuckAfter", 2);
|
|
30
|
+
const name = (id) => getAdapter(id).displayName;
|
|
31
|
+
const wt = await ctx.worktree("loop");
|
|
32
|
+
const baseline = await ctx.check(wt, "Baseline check");
|
|
33
|
+
if (!baseline) {
|
|
34
|
+
const reason = ctx.run.checkCantRun?.reason ?? "it can't run in this environment";
|
|
35
|
+
return `**No rounds run: the check can't run here** \u2014 ${reason}. Set \`setup\` and \`check\` in .codeflow/config.json, then start the run again.`;
|
|
36
|
+
}
|
|
37
|
+
ctx.log(baseline.ok ? "Baseline: the check already passes; the task must keep it passing" : "Baseline: the check fails before any change");
|
|
38
|
+
const plural = (n) => `${n} test${n === 1 ? "" : "s"}`;
|
|
39
|
+
const distance = (r) => r.ok ? 0 : r.failed === void 0 ? void 0 : r.failed + 0.5;
|
|
40
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
41
|
+
const history = [];
|
|
42
|
+
let current = 0;
|
|
43
|
+
let stuck = 0;
|
|
44
|
+
let last = baseline;
|
|
45
|
+
let handover = "";
|
|
46
|
+
let rounds = 0;
|
|
47
|
+
let toldAbout = "";
|
|
48
|
+
let unresolved = [];
|
|
49
|
+
let nudge = "";
|
|
50
|
+
let askedForProof = false;
|
|
51
|
+
let solvedBy;
|
|
52
|
+
let handoffs = 0;
|
|
53
|
+
let head = wt.base;
|
|
54
|
+
let best = {
|
|
55
|
+
head,
|
|
56
|
+
distance: distance(baseline),
|
|
57
|
+
failed: baseline.failed ?? 0,
|
|
58
|
+
where: "before any change"
|
|
59
|
+
};
|
|
60
|
+
let undone = "";
|
|
61
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
62
|
+
rounds = round;
|
|
63
|
+
const { agent, settings, identity } = workers[current];
|
|
64
|
+
const session = sessions.get(identity ?? agent);
|
|
65
|
+
const fixIt = nudge || checkSection(last) + section("Next", "Find the root cause and fix it in the code \u2014 don't weaken or skip tests. Then summarize what you changed.");
|
|
66
|
+
const prompt = session ? undone ? undone + fixIt : nudge || "The check still fails after your last change." + fixIt : preamble("worker", agent, `${WRITE} code-flow runs the check below after every turn you take; the loop ends when it passes.`) + section("Task", ctx.task) + section("Done when", `\`${ctx.run.loop.check}\` passes. Add or update tests for new behaviour, so passing the check proves the task works.`) + proofRule(ctx) + (handover ? section("Handover", handover) : "") + checkSection(last) + nudge;
|
|
67
|
+
nudge = "";
|
|
68
|
+
undone = "";
|
|
69
|
+
const turn = await ctx.ask({ title: `Round ${round}`, role: "workers", agent, settings, identity, mode: "write", cwd: wt, resume: session, prompt });
|
|
70
|
+
sessions.set(identity ?? agent, turn);
|
|
71
|
+
const sha = await ctx.commit(wt, `${agent}: round ${round}`);
|
|
72
|
+
if (sha) head = sha;
|
|
73
|
+
let result = await ctx.check(wt, `Check after round ${round}`);
|
|
74
|
+
const outcome = result.ok ? "passed" : result.failed !== void 0 ? `failed (${plural(result.failed)} failing)` : "failed";
|
|
75
|
+
history.push(`- Round ${round} (${name(agent)}): ${sha ? "changed code" : "no changes"}, check ${outcome}`);
|
|
76
|
+
const reached = distance(result);
|
|
77
|
+
const worse = !!sha && best.distance !== void 0 && (reached === void 0 || reached > best.distance);
|
|
78
|
+
if (worse) {
|
|
79
|
+
const what = result.failed !== void 0 ? `left ${plural(result.failed)} failing, more than the ${best.failed} ${best.where}` : `left no test results, where there were some ${best.where}`;
|
|
80
|
+
ctx.log(`Round ${round} ${what}; undoing it`, "warn");
|
|
81
|
+
const changed = await ctx.remember("changed", `${wt.name}|undo round ${round}`, async () => changedFiles(await ctx.gitAt(wt), best.head));
|
|
82
|
+
await ctx.remember("restore", `${wt.name}|undo round ${round}`, () => restorePaths(ctx.run.repo, wt, best.head, changed).then(() => true));
|
|
83
|
+
head = await ctx.commit(wt, `code-flow: undo round ${round}, which made the check worse`) ?? head;
|
|
84
|
+
history[history.length - 1] += ` \u2014 worse than ${best.where}, so code-flow undid it`;
|
|
85
|
+
undone = `Your last change ${what}, so code-flow undid it: the code is as it was ${best.where} (\`git log\` shows both). Take a different approach.`;
|
|
86
|
+
result = await ctx.check(wt, `Check after undoing round ${round}`);
|
|
87
|
+
}
|
|
88
|
+
const before = distance(last);
|
|
89
|
+
const after = distance(result);
|
|
90
|
+
const counted = before !== void 0 && after !== void 0 && !(after === before && result.failed === 0);
|
|
91
|
+
const progress = !worse && !!sha && (result.ok || (counted ? after < before : failureFingerprint(result) !== failureFingerprint(last)));
|
|
92
|
+
last = result;
|
|
93
|
+
if (!worse && !result.ok && after !== void 0 && (best.distance === void 0 || after <= best.distance)) {
|
|
94
|
+
best = { head, distance: after, failed: result.failed ?? 0, where: `after round ${round}`, agent };
|
|
95
|
+
}
|
|
96
|
+
if (result.ok) {
|
|
97
|
+
const uncounted = baseline.tests === void 0 || result.tests === void 0;
|
|
98
|
+
const issues = (await ctx.testChanges(wt)).filter((i) => i.severity === "high" || uncounted && i.kind === "weakened-test");
|
|
99
|
+
unresolved = issues;
|
|
100
|
+
if (!issues.length) {
|
|
101
|
+
if (!worse) best = { head, distance: 0, failed: 0, where: `after round ${round}`, agent };
|
|
102
|
+
const proof = await ctx.prove(wt, { known: baseline });
|
|
103
|
+
if (proof?.status === "unproven" && !askedForProof && round < maxRounds) {
|
|
104
|
+
askedForProof = true;
|
|
105
|
+
history[history.length - 1] += " \u2014 but no test fails without the change";
|
|
106
|
+
nudge = section(
|
|
107
|
+
"The check passes, but nothing proves the change",
|
|
108
|
+
"code-flow ran this change's tests on the original code, without the change, and they pass there too: nothing shows the change does what the task asks.\n\nAdd or update a test that fails without your change and passes with it. If the task can't be tested that way (docs, a pure refactor), change nothing and say why."
|
|
109
|
+
);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (proof?.status === "unproven") ctx.log(`The check passes, but no test fails without the change; review it before merging`, "warn");
|
|
113
|
+
solvedBy = worse ? best.agent : agent;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (issuesKey(issues) === toldAbout) {
|
|
117
|
+
ctx.log(`${name(agent)} kept its test changes after being asked to undo them; the pass doesn't count until you've reviewed them`, "warn");
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
toldAbout = issuesKey(issues);
|
|
121
|
+
ctx.log(`The check passes, but only after changes to the tests that could hide the failure:
|
|
122
|
+
${describeIssues(issues)}`, "warn");
|
|
123
|
+
history[history.length - 1] += " \u2014 but tests were weakened";
|
|
124
|
+
nudge = section(
|
|
125
|
+
"The check passes, but not on evidence",
|
|
126
|
+
"code-flow found these changes to the tests in this branch. They can make the check pass without fixing anything:\n" + describeIssues(issues) + "\n\nUndo them and fix the code instead. Only if the task itself requires these test changes, keep them and say why in your summary."
|
|
127
|
+
);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
stuck = progress ? 0 : stuck + 1;
|
|
131
|
+
if (stuck >= stuckAfter && workers.length > 1) {
|
|
132
|
+
const next = (current + 1) % workers.length;
|
|
133
|
+
ctx.log(`${name(agent)} made no progress for ${stuck} rounds; handing over to ${name(workers[next].agent)}`, "warn");
|
|
134
|
+
handover = `${name(agent)} worked on this before you without getting the check to pass. Its commits are on this branch (\`git log\` shows them); keep what's useful, and take a fresh look rather than repeating its approach.
|
|
135
|
+
${history.join("\n")}`;
|
|
136
|
+
sessions.delete(workers[next].identity ?? workers[next].agent);
|
|
137
|
+
current = next;
|
|
138
|
+
stuck = 0;
|
|
139
|
+
handoffs++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
await ctx.artifact("Result", wt);
|
|
143
|
+
const passed = last.ok && !unresolved.length;
|
|
144
|
+
if (passed || !last.ok) ctx.setVerdict(passed ? "passed" : "failing");
|
|
145
|
+
ctx.setFacts({ solvedBy: passed ? solvedBy ?? workers[current]?.agent : void 0, handoffs, proof: ctx.proofOf(wt)?.status });
|
|
146
|
+
const s = rounds === 1 ? "" : "s";
|
|
147
|
+
return [
|
|
148
|
+
passed ? `**Check passes** after ${rounds} round${s} \u2014 branch \`${wt.branch}\`` : last.ok ? `**The check passes only with changes to the tests** after ${rounds} round${s} \u2014 branch \`${wt.branch}\`; review them before merging` : `**Check still failing** after ${rounds} rounds \u2014 branch \`${wt.branch}\` has ${last.failed !== void 0 ? `the best attempt (${plural(last.failed)} failing)` : "the latest attempt"}`,
|
|
149
|
+
passed ? proofLine(ctx.proofOf(wt)) : "",
|
|
150
|
+
last.ok && !passed ? section("Changes to the tests that could hide the failure", describeIssues(unresolved)) : "",
|
|
151
|
+
section("Rounds", history.join("\n")),
|
|
152
|
+
last.ok ? "" : section("Last failure", "```\n" + last.output.slice(-1500).trim() + "\n```")
|
|
153
|
+
].join("\n");
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
export {
|
|
157
|
+
loopPattern
|
|
158
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describeIssues } from "../integrity.js";
|
|
2
|
+
import { approves, builderReply, checkReview, findingsForBuilder, findingsLine, findingsSection, REVIEW_SHAPE, reproRule } from "./findings.js";
|
|
3
|
+
import { REVIEW_SCHEMA } from "./schemas.js";
|
|
4
|
+
import { checkEvidence, fence, fixUntilGreen, jsonInstruction, planCheckpoint, preamble, proofEvidence, proofLine, proofRule, proveOrAsk, READ_ONLY, section, WRITE } from "./prompts.js";
|
|
5
|
+
const pipelinePattern = {
|
|
6
|
+
id: "pipeline",
|
|
7
|
+
name: "Plan \u2192 Build \u2192 Review",
|
|
8
|
+
summary: "Planner writes a plan, builder implements it, an independent reviewer loops until it approves.",
|
|
9
|
+
description: "The default for well-defined features and bug fixes. Cross-vendor review catches mistakes a model misses in its own work.",
|
|
10
|
+
roles: [
|
|
11
|
+
{ id: "planner", label: "Planner", description: "Reads the code, writes the implementation plan", default: ["claude"] },
|
|
12
|
+
{ id: "builder", label: "Builder", description: "Implements the plan and applies review fixes", default: ["codex"] },
|
|
13
|
+
{ id: "reviewer", label: "Reviewer", description: "Reviews the diff against the task, ideally a vendor other than the planner", default: ["codex"] }
|
|
14
|
+
],
|
|
15
|
+
options: [
|
|
16
|
+
{ id: "maxRounds", label: "Max review rounds", type: "number", default: 2, min: 1, max: 5 },
|
|
17
|
+
{ id: "checkFixes", label: "Check fix attempts per round", type: "number", default: 2, min: 0, max: 5 }
|
|
18
|
+
],
|
|
19
|
+
async run(ctx) {
|
|
20
|
+
const planner = ctx.agent("planner");
|
|
21
|
+
const builder = ctx.agent("builder");
|
|
22
|
+
const reviewer = ctx.agent("reviewer");
|
|
23
|
+
const maxRounds = ctx.option("maxRounds", 2);
|
|
24
|
+
if (reviewer === planner) ctx.log(`${planner} both plans and reviews; a reviewer from another vendor is likelier to catch a wrong plan`, "warn");
|
|
25
|
+
const wt = await ctx.worktree("build");
|
|
26
|
+
let plan = await ctx.ask({
|
|
27
|
+
title: "Plan",
|
|
28
|
+
role: "planner",
|
|
29
|
+
agent: planner,
|
|
30
|
+
mode: "read",
|
|
31
|
+
cwd: wt,
|
|
32
|
+
prompt: preamble("planner", planner, `${READ_ONLY} Another agent will implement your plan, so be concrete.`) + section("Task", ctx.task) + section(
|
|
33
|
+
"Deliverable",
|
|
34
|
+
"Explore the relevant code, then write an implementation plan: files to change, the approach, edge cases, and how to verify it (tests or commands). Keep it under 400 words. Identify focused verification commands; leave their execution to the implementation and verification stages. Do not run full build or test suites while planning unless the task explicitly requires a baseline before planning."
|
|
35
|
+
)
|
|
36
|
+
});
|
|
37
|
+
plan = await planCheckpoint(ctx, { agent: planner, role: "planner", wt, plan, what: "plan" });
|
|
38
|
+
const build = await ctx.ask({
|
|
39
|
+
title: "Implement plan",
|
|
40
|
+
role: "builder",
|
|
41
|
+
agent: builder,
|
|
42
|
+
mode: "write",
|
|
43
|
+
cwd: wt,
|
|
44
|
+
prompt: preamble("builder", builder, WRITE) + section("Task", ctx.task) + section("Plan from the planner", plan.finalText) + proofRule(ctx) + section("Deliverable", "Implement the plan, run the relevant tests, then summarize what you changed.")
|
|
45
|
+
});
|
|
46
|
+
await ctx.commit(wt, `${builder}: implement plan`);
|
|
47
|
+
const checkFixes = ctx.option("checkFixes", 2);
|
|
48
|
+
let verdict;
|
|
49
|
+
let findings = [];
|
|
50
|
+
let review;
|
|
51
|
+
let reply;
|
|
52
|
+
let fixer = build;
|
|
53
|
+
let check = null;
|
|
54
|
+
let proof = null;
|
|
55
|
+
let askedForProof = false;
|
|
56
|
+
let reviewRounds = 0;
|
|
57
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
58
|
+
const fixed = await fixUntilGreen(ctx, { agent: builder, role: "builder", wt, session: fixer, attempts: checkFixes });
|
|
59
|
+
fixer = fixed.session ?? fixer;
|
|
60
|
+
const proved = await proveOrAsk(ctx, { agent: builder, role: "builder", wt, check: fixed.check, session: fixer, attempts: checkFixes, ask: !askedForProof });
|
|
61
|
+
askedForProof ||= proved.asked;
|
|
62
|
+
check = proved.check;
|
|
63
|
+
proof = proved.proof;
|
|
64
|
+
fixer = proved.session ?? fixer;
|
|
65
|
+
if (check && !check.ok) {
|
|
66
|
+
ctx.log(`The check still fails after ${checkFixes} fix attempts; stopping before review`, "warn");
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const diff = await ctx.diff(wt);
|
|
70
|
+
const testChanges = await ctx.testChanges(wt);
|
|
71
|
+
const decided = await ctx.decide({
|
|
72
|
+
title: `Review (round ${round})`,
|
|
73
|
+
schema: REVIEW_SCHEMA,
|
|
74
|
+
what: "review verdict",
|
|
75
|
+
role: "reviewer",
|
|
76
|
+
agent: reviewer,
|
|
77
|
+
mode: "read",
|
|
78
|
+
cwd: wt,
|
|
79
|
+
resume: review,
|
|
80
|
+
prompt: (review ? "The builder addressed your previous review. Here is the updated diff.\n" : "") + builderReply(reply) + preamble("reviewer", reviewer, `${READ_ONLY} You did not write this code; review it critically.`) + section("Task", ctx.task) + // A reviewer without its earlier session hasn't seen the plan.
|
|
81
|
+
(round === 1 || !review?.sessionId ? section("The planner's plan (context, not the spec)", plan.finalText) : "") + section("Diff against base", fence(diff, "diff")) + checkEvidence(ctx, check) + proofEvidence(proof) + (testChanges.length ? section(
|
|
82
|
+
"Changes to tests (found by code-flow)",
|
|
83
|
+
`These can make the check pass without fixing anything. Raise each as a medium issue unless the task clearly requires it:
|
|
84
|
+
${describeIssues(testChanges)}`
|
|
85
|
+
) : "") + section(
|
|
86
|
+
"Deliverable",
|
|
87
|
+
"Judge the change against the task, not against the plan: the plan is one way to do it, and it can be wrong. If following it missed or broke something the task asks for, raise that and say what the task needs instead. Check correctness, missed requirements, edge cases, tests and security. Only raise real problems, not style preferences."
|
|
88
|
+
) + reproRule(ctx) + jsonInstruction(REVIEW_SHAPE)
|
|
89
|
+
});
|
|
90
|
+
review = decided.outcome;
|
|
91
|
+
verdict = decided.value;
|
|
92
|
+
reviewRounds = round;
|
|
93
|
+
findings = await checkReview(ctx, wt, { label: `Review round ${round}`, reviewer, round, review: verdict });
|
|
94
|
+
if (approves(findings) || round === maxRounds) break;
|
|
95
|
+
fixer = await ctx.ask({
|
|
96
|
+
title: `Address review (round ${round})`,
|
|
97
|
+
role: "builder",
|
|
98
|
+
agent: builder,
|
|
99
|
+
mode: "write",
|
|
100
|
+
cwd: wt,
|
|
101
|
+
resume: fixer,
|
|
102
|
+
prompt: findingsForBuilder(reviewer, findings)
|
|
103
|
+
});
|
|
104
|
+
reply = fixer.finalText;
|
|
105
|
+
await ctx.commit(wt, `${builder}: address review round ${round}`);
|
|
106
|
+
}
|
|
107
|
+
await ctx.artifact("Result", wt);
|
|
108
|
+
const green = !check || check.ok;
|
|
109
|
+
const approved = green && !!review && approves(findings);
|
|
110
|
+
if (ctx.hasCheck) ctx.setVerdict(green ? "passed" : "failing");
|
|
111
|
+
ctx.setFacts({ reviewRounds, approved, proof: ctx.proofOf(wt)?.status });
|
|
112
|
+
const headline = !green ? `Check \`${check.command}\` still fails after ${checkFixes} fix attempts` : approved ? ctx.hasCheck ? `Approved, and \`${check.command}\` passes` : "Approved" : "Not approved: findings still block after the last review round";
|
|
113
|
+
return [
|
|
114
|
+
`**${headline}** \u2014 branch \`${wt.branch}\``,
|
|
115
|
+
proofLine(ctx.proofOf(wt)),
|
|
116
|
+
review ? findingsLine(findings) : "",
|
|
117
|
+
section("Builder summary", fixer.finalText),
|
|
118
|
+
review && !approved ? findingsSection("Open review findings", findings) : "",
|
|
119
|
+
!green ? section("Last failure", "```\n" + check.output.slice(-1500).trim() + "\n```") : ""
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
export {
|
|
124
|
+
pipelinePattern
|
|
125
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { getAdapter } from "../adapters/index.js";
|
|
2
|
+
import { failureExcerpt } from "../checks.js";
|
|
3
|
+
import { RejectedError } from "../engine.js";
|
|
4
|
+
import { changedFiles, resolveCommit, restorePaths } from "../git.js";
|
|
5
|
+
function preamble(role, agent, duty) {
|
|
6
|
+
return [
|
|
7
|
+
`You are the ${role} on a team of AI coding agents (Claude Code, Codex, Antigravity) coordinated by code-flow.`,
|
|
8
|
+
`You are running as ${getAdapter(agent).displayName}, non-interactively: nobody can answer questions, so make reasonable assumptions and state them.`,
|
|
9
|
+
duty,
|
|
10
|
+
"Do not run `git commit`, `git push` or change branches \u2014 code-flow commits your work after you finish."
|
|
11
|
+
].join("\n");
|
|
12
|
+
}
|
|
13
|
+
function section(title, body) {
|
|
14
|
+
return `
|
|
15
|
+
## ${title}
|
|
16
|
+
${body.trim()}
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
function fence(body, lang = "") {
|
|
20
|
+
return `\`\`\`${lang}
|
|
21
|
+
${body}
|
|
22
|
+
\`\`\``;
|
|
23
|
+
}
|
|
24
|
+
const READ_ONLY = "You have read-only access: inspect code and run read-only commands, but do not modify any files.";
|
|
25
|
+
const WRITE = "You may edit files and run build/test commands in the current directory, which is an isolated git worktree.";
|
|
26
|
+
function jsonInstruction(shape) {
|
|
27
|
+
return `
|
|
28
|
+
End your answer with a fenced \`\`\`json block exactly matching this shape:
|
|
29
|
+
${fence(shape, "json")}`;
|
|
30
|
+
}
|
|
31
|
+
const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
32
|
+
async function planCheckpoint(ctx, opts) {
|
|
33
|
+
let plan = opts.plan;
|
|
34
|
+
for (let round = 1; ; round++) {
|
|
35
|
+
const title = round === 1 ? `Approve the ${opts.what}` : `Approve the revised ${opts.what} (${round})`;
|
|
36
|
+
const answer = await ctx.approve("plan", title, opts.render ? opts.render(plan.finalText) : plan.finalText);
|
|
37
|
+
if (answer.decision === "approve") return plan;
|
|
38
|
+
if (answer.decision === "reject") throw new RejectedError(`You rejected the ${opts.what}.${answer.note ? ` (${answer.note})` : ""}`);
|
|
39
|
+
plan = await ctx.ask({
|
|
40
|
+
title: `Revise the ${opts.what} (${round})`,
|
|
41
|
+
role: opts.role,
|
|
42
|
+
agent: opts.agent,
|
|
43
|
+
mode: "read",
|
|
44
|
+
cwd: opts.wt,
|
|
45
|
+
resume: plan.sessionId ? plan : void 0,
|
|
46
|
+
prompt: (plan.sessionId ? "" : `Your earlier ${opts.what}:
|
|
47
|
+
${plan.finalText}
|
|
48
|
+
|
|
49
|
+
`) + `The user asked for changes to your ${opts.what}:
|
|
50
|
+
${answer.note}
|
|
51
|
+
|
|
52
|
+
Revise it and reply with the complete updated ${opts.what}.`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function checkSection(result, maxOutput = 6e3) {
|
|
57
|
+
const status = result.ok ? "passed" : result.timedOut ? "timed out" : `failed (exit code ${result.exitCode})`;
|
|
58
|
+
const body = result.ok ? "All checks pass." : fence(failureExcerpt(result.output, maxOutput) || "(no output)");
|
|
59
|
+
return section(`Check \`${result.command}\` ${status} (run by code-flow)`, body);
|
|
60
|
+
}
|
|
61
|
+
function checkEvidence(ctx, check, maxOutput) {
|
|
62
|
+
if (check) return checkSection(check, maxOutput);
|
|
63
|
+
const cantRun = ctx.run.checkCantRun;
|
|
64
|
+
if (!cantRun) return "";
|
|
65
|
+
return section(
|
|
66
|
+
`Check \`${cantRun.command}\` couldn't run (run by code-flow)`,
|
|
67
|
+
`It can't run in this environment (${cantRun.reason}) \u2014 on the base commit either, so not because of this change. Nobody has run the tests on this change; review it with that in mind.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function proofRule(ctx) {
|
|
71
|
+
if (!ctx.proves) return "";
|
|
72
|
+
return section(
|
|
73
|
+
"Prove it",
|
|
74
|
+
"code-flow will run your tests on the original code, without your change: at least one should fail there and pass with your change. Add or update a test that shows the change works, unless the task can't be tested that way (docs, a pure refactor); then say why in your summary."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
function proofEvidence(proof) {
|
|
78
|
+
if (!proof) return "";
|
|
79
|
+
const tests = proof.tests.length ? `this change's tests (${proof.tests.join(", ")})` : "the existing tests";
|
|
80
|
+
if (proof.status === "proven") {
|
|
81
|
+
return section(
|
|
82
|
+
"Fail-to-pass: proven (run by code-flow)",
|
|
83
|
+
`code-flow put ${tests} onto the original code and ran \`${proof.command}\`: it fails there and passes with the change, so the tests need the change.
|
|
84
|
+
${fence(proof.detail)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (proof.status === "weak") {
|
|
88
|
+
return section(
|
|
89
|
+
"Fail-to-pass: weak (run by code-flow)",
|
|
90
|
+
`code-flow put ${tests} onto the original code and ran \`${proof.command}\`: it fails there, but only because the tests can't load (an import or build error), so they show the change adds what they import, not that it works. Look for a test that fails on the behaviour itself.
|
|
91
|
+
` + fence(proof.detail)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (proof.status === "unproven") {
|
|
95
|
+
return section(
|
|
96
|
+
"Fail-to-pass: not proven (run by code-flow)",
|
|
97
|
+
`code-flow put ${tests} onto the original code and ran \`${proof.command}\`: it passes there too, so no test shows the change works. Ask for a test that fails without the change, unless the task can't be tested that way (docs, a pure refactor).`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return section("Fail-to-pass: couldn't tell (run by code-flow)", `code-flow couldn't run the tests on the original code: ${proof.detail}.`);
|
|
101
|
+
}
|
|
102
|
+
function proofLine(proof) {
|
|
103
|
+
if (!proof) return "";
|
|
104
|
+
if (proof.status === "proven") return `Proven: \`${proof.command}\` fails without the change${proof.tests.length ? ` (with ${proof.tests.join(", ")})` : ""}, so its tests need it.`;
|
|
105
|
+
if (proof.status === "weak") return `\u26A0 Weak proof: \`${proof.command}\` fails without the change only because its tests can't load there (an import or build error), not on what they check.`;
|
|
106
|
+
if (proof.status === "unproven") return "\u26A0 Not proven: no test fails without the change.";
|
|
107
|
+
return `Couldn't prove it fails without the change: ${proof.detail}.`;
|
|
108
|
+
}
|
|
109
|
+
async function proveOrAsk(ctx, opts) {
|
|
110
|
+
let { check, session } = opts;
|
|
111
|
+
if (!check?.ok) return { check, proof: null, session, asked: false };
|
|
112
|
+
let proof = await ctx.prove(opts.wt);
|
|
113
|
+
if (proof?.status !== "unproven" || !opts.ask) return { check, proof, session, asked: false };
|
|
114
|
+
session = session?.agent === opts.agent ? session : void 0;
|
|
115
|
+
const before = await ctx.remember("head", `before proof test in ${opts.wt.name}`, async () => resolveCommit(await ctx.gitAt(opts.wt)));
|
|
116
|
+
session = await ctx.ask({
|
|
117
|
+
title: "Add a test that fails without the change",
|
|
118
|
+
role: opts.role,
|
|
119
|
+
agent: opts.agent,
|
|
120
|
+
mode: "write",
|
|
121
|
+
cwd: opts.wt,
|
|
122
|
+
resume: session,
|
|
123
|
+
prompt: (session ? "" : preamble(opts.role, opts.agent, WRITE) + section("Task", ctx.task) + (opts.context ?? "")) + "The check passes, but code-flow ran this change's tests on the original code, without the change, and they pass there too: nothing shows the change does what the task asks." + section(
|
|
124
|
+
"Next",
|
|
125
|
+
"Add or update a test that fails without your change and passes with it. Don't change the implementation unless the test finds a real bug. If the task can't be tested that way (docs, a pure refactor), change nothing and say why."
|
|
126
|
+
)
|
|
127
|
+
});
|
|
128
|
+
const sha = await ctx.commit(opts.wt, `${opts.agent}: add a test that fails without the change`);
|
|
129
|
+
if (!sha) {
|
|
130
|
+
ctx.log(`${getAdapter(opts.agent).displayName} added no test; the result stays unproven`, "warn");
|
|
131
|
+
return { check, proof, session, asked: true };
|
|
132
|
+
}
|
|
133
|
+
const fixed = await fixUntilGreen(ctx, { agent: opts.agent, role: opts.role, wt: opts.wt, session, attempts: opts.attempts, context: opts.context });
|
|
134
|
+
check = fixed.check;
|
|
135
|
+
session = fixed.session ?? session;
|
|
136
|
+
if (check && !check.ok) {
|
|
137
|
+
ctx.log(`The check fails since ${getAdapter(opts.agent).displayName} added its test, and fixing didn't help; undoing that`, "warn");
|
|
138
|
+
const changed = await ctx.remember("changed", `since proof test in ${opts.wt.name}`, async () => changedFiles(await ctx.gitAt(opts.wt), before));
|
|
139
|
+
await ctx.remember("restore-before-proof", opts.wt.name, () => restorePaths(ctx.run.repo, opts.wt, before, changed).then(() => true));
|
|
140
|
+
await ctx.commit(opts.wt, `code-flow: undo ${opts.agent}'s test that never passed`);
|
|
141
|
+
check = await ctx.check(opts.wt);
|
|
142
|
+
return { check, proof: check?.ok ? await ctx.prove(opts.wt) : null, session, asked: true };
|
|
143
|
+
}
|
|
144
|
+
proof = check?.ok ? await ctx.prove(opts.wt) : null;
|
|
145
|
+
return { check, proof, session, asked: true };
|
|
146
|
+
}
|
|
147
|
+
async function fixUntilGreen(ctx, opts) {
|
|
148
|
+
let check = opts.initial !== void 0 ? opts.initial : await ctx.check(opts.wt);
|
|
149
|
+
let session = opts.session?.agent === opts.agent ? opts.session : void 0;
|
|
150
|
+
for (let attempt = 1; check && !check.ok && attempt <= opts.attempts; attempt++) {
|
|
151
|
+
session = await ctx.ask({
|
|
152
|
+
title: `Fix failing check (${attempt}/${opts.attempts})`,
|
|
153
|
+
role: opts.role,
|
|
154
|
+
agent: opts.agent,
|
|
155
|
+
mode: "write",
|
|
156
|
+
cwd: opts.wt,
|
|
157
|
+
resume: session,
|
|
158
|
+
prompt: (session ? "" : preamble(opts.role, opts.agent, WRITE) + section("Task", ctx.task) + (opts.context ?? "")) + "code-flow ran the project's check on the current code and it fails. Fix the cause in the code \u2014 don't weaken or skip tests \u2014 then summarize." + checkSection(check)
|
|
159
|
+
});
|
|
160
|
+
await ctx.commit(opts.wt, `${opts.agent}: fix failing check`);
|
|
161
|
+
check = await ctx.check(opts.wt);
|
|
162
|
+
}
|
|
163
|
+
return { check, session };
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
LETTERS,
|
|
167
|
+
READ_ONLY,
|
|
168
|
+
WRITE,
|
|
169
|
+
checkEvidence,
|
|
170
|
+
checkSection,
|
|
171
|
+
fence,
|
|
172
|
+
fixUntilGreen,
|
|
173
|
+
jsonInstruction,
|
|
174
|
+
planCheckpoint,
|
|
175
|
+
preamble,
|
|
176
|
+
proofEvidence,
|
|
177
|
+
proofLine,
|
|
178
|
+
proofRule,
|
|
179
|
+
proveOrAsk,
|
|
180
|
+
section
|
|
181
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describeIssues } from "../integrity.js";
|
|
2
|
+
import { JUDGE_SCHEMA } from "./schemas.js";
|
|
3
|
+
import { checkSection, fence, jsonInstruction, LETTERS, preamble, proofEvidence, proofLine, proofRule, READ_ONLY, section, WRITE } from "./prompts.js";
|
|
4
|
+
const racePattern = {
|
|
5
|
+
id: "race",
|
|
6
|
+
name: "Best-of-N race",
|
|
7
|
+
summary: "All contestants solve the task in parallel; a judge picks the best anonymized diff.",
|
|
8
|
+
description: "Use when the solution space is wide or a task is risky: you get several independent attempts and keep the best one.",
|
|
9
|
+
roles: [
|
|
10
|
+
{
|
|
11
|
+
id: "contestants",
|
|
12
|
+
label: "Contestants",
|
|
13
|
+
description: "Each solves the whole task in its own worktree",
|
|
14
|
+
multiple: true,
|
|
15
|
+
default: ["claude", "codex"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "judge",
|
|
19
|
+
label: "Judge",
|
|
20
|
+
description: "Compares the results blind; pick several for a majority vote, ideally vendors that aren't competing",
|
|
21
|
+
multiple: true,
|
|
22
|
+
default: ["claude"]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
options: [],
|
|
26
|
+
async run(ctx) {
|
|
27
|
+
const contestants = ctx.participants("contestants");
|
|
28
|
+
if (contestants.length < 2) throw new Error("A race needs at least two contestants");
|
|
29
|
+
const judges = ctx.participants("judge");
|
|
30
|
+
if (!judges.length) throw new Error("Pick at least one judge");
|
|
31
|
+
const entries = await ctx.parallel(contestants, async ({ agent, settings, identity }, i) => {
|
|
32
|
+
const label = LETTERS[i] ?? String(i + 1);
|
|
33
|
+
const wt = await ctx.worktree(`candidate-${label.toLowerCase()}`);
|
|
34
|
+
const result = await ctx.ask({
|
|
35
|
+
title: `Candidate ${label}`,
|
|
36
|
+
role: "contestants",
|
|
37
|
+
agent,
|
|
38
|
+
settings,
|
|
39
|
+
identity,
|
|
40
|
+
mode: "write",
|
|
41
|
+
cwd: wt,
|
|
42
|
+
allowFailure: true,
|
|
43
|
+
prompt: preamble("contestant", agent, `${WRITE} Other agents are solving the same task independently; the best solution is kept.`) + section("Task", ctx.task) + proofRule(ctx) + section("Deliverable", "Implement the task completely, run the relevant tests, then summarize your approach and test results.")
|
|
44
|
+
});
|
|
45
|
+
await ctx.commit(wt, `candidate ${label}`);
|
|
46
|
+
const check = result.ok ? await ctx.check(wt, `Check candidate ${label}`) : null;
|
|
47
|
+
const tests = result.ok ? (await ctx.testChanges(wt)).filter((i2) => i2.severity === "high") : [];
|
|
48
|
+
return { label, agent, wt, result, check, tests, diff: result.ok ? await ctx.diff(wt) : "(attempt failed)" };
|
|
49
|
+
});
|
|
50
|
+
const changed = entries.filter((e) => e.result.ok && e.diff !== "(no changes)");
|
|
51
|
+
if (changed.length === 0) throw new Error("No contestant produced a usable change");
|
|
52
|
+
for (const e of entries) await ctx.artifact(`Candidate ${e.label} (${e.agent})`, e.wt);
|
|
53
|
+
const proofs = new Map(entries.map((e) => [e.label, ctx.proofOf(e.wt)]));
|
|
54
|
+
const passing = changed.filter((e) => e.check?.ok);
|
|
55
|
+
const honest = passing.filter((e) => !e.tests.length);
|
|
56
|
+
const viable = ctx.hasCheck && passing.length ? honest.length ? honest : passing : changed;
|
|
57
|
+
for (const e of changed.filter((e2) => e2.tests.length)) {
|
|
58
|
+
ctx.log(`Candidate ${e.label} changed tests in ways that could hide failures${honest.length && e.check?.ok ? ", so it can't win" : ""}:
|
|
59
|
+
${describeIssues(e.tests)}`, "warn");
|
|
60
|
+
}
|
|
61
|
+
if (ctx.hasCheck) {
|
|
62
|
+
ctx.log(passing.length ? `Passing the check: ${passing.map((e) => e.label).join(", ")}${passing.length < changed.length ? " (only these can win)" : ""}` : "No candidate passes the check; the judge picks the closest one", passing.length ? "info" : "warn");
|
|
63
|
+
}
|
|
64
|
+
let winner = viable[0];
|
|
65
|
+
let reasons = [];
|
|
66
|
+
if (viable.length > 1) {
|
|
67
|
+
const competing = judges.filter((j) => contestants.some((c) => c.agent === j.agent)).map((j) => j.agent);
|
|
68
|
+
if (competing.length) ctx.log(`${competing.join(", ")} both competes and judges; a judge from another vendor is fairer`, "warn");
|
|
69
|
+
const judgeWt = await ctx.worktree("judge");
|
|
70
|
+
const order = seededShuffle(viable, ctx.run.id);
|
|
71
|
+
const labels = viable.map((e) => e.label);
|
|
72
|
+
const votes = await ctx.parallel(
|
|
73
|
+
judges,
|
|
74
|
+
({ agent: judge, settings, identity }, i) => ctx.decide({
|
|
75
|
+
title: judges.length > 1 ? `Judge ${i + 1}` : "Judge candidates",
|
|
76
|
+
role: "judge",
|
|
77
|
+
agent: judge,
|
|
78
|
+
settings,
|
|
79
|
+
identity,
|
|
80
|
+
mode: "read",
|
|
81
|
+
cwd: judgeWt,
|
|
82
|
+
schema: JUDGE_SCHEMA,
|
|
83
|
+
what: "pick",
|
|
84
|
+
validate: (v) => changed.some((e) => e.label === v.winner.trim().toUpperCase()) ? void 0 : `winner must be one of ${changed.map((e) => e.label).join(", ")}`,
|
|
85
|
+
prompt: preamble("judge", judge, `${READ_ONLY} You are looking at the original, unmodified code; each candidate's changes are shown as a diff.`) + section("Task", ctx.task) + order.map(
|
|
86
|
+
(e) => section(
|
|
87
|
+
`Candidate ${e.label}`,
|
|
88
|
+
fence(e.diff, "diff") + (e.check ? checkSection(e.check, 2e3) : "") + proofEvidence(proofs.get(e.label) ?? null) + (e.tests.length ? `
|
|
89
|
+
**Changes to tests found by code-flow:**
|
|
90
|
+
${describeIssues(e.tests)}
|
|
91
|
+
` : "")
|
|
92
|
+
)
|
|
93
|
+
).join("") + section(
|
|
94
|
+
"Deliverable",
|
|
95
|
+
"Compare the candidates on correctness, completeness, test coverage, simplicity and risk" + (ctx.proves ? ", and on whether their tests prove the change (a test that fails without it)" : "") + `. Pick one winner from: ${labels.join(", ")}.`
|
|
96
|
+
) + jsonInstruction('{"winner": "A", "ranking": ["A", "B"], "reasons": "..."}')
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
const tally = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const { value } of votes) {
|
|
101
|
+
const choice = value.winner.trim().toUpperCase();
|
|
102
|
+
if (viable.some((e) => e.label === choice)) tally.set(choice, (tally.get(choice) ?? 0) + 1);
|
|
103
|
+
else ctx.log(`Judge picked candidate ${choice}, but it fails the check${tally.size || votes.length > 1 ? "" : `; using passing candidate ${winner.label}`}`, "warn");
|
|
104
|
+
reasons.push(value.reasons);
|
|
105
|
+
}
|
|
106
|
+
const best = [...tally.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
|
|
107
|
+
if (best) winner = viable.find((e) => e.label === best[0]);
|
|
108
|
+
if (judges.length > 1) ctx.log(`Votes: ${[...tally.entries()].map(([label, n]) => `${label}=${n}`).join(", ") || "none valid"}`);
|
|
109
|
+
}
|
|
110
|
+
ctx.log(`Winner: candidate ${winner.label} (${winner.agent})`);
|
|
111
|
+
if (ctx.hasCheck) ctx.setVerdict(winner.check?.ok ? "passed" : "failing");
|
|
112
|
+
ctx.setFacts({ winner: winner.agent, entrants: entries.map((e) => e.agent), proof: proofs.get(winner.label)?.status });
|
|
113
|
+
return [
|
|
114
|
+
`**Winner: Candidate ${winner.label} \u2014 ${winner.agent}** on branch \`${winner.wt.branch}\``,
|
|
115
|
+
proofLine(proofs.get(winner.label) ?? null),
|
|
116
|
+
reasons.length ? section(reasons.length > 1 ? "Judges' reasoning" : "Judge's reasoning", reasons.join("\n\n")) : "",
|
|
117
|
+
section(
|
|
118
|
+
"All candidates",
|
|
119
|
+
entries.map((e) => {
|
|
120
|
+
const status = e.result.ok ? "completed" : `failed (${e.result.error})`;
|
|
121
|
+
const check = e.check ? e.check.ok ? ", check passes" : ", check fails" : "";
|
|
122
|
+
const proof = proofs.get(e.label)?.status;
|
|
123
|
+
const proven = proof === "proven" ? ", proven" : proof === "weak" ? ", weakly proven" : proof === "unproven" ? ", not proven" : "";
|
|
124
|
+
return `- ${e.label}: ${e.agent} \u2014 ${status}${check}${proven} \u2014 \`${e.wt.branch}\``;
|
|
125
|
+
}).join("\n")
|
|
126
|
+
)
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
function seededShuffle(items, seed) {
|
|
131
|
+
let h = 2166136261;
|
|
132
|
+
for (const ch of seed) h = Math.imul(h ^ ch.charCodeAt(0), 16777619);
|
|
133
|
+
const random = () => {
|
|
134
|
+
h = Math.imul(h ^ h >>> 15, 2246822507);
|
|
135
|
+
h = Math.imul(h ^ h >>> 13, 3266489909);
|
|
136
|
+
return ((h ^= h >>> 16) >>> 0) / 4294967296;
|
|
137
|
+
};
|
|
138
|
+
const out = [...items];
|
|
139
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
140
|
+
const j = Math.floor(random() * (i + 1));
|
|
141
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
racePattern
|
|
147
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const obj = (properties) => ({
|
|
2
|
+
type: "object",
|
|
3
|
+
properties,
|
|
4
|
+
required: Object.keys(properties),
|
|
5
|
+
additionalProperties: false
|
|
6
|
+
});
|
|
7
|
+
const str = { type: "string" };
|
|
8
|
+
const strings = { type: "array", items: str };
|
|
9
|
+
const repro = obj({ path: str, contents: str, command: str });
|
|
10
|
+
const REVIEW_SCHEMA = obj({
|
|
11
|
+
verdict: { type: "string", enum: ["approve", "changes_requested"] },
|
|
12
|
+
issues: {
|
|
13
|
+
type: "array",
|
|
14
|
+
items: obj({ severity: { type: "string", enum: ["high", "medium", "low"] }, file: str, problem: str, fix: str, repro })
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const JUDGE_SCHEMA = obj({ winner: str, ranking: strings, reasons: str });
|
|
18
|
+
const SUBTASKS_SCHEMA = obj({
|
|
19
|
+
/** What every part must agree on (interfaces, names, data shapes); each worker gets it. */
|
|
20
|
+
contracts: str,
|
|
21
|
+
subtasks: { type: "array", items: obj({ id: str, title: str, instructions: str, agent: str }) }
|
|
22
|
+
});
|
|
23
|
+
const TEST_SPEC_SCHEMA = obj({ test_command: str, test_files: strings });
|
|
24
|
+
const TEST_VERDICT_SCHEMA = obj({ verdict: { type: "string", enum: ["pass", "fail"] }, failures: strings });
|
|
25
|
+
const COUNCIL_SCHEMA = obj({
|
|
26
|
+
verdict: { type: "string", enum: ["approve", "changes_requested"] },
|
|
27
|
+
summary: str,
|
|
28
|
+
findings: {
|
|
29
|
+
type: "array",
|
|
30
|
+
items: obj({ severity: { type: "string", enum: ["high", "medium", "low"] }, file: str, problem: str, fix: str, raised_by: strings, repro })
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
export {
|
|
34
|
+
COUNCIL_SCHEMA,
|
|
35
|
+
JUDGE_SCHEMA,
|
|
36
|
+
REVIEW_SCHEMA,
|
|
37
|
+
SUBTASKS_SCHEMA,
|
|
38
|
+
TEST_SPEC_SCHEMA,
|
|
39
|
+
TEST_VERDICT_SCHEMA
|
|
40
|
+
};
|