@agimon-ai/po-workflow 0.0.0-alpha.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 +29 -0
- package/LICENSE.web +29 -0
- package/README.md +146 -0
- package/dist/apiContracts.cjs +361 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +356 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/extensions/server.cjs +4289 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +4259 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +3788 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +4 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/workflowLaunchCommand-C69Nn3k5.cjs +139 -0
- package/dist/workflowLaunchCommand-C69Nn3k5.cjs.map +1 -0
- package/dist/workflowLaunchCommand-GGqimmTC.mjs +122 -0
- package/dist/workflowLaunchCommand-GGqimmTC.mjs.map +1 -0
- package/llms.txt +24 -0
- package/package.json +133 -0
- package/skills/workflow-recovery/SKILL.md +85 -0
- package/src/prompts/po-author-workflow/SKILL.md +64 -0
- package/src/prompts/po-use-workflow/SKILL.md +21 -0
- package/src/types/webWorkflowTerminal.ts +114 -0
- package/src/types/webWorkflows.ts +208 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_workflowLaunchCommand = require("./workflowLaunchCommand-C69Nn3k5.cjs");
|
|
3
|
+
exports.parseWorkflowLaunchCommand = require_workflowLaunchCommand.parseWorkflowLaunchCommand;
|
|
4
|
+
exports.validateWorkflowLaunch = require_workflowLaunchCommand.validateWorkflowLaunch;
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/services/workflowLaunchCommand/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `/workflow-launch` line, parsed.
|
|
4
|
+
*
|
|
5
|
+
* WHY A VERB AT ALL:
|
|
6
|
+
* A browser can only send a session a prompt frame, so every cockpit action
|
|
7
|
+
* that has to run inside the session travels as a slash line, the way the
|
|
8
|
+
* agent catalog sends `/run`. The workflow tools answer to the agent, not to a
|
|
9
|
+
* page, and the leader board answers to a keyboard, so neither of them can be
|
|
10
|
+
* what the launch dialog sends.
|
|
11
|
+
*
|
|
12
|
+
* THE SHAPE:
|
|
13
|
+
* /workflow-launch <workflow> [key=value ...] [prompt words]
|
|
14
|
+
*
|
|
15
|
+
* Key-value pairs come first and stop at the first token that is not one, so
|
|
16
|
+
* everything after them is the prompt exactly as it was typed, including any
|
|
17
|
+
* `=` inside it. `runner`, `command` and `choice` are reserved keys; the rest
|
|
18
|
+
* are the workflow's own `workflow_dispatch` inputs. Values may be double
|
|
19
|
+
* quoted to hold spaces.
|
|
20
|
+
*/
|
|
21
|
+
interface ParsedWorkflowLaunch {
|
|
22
|
+
/** The workflow's name or its path, as typed; resolution belongs to the caller. */
|
|
23
|
+
workflow: string;
|
|
24
|
+
runner?: string;
|
|
25
|
+
command?: string;
|
|
26
|
+
choice?: string;
|
|
27
|
+
inputs: Record<string, string>;
|
|
28
|
+
prompt?: string;
|
|
29
|
+
}
|
|
30
|
+
interface WorkflowLaunchParseFailure {
|
|
31
|
+
error: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function parseWorkflowLaunchCommand(args: string): ParsedWorkflowLaunch | WorkflowLaunchParseFailure;
|
|
34
|
+
/** What a workflow declares, as much of it as a launch has to satisfy. */
|
|
35
|
+
interface LaunchRequirements {
|
|
36
|
+
triggers: readonly string[];
|
|
37
|
+
inputs: readonly {
|
|
38
|
+
name: string;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
options?: readonly string[];
|
|
41
|
+
}[];
|
|
42
|
+
/** Absent means the workflow names no runner map, so any runner will do. */
|
|
43
|
+
runners?: readonly string[];
|
|
44
|
+
/** Commands every templated step offers; absent when no step is templated. */
|
|
45
|
+
commands?: readonly string[];
|
|
46
|
+
/** Declared model choices. */
|
|
47
|
+
choices?: readonly string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Everything wrong with a launch, before it is started.
|
|
51
|
+
*
|
|
52
|
+
* Checked here rather than left to the engine because the failure would
|
|
53
|
+
* otherwise be a run that starts and then waits forever for terminal input, or
|
|
54
|
+
* one that dies on a runner its steps never declared. Both read as the launch
|
|
55
|
+
* having worked.
|
|
56
|
+
*/
|
|
57
|
+
export declare function validateWorkflowLaunch(requirements: LaunchRequirements, parsed: Pick<ParsedWorkflowLaunch, 'inputs' | 'prompt' | 'runner' | 'command' | 'choice'>): string[];
|
|
58
|
+
//#endregion
|
|
59
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/services/workflowLaunchCommand/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;UA4BiB;;EAEf;EACA;EACA;EACA;EACA,QAAQ;EACR;;UAGe;EACf;;wBAsCc,2BAA2B,eAAe,uBAAuB;;UAmEhE;EACf;EACA;IAAmB;IAAc;IAAoB;;;EAErD;;EAEA;;EAEA;;;;;;;;;;wBAcc,uBACd,cAAc,oBACd,QAAQ,KAAK"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//#region src/services/workflowLaunchCommand/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `/workflow-launch` line, parsed.
|
|
4
|
+
*
|
|
5
|
+
* WHY A VERB AT ALL:
|
|
6
|
+
* A browser can only send a session a prompt frame, so every cockpit action
|
|
7
|
+
* that has to run inside the session travels as a slash line, the way the
|
|
8
|
+
* agent catalog sends `/run`. The workflow tools answer to the agent, not to a
|
|
9
|
+
* page, and the leader board answers to a keyboard, so neither of them can be
|
|
10
|
+
* what the launch dialog sends.
|
|
11
|
+
*
|
|
12
|
+
* THE SHAPE:
|
|
13
|
+
* /workflow-launch <workflow> [key=value ...] [prompt words]
|
|
14
|
+
*
|
|
15
|
+
* Key-value pairs come first and stop at the first token that is not one, so
|
|
16
|
+
* everything after them is the prompt exactly as it was typed, including any
|
|
17
|
+
* `=` inside it. `runner`, `command` and `choice` are reserved keys; the rest
|
|
18
|
+
* are the workflow's own `workflow_dispatch` inputs. Values may be double
|
|
19
|
+
* quoted to hold spaces.
|
|
20
|
+
*/
|
|
21
|
+
/** Reserved key: which runner map entry the run executes with. */
|
|
22
|
+
const RUNNER_KEY = "runner";
|
|
23
|
+
/** Reserved key: which command templated steps run through. */
|
|
24
|
+
const COMMAND_KEY = "command";
|
|
25
|
+
/** Reserved key: which model choice steps without their own run with. */
|
|
26
|
+
const CHOICE_KEY = "choice";
|
|
27
|
+
const KEY_VALUE = /^([A-Za-z_][\w.-]*)=(.*)$/s;
|
|
28
|
+
/** Strips one layer of surrounding double quotes, which is how a value holds spaces. */
|
|
29
|
+
function unquote(value) {
|
|
30
|
+
return value.length > 1 && value.startsWith("\"") && value.endsWith("\"") ? value.slice(1, -1) : value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Splits on whitespace, keeping double-quoted runs together.
|
|
34
|
+
*
|
|
35
|
+
* Returns each token with the offset it started at, so the caller can take the
|
|
36
|
+
* rest of the line verbatim rather than rebuilding it from tokens and losing
|
|
37
|
+
* the spacing a prompt was written with.
|
|
38
|
+
*/
|
|
39
|
+
function tokenize(line) {
|
|
40
|
+
const tokens = [];
|
|
41
|
+
let index = 0;
|
|
42
|
+
while (index < line.length) {
|
|
43
|
+
while (index < line.length && /\s/.test(line[index])) index += 1;
|
|
44
|
+
if (index >= line.length) break;
|
|
45
|
+
const start = index;
|
|
46
|
+
let quoted = false;
|
|
47
|
+
while (index < line.length && (quoted || !/\s/.test(line[index]))) {
|
|
48
|
+
if (line[index] === "\"") quoted = !quoted;
|
|
49
|
+
index += 1;
|
|
50
|
+
}
|
|
51
|
+
tokens.push({
|
|
52
|
+
text: line.slice(start, index),
|
|
53
|
+
start
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return tokens;
|
|
57
|
+
}
|
|
58
|
+
function parseWorkflowLaunchCommand(args) {
|
|
59
|
+
const tokens = tokenize(args.trim());
|
|
60
|
+
const first = tokens[0];
|
|
61
|
+
if (first === void 0) return { error: "Usage: /workflow-launch <workflow> [key=value …] [prompt]" };
|
|
62
|
+
const inputs = {};
|
|
63
|
+
const reserved = {};
|
|
64
|
+
let index = 1;
|
|
65
|
+
for (; index < tokens.length; index += 1) {
|
|
66
|
+
const token = tokens[index];
|
|
67
|
+
const match = KEY_VALUE.exec(token.text);
|
|
68
|
+
if (!match) break;
|
|
69
|
+
const key = match[1];
|
|
70
|
+
const value = unquote(match[2]);
|
|
71
|
+
if (key === RUNNER_KEY || key === COMMAND_KEY || key === CHOICE_KEY) reserved[key] = value;
|
|
72
|
+
else inputs[key] = value;
|
|
73
|
+
}
|
|
74
|
+
const rest = tokens[index];
|
|
75
|
+
const prompt = rest === void 0 ? void 0 : args.trim().slice(rest.start).trim();
|
|
76
|
+
return {
|
|
77
|
+
workflow: unquote(first.text),
|
|
78
|
+
...reserved,
|
|
79
|
+
inputs,
|
|
80
|
+
...prompt === void 0 || prompt === "" ? {} : { prompt }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The workflow a token names: its name, its path in the repository, or the
|
|
85
|
+
* tail of that path. Case-insensitive on the name, because the catalog shows
|
|
86
|
+
* names as their author capitalised them and nobody types that back exactly.
|
|
87
|
+
*/
|
|
88
|
+
function resolveWorkflowEntry(entries, token) {
|
|
89
|
+
const needle = token.trim();
|
|
90
|
+
const lower = needle.toLocaleLowerCase();
|
|
91
|
+
return entries.find((entry) => entry.name.toLocaleLowerCase() === lower) ?? entries.find((entry) => entry.relativePath === needle || entry.path === needle) ?? entries.find((entry) => entry.relativePath.endsWith(`/${needle}`) || entry.path.endsWith(`/${needle}`));
|
|
92
|
+
}
|
|
93
|
+
/** The trigger that makes a prompt mandatory: the run waits for one otherwise. */
|
|
94
|
+
const USER_PROMPT_TRIGGER = "user_prompt";
|
|
95
|
+
/**
|
|
96
|
+
* Everything wrong with a launch, before it is started.
|
|
97
|
+
*
|
|
98
|
+
* Checked here rather than left to the engine because the failure would
|
|
99
|
+
* otherwise be a run that starts and then waits forever for terminal input, or
|
|
100
|
+
* one that dies on a runner its steps never declared. Both read as the launch
|
|
101
|
+
* having worked.
|
|
102
|
+
*/
|
|
103
|
+
function validateWorkflowLaunch(requirements, parsed) {
|
|
104
|
+
const problems = [];
|
|
105
|
+
if (requirements.triggers.includes(USER_PROMPT_TRIGGER) && (parsed.prompt ?? "") === "") problems.push("This workflow is triggered by a prompt, so it needs one.");
|
|
106
|
+
const missing = requirements.inputs.filter((input) => input.required === true && (parsed.inputs[input.name] ?? "") === "").map((input) => input.name);
|
|
107
|
+
if (missing.length > 0) problems.push(`Missing required input${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}.`);
|
|
108
|
+
for (const input of requirements.inputs) {
|
|
109
|
+
const value = parsed.inputs[input.name];
|
|
110
|
+
if (value === void 0 || input.options === void 0 || input.options.includes(value)) continue;
|
|
111
|
+
problems.push(`Input ${input.name} must be one of: ${input.options.join(", ")}.`);
|
|
112
|
+
}
|
|
113
|
+
const declared = requirements.runners;
|
|
114
|
+
if (parsed.runner !== void 0 && declared !== void 0 && !declared.includes(parsed.runner)) problems.push(declared.length === 0 ? "This workflow declares no runner its steps agree on." : `Runner ${parsed.runner} is not one this workflow declares: ${declared.join(", ")}.`);
|
|
115
|
+
if (parsed.command !== void 0 && !(requirements.commands ?? []).includes(parsed.command)) problems.push(requirements.commands === void 0 || requirements.commands.length === 0 ? "This workflow has no templated steps to run through a command." : `Command ${parsed.command} is not one every step offers: ${requirements.commands.join(", ")}.`);
|
|
116
|
+
if (parsed.choice !== void 0 && !(requirements.choices ?? []).includes(parsed.choice)) problems.push(`Choice ${parsed.choice} is not declared${requirements.choices?.length ? `: ${requirements.choices.join(", ")}` : ""}.`);
|
|
117
|
+
return problems;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
Object.defineProperty(exports, "parseWorkflowLaunchCommand", {
|
|
121
|
+
enumerable: true,
|
|
122
|
+
get: function() {
|
|
123
|
+
return parseWorkflowLaunchCommand;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
Object.defineProperty(exports, "resolveWorkflowEntry", {
|
|
127
|
+
enumerable: true,
|
|
128
|
+
get: function() {
|
|
129
|
+
return resolveWorkflowEntry;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
Object.defineProperty(exports, "validateWorkflowLaunch", {
|
|
133
|
+
enumerable: true,
|
|
134
|
+
get: function() {
|
|
135
|
+
return validateWorkflowLaunch;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=workflowLaunchCommand-C69Nn3k5.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflowLaunchCommand-C69Nn3k5.cjs","names":[],"sources":["../src/services/workflowLaunchCommand/index.ts"],"sourcesContent":["/**\n * The `/workflow-launch` line, parsed.\n *\n * WHY A VERB AT ALL:\n * A browser can only send a session a prompt frame, so every cockpit action\n * that has to run inside the session travels as a slash line, the way the\n * agent catalog sends `/run`. The workflow tools answer to the agent, not to a\n * page, and the leader board answers to a keyboard, so neither of them can be\n * what the launch dialog sends.\n *\n * THE SHAPE:\n * /workflow-launch <workflow> [key=value ...] [prompt words]\n *\n * Key-value pairs come first and stop at the first token that is not one, so\n * everything after them is the prompt exactly as it was typed, including any\n * `=` inside it. `runner`, `command` and `choice` are reserved keys; the rest\n * are the workflow's own `workflow_dispatch` inputs. Values may be double\n * quoted to hold spaces.\n */\n\n/** Reserved key: which runner map entry the run executes with. */\nconst RUNNER_KEY = 'runner';\n/** Reserved key: which command templated steps run through. */\nconst COMMAND_KEY = 'command';\n/** Reserved key: which model choice steps without their own run with. */\nconst CHOICE_KEY = 'choice';\nconst KEY_VALUE = /^([A-Za-z_][\\w.-]*)=(.*)$/s;\n\nexport interface ParsedWorkflowLaunch {\n /** The workflow's name or its path, as typed; resolution belongs to the caller. */\n workflow: string;\n runner?: string;\n command?: string;\n choice?: string;\n inputs: Record<string, string>;\n prompt?: string;\n}\n\nexport interface WorkflowLaunchParseFailure {\n error: string;\n}\n\nexport function isLaunchParseFailure(\n value: ParsedWorkflowLaunch | WorkflowLaunchParseFailure,\n): value is WorkflowLaunchParseFailure {\n return 'error' in value;\n}\n\n/** Strips one layer of surrounding double quotes, which is how a value holds spaces. */\nfunction unquote(value: string): string {\n return value.length > 1 && value.startsWith('\"') && value.endsWith('\"') ? value.slice(1, -1) : value;\n}\n\n/**\n * Splits on whitespace, keeping double-quoted runs together.\n *\n * Returns each token with the offset it started at, so the caller can take the\n * rest of the line verbatim rather than rebuilding it from tokens and losing\n * the spacing a prompt was written with.\n */\nfunction tokenize(line: string): { text: string; start: number }[] {\n const tokens: { text: string; start: number }[] = [];\n let index = 0;\n while (index < line.length) {\n while (index < line.length && /\\s/.test(line[index] as string)) index += 1;\n if (index >= line.length) break;\n const start = index;\n let quoted = false;\n while (index < line.length && (quoted || !/\\s/.test(line[index] as string))) {\n if (line[index] === '\"') quoted = !quoted;\n index += 1;\n }\n tokens.push({ text: line.slice(start, index), start });\n }\n return tokens;\n}\n\nexport function parseWorkflowLaunchCommand(args: string): ParsedWorkflowLaunch | WorkflowLaunchParseFailure {\n const tokens = tokenize(args.trim());\n const first = tokens[0];\n if (first === undefined) return { error: 'Usage: /workflow-launch <workflow> [key=value …] [prompt]' };\n\n const inputs: Record<string, string> = {};\n const reserved: { runner?: string; command?: string; choice?: string } = {};\n let index = 1;\n for (; index < tokens.length; index += 1) {\n const token = tokens[index] as { text: string; start: number };\n const match = KEY_VALUE.exec(token.text);\n if (!match) break;\n const key = match[1] as string;\n const value = unquote(match[2] as string);\n if (key === RUNNER_KEY || key === COMMAND_KEY || key === CHOICE_KEY) reserved[key] = value;\n else inputs[key] = value;\n }\n\n const rest = tokens[index];\n const prompt = rest === undefined ? undefined : args.trim().slice(rest.start).trim();\n return {\n workflow: unquote(first.text),\n ...reserved,\n inputs,\n ...(prompt === undefined || prompt === '' ? {} : { prompt }),\n };\n}\n\n/** The line the cockpit sends, built from what its dialog collected. */\nexport function workflowLaunchCommand(request: ParsedWorkflowLaunch): string {\n const quote = (value: string): string => (/\\s/.test(value) ? `\"${value}\"` : value);\n const pairs = [\n ...(request.runner === undefined ? [] : [`${RUNNER_KEY}=${quote(request.runner)}`]),\n ...(request.command === undefined ? [] : [`${COMMAND_KEY}=${quote(request.command)}`]),\n ...(request.choice === undefined ? [] : [`${CHOICE_KEY}=${quote(request.choice)}`]),\n ...Object.entries(request.inputs).map(([key, value]) => `${key}=${quote(value)}`),\n ];\n const prompt = request.prompt?.trim();\n return ['/workflow-launch', quote(request.workflow), ...pairs, ...(prompt ? [prompt] : [])].join(' ');\n}\n\n/** One catalog row, as much of it as resolution needs. */\nexport interface LaunchResolvableWorkflow {\n name: string;\n path: string;\n relativePath: string;\n}\n\n/**\n * The workflow a token names: its name, its path in the repository, or the\n * tail of that path. Case-insensitive on the name, because the catalog shows\n * names as their author capitalised them and nobody types that back exactly.\n */\nexport function resolveWorkflowEntry<T extends LaunchResolvableWorkflow>(\n entries: readonly T[],\n token: string,\n): T | undefined {\n const needle = token.trim();\n const lower = needle.toLocaleLowerCase();\n return (\n entries.find((entry) => entry.name.toLocaleLowerCase() === lower) ??\n entries.find((entry) => entry.relativePath === needle || entry.path === needle) ??\n entries.find((entry) => entry.relativePath.endsWith(`/${needle}`) || entry.path.endsWith(`/${needle}`))\n );\n}\n\n/** What a workflow declares, as much of it as a launch has to satisfy. */\nexport interface LaunchRequirements {\n triggers: readonly string[];\n inputs: readonly { name: string; required?: boolean; options?: readonly string[] }[];\n /** Absent means the workflow names no runner map, so any runner will do. */\n runners?: readonly string[];\n /** Commands every templated step offers; absent when no step is templated. */\n commands?: readonly string[];\n /** Declared model choices. */\n choices?: readonly string[];\n}\n\n/** The trigger that makes a prompt mandatory: the run waits for one otherwise. */\nconst USER_PROMPT_TRIGGER = 'user_prompt';\n\n/**\n * Everything wrong with a launch, before it is started.\n *\n * Checked here rather than left to the engine because the failure would\n * otherwise be a run that starts and then waits forever for terminal input, or\n * one that dies on a runner its steps never declared. Both read as the launch\n * having worked.\n */\nexport function validateWorkflowLaunch(\n requirements: LaunchRequirements,\n parsed: Pick<ParsedWorkflowLaunch, 'inputs' | 'prompt' | 'runner' | 'command' | 'choice'>,\n): string[] {\n const problems: string[] = [];\n if (requirements.triggers.includes(USER_PROMPT_TRIGGER) && (parsed.prompt ?? '') === '') {\n problems.push('This workflow is triggered by a prompt, so it needs one.');\n }\n const missing = requirements.inputs\n .filter((input) => input.required === true && (parsed.inputs[input.name] ?? '') === '')\n .map((input) => input.name);\n if (missing.length > 0)\n problems.push(`Missing required input${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}.`);\n for (const input of requirements.inputs) {\n const value = parsed.inputs[input.name];\n if (value === undefined || input.options === undefined || input.options.includes(value)) continue;\n problems.push(`Input ${input.name} must be one of: ${input.options.join(', ')}.`);\n }\n const declared = requirements.runners;\n if (parsed.runner !== undefined && declared !== undefined && !declared.includes(parsed.runner)) {\n problems.push(\n declared.length === 0\n ? 'This workflow declares no runner its steps agree on.'\n : `Runner ${parsed.runner} is not one this workflow declares: ${declared.join(', ')}.`,\n );\n }\n if (parsed.command !== undefined && !(requirements.commands ?? []).includes(parsed.command)) {\n problems.push(\n requirements.commands === undefined || requirements.commands.length === 0\n ? 'This workflow has no templated steps to run through a command.'\n : `Command ${parsed.command} is not one every step offers: ${requirements.commands.join(', ')}.`,\n );\n }\n if (parsed.choice !== undefined && !(requirements.choices ?? []).includes(parsed.choice)) {\n problems.push(\n `Choice ${parsed.choice} is not declared${requirements.choices?.length ? `: ${requirements.choices.join(', ')}` : ''}.`,\n );\n }\n return problems;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//#region src/services/workflowLaunchCommand/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `/workflow-launch` line, parsed.
|
|
4
|
+
*
|
|
5
|
+
* WHY A VERB AT ALL:
|
|
6
|
+
* A browser can only send a session a prompt frame, so every cockpit action
|
|
7
|
+
* that has to run inside the session travels as a slash line, the way the
|
|
8
|
+
* agent catalog sends `/run`. The workflow tools answer to the agent, not to a
|
|
9
|
+
* page, and the leader board answers to a keyboard, so neither of them can be
|
|
10
|
+
* what the launch dialog sends.
|
|
11
|
+
*
|
|
12
|
+
* THE SHAPE:
|
|
13
|
+
* /workflow-launch <workflow> [key=value ...] [prompt words]
|
|
14
|
+
*
|
|
15
|
+
* Key-value pairs come first and stop at the first token that is not one, so
|
|
16
|
+
* everything after them is the prompt exactly as it was typed, including any
|
|
17
|
+
* `=` inside it. `runner`, `command` and `choice` are reserved keys; the rest
|
|
18
|
+
* are the workflow's own `workflow_dispatch` inputs. Values may be double
|
|
19
|
+
* quoted to hold spaces.
|
|
20
|
+
*/
|
|
21
|
+
/** Reserved key: which runner map entry the run executes with. */
|
|
22
|
+
const RUNNER_KEY = "runner";
|
|
23
|
+
/** Reserved key: which command templated steps run through. */
|
|
24
|
+
const COMMAND_KEY = "command";
|
|
25
|
+
/** Reserved key: which model choice steps without their own run with. */
|
|
26
|
+
const CHOICE_KEY = "choice";
|
|
27
|
+
const KEY_VALUE = /^([A-Za-z_][\w.-]*)=(.*)$/s;
|
|
28
|
+
/** Strips one layer of surrounding double quotes, which is how a value holds spaces. */
|
|
29
|
+
function unquote(value) {
|
|
30
|
+
return value.length > 1 && value.startsWith("\"") && value.endsWith("\"") ? value.slice(1, -1) : value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Splits on whitespace, keeping double-quoted runs together.
|
|
34
|
+
*
|
|
35
|
+
* Returns each token with the offset it started at, so the caller can take the
|
|
36
|
+
* rest of the line verbatim rather than rebuilding it from tokens and losing
|
|
37
|
+
* the spacing a prompt was written with.
|
|
38
|
+
*/
|
|
39
|
+
function tokenize(line) {
|
|
40
|
+
const tokens = [];
|
|
41
|
+
let index = 0;
|
|
42
|
+
while (index < line.length) {
|
|
43
|
+
while (index < line.length && /\s/.test(line[index])) index += 1;
|
|
44
|
+
if (index >= line.length) break;
|
|
45
|
+
const start = index;
|
|
46
|
+
let quoted = false;
|
|
47
|
+
while (index < line.length && (quoted || !/\s/.test(line[index]))) {
|
|
48
|
+
if (line[index] === "\"") quoted = !quoted;
|
|
49
|
+
index += 1;
|
|
50
|
+
}
|
|
51
|
+
tokens.push({
|
|
52
|
+
text: line.slice(start, index),
|
|
53
|
+
start
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return tokens;
|
|
57
|
+
}
|
|
58
|
+
function parseWorkflowLaunchCommand(args) {
|
|
59
|
+
const tokens = tokenize(args.trim());
|
|
60
|
+
const first = tokens[0];
|
|
61
|
+
if (first === void 0) return { error: "Usage: /workflow-launch <workflow> [key=value …] [prompt]" };
|
|
62
|
+
const inputs = {};
|
|
63
|
+
const reserved = {};
|
|
64
|
+
let index = 1;
|
|
65
|
+
for (; index < tokens.length; index += 1) {
|
|
66
|
+
const token = tokens[index];
|
|
67
|
+
const match = KEY_VALUE.exec(token.text);
|
|
68
|
+
if (!match) break;
|
|
69
|
+
const key = match[1];
|
|
70
|
+
const value = unquote(match[2]);
|
|
71
|
+
if (key === RUNNER_KEY || key === COMMAND_KEY || key === CHOICE_KEY) reserved[key] = value;
|
|
72
|
+
else inputs[key] = value;
|
|
73
|
+
}
|
|
74
|
+
const rest = tokens[index];
|
|
75
|
+
const prompt = rest === void 0 ? void 0 : args.trim().slice(rest.start).trim();
|
|
76
|
+
return {
|
|
77
|
+
workflow: unquote(first.text),
|
|
78
|
+
...reserved,
|
|
79
|
+
inputs,
|
|
80
|
+
...prompt === void 0 || prompt === "" ? {} : { prompt }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The workflow a token names: its name, its path in the repository, or the
|
|
85
|
+
* tail of that path. Case-insensitive on the name, because the catalog shows
|
|
86
|
+
* names as their author capitalised them and nobody types that back exactly.
|
|
87
|
+
*/
|
|
88
|
+
function resolveWorkflowEntry(entries, token) {
|
|
89
|
+
const needle = token.trim();
|
|
90
|
+
const lower = needle.toLocaleLowerCase();
|
|
91
|
+
return entries.find((entry) => entry.name.toLocaleLowerCase() === lower) ?? entries.find((entry) => entry.relativePath === needle || entry.path === needle) ?? entries.find((entry) => entry.relativePath.endsWith(`/${needle}`) || entry.path.endsWith(`/${needle}`));
|
|
92
|
+
}
|
|
93
|
+
/** The trigger that makes a prompt mandatory: the run waits for one otherwise. */
|
|
94
|
+
const USER_PROMPT_TRIGGER = "user_prompt";
|
|
95
|
+
/**
|
|
96
|
+
* Everything wrong with a launch, before it is started.
|
|
97
|
+
*
|
|
98
|
+
* Checked here rather than left to the engine because the failure would
|
|
99
|
+
* otherwise be a run that starts and then waits forever for terminal input, or
|
|
100
|
+
* one that dies on a runner its steps never declared. Both read as the launch
|
|
101
|
+
* having worked.
|
|
102
|
+
*/
|
|
103
|
+
function validateWorkflowLaunch(requirements, parsed) {
|
|
104
|
+
const problems = [];
|
|
105
|
+
if (requirements.triggers.includes(USER_PROMPT_TRIGGER) && (parsed.prompt ?? "") === "") problems.push("This workflow is triggered by a prompt, so it needs one.");
|
|
106
|
+
const missing = requirements.inputs.filter((input) => input.required === true && (parsed.inputs[input.name] ?? "") === "").map((input) => input.name);
|
|
107
|
+
if (missing.length > 0) problems.push(`Missing required input${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}.`);
|
|
108
|
+
for (const input of requirements.inputs) {
|
|
109
|
+
const value = parsed.inputs[input.name];
|
|
110
|
+
if (value === void 0 || input.options === void 0 || input.options.includes(value)) continue;
|
|
111
|
+
problems.push(`Input ${input.name} must be one of: ${input.options.join(", ")}.`);
|
|
112
|
+
}
|
|
113
|
+
const declared = requirements.runners;
|
|
114
|
+
if (parsed.runner !== void 0 && declared !== void 0 && !declared.includes(parsed.runner)) problems.push(declared.length === 0 ? "This workflow declares no runner its steps agree on." : `Runner ${parsed.runner} is not one this workflow declares: ${declared.join(", ")}.`);
|
|
115
|
+
if (parsed.command !== void 0 && !(requirements.commands ?? []).includes(parsed.command)) problems.push(requirements.commands === void 0 || requirements.commands.length === 0 ? "This workflow has no templated steps to run through a command." : `Command ${parsed.command} is not one every step offers: ${requirements.commands.join(", ")}.`);
|
|
116
|
+
if (parsed.choice !== void 0 && !(requirements.choices ?? []).includes(parsed.choice)) problems.push(`Choice ${parsed.choice} is not declared${requirements.choices?.length ? `: ${requirements.choices.join(", ")}` : ""}.`);
|
|
117
|
+
return problems;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { resolveWorkflowEntry as n, validateWorkflowLaunch as r, parseWorkflowLaunchCommand as t };
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=workflowLaunchCommand-GGqimmTC.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflowLaunchCommand-GGqimmTC.mjs","names":[],"sources":["../src/services/workflowLaunchCommand/index.ts"],"sourcesContent":["/**\n * The `/workflow-launch` line, parsed.\n *\n * WHY A VERB AT ALL:\n * A browser can only send a session a prompt frame, so every cockpit action\n * that has to run inside the session travels as a slash line, the way the\n * agent catalog sends `/run`. The workflow tools answer to the agent, not to a\n * page, and the leader board answers to a keyboard, so neither of them can be\n * what the launch dialog sends.\n *\n * THE SHAPE:\n * /workflow-launch <workflow> [key=value ...] [prompt words]\n *\n * Key-value pairs come first and stop at the first token that is not one, so\n * everything after them is the prompt exactly as it was typed, including any\n * `=` inside it. `runner`, `command` and `choice` are reserved keys; the rest\n * are the workflow's own `workflow_dispatch` inputs. Values may be double\n * quoted to hold spaces.\n */\n\n/** Reserved key: which runner map entry the run executes with. */\nconst RUNNER_KEY = 'runner';\n/** Reserved key: which command templated steps run through. */\nconst COMMAND_KEY = 'command';\n/** Reserved key: which model choice steps without their own run with. */\nconst CHOICE_KEY = 'choice';\nconst KEY_VALUE = /^([A-Za-z_][\\w.-]*)=(.*)$/s;\n\nexport interface ParsedWorkflowLaunch {\n /** The workflow's name or its path, as typed; resolution belongs to the caller. */\n workflow: string;\n runner?: string;\n command?: string;\n choice?: string;\n inputs: Record<string, string>;\n prompt?: string;\n}\n\nexport interface WorkflowLaunchParseFailure {\n error: string;\n}\n\nexport function isLaunchParseFailure(\n value: ParsedWorkflowLaunch | WorkflowLaunchParseFailure,\n): value is WorkflowLaunchParseFailure {\n return 'error' in value;\n}\n\n/** Strips one layer of surrounding double quotes, which is how a value holds spaces. */\nfunction unquote(value: string): string {\n return value.length > 1 && value.startsWith('\"') && value.endsWith('\"') ? value.slice(1, -1) : value;\n}\n\n/**\n * Splits on whitespace, keeping double-quoted runs together.\n *\n * Returns each token with the offset it started at, so the caller can take the\n * rest of the line verbatim rather than rebuilding it from tokens and losing\n * the spacing a prompt was written with.\n */\nfunction tokenize(line: string): { text: string; start: number }[] {\n const tokens: { text: string; start: number }[] = [];\n let index = 0;\n while (index < line.length) {\n while (index < line.length && /\\s/.test(line[index] as string)) index += 1;\n if (index >= line.length) break;\n const start = index;\n let quoted = false;\n while (index < line.length && (quoted || !/\\s/.test(line[index] as string))) {\n if (line[index] === '\"') quoted = !quoted;\n index += 1;\n }\n tokens.push({ text: line.slice(start, index), start });\n }\n return tokens;\n}\n\nexport function parseWorkflowLaunchCommand(args: string): ParsedWorkflowLaunch | WorkflowLaunchParseFailure {\n const tokens = tokenize(args.trim());\n const first = tokens[0];\n if (first === undefined) return { error: 'Usage: /workflow-launch <workflow> [key=value …] [prompt]' };\n\n const inputs: Record<string, string> = {};\n const reserved: { runner?: string; command?: string; choice?: string } = {};\n let index = 1;\n for (; index < tokens.length; index += 1) {\n const token = tokens[index] as { text: string; start: number };\n const match = KEY_VALUE.exec(token.text);\n if (!match) break;\n const key = match[1] as string;\n const value = unquote(match[2] as string);\n if (key === RUNNER_KEY || key === COMMAND_KEY || key === CHOICE_KEY) reserved[key] = value;\n else inputs[key] = value;\n }\n\n const rest = tokens[index];\n const prompt = rest === undefined ? undefined : args.trim().slice(rest.start).trim();\n return {\n workflow: unquote(first.text),\n ...reserved,\n inputs,\n ...(prompt === undefined || prompt === '' ? {} : { prompt }),\n };\n}\n\n/** The line the cockpit sends, built from what its dialog collected. */\nexport function workflowLaunchCommand(request: ParsedWorkflowLaunch): string {\n const quote = (value: string): string => (/\\s/.test(value) ? `\"${value}\"` : value);\n const pairs = [\n ...(request.runner === undefined ? [] : [`${RUNNER_KEY}=${quote(request.runner)}`]),\n ...(request.command === undefined ? [] : [`${COMMAND_KEY}=${quote(request.command)}`]),\n ...(request.choice === undefined ? [] : [`${CHOICE_KEY}=${quote(request.choice)}`]),\n ...Object.entries(request.inputs).map(([key, value]) => `${key}=${quote(value)}`),\n ];\n const prompt = request.prompt?.trim();\n return ['/workflow-launch', quote(request.workflow), ...pairs, ...(prompt ? [prompt] : [])].join(' ');\n}\n\n/** One catalog row, as much of it as resolution needs. */\nexport interface LaunchResolvableWorkflow {\n name: string;\n path: string;\n relativePath: string;\n}\n\n/**\n * The workflow a token names: its name, its path in the repository, or the\n * tail of that path. Case-insensitive on the name, because the catalog shows\n * names as their author capitalised them and nobody types that back exactly.\n */\nexport function resolveWorkflowEntry<T extends LaunchResolvableWorkflow>(\n entries: readonly T[],\n token: string,\n): T | undefined {\n const needle = token.trim();\n const lower = needle.toLocaleLowerCase();\n return (\n entries.find((entry) => entry.name.toLocaleLowerCase() === lower) ??\n entries.find((entry) => entry.relativePath === needle || entry.path === needle) ??\n entries.find((entry) => entry.relativePath.endsWith(`/${needle}`) || entry.path.endsWith(`/${needle}`))\n );\n}\n\n/** What a workflow declares, as much of it as a launch has to satisfy. */\nexport interface LaunchRequirements {\n triggers: readonly string[];\n inputs: readonly { name: string; required?: boolean; options?: readonly string[] }[];\n /** Absent means the workflow names no runner map, so any runner will do. */\n runners?: readonly string[];\n /** Commands every templated step offers; absent when no step is templated. */\n commands?: readonly string[];\n /** Declared model choices. */\n choices?: readonly string[];\n}\n\n/** The trigger that makes a prompt mandatory: the run waits for one otherwise. */\nconst USER_PROMPT_TRIGGER = 'user_prompt';\n\n/**\n * Everything wrong with a launch, before it is started.\n *\n * Checked here rather than left to the engine because the failure would\n * otherwise be a run that starts and then waits forever for terminal input, or\n * one that dies on a runner its steps never declared. Both read as the launch\n * having worked.\n */\nexport function validateWorkflowLaunch(\n requirements: LaunchRequirements,\n parsed: Pick<ParsedWorkflowLaunch, 'inputs' | 'prompt' | 'runner' | 'command' | 'choice'>,\n): string[] {\n const problems: string[] = [];\n if (requirements.triggers.includes(USER_PROMPT_TRIGGER) && (parsed.prompt ?? '') === '') {\n problems.push('This workflow is triggered by a prompt, so it needs one.');\n }\n const missing = requirements.inputs\n .filter((input) => input.required === true && (parsed.inputs[input.name] ?? '') === '')\n .map((input) => input.name);\n if (missing.length > 0)\n problems.push(`Missing required input${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}.`);\n for (const input of requirements.inputs) {\n const value = parsed.inputs[input.name];\n if (value === undefined || input.options === undefined || input.options.includes(value)) continue;\n problems.push(`Input ${input.name} must be one of: ${input.options.join(', ')}.`);\n }\n const declared = requirements.runners;\n if (parsed.runner !== undefined && declared !== undefined && !declared.includes(parsed.runner)) {\n problems.push(\n declared.length === 0\n ? 'This workflow declares no runner its steps agree on.'\n : `Runner ${parsed.runner} is not one this workflow declares: ${declared.join(', ')}.`,\n );\n }\n if (parsed.command !== undefined && !(requirements.commands ?? []).includes(parsed.command)) {\n problems.push(\n requirements.commands === undefined || requirements.commands.length === 0\n ? 'This workflow has no templated steps to run through a command.'\n : `Command ${parsed.command} is not one every step offers: ${requirements.commands.join(', ')}.`,\n );\n }\n if (parsed.choice !== undefined && !(requirements.choices ?? []).includes(parsed.choice)) {\n problems.push(\n `Choice ${parsed.choice} is not declared${requirements.choices?.length ? `: ${requirements.choices.join(', ')}` : ''}.`,\n );\n }\n return problems;\n}\n"],"mappings":"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"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Doom Pi Workflow
|
|
2
|
+
|
|
3
|
+
Package-owned guidance for authoring, launching, monitoring, and recovering Doom Pi workflows.
|
|
4
|
+
|
|
5
|
+
## How to author
|
|
6
|
+
|
|
7
|
+
- [Author Po workflows](./src/prompts/po-author-workflow/SKILL.md): Define workflow graphs and host-executed steps safely.
|
|
8
|
+
|
|
9
|
+
## How to use
|
|
10
|
+
|
|
11
|
+
- [Use Po workflows](./src/prompts/po-use-workflow/SKILL.md): Discover, launch, monitor, control, and recover asynchronous runs.
|
|
12
|
+
- [Workflow recovery skill](./skills/workflow-recovery/SKILL.md): Evidence-first recovery of failed or interrupted workflow runs.
|
|
13
|
+
|
|
14
|
+
## Package reference
|
|
15
|
+
|
|
16
|
+
- [Package README](./README.md): Workflow mode, leader bindings, execution model, monitoring, and public entrypoints.
|
|
17
|
+
|
|
18
|
+
## Native execution contract
|
|
19
|
+
|
|
20
|
+
Agent steps require an effective `runConfig.majorMode`; delegated workflows require top-level `runConfig.majorMode` before their owner child is created. Selection projects major mode, minor modes, profile and domains, adding the workflow minor mode once. `subagentModel` and `subagentThinking` set Team preferences independently of the step model.
|
|
21
|
+
|
|
22
|
+
The native host requires execution-state inspection before creating agent steps. After settlement it waits for idle, no pending messages and a known background snapshot without core holds or holding-provider errors before checking decisions. Unknown ownership waits closed without consuming reminders; stops interrupt reads and retry delays.
|
|
23
|
+
|
|
24
|
+
Delegated successes are informational. Errors wake the workflow owner through durable custom-message admission; the launcher gets an error notification, not an agent-waking failure completion. Diagnosis stays local. Recovery, pause, resume and stop are request-driven. User-requested stops do not trigger diagnosis or automatic restart.
|
package/package.json
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agimon-ai/po-workflow",
|
|
3
|
+
"version": "0.0.0-alpha.0",
|
|
4
|
+
"description": "GitHub Actions-style workflow graphs, artifacts, recovery, and asynchronous runs for Po.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"automation",
|
|
8
|
+
"coding-agent",
|
|
9
|
+
"developer-tools",
|
|
10
|
+
"github-actions",
|
|
11
|
+
"pi-extension",
|
|
12
|
+
"pi-package",
|
|
13
|
+
"po",
|
|
14
|
+
"workflow",
|
|
15
|
+
"workflow-orchestration"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://agimon.ai",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/AgiFlow/po/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Vuong Ngo",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/AgiFlow/po.git",
|
|
26
|
+
"directory": "packages/minor/po-workflow"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"llms.txt",
|
|
31
|
+
"README.md",
|
|
32
|
+
"src/prompts",
|
|
33
|
+
"skills/workflow-recovery/SKILL.md",
|
|
34
|
+
"src/types/webWorkflows.ts",
|
|
35
|
+
"src/types/webWorkflowTerminal.ts"
|
|
36
|
+
],
|
|
37
|
+
"type": "module",
|
|
38
|
+
"main": "./dist/index.cjs",
|
|
39
|
+
"module": "./dist/index.mjs",
|
|
40
|
+
"types": "./dist/index.d.mts",
|
|
41
|
+
"jsnext:main": "./dist/index.mjs",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.mts",
|
|
45
|
+
"import": "./dist/index.mjs",
|
|
46
|
+
"require": "./dist/index.cjs"
|
|
47
|
+
},
|
|
48
|
+
"./extensions/server": {
|
|
49
|
+
"types": "./dist/extensions/server.d.mts",
|
|
50
|
+
"import": "./dist/extensions/server.mjs",
|
|
51
|
+
"require": "./dist/extensions/server.cjs"
|
|
52
|
+
},
|
|
53
|
+
"./extensions/web": {
|
|
54
|
+
"import": "./dist/extensions/web.mjs"
|
|
55
|
+
},
|
|
56
|
+
"./skills/workflow-recovery/SKILL.md": "./skills/workflow-recovery/SKILL.md",
|
|
57
|
+
"./package.json": "./package.json"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@agimon-ai/po-core": "0.0.0-alpha.0",
|
|
64
|
+
"@agimon-ai/po-minor-mode": "0.0.0-alpha.0",
|
|
65
|
+
"@agimon-ai/po-telemetry": "0.0.0-alpha.0",
|
|
66
|
+
"@agimon-ai/po-ui": "0.0.0-alpha.0",
|
|
67
|
+
"@agimon-ai/po-web-components": "0.0.0-alpha.0",
|
|
68
|
+
"@agimon-ai/po-web-security": "0.0.0-alpha.0",
|
|
69
|
+
"@agimon-ai/workflow-mcp": "0.24.12",
|
|
70
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
71
|
+
"@modelcontextprotocol/server": "2.2.0",
|
|
72
|
+
"hono": "4.13.12",
|
|
73
|
+
"typebox": "1.3.34",
|
|
74
|
+
"zod": "4.6.5"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@agimon-ai/po-build": "0.0.0-alpha.0",
|
|
78
|
+
"@earendil-works/pi-coding-agent": "1.0.2",
|
|
79
|
+
"@tanstack/react-store": "0.11.2",
|
|
80
|
+
"@tanstack/store": "0.11.2",
|
|
81
|
+
"@types/node": "26.6.4",
|
|
82
|
+
"@types/react": "19.3.0",
|
|
83
|
+
"@types/react-dom": "19.3.0",
|
|
84
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
85
|
+
"react": "19.3.0",
|
|
86
|
+
"react-dom": "19.3.0",
|
|
87
|
+
"tsdown": "0.23.0",
|
|
88
|
+
"typescript": "7.0.2",
|
|
89
|
+
"vitest": "5.0.3"
|
|
90
|
+
},
|
|
91
|
+
"peerDependencies": {
|
|
92
|
+
"@earendil-works/pi-coding-agent": "1.0.2"
|
|
93
|
+
},
|
|
94
|
+
"peerDependenciesMeta": {
|
|
95
|
+
"@earendil-works/pi-coding-agent": {
|
|
96
|
+
"optional": true
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"engines": {
|
|
100
|
+
"node": ">=24"
|
|
101
|
+
},
|
|
102
|
+
"poServer": {
|
|
103
|
+
"entry": "./generated/server.ts",
|
|
104
|
+
"dist": "./dist/extensions/server.mjs",
|
|
105
|
+
"scopes": [
|
|
106
|
+
"global",
|
|
107
|
+
"workspace",
|
|
108
|
+
"session"
|
|
109
|
+
],
|
|
110
|
+
"contracts": {
|
|
111
|
+
"entry": "./src/exports/apiContracts.ts",
|
|
112
|
+
"dist": "./dist/apiContracts.mjs"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"poWeb": {
|
|
116
|
+
"pluginId": "workflows",
|
|
117
|
+
"channels": [
|
|
118
|
+
"workflow_catalog",
|
|
119
|
+
"workflow_runs"
|
|
120
|
+
],
|
|
121
|
+
"client": "./dist/extensions/web.mjs",
|
|
122
|
+
"scopes": [
|
|
123
|
+
"session"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"scripts": {
|
|
127
|
+
"build": "tsdown",
|
|
128
|
+
"test": "vitest --run",
|
|
129
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.web.json",
|
|
130
|
+
"lint": "oxlint . && oxfmt . --check",
|
|
131
|
+
"fixcode": "oxlint . --fix && oxfmt ."
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-recovery
|
|
3
|
+
description: Recover a failed or interrupted workflow-mcp run so it resumes from its active repair rather than starting over. Use before a recover action (workflow_run in Pi CLI, workflow_tools in server/web sessions), whenever a run shows stage error or interrupted, or when deciding between recovery, a fresh launch, and deferral. Covers evidence, permitted outcomes, judgement cases, and files that must never be edited.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recovering a workflow run
|
|
7
|
+
|
|
8
|
+
A recovery is **not** a relaunch, and the difference is expensive to get wrong. A
|
|
9
|
+
relaunch throws away the run's state and starts again; a recovery resumes the
|
|
10
|
+
existing run from its active repair, preserving the worktree and the work already
|
|
11
|
+
done. Choosing the wrong one either destroys progress or resumes a run that can
|
|
12
|
+
never succeed.
|
|
13
|
+
|
|
14
|
+
Recovery is also the explicit ownership-transfer path. A replacement Pi session
|
|
15
|
+
may recover a terminal failed run launched by an earlier session; the replay is
|
|
16
|
+
then stamped to the recovering session. This does not permit cross-session
|
|
17
|
+
control of running or completed work.
|
|
18
|
+
|
|
19
|
+
Read this before action `recover`: use `workflow_run` in Pi CLI sessions and
|
|
20
|
+
`workflow_tools` in server/web sessions. Both take the same `runKey` and separate `workspace`.
|
|
21
|
+
|
|
22
|
+
## 1. Gather evidence first
|
|
23
|
+
|
|
24
|
+
Never act on the stage alone. Before touching a failed run, collect:
|
|
25
|
+
|
|
26
|
+
- **Every log source**, not just the first one that looks relevant.
|
|
27
|
+
- **Process state**: is anything still alive from the original run?
|
|
28
|
+
- **The exact staged `run.json`** for the run.
|
|
29
|
+
- **Only the repair referenced by `activeRepairId`.** Other repairs in the
|
|
30
|
+
directory belong to earlier cycles and are not yours to act on.
|
|
31
|
+
|
|
32
|
+
Use action `recovery-evidence` on the same tool for the terminal `run.json`
|
|
33
|
+
and durable `changelog.md`, `context.md`, and progress evidence. This dedicated
|
|
34
|
+
action is terminal-only and read-only, including for failures from an earlier
|
|
35
|
+
session; it never scrapes that session's live launcher. Generic `status` and
|
|
36
|
+
`tail` remain limited to runs owned by this session.
|
|
37
|
+
|
|
38
|
+
## 2. Choose exactly one outcome
|
|
39
|
+
|
|
40
|
+
Every failed run resolves to one of three, and never two in the same pass:
|
|
41
|
+
|
|
42
|
+
| Outcome | When |
|
|
43
|
+
| ---------------------------- | ----------------------------------------------------------------------------------------- |
|
|
44
|
+
| **Recover** the existing run | The recorded target is still valid and the failure was transient or an interruption |
|
|
45
|
+
| **Launch fresh** | The run cannot resume, or the branch has diverged far enough that resuming is meaningless |
|
|
46
|
+
| **Defer** | A named prerequisite is materially unresolved |
|
|
47
|
+
|
|
48
|
+
Recovery and a fresh launch each consume one launch from the pass budget.
|
|
49
|
+
|
|
50
|
+
## 3. Judgement cases
|
|
51
|
+
|
|
52
|
+
- `interrupted` with a valid failed job: the strong recovery case.
|
|
53
|
+
- `failed` with a valid failed job: recoverable when the failure was transient
|
|
54
|
+
and the recorded target remains valid.
|
|
55
|
+
- **Empty failed job**: cannot be recovered. Launch fresh if the work is ready,
|
|
56
|
+
otherwise defer.
|
|
57
|
+
- **Terminal repair-triage run**: must never be recovered. Launch fresh once the
|
|
58
|
+
work is ready again.
|
|
59
|
+
- **Stale branch**: prefer a fresh launch once the branch has diverged enough
|
|
60
|
+
that resuming would rebase onto unrecognisable history.
|
|
61
|
+
- **No job id**: the run was launched by hand and is outside dispatch. Report
|
|
62
|
+
it; do not recover it.
|
|
63
|
+
- **Blocked**: never recover or launch a run whose job sits in a blocked column.
|
|
64
|
+
A human resolves the blocker first.
|
|
65
|
+
|
|
66
|
+
## 4. Hard constraints
|
|
67
|
+
|
|
68
|
+
- **Never edit `issue.md` or `repair.json`.** They are the record of what went
|
|
69
|
+
wrong; editing them destroys the evidence and the repair chain.
|
|
70
|
+
- **Never manufacture a recovery target.** If the recorded target is invalid,
|
|
71
|
+
that is a fresh-launch case, not a reason to invent one.
|
|
72
|
+
- Prepare `run.json` **only** when verified recovery state requires it.
|
|
73
|
+
- Preserve the active repair and the existing worktree identity.
|
|
74
|
+
- Treat an “already claimed” result as another recovery winning the race; do
|
|
75
|
+
not launch fresh or retry around the claim.
|
|
76
|
+
- Do not use `--dry-run` for a real recovery.
|
|
77
|
+
|
|
78
|
+
## 5. Verify before reporting
|
|
79
|
+
|
|
80
|
+
In Pi CLI sessions, recover through the launcher recorded for the run. In server/web
|
|
81
|
+
sessions, the replay runs inside the recovering session; closing it requests a stop.
|
|
82
|
+
Then confirm **real process and registry progress**. A `recover` call that returns is
|
|
83
|
+
not a recovery that succeeded. After ownership transfers and the replacement run is
|
|
84
|
+
visible to this session, check `workflow_run` with action `status` before reporting an outcome.
|
|
85
|
+
Say what you recovered, what you skipped and why, and anything still blocked.
|