toolroll 0.9.81 → 0.9.83

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 (168) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cli.js +1 -0
  3. package/dist/commit-extract.d.ts +18 -0
  4. package/dist/commit-extract.js +39 -1
  5. package/dist/converse.d.ts +12 -0
  6. package/dist/converse.js +18 -2
  7. package/dist/deploy-gate.js +8 -0
  8. package/dist/desktop-update.d.ts +1 -1
  9. package/dist/eval-gate.d.ts +51 -0
  10. package/dist/eval-gate.js +199 -0
  11. package/dist/eval-hooks.d.ts +39 -0
  12. package/dist/eval-hooks.js +477 -0
  13. package/dist/eval-oracle-main.d.ts +1 -0
  14. package/dist/eval-oracle-main.js +31 -0
  15. package/dist/eval-oracle.d.ts +60 -0
  16. package/dist/eval-oracle.js +217 -0
  17. package/dist/eval-world.d.ts +17 -0
  18. package/dist/eval-world.js +97 -0
  19. package/dist/evals.d.ts +299 -0
  20. package/dist/evals.js +964 -0
  21. package/dist/exec.js +15 -1
  22. package/dist/first-tasks-child.d.ts +1 -0
  23. package/dist/first-tasks-child.js +11 -0
  24. package/dist/flow-draft.js +4 -0
  25. package/dist/flow-sort.js +4 -0
  26. package/dist/folder-look.d.ts +107 -0
  27. package/dist/folder-look.js +302 -0
  28. package/dist/github-jobs.d.ts +25 -0
  29. package/dist/github-jobs.js +22 -0
  30. package/dist/invoke.d.ts +1 -1
  31. package/dist/invoke.js +14 -0
  32. package/dist/lead.js +4 -1
  33. package/dist/learning-child.d.ts +1 -0
  34. package/dist/learning-child.js +34 -0
  35. package/dist/learning-look.d.ts +25 -0
  36. package/dist/learning-look.js +72 -0
  37. package/dist/memory-pass.js +4 -0
  38. package/dist/nightly-check.d.ts +4 -1
  39. package/dist/nightly-check.js +5 -2
  40. package/dist/nightly-github.d.ts +316 -0
  41. package/dist/nightly-github.js +1022 -0
  42. package/dist/operate.d.ts +123 -1
  43. package/dist/operate.js +344 -29
  44. package/dist/project-identity.d.ts +37 -0
  45. package/dist/project-identity.js +106 -0
  46. package/dist/project-knowledge.d.ts +4 -4
  47. package/dist/project-tools.d.ts +4 -1
  48. package/dist/project-tools.js +7 -2
  49. package/dist/project.d.ts +4 -1
  50. package/dist/project.js +11 -4
  51. package/dist/provider.d.ts +3 -0
  52. package/dist/provider.js +9 -2
  53. package/dist/release-adapters.d.ts +21 -21
  54. package/dist/release-adapters.js +52 -19
  55. package/dist/release-auto.js +26 -0
  56. package/dist/release-card.js +3 -0
  57. package/dist/release-cli.js +59 -3
  58. package/dist/release-view.d.ts +3 -0
  59. package/dist/release-view.js +2 -0
  60. package/dist/release.d.ts +16 -0
  61. package/dist/release.js +16 -0
  62. package/dist/reported-usage.d.ts +69 -0
  63. package/dist/reported-usage.js +97 -0
  64. package/dist/repos.d.ts +7 -0
  65. package/dist/repos.js +57 -0
  66. package/dist/serve.js +35 -8
  67. package/dist/server/chat.js +4 -0
  68. package/dist/server/flows.js +9 -3
  69. package/dist/server/guards.d.ts +4 -0
  70. package/dist/server/guards.js +12 -3
  71. package/dist/server/http.d.ts +11 -0
  72. package/dist/server/http.js +3 -0
  73. package/dist/server/pages.js +62 -88
  74. package/dist/server/render-pages.d.ts +2 -16
  75. package/dist/server/render-pages.js +1 -55
  76. package/dist/server/render-tasks.js +1 -0
  77. package/dist/server/runtime.d.ts +4 -0
  78. package/dist/server/runtime.js +1 -0
  79. package/dist/server/settings.js +14 -3
  80. package/dist/server/tasks.js +15 -4
  81. package/dist/store.d.ts +109 -1
  82. package/dist/store.js +394 -4
  83. package/dist/subagents.js +4 -0
  84. package/dist/subscription-chat.d.ts +2 -0
  85. package/dist/subscription-chat.js +52 -13
  86. package/dist/task-sizing.js +4 -0
  87. package/dist/teardown-git.d.ts +9 -2
  88. package/dist/teardown-git.js +56 -33
  89. package/dist/up-processes.d.ts +13 -0
  90. package/dist/up-processes.js +6 -2
  91. package/evals/cases/catalog/case.json +25 -0
  92. package/evals/cases/catalog/fixture/README.md +3 -0
  93. package/evals/cases/catalog/fixture/package.json +8 -0
  94. package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
  95. package/evals/cases/catalog/oracle.mjs +23 -0
  96. package/evals/cases/catalog/probe.mjs +10 -0
  97. package/evals/cases/catalog/source.md +9 -0
  98. package/evals/cases/csv-quoting/case.json +23 -0
  99. package/evals/cases/csv-quoting/fixture/README.md +3 -0
  100. package/evals/cases/csv-quoting/fixture/csv.js +1 -0
  101. package/evals/cases/csv-quoting/fixture/package.json +8 -0
  102. package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
  103. package/evals/cases/csv-quoting/oracle.mjs +14 -0
  104. package/evals/cases/csv-quoting/probe.mjs +9 -0
  105. package/evals/cases/csv-quoting/source.md +9 -0
  106. package/evals/cases/dedupe/case.json +23 -0
  107. package/evals/cases/dedupe/fixture/README.md +3 -0
  108. package/evals/cases/dedupe/fixture/dedupe.js +1 -0
  109. package/evals/cases/dedupe/fixture/package.json +8 -0
  110. package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
  111. package/evals/cases/dedupe/oracle.mjs +16 -0
  112. package/evals/cases/dedupe/probe.mjs +13 -0
  113. package/evals/cases/dedupe/source.md +9 -0
  114. package/evals/cases/endpoint/case.json +25 -0
  115. package/evals/cases/endpoint/fixture/README.md +3 -0
  116. package/evals/cases/endpoint/fixture/package.json +8 -0
  117. package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
  118. package/evals/cases/endpoint/oracle.mjs +27 -0
  119. package/evals/cases/endpoint/probe.mjs +10 -0
  120. package/evals/cases/endpoint/source.md +11 -0
  121. package/evals/cases/math-divide/case.json +23 -0
  122. package/evals/cases/math-divide/fixture/README.md +3 -0
  123. package/evals/cases/math-divide/fixture/package.json +8 -0
  124. package/evals/cases/math-divide/fixture/src/math.js +1 -0
  125. package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
  126. package/evals/cases/math-divide/oracle.mjs +13 -0
  127. package/evals/cases/math-divide/probe.mjs +9 -0
  128. package/evals/cases/math-divide/source.md +9 -0
  129. package/evals/cases/math-multiply/case.json +23 -0
  130. package/evals/cases/math-multiply/fixture/README.md +3 -0
  131. package/evals/cases/math-multiply/fixture/package.json +8 -0
  132. package/evals/cases/math-multiply/fixture/src/math.js +2 -0
  133. package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
  134. package/evals/cases/math-multiply/oracle.mjs +12 -0
  135. package/evals/cases/math-multiply/probe.mjs +9 -0
  136. package/evals/cases/math-multiply/source.md +9 -0
  137. package/evals/cases/math-subtract/case.json +23 -0
  138. package/evals/cases/math-subtract/fixture/README.md +3 -0
  139. package/evals/cases/math-subtract/fixture/package.json +8 -0
  140. package/evals/cases/math-subtract/fixture/src/math.js +1 -0
  141. package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
  142. package/evals/cases/math-subtract/oracle.mjs +12 -0
  143. package/evals/cases/math-subtract/probe.mjs +9 -0
  144. package/evals/cases/math-subtract/source.md +9 -0
  145. package/evals/cases/merge-config/case.json +23 -0
  146. package/evals/cases/merge-config/fixture/README.md +3 -0
  147. package/evals/cases/merge-config/fixture/config.js +1 -0
  148. package/evals/cases/merge-config/fixture/package.json +8 -0
  149. package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
  150. package/evals/cases/merge-config/oracle.mjs +28 -0
  151. package/evals/cases/merge-config/probe.mjs +15 -0
  152. package/evals/cases/merge-config/source.md +9 -0
  153. package/evals/cases/paginate/case.json +23 -0
  154. package/evals/cases/paginate/fixture/README.md +3 -0
  155. package/evals/cases/paginate/fixture/package.json +8 -0
  156. package/evals/cases/paginate/fixture/paginate.js +1 -0
  157. package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
  158. package/evals/cases/paginate/oracle.mjs +18 -0
  159. package/evals/cases/paginate/probe.mjs +13 -0
  160. package/evals/cases/paginate/source.md +9 -0
  161. package/evals/cases/round2-asks/case.json +24 -0
  162. package/evals/cases/round2-asks/fixture/README.md +3 -0
  163. package/evals/cases/round2-asks/fixture/package.json +8 -0
  164. package/evals/cases/round2-asks/fixture/src/math.js +1 -0
  165. package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
  166. package/evals/cases/round2-asks/source.md +11 -0
  167. package/evals/manifest.json +15 -0
  168. package/package.json +2 -1
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Project identities as the console uses them, without touching a person's folders on its thread.
3
+ *
4
+ * A project's identity is its real path (project.ts canonicalProject). Resolving one reads the folder, and a folder can
5
+ * stop answering (folder-look.ts), so the console never resolves one on a page or at startup on its own thread:
6
+ *
7
+ * - `hot` (display and grouping only): the last answer, else the path as saved. Nothing is started by asking.
8
+ * - `strict` (visibility and authorization): only a resolved identity; an unknown one fails closed until the next
9
+ * refresh resolves it, so a stored path that leads outside its root never passes on its spelling.
10
+ * - `vouched` (a path a request names): only a path whose identity is known, or one that is exactly a saved identity.
11
+ * Anything else is refused this time and resolved by the next background pass, never resolved inline.
12
+ * - `resolveNow` (an action that needs a path checked now, a POST): resolved off the thread under the look's deadline.
13
+ * - `refresh` (the console's start and its background pass): resolves what was asked about and what has gone stale.
14
+ *
15
+ * An embedder or test that passes no identities gets `syncProjectIdentities`: the old synchronous reads, unchanged.
16
+ */
17
+ import { resolve } from "node:path";
18
+ import { lookIdentities } from "./folder-look.js";
19
+ import { canonicalProject } from "./project.js";
20
+ /** How long an answer stands before the background pass resolves the path again. */
21
+ const STALE_MS = 60_000;
22
+ export function projectIdentities(look = {}, now = Date.now) {
23
+ const answers = new Map();
24
+ const wanted = new Set();
25
+ const known = (path) => answers.get(resolve(path))?.real;
26
+ const store = (found) => {
27
+ const at = now();
28
+ for (const [path, real] of found) {
29
+ answers.set(resolve(path), { real, at });
30
+ wanted.delete(resolve(path));
31
+ // A real path is its own identity.
32
+ if (real !== null)
33
+ answers.set(real, { real, at });
34
+ }
35
+ };
36
+ const resolveNow = async (path) => {
37
+ const looked = await lookIdentities([resolve(path)], look);
38
+ if (!looked.ok)
39
+ return "no-answer";
40
+ store(looked.value);
41
+ return looked.value.get(resolve(path)) ?? null;
42
+ };
43
+ return {
44
+ hot: path => {
45
+ const key = resolve(path);
46
+ const answer = answers.get(key);
47
+ if (answer === undefined)
48
+ wanted.add(key);
49
+ return answer?.real ?? key;
50
+ },
51
+ known,
52
+ strict: path => {
53
+ const key = resolve(path);
54
+ const answer = answers.get(key);
55
+ if (answer === undefined)
56
+ wanted.add(key);
57
+ return answer?.real ?? null;
58
+ },
59
+ vouched: (path, saved) => {
60
+ const key = resolve(path);
61
+ const answer = answers.get(key);
62
+ if (answer !== undefined && answer.real !== null)
63
+ return answer.real;
64
+ for (const one of saved())
65
+ if (one === key)
66
+ return key;
67
+ wanted.add(key);
68
+ return undefined;
69
+ },
70
+ resolveNow,
71
+ refresh: async (paths) => {
72
+ const at = now();
73
+ const due = new Set(wanted);
74
+ for (const one of paths) {
75
+ const key = resolve(one);
76
+ const answer = answers.get(key);
77
+ if (answer === undefined || at - answer.at >= STALE_MS)
78
+ due.add(key);
79
+ }
80
+ for (const [key, answer] of answers)
81
+ if (at - answer.at >= STALE_MS)
82
+ due.add(key);
83
+ if (due.size === 0)
84
+ return true;
85
+ const batch = await lookIdentities([...due], look);
86
+ if (batch.ok) {
87
+ store(batch.value);
88
+ return true;
89
+ }
90
+ // One folder that doesn't answer mustn't hold the others: each on its own, in parallel (the looks are capped).
91
+ const each = await Promise.all([...due].map(one => resolveNow(one)));
92
+ return each.every(one => one !== "no-answer");
93
+ },
94
+ };
95
+ }
96
+ /** The synchronous reads, as before: for embedders and tests that pass no identities. */
97
+ export function syncProjectIdentities() {
98
+ return {
99
+ hot: path => canonicalProject(path) ?? path,
100
+ known: path => canonicalProject(path),
101
+ strict: path => canonicalProject(path),
102
+ vouched: path => canonicalProject(path) ?? undefined,
103
+ resolveNow: async (path) => canonicalProject(path),
104
+ refresh: async () => true,
105
+ };
106
+ }
@@ -22,19 +22,17 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
22
22
  export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
23
23
  /** Chat reads only an admitted project. Keep the full source in the project UI. */
24
24
  export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
25
- title?: never;
26
25
  history?: never;
27
26
  revision: number;
28
27
  stale: boolean;
29
28
  notice: string;
30
29
  decision?: never;
30
+ title?: never;
31
31
  content?: never;
32
32
  instructions?: never;
33
33
  references?: never;
34
34
  decisions?: never;
35
35
  } | {
36
- title?: never;
37
- revision?: never;
38
36
  history?: never;
39
37
  stale?: never;
40
38
  decision: {
@@ -52,7 +50,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
52
50
  id: number;
53
51
  };
54
52
  notice: string;
53
+ title?: never;
55
54
  content?: never;
55
+ revision?: never;
56
56
  instructions?: never;
57
57
  references?: never;
58
58
  decisions?: never;
@@ -68,9 +68,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
68
68
  references?: never;
69
69
  decisions?: never;
70
70
  } | {
71
- title?: never;
72
71
  stale?: never;
73
72
  decision?: never;
73
+ title?: never;
74
74
  content?: never;
75
75
  revision: number;
76
76
  history: {
@@ -103,7 +103,7 @@ export type FoundTool = {
103
103
  * Codex's (from `codex mcp list --json`, passed in). The first of each
104
104
  * name wins; unusable entries are left out.
105
105
  */
106
- export declare function discoverTools(repo: string, codexServers: unknown[] | null, home?: string, environment?: NodeJS.ProcessEnv): FoundTool[];
106
+ export declare function discoverTools(repo: string, codexServers: unknown[] | null, home?: string, environment?: NodeJS.ProcessEnv, projectConfig?: Record<string, unknown> | null): FoundTool[];
107
107
  /** Start the server once, ask for its tools, and stop it: the proof a build can use it. */
108
108
  export declare function testTool(spec: ToolSpec, values: Record<string, string>, options: {
109
109
  timeoutMs?: number;
@@ -313,3 +313,6 @@ export declare function toolServerFor(store: ToolServerStore, repo: string, opti
313
313
  home?: string;
314
314
  available?: () => boolean;
315
315
  }): ToolServer;
316
+ /** Each project's `.mcp.json` as the console last read it off its thread (its background pass): what its pages and the
317
+ * lead offer, so neither reads a project folder on the console's thread. */
318
+ export declare const savedProjectMcp: Map<string, Record<string, unknown> | null>;
@@ -496,7 +496,7 @@ function fromCodexEntry(raw, environment) {
496
496
  * Codex's (from `codex mcp list --json`, passed in). The first of each
497
497
  * name wins; unusable entries are left out.
498
498
  */
499
- export function discoverTools(repo, codexServers, home = homedir(), environment = process.env) {
499
+ export function discoverTools(repo, codexServers, home = homedir(), environment = process.env, projectConfig) {
500
500
  const found = [];
501
501
  const add = (one) => { if (one !== null && !found.some(other => other.spec.name === one.spec.name))
502
502
  found.push(one); };
@@ -509,7 +509,9 @@ export function discoverTools(repo, codexServers, home = homedir(), environment
509
509
  return null;
510
510
  }
511
511
  };
512
- const project = readJson(join(repo, ".mcp.json"));
512
+ // The project's own file as given (the console reads it off its thread: folder-look.ts), else as last read there
513
+ // (savedProjectMcp), else read here (the CLI and the worker).
514
+ const project = projectConfig !== undefined ? projectConfig : savedProjectMcp.has(repo) ? savedProjectMcp.get(repo) ?? null : readJson(join(repo, ".mcp.json"));
513
515
  for (const [name, raw] of Object.entries((project?.["mcpServers"] ?? {})))
514
516
  add(fromClaudeEntry(name, raw, "this project's .mcp.json"));
515
517
  const claude = readJson(join(home, ".claude.json"));
@@ -1149,3 +1151,6 @@ export function toolServerFor(store, repo, options = {}) {
1149
1151
  const untrusted = existsSync(cache) ? [realpathSync(cache)] : [];
1150
1152
  return { fence: fenceFor("tool-server", { databaseFile, worktree: null, home, boundary: null, binding: null, runFolders: runFolders(null), poolRoots, extra: untrusted }) };
1151
1153
  }
1154
+ /** Each project's `.mcp.json` as the console last read it off its thread (its background pass): what its pages and the
1155
+ * lead offer, so neither reads a project folder on the console's thread. */
1156
+ export const savedProjectMcp = new Map();
package/dist/project.d.ts CHANGED
@@ -29,6 +29,9 @@ export type ProjectCeiling = {
29
29
  repos: readonly string[];
30
30
  /** Canonical directories: any git repo under one is authorized. */
31
31
  roots: readonly string[];
32
+ /** How this ceiling resolves a path it is asked about (project-identity.ts): the console's saved identities, which
33
+ * read no folder. Absent: canonicalProject, a synchronous read. */
34
+ canon?: (path: string) => string | null;
32
35
  };
33
36
  /**
34
37
  * Resolve the startup configuration once. A configured path that does not
@@ -36,7 +39,7 @@ export type ProjectCeiling = {
36
39
  * dropping would silently *widen* the server toward unscoped mode, and a
37
40
  * ceiling that loosens itself when a disk is unmounted is not a ceiling.
38
41
  */
39
- export declare function resolveCeiling(repos: readonly string[], roots: readonly string[]): {
42
+ export declare function resolveCeiling(repos: readonly string[], roots: readonly string[], canon?: (path: string) => string | null | undefined): {
40
43
  ceiling: ProjectCeiling;
41
44
  unresolved: string[];
42
45
  };
package/dist/project.js CHANGED
@@ -67,10 +67,10 @@ export async function projectSelection(paths, exec = run) {
67
67
  * dropping would silently *widen* the server toward unscoped mode, and a
68
68
  * ceiling that loosens itself when a disk is unmounted is not a ceiling.
69
69
  */
70
- export function resolveCeiling(repos, roots) {
70
+ export function resolveCeiling(repos, roots, canon = canonicalProject) {
71
71
  const unresolved = [];
72
72
  const keep = (paths) => [...new Set(paths.map(path => {
73
- const canonical = canonicalProject(path);
73
+ const canonical = canon(path) ?? null;
74
74
  if (canonical !== null)
75
75
  return canonical;
76
76
  unresolved.push(path);
@@ -90,7 +90,7 @@ export function unscoped(ceiling) {
90
90
  export async function authorizedProject(ceiling, path, exec = run) {
91
91
  if (unscoped(ceiling))
92
92
  return true;
93
- const canonical = canonicalProject(path);
93
+ const canonical = (ceiling.canon ?? canonicalProject)(path);
94
94
  if (canonical === null)
95
95
  return false;
96
96
  if (ceiling.repos.includes(canonical))
@@ -114,8 +114,15 @@ export function rowVisible(ceiling, repo) {
114
114
  return true;
115
115
  // Exact saved identities are common. Check the whole set before resolving
116
116
  // aliases, so the hundredth project does not stat the other99 directories.
117
- if (ceiling.repos.includes(repo) || ceiling.repos.some(one => sameRepo(one, repo)))
117
+ const canon = ceiling.canon;
118
+ if (ceiling.repos.includes(repo) || ceiling.repos.some(one => canon === undefined ? sameRepo(one, repo) : (canon(one) ?? null) !== null && canon(one) === canon(repo)))
118
119
  return true;
120
+ // With a resolver (the console's saved identities), an identity it can't vouch for fails closed before any root is
121
+ // matched: a path's spelling never stands in for where it leads.
122
+ if (canon !== undefined) {
123
+ const resolved = canon(repo);
124
+ return resolved !== null && ceiling.roots.some(root => resolved.startsWith(root + sep));
125
+ }
119
126
  const canonical = canonicalProject(repo) ?? repo;
120
127
  return ceiling.roots.some(root => canonical.startsWith(root + sep));
121
128
  }
@@ -177,6 +177,9 @@ export type ParsedEnvelope = {
177
177
  structuralTerminal: import("./exhaustion.js").StructuralTerminal | null;
178
178
  /** What an ACP agent itself reported it used — kept on the run, never priced. Absent for every other dialect. */
179
179
  agentReportedUsage?: string | null;
180
+ /** v140: the exact model the provider itself reported for this turn (Claude: its session's init, or a buffered
181
+ * result's one `modelUsage` model). Absent or null: not reported — every other dialect today. */
182
+ modelReported?: string | null;
180
183
  /** An ACP conversation's facts the conformance check reads (acp-client.ts AcpEnvelope): nothing else does. */
181
184
  acp?: {
182
185
  offered: string[];
package/dist/provider.js CHANGED
@@ -30,6 +30,7 @@ import { ACP_AGENTS, isAcpProvider, withAcp } from "./acp-agents.js";
30
30
  import { acpParse } from "./acp-client.js";
31
31
  import { agentSpecSchema, MODEL_ID, modelIdSchema, PROVIDER_ID_VALUES, providerIdSchema } from "./contracts/provider.js";
32
32
  import { primaryResultEvent, topLevelEvent } from "./stream-scope.js";
33
+ import { claudeResultReport, reportedModelOf } from "./reported-usage.js";
33
34
  export const PROVIDER_IDS = PROVIDER_ID_VALUES;
34
35
  export function isProviderId(value) {
35
36
  return providerIdSchema.safeParse(value).success;
@@ -293,7 +294,7 @@ function claudeFinalMessage(result) {
293
294
  }
294
295
  return typeof result.result === "string" ? result.result : null;
295
296
  }
296
- function claudeEnvelopeOf(result, sessionFromInit, initObserved, initSessionConflict = false) {
297
+ function claudeEnvelopeOf(result, sessionFromInit, initObserved, initSessionConflict = false, initModel = { model: null, conflict: false }) {
297
298
  const input = result?.usage?.input_tokens;
298
299
  const output = result?.usage?.output_tokens;
299
300
  const cost = result?.total_cost_usd;
@@ -322,6 +323,8 @@ function claudeEnvelopeOf(result, sessionFromInit, initObserved, initSessionConf
322
323
  costUsd: typeof cost === "number" && cost >= 0 ? cost : null,
323
324
  ...(result !== null && result[CLAUDE_COST_UNKNOWN] === true ? { costUnknown: true } : {}),
324
325
  usageRaw: result?.usage === undefined ? null : JSON.stringify(result.usage).slice(0, USAGE_JSON_CAP),
326
+ // The model only when the session's own events name it (never the agent's words); no result, no report.
327
+ modelReported: result === null ? (initModel.conflict ? null : initModel.model) : claudeResultReport(result, initModel).modelReported,
325
328
  initObserved,
326
329
  // Structural, never prose (finding 15): consumed means the primary
327
330
  // result says SUCCESS. An error result keeps its text as diagnostics
@@ -354,6 +357,7 @@ function claudeParse(stdout) {
354
357
  let initSeen = false;
355
358
  let sessionFromInit = null;
356
359
  let initSessionConflict = false;
360
+ const initModels = new Set();
357
361
  let primary = null;
358
362
  let transportFailure = null;
359
363
  for (const line of stdout.split("\n")) {
@@ -379,6 +383,9 @@ function claudeParse(stdout) {
379
383
  }
380
384
  if (String(type) === "system" && String(event["subtype"] ?? "") === "init") {
381
385
  initSeen = true;
386
+ const model = reportedModelOf(event["model"]);
387
+ if (model !== null)
388
+ initModels.add(model);
382
389
  const id = sessionIdOf(event["session_id"]);
383
390
  if (id !== null) {
384
391
  if (sessionFromInit === null)
@@ -402,7 +409,7 @@ function claudeParse(stdout) {
402
409
  primary = result;
403
410
  }
404
411
  }
405
- const envelope = claudeEnvelopeOf(primary, sessionFromInit, initSeen, initSessionConflict);
412
+ const envelope = claudeEnvelopeOf(primary, sessionFromInit, initSeen, initSessionConflict, { model: initModels.size === 1 ? [...initModels][0] : null, conflict: initModels.size > 1 });
406
413
  if (transportFailure === null)
407
414
  return envelope;
408
415
  return {
@@ -8,6 +8,8 @@ import type { CheckRun, GateState, Installed, MainProtection, ReleaseAdapters }
8
8
  import { Store } from "./store.js";
9
9
  import { type CiObservation } from "./release-auto.js";
10
10
  import { type BuildRunner } from "./release-carry.js";
11
+ import { type GhRead } from "./nightly-github.js";
12
+ export { failedBeforeTests, type JobRecord } from "./github-jobs.js";
11
13
  import { type ReleaseRepo, type ReleaseTools } from "./release-git.js";
12
14
  export { RELEASE_PATH, RELEASE_REMOTE, releaseConfigProblem, releaseGitArgs, releaseGitEnv, releaseRemote, releaseTools, type ReleaseRepo, type ReleaseTools } from "./release-git.js";
13
15
  export type Exec = (command: string, args: readonly string[], options?: {
@@ -112,7 +114,10 @@ export declare function bumpFormula(text: string, url: string, sha256: string):
112
114
  export declare function storeReleaseAdapter(open: () => Store, done: (store: Store) => void, now: () => Date, runner?: BuildRunner,
113
115
  /** Releases that approve themselves (auto-approve-green.md): the plane's own look at CI on a commit, and where the
114
116
  * check receipts are kept. Absent: no automatic approval, every release asks a person. */
115
- auto?: AutoObservers): NonNullable<ReleaseAdapters["release"]>;
117
+ auto?: AutoObservers,
118
+ /** GitHub's nightly (nightly-github.ts): the plane's own gh reads for a boundary's one bounded read. Absent: nothing
119
+ * is read at the boundary, and the stored debt still holds inside every reservation. */
120
+ nightlyReads?: GhRead): NonNullable<ReleaseAdapters["release"]>;
116
121
  /** What the plane itself reads for an automatic release approval. */
117
122
  export type AutoObservers = {
118
123
  /** The plane's executor policy, never a report field or project setting. Unknown refuses. Production currently
@@ -135,28 +140,23 @@ export declare function readCheckRuns(run: Exec, gh: string, repo: string, sha:
135
140
  options?: {
136
141
  tag?: string;
137
142
  }): Promise<CheckRun[]>;
138
- /** One job's step outcomes as GitHub's Actions API reports them. */
139
- export type JobRecord = {
140
- id: number;
141
- status: string;
142
- conclusion: string | null;
143
- steps: {
144
- number: number;
145
- name: string;
146
- status: string;
147
- conclusion: string | null;
148
- }[];
149
- };
150
- /**
151
- * Whether a finished, unsuccessful job ended before any of the workflow's own steps ran: every step after GitHub's own
152
- * first one ("Set up job", always number 1) never ran (no conclusion, or skipped) — leaving out only GitHub's own
153
- * closing "Complete job" when it is the last step. Step numbers and outcomes are the runner's records; nothing the
154
- * candidate prints is read. A job with no steps never reached one. Anything else (a step that ran, even the checkout)
155
- * is not confirmed infrastructure.
156
- */
157
- export declare function failedBeforeTests(job: JobRecord): boolean;
158
143
  /** GitHub's job record for one check run (its job), read strictly: null when it can't be read whole. */
159
144
  export declare function readJobBeforeTests(run: Exec, gh: string, repo: string, id: number): Promise<boolean | null>;
145
+ /**
146
+ * GitHub's REST API as GitHub's nightly reads it (nightly-github.ts): GET only, github.com only, a repository path, the
147
+ * plane's own gh with the allowlisted environment, each call bounded by its caller's deadline. A background read rides
148
+ * out a short network failure (release-net.ts, within that call's own share of the deadline); a boundary's read is
149
+ * plain exec. Never an answer that isn't GitHub's whole JSON.
150
+ */
151
+ export declare function githubReads(run: Exec, gh: string, options?: {
152
+ retry?: boolean;
153
+ }): (path: string, timeoutMs: number) => Promise<{
154
+ ok: true;
155
+ body: unknown;
156
+ } | {
157
+ ok: false;
158
+ why: string;
159
+ }>;
160
160
  /** GitHub lists check runs from at most the latest 1000 check suites of a commit: the history is whole only below that. */
161
161
  export declare const CHECK_SUITE_CEILING = 1000;
162
162
  export declare function readSuitesComplete(run: Exec, gh: string, repo: string, sha: string): Promise<boolean>;
@@ -28,6 +28,9 @@ import { autoPublishHeld, holdAutoPublish, liftCiHold, authorizeAutoCheck, giveU
28
28
  import { proveCarry } from "./release-carry.js";
29
29
  import { randomBytes, randomUUID } from "node:crypto";
30
30
  import { deploymentGate } from "./deploy-gate.js";
31
+ import { failedBeforeTests } from "./github-jobs.js";
32
+ import { mainHistoryOf, nightlyBoundary } from "./nightly-github.js";
33
+ export { failedBeforeTests } from "./github-jobs.js";
31
34
  import { extractCommit } from "./commit-extract.js";
32
35
  import { teardownGit, refusal as gitRefusal } from "./teardown-git.js";
33
36
  import { RELEASE_REMOTE, releaseConfigProblem, releaseGitArgs, releaseGitEnv, releaseRemote, releaseTools } from "./release-git.js";
@@ -422,7 +425,9 @@ export function releaseAdapters(options) {
422
425
  upToDateRequired: () => readUpToDateRequired(reads, ciGh, releaseRepo().slug), evidenceRoot: join(dirname(database), "evidence"),
423
426
  // A successful Actions job authenticates the job, not the candidate-writable JSON it uploads. Until the
424
427
  // executor protects that reporting channel, production releases must take the person's approval road.
425
- reportsProtected: false });
428
+ reportsProtected: false },
429
+ // A boundary's one bounded read of GitHub's nightly: plain exec, never the release's long retries (20 s in all).
430
+ githubReads(run, ciGh));
426
431
  return {
427
432
  ...(existsSync(database) ? { release } : {}),
428
433
  now: options.now ?? (() => new Date()),
@@ -925,7 +930,10 @@ function personAnswerOf(value) {
925
930
  export function storeReleaseAdapter(open, done, now, runner,
926
931
  /** Releases that approve themselves (auto-approve-green.md): the plane's own look at CI on a commit, and where the
927
932
  * check receipts are kept. Absent: no automatic approval, every release asks a person. */
928
- auto) {
933
+ auto,
934
+ /** GitHub's nightly (nightly-github.ts): the plane's own gh reads for a boundary's one bounded read. Absent: nothing
935
+ * is read at the boundary, and the stored debt still holds inside every reservation. */
936
+ nightlyReads) {
929
937
  const withStore = (body) => { const store = open(); try {
930
938
  return body(store);
931
939
  }
@@ -962,6 +970,20 @@ auto) {
962
970
  async live(taskId, action) {
963
971
  return withStore(store => { const live = liveReleaseOf(store, taskId, action, now()); return live.applies ? { applies: true, ok: live.ok, why: live.ok ? null : live.why } : { applies: false, ok: false, why: null }; });
964
972
  },
973
+ ...(nightlyReads === undefined ? {} : {
974
+ async nightly(input) {
975
+ const store = open();
976
+ try {
977
+ const repo = store.lookupRef(input.taskId)?.repo ?? null;
978
+ if (repo === null)
979
+ return { ok: false, why: `Task ${input.taskId} is in no project, so GitHub's nightly debt can't be read for it.`, warning: null };
980
+ return await nightlyBoundary(store, { repo, taskId: input.taskId, candidate: input.candidate, pr: input.pr, phase: input.phase }, { ...mainHistoryOf(store, repo, now()), gh: nightlyReads, now });
981
+ }
982
+ finally {
983
+ done(store);
984
+ }
985
+ },
986
+ }),
965
987
  async lease(authority, owner) { return withStore(store => takeReleaseLease(store, authority, owner, now())); },
966
988
  async renew(authority, owner, epoch) { return withStore(store => renewReleaseLease(store, authority, owner, epoch, now())); },
967
989
  async carry(authority, to, requestId, epoch) {
@@ -1169,23 +1191,6 @@ options = {}) {
1169
1191
  }
1170
1192
  return runs.sort((a, b) => a.id - b.id);
1171
1193
  }
1172
- /**
1173
- * Whether a finished, unsuccessful job ended before any of the workflow's own steps ran: every step after GitHub's own
1174
- * first one ("Set up job", always number 1) never ran (no conclusion, or skipped) — leaving out only GitHub's own
1175
- * closing "Complete job" when it is the last step. Step numbers and outcomes are the runner's records; nothing the
1176
- * candidate prints is read. A job with no steps never reached one. Anything else (a step that ran, even the checkout)
1177
- * is not confirmed infrastructure.
1178
- */
1179
- export function failedBeforeTests(job) {
1180
- if (job.status !== "completed" || job.conclusion === "success" || job.conclusion === null)
1181
- return false;
1182
- const steps = [...job.steps].sort((a, b) => a.number - b.number);
1183
- if (new Set(steps.map(one => one.number)).size !== steps.length)
1184
- return false;
1185
- const last = steps.at(-1);
1186
- const own = steps.filter(one => one.number > 1 && !(one === last && one.name === "Complete job" && steps.length > 1));
1187
- return own.every(one => one.status !== "in_progress" && (one.conclusion === null || one.conclusion === "skipped"));
1188
- }
1189
1194
  /** GitHub's job record for one check run (its job), read strictly: null when it can't be read whole. */
1190
1195
  export async function readJobBeforeTests(run, gh, repo, id) {
1191
1196
  if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) || !Number.isSafeInteger(id) || id <= 0)
@@ -1214,6 +1219,34 @@ export async function readJobBeforeTests(run, gh, repo, id) {
1214
1219
  return failedBeforeTests({ id, status: String(job["status"]), conclusion: textOr(job["conclusion"]),
1215
1220
  steps: steps.map(one => ({ number: Number(one["number"]), name: String(one["name"]), status: String(one["status"]), conclusion: textOr(one["conclusion"]) })) });
1216
1221
  }
1222
+ /**
1223
+ * GitHub's REST API as GitHub's nightly reads it (nightly-github.ts): GET only, github.com only, a repository path, the
1224
+ * plane's own gh with the allowlisted environment, each call bounded by its caller's deadline. A background read rides
1225
+ * out a short network failure (release-net.ts, within that call's own share of the deadline); a boundary's read is
1226
+ * plain exec. Never an answer that isn't GitHub's whole JSON.
1227
+ */
1228
+ export function githubReads(run, gh, options = {}) {
1229
+ return async (path, timeoutMs) => {
1230
+ if (!/^repos\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_./?&=%-]+$/.test(path) || timeoutMs <= 0)
1231
+ return { ok: false, why: "not a read this makes" };
1232
+ const exec = options.retry === true ? retryingExec(run, { say: () => { }, sleep, budgetMs: timeoutMs }) : run;
1233
+ let result;
1234
+ try {
1235
+ result = await exec(gh, ["api", "--hostname", "github.com", "--method", "GET", path], { cwd: "/", env: ciEnv(), timeoutMs });
1236
+ }
1237
+ catch (error) {
1238
+ return { ok: false, why: String(error?.message ?? error).slice(0, 200) };
1239
+ }
1240
+ if (result.code !== 0)
1241
+ return { ok: false, why: result.code === 124 ? "GitHub didn't answer in time" : result.code === 127 ? "gh isn't installed" : (result.stderr || result.stdout).trim().split("\n").slice(-1)[0].slice(0, 200) || `gh exited ${result.code}` };
1242
+ try {
1243
+ return { ok: true, body: JSON.parse(result.stdout) };
1244
+ }
1245
+ catch {
1246
+ return { ok: false, why: "GitHub's answer couldn't be read" };
1247
+ }
1248
+ };
1249
+ }
1217
1250
  /** GitHub lists check runs from at most the latest 1000 check suites of a commit: the history is whole only below that. */
1218
1251
  export const CHECK_SUITE_CEILING = 1000;
1219
1252
  export async function readSuitesComplete(run, gh, repo, sha) {
@@ -17,6 +17,7 @@ import { fullCheckProblem, projectGitDir } from "./check-report.js";
17
17
  import { sealedRun } from "./release-recheck.js";
18
18
  import { runCheckLevel } from "./check-levels.js";
19
19
  import { readVerifiedCommit, verifiedBlobs, verifiedTreeDiff, verifiedTreeEntries } from "./commit-extract.js";
20
+ import { EVAL_HOOK_FILES, evalHookProblem, storeRewriteProblem } from "./eval-hooks.js";
20
21
  import { mightName, testInfrastructure } from "./test-infra.js";
21
22
  import { chatControlHref } from "./chat-controls.js";
22
23
  import { INFRA_RERUNS_PER_CANDIDATE, REQUIRED_PR_CHECKS, REQUIRED_PR_CHECKS_WORKFLOW, prChecksVerdict } from "./release.js";
@@ -27,6 +28,7 @@ import { autoReleaseApprovalOf, personApprovalOf } from "./scope.js";
27
28
  import { releaseOperations, revokeRelease } from "./store.js";
28
29
  import { RELEASE_CI_HELD_ACTION, RELEASE_CI_PASSED_ACTION, RELEASE_CI_RUNNING_WORDS, releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
29
30
  import { verificationEvidence } from "./verification-evidence.js";
31
+ import { nightlyDebtHold } from "./nightly-github.js";
30
32
  export { RELEASE_APPROVAL_ACTION, releaseApprovalSetting, releaseAutoApproves };
31
33
  /** The setting's words, one place. */
32
34
  export const RELEASE_APPROVAL_WORDS = { ask: "Ask me", auto: "Approve automatically when checks pass" };
@@ -177,6 +179,11 @@ export function authorizeAutoCheck(store, input) {
177
179
  const SRC_TEST_INFRA = /^src\/(fixtures\/|(.*\/)?(test-[^/]*|[^/]*-test-support[^/]*|[^/]*fixtures?[^/]*)\.[cm]?[jt]sx?$)/;
178
180
  /** A test file (normalized path): `*.test.ts`, never inside a fixtures folder. */
179
181
  const TEST_FILE = /^(?!.*(^|\/)fixtures?\/)[^]*\.test\.ts$/;
182
+ /** The eval set's and GitHub nightly's policy code (docs/plans/evals-nightly.md, part C, finding 10): the cases and
183
+ * oracles, the evaluator, its gate, hooks and world, reviewer scoring, nightly ingestion, their contracts, and the
184
+ * accounting readings evals trust (reported-usage.ts, run-efficiency.ts). Where they are called from, and the store's
185
+ * accounting reads, are held by eval-hooks.ts instead (evalHookProblem). */
186
+ const EVAL_POLICY = /^(evals\/|src\/evals[^/]*\.ts$|src\/eval-[^/]*\.ts$|src\/reviewer-evals\.ts$|src\/nightly-github\.ts$|src\/contracts\/evals[^/]*\.ts$|src\/reported-usage\.ts$|src\/run-efficiency\.ts$)/;
180
187
  /** Build or type configuration anywhere: a person approves it. */
181
188
  const BUILD_CONFIG = /(^|\/)(tsconfig[^/]*\.json|[^/]*\.config\.[cm]?[jt]s|\.npmrc|\.nvmrc|\.node-version)$/;
182
189
  /**
@@ -201,6 +208,8 @@ export function autoPathProblem(path) {
201
208
  }
202
209
  // Test files under test/ carry the same trust as src's (Alex: test authors are trusted) — only the `.test.ts` files
203
210
  // themselves, never a helper, fixture, setup or config file beside them, nor one inside a fixtures folder.
211
+ if (EVAL_POLICY.test(normal))
212
+ return "it's eval or nightly policy code";
204
213
  if (testUnderTest)
205
214
  return null;
206
215
  if (BUILD_CONFIG.test(normal))
@@ -312,6 +321,19 @@ function changeProblem(repo, base, candidate) {
312
321
  scans.push(found);
313
322
  }
314
323
  const infra = new Set(scans.flatMap(one => [...one.infra]));
324
+ // The eval and nightly enforcement calls the base holds must all still be in place at the candidate (eval-hooks.ts).
325
+ // One verified read of every listed file per tree (an unreadable tree reads as every file missing: a person decides).
326
+ const blobAt = (tree) => { const blobs = verifiedBlobs(gitDir, tree, EVAL_HOOK_FILES); return (file) => blobs.ok ? blobs.value.get(file)?.bytes ?? null : null; };
327
+ const hooks = evalHookProblem(blobAt(after.value.tree), blobAt(before.value.tree));
328
+ if (hooks !== null)
329
+ return `${hooks}: a person decides`;
330
+ // No changed file may write over a pinned Store method from outside store.ts (eval-hooks.ts storeRewriteProblem).
331
+ const changed = diff.value.filter(change => change.after !== null).map(change => change.path);
332
+ // A failed read is no reader at all (a person decides), never files that read as absent.
333
+ const changedAt = (tree) => { const blobs = verifiedBlobs(gitDir, tree, changed); return blobs.ok ? (file) => blobs.value.get(file)?.bytes ?? null : null; };
334
+ const rewrites = storeRewriteProblem(changed, changedAt(after.value.tree), changedAt(before.value.tree));
335
+ if (rewrites !== null)
336
+ return `${rewrites}: a person decides`;
315
337
  for (const change of diff.value) {
316
338
  const modes = [change.before?.mode, change.after?.mode].filter((one) => one !== undefined);
317
339
  if (modes.includes("120000") || modes.includes("160000"))
@@ -461,6 +483,10 @@ export function sealAutoGreen(store, input) {
461
483
  if (changed !== null)
462
484
  return refuse(changed);
463
485
  // GitHub CI, as the plane's own gh saw it moments ago, on exactly this commit.
486
+ // GitHub's nightly lane debt (evals-nightly.md, part A2): dormant while automatic approval is refused, held here too.
487
+ const nightly = nightlyDebtHold(store, { repo: facts.repo, taskId: input.taskId, candidate: input.candidate, phase: "auto", release: true, now: input.now });
488
+ if (nightly !== null)
489
+ return refuse(nightly.why);
464
490
  const ci = ciVerdict(input.ci, input.candidate, input.now);
465
491
  if (!ci.ok)
466
492
  return refuse(`CI: ${ci.problem}`);
@@ -27,6 +27,7 @@ import { routeFromJson } from "./phase-routing.js";
27
27
  import { projectName } from "./project.js";
28
28
  import { RELEASE_CI_RUNNING_WORDS, releaseCiRunningOf } from "./release-ci-hold.js";
29
29
  import { RELEASE_PATH, releaseConfigProblem, releaseGitEnv, releaseRemote, releaseTools } from "./release-git.js";
30
+ import { nightlyReleaseLines } from "./nightly-github.js";
30
31
  const NONE = "none";
31
32
  /** A budget in dollars from integer micro-dollars, in integer arithmetic: cents when it is whole cents, else every digit. */
32
33
  const money = (micro) => {
@@ -258,6 +259,8 @@ export function releaseCardOf(store, scope, formDigest, words) {
258
259
  ...(terms !== null && terms.policy.carry && terms.access !== "sandboxed" ? ["This project's checks don't run in the sandbox: later test-only commits will run outside the sandbox without being shown to you first."] : []),
259
260
  // Filed while CI runs (Alex, 2026-10-29): a person may approve now, and is told plainly what still waits.
260
261
  ...(releaseCiRunningOf(store, scope.taskId) === null ? [] : [RELEASE_CI_RUNNING_WORDS]),
262
+ // GitHub's nightly (nightly-github.ts): what holds this release, or why it can't be read. Not a signed term.
263
+ ...(terms === null ? [] : nightlyReleaseLines(store, { repo: terms.repo, taskId: scope.taskId, candidate: scope.candidate ?? null, now: new Date() })),
261
264
  "",
262
265
  ...changeWords(changes, terms, 5),
263
266
  "",