imprint-mcp 0.7.0 → 0.7.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.
Files changed (58) hide show
  1. package/README.md +66 -2
  2. package/package.json +1 -1
  3. package/prompts/auth-compile-agent.md +173 -22
  4. package/prompts/compile-agent.md +45 -8
  5. package/prompts/component-recovery.md +40 -0
  6. package/prompts/master-teach-decision.md +28 -3
  7. package/prompts/master-teach-focused-planner.md +18 -5
  8. package/src/cli.ts +26 -18
  9. package/src/imprint/agent-tool.ts +15 -0
  10. package/src/imprint/agent.ts +2 -11
  11. package/src/imprint/audit.ts +35 -13
  12. package/src/imprint/auth-browser-discovery.ts +276 -0
  13. package/src/imprint/auth-compile-agent.ts +513 -224
  14. package/src/imprint/auth-compile-tools.ts +213 -25
  15. package/src/imprint/auth-discovery-evidence.ts +81 -0
  16. package/src/imprint/auth-plan.ts +28 -0
  17. package/src/imprint/auth-verifier.ts +234 -20
  18. package/src/imprint/authentication-interaction.ts +137 -0
  19. package/src/imprint/backend-ladder.ts +271 -43
  20. package/src/imprint/build-plan.ts +2 -22
  21. package/src/imprint/cdp-browser-fetch.ts +242 -60
  22. package/src/imprint/chromium.ts +93 -29
  23. package/src/imprint/claude-cli-compile.ts +2 -0
  24. package/src/imprint/codex-cli-compile.ts +99 -46
  25. package/src/imprint/compile-agent-types.ts +9 -5
  26. package/src/imprint/compile-agent.ts +33 -0
  27. package/src/imprint/compile-tool-executor.ts +783 -0
  28. package/src/imprint/compile-tools.ts +42 -9
  29. package/src/imprint/compile-verification.ts +2 -0
  30. package/src/imprint/compile.ts +3 -0
  31. package/src/imprint/compiler-log.ts +16 -0
  32. package/src/imprint/component-recovery.ts +120 -0
  33. package/src/imprint/cookie-jar.ts +117 -7
  34. package/src/imprint/credential-extract.ts +9 -2
  35. package/src/imprint/credential-lock.ts +147 -0
  36. package/src/imprint/credential-observation.ts +261 -0
  37. package/src/imprint/credential-store.ts +284 -44
  38. package/src/imprint/live-component-recovery.ts +73 -0
  39. package/src/imprint/llm.ts +4 -3
  40. package/src/imprint/log.ts +16 -1
  41. package/src/imprint/login.ts +9 -0
  42. package/src/imprint/master-teach-agent-contracts.ts +40 -3
  43. package/src/imprint/master-teach-agents.ts +229 -14
  44. package/src/imprint/master-teach-checks.ts +8 -1
  45. package/src/imprint/master-teach-controller.ts +681 -138
  46. package/src/imprint/master-teach-prompt-projections.ts +2 -0
  47. package/src/imprint/mcp-cdp-sessions.ts +7 -2
  48. package/src/imprint/mcp-compile-server.ts +62 -850
  49. package/src/imprint/mcp-server.ts +667 -91
  50. package/src/imprint/mcp-session-recovery.ts +167 -0
  51. package/src/imprint/native-teach-agents.ts +232 -108
  52. package/src/imprint/runtime.ts +416 -47
  53. package/src/imprint/stealth-chromium.ts +67 -7
  54. package/src/imprint/teach-browser-resources.ts +88 -0
  55. package/src/imprint/teach-stage-progress.ts +38 -0
  56. package/src/imprint/terminal-user-prompt.ts +42 -0
  57. package/src/imprint/types.ts +51 -18
  58. package/src/imprint/user-interaction.ts +836 -0
@@ -6,12 +6,15 @@ import { type ChildProcess, spawnSync as nodeSpawnSync, spawn } from 'node:child
6
6
  import {
7
7
  closeSync,
8
8
  existsSync,
9
+ mkdtempSync,
9
10
  openSync,
10
11
  readFileSync,
11
12
  readdirSync,
13
+ rmSync,
12
14
  statSync,
13
15
  unlinkSync,
14
16
  } from 'node:fs';
