@boopkit/cli 0.6.11 → 0.6.13

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.
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.RESUME_PROMPT = exports.DEFAULT_BUDGET_USD = void 0;
36
+ exports.RESUME_PROMPT = exports.DEFAULT_BUDGET_USD = exports.BD_READ_MAX_BUFFER = void 0;
37
37
  exports.assertDoltServerMode = assertDoltServerMode;
38
38
  exports.discoverDoltServer = discoverDoltServer;
39
39
  exports.resolveDefaultBranch = resolveDefaultBranch;
@@ -57,6 +57,71 @@ const child_process_1 = require("child_process");
57
57
  const fs = __importStar(require("fs"));
58
58
  const path = __importStar(require("path"));
59
59
  const crypto_1 = require("crypto");
60
+ /**
61
+ * Output cap for every `bd … --json` read the loop makes.
62
+ *
63
+ * Node's execSync defaults to 1 MiB and throws ENOBUFS past it. `bd ready --json`
64
+ * returns each issue WITH its full description, and `bd show --children --json`
65
+ * on a large container is the same trap — a mature backlog crosses 1 MiB and the
66
+ * read dies. The ready-queue read also projects its output (see
67
+ * READY_QUEUE_CMD); this cap is the backstop for the reads that cannot (boop-fl22).
68
+ */
69
+ exports.BD_READ_MAX_BUFFER = 256 * 1024 * 1024;
70
+ /**
71
+ * The ready-queue read, projected to the only three fields the filters below use.
72
+ *
73
+ * Unprojected this returns every ready issue with its full description: measured
74
+ * in boop at 274,529 bytes for 112 issues, 69.7% of it description that nothing
75
+ * here reads. The projection takes that to 5,941 bytes. `-n 0` = unlimited, which
76
+ * is itself the fix for boop-pra; unlimited fetch is only safe once the per-record
77
+ * payload is bounded (boop-fl22).
78
+ */
79
+ const READY_QUEUE_CMD = `bd ready --json -n 0 2>/dev/null | jq -c '[.[] | {id, issue_type, parent}]'`;
80
+ /**
81
+ * The same read without the projection, for hosts that have no `jq`.
82
+ *
83
+ * jq ships with neither stock macOS nor stock Debian, and the projection is an
84
+ * optimisation rather than a requirement — BD_READ_MAX_BUFFER already makes the
85
+ * unprojected read safe. So a missing jq degrades the read instead of breaking
86
+ * the loop (boop-fl22).
87
+ */
88
+ const READY_QUEUE_CMD_UNPROJECTED = 'bd ready --json -n 0 2>/dev/null';
89
+ /** Warn once per process, not once per loop iteration. Never gates control flow. */
90
+ let warnedAboutProjection = false;
91
+ function readReadyQueue(execSyncFn) {
92
+ const opts = { encoding: 'utf8', maxBuffer: exports.BD_READ_MAX_BUFFER };
93
+ // Run one read and insist it produced something. A healthy read always emits
94
+ // at least `[]`, so empty output means the command failed — and `sh` has no
95
+ // pipefail, so a pipeline hides the exit status of `bd` itself. This is the
96
+ // only signal that the left side of the pipe died. The command is carried in
97
+ // the message so the diagnostic names the read that actually failed.
98
+ const read = (cmd) => {
99
+ let raw;
100
+ try {
101
+ raw = execSyncFn(cmd, opts);
102
+ }
103
+ catch (e) {
104
+ throw new Error(`${e.message.split('\n')[0]} (command: ${cmd})`);
105
+ }
106
+ if (!raw.trim()) {
107
+ throw new Error(`bd produced no output — a healthy read emits at least [] (command: ${cmd})`);
108
+ }
109
+ return raw;
110
+ };
111
+ // Always attempt the projection: a failed `jq` lookup is microseconds, and
112
+ // caching the outcome would make this function depend on call history.
113
+ try {
114
+ return read(READY_QUEUE_CMD);
115
+ }
116
+ catch (e) {
117
+ if (!warnedAboutProjection) {
118
+ warnedAboutProjection = true;
119
+ console.warn(`\x1b[33m[bd-loop]\x1b[0m Projected ready-queue read failed (${e.message}); ` +
120
+ `falling back to the unprojected read. Install \`jq\` to keep the queue read small.`);
121
+ }
122
+ }
123
+ return read(READY_QUEUE_CMD_UNPROJECTED);
124
+ }
60
125
  /**
61
126
  * Hard requirement: beads must run in Dolt server mode so concurrent worktrees
62
127
  * share one issue DB over TCP. Reads `.beads/metadata.json` and throws an
@@ -490,8 +555,9 @@ async function planWork(opts) {
490
555
  // Fallback: query bd list for recently created features
491
556
  console.log('\x1b[33m[bd-loop]\x1b[0m No FEATURE_ID in output, trying fallback query...');
492
557
  try {
493
- const execFn = opts.execSyncFn || ((cmd, o) => (0, child_process_1.execSync)(cmd, o));
494
- const raw = execFn('bd list --json --type feature 2>/dev/null', { encoding: 'utf8' });
558
+ const execFn = opts.execSyncFn ||
559
+ ((cmd, o) => (0, child_process_1.execSync)(cmd, o));
560
+ const raw = execFn('bd list --json --type feature 2>/dev/null', { encoding: 'utf8', maxBuffer: exports.BD_READ_MAX_BUFFER });
495
561
  const features = JSON.parse(raw);
496
562
  const fallbackId = findFallbackFeature(features, workContent, opts.workPath);
497
563
  if (fallbackId) {
@@ -615,7 +681,7 @@ function findNextIssue(prefix, includeFeatures) {
615
681
  }
616
682
  function getIssueParentId(id) {
617
683
  try {
618
- const raw = (0, child_process_1.execSync)(`bd show ${id} --json 2>/dev/null`, { encoding: 'utf8' });
684
+ const raw = (0, child_process_1.execSync)(`bd show ${id} --json 2>/dev/null`, { encoding: 'utf8', maxBuffer: exports.BD_READ_MAX_BUFFER });
619
685
  const data = JSON.parse(raw);
620
686
  const issue = data[0] || data;
621
687
  return issue.parent || null;
@@ -678,7 +744,7 @@ function shouldCloseFeatureRoot(feature, children) {
678
744
  }
679
745
  function getIssueChildren(id) {
680
746
  try {
681
- const raw = (0, child_process_1.execSync)(`bd show ${id} --children --json 2>/dev/null`, { encoding: 'utf8' });
747
+ const raw = (0, child_process_1.execSync)(`bd show ${id} --children --json 2>/dev/null`, { encoding: 'utf8', maxBuffer: exports.BD_READ_MAX_BUFFER });
682
748
  const data = JSON.parse(raw);
683
749
  const children = data[id];
684
750
  return Array.isArray(children) ? children : [];
@@ -714,7 +780,7 @@ function closeCompletedFeatureRoot(featureId) {
714
780
  }
715
781
  function getIssueInfoWithStatus(id) {
716
782
  try {
717
- const raw = (0, child_process_1.execSync)(`bd show ${id} --json 2>/dev/null`, { encoding: 'utf8' });
783
+ const raw = (0, child_process_1.execSync)(`bd show ${id} --json 2>/dev/null`, { encoding: 'utf8', maxBuffer: exports.BD_READ_MAX_BUFFER });
718
784
  const data = JSON.parse(raw);
719
785
  const issue = data[0] || data;
720
786
  return { status: issue.status || 'unknown', issue_type: issue.issue_type || 'task' };
@@ -723,15 +789,18 @@ function getIssueInfoWithStatus(id) {
723
789
  return null;
724
790
  }
725
791
  }
792
+ /**
793
+ * Read the ready queue, filtered to what this loop may claim.
794
+ *
795
+ * THROWS on a failed read. It must never answer a broken read with an empty
796
+ * array: the caller treats "no issues" as a clean finish, so a swallowed
797
+ * failure makes the loop exit 0 with work undone (boop-fl22).
798
+ */
726
799
  function getMatchingReadyIssues(prefix, includeFeatures, deps) {
727
800
  const execSyncFn = deps?.execSyncFn ||
728
801
  ((cmd, opts) => (0, child_process_1.execSync)(cmd, opts));
729
802
  try {
730
- // -n 0 = unlimited. A bounded window (formerly -n 20) starves the scope
731
- // filter below: with more than 20 ready issues globally, in-scope issues
732
- // sorting outside the window are never seen and the loop exits with group
733
- // work undone (boop-pra).
734
- const raw = execSyncFn('bd ready --json -n 0 2>/dev/null', { encoding: 'utf8' });
803
+ const raw = readReadyQueue(execSyncFn);
735
804
  const issues = JSON.parse(raw);
736
805
  const typeFilter = includeFeatures
737
806
  ? (i) => i.issue_type !== 'epic'
@@ -763,13 +832,13 @@ function getMatchingReadyIssues(prefix, includeFeatures, deps) {
763
832
  .filter((issue) => isIssueInScope(issue, prefix, getParentById))
764
833
  .filter((issue) => isClaimableReadyIssue(issue, hasChildren));
765
834
  }
766
- catch {
767
- return [];
835
+ catch (e) {
836
+ throw new Error(`Could not read the ready queue: ${e.message}`);
768
837
  }
769
838
  }
770
839
  function getIssueInfo(id) {
771
840
  try {
772
- const raw = (0, child_process_1.execSync)(`bd show ${id} --json 2>/dev/null`, { encoding: 'utf8' });
841
+ const raw = (0, child_process_1.execSync)(`bd show ${id} --json 2>/dev/null`, { encoding: 'utf8', maxBuffer: exports.BD_READ_MAX_BUFFER });
773
842
  const data = JSON.parse(raw);
774
843
  const issue = data[0] || data;
775
844
  return {
@@ -786,7 +855,7 @@ function getIssueInfo(id) {
786
855
  }
787
856
  function isIssueClosed(id) {
788
857
  try {
789
- const raw = (0, child_process_1.execSync)(`bd show ${id} --json 2>/dev/null`, { encoding: 'utf8' });
858
+ const raw = (0, child_process_1.execSync)(`bd show ${id} --json 2>/dev/null`, { encoding: 'utf8', maxBuffer: exports.BD_READ_MAX_BUFFER });
790
859
  const data = JSON.parse(raw);
791
860
  const status = (data[0] || data).status || 'unknown';
792
861
  return status === 'closed';
@@ -1300,7 +1369,27 @@ async function runLoop(options) {
1300
1369
  // rotation). Actual rotations are everything after the first — see the summary.
1301
1370
  const sessionStarts = [];
1302
1371
  while (true) {
1303
- const nextId = prefix ? findNextIssue(prefix, includeFeatures) : null;
1372
+ let nextId = null;
1373
+ if (prefix) {
1374
+ try {
1375
+ nextId = findNextIssue(prefix, includeFeatures);
1376
+ }
1377
+ catch (e) {
1378
+ // A failed read is not a drained queue. Treating the two alike is the
1379
+ // boop-fl22 bug: the loop printed "No more ready issues" and exited 0
1380
+ // while `bd ready` was still listing claimable work.
1381
+ console.error(`\x1b[31m[bd-loop]\x1b[0m ${e.message}`);
1382
+ if (poll) {
1383
+ console.error(`\x1b[31m[bd-loop]\x1b[0m Ready-queue read FAILED (not an empty queue); retrying in ${pollInterval}s.`);
1384
+ await sleep(pollInterval * 1000);
1385
+ continue;
1386
+ }
1387
+ console.error(`\x1b[31m[bd-loop]\x1b[0m Stopping: the ready queue could not be read, so work may be outstanding. ` +
1388
+ `This is NOT a clean finish.`);
1389
+ process.exitCode = 1;
1390
+ break;
1391
+ }
1392
+ }
1304
1393
  if (!nextId) {
1305
1394
  // Spec-driven safety net: the filter removes the workflow-owned feature
1306
1395
  // root from claimable work, so when its chain completes nobody may have