@juicesharp/rpiv-workflow 1.16.1 → 1.17.1

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
@@ -41,12 +41,14 @@ The loader merges workflows from three layers (each later layer overrides earlie
41
41
 
42
42
  ```
43
43
  built-in (programmatic — registered by sibling packages like rpiv-pi)
44
- ← user packs (~/.config/rpiv-workflow/workflows/*.ts, alpha-sorted)
45
- ← user config (~/.config/rpiv-workflow/workflows.config.ts)
46
- ← project packs (<cwd>/.rpiv-workflow/workflows/*.ts, alpha-sorted)
47
- ← project config (<cwd>/.rpiv-workflow/workflows.config.ts)
44
+ ← user packs (~/.config/rpiv-workflow/packs/*.ts, alpha-sorted)
45
+ ← user config (~/.config/rpiv-workflow/config.ts)
46
+ ← project packs (<cwd>/.rpiv/workflows/packs/*.ts, alpha-sorted)
47
+ ← project config (<cwd>/.rpiv/workflows/config.ts)
48
48
  ```
49
49
 
50
+ Run state for each `/wf` invocation lands under `<cwd>/.rpiv/workflows/runs/<run-id>.jsonl` — the third subfolder of the same domain dir.
51
+
50
52
  Two file roles per layer:
51
53
 
52
54
  - **Config file** — the one TypeScript file you hand-edit. Accepts three default-export shapes:
@@ -71,7 +73,25 @@ Two file roles per layer:
71
73
  };
72
74
  ```
73
75
 
74
- - **Pack files** (`workflows/*.ts`) — installable bundles others can drop in. Accept only `Workflow | Workflow[]`. Packs **cannot** set `default` — that lives in the config file. This is what makes installable workflow packs safe: a pack contributes new workflows without overriding the user's default.
76
+ - **Pack files** (`packs/*.ts`) — installable bundles others can drop in. Accept only `Workflow | Workflow[]`. Packs **cannot** set `default` — that lives in the config file. This is what makes installable workflow packs safe: a pack contributes new workflows without overriding the user's default.
77
+
78
+ ### Skill aliases
79
+
80
+ `skillAliases` remaps a skill name across **every** loaded workflow (built-in + user + project) with one declarative config entry — no workflow redeclaration. It's a config-file-only envelope field (packs reject it), applied at load time so preview, the JSONL audit, and the runtime skill preflight all see the final skill; the runner is untouched.
81
+
82
+ ```ts
83
+ // config.ts — alias-only is valid (no `workflows` needed)
84
+ export default { skillAliases: { commit: "attributed-commit" } };
85
+
86
+ // or alongside workflows + default
87
+ export default {
88
+ workflows: [ /* … */ ],
89
+ default: "ship",
90
+ skillAliases: { commit: "attributed-commit", "code-review": "strict-review" },
91
+ };
92
+ ```
93
+
94
+ The key is the **skill** name (`stage.skill ?? <stage key>`), not the stage id. One hop only (no transitive chains); `run`/`prompt` stages are skipped; aliases merge project-over-user per key. `/wf` shows a `Skill aliases in effect: …` banner; an alias matching no dispatched skill warns at load time (no-op); a bad target reuses the existing runtime "skill not found" preflight. Use it to point a bundled skill (say `commit`) at your own variant (`attributed-commit`) everywhere, upgrade-safe.
75
95
 
76
96
  ## Authoring DSL
77
97
 
@@ -9,6 +9,7 @@ A workflow chains Pi skills into a typed multi-stage graph with audited JSONL st
9
9
  - [Layer merging](#layer-merging)
10
10
  - [Config files](#config-files)
11
11
  - [Pack files](#pack-files)
12
+ - [Skill aliases](#skill-aliases)
12
13
  - [Example](#example)
13
14
 
14
15
  ## Running workflows
@@ -24,15 +25,16 @@ Running `/wf` without arguments shows a list of every loaded workflow and its st
24
25
  ## File structure
25
26
 
26
27
  ```
27
- <cwd>/.rpiv-workflow/
28
- ├── workflows.config.ts # The project's workflow config (hand-edited)
29
- └── workflows/ # Pack files (installable bundles)
30
- ├── my-pipeline.ts
31
- └── ship.ts
28
+ <cwd>/.rpiv/workflows/
29
+ ├── config.ts # The project's workflow config (hand-edited)
30
+ ├── packs/ # Pack files (installable bundles)
31
+ ├── my-pipeline.ts
32
+ └── ship.ts
33
+ └── runs/ # Audited JSONL run state (<run-id>.jsonl)
32
34
 
33
35
  ~/.config/rpiv-workflow/
34
- ├── workflows.config.ts # User-level config
35
- └── workflows/ # User-level packs
36
+ ├── config.ts # User-level config
37
+ └── packs/ # User-level packs
36
38
  ```
37
39
 
38
40
  Every workflow file is TypeScript, loaded via `jiti` (no build step required). Import the authoring DSL from `@juicesharp/rpiv-workflow`:
@@ -47,17 +49,17 @@ The loader merges workflows from five layers. Each later layer overrides earlier
47
49
 
48
50
  ```
49
51
  built-in (registered by sibling packages like rpiv-pi)
