@jphutchins/code-review 0.1.0-alpha.13 → 0.1.0-alpha.14

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/dist/index.js CHANGED
@@ -762,10 +762,14 @@ var money = (n) => `$${n.toFixed(2)}`;
762
762
  var mins = (ms) => `${(ms / 6e4).toFixed(1)}m`;
763
763
  var spendClause = (i) => i.spentUsd === null ? null : i.budgetUsd !== null && i.budgetUsd > 0 ? `spent ${money(i.spentUsd)}/${money(i.budgetUsd)} (${pct(i.spentUsd / i.budgetUsd)})` : `spent ${money(i.spentUsd)}`;
764
764
  var timeClause = (i) => i.elapsedMs === null ? null : i.wallMs !== null && i.wallMs > 0 ? `${mins(i.elapsedMs)}/${mins(i.wallMs)} elapsed (${pct(i.elapsedMs / i.wallMs)})` : `${mins(i.elapsedMs)} elapsed`;
765
- var directive = (phase, draftPath) => phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Write your COMPLETE findings to ${draftPath} and run \`code-review validate ${draftPath} --explain\` until it passes (--explain prints the exact schema when the shape is wrong). Other tools are blocked until that draft is written.` : `Wind down investigation and write your COMPLETE findings to ${draftPath} now, then run \`code-review validate ${draftPath} --explain\` (it prints the exact schema if the shape is wrong) \u2014 you may run out of budget before you finish otherwise.`;
766
- var budgetMessage = (i, phase, draftPath) => {
765
+ var directive = (phase, draftPath, isSubagent) => {
766
+ if (isSubagent)
767
+ return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now and report the findings you have back to the main agent in your reply. Do not write ${draftPath} yourself; only the main agent writes it.` : `Wind down investigation and report the findings you have back to the main agent in your reply \u2014 do not write ${draftPath} yourself; the main agent writes it, and you may run out of budget otherwise.`;
768
+ return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Write your COMPLETE findings to ${draftPath} and run \`code-review validate ${draftPath} --explain\` until it passes (--explain prints the exact schema when the shape is wrong). Other tools are blocked until that draft is written.` : `Wind down investigation and write your COMPLETE findings to ${draftPath} now, then run \`code-review validate ${draftPath} --explain\` (it prints the exact schema if the shape is wrong) \u2014 you may run out of budget before you finish otherwise.`;
769
+ };
770
+ var budgetMessage = (i, phase, draftPath, isSubagent) => {
767
771
  const status = [spendClause(i), timeClause(i)].filter((c) => c !== null).join(" \xB7 ");
768
- return `Budget check \u2014 ${status}. ${directive(phase, draftPath)}`;
772
+ return `Budget check \u2014 ${status}. ${directive(phase, draftPath, isSubagent)}`;
769
773
  };
770
774
  var invokesCodeReviewValidate = (toolInput) => {
771
775
  const cmd = asRecord(toolInput)?.["command"];
@@ -778,6 +782,35 @@ var blockedDuringConvergence = (toolName, toolInput) => {
778
782
  if (toolName === "Bash") return !invokesCodeReviewValidate(toolInput);
779
783
  return false;
780
784
  };
785
+ var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
786
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
787
+ var writesToDraft = (toolName, toolInput, draftPath) => {
788
+ const rec = asRecord(toolInput);
789
+ const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
790
+ if (WRITE_TOOLS.has(toolName)) {
791
+ const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
792
+ if (typeof fp === "string" && targets.some((t4) => fp === t4 || basename(fp) === basename(t4)))
793
+ return true;
794
+ }
795
+ if (toolName === "Bash") {
796
+ const cmd = rec?.["command"];
797
+ if (typeof cmd !== "string") return false;
798
+ const alt = targets.map(escapeRegExp).join("|");
799
+ const end = "(?=$|[\\s|&;)])";
800
+ const redirect = new RegExp(`>>?\\|?\\s*(['"]?)(?:${alt})\\1${end}`);
801
+ const teeArg = new RegExp(`\\btee\\b(?:\\s+-{1,2}\\S+)*\\s+(['"]?)(?:${alt})\\1${end}`);
802
+ return redirect.test(cmd) || teeArg.test(cmd);
803
+ }
804
+ return false;
805
+ };
806
+ var singleWriterMessage = (draftPath) => `Only the main agent may write ${draftPath}. When a subagent writes it too, the concurrent writers clobber each other and the review comes out empty. Do NOT write, edit, or redirect into ${draftPath} \u2014 instead, return the findings you discovered in your reply (the field names are in the schema); the main agent collects every subagent's reported findings and writes the draft itself.`;
807
+ var denyPreTool = (reason) => ({
808
+ hookSpecificOutput: {
809
+ hookEventName: "PreToolUse",
810
+ permissionDecision: "deny",
811
+ permissionDecisionReason: reason
812
+ }
813
+ });
781
814
  var evaluateBudgetHook = (input, params) => {
782
815
  const rec = asRecord(input);
783
816
  const inputs = {
@@ -788,25 +821,23 @@ var evaluateBudgetHook = (input, params) => {
788
821
  reserve: params.reserve
789
822
  };
790
823
  const phase = decideBudget(inputs);
824
+ const agentId = rec?.["agent_id"];
825
+ const isSubagent = typeof agentId === "string" && agentId.length > 0;
791
826
  switch (rec?.["hook_event_name"]) {
792
827
  case "PostToolBatch":
793
828
  return phase.kind === "ok" ? {} : {
794
829
  hookSpecificOutput: {
795
830
  hookEventName: "PostToolBatch",
796
- additionalContext: budgetMessage(inputs, phase, params.draftPath)
831
+ additionalContext: budgetMessage(inputs, phase, params.draftPath, isSubagent)
797
832
  }
798
833
  };
799
834
  case "PreToolUse": {
800
- if (phase.kind !== "hard") return {};
801
835
  const toolName = rec["tool_name"];
836
+ if (isSubagent && typeof toolName === "string" && writesToDraft(toolName, rec["tool_input"], params.draftPath))
837
+ return denyPreTool(singleWriterMessage(params.draftPath));
838
+ if (phase.kind !== "hard") return {};
802
839
  if (typeof toolName === "string" && blockedDuringConvergence(toolName, rec["tool_input"]))
803
- return {
804
- hookSpecificOutput: {
805
- hookEventName: "PreToolUse",
806
- permissionDecision: "deny",
807
- permissionDecisionReason: budgetMessage(inputs, phase, params.draftPath)
808
- }
809
- };
840
+ return denyPreTool(budgetMessage(inputs, phase, params.draftPath, isSubagent));
810
841
  return {};
811
842
  }
812
843
  default: