@fitchmultz/pi-subagents 0.52.16 → 0.52.18

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 (58) hide show
  1. package/CHANGELOG.md +30 -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/inbound-delivery.js +2 -6
  18. package/dist/pi-intercom/reply-tracker.js +47 -55
  19. package/dist/pi-intercom/session-events.js +2 -0
  20. package/dist/pi-intercom/tool-registration.js +1 -2
  21. package/dist/pi-intercom/validation.js +0 -2
  22. package/dist/shared/cooperative.js +0 -1
  23. package/docs/code-quality.md +96 -68
  24. package/docs/intercom.md +6 -2
  25. package/package.json +9 -6
  26. package/scripts/build.mjs +11 -13
  27. package/scripts/check-compat.mjs +3 -2
  28. package/scripts/compat-host.mjs +5 -5
  29. package/scripts/intercom-bridge-credentials.mjs +4 -3
  30. package/scripts/intercom-remote.mjs +0 -1
  31. package/scripts/native-package-smoke-assertions.mjs +25 -23
  32. package/scripts/native-package-smoke.mjs +38 -54
  33. package/scripts/package-smoke.mjs +39 -30
  34. package/scripts/prepare.mjs +16 -10
  35. package/scripts/real-pi-events.mjs +92 -0
  36. package/scripts/real-pi-live.mjs +28 -108
  37. package/scripts/real-pi-processes.mjs +77 -82
  38. package/scripts/real-pi-smoke.mjs +20 -18
  39. package/src/agent/host.ts +1 -1
  40. package/src/agent/notifications.ts +11 -8
  41. package/src/agent/observe-layout.ts +3 -3
  42. package/src/agent/parking.ts +0 -1
  43. package/src/agent/process.ts +1 -4
  44. package/src/agent/runner.ts +0 -2
  45. package/src/agent/shutdown.ts +0 -1
  46. package/src/agent/typebox.ts +2 -2
  47. package/src/pi-intercom/bridge-presence.ts +0 -1
  48. package/src/pi-intercom/bridge-protocol.ts +0 -2
  49. package/src/pi-intercom/bridge-session.ts +0 -1
  50. package/src/pi-intercom/bridge-wake.ts +0 -1
  51. package/src/pi-intercom/broker/client.ts +0 -2
  52. package/src/pi-intercom/broker/spawn.ts +0 -3
  53. package/src/pi-intercom/inbound-delivery.ts +2 -6
  54. package/src/pi-intercom/reply-tracker.ts +50 -58
  55. package/src/pi-intercom/session-events.ts +3 -1
  56. package/src/pi-intercom/tool-registration.ts +1 -2
  57. package/src/pi-intercom/validation.ts +0 -2
  58. package/src/shared/cooperative.ts +0 -1
@@ -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/docs/intercom.md CHANGED
@@ -330,8 +330,12 @@ not an end-to-end exactly-once guarantee for arbitrary tool side effects.
330
330
 
331
331
  Idle recovery preserves admission order within the non-passive backlog and wakes
332
332
  once on its last item; passive breadcrumbs are appended before that waking work.
333
- The first eligible ask remains the implicit reply target, independently of which
334
- message triggers the turn. A busy recipient's steer is handed to Pi's native
333
+ The first unresolved eligible ask remains the implicit reply target, independently
334
+ of which message triggers the turn. Reply attribution refers to the unresolved
335
+ ask by ID. Placing delayed input does not revive an already replied, expired, or
336
+ retired reply obligation. A failed turn can retire only an ask whose input Pi has
337
+ consumed; queued input remains answerable after abort recovery.
338
+ A busy recipient's steer is handed to Pi's native
335
339
  queue and consumed at its next tool boundary; no extension probe asks another
336
340
  tool to give up the turn.
337
341
 
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.18",
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
  );
@@ -161,8 +161,7 @@ function issueCertificate(
161
161
  }
162
162
 
163
163
  function readConfig(/** @type {string} */ path) {
164
- /** @type {{ ca?: string, clients?: unknown[], cert?: string, key?: string }} */
165
- // oxlint-disable-next-line typescript/no-unsafe-assignment
164
+ /** @type {unknown} */
166
165
  const config = JSON.parse(readFileSync(path, "utf8"));
167
166
  if (!isRecord(config) || !Array.isArray(config.clients) || typeof config.ca !== "string") {
168
167
  throw new Error("Invalid clients allowlist.");
@@ -178,7 +177,9 @@ function readConfig(/** @type {string} */ path) {
178
177
  }
179
178
  return { ...client, name: client.name, cwd: client.cwd, fingerprint256: client.fingerprint256 };
180
179
  });