50
- ← user packs (~/.config/rpiv-workflow/workflows/*.ts, alpha-sorted)
51
- ← user config (~/.config/rpiv-workflow/workflows.config.ts)
52
- ← project packs (<cwd>/.rpiv-workflow/workflows/*.ts, alpha-sorted)
53
- ← project config (<cwd>/.rpiv-workflow/workflows.config.ts)
52
+ ← user packs (~/.config/rpiv-workflow/packs/*.ts, alpha-sorted)
53
+ ← user config (~/.config/rpiv-workflow/config.ts)
54
+ ← project packs (<cwd>/.rpiv/workflows/packs/*.ts, alpha-sorted)
55
+ ← project config (<cwd>/.rpiv/workflows/config.ts)
54
56
  ```
55
57
 
56
58
  Within a layer, the config file wins by workflow name over pack files. Only the config file may set the `default` workflow (the one `/wf <input>` runs without specifying a name). Defaults cascade: `project config > user config > first registered workflow`.
57
59
 
58
60
  ## Config files
59
61
 
60
- The config file (`workflows.config.ts`) is the one TypeScript file you hand-edit. It accepts three default-export shapes:
62
+ The config file (`config.ts`) is the one TypeScript file you hand-edit. It accepts three default-export shapes:
61
63
 
62
64
  ```typescript
63
65
  // 1. A single Workflow
@@ -81,7 +83,7 @@ export default {
81
83
 
82
84
  ## Pack files
83
85
 
84
- Pack files (`workflows/*.ts`) are installable bundles others can drop in. They accept only `Workflow | Workflow[]`. Packs **cannot** set `default` — that lives in the config file.
86
+ Pack files (`packs/*.ts`) are installable bundles others can drop in. They accept only `Workflow | Workflow[]`. Packs **cannot** set `default` — that lives in the config file.
85
87
 
86
88
  ```typescript
87
89
  // workflows/my-pipeline.ts
@@ -96,6 +98,26 @@ export default defineWorkflow({
96
98
 
97
99
  This is what makes installable workflow packs safe: a pack contributes new workflows without overriding the user's default.
98
100
 
101
+ ## Skill aliases
102
+
103
+ `skillAliases` remaps a skill name everywhere — across built-in, user, and project workflows — with one declarative config entry. It lives in the config-file envelope (packs can't set it) and is applied at load time, so `/wf` preview, the JSONL audit, and the runtime skill-registry preflight all see the final skill:
104
+
105
+ ```typescript
106
+ // .rpiv/workflows/config.ts
107
+ export default {
108
+ skillAliases: { commit: "attributed-commit" },
109
+ };
110
+
111
+ // composes with workflows + default:
112
+ export default {
113
+ workflows: [myWorkflow],
114
+ default: "ship",
115
+ skillAliases: { commit: "attributed-commit", "code-review": "strict-review" },
116
+ };
117
+ ```
118
+
119
+ Every dispatching stage whose effective skill (`stage.skill ?? <stage key>`) matches an alias key is remapped to the target — note the key is the **skill** name, not the stage id. The mapping is one hop only (no transitive chains), skips `run`/`prompt` stages (they don't dispatch a `/skill:`), and merges **project over user** per key. An alias-only config (no `workflows`) is valid. `/wf` shows a `Skill aliases in effect: commit → attributed-commit` banner; an alias key that matches no dispatched skill in any workflow surfaces a load-time warning (a harmless no-op). A bad alias **target** (a skill that doesn't exist) is caught by the existing runtime "skill not found" preflight.
120
+
99
121
  ## Example
100
122
 
101
123
  A minimal workflow that chains two skills:
@@ -117,6 +139,6 @@ export default defineWorkflow({
117
139
  });
118
140
  ```
119
141
 
120
- Save this as `.rpiv-workflow/workflows.config.ts` in your project, then run `/wf review-and-ship implement auth feature`.
142
+ Save this as `.rpiv/workflows/config.ts` in your project, then run `/wf review-and-ship implement auth feature`.
121
143
 
122
144
  For the full DSL reference (all stage factories, routing, outcomes, validators), see [workflow-authoring.md](./workflow-authoring.md).
package/index.ts CHANGED
@@ -34,7 +34,9 @@
34
34
  * 3. Loader (programmatic embedders) — `./load/index.js`
35
35
  * Materialise the merged workflow registry: `loadWorkflows`,
36
36
  * `LoadedWorkflows`, `Issue`, `LoadIssue`, `ConfigLayer`,
37
- * `OverlayPaths`, `projectOverlayPaths`, `userOverlayPaths`.
37
+ * `OverlayPaths`, `projectOverlayPaths`, `userOverlayPaths`,
38
+ * `aliasSkills`. Siblings can apply the same remap to a built-in
39
+ * workflow before handing it to `runWorkflow`.
38
40
  *
39
41
  * 4. Built-in registry (sibling packages) — `./built-ins.js`
40
42
  * Contribute workflows to the lowest config layer:
@@ -74,9 +76,9 @@
74
76
  * `validateOutputData`, `SchemaValidationFailure`.
75
77
  *
76
78
  * 8. Persistence (low-level — JSONL inspect) — `./state/index.js`
77
- * Read past runs at `<cwd>/.rpiv/workflows/<run-id>.jsonl`:
79
+ * Read past runs at `<cwd>/.rpiv/workflows/runs/<run-id>.jsonl`:
78
80
  * `listRuns`, `readHeader`, `readLastStage`, `listArtifacts`,
79
- * `stateFilePath`, `workflowsDir`, `RunSummary`,
81
+ * `stateFilePath`, `runsDir`, `RunSummary`,
80
82
  * `WorkflowHeader`, `WorkflowStage`. `recordStage` lives on
81
83
  * `@juicesharp/rpiv-workflow/internal` (test-only — rpiv-pi's
82
84
  * `[I3]` regression test pokes it directly; runner owns row
@@ -166,7 +168,7 @@ export {
166
168
  export type { WorkflowContext, WorkflowHost } from "./host.js";
167
169
  export { type LifecycleContext, type LifecycleListeners, registerLifecycle, type StageRef } from "./lifecycle.js";
168
170
  export type { ConfigLayer, Issue, LoadedWorkflows, LoadIssue, OverlayPaths } from "./load/index.js";
169
- export { loadWorkflows, projectOverlayPaths, userOverlayPaths } from "./load/index.js";
171
+ export { aliasSkills, loadWorkflows, projectOverlayPaths, userOverlayPaths } from "./load/index.js";
170
172
  export {
171
173
  type DirectoryPathCollectorOpts,
172
174
  directoryPathCollector,
@@ -213,10 +215,10 @@ export {
213
215
  type RunSummary,
214
216
  readHeader,
215
217
  readLastStage,
218
+ runsDir,
216
219
  stateFilePath,
217
220
  type WorkflowHeader,
218
221
  type WorkflowStage,
219
- workflowsDir,
220
222
  } from "./state/index.js";
221
223
  export { DEFAULT_TRIGGER, type RunTrigger } from "./triggers.js";
222
224
  export { typeboxSchema } from "./typebox-adapter.js";
@@ -224,11 +226,17 @@ export type { RunState } from "./types.js";
224
226
  export { type SchemaValidationFailure, validateOutputData } from "./validate-output.js";
225
227
  export { validateWorkflow, type WorkflowValidationIssue } from "./validate-workflow.js";
226
228
 
227
- export default function (host: WorkflowHost): void {
229
+ /**
230
+ * Local intersection of the two host ports this extension's `default`
231
+ * function needs. `WorkflowHost` covers the workflow-command surface;
232
+ * `DocsProtocolHost` covers the `on("before_agent_start", ...)` hook used
233
+ * to prepend the docs-protocol block. Pi's `ExtensionAPI` structurally
234
+ * satisfies both; programmatic embedders keep using the narrower
235
+ * `WorkflowHost` port, so this alias stays local — not re-exported.
236
+ */
237
+ type ExtensionHost = WorkflowHost & DocsProtocolHost;
238
+
239
+ export default function (host: ExtensionHost): void {
228
240
  registerWorkflowCommand(host);
229
- // Pi passes the full ExtensionAPI at runtime, which has on().
230
- // WorkflowHost doesn't declare on() to keep the programmatic-embedder
231
- // surface narrow. Cast to satisfy the type — Pi always provides the
232
- // full API.
233
- registerDocsProtocol(host as unknown as DocsProtocolHost);
241
+ registerDocsProtocol(host);
234
242
  }
package/load/alias.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Declarative skill-name remapping, applied once at load time.
3
+ *
4
+ * `aliasSkills(w, aliases)` rewrites every dispatching stage whose effective
5
+ * skill (`stage.skill ?? stageName`) has an alias entry, materialising the
6
+ * `skill` field on stages that relied on the stage-key default. Because the
7
+ * remap happens in the loader — before validation and before the runner ever
8
+ * sees the workflow — `/wf` preview, JSONL audit, and the runtime
9
+ * skill-registry preflight all observe the final skill for free; the runner
10
+ * needs no change.
11
+ *
12
+ * Invariants:
13
+ * - One hop only — looks up `aliases[effective]`, never `aliases[target]`.
14
+ * No transitive chains, no cycles.
15
+ * - `run` / `prompt` stages are skipped — they don't dispatch a `/skill:`.
16
+ * (`fanout` / `iterate` stages carry neither `run` nor `prompt`, so they
17
+ * ARE dispatching stages and get aliased.)
18
+ * - Never mutates its input. Returns `w` by reference when nothing changed,
19
+ * so the shared process-wide built-in registry (anchored on a `globalThis`
20
+ * slot) is never mutated in place; a changed workflow is a new frozen copy.
21
+ */
22
+
23
+ import type { StageDef, Workflow } from "../api.js";
24
+ import type { LayerOutcome, LoadAccumulator } from "./merge.js";
25
+
26
+ /**
27
+ * A stage dispatches a `/skill:<name>` exactly when it carries neither a `run`
28
+ * (script body) nor a `prompt` (raw-text body). `fanout`/`iterate` stages carry
29
+ * neither, so they ARE dispatching stages. Single source of truth shared by the
30
+ * alias remap below (which stages to rewrite) and the no-op-alias warning in
31
+ * `applySkillAliases` (which skills count as "dispatched") — the two must agree.
32
+ */
33
+ export function isDispatchingStage(stage: StageDef): boolean {
34
+ return stage.run == null && stage.prompt == null;
35
+ }
36
+
37
+ export function aliasSkills(w: Workflow, aliases: Record<string, string>): Workflow {
38
+ if (!aliases || Object.keys(aliases).length === 0) return w;
39
+ let changed = false;
40
+ const stages: typeof w.stages = {};
41
+ for (const [name, stage] of Object.entries(w.stages)) {
42
+ const dispatches = isDispatchingStage(stage); // only /skill: stages
43
+ const effective = stage.skill ?? name;
44
+ const target = aliases[effective];
45
+ if (dispatches && target && target !== effective) {
46
+ stages[name] = { ...stage, skill: target }; // materialise implicit skill
47
+ changed = true;
48
+ } else {
49
+ stages[name] = stage;
50
+ }
51
+ }
52
+ return changed ? Object.freeze({ ...w, stages }) : w; // never mutate shared built-ins
53
+ }
54
+
55
+ /**
56
+ * Apply skill-alias remapping to every workflow in the accumulator.
57
+ *
58
+ * Merges `userOutcome.skillAliases` and `projectOutcome.skillAliases` per-key
59
+ * (project wins), snapshots the pre-remap dispatched-skill set so no-op
60
+ * warnings compare against skills authors actually wrote (not alias targets
61
+ * freshly introduced by this very remap), then rewrites every workflow via
62
+ * `aliasSkills`. The runner is untouched — by the time `runWorkflow` runs
63
+ * every `stage.skill` already reflects the final target.
64
+ *
65
+ * No-op warnings attribute to the source layer: each layer's alias map is
66
+ * walked separately so a user-layer typo points at `~/.config/rpiv-workflow/`
67
+ * and a project-layer typo points at `<cwd>/.rpiv/workflows/`. A key declared
68
+ * in BOTH layers and no-op in both emits two warnings (one per layer) so the
69
+ * user fixes both files.
70
+ *
71
+ * Mutates `acc.workflowMap` and `acc.issues` in place (same precedent as
72
+ * `loadLayer`). Returns the merged alias map for the
73
+ * `LoadedWorkflows.skillAliases` envelope; `{}` when no layer declared any.
74
+ */
75
+ export function applySkillAliases(
76
+ acc: LoadAccumulator,
77
+ userOutcome: LayerOutcome,
78
+ projectOutcome: LayerOutcome,
79
+ ): Record<string, string> {
80
+ const userAliases = userOutcome.skillAliases ?? {};
81
+ const projectAliases = projectOutcome.skillAliases ?? {};
82
+ const merged: Record<string, string> = { ...userAliases, ...projectAliases };
83
+ if (Object.keys(merged).length === 0) return merged;
84
+
85
+ // Snapshot the pre-remap dispatched-skill set so the "no-op alias" warning
86
+ // compares against the skills authors actually wrote — not alias targets
87
+ // freshly introduced by this very remap.
88
+ const dispatchedBefore = new Set<string>();
89
+ for (const w of acc.workflowMap.values()) {
90
+ for (const [stageName, stage] of Object.entries(w.stages)) {
91
+ if (isDispatchingStage(stage)) dispatchedBefore.add(stage.skill ?? stageName);
92
+ }
93
+ }
94
+ for (const [name, w] of acc.workflowMap) acc.workflowMap.set(name, aliasSkills(w, merged));
95
+
96
+ // Per-source-layer no-op attribution: walk each layer's map separately so
97
+ // each warning points at the file that actually declared the key. A key
98
+ // declared by BOTH layers and no-op in both emits two warnings — one per
99
+ // layer — so the user fixes both files.
100
+ for (const [layer, map] of [
101
+ ["user", userAliases],
102
+ ["project", projectAliases],
103
+ ] as const) {
104
+ for (const key of Object.keys(map)) {
105
+ if (!dispatchedBefore.has(key)) {
106
+ acc.issues.push({
107
+ kind: "load",
108
+ layer,
109
+ severity: "warning",
110
+ message: `skillAliases: "${key}" matches no dispatched skill in any workflow (no-op).`,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ return merged;
116
+ }
package/load/index.ts CHANGED
@@ -7,10 +7,10 @@
7
7
  * they installed.
8
8
  *
9
9
  * Paths (per layer):
10
- * user — config `~/.config/rpiv-workflow/workflows.config.ts`
11
- * packs `~/.config/rpiv-workflow/workflows/*.ts`
12
- * project — config `<cwd>/.rpiv-workflow/workflows.config.ts`
13
- * packs `<cwd>/.rpiv-workflow/workflows/*.ts`
10
+ * user — config `~/.config/rpiv-workflow/config.ts`
11
+ * packs `~/.config/rpiv-workflow/packs/*.ts`
12
+ * project — config `<cwd>/.rpiv/workflows/config.ts`
13
+ * packs `<cwd>/.rpiv/workflows/packs/*.ts`
14
14
  *
15
15
  * Config file — accepts three default-export shapes:
16
16
  * 1. A single `Workflow` — single-entry namespace
@@ -38,7 +38,7 @@
38
38
  * tool that respects `<cwd>` configuration: Pi already operates in a
39
39
  * context that implicitly trusts the current working directory. Users
40
40
  * running Pi in a freshly-cloned untrusted repo should diff
41
- * `.rpiv-workflow/workflows.config.ts` and `.rpiv-workflow/workflows/*.ts`
41
+ * `.rpiv/workflows/config.ts` and `.rpiv/workflows/packs/*.ts`
42
42
  * (the config file + pack files) before running `/wf`.
43
43
  *
44
44
  * Module map:
@@ -50,12 +50,16 @@
50
50
  * ./cache.ts — mtime-keyed jiti import cache + __resetLoadCache
51
51
  */
52
52
 
53
+ import { existsSync, readdirSync } from "node:fs";
54
+ import { dirname, join } from "node:path";
53
55
  import type { Workflow } from "../api.js";
54
56
  import { getBuiltIns } from "../built-ins.js";
55
57
  import type { ConfigLayer } from "../layers.js";
58
+ import { LEGACY_OVERLAY_NOTICE, LEGACY_RUNS_NOTICE, LEGACY_USER_CONFIG_NOTICE } from "../messages.js";
56
59
  import { validateWorkflow, type WorkflowValidationIssue } from "../validate-workflow.js";
60
+ import { applySkillAliases } from "./alias.js";
57
61
  import { type LoadAccumulator, loadLayer } from "./merge.js";
58
- import { projectOverlayPaths, userOverlayPaths } from "./paths.js";
62
+ import { type OverlayPaths, projectOverlayPaths, userOverlayPaths } from "./paths.js";
59
63
  import { resolveDefault } from "./resolve-default.js";
60
64
 
61
65
  // ===========================================================================
@@ -63,6 +67,7 @@ import { resolveDefault } from "./resolve-default.js";
63
67
  // ===========================================================================
64
68
 
65
69
  export type { ConfigLayer } from "../layers.js";
70
+ export { aliasSkills } from "./alias.js";
66
71
  export { __resetLoadCache } from "./cache.js";
67
72
  export type { OverlayPaths } from "./paths.js";
68
73
  export { projectOverlayPaths, userOverlayPaths } from "./paths.js";
@@ -91,6 +96,13 @@ export interface LoadedWorkflows {
91
96
  layers: readonly ConfigLayer[];
92
97
  /** Aggregated load + validation issues. Errors block the runner; warnings are advisory. */
93
98
  issues: readonly Issue[];
99
+ /**
100
+ * The merged, applied skill-alias map (project over user, per-key) — always
101
+ * present, `{}` when no layer declared `skillAliases`. `/wf` preview renders
102
+ * this as a banner; every dispatching stage in `workflows` already reflects
103
+ * the remap.
104
+ */
105
+ skillAliases: Readonly<Record<string, string>>;
94
106
  }
95
107
 
96
108
  // ===========================================================================
@@ -130,12 +142,28 @@ export async function loadWorkflows(cwd: string): Promise<LoadedWorkflows> {
130
142
  acc.sourcePaths.set(w.name, undefined);
131
143
  }
132
144
 
133
- const userOutcome = await loadLayer(userOverlayPaths(), "user", acc);
145
+ const userPaths = userOverlayPaths();
146
+ const userOutcome = await loadLayer(userPaths, "user", acc);
134
147
  if (userOutcome.contributed) layers.push("user");
135
148
 
136
149
  const projectOutcome = await loadLayer(projectOverlayPaths(cwd), "project", acc);
137
150
  if (projectOutcome.contributed) layers.push("project");
138
151
 
152
+ // One-time legacy migration advisories — each independent, each a warning
153
+ // (advisory, never blocks the run). The new `.rpiv/workflows/` (project) and
154
+ // `~/.config/rpiv-workflow/config.ts` (user) locations are the only ones
155
+ // read; these probes point the user at each move so nothing is silently
156
+ // ignored / stranded.
157
+ pushLegacyNotices(cwd, userPaths, acc);
158
+
159
+ // Merge + apply skill aliases (project overrides user per key) to every
160
+ // workflow — built-ins included — BEFORE the validation loop, so the
161
+ // aliased workflows are validated and preview / JSONL / the runtime
162
+ // skill-registry preflight all observe the final skill. The runner is
163
+ // untouched. No-op warnings attribute to the source layer that actually
164
+ // declared the key — see `applySkillAliases` in `./alias.ts`.
165
+ const skillAliases = applySkillAliases(acc, userOutcome, projectOutcome);
166
+
139
167
  // Validate every merged workflow once. Validation runs even on built-in so
140
168
  // that a future built-in regression surfaces in the same channel as user
141
169
  // errors. Each issue is attributed to the exact file the surviving workflow
@@ -155,5 +183,47 @@ export async function loadWorkflows(cwd: string): Promise<LoadedWorkflows> {
155
183
  workflowSources: acc.sources,
156
184
  layers,
157
185
  issues: acc.issues,
186
+ skillAliases,
158
187
  };
159
188
  }
189
+
190
+ /**
191
+ * Push the three independent legacy-migration advisories. Each is a `"warning"`
192
+ * (never blocks the run) and each probes a distinct stale layout the unified
193
+ * `.rpiv/workflows/` move left behind:
194
+ * - project dashed dir `<cwd>/.rpiv-workflow/` → config.ts + packs/
195
+ * - orphaned run JSONLs `<cwd>/.rpiv/workflows/*.jsonl` → runs/
196
+ * - user-layer rename `~/.config/rpiv-workflow/workflows.config.ts` → config.ts
197
+ */
198
+ function pushLegacyNotices(cwd: string, userPaths: OverlayPaths, acc: LoadAccumulator): void {
199
+ if (existsSync(join(cwd, ".rpiv-workflow"))) {
200
+ acc.issues.push({ kind: "load", layer: "project", severity: "warning", message: LEGACY_OVERLAY_NOTICE(cwd) });
201
+ }
202
+
203
+ if (hasOrphanedRunFiles(cwd)) {
204
+ acc.issues.push({ kind: "load", layer: "project", severity: "warning", message: LEGACY_RUNS_NOTICE(cwd) });
205
+ }
206
+
207
+ const userDir = dirname(userPaths.configFile);
208
+ if (existsSync(join(userDir, "workflows.config.ts"))) {
209
+ acc.issues.push({
210
+ kind: "load",
211
+ layer: "user",
212
+ severity: "warning",
213
+ message: LEGACY_USER_CONFIG_NOTICE(userDir),
214
+ });
215
+ }
216
+ }
217
+
218
+ /**
219
+ * True when `<cwd>/.rpiv/workflows/` holds top-level `*.jsonl` run files written
220
+ * before the `runs/` relocation. `readdirSync` lists only immediate entries, so
221
+ * files already inside `runs/` never match. A missing / unreadable dir → false.
222
+ */
223
+ function hasOrphanedRunFiles(cwd: string): boolean {
224
+ try {
225
+ return readdirSync(join(cwd, ".rpiv", "workflows")).some((f) => f.endsWith(".jsonl"));
226
+ } catch {
227
+ return false;
228
+ }
229
+ }
package/load/merge.ts CHANGED
@@ -49,6 +49,8 @@ export interface LoadAccumulator {
49
49
  export interface LayerOutcome {
50
50
  contributed: boolean;
51
51
  configDefault: string | undefined;
52
+ /** The config file's `skillAliases` map (or `undefined`). Packs can't set it. */
53
+ skillAliases: Record<string, string> | undefined;
52
54
  }
53
55
 
54
56
  export function loadError(acc: LoadAccumulator, layer: ConfigLayer, path: string | undefined, message: string): void {
@@ -70,6 +72,7 @@ export function loadError(acc: LoadAccumulator, layer: ConfigLayer, path: string
70
72
  export async function loadLayer(paths: OverlayPaths, layer: ConfigLayer, acc: LoadAccumulator): Promise<LayerOutcome> {
71
73
  let contributed = false;
72
74
  let configDefault: string | undefined;
75
+ let skillAliases: Record<string, string> | undefined;
73
76
 
74
77
  for (const packPath of enumeratePacks(paths.packsDir)) {
75
78
  const parsed = await loadOverlayFile(packPath, layer, acc, "pack");
@@ -83,11 +86,12 @@ export async function loadLayer(paths: OverlayPaths, layer: ConfigLayer, acc: Lo
83
86
  if (configParsed) {
84
87
  mergeOverlay(configParsed, layer, paths.configFile, acc);
85
88
  configDefault = configParsed.default;
89
+ skillAliases = configParsed.skillAliases;
86
90
  contributed = true;
87
91
  }
88
92
  }
89
93
 
90
- return { contributed, configDefault };
94
+ return { contributed, configDefault, skillAliases };
91
95
  }
92
96
 
93
97
  /** Alpha-sorted `*.ts` files directly under `dir`. Empty array if `dir` doesn't exist. */
package/load/normalize.ts CHANGED
@@ -6,7 +6,10 @@
6
6
  *
7
7
  * 1. A single `Workflow` — single-entry namespace
8
8
  * 2. `Workflow[]` — multi-entry, default required if > 1
9
- * 3. `{ workflows, default? }` — full envelope, explicit default
9
+ * 3. `{ workflows?, default?, skillAliases? }`
10
+ * — envelope; at least one of `workflows`,
11
+ * `default`, `skillAliases` must be
12
+ * present (alias-only is valid)
10
13
  *
11
14
  * Missing-field policy for `gate(...)` routes is documented at
12
15
  * `api.ts:gate`; loader-side, missing fields surface as `NaN` after
@@ -21,6 +24,7 @@ export type FileKind = "config" | "pack";
21
24
  export interface ParsedConfig {
22
25
  workflows: Workflow[];
23
26
  default?: string;
27
+ skillAliases?: Record<string, string>;
24
28
  }
25
29
 
26
30
  export type NormalizeResult = { kind: "ok"; value: ParsedConfig } | { kind: "err"; error: string };
@@ -56,13 +60,36 @@ export function normalizeDefaultExport(raw: unknown, kind: FileKind): NormalizeR
56
60
  kind: "err",
57
61
  error:
58
62
  "pack workflow files must export a `Workflow` or `Workflow[]` — the " +
59
- "`{ workflows, default? }` envelope is only accepted in the config file workflows.config.ts.",
63
+ "`{ workflows, default?, skillAliases? }` envelope is only accepted in the config file config.ts.",
60
64
  };
61
65
  }
62
- if (!raw.workflows.every(isWorkflow)) {
66
+ // `isEnvelope` recognises the envelope when `skillAliases`/`default` is
67
+ // present even if `workflows` is absent or malformed. A present-but-non-array
68
+ // `workflows` must error explicitly: `??` only guards null/undefined, so a
69
+ // truthy non-array (string, object) would otherwise reach `.every()` and
70
+ // throw a `TypeError`, violating the loader's "never throws" contract.
71
+ if (raw.workflows !== undefined && !Array.isArray(raw.workflows)) {
72
+ return { kind: "err", error: "default-export `workflows` must be a Workflow[]" };
73
+ }
74
+ const workflows = raw.workflows ?? [];
75
+ if (!workflows.every(isWorkflow)) {
63
76
  return { kind: "err", error: "default-export `workflows` must contain only Workflow objects" };
64
77
  }
65
- return { kind: "ok", value: { workflows: raw.workflows, default: raw.default } };
78
+ if (raw.skillAliases !== undefined) {
79
+ const aliases = raw.skillAliases as unknown;
80
+ const ok =
81
+ typeof aliases === "object" &&
82
+ aliases !== null &&
83
+ !Array.isArray(aliases) &&
84
+ Object.values(aliases).every((t) => typeof t === "string");
85
+ if (!ok) {
86
+ return {
87
+ kind: "err",
88
+ error: "`skillAliases` must be a Record<string, string> (skill name → skill name)",
89
+ };
90
+ }
91
+ }
92
+ return { kind: "ok", value: { workflows, default: raw.default, skillAliases: raw.skillAliases } };
66
93
  }
67
94
  return {
68
95
  kind: "err",
package/load/paths.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Overlay file system paths for the user and project layers.
3
3
  *
4
- * user — config `~/.config/rpiv-workflow/workflows.config.ts`
5
- * packs `~/.config/rpiv-workflow/workflows/*.ts`
6
- * project — config `<cwd>/.rpiv-workflow/workflows.config.ts`
7
- * packs `<cwd>/.rpiv-workflow/workflows/*.ts`
4
+ * user — config `~/.config/rpiv-workflow/config.ts`
5
+ * packs `~/.config/rpiv-workflow/packs/*.ts`
6
+ * project — config `<cwd>/.rpiv/workflows/config.ts`
7
+ * packs `<cwd>/.rpiv/workflows/packs/*.ts`
8
+ *
9
+ * Project config lives under the unified `.rpiv/<domain>/` tree alongside
10
+ * run state (`.rpiv/workflows/runs/`), so the package no longer carries the
11
+ * legacy `.rpiv-workflow/` outlier directory.
8
12
  */
9
13
 
10
14
  import { join } from "node:path";
@@ -17,16 +21,16 @@ export interface OverlayPaths {
17
21
  packsDir: string;
18
22
  }
19
23
 
20
- /** Project overlay paths under `<cwd>/.rpiv-workflow/`. */
24
+ /** Project overlay paths under `<cwd>/.rpiv/workflows/`. */
21
25
  export function projectOverlayPaths(cwd: string): OverlayPaths {
22
- const root = join(cwd, ".rpiv-workflow");
23
- return { configFile: join(root, "workflows.config.ts"), packsDir: join(root, "workflows") };
26
+ const root = join(cwd, ".rpiv", "workflows");
27
+ return { configFile: join(root, "config.ts"), packsDir: join(root, "packs") };
24
28
  }
25
29
 
26
30
  /** User overlay paths under `~/.config/rpiv-workflow/`. */
27
31
  export function userOverlayPaths(): OverlayPaths {
28
32
  return {
29
- configFile: configPath("rpiv-workflow", "workflows.config.ts"),
30
- packsDir: configPath("rpiv-workflow", "workflows"),
33
+ configFile: configPath("rpiv-workflow", "config.ts"),
34
+ packsDir: configPath("rpiv-workflow", "packs"),
31
35
  };
32
36
  }
@@ -8,8 +8,9 @@
8
8
  import type { Workflow } from "../api.js";
9
9
 
10
10
  export interface Envelope {
11
- workflows: Workflow[];
11
+ workflows?: Workflow[];
12
12
  default?: string;
13
+ skillAliases?: Record<string, string>;
13
14
  }
14
15
 
15
16
  export function isWorkflow(v: unknown): v is Workflow {
@@ -27,7 +28,12 @@ export function isWorkflow(v: unknown): v is Workflow {
27
28
 
28
29
  export function isEnvelope(v: unknown): v is Envelope {
29
30
  if (!v || typeof v !== "object") return false;
30
- return Array.isArray((v as Record<string, unknown>).workflows);
31
+ if (isWorkflow(v)) return false; // a bare Workflow is not an envelope
32
+ const e = v as Record<string, unknown>;
33
+ // An envelope is the config-file shape: it carries `workflows`, and/or the
34
+ // config-only fields `skillAliases` / `default`. (An alias-only config has
35
+ // no `workflows`.)
36
+ return Array.isArray(e.workflows) || "skillAliases" in e || "default" in e;
31
37
  }
32
38
 
33
39
  export function describe(v: unknown): string {
package/messages.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  * Pi's session transition (the status line is the durable channel).
6
6
  */
7
7
 
8
+ import { join } from "node:path";
9
+
8
10
  export const STATUS_KEY = "rpiv-workflow";
9
11
 
10
12
  export const STATUS_STAGE = (stage: number, total: number, skill: string) => `rpiv: stage ${stage}/${total} — ${skill}`;
@@ -196,10 +198,58 @@ export const MSG_WORKFLOW_NOT_FOUND = (name: string) => `/wf: workflow "${name}"
196
198
  * instead of trying to run with an undefined default — without rpiv-pi
197
199
  * installed and no user overlay, the merged registry is genuinely empty
198
200
  * and the user needs to install a sibling that bundles workflows or
199
- * author one in `.rpiv-workflow/workflows.config.ts`.
201
+ * author one in `.rpiv/workflows/config.ts`.
200
202
  */
201
203
  export const MSG_NO_WORKFLOWS_REGISTERED =
202
- "/wf: no workflows registered — install a sibling that bundles workflows or author one in `.rpiv-workflow/workflows.config.ts`";
204
+ "/wf: no workflows registered — install a sibling that bundles workflows or author one in `.rpiv/workflows/config.ts`";
205
+
206
+ /**
207
+ * Legacy `.rpiv-workflow/` overlay directory detected at load time. The
208
+ * package moved project config under the unified `.rpiv/workflows/` tree
209
+ * (config.ts + packs/) alongside run state. The old directory is NO LONGER
210
+ * read — this notice points the user at the new location and the one-line
211
+ * `mv` migration. Emitted as a load WARNING (advisory, non-blocking).
212
+ *
213
+ * The embedded shell is `;`-sequenced (not `&&`-chained) and each move is
214
+ * guarded (`[ -f … ]` for the config, `find … 2>/dev/null` for the packs) so
215
+ * the terminal `rm -rf` ALWAYS runs — a config-only legacy dir (no `workflows/`
216
+ * subdir) no longer halts the chain and re-fires this warning forever.
217
+ */
218
+ export const LEGACY_OVERLAY_NOTICE = (cwd: string): string =>
219
+ `rpiv-workflow: detected legacy \`${join(cwd, ".rpiv-workflow")}\` — project config now lives at ` +
220
+ "`.rpiv/workflows/config.ts` + `.rpiv/workflows/packs/` and is the only location read. " +
221
+ "Move it: `mkdir -p .rpiv/workflows/packs; " +
222
+ "[ -f .rpiv-workflow/workflows.config.ts ] && mv .rpiv-workflow/workflows.config.ts .rpiv/workflows/config.ts; " +
223
+ "find .rpiv-workflow/workflows -name '*.ts' -exec mv {} .rpiv/workflows/packs/ \\; 2>/dev/null; " +
224
+ "rm -rf .rpiv-workflow` " +
225
+ "(the old directory is ignored). " +
226
+ "Note: `.rpiv/workflows/` is commonly gitignored (it holds run state), so the moved " +
227
+ "`config.ts` + `packs/` may be silently uncommittable — add `!.rpiv/workflows/config.ts` and " +
228
+ "`!.rpiv/workflows/packs/` to your `.gitignore` to version-control team workflow config.";
229
+
230
+ /**
231
+ * Orphaned run JSONLs detected directly under `.rpiv/workflows/` at load time.
232
+ * Run state moved one level down into `.rpiv/workflows/runs/`; files written by
233
+ * an older version still sit at the parent and are no longer enumerated by
234
+ * `listRuns` (so `/wf` past-run inspection silently can't see them). Emitted as
235
+ * a load WARNING (advisory, non-blocking) — the files are orphaned, not deleted.
236
+ */
237
+ export const LEGACY_RUNS_NOTICE = (cwd: string): string =>
238
+ `rpiv-workflow: detected legacy run files directly under \`${join(cwd, ".rpiv", "workflows")}\` — ` +
239
+ "run state now lives in `.rpiv/workflows/runs/` and these top-level `*.jsonl` files are no longer " +
240
+ "read by `/wf`. Move them: `mkdir -p .rpiv/workflows/runs && mv .rpiv/workflows/*.jsonl .rpiv/workflows/runs/`.";
241
+
242
+ /**
243
+ * Legacy user-layer config filename (`workflows.config.ts`) detected at load
244
+ * time. The user overlay's inner name was aligned with the project layer
245
+ * (`config.ts`) and is the ONLY name read — a stale `workflows.config.ts` would
246
+ * otherwise silently stop contributing its aliases / default / overlay
247
+ * workflows. Mirrors `LEGACY_OVERLAY_NOTICE` at the user layer. Load WARNING.
248
+ */
249
+ export const LEGACY_USER_CONFIG_NOTICE = (dir: string): string =>
250
+ `rpiv-workflow: detected legacy \`${join(dir, "workflows.config.ts")}\` — the user-layer config now lives at ` +
251
+ `\`${join(dir, "config.ts")}\` and is the only name read. ` +
252
+ `Move it: \`mv ${join(dir, "workflows.config.ts")} ${join(dir, "config.ts")}\` (the old name is ignored).`;
203
253
 
204
254
  /** Pi command registry — displayed by Pi's `/?` / command list. */
205
255
  export const CMD_DESCRIPTION = "Run a skill workflow: /wf [workflow] [description]";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-workflow",
3
- "version": "1.16.1",
3
+ "version": "1.17.1",
4
4
  "description": "Pi extension. Chain skills into typed multi-stage workflows with audited JSONL state, predicate routing, and per-stage output validation. Skill-agnostic — bring your own skills.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -45,17 +45,17 @@
45
45
  "iterate.ts",
46
46
  "layers.ts",
47
47
  "lifecycle.ts",
48
- "load",
48
+ "load/",
49
49
  "output.ts",
50
50
  "messages.ts",
51
51
  "output-spec.ts",
52
- "outcomes",
52
+ "outcomes/",
53
53
  "predicates.ts",
54
54
  "preview.ts",
55
55
  "routing.ts",
56
- "runner",
57
- "sessions",
58
- "state",
56
+ "runner/",
57
+ "sessions/",
58
+ "state/",
59
59
  "transcript.ts",
60
60
  "triggers.ts",
61
61
  "typebox-adapter.ts",
@@ -63,7 +63,7 @@
63
63
  "validate-output.ts",
64
64
  "validate-workflow.ts",
65
65
  "docs-protocol.ts",
66
- "docs",
66
+ "docs/",
67
67
  "README.md",
68
68
  "LICENSE"
69
69
  ],
@@ -77,7 +77,7 @@
77
77
  ]
78
78
  },
79
79
  "dependencies": {
80
- "@juicesharp/rpiv-config": "^1.16.1",
80
+ "@juicesharp/rpiv-config": "^1.17.1",
81
81
  "jiti": "^2.7.0"
82
82
  },
83
83
  "peerDependencies": {
package/preview.ts CHANGED
@@ -52,11 +52,13 @@ export function formatWorkflowList(loaded: LoadedWorkflows): string {
52
52
  return ` ${name} ${stages} ${layerTag} ${defaultTag}${desc}`.trimEnd();
53
53
  });
54
54
 
55
+ const aliasBanner = formatAliasBanner(loaded.skillAliases);
55
56
  return [
56
57
  "Available workflows:",
57
58
  "",
58
59
  ...rows,
59
60
  "",
61
+ ...(aliasBanner ? [aliasBanner] : []),
60
62
  formatLayerBanner(loaded.layers),
61
63
  CMD_USAGE_LIST,
62
64
  CMD_USAGE_PREVIEW,
@@ -76,8 +78,17 @@ export function formatWorkflowDetails(loaded: LoadedWorkflows, name: string): st
76
78
  const stageRows = Object.entries(workflow.stages).map(([stageName, stage], i) =>
77
79
  formatStageRow(i + 1, stageName, stage, workflow),
78
80
  );
81
+ const aliasBanner = formatAliasBanner(loaded.skillAliases);
79
82
 
80
- return [heading, ...descriptionLine, "", ...stageRows, "", CMD_USAGE_RUN(name)].join("\n");
83
+ return [
84
+ heading,
85
+ ...descriptionLine,
86
+ "",
87
+ ...stageRows,
88
+ "",
89
+ ...(aliasBanner ? [aliasBanner, ""] : []),
90
+ CMD_USAGE_RUN(name),
91
+ ].join("\n");
81
92
  }
82
93
 
83
94
  // ===========================================================================
@@ -140,3 +151,15 @@ function formatEdge(workflow: Workflow, from: string): string | undefined {
140
151
  function formatLayerBanner(layers: readonly ConfigLayer[]): string {
141
152
  return `Sources: ${layers.map(renderConfigLayer).join(" + ")}`;
142
153
  }
154
+
155
+ /**
156
+ * "Skill aliases in effect: commit → attributed-commit, code-review → strict-review"
157
+ * — shown only when a `skillAliases` map is in effect. No silent magic: the
158
+ * banner surfaces every active remap so a reader can see why a stage dispatches
159
+ * a different skill than its name.
160
+ */
161
+ function formatAliasBanner(aliases: Readonly<Record<string, string>>): string | undefined {
162
+ const entries = Object.entries(aliases);
163
+ if (entries.length === 0) return undefined;
164
+ return `Skill aliases in effect: ${entries.map(([from, to]) => `${from} → ${to}`).join(", ")}`;
165
+ }
package/runner/runner.ts CHANGED
@@ -93,7 +93,7 @@ export interface RunWorkflowOptions {
93
93
  export interface RunWorkflowResult {
94
94
  /**
95
95
  * The run's identity on disk — the `<run-id>` portion of
96
- * `<cwd>/.rpiv/workflows/<run-id>.jsonl`. Live consumers can hand
96
+ * `<cwd>/.rpiv/workflows/runs/<run-id>.jsonl`. Live consumers can hand
97
97
  * this to `readLastStage` / `listArtifacts` / future inspect-past-run
98
98
  * helpers without recomputing the slug.
99
99
  *
package/state/index.ts CHANGED
@@ -21,7 +21,7 @@ export {
21
21
  readHeader,
22
22
  readLastStage,
23
23
  readRoutingDecisions,
24
+ runsDir,
24
25
  stateFilePath,
25
- workflowsDir,
26
26
  writeHeader,
27
27
  } from "./state.js";
package/state/paths.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * no I/O. Writes and reads import from here so the on-disk layout has
4
4
  * one authoritative source.
5
5
  *
6
- * <cwd>/.rpiv/workflows/<run-id>.jsonl
6
+ * <cwd>/.rpiv/workflows/runs/<run-id>.jsonl
7
7
  *
8
8
  * The slug format mirrors `skills/_shared/now.mjs` so audit files
9
9
  * sort chronologically by filename.
@@ -37,10 +37,10 @@ export function generateRunId(
37
37
  // Directory resolution
38
38
  // ---------------------------------------------------------------------------
39
39
 
40
- export function workflowsDir(cwd: string): string {
41
- return join(cwd, ".rpiv", "workflows");
40
+ export function runsDir(cwd: string): string {
41
+ return join(cwd, ".rpiv", "workflows", "runs");
42
42
  }
43
43
 
44
44
  export function stateFilePath(cwd: string, runId: string): string {
45
- return join(workflowsDir(cwd), `${runId}.jsonl`);
45
+ return join(runsDir(cwd), `${runId}.jsonl`);
46
46
  }
package/state/reads.ts CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { existsSync, readdirSync, readFileSync } from "node:fs";
20
20
  import type { Artifact } from "../handle.js";
21
- import { stateFilePath, workflowsDir } from "./paths.js";
21
+ import { runsDir, stateFilePath } from "./paths.js";
22
22
  import type { RoutingDecision, RunSummary, WorkflowHeader, WorkflowStage } from "./state.js";
23
23
 
24
24
  /**
@@ -150,8 +150,8 @@ export function readHeader(cwd: string, runId: string): WorkflowHeader | undefin
150
150
  }
151
151
 
152
152
  /**
153
- * Enumerate every `<cwd>/.rpiv/workflows/<run-id>.jsonl` and return its
154
- * header projected as a `RunSummary`. Empty array when the workflows
153
+ * Enumerate every `<cwd>/.rpiv/workflows/runs/<run-id>.jsonl` and return its
154
+ * header projected as a `RunSummary`. Empty array when the runs
155
155
  * directory doesn't exist (no runs yet). Files without a valid header
156
156
  * are skipped silently (corrupt / mid-write).
157
157
  *
@@ -164,7 +164,7 @@ export function readHeader(cwd: string, runId: string): WorkflowHeader | undefin
164
164
  * `runId` is monotonic for runs created on the same host).
165
165
  */
166
166
  export function listRuns(cwd: string): RunSummary[] {
167
- const dir = workflowsDir(cwd);
167
+ const dir = runsDir(cwd);
168
168
  let entries: string[];
169
169
  try {
170
170
  entries = readdirSync(dir);
package/state/state.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * JSONL state at `.rpiv/workflows/<run-id>.jsonl`. Append-only audit
2
+ * JSONL state at `.rpiv/workflows/runs/<run-id>.jsonl`. Append-only audit
3
3
  * trail; every line is a self-contained JSON object. All I/O is
4
4
  * fail-soft (logs via console.warn with `[rpiv-workflow]` prefix, never
5
5
  * throws).
6
6
  *
7
7
  * Internally split into three modules:
8
- * - paths.ts — workflowsDir + stateFilePath + generateRunId
8
+ * - paths.ts — runsDir + stateFilePath + generateRunId
9
9
  * - writes.ts — tryAppendJsonl + writeHeader + appendStage +
10
10
  * appendRoutingDecision
11
11
  * - reads.ts — readLastStage + readAllStages + readRoutingDecisions +
@@ -103,7 +103,7 @@ export interface RoutingDecision {
103
103
  // Public barrel — paths + writes + reads
104
104
  // ---------------------------------------------------------------------------
105
105
 
106
- export { generateRunId, stateFilePath, workflowsDir } from "./paths.js";
106
+ export { generateRunId, runsDir, stateFilePath } from "./paths.js";
107
107
  export {
108
108
  listArtifacts,
109
109
  listRuns,
package/state/writes.ts CHANGED
@@ -11,11 +11,11 @@
11
11
  */
12
12
 
13
13
  import { appendFileSync, mkdirSync } from "node:fs";
14
- import { stateFilePath, workflowsDir } from "./paths.js";
14
+ import { runsDir, stateFilePath } from "./paths.js";
15
15
  import type { RoutingDecision, WorkflowHeader, WorkflowStage } from "./state.js";
16
16
 
17
17
  /**
18
- * Shared append primitive: ensure the workflows directory exists, then
18
+ * Shared append primitive: ensure the runs directory exists, then
19
19
  * append one JSON-serialised row + newline. Returns true on success;
20
20
  * on any throw, warns to stderr and returns false. The three public
21
21
  * append helpers below are thin wrappers — `writeHeader` discards the
@@ -23,7 +23,7 @@ import type { RoutingDecision, WorkflowHeader, WorkflowStage } from "./state.js"
23
23
  */
24
24
  function tryAppendJsonl(cwd: string, runId: string, row: unknown): boolean {
25
25
  try {
26
- const dir = workflowsDir(cwd);
26
+ const dir = runsDir(cwd);
27
27
  mkdirSync(dir, { recursive: true });
28
28
  const filePath = stateFilePath(cwd, runId);
29
29
  appendFileSync(filePath, `${JSON.stringify(row)}\n`, "utf-8");