@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1
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 +122 -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/browser-7XI52A6V.js +8 -0
- package/public/assets/changes-EYFPASEI.js +1 -0
- package/public/assets/chunk-7H3ZLTKA.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-LLMQG23J.js +163 -0
- package/public/assets/main-KIRM2OEK.js +1 -0
- package/public/assets/settings-QJZVW22Y.js +9 -0
- package/public/assets/styles-615EBB20.css +32 -0
- package/public/assets/terminal-A2FI2NAX.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 +142 -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 +118 -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 +77 -0
- package/public/docs/installation.html +74 -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 +99 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +61 -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 +379 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +122 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -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/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +223 -0
- package/src/adapters/agy.js +297 -0
- package/src/adapters/claude.js +220 -0
- package/src/adapters/codex-app-server.js +518 -0
- package/src/adapters/codex.js +299 -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 +418 -0
- package/src/adapters/team-tools.js +102 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-container.js +155 -0
- package/src/approvals.js +121 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-handoff.js +55 -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/engine.js +4366 -0
- package/src/env.js +44 -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/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/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 +238 -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 +212 -0
- package/src/patterns/auto-team.js +659 -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 +19 -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 +49 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -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 +383 -0
- package/src/preview.js +219 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-model.js +178 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +213 -0
- package/src/project-teams-api.js +36 -0
- package/src/project-teams.js +551 -0
- package/src/projects.js +286 -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 +732 -0
- package/src/repo-tree.js +106 -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 +110 -0
- package/src/run-projection.js +75 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +54 -0
- package/src/serve.js +138 -0
- package/src/server.js +2738 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +301 -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 +116 -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/team-evaluation.js +242 -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-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 +176 -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
package/src/sweeps.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { repoRoot } from "./git.js";
|
|
2
|
+
import {
|
|
3
|
+
alertsError,
|
|
4
|
+
isPlainPath,
|
|
5
|
+
LIST_CAP,
|
|
6
|
+
listCodeScanningAlerts,
|
|
7
|
+
listDependabotAlerts,
|
|
8
|
+
listDependabotPrs,
|
|
9
|
+
listOpenIssues,
|
|
10
|
+
listSecretScanningAlerts,
|
|
11
|
+
severityRank
|
|
12
|
+
} from "./github.js";
|
|
13
|
+
const NOT_A_FIX = /^(question|wontfix|won't fix|duplicate|invalid|discussion|needs[- ]info)$/i;
|
|
14
|
+
async function listSweep(repo, kind, runs) {
|
|
15
|
+
const list = kind === "issues" ? await issueItems(repo) : kind === "dependabot" ? await dependabotItems(repo) : await alertItems(repo);
|
|
16
|
+
const root = await repoRoot(repo).catch(() => repo);
|
|
17
|
+
for (const item of list.items) {
|
|
18
|
+
const covering = item.source ? coveringRun(runs, root, item.source) : void 0;
|
|
19
|
+
if (covering) {
|
|
20
|
+
item.existingRun = covering.id;
|
|
21
|
+
item.selected = false;
|
|
22
|
+
item.note = covering.status === "running" ? "A run is working on it" : "Already has a run";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return list;
|
|
26
|
+
}
|
|
27
|
+
const cutShort = (what, cap) => `Only the first ${cap} ${what} are listed; sweep again once these are done`;
|
|
28
|
+
async function issueItems(repo) {
|
|
29
|
+
const issues = await listOpenIssues(repo);
|
|
30
|
+
return {
|
|
31
|
+
kind: "issues",
|
|
32
|
+
notices: issues.truncated ? [cutShort("open issues", LIST_CAP)] : [],
|
|
33
|
+
items: issues.items.map((i) => {
|
|
34
|
+
const skip = i.labels.find((l) => NOT_A_FIX.test(l));
|
|
35
|
+
return {
|
|
36
|
+
key: `issue#${i.number}`,
|
|
37
|
+
ref: `#${i.number}`,
|
|
38
|
+
title: i.title,
|
|
39
|
+
detail: i.labels.join(" \xB7 "),
|
|
40
|
+
labels: i.labels,
|
|
41
|
+
url: i.url,
|
|
42
|
+
selected: !skip,
|
|
43
|
+
note: skip ? `Labelled \u201C${skip}\u201D` : void 0,
|
|
44
|
+
// Not the title: whoever opened the issue wrote it, and the task is read as instructions.
|
|
45
|
+
// The run gets the title with the issue's text, fenced as information.
|
|
46
|
+
task: `Fix issue #${i.number}. Add a test that fails without the fix.`,
|
|
47
|
+
source: { kind: "issue", number: i.number }
|
|
48
|
+
};
|
|
49
|
+
})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const CI_NOTE = { pass: "CI passes \u2014 ready for you to merge", pending: "CI is still running", none: "No CI checks reported" };
|
|
53
|
+
async function dependabotItems(repo) {
|
|
54
|
+
const prs = await listDependabotPrs(repo);
|
|
55
|
+
return {
|
|
56
|
+
kind: "dependabot",
|
|
57
|
+
notices: prs.truncated ? [cutShort("Dependabot pull requests", prs.items.length)] : [],
|
|
58
|
+
items: prs.items.map((pr) => ({
|
|
59
|
+
key: `pr#${pr.number}`,
|
|
60
|
+
ref: `#${pr.number}`,
|
|
61
|
+
title: pr.title,
|
|
62
|
+
detail: `${pr.ci === "fail" ? "CI failing" : pr.ci === "pass" ? "CI green" : pr.ci === "pending" ? "CI running" : "no CI"} \xB7 ${pr.headRefName}`,
|
|
63
|
+
url: pr.url,
|
|
64
|
+
// Only a failing PR needs an agent; code-flow never merges the green ones for you.
|
|
65
|
+
selected: pr.ci === "fail",
|
|
66
|
+
note: pr.ci === "fail" ? void 0 : CI_NOTE[pr.ci],
|
|
67
|
+
// The PR's title stays out of the task: package names and versions come from upstream.
|
|
68
|
+
task: `Make CI pass on Dependabot's pull request #${pr.number}. The failing checks and their logs are below. Fix the cause by adapting the code to the new version; don't pin it back, and don't disable, skip or weaken tests.`,
|
|
69
|
+
source: { kind: "pr", number: pr.number, ciLogs: true }
|
|
70
|
+
}))
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function alertItems(repo) {
|
|
74
|
+
const [dependabot, codeScanning, secrets, prs] = await Promise.allSettled([
|
|
75
|
+
listDependabotAlerts(repo),
|
|
76
|
+
listCodeScanningAlerts(repo),
|
|
77
|
+
listSecretScanningAlerts(repo),
|
|
78
|
+
listDependabotPrs(repo)
|
|
79
|
+
]);
|
|
80
|
+
const notices = [];
|
|
81
|
+
const items = [];
|
|
82
|
+
const openPrs = prs.status === "fulfilled" ? prs.value.items : [];
|
|
83
|
+
const note = (what, list) => list.truncated && notices.push(cutShort(what, list.items.length));
|
|
84
|
+
if (dependabot.status === "fulfilled") {
|
|
85
|
+
items.push(...dependabotAlertItems(dependabot.value.items, openPrs));
|
|
86
|
+
note("Dependabot alerts", dependabot.value);
|
|
87
|
+
} else notices.push(alertsError("Dependabot alerts", dependabot.reason));
|
|
88
|
+
if (codeScanning.status === "fulfilled") {
|
|
89
|
+
note("code scanning alerts", codeScanning.value);
|
|
90
|
+
for (const a of codeScanning.value.items) {
|
|
91
|
+
const rule = a.rule ?? {};
|
|
92
|
+
const where = a.most_recent_instance?.location ?? {};
|
|
93
|
+
const severity = String(rule.security_severity_level ?? rule.severity ?? "unknown");
|
|
94
|
+
const serious = severityRank(severity) <= severityRank("medium");
|
|
95
|
+
const title = rule.description || rule.name || rule.id;
|
|
96
|
+
const at = where.path ? `${where.path}${where.start_line ? `:${where.start_line}` : ""}` : "";
|
|
97
|
+
items.push({
|
|
98
|
+
key: `code-scanning#${a.number}`,
|
|
99
|
+
ref: `#${a.number}`,
|
|
100
|
+
title,
|
|
101
|
+
detail: [at, a.tool?.name].filter(Boolean).join(" \xB7 "),
|
|
102
|
+
url: a.html_url,
|
|
103
|
+
severity,
|
|
104
|
+
selected: serious,
|
|
105
|
+
note: serious ? void 0 : "Low severity",
|
|
106
|
+
// The path is the code's: one that carries words stays out of the task (the run's prompt
|
|
107
|
+
// has it in the alert's fence).
|
|
108
|
+
task: `Fix code scanning alert #${a.number}: ${title}${at && isPlainPath(at) ? ` in ${at}` : ""}. Fix the cause rather than suppressing the alert, and add a test where it makes sense.`,
|
|
109
|
+
source: { kind: "alert", tool: "code-scanning", number: a.number }
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
} else notices.push(alertsError("Code scanning alerts", codeScanning.reason));
|
|
113
|
+
if (secrets.status === "fulfilled") {
|
|
114
|
+
note("leaked secrets", secrets.value);
|
|
115
|
+
for (const a of secrets.value.items) {
|
|
116
|
+
items.push({
|
|
117
|
+
key: `secret#${a.number}`,
|
|
118
|
+
ref: `#${a.number}`,
|
|
119
|
+
title: a.secret_type_display_name || a.secret_type || "Leaked secret",
|
|
120
|
+
detail: "Leaked secret \u2014 revoke and rotate it, then close the alert",
|
|
121
|
+
url: a.html_url,
|
|
122
|
+
severity: "critical",
|
|
123
|
+
selected: false,
|
|
124
|
+
manual: true,
|
|
125
|
+
note: "Needs you: agents never get secrets"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} else notices.push(alertsError("Secret scanning alerts", secrets.reason));
|
|
129
|
+
items.sort((a, b) => severityRank(a.severity) - severityRank(b.severity) || a.key.localeCompare(b.key));
|
|
130
|
+
return { kind: "alerts", items, notices };
|
|
131
|
+
}
|
|
132
|
+
function dependabotAlertItems(alerts, prs) {
|
|
133
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const a of alerts) {
|
|
135
|
+
const pkg = a.dependency?.package ?? a.security_vulnerability?.package ?? {};
|
|
136
|
+
const key = `${pkg.ecosystem}/${pkg.name}`;
|
|
137
|
+
byPackage.set(key, [...byPackage.get(key) ?? [], a]);
|
|
138
|
+
}
|
|
139
|
+
return [...byPackage.entries()].map(([key, group]) => {
|
|
140
|
+
group.sort((a, b) => severityRank(a.security_advisory?.severity) - severityRank(b.security_advisory?.severity));
|
|
141
|
+
const worst = group[0];
|
|
142
|
+
const pkg = worst.dependency?.package ?? worst.security_vulnerability?.package ?? {};
|
|
143
|
+
const numbers = group.map((a) => a.number).sort((a, b) => a - b);
|
|
144
|
+
const severity = String(worst.security_advisory?.severity ?? "unknown");
|
|
145
|
+
const patched = group.map((a) => a.security_vulnerability?.first_patched_version?.identifier).filter((v) => !!v).sort((a, b) => a.localeCompare(b, void 0, { numeric: true })).at(-1);
|
|
146
|
+
const covered = prs.find((pr) => upgrades(pr, pkg.name));
|
|
147
|
+
const manifest = worst.dependency?.manifest_path;
|
|
148
|
+
const count = numbers.length > 1 ? `${numbers.length} Dependabot alerts` : `Dependabot alert #${numbers[0]}`;
|
|
149
|
+
return {
|
|
150
|
+
key: `dependabot#${numbers[0]}`,
|
|
151
|
+
legacyKey: `dependabot:${key}`,
|
|
152
|
+
ref: numbers.length > 1 ? `${numbers.length} alerts` : `#${numbers[0]}`,
|
|
153
|
+
title: `${pkg.name} (${pkg.ecosystem})`,
|
|
154
|
+
detail: [`${worst.security_vulnerability?.vulnerable_version_range ?? "?"} \u2192 ${patched ?? "no fix yet"}`, worst.security_advisory?.summary].filter(Boolean).join(" \xB7 "),
|
|
155
|
+
url: worst.html_url,
|
|
156
|
+
severity,
|
|
157
|
+
selected: !covered && !!patched,
|
|
158
|
+
note: covered ? `Dependabot PR #${covered.number} already upgrades it` : patched ? void 0 : "No patched version yet",
|
|
159
|
+
task: patched ? `Fix ${count} in ${pkg.name} (${pkg.ecosystem}): upgrade it to ${patched} or later${manifest ? ` in ${manifest}` : ""}. Update anything the upgrade breaks and keep the tests passing.` : `Mitigate ${count} in ${pkg.name} (${pkg.ecosystem}): no patched version exists yet. Avoid the vulnerable code path or replace the package, and explain what you did.`,
|
|
160
|
+
source: { kind: "alert", tool: "dependabot", number: numbers[0], numbers }
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function upgrades(pr, name) {
|
|
165
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
166
|
+
return new RegExp(`\\bbump ${escaped} from\\b`, "i").test(pr.title) || new RegExp(`/${escaped.replace(/\//g, "\\/")}-\\d`).test(pr.headRefName);
|
|
167
|
+
}
|
|
168
|
+
function coveringRun(runs, repo, source) {
|
|
169
|
+
const standing = runs.filter(
|
|
170
|
+
(r) => r.repo === repo && !r.archived && r.triage?.state !== "rejected" && r.status !== "failed" && r.status !== "cancelled" && r.source?.kind === source.kind
|
|
171
|
+
);
|
|
172
|
+
if (source.kind !== "alert") return standing.find((r) => r.source.number === source.number);
|
|
173
|
+
const numbers = source.numbers?.length ? source.numbers : [source.number];
|
|
174
|
+
const covers = numbers.map((n) => standing.find((r) => r.source.alert?.tool === source.tool && (r.source.alert?.numbers ?? [r.source.number]).includes(n)));
|
|
175
|
+
return covers.every(Boolean) ? covers[0] : void 0;
|
|
176
|
+
}
|
|
177
|
+
function sourceKeys(source) {
|
|
178
|
+
if (source.kind !== "alert") return [`${source.kind}#${source.number}`];
|
|
179
|
+
const numbers = source.numbers?.length ? source.numbers : [source.number];
|
|
180
|
+
return numbers.map((n) => `${source.tool ?? "alert"}#${n}`);
|
|
181
|
+
}
|
|
182
|
+
const SWEEP_SOURCE = { issues: "issue", dependabot: "pr", alerts: "alert" };
|
|
183
|
+
const sweepTakes = (kind, source) => SWEEP_SOURCE[kind] === source.kind;
|
|
184
|
+
function findSweepItem(list, source) {
|
|
185
|
+
return list.items.find((i) => {
|
|
186
|
+
const s = i.source;
|
|
187
|
+
if (!s || s.kind !== source.kind) return false;
|
|
188
|
+
if (s.kind !== "alert") return s.number === source.number;
|
|
189
|
+
return s.tool === source.tool && (s.numbers ?? [s.number]).includes(source.number);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function upgradeLegacyKeys(list, handled) {
|
|
193
|
+
let changed = false;
|
|
194
|
+
for (const item of list.items) {
|
|
195
|
+
if (!item.legacyKey || !item.source || !handled.has(item.legacyKey)) continue;
|
|
196
|
+
handled.delete(item.legacyKey);
|
|
197
|
+
for (const key of sourceKeys(item.source)) handled.add(key);
|
|
198
|
+
changed = true;
|
|
199
|
+
}
|
|
200
|
+
return changed;
|
|
201
|
+
}
|
|
202
|
+
async function runSweep(input) {
|
|
203
|
+
const outcome = { started: [], skipped: [], failed: [] };
|
|
204
|
+
const root = await repoRoot(input.repo).catch(() => input.repo);
|
|
205
|
+
const queue = [...input.items];
|
|
206
|
+
const seen = /* @__PURE__ */ new Set();
|
|
207
|
+
const worker = async () => {
|
|
208
|
+
for (let item = queue.shift(); item; item = queue.shift()) {
|
|
209
|
+
if (seen.has(item.key)) continue;
|
|
210
|
+
seen.add(item.key);
|
|
211
|
+
const covering = coveringRun(input.runs, root, item.source);
|
|
212
|
+
if (covering) {
|
|
213
|
+
outcome.skipped.push({ key: item.key, why: `already has run ${covering.id}` });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const run = await input.start(item);
|
|
218
|
+
outcome.started.push({ key: item.key, id: run.id });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
outcome.failed.push({ key: item.key, error: err.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
await Promise.all(Array.from({ length: Math.max(1, input.concurrency ?? 2) }, worker));
|
|
225
|
+
const order = new Map(input.items.map((item, i) => [item.key, i]));
|
|
226
|
+
for (const list of [outcome.started, outcome.skipped, outcome.failed]) list.sort((a, b) => order.get(a.key) - order.get(b.key));
|
|
227
|
+
return outcome;
|
|
228
|
+
}
|
|
229
|
+
function nextSweepItems(list, handled, max) {
|
|
230
|
+
return list.items.filter((i) => i.selected && !i.manual && i.source && i.task && !sourceKeys(i.source).every((k) => handled.has(k))).slice(0, max).map((i) => ({ key: i.key, task: i.task, source: i.source }));
|
|
231
|
+
}
|
|
232
|
+
export {
|
|
233
|
+
coveringRun,
|
|
234
|
+
findSweepItem,
|
|
235
|
+
listSweep,
|
|
236
|
+
nextSweepItems,
|
|
237
|
+
runSweep,
|
|
238
|
+
sourceKeys,
|
|
239
|
+
sweepTakes,
|
|
240
|
+
upgradeLegacyKeys
|
|
241
|
+
};
|
package/src/tailscale.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { isIPv4 } from "node:net";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { certificateHostname, validateRemoteCertificate } from "./remote-tls.js";
|
|
8
|
+
import { childEnv } from "./env.js";
|
|
9
|
+
function isTailscaleIPv4(address) {
|
|
10
|
+
if (!isIPv4(address)) return false;
|
|
11
|
+
const [a, b] = address.split(".").map(Number);
|
|
12
|
+
return a === 100 && b >= 64 && b <= 127;
|
|
13
|
+
}
|
|
14
|
+
function parseTailscaleStatus(output) {
|
|
15
|
+
const raw = JSON.parse(output);
|
|
16
|
+
if (!raw || typeof raw !== "object") throw new Error("Invalid Tailscale status");
|
|
17
|
+
switch (raw.BackendState) {
|
|
18
|
+
case "NeedsLogin":
|
|
19
|
+
return { state: "needs-login", message: "Sign in to Tailscale on this computer, then refresh." };
|
|
20
|
+
case "NeedsMachineAuth":
|
|
21
|
+
return { state: "needs-approval", message: "Ask your tailnet administrator to approve this computer, then refresh." };
|
|
22
|
+
case "Stopped":
|
|
23
|
+
return { state: "stopped", message: "Turn on Tailscale on this computer, then refresh." };
|
|
24
|
+
case "Starting":
|
|
25
|
+
return { state: "starting", message: "Tailscale is connecting. Codeflow will check again automatically." };
|
|
26
|
+
case "Running":
|
|
27
|
+
break;
|
|
28
|
+
default:
|
|
29
|
+
return { state: "unavailable", message: "Tailscale is not ready. Open Tailscale on this computer and check its connection." };
|
|
30
|
+
}
|
|
31
|
+
if (raw.TUN === false) return { state: "unavailable", message: "Direct access needs Tailscale's network interface. Userspace networking is not supported." };
|
|
32
|
+
if (raw.Self?.Online === false) return { state: "unavailable", message: "Tailscale reports this computer offline. Reconnect Tailscale, then refresh." };
|
|
33
|
+
const ip = Array.isArray(raw.TailscaleIPs) ? raw.TailscaleIPs.find((value) => typeof value === "string" && isTailscaleIPv4(value)) : void 0;
|
|
34
|
+
if (!ip) return { state: "unavailable", message: "Tailscale has no usable IPv4 address. Reconnect Tailscale, then refresh." };
|
|
35
|
+
const dns = typeof raw.Self?.DNSName === "string" ? raw.Self.DNSName.replace(/\.$/, "").toLowerCase() : "";
|
|
36
|
+
const dnsName = dns.length <= 253 && /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\.ts\.net$/.test(dns) ? dns : void 0;
|
|
37
|
+
return { state: "connected", message: "Tailscale is connected.", ip, ...dnsName ? { dnsName } : {} };
|
|
38
|
+
}
|
|
39
|
+
const run = promisify(execFile);
|
|
40
|
+
function commands() {
|
|
41
|
+
const names = ["tailscale"];
|
|
42
|
+
if (process.platform === "darwin") names.push("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
|
|
43
|
+
if (process.platform === "win32") names.push(join(process.env.ProgramFiles || "C:\\Program Files", "Tailscale", "tailscale.exe"));
|
|
44
|
+
return names;
|
|
45
|
+
}
|
|
46
|
+
async function detectTailscale(candidates = commands()) {
|
|
47
|
+
for (const command of candidates) {
|
|
48
|
+
try {
|
|
49
|
+
const { stdout } = await run(command, ["status", "--json", "--peers=false"], {
|
|
50
|
+
env: childEnv({ TAILSCALE_BE_CLI: "1" }),
|
|
51
|
+
timeout: 4e3,
|
|
52
|
+
maxBuffer: 1024 * 1024,
|
|
53
|
+
windowsHide: true
|
|
54
|
+
});
|
|
55
|
+
return parseTailscaleStatus(stdout);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code === "ENOENT") continue;
|
|
58
|
+
const stdout = error.stdout;
|
|
59
|
+
if (stdout) {
|
|
60
|
+
try {
|
|
61
|
+
return parseTailscaleStatus(stdout);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { state: "unavailable", message: "Could not read Tailscale status. Open Tailscale and check that its service is running, then refresh." };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { state: "not-installed", message: "Install Tailscale on this computer and your remote device, sign in to the same tailnet, then refresh." };
|
|
69
|
+
}
|
|
70
|
+
async function requestTailscaleCertificate(hostname, candidates = commands()) {
|
|
71
|
+
const host = certificateHostname(hostname);
|
|
72
|
+
if (!host.endsWith(".ts.net")) throw new Error("Connect Tailscale with a valid tailnet DNS name first");
|
|
73
|
+
const directory = await mkdtemp(join(tmpdir(), "codeflow-tls-"));
|
|
74
|
+
try {
|
|
75
|
+
for (const command of candidates) {
|
|
76
|
+
try {
|
|
77
|
+
await run(command, ["cert", `--cert-file=${join(directory, "cert.pem")}`, `--key-file=${join(directory, "key.pem")}`, "--min-validity=720h", host], {
|
|
78
|
+
env: childEnv({ TAILSCALE_BE_CLI: "1" }),
|
|
79
|
+
timeout: 6e4,
|
|
80
|
+
maxBuffer: 64 * 1024,
|
|
81
|
+
windowsHide: true
|
|
82
|
+
});
|
|
83
|
+
const value = { hostname: host, source: "tailscale", cert: await readFile(join(directory, "cert.pem"), "utf8"), key: await readFile(join(directory, "key.pem"), "utf8") };
|
|
84
|
+
validateRemoteCertificate(value);
|
|
85
|
+
return value;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error.code === "ENOENT") continue;
|
|
88
|
+
throw new Error("Tailscale could not issue the certificate. Enable HTTPS certificates in your tailnet DNS settings, check that this user can run tailscale cert, and use a current Tailscale version. Then retry or import certificate files.");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new Error("Tailscale CLI was not found. Install Tailscale or import certificate files.");
|
|
92
|
+
} finally {
|
|
93
|
+
await rm(directory, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
detectTailscale,
|
|
98
|
+
isTailscaleIPv4,
|
|
99
|
+
parseTailscaleStatus,
|
|
100
|
+
requestTailscaleCertificate
|
|
101
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const TASK_DRAFT_SCHEMA = {
|
|
2
|
+
type: "object",
|
|
3
|
+
additionalProperties: false,
|
|
4
|
+
required: ["title", "goal", "acceptance"],
|
|
5
|
+
properties: {
|
|
6
|
+
title: { type: "string", minLength: 1, maxLength: 200 },
|
|
7
|
+
goal: { type: "string", minLength: 1, maxLength: 16384 },
|
|
8
|
+
acceptance: { type: "string", minLength: 1, maxLength: 8192 }
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function taskDraftProblem(value) {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "Expected a task draft object";
|
|
13
|
+
const draft = value;
|
|
14
|
+
if (Object.keys(draft).some((key) => !["title", "goal", "acceptance"].includes(key))) return "Unexpected task draft field";
|
|
15
|
+
for (const [key, limit] of [["title", 200], ["goal", 16384], ["acceptance", 8192]]) {
|
|
16
|
+
const text = draft[key];
|
|
17
|
+
if (typeof text !== "string" || !text.trim() || text.length > limit || text.includes("\0")) return `Draft ${key} must contain 1\u2013${limit} characters`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
TASK_DRAFT_SCHEMA,
|
|
22
|
+
taskDraftProblem
|
|
23
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { CODEFLOW_HOME } from "./config.js";
|
|
5
|
+
const finite = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
6
|
+
const interval = (start, end) => finite(start) && finite(end) && end >= start ? [[start, end]] : [];
|
|
7
|
+
function intervalDuration(intervals) {
|
|
8
|
+
let total = 0, start = -1, end = -1;
|
|
9
|
+
for (const [a, b] of [...intervals].sort((a2, b2) => a2[0] - b2[0])) {
|
|
10
|
+
if (a > end) {
|
|
11
|
+
if (start >= 0) total += end - start;
|
|
12
|
+
start = a;
|
|
13
|
+
end = b;
|
|
14
|
+
} else end = Math.max(end, b);
|
|
15
|
+
}
|
|
16
|
+
return total + (start < 0 ? 0 : end - start);
|
|
17
|
+
}
|
|
18
|
+
function buildTeamEvaluation(run, options = {}) {
|
|
19
|
+
const team = run.team;
|
|
20
|
+
const turns = run.steps.filter((step) => step.kind === "agent");
|
|
21
|
+
const coordinator = (step) => step.coordination === true || !team && step.role === "supervisor";
|
|
22
|
+
const workers = turns.filter((step) => {
|
|
23
|
+
if (!team) return step.role === "workers" || step.role === "worker";
|
|
24
|
+
const member = team.members.find((item) => item.id === step.memberId);
|
|
25
|
+
if (member) return member.role === "worker" && !coordinator(step);
|
|
26
|
+
return !!step.taskId && !coordinator(step) && step.role !== "coordinator" && step.role !== "reviewer";
|
|
27
|
+
});
|
|
28
|
+
const invocations = team?.metrics.invocations ?? turns.reduce((sum, step) => sum + (step.invocations ?? 1), 0);
|
|
29
|
+
const coordination = team?.metrics.coordinatorInvocations ?? turns.filter(coordinator).reduce((sum, step) => sum + (step.invocations ?? 1), 0);
|
|
30
|
+
const peer = (team?.messages ?? []).filter((message) => message.kind === "question" && message.recipient !== "user" && message.originalRecipient !== "user");
|
|
31
|
+
const peerIntervals = peer.flatMap((message) => interval(message.createdAt, message.answeredAt));
|
|
32
|
+
const barriers = [];
|
|
33
|
+
for (const message of peer) {
|
|
34
|
+
if (!finite(message.answeredAt)) continue;
|
|
35
|
+
const task = team?.tasks.find((item) => item.id === message.taskId);
|
|
36
|
+
if (!task || !task.dependsOn.every((id) => {
|
|
37
|
+
const dependency = team?.tasks.find((item) => item.id === id);
|
|
38
|
+
return finite(dependency?.finishedAt) && dependency.finishedAt <= message.answeredAt;
|
|
39
|
+
})) continue;
|
|
40
|
+
const next = workers.filter((step) => step.taskId === task.id && step.startedAt >= message.answeredAt).sort((a, b) => a.startedAt - b.startedAt)[0];
|
|
41
|
+
if (next) barriers.push([message.answeredAt, next.startedAt]);
|
|
42
|
+
}
|
|
43
|
+
const usage = turns.filter((step) => step.usage);
|
|
44
|
+
const integrityFlags = Object.values(run.integrity ?? {}).reduce((sum, issues) => sum + issues.length, 0);
|
|
45
|
+
const check = team?.verification?.check === "not_required" ? "not_required" : run.verdict === "passed" ? "passed" : run.verdict === "failing" ? "failed" : "unknown";
|
|
46
|
+
const approved = team ? !!team.verification?.reviewed && !!team.verification.approved : run.facts?.approved === true;
|
|
47
|
+
const requirements = team?.requirements.length ?? 0;
|
|
48
|
+
const covered = team?.requirements.filter((requirement) => {
|
|
49
|
+
const tasks = team.tasks.filter((task) => task.requirements.includes(requirement.id));
|
|
50
|
+
return tasks.length > 0 && tasks.every((task) => task.status === "completed" && !task.evidenceStale && (!task.retainedBaseline || approved));
|
|
51
|
+
}).length ?? 0;
|
|
52
|
+
const gaps = team?.gaps.length ?? 0;
|
|
53
|
+
const unchangedRerun = !!run.teamRerun && run.facts?.proofNotRequired === "unchanged-rerun" && check === "passed" && approved;
|
|
54
|
+
const verified = run.status === "done" && approved && (check === "passed" || check === "not_required") && !gaps && !integrityFlags && (!team || team.status === "completed" && covered === requirements) && (run.prove === false || check === "not_required" || run.facts?.proof === "proven" || unchangedRerun);
|
|
55
|
+
const mode = options.mode ?? "unknown";
|
|
56
|
+
const approvalAnswers = options.journal ? [...options.journal].filter(([key]) => key.startsWith("approval:")).map(([, value]) => value) : void 0;
|
|
57
|
+
const rated = options.memoryEntries?.flatMap((entry) => entry.feedback?.filter((feedback) => feedback.runId === run.id && run.memoryManifest?.included.some((ref) => ref.id === entry.id && ref.revision === feedback.revision)) ?? []);
|
|
58
|
+
return {
|
|
59
|
+
version: 1,
|
|
60
|
+
id: `evaluation-${run.id}`,
|
|
61
|
+
runId: run.id,
|
|
62
|
+
at: run.endedAt ?? Date.now(),
|
|
63
|
+
mode,
|
|
64
|
+
caseId: options.caseId ?? "unspecified",
|
|
65
|
+
pairId: options.pairId ?? run.group?.id,
|
|
66
|
+
taskHash: createHash("sha256").update(run.task).digest("hex"),
|
|
67
|
+
baseRef: run.baseRef,
|
|
68
|
+
pattern: run.pattern,
|
|
69
|
+
cliVersions: { ...options.cliVersions },
|
|
70
|
+
status: run.status,
|
|
71
|
+
verified,
|
|
72
|
+
reproducibility: {
|
|
73
|
+
nodeVersion: process.version,
|
|
74
|
+
platform: `${process.platform}/${process.arch}`,
|
|
75
|
+
...options.reproducibility,
|
|
76
|
+
requestedSettings: Object.fromEntries(Object.entries(run.agentSettings ?? {}).filter((entry) => entry[1] !== void 0).map(([id, settings]) => [id, structuredClone(settings)])),
|
|
77
|
+
stepSettings: [...new Map(turns.filter((step) => step.agent).map((step) => {
|
|
78
|
+
const settings = { provider: step.agent, model: step.settings?.model, effort: step.settings?.effort };
|
|
79
|
+
return [JSON.stringify(settings), settings];
|
|
80
|
+
})).values()]
|
|
81
|
+
},
|
|
82
|
+
verification: { checks: check, review: approved ? "approved" : run.facts?.approved === false || team?.verification?.reviewed ? "rejected" : "unknown", proof: run.facts?.proof, gaps, requirements, covered, integrityFlags, ...run.teamRerun ? { proofNotRequired: run.facts?.proofNotRequired, retainedTasks: team?.tasks.filter((task) => task.retainedBaseline).length ?? 0, newlyExecutedTasks: team?.tasks.filter((task) => !task.retainedBaseline && (task.attempts ?? 0) > 0).length ?? 0, requirementsUsingBaseline: team?.requirements.filter((requirement) => team.tasks.some((task) => task.retainedBaseline && task.requirements.includes(requirement.id))).length ?? 0 } : {} },
|
|
83
|
+
metrics: {
|
|
84
|
+
invocations,
|
|
85
|
+
coordinatorInvocations: coordination,
|
|
86
|
+
coordinatorFraction: invocations ? coordination / invocations : null,
|
|
87
|
+
workerTurns: workers.length,
|
|
88
|
+
multiWorker: new Set(workers.map((step) => team ? step.memberId ?? step.agent : step.worktree ?? step.agent)).size > 1,
|
|
89
|
+
elapsedMs: run.endedAt === void 0 ? void 0 : Math.max(0, run.endedAt - run.createdAt),
|
|
90
|
+
activeWorkMs: intervalDuration(run.steps.flatMap((step) => interval(step.startedAt, step.endedAt))),
|
|
91
|
+
coordinatorMs: turns.some(coordinator) ? turns.filter(coordinator).reduce((sum, step) => sum + (finite(step.endedAt) ? Math.max(0, step.endedAt - step.startedAt) : 0), 0) : void 0,
|
|
92
|
+
reportedTokens: usage.length ? usage.reduce((sum, step) => sum + step.usage.inputTokens + (step.usage.cacheReadTokens ?? 0) + step.usage.outputTokens, 0) : void 0,
|
|
93
|
+
reportedApiCostUsd: usage.some((step) => step.usage.costUsd > 0) ? usage.reduce((sum, step) => sum + step.usage.costUsd, 0) : void 0,
|
|
94
|
+
tokenReportingTurns: usage.length,
|
|
95
|
+
questions: team?.metrics.questionCount ?? 0,
|
|
96
|
+
peerQuestions: peer.length,
|
|
97
|
+
blockedTurns: team?.metrics.blockedTurns ?? 0,
|
|
98
|
+
peerYieldRate: workers.length ? new Set(peer.filter((message) => message.blocking).map((message) => `${message.sender}:${message.taskId}:${message.sourceAttempt ?? message.sourceStepId ?? message.id}`)).size / workers.length : null,
|
|
99
|
+
peerWaitingMs: peerIntervals.length ? peerIntervals.reduce((sum, [a, b]) => sum + b - a, 0) : peer.length ? void 0 : 0,
|
|
100
|
+
peerWaitingWallMs: peerIntervals.length ? intervalDuration(peerIntervals) : peer.length ? void 0 : 0,
|
|
101
|
+
avoidableBarrierEstimateMs: barriers.length ? intervalDuration(barriers) : peer.length ? void 0 : 0,
|
|
102
|
+
timedPeerQuestions: peerIntervals.length,
|
|
103
|
+
repairTurns: turns.filter((step) => !step.taskId && /repair|fix/i.test(step.title)).length,
|
|
104
|
+
// engine-titled repair/fix steps, never model-titled task names
|
|
105
|
+
selectedMemories: run.memoryManifest?.included.length ?? 0,
|
|
106
|
+
omittedMemories: run.memoryManifest?.omitted.length ?? 0,
|
|
107
|
+
coordinatorRevisions: team?.metrics.revisions ?? 0,
|
|
108
|
+
approvals: approvalAnswers ? { approved: approvalAnswers.filter((answer) => answer.decision === "approve").length, revised: approvalAnswers.filter((answer) => answer.decision === "revise").length, rejected: approvalAnswers.filter((answer) => answer.decision === "reject").length, withReason: approvalAnswers.filter((answer) => !!answer.note?.trim()).length } : void 0,
|
|
109
|
+
memoryFeedback: rated ? { useful: rated.filter((item) => item.rating === "useful").length, irrelevant: rated.filter((item) => item.rating === "irrelevant").length, stale: rated.filter((item) => item.rating === "stale").length, incorrect: rated.filter((item) => item.rating === "incorrect").length } : void 0,
|
|
110
|
+
replay: options.replay
|
|
111
|
+
},
|
|
112
|
+
limitations: [
|
|
113
|
+
...mode !== "real" ? [mode === "fake" ? "Deterministic fake CLIs validate orchestration, not model quality or subscription savings." : "Execution provenance is unknown; excluded from real-run rollout evidence."] : [],
|
|
114
|
+
"API-equivalent cost is not a subscription charge; unreported values remain unknown.",
|
|
115
|
+
"Model identities record requested CLI settings. A provider default or alias is not an immutable model snapshot and may change between trials.",
|
|
116
|
+
"Avoidable barrier time is a counterfactual upper-bound estimate after answered peer questions; it does not prove spare process capacity or an asynchronous speedup.",
|
|
117
|
+
"Active work is the union of observed step intervals; it excludes gaps and is not a full run-clock measurement.",
|
|
118
|
+
...run.teamRerun ? ["Selective reruns retain explicitly pinned source task inputs. Baseline task counts are not newly executed work; final review still covers every original requirement."] : [],
|
|
119
|
+
...!team ? ["Supervisor coordination includes its planning, integration and repair turns; legacy requirements have no structured coverage manifest."] : []
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function assessTeamRollout(reports) {
|
|
124
|
+
const real = reports.filter((report) => report.mode === "real" && report.status !== "running");
|
|
125
|
+
const auto = real.filter((report) => report.pattern === "auto-team");
|
|
126
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
127
|
+
for (const report of real) if (report.pairId) {
|
|
128
|
+
const key = `${report.pairId}:${report.caseId}:${report.taskHash}:${report.baseRef}`;
|
|
129
|
+
pairs.set(key, [...pairs.get(key) ?? [], report]);
|
|
130
|
+
}
|
|
131
|
+
const matched = [...pairs.values()].filter((items) => items.some((item) => item.pattern === "auto-team") && items.some((item) => item.pattern === "supervisor"));
|
|
132
|
+
const verifiedMatch = matched.some((items) => items.some((item) => item.pattern === "auto-team" && item.verified));
|
|
133
|
+
const pairedQuality = matched.every((items) => !items.some((item) => item.pattern === "supervisor" && item.verified) || items.some((item) => item.pattern === "auto-team" && item.verified));
|
|
134
|
+
const multi = auto.filter((report) => report.metrics.multiWorker);
|
|
135
|
+
const denominator = multi.reduce((sum, report) => sum + report.metrics.invocations, 0);
|
|
136
|
+
const overhead = denominator ? multi.reduce((sum, report) => sum + report.metrics.coordinatorInvocations, 0) / denominator : null;
|
|
137
|
+
const workerTurns = auto.reduce((sum, report) => sum + report.metrics.workerTurns, 0);
|
|
138
|
+
const peerYields = auto.reduce((sum, report) => sum + (report.metrics.peerYieldRate ?? 0) * report.metrics.workerTurns, 0);
|
|
139
|
+
const blockers = [];
|
|
140
|
+
if (auto.length < 20) blockers.push(`Need 20 real Auto runs; recorded ${auto.length}.`);
|
|
141
|
+
if (workerTurns < 100) blockers.push(`Need 100 real worker turns; recorded ${workerTurns}.`);
|
|
142
|
+
if (matched.length < 20) blockers.push(`Need 20 matched Auto/Supervisor comparisons; recorded ${matched.length}.`);
|
|
143
|
+
if (!pairedQuality) blockers.push("Auto verification regressed against a matched Supervisor result.");
|
|
144
|
+
if (auto.some((report) => !report.verified)) blockers.push("Some real Auto results are incomplete or unverified.");
|
|
145
|
+
if (overhead === null || overhead > 0.25) blockers.push(overhead === null ? "No multi-worker coordinator overhead sample." : "Observed multi-worker coordinator calls exceed 25% of invocations.");
|
|
146
|
+
if (real.some((report) => !Object.keys(report.cliVersions).length)) blockers.push("Some real results have no recorded CLI versions.");
|
|
147
|
+
return { realRuns: real.length, realAutoRuns: auto.length, realWorkerTurns: workerTurns, matchedPairs: matched.length, verifiedAutoRuns: auto.filter((report) => report.verified).length, overheadFraction: overhead, peerYieldRate: workerTurns ? peerYields / workerTurns : null, eligibleBeta: verifiedMatch && pairedQuality && real.every((report) => Object.keys(report.cliVersions).length > 0), eligibleDefault: blockers.length === 0, blockers };
|
|
148
|
+
}
|
|
149
|
+
class TeamEvaluationStore {
|
|
150
|
+
constructor(directory = join(CODEFLOW_HOME, "team-evaluations")) {
|
|
151
|
+
this.directory = directory;
|
|
152
|
+
}
|
|
153
|
+
queue = Promise.resolve();
|
|
154
|
+
async write(file, value) {
|
|
155
|
+
await mkdir(this.directory, { recursive: true });
|
|
156
|
+
const temporary = join(this.directory, `.${file}.${randomBytes(8).toString("hex")}.tmp`);
|
|
157
|
+
try {
|
|
158
|
+
const handle = await open(temporary, "wx", 384);
|
|
159
|
+
try {
|
|
160
|
+
await handle.writeFile(JSON.stringify(value, null, 2) + "\n");
|
|
161
|
+
await handle.sync();
|
|
162
|
+
} finally {
|
|
163
|
+
await handle.close();
|
|
164
|
+
}
|
|
165
|
+
await rename(temporary, join(this.directory, file));
|
|
166
|
+
if (process.platform !== "win32") {
|
|
167
|
+
const dir = await open(this.directory, "r");
|
|
168
|
+
try {
|
|
169
|
+
await dir.sync();
|
|
170
|
+
} finally {
|
|
171
|
+
await dir.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
await rm(temporary, { force: true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async list() {
|
|
179
|
+
const names = await readdir(this.directory).catch((error) => {
|
|
180
|
+
if (error.code === "ENOENT") return [];
|
|
181
|
+
throw error;
|
|
182
|
+
});
|
|
183
|
+
const reports = await Promise.all(names.filter((name) => /^evaluation-[\w-]+\.json$/.test(name)).map(async (name) => {
|
|
184
|
+
const value = JSON.parse(await readFile(join(this.directory, name), "utf8"));
|
|
185
|
+
if (value.version !== 1 || value.id + ".json" !== name) throw new Error(`Unsupported or damaged evaluation ${name}`);
|
|
186
|
+
return value;
|
|
187
|
+
}));
|
|
188
|
+
return reports.sort((a, b) => b.at - a.at || a.id.localeCompare(b.id));
|
|
189
|
+
}
|
|
190
|
+
async record(report) {
|
|
191
|
+
if (report.version !== 1 || !/^evaluation-[\w-]+$/.test(report.id) || !["fake", "real", "unknown"].includes(report.mode)) throw new Error("Invalid evaluation report");
|
|
192
|
+
const operation = this.queue.then(async () => {
|
|
193
|
+
const previous = await readFile(join(this.directory, `${report.id}.json`), "utf8").catch((error) => {
|
|
194
|
+
if (error.code === "ENOENT") return void 0;
|
|
195
|
+
throw error;
|
|
196
|
+
});
|
|
197
|
+
if (previous) {
|
|
198
|
+
if (JSON.stringify(JSON.parse(previous)) !== JSON.stringify(report)) throw new Error("An evaluation is immutable; use a new run for another trial");
|
|
199
|
+
return report;
|
|
200
|
+
}
|
|
201
|
+
await this.write(`${report.id}.json`, report);
|
|
202
|
+
return report;
|
|
203
|
+
});
|
|
204
|
+
this.queue = operation.catch(() => void 0);
|
|
205
|
+
return operation;
|
|
206
|
+
}
|
|
207
|
+
async rollout(enabled = true) {
|
|
208
|
+
const decision = await readFile(join(this.directory, "rollout.json"), "utf8").then((text) => JSON.parse(text), (error) => {
|
|
209
|
+
if (error.code === "ENOENT") return void 0;
|
|
210
|
+
throw error;
|
|
211
|
+
});
|
|
212
|
+
if (decision && (!["experimental", "beta", "default"].includes(decision.stage) || !Array.isArray(decision.evaluationIds))) throw new Error("Damaged team rollout decision");
|
|
213
|
+
return { enabled, stage: decision?.stage ?? "experimental", reason: enabled ? decision?.rationale ?? "No evaluation promotion decision has been recorded. The application default is configured separately." : "New Auto teams are disabled. Existing teams can still be viewed and resumed.", decision };
|
|
214
|
+
}
|
|
215
|
+
async decide(input) {
|
|
216
|
+
if (!["experimental", "beta", "default"].includes(input.stage) || typeof input.rationale !== "string" || !input.rationale.trim() || input.rationale.length > 4e3 || !Array.isArray(input.evaluationIds)) throw new Error("Choose a stage, evaluation reports and a written rationale");
|
|
217
|
+
const operation = this.queue.then(async () => {
|
|
218
|
+
const all = await this.list();
|
|
219
|
+
const ids = [...new Set(input.evaluationIds)];
|
|
220
|
+
if (ids.some((id) => !all.some((report) => report.id === id))) throw new Error("Unknown evaluation report");
|
|
221
|
+
const selected = all.filter((report) => ids.includes(report.id));
|
|
222
|
+
const assessment = assessTeamRollout(all);
|
|
223
|
+
if (input.stage === "beta" && !assessment.eligibleBeta) throw new Error("Beta needs a verified real Auto result and a matched Supervisor comparison with CLI versions and no verification regression");
|
|
224
|
+
if (input.stage === "default" && !assessment.eligibleDefault) throw new Error(`Default rollout is not supported: ${assessment.blockers.join(" ")}`);
|
|
225
|
+
if (input.stage !== "experimental" && !selected.length) throw new Error("Cite the evaluation reports the decision rests on");
|
|
226
|
+
const decision = { at: Date.now(), stage: input.stage, rationale: input.rationale.trim(), evaluationIds: ids };
|
|
227
|
+
await this.write(`decision-${decision.at}-${randomBytes(4).toString("hex")}.json`, decision);
|
|
228
|
+
await this.write("rollout.json", decision);
|
|
229
|
+
return this.rollout();
|
|
230
|
+
});
|
|
231
|
+
this.queue = operation.catch(() => void 0);
|
|
232
|
+
return operation;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const teamEvaluationStore = new TeamEvaluationStore();
|
|
236
|
+
export {
|
|
237
|
+
TeamEvaluationStore,
|
|
238
|
+
assessTeamRollout,
|
|
239
|
+
buildTeamEvaluation,
|
|
240
|
+
intervalDuration,
|
|
241
|
+
teamEvaluationStore
|
|
242
|
+
};
|