@oisincoveney/pipeline 2.8.3 → 2.9.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 (69) hide show
  1. package/.agents/skills/orchestrate/SKILL.md +45 -32
  2. package/README.md +51 -41
  3. package/defaults/pipeline.yaml +13 -12
  4. package/defaults/profiles.yaml +1 -1
  5. package/dist/argo-submit.js +1 -1
  6. package/dist/cli/doctor.d.ts +21 -0
  7. package/dist/cli/doctor.js +268 -0
  8. package/dist/cli/format.js +6 -3
  9. package/dist/cli/program.d.ts +14 -16
  10. package/dist/cli/program.js +291 -104
  11. package/dist/cli/run-resolver.js +58 -0
  12. package/dist/commands/bench-command.js +12 -4
  13. package/dist/commands/pipeline-command.js +22 -5
  14. package/dist/commands/runner-command-command.js +32 -9
  15. package/dist/config/lint.js +44 -26
  16. package/dist/config/load.js +0 -1
  17. package/dist/config/schemas.d.ts +0 -6
  18. package/dist/config/schemas.js +0 -8
  19. package/dist/context/repo-map.js +72 -56
  20. package/dist/gates.js +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +20 -14
  23. package/dist/install-commands/claude-code.js +4 -33
  24. package/dist/install-commands/opencode.js +119 -171
  25. package/dist/mcp/repo-local-backends.js +51 -39
  26. package/dist/moka-submit.js +3 -3
  27. package/dist/pipeline-runtime.js +15 -5
  28. package/dist/planning/generate.js +5 -11
  29. package/dist/run-control/commands.js +340 -0
  30. package/dist/run-control/contracts.d.ts +21 -0
  31. package/dist/run-control/contracts.js +129 -0
  32. package/dist/run-control/detach.js +79 -0
  33. package/dist/run-control/runtime-reporter.js +187 -0
  34. package/dist/run-control/store.js +304 -0
  35. package/dist/run-control/supervisor.js +192 -0
  36. package/dist/runner-command/finalize.js +28 -37
  37. package/dist/runner-command/lifecycle-context.js +130 -63
  38. package/dist/runner-command/lifecycle.js +22 -31
  39. package/dist/runner-command/run.js +120 -72
  40. package/dist/runner-command/task-descriptor.js +11 -4
  41. package/dist/runner.js +1 -1
  42. package/dist/runtime/agent-node/agent-node.js +3 -3
  43. package/dist/runtime/builtins/builtins.js +1 -3
  44. package/dist/runtime/changed-files/changed-files.js +1 -1
  45. package/dist/runtime/context/context.js +1 -1
  46. package/dist/runtime/contracts/contracts.d.ts +4 -0
  47. package/dist/runtime/hooks/hooks.js +1 -1
  48. package/dist/runtime/json-validation/json-validation.js +49 -23
  49. package/dist/runtime/local-scheduler.js +49 -26
  50. package/dist/runtime/opencode-adapter.js +14 -10
  51. package/dist/runtime/opencode-runtime.js +22 -20
  52. package/dist/runtime/opencode-session-executor.js +1 -1
  53. package/dist/runtime/parallel-node/parallel-node.js +10 -0
  54. package/dist/runtime/parallel-worktrees/parallel-worktrees.js +2 -35
  55. package/dist/runtime/run-journal.js +17 -10
  56. package/dist/runtime/services/file-system-service.js +29 -0
  57. package/dist/runtime/services/opencode-runtime-server-service.js +27 -0
  58. package/dist/runtime/services/repo-io-service.js +48 -0
  59. package/dist/runtime/services/run-journal-file-service.js +20 -0
  60. package/dist/runtime/services/runner-command-io-service.js +88 -0
  61. package/dist/runtime/workflow-lifecycle.js +76 -39
  62. package/dist/schedule/backlog-context.js +55 -29
  63. package/dist/schedule/passes/index.js +0 -1
  64. package/docs/config-architecture.md +4 -26
  65. package/docs/operator-guide.md +73 -32
  66. package/package.json +2 -1
  67. package/dist/runtime/select-candidate/select-candidate.js +0 -144
  68. package/dist/runtime/services/select-candidate-service.js +0 -13
  69. package/dist/schedule/passes/candidates.js +0 -51
@@ -1,23 +1,23 @@
1
1
  ---
2
- description: Local multi-agent orchestrator decompose a task and fan it out to the MoKa specialist roster on the current machine instead of submitting to the remote Moka pipeline. The local twin of the MoKa Orchestrator. On Claude Code, dispatch each agent with `opencode run`; on OpenCode, spawn native Task subagents. Use when the user wants parallel specialist agents driven locally rather than as Argo/k8s jobs.
2
+ description: Local multi-agent orchestration through the supervised MoKa runtime. Start canonical local work with `moka run "<task>"`; use OpenCode native Task subagents only when explicitly chosen; reserve emergency CLI fallback for titled, logged, session-captured recovery.
3
3
  name: orchestrate
4
4
  ---
5
5
 
6
6
  # Orchestrate
7
7
 
8
- The **local twin of the MoKa Orchestrator**. The MoKa Orchestrator decomposes a task and submits a schedule to Argo/k8s via `moka submit`, where the runtime executes it as DAG jobs on the cluster. Orchestrate runs the **same roster, same loop, on the current machine** — no schedule, no `moka submit`, no Argo. It is the hands-on, here-and-now path for getting work done through specialist agents.
8
+ The **local supervised twin of the MoKa Orchestrator**. Start local orchestration with `moka run "<task>"` from the repository root. It uses the package-owned roster, config, run-control state, logs, and CLI contracts on the current machine — no raw host subprocess fan-out, no unmanaged local sessions.
9
9
 
