@agimon-ai/po-workflow 0.0.0-alpha.56 → 0.0.0-alpha.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,6 +49,101 @@ needs, and `run` and `interactiveRun` steps each get their own RMUX pane. Each r
49
49
  log in `engine.log` in its run directory. See `docs/automation.md` for the full contract and the
50
50
  CLI fallback.
51
51
 
52
+ ## Job-specific tools
53
+
54
+ Managed Po agent steps can receive a workflow-owned `jobTools` code-mode namespace and a pinned
55
+ `workflow-job-tools` skill. Select a folder at workflow level as a default, or override it per job:
56
+
57
+ ```yaml
58
+ name: summarize
59
+ runConfig:
60
+ majorMode: copilot
61
+ tools: ./tools/shared
62
+ jobs:
63
+ report:
64
+ tools: ./tools/report
65
+ steps:
66
+ - name: Summarize
67
+ customRun: Read workflow-job-tools, discover jobTools, then summarize this job.
68
+ ```
69
+
70
+ The folder must contain JavaScript `index.mjs` and a non-empty `SKILL.md`. Paths are resolved relative
71
+ to the YAML that declares them, before imports and job-template inheritance. Empty paths, control
72
+ characters and URL specifiers are rejected. Parsing, listing and inspection never execute the module.
73
+ A job's main steps, `preJob` and `postJob` use its selection or the workflow default. `repair.tools`
74
+ overrides the default for repair triage. Global phases use the workflow default.
75
+
76
+ Only trusted, managed Po `customRun` and in-process `interactiveRun` agent sessions receive these
77
+ tools, not shell steps, ordinary sessions, or external Claude/Codex CLI runners. Each session uses its
78
+ captured workflow config. Global `pre` can run before the worktree YAML is reparsed, so its tools are
79
+ not necessarily selected from that worktree. Step `cwd`, `WORKTREE_PATH` and environment variables do
80
+ not select or resolve the tools folder.
81
+
82
+ `index.mjs` default-exports a factory, called once per managed session with:
83
+
84
+ - `sessionId` and the admitted working directory `cwd`.
85
+ - `workflow`: the trusted `PoWorkflowContextService`, including the captured config, job/step identity,
86
+ admitted `jobEnvironment` and `stepEnvironment`, and workflow state reader.
87
+ - Injected `fs` and `shell` capabilities.
88
+ - `signal`: the session tools lifetime, aborted on session disposal or mounting failure.
89
+
90
+ Return the existing `PoCodemodeNamespace` contract: name `jobTools`, a non-empty one-line `summary`
91
+ (maximum 600 characters), TypeScript `declarations` for discovery, and `bind(run)`. Bind returns flat
92
+ named async methods with JSON arguments and results. Each exec gets a fresh binding; keep per-session
93
+ state inside the factory, not at module scope. Validate method inputs and honour `run.signal` in long
94
+ operations. Use `run.onDispose` for exec-owned cleanup. The host refuses foreign sessions and calls
95
+ after cancellation/disposal, and detaches JSON values in both directions. Return `null`, not
96
+ `undefined`, for no output.
97
+
98
+ A minimal runnable `tools/report/index.mjs`:
99
+
100
+ ```js
101
+ export default function createJobTools({ sessionId, workflow, signal }) {
102
+ signal.throwIfAborted();
103
+ return {
104
+ name: 'jobTools',
105
+ summary: 'Describe the current workflow job with a supplied label.',
106
+ declarations: `declare const jobTools: {
107
+ describe(input: { label: string }): Promise<{ job: string; label: string }>;
108
+ };`,
109
+ bind(run) {
110
+ if (run.sessionId !== sessionId) throw new Error('Wrong session.');
111
+ return {
112
+ async describe(input) {
113
+ run.signal.throwIfAborted();
114
+ if (!input || typeof input.label !== 'string' || !input.label.trim())
115
+ throw new Error('label must be a non-empty string.');
116
+ return { job: workflow.job, label: input.label.trim() };
117
+ },
118
+ };
119
+ },
120
+ };
121
+ }
122
+ ```
123
+
124
+ Corresponding `tools/report/SKILL.md`:
125
+
126
+ ```md
127
+ # Report tools
128
+
129
+ Use jobTools.describe to label the current job before writing its summary.
130
+ Discover jobTools for the current signature, then call it through exec.
131
+ Do not guess a label or repeat an effect after cancellation without checking its outcome.
132
+ ```
133
+
134
+ Agents read the pinned skill, call `discover({ namespace: 'jobTools' })`, then use
135
+ `exec` with `return await jobTools.describe({ label: 'Verification' });`. Keep guidance in `SKILL.md`
136
+ and signatures in `declarations`. Optional namespace `approval` policies are preserved and enforced
137
+ by the existing code-mode approval path; each policy must name a returned method. An approval request
138
+ is not permission to bypass the normal approval flow.
139
+
140
+ Tools are trusted workflow code running with the host's privileges, not sandboxed code. Review the
141
+ folder before launching. Native import caches modules by file URL; editing `index.mjs` at the same
142
+ path requires a host restart. There is no direct TypeScript loading, hot reload, or external CLI
143
+ bridge. A missing file or invalid factory fails the managed step session rather than silently
144
+ omitting its selected tools. The namespace and skill mount and dispose together; disposing them does
145
+ not stop the owning workflow or change its recovery rules.
146
+
52
147
  ## Launch and monitor
