oh-my-codex 0.18.16 → 0.19.0
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 +6 -6
- package/Cargo.toml +1 -1
- package/crates/omx-sparkshell/tests/execution.rs +8 -1
- package/dist/agents/__tests__/definitions.test.js +5 -5
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +18 -14
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +2 -2
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +4 -4
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.d.ts +1 -0
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +18 -16
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +17 -4
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/autopilot/__tests__/planner-routing.test.js +2 -2
- package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +1 -1
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autopilot/fsm.js +1 -1
- package/dist/autopilot/fsm.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +78 -3
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +30 -5
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +2 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +96 -8
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +135 -1
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +2 -4
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js +14 -0
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +8 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +229 -1
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +46 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +37 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +21 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/version.test.js +13 -1
- package/dist/cli/__tests__/version.test.js.map +1 -1
- package/dist/cli/auth.d.ts +1 -1
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +49 -9
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/index.d.ts +25 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +233 -32
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +22 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +4 -0
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +3 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +7 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +12 -0
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +267 -7
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +143 -30
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +121 -0
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +110 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +1 -1
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/reconcile.d.ts +3 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +281 -151
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +4 -2
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +44 -12
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1 -4
- package/dist/hud/tmux.js.map +1 -1
- package/dist/hud/types.d.ts +5 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/leader/__tests__/contract.test.d.ts +2 -0
- package/dist/leader/__tests__/contract.test.d.ts.map +1 -0
- package/dist/leader/__tests__/contract.test.js +75 -0
- package/dist/leader/__tests__/contract.test.js.map +1 -0
- package/dist/leader/contract.d.ts +26 -0
- package/dist/leader/contract.d.ts.map +1 -0
- package/dist/leader/contract.js +97 -0
- package/dist/leader/contract.js.map +1 -0
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +22 -7
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/dispatcher.test.js +16 -0
- package/dist/notifications/__tests__/dispatcher.test.js.map +1 -1
- package/dist/notifications/__tests__/profiles.test.js +87 -0
- package/dist/notifications/__tests__/profiles.test.js.map +1 -1
- package/dist/notifications/config.d.ts +1 -0
- package/dist/notifications/config.d.ts.map +1 -1
- package/dist/notifications/config.js +60 -22
- package/dist/notifications/config.js.map +1 -1
- package/dist/notifications/dispatcher.d.ts.map +1 -1
- package/dist/notifications/dispatcher.js +6 -16
- package/dist/notifications/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +2 -0
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.d.ts.map +1 -1
- package/dist/pipeline/stages/ultragoal.js +6 -1
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/question/__tests__/renderer.test.js +58 -4
- package/dist/question/__tests__/renderer.test.js.map +1 -1
- package/dist/question/renderer.d.ts +1 -1
- package/dist/question/renderer.d.ts.map +1 -1
- package/dist/question/renderer.js +73 -5
- package/dist/question/renderer.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +202 -0
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +7 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +63 -4
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +8484 -4560
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +4387 -190
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +1137 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/planning-gate.test.js +410 -0
- package/dist/state/__tests__/planning-gate.test.js.map +1 -1
- package/dist/state/operations.d.ts +10 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +242 -8
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +4 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +5 -3
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +642 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +248 -1
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +60 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +225 -1
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +72 -4
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +44 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +127 -8
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +89 -0
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/model-contract.d.ts +3 -0
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +42 -5
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +21 -11
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +13 -11
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/tmux-session.d.ts +1 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +63 -13
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +7 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +5 -0
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +2 -2
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +19 -7
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +16 -0
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +23 -0
- package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +3 -2
- package/skills/code-review/SKILL.md +16 -0
- package/skills/ecomode/references/agent-tiers.md +1 -1
- package/skills/ultragoal/SKILL.md +23 -0
- package/skills/ultrawork/references/agent-tiers.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +14014 -9097
- package/src/scripts/codex-native-hook.ts +4459 -199
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
|
-
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from "fs";
|
|
2
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "fs";
|
|
3
3
|
import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
|
|
4
|
-
import { extname, join, relative, resolve } from "path";
|
|
4
|
+
import { extname, isAbsolute, join, relative, resolve } from "path";
|
|
5
5
|
import { pathToFileURL } from "url";
|
|
6
6
|
import { readModeStateForActiveDecision, readModeStateForSession, updateModeState } from "../modes/base.js";
|
|
7
7
|
import { redactAuthSecrets } from "../auth/redact.js";
|
|
8
8
|
import { SKILL_ACTIVE_STATE_FILE, extractSessionIdFromInitializedStatePath, getSkillActiveStatePathsForStateDir, listActiveSkills, readSkillActiveState, readVisibleSkillActiveStateForStateDir, } from "../state/skill-active.js";
|
|
9
9
|
import { isTrustedSubagentThread, readSubagentSessionSummary, readSubagentTrackingState, recordSubagentTurnForSession, } from "../subagents/tracker.js";
|
|
10
10
|
import { resolveCanonicalTeamStateRoot, resolveWorkerNotifyTeamStateRootPath } from "../team/state-root.js";
|
|
11
|
-
import {
|
|
11
|
+
import { inferTerminalLifecycleOutcome } from "../runtime/run-outcome.js";
|
|
12
|
+
import { appendToLog, isSessionStale, isSessionStateUsable, readSessionState, readUsableSessionState, reconcileNativeSessionStart, } from "../hooks/session.js";
|
|
12
13
|
import { appendTeamEvent, readTeamLeaderAttention, readTeamConfig, readTeamManifestV2, readTeamPhase, writeTeamLeaderAttention, writeTeamPhase, } from "../team/state.js";
|
|
13
|
-
import { omxNotepadPath, resolveProjectMemoryPath } from "../utils/paths.js";
|
|
14
|
+
import { codexAgentsDir, omxNotepadPath, projectCodexAgentsDir, resolveProjectMemoryPath } from "../utils/paths.js";
|
|
14
15
|
import { findGitLayout } from "../utils/git-layout.js";
|
|
15
16
|
import { getBaseStateDir, getStateFilePath, getStatePath, getAuthoritativeActiveStatePaths } from "../mcp/state-paths.js";
|
|
16
17
|
import { detectKeywords, detectPrimaryKeyword, recordSkillActivation, } from "../hooks/keyword-detector.js";
|
|
@@ -22,12 +23,15 @@ import { handleTeamWorkerPostToolUseSuccess } from "./notify-hook/team-worker-po
|
|
|
22
23
|
import { maybeNudgeLeaderForAllowedWorkerStop } from "./notify-hook/team-worker-stop.js";
|
|
23
24
|
import { resolveCodexExecutionSurface, } from "./codex-execution-surface.js";
|
|
24
25
|
import { buildNativeHookEvent, } from "../hooks/extensibility/events.js";
|
|
26
|
+
import { isTrackedWorkflowMode } from "../state/workflow-transition.js";
|
|
25
27
|
import { dispatchHookEventRuntime } from "../hooks/extensibility/runtime.js";
|
|
26
28
|
import { getNotificationConfig, getVerbosity } from "../notifications/config.js";
|
|
27
29
|
import { reconcileHudForPromptSubmit } from "../hud/reconcile.js";
|
|
28
30
|
import { onPreCompact as buildWikiPreCompactContext, onSessionStart as buildWikiSessionStartContext, } from "../wiki/lifecycle.js";
|
|
29
31
|
import { readAutoresearchCompletionStatus, readAutoresearchModeStateForActiveDecision } from "../autoresearch/skill-validation.js";
|
|
30
|
-
import {
|
|
32
|
+
import { AGENT_DEFINITIONS } from "../agents/definitions.js";
|
|
33
|
+
import { deriveAutopilotChildPhase, normalizeAutopilotPhase } from "../autopilot/fsm.js";
|
|
34
|
+
import { CONDUCTOR_ORCHESTRATION_METADATA_PREFIXES, LEADER_CONDUCTOR_BLOCK, LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE, actionKindForConductorArtifact, authorizeConductorAction, classifyConductorArtifactKind, } from "../leader/contract.js";
|
|
31
35
|
import { readRunState } from "../runtime/run-state.js";
|
|
32
36
|
import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
|
|
33
37
|
import { buildCodexGoalTerminalCleanupNotice, } from "../goal-workflows/codex-goal-snapshot.js";
|
|
@@ -45,6 +49,7 @@ const TERMINAL_MODE_PHASES = new Set(["complete", "completed", "failed", "cancel
|
|
|
45
49
|
const SKILL_STOP_BLOCKERS = new Set(["ralplan"]);
|
|
46
50
|
const TEAM_STOP_BLOCKING_TASK_STATUSES = new Set(["pending", "in_progress", "blocked"]);
|
|
47
51
|
const TEAM_WORKER_TERMINAL_RUN_STATES = new Set(["done", "complete", "completed", "failed", "stopped", "cancelled"]);
|
|
52
|
+
const LEADER_CONDUCTOR_GOLDEN_RULE = "Main-root Conductor golden rule: delegate implementation work; do not self-execute source or plan edits.";
|
|
48
53
|
const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
|
|
49
54
|
const NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE = "native-subagent-capacity-blocker.json";
|
|
50
55
|
const NATIVE_SUBAGENT_CAPACITY_BLOCKER_TTL_MS = 30 * 60_000;
|
|
@@ -84,6 +89,9 @@ const RALPH_LIVE_RISK_PATTERNS = [
|
|
|
84
89
|
/\b(?:database|db|terraform|kubectl|kubernetes|aws|gcp|azure|external|destructive)\b/i,
|
|
85
90
|
/\b(?:telegram|vps|service|restart|send|notify|notification|notifications|cron)\b/i,
|
|
86
91
|
];
|
|
92
|
+
const KNOWN_TYPED_AGENT_ROLES = new Set(Object.keys(AGENT_DEFINITIONS));
|
|
93
|
+
let installedTypedAgentRoleNamesCacheKey = "";
|
|
94
|
+
let installedTypedAgentRoleNamesCache = new Set();
|
|
87
95
|
const RALPH_TASK_TEXT_FIELDS = [
|
|
88
96
|
"task_description",
|
|
89
97
|
"taskDescription",
|
|
@@ -418,6 +426,9 @@ function readHookEventName(payload) {
|
|
|
418
426
|
function sanitizeCodexHookOutput(hookEventName, output) {
|
|
419
427
|
if (!output || hookEventName !== "PreToolUse")
|
|
420
428
|
return output;
|
|
429
|
+
const preToolUseDenyOutput = toPreToolUseDenyOutput(output);
|
|
430
|
+
if (preToolUseDenyOutput)
|
|
431
|
+
return preToolUseDenyOutput;
|
|
421
432
|
const systemMessage = safeString(output.systemMessage).trim();
|
|
422
433
|
if (systemMessage)
|
|
423
434
|
return { systemMessage };
|
|
@@ -429,6 +440,33 @@ function sanitizeCodexHookOutput(hookEventName, output) {
|
|
|
429
440
|
const derivedSystemMessage = [reason, additionalContext].filter(Boolean).join("\n\n");
|
|
430
441
|
return derivedSystemMessage ? { systemMessage: derivedSystemMessage } : {};
|
|
431
442
|
}
|
|
443
|
+
function toPreToolUseDenyOutput(output) {
|
|
444
|
+
const sourceHookSpecificOutput = safeObject(output.hookSpecificOutput);
|
|
445
|
+
const legacyBlock = output.decision === "block";
|
|
446
|
+
const hookSpecificDeny = sourceHookSpecificOutput.permissionDecision === "deny";
|
|
447
|
+
if (!legacyBlock && !hookSpecificDeny)
|
|
448
|
+
return null;
|
|
449
|
+
const permissionDecisionReason = safeString(sourceHookSpecificOutput.permissionDecisionReason).trim();
|
|
450
|
+
const legacyReason = safeString(output.reason).trim();
|
|
451
|
+
const additionalContext = safeString(sourceHookSpecificOutput.additionalContext).trim();
|
|
452
|
+
const systemMessage = safeString(output.systemMessage).trim();
|
|
453
|
+
const reason = permissionDecisionReason || legacyReason || systemMessage;
|
|
454
|
+
if (!reason) {
|
|
455
|
+
throw new Error("Malformed PreToolUse block output: explicit deny/block requires non-empty permissionDecisionReason, reason, or systemMessage.");
|
|
456
|
+
}
|
|
457
|
+
const hookSpecificOutput = {
|
|
458
|
+
hookEventName: "PreToolUse",
|
|
459
|
+
permissionDecision: "deny",
|
|
460
|
+
permissionDecisionReason: reason,
|
|
461
|
+
};
|
|
462
|
+
if (additionalContext) {
|
|
463
|
+
hookSpecificOutput.additionalContext = additionalContext;
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
...(systemMessage ? { systemMessage } : {}),
|
|
467
|
+
hookSpecificOutput,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
432
470
|
export function mapCodexHookEventToOmxEvent(hookEventName) {
|
|
433
471
|
switch (hookEventName) {
|
|
434
472
|
case "SessionStart":
|
|
@@ -1201,6 +1239,28 @@ const SOURCE_DIFF_EXTENSIONS = new Set([
|
|
|
1201
1239
|
".ts",
|
|
1202
1240
|
".tsx",
|
|
1203
1241
|
]);
|
|
1242
|
+
const PLANNING_TMP_SCRIPT_LIKE_EXTENSIONS = new Set([
|
|
1243
|
+
".bash",
|
|
1244
|
+
".bat",
|
|
1245
|
+
".cjs",
|
|
1246
|
+
".cmd",
|
|
1247
|
+
".cts",
|
|
1248
|
+
".fish",
|
|
1249
|
+
".js",
|
|
1250
|
+
".jsx",
|
|
1251
|
+
".ksh",
|
|
1252
|
+
".mjs",
|
|
1253
|
+
".mts",
|
|
1254
|
+
".php",
|
|
1255
|
+
".pl",
|
|
1256
|
+
".ps1",
|
|
1257
|
+
".py",
|
|
1258
|
+
".rb",
|
|
1259
|
+
".sh",
|
|
1260
|
+
".ts",
|
|
1261
|
+
".tsx",
|
|
1262
|
+
".zsh",
|
|
1263
|
+
]);
|
|
1204
1264
|
function gitOutput(cwd, args) {
|
|
1205
1265
|
try {
|
|
1206
1266
|
return execFileSync("git", args, {
|
|
@@ -1673,6 +1733,8 @@ function buildAutopilotPromptActivationNote(skillState, options = {}) {
|
|
|
1673
1733
|
"The ralplan phase is not complete until Planner output has been reviewed sequentially by Architect and then Critic; do not hand off to Ultragoal or implementation until the ralplan state/artifact records both ralplan_architect_review and ralplan_critic_review with approval or an explicit blocker.",
|
|
1674
1734
|
"Do not silently fall back to ordinary $plan/ralplan-only handling; keep autopilot-state.json, skill-active-state.json, HUD/statusline, and Codex goal-mode handoff guidance visible while the workflow is active.",
|
|
1675
1735
|
"When Codex goal tools are available, call get_goal/create_goal only from the active thread handoff and treat the active goal as the completion contract until code-review and ultraqa are clean.",
|
|
1736
|
+
LEADER_CONDUCTOR_BLOCK,
|
|
1737
|
+
LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE,
|
|
1676
1738
|
].filter(Boolean).join(" ");
|
|
1677
1739
|
}
|
|
1678
1740
|
function formatExecutionHandoffList(cwd) {
|
|
@@ -1684,6 +1746,9 @@ function buildAdditionalContextMessage(prompt, skillState, cwd = process.cwd(),
|
|
|
1684
1746
|
if (!prompt)
|
|
1685
1747
|
return null;
|
|
1686
1748
|
const promptPriorityMessage = buildPromptPriorityMessage(prompt);
|
|
1749
|
+
if (payload && isTypedAgentRolePayload(payload)) {
|
|
1750
|
+
return promptPriorityMessage;
|
|
1751
|
+
}
|
|
1687
1752
|
const teamMode = readTeamModeConfig(cwd);
|
|
1688
1753
|
const matches = detectKeywords(prompt).filter((entry) => teamMode.enabled || entry.skill !== "team");
|
|
1689
1754
|
const match = matches[0] ?? null;
|
|
@@ -1845,6 +1910,11 @@ function parseTeamWorkerEnv(rawValue) {
|
|
|
1845
1910
|
workerName: match[2] || "",
|
|
1846
1911
|
};
|
|
1847
1912
|
}
|
|
1913
|
+
function hasTeamWorkerEnvironment() {
|
|
1914
|
+
return parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_INTERNAL_WORKER))
|
|
1915
|
+
!== null
|
|
1916
|
+
|| parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_WORKER)) !== null;
|
|
1917
|
+
}
|
|
1848
1918
|
async function resolveTeamStateDirForWorkerContext(cwd, workerContext) {
|
|
1849
1919
|
const resolved = await resolveWorkerNotifyTeamStateRootPath(cwd, workerContext, process.env).catch(() => null);
|
|
1850
1920
|
if (resolved)
|
|
@@ -2457,6 +2527,47 @@ function readPayloadSessionId(payload) {
|
|
|
2457
2527
|
function readPayloadThreadId(payload) {
|
|
2458
2528
|
return safeString(payload.owner_codex_thread_id ?? payload.thread_id ?? payload.threadId).trim();
|
|
2459
2529
|
}
|
|
2530
|
+
function readPayloadAgentRole(payload) {
|
|
2531
|
+
const directRole = safeString(payload.agent_role
|
|
2532
|
+
?? payload.agentRole
|
|
2533
|
+
?? payload.agent_type
|
|
2534
|
+
?? payload.agentType).trim().toLowerCase();
|
|
2535
|
+
if (directRole)
|
|
2536
|
+
return directRole;
|
|
2537
|
+
const source = safeObject(payload.source);
|
|
2538
|
+
const subagent = safeObject(source?.subagent);
|
|
2539
|
+
const threadSpawn = safeObject(subagent?.thread_spawn);
|
|
2540
|
+
return safeString(threadSpawn?.agent_role
|
|
2541
|
+
?? threadSpawn?.agentRole
|
|
2542
|
+
?? threadSpawn?.agent_type
|
|
2543
|
+
?? threadSpawn?.agentType).trim().toLowerCase();
|
|
2544
|
+
}
|
|
2545
|
+
function readInstalledTypedAgentRoleNames() {
|
|
2546
|
+
const cacheKey = [codexAgentsDir(), projectCodexAgentsDir()].join("|");
|
|
2547
|
+
if (installedTypedAgentRoleNamesCacheKey === cacheKey)
|
|
2548
|
+
return installedTypedAgentRoleNamesCache;
|
|
2549
|
+
const installedRoleNames = new Set();
|
|
2550
|
+
for (const agentsDir of [codexAgentsDir(), projectCodexAgentsDir()]) {
|
|
2551
|
+
try {
|
|
2552
|
+
for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
|
|
2553
|
+
if (!entry.isFile() || !entry.name.endsWith(".toml"))
|
|
2554
|
+
continue;
|
|
2555
|
+
installedRoleNames.add(entry.name.slice(0, -5).trim().toLowerCase());
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
catch {
|
|
2559
|
+
// Ignore missing or unreadable agent directories; built-in definitions remain authoritative.
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
installedTypedAgentRoleNamesCacheKey = cacheKey;
|
|
2563
|
+
installedTypedAgentRoleNamesCache = installedRoleNames;
|
|
2564
|
+
return installedTypedAgentRoleNamesCache;
|
|
2565
|
+
}
|
|
2566
|
+
function isTypedAgentRolePayload(payload) {
|
|
2567
|
+
const agentRole = readPayloadAgentRole(payload);
|
|
2568
|
+
return agentRole !== "" && (KNOWN_TYPED_AGENT_ROLES.has(agentRole)
|
|
2569
|
+
|| readInstalledTypedAgentRoleNames().has(agentRole));
|
|
2570
|
+
}
|
|
2460
2571
|
function readPayloadTurnId(payload) {
|
|
2461
2572
|
return safeString(payload.turn_id ?? payload.turnId).trim();
|
|
2462
2573
|
}
|
|
@@ -2598,6 +2709,48 @@ async function resolveInternalSessionIdForPayload(cwd, payloadSessionId, stateDi
|
|
|
2598
2709
|
return canonicalSessionId;
|
|
2599
2710
|
return payloadSessionId;
|
|
2600
2711
|
}
|
|
2712
|
+
async function readRootSessionStateFromStateDir(stateDir) {
|
|
2713
|
+
const sessionPath = join(stateDir, "session.json");
|
|
2714
|
+
if (!existsSync(sessionPath))
|
|
2715
|
+
return null;
|
|
2716
|
+
try {
|
|
2717
|
+
const content = await readFile(sessionPath, "utf-8");
|
|
2718
|
+
return JSON.parse(content);
|
|
2719
|
+
}
|
|
2720
|
+
catch {
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
function payloadMatchesSessionPointer(payloadSessionId, state) {
|
|
2725
|
+
const canonicalSessionId = safeString(state.session_id).trim();
|
|
2726
|
+
const nativeSessionId = safeString(state.native_session_id).trim();
|
|
2727
|
+
const ownerOmxSessionId = safeString(state.owner_omx_session_id).trim();
|
|
2728
|
+
if (!payloadSessionId)
|
|
2729
|
+
return true;
|
|
2730
|
+
return payloadSessionId === canonicalSessionId
|
|
2731
|
+
|| (nativeSessionId !== "" && payloadSessionId === nativeSessionId)
|
|
2732
|
+
|| (ownerOmxSessionId !== "" && payloadSessionId === ownerOmxSessionId);
|
|
2733
|
+
}
|
|
2734
|
+
function isRootSessionPointerLive(state) {
|
|
2735
|
+
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
2736
|
+
if (!hasPidMetadata)
|
|
2737
|
+
return false;
|
|
2738
|
+
return !isSessionStale(state, {
|
|
2739
|
+
...(state.platform ? { platform: state.platform } : {}),
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
async function readLiveRootSessionPointerConflict(stateDir, payloadSessionId) {
|
|
2743
|
+
if (!payloadSessionId)
|
|
2744
|
+
return null;
|
|
2745
|
+
const rootState = await readRootSessionStateFromStateDir(stateDir);
|
|
2746
|
+
if (!rootState)
|
|
2747
|
+
return null;
|
|
2748
|
+
if (payloadMatchesSessionPointer(payloadSessionId, rootState))
|
|
2749
|
+
return null;
|
|
2750
|
+
if (!isRootSessionPointerLive(rootState))
|
|
2751
|
+
return null;
|
|
2752
|
+
return rootState;
|
|
2753
|
+
}
|
|
2601
2754
|
async function readUsableSessionStateFromStateDir(cwd, stateDir) {
|
|
2602
2755
|
const sessionPath = join(stateDir, "session.json");
|
|
2603
2756
|
if (!existsSync(sessionPath))
|
|
@@ -2621,15 +2774,23 @@ const DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES = [
|
|
|
2621
2774
|
".omx/context",
|
|
2622
2775
|
".omx/interviews",
|
|
2623
2776
|
".omx/specs",
|
|
2777
|
+
".omx/tmp",
|
|
2624
2778
|
".omx/state",
|
|
2625
2779
|
];
|
|
2626
2780
|
const RALPLAN_ALLOWED_WRITE_PREFIXES = [
|
|
2627
2781
|
".omx/context",
|
|
2628
2782
|
".omx/plans",
|
|
2629
2783
|
".omx/specs",
|
|
2784
|
+
".omx/tmp",
|
|
2630
2785
|
".omx/state",
|
|
2631
2786
|
".beads",
|
|
2632
2787
|
];
|
|
2788
|
+
const PROTECTED_PLANNING_STATE_FILE_NAMES = new Set([
|
|
2789
|
+
"autopilot-state.json",
|
|
2790
|
+
"deep-interview-state.json",
|
|
2791
|
+
"ralplan-state.json",
|
|
2792
|
+
"skill-active-state.json",
|
|
2793
|
+
]);
|
|
2633
2794
|
const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
|
|
2634
2795
|
"Write",
|
|
2635
2796
|
"Edit",
|
|
@@ -2684,20 +2845,48 @@ function hasExplicitExecutionHandoffSkill(state, sessionId, threadId) {
|
|
|
2684
2845
|
return listActiveSkills(state ?? {}).some((entry) => (RALPLAN_EXECUTION_HANDOFF_SKILLS.has(entry.skill)
|
|
2685
2846
|
&& matchesSkillStopContext(entry, state ?? {}, sessionId, threadId)));
|
|
2686
2847
|
}
|
|
2687
|
-
function
|
|
2848
|
+
function normalizePlanningArtifactRelativePath(cwd, rawPath) {
|
|
2688
2849
|
const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
|
|
2689
2850
|
if (!trimmed || trimmed.includes("\0"))
|
|
2690
|
-
return
|
|
2691
|
-
let relativePath;
|
|
2851
|
+
return null;
|
|
2692
2852
|
try {
|
|
2693
2853
|
const absolute = resolve(cwd, trimmed);
|
|
2694
|
-
relativePath = relative(cwd, absolute).replace(/\\/g, "/");
|
|
2854
|
+
const relativePath = relative(cwd, absolute).replace(/\\/g, "/");
|
|
2855
|
+
if (!relativePath || relativePath.startsWith("..") || relativePath.startsWith("/"))
|
|
2856
|
+
return null;
|
|
2857
|
+
return relativePath;
|
|
2695
2858
|
}
|
|
2696
2859
|
catch {
|
|
2697
|
-
return
|
|
2860
|
+
return null;
|
|
2698
2861
|
}
|
|
2699
|
-
|
|
2862
|
+
}
|
|
2863
|
+
function isProtectedPlanningStatePath(relativePath) {
|
|
2864
|
+
if (relativePath !== ".omx/state" && !relativePath.startsWith(".omx/state/"))
|
|
2700
2865
|
return false;
|
|
2866
|
+
const fileName = relativePath.split("/").pop() ?? "";
|
|
2867
|
+
return PROTECTED_PLANNING_STATE_FILE_NAMES.has(fileName);
|
|
2868
|
+
}
|
|
2869
|
+
function isPlanningTmpRelativePath(relativePath) {
|
|
2870
|
+
return relativePath === ".omx/tmp" || relativePath.startsWith(".omx/tmp/");
|
|
2871
|
+
}
|
|
2872
|
+
function isAllowedPlanningTmpScratchPath(relativePath) {
|
|
2873
|
+
if (!isPlanningTmpRelativePath(relativePath))
|
|
2874
|
+
return true;
|
|
2875
|
+
const fileName = relativePath.split("/").pop() ?? "";
|
|
2876
|
+
if (!fileName || fileName === "tmp")
|
|
2877
|
+
return true;
|
|
2878
|
+
const extension = extname(fileName).toLowerCase();
|
|
2879
|
+
return !PLANNING_TMP_SCRIPT_LIKE_EXTENSIONS.has(extension);
|
|
2880
|
+
}
|
|
2881
|
+
function isAllowedPlanningArtifactPath(cwd, rawPath, allowedPrefixes) {
|
|
2882
|
+
const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
|
|
2883
|
+
if (!relativePath)
|
|
2884
|
+
return false;
|
|
2885
|
+
if (isProtectedPlanningStatePath(relativePath))
|
|
2886
|
+
return false;
|
|
2887
|
+
if (isPlanningTmpRelativePath(relativePath)) {
|
|
2888
|
+
return allowedPrefixes.includes(".omx/tmp") && isAllowedPlanningTmpScratchPath(relativePath);
|
|
2889
|
+
}
|
|
2701
2890
|
return allowedPrefixes.some((prefix) => (relativePath === prefix || relativePath.startsWith(`${prefix}/`)));
|
|
2702
2891
|
}
|
|
2703
2892
|
function isAllowedDeepInterviewArtifactPath(cwd, rawPath) {
|
|
@@ -2914,29 +3103,174 @@ function resolveCommandRedirectTarget(target, assignments) {
|
|
|
2914
3103
|
}
|
|
2915
3104
|
function extractDeepInterviewCommandRedirectTargets(command) {
|
|
2916
3105
|
const targets = [];
|
|
2917
|
-
|
|
3106
|
+
const commandOutsideHeredocBodies = stripHeredocBodiesForCommandScan(command);
|
|
3107
|
+
for (const match of commandOutsideHeredocBodies.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
|
|
2918
3108
|
const candidate = safeString(match[2]).trim();
|
|
2919
3109
|
if (candidate && !isNullDeviceRedirectTarget(candidate))
|
|
2920
3110
|
targets.push(candidate);
|
|
2921
3111
|
}
|
|
2922
3112
|
return targets;
|
|
2923
3113
|
}
|
|
3114
|
+
function commandHasDestructiveGitSubcommand(command) {
|
|
3115
|
+
const destructiveSubcommands = new Set([
|
|
3116
|
+
"am",
|
|
3117
|
+
"apply",
|
|
3118
|
+
"checkout",
|
|
3119
|
+
"clean",
|
|
3120
|
+
"merge",
|
|
3121
|
+
"rebase",
|
|
3122
|
+
"reset",
|
|
3123
|
+
"restore",
|
|
3124
|
+
"rm",
|
|
3125
|
+
"switch",
|
|
3126
|
+
]);
|
|
3127
|
+
for (const segment of splitShellCommandSegments(stripHeredocBodiesForCommandScan(command))) {
|
|
3128
|
+
const words = tokenizeShellWords(segment);
|
|
3129
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
3130
|
+
if (shellWordBaseName(words[index] ?? "") !== "git")
|
|
3131
|
+
continue;
|
|
3132
|
+
const subcommandIndex = findGitSubcommandIndex(words, index + 1);
|
|
3133
|
+
if (subcommandIndex === null)
|
|
3134
|
+
continue;
|
|
3135
|
+
const subcommand = words[subcommandIndex] ?? "";
|
|
3136
|
+
if (destructiveSubcommands.has(subcommand))
|
|
3137
|
+
return true;
|
|
3138
|
+
if (subcommand === "worktree") {
|
|
3139
|
+
const worktreeSubcommandIndex = findGitSubcommandIndex(words, subcommandIndex + 1);
|
|
3140
|
+
if (worktreeSubcommandIndex === null)
|
|
3141
|
+
continue;
|
|
3142
|
+
const worktreeSubcommand = words[worktreeSubcommandIndex] ?? "";
|
|
3143
|
+
if (worktreeSubcommand === "add" || worktreeSubcommand === "move" || worktreeSubcommand === "mv" || worktreeSubcommand === "remove" || worktreeSubcommand === "rm" || worktreeSubcommand === "prune" || worktreeSubcommand === "repair" || worktreeSubcommand === "clean")
|
|
3144
|
+
return true;
|
|
3145
|
+
}
|
|
3146
|
+
if (subcommand.startsWith("checkout-"))
|
|
3147
|
+
return true;
|
|
3148
|
+
if (subcommand.startsWith("merge-") && subcommand !== "merge-base")
|
|
3149
|
+
return true;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return false;
|
|
3153
|
+
}
|
|
3154
|
+
function commandHasPackageInstallIntent(command) {
|
|
3155
|
+
for (const segment of splitShellCommandSegments(stripHeredocBodiesForCommandScan(command))) {
|
|
3156
|
+
const words = tokenizeShellWords(segment);
|
|
3157
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
3158
|
+
const headBase = shellWordBaseName(words[index] ?? "");
|
|
3159
|
+
if (headBase !== "npm" && headBase !== "pnpm" && headBase !== "yarn")
|
|
3160
|
+
continue;
|
|
3161
|
+
const subcommandIndex = findPackageInstallSubcommandIndex(words, index + 1);
|
|
3162
|
+
if (subcommandIndex === null)
|
|
3163
|
+
continue;
|
|
3164
|
+
const subcommand = words[subcommandIndex] ?? "";
|
|
3165
|
+
if (headBase === "npm" && (subcommand === "install" || subcommand === "i" || subcommand === "ci"))
|
|
3166
|
+
return true;
|
|
3167
|
+
if (headBase === "pnpm" && (subcommand === "install" || subcommand === "i" || subcommand === "add"))
|
|
3168
|
+
return true;
|
|
3169
|
+
if (headBase === "yarn" && (subcommand === "install" || subcommand === "add"))
|
|
3170
|
+
return true;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
return false;
|
|
3174
|
+
}
|
|
3175
|
+
function findPackageInstallSubcommandIndex(words, startIndex) {
|
|
3176
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
3177
|
+
const word = words[index] ?? "";
|
|
3178
|
+
if (!word || word === "--")
|
|
3179
|
+
continue;
|
|
3180
|
+
if (isShellAssignmentWord(word))
|
|
3181
|
+
continue;
|
|
3182
|
+
if (word === "-C" || word === "--prefix" || word === "--dir" || word === "--cwd" || word === "-w" || word === "--workspace") {
|
|
3183
|
+
index += 1;
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
if (word.startsWith("--prefix=") || word.startsWith("--dir=") || word.startsWith("--cwd=") || word.startsWith("--workspace="))
|
|
3187
|
+
continue;
|
|
3188
|
+
if (/^-C.+/.test(word) || /^-w.+/.test(word))
|
|
3189
|
+
continue;
|
|
3190
|
+
if (word.startsWith("-"))
|
|
3191
|
+
continue;
|
|
3192
|
+
return index;
|
|
3193
|
+
}
|
|
3194
|
+
return null;
|
|
3195
|
+
}
|
|
3196
|
+
function commandHasUntargetedPlanningForbiddenIntent(command) {
|
|
3197
|
+
return commandHasDestructiveGitSubcommand(command) || commandHasPackageInstallIntent(command);
|
|
3198
|
+
}
|
|
3199
|
+
function findGitSubcommandIndex(words, startIndex) {
|
|
3200
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
3201
|
+
const word = words[index] ?? "";
|
|
3202
|
+
if (!word || word === "--")
|
|
3203
|
+
continue;
|
|
3204
|
+
if (isShellAssignmentWord(word))
|
|
3205
|
+
continue;
|
|
3206
|
+
if (word === "-C" || word === "-c" || word === "--git-dir" || word === "--work-tree" || word === "--namespace") {
|
|
3207
|
+
index += 1;
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
3210
|
+
if (word.startsWith("--git-dir=") || word.startsWith("--work-tree=") || word.startsWith("--namespace="))
|
|
3211
|
+
continue;
|
|
3212
|
+
if (word.startsWith("-"))
|
|
3213
|
+
continue;
|
|
3214
|
+
return index;
|
|
3215
|
+
}
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
2924
3218
|
function commandHasDeepInterviewWriteIntent(command) {
|
|
2925
3219
|
return commandInvokesApplyPatch(command)
|
|
2926
3220
|
|| extractDeepInterviewCommandRedirectTargets(command).length > 0
|
|
2927
3221
|
|| /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
|
|
3222
|
+
|| extractConductorEditorWriteTargets(command).length > 0
|
|
2928
3223
|
|| /\bsed\s+(?:[^\n;&|]*\s)?-i(?:\b|['"])/.test(command)
|
|
3224
|
+
|| /\bperl\s+(?:[^\n;&|]*\s)?-[^-\s]*i(?:\b|['"])/.test(command)
|
|
2929
3225
|
|| /\b(?:python3?|node|perl|ruby)\b[\s\S]{0,260}\b(?:writeFileSync|writeFile|write_text|open\([^)]*["']w|File\.write|Path\()/.test(command)
|
|
2930
|
-
||
|
|
3226
|
+
|| extractConductorBashMutations(command).length > 0
|
|
3227
|
+
|| extractConductorInterpreterWrites(command).length > 0
|
|
3228
|
+
|| commandHasDestructiveGitSubcommand(command)
|
|
3229
|
+
|| commandHasPackageInstallIntent(command);
|
|
2931
3230
|
}
|
|
2932
3231
|
function extractDeepInterviewCommandWriteTargets(command) {
|
|
2933
3232
|
const assignments = extractCommandLiteralAssignments(command);
|
|
2934
3233
|
const targets = extractDeepInterviewCommandRedirectTargets(command)
|
|
2935
3234
|
.map((target) => resolveCommandRedirectTarget(target, assignments));
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
3235
|
+
targets.push(...extractConductorEditorWriteTargets(command));
|
|
3236
|
+
for (const mutation of extractConductorBashMutations(command)) {
|
|
3237
|
+
targets.push(...mutation.targets);
|
|
3238
|
+
}
|
|
3239
|
+
for (const write of extractConductorInterpreterWrites(command)) {
|
|
3240
|
+
targets.push(...write.targets);
|
|
3241
|
+
}
|
|
3242
|
+
for (const segment of splitShellCommandSegments(stripHeredocBodiesForCommandScan(command))) {
|
|
3243
|
+
const words = tokenizeShellWords(segment);
|
|
3244
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
3245
|
+
if (shellWordBaseName(words[index] ?? "") !== "tee")
|
|
3246
|
+
continue;
|
|
3247
|
+
let afterDoubleDash = false;
|
|
3248
|
+
for (let targetIndex = index + 1; targetIndex < words.length; targetIndex += 1) {
|
|
3249
|
+
const word = words[targetIndex] ?? "";
|
|
3250
|
+
if (!word || word === "|" || word === "|&" || word === "&&" || word === "||" || word === ";")
|
|
3251
|
+
break;
|
|
3252
|
+
if (!afterDoubleDash && word === "--") {
|
|
3253
|
+
afterDoubleDash = true;
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
if (!afterDoubleDash && word.startsWith("-"))
|
|
3257
|
+
continue;
|
|
3258
|
+
if (word === ">" || word === ">>" || word === ">&" || word === "<" || word === "<<" || word === "<<<") {
|
|
3259
|
+
targetIndex += 1;
|
|
3260
|
+
continue;
|
|
3261
|
+
}
|
|
3262
|
+
if (/^(?:>{1,2}|<)\S+/.test(word))
|
|
3263
|
+
continue;
|
|
3264
|
+
if (!isNullDeviceRedirectTarget(word))
|
|
3265
|
+
targets.push(resolveCommandRedirectTarget(word, assignments));
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
for (const nestedCommand of extractNestedShellCommandStringsForStateScan(command)) {
|
|
3270
|
+
targets.push(...extractDeepInterviewCommandWriteTargets(nestedCommand));
|
|
3271
|
+
}
|
|
3272
|
+
for (const nestedCommand of extractNestedCommandSubstitutionStringsForStateScan(command)) {
|
|
3273
|
+
targets.push(...extractDeepInterviewCommandWriteTargets(nestedCommand));
|
|
2940
3274
|
}
|
|
2941
3275
|
return targets;
|
|
2942
3276
|
}
|
|
@@ -2948,224 +3282,4057 @@ function isUnresolvedVariableTarget(target) {
|
|
|
2948
3282
|
const normalized = target.trim().replace(/^['"]|['"]$/g, "");
|
|
2949
3283
|
return /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(normalized);
|
|
2950
3284
|
}
|
|
2951
|
-
function
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
return `${operationClass} target <unresolved>; only planning artifact paths are allowed (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
2955
|
-
}
|
|
2956
|
-
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target" : `${toolName} path`;
|
|
2957
|
-
return formatPlanningWriteBlockDetail(operationClass, blockedPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
2958
|
-
}
|
|
2959
|
-
function isAllowedDeepInterviewBashWrite(cwd, command) {
|
|
2960
|
-
if (!commandHasDeepInterviewWriteIntent(command))
|
|
2961
|
-
return true;
|
|
2962
|
-
if (/\bomx\s+(?:state\s+(?:write|read|clear)|question)\b/.test(command))
|
|
2963
|
-
return true;
|
|
2964
|
-
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
2965
|
-
return targets.length > 0 && targets.every((target) => isAllowedDeepInterviewArtifactPath(cwd, target));
|
|
2966
|
-
}
|
|
2967
|
-
async function readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId) {
|
|
2968
|
-
const canonicalState = sessionId
|
|
2969
|
-
? await readVisibleSkillActiveStateForStateDir(stateDir, sessionId)
|
|
2970
|
-
: await readSkillActiveState(join(stateDir, SKILL_ACTIVE_STATE_FILE));
|
|
2971
|
-
if (!canonicalState)
|
|
3285
|
+
function normalizeSameCommandScriptTarget(cwd, rawPath, assignments) {
|
|
3286
|
+
const trimmed = resolveCommandRedirectTarget(rawPath.trim().replace(/^['"]|['"]$/g, ""), assignments);
|
|
3287
|
+
if (!trimmed || trimmed.includes("\0") || isUnresolvedVariableTarget(trimmed))
|
|
2972
3288
|
return null;
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
: await readJsonIfExists(join(stateDir, "deep-interview-state.json"));
|
|
2976
|
-
if (isActiveDeepInterviewPhase(modeState) && modeState && modeStateMatchesSkillStopContext(modeState, cwd, sessionId)) {
|
|
2977
|
-
const hasActiveDeepInterviewSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "deep-interview"
|
|
2978
|
-
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
2979
|
-
if (hasActiveDeepInterviewSkill)
|
|
2980
|
-
return modeState;
|
|
3289
|
+
try {
|
|
3290
|
+
return resolve(cwd, trimmed);
|
|
2981
3291
|
}
|
|
2982
|
-
|
|
2983
|
-
? await readStopSessionPinnedState("autopilot-state.json", cwd, sessionId, stateDir)
|
|
2984
|
-
: await readJsonIfExists(join(stateDir, "autopilot-state.json"));
|
|
2985
|
-
if (!autopilotState || autopilotState.active !== true)
|
|
2986
|
-
return null;
|
|
2987
|
-
const autopilotMode = safeString(autopilotState.mode).trim();
|
|
2988
|
-
if (autopilotMode && autopilotMode !== "autopilot")
|
|
2989
|
-
return null;
|
|
2990
|
-
if (!modeStateMatchesSkillStopContext(autopilotState, cwd, sessionId))
|
|
2991
|
-
return null;
|
|
2992
|
-
const terminalAutopilotRunState = await readCanonicalTerminalRunStateForStop(cwd, sessionId, "autopilot");
|
|
2993
|
-
if (terminalAutopilotRunState)
|
|
2994
|
-
return null;
|
|
2995
|
-
const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
|
|
2996
|
-
const autopilotIsDeepInterview = normalizeAutopilotPhase(autopilotStatePhase) === "deep-interview";
|
|
2997
|
-
const hasDeepInterviewScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "autopilot"
|
|
2998
|
-
&& normalizeAutopilotPhase(safeString(entry.phase).trim().toLowerCase()) === "deep-interview"
|
|
2999
|
-
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
3000
|
-
const hasActiveAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "autopilot"
|
|
3001
|
-
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
3002
|
-
if (!hasActiveAutopilotSkill)
|
|
3292
|
+
catch {
|
|
3003
3293
|
return null;
|
|
3004
|
-
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
function normalizeCommandDirectoryTarget(rawPath) {
|
|
3297
|
+
const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
|
|
3298
|
+
if (!trimmed
|
|
3299
|
+
|| trimmed.includes("\0")
|
|
3300
|
+
|| trimmed.startsWith("~")
|
|
3301
|
+
|| trimmed.startsWith("-")
|
|
3302
|
+
|| isUnresolvedVariableTarget(trimmed)
|
|
3303
|
+
|| /[`$]/.test(trimmed)) {
|
|
3005
3304
|
return null;
|
|
3006
|
-
|
|
3305
|
+
}
|
|
3306
|
+
return trimmed;
|
|
3007
3307
|
}
|
|
3008
|
-
|
|
3009
|
-
const
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
if (
|
|
3308
|
+
function resolveSimpleCdCommandCwd(currentCwd, words) {
|
|
3309
|
+
const commandIndex = findWrappedCommandPositionIndex(words, 0);
|
|
3310
|
+
if (commandIndex === null || shellWordBaseName(words[commandIndex] ?? "") !== "cd")
|
|
3311
|
+
return null;
|
|
3312
|
+
for (let index = commandIndex + 1; index < words.length; index += 1) {
|
|
3313
|
+
const word = words[index] ?? "";
|
|
3314
|
+
if (!word || word === "--")
|
|
3315
|
+
continue;
|
|
3316
|
+
if (word === "-L" || word === "-P" || word === "-e")
|
|
3317
|
+
continue;
|
|
3318
|
+
if (word.startsWith("-"))
|
|
3017
3319
|
return null;
|
|
3018
|
-
|
|
3320
|
+
const normalizedTarget = normalizeCommandDirectoryTarget(word);
|
|
3321
|
+
if (normalizedTarget === null)
|
|
3019
3322
|
return null;
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3323
|
+
try {
|
|
3324
|
+
return resolve(currentCwd, normalizedTarget);
|
|
3325
|
+
}
|
|
3326
|
+
catch {
|
|
3327
|
+
return null;
|
|
3328
|
+
}
|
|
3024
3329
|
}
|
|
3025
|
-
|
|
3026
|
-
? await readStopSessionPinnedState("autopilot-state.json", cwd, sessionId, stateDir)
|
|
3027
|
-
: await readJsonIfExists(join(stateDir, "autopilot-state.json"));
|
|
3028
|
-
if (!autopilotState || autopilotState.active !== true)
|
|
3029
|
-
return null;
|
|
3030
|
-
const autopilotMode = safeString(autopilotState.mode).trim();
|
|
3031
|
-
if (autopilotMode && autopilotMode !== "autopilot")
|
|
3032
|
-
return null;
|
|
3033
|
-
if (!modeStateMatchesSkillStopContext(autopilotState, cwd, sessionId))
|
|
3034
|
-
return null;
|
|
3035
|
-
const terminalAutopilotRunState = await readCanonicalTerminalRunStateForStop(cwd, sessionId, "autopilot");
|
|
3036
|
-
if (terminalAutopilotRunState)
|
|
3037
|
-
return null;
|
|
3038
|
-
if (!canonicalState)
|
|
3039
|
-
return null;
|
|
3040
|
-
const hasActiveAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "autopilot"
|
|
3041
|
-
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
3042
|
-
if (!hasActiveAutopilotSkill)
|
|
3043
|
-
return null;
|
|
3044
|
-
const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
|
|
3045
|
-
if (isAutopilotReviewReworkPhase(autopilotStatePhase))
|
|
3046
|
-
return null;
|
|
3047
|
-
if (!canAutopilotSkillMirrorSupplyRalplanPhase(autopilotStatePhase))
|
|
3048
|
-
return null;
|
|
3049
|
-
const hasRalplanScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "autopilot"
|
|
3050
|
-
&& isAutopilotRalplanLikePhase(safeString(entry.phase).trim().toLowerCase())
|
|
3051
|
-
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
3052
|
-
if (!isAutopilotRalplanLikePhase(autopilotStatePhase) && !hasRalplanScopedAutopilotSkill)
|
|
3053
|
-
return null;
|
|
3054
|
-
return hasActiveAutopilotSkill ? autopilotState : null;
|
|
3330
|
+
return null;
|
|
3055
3331
|
}
|
|
3056
|
-
function
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3332
|
+
function isEnvCwdChangingOption(word) {
|
|
3333
|
+
return word === "-C"
|
|
3334
|
+
|| word === "--chdir"
|
|
3335
|
+
|| word.startsWith("--chdir=")
|
|
3336
|
+
|| /^-C.+/.test(word);
|
|
3337
|
+
}
|
|
3338
|
+
function resolveEnvWrappedCommandCwd(currentCwd, words, envWordIndex, operandIndex) {
|
|
3339
|
+
let effectiveCwd = currentCwd;
|
|
3340
|
+
for (let index = envWordIndex + 1; index < operandIndex; index += 1) {
|
|
3341
|
+
const word = words[index] ?? "";
|
|
3342
|
+
if (!word || word === "--" || isShellAssignmentWord(word))
|
|
3343
|
+
continue;
|
|
3344
|
+
if (word === "-S" || word === "--split-string" || word.startsWith("-S") || word.startsWith("--split-string="))
|
|
3345
|
+
return null;
|
|
3346
|
+
if (word === "-u" || word === "--unset" || word === "-a" || word === "--argv0") {
|
|
3347
|
+
index += 1;
|
|
3348
|
+
continue;
|
|
3349
|
+
}
|
|
3350
|
+
if (word.startsWith("--unset=") || word.startsWith("--argv0=") || /^-u.+/.test(word) || /^-a.+/.test(word))
|
|
3351
|
+
continue;
|
|
3352
|
+
if (isEnvCwdChangingOption(word)) {
|
|
3353
|
+
let target = "";
|
|
3354
|
+
if (word === "-C" || word === "--chdir") {
|
|
3355
|
+
target = words[index + 1] ?? "";
|
|
3356
|
+
index += 1;
|
|
3357
|
+
}
|
|
3358
|
+
else if (word.startsWith("--chdir=")) {
|
|
3359
|
+
target = word.slice("--chdir=".length);
|
|
3360
|
+
}
|
|
3361
|
+
else if (word.startsWith("-C") && word.length > 2) {
|
|
3362
|
+
target = word.slice(2);
|
|
3363
|
+
}
|
|
3364
|
+
const normalizedTarget = normalizeCommandDirectoryTarget(target);
|
|
3365
|
+
if (normalizedTarget === null)
|
|
3366
|
+
return null;
|
|
3367
|
+
try {
|
|
3368
|
+
effectiveCwd = resolve(effectiveCwd, normalizedTarget);
|
|
3369
|
+
}
|
|
3370
|
+
catch {
|
|
3371
|
+
return null;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3063
3374
|
}
|
|
3064
|
-
|
|
3065
|
-
return true;
|
|
3066
|
-
if (/\bomx\s+(?:state\s+(?:write|read|clear)|question)\b/.test(command))
|
|
3067
|
-
return true;
|
|
3068
|
-
return hasAllowedTargets;
|
|
3375
|
+
return effectiveCwd;
|
|
3069
3376
|
}
|
|
3070
|
-
function
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3377
|
+
function resolveWrappedCommandExecutionContext(words, currentCwd, startIndex = 0) {
|
|
3378
|
+
let commandWordIndex = skipShellCommandPositionPrefixWords(words, startIndex);
|
|
3379
|
+
let effectiveCwd = currentCwd;
|
|
3380
|
+
for (let unwrapCount = 0; unwrapCount < 8; unwrapCount += 1) {
|
|
3381
|
+
const commandWord = words[commandWordIndex] ?? "";
|
|
3382
|
+
if (!commandWord)
|
|
3383
|
+
return null;
|
|
3384
|
+
const commandWordBase = shellWordBaseName(commandWord);
|
|
3385
|
+
const operandIndex = commandWordBase === "env"
|
|
3386
|
+
? findEnvDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3387
|
+
: commandWordBase === "command"
|
|
3388
|
+
? findCommandDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3389
|
+
: commandWordBase === "exec"
|
|
3390
|
+
? findExecDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3391
|
+
: commandWordBase === "time"
|
|
3392
|
+
? findTimeDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3393
|
+
: commandWordBase === "timeout"
|
|
3394
|
+
? findTimeoutDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3395
|
+
: commandWordBase === "nohup" || commandWordBase === "setsid"
|
|
3396
|
+
? findCommandDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3397
|
+
: commandWordBase === "coproc"
|
|
3398
|
+
? findCoprocDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3399
|
+
: commandWordBase === "xargs"
|
|
3400
|
+
? findXargsDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3401
|
+
: commandWordBase === "nice"
|
|
3402
|
+
? findNiceDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3403
|
+
: commandWordBase === "stdbuf"
|
|
3404
|
+
? findStdbufDispatchOperandIndex(words, commandWordIndex + 1)
|
|
3405
|
+
: null;
|
|
3406
|
+
if (operandIndex === null)
|
|
3407
|
+
return { index: commandWordIndex, cwd: effectiveCwd };
|
|
3408
|
+
if (commandWordBase === "env") {
|
|
3409
|
+
const envCwd = resolveEnvWrappedCommandCwd(effectiveCwd, words, commandWordIndex, operandIndex);
|
|
3410
|
+
if (envCwd === null)
|
|
3411
|
+
return null;
|
|
3412
|
+
effectiveCwd = envCwd;
|
|
3413
|
+
}
|
|
3414
|
+
const nextCommandWordIndex = skipShellCommandPositionPrefixWords(words, operandIndex);
|
|
3415
|
+
if (nextCommandWordIndex === commandWordIndex)
|
|
3416
|
+
return { index: commandWordIndex, cwd: effectiveCwd };
|
|
3417
|
+
commandWordIndex = nextCommandWordIndex;
|
|
3418
|
+
}
|
|
3419
|
+
return null;
|
|
3420
|
+
}
|
|
3421
|
+
function resolveStateWriteInputFileCwd(cwd, commandPrefix) {
|
|
3422
|
+
const words = tokenizeShellWords(stripHeredocBodiesForCommandScan(commandPrefix));
|
|
3423
|
+
let effectiveCwd = cwd;
|
|
3424
|
+
let activeWrapper = null;
|
|
3425
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
3426
|
+
const word = words[index] ?? "";
|
|
3427
|
+
if (!word || isShellAssignmentWord(word))
|
|
3428
|
+
continue;
|
|
3429
|
+
const wordBase = shellWordBaseName(word);
|
|
3430
|
+
if (wordBase === "env" || wordBase === "npm" || wordBase === "pnpm" || wordBase === "yarn") {
|
|
3431
|
+
activeWrapper = wordBase;
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3434
|
+
if (!activeWrapper)
|
|
3435
|
+
continue;
|
|
3436
|
+
const isEnvChdirOption = activeWrapper === "env" && isEnvCwdChangingOption(word);
|
|
3437
|
+
const isPackageManagerCwdOption = activeWrapper !== "env"
|
|
3438
|
+
&& (word === "-C"
|
|
3439
|
+
|| word === "--prefix"
|
|
3440
|
+
|| word === "--dir"
|
|
3441
|
+
|| word === "--cwd"
|
|
3442
|
+
|| word.startsWith("--prefix=")
|
|
3443
|
+
|| word.startsWith("--dir=")
|
|
3444
|
+
|| word.startsWith("--cwd=")
|
|
3445
|
+
|| /^-C.+/.test(word));
|
|
3446
|
+
if (isEnvChdirOption || isPackageManagerCwdOption) {
|
|
3447
|
+
let target = "";
|
|
3448
|
+
if (word === "-C" || word === "--chdir" || word === "--prefix" || word === "--dir" || word === "--cwd") {
|
|
3449
|
+
target = words[index + 1] ?? "";
|
|
3450
|
+
}
|
|
3451
|
+
else if (word.startsWith("--chdir=")) {
|
|
3452
|
+
target = word.slice("--chdir=".length);
|
|
3453
|
+
}
|
|
3454
|
+
else if (word.startsWith("--prefix=")) {
|
|
3455
|
+
target = word.slice("--prefix=".length);
|
|
3456
|
+
}
|
|
3457
|
+
else if (word.startsWith("--dir=")) {
|
|
3458
|
+
target = word.slice("--dir=".length);
|
|
3459
|
+
}
|
|
3460
|
+
else if (word.startsWith("--cwd=")) {
|
|
3461
|
+
target = word.slice("--cwd=".length);
|
|
3462
|
+
}
|
|
3463
|
+
else if (word.startsWith("-C") && word.length > 2) {
|
|
3464
|
+
target = word.slice(2);
|
|
3465
|
+
}
|
|
3466
|
+
const normalizedTarget = normalizeCommandDirectoryTarget(target);
|
|
3467
|
+
if (normalizedTarget === null)
|
|
3468
|
+
return null;
|
|
3469
|
+
effectiveCwd = resolve(effectiveCwd, normalizedTarget);
|
|
3470
|
+
if (word === "-C" || word === "--chdir" || word === "--prefix" || word === "--dir" || word === "--cwd") {
|
|
3471
|
+
index += 1;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
return effectiveCwd;
|
|
3476
|
+
}
|
|
3477
|
+
function findShellFunctionDefinitionAt(command, index) {
|
|
3478
|
+
if (index > 0) {
|
|
3479
|
+
let previous = index - 1;
|
|
3480
|
+
while (previous >= 0 && /\s/.test(command[previous] ?? ""))
|
|
3481
|
+
previous -= 1;
|
|
3482
|
+
if (previous >= 0 && !/[;&|(){}]/.test(command[previous] ?? ""))
|
|
3483
|
+
return null;
|
|
3484
|
+
}
|
|
3485
|
+
let cursor = index;
|
|
3486
|
+
while (/\s/.test(command[cursor] ?? ""))
|
|
3487
|
+
cursor += 1;
|
|
3488
|
+
const candidate = command.slice(cursor);
|
|
3489
|
+
const functionKeywordMatch = candidate.match(/^function\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(\s*\))?\s*([\{(])/);
|
|
3490
|
+
if (functionKeywordMatch) {
|
|
3491
|
+
return {
|
|
3492
|
+
name: functionKeywordMatch[1],
|
|
3493
|
+
openBraceIndex: cursor + functionKeywordMatch[0].length - 1,
|
|
3494
|
+
bodyOpenChar: functionKeywordMatch[2] === "(" ? "(" : "{",
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
const bareFunctionMatch = candidate.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*([\{(])/);
|
|
3498
|
+
if (bareFunctionMatch) {
|
|
3499
|
+
return {
|
|
3500
|
+
name: bareFunctionMatch[1],
|
|
3501
|
+
openBraceIndex: cursor + bareFunctionMatch[0].length - 1,
|
|
3502
|
+
bodyOpenChar: bareFunctionMatch[2] === "(" ? "(" : "{",
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
return null;
|
|
3506
|
+
}
|
|
3507
|
+
function findShellFunctionBodyEnd(command, openBraceIndex, bodyOpenChar) {
|
|
3508
|
+
let depth = 1;
|
|
3509
|
+
let quote = null;
|
|
3510
|
+
for (let index = openBraceIndex + 1; index < command.length; index += 1) {
|
|
3511
|
+
const char = command[index] ?? "";
|
|
3512
|
+
if (char === "\\" && quote !== "'") {
|
|
3513
|
+
index += 1;
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
if (char === "'" || char === "\"") {
|
|
3517
|
+
if (quote === char) {
|
|
3518
|
+
quote = null;
|
|
3519
|
+
}
|
|
3520
|
+
else if (!quote) {
|
|
3521
|
+
quote = char;
|
|
3522
|
+
}
|
|
3523
|
+
continue;
|
|
3524
|
+
}
|
|
3525
|
+
if (quote)
|
|
3526
|
+
continue;
|
|
3527
|
+
if (bodyOpenChar === "{" && char === "{") {
|
|
3528
|
+
depth += 1;
|
|
3529
|
+
continue;
|
|
3530
|
+
}
|
|
3531
|
+
if (bodyOpenChar === "{" && char === "}") {
|
|
3532
|
+
depth -= 1;
|
|
3533
|
+
if (depth === 0)
|
|
3534
|
+
return index;
|
|
3535
|
+
}
|
|
3536
|
+
if (bodyOpenChar === "(" && char === "(") {
|
|
3537
|
+
depth += 1;
|
|
3538
|
+
continue;
|
|
3539
|
+
}
|
|
3540
|
+
if (bodyOpenChar === "(" && char === ")") {
|
|
3541
|
+
depth -= 1;
|
|
3542
|
+
if (depth === 0)
|
|
3543
|
+
return index;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return -1;
|
|
3547
|
+
}
|
|
3548
|
+
function isShellFunctionInvokedLater(command, functionName) {
|
|
3549
|
+
for (const segment of splitShellCommandSegments(command)) {
|
|
3550
|
+
const words = tokenizeShellWords(segment);
|
|
3551
|
+
if (isShellFunctionInvokedFromWords(words, 0, functionName))
|
|
3552
|
+
return true;
|
|
3553
|
+
}
|
|
3554
|
+
return false;
|
|
3555
|
+
}
|
|
3556
|
+
function isShellFunctionInvokedFromWords(words, startIndex, functionName) {
|
|
3557
|
+
const index = skipShellCommandPositionPrefixWords(words, startIndex);
|
|
3558
|
+
const head = words[index] ?? "";
|
|
3559
|
+
if (head === functionName)
|
|
3560
|
+
return true;
|
|
3561
|
+
if (shellWordBaseName(head) === "time") {
|
|
3562
|
+
const timeOperandIndex = findTimeDispatchOperandIndex(words, index + 1);
|
|
3563
|
+
if (timeOperandIndex !== null) {
|
|
3564
|
+
const timeCommandIndex = skipShellCommandPositionPrefixWords(words, timeOperandIndex);
|
|
3565
|
+
if ((words[timeCommandIndex] ?? "") === functionName)
|
|
3566
|
+
return true;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
if (shellWordBaseName(head) === "coproc") {
|
|
3570
|
+
const coprocOperandIndex = findCoprocDispatchOperandIndex(words, index + 1);
|
|
3571
|
+
if (coprocOperandIndex !== null) {
|
|
3572
|
+
return isShellFunctionInvokedFromWords(words, coprocOperandIndex, functionName);
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
if (shellWordBaseName(head) === "setsid") {
|
|
3576
|
+
const setsidOperandIndex = findCommandDispatchOperandIndex(words, index + 1);
|
|
3577
|
+
if (setsidOperandIndex !== null) {
|
|
3578
|
+
return isShellFunctionInvokedFromWords(words, setsidOperandIndex, functionName);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
if (shellWordBaseName(head) === "command") {
|
|
3582
|
+
const commandOperandIndex = findCommandDispatchOperandIndex(words, index + 1);
|
|
3583
|
+
if (commandOperandIndex !== null && shellWordBaseName(words[commandOperandIndex] ?? "") === "time") {
|
|
3584
|
+
return isShellFunctionInvokedFromWords(words, commandOperandIndex, functionName);
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
return false;
|
|
3588
|
+
}
|
|
3589
|
+
function firstNonOptionSourceOperand(words, sourceWordIndex) {
|
|
3590
|
+
for (let index = sourceWordIndex + 1; index < words.length; index += 1) {
|
|
3591
|
+
const word = words[index] ?? "";
|
|
3592
|
+
if (!word || word === "--")
|
|
3593
|
+
continue;
|
|
3594
|
+
if (word.startsWith("-"))
|
|
3595
|
+
continue;
|
|
3596
|
+
return word;
|
|
3597
|
+
}
|
|
3598
|
+
return "";
|
|
3599
|
+
}
|
|
3600
|
+
function stdinRedirectOperands(words) {
|
|
3601
|
+
const operands = [];
|
|
3602
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
3603
|
+
const word = words[index] ?? "";
|
|
3604
|
+
if (word === "<") {
|
|
3605
|
+
const operand = words[index + 1] ?? "";
|
|
3606
|
+
if (operand)
|
|
3607
|
+
operands.push(operand);
|
|
3608
|
+
index += 1;
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
if (/^\d*<[^<&].+/.test(word)) {
|
|
3612
|
+
operands.push(word.replace(/^\d*</, ""));
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
return operands;
|
|
3616
|
+
}
|
|
3617
|
+
function firstShellScriptOperands(words, shellWordIndex) {
|
|
3618
|
+
const operands = [];
|
|
3619
|
+
for (let index = shellWordIndex + 1; index < words.length; index += 1) {
|
|
3620
|
+
const word = words[index] ?? "";
|
|
3621
|
+
if (!word || word === "--")
|
|
3622
|
+
continue;
|
|
3623
|
+
if (isShellCommandStringOption(word))
|
|
3624
|
+
return operands;
|
|
3625
|
+
if (isShellOptionWithSeparateValue(word)) {
|
|
3626
|
+
const value = words[index + 1] ?? "";
|
|
3627
|
+
if (value)
|
|
3628
|
+
operands.push(value);
|
|
3629
|
+
index += 1;
|
|
3630
|
+
continue;
|
|
3631
|
+
}
|
|
3632
|
+
if (word.startsWith("--init-file=") || word.startsWith("--rcfile=")) {
|
|
3633
|
+
operands.push(word.slice(word.indexOf("=") + 1));
|
|
3634
|
+
continue;
|
|
3635
|
+
}
|
|
3636
|
+
if (word.startsWith("-"))
|
|
3637
|
+
continue;
|
|
3638
|
+
operands.push(word);
|
|
3639
|
+
return operands;
|
|
3640
|
+
}
|
|
3641
|
+
return operands;
|
|
3642
|
+
}
|
|
3643
|
+
function firstShellScriptOperand(words, shellWordIndex) {
|
|
3644
|
+
return firstShellScriptOperands(words, shellWordIndex)[0] ?? "";
|
|
3645
|
+
}
|
|
3646
|
+
function isPythonInterpreterCommandWord(base) {
|
|
3647
|
+
return /^python(?:[0-9]+(?:\.[0-9]+)*)?$/.test(base);
|
|
3648
|
+
}
|
|
3649
|
+
function isNodeInterpreterCommandWord(base) {
|
|
3650
|
+
return /^(?:node|nodejs)$/.test(base);
|
|
3651
|
+
}
|
|
3652
|
+
function isPythonRuntimeOptionWithSeparateValue(word) {
|
|
3653
|
+
return word === "-B"
|
|
3654
|
+
|| word === "-b"
|
|
3655
|
+
|| word === "-d"
|
|
3656
|
+
|| word === "-E"
|
|
3657
|
+
|| word === "-i"
|
|
3658
|
+
|| word === "-I"
|
|
3659
|
+
|| word === "-O"
|
|
3660
|
+
|| word === "-OO"
|
|
3661
|
+
|| word === "-P"
|
|
3662
|
+
|| word === "-q"
|
|
3663
|
+
|| word === "-R"
|
|
3664
|
+
|| word === "-s"
|
|
3665
|
+
|| word === "-S"
|
|
3666
|
+
|| word === "-u"
|
|
3667
|
+
|| word === "-v"
|
|
3668
|
+
|| word === "-V"
|
|
3669
|
+
|| word === "--check-hash-based-pycs"
|
|
3670
|
+
|| word === "-W"
|
|
3671
|
+
|| word === "-X";
|
|
3672
|
+
}
|
|
3673
|
+
function isScriptInterpreterCommandWord(word) {
|
|
3674
|
+
const base = shellWordBaseName(word);
|
|
3675
|
+
return isNestedShellCommandWord(base)
|
|
3676
|
+
|| isNodeInterpreterCommandWord(base)
|
|
3677
|
+
|| base === "bun"
|
|
3678
|
+
|| base === "tsx"
|
|
3679
|
+
|| base === "deno"
|
|
3680
|
+
|| isPythonInterpreterCommandWord(base)
|
|
3681
|
+
|| base === "perl"
|
|
3682
|
+
|| base === "ruby"
|
|
3683
|
+
|| base === "php"
|
|
3684
|
+
|| base === "lua"
|
|
3685
|
+
|| base === "go"
|
|
3686
|
+
|| base === "npx"
|
|
3687
|
+
|| base === "npm"
|
|
3688
|
+
|| base === "pnpm"
|
|
3689
|
+
|| base === "yarn";
|
|
3690
|
+
}
|
|
3691
|
+
function isTsxRuntimeOptionWithSeparateValue(word) {
|
|
3692
|
+
return word === "--tsconfig";
|
|
3693
|
+
}
|
|
3694
|
+
function isTsxRuntimeModeWord(word) {
|
|
3695
|
+
return word === "watch";
|
|
3696
|
+
}
|
|
3697
|
+
function firstInterpreterScriptOperands(words, interpreterWordIndex) {
|
|
3698
|
+
const base = shellWordBaseName(words[interpreterWordIndex] ?? "");
|
|
3699
|
+
if (isNestedShellCommandWord(base))
|
|
3700
|
+
return firstShellScriptOperands(words, interpreterWordIndex);
|
|
3701
|
+
const operands = [];
|
|
3702
|
+
let sawGoRun = false;
|
|
3703
|
+
let sawPackageRunner = base === "npx";
|
|
3704
|
+
for (let index = interpreterWordIndex + 1; index < words.length; index += 1) {
|
|
3705
|
+
const word = words[index] ?? "";
|
|
3706
|
+
if (!word || word === "--")
|
|
3707
|
+
continue;
|
|
3708
|
+
if (isPythonInterpreterCommandWord(base)) {
|
|
3709
|
+
if (word === "-c" || word === "-m")
|
|
3710
|
+
return operands;
|
|
3711
|
+
if (isPythonRuntimeOptionWithSeparateValue(word)) {
|
|
3712
|
+
index += 1;
|
|
3713
|
+
continue;
|
|
3714
|
+
}
|
|
3715
|
+
if (word.startsWith("-W") || word.startsWith("-X"))
|
|
3716
|
+
continue;
|
|
3717
|
+
if (word.startsWith("-"))
|
|
3718
|
+
continue;
|
|
3719
|
+
operands.push(word);
|
|
3720
|
+
return operands;
|
|
3721
|
+
}
|
|
3722
|
+
if (isNodeInterpreterCommandWord(base) || base === "bun" || base === "tsx") {
|
|
3723
|
+
if (word === "-e" || word === "--eval" || word === "-p" || word === "--print")
|
|
3724
|
+
return operands;
|
|
3725
|
+
if (runtimeOptionConsumesNextWord(word) || (base === "tsx" && isTsxRuntimeOptionWithSeparateValue(word))) {
|
|
3726
|
+
const value = words[index + 1] ?? "";
|
|
3727
|
+
if (value && (word === "-r" || word === "--require" || word === "--import" || word === "--loader" || word === "--experimental-loader")) {
|
|
3728
|
+
operands.push(value);
|
|
3729
|
+
}
|
|
3730
|
+
index += 1;
|
|
3731
|
+
continue;
|
|
3732
|
+
}
|
|
3733
|
+
if (word.startsWith("--require=") || word.startsWith("--import=") || word.startsWith("--loader=") || word.startsWith("--experimental-loader=")) {
|
|
3734
|
+
operands.push(word.slice(word.indexOf("=") + 1));
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3737
|
+
if (word.startsWith("--eval=") || word.startsWith("--print="))
|
|
3738
|
+
return operands;
|
|
3739
|
+
if (base === "tsx" && isTsxRuntimeModeWord(word))
|
|
3740
|
+
continue;
|
|
3741
|
+
if (word.startsWith("-"))
|
|
3742
|
+
continue;
|
|
3743
|
+
operands.push(word);
|
|
3744
|
+
return operands;
|
|
3745
|
+
}
|
|
3746
|
+
if (base === "deno") {
|
|
3747
|
+
if (word === "eval" || word === "repl")
|
|
3748
|
+
return operands;
|
|
3749
|
+
if (word === "run")
|
|
3750
|
+
continue;
|
|
3751
|
+
if (word.startsWith("-"))
|
|
3752
|
+
continue;
|
|
3753
|
+
operands.push(word);
|
|
3754
|
+
return operands;
|
|
3755
|
+
}
|
|
3756
|
+
if (base === "go") {
|
|
3757
|
+
if (!sawGoRun) {
|
|
3758
|
+
if (word === "run") {
|
|
3759
|
+
sawGoRun = true;
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
return operands;
|
|
3763
|
+
}
|
|
3764
|
+
if (word.startsWith("-"))
|
|
3765
|
+
continue;
|
|
3766
|
+
operands.push(word);
|
|
3767
|
+
return operands;
|
|
3768
|
+
}
|
|
3769
|
+
if (base === "npm" || base === "pnpm" || base === "yarn" || base === "npx") {
|
|
3770
|
+
if (!sawPackageRunner) {
|
|
3771
|
+
if (word === "exec" || word === "x" || word === "dlx") {
|
|
3772
|
+
sawPackageRunner = true;
|
|
3773
|
+
continue;
|
|
3774
|
+
}
|
|
3775
|
+
return operands;
|
|
3776
|
+
}
|
|
3777
|
+
if (word.startsWith("-"))
|
|
3778
|
+
continue;
|
|
3779
|
+
if (word === "tsx" || word === "node" || word === "bun" || word === "deno")
|
|
3780
|
+
continue;
|
|
3781
|
+
operands.push(word);
|
|
3782
|
+
return operands;
|
|
3783
|
+
}
|
|
3784
|
+
if (base === "perl" || base === "ruby" || base === "php" || base === "lua") {
|
|
3785
|
+
if (word === "-e" || word.startsWith("-e"))
|
|
3786
|
+
return operands;
|
|
3787
|
+
if (word.startsWith("-"))
|
|
3788
|
+
continue;
|
|
3789
|
+
operands.push(word);
|
|
3790
|
+
return operands;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
return operands;
|
|
3794
|
+
}
|
|
3795
|
+
function firstPlanningTmpScriptExecutionTarget(cwd, command) {
|
|
3796
|
+
const scanCommand = (currentCwd, currentCommand, activeCommands) => {
|
|
3797
|
+
const normalizedCommand = stripHeredocBodiesForCommandScan(normalizeShellLineContinuations(currentCommand));
|
|
3798
|
+
const commandKey = `${currentCwd}\0${normalizedCommand.trim()}`;
|
|
3799
|
+
if (!normalizedCommand.trim() || activeCommands.has(commandKey))
|
|
3800
|
+
return null;
|
|
3801
|
+
const assignments = extractCommandLiteralAssignments(normalizedCommand);
|
|
3802
|
+
const nextActiveCommands = new Set(activeCommands);
|
|
3803
|
+
nextActiveCommands.add(commandKey);
|
|
3804
|
+
let effectiveCwd = currentCwd;
|
|
3805
|
+
const normalizeExecutionTarget = (operandCwd, rawPath) => {
|
|
3806
|
+
const absoluteTarget = normalizeSameCommandScriptTarget(operandCwd, rawPath, assignments);
|
|
3807
|
+
if (!absoluteTarget)
|
|
3808
|
+
return null;
|
|
3809
|
+
const relativePath = relative(cwd, absoluteTarget).replace(/\\/g, "/");
|
|
3810
|
+
if (!relativePath || relativePath.startsWith("..") || relativePath.startsWith("/"))
|
|
3811
|
+
return null;
|
|
3812
|
+
return relativePath;
|
|
3813
|
+
};
|
|
3814
|
+
for (const segment of splitShellCommandSegments(normalizedCommand)) {
|
|
3815
|
+
const words = tokenizeShellWords(segment);
|
|
3816
|
+
const cdCwd = resolveSimpleCdCommandCwd(effectiveCwd, words);
|
|
3817
|
+
if (cdCwd !== null) {
|
|
3818
|
+
effectiveCwd = cdCwd;
|
|
3819
|
+
continue;
|
|
3820
|
+
}
|
|
3821
|
+
const wrappedCommandContext = resolveWrappedCommandExecutionContext(words, effectiveCwd);
|
|
3822
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
3823
|
+
const word = words[index] ?? "";
|
|
3824
|
+
const operandCwd = wrappedCommandContext && index >= wrappedCommandContext.index ? wrappedCommandContext.cwd : effectiveCwd;
|
|
3825
|
+
const operands = word === "source" || word === "."
|
|
3826
|
+
? [firstNonOptionSourceOperand(words, index)].filter(Boolean)
|
|
3827
|
+
: isScriptInterpreterCommandWord(word)
|
|
3828
|
+
? [...firstInterpreterScriptOperands(words, index), ...stdinRedirectOperands(words)]
|
|
3829
|
+
: [];
|
|
3830
|
+
for (const operand of operands) {
|
|
3831
|
+
const relativePath = normalizeExecutionTarget(operandCwd, operand);
|
|
3832
|
+
if (relativePath && isPlanningTmpRelativePath(relativePath))
|
|
3833
|
+
return relativePath;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
if (wrappedCommandContext !== null) {
|
|
3837
|
+
const directTarget = words[wrappedCommandContext.index] ?? "";
|
|
3838
|
+
const relativePath = normalizeExecutionTarget(wrappedCommandContext.cwd, directTarget);
|
|
3839
|
+
if (relativePath && isPlanningTmpRelativePath(relativePath))
|
|
3840
|
+
return relativePath;
|
|
3841
|
+
}
|
|
3842
|
+
for (const nestedCommand of extractNestedShellCommandStringsForStateScan(segment)) {
|
|
3843
|
+
const nestedTarget = scanCommand(effectiveCwd, nestedCommand, nextActiveCommands);
|
|
3844
|
+
if (nestedTarget)
|
|
3845
|
+
return nestedTarget;
|
|
3846
|
+
}
|
|
3847
|
+
for (const nestedCommand of extractNestedCommandSubstitutionStringsForStateScan(segment)) {
|
|
3848
|
+
const nestedTarget = scanCommand(effectiveCwd, nestedCommand, nextActiveCommands);
|
|
3849
|
+
if (nestedTarget)
|
|
3850
|
+
return nestedTarget;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
return null;
|
|
3854
|
+
};
|
|
3855
|
+
return scanCommand(cwd, command, new Set());
|
|
3856
|
+
}
|
|
3857
|
+
function sourcesFileWrittenEarlierInSameCommand(cwd, command) {
|
|
3858
|
+
const scanCommand = (currentCwd, currentCommand, activeCommands, writtenTargets) => {
|
|
3859
|
+
const normalizedCommand = stripHeredocBodiesForCommandScan(normalizeShellLineContinuations(currentCommand));
|
|
3860
|
+
const commandKey = `${currentCwd}\0${normalizedCommand.trim()}`;
|
|
3861
|
+
if (!normalizedCommand.trim() || activeCommands.has(commandKey))
|
|
3862
|
+
return false;
|
|
3863
|
+
const assignments = extractCommandLiteralAssignments(normalizedCommand);
|
|
3864
|
+
const nextActiveCommands = new Set(activeCommands);
|
|
3865
|
+
nextActiveCommands.add(commandKey);
|
|
3866
|
+
for (const write of extractConductorInterpreterWrites(currentCommand)) {
|
|
3867
|
+
for (const target of write.targets) {
|
|
3868
|
+
const normalizedTarget = normalizeSameCommandScriptTarget(currentCwd, target, assignments);
|
|
3869
|
+
if (normalizedTarget)
|
|
3870
|
+
writtenTargets.add(normalizedTarget);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
let effectiveCwd = currentCwd;
|
|
3874
|
+
for (const segment of splitShellCommandSegments(normalizedCommand)) {
|
|
3875
|
+
const words = tokenizeShellWords(segment);
|
|
3876
|
+
const cdCwd = resolveSimpleCdCommandCwd(effectiveCwd, words);
|
|
3877
|
+
if (cdCwd !== null) {
|
|
3878
|
+
effectiveCwd = cdCwd;
|
|
3879
|
+
continue;
|
|
3880
|
+
}
|
|
3881
|
+
const wrappedCommandContext = resolveWrappedCommandExecutionContext(words, effectiveCwd);
|
|
3882
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
3883
|
+
const word = words[index] ?? "";
|
|
3884
|
+
const operandCwd = wrappedCommandContext && index >= wrappedCommandContext.index ? wrappedCommandContext.cwd : effectiveCwd;
|
|
3885
|
+
const operand = word === "source" || word === "."
|
|
3886
|
+
? normalizeSameCommandScriptTarget(operandCwd, firstNonOptionSourceOperand(words, index), assignments)
|
|
3887
|
+
: isNestedShellCommandWord(word)
|
|
3888
|
+
? normalizeSameCommandScriptTarget(operandCwd, firstShellScriptOperand(words, index), assignments)
|
|
3889
|
+
: null;
|
|
3890
|
+
if (operand && writtenTargets.has(operand))
|
|
3891
|
+
return true;
|
|
3892
|
+
}
|
|
3893
|
+
const directExecutionTarget = wrappedCommandContext === null
|
|
3894
|
+
? null
|
|
3895
|
+
: normalizeSameCommandScriptTarget(wrappedCommandContext.cwd, words[wrappedCommandContext.index] ?? "", assignments);
|
|
3896
|
+
if (directExecutionTarget && writtenTargets.has(directExecutionTarget))
|
|
3897
|
+
return true;
|
|
3898
|
+
for (const nestedCommand of extractNestedShellCommandStringsForStateScan(segment)) {
|
|
3899
|
+
if (scanCommand(effectiveCwd, nestedCommand, nextActiveCommands, writtenTargets))
|
|
3900
|
+
return true;
|
|
3901
|
+
}
|
|
3902
|
+
for (const target of extractDeepInterviewCommandWriteTargets(segment)) {
|
|
3903
|
+
const normalizedTarget = normalizeSameCommandScriptTarget(effectiveCwd, target, assignments);
|
|
3904
|
+
if (normalizedTarget)
|
|
3905
|
+
writtenTargets.add(normalizedTarget);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
return false;
|
|
3909
|
+
};
|
|
3910
|
+
return scanCommand(cwd, command, new Set(), new Set());
|
|
3911
|
+
}
|
|
3912
|
+
function describeImplementationToolBlock(toolName, blockedPath, pathCount) {
|
|
3913
|
+
if (pathCount === 0) {
|
|
3914
|
+
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target extraction failed" : `${toolName} path`;
|
|
3915
|
+
return `${operationClass} target <unresolved>; only planning artifact paths are allowed (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
3916
|
+
}
|
|
3917
|
+
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target" : `${toolName} path`;
|
|
3918
|
+
return formatPlanningWriteBlockDetail(operationClass, blockedPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
3919
|
+
}
|
|
3920
|
+
// `omx state` mutations normally route through the gate-enforcing `state_write`
|
|
3921
|
+
// backend, so the hook defers to that gate rather than blocking the transport.
|
|
3922
|
+
// The backend does NOT gate generic standalone deep-interview/ralplan
|
|
3923
|
+
// *deactivation*, and it normalizes non-terminal tracked-workflow writes to
|
|
3924
|
+
// `active=true`, so commands that would implicitly activate a tracked workflow
|
|
3925
|
+
// while planning is still protected are blocked here. Ralplan terminal closeout
|
|
3926
|
+
// is the narrow exception: the backend has a dedicated completeRalplanSession
|
|
3927
|
+
// path that coherently terminalizes root and session state when the payload is a
|
|
3928
|
+
// complete consensus-approved terminal state.
|
|
3929
|
+
function readStateWriteInputPayload(cwd, command, sourceCommand = command) {
|
|
3930
|
+
const stateWriteOperations = collectOmxStateCommandOperations(command, "write");
|
|
3931
|
+
if (stateWriteOperations.length === 0)
|
|
3932
|
+
return null;
|
|
3933
|
+
if (stateWriteOperations.length > 1)
|
|
3934
|
+
return null;
|
|
3935
|
+
const stateWriteOperation = stateWriteOperations[0];
|
|
3936
|
+
if (!stateWriteOperation)
|
|
3937
|
+
return null;
|
|
3938
|
+
const stateWriteArgs = stateWriteOperation.args;
|
|
3939
|
+
const mergeModeFlag = (payload) => {
|
|
3940
|
+
const mode = readStateWriteFlagValue(stateWriteArgs, "--mode");
|
|
3941
|
+
return normalizeStateWriteClassificationPayload(mode ? { ...payload, mode } : payload);
|
|
3942
|
+
};
|
|
3943
|
+
const inlineInput = readStateWriteFlagValue(stateWriteArgs, "--input");
|
|
3944
|
+
const inputFile = readStateWriteFlagValue(stateWriteArgs, "--input-file");
|
|
3945
|
+
if (inlineInput !== undefined && inputFile !== undefined)
|
|
3946
|
+
return null;
|
|
3947
|
+
if (inlineInput !== undefined) {
|
|
3948
|
+
try {
|
|
3949
|
+
const parsed = JSON.parse(inlineInput);
|
|
3950
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
3951
|
+
? mergeModeFlag(parsed)
|
|
3952
|
+
: null;
|
|
3953
|
+
}
|
|
3954
|
+
catch {
|
|
3955
|
+
return null;
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
if (inputFile === undefined)
|
|
3959
|
+
return null;
|
|
3960
|
+
if (stateWriteOperation.nested)
|
|
3961
|
+
return null;
|
|
3962
|
+
if (hasPriorExecutableCommand(stateWriteOperation.prefix))
|
|
3963
|
+
return null;
|
|
3964
|
+
const canonicalCommandPrefix = stateWriteOperation.commandPrefix || stateWriteOperation.prefix || "";
|
|
3965
|
+
const rawCommandPrefix = sourceCommand !== command
|
|
3966
|
+
? (() => {
|
|
3967
|
+
const sourceCommandIndex = sourceCommand.lastIndexOf(command);
|
|
3968
|
+
return sourceCommandIndex >= 0 ? sourceCommand.slice(0, sourceCommandIndex) : "";
|
|
3969
|
+
})()
|
|
3970
|
+
: "";
|
|
3971
|
+
const resolvedInputFileCwd = resolveStateWriteInputFileCwd(cwd, canonicalCommandPrefix || rawCommandPrefix);
|
|
3972
|
+
if (resolvedInputFileCwd === null)
|
|
3973
|
+
return null;
|
|
3974
|
+
try {
|
|
3975
|
+
const raw = readFileSync(resolve(resolvedInputFileCwd, inputFile.trim()), "utf-8");
|
|
3976
|
+
const parsed = JSON.parse(raw);
|
|
3977
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
3978
|
+
? mergeModeFlag(parsed)
|
|
3979
|
+
: null;
|
|
3980
|
+
}
|
|
3981
|
+
catch {
|
|
3982
|
+
return null;
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
function envCommandHasCwdChangingOption(words, startIndex) {
|
|
3986
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
3987
|
+
const token = words[index] ?? "";
|
|
3988
|
+
if (!token || token === "--")
|
|
3989
|
+
continue;
|
|
3990
|
+
if (isShellAssignmentWord(token))
|
|
3991
|
+
continue;
|
|
3992
|
+
if (isEnvCwdChangingOption(token))
|
|
3993
|
+
return true;
|
|
3994
|
+
if (token === "-S" || token === "--split-string" || token.startsWith("-S") || token.startsWith("--split-string=")) {
|
|
3995
|
+
return false;
|
|
3996
|
+
}
|
|
3997
|
+
if (token === "-u" || token === "--unset" || token === "-a" || token === "--argv0") {
|
|
3998
|
+
index += 1;
|
|
3999
|
+
continue;
|
|
4000
|
+
}
|
|
4001
|
+
if (token.startsWith("--unset=") || token.startsWith("--argv0="))
|
|
4002
|
+
continue;
|
|
4003
|
+
if (/^-u.+/.test(token) || /^-a.+/.test(token))
|
|
4004
|
+
continue;
|
|
4005
|
+
if (token.startsWith("-"))
|
|
4006
|
+
continue;
|
|
4007
|
+
return false;
|
|
4008
|
+
}
|
|
4009
|
+
return false;
|
|
4010
|
+
}
|
|
4011
|
+
function hasPriorExecutableCommand(commandPrefix) {
|
|
4012
|
+
const words = tokenizeShellWords(stripHeredocBodiesForCommandScan(commandPrefix));
|
|
4013
|
+
let commandStart = true;
|
|
4014
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
4015
|
+
const word = words[index] ?? "";
|
|
4016
|
+
if (!word)
|
|
4017
|
+
continue;
|
|
4018
|
+
if (word === "&&" || word === "||" || word === ";" || word === "&" || word === "|" || word === "|&") {
|
|
4019
|
+
commandStart = true;
|
|
4020
|
+
continue;
|
|
4021
|
+
}
|
|
4022
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word))
|
|
4023
|
+
continue;
|
|
4024
|
+
if (commandStart)
|
|
4025
|
+
return true;
|
|
4026
|
+
commandStart = false;
|
|
4027
|
+
}
|
|
4028
|
+
return false;
|
|
4029
|
+
}
|
|
4030
|
+
function tokenizeShellWords(segment) {
|
|
4031
|
+
segment = normalizeShellLineContinuations(segment);
|
|
4032
|
+
const words = [];
|
|
4033
|
+
let current = "";
|
|
4034
|
+
let quote = null;
|
|
4035
|
+
for (let index = 0; index < segment.length; index += 1) {
|
|
4036
|
+
const char = segment[index];
|
|
4037
|
+
if (char === "\\" && quote !== "'") {
|
|
4038
|
+
index += 1;
|
|
4039
|
+
current += segment[index] ?? "";
|
|
4040
|
+
continue;
|
|
4041
|
+
}
|
|
4042
|
+
if (char === "'" || char === "\"") {
|
|
4043
|
+
if (quote === char) {
|
|
4044
|
+
quote = null;
|
|
4045
|
+
}
|
|
4046
|
+
else if (!quote) {
|
|
4047
|
+
quote = char;
|
|
4048
|
+
}
|
|
4049
|
+
else {
|
|
4050
|
+
current += char;
|
|
4051
|
+
}
|
|
4052
|
+
continue;
|
|
4053
|
+
}
|
|
4054
|
+
if (!quote && (char === ";" || char === "&")) {
|
|
4055
|
+
if (current) {
|
|
4056
|
+
words.push(current);
|
|
4057
|
+
current = "";
|
|
4058
|
+
}
|
|
4059
|
+
const next = segment[index + 1] ?? "";
|
|
4060
|
+
if (char === "&" && next === "&") {
|
|
4061
|
+
words.push("&&");
|
|
4062
|
+
index += 1;
|
|
4063
|
+
}
|
|
4064
|
+
else {
|
|
4065
|
+
words.push(char);
|
|
4066
|
+
}
|
|
4067
|
+
continue;
|
|
4068
|
+
}
|
|
4069
|
+
if (!quote && /\s/.test(char)) {
|
|
4070
|
+
if (current) {
|
|
4071
|
+
words.push(current);
|
|
4072
|
+
current = "";
|
|
4073
|
+
}
|
|
4074
|
+
continue;
|
|
4075
|
+
}
|
|
4076
|
+
if (!quote && (char === "(" || char === ")" || char === "{" || char === "}")) {
|
|
4077
|
+
if (current) {
|
|
4078
|
+
words.push(current);
|
|
4079
|
+
current = "";
|
|
4080
|
+
}
|
|
4081
|
+
words.push(char);
|
|
4082
|
+
continue;
|
|
4083
|
+
}
|
|
4084
|
+
if (!quote && char === "|") {
|
|
4085
|
+
if (current) {
|
|
4086
|
+
words.push(current);
|
|
4087
|
+
current = "";
|
|
4088
|
+
}
|
|
4089
|
+
const next = segment[index + 1] ?? "";
|
|
4090
|
+
if (next === "&" || next === "|") {
|
|
4091
|
+
words.push(`${char}${next}`);
|
|
4092
|
+
index += 1;
|
|
4093
|
+
}
|
|
4094
|
+
else {
|
|
4095
|
+
words.push(char);
|
|
4096
|
+
}
|
|
4097
|
+
continue;
|
|
4098
|
+
}
|
|
4099
|
+
if (!quote && (char === "<" || char === ">")) {
|
|
4100
|
+
if (current) {
|
|
4101
|
+
words.push(current);
|
|
4102
|
+
current = "";
|
|
4103
|
+
}
|
|
4104
|
+
const next = segment[index + 1] ?? "";
|
|
4105
|
+
if (next === char) {
|
|
4106
|
+
const third = segment[index + 2] ?? "";
|
|
4107
|
+
if (char === "<" && third === "<") {
|
|
4108
|
+
words.push("<<<");
|
|
4109
|
+
index += 2;
|
|
4110
|
+
}
|
|
4111
|
+
else {
|
|
4112
|
+
words.push(`${char}${next}`);
|
|
4113
|
+
index += 1;
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
else if (char === ">" && next === "&") {
|
|
4117
|
+
words.push(">&");
|
|
4118
|
+
index += 1;
|
|
4119
|
+
}
|
|
4120
|
+
else {
|
|
4121
|
+
words.push(char);
|
|
4122
|
+
}
|
|
4123
|
+
continue;
|
|
4124
|
+
}
|
|
4125
|
+
current += char;
|
|
4126
|
+
}
|
|
4127
|
+
if (current)
|
|
4128
|
+
words.push(current);
|
|
4129
|
+
return words;
|
|
4130
|
+
}
|
|
4131
|
+
function isOmxCliEntryPath(token, runtimeWrapper) {
|
|
4132
|
+
const trimmed = token.trim().replace(/^['"]|['"]$/g, "");
|
|
4133
|
+
if (!trimmed || trimmed.includes("\0"))
|
|
4134
|
+
return false;
|
|
4135
|
+
const normalized = trimmed.replace(/\\/g, "/");
|
|
4136
|
+
const entryBasename = normalized.split("/").filter(Boolean).pop() ?? "";
|
|
4137
|
+
if (entryBasename === "omx" || entryBasename === "omx.js")
|
|
4138
|
+
return true;
|
|
4139
|
+
if (normalized.endsWith("/node_modules/.bin/omx") || normalized === "node_modules/.bin/omx")
|
|
4140
|
+
return true;
|
|
4141
|
+
if (normalized.endsWith("/dist/cli/omx.js") || normalized === "dist/cli/omx.js")
|
|
4142
|
+
return true;
|
|
4143
|
+
if (runtimeWrapper === "tsx" && (normalized.endsWith("/src/cli/omx.ts") || normalized === "src/cli/omx.ts"))
|
|
4144
|
+
return true;
|
|
4145
|
+
if (runtimeWrapper === "tsx" && (normalized.endsWith("/dist/cli/omx.js") || normalized === "dist/cli/omx.js"))
|
|
4146
|
+
return true;
|
|
4147
|
+
return false;
|
|
4148
|
+
}
|
|
4149
|
+
function extractEnvSplitStringCommand(words, startIndex) {
|
|
4150
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4151
|
+
const token = words[index] ?? "";
|
|
4152
|
+
if (!token)
|
|
4153
|
+
continue;
|
|
4154
|
+
if (token === "-S" || token === "--split-string") {
|
|
4155
|
+
const operand = words[index + 1] ?? "";
|
|
4156
|
+
if (!operand)
|
|
4157
|
+
return "";
|
|
4158
|
+
const tail = words.slice(index + 2).join(" ");
|
|
4159
|
+
return tail ? `env ${operand} ${tail}` : `env ${operand}`;
|
|
4160
|
+
}
|
|
4161
|
+
if (token.startsWith("--split-string=") || (token.startsWith("-S") && token.length > 2)) {
|
|
4162
|
+
const operand = token.startsWith("--split-string=")
|
|
4163
|
+
? token.slice("--split-string=".length)
|
|
4164
|
+
: token.slice(2);
|
|
4165
|
+
if (!operand)
|
|
4166
|
+
return "";
|
|
4167
|
+
const tail = words.slice(index + 1).join(" ");
|
|
4168
|
+
return tail ? `env ${operand} ${tail}` : `env ${operand}`;
|
|
4169
|
+
}
|
|
4170
|
+
if (isShellAssignmentWord(token))
|
|
4171
|
+
continue;
|
|
4172
|
+
if (token === "-u" || token === "--unset" || token === "-C" || token === "--chdir" || token === "-a" || token === "--argv0") {
|
|
4173
|
+
index += 1;
|
|
4174
|
+
continue;
|
|
4175
|
+
}
|
|
4176
|
+
if (token.startsWith("--unset=") || token.startsWith("--chdir=") || token.startsWith("--argv0="))
|
|
4177
|
+
continue;
|
|
4178
|
+
if (/^-u.+/.test(token) || /^-C.+/.test(token) || /^-a.+/.test(token))
|
|
4179
|
+
continue;
|
|
4180
|
+
if (token.startsWith("-"))
|
|
4181
|
+
continue;
|
|
4182
|
+
break;
|
|
4183
|
+
}
|
|
4184
|
+
return "";
|
|
4185
|
+
}
|
|
4186
|
+
function extractPackageManagerExecCommand(command, words, startIndex, headBase) {
|
|
4187
|
+
let index = startIndex;
|
|
4188
|
+
if (headBase === "npm" || headBase === "pnpm" || headBase === "yarn") {
|
|
4189
|
+
const subcommandIndex = findPackageManagerExecSubcommandIndex(words, startIndex, headBase);
|
|
4190
|
+
if (subcommandIndex === null)
|
|
4191
|
+
return "";
|
|
4192
|
+
index = subcommandIndex + 1;
|
|
4193
|
+
}
|
|
4194
|
+
else if (headBase !== "npx" && headBase !== "pnpx") {
|
|
4195
|
+
return "";
|
|
4196
|
+
}
|
|
4197
|
+
for (; index < words.length; index += 1) {
|
|
4198
|
+
const token = words[index] ?? "";
|
|
4199
|
+
if (!token)
|
|
4200
|
+
continue;
|
|
4201
|
+
if (token === "--") {
|
|
4202
|
+
const tail = sliceShellWordsTailPreservingQuoting(command, index + 1);
|
|
4203
|
+
return tail || "";
|
|
4204
|
+
}
|
|
4205
|
+
if (isShellAssignmentWord(token))
|
|
4206
|
+
continue;
|
|
4207
|
+
if (token === "-c" || token === "--call") {
|
|
4208
|
+
return words[index + 1] ?? "";
|
|
4209
|
+
}
|
|
4210
|
+
if (token.startsWith("-c") && token.length > 2)
|
|
4211
|
+
return token.slice(2);
|
|
4212
|
+
if (token.startsWith("--call="))
|
|
4213
|
+
return token.slice("--call=".length);
|
|
4214
|
+
if (token === "--package" || token === "-w" || token === "--workspace" || token === "--allow-scripts") {
|
|
4215
|
+
index += 1;
|
|
4216
|
+
continue;
|
|
4217
|
+
}
|
|
4218
|
+
if (token.startsWith("--package=") || token.startsWith("--workspace=") || token.startsWith("--allow-scripts="))
|
|
4219
|
+
continue;
|
|
4220
|
+
if (token.startsWith("-w") && token.length > 2)
|
|
4221
|
+
continue;
|
|
4222
|
+
if (token.startsWith("-"))
|
|
4223
|
+
continue;
|
|
4224
|
+
const tail = sliceShellWordsTailPreservingQuoting(command, index);
|
|
4225
|
+
return tail || "";
|
|
4226
|
+
}
|
|
4227
|
+
return "";
|
|
4228
|
+
}
|
|
4229
|
+
function findPackageManagerExecSubcommandIndex(words, startIndex, headBase) {
|
|
4230
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4231
|
+
const token = words[index] ?? "";
|
|
4232
|
+
if (!token)
|
|
4233
|
+
continue;
|
|
4234
|
+
if (isShellAssignmentWord(token))
|
|
4235
|
+
continue;
|
|
4236
|
+
if (token === "-C" || token === "--prefix" || token === "--dir" || token === "-w" || token === "--workspace" || token === "--package" || token === "--allow-scripts") {
|
|
4237
|
+
index += 1;
|
|
4238
|
+
continue;
|
|
4239
|
+
}
|
|
4240
|
+
if (token.startsWith("--prefix=") || token.startsWith("--dir=") || token.startsWith("--workspace=") || token.startsWith("--package=") || token.startsWith("--allow-scripts="))
|
|
4241
|
+
continue;
|
|
4242
|
+
if (/^-C.+/.test(token) || /^-w.+/.test(token))
|
|
4243
|
+
continue;
|
|
4244
|
+
if (token.startsWith("-"))
|
|
4245
|
+
continue;
|
|
4246
|
+
if (token === "exec" || (headBase === "npm" && token === "x") || ((headBase === "pnpm" || headBase === "yarn") && token === "dlx")) {
|
|
4247
|
+
return index;
|
|
4248
|
+
}
|
|
4249
|
+
return null;
|
|
4250
|
+
}
|
|
4251
|
+
return null;
|
|
4252
|
+
}
|
|
4253
|
+
function unwrapOmxStateTransportCommandOnce(command) {
|
|
4254
|
+
const words = tokenizeShellWords(normalizeShellLineContinuations(command).trim());
|
|
4255
|
+
if (words.length === 0)
|
|
4256
|
+
return null;
|
|
4257
|
+
let index = 0;
|
|
4258
|
+
while (index < words.length && isShellAssignmentWord(words[index] ?? ""))
|
|
4259
|
+
index += 1;
|
|
4260
|
+
if (index >= words.length)
|
|
4261
|
+
return null;
|
|
4262
|
+
const head = words[index] ?? "";
|
|
4263
|
+
if (!head)
|
|
4264
|
+
return null;
|
|
4265
|
+
const headBase = shellWordBaseName(head);
|
|
4266
|
+
if (headBase === "command" || headBase === "builtin" || headBase === "exec" || headBase === "nohup" || headBase === "setsid") {
|
|
4267
|
+
let remainderIndex = index + 1;
|
|
4268
|
+
while (remainderIndex < words.length) {
|
|
4269
|
+
const token = words[remainderIndex] ?? "";
|
|
4270
|
+
if (!token) {
|
|
4271
|
+
remainderIndex += 1;
|
|
4272
|
+
continue;
|
|
4273
|
+
}
|
|
4274
|
+
if (token === "--") {
|
|
4275
|
+
remainderIndex += 1;
|
|
4276
|
+
continue;
|
|
4277
|
+
}
|
|
4278
|
+
if (head === "exec" && token === "-a") {
|
|
4279
|
+
remainderIndex += 2;
|
|
4280
|
+
continue;
|
|
4281
|
+
}
|
|
4282
|
+
if (token.startsWith("-")) {
|
|
4283
|
+
remainderIndex += 1;
|
|
4284
|
+
continue;
|
|
4285
|
+
}
|
|
4286
|
+
break;
|
|
4287
|
+
}
|
|
4288
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, remainderIndex);
|
|
4289
|
+
return remainder || null;
|
|
4290
|
+
}
|
|
4291
|
+
if (headBase === "env") {
|
|
4292
|
+
if (envCommandHasCwdChangingOption(words, index + 1)) {
|
|
4293
|
+
return null;
|
|
4294
|
+
}
|
|
4295
|
+
const splitOperand = extractEnvSplitStringCommand(words, index + 1);
|
|
4296
|
+
if (splitOperand) {
|
|
4297
|
+
return splitOperand;
|
|
4298
|
+
}
|
|
4299
|
+
const envCommandIndex = findEnvDispatchOperandIndex(words, index + 1);
|
|
4300
|
+
if (envCommandIndex !== null) {
|
|
4301
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, envCommandIndex);
|
|
4302
|
+
return remainder || null;
|
|
4303
|
+
}
|
|
4304
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, index + 1);
|
|
4305
|
+
return remainder || null;
|
|
4306
|
+
}
|
|
4307
|
+
if (headBase === "time") {
|
|
4308
|
+
const operandIndex = findTimeDispatchOperandIndex(words, index + 1);
|
|
4309
|
+
if (operandIndex !== null) {
|
|
4310
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, operandIndex);
|
|
4311
|
+
return remainder || null;
|
|
4312
|
+
}
|
|
4313
|
+
return null;
|
|
4314
|
+
}
|
|
4315
|
+
if (headBase === "nice") {
|
|
4316
|
+
const operandIndex = findNiceDispatchOperandIndex(words, index + 1);
|
|
4317
|
+
if (operandIndex !== null) {
|
|
4318
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, operandIndex);
|
|
4319
|
+
return remainder || null;
|
|
4320
|
+
}
|
|
4321
|
+
return null;
|
|
4322
|
+
}
|
|
4323
|
+
if (headBase === "stdbuf") {
|
|
4324
|
+
const operandIndex = findStdbufDispatchOperandIndex(words, index + 1);
|
|
4325
|
+
if (operandIndex !== null) {
|
|
4326
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, operandIndex);
|
|
4327
|
+
return remainder || null;
|
|
4328
|
+
}
|
|
4329
|
+
return null;
|
|
4330
|
+
}
|
|
4331
|
+
if (headBase === "npm" || headBase === "pnpm" || headBase === "yarn" || headBase === "npx" || headBase === "pnpx") {
|
|
4332
|
+
const packageManagerCommand = extractPackageManagerExecCommand(command, words, index + 1, headBase);
|
|
4333
|
+
if (packageManagerCommand) {
|
|
4334
|
+
return packageManagerCommand;
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
if (isNestedShellCommandWord(headBase)) {
|
|
4338
|
+
const commandStringIndex = findShellCommandStringArgIndex(words, index + 1);
|
|
4339
|
+
if (commandStringIndex !== null) {
|
|
4340
|
+
const nestedCommand = words[commandStringIndex] ?? "";
|
|
4341
|
+
if (nestedCommand && !isDynamicNestedCommandString(nestedCommand)) {
|
|
4342
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, commandStringIndex + 1);
|
|
4343
|
+
return remainder ? `${nestedCommand} ${remainder}` : nestedCommand;
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
if (headBase === "eval") {
|
|
4348
|
+
const nestedCommand = words.slice(index + 1).join(" ");
|
|
4349
|
+
if (nestedCommand && !isDynamicNestedCommandString(nestedCommand))
|
|
4350
|
+
return nestedCommand;
|
|
4351
|
+
}
|
|
4352
|
+
if (headBase === "node" || headBase === "bun" || headBase === "tsx") {
|
|
4353
|
+
const entryIndex = (() => {
|
|
4354
|
+
for (let candidateIndex = index + 1; candidateIndex < words.length; candidateIndex += 1) {
|
|
4355
|
+
const candidate = words[candidateIndex] ?? "";
|
|
4356
|
+
if (!candidate)
|
|
4357
|
+
continue;
|
|
4358
|
+
if (candidate.startsWith("-"))
|
|
4359
|
+
continue;
|
|
4360
|
+
return candidateIndex;
|
|
4361
|
+
}
|
|
4362
|
+
return -1;
|
|
4363
|
+
})();
|
|
4364
|
+
if (entryIndex >= 0) {
|
|
4365
|
+
const entryPath = words[entryIndex] ?? "";
|
|
4366
|
+
if (entryPath && isOmxCliEntryPath(entryPath, headBase)) {
|
|
4367
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, entryIndex + 1);
|
|
4368
|
+
return remainder ? `omx ${remainder}` : "omx";
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
return null;
|
|
4372
|
+
}
|
|
4373
|
+
if (isOmxCliEntryPath(head, null)) {
|
|
4374
|
+
const remainder = sliceShellWordsTailPreservingQuoting(command, index + 1);
|
|
4375
|
+
return remainder ? `omx ${remainder}` : "omx";
|
|
4376
|
+
}
|
|
4377
|
+
return null;
|
|
4378
|
+
}
|
|
4379
|
+
function canonicalizeOmxStateTransportCommand(command) {
|
|
4380
|
+
let current = normalizeShellLineContinuations(command).trim();
|
|
4381
|
+
for (let passes = 0; passes < 8; passes += 1) {
|
|
4382
|
+
const next = unwrapOmxStateTransportCommandOnce(current);
|
|
4383
|
+
if (!next || next === current)
|
|
4384
|
+
return current;
|
|
4385
|
+
current = next.trim();
|
|
4386
|
+
}
|
|
4387
|
+
return current;
|
|
4388
|
+
}
|
|
4389
|
+
function sliceShellWordsTailPreservingQuoting(command, startWordIndex) {
|
|
4390
|
+
const normalized = normalizeShellLineContinuations(command);
|
|
4391
|
+
let quote = null;
|
|
4392
|
+
let wordIndex = 0;
|
|
4393
|
+
let currentWordStart = null;
|
|
4394
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
4395
|
+
const char = normalized[index] ?? "";
|
|
4396
|
+
if (char === "\\" && quote !== "'") {
|
|
4397
|
+
if (currentWordStart === null)
|
|
4398
|
+
currentWordStart = index;
|
|
4399
|
+
index += 1;
|
|
4400
|
+
continue;
|
|
4401
|
+
}
|
|
4402
|
+
if (char === "'" || char === "\"") {
|
|
4403
|
+
if (quote === char) {
|
|
4404
|
+
quote = null;
|
|
4405
|
+
}
|
|
4406
|
+
else if (!quote) {
|
|
4407
|
+
quote = char;
|
|
4408
|
+
if (currentWordStart === null)
|
|
4409
|
+
currentWordStart = index;
|
|
4410
|
+
}
|
|
4411
|
+
else if (currentWordStart === null) {
|
|
4412
|
+
currentWordStart = index;
|
|
4413
|
+
}
|
|
4414
|
+
continue;
|
|
4415
|
+
}
|
|
4416
|
+
if (!quote && /\s/.test(char)) {
|
|
4417
|
+
if (currentWordStart !== null) {
|
|
4418
|
+
if (wordIndex === startWordIndex)
|
|
4419
|
+
return normalized.slice(currentWordStart);
|
|
4420
|
+
wordIndex += 1;
|
|
4421
|
+
currentWordStart = null;
|
|
4422
|
+
}
|
|
4423
|
+
continue;
|
|
4424
|
+
}
|
|
4425
|
+
if (currentWordStart === null)
|
|
4426
|
+
currentWordStart = index;
|
|
4427
|
+
}
|
|
4428
|
+
if (currentWordStart !== null && wordIndex === startWordIndex) {
|
|
4429
|
+
return normalized.slice(currentWordStart);
|
|
4430
|
+
}
|
|
4431
|
+
return null;
|
|
4432
|
+
}
|
|
4433
|
+
function normalizeShellLineContinuations(command) {
|
|
4434
|
+
let normalized = "";
|
|
4435
|
+
let quote = null;
|
|
4436
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
4437
|
+
const char = command[index] ?? "";
|
|
4438
|
+
if (char === "'" || char === "\"") {
|
|
4439
|
+
if (quote === char) {
|
|
4440
|
+
quote = null;
|
|
4441
|
+
}
|
|
4442
|
+
else if (!quote) {
|
|
4443
|
+
quote = char;
|
|
4444
|
+
}
|
|
4445
|
+
normalized += char;
|
|
4446
|
+
continue;
|
|
4447
|
+
}
|
|
4448
|
+
if (char === "\\" && quote !== "'") {
|
|
4449
|
+
const next = command[index + 1] ?? "";
|
|
4450
|
+
if (next === "\r" && command[index + 2] === "\n") {
|
|
4451
|
+
index += 2;
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
if (next === "\n") {
|
|
4455
|
+
index += 1;
|
|
4456
|
+
continue;
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
normalized += char;
|
|
4460
|
+
}
|
|
4461
|
+
return normalized;
|
|
4462
|
+
}
|
|
4463
|
+
function readStateWriteFlagValue(args, flagName) {
|
|
4464
|
+
let value;
|
|
4465
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
4466
|
+
const arg = args[index] ?? "";
|
|
4467
|
+
if (arg === flagName) {
|
|
4468
|
+
value = args[index + 1];
|
|
4469
|
+
index += 1;
|
|
4470
|
+
continue;
|
|
4471
|
+
}
|
|
4472
|
+
if (arg.startsWith(`${flagName}=`))
|
|
4473
|
+
value = arg.slice(flagName.length + 1);
|
|
4474
|
+
}
|
|
4475
|
+
return value;
|
|
4476
|
+
}
|
|
4477
|
+
function shellWordBaseName(word) {
|
|
4478
|
+
return word.replace(/\\/g, "/").split("/").pop() ?? word;
|
|
4479
|
+
}
|
|
4480
|
+
function isOmxCliWrapperRuntime(word) {
|
|
4481
|
+
const base = shellWordBaseName(word);
|
|
4482
|
+
return base === "node" || base === "bun" || base === "tsx";
|
|
4483
|
+
}
|
|
4484
|
+
function isOmxCliWrapperScript(word) {
|
|
4485
|
+
const base = shellWordBaseName(word);
|
|
4486
|
+
return base === "omx" || base === "omx.js";
|
|
4487
|
+
}
|
|
4488
|
+
function runtimeOptionConsumesNextWord(option) {
|
|
4489
|
+
return option === "-r"
|
|
4490
|
+
|| option === "--require"
|
|
4491
|
+
|| option === "--import"
|
|
4492
|
+
|| option === "--loader"
|
|
4493
|
+
|| option === "--experimental-loader"
|
|
4494
|
+
|| option === "--env-file"
|
|
4495
|
+
|| option === "--conditions"
|
|
4496
|
+
|| option === "--title"
|
|
4497
|
+
|| option === "-C";
|
|
4498
|
+
}
|
|
4499
|
+
function runtimeOptionIsInlineCode(option) {
|
|
4500
|
+
return option === "-e"
|
|
4501
|
+
|| option === "--eval"
|
|
4502
|
+
|| option.startsWith("--eval=")
|
|
4503
|
+
|| option === "-p"
|
|
4504
|
+
|| option === "--print"
|
|
4505
|
+
|| option.startsWith("--print=");
|
|
4506
|
+
}
|
|
4507
|
+
function findOmxCliWrapperRuntimeScriptIndex(words) {
|
|
4508
|
+
if (!isOmxCliWrapperRuntime(words[0] ?? ""))
|
|
4509
|
+
return null;
|
|
4510
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
4511
|
+
const word = words[index] ?? "";
|
|
4512
|
+
if (!word)
|
|
4513
|
+
continue;
|
|
4514
|
+
if (word === "--")
|
|
4515
|
+
continue;
|
|
4516
|
+
if (runtimeOptionIsInlineCode(word))
|
|
4517
|
+
return null;
|
|
4518
|
+
if (runtimeOptionConsumesNextWord(word)) {
|
|
4519
|
+
index += 1;
|
|
4520
|
+
continue;
|
|
4521
|
+
}
|
|
4522
|
+
if (word.startsWith("-r") && word.length > 2)
|
|
4523
|
+
continue;
|
|
4524
|
+
if (word.startsWith("--") && word.includes("="))
|
|
4525
|
+
continue;
|
|
4526
|
+
if (word.startsWith("-"))
|
|
4527
|
+
continue;
|
|
4528
|
+
return isOmxCliWrapperScript(word) ? index : null;
|
|
4529
|
+
}
|
|
4530
|
+
return null;
|
|
4531
|
+
}
|
|
4532
|
+
function readOmxStateCommandArgsFromWords(words, operation) {
|
|
4533
|
+
if (isOmxCliWrapperScript(words[0] ?? "") && words[1] === "state" && words[2] === operation) {
|
|
4534
|
+
return words.slice(3);
|
|
4535
|
+
}
|
|
4536
|
+
const runtimeScriptIndex = findOmxCliWrapperRuntimeScriptIndex(words);
|
|
4537
|
+
if (runtimeScriptIndex !== null && words[runtimeScriptIndex + 1] === "state" && words[runtimeScriptIndex + 2] === operation) {
|
|
4538
|
+
return words.slice(runtimeScriptIndex + 3);
|
|
4539
|
+
}
|
|
4540
|
+
return null;
|
|
4541
|
+
}
|
|
4542
|
+
// Shell compound-command introducers can occupy command position after wrappers
|
|
4543
|
+
// such as `time`/`command`; skip them before matching the protected `omx state`
|
|
4544
|
+
// operation so wrapper-unwrapping keeps scanning the actual command body.
|
|
4545
|
+
function isShellCommandPositionPrefixWord(word) {
|
|
4546
|
+
return word === "("
|
|
4547
|
+
|| word === "{"
|
|
4548
|
+
|| word === "!"
|
|
4549
|
+
|| word === "if"
|
|
4550
|
+
|| word === "then"
|
|
4551
|
+
|| word === "else"
|
|
4552
|
+
|| word === "elif"
|
|
4553
|
+
|| word === "do"
|
|
4554
|
+
|| word === "while"
|
|
4555
|
+
|| word === "until";
|
|
4556
|
+
}
|
|
4557
|
+
function findEnvDispatchOperandIndex(words, startIndex) {
|
|
4558
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4559
|
+
const option = words[index] ?? "";
|
|
4560
|
+
if (!option || option === "--")
|
|
4561
|
+
continue;
|
|
4562
|
+
if (isShellAssignmentWord(option))
|
|
4563
|
+
continue;
|
|
4564
|
+
if (option === "-S" || option === "--split-string" || option.startsWith("-S") || option.startsWith("--split-string=")) {
|
|
4565
|
+
return null;
|
|
4566
|
+
}
|
|
4567
|
+
if (option === "-u" || option === "--unset" || option === "-C" || option === "--chdir" || option === "-a" || option === "--argv0") {
|
|
4568
|
+
index += 1;
|
|
4569
|
+
continue;
|
|
4570
|
+
}
|
|
4571
|
+
if (option.startsWith("--unset=") || option.startsWith("--chdir=") || option.startsWith("--argv0="))
|
|
4572
|
+
continue;
|
|
4573
|
+
if (/^-u.+/.test(option) || /^-C.+/.test(option) || /^-a.+/.test(option))
|
|
4574
|
+
continue;
|
|
4575
|
+
if (option.startsWith("-"))
|
|
4576
|
+
continue;
|
|
4577
|
+
return index;
|
|
4578
|
+
}
|
|
4579
|
+
return null;
|
|
4580
|
+
}
|
|
4581
|
+
function findCommandDispatchOperandIndex(words, startIndex) {
|
|
4582
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4583
|
+
const option = words[index] ?? "";
|
|
4584
|
+
if (!option || option === "--")
|
|
4585
|
+
continue;
|
|
4586
|
+
if (isShellAssignmentWord(option))
|
|
4587
|
+
continue;
|
|
4588
|
+
if (option.startsWith("-"))
|
|
4589
|
+
continue;
|
|
4590
|
+
return index;
|
|
4591
|
+
}
|
|
4592
|
+
return null;
|
|
4593
|
+
}
|
|
4594
|
+
function findExecDispatchOperandIndex(words, startIndex) {
|
|
4595
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4596
|
+
const option = words[index] ?? "";
|
|
4597
|
+
if (!option || option === "--")
|
|
4598
|
+
continue;
|
|
4599
|
+
if (isShellAssignmentWord(option))
|
|
4600
|
+
continue;
|
|
4601
|
+
if (option === "-a") {
|
|
4602
|
+
index += 1;
|
|
4603
|
+
continue;
|
|
4604
|
+
}
|
|
4605
|
+
if (option.startsWith("-"))
|
|
4606
|
+
continue;
|
|
4607
|
+
return index;
|
|
4608
|
+
}
|
|
4609
|
+
return null;
|
|
4610
|
+
}
|
|
4611
|
+
function findTimeDispatchOperandIndex(words, startIndex) {
|
|
4612
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4613
|
+
const option = words[index] ?? "";
|
|
4614
|
+
if (!option || option === "--")
|
|
4615
|
+
continue;
|
|
4616
|
+
if (isShellAssignmentWord(option))
|
|
4617
|
+
continue;
|
|
4618
|
+
if (option === "-f" || option === "--format" || option === "-o" || option === "--output") {
|
|
4619
|
+
index += 1;
|
|
4620
|
+
continue;
|
|
4621
|
+
}
|
|
4622
|
+
if (option.startsWith("--format=") || option.startsWith("--output="))
|
|
4623
|
+
continue;
|
|
4624
|
+
if (/^-[^-]/.test(option)) {
|
|
4625
|
+
const shortOptions = option.slice(1);
|
|
4626
|
+
const lastValueOptionIndex = Math.max(shortOptions.lastIndexOf("f"), shortOptions.lastIndexOf("o"));
|
|
4627
|
+
if (lastValueOptionIndex >= 0 && lastValueOptionIndex === shortOptions.length - 1) {
|
|
4628
|
+
index += 1;
|
|
4629
|
+
}
|
|
4630
|
+
continue;
|
|
4631
|
+
}
|
|
4632
|
+
if (option.startsWith("-"))
|
|
4633
|
+
continue;
|
|
4634
|
+
return index;
|
|
4635
|
+
}
|
|
4636
|
+
return null;
|
|
4637
|
+
}
|
|
4638
|
+
function findTimeoutDispatchOperandIndex(words, startIndex) {
|
|
4639
|
+
let durationSeen = false;
|
|
4640
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4641
|
+
const token = words[index] ?? "";
|
|
4642
|
+
if (!token || token === "--")
|
|
4643
|
+
continue;
|
|
4644
|
+
if (isShellAssignmentWord(token))
|
|
4645
|
+
continue;
|
|
4646
|
+
if (!durationSeen) {
|
|
4647
|
+
if (token === "-k"
|
|
4648
|
+
|| token === "--kill-after"
|
|
4649
|
+
|| token === "-s"
|
|
4650
|
+
|| token === "--signal"
|
|
4651
|
+
|| token.startsWith("--kill-after=")
|
|
4652
|
+
|| token.startsWith("--signal=")
|
|
4653
|
+
|| token.startsWith("-k")
|
|
4654
|
+
|| token.startsWith("-s")) {
|
|
4655
|
+
if (token === "-k"
|
|
4656
|
+
|| token === "--kill-after"
|
|
4657
|
+
|| token === "-s"
|
|
4658
|
+
|| token === "--signal") {
|
|
4659
|
+
index += 1;
|
|
4660
|
+
}
|
|
4661
|
+
continue;
|
|
4662
|
+
}
|
|
4663
|
+
if (token === "-f"
|
|
4664
|
+
|| token === "--foreground"
|
|
4665
|
+
|| token === "-p"
|
|
4666
|
+
|| token === "--preserve-status"
|
|
4667
|
+
|| token === "-v"
|
|
4668
|
+
|| token === "--verbose") {
|
|
4669
|
+
continue;
|
|
4670
|
+
}
|
|
4671
|
+
if (token.startsWith("-"))
|
|
4672
|
+
continue;
|
|
4673
|
+
durationSeen = true;
|
|
4674
|
+
continue;
|
|
4675
|
+
}
|
|
4676
|
+
return index;
|
|
4677
|
+
}
|
|
4678
|
+
return null;
|
|
4679
|
+
}
|
|
4680
|
+
function findNiceDispatchOperandIndex(words, startIndex) {
|
|
4681
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4682
|
+
const token = words[index] ?? "";
|
|
4683
|
+
if (!token || token === "--")
|
|
4684
|
+
continue;
|
|
4685
|
+
if (isShellAssignmentWord(token))
|
|
4686
|
+
continue;
|
|
4687
|
+
if (token === "-n" || token === "--adjustment") {
|
|
4688
|
+
index += 1;
|
|
4689
|
+
continue;
|
|
4690
|
+
}
|
|
4691
|
+
if (token.startsWith("--adjustment=") || /^-n.+/.test(token))
|
|
4692
|
+
continue;
|
|
4693
|
+
if (token.startsWith("-"))
|
|
4694
|
+
continue;
|
|
4695
|
+
return index;
|
|
4696
|
+
}
|
|
4697
|
+
return null;
|
|
4698
|
+
}
|
|
4699
|
+
function findStdbufDispatchOperandIndex(words, startIndex) {
|
|
4700
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4701
|
+
const token = words[index] ?? "";
|
|
4702
|
+
if (!token || token === "--")
|
|
4703
|
+
continue;
|
|
4704
|
+
if (isShellAssignmentWord(token))
|
|
4705
|
+
continue;
|
|
4706
|
+
if (token === "-i" || token === "-o" || token === "-e" || token === "--input" || token === "--output" || token === "--error") {
|
|
4707
|
+
index += 1;
|
|
4708
|
+
continue;
|
|
4709
|
+
}
|
|
4710
|
+
if (token.startsWith("--input=")
|
|
4711
|
+
|| token.startsWith("--output=")
|
|
4712
|
+
|| token.startsWith("--error=")
|
|
4713
|
+
|| /^-[ioe].+/.test(token)) {
|
|
4714
|
+
continue;
|
|
4715
|
+
}
|
|
4716
|
+
if (token.startsWith("-"))
|
|
4717
|
+
continue;
|
|
4718
|
+
return index;
|
|
4719
|
+
}
|
|
4720
|
+
return null;
|
|
4721
|
+
}
|
|
4722
|
+
function isXargsOptionWithNextValue(option) {
|
|
4723
|
+
return option === "-a"
|
|
4724
|
+
|| option === "--arg-file"
|
|
4725
|
+
|| option === "-d"
|
|
4726
|
+
|| option === "--delimiter"
|
|
4727
|
+
|| option === "-E"
|
|
4728
|
+
|| option === "--eof"
|
|
4729
|
+
|| option === "-I"
|
|
4730
|
+
|| option === "--replace"
|
|
4731
|
+
|| option === "-J"
|
|
4732
|
+
|| option === "-L"
|
|
4733
|
+
|| option === "--max-lines"
|
|
4734
|
+
|| option === "-n"
|
|
4735
|
+
|| option === "--max-args"
|
|
4736
|
+
|| option === "-P"
|
|
4737
|
+
|| option === "--max-procs"
|
|
4738
|
+
|| option === "-s"
|
|
4739
|
+
|| option === "--max-chars";
|
|
4740
|
+
}
|
|
4741
|
+
function isXargsStandaloneOption(option) {
|
|
4742
|
+
return option === "-0"
|
|
4743
|
+
|| option === "--null"
|
|
4744
|
+
|| option === "-r"
|
|
4745
|
+
|| option === "--no-run-if-empty"
|
|
4746
|
+
|| option === "-t"
|
|
4747
|
+
|| option === "--verbose"
|
|
4748
|
+
|| option === "-p"
|
|
4749
|
+
|| option === "--interactive"
|
|
4750
|
+
|| option === "-x"
|
|
4751
|
+
|| option === "--exit"
|
|
4752
|
+
|| option === "-o"
|
|
4753
|
+
|| option === "--open-tty"
|
|
4754
|
+
|| option === "--show-limits";
|
|
4755
|
+
}
|
|
4756
|
+
function findXargsDispatchOperandIndex(words, startIndex) {
|
|
4757
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
4758
|
+
const option = words[index] ?? "";
|
|
4759
|
+
if (!option || option === "--")
|
|
4760
|
+
continue;
|
|
4761
|
+
if (isShellAssignmentWord(option))
|
|
4762
|
+
continue;
|
|
4763
|
+
if (isXargsOptionWithNextValue(option)) {
|
|
4764
|
+
index += 1;
|
|
4765
|
+
continue;
|
|
4766
|
+
}
|
|
4767
|
+
if (option.startsWith("--arg-file=")
|
|
4768
|
+
|| option.startsWith("--delimiter=")
|
|
4769
|
+
|| option.startsWith("--eof=")
|
|
4770
|
+
|| option.startsWith("--replace=")
|
|
4771
|
+
|| option.startsWith("--max-lines=")
|
|
4772
|
+
|| option.startsWith("--max-args=")
|
|
4773
|
+
|| option.startsWith("--max-procs=")
|
|
4774
|
+
|| option.startsWith("--max-chars=")
|
|
4775
|
+
|| /^-[aAdDEIJLnPs][^-\s]*$/.test(option)) {
|
|
4776
|
+
continue;
|
|
4777
|
+
}
|
|
4778
|
+
if (isXargsStandaloneOption(option))
|
|
4779
|
+
continue;
|
|
4780
|
+
if (option.startsWith("-"))
|
|
4781
|
+
continue;
|
|
4782
|
+
return index;
|
|
4783
|
+
}
|
|
4784
|
+
return null;
|
|
4785
|
+
}
|
|
4786
|
+
function findCoprocDispatchOperandIndex(words, startIndex) {
|
|
4787
|
+
const firstIndex = findDispatchWordIndex(words, startIndex);
|
|
4788
|
+
if (firstIndex === null)
|
|
4789
|
+
return null;
|
|
4790
|
+
const firstWord = words[firstIndex] ?? "";
|
|
4791
|
+
if (isShellCommandPositionPrefixWord(firstWord))
|
|
4792
|
+
return firstIndex;
|
|
4793
|
+
const secondIndex = findDispatchWordIndex(words, firstIndex + 1);
|
|
4794
|
+
if (secondIndex !== null && isShellCommandPositionPrefixWord(words[secondIndex] ?? "")) {
|
|
4795
|
+
return secondIndex;
|
|
4796
|
+
}
|
|
4797
|
+
return firstIndex;
|
|
4798
|
+
}
|
|
4799
|
+
function findCaseArmCommandIndex(words, startIndex) {
|
|
4800
|
+
let index = startIndex;
|
|
4801
|
+
while (index < words.length && isShellAssignmentWord(words[index] ?? ""))
|
|
4802
|
+
index += 1;
|
|
4803
|
+
const head = words[index] ?? "";
|
|
4804
|
+
if (!head)
|
|
4805
|
+
return null;
|
|
4806
|
+
if (head === "case") {
|
|
4807
|
+
let inIndex = -1;
|
|
4808
|
+
for (let scanIndex = index + 1; scanIndex < words.length; scanIndex += 1) {
|
|
4809
|
+
const token = words[scanIndex] ?? "";
|
|
4810
|
+
if (!token)
|
|
4811
|
+
continue;
|
|
4812
|
+
if (token === "in") {
|
|
4813
|
+
inIndex = scanIndex;
|
|
4814
|
+
break;
|
|
4815
|
+
}
|
|
4816
|
+
if (token === "esac")
|
|
4817
|
+
return null;
|
|
4818
|
+
}
|
|
4819
|
+
if (inIndex < 0)
|
|
4820
|
+
return null;
|
|
4821
|
+
for (let scanIndex = inIndex + 1; scanIndex < words.length; scanIndex += 1) {
|
|
4822
|
+
const token = words[scanIndex] ?? "";
|
|
4823
|
+
if (!token)
|
|
4824
|
+
continue;
|
|
4825
|
+
if (token === "esac")
|
|
4826
|
+
return null;
|
|
4827
|
+
if (token === ")" || token.endsWith(")"))
|
|
4828
|
+
return scanIndex + 1;
|
|
4829
|
+
}
|
|
4830
|
+
return null;
|
|
4831
|
+
}
|
|
4832
|
+
if (head === ")" || head.endsWith(")")) {
|
|
4833
|
+
return index + 1;
|
|
4834
|
+
}
|
|
4835
|
+
if (words[index + 1] === ")") {
|
|
4836
|
+
return index + 2;
|
|
4837
|
+
}
|
|
4838
|
+
return null;
|
|
4839
|
+
}
|
|
4840
|
+
function isShellRedirectionWord(word) {
|
|
4841
|
+
return word === ">"
|
|
4842
|
+
|| word === ">>"
|
|
4843
|
+
|| word === "<"
|
|
4844
|
+
|| word === "<<"
|
|
4845
|
+
|| word === "<<<"
|
|
4846
|
+
|| word === ">&"
|
|
4847
|
+
|| word === "<&";
|
|
4848
|
+
}
|
|
4849
|
+
function skipShellCommandPositionPrefixWords(words, startIndex) {
|
|
4850
|
+
let commandWordIndex = startIndex;
|
|
4851
|
+
while (isShellAssignmentWord(words[commandWordIndex] ?? "")
|
|
4852
|
+
|| isShellCommandPositionPrefixWord(words[commandWordIndex] ?? "")) {
|
|
4853
|
+
commandWordIndex += 1;
|
|
4854
|
+
}
|
|
4855
|
+
while (commandWordIndex < words.length) {
|
|
4856
|
+
const word = words[commandWordIndex] ?? "";
|
|
4857
|
+
const nextWord = words[commandWordIndex + 1] ?? "";
|
|
4858
|
+
if (isShellRedirectionWord(word)) {
|
|
4859
|
+
commandWordIndex += 2;
|
|
4860
|
+
continue;
|
|
4861
|
+
}
|
|
4862
|
+
if (/^\d+$/.test(word) && isShellRedirectionWord(nextWord)) {
|
|
4863
|
+
commandWordIndex += 3;
|
|
4864
|
+
continue;
|
|
4865
|
+
}
|
|
4866
|
+
break;
|
|
4867
|
+
}
|
|
4868
|
+
return commandWordIndex;
|
|
4869
|
+
}
|
|
4870
|
+
function findWrappedCommandPositionIndex(words, startIndex) {
|
|
4871
|
+
let commandWordIndex = skipShellCommandPositionPrefixWords(words, startIndex);
|
|
4872
|
+
for (let unwrapCount = 0; unwrapCount < 8; unwrapCount += 1) {
|
|
4873
|
+
const commandWord = words[commandWordIndex] ?? "";
|
|
4874
|
+
if (!commandWord)
|
|
4875
|
+
return null;
|
|
4876
|
+
const commandWordBase = shellWordBaseName(commandWord);
|
|
4877
|
+
const operandIndex = commandWordBase === "env"
|
|
4878
|
+
? findEnvDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4879
|
+
: commandWordBase === "command" || commandWordBase === "builtin"
|
|
4880
|
+
? findCommandDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4881
|
+
: commandWordBase === "exec"
|
|
4882
|
+
? findExecDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4883
|
+
: commandWordBase === "time"
|
|
4884
|
+
? findTimeDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4885
|
+
: commandWordBase === "timeout"
|
|
4886
|
+
? findTimeoutDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4887
|
+
: commandWordBase === "nohup" || commandWordBase === "setsid"
|
|
4888
|
+
? findCommandDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4889
|
+
: commandWordBase === "coproc"
|
|
4890
|
+
? findCoprocDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4891
|
+
: commandWordBase === "xargs"
|
|
4892
|
+
? findXargsDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4893
|
+
: commandWordBase === "nice"
|
|
4894
|
+
? findNiceDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4895
|
+
: commandWordBase === "stdbuf"
|
|
4896
|
+
? findStdbufDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4897
|
+
: null;
|
|
4898
|
+
if (operandIndex === null)
|
|
4899
|
+
return commandWordIndex;
|
|
4900
|
+
const nextCommandWordIndex = skipShellCommandPositionPrefixWords(words, operandIndex);
|
|
4901
|
+
if (nextCommandWordIndex === commandWordIndex)
|
|
4902
|
+
return commandWordIndex;
|
|
4903
|
+
commandWordIndex = nextCommandWordIndex;
|
|
4904
|
+
}
|
|
4905
|
+
return null;
|
|
4906
|
+
}
|
|
4907
|
+
function readOmxStateCommandFromSegmentWords(words, operation) {
|
|
4908
|
+
let commandWordIndex = skipShellCommandPositionPrefixWords(words, 0);
|
|
4909
|
+
for (let unwrapCount = 0; unwrapCount < 8; unwrapCount += 1) {
|
|
4910
|
+
commandWordIndex = skipShellCommandPositionPrefixWords(words, commandWordIndex);
|
|
4911
|
+
const caseArmCommandIndex = findCaseArmCommandIndex(words, commandWordIndex);
|
|
4912
|
+
if (caseArmCommandIndex !== null) {
|
|
4913
|
+
commandWordIndex = skipShellCommandPositionPrefixWords(words, caseArmCommandIndex);
|
|
4914
|
+
}
|
|
4915
|
+
const directArgs = readOmxStateCommandArgsFromWords(words.slice(commandWordIndex), operation);
|
|
4916
|
+
if (directArgs) {
|
|
4917
|
+
return {
|
|
4918
|
+
args: directArgs,
|
|
4919
|
+
commandWords: words.slice(commandWordIndex),
|
|
4920
|
+
prefixWords: words.slice(0, commandWordIndex),
|
|
4921
|
+
};
|
|
4922
|
+
}
|
|
4923
|
+
const commandWord = words[commandWordIndex] ?? "";
|
|
4924
|
+
const operandIndex = shellWordBaseName(commandWord) === "env"
|
|
4925
|
+
? findEnvDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4926
|
+
: shellWordBaseName(commandWord) === "command"
|
|
4927
|
+
? findCommandDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4928
|
+
: shellWordBaseName(commandWord) === "exec"
|
|
4929
|
+
? findExecDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4930
|
+
: shellWordBaseName(commandWord) === "nohup"
|
|
4931
|
+
? findCommandDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4932
|
+
: shellWordBaseName(commandWord) === "timeout"
|
|
4933
|
+
? findTimeoutDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4934
|
+
: shellWordBaseName(commandWord) === "coproc"
|
|
4935
|
+
? findCoprocDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4936
|
+
: shellWordBaseName(commandWord) === "xargs"
|
|
4937
|
+
? findXargsDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4938
|
+
: shellWordBaseName(commandWord) === "time"
|
|
4939
|
+
? findTimeDispatchOperandIndex(words, commandWordIndex + 1)
|
|
4940
|
+
: null;
|
|
4941
|
+
if (operandIndex === null)
|
|
4942
|
+
return null;
|
|
4943
|
+
commandWordIndex = operandIndex;
|
|
4944
|
+
}
|
|
4945
|
+
return null;
|
|
4946
|
+
}
|
|
4947
|
+
function splitStateScanSegments(command) {
|
|
4948
|
+
const segments = [];
|
|
4949
|
+
let current = "";
|
|
4950
|
+
let segmentStart = 0;
|
|
4951
|
+
let quote = null;
|
|
4952
|
+
const pushSegment = (endIndex) => {
|
|
4953
|
+
if (current.trim()) {
|
|
4954
|
+
segments.push({
|
|
4955
|
+
segment: current,
|
|
4956
|
+
prefix: command.slice(0, segmentStart),
|
|
4957
|
+
});
|
|
4958
|
+
}
|
|
4959
|
+
current = "";
|
|
4960
|
+
segmentStart = endIndex;
|
|
4961
|
+
};
|
|
4962
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
4963
|
+
const char = command[index] ?? "";
|
|
4964
|
+
if (char === "'" || char === "\"") {
|
|
4965
|
+
if (quote === char) {
|
|
4966
|
+
quote = null;
|
|
4967
|
+
}
|
|
4968
|
+
else if (!quote) {
|
|
4969
|
+
quote = char;
|
|
4970
|
+
}
|
|
4971
|
+
current += char;
|
|
4972
|
+
continue;
|
|
4973
|
+
}
|
|
4974
|
+
if (char === "\\" && quote !== "'") {
|
|
4975
|
+
current += char;
|
|
4976
|
+
index += 1;
|
|
4977
|
+
current += command[index] ?? "";
|
|
4978
|
+
continue;
|
|
4979
|
+
}
|
|
4980
|
+
if (quote) {
|
|
4981
|
+
current += char;
|
|
4982
|
+
continue;
|
|
4983
|
+
}
|
|
4984
|
+
if (char === ";" || char === "\n" || char === "\r") {
|
|
4985
|
+
pushSegment(index + 1);
|
|
4986
|
+
continue;
|
|
4987
|
+
}
|
|
4988
|
+
if (char === "&" && command[index + 1] === "&") {
|
|
4989
|
+
pushSegment(index + 2);
|
|
4990
|
+
index += 1;
|
|
4991
|
+
continue;
|
|
4992
|
+
}
|
|
4993
|
+
if (char === "&") {
|
|
4994
|
+
const previous = command[index - 1] ?? "";
|
|
4995
|
+
const next = command[index + 1] ?? "";
|
|
4996
|
+
if (previous === ">" || previous === "<" || next === ">") {
|
|
4997
|
+
current += char;
|
|
4998
|
+
}
|
|
4999
|
+
else {
|
|
5000
|
+
pushSegment(index + 1);
|
|
5001
|
+
}
|
|
5002
|
+
continue;
|
|
5003
|
+
}
|
|
5004
|
+
if (char === "|" && command[index + 1] === "|") {
|
|
5005
|
+
pushSegment(index + 2);
|
|
5006
|
+
index += 1;
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
if (char === "|") {
|
|
5010
|
+
const next = command[index + 1] ?? "";
|
|
5011
|
+
if (next === "&") {
|
|
5012
|
+
pushSegment(index + 2);
|
|
5013
|
+
index += 1;
|
|
5014
|
+
}
|
|
5015
|
+
else {
|
|
5016
|
+
pushSegment(index + 1);
|
|
5017
|
+
}
|
|
5018
|
+
continue;
|
|
5019
|
+
}
|
|
5020
|
+
current += char;
|
|
5021
|
+
}
|
|
5022
|
+
if (current.trim()) {
|
|
5023
|
+
segments.push({
|
|
5024
|
+
segment: current,
|
|
5025
|
+
prefix: command.slice(0, segmentStart),
|
|
5026
|
+
});
|
|
5027
|
+
}
|
|
5028
|
+
return segments.length > 0 ? segments : [{ segment: command, prefix: "" }];
|
|
5029
|
+
}
|
|
5030
|
+
function collectOmxStateCommandOperations(command, operation, nested = false) {
|
|
5031
|
+
command = normalizeShellLineContinuations(stripHeredocBodiesForCommandScan(command));
|
|
5032
|
+
const operations = [];
|
|
5033
|
+
const addOperation = (candidate) => {
|
|
5034
|
+
operations.push(candidate);
|
|
5035
|
+
};
|
|
5036
|
+
const scanNestedSubstitutions = () => {
|
|
5037
|
+
let quote = null;
|
|
5038
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
5039
|
+
const char = command[index];
|
|
5040
|
+
if (char === "\\" && quote !== "'") {
|
|
5041
|
+
index += 1;
|
|
5042
|
+
continue;
|
|
5043
|
+
}
|
|
5044
|
+
if (quote !== "'" && char === "$" && command[index + 1] === "(") {
|
|
5045
|
+
const substitutionEnd = findCommandSubstitutionEnd(command, index + 2);
|
|
5046
|
+
const substitutionBodyEnd = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5047
|
+
const substitutionBody = command.slice(index + 2, substitutionBodyEnd);
|
|
5048
|
+
for (const nestedOperation of collectOmxStateCommandOperations(substitutionBody, operation, true)) {
|
|
5049
|
+
addOperation(nestedOperation);
|
|
5050
|
+
}
|
|
5051
|
+
index = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5052
|
+
continue;
|
|
5053
|
+
}
|
|
5054
|
+
if (quote !== "'" && char === "<" && command[index + 1] === "(") {
|
|
5055
|
+
const substitutionEnd = findProcessSubstitutionEnd(command, index + 2);
|
|
5056
|
+
const substitutionBodyEnd = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5057
|
+
const substitutionBody = command.slice(index + 2, substitutionBodyEnd);
|
|
5058
|
+
for (const nestedOperation of collectOmxStateCommandOperations(substitutionBody, operation, true)) {
|
|
5059
|
+
addOperation(nestedOperation);
|
|
5060
|
+
}
|
|
5061
|
+
index = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5062
|
+
continue;
|
|
5063
|
+
}
|
|
5064
|
+
if (quote !== "'" && char === ">" && command[index + 1] === "(") {
|
|
5065
|
+
const substitutionEnd = findProcessSubstitutionEnd(command, index + 2);
|
|
5066
|
+
const substitutionBodyEnd = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5067
|
+
const substitutionBody = command.slice(index + 2, substitutionBodyEnd);
|
|
5068
|
+
for (const nestedOperation of collectOmxStateCommandOperations(substitutionBody, operation, true)) {
|
|
5069
|
+
addOperation(nestedOperation);
|
|
5070
|
+
}
|
|
5071
|
+
index = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5072
|
+
continue;
|
|
5073
|
+
}
|
|
5074
|
+
if (quote !== "'" && char === "`") {
|
|
5075
|
+
const substitutionEnd = findBacktickCommandSubstitutionEnd(command, index + 1);
|
|
5076
|
+
const substitutionBodyEnd = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5077
|
+
const substitutionBody = command.slice(index + 1, substitutionBodyEnd);
|
|
5078
|
+
for (const nestedOperation of collectOmxStateCommandOperations(substitutionBody, operation, true)) {
|
|
5079
|
+
addOperation(nestedOperation);
|
|
5080
|
+
}
|
|
5081
|
+
index = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5082
|
+
continue;
|
|
5083
|
+
}
|
|
5084
|
+
if (char === "'" || char === "\"") {
|
|
5085
|
+
if (quote === char) {
|
|
5086
|
+
quote = null;
|
|
5087
|
+
}
|
|
5088
|
+
else if (!quote) {
|
|
5089
|
+
quote = char;
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
};
|
|
5094
|
+
scanNestedSubstitutions();
|
|
5095
|
+
for (const functionBody of extractInvokedShellFunctionBodiesForStateScan(command)) {
|
|
5096
|
+
for (const nestedOperation of collectOmxStateCommandOperations(functionBody, operation, true)) {
|
|
5097
|
+
addOperation(nestedOperation);
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
for (const scanSegment of splitStateScanSegments(command)) {
|
|
5101
|
+
const words = tokenizeShellWords(scanSegment.segment);
|
|
5102
|
+
const stateCommand = readOmxStateCommandFromSegmentWords(words, operation);
|
|
5103
|
+
if (stateCommand) {
|
|
5104
|
+
addOperation({
|
|
5105
|
+
args: stateCommand.args,
|
|
5106
|
+
prefix: scanSegment.prefix,
|
|
5107
|
+
commandPrefix: stateCommand.prefixWords.join(" "),
|
|
5108
|
+
nested,
|
|
5109
|
+
});
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
for (const nestedCommand of extractNestedShellCommandStringsForStateScan(command)) {
|
|
5113
|
+
for (const nestedOperation of collectOmxStateCommandOperations(nestedCommand, operation, true)) {
|
|
5114
|
+
addOperation(nestedOperation);
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
return operations;
|
|
5118
|
+
}
|
|
5119
|
+
function extractInvokedShellFunctionBodiesForStateScan(command) {
|
|
5120
|
+
const bodies = [];
|
|
5121
|
+
command = stripHeredocBodiesForCommandScan(normalizeShellLineContinuations(command));
|
|
5122
|
+
let quote = null;
|
|
5123
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
5124
|
+
const char = command[index] ?? "";
|
|
5125
|
+
if (char === "\\" && quote !== "'") {
|
|
5126
|
+
index += 1;
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
5129
|
+
if (char === "'" || char === "\"") {
|
|
5130
|
+
if (quote === char) {
|
|
5131
|
+
quote = null;
|
|
5132
|
+
}
|
|
5133
|
+
else if (!quote) {
|
|
5134
|
+
quote = char;
|
|
5135
|
+
}
|
|
5136
|
+
continue;
|
|
5137
|
+
}
|
|
5138
|
+
if (quote)
|
|
5139
|
+
continue;
|
|
5140
|
+
const functionDefinition = findShellFunctionDefinitionAt(command, index);
|
|
5141
|
+
if (!functionDefinition)
|
|
5142
|
+
continue;
|
|
5143
|
+
const functionBodyEnd = findShellFunctionBodyEnd(command, functionDefinition.openBraceIndex, functionDefinition.bodyOpenChar);
|
|
5144
|
+
if (functionBodyEnd < 0)
|
|
5145
|
+
continue;
|
|
5146
|
+
if (isShellFunctionInvokedLater(command.slice(functionBodyEnd + 1), functionDefinition.name)) {
|
|
5147
|
+
bodies.push(command.slice(functionDefinition.openBraceIndex + 1, functionBodyEnd));
|
|
5148
|
+
}
|
|
5149
|
+
index = functionBodyEnd;
|
|
5150
|
+
}
|
|
5151
|
+
return bodies;
|
|
5152
|
+
}
|
|
5153
|
+
function findUnquotedOmxStateCommandIndexes(command, operation) {
|
|
5154
|
+
return collectOmxStateCommandOperations(command, operation).map((_, index) => index);
|
|
5155
|
+
}
|
|
5156
|
+
function extractNestedShellCommandStringsForStateScan(command) {
|
|
5157
|
+
const words = tokenizeShellWords(stripHeredocBodiesForCommandScan(command));
|
|
5158
|
+
const nested = [];
|
|
5159
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
5160
|
+
const word = words[index] ?? "";
|
|
5161
|
+
if (isNestedShellCommandWord(word)) {
|
|
5162
|
+
const commandStringIndex = findShellCommandStringArgIndex(words, index + 1);
|
|
5163
|
+
if (commandStringIndex !== null) {
|
|
5164
|
+
const nestedCommand = words[commandStringIndex];
|
|
5165
|
+
if (nestedCommand) {
|
|
5166
|
+
nested.push(nestedCommand);
|
|
5167
|
+
const positionalArgs = words.slice(commandStringIndex + 1);
|
|
5168
|
+
const expandedNestedCommand = expandShellPositionalParameters(nestedCommand, positionalArgs);
|
|
5169
|
+
if (expandedNestedCommand !== nestedCommand)
|
|
5170
|
+
nested.push(expandedNestedCommand);
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
if (word === "eval") {
|
|
5175
|
+
const nestedCommand = words.slice(index + 1).join(" ");
|
|
5176
|
+
if (nestedCommand)
|
|
5177
|
+
nested.push(nestedCommand);
|
|
5178
|
+
}
|
|
5179
|
+
if (shellWordBaseName(word) === "env") {
|
|
5180
|
+
const splitStringCommand = extractEnvSplitStringCommand(words, index + 1);
|
|
5181
|
+
if (splitStringCommand) {
|
|
5182
|
+
nested.push(splitStringCommand);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
else if (isPackageManagerCommandWord(word)) {
|
|
5186
|
+
const packageManagerCommand = extractPackageManagerExecCommand(command, words, index + 1, shellWordBaseName(word));
|
|
5187
|
+
if (packageManagerCommand) {
|
|
5188
|
+
nested.push(packageManagerCommand);
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
return nested;
|
|
5193
|
+
}
|
|
5194
|
+
function extractNestedCommandSubstitutionStringsForStateScan(command) {
|
|
5195
|
+
const nested = [];
|
|
5196
|
+
let quote = null;
|
|
5197
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
5198
|
+
const char = command[index];
|
|
5199
|
+
if (char === "\\" && quote !== "'") {
|
|
5200
|
+
index += 1;
|
|
5201
|
+
continue;
|
|
5202
|
+
}
|
|
5203
|
+
if (quote !== "'" && char === "$" && command[index + 1] === "(") {
|
|
5204
|
+
const substitutionEnd = findCommandSubstitutionEnd(command, index + 2);
|
|
5205
|
+
const substitutionBodyEnd = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5206
|
+
const substitutionBody = command.slice(index + 2, substitutionBodyEnd);
|
|
5207
|
+
if (substitutionBody)
|
|
5208
|
+
nested.push(substitutionBody);
|
|
5209
|
+
index = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5210
|
+
continue;
|
|
5211
|
+
}
|
|
5212
|
+
if (quote !== "'" && char === "`") {
|
|
5213
|
+
const substitutionEnd = findBacktickCommandSubstitutionEnd(command, index + 1);
|
|
5214
|
+
const substitutionBodyEnd = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5215
|
+
const substitutionBody = command.slice(index + 1, substitutionBodyEnd);
|
|
5216
|
+
if (substitutionBody)
|
|
5217
|
+
nested.push(substitutionBody);
|
|
5218
|
+
index = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5219
|
+
continue;
|
|
5220
|
+
}
|
|
5221
|
+
if (char === "'" || char === "\"") {
|
|
5222
|
+
if (quote === char) {
|
|
5223
|
+
quote = null;
|
|
5224
|
+
}
|
|
5225
|
+
else if (!quote) {
|
|
5226
|
+
quote = char;
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
return nested;
|
|
5231
|
+
}
|
|
5232
|
+
function extractNestedProcessSubstitutionStringsForStateScan(command) {
|
|
5233
|
+
const nested = [];
|
|
5234
|
+
let quote = null;
|
|
5235
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
5236
|
+
const char = command[index];
|
|
5237
|
+
if (char === "\\" && quote !== "'") {
|
|
5238
|
+
index += 1;
|
|
5239
|
+
continue;
|
|
5240
|
+
}
|
|
5241
|
+
if (quote !== "'" && (char === "<" || char === ">") && command[index + 1] === "(") {
|
|
5242
|
+
const substitutionEnd = findProcessSubstitutionEnd(command, index + 2);
|
|
5243
|
+
const substitutionBodyEnd = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5244
|
+
const substitutionBody = command.slice(index + 2, substitutionBodyEnd);
|
|
5245
|
+
if (substitutionBody)
|
|
5246
|
+
nested.push(substitutionBody);
|
|
5247
|
+
index = substitutionEnd >= 0 ? substitutionEnd : command.length;
|
|
5248
|
+
continue;
|
|
5249
|
+
}
|
|
5250
|
+
if (char === "'" || char === "\"") {
|
|
5251
|
+
if (quote === char) {
|
|
5252
|
+
quote = null;
|
|
5253
|
+
}
|
|
5254
|
+
else if (!quote) {
|
|
5255
|
+
quote = char;
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
return nested;
|
|
5260
|
+
}
|
|
5261
|
+
function isPackageManagerCommandWord(word) {
|
|
5262
|
+
const base = shellWordBaseName(word);
|
|
5263
|
+
return base === "npm" || base === "pnpm" || base === "yarn" || base === "npx" || base === "pnpx";
|
|
5264
|
+
}
|
|
5265
|
+
function hasDynamicNestedShellExecution(command) {
|
|
5266
|
+
const commands = splitShellCommandSegments(stripHeredocBodiesForCommandScan(command));
|
|
5267
|
+
for (const segment of commands) {
|
|
5268
|
+
const words = tokenizeShellWords(segment);
|
|
5269
|
+
let sawCommandWord = false;
|
|
5270
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
5271
|
+
const word = words[index] ?? "";
|
|
5272
|
+
if (!sawCommandWord) {
|
|
5273
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word))
|
|
5274
|
+
continue;
|
|
5275
|
+
sawCommandWord = true;
|
|
5276
|
+
if (isDynamicNestedCommandString(word))
|
|
5277
|
+
return true;
|
|
5278
|
+
}
|
|
5279
|
+
if (isCommandDispatchBuiltin(word)) {
|
|
5280
|
+
const dispatchedCommand = extractDispatchBuiltinOperand(words, index + 1, word);
|
|
5281
|
+
if (dispatchedCommand && hasDynamicNestedShellExecution(dispatchedCommand))
|
|
5282
|
+
return true;
|
|
5283
|
+
}
|
|
5284
|
+
if (containsUnquotedProcessSubstitution(segment) && (isNestedShellCommandWord(word) || word === "." || word === "source"))
|
|
5285
|
+
return true;
|
|
5286
|
+
if (isNestedShellCommandWord(word)) {
|
|
5287
|
+
const commandStringIndex = findShellCommandStringArgIndex(words, index + 1);
|
|
5288
|
+
if (commandStringIndex === null && (hasUnquotedShellStdinFlowAroundShellWord(words, index) || segment.includes("<<")))
|
|
5289
|
+
return true;
|
|
5290
|
+
if (commandStringIndex !== null) {
|
|
5291
|
+
const nestedCommand = words[commandStringIndex] ?? "";
|
|
5292
|
+
if (isDynamicNestedCommandString(nestedCommand))
|
|
5293
|
+
return true;
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
if (word === "eval") {
|
|
5297
|
+
const nestedCommand = words.slice(index + 1).join(" ");
|
|
5298
|
+
if (isDynamicNestedCommandString(nestedCommand))
|
|
5299
|
+
return true;
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
return false;
|
|
5304
|
+
}
|
|
5305
|
+
function isCommandDispatchBuiltin(word) {
|
|
5306
|
+
return word === "exec" || word === "command" || word === "." || word === "source" || word === "env" || word === "nohup";
|
|
5307
|
+
}
|
|
5308
|
+
function extractDispatchBuiltinOperand(words, startIndex, builtin) {
|
|
5309
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
5310
|
+
const option = words[index] ?? "";
|
|
5311
|
+
if (!option)
|
|
5312
|
+
continue;
|
|
5313
|
+
if (option === "--")
|
|
5314
|
+
continue;
|
|
5315
|
+
if (isShellAssignmentWord(option))
|
|
5316
|
+
continue;
|
|
5317
|
+
if (builtin === "exec" && option === "-a") {
|
|
5318
|
+
index += 1;
|
|
5319
|
+
continue;
|
|
5320
|
+
}
|
|
5321
|
+
if (builtin === "env" && (option === "-S" || option === "--split-string")) {
|
|
5322
|
+
const splitOperand = words[index + 1] ?? "";
|
|
5323
|
+
return splitOperand;
|
|
5324
|
+
}
|
|
5325
|
+
if (builtin === "env" && option.startsWith("--split-string=")) {
|
|
5326
|
+
return option.slice("--split-string=".length);
|
|
5327
|
+
}
|
|
5328
|
+
if (builtin === "env" && option.startsWith("-S") && option.length > 2) {
|
|
5329
|
+
return option.slice(2);
|
|
5330
|
+
}
|
|
5331
|
+
if (option.startsWith("-"))
|
|
5332
|
+
continue;
|
|
5333
|
+
return words.slice(index).join(" ");
|
|
5334
|
+
}
|
|
5335
|
+
return "";
|
|
5336
|
+
}
|
|
5337
|
+
function isNestedShellCommandWord(word) {
|
|
5338
|
+
const base = word.split(/[\\/]/).pop() ?? word;
|
|
5339
|
+
return /^(?:bash|sh|zsh|dash|ksh|mksh|ash)$/.test(base);
|
|
5340
|
+
}
|
|
5341
|
+
function findShellCommandStringArgIndex(words, optionStartIndex) {
|
|
5342
|
+
for (let index = optionStartIndex; index < words.length; index += 1) {
|
|
5343
|
+
const option = words[index] ?? "";
|
|
5344
|
+
if (option === "--")
|
|
5345
|
+
return null;
|
|
5346
|
+
if (isShellCommandStringOption(option))
|
|
5347
|
+
return index + 1;
|
|
5348
|
+
if (isShellOptionWithSeparateValue(option)) {
|
|
5349
|
+
index += 1;
|
|
5350
|
+
continue;
|
|
5351
|
+
}
|
|
5352
|
+
if (option.startsWith("-"))
|
|
5353
|
+
continue;
|
|
5354
|
+
return null;
|
|
5355
|
+
}
|
|
5356
|
+
return null;
|
|
5357
|
+
}
|
|
5358
|
+
function isShellCommandStringOption(option) {
|
|
5359
|
+
return /^-[^-]*c[^-]*$/.test(option);
|
|
5360
|
+
}
|
|
5361
|
+
function isShellOptionWithSeparateValue(option) {
|
|
5362
|
+
return option === "--rcfile" || option === "--init-file" || option === "-o" || option === "-O";
|
|
5363
|
+
}
|
|
5364
|
+
function isDynamicNestedCommandString(command) {
|
|
5365
|
+
return /(?:^|[^\\])\$(?:[A-Za-z_][A-Za-z0-9_]*|\{[^}]+\}|\()/.test(command)
|
|
5366
|
+
|| /(?:^|[^\\])`/.test(command);
|
|
5367
|
+
}
|
|
5368
|
+
function shellQuoteForStateScan(value) {
|
|
5369
|
+
if (value === "")
|
|
5370
|
+
return "''";
|
|
5371
|
+
return `'${value.replace(/'/g, "'\"'\"'")}'`;
|
|
5372
|
+
}
|
|
5373
|
+
function expandShellPositionalParameters(command, positionalArgs) {
|
|
5374
|
+
if (!command.includes("$") || positionalArgs.length === 0)
|
|
5375
|
+
return command;
|
|
5376
|
+
let expanded = "";
|
|
5377
|
+
let replaced = false;
|
|
5378
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
5379
|
+
const char = command[index] ?? "";
|
|
5380
|
+
if (char === "\\" && index + 1 < command.length) {
|
|
5381
|
+
expanded += char;
|
|
5382
|
+
index += 1;
|
|
5383
|
+
expanded += command[index] ?? "";
|
|
5384
|
+
continue;
|
|
5385
|
+
}
|
|
5386
|
+
if (char !== "$") {
|
|
5387
|
+
expanded += char;
|
|
5388
|
+
continue;
|
|
5389
|
+
}
|
|
5390
|
+
const next = command[index + 1] ?? "";
|
|
5391
|
+
if (next === "{") {
|
|
5392
|
+
let endIndex = index + 2;
|
|
5393
|
+
let digits = "";
|
|
5394
|
+
while (endIndex < command.length) {
|
|
5395
|
+
const bodyChar = command[endIndex] ?? "";
|
|
5396
|
+
if (bodyChar === "}")
|
|
5397
|
+
break;
|
|
5398
|
+
digits += bodyChar;
|
|
5399
|
+
endIndex += 1;
|
|
5400
|
+
}
|
|
5401
|
+
if (command[endIndex] === "}" && /^\d+$/.test(digits)) {
|
|
5402
|
+
const positionalIndex = Number.parseInt(digits, 10);
|
|
5403
|
+
const replacement = positionalArgs[positionalIndex];
|
|
5404
|
+
if (replacement !== undefined) {
|
|
5405
|
+
expanded += shellQuoteForStateScan(replacement);
|
|
5406
|
+
replaced = true;
|
|
5407
|
+
index = endIndex;
|
|
5408
|
+
continue;
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
if (command[endIndex] === "}" && (digits === "@" || digits === "*")) {
|
|
5412
|
+
const expansionArgs = positionalArgs.slice(1);
|
|
5413
|
+
const replacement = expansionArgs.map((arg) => shellQuoteForStateScan(arg)).join(" ");
|
|
5414
|
+
expanded += replacement;
|
|
5415
|
+
replaced = true;
|
|
5416
|
+
index = endIndex;
|
|
5417
|
+
continue;
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
else if (next === "@" || next === "*") {
|
|
5421
|
+
const replacement = positionalArgs.slice(1).map((arg) => shellQuoteForStateScan(arg)).join(" ");
|
|
5422
|
+
if (replacement) {
|
|
5423
|
+
expanded += replacement;
|
|
5424
|
+
replaced = true;
|
|
5425
|
+
index += 1;
|
|
5426
|
+
continue;
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
else if (/[0-9]/.test(next)) {
|
|
5430
|
+
let endIndex = index + 1;
|
|
5431
|
+
let digits = "";
|
|
5432
|
+
while (endIndex < command.length && /[0-9]/.test(command[endIndex] ?? "")) {
|
|
5433
|
+
digits += command[endIndex] ?? "";
|
|
5434
|
+
endIndex += 1;
|
|
5435
|
+
}
|
|
5436
|
+
const positionalIndex = Number.parseInt(digits, 10);
|
|
5437
|
+
const replacement = positionalArgs[positionalIndex];
|
|
5438
|
+
if (replacement !== undefined) {
|
|
5439
|
+
expanded += shellQuoteForStateScan(replacement);
|
|
5440
|
+
replaced = true;
|
|
5441
|
+
index = endIndex - 1;
|
|
5442
|
+
continue;
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
expanded += char;
|
|
5446
|
+
}
|
|
5447
|
+
return replaced ? expanded : command;
|
|
5448
|
+
}
|
|
5449
|
+
function splitShellCommandSegments(command) {
|
|
5450
|
+
const segments = [];
|
|
5451
|
+
let current = "";
|
|
5452
|
+
let quote = null;
|
|
5453
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
5454
|
+
const char = command[index] ?? "";
|
|
5455
|
+
if (char === "'" || char === "\"") {
|
|
5456
|
+
if (quote === char) {
|
|
5457
|
+
quote = null;
|
|
5458
|
+
}
|
|
5459
|
+
else if (!quote) {
|
|
5460
|
+
quote = char;
|
|
5461
|
+
}
|
|
5462
|
+
current += char;
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
if (char === "\\" && quote !== "'") {
|
|
5466
|
+
current += char;
|
|
5467
|
+
index += 1;
|
|
5468
|
+
current += command[index] ?? "";
|
|
5469
|
+
continue;
|
|
5470
|
+
}
|
|
5471
|
+
if (quote) {
|
|
5472
|
+
current += char;
|
|
5473
|
+
continue;
|
|
5474
|
+
}
|
|
5475
|
+
if (char === ";" || char === "\n" || char === "\r") {
|
|
5476
|
+
if (current.trim())
|
|
5477
|
+
segments.push(current);
|
|
5478
|
+
current = "";
|
|
5479
|
+
continue;
|
|
5480
|
+
}
|
|
5481
|
+
if (char === "&" && command[index - 1] !== "|") {
|
|
5482
|
+
if (current.trim())
|
|
5483
|
+
segments.push(current);
|
|
5484
|
+
current = "";
|
|
5485
|
+
if (command[index + 1] === "&")
|
|
5486
|
+
index += 1;
|
|
5487
|
+
continue;
|
|
5488
|
+
}
|
|
5489
|
+
if (char === "|" && command[index + 1] === "|") {
|
|
5490
|
+
if (current.trim())
|
|
5491
|
+
segments.push(current);
|
|
5492
|
+
current = "";
|
|
5493
|
+
index += 1;
|
|
5494
|
+
continue;
|
|
5495
|
+
}
|
|
5496
|
+
current += char;
|
|
5497
|
+
}
|
|
5498
|
+
if (current.trim())
|
|
5499
|
+
segments.push(current);
|
|
5500
|
+
return segments.length > 0 ? segments : [command];
|
|
5501
|
+
}
|
|
5502
|
+
function findDispatchWordIndex(words, startIndex) {
|
|
5503
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
5504
|
+
const token = words[index] ?? "";
|
|
5505
|
+
if (!token || token === "--")
|
|
5506
|
+
continue;
|
|
5507
|
+
if (isShellAssignmentWord(token))
|
|
5508
|
+
continue;
|
|
5509
|
+
return index;
|
|
5510
|
+
}
|
|
5511
|
+
return null;
|
|
5512
|
+
}
|
|
5513
|
+
function isShellAssignmentWord(word) {
|
|
5514
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(word);
|
|
5515
|
+
}
|
|
5516
|
+
function hasUnquotedShellStdinFlowAroundShellWord(words, wordIndex) {
|
|
5517
|
+
for (let index = 0; index < wordIndex; index += 1) {
|
|
5518
|
+
const word = words[index] ?? "";
|
|
5519
|
+
if (word === "|" || word === "|&")
|
|
5520
|
+
return true;
|
|
5521
|
+
if (word === "<" || word === "<<" || word === "<<<" || word.startsWith("<"))
|
|
5522
|
+
return true;
|
|
5523
|
+
}
|
|
5524
|
+
for (let index = wordIndex + 1; index < words.length; index += 1) {
|
|
5525
|
+
const word = words[index] ?? "";
|
|
5526
|
+
if (word === "<" || word === "<<" || word === "<<<" || word.startsWith("<"))
|
|
5527
|
+
return true;
|
|
5528
|
+
}
|
|
5529
|
+
return false;
|
|
5530
|
+
}
|
|
5531
|
+
function findCommandSubstitutionEnd(command, bodyStartIndex) {
|
|
5532
|
+
let depth = 1;
|
|
5533
|
+
let quote = null;
|
|
5534
|
+
for (let index = bodyStartIndex; index < command.length; index += 1) {
|
|
5535
|
+
const char = command[index];
|
|
5536
|
+
if (char === "\\" && quote !== "'") {
|
|
5537
|
+
index += 1;
|
|
5538
|
+
continue;
|
|
5539
|
+
}
|
|
5540
|
+
if (char === "'" || char === "\"") {
|
|
5541
|
+
if (quote === char) {
|
|
5542
|
+
quote = null;
|
|
5543
|
+
}
|
|
5544
|
+
else if (!quote) {
|
|
5545
|
+
quote = char;
|
|
5546
|
+
}
|
|
5547
|
+
continue;
|
|
5548
|
+
}
|
|
5549
|
+
if (quote)
|
|
5550
|
+
continue;
|
|
5551
|
+
if (char === "$" && command[index + 1] === "(") {
|
|
5552
|
+
depth += 1;
|
|
5553
|
+
index += 1;
|
|
5554
|
+
continue;
|
|
5555
|
+
}
|
|
5556
|
+
if (char === ")") {
|
|
5557
|
+
depth -= 1;
|
|
5558
|
+
if (depth === 0)
|
|
5559
|
+
return index;
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
return -1;
|
|
5563
|
+
}
|
|
5564
|
+
function findBacktickCommandSubstitutionEnd(command, bodyStartIndex) {
|
|
5565
|
+
for (let index = bodyStartIndex; index < command.length; index += 1) {
|
|
5566
|
+
const char = command[index];
|
|
5567
|
+
if (char === "\\") {
|
|
5568
|
+
index += 1;
|
|
5569
|
+
continue;
|
|
5570
|
+
}
|
|
5571
|
+
if (char === "`")
|
|
5572
|
+
return index;
|
|
5573
|
+
}
|
|
5574
|
+
return -1;
|
|
5575
|
+
}
|
|
5576
|
+
function findProcessSubstitutionEnd(command, bodyStartIndex) {
|
|
5577
|
+
let depth = 1;
|
|
5578
|
+
let quote = null;
|
|
5579
|
+
for (let index = bodyStartIndex; index < command.length; index += 1) {
|
|
5580
|
+
const char = command[index];
|
|
5581
|
+
if (char === "\\" && quote !== "'") {
|
|
5582
|
+
index += 1;
|
|
5583
|
+
continue;
|
|
5584
|
+
}
|
|
5585
|
+
if (char === "'" || char === "\"") {
|
|
5586
|
+
if (quote === char) {
|
|
5587
|
+
quote = null;
|
|
5588
|
+
}
|
|
5589
|
+
else if (!quote) {
|
|
5590
|
+
quote = char;
|
|
5591
|
+
}
|
|
5592
|
+
continue;
|
|
5593
|
+
}
|
|
5594
|
+
if (quote)
|
|
5595
|
+
continue;
|
|
5596
|
+
if (char === "(") {
|
|
5597
|
+
depth += 1;
|
|
5598
|
+
continue;
|
|
5599
|
+
}
|
|
5600
|
+
if (char === ")") {
|
|
5601
|
+
depth -= 1;
|
|
5602
|
+
if (depth === 0)
|
|
5603
|
+
return index;
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
return -1;
|
|
5607
|
+
}
|
|
5608
|
+
function containsUnquotedProcessSubstitution(command) {
|
|
5609
|
+
command = normalizeShellLineContinuations(command);
|
|
5610
|
+
let quote = null;
|
|
5611
|
+
for (let index = 0; index < command.length - 1; index += 1) {
|
|
5612
|
+
const char = command[index];
|
|
5613
|
+
if (char === "\\" && quote !== "'") {
|
|
5614
|
+
index += 1;
|
|
5615
|
+
continue;
|
|
5616
|
+
}
|
|
5617
|
+
if (char === "'" || char === "\"") {
|
|
5618
|
+
if (quote === char) {
|
|
5619
|
+
quote = null;
|
|
5620
|
+
}
|
|
5621
|
+
else if (!quote) {
|
|
5622
|
+
quote = char;
|
|
5623
|
+
}
|
|
5624
|
+
continue;
|
|
5625
|
+
}
|
|
5626
|
+
if (quote)
|
|
5627
|
+
continue;
|
|
5628
|
+
if ((char === "<" || char === ">") && command[index + 1] === "(")
|
|
5629
|
+
return true;
|
|
5630
|
+
}
|
|
5631
|
+
return false;
|
|
5632
|
+
}
|
|
5633
|
+
function parseShellHeredocDelimiter(line, startIndex) {
|
|
5634
|
+
let index = startIndex;
|
|
5635
|
+
while (/\s/.test(line[index] ?? ""))
|
|
5636
|
+
index += 1;
|
|
5637
|
+
let delimiter = "";
|
|
5638
|
+
let quoted = false;
|
|
5639
|
+
let quote = null;
|
|
5640
|
+
for (; index < line.length; index += 1) {
|
|
5641
|
+
const char = line[index] ?? "";
|
|
5642
|
+
if (char === "\\" && quote !== "'") {
|
|
5643
|
+
quoted = true;
|
|
5644
|
+
index += 1;
|
|
5645
|
+
delimiter += line[index] ?? "";
|
|
5646
|
+
continue;
|
|
5647
|
+
}
|
|
5648
|
+
if (char === "'" || char === "\"") {
|
|
5649
|
+
if (quote === char) {
|
|
5650
|
+
quote = null;
|
|
5651
|
+
}
|
|
5652
|
+
else if (!quote) {
|
|
5653
|
+
quote = char;
|
|
5654
|
+
quoted = true;
|
|
5655
|
+
}
|
|
5656
|
+
else {
|
|
5657
|
+
delimiter += char;
|
|
5658
|
+
}
|
|
5659
|
+
continue;
|
|
5660
|
+
}
|
|
5661
|
+
if (!quote && (/\s/.test(char) || char === "|" || char === ";" || char === "&" || char === "<" || char === ">"))
|
|
5662
|
+
break;
|
|
5663
|
+
delimiter += char;
|
|
5664
|
+
}
|
|
5665
|
+
return delimiter ? { delimiter, quoted } : null;
|
|
5666
|
+
}
|
|
5667
|
+
function extractShellHeredocOpeners(line) {
|
|
5668
|
+
const openers = [];
|
|
5669
|
+
let quote = null;
|
|
5670
|
+
for (let index = 0; index < line.length - 1; index += 1) {
|
|
5671
|
+
const char = line[index] ?? "";
|
|
5672
|
+
if (char === "\\" && quote !== "'") {
|
|
5673
|
+
index += 1;
|
|
5674
|
+
continue;
|
|
5675
|
+
}
|
|
5676
|
+
if (char === "'" || char === "\"") {
|
|
5677
|
+
if (quote === char) {
|
|
5678
|
+
quote = null;
|
|
5679
|
+
}
|
|
5680
|
+
else if (!quote) {
|
|
5681
|
+
quote = char;
|
|
5682
|
+
}
|
|
5683
|
+
continue;
|
|
5684
|
+
}
|
|
5685
|
+
if (quote)
|
|
5686
|
+
continue;
|
|
5687
|
+
if (char !== "<" || line[index + 1] !== "<" || line[index + 2] === "<")
|
|
5688
|
+
continue;
|
|
5689
|
+
const delimiterStart = line[index + 2] === "-" ? index + 3 : index + 2;
|
|
5690
|
+
const opener = parseShellHeredocDelimiter(line, delimiterStart);
|
|
5691
|
+
if (opener)
|
|
5692
|
+
openers.push(opener);
|
|
5693
|
+
index = delimiterStart;
|
|
5694
|
+
}
|
|
5695
|
+
return openers;
|
|
5696
|
+
}
|
|
5697
|
+
function stripHeredocBodiesForCommandScan(command) {
|
|
5698
|
+
const lines = command.split("\n");
|
|
5699
|
+
const kept = [];
|
|
5700
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
5701
|
+
const line = lines[index] ?? "";
|
|
5702
|
+
kept.push(line);
|
|
5703
|
+
const openers = extractShellHeredocOpeners(line);
|
|
5704
|
+
for (const { delimiter } of openers) {
|
|
5705
|
+
index += 1;
|
|
5706
|
+
while (index < lines.length && (lines[index] ?? "").trim() !== delimiter) {
|
|
5707
|
+
kept.push("");
|
|
5708
|
+
index += 1;
|
|
5709
|
+
}
|
|
5710
|
+
if (index < lines.length)
|
|
5711
|
+
kept.push(lines[index] ?? "");
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
return kept.join("\n");
|
|
5715
|
+
}
|
|
5716
|
+
function hasUnsafeUnquotedHeredocExpansion(command) {
|
|
5717
|
+
const lines = normalizeShellLineContinuations(command).split("\n");
|
|
5718
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
5719
|
+
const line = lines[index] ?? "";
|
|
5720
|
+
for (const { delimiter, quoted } of extractShellHeredocOpeners(line)) {
|
|
5721
|
+
index += 1;
|
|
5722
|
+
const bodyLines = [];
|
|
5723
|
+
while (index < lines.length && (lines[index] ?? "").trim() !== delimiter) {
|
|
5724
|
+
bodyLines.push(lines[index] ?? "");
|
|
5725
|
+
index += 1;
|
|
5726
|
+
}
|
|
5727
|
+
if (!quoted && isDynamicNestedCommandString(bodyLines.join("\n")))
|
|
5728
|
+
return true;
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
return false;
|
|
5732
|
+
}
|
|
5733
|
+
function hasUnquotedShellSubstitution(command) {
|
|
5734
|
+
command = normalizeShellLineContinuations(command);
|
|
5735
|
+
let quote = null;
|
|
5736
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
5737
|
+
const char = command[index] ?? "";
|
|
5738
|
+
if (char === "\\" && quote !== "'") {
|
|
5739
|
+
index += 1;
|
|
5740
|
+
continue;
|
|
5741
|
+
}
|
|
5742
|
+
if (char === "'" || char === "\"") {
|
|
5743
|
+
if (quote === char) {
|
|
5744
|
+
quote = null;
|
|
5745
|
+
}
|
|
5746
|
+
else if (!quote) {
|
|
5747
|
+
quote = char;
|
|
5748
|
+
}
|
|
5749
|
+
continue;
|
|
5750
|
+
}
|
|
5751
|
+
if (quote === "'")
|
|
5752
|
+
continue;
|
|
5753
|
+
if (char === "$" && command[index + 1] === "(")
|
|
5754
|
+
return true;
|
|
5755
|
+
if (char === "`")
|
|
5756
|
+
return true;
|
|
5757
|
+
if ((char === "<" || char === ">") && command[index + 1] === "(")
|
|
5758
|
+
return true;
|
|
5759
|
+
}
|
|
5760
|
+
return false;
|
|
5761
|
+
}
|
|
5762
|
+
function normalizeStateWriteClassificationPayload(payload) {
|
|
5763
|
+
const targetMode = safeString(payload.mode).trim();
|
|
5764
|
+
const targetSessionId = safeString(payload.session_id).trim();
|
|
5765
|
+
const { mode: _mode, workingDirectory: _workingDirectory, state, ...fields } = payload;
|
|
5766
|
+
const normalized = {
|
|
5767
|
+
...fields,
|
|
5768
|
+
...safeObject(state),
|
|
5769
|
+
...(targetMode ? { mode: targetMode } : {}),
|
|
5770
|
+
...(targetSessionId ? { session_id: targetSessionId } : {}),
|
|
5771
|
+
};
|
|
5772
|
+
if (normalized.current_phase === undefined && normalized.currentPhase !== undefined) {
|
|
5773
|
+
normalized.current_phase = normalized.currentPhase;
|
|
5774
|
+
}
|
|
5775
|
+
if (normalized.run_outcome === undefined && normalized.runOutcome !== undefined) {
|
|
5776
|
+
normalized.run_outcome = normalized.runOutcome;
|
|
5777
|
+
}
|
|
5778
|
+
if (normalized.lifecycle_outcome === undefined && normalized.lifecycleOutcome !== undefined) {
|
|
5779
|
+
normalized.lifecycle_outcome = normalized.lifecycleOutcome;
|
|
5780
|
+
}
|
|
5781
|
+
if (normalized.terminal_outcome === undefined && normalized.terminalOutcome !== undefined) {
|
|
5782
|
+
normalized.terminal_outcome = normalized.terminalOutcome;
|
|
5783
|
+
}
|
|
5784
|
+
return {
|
|
5785
|
+
...normalized,
|
|
5786
|
+
};
|
|
5787
|
+
}
|
|
5788
|
+
function isPlanningPhaseDeactivationPayload(payload) {
|
|
5789
|
+
const mode = safeString(payload.mode).trim().toLowerCase();
|
|
5790
|
+
if (!mode)
|
|
5791
|
+
return false;
|
|
5792
|
+
if (mode !== "deep-interview" && mode !== "ralplan") {
|
|
5793
|
+
if (!isTrackedWorkflowMode(mode))
|
|
5794
|
+
return false;
|
|
5795
|
+
const currentPhase = safeString(payload.current_phase ?? payload.currentPhase).trim().toLowerCase();
|
|
5796
|
+
const normalizedAutopilotPhase = normalizeAutopilotPhase(currentPhase);
|
|
5797
|
+
if (mode === "autopilot" && (normalizedAutopilotPhase === "deep-interview" || normalizedAutopilotPhase === "ralplan" || normalizedAutopilotPhase === "ultragoal")) {
|
|
5798
|
+
return payload.active === false;
|
|
5799
|
+
}
|
|
5800
|
+
if (payload.active === true)
|
|
5801
|
+
return true;
|
|
5802
|
+
return inferTerminalLifecycleOutcome(payload, { includeQuestionEnforcement: false }) === undefined;
|
|
5803
|
+
}
|
|
5804
|
+
if (payload.active === false)
|
|
5805
|
+
return true;
|
|
5806
|
+
return inferTerminalLifecycleOutcome(payload, { includeQuestionEnforcement: false }) !== undefined;
|
|
5807
|
+
}
|
|
5808
|
+
function hasCompleteDeepInterviewGateMetadata(state) {
|
|
5809
|
+
const nestedState = safeObject(state.state);
|
|
5810
|
+
const gate = safeObject(state.deep_interview_gate) ?? safeObject(nestedState?.deep_interview_gate);
|
|
5811
|
+
if (!gate)
|
|
5812
|
+
return false;
|
|
5813
|
+
const status = safeString(gate.status).trim().toLowerCase().replace(/_/g, "-");
|
|
5814
|
+
if (status === "complete" || gate.complete === true) {
|
|
5815
|
+
return ["rationale", "completion_rationale", "handoff_summary", "summary", "reason"]
|
|
5816
|
+
.some((key) => safeString(gate[key]).trim().length > 0);
|
|
5817
|
+
}
|
|
5818
|
+
if (status !== "skipped")
|
|
5819
|
+
return false;
|
|
5820
|
+
const reason = safeString(gate.reason).trim() || safeString(gate.skip_reason).trim() || safeString(gate.rationale).trim();
|
|
5821
|
+
const timestamp = safeString(gate.skipped_at).trim() || safeString(gate.timestamp).trim() || safeString(gate.updated_at).trim();
|
|
5822
|
+
const source = safeString(gate.source).trim();
|
|
5823
|
+
return (gate.skip_authorized_by_user === true || gate.authorized_by_user === true)
|
|
5824
|
+
&& reason.length > 0
|
|
5825
|
+
&& timestamp.length > 0
|
|
5826
|
+
&& source.length > 0;
|
|
5827
|
+
}
|
|
5828
|
+
function isDeepInterviewRalplanHandoffStatePayload(payload) {
|
|
5829
|
+
const mode = safeString(payload.mode).trim().toLowerCase();
|
|
5830
|
+
if (mode !== "autopilot")
|
|
5831
|
+
return false;
|
|
5832
|
+
const phase = normalizeAutopilotPhase(safeString(payload.current_phase ?? payload.currentPhase).trim().toLowerCase());
|
|
5833
|
+
if (phase !== "ralplan" || payload.active === false)
|
|
5834
|
+
return false;
|
|
5835
|
+
return hasCompleteDeepInterviewGateMetadata(payload);
|
|
5836
|
+
}
|
|
5837
|
+
function hasOnlyAllowedDeepInterviewRalplanHandoffMutations(cwd, command) {
|
|
5838
|
+
for (const mutation of extractConductorBashMutations(command)) {
|
|
5839
|
+
if (mutation.targets.length === 0)
|
|
5840
|
+
return false;
|
|
5841
|
+
if (mutation.targets.some((target) => !isAllowedDeepInterviewArtifactPath(cwd, target)))
|
|
5842
|
+
return false;
|
|
5843
|
+
}
|
|
5844
|
+
for (const write of extractConductorInterpreterWrites(command)) {
|
|
5845
|
+
if (write.unresolved || write.targets.length === 0)
|
|
5846
|
+
return false;
|
|
5847
|
+
if (write.targets.some((target) => !isAllowedDeepInterviewArtifactPath(cwd, target)))
|
|
5848
|
+
return false;
|
|
5849
|
+
}
|
|
5850
|
+
return true;
|
|
5851
|
+
}
|
|
5852
|
+
function isDurableDeepInterviewHandoffEvidencePath(cwd, rawPath) {
|
|
5853
|
+
const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
|
|
5854
|
+
if (!relativePath)
|
|
5855
|
+
return false;
|
|
5856
|
+
return relativePath === ".omx/context"
|
|
5857
|
+
|| relativePath.startsWith(".omx/context/")
|
|
5858
|
+
|| relativePath === ".omx/interviews"
|
|
5859
|
+
|| relativePath.startsWith(".omx/interviews/")
|
|
5860
|
+
|| relativePath === ".omx/specs"
|
|
5861
|
+
|| relativePath.startsWith(".omx/specs/");
|
|
5862
|
+
}
|
|
5863
|
+
function hasExistingDurableDeepInterviewHandoffEvidence(cwd) {
|
|
5864
|
+
const roots = [".omx/context", ".omx/interviews", ".omx/specs"];
|
|
5865
|
+
const maxEntries = 2_000;
|
|
5866
|
+
let visited = 0;
|
|
5867
|
+
for (const root of roots) {
|
|
5868
|
+
const stack = [resolve(cwd, root)];
|
|
5869
|
+
while (stack.length > 0 && visited < maxEntries) {
|
|
5870
|
+
const current = stack.pop();
|
|
5871
|
+
if (!current)
|
|
5872
|
+
continue;
|
|
5873
|
+
visited += 1;
|
|
5874
|
+
try {
|
|
5875
|
+
const stat = statSync(current);
|
|
5876
|
+
if (stat.isFile())
|
|
5877
|
+
return true;
|
|
5878
|
+
if (!stat.isDirectory())
|
|
5879
|
+
continue;
|
|
5880
|
+
for (const entry of readdirSync(current)) {
|
|
5881
|
+
stack.push(join(current, entry));
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
catch {
|
|
5885
|
+
// Missing or unreadable roots are not completion evidence.
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
return false;
|
|
5890
|
+
}
|
|
5891
|
+
function isAllowedDeepInterviewRalplanHandoffCommand(cwd, command) {
|
|
5892
|
+
const canonicalCommand = canonicalizeOmxStateTransportCommand(command);
|
|
5893
|
+
if (hasUnsafeUnquotedHeredocExpansion(canonicalCommand))
|
|
5894
|
+
return false;
|
|
5895
|
+
if (hasUnquotedShellSubstitution(canonicalCommand))
|
|
5896
|
+
return false;
|
|
5897
|
+
if (findUnquotedOmxStateCommandIndexes(canonicalCommand, "clear").length > 0)
|
|
5898
|
+
return false;
|
|
5899
|
+
if (hasDynamicNestedShellExecution(canonicalCommand))
|
|
5900
|
+
return false;
|
|
5901
|
+
if (commandHasUntargetedPlanningForbiddenIntent(canonicalCommand))
|
|
5902
|
+
return false;
|
|
5903
|
+
if (sourcesFileWrittenEarlierInSameCommand(cwd, canonicalCommand))
|
|
5904
|
+
return false;
|
|
5905
|
+
const stateWriteOperations = collectOmxStateCommandOperations(canonicalCommand, "write");
|
|
5906
|
+
if (stateWriteOperations.length !== 1)
|
|
5907
|
+
return false;
|
|
5908
|
+
const stateWriteOperation = stateWriteOperations[0];
|
|
5909
|
+
if (!stateWriteOperation || stateWriteOperation.nested)
|
|
5910
|
+
return false;
|
|
5911
|
+
const payload = readStateWriteInputPayload(cwd, canonicalCommand, command);
|
|
5912
|
+
if (!payload || !isDeepInterviewRalplanHandoffStatePayload(payload))
|
|
5913
|
+
return false;
|
|
5914
|
+
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
5915
|
+
if (targets.length === 0) {
|
|
5916
|
+
return !hasPriorExecutableCommand(stateWriteOperation.prefix)
|
|
5917
|
+
&& hasExistingDurableDeepInterviewHandoffEvidence(cwd);
|
|
5918
|
+
}
|
|
5919
|
+
if (!targets.some((target) => isDurableDeepInterviewHandoffEvidencePath(cwd, target)))
|
|
5920
|
+
return false;
|
|
5921
|
+
if (!hasOnlyAllowedDeepInterviewRalplanHandoffMutations(cwd, command))
|
|
5922
|
+
return false;
|
|
5923
|
+
return targets.every((target) => isAllowedDeepInterviewArtifactPath(cwd, target));
|
|
5924
|
+
}
|
|
5925
|
+
function hasDeepInterviewRalplanHandoffStateMutation(cwd, command) {
|
|
5926
|
+
const canonicalCommand = canonicalizeOmxStateTransportCommand(command);
|
|
5927
|
+
const stateWriteOperations = collectOmxStateCommandOperations(canonicalCommand, "write");
|
|
5928
|
+
if (stateWriteOperations.length === 0)
|
|
5929
|
+
return false;
|
|
5930
|
+
const payload = readStateWriteInputPayload(cwd, canonicalCommand, command);
|
|
5931
|
+
return payload ? isDeepInterviewRalplanHandoffStatePayload(payload) : false;
|
|
5932
|
+
}
|
|
5933
|
+
function isCompleteRalplanTerminalWritePayload(payload, activeState, sessionId) {
|
|
5934
|
+
if (!sessionId)
|
|
5935
|
+
return false;
|
|
5936
|
+
const mode = safeString(payload.mode).trim().toLowerCase();
|
|
5937
|
+
if (mode !== "ralplan")
|
|
5938
|
+
return false;
|
|
5939
|
+
const phase = safeString(payload.current_phase ?? payload.currentPhase).trim().toLowerCase();
|
|
5940
|
+
if (payload.active !== false || phase !== "complete")
|
|
5941
|
+
return false;
|
|
5942
|
+
const payloadSessionId = safeString(payload.session_id).trim();
|
|
5943
|
+
const activeSessionId = safeString(activeState.session_id
|
|
5944
|
+
?? activeState.owner_omx_session_id
|
|
5945
|
+
?? activeState.codex_session_id
|
|
5946
|
+
?? activeState.owner_codex_session_id).trim();
|
|
5947
|
+
if (payloadSessionId && sessionId && payloadSessionId !== sessionId)
|
|
5948
|
+
return false;
|
|
5949
|
+
if (payloadSessionId && activeSessionId && payloadSessionId !== activeSessionId)
|
|
5950
|
+
return false;
|
|
5951
|
+
return true;
|
|
5952
|
+
}
|
|
5953
|
+
function isAllowedRalplanTerminalStateWriteCommand(cwd, command, activeState, sessionId) {
|
|
5954
|
+
const canonicalCommand = canonicalizeOmxStateTransportCommand(command);
|
|
5955
|
+
if (hasUnsafeUnquotedHeredocExpansion(canonicalCommand))
|
|
5956
|
+
return false;
|
|
5957
|
+
if (hasUnquotedShellSubstitution(canonicalCommand))
|
|
5958
|
+
return false;
|
|
5959
|
+
if (splitStateScanSegments(canonicalCommand).length !== 1)
|
|
5960
|
+
return false;
|
|
5961
|
+
if (sourcesFileWrittenEarlierInSameCommand(cwd, canonicalCommand))
|
|
5962
|
+
return false;
|
|
5963
|
+
if (findUnquotedOmxStateCommandIndexes(canonicalCommand, "clear").length > 0)
|
|
5964
|
+
return false;
|
|
5965
|
+
if (hasDynamicNestedShellExecution(canonicalCommand))
|
|
5966
|
+
return false;
|
|
5967
|
+
if (commandHasDeepInterviewWriteIntent(canonicalCommand))
|
|
5968
|
+
return false;
|
|
5969
|
+
const operations = collectOmxStateCommandOperations(canonicalCommand, "write");
|
|
5970
|
+
if (operations.length !== 1)
|
|
5971
|
+
return false;
|
|
5972
|
+
const operation = operations[0];
|
|
5973
|
+
if (!operation || operation.nested || hasPriorExecutableCommand(operation.prefix))
|
|
5974
|
+
return false;
|
|
5975
|
+
const payload = readStateWriteInputPayload(cwd, canonicalCommand, command);
|
|
5976
|
+
return payload ? isCompleteRalplanTerminalWritePayload(payload, activeState, sessionId) : false;
|
|
5977
|
+
}
|
|
5978
|
+
function commandEndsPlanningPhase(cwd, command) {
|
|
5979
|
+
if (findUnquotedOmxStateCommandIndexes(command, "clear").length > 0)
|
|
5980
|
+
return true;
|
|
5981
|
+
const canonicalCommand = canonicalizeOmxStateTransportCommand(command);
|
|
5982
|
+
if (hasUnsafeUnquotedHeredocExpansion(canonicalCommand))
|
|
5983
|
+
return true;
|
|
5984
|
+
if (sourcesFileWrittenEarlierInSameCommand(cwd, canonicalCommand))
|
|
5985
|
+
return true;
|
|
5986
|
+
if (findUnquotedOmxStateCommandIndexes(canonicalCommand, "clear").length > 0)
|
|
5987
|
+
return true;
|
|
5988
|
+
if (hasDynamicNestedShellExecution(canonicalCommand))
|
|
5989
|
+
return true;
|
|
5990
|
+
const stateWriteCount = findUnquotedOmxStateCommandIndexes(canonicalCommand, "write").length;
|
|
5991
|
+
if (stateWriteCount > 1)
|
|
5992
|
+
return true;
|
|
5993
|
+
if (stateWriteCount === 0)
|
|
5994
|
+
return false;
|
|
5995
|
+
const payload = readStateWriteInputPayload(cwd, canonicalCommand, command);
|
|
5996
|
+
return payload ? isPlanningPhaseDeactivationPayload(payload) : true;
|
|
5997
|
+
}
|
|
5998
|
+
function isAllowedDeepInterviewBashWrite(cwd, command) {
|
|
5999
|
+
if (isAllowedDeepInterviewRalplanHandoffCommand(cwd, command))
|
|
6000
|
+
return true;
|
|
6001
|
+
if (hasDeepInterviewRalplanHandoffStateMutation(cwd, command))
|
|
6002
|
+
return false;
|
|
6003
|
+
if (commandEndsPlanningPhase(cwd, command))
|
|
6004
|
+
return false;
|
|
6005
|
+
if (commandHasUntargetedPlanningForbiddenIntent(command))
|
|
6006
|
+
return false;
|
|
6007
|
+
if (firstPlanningTmpScriptExecutionTarget(cwd, command))
|
|
6008
|
+
return false;
|
|
6009
|
+
if (!commandHasDeepInterviewWriteIntent(command))
|
|
6010
|
+
return true;
|
|
6011
|
+
if (hasUnresolvedConductorInterpreterWrite(command))
|
|
6012
|
+
return false;
|
|
6013
|
+
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
6014
|
+
if (targets.some((target) => !isAllowedDeepInterviewArtifactPath(cwd, target)))
|
|
6015
|
+
return false;
|
|
6016
|
+
return targets.length > 0 && targets.every((target) => isAllowedDeepInterviewArtifactPath(cwd, target));
|
|
6017
|
+
}
|
|
6018
|
+
async function readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId) {
|
|
6019
|
+
const canonicalState = sessionId
|
|
6020
|
+
? await readVisibleSkillActiveStateForStateDir(stateDir, sessionId)
|
|
6021
|
+
: await readSkillActiveState(join(stateDir, SKILL_ACTIVE_STATE_FILE));
|
|
6022
|
+
if (!canonicalState)
|
|
6023
|
+
return null;
|
|
6024
|
+
const modeState = sessionId
|
|
6025
|
+
? await readStopSessionPinnedState("deep-interview-state.json", cwd, sessionId, stateDir)
|
|
6026
|
+
: await readJsonIfExists(join(stateDir, "deep-interview-state.json"));
|
|
6027
|
+
if (isActiveDeepInterviewPhase(modeState) && modeState && modeStateMatchesSkillStopContext(modeState, cwd, sessionId)) {
|
|
6028
|
+
const hasActiveDeepInterviewSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "deep-interview"
|
|
6029
|
+
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
6030
|
+
if (hasActiveDeepInterviewSkill)
|
|
6031
|
+
return modeState;
|
|
6032
|
+
}
|
|
6033
|
+
const autopilotState = sessionId
|
|
6034
|
+
? await readStopSessionPinnedState("autopilot-state.json", cwd, sessionId, stateDir)
|
|
6035
|
+
: await readJsonIfExists(join(stateDir, "autopilot-state.json"));
|
|
6036
|
+
if (!autopilotState || autopilotState.active !== true)
|
|
6037
|
+
return null;
|
|
6038
|
+
const autopilotMode = safeString(autopilotState.mode).trim();
|
|
6039
|
+
if (autopilotMode && autopilotMode !== "autopilot")
|
|
6040
|
+
return null;
|
|
6041
|
+
if (!modeStateMatchesSkillStopContext(autopilotState, cwd, sessionId))
|
|
6042
|
+
return null;
|
|
6043
|
+
const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
|
|
6044
|
+
const autopilotIsDeepInterview = normalizeAutopilotPhase(autopilotStatePhase) === "deep-interview";
|
|
6045
|
+
const hasDeepInterviewScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "autopilot"
|
|
6046
|
+
&& normalizeAutopilotPhase(safeString(entry.phase).trim().toLowerCase()) === "deep-interview"
|
|
6047
|
+
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
6048
|
+
const hasActiveAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "autopilot"
|
|
6049
|
+
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
6050
|
+
if (!hasActiveAutopilotSkill)
|
|
6051
|
+
return null;
|
|
6052
|
+
if (!autopilotIsDeepInterview && !hasDeepInterviewScopedAutopilotSkill)
|
|
6053
|
+
return null;
|
|
6054
|
+
return autopilotState;
|
|
6055
|
+
}
|
|
6056
|
+
async function readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId) {
|
|
6057
|
+
const modeState = sessionId
|
|
6058
|
+
? await readStopSessionPinnedState("ralplan-state.json", cwd, sessionId, stateDir)
|
|
6059
|
+
: await readJsonIfExists(join(stateDir, "ralplan-state.json"));
|
|
6060
|
+
const canonicalState = sessionId
|
|
6061
|
+
? await readVisibleSkillActiveStateForStateDir(stateDir, sessionId)
|
|
6062
|
+
: await readSkillActiveState(join(stateDir, SKILL_ACTIVE_STATE_FILE));
|
|
6063
|
+
if (isActiveRalplanPhase(modeState) && modeState && modeStateMatchesSkillStopContext(modeState, cwd, sessionId)) {
|
|
6064
|
+
if (hasExplicitExecutionHandoffSkill(canonicalState, sessionId, threadId))
|
|
6065
|
+
return null;
|
|
6066
|
+
if (!canonicalState)
|
|
6067
|
+
return null;
|
|
6068
|
+
const hasActiveRalplanSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "ralplan"
|
|
6069
|
+
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
6070
|
+
if (hasActiveRalplanSkill)
|
|
6071
|
+
return modeState;
|
|
6072
|
+
}
|
|
6073
|
+
const autopilotState = sessionId
|
|
6074
|
+
? await readStopSessionPinnedState("autopilot-state.json", cwd, sessionId, stateDir)
|
|
6075
|
+
: await readJsonIfExists(join(stateDir, "autopilot-state.json"));
|
|
6076
|
+
if (!autopilotState || autopilotState.active !== true)
|
|
6077
|
+
return null;
|
|
6078
|
+
const autopilotMode = safeString(autopilotState.mode).trim();
|
|
6079
|
+
if (autopilotMode && autopilotMode !== "autopilot")
|
|
6080
|
+
return null;
|
|
6081
|
+
if (!modeStateMatchesSkillStopContext(autopilotState, cwd, sessionId))
|
|
6082
|
+
return null;
|
|
6083
|
+
if (!canonicalState)
|
|
6084
|
+
return null;
|
|
6085
|
+
const hasActiveAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "autopilot"
|
|
6086
|
+
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
6087
|
+
if (!hasActiveAutopilotSkill)
|
|
6088
|
+
return null;
|
|
6089
|
+
const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
|
|
6090
|
+
if (isAutopilotReviewReworkPhase(autopilotStatePhase))
|
|
6091
|
+
return null;
|
|
6092
|
+
if (!canAutopilotSkillMirrorSupplyRalplanPhase(autopilotStatePhase))
|
|
6093
|
+
return null;
|
|
6094
|
+
const hasRalplanScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (entry.skill === "autopilot"
|
|
6095
|
+
&& isAutopilotRalplanLikePhase(safeString(entry.phase).trim().toLowerCase())
|
|
6096
|
+
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
6097
|
+
if (!isAutopilotRalplanLikePhase(autopilotStatePhase) && !hasRalplanScopedAutopilotSkill)
|
|
6098
|
+
return null;
|
|
6099
|
+
return hasActiveAutopilotSkill ? autopilotState : null;
|
|
6100
|
+
}
|
|
6101
|
+
function isAllowedRalplanBashWrite(cwd, command, activeState, sessionId) {
|
|
6102
|
+
const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
|
|
6103
|
+
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
6104
|
+
const hasAllowedTargets = targets.length > 0
|
|
6105
|
+
&& targets.every((target) => isAllowedRalplanArtifactPath(cwd, target));
|
|
6106
|
+
if (beadsCommand.present) {
|
|
6107
|
+
return beadsCommand.allowed && (targets.length === 0 || hasAllowedTargets);
|
|
6108
|
+
}
|
|
6109
|
+
if (commandEndsPlanningPhase(cwd, command)) {
|
|
6110
|
+
return isAllowedRalplanTerminalStateWriteCommand(cwd, command, activeState, sessionId);
|
|
6111
|
+
}
|
|
6112
|
+
if (commandHasUntargetedPlanningForbiddenIntent(command))
|
|
6113
|
+
return false;
|
|
6114
|
+
if (firstPlanningTmpScriptExecutionTarget(cwd, command))
|
|
6115
|
+
return false;
|
|
6116
|
+
if (!commandHasDeepInterviewWriteIntent(command))
|
|
6117
|
+
return true;
|
|
6118
|
+
if (hasUnresolvedConductorInterpreterWrite(command))
|
|
6119
|
+
return false;
|
|
6120
|
+
if (targets.some((target) => !isAllowedRalplanArtifactPath(cwd, target)))
|
|
6121
|
+
return false;
|
|
6122
|
+
return hasAllowedTargets;
|
|
6123
|
+
}
|
|
6124
|
+
function buildRalplanBashBlockedDetail(cwd, command) {
|
|
6125
|
+
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
6126
|
+
const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
|
|
6127
|
+
if (blockedTarget && isUnresolvedVariableTarget(blockedTarget)) {
|
|
6128
|
+
return `unresolved Bash write target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
6129
|
+
}
|
|
6130
|
+
if (blockedTarget) {
|
|
3077
6131
|
const operationClass = /\btee\s+(?:-a\s+)?/.test(command) ? "Bash tee write" : "Bash redirect write";
|
|
3078
6132
|
return `${operationClass} target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
3079
6133
|
}
|
|
3080
|
-
const
|
|
3081
|
-
if (
|
|
3082
|
-
return
|
|
6134
|
+
const executedTmpTarget = firstPlanningTmpScriptExecutionTarget(cwd, command);
|
|
6135
|
+
if (executedTmpTarget) {
|
|
6136
|
+
return `execution target ${executedTmpTarget} is under .omx/tmp; planning tmp artifacts must not be used as generated-script transport`;
|
|
6137
|
+
}
|
|
6138
|
+
if (commandHasPackageInstallIntent(command)) {
|
|
6139
|
+
return "package installation commands are implementation actions and cannot be combined with allowed planning artifact writes";
|
|
6140
|
+
}
|
|
6141
|
+
if (commandHasDestructiveGitSubcommand(command)) {
|
|
6142
|
+
return "destructive git commands are implementation actions and cannot be combined with allowed planning artifact writes";
|
|
6143
|
+
}
|
|
6144
|
+
const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
|
|
6145
|
+
if (beadsCommand.present && !beadsCommand.allowed) {
|
|
6146
|
+
return beadsCommand.reason ?? "Beads tracker command is not an allowed planning metadata mutation";
|
|
6147
|
+
}
|
|
6148
|
+
if (beadsCommand.present) {
|
|
6149
|
+
return "Beads tracker command also performs an implementation write outside allowed planning metadata";
|
|
6150
|
+
}
|
|
6151
|
+
return "Bash write intent did not identify an allowed planning artifact path or metadata path";
|
|
6152
|
+
}
|
|
6153
|
+
function buildDeepInterviewBashBlockedDetail(cwd, command) {
|
|
6154
|
+
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
6155
|
+
const blockedTarget = targets.find((target) => !isAllowedDeepInterviewArtifactPath(cwd, target));
|
|
6156
|
+
if (blockedTarget && isUnresolvedVariableTarget(blockedTarget)) {
|
|
6157
|
+
return `unresolved Bash write target ${blockedTarget} is not under allowed deep-interview artifact paths or metadata paths (${DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
6158
|
+
}
|
|
6159
|
+
if (blockedTarget) {
|
|
6160
|
+
const operationClass = /\btee\s+(?:-a\s+)?/.test(command) ? "Bash tee write" : "Bash write";
|
|
6161
|
+
return `${operationClass} target ${blockedTarget} is not under allowed deep-interview artifact paths or metadata paths (${DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
6162
|
+
}
|
|
6163
|
+
const executedTmpTarget = firstPlanningTmpScriptExecutionTarget(cwd, command);
|
|
6164
|
+
if (executedTmpTarget) {
|
|
6165
|
+
return `execution target ${executedTmpTarget} is under .omx/tmp; deep-interview tmp artifacts must not be used as generated-script transport`;
|
|
6166
|
+
}
|
|
6167
|
+
if (commandHasPackageInstallIntent(command)) {
|
|
6168
|
+
return "package installation commands are implementation actions and cannot be combined with allowed deep-interview artifact writes";
|
|
6169
|
+
}
|
|
6170
|
+
if (commandHasDestructiveGitSubcommand(command)) {
|
|
6171
|
+
return "destructive git commands are implementation actions and cannot be combined with allowed deep-interview artifact writes";
|
|
6172
|
+
}
|
|
6173
|
+
return "Bash write intent did not identify an allowed deep-interview artifact path or metadata path";
|
|
6174
|
+
}
|
|
6175
|
+
async function buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, resolvedSessionId) {
|
|
6176
|
+
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
6177
|
+
if (await hasTrustedTypedSubagentThreadSpawnProvenanceForPreToolUse(payload, cwd, sessionId))
|
|
6178
|
+
return null;
|
|
6179
|
+
const threadId = readPayloadThreadId(payload);
|
|
6180
|
+
const activeState = await readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId);
|
|
6181
|
+
if (!activeState)
|
|
6182
|
+
return null;
|
|
6183
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
6184
|
+
const command = readPreToolUseCommand(payload);
|
|
6185
|
+
const pathCandidates = readPreToolUsePathCandidates(payload);
|
|
6186
|
+
let blocked = false;
|
|
6187
|
+
let blockedDetail = "implementation/write tools are blocked until an explicit execution handoff workflow is activated";
|
|
6188
|
+
if (toolName === "Bash") {
|
|
6189
|
+
blocked = !isAllowedRalplanBashWrite(cwd, command, activeState, sessionId);
|
|
6190
|
+
if (blocked) {
|
|
6191
|
+
blockedDetail = buildRalplanBashBlockedDetail(cwd, command);
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
else if (toolName === "mcp__omx_state__state_clear"
|
|
6195
|
+
|| (toolName === "mcp__omx_state__state_write"
|
|
6196
|
+
&& isPlanningPhaseDeactivationPayload(normalizeStateWriteClassificationPayload(safeObject(payload.tool_input))))) {
|
|
6197
|
+
blocked = true;
|
|
6198
|
+
blockedDetail = `${toolName} would deactivate protected planning state`;
|
|
6199
|
+
}
|
|
6200
|
+
else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
|
|
6201
|
+
const toolPathCandidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
|
|
6202
|
+
if (toolPathCandidates.length === 0) {
|
|
6203
|
+
blocked = true;
|
|
6204
|
+
blockedDetail = describeImplementationToolBlock(toolName, undefined, toolPathCandidates.length);
|
|
6205
|
+
}
|
|
6206
|
+
else {
|
|
6207
|
+
const blockedPath = toolPathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
|
|
6208
|
+
blocked = blockedPath !== undefined;
|
|
6209
|
+
if (blockedPath !== undefined) {
|
|
6210
|
+
blockedDetail = describeImplementationToolBlock(toolName, blockedPath, toolPathCandidates.length);
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
if (!blocked)
|
|
6215
|
+
return null;
|
|
6216
|
+
const phase = formatPhase(activeState.current_phase ?? activeState.currentPhase, "planning");
|
|
6217
|
+
const activeMode = safeString(activeState.mode).trim().toLowerCase();
|
|
6218
|
+
const planningModeLabel = activeMode === "autopilot" ? "Autopilot planning" : "Ralplan";
|
|
6219
|
+
const planningModeDescription = activeMode === "autopilot"
|
|
6220
|
+
? "Autopilot is supervising a planning phase"
|
|
6221
|
+
: "Ralplan is consensus-planning mode";
|
|
6222
|
+
return {
|
|
6223
|
+
decision: "block",
|
|
6224
|
+
reason: `${planningModeLabel} is active (phase: ${phase}); implementation/write tools are blocked until an explicit execution handoff workflow is activated; ${blockedDetail}.`,
|
|
6225
|
+
hookSpecificOutput: {
|
|
6226
|
+
hookEventName: "PreToolUse",
|
|
6227
|
+
additionalContext: `${planningModeDescription}. `
|
|
6228
|
+
+ "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, `.omx/tmp/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
|
|
6229
|
+
+ "Do not edit implementation files or run implementation-focused writes from planning phases. "
|
|
6230
|
+
+ `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
|
|
6231
|
+
},
|
|
6232
|
+
};
|
|
6233
|
+
}
|
|
6234
|
+
async function buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, resolvedSessionId) {
|
|
6235
|
+
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
6236
|
+
if (await hasTrustedTypedSubagentThreadSpawnProvenanceForPreToolUse(payload, cwd, sessionId))
|
|
6237
|
+
return null;
|
|
6238
|
+
const threadId = readPayloadThreadId(payload);
|
|
6239
|
+
const activeState = await readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId);
|
|
6240
|
+
if (!activeState)
|
|
6241
|
+
return null;
|
|
6242
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
6243
|
+
const command = readPreToolUseCommand(payload);
|
|
6244
|
+
const pathCandidates = readPreToolUsePathCandidates(payload);
|
|
6245
|
+
let blocked = false;
|
|
6246
|
+
let blockedDetail = "implementation/write tools are blocked until an explicit handoff workflow is activated";
|
|
6247
|
+
if (toolName === "Bash") {
|
|
6248
|
+
blocked = !isAllowedDeepInterviewBashWrite(cwd, command);
|
|
6249
|
+
if (blocked) {
|
|
6250
|
+
blockedDetail = buildDeepInterviewBashBlockedDetail(cwd, command);
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
else if (toolName === "mcp__omx_state__state_clear"
|
|
6254
|
+
|| (toolName === "mcp__omx_state__state_write"
|
|
6255
|
+
&& isPlanningPhaseDeactivationPayload(normalizeStateWriteClassificationPayload(safeObject(payload.tool_input))))) {
|
|
6256
|
+
blocked = true;
|
|
6257
|
+
blockedDetail = `${toolName} would deactivate protected deep-interview planning state`;
|
|
6258
|
+
}
|
|
6259
|
+
else if (DEEP_INTERVIEW_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
|
|
6260
|
+
const candidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
|
|
6261
|
+
blocked = candidates.length === 0
|
|
6262
|
+
|| !candidates.every((candidate) => isAllowedDeepInterviewArtifactPath(cwd, candidate));
|
|
6263
|
+
if (blocked) {
|
|
6264
|
+
const blockedPath = candidates.find((candidate) => !isAllowedDeepInterviewArtifactPath(cwd, candidate));
|
|
6265
|
+
blockedDetail = describeImplementationToolBlock(toolName, blockedPath, candidates.length);
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
if (!blocked)
|
|
6269
|
+
return null;
|
|
6270
|
+
const phase = formatPhase(activeState.current_phase ?? activeState.currentPhase, "planning");
|
|
6271
|
+
return {
|
|
6272
|
+
decision: "block",
|
|
6273
|
+
reason: `Deep-interview is active (phase: ${phase}); implementation/write tools are blocked until an explicit handoff workflow is activated; ${blockedDetail}.`,
|
|
6274
|
+
hookSpecificOutput: {
|
|
6275
|
+
hookEventName: "PreToolUse",
|
|
6276
|
+
additionalContext: `Deep-interview is requirements/spec mode. Treat detailed user answers as interview/spec material, not implicit implementation authorization. You may write only deep-interview artifacts under \`.omx/context/\`, \`.omx/interviews/\`, \`.omx/specs/\`, \`.omx/tmp/\`, or required \`.omx/state/\` files. To implement, first ask for or process an explicit transition such as \`$ralplan\`, \`$autopilot\`, ${formatExecutionHandoffList(cwd)}.`,
|
|
6277
|
+
},
|
|
6278
|
+
};
|
|
6279
|
+
}
|
|
6280
|
+
function blocksDeepInterviewImplementationWrite(payload, cwd) {
|
|
6281
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
6282
|
+
if (toolName === "Bash") {
|
|
6283
|
+
return !isAllowedDeepInterviewBashWrite(cwd, readPreToolUseCommand(payload));
|
|
6284
|
+
}
|
|
6285
|
+
if (!DEEP_INTERVIEW_IMPLEMENTATION_TOOL_NAMES.has(toolName))
|
|
6286
|
+
return false;
|
|
6287
|
+
const candidates = collectImplementationToolPathCandidates(payload, toolName, readPreToolUsePathCandidates(payload));
|
|
6288
|
+
return candidates.length === 0
|
|
6289
|
+
|| !candidates.every((candidate) => isAllowedDeepInterviewArtifactPath(cwd, candidate));
|
|
6290
|
+
}
|
|
6291
|
+
// Shared builder for the "live root session pointer owned by another session"
|
|
6292
|
+
// fail-closed block. Deep-interview and ralplan/autopilot only differ in the
|
|
6293
|
+
// human-readable mode label and phase-description fragment; the decision,
|
|
6294
|
+
// structure, and guidance are identical.
|
|
6295
|
+
function buildRootPointerConflictBlock(activeState, planningModeLabel, planningPhaseDescription) {
|
|
6296
|
+
const phase = formatPhase(activeState.current_phase ?? activeState.currentPhase, "planning");
|
|
6297
|
+
return {
|
|
6298
|
+
decision: "block",
|
|
6299
|
+
reason: `${planningModeLabel} is active in the live root session pointer (phase: ${phase}), but the current native session could not be authoritatively resolved to that owner; failing closed for planning-write protection.`,
|
|
6300
|
+
hookSpecificOutput: {
|
|
6301
|
+
hookEventName: "PreToolUse",
|
|
6302
|
+
additionalContext: `OMX detected a live root session pointer owned by another session while a ${planningPhaseDescription} is active. `
|
|
6303
|
+
+ "This indicates collapsed session-root isolation. Do not perform implementation writes from this unresolved session; use the owning OMX session or restart with an isolated OMX_ROOT.",
|
|
6304
|
+
},
|
|
6305
|
+
};
|
|
6306
|
+
}
|
|
6307
|
+
function buildDeepInterviewRootPointerConflictBlock(activeState) {
|
|
6308
|
+
return buildRootPointerConflictBlock(activeState, "Deep-interview", "deep-interview planning phase");
|
|
6309
|
+
}
|
|
6310
|
+
function buildRalplanRootPointerConflictBlock(activeState) {
|
|
6311
|
+
const activeMode = safeString(activeState.mode).trim().toLowerCase();
|
|
6312
|
+
const planningModeLabel = activeMode === "autopilot" ? "Autopilot planning" : "Ralplan";
|
|
6313
|
+
return buildRootPointerConflictBlock(activeState, planningModeLabel, "ralplan/autopilot planning phase");
|
|
6314
|
+
}
|
|
6315
|
+
async function buildPlanningRootPointerConflictPreToolUseOutput(payload, cwd, stateDir, rootState) {
|
|
6316
|
+
const rootSessionId = safeString(rootState?.session_id).trim();
|
|
6317
|
+
if (!rootSessionId)
|
|
6318
|
+
return null;
|
|
6319
|
+
const ownerCwd = safeString(rootState?.cwd).trim() || cwd;
|
|
6320
|
+
const deepInterviewState = await readActiveDeepInterviewStateForPreToolUse(ownerCwd, stateDir, rootSessionId, "");
|
|
6321
|
+
if (deepInterviewState) {
|
|
6322
|
+
const conflictToolName = safeString(payload.tool_name).trim();
|
|
6323
|
+
if (conflictToolName === "mcp__omx_state__state_clear"
|
|
6324
|
+
|| (conflictToolName === "mcp__omx_state__state_write"
|
|
6325
|
+
&& isPlanningPhaseDeactivationPayload(normalizeStateWriteClassificationPayload(safeObject(payload.tool_input))))) {
|
|
6326
|
+
return buildDeepInterviewRootPointerConflictBlock(deepInterviewState);
|
|
6327
|
+
}
|
|
6328
|
+
if (blocksDeepInterviewImplementationWrite(payload, cwd)) {
|
|
6329
|
+
return buildDeepInterviewRootPointerConflictBlock(deepInterviewState);
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6332
|
+
const ralplanState = await readActiveRalplanStateForPreToolUse(ownerCwd, stateDir, rootSessionId, "");
|
|
6333
|
+
if (!ralplanState)
|
|
6334
|
+
return null;
|
|
6335
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
6336
|
+
let blocked = false;
|
|
6337
|
+
if (toolName === "Bash") {
|
|
6338
|
+
const command = readPreToolUseCommand(payload);
|
|
6339
|
+
blocked = commandEndsPlanningPhase(cwd, command)
|
|
6340
|
+
|| !isAllowedRalplanBashWrite(cwd, command, ralplanState, rootSessionId);
|
|
6341
|
+
}
|
|
6342
|
+
else if (toolName === "mcp__omx_state__state_clear"
|
|
6343
|
+
|| (toolName === "mcp__omx_state__state_write"
|
|
6344
|
+
&& isPlanningPhaseDeactivationPayload(normalizeStateWriteClassificationPayload(safeObject(payload.tool_input))))) {
|
|
6345
|
+
blocked = true;
|
|
6346
|
+
}
|
|
6347
|
+
else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
|
|
6348
|
+
const toolPathCandidates = collectImplementationToolPathCandidates(payload, toolName, readPreToolUsePathCandidates(payload));
|
|
6349
|
+
blocked = toolPathCandidates.length === 0
|
|
6350
|
+
|| toolPathCandidates.some((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
|
|
6351
|
+
}
|
|
6352
|
+
return blocked ? buildRalplanRootPointerConflictBlock(ralplanState) : null;
|
|
6353
|
+
}
|
|
6354
|
+
async function hasTrustedTypedSubagentThreadSpawnProvenanceForPreToolUse(payload, cwd, sessionId) {
|
|
6355
|
+
if (hasTeamWorkerEnvironment())
|
|
6356
|
+
return true;
|
|
6357
|
+
const source = safeObject(payload.source);
|
|
6358
|
+
const subagent = safeObject(source.subagent);
|
|
6359
|
+
const threadSpawn = safeObject(subagent.thread_spawn);
|
|
6360
|
+
const parentThreadId = safeString(threadSpawn.parent_thread_id
|
|
6361
|
+
?? threadSpawn.parentThreadId
|
|
6362
|
+
?? threadSpawn.leader_thread_id
|
|
6363
|
+
?? threadSpawn.leaderThreadId).trim();
|
|
6364
|
+
if (!parentThreadId)
|
|
6365
|
+
return false;
|
|
6366
|
+
if (!isTypedAgentRolePayload(payload))
|
|
6367
|
+
return false;
|
|
6368
|
+
const trackingState = await readSubagentTrackingState(cwd).catch(() => null);
|
|
6369
|
+
const session = trackingState?.sessions?.[sessionId];
|
|
6370
|
+
if (!session)
|
|
6371
|
+
return false;
|
|
6372
|
+
return session.leader_thread_id === parentThreadId || parentThreadId in session.threads;
|
|
6373
|
+
}
|
|
6374
|
+
function isActiveConductorModeState(state, mode, sessionId) {
|
|
6375
|
+
if (!state || state.active !== true)
|
|
6376
|
+
return false;
|
|
6377
|
+
const stateMode = safeString(state.mode).trim();
|
|
6378
|
+
if (stateMode && stateMode !== mode)
|
|
6379
|
+
return false;
|
|
6380
|
+
const stateSessionId = safeString(state.session_id).trim();
|
|
6381
|
+
if (sessionId && stateSessionId && stateSessionId !== sessionId)
|
|
6382
|
+
return false;
|
|
6383
|
+
return isNonTerminalPhase(state.current_phase ?? state.currentPhase);
|
|
6384
|
+
}
|
|
6385
|
+
async function readActiveMainRootConductorStateForPreToolUse(payload, cwd, stateDir, resolvedSessionId) {
|
|
6386
|
+
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
6387
|
+
const payloadSessionId = readPayloadSessionId(payload);
|
|
6388
|
+
if (payloadSessionId && sessionId && payloadSessionId !== sessionId) {
|
|
6389
|
+
const currentSession = await readUsableSessionStateFromStateDir(cwd, stateDir).catch(() => null);
|
|
6390
|
+
const payloadMatchesMappedSession = payloadSessionId === safeString(currentSession?.native_session_id).trim()
|
|
6391
|
+
|| payloadSessionId === safeString(currentSession?.owner_omx_session_id).trim()
|
|
6392
|
+
|| payloadSessionId === safeString(currentSession?.owner_codex_session_id).trim();
|
|
6393
|
+
if (!payloadMatchesMappedSession)
|
|
6394
|
+
return null;
|
|
6395
|
+
}
|
|
6396
|
+
const threadId = readPayloadThreadId(payload);
|
|
6397
|
+
if (!sessionId)
|
|
6398
|
+
return null;
|
|
6399
|
+
if (await hasTrustedTypedSubagentThreadSpawnProvenanceForPreToolUse(payload, cwd, sessionId))
|
|
6400
|
+
return null;
|
|
6401
|
+
const canonicalState = await readVisibleSkillActiveStateForStateDir(stateDir, sessionId);
|
|
6402
|
+
if (!canonicalState)
|
|
6403
|
+
return null;
|
|
6404
|
+
const activeEntries = listActiveSkills(canonicalState).filter((entry) => (matchesSkillStopContext(entry, canonicalState, sessionId, threadId)));
|
|
6405
|
+
const hasActiveSkill = (skill) => activeEntries.some((entry) => entry.skill === skill);
|
|
6406
|
+
if (hasActiveSkill("autopilot")) {
|
|
6407
|
+
const state = await readStopSessionPinnedState("autopilot-state.json", cwd, sessionId, stateDir);
|
|
6408
|
+
const childPhase = deriveAutopilotChildPhase(state);
|
|
6409
|
+
const hasMatchingAutopilotEntry = activeEntries.some((entry) => (entry.skill === "autopilot"
|
|
6410
|
+
&& normalizeAutopilotPhase(safeString(entry.phase).trim().toLowerCase()) === childPhase));
|
|
6411
|
+
if (state
|
|
6412
|
+
&& childPhase
|
|
6413
|
+
&& childPhase !== "deep-interview"
|
|
6414
|
+
&& childPhase !== "ralplan"
|
|
6415
|
+
&& childPhase !== "rework"
|
|
6416
|
+
&& hasMatchingAutopilotEntry
|
|
6417
|
+
&& isActiveConductorModeState(state, "autopilot", sessionId)) {
|
|
6418
|
+
return { mode: "autopilot", phase: childPhase };
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
if (hasActiveSkill("ralph")) {
|
|
6422
|
+
const state = await readStopSessionPinnedState("ralph-state.json", cwd, sessionId, stateDir);
|
|
6423
|
+
if (isActiveConductorModeState(state, "ralph", sessionId)) {
|
|
6424
|
+
const phase = safeString(state?.current_phase ?? state?.currentPhase) || "active";
|
|
6425
|
+
if (phase.toLowerCase() !== "starting")
|
|
6426
|
+
return { mode: "ralph", phase };
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
if (hasActiveSkill("ultragoal")) {
|
|
6430
|
+
const state = await readStopSessionPinnedState("ultragoal-state.json", cwd, sessionId, stateDir);
|
|
6431
|
+
if (isActiveConductorModeState(state, "ultragoal", sessionId)) {
|
|
6432
|
+
return { mode: "ultragoal", phase: safeString(state?.current_phase ?? state?.currentPhase) || "active" };
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
if (hasActiveSkill("team") && !hasTeamWorkerEnvironment()) {
|
|
6436
|
+
const teamStateForStop = await readTeamModeStateForStop(cwd, stateDir, sessionId, threadId);
|
|
6437
|
+
const state = teamStateForStop?.state ?? null;
|
|
6438
|
+
if (isActiveConductorModeState(state, "team", sessionId)) {
|
|
6439
|
+
const teamName = safeString(state?.team_name).trim();
|
|
6440
|
+
const phase = teamName ? (await readTeamPhase(teamName, cwd).catch(() => null))?.current_phase ?? state?.current_phase : state?.current_phase;
|
|
6441
|
+
if (isNonTerminalPhase(phase))
|
|
6442
|
+
return { mode: "team", phase: safeString(phase) || "active" };
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
return null;
|
|
6446
|
+
}
|
|
6447
|
+
function normalizeRepoRelativePath(cwd, rawPath) {
|
|
6448
|
+
const candidate = rawPath.trim().replace(/^['"]|['"]$/g, "");
|
|
6449
|
+
if (!candidate || isUnresolvedVariableTarget(candidate))
|
|
6450
|
+
return null;
|
|
6451
|
+
const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(cwd, candidate);
|
|
6452
|
+
let relativePath = relative(cwd, absolute).replace(/\\/g, "/");
|
|
6453
|
+
if (!relativePath || relativePath === ".")
|
|
6454
|
+
return null;
|
|
6455
|
+
if (relativePath.startsWith("../") || relativePath === "..") {
|
|
6456
|
+
relativePath = candidate.replace(/\\/g, "/");
|
|
6457
|
+
}
|
|
6458
|
+
return relativePath.replace(/^\.\//, "");
|
|
6459
|
+
}
|
|
6460
|
+
function isAllowedConductorMetadataPath(cwd, rawPath) {
|
|
6461
|
+
const relativePath = normalizeRepoRelativePath(cwd, rawPath);
|
|
6462
|
+
if (!relativePath)
|
|
6463
|
+
return false;
|
|
6464
|
+
if (isProtectedPlanningStatePath(relativePath))
|
|
6465
|
+
return false;
|
|
6466
|
+
const artifactKind = classifyConductorArtifactKind(relativePath);
|
|
6467
|
+
const actionKind = actionKindForConductorArtifact(artifactKind);
|
|
6468
|
+
return authorizeConductorAction({
|
|
6469
|
+
phase: "autopilot-supervision",
|
|
6470
|
+
laneKind: "main-conductor",
|
|
6471
|
+
actionKind,
|
|
6472
|
+
artifactKind,
|
|
6473
|
+
}).allowed;
|
|
6474
|
+
}
|
|
6475
|
+
function describeConductorBlockedWrite(toolName, blockedPath, pathCount) {
|
|
6476
|
+
if (pathCount === 0) {
|
|
6477
|
+
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target extraction failed" : `${toolName} path`;
|
|
6478
|
+
return `${operationClass} target <unresolved>; Main-root Conductor may write only workflow state/ledger/mailbox/handoff metadata`;
|
|
6479
|
+
}
|
|
6480
|
+
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target" : `${toolName} path`;
|
|
6481
|
+
return `${operationClass} target ${blockedPath ?? "<unresolved>"} is not workflow state/ledger/mailbox/handoff metadata`;
|
|
6482
|
+
}
|
|
6483
|
+
const CONDUCTOR_BASH_MUTATION_COMMANDS = new Set([
|
|
6484
|
+
"cp",
|
|
6485
|
+
"mv",
|
|
6486
|
+
"rm",
|
|
6487
|
+
"touch",
|
|
6488
|
+
"mkdir",
|
|
6489
|
+
"rmdir",
|
|
6490
|
+
"install",
|
|
6491
|
+
"ln",
|
|
6492
|
+
"chmod",
|
|
6493
|
+
"chown",
|
|
6494
|
+
"chgrp",
|
|
6495
|
+
"truncate",
|
|
6496
|
+
"sed",
|
|
6497
|
+
"perl",
|
|
6498
|
+
"dd",
|
|
6499
|
+
"rsync",
|
|
6500
|
+
]);
|
|
6501
|
+
const CONDUCTOR_BASH_TRANSPARENT_WRAPPERS = new Set([
|
|
6502
|
+
"builtin",
|
|
6503
|
+
"noglob",
|
|
6504
|
+
]);
|
|
6505
|
+
const CONDUCTOR_BASH_DOWNLOADER_COMMANDS = new Set([
|
|
6506
|
+
"curl",
|
|
6507
|
+
"wget",
|
|
6508
|
+
]);
|
|
6509
|
+
const CONDUCTOR_BASH_COMPOUND_SYNTAX_WORDS = new Set([
|
|
6510
|
+
"if",
|
|
6511
|
+
"then",
|
|
6512
|
+
"else",
|
|
6513
|
+
"elif",
|
|
6514
|
+
"fi",
|
|
6515
|
+
"for",
|
|
6516
|
+
"while",
|
|
6517
|
+
"until",
|
|
6518
|
+
"do",
|
|
6519
|
+
"done",
|
|
6520
|
+
"case",
|
|
6521
|
+
"esac",
|
|
6522
|
+
"in",
|
|
6523
|
+
"{",
|
|
6524
|
+
"}",
|
|
6525
|
+
"(",
|
|
6526
|
+
")",
|
|
6527
|
+
]);
|
|
6528
|
+
const CONDUCTOR_BASH_OPTIONS_WITH_VALUES = new Set([
|
|
6529
|
+
"-S",
|
|
6530
|
+
"--suffix",
|
|
6531
|
+
"-t",
|
|
6532
|
+
"--target-directory",
|
|
6533
|
+
"-m",
|
|
6534
|
+
"--mode",
|
|
6535
|
+
"-o",
|
|
6536
|
+
"--owner",
|
|
6537
|
+
"-g",
|
|
6538
|
+
"--group",
|
|
6539
|
+
"--reference",
|
|
6540
|
+
"--preserve",
|
|
6541
|
+
"--size",
|
|
6542
|
+
"-if",
|
|
6543
|
+
"if",
|
|
6544
|
+
"-of",
|
|
6545
|
+
"of",
|
|
6546
|
+
]);
|
|
6547
|
+
function extractConductorInterpreterWrites(command) {
|
|
6548
|
+
const writes = [];
|
|
6549
|
+
const scanCommand = normalizeShellLineContinuations(command);
|
|
6550
|
+
let recognizedPythonOpenWrites = 0;
|
|
6551
|
+
let recognizedPythonPathMutations = 0;
|
|
6552
|
+
let recognizedPythonShutilMutations = 0;
|
|
6553
|
+
for (const match of scanCommand.matchAll(/\bnode\b[\s\S]{0,520}\b(?:appendFileSync|writeFileSync|appendFile|writeFile|createWriteStream)\s*\(\s*(["'])([^"']+)\1/g)) {
|
|
6554
|
+
const target = safeString(match[2]).trim();
|
|
6555
|
+
writes.push({ runtime: "node", targets: target ? [target] : [], unresolved: !target });
|
|
6556
|
+
}
|
|
6557
|
+
if (/\bnode\b[\s\S]{0,520}\b(?:appendFileSync|writeFileSync|appendFile|writeFile|createWriteStream)\s*\(/.test(scanCommand)
|
|
6558
|
+
&& !writes.some((write) => write.runtime === "node")) {
|
|
6559
|
+
writes.push({ runtime: "node", targets: [], unresolved: true });
|
|
6560
|
+
}
|
|
6561
|
+
for (const match of scanCommand.matchAll(/\bpython3?\b[\s\S]{0,520}\bopen\s*\(\s*(["'])([^"']+)\1\s*,\s*(["'])([^"']*[wax+][^"']*)\3/g)) {
|
|
6562
|
+
const target = safeString(match[2]).trim();
|
|
6563
|
+
writes.push({ runtime: "python", targets: target ? [target] : [], unresolved: !target });
|
|
6564
|
+
recognizedPythonOpenWrites += 1;
|
|
6565
|
+
}
|
|
6566
|
+
for (const match of scanCommand.matchAll(/\bpython3?\b[\s\S]{0,520}\bPath\s*\(\s*(["'])([^"']+)\1\s*\)\s*\.\s*(?:write_text|write_bytes)\s*\(/g)) {
|
|
6567
|
+
const target = safeString(match[2]).trim();
|
|
6568
|
+
writes.push({ runtime: "python", targets: target ? [target] : [], unresolved: !target });
|
|
6569
|
+
recognizedPythonPathMutations += 1;
|
|
6570
|
+
}
|
|
6571
|
+
for (const match of scanCommand.matchAll(/\bpython3?\b[\s\S]{0,520}\bPath\s*\(\s*(["'])([^"']+)\1\s*\)\s*\.\s*mkdir\s*\(/g)) {
|
|
6572
|
+
const target = safeString(match[2]).trim();
|
|
6573
|
+
writes.push({ runtime: "python", targets: target ? [target] : [], unresolved: !target });
|
|
6574
|
+
recognizedPythonPathMutations += 1;
|
|
6575
|
+
}
|
|
6576
|
+
for (const match of scanCommand.matchAll(/\bpython3?\b[\s\S]{0,520}\bshutil\s*\.\s*(?:copyfile|copy|copy2|copytree|move)\s*\(\s*(["'])([^"']+)\1\s*,\s*(["'])([^"']+)\3/g)) {
|
|
6577
|
+
const target = safeString(match[4]).trim();
|
|
6578
|
+
writes.push({ runtime: "python", targets: target ? [target] : [], unresolved: !target });
|
|
6579
|
+
recognizedPythonShutilMutations += 1;
|
|
6580
|
+
}
|
|
6581
|
+
const hasPythonRuntime = /\bpython3?\b/.test(scanCommand);
|
|
6582
|
+
const pythonOpenWriteCalls = hasPythonRuntime
|
|
6583
|
+
? [...scanCommand.matchAll(/\bopen\s*\([^\n;)]*,\s*["'][^"']*[wax+][^"']*["']/g)].length
|
|
6584
|
+
: 0;
|
|
6585
|
+
const pythonPathMutationCalls = hasPythonRuntime
|
|
6586
|
+
? [...scanCommand.matchAll(/(?:^|[\n;])\s*(?:\([^\n;#]*\bPath\s*\([^\n;#]*\)[^\n;#]*\)|\bPath\s*\([^\n;#]*\))\s*\.\s*(?:write_text|write_bytes|mkdir)\s*\(/g)].length
|
|
6587
|
+
: 0;
|
|
6588
|
+
const pythonShutilMutationCalls = hasPythonRuntime
|
|
6589
|
+
? [...scanCommand.matchAll(/\bshutil\s*\.\s*(?:copyfile|copy|copy2|copytree|move)\s*\(/g)].length
|
|
6590
|
+
: 0;
|
|
6591
|
+
if (pythonOpenWriteCalls > recognizedPythonOpenWrites
|
|
6592
|
+
|| pythonPathMutationCalls > recognizedPythonPathMutations
|
|
6593
|
+
|| pythonShutilMutationCalls > recognizedPythonShutilMutations) {
|
|
6594
|
+
writes.push({ runtime: "python", targets: [], unresolved: true });
|
|
6595
|
+
}
|
|
6596
|
+
return writes;
|
|
6597
|
+
}
|
|
6598
|
+
function hasUnresolvedConductorInterpreterWrite(command) {
|
|
6599
|
+
return extractConductorInterpreterWrites(command).some((write) => write.unresolved || write.targets.length === 0);
|
|
6600
|
+
}
|
|
6601
|
+
function commandNameFromShellWord(word) {
|
|
6602
|
+
const base = word.trim().split(/[\\/]/).pop() ?? word.trim();
|
|
6603
|
+
return base.toLowerCase();
|
|
6604
|
+
}
|
|
6605
|
+
function isShellCommandSeparator(word) {
|
|
6606
|
+
return word === "&&" || word === "||" || word === ";" || word === "&" || word === "|" || word === "|&";
|
|
6607
|
+
}
|
|
6608
|
+
function isShellGroupingSyntaxWord(word) {
|
|
6609
|
+
return word === "(" || word === ")" || word === "{" || word === "}";
|
|
6610
|
+
}
|
|
6611
|
+
function isShellCommandTerminatorOrGroupClose(word) {
|
|
6612
|
+
return isShellCommandSeparator(word) || word === ")" || word === "}";
|
|
6613
|
+
}
|
|
6614
|
+
function commandUsesTargetDirectoryOption(commandName) {
|
|
6615
|
+
return commandName === "cp" || commandName === "mv" || commandName === "install" || commandName === "ln";
|
|
6616
|
+
}
|
|
6617
|
+
function isEnvironmentAssignmentWord(word) {
|
|
6618
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(word);
|
|
6619
|
+
}
|
|
6620
|
+
function findSudoDispatchOperandIndex(words, startIndex) {
|
|
6621
|
+
for (let index = startIndex; index < words.length; index += 1) {
|
|
6622
|
+
const token = words[index] ?? "";
|
|
6623
|
+
if (!token || token === "--")
|
|
6624
|
+
continue;
|
|
6625
|
+
if (isShellAssignmentWord(token))
|
|
6626
|
+
continue;
|
|
6627
|
+
if (token === "-u"
|
|
6628
|
+
|| token === "--user"
|
|
6629
|
+
|| token === "-g"
|
|
6630
|
+
|| token === "--group"
|
|
6631
|
+
|| token === "-h"
|
|
6632
|
+
|| token === "--host"
|
|
6633
|
+
|| token === "-p"
|
|
6634
|
+
|| token === "--prompt"
|
|
6635
|
+
|| token === "-C"
|
|
6636
|
+
|| token === "--close-from") {
|
|
6637
|
+
index += 1;
|
|
6638
|
+
continue;
|
|
6639
|
+
}
|
|
6640
|
+
if (token.startsWith("--user=")
|
|
6641
|
+
|| token.startsWith("--group=")
|
|
6642
|
+
|| token.startsWith("--host=")
|
|
6643
|
+
|| token.startsWith("--prompt=")
|
|
6644
|
+
|| token.startsWith("--close-from=")
|
|
6645
|
+
|| /^-[ughpC].+/.test(token)) {
|
|
6646
|
+
continue;
|
|
6647
|
+
}
|
|
6648
|
+
if (token.startsWith("-"))
|
|
6649
|
+
continue;
|
|
6650
|
+
return index;
|
|
6651
|
+
}
|
|
6652
|
+
return null;
|
|
6653
|
+
}
|
|
6654
|
+
function findConductorWrapperOperandIndex(commandName, words, startIndex) {
|
|
6655
|
+
switch (commandName) {
|
|
6656
|
+
case "env":
|
|
6657
|
+
return findEnvDispatchOperandIndex(words, startIndex);
|
|
6658
|
+
case "command":
|
|
6659
|
+
return findCommandDispatchOperandIndex(words, startIndex);
|
|
6660
|
+
case "exec":
|
|
6661
|
+
return findExecDispatchOperandIndex(words, startIndex);
|
|
6662
|
+
case "sudo":
|
|
6663
|
+
return findSudoDispatchOperandIndex(words, startIndex);
|
|
6664
|
+
case "nohup":
|
|
6665
|
+
return findCommandDispatchOperandIndex(words, startIndex);
|
|
6666
|
+
case "time":
|
|
6667
|
+
return findTimeDispatchOperandIndex(words, startIndex);
|
|
6668
|
+
case "timeout":
|
|
6669
|
+
return findTimeoutDispatchOperandIndex(words, startIndex);
|
|
6670
|
+
case "nice":
|
|
6671
|
+
return findNiceDispatchOperandIndex(words, startIndex);
|
|
6672
|
+
case "stdbuf":
|
|
6673
|
+
return findStdbufDispatchOperandIndex(words, startIndex);
|
|
6674
|
+
default:
|
|
6675
|
+
return undefined;
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
function isConductorDestinationOnlyMutationCommand(commandName) {
|
|
6679
|
+
return commandName === "cp" || commandName === "install" || commandName === "ln" || commandName === "rsync";
|
|
6680
|
+
}
|
|
6681
|
+
function isConductorInstallDirectoryMode(words, commandIndex) {
|
|
6682
|
+
for (let index = commandIndex + 1; index < words.length; index += 1) {
|
|
6683
|
+
const word = words[index] ?? "";
|
|
6684
|
+
if (!word || isShellCommandTerminatorOrGroupClose(word))
|
|
6685
|
+
break;
|
|
6686
|
+
if (word === "-d" || word === "--directory")
|
|
6687
|
+
return true;
|
|
6688
|
+
if (word.startsWith("-") && !word.startsWith("--") && word.includes("d"))
|
|
6689
|
+
return true;
|
|
6690
|
+
}
|
|
6691
|
+
return false;
|
|
6692
|
+
}
|
|
6693
|
+
function collectConductorDownloaderOutputTargets(commandName, words, commandIndex) {
|
|
6694
|
+
const targets = [];
|
|
6695
|
+
const explicitCurlTargets = [];
|
|
6696
|
+
let sawOutputFlag = false;
|
|
6697
|
+
let sawCurlRemoteName = false;
|
|
6698
|
+
const curlOutputDirs = [];
|
|
6699
|
+
const curlShortOptionsWithArgument = new Set("AbcCdDeEFHhKmMoPQrTuwxXyYz");
|
|
6700
|
+
const isCurlRemoteNameWord = (word) => {
|
|
6701
|
+
if (word === "--remote-name" || word === "--remote-name-all")
|
|
6702
|
+
return true;
|
|
6703
|
+
if (!word.startsWith("-") || word.startsWith("--"))
|
|
6704
|
+
return false;
|
|
6705
|
+
for (const option of word.slice(1)) {
|
|
6706
|
+
if (option === "O")
|
|
6707
|
+
return true;
|
|
6708
|
+
if (curlShortOptionsWithArgument.has(option))
|
|
6709
|
+
return false;
|
|
6710
|
+
}
|
|
6711
|
+
return false;
|
|
6712
|
+
};
|
|
6713
|
+
const pushTarget = (rawTarget) => {
|
|
6714
|
+
const target = safeString(rawTarget).trim();
|
|
6715
|
+
if (!target)
|
|
6716
|
+
return;
|
|
6717
|
+
if (commandName === "curl")
|
|
6718
|
+
explicitCurlTargets.push(target);
|
|
6719
|
+
else
|
|
6720
|
+
targets.push(target);
|
|
6721
|
+
};
|
|
6722
|
+
for (let index = commandIndex + 1; index < words.length; index += 1) {
|
|
6723
|
+
const word = words[index] ?? "";
|
|
6724
|
+
if (!word || isShellCommandTerminatorOrGroupClose(word))
|
|
6725
|
+
break;
|
|
6726
|
+
if (isEnvironmentAssignmentWord(word))
|
|
6727
|
+
continue;
|
|
6728
|
+
if (word === "--")
|
|
6729
|
+
continue;
|
|
6730
|
+
const inlineCurlOutput = commandName === "curl" ? word.match(/^--output=(.+)$/) : null;
|
|
6731
|
+
const inlineWgetOutput = commandName === "wget" ? word.match(/^--(?:output-document|output-file|append-output)=(.+)$/) : null;
|
|
6732
|
+
const inlineWgetDirectoryPrefix = commandName === "wget" ? word.match(/^--directory-prefix=(.+)$/) : null;
|
|
6733
|
+
const inlineCurlOutputDir = commandName === "curl" ? word.match(/^--output-dir=(.+)$/) : null;
|
|
6734
|
+
const inlineTarget = inlineCurlOutput?.[1] ?? inlineWgetOutput?.[1] ?? inlineWgetDirectoryPrefix?.[1];
|
|
6735
|
+
if (inlineTarget !== undefined) {
|
|
6736
|
+
sawOutputFlag = true;
|
|
6737
|
+
pushTarget(inlineTarget);
|
|
6738
|
+
continue;
|
|
6739
|
+
}
|
|
6740
|
+
if (inlineCurlOutputDir?.[1] !== undefined) {
|
|
6741
|
+
curlOutputDirs.push(inlineCurlOutputDir[1]);
|
|
6742
|
+
continue;
|
|
6743
|
+
}
|
|
6744
|
+
if (commandName === "curl" && isCurlRemoteNameWord(word)) {
|
|
6745
|
+
sawCurlRemoteName = true;
|
|
6746
|
+
continue;
|
|
6747
|
+
}
|
|
6748
|
+
if (commandName === "curl" && word.startsWith("-o") && word.length > 2) {
|
|
6749
|
+
sawOutputFlag = true;
|
|
6750
|
+
pushTarget(word.slice(2));
|
|
6751
|
+
continue;
|
|
6752
|
+
}
|
|
6753
|
+
if (commandName === "wget" && (word.startsWith("-O") || word.startsWith("-o") || word.startsWith("-a")) && word.length > 2) {
|
|
6754
|
+
sawOutputFlag = true;
|
|
6755
|
+
pushTarget(word.slice(2));
|
|
6756
|
+
continue;
|
|
6757
|
+
}
|
|
6758
|
+
const separateOutputFlag = (commandName === "curl" && (word === "-o" || word === "--output" || word === "--append-output"))
|
|
6759
|
+
|| (commandName === "wget" && (word === "-O" || word === "-o" || word === "-a" || word === "--output-document" || word === "--output-file" || word === "--append-output" || word === "-P" || word === "--directory-prefix"));
|
|
6760
|
+
if (!separateOutputFlag) {
|
|
6761
|
+
if (commandName === "curl" && word === "--output-dir") {
|
|
6762
|
+
const nextWord = words[index + 1] ?? "";
|
|
6763
|
+
if (nextWord && !isShellCommandTerminatorOrGroupClose(nextWord)) {
|
|
6764
|
+
curlOutputDirs.push(nextWord);
|
|
6765
|
+
index += 1;
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
continue;
|
|
6769
|
+
}
|
|
6770
|
+
sawOutputFlag = true;
|
|
6771
|
+
const nextWord = words[index + 1] ?? "";
|
|
6772
|
+
if (nextWord && !isShellCommandTerminatorOrGroupClose(nextWord)) {
|
|
6773
|
+
pushTarget(nextWord);
|
|
6774
|
+
index += 1;
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
6777
|
+
if (commandName === "curl" && sawCurlRemoteName) {
|
|
6778
|
+
sawOutputFlag = true;
|
|
6779
|
+
targets.push(...(curlOutputDirs.length > 0 ? curlOutputDirs : ["."]));
|
|
3083
6780
|
}
|
|
3084
|
-
if (
|
|
3085
|
-
|
|
6781
|
+
if (commandName === "curl" && explicitCurlTargets.length > 0) {
|
|
6782
|
+
const outputDir = curlOutputDirs[curlOutputDirs.length - 1];
|
|
6783
|
+
const effectiveTargets = outputDir
|
|
6784
|
+
? explicitCurlTargets.map((target) => (isAbsolute(target) ? target : join(outputDir, target)))
|
|
6785
|
+
: explicitCurlTargets;
|
|
6786
|
+
targets.push(...effectiveTargets);
|
|
3086
6787
|
}
|
|
3087
|
-
return
|
|
6788
|
+
return { sawOutputFlag, targets };
|
|
3088
6789
|
}
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
6790
|
+
function isConductorSedInPlaceOption(word) {
|
|
6791
|
+
return word === "-i" || word === "--in-place" || word.startsWith("-i") || word.startsWith("--in-place=") || /^-[^-]*i/.test(word);
|
|
6792
|
+
}
|
|
6793
|
+
function collectConductorSedTargets(words, commandIndex) {
|
|
6794
|
+
const targets = [];
|
|
6795
|
+
let sawInPlace = false;
|
|
6796
|
+
let sawExplicitScript = false;
|
|
6797
|
+
let consumedImplicitScript = false;
|
|
6798
|
+
for (let index = commandIndex + 1; index < words.length; index += 1) {
|
|
6799
|
+
const word = words[index] ?? "";
|
|
6800
|
+
if (!word || isShellCommandTerminatorOrGroupClose(word))
|
|
6801
|
+
break;
|
|
6802
|
+
if (isEnvironmentAssignmentWord(word))
|
|
6803
|
+
continue;
|
|
6804
|
+
if (word === "--") {
|
|
6805
|
+
consumedImplicitScript = true;
|
|
6806
|
+
continue;
|
|
6807
|
+
}
|
|
6808
|
+
if (isConductorSedInPlaceOption(word)) {
|
|
6809
|
+
sawInPlace = true;
|
|
6810
|
+
continue;
|
|
6811
|
+
}
|
|
6812
|
+
if (word === "-e" || word === "--expression" || word === "-f" || word === "--file") {
|
|
6813
|
+
sawExplicitScript = true;
|
|
6814
|
+
index += 1;
|
|
6815
|
+
continue;
|
|
6816
|
+
}
|
|
6817
|
+
if (word.startsWith("-e") || word.startsWith("--expression=") || word.startsWith("-f") || word.startsWith("--file=")) {
|
|
6818
|
+
sawExplicitScript = true;
|
|
6819
|
+
continue;
|
|
6820
|
+
}
|
|
6821
|
+
if (word.startsWith("-"))
|
|
6822
|
+
continue;
|
|
6823
|
+
if (!sawExplicitScript && !consumedImplicitScript) {
|
|
6824
|
+
consumedImplicitScript = true;
|
|
6825
|
+
continue;
|
|
6826
|
+
}
|
|
6827
|
+
targets.push(word);
|
|
6828
|
+
}
|
|
6829
|
+
return sawInPlace ? targets : null;
|
|
6830
|
+
}
|
|
6831
|
+
function collectConductorPerlTargets(words, commandIndex) {
|
|
6832
|
+
const targets = [];
|
|
6833
|
+
let sawInPlace = false;
|
|
6834
|
+
for (let index = commandIndex + 1; index < words.length; index += 1) {
|
|
6835
|
+
const word = words[index] ?? "";
|
|
6836
|
+
if (!word || isShellCommandTerminatorOrGroupClose(word))
|
|
6837
|
+
break;
|
|
6838
|
+
if (isEnvironmentAssignmentWord(word))
|
|
6839
|
+
continue;
|
|
6840
|
+
if (word === "--")
|
|
6841
|
+
continue;
|
|
6842
|
+
if (word === "-i" || word.startsWith("-i") || /^-[^-]*i/.test(word)) {
|
|
6843
|
+
sawInPlace = true;
|
|
6844
|
+
continue;
|
|
6845
|
+
}
|
|
6846
|
+
if (word === "-e") {
|
|
6847
|
+
index += 1;
|
|
6848
|
+
continue;
|
|
6849
|
+
}
|
|
6850
|
+
if (word.startsWith("-e") || /^-[^-]*e/.test(word))
|
|
6851
|
+
continue;
|
|
6852
|
+
if (word.startsWith("-"))
|
|
6853
|
+
continue;
|
|
6854
|
+
targets.push(word);
|
|
6855
|
+
}
|
|
6856
|
+
return sawInPlace ? targets : null;
|
|
6857
|
+
}
|
|
6858
|
+
const CONDUCTOR_XARGS_LONG_OPTIONS_WITH_REQUIRED_VALUES = new Set([
|
|
6859
|
+
"--arg-file",
|
|
6860
|
+
"--delimiter",
|
|
6861
|
+
"--max-args",
|
|
6862
|
+
"--max-chars",
|
|
6863
|
+
"--max-lines",
|
|
6864
|
+
"--max-procs",
|
|
6865
|
+
"--process-slot-var",
|
|
6866
|
+
]);
|
|
6867
|
+
const CONDUCTOR_XARGS_SHORT_OPTIONS_WITH_REQUIRED_VALUES = new Set(["a", "d", "E", "I", "L", "n", "P", "s"]);
|
|
6868
|
+
const CONDUCTOR_XARGS_SHORT_OPTIONS_WITH_OPTIONAL_ATTACHED_VALUES = new Set(["e", "i", "l"]);
|
|
6869
|
+
function conductorXargsOptionValueWordCount(words, optionIndex) {
|
|
6870
|
+
const word = words[optionIndex] ?? "";
|
|
6871
|
+
if (!word.startsWith("-") || word === "-")
|
|
6872
|
+
return 0;
|
|
6873
|
+
if (word.startsWith("--")) {
|
|
6874
|
+
const [option, inlineValue] = word.split("=", 2);
|
|
6875
|
+
return CONDUCTOR_XARGS_LONG_OPTIONS_WITH_REQUIRED_VALUES.has(option) && inlineValue === undefined ? 1 : 0;
|
|
6876
|
+
}
|
|
6877
|
+
const shortOptions = word.slice(1);
|
|
6878
|
+
for (let offset = 0; offset < shortOptions.length; offset += 1) {
|
|
6879
|
+
const option = shortOptions[offset] ?? "";
|
|
6880
|
+
if (CONDUCTOR_XARGS_SHORT_OPTIONS_WITH_REQUIRED_VALUES.has(option)) {
|
|
6881
|
+
if (offset < shortOptions.length - 1)
|
|
6882
|
+
return 0;
|
|
6883
|
+
return option === "I" && (words[optionIndex + 1] ?? "") === "{" && (words[optionIndex + 2] ?? "") === "}" ? 2 : 1;
|
|
6884
|
+
}
|
|
6885
|
+
if (CONDUCTOR_XARGS_SHORT_OPTIONS_WITH_OPTIONAL_ATTACHED_VALUES.has(option))
|
|
6886
|
+
return 0;
|
|
6887
|
+
}
|
|
6888
|
+
return 0;
|
|
6889
|
+
}
|
|
6890
|
+
function collectConductorXargsMutationTargets(words, commandIndex) {
|
|
6891
|
+
for (let index = commandIndex + 1; index < words.length; index += 1) {
|
|
6892
|
+
const word = words[index] ?? "";
|
|
6893
|
+
if (!word || isShellCommandTerminatorOrGroupClose(word))
|
|
6894
|
+
break;
|
|
6895
|
+
if (word === "--")
|
|
6896
|
+
continue;
|
|
6897
|
+
if (word.startsWith("-")) {
|
|
6898
|
+
index += conductorXargsOptionValueWordCount(words, index);
|
|
6899
|
+
continue;
|
|
6900
|
+
}
|
|
6901
|
+
let commandWordIndex = index;
|
|
6902
|
+
for (let unwrapCount = 0; unwrapCount < 8; unwrapCount += 1) {
|
|
6903
|
+
const commandName = commandNameFromShellWord(words[commandWordIndex] ?? "");
|
|
6904
|
+
if (CONDUCTOR_BASH_DOWNLOADER_COMMANDS.has(commandName) || CONDUCTOR_BASH_MUTATION_COMMANDS.has(commandName))
|
|
6905
|
+
return [];
|
|
6906
|
+
const wrapperOperandIndex = findConductorWrapperOperandIndex(commandName, words, commandWordIndex + 1);
|
|
6907
|
+
if (wrapperOperandIndex === undefined)
|
|
6908
|
+
return null;
|
|
6909
|
+
if (wrapperOperandIndex === null)
|
|
6910
|
+
return [];
|
|
6911
|
+
commandWordIndex = wrapperOperandIndex;
|
|
6912
|
+
}
|
|
6913
|
+
return [];
|
|
6914
|
+
}
|
|
6915
|
+
return null;
|
|
6916
|
+
}
|
|
6917
|
+
function collectConductorMutationCommandTargets(commandName, words, commandIndex) {
|
|
6918
|
+
const targets = [];
|
|
6919
|
+
const positionalTargets = [];
|
|
6920
|
+
const targetDirectoryTargets = [];
|
|
6921
|
+
let sawTargetDirectory = false;
|
|
6922
|
+
let positionalCount = 0;
|
|
6923
|
+
let rsyncRemovesSourceFiles = false;
|
|
6924
|
+
if (commandName === "sed")
|
|
6925
|
+
return collectConductorSedTargets(words, commandIndex);
|
|
6926
|
+
if (commandName === "perl")
|
|
6927
|
+
return collectConductorPerlTargets(words, commandIndex);
|
|
6928
|
+
const installDirectoryMode = commandName === "install" && isConductorInstallDirectoryMode(words, commandIndex);
|
|
6929
|
+
for (let index = commandIndex + 1; index < words.length; index += 1) {
|
|
6930
|
+
const word = words[index] ?? "";
|
|
6931
|
+
if (!word || isShellCommandTerminatorOrGroupClose(word))
|
|
6932
|
+
break;
|
|
6933
|
+
if (CONDUCTOR_BASH_COMPOUND_SYNTAX_WORDS.has(word))
|
|
6934
|
+
continue;
|
|
6935
|
+
if (isEnvironmentAssignmentWord(word))
|
|
6936
|
+
continue;
|
|
6937
|
+
if (commandName === "dd") {
|
|
6938
|
+
const ofMatch = word.match(/^of=(.+)$/);
|
|
6939
|
+
if (ofMatch)
|
|
6940
|
+
targets.push(safeString(ofMatch[1]).trim());
|
|
6941
|
+
continue;
|
|
6942
|
+
}
|
|
6943
|
+
if (word === "--")
|
|
6944
|
+
continue;
|
|
6945
|
+
if (commandName === "rsync" && word === "--remove-source-files") {
|
|
6946
|
+
rsyncRemovesSourceFiles = true;
|
|
6947
|
+
continue;
|
|
6948
|
+
}
|
|
6949
|
+
if (word.startsWith("--")) {
|
|
6950
|
+
const [option, inlineValue] = word.split("=", 2);
|
|
6951
|
+
if (option === "--target-directory" && commandUsesTargetDirectoryOption(commandName)) {
|
|
6952
|
+
sawTargetDirectory = true;
|
|
6953
|
+
if (inlineValue !== undefined) {
|
|
6954
|
+
const target = safeString(inlineValue).trim();
|
|
6955
|
+
if (target)
|
|
6956
|
+
targetDirectoryTargets.push(target);
|
|
6957
|
+
}
|
|
6958
|
+
else {
|
|
6959
|
+
const target = words[index + 1] ?? "";
|
|
6960
|
+
if (target && !isShellCommandTerminatorOrGroupClose(target))
|
|
6961
|
+
targetDirectoryTargets.push(target);
|
|
6962
|
+
index += 1;
|
|
6963
|
+
}
|
|
6964
|
+
continue;
|
|
6965
|
+
}
|
|
6966
|
+
if (CONDUCTOR_BASH_OPTIONS_WITH_VALUES.has(option) && inlineValue === undefined)
|
|
6967
|
+
index += 1;
|
|
6968
|
+
if ((option === "--backup" || option === "--suffix" || option === "--reference") && inlineValue) {
|
|
6969
|
+
targets.push(inlineValue);
|
|
6970
|
+
}
|
|
6971
|
+
continue;
|
|
6972
|
+
}
|
|
6973
|
+
if (word === "-t" && commandUsesTargetDirectoryOption(commandName)) {
|
|
6974
|
+
sawTargetDirectory = true;
|
|
6975
|
+
const target = words[index + 1] ?? "";
|
|
6976
|
+
if (target && !isShellCommandTerminatorOrGroupClose(target))
|
|
6977
|
+
targetDirectoryTargets.push(target);
|
|
6978
|
+
index += 1;
|
|
6979
|
+
continue;
|
|
6980
|
+
}
|
|
6981
|
+
if (word.startsWith("-t") && word.length > 2 && commandUsesTargetDirectoryOption(commandName)) {
|
|
6982
|
+
sawTargetDirectory = true;
|
|
6983
|
+
targetDirectoryTargets.push(word.slice(2));
|
|
6984
|
+
continue;
|
|
6985
|
+
}
|
|
6986
|
+
if (word.startsWith("-") && word.length > 1) {
|
|
6987
|
+
if (CONDUCTOR_BASH_OPTIONS_WITH_VALUES.has(word))
|
|
6988
|
+
index += 1;
|
|
6989
|
+
continue;
|
|
6990
|
+
}
|
|
6991
|
+
positionalCount += 1;
|
|
6992
|
+
if ((commandName === "chmod" || commandName === "chown" || commandName === "chgrp")
|
|
6993
|
+
&& positionalCount === 1) {
|
|
6994
|
+
continue;
|
|
6995
|
+
}
|
|
6996
|
+
positionalTargets.push(word);
|
|
6997
|
+
}
|
|
6998
|
+
if (installDirectoryMode)
|
|
6999
|
+
return [...targets, ...positionalTargets];
|
|
7000
|
+
if (sawTargetDirectory) {
|
|
7001
|
+
return commandName === "mv"
|
|
7002
|
+
? [...targets, ...targetDirectoryTargets, ...positionalTargets]
|
|
7003
|
+
: [...targets, ...targetDirectoryTargets];
|
|
7004
|
+
}
|
|
7005
|
+
if (commandName === "rsync" && rsyncRemovesSourceFiles)
|
|
7006
|
+
return [...targets, ...positionalTargets];
|
|
7007
|
+
if (isConductorDestinationOnlyMutationCommand(commandName))
|
|
7008
|
+
return [...targets, ...positionalTargets.slice(-1)];
|
|
7009
|
+
return [...targets, ...positionalTargets];
|
|
7010
|
+
}
|
|
7011
|
+
function extractConductorBashMutations(command) {
|
|
7012
|
+
const mutations = [];
|
|
7013
|
+
for (const segment of splitShellCommandSegments(stripHeredocBodiesForCommandScan(command))) {
|
|
7014
|
+
const words = tokenizeShellWords(segment);
|
|
7015
|
+
let commandStart = true;
|
|
7016
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
7017
|
+
const word = words[index] ?? "";
|
|
7018
|
+
if (!word)
|
|
7019
|
+
continue;
|
|
7020
|
+
if (isShellCommandSeparator(word)) {
|
|
7021
|
+
commandStart = true;
|
|
7022
|
+
continue;
|
|
7023
|
+
}
|
|
7024
|
+
if (isShellGroupingSyntaxWord(word)) {
|
|
7025
|
+
continue;
|
|
7026
|
+
}
|
|
7027
|
+
if (commandStart && isEnvironmentAssignmentWord(word))
|
|
7028
|
+
continue;
|
|
7029
|
+
if (commandStart && CONDUCTOR_BASH_COMPOUND_SYNTAX_WORDS.has(word))
|
|
7030
|
+
continue;
|
|
7031
|
+
if (commandStart && word.startsWith("-"))
|
|
7032
|
+
continue;
|
|
7033
|
+
if (!commandStart)
|
|
7034
|
+
continue;
|
|
7035
|
+
const commandName = commandNameFromShellWord(word);
|
|
7036
|
+
if (CONDUCTOR_BASH_TRANSPARENT_WRAPPERS.has(commandName)) {
|
|
7037
|
+
continue;
|
|
7038
|
+
}
|
|
7039
|
+
const wrapperOperandIndex = findConductorWrapperOperandIndex(commandName, words, index + 1);
|
|
7040
|
+
if (wrapperOperandIndex !== undefined) {
|
|
7041
|
+
if (wrapperOperandIndex === null) {
|
|
7042
|
+
mutations.push({ command: commandName, targets: [] });
|
|
7043
|
+
commandStart = false;
|
|
7044
|
+
}
|
|
7045
|
+
else {
|
|
7046
|
+
index = wrapperOperandIndex - 1;
|
|
7047
|
+
}
|
|
7048
|
+
continue;
|
|
7049
|
+
}
|
|
7050
|
+
if (CONDUCTOR_BASH_DOWNLOADER_COMMANDS.has(commandName)) {
|
|
7051
|
+
const downloaderTargets = collectConductorDownloaderOutputTargets(commandName, words, index);
|
|
7052
|
+
if (downloaderTargets.sawOutputFlag) {
|
|
7053
|
+
mutations.push({
|
|
7054
|
+
command: commandName,
|
|
7055
|
+
targets: downloaderTargets.targets,
|
|
7056
|
+
});
|
|
7057
|
+
}
|
|
7058
|
+
}
|
|
7059
|
+
else if (commandName === "xargs") {
|
|
7060
|
+
const xargsTargets = collectConductorXargsMutationTargets(words, index);
|
|
7061
|
+
if (xargsTargets !== null)
|
|
7062
|
+
mutations.push({ command: commandName, targets: xargsTargets });
|
|
7063
|
+
}
|
|
7064
|
+
else if (CONDUCTOR_BASH_MUTATION_COMMANDS.has(commandName)) {
|
|
7065
|
+
const mutationTargets = collectConductorMutationCommandTargets(commandName, words, index);
|
|
7066
|
+
if (mutationTargets !== null)
|
|
7067
|
+
mutations.push({ command: commandName, targets: mutationTargets });
|
|
7068
|
+
}
|
|
7069
|
+
commandStart = false;
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
return mutations;
|
|
7073
|
+
}
|
|
7074
|
+
const CONDUCTOR_BASH_MAX_NESTING_DEPTH = 5;
|
|
7075
|
+
function evaluateConductorBashWrite(cwd, command, depth = 0) {
|
|
7076
|
+
const commandWithHeredocBodies = normalizeShellLineContinuations(command);
|
|
7077
|
+
const normalizedCommand = stripHeredocBodiesForCommandScan(commandWithHeredocBodies);
|
|
7078
|
+
if (depth > CONDUCTOR_BASH_MAX_NESTING_DEPTH) {
|
|
7079
|
+
return {
|
|
7080
|
+
allowed: false,
|
|
7081
|
+
blockedDetail: "Bash nested shell depth exceeded Main-root Conductor validation limits",
|
|
7082
|
+
};
|
|
7083
|
+
}
|
|
7084
|
+
if (hasDynamicNestedShellExecution(normalizedCommand)) {
|
|
7085
|
+
return {
|
|
7086
|
+
allowed: false,
|
|
7087
|
+
blockedDetail: "Bash nested shell execution is dynamic and cannot be validated for Main-root Conductor writes",
|
|
7088
|
+
};
|
|
7089
|
+
}
|
|
7090
|
+
if (hasUnsafeUnquotedHeredocExpansion(commandWithHeredocBodies)) {
|
|
7091
|
+
return {
|
|
7092
|
+
allowed: false,
|
|
7093
|
+
blockedDetail: "Bash unquoted heredoc expansion is not workflow state/ledger/mailbox/handoff metadata",
|
|
7094
|
+
};
|
|
7095
|
+
}
|
|
7096
|
+
const shellMutations = extractConductorBashMutations(normalizedCommand);
|
|
7097
|
+
if (shellMutations.length > 0) {
|
|
7098
|
+
for (const mutation of shellMutations) {
|
|
7099
|
+
if (mutation.targets.length === 0) {
|
|
7100
|
+
return {
|
|
7101
|
+
allowed: false,
|
|
7102
|
+
blockedDetail: `Bash ${mutation.command} mutation target <unresolved>; Main-root Conductor may write only workflow state/ledger/mailbox/handoff metadata`,
|
|
7103
|
+
};
|
|
7104
|
+
}
|
|
7105
|
+
const blockedTarget = mutation.targets.find((target) => !isAllowedConductorMetadataPath(cwd, target));
|
|
7106
|
+
if (blockedTarget) {
|
|
7107
|
+
return {
|
|
7108
|
+
allowed: false,
|
|
7109
|
+
blockedDetail: `Bash ${mutation.command} mutation target ${blockedTarget} is not workflow state/ledger/mailbox/handoff metadata`,
|
|
7110
|
+
};
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
for (const functionBody of extractInvokedShellFunctionBodiesForStateScan(normalizedCommand)) {
|
|
7115
|
+
const nestedDecision = evaluateConductorBashWrite(cwd, functionBody, depth + 1);
|
|
7116
|
+
if (!nestedDecision.allowed)
|
|
7117
|
+
return nestedDecision;
|
|
7118
|
+
}
|
|
7119
|
+
for (const nestedCommand of extractNestedShellCommandStringsForStateScan(normalizedCommand)) {
|
|
7120
|
+
const nestedDecision = evaluateConductorBashWrite(cwd, nestedCommand, depth + 1);
|
|
7121
|
+
if (!nestedDecision.allowed)
|
|
7122
|
+
return nestedDecision;
|
|
7123
|
+
}
|
|
7124
|
+
for (const nestedCommand of extractNestedCommandSubstitutionStringsForStateScan(normalizedCommand)) {
|
|
7125
|
+
const nestedDecision = evaluateConductorBashWrite(cwd, nestedCommand, depth + 1);
|
|
7126
|
+
if (!nestedDecision.allowed)
|
|
7127
|
+
return nestedDecision;
|
|
7128
|
+
}
|
|
7129
|
+
for (const nestedCommand of extractNestedProcessSubstitutionStringsForStateScan(normalizedCommand)) {
|
|
7130
|
+
const nestedDecision = evaluateConductorBashWrite(cwd, nestedCommand, depth + 1);
|
|
7131
|
+
if (!nestedDecision.allowed)
|
|
7132
|
+
return nestedDecision;
|
|
7133
|
+
}
|
|
7134
|
+
const editorTargets = extractConductorEditorWriteTargets(commandWithHeredocBodies);
|
|
7135
|
+
if (editorTargets.length > 0) {
|
|
7136
|
+
const blockedTarget = editorTargets.find((target) => !isAllowedConductorMetadataPath(cwd, target));
|
|
7137
|
+
if (blockedTarget) {
|
|
7138
|
+
return {
|
|
7139
|
+
allowed: false,
|
|
7140
|
+
blockedDetail: `Bash editor mutation target ${blockedTarget} is not workflow state/ledger/mailbox/handoff metadata`,
|
|
7141
|
+
};
|
|
7142
|
+
}
|
|
7143
|
+
}
|
|
7144
|
+
if (commandHasDestructiveGitSubcommand(normalizedCommand)) {
|
|
7145
|
+
return {
|
|
7146
|
+
allowed: false,
|
|
7147
|
+
blockedDetail: "Bash git worktree mutation is not workflow state/ledger/mailbox/handoff metadata",
|
|
7148
|
+
};
|
|
7149
|
+
}
|
|
7150
|
+
if (commandHasPackageInstallIntent(normalizedCommand)) {
|
|
7151
|
+
return {
|
|
7152
|
+
allowed: false,
|
|
7153
|
+
blockedDetail: "Bash package manager install is not workflow state/ledger/mailbox/handoff metadata",
|
|
7154
|
+
};
|
|
7155
|
+
}
|
|
7156
|
+
const interpreterWrites = extractConductorInterpreterWrites(commandWithHeredocBodies);
|
|
7157
|
+
for (const write of interpreterWrites) {
|
|
7158
|
+
if (write.unresolved || write.targets.length === 0) {
|
|
7159
|
+
return {
|
|
7160
|
+
allowed: false,
|
|
7161
|
+
blockedDetail: `Bash ${write.runtime} write target <unresolved>; Main-root Conductor may write only workflow state/ledger/mailbox/handoff metadata`,
|
|
7162
|
+
};
|
|
7163
|
+
}
|
|
7164
|
+
const blockedTarget = write.targets.find((target) => !isAllowedConductorMetadataPath(cwd, target));
|
|
7165
|
+
if (blockedTarget) {
|
|
7166
|
+
return {
|
|
7167
|
+
allowed: false,
|
|
7168
|
+
blockedDetail: `Bash ${write.runtime} write target ${blockedTarget} is not workflow state/ledger/mailbox/handoff metadata`,
|
|
7169
|
+
};
|
|
7170
|
+
}
|
|
7171
|
+
}
|
|
7172
|
+
const hasGenericWriteIntent = commandHasDeepInterviewWriteIntent(commandWithHeredocBodies);
|
|
7173
|
+
if (!hasGenericWriteIntent)
|
|
7174
|
+
return { allowed: true };
|
|
7175
|
+
const targets = extractDeepInterviewCommandWriteTargets(commandWithHeredocBodies);
|
|
7176
|
+
const accountedShellOnlyWriteIntent = shellMutations.length > 0
|
|
7177
|
+
&& targets.length === 0
|
|
7178
|
+
&& shellMutations.every((mutation) => mutation.command === "sed" || mutation.command === "perl");
|
|
7179
|
+
if (accountedShellOnlyWriteIntent)
|
|
7180
|
+
return { allowed: true };
|
|
7181
|
+
if (commandInvokesApplyPatch(normalizedCommand) && targets.length === 0) {
|
|
7182
|
+
return {
|
|
7183
|
+
allowed: false,
|
|
7184
|
+
blockedDetail: "apply_patch target extraction failed for Main-root Conductor write",
|
|
7185
|
+
};
|
|
7186
|
+
}
|
|
7187
|
+
if (targets.length === 0) {
|
|
7188
|
+
return {
|
|
7189
|
+
allowed: false,
|
|
7190
|
+
blockedDetail: "Bash write intent target <unresolved>; Main-root Conductor may write only workflow state/ledger/mailbox/handoff metadata",
|
|
7191
|
+
};
|
|
7192
|
+
}
|
|
7193
|
+
const blockedTarget = targets.find((target) => !isAllowedConductorMetadataPath(cwd, target));
|
|
7194
|
+
if (blockedTarget) {
|
|
7195
|
+
const operationClass = /\btee\s+(?:-a\s+)?/.test(commandWithHeredocBodies) ? "Bash tee write" : "Bash write";
|
|
7196
|
+
return {
|
|
7197
|
+
allowed: false,
|
|
7198
|
+
blockedDetail: `${operationClass} target ${blockedTarget} is not workflow state/ledger/mailbox/handoff metadata`,
|
|
7199
|
+
};
|
|
7200
|
+
}
|
|
7201
|
+
return { allowed: true };
|
|
7202
|
+
}
|
|
7203
|
+
function isAllowedConductorBashWrite(cwd, command) {
|
|
7204
|
+
return evaluateConductorBashWrite(cwd, command).allowed;
|
|
7205
|
+
}
|
|
7206
|
+
function buildConductorBashBlockedDetail(cwd, command) {
|
|
7207
|
+
return evaluateConductorBashWrite(cwd, command).blockedDetail
|
|
7208
|
+
?? "Bash write intent target <unresolved>; Main-root Conductor may write only workflow state/ledger/mailbox/handoff metadata";
|
|
7209
|
+
}
|
|
7210
|
+
export async function buildConductorPreToolUseWriteGuardOutput(payload, cwd, stateDir, resolvedSessionId) {
|
|
7211
|
+
const activeState = await readActiveMainRootConductorStateForPreToolUse(payload, cwd, stateDir, resolvedSessionId);
|
|
3093
7212
|
if (!activeState)
|
|
3094
7213
|
return null;
|
|
3095
7214
|
const toolName = safeString(payload.tool_name).trim();
|
|
3096
7215
|
const command = readPreToolUseCommand(payload);
|
|
3097
7216
|
const pathCandidates = readPreToolUsePathCandidates(payload);
|
|
3098
7217
|
let blocked = false;
|
|
3099
|
-
let blockedDetail = "
|
|
7218
|
+
let blockedDetail = "Main-root Conductor write is not delegated";
|
|
3100
7219
|
if (toolName === "Bash") {
|
|
3101
|
-
blocked = !
|
|
3102
|
-
if (blocked)
|
|
3103
|
-
blockedDetail =
|
|
3104
|
-
}
|
|
7220
|
+
blocked = !isAllowedConductorBashWrite(cwd, command);
|
|
7221
|
+
if (blocked)
|
|
7222
|
+
blockedDetail = buildConductorBashBlockedDetail(cwd, command);
|
|
3105
7223
|
}
|
|
3106
7224
|
else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
|
|
3107
7225
|
const toolPathCandidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
|
|
3108
7226
|
if (toolPathCandidates.length === 0) {
|
|
3109
7227
|
blocked = true;
|
|
3110
|
-
blockedDetail =
|
|
7228
|
+
blockedDetail = describeConductorBlockedWrite(toolName, undefined, toolPathCandidates.length);
|
|
3111
7229
|
}
|
|
3112
7230
|
else {
|
|
3113
|
-
const blockedPath = toolPathCandidates.find((candidate) => !
|
|
7231
|
+
const blockedPath = toolPathCandidates.find((candidate) => !isAllowedConductorMetadataPath(cwd, candidate));
|
|
3114
7232
|
blocked = blockedPath !== undefined;
|
|
3115
7233
|
if (blockedPath !== undefined) {
|
|
3116
|
-
blockedDetail =
|
|
7234
|
+
blockedDetail = describeConductorBlockedWrite(toolName, blockedPath, toolPathCandidates.length);
|
|
3117
7235
|
}
|
|
3118
7236
|
}
|
|
3119
7237
|
}
|
|
3120
7238
|
if (!blocked)
|
|
3121
7239
|
return null;
|
|
3122
|
-
const phase = formatPhase(activeState.current_phase ?? activeState.currentPhase, "planning");
|
|
3123
|
-
const activeMode = safeString(activeState.mode).trim().toLowerCase();
|
|
3124
|
-
const planningModeLabel = activeMode === "autopilot" ? "Autopilot planning" : "Ralplan";
|
|
3125
|
-
const planningModeDescription = activeMode === "autopilot"
|
|
3126
|
-
? "Autopilot is supervising a planning phase"
|
|
3127
|
-
: "Ralplan is consensus-planning mode";
|
|
3128
7240
|
return {
|
|
3129
7241
|
decision: "block",
|
|
3130
|
-
reason:
|
|
7242
|
+
reason: `Main-root Conductor mode is active (${activeState.mode} phase: ${formatPhase(activeState.phase, "active")}); direct plan/code writes are blocked and must be delegated; ${blockedDetail}.`,
|
|
3131
7243
|
hookSpecificOutput: {
|
|
3132
7244
|
hookEventName: "PreToolUse",
|
|
3133
|
-
additionalContext: `${
|
|
3134
|
-
+ "
|
|
3135
|
-
+ "
|
|
3136
|
-
+
|
|
7245
|
+
additionalContext: `${LEADER_CONDUCTOR_GOLDEN_RULE} `
|
|
7246
|
+
+ "Use specialized agents for source edits and plan/spec authorship. "
|
|
7247
|
+
+ `Main-root Conductor may write only orchestration metadata/transport/ledger artifacts under ${CONDUCTOR_ORCHESTRATION_METADATA_PREFIXES.join(", ")}; path location alone is not authorization for substantive deliverables. `
|
|
7248
|
+
+ "Autopilot rework and typed subagent/worker lanes are exempt from this guard.",
|
|
3137
7249
|
},
|
|
3138
7250
|
};
|
|
3139
7251
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
const
|
|
3149
|
-
let
|
|
3150
|
-
|
|
3151
|
-
|
|
7252
|
+
function isInPlaceEditorCommand(word, commandName) {
|
|
7253
|
+
if (commandName === "sed")
|
|
7254
|
+
return word === "--in-place" || /^--in-place(?:=.+)?$/.test(word) || /^-[^-\s]*i(?:.*)?$/.test(word);
|
|
7255
|
+
if (commandName === "perl")
|
|
7256
|
+
return word === "-i" || word === "-pi" || /^-pi(?:\..+)?$/.test(word) || /^-p.*i(?:\..+)?$/.test(word);
|
|
7257
|
+
return false;
|
|
7258
|
+
}
|
|
7259
|
+
function collectInPlaceEditorWriteTargets(commandName, words, commandIndex) {
|
|
7260
|
+
const targets = [];
|
|
7261
|
+
let sawInPlaceEdit = false;
|
|
7262
|
+
let awaitingOptionValue = false;
|
|
7263
|
+
let consumedImplicitSedScript = commandName !== "sed";
|
|
7264
|
+
for (let index = commandIndex + 1; index < words.length; index += 1) {
|
|
7265
|
+
const word = words[index] ?? "";
|
|
7266
|
+
if (!word || isShellCommandSeparator(word))
|
|
7267
|
+
break;
|
|
7268
|
+
if (CONDUCTOR_BASH_COMPOUND_SYNTAX_WORDS.has(word))
|
|
7269
|
+
continue;
|
|
7270
|
+
if (isEnvironmentAssignmentWord(word))
|
|
7271
|
+
continue;
|
|
7272
|
+
if (awaitingOptionValue) {
|
|
7273
|
+
awaitingOptionValue = false;
|
|
7274
|
+
continue;
|
|
7275
|
+
}
|
|
7276
|
+
if (word === "--") {
|
|
7277
|
+
consumedImplicitSedScript = true;
|
|
7278
|
+
continue;
|
|
7279
|
+
}
|
|
7280
|
+
if (word === "-e" || word === "-f" || word === "--expression" || word === "--file") {
|
|
7281
|
+
awaitingOptionValue = true;
|
|
7282
|
+
consumedImplicitSedScript = true;
|
|
7283
|
+
continue;
|
|
7284
|
+
}
|
|
7285
|
+
if (word.startsWith("-e") || word.startsWith("-f") || word.startsWith("--expression=") || word.startsWith("--file=")) {
|
|
7286
|
+
consumedImplicitSedScript = true;
|
|
7287
|
+
continue;
|
|
7288
|
+
}
|
|
7289
|
+
if (isInPlaceEditorCommand(word, commandName)) {
|
|
7290
|
+
sawInPlaceEdit = true;
|
|
7291
|
+
continue;
|
|
7292
|
+
}
|
|
7293
|
+
if (word.startsWith("-"))
|
|
7294
|
+
continue;
|
|
7295
|
+
if (sawInPlaceEdit) {
|
|
7296
|
+
if (commandName === "sed" && !consumedImplicitSedScript) {
|
|
7297
|
+
consumedImplicitSedScript = true;
|
|
7298
|
+
continue;
|
|
7299
|
+
}
|
|
7300
|
+
targets.push(word);
|
|
7301
|
+
}
|
|
3152
7302
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
7303
|
+
return targets;
|
|
7304
|
+
}
|
|
7305
|
+
function extractConductorEditorWriteTargets(command) {
|
|
7306
|
+
const targets = [];
|
|
7307
|
+
for (const segment of splitShellCommandSegments(stripHeredocBodiesForCommandScan(command))) {
|
|
7308
|
+
const words = tokenizeShellWords(segment);
|
|
7309
|
+
let commandStart = true;
|
|
7310
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
7311
|
+
const word = words[index] ?? "";
|
|
7312
|
+
if (!word)
|
|
7313
|
+
continue;
|
|
7314
|
+
if (isShellCommandSeparator(word)) {
|
|
7315
|
+
commandStart = true;
|
|
7316
|
+
continue;
|
|
7317
|
+
}
|
|
7318
|
+
if (isShellGroupingSyntaxWord(word))
|
|
7319
|
+
continue;
|
|
7320
|
+
if (commandStart && isEnvironmentAssignmentWord(word))
|
|
7321
|
+
continue;
|
|
7322
|
+
if (commandStart && CONDUCTOR_BASH_COMPOUND_SYNTAX_WORDS.has(word))
|
|
7323
|
+
continue;
|
|
7324
|
+
if (commandStart && word.startsWith("-"))
|
|
7325
|
+
continue;
|
|
7326
|
+
if (!commandStart)
|
|
7327
|
+
continue;
|
|
7328
|
+
const commandName = commandNameFromShellWord(word);
|
|
7329
|
+
if (commandName === "sed" || commandName === "perl") {
|
|
7330
|
+
targets.push(...collectInPlaceEditorWriteTargets(commandName, words, index));
|
|
7331
|
+
}
|
|
7332
|
+
commandStart = false;
|
|
7333
|
+
}
|
|
3157
7334
|
}
|
|
3158
|
-
|
|
3159
|
-
return null;
|
|
3160
|
-
const phase = formatPhase(activeState.current_phase ?? activeState.currentPhase, "planning");
|
|
3161
|
-
return {
|
|
3162
|
-
decision: "block",
|
|
3163
|
-
reason: `Deep-interview is active (phase: ${phase}); implementation/write tools are blocked until an explicit handoff workflow is activated.`,
|
|
3164
|
-
hookSpecificOutput: {
|
|
3165
|
-
hookEventName: "PreToolUse",
|
|
3166
|
-
additionalContext: `Deep-interview is requirements/spec mode. Treat detailed user answers as interview/spec material, not implicit implementation authorization. You may write only deep-interview artifacts under \`.omx/context/\`, \`.omx/interviews/\`, \`.omx/specs/\`, or required \`.omx/state/\` files. To implement, first ask for or process an explicit transition such as \`$ralplan\`, \`$autopilot\`, ${formatExecutionHandoffList(cwd)}.`,
|
|
3167
|
-
},
|
|
3168
|
-
};
|
|
7335
|
+
return targets;
|
|
3169
7336
|
}
|
|
3170
7337
|
function matchesSkillStopContext(entry, state, sessionId, threadId) {
|
|
3171
7338
|
const entrySessionId = safeString(entry.session_id ?? state.session_id).trim();
|
|
@@ -4102,8 +8269,9 @@ export async function dispatchCodexNativeHook(payload, options = {}) {
|
|
|
4102
8269
|
const eventSessionId = canonicalSessionId || nativeSessionId || undefined;
|
|
4103
8270
|
const sessionIdForState = canonicalSessionId || nativeSessionId;
|
|
4104
8271
|
let outputJson = null;
|
|
8272
|
+
const typedAgentRolePayload = isTypedAgentRolePayload(payload);
|
|
4105
8273
|
const isSubagentPromptSubmit = hookEventName === "UserPromptSubmit"
|
|
4106
|
-
? await isNativeSubagentHook(cwd, canonicalSessionId, nativeSessionId, threadId, safeString(currentSessionState?.native_session_id).trim())
|
|
8274
|
+
? typedAgentRolePayload || await isNativeSubagentHook(cwd, canonicalSessionId, nativeSessionId, threadId, safeString(currentSessionState?.native_session_id).trim())
|
|
4107
8275
|
: false;
|
|
4108
8276
|
const isSubagentStop = hookEventName === "Stop"
|
|
4109
8277
|
? (await Promise.all([...new Set([
|
|
@@ -4272,12 +8440,16 @@ export async function dispatchCodexNativeHook(payload, options = {}) {
|
|
|
4272
8440
|
}
|
|
4273
8441
|
else if (hookEventName === "PreToolUse") {
|
|
4274
8442
|
const payloadSessionId = readPayloadSessionId(payload);
|
|
8443
|
+
const rootPointerConflict = await readLiveRootSessionPointerConflict(stateDir, payloadSessionId);
|
|
4275
8444
|
const preToolUseSessionId = payloadSessionId
|
|
4276
8445
|
? await resolveInternalSessionIdForPayload(cwd, payloadSessionId, stateDir)
|
|
4277
8446
|
: "";
|
|
4278
8447
|
outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
|
|
4279
8448
|
?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
|
|
8449
|
+
?? await buildPlanningRootPointerConflictPreToolUseOutput(payload, cwd, stateDir, rootPointerConflict)
|
|
8450
|
+
?? await buildConductorPreToolUseWriteGuardOutput(payload, cwd, stateDir, preToolUseSessionId)
|
|
4280
8451
|
?? await buildNativeSubagentCapacityCloseGuardOutput(payload, cwd, stateDir)
|
|
8452
|
+
?? buildMalformedPreToolUseBlockTestOutput(payload)
|
|
4281
8453
|
?? buildNativePreToolUseOutput(payload);
|
|
4282
8454
|
}
|
|
4283
8455
|
else if (hookEventName === "PostToolUse") {
|
|
@@ -4425,13 +8597,16 @@ async function buildOversizedStopActiveWorkflowOutput(cwd) {
|
|
|
4425
8597
|
systemMessage: "OMX native Stop rejected oversized stdin before parsing; active current-session workflow state is present, so Stop is blocked instead of silently allowing termination.",
|
|
4426
8598
|
};
|
|
4427
8599
|
}
|
|
8600
|
+
function buildOversizedStopInactiveWorkflowOutput() {
|
|
8601
|
+
return {};
|
|
8602
|
+
}
|
|
4428
8603
|
async function buildOversizedStdinHookOutput(rawHookEventName, cwd) {
|
|
4429
8604
|
const systemMessage = `OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
|
|
4430
8605
|
if (rawHookEventName === "PreToolUse") {
|
|
4431
8606
|
return { systemMessage };
|
|
4432
8607
|
}
|
|
4433
8608
|
if (rawHookEventName === "Stop") {
|
|
4434
|
-
return await buildOversizedStopActiveWorkflowOutput(cwd) ??
|
|
8609
|
+
return await buildOversizedStopActiveWorkflowOutput(cwd) ?? buildOversizedStopInactiveWorkflowOutput();
|
|
4435
8610
|
}
|
|
4436
8611
|
return {
|
|
4437
8612
|
continue: false,
|
|
@@ -4482,6 +8657,28 @@ function isDispatchFailureTestTrigger() {
|
|
|
4482
8657
|
return process.env.NODE_ENV === "test"
|
|
4483
8658
|
&& process.env.OMX_NATIVE_HOOK_TEST_THROW_DISPATCH === "1";
|
|
4484
8659
|
}
|
|
8660
|
+
function buildMalformedPreToolUseBlockTestOutput(payload) {
|
|
8661
|
+
if (process.env.NODE_ENV !== "test" || readHookEventName(payload) !== "PreToolUse")
|
|
8662
|
+
return null;
|
|
8663
|
+
switch (process.env.OMX_NATIVE_HOOK_TEST_MALFORMED_PRETOOL_BLOCK) {
|
|
8664
|
+
case "legacy":
|
|
8665
|
+
return {
|
|
8666
|
+
decision: "block",
|
|
8667
|
+
systemMessage: "This advisory text must not validate a malformed legacy PreToolUse block.",
|
|
8668
|
+
};
|
|
8669
|
+
case "deny":
|
|
8670
|
+
return {
|
|
8671
|
+
hookSpecificOutput: {
|
|
8672
|
+
hookEventName: "PreToolUse",
|
|
8673
|
+
permissionDecision: "deny",
|
|
8674
|
+
permissionDecisionReason: " ",
|
|
8675
|
+
},
|
|
8676
|
+
systemMessage: "This advisory text must not validate a malformed deny PreToolUse block.",
|
|
8677
|
+
};
|
|
8678
|
+
default:
|
|
8679
|
+
return null;
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
4485
8682
|
function buildStopDispatchFailureOutput(error) {
|
|
4486
8683
|
const detail = error instanceof Error ? error.message : String(error);
|
|
4487
8684
|
const reason = "OMX native Stop hook failed before normal continuation handling. Continue once more, preserve runtime state, inspect the hook logs, and retry with a valid Stop JSON response.";
|