@drisp/cli 0.7.13 → 0.7.15

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/README.md CHANGED
@@ -128,6 +128,10 @@ drisp workflow install e2e-test-builder@lespaceman/athena-workflow-marketplace
128
128
 
129
129
  `drisp run` is built for pipelines. Safe by default -- permission and question hooks fail unless you opt in.
130
130
 
131
+ ### Human gates
132
+
133
+ In a Workflow Run, four actions stop the session for a member: plan approval, merges, the token budget, and irreversible actions (deploys, releases, publishes, destructive git). Set each one to `ask` or `auto` in `.athena/config.json` under `"gates"`, or for one run with `--gate merge=ask`. Merges and irreversible actions are enforced by PreToolUse hook rules for the session and every subagent. See [docs/human-gates.md](docs/human-gates.md) for the matcher's rules.
134
+
131
135
  ### Permissions with no hub attached
132
136
 
133
137
  drisp has a single door. A permission request or a question is answered by whoever is attached to the Run: the interactive terminal, or the hub delivering decisions through the Runner. With no hub attached (a headless `drisp run`) there is nobody to ask, so under `guarded` and `standard` the request **waits**: the Run holds on the pending decision, and nothing is auto-approved or auto-denied on its behalf. The hold ends one of two ways. If you set `--timeout-ms`, the run exits with the timeout exit code when it expires. If the Run belongs to a workflow, the request is **held, then parked, then replayed**:
@@ -138,6 +142,8 @@ drisp has a single door. A permission request or a question is answered by whoev
138
142
 
139
143
  **`--isolation autonomous` runs with nobody watching.** A permission prompt that no **ask rule** claims is answered _allow_ by the preset's policy, within the tool surface the preset already grants, and the workflow runs to `WORKFLOW_COMPLETE`. Exactly two things park the Run as needs-human: an ask rule that fires, or a Turn that ends on `<!-- NEEDS_HUMAN: reason -->`. Ask rules are a provisional, per-workflow list of tool-name patterns in `workflow.json` whose prompts must always reach a person — `"askRules": ["Bash", "mcp__github__*"]` — and a parked Run shows in `drisp runs` as **Parked** with the rule that fired (or the marker's reason); `drisp run --continue` wakes it.
140
144
 
