imprint-mcp 0.6.1 → 0.6.2
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 +2 -0
- package/package.json +3 -2
- package/prompts/audit-agent.md +4 -2
- package/prompts/compile-agent.md +46 -23
- package/prompts/live-verifier-agent.md +101 -0
- package/prompts/playbook-compilation.md +8 -1
- package/src/cli.ts +83 -1
- package/src/imprint/audit.ts +219 -20
- package/src/imprint/backend-cache.ts +18 -11
- package/src/imprint/backend-ladder.ts +48 -34
- package/src/imprint/cdp-browser-fetch.ts +135 -42
- package/src/imprint/claude-cli-compile.ts +38 -16
- package/src/imprint/codex-cli-compile.ts +120 -21
- package/src/imprint/compile-agent.ts +71 -26
- package/src/imprint/compile-tools.ts +169 -38
- package/src/imprint/compile-verification.ts +187 -0
- package/src/imprint/compile.ts +71 -2
- package/src/imprint/freeform-redact.ts +27 -6
- package/src/imprint/live-verifier.ts +1665 -0
- package/src/imprint/llm.ts +16 -0
- package/src/imprint/mcp-compile-server.ts +380 -81
- package/src/imprint/mcp-live-verifier-server.ts +359 -0
- package/src/imprint/mcp-server.ts +12 -2
- package/src/imprint/probe-backends.ts +88 -7
- package/src/imprint/runtime.ts +61 -11
- package/src/imprint/teach-state.ts +18 -1
- package/src/imprint/teach.ts +77 -19
- package/src/imprint/types.ts +39 -2
|
@@ -94,6 +94,27 @@ export function jarCookiesValidated(cookies: Array<{ name: string; value: string
|
|
|
94
94
|
return cookies.some((c) => c.name === 'bm_sv');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export function buildNavigationSelectorExpression(selector: string): string {
|
|
98
|
+
return `document.querySelector(${JSON.stringify(selector)}) !== null`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildNavigationClickTargetExpression(selector: string): string {
|
|
102
|
+
return `(() => {
|
|
103
|
+
const target = document.querySelector(${JSON.stringify(selector)});
|
|
104
|
+
if (!(target instanceof Element)) return null;
|
|
105
|
+
target.scrollIntoView({ block: 'center', inline: 'center' });
|
|
106
|
+
const rect = target.getBoundingClientRect();
|
|
107
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
108
|
+
if (target.matches(':disabled')) return null;
|
|
109
|
+
const x = rect.left + rect.width / 2;
|
|
110
|
+
const y = rect.top + rect.height / 2;
|
|
111
|
+
const hit = document.elementFromPoint(x, y);
|
|
112
|
+
if (hit !== target && !target.contains(hit) && !hit?.contains(target)) return null;
|
|
113
|
+
target.focus();
|
|
114
|
+
return { x, y };
|
|
115
|
+
})()`;
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
export interface CdpBrowserFetch {
|
|
98
119
|
/** typeof fetch — executes the request inside the live trusted Chrome page. */
|
|
99
120
|
readonly fetchImpl: typeof fetch;
|
|
@@ -109,6 +130,9 @@ export interface CdpBrowserFetch {
|
|
|
109
130
|
timeoutMs?: number;
|
|
110
131
|
pollIntervalMs?: number;
|
|
111
132
|
urlIncludes?: string;
|
|
133
|
+
selector?: string;
|
|
134
|
+
actions?: Array<{ action: 'click'; selector: string }>;
|
|
135
|
+
resultSelector?: string;
|
|
112
136
|
cookie?: { name: string; domain?: string; path?: string };
|
|
113
137
|
},
|
|
114
138
|
): Promise<Response>;
|
|
@@ -1253,6 +1277,49 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1253
1277
|
return currentPageUrl;
|
|
1254
1278
|
};
|
|
1255
1279
|
|
|
1280
|
+
const dispatchTrustedClick = async (
|
|
1281
|
+
c: CdpClient,
|
|
1282
|
+
x: number,
|
|
1283
|
+
y: number,
|
|
1284
|
+
label: string,
|
|
1285
|
+
timeoutMs: number,
|
|
1286
|
+
): Promise<void> => {
|
|
1287
|
+
await withTimeout(c.Page.bringToFront(), `CDP Page.bringToFront(${label})`, timeoutMs);
|
|
1288
|
+
const timestamp = Date.now() / 1000;
|
|
1289
|
+
await withTimeout(
|
|
1290
|
+
c.Input.dispatchMouseEvent({ type: 'mouseMoved', x, y, timestamp }),
|
|
1291
|
+
`CDP Input.dispatchMouseEvent(${label} mouseMoved)`,
|
|
1292
|
+
timeoutMs,
|
|
1293
|
+
);
|
|
1294
|
+
await sleep(100);
|
|
1295
|
+
await withTimeout(
|
|
1296
|
+
c.Input.dispatchMouseEvent({
|
|
1297
|
+
type: 'mousePressed',
|
|
1298
|
+
button: 'left',
|
|
1299
|
+
buttons: 1,
|
|
1300
|
+
clickCount: 1,
|
|
1301
|
+
x,
|
|
1302
|
+
y,
|
|
1303
|
+
timestamp,
|
|
1304
|
+
}),
|
|
1305
|
+
`CDP Input.dispatchMouseEvent(${label} mousePressed)`,
|
|
1306
|
+
timeoutMs,
|
|
1307
|
+
);
|
|
1308
|
+
await sleep(50);
|
|
1309
|
+
await withTimeout(
|
|
1310
|
+
c.Input.dispatchMouseEvent({
|
|
1311
|
+
type: 'mouseReleased',
|
|
1312
|
+
button: 'left',
|
|
1313
|
+
clickCount: 1,
|
|
1314
|
+
x,
|
|
1315
|
+
y,
|
|
1316
|
+
timestamp: Date.now() / 1000,
|
|
1317
|
+
}),
|
|
1318
|
+
`CDP Input.dispatchMouseEvent(${label} mouseReleased)`,
|
|
1319
|
+
timeoutMs,
|
|
1320
|
+
);
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1256
1323
|
const navigate = async (
|
|
1257
1324
|
rawUrl: string,
|
|
1258
1325
|
options: {
|
|
@@ -1263,6 +1330,9 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1263
1330
|
timeoutMs?: number;
|
|
1264
1331
|
pollIntervalMs?: number;
|
|
1265
1332
|
urlIncludes?: string;
|
|
1333
|
+
selector?: string;
|
|
1334
|
+
actions?: Array<{ action: 'click'; selector: string }>;
|
|
1335
|
+
resultSelector?: string;
|
|
1266
1336
|
cookie?: { name: string; domain?: string; path?: string };
|
|
1267
1337
|
} = {},
|
|
1268
1338
|
): Promise<Response> => {
|
|
@@ -1402,44 +1472,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1402
1472
|
log(
|
|
1403
1473
|
`form POST: trusted click on ${String(directive.tag ?? 'control')} type=${String(directive.type ?? 'unknown')} focused=${focusProbe.result.value === true} hit=${liveTarget.hit === true}`,
|
|
1404
1474
|
);
|
|
1405
|
-
|
|
1406
|
-
await withTimeout(
|
|
1407
|
-
c.Input.dispatchMouseEvent({
|
|
1408
|
-
type: 'mouseMoved',
|
|
1409
|
-
x: clickX,
|
|
1410
|
-
y: clickY,
|
|
1411
|
-
timestamp,
|
|
1412
|
-
}),
|
|
1413
|
-
'CDP Input.dispatchMouseEvent(form mouseMoved)',
|
|
1414
|
-
cdpCommandTimeoutMs,
|
|
1415
|
-
);
|
|
1416
|
-
await sleep(100);
|
|
1417
|
-
await withTimeout(
|
|
1418
|
-
c.Input.dispatchMouseEvent({
|
|
1419
|
-
type: 'mousePressed',
|
|
1420
|
-
button: 'left',
|
|
1421
|
-
buttons: 1,
|
|
1422
|
-
clickCount: 1,
|
|
1423
|
-
x: clickX,
|
|
1424
|
-
y: clickY,
|
|
1425
|
-
timestamp,
|
|
1426
|
-
}),
|
|
1427
|
-
'CDP Input.dispatchMouseEvent(form mousePressed)',
|
|
1428
|
-
cdpCommandTimeoutMs,
|
|
1429
|
-
);
|
|
1430
|
-
await sleep(50);
|
|
1431
|
-
await withTimeout(
|
|
1432
|
-
c.Input.dispatchMouseEvent({
|
|
1433
|
-
type: 'mouseReleased',
|
|
1434
|
-
button: 'left',
|
|
1435
|
-
clickCount: 1,
|
|
1436
|
-
x: clickX,
|
|
1437
|
-
y: clickY,
|
|
1438
|
-
timestamp: Date.now() / 1000,
|
|
1439
|
-
}),
|
|
1440
|
-
'CDP Input.dispatchMouseEvent(form mouseReleased)',
|
|
1441
|
-
cdpCommandTimeoutMs,
|
|
1442
|
-
);
|
|
1475
|
+
await dispatchTrustedClick(c, clickX, clickY, 'form', cdpCommandTimeoutMs);
|
|
1443
1476
|
await sleep(250);
|
|
1444
1477
|
const clickProbe = await withTimeout(
|
|
1445
1478
|
c.Runtime.evaluate({
|
|
@@ -1491,15 +1524,28 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1491
1524
|
}
|
|
1492
1525
|
}
|
|
1493
1526
|
|
|
1494
|
-
const
|
|
1527
|
+
const selectorExists = async (selector?: string): Promise<boolean> => {
|
|
1528
|
+
if (!selector) return true;
|
|
1529
|
+
const evaluated = await withTimeout(
|
|
1530
|
+
c.Runtime.evaluate({
|
|
1531
|
+
expression: buildNavigationSelectorExpression(selector),
|
|
1532
|
+
returnByValue: true,
|
|
1533
|
+
}),
|
|
1534
|
+
'CDP Runtime.evaluate(navigation selector)',
|
|
1535
|
+
cdpCommandTimeoutMs,
|
|
1536
|
+
);
|
|
1537
|
+
return evaluated.result.value === true;
|
|
1538
|
+
};
|
|
1539
|
+
const hasPredicate = Boolean(options.urlIncludes || options.selector || options.cookie);
|
|
1540
|
+
const deadline = Date.now() + timeoutMs;
|
|
1495
1541
|
if (!hasPredicate) {
|
|
1496
1542
|
await withTimeout(lifecyclePromise, `CDP Page.${options.waitUntil ?? 'load'}`, timeoutMs);
|
|
1497
1543
|
} else {
|
|
1498
|
-
const deadline = Date.now() + timeoutMs;
|
|
1499
1544
|
let lastUrl = targetUrl;
|
|
1500
1545
|
while (Date.now() < deadline) {
|
|
1501
1546
|
lastUrl = await readCurrentPageUrl(c);
|
|
1502
1547
|
const urlMatches = !options.urlIncludes || lastUrl.includes(options.urlIncludes);
|
|
1548
|
+
const selectorMatches = await selectorExists(options.selector);
|
|
1503
1549
|
let cookieMatches = !options.cookie;
|
|
1504
1550
|
if (options.cookie) {
|
|
1505
1551
|
const expectedDomain = options.cookie.domain?.replace(/^\./, '').toLowerCase();
|
|
@@ -1515,7 +1561,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1515
1561
|
return !options.cookie?.path || cookie.path === options.cookie.path;
|
|
1516
1562
|
});
|
|
1517
1563
|
}
|
|
1518
|
-
if (urlMatches && cookieMatches) break;
|
|
1564
|
+
if (urlMatches && selectorMatches && cookieMatches) break;
|
|
1519
1565
|
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
1520
1566
|
}
|
|
1521
1567
|
|
|
@@ -1528,6 +1574,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1528
1574
|
await sleep(Math.min(500, Math.max(1, deadline - Date.now())));
|
|
1529
1575
|
|
|
1530
1576
|
const finalUrl = await readCurrentPageUrl(c);
|
|
1577
|
+
const finalSelectorMatches = await selectorExists(options.selector);
|
|
1531
1578
|
const allCookies = await snapshotAllCookies(c);
|
|
1532
1579
|
const expectedDomain = options.cookie?.domain?.replace(/^\./, '').toLowerCase();
|
|
1533
1580
|
const matchedCookie = !options.cookie
|
|
@@ -1542,9 +1589,14 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1542
1589
|
}
|
|
1543
1590
|
return !options.cookie?.path || cookie.path === options.cookie.path;
|
|
1544
1591
|
});
|
|
1545
|
-
if (
|
|
1592
|
+
if (
|
|
1593
|
+
(options.urlIncludes && !finalUrl.includes(options.urlIncludes)) ||
|
|
1594
|
+
!finalSelectorMatches ||
|
|
1595
|
+
!matchedCookie
|
|
1596
|
+
) {
|
|
1546
1597
|
const criteria = [
|
|
1547
1598
|
options.urlIncludes ? `URL containing ${JSON.stringify(options.urlIncludes)}` : null,
|
|
1599
|
+
options.selector ? `CSS selector ${JSON.stringify(options.selector)}` : null,
|
|
1548
1600
|
options.cookie ? `cookie ${options.cookie.name}` : null,
|
|
1549
1601
|
]
|
|
1550
1602
|
.filter(Boolean)
|
|
@@ -1555,6 +1607,47 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
|
|
|
1555
1607
|
}
|
|
1556
1608
|
}
|
|
1557
1609
|
|
|
1610
|
+
for (const interaction of options.actions ?? []) {
|
|
1611
|
+
if (interaction.action !== 'click') continue;
|
|
1612
|
+
const target = await withTimeout(
|
|
1613
|
+
c.Runtime.evaluate({
|
|
1614
|
+
expression: buildNavigationClickTargetExpression(interaction.selector),
|
|
1615
|
+
returnByValue: true,
|
|
1616
|
+
}),
|
|
1617
|
+
`CDP Runtime.evaluate(navigation click target ${JSON.stringify(interaction.selector)})`,
|
|
1618
|
+
Math.max(1, deadline - Date.now()),
|
|
1619
|
+
).catch(() => null);
|
|
1620
|
+
const point = target?.result.value as { x?: unknown; y?: unknown } | null | undefined;
|
|
1621
|
+
if (typeof point?.x !== 'number' || typeof point.y !== 'number') {
|
|
1622
|
+
throw new Error(
|
|
1623
|
+
`browser navigation action could not find a visible click target for ${JSON.stringify(interaction.selector)}`,
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
await dispatchTrustedClick(
|
|
1627
|
+
c,
|
|
1628
|
+
point.x,
|
|
1629
|
+
point.y,
|
|
1630
|
+
'navigation',
|
|
1631
|
+
Math.max(1, deadline - Date.now()),
|
|
1632
|
+
);
|
|
1633
|
+
await sleep(150);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
if (options.resultSelector) {
|
|
1637
|
+
let matched = false;
|
|
1638
|
+
while (Date.now() < deadline) {
|
|
1639
|
+
matched = await selectorExists(options.resultSelector).catch(() => false);
|
|
1640
|
+
if (matched) break;
|
|
1641
|
+
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
1642
|
+
}
|
|
1643
|
+
if (!matched) {
|
|
1644
|
+
throw new Error(
|
|
1645
|
+
`browser navigation timed out after ${timeoutMs}ms waiting for post-action CSS selector ${JSON.stringify(options.resultSelector)}`,
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
await sleep(Math.min(500, Math.max(1, deadline - Date.now())));
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1558
1651
|
currentPageUrl = await readCurrentPageUrl(c);
|
|
1559
1652
|
let html = '';
|
|
1560
1653
|
try {
|
|
@@ -39,7 +39,7 @@ import type {
|
|
|
39
39
|
import { formatCandidateContext, formatToolPlan } from './compile-agent-types.ts';
|
|
40
40
|
import { preferredAgentModel } from './llm.ts';
|
|
41
41
|
import { createLog } from './log.ts';
|
|
42
|
-
import { COMPILE_SENTINELS } from './mcp-compile-server.ts';
|
|
42
|
+
import { COMPILE_SENTINELS, compileDeadlineAfterVerification } from './mcp-compile-server.ts';
|
|
43
43
|
import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
|
|
44
44
|
import {
|
|
45
45
|
endTraceSpan,
|
|
@@ -60,6 +60,12 @@ const CLI_PATH = pathJoin(REPO_ROOT, 'src', 'cli.ts');
|
|
|
60
60
|
const MCP_SERVER_NAME = 'imprint-compile';
|
|
61
61
|
const MAX_VERIFICATION_CYCLES = 5;
|
|
62
62
|
|
|
63
|
+
function formatRevisionMode(enabled: boolean | undefined): string {
|
|
64
|
+
return enabled
|
|
65
|
+
? 'REVISION MODE: inspect read_session_summary.revisionContext and the listed existing artifacts/diagnostics first. Preserve proven behavior; repair or honestly narrow only what evidence contradicts.'
|
|
66
|
+
: '';
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
/**
|
|
64
70
|
* Thinking effort for the compile agent. Deliberately `high`, not `max`:
|
|
65
71
|
* empirically, max-effort thinking generates a large volume of reasoning tokens
|
|
@@ -110,6 +116,8 @@ interface CompileViaClaudeCliOptions {
|
|
|
110
116
|
sharedModules?: SharedModuleManifestEntry[];
|
|
111
117
|
/** Per-tool implementation plan injected into the agent's initial message. */
|
|
112
118
|
toolPlan?: string;
|
|
119
|
+
/** Revise existing generated artifacts from durable verification feedback. */
|
|
120
|
+
revisionMode?: boolean;
|
|
113
121
|
/** Present → drive an auth compile rather than a data compile. */
|
|
114
122
|
authMode?: AuthCliCompileMode;
|
|
115
123
|
/** Auth segments only: resume a prior segment's claude session with a new user
|
|
@@ -275,6 +283,7 @@ async function runClaudeCliAttempt(opts: CompileViaClaudeCliOptions): Promise<Co
|
|
|
275
283
|
COMPILE_SENTINELS.done,
|
|
276
284
|
COMPILE_SENTINELS.giveUp,
|
|
277
285
|
COMPILE_SENTINELS.checkpoint,
|
|
286
|
+
COMPILE_SENTINELS.verificationState,
|
|
278
287
|
]) {
|
|
279
288
|
const p = pathJoin(opts.absoluteToolDir, name);
|
|
280
289
|
if (existsSync(p)) {
|
|
@@ -325,10 +334,13 @@ async function runClaudeCliAttempt(opts: CompileViaClaudeCliOptions): Promise<Co
|
|
|
325
334
|
sessionPathAbs,
|
|
326
335
|
'--tool-dir',
|
|
327
336
|
opts.absoluteToolDir,
|
|
337
|
+
'--provider',
|
|
338
|
+
'claude-cli',
|
|
328
339
|
...(opts.candidate ? ['--candidate-json', JSON.stringify(opts.candidate)] : []),
|
|
329
340
|
...(opts.sharedContext ? ['--shared-context-json', JSON.stringify(opts.sharedContext)] : []),
|
|
330
341
|
...(opts.buildPlanPath ? ['--build-plan-path', opts.buildPlanPath] : []),
|
|
331
342
|
...(opts.sharedModules ? ['--shared-modules-json', JSON.stringify(opts.sharedModules)] : []),
|
|
343
|
+
...(opts.revisionMode ? ['--revision-mode'] : []),
|
|
332
344
|
];
|
|
333
345
|
allowedToolNames = [
|
|
334
346
|
'read_session_summary',
|
|
@@ -355,6 +367,7 @@ Tool directory: ${opts.absoluteToolDir}
|
|
|
355
367
|
You will write artifacts into the tool directory.
|
|
356
368
|
${formatCandidateContext(opts.candidate, opts.sharedContext, assignedSharedModules)}
|
|
357
369
|
${formatToolPlan(opts.toolPlan)}
|
|
370
|
+
${formatRevisionMode(opts.revisionMode)}
|
|
358
371
|
|
|
359
372
|
Begin by calling read_session_summary to orient yourself, then proceed per the system prompt.`;
|
|
360
373
|
}
|
|
@@ -425,19 +438,14 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
425
438
|
child = spawn('claude', args, {
|
|
426
439
|
cwd: REPO_ROOT,
|
|
427
440
|
// Claude CLI's default MCP_TOOL_TIMEOUT is 60s. The compile MCP
|
|
428
|
-
// server's `done` tool runs external verification inline
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
// actually complete and the agent receives the failure feedback
|
|
437
|
-
// (and ships with `liveVerified: false` via the waiver path)
|
|
438
|
-
// rather than getting `-32000: Connection closed` mid-call and
|
|
439
|
-
// wasting the rest of its turn budget. Honor user-set env so an
|
|
440
|
-
// operator on a fast network can tighten without editing source.
|
|
441
|
+
// server's `done` tool runs external verification inline: one live
|
|
442
|
+
// integration suite, parser tests, typechecking, and a fresh semantic
|
|
443
|
+
// verifier agent that may make a narrowly targeted follow-up call. On
|
|
444
|
+
// bot-protected sites one suite can still take 10-15 minutes as each
|
|
445
|
+
// assertion escalates fetch → fetch-bootstrap → stealth-fetch. Keep the
|
|
446
|
+
// 30-minute cap so the agent receives semantic feedback rather than an
|
|
447
|
+
// MCP connection-close midway through review. Honor user-set env so an
|
|
448
|
+
// operator on a fast network can tighten it without editing source.
|
|
441
449
|
// Connection-startup timeout stays at 60s for cold Playwright boot.
|
|
442
450
|
env: {
|
|
443
451
|
...process.env,
|
|
@@ -516,9 +524,19 @@ async function driveStreamJson(
|
|
|
516
524
|
};
|
|
517
525
|
|
|
518
526
|
const scheduleDeadlineCheck = (): ReturnType<typeof setTimeout> => {
|
|
519
|
-
const
|
|
527
|
+
const effectiveDeadlineMs = compileDeadlineAfterVerification(
|
|
528
|
+
opts.absoluteToolDir,
|
|
529
|
+
currentDeadlineMs,
|
|
530
|
+
);
|
|
531
|
+
const remaining = Math.max(0, effectiveDeadlineMs - Date.now());
|
|
520
532
|
return setTimeout(async () => {
|
|
521
533
|
if (childExited) return;
|
|
534
|
+
// done() verification runs in the MCP child. Re-read its lifecycle clock
|
|
535
|
+
// before killing the compiler so only reasoning time counts here.
|
|
536
|
+
if (Date.now() < compileDeadlineAfterVerification(opts.absoluteToolDir, currentDeadlineMs)) {
|
|
537
|
+
deadlineTimer = scheduleDeadlineCheck();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
522
540
|
if (opts.onDeadlineReached) {
|
|
523
541
|
const extensionMs = await opts.onDeadlineReached();
|
|
524
542
|
if (childExited) return;
|
|
@@ -748,7 +766,11 @@ async function driveStreamJson(
|
|
|
748
766
|
}
|
|
749
767
|
|
|
750
768
|
// Wall-clock deadline exceeded?
|
|
751
|
-
if (
|
|
769
|
+
if (
|
|
770
|
+
Date.now() > compileDeadlineAfterVerification(opts.absoluteToolDir, currentDeadlineMs) &&
|
|
771
|
+
!existsSync(doneSentinel) &&
|
|
772
|
+
!existsSync(giveUpSentinel)
|
|
773
|
+
) {
|
|
752
774
|
return {
|
|
753
775
|
success: false,
|
|
754
776
|
outcome: 'timeout',
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* success only after the MCP done() tool writes the verified sentinel.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { type ChildProcess, spawn } from 'node:child_process';
|
|
10
|
+
import { type ChildProcess, spawn, spawnSync } from 'node:child_process';
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { isAbsolute as pathIsAbsolute, join as pathJoin } from 'node:path';
|
|
13
13
|
import type { Span } from '@opentelemetry/api';
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
import { formatCandidateContext, formatToolPlan } from './compile-agent-types.ts';
|
|
22
22
|
import { preferredAgentModel } from './llm.ts';
|
|
23
23
|
import { createLog } from './log.ts';
|
|
24
|
-
import { COMPILE_SENTINELS } from './mcp-compile-server.ts';
|
|
24
|
+
import { COMPILE_SENTINELS, compileDeadlineAfterVerification } from './mcp-compile-server.ts';
|
|
25
25
|
import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
|
|
26
26
|
import {
|
|
27
27
|
endTraceSpan,
|
|
@@ -43,9 +43,14 @@ const REPO_ROOT = pathJoin(import.meta.dir, '..', '..');
|
|
|
43
43
|
const CLI_PATH = pathJoin(REPO_ROOT, 'src', 'cli.ts');
|
|
44
44
|
const MCP_SERVER_NAME = 'imprint-compile';
|
|
45
45
|
const MAX_VERIFICATION_CYCLES = 5;
|
|
46
|
-
const MIN_MCP_TOOL_TIMEOUT_SEC = 300;
|
|
47
46
|
const MAX_MCP_TOOL_TIMEOUT_SEC = 1800;
|
|
48
47
|
|
|
48
|
+
function formatRevisionMode(enabled: boolean | undefined): string {
|
|
49
|
+
return enabled
|
|
50
|
+
? 'REVISION MODE: inspect read_session_summary.revisionContext and the listed existing artifacts/diagnostics first. Preserve proven behavior; repair or honestly narrow only what evidence contradicts.'
|
|
51
|
+
: '';
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
interface CompileViaCodexCliOptions {
|
|
50
55
|
session: Session;
|
|
51
56
|
absoluteToolDir: string;
|
|
@@ -63,6 +68,8 @@ interface CompileViaCodexCliOptions {
|
|
|
63
68
|
sharedModules?: SharedModuleManifestEntry[];
|
|
64
69
|
/** Per-tool implementation plan injected into the agent's initial message. */
|
|
65
70
|
toolPlan?: string;
|
|
71
|
+
/** Revise existing generated artifacts from durable verification feedback. */
|
|
72
|
+
revisionMode?: boolean;
|
|
66
73
|
/** Present → drive an auth compile rather than a data compile. */
|
|
67
74
|
authMode?: AuthCliCompileMode;
|
|
68
75
|
/** Auth segments only: resume the same non-interactive Codex session. */
|
|
@@ -143,6 +150,7 @@ async function compileViaCodexCliImpl(
|
|
|
143
150
|
COMPILE_SENTINELS.done,
|
|
144
151
|
COMPILE_SENTINELS.giveUp,
|
|
145
152
|
COMPILE_SENTINELS.checkpoint,
|
|
153
|
+
COMPILE_SENTINELS.verificationState,
|
|
146
154
|
]) {
|
|
147
155
|
const p = pathJoin(opts.absoluteToolDir, name);
|
|
148
156
|
if (existsSync(p)) {
|
|
@@ -199,10 +207,13 @@ async function compileViaCodexCliImpl(
|
|
|
199
207
|
sessionPathAbs,
|
|
200
208
|
'--tool-dir',
|
|
201
209
|
opts.absoluteToolDir,
|
|
210
|
+
'--provider',
|
|
211
|
+
'codex-cli',
|
|
202
212
|
...(opts.candidate ? ['--candidate-json', JSON.stringify(opts.candidate)] : []),
|
|
203
213
|
...(opts.sharedContext ? ['--shared-context-json', JSON.stringify(opts.sharedContext)] : []),
|
|
204
214
|
...(opts.buildPlanPath ? ['--build-plan-path', opts.buildPlanPath] : []),
|
|
205
215
|
...(opts.sharedModules ? ['--shared-modules-json', JSON.stringify(opts.sharedModules)] : []),
|
|
216
|
+
...(opts.revisionMode ? ['--revision-mode'] : []),
|
|
206
217
|
];
|
|
207
218
|
const { assignedSharedModules } = resolvePlanSliceFromFile(
|
|
208
219
|
opts.buildPlanPath,
|
|
@@ -220,6 +231,7 @@ Tool directory: ${opts.absoluteToolDir}
|
|
|
220
231
|
You will write artifacts into the tool directory.
|
|
221
232
|
${formatCandidateContext(opts.candidate, opts.sharedContext, assignedSharedModules)}
|
|
222
233
|
${formatToolPlan(opts.toolPlan)}
|
|
234
|
+
${formatRevisionMode(opts.revisionMode)}
|
|
223
235
|
|
|
224
236
|
Use the imprint-compile MCP tools to inspect the session, write artifacts, run tests, and call done(). Begin by calling read_session_summary, then proceed per the system instructions.`;
|
|
225
237
|
}
|
|
@@ -227,7 +239,9 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
227
239
|
const model = opts.model ?? preferredAgentModel('codex-cli');
|
|
228
240
|
const initialTokenCount = resolveTraceTokenCount(null, initialPrompt);
|
|
229
241
|
const captureLlmIo = traceLlmIoEnabled();
|
|
230
|
-
|
|
242
|
+
// done() owns a separately bounded live-verification phase, so its transport
|
|
243
|
+
// timeout cannot be derived from the compiler's remaining reasoning budget.
|
|
244
|
+
const mcpToolTimeoutSec = MAX_MCP_TOOL_TIMEOUT_SEC;
|
|
231
245
|
setSpanAttributes(traceSpan, {
|
|
232
246
|
...llmSpanAttributes({
|
|
233
247
|
provider: 'codex-cli',
|
|
@@ -324,7 +338,20 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
324
338
|
return finalErrorResult(opts, `failed to send prompt to codex-cli: ${errMsg(err)}`);
|
|
325
339
|
}
|
|
326
340
|
|
|
327
|
-
const
|
|
341
|
+
const terminateOnParentExit = (): void => {
|
|
342
|
+
try {
|
|
343
|
+
signalCodexProcessTree(child, 'SIGTERM');
|
|
344
|
+
} catch {
|
|
345
|
+
// Parent shutdown is best-effort; the OS may already have reaped the child.
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
process.once('exit', terminateOnParentExit);
|
|
349
|
+
let result: CompileAgentResult;
|
|
350
|
+
try {
|
|
351
|
+
result = await driveJsonl(child, opts, traceSpan);
|
|
352
|
+
} finally {
|
|
353
|
+
process.removeListener('exit', terminateOnParentExit);
|
|
354
|
+
}
|
|
328
355
|
const hasActualUsage = result.inputTokens > 0 || result.outputTokens > 0;
|
|
329
356
|
const inputTokenCount = resolveTraceTokenCount(
|
|
330
357
|
hasActualUsage ? result.inputTokens : null,
|
|
@@ -350,14 +377,6 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
350
377
|
return result;
|
|
351
378
|
}
|
|
352
379
|
|
|
353
|
-
function resolveMcpToolTimeoutSec(deadlineMs: number, now = Date.now()): number {
|
|
354
|
-
const remainingSec = Math.ceil(Math.max(0, deadlineMs - now) / 1000);
|
|
355
|
-
return Math.max(
|
|
356
|
-
MIN_MCP_TOOL_TIMEOUT_SEC,
|
|
357
|
-
Math.min(MAX_MCP_TOOL_TIMEOUT_SEC, remainingSec || MIN_MCP_TOOL_TIMEOUT_SEC),
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
380
|
function buildAuthCodexInitialPrompt(systemPrompt: string, initialPrompt: string): string {
|
|
362
381
|
return `<system_instructions>
|
|
363
382
|
${systemPrompt}
|
|
@@ -382,6 +401,15 @@ async function driveJsonl(
|
|
|
382
401
|
writeFileSync(conversationLogPath, JSON.stringify(conversationLog, null, 2), 'utf8');
|
|
383
402
|
} catch {}
|
|
384
403
|
};
|
|
404
|
+
let flushLogTimer: ReturnType<typeof setTimeout> | undefined;
|
|
405
|
+
const scheduleLogFlush = (): void => {
|
|
406
|
+
if (flushLogTimer) return;
|
|
407
|
+
flushLogTimer = setTimeout(() => {
|
|
408
|
+
flushLogTimer = undefined;
|
|
409
|
+
flushLog();
|
|
410
|
+
}, 50);
|
|
411
|
+
flushLogTimer.unref?.();
|
|
412
|
+
};
|
|
385
413
|
const rawStdoutChunks: string[] = [];
|
|
386
414
|
const rawStderrChunks: string[] = [];
|
|
387
415
|
let inputTokens = 0;
|
|
@@ -430,13 +458,26 @@ async function driveJsonl(
|
|
|
430
458
|
}, graceMs);
|
|
431
459
|
forceTimer.unref?.();
|
|
432
460
|
};
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
461
|
+
let childExited = false;
|
|
462
|
+
const scheduleDeadlineCheck = (): ReturnType<typeof setTimeout> => {
|
|
463
|
+
const effectiveDeadlineMs = compileDeadlineAfterVerification(
|
|
464
|
+
opts.absoluteToolDir,
|
|
465
|
+
opts.deadlineMs,
|
|
466
|
+
);
|
|
467
|
+
return setTimeout(
|
|
468
|
+
() => {
|
|
469
|
+
if (childExited) return;
|
|
470
|
+
if (Date.now() < compileDeadlineAfterVerification(opts.absoluteToolDir, opts.deadlineMs)) {
|
|
471
|
+
deadlineTimer = scheduleDeadlineCheck();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
log('wall-clock deadline exceeded, terminating codex');
|
|
475
|
+
terminateChild(5000);
|
|
476
|
+
},
|
|
477
|
+
Math.max(0, effectiveDeadlineMs - Date.now()),
|
|
478
|
+
);
|
|
479
|
+
};
|
|
480
|
+
let deadlineTimer = scheduleDeadlineCheck();
|
|
440
481
|
|
|
441
482
|
let stdoutBuf = '';
|
|
442
483
|
child.stdout?.on('data', (chunk: Buffer) => {
|
|
@@ -459,6 +500,11 @@ async function driveJsonl(
|
|
|
459
500
|
}
|
|
460
501
|
|
|
461
502
|
conversationLog.push(evt);
|
|
503
|
+
// Keep the human-readable compile log current during a long agent turn.
|
|
504
|
+
// Rewriting is throttled so streamed bursts do not cause one full-file
|
|
505
|
+
// write per JSONL event, while a stalled/crashed turn still leaves the
|
|
506
|
+
// latest tool activity visible on disk.
|
|
507
|
+
scheduleLogFlush();
|
|
462
508
|
|
|
463
509
|
if (evt.type === 'thread.started') {
|
|
464
510
|
log(`thread_id=${evt.thread_id ?? '(none)'}`);
|
|
@@ -582,6 +628,7 @@ async function driveJsonl(
|
|
|
582
628
|
finish(-1);
|
|
583
629
|
});
|
|
584
630
|
});
|
|
631
|
+
childExited = true;
|
|
585
632
|
clearTimeout(deadlineTimer);
|
|
586
633
|
if (currentTurnSpan) endTraceSpan(currentTurnSpan);
|
|
587
634
|
for (const span of toolSpans.values()) endTraceSpan(span);
|
|
@@ -597,6 +644,7 @@ async function driveJsonl(
|
|
|
597
644
|
} catch {
|
|
598
645
|
// best effort diagnostics
|
|
599
646
|
}
|
|
647
|
+
if (flushLogTimer) clearTimeout(flushLogTimer);
|
|
600
648
|
flushLog();
|
|
601
649
|
|
|
602
650
|
const verifiedOk =
|
|
@@ -712,7 +760,7 @@ async function driveJsonl(
|
|
|
712
760
|
};
|
|
713
761
|
}
|
|
714
762
|
|
|
715
|
-
if (Date.now() > opts.deadlineMs) {
|
|
763
|
+
if (Date.now() > compileDeadlineAfterVerification(opts.absoluteToolDir, opts.deadlineMs)) {
|
|
716
764
|
return {
|
|
717
765
|
success: false,
|
|
718
766
|
outcome: 'timeout',
|
|
@@ -739,8 +787,59 @@ async function driveJsonl(
|
|
|
739
787
|
};
|
|
740
788
|
}
|
|
741
789
|
|
|
790
|
+
export function collectDescendantPids(
|
|
791
|
+
rows: Array<{ pid: number; ppid: number }>,
|
|
792
|
+
rootPid: number,
|
|
793
|
+
): number[] {
|
|
794
|
+
const childrenByParent = new Map<number, number[]>();
|
|
795
|
+
for (const row of rows) {
|
|
796
|
+
const children = childrenByParent.get(row.ppid) ?? [];
|
|
797
|
+
children.push(row.pid);
|
|
798
|
+
childrenByParent.set(row.ppid, children);
|
|
799
|
+
}
|
|
800
|
+
const descendants: number[] = [];
|
|
801
|
+
const visit = (pid: number): void => {
|
|
802
|
+
for (const childPid of childrenByParent.get(pid) ?? []) {
|
|
803
|
+
visit(childPid);
|
|
804
|
+
descendants.push(childPid);
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
visit(rootPid);
|
|
808
|
+
return descendants;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function liveDescendantPids(rootPid: number): number[] {
|
|
812
|
+
const ps = spawnSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8' });
|
|
813
|
+
if (ps.status !== 0 || typeof ps.stdout !== 'string') return [];
|
|
814
|
+
const rows = ps.stdout
|
|
815
|
+
.split('\n')
|
|
816
|
+
.map((line) => line.trim().split(/\s+/).map(Number))
|
|
817
|
+
.filter((pair): pair is [number, number] => pair.length === 2 && pair.every(Number.isFinite))
|
|
818
|
+
.map(([pid, ppid]) => ({ pid, ppid }));
|
|
819
|
+
return collectDescendantPids(rows, rootPid);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function signalPidOrGroup(pid: number, signal: NodeJS.Signals): void {
|
|
823
|
+
try {
|
|
824
|
+
process.kill(-pid, signal);
|
|
825
|
+
return;
|
|
826
|
+
} catch {
|
|
827
|
+
// The descendant may not lead its own group. Fall back to the process itself.
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
process.kill(pid, signal);
|
|
831
|
+
} catch {
|
|
832
|
+
// already gone
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
742
836
|
function signalCodexProcessTree(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
743
837
|
if (child.pid !== undefined) {
|
|
838
|
+
// Codex-launched MCP servers may create their own process groups. Killing only
|
|
839
|
+
// the detached Codex group leaves those grandchildren running after Ctrl-C,
|
|
840
|
+
// where they can keep compiling and issuing live calls. Snapshot and terminate
|
|
841
|
+
// descendants deepest-first while the parent relationship still exists.
|
|
842
|
+
for (const pid of liveDescendantPids(child.pid)) signalPidOrGroup(pid, signal);
|
|
744
843
|
try {
|
|
745
844
|
process.kill(-child.pid, signal);
|
|
746
845
|
return;
|