@feigi/fleet-ctl 3.93.1 → 3.93.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.93.1",
3
+ "version": "3.93.3",
4
4
  "description": "Agent fleet: run-team controller, merge bot, PR reviewer, and the ticket pipeline they share",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -460,25 +460,262 @@ export function foldOmpTranscript(jsonlText, filePath) {
460
460
 
461
461
  // The report files one transcript tool call writes, as { file, content } with
462
462
  // `file` the written path's basename: the `write` tool's arguments, or each
463
- // `cat > <path> <<'EOF'` heredoc a `bash` call carries. Only a path under a
464
- // `reports/` directory counts.
465
- const HEREDOC_RE = /cat\s*>\s*(\S+)\s*<<\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?[^\n]*\n([\s\S]*?)\n\2[ \t]*(?=\n|$)/g;
463
+ // report a `bash` call writes with a heredoc (`heredocReportWrites`). Only a
464
+ // path ending `/reports/<name>.json` counts.
466
465
  const REPORT_PATH_RE = /\/reports\/[^/\s]+\.json$/;
467
- function reportWritesOf(c) {
466
+ export function reportWritesOf(c) {
468
467
  if (c?.type !== "toolCall") return [];
469
468
  const a = c.arguments ?? {};
470
469
  if (c.name === "write" && typeof a.path === "string" && typeof a.content === "string") {
471
470
  return REPORT_PATH_RE.test(a.path) ? [{ file: basename(a.path), content: a.content }] : [];
472
471
  }
473
- if (c.name === "bash" && typeof a.command === "string") {
474
- const out = [];
475
- for (const m of a.command.matchAll(HEREDOC_RE)) {
476
- const path = m[1].replace(/^["']|["']$/g, "");
477
- if (REPORT_PATH_RE.test(path)) out.push({ file: basename(path), content: m[3] });
472
+ if (c.name === "bash" && typeof a.command === "string") return heredocReportWrites(a.command);
473
+ return [];
474
+ }
475
+
476
+ // One linear pass over a shell command, yielding { file, content } for each
477
+ // heredoc that writes a report, in command order:
478
+ // cat > P <<TAG cat <<TAG > P tee P [>/dev/null] <<TAG
479
+ // cat <<TAG | tee P
480
+ // with `<<` or `<<-` and a bare, 'single' or "double" quoted TAG. The command
481
+ // may follow leading keywords and the wrappers in WRAPPERS (`env FOO=1`,
482
+ // `sudo -u x`, `timeout 5`, …), and may sit inside a `$(…)` or be the string
483
+ // of a `bash -c` / `sh -c`. P is a literal path (quoted or not), a whole-word
484
+ // `$NAME` / `${NAME}` / `"$NAME"` that a `NAME=<value>` earlier in the same
485
+ // command set, or a word whose `/reports/<name>.json` tail is literal
486
+ // (`$D/reports/x.json`): the basename is all a report write is read for.
487
+ // The body is the lines up to the first terminator line, a terminator line
488
+ // being TAG with any trailing blanks or CR; under `<<-` every line is read
489
+ // with its leading tabs stripped. The body is otherwise verbatim (an unquoted
490
+ // TAG does not expand it). A heredoc whose stdin a later `<` or `<<<`
491
+ // replaces writes nothing. A heredoc with no terminator writes nothing — and
492
+ // since the shell would swallow the rest of the command into it, the scan
493
+ // stops there. Bodies are never scanned for commands.
494
+ const REDIR_RE = /&>>|&>|>>|>&|>\||>|<<<|<<-|<<|<&|</y;
495
+ const PARAM_RE = /\$(?:[A-Za-z_][A-Za-z0-9_]*|[0-9@*#?$!-]|\{[^}]*\}?)/y;
496
+ const WHOLE_PARAM_RE = /^"?\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})"?$/;
497
+ const ASSIGN_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
498
+ const DECLARE_COMMANDS = new Set(["export", "local", "declare", "readonly", "typeset"]);
499
+ const LEADING_KEYWORDS = new Set(["{", "!", "then", "do", "else", "exec", "command", "rtk", "time"]);
500
+ // Commands that run the command after them: the single-letter options of
501
+ // theirs that take a separate argument, and how many operands come before that
502
+ // command. `env` and `sudo` also take NAME=value words before it.
503
+ const WRAPPERS = new Map([
504
+ ["env", { args: "uCS", operands: 0 }],
505
+ ["sudo", { args: "ugpCUrtDRT", operands: 0 }],
506
+ ["timeout", { args: "sk", operands: 1 }],
507
+ ["nice", { args: "n", operands: 0 }],
508
+ ["stdbuf", { args: "ioe", operands: 0 }],
509
+ ["nohup", { args: "", operands: 0 }],
510
+ ]);
511
+ const SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
512
+ const WORD_BREAKS = " \t\r\n;&|<>()";
513
+ // `$(…)` and `-c` strings nested deeper than this are not scanned: the rest
514
+ // of the command reads as one word.
515
+ const MAX_NESTING = 32;
516
+
517
+ function heredocReportWrites(cmd) {
518
+ return scan(cmd, 0, false, 0).writes;
519
+ }
520
+
521
+ // The index of the command word in `words`, from `k` past leading keywords
522
+ // and wrappers with their options, operands and assignments.
523
+ function commandIndex(words, k) {
524
+ while (k < words.length) {
525
+ const t = words[k].text;
526
+ if (LEADING_KEYWORDS.has(t)) { k++; continue; }
527
+ const wrap = WRAPPERS.get(t);
528
+ if (!wrap) break;
529
+ k++;
530
+ while (k < words.length && words[k].text.startsWith("-")) {
531
+ const o = words[k++].text;
532
+ if (o === "--") break;
533
+ if (o.length === 2 && wrap.args.includes(o[1])) k++;
478
534
  }
479
- return out;
535
+ if (t === "env" || t === "sudo") while (k < words.length && ASSIGN_RE.test(words[k].raw)) k++;
536
+ k += wrap.operands;
480
537
  }
481
- return [];
538
+ return k;
539
+ }
540
+
541
+ // One shell word starting at `i`: its source text, its text with quotes
542
+ // removed, whether any `$` or backtick expansion is in it, where in `text` the
543
+ // literal tail after its last expansion starts, and the end index. A `$(…)` in
544
+ // it is scanned as a command of its own, whose report writes go to `writes`.
545
+ function scanWord(cmd, i, writes, depth) {
546
+ const n = cmd.length;
547
+ let j = i, text = "", dynamic = false, litFrom = 0;
548
+ const expansion = (e) => { text += cmd.slice(j, e); dynamic = true; litFrom = text.length; j = e; };
549
+ const substitution = () => {
550
+ if (depth >= MAX_NESTING) return expansion(n);
551
+ const sub = scan(cmd, j + 2, true, depth + 1);
552
+ for (const w of sub.writes) writes.push(w);
553
+ expansion(sub.end);
554
+ };
555
+ const parameter = () => {
556
+ PARAM_RE.lastIndex = j;
557
+ expansion(PARAM_RE.test(cmd) ? PARAM_RE.lastIndex : j + 1);
558
+ };
559
+ const backtick = () => {
560
+ const e = cmd.indexOf("`", j + 1);
561
+ expansion(e < 0 ? n : e + 1);
562
+ };
563
+ while (j < n) {
564
+ const ch = cmd[j];
565
+ if (ch === "$" && cmd[j + 1] === "(") substitution();
566
+ else if (ch === "$") parameter();
567
+ else if (ch === "`") backtick();
568
+ else if (ch === "\\") {
569
+ if (cmd[j + 1] === "\n") j += 2;
570
+ else { text += cmd[j + 1] ?? ""; j += 2; }
571
+ } else if (ch === "'") {
572
+ const e = cmd.indexOf("'", j + 1);
573
+ text += cmd.slice(j + 1, e < 0 ? n : e); j = e < 0 ? n : e + 1;
574
+ } else if (ch === '"') {
575
+ j++;
576
+ while (j < n && cmd[j] !== '"') {
577
+ if (cmd[j] === "\\" && j + 1 < n) { text += cmd[j + 1]; j += 2; }
578
+ else if (cmd[j] === "$" && cmd[j + 1] === "(") substitution();
579
+ else if (cmd[j] === "$") parameter();
580
+ else if (cmd[j] === "`") backtick();
581
+ else text += cmd[j++];
582
+ }
583
+ j++;
584
+ } else if (WORD_BREAKS.includes(ch)) break;
585
+ else { text += ch; j++; }
586
+ }
587
+ const end = Math.min(j, n);
588
+ return { raw: cmd.slice(i, end), text, dynamic, litFrom, end };
589
+ }
590
+
591
+ function stripTrailingBlanks(s) {
592
+ let e = s.length;
593
+ while (e > 0 && (s[e - 1] === " " || s[e - 1] === "\t" || s[e - 1] === "\r")) e--;
594
+ return s.slice(0, e);
595
+ }
596
+
597
+ function stripLeadingTabs(s) {
598
+ let b = 0;
599
+ while (b < s.length && s[b] === "\t") b++;
600
+ return s.slice(b);
601
+ }
602
+
603
+ const isReportPath = (t) => t !== null && REPORT_PATH_RE.test(t);
604
+ const isStdin = (r) => (r.fd === null || r.fd === "0") && (r.op === "<" || r.op === "<<<" || r.op === "<&" || r.op === "<<" || r.op === "<<-");
605
+
606
+ // The scan proper, from `start`. `nested` when `start` is just inside a `$(`,
607
+ // whose unmatched `)` then ends the scan. Returns the writes and the index
608
+ // the scan ended at (after that `)`).
609
+ function scan(cmd, start, nested, depth) {
610
+ const writes = [];
611
+ const vars = new Map();
612
+ const n = cmd.length;
613
+ let words = [], redirs = [], pending = [], expect = null, fd = null, piped = null, parens = 0, i = start;
614
+
615
+ // A word's value, when it is known enough to read a report path off: its
616
+ // text when it has no expansion, the value of an assigned NAME when it is
617
+ // exactly `$NAME` / `${NAME}` (quoted or not), its text when its report
618
+ // tail is literal, else null.
619
+ const resolve = (w) => {
620
+ if (!w.dynamic) return w.text;
621
+ const m = WHOLE_PARAM_RE.exec(w.raw);
622
+ if (m) return vars.get(m[1] ?? m[2]) ?? null;
623
+ return REPORT_PATH_RE.test(w.text.slice(w.litFrom)) ? w.text : null;
624
+ };
625
+ const assign = (w) => {
626
+ const name = w.raw.slice(0, w.raw.indexOf("="));
627
+ if (!w.dynamic || REPORT_PATH_RE.test(w.text.slice(w.litFrom))) vars.set(name, w.text.slice(name.length + 1));
628
+ else vars.delete(name);
629
+ };
630
+ const teeTargets = (args) => {
631
+ if (args.some((w) => w.text === "--append" || /^-[^-]*a/.test(w.text))) return [];
632
+ return args.filter((w) => !w.text.startsWith("-")).map(resolve).filter(isReportPath);
633
+ };
634
+
635
+ // One finished simple command, ended by `sep`: apply its assignments, scan
636
+ // its `-c` string, and queue its heredocs with the report paths the last of
637
+ // them writes. A plain `cat <<TAG` piped on hands its heredoc to a `tee`.
638
+ const flush = (sep) => {
639
+ expect = null;
640
+ const hd = redirs.filter((r) => r.op === "<<" || r.op === "<<-");
641
+ const stdin = redirs.findLast(isStdin);
642
+ const feeds = hd.length > 0 && stdin === hd.at(-1);
643
+ let k = 0;
644
+ while (k < words.length && ASSIGN_RE.test(words[k].raw)) k++;
645
+ const leading = k;
646
+ k = commandIndex(words, k);
647
+ const cmdWord = words[k];
648
+ const name = cmdWord && !cmdWord.dynamic ? cmdWord.text.split("/").pop() : null;
649
+ if (!cmdWord) {
650
+ for (const w of words.slice(0, leading)) assign(w);
651
+ } else if (DECLARE_COMMANDS.has(name)) {
652
+ for (const w of words.slice(k + 1)) if (ASSIGN_RE.test(w.raw)) assign(w);
653
+ } else if (SHELLS.has(name) && depth < MAX_NESTING) {
654
+ for (let x = k + 1; x < words.length && words[x].text.startsWith("-"); x++) {
655
+ if (!/^-[A-Za-z]*c[A-Za-z]*$/.test(words[x].text)) continue;
656
+ if (words[x + 1]) for (const w of scan(words[x + 1].text, 0, false, depth + 1).writes) writes.push(w);
657
+ break;
658
+ }
659
+ }
660
+ const plainCat = name === "cat" && words.length === k + 1;
661
+ const outs = redirs.filter((r) => (r.op === ">" || r.op === ">|" || r.op === "&>") && (r.fd === null || r.fd === "1"));
662
+ let targets = [];
663
+ if (feeds && plainCat && outs.length) targets = [resolve(outs.at(-1).word)].filter(isReportPath);
664
+ else if (feeds && name === "tee") targets = teeTargets(words.slice(k + 1));
665
+ else if (piped && name === "tee" && !stdin) piped.targets = teeTargets(words.slice(k + 1));
666
+ hd.forEach((r, idx) => pending.push({
667
+ tag: r.word.text, dash: r.op === "<<-", targets: idx === hd.length - 1 ? targets : [],
668
+ }));
669
+ piped = sep === "|" && feeds && plainCat && !outs.length ? pending.at(-1) : null;
670
+ words = []; redirs = [];
671
+ };
672
+
673
+ // Once a command line ends, read each queued heredoc's body in turn. False
674
+ // when one has no terminator.
675
+ const consume = () => {
676
+ for (const h of pending) {
677
+ const lines = [];
678
+ let found = false;
679
+ while (i < n && !found) {
680
+ const e = cmd.indexOf("\n", i);
681
+ const end = e < 0 ? n : e;
682
+ const line = h.dash ? stripLeadingTabs(cmd.slice(i, end)) : cmd.slice(i, end);
683
+ i = e < 0 ? n : e + 1;
684
+ if (stripTrailingBlanks(line) === h.tag) found = true;
685
+ else lines.push(line);
686
+ }
687
+ if (!found) return false;
688
+ const content = lines.join("\n");
689
+ for (const t of h.targets) writes.push({ file: basename(t), content });
690
+ }
691
+ pending = [];
692
+ return true;
693
+ };
694
+
695
+ while (i < n) {
696
+ const ch = cmd[i];
697
+ if (ch === "\n") { i++; flush(ch); if (!consume()) return { writes, end: n }; continue; }
698
+ if (ch === " " || ch === "\t" || ch === "\r") { i++; continue; }
699
+ if (ch === "\\" && cmd[i + 1] === "\n") { i += 2; continue; }
700
+ if (ch === "#") { const e = cmd.indexOf("\n", i); i = e < 0 ? n : e; continue; }
701
+ REDIR_RE.lastIndex = i;
702
+ const op = REDIR_RE.exec(cmd);
703
+ if (op) { expect = { op: op[0], fd }; fd = null; i += op[0].length; continue; }
704
+ if (ch === ")" && nested && parens === 0) { flush(ch); return { writes, end: i + 1 }; }
705
+ if (";&|()".includes(ch)) {
706
+ if (ch === "(") parens++;
707
+ else if (ch === ")" && parens > 0) parens--;
708
+ flush(ch); i++; continue;
709
+ }
710
+ const w = scanWord(cmd, i, writes, depth);
711
+ i = w.end;
712
+ // A file-descriptor prefix (`2>`), which the redirect that follows takes.
713
+ if (/^[0-9]{1,2}$/.test(w.raw) && (cmd[i] === "<" || cmd[i] === ">")) { fd = w.raw; continue; }
714
+ if (expect) { redirs.push({ ...expect, word: w }); expect = null; }
715
+ else words.push(w);
716
+ }
717
+ flush(null);
718
+ return { writes, end: n };
482
719
  }
483
720
 
484
721
  // The text a transcript message carries: a bare string, or its text blocks.