145
+ **A Workflow can allow the native subagent tools no preset grants.** `"allowedTools": ["SendMessage", "ToolSearch", "Monitor", "TaskStop", "ListAgents"]` in `workflow.json` answers those permission prompts _allow_ under any preset, in `drisp run` and the interactive session alike. Only these five names and exact MCP tool names (`mcp__<server>__<tool>`, no wildcards) are accepted; anything else fails validation. In `drisp run` an ask rule naming the same tool still claims it; the interactive session does not apply ask rules. The presets themselves stay as they are.
146
+
141
147
  ```bash
142
148
  drisp run "summarize risk in this PR" # plain text
143
149
  drisp run "run checks" --json --on-permission=deny --on-question=empty # JSONL
@@ -8,7 +8,7 @@ import {
8
8
  installWorkflowPlugins,
9
9
  resolveWorkflow,
10
10
  writeGlobalConfig
11
- } from "./chunk-SACVOS6T.js";
11
+ } from "./chunk-GGAPZULS.js";
12
12
  import "./chunk-G3PMV62Z.js";
13
13
 
14
14
  // src/setup/steps/WorkflowInstallWizard.tsx
@@ -93,4 +93,4 @@ function WorkflowInstallWizard({ source, onDone }) {
93
93
  export {
94
94
  WorkflowInstallWizard as default
95
95
  };
96
- //# sourceMappingURL=WorkflowInstallWizard-VPBOZJY5.js.map
96
+ //# sourceMappingURL=WorkflowInstallWizard-MVTO43MW.js.map
@@ -887,6 +887,177 @@ function resolveMarketplacePluginTargetFromRepo(ref, repoDir) {
887
887
  import fs5 from "fs";
888
888
  import os3 from "os";
889
889
  import path4 from "path";
890
+
891
+ // src/core/gates/gates.ts
892
+ import { createHash } from "crypto";
893
+ var GATE_NAMES = [
894
+ "planApproval",
895
+ "merge",
896
+ "budget",
897
+ "irreversible"
898
+ ];
899
+ var DEFAULT_GATES = Object.freeze({
900
+ planApproval: "ask",
901
+ merge: "auto",
902
+ budget: "ask",
903
+ irreversible: "ask"
904
+ });
905
+ function resolveGates(sources) {
906
+ const gates = { ...DEFAULT_GATES };
907
+ const provenance = Object.fromEntries(
908
+ GATE_NAMES.map((name) => [name, "default"])
909
+ );
910
+ for (const name of GATE_NAMES) {
911
+ const floors = ["team", "global"].filter(
912
+ (source) => sources[source]?.[name] !== void 0
913
+ );
914
+ if (floors.length === 0) continue;
915
+ const asking = floors.find((source) => sources[source][name] === "ask");
916
+ gates[name] = asking ? "ask" : "auto";
917
+ provenance[name] = asking ?? floors[0];
918
+ }
919
+ const ignored = [];
920
+ for (const source of ["project", "build"]) {
921
+ const tightened = tightenGates(gates, sources[source]);
922
+ for (const name of GATE_NAMES) {
923
+ if (tightened.gates[name] !== gates[name]) provenance[name] = source;
924
+ }
925
+ for (const gate of tightened.ignored) {
926
+ ignored.push({
927
+ gate,
928
+ source,
929
+ floor: provenance[gate]
930
+ });
931
+ }
932
+ Object.assign(gates, tightened.gates);
933
+ }
934
+ return { gates, provenance, ignored };
935
+ }
936
+ function tightenGates(base, overrides) {
937
+ const gates = { ...base };
938
+ const ignored = [];
939
+ for (const name of GATE_NAMES) {
940
+ const mode = overrides?.[name];
941
+ if (mode === "ask") gates[name] = "ask";
942
+ else if (mode === "auto" && base[name] === "ask") ignored.push(name);
943
+ }
944
+ return { gates, ignored };
945
+ }
946
+ function describeIgnoredGate(input2) {
947
+ return `gate ${input2.gate}=auto from ${input2.source} ignored: the ${input2.floor} floor is ask, and a ${input2.source} setting may only tighten a gate`;
948
+ }
949
+ function isGateName(name) {
950
+ return GATE_NAMES.includes(name);
951
+ }
952
+ function parseGateOverrides(raw, where) {
953
+ if (raw === void 0) return void 0;
954
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
955
+ throw new Error(
956
+ `Invalid config: "${where}" field "gates" must be an object of gate \u2192 "ask" | "auto"`
957
+ );
958
+ }
959
+ const out = {};
960
+ for (const [name, mode] of Object.entries(raw)) {
961
+ if (!isGateName(name)) {
962
+ throw new Error(
963
+ `Invalid config: "${where}" has unknown gate "${name}" (gates: ${GATE_NAMES.join(", ")})`
964
+ );
965
+ }
966
+ if (mode !== "ask" && mode !== "auto") {
967
+ throw new Error(
968
+ `Invalid config: "${where}" gate "${name}" must be "ask" or "auto"`
969
+ );
970
+ }
971
+ out[name] = mode;
972
+ }
973
+ return out;
974
+ }
975
+ function parseGateFlags(flags) {
976
+ if (!flags || flags.length === 0) return void 0;
977
+ const out = {};
978
+ for (const pair of flags.flatMap((flag) => flag.split(","))) {
979
+ const [name, mode] = pair.trim().split("=");
980
+ if (!name || !isGateName(name) || mode !== "ask" && mode !== "auto") {
981
+ throw new Error(
982
+ `--gate expects <gate>=ask|auto with gate one of ${GATE_NAMES.join(", ")}; got "${pair}"`
983
+ );
984
+ }
985
+ out[name] = mode;
986
+ }
987
+ return out;
988
+ }
989
+ function gateDenialReason(input2) {
990
+ return `Drisp gate: ${input2.gate} is ask \u2014 this call ${input2.reason} (\`${input2.command}\`) and needs a member's approval. It was not run. Do not retry it or work around the gate. ` + (input2.subagent ? "Report blocked to the Orchestrator with this exact command as the decision needed." : "Finish what you can without it, note it in the Journal, and end your Turn: the Run parks for the member once no subagent is running, and you are woken with their answer.");
991
+ }
992
+ function isPlanStartAnswer(value) {
993
+ if (typeof value === "string")
994
+ return /\{\s*"choice"\s*:\s*"start"\s*\}/.test(value);
995
+ return typeof value === "object" && value !== null && value.choice === "start";
996
+ }
997
+ var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
998
+ function planApprovalDenial(toolName, hits) {
999
+ const dispatch = DISPATCH_TOOLS.has(toolName);
1000
+ if (!dispatch && !hits.some((hit) => hit.gate === "merge")) return null;
1001
+ return `Drisp gate: plan approval pending: end the Turn with <!-- AWAITING_APPROVAL --> as the Journal's last line. This ${dispatch ? "subagent dispatch" : "merge or push to the default branch"} was not run: nothing is built, merged or pushed before a member starts the plan. You are woken with their answer.`;
1002
+ }
1003
+ function failingChecksReason(command, checks) {
1004
+ return `Drisp gate: merge is auto, but the pull request's required checks are failing (${checks.join(", ")}), so \`${command}\` was not run. Fix the failing checks, then merge.`;
1005
+ }
1006
+ function unclaimedSubagentPermissionReason(toolName) {
1007
+ return `Drisp: no rule answers this ${toolName} permission inside a subagent, so it is denied instead of waiting for a member. Do not retry it; report blocked to the Orchestrator with what you needed and why.`;
1008
+ }
1009
+ function directPushReason(command, options = {}) {
1010
+ return `Drisp gate: merge is auto, but \`${command}\` pushes straight to the default branch, skipping the required checks. ` + (options.driveMerge ? `It was not run. ${DRIVE_MERGE_INSTRUCTION}` : `It was not run. Push a branch, open a pull request and merge that once its checks pass.`);
1011
+ }
1012
+ var DRIVE_MERGE_INSTRUCTION = "Merge with `drisp drive merge <item> --worktree <path> [--pr <n>]`: it rebases, proves the checks and merges, and prints the outcome.";
1013
+ function driveMergeOnlyReason(command) {
1014
+ return `Drisp gate: merge is auto, but in a drive Run only \`drisp drive merge\` merges, so \`${command}\` was not run. ` + DRIVE_MERGE_INSTRUCTION;
1015
+ }
1016
+ function driveMergeSubagentReason(command) {
1017
+ return `Drisp gate: \`${command}\` was not run. Only the Orchestrator merges; end with your Report and leave the merge to it.`;
1018
+ }
1019
+ function unverifiedChecksReason(command, detail) {
1020
+ return `Drisp gate: merge is auto, but the required checks of \`${command}\` could not be verified (${detail}), so it was not run. Retry once the checks have passed.`;
1021
+ }
1022
+ function gatedCallOf(toolName, toolInput, hits) {
1023
+ const input2 = typeof toolInput === "object" && toolInput !== null ? toolInput : {};
1024
+ const command = !toolName.startsWith("mcp__") && typeof input2["command"] === "string" ? input2["command"] : JSON.stringify(toolInput ?? {});
1025
+ const parts = [];
1026
+ for (const hit of hits) {
1027
+ if (!parts.some((part) => part.rule === hit.rule)) {
1028
+ parts.push({ gate: hit.gate, rule: hit.rule, reason: hit.reason });
1029
+ }
1030
+ }
1031
+ const rules = parts.map((part) => part.rule).sort();
1032
+ const fingerprint = createHash("sha256").update(JSON.stringify([toolName, command, rules])).digest("hex");
1033
+ return { command, parts, fingerprint };
1034
+ }
1035
+ function gateQuestion(fingerprint) {
1036
+ return `gate:${fingerprint}`;
1037
+ }
1038
+ function gateHarnessCheck(input2) {
1039
+ if (input2.blocksToolCalls) return null;
1040
+ const asking = ["merge", "irreversible"].filter(
1041
+ (gate) => input2.gates[gate] === "ask"
1042
+ );
1043
+ if (asking.length === 0) return null;
1044
+ const explicit = asking.filter((gate) => input2.explicit.has(gate));
1045
+ if (explicit.length === 0) {
1046
+ return {
1047
+ kind: "warn",
1048
+ message: `gates are not enforced on ${input2.harness}; merge/irreversible run unattended (the ask defaults cannot block a tool call there; set a gate explicitly to refuse such Runs instead)`
1049
+ };
1050
+ }
1051
+ return {
1052
+ kind: "refuse",
1053
+ message: `Human gates hold only on Claude Code: ${input2.harness} cannot block a tool call, so the ${explicit.join(" and ")} gate${explicit.length > 1 ? "s" : ""} set to ask cannot be enforced. Run this Workflow on Claude Code, or set ${explicit.map((gate) => `${gate}=auto`).join(" and ")} in the Team or global gates (a project or --gate can only tighten them).`
1054
+ };
1055
+ }
1056
+ function unknownPushTargetReason(command) {
1057
+ return `Drisp gate: merge is auto, but \`${command}\` pushes to a branch that cannot be read from the command, which may be the default branch, so it was not run. Name the branch explicitly (\`git push origin <branch>\`).`;
1058
+ }
1059
+
1060
+ // src/infra/plugins/config.ts
890
1061
  var EMPTY_CONFIG = { plugins: [], additionalDirectories: [] };