10
- Use this skill when the user wants real work driven through **parallel specialist agents locally**: a task large enough to decompose into research / test / implement / verify lanes, where you stay the controller and the agents do the labor.
10
+ Use this skill when the user wants real work driven through **parallel specialist agents locally**: a task large enough to decompose into research / test / implement / verify lanes, where you stay the controller and the agents do the labor through the supervised runtime.
11
11
 
12
12
  ## When NOT to use
13
13
 
14
- - **Durable, reproducible, or cluster-scale runs** → use [[quick]] or [[execute]] (these submit through `moka submit`). Orchestrate is ephemeral and local; it leaves no schedule artifact.
14
+ - **Durable, reproducible, or cluster-scale runs** → use the remote path (for example `moka run --target remote ...` or the package compatibility commands). Orchestrate is local.
15
15
  - **Trivial single-threaded work** → just do it inline. Spawning agents for a one-line change is pure overhead.
16
- - **You need package gates enforced as part of a pipeline run** → that is the remote path's job. Orchestrate still *uses* the gate agents (Verifier, Acceptance Reviewer, Thermo Nuclear Reviewer) but does not replace pipeline-level gating.
16
+ - **You need package gates enforced by a remote pipeline** → use the remote path. Local orchestration still uses the gate agents, but does not replace remote pipeline gating.
17
17
 
18
18
  ## The roster
19
19
 
20
- The same specialist agents the MoKa pipeline uses, mirrored locally. Each is `mode: all`, so it works both as an `opencode run --agent` subprocess and as a native Task subagent.
20
+ The same specialist agents the MoKa pipeline uses, mirrored locally through package-owned config. When OpenCode native Task mode is explicitly chosen, select the same agent names.
21
21
 
22
22
  | Role | Agent name | Writes | Job |
23
23
  |-------------|-------------------------|---------------|-----|
@@ -30,43 +30,55 @@ The same specialist agents the MoKa pipeline uses, mirrored locally. Each is `mo
30
30
  | Learn | `MoKa Learner` | memory only | Store durable lessons from the completed run (qdrant memory). The pipeline's LEARN phase. |
31
31
  | Inspect | `MoKa Inspector` | nothing | Read-only repository inspection / explanation. |
32
32
 
33
- Keep each agent inside its lane — never ask the Code Writer to touch tests, or a reviewer to write files. The lane boundaries are what make fan-out safe. (`MoKa Schedule Planner` is intentionally **not** in this roster: it plans the DAG for remote `moka submit`; locally *you* do that in the Plan step.)
33
+ Keep each agent inside its lane — never ask the Code Writer to touch tests, or a reviewer to write files. The lane boundaries are what make fan-out safe. (`MoKa Schedule Planner` is intentionally **not** in this roster: it plans remote DAGs; locally *you* decompose before dispatch.)
34
34
 
35
- ## Dispatch by host
35
+ ## Dispatch
36
36
 
37
- The orchestration **doctrine below is identical on every host**. Only the spawn mechanism differs select the branch for the host you are actually running in.
37
+ The orchestration doctrine is host-neutral: **canonical local orchestration starts with `moka run`**. Host-specific dispatch applies only when the user explicitly chooses OpenCode native Task use or when supervised CLI execution is unavailable and an emergency fallback is accepted.
38
38
 
39
- ### On Claude Code `opencode run`
40
-
41
- Spawn each roster member as a headless OpenCode subprocess and read its JSON back:
39
+ ### Canonical supervised local run all hosts
42
40
 
43
41
  ```sh
