@fitchmultz/pi-subagents 0.52.16 → 0.52.17

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 (51) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/agent/host.js +1 -1
  3. package/dist/agent/notifications.js +10 -8
  4. package/dist/agent/observe-layout.js +3 -3
  5. package/dist/agent/parking.js +0 -1
  6. package/dist/agent/process.js +1 -4
  7. package/dist/agent/runner.js +0 -2
  8. package/dist/agent/shutdown.js +0 -1
  9. package/dist/agent/typebox.js +2 -2
  10. package/dist/build-info.js +1 -1
  11. package/dist/pi-intercom/bridge-presence.js +0 -1
  12. package/dist/pi-intercom/bridge-protocol.js +0 -2
  13. package/dist/pi-intercom/bridge-session.js +0 -1
  14. package/dist/pi-intercom/bridge-wake.js +0 -1
  15. package/dist/pi-intercom/broker/client.js +0 -2
  16. package/dist/pi-intercom/broker/spawn.js +0 -3
  17. package/dist/pi-intercom/tool-registration.js +1 -2
  18. package/dist/pi-intercom/validation.js +0 -2
  19. package/dist/shared/cooperative.js +0 -1
  20. package/docs/code-quality.md +96 -68
  21. package/package.json +9 -6
  22. package/scripts/build.mjs +11 -13
  23. package/scripts/check-compat.mjs +3 -2
  24. package/scripts/compat-host.mjs +5 -5
  25. package/scripts/intercom-bridge-credentials.mjs +4 -3
  26. package/scripts/intercom-remote.mjs +0 -1
  27. package/scripts/native-package-smoke-assertions.mjs +25 -23
  28. package/scripts/native-package-smoke.mjs +38 -54
  29. package/scripts/package-smoke.mjs +39 -30
  30. package/scripts/prepare.mjs +16 -10
  31. package/scripts/real-pi-events.mjs +92 -0
  32. package/scripts/real-pi-live.mjs +28 -108
  33. package/scripts/real-pi-processes.mjs +77 -82
  34. package/scripts/real-pi-smoke.mjs +20 -18
  35. package/src/agent/host.ts +1 -1
  36. package/src/agent/notifications.ts +11 -8
  37. package/src/agent/observe-layout.ts +3 -3
  38. package/src/agent/parking.ts +0 -1
  39. package/src/agent/process.ts +1 -4
  40. package/src/agent/runner.ts +0 -2
  41. package/src/agent/shutdown.ts +0 -1
  42. package/src/agent/typebox.ts +2 -2
  43. package/src/pi-intercom/bridge-presence.ts +0 -1
  44. package/src/pi-intercom/bridge-protocol.ts +0 -2
  45. package/src/pi-intercom/bridge-session.ts +0 -1
  46. package/src/pi-intercom/bridge-wake.ts +0 -1
  47. package/src/pi-intercom/broker/client.ts +0 -2
  48. package/src/pi-intercom/broker/spawn.ts +0 -3
  49. package/src/pi-intercom/tool-registration.ts +1 -2
  50. package/src/pi-intercom/validation.ts +0 -2
  51. package/src/shared/cooperative.ts +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## 0.52.17 - 2026-11-05
6
+
7
+ <!-- npm-release:unreleased:start -->
8
+ <!-- npm-release:unreleased:end -->
9
+
10
+ - Adopt strict Oxlint/Oxfmt v5.2 without finding baselines (#339) ([8de822c](https://github.com/fitchmultz/pi-subagents/commit/8de822c14a56fdad3e13961abb02a106dac71a4f))
11
+
12
+ ### What's Changed
13
+
14
+ - Adopt strict Oxlint/Oxfmt v5.2 without finding baselines by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/339
15
+
16
+ **Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.16...v0.52.17
17
+
5
18
  ## 0.52.16 - 2026-11-03
6
19
 
7
20
  <!-- npm-release:unreleased:start -->
@@ -75,7 +75,7 @@ export async function loadSDK(root) {
75
75
  return resolvedSDK;
76
76
  }
77
77
  // Selected host SDK, not a second package-local runtime hidden behind Jiti aliases.
78
- // oxlint-disable-next-line typescript/no-unsafe-assignment
78
+ // oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: selected host module must retain its runtime identity.
79
79
  const sdk = await import(__rewriteRelativeImportExtension(pathToFileURL(path.join(root, "dist/index.js")).href));
80
80
  return sdk;
81
81
  }
@@ -5,17 +5,19 @@ import { readOutputPage } from "../shared/output-page.js";
5
5
  import { journalStamp } from "../shared/native-journal.js";
