@hua-labs/tap 0.1.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/README.md +220 -0
- package/bin/tap-comms.mjs +2 -0
- package/dist/bridges/codex-bridge-runner.d.mts +2 -0
- package/dist/bridges/codex-bridge-runner.mjs +1009 -0
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +4166 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +272 -0
- package/dist/index.mjs +439 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/engine/termination.ts
|
|
12
|
+
import * as fs3 from "fs";
|
|
13
|
+
import * as crypto from "crypto";
|
|
14
|
+
function isAtOrAbove(severity, floor) {
|
|
15
|
+
return SEVERITY_RANK[severity] >= SEVERITY_RANK[floor];
|
|
16
|
+
}
|
|
17
|
+
function computeFindingHash(findings) {
|
|
18
|
+
const normalized = findings.filter((f) => isAtOrAbove(f.severity, "high")).map((f) => `${f.category}:${f.description.slice(0, 100)}`).sort().join("|");
|
|
19
|
+
if (!normalized) return "empty";
|
|
20
|
+
return crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
21
|
+
}
|
|
22
|
+
function evalManualStop(ctx) {
|
|
23
|
+
if (fs3.existsSync(ctx.stopSignalPath)) {
|
|
24
|
+
return {
|
|
25
|
+
verdict: "stop",
|
|
26
|
+
reason: `Manual stop signal found at ${ctx.stopSignalPath}`,
|
|
27
|
+
strategy: "manual-stop",
|
|
28
|
+
summary: `Review stopped manually at round ${ctx.round}`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function evalRoundCap(ctx) {
|
|
34
|
+
if (ctx.round >= ctx.config.maxRounds) {
|
|
35
|
+
return {
|
|
36
|
+
verdict: "stop",
|
|
37
|
+
reason: `Round cap reached (${ctx.round}/${ctx.config.maxRounds})`,
|
|
38
|
+
strategy: "round-cap",
|
|
39
|
+
summary: `Review stopped at round cap (${ctx.config.maxRounds})`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function evalRepetition(ctx) {
|
|
45
|
+
if (ctx.rounds.length < 2) return null;
|
|
46
|
+
const latest = ctx.rounds[ctx.rounds.length - 1];
|
|
47
|
+
if (!latest) return null;
|
|
48
|
+
let count = 0;
|
|
49
|
+
for (const round of ctx.rounds) {
|
|
50
|
+
if (round.findingHash === latest.findingHash) count++;
|
|
51
|
+
}
|
|
52
|
+
if (count >= ctx.config.repetitionThreshold) {
|
|
53
|
+
return {
|
|
54
|
+
verdict: "stop",
|
|
55
|
+
reason: `Same finding hash repeated ${count} times (threshold: ${ctx.config.repetitionThreshold})`,
|
|
56
|
+
strategy: "repetition-detection",
|
|
57
|
+
summary: `Review going in circles \u2014 same findings repeated ${count}x`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function evalQualityThreshold(ctx) {
|
|
63
|
+
if (ctx.rounds.length === 0) return null;
|
|
64
|
+
const latest = ctx.rounds[ctx.rounds.length - 1];
|
|
65
|
+
if (!latest) return null;
|
|
66
|
+
if (latest.findingCount === 0 && latest.suggestedDiffLines === 0 && latest.findings.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const significantFindings = latest.findings.filter(
|
|
70
|
+
(f) => isAtOrAbove(f.severity, ctx.config.qualitySeverityFloor)
|
|
71
|
+
);
|
|
72
|
+
if (significantFindings.length === 0) {
|
|
73
|
+
return {
|
|
74
|
+
verdict: "stop",
|
|
75
|
+
reason: `No findings at ${ctx.config.qualitySeverityFloor}+ severity in round ${ctx.round}`,
|
|
76
|
+
strategy: "quality-threshold",
|
|
77
|
+
summary: `Review clean \u2014 no ${ctx.config.qualitySeverityFloor}+ findings in round ${ctx.round}`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function evalDiffInsignificance(ctx) {
|
|
83
|
+
if (ctx.rounds.length === 0) return null;
|
|
84
|
+
const latest = ctx.rounds[ctx.rounds.length - 1];
|
|
85
|
+
if (!latest) return null;
|
|
86
|
+
if (latest.findingCount === 0 && latest.suggestedDiffLines === 0 && latest.findings.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (latest.suggestedDiffLines < ctx.config.diffThreshold) {
|
|
90
|
+
return {
|
|
91
|
+
verdict: "stop",
|
|
92
|
+
reason: `Suggested diff (${latest.suggestedDiffLines} lines) below threshold (${ctx.config.diffThreshold})`,
|
|
93
|
+
strategy: "diff-insignificance",
|
|
94
|
+
summary: `Review suggestions are trivial (${latest.suggestedDiffLines} lines)`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function evaluate(ctx) {
|
|
100
|
+
for (const strategy of ctx.config.strategies) {
|
|
101
|
+
const evaluator = STRATEGY_EVALUATORS[strategy];
|
|
102
|
+
if (!evaluator) continue;
|
|
103
|
+
const result = evaluator(ctx);
|
|
104
|
+
if (result) return result;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
verdict: "continue",
|
|
108
|
+
reason: "All strategies passed \u2014 review continues",
|
|
109
|
+
strategy: ctx.config.strategies[ctx.config.strategies.length - 1] ?? "round-cap",
|
|
110
|
+
summary: `Round ${ctx.round} complete, continuing`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
var DEFAULT_TERMINATION_CONFIG, SEVERITY_RANK, STRATEGY_EVALUATORS;
|
|
114
|
+
var init_termination = __esm({
|
|
115
|
+
"src/engine/termination.ts"() {
|
|
116
|
+
"use strict";
|
|
117
|
+
DEFAULT_TERMINATION_CONFIG = {
|
|
118
|
+
strategies: [
|
|
119
|
+
"manual-stop",
|
|
120
|
+
"round-cap",
|
|
121
|
+
"repetition-detection",
|
|
122
|
+
"quality-threshold",
|
|
123
|
+
"diff-insignificance"
|
|
124
|
+
],
|
|
125
|
+
maxRounds: 5,
|
|
126
|
+
diffThreshold: 3,
|
|
127
|
+
repetitionThreshold: 2,
|
|
128
|
+
qualitySeverityFloor: "high"
|
|
129
|
+
};
|
|
130
|
+
SEVERITY_RANK = {
|
|
131
|
+
critical: 5,
|
|
132
|
+
high: 4,
|
|
133
|
+
medium: 3,
|
|
134
|
+
low: 2,
|
|
135
|
+
nitpick: 1
|
|
136
|
+
};
|
|
137
|
+
STRATEGY_EVALUATORS = {
|
|
138
|
+
"manual-stop": evalManualStop,
|
|
139
|
+
"round-cap": evalRoundCap,
|
|
140
|
+
"repetition-detection": evalRepetition,
|
|
141
|
+
"quality-threshold": evalQualityThreshold,
|
|
142
|
+
"diff-insignificance": evalDiffInsignificance
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// src/engine/review.ts
|
|
148
|
+
import * as fs4 from "fs";
|
|
149
|
+
import * as path3 from "path";
|
|
150
|
+
import * as crypto2 from "crypto";
|
|
151
|
+
function parseInboxFilename(filename) {
|
|
152
|
+
const base = path3.basename(filename, ".md");
|
|
153
|
+
const match = base.match(/^(\d{8})-([^-]+)-([^-]+)-(.+)$/);
|
|
154
|
+
if (!match) return null;
|
|
155
|
+
return {
|
|
156
|
+
date: match[1],
|
|
157
|
+
sender: match[2],
|
|
158
|
+
recipient: match[3],
|
|
159
|
+
subject: match[4]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function extractPrNumber(text) {
|
|
163
|
+
for (const pattern of PR_NUMBER_PATTERNS) {
|
|
164
|
+
const match = text.match(pattern);
|
|
165
|
+
if (match?.[1]) return parseInt(match[1], 10);
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function detectReviewRequest(filePath, content, generation) {
|
|
170
|
+
const parsed = parseInboxFilename(filePath);
|
|
171
|
+
if (!parsed) return null;
|
|
172
|
+
const fullText = `${parsed.subject} ${content}`;
|
|
173
|
+
const isReview = REVIEW_KEYWORDS.some((re) => re.test(fullText));
|
|
174
|
+
const isReReview = REREVIEW_KEYWORDS.some((re) => re.test(fullText));
|
|
175
|
+
if (!isReview && !isReReview) return null;
|
|
176
|
+
const prNumber = extractPrNumber(fullText);
|
|
177
|
+
if (!prNumber) return null;
|
|
178
|
+
return {
|
|
179
|
+
sourcePath: filePath,
|
|
180
|
+
sender: parsed.sender,
|
|
181
|
+
recipient: parsed.recipient,
|
|
182
|
+
prNumber,
|
|
183
|
+
generation,
|
|
184
|
+
isReReview,
|
|
185
|
+
round: isReReview ? 2 : 1
|
|
186
|
+
// Will be adjusted by session tracking
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function buildReviewPrompt(request, agentName, round) {
|
|
190
|
+
const roundLabel = round > 1 ? ` (re-review round ${round})` : "";
|
|
191
|
+
return [
|
|
192
|
+
`You are a code reviewer for the HUA Platform monorepo.`,
|
|
193
|
+
``,
|
|
194
|
+
`## Task`,
|
|
195
|
+
`Review PR #${request.prNumber}${roundLabel}.`,
|
|
196
|
+
``,
|
|
197
|
+
`## Instructions`,
|
|
198
|
+
`1. Run: gh pr diff ${request.prNumber}`,
|
|
199
|
+
`2. Read changed files for understanding`,
|
|
200
|
+
`3. Apply review checklist: security > data integrity > performance > error handling > code quality`,
|
|
201
|
+
`4. Write structured findings`,
|
|
202
|
+
``,
|
|
203
|
+
`## Output`,
|
|
204
|
+
`Write review to: ${path3.join("reviews", request.generation, `review-PR${request.prNumber}-${agentName}.md`)}`,
|
|
205
|
+
``,
|
|
206
|
+
`### Review File Format`,
|
|
207
|
+
`\`\`\`markdown`,
|
|
208
|
+
`---`,
|
|
209
|
+
`date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
210
|
+
`reviewer: ${agentName}`,
|
|
211
|
+
`pr: ${request.prNumber}`,
|
|
212
|
+
`round: ${round}`,
|
|
213
|
+
`status: clean | p1-Nitems | p2-Nitems`,
|
|
214
|
+
`merge: merge | fix-then-merge | hold`,
|
|
215
|
+
`---`,
|
|
216
|
+
``,
|
|
217
|
+
`## Findings`,
|
|
218
|
+
``,
|
|
219
|
+
`### Critical / High`,
|
|
220
|
+
`- [severity] [category] file:line \u2014 description`,
|
|
221
|
+
``,
|
|
222
|
+
`### Medium / Low`,
|
|
223
|
+
`- [severity] [category] file:line \u2014 description`,
|
|
224
|
+
``,
|
|
225
|
+
`## Checks`,
|
|
226
|
+
`- [ ] Build verified`,
|
|
227
|
+
`- [ ] Typecheck passed`,
|
|
228
|
+
`- [ ] Scope check (only expected files changed)`,
|
|
229
|
+
``,
|
|
230
|
+
`## Suggested Diff Lines`,
|
|
231
|
+
`{number of lines the author should change to address findings}`,
|
|
232
|
+
``,
|
|
233
|
+
`## Decision`,
|
|
234
|
+
`{one-line merge recommendation}`,
|
|
235
|
+
`\`\`\``,
|
|
236
|
+
``,
|
|
237
|
+
`## After Review`,
|
|
238
|
+
`- Update reviews/INDEX.md`,
|
|
239
|
+
`- Write inbox reply to ${request.sender}`,
|
|
240
|
+
`- Commit and push comms changes`
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
function extractSuggestedDiffLines(content) {
|
|
244
|
+
const match = content.match(/## Suggested Diff Lines\s*\n\s*(\d+)/i);
|
|
245
|
+
if (match?.[1]) return parseInt(match[1], 10);
|
|
246
|
+
const codeBlocks = content.match(/```[\s\S]*?```/g) ?? [];
|
|
247
|
+
let totalLines = 0;
|
|
248
|
+
for (const block of codeBlocks) {
|
|
249
|
+
totalLines += block.split("\n").length - 2;
|
|
250
|
+
}
|
|
251
|
+
return totalLines;
|
|
252
|
+
}
|
|
253
|
+
function extractFindings(content) {
|
|
254
|
+
const findings = [];
|
|
255
|
+
const lines = content.split("\n");
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
const trimmed = line.trim();
|
|
258
|
+
if (!trimmed.startsWith("-") && !trimmed.startsWith("*")) continue;
|
|
259
|
+
let severity = "medium";
|
|
260
|
+
for (const [sev, pattern] of Object.entries(SEVERITY_PATTERNS)) {
|
|
261
|
+
if (pattern.test(trimmed)) {
|
|
262
|
+
severity = sev;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let category = "general";
|
|
267
|
+
for (const cat of CATEGORY_PATTERNS) {
|
|
268
|
+
if (trimmed.toLowerCase().includes(cat)) {
|
|
269
|
+
category = cat;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const fileMatch = trimmed.match(/([a-zA-Z0-9_/.-]+\.[a-zA-Z]+):(\d+)/);
|
|
274
|
+
const hasSeverityKeyword = Object.values(SEVERITY_PATTERNS).some(
|
|
275
|
+
(p) => p.test(trimmed)
|
|
276
|
+
);
|
|
277
|
+
if (hasSeverityKeyword || fileMatch) {
|
|
278
|
+
findings.push({
|
|
279
|
+
severity,
|
|
280
|
+
category,
|
|
281
|
+
description: trimmed.replace(/^[-*]\s*/, "").slice(0, 200),
|
|
282
|
+
file: fileMatch?.[1],
|
|
283
|
+
line: fileMatch?.[2] ? parseInt(fileMatch[2], 10) : void 0
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return findings;
|
|
288
|
+
}
|
|
289
|
+
function parseReviewOutput(reviewFilePath2, round) {
|
|
290
|
+
if (!fs4.existsSync(reviewFilePath2)) return null;
|
|
291
|
+
const content = fs4.readFileSync(reviewFilePath2, "utf-8");
|
|
292
|
+
const findings = extractFindings(content);
|
|
293
|
+
const suggestedDiffLines = extractSuggestedDiffLines(content);
|
|
294
|
+
return {
|
|
295
|
+
round,
|
|
296
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
297
|
+
findingCount: findings.length,
|
|
298
|
+
findings,
|
|
299
|
+
suggestedDiffLines,
|
|
300
|
+
findingHash: computeFindingHash(findings)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function reviewFilePath(commsDir, generation, prNumber, agentName) {
|
|
304
|
+
return path3.join(
|
|
305
|
+
commsDir,
|
|
306
|
+
"reviews",
|
|
307
|
+
generation,
|
|
308
|
+
`review-PR${prNumber}-${agentName}.md`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
function isStaleReviewRequest(request, commsDir, agentName) {
|
|
312
|
+
const revPath = reviewFilePath(
|
|
313
|
+
commsDir,
|
|
314
|
+
request.generation,
|
|
315
|
+
request.prNumber,
|
|
316
|
+
agentName
|
|
317
|
+
);
|
|
318
|
+
if (fs4.existsSync(revPath) && fs4.existsSync(request.sourcePath)) {
|
|
319
|
+
const reviewStat = fs4.statSync(revPath);
|
|
320
|
+
const requestStat = fs4.statSync(request.sourcePath);
|
|
321
|
+
if (reviewStat.mtimeMs > requestStat.mtimeMs) return true;
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
function computeRequestMarkerId(filePath) {
|
|
326
|
+
const stat = fs4.statSync(filePath);
|
|
327
|
+
const input = `${filePath}|${stat.mtimeMs}`;
|
|
328
|
+
return crypto2.createHash("sha1").update(input).digest("hex");
|
|
329
|
+
}
|
|
330
|
+
function isAlreadyProcessed(stateDir, filePath) {
|
|
331
|
+
const markerId = computeRequestMarkerId(filePath);
|
|
332
|
+
return fs4.existsSync(path3.join(stateDir, "processed", `${markerId}.done`));
|
|
333
|
+
}
|
|
334
|
+
function unmarkProcessed(stateDir, request) {
|
|
335
|
+
const markerId = computeRequestMarkerId(request.sourcePath);
|
|
336
|
+
const markerPath = path3.join(stateDir, "processed", `${markerId}.done`);
|
|
337
|
+
if (fs4.existsSync(markerPath)) {
|
|
338
|
+
fs4.unlinkSync(markerPath);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function markAsProcessed(stateDir, request) {
|
|
342
|
+
const markerId = computeRequestMarkerId(request.sourcePath);
|
|
343
|
+
const markerDir = path3.join(stateDir, "processed");
|
|
344
|
+
fs4.mkdirSync(markerDir, { recursive: true });
|
|
345
|
+
const markerPath = path3.join(markerDir, `${markerId}.done`);
|
|
346
|
+
const payload = {
|
|
347
|
+
prNumber: request.prNumber,
|
|
348
|
+
sourcePath: request.sourcePath,
|
|
349
|
+
processedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
350
|
+
};
|
|
351
|
+
const tmp = `${markerPath}.tmp.${process.pid}`;
|
|
352
|
+
fs4.writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf-8");
|
|
353
|
+
fs4.renameSync(tmp, markerPath);
|
|
354
|
+
}
|
|
355
|
+
function writeReviewReceipt(commsDir, request, agentName) {
|
|
356
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
|
|
357
|
+
const filename = `${date}-${agentName}-${request.sender}-PR${request.prNumber}-ack.md`;
|
|
358
|
+
const content = [
|
|
359
|
+
`## ${agentName} > ${request.sender}`,
|
|
360
|
+
``,
|
|
361
|
+
`- PR #${request.prNumber} review request received.`,
|
|
362
|
+
`- headless reviewer processing.`,
|
|
363
|
+
`- request: ${path3.basename(request.sourcePath)}`
|
|
364
|
+
].join("\n");
|
|
365
|
+
const inboxDir = path3.join(commsDir, "inbox");
|
|
366
|
+
fs4.mkdirSync(inboxDir, { recursive: true });
|
|
367
|
+
const inboxPath = path3.join(inboxDir, filename);
|
|
368
|
+
const tmp = `${inboxPath}.tmp.${process.pid}`;
|
|
369
|
+
fs4.writeFileSync(tmp, content, "utf-8");
|
|
370
|
+
fs4.renameSync(tmp, inboxPath);
|
|
371
|
+
return inboxPath;
|
|
372
|
+
}
|
|
373
|
+
function isHeadlessReviewer() {
|
|
374
|
+
return process.env.TAP_HEADLESS === "true";
|
|
375
|
+
}
|
|
376
|
+
function getHeadlessEnvConfig() {
|
|
377
|
+
if (!isHeadlessReviewer()) return null;
|
|
378
|
+
return {
|
|
379
|
+
role: process.env.TAP_AGENT_ROLE ?? "reviewer",
|
|
380
|
+
maxRounds: parseInt(process.env.TAP_MAX_REVIEW_ROUNDS ?? "5", 10),
|
|
381
|
+
qualityFloor: process.env.TAP_QUALITY_FLOOR ?? "high"
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function scanInboxForReviews(commsDir, stateDir, generation, agentName) {
|
|
385
|
+
const inboxDir = path3.join(commsDir, "inbox");
|
|
386
|
+
if (!fs4.existsSync(inboxDir)) return [];
|
|
387
|
+
const files = fs4.readdirSync(inboxDir).filter((f) => f.endsWith(".md"));
|
|
388
|
+
const requests = [];
|
|
389
|
+
for (const file of files) {
|
|
390
|
+
const filePath = path3.join(inboxDir, file);
|
|
391
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
392
|
+
const request = detectReviewRequest(filePath, content, generation);
|
|
393
|
+
if (!request) continue;
|
|
394
|
+
const to = request.recipient.toLowerCase();
|
|
395
|
+
if (to !== agentName.toLowerCase() && to !== "\uC804\uCCB4" && to !== "all" && to !== "") {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (isStaleReviewRequest(request, commsDir, agentName)) continue;
|
|
399
|
+
if (isAlreadyProcessed(stateDir, filePath)) continue;
|
|
400
|
+
requests.push(request);
|
|
401
|
+
}
|
|
402
|
+
return requests;
|
|
403
|
+
}
|
|
404
|
+
var REVIEW_KEYWORDS, REREVIEW_KEYWORDS, PR_NUMBER_PATTERNS, SEVERITY_PATTERNS, CATEGORY_PATTERNS;
|
|
405
|
+
var init_review = __esm({
|
|
406
|
+
"src/engine/review.ts"() {
|
|
407
|
+
"use strict";
|
|
408
|
+
init_termination();
|
|
409
|
+
REVIEW_KEYWORDS = [/리뷰\s*요청/, /review[- ]?request/i];
|
|
410
|
+
REREVIEW_KEYWORDS = [/재리뷰/, /re-?review/i];
|
|
411
|
+
PR_NUMBER_PATTERNS = [
|
|
412
|
+
/PR\s*#?\s*(\d+)/i,
|
|
413
|
+
/pull\/(\d+)/,
|
|
414
|
+
/review[-_ ]?(\d+)/i
|
|
415
|
+
];
|
|
416
|
+
SEVERITY_PATTERNS = {
|
|
417
|
+
critical: /\bcritical\b/i,
|
|
418
|
+
high: /\bhigh\b/i,
|
|
419
|
+
medium: /\bmedium\b/i,
|
|
420
|
+
low: /\blow\b/i,
|
|
421
|
+
nitpick: /\bnitpick\b/i
|
|
422
|
+
};
|
|
423
|
+
CATEGORY_PATTERNS = [
|
|
424
|
+
"security",
|
|
425
|
+
"performance",
|
|
426
|
+
"correctness",
|
|
427
|
+
"data-integrity",
|
|
428
|
+
"error-handling",
|
|
429
|
+
"code-quality",
|
|
430
|
+
"style"
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// src/engine/headless-loop.ts
|
|
436
|
+
var headless_loop_exports = {};
|
|
437
|
+
__export(headless_loop_exports, {
|
|
438
|
+
createHeadlessLoop: () => createHeadlessLoop
|
|
439
|
+
});
|
|
440
|
+
import * as fs5 from "fs";
|
|
441
|
+
import * as path4 from "path";
|
|
442
|
+
function createHeadlessLoop(options) {
|
|
443
|
+
const envConfig = getHeadlessEnvConfig();
|
|
444
|
+
const terminationConfig = {
|
|
445
|
+
...DEFAULT_TERMINATION_CONFIG,
|
|
446
|
+
maxRounds: envConfig?.maxRounds ?? DEFAULT_TERMINATION_CONFIG.maxRounds,
|
|
447
|
+
qualitySeverityFloor: envConfig?.qualityFloor ?? DEFAULT_TERMINATION_CONFIG.qualitySeverityFloor
|
|
448
|
+
};
|
|
449
|
+
const state = {
|
|
450
|
+
running: false,
|
|
451
|
+
activeSession: null,
|
|
452
|
+
completedSessions: 0,
|
|
453
|
+
lastPollAt: null
|
|
454
|
+
};
|
|
455
|
+
let timer = null;
|
|
456
|
+
function log(msg) {
|
|
457
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
458
|
+
console.error(`[${ts}] [headless-loop] ${msg}`);
|
|
459
|
+
}
|
|
460
|
+
function pollOnce() {
|
|
461
|
+
state.lastPollAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
462
|
+
if (state.activeSession) {
|
|
463
|
+
checkActiveSession();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const requests = scanInboxForReviews(
|
|
467
|
+
options.commsDir,
|
|
468
|
+
options.stateDir,
|
|
469
|
+
options.generation,
|
|
470
|
+
options.agentName
|
|
471
|
+
);
|
|
472
|
+
if (requests.length === 0) return;
|
|
473
|
+
const request = requests[0];
|
|
474
|
+
startReviewSession(request);
|
|
475
|
+
}
|
|
476
|
+
function startReviewSession(request) {
|
|
477
|
+
log(`Starting review for PR #${request.prNumber}`);
|
|
478
|
+
markAsProcessed(options.stateDir, request);
|
|
479
|
+
try {
|
|
480
|
+
writeReviewReceipt(options.commsDir, request, options.agentName);
|
|
481
|
+
const prompt = buildReviewPrompt(request, options.agentName, 1);
|
|
482
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
|
|
483
|
+
const dispatchFilename = `${date}-headless-${options.agentName}-review-PR${request.prNumber}.md`;
|
|
484
|
+
const inboxDir = path4.join(options.commsDir, "inbox");
|
|
485
|
+
fs5.mkdirSync(inboxDir, { recursive: true });
|
|
486
|
+
const dispatchFile = path4.join(inboxDir, dispatchFilename);
|
|
487
|
+
const tmp = `${dispatchFile}.tmp.${process.pid}`;
|
|
488
|
+
fs5.writeFileSync(tmp, prompt, "utf-8");
|
|
489
|
+
fs5.renameSync(tmp, dispatchFile);
|
|
490
|
+
state.activeSession = {
|
|
491
|
+
request,
|
|
492
|
+
agentName: options.agentName,
|
|
493
|
+
role: envConfig?.role ?? "reviewer",
|
|
494
|
+
rounds: [],
|
|
495
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
496
|
+
reviewFilePath: reviewFilePath(
|
|
497
|
+
options.commsDir,
|
|
498
|
+
request.generation,
|
|
499
|
+
request.prNumber,
|
|
500
|
+
options.agentName
|
|
501
|
+
)
|
|
502
|
+
};
|
|
503
|
+
log(`Dispatched review prompt for PR #${request.prNumber} (round 1)`);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
log(
|
|
506
|
+
`Failed to start review for PR #${request.prNumber}: ${err instanceof Error ? err.message : String(err)}`
|
|
507
|
+
);
|
|
508
|
+
unmarkProcessed(options.stateDir, request);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function checkActiveSession() {
|
|
512
|
+
if (!state.activeSession) return;
|
|
513
|
+
const session = state.activeSession;
|
|
514
|
+
const revPath = session.reviewFilePath;
|
|
515
|
+
if (!fs5.existsSync(revPath)) return;
|
|
516
|
+
const stat = fs5.statSync(revPath);
|
|
517
|
+
const lastRound = session.rounds[session.rounds.length - 1];
|
|
518
|
+
const lastCheck = lastRound?.timestamp ?? session.startedAt;
|
|
519
|
+
if (stat.mtime.toISOString() <= lastCheck) return;
|
|
520
|
+
const roundNum = session.rounds.length + 1;
|
|
521
|
+
const round = parseReviewOutput(revPath, roundNum);
|
|
522
|
+
if (!round) return;
|
|
523
|
+
session.rounds.push(round);
|
|
524
|
+
log(
|
|
525
|
+
`PR #${session.request.prNumber} round ${roundNum}: ${round.findingCount} findings, ${round.suggestedDiffLines} suggested diff lines`
|
|
526
|
+
);
|
|
527
|
+
const stopSignalPath = path4.join(options.stateDir, "stop-signal");
|
|
528
|
+
const ctx = {
|
|
529
|
+
round: roundNum,
|
|
530
|
+
rounds: session.rounds,
|
|
531
|
+
stopSignalPath,
|
|
532
|
+
config: terminationConfig
|
|
533
|
+
};
|
|
534
|
+
const result = evaluate(ctx);
|
|
535
|
+
if (result.verdict === "stop") {
|
|
536
|
+
log(
|
|
537
|
+
`PR #${session.request.prNumber} terminated: ${result.reason} (${result.strategy})`
|
|
538
|
+
);
|
|
539
|
+
completeSession(session);
|
|
540
|
+
} else {
|
|
541
|
+
log(`PR #${session.request.prNumber} continues to round ${roundNum + 1}`);
|
|
542
|
+
dispatchFollowUp(session, roundNum + 1);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function dispatchFollowUp(session, round) {
|
|
546
|
+
const prompt = buildReviewPrompt(session.request, options.agentName, round);
|
|
547
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
|
|
548
|
+
const dispatchFilename = `${date}-headless-${options.agentName}-review-PR${session.request.prNumber}-r${round}.md`;
|
|
549
|
+
const inboxDir = path4.join(options.commsDir, "inbox");
|
|
550
|
+
fs5.mkdirSync(inboxDir, { recursive: true });
|
|
551
|
+
const dispatchFile = path4.join(inboxDir, dispatchFilename);
|
|
552
|
+
const tmp = `${dispatchFile}.tmp.${process.pid}`;
|
|
553
|
+
fs5.writeFileSync(tmp, prompt, "utf-8");
|
|
554
|
+
fs5.renameSync(tmp, dispatchFile);
|
|
555
|
+
}
|
|
556
|
+
function completeSession(session) {
|
|
557
|
+
session.terminatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
558
|
+
const inboxDir = path4.join(options.commsDir, "inbox");
|
|
559
|
+
if (fs5.existsSync(inboxDir)) {
|
|
560
|
+
const prefix = `headless-${options.agentName}-review-PR${session.request.prNumber}`;
|
|
561
|
+
const files = fs5.readdirSync(inboxDir).filter((f) => f.includes(prefix));
|
|
562
|
+
for (const f of files) {
|
|
563
|
+
fs5.unlinkSync(path4.join(inboxDir, f));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
state.activeSession = null;
|
|
567
|
+
state.completedSessions++;
|
|
568
|
+
log(
|
|
569
|
+
`PR #${session.request.prNumber} review complete (${session.rounds.length} rounds)`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
start() {
|
|
574
|
+
if (!isHeadlessReviewer()) {
|
|
575
|
+
log("Not in headless mode \u2014 loop not started");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
state.running = true;
|
|
579
|
+
log(
|
|
580
|
+
`Headless review loop started (${envConfig?.role ?? "reviewer"}, poll ${options.pollIntervalMs}ms, max ${terminationConfig.maxRounds} rounds)`
|
|
581
|
+
);
|
|
582
|
+
pollOnce();
|
|
583
|
+
timer = setInterval(pollOnce, options.pollIntervalMs);
|
|
584
|
+
},
|
|
585
|
+
stop() {
|
|
586
|
+
state.running = false;
|
|
587
|
+
if (timer) {
|
|
588
|
+
clearInterval(timer);
|
|
589
|
+
timer = null;
|
|
590
|
+
}
|
|
591
|
+
log("Headless review loop stopped");
|
|
592
|
+
},
|
|
593
|
+
getState() {
|
|
594
|
+
return { ...state };
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
var init_headless_loop = __esm({
|
|
599
|
+
"src/engine/headless-loop.ts"() {
|
|
600
|
+
"use strict";
|
|
601
|
+
init_review();
|
|
602
|
+
init_termination();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// src/bridges/codex-bridge-runner.ts
|
|
607
|
+
import * as fs6 from "fs";
|
|
608
|
+
import * as path5 from "path";
|
|
609
|
+
import { spawn } from "child_process";
|
|
610
|
+
import { fileURLToPath } from "url";
|
|
611
|
+
|
|
612
|
+
// src/config/resolve.ts
|
|
613
|
+
import * as fs from "fs";
|
|
614
|
+
import * as path from "path";
|
|
615
|
+
var SHARED_CONFIG_FILE = "tap-config.json";
|
|
616
|
+
var LOCAL_CONFIG_FILE = "tap-config.local.json";
|
|
617
|
+
var DEFAULT_RUNTIME_COMMAND = "node";
|
|
618
|
+
var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
619
|
+
function findRepoRoot(startDir = process.cwd()) {
|
|
620
|
+
let dir = path.resolve(startDir);
|
|
621
|
+
while (true) {
|
|
622
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
623
|
+
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
|
|
624
|
+
const parent = path.dirname(dir);
|
|
625
|
+
if (parent === dir) break;
|
|
626
|
+
dir = parent;
|
|
627
|
+
}
|
|
628
|
+
return process.cwd();
|
|
629
|
+
}
|
|
630
|
+
function loadJsonFile(filePath) {
|
|
631
|
+
if (!fs.existsSync(filePath)) return null;
|
|
632
|
+
try {
|
|
633
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
634
|
+
return JSON.parse(raw);
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function loadSharedConfig(repoRoot) {
|
|
640
|
+
return loadJsonFile(path.join(repoRoot, SHARED_CONFIG_FILE));
|
|
641
|
+
}
|
|
642
|
+
function loadLocalConfig(repoRoot) {
|
|
643
|
+
return loadJsonFile(path.join(repoRoot, LOCAL_CONFIG_FILE));
|
|
644
|
+
}
|
|
645
|
+
function resolveConfig(overrides = {}, startDir) {
|
|
646
|
+
const repoRoot = findRepoRoot(startDir);
|
|
647
|
+
const shared = loadSharedConfig(repoRoot) ?? {};
|
|
648
|
+
const local = loadLocalConfig(repoRoot) ?? {};
|
|
649
|
+
const sources = {
|
|
650
|
+
repoRoot: "auto",
|
|
651
|
+
commsDir: "auto",
|
|
652
|
+
stateDir: "auto",
|
|
653
|
+
runtimeCommand: "auto",
|
|
654
|
+
appServerUrl: "auto"
|
|
655
|
+
};
|
|
656
|
+
let commsDir;
|
|
657
|
+
if (overrides.commsDir) {
|
|
658
|
+
commsDir = path.resolve(overrides.commsDir);
|
|
659
|
+
sources.commsDir = "cli-flag";
|
|
660
|
+
} else if (process.env.TAP_COMMS_DIR) {
|
|
661
|
+
commsDir = path.resolve(process.env.TAP_COMMS_DIR);
|
|
662
|
+
sources.commsDir = "env";
|
|
663
|
+
} else if (local.commsDir) {
|
|
664
|
+
commsDir = resolvePath(repoRoot, local.commsDir);
|
|
665
|
+
sources.commsDir = "local-config";
|
|
666
|
+
} else if (shared.commsDir) {
|
|
667
|
+
commsDir = resolvePath(repoRoot, shared.commsDir);
|
|
668
|
+
sources.commsDir = "shared-config";
|
|
669
|
+
} else {
|
|
670
|
+
commsDir = path.join(path.dirname(repoRoot), "tap-comms");
|
|
671
|
+
}
|
|
672
|
+
let stateDir;
|
|
673
|
+
if (overrides.stateDir) {
|
|
674
|
+
stateDir = path.resolve(overrides.stateDir);
|
|
675
|
+
sources.stateDir = "cli-flag";
|
|
676
|
+
} else if (process.env.TAP_STATE_DIR) {
|
|
677
|
+
stateDir = path.resolve(process.env.TAP_STATE_DIR);
|
|
678
|
+
sources.stateDir = "env";
|
|
679
|
+
} else if (local.stateDir) {
|
|
680
|
+
stateDir = resolvePath(repoRoot, local.stateDir);
|
|
681
|
+
sources.stateDir = "local-config";
|
|
682
|
+
} else if (shared.stateDir) {
|
|
683
|
+
stateDir = resolvePath(repoRoot, shared.stateDir);
|
|
684
|
+
sources.stateDir = "shared-config";
|
|
685
|
+
} else {
|
|
686
|
+
stateDir = path.join(repoRoot, ".tap-comms");
|
|
687
|
+
}
|
|
688
|
+
let runtimeCommand;
|
|
689
|
+
if (overrides.runtimeCommand) {
|
|
690
|
+
runtimeCommand = overrides.runtimeCommand;
|
|
691
|
+
sources.runtimeCommand = "cli-flag";
|
|
692
|
+
} else if (process.env.TAP_RUNTIME_COMMAND) {
|
|
693
|
+
runtimeCommand = process.env.TAP_RUNTIME_COMMAND;
|
|
694
|
+
sources.runtimeCommand = "env";
|
|
695
|
+
} else if (local.runtimeCommand) {
|
|
696
|
+
runtimeCommand = local.runtimeCommand;
|
|
697
|
+
sources.runtimeCommand = "local-config";
|
|
698
|
+
} else if (shared.runtimeCommand) {
|
|
699
|
+
runtimeCommand = shared.runtimeCommand;
|
|
700
|
+
sources.runtimeCommand = "shared-config";
|
|
701
|
+
} else {
|
|
702
|
+
runtimeCommand = DEFAULT_RUNTIME_COMMAND;
|
|
703
|
+
}
|
|
704
|
+
let appServerUrl;
|
|
705
|
+
if (overrides.appServerUrl) {
|
|
706
|
+
appServerUrl = overrides.appServerUrl;
|
|
707
|
+
sources.appServerUrl = "cli-flag";
|
|
708
|
+
} else if (process.env.TAP_APP_SERVER_URL) {
|
|
709
|
+
appServerUrl = process.env.TAP_APP_SERVER_URL;
|
|
710
|
+
sources.appServerUrl = "env";
|
|
711
|
+
} else if (local.appServerUrl) {
|
|
712
|
+
appServerUrl = local.appServerUrl;
|
|
713
|
+
sources.appServerUrl = "local-config";
|
|
714
|
+
} else if (shared.appServerUrl) {
|
|
715
|
+
appServerUrl = shared.appServerUrl;
|
|
716
|
+
sources.appServerUrl = "shared-config";
|
|
717
|
+
} else {
|
|
718
|
+
appServerUrl = DEFAULT_APP_SERVER_URL;
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
config: { repoRoot, commsDir, stateDir, runtimeCommand, appServerUrl },
|
|
722
|
+
sources
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function resolvePath(repoRoot, p) {
|
|
726
|
+
return path.isAbsolute(p) ? p : path.resolve(repoRoot, p);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/runtime/resolve-node.ts
|
|
730
|
+
import * as fs2 from "fs";
|
|
731
|
+
import * as path2 from "path";
|
|
732
|
+
import { execSync } from "child_process";
|
|
733
|
+
function readNodeVersion(repoRoot) {
|
|
734
|
+
const nvFile = path2.join(repoRoot, ".node-version");
|
|
735
|
+
if (!fs2.existsSync(nvFile)) return null;
|
|
736
|
+
try {
|
|
737
|
+
const raw = fs2.readFileSync(nvFile, "utf-8").trim();
|
|
738
|
+
return raw.length > 0 ? raw.replace(/^v/, "") : null;
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function fnmCandidateDirs() {
|
|
744
|
+
if (process.platform === "win32") {
|
|
745
|
+
return [
|
|
746
|
+
process.env.FNM_DIR,
|
|
747
|
+
process.env.APPDATA ? path2.join(process.env.APPDATA, "fnm") : null,
|
|
748
|
+
process.env.LOCALAPPDATA ? path2.join(process.env.LOCALAPPDATA, "fnm") : null,
|
|
749
|
+
process.env.USERPROFILE ? path2.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
|
|
750
|
+
].filter(Boolean);
|
|
751
|
+
}
|
|
752
|
+
return [
|
|
753
|
+
process.env.FNM_DIR,
|
|
754
|
+
process.env.HOME ? path2.join(process.env.HOME, ".local", "share", "fnm") : null,
|
|
755
|
+
process.env.HOME ? path2.join(process.env.HOME, ".fnm") : null,
|
|
756
|
+
process.env.XDG_DATA_HOME ? path2.join(process.env.XDG_DATA_HOME, "fnm") : null
|
|
757
|
+
].filter(Boolean);
|
|
758
|
+
}
|
|
759
|
+
function nodeExecutableName() {
|
|
760
|
+
return process.platform === "win32" ? "node.exe" : "node";
|
|
761
|
+
}
|
|
762
|
+
function probeFnmNode(desiredVersion) {
|
|
763
|
+
const dirs = fnmCandidateDirs();
|
|
764
|
+
const exe = nodeExecutableName();
|
|
765
|
+
for (const baseDir of dirs) {
|
|
766
|
+
const candidate = path2.join(
|
|
767
|
+
baseDir,
|
|
768
|
+
"node-versions",
|
|
769
|
+
`v${desiredVersion}`,
|
|
770
|
+
"installation",
|
|
771
|
+
exe
|
|
772
|
+
);
|
|
773
|
+
if (!fs2.existsSync(candidate)) continue;
|
|
774
|
+
try {
|
|
775
|
+
const v = execSync(`"${candidate}" --version`, {
|
|
776
|
+
encoding: "utf-8",
|
|
777
|
+
timeout: 5e3
|
|
778
|
+
}).trim();
|
|
779
|
+
if (v.startsWith(`v${desiredVersion.split(".")[0]}.`)) {
|
|
780
|
+
return candidate;
|
|
781
|
+
}
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
function detectNodeMajorVersion(command) {
|
|
788
|
+
try {
|
|
789
|
+
const version = execSync(`"${command}" --version`, {
|
|
790
|
+
encoding: "utf-8",
|
|
791
|
+
timeout: 5e3
|
|
792
|
+
}).trim();
|
|
793
|
+
const match = version.match(/^v?(\d+)\./);
|
|
794
|
+
return match ? parseInt(match[1], 10) : null;
|
|
795
|
+
} catch {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function checkStripTypesSupport(command) {
|
|
800
|
+
const major = detectNodeMajorVersion(command);
|
|
801
|
+
if (major !== null && major >= 22) return true;
|
|
802
|
+
try {
|
|
803
|
+
execSync(`"${command}" --experimental-strip-types -e ""`, {
|
|
804
|
+
timeout: 5e3,
|
|
805
|
+
stdio: "pipe"
|
|
806
|
+
});
|
|
807
|
+
return true;
|
|
808
|
+
} catch {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function findTsxFallback(repoRoot) {
|
|
813
|
+
const candidates = [
|
|
814
|
+
path2.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
|
|
815
|
+
path2.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
|
|
816
|
+
path2.join(repoRoot, "node_modules", ".bin", "tsx")
|
|
817
|
+
];
|
|
818
|
+
for (const c of candidates) {
|
|
819
|
+
if (fs2.existsSync(c)) return c;
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
function getFnmBinDir(repoRoot) {
|
|
824
|
+
const desiredVersion = readNodeVersion(repoRoot);
|
|
825
|
+
if (!desiredVersion) return null;
|
|
826
|
+
const nodePath = probeFnmNode(desiredVersion);
|
|
827
|
+
if (!nodePath) return null;
|
|
828
|
+
return path2.dirname(nodePath);
|
|
829
|
+
}
|
|
830
|
+
function resolveNodeRuntime(configCommand, repoRoot) {
|
|
831
|
+
if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
|
|
832
|
+
return {
|
|
833
|
+
command: configCommand,
|
|
834
|
+
supportsStripTypes: false,
|
|
835
|
+
source: "bun",
|
|
836
|
+
majorVersion: null
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const desiredVersion = readNodeVersion(repoRoot);
|
|
840
|
+
if (desiredVersion) {
|
|
841
|
+
const fnmNode = probeFnmNode(desiredVersion);
|
|
842
|
+
if (fnmNode) {
|
|
843
|
+
const major2 = detectNodeMajorVersion(fnmNode);
|
|
844
|
+
return {
|
|
845
|
+
command: fnmNode,
|
|
846
|
+
supportsStripTypes: checkStripTypesSupport(fnmNode),
|
|
847
|
+
source: "fnm",
|
|
848
|
+
majorVersion: major2
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const major = detectNodeMajorVersion(configCommand);
|
|
853
|
+
if (major !== null) {
|
|
854
|
+
return {
|
|
855
|
+
command: configCommand,
|
|
856
|
+
supportsStripTypes: checkStripTypesSupport(configCommand),
|
|
857
|
+
source: major === detectNodeMajorVersion("node") ? "path" : "config",
|
|
858
|
+
majorVersion: major
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const tsx = findTsxFallback(repoRoot);
|
|
862
|
+
if (tsx) {
|
|
863
|
+
return {
|
|
864
|
+
command: tsx,
|
|
865
|
+
supportsStripTypes: false,
|
|
866
|
+
source: "tsx-fallback",
|
|
867
|
+
majorVersion: null
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
command: configCommand,
|
|
872
|
+
supportsStripTypes: false,
|
|
873
|
+
source: "path",
|
|
874
|
+
majorVersion: null
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
|
|
878
|
+
const fnmBin = getFnmBinDir(repoRoot);
|
|
879
|
+
if (!fnmBin) return { ...baseEnv };
|
|
880
|
+
const pathKey = process.platform === "win32" ? "Path" : "PATH";
|
|
881
|
+
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
882
|
+
return {
|
|
883
|
+
...baseEnv,
|
|
884
|
+
[pathKey]: `${fnmBin}${path2.delimiter}${currentPath}`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/bridges/codex-bridge-runner.ts
|
|
889
|
+
function findRepoRootFromRunner() {
|
|
890
|
+
let dir = path5.resolve(path5.dirname(fileURLToPath(import.meta.url)));
|
|
891
|
+
while (true) {
|
|
892
|
+
if (fs6.existsSync(path5.join(dir, SHARED_CONFIG_FILE))) return dir;
|
|
893
|
+
if (fs6.existsSync(path5.join(dir, LOCAL_CONFIG_FILE))) return dir;
|
|
894
|
+
if (fs6.existsSync(path5.join(dir, "scripts", "codex-app-server-bridge.ts")))
|
|
895
|
+
return dir;
|
|
896
|
+
const parent = path5.dirname(dir);
|
|
897
|
+
if (parent === dir) return null;
|
|
898
|
+
dir = parent;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function maybeStartHeadlessLoop(repoRoot, commsDir, stateDir) {
|
|
902
|
+
if (process.env.TAP_HEADLESS !== "true") return;
|
|
903
|
+
Promise.resolve().then(() => (init_headless_loop(), headless_loop_exports)).then(({ createHeadlessLoop: createHeadlessLoop2 }) => {
|
|
904
|
+
const agentName = process.env.TAP_AGENT_NAME ?? process.env.CODEX_TAP_AGENT_NAME ?? "reviewer";
|
|
905
|
+
const generation = process.env.TAP_REVIEW_GENERATION ?? "gen11";
|
|
906
|
+
const resolvedStateDir = stateDir ?? path5.join(repoRoot, ".tap-comms");
|
|
907
|
+
const loop = createHeadlessLoop2({
|
|
908
|
+
commsDir,
|
|
909
|
+
stateDir: resolvedStateDir,
|
|
910
|
+
repoRoot,
|
|
911
|
+
agentName,
|
|
912
|
+
generation,
|
|
913
|
+
pollIntervalMs: 3e3
|
|
914
|
+
// Poll faster than generic bridge (5s) for review priority
|
|
915
|
+
});
|
|
916
|
+
loop.start();
|
|
917
|
+
process.on("SIGTERM", () => loop.stop());
|
|
918
|
+
process.on("SIGINT", () => loop.stop());
|
|
919
|
+
}).catch((err) => {
|
|
920
|
+
console.error("[headless-loop] Failed to start:", err);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
async function main() {
|
|
924
|
+
const repoRootHint = findRepoRootFromRunner() ?? void 0;
|
|
925
|
+
const { config } = resolveConfig({}, repoRootHint);
|
|
926
|
+
const repoRoot = config.repoRoot;
|
|
927
|
+
const commsDir = config.commsDir;
|
|
928
|
+
let appServerUrl = config.appServerUrl;
|
|
929
|
+
const instancePort = process.env.TAP_BRIDGE_PORT;
|
|
930
|
+
if (instancePort) {
|
|
931
|
+
try {
|
|
932
|
+
const url = new URL(appServerUrl);
|
|
933
|
+
url.port = instancePort;
|
|
934
|
+
appServerUrl = url.toString().replace(/\/$/, "");
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const instanceId = process.env.TAP_BRIDGE_INSTANCE_ID;
|
|
939
|
+
const stateDir = instanceId ? path5.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`) : void 0;
|
|
940
|
+
const preResolved = process.env.TAP_RESOLVED_NODE;
|
|
941
|
+
const resolved = preResolved ? {
|
|
942
|
+
command: preResolved,
|
|
943
|
+
supportsStripTypes: process.env.TAP_STRIP_TYPES === "1",
|
|
944
|
+
source: "env",
|
|
945
|
+
majorVersion: null
|
|
946
|
+
} : resolveNodeRuntime(config.runtimeCommand, repoRoot);
|
|
947
|
+
const command = resolved.command;
|
|
948
|
+
const scriptPath = path5.join(
|
|
949
|
+
repoRoot,
|
|
950
|
+
"scripts",
|
|
951
|
+
"codex-app-server-bridge.ts"
|
|
952
|
+
);
|
|
953
|
+
if (!fs6.existsSync(scriptPath)) {
|
|
954
|
+
throw new Error(
|
|
955
|
+
`Bridge script not found: ${scriptPath}
|
|
956
|
+
Ensure scripts/codex-app-server-bridge.ts exists in repo root.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
const args = [];
|
|
960
|
+
if (resolved.supportsStripTypes) {
|
|
961
|
+
args.push("--experimental-strip-types");
|
|
962
|
+
}
|
|
963
|
+
args.push(
|
|
964
|
+
scriptPath,
|
|
965
|
+
`--repo-root=${repoRoot}`,
|
|
966
|
+
`--comms-dir=${commsDir}`,
|
|
967
|
+
`--app-server-url=${appServerUrl}`
|
|
968
|
+
);
|
|
969
|
+
if (stateDir) {
|
|
970
|
+
args.push(`--state-dir=${stateDir}`);
|
|
971
|
+
}
|
|
972
|
+
const busyMode = process.env.TAP_BUSY_MODE;
|
|
973
|
+
if (busyMode) args.push(`--busy-mode=${busyMode}`);
|
|
974
|
+
const pollSeconds = process.env.TAP_POLL_SECONDS;
|
|
975
|
+
if (pollSeconds) args.push(`--poll-seconds=${pollSeconds}`);
|
|
976
|
+
const reconnectSeconds = process.env.TAP_RECONNECT_SECONDS;
|
|
977
|
+
if (reconnectSeconds) args.push(`--reconnect-seconds=${reconnectSeconds}`);
|
|
978
|
+
const lookbackMinutes = process.env.TAP_MESSAGE_LOOKBACK_MINUTES;
|
|
979
|
+
if (lookbackMinutes)
|
|
980
|
+
args.push(`--message-lookback-minutes=${lookbackMinutes}`);
|
|
981
|
+
const threadId = process.env.TAP_THREAD_ID;
|
|
982
|
+
if (threadId) args.push(`--thread-id=${threadId}`);
|
|
983
|
+
if (process.env.TAP_EPHEMERAL === "true") args.push("--ephemeral");
|
|
984
|
+
if (process.env.TAP_PROCESS_EXISTING === "true")
|
|
985
|
+
args.push("--process-existing-messages");
|
|
986
|
+
const runtimeEnv = buildRuntimeEnv(repoRoot);
|
|
987
|
+
const child = spawn(command, args, {
|
|
988
|
+
cwd: repoRoot,
|
|
989
|
+
env: runtimeEnv,
|
|
990
|
+
stdio: "inherit"
|
|
991
|
+
});
|
|
992
|
+
maybeStartHeadlessLoop(repoRoot, commsDir, stateDir);
|
|
993
|
+
child.on("exit", (code, signal) => {
|
|
994
|
+
if (signal) {
|
|
995
|
+
process.kill(process.pid, signal);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
process.exit(code ?? 0);
|
|
999
|
+
});
|
|
1000
|
+
child.on("error", (error) => {
|
|
1001
|
+
console.error(String(error));
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
main().catch((error) => {
|
|
1006
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
});
|
|
1009
|
+
//# sourceMappingURL=codex-bridge-runner.mjs.map
|