44
- opencode run --agent "MoKa Code Writer" --format json \
45
- "<scoped task + acceptance criteria + paths to read>"
42
+ moka run "<scoped task + acceptance criteria + paths to read>"
46
43
  ```
47
44
 
48
- - Select the roster member with `--agent "<exact name>"` (names from the table above).
49
- - Use `--format json` so the agent's structured result comes back machine-readable; parse it, do not eyeball it.
50
- - **Parallelize independent lanes**: launch each `opencode run` as a background Bash process (one tool call per lane in the same turn), then collect. Run dependent lanes only after their inputs land.
51
- - Pass context by path, not by paste agents read the worktree directly. Hand them the files/AC the Researcher produced.
52
- - `--model` / `--variant` only when a lane genuinely needs a different tier; otherwise inherit.
45
+ - Start here on Claude Code, OpenCode, and plain terminals. Do not pair this with unmanaged local subprocess fan-out for the same work.
46
+ - Use package flags instead of host-specific spawning: `--effort quick`, `--effort thorough`, `--read-only`, `--detach`, or `--target remote` when those modes are intended.
47
+ - Capture the `Run id` and inspect with `moka status <run-id>` and `moka logs <run-id>` rather than relying on an agent transcript alone.
48
+ - If you manually split lanes, keep each lane as its own supervised run with tight scope and explicit acceptance criteria.
53
49
 
54
- ### On OpenCode native Task subagents
50
+ ### Explicit OpenCode native Task mode
55
51
 
56
- You are already inside OpenCode — do not shell out to `opencode run`. Spawn the roster directly with the native **Task** tool, selecting the agent by the same name:
52
+ Use this branch only when the user explicitly chooses native OpenCode Task subagents and you are already inside OpenCode.
57
53
 
58
- - `task` `MoKa Researcher`, `MoKa Test Writer`, `MoKa Code Writer`, `MoKa Verifier`, `MoKa Acceptance Reviewer`, `MoKa Thermo Nuclear Reviewer`, `MoKa Learner`.
54
+ - Spawn the roster directly with the native **Task** tool, selecting the agent by the exact name from the table.
59
55
  - Issue independent Task calls together so they run concurrently; sequence dependent ones.
60
- - Each subagent's structured output returns to you as the controller — gather, do not re-do their work.
56
+ - Each subagent's structured output returns to you as the controller — gather it, do not re-do their work.
57
+ - Do not shell out from OpenCode for local orchestration; if the supervised CLI is available, return to `moka run`.
58
+
59
+ ### Emergency fallback — raw `opencode run`
60
+
61
+ Use raw `opencode run` only when `moka run` cannot execute, the user still wants local agent work, and you explicitly record that the run is outside MoKa supervision.
62
+
63
+ ```sh
64
+ mkdir -p .pipeline/runs
65
+ title="Emergency fallback: <lane>"
66
+ log=".pipeline/runs/emergency-$(date +%Y%m%d%H%M%S).log"
67
+ opencode run --agent "MoKa Code Writer" --format json \
68
+ "<scoped task + acceptance criteria + paths to read>" 2>&1 | tee "$log"
69
+ ```
70
+
71
+ - Required capture: fallback title, exact command, cwd, log path, and session id from the output/event stream. If no session id is emitted, state that explicitly.
72
+ - Keep the fallback lane scoped and one-shot; switch back to `moka run` as soon as the supervised runtime is available.
61
73
 
62
74
  ## The loop
63
75
 
64
- Whichever host you are on, run the same six steps:
76
+ Whichever allowed dispatch path you use, run the same six steps:
65
77
 
66
78
  1. **Plan** — Decompose the task into a DAG of agent lanes. Model parallelism *structurally*: independent lanes fan out together, dependents wait on their inputs (research → tests → implementation → verify). Do not invent JSON-pointer fanout; nest the work as real lanes.
67
- 2. **Dispatch** — Fan out per the host branch above. Scope each agent tightly: one job, its lane's write boundary, explicit acceptance criteria.
79
+ 2. **Dispatch** — Prefer `moka run`; use native Task only when explicitly chosen; use emergency fallback only with the required capture above. Scope each agent tightly: one job, its lane's write boundary, explicit acceptance criteria.
68
80
  3. **Gather** — Collect each agent's structured output (`research.json`, the Verifier's verdict JSON, etc.). Treat the returned artifact as the source of truth.
69
- 4. **Gate** — Run `MoKa Verifier` (checks vs AC), `MoKa Acceptance Reviewer` (audits each acceptance criterion), and `MoKa Thermo Nuclear Reviewer` (final code-quality review). Do **not** accept work on a `FAIL` from any gate. Loop the relevant lane (re-dispatch Code Writer with the failure evidence) rather than papering over it.
81
+ 4. **Gate** — Run `MoKa Verifier` (checks vs AC), `MoKa Acceptance Reviewer` (audits each acceptance criterion), and `MoKa Thermo Nuclear Reviewer` (final code-quality review). Do **not** accept work on a `FAIL` from any gate. Loop the relevant lane with the failure evidence rather than papering over it.
70
82
  5. **Learn** — Once the gates pass, run `MoKa Learner` to store durable lessons from the run (qdrant memory) when there is something worth reusing. This mirrors the canonical pipeline's LEARN phase; skip it only when the run produced nothing reusable.
71
83
  6. **Synthesize** — Report only the evidence the agents actually returned: what passed, what the diff is, what the reviewers proved. Never fabricate or assume an outcome an agent did not report.
72
84
 
@@ -75,21 +87,22 @@ Whichever host you are on, run the same six steps:
75
87
  Token usage is the dominant cost and quality lever — it explains the bulk of agent performance variance, and context degrades well before a model's window fills. But the first job of sizing is **reliable completion**: a lane an agent can't finish is worthless however cheap. Size the work accordingly:
76
88
 
77
89
  - **Size for reliable completion first.** Each lane must be small enough that a single agent session finishes it cleanly. If an agent times out, stalls, or returns having only *planned* without producing its artifact, the lane was **too big** — split it into smaller lanes (one file, section, or concern each); do **not** just raise the timeout, that re-runs the same flake. **Slow is fine; flaky is not** — many small lanes that each reliably complete beat one big lane that gambles. Lanes that share a file run sequentially; only truly independent lanes fan out. Treat repeated stalls as a decomposition bug, not bad luck.
78
- - **Under-timeouts and permission walls are the real flake sources — not a step cap.** Per opencode's docs, an agent with no `steps`/`maxSteps` set "will continue to iterate until the model chooses to stop or the user interrupts the session" — i.e. **no hard step budget by default** (the MoKa agents set none). So a Code Writer that returns having only *planned* was not hitting a step limit; it was killed by too short a dispatch timeout or blocked on a denied read (e.g. `external_directory: deny`). Fixes: give long multi-file authoring runs **generous wall-clock** (do not kill them early — they are slow, not capped); scope lanes so they don't need denied/external reads; and only if you must *bound* a runaway agent, set `steps` in its config. Smaller lanes still help (less work = faster, fewer surprises), but "multi-file authoring can't be delegated" is a timeout/scoping issue, not an opencode limit.
79
- - **Scale fan-out to complexity, not ambition.** A trivial change is one agent (or just do it inline); a bounded change is 1–3 lanes; only go wide for genuinely independent breadth. Code parallelizes poorly — keep writer lanes narrow (the pipeline caps `green`/code fan-out at 2 for exactly this reason).
90
+ - **Under-timeouts and permission walls are the real flake sources.** Give long multi-file authoring runs generous wall-clock, scope lanes so they don't need denied/external reads, and only bound runaway agents when you genuinely need a hard limit. Smaller lanes still help (less work = faster, fewer surprises), but "multi-file authoring can't be delegated" is usually a timeout/scoping issue.
91
+ - **Scale fan-out to complexity, not ambition.** A trivial change is one agent (or just do it inline); a bounded change is 1–3 lanes; only go wide for genuinely independent breadth. Code parallelizes poorly — keep writer lanes narrow.
80
92
  - **Keep each agent's context small and high-signal.** Pass context by path and hand over the distilled `research.json`, never raw repo dumps. A lane that needs half the repo in its context is mis-scoped — split it.
81
93
  - **Distilled returns.** Expect each sub-agent to return a ~1–2k-token summary of its result, not its full transcript. Gather the summary; don't re-read the work.
82
- - **Re-dispatch once, with evidence.** On a gate `FAIL`, re-dispatch the failing lane a *single* time with concentrated failure evidence — do not thrash. Each fresh `opencode run` re-pays the full cold-start context tax (~35k tokens of standup before any work), so a retry loop is expensive; fix the input, not the dice.
94
+ - **Re-dispatch once, with evidence.** On a gate `FAIL`, re-dispatch the failing lane a *single* time with concentrated failure evidence — do not thrash. Each fresh supervised lane pays the cold-start context cost; fix the input, not the dice.
83
95
  - **Smallest roster that covers the work.** Every extra lane is another cold standup. Default to the fewest specialists that close the task; add a lane only when it genuinely runs independently.
84
96
 
85
97
  ## Rules
86
98
 
87
- - **Doctrine is host-neutral; only the Dispatch section is host-specific.** Do not leak `opencode run` syntax into an OpenCode run or Task-tool talk into a Claude run.
99
+ - **Canonical local orchestration starts with `moka run`.** Do not tell Claude Code to use `moka run` and also spawn unmanaged subprocesses.
100
+ - **Host distinction is opt-in.** OpenCode native Task subagents are valid only when explicitly chosen; otherwise use the supervised CLI.
101
+ - **Emergency fallback is not canonical.** Raw host CLI use requires the title/log/session capture above and must be reported as unsupervised.
88
102
  - **You are the controller, not a worker.** Decompose, dispatch, gate, synthesize. Let the specialists do the labor inside their lanes.
89
103
  - **Evidence only.** Report what agents returned. A green claim needs a Verifier `PASS` with evidence behind it — see [[verify]].
90
104
  - **Respect lane write boundaries.** Researcher/Verifier/both Reviewers write no repo files (Researcher emits only `research.json`; Learner writes only to memory); Test Writer touches only tests; Code Writer touches only `src/`. Mixed lanes corrupt parallel fan-out.
91
- - **Local, not durable.** If the user needs a reproducible cluster run or a schedule artifact, route to [[quick]] / [[execute]] instead.
92
105
 
93
106
  ## The short version
94
107
 
95
- Orchestrate is `moka submit` brought home: same roster, same decompose dispatch gather → gate → learn → synthesize loop, run on this machine. On Claude Code each agent is an `opencode run --agent` subprocess; on OpenCode each is a native Task subagent. You stay the orchestrator — fan out the lanes, gate on real verifier *and* reviewer evidence, capture lessons via the Learner, and report only what the agents proved.
108
+ Start local orchestration with `moka run "<task>"`. Use OpenCode native Task subagents only when explicitly chosen, and use emergency raw CLI fallback only with title, log, and session capture. You stay the orchestrator — decompose the lanes, gate on real verifier *and* reviewer evidence, capture lessons via the Learner, and report only what the agents proved.
package/README.md CHANGED
@@ -82,61 +82,71 @@ moka explain-plan
82
82
 
83
83
  ## Command Surface
84
84
 
85
- `moka submit "<task>"`
85
+ `moka run "<task>"` is the primary command surface.
86
86
 
87
- Generates the full graph schedule for a task, builds the runner payload from the
88
- current git context, and submits an Argo Workflow to the configured Momokaya
89
- cluster.
90
-
91
- ```shell
92
- moka submit "Implement PIPE-123 user-facing behavior"
93
- ```
94
-
95
- `moka submit "<task>" --quick`
96
-
97
- Uses the compact graph for smaller work.
98
-
99
- ```shell
100
- moka submit "Fix the login bug" --quick
101
- ```
102
-
103
- `moka submit --schedule <schedule.yaml> "<task>"`
104
-
105
- Submits a previously approved schedule artifact.
106
-
107
- ```shell
108
- moka submit --schedule .pipeline/runs/<runId>/schedule.yaml "Implement PIPE-123"
109
- ```
110
-
111
- `moka submit --command -- <argv...>`
112
-
113
- Submits one explicit command as a one-task Argo Workflow.
114
-
115
- ```shell
116
- moka submit --command -- opencode run "fix this bug"
117
- ```
118
-
119
- `moka run "<task>"`
120
-
121
- Runs package-owned workflow config from the current worktree. Scheduled
87
+ It runs package-owned workflow config from the current worktree. Scheduled
122
88
  entrypoints generate a schedule artifact under `.pipeline/runs/<runId>/` and run
123
89
  the compiled schedule through the runtime.
124
90
 
91
+ Canonical commands:
92
+
93
+ - `moka run "<task>"`: start the primary local or remote run flow.
94
+ - `moka runs`: list known runs, newest first.
95
+ - `moka status <run-id>`: show run and node status; add `--watch` to poll.
96
+ - `moka logs <run-id> [node-id]`: print whole-run or node-specific artifacts.
97
+ - `moka stop <run-id> [node-id]`: abort a run or one active node.
98
+ - `moka export <run-id> --sanitize`: print a portable evidence bundle.
99
+ - `moka doctor`: check local prerequisites and config health.
100
+ - `moka init`: install package-owned host resources for a repository.
101
+
125
102
  ```shell
