@adhdev/daemon-core 0.9.82-rc.13 → 0.9.82-rc.131

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 (82) hide show
  1. package/dist/chat/subscription-updates.d.ts +1 -0
  2. package/dist/cli-adapter-types.d.ts +4 -1
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +25 -1
  4. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  5. package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
  6. package/dist/commands/router.d.ts +22 -0
  7. package/dist/config/chat-history.d.ts +4 -0
  8. package/dist/config/mesh-config.d.ts +68 -1
  9. package/dist/git/git-commands.d.ts +5 -1
  10. package/dist/index.d.ts +15 -5
  11. package/dist/index.js +7538 -1380
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +7494 -1364
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/installer.d.ts +1 -4
  16. package/dist/launch.d.ts +1 -1
  17. package/dist/logging/async-batch-writer.d.ts +10 -0
  18. package/dist/mesh/beads-db.d.ts +18 -0
  19. package/dist/mesh/mesh-active-work.d.ts +73 -0
  20. package/dist/mesh/mesh-events.d.ts +54 -5
  21. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  22. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  23. package/dist/mesh/mesh-ledger.d.ts +38 -1
  24. package/dist/mesh/mesh-refine-status.d.ts +27 -0
  25. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  26. package/dist/mesh/preview-freshness.d.ts +18 -0
  27. package/dist/mesh/refine-config.d.ts +193 -0
  28. package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
  29. package/dist/providers/chat-message-normalization.d.ts +1 -0
  30. package/dist/providers/cli-provider-instance.d.ts +6 -1
  31. package/dist/repo-mesh-types.d.ts +62 -0
  32. package/dist/status/reporter.d.ts +2 -0
  33. package/package.json +3 -1
  34. package/src/boot/daemon-lifecycle.ts +1 -0
  35. package/src/chat/subscription-updates.ts +5 -1
  36. package/src/cli-adapter-types.ts +2 -1
  37. package/src/cli-adapters/provider-cli-adapter.ts +473 -18
  38. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  39. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  40. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  41. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  42. package/src/cli-adapters/provider-cli-shared.ts +32 -10
  43. package/src/commands/chat-commands.ts +1065 -40
  44. package/src/commands/cli-manager.ts +138 -2
  45. package/src/commands/handler.ts +8 -1
  46. package/src/commands/mesh-coordinator.ts +13 -143
  47. package/src/commands/router.ts +3238 -423
  48. package/src/config/chat-history.ts +37 -9
  49. package/src/config/mesh-config.ts +249 -2
  50. package/src/config/recent-activity.ts +8 -2
  51. package/src/daemon/dev-cli-debug.ts +10 -1
  52. package/src/detection/ide-detector.ts +26 -16
  53. package/src/git/git-commands.ts +17 -5
  54. package/src/index.ts +41 -4
  55. package/src/installer.d.ts +1 -1
  56. package/src/installer.ts +8 -6
  57. package/src/launch.d.ts +1 -1
  58. package/src/launch.ts +37 -28
  59. package/src/logging/async-batch-writer.ts +55 -0
  60. package/src/logging/logger.ts +2 -1
  61. package/src/mesh/beads-db.ts +176 -0
  62. package/src/mesh/coordinator-prompt.ts +31 -8
  63. package/src/mesh/mesh-active-work.ts +295 -0
  64. package/src/mesh/mesh-events.ts +595 -48
  65. package/src/mesh/mesh-fast-forward.ts +430 -0
  66. package/src/mesh/mesh-host-ownership.ts +73 -0
  67. package/src/mesh/mesh-ledger.ts +138 -1
  68. package/src/mesh/mesh-refine-status.ts +145 -0
  69. package/src/mesh/mesh-work-queue.ts +199 -137
  70. package/src/mesh/preview-freshness.ts +118 -0
  71. package/src/mesh/refine-config.ts +366 -0
  72. package/src/mesh/worktree-bootstrap-config.ts +234 -0
  73. package/src/providers/approval-utils.ts +12 -5
  74. package/src/providers/chat-message-normalization.ts +7 -12
  75. package/src/providers/cli-provider-instance.ts +289 -36
  76. package/src/providers/ide-provider-instance.ts +17 -3
  77. package/src/providers/provider-loader.ts +10 -4
  78. package/src/providers/read-chat-contract.ts +1 -1
  79. package/src/providers/version-archive.ts +38 -20
  80. package/src/repo-mesh-types.ts +67 -0
  81. package/src/status/reporter.ts +15 -0
  82. package/src/system/host-memory.ts +29 -12
