@mcrescenzo/opencode-workflows 0.1.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.
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +746 -0
- package/SECURITY.md +38 -0
- package/commands/repo-bughunt.md +94 -0
- package/commands/repo-review.md +148 -0
- package/commands/workflow-live-gates-release-check.md +143 -0
- package/docs/workflow-plugin.md +400 -0
- package/opencode-workflows.js +5 -0
- package/package.json +86 -0
- package/skills/opencode-workflow-authoring/SKILL.md +180 -0
- package/skills/repo-review-command-protocol/SKILL.md +56 -0
- package/skills/workflow-model-tiering/SKILL.md +57 -0
- package/skills/workflow-plan-review/SKILL.md +91 -0
- package/workflow-kernel/approval-hashing.js +39 -0
- package/workflow-kernel/async-util.js +33 -0
- package/workflow-kernel/audited-shell-policy.js +200 -0
- package/workflow-kernel/authority-policy.js +670 -0
- package/workflow-kernel/budget-accounting.js +142 -0
- package/workflow-kernel/capability-adapter.js +753 -0
- package/workflow-kernel/child-agent-runner.js +1264 -0
- package/workflow-kernel/constants.js +117 -0
- package/workflow-kernel/diagnostics.js +152 -0
- package/workflow-kernel/drain-runtime.js +421 -0
- package/workflow-kernel/errors.js +181 -0
- package/workflow-kernel/event-journal.js +487 -0
- package/workflow-kernel/extension-registry.js +144 -0
- package/workflow-kernel/free-text-redactor.js +91 -0
- package/workflow-kernel/gate-shapes.js +82 -0
- package/workflow-kernel/git-util.js +45 -0
- package/workflow-kernel/index.js +72 -0
- package/workflow-kernel/integration-mode.js +155 -0
- package/workflow-kernel/lane-effort-policy.js +134 -0
- package/workflow-kernel/lifecycle-control.js +608 -0
- package/workflow-kernel/live-gate-probes.js +916 -0
- package/workflow-kernel/notification-toast-cards.js +393 -0
- package/workflow-kernel/notification-toast-policy.js +179 -0
- package/workflow-kernel/notification-toast-scope.js +100 -0
- package/workflow-kernel/notification-toast.js +287 -0
- package/workflow-kernel/path-policy.js +219 -0
- package/workflow-kernel/result-readback.js +106 -0
- package/workflow-kernel/role-template-loading.js +606 -0
- package/workflow-kernel/run-context.js +139 -0
- package/workflow-kernel/run-observability.js +43 -0
- package/workflow-kernel/run-store-fs.js +231 -0
- package/workflow-kernel/run-store-locks.js +180 -0
- package/workflow-kernel/run-store-projections.js +421 -0
- package/workflow-kernel/run-store-rehydrate.js +68 -0
- package/workflow-kernel/run-store-state.js +147 -0
- package/workflow-kernel/run-store-status-format.js +1154 -0
- package/workflow-kernel/run-store-status.js +107 -0
- package/workflow-kernel/sandbox-executor.js +1131 -0
- package/workflow-kernel/session-access.js +56 -0
- package/workflow-kernel/structured-output.js +143 -0
- package/workflow-kernel/test-fix-drain-adapter.js +119 -0
- package/workflow-kernel/text-json.js +136 -0
- package/workflow-kernel/workflow-plugin.js +3017 -0
- package/workflow-kernel/workflow-source.js +444 -0
- package/workflow-kernel/worktree-adapter.js +256 -0
- package/workflow-kernel/worktree-git.js +147 -0
- package/workflows/repo-bughunt.js +362 -0
- package/workflows/repo-cleanup.js +383 -0
- package/workflows/repo-complexity.js +404 -0
- package/workflows/repo-deps.js +457 -0
- package/workflows/repo-modernize.js +395 -0
- package/workflows/repo-perf.js +394 -0
- package/workflows/repo-review.js +831 -0
- package/workflows/repo-security-audit.js +466 -0
- package/workflows/repo-test-gaps.js +377 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# Workflow Plugin: Source-Of-Truth Hierarchy And Transcript Fallback
|
|
2
|
+
|
|
3
|
+
> Status: **active technical contract**. This is the deep reference for the
|
|
4
|
+
> shipped workflow tool surface and recovery/source-of-truth behavior.
|
|
5
|
+
|
|
6
|
+
This document is the reference for how the opencode-workflows plugin decides what
|
|
7
|
+
to trust about a workflow run, and for the transcript-fallback / salvage
|
|
8
|
+
architecture that recovers orphaned lane results after a crash. It mirrors
|
|
9
|
+
shipped behavior; the README's "Source Of Truth And Transcript Fallback" section
|
|
10
|
+
is the operator-facing summary, and this file is the deeper architecture note.
|
|
11
|
+
|
|
12
|
+
## Workflow Tool Reference
|
|
13
|
+
|
|
14
|
+
| Tool | Mutability | Approval / Hash Requirements | Safe Next Readback |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| `workflow_run` | Preview is read-only; approved execution creates run state and may launch lanes or approved domain mutations. | Default path: first call returns `approvalHash`; execution requires `approve: true` plus the matching `approvalHash`. With configured `options.autoApprove`, eligible `readOnly` / `worktree` / `all` tier runs can launch on the first call; `args.autoApprove` can only narrow the configured ceiling. Resume preserves the approved envelope unless changed. | `workflow_status({ runId, detail: "compact" })`, then `workflow_status({ runId, detail: "result" })` at terminal state. |
|
|
17
|
+
| `workflow_status` | Read-only. | None; `detail: "result"` requires a `runId`. | This is the authoritative readback surface. |
|
|
18
|
+
| `workflow_events` | Read-only. | None; requires a `runId`. | Use this for redacted `events.jsonl` access with `typePrefix`, `limit`, `offset`, and timestamp filters. |
|
|
19
|
+
| `workflow_reconcile` | Mutating recovery; persists stale-run recovery state and clears stale locks. | No approval hash; write-permission gated. | `workflow_status({ runId, detail: "full" })`. |
|
|
20
|
+
| `workflow_cancel` | Mutating lifecycle request; asks active or durable runs to cancel. | No approval hash; write-permission gated. | `workflow_status({ runId, detail: "full" })`. |
|
|
21
|
+
| `workflow_pause` | Mutating lifecycle request; asks active or durable runs to pause. | No approval hash; write-permission gated. | `workflow_status({ runId, detail: "full" })`; resume with `workflow_run({ resumeRunId })`. |
|
|
22
|
+
| `workflow_kill` | Mutating force-interrupt request for wedged runs. | No approval hash; write-permission gated. | `workflow_status({ runId, detail: "full" })`; resume only after interrupted/stale-lock state is settled. |
|
|
23
|
+
| `workflow_save` | Writes saved workflow source. | No approval hash; write-permission gated. | `workflow_list({ format: "json" })`. |
|
|
24
|
+
| `workflow_list` | Read-only. | None. | This is the machine-canonical workflow discovery surface. |
|
|
25
|
+
| `workflow_cleanup` | Dry-run is read-only; non-dry deletes safe terminal run directories. | No approval hash; write-permission gated for deletion. | Run with `dryRun: true` first; then `workflow_status({ limit, detail: "compact" })`. |
|
|
26
|
+
| `workflow_apply` | Mutates the primary tree and finalizes staged domain mutations. | Requires `approvalIntent: "apply"`, `approvedSourceHash`, `baseCommit`, `diffPlanHash`, and `domainMutationHash` from the reviewed run status. | `workflow_status({ runId, detail: "full" })` or `workflow_status({ runId, detail: "result" })` after apply. |
|
|
27
|
+
| `workflow_salvage` | Preview is read-only; approved salvage writes tagged synthetic journal entries for recovered read-only lanes. | Preview returns `approvalHash`; write requires `approve: true` plus matching `approvalHash`. | `workflow_status({ runId, detail: "full" })`. |
|
|
28
|
+
| `workflow_roles` | Read-only. | None. | This is the role prompt/hash/defaults readback surface. |
|
|
29
|
+
| `workflow_models` | Read-only. | None. | This is the model availability readback surface. |
|
|
30
|
+
| `workflow_templates` | Read-only; source retrieval is explicit. | None. | This is the shipped-template readback surface. |
|
|
31
|
+
| `workflow_template_save` | Writes saved workflow source from a shipped template. | No approval hash; write-permission gated. | `workflow_list({ format: "json" })`. |
|
|
32
|
+
| `workflow_live_gates` | No-probe report is read-only; probes/reset can create child sessions, scratch worktrees, and mutate probe cache. | Probe/reset flags require `approvalIntent: "probe"`; no hash. | `workflow_live_gates({ format: "json" })`; use `/workflow-live-gates-release-check` for full release evidence. |
|
|
33
|
+
|
|
34
|
+
## Role Prompts And Defaults
|
|
35
|
+
|
|
36
|
+
Workflow roles keep prompt text in user-editable `.md` files. A sibling
|
|
37
|
+
`roles.json` file may declare typed defaults per role, including `model`,
|
|
38
|
+
`tier`, `tools`, `readOnly`, `retryCount`, `correctiveRetries`, `timeoutMs`,
|
|
39
|
+
`mcpPolicy`, `secretGlobs`, and `effort`. The defaults are merged before
|
|
40
|
+
explicit per-lane `agent()` opts, so explicit opts win, and the existing lane
|
|
41
|
+
authority policy still rejects any tool or policy escalation beyond the approved
|
|
42
|
+
run authority. `workflow_roles({ format: "json" })` reports each role's prompt
|
|
43
|
+
hash provenance and typed defaults.
|
|
44
|
+
|
|
45
|
+
## How work moves between agents
|
|
46
|
+
|
|
47
|
+
The plugin passes work between subagents via **controller-owned, in-memory
|
|
48
|
+
return values** (`agent()`, `parallel()`, `pipeline()`), validated structured
|
|
49
|
+
outputs, and durable run artifacts. Subagents never hand off directly to each
|
|
50
|
+
other; the controller — the trusted kernel — is always the hub. Transcripts are
|
|
51
|
+
not the handoff substrate; they are a fallback for a narrow crash window only.
|
|
52
|
+
|
|
53
|
+
## Source-of-truth hierarchy
|
|
54
|
+
|
|
55
|
+
Stronger evidence always wins. Weaker evidence is recovery/diagnostic only and
|
|
56
|
+
may never finalize work (close Beads, apply diffs, merge integration lanes):
|
|
57
|
+
|
|
58
|
+
1. **Controller-owned run artifacts (authoritative).**
|
|
59
|
+
The append-only `journal.jsonl`, the durable `result.json`, the domain and
|
|
60
|
+
integration ledgers, and integration worktrees under
|
|
61
|
+
`.opencode/workflows/runs/<runId>/`. These are captured directly by the
|
|
62
|
+
controller and are the only evidence that may finalize domain mutations or
|
|
63
|
+
primary-tree writes. `journal.jsonl` is the authoritative resume cache.
|
|
64
|
+
2. **`workflow_status` (persisted inspection).**
|
|
65
|
+
The authoritative read-only inspection and recovery surface over those
|
|
66
|
+
artifacts. `detail: "result"` returns final workflow output; `detail: "full"`
|
|
67
|
+
is for diagnostics/apply internals. `workflow_status` never mutates state.
|
|
68
|
+
Foreground `workflow_run` also includes the redacted workflow return value
|
|
69
|
+
inline when it fits `MAX_INLINE_RESULT_BYTES`; larger inline payloads fall back
|
|
70
|
+
to the persisted result readback command. `detail: "result"` uses
|
|
71
|
+
full-fidelity, secrets-only redaction while the readback fits
|
|
72
|
+
`MAX_RESULT_READBACK_BYTES`; above that it returns a partial projection plus
|
|
73
|
+
`resultReadback.truncated` metadata instead of refusing the result file.
|
|
74
|
+
`workflow_events` is the matching sanctioned event-level reader for
|
|
75
|
+
redacted lifecycle evidence from `events.jsonl`; use it instead of raw file
|
|
76
|
+
reads when you need cache, retry, fanout, debug-capture, or lifecycle events.
|
|
77
|
+
3. **Session transcripts (diagnostic / fallback only).**
|
|
78
|
+
Child-session message history persisted in OpenCode's session store.
|
|
79
|
+
Transcript evidence is strictly weaker than a controller-captured structured
|
|
80
|
+
result. It is used only to surface and salvage orphaned lanes — never as the
|
|
81
|
+
primary substrate, never to finalize work.
|
|
82
|
+
|
|
83
|
+
Summarized: `journal / result / ledgers / worktrees` > `workflow_status` >
|
|
84
|
+
`workflow_events` > `session transcripts (diagnostic only)`.
|
|
85
|
+
|
|
86
|
+
## Debug capture mode
|
|
87
|
+
|
|
88
|
+
Debug capture is off by default. Enable it for one run with
|
|
89
|
+
`workflow_run({ debugCapture: true })` or for the process with
|
|
90
|
+
`OPENCODE_WORKFLOWS_DEBUG_CAPTURE=1`.
|
|
91
|
+
|
|
92
|
+
When enabled, each completed child lane writes private artifacts under
|
|
93
|
+
`.opencode/workflows/runs/<runId>/debug/<lane>/`:
|
|
94
|
+
|
|
95
|
+
- `prompt.md`: rendered system prompt plus task prompt after free-text secret
|
|
96
|
+
redaction.
|
|
97
|
+
- `schema.json`: the lane schema after durable secret redaction.
|
|
98
|
+
- `transcript.jsonl`: child-session messages fetched via the SDK
|
|
99
|
+
`session.messages` path after durable secret redaction.
|
|
100
|
+
|
|
101
|
+
Each file is written with private file mode and a bounded size. Capture failures
|
|
102
|
+
are recorded as `debug_capture.*` events and do not fail the lane. This mode
|
|
103
|
+
increases local sensitive evidence and disk usage, so the default flag-off
|
|
104
|
+
behavior creates no `debug/` directory and makes no extra `session.messages`
|
|
105
|
+
calls.
|
|
106
|
+
|
|
107
|
+
`MAX_RESULT_BYTES` remains the guest return cap enforced before persistence.
|
|
108
|
+
It is intentionally coupled to the 32 MB QuickJS heap in
|
|
109
|
+
`workflow-kernel/sandbox-executor.js`; raising `MAX_RESULT_BYTES` or
|
|
110
|
+
`MAX_SOURCE_BYTES` should be reviewed with that heap limit, otherwise clean size
|
|
111
|
+
errors can become guest out-of-memory failures.
|
|
112
|
+
|
|
113
|
+
## The crash window
|
|
114
|
+
|
|
115
|
+
`runChildAgent` persists the running lane projection (`lanes/<callId>.json`,
|
|
116
|
+
including `childID` and `signatureHash`) before `session.prompt`, but the
|
|
117
|
+
authoritative journal entry is written by `recordLaneOutcome` only after the
|
|
118
|
+
prompt returns, structured-output validation, and integration steps. If the
|
|
119
|
+
owning OpenCode process dies in that window, a completed child lane's result is
|
|
120
|
+
absent from workflow state even though the child's transcript still contains it.
|
|
121
|
+
|
|
122
|
+
A Phase 0 spike verified that child-session transcripts (including assistant
|
|
123
|
+
final replies) persist to OpenCode's session store and are readable via the SDK
|
|
124
|
+
by a fresh process pointing at the same data dir after the creating process has
|
|
125
|
+
died. That makes transcript salvage technically viable, but the recovered
|
|
126
|
+
evidence is still weaker than a controller-captured result, so salvage is opt-in,
|
|
127
|
+
schema-checked, tagged, and never auto-applies.
|
|
128
|
+
|
|
129
|
+
## `workflow_salvage` (preview/approve, read-only scope)
|
|
130
|
+
|
|
131
|
+
`workflow_salvage` is the explicit, hash-gated recovery path for orphaned
|
|
132
|
+
read-only lanes. It is a mutating tool gated like `workflow_reconcile`
|
|
133
|
+
(`assertWriteWorkflowAllowed`) and is denied in plan mode.
|
|
134
|
+
|
|
135
|
+
Tool arguments:
|
|
136
|
+
|
|
137
|
+
| Arg | Meaning |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `runId` (required) | The interrupted run to salvage from. |
|
|
140
|
+
| `callIds` (optional) | Narrow salvage to specific lane call ids. |
|
|
141
|
+
| `approve` (optional) | `true` to write; otherwise preview only. |
|
|
142
|
+
| `approvalHash` (optional) | Must match the preview's recomputed hash to write. |
|
|
143
|
+
|
|
144
|
+
### Preview mode
|
|
145
|
+
|
|
146
|
+
A call without `approve` (or with a non-matching `approvalHash`) returns
|
|
147
|
+
`mode: "preview"` and writes nothing. For each candidate lane it reports:
|
|
148
|
+
|
|
149
|
+
- `callId`, `childID`
|
|
150
|
+
- `parseVerdict` (`valid` / `invalid`) and `validationKind: "json-parse"` with
|
|
151
|
+
`originalSchemaAvailable: false`
|
|
152
|
+
- `finalMessageFound`, `finalMessageLength`
|
|
153
|
+
- `resumeSignatureAvailable` (whether the running projection captured a
|
|
154
|
+
`signatureHash` that the resume path can match)
|
|
155
|
+
- a length-truncated `redactedSnippet` of the final assistant message, with
|
|
156
|
+
free-text secret masking (`redactFreeTextSecrets`) applied before truncation so a
|
|
157
|
+
credential pasted into the assistant reply is masked out of the preview (the raw
|
|
158
|
+
transcript under the run directory remains local-sensitive)
|
|
159
|
+
- `skipped` reason for non-salvageable lanes
|
|
160
|
+
|
|
161
|
+
It also returns an `approvalHash` computed over the per-candidate preview state,
|
|
162
|
+
so the operator approves exactly the transcript state they previewed.
|
|
163
|
+
|
|
164
|
+
### Approve mode
|
|
165
|
+
|
|
166
|
+
Re-running with `approve: true` and the matching `approvalHash` writes a
|
|
167
|
+
synthetic journal entry for each non-skipped read-only lane, via
|
|
168
|
+
`writeSalvagedLaneOutcome` (a journal append plus lane-projection update on a
|
|
169
|
+
durable interrupted run directory). It never touches in-memory counters,
|
|
170
|
+
`state.json`, worktrees, or integration ledgers.
|
|
171
|
+
|
|
172
|
+
Validation is conservative JSON-parse only. The original per-lane AJV schema is
|
|
173
|
+
not durably persisted (it lives only in the in-memory resolved lane context
|
|
174
|
+
derived from the workflow script at runtime), so it cannot be reconstructed for
|
|
175
|
+
an existing orphan. Outcome is `success` only when the final assistant message
|
|
176
|
+
parses as JSON; otherwise the entry is written with outcome `failure` and an
|
|
177
|
+
error summary, and carries no `result`.
|
|
178
|
+
|
|
179
|
+
## The `salvagedFromTranscript` tag
|
|
180
|
+
|
|
181
|
+
Every salvaged journal entry is tagged:
|
|
182
|
+
|
|
183
|
+
- `salvagedFromTranscript: true`
|
|
184
|
+
- `salvageValidation: { kind: "json-parse", originalSchemaAvailable: false }`
|
|
185
|
+
|
|
186
|
+
so a transcript-recovered result is never mistaken for a normally-captured,
|
|
187
|
+
schema-validated result. The tag is the provenance marker the resume path and the
|
|
188
|
+
integration gates read.
|
|
189
|
+
|
|
190
|
+
## Read-only scope and the no-auto-apply rule
|
|
191
|
+
|
|
192
|
+
- **Read-only/report lanes only.** Edit/integration lanes (those carrying a
|
|
193
|
+
worktree path or integration-lane marker) are reported as
|
|
194
|
+
`salvage-skipped: edit-lane-without-commit` and are never salvaged. A salvaged
|
|
195
|
+
lane has no worktree commit by construction, and `integrate()` already requires
|
|
196
|
+
`lane.committed`. Unreadable transcripts are reported `transcript-unreadable:*`
|
|
197
|
+
and skipped.
|
|
198
|
+
- **Never finalizes domain mutations or primary writes.** Salvage never calls
|
|
199
|
+
`integrate()` or `runAutoApply`, never closes Beads, and never applies a diff.
|
|
200
|
+
It only appends a tagged synthetic journal entry and updates a lane projection.
|
|
201
|
+
- **No auto-apply / always explicit.** Salvage is never automatic on resume. It
|
|
202
|
+
requires an explicit preview, then an explicit approve with a matching hash.
|
|
203
|
+
|
|
204
|
+
## Resume reuse and the code-enforced read-only-vs-edit asymmetry
|
|
205
|
+
|
|
206
|
+
On a later resume:
|
|
207
|
+
|
|
208
|
+
- A salvaged read-only result is reused as a cache hit (no re-run, no spend
|
|
209
|
+
re-accumulation) and emits a distinct `cache.salvaged_hit` event — separate
|
|
210
|
+
from `cache.hit` (normal journal replay) and `cache.checkpoint_hit` (lane
|
|
211
|
+
checkpoint replay) — so the weaker provenance stays observable. This works
|
|
212
|
+
because the running lane projection persists `signatureHash` and the salvaged
|
|
213
|
+
entry copies it, so the existing signature-based cache-hit predicate
|
|
214
|
+
(`classifyResumeCacheHit`) fires naturally. Legacy orphans interrupted before
|
|
215
|
+
the signature was persisted report `resumeSignatureAvailable: false` and are
|
|
216
|
+
re-run rather than trusted.
|
|
217
|
+
- If an edited workflow body inserts or reorders `agent()` calls, the new
|
|
218
|
+
positional `callId` may no longer match the prior journal entry. When the
|
|
219
|
+
lane's content signature still matches a prior successful journal entry in the
|
|
220
|
+
same fan-out scope, resume claim-once reuses that entry and emits
|
|
221
|
+
`cache.signature_hit` with `originalCallId`. Edit and integration plan entries
|
|
222
|
+
copied from the prior state are retagged in place to the new `callId`, avoiding
|
|
223
|
+
duplicate patch or lane entries.
|
|
224
|
+
- Integration lanes are filtered through `isLaneIntegrable`, which requires a
|
|
225
|
+
real worktree commit and rejects any `salvagedFromTranscript` lane even if it
|
|
226
|
+
somehow carried `committed: true`. The host-owned integrate closure adds
|
|
227
|
+
defense-in-depth, rejecting salvaged lanes with reason
|
|
228
|
+
`lane-salvaged-from-transcript`. So a salvaged claim can never reach
|
|
229
|
+
`integrate()` or `runAutoApply` — the asymmetry is enforced in code, not just
|
|
230
|
+
documented.
|
|
231
|
+
|
|
232
|
+
## Raw SDK transcript reads
|
|
233
|
+
|
|
234
|
+
The controller reads child transcripts via the **raw SDK `session.messages`
|
|
235
|
+
API** (`sessionApi(pluginContext).messages({ sessionID })`), unredacted. It does
|
|
236
|
+
**not** route through the redacting `session_read` wrapper from the separate,
|
|
237
|
+
independently published [`@mcrescenzo/opencode-sessions`](https://github.com/mcrescenzo/opencode-sessions)
|
|
238
|
+
plugin (not a dependency of this package — named here only as a point of
|
|
239
|
+
comparison). This is intentional and consistent with the journal already storing
|
|
240
|
+
unredacted lane results; salvage recovers that same class of content. The preview
|
|
241
|
+
`redactedSnippet` is free-text secret-masked (`redactFreeTextSecrets`, detecting
|
|
242
|
+
Bearer/provider-token/AWS-key and common `key=value` secret assignments) and then
|
|
243
|
+
length-truncated for operator diagnosis. The masking governs only what is rendered
|
|
244
|
+
back to the operator at this display boundary; the raw transcript under the run
|
|
245
|
+
directory and the approval/source hashes (computed from `finalMessageHash`, never
|
|
246
|
+
from the masked snippet) remain local-sensitive and unaffected.
|
|
247
|
+
|
|
248
|
+
## Narrowing the crash window: lane checkpoints
|
|
249
|
+
|
|
250
|
+
Workflow-native lane checkpointing narrows the crash window so transcript salvage
|
|
251
|
+
is rarely needed. The controller writes:
|
|
252
|
+
|
|
253
|
+
- `lanes/<callId>.request.json` before `session.prompt` (prompt-time intent), and
|
|
254
|
+
- `lanes/<callId>.result.json` immediately after the prompt returns.
|
|
255
|
+
|
|
256
|
+
On resume, a same-signature `result.json` checkpoint is consulted **before** the
|
|
257
|
+
authoritative journal check and reused without re-running the lane, emitting a
|
|
258
|
+
distinct `cache.checkpoint_hit` event. Checkpoint files are a narrower, earlier,
|
|
259
|
+
transcript-independent own-store capture; `journal.jsonl` remains the source of
|
|
260
|
+
truth and supersedes them (a superseded checkpoint is removed). Unlike transcript
|
|
261
|
+
salvage, checkpoint recovery is not approval-gated, because it is a
|
|
262
|
+
controller-owned own-store capture rather than weaker transcript evidence. A
|
|
263
|
+
failed checkpoint write degrades to re-running or to ordinary journal replay on
|
|
264
|
+
the next resume.
|
|
265
|
+
|
|
266
|
+
## Background execution is not durable across process death
|
|
267
|
+
|
|
268
|
+
Transcript fallback recovers a completed child's result; it does **not** make
|
|
269
|
+
background execution durable.
|
|
270
|
+
|
|
271
|
+
A background run (`workflow_run({ background: true })`) executes inside the
|
|
272
|
+
owning OpenCode process and dies with it. When `background` is omitted, the
|
|
273
|
+
kernel defaults wide/deep/long runs to background when requested fan-out or
|
|
274
|
+
declared/requested duration trips the heuristic (per-call `maxAgents >= 8`, at
|
|
275
|
+
least three serialized concurrency waves from per-call `maxAgents` and
|
|
276
|
+
`concurrency`, or `maxRuntimeMs >= 600000`). Declared/default `maxAgents`
|
|
277
|
+
ceilings are not treated as predictions of actual fan-out. Explicit
|
|
278
|
+
`background: true` / `background: false` always wins, and resume keeps the
|
|
279
|
+
original pinned mode. If the host lacks `session.promptAsync`, background launch
|
|
280
|
+
still works but `workflow_run` warns that no completion prompt can be delivered;
|
|
281
|
+
use `workflow_status` polling for completion and final result readback.
|
|
282
|
+
|
|
283
|
+
There is no detached supervisor, no respawn, and no attach. After process death
|
|
284
|
+
the run directory is left behind and surfaces as stale until `workflow_reconcile`
|
|
285
|
+
marks it interrupted. Only then can `workflow_salvage` recover orphaned
|
|
286
|
+
read-only lane results that completed in the crash window. Completing the
|
|
287
|
+
underlying workflow work after process death remains out of scope until a
|
|
288
|
+
supervisor exists (tracked separately in `docs/claude-parity-roadmap.md`).
|
|
289
|
+
|
|
290
|
+
Notification recovery (re-enqueuing an already-persisted completion notice on
|
|
291
|
+
`session.idle`) is likewise only in-process recovery, not durable execution; see
|
|
292
|
+
the README's "Notification recovery is not durable execution" section.
|
|
293
|
+
|
|
294
|
+
## Sizing `maxAgents`: child-agent accounting
|
|
295
|
+
|
|
296
|
+
`maxAgents` is a hard cap on the number of **child agent lanes a run launches**,
|
|
297
|
+
not on phases, on `parallel()` calls, or on JavaScript work in the workflow body.
|
|
298
|
+
The kernel tracks one counter, `agentsStarted`, and every `agent()` launch
|
|
299
|
+
increments it; a launch is refused with `WorkflowBudgetStoppedError` once
|
|
300
|
+
`agentsStarted >= maxAgents` (`workflow-kernel/child-agent-runner.js`:
|
|
301
|
+
`run.agentsStarted >= run.maxAgents`). The remaining slots are visible to the
|
|
302
|
+
workflow body as `await budget.remainingAgents()`
|
|
303
|
+
(`maxAgents - agentsStarted`, floored at 0).
|
|
304
|
+
|
|
305
|
+
**What counts as one agent:**
|
|
306
|
+
|
|
307
|
+
- Each `agent()` call counts **once**. A `parallel()` of five lanes is five
|
|
308
|
+
`agent()` launches and consumes five slots; a `pipeline()` of three stages is
|
|
309
|
+
three launches and consumes three slots.
|
|
310
|
+
- A lane that **replaces a prior lane on resume** (a cache-hit replay) does
|
|
311
|
+
**not** re-increment `agentsStarted` — only genuinely new launches consume
|
|
312
|
+
budget — so a resumed run does not double-count work it already paid for.
|
|
313
|
+
|
|
314
|
+
**What does NOT count:**
|
|
315
|
+
|
|
316
|
+
- **Pure-JavaScript aggregation / synthesis inside the workflow body.** Reducing,
|
|
317
|
+
merging, ranking, deduping, or formatting the structured return values of
|
|
318
|
+
lanes in plain JS (no `agent()` call) consumes **zero** agent slots. The
|
|
319
|
+
controller already owns those return values in memory; combining them is host
|
|
320
|
+
code, not a child lane.
|
|
321
|
+
- Phases, `log()`, `phase()`, and `budget` reads. These are control-flow and
|
|
322
|
+
bookkeeping, not child launches.
|
|
323
|
+
|
|
324
|
+
`budget.ceilings()` returns the approved `{ maxCost, maxTokens }` object, with
|
|
325
|
+
unset ceilings omitted. `budget.remaining()` returns
|
|
326
|
+
`{ cost: number|null, tokens: number|null }`; `null` means that ceiling is unset,
|
|
327
|
+
and numeric values include live spend, replayed spend, and in-flight
|
|
328
|
+
reservations. Use it with `budget.remainingAgents()` for loop-until-budget
|
|
329
|
+
workflows that should stop before launching another lane.
|
|
330
|
+
|
|
331
|
+
### Synthesis: pure-JS versus agent-based
|
|
332
|
+
|
|
333
|
+
Whether a synthesis/aggregation step consumes a slot depends entirely on **how it
|
|
334
|
+
is implemented**, not on what it is called:
|
|
335
|
+
|
|
336
|
+
- **Pure-JS synthesis** (combine lane outputs in the workflow body): does not
|
|
337
|
+
call `agent()`, so it needs **no extra slot**.
|
|
338
|
+
- **Agent-based synthesis** (hand the lane outputs to one more `agent()` lane —
|
|
339
|
+
e.g. an LLM that writes a narrative report): that lane is a child launch and
|
|
340
|
+
needs **one more slot**.
|
|
341
|
+
|
|
342
|
+
### Worked examples
|
|
343
|
+
|
|
344
|
+
| Topology | Agent lanes | `maxAgents` needed |
|
|
345
|
+
| --- | --- | --- |
|
|
346
|
+
| Five research lanes, results merged in JS | 5 | `>= 5` |
|
|
347
|
+
| Five research lanes + one agent-based synthesis lane | 6 | `>= 6` |
|
|
348
|
+
| Three pipeline stages, JS post-processing | 3 | `>= 3` |
|
|
349
|
+
|
|
350
|
+
Size `maxAgents` to **(number of `agent()` lanes you will launch) + (1 per
|
|
351
|
+
agent-based synthesis/aggregation lane)**. Pure-JS synthesis adds nothing.
|
|
352
|
+
|
|
353
|
+
### Nested workflows share the parent budget
|
|
354
|
+
|
|
355
|
+
A `workflow()` call (nested workflow) does **not** get its own agent budget. The
|
|
356
|
+
nested body executes against the **same `RunContext`** as the parent
|
|
357
|
+
(`workflow-kernel/sandbox-executor.js`: `runNestedWorkflow` → `executeSandbox`
|
|
358
|
+
with the parent `run`), so every `agent()` launched inside the nested workflow
|
|
359
|
+
increments the **parent run's** `agentsStarted` and draws down the same
|
|
360
|
+
`maxAgents` ceiling. The nested workflow's own declared `meta.maxAgents` is
|
|
361
|
+
**ignored at runtime** — only the parent run's `maxAgents` (fixed at approval)
|
|
362
|
+
governs the combined launch count. Only one level of nesting is permitted, so the
|
|
363
|
+
budget you approve at the top is the budget for the whole tree: size it to cover
|
|
364
|
+
the parent lanes **plus** every lane any nested workflow will launch.
|
|
365
|
+
|
|
366
|
+
Nested workflow references must be statically visible at approval time. Use
|
|
367
|
+
`workflow("saved-name", args)` for saved workflows, or the explicit inline form
|
|
368
|
+
`workflow({ source: "return 1;", args })` for inline sources. The legacy string
|
|
369
|
+
source shorthand remains compatible for source strings containing a newline or
|
|
370
|
+
`export const meta`, but tiny inline sources should use the object form so they
|
|
371
|
+
are not mistaken for workflow names. Dynamic nested workflow names or sources are
|
|
372
|
+
rejected before approval because they cannot be snapshotted safely.
|
|
373
|
+
|
|
374
|
+
## Per-lane model effort
|
|
375
|
+
|
|
376
|
+
Workflow lanes may request an OpenAI reasoning-effort hint with
|
|
377
|
+
`agent(prompt, { effort: "minimal" | "low" | "medium" | "high" })`. The kernel
|
|
378
|
+
validates the option before child-session creation, includes the resolved effort
|
|
379
|
+
policy in the lane signature, registers the live child session in a bounded
|
|
380
|
+
childID map, and applies the setting through the plugin `chat.params` hook as:
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
output.options.providerOptions.openai.reasoningEffort = "<effort>";
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The hook merge preserves any existing `output.options` and provider option bags.
|
|
387
|
+
The childID mapping is cleared on retry teardown and final lane cleanup, so it is
|
|
388
|
+
bounded to active child sessions.
|
|
389
|
+
|
|
390
|
+
This is deliberately provider-specific. Today `effort` is supported only when
|
|
391
|
+
the resolved lane model provider is `openai`; requesting it for another provider
|
|
392
|
+
fails before launching the child instead of silently ignoring the option. Extend
|
|
393
|
+
the provider table only after verifying the provider's concrete `chat.params`
|
|
394
|
+
option key.
|
|
395
|
+
|
|
396
|
+
The native variant-selector path was inspected but is not a shipped contract for
|
|
397
|
+
this plugin. The pinned v1 `session.prompt` type used by the wrapper does not
|
|
398
|
+
expose `variant`, while the v2 generated type does; without a behavioral probe
|
|
399
|
+
proving `session.prompt` accepts a variant selector in this lane path, effort is
|
|
400
|
+
implemented only through the proven `chat.params` provider-options mechanism.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Plugin entry. The kernel barrel (workflow-kernel/index.js) owns the orchestrator
|
|
2
|
+
// export and the aggregated __test surface; the entry just re-exports the plugin factory
|
|
3
|
+
// so opencode loads exactly one factory here. Unit tests import the barrel (or the real
|
|
4
|
+
// modules) directly rather than this entry, so the entry no longer wires __test.
|
|
5
|
+
export { default } from "./workflow-kernel/index.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcrescenzo/opencode-workflows",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "opencode plugin providing durable, resumable multi-agent workflow orchestration (repo-review, a generic drain harness, and related workflows).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./opencode-workflows.js",
|
|
7
|
+
"homepage": "https://github.com/mcrescenzo/opencode-workflows#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/mcrescenzo/opencode-workflows/issues"
|
|
10
|
+
},
|
|
11
|
+
"author": "Michael Crescenzo",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./opencode-workflows.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"opencode-workflows.js",
|
|
17
|
+
"workflow-kernel/",
|
|
18
|
+
"workflows/",
|
|
19
|
+
"commands/",
|
|
20
|
+
"skills/",
|
|
21
|
+
"docs/workflow-plugin.md",
|
|
22
|
+
"README.md",
|
|
23
|
+
"CONTRIBUTING.md",
|
|
24
|
+
"CODE_OF_CONDUCT.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"SECURITY.md"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/mcrescenzo/opencode-workflows"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"opencode",
|
|
38
|
+
"opencode-plugin",
|
|
39
|
+
"workflow",
|
|
40
|
+
"orchestration"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.11.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "node --test tests/*.test.mjs",
|
|
47
|
+
"test:workflow-kernel": "node --test tests/drain-runtime.test.mjs tests/durable-state.test.mjs tests/multi-process-durability.test.mjs tests/notification-toast.test.mjs tests/run-store-status-salvage.test.mjs tests/workflow-checkpoint.test.mjs tests/workflow-salvage-resume.test.mjs tests/workflow-salvage.test.mjs",
|
|
48
|
+
"test:workflow-adapters": "node --test tests/beads-drain-adapter.test.mjs tests/test-fix-drain-adapter.test.mjs",
|
|
49
|
+
"test:beads-drain": "node --test tests/beads-drain-workflow.test.mjs tests/beads-drain-adapter.test.mjs tests/beads-drain-scratch.test.mjs tests/beads-drain-assets.test.mjs",
|
|
50
|
+
"test:extension-seam": "node --test tests/extension-registry.test.mjs tests/extension-wiring.test.mjs tests/extension-tool-contribution.test.mjs tests/extension-dir-resolution.test.mjs tests/extension-auto-apply-trust.test.mjs tests/extension-command-skill-registration.test.mjs",
|
|
51
|
+
"test:live-gates": "node --test tests/live-gate-probes.test.mjs tests/live-gates-integration.test.mjs tests/live-gates-permission.test.mjs tests/live-gates-runtime.test.mjs tests/live-gates-worktree.test.mjs",
|
|
52
|
+
"test:repo-bughunt": "node --test tests/repo-bughunt.test.mjs",
|
|
53
|
+
"test:repo-bughunt-command": "node --test tests/repo-bughunt-command-assets.test.mjs",
|
|
54
|
+
"test:repo-review-contract": "node --test tests/repo-review-leaf-contract.test.mjs",
|
|
55
|
+
"test:repo-review-command": "node --test tests/repo-review-command-assets.test.mjs",
|
|
56
|
+
"test:repo-review-cost-model-tier": "node --test tests/repo-review-cost-model-tier.test.mjs",
|
|
57
|
+
"test:repo-review-degraded-coverage": "node --test tests/repo-review-degraded-coverage.test.mjs",
|
|
58
|
+
"test:repo-review-meta-args": "node --test tests/repo-review-meta-arg-contract.test.mjs tests/repo-review-meta-smoke.test.mjs",
|
|
59
|
+
"test:repo-review-meta": "node --test tests/repo-review-meta.test.mjs",
|
|
60
|
+
"test:repo-review-meta-smoke": "node --test tests/repo-review-meta-smoke.test.mjs",
|
|
61
|
+
"test:repo-review-merge-determinism": "node --test tests/repo-review-merge-determinism.test.mjs",
|
|
62
|
+
"test:repo-review-no-mutation": "node --test tests/repo-review-no-mutation.test.mjs",
|
|
63
|
+
"test:repo-review-secret-containment": "node --test tests/repo-review-secret-containment.test.mjs",
|
|
64
|
+
"test:repo-security-audit": "node --test tests/repo-security-audit.test.mjs",
|
|
65
|
+
"test:repo-test-gaps": "node --test tests/repo-test-gaps.test.mjs",
|
|
66
|
+
"test:repo-cleanup": "node --test tests/repo-cleanup.test.mjs",
|
|
67
|
+
"test:repo-modernize": "node --test tests/repo-modernize.test.mjs",
|
|
68
|
+
"test:repo-perf": "node --test tests/repo-perf.test.mjs",
|
|
69
|
+
"test:repo-complexity": "node --test tests/repo-complexity.test.mjs",
|
|
70
|
+
"test:repo-deps": "node --test tests/repo-deps.test.mjs",
|
|
71
|
+
"test:lockfile-sync": "node scripts/check-lockfile-sync.mjs",
|
|
72
|
+
"test:workflows": "node --test tests/workflow-run.test.mjs tests/workflow-apply.test.mjs tests/model-tiering.test.mjs tests/repo-bughunt.test.mjs tests/repo-bughunt-command-assets.test.mjs tests/repo-review-leaf-contract.test.mjs tests/repo-security-audit.test.mjs tests/repo-test-gaps.test.mjs tests/repo-cleanup.test.mjs tests/repo-modernize.test.mjs tests/repo-perf.test.mjs tests/repo-complexity.test.mjs tests/repo-deps.test.mjs tests/repo-review-meta-arg-contract.test.mjs tests/repo-review-meta-defaults.test.mjs tests/repo-review-domain-scope.test.mjs tests/repo-review-lane-coverage.test.mjs tests/repo-review-artifacts.test.mjs tests/repo-review-inventory.test.mjs tests/repo-review-scalable-merge.test.mjs tests/repo-review-observability.test.mjs tests/repo-review-deep-modes.test.mjs tests/repo-review-docs-parity.test.mjs tests/repo-review-final-gate.test.mjs tests/repo-review-secret-containment.test.mjs tests/repo-review-meta-smoke.test.mjs tests/repo-review-meta.test.mjs tests/repo-review-merge-determinism.test.mjs tests/repo-review-no-mutation.test.mjs tests/repo-review-cost-model-tier.test.mjs tests/repo-review-degraded-coverage.test.mjs tests/repo-review-command-assets.test.mjs",
|
|
73
|
+
"release:no-token": "node scripts/release-no-token.mjs",
|
|
74
|
+
"release:child-system-smoke": "node scripts/child-system-smoke.mjs",
|
|
75
|
+
"release:system-smoke-required": "node scripts/child-system-smoke.mjs --required",
|
|
76
|
+
"test:parent-integration": "node scripts/parent-integration.mjs"
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"@opencode-ai/plugin": "^1.17.13",
|
|
80
|
+
"@opencode-ai/sdk": "^1.17.13",
|
|
81
|
+
"acorn": "8.17.0",
|
|
82
|
+
"ajv": "8.20.0",
|
|
83
|
+
"quickjs-emscripten": "0.32.0",
|
|
84
|
+
"quickjs-emscripten-core": "0.32.0"
|
|
85
|
+
}
|
|
86
|
+
}
|