126
103
  moka run "Implement PIPE-123 user-facing behavior"
104
+ moka run --target local --effort normal "Implement a standard local change"
127
105
  moka run --schedule .pipeline/runs/<runId>/schedule.yaml "Implement PIPE-123"
128
106
  moka run --workflow inspect "Report the app structure and available checks. Do not modify files."
129
- moka run --entrypoint quick "Implement a focused fix"
107
+ moka run --effort quick "Implement a focused fix"
108
+ moka run --effort normal "Implement a standard fix"
109
+ moka run --target remote --effort thorough "Submit a full hosted graph run"
110
+ moka run --read-only "Inspect the repository without edits"
111
+ moka run --target remote --command -- opencode run "fix this bug"
130
112
  ```
131
113
 
132
- `moka inspect "<task>"`
114
+ Flag defaults and choices:
133
115
 
134
- Runs the configured read-only inspection entrypoint.
116
+ - `--target` selects `local` (default, current worktree) or `remote` (hosted
117
+ Momokaya submission). Use canonical `moka run --target remote "<task>"` for
118
+ hosted graph runs.
119
+ - `--effort` selects `quick`, `normal`, or `thorough`; `normal` is the default.
120
+ - `--read-only` switches mode to `read`; mode defaults to `write`.
135
121
 
136
- ```shell
137
- moka inspect "Explain the app structure and available checks"
122
+ Local run artifacts live under `.pipeline/runs/<runId>/`:
123
+
124
+ ```text
125
+ .pipeline/runs/<runId>/
126
+ schedule.yaml
127
+ manifest.json
128
+ status.json
129
+ events.ndjson
130
+ nodes/<node-id>/
131
+ artifacts/
138
132
  ```
