@kontextmind/kxm 0.6.0 → 0.7.10

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 (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +7 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -0,0 +1,193 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { randomBytes } from "node:crypto";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+
6
+ export const HUB_ENV_SCHEMA = "kxm.hub-env.v1" as const;
7
+
8
+ export interface HubEnvRecord {
9
+ schema: typeof HUB_ENV_SCHEMA;
10
+ /** ISO-8601 creation time of the persisted record. */
11
+ createdAt: string;
12
+ authToken?: string | undefined;
13
+ projectTokens?: Record<string, string> | undefined;
14
+ }
15
+
16
+ export class HubEnvError extends Error {
17
+ constructor(message: string) {
18
+ super(message);
19
+ this.name = "HubEnvError";
20
+ }
21
+ }
22
+
23
+ function resolveUserStateRoot(env: NodeJS.ProcessEnv): string {
24
+ const explicit = env.KXM_STATE_HOME?.trim();
25
+ if (explicit) {
26
+ if (!isAbsolute(explicit)) throw new HubEnvError("local_state_root_not_absolute");
27
+ return resolve(explicit);
28
+ }
29
+ if (process.platform === "win32") {
30
+ const localAppData = env.LOCALAPPDATA?.trim();
31
+ const base = localAppData && isAbsolute(localAppData) ? localAppData : join(homedir(), "AppData", "Local");
32
+ return resolve(base, "KXM");
33
+ }
34
+ if (process.platform === "darwin") return resolve(homedir(), "Library", "Application Support", "KXM");
35
+ const xdgState = env.XDG_STATE_HOME?.trim();
36
+ const base = xdgState && isAbsolute(xdgState) ? xdgState : join(homedir(), ".local", "state");
37
+ return resolve(base, "kxm");
38
+ }
39
+
40
+ export function hubEnvFile(env: NodeJS.ProcessEnv = process.env): string {
41
+ return join(resolveUserStateRoot(env), "hub-env.json");
42
+ }
43
+
44
+ export function generateHubAuthToken(): string {
45
+ return `kxm_admin_${randomBytes(24).toString("base64url")}`;
46
+ }
47
+
48
+ export function readHubEnvRecord(env: NodeJS.ProcessEnv = process.env): HubEnvRecord | undefined {
49
+ const file = hubEnvFile(env);
50
+ if (!existsSync(file)) return undefined;
51
+ let parsed: unknown;
52
+ try {
53
+ parsed = JSON.parse(readFileSync(file, "utf8"));
54
+ } catch (error) {
55
+ throw new HubEnvError(`hub env file is malformed at ${file}: ${error instanceof Error ? error.message : String(error)}`);
56
+ }
57
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
58
+ throw new HubEnvError(`hub env file is malformed at ${file}: expected a JSON object`);
59
+ }
60
+ const record = parsed as Partial<HubEnvRecord>;
61
+ if (record.schema !== HUB_ENV_SCHEMA) {
62
+ throw new HubEnvError(`hub env file at ${file} has unsupported schema ${String(record.schema)}`);
63
+ }
64
+ if (record.authToken !== undefined && (typeof record.authToken !== "string" || record.authToken.trim().length === 0)) {
65
+ throw new HubEnvError(`hub env file at ${file} has an invalid authToken`);
66
+ }
67
+ if (record.projectTokens !== undefined) {
68
+ const tokens = record.projectTokens;
69
+ if (!tokens || typeof tokens !== "object" || Array.isArray(tokens)) {
70
+ throw new HubEnvError(`hub env file at ${file} has invalid projectTokens`);
71
+ }
72
+ for (const [project, token] of Object.entries(tokens)) {
73
+ if (!project.trim() || typeof token !== "string" || !token.trim()) {
74
+ throw new HubEnvError(`hub env file at ${file} has an empty project name or token`);
75
+ }
76
+ }
77
+ }
78
+ return {
79
+ schema: HUB_ENV_SCHEMA,
80
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
81
+ ...(record.authToken !== undefined ? { authToken: record.authToken } : {}),
82
+ ...(record.projectTokens !== undefined ? { projectTokens: record.projectTokens } : {}),
83
+ };
84
+ }
85
+
86
+ export function writeHubEnvRecord(record: HubEnvRecord, env: NodeJS.ProcessEnv = process.env): string {
87
+ const file = hubEnvFile(env);
88
+ const directory = dirname(file);
89
+ mkdirSync(directory, { recursive: true });
90
+ const temporary = join(directory, `.hub-env-${process.pid}.tmp`);
91
+ writeFileSync(temporary, `${JSON.stringify(record, undefined, 2)}\n`, { encoding: "utf8", mode: 0o600 });
92
+ try {
93
+ renameSync(temporary, file);
94
+ } catch (error) {
95
+ rmSync(temporary, { force: true });
96
+ throw error;
97
+ }
98
+ return file;
99
+ }
100
+
101
+ export interface ResolveHubCredentialsOptions {
102
+ env?: NodeJS.ProcessEnv | undefined;
103
+ /** Injectable token generator for tests. */
104
+ generateToken?: () => string | undefined;
105
+ /** When false, never create or update the persisted file. */
106
+ persist?: boolean | undefined;
107
+ now?: () => string | undefined;
108
+ }
109
+
110
+ export interface ResolvedHubCredentials {
111
+ authToken: string | undefined;
112
+ projectTokens: Record<string, string> | undefined;
113
+ /** Where each value came from; `generated` values were persisted when allowed. */
114
+ authTokenSource: "env" | "file" | "generated" | "none";
115
+ projectTokensSource: "env" | "file" | "generated" | "none";
116
+ envFile: string | undefined;
117
+ written: boolean;
118
+ }
119
+
120
+ function parseProjectTokens(raw: string | undefined): Record<string, string> | undefined | "invalid" {
121
+ const trimmed = raw?.trim();
122
+ if (!trimmed) return undefined;
123
+ let parsed: unknown;
124
+ try {
125
+ parsed = JSON.parse(trimmed);
126
+ } catch {
127
+ return "invalid";
128
+ }
129
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "invalid";
130
+ const entries = Object.entries(parsed as Record<string, unknown>);
131
+ if (entries.some(([project, token]) => !project.trim() || typeof token !== "string" || !token.trim())) return "invalid";
132
+ return Object.fromEntries(entries) as Record<string, string>;
133
+ }
134
+
135
+ /** Resolve hub credentials with persistence across restarts.
136
+ *
137
+ * Precedence: explicit environment variable, then the persisted user-state
138
+ * file, then (for the admin token only) a freshly generated token that is
139
+ * persisted so later restarts and workers reuse the same credential. */
140
+ export function resolveHubCredentials(options: ResolveHubCredentialsOptions = {}): ResolvedHubCredentials {
141
+ const env = options.env ?? process.env;
142
+ const persist = options.persist !== false;
143
+ const existing = readHubEnvRecord(env);
144
+ const envFile = hubEnvFile(env);
145
+
146
+ const envAuthToken = env.KXM_AUTH_TOKEN?.trim();
147
+ const envProjectTokens = parseProjectTokens(env.KXM_PROJECT_TOKENS);
148
+ if (envProjectTokens === "invalid") {
149
+ throw new HubEnvError("KXM_PROJECT_TOKENS must be a JSON object of project names to tokens");
150
+ }
151
+
152
+ let authTokenSource: ResolvedHubCredentials["authTokenSource"] = "none";
153
+ let authToken = envAuthToken || existing?.authToken;
154
+ if (envAuthToken) authTokenSource = "env";
155
+ else if (authToken) authTokenSource = "file";
156
+
157
+ if (!authToken) {
158
+ const generated = options.generateToken?.() ?? generateHubAuthToken();
159
+ if (generated) {
160
+ authToken = generated;
161
+ authTokenSource = "generated";
162
+ }
163
+ }
164
+
165
+ let projectTokensSource: ResolvedHubCredentials["projectTokensSource"] = "none";
166
+ let projectTokens = envProjectTokens ?? existing?.projectTokens;
167
+ if (envProjectTokens) projectTokensSource = "env";
168
+ else if (projectTokens) projectTokensSource = "file";
169
+
170
+ const needsPersist = persist
171
+ && (authTokenSource === "generated"
172
+ || (authTokenSource === "env" && existing?.authToken !== authToken)
173
+ || (projectTokensSource === "env" && JSON.stringify(existing?.projectTokens ?? undefined) !== JSON.stringify(projectTokens ?? undefined)));
174
+
175
+ if (needsPersist) {
176
+ const record: HubEnvRecord = {
177
+ schema: HUB_ENV_SCHEMA,
178
+ createdAt: existing?.createdAt || (options.now?.() ?? new Date().toISOString()),
179
+ ...(authToken ? { authToken } : {}),
180
+ ...(projectTokens ? { projectTokens } : {}),
181
+ };
182
+ writeHubEnvRecord(record, env);
183
+ }
184
+
185
+ return {
186
+ authToken,
187
+ projectTokens,
188
+ authTokenSource,
189
+ projectTokensSource,
190
+ envFile: needsPersist || existing ? envFile : undefined,
191
+ written: needsPersist === true,
192
+ };
193
+ }
@@ -29,6 +29,7 @@ import {
29
29
  type WorkflowMessageContext,
30
30
  } from "./protocol.ts";
