@osolmaz/pi-workflows 0.11.1 → 0.12.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 +15 -6
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +29 -3
- package/dist/builtins/autoimplement.workflow.js +876 -231
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +3 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -0
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/sanity-check-session.d.ts +17 -0
- package/dist/builtins/sanity-check-session.js +168 -0
- package/dist/builtins/sanity-check-session.js.map +1 -0
- package/dist/builtins/sanity-check.workflow.d.ts +83 -0
- package/dist/builtins/sanity-check.workflow.js +398 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +141 -5
- package/dist/controllers/sqlite.js +568 -46
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.js +7 -7
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +515 -84
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/host/runner.js +3 -0
- package/dist/host/runner.js.map +1 -1
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/engine.js +8 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -0
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +1 -1
- package/dist/workflows/migrate-sources.js.map +1 -1
- package/dist/workflows/schema.js +2 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +3 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +1 -0
- package/dist/workflows/tool-input.js +2 -2
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +4 -3
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +445 -0
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +7 -7
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
- package/docs/MONITOR.md +2 -2
- package/docs/WORKFLOW_COMPOSITION.md +2 -2
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
- package/docs/run-bundles.md +3 -1
- package/docs/workflows.md +67 -17
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +1 -1
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v2.schema.json +1 -1
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v2.schema.json +1 -1
- package/schemas/human-decision-receipt-v1.schema.json +1 -1
- package/schemas/human-decision-receipt-v2.schema.json +1 -1
- package/schemas/human-decision-request-v1.schema.json +1 -1
- package/schemas/human-decision-request-v2.schema.json +1 -1
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-resolution-v2.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +66 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +35 -21
- package/skills/pi-workflows/SKILL.md +19 -5
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1065 -261
- package/src/builtins/catalog.ts +3 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/sanity-check-session.ts +205 -0
- package/src/builtins/sanity-check.workflow.ts +624 -0
- package/src/controllers/sqlite.ts +878 -51
- package/src/extension/decision-channels.ts +7 -7
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +646 -91
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- package/src/host/runner.ts +3 -0
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/engine.ts +8 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/migrate-sources.ts +5 -1
- package/src/workflows/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/tool-input.ts +5 -2
- package/src/workflows/types.ts +4 -3
|
@@ -1,10 +1,57 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: autoimplement
|
|
3
|
-
description: Use when the user asks to implement a plan end-to-end, test it, run
|
|
3
|
+
description: Use when the user asks to implement a plan end-to-end, test it, run pi-reviewer against the base branch in a loop until no P0/P1 issues remain, and make sure CI/CD is green before finishing.
|
|
4
4
|
compatibility: Requires Pi Workflows and the built-in autoimplement workflow.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Autoimplement
|
|
8
|
+
|
|
9
|
+
## Start the workflow
|
|
10
|
+
|
|
11
|
+
Use the built-in `autoimplement` workflow when it is available. At top level, list workflows, build the complete input, and start `autoimplement` once. Do not start with a partial input and repair it in later turns.
|
|
12
|
+
|
|
13
|
+
Build the input as follows:
|
|
14
|
+
|
|
15
|
+
- `task`: Preserve the user's requested end state.
|
|
16
|
+
- `plan`: Pass the selected plan or the full contents of its canonical plan document. Do not devise a new initial plan.
|
|
17
|
+
- `repository`: Use the absolute path of the repository that owns the work.
|
|
18
|
+
- `scope`: Always include a concrete authority statement. Name every allowed repository and the allowed edit, test, commit, push, pull-request, merge, and release actions. Carry forward exclusions from the conversation. A repository path alone is not a scope.
|
|
19
|
+
- `constraints`: Include all applicable user and repository constraints. Use an empty array when none apply.
|
|
20
|
+
- `baseBranch`: Use the requested base or the repository default branch.
|
|
21
|
+
- `merge`: Set `true` only when the user explicitly requested merge or an applicable standing instruction authorizes it. Otherwise set `false`.
|
|
22
|
+
- `documents`: Include known canonical plan or specification paths. Use an empty array when none are known.
|
|
23
|
+
- `concurrency`: Include it only when the conversation gives explicit limits.
|
|
24
|
+
|
|
25
|
+
When one repository is clearly named, derive the scope without asking the user to restate it. A safe derived scope permits only work needed for the task in that repository, including local verification and normal branch and pull-request publication. It excludes unrelated repositories, merge, release, deployment, credentials, and policy changes unless those actions are explicitly authorized.
|
|
26
|
+
|
|
27
|
+
Replace the example values below with facts from the conversation, then make one start call:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"action": "start",
|
|
32
|
+
"workflow": "autoimplement",
|
|
33
|
+
"input": {
|
|
34
|
+
"task": "Implement the selected timeout fallback plan end to end.",
|
|
35
|
+
"plan": {
|
|
36
|
+
"canonicalDocument": "docs/plans/timeout-fallback-plan.md",
|
|
37
|
+
"summary": "Add a bounded timeout fallback.",
|
|
38
|
+
"requirements": [
|
|
39
|
+
"Route supported timeouts to one read-only fallback.",
|
|
40
|
+
"Keep cancellation terminal."
|
|
41
|
+
],
|
|
42
|
+
"verification": ["npm run check", "npm run test:e2e"]
|
|
43
|
+
},
|
|
44
|
+
"repository": "/absolute/path/to/repository",
|
|
45
|
+
"scope": "Only /absolute/path/to/repository. May edit and test task-related files, create commits, push the task branch, and open or update its pull request. Must not modify other repositories, merge, release, deploy, change credentials, or change repository policy.",
|
|
46
|
+
"constraints": ["Preserve immediate cancellation.", "Keep deferred-turn work separate."],
|
|
47
|
+
"baseBranch": "main",
|
|
48
|
+
"merge": false,
|
|
49
|
+
"documents": ["docs/plans/timeout-fallback-plan.md"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Do not manually duplicate stages already owned by the workflow. Autoimplement runs independent pi-reviewer commands, pending CI watches, and local verification commands from separate repositories in bounded batches. It keeps model turns, fixes, pushes, comment changes, merges, and releases ordered. One repository uses the same batch path with concurrency one.
|
|
8
55
|
|
|
9
56
|
When this skill is loaded inside an active workflow step, do not start another workflow. Complete the current step contract with the available tools.
|
|
10
57
|
|
|
@@ -21,29 +68,36 @@ Outside Pi, or when the workflow is unavailable, do the following in the order t
|
|
|
21
68
|
|
|
22
69
|
2. Once you finish implementing, make sure to test it.
|
|
23
70
|
- This will depend on the nature of the problem. If needed, run local smoke tests, spin up dev servers, make requests and such.
|
|
71
|
+
- Run commands from independent repositories in a bounded batch only when their working directories are distinct and local resources can support the overlap.
|
|
72
|
+
- Keep one repository's dependent checks in order. Do not batch shell wrappers, remote mutations, package publication, or commands with unclear side effects.
|
|
24
73
|
- Try to test as much as possible, without merging.
|
|
25
74
|
- State explicitly what could not be tested locally and what still needs staging or production verification.
|
|
26
75
|
- Do not put mutation testing on the critical path unless repository policy explicitly requires it; keep the mutation test scripts available.
|
|
27
76
|
|
|
28
77
|
3. Push your latest commits before running review so the review is always against the current PR head.
|
|
29
|
-
- Run
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
78
|
+
- Run pi-reviewer with its configured defaults against the base branch: `pi-reviewer --base <branch_name>`. The model and thinking level come from the reviewer's own config, not from this skill.
|
|
79
|
+
- Run reviewer commands for independent repositories in one bounded batch. Keep each result tied to its repository, base branch, pushed head, and relevant dependency fingerprint.
|
|
80
|
+
- In later rounds, rerun only repositories whose pushed head or relevant dependency fingerprint changed.
|
|
81
|
+
- Use a 10 minute timeout for each reviewer item, not the shell `timeout` program. If pi-reviewer takes more than 10 minutes, stop that item.
|
|
82
|
+
- Do not silently fall back to `codex review` when pi-reviewer is unavailable; stop and report the missing command or configuration for that repository.
|
|
83
|
+
- Treat truncated reviewer output as an invalid review, never a clean result.
|
|
84
|
+
- Record every review round with separate P0, P1, P2, and lower findings for each repository.
|
|
85
|
+
- Run pi-reviewer in a loop and address any P0 or P1 issues until there are none left.
|
|
86
|
+
- If a round reports only P2 or lower findings, address valid proportionate P2 findings, verify and push them, then move to the next stage without running pi-reviewer again solely because of that P2 work.
|
|
35
87
|
- Ignore issues about supporting legacy behavior unless the plan requires compatibility.
|
|
36
|
-
- Look at CI only after
|
|
88
|
+
- Look at CI only after pi-reviewer passes, meaning the last completed run found no issues or only P2 or lower issues.
|
|
37
89
|
|
|
38
|
-
4.
|
|
90
|
+
4. pi-reviewer reports findings locally and does not post them to the pull request.
|
|
39
91
|
- Separately check existing inline review comments and PR issue comments, and address valid comments.
|
|
40
92
|
- Ignore irrelevant comments and stale comments from before the latest commit unless they still apply.
|
|
41
93
|
- Reply to and resolve each comment either way.
|
|
42
94
|
- Do not wait a fixed five minutes; wait only when a required review is known to be pending, and keep that wait bounded.
|
|
43
95
|
|
|
44
96
|
5. In the final step, make sure that CI/CD is green.
|
|
45
|
-
- Inspect
|
|
46
|
-
-
|
|
97
|
+
- Inspect every pull request once before deciding to wait. If waiting is useful, state the exact `gh` tracking command for each pending pull request.
|
|
98
|
+
- Run supported pending CI watches in one bounded batch. Keep every result tied to its pull request and current head.
|
|
99
|
+
- Bound each CI watch to five minutes. If CI is still pending, use the next model turn for additional useful local tests or smoke tests instead of waiting. Then inspect CI again.
|
|
100
|
+
- Do not invent an ETA when the CI provider does not supply one.
|
|
47
101
|
- Ignore the fails unrelated to your changes, others break stuff sometimes and don't fix it.
|
|
48
102
|
- Make sure whatever changes you did don't break anything.
|
|
49
103
|
- If CI/CD is not fully green, state explicitly which failures are unrelated and why.
|
package/skills/autoplan/SKILL.md
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: autoplan
|
|
3
3
|
description: Use when the user asks to devise, choose, or plan the most elegant long-term production-ready solution, compare it with the ideal end state, and produce the best practical in-scope implementation plan without asking the user to resolve the gap.
|
|
4
|
-
compatibility: Requires
|
|
4
|
+
compatibility: Requires pi-workflows and the built-in autoplan workflow.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Autoplan
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Start the workflow
|
|
10
|
+
|
|
11
|
+
Use the built-in `autoplan` workflow when it is available. At top level, list workflows, build the complete input, and start `autoplan` once.
|
|
12
|
+
|
|
13
|
+
Build the input as follows:
|
|
14
|
+
|
|
15
|
+
- `problem`: State the decision or implementation-planning problem and its observable end state.
|
|
16
|
+
- `scope`: Name the repositories, systems, and interfaces that may change. State important exclusions. Derive an unambiguous repository-local scope without asking the user to restate it.
|
|
17
|
+
- `constraints`: Preserve all user, repository, safety, compatibility, cost, and authority limits. Use an empty array when none apply.
|
|
18
|
+
- `previousPlan`: Include it only when revising an existing plan.
|
|
19
|
+
- `newEvidence`: Include it only when evidence caused the revision request.
|
|
20
|
+
|
|
21
|
+
Replace the example values below with facts from the conversation, then make one start call:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"action": "start",
|
|
26
|
+
"workflow": "autoplan",
|
|
27
|
+
"input": {
|
|
28
|
+
"problem": "Choose a production-ready timeout fallback and write its implementation plan.",
|
|
29
|
+
"scope": "Only /absolute/path/to/repository. Plan changes to its public workflow API, built-in workflow, tests, and documentation. Exclude Pi core, external services, credentials, releases, and unrelated repositories.",
|
|
30
|
+
"constraints": ["Keep cancellation terminal.", "Use only documented public interfaces."]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
10
34
|
|
|
11
35
|
When this skill is loaded inside an active workflow step, do not start another workflow. Complete the current step contract.
|
|
12
36
|
|
package/skills/monitor/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: monitor
|
|
3
3
|
description: Use when the user asks to monitor, watch, track, or periodically check a running command, remote Job, CI run, deployment, publication, or other long-running objective. Starts the built-in Pi monitor workflow immediately in the current session and drives the objective autonomously, including routine recovery, until verified completion or a material blocker.
|
|
4
|
-
compatibility: Requires
|
|
4
|
+
compatibility: Requires pi-workflows and the built-in monitor workflow.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Monitor
|
|
@@ -10,9 +10,9 @@ Use the built-in Pi `monitor` workflow as an autopilot for the requested objecti
|
|
|
10
10
|
|
|
11
11
|
A monitor request authorizes routine, bounded work needed to preserve and finish the stated objective, subject to the conversation and repository approval boundaries. Apply other skills as safety and operating instructions. Do not turn their normal checks into new approval requests when the monitored objective and an existing approval already cover the action. Monitoring does not authorize changing the objective, method, model, data source, production selection, or other consequential contract.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Start the workflow without delay
|
|
14
14
|
|
|
15
|
-
As soon as the user invokes this skill:
|
|
15
|
+
Build the complete input and start the workflow in the same turn. As soon as the user invokes this skill:
|
|
16
16
|
|
|
17
17
|
1. Read the current conversation, active plan, repository instructions, and applicable compute, runtime, credential, deployment, or publication skills.
|
|
18
18
|
2. Preserve the exact objective, immutable execution contract, current identifiers, durable progress, cost already spent, approval ceilings, finish criteria, and known recovery rules in the workflow input. Write or update a durable plan or incident note first only when the work needs one for safe continuation.
|
|
@@ -31,6 +31,36 @@ Derive the workflow input from the full conversation:
|
|
|
31
31
|
- `stopWhen`: Infer verified completion from the full conversation. Describe completion of the complete objective, not only the end of one physical process. Also name material blockers that require human intervention.
|
|
32
32
|
- `repair`: Include this object only when the request or an existing approval authorizes mutation. Set `authorized: true` and record the repository, scope, base branch, merge policy, and constraints that apply. Omit it for observation-only work.
|
|
33
33
|
|
|
34
|
+
Replace the example values below with facts from the conversation, then make one start call:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"action": "start",
|
|
39
|
+
"workflow": "monitor",
|
|
40
|
+
"input": {
|
|
41
|
+
"task": "Monitor GitHub Actions run 123456 in owner/repository. Inspect the run and its artifacts, retry only transient status reads, and report each check. Do not change code or repository state.",
|
|
42
|
+
"everyMinutes": 5,
|
|
43
|
+
"stopWhen": "Stop when run 123456 completes and its required artifacts are verified, or when a material external blocker prevents truthful verification.",
|
|
44
|
+
"checkTimeoutMinutes": 10
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For authorized repair, add a complete `repair` object instead of leaving mutation authority implicit:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"repair": {
|
|
54
|
+
"authorized": true,
|
|
55
|
+
"repository": "/absolute/path/to/repository",
|
|
56
|
+
"scope": "Only /absolute/path/to/repository. May diagnose and fix failures related to the monitored objective, test, commit, push, and update its pull request. Must not modify other repositories, merge, release, deploy, change credentials, or change repository policy.",
|
|
57
|
+
"constraints": ["Keep the monitored objective and method unchanged."],
|
|
58
|
+
"baseBranch": "main",
|
|
59
|
+
"merge": false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
34
64
|
When the conversation gives no clear finish criterion, set `stopWhen` to `Stop only when the user explicitly asks to stop.` Do not use that fallback when a broader implementation, repair, publication, or deployment objective is clear from context.
|
|
35
65
|
|
|
36
66
|
Do not invent a finite check count. Omit `maxChecks` unless the user explicitly requests one. The workflow host can apply its own safety upper bound. Disclose that bound if it appears.
|
|
@@ -60,23 +90,7 @@ When the paid action remains within the approved method, hardware, concurrency,
|
|
|
60
90
|
|
|
61
91
|
The monitor may use a credential only when the conversation or repository has already authorized that credential's source, destination, and purpose. It may reuse that authorization for retries and replacement attempts under the same objective. It must not discover unrelated credentials, broaden scopes, copy credentials to a new store, or print secret values.
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Start the built-in workflow in the current session with this shape:
|
|
66
|
-
|
|
67
|
-
```text
|
|
68
|
-
workflow({
|
|
69
|
-
action: "start",
|
|
70
|
-
workflow: "monitor",
|
|
71
|
-
input: {
|
|
72
|
-
task: "<complete objective, contract, recovery authority, and verification task>",
|
|
73
|
-
everyMinutes: 30,
|
|
74
|
-
stopWhen: "<derived finish criterion or explicit-user-stop fallback>"
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Use the user-supplied interval instead of `30` when present. Add `maxChecks` only when the user explicitly supplies that limit. Do not send `reportWhen`; the current monitor reports every accepted check.
|
|
93
|
+
Use the user-supplied interval instead of the example value when present. Add `maxChecks` only when the user explicitly supplies that limit. Do not send `reportWhen`; the current monitor reports every accepted check.
|
|
80
94
|
|
|
81
95
|
Do not start a second monitor for the same objective while one is active. Update or replace the run only when the objective or contract changes. A replacement must preserve the previous accepted observation and durable recovery state.
|
|
82
96
|
|
|
@@ -115,7 +129,7 @@ Submit observed facts. The workflow computes rates, confidence, remaining work,
|
|
|
115
129
|
|
|
116
130
|
For several concurrent processes, publish one stable track per process. The Pi widget and viewers show them separately and keep each ETA independent.
|
|
117
131
|
|
|
118
|
-
Keep the monitored target independent of
|
|
132
|
+
Keep the monitored target independent of pi-workflows. Do not require a target Job or application to import pi-workflows, emit a Pi schema, write a Pi progress file, expose a Pi endpoint, create a progress store, or add a progress reader command solely for monitoring. Do not add provider-specific clients or credentials to pi-workflows. Target-specific observation belongs in the check task and is performed by the regular Pi model with already authorized tools.
|
|
119
133
|
|
|
120
134
|
Before proposing a new progress API, transport, schema, or persistence layer, prove that the model cannot observe the needed facts and publish them through the existing `workflow update` and `submit` path. If the target does not expose enough facts for ETA, report `ETA unavailable`. Application telemetry changes require separate scope and should expose normal operational facts rather than a Pi-specific protocol.
|
|
121
135
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-workflows
|
|
3
|
-
description: Use when creating, reviewing, debugging, starting, inspecting, or controlling
|
|
4
|
-
compatibility: Requires the
|
|
3
|
+
description: Use when creating, reviewing, debugging, starting, inspecting, or controlling pi-workflows; authoring .workflow.ts files; using the workflow tool; handling workflow step contracts, checkpoints, updates, or progress; or deciding how a task should compose workflow primitives.
|
|
4
|
+
compatibility: Requires the pi-workflows extension.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# pi-workflows
|
|
8
8
|
|
|
9
|
-
Use
|
|
9
|
+
Use pi-workflows for durable multi-step work that needs explicit routing, retries, checkpoints, scheduled waits, or progress. Keep simple one-turn work outside a workflow.
|
|
10
10
|
|
|
11
11
|
The `workflow` tool schema is the authority for call shapes. A workflow step message is the authority for its current step id, attempt id, and expected output. Do not guess these values from an earlier attempt.
|
|
12
12
|
|
|
@@ -22,7 +22,21 @@ Use the smallest applicable action:
|
|
|
22
22
|
- `update` publishes a non-completing durable update for the active step attempt.
|
|
23
23
|
- `submit` completes the active agent step with its required output.
|
|
24
24
|
|
|
25
|
-
Use `start` only once for one requested run.
|
|
25
|
+
Use `start` only once for one requested run. Before starting, load the matching workflow skill when one exists and build its complete input. Include scope, authority, constraints, identifiers, and finish criteria required by that skill. Do not start with placeholders that still need user or model repair.
|
|
26
|
+
|
|
27
|
+
For a workflow without a specialized skill, inspect its input contract and make one complete call. For example:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"action": "start",
|
|
32
|
+
"workflow": "examples/workflows/echo.workflow.ts",
|
|
33
|
+
"input": {
|
|
34
|
+
"task": "Summarize this repository in one sentence."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Do not build a manual polling loop around a workflow that already schedules its own work. Use the `monitor` skill for monitoring requests.
|
|
26
40
|
|
|
27
41
|
## Complete agent steps
|
|
28
42
|
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
MAX_COMMAND_BATCH_ITEMS,
|
|
5
|
+
validateCommandBatchRequest,
|
|
6
|
+
type CommandBatchItem,
|
|
7
|
+
} from "../workflows/command-batch.js";
|
|
8
|
+
|
|
9
|
+
export const REVIEW_TIMEOUT_MS = 10 * 60_000;
|
|
10
|
+
export const CI_WATCH_TIMEOUT_MS = 5 * 60_000;
|
|
11
|
+
export const VERIFICATION_TIMEOUT_MS = 45 * 60_000;
|
|
12
|
+
export const AUTOIMPLEMENT_BATCH_MAX_OUTPUT_CHARS = 1_000_000;
|
|
13
|
+
export const AUTOIMPLEMENT_MAX_CONCURRENCY = 8;
|
|
14
|
+
|
|
15
|
+
export type AutoimplementConcurrency = {
|
|
16
|
+
reviewer: number;
|
|
17
|
+
ciWatch: number;
|
|
18
|
+
verification: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type PublishedRepository = {
|
|
22
|
+
id: string;
|
|
23
|
+
repository: string;
|
|
24
|
+
branch: string;
|
|
25
|
+
baseBranch: string;
|
|
26
|
+
headRevision: string;
|
|
27
|
+
pr: string;
|
|
28
|
+
dependencyFingerprint?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PublishedRepositories = {
|
|
32
|
+
repositories: PublishedRepository[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type VerificationCommandPlan = {
|
|
36
|
+
commands: CommandBatchItem[];
|
|
37
|
+
untested: string[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type CiTargetInspection = {
|
|
41
|
+
id: string;
|
|
42
|
+
repository: string;
|
|
43
|
+
headRevision: string;
|
|
44
|
+
pr: string;
|
|
45
|
+
route: "green" | "failed" | "pending" | "unavailable";
|
|
46
|
+
reason: string;
|
|
47
|
+
relatedFailures: string[];
|
|
48
|
+
unrelatedFailures: string[];
|
|
49
|
+
trackingCommand?: CommandBatchItem;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type CiInspectionBatch = {
|
|
53
|
+
route: "green" | "failed" | "pending" | "unavailable";
|
|
54
|
+
reason: string;
|
|
55
|
+
relatedFailures: string[];
|
|
56
|
+
unrelatedFailures: string[];
|
|
57
|
+
targets: CiTargetInspection[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function parseAutoimplementConcurrency(value: unknown): AutoimplementConcurrency {
|
|
61
|
+
if (value === undefined) return { reviewer: 4, ciWatch: 4, verification: 2 };
|
|
62
|
+
const input = requireRecord(value, "autoimplement concurrency");
|
|
63
|
+
for (const key of Object.keys(input)) {
|
|
64
|
+
if (key !== "reviewer" && key !== "ciWatch" && key !== "verification") {
|
|
65
|
+
throw new Error(`autoimplement concurrency.${key} is not supported`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
reviewer: concurrencyValue(input.reviewer ?? 4, "reviewer"),
|
|
70
|
+
ciWatch: concurrencyValue(input.ciWatch ?? 4, "ciWatch"),
|
|
71
|
+
verification: concurrencyValue(input.verification ?? 2, "verification"),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parsePublishedRepositories(value: unknown): PublishedRepositories {
|
|
76
|
+
const result = requireRecord(value, "publication result");
|
|
77
|
+
if (!Array.isArray(result.repositories) || result.repositories.length === 0) {
|
|
78
|
+
throw new Error("publication result repositories must be a non-empty array");
|
|
79
|
+
}
|
|
80
|
+
if (result.repositories.length > MAX_COMMAND_BATCH_ITEMS) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`publication result repositories must contain at most ${MAX_COMMAND_BATCH_ITEMS} entries`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const ids = new Set<string>();
|
|
86
|
+
const repositories = result.repositories.map((entry, index) => {
|
|
87
|
+
const raw = requireRecord(entry, `publication repositories[${index}]`);
|
|
88
|
+
const repository = requireAbsolutePath(
|
|
89
|
+
raw.repository,
|
|
90
|
+
`publication repositories[${index}].repository`,
|
|
91
|
+
);
|
|
92
|
+
const id = repositoryId(repository);
|
|
93
|
+
if (ids.has(id)) throw new Error(`publication repository is duplicated: ${repository}`);
|
|
94
|
+
ids.add(id);
|
|
95
|
+
const dependencyFingerprint = optionalString(
|
|
96
|
+
raw.dependencyFingerprint,
|
|
97
|
+
`publication repositories[${index}].dependencyFingerprint`,
|
|
98
|
+
);
|
|
99
|
+
if (raw.pushed !== true) {
|
|
100
|
+
throw new Error(`publication repositories[${index}].pushed must be true`);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
repository,
|
|
105
|
+
branch: requireString(raw.branch, `publication repositories[${index}].branch`),
|
|
106
|
+
baseBranch: requireString(raw.baseBranch, `publication repositories[${index}].baseBranch`),
|
|
107
|
+
headRevision: requireString(
|
|
108
|
+
raw.headRevision,
|
|
109
|
+
`publication repositories[${index}].headRevision`,
|
|
110
|
+
),
|
|
111
|
+
pr: requireString(raw.pr, `publication repositories[${index}].pr`),
|
|
112
|
+
...(dependencyFingerprint !== undefined ? { dependencyFingerprint } : {}),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return { repositories };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function reviewerCommand(repository: PublishedRepository): CommandBatchItem {
|
|
119
|
+
return {
|
|
120
|
+
id: repository.id,
|
|
121
|
+
command: "pi-reviewer",
|
|
122
|
+
args: ["--base", repository.baseBranch],
|
|
123
|
+
cwd: repository.repository,
|
|
124
|
+
timeoutMs: REVIEW_TIMEOUT_MS,
|
|
125
|
+
maxOutputChars: AUTOIMPLEMENT_BATCH_MAX_OUTPUT_CHARS,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function parseVerificationCommandPlan(value: unknown): VerificationCommandPlan {
|
|
130
|
+
const result = requireRecord(value, "verification command plan");
|
|
131
|
+
if (!Array.isArray(result.commands) || result.commands.length === 0) {
|
|
132
|
+
throw new Error("verification commands must be a non-empty array");
|
|
133
|
+
}
|
|
134
|
+
const directories = new Set<string>();
|
|
135
|
+
const commands = result.commands.map((entry, index) => {
|
|
136
|
+
const command = parseCommandItem(entry, `verification commands[${index}]`, {
|
|
137
|
+
maxTimeoutMs: VERIFICATION_TIMEOUT_MS,
|
|
138
|
+
});
|
|
139
|
+
if (directories.has(command.cwd)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`verification commands must use distinct working directories: ${command.cwd}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
directories.add(command.cwd);
|
|
145
|
+
validateVerificationCommand(command, index);
|
|
146
|
+
return command;
|
|
147
|
+
});
|
|
148
|
+
const validated = validateCommandBatchRequest({ items: commands, maxConcurrency: 1 });
|
|
149
|
+
return {
|
|
150
|
+
commands: validated.items,
|
|
151
|
+
untested: stringArray(result.untested ?? [], "verification untested"),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function parseCiInspectionBatch(value: unknown): CiInspectionBatch {
|
|
156
|
+
const result = requireRecord(value, "CI inspection");
|
|
157
|
+
if (!Array.isArray(result.targets) || result.targets.length === 0) {
|
|
158
|
+
throw new Error("CI inspection targets must be a non-empty array");
|
|
159
|
+
}
|
|
160
|
+
if (result.targets.length > MAX_COMMAND_BATCH_ITEMS) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`CI inspection targets must contain at most ${MAX_COMMAND_BATCH_ITEMS} entries`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const ids = new Set<string>();
|
|
166
|
+
const targets = result.targets.map((entry, index) => {
|
|
167
|
+
const raw = requireRecord(entry, `CI targets[${index}]`);
|
|
168
|
+
const repository = requireAbsolutePath(raw.repository, `CI targets[${index}].repository`);
|
|
169
|
+
const id = repositoryId(repository);
|
|
170
|
+
if (ids.has(id)) throw new Error(`CI target is duplicated: ${repository}`);
|
|
171
|
+
ids.add(id);
|
|
172
|
+
if (
|
|
173
|
+
raw.route !== "green" &&
|
|
174
|
+
raw.route !== "failed" &&
|
|
175
|
+
raw.route !== "pending" &&
|
|
176
|
+
raw.route !== "unavailable"
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(`CI targets[${index}].route is invalid`);
|
|
179
|
+
}
|
|
180
|
+
const pr = requireString(raw.pr, `CI targets[${index}].pr`);
|
|
181
|
+
const target: CiTargetInspection = {
|
|
182
|
+
id,
|
|
183
|
+
repository,
|
|
184
|
+
headRevision: requireString(raw.headRevision, `CI targets[${index}].headRevision`),
|
|
185
|
+
pr,
|
|
186
|
+
route: raw.route,
|
|
187
|
+
reason: requireString(raw.reason, `CI targets[${index}].reason`),
|
|
188
|
+
relatedFailures: stringArray(
|
|
189
|
+
raw.relatedFailures ?? [],
|
|
190
|
+
`CI targets[${index}].relatedFailures`,
|
|
191
|
+
),
|
|
192
|
+
unrelatedFailures: stringArray(
|
|
193
|
+
raw.unrelatedFailures ?? [],
|
|
194
|
+
`CI targets[${index}].unrelatedFailures`,
|
|
195
|
+
),
|
|
196
|
+
};
|
|
197
|
+
if (raw.route === "pending") {
|
|
198
|
+
target.trackingCommand = parseCiCommand(raw.trackingCommand, id, repository, pr);
|
|
199
|
+
}
|
|
200
|
+
return target;
|
|
201
|
+
});
|
|
202
|
+
const route = targets.some((target) => target.route === "failed")
|
|
203
|
+
? "failed"
|
|
204
|
+
: targets.some((target) => target.route === "pending")
|
|
205
|
+
? "pending"
|
|
206
|
+
: targets.some((target) => target.route === "unavailable")
|
|
207
|
+
? "unavailable"
|
|
208
|
+
: "green";
|
|
209
|
+
return {
|
|
210
|
+
route,
|
|
211
|
+
reason: targets.map((target) => `${target.id}: ${target.reason}`).join("; "),
|
|
212
|
+
relatedFailures: targets.flatMap((target) => target.relatedFailures),
|
|
213
|
+
unrelatedFailures: targets.flatMap((target) => target.unrelatedFailures),
|
|
214
|
+
targets,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function parseCiCommand(
|
|
219
|
+
value: unknown,
|
|
220
|
+
id: string,
|
|
221
|
+
repository: string,
|
|
222
|
+
pr: string,
|
|
223
|
+
): CommandBatchItem {
|
|
224
|
+
const raw = requireRecord(value, "CI tracking command");
|
|
225
|
+
if (raw.id !== id) throw new Error("CI tracking command id must match the target repository");
|
|
226
|
+
const command = parseCommandItem(raw, "CI tracking command", {
|
|
227
|
+
id,
|
|
228
|
+
command: "gh",
|
|
229
|
+
cwd: repository,
|
|
230
|
+
maxTimeoutMs: CI_WATCH_TIMEOUT_MS,
|
|
231
|
+
});
|
|
232
|
+
const args = command.args;
|
|
233
|
+
const prWatch =
|
|
234
|
+
(args.length === 3 && args[0] === "pr" && args[1] === "checks" && args[2] === "--watch") ||
|
|
235
|
+
(args.length === 4 &&
|
|
236
|
+
args[0] === "pr" &&
|
|
237
|
+
args[1] === "checks" &&
|
|
238
|
+
args[2] === pr &&
|
|
239
|
+
args[3] === "--watch");
|
|
240
|
+
const runWatch =
|
|
241
|
+
args.length === 3 &&
|
|
242
|
+
args[0] === "run" &&
|
|
243
|
+
args[1] === "watch" &&
|
|
244
|
+
/^[1-9]\d*$/.test(args[2] ?? "");
|
|
245
|
+
if (!prWatch && !runWatch) {
|
|
246
|
+
throw new Error("CI tracking command args are not allowed for the target PR");
|
|
247
|
+
}
|
|
248
|
+
return { ...command, args: ["pr", "checks", pr, "--watch"] };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function repositoryId(repository: string): string {
|
|
252
|
+
const canonical = path.resolve(repository);
|
|
253
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseCommandItem(
|
|
257
|
+
value: unknown,
|
|
258
|
+
label: string,
|
|
259
|
+
options: {
|
|
260
|
+
id?: string;
|
|
261
|
+
command?: string;
|
|
262
|
+
cwd?: string;
|
|
263
|
+
maxTimeoutMs: number;
|
|
264
|
+
},
|
|
265
|
+
): CommandBatchItem {
|
|
266
|
+
const raw = requireRecord(value, label);
|
|
267
|
+
const id = options.id ?? requireString(raw.id, `${label}.id`);
|
|
268
|
+
const command = options.command ?? requireString(raw.command, `${label}.command`);
|
|
269
|
+
if (options.command !== undefined && raw.command !== options.command) {
|
|
270
|
+
throw new Error(`${label}.command must be ${options.command}`);
|
|
271
|
+
}
|
|
272
|
+
if (!Array.isArray(raw.args) || raw.args.some((arg) => typeof arg !== "string")) {
|
|
273
|
+
throw new Error(`${label}.args must be an array of strings`);
|
|
274
|
+
}
|
|
275
|
+
const cwd = options.cwd ?? requireAbsolutePath(raw.cwd, `${label}.cwd`);
|
|
276
|
+
if (options.cwd !== undefined && path.resolve(String(raw.cwd)) !== options.cwd) {
|
|
277
|
+
throw new Error(`${label}.cwd must match the target repository`);
|
|
278
|
+
}
|
|
279
|
+
const timeoutMs = positiveInteger(raw.timeoutMs, `${label}.timeoutMs`, options.maxTimeoutMs);
|
|
280
|
+
const maxOutputChars = positiveInteger(
|
|
281
|
+
raw.maxOutputChars ?? AUTOIMPLEMENT_BATCH_MAX_OUTPUT_CHARS,
|
|
282
|
+
`${label}.maxOutputChars`,
|
|
283
|
+
AUTOIMPLEMENT_BATCH_MAX_OUTPUT_CHARS,
|
|
284
|
+
);
|
|
285
|
+
return { id, command, args: [...raw.args] as string[], cwd, timeoutMs, maxOutputChars };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function validateVerificationCommand(command: CommandBatchItem, index: number): void {
|
|
289
|
+
const forbiddenCommands = new Set([
|
|
290
|
+
"ash",
|
|
291
|
+
"bash",
|
|
292
|
+
"cmd",
|
|
293
|
+
"cmd.exe",
|
|
294
|
+
"csh",
|
|
295
|
+
"dash",
|
|
296
|
+
"fish",
|
|
297
|
+
"gh",
|
|
298
|
+
"git",
|
|
299
|
+
"ksh",
|
|
300
|
+
"powershell",
|
|
301
|
+
"powershell.exe",
|
|
302
|
+
"pwsh",
|
|
303
|
+
"pwsh.exe",
|
|
304
|
+
"rm",
|
|
305
|
+
"sh",
|
|
306
|
+
"tcsh",
|
|
307
|
+
"zsh",
|
|
308
|
+
]);
|
|
309
|
+
const executable = path.win32.basename(path.basename(command.command)).toLowerCase();
|
|
310
|
+
if (forbiddenCommands.has(executable)) {
|
|
311
|
+
throw new Error(`verification commands[${index}].command is not allowed`);
|
|
312
|
+
}
|
|
313
|
+
const joined = command.args.join(" ").toLowerCase();
|
|
314
|
+
if (/\b(publish|release|deploy|push|merge)\b/.test(joined)) {
|
|
315
|
+
throw new Error(`verification commands[${index}] contains a mutation or publication action`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function concurrencyValue(value: unknown, field: string): number {
|
|
320
|
+
return positiveInteger(
|
|
321
|
+
value,
|
|
322
|
+
`autoimplement concurrency.${field}`,
|
|
323
|
+
AUTOIMPLEMENT_MAX_CONCURRENCY,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function positiveInteger(value: unknown, label: string, maximum: number): number {
|
|
328
|
+
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > maximum) {
|
|
329
|
+
throw new Error(`${label} must be an integer from 1 through ${maximum}`);
|
|
330
|
+
}
|
|
331
|
+
return value as number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function stringArray(value: unknown, label: string): string[] {
|
|
335
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
336
|
+
throw new Error(`${label} must be an array of strings`);
|
|
337
|
+
}
|
|
338
|
+
return [...value] as string[];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
342
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
343
|
+
throw new Error(`${label} must be an object`);
|
|
344
|
+
}
|
|
345
|
+
return value as Record<string, unknown>;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function requireString(value: unknown, label: string): string {
|
|
349
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
350
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function optionalString(value: unknown, label: string): string | undefined {
|
|
356
|
+
if (value === undefined) return undefined;
|
|
357
|
+
return requireString(value, label);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function requireAbsolutePath(value: unknown, label: string): string {
|
|
361
|
+
const resolved = requireString(value, label);
|
|
362
|
+
if (!path.isAbsolute(resolved)) throw new Error(`${label} must be absolute`);
|
|
363
|
+
return path.resolve(resolved);
|
|
364
|
+
}
|