891
1062
  var STALE_CONFIG_KEYS = ["channels"];
892
1063
  var warnedStaleKeys = /* @__PURE__ */ new Set();
@@ -952,6 +1123,13 @@ function readConfigFile(configPath, baseDir) {
952
1123
  `Invalid config: "${configPath}" field "permissionGraceMs" must be a non-negative number of milliseconds`
953
1124
  );
954
1125
  }
1126
+ if (raw.checks !== void 0 && (!Array.isArray(raw.checks) || raw.checks.length === 0 || !raw.checks.every(
1127
+ (check2) => typeof check2 === "string" && check2.trim().length > 0
1128
+ ))) {
1129
+ throw new Error(
1130
+ `Invalid config: "${configPath}" field "checks" must be a non-empty array of non-empty command strings`
1131
+ );
1132
+ }
955
1133
  if ("workflowMarketplaceSource" in raw) {
956
1134
  throw new Error(
957
1135
  `Invalid config: "${configPath}" uses deprecated "workflowMarketplaceSource"; use "workflowMarketplaceSources"`
@@ -994,7 +1172,9 @@ function readConfigFile(configPath, baseDir) {
994
1172
  // is NOT relative-resolved against baseDir (R1) — Issue 3 resolves it
995
1173
  // to an absolute path at install time.
996
1174
  mcpServers: raw.mcpServers,
997
- skills: raw.skills
1175
+ skills: raw.skills,
1176
+ gates: parseGateOverrides(raw.gates, configPath),
1177
+ ...raw.checks !== void 0 ? { checks: raw.checks } : {}
998
1178
  };
999
1179
  }
1000
1180
  function readExistingConfig(configPath) {
@@ -21134,9 +21314,21 @@ var pluginSpec = external_exports.union(
21134
21314
  ],
21135
21315
  { error: PLUGIN_MESSAGE }
21136
21316
  );
21317
+ var WORKFLOW_ALLOWABLE_TOOLS = /* @__PURE__ */ new Set([
21318
+ "SendMessage",
21319
+ "ToolSearch",
21320
+ "Monitor",
21321
+ "TaskStop",
21322
+ "ListAgents"
21323
+ ]);
21324
+ var EXACT_MCP_TOOL = /^mcp__[A-Za-z0-9_.-]+__[A-Za-z0-9_.-]+$/;
21325
+ function isWorkflowAllowableTool(name) {
21326
+ return WORKFLOW_ALLOWABLE_TOOLS.has(name) || EXACT_MCP_TOOL.test(name);
21327
+ }
21137
21328
  var FIELD_MESSAGES = {
21138
21329
  plugins: PLUGIN_MESSAGE,
21139
21330
  askRules: '"askRules" must be an array of non-empty tool-name patterns',
21331
+ allowedTools: '"allowedTools" may only name SendMessage, ToolSearch, Monitor, TaskStop, ListAgents or an exact MCP tool (mcp__<server>__<tool>, no wildcards)',
21140
21332
  examplePrompts: '"examplePrompts" must be an array of strings',
21141
21333
  skills: '"skills" must be an array of names',
21142
21334
  mcpServers: '"mcpServers" must be an array of names',
@@ -21158,6 +21350,9 @@ var workflowSchema = external_exports.looseObject({
21158
21350
  loop: loopSchema.optional(),
21159
21351
  isolation: external_exports.string().optional(),
21160
21352
  askRules: external_exports.array(external_exports.string().refine(nonEmpty), { error: FIELD_MESSAGES["askRules"] }).optional(),
21353
+ allowedTools: external_exports.array(external_exports.string().refine(isWorkflowAllowableTool), {
21354
+ error: FIELD_MESSAGES["allowedTools"]
21355
+ }).optional(),
21161
21356
  model: external_exports.string().optional(),
21162
21357
  effort: external_exports.string().optional(),
21163
21358
  env: external_exports.record(external_exports.string(), external_exports.string()).optional(),
@@ -21359,6 +21554,46 @@ function orchestratorWorkflow(workflowFile) {
21359
21554
  };
21360
21555
  }
21361
21556
 
21557
+ // src/core/workflows/builtins/drive/workflow.md
21558
+ var workflow_default2 = '# Drive: build a Plan with native subagents\n\nYou are the Orchestrator of a Drisp Build (root ADR-0018). This one session plans the work and delivers it. You read\nthe issues yourself, write the plan to your Journal, and hand each issue to a native subagent working in its own git\nworktree. Your working directory is the project\'s main checkout. You never write product code in it: subagents do.\n\nThe Run\'s prompt names the work in one of two ways:\n\n- `Linear project: <id>`: the Plan is that Linear project. Read it with the `linear` MCP server.\n- an inline issue list: each issue with its identifier, title, description, acceptance criteria and the issues it\n depends on. Use it as given; never look it up elsewhere.\n\nAnything else the member wrote in the prompt is their instruction for this Build. It defines the approved scope,\ntogether with each issue\'s own text.\n\n## 1. Plan (first Turn)\n\n1. Read every issue of the Plan. From Linear: `list_issues` for the project, then `get_issue` with\n `includeRelations: true` for each, and `list_comments` where the description leaves questions. An issue is\n eligible only in Todo with the `ready-for-agent` label and without `ready-for-human`, `needs-info`, `needs-triage`\n or `wontfix`. Skip the others and say why in the plan. An inline issue list is eligible as a whole.\n2. Order the eligible issues by their dependencies (`blockedBy` in Linear, "depends on" inline). An issue is ready\n when every issue it depends on is merged. Dependencies on an issue outside the Plan count as merged only when\n that issue is Done or Released.\n3. Prepare the main checkout: it must be clean and on the default branch, up to date with `origin` when there is one\n (`git pull --ff-only`). Subagent worktrees branch from its HEAD. Make sure `.claude/worktrees/` is ignored:\n `git check-ignore -q .claude/worktrees/x || echo \'.claude/worktrees/\' >> .git/info/exclude`. Note whether\n `origin` is a GitHub remote (`git remote get-url origin`).\n4. Write the plan into the Journal: delete the `<!-- JOURNAL_SKELETON -->` line, replace the skeleton\'s Plan\n section with these sections, and keep them current from then on:\n\n```\n## Plan\n\n| Item | Title | Depends on | Status | Attempts | Agent | Branch | PR |\n| ---- | ----- | ---------- | ------ | -------- | ----- | ------ | -- |\n\n## In flight\n\n| Item | Agent | Asked | Since |\n| ---- | ----- | ----- | ----- |\n\n## Decisions\n\n| Item | Question | Decision | Why |\n| ---- | -------- | -------- | --- |\n```\n\nStatus is one of `waiting`, `ready`, `running`, `blocked`, `done`, `merged`, `escalated`, `skipped`. Set it back to\n`running` whenever you continue a `blocked` or `done` item\'s subagent. `Agent` is the subagent\'s `agentId`, which you\nneed to continue it after a restart.\n\n5. End the Turn with `<!-- AWAITING_APPROVAL -->` as the Journal\'s last line: the member reads your plan and\n starts the Build. You are woken with the answer `{"choice":"start"}`: remove that line and dispatch (section 2).\n Woken with the member\'s words instead, revise the plan to them and end with `<!-- AWAITING_APPROVAL -->` again.\n\nKeep one step block in the Journal and revise it in place as you move from `Plan` (1) to `Deliver` (2) to `Clean up`\n(3); Drisp shows the step as the Run\'s progress:\n\n```\n<!-- TURN_PROTOCOL\nstep: Deliver\nstep_index: 2\nstep_total: 3\n-->\n```\n\n## 2. Dispatch\n\nDispatch ready items at once, at most 4 running subagents at a time: one `Agent` call per item, all in one message,\nso they run in parallel. Dispatch more as running ones finish. Each call\nuses `isolation: "worktree"` and `subagent_type: "general-purpose"`, and `model` set to the subagent model the Run\'s\nprompt names, if it names one. Before the calls, add each item to `## In flight` (Agent `pending`); when they return,\nrecord each `agentId` and `worktreeBranch` in the Plan.\n\nEach subagent\'s prompt is its brief, and it has nothing else: write it in full.\n\n- The item: identifier, title, the full description and acceptance criteria, and any comments that matter.\n- The approved scope: the member\'s instructions and the decisions in your Journal that concern this item.\n- What its dependencies delivered (their branches are merged into the default branch it starts from).\n- The rules and the Report contract below, copied verbatim.\n\n### Rules for every subagent (copy into the brief)\n\n- Work only in your own worktree, which is your working directory. Never touch the main checkout or another worktree.\n- First rename your branch to `drive/<item, lowercased>` (`git branch -m drive/<item>`; the brief names\n `drive/<item>-2` for a second attempt) and commit an empty start\n (`git commit --allow-empty -m "chore(<item>): start"`). Claude Code deletes a worktree with no commits when its\n subagent returns, so without it a `blocked` Report before any change would lose your worktree.\n- Build the item to its acceptance criteria. Run the project\'s checks that cover your change and keep their output\n as evidence. Commit with a clear message.\n- When `origin` exists, push your branch: `git push -u origin HEAD`. When `origin` is on GitHub, open a pull request\n to the default branch with `gh pr create --fill` (or a title and body that name the item), and put its URL in `pr`.\n Never merge it yourself.\n- When you need a decision you cannot take from the issue, the scope you were given or the code, stop before\n guessing and end with a `blocked` Report: the question, the options, and your recommendation. Do the same when a\n tool call is denied. You will be continued with the decision and keep your context.\n- End with `failed` when you cannot deliver for a reason a decision would not fix, with the cause in `evidence`.\n- Never wait with `sleep`. Wait for CI with `gh pr checks --watch` or `Monitor`.\n- Do not start subagents of your own.\n- Your final message ends with the Report: one fenced `json` block, nothing after it.\n\n### Report contract (copy into the brief)\n\n```json\n{\n "item": "<the item\'s identifier>",\n "status": "done | blocked | failed",\n "pr": "<pull request URL, or null>",\n "evidence": ["<check run, test output, commit, screenshot path>"],\n "question": "<the decision needed, when blocked, else null>",\n "options": ["<option>", "\u2026"],\n "recommendation": "<your preferred option and why, when blocked, else null>"\n}\n```\n\n## 3. Read each Report\n\nAn `Agent` call may return at once with `status: "async_launched"`: the subagent then runs in the background, and\nyou end your Turn as in section 4 until its `<task-notification>` wakes you.\n\nA subagent\'s Report is the last fenced `json` block in its final message: the Agent result when it returns one, or\nthe result inside the `<task-notification>` that wakes you. Read that, not your memory of the brief. When a result\nhas no parseable Report, `SendMessage` the subagent once: "End your reply with the Report: one fenced `json` block, as\nin your brief." Only if its next result still has none, count it as `failed` with "no Report" as the cause. Remove\nthe item from `## In flight` and act on its status:\n\n- `done`: check the evidence against the acceptance criteria (read the branch\'s diff with\n `git diff <default>...<branch>`; run a quick check when the evidence is thin). If it falls short, continue the\n subagent with what is missing, as for `blocked`. Otherwise record the PR and merge it (section 6).\n- `blocked`: decide it (section 4).\n- `failed`: handle it (section 5).\n\nAfter each merge, dispatch the items that became ready.\n\n## 4. Decide every `blocked` Report\n\nDecide every `blocked` Report yourself, within the approved scope; the member is not asked. Pick one of the options\n(or a better one the evidence supports) from the member\'s instructions, the issue\'s text, the repository\'s ADRs and\ndocs, and the code. Record the item, the question, the decision and the evidence it rests on in `## Decisions` before\nyou send it.\n\nThen continue the same subagent, so it keeps its context and worktree. If `SendMessage` is not loaded yet, load it\nby calling `ToolSearch` with `select:SendMessage`. Call `SendMessage` with `to` set to the subagent\'s `agentId` and a\nmessage that gives the decision, why, and "continue, then end with a Report". Put the item back in `## In flight`.\nIf the subagent\'s worktree is gone (`git worktree list`) and its branch has no commits, dispatch the item again\ninstead, with the decision in its brief. That re-dispatch does not count as an attempt.\n\n`SendMessage` returns at once; the subagent works in the background. Its reply arrives later as a\n`<task-notification>` that starts your next Turn in this same process. Do other work that is ready, then\nend your Turn. Ending a Turn while subagents run is expected: Drisp keeps the session going and you are woken by their\ncompletion. Never poll them with `sleep`.\n\nNever end a Turn waiting on nothing. Before ending one, call `ListAgents`: if no subagent is running and no\nnotification is pending, re-read `## In flight` and act on every item there (section 7) instead.\n\nEscalate instead of deciding only when the decision would change the approved scope (a new requirement, dropping an\nacceptance criterion, an irreversible or production action), and when a gate reserves it for the member: mark the\nitem `escalated` with the question, and continue the rest.\n\n## 5. Failures\n\nRe-dispatch a `failed` item once with the failure analysis: a new `Agent` call (worktree isolation) with the original\nbrief plus what failed and your analysis of why, on branch `drive/<item>-2`, or `SendMessage` to the same subagent\nwhen its worktree holds work worth keeping. Count the attempt in the Plan. On a second failure of the same item,\nstop: mark it `escalated` with both causes, mark the items that depend on it `escalated` as blocked by it, and\ncontinue every other item.\n\n## 6. Merge\n\nMerge one item at a time, in dependency order, only with this command, from the main checkout, with a Bash `timeout`\nof 600000:\n\n```\ndrisp drive merge <item> --worktree <the subagent\'s worktree> [--pr <number>]\n```\n\nIt checks whether the item is already merged, rebases, proves the checks (CI, or the project\'s own checks when there\nis none) and merges, all in code. It prints one JSON line; act on its `outcome`:\n\n- `merged` or `already_merged`: mark the item `merged`.\n- `conflict` or `checks_failed`: `SendMessage` the same subagent with `detail` and `output`: "fix your branch in\n place (rebase onto `<default branch>`, resolve, commit or remove every file, run the checks), and report\n again". That is not an attempt; a second `checks_failed` after its fix is a `failed` Report (section 5).\n- `checks_timeout`, or `checks_failed` whose `detail` starts "base kept moving": run the command again, at most\n twice; then handle it as `refused`.\n- `refused`: mark the item `escalated` with `detail`, continue the rest, and end with `NEEDS_HUMAN` (section 8).\n\nNever merge any other way: Drisp refuses `gh pr merge`, a push to the default branch, and a `git merge`, `reset` or\n`rebase` that moves it, in this Run.\nIf Drisp denies the command itself (a gate), do not retry it or work around it: note it in the Journal and end your\nTurn.\n\n## 7. After a crash or kill\n\nAfter a crash or kill, the Journal is what you have. On a fresh Turn, or a `<task-notification>` with status\n`stopped`, read `## In flight`: every item there was interrupted and its in-flight step is lost; its worktree and\nthe subagent\'s context are not. For each one:\n\n- with an `agentId`: `SendMessage` it: "Your last step was interrupted. Check your worktree\'s state, redo the step\n from your last commit, and end with a Report."\n- with Agent `pending` (the process died before the call returned): re-dispatch the item as in section 2. Check\n `git worktree list` first and name any worktree it left in the new brief.\n\n`ListAgents` shows which subagents still run; never dispatch a second subagent for an item whose subagent runs.\n\n## 8. Clean up and complete\n\nWhen every item is `merged`, `escalated` or `skipped`:\n\n1. Remove each merged item\'s worktree and branch: `git worktree remove <worktree path>` and `git branch -D <branch>`\n (only once `drisp drive merge` returned `merged` or `already_merged` for it; it already deleted the remote\n branch). Then `git worktree prune`. Keep the worktrees of escalated items.\n2. Write the outcome above the marker: each item with its status, PR and evidence, the decisions taken, and each\n escalation with what the member needs to do.\n3. If every item is `merged` or `skipped`, end the Journal with `<!-- WORKFLOW_COMPLETE -->` as its last non-empty\n line. If any item is `escalated`, end it with `<!-- NEEDS_HUMAN: <reason> -->` instead, the reason naming the\n escalated items and what the member must decide, so the Run parks for the member.\n\n## Linear (when the Plan is a Linear project)\n\nNever change an issue\'s status: Drisp derives In Progress, Review, Ready and Needs Input from the Run and its pull\nrequests. Write only two comments, through `linear`, each once: `PR: <url>` when an item\'s pull request opens, and,\nwhen an item is escalated, one comment that starts `**Needs input:**` and says what the member must decide. Never\nchange labels, priorities or descriptions.\n\n## Rules\n\n- You plan, decide, merge and clean up. Subagents write the code.\n- Keep the Journal current after every dispatch, Report, decision and merge: it is how a restart knows what was in\n flight.\n- Never claim a check passed, an item is done, or the member approved something without evidence.\n';
21559
+
21560
+ // src/core/workflows/builtins/drive/index.ts
21561
+ var DRIVE_WORKFLOW_NAME = "drive";
21562
+ var DRIVE_INSTRUCTIONS = workflow_default2;
21563
+ var DRIVE_LOOP = {
21564
+ enabled: true,
21565
+ completionMarker: DEFAULT_COMPLETION_MARKER,
21566
+ needsHumanMarker: DEFAULT_NEEDS_HUMAN_MARKER,
21567
+ maxIterations: 50,
21568
+ maxRunTokens: 6e7
21569
+ };
21570
+ var DRIVE_ALLOWED_TOOLS = [
21571
+ "SendMessage",
21572
+ "ToolSearch",
21573
+ "Monitor",
21574
+ "TaskStop",
21575
+ "ListAgents"
21576
+ ];
21577
+ function driveWorkflow(workflowFile) {
21578
+ return {
21579
+ name: DRIVE_WORKFLOW_NAME,
21580
+ description: "Builds a Plan's issues in one session: native subagents in their own worktrees, in parallel where dependencies allow, with the Orchestrator deciding their questions",
21581
+ promptTemplate: "{input}",
21582
+ plugins: [],
21583
+ model: "sonnet",
21584
+ skills: [],
21585
+ mcpServers: ["linear"],
21586
+ allowedTools: [...DRIVE_ALLOWED_TOOLS],
21587
+ loop: { ...DRIVE_LOOP },
21588
+ // The `planApproval` gate (ask by default) parks the plan Turn on this question (apps/cli docs/human-gates.md).
21589
+ approvalQuestion: BUILD_STRINGS.approvalQuestion,
21590
+ // The Runner bootstraps a Workflow Run under `standard`, where the Orchestrator parks at once on a Bash
21591
+ // permission. The human gates (merge, irreversible, budget, plan approval) are drive's safety layer.
21592
+ isolation: "autonomous",
21593
+ workflowFile
21594
+ };
21595
+ }
21596
+
21362
21597
  // src/core/workflows/builtins/index.ts
21363
21598
  var DEFAULT_NEEDS_HUMAN_CLOSED_MARKER = `${DEFAULT_NEEDS_HUMAN_MARKER} -->`;
21364
21599
  var DEFAULT_NEEDS_HUMAN_REASON_MARKER = `${DEFAULT_NEEDS_HUMAN_MARKER}: reason -->`;
@@ -21426,6 +21661,11 @@ function ensureBuiltinFile(name, fileName, content) {
21426
21661
  return filePath;
21427
21662
  }
21428
21663
  function resolveBuiltinWorkflow(name) {
21664
+ if (name === DRIVE_WORKFLOW_NAME) {
21665
+ return driveWorkflow(
21666
+ ensureBuiltinFile(name, "workflow.md", DRIVE_INSTRUCTIONS)
21667
+ );
21668
+ }
21429
21669
  if (name === ORCHESTRATOR_WORKFLOW_NAME) {
21430
21670
  return orchestratorWorkflow(
21431
21671
  ensureBuiltinFile(name, "workflow.md", ORCHESTRATOR_INSTRUCTIONS)
@@ -21453,7 +21693,7 @@ function resolveBuiltinWorkflow(name) {
21453
21693
  };
21454
21694
  }
21455
21695
  function listBuiltinWorkflows() {
21456
- return ["default", ORCHESTRATOR_WORKFLOW_NAME];
21696
+ return ["default", DRIVE_WORKFLOW_NAME, ORCHESTRATOR_WORKFLOW_NAME];
21457
21697
  }
21458
21698
 
21459
21699
  // src/core/workflows/builtins/build-child/template.ts
@@ -22852,6 +23092,14 @@ function describeDeferral(permission) {
22852
23092
  }
22853
23093
  function protocolInterruptionFor(interruption) {
22854
23094
  if (interruption.kind === "question" || !interruption.permission) return null;
23095
+ if (interruption.kind === "gate") {
23096
+ return {
23097
+ kind: "question",
23098
+ message: describeInterruption(interruption),
23099
+ requestId: interruption.permission.requestId,
23100
+ question: gateQuestion(interruption.fingerprint)
23101
+ };
23102
+ }
22855
23103
  return {
22856
23104
  kind: "question",
22857
23105
  message: describeInterruption(interruption),
@@ -22859,8 +23107,17 @@ function protocolInterruptionFor(interruption) {
22859
23107
  question: `${interruption.toolName}: ${interruption.permission.inputSummary}`
22860
23108
  };
22861
23109
  }
23110
+ var GATE_PARK_PREFIX = "Drisp gate";
23111
+ var GATE_COMMAND_MAX_CHARS = 12e3;
23112
+ function clip(text, max) {
23113
+ return text.length > max ? `${text.slice(0, max)}\u2026 (${text.length - max} more characters)` : text;
23114
+ }
22862
23115
  function describeInterruption(interruption) {
22863
23116
  switch (interruption.kind) {
23117
+ case "gate":
23118
+ return `${GATE_PARK_PREFIX}: a member must approve this ${interruption.toolName} call \u2014 ` + interruption.parts.map((part) => `${part.gate} is ask: ${part.reason} (${part.rule})`).join("; ") + `. Command:
23119
+ ${clip(interruption.command, GATE_COMMAND_MAX_CHARS)}
23120
+ Wake with --answer=allow|deny; an answer applies only to this exact call.`;
22864
23121
  case "ask_rule":
22865
23122
  if (interruption.permission) {
22866
23123
  return `ask rule "${interruption.rule}" fired on ${interruption.toolName}${describeDeferral(interruption.permission)}: ${interruption.permission.inputSummary} \u2014 wake with --answer=allow|deny`;
@@ -22891,10 +23148,19 @@ ${reply}
22891
23148
  function buildReplayGuidance(parked) {
22892
23149
  if (!parked || parked.kind !== "question" || !parked.requestId) return "";
22893
23150
  const call = parked.question ?? "the same call";
23151
+ if (call.startsWith("gate:")) {
23152
+ return `Before your previous Turn ended, a call (request ${parked.requestId}) was refused by a human gate set to ask, and the run parked for a member to decide:
23153
+ ${parked.message}
23154
+ If the reply approves it, re-issue that exact call now, with the same input (from a subagent, have it re-issue it): the member's answer is applied to it automatically. If the reply denies it, do not run it or work around the gate; carry on with what the reply says.
23155
+
23156
+ `;
23157
+ }
22894
23158
  return `Before your previous Turn ended, your request \`${call}\` (request ${parked.requestId}) was deferred because nobody answered it in time. Re-issue that exact call now, with the same input: if an answer was stored while this run was parked it is applied automatically, otherwise the request is held again for a human. Do not work around the deferred call or substitute a different one.
22895
23159
 
22896
23160
  `;
22897
23161
  }
23162
+ var PLAN_APPROVED_PROMPT = "The plan approval gate is set to auto, so your plan is approved without asking a member. Remove the `<!-- AWAITING_APPROVAL -->` line from the end of the Journal and start the work now, as the plan describes. Keep the Journal current and end by declaring a terminal marker as usual.";
23163
+ var PLAN_APPROVED_NOTICE = "plan approval gate is auto: the plan was approved without asking, and work starts now";
22898
23164
  function buildFailureDetail(event) {
22899
23165
  const parts = [];
22900
23166
  if (event.errorMessage) {
@@ -23238,6 +23504,44 @@ function handleTurnInFlight(phase, incoming, event, cfg) {
23238
23504
  };
23239
23505
  }
23240
23506
  const loop = cfg.loop;
23507
+ if (event.outcome?.kind === "suspend" && event.outcome.stopReason === AWAITING_APPROVAL_STOP_REASON && cfg.gates?.planApproval === "auto" && event.adapterSessionId) {
23508
+ const nextIteration2 = memoryAfterSuccess.iteration + 1;
23509
+ const continuation2 = {
23510
+ mode: "resume",
23511
+ handle: event.adapterSessionId
23512
+ };
23513
+ const prepared2 = prepareWorkflowTurn(cfg.workflowState, {
23514
+ prompt: cfg.initialPrompt,
23515
+ iteration: nextIteration2,
23516
+ configOverride: void 0
23517
+ });
23518
+ return {
23519
+ phase: {
23520
+ kind: "turn_in_flight",
23521
+ prompt: PLAN_APPROVED_PROMPT,
23522
+ continuation: continuation2,
23523
+ configOverride: prepared2.configOverride
23524
+ },
23525
+ memory: {
23526
+ ...memoryAfterSuccess,
23527
+ lastJournalHash: hashJournalContent(event.journalContent),
23528
+ iteration: nextIteration2,
23529
+ lastStopPrompt: PLAN_APPROVED_PROMPT,
23530
+ lastStopContinuation: continuation2
23531
+ },
23532
+ actions: [
23533
+ { type: "warn", message: PLAN_APPROVED_NOTICE },
23534
+ { type: "persist" },
23535
+ { type: "notify_iteration_complete" },
23536
+ {
23537
+ type: "start_turn",
23538
+ prompt: PLAN_APPROVED_PROMPT,
23539
+ continuation: continuation2,
23540
+ configOverride: prepared2.configOverride
23541
+ }
23542
+ ]
23543
+ };
23544
+ }
23241
23545
  if (event.outcome && (event.outcome.kind === "stop" || event.outcome.kind === "suspend")) {
23242
23546
  const outcome = event.outcome;
23243
23547
  const nextPhase = outcome.status === "completed" ? { kind: "completed" } : outcome.status === "failed" ? { kind: "failed", stopReason: outcome.stopReason } : {
@@ -23539,11 +23843,22 @@ function admitResult(result, previous, cfg) {
23539
23843
  },
23540
23844
  result.actions.filter(
23541
23845
  (a) => a.type === "notify_handover_completed" || a.type === "notify_iteration_complete"
23542
- )
23846
+ ),
23847
+ cfg.gates
23543
23848
  );
23544
23849
  }
23545
- function parkResource(memory, stop, notifications = []) {
23850
+ function parkResource(memory, stop, notifications = [], gates) {
23546
23851
  const interruption = resourceInterruption(stop);
23852
+ if (stop.cause === "tokens" && gates?.budget === "auto") {
23853
+ return {
23854
+ phase: {
23855
+ kind: "failed",
23856
+ stopReason: `${interruption.message} \u2014 the budget gate is auto, so the Run stopped instead of asking a member; the Journal holds the work done so far`
23857
+ },
23858
+ memory,
23859
+ actions: [{ type: "persist" }, ...notifications]
23860
+ };
23861
+ }
23547
23862
  return {
23548
23863
  phase: {
23549
23864
  kind: "awaiting_attention",
@@ -23568,7 +23883,9 @@ function step(phase, memory, event, cfg) {
23568
23883
  ...memory,
23569
23884
  cumulativeTokens: event.cumulativeTokens
23570
23885
  },
23571
- { ...event.stop, checkpointPath: memory.checkpoint?.path }
23886
+ { ...event.stop, checkpointPath: memory.checkpoint?.path },
23887
+ [],
23888
+ cfg.gates
23572
23889
  );
23573
23890
  return admitResult(transition(phase, memory, event, cfg), memory, cfg);
23574
23891
  }
@@ -23628,6 +23945,7 @@ function isQueuedSteer(value) {
23628
23945
  }
23629
23946
 
23630
23947
  // src/core/workflows/workflowRunner.ts
23948
+ var BUDGET_HARD_OVERRUN = 1.5;
23631
23949
  var NULL_TOKENS = {
23632
23950
  input: null,
23633
23951
  output: null,
@@ -23915,7 +24233,8 @@ function createWorkflowRunner(input2) {
23915
24233
  initialPrompt: input2.prompt,
23916
24234
  loop,
23917
24235
  journalAbsPath,
23918
- journalPromptPath
24236
+ journalPromptPath,
24237
+ ...input2.gates ? { gates: input2.gates } : {}
23919
24238
  };
23920
24239
  const initial = createInitialRun(cfg, {
23921
24240
  initialContinuation: input2.initialContinuation,
@@ -23926,7 +24245,11 @@ function createWorkflowRunner(input2) {
23926
24245
  awaitingAttentionStopReason: input2.resumedStopReason
23927
24246
  });
23928
24247
  let phase = initial.phase;
23929
- memory = initial.memory;
24248
+ memory = {
24249
+ ...initial.memory,
24250
+ ...cfg.gates ? { gates: cfg.gates } : {},
24251
+ ...input2.planApproved ? { planApproved: true } : {}
24252
+ };
23930
24253
  persist();
23931
24254
  applySteer = (steer) => {
23932
24255
  if (persistenceFailure) return false;
@@ -23965,17 +24288,32 @@ function createWorkflowRunner(input2) {
23965
24288
  }
23966
24289
  };
23967
24290
  }
23968
- function stop(reason) {
23969
- if (finished || stopped || cancelled) return;
23970
- stopped = reason;
23971
- const checkpoint = journalAbsPath ? checkpointFromJournal(readJournal(journalAbsPath)) : null;
23972
- if (checkpoint) memory = { ...memory, checkpoint };
24291
+ let stopEvent = null;
24292
+ let stopFired = false;
24293
+ const pendingStop = () => stopEvent;
24294
+ function fireStop() {
24295
+ if (stopFired || !stopEvent) return;
24296
+ stopFired = true;
23973
24297
  input2.abortCurrentTurn?.();
23974
- resolveStop({
23975
- type: "resource_exhausted",
23976
- stop: reason,
23977
- cumulativeTokens: memory.cumulativeTokens ?? startTokens
23978
- });
24298
+ resolveStop(stopEvent);
24299
+ }
24300
+ function stop(reason, force = false) {
24301
+ if (finished || cancelled) return;
24302
+ if (!stopped) {
24303
+ stopped = reason;
24304
+ const checkpoint = journalAbsPath ? checkpointFromJournal(readJournal(journalAbsPath)) : null;
24305
+ if (checkpoint) memory = { ...memory, checkpoint };
24306
+ stopEvent = {
24307
+ type: "resource_exhausted",
24308
+ stop: reason,
24309
+ cumulativeTokens: memory.cumulativeTokens ?? startTokens
24310
+ };
24311
+ }
24312
+ if (force || reason.cause !== "tokens" || !input2.whenQuiescent) {
24313
+ fireStop();
24314
+ return;
24315
+ }
24316
+ input2.whenQuiescent(fireStop);
23979
24317
  }
23980
24318
  function observe(usage, enforce = true) {
23981
24319
  if (finished) return;
@@ -23992,7 +24330,15 @@ function createWorkflowRunner(input2) {
23992
24330
  memory = { ...memory, usage: cumulativeTokens };
23993
24331
  persist();
23994
24332
  }
23995
- if (!enforce || stopped) return;
24333
+ if (!enforce) return;
24334
+ if (stopped) {
24335
+ const limit2 = loop?.maxRunTokens;
24336
+ const used = budgetTokens(memory) ?? 0;
24337
+ if (stopped.cause === "tokens" && limit2 !== void 0 && used >= limit2 * BUDGET_HARD_OVERRUN) {
24338
+ stop(stopped, true);
24339
+ }
24340
+ return;
24341
+ }
23996
24342
  const limit = admitContinuation({
23997
24343
  loop,
23998
24344
  iteration: memory.iteration,
@@ -24054,7 +24400,7 @@ function createWorkflowRunner(input2) {
24054
24400
  return finalStop(result2);
24055
24401
  }
24056
24402
  observe(result2.tokens, false);
24057
- if (isStopped()) return finalStop(await stopPromise);
24403
+ if (isStopped()) return finalStop(pendingStop() ?? await stopPromise);
24058
24404
  return result2;
24059
24405
  } finally {
24060
24406
  finished = true;
@@ -24391,12 +24737,32 @@ function collectMcpServersWithOptions(pluginDirs) {
24391
24737
  }
24392
24738
 
24393
24739
  export {
24740
+ resolveJournalPath,
24394
24741
  WAITING_ON_CHILDREN_STOP_REASON,
24395
24742
  AWAITING_APPROVAL_STOP_REASON,
24396
24743
  budgetTokens,
24397
24744
  runUsage,
24398
24745
  createSteerQueue,
24399
24746
  DEFAULT_PERMISSION_GRACE_MS,
24747
+ GATE_NAMES,
24748
+ DEFAULT_GATES,
24749
+ resolveGates,
24750
+ tightenGates,
24751
+ describeIgnoredGate,
24752
+ parseGateFlags,
24753
+ gateDenialReason,
24754
+ isPlanStartAnswer,
24755
+ planApprovalDenial,
24756
+ failingChecksReason,
24757
+ unclaimedSubagentPermissionReason,
24758
+ directPushReason,
24759
+ driveMergeOnlyReason,
24760
+ driveMergeSubagentReason,
24761
+ unverifiedChecksReason,
24762
+ gatedCallOf,
24763
+ gateQuestion,
24764
+ gateHarnessCheck,
24765
+ unknownPushTargetReason,
24400
24766
  serializeRunMemory,
24401
24767
  deserializeRunMemory,
24402
24768
  wakesFreshAfterHandover,
@@ -24428,8 +24794,10 @@ export {
24428
24794
  selectWorkflowCapabilities,
24429
24795
  BUILD_STRINGS,
24430
24796
  GATES,
24797
+ ORCHESTRATOR_WORKFLOW_NAME,
24431
24798
  ORCHESTRATOR_LIMITS,
24432
24799
  ORCHESTRATOR_LOOP,
24800
+ DRIVE_WORKFLOW_NAME,
24433
24801
  listBuiltinWorkflows,
24434
24802
  DESCRIPTION_TEMPLATE,
24435
24803
  QA_SECTIONS,
@@ -24461,4 +24829,4 @@ export {
24461
24829
  compileWorkflowPlan,
24462
24830
  collectMcpServersWithOptions
24463
24831
  };
24464
- //# sourceMappingURL=chunk-SACVOS6T.js.map
24832
+ //# sourceMappingURL=chunk-GGAPZULS.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runMachineRunners
3
- } from "./chunk-5TM2ARN3.js";
3
+ } from "./chunk-YXDCO7SI.js";
4
4
 
5
5
  // src/app/entry/runnerDaemon.ts
6
6
  if (process.argv.includes("--help")) {
@@ -13,4 +13,4 @@ if (process.argv.includes("--help")) {
13
13
  process.exitCode = 1;
14
14
  });
15
15
  }
16
- //# sourceMappingURL=chunk-CD6EY4ZE.js.map
16
+ //# sourceMappingURL=chunk-PAPYHUIO.js.map