139
133
 
134
+ Use `moka export <run-id> --sanitize` before sharing a run. The sanitized export
135
+ keeps portable evidence and omits prompt text, session body content, secrets,
136
+ tokens, and credentials.
137
+
138
+ Compatibility aliases and presets remain available for existing scripts:
139
+
140
+ - `moka quick "<task>"` is a compatibility preset for
141
+ `moka run --effort quick "<task>"`.
142
+ - `moka execute "<task>"` is a compatibility preset for
143
+ `moka run --effort thorough "<task>"`.
144
+ - `moka inspect "<task>"` is a compatibility preset for
145
+ `moka run --read-only "<task>"`.
146
+ - `moka submit "<task>"` is a compatibility alias for
147
+ `moka run --target remote --effort thorough "<task>"`. Its existing `--quick`,
148
+ `--schedule`, and `--command` options remain supported for remote submissions.
149
+
140
150
  Use `PIPELINE_TARGET_PATH=/path/to/worktree` when invoking `moka` from outside
141
151
  the target repository.
142
152
 
@@ -15,18 +15,10 @@ repo_map:
15
15
  # last passed node without re-running (or re-spending tokens on) finished work.
16
16
  durability:
17
17
  enabled: true
18
- # best_of_n / parallel_worktrees: the verifier-pattern dial. Schedule generation
19
- # + selection are validated/tested, BUT a live end-to-end run (2026-06-16) proved
20
- # the EXECUTION is not yet production-ready, so it stays OFF by default (on-by-
21
- # default made real runs hang). Two runtime gaps remain — see PIPE-83.14:
22
- # 1. The leased opencode server is rooted at the main worktree and throws
23
- # "Unexpected server error" when a candidate session runs with directory set
24
- # to its isolated worktree → candidates exit 70, retries exhaust, green loops.
25
- # Fix: lease a per-worktree opencode server for each candidate.
26
- # 2. The winning candidate's file changes live in its worktree and are never
27
- # merged back to the main tree, so downstream nodes wouldn't see them.
28
- # Enable explicitly (best_of_n.enabled + n:2 + categories:[green] + parallel_
29
- # worktrees.enabled) once those land; n=2 also ~doubles green-node spend.
18
+ # parallel_worktrees: opt-in git-worktree isolation for parallel child nodes —
19
+ # each parallel child runs on its own branch with its own per-worktree opencode
20
+ # server so concurrent edits and sessions can't collide. OFF by default; enable
21
+ # explicitly with parallel_worktrees.enabled.
30
22
  token_budget:
31
23
  default_context_window: 200000
32
24
  max_context_pct: 50
@@ -62,6 +54,15 @@ runner_command:
62
54
  setup:
63
55
  - command: bun
64
56
  args: [install, --frozen-lockfile]
57
+ # Set up package-owned pipeline support + the opencode model registration
58
+ # (.opencode/opencode.json, which declares the gpt-5.5-* reasoning selectors)
59
+ # on every run, so opencode-backed agents in the pod resolve their models
60
+ # instead of failing with "Model not found". Both commands are idempotent
61
+ # and write no repo-local pipeline config.
62
+ - command: moka
63
+ args: [init]
64
+ - command: moka
65
+ args: [install-commands]
65
66
  scheduler:
