@kontextmind/kxm 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 (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const cli = fileURLToPath(new URL("../plugins/kxm/dist/cli.js", import.meta.url));
7
+ const child = spawn(process.execPath, [
8
+ "--disable-warning=ExperimentalWarning",
9
+ cli,
10
+ ...process.argv.slice(2),
11
+ ], {
12
+ stdio: "inherit",
13
+ env: process.env,
14
+ });
15
+
16
+ child.once("error", (error) => {
17
+ process.stderr.write(`Failed to start kxm: ${error.message}\n`);
18
+ process.exitCode = 1;
19
+ });
20
+
21
+ child.once("exit", (code, signal) => {
22
+ if (signal) {
23
+ process.kill(process.pid, signal);
24
+ return;
25
+ }
26
+ process.exitCode = code ?? 1;
27
+ });
@@ -0,0 +1,20 @@
1
+ export interface RosterRoute {
2
+ readonly harness: 'grok' | 'claude' | 'codex' | 'pi';
3
+ readonly model: string;
4
+ readonly vendor: string;
5
+ readonly roles: readonly string[];
6
+ readonly permissions: readonly ('read-only' | 'edit')[];
7
+ readonly status: 'admitted' | 'retired';
8
+ }
9
+ export interface RosterPolicy {
10
+ readonly schema: 'kxm.developer-roster.v1';
11
+ readonly routes: Readonly<Record<string, RosterRoute>>;
12
+ readonly lineup: Readonly<Record<string, readonly string[]>>;
13
+ readonly required_critics: Readonly<Record<'review-arch' | 'review-cli', string>>;
14
+ readonly model_origins: Readonly<Record<string, { readonly vendor: string; readonly evidence: { readonly source: string; readonly sha256: string } }>>;
15
+ }
16
+ export interface PolicyIdentity { readonly commit: string; readonly blob: string; readonly sha256: string }
17
+ export interface LoadedPolicy { readonly identity: PolicyIdentity; readonly policy: RosterPolicy }
18
+ export function loadTrustedRosterPolicy(): LoadedPolicy;
19
+ export function resolveBoundPolicy(identity: PolicyIdentity): LoadedPolicy;
20
+ export function canonicalVendor(value: string): string;
@@ -0,0 +1,161 @@
1
+ /** Unwired developer policy foundation. No dispatch or acceptance authority. */
2
+ import { execFileSync } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { lstatSync, readFileSync, realpathSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { NATIVE_PI_BRAKE_PROVIDERS, PI_ALLOWED_PROVIDERS, ROUTES } from './harness-run.mjs';
8
+
9
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const POLICY = '.kxm/roster.json';
11
+ const TRUSTED = 'refs/remotes/origin/main';
12
+ const ROLES = ['writer', 'planner', 'reviewer-arch', 'reviewer-cli', 'experiment'];
13
+ const ALIASES = Object.freeze({ 'x-ai': 'xai', moonshotai: 'moonshot', 'google-ai': 'google', qwen: 'alibaba' });
14
+ const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
15
+ const refuse = message => { throw new Error(`Roster policy refused: ${message}`); };
16
+ const git = (...args) => {
17
+ try { return execFileSync('git', args, { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] }); }
18
+ catch { return refuse(`Git evidence unavailable (${args[0]})`); }
19
+ };
20
+ const gitText = (...args) => git(...args).toString('utf8').trim();
21
+ export const canonicalVendor = value => {
22
+ if (typeof value !== 'string') return '';
23
+ const lower = value.toLowerCase();
24
+ return ALIASES[lower] ?? lower;
25
+ };
26
+ const canonical = canonicalVendor;
27
+ const own = (obj, key) => Object.hasOwn(obj, key);
28
+ function record(value, label) {
29
+ if (!value || typeof value !== 'object' || Array.isArray(value)) refuse(`${label} must be an object`);
30
+ }
31
+ function keys(value, expected, label) {
32
+ record(value, label);
33
+ if (Object.keys(value).length !== expected.length || expected.some(key => !own(value, key))) refuse(`${label} has missing or unknown keys`);
34
+ }
35
+ function text(value, label) {
36
+ if (typeof value !== 'string' || !value || value.length > 512 || /[\s\x00-\x1f]/u.test(value)) refuse(`${label} must be a nonempty token`);
37
+ }
38
+ function list(value, allowed, label) {
39
+ if (!Array.isArray(value) || !value.length || value.some(item => !allowed.includes(item)) || new Set(value).size !== value.length) refuse(`${label} contains unsupported or duplicate values`);
40
+ }
41
+ function sourcePath(value) {
42
+ text(value, 'source path');
43
+ if (path.posix.isAbsolute(value) || value.includes('\\') || value.split('/').some(part => !part || part === '.' || part === '..')) refuse('source path must be contained and relative');
44
+ return value;
45
+ }
46
+ function objectId(value, label) {
47
+ if (typeof value !== 'string' || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value)) refuse(`invalid ${label}`);
48
+ }
49
+ function digest(value) {
50
+ if (typeof value !== 'string' || !/^[a-f0-9]{64}$/u.test(value)) refuse('invalid SHA256');
51
+ }
52
+ function frozen(value) {
53
+ if (value && typeof value === 'object') { for (const child of Object.values(value)) frozen(child); Object.freeze(value); }
54
+ return value;
55
+ }
56
+ function control() {
57
+ if (realpathSync(gitText('rev-parse', '--show-toplevel')) !== realpathSync(ROOT)) refuse('module is outside its control repository');
58
+ const head = gitText('rev-parse', '--verify', 'HEAD^{commit}');
59
+ const trusted = gitText('rev-parse', '--verify', `${TRUSTED}^{commit}`);
60
+ git('merge-base', '--is-ancestor', head, trusted);
61
+ const flags = git('ls-files', '-v', '-z').toString('utf8').split('\0').filter(Boolean);
62
+ if (flags.some(entry => entry.slice(2) !== POLICY && (entry[0] === 'S' || entry[0] === entry[0].toLowerCase()))) refuse('hidden index flags on control source');
63
+ if (git('status', '--porcelain=v1', '--untracked-files=all').length) refuse('dirty or untracked control source');
64
+ return { head, trusted };
65
+ }
66
+ function unchanged(before) {
67
+ const after = control();
68
+ if (before.head !== after.head || before.trusted !== after.trusted) refuse('control changed while reading policy');
69
+ }
70
+ function blobAt(commit, source) {
71
+ sourcePath(source);
72
+ const entry = git('ls-tree', '-z', commit, '--', source).toString('utf8');
73
+ const match = /^(100644|100755) blob ([a-f0-9]{40}(?:[a-f0-9]{24})?)\t([^\0]+)\0$/u.exec(entry);
74
+ if (!match || match[3] !== source) refuse('source is missing or not a regular committed file');
75
+ return { blob: match[2], bytes: git('cat-file', 'blob', match[2]) };
76
+ }
77
+ function workingBytes(source) {
78
+ let current = ROOT;
79
+ const parts = sourcePath(source).split('/');
80
+ for (let i = 0; i < parts.length; i++) {
81
+ current = path.join(current, parts[i]);
82
+ const info = lstatSync(current);
83
+ if (info.isSymbolicLink() || (i === parts.length - 1 ? !info.isFile() : !info.isDirectory())) refuse('working policy must be a regular contained file');
84
+ }
85
+ return readFileSync(current);
86
+ }
87
+ function validate(bytes, commit) {
88
+ let policy;
89
+ try { policy = JSON.parse(bytes.toString('utf8')); } catch { refuse('invalid policy JSON'); }
90
+ keys(policy, ['schema', 'routes', 'lineup', 'required_critics', 'model_origins'], 'policy');
91
+ if (policy.schema !== 'kxm.developer-roster.v1') refuse('unsupported schema');
92
+ record(policy.routes, 'routes'); record(policy.lineup, 'lineup'); record(policy.model_origins, 'model origins');
93
+ if (!Object.keys(policy.routes).length) refuse('empty routes');
94
+ for (const [model, origin] of Object.entries(policy.model_origins)) {
95
+ text(model, 'origin model'); keys(origin, ['vendor', 'evidence'], 'origin');
96
+ text(origin.vendor, 'origin vendor');
97
+ if (PI_ALLOWED_PROVIDERS.includes(canonical(origin.vendor.toLowerCase()))) refuse('origin/vendor must name the model vendor, not billing provider');
98
+ keys(origin.evidence, ['source', 'sha256'], 'origin evidence'); digest(origin.evidence.sha256);
99
+ if (sha256(blobAt(commit, origin.evidence.source).bytes) !== origin.evidence.sha256) refuse('origin evidence hash mismatch');
100
+ }
101
+ for (const [id, route] of Object.entries(policy.routes)) {
102
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(id)) refuse('invalid route id');
103
+ keys(route, ['harness', 'model', 'vendor', 'roles', 'permissions', 'status'], 'route');
104
+ text(route.harness, 'harness'); text(route.model, 'model'); text(route.vendor, 'vendor');
105
+ if (!own(ROUTES, route.harness)) refuse('unsupported harness');
106
+ const ceiling = ROUTES[route.harness];
107
+ list(route.roles, ceiling.roles, 'roles'); list(route.permissions, ceiling.permissions, 'permissions');
108
+ if (!['admitted', 'retired'].includes(route.status)) refuse('unsupported status');
109
+ if (route.harness === 'pi') {
110
+ const parts = route.model.split('/');
111
+ if (parts.length < 3 || !PI_ALLOWED_PROVIDERS.includes(parts[0]) || parts.some(part => !part)) refuse('unsupported Pi provider/model');
112
+ const prefix = canonical(parts[1].toLowerCase());
113
+ const vendor = canonical(route.vendor.toLowerCase());
114
+ if (NATIVE_PI_BRAKE_PROVIDERS.includes(prefix) || NATIVE_PI_BRAKE_PROVIDERS.includes(vendor)) refuse('native vendor cannot use Pi');
115
+ if (!own(policy.model_origins, route.model)) refuse('missing exact model origin');
116
+ const origin = policy.model_origins[route.model];
117
+ if (canonical(origin.vendor.toLowerCase()) !== vendor || PI_ALLOWED_PROVIDERS.includes(vendor)) refuse('model origin/vendor mismatch');
118
+ if (route.roles.includes('writer') && (route.permissions.length !== 1 || route.permissions[0] !== 'edit')) refuse('Pi writer requires edit permission only');
119
+ if (route.permissions.includes('edit') && route.roles.some(role => !['writer', 'experiment'].includes(role))) refuse('Pi critic/planner cannot edit');
120
+ } else if (canonical(route.vendor.toLowerCase()) !== ceiling.provider || route.model.includes('/')) refuse('native route vendor/model mismatch');
121
+ }
122
+ for (const [role, ids] of Object.entries(policy.lineup)) {
123
+ if (!ROLES.includes(role)) refuse('unsupported lineup role');
124
+ list(ids, Object.keys(policy.routes), 'lineup');
125
+ if (ids.some(id => policy.routes[id].status !== 'admitted' || !policy.routes[id].roles.includes(role))) refuse('lineup route not admitted for role');
126
+ }
127
+ for (const role of ['writer', 'planner', 'reviewer-arch', 'reviewer-cli']) if (!own(policy.lineup, role)) refuse('required lineup missing');
128
+ keys(policy.required_critics, ['review-arch', 'review-cli'], 'required critics');
129
+ const critics = [];
130
+ for (const [kind, role] of [['review-arch', 'reviewer-arch'], ['review-cli', 'reviewer-cli']]) {
131
+ const id = policy.required_critics[kind];
132
+ if (typeof id !== 'string' || !policy.lineup[role].includes(id)) refuse('required critic not in admitted lineup');
133
+ const route = policy.routes[id];
134
+ if (route.permissions.length !== 1 || route.permissions[0] !== 'read-only') refuse('critic must be read-only');
135
+ critics.push(canonical(route.vendor.toLowerCase()));
136
+ }
137
+ if (critics[0] === critics[1]) refuse('critics must have independent vendors');
138
+ for (const route of Object.values(policy.routes)) {
139
+ if (route.status === 'admitted' && route.roles.includes('writer') && critics.includes(canonical(route.vendor.toLowerCase()))) refuse('writer and critics must have independent vendors');
140
+ }
141
+ return policy;
142
+ }
143
+ export function loadTrustedRosterPolicy() {
144
+ const snapshot = control();
145
+ const { blob, bytes } = blobAt(snapshot.head, POLICY);
146
+ if (!workingBytes(POLICY).equals(bytes)) refuse('working policy differs from committed bytes');
147
+ const policy = validate(bytes, snapshot.head);
148
+ unchanged(snapshot);
149
+ return frozen({ identity: { commit: snapshot.head, blob, sha256: sha256(bytes) }, policy });
150
+ }
151
+ export function resolveBoundPolicy(identity) {
152
+ keys(identity, ['commit', 'blob', 'sha256'], 'identity');
153
+ objectId(identity.commit, 'commit'); objectId(identity.blob, 'blob'); digest(identity.sha256);
154
+ const snapshot = control();
155
+ git('merge-base', '--is-ancestor', identity.commit, snapshot.trusted);
156
+ const { blob, bytes } = blobAt(identity.commit, POLICY);
157
+ if (blob !== identity.blob || sha256(bytes) !== identity.sha256) refuse('bound identity mismatch');
158
+ const policy = validate(bytes, identity.commit);
159
+ unchanged(snapshot);
160
+ return frozen({ identity: { ...identity }, policy });
161
+ }