vigthoria-cli 1.13.13 → 1.13.18

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.
@@ -4,6 +4,8 @@ interface BackgroundOptions {
4
4
  json?: boolean;
5
5
  limit?: number;
6
6
  workspace?: string;
7
+ deny?: boolean;
8
+ model?: string;
7
9
  }
8
10
  export declare class BackgroundCommand {
9
11
  private config;
@@ -16,5 +18,6 @@ export declare class BackgroundCommand {
16
18
  apply(jobId: string, options?: BackgroundOptions): Promise<void>;
17
19
  cancel(jobId: string, options?: BackgroundOptions): Promise<void>;
18
20
  private extractBackgroundOutcome;
21
+ approve(jobId: string, approvalId: string, options?: BackgroundOptions): Promise<void>;
19
22
  }
20
23
  export {};
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { createSpinner, CH } from '../utils/logger.js';
3
3
  import { APIClient } from '../utils/api.js';
4
+ import { buildExecutionHints, inferAgentTaskType } from '../utils/requestIntent.js';
4
5
  export class BackgroundCommand {
5
6
  config;
6
7
  logger;
@@ -17,6 +18,15 @@ export class BackgroundCommand {
17
18
  return;
18
19
  }
19
20
  const workspacePath = options.workspace || process.cwd();
21
+ const requestedModel = String(options.model || 'agent').trim().toLowerCase();
22
+ const allowedModels = new Set(['agent', 'cloud', 'cloud-reason', 'ultra']);
23
+ if (!allowedModels.has(requestedModel)) {
24
+ this.logger.error('Background model must be one of: agent, cloud, cloud-reason, ultra');
25
+ return;
26
+ }
27
+ const cloudSelected = ['cloud', 'cloud-reason', 'ultra'].includes(requestedModel);
28
+ const agentTaskType = inferAgentTaskType(prompt);
29
+ const routedHints = buildExecutionHints(agentTaskType, prompt);
20
30
  const spinner = createSpinner('Starting background agent job...').start();
21
31
  try {
22
32
  const job = await this.api.startV3BackgroundJob(prompt, {
@@ -27,6 +37,19 @@ export class BackgroundCommand {
27
37
  executionSurface: 'cli',
28
38
  clientSurface: 'cli',
29
39
  clientToolExecution: false,
40
+ model: requestedModel,
41
+ requestedModel,
42
+ agentTaskType,
43
+ workflowType: routedHints.workflow_type,
44
+ executionHints: {
45
+ ...routedHints,
46
+ cloud_selected: cloudSelected,
47
+ // Detached jobs must preserve the selected transport. This stops a
48
+ // paid cloud request from being silently reinterpreted as a local
49
+ // router alias during recovery.
50
+ enforce_cloud_only: cloudSelected,
51
+ background_model: requestedModel,
52
+ },
30
53
  });
31
54
  spinner.stop();
32
55
  if (options.json) {
@@ -37,6 +60,7 @@ export class BackgroundCommand {
37
60
  console.log(chalk.gray(` Job: ${job.job_id}`));
38
61
  console.log(chalk.gray(` Status: ${job.status}`));
39
62
  console.log(chalk.gray(` Workspace: ${workspacePath}`));
63
+ console.log(chalk.gray(` Model: ${requestedModel}`));
40
64
  console.log();
41
65
  console.log(chalk.gray(`Use ${chalk.cyan(`vigthoria background status ${job.job_id}`)} to check progress.`));
42
66
  console.log(chalk.gray(`Use ${chalk.cyan(`vigthoria background apply ${job.job_id}`)} when it is completed.`));
@@ -100,6 +124,14 @@ export class BackgroundCommand {
100
124
  }
101
125
  if (job.error)
102
126
  console.log(chalk.red(`Error: ${job.error}`));
127
+ const approvals = Array.isArray(job.pending_approvals) ? job.pending_approvals : [];
128
+ if (approvals.length > 0) {
129
+ console.log(chalk.yellow(`Gate: ${approvals.length} command approval(s) required`));
130
+ for (const approval of approvals) {
131
+ console.log(chalk.gray(` ${approval.approval_id}: ${approval.command || '(empty command)'}`));
132
+ console.log(chalk.gray(` Resolve: vigthoria background approve ${job.job_id} ${approval.approval_id}`));
133
+ }
134
+ }
103
135
  console.log(chalk.gray(`\n${String(job.request || '').slice(0, 500)}`));
104
136
  if (job.status === 'completed' && (job.file_count || 0) > 0) {
105
137
  console.log(chalk.gray(`\nUse ${chalk.cyan(`vigthoria background apply ${job.job_id}`)} to apply files locally.`));
@@ -170,6 +202,8 @@ export class BackgroundCommand {
170
202
  let tasksCompleted = Number(result.tasks_completed ?? result.tasksCompleted ?? 0);
171
203
  let tasksTotal = Number(result.tasks_total ?? result.tasksTotal ?? 0);
172
204
  let success = typeof result.success === 'boolean' ? result.success : null;
205
+ if (result.type === 'error')
206
+ success = false;
173
207
  for (const event of events) {
174
208
  if (!event || typeof event !== 'object')
175
209
  continue;
@@ -192,7 +226,7 @@ export class BackgroundCommand {
192
226
  if (success == null && tasksTotal > 0) {
193
227
  success = tasksCompleted >= tasksTotal;
194
228
  }
195
- if (success == null && job?.status === 'completed') {
229
+ if (success == null && job?.status === 'completed' && result.type !== 'error') {
196
230
  success = true;
197
231
  }
198
232
  if (success == null && job?.status === 'failed') {
@@ -211,4 +245,27 @@ export class BackgroundCommand {
211
245
  }
212
246
  return { tasksCompleted, tasksTotal, success, headline };
213
247
  }
248
+ async approve(jobId, approvalId, options = {}) {
249
+ if (!jobId || !approvalId) {
250
+ this.logger.error('Usage: vigthoria background approve <job-id> <approval-id>');
251
+ return;
252
+ }
253
+ const approved = !options.deny;
254
+ const spinner = createSpinner(`${approved ? 'Approving' : 'Denying'} background command...`).start();
255
+ try {
256
+ const result = await this.api.resolveV3BackgroundApproval(jobId, approvalId, approved, 'once');
257
+ spinner.stop();
258
+ if (options.json) {
259
+ console.log(JSON.stringify({ ...result, job_id: jobId, approval_id: approvalId, approved }, null, 2));
260
+ return;
261
+ }
262
+ console.log(approved
263
+ ? chalk.green(`${CH.success} Approved ${approvalId} once for ${jobId}`)
264
+ : chalk.yellow(`${CH.warn} Denied ${approvalId} for ${jobId}`));
265
+ }
266
+ catch (error) {
267
+ spinner.stop();
268
+ this.logger.error(error?.message || String(error));
269
+ }
270
+ }
214
271
  }
@@ -1179,6 +1179,32 @@ export class ChatCommand {
1179
1179
  spinner.text = this.sanitizeServerPath(String(event.content || 'Routing to V3 Agent...'));
1180
1180
  return;
1181
1181
  }
1182
+ if (event.type === 'pipeline_stage') {
1183
+ if (spinner.isSpinning)
1184
+ spinner.stop();
1185
+ const stageIndex = Number.isFinite(Number(event.stage_index)) ? Number(event.stage_index) : 0;
1186
+ const stageTotal = Number.isFinite(Number(event.stage_total)) ? Number(event.stage_total) : 3;
1187
+ const stage = String(event.stage || '').trim().toLowerCase();
1188
+ const label = stage === 'dispatcher'
1189
+ ? 'Dispatcher'
1190
+ : stage === 'architect'
1191
+ ? 'Architect'
1192
+ : stage === 'executor'
1193
+ ? 'Executor'
1194
+ : 'Pipeline';
1195
+ const model = this.sanitizeServerPath(String(event.model || event.executor_model || ''));
1196
+ const route = this.sanitizeServerPath(String(event.route || ''));
1197
+ const details = [model, route ? `route=${route}` : ''].filter(Boolean).join(' · ');
1198
+ process.stderr.write(chalk.cyan(` [${stageIndex || '?'}\/${stageTotal} ${label}] `)
1199
+ + `${details || 'ready'}\n`);
1200
+ spinner.start();
1201
+ spinner.text = stage === 'dispatcher'
1202
+ ? 'Olivia routed the request. Starting Architect...'
1203
+ : stage === 'architect'
1204
+ ? 'Architect is producing the execution plan...'
1205
+ : '35B Executor is executing the approved plan...';
1206
+ return;
1207
+ }
1182
1208
  if (event.type === 'thinking') {
1183
1209
  this.v3IterationCount += 1;
1184
1210
  const iterText = this.sanitizeServerPath(event.content || '');
@@ -1481,7 +1507,8 @@ export class ChatCommand {
1481
1507
  this.sessionManager = new SessionManager();
1482
1508
  }
1483
1509
  async run(options) {
1484
- if (!this.config.isAuthenticated()) {
1510
+ const hasRuntimeToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
1511
+ if (!this.config.isAuthenticated() && !hasRuntimeToken) {
1485
1512
  if (options.json) {
1486
1513
  process.exitCode = 1;
1487
1514
  emitAgentJsonOutput({ success: false, error: 'Not authenticated. Run: vigthoria login' });
@@ -2337,6 +2364,12 @@ export class ChatCommand {
2337
2364
  : '';
2338
2365
  this.logger.debug(`Agent route: ${r.path} / ${r.taskKind} [${r.source}]${routerNote} — ${r.reason}`);
2339
2366
  }
2367
+ // V3 owns high-fidelity HTML and other complex single-file rewrites, but
2368
+ // the CLI still has the authoritative local workspace. Prime the target
2369
+ // through the client-tool bridge before handing the task to V3 so the
2370
+ // model receives the real current file and the terminal records a
2371
+ // verifiable read-before-write operation.
2372
+ await this.primeBypassedTargetFileContext(resolvedPrompt);
2340
2373
  const handledByV3Workflow = await this.tryV3AgentWorkflow(resolvedPrompt);
2341
2374
  if (handledByV3Workflow) {
2342
2375
  this.saveSession();
@@ -3209,6 +3242,7 @@ export class ChatCommand {
3209
3242
  requestedModel: this.currentModel,
3210
3243
  agentExecutionPolicy: routingPolicy,
3211
3244
  legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
3245
+ approvalLevel: this.autoApprove ? 'auto' : 'confirm',
3212
3246
  rawPrompt: prompt,
3213
3247
  contextualPrompt,
3214
3248
  history: this.getMessagesForModel(),
@@ -3688,6 +3722,7 @@ export class ChatCommand {
3688
3722
  requestedModel: this.currentModel,
3689
3723
  agentExecutionPolicy: routingPolicy,
3690
3724
  legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
3725
+ approvalLevel: this.autoApprove ? 'auto' : 'confirm',
3691
3726
  rawPrompt,
3692
3727
  history: this.getMessagesForModel(),
3693
3728
  onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
@@ -48,19 +48,31 @@ export class LegionCommand {
48
48
  }
49
49
  getHyperloopUrls() {
50
50
  const urls = new Set();
51
- const configuredApiUrl = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
52
- if (configuredApiUrl) {
53
- urls.add(`${configuredApiUrl}/api/hyperloop`);
54
- }
55
- const envUrl = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
56
- if (envUrl) {
57
- urls.add(envUrl);
58
- }
51
+ const normalizeBase = (raw) => {
52
+ let base = String(raw || '').trim().replace(/\/$/, '');
53
+ base = base.replace(/\/api\/hyperloop\/(?:health|modules|execute)$/i, '/api/hyperloop');
54
+ if (base && !/\/api\/hyperloop$/i.test(base))
55
+ base += '/api/hyperloop';
56
+ return base;
57
+ };
58
+ // On-server checks must try loopback first. The former configured 10.0.0.2
59
+ // route could consume the entire timeout while the same node was healthy.
59
60
  if (isServerRuntime()) {
60
61
  for (const internal of buildServerHyperloopUrls()) {
61
- urls.add(internal.replace(/\/$/, ''));
62
+ const normalized = normalizeBase(internal);
63
+ if (normalized)
64
+ urls.add(normalized);
62
65
  }
63
66
  }
67
+ const envUrl = normalizeBase(process.env.VIGTHORIA_HYPERLOOP_URL || '');
68
+ if (envUrl)
69
+ urls.add(envUrl);
70
+ const configuredApiUrl = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
71
+ if (configuredApiUrl) {
72
+ const normalized = normalizeBase(configuredApiUrl);
73
+ if (normalized)
74
+ urls.add(normalized);
75
+ }
64
76
  return Array.from(urls);
65
77
  }
66
78
  getHeaders() {
@@ -1495,19 +1507,42 @@ export class LegionCommand {
1495
1507
  for (const baseUrl of this.getHyperloopUrls()) {
1496
1508
  try {
1497
1509
  const response = await fetch(`${baseUrl}/status`, {
1498
- signal: AbortSignal.timeout(10000),
1510
+ signal: AbortSignal.timeout(8000),
1499
1511
  headers: this.getHeaders(),
1500
1512
  });
1513
+ let data;
1514
+ let healthOnly = false;
1501
1515
  if (!response.ok) {
1502
- lastError = `Legion status check at ${baseUrl} failed: ${response.status} ${describeUpstreamStatus(response.status)}`;
1503
- continue;
1516
+ // /status is authenticated, while the root /health endpoint is the
1517
+ // authoritative liveness probe. A CLI without the internal service
1518
+ // credential must still report a live node as online rather than
1519
+ // falling through to a stale private-network address.
1520
+ const parsed = new URL(baseUrl);
1521
+ const healthUrl = /^(?:127\.0\.0\.1|localhost|::1)$/i.test(parsed.hostname)
1522
+ ? `${parsed.origin}/health`
1523
+ : `${baseUrl}/health`;
1524
+ const healthResponse = await fetch(healthUrl, {
1525
+ signal: AbortSignal.timeout(3000),
1526
+ headers: this.getHeaders(),
1527
+ });
1528
+ if (!healthResponse.ok) {
1529
+ lastError = `Legion status check at ${baseUrl} failed: ${response.status} ${describeUpstreamStatus(response.status)}`;
1530
+ continue;
1531
+ }
1532
+ data = await this.readJsonResponse(healthResponse, `health check at ${healthUrl}`);
1533
+ healthOnly = true;
1534
+ }
1535
+ else {
1536
+ data = await this.readJsonResponse(response, `status check at ${baseUrl}`);
1504
1537
  }
1505
- const data = await this.readJsonResponse(response, `status check at ${baseUrl}`);
1506
1538
  spinner.stop();
1507
1539
  console.log();
1508
1540
  console.log(chalk.bold.white(` ${CH.hLine.repeat(3)} Legion Infrastructure ${CH.hLine.repeat(37)}`));
1509
1541
  console.log();
1510
1542
  console.log(chalk.gray(` Hyper Loop: `) + chalk.green('online'));
1543
+ if (healthOnly) {
1544
+ console.log(chalk.gray(' Control plane: liveness verified; authenticated worker details not exposed'));
1545
+ }
1511
1546
  if (data.workers || data.active_workers) {
1512
1547
  const count = data.workers || data.active_workers;
1513
1548
  console.log(chalk.gray(` Active workers: `) + chalk.white(String(typeof count === 'number' ? count : Object.keys(count).length)));
package/dist/index.js CHANGED
@@ -472,10 +472,10 @@ async function enforceGatewayAuthSession(config, logger, jsonOutputRequested) {
472
472
  if (isLocalTestfarmMode()) {
473
473
  return true;
474
474
  }
475
- if (!config.isAuthenticated()) {
475
+ const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
476
+ if (!config.isAuthenticated() && !explicitEnvToken) {
476
477
  return true;
477
478
  }
478
- const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
479
479
  // Offline mode disables every outbound preflight call.
480
480
  if (isOfflineMode()) {
481
481
  return true;
@@ -1317,6 +1317,7 @@ Examples:
1317
1317
  .command('start <prompt...>')
1318
1318
  .description('Start an Agent job in the background and return immediately')
1319
1319
  .option('--workspace <path>', 'Local workspace path', process.cwd())
1320
+ .option('--model <model>', 'Execution lane: agent, cloud, cloud-reason, or ultra', 'agent')
1320
1321
  .option('--json', 'Emit JSON output', false)
1321
1322
  .action(async (prompt, options) => {
1322
1323
  const bg = new BackgroundCommand(config, logger);
@@ -1357,6 +1358,15 @@ Examples:
1357
1358
  const bg = new BackgroundCommand(config, logger);
1358
1359
  await bg.cancel(jobId, options);
1359
1360
  });
1361
+ backgroundCommand
1362
+ .command('approve <jobId> <approvalId>')
1363
+ .description('Resolve a pending two-stage command gate for a background job')
1364
+ .option('--deny', 'Deny the command instead of approving it', false)
1365
+ .option('--json', 'Emit JSON output', false)
1366
+ .action(async (jobId, approvalId, options) => {
1367
+ const bg = new BackgroundCommand(config, logger);
1368
+ await bg.approve(jobId, approvalId, options);
1369
+ });
1360
1370
  backgroundCommand.action(() => {
1361
1371
  backgroundCommand.outputHelp();
1362
1372
  });
@@ -261,6 +261,7 @@ export declare class APIClient {
261
261
  getV3AgentRunUrl(baseUrl: string): string;
262
262
  getV3AgentContinueUrl(baseUrl: string): string;
263
263
  getV3AgentBackgroundUrl(baseUrl: string, suffix?: string): string;
264
+ getV3AgentApprovalUrl(baseUrl: string): string;
264
265
  getOperatorBaseUrls(): string[];
265
266
  getOperatorStreamUrl(baseUrl: string): string;
266
267
  getMcpBaseUrls(): string[];
@@ -359,6 +360,12 @@ export declare class APIClient {
359
360
  private buildPublicWorkspaceDescriptor;
360
361
  private buildPublicRuntimeEnvironment;
361
362
  private buildLocalWorkspaceSummary;
363
+ /**
364
+ * Build complete workspace hydration separately from model context.
365
+ * Remote audits must inspect the same byte-complete assets the user has
366
+ * locally; context compaction must never become filesystem compaction.
367
+ */
368
+ private buildOutOfBandWorkspaceFiles;
362
369
  /**
363
370
  * Collect text file contents from the workspace for V3 agent hydration.
364
371
  * Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
@@ -405,10 +412,12 @@ export declare class APIClient {
405
412
  listV3BackgroundJobs(limit?: number): Promise<any[]>;
406
413
  getV3BackgroundJob(jobId: string): Promise<any>;
407
414
  cancelV3BackgroundJob(jobId: string): Promise<any>;
415
+ resolveV3BackgroundApproval(jobId: string, approvalId: string, approved: boolean, allowMode?: string): Promise<any>;
408
416
  getV3BackgroundJobFiles(jobId: string): Promise<{
409
417
  status: string;
410
418
  files: Record<string, string>;
411
419
  file_count: number;
420
+ confirmed_mutations?: string[];
412
421
  local_workspace_path?: string;
413
422
  }>;
414
423
  applyV3BackgroundJobFiles(jobId: string, context?: Record<string, any>): Promise<{
package/dist/utils/api.js CHANGED
@@ -659,6 +659,7 @@ export class APIClient {
659
659
  getV3AgentBaseUrls(preferLocal = false) {
660
660
  const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
661
661
  const localTestMode = isLocalTestfarmMode();
662
+ const explicitV3EndpointConfigured = Boolean(process.env.VIGTHORIA_V3_AGENT_URL || process.env.V3_AGENT_URL);
662
663
  const includeLoopbackV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1'
663
664
  || (preferLocal && isServerRuntime());
664
665
  const localCandidates = [
@@ -672,7 +673,10 @@ export class APIClient {
672
673
  const normalizedRemote = remoteCandidates
673
674
  .filter(Boolean)
674
675
  .map((url) => String(url).replace(/\/$/, ''));
675
- const urls = preferLocal && localCandidates.length > 0
676
+ // An explicit V3 endpoint is an operator routing decision, not a fallback.
677
+ // Keep it ahead of the public Coder relay so stale remote user credentials
678
+ // cannot abort a healthy service-key-authenticated internal CLI run.
679
+ const urls = (preferLocal || localTestMode || explicitV3EndpointConfigured) && localCandidates.length > 0
676
680
  ? [...localCandidates, ...normalizedRemote]
677
681
  : [...normalizedRemote, ...localCandidates];
678
682
  return [...new Set(urls)];
@@ -696,6 +700,12 @@ export class APIClient {
696
700
  }
697
701
  return `${baseUrl}/api/v3-agent/background${cleanSuffix}`;
698
702
  }
703
+ getV3AgentApprovalUrl(baseUrl) {
704
+ if (this.isDirectV3AgentBaseUrl(baseUrl)) {
705
+ return `${baseUrl}/api/agent/approval`;
706
+ }
707
+ return `${baseUrl}/api/v3-agent/approval`;
708
+ }
699
709
  getOperatorBaseUrls() {
700
710
  const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
701
711
  const urls = [
@@ -876,6 +886,7 @@ export class APIClient {
876
886
  extractLinkedFrontendAssets(html, entryPath) {
877
887
  const css = new Set();
878
888
  const js = new Set();
889
+ const assets = new Set();
879
890
  const entryDir = path.posix.dirname(this.normalizeWorkspaceRelativePath(entryPath) || '.');
880
891
  const normalizeAsset = (assetPath) => {
881
892
  const clean = String(assetPath || '').trim();
@@ -887,7 +898,10 @@ export class APIClient {
887
898
  return null;
888
899
  }
889
900
  if (withoutQuery.startsWith('/')) {
890
- return this.normalizeWorkspaceRelativePath(withoutQuery.slice(1));
901
+ // Preview/runtime serve a nested HTML entry from its containing
902
+ // directory, so `/styles.css` beside `showcase/index.html` resolves to
903
+ // `showcase/styles.css` in the hydrated workspace.
904
+ return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery.slice(1))));
891
905
  }
892
906
  return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery)));
893
907
  };
@@ -906,9 +920,17 @@ export class APIClient {
906
920
  js.add(resolved);
907
921
  }
908
922
  }
923
+ const assetPattern = /<(?:link|img|source|video|audio)\b[^>]+(?:href|src)=["']([^"']+)["'][^>]*>/gi;
924
+ while ((match = assetPattern.exec(String(html || ''))) !== null) {
925
+ const resolved = normalizeAsset(match[1]);
926
+ if (resolved) {
927
+ assets.add(resolved);
928
+ }
929
+ }
909
930
  return {
910
931
  css: Array.from(css),
911
932
  js: Array.from(js),
933
+ assets: Array.from(assets),
912
934
  };
913
935
  }
914
936
  async buildFrontendForPreview(rootPath) {
@@ -1568,10 +1590,15 @@ export class APIClient {
1568
1590
  'styles.css', 'style.css', 'README.md', 'manifest.json',
1569
1591
  ]);
1570
1592
  /** Keep critical workspace files for server hydration when context must shrink. */
1571
- compactWorkspaceFilesToBudget(files, budgetChars) {
1593
+ compactWorkspaceFilesToBudget(files, budgetChars, priorityPaths = [], mandatoryPaths = priorityPaths) {
1572
1594
  const entries = Object.entries(files);
1595
+ const explicitPriority = new Map(priorityPaths.map((filePath, index) => [this.normalizeWorkspaceRelativePath(filePath), index]));
1596
+ const mandatory = new Set(mandatoryPaths.map((filePath) => this.normalizeWorkspaceRelativePath(filePath)));
1573
1597
  const rank = (filePath) => {
1574
1598
  const normalized = filePath.replace(/\\/g, '/');
1599
+ const explicitIndex = explicitPriority.get(normalized);
1600
+ if (explicitIndex !== undefined)
1601
+ return -100 + Math.min(explicitIndex, 50);
1575
1602
  const base = normalized.split('/').pop() || '';
1576
1603
  if (APIClient.HYDRATION_PRIORITY_BASENAMES.has(base))
1577
1604
  return 0;
@@ -1589,6 +1616,28 @@ export class APIClient {
1589
1616
  const clipped = content.length > maxContentLen ? `${content.slice(0, maxContentLen)}\n/* … truncated for remote context … */` : content;
1590
1617
  const entryLen = JSON.stringify(filePath).length + 1 + JSON.stringify(clipped).length + 1;
1591
1618
  if (used + entryLen > budgetChars) {
1619
+ const normalizedPath = this.normalizeWorkspaceRelativePath(filePath);
1620
+ if (mandatory.has(normalizedPath)) {
1621
+ // Explicit prompt targets are hydration requirements, not ranking
1622
+ // hints. Skipping an HTML entry merely because a larger targeted
1623
+ // stylesheet consumed the provisional budget creates false preview
1624
+ // failures in the remote V3 workspace. Keep the complete explicit
1625
+ // file and let later context-compaction phases shed history/metadata.
1626
+ trimmed[filePath] = clipped;
1627
+ used += entryLen;
1628
+ }
1629
+ else if (explicitPriority.has(normalizedPath)) {
1630
+ // Linked preview assets are important enough to hydrate, but unlike
1631
+ // edit targets they may be compacted. Keep a bounded prefix so the
1632
+ // remote static server resolves the asset instead of returning 404.
1633
+ const envelope = JSON.stringify(filePath).length + 128;
1634
+ const available = Math.max(256, budgetChars - used - envelope);
1635
+ const dependencyClip = content.length > available
1636
+ ? `${content.slice(0, available)}\n/* … compacted linked preview dependency … */`
1637
+ : content;
1638
+ trimmed[filePath] = dependencyClip;
1639
+ used += JSON.stringify(filePath).length + 1 + JSON.stringify(dependencyClip).length + 1;
1640
+ }
1592
1641
  continue;
1593
1642
  }
1594
1643
  trimmed[filePath] = clipped;
@@ -1603,16 +1652,30 @@ export class APIClient {
1603
1652
  const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
1604
1653
  const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
1605
1654
  const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
1655
+ if (resolvedContext.workspaceFilesOutOfBand === true && localWorkspaceSummary?.workspaceFiles) {
1656
+ localWorkspaceSummary.workspaceHydration = {
1657
+ delivery: 'out-of-band',
1658
+ fileCount: Object.keys(localWorkspaceSummary.workspaceFiles).length,
1659
+ complete: true,
1660
+ };
1661
+ delete localWorkspaceSummary.workspaceFiles;
1662
+ }
1606
1663
  const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
1607
1664
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
1608
1665
  const localWorkspaceName = this.getDisplayWorkspaceName(localWorkspacePath);
1609
1666
  const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
1610
1667
  const executionSurface = String(resolvedContext.executionSurface || resolvedContext.clientSurface || 'cli');
1611
1668
  const localMachineCapable = resolvedContext.localMachineCapable !== false;
1669
+ // A workspace under a configured server root is already available to the
1670
+ // V3 service. Keep tool execution on the server in that case; delegating
1671
+ // tools back to the CLI can turn server paths into invalid client paths.
1672
+ // An explicit caller override remains authoritative for specialist flows.
1612
1673
  const clientToolExecution = resolvedContext.clientToolExecution === false
1613
1674
  ? false
1614
1675
  : (resolvedContext.clientToolExecution === true
1615
- || (localMachineCapable && ['cli', 'fork', 'local-ide', 'desktop', 'local'].includes(executionSurface)));
1676
+ || (!serverWorkspacePath
1677
+ && localMachineCapable
1678
+ && ['cli', 'fork', 'local-ide', 'desktop', 'local'].includes(executionSurface)));
1616
1679
  const publicRuntimeEnvironment = this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
1617
1680
  localWorkspacePath,
1618
1681
  serverWorkspacePath,
@@ -1658,6 +1721,8 @@ export class APIClient {
1658
1721
  requestedModelResolved: resolvedModel,
1659
1722
  agentExecutionPolicy: resolvedContext.agentExecutionPolicy || null,
1660
1723
  legacyFallbackAllowed: resolvedContext.legacyFallbackAllowed === true,
1724
+ approvalLevel: resolvedContext.approvalLevel === 'auto' ? 'auto' : 'confirm',
1725
+ approval_level: resolvedContext.approvalLevel === 'auto' ? 'auto' : 'confirm',
1661
1726
  executionSurface: resolvedContext.executionSurface || 'cli',
1662
1727
  clientSurface: resolvedContext.clientSurface || 'cli',
1663
1728
  localMachineCapable,
@@ -1752,11 +1817,11 @@ export class APIClient {
1752
1817
  const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
1753
1818
  const budget = LIMIT - overhead - 1024;
1754
1819
  if (budget > 0) {
1755
- summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, budget);
1820
+ summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, budget, summary.focusFiles || [], summary.explicitFocusFiles || summary.focusFiles || []);
1756
1821
  summary.workspaceFilesCompaction = Object.keys(summary.workspaceFiles).length > 0 ? 'priority-trimmed' : 'empty';
1757
1822
  }
1758
1823
  else if (hydrationRequired) {
1759
- summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 8_000);
1824
+ summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 8_000, summary.focusFiles || [], summary.explicitFocusFiles || summary.focusFiles || []);
1760
1825
  summary.workspaceFilesCompaction = 'priority-minimal';
1761
1826
  }
1762
1827
  else {
@@ -1781,7 +1846,7 @@ export class APIClient {
1781
1846
  return finish();
1782
1847
  }
1783
1848
  if (hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
1784
- summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 4_000);
1849
+ summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 4_000, summary.focusFiles || [], summary.explicitFocusFiles || summary.focusFiles || []);
1785
1850
  summary.workspaceFilesCompaction = 'priority-minimal';
1786
1851
  json = JSON.stringify(payload);
1787
1852
  if (json.length <= LIMIT)
@@ -2657,8 +2722,30 @@ menu {
2657
2722
  };
2658
2723
  const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
2659
2724
  const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
2725
+ const focusFiles = this.extractExpectedWorkspaceFiles(requestFocus)
2726
+ .map((entry) => this.normalizeWorkspaceRelativePath(entry))
2727
+ .filter((entry) => candidatePaths.includes(entry));
2728
+ const linkedFocusAssets = [];
2729
+ for (const focusPath of focusFiles) {
2730
+ if (!/\.html?$/i.test(focusPath))
2731
+ continue;
2732
+ try {
2733
+ const html = fs.readFileSync(path.join(rootPath, focusPath), 'utf8');
2734
+ const linkedAssets = this.extractLinkedFrontendAssets(html, focusPath);
2735
+ for (const assetPath of [...linkedAssets.assets, ...linkedAssets.css, ...linkedAssets.js]) {
2736
+ if (candidatePaths.includes(assetPath) && !linkedFocusAssets.includes(assetPath)) {
2737
+ linkedFocusAssets.push(assetPath);
2738
+ }
2739
+ }
2740
+ }
2741
+ catch {
2742
+ // Missing/unreadable focused HTML remains visible to the server via
2743
+ // the original focus list; dependency expansion is best effort.
2744
+ }
2745
+ }
2746
+ const hydrationFocusFiles = [...focusFiles, ...linkedFocusAssets];
2660
2747
  const agentStatePaths = this.collectAgentStateSyncPaths(rootPath);
2661
- let orderedPaths = [...agentStatePaths, ...candidatePaths];
2748
+ let orderedPaths = [...agentStatePaths, ...hydrationFocusFiles, ...candidatePaths];
2662
2749
  try {
2663
2750
  const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
2664
2751
  const changes = getChangedFiles(rootPath, candidatePaths);
@@ -2670,6 +2757,8 @@ menu {
2670
2757
  // silently dropped progress.json and forced /continue to re-plan.
2671
2758
  for (const filePath of agentStatePaths)
2672
2759
  ordered.add(filePath);
2760
+ for (const filePath of hydrationFocusFiles)
2761
+ ordered.add(filePath);
2673
2762
  for (const filePath of changes.changed)
2674
2763
  ordered.add(filePath);
2675
2764
  for (const filePath of ranked)
@@ -2685,9 +2774,11 @@ menu {
2685
2774
  };
2686
2775
  }
2687
2776
  catch {
2688
- orderedPaths = [...agentStatePaths, ...candidatePaths];
2777
+ orderedPaths = [...agentStatePaths, ...hydrationFocusFiles, ...candidatePaths];
2689
2778
  }
2690
2779
  summary.fileCount = snapshot.fileCount;
2780
+ summary.focusFiles = hydrationFocusFiles;
2781
+ summary.explicitFocusFiles = focusFiles;
2691
2782
  summary.files = orderedPaths.slice(0, 40);
2692
2783
  const packageJsonPath = path.join(rootPath, 'package.json');
2693
2784
  if (fs.existsSync(packageJsonPath)) {
@@ -2714,6 +2805,22 @@ menu {
2714
2805
  return null;
2715
2806
  }
2716
2807
  }
2808
+ /**
2809
+ * Build complete workspace hydration separately from model context.
2810
+ * Remote audits must inspect the same byte-complete assets the user has
2811
+ * locally; context compaction must never become filesystem compaction.
2812
+ */
2813
+ buildOutOfBandWorkspaceFiles(context) {
2814
+ const resolvedContext = this.ensureExecutionContext(context);
2815
+ const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
2816
+ const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
2817
+ if (serverWorkspacePath || !localWorkspacePath)
2818
+ return undefined;
2819
+ const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
2820
+ const summary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
2821
+ const files = summary?.workspaceFiles;
2822
+ return files && typeof files === 'object' ? files : undefined;
2823
+ }
2717
2824
  /**
2718
2825
  * Collect text file contents from the workspace for V3 agent hydration.
2719
2826
  * Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
@@ -2722,7 +2829,7 @@ menu {
2722
2829
  const MAX_TOTAL_BYTES = 2 * 1024 * 1024;
2723
2830
  const MAX_FILE_BYTES = 200 * 1024;
2724
2831
  const BINARY_EXTENSIONS = new Set([
2725
- '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.avif',
2832
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.avif',
2726
2833
  '.mp3', '.mp4', '.wav', '.ogg', '.webm', '.flac', '.aac',
2727
2834
  '.zip', '.gz', '.tar', '.rar', '.7z', '.bz2',
2728
2835
  '.exe', '.dll', '.so', '.dylib', '.bin', '.dat',
@@ -3028,7 +3135,9 @@ menu {
3028
3135
  const args = event.arguments || {};
3029
3136
  const serverRoot = String(context.__v3ServerWorkspaceRoot || '').trim();
3030
3137
  const isRunCommand = name === 'run_command';
3031
- const pathArg = isRunCommand ? (args.cwd ?? args.path ?? '.') : (args.path || args.cwd || '.');
3138
+ const pathArg = isRunCommand
3139
+ ? (args.cwd ?? args.path ?? args.directory ?? '.')
3140
+ : (args.path || args.directory || args.cwd || '.');
3032
3141
  let target = this.resolveV3ClientToolPath(rootPath, pathArg, serverRoot ? [serverRoot] : []);
3033
3142
  if (!target && isRunCommand) {
3034
3143
  target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
@@ -4120,6 +4229,7 @@ document.addEventListener('DOMContentLoaded', () => {
4120
4229
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
4121
4230
  const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
4122
4231
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
4232
+ const workspaceFiles = this.buildOutOfBandWorkspaceFiles(executionContext);
4123
4233
  const preferLocalV3 = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|responsive|animated|create the required project files and write them to the workspace)/i.test(message)
4124
4234
  && context.localMachineCapable !== false;
4125
4235
  const timeoutMs = resolvePlannerAgentTimeoutMs(baseTimeoutMs, String(executionContext.agentTaskType || ''), String(executionContext.rawPrompt || message || ''));
@@ -4149,7 +4259,8 @@ document.addEventListener('DOMContentLoaded', () => {
4149
4259
  strict_mode: useRelaxedAttempt ? false : requestExecutionContext.legacyFallbackAllowed !== true,
4150
4260
  context: useRelaxedAttempt
4151
4261
  ? this.buildMinimalV3AgentContext(requestExecutionContext)
4152
- : this.buildV3AgentContext(requestExecutionContext),
4262
+ : this.buildV3AgentContext({ ...requestExecutionContext, workspaceFilesOutOfBand: !!workspaceFiles }),
4263
+ workspace_files: useRelaxedAttempt ? undefined : workspaceFiles,
4153
4264
  context_id: contextIdOverride ?? requestExecutionContext.contextId,
4154
4265
  mcp_context_id: useRelaxedAttempt ? null : requestExecutionContext.mcpContextId || null,
4155
4266
  stream: true,
@@ -4364,18 +4475,29 @@ document.addEventListener('DOMContentLoaded', () => {
4364
4475
  await this.ensureV3ServiceKey();
4365
4476
  const executionContext = await this.bindExecutionContext({
4366
4477
  ...context,
4478
+ // Background callers historically supplied only workspace metadata.
4479
+ // Without the request in the execution context, buildV3AgentContext()
4480
+ // classified every detached job from an empty prompt and emitted an
4481
+ // `analysis_only` workflow. V3 would then inspect the workspace but was
4482
+ // correctly forbidden from writing. Preserve an explicit caller value,
4483
+ // otherwise make the submitted job request authoritative for routing.
4484
+ rawPrompt: context.rawPrompt || context.prompt || message,
4485
+ prompt: context.prompt || context.rawPrompt || message,
4367
4486
  backgroundJob: true,
4368
4487
  clientToolExecution: false,
4369
4488
  });
4370
4489
  const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
4371
4490
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
4491
+ const workspaceFiles = this.buildOutOfBandWorkspaceFiles(executionContext);
4372
4492
  const body = {
4373
4493
  request: message,
4374
4494
  context: this.buildV3AgentContext({
4375
4495
  ...executionContext,
4376
4496
  backgroundJob: true,
4377
4497
  clientToolExecution: false,
4498
+ workspaceFilesOutOfBand: !!workspaceFiles,
4378
4499
  }),
4500
+ workspace_files: workspaceFiles,
4379
4501
  stream: false,
4380
4502
  model: resolvedModel,
4381
4503
  mcp_context_id: executionContext.mcpContextId || undefined,
@@ -4464,6 +4586,33 @@ document.addEventListener('DOMContentLoaded', () => {
4464
4586
  }
4465
4587
  throw new Error(`Unable to cancel background job: ${errors.join('; ')}`);
4466
4588
  }
4589
+ async resolveV3BackgroundApproval(jobId, approvalId, approved, allowMode = 'once') {
4590
+ await this.ensureV3ServiceKey();
4591
+ const errors = [];
4592
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
4593
+ try {
4594
+ const response = await fetch(this.getV3AgentApprovalUrl(baseUrl), {
4595
+ method: 'POST',
4596
+ headers: await this.getV3AgentHeaders(),
4597
+ body: JSON.stringify({
4598
+ context_id: jobId,
4599
+ approval_id: approvalId,
4600
+ approved,
4601
+ allow_mode: allowMode,
4602
+ }),
4603
+ });
4604
+ if (!response.ok) {
4605
+ errors.push(`${baseUrl}: ${response.status}`);
4606
+ continue;
4607
+ }
4608
+ return await response.json();
4609
+ }
4610
+ catch (error) {
4611
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
4612
+ }
4613
+ }
4614
+ throw new Error(`Unable to resolve background approval: ${errors.join('; ')}`);
4615
+ }
4467
4616
  async getV3BackgroundJobFiles(jobId) {
4468
4617
  await this.ensureV3ServiceKey();
4469
4618
  const errors = [];
@@ -4486,7 +4635,47 @@ document.addEventListener('DOMContentLoaded', () => {
4486
4635
  throw new Error(`Unable to fetch background job files: ${errors.join('; ')}`);
4487
4636
  }
4488
4637
  async applyV3BackgroundJobFiles(jobId, context = {}) {
4489
- const data = await this.getV3BackgroundJobFiles(jobId);
4638
+ const [data, job] = await Promise.all([
4639
+ this.getV3BackgroundJobFiles(jobId),
4640
+ this.getV3BackgroundJob(jobId),
4641
+ ]);
4642
+ if (job?.status !== 'completed' || job?.result?.success === false || job?.result?.type === 'error') {
4643
+ throw new Error(`Background job ${jobId} is not a successful completed job and cannot be applied.`);
4644
+ }
4645
+ const mutatedPaths = new Set();
4646
+ const addMutatedPath = (value) => {
4647
+ const normalized = this.normalizeWorkspaceRelativePath(String(value || ''));
4648
+ if (!normalized || path.isAbsolute(normalized) || normalized.split('/').includes('..'))
4649
+ return;
4650
+ mutatedPaths.add(normalized);
4651
+ };
4652
+ for (const filePath of data.confirmed_mutations || []) {
4653
+ addMutatedPath(filePath);
4654
+ }
4655
+ for (const event of Array.isArray(job?.events) ? job.events : []) {
4656
+ if (event?.type === 'file_mutation' && event?.action !== 'delete') {
4657
+ addMutatedPath(event.path);
4658
+ }
4659
+ if (event?.type === 'executor_complete') {
4660
+ let summary = event.summary;
4661
+ if (typeof summary === 'string') {
4662
+ try {
4663
+ summary = JSON.parse(summary);
4664
+ }
4665
+ catch {
4666
+ summary = null;
4667
+ }
4668
+ }
4669
+ if (summary?.status === 'completed') {
4670
+ for (const filePath of [...(summary.changed_files || []), ...(summary.created_files || [])]) {
4671
+ addMutatedPath(filePath);
4672
+ }
4673
+ }
4674
+ }
4675
+ }
4676
+ if (mutatedPaths.size === 0) {
4677
+ throw new Error(`Background job ${jobId} has no confirmed successful file mutations to apply.`);
4678
+ }
4490
4679
  const rootPath = this.resolveAgentTargetPath({
4491
4680
  ...context,
4492
4681
  workspacePath: context.workspacePath || data.local_workspace_path,
@@ -4499,6 +4688,11 @@ document.addEventListener('DOMContentLoaded', () => {
4499
4688
  const applied = [];
4500
4689
  const skipped = [];
4501
4690
  for (const [relativePath, content] of Object.entries(data.files || {})) {
4691
+ const normalizedRelativePath = this.normalizeWorkspaceRelativePath(relativePath);
4692
+ if (!mutatedPaths.has(normalizedRelativePath)) {
4693
+ skipped.push(relativePath);
4694
+ continue;
4695
+ }
4502
4696
  if (typeof content !== 'string') {
4503
4697
  skipped.push(relativePath);
4504
4698
  continue;
@@ -1,5 +1,14 @@
1
1
  export type ClientManifestPayload = {
2
2
  product_id: string;
3
+ context_contract_version: string;
4
+ source_product_id: string;
5
+ active_application_id: string;
6
+ active_surface_id: string;
7
+ target_product_id: string;
8
+ workspace_kind: string;
9
+ selected_engine_id: string;
10
+ capability_catalog_version: string;
11
+ capability_scope_id: string;
3
12
  version: string;
4
13
  os: string;
5
14
  execution_mode: string;
@@ -56,8 +56,18 @@ export function resolveClientOsSlug() {
56
56
  }
57
57
  export function buildClientManifest(overrides = {}) {
58
58
  const ios = isIOSEnvironment() && !overrides.os;
59
+ const productId = overrides.product_id || process.env.VIGTHORIA_SOURCE_PRODUCT_ID || 'vigthoria-cli';
59
60
  return {
60
- product_id: overrides.product_id || 'vigthoria-cli',
61
+ product_id: productId,
62
+ context_contract_version: overrides.context_contract_version || '2.0',
63
+ source_product_id: overrides.source_product_id || process.env.VIGTHORIA_SOURCE_PRODUCT_ID || productId,
64
+ active_application_id: overrides.active_application_id || process.env.VIGTHORIA_ACTIVE_APPLICATION_ID || 'cli-agent-chat',
65
+ active_surface_id: overrides.active_surface_id || process.env.VIGTHORIA_ACTIVE_SURFACE_ID || 'agent-session',
66
+ target_product_id: overrides.target_product_id || 'none',
67
+ workspace_kind: overrides.workspace_kind || process.env.VIGTHORIA_WORKSPACE_KIND || 'local-development-workspace',
68
+ selected_engine_id: overrides.selected_engine_id || 'none',
69
+ capability_catalog_version: overrides.capability_catalog_version || process.env.VIGTHORIA_CAPABILITY_CATALOG_VERSION || '2026.08.03.1',
70
+ capability_scope_id: overrides.capability_scope_id || process.env.VIGTHORIA_CAPABILITY_SCOPE_ID || 'cli.ecosystem-user',
61
71
  version: overrides.version || readPackageVersion(),
62
72
  os: overrides.os || resolveClientOsSlug(),
63
73
  execution_mode: overrides.execution_mode || (ios ? 'mobile-terminal-sandbox' : 'local-user-machine'),
@@ -3,6 +3,10 @@ const AGENT_COMMANDS = new Set([
3
3
  'agent', 'chat', 'chat-resume', 'operator', 'legion', 'workflow',
4
4
  'history', 'replay', 'fork', 'cancel',
5
5
  ]);
6
+ // Subprocess protocols are not user-facing commands. Presenting one in the
7
+ // root picker caused `v4-menu` to be launched without its required output
8
+ // file, which is exactly the failure reported from CLI 1.13.13.
9
+ const INTERNAL_COMMANDS = new Set(['v4-menu']);
6
10
  function familyFor(commandPath) {
7
11
  const root = commandPath.split(' ')[0];
8
12
  if (root === 'repo')
@@ -15,6 +19,8 @@ export function buildCommandCatalog(program) {
15
19
  const entries = [];
16
20
  const visit = (command, parents) => {
17
21
  const commandPath = [...parents, command.name()].join(' ');
22
+ if (parents.length === 0 && INTERNAL_COMMANDS.has(commandPath))
23
+ return;
18
24
  entries.push({
19
25
  path: commandPath,
20
26
  description: command.description() || '',
@@ -15,7 +15,11 @@ export declare function estimateJsonTokens(value: unknown): number;
15
15
  export declare function resolveInputBudget(runtimeNctx?: number, maxOutput?: number): number;
16
16
  /**
17
17
  * Char limit for the initial V3 context JSON payload.
18
- * ~45% of input token budget × 4 chars/token, clamped 32k–100k.
18
+ * The hydration payload is transport-only: the server writes workspaceFiles
19
+ * to an isolated directory and exposes only a compact filename summary to the
20
+ * model. It can therefore use the full calculated input envelope without
21
+ * consuming system/tool framing tokens. Clamp at 180k, far below the gateway
22
+ * body limit, while preserving compaction for genuinely large workspaces.
19
23
  */
20
24
  export declare function resolveV3ContextCharLimit(runtimeNctx?: number): number;
21
25
  export declare function compactToolListingOutput(name: string, output: string, charCap: number): string;
@@ -29,13 +29,16 @@ export function resolveInputBudget(runtimeNctx = DEFAULT_CODE_RUNTIME_CTX, maxOu
29
29
  }
30
30
  /**
31
31
  * Char limit for the initial V3 context JSON payload.
32
- * ~45% of input token budget × 4 chars/token, clamped 32k–100k.
32
+ * The hydration payload is transport-only: the server writes workspaceFiles
33
+ * to an isolated directory and exposes only a compact filename summary to the
34
+ * model. It can therefore use the full calculated input envelope without
35
+ * consuming system/tool framing tokens. Clamp at 180k, far below the gateway
36
+ * body limit, while preserving compaction for genuinely large workspaces.
33
37
  */
34
38
  export function resolveV3ContextCharLimit(runtimeNctx = DEFAULT_CODE_RUNTIME_CTX) {
35
39
  const inputBudget = resolveInputBudget(runtimeNctx);
36
- const initialTurnTokens = Math.floor(inputBudget * 0.45);
37
- const charLimit = initialTurnTokens * CHARS_PER_TOKEN_EST;
38
- return Math.min(100_000, Math.max(32_000, charLimit));
40
+ const hydrationChars = inputBudget * CHARS_PER_TOKEN_EST;
41
+ return Math.min(180_000, Math.max(32_000, hydrationChars));
39
42
  }
40
43
  export function compactToolListingOutput(name, output, charCap) {
41
44
  const text = String(output || '');
@@ -4,14 +4,15 @@
4
4
  */
5
5
  const CLI_SHAPING_BLOCK = /(?:^|\n\n)Platform:\s*(?:Windows|macOS|Linux)\.[\s\S]*?(?=\n\n[A-Z][^\n]{0,40}:|\s*$)/i;
6
6
  const READONLY_SHAPING_BLOCK = /(?:^|\n\n)(?:Read-only analysis mode is active\.|Diagnostic mode is active\.)[\s\S]*?(?=\n\n[A-Z][^\n]{0,40}:|\s*$)/i;
7
- const BUILD_VERBS = /\b(build|create|make|implement|complete|fix|repair|edit|modify|write|generate|add|finish|scaffold|develop|update|change|refactor|want|need|require|looking for|let's|let us|we need|we should|erstelle|erstellen|schreib|schreibe|bearbeite)\b/i;
8
- const ARTIFACT_NOUNS = /\b(file|files|project|game|spiel|app|website|html5|frontend|component|feature|code|workspace|repo|clone|replica|remake|page|site|screen|button|popup|modal|alert|canvas|sprite|level|levels|enemy|enemies|character|npc|world|scene|script|module|api|database|config|landing|dashboard|platformer|arcade|playable|collectible|scoreboard|leaderboard|pitfall|pacman|rogue|html|css|javascript|typescript)\b/i;
7
+ const BUILD_VERBS = /\b(build|create|make|implement|complete|redesign|overhaul|rebuild|transform|polish|art-direct|fix|repair|edit|modify|write|generate|add|finish|scaffold|develop|update|change|refactor|want|need|require|looking for|let's|let us|we need|we should|erstelle|erstellen|schreib|schreibe|bearbeite)\b/i;
8
+ const ARTIFACT_NOUNS = /\b(file|files|project|game|spiel|app|website|html5|frontend|component|feature|code|workspace|repo|clone|replica|remake|page|site|design|showcase|screen|button|popup|modal|alert|canvas|sprite|level|levels|enemy|enemies|character|npc|world|scene|script|module|api|database|config|landing|dashboard|platformer|arcade|playable|collectible|scoreboard|leaderboard|pitfall|pacman|rogue|html|css|javascript|typescript)\b/i;
9
9
  /** Canvas/playable games only — bare "html5" or "website" must NOT match. */
10
10
  const GAME_INTENT = /\b(game|spiel|playable|babylon(?:\.js|js)?|vge|webgl|3d\s+(?:game|runner|prototype|world|experience)|html5\s+(?:game|canvas)|html5\s+game|canvas\s+game|game\s+canvas|arcade|platformer|side[- ]?scroller|pitfall|pac[- ]?man|tetris|snake|breakout|pong|roguelike|metroidvania|tower\s+defense|clone\s+of\s+(?:a\s+)?(?:game|pitfall|pac|mario|zelda|arcade)|gameplay|collectible|boss\s+fight|level\s+design|sprite\s+sheet|wild\s+(?:boar|pig|forest)|forest\s+pig)\b/i;
11
11
  const WEB_PAGE_INTENT = /\b(website|web\s*site|webpage|web\s*page|landing\s+page|home\s*page|index\.html|html\s+page|static\s+page|single\s+page|popup|alert|modal|hello\s+world)\b/i;
12
+ const WEB_SHOWCASE_INTENT = /\b(web[- ]?design\s+showcase|frontend|marketing\s+site|dashboard|saas)\b/i;
12
13
  const CLONE_BUILD = /\bclone\s+of\b|\b(build|create|make)\s+(?:a|an|the|us|me)?\s*(?:clone|replica|remake|port)\b/i;
13
14
  const READ_ONLY_INTENT = /\b(analy[sz]e|analyse|analysis|audit|review|inspect|proof|understand|summari[sz]e|scan|read[\s-]?only|where we left|what is|how does|show me|tell me|identify\s+gaps?|gap\s+analysis|production\s+blockers?)\b/i;
14
- const EXPLICIT_WRITE_VERBS = /\b(implement|write|edit|modify|update|fix|repair|create|build|generate|scaffold|deploy|refactor|develop|integrate|correct|apply|patch)\b/i;
15
+ const EXPLICIT_WRITE_VERBS = /\b(implement|write|edit|modify|update|redesign|overhaul|rebuild|transform|polish|art-direct|fix|repair|create|build|generate|scaffold|deploy|refactor|develop|integrate|correct|apply|patch)\b/i;
15
16
  const TRIVIAL_HTML_PAGE = /\b(hello\s*world|simple\s+(?:html|web|page|site)|html5\s+website|html\s+5\s+website|popup|alert\s*\(|show\s+a\s+popup|one\s+page|single\s+html)\b/i;
16
17
  /** User explicitly grants write permission after an analysis or confirmation gate. */
17
18
  const WRITE_PERMISSION_GRANT = /\b(?:hereby\s+confirm|i\s+confirm|confirm(?:ed)?\s+that|give\s+(?:you|permission)|granting\s+permission|allow\s+(?:you|me\s+to)|you\s+(?:may|can|have\s+permission\s+to)|permission\s+to|authorized?\s+to|approve(?:d)?\s+(?:the\s+)?(?:changes?|writes?|edits?)|go\s+ahead\s+and(?:\s+write|\s+apply|\s+make|\s+do)?|do\s+all\s+the\s+changes|apply\s+(?:the\s+)?(?:changes?|fixes?|fix)|proceed\s+with\s+(?:the\s+)?(?:changes?|fixes?|implementation|writes?)|make\s+(?:the\s+)?changes|please\s+write)\b/i;
@@ -111,7 +112,7 @@ export function isContinueOrRetryPrompt(prompt) {
111
112
  }
112
113
  export function hasWebPageIntent(prompt) {
113
114
  const text = stripExecutionShaping(prompt);
114
- return WEB_PAGE_INTENT.test(text) || /\bhtml5\s+website\b/i.test(text);
115
+ return WEB_PAGE_INTENT.test(text) || WEB_SHOWCASE_INTENT.test(text) || /\bhtml5\s+website\b/i.test(text);
115
116
  }
116
117
  export function isTrivialHtmlPageRequest(prompt) {
117
118
  const text = stripExecutionShaping(prompt);
@@ -4,7 +4,7 @@ function stripInlineWorkspacePlaceholders(value) {
4
4
  // Only strip placeholders when they were appended to a segment (e.g. pitfall[Vigthoria service]).
5
5
  // Keep canonical scrubbed segments like /[internal]/ intact for dedicated mapping logic.
6
6
  return String(value || '')
7
- .replace(/(^|[^/])\[(?:internal|Vigthoria service|Vigthoria Agent|Vigthoria storage|temp|home|workspace)\](?=(?:\/|$))/ig, '$1')
7
+ .replace(/([^/])\[(?:internal|Vigthoria service|Vigthoria Agent|Vigthoria storage|temp|home|workspace)\](?=(?:\/|$))/ig, '$1')
8
8
  .replace(/\/+$/g, '');
9
9
  }
10
10
  /**
@@ -16,6 +16,9 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
16
16
  if (!input) {
17
17
  return '';
18
18
  }
19
+ input = input
20
+ .replace(/^var\/?\[internal\]/i, '[internal]')
21
+ .replace(/^varwww\[internal\]/i, '[internal]');
19
22
  input = stripInlineWorkspacePlaceholders(input);
20
23
  const safeRelative = (candidate) => {
21
24
  const stripped = String(candidate || '').replace(/^\/+/, '');
@@ -64,6 +67,8 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
64
67
  }
65
68
  const placeholderPatterns = [
66
69
  [/^(?:var\/)?www\/\[internal\](?:\/(.*))?$/i, 1],
70
+ [/^var\/?\[internal\](?:\/(.*))?$/i, 1],
71
+ [/^varwww\[internal\](?:\/(.*))?$/i, 1],
67
72
  [/^\[internal\](?:\/(.*))?$/i, 1],
68
73
  [/^\[Vigthoria service\](?:\/(.*))?$/i, 1],
69
74
  [/^\[Vigthoria Agent\](?:\/(.*))?$/i, 1],
@@ -134,6 +139,9 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
134
139
  }
135
140
  const rootNoLeadingSlash = normalizedRoot.replace(/^\//, '');
136
141
  const rootBase = path.posix.basename(normalizedRoot);
142
+ if (input.replace(/\/+$/g, '').toLowerCase() === normalizedRoot.toLowerCase()) {
143
+ return '';
144
+ }
137
145
  const prefixes = [
138
146
  `${normalizedRoot}/`,
139
147
  `${rootNoLeadingSlash}/`,
@@ -150,6 +158,11 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
150
158
  return safeRelative(input.slice(embeddedIndex + embeddedRoot.length));
151
159
  }
152
160
  }
161
+ // An unmatched POSIX absolute path belongs to another filesystem boundary.
162
+ // Never reinterpret it as a workspace-relative path by merely trimming '/'.
163
+ if (input.startsWith('/')) {
164
+ return '';
165
+ }
153
166
  const relative = safeRelative(input);
154
167
  if (relative === 'workspace') {
155
168
  return '';
@@ -167,7 +180,11 @@ export function isV3WorkspaceRootAliasPath(rawPath, rootPath) {
167
180
  if (!original || original === '.') {
168
181
  return true;
169
182
  }
170
- const sanitized = stripInlineWorkspacePlaceholders(original).replace(/\\/g, '/').trim();
183
+ const compactCanonical = original
184
+ .replace(/\\/g, '/')
185
+ .replace(/^var\/?\[internal\]/i, '[internal]')
186
+ .replace(/^varwww\[internal\]/i, '[internal]');
187
+ const sanitized = stripInlineWorkspacePlaceholders(compactCanonical).trim();
171
188
  if (!sanitized || sanitized === '.' || sanitized === '/') {
172
189
  return true;
173
190
  }
@@ -181,6 +198,9 @@ export function isV3WorkspaceRootAliasPath(rawPath, rootPath) {
181
198
  || lowered === '[internal]'
182
199
  || lowered === 'www/[internal]'
183
200
  || lowered === 'var/www/[internal]'
201
+ || lowered === 'var[internal]'
202
+ || lowered === 'var/[internal]'
203
+ || lowered === 'varwww[internal]'
184
204
  || lowered === '[vigthoria service]'
185
205
  || lowered === 'var/www/[vigthoria service]') {
186
206
  return true;
package/install.ps1 CHANGED
@@ -5,7 +5,7 @@
5
5
  $ErrorActionPreference = "Stop"
6
6
 
7
7
  # Configuration
8
- $CLI_VERSION = "1.13.13"
8
+ $CLI_VERSION = "1.13.18"
9
9
  $INSTALL_DIR = "$env:USERPROFILE\.vigthoria"
10
10
  $NPM_PACKAGE = "vigthoria-cli"
11
11
  $GIT_PACKAGE_URL = "git+https://market.vigthoria.io/vigthoria/vigthoria-cli.git"
package/install.sh CHANGED
@@ -26,7 +26,7 @@ else
26
26
  fi
27
27
 
28
28
  # Configuration
29
- CLI_VERSION="1.13.13"
29
+ CLI_VERSION="1.13.18"
30
30
  INSTALL_DIR="$HOME/.vigthoria"
31
31
  REPO_URL="https://market.vigthoria.io/vigthoria/vigthoria-cli"
32
32
  GIT_PACKAGE_URL="git+https://market.vigthoria.io/vigthoria/vigthoria-cli.git"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.13.13",
3
+ "version": "1.13.18",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -72,6 +72,7 @@
72
72
  "test:legion:billing:e2e": "npm run build && node scripts/test-legion-godmode-billing-e2e.js",
73
73
  "test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js",
74
74
  "test:v3-workspace-path": "npm run build && node scripts/test-v3-workspace-path.js",
75
+ "test:v3-server-tool-execution": "npm run build && node scripts/test-v3-server-tool-execution.js",
75
76
  "test:session:project-match": "npm run build && node scripts/test-session-project-match.mjs",
76
77
  "test:v3-stream-mutation": "npm run build && node scripts/test-v3-stream-mutation.js",
77
78
  "test:context:budget": "npm run build && node scripts/test-context-budget.js",
@@ -5,6 +5,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
5
  cd "$ROOT"
6
6
 
7
7
  CLI="node dist/index.js"
8
+ SERVICE_IDENTITY_MODE="${VIGTHORIA_TEST_SERVICE_IDENTITY:-0}"
8
9
  unset V3_SERVICE_KEY
9
10
  unset HYPERLOOP_SERVICE_KEY
10
11
  # Release validation runs against local model-router in this environment
@@ -27,6 +28,7 @@ printf '%s
27
28
  ' "$AUTH_JSON" > /tmp/vig-auth-preflight.json
28
29
  python3 - << 'PY'
29
30
  import json
31
+ import re
30
32
  with open('/tmp/vig-auth-preflight.json','r',encoding='utf-8') as f:
31
33
  j=json.load(f)
32
34
  msg='Vigthoria Gate way user authentification failed. Please log out and login again.'
@@ -35,6 +37,15 @@ assert j.get('error') == msg, f"unexpected auth message: {j.get('error')}"
35
37
  print('[pass] auth preflight message')
36
38
  PY
37
39
 
40
+ if [[ "$SERVICE_IDENTITY_MODE" == "1" ]]; then
41
+ # From this point onward the no-go suite exercises server-local model and
42
+ # tool routes without borrowing a human account session. The invalid-token
43
+ # gateway assertion above always runs against the canonical public boundary.
44
+ export VIGTHORIA_LOCAL_TEST_MODE=1
45
+ export VIGTHORIA_LOCAL_TEST_TOKEN="vigthoria-release-service-identity"
46
+ export VIGTHORIA_AUTH_TOKEN="$VIGTHORIA_LOCAL_TEST_TOKEN"
47
+ fi
48
+
38
49
  echo "[1.1] ranker includes agent.py top10 for SSE prompt"
39
50
  node - << 'EOF2'
40
51
  import('./dist/utils/context-ranker.js').then((m)=>{
@@ -62,7 +73,17 @@ if (!tsc || !tsc.ran || !tsc.passed) process.exit(1);
62
73
  EOF2
63
74
 
64
75
  echo "[6.2] balanced-4b no-agent path"
65
- $CLI chat --no-agent --model balanced-4b --prompt "reply exactly: ok" --json >/tmp/vig-balanced.json
76
+ for attempt in 1 2 3; do
77
+ if $CLI chat --no-agent --model balanced-4b --prompt "reply exactly: ok" --json >/tmp/vig-balanced.json; then
78
+ break
79
+ fi
80
+ if [[ "$attempt" == "3" ]]; then
81
+ echo "[fail] balanced-4b remained unavailable after 3 attempts"
82
+ exit 1
83
+ fi
84
+ echo "[retry] balanced-4b transient failure ($attempt/3)"
85
+ sleep 2
86
+ done
66
87
  python3 - << 'PY'
67
88
  import json
68
89
  j=json.load(open('/tmp/vig-balanced.json'))
@@ -76,13 +97,17 @@ $CLI hyper-loop status >/tmp/vig-hyperloop.txt
76
97
  $CLI devtools connect >/tmp/vig-devtools.txt
77
98
 
78
99
  echo "[6.11] operator flow"
79
- $CLI operator --prompt "reply with exactly: OK" --json >/tmp/vig-operator.json
80
- python3 - << 'PY'
100
+ if [[ "$SERVICE_IDENTITY_MODE" == "1" ]]; then
101
+ echo "[skip] paid-user operator probe (isolated release service identity)"
102
+ else
103
+ $CLI operator --prompt "reply with exactly: OK" --json >/tmp/vig-operator.json
104
+ python3 - << 'PY'
81
105
  import json
82
106
  j=json.load(open('/tmp/vig-operator.json'))
83
107
  assert j.get('success') is True, j
84
108
  print('[pass] operator flow')
85
109
  PY
110
+ fi
86
111
 
87
112
  echo "[7.1/7.3/7.4] local service health"
88
113
  for u in http://localhost:8030/health http://localhost:4009/health http://localhost:4011/health; do
@@ -95,6 +120,7 @@ echo "[7.2/7.8/12.3] models inventory"
95
120
  curl -s http://localhost:4009/v1/models >/tmp/vig-models.json
96
121
  python3 - << 'PY'
97
122
  import json
123
+ import re
98
124
  ids={m.get('id','') for m in json.load(open('/tmp/vig-models.json')).get('data',[])}
99
125
  if ids:
100
126
  has_router = any(
@@ -110,7 +136,7 @@ if ids:
110
136
  'vigthoria-fast-9b', 'vigthoria-fast-9b:latest',
111
137
  )
112
138
  )
113
- has_code = any('vigthoria-v3-code-35b' in i for i in ids)
139
+ has_code = any(re.search(r'^vigthoria-v3(?:\.\d+)?-code-35b(?:-|:|$)', i) for i in ids)
114
140
  assert has_router, f'missing router model in inventory: {sorted(ids)}'
115
141
  assert has_creative, f'missing creative/fast model in inventory: {sorted(ids)}'
116
142
  assert has_code, f'missing code model in inventory: {sorted(ids)}'
@@ -136,7 +162,17 @@ rg -n "coder.vigthoria.io/releases" install.ps1 install.sh README.md >/tmp/vig-r
136
162
  cat /tmp/vig-release-urls.txt
137
163
 
138
164
  echo "[12.1] no-agent governance fallback"
139
- $CLI chat --no-agent --model creative --prompt "say ok" --json >/tmp/vig-creative.json
165
+ for attempt in 1 2 3; do
166
+ if $CLI chat --no-agent --model creative --prompt "say ok" --json >/tmp/vig-creative.json; then
167
+ break
168
+ fi
169
+ if [[ "$attempt" == "3" ]]; then
170
+ echo "[fail] governance fallback remained unavailable after 3 attempts"
171
+ exit 1
172
+ fi
173
+ echo "[retry] governance fallback transient failure ($attempt/3)"
174
+ sleep 2
175
+ done
140
176
  python3 - << 'PY'
141
177
  import json
142
178
  j=json.load(open('/tmp/vig-creative.json'))