@juicesharp/rpiv-workflow 1.16.1 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/docs/workflow-basics.md +36 -14
- package/index.ts +19 -11
- package/load/alias.ts +116 -0
- package/load/index.ts +77 -7
- package/load/merge.ts +5 -1
- package/load/normalize.ts +31 -4
- package/load/paths.ts +13 -9
- package/load/shape-guards.ts +8 -2
- package/messages.ts +52 -2
- package/package.json +8 -8
- package/preview.ts +24 -1
- package/runner/runner.ts +1 -1
- package/state/index.ts +1 -1
- package/state/paths.ts +4 -4
- package/state/reads.ts +4 -4
- package/state/state.ts +3 -3
- package/state/writes.ts +3 -3
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/
|
|
45
|
-
← user config (~/.config/rpiv-workflow/
|
|
46
|
-
← project packs (<cwd>/.rpiv
|
|
47
|
-
← project config (<cwd>/.rpiv
|
|
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** (`
|
|
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
|
|
package/docs/workflow-basics.md
CHANGED
|
@@ -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
|
|
28
|
-
├──
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
├──
|
|
35
|
-
└──
|
|
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/
|
|
51
|
-
← user config (~/.config/rpiv-workflow/
|
|
52
|
-
← project packs (<cwd>/.rpiv
|
|
53
|
-
← project config (<cwd>/.rpiv
|
|
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 (`
|
|
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 (`
|
|
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
|
|
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`, `
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
11
|
-
* packs `~/.config/rpiv-workflow/
|
|
12
|
-
* project — config `<cwd>/.rpiv
|
|
13
|
-
* packs `<cwd>/.rpiv
|
|
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
|
|
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
|
|
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
|
|
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
|
|
63
|
+
"`{ workflows, default?, skillAliases? }` envelope is only accepted in the config file config.ts.",
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
|
-
|
|
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
|
-
|
|
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/
|
|
5
|
-
* packs `~/.config/rpiv-workflow/
|
|
6
|
-
* project — config `<cwd>/.rpiv
|
|
7
|
-
* packs `<cwd>/.rpiv
|
|
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
|
|
24
|
+
/** Project overlay paths under `<cwd>/.rpiv/workflows/`. */
|
|
21
25
|
export function projectOverlayPaths(cwd: string): OverlayPaths {
|
|
22
|
-
const root = join(cwd, ".rpiv
|
|
23
|
-
return { configFile: join(root, "
|
|
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", "
|
|
30
|
-
packsDir: configPath("rpiv-workflow", "
|
|
33
|
+
configFile: configPath("rpiv-workflow", "config.ts"),
|
|
34
|
+
packsDir: configPath("rpiv-workflow", "packs"),
|
|
31
35
|
};
|
|
32
36
|
}
|
package/load/shape-guards.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
import type { Workflow } from "../api.js";
|
|
9
9
|
|
|
10
10
|
export interface Envelope {
|
|
11
|
-
workflows
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "1.17.0",
|
|
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.
|
|
80
|
+
"@juicesharp/rpiv-config": "^1.17.0",
|
|
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 [
|
|
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
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
|
|
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(
|
|
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 {
|
|
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
|
|
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 =
|
|
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 —
|
|
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,
|
|
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 {
|
|
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
|
|
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 =
|
|
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");
|