@hua-labs/tap 0.5.1 → 0.6.0

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 (47) hide show
  1. package/AI_GUIDE.md +165 -0
  2. package/CHANGELOG.md +67 -0
  3. package/README.md +201 -12
  4. package/dist/bridges/codex-app-server-auth-gateway.mjs +16 -1
  5. package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
  6. package/dist/bridges/codex-app-server-bridge.d.mts +105 -12
  7. package/dist/bridges/codex-app-server-bridge.mjs +3149 -251
  8. package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
  9. package/dist/bridges/codex-bridge-runner.d.mts +4 -1
  10. package/dist/bridges/codex-bridge-runner.mjs +512 -58
  11. package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
  12. package/dist/bridges/codex-remote-ipc-relay.d.mts +1 -0
  13. package/dist/bridges/codex-remote-ipc-relay.mjs +1912 -0
  14. package/dist/bridges/codex-remote-ipc-relay.mjs.map +1 -0
  15. package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -1
  16. package/dist/cli.mjs +30944 -8415
  17. package/dist/cli.mjs.map +1 -1
  18. package/dist/codex-a2a/index.d.mts +2 -0
  19. package/dist/codex-a2a/index.mjs +416 -0
  20. package/dist/codex-a2a/index.mjs.map +1 -0
  21. package/dist/codex-health/index.d.mts +76 -0
  22. package/dist/codex-health/index.mjs +153 -0
  23. package/dist/codex-health/index.mjs.map +1 -0
  24. package/dist/codex-ipc/index.d.mts +2 -0
  25. package/dist/codex-ipc/index.mjs +1834 -0
  26. package/dist/codex-ipc/index.mjs.map +1 -0
  27. package/dist/index-D4Khz2Mh.d.mts +206 -0
  28. package/dist/index-DMToLyGd.d.mts +256 -0
  29. package/dist/index.d.mts +763 -8
  30. package/dist/index.mjs +11600 -3449
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/mcp-server.mjs +8838 -811
  33. package/dist/mcp-server.mjs.map +1 -1
  34. package/dist/types-FWvKrFUt.d.mts +43 -0
  35. package/examples/01-logic-battle-known-broken.md +46 -0
  36. package/examples/02-cross-model-review-root-cause.md +37 -0
  37. package/examples/03-convergence-pattern.md +42 -0
  38. package/examples/04-tower-broadcast.md +41 -0
  39. package/examples/05-self-awareness-paradox.md +49 -0
  40. package/examples/06-session-resurrection.md +37 -0
  41. package/examples/07-ghost-agent.md +31 -0
  42. package/examples/08-naming-creates-identity.md +36 -0
  43. package/examples/09-ceo-as-middleware.md +52 -0
  44. package/examples/10-files-as-interface.md +67 -0
  45. package/examples/README.md +34 -0
  46. package/examples/tap-profile-pack.example.json +71 -0
  47. package/package.json +21 -3