181
- return { ...config, ca: config.ca, clients };
180
+ /** @type {Record<string, unknown> & { ca: string, clients: typeof clients }} */
181
+ const validated = { ...config, ca: config.ca, clients };
182
+ return validated;
182
183
  }
183
184
 
184
185
  let args;
@@ -32,7 +32,6 @@ const actions = {
32
32
  send: ["to", "message", "id", "passive", "expects-reply", "reply-to"],
33
33
  };
34
34
  function messageId(/** @type {string | undefined} */ id) {
35
- // oxlint-disable-next-line no-control-regex
36
35
  const controls = /[\u0000-\u001f\u007f-\u009f]/;
37
36
  if (id !== undefined && (id.trim() === "" || id.length > 512 || controls.test(id))) {
38
37
  throw new Error("Invalid message ID.");
@@ -1,6 +1,22 @@
1
1
  // Journal and observation assertions shared with the packed-native smoke script.
2
2
  import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
+ import { isRecord, isUnknownArray } from "../src/shared/unknown.ts";
5
+
6
+ function jsonRecords(/** @type {string} */ file) {
7
+ return readFileSync(file, "utf8")
8
+ .trimEnd()
9
+ .split("\n")
10
+ .map((line) => {
11
+ /** @type {unknown} */ const entry = JSON.parse(line);
12
+ assert.ok(isRecord(entry));
13
+ return entry;
14
+ });
15
+ }
16
+
17
+ function containsText(/** @type {unknown} */ content, /** @type {string} */ text) {
18
+ return isUnknownArray(content) && content.some((part) => isRecord(part) && part.text === text);
19
+ }
4
20
 
5
21
  /**
6
22
  * Assert the child journal records its native tool result and completion entry.
@@ -9,23 +25,15 @@ import { readFileSync } from "node:fs";
9
25
  * @param {string} completionText
10
26
  */
11
27
  export function assertChildJournal(sessionFile, terminalEntryId, completionText) {
12
- const childEntries = readFileSync(sessionFile, "utf8")
13
- .trimEnd()
14
- .split("\n")
15
- .map((line) =>
16
- /** @type {{ id?: string, type?: string, message?: { role?: string, toolName?: string, content?: { text?: string }[] } }} */ (
17
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- journal line shape, guarded below
18
- JSON.parse(line)
19
- ),
20
- );
21
- // oxlint-disable typescript/no-unnecessary-condition -- journal fields are optional under tsc
28
+ const childEntries = jsonRecords(sessionFile);
22
29
  assert.ok(
23
30
  childEntries.some(
24
31
  (entry) =>
25
32
  entry.type === "message" &&
26
- entry.message?.role === "toolResult" &&
27
- entry.message?.toolName === "packed_probe" &&
28
- entry.message?.content?.some((part) => part.text === "PACKED_TOOL_RESULT") === true,
33
+ isRecord(entry.message) &&
34
+ entry.message.role === "toolResult" &&
35
+ entry.message.toolName === "packed_probe" &&
36
+ containsText(entry.message.content, "PACKED_TOOL_RESULT"),
29
37
  ),
30
38
  "actual child journal contains its native tool result",
31
39
  );
@@ -34,12 +42,12 @@ export function assertChildJournal(sessionFile, terminalEntryId, completionText)
34
42
  (entry) =>
35
43
  entry.id === terminalEntryId &&
36
44
  entry.type === "message" &&
37
- entry.message?.role === "assistant" &&
38
- entry.message?.content?.some((part) => part.text === completionText) === true,
45
+ isRecord(entry.message) &&
46
+ entry.message.role === "assistant" &&
47
+ containsText(entry.message.content, completionText),
39
48
  ),
40
49
  "completion references an actually published child assistant entry",
41
50
  );
42
- // oxlint-enable typescript/no-unnecessary-condition
43
51
  }
44
52
 
45
53
  /**
@@ -47,13 +55,7 @@ export function assertChildJournal(sessionFile, terminalEntryId, completionText)
47
55
  * @param {string} observations
48
56
  */
49
57
  export function assertObservations(observations) {
50
- /** @type {Record<string, unknown>[]} */
51
- // oxlint-disable-next-line typescript/no-unsafe-assignment
52
- const observed = readFileSync(observations, "utf8")
53
- .trimEnd()
54
- .split("\n")
55
- // oxlint-disable-next-line typescript/no-unsafe-return
56
- .map((line) => JSON.parse(line));
58
+ const observed = jsonRecords(observations);
57
59
  const matches = (/** @type {(item: Record<string, unknown>) => boolean} */ predicate) =>
58
60
  observed.filter(predicate).length;
59
61
  assert.equal(