@@ -0,0 +1,118 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+
5
+ export type PreviewFreshnessStatus = 'fresh' | 'stale' | 'unknown' | 'not_configured';
6
+
7
+ export interface PreviewFreshness {
8
+ status: PreviewFreshnessStatus;
9
+ lastPreviewCommit: string | null;
10
+ currentMainCommit: string | null;
11
+ currentMainCommitSource: 'origin/main' | 'HEAD' | 'unknown';
12
+ recordPath: string;
13
+ lastDeployedAt?: string;
14
+ lastTarget?: string;
15
+ previewVersion?: string;
16
+ targets: Record<'npm' | 'server' | 'web', {
17
+ commit: string | null;
18
+ deployedAt?: string;
19
+ status: PreviewFreshnessStatus;
20
+ }>;
21
+ nextAction: string;
22
+ }
23
+
24
+ const PREVIEW_DEPLOY_RECORD = '.adhdev/preview-deploy.json';
25
+
26
+ function runGit(repoRoot: string, args: readonly string[]): string {
27
+ try {
28
+ return execFileSync('git', args, {
29
+ cwd: repoRoot,
30
+ encoding: 'utf8',
31
+ stdio: ['ignore', 'pipe', 'ignore'],
32
+ timeout: 5000,
33
+ }).trim();
34
+ } catch {
35
+ return '';
36
+ }
37
+ }
38
+
39
+ function readRecord(repoRoot: string): Record<string, unknown> | null {
40
+ const path = resolve(repoRoot, PREVIEW_DEPLOY_RECORD);
41
+ if (!existsSync(path)) return null;
42
+ try {
43
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
44
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
45
+ ? parsed as Record<string, unknown>
46
+ : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function normalizeCommit(value: unknown): string | null {
53
+ return typeof value === 'string' && /^[0-9a-f]{7,40}$/i.test(value.trim())
54
+ ? value.trim()
55
+ : null;
56
+ }
57
+
58
+ function readTargetFreshness(record: Record<string, unknown> | null, currentCommit: string | null): PreviewFreshness['targets'] {
59
+ const targets = record?.targets && typeof record.targets === 'object' && !Array.isArray(record.targets)
60
+ ? record.targets as Record<string, unknown>
61
+ : {};
62
+ const result = {} as PreviewFreshness['targets'];
63
+ for (const targetName of ['npm', 'server', 'web'] as const) {
64
+ const targetRecord = targets[targetName] && typeof targets[targetName] === 'object' && !Array.isArray(targets[targetName])
65
+ ? targets[targetName] as Record<string, unknown>
66
+ : {};
67
+ const commit = normalizeCommit(targetRecord.commit);
68
+ result[targetName] = {
69
+ commit,
70
+ deployedAt: typeof targetRecord.deployedAt === 'string' ? targetRecord.deployedAt : undefined,
71
+ status: commit && currentCommit ? (commit === currentCommit ? 'fresh' : 'stale') : 'unknown',
72
+ };
73
+ }
74
+ return result;
75
+ }
76
+
77
+ function readCurrentMainCommit(repoRoot: string): Pick<PreviewFreshness, 'currentMainCommit' | 'currentMainCommitSource'> {
78
+ const originMain = runGit(repoRoot, ['rev-parse', '--verify', 'origin/main^{commit}']);
79
+ if (originMain) {
80
+ return { currentMainCommit: originMain, currentMainCommitSource: 'origin/main' };
81
+ }
82
+ const head = runGit(repoRoot, ['rev-parse', '--verify', 'HEAD']);
83
+ if (head) {
84
+ return { currentMainCommit: head, currentMainCommitSource: 'HEAD' };
85
+ }
86
+ return { currentMainCommit: null, currentMainCommitSource: 'unknown' };
87
+ }
88
+
89
+ export function buildPreviewFreshness(repoRoot: string): PreviewFreshness {
90
+ const current = readCurrentMainCommit(repoRoot);
91
+ const record = readRecord(repoRoot);
92
+ const lastPreviewCommit = normalizeCommit(record?.lastPreviewCommit);
93
+ const targets = readTargetFreshness(record, current.currentMainCommit);
94
+ let status: PreviewFreshnessStatus = 'unknown';
95
+ let nextAction = 'Run npm run deploy:preview from the current main commit, then smoke preview.';
96
+
97
+ if (lastPreviewCommit && current.currentMainCommit) {
98
+ status = lastPreviewCommit === current.currentMainCommit ? 'fresh' : 'stale';
99
+ nextAction = status === 'fresh'
100
+ ? 'No preview deploy action needed.'
101
+ : 'Run npm run deploy:preview from origin/main, then smoke preview.';
102
+ } else if (!current.currentMainCommit) {
103
+ nextAction = 'Resolve the current main commit before judging preview freshness.';
104
+ }
105
+
106
+ return {
107
+ status,
108
+ lastPreviewCommit,
109
+ currentMainCommit: current.currentMainCommit,
110
+ currentMainCommitSource: current.currentMainCommitSource,
111
+ recordPath: PREVIEW_DEPLOY_RECORD,
112
+ lastDeployedAt: typeof record?.updatedAt === 'string' ? record.updatedAt : undefined,
113
+ lastTarget: typeof record?.target === 'string' ? record.target : undefined,
114
+ previewVersion: typeof record?.previewVersion === 'string' ? record.previewVersion : undefined,
115
+ targets,
116
+ nextAction,
117
+ };
118
+ }
@@ -0,0 +1,366 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import * as yaml from 'js-yaml';
4
+
5
+ export const MESH_REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
6
+ export type MeshRefineValidationCategory = typeof MESH_REFINE_VALIDATION_CATEGORIES[number];
7
+
8
+ export interface RepoMeshRefineValidationCommandConfig {
9
+ /** Executable name or a whitespace-tokenized command string. Never executed through a shell. */
10
+ command: string;
11
+ /** Optional explicit argv. Prefer this over shell-like command strings. */
12
+ args?: string[];
13
+ category?: MeshRefineValidationCategory;
14
+ cwd?: string;
15
+ timeoutMs?: number;
16
+ outputLimitBytes?: number;
17
+ env?: Record<string, string>;
18
+ }
19
+
20
+ export interface RepoMeshRefineConfig {
21
+ version: 1;
22
+ /**
23
+ * Narrow Refinery opt-in for monorepos with submodule gitlinks.
24
+ * When true, Refinery may non-force publish unreachable submodule gitlink
25
+ * commits to the submodule remote main branch after validation and
26
+ * patch-equivalence pass, then verify remote-main reachability.
27
+ */
28
+ allowAutoPublishSubmoduleMainCommits?: boolean;
29
+ validation?: {
30
+ required?: boolean;
31
+ /**
32
+ * Optional dependency/bootstrap commands that Refinery runs before
33
+ * validation commands. Refinery never infers installs on its own.
34
+ */
35
+ bootstrapCommands?: RepoMeshRefineValidationCommandConfig[];
36
+ commands?: RepoMeshRefineValidationCommandConfig[];
37
+ };
38
+ }
39
+
40
+ export interface MeshRefineValidationCommandPlan {
41
+ command: string;
42
+ args: string[];
43
+ displayCommand: string;
44
+ category: MeshRefineValidationCategory | 'custom';
45
+ source: string;
46
+ cwd?: string;
47
+ timeoutMs?: number;
48
+ outputLimitBytes?: number;
49
+ env?: Record<string, string>;
50
+ }
51
+
52
+ export interface MeshRefineConfigLoadResult {
53
+ config?: RepoMeshRefineConfig;
54
+ source: string;
55
+ sourceType: 'mesh_policy' | 'repo_file' | 'unavailable' | 'invalid';
56
+ path?: string;
57
+ error?: string;
58
+ }
59
+
60
+ export interface MeshRefineValidationPlan {
61
+ source: string;
62
+ sourceType: MeshRefineConfigLoadResult['sourceType'];
63
+ bootstrapCommands: MeshRefineValidationCommandPlan[];
64
+ commands: MeshRefineValidationCommandPlan[];
65
+ rejectedCommands: Array<Record<string, unknown>>;
66
+ suggestions: RepoMeshRefineValidationCommandConfig[];
67
+ suggestedConfig?: RepoMeshRefineConfig;
68
+ unavailableReason?: string;
69
+ }
70
+
71
+ export const MESH_REFINE_CONFIG_LOCATIONS = [
72
+ '.adhdev/refine.json',
73
+ '.adhdev/refine.yaml',
74
+ '.adhdev/refine.yml',
75
+ '.adhdev/repo-mesh-refine.json',
76
+ '.adhdev/repo-mesh-refine.yaml',
77
+ '.adhdev/repo-mesh-refine.yml',
78
+ 'repo-mesh.refine.json',
79
+ 'repo-mesh.refine.yaml',
80
+ 'repo-mesh.refine.yml',
81
+ ];
82
+
83
+ export const MESH_REFINE_CONFIG_SCHEMA = {
84
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
85
+ title: 'ADHDev Repo Mesh Refinery Config',
86
+ type: 'object',
87
+ additionalProperties: false,
88
+ required: ['version'],
89
+ properties: {
90
+ version: { const: 1 },
91
+ allowAutoPublishSubmoduleMainCommits: {
92
+ type: 'boolean',
93
+ default: false,
94
+ description: 'When true, Refinery may non-force publish submodule gitlink commits referenced by the refined root tree to each submodule origin/main after validation and patch-equivalence pass, then verify reachability.',
95
+ },
96
+ validation: {
97
+ type: 'object',
98
+ additionalProperties: false,
99
+ properties: {
100
+ required: { type: 'boolean', default: true },
101
+ commands: {
102
+ type: 'array',
103
+ minItems: 1,
104
+ maxItems: 8,
105
+ items: {
106
+ type: 'object',
107
+ additionalProperties: false,
108
+ required: ['command'],
109
+ properties: {
110
+ command: { type: 'string', minLength: 1 },
111
+ args: { type: 'array', items: { type: 'string' } },
112
+ category: { enum: [...MESH_REFINE_VALIDATION_CATEGORIES, 'custom'] },
113
+ cwd: { type: 'string' },
114
+ timeoutMs: { type: 'number', minimum: 1000, maximum: 600000 },
115
+ outputLimitBytes: { type: 'number', minimum: 1024, maximum: 1048576 },
116
+ env: { type: 'object', additionalProperties: { type: 'string' } },
117
+ },
118
+ },
119
+ },
120
+ bootstrapCommands: {
121
+ type: 'array',
122
+ maxItems: 4,
123
+ items: {
124
+ type: 'object',
125
+ additionalProperties: false,
126
+ required: ['command'],
127
+ properties: {
128
+ command: { type: 'string', minLength: 1 },
129
+ args: { type: 'array', items: { type: 'string' } },
130
+ category: { enum: [...MESH_REFINE_VALIDATION_CATEGORIES, 'custom'] },
131
+ cwd: { type: 'string' },
132
+ timeoutMs: { type: 'number', minimum: 1000, maximum: 600000 },
133
+ outputLimitBytes: { type: 'number', minimum: 1024, maximum: 1048576 },
134
+ env: { type: 'object', additionalProperties: { type: 'string' } },
135
+ },
136
+ },
137
+ },
138
+ },
139
+ },
140
+ },
141
+ } as const;
142
+
143
+ export function isMeshConfigRecord(value: unknown): value is Record<string, unknown> {
144
+ return !!value && typeof value === 'object' && !Array.isArray(value);
145
+ }
146
+
147
+ function tokenizeCommandString(command: string): string[] | null {
148
+ const trimmed = command.trim();
149
+ if (!trimmed) return null;
150
+ // Explicit config may name any executable, but the Refinery never invokes a shell.
151
+ // Reject shell syntax, quotes and substitutions so config cannot smuggle a compound command.
152
+ if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
153
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
154
+ if (!tokens.length) return null;
155
+ if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
156
+ return tokens;
157
+ }
158
+
159
+ function validateCategory(value: unknown): MeshRefineValidationCategory | 'custom' {
160
+ return typeof value === 'string' && ([...MESH_REFINE_VALIDATION_CATEGORIES, 'custom'] as string[]).includes(value)
161
+ ? value as MeshRefineValidationCategory | 'custom'
162
+ : 'custom';
163
+ }
164
+
165
+ export function normalizeMeshCommandConfig(entry: unknown, source: string): { command?: MeshRefineValidationCommandPlan; rejected?: Record<string, unknown> } {
166
+ if (!isMeshConfigRecord(entry) || typeof entry.command !== 'string') {
167
+ return { rejected: { source, reason: 'validation command must be an object with a command string' } };
168
+ }
169
+
170
+ const commandText = entry.command.trim();
171
+ const explicitArgs = Array.isArray(entry.args) ? entry.args : undefined;
172
+ if (explicitArgs && !explicitArgs.every(arg => typeof arg === 'string')) {
173
+ return { rejected: { source, command: commandText, reason: 'args must be an array of strings' } };
174
+ }
175
+
176
+ let command = commandText;
177
+ let args = explicitArgs ? [...explicitArgs] : [];
178
+ if (!explicitArgs) {
179
+ const tokens = tokenizeCommandString(commandText);
180
+ if (!tokens) return { rejected: { source, command: commandText, reason: 'unsafe command string is not allowlisted' } };
181
+ command = tokens[0];
182
+ args = tokens.slice(1);
183
+ } else if (!tokenizeCommandString(command)) {
184
+ return { rejected: { source, command: commandText, reason: 'unsafe executable name is not allowlisted' } };
185
+ }
186
+
187
+ if (args.some(arg => /[\n\r\0]/.test(arg))) {
188
+ return { rejected: { source, command: commandText, reason: 'args cannot contain control characters' } };
189
+ }
190
+ if (entry.cwd !== undefined && typeof entry.cwd !== 'string') {
191
+ return { rejected: { source, command: commandText, reason: 'cwd must be a string when provided' } };
192
+ }
193
+ if (entry.timeoutMs !== undefined && (typeof entry.timeoutMs !== 'number' || !Number.isFinite(entry.timeoutMs) || entry.timeoutMs < 1000 || entry.timeoutMs > 600000)) {
194
+ return { rejected: { source, command: commandText, reason: 'timeoutMs must be between 1000 and 600000' } };
195
+ }
196
+ if (entry.outputLimitBytes !== undefined && (typeof entry.outputLimitBytes !== 'number' || !Number.isFinite(entry.outputLimitBytes) || entry.outputLimitBytes < 1024 || entry.outputLimitBytes > 1048576)) {
197
+ return { rejected: { source, command: commandText, reason: 'outputLimitBytes must be between 1024 and 1048576' } };
198
+ }
199
+ if (entry.env !== undefined && (!isMeshConfigRecord(entry.env) || !Object.values(entry.env).every(value => typeof value === 'string'))) {
200
+ return { rejected: { source, command: commandText, reason: 'env must be an object of string values' } };
201
+ }
202
+
203
+ return {
204
+ command: {
205
+ command,
206
+ args,
207
+ displayCommand: [command, ...args].join(' '),
208
+ category: validateCategory(entry.category),
209
+ source,
210
+ ...(typeof entry.cwd === 'string' && entry.cwd.trim() ? { cwd: entry.cwd.trim() } : {}),
211
+ ...(typeof entry.timeoutMs === 'number' ? { timeoutMs: entry.timeoutMs } : {}),
212
+ ...(typeof entry.outputLimitBytes === 'number' ? { outputLimitBytes: entry.outputLimitBytes } : {}),
213
+ ...(isMeshConfigRecord(entry.env) ? { env: entry.env as Record<string, string> } : {}),
214
+ },
215
+ };
216
+ }
217
+
218
+ const isRecord = isMeshConfigRecord;
219
+
220
+ export function validateMeshRefineConfig(config: unknown, source = 'inline'): { valid: boolean; errors: string[]; bootstrapCommands: MeshRefineValidationCommandPlan[]; commands: MeshRefineValidationCommandPlan[]; rejectedCommands: Array<Record<string, unknown>> } {
221
+ const errors: string[] = [];
222
+ const bootstrapCommands: MeshRefineValidationCommandPlan[] = [];
223
+ const commands: MeshRefineValidationCommandPlan[] = [];
224
+ const rejectedCommands: Array<Record<string, unknown>> = [];
225
+
226
+ if (!isRecord(config)) return { valid: false, errors: ['config must be an object'], bootstrapCommands, commands, rejectedCommands };
227
+ if (config.version !== 1) errors.push('version must be 1');
228
+ if (config.allowAutoPublishSubmoduleMainCommits !== undefined && typeof config.allowAutoPublishSubmoduleMainCommits !== 'boolean') {
229
+ errors.push('allowAutoPublishSubmoduleMainCommits must be a boolean when provided');
230
+ }
231
+ const validation = config.validation;
232
+ if (validation !== undefined && !isRecord(validation)) errors.push('validation must be an object');
233
+ const rawCommands = isRecord(validation) ? validation.commands : undefined;
234
+ const rawBootstrapCommands = isRecord(validation) ? validation.bootstrapCommands : undefined;
235
+ if (rawCommands !== undefined && !Array.isArray(rawCommands)) errors.push('validation.commands must be an array');
236
+ if (rawBootstrapCommands !== undefined && !Array.isArray(rawBootstrapCommands)) errors.push('validation.bootstrapCommands must be an array');
237
+ if (Array.isArray(rawBootstrapCommands)) {
238
+ rawBootstrapCommands.forEach((entry, index) => {
239
+ const normalized = normalizeMeshCommandConfig(entry, `${source}:validation.bootstrapCommands[${index}]`);
240
+ if (normalized.command) bootstrapCommands.push(normalized.command);
241
+ if (normalized.rejected) rejectedCommands.push(normalized.rejected);
242
+ });
243
+ }
244
+ if (Array.isArray(rawCommands)) {
245
+ rawCommands.forEach((entry, index) => {
246
+ const normalized = normalizeMeshCommandConfig(entry, `${source}:validation.commands[${index}]`);
247
+ if (normalized.command) commands.push(normalized.command);
248
+ if (normalized.rejected) rejectedCommands.push(normalized.rejected);
249
+ });
250
+ }
251
+ if (rejectedCommands.length) errors.push('one or more validation commands are invalid');
252
+ return { valid: errors.length === 0, errors, bootstrapCommands, commands, rejectedCommands };
253
+ }
254
+
255
+ function parseConfigText(path: string, text: string): unknown {
256
+ if (/\.json$/i.test(path)) return JSON.parse(text);
257
+ return yaml.load(text);
258
+ }
259
+
260
+ export function loadMeshRefineConfig(mesh: any, workspace: string): MeshRefineConfigLoadResult {
261
+ const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
262
+ const inline = mesh?.refineConfig || (policy as any).refineConfig || (policy as any).refine;
263
+ if (inline !== undefined) {
264
+ const validation = validateMeshRefineConfig(inline, 'mesh.policy.refineConfig');
265
+ if (!validation.valid) return { source: 'mesh.policy.refineConfig', sourceType: 'invalid', error: String(validation.rejectedCommands[0]?.reason || validation.errors.join('; ')) };
266
+ return { config: inline as RepoMeshRefineConfig, source: 'mesh.policy.refineConfig', sourceType: 'mesh_policy' };
267
+ }
268
+
269
+ for (const relative of MESH_REFINE_CONFIG_LOCATIONS) {
270
+ const configPath = join(workspace, relative);
271
+ if (!existsSync(configPath)) continue;
272
+ try {
273
+ const parsed = parseConfigText(configPath, readFileSync(configPath, 'utf-8'));
274
+ const validation = validateMeshRefineConfig(parsed, relative);
275
+ if (!validation.valid) return { source: relative, sourceType: 'invalid', path: configPath, error: String(validation.rejectedCommands[0]?.reason || validation.errors.join('; ')) };
276
+ return { config: parsed as RepoMeshRefineConfig, source: relative, sourceType: 'repo_file', path: configPath };
277
+ } catch (error: any) {
278
+ return { source: relative, sourceType: 'invalid', path: configPath, error: error?.message || String(error) };
279
+ }
280
+ }
281
+
282
+ return {
283
+ source: 'unavailable',
284
+ sourceType: 'unavailable',
285
+ error: `No repo mesh/refine config found. Checked: ${MESH_REFINE_CONFIG_LOCATIONS.join(', ')}`,
286
+ };
287
+ }
288
+
289
+ function readPackageScripts(workspace: string): Record<string, string> {
290
+ try {
291
+ const parsed = JSON.parse(readFileSync(join(workspace, 'package.json'), 'utf-8'));
292
+ return isRecord(parsed?.scripts) ? parsed.scripts as Record<string, string> : {};
293
+ } catch {
294
+ return {};
295
+ }
296
+ }
297
+
298
+ function collectProjectContextSuggestions(mesh: any): RepoMeshRefineValidationCommandConfig[] {
299
+ const commands = mesh?.projectContext?.commands;
300
+ if (!isRecord(commands)) return [];
301
+ const suggestions: RepoMeshRefineValidationCommandConfig[] = [];
302
+ for (const category of MESH_REFINE_VALIDATION_CATEGORIES) {
303
+ const entries = Array.isArray(commands[category]) ? commands[category] : [];
304
+ for (const entry of entries) {
305
+ if (isRecord(entry) && typeof entry.command === 'string') suggestions.push({ command: entry.command, category });
306
+ }
307
+ }
308
+ return suggestions;
309
+ }
310
+
311
+ function collectPackageScriptSuggestions(workspace: string): RepoMeshRefineValidationCommandConfig[] {
312
+ const scripts = readPackageScripts(workspace);
313
+ const suggestions: RepoMeshRefineValidationCommandConfig[] = [];
314
+ for (const category of MESH_REFINE_VALIDATION_CATEGORIES) {
315
+ for (const scriptName of Object.keys(scripts)) {
316
+ if (scriptName === category || scriptName.startsWith(`${category}:`)) {
317
+ suggestions.push({ command: 'npm', args: ['run', scriptName], category });
318
+ }
319
+ }
320
+ }
321
+ return suggestions;
322
+ }
323
+
324
+ export function suggestMeshRefineConfig(mesh: any, workspace: string): { suggestions: RepoMeshRefineValidationCommandConfig[]; suggestedConfig?: RepoMeshRefineConfig } {
325
+ const seen = new Set<string>();
326
+ const suggestions: RepoMeshRefineValidationCommandConfig[] = [];
327
+ for (const entry of [...collectProjectContextSuggestions(mesh), ...collectPackageScriptSuggestions(workspace)]) {
328
+ const key = `${entry.command} ${(entry.args || []).join(' ')}`.trim();
329
+ if (seen.has(key)) continue;
330
+ seen.add(key);
331
+ suggestions.push(entry);
332
+ }
333
+ return {
334
+ suggestions,
335
+ suggestedConfig: suggestions.length ? { version: 1, validation: { required: true, commands: suggestions.slice(0, 4) } } : undefined,
336
+ };
337
+ }
338
+
339
+ export function resolveMeshRefineValidationPlan(mesh: any, workspace: string): MeshRefineValidationPlan {
340
+ const loaded = loadMeshRefineConfig(mesh, workspace);
341
+ const suggestion = suggestMeshRefineConfig(mesh, workspace);
342
+ if (!loaded.config) {
343
+ return {
344
+ source: loaded.source,
345
+ sourceType: loaded.sourceType,
346
+ bootstrapCommands: [],
347
+ commands: [],
348
+ rejectedCommands: loaded.error ? [{ source: loaded.source, reason: loaded.error }] : [],
349
+ suggestions: suggestion.suggestions,
350
+ suggestedConfig: suggestion.suggestedConfig,
351
+ unavailableReason: loaded.error || 'validation_unavailable: repo mesh/refine config missing',
352
+ };
353
+ }
354
+
355
+ const validation = validateMeshRefineConfig(loaded.config, loaded.source);
356
+ return {
357
+ source: loaded.path || loaded.source,
358
+ sourceType: loaded.sourceType,
359
+ bootstrapCommands: validation.bootstrapCommands,
360
+ commands: validation.commands,
361
+ rejectedCommands: validation.rejectedCommands,
362
+ suggestions: suggestion.suggestions,
363
+ suggestedConfig: suggestion.suggestedConfig,
364
+ unavailableReason: validation.commands.length ? undefined : 'validation_unavailable: repo mesh/refine config has no validation.commands',
365
+ };
366
+ }