66
67
  commands:
67
68
  quick:
@@ -88,7 +88,7 @@ profiles:
88
88
  moka-orchestrator:
89
89
  runner: opencode
90
90
  description: Orchestrate the configured pipeline and enforce gates.
91
- instructions: { inline: "Orchestrate the configured pipeline locally. Load the `orchestrate` skill and spawn the roster as native Task subagents on this machine. Do not submit to Argo or run `moka submit`. Enforce only package-configured gates." }
91
+ instructions: { inline: "Orchestrate through the canonical local `moka run` supervisor. For compatibility slash commands, run the `moka run` command and flags shown in the command body. Treat execution as CLI/supervised runtime, not OpenCode-native Task execution. Enforce only package-configured gates." }
92
92
  skills: [orchestrate, execute, quick, inspect]
93
93
  mcp_servers: [pipeline-gateway]
94
94
  tools: [read, list, grep, glob, bash]
@@ -1,9 +1,9 @@
1
1
  import { ArgoGraphCompilerError, compileArgoExecutionGraph } from "./argo-graph.js";
2
+ import { parseRunnerCommandPayload, runnerCommandPayloadSchema } from "./runner-command-contract.js";
2
3
  import { buildRunnerTaskDescriptor } from "./runner-command/task-descriptor.js";
3
4
  import { buildRunnerArgoWorkflowManifest, runnerArgoWorkflowManifestSchema } from "./argo-workflow.js";
4
5
  import { normalizeRunnerRepositoryForSubmit } from "./git-remote-url.js";
5
6
  import { compileScheduleArtifact, parseScheduleArtifact } from "./planning/generate.js";
6
- import { parseRunnerCommandPayload, runnerCommandPayloadSchema } from "./runner-command-contract.js";
7
7
  import { KubernetesArgoService, KubernetesArgoServiceLive } from "./runtime/services/kubernetes-argo-service.js";
8
8
  import { workflowSubmitResultSchema } from "./workflow-submit-contract.js";
9
9
  import { stringify } from "yaml";
