oh-my-codex 0.18.7 → 0.18.9
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/Cargo.lock +12 -12
- package/Cargo.toml +1 -1
- package/README.md +5 -5
- package/crates/omx-sparkshell/tests/execution.rs +1 -1
- package/dist/agents/__tests__/native-config.test.js +42 -1
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +8 -0
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +1 -0
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.d.ts +5 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +17 -2
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/fsm.test.js +3 -0
- package/dist/autopilot/__tests__/fsm.test.js.map +1 -1
- package/dist/autopilot/fsm.js +2 -2
- package/dist/autopilot/fsm.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +4 -2
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +512 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +39 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +98 -6
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +28 -8
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +26 -9
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +13 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +14 -0
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +50 -1
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +89 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +65 -0
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +21 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +2 -2
- package/dist/cli/__tests__/update.test.js +323 -18
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +8 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +143 -28
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +14 -2
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +62 -15
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +3 -1
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +2 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +4 -0
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +166 -27
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +8 -1
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/tmux-hook.d.ts.map +1 -1
- package/dist/cli/tmux-hook.js +16 -0
- package/dist/cli/tmux-hook.js.map +1 -1
- package/dist/cli/update.d.ts +22 -3
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +312 -26
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/version.d.ts.map +1 -1
- package/dist/cli/version.js +5 -9
- package/dist/cli/version.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +12 -1
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +1 -0
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +2 -2
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +2 -2
- package/dist/config/generator.js.map +1 -1
- package/dist/config/team-mode.d.ts +12 -0
- package/dist/config/team-mode.d.ts.map +1 -0
- package/dist/config/team-mode.js +91 -0
- package/dist/config/team-mode.js.map +1 -0
- package/dist/hooks/__tests__/agents-overlay.test.js +88 -0
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +12 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +30 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +423 -3
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +189 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +35 -2
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +3 -3
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-guidance-contract.test.js +21 -0
- package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +36 -50
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +31 -0
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +17 -21
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +258 -12
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +6 -0
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/session.d.ts +1 -0
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/authority.test.js +435 -32
- package/dist/hud/__tests__/authority.test.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +2 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +42 -0
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +642 -15
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +61 -0
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +160 -4
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +180 -21
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/authority.d.ts +5 -0
- package/dist/hud/authority.d.ts.map +1 -1
- package/dist/hud/authority.js +324 -28
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/index.d.ts +3 -2
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +42 -19
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts +3 -3
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +128 -19
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +35 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +65 -80
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts +24 -6
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +136 -38
- package/dist/hud/tmux.js.map +1 -1
- package/dist/hud/types.d.ts +11 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +71 -1
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +32 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +113 -17
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/question/__tests__/renderer.test.js +566 -1
- package/dist/question/__tests__/renderer.test.js.map +1 -1
- package/dist/question/renderer.d.ts +9 -1
- package/dist/question/renderer.d.ts.map +1 -1
- package/dist/question/renderer.js +246 -70
- package/dist/question/renderer.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +837 -101
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-state-io.test.js +72 -1
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +57 -0
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js +74 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js +65 -0
- package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +107 -39
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/eval/eval-parity-smoke.js +1 -1
- package/dist/scripts/eval/eval-parity-smoke.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +3 -1
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +3 -10
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +62 -38
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +7 -0
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +7 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +24 -18
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +75 -11
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/run-test-files.js +193 -22
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +61 -3
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-agents.d.ts.map +1 -1
- package/dist/scripts/verify-native-agents.js +58 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +113 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +3 -16
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +25 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +57 -2
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +7 -39
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -14
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +1 -1
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +9 -4
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +195 -2
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +4 -2
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +3 -2
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/tmux-session.d.ts +2 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +142 -12
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/utils/__tests__/platform-command.test.js +16 -1
- package/dist/utils/__tests__/platform-command.test.js.map +1 -1
- package/dist/utils/__tests__/version.test.d.ts +2 -0
- package/dist/utils/__tests__/version.test.d.ts.map +1 -0
- package/dist/utils/__tests__/version.test.js +51 -0
- package/dist/utils/__tests__/version.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +16 -4
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-command.d.ts +9 -0
- package/dist/utils/platform-command.d.ts.map +1 -1
- package/dist/utils/platform-command.js +15 -0
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/version.d.ts +7 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +67 -0
- package/dist/utils/version.js.map +1 -0
- package/dist/verification/__tests__/ci-rust-gates.test.js +89 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +16 -2
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/package.json +11 -10
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +334 -21
- package/plugins/oh-my-codex/hooks/hooks.json +1 -2
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +3 -1
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +7 -7
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +51 -11
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -22
- package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +9 -0
- package/skills/autopilot/SKILL.md +3 -1
- package/skills/code-review/SKILL.md +7 -7
- package/skills/deep-interview/SKILL.md +51 -11
- package/skills/ralph/SKILL.md +22 -22
- package/skills/ultraqa/SKILL.md +9 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +946 -98
- package/src/scripts/__tests__/notify-state-io.test.ts +95 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +82 -0
- package/src/scripts/__tests__/run-test-files.test.ts +102 -0
- package/src/scripts/__tests__/verify-native-agents.test.ts +75 -0
- package/src/scripts/codex-native-hook.ts +123 -34
- package/src/scripts/demo-team-e2e.sh +10 -7
- package/src/scripts/eval/eval-parity-smoke.ts +1 -1
- package/src/scripts/notify-hook/auto-nudge.ts +3 -1
- package/src/scripts/notify-hook/ralph-session-resume.ts +2 -8
- package/src/scripts/notify-hook/state-io.ts +75 -37
- package/src/scripts/notify-hook/team-leader-nudge.ts +7 -0
- package/src/scripts/notify-hook/tmux-injection.ts +35 -19
- package/src/scripts/notify-hook.ts +91 -4
- package/src/scripts/prepare-build.js +83 -0
- package/src/scripts/run-test-files.ts +192 -22
- package/src/scripts/sync-plugin-mirror.ts +98 -9
- package/src/scripts/verify-native-agents.ts +65 -1
- package/src/scripts/postinstall-bootstrap.js +0 -23
|
@@ -21,7 +21,7 @@ import { runProcess } from './process-runner.js';
|
|
|
21
21
|
import { logTmuxHookEvent } from './log.js';
|
|
22
22
|
import { resolveInvocationSessionId, resolveManagedCurrentPane, resolveManagedSessionContext, verifyManagedPaneTarget } from './managed-tmux.js';
|
|
23
23
|
import { evaluatePaneInjectionReadiness, mapPaneInjectionReadinessReason, sendPaneInput } from './team-tmux-guard.js';
|
|
24
|
-
import { listActiveSkills,
|
|
24
|
+
import { listActiveSkills, readVisibleSkillActiveStateForStateDir } from '../../state/skill-active.js';
|
|
25
25
|
import {
|
|
26
26
|
normalizeTmuxHookConfig,
|
|
27
27
|
pickActiveMode,
|
|
@@ -99,12 +99,14 @@ async function resolveCanonicalPaneFromPaneTarget(paneTarget: any, expectedCwd:
|
|
|
99
99
|
return finalizeResolvedPane(healedPaneId, 'healed_hud_pane_target', expectedCwd);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
async function resolvePreferredModePane(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
async function resolvePreferredModePane(
|
|
103
|
+
stateDir: string,
|
|
104
|
+
allowedModes: string[],
|
|
105
|
+
options: { includeRootFallback?: boolean } = {},
|
|
106
|
+
): Promise<{ mode: string; state: any; pane: string; stateDir: string } | null> {
|
|
107
|
+
const dirs = await getScopedStateDirsForCurrentSession(stateDir, undefined, {
|
|
108
|
+
includeRootFallback: options.includeRootFallback !== false,
|
|
109
|
+
}).catch(() => [stateDir]);
|
|
108
110
|
for (const dir of dirs) {
|
|
109
111
|
for (const mode of allowedModes || []) {
|
|
110
112
|
const path = join(dir, `${mode}-state.json`);
|
|
@@ -189,12 +191,18 @@ async function validateResolvedInjectionOwnership({
|
|
|
189
191
|
return { ok: true };
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
async function readVisibleAllowedModes(
|
|
194
|
+
export async function readVisibleAllowedModes(
|
|
193
195
|
cwd: string,
|
|
194
196
|
stateDir: string,
|
|
195
197
|
payload: any,
|
|
196
198
|
allowedModes: string[],
|
|
197
|
-
): Promise<{
|
|
199
|
+
): Promise<{
|
|
200
|
+
canonicalPresent: boolean;
|
|
201
|
+
activeSkillCount: number;
|
|
202
|
+
allowedSet: Set<string> | null;
|
|
203
|
+
preferredMode: string | null;
|
|
204
|
+
sessionScoped: boolean;
|
|
205
|
+
}> {
|
|
198
206
|
const candidateSessionIds = [
|
|
199
207
|
await readCurrentSessionId(stateDir).catch(() => undefined),
|
|
200
208
|
resolveInvocationSessionId(payload),
|
|
@@ -203,41 +211,49 @@ async function readVisibleAllowedModes(
|
|
|
203
211
|
.filter(Boolean);
|
|
204
212
|
|
|
205
213
|
for (const sessionId of candidateSessionIds) {
|
|
206
|
-
const canonicalState = await
|
|
214
|
+
const canonicalState = await readVisibleSkillActiveStateForStateDir(stateDir, sessionId);
|
|
207
215
|
if (!canonicalState) continue;
|
|
208
216
|
|
|
217
|
+
const activeSkills = listActiveSkills(canonicalState);
|
|
209
218
|
const allowedSet = new Set(
|
|
210
|
-
|
|
219
|
+
activeSkills
|
|
211
220
|
.map((entry) => entry.skill)
|
|
212
221
|
.filter((skill) => allowedModes.includes(skill)),
|
|
213
222
|
);
|
|
214
223
|
return {
|
|
215
224
|
canonicalPresent: true,
|
|
225
|
+
activeSkillCount: activeSkills.length,
|
|
216
226
|
allowedSet,
|
|
217
227
|
preferredMode: pickActiveMode([...allowedSet], allowedModes),
|
|
228
|
+
sessionScoped: true,
|
|
218
229
|
};
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
if (candidateSessionIds.length === 0) {
|
|
222
|
-
const rootCanonicalState = await
|
|
233
|
+
const rootCanonicalState = await readVisibleSkillActiveStateForStateDir(stateDir).catch(() => null);
|
|
223
234
|
if (rootCanonicalState) {
|
|
235
|
+
const activeSkills = listActiveSkills(rootCanonicalState);
|
|
224
236
|
const allowedSet = new Set(
|
|
225
|
-
|
|
237
|
+
activeSkills
|
|
226
238
|
.map((entry) => entry.skill)
|
|
227
239
|
.filter((skill) => allowedModes.includes(skill)),
|
|
228
240
|
);
|
|
229
241
|
return {
|
|
230
242
|
canonicalPresent: true,
|
|
243
|
+
activeSkillCount: activeSkills.length,
|
|
231
244
|
allowedSet,
|
|
232
245
|
preferredMode: pickActiveMode([...allowedSet], allowedModes),
|
|
246
|
+
sessionScoped: false,
|
|
233
247
|
};
|
|
234
248
|
}
|
|
235
249
|
}
|
|
236
250
|
|
|
237
251
|
return {
|
|
238
252
|
canonicalPresent: false,
|
|
253
|
+
activeSkillCount: 0,
|
|
239
254
|
allowedSet: null,
|
|
240
255
|
preferredMode: null,
|
|
256
|
+
sessionScoped: candidateSessionIds.length > 0,
|
|
241
257
|
};
|
|
242
258
|
}
|
|
243
259
|
|
|
@@ -415,10 +431,11 @@ export async function handleTmuxInjection({
|
|
|
415
431
|
state.recent_keys = pruneRecentKeys(state.recent_keys, now);
|
|
416
432
|
const canonicalModeState = await readVisibleAllowedModes(cwd, stateDir, payload, config.allowed_modes).catch(() => ({
|
|
417
433
|
canonicalPresent: false,
|
|
434
|
+
activeSkillCount: 0,
|
|
418
435
|
allowedSet: null,
|
|
419
436
|
preferredMode: null,
|
|
420
437
|
}));
|
|
421
|
-
if (canonicalModeState.canonicalPresent && !canonicalModeState.preferredMode) {
|
|
438
|
+
if (canonicalModeState.canonicalPresent && canonicalModeState.activeSkillCount > 0 && !canonicalModeState.preferredMode) {
|
|
422
439
|
const nextState = {
|
|
423
440
|
...state,
|
|
424
441
|
last_reason: 'mode_not_allowed',
|
|
@@ -468,12 +485,10 @@ export async function handleTmuxInjection({
|
|
|
468
485
|
}
|
|
469
486
|
};
|
|
470
487
|
try {
|
|
471
|
-
const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir
|
|
488
|
+
const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir, undefined, {
|
|
489
|
+
includeRootFallback: !canonicalModeState.canonicalPresent && !canonicalModeState.sessionScoped,
|
|
490
|
+
});
|
|
472
491
|
await scanActiveModeStateDirs(scopedDirs);
|
|
473
|
-
|
|
474
|
-
if (!pickActiveMode(activeModes, config.allowed_modes) && !scannedStateDirs.has(resolvePath(stateDir))) {
|
|
475
|
-
await scanActiveModeStateDirs([stateDir], true);
|
|
476
|
-
}
|
|
477
492
|
} catch {
|
|
478
493
|
// Non-fatal
|
|
479
494
|
}
|
|
@@ -483,6 +498,7 @@ export async function handleTmuxInjection({
|
|
|
483
498
|
canonicalModeState.canonicalPresent
|
|
484
499
|
? (canonicalModeState.preferredMode ? [canonicalModeState.preferredMode] : [])
|
|
485
500
|
: config.allowed_modes,
|
|
501
|
+
{ includeRootFallback: !canonicalModeState.sessionScoped },
|
|
486
502
|
).catch(() => null);
|
|
487
503
|
const mode = canonicalModeState.canonicalPresent
|
|
488
504
|
? canonicalModeState.preferredMode
|
|
@@ -262,6 +262,78 @@ function classifyIdleNotificationPhase(message: unknown): 'idle' | 'progress' |
|
|
|
262
262
|
return 'idle';
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
|
|
266
|
+
function isExplicitAutopilotActivationText(text: string): boolean {
|
|
267
|
+
return /(?:^|[^\w])\$autopilot\b/i.test(text)
|
|
268
|
+
|| /^\s*\/autopilot\b/i.test(text)
|
|
269
|
+
|| /^\s*(?:please\s+)?autopilot(?:\s+(?:this|mode|workflow|skill|loop|now))?\s*[.!]?\s*$/i.test(text)
|
|
270
|
+
|| /\b(?:use|run|start|enable|launch|invoke|activate|resume|continue)\s+(?:the\s+)?autopilot(?:\s+(?:mode|workflow|skill|loop|now))?\s*[.!]?\s*$/i.test(text)
|
|
271
|
+
|| /\bautopilot\s+(?:mode|workflow|skill|loop)\b/i.test(text);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function looksLikeAutopilotTerminalHandoff(text: string): boolean {
|
|
275
|
+
return /\bAutopilot complete\b/i.test(text)
|
|
276
|
+
|| /\btask_complete\b/i.test(text)
|
|
277
|
+
|| /\bautopilot\b[\s\S]{0,120}\b(?:complete|completed|finished)\b/i.test(text);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isTerminalModeStateObject(value: unknown, mode: string): boolean {
|
|
281
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
282
|
+
const state = value as Record<string, unknown>;
|
|
283
|
+
if (safeString(state.mode).trim() !== mode) return false;
|
|
284
|
+
if (state.active === true) return false;
|
|
285
|
+
const phase = safeString(state.current_phase || state.currentPhase).trim().toLowerCase().replace(/_/g, '-');
|
|
286
|
+
if (['complete', 'completed', 'failed', 'cancelled', 'canceled', 'stopped', 'user-stopped'].includes(phase)) return true;
|
|
287
|
+
const outcome = safeString(state.run_outcome || state.outcome || state.lifecycle_outcome || state.terminal_outcome).trim().toLowerCase();
|
|
288
|
+
return ['finish', 'finished', 'complete', 'completed', 'failed', 'cancelled', 'canceled'].includes(outcome)
|
|
289
|
+
|| safeString(state.completed_at || state.completedAt).trim() !== '';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function terminalStateMatchesNotifyTurn(state: Record<string, unknown>, payload: Record<string, unknown>): boolean {
|
|
293
|
+
const payloadTurnId = safeString(payload['turn-id'] || payload.turn_id || '').trim();
|
|
294
|
+
const stateTurnId = safeString(state.turn_id || state.turnId || '').trim();
|
|
295
|
+
const payloadThreadId = safeString(payload['thread-id'] || payload.thread_id || '').trim();
|
|
296
|
+
const stateThreadId = safeString(state.thread_id || state.threadId || '').trim();
|
|
297
|
+
|
|
298
|
+
if (payloadTurnId || stateTurnId) {
|
|
299
|
+
if (!payloadTurnId || !stateTurnId || payloadTurnId !== stateTurnId) return false;
|
|
300
|
+
return !payloadThreadId || !stateThreadId || payloadThreadId === stateThreadId;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return Boolean(payloadThreadId && stateThreadId && payloadThreadId === stateThreadId);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function hasTerminalAutopilotStateForNotifyTurn(
|
|
307
|
+
stateDir: string,
|
|
308
|
+
sessionId: string,
|
|
309
|
+
payload: Record<string, unknown>,
|
|
310
|
+
): Promise<boolean> {
|
|
311
|
+
const state = await readScopedJsonIfExists(
|
|
312
|
+
stateDir,
|
|
313
|
+
'autopilot-state.json',
|
|
314
|
+
sessionId || undefined,
|
|
315
|
+
null,
|
|
316
|
+
{ includeRootFallback: true },
|
|
317
|
+
);
|
|
318
|
+
return isTerminalModeStateObject(state, 'autopilot')
|
|
319
|
+
&& terminalStateMatchesNotifyTurn(state as Record<string, unknown>, payload);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function shouldSuppressAutopilotTerminalReplayActivation(
|
|
323
|
+
stateDir: string,
|
|
324
|
+
payload: Record<string, unknown>,
|
|
325
|
+
isAutopilotActivation: boolean,
|
|
326
|
+
sessionId: string,
|
|
327
|
+
): Promise<boolean> {
|
|
328
|
+
if (!isTurnCompletePayload(payload) && !isNotifyFallbackTaskCompletePayload(payload)) return false;
|
|
329
|
+
if (!isAutopilotActivation) return false;
|
|
330
|
+
|
|
331
|
+
const lastAssistantMessage = safeString(payload['last-assistant-message'] || payload.last_assistant_message || '');
|
|
332
|
+
if (!looksLikeAutopilotTerminalHandoff(lastAssistantMessage) && !isNotifyFallbackTaskCompletePayload(payload)) return false;
|
|
333
|
+
|
|
334
|
+
return hasTerminalAutopilotStateForNotifyTurn(stateDir, sessionId, payload);
|
|
335
|
+
}
|
|
336
|
+
|
|
265
337
|
function buildIdleNotificationFingerprint(payload: Record<string, unknown>): string {
|
|
266
338
|
const lastAssistantMessage = safeString(payload['last-assistant-message'] || payload.last_assistant_message || '');
|
|
267
339
|
const summary = summarizeIdleNotificationMessage(lastAssistantMessage);
|
|
@@ -641,16 +713,28 @@ async function main() {
|
|
|
641
713
|
|
|
642
714
|
// 4.45. Skill activation tracking: update skill-active-state.json before any nudge logic.
|
|
643
715
|
try {
|
|
644
|
-
const { recordSkillActivation } = await import('../hooks/keyword-detector.js');
|
|
716
|
+
const { detectKeywords, recordSkillActivation } = await import('../hooks/keyword-detector.js');
|
|
645
717
|
if (latestUserInput) {
|
|
718
|
+
const activationSessionId = getEffectiveSessionId();
|
|
719
|
+
const isAutopilotActivation = detectKeywords(latestUserInput)
|
|
720
|
+
.some((match) => match.skill === 'autopilot')
|
|
721
|
+
|| isExplicitAutopilotActivationText(latestUserInput);
|
|
722
|
+
const suppressTerminalReplay = await shouldSuppressAutopilotTerminalReplayActivation(
|
|
723
|
+
stateDir,
|
|
724
|
+
payload,
|
|
725
|
+
isAutopilotActivation,
|
|
726
|
+
activationSessionId,
|
|
727
|
+
);
|
|
728
|
+
if (!suppressTerminalReplay) {
|
|
646
729
|
await recordSkillActivation({
|
|
647
730
|
stateDir,
|
|
648
731
|
sourceCwd: cwd,
|
|
649
732
|
text: latestUserInput,
|
|
650
|
-
sessionId:
|
|
733
|
+
sessionId: activationSessionId,
|
|
651
734
|
threadId: payloadThreadId,
|
|
652
735
|
turnId: safeString(payload['turn-id'] || payload.turn_id || ''),
|
|
653
736
|
});
|
|
737
|
+
}
|
|
654
738
|
}
|
|
655
739
|
} catch {
|
|
656
740
|
// Non-fatal: keyword detector module may not be built yet
|
|
@@ -662,8 +746,11 @@ async function main() {
|
|
|
662
746
|
// Non-fatal: lifecycle sync should not block the hook
|
|
663
747
|
}
|
|
664
748
|
|
|
665
|
-
const
|
|
666
|
-
const
|
|
749
|
+
const effectiveSessionId = getEffectiveSessionId();
|
|
750
|
+
const deepInterviewStateActive = effectiveSessionId
|
|
751
|
+
? await isDeepInterviewStateActive(stateDir, effectiveSessionId)
|
|
752
|
+
: await isDeepInterviewStateActive(stateDir, undefined);
|
|
753
|
+
const deepInterviewInputLockActive = await isDeepInterviewInputLockActive(stateDir, effectiveSessionId);
|
|
667
754
|
|
|
668
755
|
// 4.55. Notify leader when individual worker transitions to idle (worker session only)
|
|
669
756
|
if (isTeamWorker && parsedTeamWorker && !deepInterviewStateActive) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const requiredDistFiles = [
|
|
7
|
+
join(process.cwd(), 'dist', 'cli', 'omx.js'),
|
|
8
|
+
join(process.cwd(), 'dist', 'scripts', 'postinstall.js'),
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
if (requiredDistFiles.every((file) => existsSync(file))) {
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
16
|
+
const tscBin = process.platform === 'win32'
|
|
17
|
+
? join(process.cwd(), 'node_modules', '.bin', 'tsc.cmd')
|
|
18
|
+
: join(process.cwd(), 'node_modules', '.bin', 'tsc');
|
|
19
|
+
const nodeModulesDir = join(process.cwd(), 'node_modules');
|
|
20
|
+
|
|
21
|
+
function runNpm(args, env = process.env) {
|
|
22
|
+
return spawnSync(npmBin, args, {
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
stdio: process.env.npm_config_json === 'true' ? ['inherit', 'ignore', 'inherit'] : 'inherit',
|
|
25
|
+
env,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function exitOnFailure(result, label) {
|
|
30
|
+
if (result.error) {
|
|
31
|
+
console.error(`omx prepare: failed to launch ${label}: ${result.error.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (result.status !== 0) {
|
|
36
|
+
process.exit(typeof result.status === 'number' ? result.status : 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let shouldCleanupBootstrappedDependencies = false;
|
|
41
|
+
|
|
42
|
+
if (!existsSync(tscBin)) {
|
|
43
|
+
const hadNodeModules = existsSync(nodeModulesDir);
|
|
44
|
+
const installResult = runNpm(
|
|
45
|
+
[
|
|
46
|
+
'install',
|
|
47
|
+
'--global=false',
|
|
48
|
+
'--location=project',
|
|
49
|
+
'--include=dev',
|
|
50
|
+
'--ignore-scripts',
|
|
51
|
+
'--no-audit',
|
|
52
|
+
'--no-progress',
|
|
53
|
+
],
|
|
54
|
+
{
|
|
55
|
+
...process.env,
|
|
56
|
+
npm_config_global: 'false',
|
|
57
|
+
npm_config_location: 'project',
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
exitOnFailure(installResult, 'npm dependency bootstrap');
|
|
61
|
+
shouldCleanupBootstrappedDependencies = !hadNodeModules;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pathWithLocalBins = [
|
|
65
|
+
join(process.cwd(), 'node_modules', '.bin'),
|
|
66
|
+
process.env.PATH ?? '',
|
|
67
|
+
].filter(Boolean).join(delimiter);
|
|
68
|
+
|
|
69
|
+
const buildResult = spawnSync(npmBin, ['run', 'build'], {
|
|
70
|
+
cwd: process.cwd(),
|
|
71
|
+
stdio: process.env.npm_config_json === 'true' ? ['inherit', 'ignore', 'inherit'] : 'inherit',
|
|
72
|
+
env: { ...process.env, PATH: pathWithLocalBins },
|
|
73
|
+
});
|
|
74
|
+
exitOnFailure(buildResult, 'npm build');
|
|
75
|
+
|
|
76
|
+
if (shouldCleanupBootstrappedDependencies) {
|
|
77
|
+
try {
|
|
78
|
+
rmSync(nodeModulesDir, { recursive: true, force: true });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
console.warn(`[omx:prepare] Warning: could not remove bootstrapped node_modules: ${message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
|
|
2
2
|
import { readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
|
|
5
5
|
const DEFAULT_TEST_TIMEOUT_MS = 0;
|
|
6
6
|
const DEFAULT_RUNNER_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
7
|
+
const DEFAULT_FORCE_EXIT_GRACE_MS = 30_000;
|
|
7
8
|
const DEFAULT_CI_TEST_CONCURRENCY = 1;
|
|
8
9
|
const RUNTIME_STATE_ENV_KEYS = [
|
|
9
10
|
'OMX_ROOT',
|
|
@@ -47,6 +48,10 @@ function parseTimeoutMs(value: string | undefined, defaultTimeoutMs: number): nu
|
|
|
47
48
|
return Math.floor(parsed);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function isWindows(): boolean {
|
|
52
|
+
return process.platform === 'win32';
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
function parseTestConcurrency(env: NodeJS.ProcessEnv): number | undefined {
|
|
51
56
|
const rawValue = env.OMX_NODE_TEST_CONCURRENCY;
|
|
52
57
|
if (rawValue) {
|
|
@@ -74,6 +79,7 @@ if (files.length === 0) {
|
|
|
74
79
|
|
|
75
80
|
const testTimeoutMs = parseTimeoutMs(process.env.OMX_NODE_TEST_TIMEOUT_MS, DEFAULT_TEST_TIMEOUT_MS);
|
|
76
81
|
const runnerTimeoutMs = parseTimeoutMs(process.env.OMX_NODE_TEST_RUNNER_TIMEOUT_MS, DEFAULT_RUNNER_TIMEOUT_MS);
|
|
82
|
+
const forceExitGraceMs = parseTimeoutMs(process.env.OMX_NODE_TEST_FORCE_EXIT_GRACE_MS, DEFAULT_FORCE_EXIT_GRACE_MS);
|
|
77
83
|
const testConcurrency = parseTestConcurrency(process.env);
|
|
78
84
|
const forceExit = parseBooleanEnv(process.env.OMX_NODE_TEST_FORCE_EXIT);
|
|
79
85
|
const testArgs = ['--test'];
|
|
@@ -84,7 +90,7 @@ if (testConcurrency) {
|
|
|
84
90
|
testArgs.push(`--test-concurrency=${testConcurrency}`);
|
|
85
91
|
}
|
|
86
92
|
if (forceExit) {
|
|
87
|
-
testArgs.push('--test-force-exit');
|
|
93
|
+
testArgs.push('--test-force-exit', '--test-reporter=tap');
|
|
88
94
|
}
|
|
89
95
|
testArgs.push(...files);
|
|
90
96
|
|
|
@@ -92,7 +98,7 @@ console.error(
|
|
|
92
98
|
`[run-test-files] running ${files.length} test file(s) from ${targets.join(', ')}${
|
|
93
99
|
testTimeoutMs > 0 ? ` with per-test timeout ${testTimeoutMs}ms` : ' with per-test timeout disabled'
|
|
94
100
|
}${testConcurrency ? `, test concurrency ${testConcurrency}` : ', default test concurrency'}${
|
|
95
|
-
forceExit ?
|
|
101
|
+
forceExit ? `, force exit enabled with ${forceExitGraceMs}ms completion grace` : ', force exit disabled'
|
|
96
102
|
}${runnerTimeoutMs > 0 ? `, and runner timeout ${runnerTimeoutMs}ms` : ', and runner timeout disabled'}`,
|
|
97
103
|
);
|
|
98
104
|
|
|
@@ -105,26 +111,190 @@ if (!parseBooleanEnv(process.env.OMX_NODE_TEST_PRESERVE_RUNTIME_ENV)) {
|
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
})
|
|
114
|
+
function reportAbnormalExit(signal: NodeJS.Signals | null, errorMessage?: string): void {
|
|
115
|
+
if (errorMessage) {
|
|
116
|
+
console.error(`[run-test-files] node --test error: ${errorMessage}`);
|
|
117
|
+
}
|
|
118
|
+
console.error(
|
|
119
|
+
`[run-test-files] node --test did not exit normally${signal ? ` (signal: ${signal})` : ''}. `
|
|
120
|
+
+ `Roots: ${targets.join(', ')}. Test files: ${files.length}. `
|
|
121
|
+
+ `Per-test timeout: ${testTimeoutMs > 0 ? `${testTimeoutMs}ms` : 'disabled'}. `
|
|
122
|
+
+ `Test concurrency: ${testConcurrency ?? 'default'}. `
|
|
123
|
+
+ `Force exit: ${forceExit ? 'enabled' : 'disabled'}. `
|
|
124
|
+
+ `Runner timeout: ${runnerTimeoutMs > 0 ? `${runnerTimeoutMs}ms` : 'disabled'}.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
114
127
|
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
function signalChild(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
129
|
+
try {
|
|
130
|
+
if (!isWindows() && child.pid) {
|
|
131
|
+
process.kill(-child.pid, signal);
|
|
132
|
+
} else {
|
|
133
|
+
child.kill(signal);
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
try {
|
|
137
|
+
child.kill(signal);
|
|
138
|
+
} catch {
|
|
139
|
+
// Ignore kill races. The child might have exited between detection and termination.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
117
142
|
}
|
|
118
143
|
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
function terminateChild(child: ChildProcess): void {
|
|
145
|
+
signalChild(child, 'SIGTERM');
|
|
146
|
+
signalChild(child, 'SIGKILL');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function runWithCompletionForceExit(): void {
|
|
150
|
+
let finished = false;
|
|
151
|
+
let sawFailure = false;
|
|
152
|
+
let lastTapOk = 0;
|
|
153
|
+
let tapTests: number | undefined;
|
|
154
|
+
let tapPass: number | undefined;
|
|
155
|
+
let tapFail = 0;
|
|
156
|
+
let tapCancelled = 0;
|
|
157
|
+
let completedFromSummary = false;
|
|
158
|
+
let completionTimer: NodeJS.Timeout | undefined;
|
|
159
|
+
let runnerTimer: NodeJS.Timeout | undefined;
|
|
160
|
+
let stdoutRemainder = '';
|
|
161
|
+
let stderrRemainder = '';
|
|
162
|
+
|
|
163
|
+
const child = spawn(process.execPath, testArgs, {
|
|
164
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
165
|
+
env: childEnv,
|
|
166
|
+
detached: !isWindows(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function finish(exitCode: number, reason: string): void {
|
|
170
|
+
if (finished) return;
|
|
171
|
+
finished = true;
|
|
172
|
+
if (completionTimer) clearTimeout(completionTimer);
|
|
173
|
+
if (runnerTimer) clearTimeout(runnerTimer);
|
|
174
|
+
console.error(`[run-test-files] ${reason}; exiting with status ${exitCode}`);
|
|
175
|
+
terminateChild(child);
|
|
176
|
+
child.stdout?.destroy();
|
|
177
|
+
child.stderr?.destroy();
|
|
178
|
+
child.unref();
|
|
179
|
+
process.exit(exitCode);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function markFailure(): void {
|
|
183
|
+
sawFailure = true;
|
|
184
|
+
if (completionTimer) {
|
|
185
|
+
clearTimeout(completionTimer);
|
|
186
|
+
completionTimer = undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function armCompletionTimer(reason: string): void {
|
|
191
|
+
if (sawFailure) return;
|
|
192
|
+
if (completionTimer) clearTimeout(completionTimer);
|
|
193
|
+
completionTimer = setTimeout(() => {
|
|
194
|
+
if (sawFailure) return;
|
|
195
|
+
finish(0, reason);
|
|
196
|
+
}, forceExitGraceMs);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sawCleanTapSummary(): boolean {
|
|
200
|
+
if (tapTests === undefined || tapPass === undefined) return false;
|
|
201
|
+
return tapTests === tapPass && tapFail === 0 && tapCancelled === 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseTapLine(line: string): void {
|
|
205
|
+
if (/^(?:not ok|Bail out!)/.test(line)) {
|
|
206
|
+
markFailure();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const summary = line.match(/^# (tests|pass|fail|cancelled) (\d+)$/);
|
|
211
|
+
if (summary) {
|
|
212
|
+
const count = Number(summary[2]);
|
|
213
|
+
if (summary[1] === 'tests') tapTests = count;
|
|
214
|
+
if (summary[1] === 'pass') tapPass = count;
|
|
215
|
+
if (summary[1] === 'fail') tapFail = count;
|
|
216
|
+
if (summary[1] === 'cancelled') tapCancelled = count;
|
|
217
|
+
if ((summary[1] === 'fail' || summary[1] === 'cancelled') && count > 0) markFailure();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const ok = line.match(/^ok (\d+)\b/);
|
|
222
|
+
if (ok) {
|
|
223
|
+
lastTapOk = Number(ok[1]);
|
|
224
|
+
if (lastTapOk >= files.length) {
|
|
225
|
+
armCompletionTimer(`force-exit completion grace elapsed after TAP ok ${lastTapOk} with no later failures`);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const plan = line.match(/^1\.\.(\d+)$/);
|
|
231
|
+
if (plan && Number(plan[1]) === lastTapOk && !sawFailure) {
|
|
232
|
+
armCompletionTimer(`force-exit completion grace elapsed after TAP plan ${line}`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (/^# duration_ms /.test(line) && sawCleanTapSummary()) {
|
|
237
|
+
completedFromSummary = true;
|
|
238
|
+
armCompletionTimer('force-exit completion grace elapsed after clean TAP summary');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleOutput(chunk: Buffer, stream: NodeJS.WriteStream, isStdout: boolean): void {
|
|
243
|
+
stream.write(chunk);
|
|
244
|
+
const text = chunk.toString('utf8');
|
|
245
|
+
let combined = (isStdout ? stdoutRemainder : stderrRemainder) + text;
|
|
246
|
+
const lines = combined.split(/\r?\n/);
|
|
247
|
+
combined = lines.pop() ?? '';
|
|
248
|
+
if (isStdout) {
|
|
249
|
+
stdoutRemainder = combined;
|
|
250
|
+
} else {
|
|
251
|
+
stderrRemainder = combined;
|
|
252
|
+
}
|
|
253
|
+
for (const line of lines) parseTapLine(line);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
child.stdout?.on('data', (chunk: Buffer) => handleOutput(chunk, process.stdout, true));
|
|
257
|
+
child.stderr?.on('data', (chunk: Buffer) => handleOutput(chunk, process.stderr, false));
|
|
258
|
+
|
|
259
|
+
child.on('error', (error) => {
|
|
260
|
+
reportAbnormalExit(null, error.message);
|
|
261
|
+
finish(1, 'node --test failed to spawn');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
child.on('exit', (status, signal) => {
|
|
265
|
+
if (finished) return;
|
|
266
|
+
if (stdoutRemainder) parseTapLine(stdoutRemainder);
|
|
267
|
+
if (stderrRemainder) parseTapLine(stderrRemainder);
|
|
268
|
+
if (typeof status === 'number') {
|
|
269
|
+
finish(status, `node --test exited normally${completedFromSummary ? ' after clean TAP summary' : ''}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
reportAbnormalExit(signal);
|
|
273
|
+
finish(1, 'node --test exited without a numeric status');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (runnerTimeoutMs > 0) {
|
|
277
|
+
runnerTimer = setTimeout(() => {
|
|
278
|
+
reportAbnormalExit(null);
|
|
279
|
+
finish(1, `runner timeout ${runnerTimeoutMs}ms elapsed`);
|
|
280
|
+
}, runnerTimeoutMs);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (forceExit) {
|
|
285
|
+
runWithCompletionForceExit();
|
|
286
|
+
} else {
|
|
287
|
+
const result = spawnSync(process.execPath, testArgs, {
|
|
288
|
+
stdio: 'inherit',
|
|
289
|
+
env: childEnv,
|
|
290
|
+
timeout: runnerTimeoutMs > 0 ? runnerTimeoutMs : undefined,
|
|
291
|
+
killSignal: 'SIGTERM',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (typeof result.status === 'number') {
|
|
295
|
+
process.exit(result.status);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
reportAbnormalExit(result.signal, result.error?.message);
|
|
299
|
+
process.exit(1);
|
|
121
300
|
}
|
|
122
|
-
console.error(
|
|
123
|
-
`[run-test-files] node --test did not exit normally${result.signal ? ` (signal: ${result.signal})` : ''}. `
|
|
124
|
-
+ `Roots: ${targets.join(', ')}. Test files: ${files.length}. `
|
|
125
|
-
+ `Per-test timeout: ${testTimeoutMs > 0 ? `${testTimeoutMs}ms` : 'disabled'}. `
|
|
126
|
-
+ `Test concurrency: ${testConcurrency ?? 'default'}. `
|
|
127
|
-
+ `Force exit: ${forceExit ? 'enabled' : 'disabled'}. `
|
|
128
|
-
+ `Runner timeout: ${runnerTimeoutMs > 0 ? `${runnerTimeoutMs}ms` : 'disabled'}.`,
|
|
129
|
-
);
|
|
130
|
-
process.exit(1);
|