@codions/portta 0.1.0 → 0.2.0

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 (86) 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 +1542 -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/templates/README.md +3 -3
  15. package/dist/runtime/templates/overlays/06-monorepo.yaml +0 -7
  16. package/dist/runtime/templates/overlays/07-worktree.env +0 -1
  17. package/dist/runtime/templates/project/PORTTA.md +8 -7
  18. package/package.json +4 -15
  19. package/dist/acp-providers-M3DUUBYJ.js +0 -3
  20. package/dist/arktype-aI7TBD0R-SOJGD3SZ.js +0 -3
  21. package/dist/assets/skills/portta-workflows/SKILL.md +0 -285
  22. package/dist/assets/workflows/bake-off.workflow.js +0 -172
  23. package/dist/assets/workflows/code-review.workflow.js +0 -179
  24. package/dist/assets/workflows/deep-research.workflow.js +0 -223
  25. package/dist/assets/workflows/multi-provider-review.workflow.js +0 -169
  26. package/dist/assets/workflows/provider-debate.workflow.js +0 -141
  27. package/dist/assets/workflows/second-opinion.workflow.js +0 -176
  28. package/dist/chunk-3EZNE4MC.js +0 -6
  29. package/dist/chunk-4TGTGGMK.js +0 -4
  30. package/dist/chunk-4YGEG6YX.js +0 -1878
  31. package/dist/chunk-5SBYE3YH.js +0 -26
  32. package/dist/chunk-6J5HN4ZN.js +0 -3
  33. package/dist/chunk-6TV2V7RR.js +0 -3
  34. package/dist/chunk-CFWGQ7VJ.js +0 -19
  35. package/dist/chunk-EFZZFIRZ.js +0 -3
  36. package/dist/chunk-EH7H7D7J.js +0 -15
  37. package/dist/chunk-EYT7VTYA.js +0 -4
  38. package/dist/chunk-FRQCKCCK.js +0 -332
  39. package/dist/chunk-GI2EUNAK.js +0 -16
  40. package/dist/chunk-HB5T7XDN.js +0 -5
  41. package/dist/chunk-IILMQFWO.js +0 -14
  42. package/dist/chunk-LAYNTFDR.js +0 -70
  43. package/dist/chunk-MH5YJA5O.js +0 -2
  44. package/dist/chunk-O6U47HHQ.js +0 -341
  45. package/dist/chunk-OPVZAG2C.js +0 -10
  46. package/dist/chunk-PTKKSRMI.js +0 -223
  47. package/dist/chunk-Q3VNQ3LU.js +0 -3
  48. package/dist/chunk-QBKADN76.js +0 -4
  49. package/dist/chunk-QS5PHDYT.js +0 -240
  50. package/dist/chunk-SBVO6RXF.js +0 -87
  51. package/dist/chunk-T3TSZO4I.js +0 -7
  52. package/dist/chunk-TA2XHP5D.js +0 -4
  53. package/dist/chunk-UUSUJUAD.js +0 -7
  54. package/dist/chunk-UX7P3YHF.js +0 -3
  55. package/dist/chunk-VDSWNWUY.js +0 -168
  56. package/dist/chunk-WNZR7VOO.js +0 -145
  57. package/dist/chunk-YYSFNLBX.js +0 -5
  58. package/dist/cli-O2MMC25H.js +0 -98
  59. package/dist/completions-F4476JAU.js +0 -12
  60. package/dist/config-E5XLLPKE.js +0 -3
  61. package/dist/core-FSKGFGJO.js +0 -18
  62. package/dist/default-u_dwuiYb-OWTONHM6.js +0 -3
  63. package/dist/dispatch-ZCNGHCNP.js +0 -8
  64. package/dist/doctor-7F6ZXPZE.js +0 -5
  65. package/dist/effect-QlVUlMFu-4UNP2EGS.js +0 -3
  66. package/dist/esm-575VQW4T.js +0 -4
  67. package/dist/host.js +0 -8
  68. package/dist/init-HGEXUJP3.js +0 -245
  69. package/dist/linear-commands-XGHGICI7.js +0 -5
  70. package/dist/oneshot-DL57ZA55.js +0 -36
  71. package/dist/opencode-YDUTVKTG.js +0 -3
  72. package/dist/pi-L73UX3S5.js +0 -3
  73. package/dist/project-commands-2N44CRGM.js +0 -7
  74. package/dist/project-env-NWBVITHM.js +0 -4
  75. package/dist/run-commands-TCBNFJOT.js +0 -13
  76. package/dist/runtime/docker/compose/features/panel-host.yaml +0 -42
  77. package/dist/shell-ZWXPCUMD.js +0 -3
  78. package/dist/subprocess-jsonl-FQKDRENI.js +0 -3
  79. package/dist/supervisor.js +0 -179
  80. package/dist/sury-CWZTCd75-XQOYZHNU.js +0 -3
  81. package/dist/typebox-Dei93FPO-RCGWIHUQ.js +0 -3
  82. package/dist/valibot--1zFm7rT-DKLMYF44.js +0 -3
  83. package/dist/valibot-D_HTw1Gn-QTR63R64.js +0 -4
  84. package/dist/worktree-commands-F7REOKFJ.js +0 -50
  85. package/dist/zod-Bwrt9trS-6RSVELLV.js +0 -3
  86. package/dist/zod-DSgpEGAE-5JBFOUHY.js +0 -3