@@ -0,0 +1,21 @@
1
+ //#region src/cli/doctor.d.ts
2
+ interface DoctorCheck {
3
+ detail: string;
4
+ name: string;
5
+ passed: boolean;
6
+ }
7
+ interface DoctorResult {
8
+ blockers: DoctorCheck[];
9
+ checks: DoctorCheck[];
10
+ passed: boolean;
11
+ warnings: DoctorCheck[];
12
+ }
13
+ interface DoctorFlags {
14
+ cluster?: boolean | string;
15
+ json?: boolean;
16
+ kubeContext?: string;
17
+ kubeconfig?: string;
18
+ }
19
+ declare function runDoctor(cwd: string, options?: DoctorFlags): Promise<DoctorResult>;
20
+ //#endregion
21
+ export { runDoctor };
@@ -0,0 +1,268 @@
1
+ import { PipelineConfigError } from "../config/schemas.js";
2
+ import { loadPipelineConfig } from "../config/load.js";
3
+ import "../config.js";
4
+ import { opencodeAgentName } from "../runtime/opencode-agent-name.js";
5
+ import { loadMokaGlobalConfig } from "../moka-global-config.js";
6
+ import { defaultClusterDoctorNamespace, runClusterDoctor } from "../cluster-doctor.js";
7
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
+ import { execa } from "execa";
9
+ import { join } from "node:path";
10
+ import matter from "gray-matter";
11
+ //#region src/cli/doctor.ts
12
+ const HEADLESS_AGENT_PERMISSION_VALUES = new Set(["ask"]);
13
+ const RUN_READINESS_CATEGORIES = new Set([
14
+ "acceptance",
15
+ "green",
16
+ "intake",
17
+ "red",
18
+ "research",
19
+ "verification"
20
+ ]);
21
+ const OPENCODE_AGENT_LIST_ARGS = [
22
+ "agent",
23
+ "list",
24
+ "--json"
25
+ ];
26
+ const BULLET_PREFIX_RE = /^[-*]\s+/;
27
+ const LINE_RE = /\r?\n/;
28
+ async function runDoctor(cwd, options = {}) {
29
+ const commandChecks = await Promise.all([
30
+ checkCommand("npx", ["--version"], cwd),
31
+ checkCommand("opencode", ["--version"], cwd),
32
+ checkCommand("fallow", ["--version"], cwd)
33
+ ]);
34
+ const configCheck = checkPipelineConfig(cwd);
35
+ const config = configCheck.passed ? loadPipelineConfig(cwd) : null;
36
+ const [sdkCheck, agentVisibility] = await Promise.all([checkOpenCodeSdk(), config ? checkMokaAgents(cwd, config) : Promise.resolve({ check: {
37
+ detail: "skipped because pipeline config is invalid",
38
+ name: "moka-agents",
39
+ passed: true
40
+ } })]);
41
+ const globalConfig = loadMokaGlobalConfig();
42
+ const clusterResult = options.cluster ? await runClusterDoctor({
43
+ kubeContext: options.kubeContext,
44
+ kubeconfigPath: options.kubeconfig ?? globalConfig?.momokaya.kubernetes.kubeconfig,
45
+ namespace: clusterNamespace(options.cluster, globalConfig?.momokaya.kubernetes.namespace)
46
+ }) : { checks: [] };
47
+ const checks = [
48
+ ...commandChecks,
49
+ configCheck,
50
+ sdkCheck,
51
+ agentVisibility.check,
52
+ ...clusterResult.checks
53
+ ];
54
+ const warnings = [...agentVisibility.warning ? [agentVisibility.warning] : [], ...headlessPermissionWarnings(cwd)];
55
+ const blockers = checks.filter((check) => !check.passed);
56
+ return {
57
+ blockers,
58
+ checks,
59
+ passed: blockers.length === 0,
60
+ warnings
61
+ };
62
+ }
63
+ function clusterNamespace(value, configuredNamespace) {
64
+ return typeof value === "string" && value.length > 0 ? value : configuredNamespace ?? defaultClusterDoctorNamespace();
65
+ }
66
+ function checkCommand(name, args, cwd) {
67
+ return checkCommandWithRunner(name, name, args, cwd);
68
+ }
69
+ async function checkCommandWithRunner(name, command, args, cwd) {
70
+ try {
71
+ await execa(command, args, {
72
+ cwd,
73
+ stdin: "ignore"
74
+ });
75
+ return {
76
+ detail: "available",
77
+ name,
78
+ passed: true
79
+ };
80
+ } catch (err) {
81
+ return {
82
+ detail: commandErrorDetail(err),
83
+ name,
84
+ passed: false
85
+ };
86
+ }
87
+ }
88
+ function checkPipelineConfig(cwd) {
89
+ try {
90
+ loadPipelineConfig(cwd);
91
+ return {
92
+ detail: "valid",
93
+ name: "pipeline-config",
94
+ passed: true
95
+ };
96
+ } catch (err) {
97
+ let message = "invalid";
98
+ if (err instanceof PipelineConfigError) message = err.issues.map((issue) => issue.message).join("; ");
99
+ else if (err instanceof Error) message = err.message;
100
+ return {
101
+ detail: message || "missing or invalid",
102
+ name: "pipeline-config",
103
+ passed: false
104
+ };
105
+ }
106
+ }
107
+ async function checkOpenCodeSdk() {
108
+ try {
109
+ if (typeof (await import("@opencode-ai/sdk")).createOpencodeClient !== "function") return {
110
+ detail: "@opencode-ai/sdk does not export createOpencodeClient",
111
+ name: "opencode-sdk",
112
+ passed: false
113
+ };
114
+ return {
115
+ detail: "importable",
116
+ name: "opencode-sdk",
117
+ passed: true
118
+ };
119
+ } catch (err) {
120
+ return {
121
+ detail: commandErrorDetail(err),
122
+ name: "opencode-sdk",
123
+ passed: false
124
+ };
125
+ }
126
+ }
127
+ async function checkMokaAgents(cwd, config) {
128
+ const expected = expectedRunAgentNames(config);
129
+ if (expected.length === 0) return { check: {
130
+ detail: "no configured MoKa run agents",
131
+ name: "moka-agents",
132
+ passed: true
133
+ } };
134
+ try {
135
+ const visible = visibleAgentNames((await execa("opencode", OPENCODE_AGENT_LIST_ARGS, {
136
+ cwd,
137
+ stdin: "ignore"
138
+ })).stdout);
139
+ if (!visible.recognized) return skippedAgentVisibility("OpenCode agent listing output was not recognized");
140
+ if (visible.ambiguous && expected.every((name) => !visible.names.has(name))) return skippedAgentVisibility("OpenCode agent listing output did not include recognizable MoKa agent names");
141
+ const missing = expected.filter((name) => !visible.names.has(name));
142
+ return { check: missing.length ? {
143
+ detail: `missing configured MoKa agents: ${missing.join(", ")}`,
144
+ name: "moka-agents",
145
+ passed: false
146
+ } : {
147
+ detail: `visible: ${expected.join(", ")}`,
148
+ name: "moka-agents",
149
+ passed: true
150
+ } };
151
+ } catch (err) {
152
+ return skippedAgentVisibility(`Could not cheaply list OpenCode agents: ${commandErrorDetail(err)}`);
153
+ }
154
+ }
155
+ function skippedAgentVisibility(detail) {
156
+ return {
157
+ check: {
158
+ detail: "skipped because OpenCode agent listing is unavailable",
159
+ name: "moka-agents",
160
+ passed: true
161
+ },
162
+ warning: {
163
+ detail,
164
+ name: "moka-agents",
165
+ passed: true
166
+ }
167
+ };
168
+ }
169
+ function expectedRunAgentNames(config) {
170
+ const profiles = /* @__PURE__ */ new Set();
171
+ for (const catalog of Object.values(config.scheduler.node_catalogs)) for (const node of Object.values(catalog.nodes)) if (RUN_READINESS_CATEGORIES.has(node.category)) profiles.add(node.profile);
172
+ return [...profiles].filter((profileId) => config.profiles[profileId]?.runner === "opencode").map(opencodeAgentName).sort((a, b) => a.localeCompare(b));
173
+ }
174
+ function visibleAgentNames(stdout) {
175
+ const names = /* @__PURE__ */ new Set();
176
+ try {
177
+ const parsed = JSON.parse(stdout);
178
+ return {
179
+ ambiguous: false,
180
+ names,
181
+ recognized: collectAgentNames(parsed, names, Array.isArray(parsed))
182
+ };
183
+ } catch {
184
+ for (const line of stdout.split(LINE_RE)) {
185
+ const name = line.trim().replace(BULLET_PREFIX_RE, "");
186
+ if (name) names.add(name);
187
+ }
188
+ }
189
+ return {
190
+ ambiguous: true,
191
+ names,
192
+ recognized: names.size > 0
193
+ };
194
+ }
195
+ function collectAgentNames(value, names, inAgentList) {
196
+ if (typeof value === "string") {
197
+ if (inAgentList) names.add(value);
198
+ return inAgentList;
199
+ }
200
+ if (Array.isArray(value)) {
201
+ let recognized = inAgentList;
202
+ for (const item of value) recognized = collectAgentNames(item, names, inAgentList) || recognized;
203
+ return recognized;
204
+ }
205
+ if (!(value && typeof value === "object")) return false;
206
+ const record = value;
207
+ let recognized = false;
208
+ for (const key of [
209
+ "agent",
210
+ "id",
211
+ "name",
212
+ "subagent_type",
213
+ "title"
214
+ ]) if (typeof record[key] === "string") {
215
+ names.add(record[key]);
216
+ recognized = true;
217
+ }
218
+ for (const key of [
219
+ "agents",
220
+ "data",
221
+ "items",
222
+ "result"
223
+ ]) {
224
+ const item = record[key];
225
+ if (Array.isArray(item) || item && typeof item === "object") recognized = collectAgentNames(item, names, true) || recognized;
226
+ }
227
+ return recognized;
228
+ }
229
+ function headlessPermissionWarnings(cwd) {
230
+ if (!isHeadless()) return [];
231
+ const agentDir = join(cwd, ".opencode", "agents");
232
+ if (!existsSync(agentDir)) return [];
233
+ return readdirSync(agentDir).filter((entry) => entry.endsWith(".md")).flatMap((entry) => headlessPermissionWarning(join(agentDir, entry), entry));
234
+ }
235
+ function headlessPermissionWarning(path, entry) {
236
+ try {
237
+ if (!statSync(path).isFile()) return [];
238
+ const risky = interactivePermissionPaths(matter(readFileSync(path, "utf8")).data.permission);
239
+ if (risky.length === 0) return [];
240
+ return [{
241
+ detail: `${entry} requires interactive permission prompts at ${risky.join(", ")}; headless MoKa runs may block.`,
242
+ name: "headless-permissions",
243
+ passed: true
244
+ }];
245
+ } catch (err) {
246
+ return [{
247
+ detail: `Could not inspect ${entry} for headless permission risks: ${commandErrorDetail(err)}`,
248
+ name: "headless-permissions",
249
+ passed: true
250
+ }];
251
+ }
252
+ }
253
+ function interactivePermissionPaths(value, path = ["permission"]) {
254
+ if (typeof value === "string") return HEADLESS_AGENT_PERMISSION_VALUES.has(value.toLowerCase()) ? [path.join(".")] : [];
255
+ if (Array.isArray(value)) return value.flatMap((item, index) => interactivePermissionPaths(item, [...path, String(index)]));
256
+ if (!(value && typeof value === "object")) return [];
257
+ return Object.entries(value).flatMap(([key, item]) => interactivePermissionPaths(item, [...path, key]));
258
+ }
259
+ function isHeadless() {
260
+ const ci = process.env.CI?.toLowerCase();
261
+ return ci !== void 0 && ci !== "" && ci !== "0" && ci !== "false" || !process.stdin.isTTY;
262
+ }
263
+ function commandErrorDetail(err) {
264
+ const error = err;
265
+ return (error.shortMessage || error.stderr || error.message || String(err)).trim() || "not available";
266
+ }
267
+ //#endregion
268
+ export { runDoctor };
@@ -3,7 +3,8 @@ const LINE_RE = /\r?\n/;
3
3
  function createTerminalRuntimeReporter(write = (message) => console.log(message)) {
4
4
  const state = { attempts: /* @__PURE__ */ new Map() };
5
5
  return (event) => {
6
- write(formatRuntimeProgressMessage(event, state));
6
+ const message = formatRuntimeProgressMessage(event, state);
7
+ if (message !== null) write(message);
7
8
  };
8
9
  }
