@bluewombat/runtime 0.9.0 → 0.10.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 +122 -52
- package/SPECS.md +36 -14
- package/dist/cli.js +4 -4
- package/dist/config/agent-shorthand.d.ts +24 -3
- package/dist/config/agent-shorthand.d.ts.map +1 -1
- package/dist/config/agent-shorthand.js +64 -19
- package/dist/config/agent-shorthand.js.map +1 -1
- package/dist/config/home.d.ts +2 -0
- package/dist/config/home.d.ts.map +1 -1
- package/dist/config/home.js +2 -0
- package/dist/config/home.js.map +1 -1
- package/dist/config/load-config.d.ts.map +1 -1
- package/dist/config/load-config.js +298 -46
- package/dist/config/load-config.js.map +1 -1
- package/dist/config/parse-args.d.ts +1 -1
- package/dist/config/parse-args.d.ts.map +1 -1
- package/dist/config/parse-args.js +83 -56
- package/dist/config/parse-args.js.map +1 -1
- package/dist/config/types.d.ts +104 -16
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +13 -1
- package/dist/config/types.js.map +1 -1
- package/dist/loop/authority.d.ts +38 -1
- package/dist/loop/authority.d.ts.map +1 -1
- package/dist/loop/authority.js +41 -2
- package/dist/loop/authority.js.map +1 -1
- package/dist/loop/context.d.ts +16 -1
- package/dist/loop/context.d.ts.map +1 -1
- package/dist/loop/drive.d.ts +9 -3
- package/dist/loop/drive.d.ts.map +1 -1
- package/dist/loop/drive.js +17 -7
- package/dist/loop/drive.js.map +1 -1
- package/dist/loop/features.d.ts.map +1 -1
- package/dist/loop/features.js +322 -59
- package/dist/loop/features.js.map +1 -1
- package/dist/loop/open-host.d.ts.map +1 -1
- package/dist/loop/open-host.js +109 -7
- package/dist/loop/open-host.js.map +1 -1
- package/dist/loop/report.d.ts.map +1 -1
- package/dist/loop/report.js +21 -9
- package/dist/loop/report.js.map +1 -1
- package/dist/loop/trace.js +1 -1
- package/dist/loop/trace.js.map +1 -1
- package/dist/loop/transformers.d.ts +16 -2
- package/dist/loop/transformers.d.ts.map +1 -1
- package/dist/loop/transformers.js +137 -22
- package/dist/loop/transformers.js.map +1 -1
- package/dist/loop/warm.js +1 -1
- package/dist/loop/warm.js.map +1 -1
- package/dist/operator/agent-plugins.d.ts.map +1 -1
- package/dist/operator/agent-plugins.js +14 -8
- package/dist/operator/agent-plugins.js.map +1 -1
- package/dist/operator/doctor.d.ts +1 -1
- package/dist/operator/doctor.d.ts.map +1 -1
- package/dist/operator/doctor.js +79 -29
- package/dist/operator/doctor.js.map +1 -1
- package/dist/operator/explain.d.ts.map +1 -1
- package/dist/operator/explain.js +37 -8
- package/dist/operator/explain.js.map +1 -1
- package/dist/operator/film.js +0 -3
- package/dist/operator/film.js.map +1 -1
- package/dist/operator/init.js +13 -8
- package/dist/operator/init.js.map +1 -1
- package/dist/operator/log.js +9 -2
- package/dist/operator/log.js.map +1 -1
- package/dist/operator/setup.d.ts.map +1 -1
- package/dist/operator/setup.js +2 -0
- package/dist/operator/setup.js.map +1 -1
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -151,13 +151,17 @@ manager with no `probe`), stop `run` if it holds the ledger lock, then:
|
|
|
151
151
|
mason cancel github:owner/name#19
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
A unit that was stopped (Ctrl-C, a crash) is picked up where it was by the next
|
|
155
|
+
`mason run`: its workspace is kept, the Attempts it finished still count and the
|
|
156
|
+
numbering goes on. To start a unit over instead (the line moved on, the work took
|
|
157
|
+
a wrong turn), stop `run` if it holds the lock, then:
|
|
156
158
|
|
|
157
159
|
```bash
|
|
158
160
|
mason release github:owner/name#19
|
|
159
161
|
```
|
|
160
162
|
|
|
163
|
+
`release` destroys the workspace and forgets it; the next `run` isolates a fresh one.
|
|
164
|
+
|
|
161
165
|
The film is `<ledger>/events.jsonl`, next to `cursor`. It is not truth after a
|
|
162
166
|
crash — the ledger is. The file can grow; there is no rotation yet.
|
|
163
167
|
|
|
@@ -194,13 +198,12 @@ builder:
|
|
|
194
198
|
producer:
|
|
195
199
|
cmd: [node, ./mason-builder.mjs]
|
|
196
200
|
timeoutMs: 600000
|
|
197
|
-
gates: { defaultTimeoutMs: 120000, gates: [] }
|
|
198
201
|
repair:
|
|
199
202
|
cmd: [node, ./mason-repair.mjs]
|
|
200
203
|
timeoutMs: 600000
|
|
201
|
-
|
|
204
|
+
gates: { defaultTimeoutMs: 120000, gates: [] }
|
|
202
205
|
maxAttempts: 3
|
|
203
|
-
|
|
206
|
+
ticket:
|
|
204
207
|
gates: { defaultTimeoutMs: 900000, gates: [] }
|
|
205
208
|
authority:
|
|
206
209
|
enabled: false
|
|
@@ -213,6 +216,7 @@ pollIntervalMs: 10000
|
|
|
213
216
|
| `featureTracker` | `package` (a name or path that exports `createManager`) and `options`: where Features are written and signalled |
|
|
214
217
|
| `ticketTracker` | Same shape: where the Tickets of a Feature are, and the Authority that holds the lines. May name the same package as `featureTracker`, or another |
|
|
215
218
|
| `workLine.stable` | Existing directory the work folds into. Optional when the manager names a reference work line: Host then keeps its own copy at `<home>/work-line` |
|
|
219
|
+
| `workLine.targetStable` | A second copy of the work line, on the branch a Feature is folded into: what is merged into the Feature when it is brought up to date. Host keeps its own at `<home>/target-line` when the manager names a reference; required with an Authority otherwise |
|
|
216
220
|
| `workLine.isolation` | Package name or path exporting `strategy` — required. Host loads it like a manager; Isolator/Integrator do not sniff |
|
|
217
221
|
| `workLine.isolationOptions` | What that strategy is told, unread by Host — like a manager's `options`. `isolation-git` reads `exclude`, the paths a Child never gets (default `.env`, `.env.*`). A strategy without `createStrategy` refuses any |
|
|
218
222
|
| `workLine.warm` | Optional `cmd` / `timeoutMs`. Runs in the workspace after Isolation, before the Builder — typically `pnpm install --frozen-lockfile`. Absent: the Builder gets a cold tree |
|
|
@@ -223,26 +227,31 @@ pollIntervalMs: 10000
|
|
|
223
227
|
| `ledger` | WorkLedger root (created). The Cursor is a file inside it. Default `<home>/ledger` |
|
|
224
228
|
| `persist` | Package name or path exporting `openPersist({ ledgerRoot })`: how the ledger is stored under that root. Default `@bluewombat/persist-fs`; `@bluewombat/persist-sqlite` ships too. `watch` reads through the same backend |
|
|
225
229
|
| `agents` | Agents a pass names instead of writing `cmd` (§ Agent passes written short) |
|
|
226
|
-
| `builder.
|
|
227
|
-
| `builder.
|
|
230
|
+
| `builder.tests` | Required. The tests of the task, written before its code by an agent that is not the producer: `{ agent, prompt, homePlugins, appDeckProfile, timeoutMs, gates }`, or `{ agent: false }` for none. Its `gates` check them (`tests-red`: they must fail, for the right reason). What it wrote is committed as a checkpoint titled `Tests: …`, which `builder.gates` can keep intact (`tests-frozen`). A ticket the tracker labels as having no tests first (`no-tests` with the GitHub manager) runs no agent and no `tests-red` |
|
|
231
|
+
| `builder.e2e` | Optional. Declares the e2e acceptance check (see § The e2e acceptance check): the pass that makes the Ticket holding the suite, `{ agent, prompt, homePlugins, appDeckProfile, timeoutMs, gates }` or `{ cmd, timeoutMs, gates }`. One command for the first pass and for a pass a Gate refused; its own `gates` (they check the form: compiles, one test per manifest id, no application file touched). Nothing is borrowed from `builder.producer`, `builder.repair` or `builder.gates`. No tests are written first for that Ticket. Needs a Feature Gate with `skipWhen: skip-test-e2e`, and the other way round |
|
|
232
|
+
| `builder.producer` | `cmd` / `timeoutMs`. Spawned in the workspace. Exit 0 runs `builder.gates` |
|
|
233
|
+
| `builder.repair` | The same, for a pass a Gate refused. **Required**, and never inherited: it names its own command. It is judged by the same `builder.gates` as the producer |
|
|
234
|
+
| `builder.gates` | `defaultTimeoutMs` and `gates`: the one Gate list for `builder.producer` and `builder.repair`. `gates` written on either pass is refused, naming this key. Empty: the unit runs ungated |
|
|
228
235
|
| `builder.maxAttempts` | Attempts one Task may start. Default 3 |
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
236
|
+
| `ticket.fix` | `cmd` / `timeoutMs` / `gates`. Spawned when a judgement of the whole refused it — by an Authority, by `ticket.validate`, or by both. Judged by its own `gates` |
|
|
237
|
+
| `ticket.validate` | `cmd` / `timeoutMs` / `gates`. A local, read-only judge of the assembled ticket, run after align, with or without an Authority. A refusal is repaired by `ticket.fix` the same as an Authority's. Judged by its own `gates`, never `builder`'s or `ticket.fix`'s. `validate` is required when a `ticket` block is written, and so is `feature.validate`: `{ agent: false, gates }` turns the agent off and keeps the gates, and `{ agent: false }` with no gates turns the review off — a bare `gates:` counts as none. |
|
|
238
|
+
| `ticket.maxAttempts` | Attempts that one fix may start. Default 3 |
|
|
239
|
+
| `ticket.gates` | `defaultTimeoutMs` and `gates`: the produce:false judgement-only pass — no producer of its own, re-checking the assembled ticket. Empty: the Authority judges alone. Independent of `ticket.fix.gates` / `ticket.validate.gates` — never shared with either |
|
|
233
240
|
| `timeoutMs` | How long a child **outside** a Task may run: manager, isolations |
|
|
234
|
-
| `maxRefusals` | Times the work may be sent back before it escalates, by an Authority, by `
|
|
241
|
+
| `ticket.maxRefusals` | Times the work may be sent back before it escalates, by an Authority, by `ticket.validate`, or by both — one shared budget. Default 3 |
|
|
242
|
+
| `ticket.maxParallel` | How many Tickets of a Project are worked on at once. Default 1: one after the other. Above 1 needs `authority.enabled`; a Ticket waiting on a check or a person holds a place, the others go on. Tickets that wait on another (*Blocked by*) still start only once it is `done` |
|
|
243
|
+
| `feature` | Required with `authority.enabled`. How a Feature's line is delivered to the line it is folded into, by the same steps as a Ticket: the final line is merged into the Feature's work (`workLine.targetStable`, or Host's own copy of the reference), `validate` (same shape as `ticket.validate`) reviews the whole against the Feature's request and its `gates` (at least one) are where the project's checks on the whole line run, a refusal goes to `fix`, the line is published, and `gates` (at least one, the CI) judge it. `maxAttempts` and `maxRefusals` (default 3, same rule as a Ticket's) are its own. Nothing falls back to `ticket`. The merge itself is automatic only with `merge-when-ready`; the delivery to the Authority is for every Feature |
|
|
235
244
|
| `pollIntervalMs` | Set it and the process keeps draining until SIGINT. Absent: one tick. Pick it with § Choosing `pollIntervalMs` — 10000 is the floor worth having |
|
|
236
245
|
| `observability.streams` | `enabled`, `dir`, `keep`. Films what every child says, as it says it — one file per child under `<dir>/<ticket>/`, named in the journal by a `stream-opened` line. `dir` resolves against the config file; default `<home>/streams`. `keep` is `stdout` / `stderr`, default both. Off unless `enabled` is true: a stream is the Project's own code and prompts in the clear |
|
|
237
246
|
|
|
238
247
|
Every `cmd` is named where it is used, and nothing falls back to a neighbour: a
|
|
239
248
|
`repair` with no command of its own is refused rather than quietly running the
|
|
240
|
-
first-pass agent again. `builder` makes the unit of work. `
|
|
241
|
-
and, when that judgement refuses it, runs `
|
|
242
|
-
`
|
|
249
|
+
first-pass agent again. `builder` makes the unit of work. `ticket` judges the whole
|
|
250
|
+
and, when that judgement refuses it, runs `ticket.fix` — not a second first-pass.
|
|
251
|
+
`ticket.validate`, when declared, is a second, local judge of the whole: it
|
|
243
252
|
runs after align, before the Authority sees the work (or before the final
|
|
244
|
-
fold, with none). Its refusal is repaired by `
|
|
245
|
-
Authority's is, and shares the same `maxRefusals` budget.
|
|
253
|
+
fold, with none). Its refusal is repaired by `ticket.fix` exactly as an
|
|
254
|
+
Authority's is, and shares the same `ticket.maxRefusals` budget.
|
|
246
255
|
|
|
247
256
|
Paths a slot **writes** — `--transcript-dir`, `--agent-home` and
|
|
248
257
|
`--plugins-out` — always resolve against the config file's directory, even
|
|
@@ -270,32 +279,35 @@ builder:
|
|
|
270
279
|
producer:
|
|
271
280
|
agent: cursor
|
|
272
281
|
prompt: ./prompts/builder.md
|
|
273
|
-
|
|
274
|
-
|
|
282
|
+
homePlugins: [./ai-plugins/core/builder]
|
|
283
|
+
appDeckProfile: .ai/profiles/builder
|
|
275
284
|
args: [--mode, agent] # the vendor CLI's own flags
|
|
276
285
|
timeoutMs: 1200000
|
|
277
286
|
gates: { defaultTimeoutMs: 120000, gates: [] }
|
|
278
287
|
```
|
|
279
288
|
|
|
280
289
|
Host writes the `cmd` it stands for — `node <slots>/<role>.mjs --prompt-file …
|
|
281
|
-
-- node <slots>/agents/cursor.mjs --transcript-dir … --plugin-dir … --profile
|
|
290
|
+
-- node <slots>/agents/cursor.mjs --transcript-dir … --plugin-dir … --app-deck-profile
|
|
282
291
|
… --plugins-out … --agent-arg …` — with `<slots>` the
|
|
283
292
|
`node_modules/@bluewombat/slots` next to the config. Nothing past the config
|
|
284
293
|
knows a pass was written short. The roles: `builder.producer`,
|
|
285
|
-
`builder.repair`, `
|
|
286
|
-
(an argv, or `{ agent, prompt?,
|
|
294
|
+
`builder.repair`, `ticket.fix`, `ticket.validate`, and `authority.describe`
|
|
295
|
+
(an argv, or `{ agent, prompt?, homePlugins?, appDeckProfile?, args? }`).
|
|
287
296
|
|
|
288
297
|
| Key | Meaning |
|
|
289
298
|
| --------- | --------------------------------------------------------------------------------------------------------- |
|
|
290
299
|
| `agent` | A name `agents` declares. With `cmd` beside it, the pass is refused |
|
|
291
300
|
| `prompt` | `--prompt-file` of the role slot. Absent: the role's built-in template |
|
|
292
301
|
| `rules` | `--rules-file` of the role slot |
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
302
|
+
| `homePlugins` | Plugin folders of the Mason home, each a `--plugin-dir` |
|
|
303
|
+
| `appDeckProfile` | The app's deck profile for this role, as a path: `[<app dir>/].ai/profiles/<name>`. Needs the agent's `pluginsOut`. See the slots' § Agents/Plugins |
|
|
295
304
|
| `args` | Appended to the agent's own `args`, each an `--agent-arg` |
|
|
296
305
|
|
|
297
|
-
|
|
298
|
-
base plugins
|
|
306
|
+
An `appDeckProfile` is read in the app, not in the Mason home: the role gets the
|
|
307
|
+
app's base plugins, that profile's plugins and its `prompt.md`, as the app's
|
|
308
|
+
committed deck locks pin them. deck keeps profiles in `<app>/.ai/profiles/<name>`,
|
|
309
|
+
so the path has that shape; `<app dir>` is where the app's `.ai/` is in the
|
|
310
|
+
checkout (`.` by default, `apps/web` in a monorepo). It stays inside the checkout.
|
|
299
311
|
`mason doctor` checks those locks in the work line without the network.
|
|
300
312
|
|
|
301
313
|
Every tracker field lives in the `options` of its side. Host does not read them.
|
|
@@ -406,15 +418,15 @@ The config always names a producer (and a repair). Implementer can still run a
|
|
|
406
418
|
Task as its Gate sequence alone — that is the first judgement of the assembled
|
|
407
419
|
ticket, which makes nothing.
|
|
408
420
|
|
|
409
|
-
###
|
|
421
|
+
### Ticket fix
|
|
410
422
|
|
|
411
423
|
Spawned in the ticket workspace when a judgement of the whole refused it
|
|
412
424
|
(`--report`, `--report-from`). Same argv as a Builder otherwise. Host hands
|
|
413
|
-
Implementer `
|
|
425
|
+
Implementer `ticket.fix` as that pass's only agent: there is no first-pass
|
|
414
426
|
producer at this moment. The refusal it repairs may have come from an
|
|
415
|
-
Authority or from `
|
|
427
|
+
Authority or from `ticket.validate`.
|
|
416
428
|
|
|
417
|
-
###
|
|
429
|
+
### Ticket validate
|
|
418
430
|
|
|
419
431
|
Spawned in the ticket workspace after align, with or without an Authority —
|
|
420
432
|
`--intention`, `--id`, `--attempt`, `--context`. No `--report`: it is a
|
|
@@ -425,17 +437,18 @@ that completes cleanly but finds a problem is still a refusal.
|
|
|
425
437
|
|
|
426
438
|
### Gates
|
|
427
439
|
|
|
428
|
-
Four
|
|
429
|
-
`builder.repair
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
440
|
+
Four sequences: `builder.gates`, shared by `builder.producer` and
|
|
441
|
+
`builder.repair` (a repair is judged by exactly what judged the first try),
|
|
442
|
+
then one per assembly role — `ticket.fix`, `ticket.validate` — plus
|
|
443
|
+
`ticket.gates` for the one judgement-only pass with no producer of its own
|
|
444
|
+
(re-checking the assembled ticket; `ci-green` reads the checks the work line
|
|
445
|
+
ran on what the Publisher placed, so it belongs there). The assembly lists are
|
|
446
|
+
not shared with each other or with the builder, and no dispatch chooses between
|
|
447
|
+
two buckets: a check right for one pass is not necessarily right for another,
|
|
448
|
+
even within the same stage. `gates` written on `builder.producer` or
|
|
449
|
+
`builder.repair` is refused, and the message names `builder.gates`.
|
|
450
|
+
|
|
451
|
+
Implementer's Gates are told `--stage unit` or `--stage ticket` alongside
|
|
439
452
|
`--id`, `--attempt`, `--gate-id` and `--intention`.
|
|
440
453
|
Most have no use for it. `workspace-changed` does: "nothing changed" means
|
|
441
454
|
nothing was done in a unit, and is the correct outcome for an assembly that
|
|
@@ -451,11 +464,11 @@ from what another child left behind, so a slow producer cannot leave its
|
|
|
451
464
|
Gates a millisecond to answer in.
|
|
452
465
|
|
|
453
466
|
What bounds the unit is therefore what you wrote:
|
|
454
|
-
`builder.maxAttempts × (builder.producer.timeoutMs + Σ builder.
|
|
467
|
+
`builder.maxAttempts × (builder.producer.timeoutMs + Σ builder.gates timeouts)`
|
|
455
468
|
for a first pass, and the same shape with `repair` in place of `producer` once
|
|
456
469
|
a Gate has refused one.
|
|
457
470
|
A repair and an assembly fix each carry their own `timeoutMs`. An assembly fix
|
|
458
|
-
is bounded the same way from `
|
|
471
|
+
is bounded the same way from `ticket.maxAttempts` and `ticket.fix`.
|
|
459
472
|
|
|
460
473
|
```json
|
|
461
474
|
{
|
|
@@ -471,6 +484,60 @@ is bounded the same way from `assembly.maxAttempts` and `assembly.fix`.
|
|
|
471
484
|
}
|
|
472
485
|
```
|
|
473
486
|
|
|
487
|
+
### The e2e acceptance check
|
|
488
|
+
|
|
489
|
+
A Project proves each Feature by an acceptance suite, run once on the assembled Feature
|
|
490
|
+
line, when its config says so, by a pair:
|
|
491
|
+
|
|
492
|
+
```yaml
|
|
493
|
+
builder:
|
|
494
|
+
e2e: # makes the Ticket that holds the suite (label `e2e` on the tracker)
|
|
495
|
+
agent: claude
|
|
496
|
+
prompt: ./prompts/e2e.md # {{task}} is the scenarios; {{report}}, {{refused_by}}, {{refusal}} on a refused pass
|
|
497
|
+
timeoutMs: 1800000
|
|
498
|
+
gates: # form only: it compiles, one test per manifest id, no application file touched
|
|
499
|
+
gates:
|
|
500
|
+
- { id: e2e-form, argv: [...], timeoutMs: 120000 }
|
|
501
|
+
feature:
|
|
502
|
+
validate:
|
|
503
|
+
agent: false
|
|
504
|
+
gates:
|
|
505
|
+
defaultTimeoutMs: 120000
|
|
506
|
+
gates:
|
|
507
|
+
- id: e2e
|
|
508
|
+
skipWhen: skip-test-e2e # Host does not run this Gate for a Feature that carries the label
|
|
509
|
+
timeoutMs: 720000
|
|
510
|
+
argv:
|
|
511
|
+
- node
|
|
512
|
+
- ./node_modules/@bluewombat/slots/gates/e2e.mjs
|
|
513
|
+
- --up
|
|
514
|
+
- pnpm e2e:up
|
|
515
|
+
- --ready
|
|
516
|
+
- pnpm e2e:ready
|
|
517
|
+
- --run
|
|
518
|
+
- pnpm e2e
|
|
519
|
+
- --down
|
|
520
|
+
- pnpm e2e:down
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
`skipWhen` is the one condition Host reads for a Gate, on a Feature's Gates only
|
|
524
|
+
(`feature.validate`, `feature.gates`, `feature.fix`); its one value today is
|
|
525
|
+
`skip-test-e2e`, whatever the tracker calls that label. A Gate left out is not a Gate that
|
|
526
|
+
passed: the journal gets `gate-skipped` and the trace says "`e2e` not run: the Feature carries
|
|
527
|
+
skip-test-e2e". The Gate knows nothing of the skip and no environment variable carries it.
|
|
528
|
+
|
|
529
|
+
The tracker side: a Ticket carrying the e2e label (`e2eLabel` of the TicketTracker manager) is
|
|
530
|
+
made by `builder.e2e` and has no tests written first; a Feature carrying the skip label
|
|
531
|
+
(`skipTestE2eLabel` of the FeatureTracker manager) has that Ticket cancelled, with a line in
|
|
532
|
+
the trace. The label is read again until the Feature is submitted. A Project that declares the
|
|
533
|
+
check refuses a Feature that holds neither an e2e Ticket nor the skip label, or whose e2e
|
|
534
|
+
Ticket is not blocked by every other Ticket, as `invalid`, with the reason as a comment.
|
|
535
|
+
|
|
536
|
+
What the Gate proves is a red/green verdict; what the operator reads is its text in the
|
|
537
|
+
journal (`gate-finished`) — a red suite is also in the reason of the refusal the fix is
|
|
538
|
+
given and in the escalation. A passing suite's text is not shown on the pull request: see
|
|
539
|
+
RESERVATIONS I17.
|
|
540
|
+
|
|
474
541
|
### Describer
|
|
475
542
|
|
|
476
543
|
How a delivered ticket describes itself — the words the manager puts on the
|
|
@@ -543,25 +610,28 @@ it each stop `mason run` at boot, and `mason doctor` reports the same offline
|
|
|
543
610
|
and is the operator's directory.
|
|
544
611
|
|
|
545
612
|
```
|
|
546
|
-
assembled → align →
|
|
547
|
-
|
|
613
|
+
assembled → align → recheck (builder.gates again, on the aligned work)
|
|
614
|
+
conflict or failed → ticket.fix, then align again
|
|
615
|
+
→ ticket.validate (optional, local)
|
|
616
|
+
refused → ticket.fix, then align again, recheck, validate again
|
|
548
617
|
accepted → Publisher places the work → manager opens the Submission
|
|
549
618
|
→ submitted, and the run comes back to it on each poll
|
|
550
|
-
|
|
551
|
-
refused →
|
|
552
|
-
accepted → the
|
|
619
|
+
ticket.gates judge what was published
|
|
620
|
+
refused → ticket.fix, then the same Submission is offered again
|
|
621
|
+
accepted → has the line moved on since the align? yes: align again
|
|
622
|
+
no: the Authority folds it → done (a refused fold also aligns again)
|
|
553
623
|
```
|
|
554
624
|
|
|
555
|
-
Neither half judges: that is what `
|
|
556
|
-
and folds; a Gate says whether the fold may happen. `
|
|
625
|
+
Neither half judges: that is what `ticket.gates` are for. The Authority places
|
|
626
|
+
and folds; a Gate says whether the fold may happen. `ticket.validate`, when
|
|
557
627
|
declared, judges before either half — the same verdict has the same fate
|
|
558
628
|
whether or not there is an Authority to offer the work to afterwards. After the
|
|
559
629
|
Authority folds, Host brings its copy of the work line up to date with a
|
|
560
630
|
fast-forward — the work line moved where the Authority is, and the next
|
|
561
631
|
Isolation must start from that version, not from a copy left behind.
|
|
562
632
|
|
|
563
|
-
A refusal is not a second failure path: its report reaches `
|
|
564
|
-
exactly as a Gate's report reaches `builder.repair`. After `maxRefusals`
|
|
633
|
+
A refusal is not a second failure path: its report reaches `ticket.fix`
|
|
634
|
+
exactly as a Gate's report reaches `builder.repair`. After `ticket.maxRefusals`
|
|
565
635
|
refusals the ticket escalates with that report as its Trace, and `done` names
|
|
566
636
|
the Submission rather than a directory on the machine that ran.
|
|
567
637
|
|
package/SPECS.md
CHANGED
|
@@ -103,6 +103,7 @@ The FeatureTracker's `listen` delivers Features whose specification is final.
|
|
|
103
103
|
| `invalid` (new, or the intention changed) | emit `invalid` on the Feature, keyed by fingerprint |
|
|
104
104
|
| `invalid` (same fingerprint as recorded) | leave it; the tracker was told, and telling it is what delivered it again |
|
|
105
105
|
| `upsert` (absent or `waiting`) | `admitFeature`, emit `accepted`. Nothing is driven here: the sweep does it |
|
|
106
|
+
| `upsert` (`open` or `escalated`) whose `skip_e2e` differs from the ledger's | `setFeatureSkipE2e`; journal `skip-e2e`; no other change |
|
|
106
107
|
| `upsert` (any other state) | leave it; journal `skipped`. An echo of a Host report is this row |
|
|
107
108
|
| `upsert` + `signalsResume` while `escalated` | same as `resume` — a poll snapshot has no `labeled` edge |
|
|
108
109
|
| `resume` | resume the Feature, emit `resumed` |
|
|
@@ -139,6 +140,22 @@ advanced (`advanceProject`):
|
|
|
139
140
|
4. A Feature whose Tickets are all blocked escalates; one that cannot be
|
|
140
141
|
settled because the Authority lacks `settled` waits.
|
|
141
142
|
|
|
143
|
+
The e2e acceptance check, when the Project declares it (`builder.e2e`):
|
|
144
|
+
|
|
145
|
+
- A Feature does not open (and is reported `invalid`, once per reason) unless it holds exactly
|
|
146
|
+
one e2e Ticket (`TicketEntry.e2e`), blocked by every other live Ticket, or carries `skip_e2e`.
|
|
147
|
+
- A Ticket with `e2e` is admitted with `e2e` and `no_tests`: no tests are written first, and
|
|
148
|
+
Transformers makes it with `builder.e2e` (its command, its Gates, for the first pass, a refused
|
|
149
|
+
pass and the recheck) instead of the producer and the repair.
|
|
150
|
+
- A Feature with `skip_e2e` has its e2e Ticket cancelled by the sweep (journal
|
|
151
|
+
`e2e-ticket-cancelled`, a trace line), and Transformers leaves out, for that Feature, every
|
|
152
|
+
Gate of `feature.validate`, `feature.gates` and `feature.fix` that has `skipWhen:
|
|
153
|
+
skip-test-e2e`: the journal gets `gate-skipped` with the Gate id and the reason, and the trace
|
|
154
|
+
says "`<id>` not run: the Feature carries skip-test-e2e". The mark is read from the ledger as
|
|
155
|
+
each pass starts (`TransformerSlots.marks`).
|
|
156
|
+
- A Feature that is not marked and no longer holds a live e2e Ticket (it was cancelled while the
|
|
157
|
+
mark was on) escalates when it closes.
|
|
158
|
+
|
|
142
159
|
Host never drives an `escalated` unit: that state is frozen until a human `resume`.
|
|
143
160
|
|
|
144
161
|
Every Event Host emits is a write to the tracker, and a tracker that lists by
|
|
@@ -212,22 +229,22 @@ and the Gate sequence starts empty. Example Gates live in
|
|
|
212
229
|
`@bluewombat/slots/gates/` (`parent-clean`, `sensitive-path`,
|
|
213
230
|
`workspace-changed`, `ci-green`).
|
|
214
231
|
|
|
215
|
-
Four
|
|
216
|
-
`
|
|
217
|
-
pass with no producer of its own.
|
|
218
|
-
dispatched between buckets: a check that is right for one pass can be wrong
|
|
232
|
+
Four Gate sequences: `builder.gates`, shared by `builder.producer` and
|
|
233
|
+
`builder.repair`, then `ticket.fix.gates`, `ticket.validate.gates` and
|
|
234
|
+
`ticket.gates` for the one judgement-only pass with no producer of its own.
|
|
235
|
+
The assembly sequences are not shared and nothing is dispatched between buckets: a check that is right for one pass can be wrong
|
|
219
236
|
for another, even within the same stage — `workspace-changed` says an Attempt
|
|
220
237
|
that changed nothing did nothing, which is true of `builder.producer`/`repair`
|
|
221
|
-
and false of `
|
|
222
|
-
was left to do.
|
|
223
|
-
|
|
238
|
+
and false of `ticket.gates`'s pass, whose correct outcome is that nothing
|
|
239
|
+
was left to do. A repair Attempt is judged by `builder.gates`, the same list as the
|
|
240
|
+
producer's (Host hands Implementer that list as both `gates` and `repairGates`). The assembly's
|
|
224
241
|
judgement-only pass produces nothing unless something refused it first: it is
|
|
225
|
-
a judgement, and a refusal is `
|
|
226
|
-
things may judge it: `
|
|
227
|
-
was published) and, optionally, `
|
|
228
|
-
that runs before either, judged by its own `
|
|
229
|
-
`
|
|
230
|
-
`maxRefusals` budget.
|
|
242
|
+
a judgement, and a refusal is `ticket.fix`, not a second first-pass. Two
|
|
243
|
+
things may judge it: `ticket.gates` (and an Authority's own checks on what
|
|
244
|
+
was published) and, optionally, `ticket.validate` — a local, read-only pass
|
|
245
|
+
that runs before either, judged by its own `ticket.validate.gates`, not
|
|
246
|
+
`ticket.gates`. Both send a refusal to the same `fix` and spend the same
|
|
247
|
+
`ticket.maxRefusals` budget.
|
|
231
248
|
|
|
232
249
|
Every child of a Task carries its own ceiling and no other. Every `cmd` names
|
|
233
250
|
its own `timeoutMs`, and none is inherited from a neighbour — nothing else
|
|
@@ -236,7 +253,7 @@ or its sequence's `defaultTimeoutMs`, and a Gate that would end up with neither
|
|
|
236
253
|
is refused when the config loads. Nothing is derived from what another child
|
|
237
254
|
left behind, so what bounds a whole Task is arithmetic the Project can do:
|
|
238
255
|
the builder's `maxAttempts` times its producer's ceiling plus the sum of its Gates'.
|
|
239
|
-
An assembly fix is bounded the same way from `
|
|
256
|
+
An assembly fix is bounded the same way from `ticket.maxAttempts` and `ticket.fix`.
|
|
240
257
|
Root `timeoutMs` bounds what is outside a Task — the manager, the
|
|
241
258
|
isolations. Host has no flag named after a tracker (`--repo` is not a Host
|
|
242
259
|
flag): tracker fields come from that package.
|
|
@@ -406,6 +423,11 @@ positional: `release github:owner/name#19`.
|
|
|
406
423
|
open, escalated or submitted. A Feature blocked by another does not open
|
|
407
424
|
until it is `done`. A Feature whose Tickets are not all `task-ready` does
|
|
408
425
|
not open.
|
|
426
|
+
16b. With the e2e check declared: a Feature without an e2e Ticket nor `skip_e2e` does not open and
|
|
427
|
+
is reported `invalid`; with `skip_e2e` its e2e Ticket is cancelled and the e2e Gate is not run,
|
|
428
|
+
the journal and the trace saying so; with an e2e Ticket the line is judged once by the suite,
|
|
429
|
+
red goes to `feature.fix` and green lets the line be offered. A Project without `builder.e2e`
|
|
430
|
+
is unchanged.
|
|
409
431
|
17. After an escalation, a `probe` of `gone` cancels that Ticket (or Feature)
|
|
410
432
|
and the sweep goes on. `unavailable` leaves the
|
|
411
433
|
escalation. A listen that missed the source does not probe. GitHub: a 404
|
package/dist/cli.js
CHANGED
|
@@ -35,15 +35,15 @@ Shared flags: --config FILE --feature-tracker NAME --feature-tracker-option KEY=
|
|
|
35
35
|
--builder -- CMD --builder-timeout-ms N --builder-max-attempts N
|
|
36
36
|
--builder-repair -- CMD --builder-repair-timeout-ms N
|
|
37
37
|
--builder-gate ID -- CMD --builder-gate-timeout-ms N
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
--
|
|
38
|
+
--ticket-fix -- CMD --ticket-fix-timeout-ms N --ticket-max-attempts N
|
|
39
|
+
--ticket-validate -- CMD --ticket-validate-timeout-ms N
|
|
40
|
+
--ticket-gate ID -- CMD --ticket-gate-timeout-ms N
|
|
41
41
|
--timeout-ms N --poll-interval-ms N
|
|
42
42
|
|
|
43
43
|
A gate's --*-gate-timeout-ms comes before the --*-gate it bounds.
|
|
44
44
|
|
|
45
45
|
watch / status also take --json (snapshot as JSON). explain takes an optional role
|
|
46
|
-
(builder.producer, builder.repair,
|
|
46
|
+
(builder.producer, builder.repair, ticket.fix, ticket.validate, authority.describe) and --json.
|
|
47
47
|
|
|
48
48
|
log also takes --problems, --all, and --json (every kept line).
|
|
49
49
|
--tail N limits the text film (0 = no limit). With --json it names kept and shown when it drops lines.
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
export declare const ROLE_SCRIPTS: {
|
|
3
3
|
readonly "builder.producer": "builders/producer.mjs";
|
|
4
4
|
readonly "builder.repair": "builders/repair.mjs";
|
|
5
|
-
readonly "
|
|
6
|
-
readonly "
|
|
5
|
+
readonly "builder.tests": "builders/tests.mjs";
|
|
6
|
+
readonly "builder.e2e": "builders/e2e.mjs";
|
|
7
|
+
readonly "ticket.fix": "ticket/fix.mjs";
|
|
8
|
+
readonly "ticket.validate": "ticket/validate.mjs";
|
|
9
|
+
readonly "feature.fix": "feature/fix.mjs";
|
|
10
|
+
readonly "feature.validate": "feature/validate.mjs";
|
|
7
11
|
readonly "authority.describe": "messages/git-producer.mjs";
|
|
8
12
|
};
|
|
9
13
|
export type AgentRole = keyof typeof ROLE_SCRIPTS;
|
|
@@ -36,9 +40,26 @@ export declare function readAgents(value: unknown, configDir: string): Read<Map<
|
|
|
36
40
|
* mapping; its keys were checked by the caller.
|
|
37
41
|
*/
|
|
38
42
|
export declare function expandAgentPass(object: Record<string, unknown>, role: AgentRole, agents: Agents, configDir: string, key: string): Read<string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Where an app's deck profile is, from the path the config gives it: the app
|
|
45
|
+
* folder that holds `.ai/` (`.` for the checkout itself) and the profile's name.
|
|
46
|
+
* deck keeps its profiles in `<app>/.ai/profiles/<name>`, so that is the shape;
|
|
47
|
+
* the app folder is what a Project may place elsewhere in its checkout.
|
|
48
|
+
*
|
|
49
|
+
* The slot that builds the profile reads the path by the same rule
|
|
50
|
+
* (`parseAppDeckProfile` in `agent-plugins.mjs`); neither depends on the other.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseAppDeckProfile(given: string): {
|
|
53
|
+
ok: true;
|
|
54
|
+
root: string;
|
|
55
|
+
name: string;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
39
60
|
/** The agent and profile a pass's argv names, written short or not; for `doctor`. */
|
|
40
61
|
export declare function agentPartOf(argv: readonly string[]): {
|
|
41
|
-
|
|
62
|
+
appDeckProfile?: string;
|
|
42
63
|
pluginsOut?: string;
|
|
43
64
|
pluginDirs: string[];
|
|
44
65
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-shorthand.d.ts","sourceRoot":"","sources":["../../src/config/agent-shorthand.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent-shorthand.d.ts","sourceRoot":"","sources":["../../src/config/agent-shorthand.ts"],"names":[],"mappings":"AA4BA,mEAAmE;AACnE,eAAO,MAAM,YAAY;;;;;;;;;;CAUf,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,YAAY,CAAC;AAgBlD,uFAAuF;AACvF,eAAO,MAAM,eAAe,aAO1B,CAAC;AAEH,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEpD,KAAK,IAAI,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAwG1F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,IAAI,CAAC,MAAM,EAAE,CAAC,CAoFhB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GACZ;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAmB1E;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAoBA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
2
|
/**
|
|
3
3
|
* The short way to write an agent pass: which agent, which prompt, which
|
|
4
4
|
* plugins — and Host writes the argv a pass has always been.
|
|
@@ -8,13 +8,14 @@ import { resolve } from "node:path";
|
|
|
8
8
|
* transcripts: ./.mason/transcripts, pluginsOut: ./.mason/plugins }
|
|
9
9
|
* builder:
|
|
10
10
|
* producer: { agent: cursor, prompt: ./prompts/builder.md,
|
|
11
|
-
*
|
|
11
|
+
* homePlugins: [./ai-plugins/core/builder],
|
|
12
|
+
* appDeckProfile: .ai/profiles/builder, timeoutMs: … }
|
|
12
13
|
*
|
|
13
14
|
* expands to
|
|
14
15
|
*
|
|
15
16
|
* node <slots>/builders/producer.mjs --prompt-file <prompt> --
|
|
16
17
|
* node <slots>/agents/cursor.mjs --transcript-dir … --agent-home … --api-key-env …
|
|
17
|
-
* --plugin-dir <core> --profile builder --plugins-out …
|
|
18
|
+
* --plugin-dir <core> --app-deck-profile .ai/profiles/builder --plugins-out …
|
|
18
19
|
*
|
|
19
20
|
* The expansion is the whole ticket: nothing downstream knows a pass was
|
|
20
21
|
* written short, so `cmd` stays the one shape the rest of Host reads, and a
|
|
@@ -26,8 +27,12 @@ const SLOTS_PACKAGE = "node_modules/@bluewombat/slots";
|
|
|
26
27
|
export const ROLE_SCRIPTS = {
|
|
27
28
|
"builder.producer": "builders/producer.mjs",
|
|
28
29
|
"builder.repair": "builders/repair.mjs",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
30
|
+
"builder.tests": "builders/tests.mjs",
|
|
31
|
+
"builder.e2e": "builders/e2e.mjs",
|
|
32
|
+
"ticket.fix": "ticket/fix.mjs",
|
|
33
|
+
"ticket.validate": "ticket/validate.mjs",
|
|
34
|
+
"feature.fix": "feature/fix.mjs",
|
|
35
|
+
"feature.validate": "feature/validate.mjs",
|
|
31
36
|
"authority.describe": "messages/git-producer.mjs",
|
|
32
37
|
};
|
|
33
38
|
const ADAPTERS = new Set(["cursor", "claude"]);
|
|
@@ -44,7 +49,14 @@ const AGENT_KEYS = new Set([
|
|
|
44
49
|
"args",
|
|
45
50
|
]);
|
|
46
51
|
/** The keys a pass written short may carry, besides its own (`timeoutMs`, `gates`). */
|
|
47
|
-
export const SHORT_PASS_KEYS = new Set([
|
|
52
|
+
export const SHORT_PASS_KEYS = new Set([
|
|
53
|
+
"agent",
|
|
54
|
+
"prompt",
|
|
55
|
+
"rules",
|
|
56
|
+
"homePlugins",
|
|
57
|
+
"appDeckProfile",
|
|
58
|
+
"args",
|
|
59
|
+
]);
|
|
48
60
|
export function readAgents(value, configDir) {
|
|
49
61
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
50
62
|
return { ok: false, reason: 'Config "agents" must be a YAML mapping of agent names.' };
|
|
@@ -176,19 +188,25 @@ export function expandAgentPass(object, role, agents, configDir, key) {
|
|
|
176
188
|
if (rules.ok === false) {
|
|
177
189
|
return rules;
|
|
178
190
|
}
|
|
179
|
-
const plugins = stringList(object.
|
|
191
|
+
const plugins = stringList(object.homePlugins, `${key}.homePlugins`);
|
|
180
192
|
if (plugins.ok === false) {
|
|
181
193
|
return plugins;
|
|
182
194
|
}
|
|
183
|
-
const profile = object.
|
|
184
|
-
if (profile !== undefined
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
reason: `Config "${key}"
|
|
191
|
-
}
|
|
195
|
+
const profile = object.appDeckProfile;
|
|
196
|
+
if (profile !== undefined) {
|
|
197
|
+
if (typeof profile !== "string") {
|
|
198
|
+
return { ok: false, reason: `Config "${key}.appDeckProfile" must be a path.` };
|
|
199
|
+
}
|
|
200
|
+
const located = parseAppDeckProfile(profile);
|
|
201
|
+
if (!located.ok) {
|
|
202
|
+
return { ok: false, reason: `Config "${key}.appDeckProfile": ${located.reason}` };
|
|
203
|
+
}
|
|
204
|
+
if (agent.pluginsOut === undefined) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
reason: `Config "${key}" names an app deck profile, so "agents.${name}.pluginsOut" must say where it is built.`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
192
210
|
}
|
|
193
211
|
const args = stringList(object.args, `${key}.args`);
|
|
194
212
|
if (args.ok === false) {
|
|
@@ -224,13 +242,40 @@ export function expandAgentPass(object, role, agents, configDir, key) {
|
|
|
224
242
|
argv.push("--plugin-dir", resolve(configDir, dir));
|
|
225
243
|
}
|
|
226
244
|
if (profile !== undefined && agent.pluginsOut !== undefined) {
|
|
227
|
-
argv.push("--profile", profile, "--plugins-out", agent.pluginsOut);
|
|
245
|
+
argv.push("--app-deck-profile", profile, "--plugins-out", agent.pluginsOut);
|
|
228
246
|
}
|
|
229
247
|
for (const arg of [...agent.args, ...args.value]) {
|
|
230
248
|
argv.push("--agent-arg", arg);
|
|
231
249
|
}
|
|
232
250
|
return { ok: true, value: argv };
|
|
233
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Where an app's deck profile is, from the path the config gives it: the app
|
|
254
|
+
* folder that holds `.ai/` (`.` for the checkout itself) and the profile's name.
|
|
255
|
+
* deck keeps its profiles in `<app>/.ai/profiles/<name>`, so that is the shape;
|
|
256
|
+
* the app folder is what a Project may place elsewhere in its checkout.
|
|
257
|
+
*
|
|
258
|
+
* The slot that builds the profile reads the path by the same rule
|
|
259
|
+
* (`parseAppDeckProfile` in `agent-plugins.mjs`); neither depends on the other.
|
|
260
|
+
*/
|
|
261
|
+
export function parseAppDeckProfile(given) {
|
|
262
|
+
const matched = /^(?:(.*?)\/)?\.ai\/profiles\/([^/]+)\/?$/.exec(given.trim());
|
|
263
|
+
const name = matched?.[2];
|
|
264
|
+
if (matched === null ||
|
|
265
|
+
name === undefined ||
|
|
266
|
+
!/^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(name) ||
|
|
267
|
+
name === "current") {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
reason: `"${given}" is not a deck profile path: [<app dir>/].ai/profiles/<name>.`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const root = matched[1] ?? ".";
|
|
274
|
+
if (isAbsolute(root) || root.split("/").includes("..")) {
|
|
275
|
+
return { ok: false, reason: `"${given}" must stay inside the checkout.` };
|
|
276
|
+
}
|
|
277
|
+
return { ok: true, root, name };
|
|
278
|
+
}
|
|
234
279
|
/** The agent and profile a pass's argv names, written short or not; for `doctor`. */
|
|
235
280
|
export function agentPartOf(argv) {
|
|
236
281
|
const split = argv.indexOf("--");
|
|
@@ -243,8 +288,8 @@ export function agentPartOf(argv) {
|
|
|
243
288
|
if (value === undefined) {
|
|
244
289
|
return;
|
|
245
290
|
}
|
|
246
|
-
if (token === "--profile") {
|
|
247
|
-
found.
|
|
291
|
+
if (token === "--app-deck-profile") {
|
|
292
|
+
found.appDeckProfile = value;
|
|
248
293
|
}
|
|
249
294
|
else if (token === "--plugins-out") {
|
|
250
295
|
found.pluginsOut = value;
|