vigthoria-cli 1.13.25 → 1.13.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/commands/chat.js +83 -27
  2. package/dist/commands/config.js +4 -4
  3. package/dist/commands/fork.d.ts +3 -2
  4. package/dist/commands/fork.js +124 -123
  5. package/dist/commands/game.d.ts +8 -0
  6. package/dist/commands/game.js +118 -10
  7. package/dist/commands/history.d.ts +0 -1
  8. package/dist/commands/history.js +8 -22
  9. package/dist/commands/hub.d.ts +20 -0
  10. package/dist/commands/hub.js +17 -3
  11. package/dist/commands/preview.d.ts +1 -0
  12. package/dist/commands/preview.js +36 -15
  13. package/dist/commands/product-run-registration.js +1 -1
  14. package/dist/commands/replay.d.ts +0 -1
  15. package/dist/commands/replay.js +10 -19
  16. package/dist/commands/repo.d.ts +35 -2
  17. package/dist/commands/repo.js +76 -34
  18. package/dist/commands/update-registration.js +4 -3
  19. package/dist/commands/workflow.d.ts +4 -0
  20. package/dist/commands/workflow.js +27 -0
  21. package/dist/index.js +6 -4
  22. package/dist/utils/agentRunOutcome.d.ts +7 -0
  23. package/dist/utils/agentRunOutcome.js +13 -0
  24. package/dist/utils/api.d.ts +20 -5
  25. package/dist/utils/api.js +440 -54
  26. package/dist/utils/command-policy.js +1 -1
  27. package/dist/utils/config.d.ts +2 -0
  28. package/dist/utils/config.js +8 -3
  29. package/dist/utils/frontend-preview-service.d.ts +1 -0
  30. package/dist/utils/frontend-preview-service.js +54 -5
  31. package/dist/utils/model-governance.js +23 -14
  32. package/dist/utils/model-transport-service.js +1 -1
  33. package/dist/utils/network-policy.js +16 -4
  34. package/dist/utils/operator-client.js +23 -4
  35. package/dist/utils/post-write-validator.js +7 -3
  36. package/dist/utils/preview-screenshot-adapter.d.ts +19 -3
  37. package/dist/utils/preview-screenshot-adapter.js +289 -28
  38. package/dist/utils/runtime-capability.d.ts +7 -0
  39. package/dist/utils/runtime-capability.js +11 -0
  40. package/dist/utils/runtime-temp.d.ts +6 -2
  41. package/dist/utils/runtime-temp.js +143 -27
  42. package/dist/utils/tools.js +1 -1
  43. package/dist/utils/v3-stream-events.d.ts +8 -0
  44. package/dist/utils/v3-stream-events.js +70 -0
  45. package/dist/utils/v3-workspace-service.d.ts +1 -0
  46. package/dist/utils/v3-workspace-service.js +38 -1
  47. package/dist/utils/vigflow-client.d.ts +9 -0
  48. package/dist/utils/vigflow-client.js +48 -2
  49. package/dist/utils/workspace-reference.d.ts +8 -0
  50. package/dist/utils/workspace-reference.js +21 -0
  51. package/package.json +4 -6
  52. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +2 -2
  53. package/scripts/release/validate-live-service-gates.sh +3 -3
  54. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -4,11 +4,14 @@ import * as path from 'node:path';
4
4
  const MIB = 1024 * 1024;
5
5
  const DEFAULT_MAX_BYTES = 1024 * MIB;
6
6
  const DEFAULT_MIN_FREE_BYTES = 128 * MIB;
7
+ const DEFAULT_ALLOCATION_MAX_BYTES = 64 * MIB;
7
8
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
8
9
  const CLEANUP_LOCK_MAX_AGE_MS = 10 * 60 * 1000;
10
+ const LOCK_RETRY_WAIT_MS = 20;
9
11
  const MAX_SCAN_ENTRIES = 100_000;
10
12
  const LEASE_FILE = '.vigthoria-temp-lease.json';
11
13
  const CLEANUP_LOCK = '.vigthoria-temp-cleanup.lock';
14
+ const ALLOCATION_LOCK = '.vigthoria-temp-allocation.lock';
12
15
  const ROOT_MARKER = '.vigthoria-temp-root.json';
13
16
  const LEGACY_CLEANUP_MARKER = '.vigthoria-legacy-temp-cleanup-v1';
