@codions/portta 0.0.0-develop.147.sha-1859ad1
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/dist/assets/skills/portta-workflows/SKILL.md +280 -0
- package/dist/assets/workflows/bake-off.workflow.js +171 -0
- package/dist/assets/workflows/code-review.workflow.js +179 -0
- package/dist/assets/workflows/deep-research.workflow.js +222 -0
- package/dist/assets/workflows/multi-provider-review.workflow.js +168 -0
- package/dist/assets/workflows/provider-debate.workflow.js +140 -0
- package/dist/assets/workflows/second-opinion.workflow.js +175 -0
- package/dist/cli.js +129793 -0
- package/dist/documentation.json +1 -0
- package/dist/host.js +73741 -0
- package/dist/runtime/.env.example +460 -0
- package/dist/runtime/VERSION +1 -0
- package/dist/runtime/config/traefik/dynamic/auth.example.yaml.disabled +24 -0
- package/dist/runtime/config/traefik/dynamic/middlewares.yaml +35 -0
- package/dist/runtime/config/traefik/dynamic/tcp.yaml +23 -0
- package/dist/runtime/docker/compose/attach/host.yaml +24 -0
- package/dist/runtime/docker/compose/attach/tailscale.yaml +70 -0
- package/dist/runtime/docker/compose/compose.yaml +214 -0
- package/dist/runtime/docker/compose/features/auth-build.yaml +19 -0
- package/dist/runtime/docker/compose/features/auth-dev.yaml +20 -0
- package/dist/runtime/docker/compose/features/cloudflare-tunnel.yaml +69 -0
- package/dist/runtime/docker/compose/features/dashboard-tailscale.yaml +21 -0
- package/dist/runtime/docker/compose/features/dashboard.yaml +21 -0
- package/dist/runtime/docker/compose/features/db.yaml +63 -0
- package/dist/runtime/docker/compose/features/panel-domain.yaml +42 -0
- package/dist/runtime/docker/compose/features/panel-public.yaml +47 -0
- package/dist/runtime/docker/compose/features/panel-webhook.yaml +46 -0
- package/dist/runtime/docker/compose/features/taskflow.yaml +38 -0
- package/dist/runtime/docker/compose/features/tcp-tailscale.yaml +30 -0
- package/dist/runtime/docker/compose/features/tcp.yaml +40 -0
- package/dist/runtime/docker/compose/features/web-bind.yaml +19 -0
- package/dist/runtime/docker/compose/features/web-build.yaml +21 -0
- package/dist/runtime/docker/compose/features/web-dev.yaml +54 -0
- package/dist/runtime/docker/compose/features/web-vpn.yaml +24 -0
- package/dist/runtime/docker/compose/features/web.yaml +283 -0
- package/dist/runtime/docker/compose/profiles/local-tls.yaml +23 -0
- package/dist/runtime/docker/compose/profiles/local.yaml +15 -0
- package/dist/runtime/docker/compose/profiles/public.yaml +19 -0
- package/dist/runtime/docker/compose/profiles/remote-tls-dns.yaml +26 -0
- package/dist/runtime/docker/compose/profiles/remote-tls-http.yaml +32 -0
- package/dist/runtime/docker/compose/profiles/remote-tls.yaml +31 -0
- package/dist/runtime/docker/compose/profiles/remote.yaml +23 -0
- package/dist/runtime/docker/images/README.md +12 -0
- package/dist/runtime/docker/images/apply/Dockerfile +45 -0
- package/dist/runtime/docker/images/sandbox/Dockerfile +79 -0
- package/dist/runtime/docker/images/sandbox/README.md +109 -0
- package/dist/runtime/docker/images/sandbox/VERSION +1 -0
- package/dist/runtime/docker/images/sandbox/agents/claude-code.sh +18 -0
- package/dist/runtime/docker/images/sandbox/agents/codex.sh +4 -0
- package/dist/runtime/docker/images/sandbox/agents/opencode.sh +4 -0
- package/dist/runtime/docker/images/sandbox/agents/pi.sh +4 -0
- package/dist/runtime/docker/images/sandbox/scripts/entrypoint.sh +8 -0
- package/dist/runtime/docker/images/sandbox/scripts/generate-manifest.mjs +81 -0
- package/dist/runtime/docker/images/sandbox/scripts/install-tools.sh +46 -0
- package/dist/runtime/docker/images/sandbox/scripts/verify-image.sh +18 -0
- package/dist/runtime/docker/images/toolbox/Dockerfile +38 -0
- package/dist/runtime/scripts/lib/runner-exec.sh +189 -0
- package/dist/runtime/templates/README.md +43 -0
- package/dist/runtime/templates/overlays/01-single-web.yaml +23 -0
- package/dist/runtime/templates/overlays/02-web-api.yaml +30 -0
- package/dist/runtime/templates/overlays/03-web-api-postgres.yaml +33 -0
- package/dist/runtime/templates/overlays/04-web-api-postgres-redis.yaml +30 -0
- package/dist/runtime/templates/overlays/05-multiple-apis.yaml +37 -0
- package/dist/runtime/templates/overlays/06-monorepo.yaml +63 -0
- package/dist/runtime/templates/overlays/07-worktree.env +47 -0
- package/dist/runtime/templates/overlays/08-nonstandard-port.yaml +41 -0
- package/dist/runtime/templates/overlays/09-tcp-routing.yaml +70 -0
- package/dist/runtime/templates/overlays/10-mailpit.yaml +24 -0
- package/dist/runtime/templates/overlays/11-rustfs.yaml +25 -0
- package/dist/runtime/templates/project/PORTTA.md +107 -0
- package/dist/supervisor.js +23928 -0
- package/package.json +63 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: taskflow-workflows
|
|
3
|
+
description: Author and run custom multi-agent workflows with Taskflow — JavaScript files that orchestrate Codex, Claude Code, OpenCode, and Pi agents deterministically via agent()/parallel()/pipeline()/phase(). Use when a task should be decomposed across agents, independently verified, resumed durably, or managed as a reusable project workflow.
|
|
4
|
+
metadata:
|
|
5
|
+
type: reference
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Taskflow workflows
|
|
9
|
+
|
|
10
|
+
Create custom workflows in `<project>/.portta/workflows/`. `portta flow workflows list` shows the project catalog; `portta flow run workflow <workflow-id>` starts a durable Taskflow Run that can be observed in the dashboard and resumed with `portta flow runs resume <run-id>`. Each `agent()` call spawns a real **Codex**, **Claude Code**, **OpenCode**, or **Pi** agent — you pick the provider per call.
|
|
11
|
+
|
|
12
|
+
A workflow structures work across many agents — to be comprehensive (decompose and cover in parallel), to be confident (independent perspectives and adversarial checks before committing), or to take on scale one context can't hold (migrations, audits, broad sweeps). The file is where you encode that structure: what fans out, what verifies, what synthesizes.
|
|
13
|
+
|
|
14
|
+
When you write one, the right move is often **hybrid**: scout first (list the files, find the channels, scope the diff) to discover the work-list, then write a workflow to pipeline over it. You don't need to know the shape before the *task* — only before the *orchestration step*.
|
|
15
|
+
|
|
16
|
+
Common single-phase workflows you can chain across runs:
|
|
17
|
+
- **Understand** — parallel readers over relevant subsystems → structured map
|
|
18
|
+
- **Design** — judge panel of N independent approaches → scored synthesis
|
|
19
|
+
- **Review** — dimensions → find → adversarially verify (example below)
|
|
20
|
+
- **Research** — multi-modal sweep → deep-read → synthesize
|
|
21
|
+
- **Migrate** — discover sites → transform each (workspace isolation) → verify
|
|
22
|
+
|
|
23
|
+
For larger work, run several in sequence — read each result before deciding the next phase. You stay in the loop; each workflow is one well-scoped fan-out.
|
|
24
|
+
|
|
25
|
+
Every script must begin with `export const meta = {...}`:
|
|
26
|
+
```js
|
|
27
|
+
export const meta = {
|
|
28
|
+
name: 'find-flaky-tests',
|
|
29
|
+
description: 'Find flaky tests and propose fixes', // one-line summary
|
|
30
|
+
phases: [ // one entry per phase() call
|
|
31
|
+
{ title: 'Scan', detail: 'grep test logs for retries' },
|
|
32
|
+
{ title: 'Fix', detail: 'one agent per flaky test' },
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
// script body starts here — use agent()/parallel()/pipeline()/phase()/log()
|
|
36
|
+
phase('Scan')
|
|
37
|
+
const flaky = await agent('grep CI logs for retry markers', { schema: FLAKY_SCHEMA })
|
|
38
|
+
// ...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The `meta` object must be a PURE LITERAL — no variables, function calls, spreads, or template interpolation. Required fields: `name`, `description`. Optional fields: `phases`, `defaultProvider` with `defaultModel`, `defaultSandbox`, `whenToUse`, and `workspace`. Use the SAME phase titles in `meta.phases` as in `phase()` calls — titles are matched exactly; a `phase()` call with no matching meta entry just gets its own progress group.
|
|
42
|
+
|
|
43
|
+
Script body hooks:
|
|
44
|
+
- agent(prompt: string, opts?: {provider?: 'codex' | 'claude-code' | 'opencode' | 'pi', model?: string, effort?: string, schema?: object, label?: string, phase?: string, sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access', approval?: 'never' | 'on-request', cwd?: string, instructions?: string, maxTurns?: number, worktree?: boolean | string, key?: string}): Promise<any> — spawn an agent. Without schema, returns its final text as a string. With schema (a JSON Schema), the agent is forced to return JSON matching it and `agent()` returns the validated object — no parsing needed. Returns null if the user skips the agent mid-run (filter with `.filter(Boolean)`). `opts.provider` / `opts.model`: **default to omitting both** — the agent inherits the provider and model the workflow is being run with, which is almost always correct. Only set them when the user explicitly asks for a specific provider/model, or you're highly confident a particular step needs a different one — and then set them **together**: provider and model are both-or-neither. `opts.label` overrides the display label. `opts.phase` assigns one call to a named phase without changing the ambient phase. `opts.sandbox` defaults to `read-only`; use `workspace-write` only when the agent must write. `opts.worktree` requests an isolated child workspace: in a durable Taskflow Run this is a Taskflow-managed fork lease; in a standalone workflow run it is an engine-managed git worktree. Use it only when parallel agents mutate files and would otherwise conflict. `opts.key` is a stable resume pin that survives prompt-wording/reordering edits.
|
|
45
|
+
- pipeline(items, stage1, stage2, ...): Promise<any[]> — run each item through all stages independently, NO barrier between stages. Item A can be in stage 3 while item B is still in stage 1. This is the DEFAULT for multi-stage work. Wall-clock = slowest single-item chain, not sum-of-slowest-per-stage. Every stage callback receives `(prevResult, originalItem, index)` — use originalItem/index in later stages to label work without threading context through stage 1's return value. A stage that throws drops that item to `null` and skips its remaining stages.
|
|
46
|
+
- parallel(thunks: Array<() => Promise<any>>): Promise<any[]> — run tasks concurrently. This is a BARRIER: awaits all thunks before returning. A thunk that throws (or whose agent errors) resolves to `null` in the result array — the call itself never rejects, so `.filter(Boolean)` before using the results. Use ONLY when you genuinely need all results together.
|
|
47
|
+
- log(message: string): void — emit a progress message to the user (shown as a narrator line above the progress tree)
|
|
48
|
+
- phase(title: string): void — start a new phase; subsequent `agent()` calls are grouped under this title in the progress display
|
|
49
|
+
- args: any — the workflow input. Durable Taskflow Runs receive it from `--input` / `--input-json`; standalone runs receive it from `--args` / `--args-file`. Use this to parameterize a workflow — e.g. pass a research question, target path, or config object.
|
|
50
|
+
- budget: {total: number|null, spent(): number, remaining(): number} — the run's output-token target, set with `--budget N` for standalone runs. `budget.total` is null if no target was set. `budget.spent()` returns output tokens spent this run. `budget.remaining()` returns `max(0, total - spent())`, or `Infinity` if no target. The target is a HARD ceiling, not advisory: once `spent()` reaches `total`, further `agent()` calls throw. Use for dynamic loops: `while (budget.total && budget.remaining() > 50_000) { ... }`, or static scaling: `const FLEET = budget.total ? Math.floor(budget.total / 100_000) : 5`.
|
|
51
|
+
- now(): number / random(): number — journal-seeded deterministic time/RNG. Use these instead of `Date.now()`/`Math.random()` (which throw — see below).
|
|
52
|
+
|
|
53
|
+
Agents are told their final text IS the return value (not a human-facing message), so they return raw data. For structured output, use the schema option — validation happens at the worker layer and the agent retries once on a mismatch.
|
|
54
|
+
|
|
55
|
+
## Providers
|
|
56
|
+
|
|
57
|
+
Every `agent()` runs under a provider/model. **By default, do NOT set `provider` or `model` per agent** — each agent inherits the provider/model the workflow is being run with, which is almost always what you want. Most workflows (including the canonical example and the patterns above) omit them entirely. Pin them only when the user explicitly asks for a specific provider/model, or you're confident a particular step needs a different one.
|
|
58
|
+
|
|
59
|
+
**Provider and model are both-or-neither** — at every site (per-call opts, `meta.defaultProvider`/`defaultModel`, standalone `--provider`/`--model`): set both, or omit both to inherit the run defaults. A lone `provider:` (or lone `model:`) is rejected before the agent spawns. This exists because a lone provider override could inherit a model belonging to a *different* provider. When you pin a provider, name the model explicitly. `portta flow workflows doctor` shows which providers are installed/authenticated.
|
|
60
|
+
|
|
61
|
+
The four providers:
|
|
62
|
+
- **codex** (OpenAI) — the default. `opts.effort` tunes reasoning depth: `"none" | "minimal" | "low" | "medium" | "high" | "xhigh"`. Requires the `codex` CLI authenticated (ChatGPT login); its built-in tools come from that auth, and it ignores `OPENAI_API_KEY`. Native structured output uses a free-form working turn followed by a schema-constrained extraction turn.
|
|
63
|
+
- **claude-code** (Anthropic) — via the Claude Agent SDK; requires the `claude` CLI / SDK available. `opts.effort` accepts `"low" | "medium" | "high" | "xhigh" | "max"` (`max` is Opus-tier; the model silently downgrades unsupported levels). Structured output is delivered on the final result only (intermediate messages stay free-form).
|
|
64
|
+
- **opencode** (OpenCode CLI ≥ 1.16.2) — spawn-per-call subprocess backend. `model` is an open `provider/model` string routed by OpenCode, required whenever `provider: "opencode"` is set. **Requires `sandbox: "danger-full-access"`** — OpenCode has no enforceable confinement, so the default `read-only` sandbox is rejected. `effort` maps directly to OpenCode's provider-specific `--variant` flag. `maxTurns` is rejected. Structured output uses a silent extraction turn that reuses the working session.
|
|
65
|
+
- **pi** (`@earendil-works/pi-coding-agent` ≥ 0.79.1) — spawn-per-call subprocess backend. `model` is an open Pi model reference. **Requires `sandbox: "danger-full-access"`** — Pi's tool allowlists are not OS confinement, so `read-only`/`workspace-write` are rejected. `effort` maps onto Pi's full thinking range (`none`→off … `max`→xhigh); `maxTurns` is rejected. Structured output uses a silent tool-less extraction turn.
|
|
66
|
+
|
|
67
|
+
(`opts.effort` is a single union — `"none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"` — and each worker maps to its nearest supported level: `minimal`→`low` on Claude, `max`→`xhigh` on Codex/Pi. OpenCode passes the value through as `--variant`.)
|
|
68
|
+
|
|
69
|
+
Codex and Claude Code honor every `sandbox` mode (read-only by default); **OpenCode and Pi are full-access-only** — every call on them needs an explicit `sandbox: "danger-full-access"` (per call or via the standalone `--sandbox`), a deliberate fail-closed choice because neither CLI can honestly enforce confinement. All providers honor `cwd`; `worktree` isolates parallel file edits regardless of provider. Outdated OpenCode/Pi binaries are refused at runtime (`provider_outdated`); `portta flow workflows doctor` flags them up front.
|
|
70
|
+
|
|
71
|
+
The default case — omit provider, so fan-out and synthesis run on the workflow's selected provider:
|
|
72
|
+
```js
|
|
73
|
+
const findings = await parallel(AREAS.map(area => () =>
|
|
74
|
+
agent(`Inspect ${area} and list concrete issues.`, { schema: FINDINGS_SCHEMA })))
|
|
75
|
+
const report = await agent(`Synthesize into a prioritized report:\n${JSON.stringify(findings.filter(Boolean), null, 2)}`)
|
|
76
|
+
return report
|
|
77
|
+
```
|
|
78
|
+
Pin a provider only when you mean to — e.g. a deliberate cross-provider verify pass when the user has asked for that diversity:
|
|
79
|
+
```js
|
|
80
|
+
const verified = await pipeline(
|
|
81
|
+
suspects,
|
|
82
|
+
s => agent(`Is this a real bug? ${s.desc}`, { schema: VERDICT }),
|
|
83
|
+
(v, s) => agent(`Try to REFUTE that ${s.desc} is a bug; default to refuted if unsure.`,
|
|
84
|
+
{ provider: "claude-code", model: "<claude-model>", schema: VERDICT }).then(r => ({ ...s, real: v.real && !r.refuted })))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Scripts are plain JavaScript, NOT TypeScript — type annotations (`: string[]`), interfaces, and generics fail to parse. The script body runs in an async context — use await directly. Standard JS built-ins (JSON, Math, Array, etc.) are available — EXCEPT `Date.now()`/`Math.random()`/argless `new Date()`, which throw (they would break resume) and are rejected by a submit-time lint; use the injected `now()`/`random()` instead, or pass timestamps in via `args`. No filesystem, network, or relative import/require in the workflow body (the agents do the I/O).
|
|
88
|
+
|
|
89
|
+
DEFAULT TO `pipeline()`. Only reach for a barrier (`parallel()` between stages) when you genuinely need ALL prior-stage results together.
|
|
90
|
+
|
|
91
|
+
A barrier is correct ONLY when stage N needs cross-item context from all of stage N-1:
|
|
92
|
+
- Dedup/merge across the full result set before expensive downstream work
|
|
93
|
+
- Early-exit if the total count is zero ("0 bugs found → skip verification entirely")
|
|
94
|
+
- Stage N's prompt references "the other findings" for comparison
|
|
95
|
+
|
|
96
|
+
A barrier is NOT justified by:
|
|
97
|
+
- "I need to flatten/map/filter first" — do it inside a pipeline stage: `pipeline(items, stageA, r => transform([r]).flat(), stageB)`
|
|
98
|
+
- "The stages are conceptually separate" — that's what `pipeline()` models. Separate stages ≠ synchronized stages.
|
|
99
|
+
- "It's cleaner code" — barrier latency is real. If 5 finders run and the slowest takes 3× the fastest, a barrier wastes 2/3 of the fast finders' idle time.
|
|
100
|
+
|
|
101
|
+
Smell test: if you wrote
|
|
102
|
+
```js
|
|
103
|
+
const a = await parallel(...)
|
|
104
|
+
const b = transform(a) // flatten, map, filter — no cross-item dependency
|
|
105
|
+
const c = await parallel(b.map(...))
|
|
106
|
+
```
|
|
107
|
+
that middle transform doesn't need the barrier. Rewrite as a pipeline with the transform inside a stage. When in doubt: pipeline.
|
|
108
|
+
|
|
109
|
+
Concurrent `agent()` calls are capped at 100 per standalone workflow by default (override with `--concurrency N`) — excess calls queue and run as slots free up. You can pass more items than the cap to `parallel()`/`pipeline()` and they all complete; only up to the cap run at any moment. Total agent count across a workflow's lifetime is capped at 1000. A single `parallel()`/`pipeline()` call accepts at most 4096 items; passing more is an explicit error, not a silent truncation.
|
|
110
|
+
|
|
111
|
+
The canonical multi-stage pattern — pipeline by default, each dimension verifies as soon as its review completes:
|
|
112
|
+
```js
|
|
113
|
+
export const meta = {
|
|
114
|
+
name: 'review-changes',
|
|
115
|
+
description: 'Review changed files across dimensions, verify each finding',
|
|
116
|
+
phases: [{ title: 'Review' }, { title: 'Verify' }],
|
|
117
|
+
}
|
|
118
|
+
const DIMENSIONS = [{ key: 'bugs', prompt: '...' }, { key: 'perf', prompt: '...' }]
|
|
119
|
+
const results = await pipeline(
|
|
120
|
+
DIMENSIONS,
|
|
121
|
+
d => agent(d.prompt, { label: `review:${d.key}`, phase: 'Review', schema: FINDINGS_SCHEMA }),
|
|
122
|
+
review => parallel(review.findings.map(f => () =>
|
|
123
|
+
agent(`Adversarially verify: ${f.title}`, { label: `verify:${f.file}`, phase: 'Verify', schema: VERDICT_SCHEMA })
|
|
124
|
+
.then(v => ({ ...f, verdict: v }))
|
|
125
|
+
))
|
|
126
|
+
)
|
|
127
|
+
const confirmed = results.flat().filter(Boolean).filter(f => f.verdict?.isReal)
|
|
128
|
+
return { confirmed }
|
|
129
|
+
// Dimension 'bugs' findings verify while dimension 'perf' is still reviewing. No wasted wall-clock.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
When a barrier IS correct — dedup across all findings before expensive verification:
|
|
133
|
+
```js
|
|
134
|
+
const all = await parallel(DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS_SCHEMA })))
|
|
135
|
+
const deduped = dedupeByFileAndLine(all.filter(Boolean).flatMap(r => r.findings))
|
|
136
|
+
const verified = await parallel(deduped.map(f => () => agent(verifyPrompt(f), { schema: VERDICT_SCHEMA })))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Loop-until-count pattern — accumulate to a target:
|
|
140
|
+
```js
|
|
141
|
+
const bugs = []
|
|
142
|
+
while (bugs.length < 10) {
|
|
143
|
+
const result = await agent("Find bugs in this codebase.", { schema: BUGS_SCHEMA })
|
|
144
|
+
bugs.push(...result.bugs)
|
|
145
|
+
log(`${bugs.length}/10 found`)
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Loop-until-budget pattern — scale depth to `--budget`. Guard on `budget.total`: with no target set, `remaining()` is Infinity and the loop would run straight to the 1000-agent cap.
|
|
150
|
+
```js
|
|
151
|
+
const bugs = []
|
|
152
|
+
while (budget.total && budget.remaining() > 50_000) {
|
|
153
|
+
const result = await agent("Find bugs in this codebase.", { schema: BUGS_SCHEMA })
|
|
154
|
+
bugs.push(...result.bugs)
|
|
155
|
+
log(`${bugs.length} found, ${Math.round(budget.remaining() / 1000)}k remaining`)
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Composing patterns — exhaustive review (find → dedup vs seen → diverse-lens panel → loop-until-dry):
|
|
160
|
+
```js
|
|
161
|
+
const seen = new Set(), confirmed = []
|
|
162
|
+
let dry = 0
|
|
163
|
+
while (dry < 2) {
|
|
164
|
+
const found = (await parallel(FINDERS.map(f => () =>
|
|
165
|
+
agent(f.prompt, { phase: 'Find', schema: BUGS })))).filter(Boolean).flatMap(r => r.bugs)
|
|
166
|
+
const fresh = found.filter(b => !seen.has(key(b)))
|
|
167
|
+
if (!fresh.length) { dry++; continue }
|
|
168
|
+
dry = 0; fresh.forEach(b => seen.add(key(b)))
|
|
169
|
+
const judged = await parallel(fresh.map(b => () =>
|
|
170
|
+
parallel(['correctness', 'security', 'repro'].map(lens => () =>
|
|
171
|
+
agent(`Judge "${b.desc}" via the ${lens} lens — real?`, { phase: 'Verify', schema: VERDICT })))
|
|
172
|
+
.then(vs => ({ b, real: vs.filter(Boolean).filter(v => v.real).length >= 2 }))))
|
|
173
|
+
confirmed.push(...judged.filter(v => v.real).map(v => v.b))
|
|
174
|
+
}
|
|
175
|
+
return confirmed
|
|
176
|
+
// dedup vs `seen`, NOT `confirmed` — else judge-rejected findings reappear every round and it never converges.
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Quality patterns — common shapes; pick by task and compose freely:
|
|
180
|
+
- Adversarial verify: spawn N independent skeptics per finding, each prompted to REFUTE. Kill if ≥majority refute. Prevents plausible-but-wrong findings from surviving.
|
|
181
|
+
```js
|
|
182
|
+
const votes = await parallel(Array.from({ length: 3 }, () => () =>
|
|
183
|
+
agent(`Try to refute: ${claim}. Default to refuted=true if uncertain.`, { schema: VERDICT })))
|
|
184
|
+
const survives = votes.filter(Boolean).filter(v => !v.refuted).length >= 2
|
|
185
|
+
```
|
|
186
|
+
- Perspective-diverse verify: when a finding can fail in more than one way, give each verifier a distinct lens (correctness, security, perf, does-it-reproduce) instead of N identical refuters — diversity catches failure modes redundancy can't.
|
|
187
|
+
- Judge panel: generate N independent attempts from different angles (e.g. MVP-first, risk-first, user-first), score with parallel judges, synthesize from the winner while grafting the best ideas from runners-up. Beats one-attempt-iterated when the solution space is wide.
|
|
188
|
+
- Loop-until-dry: for unknown-size discovery (bugs, issues, edge cases), keep spawning finders until K consecutive rounds return nothing new. Simple counters (`while count < N`) miss the tail.
|
|
189
|
+
- Multi-modal sweep: parallel agents each searching a different way (by-container, by-content, by-entity, by-time). Each is blind to what the others surface; useful when one search angle won't find everything.
|
|
190
|
+
- Completeness critic: a final agent that asks "what's missing — modality not run, claim unverified, source unread?" What it finds becomes the next round of work.
|
|
191
|
+
- No silent caps: if a workflow bounds coverage (top-N, no-retry, sampling), `log()` what was dropped — silent truncation reads as "covered everything" when it didn't.
|
|
192
|
+
|
|
193
|
+
Scale to what the user asked for. "find any bugs" → a few finders, single-vote verify. "thoroughly audit this" or "be comprehensive" → larger finder pool, 3–5 vote adversarial pass, synthesis stage. When unsure, lean toward thoroughness for research/review/audit requests and toward brevity for quick checks.
|
|
194
|
+
|
|
195
|
+
These patterns aren't exhaustive — compose novel harnesses when the task calls for it (tournament brackets, self-repair loops, staged escalation, whatever fits).
|
|
196
|
+
|
|
197
|
+
Use a workflow for multi-step orchestration where control flow should be deterministic (loops, conditionals, fan-out) rather than model-driven.
|
|
198
|
+
|
|
199
|
+
## Workspaces
|
|
200
|
+
|
|
201
|
+
`meta.workspace` declares the Run-level workspace policy:
|
|
202
|
+
```js
|
|
203
|
+
workspace: {
|
|
204
|
+
default: 'isolated_worktree',
|
|
205
|
+
allowed: ['isolated_worktree', 'new_branch', 'current_branch'],
|
|
206
|
+
mutatesRepository: true,
|
|
207
|
+
reason: 'Review agents may prepare fixes',
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Taskflow owns the lifecycle of Run workspaces. Select one at launch with `--workspace worktree|branch|current`; do not create or remove those worktrees manually. Use `agent(..., { worktree: true })` only for child agents that need isolated parallel writes. Taskflow grants each such execution a fork lease and releases the lease after the agent finishes, preserving changed branches for inspection.
|
|
212
|
+
|
|
213
|
+
## Resume
|
|
214
|
+
|
|
215
|
+
Every durable Taskflow Run has a run ID. `portta flow runs resume <run-id>` resumes an interrupted Run from its frozen workflow snapshot and original input. Completed agent calls replay from the durable journal while incomplete or failed calls run again. Stable `opts.key` values keep independently meaningful calls associated with their cached results across safe edits and reorderings.
|
|
216
|
+
|
|
217
|
+
For a standalone workflow run, re-run the same file with `portta flow workflows run <file> --resume <run-id>` — the longest unchanged prefix of agent calls returns cached results instantly; the first edited/new call and everything after it runs live. Same file + same args gives a full cache hit. `Date.now()`/`Math.random()`/`new Date()` are unavailable in scripts because they would break this — use `now()`/`random()`, or pass timestamps via `args`.
|
|
218
|
+
|
|
219
|
+
## CLI commands
|
|
220
|
+
|
|
221
|
+
Durable Taskflow Runs:
|
|
222
|
+
```
|
|
223
|
+
portta flow workflows list
|
|
224
|
+
portta flow run workflow <workflow-id> [--input <text> | --input-json <json>]
|
|
225
|
+
[--profile <name>]
|
|
226
|
+
[--workspace worktree|branch|current]
|
|
227
|
+
[--branch <name>] [--base <branch>]
|
|
228
|
+
portta flow runs list
|
|
229
|
+
portta flow runs show <run-id>
|
|
230
|
+
portta flow runs cancel <run-id>
|
|
231
|
+
portta flow runs resume <run-id>
|
|
232
|
+
portta flow runs transcript <execution-id>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Authoring and standalone validation/execution:
|
|
236
|
+
```
|
|
237
|
+
portta flow workflows run <file.workflow.js | name> [--args '<json>' | --args-file <f>]
|
|
238
|
+
[--provider codex|claude-code|opencode|pi --model m] [--effort e]
|
|
239
|
+
[--sandbox read-only|workspace-write|danger-full-access] [--cwd dir]
|
|
240
|
+
[--concurrency N] [--budget N] [--resume <runId>] [--fake] [--json] [--open]
|
|
241
|
+
portta flow workflows save <file.workflow.js> [--project] [--force]
|
|
242
|
+
portta flow workflows validate <file.workflow.js | name>
|
|
243
|
+
portta flow workflows doctor
|
|
244
|
+
portta flow workflows guide
|
|
245
|
+
portta flow workflows install-skill [--claude] [--agents|--codex]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Use `portta flow workflows validate <file>` before adding or changing a catalog workflow. `--fake` runs with a fake worker for a fast smoke test; `--json` prints `{runId, status, url, result, error}`. A standalone `run` auto-starts its live viewer and prints the URL; `--open` also opens it in a browser.
|
|
249
|
+
|
|
250
|
+
## Saved / named workflows
|
|
251
|
+
|
|
252
|
+
Workflow IDs come from **`meta.name`**, not the filename. The durable Taskflow catalog resolves across three tiers, highest precedence first: **project** (`<project>/.portta/workflows/`), **global** (`~/.portta/state/host/workflows/`), and package **built-ins**. Place new workflows in Taskflow directories.
|
|
253
|
+
|
|
254
|
+
Six built-ins ship with Taskflow. Two are ports of Claude Code's bundled workflows:
|
|
255
|
+
- **`deep-research`** — scope → 5 parallel web searches → fetch/dedup top sources → 3-vote adversarial verification per claim → cited report.
|
|
256
|
+
- **`code-review`** — one finder per review angle, an independent verifier per finding, a gap-sweep at high levels, ranked report.
|
|
257
|
+
|
|
258
|
+
The other four are multi-provider workflows — designs where two models' decorrelated errors are the point:
|
|
259
|
+
- **`multi-provider-review`** — Codex and Claude independently review the same target, then a synthesis agent merges consensus, unique catches, and disagreements.
|
|
260
|
+
- **`bake-off`** — both providers implement the same task in isolated workspaces, blind judges score the diffs, and a tie-break settles splits.
|
|
261
|
+
- **`provider-debate`** — one provider proposes, the other attacks, N rounds of rebuttal, then a judge rules.
|
|
262
|
+
- **`second-opinion`** — both providers answer cheaply; disagreement triggers deeper passes and adjudication.
|
|
263
|
+
|
|
264
|
+
## Running a workflow for a user (from an agent)
|
|
265
|
+
|
|
266
|
+
When you (an agent such as Claude Code or Codex) create or run a workflow on a user's behalf:
|
|
267
|
+
|
|
268
|
+
- Put reusable project workflows in `.portta/workflows/`, validate them, and confirm they appear in `portta flow workflows list` before starting a durable Run.
|
|
269
|
+
- Prefer `portta flow run workflow <workflow-id>` for work the user should observe, cancel, inspect, or resume in Taskflow. Return the Run ID immediately and tell the user it is visible in the Taskflow dashboard; then inspect the final Run/result when it completes.
|
|
270
|
+
- Use `portta flow workflows run <file> --fake --no-serve` for a cheap authoring smoke test. Use a real standalone run only when the user explicitly wants the standalone viewer or CLI-only execution.
|
|
271
|
+
- For a real standalone run, background it in Claude Code or block on it in Codex unless the user specifies otherwise. Surface the printed viewer URL immediately so the user can watch live progress, then report the result when the workflow completes.
|
|
272
|
+
|
|
273
|
+
## Installation
|
|
274
|
+
|
|
275
|
+
Install this repository's skill with:
|
|
276
|
+
```
|
|
277
|
+
npx skills add <owner/repo> --skill taskflow-workflows
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The Skills CLI can install it for Claude Code, Codex, OpenCode, Pi, and other supported agents. `portta flow workflows install-skill` remains available as a compatibility installer for Claude Code and the generic/Codex skill directory.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Taskflow bake-off: both providers implement the same task in isolated fork leases, then
|
|
2
|
+
// blind judges from both providers score the two diffs (neutral A/B labels, shuffled per run, so
|
|
3
|
+
// neither judge knows whose work is whose), a tie-break settles splits, and the closing report
|
|
4
|
+
// says what to graft from the loser. Both worktree branches are preserved for the user.
|
|
5
|
+
// Run with: portta flow workflows run bake-off --args '"<task>"' (or --args '{"task": "..."}')
|
|
6
|
+
|
|
7
|
+
export const meta = {
|
|
8
|
+
name: "bake-off",
|
|
9
|
+
description:
|
|
10
|
+
"Implementation bake-off — Codex and Claude each build the same task in isolated worktrees, blind cross-provider judges score the diffs, tie-break on splits, report with graft suggestions.",
|
|
11
|
+
defaultSandbox: "read-only",
|
|
12
|
+
workspace: {
|
|
13
|
+
default: "current_branch",
|
|
14
|
+
allowed: ["isolated_worktree", "new_branch", "current_branch"],
|
|
15
|
+
mutatesRepository: true,
|
|
16
|
+
reason: "The checkout is only the stable base; Taskflow creates one isolated child worktree for each contender.",
|
|
17
|
+
},
|
|
18
|
+
phases: [
|
|
19
|
+
{ title: "Implement", detail: "each provider builds the task in its own git worktree" },
|
|
20
|
+
{ title: "Judge", detail: "blind A/B scoring by one judge per provider; tie-break on splits" },
|
|
21
|
+
{ title: "Report", detail: "verdict, preserved branches, what to graft from the loser" },
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CONTENDERS = [
|
|
26
|
+
{ provider: "codex", model: "gpt-5.5", name: "Codex" },
|
|
27
|
+
{ provider: "claude-code", model: "claude-fable-5", name: "Claude" },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const task =
|
|
31
|
+
typeof args === "string" && args.trim()
|
|
32
|
+
? args.trim()
|
|
33
|
+
: args && typeof args === "object" && typeof args.task === "string" && args.task.trim()
|
|
34
|
+
? args.task.trim()
|
|
35
|
+
: null
|
|
36
|
+
if (!task) throw new Error(`bake-off needs a task — pass --args '"<task>"' or --args '{"task": "..."}'`)
|
|
37
|
+
|
|
38
|
+
// Neutral, unique-per-run branch names (now() is resume-stable) with a shuffled provider→slot
|
|
39
|
+
// mapping, so the judges' A/B labels carry no provider hint. Best-effort blinding: the diff
|
|
40
|
+
// itself can still hint at a model's style, so judges are told to score only the code.
|
|
41
|
+
const stamp = now().toString(36)
|
|
42
|
+
const shuffled = random() < 0.5 ? [CONTENDERS[1], CONTENDERS[0]] : [...CONTENDERS]
|
|
43
|
+
const slots = shuffled.map((c, i) => ({
|
|
44
|
+
...c,
|
|
45
|
+
slot: i === 0 ? "A" : "B",
|
|
46
|
+
branch: `bake-off/${stamp}-${i === 0 ? "a" : "b"}`,
|
|
47
|
+
}))
|
|
48
|
+
|
|
49
|
+
const IMPL_SCHEMA = {
|
|
50
|
+
type: "object",
|
|
51
|
+
required: ["approach", "tested"],
|
|
52
|
+
properties: { approach: { type: "string" }, tested: { type: "string" } },
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const JUDGE_SCHEMA = {
|
|
56
|
+
type: "object",
|
|
57
|
+
required: ["winner", "scoreA", "scoreB", "reasoning"],
|
|
58
|
+
properties: {
|
|
59
|
+
winner: { type: "string", enum: ["A", "B", "tie"] },
|
|
60
|
+
scoreA: { type: "number" },
|
|
61
|
+
scoreB: { type: "number" },
|
|
62
|
+
reasoning: { type: "string" },
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Implement — same prompt, separate worktrees. Committing is mandatory: the
|
|
68
|
+
// judges read each entry as `git diff HEAD...<branch>`, so uncommitted work
|
|
69
|
+
// is invisible to them (and an untouched worktree is auto-removed).
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
phase("Implement")
|
|
72
|
+
log(`bake-off: ${task}`)
|
|
73
|
+
|
|
74
|
+
const built = (
|
|
75
|
+
await parallel(
|
|
76
|
+
slots.map((s) => () =>
|
|
77
|
+
agent(
|
|
78
|
+
`Implement the following task in this repository, completely:\n\n${task}\n\n` +
|
|
79
|
+
`Match the existing code style and conventions. Run the project's relevant tests or build ` +
|
|
80
|
+
`if available and make them pass. COMMIT all of your work to the current branch with clear ` +
|
|
81
|
+
`messages — uncommitted changes will not be judged. Do not push.\n\n` +
|
|
82
|
+
`When done, summarize your approach and exactly what you verified (tests run, manual checks).`,
|
|
83
|
+
{ provider: s.provider, model: s.model, label: `implement: ${s.name}`, phase: "Implement", worktree: s.branch, schema: IMPL_SCHEMA },
|
|
84
|
+
).then((r) => ({ ...s, approach: r.approach, tested: r.tested })),
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
).filter(Boolean)
|
|
88
|
+
|
|
89
|
+
if (built.length === 0) throw new Error("both implementations failed — nothing to judge")
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Judge — one judge per provider scores both diffs blind. Agreement on a
|
|
93
|
+
// non-tie winner decides it; a split or all-tie goes to a tie-break judge
|
|
94
|
+
// who also sees the first judges' reasoning.
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
let verdicts = []
|
|
97
|
+
let winnerSlot = null
|
|
98
|
+
|
|
99
|
+
if (built.length === 1) {
|
|
100
|
+
winnerSlot = built[0].slot
|
|
101
|
+
log(`only ${built[0].name} finished — wins by default`)
|
|
102
|
+
} else {
|
|
103
|
+
phase("Judge")
|
|
104
|
+
const [a, b] = built[0].slot === "A" ? built : [built[1], built[0]]
|
|
105
|
+
const judgePrompt =
|
|
106
|
+
`Two competing implementations of the same task exist as branches in this repository.\n\n` +
|
|
107
|
+
`Task:\n${task}\n\n` +
|
|
108
|
+
`Implementation A: branch ${a.branch}\nImplementation B: branch ${b.branch}\n\n` +
|
|
109
|
+
`Review each with \`git diff HEAD...<branch>\` and read enough surrounding code to judge it in ` +
|
|
110
|
+
`context. Judge ONLY the code — ignore commit authorship, message style, and anything else ` +
|
|
111
|
+
`that hints at who wrote it. Score each implementation 0-10 weighing correctness, ` +
|
|
112
|
+
`completeness against the task, code quality, and test coverage, then pick a winner — "tie" ` +
|
|
113
|
+
`only when they are genuinely inseparable. Be specific about what decided it.`
|
|
114
|
+
|
|
115
|
+
verdicts = (
|
|
116
|
+
await parallel(
|
|
117
|
+
CONTENDERS.map((j) => () =>
|
|
118
|
+
agent(judgePrompt, { provider: j.provider, model: j.model, label: `judge: ${j.name}`, phase: "Judge", schema: JUDGE_SCHEMA }).then(
|
|
119
|
+
(v) => ({ judge: j.name, ...v }),
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
).filter(Boolean)
|
|
124
|
+
|
|
125
|
+
const calls = verdicts.map((v) => v.winner).filter((w) => w !== "tie")
|
|
126
|
+
if (verdicts.length > 0 && calls.length > 0 && new Set(calls).size === 1) {
|
|
127
|
+
winnerSlot = calls[0]
|
|
128
|
+
} else if (verdicts.length === 0) {
|
|
129
|
+
log("both judges failed — reporting without a verdict")
|
|
130
|
+
} else {
|
|
131
|
+
log(`judges split (${verdicts.map((v) => `${v.judge}: ${v.winner}`).join(", ")}) — tie-break`)
|
|
132
|
+
const tiebreak = await agent(
|
|
133
|
+
`${judgePrompt}\n\nTwo judges already scored these and could not agree:\n` +
|
|
134
|
+
`${JSON.stringify(verdicts.map((v) => ({ winner: v.winner, scoreA: v.scoreA, scoreB: v.scoreB, reasoning: v.reasoning })), null, 2)}\n\n` +
|
|
135
|
+
`You are the tie-break. Weigh their reasoning against the actual diffs and make the final ` +
|
|
136
|
+
`call — "tie" only as a last resort.`,
|
|
137
|
+
{ label: "tie-break", phase: "Judge", effort: "xhigh", schema: JUDGE_SCHEMA },
|
|
138
|
+
)
|
|
139
|
+
verdicts.push({ judge: "tie-break", ...tiebreak })
|
|
140
|
+
winnerSlot = tiebreak.winner === "tie" ? null : tiebreak.winner
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Report — the A/B mapping is revealed only here, after all judging is done.
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
phase("Report")
|
|
148
|
+
|
|
149
|
+
const winner = winnerSlot ? built.find((c) => c.slot === winnerSlot) : null
|
|
150
|
+
log(winner ? `winner: ${winner.name} (${winner.branch})` : "verdict: tie")
|
|
151
|
+
|
|
152
|
+
const report = await agent(
|
|
153
|
+
`Write the closing report for an implementation bake-off.\n\n` +
|
|
154
|
+
`Task:\n${task}\n\n` +
|
|
155
|
+
`Contenders (the judges saw only the A/B labels):\n` +
|
|
156
|
+
`${JSON.stringify(built.map((c) => ({ slot: c.slot, provider: c.name, branch: c.branch, approach: c.approach, tested: c.tested })), null, 2)}\n\n` +
|
|
157
|
+
`Judge verdicts:\n${JSON.stringify(verdicts, null, 2)}\n\n` +
|
|
158
|
+
`Verdict: ${winner ? `${winner.name} (slot ${winner.slot}) won` : "tie / no verdict"}.\n\n` +
|
|
159
|
+
`Cover: the verdict and the decisive reasons; concrete strengths of the winner; anything in the ` +
|
|
160
|
+
`other implementation worth grafting — read its diff (git diff HEAD...<branch>) and name ` +
|
|
161
|
+
`specific files or ideas; and exact next steps (git commands to inspect each branch, merge the ` +
|
|
162
|
+
`winner, cherry-pick grafts). Both worktree branches are preserved.`,
|
|
163
|
+
{ label: "report", phase: "Report" },
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
winner: winner ? winner.name : "tie",
|
|
168
|
+
branches: Object.fromEntries(built.map((c) => [c.name, c.branch])),
|
|
169
|
+
judges: verdicts,
|
|
170
|
+
report,
|
|
171
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Port of Claude Code's built-in `code-review` workflow: one finder agent per review angle, an
|
|
2
|
+
// independent verifier per candidate finding (CONFIRMED / PLAUSIBLE / REFUTED), a gap-sweep at
|
|
3
|
+
// the higher levels, then a ranked, capped report.
|
|
4
|
+
// Run with: portta flow workflows run code-review [--args '{"target": "<ref|path|diff>", "level": "high|xhigh|max"}']
|
|
5
|
+
|
|
6
|
+
export const meta = {
|
|
7
|
+
name: "code-review",
|
|
8
|
+
description:
|
|
9
|
+
"Multi-agent code review — one finder per angle, independent verification of every finding, gap-sweep at higher levels, ranked report.",
|
|
10
|
+
defaultSandbox: "read-only",
|
|
11
|
+
workspace: {
|
|
12
|
+
default: "current_branch",
|
|
13
|
+
allowed: ["isolated_worktree", "new_branch", "current_branch"],
|
|
14
|
+
mutatesRepository: false,
|
|
15
|
+
reason: "Review should inspect the active branch and its uncommitted changes without modifying them.",
|
|
16
|
+
},
|
|
17
|
+
phases: [
|
|
18
|
+
{ title: "Review", detail: "one finder agent per review angle" },
|
|
19
|
+
{ title: "Verify", detail: "independent verifier per candidate finding" },
|
|
20
|
+
{ title: "Sweep", detail: "hunt for what the angle reviewers missed (xhigh/max)" },
|
|
21
|
+
{ title: "Report", detail: "rank, cap, and write up the confirmed findings" },
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Review angles in priority order; each level takes a prefix and scales the caps.
|
|
26
|
+
const ANGLES = [
|
|
27
|
+
"correctness — logic errors, wrong conditions, off-by-ones, broken control flow",
|
|
28
|
+
"security — injection, traversal, unsafe deserialization, secrets, authz gaps",
|
|
29
|
+
"error-handling — swallowed errors, missing edge/null cases, bad failure modes",
|
|
30
|
+
"resource-handling — leaks, missing cleanup, unbounded growth, races on shutdown",
|
|
31
|
+
"concurrency — data races, missing awaits, ordering assumptions, deadlocks",
|
|
32
|
+
"api-contracts — breaking changes, type mismatches, violated invariants between modules",
|
|
33
|
+
"performance — accidental O(n²), needless I/O in loops, oversized payloads",
|
|
34
|
+
"test-gaps — changed behavior with no covering test, tests asserting the wrong thing",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const LEVEL_PARAMS = {
|
|
38
|
+
high: { angles: 4, maxPerAngle: 8, maxReport: 10, gapSweep: false },
|
|
39
|
+
xhigh: { angles: 6, maxPerAngle: 10, maxReport: 20, gapSweep: true },
|
|
40
|
+
max: { angles: 8, maxPerAngle: 12, maxReport: 30, gapSweep: true },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const level = args && typeof args === "object" && typeof args.level === "string" ? args.level : "high"
|
|
44
|
+
const params = LEVEL_PARAMS[level]
|
|
45
|
+
if (!params) throw new Error(`unknown level "${level}" — expected one of: ${Object.keys(LEVEL_PARAMS).join(", ")}`)
|
|
46
|
+
|
|
47
|
+
const target =
|
|
48
|
+
args && typeof args === "object" && typeof args.target === "string" && args.target.trim()
|
|
49
|
+
? args.target.trim()
|
|
50
|
+
: "the uncommitted changes in this repository (git diff HEAD; if empty, review the most recent commit)"
|
|
51
|
+
|
|
52
|
+
const angles = ANGLES.slice(0, params.angles)
|
|
53
|
+
|
|
54
|
+
const FINDING_SCHEMA = {
|
|
55
|
+
type: "object",
|
|
56
|
+
required: ["findings"],
|
|
57
|
+
properties: {
|
|
58
|
+
findings: {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: {
|
|
61
|
+
type: "object",
|
|
62
|
+
required: ["file", "title", "why", "severity"],
|
|
63
|
+
properties: {
|
|
64
|
+
file: { type: "string" },
|
|
65
|
+
line: { type: "number" },
|
|
66
|
+
title: { type: "string" },
|
|
67
|
+
why: { type: "string" },
|
|
68
|
+
severity: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const VERDICT_SCHEMA = {
|
|
76
|
+
type: "object",
|
|
77
|
+
required: ["verdict", "reason"],
|
|
78
|
+
properties: {
|
|
79
|
+
verdict: { type: "string", enum: ["CONFIRMED", "PLAUSIBLE", "REFUTED"] },
|
|
80
|
+
reason: { type: "string" },
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Each candidate gets an independent verifier; CONFIRMED/PLAUSIBLE survive, REFUTED dies.
|
|
85
|
+
const verify = (finding) =>
|
|
86
|
+
agent(
|
|
87
|
+
`You are a skeptical senior reviewer double-checking one flagged issue in ${target}. ` +
|
|
88
|
+
`Read the actual code before judging. Verdicts:\n` +
|
|
89
|
+
`- CONFIRMED: you reproduced the reasoning against the real code and the issue is real.\n` +
|
|
90
|
+
`- PLAUSIBLE: you could not refute it, but could not fully confirm it from the code either.\n` +
|
|
91
|
+
`- REFUTED: false positive — mis-read code, already handled, intended behavior, or not present.\n` +
|
|
92
|
+
`Default toward REFUTED when the finding is vague or you cannot locate the code it describes.\n\n` +
|
|
93
|
+
`Finding (JSON):\n${JSON.stringify(finding, null, 2)}`,
|
|
94
|
+
{ label: `verify: ${finding.title.slice(0, 40)}`, phase: "Verify", schema: VERDICT_SCHEMA },
|
|
95
|
+
).then((v) => (v.verdict === "REFUTED" ? null : { ...finding, verdict: v.verdict, verdictReason: v.reason }))
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Review + Verify — pipeline: each angle's findings go to verification as soon
|
|
99
|
+
// as that finder returns, while slower finders are still reading.
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
phase("Review")
|
|
102
|
+
log(`reviewing ${target} at level ${level}: ${angles.length} angles`)
|
|
103
|
+
|
|
104
|
+
const verifiedByAngle = await pipeline(
|
|
105
|
+
angles,
|
|
106
|
+
(angle) =>
|
|
107
|
+
agent(
|
|
108
|
+
`Review ${target} strictly through this lens:\n${angle}\n\n` +
|
|
109
|
+
`Read the relevant code (and surrounding context) rather than judging from the diff text ` +
|
|
110
|
+
`alone. Report up to ${params.maxPerAngle} concrete findings: file, line when you can ` +
|
|
111
|
+
`pinpoint it, a short title, why it is a real problem through this lens, and a severity ` +
|
|
112
|
+
`(critical|high|medium|low). Only report issues you can point to in the code — no ` +
|
|
113
|
+
`speculation, no style nits.`,
|
|
114
|
+
{ label: `find: ${angle.split(" — ")[0]}`, phase: "Review", schema: FINDING_SCHEMA },
|
|
115
|
+
),
|
|
116
|
+
(review, angle) =>
|
|
117
|
+
parallel(
|
|
118
|
+
(review.findings || [])
|
|
119
|
+
.slice(0, params.maxPerAngle)
|
|
120
|
+
.map((f) => () => verify({ ...f, angle: angle.split(" — ")[0] })),
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
let confirmed = verifiedByAngle.filter(Boolean).flat().filter(Boolean)
|
|
125
|
+
log(`${confirmed.length} findings survived verification`)
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Sweep (xhigh/max) — one agent hunts for what the per-angle reviewers missed;
|
|
129
|
+
// its candidates go through the same verifier.
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
if (params.gapSweep) {
|
|
132
|
+
phase("Sweep")
|
|
133
|
+
const sweep = await agent(
|
|
134
|
+
`You are the final gap-sweep on a multi-agent review of ${target}. The angles already ` +
|
|
135
|
+
`covered: ${angles.map((a) => a.split(" — ")[0]).join(", ")}. Findings already confirmed ` +
|
|
136
|
+
`(do NOT re-report these):\n${JSON.stringify(confirmed.map((f) => ({ file: f.file, title: f.title })), null, 2)}\n\n` +
|
|
137
|
+
`Look for what they missed: cross-cutting issues, interactions between changes, anything in ` +
|
|
138
|
+
`the changed code that no single angle would catch. Up to ${params.maxPerAngle} findings, ` +
|
|
139
|
+
`same bar: concrete, code-anchored, no nits.`,
|
|
140
|
+
{ label: "gap-sweep", phase: "Sweep", schema: FINDING_SCHEMA },
|
|
141
|
+
)
|
|
142
|
+
const sweepVerified = await parallel(
|
|
143
|
+
(sweep.findings || []).slice(0, params.maxPerAngle).map((f) => () => verify({ ...f, angle: "gap-sweep" })),
|
|
144
|
+
)
|
|
145
|
+
confirmed = confirmed.concat(sweepVerified.filter(Boolean))
|
|
146
|
+
log(`${confirmed.length} findings after gap-sweep`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Report — rank (CONFIRMED before PLAUSIBLE, then severity), cap, write up.
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
phase("Report")
|
|
153
|
+
|
|
154
|
+
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 }
|
|
155
|
+
const ranked = confirmed
|
|
156
|
+
.slice()
|
|
157
|
+
.sort(
|
|
158
|
+
(a, b) =>
|
|
159
|
+
(a.verdict === b.verdict ? 0 : a.verdict === "CONFIRMED" ? -1 : 1) ||
|
|
160
|
+
SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||
|
|
161
|
+
a.file.localeCompare(b.file),
|
|
162
|
+
)
|
|
163
|
+
const findings = ranked.slice(0, params.maxReport)
|
|
164
|
+
if (ranked.length > findings.length) log(`reporting top ${params.maxReport} of ${ranked.length} findings (cap)`)
|
|
165
|
+
|
|
166
|
+
if (findings.length === 0) {
|
|
167
|
+
return { findings: [], report: `Reviewed ${target} across ${angles.length} angles at level ${level}: no findings survived independent verification.` }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const report = await agent(
|
|
171
|
+
`Write a concise code-review report for ${target}. The findings below are already independently ` +
|
|
172
|
+
`verified and ranked — keep that order. For each: severity, verdict (CONFIRMED/PLAUSIBLE), ` +
|
|
173
|
+
`file:line, what is wrong, and a suggested fix direction in a sentence. Open with a 2-3 ` +
|
|
174
|
+
`sentence summary of the overall state of the change.\n\n` +
|
|
175
|
+
`Findings (JSON):\n${JSON.stringify(findings, null, 2)}`,
|
|
176
|
+
{ label: "write report", phase: "Report" },
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return { findings, report }
|