oh-my-codex 0.19.1 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +3 -1
- package/crates/omx-api/src/lib.rs +3 -3
- package/crates/omx-api/tests/cli.rs +4 -4
- package/crates/omx-explore/src/main.rs +4 -4
- package/crates/omx-sparkshell/src/codex_bridge.rs +2 -2
- package/dist/agents/__tests__/definitions.test.js +3 -3
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +71 -42
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +1 -1
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +3 -3
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -2
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +7 -5
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/planner-routing.test.js +19 -12
- package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +151 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/planner-routing.d.ts.map +1 -1
- package/dist/autopilot/planner-routing.js +4 -2
- package/dist/autopilot/planner-routing.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts +1 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +38 -2
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/autoresearch/runtime.d.ts +2 -0
- package/dist/autoresearch/runtime.d.ts.map +1 -1
- package/dist/autoresearch/runtime.js +8 -0
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/capabilities/__tests__/lockfile.test.d.ts +2 -0
- package/dist/capabilities/__tests__/lockfile.test.d.ts.map +1 -0
- package/dist/capabilities/__tests__/lockfile.test.js +115 -0
- package/dist/capabilities/__tests__/lockfile.test.js.map +1 -0
- package/dist/capabilities/lockfile.d.ts +102 -0
- package/dist/capabilities/lockfile.d.ts.map +1 -0
- package/dist/capabilities/lockfile.js +334 -0
- package/dist/capabilities/lockfile.js.map +1 -0
- package/dist/cli/__tests__/agents.test.js +5 -5
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/capabilities.test.d.ts +2 -0
- package/dist/cli/__tests__/capabilities.test.d.ts.map +1 -0
- package/dist/cli/__tests__/capabilities.test.js +135 -0
- package/dist/cli/__tests__/capabilities.test.js.map +1 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js +264 -6
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +10 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-spark-routing.test.js +22 -6
- package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +87 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +53 -18
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js +3 -3
- package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +95 -6
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +54 -2
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +49 -21
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +3 -3
- package/dist/cli/__tests__/setup-refresh.test.js +166 -12
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +27 -3
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +301 -7
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/agents.js +1 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/capabilities.d.ts +3 -0
- package/dist/cli/capabilities.d.ts.map +1 -0
- package/dist/cli/capabilities.js +118 -0
- package/dist/cli/capabilities.js.map +1 -0
- package/dist/cli/doctor.d.ts +3 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +50 -34
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +7 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +77 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +1 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +31 -0
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +9 -6
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +3 -4
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +210 -114
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +26 -52
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +51 -26
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +20 -11
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +169 -152
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +16 -6
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +19 -8
- package/dist/config/models.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +12 -4
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js +2 -2
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js +2 -2
- package/dist/leader/__tests__/contract.test.js +150 -1
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +26 -0
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +188 -0
- package/dist/leader/contract.js.map +1 -1
- package/dist/modes/base.d.ts +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +27 -1
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts +2 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +1 -0
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.d.ts +5 -1
- package/dist/pipeline/stages/ultragoal.d.ts.map +1 -1
- package/dist/pipeline/stages/ultragoal.js +14 -4
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +132 -0
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +17 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +97 -1
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1528 -32
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +377 -31
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +450 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +14 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +72 -11
- package/dist/state/operations.js.map +1 -1
- package/dist/state/terminal-normalization.d.ts +8 -0
- package/dist/state/terminal-normalization.d.ts.map +1 -0
- package/dist/state/terminal-normalization.js +127 -0
- package/dist/state/terminal-normalization.js.map +1 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +3 -1
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/team/__tests__/delegation-policy.test.js +2 -2
- package/dist/team/__tests__/delegation-policy.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +25 -25
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +28 -28
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +7 -7
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js +5 -5
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +12 -12
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +57 -9
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +3 -3
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/model-contract.d.ts +1 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +11 -0
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +2 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +6 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +68 -1
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +5 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +41 -9
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +16 -0
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +7 -7
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/__tests__/worktree-tool-context.test.d.ts +2 -0
- package/dist/utils/__tests__/worktree-tool-context.test.d.ts.map +1 -0
- package/dist/utils/__tests__/worktree-tool-context.test.js +118 -0
- package/dist/utils/__tests__/worktree-tool-context.test.js.map +1 -0
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +4 -3
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/worktree-tool-context.d.ts +25 -0
- package/dist/utils/worktree-tool-context.d.ts.map +1 -0
- package/dist/utils/worktree-tool-context.js +116 -0
- package/dist/utils/worktree-tool-context.js.map +1 -0
- 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 +96 -10
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +5 -5
- package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +3 -3
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +2 -2
- package/prompts/prometheus-strict-metis.md +1 -1
- package/skills/ai-slop-cleaner/SKILL.md +1 -1
- package/skills/code-review/SKILL.md +1 -1
- package/skills/ecomode/references/agent-tiers.md +1 -1
- package/skills/omx-setup/SKILL.md +5 -5
- package/skills/prometheus-strict/SKILL.md +3 -3
- package/skills/ralplan/SKILL.md +1 -1
- package/skills/team/SKILL.md +1 -1
- package/skills/ultrawork/references/agent-tiers.md +2 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +1818 -113
- package/src/scripts/codex-native-hook.ts +414 -27
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
isTrustedSubagentThread,
|
|
20
20
|
readSubagentSessionSummary,
|
|
21
|
+
readSubagentSessionLedger,
|
|
21
22
|
readSubagentTrackingState,
|
|
22
23
|
recordSubagentTurnForSession,
|
|
23
24
|
} from "../subagents/tracker.js";
|
|
@@ -94,9 +95,14 @@ import {
|
|
|
94
95
|
CONDUCTOR_ORCHESTRATION_METADATA_PREFIXES,
|
|
95
96
|
LEADER_CONDUCTOR_BLOCK,
|
|
96
97
|
LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE,
|
|
98
|
+
NATIVE_SUBAGENT_SUPPORT_BLOCKER_FILE,
|
|
97
99
|
actionKindForConductorArtifact,
|
|
98
100
|
authorizeConductorAction,
|
|
101
|
+
buildUnsupportedNativeSubagentGuidance,
|
|
99
102
|
classifyConductorArtifactKind,
|
|
103
|
+
isUnsupportedNativeSubagentEvidence,
|
|
104
|
+
resolveNativeSubagentSupportStatus,
|
|
105
|
+
type NativeSubagentUnsupportedReason,
|
|
100
106
|
} from "../leader/contract.js";
|
|
101
107
|
import { readRunState } from "../runtime/run-state.js";
|
|
102
108
|
import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
|
|
@@ -124,7 +130,6 @@ import {
|
|
|
124
130
|
} from "../question/deep-interview.js";
|
|
125
131
|
import { readAutopilotDeepInterviewQuestionWaitState } from "../question/autopilot-wait.js";
|
|
126
132
|
import {
|
|
127
|
-
buildDocumentRefreshAdvisoryOutput,
|
|
128
133
|
evaluateFinalHandoffDocumentRefresh,
|
|
129
134
|
isFinalHandoffDocumentRefreshCandidate,
|
|
130
135
|
} from "../document-refresh/enforcer.js";
|
|
@@ -1792,6 +1797,101 @@ async function ensureOmxLocalIgnoreEntry(cwd: string): Promise<{ changed: boolea
|
|
|
1792
1797
|
return { changed: true, excludePath };
|
|
1793
1798
|
}
|
|
1794
1799
|
|
|
1800
|
+
function readSessionStartSource(payload: CodexHookPayload | undefined): string {
|
|
1801
|
+
return safeString(payload?.source ?? payload?.session_start_source ?? payload?.sessionStartSource).trim().toLowerCase();
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function shouldBuildSubagentReopenContext(options: {
|
|
1805
|
+
hookEventName?: CodexHookEventName | null;
|
|
1806
|
+
payload?: CodexHookPayload;
|
|
1807
|
+
}): boolean {
|
|
1808
|
+
if (options.hookEventName !== "SessionStart") return false;
|
|
1809
|
+
const source = readSessionStartSource(options.payload);
|
|
1810
|
+
return source === "startup" || source === "resume";
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function formatSubagentLedgerMetadata(entry: {
|
|
1814
|
+
role?: string;
|
|
1815
|
+
laneId?: string;
|
|
1816
|
+
scope?: string;
|
|
1817
|
+
status?: string;
|
|
1818
|
+
lastHandoffSummary?: string;
|
|
1819
|
+
resumeFailureReason?: string;
|
|
1820
|
+
}): string {
|
|
1821
|
+
const metadata = [
|
|
1822
|
+
entry.role ? `role: ${entry.role}` : null,
|
|
1823
|
+
entry.laneId ? `lane: ${entry.laneId}` : null,
|
|
1824
|
+
entry.scope ? `scope: ${entry.scope}` : null,
|
|
1825
|
+
entry.status ? `status: ${entry.status}` : null,
|
|
1826
|
+
entry.lastHandoffSummary ? `handoff: ${entry.lastHandoffSummary.slice(0, 120)}` : null,
|
|
1827
|
+
entry.resumeFailureReason ? `last failure: ${entry.resumeFailureReason.slice(0, 120)}` : null,
|
|
1828
|
+
].filter((item): item is string => Boolean(item));
|
|
1829
|
+
return metadata.length > 0 ? ` (${metadata.join("; ")})` : "";
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
async function buildPersistedSubagentReopenContext(
|
|
1833
|
+
cwd: string,
|
|
1834
|
+
sessionId: string,
|
|
1835
|
+
options: {
|
|
1836
|
+
hookEventName?: CodexHookEventName | null;
|
|
1837
|
+
payload?: CodexHookPayload;
|
|
1838
|
+
},
|
|
1839
|
+
): Promise<string | null> {
|
|
1840
|
+
if (!shouldBuildSubagentReopenContext(options)) return null;
|
|
1841
|
+
|
|
1842
|
+
const ledger = await readSubagentSessionLedger(cwd, sessionId).catch(() => null);
|
|
1843
|
+
if (!ledger || ledger.savedSubagents.length === 0) return null;
|
|
1844
|
+
|
|
1845
|
+
const source = readSessionStartSource(options.payload);
|
|
1846
|
+
const reopenTargets = ledger.resumeTargets.filter((entry) => entry.status !== "unavailable");
|
|
1847
|
+
const unavailableTargets = ledger.unavailableSubagents;
|
|
1848
|
+
const failedTargets = ledger.savedSubagents.filter((entry) => entry.resumeFailedAt || entry.resumeFailureReason);
|
|
1849
|
+
const nowIso = new Date().toISOString();
|
|
1850
|
+
|
|
1851
|
+
await Promise.all(reopenTargets.map((entry) => recordSubagentTurnForSession(cwd, {
|
|
1852
|
+
sessionId,
|
|
1853
|
+
threadId: entry.threadId,
|
|
1854
|
+
kind: "subagent",
|
|
1855
|
+
role: entry.role,
|
|
1856
|
+
laneId: entry.laneId,
|
|
1857
|
+
scope: entry.scope,
|
|
1858
|
+
agentNickname: entry.agentNickname,
|
|
1859
|
+
status: entry.status,
|
|
1860
|
+
resumeRequestedAt: nowIso,
|
|
1861
|
+
preserveCompletionEvidence: true,
|
|
1862
|
+
}).catch(() => null)));
|
|
1863
|
+
|
|
1864
|
+
const lines = [
|
|
1865
|
+
"[Persisted subagent reopen]",
|
|
1866
|
+
`- SessionStart source: ${source}; saved subagent ids found: ${ledger.savedSubagents.length}.`,
|
|
1867
|
+
];
|
|
1868
|
+
|
|
1869
|
+
if (reopenTargets.length > 0) {
|
|
1870
|
+
lines.push("- Reopen these persisted subagents by id before continuing work or spawning any same-role/same-lane replacement:");
|
|
1871
|
+
for (const entry of reopenTargets.slice(0, 12)) {
|
|
1872
|
+
lines.push(` - resume_agent(${JSON.stringify(entry.agentId)})${formatSubagentLedgerMetadata(entry)}`);
|
|
1873
|
+
}
|
|
1874
|
+
if (reopenTargets.length > 12) {
|
|
1875
|
+
lines.push(` - ... ${reopenTargets.length - 12} more saved subagent id(s) omitted from this compact SessionStart context; consult .omx/state/subagent-tracking.json before spawning replacements.`);
|
|
1876
|
+
}
|
|
1877
|
+
} else {
|
|
1878
|
+
lines.push("- No compatible saved subagent id is currently marked reopenable; do not spawn a replacement merely because reopen was unavailable.");
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
lines.push("- Silver rule: when follow-up work targets an existing role/lane, reuse the matching reopened id; avoid duplicate same-type subagent spawns.");
|
|
1882
|
+
lines.push("- If resume_agent fails, surface a clear warning with the id and reason, then continue in the root or another compatible existing lane; do not spawn a new agent solely because reopen failed.");
|
|
1883
|
+
|
|
1884
|
+
const warningEntries = [...new Map([...unavailableTargets, ...failedTargets].map((entry) => [entry.agentId, entry])).values()];
|
|
1885
|
+
if (warningEntries.length > 0) {
|
|
1886
|
+
lines.push("- Reopen warnings:");
|
|
1887
|
+
for (const entry of warningEntries.slice(0, 8)) {
|
|
1888
|
+
lines.push(` - ${entry.agentId}${formatSubagentLedgerMetadata(entry)}`);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
return lines.join("\n");
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1795
1895
|
async function buildSessionStartContext(
|
|
1796
1896
|
cwd: string,
|
|
1797
1897
|
sessionId: string,
|
|
@@ -1901,6 +2001,14 @@ async function buildSessionStartContext(
|
|
|
1901
2001
|
sections.push(`[Subagents]\n- active subagent threads: ${subagentSummary.activeSubagentThreadIds.length}`);
|
|
1902
2002
|
}
|
|
1903
2003
|
|
|
2004
|
+
const persistedSubagentReopenContext = await buildPersistedSubagentReopenContext(cwd, sessionId, {
|
|
2005
|
+
hookEventName: options.hookEventName,
|
|
2006
|
+
payload: options.payload,
|
|
2007
|
+
});
|
|
2008
|
+
if (persistedSubagentReopenContext) {
|
|
2009
|
+
sections.push(persistedSubagentReopenContext);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1904
2012
|
return sections.length > 0 ? sections.join("\n\n") : null;
|
|
1905
2013
|
}
|
|
1906
2014
|
|
|
@@ -2111,12 +2219,23 @@ function buildSkillStateCliInstruction(mode: string, statePath: string): string
|
|
|
2111
2219
|
|
|
2112
2220
|
function buildAutopilotPromptActivationNote(
|
|
2113
2221
|
skillState?: SkillActiveState | null,
|
|
2114
|
-
options: { markedQuestionAnswer?: boolean; cwd?: string } = {},
|
|
2222
|
+
options: { markedQuestionAnswer?: boolean; cwd?: string; payload?: CodexHookPayload; sessionId?: string } = {},
|
|
2115
2223
|
): string | null {
|
|
2116
2224
|
if (skillState?.initialized_mode !== "autopilot") return null;
|
|
2117
2225
|
const teamHandoff = readTeamModeConfig(options.cwd).enabled
|
|
2118
2226
|
? " (+ $team if needed)"
|
|
2119
2227
|
: "";
|
|
2228
|
+
const stateDir = getBaseStateDir(options.cwd);
|
|
2229
|
+
const nativeSubagentSupport = resolveNativeSubagentSupportStatus({
|
|
2230
|
+
payload: options.payload,
|
|
2231
|
+
persistedSupportBlocker: readJsonSyncIfExists(nativeSubagentSupportBlockerPath(stateDir)),
|
|
2232
|
+
persistedCapacityBlocker: readJsonSyncIfExists(nativeSubagentCapacityBlockerPath(stateDir)),
|
|
2233
|
+
cwd: options.cwd,
|
|
2234
|
+
sessionId: options.sessionId,
|
|
2235
|
+
});
|
|
2236
|
+
const conductorGuidance = nativeSubagentSupport.status === "unsupported"
|
|
2237
|
+
? buildUnsupportedNativeSubagentGuidance(nativeSubagentSupport)
|
|
2238
|
+
: `${LEADER_CONDUCTOR_BLOCK} ${LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE}`;
|
|
2120
2239
|
return [
|
|
2121
2240
|
`Autopilot protocol: the durable default chain is $deep-interview -> $ralplan -> $ultragoal${teamHandoff} -> $code-review -> $ultraqa (deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa).`,
|
|
2122
2241
|
"Start/resume at current_phase=deep-interview unless the task is clear and bounded; if deep-interview is intentionally skipped, persist and state an explicit deep_interview_gate.skip_reason before moving to ralplan.",
|
|
@@ -2128,8 +2247,7 @@ function buildAutopilotPromptActivationNote(
|
|
|
2128
2247
|
"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.",
|
|
2129
2248
|
"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.",
|
|
2130
2249
|
"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.",
|
|
2131
|
-
|
|
2132
|
-
LEADER_CONDUCTOR_REUSE_AND_LEDGER_GUIDANCE,
|
|
2250
|
+
conductorGuidance,
|
|
2133
2251
|
].filter(Boolean).join(" ");
|
|
2134
2252
|
}
|
|
2135
2253
|
|
|
@@ -2161,7 +2279,7 @@ function buildAdditionalContextMessage(
|
|
|
2161
2279
|
: null;
|
|
2162
2280
|
const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
|
|
2163
2281
|
const markedQuestionAnswer = /^\s*\[omx question answered\]/i.test(prompt);
|
|
2164
|
-
const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer, cwd });
|
|
2282
|
+
const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer, cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
|
|
2165
2283
|
return [
|
|
2166
2284
|
`OMX native UserPromptSubmit continued active workflow skill "${continuedSkill}".`,
|
|
2167
2285
|
promptPriorityMessage,
|
|
@@ -2187,7 +2305,7 @@ function buildAdditionalContextMessage(
|
|
|
2187
2305
|
? buildDeepInterviewQuestionBridgeInstruction(cwd, payload)
|
|
2188
2306
|
: null;
|
|
2189
2307
|
const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
|
|
2190
|
-
const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true, cwd });
|
|
2308
|
+
const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true, cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
|
|
2191
2309
|
return [
|
|
2192
2310
|
`OMX native UserPromptSubmit continued active workflow skill "${continuedSkill}"; workflow-like tokens inside the marked omx question answer are treated as answer text, not a new workflow activation.`,
|
|
2193
2311
|
promptPriorityMessage,
|
|
@@ -2220,7 +2338,7 @@ function buildAdditionalContextMessage(
|
|
|
2220
2338
|
const ultragoalPromptActivationNote = match.skill === "ultragoal"
|
|
2221
2339
|
? "Ultragoal protocol: use `omx ultragoal create-goals` / `complete-goals` / `checkpoint` for `.omx/ultragoal` artifacts, then use Codex goal model tools only from the active agent handoff (`get_goal`, `create_goal`, `update_goal`) and never overwrite a different active Codex goal. Ultragoal does not call `/goal clear`; for multiple sequential ultragoal runs in one Codex session/thread, manually clear the completed Codex goal in the UI before creating the next aggregate goal."
|
|
2222
2340
|
: null;
|
|
2223
|
-
const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { cwd });
|
|
2341
|
+
const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { cwd, payload, sessionId: safeString(skillState?.session_id).trim() });
|
|
2224
2342
|
const combinedTransitionMessage = (() => {
|
|
2225
2343
|
if (!skillState?.transition_message) return null;
|
|
2226
2344
|
if (matches.length <= 1 || activeSkills.length <= 1) return skillState.transition_message;
|
|
@@ -2621,8 +2739,6 @@ async function buildModeBasedStopOutput(
|
|
|
2621
2739
|
reason: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}); continue the task and gather fresh verification evidence before stopping.`,
|
|
2622
2740
|
stopReason: `${mode}_${phase}`,
|
|
2623
2741
|
systemMessage,
|
|
2624
|
-
statePath,
|
|
2625
|
-
canonicalDisagreement,
|
|
2626
2742
|
};
|
|
2627
2743
|
}
|
|
2628
2744
|
|
|
@@ -3094,6 +3210,18 @@ function nativeSubagentCapacityBlockerPath(stateDir: string): string {
|
|
|
3094
3210
|
return join(stateDir, NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE);
|
|
3095
3211
|
}
|
|
3096
3212
|
|
|
3213
|
+
function nativeSubagentSupportBlockerPath(stateDir: string): string {
|
|
3214
|
+
return join(stateDir, NATIVE_SUBAGENT_SUPPORT_BLOCKER_FILE);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function readJsonSyncIfExists(path: string): Record<string, unknown> | null {
|
|
3218
|
+
try {
|
|
3219
|
+
return JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
|
|
3220
|
+
} catch {
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3097
3225
|
function stringifyUnknown(value: unknown): string {
|
|
3098
3226
|
if (typeof value === "string") return value;
|
|
3099
3227
|
if (value === undefined || value === null) return "";
|
|
@@ -3121,6 +3249,45 @@ function isNativeSubagentCapacityFailure(payload: CodexHookPayload): boolean {
|
|
|
3121
3249
|
return !toolName || /(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName);
|
|
3122
3250
|
}
|
|
3123
3251
|
|
|
3252
|
+
function nativeSubagentFailureReason(payload: CodexHookPayload): NativeSubagentUnsupportedReason | null {
|
|
3253
|
+
const evidence = payloadEvidenceText(payload);
|
|
3254
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
3255
|
+
if (toolName && !/(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName)) return null;
|
|
3256
|
+
if (/\bagent thread limit reached\b/i.test(evidence)) return null;
|
|
3257
|
+
if (/\bnative subagents? (?:unsupported|disabled|not enabled|unavailable|not found)\b/i.test(evidence)) return "native_subagents_unsupported";
|
|
3258
|
+
if (/\bmulti_agent_v1\b/i.test(evidence) && /\b(?:unavailable|unknown tool|disabled|not enabled|not found|unsupported)\b/i.test(evidence)) return "multi_agent_v1_unavailable";
|
|
3259
|
+
if (/\b(?:unknown tool|tool not found|not enabled|disabled|unavailable|unsupported)\b/i.test(evidence)) return "multi_agent_v1_unavailable";
|
|
3260
|
+
return null;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
function summarizeNativeSubagentSupportFailure(text: string): string {
|
|
3264
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
3265
|
+
return (normalized || "native subagent support unavailable").slice(0, 500);
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
async function recordNativeSubagentSupportBlocker(
|
|
3269
|
+
cwd: string,
|
|
3270
|
+
stateDir: string,
|
|
3271
|
+
payload: CodexHookPayload,
|
|
3272
|
+
): Promise<void> {
|
|
3273
|
+
const reason = nativeSubagentFailureReason(payload);
|
|
3274
|
+
if (!reason) return;
|
|
3275
|
+
const nowIso = new Date().toISOString();
|
|
3276
|
+
await mkdir(stateDir, { recursive: true });
|
|
3277
|
+
await writeFile(nativeSubagentSupportBlockerPath(stateDir), JSON.stringify({
|
|
3278
|
+
schema_version: 1,
|
|
3279
|
+
status: "unsupported",
|
|
3280
|
+
reason,
|
|
3281
|
+
...(readPayloadSessionId(payload) ? { session_id: readPayloadSessionId(payload) } : {}),
|
|
3282
|
+
...(readPayloadThreadId(payload) ? { thread_id: readPayloadThreadId(payload) } : {}),
|
|
3283
|
+
...(readPayloadTurnId(payload) ? { turn_id: readPayloadTurnId(payload) } : {}),
|
|
3284
|
+
...(safeString(payload.tool_name).trim() ? { tool_name: safeString(payload.tool_name).trim() } : {}),
|
|
3285
|
+
evidence: summarizeNativeSubagentSupportFailure(payloadEvidenceText(payload)),
|
|
3286
|
+
observed_at: nowIso,
|
|
3287
|
+
cwd,
|
|
3288
|
+
}, null, 2));
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3124
3291
|
function summarizeCapacityFailure(text: string): string {
|
|
3125
3292
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
3126
3293
|
if (!normalized) return "agent thread limit reached";
|
|
@@ -3329,6 +3496,8 @@ const PROTECTED_PLANNING_STATE_FILE_NAMES = new Set([
|
|
|
3329
3496
|
"ralplan-state.json",
|
|
3330
3497
|
"skill-active-state.json",
|
|
3331
3498
|
]);
|
|
3499
|
+
const RUNTIME_SESSION_ID_SEGMENT_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
3500
|
+
|
|
3332
3501
|
|
|
3333
3502
|
const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
|
|
3334
3503
|
"Write",
|
|
@@ -3419,6 +3588,33 @@ function isProtectedPlanningStatePath(relativePath: string): boolean {
|
|
|
3419
3588
|
const fileName = relativePath.split("/").pop() ?? "";
|
|
3420
3589
|
return PROTECTED_PLANNING_STATE_FILE_NAMES.has(fileName);
|
|
3421
3590
|
}
|
|
3591
|
+
function isAllowedAuthoritativeRuntimePlanningStatePath(cwd: string, rawPath: string): boolean {
|
|
3592
|
+
const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
|
|
3593
|
+
if (!trimmed || trimmed.includes("\0")) return false;
|
|
3594
|
+
|
|
3595
|
+
let targetPath: string;
|
|
3596
|
+
let baseStateDir: string;
|
|
3597
|
+
try {
|
|
3598
|
+
targetPath = resolve(cwd, trimmed);
|
|
3599
|
+
baseStateDir = resolve(getBaseStateDir(cwd));
|
|
3600
|
+
} catch {
|
|
3601
|
+
return false;
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
const localStateDir = resolve(cwd, ".omx", "state");
|
|
3605
|
+
if (baseStateDir === localStateDir) return false;
|
|
3606
|
+
|
|
3607
|
+
const relativeToBase = relative(baseStateDir, targetPath).replace(/\\/g, "/");
|
|
3608
|
+
if (!relativeToBase || relativeToBase.startsWith("..") || relativeToBase.startsWith("/")) return false;
|
|
3609
|
+
|
|
3610
|
+
const segments = relativeToBase.split("/");
|
|
3611
|
+
if (segments.length !== 3 || segments[0] !== "sessions") return false;
|
|
3612
|
+
const sessionId = segments[1] ?? "";
|
|
3613
|
+
const fileName = segments[2] ?? "";
|
|
3614
|
+
return RUNTIME_SESSION_ID_SEGMENT_PATTERN.test(sessionId)
|
|
3615
|
+
&& PROTECTED_PLANNING_STATE_FILE_NAMES.has(fileName);
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3422
3618
|
|
|
3423
3619
|
function isPlanningTmpRelativePath(relativePath: string): boolean {
|
|
3424
3620
|
return relativePath === ".omx/tmp" || relativePath.startsWith(".omx/tmp/");
|
|
@@ -3439,8 +3635,8 @@ function isAllowedPlanningArtifactPath(
|
|
|
3439
3635
|
allowedPrefixes: readonly string[],
|
|
3440
3636
|
): boolean {
|
|
3441
3637
|
const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
|
|
3442
|
-
if (!relativePath) return
|
|
3443
|
-
if (isProtectedPlanningStatePath(relativePath)) return
|
|
3638
|
+
if (!relativePath) return isAllowedAuthoritativeRuntimePlanningStatePath(cwd, rawPath);
|
|
3639
|
+
if (isProtectedPlanningStatePath(relativePath)) return isAllowedAuthoritativeRuntimePlanningStatePath(cwd, rawPath);
|
|
3444
3640
|
if (isPlanningTmpRelativePath(relativePath)) {
|
|
3445
3641
|
return allowedPrefixes.includes(".omx/tmp") && isAllowedPlanningTmpScratchPath(relativePath);
|
|
3446
3642
|
}
|
|
@@ -3454,8 +3650,14 @@ function isAllowedDeepInterviewArtifactPath(cwd: string, rawPath: string): boole
|
|
|
3454
3650
|
return isAllowedPlanningArtifactPath(cwd, rawPath, DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES);
|
|
3455
3651
|
}
|
|
3456
3652
|
|
|
3653
|
+
function isAllowedRalplanDraftPath(cwd: string, rawPath: string): boolean {
|
|
3654
|
+
const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
|
|
3655
|
+
return relativePath !== null && /^\.omx\/drafts\/[^/]+\.md$/.test(relativePath);
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3457
3658
|
function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
|
|
3458
|
-
return
|
|
3659
|
+
return isAllowedRalplanDraftPath(cwd, rawPath)
|
|
3660
|
+
|| isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
3459
3661
|
}
|
|
3460
3662
|
|
|
3461
3663
|
interface RalplanBeadsCommandClassification {
|
|
@@ -3675,9 +3877,72 @@ function resolveCommandRedirectTarget(target: string, assignments: Map<string, s
|
|
|
3675
3877
|
return resolved !== undefined ? resolved : target;
|
|
3676
3878
|
}
|
|
3677
3879
|
|
|
3880
|
+
// Masks redirect metacharacters (`<`/`>`) that appear INSIDE shell quotes so a
|
|
3881
|
+
// quoted regex/source value (e.g. `gh issue create --body '...>{1,2}...'` or
|
|
3882
|
+
// `omx state write --input '{"reason":"a>b"}'`) is not misread as a redirect
|
|
3883
|
+
// write target. Escaped quotes at the top level (`\'`, `\"`) are literal
|
|
3884
|
+
// characters and must NOT open a span, and `$'...'` ANSI-C quoting processes
|
|
3885
|
+
// backslash escapes (so `\'` does not close it) — otherwise a genuine `>`
|
|
3886
|
+
// redirect could be masked behind a false span and its write missed. Only
|
|
3887
|
+
// characters inside a real quoted span are masked; unquoted redirect operators
|
|
3888
|
+
// survive intact. Unterminated/ambiguous quoting fails closed: the original
|
|
3889
|
+
// command is returned unmasked so genuine redirects stay visible to the scan.
|
|
3890
|
+
function maskQuotedRedirectMetacharsForCommandScan(command: string): string {
|
|
3891
|
+
let masked = "";
|
|
3892
|
+
// null = unquoted; "'" = single quotes (no escapes); '"' = double quotes
|
|
3893
|
+
// (backslash escapes); "$'" = ANSI-C $'...' (backslash escapes, incl. \').
|
|
3894
|
+
let quote: "'" | "\"" | "$'" | null = null;
|
|
3895
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
3896
|
+
const char = command[index] ?? "";
|
|
3897
|
+
if (quote === null) {
|
|
3898
|
+
// A backslash escapes the next character at the top level, so an escaped
|
|
3899
|
+
// quote is a literal char and must not open a quoted span.
|
|
3900
|
+
if (char === "\\") {
|
|
3901
|
+
masked += char;
|
|
3902
|
+
const next = command[index + 1];
|
|
3903
|
+
if (next !== undefined) {
|
|
3904
|
+
masked += next;
|
|
3905
|
+
index += 1;
|
|
3906
|
+
}
|
|
3907
|
+
continue;
|
|
3908
|
+
}
|
|
3909
|
+
if (char === "$" && command[index + 1] === "'") {
|
|
3910
|
+
quote = "$'";
|
|
3911
|
+
masked += "$'";
|
|
3912
|
+
index += 1;
|
|
3913
|
+
continue;
|
|
3914
|
+
}
|
|
3915
|
+
if (char === "'" || char === "\"") quote = char;
|
|
3916
|
+
masked += char;
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3919
|
+
if ((quote === "\"" || quote === "$'") && char === "\\") {
|
|
3920
|
+
// Escapes inside double/ANSI-C spans keep the backslash and its escaped
|
|
3921
|
+
// char (masking a redirect metachar so quoted data cannot be a false
|
|
3922
|
+
// target); the escaped char never closes the span.
|
|
3923
|
+
masked += char;
|
|
3924
|
+
const next = command[index + 1];
|
|
3925
|
+
if (next !== undefined) {
|
|
3926
|
+
masked += next === "<" || next === ">" ? "_" : next;
|
|
3927
|
+
index += 1;
|
|
3928
|
+
}
|
|
3929
|
+
continue;
|
|
3930
|
+
}
|
|
3931
|
+
const closesSpan = quote === "$'" ? char === "'" : char === quote;
|
|
3932
|
+
if (closesSpan) {
|
|
3933
|
+
quote = null;
|
|
3934
|
+
masked += char;
|
|
3935
|
+
continue;
|
|
3936
|
+
}
|
|
3937
|
+
masked += char === "<" || char === ">" ? "_" : char;
|
|
3938
|
+
}
|
|
3939
|
+
if (quote !== null) return command;
|
|
3940
|
+
return masked;
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3678
3943
|
function extractDeepInterviewCommandRedirectTargets(command: string): string[] {
|
|
3679
3944
|
const targets: string[] = [];
|
|
3680
|
-
const commandOutsideHeredocBodies = stripHeredocBodiesForCommandScan(command);
|
|
3945
|
+
const commandOutsideHeredocBodies = maskQuotedRedirectMetacharsForCommandScan(stripHeredocBodiesForCommandScan(command));
|
|
3681
3946
|
for (const match of commandOutsideHeredocBodies.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
|
|
3682
3947
|
const candidate = safeString(match[2]).trim();
|
|
3683
3948
|
if (candidate && !isNullDeviceRedirectTarget(candidate)) targets.push(candidate);
|
|
@@ -3775,7 +4040,7 @@ function findGitSubcommandIndex(words: string[], startIndex: number): number | n
|
|
|
3775
4040
|
}
|
|
3776
4041
|
|
|
3777
4042
|
|
|
3778
|
-
function commandHasDeepInterviewWriteIntent(command: string): boolean {
|
|
4043
|
+
function commandHasDeepInterviewWriteIntent(command: string, depth = 0): boolean {
|
|
3779
4044
|
return commandInvokesApplyPatch(command)
|
|
3780
4045
|
|| extractDeepInterviewCommandRedirectTargets(command).length > 0
|
|
3781
4046
|
|| /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
|
|
@@ -3786,7 +4051,19 @@ function commandHasDeepInterviewWriteIntent(command: string): boolean {
|
|
|
3786
4051
|
|| extractConductorBashMutations(command).length > 0
|
|
3787
4052
|
|| extractConductorInterpreterWrites(command).length > 0
|
|
3788
4053
|
|| commandHasDestructiveGitSubcommand(command)
|
|
3789
|
-
|| commandHasPackageInstallIntent(command)
|
|
4054
|
+
|| commandHasPackageInstallIntent(command)
|
|
4055
|
+
// Recurse into wrapped shells (`bash -lc "cat > f"`, `eval`, `env`) and
|
|
4056
|
+
// command substitutions so a real redirect INSIDE a nested command string
|
|
4057
|
+
// is still classified as write intent. This is required because quoted
|
|
4058
|
+
// redirect metacharacters are now masked at the outer scan (#3119): the
|
|
4059
|
+
// mask removes false positives from quoted DATA values while nested-command
|
|
4060
|
+
// recursion re-detects genuine redirects, preserving fail-closed blocking.
|
|
4061
|
+
// The depth guard mirrors evaluateConductorBashWrite's nesting bound;
|
|
4062
|
+
// extractors return strict substrings, so recursion always terminates.
|
|
4063
|
+
|| (depth < CONDUCTOR_BASH_MAX_NESTING_DEPTH && (
|
|
4064
|
+
extractNestedShellCommandStringsForStateScan(command).some((nested) => commandHasDeepInterviewWriteIntent(nested, depth + 1))
|
|
4065
|
+
|| extractNestedCommandSubstitutionStringsForStateScan(command).some((nested) => commandHasDeepInterviewWriteIntent(nested, depth + 1))
|
|
4066
|
+
));
|
|
3790
4067
|
}
|
|
3791
4068
|
|
|
3792
4069
|
function extractDeepInterviewCommandWriteTargets(command: string): string[] {
|
|
@@ -5829,6 +6106,55 @@ function isPackageManagerCommandWord(word: string): boolean {
|
|
|
5829
6106
|
return base === "npm" || base === "pnpm" || base === "yarn" || base === "npx" || base === "pnpx";
|
|
5830
6107
|
}
|
|
5831
6108
|
|
|
6109
|
+
function isStructuredUltragoalSteeringShellCommand(command: string): boolean {
|
|
6110
|
+
const segments = splitShellCommandSegments(stripHeredocBodiesForCommandScan(command));
|
|
6111
|
+
let sawStructuredSteer = false;
|
|
6112
|
+
|
|
6113
|
+
for (const segment of segments) {
|
|
6114
|
+
const words = tokenizeShellWords(segment);
|
|
6115
|
+
let commandStart = true;
|
|
6116
|
+
let inForHeader = false;
|
|
6117
|
+
|
|
6118
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
6119
|
+
const word = words[index] ?? "";
|
|
6120
|
+
if (!word) continue;
|
|
6121
|
+
if (isShellCommandSeparator(word)) {
|
|
6122
|
+
commandStart = true;
|
|
6123
|
+
continue;
|
|
6124
|
+
}
|
|
6125
|
+
if (inForHeader) {
|
|
6126
|
+
if (word === "do") {
|
|
6127
|
+
inForHeader = false;
|
|
6128
|
+
commandStart = true;
|
|
6129
|
+
}
|
|
6130
|
+
continue;
|
|
6131
|
+
}
|
|
6132
|
+
if (isShellGroupingSyntaxWord(word)) continue;
|
|
6133
|
+
if (commandStart && isEnvironmentAssignmentWord(word)) continue;
|
|
6134
|
+
if (commandStart && CONDUCTOR_BASH_COMPOUND_SYNTAX_WORDS.has(word)) {
|
|
6135
|
+
if (word === "for") inForHeader = true;
|
|
6136
|
+
continue;
|
|
6137
|
+
}
|
|
6138
|
+
if (commandStart && word.startsWith("-")) continue;
|
|
6139
|
+
if (!commandStart) continue;
|
|
6140
|
+
|
|
6141
|
+
const commandName = commandNameFromShellWord(word);
|
|
6142
|
+
if (commandName === "read" || commandName === "true" || commandName === ":") {
|
|
6143
|
+
commandStart = false;
|
|
6144
|
+
continue;
|
|
6145
|
+
}
|
|
6146
|
+
if (commandName === "omx" && words[index + 1] === "ultragoal" && words[index + 2] === "steer") {
|
|
6147
|
+
sawStructuredSteer = true;
|
|
6148
|
+
commandStart = false;
|
|
6149
|
+
continue;
|
|
6150
|
+
}
|
|
6151
|
+
return false;
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
return sawStructuredSteer;
|
|
6156
|
+
}
|
|
6157
|
+
|
|
5832
6158
|
function hasDynamicNestedShellExecution(command: string): boolean {
|
|
5833
6159
|
const commands = splitShellCommandSegments(stripHeredocBodiesForCommandScan(command));
|
|
5834
6160
|
for (const segment of commands) {
|
|
@@ -5851,12 +6177,12 @@ function hasDynamicNestedShellExecution(command: string): boolean {
|
|
|
5851
6177
|
if (commandStringIndex === null && (hasUnquotedShellStdinFlowAroundShellWord(words, index) || segment.includes("<<"))) return true;
|
|
5852
6178
|
if (commandStringIndex !== null) {
|
|
5853
6179
|
const nestedCommand = words[commandStringIndex] ?? "";
|
|
5854
|
-
if (isDynamicNestedCommandString(nestedCommand)) return true;
|
|
6180
|
+
if (isDynamicNestedCommandString(nestedCommand) && !isStructuredUltragoalSteeringShellCommand(nestedCommand)) return true;
|
|
5855
6181
|
}
|
|
5856
6182
|
}
|
|
5857
6183
|
if (word === "eval") {
|
|
5858
6184
|
const nestedCommand = words.slice(index + 1).join(" ");
|
|
5859
|
-
if (isDynamicNestedCommandString(nestedCommand)) return true;
|
|
6185
|
+
if (isDynamicNestedCommandString(nestedCommand) && !isStructuredUltragoalSteeringShellCommand(nestedCommand)) return true;
|
|
5860
6186
|
}
|
|
5861
6187
|
}
|
|
5862
6188
|
}
|
|
@@ -6723,7 +7049,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
6723
7049
|
resolvedSessionId?: string,
|
|
6724
7050
|
): Promise<Record<string, unknown> | null> {
|
|
6725
7051
|
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
6726
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
|
|
7052
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId)) return null;
|
|
6727
7053
|
const threadId = readPayloadThreadId(payload);
|
|
6728
7054
|
const activeState = await readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId);
|
|
6729
7055
|
if (!activeState) return null;
|
|
@@ -6777,7 +7103,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
6777
7103
|
hookEventName: "PreToolUse",
|
|
6778
7104
|
additionalContext:
|
|
6779
7105
|
`${planningModeDescription}. `
|
|
6780
|
-
+ "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, `.omx/tmp/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
|
|
7106
|
+
+ "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, `.omx/tmp/`, required `.omx/state/` files, Markdown drafts under `.omx/drafts/*.md`, or tracker metadata under `.beads/`. "
|
|
6781
7107
|
+ "Do not edit implementation files or run implementation-focused writes from planning phases. "
|
|
6782
7108
|
+ `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
|
|
6783
7109
|
},
|
|
@@ -6791,7 +7117,7 @@ async function buildDeepInterviewPreToolUseBoundaryOutput(
|
|
|
6791
7117
|
resolvedSessionId?: string,
|
|
6792
7118
|
): Promise<Record<string, unknown> | null> {
|
|
6793
7119
|
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
6794
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
|
|
7120
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId)) return null;
|
|
6795
7121
|
const threadId = readPayloadThreadId(payload);
|
|
6796
7122
|
const activeState = await readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId);
|
|
6797
7123
|
if (!activeState) return null;
|
|
@@ -6962,17 +7288,67 @@ interface ActiveConductorState {
|
|
|
6962
7288
|
async function hasTrustedTypedSubagentProvenanceForPreToolUse(
|
|
6963
7289
|
payload: CodexHookPayload,
|
|
6964
7290
|
cwd: string,
|
|
7291
|
+
stateDir: string,
|
|
6965
7292
|
sessionId: string,
|
|
7293
|
+
options: { allowUntypedProvenance?: boolean } = {},
|
|
6966
7294
|
): Promise<boolean> {
|
|
6967
7295
|
if (hasTeamWorkerEnvironment()) return true;
|
|
6968
|
-
if (!isTypedAgentRolePayload(payload)) return false;
|
|
6969
7296
|
const trackingState = await readSubagentTrackingState(cwd).catch(() => null);
|
|
6970
7297
|
const session = trackingState?.sessions?.[sessionId];
|
|
6971
7298
|
if (!session) return false;
|
|
6972
7299
|
|
|
6973
7300
|
const payloadThreadId = readPayloadThreadId(payload);
|
|
7301
|
+
|
|
7302
|
+
// Resolve the Main-root leader THREAD identity from the tracker's leader_thread_id
|
|
7303
|
+
// plus the canonical session's native_session_id (the leader's native thread). Only
|
|
7304
|
+
// genuine leader-thread identifiers may anchor the leader: owner_*_session_id are
|
|
7305
|
+
// session-level ids, not thread anchors, so they must NOT populate this set — their
|
|
7306
|
+
// mere presence would make it non-empty and suppress the fail-closed guard below
|
|
7307
|
+
// without ever excluding the leader thread (#3117 P4). The native_session_id anchor is
|
|
7308
|
+
// honored only when the root session pointer provably maps to the sessionId under
|
|
7309
|
+
// evaluation, so a stale/foreign session.json cannot supply another session's leader
|
|
7310
|
+
// anchor (#3117 P3); the tracker alone is insufficient because it can omit
|
|
7311
|
+
// leader_thread_id or corruptly label the leader kind:"subagent" (#3117 P2).
|
|
7312
|
+
const sessionState = await readRootSessionStateFromStateDir(stateDir).catch(() => null);
|
|
7313
|
+
const trackerLeaderThreadId = safeString(session.leader_thread_id).trim();
|
|
7314
|
+
const leaderIdentityAnchors = new Set<string>();
|
|
7315
|
+
if (trackerLeaderThreadId) leaderIdentityAnchors.add(trackerLeaderThreadId);
|
|
7316
|
+
if (sessionState && sessionId && payloadMatchesSessionPointer(sessionId, sessionState)) {
|
|
7317
|
+
const nativeSessionId = safeString(sessionState.native_session_id).trim();
|
|
7318
|
+
if (nativeSessionId) leaderIdentityAnchors.add(nativeSessionId);
|
|
7319
|
+
}
|
|
7320
|
+
|
|
7321
|
+
// Leader self-guard: the Main-root Conductor is never a delegated executor. Block it
|
|
7322
|
+
// ahead of every trust path, even when tracker or payload provenance is (possibly
|
|
7323
|
+
// corruptly) attached to the leader thread.
|
|
7324
|
+
if (payloadThreadId && leaderIdentityAnchors.has(payloadThreadId)) return false;
|
|
7325
|
+
|
|
7326
|
+
// Fail closed: without an authoritative leader anchor we cannot affirm the payload is
|
|
7327
|
+
// a non-leader delegated performer rather than a mislabeled leader, so we refuse trust
|
|
7328
|
+
// instead of inferring it from a corrupt tracker kind:"subagent" alone (#3117 P2).
|
|
7329
|
+
if (leaderIdentityAnchors.size === 0) return false;
|
|
7330
|
+
|
|
7331
|
+
// Planning boundary guards (ralplan, deep-interview) still require a recognized typed
|
|
7332
|
+
// agent role, so an untyped collaboration.spawn_agent child cannot write before an
|
|
7333
|
+
// execution handoff/approval. Only the Main-root Conductor/Ralph executing guard opts
|
|
7334
|
+
// into untyped tracker/runtime provenance (#3116, #3117).
|
|
7335
|
+
if (options.allowUntypedProvenance !== true && !isTypedAgentRolePayload(payload)) {
|
|
7336
|
+
return false;
|
|
7337
|
+
}
|
|
7338
|
+
|
|
7339
|
+
// Tracker-backed provenance: the payload's own thread is a recorded, non-leader
|
|
7340
|
+
// subagent for this session — the non-spoofable anchor that lets native
|
|
7341
|
+
// collaboration.spawn_agent children/descendants edit under the Conductor guard even
|
|
7342
|
+
// without a recognized typed role (#3116). subagent-tracking.json is derived from
|
|
7343
|
+
// child SessionStart transcript metadata, not the live tool-call payload.
|
|
6974
7344
|
if (payloadThreadId && isTrustedSubagentThread(session, payloadThreadId)) return true;
|
|
6975
7345
|
|
|
7346
|
+
// Runtime-attached spawn provenance: trust a genuine spawned subagent turn whose
|
|
7347
|
+
// declared parent maps to this session's leader or a tracked thread. parentThreadId
|
|
7348
|
+
// comes from the runtime-set source.subagent.thread_spawn (not agent-controlled tool
|
|
7349
|
+
// arguments); an absent parent is rejected, the leader self-guard above blocks the
|
|
7350
|
+
// main root, and cross-session parents fail because they are not in this session's
|
|
7351
|
+
// tracked threads (#3116).
|
|
6976
7352
|
const source = safeObject(payload.source);
|
|
6977
7353
|
const subagent = safeObject(source.subagent);
|
|
6978
7354
|
const threadSpawn = safeObject(subagent.thread_spawn);
|
|
@@ -6983,9 +7359,7 @@ async function hasTrustedTypedSubagentProvenanceForPreToolUse(
|
|
|
6983
7359
|
?? threadSpawn.leaderThreadId,
|
|
6984
7360
|
).trim();
|
|
6985
7361
|
if (!parentThreadId) return false;
|
|
6986
|
-
|
|
6987
|
-
if (payloadThreadId && leaderThreadId && payloadThreadId === leaderThreadId) return false;
|
|
6988
|
-
return leaderThreadId === parentThreadId || parentThreadId in session.threads;
|
|
7362
|
+
return leaderIdentityAnchors.has(parentThreadId) || parentThreadId in session.threads;
|
|
6989
7363
|
}
|
|
6990
7364
|
|
|
6991
7365
|
function isActiveConductorModeState(state: Record<string, unknown> | null, mode: string, sessionId: string): boolean {
|
|
@@ -7014,7 +7388,7 @@ async function readActiveMainRootConductorStateForPreToolUse(
|
|
|
7014
7388
|
}
|
|
7015
7389
|
const threadId = readPayloadThreadId(payload);
|
|
7016
7390
|
if (!sessionId) return null;
|
|
7017
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
|
|
7391
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId, { allowUntypedProvenance: true })) return null;
|
|
7018
7392
|
|
|
7019
7393
|
const canonicalState = await readVisibleSkillActiveStateForStateDir(stateDir, sessionId);
|
|
7020
7394
|
if (!canonicalState) return null;
|
|
@@ -7865,6 +8239,14 @@ export async function buildConductorPreToolUseWriteGuardOutput(
|
|
|
7865
8239
|
): Promise<Record<string, unknown> | null> {
|
|
7866
8240
|
const activeState = await readActiveMainRootConductorStateForPreToolUse(payload, cwd, stateDir, resolvedSessionId);
|
|
7867
8241
|
if (!activeState) return null;
|
|
8242
|
+
const nativeSubagentSupport = resolveNativeSubagentSupportStatus({
|
|
8243
|
+
payload,
|
|
8244
|
+
persistedSupportBlocker: await readJsonIfExists(nativeSubagentSupportBlockerPath(stateDir)),
|
|
8245
|
+
persistedCapacityBlocker: await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir)),
|
|
8246
|
+
cwd,
|
|
8247
|
+
sessionId: resolvedSessionId || readPayloadSessionId(payload),
|
|
8248
|
+
});
|
|
8249
|
+
|
|
7868
8250
|
|
|
7869
8251
|
const toolName = safeString(payload.tool_name).trim();
|
|
7870
8252
|
const command = readPreToolUseCommand(payload);
|
|
@@ -7891,6 +8273,9 @@ export async function buildConductorPreToolUseWriteGuardOutput(
|
|
|
7891
8273
|
|
|
7892
8274
|
if (!blocked) return null;
|
|
7893
8275
|
|
|
8276
|
+
const unsupportedNativeGuidance = isUnsupportedNativeSubagentEvidence(nativeSubagentSupport)
|
|
8277
|
+
? ` ${buildUnsupportedNativeSubagentGuidance(nativeSubagentSupport)} Treat the active conductor workflow as blocked/cancelled for native delegation recovery; do not call multi_agent_v1.close_agent.`
|
|
8278
|
+
: "";
|
|
7894
8279
|
return {
|
|
7895
8280
|
decision: "block",
|
|
7896
8281
|
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}.`,
|
|
@@ -7900,7 +8285,8 @@ export async function buildConductorPreToolUseWriteGuardOutput(
|
|
|
7900
8285
|
`${LEADER_CONDUCTOR_GOLDEN_RULE} `
|
|
7901
8286
|
+ "Use specialized agents for source edits and plan/spec authorship. "
|
|
7902
8287
|
+ `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. `
|
|
7903
|
-
+
|
|
8288
|
+
+ unsupportedNativeGuidance
|
|
8289
|
+
+ " Autopilot rework and typed subagent/worker lanes are exempt from this guard.",
|
|
7904
8290
|
},
|
|
7905
8291
|
};
|
|
7906
8292
|
}
|
|
@@ -9278,7 +9664,7 @@ async function buildStopHookOutput(
|
|
|
9278
9664
|
documentRefreshWarning.triggeringPaths.join("|"),
|
|
9279
9665
|
canonicalSessionId,
|
|
9280
9666
|
),
|
|
9281
|
-
|
|
9667
|
+
{ systemMessage: documentRefreshWarning.message },
|
|
9282
9668
|
canonicalSessionId,
|
|
9283
9669
|
{ allowRepeatDuringStopHook: false },
|
|
9284
9670
|
);
|
|
@@ -9643,6 +10029,7 @@ export async function dispatchCodexNativeHook(
|
|
|
9643
10029
|
?? buildNativePreToolUseOutput(payload);
|
|
9644
10030
|
} else if (hookEventName === "PostToolUse") {
|
|
9645
10031
|
await recordNativeSubagentCapacityBlocker(cwd, stateDir, payload).catch(() => {});
|
|
10032
|
+
await recordNativeSubagentSupportBlocker(cwd, stateDir, payload).catch(() => {});
|
|
9646
10033
|
if (detectMcpTransportFailure(payload)) {
|
|
9647
10034
|
await markTeamTransportFailure(cwd, payload);
|
|
9648
10035
|
}
|