oh-my-codex 0.20.0 → 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/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
- 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 +9 -0
- package/dist/cli/__tests__/doctor-invalid-config.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__/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 +3 -3
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +137 -5
- 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 +297 -3
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +42 -31
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/setup.js +2 -2
- 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 +200 -104
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +22 -48
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +19 -10
- 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/leader/__tests__/contract.test.js +43 -1
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +23 -5
- package/dist/leader/contract.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +1104 -20
- 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 +139 -19
- package/dist/scripts/codex-native-hook.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/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/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +1281 -53
- package/src/scripts/codex-native-hook.ts +142 -16
|
@@ -130,7 +130,6 @@ import {
|
|
|
130
130
|
} from "../question/deep-interview.js";
|
|
131
131
|
import { readAutopilotDeepInterviewQuestionWaitState } from "../question/autopilot-wait.js";
|
|
132
132
|
import {
|
|
133
|
-
buildDocumentRefreshAdvisoryOutput,
|
|
134
133
|
evaluateFinalHandoffDocumentRefresh,
|
|
135
134
|
isFinalHandoffDocumentRefreshCandidate,
|
|
136
135
|
} from "../document-refresh/enforcer.js";
|
|
@@ -2740,8 +2739,6 @@ async function buildModeBasedStopOutput(
|
|
|
2740
2739
|
reason: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}); continue the task and gather fresh verification evidence before stopping.`,
|
|
2741
2740
|
stopReason: `${mode}_${phase}`,
|
|
2742
2741
|
systemMessage,
|
|
2743
|
-
statePath,
|
|
2744
|
-
canonicalDisagreement,
|
|
2745
2742
|
};
|
|
2746
2743
|
}
|
|
2747
2744
|
|
|
@@ -3653,8 +3650,14 @@ function isAllowedDeepInterviewArtifactPath(cwd: string, rawPath: string): boole
|
|
|
3653
3650
|
return isAllowedPlanningArtifactPath(cwd, rawPath, DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES);
|
|
3654
3651
|
}
|
|
3655
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
|
+
|
|
3656
3658
|
function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
|
|
3657
|
-
return
|
|
3659
|
+
return isAllowedRalplanDraftPath(cwd, rawPath)
|
|
3660
|
+
|| isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
3658
3661
|
}
|
|
3659
3662
|
|
|
3660
3663
|
interface RalplanBeadsCommandClassification {
|
|
@@ -3874,9 +3877,72 @@ function resolveCommandRedirectTarget(target: string, assignments: Map<string, s
|
|
|
3874
3877
|
return resolved !== undefined ? resolved : target;
|
|
3875
3878
|
}
|
|
3876
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
|
+
|
|
3877
3943
|
function extractDeepInterviewCommandRedirectTargets(command: string): string[] {
|
|
3878
3944
|
const targets: string[] = [];
|
|
3879
|
-
const commandOutsideHeredocBodies = stripHeredocBodiesForCommandScan(command);
|
|
3945
|
+
const commandOutsideHeredocBodies = maskQuotedRedirectMetacharsForCommandScan(stripHeredocBodiesForCommandScan(command));
|
|
3880
3946
|
for (const match of commandOutsideHeredocBodies.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
|
|
3881
3947
|
const candidate = safeString(match[2]).trim();
|
|
3882
3948
|
if (candidate && !isNullDeviceRedirectTarget(candidate)) targets.push(candidate);
|
|
@@ -3974,7 +4040,7 @@ function findGitSubcommandIndex(words: string[], startIndex: number): number | n
|
|
|
3974
4040
|
}
|
|
3975
4041
|
|
|
3976
4042
|
|
|
3977
|
-
function commandHasDeepInterviewWriteIntent(command: string): boolean {
|
|
4043
|
+
function commandHasDeepInterviewWriteIntent(command: string, depth = 0): boolean {
|
|
3978
4044
|
return commandInvokesApplyPatch(command)
|
|
3979
4045
|
|| extractDeepInterviewCommandRedirectTargets(command).length > 0
|
|
3980
4046
|
|| /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
|
|
@@ -3985,7 +4051,19 @@ function commandHasDeepInterviewWriteIntent(command: string): boolean {
|
|
|
3985
4051
|
|| extractConductorBashMutations(command).length > 0
|
|
3986
4052
|
|| extractConductorInterpreterWrites(command).length > 0
|
|
3987
4053
|
|| commandHasDestructiveGitSubcommand(command)
|
|
3988
|
-
|| 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
|
+
));
|
|
3989
4067
|
}
|
|
3990
4068
|
|
|
3991
4069
|
function extractDeepInterviewCommandWriteTargets(command: string): string[] {
|
|
@@ -6971,7 +7049,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
6971
7049
|
resolvedSessionId?: string,
|
|
6972
7050
|
): Promise<Record<string, unknown> | null> {
|
|
6973
7051
|
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
6974
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
|
|
7052
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId)) return null;
|
|
6975
7053
|
const threadId = readPayloadThreadId(payload);
|
|
6976
7054
|
const activeState = await readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId);
|
|
6977
7055
|
if (!activeState) return null;
|
|
@@ -7025,7 +7103,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
7025
7103
|
hookEventName: "PreToolUse",
|
|
7026
7104
|
additionalContext:
|
|
7027
7105
|
`${planningModeDescription}. `
|
|
7028
|
-
+ "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/`. "
|
|
7029
7107
|
+ "Do not edit implementation files or run implementation-focused writes from planning phases. "
|
|
7030
7108
|
+ `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
|
|
7031
7109
|
},
|
|
@@ -7039,7 +7117,7 @@ async function buildDeepInterviewPreToolUseBoundaryOutput(
|
|
|
7039
7117
|
resolvedSessionId?: string,
|
|
7040
7118
|
): Promise<Record<string, unknown> | null> {
|
|
7041
7119
|
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
7042
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
|
|
7120
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId)) return null;
|
|
7043
7121
|
const threadId = readPayloadThreadId(payload);
|
|
7044
7122
|
const activeState = await readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId);
|
|
7045
7123
|
if (!activeState) return null;
|
|
@@ -7210,17 +7288,67 @@ interface ActiveConductorState {
|
|
|
7210
7288
|
async function hasTrustedTypedSubagentProvenanceForPreToolUse(
|
|
7211
7289
|
payload: CodexHookPayload,
|
|
7212
7290
|
cwd: string,
|
|
7291
|
+
stateDir: string,
|
|
7213
7292
|
sessionId: string,
|
|
7293
|
+
options: { allowUntypedProvenance?: boolean } = {},
|
|
7214
7294
|
): Promise<boolean> {
|
|
7215
7295
|
if (hasTeamWorkerEnvironment()) return true;
|
|
7216
|
-
if (!isTypedAgentRolePayload(payload)) return false;
|
|
7217
7296
|
const trackingState = await readSubagentTrackingState(cwd).catch(() => null);
|
|
7218
7297
|
const session = trackingState?.sessions?.[sessionId];
|
|
7219
7298
|
if (!session) return false;
|
|
7220
7299
|
|
|
7221
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.
|
|
7222
7344
|
if (payloadThreadId && isTrustedSubagentThread(session, payloadThreadId)) return true;
|
|
7223
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).
|
|
7224
7352
|
const source = safeObject(payload.source);
|
|
7225
7353
|
const subagent = safeObject(source.subagent);
|
|
7226
7354
|
const threadSpawn = safeObject(subagent.thread_spawn);
|
|
@@ -7231,9 +7359,7 @@ async function hasTrustedTypedSubagentProvenanceForPreToolUse(
|
|
|
7231
7359
|
?? threadSpawn.leaderThreadId,
|
|
7232
7360
|
).trim();
|
|
7233
7361
|
if (!parentThreadId) return false;
|
|
7234
|
-
|
|
7235
|
-
if (payloadThreadId && leaderThreadId && payloadThreadId === leaderThreadId) return false;
|
|
7236
|
-
return leaderThreadId === parentThreadId || parentThreadId in session.threads;
|
|
7362
|
+
return leaderIdentityAnchors.has(parentThreadId) || parentThreadId in session.threads;
|
|
7237
7363
|
}
|
|
7238
7364
|
|
|
7239
7365
|
function isActiveConductorModeState(state: Record<string, unknown> | null, mode: string, sessionId: string): boolean {
|
|
@@ -7262,7 +7388,7 @@ async function readActiveMainRootConductorStateForPreToolUse(
|
|
|
7262
7388
|
}
|
|
7263
7389
|
const threadId = readPayloadThreadId(payload);
|
|
7264
7390
|
if (!sessionId) return null;
|
|
7265
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId)) return null;
|
|
7391
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId, { allowUntypedProvenance: true })) return null;
|
|
7266
7392
|
|
|
7267
7393
|
const canonicalState = await readVisibleSkillActiveStateForStateDir(stateDir, sessionId);
|
|
7268
7394
|
if (!canonicalState) return null;
|
|
@@ -9538,7 +9664,7 @@ async function buildStopHookOutput(
|
|
|
9538
9664
|
documentRefreshWarning.triggeringPaths.join("|"),
|
|
9539
9665
|
canonicalSessionId,
|
|
9540
9666
|
),
|
|
9541
|
-
|
|
9667
|
+
{ systemMessage: documentRefreshWarning.message },
|
|
9542
9668
|
canonicalSessionId,
|
|
9543
9669
|
{ allowRepeatDuringStopHook: false },
|
|
9544
9670
|
);
|