17
+ import { rm } from 'node:fs/promises';
15
18
  import { createRequire } from 'node:module';
16
19
  import { createServer } from 'node:net';
17
20
  import { homedir, tmpdir } from 'node:os';
@@ -26,6 +29,7 @@ import { isDebug } from './log.ts';
26
29
  * Chrome-launch module, a dependency leaf) rather than in backend-ladder so the
27
30
  * ladder→cdp-browser-fetch import does not form a cycle. */
28
31
  const chromeProcessPids = new Set<number>();
32
+ const chromeTemporaryProfiles = new Map<number, string>();
29
33
 
30
34
  /** Register a Chrome PID for exit-time cleanup. Called on launch. */
31
35
  export function __registerChromePid(pid: number): void {
@@ -47,8 +51,16 @@ process.on('exit', () => {
47
51
  for (const pid of chromeProcessPids) {
48
52
  try {
49
53
  process.kill(pid, 'SIGKILL');
50
- } catch {
51
- // Already dead or not owned by this process — ignore.
54
+ } catch (error) {
55
+ if ((error as NodeJS.ErrnoException).code !== 'ESRCH') continue;
56
+ }
57
+ const profile = chromeTemporaryProfiles.get(pid);
58
+ if (profile) {
59
+ try {
60
+ rmSync(profile, { recursive: true, force: true });
61
+ } catch {
62
+ // A forced host exit cannot await cleanup; normal close reports errors.
63
+ }
52
64
  }
53
65
  }
54
66
  });
@@ -556,17 +568,18 @@ async function pickFreePort(): Promise<number> {
556
568
  });
557
569
  }
558
570
 
559
- async function waitForCdp(port: number, timeoutMs = 10_000): Promise<void> {
571
+ async function waitForCdp(port: number, timeoutMs = 10_000, signal?: AbortSignal): Promise<void> {
560
572
  const deadline = Date.now() + timeoutMs;
561
573
  let lastError: unknown;
562
574
  while (Date.now() < deadline) {
575
+ signal?.throwIfAborted();
563
576
  try {
564
- const res = await fetch(`http://127.0.0.1:${port}/json/version`);
577
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, { signal });
565
578
  if (res.ok) return;
566
579
  } catch (err) {
567
580
  lastError = err;
568
581
  }
569
- await sleep(100);
582
+ await sleep(100, undefined, { signal });
570
583
  }
571
584
  throw new Error(
572
585
  `Chromium did not open CDP on port ${port} within ${timeoutMs}ms (${String(lastError)})`,
@@ -578,8 +591,9 @@ export async function launchChromium(opts: LaunchOptions = {}): Promise<Launched
578
591
  log: (message) => process.stderr.write(`[imprint] ${message}\n`),
579
592
  }).path;
580
593
  const port = opts.port ?? (await pickFreePort());
594
+ const ownsProfile = opts.userDataDir === undefined;
581
595
  const userDataDir =
582
- opts.userDataDir ?? pathJoin(tmpdir(), `imprint-chrome-${Date.now()}-${process.pid}`);
596
+ opts.userDataDir ?? mkdtempSync(pathJoin(tmpdir(), `imprint-chrome-${process.pid}-`));
583
597
 
