@agimon-ai/workflow-mcp 0.1.4
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 +52 -0
- package/README.md +43 -0
- package/dist/cli.cjs +2 -0
- package/dist/cli.mjs +2 -0
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +1 -0
- package/dist/stdio-CCINoZoW.mjs +49 -0
- package/dist/stdio-CZ47my0L.cjs +49 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Parameters
|
|
4
|
+
|
|
5
|
+
Licensor: AgiFlow
|
|
6
|
+
Licensed Work: @agimon-ai/public-packages
|
|
7
|
+
The Licensed Work is (c) 2026 AgiFlow.
|
|
8
|
+
Additional Use Grant: None
|
|
9
|
+
Change Date: 2030-03-06
|
|
10
|
+
Change License: Apache License, Version 2.0
|
|
11
|
+
|
|
12
|
+
Terms
|
|
13
|
+
|
|
14
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
15
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
16
|
+
Licensor may make an Additional Use Grant, above, permitting limited
|
|
17
|
+
production use.
|
|
18
|
+
|
|
19
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
20
|
+
available distribution of a specific version of the Licensed Work under this
|
|
21
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
22
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
23
|
+
above terminate.
|
|
24
|
+
|
|
25
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
26
|
+
currently in effect as described in this License, you must purchase a
|
|
27
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
28
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
29
|
+
|
|
30
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
31
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
32
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
33
|
+
for each version of the Licensed Work released by Licensor.
|
|
34
|
+
|
|
35
|
+
You must conspicuously display this License on each original or modified copy
|
|
36
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
37
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
38
|
+
License apply to your use of that work.
|
|
39
|
+
|
|
40
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
41
|
+
terminate your rights under this License for the current and all other
|
|
42
|
+
versions of the Licensed Work.
|
|
43
|
+
|
|
44
|
+
This License does not grant you any right in any trademark or logo of
|
|
45
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
46
|
+
Licensor as expressly required by this License).
|
|
47
|
+
|
|
48
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
49
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
50
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
51
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
52
|
+
TITLE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# workflow-mcp
|
|
2
|
+
|
|
3
|
+
Local GitHub Actions workflow runner with MCP server support. Parses workflow YAML files and executes jobs sequentially on macOS, with support for arbitrary runner-keyed commands, worktrees, and fix-loop recycling.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @agimon-ai/workflow-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## MCP Server
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
workflow-mcp mcp-serve
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Add to Claude Code configuration:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"workflow-mcp": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["workflow-mcp", "mcp-serve"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
workflow-mcp run-workflow .github/workflows/development.yml -p "Add a health check"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
- [CLI reference](https://github.com/AgiFlow/public-packages-internals/blob/main/packages/mcp/workflow-mcp/docs/cli.md) — all commands, flags, and examples
|
|
39
|
+
- [Workflow YAML schema](https://github.com/AgiFlow/public-packages-internals/blob/main/packages/mcp/workflow-mcp/PROMPT.md) — authoring reference for workflow files
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
BUSL-1.1
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const e=require(`./stdio-CZ47my0L.cjs`);let t=require(`node:child_process`),n=require(`node:path`),r=require(`@agimon-ai/foundation-process-registry`),i=require(`commander`),a=require(`node:module`);var o=`0.0.0`,s=class extends Error{constructor(e,t,n={},r){super(e,r),this.code=t,this.context=n,this.name=`WorkflowCommandError`}};const c=`check-codex-quota`;function l(t={}){let n=t.createService??(()=>new e.i),r=t.exit??(e=>process.exit(e)),a=t.logError??((e,t)=>console.error(e,t)),o=t.writeStdout??(e=>process.stdout.write(e));return new i.Command(c).description(`Check whether Codex quota is currently blocking new work`).action(async()=>{try{let e=await n().getQuotaStatus();if(o(`${JSON.stringify({blockingLimit:e?.blockingLimit??null,planType:e?.planType??null},null,2)}\n`),e?.blockingLimit){let t=new s(`Codex quota is blocking work at ${e.blockingLimit.limitId}/${e.blockingLimit.window}.`,`CODEX_QUOTA_BLOCKED`,{command:c,limitId:e.blockingLimit.limitId,limitName:e.blockingLimit.limitName,window:e.blockingLimit.window});a(`${t.message} [${t.code}]`,t),r(2);return}r(0)}catch(e){let t=e instanceof s?e:new s(`Error checking Codex quota.`,`CHECK_CODEX_QUOTA_COMMAND_FAILED`,{command:c},{cause:e});a(`${t.message} [${t.code}]`,t),r(1)}})}const u=l(),d=`list-crons`;function f(t={}){let n=t.createService??(()=>new e.o),r=t.exit??(e=>process.exit(e)),a=t.logError??((e,t)=>console.error(e,t)),o=t.writeStdout??(e=>process.stdout.write(e));return new i.Command(d).description(`List cron jobs scheduled via workflow-mcp`).action(async()=>{try{let e=await n().list();o(`${JSON.stringify(e,null,2)}\n`),r(0)}catch(e){let t=new s(`Error listing cron jobs.`,`LIST_CRONS_COMMAND_FAILED`,{command:d},{cause:e});a(`${t.message} [${t.code}]`,t),r(1)}})}const p=f(),m=`list-workflow-statuses`;function h(t={}){let n=t.createRegistry??(()=>new e.a),r=t.exit??(e=>process.exit(e)),a=t.logError??((e,t)=>console.error(e,t)),o=t.writeStdout??(e=>process.stdout.write(e));return new i.Command(m).description(`List tracked workflow runs and their current stages`).option(`-w, --workspace <name>`,`Filter workflow runs by workspace`,`all`).action(async e=>{try{let t=n(),i=e.workspace===`all`?void 0:e.workspace,a=await t.listRuns(i);o(`${JSON.stringify(a,null,2)}\n`),r(0)}catch(t){let n=new s(`Error listing workflow statuses.`,`LIST_WORKFLOW_STATUSES_FAILED`,{command:m,workspace:e.workspace??`all`},{cause:t});a(`${n.message} [${n.code}]`,n),r(1)}})}const g=h(),_=()=>{try{return(0,a.createRequire)(require(`url`).pathToFileURL(__filename).href)(`@agimon-ai/foundation-process-registry`)}catch{return null}};function v(e,t){for(let n of e){if(n==null||typeof n!=`object`&&typeof n!=`function`)continue;let e=n;for(let n of t){let t=e[n];if(typeof t==`function`)return t}}return null}function y(e){let t=[e];if(e&&typeof e==`object`){let n=e.default;if(n&&(t.push(n),typeof n==`function`))try{t.push(new n)}catch{}}return t}async function b(e){let t=_();if(!t)return async()=>{};let n=y(t),r=v(n,[`registerProcess`,`register`,`registerProcessInstance`]),i=v(n,[`unregisterProcess`,`unregister`,`releaseProcess`]);if(!r)return async()=>{};try{await Promise.resolve(r({name:e,pid:process.pid,command:process.argv.join(` `)}))}catch{return async()=>{}}return i?async()=>{await Promise.resolve(i({name:e,pid:process.pid}))}:async()=>{}}async function x(e,t){await e.start();let n=async n=>{console.error(`\\nReceived ${n}, shutting down gracefully...`);let r=0;try{await e.stop()}catch(e){console.error(`Error during shutdown:`,e),r=1}try{await t()}catch(e){console.error(`Error during resource cleanup:`,e),r=1}process.exit(r)};process.on(`SIGINT`,()=>{n(`SIGINT`)}),process.on(`SIGTERM`,()=>{n(`SIGTERM`)})}async function S(e){try{await e()}catch{}}const C=new i.Command(`mcp-serve`).description(`Start MCP server with specified transport`).option(`-t, --type <type>`,`Transport type: stdio`,`stdio`).option(`--service-name <name>`,`Service name for registry tracking`,`workflow-mcp`).action(async t=>{let n=await b(t.serviceName);try{let r=t.type.toLowerCase();r===`stdio`?await x(new e.t(e.n()),n):(console.error(`Unknown transport type: ${r}. Use: stdio`),process.exit(1))}catch(e){await S(n),console.error(`Failed to start MCP server:`,e),process.exit(1)}}),w=`run-workflow`,T=`RUN_WORKFLOW_COMMAND_FAILED`,E=`WORKFLOW_MCP_BACKGROUND_CHILD`,D=`workflow-mcp-background-run`;function O(e){if(!e||e.length===0)return;let t={};for(let n of e){let[e,...r]=n.split(`=`);t[e]=r.join(`=`)}return Object.keys(t).length>0?t:void 0}function k(e){if(e.runner&&e.cliAgent&&e.runner!==e.cliAgent)throw new s(`Conflicting runner selectors.`,T,{cliAgent:e.cliAgent,command:w,runner:e.runner});return e.runner??e.cliAgent}function A(e,n){let r=process.argv[1];if(!r)throw new s(`Unable to determine the workflow-mcp CLI entry point for background execution.`,`RUN_WORKFLOW_BACKGROUND_LAUNCH_FAILED`,{command:w,workflow:e,workspace:n.workspace});let i=[r,`run-workflow`,e];n.job&&i.push(`--job`,n.job);for(let e of n.input??[])i.push(`--input`,e);for(let e of n.env??[])i.push(`--env`,e);n.secretFile&&i.push(`--secret-file`,n.secretFile),n.dryRun&&i.push(`--dry-run`),n.continueOnError&&i.push(`--continue-on-error`),n.keepWorktree&&i.push(`--keep-worktree`);let a=k(n);a&&i.push(`--runner`,a),n.prompt&&i.push(`--prompt`,n.prompt),n.name&&i.push(`--name`,n.name),n.workspace&&i.push(`--workspace`,n.workspace);let o=(0,t.spawn)(process.execPath,i,{detached:!0,env:{...process.env,[E]:`1`},stdio:`ignore`});return o.unref(),o.pid}async function j(e,t){if(process.env[E]!==`1`)return async()=>{};let i=new r.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH),a=(0,n.resolve)(process.cwd()),o=process.env.NODE_ENV??`development`;return(await i.registerProcess({repositoryPath:a,serviceName:D,serviceType:`tool`,environment:o,pid:process.pid,command:process.argv.join(` `),args:process.argv.slice(2),metadata:{workflow:e,workspace:t.workspace,job:t.job,name:t.name,runner:t.runner??t.cliAgent},force:!0})).success?async()=>{let e=await i.releaseProcess({repositoryPath:a,serviceName:D,serviceType:`tool`,environment:o,pid:process.pid,kill:!1,releasePort:!1,force:!0});if(!e.success&&!e.error?.includes(`No matching process entry`))throw Error(e.error??`Failed to release workflow background process`)}:async()=>{}}function M(t={}){let n=t.createService??(()=>new e.r),r=t.exit??(e=>process.exit(e)),a=t.launchBackgroundRun??A,o=t.registerBackgroundChild??j,c=t.logError??((e,t)=>console.error(e,t)),l=t.logInfo??(e=>process.stdout.write(`${e}\n`));return new i.Command(w).description(`Run a GitHub Actions workflow file locally on macOS`).argument(`<workflow>`,`Path to the workflow YAML file`).option(`-j, --job <name>`,`Run only this job (and its dependencies)`).option(`-i, --input <key=value...>`,`Set workflow_dispatch input (repeatable)`).option(`-e, --env <key=value...>`,`Set extra environment variable (repeatable)`).option(`--secret-file <path>`,`Load secrets from a dotenv-style file`).option(`--dry-run`,`Print steps without executing`).option(`--continue-on-error`,`Continue past step failures`).option(`--runner <runner>`,`Preferred runner key for step command maps`).option(`--cli-agent <agent>`,`Deprecated alias for --runner`).option(`-p, --prompt <text>`,`User prompt for user_prompt trigger workflows`).option(`-n, --name <name>`,`Name for the workflow run context directory`).option(`-w, --workspace <name>`,`Workspace for workflow registry storage`).option(`--keep-worktree`,`Keep worktree on completion (skip merge and cleanup for retry)`).option(`--skip-launch`,`Skip launch-command delegation (used by inner invocations)`).option(`-b, --background`,`Run the workflow in a detached background process`).action(async(e,t)=>{try{if(t.background){let n=a(e,t);l(`Started workflow in background${n?` (PID: ${n})`:``}`),r(0);return}let i=await o(e,t),s=n();try{let n=k(t),a=await s.run({cliAgent:t.cliAgent,runner:n,workflowPath:e,job:t.job,inputs:O(t.input),env:O(t.env),secretFile:t.secretFile,dryRun:t.dryRun,continueOnError:t.continueOnError,keepWorktree:t.keepWorktree,prompt:t.prompt,name:t.name,workspace:t.workspace,skipLaunch:t.skipLaunch});await i(),r(a.exitCode)}catch(e){throw await i(),e}}catch(n){let i=n instanceof s?n:new s(`Error executing run-workflow.`,T,{background:!!t.background,command:w,workflow:e,workspace:t.workspace},{cause:n});c(`${i.message} [${i.code}]`,i),r(1)}})}const N=M(),P=`schedule-cron`;function F(t={}){let n=t.createService??(()=>new e.o),r=t.exit??(e=>process.exit(e)),a=t.logError??((e,t)=>console.error(e,t)),o=t.writeStdout??(e=>process.stdout.write(`${e}\n`));return new i.Command(P).description(`Schedule a headless Claude Code or Codex CLI run via system crontab`).argument(`<name>`,`Unique name for this cron job`).option(`-d, --cwd <path>`,`Working directory for the CLI run`,process.cwd()).option(`-c, --cli <cli>`,`CLI to use: claude or codex`,e.s).option(`-p, --prompt <text>`,`Prompt to pass to the CLI`).option(`-f, --prompt-file <path>`,`Path to a file whose content is used as the prompt (read at cron execution time)`).option(`-s, --schedule <cron>`,`Cron expression (e.g., "*/10 * * * *")`).option(`-i, --interval-minutes <minutes>`,`Run every N minutes (alternative to --schedule)`).action(async(t,i)=>{try{let a=e.c.parse({name:t,cwd:i.cwd??process.cwd(),cli:i.cli??`claude`,prompt:i.prompt,promptFile:i.promptFile,schedule:i.schedule,intervalMinutes:i.intervalMinutes?Number.parseInt(i.intervalMinutes,10):void 0}),s=await n().schedule(a);o(`Scheduled cron job "${s.name}" with schedule: ${s.schedule}`),o(`CLI: ${s.cli} | CWD: ${s.cwd}`),s.prompt&&o(`Prompt: ${s.prompt}`),r(0)}catch(e){let n=e instanceof s?e:new s(`Error scheduling cron job.`,`SCHEDULE_CRON_COMMAND_FAILED`,{command:P,name:t},{cause:e});a(`${n.message} [${n.code}]`,n),r(1)}})}const I=F();async function L(){let e=new i.Command;e.name(`workflow-mcp`).description(`MCP server for running GitHub Actions workflows locally`).version(o),e.addCommand(p),e.addCommand(g),e.addCommand(u),e.addCommand(C),e.addCommand(N),e.addCommand(I),await e.parseAsync(process.argv)}L().catch(e=>{console.error(`[CLI_STARTUP_ERROR] workflow-mcp startup failed:`,e),process.exit(1)});
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as e,c as t,i as n,n as r,o as i,r as a,s as o,t as s}from"./stdio-CCINoZoW.mjs";import{createRequire as c}from"node:module";import{spawn as l}from"node:child_process";import{resolve as u}from"node:path";import{ProcessRegistryService as d}from"@agimon-ai/foundation-process-registry";import{Command as f}from"commander";var p=`0.0.0`,m=class extends Error{constructor(e,t,n={},r){super(e,r),this.code=t,this.context=n,this.name=`WorkflowCommandError`}};const h=`check-codex-quota`;function g(e={}){let t=e.createService??(()=>new n),r=e.exit??(e=>process.exit(e)),i=e.logError??((e,t)=>console.error(e,t)),a=e.writeStdout??(e=>process.stdout.write(e));return new f(h).description(`Check whether Codex quota is currently blocking new work`).action(async()=>{try{let e=await t().getQuotaStatus();if(a(`${JSON.stringify({blockingLimit:e?.blockingLimit??null,planType:e?.planType??null},null,2)}\n`),e?.blockingLimit){let t=new m(`Codex quota is blocking work at ${e.blockingLimit.limitId}/${e.blockingLimit.window}.`,`CODEX_QUOTA_BLOCKED`,{command:h,limitId:e.blockingLimit.limitId,limitName:e.blockingLimit.limitName,window:e.blockingLimit.window});i(`${t.message} [${t.code}]`,t),r(2);return}r(0)}catch(e){let t=e instanceof m?e:new m(`Error checking Codex quota.`,`CHECK_CODEX_QUOTA_COMMAND_FAILED`,{command:h},{cause:e});i(`${t.message} [${t.code}]`,t),r(1)}})}const _=g(),v=`list-crons`;function y(e={}){let t=e.createService??(()=>new i),n=e.exit??(e=>process.exit(e)),r=e.logError??((e,t)=>console.error(e,t)),a=e.writeStdout??(e=>process.stdout.write(e));return new f(v).description(`List cron jobs scheduled via workflow-mcp`).action(async()=>{try{let e=await t().list();a(`${JSON.stringify(e,null,2)}\n`),n(0)}catch(e){let t=new m(`Error listing cron jobs.`,`LIST_CRONS_COMMAND_FAILED`,{command:v},{cause:e});r(`${t.message} [${t.code}]`,t),n(1)}})}const b=y(),x=`list-workflow-statuses`;function S(t={}){let n=t.createRegistry??(()=>new e),r=t.exit??(e=>process.exit(e)),i=t.logError??((e,t)=>console.error(e,t)),a=t.writeStdout??(e=>process.stdout.write(e));return new f(x).description(`List tracked workflow runs and their current stages`).option(`-w, --workspace <name>`,`Filter workflow runs by workspace`,`all`).action(async e=>{try{let t=n(),i=e.workspace===`all`?void 0:e.workspace,o=await t.listRuns(i);a(`${JSON.stringify(o,null,2)}\n`),r(0)}catch(t){let n=new m(`Error listing workflow statuses.`,`LIST_WORKFLOW_STATUSES_FAILED`,{command:x,workspace:e.workspace??`all`},{cause:t});i(`${n.message} [${n.code}]`,n),r(1)}})}const C=S(),w=()=>{try{return c(import.meta.url)(`@agimon-ai/foundation-process-registry`)}catch{return null}};function T(e,t){for(let n of e){if(n==null||typeof n!=`object`&&typeof n!=`function`)continue;let e=n;for(let n of t){let t=e[n];if(typeof t==`function`)return t}}return null}function E(e){let t=[e];if(e&&typeof e==`object`){let n=e.default;if(n&&(t.push(n),typeof n==`function`))try{t.push(new n)}catch{}}return t}async function D(e){let t=w();if(!t)return async()=>{};let n=E(t),r=T(n,[`registerProcess`,`register`,`registerProcessInstance`]),i=T(n,[`unregisterProcess`,`unregister`,`releaseProcess`]);if(!r)return async()=>{};try{await Promise.resolve(r({name:e,pid:process.pid,command:process.argv.join(` `)}))}catch{return async()=>{}}return i?async()=>{await Promise.resolve(i({name:e,pid:process.pid}))}:async()=>{}}async function O(e,t){await e.start();let n=async n=>{console.error(`\\nReceived ${n}, shutting down gracefully...`);let r=0;try{await e.stop()}catch(e){console.error(`Error during shutdown:`,e),r=1}try{await t()}catch(e){console.error(`Error during resource cleanup:`,e),r=1}process.exit(r)};process.on(`SIGINT`,()=>{n(`SIGINT`)}),process.on(`SIGTERM`,()=>{n(`SIGTERM`)})}async function k(e){try{await e()}catch{}}const A=new f(`mcp-serve`).description(`Start MCP server with specified transport`).option(`-t, --type <type>`,`Transport type: stdio`,`stdio`).option(`--service-name <name>`,`Service name for registry tracking`,`workflow-mcp`).action(async e=>{let t=await D(e.serviceName);try{let n=e.type.toLowerCase();n===`stdio`?await O(new s(r()),t):(console.error(`Unknown transport type: ${n}. Use: stdio`),process.exit(1))}catch(e){await k(t),console.error(`Failed to start MCP server:`,e),process.exit(1)}}),j=`run-workflow`,M=`RUN_WORKFLOW_COMMAND_FAILED`,N=`WORKFLOW_MCP_BACKGROUND_CHILD`,P=`workflow-mcp-background-run`;function F(e){if(!e||e.length===0)return;let t={};for(let n of e){let[e,...r]=n.split(`=`);t[e]=r.join(`=`)}return Object.keys(t).length>0?t:void 0}function I(e){if(e.runner&&e.cliAgent&&e.runner!==e.cliAgent)throw new m(`Conflicting runner selectors.`,M,{cliAgent:e.cliAgent,command:j,runner:e.runner});return e.runner??e.cliAgent}function L(e,t){let n=process.argv[1];if(!n)throw new m(`Unable to determine the workflow-mcp CLI entry point for background execution.`,`RUN_WORKFLOW_BACKGROUND_LAUNCH_FAILED`,{command:j,workflow:e,workspace:t.workspace});let r=[n,`run-workflow`,e];t.job&&r.push(`--job`,t.job);for(let e of t.input??[])r.push(`--input`,e);for(let e of t.env??[])r.push(`--env`,e);t.secretFile&&r.push(`--secret-file`,t.secretFile),t.dryRun&&r.push(`--dry-run`),t.continueOnError&&r.push(`--continue-on-error`),t.keepWorktree&&r.push(`--keep-worktree`);let i=I(t);i&&r.push(`--runner`,i),t.prompt&&r.push(`--prompt`,t.prompt),t.name&&r.push(`--name`,t.name),t.workspace&&r.push(`--workspace`,t.workspace);let a=l(process.execPath,r,{detached:!0,env:{...process.env,[N]:`1`},stdio:`ignore`});return a.unref(),a.pid}async function R(e,t){if(process.env[N]!==`1`)return async()=>{};let n=new d(process.env.PROCESS_REGISTRY_PATH),r=u(process.cwd()),i=process.env.NODE_ENV??`development`;return(await n.registerProcess({repositoryPath:r,serviceName:P,serviceType:`tool`,environment:i,pid:process.pid,command:process.argv.join(` `),args:process.argv.slice(2),metadata:{workflow:e,workspace:t.workspace,job:t.job,name:t.name,runner:t.runner??t.cliAgent},force:!0})).success?async()=>{let e=await n.releaseProcess({repositoryPath:r,serviceName:P,serviceType:`tool`,environment:i,pid:process.pid,kill:!1,releasePort:!1,force:!0});if(!e.success&&!e.error?.includes(`No matching process entry`))throw Error(e.error??`Failed to release workflow background process`)}:async()=>{}}function z(e={}){let t=e.createService??(()=>new a),n=e.exit??(e=>process.exit(e)),r=e.launchBackgroundRun??L,i=e.registerBackgroundChild??R,o=e.logError??((e,t)=>console.error(e,t)),s=e.logInfo??(e=>process.stdout.write(`${e}\n`));return new f(j).description(`Run a GitHub Actions workflow file locally on macOS`).argument(`<workflow>`,`Path to the workflow YAML file`).option(`-j, --job <name>`,`Run only this job (and its dependencies)`).option(`-i, --input <key=value...>`,`Set workflow_dispatch input (repeatable)`).option(`-e, --env <key=value...>`,`Set extra environment variable (repeatable)`).option(`--secret-file <path>`,`Load secrets from a dotenv-style file`).option(`--dry-run`,`Print steps without executing`).option(`--continue-on-error`,`Continue past step failures`).option(`--runner <runner>`,`Preferred runner key for step command maps`).option(`--cli-agent <agent>`,`Deprecated alias for --runner`).option(`-p, --prompt <text>`,`User prompt for user_prompt trigger workflows`).option(`-n, --name <name>`,`Name for the workflow run context directory`).option(`-w, --workspace <name>`,`Workspace for workflow registry storage`).option(`--keep-worktree`,`Keep worktree on completion (skip merge and cleanup for retry)`).option(`--skip-launch`,`Skip launch-command delegation (used by inner invocations)`).option(`-b, --background`,`Run the workflow in a detached background process`).action(async(e,a)=>{try{if(a.background){let t=r(e,a);s(`Started workflow in background${t?` (PID: ${t})`:``}`),n(0);return}let o=await i(e,a),c=t();try{let t=I(a),r=await c.run({cliAgent:a.cliAgent,runner:t,workflowPath:e,job:a.job,inputs:F(a.input),env:F(a.env),secretFile:a.secretFile,dryRun:a.dryRun,continueOnError:a.continueOnError,keepWorktree:a.keepWorktree,prompt:a.prompt,name:a.name,workspace:a.workspace,skipLaunch:a.skipLaunch});await o(),n(r.exitCode)}catch(e){throw await o(),e}}catch(t){let r=t instanceof m?t:new m(`Error executing run-workflow.`,M,{background:!!a.background,command:j,workflow:e,workspace:a.workspace},{cause:t});o(`${r.message} [${r.code}]`,r),n(1)}})}const B=z(),V=`schedule-cron`;function H(e={}){let n=e.createService??(()=>new i),r=e.exit??(e=>process.exit(e)),a=e.logError??((e,t)=>console.error(e,t)),s=e.writeStdout??(e=>process.stdout.write(`${e}\n`));return new f(V).description(`Schedule a headless Claude Code or Codex CLI run via system crontab`).argument(`<name>`,`Unique name for this cron job`).option(`-d, --cwd <path>`,`Working directory for the CLI run`,process.cwd()).option(`-c, --cli <cli>`,`CLI to use: claude or codex`,o).option(`-p, --prompt <text>`,`Prompt to pass to the CLI`).option(`-f, --prompt-file <path>`,`Path to a file whose content is used as the prompt (read at cron execution time)`).option(`-s, --schedule <cron>`,`Cron expression (e.g., "*/10 * * * *")`).option(`-i, --interval-minutes <minutes>`,`Run every N minutes (alternative to --schedule)`).action(async(e,i)=>{try{let a=t.parse({name:e,cwd:i.cwd??process.cwd(),cli:i.cli??`claude`,prompt:i.prompt,promptFile:i.promptFile,schedule:i.schedule,intervalMinutes:i.intervalMinutes?Number.parseInt(i.intervalMinutes,10):void 0}),o=await n().schedule(a);s(`Scheduled cron job "${o.name}" with schedule: ${o.schedule}`),s(`CLI: ${o.cli} | CWD: ${o.cwd}`),o.prompt&&s(`Prompt: ${o.prompt}`),r(0)}catch(t){let n=t instanceof m?t:new m(`Error scheduling cron job.`,`SCHEDULE_CRON_COMMAND_FAILED`,{command:V,name:e},{cause:t});a(`${n.message} [${n.code}]`,n),r(1)}})}const U=H();async function W(){let e=new f;e.name(`workflow-mcp`).description(`MCP server for running GitHub Actions workflows locally`).version(p),e.addCommand(b),e.addCommand(C),e.addCommand(_),e.addCommand(A),e.addCommand(B),e.addCommand(U),await e.parseAsync(process.argv)}W().catch(e=>{console.error(`[CLI_STARTUP_ERROR] workflow-mcp startup failed:`,e),process.exit(1)});export{};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./stdio-CZ47my0L.cjs`);exports.StdioTransportHandler=e.t,exports.createServer=e.n;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e,t}from"./stdio-CCINoZoW.mjs";export{t as StdioTransportHandler,e as createServer};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import{Server as e}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as t,ListToolsRequestSchema as n}from"@modelcontextprotocol/sdk/types.js";import{z as r}from"zod";import{execFile as i,execSync as a,spawn as o}from"node:child_process";import{promisify as s}from"node:util";import{access as c,mkdir as l,readFile as u,readdir as d,rename as f,rm as p,unlink as m,writeFile as h}from"node:fs/promises";import{homedir as g,tmpdir as ee}from"node:os";import{basename as te,dirname as _,join as v,resolve as y}from"node:path";import{existsSync as b,mkdirSync as x,readFileSync as S,rmSync as C,unlinkSync as w,watch as ne,writeFileSync as T}from"node:fs";import{PortRegistryService as re}from"@agimon-ai/foundation-port-registry";import{ProcessRegistryService as ie}from"@agimon-ai/foundation-process-registry";import{randomUUID as ae}from"node:crypto";import{createInterface as oe}from"node:readline";import{load as E}from"js-yaml";import{StdioServerTransport as se}from"@modelcontextprotocol/sdk/server/stdio.js";var D=class extends Error{code=`WORKFLOW_TOOL_NOT_FOUND`;constructor(e,t){super(`Unknown tool "${e}". Available tools: ${t.join(`, `)}`),this.toolName=e,this.availableTools=t,this.name=`WorkflowToolNotFoundError`}},O=class extends Error{constructor(e,t,n={},r){super(e,r),this.code=t,this.context=n,this.name=`WorkflowToolError`}},k=class extends Error{constructor(e,t,n={},r){super(e,r),this.code=t,this.context=n,this.name=`CronError`}};const ce=`# workflow-mcp:cron:`,le=`crontab`,ue=`claude`,de=`codex`,fe=ue,pe=`--cwd`,me=`CRON_WRITE_FAILED`,he=r.enum([ue,de]),ge=r.object({name:r.string().min(1),cwd:r.string().min(1),cli:he,schedule:r.string().min(1),prompt:r.string().optional(),promptFile:r.string().optional(),createdAt:r.string().min(1)}),_e=r.object({name:r.string().min(1),cwd:r.string().min(1),cli:he.optional(),prompt:r.string().optional(),promptFile:r.string().optional(),schedule:r.string().optional(),intervalMinutes:r.number().positive().optional()});function ve(e){return`'${e.replace(/'/g,`'\\''`)}'`}function ye(e,t,n,r){let i=n?ve(n):r?`"$(cat ${ve(r)})"`:void 0;if(e===`codex`){let e=[de,`--approval-mode`,`full-auto`];return i&&e.push(`-q`,i),e.push(pe,ve(t)),e.join(` `)}let a=[ue,`--dangerously-skip-permissions`];return i&&a.push(`-p`,i),a.push(pe,ve(t)),a.join(` `)}function be(e){if(e<=0)throw Error(`Interval must be a positive number of minutes`);if(e<60)return`*/${e} * * * *`;let t=Math.floor(e/60);return t<24?`0 */${t} * * *`:`0 0 */${Math.floor(t/24)} * *`}const xe=s(i);var Se=class{logger;constructor(e){this.logger=e??{info:e=>process.stdout.write(`${e}\n`),error:e=>console.error(e),warn:e=>console.error(e)}}async readCrontab(){try{let{stdout:e}=await xe(le,[`-l`]);return e}catch(e){let t=e.code;if(t===`ENOENT`||(e.stderr??``).includes(`no crontab for`))return``;throw this.logger.error(`[CronService.readCrontab] failed exitCode=${t}`),new k(`Failed to read current crontab.`,`CRON_READ_FAILED`,{exitCode:t},{cause:e})}}async writeCrontab(e){let t=``,n=i(le,[`-`]);n.stderr?.on(`data`,e=>{t+=e}),n.stdin?.write(e),n.stdin?.end(),await new Promise((r,i)=>{n.on(`close`,n=>{n===0?r():(this.logger.error(`[CronService.writeCrontab] failed exitCode=${n} stderr=${t}`),i(new k(`crontab rejected input (exit ${n}).`,me,{exitCode:n,stderr:t,contentLength:e.length})))}),n.on(`error`,e=>{this.logger.error(`[CronService.writeCrontab] spawn failed: ${e.message}`),i(new k(`Failed to spawn crontab process.`,me,{},{cause:e}))})})}async schedule(e){let t=e.cli??`claude`;if(!e.schedule&&!e.intervalMinutes)throw new k(`Either schedule (cron expression) or intervalMinutes must be provided.`,`CRON_INVALID_INPUT`,{name:e.name});let n=e.schedule??be(e.intervalMinutes),r=ye(t,e.cwd,e.prompt,e.promptFile),i=new Date().toISOString(),a=ge.parse({name:e.name,cwd:e.cwd,cli:t,schedule:n,prompt:e.prompt,promptFile:e.promptFile,createdAt:i}),o=await this.readCrontab(),s=this.removeCronBlock(o,e.name),c=`${`${ce}${e.name}`}\n${`# workflow-mcp:meta:${JSON.stringify(a)}`}\n${`${n} ${r}`}`,l=s.trimEnd()?`${s.trimEnd()}\n${c}\n`:`${c}\n`;return await this.writeCrontab(l),this.logger.info(`Scheduled cron "${e.name}" [${n}]`),a}async list(){let e=await this.readCrontab(),t=[],n=e.split(`
|
|
2
|
+
`);for(let e=0;e<n.length;e++){let r=n[e];if(r.startsWith(`# workflow-mcp:meta:`))try{let e=r.slice(20),n=ge.parse(JSON.parse(e));t.push(n)}catch(t){this.logger.warn(`[CronService.list] skipping malformed metadata at line ${e+1}: ${t.message}`)}}return t}async remove(e){let t=await this.readCrontab(),n=this.removeCronBlock(t,e);return n===t?!1:(await this.writeCrontab(n),this.logger.info(`Removed cron "${e}"`),!0)}removeCronBlock(e,t){let n=e.split(`
|
|
3
|
+
`),r=[],i=`${ce}${t}`,a=!1;for(let e of n){if(e===i){a=!0;continue}if(a){if(e.startsWith(`# workflow-mcp:meta:`))continue;a=!1;continue}r.push(e)}return r.join(`
|
|
4
|
+
`)}},Ce=class e{static TOOL_NAME=`list-crons`;constructor(e=new Se){this.service=e}getInputSchema(){return r.object({})}getDefinition(){return{name:e.TOOL_NAME,description:`List all cron jobs scheduled via workflow-mcp, showing name, schedule, CLI, cwd, and prompt.`,inputSchema:r.toJSONSchema(r.object({}))}}async execute(){try{let e=await this.service.list();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}catch(t){let n=new O(`Failed to list cron jobs.`,`LIST_CRONS_TOOL_FAILED`,{tool:e.TOOL_NAME},{cause:t});return console.error(`[${e.TOOL_NAME}] ${n.message}`,n.context),{content:[{type:`text`,text:JSON.stringify({code:n.code,context:n.context,message:n.message},null,2)}],isError:!0}}}},we=class extends Error{code=`INVALID_WORKFLOW_RUN_RECORD`;constructor(e,t,n){super(`Invalid workflow run record at "${e}": ${t}`,n),this.recordPath=e,this.name=`InvalidWorkflowRunRecordError`}},Te=class extends Error{code=`WORKFLOW_RUN_CONFLICT`;constructor(e,t){super(`Workflow "${t}" is already running in workspace "${e}"`),this.workspace=e,this.runKey=t,this.name=`WorkflowRunConflictError`}};const A=r.record(r.string(),r.coerce.string()),Ee=r.object({description:r.string().optional(),required:r.boolean().optional(),default:r.string().optional(),type:r.string().optional(),options:r.array(r.string()).optional()}),De=r.record(r.string(),r.string()),Oe=r.record(r.string(),r.string()),ke=r.union([r.string(),Oe]),Ae=r.object({"fail-fast":r.boolean().optional(),matrix:r.looseObject({include:r.array(De).optional()}).optional()}),j=r.object({name:r.string().optional(),uses:r.string().optional(),run:ke.optional(),interactiveRun:ke.optional(),"timeout-minutes":r.number().optional(),env:A.optional(),with:r.record(r.string(),r.string()).optional(),"working-directory":r.string().optional(),if:r.string().optional(),"continue-on-error":r.boolean().optional(),"timeout-retries":r.number().optional(),id:r.string().optional()}),je=r.object({name:r.string(),run:r.string(),env:A.optional(),"working-directory":r.string().optional(),"ready-check":r.union([r.string(),r.boolean()]).optional(),"ready-timeout":r.number().optional(),host:r.string().optional(),port:r.number().int().min(1).max(65535).optional(),"port-range":r.object({min:r.number().int().min(1).max(65535),max:r.number().int().min(1).max(65535)}).optional(),"service-type":r.enum([`tool`,`service`]).optional()}),Me=r.object({services:r.array(je).optional(),steps:r.array(j).optional()}),Ne=r.object({steps:r.array(j).optional()}),Pe=r.object({"runs-on":r.string().optional(),needs:r.union([r.string(),r.array(r.string())]).optional(),extends:r.union([r.string(),r.array(r.string())]).optional(),description:r.string().optional(),strategy:Ae.optional(),steps:r.array(j),preJob:Ne.optional(),postJob:Ne.optional(),env:A.optional(),if:r.string().optional(),"system-prompt":r.string().optional(),context:r.string().optional()}),Fe=r.object({steps:r.array(j)}),Ie=r.object({steps:r.array(j)}),Le=r.object({beforeCreate:Ie.optional(),afterCreate:Ie.optional(),beforeCompleted:Ie.optional(),afterCompleted:Ie.optional()}),Re=r.object({name:r.string().optional(),workspace:r.string().optional(),imports:r.array(r.string()).optional(),"system-prompt":r.string().optional(),"max-workflows":r.number().int().positive().optional(),"launch-command":r.string().optional(),on:r.looseObject({workflow_dispatch:r.object({inputs:r.record(r.string(),Ee).optional()}).nullable().optional(),agent_decision:Fe.optional()}).optional(),env:A.optional(),pre:Me.optional(),post:Me.optional(),worktree:Le.optional(),jobs:r.record(r.string(),Pe)}),ze=r.enum([`running`,`completed`,`error`]),Be=r.enum([`success`,`skipped`,`failed`,`interrupted`]),Ve=r.object({displayName:r.string(),dryRun:r.boolean(),errorMessage:r.string().optional(),exitCode:r.number().optional(),finishedAt:r.string().optional(),outcome:Be.optional(),pid:r.number().int().positive().optional(),runKey:r.string(),stale:r.boolean().optional(),staleReason:r.string().optional(),stage:ze,startedAt:r.string(),workflowPath:r.string(),workspace:r.string()});r.object({changelogPath:r.string(),contextPath:r.string(),displayName:r.string(),recordPath:r.string(),runDir:r.string(),runKey:r.string(),workspace:r.string()});const He=`default`,M=`running`,Ue=`completed`,N=`error`,We=`workspaces`,P=`run.json`,Ge=`Workflow process is no longer running`,Ke={running:0,error:1,completed:2};var qe=class{constructor(e=y(g(),`.workflow-mcp`)){this.homeDir=e}resolveWorkspace(e,t){return this.slugifySegment(e||t||He,He)}async createRun(e){let t=this.resolveWorkspace(e.workspace,e.workflowWorkspace),n=this.slugifySegment(e.displayName,`workflow-run`);await this.ensureWorkspaceStructure(t);let r=await this.findReusableRunStage(t,n);if(r===M)throw new Te(t,n);let i=this.getRunDirectory(t,M,n);r?(await p(i,{recursive:!0,force:!0}),await f(this.getRunDirectory(t,r,n),i),await this.removeFileIfExists(y(i,`fix.md`))):await l(i,{recursive:!0});let a={displayName:e.displayName,dryRun:e.dryRun,runKey:n,stage:M,startedAt:new Date().toISOString(),pid:e.pid??process.pid,workflowPath:e.workflowPath,workspace:t},o=y(i,P);return await this.writeRunRecord(o,a),{changelogPath:y(i,`changelog.md`),contextPath:y(i,`context.md`),displayName:e.displayName,recordPath:o,runDir:i,runKey:n,workspace:t}}async finalizeRun(e,t){await this.ensureWorkspaceStructure(e.workspace);let n=this.getRunDirectory(e.workspace,t.stage,e.runKey),r=await this.readRunRecord(e.recordPath),i={...r,errorMessage:t.errorMessage,exitCode:t.exitCode,finishedAt:new Date().toISOString(),outcome:t.outcome,pid:r.pid,stage:t.stage};e.runDir!==n&&(await p(n,{recursive:!0,force:!0}),await f(e.runDir,n));let a=y(n,P);return await this.writeRunRecord(a,i),{...e,recordPath:a,runDir:n}}async readRunRecord(e){try{return this.validateRunRecord(JSON.parse(await u(e,`utf-8`)),e)}catch(t){throw t instanceof we?t:new we(e,t instanceof Error?t.message:`Unknown parse failure`,{cause:t})}}async listRuns(e){let t=e?[this.resolveWorkspace(e)]:await this.listWorkspaceNames();return(await Promise.all(t.map(async e=>this.listWorkspaceRuns(e)))).flat().sort((e,t)=>this.compareRunRecords(e,t))}async countRunningWorkflows(e){let t=this.getRunStageDirectory(e,M);if(!await this.pathExists(t))return 0;let n=await d(t,{withFileTypes:!0}),r=0;for(let e of n){if(!e.isDirectory())continue;let n=y(t,e.name,P);if(await this.pathExists(n))try{let e=await this.readRunRecord(n);e.pid&&this.isProcessAlive(e.pid)&&r++}catch{}}return r}async ensureWorkspaceStructure(e){await l(this.getRunStageDirectory(e,M),{recursive:!0}),await l(this.getRunStageDirectory(e,Ue),{recursive:!0}),await l(this.getRunStageDirectory(e,N),{recursive:!0})}async findRunStage(e,t){for(let n of[M,Ue,N])if(await this.pathExists(this.getRunDirectory(e,n,t)))return n;return null}async listWorkspaceRuns(e){let t=new Map;for(let n of[M,Ue,N]){let r=this.getRunStageDirectory(e,n);if(!await this.pathExists(r))continue;let i=await d(r,{withFileTypes:!0});for(let a of i){if(!a.isDirectory())continue;let i=y(r,a.name,P);if(!await this.pathExists(i))continue;let o=await this.readListableRunRecord(e,n,a.name,i);o&&t.set(o.runKey,o)}}return[...t.values()]}async findReusableRunStage(e,t){let n=await this.findRunStage(e,t);return n===M?(await this.reconcileStaleRunningRecord(e,t),this.findRunStage(e,t)):n}async inspectRunningRecord(e,t){let n=y(this.getRunDirectory(e,M,t),P),r=await this.readRunRecord(n);return!r.pid||this.isProcessAlive(r.pid)?r:{...r,stale:!0,staleReason:`${Ge}: pid ${r.pid}`}}async readListableRunRecord(e,t,n,r){try{return t===M?await this.inspectRunningRecord(e,n):await this.readRunRecord(r)}catch(e){if(e instanceof we)return null;throw e}}async reconcileStaleRunningRecord(e,t){let n=this.getRunDirectory(e,M,t),r=y(n,P),i=await this.readRunRecord(r);if(!i.pid||this.isProcessAlive(i.pid))return i;let a=this.getRunDirectory(e,N,t),o={...i,errorMessage:`${Ge}: pid ${i.pid}`,exitCode:130,finishedAt:new Date().toISOString(),outcome:`interrupted`,stage:N};return await p(a,{recursive:!0,force:!0}),await f(n,a),await this.writeRunRecord(y(a,P),o),o}async listWorkspaceNames(){let e=y(this.homeDir,We);return await this.pathExists(e)?(await d(e,{withFileTypes:!0})).filter(e=>e.isDirectory()).map(e=>e.name).sort((e,t)=>e.localeCompare(t)):[]}compareRunRecords(e,t){let n=Ke[e.stage]-Ke[t.stage];if(n!==0)return n;let r=e.finishedAt??e.startedAt,i=(t.finishedAt??t.startedAt).localeCompare(r);if(i!==0)return i;let a=e.workspace.localeCompare(t.workspace);return a===0?e.runKey.localeCompare(t.runKey):a}async writeRunRecord(e,t){await h(e,`${JSON.stringify(t,null,2)}\n`,`utf-8`)}validateRunRecord(e,t){let n=Ve.safeParse(e);if(!n.success)throw new we(t,n.error.message);return n.data}getRunStageDirectory(e,t){return y(this.homeDir,We,e,t)}getRunDirectory(e,t,n){return y(this.getRunStageDirectory(e,t),n)}async pathExists(e){try{return await c(e),!0}catch(e){if(e.code===`ENOENT`)return!1;throw e}}async removeFileIfExists(e){try{await m(e)}catch(e){if(e.code!==`ENOENT`)throw e}}slugifySegment(e,t){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,`-`).replace(/^-+|-+$/g,``)||t}isProcessAlive(e){try{return process.kill(e,0),!0}catch(e){let t=e.code;if(t===`ESRCH`)return!1;if(t===`EPERM`)return!0;throw e}}};const Je=r.object({workspace:r.string().optional().describe(`Optional workspace filter. When omitted, runs from all workspaces are returned.`)});var Ye=class e{static TOOL_NAME=`list_workflow_statuses`;constructor(e=new qe){this.registry=e}getInputSchema(){return Je}getDefinition(){return{name:e.TOOL_NAME,description:`List tracked workflow runs from the local workflow registry, including their workspace, stage, outcome, and timestamps.`,inputSchema:r.toJSONSchema(Je)}}async execute(e={}){try{let t=Je.parse(e),n=await this.registry.listRuns(t.workspace);return{content:[{type:`text`,text:JSON.stringify(n,null,2)}]}}catch(t){let n=new O(`Failed to list workflow statuses.`,`LIST_WORKFLOW_STATUSES_TOOL_FAILED`,{workspace:e.workspace??`all`},{cause:t});return{content:[{type:`text`,text:JSON.stringify({code:n.code,context:n.context,message:n.message},null,2)}],isError:!0}}}},Xe=class extends Error{code=`WORKFLOW_CAPACITY_EXCEEDED`;constructor(e,t,n){super(`Workspace "${e}" is at capacity: ${t}/${n} workflows running.\n → Check running workflows: list-workflow-statuses --workspace ${e}\n → Wait for a running workflow to complete, or cancel one before dispatching again.`),this.workspace=e,this.running=t,this.max=n,this.name=`WorkflowCapacityError`}},F=class extends Error{code=`WORKFLOW_INTERRUPTED`;context;constructor(e,t={}){let n=t.context?` during ${t.context.phase}`:``;super(`Workflow interrupted by ${e}${n}`,{cause:t.cause??t.context}),this.signal=e,this.name=`WorkflowInterruptedError`,this.context=t.context}};const I=`\x1B[0m`,Ze=`\x1B[1m`,Qe=`\x1B[2m`,$e=`\x1B[31m`,et=`\x1B[34m`,tt=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function nt(e){let t=y(e);for(;;){for(let e of tt)if(b(v(t,e)))return t;let n=_(t);if(n===t)return e;t=n}}var rt=class{runningServices=[];processRegistry=new ie(process.env.PROCESS_REGISTRY_PATH);portRegistry=new re(process.env.PORT_REGISTRY_PATH);constructor(e){this.logger=e}resolveWorkingDirectory(e,t){return e[`working-directory`]?y(t,e[`working-directory`]):t}getRunningServices(){return this.runningServices}async startService(e,t,n,r){let i=e.run;if(r)return this.logger.info(` ${et}dry ${I} ${e.name}`),this.logger.info(` ${Qe}$ ${i}${I}`),null;this.logger.info(` ${et}start${I} ${e.name}`),this.logger.info(` ${Qe}$ ${i}${I}`);let a={...process.env,...t};if(e.env)for(let[t,n]of Object.entries(e.env))a[t]=String(n);let s=this.resolveWorkingDirectory(e,n),c=nt(s),l=a.NODE_ENV??process.env.NODE_ENV??`development`,u=e.host??`127.0.0.1`,d;if(e.port!==void 0||e[`port-range`]){let t=await this.portRegistry.reservePort({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,preferredPort:e.port,portRange:e[`port-range`],host:u,force:!0});if(!t.success||t.port===void 0)throw Error(t.error??`Failed to reserve port for background service ${e.name}`);d=t.port,a.PORT??=String(d),a.SERVICE_PORT??=String(d),a.HOST??=u,a.SERVICE_HOST??=u}let f=o(i,[],{stdio:[`ignore`,`pipe`,`pipe`],cwd:s,env:{...a,FORCE_COLOR:`1`},shell:`/bin/zsh`,detached:!0});if(f.pid===void 0)throw d!==void 0&&await this.portRegistry.releasePort({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,force:!0}),Error(`Failed to spawn background service ${e.name}`);let p=`${et}[${e.name}]${I}`;f.stdout?.on(`data`,e=>{for(let t of e.toString().split(`
|
|
5
|
+
`))t.trim()&&process.stdout.write(`${p} ${t}\n`)}),f.stderr?.on(`data`,e=>{for(let t of e.toString().split(`
|
|
6
|
+
`))t.trim()&&process.stderr.write(`${p} ${t}\n`)}),f.on(`error`,e=>{this.logger.error(`${p} Process error: ${e.message}`)});let m=async()=>{d!==void 0&&await this.portRegistry.releasePort({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,force:!0})};if(f.pid!==void 0){let t=await this.processRegistry.registerProcess({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,pid:f.pid,port:d,host:d===void 0?void 0:u,command:i,args:[],metadata:{workingDirectory:s,readyCheck:e[`ready-check`]},force:!0});if(!t.success){try{process.kill(-f.pid,`SIGTERM`)}catch{f.kill(`SIGTERM`)}throw d!==void 0&&await this.portRegistry.releasePort({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,force:!0}),Error(t.error??`Failed to register process for background service ${e.name}`)}m=async()=>{let t=await this.processRegistry.releaseProcess({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,pid:f.pid,kill:!0,releasePort:!0,force:!0});if(!t.success&&!t.error?.includes(`No matching process entry`))throw Error(t.error??`Failed to release ${e.name}`)}}let h={name:e.name,process:f,env:a,release:m};return this.runningServices.push(h),h}async waitForServiceReady(e,t,n,r){if(!e[`ready-check`]||typeof e[`ready-check`]!=`string`)return!0;let i=(e[`ready-timeout`]||30)*1e3,o=Date.now(),s=e[`ready-check`],c=this.resolveWorkingDirectory(e,t),l=this.runningServices.find(t=>t.name===e.name)?.env??{...process.env,...n};for(this.logger.info(` ${Qe}wait${I} Waiting for ${e.name} to be ready...`);Date.now()-o<i;){if(r?.())return!1;try{return a(s,{stdio:`ignore`,cwd:c,env:l,shell:`/bin/zsh`,timeout:5e3}),this.logger.info(` [32mready${I} ${e.name}`),!0}catch{if(r?.())return!1}await new Promise(e=>{setTimeout(e,1e3)})}return r?.()||this.logger.error(` ${$e}timeout${I} ${e.name} not ready after ${e[`ready-timeout`]||30}s`),!1}async stopAll(){if(this.runningServices.length!==0){this.logger.info(`\n ${et}${Ze}pre${I} ${Ze}Stopping services${I}`),this.logger.info(` ${`─`.repeat(50)}`);for(let e of this.runningServices){if(!e.process.killed&&e.process.pid&&this.logger.info(` ${Qe}stop${I} ${e.name} (pid ${e.process.pid})`),e.release)try{await e.release();continue}catch(t){this.logger.warn(` ${$e}warn${I} Failed registry cleanup for ${e.name}: ${t instanceof Error?t.message:String(t)}`)}if(!e.process.killed&&e.process.pid)try{process.kill(-e.process.pid,`SIGTERM`)}catch{e.process.kill(`SIGTERM`)}}this.runningServices=[]}}};const L=`\x1B[0m`,it=`\x1B[1m`,R=`\x1B[2m`,at=`\x1B[33m`,ot=`WORKFLOW_JOB_ID`,st=`PROCESS_REGISTRY_TAG`;var ct=class{constructor(e,t,n){this.parser=e,this.stepRunner=t,this.logger=n}async runJob(e,t,n,r){let i=this.parser.expandMatrix(t),a=r.maxRetries;for(let o of i){let i=ae(),s=Object.keys(o).length>0?` ${R}(${Object.entries(o).map(([e,t])=>`${e}=${t}`).join(`, `)})${L}`:``;this.logger.info(`\n [35m${it}job${L} ${it}${e}${L}${s}`),this.logger.info(` ${`─`.repeat(50)}`);let c={...n.workflowEnv,[ot]:i,[st]:i};if(t.env)for(let[e,r]of Object.entries(t.env))c[e]=this.parser.interpolate(String(r),{inputs:n.inputs,matrix:o,secrets:n.secrets,env:c});c[ot]=i,c[st]=i,this.logger.info(` ${R}jobid${L} ${i}`);let l=[];if(c.WORKFLOW_RUN_DIR){let t=n.jobOrder.map(e=>{let t=n.allJobs[e]?.description;return t?` - ${e}: ${t}`:` - ${e}`}).join(`
|
|
7
|
+
`);l.push(`You are currently running job "${e}" in the following workflow pipeline:\n${t}\n\nIf you find bugs, missing features, or issues that need fixes from a previous job, write to ${c.WORKFLOW_RUN_DIR}/fix.md with a YAML frontmatter restart-from field specifying which job should handle the fix, followed by a detailed description. Example:\n---\nrestart-from: development\n---\nDescription of what needs to be fixed...\n\nThe workflow runner will restart from the specified job. Only create fix.md for issues that require code changes — fix minor issues yourself.`)}if(n.workflowSystemPrompt&&l.push(this.parser.interpolate(n.workflowSystemPrompt,{inputs:n.inputs,matrix:o,secrets:n.secrets,env:c})),t[`system-prompt`]&&l.push(this.parser.interpolate(t[`system-prompt`],{inputs:n.inputs,matrix:o,secrets:n.secrets,env:c})),l.length>0){let e=l.join(`
|
|
8
|
+
|
|
9
|
+
`);c.JOB_SYSTEM_PROMPT=e,this.logger.info(` ${R}prompt${L} ${e.split(`
|
|
10
|
+
`)[0].slice(0,80)}${e.includes(`
|
|
11
|
+
`)?`...`:``}`)}let u=t.context?this.parser.interpolate(t.context,{inputs:n.inputs,matrix:o,secrets:n.secrets,env:c}):c.CONTEXT_FILE;if(u){let e=y(r.workflowDir,u);b(e)?(c.WORKFLOW_CONTEXT=e,c.WORKFLOW_CONTEXT_FILE=e,this.logger.info(` ${R}ctx ${L} ${u}`)):r.dryRun||this.logger.warn(` ${at}warn${L} context file not found: ${u}`)}if(c.CHANGELOG_FILE){let e=y(r.workflowDir,c.CHANGELOG_FILE),t=_(e);b(t)||x(t,{recursive:!0}),b(e)||T(e,`# Changelog
|
|
12
|
+
`,`utf-8`),c.WORKFLOW_CHANGELOG=e,this.logger.info(` ${R}log ${L} changelog at ${c.CHANGELOG_FILE}`)}let d={inputs:n.inputs,matrix:o,secrets:n.secrets,env:c},f=!1,p=t.preJob?.steps??[],m=t.steps??[],h=t.postJob?.steps??[];for(let t=1;t<=a;t++){t>1&&(this.logger.info(`\n ${at}retry${L} ${it}${e}${L}${s} (attempt ${t}/${a})`),this.logger.info(` ${`─`.repeat(50)}`));let n=!0;if(p.length>0&&(n=await this.runStepSequence(p,d,r)),n&&=await this.runStepSequence(m,d,r),h.length>0){let e=await this.runStepSequence(h,d,r);n&&=e}if(n){f=!0;break}t<a&&this.logger.warn(` ${at}Job "${e}" failed, retrying...${L}`)}if(f)this.logger.info(` [32mJob "${e}" completed${L}${s}`);else if(this.logger.error(`\n [31mJob "${e}" failed after ${a} attempts${L}`),t.strategy?.[`fail-fast`]!==!1)return!1}return!0}async runStepSequence(e,t,n){for(let r of e)if(!await this.stepRunner.runStep(r,t,n))return!1;return!0}},lt=class extends Error{code=`WORKFLOW_STEP_SPAWN_FAILED`;constructor(e,t={}){super(`Failed to start workflow step "${e.stepName}"`,{cause:t.cause??e}),this.context=e,this.name=`WorkflowStepSpawnError`}},ut=class extends Error{code=`WORKFLOW_STEP_TIMEOUT_CONFIG_INVALID`;constructor(e,t={}){super(`Invalid ${e.configKey} for workflow step "${e.stepName}"`,{cause:t.cause??e}),this.context=e,this.name=`WorkflowStepTimeoutConfigError`}};const dt=`/backend-api`;var ft=class{codexHome;fetchFn;now;readTextFile;constructor(e={}){this.codexHome=e.codexHome??v(g(),`.codex`),this.fetchFn=e.fetchFn??fetch,this.now=e.now??(()=>Date.now()),this.readTextFile=e.readTextFile??(e=>u(e,`utf8`))}isCodexCommand(e,t){let n=e.trim();return n===`codex`||n.startsWith(`codex `)}async getQuotaStatus(){let e=await this.readAuthFile(),t=e?.tokens?.access_token?.trim(),n=e?.tokens?.account_id?.trim();if(!t||!n)return null;let r=await this.readChatgptBaseUrl(),i=await this.fetchFn(this.buildUsageUrl(r),{headers:{Authorization:`Bearer ${t}`,"ChatGPT-Account-Id":n,"User-Agent":`codex-cli`}});if(!i.ok)throw Error(`codex quota request failed with HTTP ${i.status}`);let a=await i.json();return{blockingLimit:this.findBlockingLimit(a),planType:a.plan_type??null}}async readAuthFile(){let e=await this.readOptionalTextFile(v(this.codexHome,`auth.json`));return e?JSON.parse(e):null}async readChatgptBaseUrl(){let e=(await this.readOptionalTextFile(v(this.codexHome,`config.toml`)))?.match(/^\s*chatgpt_base_url\s*=\s*"([^"]+)"/m);return this.normalizeBaseUrl(e?.[1]??`https://chatgpt.com/backend-api/`)}buildUsageUrl(e){return e.includes(dt)?`${e}/wham/usage`:`${e}/api/codex/usage`}normalizeBaseUrl(e){let t=e.trim().replace(/\/+$/,``);return(t.startsWith(`https://chatgpt.com`)||t.startsWith(`https://chat.openai.com`))&&!t.includes(dt)&&(t=`${t}${dt}`),t}async readOptionalTextFile(e){try{return await this.readTextFile(e)}catch(e){if(e.code===`ENOENT`)return null;throw e}}findBlockingLimit(e){let t=[];this.collectBlockingLimit(t,`codex`,`codex`,e.rate_limit);for(let n of e.additional_rate_limits??[])this.collectBlockingLimit(t,n.metered_feature??n.limit_name??`codex`,n.limit_name??n.metered_feature??`codex`,n.rate_limit);return t.sort((e,t)=>e.resetAt-t.resetAt||t.usedPercent-e.usedPercent),t[0]??null}collectBlockingLimit(e,t,n,r){if(!r)return;let i=[{payload:r.primary_window,window:`primary`},{payload:r.secondary_window,window:`secondary`}];for(let{payload:a,window:o}of i){let i=a?.reset_at,s=a?.used_percent??0;typeof i==`number`&&(s>=100||r.limit_reached===!0&&i*1e3>this.now()||r.allowed===!1&&i*1e3>this.now())&&e.push({limitId:t,limitName:n,resetAfterSeconds:a?.reset_after_seconds??null,resetAt:i,usedPercent:s,window:o,windowSeconds:a?.limit_window_seconds??null})}}};const z=`\x1B[0m`,B=`\x1B[2m`,pt=`\x1B[36m`,mt=`\x1B[32m`,V=`\x1B[33m`,H=`\x1B[31m`,ht=`SIGINT`,U=`SIGTERM`,gt=`/bin/zsh`,_t=`FORCE_COLOR`,vt=`inherit`,yt=`pipe`,bt=` `,xt=`
|
|
13
|
+
${bt}\$ `,St=`skip`,W=`warn`,G=`error`,K=`fail`,Ct=`interrupted`,wt=`already stopping`,Tt=`failed to signal`,q=`(continue-on-error)`,Et=`ESRCH`,Dt=process.platform!==`win32`,Ot=`interactiveRun`,kt=`timeout-minutes`,At=`timeout-retries`,jt=`retry`,Mt=`SIGKILL`,Nt=1e3,Pt=6e4,Ft=Math.floor(2147483647/Pt),It={status:`status_completed`},Lt=new Set([`ENOENT`,`EBUSY`,`EAGAIN`,`EPERM`]),Rt=`done`,zt=1e3,Bt=r.number().positive().finite().max(Ft),Vt=r.number().int().min(0).max(10).default(2);var Ht=class{activeStep=null;activeQuotaWait=null;constructor(e,t,n=new ft,r=ne){this.parser=e,this.logger=t,this.quotaService=n,this.watchStatusFile=r}stopActiveStep(e){if(this.activeQuotaWait){if(this.activeQuotaWait.controller.signal.aborted){this.logger.info(` ${B}${St}${z} ${this.activeQuotaWait.stepName} ${wt}`);return}this.activeQuotaWait.signal=e,this.activeQuotaWait.controller.abort(),this.logger.info(` ${V}${Ct}${z} cancelling quota wait for ${this.activeQuotaWait.stepName}`);return}if(!this.activeStep){this.logger.info(` ${B}${St}${z} no active step to stop`);return}if(this.activeStep.process.killed){this.logger.info(` ${B}${St}${z} ${this.activeStep.stepName} ${wt}`);return}this.logger.info(` ${V}${Ct}${z} sending ${e} to ${this.activeStep.stepName}`),this.killActiveStep(e)||this.logger.warn(` ${V}${W}${z} ${Tt} ${this.activeStep.stepName}`)}async runStep(e,t,n){let r=this.resolveRunner(n),i=this.resolveStepCommand(e,t,r),a=e.name?this.parser.interpolate(e.name,t):i?.command.slice(0,60)||e.uses||`unnamed`;if(e.uses){let n=this.parser.interpolate(e.uses,t);return this.parser.isActionSkipped(n)?(this.logger.info(` ${B}${St}${z} ${n}`),!0):(this.logger.warn(` ${V}${W}${z} Unsupported action: ${n} (skipped)`),!0)}if(!i)return!0;if(i.missingRunner)return this.logger.error(` ${H}${G}${z} runner "${i.missingRunner.runner}" not found for step "${a}" (available runner keys: ${i.missingRunner.availableKeys.join(`, `)})`),e[`continue-on-error`]||n.continueOnError?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1);let{command:o,interactive:s}=i,c=this.quotaService.isCodexCommand(o,i.agentKey),l=e[`continue-on-error`]||n.continueOnError,u;try{u=this.resolveTimeoutMs(e,a)}catch(e){if(!(e instanceof ut))throw e;return this.logger.error(` ${H}${G}${z} invalid ${e.context.configKey} for "${a}": ${String(e.context.timeoutValue)}`),l?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1)}let d={...process.env,...t.env};if(e.env)for(let[n,r]of Object.entries(e.env))d[n]=this.parser.interpolate(String(r),t);let f;s&&(f=this.createStatusFile(),d.WORKFLOW_STATUS_FILE=f,this.logger.info(` ${B}status-file${z} ${f}`));let p=e[`working-directory`]?y(n.workflowDir,this.parser.interpolate(e[`working-directory`],t)):n.workflowDir;if(n.dryRun)return this.logger.info(` ${pt}dry ${z} ${a}`),this.logger.info(`${bt}${B}\$ ${o.split(`
|
|
14
|
+
`).join(xt)}${z}`),!0;let m;try{m=this.resolveTimeoutRetries(e,a)}catch(e){if(!(e instanceof ut))throw e;return this.logger.error(` ${H}${G}${z} invalid ${e.context.configKey} for "${a}": ${String(e.context.timeoutValue)}`),l?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1)}let h=!1;for(let e=0;e<=m;e++){await this.waitForCodexQuotaAvailability(a,o,c),e>0?this.logger.warn(` ${V}${jt}${z} retrying step after timeout "${a}" (attempt ${e+1}/${m+1})`):this.logger.info(` ${pt}run ${z} ${a}`),this.logger.info(`${bt}${B}\$ ${o.split(`
|
|
15
|
+
`)[0]}${o.includes(`
|
|
16
|
+
`)?` ...`:``}${z}`);let t=this.executeCommand(o,p,d,a,s,u),n=s&&f?await this.raceStatusFile(t,f,a):await t;if(n.status===`signaled`)throw this.logger.info(`\n ${V}${Ct}${z} ${a}`),new F(n.signal,{cause:Error(`Step process ended with ${n.signal}`),context:this.createInterruptContext(a,o)});if(n.status===`completed`&&(n.exitCode===130||n.exitCode===143)){let e=n.exitCode===130?ht:U;throw this.logger.info(`\n ${V}${Ct}${z} ${a}`),new F(e,{cause:Error(`Step process exited with ${n.exitCode}`),context:this.createInterruptContext(a,o)})}if(n.status===`spawn_error`)return this.logger.error(` ${H}${G}${z} unable to start step "${a}" in ${n.error.context.workDir} (${this.formatSpawnError(n.error)})`),l?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1);if(n.status===`timed_out`){if(this.logger.error(` ${H}${G}${z} step timed out after ${this.formatTimeoutMs(n.timeoutMs)} "${a}" (${o.split(`
|
|
17
|
+
`)[0]})`),e<m){if(f)try{T(f,``,`utf-8`)}catch(e){this.logger.warn(` ${V}${W}${z} status-file reset failed for ${a}: ${e instanceof Error?e.message:String(e)}`)}continue}return f&&this.cleanupStatusFile(f),l?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1)}if(f&&this.cleanupStatusFile(f),n.status===`status_completed`)return this.logger.info(` ${mt}${Rt}${z} ${a} (status-file signaled completion)\n`),!0;if(n.status===`completed`&&n.exitCode===0)return this.logger.info(` ${mt}pass${z} ${a}\n`),!0;if(n.status===`completed`&&this.logger.error(` ${H}${G}${z} step "${a}" exited with code ${n.exitCode} (${o.split(`
|
|
18
|
+
`)[0]})`),!h&&await this.shouldRetryForCodexQuota(a,c)){h=!0,this.logger.warn(` ${V}${jt}${z} codex quota reached after failed step, retrying after reset`),--e;continue}return l?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1)}return!1}async executeCommand(e,t,n,r,i,a){return i&&this.logger.info(` ${B}(interactive: output renders directly to terminal)${z}`),await new Promise(s=>{let c=i?o(e,[],{stdio:[vt,vt,vt],cwd:t,detached:!1,env:{...n,[_t]:`1`},shell:gt}):o(e,[],{stdio:[vt,yt,yt],cwd:t,detached:Dt,env:{...n,[_t]:`1`},shell:gt});this.activeStep={command:e,process:c,processGroupId:Dt&&!i&&typeof c.pid==`number`?c.pid:null,stepName:r};let l=null,u=!1,d=!1,f=e=>{d||(d=!0,p&&clearTimeout(p),l&&clearTimeout(l),this.activeStep=null,s(e))},p=a===null?null:setTimeout(()=>{if(u=!0,this.killActiveStep(U)){l=setTimeout(()=>{d||(this.logger.warn(` ${V}${W}${z} ${r} forcing SIGKILL`),this.killActiveStep(Mt))},Nt);return}f({status:`timed_out`,timeoutMs:a})},a);c.stdout?.on(`data`,e=>{for(let t of e.toString().split(`
|
|
19
|
+
`))t&&this.logger.info(t)}),c.stderr?.on(`data`,e=>{for(let t of e.toString().split(`
|
|
20
|
+
`))t&&this.logger.error(t)}),c.on(`error`,n=>{f({status:`spawn_error`,error:new lt({commandPreview:e.split(`
|
|
21
|
+
`)[0],source:`StepRunnerService.executeCommand`,stepName:r,workDir:t},{cause:n})})}),c.on(`exit`,(e,t)=>{if(this.cleanupProcessGroup(),u&&a!==null){f({status:`timed_out`,timeoutMs:a});return}if(t===ht){f({status:`signaled`,signal:ht});return}if(t===U){f({status:`signaled`,signal:U});return}f({status:`completed`,exitCode:e??1})})})}createInterruptContext(e,t){return{phase:`step_execution`,stepName:e,commandPreview:t.split(`
|
|
22
|
+
`)[0],source:`StepRunnerService.runStep`}}formatSpawnError(e){let t=e.cause;return t instanceof Error?`${`code`in t&&typeof t.code==`string`?t.code:`unknown-spawn-error`}${`path`in t&&typeof t.path==`string`?` at ${t.path}`:``}; verify command, shell, and permissions`:`check shell availability, PATH, and permissions`}resolveStepCommand(e,t,n){let r=e[Ot]!=null,i=r?{command:e[Ot],interactive:!0}:{command:e.run,interactive:!1},a=r?{command:e.run,interactive:!1}:{command:e[Ot],interactive:!0},o=this.resolveExactAgentCommand(i.command,t,n);if(o)return{...o,interactive:i.interactive};let s=this.resolveExactAgentCommand(a.command,t,n);if(s)return{...s,interactive:a.interactive};let c=this.resolveMissingExplicitRunner(e,n);if(c)return{command:``,interactive:!1,missingRunner:c};let l=this.resolveStringCommand(i.command,t);if(l)return{command:l,interactive:i.interactive};let u=this.resolveStringCommand(a.command,t);if(u)return{command:u,interactive:a.interactive};let d=this.resolveFirstMappedCommand(i.command,t);if(d)return{command:d,interactive:i.interactive};let f=this.resolveFirstMappedCommand(a.command,t);return f?{command:f,interactive:a.interactive}:null}resolveExactAgentCommand(e,t,n){if(!n||!e||typeof e==`string`)return null;let r=e[n];return r?{agentKey:n,command:this.parser.interpolate(r,t),interactive:!1}:null}resolveStringCommand(e,t){return!e||typeof e!=`string`?null:this.parser.interpolate(e,t)}resolveFirstMappedCommand(e,t){if(!e||typeof e==`string`)return null;let[n]=Object.values(e);return n?this.parser.interpolate(n,t):null}resolveMissingExplicitRunner(e,t){if(!t)return null;let n=this.collectMappedRunnerKeys(e);return n.length===0||n.includes(t)?null:{availableKeys:n,runner:t}}collectMappedRunnerKeys(e){let t=new Set;for(let n of[e[Ot],e.run])if(n&&typeof n!=`string`)for(let e of Object.keys(n))t.add(e);return[...t]}resolveRunner(e){return e.runner??e.cliAgent}async shouldRetryForCodexQuota(e,t){return t?(await this.readCodexQuotaStatus(e))?.blockingLimit!=null:!1}async waitForCodexQuotaAvailability(e,t,n){if(n)for(;;){let n=await this.readCodexQuotaStatus(e),r=n?.blockingLimit;if(!r)return;let i=r.resetAt*1e3+zt,a=Math.max(i-Date.now(),zt),o=new Date(i).toLocaleString(),s=n?.planType?` (${n.planType})`:``;this.logger.warn(` ${V}wait ${z} codex quota ${r.limitName}/${r.window} is at ${r.usedPercent}%${s}; waiting until ${o}`),await this.waitForQuotaDelay(e,t,a)}}async readCodexQuotaStatus(e){try{return await this.quotaService.getQuotaStatus()}catch(t){return this.logger.warn(` ${V}${W}${z} unable to read codex quota for "${e}" (${this.formatQuotaError(t)})`),null}}async waitForQuotaDelay(e,t,n){let r=new AbortController;this.activeQuotaWait={controller:r,signal:null,stepName:e};try{let e=n;for(;e>0;){let t=Math.min(e,3e4);await this.waitForDelay(t,r.signal),e-=t}}catch(n){throw r.signal.aborted?new F(this.activeQuotaWait?.signal??U,{cause:Error(`Interrupted while waiting for Codex quota reset`),context:{...this.createInterruptContext(e,t),phase:`StepRunnerService.waitForCodexQuota`}}):n}finally{this.activeQuotaWait=null}}async waitForDelay(e,t){await new Promise((n,r)=>{let i=setTimeout(()=>{t.removeEventListener(`abort`,a),n()},e),a=()=>{clearTimeout(i),t.removeEventListener(`abort`,a),r(Error(`aborted`))};t.addEventListener(`abort`,a,{once:!0})})}formatQuotaError(e){return e instanceof Error&&e.message.trim().length>0?e.message:`unknown quota error`}resolveTimeoutMs(e,t){let n=e[kt];if(n===void 0)return null;try{let e=Bt.parse(n);return Math.ceil(e*Pt)}catch(e){throw new ut({stepName:t,configKey:kt,timeoutValue:n},{cause:e})}}resolveTimeoutRetries(e,t){let n=e[At];try{return Vt.parse(n)}catch(e){throw new ut({stepName:t,configKey:At,timeoutValue:n},{cause:e})}}formatTimeoutMs(e){return e%Pt===0?`${e/Pt} minute(s)`:`${e}ms`}cleanupProcessGroup(){if(!this.activeStep)return;let{processGroupId:e,stepName:t}=this.activeStep;if(e!==null)try{process.kill(-e,U)}catch(n){let r=n.code;r===Et?this.logger.info(` ${B}cleanup${z} process group ${e} already exited (ESRCH)`):this.logger.warn(` ${V}${W}${z} failed to clean up process group for ${t} (pgid=${e}, error=${r})`)}}killActiveStep(e){if(!this.activeStep)return!1;let{process:t,processGroupId:n}=this.activeStep;if(n!==null)try{return process.kill(-n,e),!0}catch(e){e.code===Et?this.logger.info(` ${B}signal${z} process group for ${this.activeStep.stepName} already exited (ESRCH)`):this.logger.warn(` ${V}${W}${z} ${Tt} process group for ${this.activeStep.stepName}`)}try{return t.kill(e)}catch(e){return this.logger.warn(` ${V}${W}${z} ${Tt} ${this.activeStep.stepName} (${e.message})`),!1}}createStatusFile(){let e=v(ee(),`workflow-step-${ae()}.status`);return T(e,``,`utf-8`),e}async raceStatusFile(e,t,n){return new Promise(r=>{let i=!1,a=!1,o=null,s=null,c=null,l=e=>{i||(i=!0,s&&=(s.close(),null),c&&=(clearInterval(c),null),o&&=(clearTimeout(o),null),r(e))},u=!1,d=!1,f=()=>{try{return b(t)&&S(t,`utf-8`).trim()===`YES`}catch(e){let t=e.code;return!t||!Lt.has(t)?this.logger.warn(` ${V}${W}${z} status-file read failed for ${n}: ${e instanceof Error?e.message:String(e)}`):this.logger.info(` ${B}status-file${z} transient FS error (${t}) polling ${n}`),!1}},p=()=>{if(!(a||!f())){if(a=!0,this.logger.info(` ${mt}${Rt}${z} ${n} status-file received YES`),u=this.killActiveStep(U),!u){this.logger.warn(` ${V}${W}${z} ${n} status-file: SIGTERM could not be delivered, process may have already exited`);return}o=setTimeout(()=>{i||(d=!0,this.logger.warn(` ${V}${W}${z} ${n} did not exit after status-file SIGTERM, forcing SIGKILL`),this.killActiveStep(Mt))},Nt)}};try{s=this.watchStatusFile(t,()=>p())}catch(e){this.logger.warn(` ${V}${W}${z} status-file watcher setup failed for ${n}: ${e instanceof Error?e.message:String(e)}`)}c=setInterval(p,1e3),p(),e.then(e=>{if(!(a||f())){l(e);return}switch(a||(a=!0,this.logger.info(` ${mt}${Rt}${z} ${n} status-file observed during process exit handling`)),e.status){case`spawn_error`:case`timed_out`:l(e);return;case`signaled`:d&&this.logger.warn(` ${V}${W}${z} ${n} required SIGKILL escalation (work completed via status-file)`),l(It);return;case`completed`:e.exitCode===0||e.exitCode>128||d||!u?(d&&this.logger.warn(` ${V}${W}${z} ${n} required SIGKILL escalation (work completed via status-file, exit code ${e.exitCode})`),l(It)):l(e);return;case`status_completed`:l(e);return;default:l(e)}})})}cleanupStatusFile(e){try{b(e)&&w(e)}catch(e){this.logger.warn(` ${V}${W}${z} status-file cleanup failed: ${e instanceof Error?e.message:String(e)}`)}}};const J=`\x1B[0m`,Ut=`\x1B[1m`,Wt=`\x1B[2m`,Gt=`\x1B[36m`,Kt=`\x1B[32m`,qt=`\x1B[31m`,Jt=`\x1B[34m`;var Yt=class{activePrompt=null;activePromptSignal=null;constructor(e,t){this.stepRunner=e,this.logger=t}async pathExists(e){try{return await c(e),!0}catch{return!1}}abortActivePrompt(e){this.activePromptSignal=e,this.activePrompt?.close()}async handleUserPrompt(e,t,n,r,i){if(!e.on||!(`user_prompt`in e.on))return!0;this.logger.info(`\n ${Jt}${Ut}trigger${J} ${Ut}user_prompt${J}`),this.logger.info(` ${`─`.repeat(50)}`);let a=t,o=n.CONTEXT_FILE;if(!a&&o){let e=y(r,o);await this.pathExists(e)&&(a=(await u(e,`utf-8`)).trim(),a&&this.logger.info(` ${Kt}found${J} Existing context.md -> ${o}`))}if(!a)if(i)this.logger.info(` ${Gt}dry ${J} Would prompt user for input`),a=`(dry-run: no prompt collected)`;else{this.logger.info(` ${Gt}input${J} Enter your prompt (press Enter twice to finish):\n`);let e=[],t=oe({input:process.stdin,output:process.stdout});this.activePrompt=t,this.activePromptSignal=null,a=await new Promise((n,r)=>{let i=!1,a=e=>{i||(i=!0,this.activePrompt=null,e())};t.on(`line`,n=>{n===``&&e.length>0&&e[e.length-1]===``?t.close():e.push(n)}),t.on(`SIGINT`,()=>{this.activePromptSignal=`SIGINT`,t.close()}),t.on(`close`,()=>a(()=>{let t=this.activePromptSignal;if(this.activePromptSignal=null,t){r(new F(t));return}e.length>0&&e[e.length-1]===``&&e.pop(),n(e.join(`
|
|
23
|
+
`))}))})}if(!a)return this.logger.error(` ${qt}fail${J} No prompt provided. Aborting.`),!1;if(o){let e=y(r,o);await l(_(e),{recursive:!0}),await h(e,a,`utf-8`),this.logger.info(` ${Kt}saved${J} ${a.split(`
|
|
24
|
+
`).length} line(s) -> ${o}`)}return n.USER_PROMPT=a,this.logger.info(` ${Wt}prompt${J} ${a.split(`
|
|
25
|
+
`)[0].slice(0,80)}${a.includes(`
|
|
26
|
+
`)?`...`:``}`),!0}async handleAgentDecision(e,t,n,r,i,a){let o=e.on?.agent_decision;if(!o?.steps)return!0;this.logger.info(`\n ${Jt}${Ut}trigger${J} ${Ut}agent_decision${J}`),this.logger.info(` ${`─`.repeat(50)}`),a&&(t.HEARTBEAT_PROMPT=a,this.logger.info(` ${Wt}prompt${J} ${a.split(`
|
|
27
|
+
`)[0].slice(0,80)}${a.includes(`
|
|
28
|
+
`)?`...`:``}`));let s=t.CONTEXT_FILE;s&&await l(_(y(i.workflowDir,s)),{recursive:!0});let c={inputs:n,matrix:{},secrets:r,env:t};for(let e of o.steps)if(!await this.stepRunner.runStep(e,c,i))return this.logger.error(`\n ${qt}Agent decision aborted workflow${J}`),!1;if(s&&!i.dryRun){let e=y(i.workflowDir,s);if(!await this.pathExists(e))return this.logger.error(` ${qt}fail${J} Context file not created: ${s}`),!1;let t=(await u(e,`utf-8`)).trim();if(!t)return this.logger.error(` ${qt}fail${J} Context file is empty: ${s}`),!1;this.logger.info(` ${Kt}ready${J} context has ${t.split(`
|
|
29
|
+
`).length} line(s)`)}return!0}};const Xt=[`actions/checkout`,`actions/setup-node`,`actions/cache`,`actions/upload-artifact`,`actions/download-artifact`,`pnpm/action-setup`],Zt=`bold.calm.cool.dark.deep.fair.fast.free.gold.keen.kind.loud.neat.pure.rare.safe.slim.soft.tall.warm.wild.wise.blue.gray.jade.iron.zinc.ruby.opal.onyx`.split(`.`),Qt=`arch.beam.bird.bolt.cape.cove.dawn.dove.echo.fern.flux.gale.hawk.haze.iris.jade.kite.lake.lynx.mesa.moth.node.palm.peak.pine.raft.reef.sage.tide.vale`.split(`.`);var $t=class{generateHumanReadableId(){return`${Zt[Math.floor(Math.random()*Zt.length)]}-${Qt[Math.floor(Math.random()*Qt.length)]}`}parseWorkflowFile(e){let t=y(e);if(!b(t))throw Error(`Workflow file not found: ${t}`);let n=S(t,`utf-8`),r=Re.parse(E(n));if(r.imports&&this.resolveImports(r,t),!r.jobs||Object.keys(r.jobs).length===0)throw Error(`No jobs found in workflow file`);return this.resolveExtends(r),r}resolveImports(e,t,n=new Set){let r=y(t);if(n.has(r))throw Error(`Circular import detected: ${r}`);n.add(r);let i=r.replace(/\/[^/]+$/,``);for(let t of e.imports??[]){let a=y(i,t);if(!b(a))throw Error(`Imported workflow file not found: ${a} (from ${r})`);let o=E(S(a,`utf-8`));if(o.imports&&this.resolveImports(o,a,n),o.env&&(e.env={...o.env,...e.env}),o.pre&&(e.pre||={},o.pre.services&&(e.pre.services=[...o.pre.services,...e.pre.services??[]]),o.pre.steps&&(e.pre.steps=[...o.pre.steps,...e.pre.steps??[]])),o.post&&(e.post||={},o.post.services&&(e.post.services=[...o.post.services,...e.post.services??[]]),o.post.steps&&(e.post.steps=[...o.post.steps,...e.post.steps??[]])),o.worktree){e.worktree||={};for(let t of[`beforeCreate`,`afterCreate`,`beforeCompleted`,`afterCompleted`])o.worktree[t]&&!e.worktree[t]&&(e.worktree[t]=o.worktree[t])}if(o[`max-workflows`]&&!e[`max-workflows`]&&(e[`max-workflows`]=o[`max-workflows`]),o[`launch-command`]&&!e[`launch-command`]&&(e[`launch-command`]=o[`launch-command`]),o[`system-prompt`]&&(e[`system-prompt`]?e[`system-prompt`]=`${o[`system-prompt`]}\n\n${e[`system-prompt`]}`:e[`system-prompt`]=o[`system-prompt`]),o.jobs)for(let[t,n]of Object.entries(o.jobs))t in(e.jobs??{})||(e.jobs||={},e.jobs[t]=n)}delete e.imports}resolveExtends(e){let{jobs:t}=e,n=new Map;for(let[e,r]of Object.entries(t))e.startsWith(`.`)&&n.set(e,r);for(let[e,r]of Object.entries(t)){if(e.startsWith(`.`)||!r.extends)continue;let t=Array.isArray(r.extends)?r.extends:[r.extends],i=[...r.steps??[]],a=[...r.preJob?.steps??[]],o=[...r.postJob?.steps??[]],s=[],c=[],l=[];for(let i of t){let t=n.get(i);if(!t)throw Error(`Job "${e}" extends "${i}" but template not found`);s.push(...t.steps??[]),c.push(...t.preJob?.steps??[]),l.push(...t.postJob?.steps??[]),this.applyTemplate(r,t)}r.steps=[...s,...i],this.assignJobHookSteps(r,`preJob`,[...c,...a]),this.assignJobHookSteps(r,`postJob`,[...l,...o]),delete r.extends}for(let e of n.keys())delete t[e]}applyTemplate(e,t){e.steps=[...t.steps??[],...e.steps??[]],this.mergeJobHookSteps(e,t,`preJob`),this.mergeJobHookSteps(e,t,`postJob`),(t.env||e.env)&&(e.env={...t.env,...e.env}),t[`system-prompt`]&&e[`system-prompt`]?e[`system-prompt`]=`${t[`system-prompt`]}\n\n${e[`system-prompt`]}`:t[`system-prompt`]&&(e[`system-prompt`]=t[`system-prompt`]),t[`runs-on`]&&!e[`runs-on`]&&(e[`runs-on`]=t[`runs-on`]),t.strategy&&!e.strategy&&(e.strategy=t.strategy),t.context&&!e.context&&(e.context=t.context)}mergeJobHookSteps(e,t,n){let r=t[n]?.steps??[],i=e[n]?.steps??[];this.assignJobHookSteps(e,n,[...r,...i])}assignJobHookSteps(e,t,n){if(n.length>0){e[t]={steps:n};return}delete e[t]}loadSecrets(e){let t=new Map;if(!b(e))return t;let n=S(e,`utf-8`);for(let e of n.split(`
|
|
30
|
+
`)){let n=e.trim();if(!n||n.startsWith(`#`))continue;let r=n.indexOf(`=`);r!==-1&&t.set(n.slice(0,r),n.slice(r+1))}return t}interpolate(e,t){return e.replace(/\$\{\{\s*(.+?)\s*\}\}/g,(e,n)=>{let r=n.trim().split(`.`);if(r[0]===`inputs`&&r[1])return t.inputs.get(r[1])||``;if(r[0]===`matrix`&&r[1])return t.matrix[r[1]]||``;if(r[0]===`secrets`&&r[1])return t.secrets.get(r[1])||process.env[r[1]]||``;if(r[0]===`env`&&r[1])return t.env[r[1]]||process.env[r[1]]||``;if(r[0]===`runner`&&r[1]===`os`)return`macOS`;if(r[0]===`github`&&r[1]===`sha`)try{return a(`git rev-parse HEAD`,{encoding:`utf-8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim()}catch{return`local`}return n.includes(`hashFiles`)?`local`:`\${{ ${n} }}`})}isActionSkipped(e){return Xt.some(t=>e.startsWith(t))}resolveJobOrder(e,t){let n=new Set,r=[],i=t=>{if(n.has(t))return;n.add(t);let a=e[t];if(!a)throw Error(`Job not found: ${t}`);let o=a.needs?Array.isArray(a.needs)?a.needs:[a.needs]:[];for(let e of o)i(e);r.push(t)};if(t)i(t);else for(let t of Object.keys(e))i(t);return r}expandMatrix(e){if(!e.strategy?.matrix)return[{}];let{include:t,...n}=e.strategy.matrix;if(t&&t.length>0)return t;let r=Object.keys(n).filter(e=>Array.isArray(n[e]));if(r.length===0)return[{}];let i=[{}];for(let e of r){let t=n[e],r=[];for(let n of i)for(let i of t)r.push({...n,[e]:String(i)});i=r}return i}};const en=`\x1B[0m`,tn=`\x1B[2m`;var nn=class{constructor(e,t){this.stepRunner=e,this.logger=t}createWorktree(e,t){let n=t.toLowerCase().replace(/[^a-z0-9-]+/g,`-`).replace(/^-|-$/g,``),r=y(_(e),`${te(e)}-worktree`),i=y(r,n);b(r)||x(r,{recursive:!0});let o=`worktree/${n}`;b(i)&&(this.logger.info(` ${tn}clean${en} Removing stale worktree at ${i}`),this.cleanupWorktreePath(e,i));try{a(`git branch -D "${o}"`,{cwd:e,stdio:[`ignore`,`pipe`,`ignore`]})}catch{}return this.logger.info(` [36mcreate${en} Worktree at ${i} (branch: ${o})`),a(`git worktree add -b "${o}" "${i}" HEAD`,{cwd:e,stdio:[`ignore`,`pipe`,`ignore`]}),i}removeWorktree(e,t){if(b(t)){this.logger.info(` ${tn}remove${en} Worktree at ${t}`);try{this.cleanupWorktreePath(e,t)}catch{this.logger.warn(` [33mwarn${en} Failed to remove worktree (may need manual cleanup)`)}}}cleanupWorktreePath(e,t){if(this.isRegisteredWorktree(e,t)){a(`git worktree remove --force "${t}"`,{cwd:e,stdio:[`ignore`,`pipe`,`ignore`]});return}C(t,{recursive:!0,force:!0})}isRegisteredWorktree(e,t){try{return a(`git worktree list --porcelain`,{cwd:e,encoding:`utf-8`,stdio:[`ignore`,`pipe`,`ignore`]}).split(`
|
|
31
|
+
`).some(e=>e.startsWith(`worktree `)&&y(e.slice(9))===t)}catch{return!1}}async runHook(e,t,n,r,i,a){let o={inputs:r,matrix:{},secrets:i,env:n};for(let n of e.steps)if(!await this.stepRunner.runStep(n,o,{...a,workflowDir:t}))return!1;return!0}};const Y=`\x1B[0m`,X=`\x1B[1m`,rn=`\x1B[2m`,an=`\x1B[32m`,Z=`\x1B[33m`,Q=`\x1B[31m`,$=`\x1B[34m`,on=`SIGINT`,sn=`SIGTERM`,cn=`workflow_execution`,ln=`restart-from`,un=`user_prompt`,dn=`agent_decision`,fn=`workflow_dispatch`,pn=`Workflow post-cleanup failed`;var mn=class{logger;parser;serviceManager;stepRunner;jobRunner;triggerService;worktreeService;registry;outputLines=[];interruptedSignal=null;constructor(e,t={}){let n=e||{info:e=>process.stdout.write(`${e}\n`),error:e=>console.error(e),warn:e=>console.error(e)},r=e=>{this.outputLines.push(e),this.outputLines.length>5e3&&this.outputLines.shift()};this.logger={info:e=>{r(e),n.info(e)},error:e=>{r(e),n.error(e)},warn:e=>{r(e),n.warn(e)}},this.parser=t.parser??new $t,this.serviceManager=t.serviceManager??new rt(this.logger),this.stepRunner=t.stepRunner??new Ht(this.parser,this.logger),this.jobRunner=t.jobRunner??new ct(this.parser,this.stepRunner,this.logger),this.triggerService=t.triggerService??new Yt(this.stepRunner,this.logger),this.worktreeService=t.worktreeService??new nn(this.stepRunner,this.logger),this.registry=t.registry??new qe}async run(e){this.outputLines=[],this.interruptedSignal=null;let t=e.dryRun??!1,n=e.continueOnError??!1,r=!1,i=e=>{r||(r=!0,this.interrupt(e))},a=()=>i(on),o=()=>i(sn);process.on(on,a),process.on(sn,o);try{return await this.executeWorkflow(e,t,n,e.keepWorktree??!1)}catch(e){if(e instanceof F||this.isInterrupted())return await this.serviceManager.stopAll(),this.createInterruptedResult();throw e}finally{process.off(on,a),process.off(sn,o),await this.serviceManager.stopAll()}}interrupt(e,t={phase:cn}){if(this.interruptedSignal)return;this.interruptedSignal=e;let n=t.phase===cn?``:` (${t.phase})`;this.logger.info(`\n\n${Z}${X}Interrupted — shutting down...${Y}${n}`),this.triggerService.abortActivePrompt(e),this.stepRunner.stopActiveStep(e),this.serviceManager.stopAll()}isInterrupted(){return this.interruptedSignal===on||this.interruptedSignal===sn}createInterruptedResult(){return{exitCode:130,output:this.outputLines.join(`
|
|
32
|
+
`)}}parseFixRestartFrom(e,t){if(!e.startsWith(`---`))return{};let n=e.indexOf(`---`,3);if(n<0)return{};let r=e.slice(3,n);for(let e of r.split(`
|
|
33
|
+
`)){let n=e.trim();if(n.startsWith(`${ln}:`)){let e=n.slice(`${ln}:`.length).trim();return t.includes(e)?{restartFrom:e}:{invalidTarget:e}}}return{}}async pathExists(e){try{return await c(e),!0}catch(e){if(e.code===`ENOENT`)return!1;throw e}}async waitForServiceStartup(){await new Promise(e=>{setTimeout(e,2e3)})}resolveRunner(e){if(e.runner&&e.cliAgent&&e.runner!==e.cliAgent)throw Error(`Conflicting runner selectors: runner="${e.runner}" cliAgent="${e.cliAgent}"`);return e.runner??e.cliAgent}async executeWorkflow(e,t,n,r=!1){let i=y(e.workflowPath),o=this.parser.parseWorkflowFile(i),s=this.resolveRunner(e),c=null,l=null,d=null,f=_(i),p=f,g=null,ee=!1,v=!1,b=!!o.worktree,x=new Map,S=e.secretFile?this.parser.loadSecrets(e.secretFile):new Map,C={};try{let d=o.on?.[fn]?.inputs||{};for(let[e,t]of Object.entries(d))t.default&&x.set(e,t.default);if(e.inputs)for(let[t,n]of Object.entries(e.inputs))x.set(t,n);if(o.env)for(let[e,t]of Object.entries(o.env))C[e]=String(t);if(e.env)for(let[t,n]of Object.entries(e.env))C[t]=n;s&&(C.WORKFLOW_RUNNER=s,C.WORKFLOW_CLI_AGENT=s);let w=f;for(;w!==`/`;){if(await this.pathExists(y(w,`.git`))){f=w;break}w=_(w)}let ne=o[`max-workflows`];if(ne){let t=this.registry.resolveWorkspace(e.workspace,o.workspace),n=await this.registry.countRunningWorkflows(t);if(n>=ne)throw new Xe(t,n,ne)}let T=o[`launch-command`];if(T&&!e.skipLaunch){let t=this.buildLaunchCliCommand(e,i),n=e.name||o.name||te(i),r=e.name?n:`${n}-${this.parser.generateHumanReadableId()}`,s=T.replace(`{name}`,r).replace(`{command}`,t);this.logger.info(`${rn}Delegating via launch-command: ${s}${Y}`);try{return a(s,{stdio:`inherit`,cwd:f}),{exitCode:0,output:this.outputLines.join(`
|
|
34
|
+
`)}}catch(e){return{exitCode:e.status??1,output:this.outputLines.join(`
|
|
35
|
+
`)}}}let re=e.name||o.name||te(i),ie=e.name?re:`${re}-${this.parser.generateHumanReadableId()}`;if(c=await this.registry.createRun({displayName:ie,dryRun:t,workflowPath:i,workflowWorkspace:o.workspace,workspace:e.workspace}),C.WORKFLOW_RUN_DIR=c.runDir,C.WORKFLOW_WORKSPACE=c.workspace,C.CONTEXT_FILE&&(C.CONTEXT_FILE=c.contextPath,C.CHANGELOG_FILE=c.changelogPath),this.logger.info(`${X}${`═`.repeat(60)}${Y}`),this.logger.info(` ${X}run-workflow${Y} - ${c.displayName}`),this.logger.info(`${X}${`═`.repeat(60)}${Y}`),this.logger.info(` File: ${i}`),this.logger.info(` Workspace: ${c.workspace||`default`}`),this.logger.info(` Run Dir: ${c.runDir}`),this.logger.info(` CWD: ${f}`),x.size>0&&this.logger.info(` Inputs: ${[...x.entries()].map(([e,t])=>`${e}=${t}`).join(`, `)}`),S.size>0&&this.logger.info(` Secrets: ${[...S.keys()].join(`, `)}`),e.job&&this.logger.info(` Target: ${e.job}`),s&&this.logger.info(` Runner: ${s}`),o.pre){let e=o.pre.services?.length||0,t=o.pre.steps?.length||0;this.logger.info(` Pre: ${e} service(s), ${t} setup step(s)`)}if(o.post){let e=o.post.services?.length||0,t=o.post.steps?.length||0;this.logger.info(` Post: ${e} service(s), ${t} cleanup step(s)`)}b&&this.logger.info(` Worktree: enabled${r?` (keep on completion)`:``}`);let ae=o.on&&un in o.on?un:o.on?.[dn]?dn:fn;if(this.logger.info(` Trigger: ${ae}`),this.logger.info(` Mode: ${t?`dry-run`:`execute`}`),this.logger.info(`${X}${`═`.repeat(60)}${Y}`),!await this.triggerService.handleUserPrompt(o,e.prompt||null,C,f,t))return this.isInterrupted()?(l=this.createInterruptedResult(),l):(this.logger.error(`\n${Q}${X}Workflow aborted: no prompt provided${Y}`),l={exitCode:1,output:this.outputLines.join(`
|
|
36
|
+
`)},l);if(!await this.triggerService.handleAgentDecision(o,C,x,S,{runner:s,cliAgent:e.cliAgent,dryRun:t,continueOnError:n,workflowDir:f},e.prompt||null))return this.isInterrupted()?(l=this.createInterruptedResult(),l):(this.logger.info(`\n${Z}${X}Workflow skipped: agent decided nothing to do${Y}`),l={exitCode:2,output:this.outputLines.join(`
|
|
37
|
+
`)},l);if(p=f,b&&!t)o.worktree?.beforeCreate&&(this.logger.info(`\n ${$}${X}worktree${Y} ${X}beforeCreate${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.worktreeService.runHook(o.worktree.beforeCreate,f,C,x,S,{runner:s,cliAgent:e.cliAgent,dryRun:t,continueOnError:n})||(this.isInterrupted()?l=this.createInterruptedResult():(this.logger.error(`\n${Q}${X}Workflow aborted: worktree beforeCreate hook failed${Y}`),l={exitCode:1,output:this.outputLines.join(`
|
|
38
|
+
`)}))),l||(this.logger.info(`\n ${$}${X}worktree${Y} ${X}Creating worktree${Y}`),this.logger.info(` ${`─`.repeat(50)}`),g=this.worktreeService.createWorktree(f,c.displayName),f=g,C.WORKTREE_PATH=g,C.WORKFLOW_NAME=c.displayName,C.ORIGINAL_REPO_PATH=p,o.worktree?.afterCreate&&(this.logger.info(`\n ${$}${X}worktree${Y} ${X}afterCreate${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.worktreeService.runHook(o.worktree.afterCreate,g,C,x,S,{runner:s,cliAgent:e.cliAgent,dryRun:t,continueOnError:n})||(this.isInterrupted()?l=this.createInterruptedResult():(this.logger.error(`\n${Q}${X}Workflow aborted: worktree afterCreate hook failed${Y}`),l={exitCode:1,output:this.outputLines.join(`
|
|
39
|
+
`)}),v=!r)));else if(b&&t){let e=_(f),t=te(f),n=c.displayName.toLowerCase().replace(/[^a-z0-9-]+/g,`-`).replace(/^-|-$/g,``),r=y(e,`${t}-worktree`,n);this.logger.info(`\n ${$}${X}worktree${Y} ${X}(dry-run)${Y} Would create at ${r}`)}!l&&o.pre&&(await this.runPreBlock(o.pre,C,x,S,{runner:s,cliAgent:e.cliAgent,dryRun:t,continueOnError:n,workflowDir:f})||(this.isInterrupted()?l=this.createInterruptedResult():(this.logger.error(`\n${Q}${X}Workflow aborted: pre-conditions failed${Y}`),l={exitCode:1,output:this.outputLines.join(`
|
|
40
|
+
`)}),v=!!(g&&!r)));let oe=Number(C.MAX_RETRIES)||3,E=this.parser.resolveJobOrder(o.jobs,e.job||null);this.logger.info(`\n ${rn}Job order: ${E.join(` -> `)} (max retries: ${oe})${Y}`);let se=new Set,D=C.WORKFLOW_RUN_DIR?y(C.WORKFLOW_RUN_DIR,`fix.md`):null,O=0,k=0;for(;!l&&k<E.length;){let r=E[k],i=o.jobs[r],a=await this.jobRunner.runJob(r,i,{inputs:x,secrets:S,workflowEnv:C,workflowSystemPrompt:o[`system-prompt`],jobOrder:E,allJobs:o.jobs},{runner:s,cliAgent:e.cliAgent,dryRun:t,continueOnError:n,workflowDir:f,maxRetries:oe});if(this.isInterrupted()&&(l=this.createInterruptedResult()),!l&&!a&&(this.logger.error(`\n${Q}${X}Workflow failed at job "${r}"${Y}`),await this.serviceManager.stopAll(),l={exitCode:1,output:this.outputLines.join(`
|
|
41
|
+
`)}),!l){if(se.add(r),D&&await this.pathExists(D)&&!t){if(O++,O>3){this.logger.error(`\n${Q}${X}Fix loop limit reached (3 cycles). Aborting.${Y}`),l={exitCode:1,output:this.outputLines.join(`
|
|
42
|
+
`)};continue}let e=(await u(D,`utf-8`)).trim();await m(D);let{restartFrom:t,invalidTarget:n}=this.parseFixRestartFrom(e,E);if(this.logger.info(`\n${Z}${X}fix.md detected after "${r}" (cycle ${O}/3)${Y}`),this.logger.info(`${rn}${e.split(`
|
|
43
|
+
`)[0].slice(0,100)}${e.includes(`
|
|
44
|
+
`)?`...`:``}${Y}`),n&&this.logger.warn(`${Z}fix.md restart-from "${n}" is not a valid job (available: ${E.join(`, `)}), falling back to default${Y}`),C.CONTEXT_FILE&&await this.pathExists(C.CONTEXT_FILE)){let t=await u(C.CONTEXT_FILE,`utf-8`);await h(C.CONTEXT_FILE,`${t}\n\n---\n## Fix Request (cycle ${O})\n${e}`,`utf-8`)}let i=t?E.indexOf(t):-1,a=E.indexOf(`development`);k=i>=0?i:a>=0?a:1,this.logger.info(`${Z}Restarting from job "${E[k]}"${Y}\n`);continue}k++}}!l&&this.isInterrupted()&&(l=this.createInterruptedResult()),l||=(ee=!0,this.logger.info(`\n${X}${`═`.repeat(60)}${Y}`),this.logger.info(` ${an}${X}Workflow completed successfully${Y}`),this.logger.info(` Jobs run: ${[...se].join(`, `)}`),g&&this.logger.info(` Worktree: ${g}`),this.logger.info(`${X}${`═`.repeat(60)}${Y}\n`),{exitCode:0,output:this.outputLines.join(`
|
|
45
|
+
`)})}catch(e){if(d=e,e instanceof F||this.isInterrupted())l=this.createInterruptedResult();else throw e}finally{if(ee&&l?.exitCode===0&&b&&o.worktree?.beforeCompleted&&!t&&g&&(this.logger.info(`\n ${$}${X}worktree${Y} ${X}beforeCompleted${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.worktreeService.runHook(o.worktree.beforeCompleted,g,C,x,S,{runner:s,cliAgent:e.cliAgent,dryRun:t,continueOnError:n})||(this.isInterrupted()?l=this.createInterruptedResult():this.logger.warn(`\n${Z}${X}Warning: worktree beforeCompleted hook failed${Y}`))),o.post)try{await this.runPostBlock(o.post,C,x,S,{runner:s,cliAgent:e.cliAgent,dryRun:t,continueOnError:n,workflowDir:f})||(this.logger.error(`\n${Q}${X}${pn}${Y}`),(!l||l.exitCode===0||l.exitCode===2)&&(l={exitCode:1,output:this.outputLines.join(`
|
|
46
|
+
`)}))}catch(e){d??=e,this.logger.error(`\n${Q}${X}${pn}${Y}`),(!l||l.exitCode===0||l.exitCode===2)&&(l={exitCode:1,output:this.outputLines.join(`
|
|
47
|
+
`)})}ee&&l?.exitCode===0&&b&&o.worktree?.afterCompleted&&!t&&!r&&(this.logger.info(`\n ${$}${X}worktree${Y} ${X}afterCompleted${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.worktreeService.runHook(o.worktree.afterCompleted,p,C,x,S,{runner:s,cliAgent:e.cliAgent,dryRun:t,continueOnError:n})||(this.isInterrupted()?l=this.createInterruptedResult():this.logger.warn(`\n${Z}${X}Warning: worktree afterCompleted hook failed${Y}`))),v&&g&&this.worktreeService.removeWorktree(p,g),l&&={...l,output:this.outputLines.join(`
|
|
48
|
+
`)},c&&await this.finalizeRegistryRun(c,l,d)}return l??{exitCode:1,output:this.outputLines.join(`
|
|
49
|
+
`)}}async finalizeRegistryRun(e,t,n){let r=t?.exitCode===130||n instanceof F,i=t?.exitCode??(r?130:1),a=i===0||i===2?`completed`:`error`,o=i===0?`success`:i===2?`skipped`:r||this.isInterrupted()?`interrupted`:`failed`,s=n instanceof Error?n.message:i===1?this.lastMeaningfulOutputLine():void 0;await this.registry.finalizeRun(e,{errorMessage:s,exitCode:i,outcome:o,stage:a})}buildLaunchCliCommand(e,t){let n=[`bun`,`packages/mcp/workflow-mcp/src/cli.ts`,`run-workflow`,t,`--skip-launch`],r=this.resolveRunner(e);if(e.job&&n.push(`--job`,e.job),r&&n.push(`--runner`,r),e.dryRun&&n.push(`--dry-run`),e.continueOnError&&n.push(`--continue-on-error`),e.keepWorktree&&n.push(`--keep-worktree`),e.prompt&&n.push(`--prompt`,`'${e.prompt.replace(/'/g,`'\\''`)}'`),e.name&&n.push(`--name`,`'${e.name}'`),e.workspace&&n.push(`--workspace`,`'${e.workspace}'`),e.secretFile&&n.push(`--secret-file`,e.secretFile),e.inputs)for(let[t,r]of Object.entries(e.inputs))n.push(`--input`,`${t}=${r}`);if(e.env)for(let[t,r]of Object.entries(e.env))n.push(`--env`,`${t}=${r}`);return n.join(` `)}lastMeaningfulOutputLine(){return[...this.outputLines].reverse().find(e=>e.trim().length>0)}async runPreBlock(e,t,n,r,i){if(this.logger.info(`\n ${$}${X}pre${Y} ${X}Pre-conditions${Y}`),this.logger.info(` ${`─`.repeat(50)}`),t.CONTEXT_FILE&&await l(_(y(i.workflowDir,t.CONTEXT_FILE)),{recursive:!0}),e.services&&e.services.length>0){this.logger.info(`\n ${$}${X}services${Y}`);for(let n of e.services)if(await this.serviceManager.startService(n,t,i.workflowDir,i.dryRun)&&n[`ready-check`]&&!i.dryRun){let e=await this.serviceManager.waitForServiceReady(n,i.workflowDir,t,()=>this.interruptedSignal!==null);if(this.isInterrupted())return!1;if(!e&&!i.continueOnError)return await this.serviceManager.stopAll(),!1}!i.dryRun&&this.serviceManager.getRunningServices().length>0&&await this.waitForServiceStartup()}if(e.steps&&e.steps.length>0){this.logger.info(`\n ${$}${X}setup${Y}`);let a={inputs:n,matrix:{},secrets:r,env:t};for(let t of e.steps)if(!await this.stepRunner.runStep(t,a,i))return this.logger.error(`\n ${Q}Pre-condition step failed${Y}`),await this.serviceManager.stopAll(),!1}return this.logger.info(`\n ${an}Pre-conditions ready${Y}`),!0}async runPostBlock(e,t,n,r,i){this.logger.info(`\n ${$}${X}post${Y} ${X}Cleanup${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.serviceManager.stopAll();try{if(e.services&&e.services.length>0){this.logger.info(`\n ${$}${X}services${Y}`);for(let n of e.services)if(await this.serviceManager.startService(n,t,i.workflowDir,i.dryRun)&&n[`ready-check`]&&!i.dryRun&&!await this.serviceManager.waitForServiceReady(n,i.workflowDir,t,()=>!1)&&!i.continueOnError)return!1;!i.dryRun&&this.serviceManager.getRunningServices().length>0&&await this.waitForServiceStartup()}if(e.steps&&e.steps.length>0){this.logger.info(`\n ${$}${X}cleanup${Y}`);let a={inputs:n,matrix:{},secrets:r,env:t};for(let t of e.steps)if(!await this.stepRunner.runStep(t,a,i))return this.logger.error(`\n ${Q}Post-cleanup step failed${Y}`),!1}return this.logger.info(`\n ${an}Cleanup complete${Y}`),!0}finally{await this.serviceManager.stopAll()}}};const hn=r.object({workflowPath:r.string().describe(`Path to the workflow YAML file (e.g., .github/workflows/deploy.yml)`),runner:r.string().optional().describe(`Preferred runner key for step command maps (e.g. ollama, claude, codex)`),cliAgent:r.string().optional().describe(`Deprecated alias for runner. Preferred runner command key for step command maps.`),job:r.string().optional().describe(`Run only this job (and its dependencies)`),inputs:r.record(r.string(),r.string()).optional().describe(`Workflow dispatch inputs as key-value pairs`),env:r.record(r.string(),r.string()).optional().describe(`Extra environment variables as key-value pairs`),secretFile:r.string().optional().describe(`Path to a dotenv-style secrets file`),dryRun:r.boolean().optional().describe(`Print steps without executing`),continueOnError:r.boolean().optional().describe(`Continue past step failures`),keepWorktree:r.boolean().optional().describe(`Keep worktree on completion (skip merge and cleanup for retry)`),prompt:r.string().optional().describe(`User prompt for user_prompt trigger workflows`),name:r.string().optional().describe(`Name for the workflow run context directory`),workspace:r.string().optional().describe(`Workspace for workflow registry storage`)});var gn=class e{static TOOL_NAME=`run_workflow`;constructor(e=new mn){this.service=e}getInputSchema(){return hn}getDefinition(){return{name:e.TOOL_NAME,description:`Run a GitHub Actions workflow file locally. Parses the workflow YAML and executes run steps on the host machine, respecting job dependencies, matrix strategies, environment variables, and workflow_dispatch inputs.`,inputSchema:r.toJSONSchema(hn)}}async execute(e){try{let t=hn.parse(e);if(t.runner&&t.cliAgent&&t.runner!==t.cliAgent)throw Error(`Conflicting runner selectors: runner="${t.runner}" cliAgent="${t.cliAgent}"`);let n={cliAgent:t.cliAgent,runner:t.runner??t.cliAgent,workflowPath:t.workflowPath,job:t.job,inputs:t.inputs,env:t.env,secretFile:t.secretFile,dryRun:t.dryRun,continueOnError:t.continueOnError,keepWorktree:t.keepWorktree,prompt:t.prompt,name:t.name,workspace:t.workspace},r=await this.service.run(n);return r.exitCode!==0&&r.exitCode!==2?{content:[{type:`text`,text:`Workflow failed (exit code ${r.exitCode}):\n\n${r.output}`}],isError:!0}:{content:[{type:`text`,text:r.output||`Workflow completed successfully.`}]}}catch(e){return{content:[{type:`text`,text:`Error: ${e instanceof Error?e.message:`Unknown error`}`}],isError:!0}}}},_n=class e{static TOOL_NAME=`schedule-cron`;constructor(e=new Se){this.service=e}getInputSchema(){return _e}getDefinition(){return{name:e.TOOL_NAME,description:`Schedule a headless Claude Code or Codex CLI run as a system cron job. Specify either a cron expression or an interval in minutes.`,inputSchema:r.toJSONSchema(_e)}}async execute(t){try{let e=_e.parse(t),n=await this.service.schedule(e);return{content:[{type:`text`,text:JSON.stringify(n,null,2)}]}}catch(n){let r=new O(`Failed to schedule cron job.`,`SCHEDULE_CRON_TOOL_FAILED`,{tool:e.TOOL_NAME,name:t.name,cwd:t.cwd},{cause:n});return console.error(`[${e.TOOL_NAME}] ${r.message}`,r.context),{content:[{type:`text`,text:JSON.stringify({code:r.code,context:r.context,message:r.message},null,2)}],isError:!0}}}};const vn=[gn.TOOL_NAME,Ye.TOOL_NAME,_n.TOOL_NAME,Ce.TOOL_NAME];function yn(){let r=new e({name:`workflow-mcp`,version:`0.1.0`},{capabilities:{tools:{}}}),i=new gn,a=new Ye,o=new _n,s=new Ce;return r.setRequestHandler(n,async()=>({tools:[i.getDefinition(),a.getDefinition(),o.getDefinition(),s.getDefinition()]})),r.setRequestHandler(t,async e=>{let{name:t,arguments:n}=e.params;if(t===gn.TOOL_NAME)return await i.execute(n);if(t===Ye.TOOL_NAME)return await a.execute(n);if(t===_n.TOOL_NAME)return await o.execute(n);if(t===Ce.TOOL_NAME)return await s.execute();throw new D(t,vn)}),r}var bn=class{server;transport=null;constructor(e){this.server=e}async start(){this.transport=new se,await this.server.connect(this.transport),console.error(`workflow-mcp MCP server started on stdio`)}async stop(){this.transport&&=(await this.transport.close(),null)}};export{qe as a,_e as c,ft as i,yn as n,Se as o,mn as r,fe as s,bn as t};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
let e=require(`@modelcontextprotocol/sdk/server/index.js`),t=require(`@modelcontextprotocol/sdk/types.js`),n=require(`zod`),r=require(`node:child_process`),i=require(`node:util`),a=require(`node:fs/promises`),o=require(`node:os`),s=require(`node:path`),c=require(`node:fs`),l=require(`@agimon-ai/foundation-port-registry`),u=require(`@agimon-ai/foundation-process-registry`),d=require(`node:crypto`),f=require(`node:readline`),p=require(`js-yaml`),m=require(`@modelcontextprotocol/sdk/server/stdio.js`);var h=class extends Error{code=`WORKFLOW_TOOL_NOT_FOUND`;constructor(e,t){super(`Unknown tool "${e}". Available tools: ${t.join(`, `)}`),this.toolName=e,this.availableTools=t,this.name=`WorkflowToolNotFoundError`}},g=class extends Error{constructor(e,t,n={},r){super(e,r),this.code=t,this.context=n,this.name=`WorkflowToolError`}},_=class extends Error{constructor(e,t,n={},r){super(e,r),this.code=t,this.context=n,this.name=`CronError`}};const v=`# workflow-mcp:cron:`,y=`crontab`,b=`claude`,x=`codex`,S=`--cwd`,C=`CRON_WRITE_FAILED`,ee=n.z.enum([b,x]),te=n.z.object({name:n.z.string().min(1),cwd:n.z.string().min(1),cli:ee,schedule:n.z.string().min(1),prompt:n.z.string().optional(),promptFile:n.z.string().optional(),createdAt:n.z.string().min(1)}),w=n.z.object({name:n.z.string().min(1),cwd:n.z.string().min(1),cli:ee.optional(),prompt:n.z.string().optional(),promptFile:n.z.string().optional(),schedule:n.z.string().optional(),intervalMinutes:n.z.number().positive().optional()});function T(e){return`'${e.replace(/'/g,`'\\''`)}'`}function ne(e,t,n,r){let i=n?T(n):r?`"$(cat ${T(r)})"`:void 0;if(e===`codex`){let e=[x,`--approval-mode`,`full-auto`];return i&&e.push(`-q`,i),e.push(S,T(t)),e.join(` `)}let a=[b,`--dangerously-skip-permissions`];return i&&a.push(`-p`,i),a.push(S,T(t)),a.join(` `)}function E(e){if(e<=0)throw Error(`Interval must be a positive number of minutes`);if(e<60)return`*/${e} * * * *`;let t=Math.floor(e/60);return t<24?`0 */${t} * * *`:`0 0 */${Math.floor(t/24)} * *`}const re=(0,i.promisify)(r.execFile);var D=class{logger;constructor(e){this.logger=e??{info:e=>process.stdout.write(`${e}\n`),error:e=>console.error(e),warn:e=>console.error(e)}}async readCrontab(){try{let{stdout:e}=await re(y,[`-l`]);return e}catch(e){let t=e.code;if(t===`ENOENT`||(e.stderr??``).includes(`no crontab for`))return``;throw this.logger.error(`[CronService.readCrontab] failed exitCode=${t}`),new _(`Failed to read current crontab.`,`CRON_READ_FAILED`,{exitCode:t},{cause:e})}}async writeCrontab(e){let t=``,n=(0,r.execFile)(y,[`-`]);n.stderr?.on(`data`,e=>{t+=e}),n.stdin?.write(e),n.stdin?.end(),await new Promise((r,i)=>{n.on(`close`,n=>{n===0?r():(this.logger.error(`[CronService.writeCrontab] failed exitCode=${n} stderr=${t}`),i(new _(`crontab rejected input (exit ${n}).`,C,{exitCode:n,stderr:t,contentLength:e.length})))}),n.on(`error`,e=>{this.logger.error(`[CronService.writeCrontab] spawn failed: ${e.message}`),i(new _(`Failed to spawn crontab process.`,C,{},{cause:e}))})})}async schedule(e){let t=e.cli??`claude`;if(!e.schedule&&!e.intervalMinutes)throw new _(`Either schedule (cron expression) or intervalMinutes must be provided.`,`CRON_INVALID_INPUT`,{name:e.name});let n=e.schedule??E(e.intervalMinutes),r=ne(t,e.cwd,e.prompt,e.promptFile),i=new Date().toISOString(),a=te.parse({name:e.name,cwd:e.cwd,cli:t,schedule:n,prompt:e.prompt,promptFile:e.promptFile,createdAt:i}),o=await this.readCrontab(),s=this.removeCronBlock(o,e.name),c=`${`${v}${e.name}`}\n${`# workflow-mcp:meta:${JSON.stringify(a)}`}\n${`${n} ${r}`}`,l=s.trimEnd()?`${s.trimEnd()}\n${c}\n`:`${c}\n`;return await this.writeCrontab(l),this.logger.info(`Scheduled cron "${e.name}" [${n}]`),a}async list(){let e=await this.readCrontab(),t=[],n=e.split(`
|
|
2
|
+
`);for(let e=0;e<n.length;e++){let r=n[e];if(r.startsWith(`# workflow-mcp:meta:`))try{let e=r.slice(20),n=te.parse(JSON.parse(e));t.push(n)}catch(t){this.logger.warn(`[CronService.list] skipping malformed metadata at line ${e+1}: ${t.message}`)}}return t}async remove(e){let t=await this.readCrontab(),n=this.removeCronBlock(t,e);return n===t?!1:(await this.writeCrontab(n),this.logger.info(`Removed cron "${e}"`),!0)}removeCronBlock(e,t){let n=e.split(`
|
|
3
|
+
`),r=[],i=`${v}${t}`,a=!1;for(let e of n){if(e===i){a=!0;continue}if(a){if(e.startsWith(`# workflow-mcp:meta:`))continue;a=!1;continue}r.push(e)}return r.join(`
|
|
4
|
+
`)}},O=class e{static TOOL_NAME=`list-crons`;constructor(e=new D){this.service=e}getInputSchema(){return n.z.object({})}getDefinition(){return{name:e.TOOL_NAME,description:`List all cron jobs scheduled via workflow-mcp, showing name, schedule, CLI, cwd, and prompt.`,inputSchema:n.z.toJSONSchema(n.z.object({}))}}async execute(){try{let e=await this.service.list();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}catch(t){let n=new g(`Failed to list cron jobs.`,`LIST_CRONS_TOOL_FAILED`,{tool:e.TOOL_NAME},{cause:t});return console.error(`[${e.TOOL_NAME}] ${n.message}`,n.context),{content:[{type:`text`,text:JSON.stringify({code:n.code,context:n.context,message:n.message},null,2)}],isError:!0}}}},k=class extends Error{code=`INVALID_WORKFLOW_RUN_RECORD`;constructor(e,t,n){super(`Invalid workflow run record at "${e}": ${t}`,n),this.recordPath=e,this.name=`InvalidWorkflowRunRecordError`}},ie=class extends Error{code=`WORKFLOW_RUN_CONFLICT`;constructor(e,t){super(`Workflow "${t}" is already running in workspace "${e}"`),this.workspace=e,this.runKey=t,this.name=`WorkflowRunConflictError`}};const ae=n.z.record(n.z.string(),n.z.coerce.string()),oe=n.z.object({description:n.z.string().optional(),required:n.z.boolean().optional(),default:n.z.string().optional(),type:n.z.string().optional(),options:n.z.array(n.z.string()).optional()}),se=n.z.record(n.z.string(),n.z.string()),ce=n.z.record(n.z.string(),n.z.string()),le=n.z.union([n.z.string(),ce]),ue=n.z.object({"fail-fast":n.z.boolean().optional(),matrix:n.z.looseObject({include:n.z.array(se).optional()}).optional()}),A=n.z.object({name:n.z.string().optional(),uses:n.z.string().optional(),run:le.optional(),interactiveRun:le.optional(),"timeout-minutes":n.z.number().optional(),env:ae.optional(),with:n.z.record(n.z.string(),n.z.string()).optional(),"working-directory":n.z.string().optional(),if:n.z.string().optional(),"continue-on-error":n.z.boolean().optional(),"timeout-retries":n.z.number().optional(),id:n.z.string().optional()}),de=n.z.object({name:n.z.string(),run:n.z.string(),env:ae.optional(),"working-directory":n.z.string().optional(),"ready-check":n.z.union([n.z.string(),n.z.boolean()]).optional(),"ready-timeout":n.z.number().optional(),host:n.z.string().optional(),port:n.z.number().int().min(1).max(65535).optional(),"port-range":n.z.object({min:n.z.number().int().min(1).max(65535),max:n.z.number().int().min(1).max(65535)}).optional(),"service-type":n.z.enum([`tool`,`service`]).optional()}),fe=n.z.object({services:n.z.array(de).optional(),steps:n.z.array(A).optional()}),pe=n.z.object({steps:n.z.array(A).optional()}),me=n.z.object({"runs-on":n.z.string().optional(),needs:n.z.union([n.z.string(),n.z.array(n.z.string())]).optional(),extends:n.z.union([n.z.string(),n.z.array(n.z.string())]).optional(),description:n.z.string().optional(),strategy:ue.optional(),steps:n.z.array(A),preJob:pe.optional(),postJob:pe.optional(),env:ae.optional(),if:n.z.string().optional(),"system-prompt":n.z.string().optional(),context:n.z.string().optional()}),he=n.z.object({steps:n.z.array(A)}),ge=n.z.object({steps:n.z.array(A)}),_e=n.z.object({beforeCreate:ge.optional(),afterCreate:ge.optional(),beforeCompleted:ge.optional(),afterCompleted:ge.optional()}),ve=n.z.object({name:n.z.string().optional(),workspace:n.z.string().optional(),imports:n.z.array(n.z.string()).optional(),"system-prompt":n.z.string().optional(),"max-workflows":n.z.number().int().positive().optional(),"launch-command":n.z.string().optional(),on:n.z.looseObject({workflow_dispatch:n.z.object({inputs:n.z.record(n.z.string(),oe).optional()}).nullable().optional(),agent_decision:he.optional()}).optional(),env:ae.optional(),pre:fe.optional(),post:fe.optional(),worktree:_e.optional(),jobs:n.z.record(n.z.string(),me)}),ye=n.z.enum([`running`,`completed`,`error`]),be=n.z.enum([`success`,`skipped`,`failed`,`interrupted`]),xe=n.z.object({displayName:n.z.string(),dryRun:n.z.boolean(),errorMessage:n.z.string().optional(),exitCode:n.z.number().optional(),finishedAt:n.z.string().optional(),outcome:be.optional(),pid:n.z.number().int().positive().optional(),runKey:n.z.string(),stale:n.z.boolean().optional(),staleReason:n.z.string().optional(),stage:ye,startedAt:n.z.string(),workflowPath:n.z.string(),workspace:n.z.string()});n.z.object({changelogPath:n.z.string(),contextPath:n.z.string(),displayName:n.z.string(),recordPath:n.z.string(),runDir:n.z.string(),runKey:n.z.string(),workspace:n.z.string()});const Se=`default`,j=`running`,Ce=`completed`,M=`error`,we=`workspaces`,N=`run.json`,Te=`Workflow process is no longer running`,Ee={running:0,error:1,completed:2};var De=class{constructor(e=(0,s.resolve)((0,o.homedir)(),`.workflow-mcp`)){this.homeDir=e}resolveWorkspace(e,t){return this.slugifySegment(e||t||Se,Se)}async createRun(e){let t=this.resolveWorkspace(e.workspace,e.workflowWorkspace),n=this.slugifySegment(e.displayName,`workflow-run`);await this.ensureWorkspaceStructure(t);let r=await this.findReusableRunStage(t,n);if(r===j)throw new ie(t,n);let i=this.getRunDirectory(t,j,n);r?(await(0,a.rm)(i,{recursive:!0,force:!0}),await(0,a.rename)(this.getRunDirectory(t,r,n),i),await this.removeFileIfExists((0,s.resolve)(i,`fix.md`))):await(0,a.mkdir)(i,{recursive:!0});let o={displayName:e.displayName,dryRun:e.dryRun,runKey:n,stage:j,startedAt:new Date().toISOString(),pid:e.pid??process.pid,workflowPath:e.workflowPath,workspace:t},c=(0,s.resolve)(i,N);return await this.writeRunRecord(c,o),{changelogPath:(0,s.resolve)(i,`changelog.md`),contextPath:(0,s.resolve)(i,`context.md`),displayName:e.displayName,recordPath:c,runDir:i,runKey:n,workspace:t}}async finalizeRun(e,t){await this.ensureWorkspaceStructure(e.workspace);let n=this.getRunDirectory(e.workspace,t.stage,e.runKey),r=await this.readRunRecord(e.recordPath),i={...r,errorMessage:t.errorMessage,exitCode:t.exitCode,finishedAt:new Date().toISOString(),outcome:t.outcome,pid:r.pid,stage:t.stage};e.runDir!==n&&(await(0,a.rm)(n,{recursive:!0,force:!0}),await(0,a.rename)(e.runDir,n));let o=(0,s.resolve)(n,N);return await this.writeRunRecord(o,i),{...e,recordPath:o,runDir:n}}async readRunRecord(e){try{return this.validateRunRecord(JSON.parse(await(0,a.readFile)(e,`utf-8`)),e)}catch(t){throw t instanceof k?t:new k(e,t instanceof Error?t.message:`Unknown parse failure`,{cause:t})}}async listRuns(e){let t=e?[this.resolveWorkspace(e)]:await this.listWorkspaceNames();return(await Promise.all(t.map(async e=>this.listWorkspaceRuns(e)))).flat().sort((e,t)=>this.compareRunRecords(e,t))}async countRunningWorkflows(e){let t=this.getRunStageDirectory(e,j);if(!await this.pathExists(t))return 0;let n=await(0,a.readdir)(t,{withFileTypes:!0}),r=0;for(let e of n){if(!e.isDirectory())continue;let n=(0,s.resolve)(t,e.name,N);if(await this.pathExists(n))try{let e=await this.readRunRecord(n);e.pid&&this.isProcessAlive(e.pid)&&r++}catch{}}return r}async ensureWorkspaceStructure(e){await(0,a.mkdir)(this.getRunStageDirectory(e,j),{recursive:!0}),await(0,a.mkdir)(this.getRunStageDirectory(e,Ce),{recursive:!0}),await(0,a.mkdir)(this.getRunStageDirectory(e,M),{recursive:!0})}async findRunStage(e,t){for(let n of[j,Ce,M])if(await this.pathExists(this.getRunDirectory(e,n,t)))return n;return null}async listWorkspaceRuns(e){let t=new Map;for(let n of[j,Ce,M]){let r=this.getRunStageDirectory(e,n);if(!await this.pathExists(r))continue;let i=await(0,a.readdir)(r,{withFileTypes:!0});for(let a of i){if(!a.isDirectory())continue;let i=(0,s.resolve)(r,a.name,N);if(!await this.pathExists(i))continue;let o=await this.readListableRunRecord(e,n,a.name,i);o&&t.set(o.runKey,o)}}return[...t.values()]}async findReusableRunStage(e,t){let n=await this.findRunStage(e,t);return n===j?(await this.reconcileStaleRunningRecord(e,t),this.findRunStage(e,t)):n}async inspectRunningRecord(e,t){let n=(0,s.resolve)(this.getRunDirectory(e,j,t),N),r=await this.readRunRecord(n);return!r.pid||this.isProcessAlive(r.pid)?r:{...r,stale:!0,staleReason:`${Te}: pid ${r.pid}`}}async readListableRunRecord(e,t,n,r){try{return t===j?await this.inspectRunningRecord(e,n):await this.readRunRecord(r)}catch(e){if(e instanceof k)return null;throw e}}async reconcileStaleRunningRecord(e,t){let n=this.getRunDirectory(e,j,t),r=(0,s.resolve)(n,N),i=await this.readRunRecord(r);if(!i.pid||this.isProcessAlive(i.pid))return i;let o=this.getRunDirectory(e,M,t),c={...i,errorMessage:`${Te}: pid ${i.pid}`,exitCode:130,finishedAt:new Date().toISOString(),outcome:`interrupted`,stage:M};return await(0,a.rm)(o,{recursive:!0,force:!0}),await(0,a.rename)(n,o),await this.writeRunRecord((0,s.resolve)(o,N),c),c}async listWorkspaceNames(){let e=(0,s.resolve)(this.homeDir,we);return await this.pathExists(e)?(await(0,a.readdir)(e,{withFileTypes:!0})).filter(e=>e.isDirectory()).map(e=>e.name).sort((e,t)=>e.localeCompare(t)):[]}compareRunRecords(e,t){let n=Ee[e.stage]-Ee[t.stage];if(n!==0)return n;let r=e.finishedAt??e.startedAt,i=(t.finishedAt??t.startedAt).localeCompare(r);if(i!==0)return i;let a=e.workspace.localeCompare(t.workspace);return a===0?e.runKey.localeCompare(t.runKey):a}async writeRunRecord(e,t){await(0,a.writeFile)(e,`${JSON.stringify(t,null,2)}\n`,`utf-8`)}validateRunRecord(e,t){let n=xe.safeParse(e);if(!n.success)throw new k(t,n.error.message);return n.data}getRunStageDirectory(e,t){return(0,s.resolve)(this.homeDir,we,e,t)}getRunDirectory(e,t,n){return(0,s.resolve)(this.getRunStageDirectory(e,t),n)}async pathExists(e){try{return await(0,a.access)(e),!0}catch(e){if(e.code===`ENOENT`)return!1;throw e}}async removeFileIfExists(e){try{await(0,a.unlink)(e)}catch(e){if(e.code!==`ENOENT`)throw e}}slugifySegment(e,t){return e.trim().toLowerCase().replace(/[^a-z0-9]+/g,`-`).replace(/^-+|-+$/g,``)||t}isProcessAlive(e){try{return process.kill(e,0),!0}catch(e){let t=e.code;if(t===`ESRCH`)return!1;if(t===`EPERM`)return!0;throw e}}};const Oe=n.z.object({workspace:n.z.string().optional().describe(`Optional workspace filter. When omitted, runs from all workspaces are returned.`)});var ke=class e{static TOOL_NAME=`list_workflow_statuses`;constructor(e=new De){this.registry=e}getInputSchema(){return Oe}getDefinition(){return{name:e.TOOL_NAME,description:`List tracked workflow runs from the local workflow registry, including their workspace, stage, outcome, and timestamps.`,inputSchema:n.z.toJSONSchema(Oe)}}async execute(e={}){try{let t=Oe.parse(e),n=await this.registry.listRuns(t.workspace);return{content:[{type:`text`,text:JSON.stringify(n,null,2)}]}}catch(t){let n=new g(`Failed to list workflow statuses.`,`LIST_WORKFLOW_STATUSES_TOOL_FAILED`,{workspace:e.workspace??`all`},{cause:t});return{content:[{type:`text`,text:JSON.stringify({code:n.code,context:n.context,message:n.message},null,2)}],isError:!0}}}},Ae=class extends Error{code=`WORKFLOW_CAPACITY_EXCEEDED`;constructor(e,t,n){super(`Workspace "${e}" is at capacity: ${t}/${n} workflows running.\n → Check running workflows: list-workflow-statuses --workspace ${e}\n → Wait for a running workflow to complete, or cancel one before dispatching again.`),this.workspace=e,this.running=t,this.max=n,this.name=`WorkflowCapacityError`}},P=class extends Error{code=`WORKFLOW_INTERRUPTED`;context;constructor(e,t={}){let n=t.context?` during ${t.context.phase}`:``;super(`Workflow interrupted by ${e}${n}`,{cause:t.cause??t.context}),this.signal=e,this.name=`WorkflowInterruptedError`,this.context=t.context}};const F=`\x1B[0m`,je=`\x1B[1m`,Me=`\x1B[2m`,Ne=`\x1B[31m`,I=`\x1B[34m`,Pe=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function Fe(e){let t=(0,s.resolve)(e);for(;;){for(let e of Pe)if((0,c.existsSync)((0,s.join)(t,e)))return t;let n=(0,s.dirname)(t);if(n===t)return e;t=n}}var Ie=class{runningServices=[];processRegistry=new u.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH);portRegistry=new l.PortRegistryService(process.env.PORT_REGISTRY_PATH);constructor(e){this.logger=e}resolveWorkingDirectory(e,t){return e[`working-directory`]?(0,s.resolve)(t,e[`working-directory`]):t}getRunningServices(){return this.runningServices}async startService(e,t,n,i){let a=e.run;if(i)return this.logger.info(` ${I}dry ${F} ${e.name}`),this.logger.info(` ${Me}$ ${a}${F}`),null;this.logger.info(` ${I}start${F} ${e.name}`),this.logger.info(` ${Me}$ ${a}${F}`);let o={...process.env,...t};if(e.env)for(let[t,n]of Object.entries(e.env))o[t]=String(n);let s=this.resolveWorkingDirectory(e,n),c=Fe(s),l=o.NODE_ENV??process.env.NODE_ENV??`development`,u=e.host??`127.0.0.1`,d;if(e.port!==void 0||e[`port-range`]){let t=await this.portRegistry.reservePort({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,preferredPort:e.port,portRange:e[`port-range`],host:u,force:!0});if(!t.success||t.port===void 0)throw Error(t.error??`Failed to reserve port for background service ${e.name}`);d=t.port,o.PORT??=String(d),o.SERVICE_PORT??=String(d),o.HOST??=u,o.SERVICE_HOST??=u}let f=(0,r.spawn)(a,[],{stdio:[`ignore`,`pipe`,`pipe`],cwd:s,env:{...o,FORCE_COLOR:`1`},shell:`/bin/zsh`,detached:!0});if(f.pid===void 0)throw d!==void 0&&await this.portRegistry.releasePort({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,force:!0}),Error(`Failed to spawn background service ${e.name}`);let p=`${I}[${e.name}]${F}`;f.stdout?.on(`data`,e=>{for(let t of e.toString().split(`
|
|
5
|
+
`))t.trim()&&process.stdout.write(`${p} ${t}\n`)}),f.stderr?.on(`data`,e=>{for(let t of e.toString().split(`
|
|
6
|
+
`))t.trim()&&process.stderr.write(`${p} ${t}\n`)}),f.on(`error`,e=>{this.logger.error(`${p} Process error: ${e.message}`)});let m=async()=>{d!==void 0&&await this.portRegistry.releasePort({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,force:!0})};if(f.pid!==void 0){let t=await this.processRegistry.registerProcess({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,pid:f.pid,port:d,host:d===void 0?void 0:u,command:a,args:[],metadata:{workingDirectory:s,readyCheck:e[`ready-check`]},force:!0});if(!t.success){try{process.kill(-f.pid,`SIGTERM`)}catch{f.kill(`SIGTERM`)}throw d!==void 0&&await this.portRegistry.releasePort({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,force:!0}),Error(t.error??`Failed to register process for background service ${e.name}`)}m=async()=>{let t=await this.processRegistry.releaseProcess({repositoryPath:c,serviceName:e.name,serviceType:e[`service-type`]??`tool`,environment:l,pid:f.pid,kill:!0,releasePort:!0,force:!0});if(!t.success&&!t.error?.includes(`No matching process entry`))throw Error(t.error??`Failed to release ${e.name}`)}}let h={name:e.name,process:f,env:o,release:m};return this.runningServices.push(h),h}async waitForServiceReady(e,t,n,i){if(!e[`ready-check`]||typeof e[`ready-check`]!=`string`)return!0;let a=(e[`ready-timeout`]||30)*1e3,o=Date.now(),s=e[`ready-check`],c=this.resolveWorkingDirectory(e,t),l=this.runningServices.find(t=>t.name===e.name)?.env??{...process.env,...n};for(this.logger.info(` ${Me}wait${F} Waiting for ${e.name} to be ready...`);Date.now()-o<a;){if(i?.())return!1;try{return(0,r.execSync)(s,{stdio:`ignore`,cwd:c,env:l,shell:`/bin/zsh`,timeout:5e3}),this.logger.info(` [32mready${F} ${e.name}`),!0}catch{if(i?.())return!1}await new Promise(e=>{setTimeout(e,1e3)})}return i?.()||this.logger.error(` ${Ne}timeout${F} ${e.name} not ready after ${e[`ready-timeout`]||30}s`),!1}async stopAll(){if(this.runningServices.length!==0){this.logger.info(`\n ${I}${je}pre${F} ${je}Stopping services${F}`),this.logger.info(` ${`─`.repeat(50)}`);for(let e of this.runningServices){if(!e.process.killed&&e.process.pid&&this.logger.info(` ${Me}stop${F} ${e.name} (pid ${e.process.pid})`),e.release)try{await e.release();continue}catch(t){this.logger.warn(` ${Ne}warn${F} Failed registry cleanup for ${e.name}: ${t instanceof Error?t.message:String(t)}`)}if(!e.process.killed&&e.process.pid)try{process.kill(-e.process.pid,`SIGTERM`)}catch{e.process.kill(`SIGTERM`)}}this.runningServices=[]}}};const L=`\x1B[0m`,Le=`\x1B[1m`,R=`\x1B[2m`,Re=`\x1B[33m`,ze=`WORKFLOW_JOB_ID`,Be=`PROCESS_REGISTRY_TAG`;var Ve=class{constructor(e,t,n){this.parser=e,this.stepRunner=t,this.logger=n}async runJob(e,t,n,r){let i=this.parser.expandMatrix(t),a=r.maxRetries;for(let o of i){let i=(0,d.randomUUID)(),l=Object.keys(o).length>0?` ${R}(${Object.entries(o).map(([e,t])=>`${e}=${t}`).join(`, `)})${L}`:``;this.logger.info(`\n [35m${Le}job${L} ${Le}${e}${L}${l}`),this.logger.info(` ${`─`.repeat(50)}`);let u={...n.workflowEnv,[ze]:i,[Be]:i};if(t.env)for(let[e,r]of Object.entries(t.env))u[e]=this.parser.interpolate(String(r),{inputs:n.inputs,matrix:o,secrets:n.secrets,env:u});u[ze]=i,u[Be]=i,this.logger.info(` ${R}jobid${L} ${i}`);let f=[];if(u.WORKFLOW_RUN_DIR){let t=n.jobOrder.map(e=>{let t=n.allJobs[e]?.description;return t?` - ${e}: ${t}`:` - ${e}`}).join(`
|
|
7
|
+
`);f.push(`You are currently running job "${e}" in the following workflow pipeline:\n${t}\n\nIf you find bugs, missing features, or issues that need fixes from a previous job, write to ${u.WORKFLOW_RUN_DIR}/fix.md with a YAML frontmatter restart-from field specifying which job should handle the fix, followed by a detailed description. Example:\n---\nrestart-from: development\n---\nDescription of what needs to be fixed...\n\nThe workflow runner will restart from the specified job. Only create fix.md for issues that require code changes — fix minor issues yourself.`)}if(n.workflowSystemPrompt&&f.push(this.parser.interpolate(n.workflowSystemPrompt,{inputs:n.inputs,matrix:o,secrets:n.secrets,env:u})),t[`system-prompt`]&&f.push(this.parser.interpolate(t[`system-prompt`],{inputs:n.inputs,matrix:o,secrets:n.secrets,env:u})),f.length>0){let e=f.join(`
|
|
8
|
+
|
|
9
|
+
`);u.JOB_SYSTEM_PROMPT=e,this.logger.info(` ${R}prompt${L} ${e.split(`
|
|
10
|
+
`)[0].slice(0,80)}${e.includes(`
|
|
11
|
+
`)?`...`:``}`)}let p=t.context?this.parser.interpolate(t.context,{inputs:n.inputs,matrix:o,secrets:n.secrets,env:u}):u.CONTEXT_FILE;if(p){let e=(0,s.resolve)(r.workflowDir,p);(0,c.existsSync)(e)?(u.WORKFLOW_CONTEXT=e,u.WORKFLOW_CONTEXT_FILE=e,this.logger.info(` ${R}ctx ${L} ${p}`)):r.dryRun||this.logger.warn(` ${Re}warn${L} context file not found: ${p}`)}if(u.CHANGELOG_FILE){let e=(0,s.resolve)(r.workflowDir,u.CHANGELOG_FILE),t=(0,s.dirname)(e);(0,c.existsSync)(t)||(0,c.mkdirSync)(t,{recursive:!0}),(0,c.existsSync)(e)||(0,c.writeFileSync)(e,`# Changelog
|
|
12
|
+
`,`utf-8`),u.WORKFLOW_CHANGELOG=e,this.logger.info(` ${R}log ${L} changelog at ${u.CHANGELOG_FILE}`)}let m={inputs:n.inputs,matrix:o,secrets:n.secrets,env:u},h=!1,g=t.preJob?.steps??[],_=t.steps??[],v=t.postJob?.steps??[];for(let t=1;t<=a;t++){t>1&&(this.logger.info(`\n ${Re}retry${L} ${Le}${e}${L}${l} (attempt ${t}/${a})`),this.logger.info(` ${`─`.repeat(50)}`));let n=!0;if(g.length>0&&(n=await this.runStepSequence(g,m,r)),n&&=await this.runStepSequence(_,m,r),v.length>0){let e=await this.runStepSequence(v,m,r);n&&=e}if(n){h=!0;break}t<a&&this.logger.warn(` ${Re}Job "${e}" failed, retrying...${L}`)}if(h)this.logger.info(` [32mJob "${e}" completed${L}${l}`);else if(this.logger.error(`\n [31mJob "${e}" failed after ${a} attempts${L}`),t.strategy?.[`fail-fast`]!==!1)return!1}return!0}async runStepSequence(e,t,n){for(let r of e)if(!await this.stepRunner.runStep(r,t,n))return!1;return!0}},He=class extends Error{code=`WORKFLOW_STEP_SPAWN_FAILED`;constructor(e,t={}){super(`Failed to start workflow step "${e.stepName}"`,{cause:t.cause??e}),this.context=e,this.name=`WorkflowStepSpawnError`}},Ue=class extends Error{code=`WORKFLOW_STEP_TIMEOUT_CONFIG_INVALID`;constructor(e,t={}){super(`Invalid ${e.configKey} for workflow step "${e.stepName}"`,{cause:t.cause??e}),this.context=e,this.name=`WorkflowStepTimeoutConfigError`}};const We=`/backend-api`;var Ge=class{codexHome;fetchFn;now;readTextFile;constructor(e={}){this.codexHome=e.codexHome??(0,s.join)((0,o.homedir)(),`.codex`),this.fetchFn=e.fetchFn??fetch,this.now=e.now??(()=>Date.now()),this.readTextFile=e.readTextFile??(e=>(0,a.readFile)(e,`utf8`))}isCodexCommand(e,t){let n=e.trim();return n===`codex`||n.startsWith(`codex `)}async getQuotaStatus(){let e=await this.readAuthFile(),t=e?.tokens?.access_token?.trim(),n=e?.tokens?.account_id?.trim();if(!t||!n)return null;let r=await this.readChatgptBaseUrl(),i=await this.fetchFn(this.buildUsageUrl(r),{headers:{Authorization:`Bearer ${t}`,"ChatGPT-Account-Id":n,"User-Agent":`codex-cli`}});if(!i.ok)throw Error(`codex quota request failed with HTTP ${i.status}`);let a=await i.json();return{blockingLimit:this.findBlockingLimit(a),planType:a.plan_type??null}}async readAuthFile(){let e=await this.readOptionalTextFile((0,s.join)(this.codexHome,`auth.json`));return e?JSON.parse(e):null}async readChatgptBaseUrl(){let e=(await this.readOptionalTextFile((0,s.join)(this.codexHome,`config.toml`)))?.match(/^\s*chatgpt_base_url\s*=\s*"([^"]+)"/m);return this.normalizeBaseUrl(e?.[1]??`https://chatgpt.com/backend-api/`)}buildUsageUrl(e){return e.includes(We)?`${e}/wham/usage`:`${e}/api/codex/usage`}normalizeBaseUrl(e){let t=e.trim().replace(/\/+$/,``);return(t.startsWith(`https://chatgpt.com`)||t.startsWith(`https://chat.openai.com`))&&!t.includes(We)&&(t=`${t}${We}`),t}async readOptionalTextFile(e){try{return await this.readTextFile(e)}catch(e){if(e.code===`ENOENT`)return null;throw e}}findBlockingLimit(e){let t=[];this.collectBlockingLimit(t,`codex`,`codex`,e.rate_limit);for(let n of e.additional_rate_limits??[])this.collectBlockingLimit(t,n.metered_feature??n.limit_name??`codex`,n.limit_name??n.metered_feature??`codex`,n.rate_limit);return t.sort((e,t)=>e.resetAt-t.resetAt||t.usedPercent-e.usedPercent),t[0]??null}collectBlockingLimit(e,t,n,r){if(!r)return;let i=[{payload:r.primary_window,window:`primary`},{payload:r.secondary_window,window:`secondary`}];for(let{payload:a,window:o}of i){let i=a?.reset_at,s=a?.used_percent??0;typeof i==`number`&&(s>=100||r.limit_reached===!0&&i*1e3>this.now()||r.allowed===!1&&i*1e3>this.now())&&e.push({limitId:t,limitName:n,resetAfterSeconds:a?.reset_after_seconds??null,resetAt:i,usedPercent:s,window:o,windowSeconds:a?.limit_window_seconds??null})}}};const z=`\x1B[0m`,B=`\x1B[2m`,Ke=`\x1B[36m`,qe=`\x1B[32m`,V=`\x1B[33m`,H=`\x1B[31m`,Je=`SIGINT`,U=`SIGTERM`,Ye=`/bin/zsh`,Xe=`FORCE_COLOR`,Ze=`inherit`,Qe=`pipe`,$e=` `,et=`
|
|
13
|
+
${$e}\$ `,tt=`skip`,W=`warn`,G=`error`,K=`fail`,nt=`interrupted`,rt=`already stopping`,it=`failed to signal`,q=`(continue-on-error)`,at=`ESRCH`,ot=process.platform!==`win32`,st=`interactiveRun`,ct=`timeout-minutes`,lt=`timeout-retries`,ut=`retry`,dt=`SIGKILL`,ft=1e3,pt=6e4,mt=Math.floor(2147483647/pt),ht={status:`status_completed`},gt=new Set([`ENOENT`,`EBUSY`,`EAGAIN`,`EPERM`]),_t=`done`,vt=1e3,yt=n.z.number().positive().finite().max(mt),bt=n.z.number().int().min(0).max(10).default(2);var xt=class{activeStep=null;activeQuotaWait=null;constructor(e,t,n=new Ge,r=c.watch){this.parser=e,this.logger=t,this.quotaService=n,this.watchStatusFile=r}stopActiveStep(e){if(this.activeQuotaWait){if(this.activeQuotaWait.controller.signal.aborted){this.logger.info(` ${B}${tt}${z} ${this.activeQuotaWait.stepName} ${rt}`);return}this.activeQuotaWait.signal=e,this.activeQuotaWait.controller.abort(),this.logger.info(` ${V}${nt}${z} cancelling quota wait for ${this.activeQuotaWait.stepName}`);return}if(!this.activeStep){this.logger.info(` ${B}${tt}${z} no active step to stop`);return}if(this.activeStep.process.killed){this.logger.info(` ${B}${tt}${z} ${this.activeStep.stepName} ${rt}`);return}this.logger.info(` ${V}${nt}${z} sending ${e} to ${this.activeStep.stepName}`),this.killActiveStep(e)||this.logger.warn(` ${V}${W}${z} ${it} ${this.activeStep.stepName}`)}async runStep(e,t,n){let r=this.resolveRunner(n),i=this.resolveStepCommand(e,t,r),a=e.name?this.parser.interpolate(e.name,t):i?.command.slice(0,60)||e.uses||`unnamed`;if(e.uses){let n=this.parser.interpolate(e.uses,t);return this.parser.isActionSkipped(n)?(this.logger.info(` ${B}${tt}${z} ${n}`),!0):(this.logger.warn(` ${V}${W}${z} Unsupported action: ${n} (skipped)`),!0)}if(!i)return!0;if(i.missingRunner)return this.logger.error(` ${H}${G}${z} runner "${i.missingRunner.runner}" not found for step "${a}" (available runner keys: ${i.missingRunner.availableKeys.join(`, `)})`),e[`continue-on-error`]||n.continueOnError?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1);let{command:o,interactive:l}=i,u=this.quotaService.isCodexCommand(o,i.agentKey),d=e[`continue-on-error`]||n.continueOnError,f;try{f=this.resolveTimeoutMs(e,a)}catch(e){if(!(e instanceof Ue))throw e;return this.logger.error(` ${H}${G}${z} invalid ${e.context.configKey} for "${a}": ${String(e.context.timeoutValue)}`),d?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1)}let p={...process.env,...t.env};if(e.env)for(let[n,r]of Object.entries(e.env))p[n]=this.parser.interpolate(String(r),t);let m;l&&(m=this.createStatusFile(),p.WORKFLOW_STATUS_FILE=m,this.logger.info(` ${B}status-file${z} ${m}`));let h=e[`working-directory`]?(0,s.resolve)(n.workflowDir,this.parser.interpolate(e[`working-directory`],t)):n.workflowDir;if(n.dryRun)return this.logger.info(` ${Ke}dry ${z} ${a}`),this.logger.info(`${$e}${B}\$ ${o.split(`
|
|
14
|
+
`).join(et)}${z}`),!0;let g;try{g=this.resolveTimeoutRetries(e,a)}catch(e){if(!(e instanceof Ue))throw e;return this.logger.error(` ${H}${G}${z} invalid ${e.context.configKey} for "${a}": ${String(e.context.timeoutValue)}`),d?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1)}let _=!1;for(let e=0;e<=g;e++){await this.waitForCodexQuotaAvailability(a,o,u),e>0?this.logger.warn(` ${V}${ut}${z} retrying step after timeout "${a}" (attempt ${e+1}/${g+1})`):this.logger.info(` ${Ke}run ${z} ${a}`),this.logger.info(`${$e}${B}\$ ${o.split(`
|
|
15
|
+
`)[0]}${o.includes(`
|
|
16
|
+
`)?` ...`:``}${z}`);let t=this.executeCommand(o,h,p,a,l,f),n=l&&m?await this.raceStatusFile(t,m,a):await t;if(n.status===`signaled`)throw this.logger.info(`\n ${V}${nt}${z} ${a}`),new P(n.signal,{cause:Error(`Step process ended with ${n.signal}`),context:this.createInterruptContext(a,o)});if(n.status===`completed`&&(n.exitCode===130||n.exitCode===143)){let e=n.exitCode===130?Je:U;throw this.logger.info(`\n ${V}${nt}${z} ${a}`),new P(e,{cause:Error(`Step process exited with ${n.exitCode}`),context:this.createInterruptContext(a,o)})}if(n.status===`spawn_error`)return this.logger.error(` ${H}${G}${z} unable to start step "${a}" in ${n.error.context.workDir} (${this.formatSpawnError(n.error)})`),d?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1);if(n.status===`timed_out`){if(this.logger.error(` ${H}${G}${z} step timed out after ${this.formatTimeoutMs(n.timeoutMs)} "${a}" (${o.split(`
|
|
17
|
+
`)[0]})`),e<g){if(m)try{(0,c.writeFileSync)(m,``,`utf-8`)}catch(e){this.logger.warn(` ${V}${W}${z} status-file reset failed for ${a}: ${e instanceof Error?e.message:String(e)}`)}continue}return m&&this.cleanupStatusFile(m),d?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1)}if(m&&this.cleanupStatusFile(m),n.status===`status_completed`)return this.logger.info(` ${qe}${_t}${z} ${a} (status-file signaled completion)\n`),!0;if(n.status===`completed`&&n.exitCode===0)return this.logger.info(` ${qe}pass${z} ${a}\n`),!0;if(n.status===`completed`&&this.logger.error(` ${H}${G}${z} step "${a}" exited with code ${n.exitCode} (${o.split(`
|
|
18
|
+
`)[0]})`),!_&&await this.shouldRetryForCodexQuota(a,u)){_=!0,this.logger.warn(` ${V}${ut}${z} codex quota reached after failed step, retrying after reset`),--e;continue}return d?(this.logger.warn(` ${V}${K}${z} ${a} ${q}\n`),!0):(this.logger.error(` ${H}${K}${z} ${a}\n`),!1)}return!1}async executeCommand(e,t,n,i,a,o){return a&&this.logger.info(` ${B}(interactive: output renders directly to terminal)${z}`),await new Promise(s=>{let c=a?(0,r.spawn)(e,[],{stdio:[Ze,Ze,Ze],cwd:t,detached:!1,env:{...n,[Xe]:`1`},shell:Ye}):(0,r.spawn)(e,[],{stdio:[Ze,Qe,Qe],cwd:t,detached:ot,env:{...n,[Xe]:`1`},shell:Ye});this.activeStep={command:e,process:c,processGroupId:ot&&!a&&typeof c.pid==`number`?c.pid:null,stepName:i};let l=null,u=!1,d=!1,f=e=>{d||(d=!0,p&&clearTimeout(p),l&&clearTimeout(l),this.activeStep=null,s(e))},p=o===null?null:setTimeout(()=>{if(u=!0,this.killActiveStep(U)){l=setTimeout(()=>{d||(this.logger.warn(` ${V}${W}${z} ${i} forcing SIGKILL`),this.killActiveStep(dt))},ft);return}f({status:`timed_out`,timeoutMs:o})},o);c.stdout?.on(`data`,e=>{for(let t of e.toString().split(`
|
|
19
|
+
`))t&&this.logger.info(t)}),c.stderr?.on(`data`,e=>{for(let t of e.toString().split(`
|
|
20
|
+
`))t&&this.logger.error(t)}),c.on(`error`,n=>{f({status:`spawn_error`,error:new He({commandPreview:e.split(`
|
|
21
|
+
`)[0],source:`StepRunnerService.executeCommand`,stepName:i,workDir:t},{cause:n})})}),c.on(`exit`,(e,t)=>{if(this.cleanupProcessGroup(),u&&o!==null){f({status:`timed_out`,timeoutMs:o});return}if(t===Je){f({status:`signaled`,signal:Je});return}if(t===U){f({status:`signaled`,signal:U});return}f({status:`completed`,exitCode:e??1})})})}createInterruptContext(e,t){return{phase:`step_execution`,stepName:e,commandPreview:t.split(`
|
|
22
|
+
`)[0],source:`StepRunnerService.runStep`}}formatSpawnError(e){let t=e.cause;return t instanceof Error?`${`code`in t&&typeof t.code==`string`?t.code:`unknown-spawn-error`}${`path`in t&&typeof t.path==`string`?` at ${t.path}`:``}; verify command, shell, and permissions`:`check shell availability, PATH, and permissions`}resolveStepCommand(e,t,n){let r=e[st]!=null,i=r?{command:e[st],interactive:!0}:{command:e.run,interactive:!1},a=r?{command:e.run,interactive:!1}:{command:e[st],interactive:!0},o=this.resolveExactAgentCommand(i.command,t,n);if(o)return{...o,interactive:i.interactive};let s=this.resolveExactAgentCommand(a.command,t,n);if(s)return{...s,interactive:a.interactive};let c=this.resolveMissingExplicitRunner(e,n);if(c)return{command:``,interactive:!1,missingRunner:c};let l=this.resolveStringCommand(i.command,t);if(l)return{command:l,interactive:i.interactive};let u=this.resolveStringCommand(a.command,t);if(u)return{command:u,interactive:a.interactive};let d=this.resolveFirstMappedCommand(i.command,t);if(d)return{command:d,interactive:i.interactive};let f=this.resolveFirstMappedCommand(a.command,t);return f?{command:f,interactive:a.interactive}:null}resolveExactAgentCommand(e,t,n){if(!n||!e||typeof e==`string`)return null;let r=e[n];return r?{agentKey:n,command:this.parser.interpolate(r,t),interactive:!1}:null}resolveStringCommand(e,t){return!e||typeof e!=`string`?null:this.parser.interpolate(e,t)}resolveFirstMappedCommand(e,t){if(!e||typeof e==`string`)return null;let[n]=Object.values(e);return n?this.parser.interpolate(n,t):null}resolveMissingExplicitRunner(e,t){if(!t)return null;let n=this.collectMappedRunnerKeys(e);return n.length===0||n.includes(t)?null:{availableKeys:n,runner:t}}collectMappedRunnerKeys(e){let t=new Set;for(let n of[e[st],e.run])if(n&&typeof n!=`string`)for(let e of Object.keys(n))t.add(e);return[...t]}resolveRunner(e){return e.runner??e.cliAgent}async shouldRetryForCodexQuota(e,t){return t?(await this.readCodexQuotaStatus(e))?.blockingLimit!=null:!1}async waitForCodexQuotaAvailability(e,t,n){if(n)for(;;){let n=await this.readCodexQuotaStatus(e),r=n?.blockingLimit;if(!r)return;let i=r.resetAt*1e3+vt,a=Math.max(i-Date.now(),vt),o=new Date(i).toLocaleString(),s=n?.planType?` (${n.planType})`:``;this.logger.warn(` ${V}wait ${z} codex quota ${r.limitName}/${r.window} is at ${r.usedPercent}%${s}; waiting until ${o}`),await this.waitForQuotaDelay(e,t,a)}}async readCodexQuotaStatus(e){try{return await this.quotaService.getQuotaStatus()}catch(t){return this.logger.warn(` ${V}${W}${z} unable to read codex quota for "${e}" (${this.formatQuotaError(t)})`),null}}async waitForQuotaDelay(e,t,n){let r=new AbortController;this.activeQuotaWait={controller:r,signal:null,stepName:e};try{let e=n;for(;e>0;){let t=Math.min(e,3e4);await this.waitForDelay(t,r.signal),e-=t}}catch(n){throw r.signal.aborted?new P(this.activeQuotaWait?.signal??U,{cause:Error(`Interrupted while waiting for Codex quota reset`),context:{...this.createInterruptContext(e,t),phase:`StepRunnerService.waitForCodexQuota`}}):n}finally{this.activeQuotaWait=null}}async waitForDelay(e,t){await new Promise((n,r)=>{let i=setTimeout(()=>{t.removeEventListener(`abort`,a),n()},e),a=()=>{clearTimeout(i),t.removeEventListener(`abort`,a),r(Error(`aborted`))};t.addEventListener(`abort`,a,{once:!0})})}formatQuotaError(e){return e instanceof Error&&e.message.trim().length>0?e.message:`unknown quota error`}resolveTimeoutMs(e,t){let n=e[ct];if(n===void 0)return null;try{let e=yt.parse(n);return Math.ceil(e*pt)}catch(e){throw new Ue({stepName:t,configKey:ct,timeoutValue:n},{cause:e})}}resolveTimeoutRetries(e,t){let n=e[lt];try{return bt.parse(n)}catch(e){throw new Ue({stepName:t,configKey:lt,timeoutValue:n},{cause:e})}}formatTimeoutMs(e){return e%pt===0?`${e/pt} minute(s)`:`${e}ms`}cleanupProcessGroup(){if(!this.activeStep)return;let{processGroupId:e,stepName:t}=this.activeStep;if(e!==null)try{process.kill(-e,U)}catch(n){let r=n.code;r===at?this.logger.info(` ${B}cleanup${z} process group ${e} already exited (ESRCH)`):this.logger.warn(` ${V}${W}${z} failed to clean up process group for ${t} (pgid=${e}, error=${r})`)}}killActiveStep(e){if(!this.activeStep)return!1;let{process:t,processGroupId:n}=this.activeStep;if(n!==null)try{return process.kill(-n,e),!0}catch(e){e.code===at?this.logger.info(` ${B}signal${z} process group for ${this.activeStep.stepName} already exited (ESRCH)`):this.logger.warn(` ${V}${W}${z} ${it} process group for ${this.activeStep.stepName}`)}try{return t.kill(e)}catch(e){return this.logger.warn(` ${V}${W}${z} ${it} ${this.activeStep.stepName} (${e.message})`),!1}}createStatusFile(){let e=(0,s.join)((0,o.tmpdir)(),`workflow-step-${(0,d.randomUUID)()}.status`);return(0,c.writeFileSync)(e,``,`utf-8`),e}async raceStatusFile(e,t,n){return new Promise(r=>{let i=!1,a=!1,o=null,s=null,l=null,u=e=>{i||(i=!0,s&&=(s.close(),null),l&&=(clearInterval(l),null),o&&=(clearTimeout(o),null),r(e))},d=!1,f=!1,p=()=>{try{return(0,c.existsSync)(t)&&(0,c.readFileSync)(t,`utf-8`).trim()===`YES`}catch(e){let t=e.code;return!t||!gt.has(t)?this.logger.warn(` ${V}${W}${z} status-file read failed for ${n}: ${e instanceof Error?e.message:String(e)}`):this.logger.info(` ${B}status-file${z} transient FS error (${t}) polling ${n}`),!1}},m=()=>{if(!(a||!p())){if(a=!0,this.logger.info(` ${qe}${_t}${z} ${n} status-file received YES`),d=this.killActiveStep(U),!d){this.logger.warn(` ${V}${W}${z} ${n} status-file: SIGTERM could not be delivered, process may have already exited`);return}o=setTimeout(()=>{i||(f=!0,this.logger.warn(` ${V}${W}${z} ${n} did not exit after status-file SIGTERM, forcing SIGKILL`),this.killActiveStep(dt))},ft)}};try{s=this.watchStatusFile(t,()=>m())}catch(e){this.logger.warn(` ${V}${W}${z} status-file watcher setup failed for ${n}: ${e instanceof Error?e.message:String(e)}`)}l=setInterval(m,1e3),m(),e.then(e=>{if(!(a||p())){u(e);return}switch(a||(a=!0,this.logger.info(` ${qe}${_t}${z} ${n} status-file observed during process exit handling`)),e.status){case`spawn_error`:case`timed_out`:u(e);return;case`signaled`:f&&this.logger.warn(` ${V}${W}${z} ${n} required SIGKILL escalation (work completed via status-file)`),u(ht);return;case`completed`:e.exitCode===0||e.exitCode>128||f||!d?(f&&this.logger.warn(` ${V}${W}${z} ${n} required SIGKILL escalation (work completed via status-file, exit code ${e.exitCode})`),u(ht)):u(e);return;case`status_completed`:u(e);return;default:u(e)}})})}cleanupStatusFile(e){try{(0,c.existsSync)(e)&&(0,c.unlinkSync)(e)}catch(e){this.logger.warn(` ${V}${W}${z} status-file cleanup failed: ${e instanceof Error?e.message:String(e)}`)}}};const J=`\x1B[0m`,St=`\x1B[1m`,Ct=`\x1B[2m`,wt=`\x1B[36m`,Tt=`\x1B[32m`,Et=`\x1B[31m`,Dt=`\x1B[34m`;var Ot=class{activePrompt=null;activePromptSignal=null;constructor(e,t){this.stepRunner=e,this.logger=t}async pathExists(e){try{return await(0,a.access)(e),!0}catch{return!1}}abortActivePrompt(e){this.activePromptSignal=e,this.activePrompt?.close()}async handleUserPrompt(e,t,n,r,i){if(!e.on||!(`user_prompt`in e.on))return!0;this.logger.info(`\n ${Dt}${St}trigger${J} ${St}user_prompt${J}`),this.logger.info(` ${`─`.repeat(50)}`);let o=t,c=n.CONTEXT_FILE;if(!o&&c){let e=(0,s.resolve)(r,c);await this.pathExists(e)&&(o=(await(0,a.readFile)(e,`utf-8`)).trim(),o&&this.logger.info(` ${Tt}found${J} Existing context.md -> ${c}`))}if(!o)if(i)this.logger.info(` ${wt}dry ${J} Would prompt user for input`),o=`(dry-run: no prompt collected)`;else{this.logger.info(` ${wt}input${J} Enter your prompt (press Enter twice to finish):\n`);let e=[],t=(0,f.createInterface)({input:process.stdin,output:process.stdout});this.activePrompt=t,this.activePromptSignal=null,o=await new Promise((n,r)=>{let i=!1,a=e=>{i||(i=!0,this.activePrompt=null,e())};t.on(`line`,n=>{n===``&&e.length>0&&e[e.length-1]===``?t.close():e.push(n)}),t.on(`SIGINT`,()=>{this.activePromptSignal=`SIGINT`,t.close()}),t.on(`close`,()=>a(()=>{let t=this.activePromptSignal;if(this.activePromptSignal=null,t){r(new P(t));return}e.length>0&&e[e.length-1]===``&&e.pop(),n(e.join(`
|
|
23
|
+
`))}))})}if(!o)return this.logger.error(` ${Et}fail${J} No prompt provided. Aborting.`),!1;if(c){let e=(0,s.resolve)(r,c);await(0,a.mkdir)((0,s.dirname)(e),{recursive:!0}),await(0,a.writeFile)(e,o,`utf-8`),this.logger.info(` ${Tt}saved${J} ${o.split(`
|
|
24
|
+
`).length} line(s) -> ${c}`)}return n.USER_PROMPT=o,this.logger.info(` ${Ct}prompt${J} ${o.split(`
|
|
25
|
+
`)[0].slice(0,80)}${o.includes(`
|
|
26
|
+
`)?`...`:``}`),!0}async handleAgentDecision(e,t,n,r,i,o){let c=e.on?.agent_decision;if(!c?.steps)return!0;this.logger.info(`\n ${Dt}${St}trigger${J} ${St}agent_decision${J}`),this.logger.info(` ${`─`.repeat(50)}`),o&&(t.HEARTBEAT_PROMPT=o,this.logger.info(` ${Ct}prompt${J} ${o.split(`
|
|
27
|
+
`)[0].slice(0,80)}${o.includes(`
|
|
28
|
+
`)?`...`:``}`));let l=t.CONTEXT_FILE;l&&await(0,a.mkdir)((0,s.dirname)((0,s.resolve)(i.workflowDir,l)),{recursive:!0});let u={inputs:n,matrix:{},secrets:r,env:t};for(let e of c.steps)if(!await this.stepRunner.runStep(e,u,i))return this.logger.error(`\n ${Et}Agent decision aborted workflow${J}`),!1;if(l&&!i.dryRun){let e=(0,s.resolve)(i.workflowDir,l);if(!await this.pathExists(e))return this.logger.error(` ${Et}fail${J} Context file not created: ${l}`),!1;let t=(await(0,a.readFile)(e,`utf-8`)).trim();if(!t)return this.logger.error(` ${Et}fail${J} Context file is empty: ${l}`),!1;this.logger.info(` ${Tt}ready${J} context has ${t.split(`
|
|
29
|
+
`).length} line(s)`)}return!0}};const kt=[`actions/checkout`,`actions/setup-node`,`actions/cache`,`actions/upload-artifact`,`actions/download-artifact`,`pnpm/action-setup`],At=`bold.calm.cool.dark.deep.fair.fast.free.gold.keen.kind.loud.neat.pure.rare.safe.slim.soft.tall.warm.wild.wise.blue.gray.jade.iron.zinc.ruby.opal.onyx`.split(`.`),jt=`arch.beam.bird.bolt.cape.cove.dawn.dove.echo.fern.flux.gale.hawk.haze.iris.jade.kite.lake.lynx.mesa.moth.node.palm.peak.pine.raft.reef.sage.tide.vale`.split(`.`);var Mt=class{generateHumanReadableId(){return`${At[Math.floor(Math.random()*At.length)]}-${jt[Math.floor(Math.random()*jt.length)]}`}parseWorkflowFile(e){let t=(0,s.resolve)(e);if(!(0,c.existsSync)(t))throw Error(`Workflow file not found: ${t}`);let n=(0,c.readFileSync)(t,`utf-8`),r=ve.parse((0,p.load)(n));if(r.imports&&this.resolveImports(r,t),!r.jobs||Object.keys(r.jobs).length===0)throw Error(`No jobs found in workflow file`);return this.resolveExtends(r),r}resolveImports(e,t,n=new Set){let r=(0,s.resolve)(t);if(n.has(r))throw Error(`Circular import detected: ${r}`);n.add(r);let i=r.replace(/\/[^/]+$/,``);for(let t of e.imports??[]){let a=(0,s.resolve)(i,t);if(!(0,c.existsSync)(a))throw Error(`Imported workflow file not found: ${a} (from ${r})`);let o=(0,p.load)((0,c.readFileSync)(a,`utf-8`));if(o.imports&&this.resolveImports(o,a,n),o.env&&(e.env={...o.env,...e.env}),o.pre&&(e.pre||={},o.pre.services&&(e.pre.services=[...o.pre.services,...e.pre.services??[]]),o.pre.steps&&(e.pre.steps=[...o.pre.steps,...e.pre.steps??[]])),o.post&&(e.post||={},o.post.services&&(e.post.services=[...o.post.services,...e.post.services??[]]),o.post.steps&&(e.post.steps=[...o.post.steps,...e.post.steps??[]])),o.worktree){e.worktree||={};for(let t of[`beforeCreate`,`afterCreate`,`beforeCompleted`,`afterCompleted`])o.worktree[t]&&!e.worktree[t]&&(e.worktree[t]=o.worktree[t])}if(o[`max-workflows`]&&!e[`max-workflows`]&&(e[`max-workflows`]=o[`max-workflows`]),o[`launch-command`]&&!e[`launch-command`]&&(e[`launch-command`]=o[`launch-command`]),o[`system-prompt`]&&(e[`system-prompt`]?e[`system-prompt`]=`${o[`system-prompt`]}\n\n${e[`system-prompt`]}`:e[`system-prompt`]=o[`system-prompt`]),o.jobs)for(let[t,n]of Object.entries(o.jobs))t in(e.jobs??{})||(e.jobs||={},e.jobs[t]=n)}delete e.imports}resolveExtends(e){let{jobs:t}=e,n=new Map;for(let[e,r]of Object.entries(t))e.startsWith(`.`)&&n.set(e,r);for(let[e,r]of Object.entries(t)){if(e.startsWith(`.`)||!r.extends)continue;let t=Array.isArray(r.extends)?r.extends:[r.extends],i=[...r.steps??[]],a=[...r.preJob?.steps??[]],o=[...r.postJob?.steps??[]],s=[],c=[],l=[];for(let i of t){let t=n.get(i);if(!t)throw Error(`Job "${e}" extends "${i}" but template not found`);s.push(...t.steps??[]),c.push(...t.preJob?.steps??[]),l.push(...t.postJob?.steps??[]),this.applyTemplate(r,t)}r.steps=[...s,...i],this.assignJobHookSteps(r,`preJob`,[...c,...a]),this.assignJobHookSteps(r,`postJob`,[...l,...o]),delete r.extends}for(let e of n.keys())delete t[e]}applyTemplate(e,t){e.steps=[...t.steps??[],...e.steps??[]],this.mergeJobHookSteps(e,t,`preJob`),this.mergeJobHookSteps(e,t,`postJob`),(t.env||e.env)&&(e.env={...t.env,...e.env}),t[`system-prompt`]&&e[`system-prompt`]?e[`system-prompt`]=`${t[`system-prompt`]}\n\n${e[`system-prompt`]}`:t[`system-prompt`]&&(e[`system-prompt`]=t[`system-prompt`]),t[`runs-on`]&&!e[`runs-on`]&&(e[`runs-on`]=t[`runs-on`]),t.strategy&&!e.strategy&&(e.strategy=t.strategy),t.context&&!e.context&&(e.context=t.context)}mergeJobHookSteps(e,t,n){let r=t[n]?.steps??[],i=e[n]?.steps??[];this.assignJobHookSteps(e,n,[...r,...i])}assignJobHookSteps(e,t,n){if(n.length>0){e[t]={steps:n};return}delete e[t]}loadSecrets(e){let t=new Map;if(!(0,c.existsSync)(e))return t;let n=(0,c.readFileSync)(e,`utf-8`);for(let e of n.split(`
|
|
30
|
+
`)){let n=e.trim();if(!n||n.startsWith(`#`))continue;let r=n.indexOf(`=`);r!==-1&&t.set(n.slice(0,r),n.slice(r+1))}return t}interpolate(e,t){return e.replace(/\$\{\{\s*(.+?)\s*\}\}/g,(e,n)=>{let i=n.trim().split(`.`);if(i[0]===`inputs`&&i[1])return t.inputs.get(i[1])||``;if(i[0]===`matrix`&&i[1])return t.matrix[i[1]]||``;if(i[0]===`secrets`&&i[1])return t.secrets.get(i[1])||process.env[i[1]]||``;if(i[0]===`env`&&i[1])return t.env[i[1]]||process.env[i[1]]||``;if(i[0]===`runner`&&i[1]===`os`)return`macOS`;if(i[0]===`github`&&i[1]===`sha`)try{return(0,r.execSync)(`git rev-parse HEAD`,{encoding:`utf-8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim()}catch{return`local`}return n.includes(`hashFiles`)?`local`:`\${{ ${n} }}`})}isActionSkipped(e){return kt.some(t=>e.startsWith(t))}resolveJobOrder(e,t){let n=new Set,r=[],i=t=>{if(n.has(t))return;n.add(t);let a=e[t];if(!a)throw Error(`Job not found: ${t}`);let o=a.needs?Array.isArray(a.needs)?a.needs:[a.needs]:[];for(let e of o)i(e);r.push(t)};if(t)i(t);else for(let t of Object.keys(e))i(t);return r}expandMatrix(e){if(!e.strategy?.matrix)return[{}];let{include:t,...n}=e.strategy.matrix;if(t&&t.length>0)return t;let r=Object.keys(n).filter(e=>Array.isArray(n[e]));if(r.length===0)return[{}];let i=[{}];for(let e of r){let t=n[e],r=[];for(let n of i)for(let i of t)r.push({...n,[e]:String(i)});i=r}return i}};const Nt=`\x1B[0m`,Pt=`\x1B[2m`;var Ft=class{constructor(e,t){this.stepRunner=e,this.logger=t}createWorktree(e,t){let n=t.toLowerCase().replace(/[^a-z0-9-]+/g,`-`).replace(/^-|-$/g,``),i=(0,s.resolve)((0,s.dirname)(e),`${(0,s.basename)(e)}-worktree`),a=(0,s.resolve)(i,n);(0,c.existsSync)(i)||(0,c.mkdirSync)(i,{recursive:!0});let o=`worktree/${n}`;(0,c.existsSync)(a)&&(this.logger.info(` ${Pt}clean${Nt} Removing stale worktree at ${a}`),this.cleanupWorktreePath(e,a));try{(0,r.execSync)(`git branch -D "${o}"`,{cwd:e,stdio:[`ignore`,`pipe`,`ignore`]})}catch{}return this.logger.info(` [36mcreate${Nt} Worktree at ${a} (branch: ${o})`),(0,r.execSync)(`git worktree add -b "${o}" "${a}" HEAD`,{cwd:e,stdio:[`ignore`,`pipe`,`ignore`]}),a}removeWorktree(e,t){if((0,c.existsSync)(t)){this.logger.info(` ${Pt}remove${Nt} Worktree at ${t}`);try{this.cleanupWorktreePath(e,t)}catch{this.logger.warn(` [33mwarn${Nt} Failed to remove worktree (may need manual cleanup)`)}}}cleanupWorktreePath(e,t){if(this.isRegisteredWorktree(e,t)){(0,r.execSync)(`git worktree remove --force "${t}"`,{cwd:e,stdio:[`ignore`,`pipe`,`ignore`]});return}(0,c.rmSync)(t,{recursive:!0,force:!0})}isRegisteredWorktree(e,t){try{return(0,r.execSync)(`git worktree list --porcelain`,{cwd:e,encoding:`utf-8`,stdio:[`ignore`,`pipe`,`ignore`]}).split(`
|
|
31
|
+
`).some(e=>e.startsWith(`worktree `)&&(0,s.resolve)(e.slice(9))===t)}catch{return!1}}async runHook(e,t,n,r,i,a){let o={inputs:r,matrix:{},secrets:i,env:n};for(let n of e.steps)if(!await this.stepRunner.runStep(n,o,{...a,workflowDir:t}))return!1;return!0}};const Y=`\x1B[0m`,X=`\x1B[1m`,It=`\x1B[2m`,Lt=`\x1B[32m`,Z=`\x1B[33m`,Q=`\x1B[31m`,$=`\x1B[34m`,Rt=`SIGINT`,zt=`SIGTERM`,Bt=`workflow_execution`,Vt=`restart-from`,Ht=`user_prompt`,Ut=`agent_decision`,Wt=`workflow_dispatch`,Gt=`Workflow post-cleanup failed`;var Kt=class{logger;parser;serviceManager;stepRunner;jobRunner;triggerService;worktreeService;registry;outputLines=[];interruptedSignal=null;constructor(e,t={}){let n=e||{info:e=>process.stdout.write(`${e}\n`),error:e=>console.error(e),warn:e=>console.error(e)},r=e=>{this.outputLines.push(e),this.outputLines.length>5e3&&this.outputLines.shift()};this.logger={info:e=>{r(e),n.info(e)},error:e=>{r(e),n.error(e)},warn:e=>{r(e),n.warn(e)}},this.parser=t.parser??new Mt,this.serviceManager=t.serviceManager??new Ie(this.logger),this.stepRunner=t.stepRunner??new xt(this.parser,this.logger),this.jobRunner=t.jobRunner??new Ve(this.parser,this.stepRunner,this.logger),this.triggerService=t.triggerService??new Ot(this.stepRunner,this.logger),this.worktreeService=t.worktreeService??new Ft(this.stepRunner,this.logger),this.registry=t.registry??new De}async run(e){this.outputLines=[],this.interruptedSignal=null;let t=e.dryRun??!1,n=e.continueOnError??!1,r=!1,i=e=>{r||(r=!0,this.interrupt(e))},a=()=>i(Rt),o=()=>i(zt);process.on(Rt,a),process.on(zt,o);try{return await this.executeWorkflow(e,t,n,e.keepWorktree??!1)}catch(e){if(e instanceof P||this.isInterrupted())return await this.serviceManager.stopAll(),this.createInterruptedResult();throw e}finally{process.off(Rt,a),process.off(zt,o),await this.serviceManager.stopAll()}}interrupt(e,t={phase:Bt}){if(this.interruptedSignal)return;this.interruptedSignal=e;let n=t.phase===Bt?``:` (${t.phase})`;this.logger.info(`\n\n${Z}${X}Interrupted — shutting down...${Y}${n}`),this.triggerService.abortActivePrompt(e),this.stepRunner.stopActiveStep(e),this.serviceManager.stopAll()}isInterrupted(){return this.interruptedSignal===Rt||this.interruptedSignal===zt}createInterruptedResult(){return{exitCode:130,output:this.outputLines.join(`
|
|
32
|
+
`)}}parseFixRestartFrom(e,t){if(!e.startsWith(`---`))return{};let n=e.indexOf(`---`,3);if(n<0)return{};let r=e.slice(3,n);for(let e of r.split(`
|
|
33
|
+
`)){let n=e.trim();if(n.startsWith(`${Vt}:`)){let e=n.slice(`${Vt}:`.length).trim();return t.includes(e)?{restartFrom:e}:{invalidTarget:e}}}return{}}async pathExists(e){try{return await(0,a.access)(e),!0}catch(e){if(e.code===`ENOENT`)return!1;throw e}}async waitForServiceStartup(){await new Promise(e=>{setTimeout(e,2e3)})}resolveRunner(e){if(e.runner&&e.cliAgent&&e.runner!==e.cliAgent)throw Error(`Conflicting runner selectors: runner="${e.runner}" cliAgent="${e.cliAgent}"`);return e.runner??e.cliAgent}async executeWorkflow(e,t,n,i=!1){let o=(0,s.resolve)(e.workflowPath),c=this.parser.parseWorkflowFile(o),l=this.resolveRunner(e),u=null,d=null,f=null,p=(0,s.dirname)(o),m=p,h=null,g=!1,_=!1,v=!!c.worktree,y=new Map,b=e.secretFile?this.parser.loadSecrets(e.secretFile):new Map,x={};try{let f=c.on?.[Wt]?.inputs||{};for(let[e,t]of Object.entries(f))t.default&&y.set(e,t.default);if(e.inputs)for(let[t,n]of Object.entries(e.inputs))y.set(t,n);if(c.env)for(let[e,t]of Object.entries(c.env))x[e]=String(t);if(e.env)for(let[t,n]of Object.entries(e.env))x[t]=n;l&&(x.WORKFLOW_RUNNER=l,x.WORKFLOW_CLI_AGENT=l);let S=p;for(;S!==`/`;){if(await this.pathExists((0,s.resolve)(S,`.git`))){p=S;break}S=(0,s.dirname)(S)}let C=c[`max-workflows`];if(C){let t=this.registry.resolveWorkspace(e.workspace,c.workspace),n=await this.registry.countRunningWorkflows(t);if(n>=C)throw new Ae(t,n,C)}let ee=c[`launch-command`];if(ee&&!e.skipLaunch){let t=this.buildLaunchCliCommand(e,o),n=e.name||c.name||(0,s.basename)(o),i=e.name?n:`${n}-${this.parser.generateHumanReadableId()}`,a=ee.replace(`{name}`,i).replace(`{command}`,t);this.logger.info(`${It}Delegating via launch-command: ${a}${Y}`);try{return(0,r.execSync)(a,{stdio:`inherit`,cwd:p}),{exitCode:0,output:this.outputLines.join(`
|
|
34
|
+
`)}}catch(e){return{exitCode:e.status??1,output:this.outputLines.join(`
|
|
35
|
+
`)}}}let te=e.name||c.name||(0,s.basename)(o),w=e.name?te:`${te}-${this.parser.generateHumanReadableId()}`;if(u=await this.registry.createRun({displayName:w,dryRun:t,workflowPath:o,workflowWorkspace:c.workspace,workspace:e.workspace}),x.WORKFLOW_RUN_DIR=u.runDir,x.WORKFLOW_WORKSPACE=u.workspace,x.CONTEXT_FILE&&(x.CONTEXT_FILE=u.contextPath,x.CHANGELOG_FILE=u.changelogPath),this.logger.info(`${X}${`═`.repeat(60)}${Y}`),this.logger.info(` ${X}run-workflow${Y} - ${u.displayName}`),this.logger.info(`${X}${`═`.repeat(60)}${Y}`),this.logger.info(` File: ${o}`),this.logger.info(` Workspace: ${u.workspace||`default`}`),this.logger.info(` Run Dir: ${u.runDir}`),this.logger.info(` CWD: ${p}`),y.size>0&&this.logger.info(` Inputs: ${[...y.entries()].map(([e,t])=>`${e}=${t}`).join(`, `)}`),b.size>0&&this.logger.info(` Secrets: ${[...b.keys()].join(`, `)}`),e.job&&this.logger.info(` Target: ${e.job}`),l&&this.logger.info(` Runner: ${l}`),c.pre){let e=c.pre.services?.length||0,t=c.pre.steps?.length||0;this.logger.info(` Pre: ${e} service(s), ${t} setup step(s)`)}if(c.post){let e=c.post.services?.length||0,t=c.post.steps?.length||0;this.logger.info(` Post: ${e} service(s), ${t} cleanup step(s)`)}v&&this.logger.info(` Worktree: enabled${i?` (keep on completion)`:``}`);let T=c.on&&Ht in c.on?Ht:c.on?.[Ut]?Ut:Wt;if(this.logger.info(` Trigger: ${T}`),this.logger.info(` Mode: ${t?`dry-run`:`execute`}`),this.logger.info(`${X}${`═`.repeat(60)}${Y}`),!await this.triggerService.handleUserPrompt(c,e.prompt||null,x,p,t))return this.isInterrupted()?(d=this.createInterruptedResult(),d):(this.logger.error(`\n${Q}${X}Workflow aborted: no prompt provided${Y}`),d={exitCode:1,output:this.outputLines.join(`
|
|
36
|
+
`)},d);if(!await this.triggerService.handleAgentDecision(c,x,y,b,{runner:l,cliAgent:e.cliAgent,dryRun:t,continueOnError:n,workflowDir:p},e.prompt||null))return this.isInterrupted()?(d=this.createInterruptedResult(),d):(this.logger.info(`\n${Z}${X}Workflow skipped: agent decided nothing to do${Y}`),d={exitCode:2,output:this.outputLines.join(`
|
|
37
|
+
`)},d);if(m=p,v&&!t)c.worktree?.beforeCreate&&(this.logger.info(`\n ${$}${X}worktree${Y} ${X}beforeCreate${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.worktreeService.runHook(c.worktree.beforeCreate,p,x,y,b,{runner:l,cliAgent:e.cliAgent,dryRun:t,continueOnError:n})||(this.isInterrupted()?d=this.createInterruptedResult():(this.logger.error(`\n${Q}${X}Workflow aborted: worktree beforeCreate hook failed${Y}`),d={exitCode:1,output:this.outputLines.join(`
|
|
38
|
+
`)}))),d||(this.logger.info(`\n ${$}${X}worktree${Y} ${X}Creating worktree${Y}`),this.logger.info(` ${`─`.repeat(50)}`),h=this.worktreeService.createWorktree(p,u.displayName),p=h,x.WORKTREE_PATH=h,x.WORKFLOW_NAME=u.displayName,x.ORIGINAL_REPO_PATH=m,c.worktree?.afterCreate&&(this.logger.info(`\n ${$}${X}worktree${Y} ${X}afterCreate${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.worktreeService.runHook(c.worktree.afterCreate,h,x,y,b,{runner:l,cliAgent:e.cliAgent,dryRun:t,continueOnError:n})||(this.isInterrupted()?d=this.createInterruptedResult():(this.logger.error(`\n${Q}${X}Workflow aborted: worktree afterCreate hook failed${Y}`),d={exitCode:1,output:this.outputLines.join(`
|
|
39
|
+
`)}),_=!i)));else if(v&&t){let e=(0,s.dirname)(p),t=(0,s.basename)(p),n=u.displayName.toLowerCase().replace(/[^a-z0-9-]+/g,`-`).replace(/^-|-$/g,``),r=(0,s.resolve)(e,`${t}-worktree`,n);this.logger.info(`\n ${$}${X}worktree${Y} ${X}(dry-run)${Y} Would create at ${r}`)}!d&&c.pre&&(await this.runPreBlock(c.pre,x,y,b,{runner:l,cliAgent:e.cliAgent,dryRun:t,continueOnError:n,workflowDir:p})||(this.isInterrupted()?d=this.createInterruptedResult():(this.logger.error(`\n${Q}${X}Workflow aborted: pre-conditions failed${Y}`),d={exitCode:1,output:this.outputLines.join(`
|
|
40
|
+
`)}),_=!!(h&&!i)));let ne=Number(x.MAX_RETRIES)||3,E=this.parser.resolveJobOrder(c.jobs,e.job||null);this.logger.info(`\n ${It}Job order: ${E.join(` -> `)} (max retries: ${ne})${Y}`);let re=new Set,D=x.WORKFLOW_RUN_DIR?(0,s.resolve)(x.WORKFLOW_RUN_DIR,`fix.md`):null,O=0,k=0;for(;!d&&k<E.length;){let r=E[k],i=c.jobs[r],o=await this.jobRunner.runJob(r,i,{inputs:y,secrets:b,workflowEnv:x,workflowSystemPrompt:c[`system-prompt`],jobOrder:E,allJobs:c.jobs},{runner:l,cliAgent:e.cliAgent,dryRun:t,continueOnError:n,workflowDir:p,maxRetries:ne});if(this.isInterrupted()&&(d=this.createInterruptedResult()),!d&&!o&&(this.logger.error(`\n${Q}${X}Workflow failed at job "${r}"${Y}`),await this.serviceManager.stopAll(),d={exitCode:1,output:this.outputLines.join(`
|
|
41
|
+
`)}),!d){if(re.add(r),D&&await this.pathExists(D)&&!t){if(O++,O>3){this.logger.error(`\n${Q}${X}Fix loop limit reached (3 cycles). Aborting.${Y}`),d={exitCode:1,output:this.outputLines.join(`
|
|
42
|
+
`)};continue}let e=(await(0,a.readFile)(D,`utf-8`)).trim();await(0,a.unlink)(D);let{restartFrom:t,invalidTarget:n}=this.parseFixRestartFrom(e,E);if(this.logger.info(`\n${Z}${X}fix.md detected after "${r}" (cycle ${O}/3)${Y}`),this.logger.info(`${It}${e.split(`
|
|
43
|
+
`)[0].slice(0,100)}${e.includes(`
|
|
44
|
+
`)?`...`:``}${Y}`),n&&this.logger.warn(`${Z}fix.md restart-from "${n}" is not a valid job (available: ${E.join(`, `)}), falling back to default${Y}`),x.CONTEXT_FILE&&await this.pathExists(x.CONTEXT_FILE)){let t=await(0,a.readFile)(x.CONTEXT_FILE,`utf-8`);await(0,a.writeFile)(x.CONTEXT_FILE,`${t}\n\n---\n## Fix Request (cycle ${O})\n${e}`,`utf-8`)}let i=t?E.indexOf(t):-1,o=E.indexOf(`development`);k=i>=0?i:o>=0?o:1,this.logger.info(`${Z}Restarting from job "${E[k]}"${Y}\n`);continue}k++}}!d&&this.isInterrupted()&&(d=this.createInterruptedResult()),d||=(g=!0,this.logger.info(`\n${X}${`═`.repeat(60)}${Y}`),this.logger.info(` ${Lt}${X}Workflow completed successfully${Y}`),this.logger.info(` Jobs run: ${[...re].join(`, `)}`),h&&this.logger.info(` Worktree: ${h}`),this.logger.info(`${X}${`═`.repeat(60)}${Y}\n`),{exitCode:0,output:this.outputLines.join(`
|
|
45
|
+
`)})}catch(e){if(f=e,e instanceof P||this.isInterrupted())d=this.createInterruptedResult();else throw e}finally{if(g&&d?.exitCode===0&&v&&c.worktree?.beforeCompleted&&!t&&h&&(this.logger.info(`\n ${$}${X}worktree${Y} ${X}beforeCompleted${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.worktreeService.runHook(c.worktree.beforeCompleted,h,x,y,b,{runner:l,cliAgent:e.cliAgent,dryRun:t,continueOnError:n})||(this.isInterrupted()?d=this.createInterruptedResult():this.logger.warn(`\n${Z}${X}Warning: worktree beforeCompleted hook failed${Y}`))),c.post)try{await this.runPostBlock(c.post,x,y,b,{runner:l,cliAgent:e.cliAgent,dryRun:t,continueOnError:n,workflowDir:p})||(this.logger.error(`\n${Q}${X}${Gt}${Y}`),(!d||d.exitCode===0||d.exitCode===2)&&(d={exitCode:1,output:this.outputLines.join(`
|
|
46
|
+
`)}))}catch(e){f??=e,this.logger.error(`\n${Q}${X}${Gt}${Y}`),(!d||d.exitCode===0||d.exitCode===2)&&(d={exitCode:1,output:this.outputLines.join(`
|
|
47
|
+
`)})}g&&d?.exitCode===0&&v&&c.worktree?.afterCompleted&&!t&&!i&&(this.logger.info(`\n ${$}${X}worktree${Y} ${X}afterCompleted${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.worktreeService.runHook(c.worktree.afterCompleted,m,x,y,b,{runner:l,cliAgent:e.cliAgent,dryRun:t,continueOnError:n})||(this.isInterrupted()?d=this.createInterruptedResult():this.logger.warn(`\n${Z}${X}Warning: worktree afterCompleted hook failed${Y}`))),_&&h&&this.worktreeService.removeWorktree(m,h),d&&={...d,output:this.outputLines.join(`
|
|
48
|
+
`)},u&&await this.finalizeRegistryRun(u,d,f)}return d??{exitCode:1,output:this.outputLines.join(`
|
|
49
|
+
`)}}async finalizeRegistryRun(e,t,n){let r=t?.exitCode===130||n instanceof P,i=t?.exitCode??(r?130:1),a=i===0||i===2?`completed`:`error`,o=i===0?`success`:i===2?`skipped`:r||this.isInterrupted()?`interrupted`:`failed`,s=n instanceof Error?n.message:i===1?this.lastMeaningfulOutputLine():void 0;await this.registry.finalizeRun(e,{errorMessage:s,exitCode:i,outcome:o,stage:a})}buildLaunchCliCommand(e,t){let n=[`bun`,`packages/mcp/workflow-mcp/src/cli.ts`,`run-workflow`,t,`--skip-launch`],r=this.resolveRunner(e);if(e.job&&n.push(`--job`,e.job),r&&n.push(`--runner`,r),e.dryRun&&n.push(`--dry-run`),e.continueOnError&&n.push(`--continue-on-error`),e.keepWorktree&&n.push(`--keep-worktree`),e.prompt&&n.push(`--prompt`,`'${e.prompt.replace(/'/g,`'\\''`)}'`),e.name&&n.push(`--name`,`'${e.name}'`),e.workspace&&n.push(`--workspace`,`'${e.workspace}'`),e.secretFile&&n.push(`--secret-file`,e.secretFile),e.inputs)for(let[t,r]of Object.entries(e.inputs))n.push(`--input`,`${t}=${r}`);if(e.env)for(let[t,r]of Object.entries(e.env))n.push(`--env`,`${t}=${r}`);return n.join(` `)}lastMeaningfulOutputLine(){return[...this.outputLines].reverse().find(e=>e.trim().length>0)}async runPreBlock(e,t,n,r,i){if(this.logger.info(`\n ${$}${X}pre${Y} ${X}Pre-conditions${Y}`),this.logger.info(` ${`─`.repeat(50)}`),t.CONTEXT_FILE&&await(0,a.mkdir)((0,s.dirname)((0,s.resolve)(i.workflowDir,t.CONTEXT_FILE)),{recursive:!0}),e.services&&e.services.length>0){this.logger.info(`\n ${$}${X}services${Y}`);for(let n of e.services)if(await this.serviceManager.startService(n,t,i.workflowDir,i.dryRun)&&n[`ready-check`]&&!i.dryRun){let e=await this.serviceManager.waitForServiceReady(n,i.workflowDir,t,()=>this.interruptedSignal!==null);if(this.isInterrupted())return!1;if(!e&&!i.continueOnError)return await this.serviceManager.stopAll(),!1}!i.dryRun&&this.serviceManager.getRunningServices().length>0&&await this.waitForServiceStartup()}if(e.steps&&e.steps.length>0){this.logger.info(`\n ${$}${X}setup${Y}`);let a={inputs:n,matrix:{},secrets:r,env:t};for(let t of e.steps)if(!await this.stepRunner.runStep(t,a,i))return this.logger.error(`\n ${Q}Pre-condition step failed${Y}`),await this.serviceManager.stopAll(),!1}return this.logger.info(`\n ${Lt}Pre-conditions ready${Y}`),!0}async runPostBlock(e,t,n,r,i){this.logger.info(`\n ${$}${X}post${Y} ${X}Cleanup${Y}`),this.logger.info(` ${`─`.repeat(50)}`),await this.serviceManager.stopAll();try{if(e.services&&e.services.length>0){this.logger.info(`\n ${$}${X}services${Y}`);for(let n of e.services)if(await this.serviceManager.startService(n,t,i.workflowDir,i.dryRun)&&n[`ready-check`]&&!i.dryRun&&!await this.serviceManager.waitForServiceReady(n,i.workflowDir,t,()=>!1)&&!i.continueOnError)return!1;!i.dryRun&&this.serviceManager.getRunningServices().length>0&&await this.waitForServiceStartup()}if(e.steps&&e.steps.length>0){this.logger.info(`\n ${$}${X}cleanup${Y}`);let a={inputs:n,matrix:{},secrets:r,env:t};for(let t of e.steps)if(!await this.stepRunner.runStep(t,a,i))return this.logger.error(`\n ${Q}Post-cleanup step failed${Y}`),!1}return this.logger.info(`\n ${Lt}Cleanup complete${Y}`),!0}finally{await this.serviceManager.stopAll()}}};const qt=n.z.object({workflowPath:n.z.string().describe(`Path to the workflow YAML file (e.g., .github/workflows/deploy.yml)`),runner:n.z.string().optional().describe(`Preferred runner key for step command maps (e.g. ollama, claude, codex)`),cliAgent:n.z.string().optional().describe(`Deprecated alias for runner. Preferred runner command key for step command maps.`),job:n.z.string().optional().describe(`Run only this job (and its dependencies)`),inputs:n.z.record(n.z.string(),n.z.string()).optional().describe(`Workflow dispatch inputs as key-value pairs`),env:n.z.record(n.z.string(),n.z.string()).optional().describe(`Extra environment variables as key-value pairs`),secretFile:n.z.string().optional().describe(`Path to a dotenv-style secrets file`),dryRun:n.z.boolean().optional().describe(`Print steps without executing`),continueOnError:n.z.boolean().optional().describe(`Continue past step failures`),keepWorktree:n.z.boolean().optional().describe(`Keep worktree on completion (skip merge and cleanup for retry)`),prompt:n.z.string().optional().describe(`User prompt for user_prompt trigger workflows`),name:n.z.string().optional().describe(`Name for the workflow run context directory`),workspace:n.z.string().optional().describe(`Workspace for workflow registry storage`)});var Jt=class e{static TOOL_NAME=`run_workflow`;constructor(e=new Kt){this.service=e}getInputSchema(){return qt}getDefinition(){return{name:e.TOOL_NAME,description:`Run a GitHub Actions workflow file locally. Parses the workflow YAML and executes run steps on the host machine, respecting job dependencies, matrix strategies, environment variables, and workflow_dispatch inputs.`,inputSchema:n.z.toJSONSchema(qt)}}async execute(e){try{let t=qt.parse(e);if(t.runner&&t.cliAgent&&t.runner!==t.cliAgent)throw Error(`Conflicting runner selectors: runner="${t.runner}" cliAgent="${t.cliAgent}"`);let n={cliAgent:t.cliAgent,runner:t.runner??t.cliAgent,workflowPath:t.workflowPath,job:t.job,inputs:t.inputs,env:t.env,secretFile:t.secretFile,dryRun:t.dryRun,continueOnError:t.continueOnError,keepWorktree:t.keepWorktree,prompt:t.prompt,name:t.name,workspace:t.workspace},r=await this.service.run(n);return r.exitCode!==0&&r.exitCode!==2?{content:[{type:`text`,text:`Workflow failed (exit code ${r.exitCode}):\n\n${r.output}`}],isError:!0}:{content:[{type:`text`,text:r.output||`Workflow completed successfully.`}]}}catch(e){return{content:[{type:`text`,text:`Error: ${e instanceof Error?e.message:`Unknown error`}`}],isError:!0}}}},Yt=class e{static TOOL_NAME=`schedule-cron`;constructor(e=new D){this.service=e}getInputSchema(){return w}getDefinition(){return{name:e.TOOL_NAME,description:`Schedule a headless Claude Code or Codex CLI run as a system cron job. Specify either a cron expression or an interval in minutes.`,inputSchema:n.z.toJSONSchema(w)}}async execute(t){try{let e=w.parse(t),n=await this.service.schedule(e);return{content:[{type:`text`,text:JSON.stringify(n,null,2)}]}}catch(n){let r=new g(`Failed to schedule cron job.`,`SCHEDULE_CRON_TOOL_FAILED`,{tool:e.TOOL_NAME,name:t.name,cwd:t.cwd},{cause:n});return console.error(`[${e.TOOL_NAME}] ${r.message}`,r.context),{content:[{type:`text`,text:JSON.stringify({code:r.code,context:r.context,message:r.message},null,2)}],isError:!0}}}};const Xt=[Jt.TOOL_NAME,ke.TOOL_NAME,Yt.TOOL_NAME,O.TOOL_NAME];function Zt(){let n=new e.Server({name:`workflow-mcp`,version:`0.1.0`},{capabilities:{tools:{}}}),r=new Jt,i=new ke,a=new Yt,o=new O;return n.setRequestHandler(t.ListToolsRequestSchema,async()=>({tools:[r.getDefinition(),i.getDefinition(),a.getDefinition(),o.getDefinition()]})),n.setRequestHandler(t.CallToolRequestSchema,async e=>{let{name:t,arguments:n}=e.params;if(t===Jt.TOOL_NAME)return await r.execute(n);if(t===ke.TOOL_NAME)return await i.execute(n);if(t===Yt.TOOL_NAME)return await a.execute(n);if(t===O.TOOL_NAME)return await o.execute();throw new h(t,Xt)}),n}var Qt=class{server;transport=null;constructor(e){this.server=e}async start(){this.transport=new m.StdioServerTransport,await this.server.connect(this.transport),console.error(`workflow-mcp MCP server started on stdio`)}async stop(){this.transport&&=(await this.transport.close(),null)}};Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return De}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return Ge}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return Zt}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return D}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return Kt}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return`claude`}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return Qt}});
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agimon-ai/workflow-mcp",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "MCP server for running GitHub Actions workflows locally",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"typescript"
|
|
9
|
+
],
|
|
10
|
+
"license": "BUSL-1.1",
|
|
11
|
+
"bin": {
|
|
12
|
+
"workflow-mcp": "./dist/cli.cjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.mjs",
|
|
21
|
+
"types": "./dist/index.d.cts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/index.mjs",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./cli": {
|
|
28
|
+
"import": "./dist/cli.mjs",
|
|
29
|
+
"require": "./dist/cli.cjs"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
35
|
+
"chalk": "5.6.2",
|
|
36
|
+
"commander": "14.0.3",
|
|
37
|
+
"js-yaml": "4.1.1",
|
|
38
|
+
"zod": "4.3.6",
|
|
39
|
+
"@agimon-ai/foundation-port-registry": "0.8.3",
|
|
40
|
+
"@agimon-ai/foundation-process-registry": "0.8.3",
|
|
41
|
+
"@agimon-ai/foundation-validator": "0.5.3",
|
|
42
|
+
"@agimon-ai/log-sink-mcp": "0.8.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/js-yaml": "4.0.9",
|
|
46
|
+
"@types/node": "25.6.0",
|
|
47
|
+
"tsdown": "0.21.7",
|
|
48
|
+
"typescript": "6.0.2",
|
|
49
|
+
"vitest": "4.1.4"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"dev": "node --loader ts-node/esm src/cli.ts mcp-serve",
|
|
56
|
+
"build": "tsdown",
|
|
57
|
+
"test": "vitest --run",
|
|
58
|
+
"lint": "eslint src",
|
|
59
|
+
"typecheck": "tsc --noEmit"
|
|
60
|
+
}
|
|
61
|
+
}
|