@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/watch.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { git } from "./git.js";
|
|
2
|
+
import { CODEFLOW_MARKER, failedLogs, GH_AUTH_ERROR, ghAuthStatus, githubUser, prChecks, prStatus, safeFence } from "./github.js";
|
|
3
|
+
import { logWarn } from "./log.js";
|
|
4
|
+
const ACTIVE = /* @__PURE__ */ new Set(["watching", "fixing", "green"]);
|
|
5
|
+
const FAILURES_BEFORE_NEEDS_YOU = 3;
|
|
6
|
+
const MAX_BACKOFF_MS = 30 * 6e4;
|
|
7
|
+
const MAX_QUIET_MS = 10 * 6e4;
|
|
8
|
+
class PrWatcher {
|
|
9
|
+
constructor(manager, intervalMs = Number(process.env.CODEFLOW_WATCH_INTERVAL_MS) || 6e4) {
|
|
10
|
+
this.manager = manager;
|
|
11
|
+
this.intervalMs = intervalMs;
|
|
12
|
+
}
|
|
13
|
+
timer;
|
|
14
|
+
polling;
|
|
15
|
+
stopped = false;
|
|
16
|
+
/** Watches whose polls are failing, by `<run id>/<branch>`; forgotten on restart. */
|
|
17
|
+
failing = /* @__PURE__ */ new Map();
|
|
18
|
+
/** Watches with nothing to do lately, by `<run id>/<branch>`; forgotten on restart. */
|
|
19
|
+
quiet = /* @__PURE__ */ new Map();
|
|
20
|
+
start() {
|
|
21
|
+
if (this.timer) return;
|
|
22
|
+
this.stopped = false;
|
|
23
|
+
this.timer = setInterval(() => void this.poll(), this.intervalMs);
|
|
24
|
+
this.timer.unref?.();
|
|
25
|
+
}
|
|
26
|
+
stop() {
|
|
27
|
+
this.stopped = true;
|
|
28
|
+
if (this.timer) clearInterval(this.timer);
|
|
29
|
+
this.timer = void 0;
|
|
30
|
+
return this.polling;
|
|
31
|
+
}
|
|
32
|
+
/** One pass over every active watch; concurrent calls share the pass in progress. `now` is for tests. */
|
|
33
|
+
poll(now) {
|
|
34
|
+
if (this.stopped) return Promise.resolve();
|
|
35
|
+
this.polling ??= this.pollAll(now).finally(() => this.polling = void 0);
|
|
36
|
+
return this.polling;
|
|
37
|
+
}
|
|
38
|
+
async pollAll(now = Date.now()) {
|
|
39
|
+
for (const run of this.manager.watchedRuns()) {
|
|
40
|
+
for (const watch of Object.values(run.watches)) {
|
|
41
|
+
if (this.stopped) return;
|
|
42
|
+
const key = `${run.id}/${watch.branch}`;
|
|
43
|
+
const failing = this.failing.get(key);
|
|
44
|
+
const retrying = watch.state === "needs-you" && !!failing;
|
|
45
|
+
if (!ACTIVE.has(watch.state) && !retrying || run.status === "running" || run.archived) {
|
|
46
|
+
if (!retrying) this.failing.delete(key);
|
|
47
|
+
this.quiet.delete(key);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (failing && failing.nextAt > now) continue;
|
|
51
|
+
if ((this.quiet.get(key)?.nextAt ?? 0) > now) continue;
|
|
52
|
+
try {
|
|
53
|
+
this.noteQuiet(key, await this.pollOne(run, watch), now);
|
|
54
|
+
this.failing.delete(key);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
this.quiet.delete(key);
|
|
57
|
+
await this.pollFailed(run, watch, key, err, now);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** After a poll: `seen` is what a poll with nothing to do found (undefined when it found something). */
|
|
63
|
+
noteQuiet(key, seen, now) {
|
|
64
|
+
if (!seen) {
|
|
65
|
+
this.quiet.delete(key);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const prev = this.quiet.get(key);
|
|
69
|
+
const count = prev?.seen === seen ? prev.count + 1 : 1;
|
|
70
|
+
const wait = Math.min(MAX_QUIET_MS, this.intervalMs * 2 ** Math.min(count - 1, 20));
|
|
71
|
+
this.quiet.set(key, { count, seen, nextAt: wait > this.intervalMs ? now + wait - this.intervalMs / 2 : 0 });
|
|
72
|
+
}
|
|
73
|
+
/** Count a failed poll and wait longer before the next; the third in a row hands the PR to you. */
|
|
74
|
+
async pollFailed(run, watch, key, err, now) {
|
|
75
|
+
const count = (this.failing.get(key)?.count ?? 0) + 1;
|
|
76
|
+
let error = err.message;
|
|
77
|
+
if (count === 1 || GH_AUTH_ERROR.test(error)) {
|
|
78
|
+
const auth = await ghAuthStatus(run.repo).catch(() => void 0);
|
|
79
|
+
if (auth && !auth.ok) error = `gh isn't signed in \u2014 run \`gh auth login\`${auth.message ? ` (gh auth status: ${auth.message.slice(0, 200)})` : ""}`;
|
|
80
|
+
}
|
|
81
|
+
const wait = Math.min(MAX_BACKOFF_MS, this.intervalMs * 2 ** (count - 1));
|
|
82
|
+
this.failing.set(key, { count, nextAt: now + wait, error });
|
|
83
|
+
const detail = `Couldn't check the PR ${count} time${count === 1 ? "" : "s"} in a row: ${error}`;
|
|
84
|
+
if (count >= FAILURES_BEFORE_NEEDS_YOU) {
|
|
85
|
+
if (count === FAILURES_BEFORE_NEEDS_YOU) logWarn(`PR watch on ${watch.pr} keeps failing`, { run: run.id, error });
|
|
86
|
+
this.manager.updateWatch(run.id, watch.branch, { state: "needs-you", detail, lastPolled: now });
|
|
87
|
+
} else {
|
|
88
|
+
this.manager.updateWatch(run.id, watch.branch, { detail, lastPolled: now });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** One poll. Returns what it found when there was nothing to do (green, no new comments), else undefined. */
|
|
92
|
+
async pollOne(run, w) {
|
|
93
|
+
const update = (patch) => {
|
|
94
|
+
this.manager.updateWatch(run.id, w.branch, { lastPolled: Date.now(), ...patch });
|
|
95
|
+
return void 0;
|
|
96
|
+
};
|
|
97
|
+
const status = await prStatus(run.repo, w.pr);
|
|
98
|
+
if (status.state !== "OPEN") {
|
|
99
|
+
return update({ state: "closed", detail: status.state === "MERGED" ? "The pull request was merged" : "The pull request was closed" });
|
|
100
|
+
}
|
|
101
|
+
if (status.commentsTruncated) return update({ state: "needs-you", detail: "The PR has at least 1,000 inline review comments; review its complete discussion before restarting follow-through" });
|
|
102
|
+
if (w.state === "fixing" || run.status !== "done") {
|
|
103
|
+
return update({ state: "needs-you", detail: `The run is ${run.status}; resume it, then watch again` });
|
|
104
|
+
}
|
|
105
|
+
const worktree = run.worktrees.find((x) => x.branch === w.branch);
|
|
106
|
+
if (!worktree) return update({ state: "needs-you", detail: "The branch's worktree is gone" });
|
|
107
|
+
const seen = new Set(w.seenComments ?? status.comments.filter((c) => Date.parse(c.createdAt) < w.startedAt).map((c) => c.id));
|
|
108
|
+
if (!w.seenComments) {
|
|
109
|
+
this.manager.log(run.id, `Watching ${w.pr}: line comments on the diff (review threads) aren't read yet; only PR comments and review summaries reach the agent`);
|
|
110
|
+
}
|
|
111
|
+
const fresh = status.comments.filter((c) => !seen.has(c.id));
|
|
112
|
+
const seenComments = [...seen, ...fresh.map((c) => c.id)];
|
|
113
|
+
const me = await githubUser(run.repo).catch(() => "");
|
|
114
|
+
const allowed = new Set([me, ...w.allow ?? []].filter(Boolean).map((login) => login.toLowerCase()));
|
|
115
|
+
const actionable = fresh.filter((c) => !c.body.includes(CODEFLOW_MARKER) && allowed.has(c.author.toLowerCase()));
|
|
116
|
+
for (const c of fresh) {
|
|
117
|
+
if (!c.body.includes(CODEFLOW_MARKER) && !actionable.includes(c)) {
|
|
118
|
+
this.manager.log(
|
|
119
|
+
run.id,
|
|
120
|
+
`Ignored a PR comment from @${c.author} (${c.association || "no association"}): only you${w.allow?.length ? ` and ${w.allow.map((a) => `@${a}`).join(", ")}` : ""} can direct agents on this watch`,
|
|
121
|
+
"warn"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const synced = (await git(["merge-base", "--is-ancestor", status.headRefOid, w.branch], run.repo)).code === 0;
|
|
126
|
+
if (actionable.length) {
|
|
127
|
+
if (!synced) return update({ seenComments, state: "needs-you", detail: "The PR has commits code-flow doesn't have" });
|
|
128
|
+
if (w.fixes >= w.maxFixes) return update({ seenComments, state: "needs-you", detail: `New comments after ${w.fixes} fixes; over to you` });
|
|
129
|
+
const who = [...new Set(actionable.map((c) => `@${c.author}`))].join(", ");
|
|
130
|
+
update({ seenComments, state: "fixing", fixes: w.fixes + 1, detail: `Addressing ${actionable.length} new comment${actionable.length === 1 ? "" : "s"} from ${who}` });
|
|
131
|
+
await this.manager.followUp(run.id, {
|
|
132
|
+
kind: "comments",
|
|
133
|
+
worktree: worktree.name,
|
|
134
|
+
agent: w.agent,
|
|
135
|
+
from: `${who} on the pull request`,
|
|
136
|
+
message: actionable.map((c) => {
|
|
137
|
+
const location = c.path ? `File: ${c.path}${c.line ? `:${c.line}` : ""}${c.side ? ` (${c.side.toLowerCase()} side)` : ""}${c.outdated ? " [outdated diff]" : ""}` : "";
|
|
138
|
+
return `@${c.author}:
|
|
139
|
+
${safeFence([location, c.threadId ? `Thread: ${c.threadId}` : "", c.body.trim()].filter(Boolean).join("\n\n"))}`;
|
|
140
|
+
}).join("\n\n"),
|
|
141
|
+
push: w.remoteBranch,
|
|
142
|
+
watch: w.branch,
|
|
143
|
+
trust: unattendedTrust(run)
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const checks = await prChecks(run.repo, w.pr);
|
|
148
|
+
const failing = checks.filter((c) => c.bucket === "fail");
|
|
149
|
+
const counts = {
|
|
150
|
+
pass: checks.filter((c) => c.bucket === "pass" || c.bucket === "skipping").length,
|
|
151
|
+
fail: failing.length,
|
|
152
|
+
pending: checks.filter((c) => c.bucket === "pending").length
|
|
153
|
+
};
|
|
154
|
+
if (counts.pending) return update({ seenComments, checks: counts, state: "watching", detail: `CI is running (${counts.pending} pending)` });
|
|
155
|
+
if (!counts.fail) {
|
|
156
|
+
update({ seenComments, checks: counts, state: "green", detail: checks.length ? "All checks pass" : "No CI checks reported" });
|
|
157
|
+
return fresh.length ? void 0 : `${status.headRefOid} ${counts.pass}/${counts.fail}/${counts.pending} ${w.startedAt}`;
|
|
158
|
+
}
|
|
159
|
+
if (w.fixedHeads.includes(status.headRefOid)) {
|
|
160
|
+
return update({ seenComments, checks: counts, state: "needs-you", detail: "CI still fails on a commit code-flow already tried to fix" });
|
|
161
|
+
}
|
|
162
|
+
if (w.fixes >= w.maxFixes) return update({ seenComments, checks: counts, state: "needs-you", detail: `CI still fails after ${w.fixes} fixes; over to you` });
|
|
163
|
+
if (!synced) return update({ seenComments, checks: counts, state: "needs-you", detail: "The PR has commits code-flow doesn't have" });
|
|
164
|
+
const logs = await failedLogs(run.repo, checks, 8e3);
|
|
165
|
+
update({
|
|
166
|
+
seenComments,
|
|
167
|
+
checks: counts,
|
|
168
|
+
state: "fixing",
|
|
169
|
+
fixes: w.fixes + 1,
|
|
170
|
+
fixedHeads: [...w.fixedHeads, status.headRefOid],
|
|
171
|
+
detail: `Fixing ${failing.map((c) => c.name).join(", ")} (fix ${w.fixes + 1} of ${w.maxFixes})`
|
|
172
|
+
});
|
|
173
|
+
await this.manager.followUp(run.id, {
|
|
174
|
+
kind: "ci-fix",
|
|
175
|
+
worktree: worktree.name,
|
|
176
|
+
agent: w.agent,
|
|
177
|
+
message: `Failing checks:
|
|
178
|
+
${failing.map((c) => `- ${c.name}${c.link ? ` (${c.link})` : ""}`).join("\n")}` + (logs ? `
|
|
179
|
+
|
|
180
|
+
Logs of the failed jobs:
|
|
181
|
+
${safeFence(logs)}` : "\n\n(No logs available; reproduce the failure locally.)"),
|
|
182
|
+
push: w.remoteBranch,
|
|
183
|
+
watch: w.branch,
|
|
184
|
+
trust: unattendedTrust(run)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function unattendedTrust(run) {
|
|
189
|
+
return run.trust === "trusted" || run.trust === "full" ? "standard" : run.trust;
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
FAILURES_BEFORE_NEEDS_YOU,
|
|
193
|
+
PrWatcher,
|
|
194
|
+
unattendedTrust
|
|
195
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class WorkQueue {
|
|
2
|
+
constructor(concurrency = 4) {
|
|
3
|
+
this.concurrency = concurrency;
|
|
4
|
+
}
|
|
5
|
+
tails = /* @__PURE__ */ new Map();
|
|
6
|
+
ready = [];
|
|
7
|
+
active = 0;
|
|
8
|
+
pending = 0;
|
|
9
|
+
bytes = 0;
|
|
10
|
+
get stats() {
|
|
11
|
+
return { active: this.active, pending: this.pending, bytes: this.bytes, keys: this.tails.size };
|
|
12
|
+
}
|
|
13
|
+
run(key, operation, bytes = 0) {
|
|
14
|
+
this.pending++;
|
|
15
|
+
this.bytes += bytes;
|
|
16
|
+
const work = (this.tails.get(key) ?? Promise.resolve()).then(async () => {
|
|
17
|
+
await new Promise((resolve) => {
|
|
18
|
+
const grant = () => {
|
|
19
|
+
this.active++;
|
|
20
|
+
resolve();
|
|
21
|
+
};
|
|
22
|
+
if (this.active < this.concurrency) grant();
|
|
23
|
+
else this.ready.push(grant);
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
return await operation();
|
|
27
|
+
} finally {
|
|
28
|
+
this.active--;
|
|
29
|
+
this.pending--;
|
|
30
|
+
this.bytes -= bytes;
|
|
31
|
+
this.ready.shift()?.();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const settled = work.then(() => void 0, () => void 0);
|
|
35
|
+
this.tails.set(key, settled);
|
|
36
|
+
void settled.then(() => {
|
|
37
|
+
if (this.tails.get(key) === settled) this.tails.delete(key);
|
|
38
|
+
});
|
|
39
|
+
return work;
|
|
40
|
+
}
|
|
41
|
+
async flush(key) {
|
|
42
|
+
await Promise.all(key === void 0 ? [...this.tails.values()] : [this.tails.get(key)]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
WorkQueue
|
|
47
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { planProblem } from "./patterns/auto-team-model.js";
|
|
2
|
+
class TeamPlanEditError extends Error {
|
|
3
|
+
constructor(message, status = 400) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
const boundedText = (value, max, empty = false) => typeof value === "string" && value.length <= max && !value.includes("\0") && (empty || !!value.trim());
|
|
10
|
+
const identifiers = /^[a-z][a-z0-9-]{0,39}$/;
|
|
11
|
+
const clamp = (value, fallback, min, max) => typeof value === "number" && Number.isFinite(value) ? Math.max(min, Math.min(max, Math.floor(value))) : fallback;
|
|
12
|
+
function strings(value, count, maxLength, label) {
|
|
13
|
+
if (!Array.isArray(value) || value.length > count || value.some((item) => !boundedText(item, maxLength))) throw new TeamPlanEditError(`${label} must contain at most ${count} bounded, nonempty strings.`);
|
|
14
|
+
if (new Set(value).size !== value.length) throw new TeamPlanEditError(`${label} must not contain duplicates.`);
|
|
15
|
+
return [...value];
|
|
16
|
+
}
|
|
17
|
+
function editableTeamPlan(team) {
|
|
18
|
+
return { revision: team.revision, rationale: team.rationale, contracts: [...team.contracts], tasks: team.tasks.map(({ id, title, instructions, owner, kind, dependsOn, dependencyTypes, requirements, acceptance, paths }) => ({ id, title, instructions, owner, kind, dependsOn: [...dependsOn], ...dependencyTypes ? { dependencyTypes: { ...dependencyTypes } } : {}, requirements: [...requirements], acceptance: [...acceptance], paths: [...paths] })) };
|
|
19
|
+
}
|
|
20
|
+
function teamPlanPolicy(team, input) {
|
|
21
|
+
const coordinator = input.roles.coordinator?.[0];
|
|
22
|
+
if (!coordinator || !team.inventory) throw new TeamPlanEditError("The original provider inventory is unavailable; ask the coordinator for a revised plan instead.");
|
|
23
|
+
return { coordinator, pool: [...input.roles.pool ?? []], inventory: structuredClone(team.inventory), maxMembers: clamp(input.options.maxMembers, 4, 2, 6), maxTasks: clamp(input.options.maxTasks, 12, 1, 24), differentProviderReview: input.options.differentProviderReview === true };
|
|
24
|
+
}
|
|
25
|
+
function validateTeamPlanEdit(value, team, policy) {
|
|
26
|
+
if (team.status !== "awaiting_approval" || team.tasks.some((task) => task.status && task.status !== "pending" || (task.attempts ?? 0) > 0 || task.startedAt !== void 0 || task.finishedAt !== void 0 || task.head || task.artifacts?.length)) throw new TeamPlanEditError("This team has already started work. Edit a new pending plan instead.", 409);
|
|
27
|
+
if (!object(value) || Object.keys(value).some((key) => !["revision", "tasks", "contracts", "rationale"].includes(key))) throw new TeamPlanEditError("Only tasks, contracts and rationale may be edited. Members and requirements stay fixed.");
|
|
28
|
+
if (!Number.isSafeInteger(value.revision) || value.revision !== team.revision) throw new TeamPlanEditError("The plan changed while you were editing. Refresh the plan and merge your edits before saving.", 409);
|
|
29
|
+
if (!Array.isArray(value.tasks) || value.tasks.length < 1 || value.tasks.length > policy.maxTasks) throw new TeamPlanEditError(`Keep between 1 and ${policy.maxTasks} tasks.`);
|
|
30
|
+
const tasks = value.tasks.map((item, index) => {
|
|
31
|
+
if (!object(item) || Object.keys(item).some((key) => !["id", "title", "instructions", "owner", "kind", "dependsOn", "dependencyTypes", "requirements", "acceptance", "paths"].includes(key))) throw new TeamPlanEditError(`Task ${index + 1} contains unsupported or runtime fields.`);
|
|
32
|
+
if (typeof item.id !== "string" || !identifiers.test(item.id) || typeof item.owner !== "string" || !identifiers.test(item.owner) || !boundedText(item.title, 180) || !boundedText(item.instructions, 16e3) || !["implementation", "investigation"].includes(String(item.kind))) throw new TeamPlanEditError(`Task ${index + 1} needs a valid ID, owner, title, kind and instructions.`);
|
|
33
|
+
const dependsOn = strings(item.dependsOn, policy.maxTasks, 40, `${item.id} dependencies`);
|
|
34
|
+
let dependencyTypes;
|
|
35
|
+
if (item.dependencyTypes !== void 0) {
|
|
36
|
+
if (!object(item.dependencyTypes) || Object.keys(item.dependencyTypes).length > policy.maxTasks || Object.entries(item.dependencyTypes).some(([id, kind]) => !dependsOn.includes(id) || !["completion", "artifact", "knowledge"].includes(String(kind)))) throw new TeamPlanEditError(`${item.id} contains invalid dependency types.`);
|
|
37
|
+
dependencyTypes = { ...item.dependencyTypes };
|
|
38
|
+
}
|
|
39
|
+
return { id: item.id, title: item.title, instructions: item.instructions, owner: item.owner, kind: item.kind, dependsOn, ...dependencyTypes ? { dependencyTypes } : {}, requirements: strings(item.requirements, 48, 40, `${item.id} requirements`), acceptance: strings(item.acceptance, 16, 2e3, `${item.id} acceptance criteria`), paths: strings(item.paths, 64, 500, `${item.id} paths`), status: "pending", attempts: 0 };
|
|
40
|
+
});
|
|
41
|
+
if (!boundedText(value.rationale, 4e3, true)) throw new TeamPlanEditError("Rationale must be text of at most 4,000 characters.");
|
|
42
|
+
const plan = { requirements: structuredClone(team.requirements), members: structuredClone(team.members), tasks, contracts: strings(value.contracts, 24, 8e3, "Shared contracts"), rationale: value.rationale };
|
|
43
|
+
if (team.templateUses?.length) {
|
|
44
|
+
for (const original of team.tasks.filter((task) => team.templateUses.some((use) => task.id.startsWith(`${use.id}-`)))) {
|
|
45
|
+
const edited = tasks.find((task) => task.id === original.id);
|
|
46
|
+
if (!edited || edited.kind !== original.kind || !original.acceptance.every((item) => edited.acceptance.includes(item)) || !original.dependsOn.every((item) => edited.dependsOn.includes(item)) || !original.requirements.every((item) => edited.requirements.includes(item))) throw new TeamPlanEditError("Template tasks, dependencies and required checks must be preserved. Create a new template version to change its guarantees.");
|
|
47
|
+
if (edited.instructions !== original.instructions && !edited.instructions.startsWith(original.instructions + "\n")) throw new TeamPlanEditError("Template instructions can only be extended. Publish a new template version to replace them.");
|
|
48
|
+
if (edited.paths.some((path) => !original.paths.some((base) => base === "*" || path === base || path.startsWith(base + "/")))) throw new TeamPlanEditError("Template file ownership cannot be broadened");
|
|
49
|
+
if (original.dependsOn.some((id) => (edited.dependencyTypes?.[id] ?? "completion") !== (original.dependencyTypes?.[id] ?? "completion"))) throw new TeamPlanEditError("Preserve template dependency types");
|
|
50
|
+
}
|
|
51
|
+
if (!team.contracts.every((contract) => plan.contracts.includes(contract))) throw new TeamPlanEditError("Preserve template contracts");
|
|
52
|
+
plan.templateUses = structuredClone(team.templateUses);
|
|
53
|
+
}
|
|
54
|
+
const problem = planProblem(plan, policy);
|
|
55
|
+
if (problem) throw new TeamPlanEditError(problem);
|
|
56
|
+
return plan;
|
|
57
|
+
}
|
|
58
|
+
function summarizeTeamPlan(team) {
|
|
59
|
+
return `${team.rationale}
|
|
60
|
+
|
|
61
|
+
Team:
|
|
62
|
+
${team.members.map((member) => `- ${member.name} (${member.id}, ${member.role}) \xB7 ${member.provider}`).join("\n")}
|
|
63
|
+
|
|
64
|
+
Requirements:
|
|
65
|
+
${team.requirements.map((requirement) => `- ${requirement.id}: ${requirement.description}`).join("\n")}
|
|
66
|
+
|
|
67
|
+
Tasks:
|
|
68
|
+
${team.tasks.map((task) => `- ${task.id}: ${task.title} \u2192 ${task.owner}; depends on ${task.dependsOn.join(", ") || "nothing"}; paths ${task.paths.join(", ") || "read only"}
|
|
69
|
+
${task.instructions}
|
|
70
|
+
Acceptance: ${task.acceptance.join("; ")}`).join("\n")}
|
|
71
|
+
|
|
72
|
+
Shared contracts:
|
|
73
|
+
${team.contracts.join("\n") || "None"}
|
|
74
|
+
|
|
75
|
+
Limits: ${team.invocationLimit} calls; ${team.coordinationLimit} coordinator calls; ${team.maxWaves} task attempts/waves; ${team.maxRepairs} repairs.`;
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
TeamPlanEditError,
|
|
79
|
+
editableTeamPlan,
|
|
80
|
+
summarizeTeamPlan,
|
|
81
|
+
teamPlanPolicy,
|
|
82
|
+
validateTeamPlanEdit
|
|
83
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { isAgentId } from "./adapters/index.js";
|
|
2
|
+
import { open, readFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
|
+
import { assertWorktree, repoRoot, resolveCommit, git, worktreeGit, WorktreeMismatchError } from "./git.js";
|
|
5
|
+
import { readJson, HttpError } from "./http.js";
|
|
6
|
+
import { inspectReadiness, inTemporaryWorktree } from "./readiness.js";
|
|
7
|
+
import { applyCheckpoint, checkpoints, readEditableFile, saveEditableFile, searchCode, WorkspaceConflictError } from "./workspace.js";
|
|
8
|
+
import { detectVerificationProfile, renderVerificationReport, runVerification, validateVerificationProfile } from "./verification.js";
|
|
9
|
+
import { runCheck } from "./checks.js";
|
|
10
|
+
import { redactExport } from "./privacy.js";
|
|
11
|
+
import { CODEFLOW_HOME } from "./config.js";
|
|
12
|
+
import { writeHealth } from "./store.js";
|
|
13
|
+
import { mergeFileDraft, checkpointHunks, applyCheckpointHunks } from "./workspace-changes.js";
|
|
14
|
+
import { verificationIntegrity, VerificationIntegrityError } from "./verification-integrity.js";
|
|
15
|
+
import { inspectContext, ContextInspectionError } from "./context-inspection.js";
|
|
16
|
+
import { applyCommitRevert, compareCheckpoint, previewCommitRevert } from "./checkpoint-review.js";
|
|
17
|
+
import { searchSemanticSymbols, findCodeReferences } from "./code-intelligence.js";
|
|
18
|
+
import { inspectArchitecture } from "./architecture.js";
|
|
19
|
+
async function ownedCheckout(repo, tree) {
|
|
20
|
+
try {
|
|
21
|
+
return await assertWorktree(repo, tree);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof WorktreeMismatchError) throw new HttpError(409, error.message);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function diagnosticTail(path) {
|
|
28
|
+
const file = await open(path, "r").catch(() => void 0);
|
|
29
|
+
if (!file) return "";
|
|
30
|
+
try {
|
|
31
|
+
const info = await file.stat();
|
|
32
|
+
if (!info.isFile()) return "";
|
|
33
|
+
const bytes = Math.min(info.size, 32e3);
|
|
34
|
+
const buffer = Buffer.alloc(bytes);
|
|
35
|
+
const { bytesRead } = await file.read(buffer, 0, bytes, Math.max(0, info.size - bytes));
|
|
36
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
37
|
+
} finally {
|
|
38
|
+
await file.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function handleWorkspaceApi(req, url, parts, manager, signal) {
|
|
42
|
+
if (!["readiness", "verification", "workspace", "diagnostics", "v1"].includes(parts[1] ?? "")) return void 0;
|
|
43
|
+
const body = req.method === "POST" ? await readJson(req) : {};
|
|
44
|
+
const selected = body.repo ?? url.searchParams.get("repo");
|
|
45
|
+
const absolute = typeof selected === "string" && selected.startsWith("/") ? resolve(selected) : void 0;
|
|
46
|
+
const owned = absolute ? manager.list().flatMap((run) => run.worktrees.map((tree) => ({ run, tree }))).find(({ tree }) => absolute === resolve(tree.path) || absolute.startsWith(resolve(tree.path) + sep)) : void 0;
|
|
47
|
+
const repo = owned?.tree.path ?? (absolute ? await repoRoot(absolute).catch(() => {
|
|
48
|
+
throw new HttpError(400, "Choose an existing Git repository");
|
|
49
|
+
}) : void 0);
|
|
50
|
+
const requireRepo = () => {
|
|
51
|
+
if (!repo) throw new HttpError(400, "Choose a repository");
|
|
52
|
+
return repo;
|
|
53
|
+
};
|
|
54
|
+
const readRepo = () => owned ? worktreeGit(owned.run.repo, owned.tree.path) : Promise.resolve(requireRepo());
|
|
55
|
+
const writeRepo = () => owned ? ownedCheckout(owned.run.repo, owned.tree) : Promise.resolve(requireRepo());
|
|
56
|
+
const boundedCommand = (value) => {
|
|
57
|
+
if (value === void 0) return void 0;
|
|
58
|
+
if (typeof value !== "string" || value.length > 4e3 || value.includes("\0")) throw new HttpError(400, "Commands must be text under 4,000 characters");
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
if (parts[1] === "v1" && parts[2] === "capabilities" && req.method === "GET") return { status: 200, body: { version: 1, features: ["readiness", "verification", "file-editing", "code-search", "dependencies", "checkpoints", "diagnostics", "run-export", "cursor-history", "shared-projects", "ssh-projects", "ssh-workers", "coordinated-changes", "code-hosts", "github-review-threads", "filesystem-policies", "filtered-tasks", "provider-workflow-extensions", "team-collaboration"], authentication: "Bearer CODEFLOW_TOKEN", api: "/api", docs: "docs/api.md" } };
|
|
62
|
+
if (parts[1] === "readiness" && req.method === "POST") {
|
|
63
|
+
const boundedId = (value) => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_:/.-]{0,127}$/.test(value);
|
|
64
|
+
const provider = (value) => boundedId(value) && (value === "auto" || isAgentId(value));
|
|
65
|
+
const providerList = (value) => Array.isArray(value) && value.length <= 128 && value.every(provider);
|
|
66
|
+
if (body.pattern !== void 0 && !boundedId(body.pattern)) throw new HttpError(400, "Invalid workflow pattern");
|
|
67
|
+
if (body.providers !== void 0 && !providerList(body.providers)) throw new HttpError(400, "Invalid readiness providers");
|
|
68
|
+
if (body.roles !== void 0 && (!body.roles || typeof body.roles !== "object" || Array.isArray(body.roles) || Object.keys(body.roles).length > 64 || !Object.entries(body.roles).every(([role, value]) => boundedId(role) && (typeof value === "string" ? provider(value) : providerList(value))))) throw new HttpError(400, "Invalid readiness roles");
|
|
69
|
+
if (body.execute === true) manager.noteActivity();
|
|
70
|
+
return { status: 200, body: await inspectReadiness({ repo: owned?.run.repo ?? requireRepo(), baseRef: await resolveCommit(await readRepo(), typeof body.baseRef === "string" && body.baseRef ? body.baseRef : "HEAD"), setup: boundedCommand(body.setup), check: boundedCommand(body.check), pattern: body.pattern, roles: body.roles, providers: body.providers, execute: body.execute === true, signal }) };
|
|
71
|
+
}
|
|
72
|
+
if (parts[1] === "verification") {
|
|
73
|
+
if (req.method === "GET" && parts[2] === "profile") return { status: 200, body: await detectVerificationProfile(requireRepo(), url.searchParams.get("preset") || "standard") };
|
|
74
|
+
if (req.method === "POST" && !parts[2]) {
|
|
75
|
+
const selectedRepo = requireRepo();
|
|
76
|
+
const profile = body.profile ? validateVerificationProfile(body.profile) : await detectVerificationProfile(selectedRepo, body.preset ?? "standard");
|
|
77
|
+
const originalRepo = owned?.run.repo ?? selectedRepo;
|
|
78
|
+
const at = await readRepo();
|
|
79
|
+
const ref = await resolveCommit(at, typeof body.baseRef === "string" && body.baseRef ? body.baseRef : "HEAD");
|
|
80
|
+
const setup = boundedCommand(body.setup) ?? profile.setup;
|
|
81
|
+
const run = async (path, baseline2) => {
|
|
82
|
+
const integrity = await verificationIntegrity(await assertWorktree(originalRepo, { path }), signal);
|
|
83
|
+
const report = await runVerification(path, profile, { signal, repeats: body.repeats, baseline: baseline2, execute: async (check, options) => {
|
|
84
|
+
await integrity.assert(`before gate ${check.id}`);
|
|
85
|
+
await assertWorktree(originalRepo, { path });
|
|
86
|
+
const result = await runCheck(path, check.command, options);
|
|
87
|
+
await integrity.assert(`gate ${check.id}`);
|
|
88
|
+
return result;
|
|
89
|
+
} });
|
|
90
|
+
await integrity.assert("completion");
|
|
91
|
+
return report;
|
|
92
|
+
};
|
|
93
|
+
manager.noteActivity();
|
|
94
|
+
let baseline;
|
|
95
|
+
const prepare = async (path) => {
|
|
96
|
+
const integrity = await verificationIntegrity(await assertWorktree(originalRepo, { path }), signal);
|
|
97
|
+
if (setup) {
|
|
98
|
+
await assertWorktree(originalRepo, { path });
|
|
99
|
+
const result = await runCheck(path, setup, { timeoutMs: 12e4, signal });
|
|
100
|
+
await integrity.assert("setup");
|
|
101
|
+
if (!result.ok) throw new HttpError(422, `Verification setup failed: ${result.output.slice(-2e3)}`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
if (typeof body.baseline === "string" && body.baseline) {
|
|
106
|
+
const base = await resolveCommit(at, body.baseline);
|
|
107
|
+
baseline = await inTemporaryWorktree(originalRepo, base, async (path) => {
|
|
108
|
+
await prepare(path);
|
|
109
|
+
const report2 = await run(path);
|
|
110
|
+
return Object.fromEntries(report2.checks.filter((check) => check.attempts.length).map((check) => [check.check.id, check.attempts[0]]));
|
|
111
|
+
}, signal);
|
|
112
|
+
}
|
|
113
|
+
const report = await inTemporaryWorktree(originalRepo, ref, async (path) => {
|
|
114
|
+
await prepare(path);
|
|
115
|
+
return run(path, baseline);
|
|
116
|
+
}, signal);
|
|
117
|
+
return { status: 200, body: redactExport({ ...report, repo: selectedRepo, commit: ref, markdown: renderVerificationReport(report) }) };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof VerificationIntegrityError || error instanceof WorktreeMismatchError) throw new HttpError(409, error.message);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (parts[1] === "workspace") {
|
|
125
|
+
const selectedRepo = requireRepo();
|
|
126
|
+
if (parts[2] === "compare-checkpoint" && req.method === "POST") {
|
|
127
|
+
if (typeof body.from !== "string" || typeof body.to !== "string") throw new HttpError(400, "Choose two commits to compare");
|
|
128
|
+
return { status: 200, body: await compareCheckpoint(await readRepo(), body, signal) };
|
|
129
|
+
}
|
|
130
|
+
if (["preview-revert", "apply-revert"].includes(parts[2] ?? "") && req.method === "POST") {
|
|
131
|
+
const run = typeof body.runId === "string" ? manager.get(body.runId) : void 0;
|
|
132
|
+
const tree = run?.worktrees.find((tree2) => tree2.path === selectedRepo);
|
|
133
|
+
if (!run || !tree) throw new HttpError(409, "Choose an owned run worktree before reverting a commit");
|
|
134
|
+
if (typeof body.commit !== "string" || typeof body.expectedHead !== "string") throw new HttpError(400, "Choose a commit and refresh the current HEAD");
|
|
135
|
+
try {
|
|
136
|
+
return { status: 200, body: await manager.withIdleRepository(selectedRepo, async () => {
|
|
137
|
+
const at = await ownedCheckout(run.repo, tree);
|
|
138
|
+
return parts[2] === "preview-revert" ? previewCommitRevert(at, body, signal, run.repo) : applyCommitRevert(at, body, signal, run.repo);
|
|
139
|
+
}, { kind: "checkpoint-revert", owner: run.id }) };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof WorkspaceConflictError) throw new HttpError(409, error.message);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (parts[2] === "merge-file" && req.method === "POST") return { status: 200, body: await mergeFileDraft(await readRepo(), body.path, body.base, body.draft) };
|
|
146
|
+
if (["checkpoint-hunks", "apply-hunks"].includes(parts[2] ?? "") && req.method === "POST") {
|
|
147
|
+
const run = typeof body.runId === "string" ? manager.get(body.runId) : void 0;
|
|
148
|
+
const tree = run?.worktrees.find((tree2) => tree2.path === selectedRepo);
|
|
149
|
+
if (!run || !tree || run.status === "running") throw new HttpError(409, "Choose an idle run worktree before applying checkpoint hunks");
|
|
150
|
+
try {
|
|
151
|
+
return { status: 200, body: await manager.withIdleRepository(selectedRepo, async () => {
|
|
152
|
+
const at = await ownedCheckout(run.repo, tree);
|
|
153
|
+
return parts[2] === "checkpoint-hunks" ? checkpointHunks(at, body) : applyCheckpointHunks(at, body);
|
|
154
|
+
}, { kind: "checkpoint-hunks", owner: run.id }) };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof WorkspaceConflictError) throw new HttpError(409, error.message);
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (parts[2] === "file") {
|
|
161
|
+
const path = typeof body.path === "string" ? body.path : url.searchParams.get("path") ?? "";
|
|
162
|
+
if (req.method === "GET") return { status: 200, body: await readEditableFile(await readRepo(), path) };
|
|
163
|
+
if (req.method === "POST") {
|
|
164
|
+
const busy = manager.list().find((run) => run.status === "running" && (run.repo === selectedRepo || run.worktrees.some((tree) => tree.path === selectedRepo)));
|
|
165
|
+
if (busy) throw new HttpError(409, "Finish or stop the active run before editing its workspace");
|
|
166
|
+
try {
|
|
167
|
+
return { status: 200, body: await manager.withIdleRepository(selectedRepo, async () => saveEditableFile(await writeRepo(), path, body.text, body.expectedHash), { kind: "file-edit", owner: "workspace" }) };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error instanceof WorkspaceConflictError) throw new HttpError(409, error.message);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (parts[2] === "search" && req.method === "GET") return { status: 200, body: url.searchParams.get("mode") === "semantic" ? await searchSemanticSymbols(selectedRepo, url.searchParams.get("q") ?? "", signal) : await searchCode(await readRepo(), url.searchParams.get("q") ?? "", url.searchParams.get("mode") === "symbols" ? "symbols" : "text") };
|
|
175
|
+
if (parts[2] === "references" && req.method === "GET") return { status: 200, body: await findCodeReferences(selectedRepo, { path: url.searchParams.get("path") ?? "", line: Number(url.searchParams.get("line")), column: Number(url.searchParams.get("column")) }, signal) };
|
|
176
|
+
if (parts[2] === "dependencies" && req.method === "GET") return { status: 200, body: await inspectArchitecture(selectedRepo, signal) };
|
|
177
|
+
if (parts[2] === "checkpoints" && req.method === "GET") {
|
|
178
|
+
const at = await readRepo();
|
|
179
|
+
return { status: 200, body: { head: await resolveCommit(at), checkpoints: await checkpoints(at, url.searchParams.get("ref") || "HEAD") } };
|
|
180
|
+
}
|
|
181
|
+
if (parts[2] === "apply-checkpoint" && req.method === "POST") {
|
|
182
|
+
const run = typeof body.runId === "string" ? manager.get(body.runId) : void 0;
|
|
183
|
+
const tree = run?.worktrees.find((tree2) => tree2.path === selectedRepo);
|
|
184
|
+
if (!run || !tree || run.status === "running") throw new HttpError(409, "Choose an idle run worktree before applying checkpoint files");
|
|
185
|
+
if (!Array.isArray(body.paths) || body.paths.some((path) => typeof path !== "string")) throw new HttpError(400, "Choose repository paths");
|
|
186
|
+
return { status: 200, body: await manager.withIdleRepository(selectedRepo, async () => applyCheckpoint(await ownedCheckout(run.repo, tree), { source: String(body.source ?? ""), paths: body.paths, expectedHead: String(body.expectedHead ?? "") }), { kind: "checkpoint-files", owner: run.id }) };
|
|
187
|
+
}
|
|
188
|
+
if (parts[2] === "context" && req.method === "POST") {
|
|
189
|
+
try {
|
|
190
|
+
return { status: 200, body: await inspectContext({ repo: selectedRepo, baseRef: typeof body.baseRef === "string" ? body.baseRef : void 0, paths: body.paths, attachments: body.attachments, signal }) };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof ContextInspectionError) throw new HttpError(error.status, error.message);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (parts[1] === "diagnostics" && req.method === "GET") {
|
|
198
|
+
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
199
|
+
const log = await diagnosticTail(join(CODEFLOW_HOME, "server.log"));
|
|
200
|
+
const runs = manager.list();
|
|
201
|
+
return { status: 200, body: redactExport({ version: 1, application: packageJson.version, node: process.version, platform: process.platform, at: Date.now(), busy: manager.busy, activity: manager.inspectRepositoryActivity(), writes: writeHealth, runs: { total: runs.length, active: runs.filter((run) => run.status === "running").length, interrupted: runs.filter((run) => run.status === "interrupted").length }, recentErrors: log.slice(-32e3), capabilities: { workspace: true, verificationProfiles: true }, git: repo ? await git(["--version"], repo).then((result) => result.stdout.trim()) : void 0 }) };
|
|
202
|
+
}
|
|
203
|
+
throw new HttpError(404, "Unknown workspace operation");
|
|
204
|
+
}
|
|
205
|
+
export {
|
|
206
|
+
handleWorkspaceApi
|
|
207
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { git, resolveCommit, withRepoLock } from "./git.js";
|
|
6
|
+
import { normalizeRepoPath } from "./repo-tree.js";
|
|
7
|
+
import { readEditableFile, WorkspaceConflictError } from "./workspace.js";
|
|
8
|
+
const digest = (text) => createHash("sha256").update(text).digest("hex");
|
|
9
|
+
const MAX_TEXT = 512 * 1024;
|
|
10
|
+
async function mergeFileDraft(repo, path, base, draft) {
|
|
11
|
+
if ([base, draft].some((value) => typeof value !== "string" || Buffer.byteLength(value) > MAX_TEXT || value.includes("\0"))) throw new Error("Merge drafts must be text under 512 KB");
|
|
12
|
+
const current = await readEditableFile(repo, path);
|
|
13
|
+
const folder = await mkdtemp(join(tmpdir(), "codeflow-file-merge-"));
|
|
14
|
+
try {
|
|
15
|
+
await Promise.all([["current", current.text], ["base", base], ["draft", draft]].map(([name, text]) => writeFile(join(folder, name), text, { mode: 384, flag: "wx" })));
|
|
16
|
+
const merged = await git(["merge-file", "--stdout", "--diff3", "-L", "Current file", "-L", "Loaded base", "-L", "Your draft", "current", "base", "draft"], folder, { timeoutMs: 1e4 });
|
|
17
|
+
if (merged.code < 0 || merged.code > 127 || merged.timedOut || merged.aborted || merged.stderr.trim()) throw new Error(merged.stderr.trim() || "Cannot merge the file drafts");
|
|
18
|
+
return { current, text: merged.stdout, conflicts: merged.code, clean: merged.code === 0 };
|
|
19
|
+
} finally {
|
|
20
|
+
await rm(folder, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function checkpointHunks(repo, input) {
|
|
24
|
+
const path = normalizeRepoPath(input.path);
|
|
25
|
+
if (!path || path.split("/").some((part) => part.toLowerCase() === ".git")) throw new Error("Choose a regular repository file outside .git");
|
|
26
|
+
await readEditableFile(repo, path);
|
|
27
|
+
const [head, source] = await Promise.all([resolveCommit(repo), resolveCommit(repo, input.source)]);
|
|
28
|
+
const modes = await Promise.all([head, source].map((ref) => git(["--literal-pathspecs", "ls-tree", "-z", ref, "--", path], repo)));
|
|
29
|
+
if (modes.some((result) => result.code || !/^100(?:644|755) blob /.test(result.stdout))) throw new Error("Hunks support existing regular text files. Apply added, deleted, renamed or binary files as complete files instead.");
|
|
30
|
+
const diff = await git(["--literal-pathspecs", "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--unified=3", head, source, "--", path], repo);
|
|
31
|
+
if (diff.code) throw new Error(diff.stderr.trim() || "Cannot compare the checkpoint file");
|
|
32
|
+
if (diff.stdout.length > 2 * 1024 * 1024 || /^(?:Binary files |GIT binary patch)/m.test(diff.stdout)) throw new Error("The patch is binary or too large for selective hunks");
|
|
33
|
+
const first = diff.stdout.search(/^@@ /m);
|
|
34
|
+
if (first < 0) return { head, source, path, patchHeader: "", hunks: [] };
|
|
35
|
+
const patchHeader = diff.stdout.slice(0, first).replace(/^index .*\n/m, "");
|
|
36
|
+
const chunks = diff.stdout.slice(first).split(/(?=^@@ )/m).filter(Boolean);
|
|
37
|
+
if (chunks.length > 200) throw new Error("This file has too many hunks; apply the complete file instead");
|
|
38
|
+
return { head, source, path, patchHeader, hunks: chunks.map((patch) => ({ id: digest(`${head}\0${source}\0${path}\0${patch}`), header: patch.split("\n")[0], patch })) };
|
|
39
|
+
}
|
|
40
|
+
async function applyCheckpointHunks(repo, input) {
|
|
41
|
+
if (!Array.isArray(input.hunks) || input.hunks.length < 1 || input.hunks.length > 200 || input.hunks.some((id) => typeof id !== "string" || !/^[a-f0-9]{64}$/.test(id)) || new Set(input.hunks).size !== input.hunks.length) throw new Error("Choose 1\u2013200 distinct checkpoint hunks");
|
|
42
|
+
return withRepoLock(typeof repo === "string" ? repo : repo.path, async () => {
|
|
43
|
+
if (await resolveCommit(repo) !== input.expectedHead) throw new WorkspaceConflictError(await resolveCommit(repo));
|
|
44
|
+
const status = await git(["status", "--porcelain"], repo);
|
|
45
|
+
if (status.code || status.stdout.trim()) throw new Error("Commit or save existing changes before applying checkpoint hunks");
|
|
46
|
+
const preview = await checkpointHunks(repo, input);
|
|
47
|
+
const chosen = preview.hunks.filter((hunk) => input.hunks.includes(hunk.id));
|
|
48
|
+
if (chosen.length !== input.hunks.length) throw new Error("The checkpoint hunks changed. Refresh the comparison before applying");
|
|
49
|
+
const patch = preview.patchHeader + chosen.map((hunk) => hunk.patch).join("");
|
|
50
|
+
const checked = await git(["apply", "--check", "--whitespace=nowarn", "-"], repo, { input: patch });
|
|
51
|
+
if (checked.code) throw new Error(checked.stderr.trim() || "The selected hunks cannot be applied safely");
|
|
52
|
+
const applied = await git(["apply", "--whitespace=nowarn", "-"], repo, { input: patch });
|
|
53
|
+
if (applied.code) throw new Error(applied.stderr.trim() || "Cannot apply the selected hunks");
|
|
54
|
+
return { head: preview.head, source: preview.source, path: preview.path, hunks: input.hunks, staged: false };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
applyCheckpointHunks,
|
|
59
|
+
checkpointHunks,
|
|
60
|
+
mergeFileDraft
|
|
61
|
+
};
|