@codions/portta 0.1.0 → 0.3.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.
Files changed (88) hide show
  1. package/README.md +41 -88
  2. package/dist/assets/agents/hook.sh.tmpl +21 -0
  3. package/dist/assets/agents/instruction.md +8 -0
  4. package/dist/assets/agents/portta-context.ts.tmpl +24 -0
  5. package/dist/assets/skills/portta-adopt-compose/SKILL.md +220 -0
  6. package/dist/assets/skills/portta-dockerize/SKILL.md +118 -0
  7. package/dist/assets/skills/portta-host-context/SKILL.md +142 -0
  8. package/dist/cli.js +1568 -1017
  9. package/dist/documentation.json.gz +0 -0
  10. package/dist/runtime/.env.example +6 -42
  11. package/dist/runtime/VERSION +1 -1
  12. package/dist/runtime/docker/compose/features/web-dev.yaml +0 -2
  13. package/dist/runtime/docker/compose/features/web.yaml +0 -8
  14. package/dist/runtime/docker/images/apply/Dockerfile +1 -1
  15. package/dist/runtime/docker/images/toolbox/Dockerfile +1 -1
  16. package/dist/runtime/templates/README.md +3 -3
  17. package/dist/runtime/templates/overlays/06-monorepo.yaml +0 -7
  18. package/dist/runtime/templates/overlays/07-worktree.env +0 -1
  19. package/dist/runtime/templates/project/PORTTA.md +8 -7
  20. package/package.json +5 -16
  21. package/dist/acp-providers-M3DUUBYJ.js +0 -3
  22. package/dist/arktype-aI7TBD0R-SOJGD3SZ.js +0 -3
  23. package/dist/assets/skills/portta-workflows/SKILL.md +0 -285
  24. package/dist/assets/workflows/bake-off.workflow.js +0 -172
  25. package/dist/assets/workflows/code-review.workflow.js +0 -179
  26. package/dist/assets/workflows/deep-research.workflow.js +0 -223
  27. package/dist/assets/workflows/multi-provider-review.workflow.js +0 -169
  28. package/dist/assets/workflows/provider-debate.workflow.js +0 -141
  29. package/dist/assets/workflows/second-opinion.workflow.js +0 -176
  30. package/dist/chunk-3EZNE4MC.js +0 -6
  31. package/dist/chunk-4TGTGGMK.js +0 -4
  32. package/dist/chunk-4YGEG6YX.js +0 -1878
  33. package/dist/chunk-5SBYE3YH.js +0 -26
  34. package/dist/chunk-6J5HN4ZN.js +0 -3
  35. package/dist/chunk-6TV2V7RR.js +0 -3
  36. package/dist/chunk-CFWGQ7VJ.js +0 -19
  37. package/dist/chunk-EFZZFIRZ.js +0 -3
  38. package/dist/chunk-EH7H7D7J.js +0 -15
  39. package/dist/chunk-EYT7VTYA.js +0 -4
  40. package/dist/chunk-FRQCKCCK.js +0 -332
  41. package/dist/chunk-GI2EUNAK.js +0 -16
  42. package/dist/chunk-HB5T7XDN.js +0 -5
  43. package/dist/chunk-IILMQFWO.js +0 -14
  44. package/dist/chunk-LAYNTFDR.js +0 -70
  45. package/dist/chunk-MH5YJA5O.js +0 -2
  46. package/dist/chunk-O6U47HHQ.js +0 -341
  47. package/dist/chunk-OPVZAG2C.js +0 -10
  48. package/dist/chunk-PTKKSRMI.js +0 -223
  49. package/dist/chunk-Q3VNQ3LU.js +0 -3
  50. package/dist/chunk-QBKADN76.js +0 -4
  51. package/dist/chunk-QS5PHDYT.js +0 -240
  52. package/dist/chunk-SBVO6RXF.js +0 -87
  53. package/dist/chunk-T3TSZO4I.js +0 -7
  54. package/dist/chunk-TA2XHP5D.js +0 -4
  55. package/dist/chunk-UUSUJUAD.js +0 -7
  56. package/dist/chunk-UX7P3YHF.js +0 -3
  57. package/dist/chunk-VDSWNWUY.js +0 -168
  58. package/dist/chunk-WNZR7VOO.js +0 -145
  59. package/dist/chunk-YYSFNLBX.js +0 -5
  60. package/dist/cli-O2MMC25H.js +0 -98
  61. package/dist/completions-F4476JAU.js +0 -12
  62. package/dist/config-E5XLLPKE.js +0 -3
  63. package/dist/core-FSKGFGJO.js +0 -18
  64. package/dist/default-u_dwuiYb-OWTONHM6.js +0 -3
  65. package/dist/dispatch-ZCNGHCNP.js +0 -8
  66. package/dist/doctor-7F6ZXPZE.js +0 -5
  67. package/dist/effect-QlVUlMFu-4UNP2EGS.js +0 -3
  68. package/dist/esm-575VQW4T.js +0 -4
  69. package/dist/host.js +0 -8
  70. package/dist/init-HGEXUJP3.js +0 -245
  71. package/dist/linear-commands-XGHGICI7.js +0 -5
  72. package/dist/oneshot-DL57ZA55.js +0 -36
  73. package/dist/opencode-YDUTVKTG.js +0 -3
  74. package/dist/pi-L73UX3S5.js +0 -3
  75. package/dist/project-commands-2N44CRGM.js +0 -7
  76. package/dist/project-env-NWBVITHM.js +0 -4
  77. package/dist/run-commands-TCBNFJOT.js +0 -13
  78. package/dist/runtime/docker/compose/features/panel-host.yaml +0 -42
  79. package/dist/shell-ZWXPCUMD.js +0 -3
  80. package/dist/subprocess-jsonl-FQKDRENI.js +0 -3
  81. package/dist/supervisor.js +0 -179
  82. package/dist/sury-CWZTCd75-XQOYZHNU.js +0 -3
  83. package/dist/typebox-Dei93FPO-RCGWIHUQ.js +0 -3
  84. package/dist/valibot--1zFm7rT-DKLMYF44.js +0 -3
  85. package/dist/valibot-D_HTw1Gn-QTR63R64.js +0 -4
  86. package/dist/worktree-commands-F7REOKFJ.js +0 -50
  87. package/dist/zod-Bwrt9trS-6RSVELLV.js +0 -3
  88. package/dist/zod-DSgpEGAE-5JBFOUHY.js +0 -3
