omp-conductor 0.19.6 → 0.20.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.
Files changed (71) hide show
  1. package/REFERENCE.md +27 -2
  2. package/agents/to-spec.md +76 -9
  3. package/package.json +1 -1
  4. package/schema/config.schema.json +4 -0
  5. package/src/arm-challenge.ts +204 -85
  6. package/src/ask.ts +130 -615
  7. package/src/board.ts +7 -1
  8. package/src/brief-upgrade.ts +24 -0
  9. package/src/briefs/console.md +253 -0
  10. package/src/briefs/correction.md +203 -0
  11. package/src/briefs/orchestrator.md +167 -97
  12. package/src/briefs/policy.md +19 -16
  13. package/src/briefs/to-spec.md +76 -9
  14. package/src/briefs/worker.md +50 -16
  15. package/src/cli.ts +4 -0
  16. package/src/command-manifest.ts +54 -8
  17. package/src/commands/arm.ts +113 -49
  18. package/src/commands/console.ts +70 -0
  19. package/src/commands/context.ts +2 -0
  20. package/src/commands/epic.ts +132 -0
  21. package/src/commands/extend.ts +9 -1
  22. package/src/commands/intake.ts +44 -14
  23. package/src/commands/stats.ts +19 -4
  24. package/src/commands/worker.ts +9 -1
  25. package/src/config-schema.ts +13 -0
  26. package/src/config.ts +27 -0
  27. package/src/daemon/ack.ts +159 -0
  28. package/src/daemon/admission-pass.ts +135 -0
  29. package/src/daemon/brief.ts +461 -0
  30. package/src/daemon/deps.ts +539 -0
  31. package/src/daemon/dispatch.ts +1779 -0
  32. package/src/daemon/drain.ts +185 -0
  33. package/src/daemon/groom-pass.ts +412 -0
  34. package/src/daemon/http.ts +417 -0
  35. package/src/daemon/integrity.ts +108 -0
  36. package/src/daemon/panes.ts +180 -0
  37. package/src/daemon/review.ts +1888 -0
  38. package/src/daemon/runtime.ts +736 -0
  39. package/src/daemon/settle-pass.ts +589 -0
  40. package/src/daemon/supervision.ts +438 -0
  41. package/src/daemon/tick.ts +968 -0
  42. package/src/daemon/views.ts +751 -0
  43. package/src/daemon.ts +105 -7832
  44. package/src/dashboard/app.js +58 -0
  45. package/src/dashboard/controls.ts +22 -3
  46. package/src/dashboard/server.ts +4 -0
  47. package/src/diff-flags.ts +24 -3
  48. package/src/doctor.ts +17 -12
  49. package/src/escalate.ts +39 -21
  50. package/src/failure-class.ts +75 -1
  51. package/src/fleet.ts +1218 -304
  52. package/src/groom.ts +461 -0
  53. package/src/http-token.ts +142 -0
  54. package/src/knowledge.ts +229 -0
  55. package/src/mining.ts +316 -0
  56. package/src/orchestrator-tick.ts +428 -1681
  57. package/src/ready-gate.ts +267 -0
  58. package/src/settlement.ts +72 -6
  59. package/src/setup-host.ts +32 -9
  60. package/src/setup-wizard.ts +55 -7
  61. package/src/setup.ts +229 -3
  62. package/src/stats.ts +257 -2
  63. package/src/status-render.ts +158 -7
  64. package/src/store.ts +646 -26
  65. package/src/to-spec.ts +194 -21
  66. package/src/tracker/github.ts +50 -0
  67. package/src/types.ts +435 -15
  68. package/src/verbs/protocol.ts +28 -0
  69. package/src/verbs/server.ts +384 -12
  70. package/src/wake.ts +19 -2
  71. package/src/worker.ts +456 -1
package/REFERENCE.md CHANGED
@@ -848,6 +848,22 @@ the queue label is refused with `file-lane-unparseable` and an actionable
848
848
  message, because promoting beside overlapping work on a section that plainly
849
849
  tried to declare is the exact gap the interlock exists to close.
850
850
 
