the-high-council 0.6.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 (78) hide show
  1. package/.env.example +19 -0
  2. package/CONTRIBUTING.md +41 -0
  3. package/LICENSE +21 -0
  4. package/PERSONAS.md +51 -0
  5. package/README.md +499 -0
  6. package/TROUBLESHOOTING.md +45 -0
  7. package/chains/cheap.json +35 -0
  8. package/chains/gp-judge-v1.json +80 -0
  9. package/chains/idea-open-c2.json +106 -0
  10. package/chains/mock-budget.json +15 -0
  11. package/chains/mock-debate.json +59 -0
  12. package/chains/mock-external.json +59 -0
  13. package/chains/mock-open.json +60 -0
  14. package/chains/mock-proposals.json +57 -0
  15. package/chains/mock-questions-wait.json +63 -0
  16. package/chains/mock-questions.json +63 -0
  17. package/chains/mock-relay.json +42 -0
  18. package/chains/mock-unanimous.json +42 -0
  19. package/chains/mock.json +17 -0
  20. package/chains/plan-auto-mistral.json +101 -0
  21. package/chains/plan-auto.json +131 -0
  22. package/chains/plan-cheap.json +82 -0
  23. package/chains/plan-debate-c2.json +86 -0
  24. package/chains/plan-debate-open-c2.json +98 -0
  25. package/chains/plan-debate-roles-c1.json +149 -0
  26. package/chains/plan-debate.json +116 -0
  27. package/chains/plan-fast.json +66 -0
  28. package/chains/plan-proposals-best-of-5.json +98 -0
  29. package/chains/plan-proposals.json +96 -0
  30. package/chains/plan-relay.json +83 -0
  31. package/chains/plan-thorough.json +82 -0
  32. package/chains/plan-unanimous.json +82 -0
  33. package/chains/seven-cheap.json +67 -0
  34. package/chains/seven.json +60 -0
  35. package/chains/smo-v2-cheap.json +67 -0
  36. package/chains/smo.json +50 -0
  37. package/chains/verify.json +40 -0
  38. package/package.json +55 -0
  39. package/src/board-export.js +77 -0
  40. package/src/budget.js +126 -0
  41. package/src/buys.js +44 -0
  42. package/src/cache-integrity.js +38 -0
  43. package/src/chain-lint.js +107 -0
  44. package/src/chain.js +880 -0
  45. package/src/cli.js +1031 -0
  46. package/src/cost-forecast.js +70 -0
  47. package/src/cost.js +139 -0
  48. package/src/errors.js +82 -0
  49. package/src/http-server.js +349 -0
  50. package/src/integrity.js +48 -0
  51. package/src/key-redaction.js +97 -0
  52. package/src/mcp/server.js +319 -0
  53. package/src/name-lint.js +186 -0
  54. package/src/partial-deliverable.js +38 -0
  55. package/src/peer-claim.js +98 -0
  56. package/src/personas.js +54 -0
  57. package/src/preflight.js +84 -0
  58. package/src/pricing.json +120 -0
  59. package/src/providers.js +268 -0
  60. package/src/replay.js +40 -0
  61. package/src/resume-brief.js +114 -0
  62. package/src/role-diagnostics.js +181 -0
  63. package/src/roles.js +520 -0
  64. package/src/schema-version.js +39 -0
  65. package/src/scope-freeze.js +35 -0
  66. package/src/seat-role.js +96 -0
  67. package/src/shape-rounds.js +58 -0
  68. package/src/spend.js +193 -0
  69. package/src/stage-contract.js +223 -0
  70. package/src/stage-submission.js +53 -0
  71. package/src/tie-break.js +94 -0
  72. package/src/ui/app.js +139 -0
  73. package/src/ui/index.html +43 -0
  74. package/src/ui/parse.js +101 -0
  75. package/src/ui/server.js +117 -0
  76. package/src/ui/style.css +37 -0
  77. package/src/verdict-stats.js +267 -0
  78. package/src/withdrawal-ledger.js +47 -0