@@ -1,172 +0,0 @@
1
- // Bake-off: both providers implement the same task in isolated fork leases, then blind judges
2
- // from both providers score the two diffs (neutral A/B labels, shuffled per run, so neither judge
3
- // knows whose work is whose), a tie-break settles splits, and the closing report says what to
4
- // graft from the loser. Both worktree branches are preserved for the user.
5
- // Run with: portta flow run workflow bake-off --input '<task>'
6
- // or: portta flow workflows run bake-off --args '"<task>"' (or --args '{"task": "..."}')
7
-
8
- export const meta = {
9
- name: "bake-off",
10
- description:
11
- "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.",
12
- defaultSandbox: "read-only",
13
- workspace: {
14
- default: "current_branch",
15
- allowed: ["isolated_worktree", "new_branch", "current_branch"],
16
- mutatesRepository: true,
17
- reason: "The checkout is only the stable base; Taskflow creates one isolated child worktree for each contender.",
18
- },
19
- phases: [
20
- { title: "Implement", detail: "each provider builds the task in its own git worktree" },
21
- { title: "Judge", detail: "blind A/B scoring by one judge per provider; tie-break on splits" },
22
- { title: "Report", detail: "verdict, preserved branches, what to graft from the loser" },
23
- ],
24
- }
25
-
26
- const CONTENDERS = [
27
- { provider: "codex", model: "gpt-5.5", name: "Codex" },
28
- { provider: "claude-code", model: "claude-fable-5", name: "Claude" },
29
- ]
30
-
31
- const task =
32
- typeof args === "string" && args.trim()
33
- ? args.trim()
34
- : args && typeof args === "object" && typeof args.task === "string" && args.task.trim()
35
- ? args.task.trim()
36
- : null
37
- if (!task) throw new Error(`bake-off needs a task — pass --args '"<task>"' or --args '{"task": "..."}'`)
38
-
39
- // Neutral, unique-per-run branch names (now() is resume-stable) with a shuffled provider→slot
40
- // mapping, so the judges' A/B labels carry no provider hint. Best-effort blinding: the diff
41
- // itself can still hint at a model's style, so judges are told to score only the code.
42
- const stamp = now().toString(36)
43
- const shuffled = random() < 0.5 ? [CONTENDERS[1], CONTENDERS[0]] : [...CONTENDERS]
44
- const slots = shuffled.map((c, i) => ({
45
- ...c,
46
- slot: i === 0 ? "A" : "B",
47
- branch: `bake-off/${stamp}-${i === 0 ? "a" : "b"}`,
48
- }))
49
-
50
- const IMPL_SCHEMA = {
51
- type: "object",
52
- required: ["approach", "tested"],
53
- properties: { approach: { type: "string" }, tested: { type: "string" } },
54
- }
55
-
56
- const JUDGE_SCHEMA = {
57
- type: "object",
58
- required: ["winner", "scoreA", "scoreB", "reasoning"],
59
- properties: {
60
- winner: { type: "string", enum: ["A", "B", "tie"] },
61
- scoreA: { type: "number" },
62
- scoreB: { type: "number" },
63
- reasoning: { type: "string" },
64
- },
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Implement — same prompt, separate worktrees. Committing is mandatory: the
69
- // judges read each entry as `git diff HEAD...<branch>`, so uncommitted work
70
- // is invisible to them (and an untouched worktree is auto-removed).
71
- // ---------------------------------------------------------------------------
72
- phase("Implement")
73
- log(`bake-off: ${task}`)
74
-
75
- const built = (
76
- await parallel(
77
- slots.map((s) => () =>
78
- agent(
79
- `Implement the following task in this repository, completely:\n\n${task}\n\n` +
80
- `Match the existing code style and conventions. Run the project's relevant tests or build ` +
81
- `if available and make them pass. COMMIT all of your work to the current branch with clear ` +
82
- `messages — uncommitted changes will not be judged. Do not push.\n\n` +
83
- `When done, summarize your approach and exactly what you verified (tests run, manual checks).`,
84
- { provider: s.provider, model: s.model, label: `implement: ${s.name}`, phase: "Implement", worktree: s.branch, schema: IMPL_SCHEMA },
85
- ).then((r) => ({ ...s, approach: r.approach, tested: r.tested })),
86
- ),
87
- )
88
- ).filter(Boolean)
89
-
90
- if (built.length === 0) throw new Error("both implementations failed — nothing to judge")
91
-
92
- // ---------------------------------------------------------------------------
93
- // Judge — one judge per provider scores both diffs blind. Agreement on a
94
- // non-tie winner decides it; a split or all-tie goes to a tie-break judge
95
- // who also sees the first judges' reasoning.
96
- // ---------------------------------------------------------------------------
97
- let verdicts = []
98
- let winnerSlot = null
99
-
100
- if (built.length === 1) {
101
- winnerSlot = built[0].slot
102
- log(`only ${built[0].name} finished — wins by default`)
103
- } else {
104
- phase("Judge")
105
- const [a, b] = built[0].slot === "A" ? built : [built[1], built[0]]
106
- const judgePrompt =
107
- `Two competing implementations of the same task exist as branches in this repository.\n\n` +
108
- `Task:\n${task}\n\n` +
109
- `Implementation A: branch ${a.branch}\nImplementation B: branch ${b.branch}\n\n` +
110
- `Review each with \`git diff HEAD...<branch>\` and read enough surrounding code to judge it in ` +
111
- `context. Judge ONLY the code — ignore commit authorship, message style, and anything else ` +
112
- `that hints at who wrote it. Score each implementation 0-10 weighing correctness, ` +
113
- `completeness against the task, code quality, and test coverage, then pick a winner — "tie" ` +
114
- `only when they are genuinely inseparable. Be specific about what decided it.`
115
-
116
- verdicts = (
117
- await parallel(
118
- CONTENDERS.map((j) => () =>
119
- agent(judgePrompt, { provider: j.provider, model: j.model, label: `judge: ${j.name}`, phase: "Judge", schema: JUDGE_SCHEMA }).then(
120
- (v) => ({ judge: j.name, ...v }),
121
- ),
122
- ),
123
- )
124
- ).filter(Boolean)
125
-
126
- const calls = verdicts.map((v) => v.winner).filter((w) => w !== "tie")
127
- if (verdicts.length > 0 && calls.length > 0 && new Set(calls).size === 1) {
128
- winnerSlot = calls[0]
129
- } else if (verdicts.length === 0) {
130
- log("both judges failed — reporting without a verdict")
131
- } else {
132
- log(`judges split (${verdicts.map((v) => `${v.judge}: ${v.winner}`).join(", ")}) — tie-break`)
133
- const tiebreak = await agent(
134
- `${judgePrompt}\n\nTwo judges already scored these and could not agree:\n` +
135
- `${JSON.stringify(verdicts.map((v) => ({ winner: v.winner, scoreA: v.scoreA, scoreB: v.scoreB, reasoning: v.reasoning })), null, 2)}\n\n` +
136
- `You are the tie-break. Weigh their reasoning against the actual diffs and make the final ` +
137
- `call — "tie" only as a last resort.`,
138
- { label: "tie-break", phase: "Judge", effort: "xhigh", schema: JUDGE_SCHEMA },
139
- )
140
- verdicts.push({ judge: "tie-break", ...tiebreak })
141
- winnerSlot = tiebreak.winner === "tie" ? null : tiebreak.winner
142
- }
143
- }
144
-
145
- // ---------------------------------------------------------------------------
146
- // Report — the A/B mapping is revealed only here, after all judging is done.
147
- // ---------------------------------------------------------------------------
148
- phase("Report")
149
-
150
- const winner = winnerSlot ? built.find((c) => c.slot === winnerSlot) : null
151
- log(winner ? `winner: ${winner.name} (${winner.branch})` : "verdict: tie")
152
-
153
- const report = await agent(
154
- `Write the closing report for an implementation bake-off.\n\n` +
155
- `Task:\n${task}\n\n` +
156
- `Contenders (the judges saw only the A/B labels):\n` +
157
- `${JSON.stringify(built.map((c) => ({ slot: c.slot, provider: c.name, branch: c.branch, approach: c.approach, tested: c.tested })), null, 2)}\n\n` +
158
- `Judge verdicts:\n${JSON.stringify(verdicts, null, 2)}\n\n` +
159
- `Verdict: ${winner ? `${winner.name} (slot ${winner.slot}) won` : "tie / no verdict"}.\n\n` +
160
- `Cover: the verdict and the decisive reasons; concrete strengths of the winner; anything in the ` +
161
- `other implementation worth grafting — read its diff (git diff HEAD...<branch>) and name ` +
162
- `specific files or ideas; and exact next steps (git commands to inspect each branch, merge the ` +
163
- `winner, cherry-pick grafts). Both worktree branches are preserved.`,
164
- { label: "report", phase: "Report" },
165
- )
166
-
167
- return {
168
- winner: winner ? winner.name : "tie",
169
- branches: Object.fromEntries(built.map((c) => [c.name, c.branch])),
170
- judges: verdicts,
171
- report,
172
- }
@@ -1,179 +0,0 @@
1
- // Code review: one finder agent per review angle, an independent verifier per candidate finding
2
- // (CONFIRMED / PLAUSIBLE / REFUTED), a gap-sweep at the higher levels, then a ranked, capped report.
3
- // Run with: portta flow run workflow code-review [--input-json '{"target": "<ref|path|diff>", "level": "high|xhigh|max"}']
4
- // or: 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 }
@@ -1,223 +0,0 @@
1
- // Deep research: fan out web searches, deep-read the best sources, adversarially verify every
2
- // claim with a 3-vote panel, synthesize a cited report.
3
- // Run with: portta flow run workflow deep-research --input '<question>'
4
- // or: portta flow workflows run deep-research --args '"<question>"'
5
-
6
- export const meta = {
7
- name: "deep-research",
8
- description:
9
- "Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report.",
10
- defaultSandbox: "read-only",
11
- workspace: {
12
- default: "current_branch",
13
- allowed: ["isolated_worktree", "new_branch", "current_branch"],
14
- mutatesRepository: false,
15
- reason: "Research is read-only and normally does not need an isolated Git worktree.",
16
- },
17
- phases: [
18
- { title: "Scope", detail: "break the question into distinct search directives" },
19
- { title: "Search", detail: "5 parallel web-search agents" },
20
- { title: "Fetch", detail: "dedup URLs, deep-read the top sources" },
21
- { title: "Verify", detail: "3-vote adversarial panel per claim" },
22
- { title: "Synthesize", detail: "cited report from surviving claims" },
23
- ],
24
- }
25
-
26
- const SEARCHERS = 5
27
- const VOTES_PER_CLAIM = 3
28
- const REFUTATIONS_REQUIRED = 2 // 2 of 3 refute votes kill a claim
29
- const MAX_FETCH = 15
30
- const MAX_VERIFY_CLAIMS = 25
31
-
32
- const question =
33
- typeof args === "string" && args.trim()
34
- ? args.trim()
35
- : args && typeof args === "object" && typeof args.question === "string" && args.question.trim()
36
- ? args.question.trim()
37
- : null
38
- if (!question) {
39
- throw new Error(`deep-research needs a question — pass --args '"<question>"' or --args '{"question": "..."}'`)
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // Scope — turn the question into SEARCHERS distinct search directives.
44
- // ---------------------------------------------------------------------------
45
- phase("Scope")
46
- log(`scoping: ${question}`)
47
-
48
- const scope = await agent(
49
- `You are scoping a deep web-research project for this question:\n\n${question}\n\n` +
50
- `Produce exactly ${SEARCHERS} distinct search directives. Each directive is a short instruction ` +
51
- `for an independent researcher with web search: what to search for and what kind of sources to ` +
52
- `prefer (primary data, official docs, expert analysis, recent news, contrarian takes, ...). ` +
53
- `The directives must not overlap — together they should cover the question from genuinely ` +
54
- `different angles (different sub-questions, source types, or time windows).`,
55
- {
56
- label: "scope directives",
57
- schema: {
58
- type: "object",
59
- required: ["directives"],
60
- properties: {
61
- directives: { type: "array", minItems: SEARCHERS, maxItems: SEARCHERS, items: { type: "string" } },
62
- },
63
- },
64
- },
65
- )
66
-
67
- // ---------------------------------------------------------------------------
68
- // Search — one web-search agent per directive, in parallel. Barrier: the Fetch
69
- // stage dedups URLs ACROSS all searchers before spending deep-read agents.
70
- // ---------------------------------------------------------------------------
71
- phase("Search")
72
-
73
- const searches = await parallel(
74
- scope.directives.map((directive, i) => () =>
75
- agent(
76
- `Research question: ${question}\n\nYour directive (#${i + 1} of ${SEARCHERS}):\n${directive}\n\n` +
77
- `Use your web search tool to find the most authoritative sources bearing on this directive. ` +
78
- `Return up to 6 sources. For each give the exact URL, its title, and a relevance score 1-5 ` +
79
- `(5 = directly answers the question with primary evidence). Only return URLs you actually ` +
80
- `found via search — never construct or guess a URL.`,
81
- {
82
- label: `search ${i + 1}: ${directive.slice(0, 40)}`,
83
- schema: {
84
- type: "object",
85
- required: ["sources"],
86
- properties: {
87
- sources: {
88
- type: "array",
89
- items: {
90
- type: "object",
91
- required: ["url", "title", "relevance"],
92
- properties: {
93
- url: { type: "string" },
94
- title: { type: "string" },
95
- relevance: { type: "number" },
96
- },
97
- },
98
- },
99
- },
100
- },
101
- },
102
- ),
103
- ),
104
- )
105
-
106
- // Dedup by URL (highest relevance wins), rank, keep the top MAX_FETCH.
107
- const byUrl = new Map()
108
- for (const s of searches.filter(Boolean).flatMap((r) => r.sources || [])) {
109
- if (!s || !s.url) continue
110
- const prev = byUrl.get(s.url)
111
- if (!prev || s.relevance > prev.relevance) byUrl.set(s.url, s)
112
- }
113
- const ranked = [...byUrl.values()].sort((a, b) => b.relevance - a.relevance)
114
- const sources = ranked.slice(0, MAX_FETCH)
115
- if (ranked.length > sources.length) log(`dropping ${ranked.length - sources.length} lower-relevance sources (cap ${MAX_FETCH})`)
116
- log(`${sources.length} unique sources to read`)
117
-
118
- // ---------------------------------------------------------------------------
119
- // Fetch — deep-read each source and extract concrete, checkable claims.
120
- // ---------------------------------------------------------------------------
121
- phase("Fetch")
122
-
123
- const fetched = await parallel(
124
- sources.map((src) => () =>
125
- agent(
126
- `Research question: ${question}\n\nFetch and read this source:\n${src.title}\n${src.url}\n\n` +
127
- `Extract the concrete, checkable claims it makes that bear on the research question — facts, ` +
128
- `numbers, findings, dated events. For each claim include a short supporting quote from the ` +
129
- `source. Skip opinions, marketing, and anything the source merely speculates about. If the ` +
130
- `page is unreachable or irrelevant, return an empty list.`,
131
- {
132
- label: `fetch: ${src.title.slice(0, 40)}`,
133
- schema: {
134
- type: "object",
135
- required: ["claims"],
136
- properties: {
137
- claims: {
138
- type: "array",
139
- items: {
140
- type: "object",
141
- required: ["claim", "quote"],
142
- properties: { claim: { type: "string" }, quote: { type: "string" } },
143
- },
144
- },
145
- },
146
- },
147
- },
148
- ).then((r) => (r.claims || []).map((c) => ({ ...c, url: src.url, title: src.title }))),
149
- ),
150
- )
151
-
152
- const allClaims = fetched.filter(Boolean).flat()
153
- const claims = allClaims.slice(0, MAX_VERIFY_CLAIMS)
154
- if (allClaims.length > claims.length) log(`verifying first ${MAX_VERIFY_CLAIMS} of ${allClaims.length} claims (cap)`)
155
- log(`${claims.length} claims to verify`)
156
-
157
- // ---------------------------------------------------------------------------
158
- // Verify — a 3-vote adversarial panel per claim; 2 of 3 refutes kill it. Each
159
- // voter gets a different lens so the panel's failure modes are decorrelated.
160
- // ---------------------------------------------------------------------------
161
- phase("Verify")
162
-
163
- const LENSES = [
164
- "Check the claim against the quoted text: does the source actually say this, or is it an overreach/misreading?",
165
- "Check the claim against your own knowledge and a quick independent web search: is it contradicted elsewhere?",
166
- "Check the claim's precision: are the numbers, dates, and attributions right, or materially misleading as stated?",
167
- ]
168
-
169
- const verified = await pipeline(claims, (claim) =>
170
- parallel(
171
- LENSES.slice(0, VOTES_PER_CLAIM).map((lens, v) => () =>
172
- agent(
173
- `You are a skeptical fact-checker. Your job is to try to REFUTE this claim.\n\n` +
174
- `Claim: ${claim.claim}\n` +
175
- `Source: ${claim.title} — ${claim.url}\n` +
176
- `Supporting quote: "${claim.quote}"\n\n` +
177
- `${lens}\n\n` +
178
- `Vote refuted=true if the claim is false, unsupported, or materially misleading; when ` +
179
- `genuinely uncertain, default to refuted=true.`,
180
- {
181
- label: `verify ${v + 1}/${VOTES_PER_CLAIM}: ${claim.claim.slice(0, 32)}`,
182
- schema: {
183
- type: "object",
184
- required: ["refuted", "reason"],
185
- properties: { refuted: { type: "boolean" }, reason: { type: "string" } },
186
- },
187
- },
188
- ),
189
- ),
190
- ).then((votes) => {
191
- const cast = votes.filter(Boolean)
192
- const refutes = cast.filter((v) => v.refuted).length
193
- // A skipped/errored vote counts as a refute: a claim must EARN its place in the report.
194
- const killed = refutes + (VOTES_PER_CLAIM - cast.length) >= REFUTATIONS_REQUIRED
195
- return killed ? null : { ...claim, refutes, votes: VOTES_PER_CLAIM }
196
- }),
197
- )
198
-
199
- const survivors = verified.filter(Boolean)
200
- log(`${survivors.length} of ${claims.length} claims survived the panel`)
201
-
202
- // ---------------------------------------------------------------------------
203
- // Synthesize — a cited report from the surviving claims only.
204
- // ---------------------------------------------------------------------------
205
- phase("Synthesize")
206
-
207
- if (survivors.length === 0) {
208
- return (
209
- `# Research report: ${question}\n\n` +
210
- `No claims survived adversarial verification (${claims.length} candidates, ${VOTES_PER_CLAIM}-vote panel, ` +
211
- `${REFUTATIONS_REQUIRED} refutes to kill). The available sourcing was too weak to answer confidently.`
212
- )
213
- }
214
-
215
- return await agent(
216
- `Write a well-structured research report answering:\n\n${question}\n\n` +
217
- `Use ONLY the verified claims below — every factual statement must trace to one of them, cited ` +
218
- `inline as a markdown link to its source URL. Group related claims into sections, lead with a ` +
219
- `direct answer, and close with what remains uncertain (claims that were killed or angles that ` +
220
- `found nothing). Do not introduce facts that are not in the claims.\n\n` +
221
- `Verified claims (JSON):\n${JSON.stringify(survivors, null, 2)}`,
222
- { label: "synthesize report" },
223
- )