14
17
  const LEGACY_PREFIXES = [
@@ -18,6 +21,12 @@ const LEGACY_PREFIXES = [
18
21
  'vigthoria-cli-install-',
19
22
  'puppeteer_dev_chrome_profile-',
20
23
  ];
24
+ // Capture the operating-system temp root before CLI initialization rewrites
25
+ // TMP/TEMP/TMPDIR to the managed per-user root. `os.tmpdir()` is dynamic on
26
+ // Node: after the rewrite it returns the managed root, which previously meant
27
+ // a second public RuntimeTempManager instance could mistake the original
28
+ // shared %TEMP% for a safe explicit override.
29
+ const PROCESS_START_SYSTEM_TEMP = os.tmpdir();
21
30
  export class RuntimeTempError extends Error {
22
31
  code;
23
32
  details;
@@ -51,7 +60,15 @@ function rejectUnsafeWindowsPath(value) {
51
60
  }
52
61
  function isKnownSharedPosixTemp(value) {
53
62
  const normalized = path.posix.resolve(value);
54
- return normalized === '/tmp' || normalized === '/var/tmp' || normalized === '/usr/tmp';
63
+ return ['/tmp', '/var/tmp', '/usr/tmp'].some((root) => pathIsContained(root, normalized, path.posix));
64
+ }
65
+ function isWithinSharedTemp(candidate, root, platform) {
66
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
67
+ const normalize = (value) => {
68
+ const resolved = pathApi.resolve(value);
69
+ return platform === 'win32' ? resolved.toLocaleLowerCase('en-US') : resolved;
70
+ };
71
+ return pathIsContained(normalize(root), normalize(candidate), pathApi);
55
72
  }
56
73
  function pathsEqual(left, right, platform) {
57
74
  const pathApi = platform === 'win32' ? path.win32 : path.posix;
@@ -91,6 +108,10 @@ function defaultProcessAlive(pid) {
91
108
  return error?.code === 'EPERM';
92
109
  }
93
110
  }
111
+ function synchronousWait(milliseconds) {
112
+ const signal = new Int32Array(new SharedArrayBuffer(4));
113
+ Atomics.wait(signal, 0, 0, milliseconds);
114
+ }
94
115
  export class RuntimeTempManager {
95
116
  environment;
96
117
  platform;
@@ -99,10 +120,12 @@ export class RuntimeTempManager {
99
120
  pid;
100
121
  isProcessAlive;
101
122
  systemTempRoot;
123
+ sharedTempRoots;
102
124
  configuredRoot;
103
125
  source;
104
126
  maxBytes;
105
127
  minimumFreeBytes;
128
+ allocationMaxBytes;
106
129
  ttlMs;
107
130
  initializedRoot = null;
108
131
  initializedIdentity = null;
@@ -121,12 +144,24 @@ export class RuntimeTempManager {
121
144
  this.now = options.now || Date.now;
122
145
  this.pid = options.pid || process.pid;
123
146
  this.isProcessAlive = options.isProcessAlive || defaultProcessAlive;
124
- this.systemTempRoot = options.systemTempDirectory || os.tmpdir();
147
+ const platformTemp = this.platform === 'win32'
148
+ ? String(this.environment.TEMP || this.environment.TMP || '').trim()
149
+ : String(this.environment.TMPDIR || '').trim();
150
+ this.systemTempRoot = options.systemTempDirectory || platformTemp || (this.platform === process.platform ? PROCESS_START_SYSTEM_TEMP : os.tmpdir());
151
+ this.sharedTempRoots = [
152
+ options.systemTempDirectory,
153
+ platformTemp,
154
+ this.environment.TEMP,
155
+ this.environment.TMP,
156
+ this.environment.TMPDIR,
157
+ this.platform === process.platform ? PROCESS_START_SYSTEM_TEMP : undefined,
158
+ ].filter((value) => typeof value === 'string' && value.trim().length > 0);
125
159
  const resolved = resolveRuntimeTempRoot({ ...options, environment: this.environment, platform: this.platform, homeDirectory: this.homeDirectory });
126
160
  this.configuredRoot = resolved.root;
127
161
  this.source = resolved.source;
128
162
  this.maxBytes = numericSetting('VIGTHORIA_TEMP_MAX_BYTES', this.environment.VIGTHORIA_TEMP_MAX_BYTES, DEFAULT_MAX_BYTES, 64 * MIB, 64 * 1024 * MIB);
129
163
  this.minimumFreeBytes = numericSetting('VIGTHORIA_TEMP_MIN_FREE_BYTES', this.environment.VIGTHORIA_TEMP_MIN_FREE_BYTES, DEFAULT_MIN_FREE_BYTES, 64 * MIB, 64 * 1024 * MIB);
164
+ this.allocationMaxBytes = numericSetting('VIGTHORIA_TEMP_ALLOCATION_MAX_BYTES', this.environment.VIGTHORIA_TEMP_ALLOCATION_MAX_BYTES, DEFAULT_ALLOCATION_MAX_BYTES, MIB, this.maxBytes);
130
165
  const ttlHours = numericSetting('VIGTHORIA_TEMP_TTL_HOURS', this.environment.VIGTHORIA_TEMP_TTL_HOURS, DEFAULT_TTL_MS / 3_600_000, 1, 720);
131
166
  this.ttlMs = ttlHours * 3_600_000;
132
167
  }
@@ -134,6 +169,14 @@ export class RuntimeTempManager {
134
169
  if (this.initializedRoot || this.initializationError)
135
170
  return this.status();
136
171
  try {
172
+ // Reject the shared OS temp path before touching the filesystem. This is
173
+ // also what makes cross-platform policy probes deterministic: a Windows
174
+ // policy instance running in a non-Windows test host must not attempt to
175
+ // create a drive-letter path before it can reject `%TEMP%`.
176
+ if (this.sharedTempRoots.some((root) => isWithinSharedTemp(this.configuredRoot, root, this.platform))
177
+ || (this.platform !== 'win32' && isKnownSharedPosixTemp(this.configuredRoot))) {
178
+ throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
179
+ }
137
180
  if (this.source === 'per-user-default' && !fs.existsSync(this.homeDirectory)) {
138
181
  throw new RuntimeTempError('The user home directory does not exist; managed temporary storage cannot be initialized.', 'TEMP_HOME_UNAVAILABLE');
139
182
  }
@@ -154,7 +197,7 @@ export class RuntimeTempManager {
154
197
  }
155
198
  }
156
199
  const pathApi = this.platform === 'win32' ? path.win32 : path.posix;
157
- if (pathsEqual(realRoot, this.systemTempRoot, this.platform)
200
+ if (this.sharedTempRoots.some((root) => isWithinSharedTemp(realRoot, root, this.platform))
158
201
  || (this.platform !== 'win32' && isKnownSharedPosixTemp(realRoot))) {
159
202
  throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
160
203
  }
@@ -198,31 +241,53 @@ export class RuntimeTempManager {
198
241
  }
199
242
  return this.status();
200
243
  }
201
- createDirectory(prefix = 'run-') {
244
+ createDirectory(prefix = 'run-', requestedMaxBytes = this.allocationMaxBytes) {
202
245
  this.initialize();
203
246
  const root = this.requireRoot();
204
247
  if (!/^[a-z0-9][a-z0-9-]{0,47}-$/i.test(prefix)) {
205
248
  throw new RuntimeTempError('Temporary directory prefix is invalid.', 'TEMP_PREFIX_INVALID');
206
249
  }
250
+ if (!Number.isSafeInteger(requestedMaxBytes) || requestedMaxBytes < MIB || requestedMaxBytes > this.maxBytes) {
251
+ throw new RuntimeTempError('Temporary allocation reservation is outside the managed root contract.', 'TEMP_RESERVATION_INVALID');
252
+ }
207
253
  this.lastCleanup = this.scavenge({ includeLegacy: false });
208
- const before = this.measure(root);
209
- if (before.usedBytes >= this.maxBytes || (before.freeBytes !== null && before.freeBytes < this.minimumFreeBytes)) {
210
- throw new RuntimeTempError('Vigthoria temporary storage has insufficient capacity after cleanup.', 'TEMP_CAPACITY_EXHAUSTED', {
211
- root,
212
- usedBytes: before.usedBytes,
213
- maxBytes: this.maxBytes,
214
- freeBytes: before.freeBytes,
215
- minimumFreeBytes: this.minimumFreeBytes,
254
+ const allocationLock = this.acquireLock(path.join(root, ALLOCATION_LOCK));
255
+ if (allocationLock === null) {
256
+ throw new RuntimeTempError('Temporary allocation admission is busy.', 'TEMP_ALLOCATION_BUSY');
257
+ }
258
+ try {
259
+ const entries = this.readEntries(root);
260
+ const before = this.measure(root, entries);
261
+ const reservedBytes = entries.reduce((total, entry) => total + Math.max(entry.bytes, entry.active ? entry.reservedBytes : 0), 0);
262
+ if (reservedBytes + requestedMaxBytes > this.maxBytes
263
+ || (before.freeBytes !== null && before.freeBytes - requestedMaxBytes < this.minimumFreeBytes)) {
264
+ throw new RuntimeTempError('Vigthoria temporary storage has insufficient reserved capacity after cleanup.', 'TEMP_CAPACITY_EXHAUSTED', {
265
+ root,
266
+ usedBytes: before.usedBytes,
267
+ reservedBytes,
268
+ requestedMaxBytes,
269
+ maxBytes: this.maxBytes,
270
+ freeBytes: before.freeBytes,
271
+ minimumFreeBytes: this.minimumFreeBytes,
272
+ });
273
+ }
274
+ const directory = fs.mkdtempSync(path.join(root, prefix));
275
+ if (this.platform !== 'win32')
276
+ fs.chmodSync(directory, 0o700);
277
+ fs.writeFileSync(path.join(directory, LEASE_FILE), `${JSON.stringify({
278
+ schemaVersion: 1,
279
+ pid: this.pid,
280
+ createdAt: new Date(this.now()).toISOString(),
281
+ reservedMaxBytes: requestedMaxBytes,
282
+ })}\n`, {
283
+ flag: 'wx',
284
+ mode: 0o600,
216
285
  });
286
+ return directory;
287
+ }
288
+ finally {
289
+ this.releaseLock(path.join(root, ALLOCATION_LOCK), allocationLock);
217
290
  }
218
- const directory = fs.mkdtempSync(path.join(root, prefix));
219
- if (this.platform !== 'win32')
220
- fs.chmodSync(directory, 0o700);
221
- fs.writeFileSync(path.join(directory, LEASE_FILE), `${JSON.stringify({ schemaVersion: 1, pid: this.pid, createdAt: new Date(this.now()).toISOString() })}\n`, {
222
- flag: 'wx',
223
- mode: 0o600,
224
- });
225
- return directory;
226
291
  }
227
292
  removeDirectory(directory) {
228
293
  const root = this.requireRoot();
@@ -230,7 +295,7 @@ export class RuntimeTempManager {
230
295
  if (path.dirname(absolute) !== root || absolute === root) {
231
296
  throw new RuntimeTempError('Refusing to remove a path outside the Vigthoria temporary root.', 'TEMP_CLEANUP_ESCAPE');
232
297
  }
233
- fs.rmSync(absolute, { recursive: true, force: true });
298
+ fs.rmSync(absolute, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
234
299
  }
235
300
  scavenge(options = {}) {
236
301
  const root = this.requireRoot();
@@ -262,6 +327,13 @@ export class RuntimeTempManager {
262
327
  }
263
328
  if (lockAge <= CLEANUP_LOCK_MAX_AGE_MS)
264
329
  return result;
330
+ let ownerPid = 0;
331
+ try {
332
+ ownerPid = Number(fs.readFileSync(lockPath, 'utf8').trim());
333
+ }
334
+ catch { /* malformed stale lock */ }
335
+ if (ownerPid > 0 && this.isProcessAlive(ownerPid))
336
+ return result;
265
337
  try {
266
338
  fs.unlinkSync(lockPath);
267
339
  }
@@ -367,7 +439,7 @@ export class RuntimeTempManager {
367
439
  readEntries(root) {
368
440
  const entries = [];
369
441
  for (const name of fs.readdirSync(root)) {
370
- if (name === CLEANUP_LOCK || name === ROOT_MARKER || name === LEGACY_CLEANUP_MARKER)
442
+ if (name === CLEANUP_LOCK || name === ALLOCATION_LOCK || name === ROOT_MARKER || name === LEGACY_CLEANUP_MARKER)
371
443
  continue;
372
444
  const absolutePath = path.join(root, name);
373
445
  let stat;
@@ -380,15 +452,19 @@ export class RuntimeTempManager {
380
452
  throw error;
381
453
  }
382
454
  let active = false;
455
+ let reservedBytes = 0;
383
456
  if (stat.isDirectory() && !stat.isSymbolicLink()) {
384
457
  const leasePath = path.join(absolutePath, LEASE_FILE);
385
458
  try {
386
459
  const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
387
460
  active = typeof lease.pid === 'number' && this.isProcessAlive(lease.pid);
461
+ if (Number.isSafeInteger(lease.reservedMaxBytes) && Number(lease.reservedMaxBytes) > 0) {
462
+ reservedBytes = Number(lease.reservedMaxBytes);
463
+ }
388
464
  }
389
465
  catch { /* third-party and abandoned directories have no live lease */ }
390
466
  }
391
- entries.push({ absolutePath, name, bytes: this.entrySize(absolutePath), modifiedAt: stat.mtimeMs, active });
467
+ entries.push({ absolutePath, name, bytes: this.entrySize(absolutePath), reservedBytes, modifiedAt: stat.mtimeMs, active });
392
468
  }
393
469
  return entries;
394
470
  }
@@ -436,10 +512,50 @@ export class RuntimeTempManager {
436
512
  if (path.dirname(entry.absolutePath) !== this.requireRoot()) {
437
513
  throw new RuntimeTempError('Temporary cleanup target escaped its root.', 'TEMP_CLEANUP_ESCAPE');
438
514
  }
439
- fs.rmSync(entry.absolutePath, { recursive: true, force: true });
515
+ fs.rmSync(entry.absolutePath, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
440
516
  result.removedEntries += 1;
441
517
  result.removedBytes += entry.bytes;
442
518
  }
519
+ acquireLock(lockPath) {
520
+ for (let attempt = 0; attempt < 100; attempt += 1) {
521
+ try {
522
+ const descriptor = fs.openSync(lockPath, 'wx', 0o600);
523
+ fs.writeFileSync(descriptor, `${this.pid}\n`);
524
+ return descriptor;
525
+ }
526
+ catch (error) {
527
+ if (error?.code !== 'EEXIST')
528
+ throw error;
529
+ try {
530
+ if (this.now() - fs.statSync(lockPath).mtimeMs > CLEANUP_LOCK_MAX_AGE_MS) {
531
+ let ownerPid = 0;
532
+ try {
533
+ ownerPid = Number(fs.readFileSync(lockPath, 'utf8').trim());
534
+ }
535
+ catch { /* malformed stale lock */ }
536
+ if (ownerPid <= 0 || !this.isProcessAlive(ownerPid)) {
537
+ fs.unlinkSync(lockPath);
538
+ continue;
539
+ }
540
+ }
541
+ }
542
+ catch (lockError) {
543
+ if (lockError?.code === 'ENOENT')
544
+ continue;
545
+ throw lockError;
546
+ }
547
+ synchronousWait(LOCK_RETRY_WAIT_MS);
548
+ }
549
+ }
550
+ return null;
551
+ }
552
+ releaseLock(lockPath, descriptor) {
553
+ fs.closeSync(descriptor);
554
+ try {
555
+ fs.unlinkSync(lockPath);
556
+ }
557
+ catch { /* stale-lock recovery may have raced */ }
558
+ }
443
559
  cleanupLegacySystemTemp(result) {
444
560
  const legacyRoot = this.systemTempRoot;
445
561
  if (!fs.existsSync(legacyRoot) || path.resolve(legacyRoot) === this.requireRoot())
@@ -467,7 +583,7 @@ export class RuntimeTempManager {
467
583
  if (this.now() - stat.mtimeMs <= this.ttlMs)
468
584
  continue;
469
585
  const bytes = this.entrySize(candidate);
470
- fs.rmSync(candidate, { recursive: true, force: true });
586
+ fs.rmSync(candidate, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
471
587
  result.legacyRemovedEntries += 1;
472
588
  result.legacyRemovedBytes += bytes;
473
589
  }
@@ -484,8 +600,8 @@ export function initializeRuntimeTempStorage() {
484
600
  export function runtimeTempStatus() {
485
601
  return getRuntimeTempManager().initialize();
486
602
  }
487
- export function createRuntimeTempDirectory(prefix) {
488
- return getRuntimeTempManager().createDirectory(prefix);
603
+ export function createRuntimeTempDirectory(prefix, requestedMaxBytes) {
604
+ return getRuntimeTempManager().createDirectory(prefix, requestedMaxBytes);
489
605
  }
490
606
  export function removeRuntimeTempDirectory(directory) {
491
607
  getRuntimeTempManager().removeDirectory(directory);
@@ -1344,7 +1344,7 @@ export class AgenticTools {
1344
1344
  validateJavaScriptSyntax(source) {
1345
1345
  let tempDirectory;
1346
1346
  try {
1347
- tempDirectory = createRuntimeTempDirectory('syntax-');
1347
+ tempDirectory = createRuntimeTempDirectory('syntax-', 16 * 1024 * 1024);
1348
1348
  }
1349
1349
  catch (error) {
1350
1350
  return this.formatExternalToolError('syntax_check', 'allocate bounded temporary storage', error);
@@ -3,3 +3,11 @@
3
3
  * connections alive — not substantive work the CLI should surface as progress.
4
4
  */
5
5
  export declare function isV3StreamKeepaliveEvent(event: unknown): boolean;
6
+ export declare class AgentPlanContractError extends Error {
7
+ readonly code = "AGENT_PLAN_INVALID";
8
+ constructor(message: string);
9
+ }
10
+ /** Validate a server-authored execution graph before any task can be credited.
11
+ * A cyclic, duplicate, self-referencing, or dangling graph is never runnable
12
+ * and must not reach the UI callback where it could later be counted as 2/2. */
13
+ export declare function assertValidAgentPlanEvent(event: unknown): void;
@@ -39,3 +39,73 @@ export function isV3StreamKeepaliveEvent(event) {
39
39
  }
40
40
  return false;
41
41
  }
42
+ export class AgentPlanContractError extends Error {
43
+ code = 'AGENT_PLAN_INVALID';
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = 'AgentPlanContractError';
47
+ }
48
+ }
49
+ function dependencyId(value) {
50
+ if (typeof value === 'string' || typeof value === 'number')
51
+ return String(value).trim();
52
+ if (!value || typeof value !== 'object')
53
+ return '';
54
+ const record = value;
55
+ return String(record.id || record.task_id || record.taskId || '').trim();
56
+ }
57
+ /** Validate a server-authored execution graph before any task can be credited.
58
+ * A cyclic, duplicate, self-referencing, or dangling graph is never runnable
59
+ * and must not reach the UI callback where it could later be counted as 2/2. */
60
+ export function assertValidAgentPlanEvent(event) {
61
+ if (!event || typeof event !== 'object')
62
+ return;
63
+ const record = event;
64
+ if (record.type === 'error' && record.code === 'AGENT_PLAN_INVALID') {
65
+ throw new AgentPlanContractError(String(record.message || 'Agent plan graph is invalid.'));
66
+ }
67
+ if (record.type !== 'plan')
68
+ return;
69
+ // Accept every server envelope used by V3/Operator integrations, while
70
+ // ignoring planning keepalives that intentionally have no task graph.
71
+ const taskCandidate = record.plan?.tasks ?? record.tasks ?? record.data?.tasks;
72
+ if (!Array.isArray(taskCandidate))
73
+ return;
74
+ const tasks = taskCandidate;
75
+ const ids = tasks.map((task, index) => {
76
+ const id = dependencyId(task.id ?? task.task_id ?? task.taskId);
77
+ if (!id)
78
+ throw new AgentPlanContractError(`Agent plan task at index ${index} has no stable ID.`);
79
+ return id;
80
+ });
81
+ if (new Set(ids).size !== ids.length)
82
+ throw new AgentPlanContractError('Agent plan contains duplicate task IDs.');
83
+ const idSet = new Set(ids);
84
+ const graph = new Map();
85
+ tasks.forEach((task, index) => {
86
+ const raw = task.depends_on ?? task.dependsOn ?? task.dependencies ?? [];
87
+ const dependencies = (Array.isArray(raw) ? raw : [raw]).map(dependencyId).filter(Boolean);
88
+ for (const dependency of dependencies) {
89
+ if (dependency === ids[index])
90
+ throw new AgentPlanContractError(`Agent plan task ${ids[index]} depends on itself.`);
91
+ if (!idSet.has(dependency))
92
+ throw new AgentPlanContractError(`Agent plan task ${ids[index]} depends on missing task ${dependency}.`);
93
+ }
94
+ graph.set(ids[index], dependencies);
95
+ });
96
+ const visiting = new Set();
97
+ const visited = new Set();
98
+ const visit = (id, trail) => {
99
+ if (visiting.has(id))
100
+ throw new AgentPlanContractError(`Agent plan contains a dependency cycle: ${[...trail, id].join(' -> ')}.`);
101
+ if (visited.has(id))
102
+ return;
103
+ visiting.add(id);
104
+ for (const dependency of graph.get(id) || [])
105
+ visit(dependency, [...trail, id]);
106
+ visiting.delete(id);
107
+ visited.add(id);
108
+ };
109
+ for (const id of ids)
110
+ visit(id, []);
111
+ }
@@ -21,6 +21,7 @@ export declare class V3WorkspaceService {
21
21
  relativePath: string;
22
22
  absolutePath: string;
23
23
  } | null;
24
+ private resolveCaseInsensitiveWorkspacePath;
24
25
  writeFile(rootPath: string, rawPath: string, content: string, sourceRoot?: string): boolean;
25
26
  deleteFile(rootPath: string, rawPath: string): boolean;
26
27
  recover(context?: Record<string, any>, streamedFiles?: Record<string, string>, expectedFiles?: string[]): void;
@@ -133,6 +133,14 @@ export class V3WorkspaceService {
133
133
  }
134
134
  extractExpectedFiles(message = '', context = {}) {
135
135
  const candidates = new Set();
136
+ if (Array.isArray(context.expectedFiles)) {
137
+ for (const value of context.expectedFiles) {
138
+ const filePath = String(value || '').trim().replace(/^\.\//, '');
139
+ if (filePath && !/^https?:\/\//i.test(filePath) && !isSensitivePath(filePath)) {
140
+ candidates.add(filePath);
141
+ }
142
+ }
143
+ }
136
144
  for (const value of [message, context.rawMessage, context.agentPrompt]) {
137
145
  const text = String(value || '');
138
146
  const extensions = 'c|cc|cpp|cxx|h|hpp|cs|css|go|html|htm|ini|java|js|jsx|json|kt|kts|md|mjs|cjs|php|ps1|py|rb|rs|scss|sh|sql|svelte|toml|ts|tsx|txt|vue|xml|yaml|yml';
@@ -208,15 +216,44 @@ export class V3WorkspaceService {
208
216
  if (relativePath && isSensitivePath(relativePath))
209
217
  return null;
210
218
  try {
211
- const absolutePath = relativePath
219
+ let absolutePath = relativePath
212
220
  ? resolveWorkspacePath(rootPath, relativePath, { allowMissing: true })
213
221
  : fs.realpathSync(path.resolve(rootPath));
222
+ if (relativePath && !fs.existsSync(absolutePath)) {
223
+ const canonical = this.resolveCaseInsensitiveWorkspacePath(rootPath, relativePath);
224
+ relativePath = canonical.relativePath;
225
+ absolutePath = canonical.absolutePath;
226
+ }
214
227
  return { relativePath: relativePath || '.', absolutePath };
215
228
  }
216
229
  catch {
217
230
  return null;
218
231
  }
219
232
  }
233
+ resolveCaseInsensitiveWorkspacePath(rootPath, relativePath) {
234
+ const suppliedSegments = relativePath.split('/').filter(Boolean);
235
+ const canonicalSegments = [];
236
+ let current = fs.realpathSync(path.resolve(rootPath));
237
+ for (let index = 0; index < suppliedSegments.length; index += 1) {
238
+ const supplied = suppliedSegments[index];
239
+ let selected = supplied;
240
+ if (fs.existsSync(current) && fs.statSync(current).isDirectory()) {
241
+ const exact = path.join(current, supplied);
242
+ if (!fs.existsSync(exact)) {
243
+ const foldedMatches = fs.readdirSync(current).filter((entry) => entry.toLowerCase() === supplied.toLowerCase());
244
+ if (foldedMatches.length === 1)
245
+ selected = foldedMatches[0];
246
+ }
247
+ }
248
+ canonicalSegments.push(selected);
249
+ current = path.join(current, selected);
250
+ }
251
+ const canonicalRelative = canonicalSegments.join('/');
252
+ return {
253
+ relativePath: canonicalRelative,
254
+ absolutePath: resolveWorkspacePath(rootPath, canonicalRelative, { allowMissing: true }),
255
+ };
256
+ }
220
257
  writeFile(rootPath, rawPath, content, sourceRoot) {
221
258
  const relativePath = this.normalizeRelativePath(rawPath, sourceRoot || rootPath);
222
259
  if (!relativePath || isSensitivePath(relativePath)) {
@@ -61,6 +61,9 @@ export interface VigFlowTransport {
61
61
  post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<{
62
62
  data: T;
63
63
  }>;
64
+ delete<T>(url: string, config?: AxiosRequestConfig): Promise<{
65
+ data: T;
66
+ }>;
64
67
  }
65
68
  export interface VigFlowClientDependencies {
66
69
  getBaseUrls(): string[];
@@ -76,6 +79,7 @@ export declare class VigFlowClient {
76
79
  clearCredentials(): void;
77
80
  private accessToken;
78
81
  private withBackend;
82
+ private withMutationBackend;
79
83
  private endpoint;
80
84
  listTemplates(options?: {
81
85
  category?: string;
@@ -92,4 +96,9 @@ export declare class VigFlowClient {
92
96
  executionOptions?: Record<string, unknown>;
93
97
  }): Promise<VigFlowExecutionResult>;
94
98
  executionStatus(executionId: string): Promise<VigFlowExecutionStatus>;
99
+ deleteWorkflow(selector: string): Promise<{
100
+ id: string;
101
+ name?: string;
102
+ alreadyDeleted?: boolean;
103
+ }>;
95
104
  }
@@ -37,6 +37,29 @@ export class VigFlowClient {
37
37
  }
38
38
  throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
39
39
  }
40
+ async withMutationBackend(operation, action) {
41
+ const authenticationErrors = [];
42
+ for (const baseUrl of this.dependencies.getBaseUrls()) {
43
+ let token;
44
+ try {
45
+ token = await this.accessToken(baseUrl);
46
+ }
47
+ catch (error) {
48
+ authenticationErrors.push(`${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
49
+ this.dependencies.debug(`VigFlow ${operation} authentication via ${baseUrl} failed:`, error instanceof Error ? error.message : String(error));
50
+ continue;
51
+ }
52
+ // Once a mutation request is dispatched its outcome can be ambiguous.
53
+ // Never repeat it against a different backend; callers must reconcile
54
+ // the operation by its identifier before choosing to retry.
55
+ return action(baseUrl, {
56
+ 'Content-Type': 'application/json',
57
+ Accept: 'application/json',
58
+ Authorization: `Bearer ${token}`,
59
+ });
60
+ }
61
+ throw new Error(`No VigFlow backend accepted authentication for ${operation}. ${authenticationErrors.join(' | ')}`.trim());
62
+ }
40
63
  endpoint(baseUrl, subPath) {
41
64
  return /\/api\/vigflow\/?$/i.test(baseUrl) ? `${baseUrl.replace(/\/$/, '')}${subPath}` : `${baseUrl}/api${subPath}`;
42
65
  }
@@ -79,7 +102,7 @@ export class VigFlowClient {
79
102
  throw new Error(`No VigFlow workflow matched "${normalized}".`);
80
103
  }
81
104
  useTemplate(templateId, options = {}) {
82
- return this.withBackend('use template', async (baseUrl, headers) => {
105
+ return this.withMutationBackend('use template', async (baseUrl, headers) => {
83
106
  const response = await this.transport.post(this.endpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`), scanOutboundContext({ name: options.name, variables: options.variables || {} }).value, { headers, timeout: 30_000 });
84
107
  if (!response.data.workflow?.id)
85
108
  throw new Error('VigFlow use-template response did not include a workflow id.');
@@ -87,7 +110,7 @@ export class VigFlowClient {
87
110
  });
88
111
  }
89
112
  runWorkflow(workflowId, options = {}) {
90
- return this.withBackend('run workflow', async (baseUrl, headers) => {
113
+ return this.withMutationBackend('run workflow', async (baseUrl, headers) => {
91
114
  const response = await this.transport.post(this.endpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`), scanOutboundContext({ data: options.data || {}, options: options.executionOptions || {} }).value, { headers, timeout: 60_000 });
92
115
  if (!response.data.executionId)
93
116
  throw new Error('VigFlow run response did not include an execution id.');
@@ -102,4 +125,27 @@ export class VigFlowClient {
102
125
  return response.data.execution;
103
126
  });
104
127
  }
128
+ async deleteWorkflow(selector) {
129
+ const normalized = String(selector || '').trim();
130
+ if (!normalized)
131
+ throw new Error('Workflow selector is required. Provide a workflow id or name.');
132
+ // Native VigFlow IDs can be retried idempotently without first listing an
133
+ // object that may already be gone. Human-readable names remain resolved
134
+ // through the ownership-scoped list to avoid ambiguous deletion.
135
+ const target = /^(?:wf_[a-z0-9_-]+|[0-9]+|[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i.test(normalized)
136
+ ? { id: normalized, name: undefined }
137
+ : await this.resolveWorkflow(normalized);
138
+ return this.withMutationBackend('delete workflow', async (baseUrl, headers) => {
139
+ try {
140
+ const response = await this.transport.delete(this.endpoint(baseUrl, `/workflows/${encodeURIComponent(target.id)}`), { headers, timeout: 30_000 });
141
+ return { id: target.id, name: target.name, alreadyDeleted: response.data.alreadyDeleted === true };
142
+ }
143
+ catch (error) {
144
+ if (Number(error?.response?.status) === 404) {
145
+ return { id: target.id, name: target.name, alreadyDeleted: true };
146
+ }
147
+ throw error;
148
+ }
149
+ });
150
+ }
105
151
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Return a stable, non-filesystem workspace label for hosted history APIs.
3
+ *
4
+ * Client absolute paths are private machine metadata and must never be used as
5
+ * server workspace roots. File access is provided separately by the
6
+ * authenticated client-tool bridge.
7
+ */
8
+ export declare function buildLocalWorkspaceReference(workspacePath: string, accountNamespace?: string): string;
@@ -0,0 +1,21 @@
1
+ import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ /**
4
+ * Return a stable, non-filesystem workspace label for hosted history APIs.
5
+ *
6
+ * Client absolute paths are private machine metadata and must never be used as
7
+ * server workspace roots. File access is provided separately by the
8
+ * authenticated client-tool bridge.
9
+ */
10
+ export function buildLocalWorkspaceReference(workspacePath, accountNamespace = '') {
11
+ const normalized = String(workspacePath || 'workspace').replace(/\\/g, '/').replace(/\/+$/, '');
12
+ const displayName = path.posix.basename(normalized) || 'workspace';
13
+ const canonicalIdentity = process.platform === 'win32' ? normalized.toLowerCase() : normalized;
14
+ const opaqueId = createHash('sha256')
15
+ .update(String(accountNamespace || 'anonymous'))
16
+ .update('\0')
17
+ .update(canonicalIdentity)
18
+ .digest('hex')
19
+ .slice(0, 24);
20
+ return `vigthoria://local-workspace/${encodeURIComponent(displayName)}?id=${opaqueId}`;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.13.25",
3
+ "version": "1.13.29",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -84,13 +84,14 @@
84
84
  "test:legion:billing:e2e": "npm run build && node scripts/test-legion-godmode-billing-e2e.js",
85
85
  "test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js",
86
86
  "test:v3-workspace-path": "npm run build && node scripts/test-v3-workspace-path.js",
87
+ "test:workspace:reference": "npm run build && node scripts/test-workspace-reference.mjs",
87
88
  "test:v3-server-tool-execution": "npm run build && node scripts/test-v3-server-tool-execution.js",
88
89
  "test:session:project-match": "npm run build && node scripts/test-session-project-match.mjs",
89
90
  "test:external-client-resume": "npm run build && node scripts/test-external-client-resume.mjs",
90
91
  "test:v3-stream-mutation": "npm run build && node scripts/test-v3-stream-mutation.js",
91
92
  "test:context:budget": "npm run build && node scripts/test-context-budget.js",
92
93
  "test:v3-client-tool-quality": "npm run build && node scripts/test-v3-client-tool-quality.mjs",
93
- "test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.js",
94
+ "test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.mjs",
94
95
  "test:game:command": "node scripts/test-game-command.mjs",
95
96
  "test:security:dependencies": "node scripts/test-dependency-security.mjs",
96
97
  "test:security:network": "npm run build && node scripts/test-network-policy.mjs && node scripts/test-installer-trust-policy.mjs && node scripts/test-local-security-service.mjs",
@@ -131,7 +132,6 @@
131
132
  "marked": "^11.0.0",
132
133
  "marked-terminal": "^6.2.0",
133
134
  "ora": "^7.0.1",
134
- "puppeteer": "^24.40.0",
135
135
  "ws": "^8.14.2"
136
136
  },
137
137
  "devDependencies": {
@@ -148,8 +148,6 @@
148
148
  "overrides": {
149
149
  "glob": "^13.0.6",
150
150
  "brace-expansion": "5.0.9",
151
- "fast-uri": "3.1.5",
152
- "ip-address": "10.3.1",
153
- "js-yaml": "4.3.1"
151
+ "fast-uri": "3.1.5"
154
152
  }
155
153
  }