@yycholla/pi-dynamic-workflows 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +325 -0
  3. package/assets/readme/hero.png +0 -0
  4. package/assets/readme/hero.svg +70 -0
  5. package/assets/readme/package-cover.png +0 -0
  6. package/assets/readme/package-cover.svg +66 -0
  7. package/assets/readme/workflow.png +0 -0
  8. package/assets/readme/workflow.svg +77 -0
  9. package/dist/accept-workflow-guidance.d.ts +14 -0
  10. package/dist/accept-workflow-guidance.js +53 -0
  11. package/dist/adversarial-review.d.ts +30 -0
  12. package/dist/adversarial-review.js +107 -0
  13. package/dist/agent-history.d.ts +20 -0
  14. package/dist/agent-history.js +122 -0
  15. package/dist/agent-registry.d.ts +83 -0
  16. package/dist/agent-registry.js +190 -0
  17. package/dist/agent.d.ts +364 -0
  18. package/dist/agent.js +714 -0
  19. package/dist/builtin-commands.d.ts +19 -0
  20. package/dist/builtin-commands.js +251 -0
  21. package/dist/builtin-workflows.d.ts +45 -0
  22. package/dist/builtin-workflows.js +121 -0
  23. package/dist/code-review.d.ts +26 -0
  24. package/dist/code-review.js +181 -0
  25. package/dist/config.d.ts +37 -0
  26. package/dist/config.js +44 -0
  27. package/dist/deep-research.d.ts +30 -0
  28. package/dist/deep-research.js +124 -0
  29. package/dist/display.d.ts +134 -0
  30. package/dist/display.js +248 -0
  31. package/dist/effort-command.d.ts +28 -0
  32. package/dist/effort-command.js +68 -0
  33. package/dist/enums.d.ts +69 -0
  34. package/dist/enums.js +78 -0
  35. package/dist/errors.d.ts +113 -0
  36. package/dist/errors.js +140 -0
  37. package/dist/extension-reload.d.ts +37 -0
  38. package/dist/extension-reload.js +78 -0
  39. package/dist/fs-persistence.d.ts +63 -0
  40. package/dist/fs-persistence.js +102 -0
  41. package/dist/index.d.ts +57 -0
  42. package/dist/index.js +35 -0
  43. package/dist/logger.d.ts +21 -0
  44. package/dist/logger.js +66 -0
  45. package/dist/model-routing.d.ts +30 -0
  46. package/dist/model-routing.js +50 -0
  47. package/dist/model-spec.d.ts +29 -0
  48. package/dist/model-spec.js +252 -0
  49. package/dist/model-tier-config.d.ts +133 -0
  50. package/dist/model-tier-config.js +249 -0
  51. package/dist/run-persistence.d.ts +180 -0
  52. package/dist/run-persistence.js +294 -0
  53. package/dist/saved-commands.d.ts +28 -0
  54. package/dist/saved-commands.js +100 -0
  55. package/dist/shared-store.d.ts +98 -0
  56. package/dist/shared-store.js +212 -0
  57. package/dist/structured-output.d.ts +19 -0
  58. package/dist/structured-output.js +30 -0
  59. package/dist/task-panel.d.ts +61 -0
  60. package/dist/task-panel.js +422 -0
  61. package/dist/usage-limit-scheduler.d.ts +145 -0
  62. package/dist/usage-limit-scheduler.js +368 -0
  63. package/dist/web-tools.d.ts +20 -0
  64. package/dist/web-tools.js +120 -0
  65. package/dist/workflow-authoring-coverage.d.ts +70 -0
  66. package/dist/workflow-authoring-coverage.js +421 -0
  67. package/dist/workflow-authoring-reference.d.ts +20 -0
  68. package/dist/workflow-authoring-reference.js +156 -0
  69. package/dist/workflow-capability-contract.d.ts +131 -0
  70. package/dist/workflow-capability-contract.js +604 -0
  71. package/dist/workflow-commands.d.ts +18 -0
  72. package/dist/workflow-commands.js +260 -0
  73. package/dist/workflow-comprehension.d.ts +133 -0
  74. package/dist/workflow-comprehension.js +1321 -0
  75. package/dist/workflow-context-measurement.d.ts +72 -0
  76. package/dist/workflow-context-measurement.js +213 -0
  77. package/dist/workflow-control-tool.d.ts +30 -0
  78. package/dist/workflow-control-tool.js +176 -0
  79. package/dist/workflow-delivery-choice.d.ts +20 -0
  80. package/dist/workflow-delivery-choice.js +48 -0
  81. package/dist/workflow-editor.d.ts +93 -0
  82. package/dist/workflow-editor.js +363 -0
  83. package/dist/workflow-manager.d.ts +492 -0
  84. package/dist/workflow-manager.js +1124 -0
  85. package/dist/workflow-paths.d.ts +22 -0
  86. package/dist/workflow-paths.js +46 -0
  87. package/dist/workflow-release-gate.d.ts +39 -0
  88. package/dist/workflow-release-gate.js +309 -0
  89. package/dist/workflow-saved.d.ts +38 -0
  90. package/dist/workflow-saved.js +126 -0
  91. package/dist/workflow-settings.d.ts +70 -0
  92. package/dist/workflow-settings.js +131 -0
  93. package/dist/workflow-tool.d.ts +71 -0
  94. package/dist/workflow-tool.js +367 -0
  95. package/dist/workflow-ui.d.ts +182 -0
  96. package/dist/workflow-ui.js +1587 -0
  97. package/dist/workflow.d.ts +333 -0
  98. package/dist/workflow.js +1151 -0
  99. package/dist/workflows-models-command.d.ts +31 -0
  100. package/dist/workflows-models-command.js +156 -0
  101. package/dist/worktree.d.ts +25 -0
  102. package/dist/worktree.js +61 -0
  103. package/extensions/workflow.ts +151 -0
  104. package/package.json +104 -0
  105. package/skills/workflow-authoring/SKILL.md +30 -0
  106. package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
  107. package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
  108. package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
  109. package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
  110. package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
  111. package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
  112. package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
  113. package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
  114. package/skills/workflow-authoring/examples/structured-output.js +37 -0
  115. package/skills/workflow-authoring/examples/tournament.js +81 -0
  116. package/skills/workflow-authoring/examples/validated-gate.js +63 -0
  117. package/skills/workflow-authoring/references/capabilities.md +41 -0
  118. package/skills/workflow-authoring/references/capability-details.md +357 -0
  119. package/skills/workflow-authoring/references/common-helpers.md +4 -0
  120. package/skills/workflow-authoring/references/debugging.md +27 -0
  121. package/skills/workflow-authoring/references/focused-recipes.md +11 -0
  122. package/skills/workflow-authoring/references/helpers.md +9 -0
  123. package/skills/workflow-authoring/references/lifecycle.md +33 -0
  124. package/skills/workflow-authoring/references/pattern-selection.md +15 -0
  125. package/skills/workflow-authoring/references/quality-helpers.md +8 -0
  126. package/skills/workflow-authoring/references/registry-ownership.md +15 -0
  127. package/skills/workflow-authoring/references/retry-helper.md +5 -0
  128. package/skills/workflow-authoring/references/review.md +43 -0
  129. package/skills/workflow-authoring/references/runtime.md +27 -0
  130. package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
  131. package/skills/workflow-authoring/references/versions.md +13 -0
  132. package/skills/workflow-patterns/SKILL.md +51 -0
  133. package/src/accept-workflow-guidance.ts +71 -0
  134. package/src/adversarial-review.ts +120 -0
  135. package/src/agent-history.ts +157 -0
  136. package/src/agent-registry.ts +221 -0
  137. package/src/agent.ts +929 -0
  138. package/src/builtin-commands.ts +286 -0
  139. package/src/builtin-workflows.ts +155 -0
  140. package/src/code-review.ts +183 -0
  141. package/src/config.ts +55 -0
  142. package/src/deep-research.ts +135 -0
  143. package/src/display.ts +367 -0
  144. package/src/effort-command.ts +87 -0
  145. package/src/enums.ts +77 -0
  146. package/src/errors.ts +199 -0
  147. package/src/extension-reload.ts +100 -0
  148. package/src/fs-persistence.ts +124 -0
  149. package/src/index.ts +176 -0
  150. package/src/logger.ts +88 -0
  151. package/src/model-routing.ts +73 -0
  152. package/src/model-spec.ts +309 -0
  153. package/src/model-tier-config.ts +296 -0
  154. package/src/run-persistence.ts +484 -0
  155. package/src/saved-commands.ts +115 -0
  156. package/src/shared-store.ts +228 -0
  157. package/src/structured-output.ts +47 -0
  158. package/src/task-panel.ts +490 -0
  159. package/src/usage-limit-scheduler.ts +432 -0
  160. package/src/web-tools.ts +124 -0
  161. package/src/workflow-authoring-coverage.ts +486 -0
  162. package/src/workflow-authoring-reference.ts +186 -0
  163. package/src/workflow-capability-contract.ts +806 -0
  164. package/src/workflow-commands.ts +287 -0
  165. package/src/workflow-comprehension.ts +1673 -0
  166. package/src/workflow-context-measurement.ts +262 -0
  167. package/src/workflow-control-tool.ts +238 -0
  168. package/src/workflow-delivery-choice.ts +69 -0
  169. package/src/workflow-editor.ts +444 -0
  170. package/src/workflow-manager.ts +1405 -0
  171. package/src/workflow-paths.ts +63 -0
  172. package/src/workflow-release-gate.ts +529 -0
  173. package/src/workflow-saved.ts +180 -0
  174. package/src/workflow-settings.ts +194 -0
  175. package/src/workflow-tool.ts +464 -0
  176. package/src/workflow-ui.ts +1789 -0
  177. package/src/workflow.ts +1615 -0
  178. package/src/workflows-models-command.ts +211 -0
  179. package/src/worktree.ts +76 -0
