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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex-native-hook.d.ts","sourceRoot":"","sources":["../../src/scripts/codex-native-hook.ts"],"names":[],"mappings":"AA+CA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,8BAA8B,CAAC;AAiCtC,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"codex-native-hook.d.ts","sourceRoot":"","sources":["../../src/scripts/codex-native-hook.ts"],"names":[],"mappings":"AA+CA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,8BAA8B,CAAC;AAiCtC,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAwDlE,KAAK,kBAAkB,GACnB,cAAc,GACd,YAAY,GACZ,aAAa,GACb,kBAAkB,GAClB,YAAY,GACZ,aAAa,GACb,MAAM,CAAC;AAEX,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEhD,UAAU,yBAAyB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6BAA6B,CAAC,EAAE,OAAO,2BAA2B,CAAC;CACpE;AAED,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACzC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5C;AAmfD,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,kBAAkB,GAAG,IAAI,GACvC,MAAM,GAAG,IAAI,CAmBf;AAsxBD,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IACP,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC/C,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACzC,GACL,MAAM,GAAG,IAAI,CAuBf;AA0tCD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAMnE;AA22KD,wBAAsB,wCAAwC,CAC5D,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAqDzC;AA83CD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,gBAAgB,EACzB,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,wBAAwB,CAAC,CA6VnC;AA6BD,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAGT;AAyOD,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAyC3D"}
|
|
@@ -42,7 +42,7 @@ import { readTriageConfig } from "../hooks/triage-config.js";
|
|
|
42
42
|
import { readTriageState, writeTriageState, shouldSuppressFollowup, promptSignature, } from "../hooks/triage-state.js";
|
|
43
43
|
import { isPendingDeepInterviewQuestionEnforcement, reconcileDeepInterviewQuestionEnforcementFromAnsweredRecords, } from "../question/deep-interview.js";
|
|
44
44
|
import { readAutopilotDeepInterviewQuestionWaitState } from "../question/autopilot-wait.js";
|
|
45
|
-
import {
|
|
45
|
+
import { evaluateFinalHandoffDocumentRefresh, isFinalHandoffDocumentRefreshCandidate, } from "../document-refresh/enforcer.js";
|
|
46
46
|
import { buildExecFollowupStopOutput } from "../exec/followup.js";
|
|
47
47
|
import { MAX_NATIVE_STDIN_JSON_BYTES, extractRawCodexHookEventName, } from "./hook-payload-guard.js";
|
|
48
48
|
const TERMINAL_MODE_PHASES = new Set(["complete", "completed", "failed", "cancelled"]);
|
|
@@ -2252,8 +2252,6 @@ async function buildModeBasedStopOutput(mode, cwd, sessionId) {
|
|
|
2252
2252
|
reason: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}); continue the task and gather fresh verification evidence before stopping.`,
|
|
2253
2253
|
stopReason: `${mode}_${phase}`,
|
|
2254
2254
|
systemMessage,
|
|
2255
|
-
statePath,
|
|
2256
|
-
canonicalDisagreement,
|
|
2257
2255
|
};
|
|
2258
2256
|
}
|
|
2259
2257
|
export function looksLikeGoalCompletionPrompt(text) {
|
|
@@ -3065,8 +3063,13 @@ function isAllowedPlanningArtifactPath(cwd, rawPath, allowedPrefixes) {
|
|
|
3065
3063
|
function isAllowedDeepInterviewArtifactPath(cwd, rawPath) {
|
|
3066
3064
|
return isAllowedPlanningArtifactPath(cwd, rawPath, DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES);
|
|
3067
3065
|
}
|
|
3066
|
+
function isAllowedRalplanDraftPath(cwd, rawPath) {
|
|
3067
|
+
const relativePath = normalizePlanningArtifactRelativePath(cwd, rawPath);
|
|
3068
|
+
return relativePath !== null && /^\.omx\/drafts\/[^/]+\.md$/.test(relativePath);
|
|
3069
|
+
}
|
|
3068
3070
|
function isAllowedRalplanArtifactPath(cwd, rawPath) {
|
|
3069
|
-
return
|
|
3071
|
+
return isAllowedRalplanDraftPath(cwd, rawPath)
|
|
3072
|
+
|| isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
3070
3073
|
}
|
|
3071
3074
|
function shellTokenizeLiteralCommand(command) {
|
|
3072
3075
|
const tokens = [];
|
|
@@ -3274,9 +3277,73 @@ function resolveCommandRedirectTarget(target, assignments) {
|
|
|
3274
3277
|
const resolved = assignments.get(safeString(variableMatch[1]));
|
|
3275
3278
|
return resolved !== undefined ? resolved : target;
|
|
3276
3279
|
}
|
|
3280
|
+
// Masks redirect metacharacters (`<`/`>`) that appear INSIDE shell quotes so a
|
|
3281
|
+
// quoted regex/source value (e.g. `gh issue create --body '...>{1,2}...'` or
|
|
3282
|
+
// `omx state write --input '{"reason":"a>b"}'`) is not misread as a redirect
|
|
3283
|
+
// write target. Escaped quotes at the top level (`\'`, `\"`) are literal
|
|
3284
|
+
// characters and must NOT open a span, and `$'...'` ANSI-C quoting processes
|
|
3285
|
+
// backslash escapes (so `\'` does not close it) — otherwise a genuine `>`
|
|
3286
|
+
// redirect could be masked behind a false span and its write missed. Only
|
|
3287
|
+
// characters inside a real quoted span are masked; unquoted redirect operators
|
|
3288
|
+
// survive intact. Unterminated/ambiguous quoting fails closed: the original
|
|
3289
|
+
// command is returned unmasked so genuine redirects stay visible to the scan.
|
|
3290
|
+
function maskQuotedRedirectMetacharsForCommandScan(command) {
|
|
3291
|
+
let masked = "";
|
|
3292
|
+
// null = unquoted; "'" = single quotes (no escapes); '"' = double quotes
|
|
3293
|
+
// (backslash escapes); "$'" = ANSI-C $'...' (backslash escapes, incl. \').
|
|
3294
|
+
let quote = null;
|
|
3295
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
3296
|
+
const char = command[index] ?? "";
|
|
3297
|
+
if (quote === null) {
|
|
3298
|
+
// A backslash escapes the next character at the top level, so an escaped
|
|
3299
|
+
// quote is a literal char and must not open a quoted span.
|
|
3300
|
+
if (char === "\\") {
|
|
3301
|
+
masked += char;
|
|
3302
|
+
const next = command[index + 1];
|
|
3303
|
+
if (next !== undefined) {
|
|
3304
|
+
masked += next;
|
|
3305
|
+
index += 1;
|
|
3306
|
+
}
|
|
3307
|
+
continue;
|
|
3308
|
+
}
|
|
3309
|
+
if (char === "$" && command[index + 1] === "'") {
|
|
3310
|
+
quote = "$'";
|
|
3311
|
+
masked += "$'";
|
|
3312
|
+
index += 1;
|
|
3313
|
+
continue;
|
|
3314
|
+
}
|
|
3315
|
+
if (char === "'" || char === "\"")
|
|
3316
|
+
quote = char;
|
|
3317
|
+
masked += char;
|
|
3318
|
+
continue;
|
|
3319
|
+
}
|
|
3320
|
+
if ((quote === "\"" || quote === "$'") && char === "\\") {
|
|
3321
|
+
// Escapes inside double/ANSI-C spans keep the backslash and its escaped
|
|
3322
|
+
// char (masking a redirect metachar so quoted data cannot be a false
|
|
3323
|
+
// target); the escaped char never closes the span.
|
|
3324
|
+
masked += char;
|
|
3325
|
+
const next = command[index + 1];
|
|
3326
|
+
if (next !== undefined) {
|
|
3327
|
+
masked += next === "<" || next === ">" ? "_" : next;
|
|
3328
|
+
index += 1;
|
|
3329
|
+
}
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
const closesSpan = quote === "$'" ? char === "'" : char === quote;
|
|
3333
|
+
if (closesSpan) {
|
|
3334
|
+
quote = null;
|
|
3335
|
+
masked += char;
|
|
3336
|
+
continue;
|
|
3337
|
+
}
|
|
3338
|
+
masked += char === "<" || char === ">" ? "_" : char;
|
|
3339
|
+
}
|
|
3340
|
+
if (quote !== null)
|
|
3341
|
+
return command;
|
|
3342
|
+
return masked;
|
|
3343
|
+
}
|
|
3277
3344
|
function extractDeepInterviewCommandRedirectTargets(command) {
|
|
3278
3345
|
const targets = [];
|
|
3279
|
-
const commandOutsideHeredocBodies = stripHeredocBodiesForCommandScan(command);
|
|
3346
|
+
const commandOutsideHeredocBodies = maskQuotedRedirectMetacharsForCommandScan(stripHeredocBodiesForCommandScan(command));
|
|
3280
3347
|
for (const match of commandOutsideHeredocBodies.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
|
|
3281
3348
|
const candidate = safeString(match[2]).trim();
|
|
3282
3349
|
if (candidate && !isNullDeviceRedirectTarget(candidate))
|
|
@@ -3388,7 +3455,7 @@ function findGitSubcommandIndex(words, startIndex) {
|
|
|
3388
3455
|
}
|
|
3389
3456
|
return null;
|
|
3390
3457
|
}
|
|
3391
|
-
function commandHasDeepInterviewWriteIntent(command) {
|
|
3458
|
+
function commandHasDeepInterviewWriteIntent(command, depth = 0) {
|
|
3392
3459
|
return commandInvokesApplyPatch(command)
|
|
3393
3460
|
|| extractDeepInterviewCommandRedirectTargets(command).length > 0
|
|
3394
3461
|
|| /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
|
|
@@ -3399,7 +3466,17 @@ function commandHasDeepInterviewWriteIntent(command) {
|
|
|
3399
3466
|
|| extractConductorBashMutations(command).length > 0
|
|
3400
3467
|
|| extractConductorInterpreterWrites(command).length > 0
|
|
3401
3468
|
|| commandHasDestructiveGitSubcommand(command)
|
|
3402
|
-
|| commandHasPackageInstallIntent(command)
|
|
3469
|
+
|| commandHasPackageInstallIntent(command)
|
|
3470
|
+
// Recurse into wrapped shells (`bash -lc "cat > f"`, `eval`, `env`) and
|
|
3471
|
+
// command substitutions so a real redirect INSIDE a nested command string
|
|
3472
|
+
// is still classified as write intent. This is required because quoted
|
|
3473
|
+
// redirect metacharacters are now masked at the outer scan (#3119): the
|
|
3474
|
+
// mask removes false positives from quoted DATA values while nested-command
|
|
3475
|
+
// recursion re-detects genuine redirects, preserving fail-closed blocking.
|
|
3476
|
+
// The depth guard mirrors evaluateConductorBashWrite's nesting bound;
|
|
3477
|
+
// extractors return strict substrings, so recursion always terminates.
|
|
3478
|
+
|| (depth < CONDUCTOR_BASH_MAX_NESTING_DEPTH && (extractNestedShellCommandStringsForStateScan(command).some((nested) => commandHasDeepInterviewWriteIntent(nested, depth + 1))
|
|
3479
|
+
|| extractNestedCommandSubstitutionStringsForStateScan(command).some((nested) => commandHasDeepInterviewWriteIntent(nested, depth + 1))));
|
|
3403
3480
|
}
|
|
3404
3481
|
function extractDeepInterviewCommandWriteTargets(command) {
|
|
3405
3482
|
const assignments = extractCommandLiteralAssignments(command);
|
|
@@ -6399,7 +6476,7 @@ function buildDeepInterviewBashBlockedDetail(cwd, command) {
|
|
|
6399
6476
|
}
|
|
6400
6477
|
async function buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, resolvedSessionId) {
|
|
6401
6478
|
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
6402
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId))
|
|
6479
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId))
|
|
6403
6480
|
return null;
|
|
6404
6481
|
const threadId = readPayloadThreadId(payload);
|
|
6405
6482
|
const activeState = await readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId);
|
|
@@ -6450,7 +6527,7 @@ async function buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, reso
|
|
|
6450
6527
|
hookSpecificOutput: {
|
|
6451
6528
|
hookEventName: "PreToolUse",
|
|
6452
6529
|
additionalContext: `${planningModeDescription}. `
|
|
6453
|
-
+ "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, `.omx/tmp/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
|
|
6530
|
+
+ "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/`. "
|
|
6454
6531
|
+ "Do not edit implementation files or run implementation-focused writes from planning phases. "
|
|
6455
6532
|
+ `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
|
|
6456
6533
|
},
|
|
@@ -6458,7 +6535,7 @@ async function buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, reso
|
|
|
6458
6535
|
}
|
|
6459
6536
|
async function buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, resolvedSessionId) {
|
|
6460
6537
|
const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
|
|
6461
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId))
|
|
6538
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId))
|
|
6462
6539
|
return null;
|
|
6463
6540
|
const threadId = readPayloadThreadId(payload);
|
|
6464
6541
|
const activeState = await readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId);
|
|
@@ -6576,18 +6653,64 @@ async function buildPlanningRootPointerConflictPreToolUseOutput(payload, cwd, st
|
|
|
6576
6653
|
}
|
|
6577
6654
|
return blocked ? buildRalplanRootPointerConflictBlock(ralplanState) : null;
|
|
6578
6655
|
}
|
|
6579
|
-
async function hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId) {
|
|
6656
|
+
async function hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId, options = {}) {
|
|
6580
6657
|
if (hasTeamWorkerEnvironment())
|
|
6581
6658
|
return true;
|
|
6582
|
-
if (!isTypedAgentRolePayload(payload))
|
|
6583
|
-
return false;
|
|
6584
6659
|
const trackingState = await readSubagentTrackingState(cwd).catch(() => null);
|
|
6585
6660
|
const session = trackingState?.sessions?.[sessionId];
|
|
6586
6661
|
if (!session)
|
|
6587
6662
|
return false;
|
|
6588
6663
|
const payloadThreadId = readPayloadThreadId(payload);
|
|
6664
|
+
// Resolve the Main-root leader THREAD identity from the tracker's leader_thread_id
|
|
6665
|
+
// plus the canonical session's native_session_id (the leader's native thread). Only
|
|
6666
|
+
// genuine leader-thread identifiers may anchor the leader: owner_*_session_id are
|
|
6667
|
+
// session-level ids, not thread anchors, so they must NOT populate this set — their
|
|
6668
|
+
// mere presence would make it non-empty and suppress the fail-closed guard below
|
|
6669
|
+
// without ever excluding the leader thread (#3117 P4). The native_session_id anchor is
|
|
6670
|
+
// honored only when the root session pointer provably maps to the sessionId under
|
|
6671
|
+
// evaluation, so a stale/foreign session.json cannot supply another session's leader
|
|
6672
|
+
// anchor (#3117 P3); the tracker alone is insufficient because it can omit
|
|
6673
|
+
// leader_thread_id or corruptly label the leader kind:"subagent" (#3117 P2).
|
|
6674
|
+
const sessionState = await readRootSessionStateFromStateDir(stateDir).catch(() => null);
|
|
6675
|
+
const trackerLeaderThreadId = safeString(session.leader_thread_id).trim();
|
|
6676
|
+
const leaderIdentityAnchors = new Set();
|
|
6677
|
+
if (trackerLeaderThreadId)
|
|
6678
|
+
leaderIdentityAnchors.add(trackerLeaderThreadId);
|
|
6679
|
+
if (sessionState && sessionId && payloadMatchesSessionPointer(sessionId, sessionState)) {
|
|
6680
|
+
const nativeSessionId = safeString(sessionState.native_session_id).trim();
|
|
6681
|
+
if (nativeSessionId)
|
|
6682
|
+
leaderIdentityAnchors.add(nativeSessionId);
|
|
6683
|
+
}
|
|
6684
|
+
// Leader self-guard: the Main-root Conductor is never a delegated executor. Block it
|
|
6685
|
+
// ahead of every trust path, even when tracker or payload provenance is (possibly
|
|
6686
|
+
// corruptly) attached to the leader thread.
|
|
6687
|
+
if (payloadThreadId && leaderIdentityAnchors.has(payloadThreadId))
|
|
6688
|
+
return false;
|
|
6689
|
+
// Fail closed: without an authoritative leader anchor we cannot affirm the payload is
|
|
6690
|
+
// a non-leader delegated performer rather than a mislabeled leader, so we refuse trust
|
|
6691
|
+
// instead of inferring it from a corrupt tracker kind:"subagent" alone (#3117 P2).
|
|
6692
|
+
if (leaderIdentityAnchors.size === 0)
|
|
6693
|
+
return false;
|
|
6694
|
+
// Planning boundary guards (ralplan, deep-interview) still require a recognized typed
|
|
6695
|
+
// agent role, so an untyped collaboration.spawn_agent child cannot write before an
|
|
6696
|
+
// execution handoff/approval. Only the Main-root Conductor/Ralph executing guard opts
|
|
6697
|
+
// into untyped tracker/runtime provenance (#3116, #3117).
|
|
6698
|
+
if (options.allowUntypedProvenance !== true && !isTypedAgentRolePayload(payload)) {
|
|
6699
|
+
return false;
|
|
6700
|
+
}
|
|
6701
|
+
// Tracker-backed provenance: the payload's own thread is a recorded, non-leader
|
|
6702
|
+
// subagent for this session — the non-spoofable anchor that lets native
|
|
6703
|
+
// collaboration.spawn_agent children/descendants edit under the Conductor guard even
|
|
6704
|
+
// without a recognized typed role (#3116). subagent-tracking.json is derived from
|
|
6705
|
+
// child SessionStart transcript metadata, not the live tool-call payload.
|
|
6589
6706
|
if (payloadThreadId && isTrustedSubagentThread(session, payloadThreadId))
|
|
6590
6707
|
return true;
|
|
6708
|
+
// Runtime-attached spawn provenance: trust a genuine spawned subagent turn whose
|
|
6709
|
+
// declared parent maps to this session's leader or a tracked thread. parentThreadId
|
|
6710
|
+
// comes from the runtime-set source.subagent.thread_spawn (not agent-controlled tool
|
|
6711
|
+
// arguments); an absent parent is rejected, the leader self-guard above blocks the
|
|
6712
|
+
// main root, and cross-session parents fail because they are not in this session's
|
|
6713
|
+
// tracked threads (#3116).
|
|
6591
6714
|
const source = safeObject(payload.source);
|
|
6592
6715
|
const subagent = safeObject(source.subagent);
|
|
6593
6716
|
const threadSpawn = safeObject(subagent.thread_spawn);
|
|
@@ -6597,10 +6720,7 @@ async function hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sess
|
|
|
6597
6720
|
?? threadSpawn.leaderThreadId).trim();
|
|
6598
6721
|
if (!parentThreadId)
|
|
6599
6722
|
return false;
|
|
6600
|
-
|
|
6601
|
-
if (payloadThreadId && leaderThreadId && payloadThreadId === leaderThreadId)
|
|
6602
|
-
return false;
|
|
6603
|
-
return leaderThreadId === parentThreadId || parentThreadId in session.threads;
|
|
6723
|
+
return leaderIdentityAnchors.has(parentThreadId) || parentThreadId in session.threads;
|
|
6604
6724
|
}
|
|
6605
6725
|
function isActiveConductorModeState(state, mode, sessionId) {
|
|
6606
6726
|
if (!state || state.active !== true)
|
|
@@ -6627,7 +6747,7 @@ async function readActiveMainRootConductorStateForPreToolUse(payload, cwd, state
|
|
|
6627
6747
|
const threadId = readPayloadThreadId(payload);
|
|
6628
6748
|
if (!sessionId)
|
|
6629
6749
|
return null;
|
|
6630
|
-
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, sessionId))
|
|
6750
|
+
if (await hasTrustedTypedSubagentProvenanceForPreToolUse(payload, cwd, stateDir, sessionId, { allowUntypedProvenance: true }))
|
|
6631
6751
|
return null;
|
|
6632
6752
|
const canonicalState = await readVisibleSkillActiveStateForStateDir(stateDir, sessionId);
|
|
6633
6753
|
if (!canonicalState)
|
|
@@ -8447,7 +8567,7 @@ async function buildStopHookOutput(payload, cwd, stateDir, options = {}) {
|
|
|
8447
8567
|
if (isFinalHandoffDocumentRefreshCandidate(lastAssistantMessage)) {
|
|
8448
8568
|
const documentRefreshWarning = evaluateFinalHandoffDocumentRefresh(cwd, lastAssistantMessage);
|
|
8449
8569
|
if (documentRefreshWarning) {
|
|
8450
|
-
return await maybeReturnRepeatableStopOutput(payload, stateDir, buildRepeatableStopSignature(payload, "document-refresh-stop", documentRefreshWarning.triggeringPaths.join("|"), canonicalSessionId),
|
|
8570
|
+
return await maybeReturnRepeatableStopOutput(payload, stateDir, buildRepeatableStopSignature(payload, "document-refresh-stop", documentRefreshWarning.triggeringPaths.join("|"), canonicalSessionId), { systemMessage: documentRefreshWarning.message }, canonicalSessionId, { allowRepeatDuringStopHook: false });
|
|
8451
8571
|
}
|
|
8452
8572
|
}
|
|
8453
8573
|
return null;
|