@adhdev/daemon-core 0.7.46 → 0.8.1

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.
@@ -13,6 +13,48 @@ import type * as http from 'http';
13
13
  import type { DevServerContext, ProviderCategory } from './dev-server-types.js';
14
14
  import { DEV_SERVER_PORT } from './dev-server.js';
15
15
  import { LOG } from '../logging/logger.js';
16
+ import { runCliAutoImplVerification } from './dev-cli-debug.js';
17
+
18
+ type CliExerciseVerification = {
19
+ request?: Record<string, any>;
20
+ mustContainAny?: string[];
21
+ mustNotContainAny?: string[];
22
+ mustMatchAny?: string[];
23
+ mustNotMatchAny?: string[];
24
+ lastAssistantMustContainAny?: string[];
25
+ lastAssistantMustNotContainAny?: string[];
26
+ lastAssistantMustMatchAny?: string[];
27
+ lastAssistantMustNotMatchAny?: string[];
28
+ inspectFields?: string[];
29
+ description?: string;
30
+ fixtureName?: string;
31
+ fixtureNames?: string[];
32
+ };
33
+
34
+ function getAutoImplPid(ctx: DevServerContext): number | null {
35
+ const proc: any = ctx.autoImplProcess;
36
+ return proc && typeof proc.pid === 'number' && proc.pid > 0 ? proc.pid : null;
37
+ }
38
+
39
+ function isPidAlive(pid: number): boolean {
40
+ try {
41
+ process.kill(pid, 0);
42
+ return true;
43
+ } catch (error: any) {
44
+ return error?.code === 'EPERM';
45
+ }
46
+ }
47
+
48
+ function clearStaleAutoImplState(ctx: DevServerContext, reason: string): void {
49
+ if (!ctx.autoImplStatus.running && !ctx.autoImplProcess) return;
50
+
51
+ const pid = getAutoImplPid(ctx);
52
+ if (pid && isPidAlive(pid)) return;
53
+
54
+ ctx.log(`Clearing stale auto-implement state: ${reason}${pid ? ` (pid ${pid})` : ''}`);
55
+ ctx.autoImplProcess = null;
56
+ ctx.autoImplStatus.running = false;
57
+ }
16
58
 
