@jphutchins/code-review 0.1.0-alpha.18 → 0.1.0-alpha.19

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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from 'citty';
3
- import { readFileSync, writeFileSync, readdirSync } from 'fs';
3
+ import { readFileSync, writeFileSync, statSync, readdirSync } from 'fs';
4
4
  import { resolve as resolve$1, join, dirname, basename } from 'path';
5
5
  import { Eta } from 'eta';
6
6
  import parseDiff from 'parse-diff';
@@ -765,7 +765,7 @@ var timeClause = (i) => i.elapsedMs === null ? null : i.wallMs !== null && i.wal
765
765
  var directive = (phase, draftPath, isSubagent) => {
766
766
  if (isSubagent)
767
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.`;
768
+ return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Do not wait for subagents still running in the background. Write your COMPLETE findings from what you already have 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 fold in the subagent reports you already have rather than waiting on stragglers; you may run out of budget before you finish otherwise.`;
769
769
  };
770
770
  var budgetMessage = (i, phase, draftPath, isSubagent) => {
771
771
  const status = [spendClause(i), timeClause(i)].filter((c) => c !== null).join(" \xB7 ");
@@ -804,6 +804,16 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
804
804
  return false;
805
805
  };
806
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 seedMarkerPath = (draftPath) => `${draftPath}.seed`;
808
+ var mainHasWrittenDraft = (draftMtimeMs, seedMarkerMtimeMs) => draftMtimeMs !== null && (seedMarkerMtimeMs === null || draftMtimeMs > seedMarkerMtimeMs);
809
+ var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and a pre-seeded draft does not count until you have revised it yourself this run. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
810
+ var forceBackgroundSpawn = (toolInput) => ({
811
+ hookSpecificOutput: {
812
+ hookEventName: "PreToolUse",
813
+ permissionDecision: "allow",
814
+ updatedInput: { ...asRecord(toolInput) ?? {}, run_in_background: true }
815
+ }
816
+ });
807
817
  var denyPreTool = (reason) => ({
808
818
  hookSpecificOutput: {
809
819
  hookEventName: "PreToolUse",
@@ -833,11 +843,16 @@ var evaluateBudgetHook = (input, params) => {
833
843
  };
834
844
  case "PreToolUse": {
835
845
  const toolName = rec["tool_name"];
836
- if (isSubagent && typeof toolName === "string" && writesToDraft(toolName, rec["tool_input"], params.draftPath))
846
+ if (typeof toolName !== "string") return {};
847
+ if (isSubagent && writesToDraft(toolName, rec["tool_input"], params.draftPath))
837
848
  return denyPreTool(singleWriterMessage(params.draftPath));
838
- if (phase.kind !== "hard") return {};
839
- if (typeof toolName === "string" && blockedDuringConvergence(toolName, rec["tool_input"]))
849
+ if (phase.kind === "hard" && blockedDuringConvergence(toolName, rec["tool_input"]))
840
850
  return denyPreTool(budgetMessage(inputs, phase, params.draftPath, isSubagent));
851
+ if (SPAWN_TOOLS.has(toolName)) {
852
+ if (!isSubagent && !params.mainDraftWritten)
853
+ return denyPreTool(spawnFloorMessage(params.draftPath));
854
+ return forceBackgroundSpawn(rec["tool_input"]);
855
+ }
841
856
  return {};
842
857
  }
843
858
  default:
@@ -2231,6 +2246,13 @@ var parseBudgetUsd = (raw) => {
2231
2246
  const n = Number.parseFloat(raw);
2232
2247
  return Number.isFinite(n) && n >= 0 ? n : null;
2233
2248
  };
2249
+ var mtimeMsOf = (path) => {
2250
+ try {
2251
+ return statSync(path).mtimeMs;
2252
+ } catch {
2253
+ return null;
2254
+ }
2255
+ };
2234
2256
  var transcriptPathOf = (input) => {
2235
2257
  const tp = (typeof input === "object" && input !== null ? input : {})["transcript_path"];
2236
2258
  return typeof tp === "string" ? tp : void 0;
@@ -2238,7 +2260,7 @@ var transcriptPathOf = (input) => {
2238
2260
  var budgetHookCmd = defineCommand({
2239
2261
  meta: {
2240
2262
  name: "budget-hook",
2241
- description: "Self-dispatching Claude Code hook for budget discipline (issue #38): on PostToolBatch, steer the agent to converge once spend or wall-clock enters its soft wind-down reserve; on PreToolUse, inside the hard reserve, deny the budget-burning tools (subagent spawns, arbitrary shell, web) while leaving the draft-delivery path open. Reads the hook payload on stdin, measures spend from its transcript, and decides on $ and/or time. Degrades to a no-op on any error."
2263
+ description: "Self-dispatching Claude Code hook for budget discipline (issue #38): on PostToolBatch, steer the agent to converge once spend or wall-clock enters its soft wind-down reserve; on PreToolUse, inside the hard reserve, deny the budget-burning tools (subagent spawns, arbitrary shell, web) while leaving the draft-delivery path open. At every phase, the main agent's subagent spawns are denied until it has written its own first-pass draft (seed-draft's sidecar marker tells the seed apart), and permitted spawns are rewritten to run in the background so no batch join can block the spawner (issue #73). Reads the hook payload on stdin, measures spend from its transcript, and decides on $ and/or time. Degrades to a no-op on any error."
2242
2264
  },
2243
2265
  args: {
2244
2266
  draft: {
@@ -2301,7 +2323,11 @@ var budgetHookCmd = defineCommand({
2301
2323
  flatUsd: parseBudgetUsd(args["reserve-usd"]) ?? DEFAULT_RESERVE.flatUsd,
2302
2324
  flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs
2303
2325
  },
2304
- draftPath
2326
+ draftPath,
2327
+ mainDraftWritten: mainHasWrittenDraft(
2328
+ mtimeMsOf(draftPath),
2329
+ mtimeMsOf(seedMarkerPath(draftPath))
2330
+ )
2305
2331
  });
2306
2332
  process.stdout.write(`${JSON.stringify(output)}
2307
2333
  `);
@@ -2484,7 +2510,7 @@ ${printableSchema(schemaPath)}
2484
2510
  var seedDraftCmd = defineCommand({
2485
2511
  meta: {
2486
2512
  name: "seed-draft",
2487
- description: "Write a valid findings $DRAFT before the review runs: the decoded findings from a prior review when one exists and still validates (incremental re-review), else an empty-but-valid scaffold \u2014 so a valid draft exists from turn 0 (issues #52, #53). Prints the mode chosen (prior|empty|none \u2014 none when even the scaffold write failed) to stdout; always exits 0"
2513
+ description: "Write a valid findings $DRAFT before the review runs: the decoded findings from a prior review when one exists and still validates (incremental re-review), else an empty-but-valid scaffold \u2014 so a valid draft exists from turn 0 (issues #52, #53). Also drops a sidecar marker beside the seed so the budget hook can tell the untouched seed from a draft the agent wrote itself (issue #73). Prints the mode chosen (prior|empty|none \u2014 none when even the scaffold write failed) to stdout; always exits 0"
2488
2514
  },
2489
2515
  args: {
2490
2516
  prior: {
@@ -2519,10 +2545,21 @@ var seedDraftCmd = defineCommand({
2519
2545
  `
2520
2546
  );
2521
2547
  }
2548
+ const writeSeedMarker = () => {
2549
+ try {
2550
+ writeFileSync(seedMarkerPath(outPath), "code-review seed marker\n");
2551
+ } catch (err) {
2552
+ process.stderr.write(
2553
+ `Warning: could not write the seed marker beside ${outPath} (${err instanceof Error ? err.message : String(err)}) \u2014 the seeded draft will count as agent-written
2554
+ `
2555
+ );
2556
+ }
2557
+ };
2522
2558
  const writeEmptyScaffold = () => {
2523
2559
  try {
2524
2560
  writeFileSync(outPath, `${JSON.stringify(noticeFindings(""), null, 2)}
2525
2561
  `);
2562
+ writeSeedMarker();
2526
2563
  process.stderr.write(
2527
2564
  `Seeded ${outPath} with an empty valid scaffold \u2014 no decodable prior findings to build on
2528
2565
  `
@@ -2558,6 +2595,7 @@ var seedDraftCmd = defineCommand({
2558
2595
  if (!validateAgainstSchema(priorFindings, schemaPath).valid) return false;
2559
2596
  writeFileSync(outPath, `${JSON.stringify(priorFindings, null, 2)}
2560
2597
  `);
2598
+ writeSeedMarker();
2561
2599
  return true;
2562
2600
  } catch (err) {
2563
2601
  process.stderr.write(