@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
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
|
+
};
|
package/src/task-team.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { git, worktreeGit } from "./git.js";
|
|
5
|
+
import { validateAgentSettings } from "./adapters/index.js";
|
|
6
|
+
class TaskTeamConflict extends Error {
|
|
7
|
+
constructor(reconciliation) {
|
|
8
|
+
super("Your edits overlap the team's changes. Review the conflicting files; both versions are preserved.");
|
|
9
|
+
this.reconciliation = reconciliation;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const normalizedRequest = (request) => request.trim().toLowerCase().replace(/[’‘]/g, "'").replace(/^(?:(?:please|can you|could you|would you)\s+)+/, "");
|
|
13
|
+
function taskTeamControl(request) {
|
|
14
|
+
const text = normalizedRequest(request);
|
|
15
|
+
const match = /^(stop|cancel|pause|wait|hold on)(?:\s+(?:(?:the|this|all|current)\s+)?(?:team|task|run|work|execution|everything|working))?(?=\s*(?:[.!?]|$|(?:and|for now|please)\b))/.exec(text);
|
|
16
|
+
return match ? /^(pause|wait|hold on)$/.test(match[1]) ? "pause" : "stop" : void 0;
|
|
17
|
+
}
|
|
18
|
+
function taskTeamQuestionReply(request) {
|
|
19
|
+
const text = normalizedRequest(request);
|
|
20
|
+
if (taskTeamControl(text)) return false;
|
|
21
|
+
if (/^(?:what|why|how|explain|summari[sz]e|describe|tell me|status)\b/.test(text)) return false;
|
|
22
|
+
if (/^(?:implement|build|add|remove|delete|refactor|fix|update|change|create|deploy|commit|push|run|test|keep|preserve|stop|avoid|leave|don't|do not)\b/.test(text)) return false;
|
|
23
|
+
if (/(?:[;.!?]|\bbut|\bthen)\s*(?:please\s+)?(?:fix|update|change|stop|keep|preserve|use|switch)\b/.test(text)) return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function taskTeamRoute(request, active = false) {
|
|
27
|
+
const text = normalizedRequest(request);
|
|
28
|
+
if (/\b(use|ask|start|run|delegate|bring in)\b.{0,32}\b(team|teammates|workers)\b/.test(text)) return "team";
|
|
29
|
+
const work = "implement|build|add|remove|delete|refactor|fix|update|change|create|deploy|commit|push|run|test|upload|download|click|type|keep|preserve|stop|avoid|leave|don't|do not";
|
|
30
|
+
const alsoWork = new RegExp(`(?:\\b(?:and|then|also|now|but|instead)\\b|[.;?!,]|\\n)\\s*(?:(?:can|could|would) you\\s+|please\\s+)?(?:${work}|use|switch|assign|delegate)\\b`).test(text);
|
|
31
|
+
if (alsoWork || /^(?:keep|preserve|stop|avoid|leave|don't|do not)\b/.test(text) || /^what about (?:keeping|preserving|leaving|stopping|avoiding|changing|updating)\b/.test(text)) return "team";
|
|
32
|
+
if (/^(hi|hello|hey|thanks|thank you|ok|okay|great|got it)[.! ]*$/.test(text)) return "direct";
|
|
33
|
+
if (/^(what\b|why\b|how (?:does|do|did|is|are|can|would)\b|explain\b|summari[sz]e\b|describe\b|tell me\b|status\b|any (?:update|progress))/.test(text) && text.length < 3e3) return "direct";
|
|
34
|
+
if (/^(?:is|are|was|were|does|should)\b/.test(text) && text.endsWith("?")) return "direct";
|
|
35
|
+
if (!active && /^(?:just|only) (?:rename|change|fix|update|remove)\b/.test(text) && text.length < 300 && !/\b(?:and|then|also)\b|[;\n]/.test(text)) return "direct";
|
|
36
|
+
return "team";
|
|
37
|
+
}
|
|
38
|
+
function validateTaskTeam(raw) {
|
|
39
|
+
const v = raw;
|
|
40
|
+
if (!v || typeof v !== "object" || typeof v.enabled !== "boolean") throw new Error("Choose whether Team is enabled");
|
|
41
|
+
const supported = (id) => ["claude", "codex", "agy"].includes(String(id));
|
|
42
|
+
if (!supported(v.coordinator) || !Array.isArray(v.providers) || !v.providers.length || v.providers.length > 3 || !v.providers.every(supported) || new Set(v.providers).size !== v.providers.length) throw new Error("Choose a coordinator and at least one available team provider");
|
|
43
|
+
if (!["adaptive", "build-review", "investigate"].includes(v.preset)) throw new Error("Choose a team template");
|
|
44
|
+
for (const [name, min, max] of [["maxMembers", 3, 6], ["maxCalls", 4, 200], ["maxMinutes", 1, 720]]) if (!Number.isInteger(v[name]) || v[name] < min || v[name] > max) throw new Error(`${name} must be between ${min} and ${max}`);
|
|
45
|
+
if (typeof v.approvePlan !== "boolean") throw new Error("Choose a plan approval preference");
|
|
46
|
+
const settings = validateAgentSettings(v.settings ?? {});
|
|
47
|
+
let template;
|
|
48
|
+
if (v.template) {
|
|
49
|
+
const t = v.template;
|
|
50
|
+
if (typeof t.id !== "string" || !Number.isInteger(t.version) || t.version < 1 || !t.inputs || typeof t.inputs !== "object" || Array.isArray(t.inputs) || Object.values(t.inputs).some((value) => typeof value !== "string" || value.length > 1e5)) throw new Error("Invalid saved team template");
|
|
51
|
+
template = { id: t.id, version: t.version, inputs: { ...t.inputs } };
|
|
52
|
+
}
|
|
53
|
+
return { enabled: v.enabled, preset: v.preset, coordinator: v.coordinator, providers: [...v.providers], settings, maxMembers: v.maxMembers, maxCalls: v.maxCalls, maxMinutes: v.maxMinutes, approvePlan: v.approvePlan, ...template ? { template } : {} };
|
|
54
|
+
}
|
|
55
|
+
async function checked(at, args, extra = {}) {
|
|
56
|
+
const r = await git(args, at, extra);
|
|
57
|
+
if (r.code !== 0) throw new Error(r.stderr || "Could not prepare the team workspace");
|
|
58
|
+
return r.stdout.trim();
|
|
59
|
+
}
|
|
60
|
+
async function taskCheckpoint(repo, path) {
|
|
61
|
+
const at = await worktreeGit(repo, path), temp = await mkdtemp(join(tmpdir(), "codeflow-team-index-"));
|
|
62
|
+
try {
|
|
63
|
+
const env = { GIT_INDEX_FILE: join(temp, "index") };
|
|
64
|
+
await checked(at, ["read-tree", "HEAD"], { env });
|
|
65
|
+
await checked(at, ["add", "-A", "--", ".", ":(exclude)**/.codeflow-context/**"], { env });
|
|
66
|
+
const tree = await checked(at, ["write-tree"], { env });
|
|
67
|
+
const head = await checked(at, ["rev-parse", "HEAD"]);
|
|
68
|
+
return await checked(at, ["-c", "user.name=code-flow", "-c", "user.email=code-flow@local.invalid", "commit-tree", tree, "-p", head, "-m", "Private team handoff checkpoint"]);
|
|
69
|
+
} finally {
|
|
70
|
+
await rm(temp, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function applyTaskTeamResult(repo, path, baseline, result) {
|
|
74
|
+
const at = await worktreeGit(repo, path);
|
|
75
|
+
const current = await taskCheckpoint(repo, path);
|
|
76
|
+
const tree = (ref) => checked(at, ["rev-parse", `${ref}^{tree}`]);
|
|
77
|
+
const [before, after, now] = await Promise.all([tree(baseline), tree(result), tree(current)]);
|
|
78
|
+
if (now === after || before === after) return { reconciled: false };
|
|
79
|
+
let target = result;
|
|
80
|
+
if (now !== before) {
|
|
81
|
+
const merge = await prepareTaskReconciliation(repo, path, baseline, result, current);
|
|
82
|
+
if (merge.conflicts.length) throw new TaskTeamConflict(merge);
|
|
83
|
+
target = merge.merged;
|
|
84
|
+
}
|
|
85
|
+
await applySnapshot(at, current, target);
|
|
86
|
+
return { reconciled: now !== before };
|
|
87
|
+
}
|
|
88
|
+
async function applySnapshot(at, current, result) {
|
|
89
|
+
const diff = await git(["diff", "--binary", "--no-ext-diff", "--no-textconv", current, result, "--"], at);
|
|
90
|
+
if (diff.code) throw new Error(diff.stderr);
|
|
91
|
+
if (!diff.stdout) return;
|
|
92
|
+
await checked(at, ["apply", "--check", "--whitespace=nowarn", "-"], { input: diff.stdout });
|
|
93
|
+
await checked(at, ["apply", "--whitespace=nowarn", "-"], { input: diff.stdout });
|
|
94
|
+
}
|
|
95
|
+
async function prepareTaskReconciliation(repo, path, baseline, result, current) {
|
|
96
|
+
const at = await worktreeGit(repo, path);
|
|
97
|
+
current ??= await taskCheckpoint(repo, path);
|
|
98
|
+
const sibling = async (ref) => checked(at, ["-c", "user.name=code-flow", "-c", "user.email=code-flow@local.invalid", "commit-tree", await checked(at, ["rev-parse", `${ref}^{tree}`]), "-p", baseline, "-m", "Private reconciliation snapshot"]);
|
|
99
|
+
const merged = await git(["merge-tree", "--write-tree", "--name-only", "-z", await sibling(current), await sibling(result)], at);
|
|
100
|
+
if (merged.code !== 0 && merged.code !== 1) throw new Error(merged.stderr || "Could not compare workspace changes");
|
|
101
|
+
const fields = merged.stdout.split("\0"), tree = fields.shift();
|
|
102
|
+
if (!/^[a-f0-9]{40,64}$/.test(tree)) throw new Error("Invalid reconciliation tree");
|
|
103
|
+
const end = fields.indexOf("");
|
|
104
|
+
const conflicts = merged.code === 1 ? fields.slice(0, end < 0 ? fields.length : end) : [];
|
|
105
|
+
if (merged.code === 1 && !conflicts.length) throw new Error("Could not identify conflicting files");
|
|
106
|
+
return { current, result, merged: tree, conflicts: [...new Set(conflicts)] };
|
|
107
|
+
}
|
|
108
|
+
async function reconciliationFile(repo, path, ref, file) {
|
|
109
|
+
const at = await worktreeGit(repo, path);
|
|
110
|
+
const entry = await checked(at, ["ls-tree", "-z", ref, "--", file]);
|
|
111
|
+
if (!entry) return { exists: false, text: "", mode: "" };
|
|
112
|
+
const [mode, kind, object] = entry.split(/\s/);
|
|
113
|
+
if (kind !== "blob" || !object) return { exists: true, binary: true, text: "", mode };
|
|
114
|
+
const size = Number(await checked(at, ["cat-file", "-s", object]));
|
|
115
|
+
if (size > 256 * 1024 || mode === "120000") return { exists: true, binary: true, text: "", mode };
|
|
116
|
+
const value = await git(["cat-file", "blob", object], at);
|
|
117
|
+
if (value.code) throw new Error("Could not read conflict version");
|
|
118
|
+
return { exists: true, binary: value.stdout.includes("\0") || value.stdout.includes("\uFFFD"), text: value.stdout, mode };
|
|
119
|
+
}
|
|
120
|
+
async function resolveTaskReconciliation(repo, path, merge, choices) {
|
|
121
|
+
if (!Array.isArray(choices) || choices.length !== merge.conflicts.length || new Set(choices.map((choice) => choice.path)).size !== choices.length || choices.some((choice) => !merge.conflicts.includes(choice.path) || !["current", "team", "edited"].includes(choice.choice))) throw new Error("Choose a resolution for each conflicting file");
|
|
122
|
+
const at = await worktreeGit(repo, path), current = await taskCheckpoint(repo, path);
|
|
123
|
+
if (await checked(at, ["rev-parse", `${current}^{tree}`]) !== await checked(at, ["rev-parse", `${merge.current}^{tree}`])) throw new Error("Your workspace changed since this comparison. Refresh the comparison before applying.");
|
|
124
|
+
const temp = await mkdtemp(join(tmpdir(), "codeflow-resolution-"));
|
|
125
|
+
try {
|
|
126
|
+
const env = { GIT_INDEX_FILE: join(temp, "index") };
|
|
127
|
+
await checked(at, ["read-tree", merge.merged], { env });
|
|
128
|
+
for (const choice of choices) {
|
|
129
|
+
if (choice.choice === "edited") {
|
|
130
|
+
if (typeof choice.text !== "string" || Buffer.byteLength(choice.text) > 256 * 1024 || /^(<<<<<<<|=======|>>>>>>>)(?: |$)/m.test(choice.text)) throw new Error("Provide resolved text without conflict markers (up to 256 KB)");
|
|
131
|
+
const versions = await Promise.all([merge.current, merge.result].map((ref) => reconciliationFile(repo, path, ref, choice.path)));
|
|
132
|
+
if (versions.some((version) => version.binary)) throw new Error("Choose an existing version for binary or symbolic-link conflicts");
|
|
133
|
+
const mode = versions.find((version) => version.exists)?.mode || "100644";
|
|
134
|
+
const object = await checked(at, ["hash-object", "-w", "--stdin"], { input: choice.text });
|
|
135
|
+
await checked(at, ["update-index", "--add", "--cacheinfo", `${mode},${object},${choice.path}`], { env });
|
|
136
|
+
} else {
|
|
137
|
+
const ref = choice.choice === "current" ? merge.current : merge.result;
|
|
138
|
+
const entry = await checked(at, ["ls-tree", "-z", ref, "--", choice.path]);
|
|
139
|
+
if (!entry) await checked(at, ["update-index", "--force-remove", "--", choice.path], { env });
|
|
140
|
+
else {
|
|
141
|
+
const [mode, , object] = entry.split(/\s/);
|
|
142
|
+
await checked(at, ["update-index", "--add", "--cacheinfo", `${mode},${object},${choice.path}`], { env });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const tree = await checked(at, ["write-tree"], { env });
|
|
147
|
+
await applySnapshot(at, current, tree);
|
|
148
|
+
return tree;
|
|
149
|
+
} finally {
|
|
150
|
+
await rm(temp, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
TaskTeamConflict,
|
|
155
|
+
applyTaskTeamResult,
|
|
156
|
+
prepareTaskReconciliation,
|
|
157
|
+
reconciliationFile,
|
|
158
|
+
resolveTaskReconciliation,
|
|
159
|
+
taskCheckpoint,
|
|
160
|
+
taskTeamControl,
|
|
161
|
+
taskTeamQuestionReply,
|
|
162
|
+
taskTeamRoute,
|
|
163
|
+
validateTaskTeam
|
|
164
|
+
};
|