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.
Files changed (56) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
  4. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  5. package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
  6. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  7. package/dist/cli/__tests__/doctor-invalid-config.test.js +9 -0
  8. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  9. package/dist/cli/__tests__/doctor-warning-copy.test.js +87 -1
  10. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  11. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +54 -2
  12. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  13. package/dist/cli/__tests__/setup-install-mode.test.js +3 -3
  14. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  15. package/dist/cli/__tests__/setup-refresh.test.js +137 -5
  16. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  17. package/dist/cli/__tests__/setup-scope.test.js +27 -3
  18. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  19. package/dist/cli/__tests__/uninstall.test.js +297 -3
  20. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  21. package/dist/cli/doctor.d.ts +2 -0
  22. package/dist/cli/doctor.d.ts.map +1 -1
  23. package/dist/cli/doctor.js +42 -31
  24. package/dist/cli/doctor.js.map +1 -1
  25. package/dist/cli/setup.js +2 -2
  26. package/dist/cli/setup.js.map +1 -1
  27. package/dist/cli/uninstall.d.ts.map +1 -1
  28. package/dist/cli/uninstall.js +3 -4
  29. package/dist/cli/uninstall.js.map +1 -1
  30. package/dist/config/__tests__/generator-idempotent.test.js +200 -104
  31. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  32. package/dist/config/__tests__/generator-notify.test.js +22 -48
  33. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  34. package/dist/config/generator.d.ts +19 -10
  35. package/dist/config/generator.d.ts.map +1 -1
  36. package/dist/config/generator.js +169 -152
  37. package/dist/config/generator.js.map +1 -1
  38. package/dist/leader/__tests__/contract.test.js +43 -1
  39. package/dist/leader/__tests__/contract.test.js.map +1 -1
  40. package/dist/leader/contract.d.ts.map +1 -1
  41. package/dist/leader/contract.js +23 -5
  42. package/dist/leader/contract.js.map +1 -1
  43. package/dist/scripts/__tests__/codex-native-hook.test.js +1104 -20
  44. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  45. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  46. package/dist/scripts/codex-native-hook.js +139 -19
  47. package/dist/scripts/codex-native-hook.js.map +1 -1
  48. package/dist/utils/__tests__/agents-md.test.js +16 -0
  49. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  50. package/dist/utils/agents-md.d.ts.map +1 -1
  51. package/dist/utils/agents-md.js +4 -3
  52. package/dist/utils/agents-md.js.map +1 -1
  53. package/package.json +1 -1
  54. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  55. package/src/scripts/__tests__/codex-native-hook.test.ts +1281 -53
  56. 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;AAyDlE,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;AA4tCD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAMnE;AA0uKD,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"}
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 { buildDocumentRefreshAdvisoryOutput, evaluateFinalHandoffDocumentRefresh, isFinalHandoffDocumentRefreshCandidate, } from "../document-refresh/enforcer.js";
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 isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
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
- const leaderThreadId = safeString(session.leader_thread_id).trim();
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), buildDocumentRefreshAdvisoryOutput(documentRefreshWarning, "Stop"), canonicalSessionId, { allowRepeatDuringStopHook: false });
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;