the-high-council 0.7.9 → 0.8.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 +16 -4
- package/chains/cheap-7-v2.json +1 -0
- package/chains/local-ollama.json +1 -0
- package/chains/plan-daily-7.json +1 -0
- package/chains/plan-highest-7.json +1 -0
- package/chains/plan-premium-7.json +1 -0
- package/chains/security-review-only.json +29 -0
- package/package.json +1 -1
- package/schemas/report-v1.json +135 -0
- package/src/cache-integrity.js +5 -1
- package/src/chain-lint.js +10 -0
- package/src/chain.js +54 -11
- package/src/cli.js +245 -7
- package/src/criteria-ledger.js +38 -0
- package/src/criteria-lints.js +51 -0
- package/src/criteria-lock.js +80 -0
- package/src/decision-records.js +54 -0
- package/src/denied-models.js +9 -1
- package/src/dry-run.js +86 -0
- package/src/handoff-from-run.js +80 -0
- package/src/mcp/server.js +8 -4
- package/src/providers.js +10 -0
- package/src/report-shape.js +17 -0
- package/src/run-status.js +35 -1
- package/src/scope-freeze.js +46 -3
- package/src/spend.js +11 -2
- package/src/ui/app.js +12 -3
- package/src/ui/guard.js +94 -0
- package/src/ui/server.js +22 -11
- package/src/ui/style.css +2 -0
package/README.md
CHANGED
|
@@ -148,7 +148,10 @@ A run moves through fixed stages. Which stages fire depends on the chain you pic
|
|
|
148
148
|
7. **Panel review** - every critic independently grades the draft against the acceptance criteria,
|
|
149
149
|
blind. Not unanimous? It revises against the union of every objection and the panel reviews
|
|
150
150
|
again, up to the chain's round cap.
|
|
151
|
-
8. **Handoff** - a `HANDOFF.md` written for whoever executes the result.
|
|
151
|
+
8. **Handoff** - a `HANDOFF.md` written for whoever executes the result. It ends with a block the
|
|
152
|
+
harness writes, not a model: the acceptance criteria the run settled, with a fingerprint.
|
|
153
|
+
`council check-lock HANDOFF.md` ($0) says whether a copy still carries them; it catches an edit or
|
|
154
|
+
a slip, and is not a signature.
|
|
152
155
|
|
|
153
156
|
Some chains add optional stages to this: whole alternative architectures written blind before
|
|
154
157
|
the skeleton, a deep-dive check of the first draft, a dispute stage that records unresolved
|
|
@@ -157,6 +160,12 @@ objections at the top of the plan, and a final security review.
|
|
|
157
160
|
Every run writes a folder: the deliverable, `BOARD.md` (the full debate - every post, every
|
|
158
161
|
withdrawal), `HANDOFF.md`, a per-lab scoreboard, and the real token/cost accounting.
|
|
159
162
|
|
|
163
|
+
A run that stopped before it wrote its handoff (the spend cap, an abandoned pause, a crash) can get one from
|
|
164
|
+
its latest draft: `council handoff --from-run runs/<id>`. It is one call on the chain's handoff seat, under the
|
|
165
|
+
run's own spend cap (what the folder already cost counts toward it; `--max-usd N` is a total, as on `--resume`),
|
|
166
|
+
the same policy check and key scan as a run, and a run no panel signed off gets a note at the top, written by
|
|
167
|
+
the harness, saying so. It never overwrites an existing `HANDOFF.md`.
|
|
168
|
+
|
|
160
169
|
### Reading a run with a program
|
|
161
170
|
|
|
162
171
|
`BOARD.md` is for people. **`report.json` is the same board, structured** - if you are building
|
|
@@ -276,8 +285,8 @@ ones. Put only the keys your chosen chain needs in that `.env`, one `NAME=value`
|
|
|
276
285
|
`DEEPSEEK_API_KEY` · `GROQ_API_KEY` · `TOGETHER_API_KEY` · `COHERE_API_KEY` ·
|
|
277
286
|
`OPENROUTER_API_KEY` · `ZAI_API_KEY`
|
|
278
287
|
|
|
279
|
-
xAI/Grok is never seated, and neither is a router id (`openrouter/auto`, the Pareto Code router
|
|
280
|
-
that could route to it: chain-lint refuses such a chain and the run itself refuses such a seat,
|
|
288
|
+
xAI/Grok is never seated, and neither is a router id (`openrouter/auto`, the Pareto Code router,
|
|
289
|
+
any other `openrouter/<name>` id) that could route to it: chain-lint refuses such a chain and the run itself refuses such a seat,
|
|
281
290
|
with no override. There is no `xai` provider. Kimi/Moonshot models are not in any chain this
|
|
282
291
|
package ships, but you may seat one in a chain of your own.
|
|
283
292
|
|
|
@@ -423,7 +432,7 @@ Each outcome has its own code, so a script or CI job can branch on it:
|
|
|
423
432
|
| 11 | Something in the input must not be sent: a prompt matches one of the key formats listed in `src/secret-patterns.js` (checked by default; `--allow-secret-shaped` overrides), or, under `--pii-gate hard-stop`, a PII shape. |
|
|
424
433
|
| 12 | `policy.json` refused the chain. |
|
|
425
434
|
| 13 | Another process is already running this run folder. |
|
|
426
|
-
| 14 | The task file changed since the run started and `AMENDMENTS.md` does not cover it. |
|
|
435
|
+
| 14 | The task file, or (since 0.8.0) a `--context` document, changed since the run started and `AMENDMENTS.md` does not cover it. The message prints the hashes to write; a context hash is written `ctx-<hash>`. |
|
|
427
436
|
| 15 | `--resume` of a run that already finished (it has a `report.json`); nothing is run or spent. |
|
|
428
437
|
| 16 | The run stopped at an unexpected error (a seat that could not be reached, a crash); `STOPPED-error.md` says what, and `--resume` continues from the completed stages. |
|
|
429
438
|
| 17 | A draft (build, revise, dispute, final edit or handoff) was cut off at its token cap, and its one larger-cap retry was cut off too; the fragment is never graded or shipped (`STOPPED-truncated.md`). |
|
|
@@ -727,6 +736,9 @@ what each rule guards against and why. Copy the folders you want into your proje
|
|
|
727
736
|
input plus the seat's entire `maxTokens` budget billed as output (doubled for Anthropic seats,
|
|
728
737
|
which may retry once). Real stages almost never cost that much, so a run can stop with headroom
|
|
729
738
|
left. That is the intended trade - resume it with a higher ceiling.
|
|
739
|
+
- The dry run (`--dry-run`, MCP `dry_run`) prices the task alone and does not read `--context`
|
|
740
|
+
documents, so for a run with context its estimate is too low. The spend cap is not affected: it
|
|
741
|
+
checks every real prompt, context included, before each call.
|
|
730
742
|
- A seat whose model has no entry in `src/pricing.json` is **unpriced, and therefore uncapped**.
|
|
731
743
|
It contributes $0 to the running total no matter what it really costs. Check `dry_run` output
|
|
732
744
|
for `unpriced` before trusting a ceiling. `ollama` seats are the one deliberate exception: they
|
package/chains/cheap-7-v2.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cheap-7-v2",
|
|
3
3
|
"description": "Seven independent labs vote, none of them the author's, all on current-generation models through one OpenRouter key: OpenAI GPT-5.6 Luna, Google Gemini 3.8 Flash, DeepSeek v4.1 Flash, Z.ai GLM 5.3 Flash, Qwen 3.7 Plus, MiniMax M3, Mistral Medium 3.5. Blind proposals, debate, unanimous signoff, with a round cap of 7 and a stall rule: if the same objections from the same labs survive two rounds running, the revise loop stops rather than buying more rounds of the same argument, and the unresolved dissent is recorded at the top of the deliverable. A run that ends that way reports no consensus - the dispute stage records disagreement, it never resolves it. Anthropic Sonnet 5 writes the skeleton, the draft, every revision and the handoff and holds NO panel seat; Meta Llama 4 Maverick writes the criteria and also holds no panel seat, so neither the lab that drafts nor the lab that sets the bar votes on whether the bar was cleared. This is the revision of cheap-7 after its first real run, where one lab held the criteria, skeleton, builder, reviser and handoff seats AND a critic seat, voted on its own draft, and a claim it wrote survived to the deliverable. Anthropic is deliberately absent from the panel: plans from this chain are read by a Claude session before anything is built, so that review happens downstream rather than inside the vote. Kimi K3 is deliberately absent too - a seat that may route through another lab is not an independent seat. Every seat gets a 36000-token ceiling and reasoning is left on: a debate is not squeezed to a short answer, and a reply cut off at the ceiling is retried once, then the seat is re-asked alone - it is never counted as agreement. The cap is a ceiling, not a price; only generated tokens are billed. Prices are from src/pricing.json, refreshed against OpenRouter's live model list on 2026-09-20 - price a real task with `npm run dry` before running.",
|
|
4
|
+
"summary": "Seven labs on one OpenRouter key: blind proposals, debate, unanimous sign-off.",
|
|
4
5
|
"maxRounds": 7,
|
|
5
6
|
"signoff": "unanimous",
|
|
6
7
|
"dispute": {
|
package/chains/local-ollama.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-ollama",
|
|
3
3
|
"description": "BYO-hardware: every seat runs on a local Ollama instance. No API key, no spend - `dry_run` prices this chain at $0. Swap the model names for whatever you've pulled (`ollama pull <model>`); swap `baseUrl` if Ollama (or an LM Studio / other OpenAI-compatible local runner) listens somewhere other than the default `http://localhost:11434/v1`.",
|
|
4
|
+
"summary": "Every seat on a local Ollama model: no API key and no spend.",
|
|
4
5
|
"maxRounds": 2,
|
|
5
6
|
"stopOnPass": true,
|
|
6
7
|
"estimate": {
|
package/chains/plan-daily-7.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plan-daily-7",
|
|
3
3
|
"description": "daily7 (the owner's name, 2026-09-27: cheap API seats, and the expensive Opus 5.5 seat runs on a local Claude Code session). Tiered council for planning ONE feature cheaply (Muad, 2026-09-27: \"Opus 5.5 on a local sub that does the important tasks from the highest7 setup ... all together ~1$ per council run\"). Same stages and flags as plan-highest-7 (alternatives, debate, decision records, deep dive, majority guard ON, canary, dispute, handoff); only the seats and the deep-dive cap differ. ANCHOR SEATS write the whole alternative architectures (seats.alternatives), answer the posts on them, and are the only panel that votes, unanimously (seats.critics): Claude Opus 5.5 on the owner's Claude subscription (provider external, model subscription:opus-5.5, lab opus5.5-sub: $0 API, a separate Opus 5.5 Claude Code session answers its paused stages), GPT-6 Luna and DeepSeek V4 Pro through OpenRouter. MASS SEATS (seats.proposers, seven labs from the strong and low-cost part of arena.ai's agent leaderboard, fetched 2026-09-27): Tencent Hy4 preview, Gemini 3.8 Flash, DeepSeek V4.1 Flash, GLM 5.2, Muse Spark 1.3, Qwen 3.8 Max, Mistral Medium 3.5 (kept on the owner's word) write the blind proposals and every debate post and reply, and post on the anchors' architectures (alternatives.debaters \"all\"); they argue, they never vote. DEEP-DIVE SEAT: DeepSeek V4.1 Flash, a mass-tier model (a different model from the voting V4 Pro, so chain-lint deep-dive-votes passes), under its own $0.50 cap: a feature-sized task is one 40,000-character call, and a $4 sub-cap would be larger than this chain's whole worst case. Criteria: Llama 4 Maverick, which holds no other seat. WRITER SEATS (skeleton, build, deep-dive-revise, revise-N, dispute, handoff) are a Claude Code session running Sonnet 5, which builds and holds no panel seat. The Opus 5.5 panel seat is a DIFFERENT Anthropic model in a separate session: the owner's plan-highest-7 exception (an Anthropic model reviews a draft another Claude writes), not selfReview - no model writes and votes. ROUTING, because a NEEDS file names only its label: every label containing opus5.5-sub goes to the Opus 5.5 session (alternative-, alt-debate-, alt-reply-, panel-N-, including the suffixed forms panel-N-opus5.5-sub-retry, -reask1, -reask2 and alternative-opus5.5-sub-retry); skeleton, build, deep-dive-revise, revise-N, dispute and handoff go to the Sonnet 5 session. Nothing checks who wrote an answer. Dry run on a one-page feature task (2026-09-27, GPT-6 Luna in place of GPT-6 Sol on the owner's word: \"GPT 6 Sol is too expensive, take GPT 6 luna\"): $1.90 worst case at 7 rounds; under the default $7 cap. No Kimi, no xAI. Run once with real models (2026-09-28); none of its mechanisms has been measured. Price your task with `npm run dry` and get an explicit go before any paid run.",
|
|
4
|
+
"summary": "One feature: three anchors write architectures and vote, seven low-cost labs debate; Opus 5.5 via your Claude plan.",
|
|
4
5
|
"maxRounds": 7,
|
|
5
6
|
"signoff": "unanimous",
|
|
6
7
|
"dispute": { "enabled": true, "stall_rounds": 2 },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plan-highest-7",
|
|
3
3
|
"description": "Tiered council, Muad's design (2026-09-26): the highest models for the highest tasks, the cheapest models for the mass work. MASS SEATS (seats.proposers, the cheap-7-v2 roster: GPT-5.6 Luna, Gemini 3.8 Flash, DeepSeek V4.1 Flash, GLM 5.3 Flash, Qwen 3.7 Plus, MiniMax M3, Mistral Medium 3.5) write the blind proposals and all the proposal debate posts and replies, and post on the anchors' architectures (alternatives.debaters \"all\"). ANCHOR SEATS: GPT-6 Astra and Claude Opus 5.5 (Muad's pick 2026-09-26, in place of Fable 5.1; still his deliberate exception: an Anthropic model reviews a draft a Claude Code session writes), DeepSeek V4 Pro and GLM-5.3 write the whole alternative architectures (seats.alternatives), answer the posts on them, and form the review panel that must sign off unanimously (seats.critics). The mass seats argue; only the anchors vote, so a cheap majority can never out-vote them. DEEP-DIVE SEAT (seats.deep_dive, DeepSeek V4.1 Flash, a mass-tier model: it must not share a lab with a voting anchor, chain-lint deep-dive-votes): one job, checking the first draft against every part of the task, 40,000 characters per call at a 36k output cap, up to 24 calls under its OWN $4 cap inside the run's; it does not vote, and the reviser answers its findings before the anchors' first review. The MAJORITY GUARD is ON (majority_guard, Muad's go 2026-09-26; off in every other shipped chain but mock-tiered): authors see each argument once with no lab letter or count, the reply prompts carry an evidence bar, and a withdrawal that does not quote the argument it concedes to is kept for the builder. Criteria move to Llama 4 Maverick, which holds no other seat. Same Claude Code writer seats and stages as plan-premium-7, including decision records and the dispute stage. No Kimi, no xAI. Untested with real models: price it with `npm run dry` and get an explicit go before any paid run.",
|
|
4
|
+
"summary": "Four anchor labs write architectures and vote; seven low-cost labs write proposals and debate.",
|
|
4
5
|
"maxRounds": 7,
|
|
5
6
|
"signoff": "unanimous",
|
|
6
7
|
"dispute": { "enabled": true, "stall_rounds": 2 },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plan-premium-7",
|
|
3
3
|
"description": "Premium panel, Muad's roster (2026-09-22): OpenAI GPT-6 Astra, Anthropic Claude Fable 5.1, Google Gemini 3.8 Flash, Meta Muse Spark 1.2, Qwen 3.8 Max, DeepSeek V4 Pro, Z.ai GLM-5.3 - seven labs, same stages and external Claude-role seats as plan-open-7. DELIBERATE EXCEPTION, Muad's call: an Anthropic model (Fable 5.1) sits on the panel although the draft is written by a Claude Code session, so this panel is not fully independent of the drafting lab - read its sign-off with that in mind. Criteria move to Mistral Medium 3.5, which holds no panel seat, because Meta now votes. No Kimi K3, no xAI. Each seat carries a persona (debate stage only; see PERSONAS.md): the five defaults plus Hypatia and Avicenna via COUNCIL_PERSONAS_FILE. Several times the cost of plan-open-7: price with `npm run dry` and get an explicit go first. Decision records and whole alternative architectures on (2026-09-23): each lab blind-proposes one whole architecture before the skeleton, the labs debate them, and the plan's Decisions section records why the losers lost.",
|
|
4
|
+
"summary": "Seven premium-tier labs argue whole architectures first; a Claude Code session writes the drafts.",
|
|
4
5
|
"maxRounds": 7,
|
|
5
6
|
"signoff": "unanimous",
|
|
6
7
|
"dispute": {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "security-review-only",
|
|
3
|
+
"description": "A tiny chain whose only real content is a human-supplied artifact and the final security-review gate (515439a, docs/security-review-gate.md). The builder seat is external: the CLI pauses, an operator (a person or an orchestrator like Sophi-A) pastes the artifact under review as the paused stage's own <label>.md, and resumes. No criteria call (criteria is hardcoded so that stage never runs) and one always-passing mock critic round, present only because chain-lint requires at least one seats.critics entry - this chain's real gate is security_review, not the critic panel. Exit 0 = pass, 7 = blocked, 8 = not_judged. Swap seats.security_reviewer's model to mock-security-block for the blocking path (this file's default), or to any other name (e.g. mock-security-clean) for the passing path - see docs/security-review-gate.md. Zero engine changes: this is a config-only chain built entirely from existing seat kinds and the existing security_review flag.",
|
|
4
|
+
"maxRounds": 1,
|
|
5
|
+
"criteria": [
|
|
6
|
+
"The artifact under review is present as the resumed external stage's content.",
|
|
7
|
+
"The final security-review gate ran and recorded a gate verdict."
|
|
8
|
+
],
|
|
9
|
+
"seats": {
|
|
10
|
+
"builder": {
|
|
11
|
+
"provider": "external"
|
|
12
|
+
},
|
|
13
|
+
"critics": [
|
|
14
|
+
{
|
|
15
|
+
"provider": "mock",
|
|
16
|
+
"model": "mock-critic-passer",
|
|
17
|
+
"lab": "mock-passthrough"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"security_reviewer": {
|
|
21
|
+
"provider": "mock",
|
|
22
|
+
"model": "mock-security-block",
|
|
23
|
+
"lab": "mock-security"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"security_review": {
|
|
27
|
+
"enabled": true
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "the-high-council",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"mcpName": "io.github.muad-yasin/the-high-council",
|
|
5
5
|
"description": "The High Council: a multi-model planning harness. One builder, independent critics from other labs, checkable acceptance criteria, a debate on the record, and a hard stop.",
|
|
6
6
|
"type": "module",
|
package/schemas/report-v1.json
CHANGED
|
@@ -502,6 +502,141 @@
|
|
|
502
502
|
},
|
|
503
503
|
"description": "Present only on chains with argued.enabled: where the 'How this plan was argued' text is, and the check of every id and lab it names against this run's record."
|
|
504
504
|
},
|
|
505
|
+
"criteria_sha256": {
|
|
506
|
+
"x-stability": "experimental",
|
|
507
|
+
"type": "string",
|
|
508
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
509
|
+
"description": "sha256 over the criteria list, each criterion collapsed to one line, order counted (0.8.0). The same fingerprint the HANDOFF.md lock block carries; `council check-lock` compares them."
|
|
510
|
+
},
|
|
511
|
+
"criteria_lints": {
|
|
512
|
+
"x-stability": "experimental",
|
|
513
|
+
"type": "array",
|
|
514
|
+
"description": "Word-level lints over the acceptance criteria, run before any paid review round (0.8.0). Heuristics: they miss a rephrasing and can flag a good criterion. Recorded only: never a stop. [] when clean.",
|
|
515
|
+
"items": {
|
|
516
|
+
"type": "object",
|
|
517
|
+
"required": ["id", "message"],
|
|
518
|
+
"properties": {
|
|
519
|
+
"id": { "enum": ["no-consistency", "presence-heavy", "vague-word"] },
|
|
520
|
+
"message": { "type": "string" },
|
|
521
|
+
"criterion_ids": { "type": "array", "items": { "type": "string" } }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
"criteria_ids": {
|
|
526
|
+
"x-stability": "experimental",
|
|
527
|
+
"type": "array",
|
|
528
|
+
"items": {
|
|
529
|
+
"type": "string"
|
|
530
|
+
},
|
|
531
|
+
"description": "Positional ids for `criteria`, index-aligned: C1, C2, ... (0.8.0)."
|
|
532
|
+
},
|
|
533
|
+
"missing_criteria": {
|
|
534
|
+
"x-stability": "experimental",
|
|
535
|
+
"type": "array",
|
|
536
|
+
"description": "Sign-offs whose criteria table left criteria out (0.8.0). Recorded only: no verdict, prompt or stop reads it. [] when there were none. A reply with no table at all is not listed.",
|
|
537
|
+
"items": {
|
|
538
|
+
"type": "object",
|
|
539
|
+
"required": [
|
|
540
|
+
"round",
|
|
541
|
+
"lab",
|
|
542
|
+
"criterion_ids"
|
|
543
|
+
],
|
|
544
|
+
"properties": {
|
|
545
|
+
"round": {
|
|
546
|
+
"type": "integer"
|
|
547
|
+
},
|
|
548
|
+
"lab": {
|
|
549
|
+
"type": "string"
|
|
550
|
+
},
|
|
551
|
+
"criterion_ids": {
|
|
552
|
+
"type": "array",
|
|
553
|
+
"items": {
|
|
554
|
+
"type": "string"
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
"table_rows": {
|
|
558
|
+
"type": "integer"
|
|
559
|
+
},
|
|
560
|
+
"criteria_total": {
|
|
561
|
+
"type": "integer"
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
"cut_despite_support": {
|
|
567
|
+
"x-stability": "experimental",
|
|
568
|
+
"type": "array",
|
|
569
|
+
"description": "Proposals the plan cut although another lab supported them in the debate (0.8.0). Derived from proposals[] and debate.posts[]; present only when the run had both.",
|
|
570
|
+
"items": {
|
|
571
|
+
"type": "object",
|
|
572
|
+
"required": [
|
|
573
|
+
"id",
|
|
574
|
+
"supporters"
|
|
575
|
+
],
|
|
576
|
+
"properties": {
|
|
577
|
+
"id": {
|
|
578
|
+
"type": "string"
|
|
579
|
+
},
|
|
580
|
+
"title": {},
|
|
581
|
+
"author_lab": {
|
|
582
|
+
"type": [
|
|
583
|
+
"string",
|
|
584
|
+
"null"
|
|
585
|
+
]
|
|
586
|
+
},
|
|
587
|
+
"supporters": {
|
|
588
|
+
"type": "array",
|
|
589
|
+
"items": {
|
|
590
|
+
"type": "string"
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
"objectors": {
|
|
594
|
+
"type": "array",
|
|
595
|
+
"items": {
|
|
596
|
+
"type": "string"
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
"note": {
|
|
600
|
+
"type": "string"
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
"disagreement_map": {
|
|
606
|
+
"x-stability": "experimental",
|
|
607
|
+
"type": "array",
|
|
608
|
+
"description": "One row per review round: each lab's verdict and the counts (0.8.0). Derived from panelVerdicts[]. A verdict marked `(carried)` was reused on an unchanged draft.",
|
|
609
|
+
"items": {
|
|
610
|
+
"type": "object",
|
|
611
|
+
"required": [
|
|
612
|
+
"round",
|
|
613
|
+
"labs"
|
|
614
|
+
],
|
|
615
|
+
"properties": {
|
|
616
|
+
"round": {
|
|
617
|
+
"type": "integer"
|
|
618
|
+
},
|
|
619
|
+
"labs": {
|
|
620
|
+
"type": "object",
|
|
621
|
+
"additionalProperties": {
|
|
622
|
+
"type": "string"
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
"signed_off": {
|
|
626
|
+
"type": "integer"
|
|
627
|
+
},
|
|
628
|
+
"objected": {
|
|
629
|
+
"type": "integer"
|
|
630
|
+
},
|
|
631
|
+
"unheard": {
|
|
632
|
+
"type": "integer"
|
|
633
|
+
},
|
|
634
|
+
"passed": {
|
|
635
|
+
"type": "integer"
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
},
|
|
505
640
|
"criteria_kinds": {
|
|
506
641
|
"x-stability": "experimental",
|
|
507
642
|
"type": "array",
|
package/src/cache-integrity.js
CHANGED
|
@@ -15,7 +15,11 @@ import { createHash } from 'node:crypto';
|
|
|
15
15
|
|
|
16
16
|
/** A short fingerprint of the inputs a cached stage's validity depends on. */
|
|
17
17
|
export function fingerprintInputs(taskText, config) {
|
|
18
|
-
|
|
18
|
+
// `summary` is a chain card's display line: no stage reads it, so editing it must not make a
|
|
19
|
+
// resumed run read every finished stage as stale. A config without one hashes exactly as before.
|
|
20
|
+
let forHash = config;
|
|
21
|
+
if (config && typeof config === 'object' && 'summary' in config) { const { summary: _display, ...rest } = config; forHash = rest; }
|
|
22
|
+
return createHash('sha256').update(JSON.stringify({ taskText, config: forHash })).digest('hex').slice(0, 12);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/**
|
package/src/chain-lint.js
CHANGED
|
@@ -162,6 +162,16 @@ export function lintChain(config, filePath = '<chain>') {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// 5a. `summary` (0.8.0 WM0): a chain card's one-line display text. Nothing reads it but a UI, so a
|
|
166
|
+
// wrong type or a paragraph would only show up there; refuse it here.
|
|
167
|
+
if (config && 'summary' in config && !(typeof config.summary === 'string' && config.summary.length >= 1 && config.summary.length <= 120 && !/[\r\n]/.test(config.summary))) {
|
|
168
|
+
findings.push({
|
|
169
|
+
kind: 'invalid-summary',
|
|
170
|
+
message: `summary must be one line of 1 to 120 characters (got ${typeof config.summary === 'string' ? `${config.summary.length} characters` : JSON.stringify(config.summary)}).`,
|
|
171
|
+
fix: `Shorten "summary" in ${filePath}, or move the detail into "description".`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
165
175
|
// 5b. Opt-in flag blocks with a closed key set (pre-release audit 2026-09-23, lint #2). Each of
|
|
166
176
|
// these is read in chain.js as `config.<block>?.enabled` plus the named keys below, and nothing
|
|
167
177
|
// else - so a typo'd key ("alternatives": { "enable": true }) used to validate, lint clean, and
|
package/src/chain.js
CHANGED
|
@@ -17,6 +17,8 @@ import { assertNoDeniedModels, deniedReasonsOf, DeniedModel } from './denied-mod
|
|
|
17
17
|
import { promptHashOf, cacheVerdict } from './cache-integrity.js';
|
|
18
18
|
import { buildArguedFacts, checkArguedRefs, ARGUED_SYSTEM, arguedUser, ARGUED_LABEL, ARGUED_FILE } from './argued.js';
|
|
19
19
|
import { normaliseCriteria, criteriaSummary, kindsRecord, checksSection, unevidencedCheckableMets, summaryLine, MET_VERDICT } from './criteria-kinds.js';
|
|
20
|
+
import { missingCriteriaRows } from './criteria-ledger.js';
|
|
21
|
+
import { lintCriteria } from './criteria-lints.js';
|
|
20
22
|
import { runDeepDive, deepDiveFailures } from './deep-dive.js';
|
|
21
23
|
import { assertOutboundClean, SecretShapedPrompt } from './outbound-scan.js';
|
|
22
24
|
export { DeniedModel };
|
|
@@ -1008,6 +1010,16 @@ export function setChargeHook(fn) { chargeHook = fn || (() => {}); }
|
|
|
1008
1010
|
let outboundScan = { allow: false };
|
|
1009
1011
|
export function setOutboundScan(opts) { outboundScan = { allow: !!opts?.allow }; }
|
|
1010
1012
|
|
|
1013
|
+
/**
|
|
1014
|
+
* One free-text stage on a seat, outside runChain: the same invoke() every stage goes through, so the
|
|
1015
|
+
* spend cap, the denied-model backstop and the outbound key scan all apply. For a caller that needs a
|
|
1016
|
+
* single call (src/handoff-from-run.js), never as a way round any of them. Set the cap first
|
|
1017
|
+
* (setBudget) or it is uncapped, as for any caller.
|
|
1018
|
+
*/
|
|
1019
|
+
export async function runSingleStage(seat, { system, user, log = () => {}, label }) {
|
|
1020
|
+
return invoke(seat, { system, user, log, label });
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1011
1023
|
async function invoke(seat, { system, user, log, label }) {
|
|
1012
1024
|
const started = Date.now();
|
|
1013
1025
|
// Backstop for the check at the top of runChain: every call, including a replay from disk.
|
|
@@ -1257,14 +1269,23 @@ export function resolveChainSeats(config) {
|
|
|
1257
1269
|
return { ...config, seats };
|
|
1258
1270
|
}
|
|
1259
1271
|
|
|
1260
|
-
|
|
1272
|
+
// Every seat slot a chain can fill, with the name of the slot (`role`), in one place. `allSeatsOf`
|
|
1273
|
+
// and `everySeatOf` below are this list with the names dropped, so a reader that wants the roles
|
|
1274
|
+
// (the dry run's per-seat key report, src/dry-run.js) cannot drift from the guards that read the
|
|
1275
|
+
// seats. The order is the one those functions always had.
|
|
1276
|
+
const SINGLE_SEAT_KEYS = ['criteria', 'builder', 'reviser', 'finalist', 'skeleton', 'handoff', 'questions', 'judge', 'challenger', 'coldRead', 'claims', 'security_reviewer', 'deep_dive'];
|
|
1277
|
+
const LIST_SEAT_KEYS = ['critics', 'proposers', 'ambiguity', 'alternatives'];
|
|
1278
|
+
export function seatSlotsOf(config) {
|
|
1261
1279
|
const s = config.seats || {};
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1280
|
+
const out = [];
|
|
1281
|
+
for (const key of SINGLE_SEAT_KEYS) if (s[key]) out.push({ role: key, seat: s[key] });
|
|
1282
|
+
for (const key of LIST_SEAT_KEYS) (s[key] || []).forEach((seat, i) => { if (seat) out.push({ role: `${key}[${i}]`, seat }); });
|
|
1283
|
+
for (const [stage, seat] of Object.entries(s.descending || {})) if (seat) out.push({ role: `descending.${stage}`, seat });
|
|
1284
|
+
return out;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function allSeatsOf(config) {
|
|
1288
|
+
return seatSlotsOf(config).map(x => x.seat);
|
|
1268
1289
|
}
|
|
1269
1290
|
|
|
1270
1291
|
// Every seat a run can call, for the guards (policy.json, the missing-key check). Bug-audit fix,
|
|
@@ -1273,10 +1294,14 @@ function allSeatsOf(config) {
|
|
|
1273
1294
|
// ambiguity, descending, preflight or the default security reviewer - a Grok challenger passed an
|
|
1274
1295
|
// EU/Mistral-only policy. This adds the two seats allSeatsOf cannot see (preflight.seats and the
|
|
1275
1296
|
// security reviewer a run falls back to) and is the one list the CLI's guards read.
|
|
1297
|
+
export function everySeatSlotsOf(config) {
|
|
1298
|
+
const slots = seatSlotsOf(config);
|
|
1299
|
+
if (Array.isArray(config?.preflight?.seats)) config.preflight.seats.forEach((seat, i) => { if (seat) slots.push({ role: `preflight.seats[${i}]`, seat }); });
|
|
1300
|
+
if (config?.security_review?.enabled === true && !config.seats?.security_reviewer) slots.push({ role: 'security_reviewer (default)', seat: DEFAULT_SECURITY_REVIEWER_SEAT });
|
|
1301
|
+
return slots;
|
|
1302
|
+
}
|
|
1276
1303
|
export function everySeatOf(config) {
|
|
1277
|
-
|
|
1278
|
-
if (config?.security_review?.enabled === true && !config.seats?.security_reviewer) seats.push(DEFAULT_SECURITY_REVIEWER_SEAT);
|
|
1279
|
-
return seats.filter(Boolean);
|
|
1304
|
+
return everySeatSlotsOf(config).map(x => x.seat);
|
|
1280
1305
|
}
|
|
1281
1306
|
|
|
1282
1307
|
export function checkSeats(seats) {
|
|
@@ -1478,7 +1503,7 @@ export async function runDescendingChain({ request, config, log = console.log, o
|
|
|
1478
1503
|
// Pre-release audit 2026-09-23 (GuardLayer #1, and PanelSignoff's backlog): these were dropped
|
|
1479
1504
|
// here, so a descending chain's security gate never reached the CLI (no exit 7/8, no gate in
|
|
1480
1505
|
// report.json) and the panel/dispute record was lost. Forwarded only when the final run set them.
|
|
1481
|
-
...Object.fromEntries(['security_review', 'panelVerdicts', 'dispute', 'regressions', 'noHeardReviewer', 'notQuorate']
|
|
1506
|
+
...Object.fromEntries(['security_review', 'panelVerdicts', 'missingCriteria', 'criteriaLints', 'dispute', 'regressions', 'noHeardReviewer', 'notQuorate']
|
|
1482
1507
|
.filter(k => finalResult[k] !== undefined).map(k => [k, finalResult[k]])),
|
|
1483
1508
|
totals: summarise(stages),
|
|
1484
1509
|
};
|
|
@@ -1579,6 +1604,8 @@ async function runChainStages({ request: requestIn, config, draft: initialDraft
|
|
|
1579
1604
|
disputes: peek(() => disputes, []),
|
|
1580
1605
|
regressions: peek(() => regressions, []),
|
|
1581
1606
|
panelVerdicts: peek(() => panelVerdicts, []),
|
|
1607
|
+
missingCriteria: peek(() => missingCriteria, []),
|
|
1608
|
+
criteriaLints: peek(() => criteriaLints, []),
|
|
1582
1609
|
history: peek(() => history, []),
|
|
1583
1610
|
stages,
|
|
1584
1611
|
totals: summarise(stages),
|
|
@@ -1784,6 +1811,13 @@ async function runChainStages({ request: requestIn, config, draft: initialDraft
|
|
|
1784
1811
|
return n.texts;
|
|
1785
1812
|
};
|
|
1786
1813
|
const metWithoutEvidence = [];
|
|
1814
|
+
// A sign-off whose criteria table left criteria out (src/criteria-ledger.js): recorded, never acted on.
|
|
1815
|
+
const missingCriteria = [];
|
|
1816
|
+
const noteMissingRows = (critique, round, lab) => {
|
|
1817
|
+
if (critique?.meets !== true) return;
|
|
1818
|
+
const m = missingCriteriaRows(critique, criteria);
|
|
1819
|
+
if (m) missingCriteria.push({ round, lab, ...m });
|
|
1820
|
+
};
|
|
1787
1821
|
const noteUnevidenced = (critique, round, lab, say) => {
|
|
1788
1822
|
for (const criterion of unevidencedCheckableMets(critique, criteria, criteriaKinds)) {
|
|
1789
1823
|
metWithoutEvidence.push({ round, lab, criterion });
|
|
@@ -1885,6 +1919,10 @@ async function runChainStages({ request: requestIn, config, draft: initialDraft
|
|
|
1885
1919
|
criteriaKinds.forEach((k, i) => { if (k.kind === 'checkable') log(` ${i + 1}: checkable on ${k.on} - ${k.check}`); });
|
|
1886
1920
|
log(` ${summaryLine(criteriaSummary(criteria, criteriaKinds))}`);
|
|
1887
1921
|
}
|
|
1922
|
+
// $0 word-level lints over the list, before any paid review round (src/criteria-lints.js): recorded
|
|
1923
|
+
// and logged, never a stop.
|
|
1924
|
+
const criteriaLints = lintCriteria(criteria);
|
|
1925
|
+
for (const f of criteriaLints) log(` criteria lint (${f.id}): ${f.message}`);
|
|
1888
1926
|
// The "How the checkable criteria are settled" block for critic and handoff prompts; '' when
|
|
1889
1927
|
// kinds are off or nothing is checkable, which keeps those prompts byte-identical.
|
|
1890
1928
|
const checks = kindsOn ? checksSection(criteria, criteriaKinds) : '';
|
|
@@ -2683,6 +2721,7 @@ async function runChainStages({ request: requestIn, config, draft: initialDraft
|
|
|
2683
2721
|
return { seat: criticSeat, critique: null, abstained: true, reasonCode };
|
|
2684
2722
|
}
|
|
2685
2723
|
say(` ${labOf(criticSeat)}/${criticSeat.model}: ${critique.meets ? 'SIGNED OFF' : `${critique.failures.length} failure(s)`} - ${critique.verdict_line || ''}`);
|
|
2724
|
+
noteMissingRows(critique, round, labOf(criticSeat));
|
|
2686
2725
|
if (checks) noteUnevidenced(critique, round, labOf(criticSeat), say);
|
|
2687
2726
|
// v5 item 3, touch point 2: the verdict update the live view needs - `passed` (chain.js's
|
|
2688
2727
|
// own name for "meets every criterion") is already computed here, no new parsing.
|
|
@@ -3111,6 +3150,7 @@ async function runChainStages({ request: requestIn, config, draft: initialDraft
|
|
|
3111
3150
|
break;
|
|
3112
3151
|
}
|
|
3113
3152
|
lastCritique = critique;
|
|
3153
|
+
noteMissingRows(critique, round, labOf(criticSeat));
|
|
3114
3154
|
if (checks) noteUnevidenced(critique, round, labOf(criticSeat), log);
|
|
3115
3155
|
panelVerdicts.push({ round, lab: labOf(criticSeat), model: criticSeat.model, verdict: critique.meets === true ? 'signed_off' : 'objected', reason_code: null, reasked: false });
|
|
3116
3156
|
const failures = critique.failures;
|
|
@@ -3524,6 +3564,9 @@ async function runChainStages({ request: requestIn, config, draft: initialDraft
|
|
|
3524
3564
|
...(alternatives !== null ? { alternatives } : {}),
|
|
3525
3565
|
// Additive: absent on any chain without the deep-dive seat (tiered councils).
|
|
3526
3566
|
...(deepDive !== undefined ? { deep_dive: deepDive } : {}),
|
|
3567
|
+
// Additive (0.8.0): the sign-offs whose criteria table skipped criteria; [] when there were none.
|
|
3568
|
+
missingCriteria,
|
|
3569
|
+
criteriaLints,
|
|
3527
3570
|
// Additive: absent unless the chain enabled criterion kinds (src/criteria-kinds.js).
|
|
3528
3571
|
...(kindsOn ? {
|
|
3529
3572
|
criteriaKinds: kindsRecord(criteria, criteriaKinds),
|