vigthoria-cli 1.13.20 → 1.13.23

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 (187) hide show
  1. package/completions/_vigthoria +49 -0
  2. package/completions/vigthoria.bash +6 -0
  3. package/completions/vigthoria.fish +41 -0
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +63 -128
  6. package/dist/commands/background.d.ts +0 -2
  7. package/dist/commands/background.js +26 -34
  8. package/dist/commands/bridge.js +45 -41
  9. package/dist/commands/cancel.d.ts +1 -0
  10. package/dist/commands/cancel.js +22 -16
  11. package/dist/commands/chat.d.ts +3 -42
  12. package/dist/commands/chat.js +378 -1123
  13. package/dist/commands/coding-registration.d.ts +4 -0
  14. package/dist/commands/coding-registration.js +24 -0
  15. package/dist/commands/config.js +23 -29
  16. package/dist/commands/conversation-registration.d.ts +4 -0
  17. package/dist/commands/conversation-registration.js +122 -0
  18. package/dist/commands/deploy.d.ts +8 -2
  19. package/dist/commands/deploy.js +92 -90
  20. package/dist/commands/device.d.ts +1 -2
  21. package/dist/commands/device.js +37 -21
  22. package/dist/commands/doctor-registration.d.ts +4 -0
  23. package/dist/commands/doctor-registration.js +207 -0
  24. package/dist/commands/edit.js +22 -33
  25. package/dist/commands/explain.js +7 -10
  26. package/dist/commands/fork.d.ts +1 -2
  27. package/dist/commands/fork.js +72 -22
  28. package/dist/commands/game.d.ts +1 -1
  29. package/dist/commands/game.js +17 -6
  30. package/dist/commands/generate.js +7 -9
  31. package/dist/commands/history.js +11 -7
  32. package/dist/commands/hub.d.ts +2 -2
  33. package/dist/commands/hub.js +34 -35
  34. package/dist/commands/legion.d.ts +0 -1
  35. package/dist/commands/legion.js +62 -79
  36. package/dist/commands/music.d.ts +1 -2
  37. package/dist/commands/music.js +62 -27
  38. package/dist/commands/platform-registration.d.ts +4 -0
  39. package/dist/commands/platform-registration.js +45 -0
  40. package/dist/commands/preview.d.ts +0 -1
  41. package/dist/commands/preview.js +82 -35
  42. package/dist/commands/product-run-registration.d.ts +4 -0
  43. package/dist/commands/product-run-registration.js +490 -0
  44. package/dist/commands/replay.d.ts +1 -2
  45. package/dist/commands/replay.js +15 -11
  46. package/dist/commands/repo.d.ts +14 -7
  47. package/dist/commands/repo.js +244 -198
  48. package/dist/commands/review.js +9 -12
  49. package/dist/commands/security.d.ts +1 -2
  50. package/dist/commands/security.js +12 -6
  51. package/dist/commands/update-registration.d.ts +3 -0
  52. package/dist/commands/update-registration.js +368 -0
  53. package/dist/commands/v4-registration.d.ts +4 -0
  54. package/dist/commands/v4-registration.js +24 -0
  55. package/dist/commands/v4.d.ts +1 -1
  56. package/dist/commands/v4.js +46 -8
  57. package/dist/commands/wallet.d.ts +1 -3
  58. package/dist/commands/wallet.js +18 -45
  59. package/dist/commands/workflow.d.ts +2 -0
  60. package/dist/commands/workflow.js +52 -39
  61. package/dist/index.js +124 -1589
  62. package/dist/utils/agent-stream-state.d.ts +27 -0
  63. package/dist/utils/agent-stream-state.js +93 -0
  64. package/dist/utils/agentRoute.d.ts +2 -1
  65. package/dist/utils/agentRoute.js +22 -9
  66. package/dist/utils/agentRunOutcome.d.ts +16 -0
  67. package/dist/utils/agentRunOutcome.js +37 -2
  68. package/dist/utils/api-client-factory.d.ts +8 -0
  69. package/dist/utils/api-client-factory.js +8 -0
  70. package/dist/utils/api.d.ts +44 -266
  71. package/dist/utils/api.js +647 -3180
  72. package/dist/utils/auth-session.d.ts +41 -0
  73. package/dist/utils/auth-session.js +224 -0
  74. package/dist/utils/brain-hub-client.d.ts +0 -4
  75. package/dist/utils/brain-hub-client.js +6 -3
  76. package/dist/utils/bridge-client.js +2 -0
  77. package/dist/utils/capability-health-service.d.ts +48 -0
  78. package/dist/utils/capability-health-service.js +64 -0
  79. package/dist/utils/chat-prompt-policy.d.ts +18 -0
  80. package/dist/utils/chat-prompt-policy.js +61 -0
  81. package/dist/utils/chat-tool-call-parser.d.ts +11 -0
  82. package/dist/utils/chat-tool-call-parser.js +71 -0
  83. package/dist/utils/cli-state.d.ts +22 -4
  84. package/dist/utils/cli-state.js +105 -47
  85. package/dist/utils/code-operations-service.d.ts +104 -0
  86. package/dist/utils/code-operations-service.js +837 -0
  87. package/dist/utils/codebase-indexer.d.ts +1 -0
  88. package/dist/utils/codebase-indexer.js +28 -8
  89. package/dist/utils/command-contract.d.ts +61 -0
  90. package/dist/utils/command-contract.js +149 -0
  91. package/dist/utils/command-policy.d.ts +10 -0
  92. package/dist/utils/command-policy.js +54 -0
  93. package/dist/utils/config.d.ts +13 -5
  94. package/dist/utils/config.js +220 -94
  95. package/dist/utils/context-ranker.js +11 -2
  96. package/dist/utils/context-state.d.ts +39 -0
  97. package/dist/utils/context-state.js +52 -0
  98. package/dist/utils/deckEvents.d.ts +0 -5
  99. package/dist/utils/deckEvents.js +3 -2
  100. package/dist/utils/desktop-bridge-client.js +2 -0
  101. package/dist/utils/direct-output-policy.d.ts +3 -0
  102. package/dist/utils/direct-output-policy.js +76 -0
  103. package/dist/utils/durable-json.d.ts +57 -0
  104. package/dist/utils/durable-json.js +319 -0
  105. package/dist/utils/fastAgentRouter.d.ts +5 -6
  106. package/dist/utils/fastAgentRouter.js +17 -25
  107. package/dist/utils/files.js +21 -13
  108. package/dist/utils/frontend-preview-service.d.ts +76 -0
  109. package/dist/utils/frontend-preview-service.js +482 -0
  110. package/dist/utils/goaEvents.d.ts +0 -4
  111. package/dist/utils/goaEvents.js +5 -2
  112. package/dist/utils/logger.d.ts +2 -1
  113. package/dist/utils/logger.js +27 -20
  114. package/dist/utils/mcp-context-client.d.ts +24 -0
  115. package/dist/utils/mcp-context-client.js +58 -0
  116. package/dist/utils/model-governance.d.ts +18 -0
  117. package/dist/utils/model-governance.js +75 -0
  118. package/dist/utils/model-transport-service.d.ts +44 -0
  119. package/dist/utils/model-transport-service.js +111 -0
  120. package/dist/utils/mutation-journal.d.ts +42 -0
  121. package/dist/utils/mutation-journal.js +387 -0
  122. package/dist/utils/network-policy.d.ts +24 -0
  123. package/dist/utils/network-policy.js +202 -0
  124. package/dist/utils/operator-client.d.ts +45 -0
  125. package/dist/utils/operator-client.js +159 -0
  126. package/dist/utils/post-write-validator.js +7 -9
  127. package/dist/utils/preview-screenshot-adapter.d.ts +15 -0
  128. package/dist/utils/preview-screenshot-adapter.js +51 -0
  129. package/dist/utils/process-policy.d.ts +23 -0
  130. package/dist/utils/process-policy.js +284 -0
  131. package/dist/utils/project-memory.d.ts +13 -3
  132. package/dist/utils/project-memory.js +70 -17
  133. package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
  134. package/dist/utils/prompt-workspace-resolver.js +64 -0
  135. package/dist/utils/release-install.d.ts +11 -0
  136. package/dist/utils/release-install.js +155 -0
  137. package/dist/utils/release-policy.d.ts +64 -0
  138. package/dist/utils/release-policy.js +293 -0
  139. package/dist/utils/requestIntent.js +22 -3
  140. package/dist/utils/runtime-capability.d.ts +8 -0
  141. package/dist/utils/runtime-capability.js +16 -0
  142. package/dist/utils/runtime-temp.d.ts +76 -0
  143. package/dist/utils/runtime-temp.js +492 -0
  144. package/dist/utils/secret-policy.d.ts +20 -0
  145. package/dist/utils/secret-policy.js +160 -0
  146. package/dist/utils/session.d.ts +17 -3
  147. package/dist/utils/session.js +354 -110
  148. package/dist/utils/tool-approval-service.d.ts +17 -0
  149. package/dist/utils/tool-approval-service.js +78 -0
  150. package/dist/utils/tool-capability-providers.d.ts +57 -0
  151. package/dist/utils/tool-capability-providers.js +57 -0
  152. package/dist/utils/tools.d.ts +2 -23
  153. package/dist/utils/tools.js +226 -512
  154. package/dist/utils/update-policy.d.ts +18 -0
  155. package/dist/utils/update-policy.js +137 -0
  156. package/dist/utils/v3-agent-client.d.ts +40 -0
  157. package/dist/utils/v3-agent-client.js +165 -0
  158. package/dist/utils/v3-workspace-service.d.ts +28 -0
  159. package/dist/utils/v3-workspace-service.js +279 -0
  160. package/dist/utils/vigflow-client.d.ts +95 -0
  161. package/dist/utils/vigflow-client.js +105 -0
  162. package/dist/utils/workspace-boundary.d.ts +45 -0
  163. package/dist/utils/workspace-boundary.js +377 -0
  164. package/dist/utils/workspace-brain-service.d.ts +2 -0
  165. package/dist/utils/workspace-brain-service.js +11 -1
  166. package/dist/utils/workspace-cache.js +37 -15
  167. package/dist/utils/workspace-stream.js +35 -10
  168. package/install.ps1 +115 -142
  169. package/install.sh +112 -81
  170. package/package.json +43 -9
  171. package/release-policy.json +43 -0
  172. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  173. package/scripts/release/generate-completions.mjs +24 -0
  174. package/scripts/release/install-release.mjs +10 -0
  175. package/scripts/release/publish-cli-release.mjs +116 -0
  176. package/scripts/release/publish-cli-release.sh +2 -70
  177. package/scripts/release/resolve-release-manifest.mjs +26 -0
  178. package/scripts/release/test-balanced-model-live.sh +45 -0
  179. package/scripts/release/validate-live-service-gates.sh +93 -0
  180. package/scripts/release/validate-no-go-gates.sh +145 -174
  181. package/scripts/release/verify-runtime-consistency.mjs +4 -1
  182. package/dist/commands/index.d.ts +0 -12
  183. package/dist/commands/index.js +0 -194
  184. package/dist/commands/update.d.ts +0 -9
  185. package/dist/commands/update.js +0 -301
  186. package/dist/utils/templateInstantPath.d.ts +0 -18
  187. package/dist/utils/templateInstantPath.js +0 -228