Binary file
@@ -234,12 +234,6 @@ PORTTA_RUNTIME_DOCS=true
234
234
  # always available.
235
235
  PORTTA_RUNTIME_API_DOCS=
236
236
 
237
- # The MCP tools over HTTP, at POST /api/mcp, for an agent that has no CLI where
238
- # it runs (a Dev Container, a remote worktree, a cloud runner). The same tools
239
- # `portta mcp` serves, behind the same credential and the same agent ceiling.
240
- # Off by default: it is reachable from wherever the panel is.
241
- PORTTA_MCP_HTTP=false
242
-
243
237
  # Panel persistence: one SQLite file under this installation, at
244
238
  # `state/panel/portta.db`. There is nothing to configure, no password to
245
239
  # generate and no service to start — the file is created on first open
@@ -313,25 +307,12 @@ PORTTA_APPLY=true
313
307
  # See docs/development/adr/0030-the-panel-and-a-project-lifecycle.md.
314
308
  PORTTA_RUNNER=true
315
309
 
316
- # ----------------------------------------------------------------------------
317
- # The host daemon
318
- # ----------------------------------------------------------------------------
319
- # Work that needs the host -- issues through `gh` and Linear, and Taskflow's
320
- # git worktrees, tmux and agent CLIs -- runs in `portta host serve`, a daemon on
321
- # the host. Whenever the panel is on, it gets a name for the host and the
322
- # daemon's token (state/host/token), read-only, and reaches the daemon through
323
- # an authorised proxy.
324
- #
325
- # The daemon binds loopback by default, which a panel container reaches on
326
- # Docker Desktop. On Linux, bind it to the Docker bridge gateway instead, for
327
- # example PORTTA_HOST_BIND=172.17.0.1, and never to 0.0.0.0.
328
- # See docs/product/guides/host-daemon.md.
329
- PORTTA_HOST_BIND=127.0.0.1
330
- PORTTA_HOST_PORT=5111
331
-
332
- # Where the panel reaches the daemon. Empty means http://host.docker.internal on
333
- # PORTTA_HOST_PORT.
334
- PORTTA_HOST_URL=
310
+ # On a public host (PORTTA_PROFILE=remote-public) the runner stays off the panel
311
+ # unless this is true AND the panel is on its own domain (PORTTA_WEB_EXPOSE=domain),
312
+ # sign-in is required (PORTTA_AUTH_MODE=required) and TLS is on (TLS_ENABLED=true).
313
+ # Like PORTTA_RUNNER it is absent from the Settings catalogue: only an edit on the
314
+ # host can change it, so a panel cannot release itself.
315
+ # PORTTA_RUNNER_PUBLIC=true
335
316
 
336
317
  # ----------------------------------------------------------------------------
337
318
  # TLS
@@ -422,23 +403,6 @@ CLOUDFLARE_ENABLED=false
422
403
  CF_DNS_API_TOKEN=