851
+ Since #1036, a promotion also checks a contract. When the issue carries a
852
+ durable `promotable` grooming verdict (the store's grooming row, written by
853
+ the to-spec batch), adding the queue label first recovers that verdict's strict
854
+ payload and requires its `PROMOTABLE` result, then compares the lane admission
855
+ would enforce against the verdict's `fileLane` as deduplicated,
856
+ order-independent path sets — exact in both directions, so a missing path
857
+ refuses exactly like an extra one, and an order-only difference promotes. Any
858
+ disagreement refuses with `promotion-brief-mismatch`, naming both sets; so
859
+ does evidence behind a `promotable` row that does not recover as a strict
860
+ PROMOTABLE result, and an issue unreadable at promotion time behind such a
861
+ row. The durable verdict is what made the issue dispatchable, so the gate
862
+ fails closed before any tracker mutation or daemon wake — but it never edits
863
+ the issue itself: the remediation is applying the verdict's `proposedBrief`
864
+ to the issue (its `## Exact write lane` included), then adding the label
865
+ again. An issue without a `promotable` row promotes exactly as before.
866
+
851
867
  #### What status shows
852
868
 
853
869
  `status` renders the two lifecycle kinds as two blocks, because "a branch is
@@ -2795,7 +2811,9 @@ omp-conductor drain status [--project NAME]
2795
2811
  omp-conductor drain cancel [--project NAME]
2796
2812
  omp-conductor stop [--pane] [--project NAME]
2797
2813
  omp-conductor arm [--project NAME]
2814
+ omp-conductor arm --reply TEXT [--project NAME]
2798
2815
  omp-conductor disarm [--project NAME]
2816
+ omp-conductor console [--project NAME]
2799
2817
  omp-conductor tail <issue> [--project NAME]
2800
2818
  omp-conductor extend <issue> --turns N [--project NAME]
2801
2819
  omp-conductor worker pause <issue> [--project NAME]
@@ -2813,6 +2831,9 @@ omp-conductor decision withdraw <id> [--reason TEXT] [--project NAME]
2813
2831
  omp-conductor decision list [--project NAME] [--json]
2814
2832
  omp-conductor watch add --note TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]
2815
2833
  omp-conductor watch list [--project NAME] [--json]
2834
+ omp-conductor epic approve <issue> [--project NAME]
2835
+ omp-conductor epic revoke <issue> [--project NAME]
2836
+ omp-conductor epic list [--project NAME] [--json]
2816
2837
  omp-conductor daemon [--once] [--port N] [--project NAME]
2817
2838
  omp-conductor resume [--project NAME]
2818
2839
  omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
