pi-harness-runtime 1.1.406 → 1.1.407

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 (128) hide show
  1. package/index.ts +67 -0
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/durable-review/package.json +1 -1
  31. package/packages/e2e-tester/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-sink/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/logging/package.json +1 -1
  52. package/packages/mcp-adapter/package.json +1 -1
  53. package/packages/memory-engine/package.json +1 -1
  54. package/packages/milestone-manager/package.json +1 -1
  55. package/packages/model-registry/package.json +1 -1
  56. package/packages/nextjs-plugin/package.json +1 -1
  57. package/packages/notification/package.json +1 -1
  58. package/packages/observability/package.json +1 -1
  59. package/packages/okf-indexer/package.json +1 -1
  60. package/packages/paperclip-integration/package.json +1 -1
  61. package/packages/performance-optimizer/package.json +1 -1
  62. package/packages/permission-mode/package.json +1 -1
  63. package/packages/privilege-broker/package.json +1 -1
  64. package/packages/project-analyzer/package.json +1 -1
  65. package/packages/project-bootstrap/package.json +1 -1
  66. package/packages/projection-engine/package.json +1 -1
  67. package/packages/prompt-compiler/package.json +1 -1
  68. package/packages/prompt-versioning/package.json +1 -1
  69. package/packages/provider-adapter-sdk/package.json +1 -1
  70. package/packages/provider-router/package.json +1 -1
  71. package/packages/provider-selector/package.json +1 -1
  72. package/packages/providers/package.json +1 -1
  73. package/packages/qdrant-skills/package.json +1 -1
  74. package/packages/quota-manager/package.json +1 -1
  75. package/packages/rate-limiter/package.json +1 -1
  76. package/packages/react-vite-plugin/package.json +1 -1
  77. package/packages/relay/package.json +1 -1
  78. package/packages/release-manager/package.json +1 -1
  79. package/packages/requirement-compiler/package.json +1 -1
  80. package/packages/runtime/package.json +1 -1
  81. package/packages/scheduler/package.json +1 -1
  82. package/packages/scheduler-adapter/package.json +1 -1
  83. package/packages/session/package.json +1 -1
  84. package/packages/session-api/package.json +1 -1
  85. package/packages/session-export/package.json +1 -1
  86. package/packages/shared-context/package.json +1 -1
  87. package/packages/skill-mcp-client/package.json +1 -1
  88. package/packages/skill-registry/package.json +1 -1
  89. package/packages/skills/package.json +1 -1
  90. package/packages/sprint-planner/package.json +1 -1
  91. package/packages/subscription-engine/package.json +1 -1
  92. package/packages/task-compiler/package.json +1 -1
  93. package/packages/telegram/package.json +1 -1
  94. package/packages/tencentdb-memory/package.json +1 -1
  95. package/packages/tencentdb-sync/package.json +1 -1
  96. package/packages/test-data-generator/package.json +1 -1
  97. package/packages/test-generator/package.json +1 -1
  98. package/packages/todo-bd-sync/package.json +1 -1
  99. package/packages/token-estimation/package.json +1 -1
  100. package/packages/token-optimizer/package.json +1 -1
  101. package/packages/tooling-preference/dist/changed-files.d.ts +45 -0
  102. package/packages/tooling-preference/dist/changed-files.d.ts.map +1 -0
  103. package/packages/tooling-preference/dist/changed-files.js +125 -0
  104. package/packages/tooling-preference/dist/changed-files.js.map +1 -0
  105. package/packages/tooling-preference/dist/index.d.ts +2 -0
  106. package/packages/tooling-preference/dist/index.d.ts.map +1 -1
  107. package/packages/tooling-preference/dist/index.js +2 -0
  108. package/packages/tooling-preference/dist/index.js.map +1 -1
  109. package/packages/tooling-preference/dist/quality-check.d.ts +75 -0
  110. package/packages/tooling-preference/dist/quality-check.d.ts.map +1 -0
  111. package/packages/tooling-preference/dist/quality-check.js +216 -0
  112. package/packages/tooling-preference/dist/quality-check.js.map +1 -0
  113. package/packages/tooling-preference/package.json +1 -1
  114. package/packages/tooling-preference/src/changed-files.ts +147 -0
  115. package/packages/tooling-preference/src/index.ts +19 -0
  116. package/packages/tooling-preference/src/quality-check.ts +279 -0
  117. package/packages/tooling-preference/tests/changed-files.test.ts +124 -0
  118. package/packages/tooling-preference/tests/quality-check.test.ts +272 -0
  119. package/packages/trajectory/package.json +1 -1
  120. package/packages/tui/package.json +1 -1
  121. package/packages/types/package.json +1 -1
  122. package/packages/voice-api/package.json +1 -1
  123. package/packages/webmcp-tester/package.json +1 -1
  124. package/packages/wiki/package.json +1 -1
  125. package/packages/workflow-events/package.json +1 -1
  126. package/packages/workspace-scanner/package.json +1 -1
  127. package/packages/worktree/package.json +1 -1
  128. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,279 @@