6
6
  import { launchAt, list, runDirectory } from "./storage.js";
7
7
  import { readCanonicalResult, readResult } from "./usage.js";
8
+ function isStringSet(value) {
9
+ if (!(value instanceof Set)) {
10
+ return false;
11
+ }
12
+ const entries = value;
13
+ return [...entries].every((entry) => typeof entry === "string");
14
+ }
8
15
  function queuedAdmissions() {
9
16
  const store = globalThis;
10
17
  const existing = store.__pi_single_agent_admissions__;
11
- if (existing instanceof Set) {
12
- const values = existing;
13
- const strings = [...values].filter((value) => typeof value === "string");
14
- if (strings.length === values.size) {
15
- // Keep one live queue identity across native extension reloads.
16
- // oxlint-disable-next-line typescript/no-unsafe-return
17
- return existing;
18
- }
18
+ if (isStringSet(existing)) {
19
+ // Keep one live queue identity across native extension reloads.
20
+ return existing;
19
21
  }
20
22
  const queued = new Set();
21
23
  store.__pi_single_agent_admissions__ = queued;
@@ -19,11 +19,11 @@ if (manifest === undefined) {
19
19
  throw new Error("Cannot locate the native Pi TUI package.");
20
20
  }
21
21
  const root = pathToFileURL(path.join(path.dirname(manifest), "dist/index.js"));
22
- // oxlint-disable-next-line typescript/no-unsafe-assignment
22
+ // oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: resolve the selected host TUI, not a second package instance.
23
23
  const tuiPromise = import(__rewriteRelativeImportExtension(root.href));
24
- // oxlint-disable-next-line typescript/no-unsafe-assignment
24
+ // oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: layout must share the selected host TUI instance.
25
25
  const layoutPromise = import(__rewriteRelativeImportExtension(new URL("./layout.js", root).href));
26
- // oxlint-disable-next-line typescript/no-unsafe-assignment
26
+ // oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: dispatch must share the selected host TUI instance.
27
27
  const dispatchPromise = import(__rewriteRelativeImportExtension(new URL("./tui.js", root).href));
28
28
  const [tui, layout, dispatch, sdk] = await Promise.all([
29
29
  tuiPromise,
@@ -297,7 +297,6 @@ async function awaitWake(base, launch, signal, legacy) {
297
297
  throw new Error(failure);
298
298
  }
299
299
  // DirectoryWatch reconciles dropped hints without imposing a deadline on native startup.
300
- // oxlint-disable-next-line no-await-in-loop
301
300
  await wake.sleep(200);
302
301
  }
303
302
  }
@@ -38,9 +38,7 @@ export async function observeAlive(identities) {
38
38
  for (const claim of identities) {
39
39
  // One `ps` at a time: each spawn briefly holds the event loop, so concurrent spawns add up.
40
40
  const { pid, identity, clock } = claim;
41
- verdicts.push(probe(claim) ??
42
- // oxlint-disable-next-line no-await-in-loop
43
- !(await observeChildProcessReplaced(pid, identity, claim.published, clock)));
41
+ verdicts.push(probe(claim) ?? !(await observeChildProcessReplaced(pid, identity, claim.published, clock)));
44
42
  }
45
43
  return verdicts;
46
44
  }
@@ -198,7 +196,6 @@ async function awaitStopping(dir, existing, signal) {
198
196
  throw new Error("Previous actor is still stopping; no replacement was launched.");
199
197
  }
200
198
  // The old actor must actually exit before replacing its generation.
201
- // oxlint-disable-next-line no-await-in-loop
202
199
  await delay(25);
203
200
  }
204
201
  }
@@ -310,13 +310,11 @@ class Actor {
310
310
  this.retry = false;
311
311
  this.pending().forEach((command) => this.accept(command));
312
312
  }
313
- // oxlint-disable-next-line no-await-in-loop
314
313
  if (await this.park(runtime, parked)) {
315
314
  return;
316
315
  }
317
316
  const polling = this.retry || this.stopping || watch === "unwatched";
318
317
  const tick = Math.max(1, this.nextParentCheck - performance.now());
319
- // oxlint-disable-next-line no-await-in-loop
320
318
  await this.wake.sleep(polling ? 100 : tick);
321
319
  }
322
320
  }
@@ -37,7 +37,6 @@ async function waitForExit(targets, timeout) {
37
37
  const deadline = Date.now() + timeout;
38
38
  while (Date.now() < deadline && targets.some(alive)) {
39
39
  // Join actual process exit while native abort persists the interrupted turn.
40
- // oxlint-disable-next-line no-await-in-loop
41
40
  await delay(25);
42
41
  }
43
42
  }