53
148
 
54
149
  Core tools are:
@@ -98,8 +193,9 @@ adopts the eligible failure, validates the evidence, and transfers ownership to
98
193
  Live controls remain scoped to the owning session. Recovery does not launch a second copy beside a
99
194
  still-running job and does not guarantee that parent-session work survives shutdown.
100
195
 
101
- The package ships two skills: pinned, ungated `codemode-how-to-workflow` for both namespaces,
102
- and unpinned, Help-gated `help-how-to-workflow` for authoring and maintenance.
196
+ The package ships two skills: pinned, ungated `codemode-how-to-workflow` for workflow code-mode
197
+ namespaces and selected job tools, and unpinned, Help-gated `help-how-to-workflow` for authoring and
198
+ maintenance. A selected tools folder also supplies the managed session's pinned `workflow-job-tools` skill.
103
199
 
104
200
  ## Workflow sessions
105
201
 
@@ -12,6 +12,7 @@ let node_fs = require("node:fs");
12
12
  let node_fs_promises = require("node:fs/promises");
13
13
  node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises, 1);
14
14
  let node_readline = require("node:readline");
15
+ let node_url = require("node:url");
15
16
  let proper_lockfile = require("proper-lockfile");
16
17
  let node_perf_hooks = require("node:perf_hooks");
17
18
  let node_os = require("node:os");
@@ -250,7 +251,9 @@ const PreBlockSchema = zod.z.object({
250
251
  steps: zod.z.array(StepSchema).optional()
251
252
  });
252
253
  const JobHookBlockSchema = zod.z.object({ steps: zod.z.array(StepSchema).optional() });