31
31
  import { workflowScopeExtras } from "./diagnostics.ts";
32
+ import { timingSafeStringCompare } from "./commands.ts";
32
33
  import { arbitrate, explainContextItem, journalEntryToContextItem, memoryRecordToContextItem, rolePolicy } from "./arbiter.ts";
33
34
  import { loadAuthoredMemory } from "./memory.ts";
34
35
  import { contextItemAuditMetadata, CONTEXT_AUTHORITIES, CONTEXT_CONFIDENCES, type ContextAuthority, type ContextConfidence, type ContextItem } from "./context.ts";
@@ -123,10 +124,7 @@ function publicAgent(agent: StoredAgent): AgentRecord {
123
124
  }
124
125
 
125
126
  function safeTokenEqual(actual: string | undefined, expected: string): boolean {
126
- if (!actual) return false;
127
- const actualHash = createHash("sha256").update(actual).digest();
128
- const expectedHash = createHash("sha256").update(expected).digest();
129
- return timingSafeEqual(actualHash, expectedHash);
127
+ return timingSafeStringCompare(actual, expected);
130
128
  }
131
129
 
132
130
  function bearerToken(request: IncomingMessage): string | undefined {
@@ -381,3 +381,75 @@ export function formatImprovementReport(report: ImprovementReport): string {
381
381
 
382
382
  return lines.join("\n");
383
383
  }
384
+
385
+ export interface PromotionDecision {
386
+ eligible: boolean;
387
+ policy: "manual_pr" | "critic_quorum" | "auto_threshold";
388
+ authorized: boolean;
389
+ reason: string;
390
+ }
391
+
392
+ /**
393
+ * Evaluates candidate promotion policy (Decision Q11).
394
+ * Supports:
395
+ * - manual_pr: strict operator signoff via Git PR / CLI (fail-closed anti-privilege-escalation)
396
+ * - critic_quorum: requires dual critic approval before auto-promotion
397
+ * - auto_threshold: requires candidate to exceed recurrence and verifyPassRate thresholds
398
+ */
399
+ export function evaluatePromotionPolicy(
400
+ candidate: ImprovementCandidate,
401
+ policy: "manual_pr" | "critic_quorum" | "auto_threshold" = "manual_pr",
402
+ options?: {
403
+ criticApprovals?: string[] | undefined;
404
+ autoThreshold?: {
405
+ minRuns: number;
406
+ minPassRate: number;
407
+ minCostSavings?: number | undefined;
408
+ } | undefined;
409
+ },
410
+ ): PromotionDecision {
411
+ if (policy === "manual_pr") {
412
+ return {
413
+ eligible: true,
414
+ policy,
415
+ authorized: false,
416
+ reason: "Manual PR review and signoff required by policy (fail-closed anti-privilege-escalation)",
417
+ };
418
+ }
419
+
420
+ if (policy === "critic_quorum") {
421
+ const approvals = options?.criticApprovals ?? [];
422
+ const hasQuorum = approvals.length >= 2;
423
+ return {
424
+ eligible: true,
425
+ policy,
426
+ authorized: hasQuorum,
427
+ reason: hasQuorum
428
+ ? `Authorized by critic quorum (${approvals.join(", ")})`
429
+ : `Requires dual critic quorum; current approvals: ${approvals.length}/2`,
430
+ };
431
+ }
432
+
433
+ if (policy === "auto_threshold") {
434
+ const threshold = options?.autoThreshold ?? { minRuns: 10, minPassRate: 0.95 };
435
+ const recurrence = candidate.baselineMetrics.recurrence;
436
+ const passRate = candidate.baselineMetrics.verifyPassRate;
437
+ const meetsThreshold = recurrence >= threshold.minRuns && passRate >= threshold.minPassRate;
438
+
439
+ return {
440
+ eligible: true,
441
+ policy,
442
+ authorized: meetsThreshold,
443
+ reason: meetsThreshold
444
+ ? `Authorized by auto-threshold (runs=${recurrence}>=${threshold.minRuns}, passRate=${passRate}>=${threshold.minPassRate})`
445
+ : `Auto-threshold not met: runs=${recurrence}/${threshold.minRuns}, passRate=${passRate}/${threshold.minPassRate}`,
446
+ };
447
+ }
448
+
449
+ return {
450
+ eligible: false,
451
+ policy,
452
+ authorized: false,
453
+ reason: `Unknown promotion policy: ${String(policy)}`,
454
+ };
455
+ }