@adhdev/daemon-core 0.9.4 → 0.9.5

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.
@@ -329,12 +329,21 @@ export interface ProviderModule {
329
329
  extensionIdPattern_flags?: string;
330
330
  compatibility?: ProviderCompatibilityEntry[];
331
331
  defaultScriptDir?: string;
332
+ /**
333
+ * Scripts that can run at the IDE main-page level (not just inside the extension webview session frame).
334
+ * Default: ['listModes', 'setMode', 'listModels', 'setModel'].
335
+ * Add extra scripts here if the provider supports them at the IDE level (e.g. 'setModelGui').
336
+ * Replaces hardcoded claude-code-vscode special-case in stream-commands.ts.
337
+ */
338
+ ideLevelScripts?: string[];
332
339
  binary?: string;
333
340
  spawn?: {
334
341
  command: string;
335
342
  args?: string[];
336
343
  shell?: boolean;
337
344
  env?: Record<string, string>;
345
+ /** Auto-implement spawn config — controls how this provider is invoked for autonomous script generation */
346
+ autoImpl?: ProviderAutoImplSpawnConfig;
338
347
  };
339
348
  /** Delay before submitting typed CLI input (provider-specific TUI tuning) */
340
349
  sendDelayMs?: number;
@@ -360,6 +369,24 @@ export interface ProviderModule {
360
369
  allowInputDuringGeneration?: boolean;
361
370
  /** Approval button priority hints used when auto-approve must pick a positive action */
362
371
  approvalPositiveHints?: string[];
372
+ /**
373
+ * Regex pattern (as string) that a valid provider session ID must match.
374
+ * If set and the ID doesn't match, it is rejected (treated as invalid).
375
+ * Replaces hardcoded HERMES_SESSION_ID_RE / CLAUDE_SESSION_ID_RE checks.
376
+ */
377
+ sessionIdPattern?: string;
378
+ /** History behavior config — controls message filtering and collapse during replay */
379
+ historyBehavior?: ProviderHistoryBehavior;
380
+ /**
381
+ * Canonical history sync config — for providers that maintain native history files.
382
+ * When set, daemon syncs from native format into ADHDev JSONL store on each tick.
383
+ */
384
+ canonicalHistory?: ProviderCanonicalHistoryConfig;
385
+ /**
386
+ * Auto-fix verification profile — provider-specific test expectations for `provider fix`.
387
+ * If not set, provider fix runs without pre/post verification.
388
+ */
389
+ autoFixProfile?: ProviderAutoFixProfile;
363
390
  scripts?: ProviderScripts;
364
391
  vscodeCommands?: {
365
392
  focusPanel?: string;
@@ -438,6 +465,90 @@ export interface ProviderResumeCapability {
438
465
  resumeSessionArgs?: string[];
439
466
  newSessionArgs?: string[];
440
467
  sessionIdFormat?: 'uuid' | 'string';
468
+ /** Skip session ID probing when launchMode is 'new' — for providers that manage their own session IDs on new sessions */
469
+ skipProbeOnNewSession?: boolean;
470
+ /**
471
+ * Subcommands that carry a session ID as their next positional argument.
472
+ * e.g. ['resume', 'fork'] for codex-cli (codex resume <id> / codex fork <id>).
473
+ * Replaces the hardcoded readCodexResumeSessionId check in cli-manager.ts.
474
+ */
475
+ sessionIdFromSubcommand?: string[];
476
+ /**
477
+ * When --session-id is present without an explicit resume flag, treat as 'new' rather than 'resume'.
478
+ * e.g. goose-cli passes --session-id on new sessions but requires --resume/-r to actually resume.
479
+ * Replaces the hardcoded goose-cli check in cli-manager.ts.
480
+ */
481
+ sessionIdIsNewByDefault?: boolean;
482
+ }
483
+ /**
484
+ * History behavior config — controls how history messages are processed for this provider.
485
+ * Replaces hardcoded agentType checks in chat-history.ts.
486
+ */
487
+ export interface ProviderHistoryBehavior {
488
+ /** Collapse consecutive assistant turns during history replay (e.g. codex-cli shows replayed intermediate turns) */
489
+ collapseConsecutiveAssistantTurns?: boolean;
490
+ /** Regex patterns (as strings) to filter out from assistant messages — e.g. CLI starter prompt suggestions */
491
+ filterAssistantPatterns?: string[];
492
+ /** If true, session ID must match sessionIdPattern exactly — reject and return '' if it doesn't match */
493
+ requireStrictSessionIdFormat?: boolean;
494
+ }
495
+ /**
496
+ * Canonical history sync config — for providers that maintain their own native history files.
497
+ * When set, daemon syncs from the provider's native format into the ADHDev JSONL store.
498
+ * Replaces hardcoded hermes-cli / claude-cli checks in cli-provider-instance.ts.
499
+ */
500
+ export interface ProviderCanonicalHistoryConfig {
501
+ /**
502
+ * Native history format.
503
+ * - 'hermes-json': single JSON file per session (~/.hermes/sessions/session_{{sessionId}}.json)
504
+ * - 'claude-jsonl': JSONL transcript under ~/.claude/projects/
505
+ */
506
+ format: 'hermes-json' | 'claude-jsonl';
507
+ /**
508
+ * Path to the native history file. Supports ~ and {{sessionId}} placeholder.
509
+ * e.g. "~/.hermes/sessions/session_{{sessionId}}.json"
510
+ */
511
+ watchPath: string;
512
+ }
513
+ /**
514
+ * Auto-implement spawn config — controls how the provider is spawned for autonomous AI-driven
515
+ * provider script implementation (dev-auto-implement.ts).
516
+ * Replaces hardcoded per-command branching.
517
+ */
518
+ export interface ProviderAutoImplSpawnConfig {
519
+ /**
520
+ * How the meta-prompt is passed to the agent.
521
+ * - 'flag': passed via a CLI flag (e.g. `claude -p "..."`)
522
+ * - 'stdin': piped via stdin (generic fallback)
523
+ * - 'subcommand': prepended as a subcommand (e.g. `codex exec "..."`)
524
+ */
525
+ promptMode: 'flag' | 'stdin' | 'subcommand';
526
+ /** CLI flag used to pass the prompt (promptMode: 'flag') — e.g. '-p' */
527
+ promptFlag?: string;
528
+ /** Subcommand prepended before the prompt (promptMode: 'subcommand') — e.g. 'exec' */
529
+ subcommand?: string;
530
+ /** Extra args appended in auto-impl mode — e.g. ['--dangerously-skip-permissions'] */
531
+ extraArgs?: string[];
532
+ /** Custom meta-prompt template; use {{promptFile}} placeholder. If omitted, generic prompt is used. */
533
+ metaPrompt?: string;
534
+ /**
535
+ * If true, schedule an auto-stop timer when the agent output goes quiet during verification.
536
+ * Replaces the hardcoded `command !== 'codex'` check in dev-auto-implement.ts.
537
+ */
538
+ autoStopOnQuiet?: boolean;
539
+ }
540
+ /**
541
+ * Auto-fix verification profile — provider-specific test expectations for `provider fix`.
542
+ * Replaces the hardcoded CLI_AUTO_FIX_VERIFICATION_PROFILES record in provider-commands.ts.
543
+ */
544
+ export interface ProviderAutoFixProfile {
545
+ fixtureName: string;
546
+ description: string;
547
+ inspectFields?: string[];
548
+ focusAreas?: string[];
549
+ lastAssistantMustContainAny?: string[];
550
+ lastAssistantMustNotContainAny?: string[];
551
+ timeoutMs?: number;
441
552
  }
442
553
  /**
443
554
  * Declarative session ID probe config for CLI providers.
@@ -1,2 +1,6 @@
1
- export declare function normalizeProviderSessionId(providerType: string | undefined, providerSessionId: string | null | undefined): string;
2
- export declare function isLegacyVolatileSessionReadKey(key: string | null | undefined): boolean;
1
+ import type { ProviderModule } from './contracts.js';
2
+ /**
3
+ * Normalize and validate a provider session ID using the declarative `sessionIdPattern`
4
+ * from the provider's ProviderModule definition.
5
+ */
6
+ export declare function normalizeProviderSessionId(provider: ProviderModule | undefined, providerSessionId: string | null | undefined): string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -131,8 +131,8 @@ function expandResumeArgs(template: string[] | undefined, sessionId: string): st
131
131
  return template.map((part) => part === '{{id}}' ? sessionId : part);
132
132
  }
133
133
 
134
- function readCodexResumeSessionId(args: string[]): string | undefined {
135
- const resumeIndex = args.findIndex((arg) => arg === 'resume' || arg === 'fork');
134
+ function readSubcommandSessionId(args: string[], subcommands: string[]): string | undefined {
135
+ const resumeIndex = args.findIndex((arg) => subcommands.includes(arg));
136
136
  if (resumeIndex < 0) return undefined;
137
137
  const candidate = args[resumeIndex + 1];
138
138
  if (!candidate || candidate.startsWith('-')) return undefined;
@@ -140,9 +140,11 @@ function readCodexResumeSessionId(args: string[]): string | undefined {
140
140
  }
141
141
 
142
142
  function detectExplicitProviderSessionId(
143
- normalizedType: string,
143
+ provider: ProviderModule | undefined,
144
144
  args: string[],
145
145
  ): { providerSessionId?: string; launchMode: CliLaunchMode } {
146
+ const resume = provider?.resume;
147
+
146
148
  const explicitResumeId = readArgValue(args, ['--resume', '-r']);
147
149
  if (explicitResumeId) {
148
150
  return { providerSessionId: explicitResumeId, launchMode: 'resume' };
@@ -158,10 +160,10 @@ function detectExplicitProviderSessionId(
158
160
 
159
161
  const explicitSessionId = readArgValue(args, ['--session-id']);
160
162
  if (explicitSessionId) {
161
- if (normalizedType === 'goose-cli' && !hasArg(args, ['--resume', '-r'])) {
163
+ if (resume?.sessionIdIsNewByDefault && !hasArg(args, ['--resume', '-r'])) {
162
164
  return { launchMode: 'manual' };
163
165
  }
164
- const isResume = normalizedType === 'goose-cli'
166
+ const isResume = resume?.sessionIdIsNewByDefault
165
167
  ? hasArg(args, ['--resume', '-r'])
166
168
  : (hasArg(args, ['--continue']) || hasArg(args, ['--resume', '-r']));
167
169
  return {
@@ -170,10 +172,11 @@ function detectExplicitProviderSessionId(
170
172
  };
171
173
  }
172
174
 
173
- if (normalizedType === 'codex-cli') {
174
- const codexSessionId = readCodexResumeSessionId(args);
175
- if (codexSessionId) {
176
- return { providerSessionId: codexSessionId, launchMode: 'resume' };
175
+ const subcommands = resume?.sessionIdFromSubcommand;
176
+ if (Array.isArray(subcommands) && subcommands.length > 0) {
177
+ const subcommandSessionId = readSubcommandSessionId(args, subcommands);
178
+ if (subcommandSessionId) {
179
+ return { providerSessionId: subcommandSessionId, launchMode: 'resume' };
177
180
  }
178
181
  }
179
182
 
@@ -200,7 +203,7 @@ export function resolveCliSessionBinding(
200
203
  return { cliArgs: baseArgs, launchMode: 'manual' };
201
204
  }
202
205
 
203
- const explicit = detectExplicitProviderSessionId(normalizedType, baseArgs || []);
206
+ const explicit = detectExplicitProviderSessionId(provider, baseArgs || []);
204
207
  if (explicit.providerSessionId) {
205
208
  return {
206
209
  cliArgs: baseArgs,
@@ -208,6 +211,12 @@ export function resolveCliSessionBinding(
208
211
  launchMode: explicit.launchMode,
209
212
  };
210
213
  }
214
+ if (explicit.launchMode === 'manual' && hasArg(baseArgs || [], ['--session-id'])) {
215
+ return {
216
+ cliArgs: baseArgs,
217
+ launchMode: 'manual',
218
+ };
219
+ }
211
220
 
212
221
  if (requestedResumeSessionId) {
213
222
  if (resume.sessionIdFormat === 'uuid' && !isUuid(requestedResumeSessionId)) {
@@ -382,9 +382,8 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
382
382
  const targetSessionId = managed?.cdpSessionId || null;
383
383
 
384
384
  // IDE-level scripts (model/mode) — try session frame first, fallback to main page
385
- const IDE_LEVEL_SCRIPTS = provider.type === 'claude-code-vscode'
386
- ? ['listModes', 'setMode', 'listModels', 'setModel', 'setModelGui']
387
- : ['listModes', 'setMode', 'listModels', 'setModel'];
385
+ const DEFAULT_IDE_LEVEL_SCRIPTS = ['listModes', 'setMode', 'listModels', 'setModel'];
386
+ const IDE_LEVEL_SCRIPTS = provider.ideLevelScripts ?? DEFAULT_IDE_LEVEL_SCRIPTS;
388
387
  if (IDE_LEVEL_SCRIPTS.includes(scriptName)) {
389
388
  // Try session frame first (some extensions embed mode selector in their webview)
390
389
  if (targetSessionId) {
@@ -13,7 +13,7 @@ import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import * as os from 'os';
15
15
  import { buildRuntimeSystemChatMessage } from '../providers/chat-message-normalization.js';
16
- import { normalizeProviderSessionId } from '../providers/provider-session-id.js';
16
+ import type { ProviderHistoryBehavior } from '../providers/contracts.js';
17
17
 
18
18
  const HISTORY_DIR = path.join(os.homedir(), '.adhdev', 'history');
19
19
  const RETAIN_DAYS = 30;
@@ -72,24 +72,27 @@ interface HistoryMessage {
72
72
  workspace?: string; // Working directory at session start (kind: 'session_start' only)
73
73
  }
74
74
 
75
- const CODEX_STARTER_PROMPT_RE = /^(?:[›❯]\s*)?(?:Find and fix a bug in @filename|Improve documentation in @filename|Write tests for @filename|Explain this codebase|Summarize recent commits|Implement \{feature\}|Use \/skills(?: to list available skills)?|Run \/review on my current changes)$/i;
76
-
77
75
  function normalizeHistoryComparable(text: string): string {
78
76
  return String(text || '').replace(/\s+/g, ' ').trim();
79
77
  }
80
78
 
81
- function cleanupHistoryContent(agentType: string, role: HistoryMessage['role'], content: string): string {
79
+ function cleanupHistoryContent(agentType: string, role: HistoryMessage['role'], content: string, historyBehavior?: ProviderHistoryBehavior): string {
82
80
  let value = String(content || '').replace(/\r\n/g, '\n').trim();
83
81
  if (!value) return '';
84
82
 
85
- if (agentType === 'codex-cli' && role === 'assistant') {
86
- const filtered = value
87
- .split('\n')
88
- .filter((line) => !CODEX_STARTER_PROMPT_RE.test(line.trim()))
89
- .join('\n')
90
- .replace(/\n{3,}/g, '\n\n')
91
- .trim();
92
- value = filtered;
83
+ if (role === 'assistant' && historyBehavior?.filterAssistantPatterns?.length) {
84
+ const filters = historyBehavior.filterAssistantPatterns.map((p) => {
85
+ try { return new RegExp(p, 'i'); } catch { return null; }
86
+ }).filter(Boolean) as RegExp[];
87
+ if (filters.length > 0) {
88
+ const filtered = value
89
+ .split('\n')
90
+ .filter((line) => !filters.some((re) => re.test(line.trim())))
91
+ .join('\n')
92
+ .replace(/\n{3,}/g, '\n\n')
93
+ .trim();
94
+ value = filtered;
95
+ }
93
96
  }
94
97
 
95
98
  return value;
@@ -121,8 +124,8 @@ function isAdjacentHistoryDuplicate(
121
124
  return buildHistoryMessageSignature(agentType, previous) === buildHistoryMessageSignature(agentType, next);
122
125
  }
123
126
 
124
- function collapseReplayAssistantTurns(agentType: string, messages: HistoryMessage[]): HistoryMessage[] {
125
- if (agentType !== 'codex-cli') return messages;
127
+ function collapseReplayAssistantTurns(messages: HistoryMessage[], historyBehavior?: ProviderHistoryBehavior): HistoryMessage[] {
128
+ if (!historyBehavior?.collapseConsecutiveAssistantTurns) return messages;
126
129
 
127
130
  const collapsed: HistoryMessage[] = [];
128
131
  let sawAssistantSinceLastUser = false;
@@ -257,17 +260,13 @@ function listHistoryFiles(dir: string, historySessionId?: string): string[] {
257
260
  .reverse();
258
261
  }
259
262
 
260
- function normalizeSavedHistorySessionId(agentType: string, historySessionId: string): string {
261
- const normalizedId = String(historySessionId || '').trim();
262
- if (!normalizedId) return '';
263
- const strictProviderId = normalizeProviderSessionId(agentType, normalizedId);
264
- if (strictProviderId) return strictProviderId;
265
- return agentType === 'hermes-cli' ? '' : normalizedId;
263
+ function normalizeSavedHistorySessionId(historySessionId: string): string {
264
+ return String(historySessionId || '').trim();
266
265
  }
267
266
 
268
- function extractSavedHistorySessionIdFromFile(agentType: string, file: string): string {
267
+ function extractSavedHistorySessionIdFromFile(file: string): string {
269
268
  const match = file.match(/^([A-Za-z0-9_-]+)_\d{4}-\d{2}-\d{2}\.jsonl$/);
270
- return normalizeSavedHistorySessionId(agentType, match?.[1] || '');
269
+ return normalizeSavedHistorySessionId(match?.[1] || '');
271
270
  }
272
271
 
273
272
  function buildSavedHistoryFileSignatureMap(dir: string, files: string[]): Map<string, string> {
@@ -484,7 +483,7 @@ function persistSavedHistoryFileSummaryEntry(agentType: string, dir: string, fil
484
483
  }
485
484
 
486
485
  function updateSavedHistoryIndexForSessionStart(agentType: string, dir: string, file: string, historySessionId: string, workspace: string): void {
487
- const normalizedSessionId = normalizeSavedHistorySessionId(agentType, historySessionId);
486
+ const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
488
487
  const normalizedWorkspace = String(workspace || '').trim();
489
488
  if (!normalizedSessionId || !normalizedWorkspace) return;
490
489
  persistSavedHistoryFileSummaryEntry(agentType, dir, file, (currentSummary) => ({
@@ -506,7 +505,7 @@ function updateSavedHistoryIndexForAppendedMessages(
506
505
  historySessionId: string | undefined,
507
506
  messages: HistoryMessage[],
508
507
  ): void {
509
- const normalizedSessionId = normalizeSavedHistorySessionId(agentType, historySessionId || '');
508
+ const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
510
509
  if (!normalizedSessionId || messages.length === 0) return;
511
510
  persistSavedHistoryFileSummaryEntry(agentType, dir, file, (currentSummary) => {
512
511
  const nextSummary: SavedHistoryFileSummary = {
@@ -546,8 +545,8 @@ function updateSavedHistoryIndexForAppendedMessages(
546
545
  });
547
546
  }
548
547
 
549
- function computeSavedHistoryFileSummary(agentType: string, dir: string, file: string): SavedHistoryFileSummary | null {
550
- const historySessionId = extractSavedHistorySessionIdFromFile(agentType, file);
548
+ function computeSavedHistoryFileSummary(dir: string, file: string): SavedHistoryFileSummary | null {
549
+ const historySessionId = extractSavedHistorySessionIdFromFile(file);
551
550
  if (!historySessionId) return null;
552
551
 
553
552
  const filePath = path.join(dir, file);
@@ -660,7 +659,7 @@ function computeSavedHistorySessionSummaries(
660
659
  : persisted?.signature === signature
661
660
  ? persisted
662
661
  : null;
663
- const fileSummary = reusableEntry?.summary || computeSavedHistoryFileSummary(agentType, dir, file);
662
+ const fileSummary = reusableEntry?.summary || computeSavedHistoryFileSummary(dir, file);
664
663
  const nextEntry: SavedHistoryFileSummaryCacheEntry = reusableEntry || {
665
664
  signature,
666
665
  summary: fileSummary,
@@ -1027,7 +1026,7 @@ export class ChatHistoryWriter {
1027
1026
  }
1028
1027
  }
1029
1028
 
1030
- compactHistorySession(agentType: string, historySessionId: string): void {
1029
+ compactHistorySession(agentType: string, historySessionId: string, historyBehavior?: ProviderHistoryBehavior): void {
1031
1030
  const sessionId = String(historySessionId || '').trim();
1032
1031
  if (!sessionId) return;
1033
1032
 
@@ -1072,7 +1071,7 @@ export class ChatHistoryWriter {
1072
1071
  dedupedAdjacent.push(entry);
1073
1072
  if (entry.role !== 'system') lastTurn = entry;
1074
1073
  }
1075
- const collapsed = collapseReplayAssistantTurns(agentType, dedupedAdjacent);
1074
+ const collapsed = collapseReplayAssistantTurns(dedupedAdjacent, historyBehavior);
1076
1075
  if (collapsed.length === 0) {
1077
1076
  fs.unlinkSync(filePath);
1078
1077
  continue;
@@ -1145,6 +1144,7 @@ export function readChatHistory(
1145
1144
  limit: number = 30,
1146
1145
  historySessionId?: string,
1147
1146
  excludeRecentCount: number = 0,
1147
+ historyBehavior?: ProviderHistoryBehavior,
1148
1148
  ): { messages: HistoryMessage[]; hasMore: boolean } {
1149
1149
  try {
1150
1150
  const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
@@ -1185,7 +1185,7 @@ export function readChatHistory(
1185
1185
  chronological.push(message);
1186
1186
  if (message.role !== 'system') lastTurn = message;
1187
1187
  }
1188
- const collapsed = collapseReplayAssistantTurns(agentType, chronological);
1188
+ const collapsed = collapseReplayAssistantTurns(chronological, historyBehavior);
1189
1189
 
1190
1190
  // Page backwards from the newest saved messages while keeping the returned
1191
1191
  // slice in chronological order for prepend-based UI rendering.
@@ -1206,6 +1206,7 @@ export function readChatHistory(
1206
1206
  export function listSavedHistorySessions(
1207
1207
  agentType: string,
1208
1208
  options: { offset?: number; limit?: number } = {},
1209
+ historyBehavior?: ProviderHistoryBehavior,
1209
1210
  ): { sessions: SavedHistorySessionSummary[]; hasMore: boolean } {
1210
1211
  try {
1211
1212
  const sanitized = agentType.replace(/[^a-zA-Z0-9_-]/g, '_');
@@ -1351,7 +1352,7 @@ function rewriteCanonicalSavedHistory(agentType: string, historySessionId: strin
1351
1352
  }
1352
1353
 
1353
1354
  export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId: string): boolean {
1354
- const normalizedSessionId = normalizeSavedHistorySessionId('hermes-cli', historySessionId);
1355
+ const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1355
1356
  if (!normalizedSessionId) return false;
1356
1357
 
1357
1358
  try {
@@ -1523,7 +1524,7 @@ function extractClaudeUserContentParts(content: unknown): Array<{ role: 'user' |
1523
1524
  }
1524
1525
 
1525
1526
  export function rebuildClaudeSavedHistoryFromNativeProject(historySessionId: string, workspace?: string): boolean {
1526
- const normalizedSessionId = normalizeSavedHistorySessionId('claude-cli', historySessionId);
1527
+ const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId);
1527
1528
  if (!normalizedSessionId) return false;
1528
1529
 
1529
1530
  try {
@@ -12,7 +12,6 @@ import { join } from 'path';
12
12
  import { getConfigDir } from './config.js';
13
13
  import type { RecentActivityEntry } from './recent-activity.js';
14
14
  import type { SavedProviderSessionEntry } from './saved-sessions.js';
15
- import { isLegacyVolatileSessionReadKey, normalizeProviderSessionId } from '../providers/provider-session-id.js';
16
15
 
17
16
  export interface DaemonState {
18
17
  /** Unified recent activity across IDE / CLI / ACP launch flows */
@@ -52,38 +51,31 @@ function normalizeState(raw: unknown): DaemonState {
52
51
  const recentActivity = (Array.isArray(parsed.recentActivity) ? parsed.recentActivity : [])
53
52
  .filter((entry): entry is RecentActivityEntry => {
54
53
  if (!isPlainObject(entry)) return false;
55
- const normalizedId = normalizeProviderSessionId(
56
- typeof entry.providerType === 'string' ? entry.providerType : '',
57
- typeof entry.providerSessionId === 'string' ? entry.providerSessionId : '',
58
- );
59
- if (typeof entry.providerSessionId === 'string' && !normalizedId) return false;
54
+ if (typeof entry.providerSessionId === 'string' && !entry.providerSessionId.trim()) return false;
60
55
  return true;
61
56
  });
62
57
 
63
58
  const savedProviderSessions = (Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions : [])
64
59
  .filter((entry): entry is SavedProviderSessionEntry => {
65
60
  if (!isPlainObject(entry)) return false;
66
- return !!normalizeProviderSessionId(
67
- typeof entry.providerType === 'string' ? entry.providerType : '',
68
- typeof entry.providerSessionId === 'string' ? entry.providerSessionId : '',
69
- );
61
+ return typeof entry.providerSessionId === 'string' && !!entry.providerSessionId.trim();
70
62
  });
71
63
 
72
64
  const sessionReads = Object.fromEntries(
73
65
  Object.entries(isPlainObject(parsed.sessionReads) ? parsed.sessionReads : {})
74
- .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'number' && Number.isFinite(value as number))
66
+ .filter(([, value]) => typeof value === 'number' && Number.isFinite(value as number))
75
67
  );
76
68
  const sessionReadMarkers = Object.fromEntries(
77
69
  Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
78
- .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'string')
70
+ .filter(([, value]) => typeof value === 'string')
79
71
  );
80
72
  const sessionNotificationDismissals = Object.fromEntries(
81
73
  Object.entries(isPlainObject(parsed.sessionNotificationDismissals) ? parsed.sessionNotificationDismissals : {})
82
- .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'string' && value.length > 0)
74
+ .filter(([, value]) => typeof value === 'string' && value.length > 0)
83
75
  );
84
76
  const sessionNotificationUnreadOverrides = Object.fromEntries(
85
77
  Object.entries(isPlainObject(parsed.sessionNotificationUnreadOverrides) ? parsed.sessionNotificationUnreadOverrides : {})
86
- .filter(([key, value]) => !isLegacyVolatileSessionReadKey(key) && typeof value === 'string' && value.length > 0)
78
+ .filter(([, value]) => typeof value === 'string' && value.length > 0)
87
79
  );
88
80
 
89
81
  return {
@@ -67,20 +67,18 @@ function tryKillAutoImplProcess(processRef: ChildProcess | null, signal: NodeJS.
67
67
  }
68
68
  }
69
69
 
70
+ export function shouldScheduleAutoStopOnQuiet(options: {
71
+ verification?: unknown;
72
+ autoImpl?: { autoStopOnQuiet?: boolean } | null;
73
+ }): boolean {
74
+ return !!options.verification && options.autoImpl?.autoStopOnQuiet === true;
75
+ }
76
+
70
77
  export function getDefaultAutoImplReference(ctx: DevServerContext, category: string, type: string): string {
71
- if (category === 'cli') {
72
- return type === 'codex-cli' ? 'claude-cli' : 'codex-cli';
73
- }
74
- if (category === 'extension') {
75
- const preferred = ['claude-code-vscode', 'codex', 'cline', 'roo-code'];
76
- for (const ref of preferred) {
77
- if (ref === type) continue;
78
- if (ctx.providerLoader.resolve(ref) || ctx.providerLoader.getMeta(ref)) return ref;
79
- }
80
- const all = ctx.providerLoader.getAll();
81
- const fb = all.find((p: any) => p.category === 'extension' && p.type !== type);
82
- if (fb?.type) return fb.type;
83
- }
78
+ const all = ctx.providerLoader.getAll();
79
+ // Pick any other provider in the same category as a reference
80
+ const sameCategoryOther = all.find((p: any) => p.category === category && p.type !== type);
81
+ if (sameCategoryOther?.type) return sameCategoryOther.type;
84
82
  return 'antigravity';
85
83
  }
86
84
 
@@ -424,49 +422,43 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
424
422
  return;
425
423
  }
426
424
 
427
- // ─── CLI Agent: stdin pipe approach ───
425
+ // ─── CLI Agent: declarative autoImpl config from provider.json ───
428
426
  const command: string = spawn.command;
427
+ const autoImpl = spawn.autoImpl;
429
428
  // Strip interactive-only flags for auto-implement (non-interactive mode)
430
429
  const interactiveFlags = ['--yolo', '--interactive', '-i'];
431
430
  const baseArgs: string[] = [...(spawn.args || [])].filter((a: string) => !interactiveFlags.includes(a));
432
431
 
433
- // 6. Construct the complete shell command per-agent
432
+ // 6. Construct the complete shell command from provider.json autoImpl config
434
433
  let shellCmd: string;
435
434
  const isWin = os.platform() === 'win32';
436
435
  const escapeArg = (a: string) => isWin ? `"${a.replace(/"/g, '""')}"` : `'${a.replace(/'/g, "'\\''")}'`;
437
436
 
438
- if (command === 'claude') {
439
- // Claude Code: autonomous agent mode (no --print), skip permissions, prompt via meta-prompt
440
- const args = [...baseArgs, '--dangerously-skip-permissions'];
437
+ const promptMode = autoImpl?.promptMode ?? 'stdin';
438
+ const extraArgs = autoImpl?.extraArgs ?? [];
439
+ const rawMetaPrompt = autoImpl?.metaPrompt
440
+ ? autoImpl.metaPrompt.replace('{{promptFile}}', promptFile)
441
+ : `Read the file at ${promptFile} and follow ALL the instructions in it exactly. Do not ask questions, just execute.`;
442
+
443
+ if (promptMode === 'flag') {
444
+ const flag = autoImpl?.promptFlag ?? '-p';
445
+ const args = [...baseArgs, ...extraArgs];
441
446
  if (model) args.push('--model', model);
442
447
  const escapedArgs = args.map(escapeArg).join(' ');
443
- const metaPrompt = `Read the file at ${promptFile} and follow ALL the instructions. Implement the specific function requested, then test it via CDP curl targeting 127.0.0.1:19280, wait for confirmation of success, and then close. DO NOT start working on other features not listed in the prompt constraint.`;
444
- shellCmd = `${command} ${escapedArgs} -p ${escapeArg(metaPrompt)}`;
445
- } else if (command === 'gemini') {
446
- // Gemini CLI: non-interactive prompt mode
447
- // We can't use @file syntax (causes Parts object parsing bug) or $(cat) (arg too long).
448
- // Solution: meta-prompt that tells Gemini to read the instructions file itself.
449
- const args = [...baseArgs, '-y', '-s', 'false'];
450
- if (model) args.push('-m', model);
451
- const escapedArgs = args.map(escapeArg).join(' ');
452
- const metaPrompt = `Read the file at ${promptFile} and follow ALL the instructions in it exactly. Do not ask questions, just execute.`;
453
- shellCmd = `${command} ${escapedArgs} -p ${escapeArg(metaPrompt)}`;
454
-
455
- } else if (command === 'codex') {
456
- const args = ['exec', ...baseArgs];
457
- if (!args.includes('--dangerously-bypass-approvals-and-sandbox')) {
458
- args.push('--dangerously-bypass-approvals-and-sandbox');
459
- }
460
- if (!args.includes('--skip-git-repo-check')) {
461
- args.push('--skip-git-repo-check');
448
+ shellCmd = `${command} ${escapedArgs} ${flag} ${escapeArg(rawMetaPrompt)}`;
449
+ } else if (promptMode === 'subcommand') {
450
+ const subcommand = autoImpl?.subcommand ?? '';
451
+ const args = subcommand ? [subcommand, ...baseArgs] : [...baseArgs];
452
+ for (const extra of extraArgs) {
453
+ if (!args.includes(extra)) args.push(extra);
462
454
  }
463
455
  if (model) args.push('--model', model);
464
456
  const escapedArgs = args.map(escapeArg).join(' ');
465
- const metaPrompt = `Read the file at ${promptFile} and follow ALL instructions strictly. DO NOT spend time exploring the filesystem or other providers. You have full authority to implement ALL required script files and independently test them against 127.0.0.1:19280 via CDP CURL. Upon complete validation of ALL assigned files, print exactly "_PIPELINE_COMPLETE_SIGNAL_" to gracefully close the pipeline. DO NOT WAIT FOR APPROVAL, execute completely autonomously.`;
466
- shellCmd = `${command} ${escapedArgs} ${escapeArg(metaPrompt)}`;
457
+ shellCmd = `${command} ${escapedArgs} ${escapeArg(rawMetaPrompt)}`;
467
458
  } else {
468
- // Generic fallback: pipe prompt via stdin
469
- const escapedArgs = baseArgs.map(escapeArg).join(' ');
459
+ // stdin fallback (generic)
460
+ const args = [...baseArgs, ...extraArgs];
461
+ const escapedArgs = args.map(escapeArg).join(' ');
470
462
  if (isWin) {
471
463
  shellCmd = `type "${promptFile}" | ${command} ${escapedArgs}`;
472
464
  } else {
@@ -503,10 +495,9 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
503
495
  shell: false,
504
496
  timeout: 900000,
505
497
  stdio: ['pipe', 'pipe', 'pipe'],
506
- env: {
507
- ...process.env,
498
+ env: {
499
+ ...process.env,
508
500
  ...(spawn.env || {}),
509
- ...(command === 'gemini' ? { SANDBOX: '1', GEMINI_CLI_NO_RELAUNCH: '1' } : {}),
510
501
  },
511
502
  });
512
503
  child.on('error', (err: Error) => {
@@ -582,7 +573,7 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
582
573
  };
583
574
 
584
575
  const scheduleAutoStopForVerification = () => {
585
- if (!verification || command !== 'codex' || completionSignalSeen || autoStopIssued) return;
576
+ if (!shouldScheduleAutoStopOnQuiet({ verification, autoImpl }) || completionSignalSeen || autoStopIssued) return;
586
577
  const elapsed = Date.now() - spawnedAt;
587
578
  if (elapsed < 30000) return;
588
579
  clearAutoStopTimer();