423
404
  CLOUDFLARE_ZONE=
424
405
 
425
- # ----------------------------------------------------------------------------
426
- # Issues: GitHub through `gh`, and Linear
427
- # ----------------------------------------------------------------------------
428
- # Nothing is configured here, and that is the point.
429
- #
430
- # The panel reads and writes issues through the host daemon, which runs the
431
- # GitHub CLI as whoever is signed in on the host — so the credential is `gh
432
- # auth`'s, kept where `gh` keeps it, and never in this file or in the panel's
433
- # container (docs/development/adr/0018-github-issues-through-the-gh-cli.md).
434
- #
435
- # gh auth login once, on the host
436
- # portta host serve the daemon the panel reaches it through
437
- #
438
- # Linear is optional and works the same way: export LINEAR_API_KEY in the
439
- # environment that runs `portta host serve`. It is deliberately not a variable
440
- # here, because this file is one the panel itself can write.
441
-
442
406
  # ----------------------------------------------------------------------------
443
407
  # Cloudflare Tunnel: optional connector (credential stays in state/cloudflared)
444
408
  # ----------------------------------------------------------------------------
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -30,7 +30,6 @@ services:
30
30
  - ./apps/web/app:/app/apps/web/app
31
31
  - ./apps/web/components:/app/apps/web/components
32
32
  - ./apps/web/lib:/app/apps/web/lib
33
- - ./apps/web/modules:/app/apps/web/modules
34
33
  - ./apps/web/messages:/app/apps/web/messages
35
34
  - ./apps/web/server:/app/apps/web/server
36
35
  - ./apps/web/public:/app/apps/web/public
@@ -39,7 +38,6 @@ services:
39
38
  # services and API.
40
39
  - ./packages/core/src:/app/packages/core/src
41
40
  - ./packages/contracts/src:/app/packages/contracts/src
42
- - ./packages/mcp/src:/app/packages/mcp/src
43
41
  - ./packages/auth/src:/app/packages/auth/src
44
42
  - ./packages/db/src:/app/packages/db/src
45
43
  # The generated migrations the process applies on the next migrate.
@@ -136,9 +136,6 @@ services:
136
136
  # while the panel stays on loopback.
137
137
  PORTTA_RUNTIME_DOCS: ${PORTTA_RUNTIME_DOCS:-true}
138
138
  PORTTA_RUNTIME_API_DOCS: ${PORTTA_RUNTIME_API_DOCS:-}
139
- # The MCP tools over HTTP at /api/mcp, off unless the operator turned
140
- # them on: the endpoint follows the panel's own exposure.
141
- PORTTA_MCP_HTTP: ${PORTTA_MCP_HTTP:-false}
142
139
  # The resolved gateway configuration, so the panel and the CLI always
143
140
  # describe the same gateway.
144
141
  PORTTA_PROFILE: ${PORTTA_PROFILE:-local}
@@ -201,7 +198,6 @@ services:
201
198
  PORTTA_RUNTIME_AUTH_STORE: /app/state/auth/protections.json
202
199
  PORTTA_RUNTIME_RUNNER_DIR: /app/state/runner
203
200
  PORTTA_RUNTIME_ACCESS_DIR: /app/state/access
204
- PORTTA_RUNTIME_SSH_DIR: /app/state/ssh
205
201
  PORTTA_PANEL_ADVERTISED_HOST: ${PORTTA_PANEL_ADVERTISED_HOST:-}
206
202
  TLS_ENABLED: ${TLS_ENABLED:-false}
207
203
  TLS_MODE: ${TLS_MODE:-local}
@@ -220,14 +216,12 @@ services:
220
216
  # mounted here: see docs/development/adr/0001-decoupled-infrastructure.md.
221
217
  #
222
218
  # .env the configuration the Settings page edits
223
- # state/git metadata `portta repos scan` collected, read-only
224
219
  # state/metrics current.json + history from `portta host watch`
225
220
  # state/environment host readiness report from the same collector
226
221
  # state/runner the `{ verb, project }` request the runner reads
227
222
  # state/access tunnel records cleaned when a project is removed
228
223
  # state/panel the panel's SQLite database, read-write and its own