@@ -0,0 +1,363 @@
1
+ /**
2
+ * "Workflows mode" keyword trigger: while the submitted message contains the
3
+ * bounded word `workflow`/`workflows` (or a configured custom trigger word),
4
+ * the message is transformed at submit time to instruct Pi to actually run the
5
+ * workflow tool. Detection is purely textual (`event.text` on the `input`
6
+ * hook) — it does not depend on, or own, the host's editor component.
7
+ */
8
+ import { DEFAULT_KEYWORD_TRIGGER_WORD, normalizeKeywordTriggerWord } from "./config.js";
9
+ import { effortDirective, isSubstantive } from "./effort-command.js";
10
+ import { loadWorkflowSettings, saveWorkflowSettings, } from "./workflow-settings.js";
11
+ // A keyword trigger is a configured literal term. All trigger words use token
12
+ // boundaries so slash commands, paths, and identifier-like text stay untouched.
13
+ // The default `workflow` trigger additionally supports the plural `workflows`.
14
+ function escapeRegExp(text) {
15
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16
+ }
17
+ function triggerSource(triggerWord) {
18
+ const escaped = escapeRegExp(triggerWord);
19
+ const plural = triggerWord.toLowerCase() === DEFAULT_KEYWORD_TRIGGER_WORD ? "s?" : "";
20
+ return `(?<![/\\p{ID_Continue}$-])(?<!\\\\)${escaped}${plural}(?![/\\p{ID_Continue}$-])(?!\\\\)`;
21
+ }
22
+ function triggerRegex(triggerWord = DEFAULT_KEYWORD_TRIGGER_WORD, flags = "iu", atEnd = false) {
23
+ const word = normalizeKeywordTriggerWord(triggerWord) ?? DEFAULT_KEYWORD_TRIGGER_WORD;
24
+ return new RegExp(`${triggerSource(word)}${atEnd ? "$" : ""}`, flags);
25
+ }
26
+ export function hasTrigger(text, triggerWord = DEFAULT_KEYWORD_TRIGGER_WORD) {
27
+ return triggerRegex(triggerWord).test(text);
28
+ }
29
+ export function endsWithTrigger(textBeforeCursor, triggerWord = DEFAULT_KEYWORD_TRIGGER_WORD) {
30
+ return triggerRegex(triggerWord, "iu", true).test(textBeforeCursor);
31
+ }
32
+ /**
33
+ * Appended to the effort-path directive: standing `/effort` arms on every
34
+ * substantive message, so the model must be told it can decline the workflow on
35
+ * a conversational or trivial turn (mirrors "solo only on conversational turns").
36
+ */
37
+ export const EFFORT_CONVERSATIONAL_ESCAPE = "This turn was armed by standing effort mode, not by an explicit workflow request: if it is conversational or trivial, skip the workflow and just respond directly.";
38
+ /** The one-line, truthful "why armed" clause for each heuristic arming path. */
39
+ function armReasonClause(reason) {
40
+ return reason === "keyword"
41
+ ? "you typed the workflow trigger word, which counts as an explicit opt-in to multi-agent orchestration"
42
+ : "standing effort mode armed this turn (you did not explicitly ask for a workflow)";
43
+ }
44
+ /**
45
+ * The #89 reassurance shared by every arming banner: a background run ENDING the
46
+ * turn is expected, not a stall — the result auto-delivers back — so the model
47
+ * shouldn't feel it must stay and block, nor avoid the tool to stay interactive.
48
+ * Names when `background:false` is the right call (user waiting inline).
49
+ */
50
+ const BACKGROUND_DELIVERY_REASSURANCE = "If you do call `workflow`, it runs in the background by default: this turn will end and the result is delivered back into the conversation automatically when it finishes — that's expected, not a stall, so you do not need to stay and block. Only pass background:false if the user is waiting for the result inline in this same turn.";
51
+ /**
52
+ * The directive appended to a submitted message when workflows mode is ARMED by a
53
+ * HEURISTIC path — the keyword trigger or standing `/effort`. (The explicit
54
+ * `/workflows run` command uses {@link buildForcedWorkflowPrompt} instead.)
55
+ *
56
+ * This authorizes — it does not force. Arming is a confirmed opt-in signal that
57
+ * lifts the always-on "do not call the tool" gate for THIS message; the model
58
+ * still decides whether the message is actually a request to do work (→ call the
59
+ * `workflow` tool) or just talk about workflows (→ answer directly). The old
60
+ * "You MUST / the ONLY acceptable action / Do NOT answer directly" forcing text
61
+ * caused two bugs: it over-triggered on messages that merely mention workflows
62
+ * (#88), and — by commanding the model to emit nothing but one `workflow` call
63
+ * and not talk — it produced a bare background run that ends the turn and leaves
64
+ * the user at an idle prompt (#89).
65
+ *
66
+ * The banner therefore (1) LEADS with the decision boundary (question/trivial →
67
+ * answer directly; a real decomposable request → call `workflow`) rather than
68
+ * leading with "call the tool"; (2) states the truthful opt-in `reason` for THIS
69
+ * path (no "the word you typed" on the effort path, where none was); and (3)
70
+ * carries the #89 background/deliver-back reassurance so an ending turn reads as
71
+ * expected. The how-to mechanics are NOT here — they live in the tool's static
72
+ * `description` (see createWorkflowTool), visible whenever the model looks at the
73
+ * tool, so they aren't re-injected per armed turn (#65).
74
+ *
75
+ * `extraDirective` (e.g. an effort-tier nudge + EFFORT_CONVERSATIONAL_ESCAPE) is
76
+ * appended when present.
77
+ */
78
+ export function buildArmedWorkflowPrompt(text, opts = {}) {
79
+ const reason = opts.reason ?? "keyword";
80
+ const lines = [
81
+ text,
82
+ "",
83
+ "---",
84
+ "[workflows mode armed. Decide first: if this message is a question, a trivial task, or",
85
+ "just talk (about workflows, this repo, or the tool itself), answer it directly and stay",
86
+ "conversational — arming authorizes the tool, it does not force it. If it is a real,",
87
+ "decomposable request to do work, handle it by calling the `workflow` tool: write a script",
88
+ "that fans the task out across subagents via agent()/parallel()/pipeline().",
89
+ `Why this turn is armed: ${armReasonClause(reason)}.`,
90
+ BACKGROUND_DELIVERY_REASSURANCE + "]",
91
+ ];
92
+ if (opts.extraDirective)
93
+ lines.push("", opts.extraDirective);
94
+ return lines.join("\n");
95
+ }
96
+ /**
97
+ * The directive for the explicit `/workflows run <prompt>` command. Unlike the
98
+ * heuristic {@link buildArmedWorkflowPrompt}, `/workflows run` is a maximal-intent
99
+ * command — the user typed a command whose whole purpose is to execute a workflow
100
+ * now — so it does NOT get the "if it's a question, just answer" escape. It still
101
+ * avoids the old MUST/ONLY forcing language (which caused #88/#89) and still
102
+ * carries the #89 background/deliver-back reassurance so an ending turn reads as
103
+ * expected. `extraDirective` (e.g. a standing effort-tier nudge) is appended.
104
+ */
105
+ export function buildForcedWorkflowPrompt(text, extraDirective) {
106
+ const lines = [
107
+ text,
108
+ "",
109
+ "---",
110
+ "[/workflows run — you ran an explicit command to execute a workflow for this request.",
111
+ "Call the `workflow` tool now: write a script that fans this task out across subagents",
112
+ "via agent()/parallel()/pipeline(). (This is a direct command, not a heuristic guess, so",
113
+ "do not answer in prose instead of running the workflow.)",
114
+ BACKGROUND_DELIVERY_REASSURANCE + "]",
115
+ ];
116
+ if (extraDirective)
117
+ lines.push("", extraDirective);
118
+ return lines.join("\n");
119
+ }
120
+ /** The exact name of the workflow tool that workflows mode forces. */
121
+ export const WORKFLOW_TOOL_NAME = "workflow";
122
+ export function registerWorkflowTriggerCommand(pi, state, settingsStore = DEFAULT_SETTINGS_STORE) {
123
+ pi.registerCommand?.("workflows-trigger", {
124
+ description: "Keyword workflow trigger: on | off | set <word> | reset | status",
125
+ async handler(args, _ctx) {
126
+ const raw = args.trim();
127
+ const [command = "status", ...rest] = raw.split(/\s+/);
128
+ const arg = command.toLowerCase();
129
+ const say = (content) => pi.sendMessage({ customType: "workflows-trigger", content, display: true });
130
+ if (arg === "on") {
131
+ state.keywordTriggerEnabled = true;
132
+ state.suppressedKeywordText = undefined;
133
+ const saved = persistWorkflowTriggerSettings(settingsStore, { keywordTriggerEnabled: true });
134
+ await say(saved
135
+ ? `Workflows keyword trigger on — mentioning ${triggerDisplayName(state.keywordTriggerWord)} in an interactive message will auto-arm workflows mode. Saved for new sessions.`
136
+ : "Workflows keyword trigger on for this session, but the preference could not be saved.");
137
+ return;
138
+ }
139
+ if (arg === "off") {
140
+ state.keywordTriggerEnabled = false;
141
+ state.active = false;
142
+ state.suppressedKeywordText = undefined;
143
+ const saved = persistWorkflowTriggerSettings(settingsStore, { keywordTriggerEnabled: false });
144
+ await say(saved
145
+ ? `Workflows keyword trigger off — messages can mention ${triggerDisplayName(state.keywordTriggerWord)} without forcing the workflow tool. Saved for new sessions. Use /workflows-trigger on to restore.`
146
+ : "Workflows keyword trigger off for this session, but the preference could not be saved. Use /workflows-trigger on to restore.");
147
+ return;
148
+ }
149
+ if (arg === "set") {
150
+ const requested = rest.join(" ");
151
+ const keywordTriggerWord = normalizeKeywordTriggerWord(requested);
152
+ if (!keywordTriggerWord) {
153
+ await say('Invalid trigger word. Use a non-empty term with no spaces and no leading "/", e.g. /workflows-trigger set pi-workflow');
154
+ return;
155
+ }
156
+ state.keywordTriggerWord = keywordTriggerWord;
157
+ state.suppressedKeywordText = undefined;
158
+ const saved = persistWorkflowTriggerSettings(settingsStore, { keywordTriggerWord });
159
+ await say(saved
160
+ ? `Workflows keyword trigger word set to "${keywordTriggerWord}". Saved for new sessions.`
161
+ : `Workflows keyword trigger word set to "${keywordTriggerWord}" for this session, but the preference could not be saved.`);
162
+ return;
163
+ }
164
+ if (arg === "reset") {
165
+ state.keywordTriggerWord = DEFAULT_KEYWORD_TRIGGER_WORD;
166
+ state.suppressedKeywordText = undefined;
167
+ const saved = persistWorkflowTriggerSettings(settingsStore, {
168
+ keywordTriggerWord: DEFAULT_KEYWORD_TRIGGER_WORD,
169
+ });
170
+ await say(saved
171
+ ? 'Workflows keyword trigger word reset to "workflow" (also matches "workflows"). Saved for new sessions.'
172
+ : 'Workflows keyword trigger word reset to "workflow" for this session, but the preference could not be saved.');
173
+ return;
174
+ }
175
+ const keywordTriggerWord = resolvedTriggerWord(state.keywordTriggerWord);
176
+ await say(`Workflows keyword trigger is ${state.keywordTriggerEnabled ? "on" : "off"}; trigger word is "${keywordTriggerWord}". Changes are saved for new sessions. Usage: /workflows-trigger on | off | set <word> | reset | status`);
177
+ },
178
+ });
179
+ }
180
+ /**
181
+ * Register the bottom progress-panel preference command:
182
+ * - `/workflows-progress compact|detailed|status` — switch (or report) the panel mode.
183
+ * - `/workflows-progress max <1-1000>` — cap agents shown per phase in detailed mode.
184
+ * Both persist via `settingsStore` and take effect on the next live run (the panel
185
+ * live-reads its settings), so no session restart is needed.
186
+ */
187
+ export function registerWorkflowProgressCommands(pi, settingsStore = DEFAULT_SETTINGS_STORE) {
188
+ pi.registerCommand?.("workflows-progress", {
189
+ description: "Bottom progress panel: compact | detailed | status | max <N>",
190
+ async handler(args, _ctx) {
191
+ const trimmed = args.trim();
192
+ const say = (content) => pi.sendMessage({ customType: "workflows-progress", content, display: true });
193
+ const spaceIdx = trimmed.indexOf(" ");
194
+ const verb = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toLowerCase();
195
+ const rest = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1).trim();
196
+ if (verb === "compact" || verb === "detailed") {
197
+ const saved = persistProgressSettings(settingsStore, { progressPanelMode: verb });
198
+ await say(saved
199
+ ? `Workflow progress panel set to ${verb} — takes effect on the next render of a live run (no restart needed).`
200
+ : `Workflow progress panel set to ${verb} for this session, but the preference could not be saved.`);
201
+ return;
202
+ }
203
+ if (verb === "max") {
204
+ if (!rest) {
205
+ await say(`Detailed progress shows up to ${loadProgressMaxAgents(settingsStore)} agents per phase. Usage: /workflows-progress max <1-1000>`);
206
+ return;
207
+ }
208
+ const n = Number.parseInt(rest, 10);
209
+ if (!Number.isFinite(n) || n < 1) {
210
+ await say(`Invalid value "${rest}". Usage: /workflows-progress max <1-1000> (a whole number ≥ 1).`);
211
+ return;
212
+ }
213
+ const clamped = Math.min(1000, n);
214
+ const saved = persistProgressSettings(settingsStore, { progressPanelMaxAgents: clamped });
215
+ await say(saved
216
+ ? `Detailed progress now shows up to ${clamped} agents per phase.`
217
+ : `Set to ${clamped} for this session, but the preference could not be saved.`);
218
+ return;
219
+ }
220
+ await say(`Workflow progress panel is ${loadProgressMode(settingsStore)}, showing up to ${loadProgressMaxAgents(settingsStore)} agents per phase. Usage: /workflows-progress compact | detailed | status | max <N>`);
221
+ },
222
+ });
223
+ }
224
+ /**
225
+ * Install the keyword-trigger arming hook (submit-time detection + prompt
226
+ * rewrite) and the related trigger/progress commands. Call once (e.g. in
227
+ * `session_start`).
228
+ */
229
+ export function installWorkflowKeywordArming(pi, effort, options = {}) {
230
+ const settingsStore = options.settingsStore ?? DEFAULT_SETTINGS_STORE;
231
+ const initialSettings = loadInitialWorkflowSettings(settingsStore);
232
+ const state = {
233
+ active: false,
234
+ keywordTriggerEnabled: initialSettings.keywordTriggerEnabled ?? true,
235
+ keywordTriggerWord: initialSettings.keywordTriggerWord ?? DEFAULT_KEYWORD_TRIGGER_WORD,
236
+ };
237
+ registerWorkflowTriggerCommand(pi, state, settingsStore);
238
+ registerWorkflowProgressCommands(pi, settingsStore);
239
+ // Active tools saved while a turn is restricted to `workflow`; restored on turn_end.
240
+ let savedTools;
241
+ // When armed at submit time, rewrite the user's message to force a workflow AND
242
+ // ensure the `workflow` tool is in the active tool set, so the model can call it.
243
+ // We keep all existing tools (bash, read, edit, write, web_search, etc.) because
244
+ // the model often needs them BEFORE writing the workflow script (e.g. exploring
245
+ // the codebase, reading files, searching for context). This only ADDS the
246
+ // workflow tool to the active set; no tools are removed (the original set is
247
+ // saved in `savedTools` and restored elsewhere).
248
+ //
249
+ // NOTE: we check event.text directly (hasTrigger) rather than state.active from
250
+ // the editor, because the editor's state is reset synchronously by submitValue()
251
+ // BEFORE the input event fires (the actual prompt processing is async).
252
+ pi.on("input", (event) => {
253
+ if (event.source !== "interactive" || !event.text)
254
+ return { action: "continue" };
255
+ // Arm either when the user typed the "workflow(s)" trigger, or when standing
256
+ // effort mode is on and the message is a substantive request.
257
+ const normalizedText = event.text.trim();
258
+ const suppressed = state.suppressedKeywordText === normalizedText;
259
+ if (suppressed)
260
+ state.suppressedKeywordText = undefined;
261
+ const triggered = state.keywordTriggerEnabled && !suppressed && hasTrigger(event.text, state.keywordTriggerWord);
262
+ const byEffort = !triggered && !!effort && effort.level !== "off" && isSubstantive(event.text);
263
+ if (!triggered && !byEffort)
264
+ return { action: "continue" };
265
+ try {
266
+ if (savedTools === undefined) {
267
+ savedTools = pi.getActiveTools?.() ?? [];
268
+ const current = [...savedTools];
269
+ if (!current.includes(WORKFLOW_TOOL_NAME)) {
270
+ current.push(WORKFLOW_TOOL_NAME);
271
+ }
272
+ pi.setActiveTools?.(current);
273
+ }
274
+ }
275
+ catch {
276
+ // Tool restriction is best-effort; the armed directive still authorizes the workflow.
277
+ }
278
+ // Effort path: the trigger word was NOT typed — this arms on ANY substantive
279
+ // message while standing effort is on. So the directive must (a) state the
280
+ // truthful "effort" reason (not "the word you typed"), and (b) let the model
281
+ // skip the workflow entirely on conversational/trivial turns, not just on
282
+ // questions about workflows.
283
+ const extra = byEffort && effort
284
+ ? [effortDirective(effort.level), EFFORT_CONVERSATIONAL_ESCAPE].filter(Boolean).join(" ")
285
+ : undefined;
286
+ const reason = byEffort ? "effort" : "keyword";
287
+ return {
288
+ action: "transform",
289
+ text: buildArmedWorkflowPrompt(event.text, { reason, extraDirective: extra }),
290
+ };
291
+ });
292
+ // Restore the user's full tool set once the forced turn completes.
293
+ pi.on("turn_end", () => {
294
+ if (savedTools === undefined)
295
+ return;
296
+ const restore = savedTools;
297
+ savedTools = undefined;
298
+ try {
299
+ pi.setActiveTools?.(restore);
300
+ }
301
+ catch {
302
+ // ignore — nothing we can do if the host rejects the restore
303
+ }
304
+ });
305
+ return state;
306
+ }
307
+ const DEFAULT_SETTINGS_STORE = {
308
+ load: loadWorkflowSettings,
309
+ save: saveWorkflowSettings,
310
+ };
311
+ function loadInitialWorkflowSettings(settingsStore) {
312
+ try {
313
+ const settings = settingsStore.load();
314
+ return {
315
+ keywordTriggerEnabled: settings.keywordTriggerEnabled,
316
+ keywordTriggerWord: normalizeKeywordTriggerWord(settings.keywordTriggerWord) ?? DEFAULT_KEYWORD_TRIGGER_WORD,
317
+ };
318
+ }
319
+ catch {
320
+ return { keywordTriggerEnabled: true, keywordTriggerWord: DEFAULT_KEYWORD_TRIGGER_WORD };
321
+ }
322
+ }
323
+ function persistWorkflowTriggerSettings(settingsStore, settings) {
324
+ try {
325
+ settingsStore.save(settings);
326
+ return true;
327
+ }
328
+ catch {
329
+ return false;
330
+ }
331
+ }
332
+ function resolvedTriggerWord(keywordTriggerWord) {
333
+ return normalizeKeywordTriggerWord(keywordTriggerWord) ?? DEFAULT_KEYWORD_TRIGGER_WORD;
334
+ }
335
+ function triggerDisplayName(keywordTriggerWord) {
336
+ const word = resolvedTriggerWord(keywordTriggerWord);
337
+ return word.toLowerCase() === DEFAULT_KEYWORD_TRIGGER_WORD ? "workflow/workflows" : `"${word}"`;
338
+ }
339
+ function persistProgressSettings(settingsStore, settings) {
340
+ try {
341
+ settingsStore.save(settings);
342
+ return true;
343
+ }
344
+ catch {
345
+ return false;
346
+ }
347
+ }
348
+ function loadProgressMode(settingsStore) {
349
+ try {
350
+ return settingsStore.load().progressPanelMode ?? "compact";
351
+ }
352
+ catch {
353
+ return "compact";
354
+ }
355
+ }
356
+ function loadProgressMaxAgents(settingsStore) {
357
+ try {
358
+ return settingsStore.load().progressPanelMaxAgents ?? 8;
359
+ }
360
+ catch {
361
+ return 8;
362
+ }
363
+ }