254
+ const WorkflowToolsPathSchema = zod.z.string().min(1).refine((value) => value.trim().length > 0 && !/^[a-z][a-z0-9+.-]*:/iu.test(value.trimStart()) && value.split("").every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127), "Workflow tools must be a local filesystem path without URL schemes or control characters.").describe("Tools folder relative to its declaring YAML file, containing index.mjs and SKILL.md.");
253
255
  const JobSchema = zod.z.object({
256
+ tools: WorkflowToolsPathSchema.optional(),
254
257
  "runs-on": zod.z.string().optional(),
255
258
  needs: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
256
259
  extends: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
@@ -329,6 +332,7 @@ const RunDirectorySchema = zod.z.object({
329
332
  });
330
333
  const WorkflowSchema = zod.z.object({
331
334
  name: zod.z.string().optional(),
335
+ tools: WorkflowToolsPathSchema.optional(),
332
336
  tags: zod.z.array(zod.z.string().trim().min(1)).optional().describe("Tags used to categorize and filter workflow definitions."),
333
337
  workspace: zod.z.string().optional(),
334
338
  runner: zod.z.string().optional().describe("Default runner key for step command maps when no CLI runner is provided."),
@@ -1771,6 +1775,21 @@ var WorkflowRegistryService = class {
1771
1775
  ownerSessionId
1772
1776
  });
1773
1777
  }
1778
+ /** A person's Stop remains terminal even when the owning process is already gone. */
1779
+ async requestStaleStop(workspace, runKey, reason, expectedRunId, ownerSessionId) {
1780
+ const request = {
1781
+ reason: reason?.trim() || void 0,
1782
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
1783
+ runId: expectedRunId,
1784
+ schemaVersion: 1
1785
+ };
1786
+ await this.reconcileStaleRunningRecord(this.resolveWorkspace(workspace), this.slugifySegment(runKey, FALLBACK_RUN_KEY), {
1787
+ runId: expectedRunId,
1788
+ ownerSessionId,
1789
+ stop: request
1790
+ });
1791
+ return request;
1792
+ }
1774
1793
  /**
1775
1794
  * Ensure the stage directories exist for the workspace.
1776
1795
  */
@@ -1884,7 +1903,18 @@ var WorkflowRegistryService = class {
1884
1903
  const runningRecordPath = (0, node_path.resolve)(runningDir, RUN_RECORD_FILENAME$1);
1885
1904
  return this.writeCoordinator.forKey(runningRecordPath)(async () => {
1886
1905
  const currentRecord = await this.readRunRecord(runningRecordPath);
1887
- if (expected !== void 0 && (currentRecord.runId !== expected.runId || currentRecord.env?.PI_SESSION_ID !== expected.ownerSessionId) || !currentRecord.pid || this.isProcessAlive(currentRecord.pid)) return currentRecord;
1906
+ if (expected !== void 0 && (currentRecord.runId !== expected.runId || currentRecord.env?.PI_SESSION_ID !== expected.ownerSessionId)) {
1907
+ if (expected.stop) throw new Error(`Workflow run ownership changed: ${workspace}/${runKey}`);
1908
+ return currentRecord;
1909
+ }
1910
+ if (!currentRecord.pid || this.isProcessAlive(currentRecord.pid)) {
1911
+ if (expected?.stop) throw new Error(`Workflow process is not known to be dead: ${workspace}/${runKey}`);
1912
+ return currentRecord;
1913
+ }
1914
+ if (expected?.stop) {
1915
+ if (currentRecord.stage !== RUNNING_STAGE) throw new Error(`Workflow run is no longer controllable: ${workspace}/${runKey}`);
1916
+ await this.writeAtomic((0, node_path.resolve)(runningDir, STOP_REQUEST_FILENAME), expected.stop);
1917
+ }
1888
1918
  if (currentRecord.stage !== RUNNING_STAGE) {
1889
1919
  const stageDir = this.getRunDirectory(workspace, currentRecord.stage, runKey);
1890
1920
  await this.removeFileIfExists((0, node_path.resolve)(runningDir, FINALIZING_FILENAME));
@@ -1903,14 +1933,15 @@ var WorkflowRegistryService = class {
1903
1933
  failedJob: currentRecord.executionCursor?.job ?? currentRecord.failedJob,
1904
1934
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1905
1935
  outcome: "interrupted",
1906
- stage: ERROR_STAGE$2
1936
+ stage: ERROR_STAGE$2,
1937
+ ...expected?.stop ? { resume: void 0 } : {}
1907
1938
  };
1939
+ await this.writeRunRecord(runningRecordPath, finalRecord);
1908
1940
  (0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.remove(errorDir, {
1909
1941
  recursive: true,
1910
1942
  force: true
1911
1943
  }, this.runtime.ioContext));
1912
1944
  (0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.renameFile(runningDir, errorDir, this.runtime.ioContext));
1913
- await this.writeRunRecord((0, node_path.resolve)(errorDir, RUN_RECORD_FILENAME$1), finalRecord);
1914
1945
  return finalRecord;
1915
1946
  });
1916
1947
  }
@@ -2437,6 +2468,7 @@ var WorkflowParserService = class {
2437
2468
  this.githubSha = "local";
2438
2469
  }
2439
2470
  const workflow = WorkflowSchema.parse((0, js_yaml.load)(content));
2471
+ this.resolveToolsPaths(workflow, resolved);
2440
2472
  if (workflow.imports) await this.resolveImports(workflow, resolved);
2441
2473
  if (!workflow.jobs || Object.keys(workflow.jobs).length === 0) throw new Error("No jobs found in workflow file");
2442
2474
  this.resolveExtends(workflow);
@@ -2455,11 +2487,20 @@ var WorkflowParserService = class {
2455
2487
  const parsed = (0, js_yaml.load)(content);
2456
2488
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Workflow template must be a YAML object: ${resolved}`);
2457
2489
  const workflow = parsed;
2490
+ this.resolveToolsPaths(workflow, resolved);
2458
2491
  workflow.jobs ??= {};
2459
2492
  if (workflow.imports) await this.resolveImports(workflow, resolved);
2460
2493
  workflow.jobs ??= {};
2461
2494
  return workflow;
2462
2495
  }
2496
+ /** Resolve before merging so imported jobs and defaults keep their declaring file's origin. */
2497
+ resolveToolsPaths(workflow, workflowPath) {
2498
+ for (const declaration of [
2499
+ workflow,
2500
+ ...Object.values(workflow.jobs ?? {}),
2501
+ workflow.repair
2502
+ ]) if (declaration?.tools !== void 0) declaration.tools = (0, node_path.resolve)((0, node_path.dirname)(workflowPath), WorkflowToolsPathSchema.parse(declaration.tools));
2503
+ }
2463
2504
  /**
2464
2505
  * Render a command template into the shell command a step runs.
2465
2506
  *
@@ -2601,7 +2642,8 @@ var WorkflowParserService = class {
2601
2642
  * - system-prompt: concatenated (imported + main)
2602
2643
  * - commands, choices: merged per name (main overrides imported)
2603
2644
  * - runConfig, sessionConfig: merged per key (main overrides imported)
2604
- * Imports are processed in order; later imports override earlier ones.
2645
+ * - tools: scalar default (main wins, otherwise first imported selection)
2646
+ * Imports are processed in order; earlier merged values act as main overrides.
2605
2647
  * Circular imports are detected and throw an error.
2606
2648
  */
2607
2649
  async resolveImports(workflow, workflowPath, visited = /* @__PURE__ */ new Set()) {
@@ -2614,7 +2656,9 @@ var WorkflowParserService = class {
2614
2656
  if (!await (0, _agimon_ai_po_core_platform.pathExists)(this.runtime.fs, resolvedImport, this.runtime.ioContext)) throw new Error(`Imported workflow file not found: ${resolvedImport} (from ${normalizedPath})`);
2615
2657
  const importContent = (0, _agimon_ai_po_core_platform.getOrThrow)(await this.runtime.fs.readTextFile(resolvedImport, this.runtime.ioContext));
2616
2658
  const imported = (0, js_yaml.load)(importContent);
2659
+ this.resolveToolsPaths(imported, resolvedImport);
2617
2660
  if (imported.imports) await this.resolveImports(imported, resolvedImport, visited);
2661
+ workflow.tools ??= imported.tools;
2618
2662
  if (imported.env) workflow.env = {
2619
2663
  ...imported.env,
2620
2664
  ...workflow.env
@@ -2694,7 +2738,7 @@ var WorkflowParserService = class {
2694
2738
  * - postJob.steps: template steps prepended before job postJob steps
2695
2739
  * - env: merged (job overrides template)
2696
2740
  * - system-prompt: concatenated (template + job)
2697
- * - all other fields: job overrides template
2741
+ * - runs-on, strategy, context, tools: inherited when not set on the job
2698
2742
  * Template jobs are removed from the jobs map after resolution.
2699
2743
  */
2700
2744
  resolveExtends(workflow) {
@@ -2743,6 +2787,7 @@ var WorkflowParserService = class {
2743
2787
  if (template["runs-on"] && !job["runs-on"]) job["runs-on"] = template["runs-on"];
2744
2788
  if (template.strategy && !job.strategy) job.strategy = template.strategy;
2745
2789
  if (template.context && !job.context) job.context = template.context;
2790
+ job.tools ??= template.tools;
2746
2791
  }
2747
2792
  mergeJobHookSteps(job, template, hookKey) {
2748
2793
  const templateSteps = template[hookKey]?.steps ?? [];
@@ -5109,6 +5154,10 @@ function createWorkflowHost(options) {
5109
5154
  input: process.stdin,
5110
5155
  output: process.stdout
5111
5156
  }),
5157
+ importJobTools: (path) => import(
5158
+ /* @vite-ignore */
5159
+ (0, node_url.pathToFileURL)(path).href
5160
+ ),
5112
5161
  openArtifact,
5113
5162
  writeExclusive: (path, content) => (0, node_fs_promises.writeFile)(path, content, {
5114
5163
  flag: "wx",
@@ -11359,6 +11408,12 @@ Object.defineProperty(exports, "WorkflowStageSchema", {
11359
11408
  return WorkflowStageSchema;
11360
11409
  }
11361
11410
  });
11411
+ Object.defineProperty(exports, "WorkflowToolsPathSchema", {
11412
+ enumerable: true,
11413
+ get: function() {
11414
+ return WorkflowToolsPathSchema;
11415
+ }
11416
+ });
11362
11417
  Object.defineProperty(exports, "connectNativeTerminal", {
11363
11418
  enumerable: true,
11364
11419
  get: function() {
@@ -11486,4 +11541,4 @@ Object.defineProperty(exports, "workflowShellOutput", {
11486
11541
  }
11487
11542
  });
11488
11543
 
11489
- //# sourceMappingURL=RecoverWorkflowService-C1lWL4fC.cjs.map
11544
+ //# sourceMappingURL=RecoverWorkflowService-DL5itVWA.cjs.map