584
598
  const args = [
585
599
  `--remote-debugging-port=${port}`,
@@ -614,38 +628,88 @@ export async function launchChromium(opts: LaunchOptions = {}): Promise<Launched
614
628
  let xvfb: XvfbHandle | undefined;
615
629
  let display = opts.display ?? process.env.DISPLAY;
616
630
  if (process.platform === 'linux' && !opts.headless && !display) {
617
- xvfb = await startXvfb();
618
- display = xvfb.display;
631
+ try {
632
+ xvfb = await startXvfb();
633
+ display = xvfb.display;
634
+ } catch (error) {
635
+ if (ownsProfile) await rm(userDataDir, { recursive: true, force: true });
636
+ throw error;
637
+ }
619
638
  }
620
639
 
621
- const child = spawn(exe, args, {
622
- stdio: ['ignore', 'ignore', 'pipe'],
623
- detached: false,
624
- env: display ? { ...process.env, DISPLAY: display } : process.env,
625
- });
640
+ let child: ChildProcess;
641
+ try {
642
+ child = spawn(exe, args, {
643
+ stdio: ['ignore', 'ignore', 'pipe'],
644
+ detached: false,
645
+ env: display ? { ...process.env, DISPLAY: display } : process.env,
646
+ });
647
+ } catch (error) {
648
+ await xvfb?.close().catch(() => {});
649
+ if (ownsProfile) await rm(userDataDir, { recursive: true, force: true });
650
+ throw error;
651
+ }
652
+ if (child.pid !== undefined) {
653
+ __registerChromePid(child.pid);
654
+ if (ownsProfile) chromeTemporaryProfiles.set(child.pid, userDataDir);
655
+ }
626
656
 
627
657
  // Chromium is noisy — only surface stderr under IMPRINT_DEBUG.
628
658
  if (isDebug()) {
629
659
  child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
630
660
  }
631
661
 
632
- const ready = waitForCdp(port);
633
-
634
- let closed = false;
635
- const close = async (): Promise<void> => {
636
- if (closed) return;
637
- closed = true;
638
- if (child.exitCode === null && child.signalCode === null) {
639
- child.kill('SIGTERM');
640
- await Promise.race([
641
- new Promise<void>((resolve) => child.once('exit', () => resolve())),
642
- sleep(2000),
643
- ]);
644
- if (child.exitCode === null) child.kill('SIGKILL');
645
- }
646
- // Tear down the virtual display we started for this launch (if any).
647
- await xvfb?.close().catch(() => {});
662
+ const startup = new AbortController();
663
+ let stopped = false;
664
+ let spawnError: Error | undefined;
665
+ const exit = Promise.withResolvers<void>();
666
+ let closing: Promise<void> | undefined;
667
+ const close = (): Promise<void> => {
668
+ closing ??= (async () => {
669
+ startup.abort(new Error('Chromium closed before CDP became ready'));
670
+ if (!stopped) {
671
+ child.kill('SIGTERM');
672
+ await Promise.race([exit.promise, sleep(2000)]);
673
+ if (!stopped) {
674
+ child.kill('SIGKILL');
675
+ await Promise.race([exit.promise, sleep(2000)]);
676
+ }
677
+ }
678
+ await xvfb?.close().catch(() => {});
679
+ if (!stopped) throw new Error('Chromium did not exit; preserving its profile');
680
+ if (ownsProfile) await rm(userDataDir, { recursive: true, force: true });
681
+ if (child.pid !== undefined) {
682
+ chromeTemporaryProfiles.delete(child.pid);
683
+ __unregisterChromePid(child.pid);
684
+ }
685
+ })();
686
+ return closing;
648
687
  };
688
+ child.once('error', (error) => {
689
+ spawnError = error;
690
+ if (child.pid === undefined) {
691
+ stopped = true;
692
+ exit.resolve();
693
+ }
694
+ startup.abort(error);
695
+ });
696
+ child.once('exit', () => {
697
+ stopped = true;
698
+ if (child.pid !== undefined) __unregisterChromePid(child.pid);
699
+ exit.resolve();
700
+ void close().catch((error) => {
701
+ process.stderr.write(`[imprint] browser cleanup: ${String(error)}\n`);
702
+ });
703
+ });
704
+ const ready = Promise.race([
705
+ waitForCdp(port, 10_000, startup.signal),
706
+ exit.promise.then(() => {
707
+ throw spawnError ?? new Error('Chromium exited before CDP became ready');
708
+ }),
709
+ ]).catch(async (error) => {
710
+ await close();
711
+ throw error;
712
+ });
649
713
 
650
714
  return { process: child, port, userDataDir, ready, close };
651
715
  }
@@ -85,6 +85,8 @@ const MAX_VERIFICATION_CYCLES = 5;
85
85
  /** Data-compile tools exposed to claude-cli. Keep this list in one exported
86
86
  * place so tests catch prompt/tool drift before a real teach run does. */
87
87
  export const DATA_COMPILE_TOOL_NAMES = [
88
+ 'prompt_user',
89
+ 'authenticate',
88
90
  'read_session_summary',
89
91
  'search_requests',
90
92
  'read_request',
@@ -1,5 +1,7 @@
1
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';
3
+ import { AuthToolPlanSchema } from './auth-plan.ts';
4
+ import { createCompileToolExecutor } from './compile-tool-executor.ts';
3
5
  import { currentNativeTeachAgents } from './native-teach-agents.ts';
4
6
  import { SHARED_RESEARCH_INSTRUCTIONS } from './teach-research-memory.ts';
5
7
  /**
@@ -12,17 +14,14 @@ import { SHARED_RESEARCH_INSTRUCTIONS } from './teach-research-memory.ts';
12
14
  */
13
15
 
14
16
  import type { ChildProcess } from 'node:child_process';
15
- import {
16
- appendFileSync,
17
- existsSync,
18
- mkdirSync,
19
- readFileSync,
20
- unlinkSync,
21
- writeFileSync,
22
- } from 'node:fs';
17
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
23
18
  import { isAbsolute as pathIsAbsolute, join as pathJoin } from 'node:path';
24
19
  import { type Span, context as otelContext } from '@opentelemetry/api';
25
- import type { AuthCliCompileMode } from './auth-compile-tools.ts';
20
+ import {
21
+ AUTH_CHECKPOINT_TOOLS,
22
+ type AuthCliCompileMode,
23
+ parseAuthCheckpoint,
24
+ } from './auth-compile-tools.ts';
26
25
  import { type SharedModuleManifestEntry, resolvePlanSliceFromFile } from './build-plan.ts';
27
26
  import type {
28
27
  AuthCheckpoint,
@@ -43,7 +42,7 @@ import {
43
42
  watchCompileProviderDeadline,
44
43
  } from './compile-provider-control.ts';
45
44
  import type { CompileStrategyKind } from './compile-strategy.ts';
46
- import { recordCompilerHostError } from './compiler-log.ts';
45
+ import { compilerToolFailure, recordCompilerHostError } from './compiler-log.ts';
47
46
  import {
48
47
  collectOwnedProcess,
49
48
  spawnOwnedProcess,
@@ -392,28 +391,30 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
392
391
 
393
392
  const family = currentNativeTeachAgents();
394
393
  if (family) {
395
- const client = new Client({ name: 'imprint-native-compiler', version: '1.0.0' });
396
- const providerControl = createCompileProviderControl(runDeadline ?? opts.deadlineMs);
397
- const env = Object.fromEntries(
398
- Object.entries({ ...process.env, ...providerControl.env }).filter(
399
- (entry): entry is [string, string] =>
400
- entry[0] !== 'IMPRINT_TEACH_CREDENTIALS' && entry[1] !== undefined,
401
- ),
394
+ const executor = createCompileToolExecutor(
395
+ {
396
+ sessionPath: sessionPathAbs,
397
+ toolDir: opts.absoluteToolDir,
398
+ site: opts.authMode?.site,
399
+ authToolPlan: opts.authMode
400
+ ? AuthToolPlanSchema.parse(JSON.parse(opts.authMode.authPlanJson))
401
+ : undefined,
402
+ provider: 'codex-cli',
403
+ candidate: opts.candidate,
404
+ sharedContext: opts.sharedContext,
405
+ buildPlanPath: opts.buildPlanPath,
406
+ sharedModules: opts.sharedModules,
407
+ strategyKind: opts.strategyKind,
408
+ revisionMode: opts.revisionMode,
409
+ verificationMode: opts.verificationMode,
410
+ sharedTriageSelection: opts.sharedTriageSelection,
411
+ },
412
+ { session: opts.session, signal: opts.signal, runDeadline },
402
413
  );
403
- const transport = new StdioClientTransport({
404
- env,
405
- command: bunPath,
406
- args: mcpArgs,
407
- cwd: opts.absoluteToolDir,
408
- stderr: 'pipe',
409
- });
410
414
  const conversationLogPath = pathJoin(opts.absoluteToolDir, '.compile-log.json');
411
415
  const events: unknown[] = existsSync(conversationLogPath)
412
416
  ? JSON.parse(readFileSync(conversationLogPath, 'utf8'))
413
417
  : [];
414
- transport.stderr?.on('data', (chunk) =>
415
- appendFileSync(pathJoin(opts.absoluteToolDir, '.native-mcp-stderr.log'), chunk),
416
- );
417
418
  const sharedResearch = family.sharedResearch
418
419
  ? {
419
420
  runId: family.sharedResearch.runId,
@@ -428,30 +429,83 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
428
429
  ? `${SHARED_RESEARCH_INSTRUCTIONS}\n${JSON.stringify({ sharedResearch })}\nUse the native shared_research tool with this exchange schema to publish/query findings; do not embed publications in your final compiler summary.\n`
429
430
  : '';
430
431
  try {
431
- await client.connect(transport);
432
432
  const response = await family.submit(
433
- `${sharedPrompt}${initialPrompt}\n\nNative compiler: use list_assignment_tools and call_assignment_tool for this assignment in place of direct imprint-compile MCP names. All writes and tests must go through those tools. After done, give_up, or an auth checkpoint returns, submit a short summary to the host and stop this turn. Native subagents can assist with independent implementation questions; retain separate workspaces and do not substitute their prose for test evidence.`,
433
+ `${sharedPrompt}${initialPrompt}\n\nNative compiler: use list_assignment_tools and call_assignment_tool for this assignment in place of direct imprint-compile MCP names. All writes and tests must go through those tools. ${opts.authMode?.executeCheckpoint ? 'Auth host tools execute inline: await each verification, browser operation, human question, or cooldown result and continue this same assignment. Submit a short summary only after done or give_up returns.' : 'After done, give_up, or a recorded auth checkpoint returns, submit a short summary to the host and stop this turn.'} Native subagents can assist with independent implementation questions; retain separate workspaces and do not substitute their prose for test evidence.`,
434
434
  {
435
435
  // The family already scopes this key to the run. Revision directories
436
436
  // change; the public tool's retained compiler conversation must not.
437
437
  conversation: `compiler:${opts.candidate?.toolName ?? opts.absoluteToolDir}`,
438
+ contextRef: opts.authMode?.contextRef,
438
439
  signal: opts.signal,
439
- call: async (name, args) => {
440
- const result =
441
- name === '__list'
442
- ? await client.listTools()
443
- : await client.callTool({ name, arguments: args }, undefined, {
444
- signal: opts.signal,
445
- timeout: MAX_MCP_TOOL_TIMEOUT_SEC * 1000,
446
- });
447
- events.push({ timestamp: new Date().toISOString(), name, args, result });
448
- writeFileSync(conversationLogPath, JSON.stringify(events), { mode: 0o600 });
449
- return result;
440
+ call: async (name, args, requestSignal) => {
441
+ const signal = requestSignal ?? opts.signal;
442
+ const requestId = randomUUID();
443
+ const started = performance.now();
444
+ const save = () =>
445
+ writeFileSync(conversationLogPath, JSON.stringify(events), { mode: 0o600 });
446
+ events.push({
447
+ type: 'tool.started',
448
+ timestamp: new Date().toISOString(),
449
+ name,
450
+ requestId,
451
+ });
452
+ save();
453
+ let failure: ReturnType<typeof compilerToolFailure> | undefined;
454
+ let result: CallToolResult | { tools: Tool[] };
455
+ try {
456
+ if (name === '__list') {
457
+ const listed = { tools: executor.tools };
458
+ result = opts.authMode?.executeCheckpoint
459
+ ? {
460
+ ...listed,
461
+ tools: listed.tools.map((tool) => {
462
+ const checkpoint = AUTH_CHECKPOINT_TOOLS.find(
463
+ (definition) => definition.name === tool.name,
464
+ );
465
+ return checkpoint
466
+ ? {
467
+ ...tool,
468
+ description: `${checkpoint.description} Await the host result and continue in this same assignment.`,
469
+ }
470
+ : tool;
471
+ }),
472
+ }
473
+ : listed;
474
+ } else if (
475
+ opts.authMode?.executeCheckpoint &&
476
+ AUTH_CHECKPOINT_TOOLS.some((tool) => tool.name === name)
477
+ ) {
478
+ const executed = await opts.authMode.executeCheckpoint(
479
+ parseAuthCheckpoint(name, args),
480
+ signal,
481
+ );
482
+ result = {
483
+ content: [{ type: 'text', text: executed.result }],
484
+ ...(executed.isError ? { isError: true } : {}),
485
+ };
486
+ } else {
487
+ result = await executor.callTool(name, args, signal);
488
+ }
489
+ events.push({ timestamp: new Date().toISOString(), name, args, result });
490
+ return result;
491
+ } catch (error) {
492
+ failure = compilerToolFailure(error);
493
+ throw error;
494
+ } finally {
495
+ events.push({
496
+ type: 'tool.settled',
497
+ timestamp: new Date().toISOString(),
498
+ name,
499
+ requestId,
500
+ durationMs: performance.now() - started,
501
+ status: signal?.aborted ? 'cancelled' : failure ? 'threw' : 'returned',
502
+ ...(failure ? { error: failure } : {}),
503
+ });
504
+ save();
505
+ }
450
506
  },
451
507
  },
452
508
  );
453
- const interruption = providerControl.interruption();
454
- if (interruption) throw compileProviderInterruptionError(interruption);
455
509
  events.push({ type: 'native.completed', ...response });
456
510
  writeFileSync(conversationLogPath, JSON.stringify(events), { mode: 0o600 });
457
511
  return completedCompileResult(opts, {
@@ -477,8 +531,7 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
477
531
  ),
478
532
  });
479
533
  } finally {
480
- providerControl.dispose();
481
- await client.close();
534
+ await executor.close();
482
535
  }
483
536
  }
484
537
 
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { AgentProgress } from './agent.ts';
10
+ import type { AuthBrowserAction } from './auth-browser-discovery.ts';
10
11
  import { type AssignedSharedModule, describeAssignedModules } from './build-plan.ts';
11
12
  import type { ProviderInterruptionReason, ProviderReportedError } from './provider-retry.ts';
12
13
  import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
@@ -123,12 +124,10 @@ export interface CompileAgentProgress extends AgentProgress {
123
124
  };
124
125
  }
125
126
 
126
- /** A mid-loop checkpoint the auth compile agent reaches: it calls a checkpoint
127
- * tool (which writes a sentinel) and then STOPS its turn. The orchestrator
128
- * (teach) performs the action — it owns the live browser session, the TUI, and
129
- * the cooldown — then resumes the agent (`claude --resume`) with the result as a
130
- * follow-up user message. Site/channel-agnostic. */
127
+ /** A host operation shared by inline native/API tools and CLI checkpoint/resume.
128
+ * The host owns the browser, prompt references, and declared continuation. */
131
129
  export type AuthCheckpoint =
130
+ | ({ kind: 'interact_verification_browser' } & AuthBrowserAction)
132
131
  | {
133
132
  kind: 'run_verification';
134
133
  action: string;
@@ -144,6 +143,11 @@ export type AuthCheckpoint =
144
143
  | { kind: 'prompt_user'; message: string; options?: string[] }
145
144
  | { kind: 'wait_for_cooldown'; minutes: number; reason?: string };
146
145
 
146
+ export interface AuthCheckpointResult {
147
+ result: string;
148
+ isError?: boolean;
149
+ }
150
+
147
151
  export interface CompileAgentResult {
148
152
  /** True only if external verification passed. */
149
153
  success: boolean;
@@ -16,6 +16,7 @@ import {
16
16
  giveUpTool,
17
17
  runAgentLoop,
18
18
  } from './agent.ts';
19
+ import { attachRecordedAuthentication } from './authentication-interaction.ts';
19
20
  import { type SharedModuleManifestEntry, resolvePlanSliceFromFile } from './build-plan.ts';
20
21
  import { compileViaClaudeCli } from './claude-cli-compile.ts';
21
22
  import { compileViaCodexCli } from './codex-cli-compile.ts';
@@ -70,6 +71,12 @@ import {
70
71
  minimalSharedTriageSelection,
71
72
  } from './triage-selection.ts';
72
73
  import { type Session, SessionSchema, WorkflowSchema } from './types.ts';
74
+ import {
75
+ type OnUserPrompt,
76
+ UserInteraction,
77
+ currentUserInteraction,
78
+ withUserInteraction,
79
+ } from './user-interaction.ts';
73
80
 
74
81
  export type { CompileAgentProgress } from './compile-agent-types.ts';
75
82
 
@@ -105,6 +112,7 @@ function removeEphemeralTests(toolDir: string): void {
105
112
  }
106
113
 
107
114
  interface CompileAgentOptions {
115
+ onPrompt?: OnUserPrompt;
108
116
  /** Path to the recorded session JSON (absolute or relative). */
109
117
  sessionPath: string;
110
118
  /** Hard wall-clock budget. Default 20 minutes. */
@@ -172,6 +180,11 @@ function formatRevisionMode(enabled: boolean | undefined): string {
172
180
  }
173
181
 
174
182
  export async function compileAgent(opts: CompileAgentOptions): Promise<CompileAgentResult> {
183
+ if (!currentUserInteraction()) {
184
+ return await withUserInteraction(new UserInteraction(opts.onPrompt, opts.signal), () =>
185
+ compileAgent(opts),
186
+ );
187
+ }
175
188
  const startTime = Date.now();
176
189
  // Build-plan fields are bounded proposals for the compile/master agents.
177
190
  const { assignedSharedModules } = resolvePlanSliceFromFile(
@@ -192,6 +205,12 @@ export async function compileAgent(opts: CompileAgentOptions): Promise<CompileAg
192
205
  'session',
193
206
  );
194
207
 
208
+ const extractedAuthValues = Object.fromEntries(
209
+ extractCredentials(session).replacements.flatMap(({ originalValue, placeholder }) => {
210
+ const match = /^\$\{credential\.([^}]+)\}$/.exec(placeholder);
211
+ return match?.[1] ? [[match[1], originalValue]] : [];
212
+ }),
213
+ );
195
214
  // 2. Auto-redact raw direct-generate inputs. Teach normally supplies an
196
215
  // already-redacted focused recording.
197
216
  const looksRedacted = JSON.stringify(session).includes('[REDACTED:');
@@ -265,6 +284,7 @@ export async function compileAgent(opts: CompileAgentOptions): Promise<CompileAg
265
284
  sharedModules: opts.sharedModules,
266
285
  strategyKind: opts.strategyKind,
267
286
  revisionMode: opts.revisionMode,
287
+ signal: opts.signal,
268
288
  }),
269
289
  doneTool(),
270
290
  giveUpTool(),
@@ -289,6 +309,19 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
289
309
  opts.deadlineMs ?? Date.now() + (opts.maxDurationMs ?? 20 * 60 * 1000),
290
310
  );
291
311
 
312
+ if (!currentUserInteraction()?.hasAuthentication) {
313
+ currentUserInteraction()?.setRunControl(runDeadline, opts.onDeadlineReached);
314
+ attachRecordedAuthentication({
315
+ session,
316
+ sessionPath: sessionPathAbs,
317
+ root: pathJoin(absoluteToolDir, '.authentication'),
318
+ values: { ...extractedAuthValues, ...opts.teachCredentials?.values },
319
+ llmConfig: opts.llmConfig,
320
+ runDeadline,
321
+ report: log,
322
+ });
323
+ }
324
+
292
325
  // 8. Instantiate provider (or use injected one for testing).
293
326
  // CLI providers take a different path: they don't implement Anthropic
294
327
  // messageWithTools, so we shell out with the same toolset registered as a