@wix/pathgrade 0.29.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 (238) hide show
  1. package/bin/pathgrade.js +2 -0
  2. package/dist/affected/anchor.d.ts +18 -0
  3. package/dist/affected/anchor.js +42 -0
  4. package/dist/affected/config.d.ts +25 -0
  5. package/dist/affected/config.js +98 -0
  6. package/dist/affected/format.d.ts +45 -0
  7. package/dist/affected/format.js +87 -0
  8. package/dist/affected/git.d.ts +33 -0
  9. package/dist/affected/git.js +68 -0
  10. package/dist/affected/glob.d.ts +17 -0
  11. package/dist/affected/glob.js +25 -0
  12. package/dist/affected/meta.d.ts +26 -0
  13. package/dist/affected/meta.js +158 -0
  14. package/dist/affected/select.d.ts +30 -0
  15. package/dist/affected/select.js +101 -0
  16. package/dist/affected/sidecar.d.ts +32 -0
  17. package/dist/affected/sidecar.js +86 -0
  18. package/dist/affected/types.d.ts +43 -0
  19. package/dist/affected/types.js +7 -0
  20. package/dist/agents/claude/ask-user-answer-store.d.ts +28 -0
  21. package/dist/agents/claude/ask-user-answer-store.js +29 -0
  22. package/dist/agents/claude/ask-user-bridge.d.ts +56 -0
  23. package/dist/agents/claude/ask-user-bridge.js +162 -0
  24. package/dist/agents/claude/sdk-message-projector.d.ts +43 -0
  25. package/dist/agents/claude/sdk-message-projector.js +208 -0
  26. package/dist/agents/claude/sdk-options.d.ts +52 -0
  27. package/dist/agents/claude/sdk-options.js +79 -0
  28. package/dist/agents/claude.d.ts +56 -0
  29. package/dist/agents/claude.js +143 -0
  30. package/dist/agents/codex-app-server/agent.d.ts +35 -0
  31. package/dist/agents/codex-app-server/agent.js +388 -0
  32. package/dist/agents/codex-app-server/fixtures/app-server-client.d.ts +19 -0
  33. package/dist/agents/codex-app-server/fixtures/app-server-client.js +104 -0
  34. package/dist/agents/codex-app-server/fixtures/run-gate.d.ts +25 -0
  35. package/dist/agents/codex-app-server/fixtures/run-gate.js +48 -0
  36. package/dist/agents/codex-app-server/protocol/ClientRequest.d.ts +27 -0
  37. package/dist/agents/codex-app-server/protocol/ClientRequest.js +7 -0
  38. package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.d.ts +9 -0
  39. package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.js +4 -0
  40. package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.d.ts +6 -0
  41. package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.js +7 -0
  42. package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.d.ts +23 -0
  43. package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.js +8 -0
  44. package/dist/agents/codex-app-server/protocol/Op.d.ts +11 -0
  45. package/dist/agents/codex-app-server/protocol/Op.js +5 -0
  46. package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.d.ts +10 -0
  47. package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.js +6 -0
  48. package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.d.ts +15 -0
  49. package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.js +4 -0
  50. package/dist/agents/codex-app-server/protocol/SandboxMode.d.ts +1 -0
  51. package/dist/agents/codex-app-server/protocol/SandboxMode.js +4 -0
  52. package/dist/agents/codex-app-server/protocol/ServerRequest.d.ts +42 -0
  53. package/dist/agents/codex-app-server/protocol/ServerRequest.js +7 -0
  54. package/dist/agents/codex-app-server/protocol/ThreadStartParams.d.ts +45 -0
  55. package/dist/agents/codex-app-server/protocol/ThreadStartParams.js +10 -0
  56. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.d.ts +6 -0
  57. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.js +4 -0
  58. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.d.ts +7 -0
  59. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.js +4 -0
  60. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.d.ts +10 -0
  61. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.js +4 -0
  62. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.d.ts +12 -0
  63. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.js +4 -0
  64. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.d.ts +9 -0
  65. package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.js +4 -0
  66. package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.d.ts +5 -0
  67. package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.js +4 -0
  68. package/dist/agents/codex-app-server/protocol/index.d.ts +16 -0
  69. package/dist/agents/codex-app-server/protocol/index.js +11 -0
  70. package/dist/agents/codex-app-server/transport.d.ts +84 -0
  71. package/dist/agents/codex-app-server/transport.js +246 -0
  72. package/dist/agents/codex-app-server/wire-translators.d.ts +19 -0
  73. package/dist/agents/codex-app-server/wire-translators.js +52 -0
  74. package/dist/agents/codex.d.ts +7 -0
  75. package/dist/agents/codex.js +164 -0
  76. package/dist/agents/cursor.d.ts +39 -0
  77. package/dist/agents/cursor.js +233 -0
  78. package/dist/agents/registry.d.ts +17 -0
  79. package/dist/agents/registry.js +27 -0
  80. package/dist/agents/transcript-agent.d.ts +14 -0
  81. package/dist/agents/transcript-agent.js +59 -0
  82. package/dist/analytics/engine.d.ts +18 -0
  83. package/dist/analytics/engine.js +66 -0
  84. package/dist/commands/affected.d.ts +31 -0
  85. package/dist/commands/affected.js +150 -0
  86. package/dist/commands/analyze.d.ts +21 -0
  87. package/dist/commands/analyze.js +167 -0
  88. package/dist/commands/init.d.ts +3 -0
  89. package/dist/commands/init.js +242 -0
  90. package/dist/commands/preview-reactions.d.ts +10 -0
  91. package/dist/commands/preview-reactions.js +88 -0
  92. package/dist/commands/preview.d.ts +5 -0
  93. package/dist/commands/preview.js +17 -0
  94. package/dist/commands/report.d.ts +39 -0
  95. package/dist/commands/report.js +108 -0
  96. package/dist/commands/run-args.d.ts +22 -0
  97. package/dist/commands/run-args.js +69 -0
  98. package/dist/commands/run-changed.d.ts +26 -0
  99. package/dist/commands/run-changed.js +138 -0
  100. package/dist/commands/validate.d.ts +13 -0
  101. package/dist/commands/validate.js +233 -0
  102. package/dist/core/mcp-mock.d.ts +3 -0
  103. package/dist/core/mcp-mock.js +23 -0
  104. package/dist/core/mcp-mock.types.d.ts +15 -0
  105. package/dist/core/mcp-mock.types.js +1 -0
  106. package/dist/core/skills.d.ts +15 -0
  107. package/dist/core/skills.js +76 -0
  108. package/dist/evals/discovery.d.ts +7 -0
  109. package/dist/evals/discovery.js +69 -0
  110. package/dist/mcp-mock-server.d.ts +1 -0
  111. package/dist/mcp-mock-server.js +116 -0
  112. package/dist/pathgrade.d.ts +10 -0
  113. package/dist/pathgrade.js +243 -0
  114. package/dist/plugin/index.d.ts +9 -0
  115. package/dist/plugin/index.js +81 -0
  116. package/dist/plugin/lifecycle.d.ts +45 -0
  117. package/dist/plugin/lifecycle.js +146 -0
  118. package/dist/plugin/reporter.d.ts +36 -0
  119. package/dist/plugin/reporter.js +275 -0
  120. package/dist/plugin/setup.d.ts +1 -0
  121. package/dist/plugin/setup.js +3 -0
  122. package/dist/providers/copy-filter.d.ts +18 -0
  123. package/dist/providers/copy-filter.js +66 -0
  124. package/dist/providers/credentials.d.ts +36 -0
  125. package/dist/providers/credentials.js +202 -0
  126. package/dist/providers/mcp-config.d.ts +34 -0
  127. package/dist/providers/mcp-config.js +55 -0
  128. package/dist/providers/sandbox-exec.d.ts +8 -0
  129. package/dist/providers/sandbox-exec.js +82 -0
  130. package/dist/providers/sandbox.d.ts +22 -0
  131. package/dist/providers/sandbox.js +158 -0
  132. package/dist/providers/sandboxed-claude-spawn.d.ts +24 -0
  133. package/dist/providers/sandboxed-claude-spawn.js +57 -0
  134. package/dist/providers/workspace.d.ts +15 -0
  135. package/dist/providers/workspace.js +73 -0
  136. package/dist/reporters/browser.d.ts +2 -0
  137. package/dist/reporters/browser.js +85 -0
  138. package/dist/reporters/cli.d.ts +2 -0
  139. package/dist/reporters/cli.js +131 -0
  140. package/dist/reporters/diagnostics.d.ts +46 -0
  141. package/dist/reporters/diagnostics.js +147 -0
  142. package/dist/reporters/github-comment.d.ts +67 -0
  143. package/dist/reporters/github-comment.js +252 -0
  144. package/dist/reporters/loader.d.ts +8 -0
  145. package/dist/reporters/loader.js +36 -0
  146. package/dist/reporters/results-path.d.ts +2 -0
  147. package/dist/reporters/results-path.js +10 -0
  148. package/dist/reporters/verbose-emitter.d.ts +69 -0
  149. package/dist/reporters/verbose-emitter.js +103 -0
  150. package/dist/sdk/agent-crash.d.ts +19 -0
  151. package/dist/sdk/agent-crash.js +20 -0
  152. package/dist/sdk/agent-resolution.d.ts +12 -0
  153. package/dist/sdk/agent-resolution.js +21 -0
  154. package/dist/sdk/agent-result-log.d.ts +19 -0
  155. package/dist/sdk/agent-result-log.js +45 -0
  156. package/dist/sdk/agent.d.ts +4 -0
  157. package/dist/sdk/agent.js +354 -0
  158. package/dist/sdk/ask-bus/bus.d.ts +21 -0
  159. package/dist/sdk/ask-bus/bus.js +129 -0
  160. package/dist/sdk/ask-bus/handler.d.ts +31 -0
  161. package/dist/sdk/ask-bus/handler.js +244 -0
  162. package/dist/sdk/ask-bus/parsers.d.ts +9 -0
  163. package/dist/sdk/ask-bus/parsers.js +30 -0
  164. package/dist/sdk/ask-bus/projection.d.ts +26 -0
  165. package/dist/sdk/ask-bus/projection.js +45 -0
  166. package/dist/sdk/ask-bus/types.d.ts +69 -0
  167. package/dist/sdk/ask-bus/types.js +1 -0
  168. package/dist/sdk/chat.d.ts +29 -0
  169. package/dist/sdk/chat.js +93 -0
  170. package/dist/sdk/conversation-window.d.ts +14 -0
  171. package/dist/sdk/conversation-window.js +38 -0
  172. package/dist/sdk/converse.d.ts +42 -0
  173. package/dist/sdk/converse.js +329 -0
  174. package/dist/sdk/eval-runtime.d.ts +10 -0
  175. package/dist/sdk/eval-runtime.js +17 -0
  176. package/dist/sdk/evaluate.d.ts +13 -0
  177. package/dist/sdk/evaluate.js +325 -0
  178. package/dist/sdk/index.d.ts +31 -0
  179. package/dist/sdk/index.js +20 -0
  180. package/dist/sdk/judge-pipeline.d.ts +7 -0
  181. package/dist/sdk/judge-pipeline.js +178 -0
  182. package/dist/sdk/judge-prompt-builder.d.ts +7 -0
  183. package/dist/sdk/judge-prompt-builder.js +76 -0
  184. package/dist/sdk/judge-tool-runner.d.ts +19 -0
  185. package/dist/sdk/judge-tool-runner.js +51 -0
  186. package/dist/sdk/judge-tool-session.d.ts +47 -0
  187. package/dist/sdk/judge-tool-session.js +213 -0
  188. package/dist/sdk/judge-tools.d.ts +29 -0
  189. package/dist/sdk/judge-tools.js +283 -0
  190. package/dist/sdk/managed-session.d.ts +50 -0
  191. package/dist/sdk/managed-session.js +110 -0
  192. package/dist/sdk/persona.d.ts +5 -0
  193. package/dist/sdk/persona.js +33 -0
  194. package/dist/sdk/reaction-loader.d.ts +4 -0
  195. package/dist/sdk/reaction-loader.js +119 -0
  196. package/dist/sdk/reaction-preview.d.ts +6 -0
  197. package/dist/sdk/reaction-preview.js +94 -0
  198. package/dist/sdk/run-scorer.d.ts +10 -0
  199. package/dist/sdk/run-scorer.js +124 -0
  200. package/dist/sdk/runtime-policy.d.ts +21 -0
  201. package/dist/sdk/runtime-policy.js +89 -0
  202. package/dist/sdk/scorer-utils.d.ts +6 -0
  203. package/dist/sdk/scorer-utils.js +39 -0
  204. package/dist/sdk/scorers.d.ts +64 -0
  205. package/dist/sdk/scorers.js +80 -0
  206. package/dist/sdk/snapshots.d.ts +41 -0
  207. package/dist/sdk/snapshots.js +148 -0
  208. package/dist/sdk/types.d.ts +416 -0
  209. package/dist/sdk/types.js +17 -0
  210. package/dist/sdk/visible-turn.d.ts +4 -0
  211. package/dist/sdk/visible-turn.js +23 -0
  212. package/dist/tool-events.d.ts +45 -0
  213. package/dist/tool-events.js +167 -0
  214. package/dist/types.d.ts +372 -0
  215. package/dist/types.js +37 -0
  216. package/dist/utils/cli.d.ts +55 -0
  217. package/dist/utils/cli.js +131 -0
  218. package/dist/utils/env.d.ts +8 -0
  219. package/dist/utils/env.js +26 -0
  220. package/dist/utils/llm-mocks.d.ts +43 -0
  221. package/dist/utils/llm-mocks.js +108 -0
  222. package/dist/utils/llm-providers/anthropic.d.ts +2 -0
  223. package/dist/utils/llm-providers/anthropic.js +132 -0
  224. package/dist/utils/llm-providers/cli.d.ts +20 -0
  225. package/dist/utils/llm-providers/cli.js +162 -0
  226. package/dist/utils/llm-providers/openai.d.ts +2 -0
  227. package/dist/utils/llm-providers/openai.js +56 -0
  228. package/dist/utils/llm-types.d.ts +107 -0
  229. package/dist/utils/llm-types.js +1 -0
  230. package/dist/utils/llm.d.ts +36 -0
  231. package/dist/utils/llm.js +209 -0
  232. package/dist/utils/shutdown.d.ts +15 -0
  233. package/dist/utils/shutdown.js +41 -0
  234. package/dist/utils/timeout.d.ts +8 -0
  235. package/dist/utils/timeout.js +32 -0
  236. package/dist/viewer.html +1178 -0
  237. package/package.json +88 -0
  238. package/templates/eval.ts.template +28 -0
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Core selection pipeline for affected-eval-selection.
3
+ *
4
+ * Given a set of discovered eval files and a set of changed files,
5
+ * computes which evals should run. See `docs/003-affected-eval-selection.md`
6
+ * §"Design Overview" and §"Precedence" for the authoritative specification.
7
+ *
8
+ * Precedence (when multiple mechanisms apply):
9
+ * 1. `affected.global` match short-circuits and selects every discovered
10
+ * eval. (Wiring lands in Issue 8.)
11
+ * 2. `alwaysRun: true` unconditionally unions the eval into selection.
12
+ * 3. `deps` (full override) replaces the auto-detected skill root.
13
+ * 4. `extraDeps` is unioned with `deps` or the skill root.
14
+ * 5. Auto-detected skill root (`<skillRoot>/**`) is the default.
15
+ * 6. `onMissing` (no anchor + no meta) → fail-closed select with warning.
16
+ */
17
+ import type { SelectionResult } from './types.js';
18
+ export interface SelectAffectedInput {
19
+ evalFiles: string[];
20
+ changedFiles: string[];
21
+ repoRoot: string;
22
+ baseRef: string;
23
+ /**
24
+ * Repo-level "rerun everything" triggers from `pathgrade()` plugin
25
+ * options (`affected.global`). Any match short-circuits selection and
26
+ * every discovered eval is selected with reason `global-match`.
27
+ */
28
+ global?: string[];
29
+ }
30
+ export declare function selectAffected(input: SelectAffectedInput): SelectionResult;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Core selection pipeline for affected-eval-selection.
3
+ *
4
+ * Given a set of discovered eval files and a set of changed files,
5
+ * computes which evals should run. See `docs/003-affected-eval-selection.md`
6
+ * §"Design Overview" and §"Precedence" for the authoritative specification.
7
+ *
8
+ * Precedence (when multiple mechanisms apply):
9
+ * 1. `affected.global` match short-circuits and selects every discovered
10
+ * eval. (Wiring lands in Issue 8.)
11
+ * 2. `alwaysRun: true` unconditionally unions the eval into selection.
12
+ * 3. `deps` (full override) replaces the auto-detected skill root.
13
+ * 4. `extraDeps` is unioned with `deps` or the skill root.
14
+ * 5. Auto-detected skill root (`<skillRoot>/**`) is the default.
15
+ * 6. `onMissing` (no anchor + no meta) → fail-closed select with warning.
16
+ */
17
+ import * as path from 'path';
18
+ import { findSkillRoot } from './anchor.js';
19
+ import { parsePathgradeMeta } from './meta.js';
20
+ import { intersect } from './glob.js';
21
+ export function selectAffected(input) {
22
+ const { evalFiles, changedFiles, repoRoot, baseRef, global } = input;
23
+ const selected = [];
24
+ const skipped = [];
25
+ const warnings = [];
26
+ // Precedence #1: global match short-circuits everything.
27
+ if (global && global.length > 0) {
28
+ const hit = intersect(global, changedFiles);
29
+ if (hit.matched) {
30
+ for (const evalFile of evalFiles) {
31
+ selected.push({ file: evalFile, reason: 'global-match' });
32
+ }
33
+ selected.sort((a, b) => a.file.localeCompare(b.file));
34
+ return {
35
+ baseRef,
36
+ changedFiles: [...changedFiles],
37
+ globalMatch: hit.matchedGlob,
38
+ selected,
39
+ skipped,
40
+ warnings,
41
+ };
42
+ }
43
+ }
44
+ for (const evalFile of evalFiles) {
45
+ const absEval = path.resolve(repoRoot, evalFile);
46
+ const skillRoot = findSkillRoot(absEval, repoRoot);
47
+ const meta = parsePathgradeMeta(absEval);
48
+ // Precedence #2: alwaysRun wins over dep matching.
49
+ if (meta?.alwaysRun === true) {
50
+ selected.push({ file: evalFile, reason: 'always-run' });
51
+ continue;
52
+ }
53
+ // Precedence #6: fail-closed when we have no signal at all.
54
+ if (!skillRoot && !meta) {
55
+ selected.push({ file: evalFile, reason: 'on-missing-fail-closed' });
56
+ warnings.push(`${evalFile}: no SKILL.md anchor and no __pathgradeMeta — selecting fail-closed`);
57
+ continue;
58
+ }
59
+ const depGlobs = resolveDepGlobs(skillRoot, meta);
60
+ if (depGlobs.length === 0) {
61
+ // Meta present but declares no deps (e.g., `{}`). Treat as
62
+ // fail-closed — the user left deps ambiguous.
63
+ selected.push({ file: evalFile, reason: 'on-missing-fail-closed' });
64
+ warnings.push(`${evalFile}: __pathgradeMeta declares no deps and no SKILL.md anchor — selecting fail-closed`);
65
+ continue;
66
+ }
67
+ const hit = intersect(depGlobs, changedFiles);
68
+ if (hit.matched) {
69
+ selected.push({
70
+ file: evalFile,
71
+ reason: 'deps-match',
72
+ matchedGlob: hit.matchedGlob,
73
+ });
74
+ }
75
+ else {
76
+ skipped.push({ file: evalFile, reason: 'no-matching-deps' });
77
+ }
78
+ }
79
+ selected.sort((a, b) => a.file.localeCompare(b.file));
80
+ skipped.sort((a, b) => a.file.localeCompare(b.file));
81
+ return {
82
+ baseRef,
83
+ changedFiles: [...changedFiles],
84
+ selected,
85
+ skipped,
86
+ warnings,
87
+ };
88
+ }
89
+ function resolveDepGlobs(skillRoot, meta) {
90
+ const globs = [];
91
+ if (meta?.deps && meta.deps.length > 0) {
92
+ globs.push(...meta.deps);
93
+ }
94
+ else if (skillRoot) {
95
+ globs.push(`${skillRoot}/**`);
96
+ }
97
+ if (meta?.extraDeps && meta.extraDeps.length > 0) {
98
+ globs.push(...meta.extraDeps);
99
+ }
100
+ return globs;
101
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Selection sidecar — plumbing between CLI and reporter.
3
+ *
4
+ * `pathgrade run --changed` computes selection in the CLI, before vitest
5
+ * spawns. `PathgradeReporter` runs inside the vitest process and can't
6
+ * see that decision directly. The sidecar bridges them: the CLI writes a
7
+ * small JSON file at `.pathgrade/selection.json`; the reporter reads it
8
+ * (if present) at run-end and merges it into `results.json`.
9
+ *
10
+ * Shape mirrors Issue 10's wire format but flattens `selected` to file
11
+ * paths and summarizes `changed_files` as a count — downstream consumers
12
+ * (PR comments, dashboards) want the decision, not the per-trial reason
13
+ * breakdown.
14
+ */
15
+ import type { SelectionResult } from './types.js';
16
+ import type { PathgradeSelectionReport } from '../types.js';
17
+ export declare const SIDECAR_FILENAME = "selection.json";
18
+ export declare function getSidecarPath(cwd: string): string;
19
+ /**
20
+ * Flatten a `SelectionResult` into the compact shape written to
21
+ * `.pathgrade/selection.json` (and merged into `results.json` as
22
+ * `PathgradeReport.selection`).
23
+ */
24
+ export declare function toSelectionReport(result: SelectionResult): PathgradeSelectionReport;
25
+ export declare function writeSidecar(cwd: string, result: SelectionResult): Promise<void>;
26
+ /**
27
+ * Read the sidecar if present. Returns `null` when missing, and `null`
28
+ * with an optional warning when the file exists but is malformed — the
29
+ * reporter is expected to tolerate corruption rather than break the run.
30
+ */
31
+ export declare function readSidecar(cwd: string, onWarning?: (msg: string) => void): Promise<PathgradeSelectionReport | null>;
32
+ export declare function clearSidecar(cwd: string): Promise<void>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Selection sidecar — plumbing between CLI and reporter.
3
+ *
4
+ * `pathgrade run --changed` computes selection in the CLI, before vitest
5
+ * spawns. `PathgradeReporter` runs inside the vitest process and can't
6
+ * see that decision directly. The sidecar bridges them: the CLI writes a
7
+ * small JSON file at `.pathgrade/selection.json`; the reporter reads it
8
+ * (if present) at run-end and merges it into `results.json`.
9
+ *
10
+ * Shape mirrors Issue 10's wire format but flattens `selected` to file
11
+ * paths and summarizes `changed_files` as a count — downstream consumers
12
+ * (PR comments, dashboards) want the decision, not the per-trial reason
13
+ * breakdown.
14
+ */
15
+ import * as path from 'path';
16
+ import fs from 'fs-extra';
17
+ import { getPathgradeDir } from '../reporters/results-path.js';
18
+ export const SIDECAR_FILENAME = 'selection.json';
19
+ export function getSidecarPath(cwd) {
20
+ return path.join(getPathgradeDir(cwd), SIDECAR_FILENAME);
21
+ }
22
+ /**
23
+ * Flatten a `SelectionResult` into the compact shape written to
24
+ * `.pathgrade/selection.json` (and merged into `results.json` as
25
+ * `PathgradeReport.selection`).
26
+ */
27
+ export function toSelectionReport(result) {
28
+ const report = {
29
+ base_ref: result.baseRef,
30
+ changed_files_count: result.changedFiles.length,
31
+ selected: result.selected.map(s => s.file).sort(),
32
+ skipped: [...result.skipped].sort((a, b) => a.file.localeCompare(b.file)).map(s => ({
33
+ file: s.file,
34
+ reason: s.reason,
35
+ })),
36
+ };
37
+ if (result.globalMatch !== undefined)
38
+ report.global_match = result.globalMatch;
39
+ return report;
40
+ }
41
+ export async function writeSidecar(cwd, result) {
42
+ const sidecarPath = getSidecarPath(cwd);
43
+ await fs.ensureDir(path.dirname(sidecarPath));
44
+ await fs.writeJSON(sidecarPath, toSelectionReport(result), { spaces: 2 });
45
+ }
46
+ /**
47
+ * Read the sidecar if present. Returns `null` when missing, and `null`
48
+ * with an optional warning when the file exists but is malformed — the
49
+ * reporter is expected to tolerate corruption rather than break the run.
50
+ */
51
+ export async function readSidecar(cwd, onWarning) {
52
+ const sidecarPath = getSidecarPath(cwd);
53
+ if (!(await fs.pathExists(sidecarPath)))
54
+ return null;
55
+ try {
56
+ const raw = await fs.readJSON(sidecarPath);
57
+ if (!isSelectionReport(raw)) {
58
+ onWarning?.(`${sidecarPath}: malformed sidecar (shape mismatch), ignoring.`);
59
+ return null;
60
+ }
61
+ return raw;
62
+ }
63
+ catch (err) {
64
+ const msg = err instanceof Error ? err.message : String(err);
65
+ onWarning?.(`${sidecarPath}: malformed sidecar (${msg}), ignoring.`);
66
+ return null;
67
+ }
68
+ }
69
+ export async function clearSidecar(cwd) {
70
+ const sidecarPath = getSidecarPath(cwd);
71
+ try {
72
+ await fs.remove(sidecarPath);
73
+ }
74
+ catch {
75
+ // Best-effort clear — a missing file is the expected case.
76
+ }
77
+ }
78
+ function isSelectionReport(value) {
79
+ if (!value || typeof value !== 'object')
80
+ return false;
81
+ const v = value;
82
+ return (typeof v.base_ref === 'string' &&
83
+ typeof v.changed_files_count === 'number' &&
84
+ Array.isArray(v.selected) &&
85
+ Array.isArray(v.skipped));
86
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared types for the affected-eval-selection pipeline.
3
+ *
4
+ * This file is the single source of truth for the `SelectionResult` shape
5
+ * and `SelectionReason` enum. Extended across Issues 5–11; do not duplicate.
6
+ */
7
+ export type SelectionReason = 'deps-match' | 'always-run' | 'global-match' | 'on-missing-fail-closed';
8
+ export interface SelectionEntry {
9
+ file: string;
10
+ reason: SelectionReason;
11
+ /** Glob that matched a changed file (populated for `deps-match` / `global-match`). */
12
+ matchedGlob?: string;
13
+ }
14
+ export interface SelectionSkipped {
15
+ file: string;
16
+ reason: 'no-matching-deps';
17
+ }
18
+ export interface SelectionResult {
19
+ /** `"<ref>@<sha>"`, or `"explicit"` for `--changed-files` inputs. */
20
+ baseRef: string;
21
+ changedFiles: string[];
22
+ /** Glob from `affected.global` that fired short-circuit, if any. */
23
+ globalMatch?: string;
24
+ selected: SelectionEntry[];
25
+ skipped: SelectionSkipped[];
26
+ warnings: string[];
27
+ }
28
+ /**
29
+ * Public SDK type — exported from `@wix/pathgrade` so evals can annotate
30
+ * their `__pathgradeMeta` export with a typed shape.
31
+ *
32
+ * `onMissing` is intentionally NOT a user-writable field: it names
33
+ * pathgrade's *runtime behavior* when an eval has neither a SKILL.md
34
+ * anchor nor an explicit meta declaration.
35
+ */
36
+ export interface PathgradeMeta {
37
+ /** Full-override dep globs (repo-relative). Replaces auto-detected SKILL.md anchor. */
38
+ deps?: string[];
39
+ /** Additional dep globs unioned with the auto-detected SKILL.md anchor. */
40
+ extraDeps?: string[];
41
+ /** When true, unconditionally include this eval in every `pathgrade run --changed`. */
42
+ alwaysRun?: boolean;
43
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared types for the affected-eval-selection pipeline.
3
+ *
4
+ * This file is the single source of truth for the `SelectionResult` shape
5
+ * and `SelectionReason` enum. Extended across Issues 5–11; do not duplicate.
6
+ */
7
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Per-turn answer store shared by the ask-user-bridge (writer) and the
3
+ * SDK-message-projector (reader).
4
+ *
5
+ * The bridge resolves an `AskUserQuestion` ask-bus batch synchronously inside
6
+ * its `canUseTool` callback and writes the resulting `answers` map plus the
7
+ * answer source (`'reaction' | 'fallback' | 'declined'`) keyed by the SDK's
8
+ * `toolUseID`. After the turn's `for await` of the typed-message stream
9
+ * completes, the projector consults the store while building the `ToolEvent`
10
+ * for each `AskUserQuestion` tool-use block — the structured input the SDK
11
+ * already gave us, plus the answer values and source the bridge attached.
12
+ *
13
+ * This is the only piece of cross-module state the bridge owns. It exists
14
+ * because the SDK's `canUseTool` callback runs ahead of the assistant message
15
+ * the projector ultimately sees — both sides need a join key.
16
+ */
17
+ import type { AskAnswerSource } from '../../sdk/ask-bus/types.js';
18
+ export interface AskUserAnswerEntry {
19
+ /** The SDK-shape `answers` map (question text → answer string). */
20
+ readonly answers: Record<string, string>;
21
+ /** Source tag from the `AskAnswer` resolution — what the projector stamps. */
22
+ readonly source: AskAnswerSource;
23
+ }
24
+ export interface AskUserAnswerStore {
25
+ record(toolUseId: string, entry: AskUserAnswerEntry): void;
26
+ get(toolUseId: string | undefined): AskUserAnswerEntry | undefined;
27
+ }
28
+ export declare function createAskUserAnswerStore(): AskUserAnswerStore;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Per-turn answer store shared by the ask-user-bridge (writer) and the
3
+ * SDK-message-projector (reader).
4
+ *
5
+ * The bridge resolves an `AskUserQuestion` ask-bus batch synchronously inside
6
+ * its `canUseTool` callback and writes the resulting `answers` map plus the
7
+ * answer source (`'reaction' | 'fallback' | 'declined'`) keyed by the SDK's
8
+ * `toolUseID`. After the turn's `for await` of the typed-message stream
9
+ * completes, the projector consults the store while building the `ToolEvent`
10
+ * for each `AskUserQuestion` tool-use block — the structured input the SDK
11
+ * already gave us, plus the answer values and source the bridge attached.
12
+ *
13
+ * This is the only piece of cross-module state the bridge owns. It exists
14
+ * because the SDK's `canUseTool` callback runs ahead of the assistant message
15
+ * the projector ultimately sees — both sides need a join key.
16
+ */
17
+ export function createAskUserAnswerStore() {
18
+ const entries = new Map();
19
+ return {
20
+ record(toolUseId, entry) {
21
+ entries.set(toolUseId, entry);
22
+ },
23
+ get(toolUseId) {
24
+ if (toolUseId === undefined)
25
+ return undefined;
26
+ return entries.get(toolUseId);
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Ask-user bridge — `canUseTool` callback expressed as a pure factory over
3
+ * `(askBus, getTurnNumber, answerStore)`.
4
+ *
5
+ * - For every tool name that is NOT `AskUserQuestion`, the bridge returns
6
+ * `{ behavior: 'allow', updatedInput: input }`. That matches the
7
+ * `permissionMode: 'default'` choice — every tool decision still routes
8
+ * through this callback, but only `AskUserQuestion` actually blocks on
9
+ * the ask-bus.
10
+ * - For `AskUserQuestion`, the bridge constructs a live `AskBatch` from
11
+ * the SDK's already-typed `AskUserQuestionInput`, emits it onto the bus,
12
+ * awaits resolution, and returns the SDK's documented `answers` shape
13
+ * (`{ [questionText]: string }`, multi-select comma-joined per the SDK's
14
+ * own field comment) on `updatedInput`.
15
+ * - **Declined resolution → SDK deny.** When every `AskAnswer` carries
16
+ * `source: 'declined'` (the `'decline'` fallback path, plus the
17
+ * declined-shape any `'error'` unmatched signal produces), the bridge
18
+ * surfaces the SDK's `{ behavior: 'deny', message: 'User declined to
19
+ * answer' }` instead of `'allow'` with empty answer strings.
20
+ * - **Bus rejection → SDK deny + `lastError()`.** A bus timeout (or any
21
+ * other rejection) becomes `{ behavior: 'deny', message: err.message }`
22
+ * to the SDK *and* is captured on `lastError()` so the driver can
23
+ * re-throw after the SDK stream ends. The conversation runner's catch
24
+ * reports `completionReason: 'error'` with the same message.
25
+ */
26
+ import type { CanUseTool } from '@anthropic-ai/claude-agent-sdk';
27
+ import type { AskBus } from '../../sdk/ask-bus/types.js';
28
+ import type { AskUserAnswerStore } from './ask-user-answer-store.js';
29
+ export interface AskUserBridgeDeps {
30
+ /** Per-conversation bus from `AgentSessionOptions.askBus`. */
31
+ askBus: AskBus;
32
+ /** Returns the current turn number — driver increments before runTurn. */
33
+ getTurnNumber: () => number;
34
+ /** Per-turn answer store written here, read by the projector. */
35
+ answerStore: AskUserAnswerStore;
36
+ }
37
+ /**
38
+ * The bridge is a callable `CanUseTool` with two side accessors used by the
39
+ * driver to bubble bus rejections out of the turn loop:
40
+ *
41
+ * - `lastError()` returns the most recent ask-bus rejection captured by
42
+ * this bridge instance, or null. The driver checks it after the SDK
43
+ * stream ends; a non-null value gets thrown so `runConversation` reports
44
+ * `completionReason: 'error'` with the bus error message.
45
+ * - `clearLastError()` resets the field — called at the top of each turn
46
+ * so a stale error from a prior turn never triggers a spurious throw.
47
+ *
48
+ * Encoded as attached function properties so existing call sites
49
+ * (`const canUseTool = createAskUserBridge(...); canUseTool(...)`) keep
50
+ * working unchanged.
51
+ */
52
+ export type AskUserBridge = CanUseTool & {
53
+ lastError(): Error | null;
54
+ clearLastError(): void;
55
+ };
56
+ export declare function createAskUserBridge(deps: AskUserBridgeDeps): AskUserBridge;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Ask-user bridge — `canUseTool` callback expressed as a pure factory over
3
+ * `(askBus, getTurnNumber, answerStore)`.
4
+ *
5
+ * - For every tool name that is NOT `AskUserQuestion`, the bridge returns
6
+ * `{ behavior: 'allow', updatedInput: input }`. That matches the
7
+ * `permissionMode: 'default'` choice — every tool decision still routes
8
+ * through this callback, but only `AskUserQuestion` actually blocks on
9
+ * the ask-bus.
10
+ * - For `AskUserQuestion`, the bridge constructs a live `AskBatch` from
11
+ * the SDK's already-typed `AskUserQuestionInput`, emits it onto the bus,
12
+ * awaits resolution, and returns the SDK's documented `answers` shape
13
+ * (`{ [questionText]: string }`, multi-select comma-joined per the SDK's
14
+ * own field comment) on `updatedInput`.
15
+ * - **Declined resolution → SDK deny.** When every `AskAnswer` carries
16
+ * `source: 'declined'` (the `'decline'` fallback path, plus the
17
+ * declined-shape any `'error'` unmatched signal produces), the bridge
18
+ * surfaces the SDK's `{ behavior: 'deny', message: 'User declined to
19
+ * answer' }` instead of `'allow'` with empty answer strings.
20
+ * - **Bus rejection → SDK deny + `lastError()`.** A bus timeout (or any
21
+ * other rejection) becomes `{ behavior: 'deny', message: err.message }`
22
+ * to the SDK *and* is captured on `lastError()` so the driver can
23
+ * re-throw after the SDK stream ends. The conversation runner's catch
24
+ * reports `completionReason: 'error'` with the same message.
25
+ */
26
+ export function createAskUserBridge(deps) {
27
+ const { askBus, getTurnNumber, answerStore } = deps;
28
+ let lastError = null;
29
+ const canUseTool = async (toolName, input, options) => {
30
+ if (toolName !== 'AskUserQuestion') {
31
+ return { behavior: 'allow', updatedInput: input };
32
+ }
33
+ const turnNumber = getTurnNumber();
34
+ const batch = buildBatchFromInput(input, turnNumber, options.toolUseID);
35
+ const handle = askBus.emit(batch);
36
+ let resolution;
37
+ try {
38
+ resolution = await handle.resolution;
39
+ }
40
+ catch (err) {
41
+ // Ask-bus timeouts and other rejections deny the SDK with
42
+ // the underlying error message AND surface on `lastError()` so
43
+ // the driver re-throws after the turn ends. The conversation
44
+ // runner's catch reports `completionReason: 'error'` with the
45
+ // same message — no half-state where the SDK keeps running on
46
+ // a tool refusal that the runner never noticed.
47
+ lastError = err instanceof Error ? err : new Error(String(err));
48
+ return { behavior: 'deny', message: lastError.message };
49
+ }
50
+ // Live batches always settle (either to a resolution or a rejection).
51
+ // A null resolution means a 'post-hoc' batch — impossible here since
52
+ // we just emitted lifecycle: 'live'. Throw if the bus contract is
53
+ // ever weakened upstream.
54
+ if (resolution === null) {
55
+ throw new Error(`ask-user bridge: live batch ${batch.batchId} resolved to null`);
56
+ }
57
+ const answers = buildSdkAnswers(batch.questions, resolution.answers);
58
+ const source = pickAnswerSource(resolution.answers);
59
+ answerStore.record(options.toolUseID, { answers, source });
60
+ // A fully-declined resolution (every answer `source: 'declined'`)
61
+ // surfaces as the SDK's documented `deny` shape. Returning an `allow`
62
+ // with empty `answers` strings would make Claude believe the user
63
+ // typed empty answers — strictly worse than telling the SDK the
64
+ // tool call was refused. Mixed-disposition resolutions (e.g. one
65
+ // declined among reactions) still allow with whatever `answers`
66
+ // the bus yielded, since at least one question carries a real value.
67
+ if (resolution.answers.length > 0
68
+ && resolution.answers.every((a) => a.source === 'declined')) {
69
+ return { behavior: 'deny', message: 'User declined to answer' };
70
+ }
71
+ const allow = {
72
+ behavior: 'allow',
73
+ updatedInput: { ...input, answers },
74
+ };
75
+ return allow;
76
+ };
77
+ const bridge = canUseTool;
78
+ bridge.lastError = () => lastError;
79
+ bridge.clearLastError = () => { lastError = null; };
80
+ return bridge;
81
+ }
82
+ /**
83
+ * Build an `AskBatch` (lifecycle: 'live') from the SDK's `AskUserQuestionInput`.
84
+ *
85
+ * The SDK input is already typed and validated (1-4 questions, 2-4 options
86
+ * each), so this is a structural translation rather than a parse. The batch
87
+ * id reuses the SDK's `toolUseID` so the projector can join the resolution
88
+ * back onto the matching `tool_use` block in the assistant message.
89
+ */
90
+ function buildBatchFromInput(input, turnNumber, toolUseId) {
91
+ const rawQuestions = Array.isArray(input.questions) ? input.questions : [];
92
+ const questions = [];
93
+ rawQuestions.forEach((raw, index) => {
94
+ const text = typeof raw.question === 'string' ? raw.question : '';
95
+ const header = typeof raw.header === 'string' && raw.header.length > 0
96
+ ? raw.header
97
+ : undefined;
98
+ const options = readOptions(raw.options);
99
+ questions.push({
100
+ id: `q-${index}`,
101
+ ...(header ? { header } : {}),
102
+ question: text,
103
+ options,
104
+ isOther: false,
105
+ isSecret: false,
106
+ });
107
+ });
108
+ return {
109
+ batchId: toolUseId,
110
+ turnNumber,
111
+ source: 'claude',
112
+ lifecycle: 'live',
113
+ sourceTool: 'AskUserQuestion',
114
+ toolUseId,
115
+ questions,
116
+ };
117
+ }
118
+ function readOptions(raw) {
119
+ if (!Array.isArray(raw))
120
+ return null;
121
+ return raw.map((opt) => {
122
+ const label = typeof opt.label === 'string' ? opt.label : '';
123
+ const description = typeof opt.description === 'string' && opt.description.length > 0
124
+ ? opt.description
125
+ : undefined;
126
+ return description ? { label, description } : { label };
127
+ });
128
+ }
129
+ /**
130
+ * Translate the bus's `AskAnswer[]` into the SDK's documented `answers` shape
131
+ * — `{ [questionText]: string }` per `sdk-tools.d.ts:2702`. Multi-select is
132
+ * joined with a single comma; the SDK's own field comment specifies that
133
+ * "multi-select answers are comma-separated" without prescribing whitespace,
134
+ * so we follow whatever the bundled binary expects rather than imposing a
135
+ * stricter "comma-space" assumption.
136
+ */
137
+ function buildSdkAnswers(questions, answers) {
138
+ const byId = new Map();
139
+ for (const a of answers)
140
+ byId.set(a.questionId, a);
141
+ const out = {};
142
+ for (const q of questions) {
143
+ const a = byId.get(q.id);
144
+ if (!a)
145
+ continue;
146
+ out[q.question] = a.values.join(',');
147
+ }
148
+ return out;
149
+ }
150
+ /**
151
+ * Pick a single source tag for the whole batch. In the happy path every
152
+ * question is answered by the same disposition; if mixed, the most-specific
153
+ * source wins (`reaction` > `fallback` > `declined`). The projector consumes
154
+ * a single tag per tool event.
155
+ */
156
+ function pickAnswerSource(answers) {
157
+ if (answers.some((a) => a.source === 'reaction'))
158
+ return 'reaction';
159
+ if (answers.some((a) => a.source === 'fallback'))
160
+ return 'fallback';
161
+ return 'declined';
162
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * SDK message projector — typed `SDKMessage[]` → `AgentTurnResult`.
3
+ *
4
+ * Replaces the legacy NDJSON parser (`extractClaudeStreamJsonEvents`). Pure
5
+ * function: no I/O, no ask-bus, no session state. The Claude SDK driver
6
+ * buffers each turn's `SDKMessage` stream (`for await`) and hands the array
7
+ * here; the projector returns the public `AgentTurnResult` plus the
8
+ * `session_id` the orchestrator threads into the next turn's
9
+ * `Options.resume`.
10
+ *
11
+ * Boundary with the live ask-user bridge: for an `AskUserQuestion`
12
+ * tool-use block the projector emits a `ToolEvent` whose `arguments` shape
13
+ * is the structured `AskUserQuestionInput` (questions / headers / options /
14
+ * multiSelect) plus `answerSource: 'unknown'`. The bridge attaches the
15
+ * answer values and the `'reaction' | 'fallback' | 'declined'` source tag
16
+ * onto the same envelope; the projector itself never mints them.
17
+ */
18
+ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
19
+ import type { AgentTurnResult } from '../../types.js';
20
+ import type { AskUserAnswerStore } from './ask-user-answer-store.js';
21
+ export interface ProjectTurnInput {
22
+ /** Buffered typed-message stream from one `query()` call. */
23
+ messages: SDKMessage[];
24
+ /** Forwarded onto `ToolEvent.turnNumber`; optional for projector unit use. */
25
+ turnNumber?: number;
26
+ /** First user message of the turn — used for slash-command skill detection. */
27
+ firstMessage?: string;
28
+ /**
29
+ * Optional per-turn answer store written by the ask-user-bridge.
30
+ * When present, the projector merges `answers` + `answerSource` from the
31
+ * store onto each `AskUserQuestion` ToolEvent envelope using the SDK's
32
+ * `toolUseID` (the tool-use block `id`) as the join key. When absent or
33
+ * empty for a given `toolUseID`, the projector keeps the boundary stamp
34
+ * `answerSource: 'unknown'` it stamped before the bridge existed.
35
+ */
36
+ answerStore?: AskUserAnswerStore;
37
+ }
38
+ export interface ProjectedTurn {
39
+ result: AgentTurnResult;
40
+ /** `session_id` reported on init/result; the orchestrator uses this for `resume`. */
41
+ sessionId?: string;
42
+ }
43
+ export declare function projectSdkMessages(input: ProjectTurnInput): ProjectedTurn;