17
59
  export function getDefaultAutoImplReference(ctx: DevServerContext, category: string, type: string): string {
18
60
  if (category === 'cli') {
@@ -118,12 +160,21 @@ export function loadAutoImplReferenceScripts(ctx: DevServerContext, referenceTyp
118
160
 
119
161
  export async function handleAutoImplement(ctx: DevServerContext, type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
120
162
  const body = await ctx.readBody(req);
121
- const { agent = 'claude-cli', functions, reference, model, comment, providerDir: requestedProviderDir } = body;
163
+ const {
164
+ agent = 'claude-cli',
165
+ functions,
166
+ reference,
167
+ model,
168
+ comment,
169
+ providerDir: requestedProviderDir,
170
+ verification,
171
+ } = body;
122
172
  if (!functions || !Array.isArray(functions) || functions.length === 0) {
123
173
  ctx.json(res, 400, { error: 'functions[] is required (e.g. ["readChat", "sendMessage"])' });
124
174
  return;
125
175
  }
126
176
 
177
+ clearStaleAutoImplState(ctx, 'new auto-implement request');
127
178
  if (ctx.autoImplStatus.running) {
128
179
  ctx.json(res, 409, { error: 'Auto-implement already in progress', type: ctx.autoImplStatus.type });
129
180
  return;
@@ -141,7 +192,57 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
141
192
  }
142
193
  const providerDir = writableProvider.dir;
143
194
 
195
+ ctx.autoImplStatus = { running: false, type, progress: [] };
196
+
197
+ if (provider.category === 'cli' && verification && (verification.fixtureName || (verification.fixtureNames && verification.fixtureNames.length > 0))) {
198
+ sendAutoImplSSE(ctx, {
199
+ event: 'progress',
200
+ data: {
201
+ function: '_preflight',
202
+ status: 'verifying',
203
+ message: 'Running preflight verification before spawning agent...',
204
+ }
205
+ });
206
+ try {
207
+ const preflight = await runCliAutoImplVerification(ctx, type, verification);
208
+ sendAutoImplSSE(ctx, { event: 'verification', data: preflight });
209
+ if (preflight.pass) {
210
+ sendAutoImplSSE(ctx, {
211
+ event: 'complete',
212
+ data: {
213
+ success: true,
214
+ exitCode: 0,
215
+ functions,
216
+ message: `✅ No-op: exact ${preflight.mode} already passes`,
217
+ verification: preflight,
218
+ skipped: true,
219
+ },
220
+ });
221
+ ctx.json(res, 200, {
222
+ started: false,
223
+ skipped: true,
224
+ type,
225
+ functions,
226
+ providerDir,
227
+ verification: preflight,
228
+ message: 'Preflight verification already passes. No auto-implement run needed.',
229
+ });
230
+ return;
231
+ }
232
+ } catch (error: any) {
233
+ sendAutoImplSSE(ctx, {
234
+ event: 'progress',
235
+ data: {
236
+ function: '_preflight',
237
+ status: 'verify_failed',
238
+ message: `Preflight verification errored, continuing to agent run: ${error?.message || error}`,
239
+ }
240
+ });
241
+ }
242
+ }
243
+
144
244
  try {
245
+ ctx.autoImplStatus = { running: true, type, progress: ctx.autoImplStatus.progress };
145
246
  // 1. Collect DOM context
146
247
  // 1. Skip heavy DOM pre-parsing (Agent will use cURL to explore via CDP!)
147
248
  const resolvedReference = resolveAutoImplReference(ctx, provider.category, reference, type);
@@ -170,7 +271,7 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
170
271
  const referenceScripts = loadAutoImplReferenceScripts(ctx, resolvedReference);
171
272
 
172
273
  // 3. Build the prompt
173
- const prompt = buildAutoImplPrompt(ctx, type, provider, providerDir, functions, domContext, referenceScripts, comment, resolvedReference);
274
+ const prompt = buildAutoImplPrompt(ctx, type, provider, providerDir, functions, domContext, referenceScripts, comment, resolvedReference, verification);
174
275
 
175
276
  // 4. Write prompt to temp file (avoids shell escaping issues with special chars)
176
277
  const tmpDir = path.join(os.tmpdir(), 'adhdev-autoimpl');
@@ -193,7 +294,8 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
193
294
  // ─── ACP Agent: use ACP SDK (JSON-RPC protocol) ───
194
295
  if (agentCategory === 'acp') {
195
296
  sendAutoImplSSE(ctx, { event: 'progress', data: { function: '_init', status: 'spawning', message: `Spawning ACP agent: ${spawn.command} ${(spawn.args || []).join(' ')}` } });
196
- ctx.autoImplStatus = { running: true, type, progress: [] };
297
+ ctx.autoImplStatus.running = true;
298
+ ctx.autoImplStatus.type = type;
197
299
 
198
300
  // Dynamic import ACP SDK
199
301
  const { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION } = await import('@agentclientprotocol/sdk');
@@ -366,7 +468,8 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
366
468
 
367
469
  sendAutoImplSSE(ctx, { event: 'progress', data: { function: '_init', status: 'spawning', message: `Spawning agent: ${shellCmd.substring(0, 200)}... (prompt: ${prompt.length} chars)` } });
368
470
 
369
- ctx.autoImplStatus = { running: true, type, progress: [] };
471
+ ctx.autoImplStatus.running = true;
472
+ ctx.autoImplStatus.type = type;
370
473
  const spawnedAt = Date.now();
371
474
 
372
475
  let child: any;
@@ -412,6 +515,7 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
412
515
  let approvalKeys: Record<number, string> = { 0: 'y\r' };
413
516
  let approvalBuffer = '';
414
517
  let lastApprovalTime = 0;
518
+ let completionSignalSeen = false;
415
519
 
416
520
  try {
417
521
  const { normalizeCliProviderForRuntime } = await import('../cli-adapters/provider-cli-adapter.js');
@@ -435,6 +539,7 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
435
539
  // Force exit on completion signal (check cleanData directly to avoid stale buffer echo matches)
436
540
  const elapsed = Date.now() - spawnedAt;
437
541
  if (elapsed > 15000 && cleanData.includes('_PIPELINE_COMPLETE_SIGNAL_')) {
542
+ completionSignalSeen = true;
438
543
  ctx.log(`Agent finished task after ${Math.round(elapsed/1000)}s. Terminating interactive CLI session to unblock pipeline.`);
439
544
  sendAutoImplSSE(ctx, { event: 'output', data: { chunk: `\n[🤖 ADHDev Pipeline] Completion token detected. Proceeding...\n`, stream: 'stdout' } });
440
545
  approvalBuffer = '';
@@ -461,6 +566,57 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
461
566
  }
462
567
  };
463
568
 
569
+ const finalizeCliAutoImpl = async (code: number | null) => {
570
+ ctx.autoImplProcess = null;
571
+ let success = completionSignalSeen || code === 0;
572
+ let message = success
573
+ ? (completionSignalSeen && code !== 0 ? '✅ Auto-implement complete (completion signal)' : '✅ Auto-implement complete')
574
+ : `❌ Agent exited (code: ${code})`;
575
+ let verificationSummary: any = null;
576
+
577
+ try { ctx.providerLoader.reload(); } catch { /* ignore */ }
578
+
579
+ if (provider.category === 'cli' && verification) {
580
+ sendAutoImplSSE(ctx, {
581
+ event: 'progress',
582
+ data: {
583
+ function: '_verify',
584
+ status: 'running',
585
+ message: 'Running exact post-patch verification...',
586
+ },
587
+ });
588
+ try {
589
+ verificationSummary = await runCliAutoImplVerification(ctx, type, verification);
590
+ sendAutoImplSSE(ctx, { event: 'verification', data: verificationSummary });
591
+ success = verificationSummary.pass;
592
+ message = verificationSummary.pass
593
+ ? `✅ Auto-implement complete (${verificationSummary.mode})`
594
+ : `❌ Post-patch verification failed (${verificationSummary.mode}): ${verificationSummary.failures.join('; ') || 'unknown failure'}`;
595
+ } catch (error: any) {
596
+ success = false;
597
+ message = `❌ Post-patch verification error: ${error?.message || error}`;
598
+ sendAutoImplSSE(ctx, {
599
+ event: 'verification',
600
+ data: { pass: false, error: error?.message || String(error) },
601
+ });
602
+ }
603
+ }
604
+
605
+ ctx.autoImplStatus.running = false;
606
+ sendAutoImplSSE(ctx, {
607
+ event: 'complete',
608
+ data: {
609
+ success,
610
+ exitCode: code,
611
+ functions,
612
+ message,
613
+ verification: verificationSummary,
614
+ },
615
+ });
616
+ try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
617
+ ctx.log(`Auto-implement ${success ? 'completed' : 'failed'}: ${type} (exit: ${code})${verificationSummary ? ` verify=${verificationSummary.pass ? 'pass' : 'fail'}` : ''}`);
618
+ };
619
+
464
620
  if (isPty) {
465
621
  child.onData((data: string) => {
466
622
  stdout += data;
@@ -472,15 +628,7 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
472
628
  sendAutoImplSSE(ctx, { event: 'output', data: { chunk: data, stream: 'stdout' } });
473
629
  });
474
630
  child.onExit(({ exitCode: code }: { exitCode: number }) => {
475
- ctx.autoImplProcess = null;
476
- ctx.autoImplStatus.running = false;
477
- const success = code === 0;
478
- sendAutoImplSSE(ctx, {
479
- event: 'complete',
480
- data: { success, exitCode: code, functions, message: success ? '✅ Auto-implement complete' : `❌ Agent exited (code: ${code})` },
481
- });
482
- try { ctx.providerLoader.reload(); } catch { /* ignore */ }
483
- try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
631
+ void finalizeCliAutoImpl(code);
484
632
  });
485
633
  } else {
486
634
  child.stdout?.on('data', (d: Buffer) => {
@@ -497,21 +645,7 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
497
645
  sendAutoImplSSE(ctx, { event: 'output', data: { chunk, stream: 'stderr' } });
498
646
  });
499
647
  child.on('exit', (code: number) => {
500
- ctx.autoImplProcess = null;
501
- ctx.autoImplStatus.running = false;
502
- const success = code === 0;
503
- sendAutoImplSSE(ctx, {
504
- event: 'complete',
505
- data: {
506
- success,
507
- exitCode: code,
508
- functions,
509
- message: success ? '✅ Auto-implement complete' : `❌ Agent exited (code: ${code})`,
510
- },
511
- });
512
- try { ctx.providerLoader.reload(); } catch { /* ignore */ }
513
- try { fs.unlinkSync(promptFile); } catch { /* ignore */ }
514
- ctx.log(`Auto-implement ${success ? 'completed' : 'failed'}: ${type} (exit: ${code})`);
648
+ void finalizeCliAutoImpl(code);
515
649
  });
516
650
  }
517
651
  ctx.json(res, 202, {
@@ -538,9 +672,10 @@ export function buildAutoImplPrompt(ctx: DevServerContext,
538
672
  referenceScripts: Record<string, string>,
539
673
  userComment?: string,
540
674
  referenceType?: string | null,
675
+ verification?: CliExerciseVerification,
541
676
  ): string {
542
677
  if (provider.category === 'cli') {
543
- return buildCliAutoImplPrompt(ctx, type, provider, providerDir, functions, referenceScripts, userComment, referenceType);
678
+ return buildCliAutoImplPrompt(ctx, type, provider, providerDir, functions, referenceScripts, userComment, referenceType, verification);
544
679
  }
545
680
 
546
681
  const lines: string[] = [];
@@ -833,8 +968,74 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
833
968
  referenceScripts: Record<string, string>,
834
969
  userComment?: string,
835
970
  referenceType?: string | null,
971
+ verification?: CliExerciseVerification,
836
972
  ): string {
837
973
  const lines: string[] = [];
974
+ const defaultExercisePayload = {
975
+ type,
976
+ workingDir: providerDir,
977
+ freshSession: true,
978
+ autoLaunch: true,
979
+ autoResolveApprovals: true,
980
+ approvalButtonIndex: 0,
981
+ timeoutMs: 45000,
982
+ traceLimit: 200,
983
+ text: 'Create a file at tmp/adhdev_provider_fix_test.py that prints the current working directory and the squares of 1 through 5, then run python3 tmp/adhdev_provider_fix_test.py and tell me the exact output.',
984
+ };
985
+ const exercisePayload = {
986
+ ...defaultExercisePayload,
987
+ ...(verification?.request || {}),
988
+ type,
989
+ workingDir: providerDir,
990
+ };
991
+ const exerciseJson = JSON.stringify(exercisePayload).replace(/\\/g, '\\\\').replace(/'/g, `'\\''`);
992
+ const verificationInspectFields = verification?.inspectFields?.length
993
+ ? verification.inspectFields
994
+ : [
995
+ 'debug.messages',
996
+ 'trace.entries[].payload.parsedLastAssistant',
997
+ 'trace.entries[].payload.lastAssistant',
998
+ ];
999
+ const verificationMustContainAny = verification?.mustContainAny || [];
1000
+ const verificationMustNotContainAny = verification?.mustNotContainAny || [];
1001
+ const verificationMustMatchAny = verification?.mustMatchAny || [];
1002
+ const verificationMustNotMatchAny = verification?.mustNotMatchAny || [];
1003
+ const verificationLastAssistantMustContainAny = verification?.lastAssistantMustContainAny || [];
1004
+ const verificationLastAssistantMustNotContainAny = verification?.lastAssistantMustNotContainAny || [];
1005
+ const verificationLastAssistantMustMatchAny = verification?.lastAssistantMustMatchAny || [];
1006
+ const verificationLastAssistantMustNotMatchAny = verification?.lastAssistantMustNotMatchAny || [];
1007
+ const quotedMustContain = verificationMustContainAny.map((value) => JSON.stringify(value)).join(', ');
1008
+ const quotedMustNotContain = verificationMustNotContainAny.map((value) => JSON.stringify(value)).join(', ');
1009
+ const quotedMustMatch = verificationMustMatchAny.map((value) => JSON.stringify(value)).join(', ');
1010
+ const quotedMustNotMatch = verificationMustNotMatchAny.map((value) => JSON.stringify(value)).join(', ');
1011
+ const quotedLastAssistantMustContain = verificationLastAssistantMustContainAny.map((value) => JSON.stringify(value)).join(', ');
1012
+ const quotedLastAssistantMustNotContain = verificationLastAssistantMustNotContainAny.map((value) => JSON.stringify(value)).join(', ');
1013
+ const quotedLastAssistantMustMatch = verificationLastAssistantMustMatchAny.map((value) => JSON.stringify(value)).join(', ');
1014
+ const quotedLastAssistantMustNotMatch = verificationLastAssistantMustNotMatchAny.map((value) => JSON.stringify(value)).join(', ');
1015
+ const fixtureName = verification?.fixtureName || `${type}-provider-fix`;
1016
+ const fixtureNames = Array.isArray(verification?.fixtureNames)
1017
+ ? verification!.fixtureNames.map((value) => String(value || '').trim()).filter(Boolean)
1018
+ : [];
1019
+ const fixtureCaptureJson = JSON.stringify({
1020
+ type,
1021
+ name: fixtureName,
1022
+ request: exercisePayload,
1023
+ assertions: {
1024
+ mustContainAny: verificationMustContainAny,
1025
+ mustNotContainAny: verificationMustNotContainAny,
1026
+ mustMatchAny: verificationMustMatchAny,
1027
+ mustNotMatchAny: verificationMustNotMatchAny,
1028
+ lastAssistantMustContainAny: verificationLastAssistantMustContainAny,
1029
+ lastAssistantMustNotContainAny: verificationLastAssistantMustNotContainAny,
1030
+ lastAssistantMustMatchAny: verificationLastAssistantMustMatchAny,
1031
+ lastAssistantMustNotMatchAny: verificationLastAssistantMustNotMatchAny,
1032
+ requireNotTimedOut: true,
1033
+ },
1034
+ }).replace(/\\/g, '\\\\').replace(/'/g, `'\\''`);
1035
+ const fixtureReplayJson = JSON.stringify({
1036
+ type,
1037
+ name: fixtureName,
1038
+ }).replace(/\\/g, '\\\\').replace(/'/g, `'\\''`);
838
1039
 
839
1040
  lines.push('You are implementing PTY parsing scripts for a CLI provider.');
840
1041
  lines.push('Be concise. Do NOT explain your reasoning. Edit files directly and verify with the local DevServer.');
@@ -934,7 +1135,7 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
934
1135
  };
935
1136
 
936
1137
  const providerGuide = loadGuide('PROVIDER_GUIDE.md');
937
- if (providerGuide) {
1138
+ if (providerGuide && provider.category !== 'cli') {
938
1139
  lines.push('## Documentation: PROVIDER_GUIDE.md');
939
1140
  lines.push('```markdown');
940
1141
  lines.push(providerGuide);
@@ -978,6 +1179,11 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
978
1179
  lines.push('13. After the first successful live repro, stop broad diagnosis. Edit the scripts, reload, and verify. Do not burn tokens on repeated re-inspection without code changes.');
979
1180
  lines.push('14. If the visible current screen is clean and sufficient, do NOT fall back to complex buffer heuristics. Simpler current-screen parsing is preferred.');
980
1181
  lines.push('15. Before changing parser logic, verify whether `provider.json` submit/approval behavior (`sendDelayMs`, `approvalKeys`, submit strategy) is the simpler and more correct fix.');
1182
+ lines.push('16. Do NOT patch transcript bugs by piling up one-off literal string exceptions (`includes("foo")`, `=== "bar"`, ad hoc allowlists/denylists) for every observed variant. Model the UI as PATTERN FAMILIES using reusable regex classifiers and normalization first.');
1183
+ lines.push('17. If you find yourself adding a second or third near-duplicate literal check for spinner words, tool headers, approval prompts, footer chrome, or OSC residue, STOP and replace them with a broader regex or helper classifier.');
1184
+ lines.push('18. Prefer a small number of named classifiers such as "status line", "tool header", "tool detail", "footer chrome", "approval cue", "prompt line", and "OSC residue" over a long chain of unrelated string checks.');
1185
+ lines.push('19. Literal string checks are allowed only for stable proper nouns or exact product chrome that cannot be expressed safely as a broader pattern. Everything else should generalize.');
1186
+ lines.push('20. When a bug comes from noisy PTY text, first normalize and classify the line family; do NOT just append another special-case substring to the parser.');
981
1187
  lines.push('');
982
1188
 
983
1189
  lines.push('## Task');
@@ -987,35 +1193,154 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
987
1193
  lines.push('## Verification API');
988
1194
  lines.push('Use the DevServer CLI debug endpoints, not DOM/CDP routes.');
989
1195
  lines.push('');
990
- lines.push('### 1. Launch the target CLI');
1196
+ lines.push('### 1. Preferred: run a full autonomous repro');
1197
+ lines.push('Use the exercise endpoint first. It launches a fresh CLI session, sends the repro prompt, auto-resolves approvals, waits for the session to settle, and returns the final debug + trace payload in one response.');
991
1198
  lines.push('```bash');
992
- lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/launch \\`);
1199
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/exercise \\`);
993
1200
  lines.push(' -H "Content-Type: application/json" \\');
994
- lines.push(` -d '{"type":"${type}","workingDir":"${providerDir.replace(/\\/g, '\\\\')}"}'`);
1201
+ lines.push(` -d '${exerciseJson}'`);
995
1202
  lines.push('```');
996
1203
  lines.push('');
1204
+ if (verification?.description) {
1205
+ lines.push('Verification intent:');
1206
+ lines.push(verification.description);
1207
+ lines.push('');
1208
+ }
1209
+ lines.push('Read the JSON response carefully. It already includes:');
1210
+ lines.push('1. `instanceId`');
1211
+ lines.push('2. `statusesSeen` and `approvalsResolved`');
1212
+ lines.push('3. `debug` for the final settled state');
1213
+ lines.push('4. `trace.entries` for the repro turn');
1214
+ lines.push('');
1215
+ lines.push('Save the response to a temp file and inspect the exact parsed transcript fields before editing:');
1216
+ lines.push('```bash');
1217
+ lines.push(`EXERCISE_JSON=$(mktemp)`);
1218
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/exercise \\`);
1219
+ lines.push(' -H "Content-Type: application/json" \\');
1220
+ lines.push(` -d '${exerciseJson}' > "$EXERCISE_JSON"`);
1221
+ lines.push(`jq '{timedOut,statusesSeen,approvalsResolved,inspect:{${verificationInspectFields.map((field, index) => `f${index + 1}: .${field}`).join(', ')}}}' "$EXERCISE_JSON"`);
1222
+ lines.push('```');
1223
+ lines.push('');
1224
+ if (
1225
+ verificationMustContainAny.length > 0
1226
+ || verificationMustNotContainAny.length > 0
1227
+ || verificationMustMatchAny.length > 0
1228
+ || verificationMustNotMatchAny.length > 0
1229
+ || verificationLastAssistantMustContainAny.length > 0
1230
+ || verificationLastAssistantMustNotContainAny.length > 0
1231
+ || verificationLastAssistantMustMatchAny.length > 0
1232
+ || verificationLastAssistantMustNotMatchAny.length > 0
1233
+ ) {
1234
+ lines.push('The exact repro below is mandatory. Do NOT declare success unless these transcript assertions pass on the exercise JSON from the PATCHED provider.');
1235
+ lines.push('```bash');
1236
+ if (verificationMustContainAny.length > 0) {
1237
+ lines.push(`node -e 'const fs=require(\"fs\");const text=fs.readFileSync(process.argv[1],\"utf8\");const required=[${quotedMustContain}];const missing=required.filter(v=>!text.includes(v));if(missing.length){console.error(\"Missing required substrings:\\n\"+missing.join(\"\\n\"));process.exit(1);}' "$EXERCISE_JSON"`);
1238
+ }
1239
+ if (verificationMustNotContainAny.length > 0) {
1240
+ lines.push(`node -e 'const fs=require(\"fs\");const text=fs.readFileSync(process.argv[1],\"utf8\");const banned=[${quotedMustNotContain}];const hits=banned.filter(v=>text.includes(v));if(hits.length){console.error(\"Found banned substrings:\\n\"+hits.join(\"\\n\"));process.exit(1);}' "$EXERCISE_JSON"`);
1241
+ }
1242
+ if (verificationMustMatchAny.length > 0) {
1243
+ lines.push(`node -e 'const fs=require(\"fs\");const text=fs.readFileSync(process.argv[1],\"utf8\");const required=[${quotedMustMatch}].map(v=>new RegExp(v,\"m\"));const missing=required.filter(v=>!v.test(text)).map(v=>String(v));if(missing.length){console.error(\"Missing required regex matches:\\n\"+missing.join(\"\\n\"));process.exit(1);}' "$EXERCISE_JSON"`);
1244
+ }
1245
+ if (verificationMustNotMatchAny.length > 0) {
1246
+ lines.push(`node -e 'const fs=require(\"fs\");const text=fs.readFileSync(process.argv[1],\"utf8\");const banned=[${quotedMustNotMatch}].map(v=>new RegExp(v,\"m\"));const hits=banned.filter(v=>v.test(text)).map(v=>String(v));if(hits.length){console.error(\"Found banned regex matches:\\n\"+hits.join(\"\\n\"));process.exit(1);}' "$EXERCISE_JSON"`);
1247
+ }
1248
+ if (verificationLastAssistantMustContainAny.length > 0) {
1249
+ lines.push(`node -e 'const fs=require(\"fs\");const payload=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\"));const text=String(payload.lastAssistant||\"\");const required=[${quotedLastAssistantMustContain}];const missing=required.filter(v=>!text.includes(v));if(missing.length){console.error(\"Missing required lastAssistant substrings:\\n\"+missing.join(\"\\n\"));process.exit(1);}' "$EXERCISE_JSON"`);
1250
+ }
1251
+ if (verificationLastAssistantMustNotContainAny.length > 0) {
1252
+ lines.push(`node -e 'const fs=require(\"fs\");const payload=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\"));const text=String(payload.lastAssistant||\"\");const banned=[${quotedLastAssistantMustNotContain}];const hits=banned.filter(v=>text.includes(v));if(hits.length){console.error(\"Found banned lastAssistant substrings:\\n\"+hits.join(\"\\n\"));process.exit(1);}' "$EXERCISE_JSON"`);
1253
+ }
1254
+ if (verificationLastAssistantMustMatchAny.length > 0) {
1255
+ lines.push(`node -e 'const fs=require(\"fs\");const payload=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\"));const text=String(payload.lastAssistant||\"\");const required=[${quotedLastAssistantMustMatch}].map(v=>new RegExp(v,\"m\"));const missing=required.filter(v=>!v.test(text)).map(v=>String(v));if(missing.length){console.error(\"Missing required lastAssistant regex matches:\\n\"+missing.join(\"\\n\"));process.exit(1);}' "$EXERCISE_JSON"`);
1256
+ }
1257
+ if (verificationLastAssistantMustNotMatchAny.length > 0) {
1258
+ lines.push(`node -e 'const fs=require(\"fs\");const payload=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\"));const text=String(payload.lastAssistant||\"\");const banned=[${quotedLastAssistantMustNotMatch}].map(v=>new RegExp(v,\"m\"));const hits=banned.filter(v=>v.test(text)).map(v=>String(v));if(hits.length){console.error(\"Found banned lastAssistant regex matches:\\n\"+hits.join(\"\\n\"));process.exit(1);}' "$EXERCISE_JSON"`);
1259
+ }
1260
+ lines.push('```');
1261
+ lines.push('');
1262
+ }
1263
+ lines.push('If you need a manual follow-up repro after patching, use the SAME endpoint again with the SAME prompt and compare the new trace to the previous one.');
1264
+ lines.push('');
1265
+ lines.push('### 1b. Persist or replay the exact repro as a reusable fixture');
1266
+ if (fixtureNames.length > 0) {
1267
+ lines.push(`Replay this exact fixture suite before editing, and replay the SAME suite again after patching. Do not declare success unless EVERY fixture passes: ${fixtureNames.map((name) => `\`${name}\``).join(', ')}.`);
1268
+ for (const name of fixtureNames) {
1269
+ const replayJson = JSON.stringify({ type, name }).replace(/\\/g, '\\\\').replace(/'/g, `'\\''`);
1270
+ lines.push('```bash');
1271
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/fixture/replay \\`);
1272
+ lines.push(' -H "Content-Type: application/json" \\');
1273
+ lines.push(` -d '${replayJson}'`);
1274
+ lines.push('```');
1275
+ lines.push('');
1276
+ }
1277
+ lines.push('Do not create new fixtures unless one of the listed fixtures is missing or stale.');
1278
+ } else if (verification?.fixtureName) {
1279
+ lines.push(`Replay the EXISTING saved fixture \`${fixtureName}\` before editing, and replay the SAME fixture again after patching. Do not declare success unless that exact fixture passes.`);
1280
+ lines.push('```bash');
1281
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/fixture/replay \\`);
1282
+ lines.push(' -H "Content-Type: application/json" \\');
1283
+ lines.push(` -d '${fixtureReplayJson}'`);
1284
+ lines.push('```');
1285
+ lines.push('');
1286
+ lines.push('Only if the named fixture is missing or outdated should you recapture it. Prefer replaying the existing failing fixture over creating a new one.');
1287
+ lines.push('```bash');
1288
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/fixture/capture \\`);
1289
+ lines.push(' -H "Content-Type: application/json" \\');
1290
+ lines.push(` -d '${fixtureCaptureJson}'`);
1291
+ lines.push('```');
1292
+ } else {
1293
+ lines.push('Capture the exact exercise once before editing. After patching, replay THIS fixture and do not declare success unless replay passes.');
1294
+ lines.push('```bash');
1295
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/fixture/capture \\`);
1296
+ lines.push(' -H "Content-Type: application/json" \\');
1297
+ lines.push(` -d '${fixtureCaptureJson}'`);
1298
+ lines.push('');
1299
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/fixture/replay \\`);
1300
+ lines.push(' -H "Content-Type: application/json" \\');
1301
+ lines.push(` -d '${fixtureReplayJson}'`);
1302
+ lines.push('```');
1303
+ }
1304
+ lines.push('');
1305
+ lines.push('The capture endpoint saves the exact request, initial result, and transcript assertions into the provider directory. The replay endpoint reruns the SAME exercise against your patched scripts and returns pass/fail.');
1306
+ lines.push('');
997
1307
  lines.push('### 2. Inspect parsed + raw adapter state');
998
1308
  lines.push('```bash');
1309
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/launch \\`);
1310
+ lines.push(' -H "Content-Type: application/json" \\');
1311
+ lines.push(` -d '{"type":"${type}","workingDir":"${providerDir.replace(/\\/g, '\\\\')}"}'`);
999
1312
  lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/debug/${type}`);
1313
+ lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/trace/${type}`);
1000
1314
  lines.push(`curl -sS http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/status`);
1001
1315
  lines.push('```');
1002
1316
  lines.push('');
1317
+ lines.push('The CLI trace endpoint is the primary debugging source. Read it BEFORE editing any parser code.');
1318
+ lines.push('Use the trace timeline to find the latest `settled` or `commit_transcript` frame for the repro turn and inspect these fields first:');
1319
+ lines.push('1. `payload.screenText`');
1320
+ lines.push('2. `payload.detectStatus` and `payload.parsedStatus`');
1321
+ lines.push('3. `payload.parsedLastAssistant`');
1322
+ lines.push('4. `payload.approval` / `payload.parsedActiveModal`');
1323
+ lines.push('5. `payload.rawPreview` only when control-sequence residue matters');
1324
+ lines.push('');
1003
1325
  lines.push('The debug payload should be read in this priority order:');
1004
1326
  lines.push('1. `screenText` / current visible state');
1005
1327
  lines.push('2. parsed `status`, `messages`, `activeModal`');
1006
1328
  lines.push('3. `rawBuffer` only for style/control-sequence cues');
1007
1329
  lines.push('4. `buffer` only when the current screen is insufficient');
1008
1330
  lines.push('');
1009
- lines.push('Extract the current `instanceId` from the launch or status response and keep using it below.');
1331
+ lines.push('If the bug is transcript corruption, quote the exact bad `parsedLastAssistant` or bad committed assistant message from the trace and patch against that concrete failure.');
1332
+ lines.push('Do NOT guess based only on the final chat bubble or a truncated UI preview.');
1333
+ lines.push('');
1334
+ lines.push('Extract the current `instanceId` from the exercise, launch, or status response and keep using it below.');
1010
1335
  lines.push('');
1011
- lines.push('### 3. Send a realistic approval-triggering prompt');
1336
+ lines.push('### 3. Manual fallback only: send a realistic approval-triggering prompt');
1012
1337
  lines.push('```bash');
1013
1338
  lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/send \\`);
1014
1339
  lines.push(' -H "Content-Type: application/json" \\');
1015
1340
  lines.push(` -d '{"type":"${type}","instanceId":"<INSTANCE_ID>","text":"Create a file at tmp/adhdev_provider_fix_test.py that prints the current working directory and the squares of 1 through 5, then run python3 tmp/adhdev_provider_fix_test.py and tell me the exact output."}'`);
1016
1341
  lines.push('```');
1017
1342
  lines.push('');
1018
- lines.push('### 4. If approval appears, resolve it until the CLI reaches idle');
1343
+ lines.push('### 4. Manual fallback only: if approval appears, resolve it until the CLI reaches idle');
1019
1344
  lines.push('```bash');
1020
1345
  lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/cli/resolve \\`);
1021
1346
  lines.push(' -H "Content-Type: application/json" \\');
@@ -1025,10 +1350,14 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
1025
1350
  lines.push(` -d '{"type":"${type}","instanceId":"<INSTANCE_ID>","keys":"1"}'`);
1026
1351
  lines.push('```');
1027
1352
  lines.push('');
1028
- lines.push('Use `resolve` when the parsed modal buttons are correct. Use `raw` when the CLI expects a literal keystroke like `1`, `y`, or Enter. Repeat until idle.');
1353
+ lines.push('Use `resolve` when the parsed modal buttons are correct. Use `raw` when the CLI expects a literal keystroke like `1`, `y`, or Enter. Repeat until idle. Prefer the exercise endpoint instead of doing this by hand.');
1029
1354
  lines.push('');
1030
1355
  lines.push('### Patch Discipline');
1031
1356
  lines.push('Once the repro is confirmed, immediately edit the target files. Avoid loops where you keep re-reading long files or re-running the same debug commands without changing code.');
1357
+ lines.push('For CLI transcript bugs, reproduce once with the exercise endpoint, inspect the returned trace once, patch immediately, then re-run the SAME exercise and compare the new `commit_transcript` frame.');
1358
+ lines.push('If the patched run still fails the exact required/banned substring checks above, the task is NOT complete even if the CLI exits normally.');
1359
+ lines.push('When you patch, write down the pattern family you are fixing: e.g. spinner/status, tool block, approval modal, footer chrome, OSC/control residue, prompt echo, or long-output continuation. Patch that family once instead of adding case-by-case literals.');
1360
+ lines.push('Bad fix pattern: add another `includes("Drizzling")` or `includes("Show more (")` check. Good fix pattern: broaden the regex/helper that recognizes spinner words, collapsed tool overflow lines, or footer chrome as a family.');
1032
1361
  lines.push('');
1033
1362
  lines.push('### 5. Verify the side effects outside the CLI');
1034
1363
  lines.push('```bash');
@@ -1054,6 +1383,8 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
1054
1383
  lines.push('7. Re-run the debug endpoints after edits. Do NOT finish until the parsed result looks correct.');
1055
1384
  lines.push('8. Confirm the parser still works after a redraw or scroll change without duplicating transcript history.');
1056
1385
  lines.push('9. Confirm the implementation prefers current-screen signals over stale history when both are present.');
1386
+ lines.push('10. For transcript-cleanliness bugs, confirm the latest `commit_transcript` trace frame no longer contains tool headers, approval prompts, OSC residue like `0;`, or footer chrome unless they are truly user-facing answer content.');
1387
+ lines.push('11. Confirm the implementation uses generalized pattern classifiers or regexes for noisy UI families instead of accumulating one-off literal string exceptions for each observed sample.');
1057
1388
  lines.push('');
1058
1389
 
1059
1390
  if (userComment) {
@@ -1064,12 +1395,13 @@ export function buildCliAutoImplPrompt(ctx: DevServerContext,
1064
1395
  lines.push('');
1065
1396
  }
1066
1397
 
1067
- lines.push('Start NOW. Launch the CLI, inspect PTY state, edit the scripts, and verify via the CLI debug endpoints.');
1398
+ lines.push('Start NOW. Launch the CLI, inspect the trace and PTY state, edit the scripts, and verify via the CLI debug + trace endpoints.');
1068
1399
 
1069
1400
  return lines.join('\n');
1070
1401
  }
1071
1402
 
1072
1403
  export function handleAutoImplSSE(ctx: DevServerContext, type: string, req: http.IncomingMessage, res: http.ServerResponse): void {
1404
+ clearStaleAutoImplState(ctx, 'SSE connection opened');
1073
1405
  res.writeHead(200, {
1074
1406
  'Content-Type': 'text/event-stream',
1075
1407
  'Cache-Control': 'no-cache',
@@ -1090,6 +1422,7 @@ export function handleAutoImplSSE(ctx: DevServerContext, type: string, req: http
1090
1422
  }
1091
1423
 
1092
1424
  export function handleAutoImplCancel(ctx: DevServerContext, _type: string, _req: http.IncomingMessage, res: http.ServerResponse): void {
1425
+ clearStaleAutoImplState(ctx, 'cancel request');
1093
1426
  if (ctx.autoImplProcess) {
1094
1427
  ctx.autoImplProcess.kill('SIGTERM');
1095
1428
  setTimeout(() => { if (ctx.autoImplProcess) ctx.autoImplProcess.kill('SIGKILL'); }, 3000);