@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,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: opencode-workflow-authoring
|
|
3
|
+
description: Use when creating, editing, reviewing, debugging, or explaining OpenCode workflows run by the workflow_* plugin tools. Covers QuickJS sandbox limits, top-level return shape, agent/parallel/pipeline fan-out, the scoped-callback arity contract, phase-loop orchestration, model tiers and per-lane effort, budget self-scaling, sourceHash/approvalHash and autoApprove launch modes, inline result readback, edit/apply boundaries, static nested workflows, roles, schema lanes, and background runs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OpenCode Workflow Authoring
|
|
7
|
+
|
|
8
|
+
Use this skill for workflow source and behavior: writing a new workflow, editing
|
|
9
|
+
or reviewing an existing one, debugging a stalled or misleading run, or
|
|
10
|
+
explaining how approval, sandboxing, fan-out, edit/apply, background, or resume
|
|
11
|
+
works. For simple "run / status / cancel / save this workflow" operations, use
|
|
12
|
+
the relevant command or `workflow_*` tool directly.
|
|
13
|
+
|
|
14
|
+
## Phase Loop
|
|
15
|
+
|
|
16
|
+
The primary pattern is **author -> run -> read -> decide**:
|
|
17
|
+
|
|
18
|
+
1. Author a small workflow source or choose a saved workflow.
|
|
19
|
+
2. Run a narrow slice first. Prefer `profile: "read-only-review"` unless the
|
|
20
|
+
question truly needs shell, network, MCP, or edit authority.
|
|
21
|
+
3. Read the result with `workflow_status({ runId, detail: "result" })`, or use
|
|
22
|
+
the inline foreground result returned by `workflow_run` when it fits the safe
|
|
23
|
+
display cap.
|
|
24
|
+
4. Decide whether to widen scope, add lanes, raise budget, switch authority, or
|
|
25
|
+
stop. Do not build one giant workflow before proving the slice.
|
|
26
|
+
|
|
27
|
+
If the plugin owner configured `options.autoApprove`, eligible runs can launch on
|
|
28
|
+
the first `workflow_run` call when their resolved authority tier is within that
|
|
29
|
+
ceiling. Otherwise, use the preview plus `approvalHash` flow. `workflow_apply`
|
|
30
|
+
keeps its separate hash gate either way.
|
|
31
|
+
|
|
32
|
+
## Source Shape
|
|
33
|
+
|
|
34
|
+
A workflow body is top-level JavaScript statements ending in `return <result>`.
|
|
35
|
+
The only allowed export is `export const meta = { ... }`. Do not wrap the body
|
|
36
|
+
in a function and do not use `export default`.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
export const meta = {
|
|
40
|
+
name: "review-slice",
|
|
41
|
+
description: "One narrow read-only review slice",
|
|
42
|
+
profile: "read-only-review",
|
|
43
|
+
maxAgents: 2,
|
|
44
|
+
concurrency: 2,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const notes = await parallel([
|
|
48
|
+
async ({ agent }) => await agent("Inspect the entrypoints", { role: "explorer" }),
|
|
49
|
+
async ({ agent }) => await agent("Inspect the tests", { role: "explorer" }),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
return { notes };
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Injected globals are `agent`, `parallel`, `pipeline`, `workflow`, `phase`,
|
|
56
|
+
`log`, `budget`, and `args`; do not import them.
|
|
57
|
+
|
|
58
|
+
## QuickJS Sandbox
|
|
59
|
+
|
|
60
|
+
The body runs in a deterministic QuickJS sandbox, not Node. Filesystem, process,
|
|
61
|
+
network, clocks, timers, randomness, `crypto`, and imports are unavailable.
|
|
62
|
+
`Date`, `Date.now`, `Math.random`, `performance`, `setTimeout`, `setInterval`,
|
|
63
|
+
`clearTimeout`, and `clearInterval` throw if called. Use `workflow_status` run
|
|
64
|
+
artifacts for timing and diagnostics instead of in-guest clocks.
|
|
65
|
+
|
|
66
|
+
## Fan-Out And Arity
|
|
67
|
+
|
|
68
|
+
Use the scoped-helper callback form for `parallel()` and `pipeline()`:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
await parallel([
|
|
72
|
+
async ({ agent }) => await agent("first task"),
|
|
73
|
+
async ({ agent }) => await agent("second task"),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
await pipeline(items,
|
|
77
|
+
async (previous, { agent, item, itemIndex, stageIndex }) => await agent(`inspect ${item}`),
|
|
78
|
+
async (finding, { agent, item }) => await agent(`review ${item}: ${finding}`),
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Callback functions must declare the expected parameters. The kernel hard-errors
|
|
83
|
+
ambiguous arity instead of guessing whether a zero-arg thunk should be scoped or
|
|
84
|
+
legacy sequential behavior. Keep call order deterministic so lane signatures and
|
|
85
|
+
resume replay stay stable.
|
|
86
|
+
|
|
87
|
+
Guard every wave's outcome. A wave that silently drops most lanes can feed empty
|
|
88
|
+
data into synthesis and produce a false "nothing found" result. Use `failFast`
|
|
89
|
+
or explicit result-count checks when missing lanes invalidate the next phase.
|
|
90
|
+
|
|
91
|
+
## Models, Effort, And Roles
|
|
92
|
+
|
|
93
|
+
Set `modelTiers: { fast, deep }` on `workflow_run` and tag lanes with
|
|
94
|
+
`tier: "fast"` or `tier: "deep"` when a workflow needs different model strength.
|
|
95
|
+
Run `workflow_models` first and keep tiers inside the session family unless the
|
|
96
|
+
user approves a deviation.
|
|
97
|
+
|
|
98
|
+
OpenAI lanes may additionally request `effort: "minimal" | "low" | "medium" |
|
|
99
|
+
"high"`. This is applied through OpenAI `chat.params` provider options and fails
|
|
100
|
+
before child launch for unsupported providers.
|
|
101
|
+
|
|
102
|
+
Pass `role: "explorer" | "skeptic" | "verifier" | "synthesizer" |
|
|
103
|
+
"implementer"` to prepend a specialist prompt. Role `.md` files stay pure prompt
|
|
104
|
+
text; optional sibling `roles.json` defaults can set model/tier, tools/readOnly,
|
|
105
|
+
retry/timeout, and narrow policy knobs before explicit lane opts override them.
|
|
106
|
+
Use `workflow_roles` to inspect available role files, hashes, and defaults.
|
|
107
|
+
|
|
108
|
+
## Budgets And Scaling
|
|
109
|
+
|
|
110
|
+
`maxAgents` caps total child lanes launched. `concurrency` caps in-flight lanes.
|
|
111
|
+
Each `agent()` call consumes one slot; pure-JavaScript synthesis consumes none.
|
|
112
|
+
|
|
113
|
+
Use `budget.remainingAgents()`, `budget.remaining()`, and `budget.ceilings()` to
|
|
114
|
+
self-scale loops. Budget headroom includes live spend, replayed spend, and
|
|
115
|
+
in-flight reservations, so a loop can stop before launching the next child.
|
|
116
|
+
Setting `maxCost` or `maxTokens` is an approval-envelope decision, not a casual
|
|
117
|
+
throttle.
|
|
118
|
+
|
|
119
|
+
## Schema Lanes
|
|
120
|
+
|
|
121
|
+
Use `schema` when a lane result must be structured. Native structured output is
|
|
122
|
+
used when available; otherwise the kernel uses structured-text instructions plus
|
|
123
|
+
Ajv validation. Correctable schema/text failures can retry in the same child
|
|
124
|
+
session according to `correctiveRetries`, and exhausted validation failures are
|
|
125
|
+
journaled distinctly from transient provider errors.
|
|
126
|
+
|
|
127
|
+
## Nested Workflows
|
|
128
|
+
|
|
129
|
+
Use direct static literals only: `workflow("saved-name", args)` or
|
|
130
|
+
`workflow({ source: "return 1;", args })`. Dynamic or aliased nested workflow
|
|
131
|
+
calls are rejected before approval because the source cannot be snapshotted into
|
|
132
|
+
the hash. Only one nesting level is supported, and nested lanes share the parent
|
|
133
|
+
run's `maxAgents`, concurrency, and budget ceilings.
|
|
134
|
+
|
|
135
|
+
## Launch And Readback
|
|
136
|
+
|
|
137
|
+
Default launch is two-phase: call `workflow_run` for a preview, then call again
|
|
138
|
+
with `approve: true` and the matching `approvalHash`. With configured
|
|
139
|
+
`options.autoApprove`, eligible readOnly/worktree/all-tier runs can launch on
|
|
140
|
+
the first call; a per-call `autoApprove` argument may narrow that ceiling.
|
|
141
|
+
|
|
142
|
+
Foreground runs return a size-capped, secrets-redacted inline result when it
|
|
143
|
+
fits. Always prefer `workflow_status({ runId, detail: "result" })` for the final
|
|
144
|
+
answer path, especially for large results where inline output is omitted or
|
|
145
|
+
partial.
|
|
146
|
+
|
|
147
|
+
## Background Runs
|
|
148
|
+
|
|
149
|
+
Explicit `background: true` returns quickly with a run id while execution
|
|
150
|
+
continues in the current OpenCode process. When `background` is omitted, the
|
|
151
|
+
kernel defaults wide, deep, or long runs to background using the
|
|
152
|
+
wide/deep/long heuristic; explicit `background: true` or `false` wins, and
|
|
153
|
+
resume keeps the pinned mode. Background execution is not durable across process
|
|
154
|
+
death; use `workflow_status`, `workflow_cancel`, `workflow_pause`, and
|
|
155
|
+
`workflow_reconcile` for inspection and lifecycle control.
|
|
156
|
+
|
|
157
|
+
## Edit And Apply
|
|
158
|
+
|
|
159
|
+
Edit authority is only a cap. A lane receives edit tools only when it explicitly
|
|
160
|
+
requests edit/worktree behavior. Edit-capable lanes run in isolated workflow
|
|
161
|
+
worktrees or directories. Primary-tree writes happen through `workflow_apply`
|
|
162
|
+
after source/base/diff/domain hashes and Git state are checked. Successful
|
|
163
|
+
non-dry Beads drain is the special trusted extension path that can finalize
|
|
164
|
+
in-run after its launch approval.
|
|
165
|
+
|
|
166
|
+
## Review Checklist
|
|
167
|
+
|
|
168
|
+
- Top-level body ends in `return`; only `export const meta` is exported.
|
|
169
|
+
- No Node, imports, clocks, timers, randomness, or filesystem assumptions.
|
|
170
|
+
- `parallel()` / `pipeline()` callbacks use explicit scoped-helper arity.
|
|
171
|
+
- Fan-out waves check dropped/null lane outcomes before synthesis.
|
|
172
|
+
- `maxAgents`, `concurrency`, timeouts, and budget ceilings match expected
|
|
173
|
+
lane count and cost risk.
|
|
174
|
+
- Model tiers, per-lane `effort`, roles, and schemas are deliberate.
|
|
175
|
+
- Nested workflow calls are static and one level deep.
|
|
176
|
+
- Readbacks use inline result only when it fits; otherwise use
|
|
177
|
+
`workflow_status({ detail: "result" })`.
|
|
178
|
+
- Edit lanes stop at `workflow_apply` unless using a trusted autonomous drain.
|
|
179
|
+
- After changing workflow source, commands, skills, plugin code, or registration
|
|
180
|
+
behavior, restart OpenCode or use a fresh child process.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: repo-review-command-protocol
|
|
3
|
+
description: Use when running or maintaining the bundled /repo-bughunt and /repo-review commands. Provides the shared command wrapper protocol for JSON argument validation, workflow_run launch by name, workflow_status detail=result readback, .repo-review/runs report persistence, report-only mutation boundaries, and concise closeout.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Repo-Review Command Protocol
|
|
7
|
+
|
|
8
|
+
This skill holds the shared command wrapper protocol for `/repo-bughunt` and
|
|
9
|
+
`/repo-review`. The command markdown supplies command-specific defaults,
|
|
10
|
+
workflow name, model-tier policy, result fields, and optional materialization
|
|
11
|
+
handling.
|
|
12
|
+
|
|
13
|
+
## Shared Steps
|
|
14
|
+
|
|
15
|
+
1. Parse `$ARGUMENTS` before touching `workflow_run`.
|
|
16
|
+
- Empty arguments use the command's documented default object.
|
|
17
|
+
- Non-empty arguments must parse as valid JSON and resolve to a plain object.
|
|
18
|
+
- On parse failure or non-object input, STOP before `workflow_run`; report the
|
|
19
|
+
offending value and expected object shape.
|
|
20
|
+
|
|
21
|
+
2. Resolve models before launch.
|
|
22
|
+
- Follow `workflow-model-tiering` unless the command markdown declares a
|
|
23
|
+
command-specific model policy.
|
|
24
|
+
- Pass the final map as `modelTiers`.
|
|
25
|
+
|
|
26
|
+
3. Launch by bundled workflow name, never by path.
|
|
27
|
+
- Use `workflow_run({ name, args, modelTiers, format: "json" })`.
|
|
28
|
+
- If the call returns a preview, report `approvalHash`,
|
|
29
|
+
`authority.profile`, and `modelPlan`, then launch with `approve: true` and
|
|
30
|
+
the matching hash.
|
|
31
|
+
- If configured `autoApprove` makes the first call execute immediately, report
|
|
32
|
+
the returned `runId` and status instead of inventing a preview step.
|
|
33
|
+
|
|
34
|
+
4. Read the terminal result through
|
|
35
|
+
`workflow_status({ runId, detail: "result" })`.
|
|
36
|
+
- Use `.result.output` as the envelope.
|
|
37
|
+
- Treat raw `.opencode/workflows/runs/` files as local-sensitive artifacts.
|
|
38
|
+
|
|
39
|
+
5. Persist exactly one report under `.repo-review/runs/`.
|
|
40
|
+
- Create the directory if needed with `mkdir -p .repo-review/runs`.
|
|
41
|
+
- Use the command-specific filename suffix.
|
|
42
|
+
- Prefer the command-specific full report source. If the full markdown is
|
|
43
|
+
absent or omitted for size, synthesize the documented fallback summary from
|
|
44
|
+
returned `summary`, `counts`, and `findings` only.
|
|
45
|
+
- Add a `Report-only - nothing applied.` footer.
|
|
46
|
+
|
|
47
|
+
6. Enforce the report-only boundary.
|
|
48
|
+
- The ONLY allowed workspace write is the local report file.
|
|
49
|
+
- The command itself must never run `materialize`, `beads-drain`,
|
|
50
|
+
`workflow_apply`, any `git` write, or any `bd` create/update/close/claim.
|
|
51
|
+
- Do not stage or commit the report; `.repo-review/` is gitignored.
|
|
52
|
+
|
|
53
|
+
7. Report back concisely.
|
|
54
|
+
- Include validated args, model plan, approval hash when there was a preview,
|
|
55
|
+
run id, terminal status, envelope status/summary/counts, report source or
|
|
56
|
+
fallback mode, truncation flags, and the absolute report path.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-model-tiering
|
|
3
|
+
description: Use BEFORE running any workflow whose lanes declare fast/deep tiers (e.g. repo-bughunt). Enumerate available models with workflow_models, map fast/deep to concrete models in the invoking session's family, and confirm with the user ONLY when the plan deviates from that family.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Workflow model tiering
|
|
7
|
+
|
|
8
|
+
Workflows declare each lane's intent as `tier: "fast"` (cheap/bulk) or `tier: "deep"`
|
|
9
|
+
(subtle reasoning); pure-JS lanes declare no model. The kernel resolves each tier to a
|
|
10
|
+
concrete `provider/model` string from the run's `modelTiers` map, falling back to the
|
|
11
|
+
invoking session's model (and finally to `DEFAULT_CHILD_MODEL`). Your job before running
|
|
12
|
+
such a workflow is to pick the concrete fast/deep models and pass them as `modelTiers`.
|
|
13
|
+
|
|
14
|
+
## Procedure
|
|
15
|
+
|
|
16
|
+
1. **Enumerate.** Call `workflow_models`. Read `session.model` / `session.family` and the
|
|
17
|
+
available `providers[].models` (each model may list reasoning `variants`, e.g. `max`).
|
|
18
|
+
`suggested.fast` / `suggested.deep` both default to the session model — a safe
|
|
19
|
+
no-deviation starting point.
|
|
20
|
+
|
|
21
|
+
2. **Map within the session family.** Choose concrete `fast`/`deep` models *inside the
|
|
22
|
+
session family*:
|
|
23
|
+
- `fast` = the session model (or the cheapest reasonable model in the family).
|
|
24
|
+
- `deep` = a higher-reasoning model or reasoning variant in the same family if one
|
|
25
|
+
exists (e.g. a `max` variant). If the family has only one model, `deep = fast`.
|
|
26
|
+
|
|
27
|
+
3. **Deviation check.** The plan is NON-deviating when both tiers stay in the session
|
|
28
|
+
family at standard escalation. It DEVIATES when you pick a different provider family, a
|
|
29
|
+
premium cross-family `deep`, or mix families.
|
|
30
|
+
- **No deviation:** proceed. The `workflow_run` approval preview shows the model plan
|
|
31
|
+
(`Model plan: fast=… deep=…`); the user approving that preview is the confirmation.
|
|
32
|
+
Do NOT add a separate prompt.
|
|
33
|
+
- **Deviation:** STOP and confirm explicitly with the user — state the chosen models and
|
|
34
|
+
why — BEFORE calling `workflow_run`.
|
|
35
|
+
|
|
36
|
+
4. **Run.** Call `workflow_run({ name: "<workflow>", args: {...}, modelTiers: { fast, deep } })`,
|
|
37
|
+
read the `approvalHash` from the returned preview, then call again with `approve: true`
|
|
38
|
+
and that `approvalHash`. Never set `maxCost` / `maxTokens` casually; ceilings are
|
|
39
|
+
part of the approved envelope, and workflow bodies should use `budget.remaining()`
|
|
40
|
+
or `budget.ceilings()` when they need to self-scale.
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
- Omitting `modelTiers` is valid: every tier then resolves to the session model (a safe
|
|
45
|
+
no-deviation default). Pass `modelTiers` only to differentiate `deep` from `fast`.
|
|
46
|
+
- An explicit per-lane `model` in a workflow's source overrides tiers entirely;
|
|
47
|
+
report-only review workflows should not use it.
|
|
48
|
+
- A per-lane `effort` hint is separate from tier selection. Use it only for lanes
|
|
49
|
+
that resolve to an OpenAI provider, with `minimal`, `low`, `medium`, or `high`
|
|
50
|
+
values, for example `agent(prompt, { tier: "deep", effort: "high" })`. The
|
|
51
|
+
plugin applies it through OpenAI `chat.params` provider options. Do not assume
|
|
52
|
+
provider variants or non-OpenAI providers support this knob; unsupported
|
|
53
|
+
providers fail before child launch.
|
|
54
|
+
- `modelTiers` is covered by the approval hash, so changing the model plan re-triggers
|
|
55
|
+
approval — you cannot silently swap models behind an already-approved hash.
|
|
56
|
+
- Models are `provider/model` strings (exactly one `/`). A malformed tier value is a hard
|
|
57
|
+
error at planning time.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-plan-review
|
|
3
|
+
description: Use whenever you are about to run an OpenCode workflow via workflow_run, or have just called it without approve and are holding the approval summary. Covers presenting the workflow plan to the user human-first, offering structured refinement knobs plus free-text, choosing between configured autoApprove and manual approvalHash launch, and reading results back. Distinct from workflow-model-tiering and opencode-workflow-authoring.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Workflow plan review (the run handoff)
|
|
7
|
+
|
|
8
|
+
Workflow authority is fixed **once at launch**. On the manual path, the approval
|
|
9
|
+
hash covers the whole envelope (source, args, authority, models, budgets,
|
|
10
|
+
concurrency, capabilities, nested snapshots). On the configured `autoApprove`
|
|
11
|
+
path, eligible runs can launch on the first call when their resolved authority
|
|
12
|
+
tier is within the configured ceiling. In both cases, treat the launch plan as
|
|
13
|
+
something to understand and refine, not a hash to echo back.
|
|
14
|
+
|
|
15
|
+
## Procedure
|
|
16
|
+
|
|
17
|
+
1. **Get the plan or run.** Prefer `workflow_run({ name: "...", args: {...}, format: "json", ... })`.
|
|
18
|
+
If the plugin is not auto-approving this authority tier, the returned JSON is a typed
|
|
19
|
+
`workflow_preview` envelope with `executed: false`, `approvalHash`, `workflow`, `source`,
|
|
20
|
+
`runtimeArgsPreview`, `laneBudget`, `modelPlan`, `budgetCeilings`, `background`, `authority`,
|
|
21
|
+
`mutationDomains`, `capabilities`, and `nestedSnapshots`. If configured `autoApprove` makes the
|
|
22
|
+
call execute immediately, report the run id/status and move to result readback instead of
|
|
23
|
+
inventing a preview step.
|
|
24
|
+
|
|
25
|
+
2. **Present human-first.** Summarize the plan to the user in your own words, leading with what
|
|
26
|
+
they actually care about, in roughly this order:
|
|
27
|
+
- **What it will do** — the Description and the scope in `Runtime args preview`.
|
|
28
|
+
- **Models** — `Default child model` and `Model plan: fast=… deep=…`.
|
|
29
|
+
- **Lane budget** — `Max agents` (the ceiling on child lanes) and `Concurrency` (peak parallel).
|
|
30
|
+
- **Cost/time** — `Budget ceilings`, `Lane timeout`, and the run deadline if present.
|
|
31
|
+
- **Authority** — `Authority profile`, `Required gates`, `Isolation`, `Mutation domains`. Call
|
|
32
|
+
out anything that mutates state or stops at `awaiting-diff-approval` / in-run apply.
|
|
33
|
+
- **Background** — `Background: true/false`, and the heuristic recommendation line if present.
|
|
34
|
+
You may condense or omit the technical envelope (hashes/capabilities/consent) from what you
|
|
35
|
+
show the user — it is there for accuracy, not for the headline.
|
|
36
|
+
|
|
37
|
+
3. **Offer refinement.** Give the user concrete knobs to tweak, plus a free-text channel. Surface
|
|
38
|
+
the knobs that are relevant to *this* workflow.
|
|
39
|
+
|
|
40
|
+
**Universal knobs** (every workflow accepts these as `workflow_run` args):
|
|
41
|
+
- `modelTiers: { fast, deep }` — see the `workflow-model-tiering` skill for how to pick them.
|
|
42
|
+
- `maxAgents` — ceiling on child lanes launched (one slot per `agent()` lane).
|
|
43
|
+
- `concurrency` — peak parallel lanes.
|
|
44
|
+
- `background: true/false` — keep a control channel for pause/cancel on long runs.
|
|
45
|
+
- `maxCost` / `maxTokens` — budget ceilings are approval-envelope decisions; workflow bodies can
|
|
46
|
+
self-scale with `budget.remaining()` and `budget.ceilings()`.
|
|
47
|
+
- `laneTimeoutMs` (alias `childPromptTimeoutMs`) — per-lane prompt cap.
|
|
48
|
+
|
|
49
|
+
**Per-workflow scope knobs** — read them from `runtimeArgsPreview` (or the text preview's
|
|
50
|
+
`Runtime args preview` line) and the workflow's declared args (e.g. `paths`/`depth`/
|
|
51
|
+
`categories` for repo-bughunt, `domains`/
|
|
52
|
+
`batchSize` for repo-review, `mode`/`scope` for beads-drain). If you are unsure which scope args
|
|
53
|
+
a named workflow accepts, check `workflow_list` or read its source before offering them.
|
|
54
|
+
|
|
55
|
+
4. **Re-plan on any change.** If the user refines anything, re-call `workflow_run` **without**
|
|
56
|
+
`approve` with the new args/modelTiers/budget. You get a fresh preview and a fresh `approvalHash`
|
|
57
|
+
(the old hash is now stale and `approve:true` with it returns `approval_mismatch` and
|
|
58
|
+
`executed:false`). Present again. This loop is free — use it.
|
|
59
|
+
|
|
60
|
+
5. **Approve or continue.** For the manual path, once the user confirms the plan, re-call
|
|
61
|
+
`workflow_run` with `approve: true` and the matching `approvalHash` from the most recent preview.
|
|
62
|
+
Do not call `approve: true` in the same turn you presented the plan unless the user already said
|
|
63
|
+
to run it. For an auto-approved run, skip this step and read back the result.
|
|
64
|
+
|
|
65
|
+
## When you may skip the confirmation step
|
|
66
|
+
|
|
67
|
+
- The plugin is configured with `options.autoApprove` and the resolved authority tier is covered by
|
|
68
|
+
that ceiling; the first `workflow_run` call may execute immediately.
|
|
69
|
+
- The user explicitly said to just run it ("go ahead", "run it", "approve") in this turn or a prior
|
|
70
|
+
instruction that clearly covers this run.
|
|
71
|
+
- You are **resuming** an already-approved envelope via `resumeRunId` (the user already approved
|
|
72
|
+
that envelope; resume replays/re-runs under the same hash). A *cold* plan is always confirmed.
|
|
73
|
+
|
|
74
|
+
If unsure whether the user pre-authorized, ask. A confirmation question is always cheaper than an
|
|
75
|
+
unwanted multi-lane run.
|
|
76
|
+
|
|
77
|
+
## Graceful degradation
|
|
78
|
+
|
|
79
|
+
Not every workflow has a static lane count you can quote up front:
|
|
80
|
+
|
|
81
|
+
- **Static fan-out** (repo-bughunt, repo-* leaves, repo-review): the structure is knowable
|
|
82
|
+
(e.g. recon → N finders → skeptics → pure-JS synth), but some counts depend on results (number
|
|
83
|
+
of skeptics depends on number of findings). Present the *structure* and flag counts that are
|
|
84
|
+
data-dependent rather than implying a fixed number.
|
|
85
|
+
- **Discovery-driven** (beads-drain): the lane count depends on the live backlog. For beads-drain
|
|
86
|
+
specifically, the dry-run (`mode: "dry-run"`) **is** the plan — it reports the ready items that
|
|
87
|
+
would be worked. Offer a dry-run as the preview for non-dry intent.
|
|
88
|
+
- **Dynamic/inline**: if you cannot determine the structure, say so plainly and lean on the
|
|
89
|
+
envelope (authority, models, budget, background) plus `Max agents` as the ceiling.
|
|
90
|
+
|
|
91
|
+
In all cases, present what you know and label what is uncertain. Never invent a precise lane count.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { hash, hashStable, stableStringify } from "./text-json.js";
|
|
2
|
+
|
|
3
|
+
export function approvalSnapshotList(nestedSnapshots) {
|
|
4
|
+
return [...new Map([...(nestedSnapshots?.values?.() ?? [])].map((item) => [item.sourcePath, item])).values()]
|
|
5
|
+
.map(({ sourcePath, sourceHash }) => ({ sourcePath, sourceHash }))
|
|
6
|
+
.sort((a, b) => `${a.sourcePath}:${a.sourceHash}`.localeCompare(`${b.sourcePath}:${b.sourceHash}`));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function approvalEnvelope(approval) {
|
|
10
|
+
return {
|
|
11
|
+
version: 2,
|
|
12
|
+
sourcePath: approval.sourcePath,
|
|
13
|
+
sourceHash: approval.sourceHash,
|
|
14
|
+
runtimeArgs: approval.runtimeArgs ?? null,
|
|
15
|
+
maxAgents: approval.maxAgents,
|
|
16
|
+
concurrency: approval.concurrency,
|
|
17
|
+
defaultChildModel: approval.defaultChildModel,
|
|
18
|
+
modelTiers: approval.modelTiers ?? null,
|
|
19
|
+
authority: approval.authority,
|
|
20
|
+
budgetCeilings: approval.budgetCeilings,
|
|
21
|
+
baseCommit: approval.baseCommit ?? null,
|
|
22
|
+
guestDeadlineMs: approval.guestDeadlineMs,
|
|
23
|
+
laneTimeoutMs: approval.laneTimeoutMs ?? null,
|
|
24
|
+
debugCapture: approval.debugCapture === true,
|
|
25
|
+
background: approval.background === true,
|
|
26
|
+
resumeRunId: approval.resumeRunId ?? null,
|
|
27
|
+
resumePolicy: approval.resumePolicy ?? null,
|
|
28
|
+
capabilities: approval.capabilities,
|
|
29
|
+
nestedSnapshots: approvalSnapshotList(approval.nestedSnapshots),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function approvalHash(approval) {
|
|
34
|
+
return hashStable(approvalEnvelope(approval));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function computeDiffPlanHash(plan) {
|
|
38
|
+
return hash(stableStringify({ patches: plan.patches, sourceHash: plan.sourceHash, baseCommit: plan.baseCommit, domainMutationHash: plan.domainMutationHash }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WorkflowCancelledError, WorkflowTimeoutError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export async function withTimeout(factory, { timeoutMs, signal, label, onTimeout }) {
|
|
4
|
+
if (signal?.aborted) {
|
|
5
|
+
Promise.resolve().then(() => onTimeout?.("abort")).catch(() => {});
|
|
6
|
+
throw new WorkflowCancelledError();
|
|
7
|
+
}
|
|
8
|
+
let timeout;
|
|
9
|
+
let abortListener;
|
|
10
|
+
try {
|
|
11
|
+
return await Promise.race([
|
|
12
|
+
Promise.resolve().then(factory),
|
|
13
|
+
new Promise((_, reject) => {
|
|
14
|
+
timeout = setTimeout(() => {
|
|
15
|
+
reject(new WorkflowTimeoutError(`${label} timed out after ${timeoutMs}ms`));
|
|
16
|
+
Promise.resolve().then(() => onTimeout?.()).catch(() => {
|
|
17
|
+
// Timeout cleanup is best effort; the timeout itself is still authoritative.
|
|
18
|
+
});
|
|
19
|
+
}, timeoutMs);
|
|
20
|
+
abortListener = () => {
|
|
21
|
+
reject(new WorkflowCancelledError());
|
|
22
|
+
Promise.resolve().then(() => onTimeout?.("abort")).catch(() => {
|
|
23
|
+
// Abort cleanup is best effort; the abort itself is still authoritative.
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
signal?.addEventListener("abort", abortListener, { once: true });
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timeout);
|
|
31
|
+
if (abortListener) signal?.removeEventListener("abort", abortListener);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Audited-shell + network-advisory deep-mode policy for repo-review (iui1.7).
|
|
2
|
+
//
|
|
3
|
+
// repo-review ships static + read-only by default (profile "read-only-review": readOnly, no
|
|
4
|
+
// shell/network/mcp). Two OPTIONAL deep modes are available behind verified gates:
|
|
5
|
+
// - audited-shell: the inspect-with-shell profile (readOnly + shell) with a STRICT read-only
|
|
6
|
+
// command allowlist (git ls-files, git log --numstat, npm ls --depth=0,
|
|
7
|
+
// cargo tree, pip list, go list). Installs, audit-network, and ANY mutation
|
|
8
|
+
// command are denied. Enables real git-churn for complexity + live dep lists.
|
|
9
|
+
// - network-advisory: a network:true profile for advisory API lookups (npm advisory, OSV,
|
|
10
|
+
// GitHub advisories). No mutation.
|
|
11
|
+
//
|
|
12
|
+
// Both modes are EXPLICITLY opt-in (args.deepMode or args.profile). Static mode never requests
|
|
13
|
+
// shell or network. When a deep mode is requested but the required gates are UNVERIFIED, the mode
|
|
14
|
+
// FAILS CLOSED: it degrades to static and records the limitation (shellCoverage stays "none").
|
|
15
|
+
//
|
|
16
|
+
// This module is PURE (no fs/shell/network) so the policy is fully unit-testable without verified
|
|
17
|
+
// gates or a real repo. The QuickJS repo-review guest cannot import it; the guest stays static and
|
|
18
|
+
// surfaces the requested mode + coverage limitation, while the command wrapper / kernel rely on
|
|
19
|
+
// this policy when a caller opts in via workflow_run({ profile }) — the kernel enforces the
|
|
20
|
+
// profile's requiredGates at plan time.
|
|
21
|
+
|
|
22
|
+
export const AUDITED_SHELL_ALLOWLIST = Object.freeze([
|
|
23
|
+
// Read-only manifest/tree/listing commands only. Each entry is a command-prefix (program + the
|
|
24
|
+
// fixed leading args) that a shell lens may run. Anything else is denied.
|
|
25
|
+
{ id: "git-ls-files", prefix: ["git", "ls-files"], note: "tracked file inventory" },
|
|
26
|
+
{ id: "git-log-numstat", prefix: ["git", "log", "--numstat"], note: "per-file churn history" },
|
|
27
|
+
{ id: "npm-ls", prefix: ["npm", "ls", "--depth=0"], note: "installed dependency tree (local)" },
|
|
28
|
+
{ id: "cargo-tree", prefix: ["cargo", "tree"], note: "cargo dependency tree (local)" },
|
|
29
|
+
{ id: "pip-list", prefix: ["pip", "list"], note: "installed python packages (local)" },
|
|
30
|
+
{ id: "go-list", prefix: ["go", "list"], note: "go module list (local)" },
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// Forbidden substrings/tokens — any command matching one of these is REJECTED even if its prefix is
|
|
34
|
+
// allowlisted. These are the install / audit-network / mutation / shell-meta dangers.
|
|
35
|
+
export const AUDITED_SHELL_DENY = Object.freeze([
|
|
36
|
+
{ id: "install", test: /\b(npm|yarn|pnpm)\s+(install|i|add)\b|\bpip3?\s+install\b|\binstall-packages\b|\bpip-install\b/i, reason: "package installation is a mutation" },
|
|
37
|
+
{ id: "yarn-add", test: /\byarn\s+add\b/i, reason: "yarn add is a mutation" },
|
|
38
|
+
{ id: "npm-audit", test: /\bnpm\s+audit\b/i, reason: "npm audit reaches the network; use network-advisory mode for advisories" },
|
|
39
|
+
{ id: "pip-audit", test: /\bpip-?audit\b/i, reason: "pip-audit reaches the network" },
|
|
40
|
+
{ id: "npm-publish", test: /\b(npm\s+publish|publish)\b/i, reason: "publishing is a mutation" },
|
|
41
|
+
{ id: "git-mutation", test: /\bgit\s+(commit|push|merge|rebase|reset|checkout|switch|stash|cherry-pick|tag)\b/i, reason: "git mutation" },
|
|
42
|
+
{ id: "go-get", test: /\bgo\s+get\b/i, reason: "go get mutates modules / reaches the network" },
|
|
43
|
+
{ id: "go-install", test: /\bgo\s+install\b/i, reason: "go install mutates the local toolchain/module cache" },
|
|
44
|
+
{ id: "cargo-add", test: /\bcargo\s+add\b/i, reason: "cargo add is a mutation" },
|
|
45
|
+
{ id: "cargo-install", test: /\bcargo\s+install\b/i, reason: "cargo install mutates the local toolchain cache" },
|
|
46
|
+
{ id: "redirect", test: /(^|\s)(>|>>|\||&&|;|\|\|)\s*/, reason: "shell redirection/chaining is forbidden" },
|
|
47
|
+
{ id: "substitution", test: /(\$\(|`|<\()/, reason: "shell command/process substitution is forbidden" },
|
|
48
|
+
{ id: "rm-mutation", test: /\b(rm|mv|cp|mkdir|rmdir|chmod|chown|touch|tee)\b/i, reason: "filesystem mutation" },
|
|
49
|
+
{ id: "curl-wget", test: /\b(curl|wget|nc|ssh|scp|rsync)\b/i, reason: "network fetch belongs in network-advisory mode, not audited-shell" },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// Validate a single shell command against the audited-shell policy. Returns {allowed, reason}.
|
|
53
|
+
// `command` is a string (the full command line) or an argv array.
|
|
54
|
+
export function validateAuditedCommand(command) {
|
|
55
|
+
const argv = Array.isArray(command)
|
|
56
|
+
? command.map(String)
|
|
57
|
+
: String(command ?? "").trim().split(/\s+/).filter(Boolean);
|
|
58
|
+
if (argv.length === 0) return { allowed: false, reason: "empty command" };
|
|
59
|
+
// Denylist first (defense-in-depth): an allowlisted prefix with a forbidden token is still rejected.
|
|
60
|
+
const joined = argv.join(" ");
|
|
61
|
+
for (const rule of AUDITED_SHELL_DENY) {
|
|
62
|
+
if (rule.test.test(joined)) return { allowed: false, reason: `denied (${rule.id}): ${rule.reason}` };
|
|
63
|
+
}
|
|
64
|
+
// Allowlist: the command's leading tokens must EXACTLY match one allowlisted prefix.
|
|
65
|
+
const match = AUDITED_SHELL_ALLOWLIST.find((entry) => {
|
|
66
|
+
if (argv.length < entry.prefix.length) return false;
|
|
67
|
+
return entry.prefix.every((tok, i) => argv[i] === tok);
|
|
68
|
+
});
|
|
69
|
+
if (!match) {
|
|
70
|
+
return { allowed: false, reason: `command not in the audited-shell allowlist (allowed: ${AUDITED_SHELL_ALLOWLIST.map((a) => a.prefix.join(" ")).join("; ")})` };
|
|
71
|
+
}
|
|
72
|
+
return { allowed: true, reason: `allowlisted:${match.id}`, matched: match };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// OpenCode permission-rule wildcard deny patterns that translate the AUDITED_SHELL_DENY concerns into
|
|
76
|
+
// the runtime permission ruleset. These are positioned AFTER the allow patterns in the generated
|
|
77
|
+
// rules (last-match-wins), so a dangerous argument tacked onto an allowlisted command is still
|
|
78
|
+
// denied (e.g. `git ls-files && rm x` matches `*&&*`). Patterns use OpenCode simple wildcards:
|
|
79
|
+
// `*` = zero-or-more of any char, `?` = exactly one char, all else literal.
|
|
80
|
+
//
|
|
81
|
+
// Kept in lock-step with AUDITED_SHELL_DENY above so validateAuditedCommand (the pure unit-testable
|
|
82
|
+
// validator) and the runtime permission ruleset enforce the SAME dangerous classes.
|
|
83
|
+
export const SHELL_PERMISSION_DENY_PATTERNS = Object.freeze([
|
|
84
|
+
// Shell chaining / pipes / redirection — a read-only inspection shell never composes or redirects.
|
|
85
|
+
"*&&*", "*||*", "*;*", "*|*", "*>*", "*<*",
|
|
86
|
+
// Command/process substitution (AUDITED_SHELL_DENY "substitution").
|
|
87
|
+
"*$(*", "*`*",
|
|
88
|
+
// Filesystem mutation (AUDITED_SHELL_DENY "rm-mutation").
|
|
89
|
+
"*rm *", "*rmdir *", "*mv *", "*cp *", "*mkdir *", "*chmod *", "*chown *", "*touch *", "*tee *",
|
|
90
|
+
// Network fetch (AUDITED_SHELL_DENY "curl-wget") — belongs in network-advisory, not audited-shell.
|
|
91
|
+
"*curl*", "*wget*", "*nc *", "*ssh *", "*scp *", "*rsync *",
|
|
92
|
+
// Package install / add / publish (AUDITED_SHELL_DENY "install"/"yarn-add"/"cargo-add"/"go-get"/"go-install"/"npm-publish").
|
|
93
|
+
"npm install", "npm install *", "npm i", "npm i *", "npm add", "npm add *",
|
|
94
|
+
"pnpm install", "pnpm install *", "pnpm add", "pnpm add *",
|
|
95
|
+
"yarn install", "yarn install *", "yarn add", "yarn add *",
|
|
96
|
+
"pip install", "pip install *", "pip3 install", "pip3 install *", "*install-packages*", "*pip-install*",
|
|
97
|
+
"go get", "go get *", "go install", "go install *", "cargo add", "cargo add *", "cargo install", "cargo install *", "*publish*",
|
|
98
|
+
// Networked audit tools (AUDITED_SHELL_DENY "npm-audit"/"pip-audit").
|
|
99
|
+
"*npm audit*", "*pip-audit*", "*pip audit*",
|
|
100
|
+
// Git mutation (AUDITED_SHELL_DENY "git-mutation").
|
|
101
|
+
"*git commit*", "*git push*", "*git merge*", "*git rebase*", "*git reset*", "*git checkout*", "*git switch*", "*git stash*", "*git cherry-pick*", "*git tag*",
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
// Translate the audited-shell allowlist + denylist into OpenCode permission-rule wildcard patterns
|
|
105
|
+
// ({ allow: [...], deny: [...] }). Each allowlisted command prefix becomes TWO patterns — the exact
|
|
106
|
+
// prefix (matches the bare command, e.g. `git ls-files`) and a trailing " *" variant (matches the
|
|
107
|
+
// command with arguments, e.g. `git ls-files path/to/dir`). The deny patterns (above) are returned
|
|
108
|
+
// as-is; the caller pushes allow THEN deny so last-match-wins keeps dangerous arguments denied.
|
|
109
|
+
//
|
|
110
|
+
// Pure + deterministic; reused by resolveRunAuthority for the inspect-with-shell profile so the
|
|
111
|
+
// runtime permission ruleset enforces the SAME allowlist that validateAuditedCommand and the docs
|
|
112
|
+
// describe — the lists are never duplicated.
|
|
113
|
+
export function auditedShellPermissionPatterns() {
|
|
114
|
+
const allow = [];
|
|
115
|
+
for (const entry of AUDITED_SHELL_ALLOWLIST) {
|
|
116
|
+
const base = entry.prefix.join(" ");
|
|
117
|
+
allow.push(base);
|
|
118
|
+
allow.push(`${base} *`);
|
|
119
|
+
}
|
|
120
|
+
return { allow, deny: [...SHELL_PERMISSION_DENY_PATTERNS] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The required gates per deep mode. audited-shell reuses inspect-with-shell; network-advisory adds
|
|
124
|
+
// network + the permissionEnforcement gate.
|
|
125
|
+
export const DEEP_MODE_REQUIRED_GATES = Object.freeze({
|
|
126
|
+
"audited-shell": Object.freeze(["permissionEnforcement", "commandScopedBash"]),
|
|
127
|
+
"network-advisory": Object.freeze(["permissionEnforcement", "networkAccess"]),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Map a run's shell-lens state to a shellCoverage grade. "none" unless shell mode is active AND the
|
|
131
|
+
// gates are verified (fail-closed). "partial" when the shell lens is enabled (churn measured for
|
|
132
|
+
// some dirs); "full" when every dir's churn was measured.
|
|
133
|
+
export function resolveShellCoverage({ shellMode, gatesVerified, measured = 0, expected = 0 } = {}) {
|
|
134
|
+
if (!shellMode || !gatesVerified) return "none";
|
|
135
|
+
if (expected > 0 && measured >= expected) return "full";
|
|
136
|
+
return "partial";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Resolve the effective deep mode from runtime args. Pure + deterministic; the fail-closed rule
|
|
140
|
+
// means an unverified-gate request degrades to static (the guest/command never actually enables
|
|
141
|
+
// shell/network without verified gates). Returns a structured descriptor for the envelope + report.
|
|
142
|
+
export function resolveDeepMode({ deepMode, profile, gatesVerified = false } = {}) {
|
|
143
|
+
const requested = String(deepMode ?? profile ?? "static");
|
|
144
|
+
const isShell = requested === "audited-shell" || requested === "inspect-with-shell";
|
|
145
|
+
const isNetwork = requested === "network-advisory" || requested === "network";
|
|
146
|
+
if (!isShell && !isNetwork) {
|
|
147
|
+
return {
|
|
148
|
+
mode: "static",
|
|
149
|
+
requested: "static",
|
|
150
|
+
authorityProfile: "read-only-review",
|
|
151
|
+
requiredGates: [],
|
|
152
|
+
shellMode: false,
|
|
153
|
+
networkMode: false,
|
|
154
|
+
shellCoverage: "none",
|
|
155
|
+
coverageLimitations: "Static, read-only analysis. Git churn is not measured (no shell); dependency/network advisories are not fetched (no network). Enable audited-shell or network-advisory (verified gates) for deeper coverage.",
|
|
156
|
+
failClosed: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const requiredGates = isShell ? DEEP_MODE_REQUIRED_GATES["audited-shell"] : DEEP_MODE_REQUIRED_GATES["network-advisory"];
|
|
160
|
+
// FAIL CLOSED: a requested deep mode that cannot verify its gates degrades to static. The mode is
|
|
161
|
+
// NEVER enabled by accident — shell/network require explicit opt-in AND verified gates.
|
|
162
|
+
if (!gatesVerified) {
|
|
163
|
+
return {
|
|
164
|
+
mode: "static",
|
|
165
|
+
requested,
|
|
166
|
+
authorityProfile: "read-only-review",
|
|
167
|
+
requiredGates,
|
|
168
|
+
shellMode: false,
|
|
169
|
+
networkMode: false,
|
|
170
|
+
shellCoverage: "none",
|
|
171
|
+
coverageLimitations: `${requested} was requested but required gates (${requiredGates.join(", ")}) are unverified; degraded to static read-only review. Verify the gates and re-run to enable the deep mode.`,
|
|
172
|
+
failClosed: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// Gates verified → the deep mode is active.
|
|
176
|
+
if (isShell) {
|
|
177
|
+
return {
|
|
178
|
+
mode: "audited-shell",
|
|
179
|
+
requested,
|
|
180
|
+
authorityProfile: "inspect-with-shell",
|
|
181
|
+
requiredGates,
|
|
182
|
+
shellMode: true,
|
|
183
|
+
networkMode: false,
|
|
184
|
+
shellCoverage: "partial",
|
|
185
|
+
coverageLimitations: null,
|
|
186
|
+
failClosed: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
mode: "network-advisory",
|
|
191
|
+
requested,
|
|
192
|
+
authorityProfile: "network-advisory",
|
|
193
|
+
requiredGates,
|
|
194
|
+
shellMode: false,
|
|
195
|
+
networkMode: true,
|
|
196
|
+
shellCoverage: "none",
|
|
197
|
+
coverageLimitations: null,
|
|
198
|
+
failClosed: false,
|
|
199
|
+
};
|
|
200
|
+
}
|