package/src/replay.js ADDED
@@ -0,0 +1,40 @@
1
+ // v5 §1 candidate 9: a numbered, indented, step-by-step transcript of a
2
+ // run's reasoning - a CLI-native complement to the HTML export (candidate
3
+ // 8): CLI-stepped replay for a maintainer debugging a run, vs. a
4
+ // shareable artifact for a stranger. Read-only, derived from report.json.
5
+
6
+ /**
7
+ * An ordered list of transcript steps from a run's report.json: proposals,
8
+ * then each recorded stage, then the final verdict. Each step is
9
+ * `{ n, kind, indent, text }` so a renderer can number and indent without
10
+ * re-deriving structure.
11
+ */
12
+ export function buildTranscript(report) {
13
+ const steps = [];
14
+ let n = 1;
15
+ const push = (kind, indent, text) => steps.push({ n: n++, kind, indent, text });
16
+
17
+ push('meta', 0, `Chain: ${report.chain || 'unknown'}${report.runId ? `, run: ${report.runId}` : ''}`);
18
+
19
+ for (const p of report.proposals || []) {
20
+ push('proposal', 0, `Proposal ${p.id} (${p.lab}): ${p.title}`);
21
+ if (p.what) push('proposal-detail', 1, `What: ${p.what}`);
22
+ if (p.withdrawn) push('proposal-detail', 1, `WITHDRAWN${p.replaced_by ? ` in favour of ${p.replaced_by}` : ''}`);
23
+ }
24
+
25
+ for (const s of report.stages || []) {
26
+ push('stage', 0, `Stage ${s.label} (${s.provider}/${s.model})`);
27
+ }
28
+
29
+ for (const s of report.signoff || []) {
30
+ push('signoff', 1, `${s.provider}: ${s.signedOff === null ? 'abstained' : s.signedOff ? 'signed off' : 'objected'}`);
31
+ }
32
+
33
+ push('verdict', 0, report.passed ? 'Verdict: PASSED - every lab signed off' : 'Verdict: OPEN OBJECTIONS');
34
+
35
+ return steps;
36
+ }
37
+
38
+ export function renderTranscriptText(steps) {
39
+ return steps.map(s => `${String(s.n).padStart(4)}. ${' '.repeat(s.indent)}${s.text}`).join('\n');
40
+ }
@@ -0,0 +1,114 @@
1
+ // Resume-brief mechanism (v2 plan §5, DEEPSEEK-4 / GLM-4 / MISTRAL-4 merged design).
2
+ //
3
+ // A short, deterministic, regenerable summary of one run's state, so a driving session
4
+ // returning after time away - possibly with fully fresh context - can re-enter the run
5
+ // by reading one short file instead of reconstructing state from raw board files.
6
+ //
7
+ // Load-bearing property: RESUME.md is never itself authoritative. It is always
8
+ // regenerable from the run's actual state on disk (run.json + which stage files exist),
9
+ // so a deleted, stale, or hand-edited copy can never become a lying source of truth -
10
+ // the next call just rebuilds it identically. No LLM call: an LLM-generated brief could
11
+ // itself hallucinate state and become a new silent-failure vector, which this project has
12
+ // already been burned by once (the lab-dropout incident) and does not want a second version of.
13
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { stageKindOf, stageKindsFor } from './stage-contract.js';
16
+ import { checkClaimStaleness } from './peer-claim.js';
17
+
18
+ // Mirrors src/mcp/server.js's waiting(dir), duplicated rather than imported: server.js is
19
+ // the MCP entrypoint (constructs and owns the server instance as a side effect of import),
20
+ // not a library other modules should pull in just to read run-folder state.
21
+ function pendingStage(dir) {
22
+ if (!existsSync(dir)) return null;
23
+ const w = readdirSync(dir).filter(f => f.startsWith('NEEDS-')).map(f => f.slice(6, -3)).filter(l => !existsSync(join(dir, `${l}.md`)));
24
+ return w[0] || null;
25
+ }
26
+
27
+ function oneLineTaskSummary(taskPath) {
28
+ if (!taskPath || !existsSync(taskPath)) return '(task file not found on disk)';
29
+ const text = readFileSync(taskPath, 'utf8').trim();
30
+ const firstLine = text.split('\n').find(l => l.trim().length > 0) || text;
31
+ const clean = firstLine.replace(/^#+\s*/, '').trim();
32
+ return clean.length > 140 ? `${clean.slice(0, 137)}...` : clean;
33
+ }
34
+
35
+ /**
36
+ * Which stage-kind labels have at least one completed stage file on disk, in the order
37
+ * chain.js would have run them (per stageKindsFor). A "completed" label is any file
38
+ * `<label>.md` in the run folder whose label maps to a known stage kind - dedup by kind
39
+ * since chain.js emits one label per lab/round for several kinds.
40
+ */
41
+ function completedKinds(dir, chainConfig) {
42
+ if (!existsSync(dir)) return [];
43
+ const files = readdirSync(dir).filter(f => f.endsWith('.md') && !f.startsWith('NEEDS-') && f !== 'RESUME.md' && f !== 'BOARD.md' && f !== 'HANDOFF.md' && f !== 'deliverable.md' && f !== 'WARNINGS.md' && f !== 'proposals.md' && f !== 'proposals-pool.md');
44
+ const seen = new Set();
45
+ for (const f of files) {
46
+ const kind = stageKindOf(f.replace(/\.md$/, ''));
47
+ if (kind) seen.add(kind);
48
+ }
49
+ // Preserve chain-run order rather than filesystem order, so the brief reads as a timeline.
50
+ return stageKindsFor(chainConfig).filter(k => seen.has(k));
51
+ }
52
+
53
+ /**
54
+ * Build the resume brief for one run. Pure given its inputs: same run.json + same files on
55
+ * disk always produce the same text (aside from being able to note new files appearing).
56
+ */
57
+ export function generateResumeBrief({ runId, dir, runMeta, chainConfig }) {
58
+ const kindsInOrder = chainConfig ? stageKindsFor(chainConfig) : [];
59
+ const done = chainConfig ? completedKinds(dir, chainConfig) : [];
60
+ const pending = pendingStage(dir);
61
+ const deliverableDone = existsSync(join(dir, 'deliverable.md'));
62
+
63
+ const lines = [];
64
+ lines.push(`# RESUME BRIEF — run ${runId}`);
65
+ lines.push(`Task: ${oneLineTaskSummary(runMeta?.task)}`);
66
+ lines.push(`Status: ${deliverableDone ? kindsInOrder.length : done.length} of ${kindsInOrder.length || '?'} stages complete`);
67
+ lines.push('');
68
+ lines.push('## Decided so far');
69
+ if (done.length === 0) lines.push('- (nothing completed yet)');
70
+ else for (const kind of done) lines.push(`- ${kind}: complete`);
71
+ // v3 §2: a declared scope amendment goes on the record here - the one block every
72
+ // subsequent stage's prompt already reads - so a critic sees it instead of an
73
+ // unexplained late requirement.
74
+ const amendmentsPath = join(dir, 'AMENDMENTS.md');
75
+ if (existsSync(amendmentsPath)) {
76
+ lines.push('- Scope amendment(s) recorded, see below:');
77
+ lines.push(readFileSync(amendmentsPath, 'utf8').trim());
78
+ }
79
+ lines.push('');
80
+ lines.push('## Currently pending');
81
+ if (deliverableDone) {
82
+ lines.push('Stage: none - run is complete');
83
+ } else if (pending) {
84
+ lines.push(`Stage: ${pending}`);
85
+ lines.push('Waiting on: an external seat\'s reply (see NEEDS-<stage>.md, or dispatch it with prepare_stage_prompt)');
86
+ // v3 plan §1: surfaced here too, not only from external_prompt, so a returning session
87
+ // sees a stalled or contested claim without an extra tool call.
88
+ const claim = checkClaimStaleness(dir, pending);
89
+ lines.push(`Claim: ${claim ? claim.type : 'none'}`);
90
+ } else {
91
+ lines.push('Stage: in progress or between stages');
92
+ lines.push('Waiting on: nothing external right now');
93
+ }
94
+ lines.push(`Warnings: ${existsSync(join(dir, 'WARNINGS.md')) ? 'see WARNINGS.md' : 'none recorded'}`);
95
+ // v5 §1 candidate 3: read back from report.json rather than recomputing -
96
+ // by the time this runs (post-deliverable), the ledger is already settled
97
+ // and recorded there; a resume brief has no reason to duplicate the walk.
98
+ const reportPath = join(dir, 'report.json');
99
+ if (existsSync(reportPath)) {
100
+ try {
101
+ const report = JSON.parse(readFileSync(reportPath, 'utf8'));
102
+ if (report.orphanSections?.length) {
103
+ lines.push(`Orphaned withdrawals: ${report.withdrawalCycles} cycle(s), no surviving owner for ${report.orphanSections.join(', ')} - see BOARD.md`);
104
+ }
105
+ } catch { /* malformed report.json: say nothing rather than guess */ }
106
+ }
107
+ lines.push('');
108
+ lines.push('## Next action for the driving session');
109
+ if (deliverableDone) lines.push(`Run is complete. Deliverable at ${join(dir, 'deliverable.md')}.`);
110
+ else if (pending) lines.push(`Call prepare_stage_prompt(${JSON.stringify(runId)}, ${JSON.stringify(pending)}), or answer it directly and call submit_stage.`);
111
+ else lines.push(`Call resume_run(${JSON.stringify(runId)}) or run_status(${JSON.stringify(runId)}) to check current progress.`);
112
+
113
+ return `${lines.join('\n')}\n`;
114
+ }
@@ -0,0 +1,181 @@
1
+ // v6 §7: four failure-mode detectors, reconsidered against the corrected
2
+ // framing (Muad, 2026-09-13): the feature is not "catch more planted
3
+ // defects" - it is debate diversity among real model seats reasoning
4
+ // differently about a real infrastructure decision. A deterministic
5
+ // heuristic probe (v6 phase 4) cannot have a philosophy, so it cannot
6
+ // see the thing this feature is for. See docs/v6-decisions.md for the
7
+ // full argument: two of the plan's four detectors originally read
8
+ // per-seat catch-rate data from the phase 4 probe; both are redesigned
9
+ // here to read real debate-stage output (a run's own report.json)
10
+ // instead, so all four detectors measure something the feature's actual
11
+ // purpose can be judged by, not a proxy for a question the feature was
12
+ // never trying to answer.
13
+ //
14
+ // Every function here is pure and read-only: no ledger, nothing
15
+ // transmitted, computed from `report.debate.posts` on disk exactly the
16
+ // same way verdict-stats.js and cost-forecast.js already derive their
17
+ // numbers from run folders.
18
+
19
+ const QUOTE_RE = /"([^"]{3,})"/g;
20
+
21
+ function quotedSpans(text) {
22
+ return [...String(text || '').matchAll(QUOTE_RE)].map(m => m[1]);
23
+ }
24
+
25
+ function wordSet(text) {
26
+ return new Set(String(text || '').toLowerCase().match(/[a-z0-9]{4,}/g) || []);
27
+ }
28
+
29
+ function jaccard(a, b) {
30
+ if (!a.size && !b.size) return null;
31
+ const inter = [...a].filter(x => b.has(x)).length;
32
+ const union = new Set([...a, ...b]).size;
33
+ return union ? inter / union : null;
34
+ }
35
+
36
+ /**
37
+ * Per-seat (by `by`, the anonymised or real lab tag on each post)
38
+ * substance and evidence-share metrics, computed only from a run's own
39
+ * debate.posts. Returns `null` for a lab with no posts requiring a
40
+ * quote (support posts don't - DEBATE_SYSTEM only asks for one on
41
+ * object/merge), rather than a misleading 0.
42
+ *
43
+ * substanceRatio: v6 §7 failure mode 1, character-performed-not-
44
+ * reviewed. Fraction of a seat's object/merge posts that contain at
45
+ * least one quoted span, since DEBATE_SYSTEM's own instructions
46
+ * require quoting the phrase being objected to or merged - a seat that
47
+ * never quotes despite the prompt asking for it is arguing without
48
+ * engaging the text in front of it, whatever its role.
49
+ *
50
+ * evidenceTokenShare: v6 §7 failure mode 4, token spend shifting from
51
+ * substance to voice. Fraction of a seat's total post-text words that
52
+ * fall inside a quoted span, across every post regardless of stance.
53
+ */
54
+ export function perSeatMetrics(posts) {
55
+ const byLab = new Map();
56
+ for (const p of posts || []) {
57
+ if (!p.by) continue;
58
+ if (!byLab.has(p.by)) byLab.set(p.by, { evidencePosts: 0, quotedPosts: 0, totalWords: 0, quotedWords: 0 });
59
+ const m = byLab.get(p.by);
60
+ const text = String(p.text || '');
61
+ const words = (text.match(/\S+/g) || []).length;
62
+ m.totalWords += words;
63
+ const spans = quotedSpans(text);
64
+ const quotedWords = spans.reduce((s, q) => s + (q.match(/\S+/g) || []).length, 0);
65
+ m.quotedWords += quotedWords;
66
+ if (p.stance === 'object' || p.stance === 'merge') {
67
+ m.evidencePosts += 1;
68
+ if (spans.length) m.quotedPosts += 1;
69
+ }
70
+ }
71
+ const out = {};
72
+ for (const [lab, m] of byLab) {
73
+ out[lab] = {
74
+ substanceRatio: m.evidencePosts ? m.quotedPosts / m.evidencePosts : null,
75
+ evidenceTokenShare: m.totalWords ? m.quotedWords / m.totalWords : null,
76
+ };
77
+ }
78
+ return out;
79
+ }
80
+
81
+ /**
82
+ * v6 §7 failure mode 2, role-correlation collapse - reconsidered.
83
+ * Originally specified as a comparison against the phase 4 probe's
84
+ * plain-arm baseline; that baseline is a deterministic heuristic mock,
85
+ * not a real model, and cannot speak to real debate diversity. This
86
+ * instead measures the mean pairwise textual overlap (Jaccard over
87
+ * 4+ letter words) between different labs' posts on the SAME proposal,
88
+ * within one real debate stage - a lens meant to diversify argument
89
+ * that instead makes every seat's objection read the same is a real,
90
+ * checkable signal computed from what seats actually wrote. Returns
91
+ * `null` when there are fewer than two comparable posts (nothing to
92
+ * compare, not evidence of agreement).
93
+ */
94
+ export function pairwiseAgreement(posts) {
95
+ const byTarget = new Map();
96
+ for (const p of posts || []) {
97
+ if (!p.on || !p.by) continue;
98
+ if (!byTarget.has(p.on)) byTarget.set(p.on, []);
99
+ byTarget.get(p.on).push(p);
100
+ }
101
+ const scores = [];
102
+ for (const group of byTarget.values()) {
103
+ for (let i = 0; i < group.length; i++) {
104
+ for (let j = i + 1; j < group.length; j++) {
105
+ if (group[i].by === group[j].by) continue;
106
+ const score = jaccard(wordSet(group[i].text), wordSet(group[j].text));
107
+ if (score !== null) scores.push(score);
108
+ }
109
+ }
110
+ }
111
+ return scores.length ? scores.reduce((a, b) => a + b, 0) / scores.length : null;
112
+ }
113
+
114
+ /**
115
+ * v6 §7 failure mode 3, a role degrading a weaker model - reconsidered.
116
+ * Originally specified as a per-seat catch-rate delta read from the
117
+ * phase 4 probe; catch rate against planted synthetic defects is not
118
+ * what this feature is for (see module header). This instead compares,
119
+ * within the SAME real run, the mean substanceRatio (above) of seats
120
+ * carrying a role against seats that do not - a same-run, cross-
121
+ * sectional read, not a longitudinal one (it cannot tell you whether
122
+ * role-bearing seat X got worse than seat X used to be without a role,
123
+ * only whether role-bearing seats collectively argued less
124
+ * substantively than role-less seats in this one run). Returns `null`
125
+ * when a run has no mix of role-bearing and role-less seats to compare
126
+ * - a chain where every seat has the same role status has nothing this
127
+ * detector can say.
128
+ */
129
+ export function roleVsPlainSubstanceGap(perSeat, roleLabs) {
130
+ const roleSeats = [];
131
+ const plainSeats = [];
132
+ for (const [lab, m] of Object.entries(perSeat)) {
133
+ if (m.substanceRatio === null) continue;
134
+ (roleLabs.has(lab) ? roleSeats : plainSeats).push(m.substanceRatio);
135
+ }
136
+ if (!roleSeats.length || !plainSeats.length) return null;
137
+ const mean = xs => xs.reduce((a, b) => a + b, 0) / xs.length;
138
+ return mean(roleSeats) - mean(plainSeats);
139
+ }
140
+
141
+ // Placeholder thresholds, author to tighten (v6 Decisions left to the
142
+ // author already names this class of number as open) - not derived
143
+ // from any measurement, only from "clearly bad" being obviously below/
144
+ // above a midpoint.
145
+ const SUBSTANCE_FLOOR = 0.5;
146
+ const EVIDENCE_SHARE_FLOOR = 0.15;
147
+ const AGREEMENT_CEILING = 0.6;
148
+ const SUBSTANCE_GAP_FLOOR = -0.2;
149
+
150
+ /**
151
+ * The full diagnostics object for one run's `report.debate`, meant to
152
+ * be attached at `report.json`'s `debate.diagnostics` per v6 §7. Never
153
+ * throws on a run with no debate stage or no posts - returns nulls and
154
+ * empty flags, not an error, matching every other read-only reporting
155
+ * module in this project.
156
+ */
157
+ export function computeRoleDiagnostics(debate, roleLabs = new Set()) {
158
+ const posts = debate?.posts || [];
159
+ const perSeat = perSeatMetrics(posts);
160
+ const agreement = pairwiseAgreement(posts);
161
+ const substanceGap = roleVsPlainSubstanceGap(perSeat, roleLabs);
162
+
163
+ const characterPerformedNotReviewed = Object.entries(perSeat)
164
+ .filter(([, m]) => m.substanceRatio !== null && m.substanceRatio < SUBSTANCE_FLOOR)
165
+ .map(([lab]) => lab);
166
+ const tokenShiftToVoice = Object.entries(perSeat)
167
+ .filter(([, m]) => m.evidenceTokenShare !== null && m.evidenceTokenShare < EVIDENCE_SHARE_FLOOR)
168
+ .map(([lab]) => lab);
169
+
170
+ return {
171
+ perSeat,
172
+ pairwiseAgreement: agreement,
173
+ roleVsPlainSubstanceGap: substanceGap,
174
+ flags: {
175
+ characterPerformedNotReviewed,
176
+ tokenShiftToVoice,
177
+ roleCorrelationCollapse: agreement !== null && agreement > AGREEMENT_CEILING,
178
+ roleDegradesOutput: substanceGap !== null && substanceGap < SUBSTANCE_GAP_FLOOR,
179
+ },
180
+ };
181
+ }