1
+ /**
2
+ * The post-change quality check of a JavaScript/TypeScript workspace.
3
+ *
4
+ * The check honors the saved preference: "prefer_biome" runs Biome on the
5
+ * changed files, "use_project_tools" runs the project's own lint script.
6
+ * The check never writes: it reports what it found and the agent fixes it.
7
+ */
8
+
9
+ import { spawnSync } from "node:child_process";
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import type { ToolingContext } from "./detect.js";
13
+ import type { ToolingPreference } from "./proposal.js";
14
+
15
+ /** The tool that checks the changed files. */
16
+ export type QualityCheckTool = "biome" | "project" | "none";
17
+
18
+ export interface QualityCheckPlan {
19
+ /** The tool that would run. */
20
+ tool: QualityCheckTool;
21
+ /** The binary to run - never a shell string. */
22
+ command: string | null;
23
+ /** The arguments of the command. */
24
+ args: string[];
25
+ /** The files the check looks at. */
26
+ files: string[];
27
+ /** Why this plan - a human wording. */
28
+ reason: string;
29
+ }
30
+
31
+ export interface QualityCheckResult {
32
+ /** Did the check run? */
33
+ ran: boolean;
34
+ /** Did the check pass? Meaningless when the check did not run. */
35
+ passed: boolean;
36
+ /** The tool that ran. */
37
+ tool: QualityCheckTool;
38
+ /** How many files the check looked at. */
39
+ fileCount: number;
40
+ /** The exit code of the check. */
41
+ exitCode: number | null;
42
+ /** Did the check hit the timeout? */
43
+ timedOut: boolean;
44
+ /** The output of the check, truncated. */
45
+ output: string;
46
+ /** Why the result is what it is - a human wording. */
47
+ reason: string;
48
+ }
49
+
50
+ /** A command to run: the binary and its arguments, never a shell string. */
51
+ interface CommandSpec {
52
+ command: string;
53
+ args: string[];
54
+ }
55
+
56
+ /** How the tool is started with the package manager of the workspace. */
57
+ function packageRunner(packageManager: string | null, binary: string): CommandSpec {
58
+ switch (packageManager) {
59
+ case "bun":
60
+ return { command: "bunx", args: [binary] };
61
+ case "pnpm":
62
+ return { command: "pnpm", args: ["exec", binary] };
63
+ case "yarn":
64
+ return { command: "yarn", args: [binary] };
65
+ case "npm":
66
+ return { command: "npx", args: ["--no-install", binary] };
67
+ default:
68
+ return { command: binary, args: [] };
69
+ }
70
+ }
71
+
72
+ /** How a package script is started with the package manager of the workspace. */
73
+ function scriptRunner(packageManager: string | null, script: string): CommandSpec {
74
+ switch (packageManager) {
75
+ case "bun":
76
+ return { command: "bun", args: ["run", script] };
77
+ case "pnpm":
78
+ return { command: "pnpm", args: ["run", script] };
79
+ case "yarn":
80
+ return { command: "yarn", args: [script] };
81
+ default:
82
+ return { command: "npm", args: ["run", script] };
83
+ }
84
+ }
85
+
86
+ /** The scripts of the workspace, if it has a package.json. */
87
+ export function readWorkspaceScripts(root: string): Record<string, string> {
88
+ try {
89
+ const path = join(root, "package.json");
90
+ if (!existsSync(path)) return {};
91
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as {
92
+ scripts?: Record<string, string>;
93
+ };
94
+ const scripts = parsed?.scripts;
95
+ if (!scripts || typeof scripts !== "object") return {};
96
+ return scripts;
97
+ } catch {
98
+ return {};
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Choose the check that runs after a change.
104
+ *
105
+ * The preference decides the tool: Biome when the user asked for it, the
106
+ * project's own script otherwise. A workspace without a saved decision is
107
+ * never checked - the user has not chosen a tool yet.
108
+ */
109
+ export function selectQualityCheck(options: {
110
+ context: ToolingContext;
111
+ preference: ToolingPreference | undefined;
112
+ files: string[];
113
+ scripts?: Record<string, string>;
114
+ }): QualityCheckPlan {
115
+ const { context, preference, files } = options;
116
+ const scripts = options.scripts ?? readWorkspaceScripts(context.root);
117
+
118
+ const none = (reason: string): QualityCheckPlan => ({
119
+ tool: "none",
120
+ command: null,
121
+ args: [],
122
+ files: [],
123
+ reason,
124
+ });
125
+
126
+ if (files.length === 0) {
127
+ return none("no JavaScript/TypeScript file changed");
128
+ }
129
+ if (!preference) {
130
+ return none("the user has not chosen a tool yet");
131
+ }
132
+
133
+ const projectScript = (): { script: string; spec: CommandSpec } | null => {
134
+ for (const name of ["lint", "typecheck"]) {
135
+ if (typeof scripts[name] === "string") {
136
+ return { script: name, spec: scriptRunner(context.packageManager, name) };
137
+ }
138
+ }
139
+ return null;
140
+ };
141
+
142
+ if (preference === "prefer_biome") {
143
+ if (!context.usesBiome) {
144
+ // The user asked for Biome, but the workspace does not have it
145
+ // (yet). The project's own tool is the honest fallback.
146
+ const fallback = projectScript();
147
+ if (!fallback) {
148
+ return none("Biome is not installed and the project has no lint script");
149
+ }
150
+ return {
151
+ tool: "project",
152
+ command: fallback.spec.command,
153
+ args: fallback.spec.args,
154
+ files,
155
+ reason: `Biome is not installed yet - the project script "${fallback.script}" runs instead`,
156
+ };
157
+ }
158
+ const runner = packageRunner(context.packageManager, "biome");
159
+ return {
160
+ tool: "biome",
161
+ command: runner.command,
162
+ args: [...runner.args, "check", ...files],
163
+ files,
164
+ reason: "the preference asks for Biome",
165
+ };
166
+ }
167
+
168
+ const script = projectScript();
169
+ if (!script) {
170
+ return none("the project has neither a lint nor a typecheck script");
171
+ }
172
+ return {
173
+ tool: "project",
174
+ command: script.spec.command,
175
+ args: script.spec.args,
176
+ files,
177
+ reason: `the project script "${script.script}" runs`,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Run the check.
183
+ *
184
+ * The command is started without a shell, with a timeout, and it never
185
+ * writes: a check that wants to fix files is not a check.
186
+ */
187
+ export function runQualityCheck(
188
+ plan: QualityCheckPlan,
189
+ options: { cwd: string; timeoutMs?: number; maxOutputChars?: number },
190
+ ): QualityCheckResult {
191
+ const maxOutputChars = options.maxOutputChars ?? 2000;
192
+ const base = {
193
+ tool: plan.tool,
194
+ fileCount: plan.files.length,
195
+ };
196
+
197
+ if (!plan.command) {
198
+ return {
199
+ ...base,
200
+ ran: false,
201
+ passed: false,
202
+ exitCode: null,
203
+ timedOut: false,
204
+ output: "",
205
+ reason: plan.reason,
206
+ };
207
+ }
208
+
209
+ try {
210
+ const result = spawnSync(plan.command, plan.args, {
211
+ cwd: options.cwd,
212
+ encoding: "utf8",
213
+ timeout: options.timeoutMs ?? 60_000,
214
+ maxBuffer: 8 * 1024 * 1024,
215
+ shell: false,
216
+ env: { ...process.env, CI: process.env.CI ?? "1" },
217
+ });
218
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
219
+ const error = result.error;
220
+ const timedOut = Boolean(error && /ETIMEDOUT/i.test(String(error.message)));
221
+ const exitCode = result.status ?? null;
222
+
223
+ if (error && !timedOut) {
224
+ // The binary does not exist: the check never started.
225
+ return {
226
+ ...base,
227
+ ran: false,
228
+ passed: false,
229
+ exitCode: null,
230
+ timedOut: false,
231
+ output: output.slice(0, maxOutputChars),
232
+ reason: `the check could not start: ${String(error.message).slice(0, 200)}`,
233
+ };
234
+ }
235
+
236
+ let reason = "the check reported problems";
237
+ if (timedOut) {
238
+ reason = "the check hit the timeout";
239
+ } else if (exitCode === 0) {
240
+ reason = "the check passed";
241
+ }
242
+ return {
243
+ ...base,
244
+ ran: true,
245
+ passed: !timedOut && exitCode === 0,
246
+ exitCode,
247
+ timedOut,
248
+ output: output.slice(0, maxOutputChars),
249
+ reason,
250
+ };
251
+ } catch (error) {
252
+ return {
253
+ ...base,
254
+ ran: false,
255
+ passed: false,
256
+ exitCode: null,
257
+ timedOut: false,
258
+ output: "",
259
+ reason: `the check could not start: ${String(error).slice(0, 200)}`,
260
+ };
261
+ }
262
+ }
263
+
264
+ /**
265
+ * The status line of the check.
266
+ *
267
+ * The line stays plain ASCII: it is read by the pi host and by terminals
268
+ * without a font of their own.
269
+ */
270
+ export function formatQualityCheckStatus(result: QualityCheckResult): string {
271
+ if (!result.ran) {
272
+ return `[TOOL] no check: ${result.reason}`;
273
+ }
274
+ const tool = result.tool === "biome" ? "biome" : "project lint";
275
+ if (result.timedOut) {
276
+ return `[TOOL] ${tool}: ${result.fileCount} file(s), timeout`;
277
+ }
278
+ return `[TOOL] ${tool}: ${result.fileCount} file(s), ${result.passed ? "ok" : "failed"}`;
279
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The changed JavaScript/TypeScript files of a workspace.
3
+ *
4
+ * The detection reads the worktree of the session: a file that changed is
5
+ * a file the check may read. A workspace without git yields nothing - the
6
+ * harness never guesses which files belong to the session.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
10
+ import { execFileSync } from "node:child_process";
11
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import {
15
+ detectChangedJsTsFiles,
16
+ isIgnoredPath,
17
+ isJsTsFile,
18
+ parseGitStatus,
19
+ } from "../src/changed-files.js";
20
+
21
+ let root = "";
22
+
23
+ function git(...args: string[]): void {
24
+ execFileSync("git", args, { cwd: root, stdio: "ignore" });
25
+ }
26
+
27
+ beforeEach(() => {
28
+ root = mkdtempSync(join(tmpdir(), "tooling-changed-"));
29
+ git("init", "-q");
30
+ git("config", "user.email", "test@example.com");
31
+ git("config", "user.name", "Test");
32
+ });
33
+
34
+ afterEach(() => {
35
+ rmSync(root, { recursive: true, force: true });
36
+ });
37
+
38
+ describe("the changed file detection", () => {
39
+ it("finds the JavaScript and TypeScript files of the worktree", () => {
40
+ mkdirSync(join(root, "src"), { recursive: true });
41
+ writeFileSync(join(root, "src", "app.ts"), "export const a = 1;\n");
42
+ writeFileSync(join(root, "src", "app.tsx"), "export const B = 1;\n");
43
+ writeFileSync(join(root, "src", "notes.md"), "# notes\n");
44
+ writeFileSync(join(root, "src", "styles.css"), "body {}\n");
45
+
46
+ const result = detectChangedJsTsFiles(root);
47
+
48
+ expect(result.detected).toBe(true);
49
+ expect(result.files).toEqual(["src/app.ts", "src/app.tsx"]);
50
+ });
51
+
52
+ it("finds the files that were changed after the commit", () => {
53
+ writeFileSync(join(root, "a.ts"), "export const a = 1;\n");
54
+ git("add", ".");
55
+ git("commit", "-q", "-m", "first");
56
+ writeFileSync(join(root, "a.ts"), "export const a = 2;\n");
57
+
58
+ const result = detectChangedJsTsFiles(root);
59
+
60
+ expect(result.files).toEqual(["a.ts"]);
61
+ });
62
+
63
+ it("finds a file that was renamed", () => {
64
+ writeFileSync(join(root, "old.ts"), "export const a = 1;\n");
65
+ git("add", ".");
66
+ git("commit", "-q", "-m", "first");
67
+ git("mv", "old.ts", "new.ts");
68
+
69
+ const result = detectChangedJsTsFiles(root);
70
+
71
+ expect(result.files).toEqual(["new.ts"]);
72
+ });
73
+
74
+ it("ignores the files of a build directory", () => {
75
+ mkdirSync(join(root, "dist"), { recursive: true });
76
+ writeFileSync(join(root, "dist", "bundle.js"), "// built\n");
77
+
78
+ const result = detectChangedJsTsFiles(root);
79
+
80
+ expect(result.files).toEqual([]);
81
+ });
82
+
83
+ it("yields nothing in a workspace that is not a repository", () => {
84
+ const plain = mkdtempSync(join(tmpdir(), "tooling-plain-"));
85
+ try {
86
+ writeFileSync(join(plain, "app.ts"), "export const a = 1;\n");
87
+
88
+ const result = detectChangedJsTsFiles(plain);
89
+
90
+ expect(result.detected).toBe(false);
91
+ expect(result.files).toEqual([]);
92
+ } finally {
93
+ rmSync(plain, { recursive: true, force: true });
94
+ }
95
+ });
96
+ });
97
+
98
+ describe("the git status parser", () => {
99
+ it("reads the path of every record", () => {
100
+ const output = " M src/app.ts\0?? src/new.ts\0";
101
+ expect(parseGitStatus(output)).toEqual(["src/app.ts", "src/new.ts"]);
102
+ });
103
+
104
+ it("skips the original path of a rename", () => {
105
+ const output = "R src/new.ts\0src/old.ts\0";
106
+ expect(parseGitStatus(output)).toEqual(["src/new.ts"]);
107
+ });
108
+ });
109
+
110
+ describe("the file filters", () => {
111
+ it("knows the JavaScript and TypeScript extensions", () => {
112
+ expect(isJsTsFile("src/app.ts")).toBe(true);
113
+ expect(isJsTsFile("src/app.tsx")).toBe(true);
114
+ expect(isJsTsFile("src/app.mjs")).toBe(true);
115
+ expect(isJsTsFile("src/app.json")).toBe(false);
116
+ expect(isJsTsFile("README.md")).toBe(false);
117
+ });
118
+
119
+ it("knows the directories that hold no source", () => {
120
+ expect(isIgnoredPath("node_modules/pkg/index.js")).toBe(true);
121
+ expect(isIgnoredPath("dist/bundle.js")).toBe(true);
122
+ expect(isIgnoredPath("src/app.ts")).toBe(false);
123
+ });
124
+ });
@@ -0,0 +1,272 @@
1
+ /**
2
+ * The post-change quality check.
3
+ *
4
+ * The saved preference decides the tool: Biome when the user asked for it,
5
+ * the project's own script otherwise. The check never writes and never runs
6
+ * without a decision of the user.
7
+ */
8
+
9
+ import { describe, it, expect, afterEach } from "bun:test";
10
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import {
14
+ formatQualityCheckStatus,
15
+ runQualityCheck,
16
+ selectQualityCheck,
17
+ } from "../src/quality-check.js";
18
+ import type { ToolingContext } from "../src/detect.js";
19
+
20
+ const cleanups: string[] = [];
21
+
22
+ function workspace(pkg: unknown, files: Record<string, string> = {}): string {
23
+ const root = mkdtempSync(join(tmpdir(), "tooling-check-"));
24
+ cleanups.push(root);
25
+ writeFileSync(join(root, "package.json"), JSON.stringify(pkg), "utf8");
26
+ for (const [name, content] of Object.entries(files)) {
27
+ writeFileSync(join(root, name), content, "utf8");
28
+ }
29
+ return root;
30
+ }
31
+
32
+ function context(root: string, overrides: Partial<ToolingContext> = {}): ToolingContext {
33
+ return {
34
+ root,
35
+ hasPackageJson: true,
36
+ isJavaScriptTypeScript: true,
37
+ isNextJs: false,
38
+ isReact: false,
39
+ usesBiome: false,
40
+ usesEslint: true,
41
+ usesPrettier: false,
42
+ packageManager: "npm",
43
+ frameworks: [],
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ afterEach(() => {
49
+ while (cleanups.length > 0) {
50
+ const root = cleanups.pop();
51
+ if (root) rmSync(root, { recursive: true, force: true });
52
+ }
53
+ });
54
+
55
+ describe("the check selection", () => {
56
+ it("runs Biome on the changed files when the preference asks for it", () => {
57
+ const root = workspace({ name: "web" });
58
+ const plan = selectQualityCheck({
59
+ context: context(root, { usesBiome: true, packageManager: "bun" }),
60
+ preference: "prefer_biome",
61
+ files: ["src/app.ts"],
62
+ });
63
+
64
+ expect(plan.tool).toBe("biome");
65
+ expect(plan.command).toBe("bunx");
66
+ expect(plan.args).toEqual(["biome", "check", "src/app.ts"]);
67
+ expect(plan.files).toEqual(["src/app.ts"]);
68
+ });
69
+
70
+ it("never asks Biome to fix the files", () => {
71
+ const root = workspace({ name: "web" });
72
+ const plan = selectQualityCheck({
73
+ context: context(root, { usesBiome: true }),
74
+ preference: "prefer_biome",
75
+ files: ["src/app.ts"],
76
+ });
77
+
78
+ expect(plan.args.join(" ")).not.toContain("--write");
79
+ expect(plan.args.join(" ")).not.toContain("--fix");
80
+ });
81
+
82
+ it("runs the project script when the preference keeps the project tools", () => {
83
+ const root = workspace({ name: "web", scripts: { lint: "eslint ." } });
84
+ const plan = selectQualityCheck({
85
+ context: context(root),
86
+ preference: "use_project_tools",
87
+ files: ["src/app.ts"],
88
+ });
89
+
90
+ expect(plan.tool).toBe("project");
91
+ expect(plan.command).toBe("npm");
92
+ expect(plan.args).toEqual(["run", "lint"]);
93
+ });
94
+
95
+ it("falls back to the project script when Biome is not installed yet", () => {
96
+ const root = workspace({ name: "web", scripts: { lint: "eslint ." } });
97
+ const plan = selectQualityCheck({
98
+ context: context(root, { usesBiome: false }),
99
+ preference: "prefer_biome",
100
+ files: ["src/app.ts"],
101
+ });
102
+
103
+ expect(plan.tool).toBe("project");
104
+ expect(plan.reason).toContain("Biome is not installed");
105
+ });
106
+
107
+ it("checks nothing without a decision of the user", () => {
108
+ const root = workspace({ name: "web", scripts: { lint: "eslint ." } });
109
+ const plan = selectQualityCheck({
110
+ context: context(root),
111
+ preference: undefined,
112
+ files: ["src/app.ts"],
113
+ });
114
+
115
+ expect(plan.tool).toBe("none");
116
+ expect(plan.command).toBeNull();
117
+ expect(plan.reason).toContain("not chosen");
118
+ });
119
+
120
+ it("checks nothing when no file changed", () => {
121
+ const root = workspace({ name: "web", scripts: { lint: "eslint ." } });
122
+ const plan = selectQualityCheck({
123
+ context: context(root),
124
+ preference: "use_project_tools",
125
+ files: [],
126
+ });
127
+
128
+ expect(plan.tool).toBe("none");
129
+ expect(plan.reason).toContain("no JavaScript/TypeScript file changed");
130
+ });
131
+
132
+ it("checks nothing when the project has no script to run", () => {
133
+ const root = workspace({ name: "web" });
134
+ const plan = selectQualityCheck({
135
+ context: context(root),
136
+ preference: "use_project_tools",
137
+ files: ["src/app.ts"],
138
+ });
139
+
140
+ expect(plan.tool).toBe("none");
141
+ expect(plan.reason).toContain("neither a lint nor a typecheck");
142
+ });
143
+
144
+ it("uses the typecheck script when there is no lint script", () => {
145
+ const root = workspace({ name: "web", scripts: { typecheck: "tsc --noEmit" } });
146
+ const plan = selectQualityCheck({
147
+ context: context(root),
148
+ preference: "use_project_tools",
149
+ files: ["src/app.ts"],
150
+ });
151
+
152
+ expect(plan.command).toBe("npm");
153
+ expect(plan.args).toEqual(["run", "typecheck"]);
154
+ });
155
+ });
156
+
157
+ describe("the check itself", () => {
158
+ it("reports a passing check", () => {
159
+ const root = workspace({ name: "web" });
160
+ const plan = selectQualityCheck({
161
+ context: context(root),
162
+ preference: "use_project_tools",
163
+ files: ["src/app.ts"],
164
+ });
165
+ const pass = {
166
+ tool: plan.tool,
167
+ command: "node",
168
+ args: ["-e", "process.exit(0)"],
169
+ files: ["src/app.ts"],
170
+ reason: "test",
171
+ };
172
+
173
+ const result = runQualityCheck(pass, { cwd: root, timeoutMs: 10_000 });
174
+
175
+ expect(result.ran).toBe(true);
176
+ expect(result.passed).toBe(true);
177
+ expect(result.reason).toContain("passed");
178
+ });
179
+
180
+ it("reports a failing check", () => {
181
+ const root = workspace({ name: "web" });
182
+ const fail = {
183
+ tool: "project" as const,
184
+ command: "node",
185
+ args: ["-e", "process.stderr.write('problem'); process.exit(2)"],
186
+ files: ["src/app.ts"],
187
+ reason: "test",
188
+ };
189
+
190
+ const result = runQualityCheck(fail, { cwd: root, timeoutMs: 10_000 });
191
+
192
+ expect(result.ran).toBe(true);
193
+ expect(result.passed).toBe(false);
194
+ expect(result.exitCode).toBe(2);
195
+ expect(result.output).toContain("problem");
196
+ });
197
+
198
+ it("reports a check that cannot start", () => {
199
+ const result = runQualityCheck(
200
+ {
201
+ tool: "project",
202
+ command: "definitely-not-a-command",
203
+ args: [],
204
+ files: ["src/app.ts"],
205
+ reason: "test",
206
+ },
207
+ { cwd: process.cwd(), timeoutMs: 10_000 },
208
+ );
209
+
210
+ expect(result.ran).toBe(false);
211
+ expect(result.reason).toContain("could not start");
212
+ });
213
+
214
+ it("runs nothing when the plan has no command", () => {
215
+ const result = runQualityCheck(
216
+ { tool: "none", command: null, args: [], files: [], reason: "nothing to do" },
217
+ { cwd: process.cwd(), timeoutMs: 10_000 },
218
+ );
219
+
220
+ expect(result.ran).toBe(false);
221
+ expect(result.passed).toBe(false);
222
+ });
223
+ });
224
+
225
+ describe("the status line", () => {
226
+ it("names the tool and the outcome", () => {
227
+ const line = formatQualityCheckStatus({
228
+ ran: true,
229
+ passed: true,
230
+ tool: "biome",
231
+ fileCount: 3,
232
+ exitCode: 0,
233
+ timedOut: false,
234
+ output: "",
235
+ reason: "the check passed",
236
+ });
237
+
238
+ expect(line).toBe("[TOOL] biome: 3 file(s), ok");
239
+ expect(/[^\x20-\x7e]/.test(line)).toBe(false);
240
+ });
241
+
242
+ it("names the reason when no check ran", () => {
243
+ const line = formatQualityCheckStatus({
244
+ ran: false,
245
+ passed: false,
246
+ tool: "none",
247
+ fileCount: 0,
248
+ exitCode: null,
249
+ timedOut: false,
250
+ output: "",
251
+ reason: "no JavaScript/TypeScript file changed",
252
+ });
253
+
254
+ expect(line).toContain("no check");
255
+ expect(line).toContain("no JavaScript/TypeScript file changed");
256
+ });
257
+
258
+ it("names a timeout", () => {
259
+ const line = formatQualityCheckStatus({
260
+ ran: true,
261
+ passed: false,
262
+ tool: "project",
263
+ fileCount: 2,
264
+ exitCode: null,
265
+ timedOut: true,
266
+ output: "",
267
+ reason: "the check hit the timeout",
268
+ });
269
+
270
+ expect(line).toContain("timeout");
271
+ });
272
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/trajectory",
3
- "version": "1.1.406",
3
+ "version": "1.1.407",
4
4
  "description": "Trajectory capture for the write-review loop — persists cycle records for learning",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/tui",
3
- "version": "1.1.406",
3
+ "version": "1.1.407",
4
4
  "description": "pi-harness-runtime — tui",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/types",
3
- "version": "1.1.406",
3
+ "version": "1.1.407",
4
4
  "description": "Shared TypeScript types for pi-harness-runtime packages",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",