@gajae-code/coding-agent 0.16.7 → 0.17.1
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 +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/web/search/providers/openai-compatible.ts +191 -14
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor bootstrap isolation supervisor (parent side).
|
|
3
|
+
*
|
|
4
|
+
* `gjc doctor` must stay usable when the normal runtime is broken (malformed
|
|
5
|
+
* config, a faulting native addon, an unbounded filesystem stall). A
|
|
6
|
+
* `Promise.race` inside the in-process runner cannot bound synchronous or
|
|
7
|
+
* native work (fs realpath/read, native addon calls, fsync) — only an owned
|
|
8
|
+
* child-process boundary can. This module owns that boundary from the
|
|
9
|
+
* parent's side; the child worker entry (imports `./doctor/runner` and is
|
|
10
|
+
* re-entered via `cli.ts`'s `--internal-doctor-worker` admission branch) is
|
|
11
|
+
* implemented in `doctor-worker.ts`.
|
|
12
|
+
*
|
|
13
|
+
* Protocol: newline-delimited JSON over the child's stdin/stdout, each
|
|
14
|
+
* message authenticated by a random per-run token delivered only via
|
|
15
|
+
* `DOCTOR_WORKER_TOKEN_ENV` (never argv, never logged, never in the report).
|
|
16
|
+
* Token comparison is constant-time. The child's stdio is fully owned by
|
|
17
|
+
* this protocol, so the interactive confirmation prompt (default No, exact
|
|
18
|
+
* action/target/value, SIGINT- and deadline-aware) is rendered by THIS
|
|
19
|
+
* parent against the real terminal — the child never touches the TTY.
|
|
20
|
+
*
|
|
21
|
+
* "Repair admitted" is a permanent fence: it is latched synchronously the
|
|
22
|
+
* instant the `admitted` message is observed, before any further await, and
|
|
23
|
+
* is never cleared. Any abnormal ending after admission is reported as an
|
|
24
|
+
* uncertain post-effect outcome (exit 4); before admission it is "nothing
|
|
25
|
+
* started" (exit 3), and a runtime that itself was admitted-then-lost is
|
|
26
|
+
* evidenced with `worker_termination_unverified`, never a silently clean
|
|
27
|
+
* exit. SIGINT is forwarded to the child; a verified clean stop after SIGINT
|
|
28
|
+
* and before admission reports 130, never fabricated merely because a
|
|
29
|
+
* `Promise.race` resolved. No successor worker is ever spawned after an
|
|
30
|
+
* unverified termination.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { ChildProcessByStdio } from "node:child_process";
|
|
34
|
+
import * as childProcess from "node:child_process";
|
|
35
|
+
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
36
|
+
import * as path from "node:path";
|
|
37
|
+
import * as readline from "node:readline";
|
|
38
|
+
import { createInterface as createPromptInterface } from "node:readline/promises";
|
|
39
|
+
import type { Readable, Writable } from "node:stream";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
import { VERSION } from "@gajae-code/utils/dirs";
|
|
42
|
+
import type { DoctorAction, DoctorConfirmationResult, DoctorOptions } from "./doctor/args";
|
|
43
|
+
import { DOCTOR_EXIT_CODES, finalizeReport } from "./doctor/report";
|
|
44
|
+
import type { DoctorMode, DoctorRepair, DoctorReport } from "./doctor/types";
|
|
45
|
+
|
|
46
|
+
/** Re-entrant argv marker for the isolated doctor worker. Wiring into `cli.ts`'s admission chain is a coordinated follow-up. */
|
|
47
|
+
export const DOCTOR_WORKER_ARG = "--internal-doctor-worker";
|
|
48
|
+
/** Per-spawn random token; the worker must refuse to run the protocol route without an exact match. */
|
|
49
|
+
export const DOCTOR_WORKER_TOKEN_ENV = "GJC_DOCTOR_WORKER_TOKEN";
|
|
50
|
+
|
|
51
|
+
const DIAGNOSE_BUDGET_MS = 15_000;
|
|
52
|
+
const SHORT_REPAIR_BUDGET_MS = 10_000;
|
|
53
|
+
const LONG_REPAIR_BUDGET_MS = 120_000;
|
|
54
|
+
const SERVICE_RESTART_BASE_BUDGET_MS = 15_000 + 15_000;
|
|
55
|
+
const TERMINATION_GRACE_MS = 2_000;
|
|
56
|
+
|
|
57
|
+
const WORKER_ENV_KEYS = [
|
|
58
|
+
"PATH",
|
|
59
|
+
"HOME",
|
|
60
|
+
"USERPROFILE",
|
|
61
|
+
"SYSTEMROOT",
|
|
62
|
+
"WINDIR",
|
|
63
|
+
"TMPDIR",
|
|
64
|
+
"TEMP",
|
|
65
|
+
"TMP",
|
|
66
|
+
"LANG",
|
|
67
|
+
"LC_ALL",
|
|
68
|
+
"TZ",
|
|
69
|
+
"GJC_CONFIG_DIR",
|
|
70
|
+
"PI_CONFIG_DIR",
|
|
71
|
+
"GJC_CODING_AGENT_DIR",
|
|
72
|
+
"PI_CODING_AGENT_DIR",
|
|
73
|
+
"XDG_CONFIG_HOME",
|
|
74
|
+
"XDG_DATA_HOME",
|
|
75
|
+
"XDG_STATE_HOME",
|
|
76
|
+
"XDG_CACHE_HOME",
|
|
77
|
+
] as const;
|
|
78
|
+
|
|
79
|
+
const COMPILED = /(?:\/\$bunfs\/|\/~BUN\/|\/%7EBUN\/)/i.test(import.meta.url);
|
|
80
|
+
|
|
81
|
+
export interface SupervisedDoctorInput {
|
|
82
|
+
/** Raw argv following `doctor` (unparsed). The worker re-derives options and re-runs full security admission itself. */
|
|
83
|
+
readonly argv: readonly string[];
|
|
84
|
+
/** Parsed options for local budget/cwd/tty decisions only; `confirmRepair`/`onRepairAdmitted` here are ignored — the parent supplies live protocol-backed equivalents. */
|
|
85
|
+
readonly options: DoctorOptions;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type WorkerToSupervisorMessage =
|
|
89
|
+
| {
|
|
90
|
+
readonly type: "confirm";
|
|
91
|
+
readonly token: string;
|
|
92
|
+
readonly id: number;
|
|
93
|
+
readonly repair: DoctorRepair;
|
|
94
|
+
readonly setValue?: boolean;
|
|
95
|
+
}
|
|
96
|
+
| { readonly type: "admitted"; readonly token: string }
|
|
97
|
+
| { readonly type: "report"; readonly token: string; readonly report: DoctorReport }
|
|
98
|
+
| { readonly type: "error"; readonly token: string; readonly code: string; readonly message: string };
|
|
99
|
+
/** Distributes `Omit` over the message union so each variant keeps its own extra fields (plain `Omit<Union, K>` only keeps keys common to every member). */
|
|
100
|
+
export type WorkerToSupervisorPayload = WorkerToSupervisorMessage extends infer M
|
|
101
|
+
? M extends { readonly token: string }
|
|
102
|
+
? Omit<M, "token">
|
|
103
|
+
: never
|
|
104
|
+
: never;
|
|
105
|
+
|
|
106
|
+
export type SupervisorToWorkerMessage =
|
|
107
|
+
| {
|
|
108
|
+
readonly type: "init";
|
|
109
|
+
readonly token: string;
|
|
110
|
+
readonly argv: readonly string[];
|
|
111
|
+
readonly cwd: string;
|
|
112
|
+
readonly tty: boolean;
|
|
113
|
+
readonly runId: string;
|
|
114
|
+
readonly timeoutMs: number;
|
|
115
|
+
readonly deadlineAt: number;
|
|
116
|
+
}
|
|
117
|
+
| {
|
|
118
|
+
readonly type: "confirm-result";
|
|
119
|
+
readonly token: string;
|
|
120
|
+
readonly id: number;
|
|
121
|
+
readonly result: DoctorConfirmationResult;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function actionCeilingMs(action: DoctorAction, drainSeconds: number | undefined): number {
|
|
125
|
+
switch (action) {
|
|
126
|
+
case "config.set-validated":
|
|
127
|
+
case "mcp.set-startup-policy":
|
|
128
|
+
case "permissions.restrict-owned-config":
|
|
129
|
+
case "install.repair-managed-link":
|
|
130
|
+
case "service.detach-owned-stale-artifact":
|
|
131
|
+
return SHORT_REPAIR_BUDGET_MS;
|
|
132
|
+
case "install.restore-binary":
|
|
133
|
+
case "plugin.restore-known-artifact":
|
|
134
|
+
case "plugin.quarantine-selected":
|
|
135
|
+
return LONG_REPAIR_BUDGET_MS;
|
|
136
|
+
case "service.restart-owned":
|
|
137
|
+
return SERVICE_RESTART_BASE_BUDGET_MS + (drainSeconds ? drainSeconds * 1_000 : 0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** An explicit `--timeout-ms` may only shorten the action-specific ceiling, never raise it. */
|
|
142
|
+
export function resolveSupervisedBudgetMs(options: DoctorOptions): number {
|
|
143
|
+
const ceiling =
|
|
144
|
+
options.mode === "fix" && options.repair
|
|
145
|
+
? actionCeilingMs(options.repair, options.drainSeconds)
|
|
146
|
+
: DIAGNOSE_BUDGET_MS;
|
|
147
|
+
return options.timeoutMs !== undefined ? Math.min(options.timeoutMs, ceiling) : ceiling;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveWorkerSpawnArgv(): string[] | undefined {
|
|
151
|
+
if (COMPILED) return [process.execPath, DOCTOR_WORKER_ARG];
|
|
152
|
+
try {
|
|
153
|
+
const cliPath = fileURLToPath(new URL("../cli.ts", import.meta.url));
|
|
154
|
+
const configPath = fileURLToPath(new URL("../sdk/broker/internal-source.bunfig.toml", import.meta.url));
|
|
155
|
+
if (!path.isAbsolute(cliPath) || !path.isAbsolute(configPath)) return undefined;
|
|
156
|
+
return [process.execPath, "--no-env-file", `--config=${configPath}`, cliPath, DOCTOR_WORKER_ARG];
|
|
157
|
+
} catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function workerEnvironment(token: string): NodeJS.ProcessEnv {
|
|
163
|
+
const environment: NodeJS.ProcessEnv = {
|
|
164
|
+
NO_COLOR: "1",
|
|
165
|
+
BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0",
|
|
166
|
+
[DOCTOR_WORKER_TOKEN_ENV]: token,
|
|
167
|
+
};
|
|
168
|
+
for (const key of WORKER_ENV_KEYS) {
|
|
169
|
+
const value = process.env[key];
|
|
170
|
+
if (value !== undefined) environment[key] = value;
|
|
171
|
+
}
|
|
172
|
+
return environment;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Length is compared before the constant-time byte compare so a length mismatch is not itself a timing side channel on the byte contents. */
|
|
176
|
+
function tokenMatches(received: string, expected: string): boolean {
|
|
177
|
+
const receivedBuffer = Buffer.from(received, "utf8");
|
|
178
|
+
const expectedBuffer = Buffer.from(expected, "utf8");
|
|
179
|
+
return receivedBuffer.length === expectedBuffer.length && timingSafeEqual(receivedBuffer, expectedBuffer);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Structural gate for everything the supervisor dereferences afterwards. The
|
|
184
|
+
* `selection` envelope is validated here rather than at the use site because
|
|
185
|
+
* the readline handler reads `selection.repair`/`selection.targetId` directly:
|
|
186
|
+
* a worker that emits a report without a well-formed selection must be
|
|
187
|
+
* classified `worker_report_invalid`, never throw inside the `line` listener
|
|
188
|
+
* (which would escape the protocol and take the parent down).
|
|
189
|
+
*/
|
|
190
|
+
function isPlausibleDoctorReport(value: unknown): value is DoctorReport {
|
|
191
|
+
if (!value || typeof value !== "object") return false;
|
|
192
|
+
const report = value as Partial<DoctorReport>;
|
|
193
|
+
const summary = report.summary as { exitCode?: unknown } | undefined;
|
|
194
|
+
const selection = report.selection as { checks?: unknown; repair?: unknown; targetId?: unknown } | undefined;
|
|
195
|
+
return (
|
|
196
|
+
report.schemaVersion === 1 &&
|
|
197
|
+
report.command === "doctor" &&
|
|
198
|
+
typeof report.runId === "string" &&
|
|
199
|
+
typeof report.mode === "string" &&
|
|
200
|
+
typeof summary === "object" &&
|
|
201
|
+
summary !== null &&
|
|
202
|
+
typeof summary.exitCode === "number" &&
|
|
203
|
+
typeof selection === "object" &&
|
|
204
|
+
selection !== null &&
|
|
205
|
+
!Array.isArray(selection) &&
|
|
206
|
+
Array.isArray(selection.checks) &&
|
|
207
|
+
(selection.repair === undefined || typeof selection.repair === "string") &&
|
|
208
|
+
(selection.targetId === undefined || typeof selection.targetId === "string") &&
|
|
209
|
+
Array.isArray(report.checks) &&
|
|
210
|
+
Array.isArray(report.repairs)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Accept a worker `report` message only when it is both structurally complete
|
|
216
|
+
* and bound to this exact run. Returns the report on success so callers never
|
|
217
|
+
* re-narrow it, and `undefined` for every rejected shape — including a missing
|
|
218
|
+
* or malformed `selection`, which used to throw at the dereference.
|
|
219
|
+
*/
|
|
220
|
+
export function acceptWorkerReport(
|
|
221
|
+
value: unknown,
|
|
222
|
+
expected: {
|
|
223
|
+
readonly runId: string;
|
|
224
|
+
readonly mode: DoctorMode;
|
|
225
|
+
readonly repair?: string;
|
|
226
|
+
readonly targetId?: string;
|
|
227
|
+
},
|
|
228
|
+
): DoctorReport | undefined {
|
|
229
|
+
if (!isPlausibleDoctorReport(value)) return undefined;
|
|
230
|
+
if (value.runId !== expected.runId || value.mode !== expected.mode) return undefined;
|
|
231
|
+
if (value.selection.repair !== expected.repair || value.selection.targetId !== expected.targetId) return undefined;
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Write side of the parent→worker protocol.
|
|
237
|
+
*
|
|
238
|
+
* The channel is a one-way latch: once the worker has settled (exit, close, or
|
|
239
|
+
* a pipe error) no further byte is written to its stdin. A confirmation prompt
|
|
240
|
+
* can resolve arbitrarily late — after the deadline fired, after SIGINT, after
|
|
241
|
+
* the worker crashed — and its callback would otherwise write to a closed pipe,
|
|
242
|
+
* surfacing as an EPIPE / ERR_STREAM_DESTROYED that crashes the parent. The
|
|
243
|
+
* latch check and the write happen in one synchronous step, so no stream event
|
|
244
|
+
* can interleave between them, and any residual race is contained: it becomes a
|
|
245
|
+
* worker-failure outcome instead of an exception.
|
|
246
|
+
*/
|
|
247
|
+
export interface SupervisorChannel {
|
|
248
|
+
readonly settled: boolean;
|
|
249
|
+
/** Latch the channel closed without reporting a failure (normal termination). */
|
|
250
|
+
settle(): void;
|
|
251
|
+
send(message: SupervisorToWorkerMessage): void;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createSupervisorChannel(stdin: Writable, onChannelClosed: () => void): SupervisorChannel {
|
|
255
|
+
let settled = false;
|
|
256
|
+
const fail = (): void => {
|
|
257
|
+
settled = true;
|
|
258
|
+
onChannelClosed();
|
|
259
|
+
};
|
|
260
|
+
// An asynchronous pipe teardown arrives as a stream `error` event, not as a
|
|
261
|
+
// throw from `write`; without this listener the stream emits an unhandled
|
|
262
|
+
// 'error' and terminates the parent process.
|
|
263
|
+
stdin.on("error", fail);
|
|
264
|
+
return {
|
|
265
|
+
get settled() {
|
|
266
|
+
return settled;
|
|
267
|
+
},
|
|
268
|
+
settle() {
|
|
269
|
+
settled = true;
|
|
270
|
+
},
|
|
271
|
+
send(message) {
|
|
272
|
+
if (settled || stdin.destroyed || !stdin.writable) {
|
|
273
|
+
fail();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
stdin.write(`${JSON.stringify(message)}\n`);
|
|
278
|
+
} catch {
|
|
279
|
+
fail();
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function synthesizedReport(input: {
|
|
286
|
+
readonly options: DoctorOptions;
|
|
287
|
+
readonly runId: string;
|
|
288
|
+
readonly started: number;
|
|
289
|
+
readonly exitCode: number;
|
|
290
|
+
readonly invocationError: string;
|
|
291
|
+
readonly interrupted?: boolean;
|
|
292
|
+
}): DoctorReport {
|
|
293
|
+
const report = finalizeReport({
|
|
294
|
+
runId: input.runId,
|
|
295
|
+
mode: input.options.mode,
|
|
296
|
+
generatedAt: new Date().toISOString(),
|
|
297
|
+
durationMs: Math.round(performance.now() - input.started),
|
|
298
|
+
subject: {
|
|
299
|
+
gjcVersion: VERSION,
|
|
300
|
+
platform: process.platform,
|
|
301
|
+
arch: process.arch,
|
|
302
|
+
channel: COMPILED ? "standalone" : "source",
|
|
303
|
+
scope: input.options.scope,
|
|
304
|
+
rootIds: [],
|
|
305
|
+
},
|
|
306
|
+
selection: { checks: [], repair: input.options.repair, targetId: input.options.targetId },
|
|
307
|
+
coverage: { requested: 0, expanded: 0, attempted: 0, completed: 0, blocked: 0, timedOut: 0, unsupported: 0 },
|
|
308
|
+
checks: [],
|
|
309
|
+
repairs: [],
|
|
310
|
+
invocationError: input.invocationError,
|
|
311
|
+
interrupted: input.interrupted,
|
|
312
|
+
});
|
|
313
|
+
// The synthesized envelope always carries a concrete supervisor-decided exit code
|
|
314
|
+
// (admission-fenced, never a bare Promise.race artifact); finalizeReport's own
|
|
315
|
+
// exit derivation from empty coverage/checks is not authoritative here.
|
|
316
|
+
return { ...report, summary: { ...report.summary, exitCode: input.exitCode } };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Real terminal confirmation prompt, rendered by the parent against its own stdio (never the child's). */
|
|
320
|
+
async function promptForConfirmation(
|
|
321
|
+
repair: DoctorRepair,
|
|
322
|
+
setValue: boolean | undefined,
|
|
323
|
+
deadline: number,
|
|
324
|
+
sigintSignal: AbortSignal,
|
|
325
|
+
): Promise<DoctorConfirmationResult> {
|
|
326
|
+
const terminal = createPromptInterface({ input: process.stdin, output: process.stderr });
|
|
327
|
+
const remaining = Math.max(1, Math.floor(deadline - performance.now()));
|
|
328
|
+
const timeoutSignal = AbortSignal.timeout(remaining);
|
|
329
|
+
try {
|
|
330
|
+
const answer = await terminal.question(
|
|
331
|
+
`Apply ${repair.id} to ${repair.targetId}${setValue === undefined ? "" : `, value ${setValue}`}? [y/N] `,
|
|
332
|
+
{ signal: AbortSignal.any([sigintSignal, timeoutSignal]) },
|
|
333
|
+
);
|
|
334
|
+
return /^(y|yes)$/i.test(answer.trim()) ? "confirmed" : "confirmation_declined";
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
337
|
+
return sigintSignal.aborted ? "cancelled" : "confirmation_timeout";
|
|
338
|
+
return "confirmation_declined";
|
|
339
|
+
} finally {
|
|
340
|
+
terminal.close();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Run doctor diagnostics/repair inside an owned, verified-terminated child.
|
|
346
|
+
* Never falls back to in-process execution and never spawns a successor
|
|
347
|
+
* worker after this attempt's child is gone.
|
|
348
|
+
*/
|
|
349
|
+
export async function runSupervisedDoctor(input: SupervisedDoctorInput): Promise<DoctorReport> {
|
|
350
|
+
const started = performance.now();
|
|
351
|
+
const runId = randomUUID();
|
|
352
|
+
const options = input.options;
|
|
353
|
+
const budgetMs = resolveSupervisedBudgetMs(options);
|
|
354
|
+
const deadline = started + budgetMs;
|
|
355
|
+
const spawnArgv = resolveWorkerSpawnArgv();
|
|
356
|
+
if (!spawnArgv)
|
|
357
|
+
return synthesizedReport({
|
|
358
|
+
options,
|
|
359
|
+
runId,
|
|
360
|
+
started,
|
|
361
|
+
exitCode: DOCTOR_EXIT_CODES.incomplete,
|
|
362
|
+
invocationError: "worker_runtime_identity_unavailable",
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const token = randomBytes(32).toString("hex");
|
|
366
|
+
const [command, ...args] = spawnArgv;
|
|
367
|
+
let child: ChildProcessByStdio<Writable, Readable, null>;
|
|
368
|
+
try {
|
|
369
|
+
child = childProcess.spawn(command!, args, {
|
|
370
|
+
cwd: options.cwd ?? process.cwd(),
|
|
371
|
+
env: workerEnvironment(token),
|
|
372
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
373
|
+
windowsHide: true,
|
|
374
|
+
});
|
|
375
|
+
} catch {
|
|
376
|
+
return synthesizedReport({
|
|
377
|
+
options,
|
|
378
|
+
runId,
|
|
379
|
+
started,
|
|
380
|
+
exitCode: DOCTOR_EXIT_CODES.incomplete,
|
|
381
|
+
invocationError: "worker_spawn_failed",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Latched exactly once, synchronously, on first observation of "admitted" —
|
|
386
|
+
// never cleared afterward regardless of any later cancel/timeout/signal.
|
|
387
|
+
let admitted = false;
|
|
388
|
+
let childExited = false;
|
|
389
|
+
let settled = false;
|
|
390
|
+
let finalReport: DoctorReport | undefined;
|
|
391
|
+
let abnormalReasonCode: string | undefined;
|
|
392
|
+
const outcome = Promise.withResolvers<void>();
|
|
393
|
+
|
|
394
|
+
const finishOnce = (report: DoctorReport | undefined, reasonCode: string | undefined): void => {
|
|
395
|
+
if (settled) return;
|
|
396
|
+
settled = true;
|
|
397
|
+
finalReport = report;
|
|
398
|
+
abnormalReasonCode = reasonCode;
|
|
399
|
+
outcome.resolve();
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const channel = createSupervisorChannel(child.stdin, () => finishOnce(undefined, "worker_channel_closed"));
|
|
403
|
+
const send = channel.send;
|
|
404
|
+
|
|
405
|
+
const exitEvent = Promise.withResolvers<void>();
|
|
406
|
+
child.once("exit", () => {
|
|
407
|
+
childExited = true;
|
|
408
|
+
channel.settle();
|
|
409
|
+
exitEvent.resolve();
|
|
410
|
+
});
|
|
411
|
+
const waitForExit = async (milliseconds: number): Promise<boolean> => {
|
|
412
|
+
if (childExited) return true;
|
|
413
|
+
const timeout = Promise.withResolvers<boolean>();
|
|
414
|
+
const timer = setTimeout(() => timeout.resolve(false), milliseconds);
|
|
415
|
+
try {
|
|
416
|
+
return await Promise.race([exitEvent.promise.then(() => true), timeout.promise]);
|
|
417
|
+
} finally {
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const sigintController = new AbortController();
|
|
423
|
+
const onSigint = (): void => {
|
|
424
|
+
sigintController.abort();
|
|
425
|
+
try {
|
|
426
|
+
child.kill("SIGINT");
|
|
427
|
+
} catch {
|
|
428
|
+
// Child already gone.
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
process.on("SIGINT", onSigint);
|
|
432
|
+
|
|
433
|
+
const lineReader = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
434
|
+
lineReader.on("line", line => {
|
|
435
|
+
let message: WorkerToSupervisorMessage;
|
|
436
|
+
try {
|
|
437
|
+
message = JSON.parse(line) as WorkerToSupervisorMessage;
|
|
438
|
+
} catch {
|
|
439
|
+
finishOnce(undefined, "worker_protocol_invalid");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (typeof message.token !== "string" || !tokenMatches(message.token, token)) {
|
|
443
|
+
finishOnce(undefined, "worker_protocol_unauthenticated");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (message.type === "admitted") {
|
|
447
|
+
admitted = true;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (message.type === "confirm") {
|
|
451
|
+
// A confirmation that arrives after admission is refused, never defaulted to yes:
|
|
452
|
+
// the repair has already crossed into effect-capable territory.
|
|
453
|
+
if (admitted) {
|
|
454
|
+
send({ type: "confirm-result", token, id: message.id, result: "cancelled" });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
void promptForConfirmation(message.repair, message.setValue, deadline, sigintController.signal).then(
|
|
458
|
+
result => {
|
|
459
|
+
send({ type: "confirm-result", token, id: message.id, result });
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (message.type === "report") {
|
|
465
|
+
const accepted = acceptWorkerReport(message.report, {
|
|
466
|
+
runId,
|
|
467
|
+
mode: options.mode,
|
|
468
|
+
repair: options.repair,
|
|
469
|
+
targetId: options.targetId,
|
|
470
|
+
});
|
|
471
|
+
if (!accepted) {
|
|
472
|
+
finishOnce(undefined, "worker_report_invalid");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
finishOnce(accepted, undefined);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (message.type === "error") {
|
|
479
|
+
finishOnce(undefined, "worker_reported_error");
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
send({
|
|
484
|
+
type: "init",
|
|
485
|
+
token,
|
|
486
|
+
argv: input.argv,
|
|
487
|
+
cwd: options.cwd ?? process.cwd(),
|
|
488
|
+
tty: Boolean(options.tty),
|
|
489
|
+
runId,
|
|
490
|
+
timeoutMs: budgetMs,
|
|
491
|
+
deadlineAt: Date.now() + Math.max(1, deadline - performance.now()),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// `close` fires only after stdout/stderr are fully drained, so a report/error
|
|
495
|
+
// line the worker flushed right before exiting is always processed by the
|
|
496
|
+
// line reader above BEFORE this fallback can fire (matches the bash-shell
|
|
497
|
+
// supervisor's own close-vs-exit ordering rationale).
|
|
498
|
+
child.once("close", () => {
|
|
499
|
+
channel.settle();
|
|
500
|
+
finishOnce(undefined, "worker_exited_without_report");
|
|
501
|
+
});
|
|
502
|
+
child.once("error", () => {
|
|
503
|
+
finishOnce(undefined, "worker_spawn_failed");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const timeoutHandle = setTimeout(
|
|
507
|
+
() => finishOnce(undefined, "worker_deadline_exceeded"),
|
|
508
|
+
Math.max(0, deadline - performance.now()),
|
|
509
|
+
);
|
|
510
|
+
try {
|
|
511
|
+
await outcome.promise;
|
|
512
|
+
} finally {
|
|
513
|
+
clearTimeout(timeoutHandle);
|
|
514
|
+
process.off("SIGINT", onSigint);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Verified-termination sequence: never leave the worker running, never spawn a
|
|
518
|
+
// successor, and never report success/interruption without confirming the exit.
|
|
519
|
+
if (finalReport) child.stdin.end();
|
|
520
|
+
let terminationVerified = childExited || (finalReport !== undefined && (await waitForExit(200)));
|
|
521
|
+
if (!terminationVerified) {
|
|
522
|
+
try {
|
|
523
|
+
child.kill("SIGTERM");
|
|
524
|
+
terminationVerified = await waitForExit(TERMINATION_GRACE_MS);
|
|
525
|
+
} catch {
|
|
526
|
+
terminationVerified = false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!terminationVerified) {
|
|
530
|
+
try {
|
|
531
|
+
child.kill("SIGKILL");
|
|
532
|
+
terminationVerified = await waitForExit(TERMINATION_GRACE_MS);
|
|
533
|
+
} catch {
|
|
534
|
+
terminationVerified = false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
child.stdin.destroy();
|
|
539
|
+
} catch {
|
|
540
|
+
// Already closed.
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (finalReport && terminationVerified && !abnormalReasonCode) return finalReport;
|
|
544
|
+
|
|
545
|
+
if (!terminationVerified) {
|
|
546
|
+
return synthesizedReport({
|
|
547
|
+
options,
|
|
548
|
+
runId,
|
|
549
|
+
started,
|
|
550
|
+
exitCode: admitted ? DOCTOR_EXIT_CODES.mutationFailure : DOCTOR_EXIT_CODES.incomplete,
|
|
551
|
+
invocationError: "worker_termination_unverified",
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (sigintController.signal.aborted && !admitted) {
|
|
555
|
+
return synthesizedReport({
|
|
556
|
+
options,
|
|
557
|
+
runId,
|
|
558
|
+
started,
|
|
559
|
+
exitCode: DOCTOR_EXIT_CODES.interrupted,
|
|
560
|
+
invocationError: "worker_interrupted",
|
|
561
|
+
interrupted: true,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return synthesizedReport({
|
|
565
|
+
options,
|
|
566
|
+
runId,
|
|
567
|
+
started,
|
|
568
|
+
exitCode: admitted ? DOCTOR_EXIT_CODES.mutationFailure : DOCTOR_EXIT_CODES.incomplete,
|
|
569
|
+
invocationError: abnormalReasonCode ?? "worker_terminated_without_report",
|
|
570
|
+
});
|
|
571
|
+
}
|