@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,222 @@
|
|
|
1
|
+
// Port of Claude Code's built-in `deep-research` workflow: fan out web searches, deep-read the
|
|
2
|
+
// best sources, adversarially verify every claim with a 3-vote panel, synthesize a cited report.
|
|
3
|
+
// Run with: portta flow workflows run deep-research --args '"<question>"'
|
|
4
|
+
|
|
5
|
+
export const meta = {
|
|
6
|
+
name: "deep-research",
|
|
7
|
+
description:
|
|
8
|
+
"Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report.",
|
|
9
|
+
defaultSandbox: "read-only",
|
|
10
|
+
workspace: {
|
|
11
|
+
default: "current_branch",
|
|
12
|
+
allowed: ["isolated_worktree", "new_branch", "current_branch"],
|
|
13
|
+
mutatesRepository: false,
|
|
14
|
+
reason: "Research is read-only and normally does not need an isolated Git worktree.",
|
|
15
|
+
},
|
|
16
|
+
phases: [
|
|
17
|
+
{ title: "Scope", detail: "break the question into distinct search directives" },
|
|
18
|
+
{ title: "Search", detail: "5 parallel web-search agents" },
|
|
19
|
+
{ title: "Fetch", detail: "dedup URLs, deep-read the top sources" },
|
|
20
|
+
{ title: "Verify", detail: "3-vote adversarial panel per claim" },
|
|
21
|
+
{ title: "Synthesize", detail: "cited report from surviving claims" },
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SEARCHERS = 5
|
|
26
|
+
const VOTES_PER_CLAIM = 3
|
|
27
|
+
const REFUTATIONS_REQUIRED = 2 // 2 of 3 refute votes kill a claim
|
|
28
|
+
const MAX_FETCH = 15
|
|
29
|
+
const MAX_VERIFY_CLAIMS = 25
|
|
30
|
+
|
|
31
|
+
const question =
|
|
32
|
+
typeof args === "string" && args.trim()
|
|
33
|
+
? args.trim()
|
|
34
|
+
: args && typeof args === "object" && typeof args.question === "string" && args.question.trim()
|
|
35
|
+
? args.question.trim()
|
|
36
|
+
: null
|
|
37
|
+
if (!question) {
|
|
38
|
+
throw new Error(`deep-research needs a question — pass --args '"<question>"' or --args '{"question": "..."}'`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Scope — turn the question into SEARCHERS distinct search directives.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
phase("Scope")
|
|
45
|
+
log(`scoping: ${question}`)
|
|
46
|
+
|
|
47
|
+
const scope = await agent(
|
|
48
|
+
`You are scoping a deep web-research project for this question:\n\n${question}\n\n` +
|
|
49
|
+
`Produce exactly ${SEARCHERS} distinct search directives. Each directive is a short instruction ` +
|
|
50
|
+
`for an independent researcher with web search: what to search for and what kind of sources to ` +
|
|
51
|
+
`prefer (primary data, official docs, expert analysis, recent news, contrarian takes, ...). ` +
|
|
52
|
+
`The directives must not overlap — together they should cover the question from genuinely ` +
|
|
53
|
+
`different angles (different sub-questions, source types, or time windows).`,
|
|
54
|
+
{
|
|
55
|
+
label: "scope directives",
|
|
56
|
+
schema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
required: ["directives"],
|
|
59
|
+
properties: {
|
|
60
|
+
directives: { type: "array", minItems: SEARCHERS, maxItems: SEARCHERS, items: { type: "string" } },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Search — one web-search agent per directive, in parallel. Barrier: the Fetch
|
|
68
|
+
// stage dedups URLs ACROSS all searchers before spending deep-read agents.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
phase("Search")
|
|
71
|
+
|
|
72
|
+
const searches = await parallel(
|
|
73
|
+
scope.directives.map((directive, i) => () =>
|
|
74
|
+
agent(
|
|
75
|
+
`Research question: ${question}\n\nYour directive (#${i + 1} of ${SEARCHERS}):\n${directive}\n\n` +
|
|
76
|
+
`Use your web search tool to find the most authoritative sources bearing on this directive. ` +
|
|
77
|
+
`Return up to 6 sources. For each give the exact URL, its title, and a relevance score 1-5 ` +
|
|
78
|
+
`(5 = directly answers the question with primary evidence). Only return URLs you actually ` +
|
|
79
|
+
`found via search — never construct or guess a URL.`,
|
|
80
|
+
{
|
|
81
|
+
label: `search ${i + 1}: ${directive.slice(0, 40)}`,
|
|
82
|
+
schema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
required: ["sources"],
|
|
85
|
+
properties: {
|
|
86
|
+
sources: {
|
|
87
|
+
type: "array",
|
|
88
|
+
items: {
|
|
89
|
+
type: "object",
|
|
90
|
+
required: ["url", "title", "relevance"],
|
|
91
|
+
properties: {
|
|
92
|
+
url: { type: "string" },
|
|
93
|
+
title: { type: "string" },
|
|
94
|
+
relevance: { type: "number" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// Dedup by URL (highest relevance wins), rank, keep the top MAX_FETCH.
|
|
106
|
+
const byUrl = new Map()
|
|
107
|
+
for (const s of searches.filter(Boolean).flatMap((r) => r.sources || [])) {
|
|
108
|
+
if (!s || !s.url) continue
|
|
109
|
+
const prev = byUrl.get(s.url)
|
|
110
|
+
if (!prev || s.relevance > prev.relevance) byUrl.set(s.url, s)
|
|
111
|
+
}
|
|
112
|
+
const ranked = [...byUrl.values()].sort((a, b) => b.relevance - a.relevance)
|
|
113
|
+
const sources = ranked.slice(0, MAX_FETCH)
|
|
114
|
+
if (ranked.length > sources.length) log(`dropping ${ranked.length - sources.length} lower-relevance sources (cap ${MAX_FETCH})`)
|
|
115
|
+
log(`${sources.length} unique sources to read`)
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Fetch — deep-read each source and extract concrete, checkable claims.
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
phase("Fetch")
|
|
121
|
+
|
|
122
|
+
const fetched = await parallel(
|
|
123
|
+
sources.map((src) => () =>
|
|
124
|
+
agent(
|
|
125
|
+
`Research question: ${question}\n\nFetch and read this source:\n${src.title}\n${src.url}\n\n` +
|
|
126
|
+
`Extract the concrete, checkable claims it makes that bear on the research question — facts, ` +
|
|
127
|
+
`numbers, findings, dated events. For each claim include a short supporting quote from the ` +
|
|
128
|
+
`source. Skip opinions, marketing, and anything the source merely speculates about. If the ` +
|
|
129
|
+
`page is unreachable or irrelevant, return an empty list.`,
|
|
130
|
+
{
|
|
131
|
+
label: `fetch: ${src.title.slice(0, 40)}`,
|
|
132
|
+
schema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
required: ["claims"],
|
|
135
|
+
properties: {
|
|
136
|
+
claims: {
|
|
137
|
+
type: "array",
|
|
138
|
+
items: {
|
|
139
|
+
type: "object",
|
|
140
|
+
required: ["claim", "quote"],
|
|
141
|
+
properties: { claim: { type: "string" }, quote: { type: "string" } },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
).then((r) => (r.claims || []).map((c) => ({ ...c, url: src.url, title: src.title }))),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const allClaims = fetched.filter(Boolean).flat()
|
|
152
|
+
const claims = allClaims.slice(0, MAX_VERIFY_CLAIMS)
|
|
153
|
+
if (allClaims.length > claims.length) log(`verifying first ${MAX_VERIFY_CLAIMS} of ${allClaims.length} claims (cap)`)
|
|
154
|
+
log(`${claims.length} claims to verify`)
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Verify — a 3-vote adversarial panel per claim; 2 of 3 refutes kill it. Each
|
|
158
|
+
// voter gets a different lens so the panel's failure modes are decorrelated.
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
phase("Verify")
|
|
161
|
+
|
|
162
|
+
const LENSES = [
|
|
163
|
+
"Check the claim against the quoted text: does the source actually say this, or is it an overreach/misreading?",
|
|
164
|
+
"Check the claim against your own knowledge and a quick independent web search: is it contradicted elsewhere?",
|
|
165
|
+
"Check the claim's precision: are the numbers, dates, and attributions right, or materially misleading as stated?",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
const verified = await pipeline(claims, (claim) =>
|
|
169
|
+
parallel(
|
|
170
|
+
LENSES.slice(0, VOTES_PER_CLAIM).map((lens, v) => () =>
|
|
171
|
+
agent(
|
|
172
|
+
`You are a skeptical fact-checker. Your job is to try to REFUTE this claim.\n\n` +
|
|
173
|
+
`Claim: ${claim.claim}\n` +
|
|
174
|
+
`Source: ${claim.title} — ${claim.url}\n` +
|
|
175
|
+
`Supporting quote: "${claim.quote}"\n\n` +
|
|
176
|
+
`${lens}\n\n` +
|
|
177
|
+
`Vote refuted=true if the claim is false, unsupported, or materially misleading; when ` +
|
|
178
|
+
`genuinely uncertain, default to refuted=true.`,
|
|
179
|
+
{
|
|
180
|
+
label: `verify ${v + 1}/${VOTES_PER_CLAIM}: ${claim.claim.slice(0, 32)}`,
|
|
181
|
+
schema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
required: ["refuted", "reason"],
|
|
184
|
+
properties: { refuted: { type: "boolean" }, reason: { type: "string" } },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
).then((votes) => {
|
|
190
|
+
const cast = votes.filter(Boolean)
|
|
191
|
+
const refutes = cast.filter((v) => v.refuted).length
|
|
192
|
+
// A skipped/errored vote counts as a refute: a claim must EARN its place in the report.
|
|
193
|
+
const killed = refutes + (VOTES_PER_CLAIM - cast.length) >= REFUTATIONS_REQUIRED
|
|
194
|
+
return killed ? null : { ...claim, refutes, votes: VOTES_PER_CLAIM }
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const survivors = verified.filter(Boolean)
|
|
199
|
+
log(`${survivors.length} of ${claims.length} claims survived the panel`)
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Synthesize — a cited report from the surviving claims only.
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
phase("Synthesize")
|
|
205
|
+
|
|
206
|
+
if (survivors.length === 0) {
|
|
207
|
+
return (
|
|
208
|
+
`# Research report: ${question}\n\n` +
|
|
209
|
+
`No claims survived adversarial verification (${claims.length} candidates, ${VOTES_PER_CLAIM}-vote panel, ` +
|
|
210
|
+
`${REFUTATIONS_REQUIRED} refutes to kill). The available sourcing was too weak to answer confidently.`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return await agent(
|
|
215
|
+
`Write a well-structured research report answering:\n\n${question}\n\n` +
|
|
216
|
+
`Use ONLY the verified claims below — every factual statement must trace to one of them, cited ` +
|
|
217
|
+
`inline as a markdown link to its source URL. Group related claims into sections, lead with a ` +
|
|
218
|
+
`direct answer, and close with what remains uncertain (claims that were killed or angles that ` +
|
|
219
|
+
`found nothing). Do not introduce facts that are not in the claims.\n\n` +
|
|
220
|
+
`Verified claims (JSON):\n${JSON.stringify(survivors, null, 2)}`,
|
|
221
|
+
{ label: "synthesize report" },
|
|
222
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// taskflow-original built-in: the same change reviewed twice — once by Codex, once by Claude —
|
|
2
|
+
// each over the ENTIRE feature/branch, then a synthesis pass merges both: consensus findings
|
|
3
|
+
// (both providers saw it → highest confidence), unique catches, and disagreements called out.
|
|
4
|
+
// Run with: portta flow workflows run multi-provider-review [--args '{"target": "<ref|path|diff>"}']
|
|
5
|
+
|
|
6
|
+
export const meta = {
|
|
7
|
+
name: "multi-provider-review",
|
|
8
|
+
description:
|
|
9
|
+
"Dual-provider review — Codex and Claude each review the full feature/branch independently, then a synthesis merges both: consensus, unique catches, disagreements.",
|
|
10
|
+
defaultSandbox: "read-only",
|
|
11
|
+
workspace: {
|
|
12
|
+
default: "current_branch",
|
|
13
|
+
allowed: ["isolated_worktree", "new_branch", "current_branch"],
|
|
14
|
+
mutatesRepository: false,
|
|
15
|
+
reason: "Both reviewers inspect the active checkout read-only.",
|
|
16
|
+
},
|
|
17
|
+
phases: [
|
|
18
|
+
{ title: "Review", detail: "Codex and Claude each review the entire change independently" },
|
|
19
|
+
{ title: "Synthesize", detail: "merge both reviews: consensus, unique findings, ranked report" },
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MAX_FINDINGS_PER_REVIEW = 15
|
|
24
|
+
|
|
25
|
+
const REVIEWERS = [
|
|
26
|
+
{ provider: "codex", model: "gpt-5.5", name: "Codex" },
|
|
27
|
+
{ provider: "claude-code", model: "claude-fable-5", name: "Claude" },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const target =
|
|
31
|
+
typeof args === "string" && args.trim()
|
|
32
|
+
? args.trim()
|
|
33
|
+
: args && typeof args === "object" && typeof args.target === "string" && args.target.trim()
|
|
34
|
+
? args.target.trim()
|
|
35
|
+
: "the current branch in its entirety — every change relative to its merge-base with the " +
|
|
36
|
+
"repository's default branch (git merge-base, then git diff <base>...HEAD), plus any " +
|
|
37
|
+
"uncommitted changes; if already on the default branch, the most recent commit plus any " +
|
|
38
|
+
"uncommitted changes"
|
|
39
|
+
|
|
40
|
+
const REVIEW_SCHEMA = {
|
|
41
|
+
type: "object",
|
|
42
|
+
required: ["assessment", "findings"],
|
|
43
|
+
properties: {
|
|
44
|
+
assessment: { type: "string" },
|
|
45
|
+
findings: {
|
|
46
|
+
type: "array",
|
|
47
|
+
items: {
|
|
48
|
+
type: "object",
|
|
49
|
+
required: ["file", "title", "why", "severity"],
|
|
50
|
+
properties: {
|
|
51
|
+
file: { type: "string" },
|
|
52
|
+
line: { type: "number" },
|
|
53
|
+
title: { type: "string" },
|
|
54
|
+
why: { type: "string" },
|
|
55
|
+
severity: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const SYNTHESIS_SCHEMA = {
|
|
63
|
+
type: "object",
|
|
64
|
+
required: ["report", "findings"],
|
|
65
|
+
properties: {
|
|
66
|
+
report: { type: "string" },
|
|
67
|
+
findings: {
|
|
68
|
+
type: "array",
|
|
69
|
+
items: {
|
|
70
|
+
type: "object",
|
|
71
|
+
required: ["file", "title", "why", "severity", "foundBy"],
|
|
72
|
+
properties: {
|
|
73
|
+
file: { type: "string" },
|
|
74
|
+
line: { type: "number" },
|
|
75
|
+
title: { type: "string" },
|
|
76
|
+
why: { type: "string" },
|
|
77
|
+
severity: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
78
|
+
foundBy: { type: "array", items: { type: "string", enum: REVIEWERS.map((r) => r.name) } },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Review — both providers read the same full change, blind to each other.
|
|
87
|
+
// The prompts are identical so differences in output reflect the models, not
|
|
88
|
+
// the framing; neither is told another reviewer exists.
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
phase("Review")
|
|
91
|
+
log(`dual-provider review of ${target}`)
|
|
92
|
+
|
|
93
|
+
const reviewPrompt =
|
|
94
|
+
`You are conducting a complete, independent code review of ${target}. Review the WHOLE change, ` +
|
|
95
|
+
`not a slice of it: read the full diff and enough surrounding code to judge it in context. ` +
|
|
96
|
+
`Cover correctness, security, error handling, concurrency, resource handling, API contracts, ` +
|
|
97
|
+
`performance, and test coverage.\n\n` +
|
|
98
|
+
`Report up to ${MAX_FINDINGS_PER_REVIEW} concrete findings: file, line when you can pinpoint ` +
|
|
99
|
+
`it, a short title, why it is a real problem, and a severity (critical|high|medium|low). Only ` +
|
|
100
|
+
`report issues you can point to in the code — no speculation, no style nits. Also give an ` +
|
|
101
|
+
`overall assessment (2-4 sentences): the state of the change, its main risks, and whether it ` +
|
|
102
|
+
`looks ready to merge.`
|
|
103
|
+
|
|
104
|
+
const reviews = (
|
|
105
|
+
await parallel(
|
|
106
|
+
REVIEWERS.map((r) => () =>
|
|
107
|
+
agent(reviewPrompt, {
|
|
108
|
+
provider: r.provider,
|
|
109
|
+
model: r.model,
|
|
110
|
+
label: `review: ${r.name}`,
|
|
111
|
+
phase: "Review",
|
|
112
|
+
schema: REVIEW_SCHEMA,
|
|
113
|
+
}).then((review) => ({
|
|
114
|
+
reviewer: r.name,
|
|
115
|
+
assessment: review.assessment,
|
|
116
|
+
findings: (review.findings || []).slice(0, MAX_FINDINGS_PER_REVIEW),
|
|
117
|
+
})),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
).filter(Boolean)
|
|
121
|
+
|
|
122
|
+
if (reviews.length === 0) throw new Error("both provider reviews failed — nothing to synthesize")
|
|
123
|
+
if (reviews.length < REVIEWERS.length)
|
|
124
|
+
log(`warning: only ${reviews.map((r) => r.reviewer).join(", ")} completed — synthesis will lack cross-provider consensus`)
|
|
125
|
+
for (const r of reviews) log(`${r.reviewer}: ${r.findings.length} findings`)
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Synthesize — one agent (run-default provider; override with --provider) sees
|
|
129
|
+
// both labeled reviews and merges them into a single ranked picture.
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
phase("Synthesize")
|
|
132
|
+
|
|
133
|
+
const synthesis = await agent(
|
|
134
|
+
`You are synthesizing ${reviews.length} independent full code reviews of ${target}, each from a ` +
|
|
135
|
+
`different model provider. Merge them into one picture:\n` +
|
|
136
|
+
`1. Match findings that describe the same underlying issue — even when worded differently or ` +
|
|
137
|
+
`pointing at slightly different lines — and merge them into one finding with foundBy listing ` +
|
|
138
|
+
`every reviewer that caught it. Consensus findings are the highest-confidence ones; rank them ` +
|
|
139
|
+
`first, then by severity.\n` +
|
|
140
|
+
`2. Keep findings only one reviewer caught, attributed via foundBy. Spot-check any that look ` +
|
|
141
|
+
`dubious against the actual code and drop clear false positives (say so in the report).\n` +
|
|
142
|
+
`3. Where the reviews DISAGREE — about the same code, or in their overall assessments — call ` +
|
|
143
|
+
`the disagreement out explicitly in the report rather than silently picking a side.\n\n` +
|
|
144
|
+
`The report should open with a 2-4 sentence overall verdict that synthesizes both assessments, ` +
|
|
145
|
+
`then note where the reviewers agreed and disagreed, then walk the merged findings in ranked ` +
|
|
146
|
+
`order (severity, foundBy, file:line, what is wrong, fix direction in a sentence).\n\n` +
|
|
147
|
+
`Reviews (JSON):\n${JSON.stringify(reviews, null, 2)}`,
|
|
148
|
+
{ label: "synthesize", phase: "Synthesize", schema: SYNTHESIS_SCHEMA },
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 }
|
|
152
|
+
const findings = (synthesis.findings || [])
|
|
153
|
+
.slice()
|
|
154
|
+
.sort(
|
|
155
|
+
(a, b) =>
|
|
156
|
+
(b.foundBy || []).length - (a.foundBy || []).length ||
|
|
157
|
+
SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||
|
|
158
|
+
a.file.localeCompare(b.file),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const consensus = findings.filter((f) => (f.foundBy || []).length > 1).length
|
|
162
|
+
log(`${findings.length} merged findings (${consensus} consensus)`)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
reviews: reviews.map((r) => ({ reviewer: r.reviewer, assessment: r.assessment, findingCount: r.findings.length })),
|
|
166
|
+
findings,
|
|
167
|
+
report: synthesis.report,
|
|
168
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// taskflow-original built-in: one provider proposes a position, the other attacks it, the
|
|
2
|
+
// proposer rebuts or concedes, for N rounds — then a judge rules on what actually survived.
|
|
3
|
+
// The cross-provider attacker is the point: a model is measurably worse at finding holes in its
|
|
4
|
+
// own reasoning than another model is.
|
|
5
|
+
// Run with: portta flow workflows run provider-debate --args '"<question>"'
|
|
6
|
+
// or: --args '{"question": "...", "rounds": 2, "proposer": "codex" | "claude-code"}'
|
|
7
|
+
|
|
8
|
+
export const meta = {
|
|
9
|
+
name: "provider-debate",
|
|
10
|
+
description:
|
|
11
|
+
"Adversarial debate — one provider proposes, the other attacks, N rounds of rebuttal, then a judge rules on what survived, what was conceded, and what to actually do.",
|
|
12
|
+
defaultSandbox: "read-only",
|
|
13
|
+
workspace: {
|
|
14
|
+
default: "current_branch",
|
|
15
|
+
allowed: ["isolated_worktree", "new_branch", "current_branch"],
|
|
16
|
+
mutatesRepository: false,
|
|
17
|
+
reason: "The debate is read-only and normally needs no Git isolation.",
|
|
18
|
+
},
|
|
19
|
+
phases: [
|
|
20
|
+
{ title: "Propose", detail: "the proposer takes a concrete position" },
|
|
21
|
+
{ title: "Debate", detail: "attack and rebuttal, one exchange per round" },
|
|
22
|
+
{ title: "Verdict", detail: "a judge rules: survived, conceded, open, recommendation" },
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PROVIDERS = {
|
|
27
|
+
codex: { provider: "codex", model: "gpt-5.5", name: "Codex" },
|
|
28
|
+
"claude-code": { provider: "claude-code", model: "claude-fable-5", name: "Claude" },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const question =
|
|
32
|
+
typeof args === "string" && args.trim()
|
|
33
|
+
? args.trim()
|
|
34
|
+
: args && typeof args === "object" && typeof args.question === "string" && args.question.trim()
|
|
35
|
+
? args.question.trim()
|
|
36
|
+
: null
|
|
37
|
+
if (!question) {
|
|
38
|
+
throw new Error(`provider-debate needs a question — pass --args '"<question>"' or --args '{"question": "..."}'`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const proposerId = args && typeof args === "object" && args.proposer !== undefined ? args.proposer : "codex"
|
|
42
|
+
const proposer = PROVIDERS[proposerId]
|
|
43
|
+
if (!proposer) throw new Error(`unknown proposer "${proposerId}" — expected "codex" or "claude-code"`)
|
|
44
|
+
const attacker = proposer === PROVIDERS.codex ? PROVIDERS["claude-code"] : PROVIDERS.codex
|
|
45
|
+
|
|
46
|
+
const rawRounds = args && typeof args === "object" && typeof args.rounds === "number" ? args.rounds : 2
|
|
47
|
+
const rounds = Math.max(1, Math.min(5, Math.floor(rawRounds)))
|
|
48
|
+
|
|
49
|
+
const VERDICT_SCHEMA = {
|
|
50
|
+
type: "object",
|
|
51
|
+
required: ["recommendation", "survived", "conceded", "open", "report"],
|
|
52
|
+
properties: {
|
|
53
|
+
recommendation: { type: "string" },
|
|
54
|
+
survived: { type: "array", items: { type: "string" } },
|
|
55
|
+
conceded: { type: "array", items: { type: "string" } },
|
|
56
|
+
open: { type: "array", items: { type: "string" } },
|
|
57
|
+
report: { type: "string" },
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const transcriptText = (transcript) =>
|
|
62
|
+
transcript
|
|
63
|
+
.map((t) => `--- Round ${t.round} ---\nObjections:\n${t.critique}\n\nRebuttal:\n${t.rebuttal}`)
|
|
64
|
+
.join("\n\n")
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Propose — a concrete, attackable position.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
phase("Propose")
|
|
70
|
+
log(`debate: ${question} (${proposer.name} proposes, ${attacker.name} attacks, ${rounds} round${rounds === 1 ? "" : "s"})`)
|
|
71
|
+
|
|
72
|
+
const proposal = await agent(
|
|
73
|
+
`Take a clear position on the following and propose a concrete answer or design:\n\n${question}\n\n` +
|
|
74
|
+
`If this concerns the repository you are in, read the relevant code first and ground your ` +
|
|
75
|
+
`position in it. Make your reasoning, assumptions, and tradeoffs explicit — this proposal will ` +
|
|
76
|
+
`be scrutinized line by line, so vagueness counts against it.`,
|
|
77
|
+
{ provider: proposer.provider, model: proposer.model, label: `propose: ${proposer.name}`, phase: "Propose" },
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Debate — each round is one attack/rebuttal exchange. The rebuttal ends with
|
|
82
|
+
// the full current proposal, which becomes the target of the next attack.
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
phase("Debate")
|
|
85
|
+
|
|
86
|
+
const transcript = []
|
|
87
|
+
let current = proposal
|
|
88
|
+
|
|
89
|
+
for (let round = 1; round <= rounds; round++) {
|
|
90
|
+
const history = transcript.length > 0 ? `\n\nDebate so far:\n${transcriptText(transcript)}` : ""
|
|
91
|
+
|
|
92
|
+
const critique = await agent(
|
|
93
|
+
`You are the adversarial examiner in round ${round} of ${rounds} of a structured debate.\n\n` +
|
|
94
|
+
`Question:\n${question}\n\n` +
|
|
95
|
+
`Current proposal:\n${current}${history}\n\n` +
|
|
96
|
+
`Attack the proposal with the strongest objections you can ground in evidence: flaws in ` +
|
|
97
|
+
`reasoning, missing considerations, risks, and concretely better alternatives. Where the ` +
|
|
98
|
+
`question concerns this repository, verify your objections against the actual code — a ` +
|
|
99
|
+
`refuted attack weakens your side. Do not be agreeable, and do not repeat objections that ` +
|
|
100
|
+
`earlier rebuttals already answered; move to stronger ground instead.`,
|
|
101
|
+
{ provider: attacker.provider, model: attacker.model, label: `attack: round ${round}`, phase: "Debate" },
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const rebuttal = await agent(
|
|
105
|
+
`You are the proposer in round ${round} of ${rounds} of a structured debate, defending your proposal.\n\n` +
|
|
106
|
+
`Question:\n${question}\n\n` +
|
|
107
|
+
`Your current proposal:\n${current}${history}\n\n` +
|
|
108
|
+
`New objections:\n${critique}\n\n` +
|
|
109
|
+
`Respond to each objection in turn: defend it with reasoning and evidence (check the code ` +
|
|
110
|
+
`where it applies), or concede it explicitly and revise. Conceding a weak point is better ` +
|
|
111
|
+
`than defending it badly. End with your full current proposal, revised where you conceded.`,
|
|
112
|
+
{ provider: proposer.provider, model: proposer.model, label: `rebut: round ${round}`, phase: "Debate" },
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
transcript.push({ round, critique, rebuttal })
|
|
116
|
+
current = rebuttal
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Verdict — the judge runs on the run-default provider (override with
|
|
121
|
+
// --provider); it rules on the merits, not on who argued better.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
phase("Verdict")
|
|
124
|
+
|
|
125
|
+
const verdict = await agent(
|
|
126
|
+
`You are the judge of a structured debate. Rule on the merits of the arguments, not the rhetoric.\n\n` +
|
|
127
|
+
`Question:\n${question}\n\n` +
|
|
128
|
+
`Initial proposal:\n${proposal}\n\n` +
|
|
129
|
+
`Full debate:\n${transcriptText(transcript)}\n\n` +
|
|
130
|
+
`Where a decisive claim about this repository can be checked, check it yourself rather than ` +
|
|
131
|
+
`taking either side's word. Deliver:\n` +
|
|
132
|
+
`- recommendation: the position the asker should act on (it may differ from both sides),\n` +
|
|
133
|
+
`- survived: the claims that withstood attack,\n` +
|
|
134
|
+
`- conceded: the points the proposer gave up or clearly lost,\n` +
|
|
135
|
+
`- open: unresolved questions worth investigating before committing,\n` +
|
|
136
|
+
`- report: a concise walkthrough of how you weighed it.`,
|
|
137
|
+
{ label: "verdict", phase: "Verdict", schema: VERDICT_SCHEMA },
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return { question, proposer: proposer.name, attacker: attacker.name, proposal, transcript, verdict }
|