229
224
  # state/auth credentials the panel manages, never served directly
230
- # state/ssh SSH credentials the panel manages, never served directly
231
225
  # traefik/dynamic read-write, and the ONE place the panel configures
232
226
  # Traefik. It may write three generated filenames there
233
227
  # and refuses every other path in its own process.
@@ -235,7 +229,6 @@ services:
235
229
  - ./.env:/app/state/.env
236
230
  - ./.env.example:/app/state/.env.example:ro
237
231
  - ./.env-lock:/app/state/.env-lock
238
- - ./state/git:/app/state/git:ro
239
232
  - ./state/metrics:/app/state/metrics:ro
240
233
  - ./state/environment:/app/state/environment:ro
241
234
  - ./state/runner:/app/state/runner
@@ -246,7 +239,6 @@ services:
246
239
  # container, which is how a restart loses the most recent writes.
247
240
  - ./state/panel:/app/state/panel
248
241
  - ./state/auth:/app/state/auth
249
- - ./state/ssh:/app/state/ssh
250
242
  - ./config/traefik/dynamic:/app/state/traefik-dynamic
251
243
  # The Cloudflare Tunnel connector's generated config and its credential.
252
244
  # Read-write because setting the tunnel up is something the panel is asked
@@ -1,15 +1,15 @@
1
1
  # Templates
2
2
 
3
3
  Reference overlays for the shapes a project usually has, for **manual mode**:
4
- a project that owns its own `compose.portta.yaml` instead of the overlay
4
+ a project that writes its own overlay instead of the one
5
5
  `portta adopt` generates (see
6
6
  [Add an existing project](../docs/product/guides/adopting-projects.md#manual-mode)).
7
7
  They are **references to copy from**, not runtime that projects depend on:
8
8
  nothing here is included, mounted or extended by a consumer project. Copy the
9
9
  closest one, adjust the service names and ports, and it is yours.
10
10
 
11
- `portta envs init <path>` generates the same thing from a project's actual
12
- Compose file, which is usually faster.
11
+ `portta adopt <path> --dry-run` prints the plan Portta would generate from a
12
+ project's actual Compose file, which is usually faster.
13
13
 
14
14
  | Template | Shape |
15
15
  |---|---|
@@ -29,13 +29,6 @@ services:
29
29
  web:
30
30
  networks: [default, portta]
31
31
  labels:
32
- # Optional, and only useful because inference cannot get them right here:
33
- # `portta.git.root` says where the repository actually starts when
34
- # the Compose file is not at its root, and `portta.repo` gives the
35
- # panel a repository link with no host-side Git at all. Delete both and
36
- # everything behaves exactly as it does now.
37
- # - "portta.repo=owner/monorepo"
38
- # - "portta.git.root=/srv/dev/monorepo"
39
32
  - "traefik.enable=true"
40
33
  - "traefik.docker.network=${PORTTA_NETWORK:-portta}"
41
34
  - "traefik.http.services.${COMPOSE_PROJECT_NAME}-web.loadbalancer.server.port=3000"
@@ -36,7 +36,6 @@ PORTTA_NETWORK=portta
36
36
  #
37
37
  # labels:
38
38
  # - "portta.project=base-empresarial"
39
- # - "portta.repo=owner/base-empresarial"
40
39
 
41
40
  # ---------------------------------------------------------------------------
42
41
  # What must NOT be shared with the other worktree
@@ -2,24 +2,25 @@
2
2
 
3
3
  > Copy this file into the project and adjust the names. It covers only what
4
4
  > someone working on *this* project needs. The rules themselves live in the
5
- > Portta repository, which stays the single source of truth.
5
+ > source repository of Portta, which stays the single source of truth.
6
6
 
7
7
  ## Prerequisites
8
8
 
9
9
  Portta must be running on this machine:
10
10
 
11
11
  ```bash
12
- portta status # if this fails: portta bootstrap && portta up local
12
+ portta status # if this fails: portta setup
13
13
  ```
14
14
 
15
15
  ## Starting
16
16
 
17
17
  ```bash
18
- docker compose -f compose.yaml -f compose.portta.yaml up -d
18
+ portta adopt . # once: records the plan outside this repository
19
+ portta runtime up .
19
20
  ```
20
21
 
21
- Set `COMPOSE_FILE=compose.yaml:compose.portta.yaml` in `.env` to drop the
22
- `-f` flags.
22
+ Portta keeps the generated overlay with its own state, so this repository has
23
+ no `compose.portta.yaml` and its Compose file is never edited.
23
24
 
24
25
  ## Namespace
25
26
 
@@ -73,8 +74,8 @@ Credentials are this project's, from its own `.env`.
73
74
  ```bash
74
75
  git worktree add --relative-paths ../<project>-issue59 issue59
75
76
  cd ../<project>-issue59
76
- echo "COMPOSE_PROJECT_NAME=<project>-issue59" >> .env
77
- docker compose -f compose.yaml -f compose.portta.yaml up -d
77
+ portta adopt . # the branch gives it its own namespace and URL
78
+ portta runtime up .
78
79
  # -> http://<project>-issue59-web.localhost
79
80
  ```
80
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codions/portta",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Machine-first CLI for a shared Docker development gateway",
5
5
  "keywords": [
6
6
  "docker",
@@ -9,8 +9,7 @@
9
9
  "development",
10
10
  "gateway",
11
11
  "devcontainer",
12
- "reverse-proxy",
13
- "mcp"
12
+ "reverse-proxy"
14
13
  ],
15
14
  "license": "MIT",
16
15
  "type": "module",
@@ -42,30 +41,20 @@
42
41
  "homepage": "https://github.com/codions-labs/portta#readme",
43
42
  "bugs": "https://github.com/codions-labs/portta/issues",
44
43
  "dependencies": {
45
- "@agentclientprotocol/claude-agent-acp": "^0.76.0",
46
- "@agentclientprotocol/codex-acp": "^1.11.0",
47
- "@anthropic-ai/claude-agent-sdk": "^0.3.163",
48
44
  "@clack/prompts": "^1.1.0",
49
- "@devcontainers/cli": "0.89.0",
50
- "@modelcontextprotocol/sdk": "^1.30.0",
51
- "better-sqlite3": "^12.11.1",
52
45
  "commander": "14.0.2",
53
46
  "execa": "9.6.1",
54
- "pi-acp": "^0.0.33",
55
47
  "systeminformation": "^5.33.8",
56
- "ws": "8.19.0",
57
48
  "yaml": "^2.8.2",
58
- "zod": "4.5.4"
49
+ "zod": "4.6.7"
59
50
  },
60
51
  "devDependencies": {
61
52
  "@types/node": "24.13.3",
62
53
  "esbuild": "0.28.2",
63
54
  "portta-contracts": "0.1.0",
64
55
  "portta-core": "0.1.0",
65
- "portta-host": "0.1.0",
66
- "portta-mcp": "0.1.0",
67
56
  "typescript": "7.0.2",
68
- "vitest": "4.1.11",
57
+ "vitest": "4.1.12",
69
58
  "tsx": "4.23.13"
70
59
  }
71
60
  }
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
3
- import{a,b,c}from"./chunk-QBKADN76.js";import"./chunk-EFZZFIRZ.js";import"./chunk-6J5HN4ZN.js";export{a as BUILTIN_ACP_PROVIDERS,c as availableProviders,b as resolveAcpProviders};
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
3
- import{a as e}from"./chunk-6J5HN4ZN.js";function t(){return(n,o)=>n.toJsonSchema(o)}e(t,"getToJsonSchemaFn");export{t as default};
@@ -1,285 +0,0 @@
1
- ---
2
- name: portta-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
- license: MIT
5
- compatibility: Requires the portta CLI and at least one agent CLI (Codex, Claude Code, OpenCode or Pi).
6
- metadata:
7
- author: codions-labs
8
- version: "1"
9
- ---
10
-
11
- # Taskflow workflows
12
-
13
- 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.
14
-
15
- 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.
16
-
17
- 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*.
18
-
19
- Common single-phase workflows you can chain across runs:
20
- - **Understand** — parallel readers over relevant subsystems → structured map
21
- - **Design** — judge panel of N independent approaches → scored synthesis
22
- - **Review** — dimensions → find → adversarially verify (example below)
23
- - **Research** — multi-modal sweep → deep-read → synthesize
24
- - **Migrate** — discover sites → transform each (workspace isolation) → verify
25
-
26
- 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.
27
-
28
- Every script must begin with `export const meta = {...}`:
29
- ```js
30
- export const meta = {
31
- name: 'find-flaky-tests',
32
- description: 'Find flaky tests and propose fixes', // one-line summary
33
- phases: [ // one entry per phase() call
34
- { title: 'Scan', detail: 'grep test logs for retries' },
35
- { title: 'Fix', detail: 'one agent per flaky test' },
36
- ],
37
- }
38
- // script body starts here — use agent()/parallel()/pipeline()/phase()/log()
39
- phase('Scan')
40
- const flaky = await agent('grep CI logs for retry markers', { schema: FLAKY_SCHEMA })
41
- // ...
42
- ```
43
-
44
- 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.
45
-
46
- Script body hooks:
47
- - 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.
48
- - 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.
49
- - 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.
50
- - log(message: string): void — emit a progress message to the user (shown as a narrator line above the progress tree)
51
- - phase(title: string): void — start a new phase; subsequent `agent()` calls are grouped under this title in the progress display
52
- - 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.
53
- - 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`.
54
- - now(): number / random(): number — journal-seeded deterministic time/RNG. Use these instead of `Date.now()`/`Math.random()` (which throw — see below).
55
-
56
- 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.
57
-
58
- ## Providers
59
-
60
- 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.
61
-
62
- **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.
63
-
64
- The four providers:
65
- - **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.
66
- - **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).
67
- - **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.
68
- - **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.
69
-
70
- (`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`.)
71
-
72
- 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.
73
-
74
- The default case — omit provider, so fan-out and synthesis run on the workflow's selected provider:
75
- ```js
76
- const findings = await parallel(AREAS.map(area => () =>
77
- agent(`Inspect ${area} and list concrete issues.`, { schema: FINDINGS_SCHEMA })))
78
- const report = await agent(`Synthesize into a prioritized report:\n${JSON.stringify(findings.filter(Boolean), null, 2)}`)
79
- return report
80
- ```
81
- Pin a provider only when you mean to — e.g. a deliberate cross-provider verify pass when the user has asked for that diversity:
82
- ```js
83
- const verified = await pipeline(
84
- suspects,
85
- s => agent(`Is this a real bug? ${s.desc}`, { schema: VERDICT }),
86
- (v, s) => agent(`Try to REFUTE that ${s.desc} is a bug; default to refuted if unsure.`,
87
- { provider: "claude-code", model: "<claude-model>", schema: VERDICT }).then(r => ({ ...s, real: v.real && !r.refuted })))
88
- ```
89
-
90
- 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).
91
-
92
- DEFAULT TO `pipeline()`. Only reach for a barrier (`parallel()` between stages) when you genuinely need ALL prior-stage results together.
93
-
94
- A barrier is correct ONLY when stage N needs cross-item context from all of stage N-1:
95
- - Dedup/merge across the full result set before expensive downstream work
96
- - Early-exit if the total count is zero ("0 bugs found → skip verification entirely")
97
- - Stage N's prompt references "the other findings" for comparison
98
-
99
- A barrier is NOT justified by:
100
- - "I need to flatten/map/filter first" — do it inside a pipeline stage: `pipeline(items, stageA, r => transform([r]).flat(), stageB)`
101
- - "The stages are conceptually separate" — that's what `pipeline()` models. Separate stages ≠ synchronized stages.
102
- - "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.
103
-
104
- Smell test: if you wrote
105
- ```js
106
- const a = await parallel(...)
107
- const b = transform(a) // flatten, map, filter — no cross-item dependency
108
- const c = await parallel(b.map(...))
109
- ```
110
- that middle transform doesn't need the barrier. Rewrite as a pipeline with the transform inside a stage. When in doubt: pipeline.
111
-
112
- 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.
113
-
114
- The canonical multi-stage pattern — pipeline by default, each dimension verifies as soon as its review completes:
115
- ```js
116
- export const meta = {
117
- name: 'review-changes',
118
- description: 'Review changed files across dimensions, verify each finding',
119
- phases: [{ title: 'Review' }, { title: 'Verify' }],
120
- }
121
- const DIMENSIONS = [{ key: 'bugs', prompt: '...' }, { key: 'perf', prompt: '...' }]
122
- const results = await pipeline(
123
- DIMENSIONS,
124
- d => agent(d.prompt, { label: `review:${d.key}`, phase: 'Review', schema: FINDINGS_SCHEMA }),
125
- review => parallel(review.findings.map(f => () =>
126
- agent(`Adversarially verify: ${f.title}`, { label: `verify:${f.file}`, phase: 'Verify', schema: VERDICT_SCHEMA })
127
- .then(v => ({ ...f, verdict: v }))
128
- ))
129
- )
130
- const confirmed = results.flat().filter(Boolean).filter(f => f.verdict?.isReal)
131
- return { confirmed }
132
- // Dimension 'bugs' findings verify while dimension 'perf' is still reviewing. No wasted wall-clock.
133
- ```
134
-
135
- When a barrier IS correct — dedup across all findings before expensive verification:
136
- ```js
137
- const all = await parallel(DIMENSIONS.map(d => () => agent(d.prompt, { schema: FINDINGS_SCHEMA })))
138
- const deduped = dedupeByFileAndLine(all.filter(Boolean).flatMap(r => r.findings))
139
- const verified = await parallel(deduped.map(f => () => agent(verifyPrompt(f), { schema: VERDICT_SCHEMA })))
140
- ```
141
-
142
- Loop-until-count pattern — accumulate to a target:
143
- ```js
144
- const bugs = []
145
- while (bugs.length < 10) {
146
- const result = await agent("Find bugs in this codebase.", { schema: BUGS_SCHEMA })
147
- bugs.push(...result.bugs)
148
- log(`${bugs.length}/10 found`)
149
- }
150
- ```
151
-
152
- 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.
153
- ```js
154
- const bugs = []
155
- while (budget.total && budget.remaining() > 50_000) {
156
- const result = await agent("Find bugs in this codebase.", { schema: BUGS_SCHEMA })
157
- bugs.push(...result.bugs)
158
- log(`${bugs.length} found, ${Math.round(budget.remaining() / 1000)}k remaining`)
159
- }
160
- ```
161
-
162
- Composing patterns — exhaustive review (find → dedup vs seen → diverse-lens panel → loop-until-dry):
163
- ```js
164
- const seen = new Set(), confirmed = []
165
- let dry = 0
166
- while (dry < 2) {
167
- const found = (await parallel(FINDERS.map(f => () =>
168
- agent(f.prompt, { phase: 'Find', schema: BUGS })))).filter(Boolean).flatMap(r => r.bugs)
169
- const fresh = found.filter(b => !seen.has(key(b)))
170
- if (!fresh.length) { dry++; continue }
171
- dry = 0; fresh.forEach(b => seen.add(key(b)))
172
- const judged = await parallel(fresh.map(b => () =>
173
- parallel(['correctness', 'security', 'repro'].map(lens => () =>
174
- agent(`Judge "${b.desc}" via the ${lens} lens — real?`, { phase: 'Verify', schema: VERDICT })))
175
- .then(vs => ({ b, real: vs.filter(Boolean).filter(v => v.real).length >= 2 }))))
176
- confirmed.push(...judged.filter(v => v.real).map(v => v.b))
177
- }
178
- return confirmed
179
- // dedup vs `seen`, NOT `confirmed` — else judge-rejected findings reappear every round and it never converges.
180
- ```
181
-
182
- Quality patterns — common shapes; pick by task and compose freely:
183
- - Adversarial verify: spawn N independent skeptics per finding, each prompted to REFUTE. Kill if ≥majority refute. Prevents plausible-but-wrong findings from surviving.
184
- ```js
185
- const votes = await parallel(Array.from({ length: 3 }, () => () =>
186
- agent(`Try to refute: ${claim}. Default to refuted=true if uncertain.`, { schema: VERDICT })))
187
- const survives = votes.filter(Boolean).filter(v => !v.refuted).length >= 2
188
- ```
189
- - 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.
190
- - 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.
191
- - 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.
192
- - 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.
193
- - 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.
194
- - 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.
195
-
196
- 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.
197
-
198
- These patterns aren't exhaustive — compose novel harnesses when the task calls for it (tournament brackets, self-repair loops, staged escalation, whatever fits).
199
-
200
- Use a workflow for multi-step orchestration where control flow should be deterministic (loops, conditionals, fan-out) rather than model-driven.
201
-
202
- ## Workspaces
203
-
204
- `meta.workspace` declares the Run-level workspace policy:
205
- ```js
206
- workspace: {
207
- default: 'isolated_worktree',
208
- allowed: ['isolated_worktree', 'new_branch', 'current_branch'],
209
- mutatesRepository: true,
210
- reason: 'Review agents may prepare fixes',
211
- }
212
- ```
213
-
214
- 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.
215
-
216
- ## Resume
217
-
218
- 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.
219
-
220
- 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`.
221
-
222
- ## CLI commands
223
-
224
- Durable Taskflow Runs:
225
- ```
226
- portta flow workflows list
227
- portta flow run workflow <workflow-id> [--input <text> | --input-json <json>]
228
- [--profile <name>]
229
- [--workspace worktree|branch|current]
230
- [--branch <name>] [--base <branch>]
231
- portta flow runs list
232
- portta flow runs show <run-id>
233
- portta flow runs cancel <run-id>
234
- portta flow runs resume <run-id>
235
- portta flow runs transcript <execution-id>
236
- ```
237
-
238
- Authoring and standalone validation/execution:
239
- ```
240
- portta flow workflows run <file.workflow.js | name> [--args '<json>' | --args-file <f>]
241
- [--provider codex|claude-code|opencode|pi --model m] [--effort e]
242
- [--sandbox read-only|workspace-write|danger-full-access] [--cwd dir]
243
- [--concurrency N] [--budget N] [--resume <runId>] [--fake] [--json] [--open]
244
- portta flow workflows save <file.workflow.js> [--project] [--force]
245
- portta flow workflows validate <file.workflow.js | name>
246
- portta flow workflows doctor
247
- portta flow workflows guide
248
- ```
249
-
250
- 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.
251
-
252
- ## Saved / named workflows
253
-
254
- 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.
255
-
256
- Six built-ins ship with Taskflow. Two are ports of Claude Code's bundled workflows:
257
- - **`deep-research`** — scope → 5 parallel web searches → fetch/dedup top sources → 3-vote adversarial verification per claim → cited report.
258
- - **`code-review`** — one finder per review angle, an independent verifier per finding, a gap-sweep at high levels, ranked report.
259
-
260
- The other four are multi-provider workflows — designs where two models' decorrelated errors are the point:
261
- - **`multi-provider-review`** — Codex and Claude independently review the same target, then a synthesis agent merges consensus, unique catches, and disagreements.
262
- - **`bake-off`** — both providers implement the same task in isolated workspaces, blind judges score the diffs, and a tie-break settles splits.
263
- - **`provider-debate`** — one provider proposes, the other attacks, N rounds of rebuttal, then a judge rules.
264
- - **`second-opinion`** — both providers answer cheaply; disagreement triggers deeper passes and adjudication.
265
-
266
- ## Running a workflow for a user (from an agent)
267
-
268
- When you (an agent such as Claude Code or Codex) create or run a workflow on a user's behalf:
269
-
270
- - Put reusable project workflows in `.portta/workflows/`, validate them, and confirm they appear in `portta flow workflows list` before starting a durable Run.
271
- - 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.
272
- - 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.
273
- - 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.
274
-
275
- ## Installation
276
-
277
- Install with the [Skills CLI](https://www.skills.sh):
278
-
279
- ```
280
- npx skills add codions-labs/portta --skill portta-workflows -g
281
- ```
282
-
283
- It installs into Claude Code, Codex, OpenCode, Cursor, Zed and the other agents it supports, symlinking by default so an update reaches every agent at once. `npx skills update` refreshes it; `npx skills remove` takes it out.
284
-
285
- Pair it with `portta-method`, which carries the branching, worktree, commit and pull request rules this project follows.