@@ -148,6 +148,7 @@ var init_termination = __esm({
148
148
  import * as fs6 from "fs";
149
149
  import * as path5 from "path";
150
150
  import * as crypto2 from "crypto";
151
+ import { spawnSync } from "child_process";
151
152
  function trimAddress(value) {
152
153
  return value.trim();
153
154
  }
@@ -177,26 +178,70 @@ function extractPrNumber(text) {
177
178
  }
178
179
  return null;
179
180
  }
181
+ function computePendingRequestKey(request) {
182
+ return request.messageId ? `message:${request.messageId}` : `source:${request.sourcePath}`;
183
+ }
184
+ function parseInboxContentFrontmatter(content) {
185
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
186
+ if (!match?.[1]) return null;
187
+ const fields = {};
188
+ for (const line of match[1].split("\n")) {
189
+ const kv = line.match(/^(\w+):\s*(.+)$/);
190
+ if (kv?.[1] && kv[2]) fields[kv[1]] = kv[2].trim();
191
+ }
192
+ return {
193
+ sent_at: fields.sent_at,
194
+ message_id: fields.message_id,
195
+ to: fields.to
196
+ };
197
+ }
180
198
  function detectReviewRequest(filePath, content, generation) {
181
199
  const parsed = parseInboxFilename(filePath);
182
200
  if (!parsed) return null;
201
+ if (parsed.subject.startsWith("headless-dispatch-")) return null;
183
202
  const fullText = `${parsed.subject} ${content}`;
184
203
  const isReview = REVIEW_KEYWORDS.some((re) => re.test(fullText));
185
204
  const isReReview = REREVIEW_KEYWORDS.some((re) => re.test(fullText));
186
205
  if (!isReview && !isReReview) return null;
187
206
  const prNumber = extractPrNumber(fullText);
188
207
  if (!prNumber) return null;
208
+ const sourceMtimeMs = fs6.existsSync(filePath) ? fs6.statSync(filePath).mtimeMs : 0;
209
+ const fm = parseInboxContentFrontmatter(content);
189
210
  return {
190
211
  sourcePath: filePath,
212
+ sourceMtimeMs,
213
+ requestTimestampMs: extractRequestTimestampMs(
214
+ parsed.date,
215
+ fm,
216
+ sourceMtimeMs
217
+ ),
191
218
  sender: parsed.sender,
192
219
  recipient: parsed.recipient,
193
220
  prNumber,
194
221
  generation,
195
222
  isReReview,
196
- round: isReReview ? 2 : 1
223
+ round: isReReview ? 2 : 1,
197
224
  // Will be adjusted by session tracking
225
+ messageId: fm?.message_id || void 0,
226
+ dedupeRecipient: fm?.to || void 0
198
227
  };
199
228
  }
229
+ function extractRequestTimestampMs(inboxDate, fm, fallbackMtimeMs) {
230
+ if (fm?.sent_at) {
231
+ const sentAtMs = new Date(fm.sent_at).getTime();
232
+ if (Number.isFinite(sentAtMs) && sentAtMs > 0) return sentAtMs;
233
+ }
234
+ const dateMatch = inboxDate.match(/^(\d{4})(\d{2})(\d{2})$/);
235
+ if (dateMatch) {
236
+ const [, year, month, day] = dateMatch;
237
+ return Date.UTC(
238
+ Number.parseInt(year, 10),
239
+ Number.parseInt(month, 10) - 1,
240
+ Number.parseInt(day, 10)
241
+ );
242
+ }
243
+ return fallbackMtimeMs;
244
+ }
200
245
  function buildReviewPrompt(request, agentName, round) {
201
246
  const roundLabel = round > 1 ? ` (re-review round ${round})` : "";
202
247
  return [
@@ -311,17 +356,17 @@ function parseReviewOutput(reviewFilePath2, round) {
311
356
  findingHash: computeFindingHash(findings)
312
357
  };
313
358
  }
314
- function reviewFilePath(commsDir, generation, prNumber, agentName) {
359
+ function reviewFilePath(repoRoot, generation, prNumber, agentName) {
315
360
  return path5.join(
316
- commsDir,
361
+ repoRoot,
317
362
  "reviews",
318
363
  generation,
319
364
  `review-PR${prNumber}-${agentName}.md`
320
365
  );
321
366
  }
322
- function isStaleReviewRequest(request, commsDir, agentName) {
367
+ function isStaleReviewRequest(request, repoRoot, agentName) {
323
368
  const revPath = reviewFilePath(
324
- commsDir,
369
+ repoRoot,
325
370
  request.generation,
326
371
  request.prNumber,
327
372
  agentName
@@ -333,29 +378,87 @@ function isStaleReviewRequest(request, commsDir, agentName) {
333
378
  }
334
379
  return false;
335
380
  }
336
- function computeRequestMarkerId(filePath) {
337
- const stat = fs6.statSync(filePath);
338
- const input = `${filePath}|${stat.mtimeMs}`;
381
+ function resolvePrHead(repoRoot, request, cache) {
382
+ const cacheKey = request.messageId ? `message:${request.messageId}` : `source:${request.sourcePath}:mtime:${request.sourceMtimeMs}`;
383
+ const cached = cache.get(cacheKey);
384
+ if (cached && Date.now() - cached.checkedAtMs < PR_HEAD_CACHE_REVALIDATE_MS) {
385
+ return cached.value;
386
+ }
387
+ let result = null;
388
+ try {
389
+ const command = spawnSync(
390
+ "gh",
391
+ [
392
+ "pr",
393
+ "view",
394
+ String(request.prNumber),
395
+ "--json",
396
+ "headRefName,headRefOid"
397
+ ],
398
+ { cwd: repoRoot, encoding: "utf-8", timeout: 1e4 }
399
+ );
400
+ if (command.status === 0 && command.stdout.trim()) {
401
+ const parsed = JSON.parse(command.stdout);
402
+ result = {
403
+ headRefName: parsed.headRefName,
404
+ headRefOid: parsed.headRefOid
405
+ };
406
+ }
407
+ } catch {
408
+ result = null;
409
+ }
410
+ cache.set(cacheKey, {
411
+ value: result,
412
+ checkedAtMs: Date.now()
413
+ });
414
+ return result;
415
+ }
416
+ function computeRequestMarkerId(request) {
417
+ const recipient = request.dedupeRecipient || request.recipient;
418
+ if (request.prTipSha) {
419
+ return crypto2.createHash("sha1").update(
420
+ `pr:${request.prNumber}:tip:${request.prTipSha}:recipient:${recipient}`
421
+ ).digest("hex");
422
+ }
423
+ if (request.messageId) {
424
+ return crypto2.createHash("sha1").update(`message_id:${request.messageId}:recipient:${recipient}`).digest("hex");
425
+ }
426
+ let contentHash = "";
427
+ try {
428
+ const content = fs6.readFileSync(request.sourcePath, "utf-8");
429
+ contentHash = crypto2.createHash("sha1").update(content).digest("hex");
430
+ } catch {
431
+ }
432
+ const input = JSON.stringify({
433
+ sourcePath: request.sourcePath,
434
+ sender: request.sender,
435
+ recipient: request.recipient,
436
+ prNumber: request.prNumber,
437
+ generation: request.generation,
438
+ isReReview: request.isReReview,
439
+ contentHash
440
+ });
339
441
  return crypto2.createHash("sha1").update(input).digest("hex");
340
442
  }
341
- function isAlreadyProcessed(stateDir, filePath) {
342
- const markerId = computeRequestMarkerId(filePath);
443
+ function isAlreadyProcessed(stateDir, request) {
444
+ const markerId = computeRequestMarkerId(request);
343
445
  return fs6.existsSync(path5.join(stateDir, "processed", `${markerId}.done`));
344
446
  }
345
447
  function unmarkProcessed(stateDir, request) {
346
- const markerId = computeRequestMarkerId(request.sourcePath);
448
+ const markerId = computeRequestMarkerId(request);
347
449
  const markerPath = path5.join(stateDir, "processed", `${markerId}.done`);
348
450
  if (fs6.existsSync(markerPath)) {
349
451
  fs6.unlinkSync(markerPath);
350
452
  }
351
453
  }
352
454
  function markAsProcessed(stateDir, request) {
353
- const markerId = computeRequestMarkerId(request.sourcePath);
455
+ const markerId = computeRequestMarkerId(request);
354
456
  const markerDir = path5.join(stateDir, "processed");
355
457
  fs6.mkdirSync(markerDir, { recursive: true });
356
458
  const markerPath = path5.join(markerDir, `${markerId}.done`);
357
459
  const payload = {
358
460
  prNumber: request.prNumber,
461
+ prTipSha: request.prTipSha ?? null,
359
462
  sourcePath: request.sourcePath,
360
463
  processedAt: (/* @__PURE__ */ new Date()).toISOString()
361
464
  };
@@ -392,11 +495,13 @@ function getHeadlessEnvConfig() {
392
495
  qualityFloor: process.env.TAP_QUALITY_FLOOR ?? "high"
393
496
  };
394
497
  }
395
- function scanInboxForReviews(commsDir, stateDir, generation, agentName, agentId = agentName) {
498
+ function scanInboxForReviews(commsDir, stateDir, repoRoot, generation, agentName, agentId = agentName, activeSessionPrNumber, prHeadCache) {
396
499
  const inboxDir = path5.join(commsDir, "inbox");
397
500
  if (!fs6.existsSync(inboxDir)) return [];
398
501
  const files = fs6.readdirSync(inboxDir).filter((f) => f.endsWith(".md"));
399
502
  const requests = [];
503
+ const shouldResolvePrHead = fs6.existsSync(path5.join(repoRoot, ".git"));
504
+ const activePrHeadCache = shouldResolvePrHead ? prHeadCache ?? /* @__PURE__ */ new Map() : null;
400
505
  for (const file of files) {
401
506
  const filePath = path5.join(inboxDir, file);
402
507
  const content = fs6.readFileSync(filePath, "utf-8");
@@ -407,13 +512,34 @@ function scanInboxForReviews(commsDir, stateDir, generation, agentName, agentId
407
512
  continue;
408
513
  }
409
514
  if (isOwnMessageAddress(request.sender, agentId, agentName)) continue;
410
- if (isStaleReviewRequest(request, commsDir, agentName)) continue;
411
- if (isAlreadyProcessed(stateDir, filePath)) continue;
515
+ if (activePrHeadCache) {
516
+ const prHead = resolvePrHead(repoRoot, request, activePrHeadCache);
517
+ if (prHead?.headRefName) request.branch = prHead.headRefName;
518
+ if (prHead?.headRefOid) request.prTipSha = prHead.headRefOid;
519
+ }
520
+ const bypassProcessedCheck = request.isReReview && activeSessionPrNumber != null && request.prNumber === activeSessionPrNumber;
521
+ const bypassStaleCheck = request.isReReview && activeSessionPrNumber != null && request.prNumber === activeSessionPrNumber;
522
+ if (!bypassStaleCheck && isStaleReviewRequest(request, repoRoot, agentName))
523
+ continue;
524
+ if (!bypassProcessedCheck && isAlreadyProcessed(stateDir, request))
525
+ continue;
412
526
  requests.push(request);
413
527
  }
528
+ requests.sort((a, b) => {
529
+ if (a.isReReview !== b.isReReview) {
530
+ return Number(b.isReReview) - Number(a.isReReview);
531
+ }
532
+ if (a.requestTimestampMs !== b.requestTimestampMs) {
533
+ return b.requestTimestampMs - a.requestTimestampMs;
534
+ }
535
+ if (a.sourceMtimeMs !== b.sourceMtimeMs) {
536
+ return b.sourceMtimeMs - a.sourceMtimeMs;
537
+ }
538
+ return b.prNumber - a.prNumber;
539
+ });
414
540
  return requests;
415
541
  }
416
- var REVIEW_KEYWORDS, REREVIEW_KEYWORDS, PR_NUMBER_PATTERNS, SEVERITY_PATTERNS, CATEGORY_PATTERNS;
542
+ var REVIEW_KEYWORDS, REREVIEW_KEYWORDS, PR_NUMBER_PATTERNS, PR_HEAD_CACHE_REVALIDATE_MS, SEVERITY_PATTERNS, CATEGORY_PATTERNS;
417
543
  var init_review = __esm({
418
544
  "src/engine/review.ts"() {
419
545
  "use strict";
@@ -425,6 +551,7 @@ var init_review = __esm({
425
551
  /pull\/(\d+)/,
426
552
  /review[-_ ]?(\d+)/i
427
553
  ];
554
+ PR_HEAD_CACHE_REVALIDATE_MS = 3e4;
428
555
  SEVERITY_PATTERNS = {
429
556
  critical: /\bcritical\b/i,
430
557
  high: /\bhigh\b/i,
@@ -447,10 +574,37 @@ var init_review = __esm({
447
574
  // src/engine/headless-loop.ts
448
575
  var headless_loop_exports = {};
449
576
  __export(headless_loop_exports, {
450
- createHeadlessLoop: () => createHeadlessLoop
577
+ createHeadlessLoop: () => createHeadlessLoop,
578
+ mergePendingRequests: () => mergePendingRequests,
579
+ sortRequests: () => sortRequests
451
580
  });
452
581
  import * as fs7 from "fs";
453
582
  import * as path6 from "path";
583
+ function sortRequests(requests, consecutiveReReviews) {
584
+ const reReviewQuotaExhausted = consecutiveReReviews >= MAX_CONSECUTIVE_REREVIEWS;
585
+ return [...requests].sort((a, b) => {
586
+ if (a.isReReview !== b.isReReview) {
587
+ if (reReviewQuotaExhausted) {
588
+ return Number(a.isReReview) - Number(b.isReReview);
589
+ }
590
+ return Number(b.isReReview) - Number(a.isReReview);
591
+ }
592
+ if (a.requestTimestampMs !== b.requestTimestampMs) {
593
+ return b.requestTimestampMs - a.requestTimestampMs;
594
+ }
595
+ if (a.sourceMtimeMs !== b.sourceMtimeMs) {
596
+ return b.sourceMtimeMs - a.sourceMtimeMs;
597
+ }
598
+ return b.prNumber - a.prNumber;
599
+ });
600
+ }
601
+ function mergePendingRequests(queued, scanned, consecutiveReReviews) {
602
+ const merged = /* @__PURE__ */ new Map();
603
+ for (const request of [...queued, ...scanned]) {
604
+ merged.set(computePendingRequestKey(request), request);
605
+ }
606
+ return sortRequests([...merged.values()], consecutiveReReviews);
607
+ }
454
608
  function createHeadlessLoop(options) {
455
609
  const envConfig = getHeadlessEnvConfig();
456
610
  const terminationConfig = {
@@ -461,14 +615,143 @@ function createHeadlessLoop(options) {
461
615
  const state = {
462
616
  running: false,
463
617
  activeSession: null,
618
+ pendingRequests: [],
464
619
  completedSessions: 0,
465
620
  lastPollAt: null
466
621
  };
622
+ let sessionTimeouts = 0;
623
+ let roundTimeouts = 0;
624
+ let consecutiveReReviews = 0;
625
+ let lastCompletionAt = null;
626
+ const PROCESSED_MARKER_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
627
+ const GC_INTERVAL_MS = 60 * 60 * 1e3;
628
+ let lastGcAt = 0;
467
629
  let timer = null;
630
+ const prHeadCache = /* @__PURE__ */ new Map();
631
+ let watcher = null;
632
+ let watchRestartTimer = null;
633
+ const WATCH_DEBOUNCE_MS = 150;
634
+ const WATCH_RESTART_MS = 2e3;
635
+ let lastWatchWakeMs = 0;
636
+ function startInboxWatcher() {
637
+ disposeInboxWatcher();
638
+ const inboxDir = path6.join(options.commsDir, "inbox");
639
+ if (!fs7.existsSync(inboxDir)) {
640
+ fs7.mkdirSync(inboxDir, { recursive: true });
641
+ }
642
+ try {
643
+ watcher = fs7.watch(inboxDir, (eventType, filename) => {
644
+ if (!filename || !filename.endsWith(".md")) return;
645
+ if (filename.includes("headless-dispatch-")) return;
646
+ const now = Date.now();
647
+ if (now - lastWatchWakeMs < WATCH_DEBOUNCE_MS) return;
648
+ lastWatchWakeMs = now;
649
+ log2(`fs.watch wake: ${eventType} ${filename}`);
650
+ pollOnce();
651
+ });
652
+ watcher.on("error", (error) => {
653
+ log2(
654
+ `fs.watch error: ${error instanceof Error ? error.message : String(error)}`
655
+ );
656
+ scheduleWatchRestart("error");
657
+ });
658
+ watcher.on("close", () => {
659
+ scheduleWatchRestart("close");
660
+ });
661
+ log2("fs.watch active on inbox");
662
+ } catch (error) {
663
+ log2(
664
+ `fs.watch start failed: ${error instanceof Error ? error.message : String(error)}`
665
+ );
666
+ scheduleWatchRestart("start-failed");
667
+ }
668
+ }
669
+ function disposeInboxWatcher() {
670
+ if (!watcher) return;
671
+ watcher.removeAllListeners();
672
+ try {
673
+ watcher.close();
674
+ } catch {
675
+ }
676
+ watcher = null;
677
+ }
678
+ function scheduleWatchRestart(reason) {
679
+ if (watchRestartTimer) return;
680
+ log2(`fs.watch restart scheduled (${reason})`);
681
+ watchRestartTimer = setTimeout(() => {
682
+ watchRestartTimer = null;
683
+ if (!state.running) return;
684
+ startInboxWatcher();
685
+ }, WATCH_RESTART_MS);
686
+ if (watchRestartTimer.unref) watchRestartTimer.unref();
687
+ }
468
688
  function log2(msg) {
469
689
  const ts = (/* @__PURE__ */ new Date()).toISOString();
470
690
  console.error(`[${ts}] [headless-loop] ${msg}`);
471
691
  }
692
+ function dispatchSender() {
693
+ return "headless";
694
+ }
695
+ function dispatchSubject(prNumber, round) {
696
+ return round == null ? `headless-dispatch-pr${prNumber}` : `headless-dispatch-pr${prNumber}-r${round}`;
697
+ }
698
+ function dispatchFilename(prNumber, round) {
699
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
700
+ return `${date}-${dispatchSender()}-${options.agentName}-${dispatchSubject(prNumber, round)}.md`;
701
+ }
702
+ function dispatchFileMatch(prNumber) {
703
+ return `-${dispatchSender()}-${options.agentName}-headless-dispatch-pr${prNumber}`;
704
+ }
705
+ function computeOldestPendingMs() {
706
+ if (state.pendingRequests.length === 0) return null;
707
+ const oldest = Math.min(
708
+ ...state.pendingRequests.map((r) => r.requestTimestampMs)
709
+ );
710
+ return Date.now() - oldest;
711
+ }
712
+ function computeActiveSessionAgeMs() {
713
+ if (!state.activeSession) return null;
714
+ return Date.now() - new Date(state.activeSession.startedAt).getTime();
715
+ }
716
+ function countProcessedMarkers() {
717
+ const markerDir = path6.join(options.stateDir, "processed");
718
+ if (!fs7.existsSync(markerDir)) return 0;
719
+ try {
720
+ return fs7.readdirSync(markerDir).filter((f) => f.endsWith(".done")).length;
721
+ } catch {
722
+ return 0;
723
+ }
724
+ }
725
+ function maybeGcProcessedMarkers() {
726
+ const now = Date.now();
727
+ if (now - lastGcAt < GC_INTERVAL_MS) return;
728
+ lastGcAt = now;
729
+ gcProcessedMarkers();
730
+ }
731
+ function gcProcessedMarkers() {
732
+ const markerDir = path6.join(options.stateDir, "processed");
733
+ if (!fs7.existsSync(markerDir)) return 0;
734
+ const now = Date.now();
735
+ let removed = 0;
736
+ try {
737
+ for (const file of fs7.readdirSync(markerDir)) {
738
+ if (!file.endsWith(".done")) continue;
739
+ const filePath = path6.join(markerDir, file);
740
+ try {
741
+ const age = now - fs7.statSync(filePath).mtimeMs;
742
+ if (age > PROCESSED_MARKER_MAX_AGE_MS) {
743
+ fs7.unlinkSync(filePath);
744
+ removed++;
745
+ }
746
+ } catch {
747
+ }
748
+ }
749
+ } catch {
750
+ }
751
+ if (removed > 0)
752
+ log2(`GC: removed ${removed} stale processed markers (>7d)`);
753
+ return removed;
754
+ }
472
755
  function writeStateFile() {
473
756
  try {
474
757
  const payload = {
@@ -478,6 +761,13 @@ function createHeadlessLoop(options) {
478
761
  pollIntervalMs: options.pollIntervalMs,
479
762
  completedSessions: state.completedSessions,
480
763
  lastPollAt: state.lastPollAt,
764
+ pendingReviewCount: state.pendingRequests.length,
765
+ pendingReviews: state.pendingRequests.map((request) => ({
766
+ prNumber: request.prNumber,
767
+ sender: request.sender,
768
+ isReReview: request.isReReview,
769
+ round: request.round
770
+ })),
481
771
  activeReview: state.activeSession ? {
482
772
  prNumber: state.activeSession.request.prNumber,
483
773
  round: state.activeSession.rounds.length + 1,
@@ -488,6 +778,16 @@ function createHeadlessLoop(options) {
488
778
  maxRounds: terminationConfig.maxRounds,
489
779
  qualitySeverityFloor: terminationConfig.qualitySeverityFloor
490
780
  },
781
+ // M326: Operational metrics for queue health monitoring
782
+ metrics: {
783
+ oldestPendingMs: computeOldestPendingMs(),
784
+ activeSessionAgeMs: computeActiveSessionAgeMs(),
785
+ lastCompletionAt,
786
+ sessionTimeouts,
787
+ roundTimeouts,
788
+ consecutiveReReviews,
789
+ processedMarkerCount: countProcessedMarkers()
790
+ },
491
791
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
492
792
  };
493
793
  const filePath = path6.join(options.stateDir, "headless-state.json");
@@ -497,25 +797,55 @@ function createHeadlessLoop(options) {
497
797
  } catch {
498
798
  }
499
799
  }
800
+ function requestStillEligible(request) {
801
+ if (!fs7.existsSync(request.sourcePath)) return false;
802
+ const bypassProcessedCheck = request.isReReview && state.activeSession?.request.prNumber === request.prNumber;
803
+ if (!bypassProcessedCheck && isAlreadyProcessed(options.stateDir, request))
804
+ return false;
805
+ const bypassStaleCheck = request.isReReview && state.activeSession?.request.prNumber === request.prNumber;
806
+ if (!bypassStaleCheck && isStaleReviewRequest(request, options.repoRoot, options.agentName))
807
+ return false;
808
+ return true;
809
+ }
810
+ function refreshPendingQueue() {
811
+ const queued = state.pendingRequests.filter(requestStillEligible);
812
+ const scanned = scanInboxForReviews(
813
+ options.commsDir,
814
+ options.stateDir,
815
+ options.repoRoot,
816
+ options.generation,
817
+ options.agentName,
818
+ options.agentId,
819
+ state.activeSession?.request.prNumber ?? null,
820
+ prHeadCache
821
+ );
822
+ state.pendingRequests = mergePendingRequests(
823
+ queued,
824
+ scanned,
825
+ consecutiveReReviews
826
+ );
827
+ }
500
828
  function pollOnce() {
829
+ if (!state.running) return;
501
830
  state.lastPollAt = (/* @__PURE__ */ new Date()).toISOString();
831
+ maybeGcProcessedMarkers();
832
+ refreshPendingQueue();
502
833
  if (state.activeSession) {
503
834
  checkActiveSession();
835
+ if (state.activeSession) {
836
+ writeStateFile();
837
+ return;
838
+ }
839
+ }
840
+ if (state.pendingRequests.length === 0) {
504
841
  writeStateFile();
505
842
  return;
506
843
  }
507
- const requests = scanInboxForReviews(
508
- options.commsDir,
509
- options.stateDir,
510
- options.generation,
511
- options.agentName,
512
- options.agentId
513
- );
514
- if (requests.length === 0) {
844
+ const request = state.pendingRequests.shift();
845
+ if (!request) {
515
846
  writeStateFile();
516
847
  return;
517
848
  }
518
- const request = requests[0];
519
849
  startReviewSession(request);
520
850
  writeStateFile();
521
851
  }
@@ -525,11 +855,12 @@ function createHeadlessLoop(options) {
525
855
  try {
526
856
  writeReviewReceipt(options.commsDir, request, options.agentName);
527
857
  const prompt = buildReviewPrompt(request, options.agentName, 1);
528
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
529
- const dispatchFilename = `${date}-headless-${options.agentName}-review-PR${request.prNumber}.md`;
530
858
  const inboxDir = path6.join(options.commsDir, "inbox");
531
859
  fs7.mkdirSync(inboxDir, { recursive: true });
532
- const dispatchFile = path6.join(inboxDir, dispatchFilename);
860
+ const dispatchFile = path6.join(
861
+ inboxDir,
862
+ dispatchFilename(request.prNumber)
863
+ );
533
864
  const tmp = `${dispatchFile}.tmp.${process.pid}`;
534
865
  fs7.writeFileSync(tmp, prompt, "utf-8");
535
866
  fs7.renameSync(tmp, dispatchFile);
@@ -540,7 +871,7 @@ function createHeadlessLoop(options) {
540
871
  rounds: [],
541
872
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
542
873
  reviewFilePath: reviewFilePath(
543
- options.commsDir,
874
+ options.repoRoot,
544
875
  request.generation,
545
876
  request.prNumber,
546
877
  options.agentName
@@ -600,6 +931,7 @@ function createHeadlessLoop(options) {
600
931
  log2(
601
932
  `PR #${session.request.prNumber} timed out \u2014 no output after ${Math.round(elapsed / 6e4)}min. Releasing session.`
602
933
  );
934
+ sessionTimeouts++;
603
935
  state.activeSession = null;
604
936
  return;
605
937
  }
@@ -612,6 +944,7 @@ function createHeadlessLoop(options) {
612
944
  log2(
613
945
  `PR #${session.request.prNumber} round timeout \u2014 no new output after ${Math.round((Date.now() - lastRoundTime) / 6e4)}min. Completing session.`
614
946
  );
947
+ roundTimeouts++;
615
948
  completeSession(session);
616
949
  return;
617
950
  }
@@ -619,20 +952,27 @@ function createHeadlessLoop(options) {
619
952
  }
620
953
  function dispatchFollowUp(session, round) {
621
954
  const prompt = buildReviewPrompt(session.request, options.agentName, round);
622
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
623
- const dispatchFilename = `${date}-headless-${options.agentName}-review-PR${session.request.prNumber}-r${round}.md`;
624
955
  const inboxDir = path6.join(options.commsDir, "inbox");
625
956
  fs7.mkdirSync(inboxDir, { recursive: true });
626
- const dispatchFile = path6.join(inboxDir, dispatchFilename);
957
+ const dispatchFile = path6.join(
958
+ inboxDir,
959
+ dispatchFilename(session.request.prNumber, round)
960
+ );
627
961
  const tmp = `${dispatchFile}.tmp.${process.pid}`;
628
962
  fs7.writeFileSync(tmp, prompt, "utf-8");
629
963
  fs7.renameSync(tmp, dispatchFile);
630
964
  }
631
965
  function completeSession(session) {
632
966
  session.terminatedAt = (/* @__PURE__ */ new Date()).toISOString();
967
+ lastCompletionAt = session.terminatedAt;
968
+ if (session.request.isReReview) {
969
+ consecutiveReReviews++;
970
+ } else {
971
+ consecutiveReReviews = 0;
972
+ }
633
973
  const inboxDir = path6.join(options.commsDir, "inbox");
634
974
  if (fs7.existsSync(inboxDir)) {
635
- const prefix = `headless-${options.agentName}-review-PR${session.request.prNumber}`;
975
+ const prefix = dispatchFileMatch(session.request.prNumber);
636
976
  const files = fs7.readdirSync(inboxDir).filter((f) => f.includes(prefix));
637
977
  for (const f of files) {
638
978
  fs7.unlinkSync(path6.join(inboxDir, f));
@@ -654,8 +994,11 @@ function createHeadlessLoop(options) {
654
994
  log2(
655
995
  `Headless review loop started (${envConfig?.role ?? "reviewer"}, poll ${options.pollIntervalMs}ms, max ${terminationConfig.maxRounds} rounds)`
656
996
  );
997
+ gcProcessedMarkers();
998
+ lastGcAt = Date.now();
657
999
  writeStateFile();
658
1000
  pollOnce();
1001
+ startInboxWatcher();
659
1002
  timer = setInterval(pollOnce, options.pollIntervalMs);
660
1003
  },
661
1004
  stop() {
@@ -664,6 +1007,11 @@ function createHeadlessLoop(options) {
664
1007
  clearInterval(timer);
665
1008
  timer = null;
666
1009
  }
1010
+ disposeInboxWatcher();
1011
+ if (watchRestartTimer) {
1012
+ clearTimeout(watchRestartTimer);
1013
+ watchRestartTimer = null;
1014
+ }
667
1015
  writeStateFile();
668
1016
  log2("Headless review loop stopped");
669
1017
  },
@@ -672,11 +1020,13 @@ function createHeadlessLoop(options) {
672
1020
  }
673
1021
  };
674
1022
  }
1023
+ var MAX_CONSECUTIVE_REREVIEWS;
675
1024
  var init_headless_loop = __esm({
676
1025
  "src/engine/headless-loop.ts"() {
677
1026
  "use strict";
678
1027
  init_review();
679
1028
  init_termination();
1029
+ MAX_CONSECUTIVE_REREVIEWS = 3;
680
1030
  }
681
1031
  });
682
1032
 
@@ -693,6 +1043,19 @@ import * as path2 from "path";
693
1043
  // src/utils.ts
694
1044
  import * as fs from "fs";
695
1045
  import * as path from "path";
1046
+ function normalizeTapPath(input, platform = process.platform) {
1047
+ const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
1048
+ if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
1049
+ return trimmed;
1050
+ }
1051
+ if (platform === "win32") {
1052
+ const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
1053
+ if (match) {
1054
+ return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
1055
+ }
1056
+ }
1057
+ return trimmed;
1058
+ }
696
1059
  var _noGitWarned = false;
697
1060
  function _setNoGitWarned() {
698
1061
  _noGitWarned = true;
@@ -775,7 +1138,9 @@ function resolveConfig(overrides = {}, startDir) {
775
1138
  stateDir: "auto",
776
1139
  runtimeCommand: "auto",
777
1140
  appServerUrl: "auto",
778
- towerName: "auto"
1141
+ towerName: "auto",
1142
+ remoteAgents: "auto",
1143
+ portMap: "auto"
779
1144
  };
780
1145
  let commsDir;
781
1146
  if (overrides.commsDir) {
@@ -845,6 +1210,19 @@ function resolveConfig(overrides = {}, startDir) {
845
1210
  appServerUrl = DEFAULT_APP_SERVER_URL;
846
1211
  }
847
1212
  const towerName = local.towerName ?? shared.towerName ?? null;
1213
+ const remoteAgents = [
1214
+ ...shared.remoteAgents ?? [],
1215
+ ...local.remoteAgents ?? []
1216
+ ];
1217
+ const portMap = {};
1218
+ if (shared.portMap) {
1219
+ Object.assign(portMap, shared.portMap);
1220
+ sources.portMap = "shared-config";
1221
+ }
1222
+ if (local.portMap) {
1223
+ Object.assign(portMap, local.portMap);
1224
+ sources.portMap = "local-config";
1225
+ }
848
1226
  return {
849
1227
  config: {
850
1228
  repoRoot,
@@ -852,7 +1230,9 @@ function resolveConfig(overrides = {}, startDir) {
852
1230
  stateDir,
853
1231
  runtimeCommand,
854
1232
  appServerUrl,
855
- towerName
1233
+ towerName,
1234
+ remoteAgents,
1235
+ portMap
856
1236
  },
857
1237
  sources
858
1238
  };
@@ -861,19 +1241,6 @@ function resolvePath(repoRoot, p) {
861
1241
  const normalized = normalizeTapPath(p);
862
1242
  return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
863
1243
  }
864
- function normalizeTapPath(input) {
865
- const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
866
- if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
867
- return trimmed;
868
- }
869
- if (process.platform === "win32") {
870
- const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
871
- if (match) {
872
- return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
873
- }
874
- }
875
- return trimmed;
876
- }
877
1244
 
878
1245
  // src/runtime/resolve-node.ts
879
1246
  import * as fs3 from "fs";
@@ -1053,12 +1420,21 @@ function resolveAppServerUrl(baseUrl, port) {
1053
1420
  }
1054
1421
 
1055
1422
  // src/bridges/codex-bridge-runner.ts
1056
- function findRepoRootFromRunner() {
1057
- let dir = path7.resolve(path7.dirname(fileURLToPath(import.meta.url)));
1423
+ function resolveRepoRootHintFromRunner(runnerUrl = import.meta.url, env = process.env, fileExists = fs8.existsSync) {
1424
+ const envRepoRoot = env.TAP_REPO_ROOT?.trim();
1425
+ if (envRepoRoot) {
1426
+ return path7.resolve(envRepoRoot);
1427
+ }
1428
+ let dir = path7.resolve(path7.dirname(fileURLToPath(runnerUrl)));
1058
1429
  while (true) {
1059
- if (fs8.existsSync(path7.join(dir, SHARED_CONFIG_FILE))) return dir;
1060
- if (fs8.existsSync(path7.join(dir, LOCAL_CONFIG_FILE))) return dir;
1061
- if (fs8.existsSync(path7.join(dir, "scripts", "codex-app-server-bridge.ts")))
1430
+ if (fileExists(path7.join(dir, SHARED_CONFIG_FILE))) return dir;
1431
+ if (fileExists(path7.join(dir, LOCAL_CONFIG_FILE))) return dir;
1432
+ if (fileExists(
1433
+ path7.join(dir, "scripts", "codex", "codex-app-server-bridge.ts")
1434
+ )) {
1435
+ return dir;
1436
+ }
1437
+ if (fileExists(path7.join(dir, "scripts", "codex-app-server-bridge.ts")))
1062
1438
  return dir;
1063
1439
  const parent = path7.dirname(dir);
1064
1440
  if (parent === dir) return null;
@@ -1070,7 +1446,7 @@ function maybeStartHeadlessLoop(repoRoot, commsDir, stateDir) {
1070
1446
  Promise.resolve().then(() => (init_headless_loop(), headless_loop_exports)).then(({ createHeadlessLoop: createHeadlessLoop2 }) => {
1071
1447
  const agentName = process.env.TAP_AGENT_NAME ?? process.env.CODEX_TAP_AGENT_NAME ?? "reviewer";
1072
1448
  const agentId = process.env.TAP_AGENT_ID ?? process.env.TAP_BRIDGE_INSTANCE_ID ?? agentName;
1073
- const generation = process.env.TAP_REVIEW_GENERATION ?? "gen11";
1449
+ const generation = resolveHeadlessReviewGeneration(repoRoot, commsDir);
1074
1450
  const resolvedStateDir = stateDir ?? path7.join(repoRoot, ".tap-comms");
1075
1451
  const loop = createHeadlessLoop2({
1076
1452
  commsDir,
@@ -1089,6 +1465,45 @@ function maybeStartHeadlessLoop(repoRoot, commsDir, stateDir) {
1089
1465
  console.error("[headless-loop] Failed to start:", err);
1090
1466
  });
1091
1467
  }
1468
+ function resolveHeadlessReviewGeneration(repoRoot, commsDir, env = process.env) {
1469
+ const explicit = env.TAP_REVIEW_GENERATION?.trim();
1470
+ if (explicit) return explicit;
1471
+ const envGeneration = normalizeGenerationValue(env.TAP_GENERATION);
1472
+ if (envGeneration) return envGeneration;
1473
+ try {
1474
+ const reviewsDir = path7.join(repoRoot, "reviews");
1475
+ const generations = readGenerationNumbers(reviewsDir);
1476
+ if (generations.length > 0) {
1477
+ return `gen${generations[0]}`;
1478
+ }
1479
+ } catch {
1480
+ }
1481
+ const resolvedCommsDir = commsDir?.trim() || env.TAP_COMMS_DIR?.trim() || null;
1482
+ if (resolvedCommsDir) {
1483
+ const commsGenerations = [
1484
+ ...readGenerationNumbers(path7.join(resolvedCommsDir, "retros")),
1485
+ ...readGenerationNumbers(path7.join(resolvedCommsDir, "letters"))
1486
+ ].sort((a, b) => b - a);
1487
+ if (commsGenerations.length > 0) {
1488
+ return `gen${commsGenerations[0]}`;
1489
+ }
1490
+ }
1491
+ return "gen1";
1492
+ }
1493
+ function normalizeGenerationValue(value) {
1494
+ const trimmed = value?.trim();
1495
+ if (!trimmed) return null;
1496
+ const match = trimmed.match(/^gen(\d+)$/i) ?? trimmed.match(/^(\d+)$/);
1497
+ if (!match?.[1]) return null;
1498
+ return `gen${Number.parseInt(match[1], 10)}`;
1499
+ }
1500
+ function readGenerationNumbers(dir) {
1501
+ try {
1502
+ return fs8.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => normalizeGenerationValue(entry.name)).filter((value) => Boolean(value)).map((value) => Number.parseInt(value.slice(3), 10)).filter(Number.isFinite).sort((a, b) => b - a);
1503
+ } catch {
1504
+ return [];
1505
+ }
1506
+ }
1092
1507
  function resolveBridgeDaemonScript(repoRoot, runnerUrl = import.meta.url, fileExists = fs8.existsSync) {
1093
1508
  const moduleDir = path7.dirname(fileURLToPath(runnerUrl));
1094
1509
  const candidates = [
@@ -1114,7 +1529,9 @@ function resolveBridgeDaemonScript(repoRoot, runnerUrl = import.meta.url, fileEx
1114
1529
  "bridges",
1115
1530
  "codex-app-server-bridge.ts"
1116
1531
  ),
1117
- // 5. Legacy monorepo root script
1532
+ // 5. Monorepo scripts/codex/ subfolder
1533
+ path7.join(repoRoot, "scripts", "codex", "codex-app-server-bridge.ts"),
1534
+ // 6. Legacy monorepo root script (pre-cleanup)
1118
1535
  path7.join(repoRoot, "scripts", "codex-app-server-bridge.ts")
1119
1536
  ];
1120
1537
  for (const candidate of candidates) {
@@ -1148,8 +1565,37 @@ function buildBridgeDaemonEnv(parentEnv, runtimeEnv) {
1148
1565
  ...runtimeEnv
1149
1566
  };
1150
1567
  }
1568
+ function normalizeRoutingSlot(value) {
1569
+ const normalized = value?.trim().toLowerCase();
1570
+ if (!normalized) return null;
1571
+ if (normalized === "tower") return "tower";
1572
+ if (normalized === "reviewer") return "reviewer";
1573
+ const worktreeMatch = normalized.match(/^wt[-_]?(\d+)$/);
1574
+ if (worktreeMatch) {
1575
+ return `wt-${Number.parseInt(worktreeMatch[1], 10)}`;
1576
+ }
1577
+ return null;
1578
+ }
1579
+ function resolveBridgeRoutingSlot(repoRoot, env = process.env) {
1580
+ const explicit = normalizeRoutingSlot(env.TAP_ROUTING_SLOT);
1581
+ if (explicit) return explicit;
1582
+ const instanceId = env.TAP_INSTANCE_ID?.trim() || env.TAP_BRIDGE_INSTANCE_ID?.trim() || "";
1583
+ const normalizedInstance = instanceId.toLowerCase().replace(/_/g, "-");
1584
+ if (normalizedInstance === "tower" || normalizedInstance === "claude-main" || normalizedInstance === "codex-main") {
1585
+ return "tower";
1586
+ }
1587
+ if (normalizedInstance === "reviewer" || normalizedInstance === "claude-reviewer" || normalizedInstance === "codex-reviewer") {
1588
+ return "reviewer";
1589
+ }
1590
+ if (/^(?:(?:claude|codex)-)?wt-?(\d+)$/.test(normalizedInstance)) {
1591
+ return normalizeRoutingSlot(
1592
+ normalizedInstance.replace(/^(?:claude|codex)-/, "")
1593
+ );
1594
+ }
1595
+ return normalizeRoutingSlot(path7.basename(repoRoot));
1596
+ }
1151
1597
  async function main() {
1152
- const repoRootHint = findRepoRootFromRunner() ?? void 0;
1598
+ const repoRootHint = resolveRepoRootHintFromRunner() ?? void 0;
1153
1599
  const { config } = resolveConfig({}, repoRootHint);
1154
1600
  const repoRoot = config.repoRoot;
1155
1601
  const commsDir = config.commsDir;
@@ -1224,6 +1670,10 @@ Expected a packaged dist/bridges/codex-app-server-bridge.mjs or monorepo bridge
1224
1670
  args.push("--process-existing-messages");
1225
1671
  const runtimeEnv = buildRuntimeEnv(repoRoot);
1226
1672
  const daemonEnv = buildBridgeDaemonEnv(process.env, runtimeEnv);
1673
+ const routingSlot = resolveBridgeRoutingSlot(repoRoot, daemonEnv);
1674
+ if (routingSlot && !daemonEnv.TAP_ROUTING_SLOT) {
1675
+ daemonEnv.TAP_ROUTING_SLOT = routingSlot;
1676
+ }
1227
1677
  const child = spawn(command, args, {
1228
1678
  cwd: repoRoot,
1229
1679
  env: daemonEnv,
@@ -1245,6 +1695,7 @@ Expected a packaged dist/bridges/codex-app-server-bridge.mjs or monorepo bridge
1245
1695
  function isDirectExecution() {
1246
1696
  const entry = process.argv[1];
1247
1697
  if (!entry) return false;
1698
+ if (!path7.basename(entry).startsWith("codex-bridge-runner")) return false;
1248
1699
  return import.meta.url === pathToFileURL(path7.resolve(entry)).href;
1249
1700
  }
1250
1701
  if (isDirectExecution()) {
@@ -1256,6 +1707,9 @@ if (isDirectExecution()) {
1256
1707
  export {
1257
1708
  buildBridgeDaemonEnv,
1258
1709
  buildBridgeScriptArgs,
1259
- resolveBridgeDaemonScript
1710
+ resolveBridgeDaemonScript,
1711
+ resolveBridgeRoutingSlot,
1712
+ resolveHeadlessReviewGeneration,
1713
+ resolveRepoRootHintFromRunner
1260
1714
  };
1261
1715
  //# sourceMappingURL=codex-bridge-runner.mjs.map