9
10
  function formatRuntimeProgressMessage(event, state = { attempts: /* @__PURE__ */ new Map() }) {
@@ -72,7 +73,7 @@ function formatRuntimeEventOutput(output) {
72
73
  function formatRepairProgress(event) {
73
74
  switch (event.type) {
74
75
  case "output.repair": return `Output repair ${event.passed ? "passed" : "failed"}: ${event.nodeId} attempt=${event.attempt}${event.reason ? ` (${event.reason})` : ""}`;
75
- default: throw new Error(`Unhandled runtime event: ${event.type}`);
76
+ default: return null;
76
77
  }
77
78
  }
78
79
  function formatObservabilityProgress(event) {
@@ -117,7 +118,9 @@ function formatRuntimeFailure(result) {
117
118
  return lines.join("\n");
118
119
  }
119
120
  function formatDoctorResult(result) {
120
- return [`Doctor: ${result.passed ? "PASS" : "FAIL"}`, ...result.checks.map((check) => `- ${check.passed ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`)].join("\n");
121
+ const lines = [`Doctor: ${result.passed ? "PASS" : "FAIL"}`, ...result.checks.map((check) => `- ${check.passed ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`)];
122
+ if (result.warnings?.length) lines.push(...result.warnings.map((check) => `- WARN ${check.name}: ${check.detail}`));
123
+ return lines.join("\n");
121
124
  }
122
125
  function appendIndentedSection(lines, label, values) {
123
126
  const text = values.filter(Boolean).join("\n").trim();