@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.
- package/AI_GUIDE.md +165 -0
- package/CHANGELOG.md +67 -0
- package/README.md +201 -12
- package/dist/bridges/codex-app-server-auth-gateway.mjs +16 -1
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
- package/dist/bridges/codex-app-server-bridge.d.mts +105 -12
- package/dist/bridges/codex-app-server-bridge.mjs +3149 -251
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.d.mts +4 -1
- package/dist/bridges/codex-bridge-runner.mjs +512 -58
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/bridges/codex-remote-ipc-relay.d.mts +1 -0
- package/dist/bridges/codex-remote-ipc-relay.mjs +1912 -0
- package/dist/bridges/codex-remote-ipc-relay.mjs.map +1 -0
- package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -1
- package/dist/cli.mjs +30944 -8415
- package/dist/cli.mjs.map +1 -1
- package/dist/codex-a2a/index.d.mts +2 -0
- package/dist/codex-a2a/index.mjs +416 -0
- package/dist/codex-a2a/index.mjs.map +1 -0
- package/dist/codex-health/index.d.mts +76 -0
- package/dist/codex-health/index.mjs +153 -0
- package/dist/codex-health/index.mjs.map +1 -0
- package/dist/codex-ipc/index.d.mts +2 -0
- package/dist/codex-ipc/index.mjs +1834 -0
- package/dist/codex-ipc/index.mjs.map +1 -0
- package/dist/index-D4Khz2Mh.d.mts +206 -0
- package/dist/index-DMToLyGd.d.mts +256 -0
- package/dist/index.d.mts +763 -8
- package/dist/index.mjs +11600 -3449
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +8838 -811
- package/dist/mcp-server.mjs.map +1 -1
- package/dist/types-FWvKrFUt.d.mts +43 -0
- package/examples/01-logic-battle-known-broken.md +46 -0
- package/examples/02-cross-model-review-root-cause.md +37 -0
- package/examples/03-convergence-pattern.md +42 -0
- package/examples/04-tower-broadcast.md +41 -0
- package/examples/05-self-awareness-paradox.md +49 -0
- package/examples/06-session-resurrection.md +37 -0
- package/examples/07-ghost-agent.md +31 -0
- package/examples/08-naming-creates-identity.md +36 -0
- package/examples/09-ceo-as-middleware.md +52 -0
- package/examples/10-files-as-interface.md +67 -0
- package/examples/README.md +34 -0
- package/examples/tap-profile-pack.example.json +71 -0
- 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(
|
|
359
|
+
function reviewFilePath(repoRoot, generation, prNumber, agentName) {
|
|
315
360
|
return path5.join(
|
|
316
|
-
|
|
361
|
+
repoRoot,
|
|
317
362
|
"reviews",
|
|
318
363
|
generation,
|
|
319
364
|
`review-PR${prNumber}-${agentName}.md`
|
|
320
365
|
);
|
|
321
366
|
}
|
|
322
|
-
function isStaleReviewRequest(request,
|
|
367
|
+
function isStaleReviewRequest(request, repoRoot, agentName) {
|
|
323
368
|
const revPath = reviewFilePath(
|
|
324
|
-
|
|
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
|
|
337
|
-
const
|
|
338
|
-
const
|
|
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,
|
|
342
|
-
const markerId = computeRequestMarkerId(
|
|
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
|
|
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
|
|
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 (
|
|
411
|
-
|
|
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
|
|
508
|
-
|
|
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(
|
|
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.
|
|
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(
|
|
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 =
|
|
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
|
|
1057
|
-
|
|
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 (
|
|
1060
|
-
if (
|
|
1061
|
-
if (
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|