@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/browser-7XI52A6V.js +8 -0
- package/public/assets/changes-EYFPASEI.js +1 -0
- package/public/assets/chunk-7H3ZLTKA.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-LLMQG23J.js +163 -0
- package/public/assets/main-KIRM2OEK.js +1 -0
- package/public/assets/settings-QJZVW22Y.js +9 -0
- package/public/assets/styles-615EBB20.css +32 -0
- package/public/assets/terminal-A2FI2NAX.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +142 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +118 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +77 -0
- package/public/docs/installation.html +74 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +99 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +61 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +379 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +122 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +223 -0
- package/src/adapters/agy.js +297 -0
- package/src/adapters/claude.js +220 -0
- package/src/adapters/codex-app-server.js +518 -0
- package/src/adapters/codex.js +299 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +418 -0
- package/src/adapters/team-tools.js +102 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-container.js +155 -0
- package/src/approvals.js +121 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-handoff.js +55 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/engine.js +4366 -0
- package/src/env.js +44 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +238 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +212 -0
- package/src/patterns/auto-team.js +659 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +19 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +49 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +383 -0
- package/src/preview.js +219 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-model.js +178 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +213 -0
- package/src/project-teams-api.js +36 -0
- package/src/project-teams.js +551 -0
- package/src/projects.js +286 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +732 -0
- package/src/repo-tree.js +106 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +110 -0
- package/src/run-projection.js +75 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +54 -0
- package/src/serve.js +138 -0
- package/src/server.js +2738 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +301 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +116 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/team-evaluation.js +242 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +176 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { cantRunReason, detectCheckCommand, failureExcerpt, failureFingerprint, runCheck } from "./checks.js";
|
|
5
|
+
const KINDS = /* @__PURE__ */ new Set(["build", "typecheck", "lint", "unit", "integration", "browser", "mutation", "visual", "accessibility", "custom"]);
|
|
6
|
+
const DEFAULT_CHECK_TIMEOUT = 5 * 6e4;
|
|
7
|
+
const MAX_CHECK_TIMEOUT = 30 * 6e4;
|
|
8
|
+
const MAX_SUITE_TIMEOUT = 60 * 6e4;
|
|
9
|
+
function validateVerificationProfile(value) {
|
|
10
|
+
if (!value || typeof value !== "object") throw new Error("A verification profile is required");
|
|
11
|
+
const p = value;
|
|
12
|
+
if (typeof p.name !== "string" || !p.name.trim() || p.name.length > 120) throw new Error("Verification profile needs a name of at most 120 characters");
|
|
13
|
+
if (!Array.isArray(p.checks) || p.checks.length > 12) throw new Error("Verification profiles support at most 12 checks");
|
|
14
|
+
const ids = /* @__PURE__ */ new Set();
|
|
15
|
+
const checks = p.checks.map((raw) => {
|
|
16
|
+
if (!raw || typeof raw !== "object") throw new Error("Invalid verification check");
|
|
17
|
+
const c = raw;
|
|
18
|
+
if (typeof c.id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(c.id) || ids.has(c.id)) throw new Error("Verification check IDs must be unique and contain letters, digits, underscores or hyphens");
|
|
19
|
+
ids.add(c.id);
|
|
20
|
+
if (!KINDS.has(c.kind)) throw new Error(`Unknown verification kind for ${c.id}`);
|
|
21
|
+
if (typeof c.command !== "string" || !c.command.trim() || c.command.length > 4e3 || c.command.includes("\0")) throw new Error(`Invalid command for ${c.id}`);
|
|
22
|
+
const timeoutMs = c.timeoutMs ?? DEFAULT_CHECK_TIMEOUT;
|
|
23
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_CHECK_TIMEOUT) throw new Error(`Check timeout must be between 1 ms and ${MAX_CHECK_TIMEOUT} ms`);
|
|
24
|
+
if (c.required !== void 0 && typeof c.required !== "boolean") throw new Error(`Invalid required flag for ${c.id}`);
|
|
25
|
+
return { id: c.id, ...typeof c.title === "string" && c.title.trim() ? { title: c.title.trim().slice(0, 120) } : {}, kind: c.kind, command: c.command.trim(), required: c.required !== false, timeoutMs };
|
|
26
|
+
});
|
|
27
|
+
let requirements;
|
|
28
|
+
if (p.requirements !== void 0) {
|
|
29
|
+
if (!Array.isArray(p.requirements) || p.requirements.length > 48) throw new Error("Verification supports at most 48 requirement mappings");
|
|
30
|
+
const requirementIds = /* @__PURE__ */ new Set();
|
|
31
|
+
requirements = p.requirements.map((raw) => {
|
|
32
|
+
if (!raw || typeof raw !== "object") throw new Error("Invalid requirement mapping");
|
|
33
|
+
const requirement = raw;
|
|
34
|
+
if (typeof requirement.id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(requirement.id) || requirementIds.has(requirement.id) || typeof requirement.description !== "string" || !requirement.description.trim() || requirement.description.length > 4e3 || !Array.isArray(requirement.checks) || requirement.checks.some((id) => typeof id !== "string" || !ids.has(id)) || new Set(requirement.checks).size !== requirement.checks.length) throw new Error("Requirements need unique IDs, descriptions, and references to configured checks");
|
|
35
|
+
requirementIds.add(requirement.id);
|
|
36
|
+
return { id: requirement.id, description: requirement.description.trim(), checks: [...requirement.checks] };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return { name: p.name.trim(), checks, ...requirements ? { requirements } : {}, ...typeof p.source === "string" ? { source: p.source.slice(0, 500) } : {}, ...typeof p.setup === "string" && p.setup.trim() ? { setup: p.setup.trim().slice(0, 4e3) } : {} };
|
|
40
|
+
}
|
|
41
|
+
async function detectVerificationProfile(repo, preset = "standard") {
|
|
42
|
+
if (!["lightweight", "standard", "thorough"].includes(preset)) throw new Error("Unknown verification preset");
|
|
43
|
+
const include = preset === "lightweight" ? ["typecheck", "lint", "unit"] : preset === "standard" ? ["build", "typecheck", "lint", "unit"] : ["build", "typecheck", "lint", "unit", "integration", "browser", "mutation", "visual", "accessibility"];
|
|
44
|
+
const has = (path) => existsSync(join(repo, path));
|
|
45
|
+
const checks = [];
|
|
46
|
+
if (has("package.json")) {
|
|
47
|
+
let pkg;
|
|
48
|
+
try {
|
|
49
|
+
pkg = JSON.parse(await readFile(join(repo, "package.json"), "utf8"));
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error("Cannot detect verification: package.json is not valid JSON");
|
|
52
|
+
}
|
|
53
|
+
const runner = has("pnpm-lock.yaml") ? "pnpm run" : has("yarn.lock") ? "yarn run" : has("bun.lock") || has("bun.lockb") ? "bun run" : "npm run";
|
|
54
|
+
const scripts = [["build", ["build"]], ["typecheck", ["typecheck", "check:types"]], ["lint", ["lint"]], ["unit", ["test:unit", "test"]], ["integration", ["test:integration"]], ["browser", ["test:e2e", "test:browser"]], ["mutation", ["test:mutation", "mutation"]], ["visual", ["test:visual"]], ["accessibility", ["test:a11y", "test:accessibility"]]];
|
|
55
|
+
for (const [kind, candidates] of scripts) {
|
|
56
|
+
if (!include.includes(kind)) continue;
|
|
57
|
+
const script = candidates.find((name) => typeof pkg.scripts?.[name] === "string" && String(pkg.scripts[name]).trim() && !/no test specified/i.test(String(pkg.scripts[name])));
|
|
58
|
+
if (script) checks.push({ id: kind, kind, command: `${runner} ${script}`, required: true, timeoutMs: DEFAULT_CHECK_TIMEOUT });
|
|
59
|
+
}
|
|
60
|
+
if (checks.length) return { name: preset, checks, source: "package.json" };
|
|
61
|
+
}
|
|
62
|
+
const detected = await detectCheckCommand(repo);
|
|
63
|
+
if (detected) checks.push({ id: "unit", kind: "unit", command: detected.command, required: true, timeoutMs: DEFAULT_CHECK_TIMEOUT });
|
|
64
|
+
return { name: preset, checks, source: detected?.source, setup: detected?.setup };
|
|
65
|
+
}
|
|
66
|
+
function diagnoseChecks(attempts, baseline) {
|
|
67
|
+
if (!attempts.length) return { diagnosis: "failed", reason: "No check attempt completed" };
|
|
68
|
+
if (attempts.some((a) => a.ok) && attempts.some((a) => !a.ok)) return { diagnosis: "flaky", reason: "The same command both passed and failed on this revision" };
|
|
69
|
+
if (attempts.every((a) => a.ok)) return { diagnosis: "passed" };
|
|
70
|
+
const reason = attempts.map(cantRunReason).find(Boolean);
|
|
71
|
+
if (reason) return { diagnosis: "environment", reason };
|
|
72
|
+
if (baseline?.ok) return { diagnosis: "regression", reason: "This check passed on the baseline revision" };
|
|
73
|
+
if (baseline && !baseline.ok && attempts.every((a) => failureFingerprint(a) === failureFingerprint(baseline))) return { diagnosis: "pre-existing", reason: "The baseline reports the same failure; this check still requires attention" };
|
|
74
|
+
return { diagnosis: "failed" };
|
|
75
|
+
}
|
|
76
|
+
function resultState(checks, skipped = []) {
|
|
77
|
+
if (skipped.some((c) => c.check.required)) return "blocked";
|
|
78
|
+
const required = checks.filter((c) => c.check.required);
|
|
79
|
+
if (required.some((c) => c.diagnosis === "environment")) return "blocked";
|
|
80
|
+
if (required.some((c) => c.diagnosis !== "passed")) return "needs-changes";
|
|
81
|
+
return required.length ? "verified" : "unchecked";
|
|
82
|
+
}
|
|
83
|
+
async function runVerification(cwd, input, opts = {}) {
|
|
84
|
+
const profile = validateVerificationProfile(input);
|
|
85
|
+
const repeats = opts.repeats ?? 1;
|
|
86
|
+
if (!Number.isInteger(repeats) || repeats < 1 || repeats > 3) throw new Error("Verification repeats must be between 1 and 3");
|
|
87
|
+
const timeoutMs = opts.timeoutMs ?? 15 * 6e4;
|
|
88
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_SUITE_TIMEOUT) throw new Error("Verification deadline must be between 1 ms and one hour");
|
|
89
|
+
const startedAt = Date.now();
|
|
90
|
+
const checks = [];
|
|
91
|
+
const skipped = [];
|
|
92
|
+
for (const check of profile.checks) {
|
|
93
|
+
if (opts.signal?.aborted || Date.now() - startedAt >= timeoutMs) {
|
|
94
|
+
skipped.push({ check, reason: opts.signal?.aborted ? "Verification was cancelled" : "The verification deadline was reached" });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const attempts = [];
|
|
98
|
+
for (let i = 0; i < repeats; i++) {
|
|
99
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
100
|
+
if (opts.signal?.aborted || remaining <= 0) break;
|
|
101
|
+
const execution = { timeoutMs: Math.min(check.timeoutMs, remaining), signal: opts.signal, container: opts.container };
|
|
102
|
+
attempts.push(await (opts.execute ? opts.execute(check, execution) : runCheck(cwd, check.command, execution)));
|
|
103
|
+
}
|
|
104
|
+
const baseline = opts.baseline?.[check.id];
|
|
105
|
+
const diagnosis = diagnoseChecks(attempts, baseline);
|
|
106
|
+
if (["unit", "integration", "browser"].includes(check.kind) && attempts.some((attempt) => attempt.tests === 0)) {
|
|
107
|
+
diagnosis.diagnosis = "failed";
|
|
108
|
+
diagnosis.reason = "The test runner reported zero executed tests";
|
|
109
|
+
}
|
|
110
|
+
checks.push({ check, attempts, baseline, ...diagnosis });
|
|
111
|
+
if (attempts.length < repeats || opts.signal?.aborted) skipped.push({ check, reason: opts.signal?.aborted ? "Verification was cancelled" : "Repeated verification did not complete before the deadline" });
|
|
112
|
+
}
|
|
113
|
+
const requirements = (opts.requirements ?? profile.requirements)?.map((r) => {
|
|
114
|
+
const missing = r.checks.filter((id) => !checks.some((c) => c.check.id === id && c.diagnosis === "passed"));
|
|
115
|
+
return { id: r.id, description: r.description, covered: r.checks.length > 0 && !missing.length, missing };
|
|
116
|
+
});
|
|
117
|
+
let state = resultState(checks, skipped);
|
|
118
|
+
if (state === "verified" && requirements?.some((r) => !r.covered)) state = "needs-changes";
|
|
119
|
+
return {
|
|
120
|
+
version: 1,
|
|
121
|
+
profile: profile.name,
|
|
122
|
+
state,
|
|
123
|
+
startedAt,
|
|
124
|
+
endedAt: Date.now(),
|
|
125
|
+
checks,
|
|
126
|
+
skipped,
|
|
127
|
+
requirements,
|
|
128
|
+
...opts.changedFiles ? { risk: assessChangeRisk(opts.changedFiles), review: reviewCoverage(opts.changedFiles.map((f) => f.path), opts.reviewedFiles ?? []) } : {}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function assessChangeRisk(files) {
|
|
132
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
133
|
+
const sensitiveFiles = [];
|
|
134
|
+
for (const f of files) {
|
|
135
|
+
const path = f.path.replaceAll("\\", "/");
|
|
136
|
+
let sensitive = false;
|
|
137
|
+
for (const [pattern, reason] of [
|
|
138
|
+
[/(^|\/)(migrations?|schema)(\/|\.)|\.sql$/i, "Database or schema changes require migration review"],
|
|
139
|
+
[/(^|[\/_-])(auth|permissions?|security|secrets?|credentials?|crypto)([\/_.-]|$)/i, "Authentication, permissions or sensitive data handling changed"],
|
|
140
|
+
[/(^|\/)(\.github\/workflows|Dockerfile|\.env)|(^|\/)(deploy|infra|terraform)(\/|\.)/i, "Execution or deployment configuration changed"]
|
|
141
|
+
]) if (pattern.test(path)) {
|
|
142
|
+
reasons.add(reason);
|
|
143
|
+
sensitive = true;
|
|
144
|
+
}
|
|
145
|
+
if (sensitive) sensitiveFiles.push(f.path);
|
|
146
|
+
}
|
|
147
|
+
const lines = files.reduce((n, f) => n + (f.additions ?? 0) + (f.deletions ?? 0), 0);
|
|
148
|
+
if (files.length > 20 || lines > 1e3) reasons.add("Large change surface needs broader verification");
|
|
149
|
+
return { level: sensitiveFiles.length ? "high" : reasons.size || files.length > 5 ? "medium" : "low", reasons: [...reasons], sensitiveFiles };
|
|
150
|
+
}
|
|
151
|
+
function reviewCoverage(changed, reviewed) {
|
|
152
|
+
const normalize = (p) => p.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
153
|
+
const files = [...new Set(changed.map(normalize))];
|
|
154
|
+
const seen = new Set(reviewed.map(normalize));
|
|
155
|
+
const unreviewed = files.filter((f) => !seen.has(f));
|
|
156
|
+
return { changed: files.length, reviewed: files.length - unreviewed.length, percent: files.length ? Math.round(100 * (files.length - unreviewed.length) / files.length) : 100, unreviewed };
|
|
157
|
+
}
|
|
158
|
+
function renderVerificationReport(report) {
|
|
159
|
+
const cell = (s) => s.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
160
|
+
const parts = [`# Verification: ${report.state}`, "", `Profile: ${cell(report.profile)} \xB7 Duration: ${((report.endedAt - report.startedAt) / 1e3).toFixed(1)}s`, "", "| Check | Required | Result | Attempts |", "| --- | --- | --- | --- |"];
|
|
161
|
+
for (const c of report.checks) parts.push(`| ${cell(c.check.title ?? c.check.id)} | ${c.check.required ? "yes" : "no"} | ${c.diagnosis} | ${c.attempts.length} |`);
|
|
162
|
+
for (const s of report.skipped) parts.push(`| ${cell(s.check.id)} | ${s.check.required ? "yes" : "no"} | skipped: ${cell(s.reason)} | 0 |`);
|
|
163
|
+
for (const c of report.checks.filter((c2) => c2.diagnosis !== "passed")) {
|
|
164
|
+
const output = failureExcerpt(c.attempts.find((a) => !a.ok)?.output ?? c.reason ?? "No output");
|
|
165
|
+
const fence = "`".repeat(Math.max(3, ...[...output.matchAll(/`+/g)].map((m) => m[0].length + 1)));
|
|
166
|
+
parts.push("", `## ${cell(c.check.id)}: ${c.diagnosis}`, "", c.reason ?? "The check did not pass.", "", fence, output, fence);
|
|
167
|
+
}
|
|
168
|
+
if (report.risk) parts.push("", `Change risk: ${report.risk.level}`, ...report.risk.reasons.map((r) => `- ${r}`));
|
|
169
|
+
if (report.review) parts.push("", `Review coverage: ${report.review.reviewed}/${report.review.changed} files (${report.review.percent}%)`, ...report.review.unreviewed.map((p) => `- Unreviewed: ${p}`));
|
|
170
|
+
if (report.requirements) parts.push("", "Requirements:", ...report.requirements.map((r) => `- ${r.covered ? "[x]" : "[ ]"} ${r.id}: ${r.description}${r.missing.length ? ` (missing: ${r.missing.join(", ")})` : ""}`));
|
|
171
|
+
return `${parts.join("\n")}
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
export {
|
|
175
|
+
assessChangeRisk,
|
|
176
|
+
detectVerificationProfile,
|
|
177
|
+
diagnoseChecks,
|
|
178
|
+
renderVerificationReport,
|
|
179
|
+
resultState,
|
|
180
|
+
reviewCoverage,
|
|
181
|
+
runVerification,
|
|
182
|
+
validateVerificationProfile
|
|
183
|
+
};
|
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
|
+
};
|