@@ -0,0 +1,76 @@
1
+ export function stripHiddenThoughtBlocks(text) {
2
+ if (!text)
3
+ return text;
4
+ return text
5
+ .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
6
+ .replace(/<think>[\s\S]*?<\/redacted_thinking>/gi, '')
7
+ .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
8
+ .replace(/<\/redacted_thinking>/gi, '')
9
+ .replace(/<\/thinking>/gi, '')
10
+ .replace(/<\|(?:mask_start|mask_end)\|>/g, '')
11
+ .trim();
12
+ }
13
+ export function sanitizeDirectModeOutput(text) {
14
+ let cleaned = stripHiddenThoughtBlocks(text)
15
+ .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
16
+ .replace(/<think>[\s\S]*?<\/think>/gi, '')
17
+ .replace(/<\|(?:mask_start|mask_end)\|>/g, '');
18
+ cleaned = cleaned.replace(/Tool (?:read_file|grep|list_dir|glob|bash|write_file|edit_file|ssh_exec|task|multi_edit|codebase_search) (?:succeeded|FAILED)\.[\s\S]*?(?=\nTool |\n\n|$)/g, '');
19
+ const contaminationPatterns = [
20
+ /^\[Agent recovered from backend failure[^\]]*\]\s*/m,
21
+ /^Evidence gathered before backend failure:?\s*/m,
22
+ /^MANDATORY CROSS-FILE EVIDENCE[^\n]*\n?/m,
23
+ /^CONFIRMED CONFLICTING keys[^\n]*/m,
24
+ /^Keys found ONLY in [^\n]*/m,
25
+ /^CONSTRAINT: Your answer MUST[^\n]*/m,
26
+ /^GROUNDING CHECK:[^\n]*/m,
27
+ /^VERIFICATION PROTOCOL[^\n]*/m,
28
+ /^Quality gate:[^\n]*/m,
29
+ /^Evidence collected:[^\n]*/m,
30
+ /^Warning: \d+ search tool[^\n]*/m,
31
+ /^Tool results received for direct mode[^\n]*/m,
32
+ /^Step \d+ complete\. Task:[^\n]*/m,
33
+ /^Original user request:[^\n]*/m,
34
+ /^Project root boundary:[^\n]*/m,
35
+ /^Do not declare success[^\n]*/m,
36
+ /^Keep working from concrete[^\n]*/m,
37
+ /^Continue with tool calls if more[^\n]*/m,
38
+ /^IMPORTANT: Your response must be ONLY[^\n]*/m,
39
+ /^Because this is a debugging[^\n]*/m,
40
+ /^If a user is asking which file[^\n]*/m,
41
+ /^If the request is already[^\n]*/m,
42
+ /^If more work is required[^\n]*/m,
43
+ /^Do not ask follow-up[^\n]*/m,
44
+ /^CRITICAL GROUNDING RULE:[^\n]*/m,
45
+ /^CROSS-FILE ATTRIBUTION:[^\n]*/m,
46
+ /^EVIDENCE-GROUNDING RULE:[^\n]*/m,
47
+ /^CROSS-FILE RULE:[^\n]*/m,
48
+ /^OUTPUT DISCIPLINE:[^\n]*/m,
49
+ /^Vigthoria CLI agent operating contract\.[^\n]*/m,
50
+ /^You are operating inside the project root:[^\n]*/m,
51
+ /^CRITICAL: Begin working on the user's task[^\n]*/m,
52
+ /^File: \S+\s*$/m,
53
+ /^Search status: \S+\s*$/m,
54
+ /^Output:\s*$/m,
55
+ /^\[\.\.\. ?truncated\]\s*$/m,
56
+ /^---\s*$/m,
57
+ ];
58
+ for (const pattern of contaminationPatterns)
59
+ cleaned = cleaned.replace(pattern, '');
60
+ cleaned = cleaned.split(/\n\n+/).filter((paragraph) => {
61
+ const value = paragraph.trim();
62
+ if (!value || /^Tool (?:read_file|grep|list_dir|glob|bash|write_file) /i.test(value))
63
+ return false;
64
+ const lines = value.split('\n');
65
+ if (lines.length > 6) {
66
+ const codeLines = lines.filter((line) => /[{};=]|function |class |const |let |var |import |export |switch |case /.test(line));
67
+ if (codeLines.length / lines.length > 0.4)
68
+ return false;
69
+ }
70
+ return true;
71
+ }).join('\n\n');
72
+ return cleaned.replace(/\n{3,}/g, '\n\n').trim() || stripHiddenThoughtBlocks(text);
73
+ }
74
+ export function isDirectModeFollowUpQuestion(text) {
75
+ return /^(would you like me|do you want me|which aspect|what aspect|can you clarify|could you clarify|should i focus on|i will follow|i understand|i('ll| will) adhere|provide your|waiting for)/i.test(text.trim());
76
+ }
@@ -0,0 +1,57 @@
1
+ export interface DurableJsonEnvelope<T> {
2
+ schemaVersion: number;
3
+ revision: number;
4
+ updatedAt: string;
5
+ data: T;
6
+ }
7
+ export interface DurableJsonStoreOptions<T> {
8
+ filePath: string;
9
+ schemaVersion: number;
10
+ defaultValue: () => T;
11
+ validate: (value: unknown) => value is T;
12
+ migrate?: (value: unknown, fromVersion: number) => T;
13
+ mode?: number;
14
+ lockTimeoutMs?: number;
15
+ staleLockMs?: number;
16
+ now?: () => number;
17
+ }
18
+ export declare class DurableStateError extends Error {
19
+ readonly code: 'LOCK_TIMEOUT' | 'INVALID_STATE' | 'MIGRATION_FAILED' | 'WRITE_FAILED';
20
+ readonly filePath: string;
21
+ constructor(message: string, code: 'LOCK_TIMEOUT' | 'INVALID_STATE' | 'MIGRATION_FAILED' | 'WRITE_FAILED', filePath: string, options?: {
22
+ cause?: unknown;
23
+ });
24
+ }
25
+ export declare class DurableJsonStore<T> {
26
+ readonly filePath: string;
27
+ readonly backupPath: string;
28
+ readonly lockPath: string;
29
+ private readonly schemaVersion;
30
+ private readonly defaultValue;
31
+ private readonly validate;
32
+ private readonly migrate?;
33
+ private readonly mode;
34
+ private readonly lockTimeoutMs;
35
+ private readonly staleLockMs;
36
+ private readonly now;
37
+ constructor(options: DurableJsonStoreOptions<T>);
38
+ read(): T;
39
+ readEnvelope(): DurableJsonEnvelope<T>;
40
+ write(value: T): DurableJsonEnvelope<T>;
41
+ update(mutator: (current: T) => T): DurableJsonEnvelope<T>;
42
+ remove(): boolean;
43
+ private createEnvelope;
44
+ private readEnvelopeLocked;
45
+ private parseFile;
46
+ private decode;
47
+ private runMigration;
48
+ private preserveMigrationSource;
49
+ private tryRecoverFromBackup;
50
+ private writeEnvelopeLocked;
51
+ private writeBackupEnvelope;
52
+ private applyMode;
53
+ private syncDirectory;
54
+ private withLock;
55
+ private acquireLock;
56
+ private removeStaleLock;
57
+ }
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Durable, schema-versioned JSON persistence for CLI-owned state.
3
+ *
4
+ * The store deliberately stays synchronous because the existing CLI state APIs
5
+ * are synchronous. Every read that may migrate/recover and every write is
6
+ * serialized by an inter-process directory lock. Writes use a same-directory
7
+ * temporary file, fsync, and atomic rename; the last valid generation is kept
8
+ * as a recovery backup.
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { randomUUID } from 'crypto';
13
+ export class DurableStateError extends Error {
14
+ code;
15
+ filePath;
16
+ constructor(message, code, filePath, options) {
17
+ super(message, options);
18
+ this.code = code;
19
+ this.filePath = filePath;
20
+ this.name = 'DurableStateError';
21
+ }
22
+ }
23
+ function sleepSync(milliseconds) {
24
+ const waitBuffer = new Int32Array(new SharedArrayBuffer(4));
25
+ Atomics.wait(waitBuffer, 0, 0, milliseconds);
26
+ }
27
+ function isEnvelope(value) {
28
+ if (!value || typeof value !== 'object')
29
+ return false;
30
+ const candidate = value;
31
+ return Number.isInteger(candidate.schemaVersion)
32
+ && Number.isInteger(candidate.revision)
33
+ && typeof candidate.updatedAt === 'string'
34
+ && Object.prototype.hasOwnProperty.call(candidate, 'data');
35
+ }
36
+ export class DurableJsonStore {
37
+ filePath;
38
+ backupPath;
39
+ lockPath;
40
+ schemaVersion;
41
+ defaultValue;
42
+ validate;
43
+ migrate;
44
+ mode;
45
+ lockTimeoutMs;
46
+ staleLockMs;
47
+ now;
48
+ constructor(options) {
49
+ if (!Number.isInteger(options.schemaVersion) || options.schemaVersion < 1) {
50
+ throw new Error('Durable JSON schemaVersion must be a positive integer.');
51
+ }
52
+ this.filePath = path.resolve(options.filePath);
53
+ this.backupPath = `${this.filePath}.bak`;
54
+ this.lockPath = `${this.filePath}.lock`;
55
+ this.schemaVersion = options.schemaVersion;
56
+ this.defaultValue = options.defaultValue;
57
+ this.validate = options.validate;
58
+ this.migrate = options.migrate;
59
+ this.mode = options.mode ?? 0o600;
60
+ this.lockTimeoutMs = options.lockTimeoutMs ?? 5000;
61
+ this.staleLockMs = options.staleLockMs ?? 30_000;
62
+ this.now = options.now ?? Date.now;
63
+ }
64
+ read() {
65
+ return this.withLock(() => this.readEnvelopeLocked(true).data);
66
+ }
67
+ readEnvelope() {
68
+ return this.withLock(() => this.readEnvelopeLocked(true));
69
+ }
70
+ write(value) {
71
+ if (!this.validate(value)) {
72
+ throw new DurableStateError('Refusing to write invalid durable state.', 'INVALID_STATE', this.filePath);
73
+ }
74
+ return this.withLock(() => {
75
+ const current = this.readEnvelopeLocked(false);
76
+ const envelope = this.createEnvelope(value, current.revision + 1);
77
+ this.writeEnvelopeLocked(envelope);
78
+ return envelope;
79
+ });
80
+ }
81
+ update(mutator) {
82
+ return this.withLock(() => {
83
+ const current = this.readEnvelopeLocked(true);
84
+ const next = mutator(current.data);
85
+ if (!this.validate(next)) {
86
+ throw new DurableStateError('Durable state update produced an invalid value.', 'INVALID_STATE', this.filePath);
87
+ }
88
+ const envelope = this.createEnvelope(next, current.revision + 1);
89
+ this.writeEnvelopeLocked(envelope);
90
+ return envelope;
91
+ });
92
+ }
93
+ remove() {
94
+ return this.withLock(() => {
95
+ let removed = false;
96
+ for (const candidate of [this.filePath, this.backupPath]) {
97
+ try {
98
+ fs.unlinkSync(candidate);
99
+ removed = true;
100
+ }
101
+ catch (error) {
102
+ if (error.code !== 'ENOENT')
103
+ throw error;
104
+ }
105
+ }
106
+ this.syncDirectory();
107
+ return removed;
108
+ });
109
+ }
110
+ createEnvelope(data, revision) {
111
+ return {
112
+ schemaVersion: this.schemaVersion,
113
+ revision,
114
+ updatedAt: new Date(this.now()).toISOString(),
115
+ data,
116
+ };
117
+ }
118
+ readEnvelopeLocked(repair) {
119
+ if (!fs.existsSync(this.filePath)) {
120
+ return this.createEnvelope(this.defaultValue(), 0);
121
+ }
122
+ try {
123
+ const parsed = this.parseFile(this.filePath);
124
+ const decoded = this.decode(parsed);
125
+ if (decoded.requiresRewrite && repair) {
126
+ this.preserveMigrationSource();
127
+ this.writeEnvelopeLocked(decoded.envelope);
128
+ }
129
+ return decoded.envelope;
130
+ }
131
+ catch (primaryError) {
132
+ if (repair) {
133
+ const recovered = this.tryRecoverFromBackup(primaryError);
134
+ if (recovered)
135
+ return recovered;
136
+ }
137
+ if (primaryError instanceof DurableStateError)
138
+ throw primaryError;
139
+ throw new DurableStateError('Durable state is unreadable and no valid recovery copy exists.', 'INVALID_STATE', this.filePath, { cause: primaryError });
140
+ }
141
+ }
142
+ parseFile(candidate) {
143
+ const text = fs.readFileSync(candidate, 'utf8');
144
+ if (!text.trim()) {
145
+ throw new Error('State file is empty.');
146
+ }
147
+ return JSON.parse(text);
148
+ }
149
+ decode(parsed) {
150
+ if (isEnvelope(parsed)) {
151
+ if (parsed.schemaVersion > this.schemaVersion) {
152
+ throw new DurableStateError(`State schema ${parsed.schemaVersion} is newer than supported schema ${this.schemaVersion}.`, 'INVALID_STATE', this.filePath);
153
+ }
154
+ if (parsed.schemaVersion === this.schemaVersion && this.validate(parsed.data)) {
155
+ return { envelope: parsed, requiresRewrite: false };
156
+ }
157
+ return {
158
+ envelope: this.createEnvelope(this.runMigration(parsed.data, parsed.schemaVersion), parsed.revision + 1),
159
+ requiresRewrite: true,
160
+ };
161
+ }
162
+ return {
163
+ envelope: this.createEnvelope(this.runMigration(parsed, 0), 1),
164
+ requiresRewrite: true,
165
+ };
166
+ }
167
+ runMigration(value, fromVersion) {
168
+ try {
169
+ const migrated = this.migrate ? this.migrate(value, fromVersion) : value;
170
+ if (!this.validate(migrated))
171
+ throw new Error('Migrated value failed schema validation.');
172
+ return migrated;
173
+ }
174
+ catch (error) {
175
+ throw new DurableStateError(`Could not migrate durable state from schema ${fromVersion} to ${this.schemaVersion}.`, 'MIGRATION_FAILED', this.filePath, { cause: error });
176
+ }
177
+ }
178
+ preserveMigrationSource() {
179
+ const migrationBackup = `${this.filePath}.pre-migration.bak`;
180
+ if (!fs.existsSync(migrationBackup)) {
181
+ fs.copyFileSync(this.filePath, migrationBackup, fs.constants.COPYFILE_EXCL);
182
+ this.applyMode(migrationBackup);
183
+ }
184
+ }
185
+ tryRecoverFromBackup(primaryError) {
186
+ if (!fs.existsSync(this.backupPath))
187
+ return null;
188
+ try {
189
+ const recovered = this.decode(this.parseFile(this.backupPath)).envelope;
190
+ const corruptPath = `${this.filePath}.corrupt-${this.now()}-${process.pid}`;
191
+ fs.renameSync(this.filePath, corruptPath);
192
+ this.applyMode(corruptPath);
193
+ this.writeEnvelopeLocked(recovered, false);
194
+ return recovered;
195
+ }
196
+ catch (backupError) {
197
+ throw new DurableStateError('Durable state and its recovery backup are both invalid.', 'INVALID_STATE', this.filePath, { cause: { primaryError, backupError } });
198
+ }
199
+ }
200
+ writeEnvelopeLocked(envelope, rotateBackup = true) {
201
+ const parent = path.dirname(this.filePath);
202
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
203
+ if (process.platform !== 'win32') {
204
+ try {
205
+ fs.chmodSync(parent, 0o700);
206
+ }
207
+ catch { /* filesystem may not support chmod */ }
208
+ }
209
+ const temporary = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
210
+ let descriptor;
211
+ try {
212
+ descriptor = fs.openSync(temporary, 'wx', this.mode);
213
+ fs.writeFileSync(descriptor, `${JSON.stringify(envelope, null, 2)}\n`, 'utf8');
214
+ fs.fsyncSync(descriptor);
215
+ fs.closeSync(descriptor);
216
+ descriptor = undefined;
217
+ this.applyMode(temporary);
218
+ if (rotateBackup && fs.existsSync(this.filePath)) {
219
+ try {
220
+ const current = this.decode(this.parseFile(this.filePath)).envelope;
221
+ this.writeBackupEnvelope(current);
222
+ }
223
+ catch {
224
+ // Never replace a known-good backup with a corrupt current file.
225
+ }
226
+ }
227
+ fs.renameSync(temporary, this.filePath);
228
+ this.applyMode(this.filePath);
229
+ this.syncDirectory();
230
+ }
231
+ catch (error) {
232
+ if (descriptor !== undefined) {
233
+ try {
234
+ fs.closeSync(descriptor);
235
+ }
236
+ catch { /* already closed */ }
237
+ }
238
+ try {
239
+ fs.unlinkSync(temporary);
240
+ }
241
+ catch { /* absent */ }
242
+ if (error instanceof DurableStateError)
243
+ throw error;
244
+ throw new DurableStateError('Atomic durable-state write failed.', 'WRITE_FAILED', this.filePath, { cause: error });
245
+ }
246
+ }
247
+ writeBackupEnvelope(envelope) {
248
+ const temporary = `${this.backupPath}.${process.pid}.${randomUUID()}.tmp`;
249
+ fs.writeFileSync(temporary, `${JSON.stringify(envelope, null, 2)}\n`, { encoding: 'utf8', mode: this.mode, flag: 'wx' });
250
+ this.applyMode(temporary);
251
+ fs.renameSync(temporary, this.backupPath);
252
+ this.applyMode(this.backupPath);
253
+ }
254
+ applyMode(candidate) {
255
+ if (process.platform === 'win32')
256
+ return;
257
+ try {
258
+ fs.chmodSync(candidate, this.mode);
259
+ }
260
+ catch { /* filesystem may not support chmod */ }
261
+ }
262
+ syncDirectory() {
263
+ if (process.platform === 'win32')
264
+ return;
265
+ let descriptor;
266
+ try {
267
+ descriptor = fs.openSync(path.dirname(this.filePath), 'r');
268
+ fs.fsyncSync(descriptor);
269
+ }
270
+ catch {
271
+ // Some filesystems do not allow directory fsync.
272
+ }
273
+ finally {
274
+ if (descriptor !== undefined)
275
+ fs.closeSync(descriptor);
276
+ }
277
+ }
278
+ withLock(operation) {
279
+ this.acquireLock();
280
+ try {
281
+ return operation();
282
+ }
283
+ finally {
284
+ try {
285
+ fs.rmdirSync(this.lockPath);
286
+ }
287
+ catch { /* stale lock cleanup handles leftovers */ }
288
+ }
289
+ }
290
+ acquireLock() {
291
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
292
+ const startedAt = this.now();
293
+ while (true) {
294
+ try {
295
+ fs.mkdirSync(this.lockPath, { mode: 0o700 });
296
+ return;
297
+ }
298
+ catch (error) {
299
+ if (error.code !== 'EEXIST')
300
+ throw error;
301
+ this.removeStaleLock();
302
+ if (this.now() - startedAt >= this.lockTimeoutMs) {
303
+ throw new DurableStateError('Timed out waiting for the durable-state writer lock.', 'LOCK_TIMEOUT', this.filePath);
304
+ }
305
+ sleepSync(10);
306
+ }
307
+ }
308
+ }
309
+ removeStaleLock() {
310
+ try {
311
+ const age = this.now() - fs.statSync(this.lockPath).mtimeMs;
312
+ if (age > this.staleLockMs)
313
+ fs.rmdirSync(this.lockPath);
314
+ }
315
+ catch {
316
+ // Another writer may have released the lock between stat and removal.
317
+ }
318
+ }
319
+ }
@@ -1,8 +1,5 @@
1
- /**
2
- * Vigthoria v4 Assistant 9B — fast agent path classifier through the governed router.
3
- * Regex/trivial detection always wins; LLM router refines ambiguous build prompts only.
4
- */
5
- export type AgentRoutePath = 'template-instant' | 'v3-agent' | 'analysis-only';
1
+ /** The CLI has one executable agent transport; specialized behavior is a server-side hint. */
2
+ export type AgentRoutePath = 'v3-agent';
6
3
  export interface BalancedRouterJson {
7
4
  path?: string;
8
5
  task_kind?: string;
@@ -21,14 +18,16 @@ export interface BalancedRouterResult {
21
18
  endpoint?: string;
22
19
  error?: string;
23
20
  }
21
+ export type InferenceFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
24
22
  export declare function parseBalancedRouterJson(raw: string): BalancedRouterJson | null;
25
23
  /**
26
24
  * Inference bases for the V4 Assistant routing lane only.
27
25
  * Local developer machines must opt in (env) — avoids ~1.5s localhost probe on every Windows turn.
28
26
  */
29
27
  export declare function getInferenceRouterBaseUrls(selfHostedUrl: string | null): string[];
30
- export declare function isFastInferenceRouterAvailable(selfHostedUrl: string | null, timeoutMs?: number): Promise<boolean>;
28
+ export declare function isFastInferenceRouterAvailable(selfHostedUrl: string | null, timeoutMs?: number, fetchImpl?: InferenceFetch): Promise<boolean>;
31
29
  export declare function routeWithBalanced4b(prompt: string, selfHostedUrl: string | null, options?: {
32
30
  timeoutMs?: number;
31
+ fetchImpl?: InferenceFetch;
33
32
  }): Promise<BalancedRouterResult>;
34
33
  export declare function shouldUseBalancedRouter(prompt: string, regexTaskKind?: string): boolean;
@@ -2,6 +2,7 @@
2
2
  * Vigthoria v4 Assistant 9B — fast agent path classifier through the governed router.
3
3
  * Regex/trivial detection always wins; LLM router refines ambiguous build prompts only.
4
4
  */
5
+ import { redactSensitiveText } from './secret-policy.js';
5
6
  const ROUTER_SYSTEM = `You are Vigthoria Agent Router. Reply with ONE JSON object only. No markdown, no HTML, no code fences.
6
7
 
7
8
  Paths:
@@ -16,19 +17,6 @@ Example: {"path":"template-instant","task_kind":"web-build","fast_path":"templat
16
17
  const INFERENCE_MODEL_IDS = [
17
18
  'Vigthoria-v4-Assistant-9B',
18
19
  ];
19
- function normalizePath(value) {
20
- const p = String(value || '').trim().toLowerCase();
21
- if (p === 'template-instant' || p === 'template' || p === 'html-template') {
22
- return 'template-instant';
23
- }
24
- if (p === 'analysis-only' || p === 'analysis') {
25
- return 'analysis-only';
26
- }
27
- if (p === 'v3-agent' || p === 'v3' || p === 'agent') {
28
- return 'v3-agent';
29
- }
30
- return null;
31
- }
32
20
  export function parseBalancedRouterJson(raw) {
33
21
  const text = String(raw || '').trim();
34
22
  if (!text)
@@ -94,15 +82,15 @@ function modelIdsFromPayload(payload) {
94
82
  }).filter(Boolean);
95
83
  }
96
84
  function includesBalancedRouterModel(modelIds) {
97
- const allowed = new Set(INFERENCE_MODEL_IDS);
85
+ const allowed = new Set(INFERENCE_MODEL_IDS.map((id) => id.toLowerCase()));
98
86
  return modelIds.some((id) => allowed.has(String(id).trim().toLowerCase().replace(/:latest$/, '')));
99
87
  }
100
- async function probeBalancedRouterBase(base, timeoutMs) {
88
+ async function probeBalancedRouterBase(base, timeoutMs, fetchImpl) {
101
89
  const fetchJson = async (path) => {
102
90
  const controller = new AbortController();
103
91
  const timer = setTimeout(() => controller.abort(), timeoutMs);
104
92
  try {
105
- const response = await fetch(`${base}${path}`, { signal: controller.signal });
93
+ const response = await fetchImpl(`${base}${path}`, { signal: controller.signal });
106
94
  const payload = await response.json().catch(() => null);
107
95
  return { ok: response.ok, payload };
108
96
  }
@@ -124,33 +112,37 @@ async function probeBalancedRouterBase(base, timeoutMs) {
124
112
  const catalog = await fetchJson('/v1/models');
125
113
  return catalog.ok && includesBalancedRouterModel(modelIdsFromPayload(catalog.payload));
126
114
  }
127
- async function compatibleInferenceRouterBases(selfHostedUrl, timeoutMs) {
115
+ async function compatibleInferenceRouterBases(selfHostedUrl, timeoutMs, fetchImpl) {
128
116
  const bases = getInferenceRouterBaseUrls(selfHostedUrl);
129
117
  const key = bases.join('|');
130
118
  const now = Date.now();
131
- if (inferenceHealthCache
119
+ if (fetchImpl === globalThis.fetch
120
+ &&
121
+ inferenceHealthCache
132
122
  && inferenceHealthCache.key === key
133
123
  && now - inferenceHealthCache.checkedAt < 60_000) {
134
124
  return inferenceHealthCache.compatibleBases;
135
125
  }
136
126
  const compatibleBases = [];
137
127
  for (const base of bases) {
138
- if (await probeBalancedRouterBase(base, timeoutMs)) {
128
+ if (await probeBalancedRouterBase(base, timeoutMs, fetchImpl)) {
139
129
  compatibleBases.push(base);
140
130
  }
141
131
  }
142
- inferenceHealthCache = { key, compatibleBases, checkedAt: now };
132
+ if (fetchImpl === globalThis.fetch) {
133
+ inferenceHealthCache = { key, compatibleBases, checkedAt: now };
134
+ }
143
135
  return compatibleBases;
144
136
  }
145
- export async function isFastInferenceRouterAvailable(selfHostedUrl, timeoutMs = 400) {
137
+ export async function isFastInferenceRouterAvailable(selfHostedUrl, timeoutMs = 400, fetchImpl = globalThis.fetch) {
146
138
  if (process.env.VIGTHORIA_AGENT_LLM_ROUTER === '0') {
147
139
  return false;
148
140
  }
149
- return (await compatibleInferenceRouterBases(selfHostedUrl, timeoutMs)).length > 0;
141
+ return (await compatibleInferenceRouterBases(selfHostedUrl, timeoutMs, fetchImpl)).length > 0;
150
142
  }
151
143
  export async function routeWithBalanced4b(prompt, selfHostedUrl, options = {}) {
152
144
  const timeoutMs = options.timeoutMs ?? Number.parseInt(process.env.VIGTHORIA_AGENT_ROUTER_TIMEOUT_MS || '8000', 10);
153
- const bases = await compatibleInferenceRouterBases(selfHostedUrl, Math.min(timeoutMs, 1500));
145
+ const bases = await compatibleInferenceRouterBases(selfHostedUrl, Math.min(timeoutMs, 1500), options.fetchImpl || globalThis.fetch);
154
146
  let lastError = 'Inference router unreachable';
155
147
  if (bases.length === 0) {
156
148
  return { ok: false, error: 'No compatible Balanced 4B inference endpoint is available' };
@@ -161,14 +153,14 @@ export async function routeWithBalanced4b(prompt, selfHostedUrl, options = {}) {
161
153
  const controller = new AbortController();
162
154
  const timer = setTimeout(() => controller.abort(), timeoutMs);
163
155
  try {
164
- const response = await fetch(`${base}/v1/chat/completions`, {
156
+ const response = await (options.fetchImpl || globalThis.fetch)(`${base}/v1/chat/completions`, {
165
157
  method: 'POST',
166
158
  headers: { 'Content-Type': 'application/json' },
167
159
  body: JSON.stringify({
168
160
  model,
169
161
  messages: [
170
162
  { role: 'system', content: ROUTER_SYSTEM },
171
- { role: 'user', content: String(prompt || '').slice(0, 1200) },
163
+ { role: 'user', content: redactSensitiveText(String(prompt || '')).slice(0, 1200) },
172
164
  ],
173
165
  max_tokens: 96,
174
166
  temperature: 0,
@@ -4,6 +4,8 @@
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import { glob } from 'glob';
7
+ import { assertContainedAbsolutePath, resolveWorkspacePath } from './workspace-boundary.js';
8
+ import { containsHighConfidenceSecret, isSensitivePath } from './secret-policy.js';
7
9
  export class FileUtils {
8
10
  projectRoot;
9
11
  ignorePatterns;
@@ -21,22 +23,19 @@ export class FileUtils {
21
23
  // Read file with metadata
22
24
  readFile(filePath) {
23
25
  try {
24
- let absolutePath = path.isAbsolute(filePath)
25
- ? filePath
26
- : path.join(this.projectRoot, filePath);
27
- // Resolve symlinks for consistent path handling
28
- try {
29
- absolutePath = fs.realpathSync(absolutePath);
30
- }
31
- catch {
32
- // Keep original path if realpath fails
33
- }
26
+ if (isSensitivePath(filePath))
27
+ return null;
28
+ const absolutePath = path.isAbsolute(filePath)
29
+ ? assertContainedAbsolutePath(this.projectRoot, filePath)
30
+ : resolveWorkspacePath(this.projectRoot, filePath);
34
31
  // Check if it's a regular file (not a directory or special file)
35
32
  const stats = fs.statSync(absolutePath);
36
33
  if (!stats.isFile()) {
37
34
  return null;
38
35
  }
39
36
  const content = fs.readFileSync(absolutePath, 'utf-8');
37
+ if (containsHighConfidenceSecret(content))
38
+ return null;
40
39
  const relativePath = path.relative(this.projectRoot, absolutePath);
41
40
  return {
42
41
  path: absolutePath,
@@ -62,8 +61,8 @@ export class FileUtils {
62
61
  writeFile(filePath, content) {
63
62
  try {
64
63
  const absolutePath = path.isAbsolute(filePath)
65
- ? filePath
66
- : path.join(this.projectRoot, filePath);
64
+ ? assertContainedAbsolutePath(this.projectRoot, filePath, { allowMissing: true })
65
+ : resolveWorkspacePath(this.projectRoot, filePath, { allowMissing: true });
67
66
  // Create directory if needed
68
67
  const dir = path.dirname(absolutePath);
69
68
  if (!fs.existsSync(dir)) {
@@ -98,8 +97,17 @@ export class FileUtils {
98
97
  cwd: this.projectRoot,
99
98
  ignore: this.ignorePatterns,
100
99
  nodir: true,
100
+ follow: false,
101
+ });
102
+ return files.filter((file) => {
103
+ try {
104
+ resolveWorkspacePath(this.projectRoot, file);
105
+ return !isSensitivePath(file);
106
+ }
107
+ catch {
108
+ return false;
109
+ }
101
110
  });
102
- return files;
103
111
  }
104
112
  catch (error) {
105
113
  // Return empty array on error (e.g., permission issues on Windows)