@fitchmultz/pi-subagents 0.52.15 → 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.
- package/CHANGELOG.md +26 -0
- package/dist/agent/host.js +1 -1
- package/dist/agent/notifications.js +10 -8
- package/dist/agent/observe-layout.js +3 -3
- package/dist/agent/parking.js +0 -1
- package/dist/agent/process.js +1 -4
- package/dist/agent/runner.js +0 -2
- package/dist/agent/shutdown.js +0 -1
- package/dist/agent/typebox.js +2 -2
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/bridge-presence.js +0 -1
- package/dist/pi-intercom/bridge-protocol.js +0 -2
- package/dist/pi-intercom/bridge-session.js +0 -1
- package/dist/pi-intercom/bridge-wake.js +0 -1
- package/dist/pi-intercom/broker/client.js +0 -2
- package/dist/pi-intercom/broker/spawn.js +0 -3
- package/dist/pi-intercom/tool-registration.js +1 -2
- package/dist/pi-intercom/validation.js +0 -2
- package/dist/shared/cooperative.js +0 -1
- package/docs/code-quality.md +96 -68
- package/package.json +9 -6
- package/scripts/build.mjs +11 -13
- package/scripts/check-compat.mjs +3 -2
- package/scripts/compat-host.mjs +5 -5
- package/scripts/intercom-bridge-credentials.mjs +4 -3
- package/scripts/intercom-remote.mjs +0 -1
- package/scripts/native-package-smoke-assertions.mjs +25 -23
- package/scripts/native-package-smoke.mjs +38 -54
- package/scripts/package-smoke.mjs +39 -30
- package/scripts/prepare.mjs +16 -10
- package/scripts/real-pi-events.mjs +92 -0
- package/scripts/real-pi-live.mjs +28 -108
- package/scripts/real-pi-processes.mjs +77 -82
- package/scripts/real-pi-smoke.mjs +20 -18
- package/src/agent/host.ts +1 -1
- package/src/agent/notifications.ts +11 -8
- package/src/agent/observe-layout.ts +3 -3
- package/src/agent/parking.ts +0 -1
- package/src/agent/process.ts +1 -4
- package/src/agent/runner.ts +0 -2
- package/src/agent/shutdown.ts +0 -1
- package/src/agent/typebox.ts +2 -2
- package/src/pi-intercom/bridge-presence.ts +0 -1
- package/src/pi-intercom/bridge-protocol.ts +0 -2
- package/src/pi-intercom/bridge-session.ts +0 -1
- package/src/pi-intercom/bridge-wake.ts +0 -1
- package/src/pi-intercom/broker/client.ts +0 -2
- package/src/pi-intercom/broker/spawn.ts +0 -3
- package/src/pi-intercom/tool-registration.ts +1 -2
- package/src/pi-intercom/validation.ts +0 -2
- package/src/shared/cooperative.ts +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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
|
+
|
|
18
|
+
## 0.52.16 - 2026-11-03
|
|
19
|
+
|
|
20
|
+
<!-- npm-release:unreleased:start -->
|
|
21
|
+
<!-- npm-release:unreleased:end -->
|
|
22
|
+
|
|
23
|
+
- Skip no-op releases and restate malformed remediation dispositions (#336) ([579c60f](https://github.com/fitchmultz/pi-subagents/commit/579c60ff5beddd38a3090f91d6ad566e88a0e1c1))
|
|
24
|
+
|
|
25
|
+
### What's Changed
|
|
26
|
+
|
|
27
|
+
- Skip no-op releases and restate malformed remediation dispositions by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/336
|
|
28
|
+
|
|
29
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.15...v0.52.16
|
|
30
|
+
|
|
5
31
|
## 0.52.15 - 2026-11-02
|
|
6
32
|
|
|
7
33
|
<!-- npm-release:unreleased:start -->
|
package/dist/agent/host.js
CHANGED
|
@@ -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
|
|
12
|
-
|
|
13
|
-
|
|
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,
|
package/dist/agent/parking.js
CHANGED
|
@@ -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
|
}
|
package/dist/agent/process.js
CHANGED
|
@@ -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
|
}
|
package/dist/agent/runner.js
CHANGED
|
@@ -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
|
}
|
package/dist/agent/shutdown.js
CHANGED
|
@@ -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
|
}
|
package/dist/agent/typebox.js
CHANGED
|
@@ -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));
|
package/dist/build-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.
|
|
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
|
-
|
|
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
|
}
|
package/docs/code-quality.md
CHANGED
|
@@ -1,85 +1,113 @@
|
|
|
1
1
|
# Code quality
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
17
|
+
npm run typecheck:declarations
|
|
18
|
+
npm run check:quality
|
|
19
|
+
npm run build
|
|
14
20
|
```
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
substitutes the
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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`.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"oxlint
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 {
|
|
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
|
}
|
package/scripts/check-compat.mjs
CHANGED
|
@@ -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
|
-
|
|
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" });
|
package/scripts/compat-host.mjs
CHANGED
|
@@ -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 {
|
|
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
|
);
|