@gtrabanco/pi-agentic-workflow 0.1.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/LICENSE +21 -0
- package/README.es.md +154 -0
- package/README.md +148 -0
- package/dist/config/defaults.d.ts +8 -0
- package/dist/config/defaults.js +11 -0
- package/dist/config/load.d.ts +19 -0
- package/dist/config/load.js +76 -0
- package/dist/config/merge.d.ts +4 -0
- package/dist/config/merge.js +43 -0
- package/dist/config/schema.d.ts +21 -0
- package/dist/config/schema.js +144 -0
- package/dist/config/types.d.ts +53 -0
- package/dist/config/types.js +12 -0
- package/dist/extension/factory.d.ts +47 -0
- package/dist/extension/factory.js +49 -0
- package/dist/extension/index.d.ts +4 -0
- package/dist/extension/index.js +84 -0
- package/dist/routing/catalogue.d.ts +35 -0
- package/dist/routing/catalogue.js +79 -0
- package/dist/routing/dispatch.d.ts +34 -0
- package/dist/routing/dispatch.js +191 -0
- package/dist/routing/state.d.ts +13 -0
- package/dist/routing/state.js +65 -0
- package/dist/routing/types.d.ts +90 -0
- package/dist/routing/types.js +3 -0
- package/dist/settings/console.d.ts +55 -0
- package/dist/settings/console.js +266 -0
- package/dist/settings/store.d.ts +3 -0
- package/dist/settings/store.js +24 -0
- package/dist/settings/view.d.ts +11 -0
- package/dist/settings/view.js +46 -0
- package/package.json +59 -0
- package/skills/audit-docs/SKILL.md +171 -0
- package/skills/audit-pr/SKILL.md +201 -0
- package/skills/audit-pr/references/01_MERGE_GATES.md +24 -0
- package/skills/audit-pr/references/02_CLOSURE_AND_SCOPE_GATES.md +83 -0
- package/skills/audit-pr/references/03_AUDIT_PROCESS.md +69 -0
- package/skills/audit-pr/references/04_VERDICT.md +69 -0
- package/skills/audit-pr/references/05_ROUTING_AND_GUARDRAILS.md +58 -0
- package/skills/audit-pr/references/PORTABILITY.md +17 -0
- package/skills/design-feature/SKILL.md +162 -0
- package/skills/design-feature/references/INTERVIEW.md +52 -0
- package/skills/design-feature/references/PORTABILITY.md +15 -0
- package/skills/design-feature/references/UPSERT_EXAMPLE.md +8 -0
- package/skills/design-feature/references/WRITE_AND_UPSERT.md +92 -0
- package/skills/discover-repository-state/SKILL.md +83 -0
- package/skills/execute-phase/SKILL.md +115 -0
- package/skills/execute-phase/references/BATCH_AND_PORTABILITY.md +54 -0
- package/skills/execute-phase/references/CLOSEOUT.md +74 -0
- package/skills/execute-phase/references/DESCOPE.md +35 -0
- package/skills/execute-phase/references/EXECUTION_CONTRACT.md +147 -0
- package/skills/execute-phase/references/FOLDING.md +73 -0
- package/skills/execute-phase/references/FORGE_BODY.md +24 -0
- package/skills/execute-phase/references/HANDOFF.md +21 -0
- package/skills/execute-phase/references/OPPORTUNISTIC_FINDING.md +45 -0
- package/skills/execute-phase/references/PREFLIGHT.md +152 -0
- package/skills/execute-phase/references/UNIT_LOOP.md +88 -0
- package/skills/execute-phase/references/WORKFLOWS_FEATURE.md +37 -0
- package/skills/execute-phase/references/WORKFLOWS_FIX.md +43 -0
- package/skills/execute-phase/references/WORKFLOWS_LEGACY.md +26 -0
- package/skills/execute-phase/references/WORKFLOWS_SMALL_PHASED.md +38 -0
- package/skills/fold-findings/SKILL.md +141 -0
- package/skills/fold-findings/references/FOLD_POLICY.md +43 -0
- package/skills/fold-findings/references/FOLD_PROCESS.md +41 -0
- package/skills/generate-docs/SKILL.md +145 -0
- package/skills/generate-docs/references/ADAPTERS.md +15 -0
- package/skills/generate-docs/references/ADAPTER_DISCOVERY.md +33 -0
- package/skills/generate-docs/references/GENERATION_PROCESS.md +90 -0
- package/skills/init-workspace/SKILL.md +152 -0
- package/skills/init-workspace/references/BOOTSTRAP_DISCOVERY.md +70 -0
- package/skills/init-workspace/references/BOOTSTRAP_WRITE.md +58 -0
- package/skills/init-workspace/references/PORTABILITY.md +16 -0
- package/skills/init-workspace/references/UPGRADE.md +83 -0
- package/skills/log-session/SKILL.md +159 -0
- package/skills/loop-review-fold/SKILL.md +168 -0
- package/skills/orchestration-envelope/SKILL.md +81 -0
- package/skills/orchestration-envelope/references/TURN_CONTRACT.md +21 -0
- package/skills/phase-contract/SKILL.md +82 -0
- package/skills/plan-feature/SKILL.md +179 -0
- package/skills/plan-feature/references/ROUTING.md +83 -0
- package/skills/plan-feature-from-issue/SKILL.md +140 -0
- package/skills/plan-feature-scaffold/SKILL.md +93 -0
- package/skills/plan-feature-scaffold/references/SCAFFOLD_PROCESS.md +76 -0
- package/skills/plan-fix/SKILL.md +115 -0
- package/skills/plan-fix/references/PLANNING_PROCESS.md +118 -0
- package/skills/plan-fix/references/SPEC_CONTRACT.md +22 -0
- package/skills/planning-preflight/SKILL.md +130 -0
- package/skills/product-audit/SKILL.md +225 -0
- package/skills/product-audit/references/AUDIT_DIMENSIONS.md +24 -0
- package/skills/product-audit/references/AUDIT_PROCESS.md +85 -0
- package/skills/resolve-repository-state/SKILL.md +79 -0
- package/skills/review-a11y/SKILL.md +60 -0
- package/skills/review-brand/SKILL.md +65 -0
- package/skills/review-change/SKILL.md +156 -0
- package/skills/review-change/references/ADVERSARIAL_RECOMMENDATION.md +31 -0
- package/skills/review-change/references/ADVERSARIAL_SETUP.md +86 -0
- package/skills/review-change/references/ADVERSARIAL_SYNTHESIS.md +53 -0
- package/skills/review-change/references/OUTPUT_AND_GUARDRAILS.md +85 -0
- package/skills/review-change/references/PERSIST_AND_DECIDE.md +151 -0
- package/skills/review-change/references/PORTABILITY.md +74 -0
- package/skills/review-change/references/REVIEW_PROCESS.md +86 -0
- package/skills/review-code/SKILL.md +69 -0
- package/skills/review-debt/SKILL.md +72 -0
- package/skills/review-design/SKILL.md +60 -0
- package/skills/review-implementation/SKILL.md +108 -0
- package/skills/review-implementation/references/CLASSIFY.md +96 -0
- package/skills/review-implementation/references/FIND.md +35 -0
- package/skills/review-perf/SKILL.md +79 -0
- package/skills/review-security/SKILL.md +65 -0
- package/skills/review-seo/SKILL.md +65 -0
- package/skills/review-verify/SKILL.md +66 -0
- package/skills/ship-roadmap/SKILL.md +120 -0
- package/skills/ship-roadmap/references/ADVANCE.md +90 -0
- package/skills/ship-roadmap/references/AUDIT_AND_MERGE.md +64 -0
- package/skills/ship-roadmap/references/CLOSEOUT_AND_LOG.md +26 -0
- package/skills/ship-roadmap/references/FOUNDING.md +101 -0
- package/skills/ship-roadmap/references/GUARDRAILS.md +56 -0
- package/skills/ship-roadmap/references/MODEL_ROUTING.md +14 -0
- package/skills/ship-roadmap/references/PORTABILITY.md +34 -0
- package/skills/ship-roadmap/references/RECOVERY_AND_SELECTION.md +93 -0
- package/skills/ship-roadmap/references/STOP_CONDITIONS.md +9 -0
- package/skills/ship-roadmap/references/TERMINAL_REPORT.md +34 -0
- package/skills/triage-issue/SKILL.md +148 -0
- package/skills/triage-issue/references/AUDIT_FINDINGS.md +35 -0
- package/skills/triage-issue/references/FOLD_LEDGER.md +32 -0
- package/skills/triage-issue/references/ISSUE_PROCESS.md +101 -0
- package/skills/triage-issue/references/LABELS.md +83 -0
- package/skills/triage-issue/references/REVIEW_FINDING_PROCESS.md +41 -0
- package/skills/verification-contract/SKILL.md +77 -0
- package/skills/workflow-status/SKILL.md +143 -0
- package/skills/workflow-status/references/CRASH_RECOVERY.md +66 -0
- package/skills/workflow-status/references/ENVELOPE_CORE.md +107 -0
- package/skills/workflow-status/references/ENVELOPE_FIELDS.md +77 -0
- package/skills/workflow-status/references/GUARDRAILS.md +17 -0
- package/skills/workflow-status/references/PORTABILITY.md +14 -0
- package/skills/workflow-status/references/SENSOR_CORE.md +95 -0
- package/skills/workflow-status/references/SENSOR_SIGNALS.md +128 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriel Trabanco <gtrabanco@users.noreply.github.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.es.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @gtrabanco/pi-agentic-workflow
|
|
2
|
+
|
|
3
|
+
> 🇬🇧 [English version](README.md)
|
|
4
|
+
|
|
5
|
+
Una sola instalación del método
|
|
6
|
+
[agentic-workflow](https://github.com/gtrabanco/agentic-workflow) en
|
|
7
|
+
[Pi](https://github.com/badlogic/pi-mono): las skills canónicas, un comando por
|
|
8
|
+
cada una y, si quieres, modelo distinto por comando que devuelve tu sesión
|
|
9
|
+
después.
|
|
10
|
+
|
|
11
|
+
- **Skills canónicas, sin cambios.** El paquete trae los mismos archivos
|
|
12
|
+
`SKILL.md` de este repositorio, byte a byte — sin bifurcación para Pi que se
|
|
13
|
+
desincronice.
|
|
14
|
+
- **Comandos claros.** Escribes `/plan-feature --next`, no
|
|
15
|
+
`/skill:plan-feature --next`.
|
|
16
|
+
- **Enrutamiento que puedes olvidar.** Por defecto no se configura nada: cada
|
|
17
|
+
comando corre en el modelo que ya tenías.
|
|
18
|
+
|
|
19
|
+
## Instalación
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pi install npm:@gtrabanco/pi-agentic-workflow
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Reinicia Pi. `/agentic-workflow-settings` y los comandos de workflow quedan
|
|
26
|
+
disponibles en cualquier proyecto. Si antes copiaste las skills a mano a
|
|
27
|
+
`~/.pi/agent/skills`, borra esa copia: el paquete ya las aporta, y dos copias
|
|
28
|
+
significan dos versiones del mismo método.
|
|
29
|
+
|
|
30
|
+
## Comandos
|
|
31
|
+
|
|
32
|
+
Cada skill incluida cuyo frontmatter dice `user-invocable: true` obtiene un
|
|
33
|
+
comando con el mismo nombre. La lista se lee de las skills al arrancar, así que
|
|
34
|
+
añadir una skill añade su comando — no existe una tabla de alias que mantener.
|
|
35
|
+
Las skills internas que componen una skill pública (los pasos de revisión, el
|
|
36
|
+
preflight de planificación, el contrato del envelope) viajan en el paquete pero
|
|
37
|
+
no reciben comando propio — los componen los de arriba:
|
|
38
|
+
|
|
39
|
+
| Comando | Para qué lo usas |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `/audit-docs` | Comprobar que docs, roadmap, código y el índice de fixes coinciden. |
|
|
42
|
+
| `/audit-pr` | La puerta de merge: ¿está este PR listo? |
|
|
43
|
+
| `/design-feature` | Convertir una idea suelta en un SPEC diseñado. |
|
|
44
|
+
| `/discover-repository-state` | Congelar hechos verificados del repositorio. |
|
|
45
|
+
| `/execute-phase` | Implementar las fases restantes de una unidad planificada. |
|
|
46
|
+
| `/fold-findings` | Reparar los hallazgos «fix-now» persistidos. |
|
|
47
|
+
| `/generate-docs` | Generar guías incrementales basadas en el diff. |
|
|
48
|
+
| `/init-workspace` | Adaptar el andamiaje del workflow a un repositorio. |
|
|
49
|
+
| `/log-session` | Añadir una entrada de sesión estructurada a `docs/LOGS.md`. |
|
|
50
|
+
| `/loop-review-fold` | Revisar una unidad y plegar lo que encontró. |
|
|
51
|
+
| `/plan-feature` | Dirigir el trabajo diseñado a planificación y roadmap. |
|
|
52
|
+
| `/plan-fix` | Redactar un SPEC de fix por fases desde uno o varios issues. |
|
|
53
|
+
| `/product-audit` | Auditar la superficie del producto, no solo el diff. |
|
|
54
|
+
| `/resolve-repository-state` | Resolver una contradicción en hechos congelados. |
|
|
55
|
+
| `/review-change` | Revisar un cambio con los ejes que apliquen. |
|
|
56
|
+
| `/ship-roadmap` | Encontrar o continuar un roadmap, una etapa por ejecución. |
|
|
57
|
+
| `/triage-issue` | Verificar un issue o hallazgo contra el código actual. |
|
|
58
|
+
| `/workflow-status` | Estado de solo lectura del repositorio y el roadmap. |
|
|
59
|
+
|
|
60
|
+
Los argumentos se reenvían tal cual: `/execute-phase P3 --fix` llega a la skill
|
|
61
|
+
como `P3 --fix`.
|
|
62
|
+
|
|
63
|
+
## Enrutamiento por modelo
|
|
64
|
+
|
|
65
|
+
Dos archivos JSON, ambos opcionales:
|
|
66
|
+
|
|
67
|
+
| Alcance | Ruta | Se lee cuando |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| Global | `~/.pi/agent/pi-agentic-workflow.json` | siempre |
|
|
70
|
+
| Proyecto | `<repo>/.pi/pi-agentic-workflow.json` | el proyecto es de confianza |
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"default": { "model": "anthropic/claude-opus-4-5", "thinking": "high" },
|
|
75
|
+
"commands": {
|
|
76
|
+
"plan-feature": { "model": "anthropic/claude-sonnet-4-5", "thinking": "medium" },
|
|
77
|
+
"review-change": { "thinking": "max" }
|
|
78
|
+
},
|
|
79
|
+
"onUnavailableRoute": "stop"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Cada valor se toma del primer sitio que lo declara: **comando en proyecto →
|
|
84
|
+
comando global → ruta por defecto resuelta → defecto del paquete**. `review-change`
|
|
85
|
+
de arriba usa el modelo del default con thinking `max`; todo lo demás corre con lo
|
|
86
|
+
que ya tuviera la sesión, porque la ruta por defecto del paquete es
|
|
87
|
+
`{"model": "inherit", "thinking": "inherit"}`.
|
|
88
|
+
|
|
89
|
+
- `model` debe ser `provider/modelId` — la referencia exacta que muestra
|
|
90
|
+
`/model` — o `"inherit"`.
|
|
91
|
+
- `thinking` es uno de `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`,
|
|
92
|
+
o `"inherit"`.
|
|
93
|
+
- Claves desconocidas, `null` y referencias mal formadas se **rechazan**, no se
|
|
94
|
+
ignoran: un error tipográfico que no hace nada en silencio es el bug que nunca
|
|
95
|
+
encuentras.
|
|
96
|
+
|
|
97
|
+
El primer comando de workflow tras la instalación avisa una sola vez que el
|
|
98
|
+
enrutamiento es configurable, y no vuelve a insistir. Ese aviso se guarda en
|
|
99
|
+
`~/.pi/agent/pi-agentic-workflow-state.json`, no en tu configuración.
|
|
100
|
+
|
|
101
|
+
## Cuando el modelo configurado no está disponible
|
|
102
|
+
|
|
103
|
+
Por defecto el comando **se niega a arrancar** y te dice por qué: el modelo no
|
|
104
|
+
está en el registro, no tiene credenciales, o no se pudo seleccionar. No se envía
|
|
105
|
+
nada, así que nada corre en un modelo que no elegiste. Para usar el modelo actual
|
|
106
|
+
aun así:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "onUnavailableRoute": "inherit" }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Tu sesión vuelve
|
|
113
|
+
|
|
114
|
+
El enrutamiento dura un comando. Cuando el turno termina, la sesión vuelve a
|
|
115
|
+
como estaba — el modelo *y* el nivel de thinking, porque seleccionar un modelo
|
|
116
|
+
puede mover el nivel. Si cambias el modelo tú mismo a mitad de turno, con
|
|
117
|
+
`/model` por ejemplo, no se restaura nada: tu elección gana, y el comando lo
|
|
118
|
+
dice. Si mueves solo el nivel de thinking, lo conservas mientras el modelo
|
|
119
|
+
vuelve.
|
|
120
|
+
|
|
121
|
+
## Consola de configuración
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
/agentic-workflow-settings
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Muestra en qué corre cada comando ahora mismo, y qué archivo se niega a parsearse,
|
|
128
|
+
y luego deja editar **un archivo a la vez** y guardar en alcance global o de
|
|
129
|
+
proyecto. No guarda encima de un archivo que no sabe leer, y no toca el
|
|
130
|
+
archivo de proyecto mientras el proyecto no sea de confianza.
|
|
131
|
+
|
|
132
|
+
## Diagnóstico
|
|
133
|
+
|
|
134
|
+
| Ves | Significa |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `refused: invalid configuration` | Un archivo de configuración fue rechazado. El mismo mensaje nombra el campo, p. ej. `$.commands.plan-feature.model`. Ejecuta `/agentic-workflow-settings` para ver el archivo o arregla el JSON. |
|
|
137
|
+
| `stopped: the configured model` … `is not in the model registry` | La referencia es incorrecta o el proveedor no está configurado. Usa `/model` para ver el `provider/modelId` exacto. |
|
|
138
|
+
| `has no configured credentials` | El modelo existe pero aún no puedes usarlo. Autentica, o pon `onUnavailableRoute` en `inherit`. |
|
|
139
|
+
| `could not be selected` | Pi rechazó el cambio. El comando se detiene con el motivo — salvo que `onUnavailableRoute` sea `inherit`, en cuyo caso avisa y se ejecuta con tu modelo actual. |
|
|
140
|
+
| `refused: the agent is busy` | Hay un turno en ejecución. Espera a que termine. |
|
|
141
|
+
| `is still routed` | El comando enrutado anterior no ha terminado. |
|
|
142
|
+
| `leaving the model you chose in place` | Cambiaste el modelo durante el turno enrutado, así que no se restauró nada: tu elección ganó. |
|
|
143
|
+
| `these configured routes match no command` | Una clave de `commands` no nombra nada. Corrige la escritura o elimina la entrada. |
|
|
144
|
+
|
|
145
|
+
## Notas
|
|
146
|
+
|
|
147
|
+
- Probado con Pi 0.84.3 (`pi install`, skills de paquete, `sendUserMessage` con
|
|
148
|
+
expansión de plantillas).
|
|
149
|
+
- El paquete declara Pi como peer dependency; no incluye ninguna copia de Pi.
|
|
150
|
+
- Las skills pueden indicar al modelo que ejecute comandos. Revísalas como con
|
|
151
|
+
cualquier paquete de terceros.
|
|
152
|
+
|
|
153
|
+
MIT · [Repositorio](https://github.com/gtrabanco/agentic-workflow) ·
|
|
154
|
+
[`docs/features/27-pi-agentic-workflow/`](../../docs/features/27-pi-agentic-workflow/SPEC.md)
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# @gtrabanco/pi-agentic-workflow
|
|
2
|
+
|
|
3
|
+
> 🇪🇸 [Versión en español](README.es.md)
|
|
4
|
+
|
|
5
|
+
One install of the [agentic-workflow](https://github.com/gtrabanco/agentic-workflow)
|
|
6
|
+
method into [Pi](https://github.com/badlogic/pi-mono): the canonical skills, a
|
|
7
|
+
friendly slash command for each of them, and optional per-command model routing
|
|
8
|
+
that gives your session back afterwards.
|
|
9
|
+
|
|
10
|
+
- **Canonical skills, unchanged.** The package ships the same `SKILL.md` files as
|
|
11
|
+
this repository, byte for byte — no Pi-specific fork to drift.
|
|
12
|
+
- **Friendly commands.** Type `/plan-feature --next`, not
|
|
13
|
+
`/skill:plan-feature --next`.
|
|
14
|
+
- **Routing you can forget about.** Nothing is configured by default: every
|
|
15
|
+
command runs on the model you already have.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pi install npm:@gtrabanco/pi-agentic-workflow
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Restart Pi. `/agentic-workflow-settings` and the workflow commands are then
|
|
24
|
+
available in any project. If you previously copied the skills into
|
|
25
|
+
`~/.pi/agent/skills` by hand, delete that copy — the package provides them, and
|
|
26
|
+
two copies means two versions of the same method.
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
Every bundled skill whose frontmatter says `user-invocable: true` gets a command
|
|
31
|
+
with the same name. The list is read from the skills at startup, so adding a
|
|
32
|
+
skill adds its command — there is no alias table to keep in sync. Internal
|
|
33
|
+
skills that a user-facing skill composes (the review passes, the planning
|
|
34
|
+
preflight, the envelope contract) ship inside the package but get no command of
|
|
35
|
+
their own — they are composed by the ones above:
|
|
36
|
+
|
|
37
|
+
| Command | Use it for |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `/audit-docs` | Check that docs, roadmap, code and the fix index agree. |
|
|
40
|
+
| `/audit-pr` | The merge gate: is this PR ready? |
|
|
41
|
+
| `/design-feature` | Turn a raw idea into a designed SPEC. |
|
|
42
|
+
| `/discover-repository-state` | Freeze verified repository facts. |
|
|
43
|
+
| `/execute-phase` | Implement the remaining phases of a planned unit. |
|
|
44
|
+
| `/fold-findings` | Repair persisted fix-now findings. |
|
|
45
|
+
| `/generate-docs` | Generate incremental, diff-driven developer guides. |
|
|
46
|
+
| `/init-workspace` | Adapt the workflow scaffold to a repository. |
|
|
47
|
+
| `/log-session` | Append a structured session entry to `docs/LOGS.md`. |
|
|
48
|
+
| `/loop-review-fold` | Review a unit, then fold what it found. |
|
|
49
|
+
| `/plan-feature` | Route designed work into planning and the roadmap. |
|
|
50
|
+
| `/plan-fix` | Draft a phased fix SPEC from one or more issues. |
|
|
51
|
+
| `/product-audit` | Audit the product surface, not just the diff. |
|
|
52
|
+
| `/resolve-repository-state` | Resolve a contradiction in frozen facts. |
|
|
53
|
+
| `/review-change` | Review a change with the applicable axes. |
|
|
54
|
+
| `/ship-roadmap` | Find or continue a roadmap, one stage per run. |
|
|
55
|
+
| `/triage-issue` | Verify an issue or finding against current code. |
|
|
56
|
+
| `/workflow-status` | Read-only state of the repository and roadmap. |
|
|
57
|
+
|
|
58
|
+
Arguments are forwarded verbatim: `/execute-phase P3 --fix` reaches the skill as
|
|
59
|
+
`P3 --fix`.
|
|
60
|
+
|
|
61
|
+
## Model routing
|
|
62
|
+
|
|
63
|
+
Two JSON files, both optional:
|
|
64
|
+
|
|
65
|
+
| Scope | Path | Read when |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| Global | `~/.pi/agent/pi-agentic-workflow.json` | always |
|
|
68
|
+
| Project | `<repo>/.pi/pi-agentic-workflow.json` | the project is trusted |
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"default": { "model": "anthropic/claude-opus-4-5", "thinking": "high" },
|
|
73
|
+
"commands": {
|
|
74
|
+
"plan-feature": { "model": "anthropic/claude-sonnet-4-5", "thinking": "medium" },
|
|
75
|
+
"review-change": { "thinking": "max" }
|
|
76
|
+
},
|
|
77
|
+
"onUnavailableRoute": "stop"
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A value is taken from the first place that declares it: **project command →
|
|
82
|
+
global command → resolved default route → shipped default**. `review-change`
|
|
83
|
+
above runs on the default model with `max` thinking; anything else runs on
|
|
84
|
+
whatever the session already had, because the shipped default route is
|
|
85
|
+
`{"model": "inherit", "thinking": "inherit"}`.
|
|
86
|
+
|
|
87
|
+
- `model` must be `provider/modelId` — the exact reference `/model` shows — or
|
|
88
|
+
`"inherit"`.
|
|
89
|
+
- `thinking` is one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`,
|
|
90
|
+
or `"inherit"`.
|
|
91
|
+
- Unknown keys, `null`s and malformed references are **rejected**, not ignored: a
|
|
92
|
+
typo that silently did nothing is the bug you would never find.
|
|
93
|
+
|
|
94
|
+
The first workflow command you run after install says once that routing is
|
|
95
|
+
configurable, then never again. That acknowledgement is stored in
|
|
96
|
+
`~/.pi/agent/pi-agentic-workflow-state.json`, not in your config.
|
|
97
|
+
|
|
98
|
+
## When a configured model is unavailable
|
|
99
|
+
|
|
100
|
+
Default: the command **refuses to start** and tells you why — the model is not in
|
|
101
|
+
the registry, has no credentials, or could not be selected. Nothing is sent, so
|
|
102
|
+
nothing runs on a model you did not pick. To run anyway on the current model, set:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{ "onUnavailableRoute": "inherit" }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Your session comes back
|
|
109
|
+
|
|
110
|
+
Routing lasts one command. When the turn settles, the session is put back the way
|
|
111
|
+
you had it — the model *and* the thinking level, because selecting a model can
|
|
112
|
+
move the level. If you change the model yourself mid-turn, with `/model`, say,
|
|
113
|
+
nothing is restored: your choice wins, and the command says so. Change only the
|
|
114
|
+
thinking level and you keep it while the model still comes back.
|
|
115
|
+
|
|
116
|
+
## Settings console
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
/agentic-workflow-settings
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Shows what each command runs on right now, and which file is refusing to parse,
|
|
123
|
+
then lets you edit **one file at a time** and save to global or project scope. It will not save over a file it cannot parse, and it will not
|
|
124
|
+
touch the project file while the project is untrusted.
|
|
125
|
+
|
|
126
|
+
## Troubleshooting
|
|
127
|
+
|
|
128
|
+
| You see | Means |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `refused: invalid configuration` | A config file was rejected. The same message names the field, e.g. `$.commands.plan-feature.model`. Run `/agentic-workflow-settings` to see the file, or fix the JSON. |
|
|
131
|
+
| `stopped: the configured model` … `is not in the model registry` | The reference is wrong or the provider is not configured. Use `/model` to see the exact `provider/modelId`. |
|
|
132
|
+
| `has no configured credentials` | The model exists but you cannot use it yet. Authenticate, or set `onUnavailableRoute` to `inherit`. |
|
|
133
|
+
| `could not be selected` | Pi refused the switch. The command stops with the reason — unless `onUnavailableRoute` is `inherit`, in which case it warns and runs on your current model. |
|
|
134
|
+
| `refused: the agent is busy` | A turn is running. Wait for it to settle. |
|
|
135
|
+
| `is still routed` | The previous routed command has not settled yet. |
|
|
136
|
+
| `leaving the model you chose in place` | You changed the model during a routed turn, so nothing was restored — your choice won. |
|
|
137
|
+
| `these configured routes match no command` | A `commands` key names nothing. Fix the spelling or delete the entry. |
|
|
138
|
+
|
|
139
|
+
## Notes
|
|
140
|
+
|
|
141
|
+
- Verified against Pi 0.84.3 (`pi install`, package skills, `sendUserMessage` with
|
|
142
|
+
prompt template expansion).
|
|
143
|
+
- The package declares Pi as a peer dependency; it bundles no copy of Pi.
|
|
144
|
+
- Skills can instruct the model to run commands. Review them as you would any
|
|
145
|
+
third-party package.
|
|
146
|
+
|
|
147
|
+
MIT · [Repository](https://github.com/gtrabanco/agentic-workflow) ·
|
|
148
|
+
[`docs/features/27-pi-agentic-workflow/`](../../docs/features/27-pi-agentic-workflow/SPEC.md)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EffectiveConfig, Route } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The in-package default (SPEC S6, D-P6): a fresh install with no config file
|
|
4
|
+
* anywhere routes every command on the session model, and the fail-closed
|
|
5
|
+
* fallback policy starts at `stop`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_ROUTE: Route;
|
|
8
|
+
export declare const DEFAULT_CONFIG: EffectiveConfig;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-package default (SPEC S6, D-P6): a fresh install with no config file
|
|
3
|
+
* anywhere routes every command on the session model, and the fail-closed
|
|
4
|
+
* fallback policy starts at `stop`.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_ROUTE = { model: "inherit", thinking: "inherit" };
|
|
7
|
+
export const DEFAULT_CONFIG = {
|
|
8
|
+
default: { ...DEFAULT_ROUTE },
|
|
9
|
+
commands: {},
|
|
10
|
+
onUnavailableRoute: "stop",
|
|
11
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ConfigPaths, ConfigProblem, EffectiveConfig } from "./types.js";
|
|
2
|
+
export declare function configFilePaths(agentDir: string, cwd: string): ConfigPaths;
|
|
3
|
+
export interface LoadConfigInput {
|
|
4
|
+
agentDir: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
projectTrusted: boolean;
|
|
7
|
+
/** Returns the file text, or null when the path does not exist. A reader that
|
|
8
|
+
* cannot honour an existing path throws, and the throw becomes a problem:
|
|
9
|
+
* "unreadable" must not quietly mean "unconfigured" (F7). */
|
|
10
|
+
readFile?: (path: string) => string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface LoadedConfig {
|
|
13
|
+
/** False when any present file failed to parse or validate → refuse dispatch. */
|
|
14
|
+
ok: boolean;
|
|
15
|
+
/** The merged configuration; the shipped default whenever `ok` is false. */
|
|
16
|
+
config: EffectiveConfig;
|
|
17
|
+
problems: ConfigProblem[];
|
|
18
|
+
}
|
|
19
|
+
export declare function loadConfig({ agentDir, cwd, projectTrusted, readFile }: LoadConfigInput): LoadedConfig;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { DEFAULT_CONFIG } from "./defaults.js";
|
|
4
|
+
import { mergeConfigs } from "./merge.js";
|
|
5
|
+
import { parseConfigFile } from "./schema.js";
|
|
6
|
+
/**
|
|
7
|
+
* Load the two dedicated JSON files into one effective configuration
|
|
8
|
+
* (SPEC S5, S11; D-E5, D-P6).
|
|
9
|
+
*
|
|
10
|
+
* The three rules this module owns, and nothing else:
|
|
11
|
+
* - a MISSING file resolves to the shipped default (`inherit`);
|
|
12
|
+
* - a PRESENT-but-invalid file is a problem — never a silent `inherit`, so an
|
|
13
|
+
* operator can't believe a strong model ran when it did not;
|
|
14
|
+
* - a file that EXISTS but cannot be read is the same kind of problem: it is
|
|
15
|
+
* present, so its routing intentions are unknown, and unknown is refused
|
|
16
|
+
* rather than defaulted (F7);
|
|
17
|
+
* - the project file is not even read while the project is untrusted, because
|
|
18
|
+
* a cloned repository must not be able to steer routing (S11).
|
|
19
|
+
*
|
|
20
|
+
* Reading is injected so tests drive every branch from memory; the caller
|
|
21
|
+
* supplies the directories (Pi's `getAgentDir()` and `ctx.cwd`), never this
|
|
22
|
+
* module.
|
|
23
|
+
*/
|
|
24
|
+
const CONFIG_FILE_NAME = "pi-agentic-workflow.json";
|
|
25
|
+
const PROJECT_CONFIG_DIR_NAME = ".pi";
|
|
26
|
+
export function configFilePaths(agentDir, cwd) {
|
|
27
|
+
return {
|
|
28
|
+
global: join(agentDir, CONFIG_FILE_NAME),
|
|
29
|
+
project: join(cwd, PROJECT_CONFIG_DIR_NAME, CONFIG_FILE_NAME),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const NOT_THERE = new Set(["ENOENT", "ENOTDIR"]);
|
|
33
|
+
/** Absent is `null`; present-but-unreadable throws so the caller records it. */
|
|
34
|
+
const readIfExists = (path) => {
|
|
35
|
+
try {
|
|
36
|
+
return readFileSync(path, "utf8");
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (NOT_THERE.has(error.code ?? ""))
|
|
40
|
+
return null;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function loadScope(scope, path, readFile, problems) {
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = readFile(path);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const code = error.code ?? "read error";
|
|
51
|
+
problems.push({ scope, path: "$", message: `the file could not be read (${code})` });
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
if (text === null)
|
|
55
|
+
return {};
|
|
56
|
+
const result = parseConfigFile(text);
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
for (const issue of result.issues)
|
|
59
|
+
problems.push({ scope, path: issue.path, message: issue.message });
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
return result.config;
|
|
63
|
+
}
|
|
64
|
+
export function loadConfig({ agentDir, cwd, projectTrusted, readFile = readIfExists }) {
|
|
65
|
+
const paths = configFilePaths(agentDir, cwd);
|
|
66
|
+
const problems = [];
|
|
67
|
+
const globalFile = loadScope("global", paths.global, readFile, problems);
|
|
68
|
+
const projectFile = projectTrusted ? loadScope("project", paths.project, readFile, problems) : {};
|
|
69
|
+
return {
|
|
70
|
+
ok: problems.length === 0,
|
|
71
|
+
// Fail closed: a broken file hands the caller the unrouted default and the
|
|
72
|
+
// problems that explain why dispatch is refused.
|
|
73
|
+
config: problems.length === 0 ? mergeConfigs(globalFile, projectFile) : DEFAULT_CONFIG,
|
|
74
|
+
problems,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ConfigFile, EffectiveConfig, Route } from "./types.js";
|
|
2
|
+
export declare function mergeConfigs(globalFile?: ConfigFile, projectFile?: ConfigFile): EffectiveConfig;
|
|
3
|
+
/** The route a command runs under: its own resolved override, else the default route. */
|
|
4
|
+
export declare function effectiveRoute(config: EffectiveConfig, command: string): Route;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DEFAULT_CONFIG, DEFAULT_ROUTE } from "./defaults.js";
|
|
2
|
+
/**
|
|
3
|
+
* Project-over-global merge (SPEC S5, S6).
|
|
4
|
+
*
|
|
5
|
+
* Merge granularity is the individual route key, not the route object: a
|
|
6
|
+
* project that sets only `commands.x.model` keeps the global `thinking` for
|
|
7
|
+
* that command. A command neither scope mentions falls back to the resolved
|
|
8
|
+
* default route, and an absent default is the shipped `inherit`.
|
|
9
|
+
*
|
|
10
|
+
* Inputs are validated files — an invalid file never reaches this function
|
|
11
|
+
* (D-E5) — and both are treated as read-only: the result is built from fresh
|
|
12
|
+
* objects so no caller can alias through it.
|
|
13
|
+
*/
|
|
14
|
+
function pick(fallback, ...values) {
|
|
15
|
+
for (const value of values)
|
|
16
|
+
if (value !== undefined)
|
|
17
|
+
return value;
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
function resolveRoute(defaults, globalRoute, projectRoute) {
|
|
21
|
+
return {
|
|
22
|
+
model: pick(DEFAULT_ROUTE.model, projectRoute?.model, globalRoute?.model, defaults.model),
|
|
23
|
+
thinking: pick(DEFAULT_ROUTE.thinking, projectRoute?.thinking, globalRoute?.thinking, defaults.thinking),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function mergeConfigs(globalFile = {}, projectFile = {}) {
|
|
27
|
+
const effectiveDefault = resolveRoute(DEFAULT_ROUTE, globalFile.default, projectFile.default);
|
|
28
|
+
const globalCommands = globalFile.commands ?? {};
|
|
29
|
+
const projectCommands = projectFile.commands ?? {};
|
|
30
|
+
const commands = {};
|
|
31
|
+
for (const name of new Set([...Object.keys(globalCommands), ...Object.keys(projectCommands)])) {
|
|
32
|
+
commands[name] = resolveRoute(effectiveDefault, globalCommands[name], projectCommands[name]);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
default: effectiveDefault,
|
|
36
|
+
commands,
|
|
37
|
+
onUnavailableRoute: projectFile.onUnavailableRoute ?? globalFile.onUnavailableRoute ?? DEFAULT_CONFIG.onUnavailableRoute,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** The route a command runs under: its own resolved override, else the default route. */
|
|
41
|
+
export function effectiveRoute(config, command) {
|
|
42
|
+
return config.commands[command] ?? config.default;
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ConfigFile, ConfigIssue } from "./types.js";
|
|
2
|
+
/** The split Pi performs when resolving a reference: provider before the first slash. */
|
|
3
|
+
export interface ModelParts {
|
|
4
|
+
provider: string;
|
|
5
|
+
id: string;
|
|
6
|
+
}
|
|
7
|
+
/** Same rule as `isModelReference`, plus the split Pi performs when resolving. */
|
|
8
|
+
export declare function parseModelReference(value: unknown): ModelParts | undefined;
|
|
9
|
+
export type ParseResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
config: ConfigFile;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
issues: ConfigIssue[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Parse and validate one config file's text. A blank file is an empty config so
|
|
18
|
+
* the loader can treat "nothing declared" like "nothing present" without a
|
|
19
|
+
* second read.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseConfigFile(text: string): ParseResult;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { THINKING_LEVELS, UNAVAILABLE_ROUTE_POLICIES } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Strict validator for one config file (SPEC S5-S8, D-E5).
|
|
4
|
+
*
|
|
5
|
+
* Strict means: only the documented keys, only the documented value shapes, and
|
|
6
|
+
* no coercion. A file that is *present but invalid* must produce issues,
|
|
7
|
+
* because silently falling back to `inherit` would let an operator believe a
|
|
8
|
+
* strong model ran when it did not. Only a *missing* file resolves to the
|
|
9
|
+
* default, and that decision belongs to the loader.
|
|
10
|
+
*/
|
|
11
|
+
const ROOT_KEYS = new Set(["default", "commands", "onUnavailableRoute"]);
|
|
12
|
+
const ROUTE_KEYS = new Set(["model", "thinking"]);
|
|
13
|
+
const COMMAND_NAME = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
/** Bracket-quoted when a key is not a plain slug, so the path stays parseable. */
|
|
18
|
+
function displayKey(key) {
|
|
19
|
+
return COMMAND_NAME.test(key) ? key : JSON.stringify(key);
|
|
20
|
+
}
|
|
21
|
+
/** An exact `provider/modelId`. The id may contain slashes: Pi splits at the first one. */
|
|
22
|
+
function isModelReference(value) {
|
|
23
|
+
return parseModelReference(value) !== undefined;
|
|
24
|
+
}
|
|
25
|
+
/** Same rule as `isModelReference`, plus the split Pi performs when resolving. */
|
|
26
|
+
export function parseModelReference(value) {
|
|
27
|
+
if (typeof value !== "string" || value !== value.trim() || /\s/u.test(value))
|
|
28
|
+
return undefined;
|
|
29
|
+
const slash = value.indexOf("/");
|
|
30
|
+
if (slash <= 0 || slash >= value.length - 1)
|
|
31
|
+
return undefined;
|
|
32
|
+
return { provider: value.slice(0, slash), id: value.slice(slash + 1) };
|
|
33
|
+
}
|
|
34
|
+
function isThinkingSetting(value) {
|
|
35
|
+
return value === "inherit" || THINKING_LEVELS.includes(value);
|
|
36
|
+
}
|
|
37
|
+
function isUnavailableRoutePolicy(value) {
|
|
38
|
+
return UNAVAILABLE_ROUTE_POLICIES.includes(value);
|
|
39
|
+
}
|
|
40
|
+
function describe(value) {
|
|
41
|
+
return typeof value === "string" ? `"${value}"` : JSON.stringify(value) ?? String(value);
|
|
42
|
+
}
|
|
43
|
+
function checkRoute(value, path, issues) {
|
|
44
|
+
if (!isRecord(value)) {
|
|
45
|
+
issues.push({ path, message: "must be an object with optional model/thinking keys" });
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const route = {};
|
|
49
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
50
|
+
if (!ROUTE_KEYS.has(key)) {
|
|
51
|
+
issues.push({ path: `${path}.${displayKey(key)}`, message: `unknown route key "${key}" (allowed: model, thinking)` });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (key === "model") {
|
|
55
|
+
if (entry !== "inherit" && !isModelReference(entry)) {
|
|
56
|
+
issues.push({
|
|
57
|
+
path: `${path}.model`,
|
|
58
|
+
message: `must be "inherit" or "provider/modelId", got ${describe(entry)}`,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
route.model = entry;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!isThinkingSetting(entry)) {
|
|
66
|
+
issues.push({
|
|
67
|
+
path: `${path}.thinking`,
|
|
68
|
+
message: `must be "inherit" or one of ${THINKING_LEVELS.join(", ")}, got ${describe(entry)}`,
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
route.thinking = entry;
|
|
73
|
+
}
|
|
74
|
+
return route;
|
|
75
|
+
}
|
|
76
|
+
/** Validate an already-parsed JSON value. */
|
|
77
|
+
function validateConfig(value) {
|
|
78
|
+
if (!isRecord(value)) {
|
|
79
|
+
return { ok: false, issues: [{ path: "$", message: "config root must be a JSON object" }] };
|
|
80
|
+
}
|
|
81
|
+
const issues = [];
|
|
82
|
+
const config = {};
|
|
83
|
+
for (const key of Object.keys(value)) {
|
|
84
|
+
if (!ROOT_KEYS.has(key)) {
|
|
85
|
+
issues.push({
|
|
86
|
+
path: `$.${displayKey(key)}`,
|
|
87
|
+
message: `unknown config key "${key}" (allowed: default, commands, onUnavailableRoute)`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (value.default !== undefined) {
|
|
92
|
+
const route = checkRoute(value.default, "$.default", issues);
|
|
93
|
+
if (route)
|
|
94
|
+
config.default = route;
|
|
95
|
+
}
|
|
96
|
+
if (value.commands !== undefined) {
|
|
97
|
+
if (!isRecord(value.commands)) {
|
|
98
|
+
issues.push({ path: "$.commands", message: "must be an object mapping command names to routes" });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const commands = {};
|
|
102
|
+
for (const [name, raw] of Object.entries(value.commands)) {
|
|
103
|
+
if (!COMMAND_NAME.test(name)) {
|
|
104
|
+
issues.push({ path: `$.commands.${displayKey(name)}`, message: "command name must be a single lowercase slug" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const route = checkRoute(raw, `$.commands.${displayKey(name)}`, issues);
|
|
108
|
+
if (route)
|
|
109
|
+
commands[name] = route;
|
|
110
|
+
}
|
|
111
|
+
config.commands = commands;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (value.onUnavailableRoute !== undefined) {
|
|
115
|
+
if (!isUnavailableRoutePolicy(value.onUnavailableRoute)) {
|
|
116
|
+
issues.push({
|
|
117
|
+
path: "$.onUnavailableRoute",
|
|
118
|
+
message: `must be "stop" or "inherit", got ${describe(value.onUnavailableRoute)}`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
config.onUnavailableRoute = value.onUnavailableRoute;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return issues.length > 0 ? { ok: false, issues } : { ok: true, config };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Parse and validate one config file's text. A blank file is an empty config so
|
|
129
|
+
* the loader can treat "nothing declared" like "nothing present" without a
|
|
130
|
+
* second read.
|
|
131
|
+
*/
|
|
132
|
+
export function parseConfigFile(text) {
|
|
133
|
+
if (text.trim() === "")
|
|
134
|
+
return { ok: true, config: {} };
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = JSON.parse(text);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
141
|
+
return { ok: false, issues: [{ path: "$", message: `invalid JSON: ${reason}` }] };
|
|
142
|
+
}
|
|
143
|
+
return validateConfig(parsed);
|
|
144
|
+
}
|