@@ -13,11 +13,11 @@ import { isChild, packageRoot } from "./host.js";
13
13
  // Pi aliases these imports to its own instance (including bundled virtual modules).
14
14
  // Only the detached runner needs absolute imports from the selected host package.
15
15
  const require = isChild() ? createRequire(path.join(packageRoot(), "dist/index.js")) : undefined;
16
- // oxlint-disable-next-line typescript/no-unsafe-assignment
16
+ // oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: detached runners resolve the selected host schema instance.
17
17
  const typeboxPromise = require === undefined
18
18
  ? import("typebox")
19
19
  : import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox")).href));
20
- // oxlint-disable-next-line typescript/no-unsafe-assignment
20
+ // oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: validation must use the selected host schema instance.
21
21
  const valuePromise = require === undefined
22
22
  ? import("typebox/value")
23
23
  : import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox/value")).href));
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.52.16","sha256":"ec0886af93d91687cfae1ed692e1e2e31127d3fbf98abadf192034a044e44d51"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.52.17","sha256":"b18d11c5a8e05e11577a3347c2d40808b644af0b5bd5248f4a3314d68c97a640"});
@@ -88,7 +88,6 @@ export class PersistentPresence {
88
88
  }
89
89
  try {
90
90
  // Each identity has one connection attempt; preserve the native broker's spawn safeguards.
91
- // oxlint-disable-next-line no-await-in-loop
92
91
  await this.owner.connect(identity, registration.expires);
93
92
  }
