@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.
- package/LICENSE +22 -0
- package/README.md +325 -0
- package/assets/readme/hero.png +0 -0
- package/assets/readme/hero.svg +70 -0
- package/assets/readme/package-cover.png +0 -0
- package/assets/readme/package-cover.svg +66 -0
- package/assets/readme/workflow.png +0 -0
- package/assets/readme/workflow.svg +77 -0
- package/dist/accept-workflow-guidance.d.ts +14 -0
- package/dist/accept-workflow-guidance.js +53 -0
- package/dist/adversarial-review.d.ts +30 -0
- package/dist/adversarial-review.js +107 -0
- package/dist/agent-history.d.ts +20 -0
- package/dist/agent-history.js +122 -0
- package/dist/agent-registry.d.ts +83 -0
- package/dist/agent-registry.js +190 -0
- package/dist/agent.d.ts +364 -0
- package/dist/agent.js +714 -0
- package/dist/builtin-commands.d.ts +19 -0
- package/dist/builtin-commands.js +251 -0
- package/dist/builtin-workflows.d.ts +45 -0
- package/dist/builtin-workflows.js +121 -0
- package/dist/code-review.d.ts +26 -0
- package/dist/code-review.js +181 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +44 -0
- package/dist/deep-research.d.ts +30 -0
- package/dist/deep-research.js +124 -0
- package/dist/display.d.ts +134 -0
- package/dist/display.js +248 -0
- package/dist/effort-command.d.ts +28 -0
- package/dist/effort-command.js +68 -0
- package/dist/enums.d.ts +69 -0
- package/dist/enums.js +78 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.js +140 -0
- package/dist/extension-reload.d.ts +37 -0
- package/dist/extension-reload.js +78 -0
- package/dist/fs-persistence.d.ts +63 -0
- package/dist/fs-persistence.js +102 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +35 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +66 -0
- package/dist/model-routing.d.ts +30 -0
- package/dist/model-routing.js +50 -0
- package/dist/model-spec.d.ts +29 -0
- package/dist/model-spec.js +252 -0
- package/dist/model-tier-config.d.ts +133 -0
- package/dist/model-tier-config.js +249 -0
- package/dist/run-persistence.d.ts +180 -0
- package/dist/run-persistence.js +294 -0
- package/dist/saved-commands.d.ts +28 -0
- package/dist/saved-commands.js +100 -0
- package/dist/shared-store.d.ts +98 -0
- package/dist/shared-store.js +212 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/task-panel.d.ts +61 -0
- package/dist/task-panel.js +422 -0
- package/dist/usage-limit-scheduler.d.ts +145 -0
- package/dist/usage-limit-scheduler.js +368 -0
- package/dist/web-tools.d.ts +20 -0
- package/dist/web-tools.js +120 -0
- package/dist/workflow-authoring-coverage.d.ts +70 -0
- package/dist/workflow-authoring-coverage.js +421 -0
- package/dist/workflow-authoring-reference.d.ts +20 -0
- package/dist/workflow-authoring-reference.js +156 -0
- package/dist/workflow-capability-contract.d.ts +131 -0
- package/dist/workflow-capability-contract.js +604 -0
- package/dist/workflow-commands.d.ts +18 -0
- package/dist/workflow-commands.js +260 -0
- package/dist/workflow-comprehension.d.ts +133 -0
- package/dist/workflow-comprehension.js +1321 -0
- package/dist/workflow-context-measurement.d.ts +72 -0
- package/dist/workflow-context-measurement.js +213 -0
- package/dist/workflow-control-tool.d.ts +30 -0
- package/dist/workflow-control-tool.js +176 -0
- package/dist/workflow-delivery-choice.d.ts +20 -0
- package/dist/workflow-delivery-choice.js +48 -0
- package/dist/workflow-editor.d.ts +93 -0
- package/dist/workflow-editor.js +363 -0
- package/dist/workflow-manager.d.ts +492 -0
- package/dist/workflow-manager.js +1124 -0
- package/dist/workflow-paths.d.ts +22 -0
- package/dist/workflow-paths.js +46 -0
- package/dist/workflow-release-gate.d.ts +39 -0
- package/dist/workflow-release-gate.js +309 -0
- package/dist/workflow-saved.d.ts +38 -0
- package/dist/workflow-saved.js +126 -0
- package/dist/workflow-settings.d.ts +70 -0
- package/dist/workflow-settings.js +131 -0
- package/dist/workflow-tool.d.ts +71 -0
- package/dist/workflow-tool.js +367 -0
- package/dist/workflow-ui.d.ts +182 -0
- package/dist/workflow-ui.js +1587 -0
- package/dist/workflow.d.ts +333 -0
- package/dist/workflow.js +1151 -0
- package/dist/workflows-models-command.d.ts +31 -0
- package/dist/workflows-models-command.js +156 -0
- package/dist/worktree.d.ts +25 -0
- package/dist/worktree.js +61 -0
- package/extensions/workflow.ts +151 -0
- package/package.json +104 -0
- package/skills/workflow-authoring/SKILL.md +30 -0
- package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
- package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
- package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
- package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
- package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
- package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
- package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
- package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
- package/skills/workflow-authoring/examples/structured-output.js +37 -0
- package/skills/workflow-authoring/examples/tournament.js +81 -0
- package/skills/workflow-authoring/examples/validated-gate.js +63 -0
- package/skills/workflow-authoring/references/capabilities.md +41 -0
- package/skills/workflow-authoring/references/capability-details.md +357 -0
- package/skills/workflow-authoring/references/common-helpers.md +4 -0
- package/skills/workflow-authoring/references/debugging.md +27 -0
- package/skills/workflow-authoring/references/focused-recipes.md +11 -0
- package/skills/workflow-authoring/references/helpers.md +9 -0
- package/skills/workflow-authoring/references/lifecycle.md +33 -0
- package/skills/workflow-authoring/references/pattern-selection.md +15 -0
- package/skills/workflow-authoring/references/quality-helpers.md +8 -0
- package/skills/workflow-authoring/references/registry-ownership.md +15 -0
- package/skills/workflow-authoring/references/retry-helper.md +5 -0
- package/skills/workflow-authoring/references/review.md +43 -0
- package/skills/workflow-authoring/references/runtime.md +27 -0
- package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
- package/skills/workflow-authoring/references/versions.md +13 -0
- package/skills/workflow-patterns/SKILL.md +51 -0
- package/src/accept-workflow-guidance.ts +71 -0
- package/src/adversarial-review.ts +120 -0
- package/src/agent-history.ts +157 -0
- package/src/agent-registry.ts +221 -0
- package/src/agent.ts +929 -0
- package/src/builtin-commands.ts +286 -0
- package/src/builtin-workflows.ts +155 -0
- package/src/code-review.ts +183 -0
- package/src/config.ts +55 -0
- package/src/deep-research.ts +135 -0
- package/src/display.ts +367 -0
- package/src/effort-command.ts +87 -0
- package/src/enums.ts +77 -0
- package/src/errors.ts +199 -0
- package/src/extension-reload.ts +100 -0
- package/src/fs-persistence.ts +124 -0
- package/src/index.ts +176 -0
- package/src/logger.ts +88 -0
- package/src/model-routing.ts +73 -0
- package/src/model-spec.ts +309 -0
- package/src/model-tier-config.ts +296 -0
- package/src/run-persistence.ts +484 -0
- package/src/saved-commands.ts +115 -0
- package/src/shared-store.ts +228 -0
- package/src/structured-output.ts +47 -0
- package/src/task-panel.ts +490 -0
- package/src/usage-limit-scheduler.ts +432 -0
- package/src/web-tools.ts +124 -0
- package/src/workflow-authoring-coverage.ts +486 -0
- package/src/workflow-authoring-reference.ts +186 -0
- package/src/workflow-capability-contract.ts +806 -0
- package/src/workflow-commands.ts +287 -0
- package/src/workflow-comprehension.ts +1673 -0
- package/src/workflow-context-measurement.ts +262 -0
- package/src/workflow-control-tool.ts +238 -0
- package/src/workflow-delivery-choice.ts +69 -0
- package/src/workflow-editor.ts +444 -0
- package/src/workflow-manager.ts +1405 -0
- package/src/workflow-paths.ts +63 -0
- package/src/workflow-release-gate.ts +529 -0
- package/src/workflow-saved.ts +180 -0
- package/src/workflow-settings.ts +194 -0
- package/src/workflow-tool.ts +464 -0
- package/src/workflow-ui.ts +1789 -0
- package/src/workflow.ts +1615 -0
- package/src/workflows-models-command.ts +211 -0
- 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
|
+
}
|