@@ -2836,8 +2857,9 @@ omp-conductor help
2836
2857
  | `hold [--keep-ticks] [--project NAME]` | fleet | Soft stop: pause claiming **and** disarm ticks. Daemon and pane stay up. Prefer this when the intent is "stop the conductor" without killing processes. `--keep-ticks` pauses claiming but leaves the arm marker, so the heartbeat keeps reporting and `resume` alone restores the fleet — no fresh arm challenge. See [Stop the conductor](README.md#stop-the-conductor-hold--stop). |
2837
2858
  | `drain start --until ISO\|DURATION [--reason TEXT]` / `drain status` / `drain cancel` | project | Start, inspect, or cancel the project's self-expiring admission fence (#484): a durable, bounded alternative to queue-label churn before a release. `start` writes the project's drain record through the landed `createDrain` — new claims pause while active runs settle, and admission resumes automatically at the absolute deadline even if the orchestrator dies. A successful mediated `conductor_release` also clears the drain once the terminal release act completes — version-bump preparation stays latched until the tag is actually cut — so the release window ends with the release itself rather than latching until the deadline (#791). `--until` takes an ISO instant or a relative duration (`90s`, `45m`, `2h`, `1d`) that must be bounded and in the future; a missing, unparseable, unbounded, or past expiry exits `2` before any state changes. `--reason` (1–500 characters) is persisted on the record. `status` reports inactive, or the active drain's creation time, absolute expiry, reason, and remaining active runs from the structured status snapshot. `cancel` removes only the named project's drain and is idempotent. The drain never touches the pause sentinel, the arm marker, or any queue label — it is a file record, not a hold. |
2838
2859
  | `stop [--pane] [--project NAME]` | fleet | Stop the conductor: pause claiming, disarm ticks, then stop the dispatch daemon (systemctl-aware). Pane stays up unless `--pane` is passed. `stop --pane` also pins herdr-conductor recovery off for the conductor agent only — it does **not** stop `herdr-fleet.service` or any other herdr session. Fail-closed: exits nonzero unless the agent is proven gone. To bounce the daemon without stopping the fleet, use `restart`. |
2839
- | `arm [--project NAME]` | fleet | Proof-gated: send a Telegram challenge and write this project's arm marker only after the orchestrator's inbound adapter has acknowledged your reply in conductor's own state — no transcript is read, so where (or whether) a session file lives does not matter (#614). The challenge names the project, so a host running two fleets is not ambiguous. Never auto-armed by `resume` / `hold`. |
2860
+ | `arm [--project NAME]` / `arm --reply TEXT [--project NAME]` | fleet | Two mechanical halves, because nothing waits inside a session any more: the operator's reply lands in the console, which runs no tick extension, so the in-session acknowledgement this replaces could never be satisfied. `arm` files a challenge, sends it to the owner, and returns immediately — **nothing is armed yet** — printing the challenge id, how long it stays valid, the markers a reply will write, and the exact follow-up command. `arm --reply "<the operator's message, verbatim>"` verifies that message against the recorded challenge, writes the arm marker for exactly the projects that challenge named, and settles the challenge so the same code can never arm anything twice. Still proof-gated on conductor's own state — no transcript is read, so where (or whether) a session file lives does not matter (#614). A bare or empty `--reply` is refused rather than silently sending a second challenge in place of a verification. One ceremony covers the whole fleet (#991), and the challenge names its projects, so a host running two fleets is not ambiguous; `--project X` is still one project, one challenge, one marker. When the plumbing verdict is `claim-only` no challenge is sent and `arm` arms directly, naming that proof instead. Never auto-armed by `resume` / `hold`. |
2840
2861
  | `disarm [--project NAME]` | fleet | Remove this project's arm marker so its ticks skip; another project's ticks keep running. Also clears a pre-per-project shared `armed` marker while that marker is still what holds this fleet's gate open — otherwise the disarm would not disarm. Processes untouched. |
2862
+ | `console [--project NAME]` | project | Provision the operator console's workspace and print how to launch it: a dedicated cwd at `<state dir>/console/<project>`, this project's rendered console brief as `CONSOLE.md`, and an `AGENTS.md` symlink beside it so a session started there loads that brief. Idempotent — an already-current brief and a correct link are reported and left alone, and a *regular* `AGENTS.md` an operator wrote is never overwritten. It launches nothing: the printed pane command (`cd <cwd> && OMP_TELEGRAM_DM_OWNER=1 omp`, without the env prefix on a host configuring more than one project, which names the others and why) is what `herdr/bin/recover.sh` restores with the fleet, and what you run by hand otherwise. The console cwd deliberately holds no `.conductor-tick.json`, so the heartbeat extension cannot activate in it. |
2841
2863
  | `tail <issue>` | project | Follow the newest run for that issue: the worker's assistant text as `assistant: …` and each tool it calls as `tool: <name>`, printed as they land. Workers are omp sessions inside the daemon rather than terminals, so this is the only way to watch one live — a herdr pane running it becomes an observation window. Starts from the top of the transcript, not the end, so attaching to a run that is already ten turns in shows those ten turns. Exits `1` with `no run recorded for #N` when the issue has never been dispatched, or `no transcript yet (state: …)` when the attempt has not opened one. Otherwise it runs until `Ctrl-C`, or until the run has finished and its transcript has been silent for five seconds, and prints `run ended: <state>`. |
2842
2864
  | `extend <issue> --turns N [--project NAME]` | project | Raise a live worker's effective turn ceiling through its owning daemon without restarting its session. If the latest run is failed, killed, orphaned, or blocked and has no live controller, store a one-shot ceiling for that issue's next claimed attempt instead. A next-attempt value must exceed the project base, every extension must stay at or below `workerMaxTurnsCeiling`, and live extensions remain monotonic. The pending value appears in `status`, is recorded in `ledger`, and is consumed atomically by one claim. |
2843
2865
  | `worker pause <issue>` / `worker resume <issue>` | project | Cooperatively park one live worker without changing its run state or lane. Pause aborts the active turn to harness idle and freezes the remaining wall-clock budget; resume continues the same session with a prompt to re-check its last action before repeating it. This is separate from fleet-level `hold`, which refuses new claims and work-starting mutations while allowing pre-pause completion work and releases. |
@@ -2853,6 +2875,9 @@ omp-conductor help
2853
2875
  | `decision list [--json]` | project | Open questions, oldest first: id, age, what each blocks, whether its condition is met, and the question. `--json` emits `{ project, decisions }`; the empty state is an empty array. Prints `no open decisions` in text mode when there are none. Watches are not listed here — `watch list` shows those. |
2854
2876
  | `watch add --note TEXT [--blocks TEXT] [--resolves-when COND]` | project | Record a condition or carry note the orchestrator set for itself, with no human in the loop (#459). `--resolves-when` attaches a machine-checkable condition the daemon checks for you; a met watch wakes the next tick exactly as a met question does. Renders under its own "Watches" heading, is never counted in `decisions N open`, and never expires while it is *waiting* — once a condition fires, the ordinary seven-day window runs from that instant (#966). |
2855
2877
  | `watch list [--json]` | project | Open watches, oldest first: id, age, what each blocks, whether its condition is met, and the note. `--json` emits `{ project, watches }`; the empty state is an empty array. Prints `no watches` in text mode when there are none. |
2878
+ | `epic approve <issue>` | project | Record standing consent to an epic's scope (#1041): brief-complete children of it pass the promotion ready gate and are queued without a fresh operator decision. Its own durable fact, not a resolved decision row — an approval never expires, and the gate reads a boolean instead of parsing an answer somebody wrote in prose. Idempotent and first-write-wins: re-approving keeps the original timestamp and prints that nothing changed, so a second approve is never mistaken for one that failed to stick. Whether the number really names an epic is not checked here — parents and children are native GitHub sub-issues, which the daemon resolves at promotion time. |
2879
+ | `epic revoke <issue>` | project | Withdraw an epic's approval, so its children need a decision again. A no-op on an epic that was never approved, and it exits `0`: `revoke` states what is true afterwards, exactly as `drain cancel` does. |
2880
+ | `epic list [--json]` | project | Approved epics, issue-ascending: issue, when it was approved, and who approved it. `--json` emits `{ project, epics }`; the empty state is an empty array. Prints `no approved epics` in text mode when there are none. |
2856
2881
  | `daemon` | host | Run the loop in the **foreground**, ticking every 5 minutes and serving `/healthz`. Admitted workers run in a tracked background pool, so settlement and capacity checks remain periodic while they work; shutdown drains the pool before closing the store. This is what `start` launches and what a systemd unit should call. |
2857
2882
  | `daemon --once` | host | Run a single tick, wait for workers admitted by that tick, and exit. No HTTP server or pidfile — a drill must not register itself as the daemon, or the next reader believes it and the real daemon's in-flight runs get reconciled as orphans. |
2858
2883
  | `--port N` | — | Accepted by `start`, `restart` and `daemon`. Both `--port 9000` and `--port=9000` work; missing or out of range exits `2` rather than falling back to the default, because probing the wrong endpoint is worse than a hard failure. |
@@ -3099,7 +3124,7 @@ daemon, across a process boundary, not in a prompt the model can rewrite.
3099
3124
  | `conductor_pr_status` | worker or orchestrator | Read-only — nothing to gate. A worker reads only its own run's PR; an orchestrator may name any syntactically valid PR URL, open, merged, or closed, and gets its live state and head (checks are reported when available; a merged or closed PR reports its state instead of an `expected OPEN` refusal). |
3100
3125
  | `conductor_pr_update_branch` | orchestrator, or the worker owning the run | The PR belongs to this project and is open. A worker may only name its own run's PR. |
3101
3126
  | `conductor_pr_merge` | **orchestrator only** | Ordinarily, `authority.merge` equals the caller. A hand-edited `recoveryMerges` entry may instead authorize one exact unrecorded PR/head/reason while held. In both paths, while the project has an [active release composition](#the-active-release-composition-850), the PR must be named in it or carry an explicit operator override; then `headSha` equals the live head *at execution time*; checks are green at that same SHA; no [review evidence](#the-exact-head-review-gate-888) stands at that head without a recorded `conductor_pr_review_clear`; the project route and migration chain are valid; the project's single merge slot is free. |
3102
- | `conductor_label` | **orchestrator only** | The label is in the project's own vocabulary. Lifecycle labels stay the daemon's. Adding the queue label echoes the parsed [file lane](#the-file-lane-declaration), or refuses with `file-lane-unparseable` when a clearly delimited write-lane section parsed nothing — it never claims fail-open beside a declaration that was actually attempted. |
3127
+ | `conductor_label` | **orchestrator only** | The label is in the project's own vocabulary. Lifecycle labels stay the daemon's. Adding the queue label echoes the parsed [file lane](#the-file-lane-declaration), or refuses with `file-lane-unparseable` when a clearly delimited write-lane section parsed nothing — it never claims fail-open beside a declaration that was actually attempted. When the issue carries a durable `promotable` grooming verdict, adding the queue label also verifies the current brief against that verdict before any mutation: a brief that disagrees with the verdict's `fileLane` (or evidence that no longer recovers as a strict PROMOTABLE result) refuses with `promotion-brief-mismatch` — apply the verdict's `proposedBrief`, then re-add ([the file-lane declaration](#the-file-lane-declaration)). |
3103
3128
  | `conductor_release` | **orchestrator only** | `authority.release` equals the caller; the per-shape grant permits it; the artefact or environment was declared; the release preconditions hold; the `reason` is in the closed enum. `version-bump-pr` creates or re-validates one deterministic version-only PR and, on a later call, merges only its exact green head through the project's single merge slot. |
3104
3129
  | `conductor_install` | **orchestrator only** | Gated like a release act: the `install` shape defaults to `human` and a grant is what moves it. The daemon refuses a version npm does not expose with a full `gitHead`, refuses while another install is still in flight, and otherwise starts a detached transient unit that pauses, drains, installs the CLI/omp plugin/Herdr plugin and reloads — outside this session and the daemon. The unit never declares its own success; the first tick after the restart verifies and reports through the durable outbox. |
3105
3130
 
package/agents/to-spec.md CHANGED
@@ -29,15 +29,70 @@ Exactly one of these five strings, nothing else:
29
29
  - `ALREADY DONE` — the work already exists in the source (a later epic retired
30
30
  the issue's premise counts as done; prove it with the symbol/file, never the
31
31
  title).
32
- - `PROMOTABLE` — well-specified, fits one worker budget, and the acceptance
33
- criteria are checkable; carries the proposed brief.
34
- - `NEEDS DECOMPOSITION` — the plan is real but too big for one budget; say what
35
- slices it splits into and why each is a separate slice.
32
+ - `PROMOTABLE` — well-specified, the acceptance criteria are checkable, and it
33
+ fits one worker budget *with the sizing evidence below to prove it*; carries
34
+ the proposed brief.
35
+ - `NEEDS DECOMPOSITION` the plan is real but does not fit one budget; carries
36
+ the ordered children it splits into and why each is its own slice.
36
37
  - `BLOCKED` — a named open prerequisite, lane, or credential gap stands in the
37
38
  way.
38
39
  - `NEEDS PRODUCT DECISION` — the issue cannot proceed until a human decides
39
40
  product shape, slice order, or scope; state the one question that unblocks it.
40
41
 
42
+ ## Sizing: the one-budget claim needs a falsifier
43
+
44
+ `PROMOTABLE` asserts that one worker finishes this slice inside one attempt —
45
+ a single session under the fleet's configured turn ceiling and its 90-minute
46
+ wall clock, with continuations held in reserve for operational restarts and
47
+ not for the rest of the work. That assertion is worth nothing on its own.
48
+ Issue #1035 was groomed `PROMOTABLE` with nine acceptance criteria across four
49
+ files; attempt 1 died at 181 of its 180 turns with the feature half-built. The
50
+ verdict had claimed one-budget fit. What it never carried was evidence that
51
+ could have falsified the claim.
52
+
53
+ `sizingEvidence` is that evidence, read out of the source you just read:
54
+
55
+ 1. **The distinct behaviours.** Name each behaviour or state transition the
56
+ slice introduces — not each acceptance-criterion bullet. Two bullets about
57
+ one transition are one behaviour; one bullet hiding "create it, reconcile
58
+ it, clean it up" is three.
59
+ 2. **What each behaviour costs.** For every behaviour, the production module
60
+ that implements it and the focused test surface that proves it. A behaviour
61
+ whose proof needs a new integration harness costs far more than one an
62
+ existing unit test already reaches.
63
+ 3. **The sequencing between them.** Which behaviours write the same file, and
64
+ so cannot be discovered, implemented and re-proved independently inside one
65
+ session.
66
+ 4. **The conclusion.** Why that inventory fits one attempt — or, when it does
67
+ not, `NEEDS DECOMPOSITION`.
68
+
69
+ **The multi-lifecycle shape is the one to refuse.** A candidate bundling core
70
+ topology (creating the thing), lifecycle reconciliation (duplicate, live, dead
71
+ and orphaned states converging) and daemon/restart integration (the same
72
+ behaviour surviving a process restart) is three independently provable slices.
73
+ That candidate is `NEEDS DECOMPOSITION` unless your verdict names the existing
74
+ deep seam that already makes them one bounded change — a single reconciler
75
+ both paths funnel through, say — by file and symbol. "They are related" is not
76
+ a seam.
77
+
78
+ **A small file count is not a small slice.** Four files can be 28,812 lines
79
+ (that was #1035's own inventory) when they are high-fanout lifecycle modules
80
+ with large integration suites. Weigh the fanout of what you touch and the size
81
+ of the suites you must keep green, never the length of the write lane.
82
+
83
+ No threshold decides this — not a line count, not a criterion count, not a
84
+ file count. A threshold would be gamed by rewriting prose, and the sizes that
85
+ matter are structural. The judgement is yours; the evidence is what makes it
86
+ reviewable.
87
+
88
+ **When it does not fit**, `decomposition` is a filing instruction and not a
89
+ regret: the children in the order they must land, each with the title it would
90
+ be filed under, the exact write lane it owns, what it waits on, the silent
91
+ fake it invites and the commands that prove it. Two children writing the same
92
+ core file must be serialised — the later one names the earlier in its
93
+ dependencies — because two workers in one module is the collision the file
94
+ lane exists to prevent.
95
+
41
96
  ## The return contract
42
97
 
43
98
  Answer in **one fenced JSON block, nothing else after it**. Every field is
@@ -46,10 +101,12 @@ required and no extra keys are accepted:
46
101
  - `verdict` — one of the five strings above.
47
102
  - `routing` — exactly one `owner/repo`, or `"MULTI"`.
48
103
  - `routingSplit` — required iff `routing` is `"MULTI"`: what each slice goes to.
49
- - `source` — `{ name, ref, freshAt }`: the authoritative source you read, the
50
- exact ref, and `freshAt` = epoch milliseconds when you actually observed it.
51
- Conductor refuses results whose source is older than 24 hours or missing
52
- name/ref/freshAt an unsourced verdict is not grooming, it is prose.
104
+ - `source` — `{ name, ref }`: the authoritative source you read and the exact
105
+ ref you read it at. Conductor refuses a result that does not name both — an
106
+ unsourced verdict is not grooming, it is prose. Do not send a `freshAt`:
107
+ conductor stamps the observation time itself from the batch window and
108
+ refuses a batch whose window exceeds 24 hours, so inventing a timestamp is
109
+ neither required nor read (#1000).
53
110
  - `evidence` — the files/symbols that prove the verdict. Required for
54
111
  `ALREADY DONE`: name the symbol/file that already does the work, never a
55
112
  title match. Welcome on every other verdict.
@@ -69,6 +126,16 @@ required and no extra keys are accepted:
69
126
  - `fileLane` — the files and directories this slice writes.
70
127
  - `dependencies` — open prerequisite issue numbers, each a bare number
71
128
  (`875`) or a string (`"875"`); `[]` when none.
129
+ - `sizingEvidence` — required iff `verdict` is `PROMOTABLE`: the one-budget
130
+ analysis from the sizing section above — the distinct behaviours, the
131
+ production module and focused test surface each one needs, the shared-file
132
+ sequencing between them, and why that fits one attempt.
133
+ - `decomposition` — required iff `verdict` is `NEEDS DECOMPOSITION`, and
134
+ accepted on no other verdict: the ordered children, first to last. Each is
135
+ an object with `title`, `writeLane` (its exact write lane), `dependsOn` (an
136
+ earlier child's title or an existing issue number, `[]` for the first),
137
+ `likelySilentFake` and `proofCommands`. Children sharing a write-lane path
138
+ must serialise through `dependsOn`.
72
139
  - `proposedBrief` — required iff `verdict` is `PROMOTABLE`: the brief a worker
73
140
  would be dispatched with, including the silent fake and the proof commands.
74
141
  - `reasonNotToPromote` — required for every other verdict: why this must not
@@ -78,7 +145,7 @@ required and no extra keys are accepted:
78
145
 
79
146
  - **Prose is not evidence.** A verdict without the source-backed contract is
80
147
  refused as malformed: every field above is required, and `source` must name
81
- the ref you read and when.
148
+ the ref you actually read.
82
149
  - **Stale source reads like good source.** Judge the candidate against the
83
150
  stated ref as it is now; a verdict drawn from memory of a different clone is
84
151
  stale and will be refused.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.19.6",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
@@ -184,6 +184,10 @@
184
184
  }
185
185
  ]
186
186
  },
187
+ "groomRole": {
188
+ "type": "string",
189
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
190
+ },
187
191
  "stateLabels": {
188
192
  "type": "object",
189
193
  "properties": {