94
93
  catch {
@@ -25,7 +25,6 @@ export function object(value, fields) {
25
25
  }
26
26
  export function label(value, field, max = 512) {
27
27
  // Labels are single-line identifiers and paths; reject C0/C1 controls and DEL.
28
- // oxlint-disable-next-line no-control-regex
29
28
  const controls = /[\u0000-\u001f\u007f-\u009f]/;
30
29
  if (typeof value !== "string" ||
31
30
  value.trim() === "" ||
@@ -37,7 +36,6 @@ export function label(value, field, max = 512) {
37
36
  }
38
37
  export function text(value) {
39
38
  // Message text permits tab/newline/CR, but rejects other C0/C1 controls and DEL.
40
- // oxlint-disable-next-line no-control-regex
41
39
  const controls = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/;
42
40
  if (typeof value !== "string" || value.trim() === "" || controls.test(value)) {
43
41
  throw new Fault(400, "invalid_input", "Invalid message.");
@@ -303,7 +303,6 @@ export class Session {
303
303
  log("inbox_wait", { ...this.fields(), requestId: context.requestId, after: operation.after });
304
304
  try {
305
305
  // A nonmatching commit must rearm the same cursor, not start parallel readers.
306
- // oxlint-disable-next-line no-await-in-loop
307
306
  await delay(remaining, undefined, { signal });
308
307
  }
309
308
  catch (error) {
@@ -167,7 +167,6 @@ export class WakeDeliveries {
167
167
  // ponytail: two cold Macs can each admit one wake. Literal cross-Mac single admission
168
168
  // requires a neutral quota authority governing every writer, not a URL hash or HTTP 2xx.
169
169
  try {
170
- // oxlint-disable-next-line no-await-in-loop
171
170
  await this.post(candidate);
172
171
  }
173
172
  finally {
@@ -148,12 +148,10 @@ export class IntercomClient extends EventEmitter {
148
148
  async restoreTopics(topics, subscriptions) {
149
149
  for (const topic of topics ?? []) {
150
150
  // Restore frames are individually acknowledged to preserve order and bound socket payloads.
151
- // oxlint-disable-next-line no-await-in-loop
152
151
  await this.updateTopics({ action: "restore", topic });
153
152
  }
154
153
  for (const subscription of subscriptions ?? []) {
155
154
  // Subscriptions are committed before connect resolves and subsequent messages can arrive.
156
- // oxlint-disable-next-line no-await-in-loop
157
155
  await this.updateTopics({ action: "restore", subscription });
158
156
  }
159
157
  }
@@ -138,7 +138,6 @@ async function previousBrokerWarning() {
138
138
  const previousSockets = getPreviousBrokerSocketPaths();
139
139
  for (const socketPath of previousSockets) {
140
140
  // Migration probes are sequential and never register or send to old peers.
141
- // oxlint-disable-next-line no-await-in-loop
142
141
  if (isOwnedBrokerSocket(socketPath) && (await checkOneSocket(socketPath))) {
143
142
  return "An older agent-directory Intercom broker is still running. Its peers are not part of shared same-user discovery until they reload with the updated extension. Existing sessions and accepted work are untouched; no legacy fallback is used.";
144
143
  }
@@ -240,12 +239,10 @@ async function waitForBroker(timeoutMs = 5000) {
240
239
  const start = Date.now();
241
240
  while (Date.now() - start < timeoutMs) {
242
241
  // Each probe must finish before the next to avoid parallel socket attempts.
243
- // oxlint-disable-next-line no-await-in-loop
244
242
  if (await checkSocketConnectable()) {
245
243
  return;
246
244
  }
247
245
  // Polling waits between probes rather than starting an unbounded batch.
248
- // oxlint-disable-next-line no-await-in-loop
249
246
  await sleep(100);
250
247
  }
251
248
  throw new Error("Broker failed to start within timeout");
@@ -9,8 +9,7 @@ export function registerIntercomTools(pi, tools) {
9
9
  description: "Message connected Pi sessions in your group; mains also reach other mains across projects. List shows target IDs. Ask waits for a correlated reply; reply answers the active or sole pending ask. Pending lists asks; status shows connection and inbound queue. Last pages a peer's last completed response without waking it. Broker acceptance is not recipient persistence, consumption or completion.",
10
10
  parameters: intercomSchema,
11
11
  renderResult: renderCompactResult,
12
- // oxlint-disable-next-line max-params
13
- async execute(_id, params, signal, _update, ctx) {
12
+ async execute(...[, params, signal, , ctx]) {
14
13
  const result = throwIfToolError(await tools.execute(params, signal, ctx));
15
14
  return { content: [...result.content], details: result.details };
16
15
  },
@@ -34,10 +34,8 @@ export function asError(error) {
34
34
  return error instanceof Error ? error : new Error(errorMessage(error));
35
35
  }
36
36
  // Protocol text permits line feeds and tabs, but rejects other C0/C1 controls.
37
- // oxlint-disable-next-line no-control-regex
38
37
  const UNSAFE_TEXT_CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/;
39
38
  // Protocol labels are single-line fields and reject every C0/C1 control.
40
- // oxlint-disable-next-line no-control-regex
41
39
  const UNSAFE_LABEL_CONTROLS = /[\u0000-\u001f\u007f-\u009f]/;
42
40
  export function safeText(value) {
43
41
  return typeof value === "string" && !UNSAFE_TEXT_CONTROLS.test(value);
@@ -14,7 +14,6 @@ export async function runCooperatively(steps) {
14
14
  // single-record stall off-thread rather than increasing this batch size.
15
15
  if (++count === 32) {
16
16
  // Yield between ordered atomic recovery records; concurrency would reorder delivery.
17
- // oxlint-disable-next-line no-await-in-loop
18
17
  await yieldToInput();
19
18
  count = 0;
20
19
  }
@@ -1,85 +1,113 @@
1
1
  # Code quality
2
2
 
3
- Oxlint checks maintained TypeScript and JavaScript. Oxfmt owns formatting.
4
- Both tools use the committed npm versions and repository configuration.
3
+ The [Strict Oxlint/Oxfmt Engineering Standard v5.2](strict-oxlint-oxfmt-standard.md)
4
+ owns the policy. [Quality exceptions](quality-exceptions.md) records the few
5
+ operations that require different treatment. No finding budget or suppression
6
+ baseline is accepted.
7
+
8
+ ## Commands
9
+
10
+ Use the committed npm lockfile. Local checks are:
5
11
 
6
12
  ```sh
7
13
  npm ci --ignore-scripts
8
14
  npm run lint
9
- npm run lint:fix
10
- npm run format
11
15
  npm run format:check
12
16
  npm run typecheck
13
- npm run ci
17
+ npm run typecheck:declarations
18
+ npm run check:quality
19
+ npm run build
14
20
  ```
15
21
 
16
- Lint rejects unsafe types, unchecked assertions, floating promises, accidental
17
- mutation, import cycles and ambiguous control flow. Application readonly types
18
- remain checked by TypeScript. `typescript/prefer-readonly-parameter-types` is
19
- disabled: the published backend cannot express the existing native SDK aliases
20
- with package-scoped allowances. The project does not patch the checker or add
21
- production wrappers to satisfy that rule.
22
+ `npm run lint:fix` and `npm run format` are writing conveniences.
23
+ `npm run lint:agent` is another output format for the same lint gate.
22
24
 
23
- Production limits are complexity 10, depth 3, parameters 4, statements 40,
24
- function lines 80 and file lines 500, excluding blank lines and comments.
25
- Tests retain complexity 15, depth 4 and parameters 6. Cohesive test suites have
26
- no size or statement limits.
25
+ Full `npm run ci` remains hosted-only. It runs each independent gate even when
26
+ another fails: native lint, format, implementation types, handwritten declaration
27
+ types, policy and coverage, package size, package/install smokes, unit tests, and
28
+ integration tests. Package smokes build the actual artifacts before testing them.
29
+ The aggregate fails if any gate fails.
30
+
31
+ CI qualifies official Pi on Linux and the maintained fork on Linux and macOS.
32
+ Those are distinct runtime graphs, not interchangeable version labels. Android
33
+ has no published oxlint-tsgolint backend; native semantic qualification runs on
34
+ a supported host. A skipped local native probe is not hosted acceptance.
35
+
36
+ ## Coverage and enforcement
37
+
38
+ `quality/check.ts` compares the effective policy with the normative JSON in the
39
+ standard and the explicit repository mappings. It checks native lint membership,
40
+ resolved compiler membership, formatter ownership, and required command scope.
41
+ The inventory includes ignored files outside generated, dependency, and private
42
+ runtime trees, so hiding maintained code with a Git ignore does not exempt it.
43
+
44
+ The compiler's own resolution checks production dependencies on test support,
45
+ including aliases, re-exports, and literal dynamic imports. Oxc's parser identifies
46
+ actual comments and operation-bound directives. Strings containing example
47
+ directives are data. Copied, moved, duplicate, stale, and wrong-operation
48
+ directives fail; unused directives also fail native lint.
49
+
50
+ The source profiles are:
27
51
 
28
- Formatting uses width 100, two spaces, LF and double quotes. npm owns lockfile
29
- formatting. Build outputs keep their own bytes.
30
- The VS Code Oxc extension uses these same lint and formatting configurations.
52
+ - `src/**`, `scripts/**`, `quality/**`, and `.github/scripts/**` are production
53
+ implementation. Mixed-use helpers retain that profile.
54
+ - `test/**` contains tests, fixtures, and exclusively test-used support.
55
+ - `*.test-d.{ts,mts,cts}` are compiler-checked negative consumer contracts with
56
+ explained expected failures and passing counterparts.
57
+ - Handwritten `*.d.{ts,mts,cts}` files receive the declaration profile and an
58
+ actual `skipLibCheck:false` check through `quality/declarations.ts`.
31
59
 
32
- `npm run ci` runs lint, formatting, TypeScript, package/install smokes and all
33
- tests. CI separately qualifies the latest official Pi on GitHub-hosted Linux and
34
- the maintained fork on Linux and macOS. There is no custom checker build, probe
35
- suite or self-hosted runner requirement.
60
+ The root `tsconfig.json` checks all maintained TS and JS with the full required
61
+ strict options. The application retains `skipLibCheck:true` because the installed
62
+ SDK dependency closure includes incompatible third-party declaration imports.
63
+ The declaration gate checks any handwritten declarations and their real dependency
64
+ closure without that option or permissive stubs. There are currently no handwritten
65
+ declaration files.
36
66
 
37
- CI's lint job also enforces the production byte ceiling before qualification
38
- substitutes the selected host graph. The retired workflow schema generator and
39
- its patching dependency are not part of the new runtime. `npm run check:size`
40
- still reports source and production line counts for visibility; no line count
41
- is mandated. The 1 MiB byte ceiling and all lint limits stay unchanged.
67
+ `tsconfig.build.json` and `tsconfig.client.json` are emission configurations, not
68
+ substitutes for the checked root project. Build-swap tests, packed-consumer type
69
+ tests, and actual package/native-install smokes verify their outputs.
70
+
71
+ Production limits are complexity 10, depth 3, parameters 4, statements 40,
72
+ function lines 80, and file lines 500. Tests retain complexity 15, depth 4, and
73
+ parameters 6 without statement or line limits. These are the standard's profiles,
74
+ not local exceptions. There is no React, DOM JSX, Jest, or Vitest implementation;
75
+ Node's test runner owns execution. A real failing assertion is qualified through
76
+ the repository test command.
77
+
78
+ Oxfmt owns supported maintained files, with width 100, two spaces, LF, double
79
+ quotes, and no import/package/Tailwind/JSDoc transformations. npm owns
80
+ `package-lock.json`; the build owns `dist/**` and temporary publication trees.
81
+ Captured `docs/evidence/*.log` files retain original command bytes. Git, Pi state,
82
+ dependency trees, npm cache, and OS metadata are not maintained source.
83
+ No committed generated output requires a separate freshness generator.
84
+
85
+ The existing viewer import restriction remains: lazy viewer values must come
86
+ from the selected host through `observe-layout.ts`. It strengthens the base
87
+ policy and protects runtime identity. The existing 1 MiB production-byte ceiling
88
+ also remains. Neither is a new requirement of this standard.
42
89
 
43
90
  ## Adoption record
44
91
 
45
- Repository: `fitchmultz/pi-subagents`. Adopted standard: strict Oxlint/Oxfmt
46
- engineering standard v5.1.2, kept as the repository copy at
47
- `docs/strict-oxlint-oxfmt-standard.md`. Exception records live in
48
- [Quality exceptions](quality-exceptions.md) and follow the standard's Appendix B.
49
-
50
- Supported runtime and package manager: Node `>=24.21.0` with npm and the
51
- committed lockfile. Installed tool versions (devDependencies): oxlint 1.90.0,
52
- oxlint-tsgolint 7.0.2005, oxfmt 0.75.0, `@types/node` 24.19.2, typebox 1.4.3.
53
-
54
- Configuration and acceptance paths: lint `.oxlintrc.json`, formatting
55
- `.oxfmtrc.json`, compiler `tsconfig.json`. Local acceptance commands are
56
- `npm run lint`, `npm run format:check` and `npm run typecheck`; the aggregate
57
- acceptance command is hosted `npm run ci` (lint, formatting, TypeScript, byte
58
- ceiling, package/install smokes and the complete test suites, plus separate
59
- official-Pi and fork qualification jobs).
60
-
61
- Compiler projects and source profiles: one `tsc --noEmit` project covers all
62
- maintained TypeScript and checked-JavaScript (`src`, `scripts`, `test`,
63
- `.github`) with `strict`, `checkJs`, `noUncheckedIndexedAccess`,
64
- `exactOptionalPropertyTypes` and the full strict flag set. Lint profiles:
65
- production limits (complexity 10, depth 3, parameters 4, statements 40,
66
- function lines 80, file lines 500), test/fixture limits (complexity 15, depth
67
- 4, parameters 6, no size limits) and declaration files with size limits off.
68
-
69
- Formatter and generator ownership: oxfmt formats all maintained source at
70
- width 100; npm owns lockfile formatting; build outputs keep their own bytes.
71
-
72
- Migration status: complete. The toolchain, baseline configuration, strict
73
- TypeScript compiler options, checked-JavaScript coverage and this records
74
- policy landed as PRs #319, #320, #321, #325 and the records PR; the policy
75
- integrity keeper `test/unit/strict-oxlint-policy.test.ts` pins the effective
76
- configuration, the four-entry overrides inventory and the directive rule-name
77
- inventory against drift.
78
-
79
- Integrated verification: hosted `npm run ci` on the merge of each part, with
80
- the single-agent-runtime, agent-receipts, pi-intercom-native-replay and
81
- retired-history-archive keepers exercising the native contracts. Locally
82
- verified state for the checked-JavaScript part is recorded in PR #325; the
83
- retired-history-archive suite requires passwordless `sudo` and
84
- `scripts/package-smoke.mjs` requires a short Unix socket directory, so hosted
85
- CI is the authoritative gate for both.
92
+ Repository: `fitchmultz/pi-subagents`. Standard: v5.2, 3 November 2026.
93
+ Runtime: Node `>=24.21.0`, npm, committed lockfile.
94
+ Qualified tools: Oxlint 1.91.0, oxlint-tsgolint 7.0.2006, Oxfmt 0.76.0,
95
+ TypeScript 7.0.2, oxc-parser 0.157.0, and `@types/node` 24.19.2.
96
+
97
+ The adoption starts at `7f449e2dcc8e7e82432bea79a2709f0b49256239`.
98
+ The adoption PR records the exact integrated head and hosted results; CI verifies
99
+ the source SHA before qualification. Local M1 evidence uses Node 24.22.1 and
100
+ official Pi 1.5.0; separate native fork coverage runs against its selected graph.
101
+
102
+ Qualification lives in `strict-oxlint-qualification.test.ts`,
103
+ `lint-node-test-allowance.test.ts`, `quality-coverage.test.ts`,
104
+ `strict-oxlint-policy.test.ts`, and `quality-acceptance.test.ts`.
105
+ These cover semantic and category rules, all six structural boundaries, Node
106
+ registration origins, operation attachment, strict JS/TS/declaration checking,
107
+ formatting idempotence, coverage omissions, and acceptance exit propagation.
108
+ The adoption also exercised each aggregate gate's failure independently; ordinary
109
+ regression coverage retains one representative failure plus the all-green case.
110
+
111
+ The policy and source repairs are integrated. Completion requires the adoption
112
+ PR's exact-head hosted checks and release verification; local passes alone do not
113
+ establish complete adoption.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitchmultz/pi-subagents",
3
- "version": "0.52.16",
3
+ "version": "0.52.17",
4
4
  "description": "Pi extension for subagent orchestration and local session coordination",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -68,8 +68,10 @@
68
68
  "lint:agent": "oxlint --type-aware --deny-warnings --format=agent .",
69
69
  "format": "oxfmt --write .",
70
70
  "format:check": "oxfmt --check .",
71
- "ci": "npm run lint && npm run format:check && npm run typecheck && npm run check:size && npm run smoke:package && npm run smoke:install && npm run test:all",
72
- "smoke:real-pi": "node scripts/real-pi-smoke.mjs"
71
+ "ci": "node quality/acceptance.ts",
72
+ "smoke:real-pi": "node scripts/real-pi-smoke.mjs",
73
+ "check:quality": "node quality/check.ts",
74
+ "typecheck:declarations": "node quality/declarations.ts"
73
75
  },
74
76
  "pi": {
75
77
  "extensions": [
@@ -110,9 +112,10 @@
110
112
  "@earendil-works/pi-tui": "1.5.0",
111
113
  "@types/node": "24.19.2",
112
114
  "@types/sinon": "22.0.2",
113
- "oxfmt": "0.75.0",
114
- "oxlint": "1.90.0",
115
- "oxlint-tsgolint": "7.0.2005",
115
+ "oxc-parser": "0.157.0",
116
+ "oxfmt": "0.76.0",
117
+ "oxlint": "1.91.0",
118
+ "oxlint-tsgolint": "7.0.2006",
116
119
  "sinon": "22.1.0",
117
120
  "type-fest": "5.10.0",
118
121
  "typebox": "1.4.3",
package/scripts/build.mjs CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { execFile as execFileCallback } from "node:child_process";
14
+ import assert from "node:assert/strict";
14
15
  import { createHash } from "node:crypto";
15
16
  import { existsSync } from "node:fs";
16
17
  import { readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
@@ -21,7 +22,6 @@ import { promisify } from "node:util";
21
22
  import { compilerInvocation } from "./typescript.mjs";
22
23
 
23
24
  /** @type {(file: string, args: readonly string[], options: import("node:child_process").ExecFileOptions) => Promise<{ stdout: string, stderr: string }>} */
24
- // oxlint-disable-next-line typescript/strict-void-return
25
25
  const execFile = promisify(execFileCallback);
26
26
  const RM_OPTIONS = { force: true, maxRetries: 5, recursive: true, retryDelay: 100 };
27
27
 
@@ -80,7 +80,6 @@ async function reapStrandedStaging(/** @type {string} */ cwd) {
80
80
  continue;
81
81
  }
82
82
  // Reap abandoned trees sequentially to bound filesystem removal pressure.
83
- // oxlint-disable-next-line no-await-in-loop
84
83
  await discardStaging(join(cwd, entry.name));
85
84
  }
86
85
  }
@@ -114,13 +113,13 @@ async function compileToStaging(
114
113
  await compileProject(cwd, stagingDir, compiler, "tsconfig.client.json");
115
114
  await stampBuild(cwd, stagingDir);
116
115
  } catch (error) {
117
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- exec rejections carry partial output
118
- const output = /** @type {{ stdout?: string, stderr?: string }} */ (error);
119
- if (output.stdout !== undefined && output.stdout !== "") {
120
- process.stdout.write(output.stdout);
121
- }
122
- if (output.stderr !== undefined && output.stderr !== "") {
123
- process.stderr.write(output.stderr);
116
+ if (typeof error === "object" && error !== null) {
117
+ if ("stdout" in error && typeof error.stdout === "string") {
118
+ process.stdout.write(error.stdout);
119
+ }
120
+ if ("stderr" in error && typeof error.stderr === "string") {
121
+ process.stderr.write(error.stderr);
122
+ }
124
123
  }
125
124
  await discardStaging(stagingDir);
126
125
  throw error;
@@ -135,13 +134,13 @@ async function stampBuild(/** @type {string} */ cwd, /** @type {string} */ stagi
135
134
  .sort();
136
135
  for (const file of files) {
137
136
  // Hash sorted path/content pairs in order without buffering the whole emitted tree.
138
- // oxlint-disable-next-line no-await-in-loop
139
137
  const content = await readFile(join(stagingDir, file));
140
138
  hash.update(file).update("\0").update(content).update("\0");
141
139
  }
142
- /** @type {{ version: string }} */
143
- // oxlint-disable-next-line typescript/no-unsafe-assignment
140
+ /** @type {unknown} */
144
141
  const pkg = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
142
+ assert.ok(typeof pkg === "object" && pkg !== null && "version" in pkg);
143
+ assert.ok(typeof pkg.version === "string");
145
144
  const { version } = pkg;
146
145
  await writeFile(
147
146
  join(stagingDir, stamp),
@@ -184,7 +183,6 @@ async function publishStaging(/** @type {string} */ stagingDir, /** @type {strin
184
183
  });
185
184
  for (let retry = 0; ; retry++) {
186
185
  // Each retry must observe publication before another rename can race it.
187
- // oxlint-disable-next-line no-await-in-loop
188
186
  if (await tryPublish(stagingDir, distDir, retry)) {
189
187
  break;
190
188
  }
@@ -19,6 +19,8 @@ const run = (
19
19
  /** @type {import("node:child_process").SpawnSyncOptions} */ options = {},
20
20
  ) => {
21
21
  // Outlasts the 1_200_000 ms integration watchdog in scripts/run-tests.mjs.
22
+ // Inherited stdio produces null at runtime despite SpawnSyncReturns<T>'s declaration.
23
+ /** @type {import("node:child_process").SpawnSyncReturns<string | Buffer | null>} */
22
24
  const result = spawnSync(command, args, { stdio: "inherit", timeout: 1_500_000, ...options });
23
25
  if (result.error) {
24
26
  throw result.error;
@@ -28,8 +30,7 @@ const run = (
28
30
  0,
29
31
  `${command} ${args.join(" ")} failed (${result.status ?? result.signal ?? "unknown"})`,
30
32
  );
31
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spawnSync stdout shape depends on caller options
32
- return /** @type {string | undefined} */ (/** @type {unknown} */ (result.stdout))?.trim();
33
+ return result.stdout === null ? undefined : result.stdout.toString().trim();
33
34
  };
34
35
  const git = (/** @type {string} */ cwd, /** @type {string[]} */ args) =>
35
36
  run("git", args, { cwd, stdio: ["ignore", "pipe", "inherit"], encoding: "utf8" });
@@ -12,9 +12,12 @@ if (hostPackageJson === undefined) {
12
12
  }
13
13
  export const hostRoot = realpathSync(dirname(hostPackageJson));
14
14
  export const hostIndex = join(hostRoot, "dist/index.js");
15
- /** @type {{ version: string, bin: { pi: string } }} */
16
- // oxlint-disable-next-line typescript/no-unsafe-assignment
15
+ /** @type {unknown} */
17
16
  const manifest = JSON.parse(readFileSync(join(hostRoot, "package.json"), "utf8"));
17
+ assert.ok(typeof manifest === "object" && manifest !== null);
18
+ assert.ok("version" in manifest && typeof manifest.version === "string");
19
+ assert.ok("bin" in manifest && typeof manifest.bin === "object" && manifest.bin !== null);
20
+ assert.ok("pi" in manifest.bin && typeof manifest.bin.pi === "string");
18
21
  export const hostCli = realpathSync(join(hostRoot, manifest.bin.pi));
19
22
  /** @type {[string, string][]} */
20
23
  const expectedHostPaths = [
@@ -26,7 +29,6 @@ for (const [name, actual] of expectedHostPaths) {
26
29
  const expected = process.env[name];
27
30
  if (expected !== undefined) {
28
31
  assert.equal(
29
- // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
30
32
  realpathSync(expected),
31
33
  realpathSync(actual),
32
34
  `${name} must select the installed graph`,
@@ -35,11 +37,9 @@ for (const [name, actual] of expectedHostPaths) {
35
37
  }
36
38
  const expectedVersion = process.env.PI_COMPAT_EXPECTED_VERSION;
37
39
  if (expectedVersion !== undefined) {
38
- // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
39
40
  assert.equal(manifest.version, expectedVersion);
40
41
  }
41
42
  assert.equal(
42
- // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
43
43
  realpathSync(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))),
44
44
  realpathSync(hostIndex),
45
45
  );