@moda-ai/cli 1.22.1 → 1.23.0

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 (118) hide show
  1. package/dist/cli-c4se7a5d.js +762 -0
  2. package/dist/cli-mnqa4rp6.js +1563 -0
  3. package/dist/cli-stmjdgvy.js +2842 -0
  4. package/dist/cli-y55bgg14.js +6980 -0
  5. package/dist/cli-ysegdrm5.js +286 -0
  6. package/dist/cli.js +4779 -1159
  7. package/dist/harness-github-actions-7t3cncnt.js +95 -0
  8. package/dist/index-4ser01zr.js +2769 -0
  9. package/dist/provision-hm1ppwj6.js +74 -0
  10. package/package.json +3 -2
  11. package/skills/integration/index.json +2 -2
  12. package/dist/agent-schemas.d.ts +0 -3
  13. package/dist/agent-schemas.js +0 -358
  14. package/dist/agent.d.ts +0 -5
  15. package/dist/agent.js +0 -477
  16. package/dist/analyst-tui.d.ts +0 -81
  17. package/dist/analyst-tui.js +0 -314
  18. package/dist/api-client.d.ts +0 -46
  19. package/dist/api-client.js +0 -120
  20. package/dist/ask-stream.d.ts +0 -146
  21. package/dist/ask-stream.js +0 -473
  22. package/dist/auth-storage.d.ts +0 -35
  23. package/dist/auth-storage.js +0 -159
  24. package/dist/auth.d.ts +0 -50
  25. package/dist/auth.js +0 -694
  26. package/dist/cli-session.d.ts +0 -26
  27. package/dist/cli-session.js +0 -169
  28. package/dist/cli.d.ts +0 -44
  29. package/dist/command-context.d.ts +0 -62
  30. package/dist/command-context.js +0 -57
  31. package/dist/command-registry.d.ts +0 -22
  32. package/dist/command-registry.js +0 -15
  33. package/dist/config.d.ts +0 -60
  34. package/dist/config.js +0 -110
  35. package/dist/doctor.d.ts +0 -79
  36. package/dist/doctor.js +0 -1182
  37. package/dist/errors.d.ts +0 -50
  38. package/dist/errors.js +0 -64
  39. package/dist/exit-funnel.d.ts +0 -14
  40. package/dist/exit-funnel.js +0 -53
  41. package/dist/harness-artifact-bodies.d.ts +0 -28
  42. package/dist/harness-artifact-bodies.js +0 -276
  43. package/dist/harness-candidates.d.ts +0 -77
  44. package/dist/harness-candidates.js +0 -482
  45. package/dist/harness-remote.d.ts +0 -135
  46. package/dist/harness-remote.js +0 -540
  47. package/dist/harness-report.d.ts +0 -145
  48. package/dist/harness-report.js +0 -1579
  49. package/dist/harness-sync-limits.d.ts +0 -8
  50. package/dist/harness-sync-limits.js +0 -21
  51. package/dist/harness.d.ts +0 -463
  52. package/dist/harness.js +0 -4009
  53. package/dist/init/index.d.ts +0 -78
  54. package/dist/init/index.js +0 -1442
  55. package/dist/init/mcp.d.ts +0 -2
  56. package/dist/init/mcp.js +0 -27
  57. package/dist/init/progress-board.d.ts +0 -80
  58. package/dist/init/progress-board.js +0 -225
  59. package/dist/init/rules.d.ts +0 -6
  60. package/dist/init/rules.js +0 -171
  61. package/dist/init/sdk-catalog.d.ts +0 -10
  62. package/dist/init/sdk-catalog.js +0 -80
  63. package/dist/init/sdk-guide.d.ts +0 -19
  64. package/dist/init/sdk-guide.js +0 -185
  65. package/dist/init/sdk-integration.d.ts +0 -53
  66. package/dist/init/sdk-integration.js +0 -835
  67. package/dist/init/sdk-protocol.d.ts +0 -135
  68. package/dist/init/sdk-protocol.js +0 -152
  69. package/dist/init/sdk-verification.d.ts +0 -27
  70. package/dist/init/sdk-verification.js +0 -123
  71. package/dist/init/tenant.d.ts +0 -67
  72. package/dist/init/tenant.js +0 -421
  73. package/dist/intelligence-schema.d.ts +0 -349
  74. package/dist/intelligence-schema.js +0 -174
  75. package/dist/keys.d.ts +0 -47
  76. package/dist/keys.js +0 -136
  77. package/dist/manifest.d.ts +0 -5
  78. package/dist/manifest.js +0 -114
  79. package/dist/markdown-terminal.d.ts +0 -49
  80. package/dist/markdown-terminal.js +0 -212
  81. package/dist/output.d.ts +0 -191
  82. package/dist/output.js +0 -624
  83. package/dist/production-intelligence.d.ts +0 -175
  84. package/dist/production-intelligence.js +0 -1430
  85. package/dist/profile-commands.d.ts +0 -10
  86. package/dist/profile-commands.js +0 -342
  87. package/dist/profiles.d.ts +0 -112
  88. package/dist/profiles.js +0 -573
  89. package/dist/prompts-ab.d.ts +0 -71
  90. package/dist/prompts-ab.js +0 -313
  91. package/dist/prompts-propose.d.ts +0 -15
  92. package/dist/prompts-propose.js +0 -212
  93. package/dist/prompts.d.ts +0 -58
  94. package/dist/prompts.js +0 -418
  95. package/dist/provision.d.ts +0 -28
  96. package/dist/provision.js +0 -104
  97. package/dist/redact.d.ts +0 -2
  98. package/dist/redact.js +0 -90
  99. package/dist/schemas.d.ts +0 -225
  100. package/dist/schemas.js +0 -126
  101. package/dist/skills-bundled.d.ts +0 -35
  102. package/dist/skills-bundled.js +0 -157
  103. package/dist/skills-manifest.d.ts +0 -33
  104. package/dist/skills-manifest.js +0 -244
  105. package/dist/skills.d.ts +0 -57
  106. package/dist/skills.js +0 -783
  107. package/dist/sync.d.ts +0 -5
  108. package/dist/sync.js +0 -36
  109. package/dist/telemetry.d.ts +0 -56
  110. package/dist/telemetry.js +0 -167
  111. package/dist/transforms.d.ts +0 -25
  112. package/dist/transforms.js +0 -102
  113. package/dist/tui.d.ts +0 -48
  114. package/dist/tui.js +0 -393
  115. package/dist/update-check.d.ts +0 -31
  116. package/dist/update-check.js +0 -141
  117. package/dist/workspace.d.ts +0 -12
  118. package/dist/workspace.js +0 -1294
package/dist/doctor.js DELETED
@@ -1,1182 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { spawnSync } from 'node:child_process';
3
- import { existsSync, readFileSync, statSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { callDataAPI } from './api-client.js';
6
- import { isAuthSessionValid, loadAuthSession } from './auth.js';
7
- import { resolveApiKey, resolveIngestUrl, resolveModaBaseUrl, resolveTenantId } from './config.js';
8
- import { readHarnessSyncState, scanHarness } from './harness.js';
9
- import { HARNESS_REPORT_APPROVAL_PATH, HARNESS_REPORT_APPROVAL_SCHEMA_VERSION, HARNESS_REPORT_PATH, hashHarnessReport, readHarnessReport, validateHarnessReport, } from './harness-report.js';
10
- import { ensureSecretFilePermissions, getActiveProfile, getModaPaths, isLoopbackBaseUrl, loadProfileConfig, resolveSecretReference, secretFilePathFromRef, } from './profiles.js';
11
- import { renderDoctorHuman, renderStatusHuman } from './tui.js';
12
- export async function runDoctorCommand(context) {
13
- const report = await buildDoctorReport(context, {
14
- offline: context.flags.offline === 'true',
15
- });
16
- if (context.outputMode === 'human') {
17
- if (!context.quiet)
18
- console.log(renderDoctorHuman(report));
19
- return { exitCode: 0 };
20
- }
21
- context.output.writeData(report, {
22
- status: report.summary.fail > 0 ? 'error' : 'success',
23
- ...doctorAgentWriteOptions(report, 'doctor'),
24
- });
25
- return { exitCode: 0 };
26
- }
27
- export async function runStatusCommand(context) {
28
- const report = await buildDoctorReport(context, {
29
- offline: context.flags.offline === 'true' || context.flags.online !== 'true',
30
- });
31
- const topBroken = firstBrokenCheck(report.checks);
32
- const statusPayload = {
33
- schemaVersion: 'status.v0.1',
34
- generatedAt: report.generatedAt,
35
- status: report.summary.status,
36
- activeProfile: report.profile.active,
37
- auth: {
38
- apiKey: resolveApiKey() ? 'configured' : 'missing',
39
- },
40
- workspace: {
41
- tenantId: resolveTenantId() ?? null,
42
- baseUrl: resolveModaBaseUrl(),
43
- ingestUrl: resolveIngestUrl(),
44
- },
45
- dataFlow: checkStatus(report.checks, 'cloud_data_flow'),
46
- harness: checkStatus(report.checks, 'harness_manifest_current'),
47
- harnessSync: checkStatus(report.checks, 'harness_sync_state_present'),
48
- harnessReport: checkStatus(report.checks, 'harness_report_approved_synced'),
49
- shadowSpark: checkStatus(report.checks, 'shadow_spark_workspace_health'),
50
- prompts: checkStatus(report.checks, 'prompts_discovered'),
51
- skills: checkStatus(report.checks, 'skills_discovered'),
52
- topBrokenCheck: topBroken
53
- ? {
54
- id: topBroken.id,
55
- status: topBroken.status,
56
- severity: topBroken.severity,
57
- nextCommand: topBroken.nextCommand,
58
- }
59
- : null,
60
- nextCommands: report.nextCommands.slice(0, 3),
61
- };
62
- if (context.outputMode === 'human') {
63
- if (!context.quiet)
64
- console.log(renderStatusHuman(statusPayload));
65
- return { exitCode: 0 };
66
- }
67
- context.output.writeData(statusPayload, {
68
- status: report.summary.fail > 0 ? 'error' : 'success',
69
- ...doctorAgentWriteOptions(report, 'status'),
70
- });
71
- return { exitCode: 0 };
72
- }
73
- export async function buildDoctorReport(context, options) {
74
- const checks = [];
75
- const profileConfig = loadProfileConfig({ env: context.env });
76
- const activeProfile = getActiveProfile({ profile: context.profile, env: context.env });
77
- const paths = getModaPaths({ env: context.env });
78
- const profileOptions = { profile: context.profile, env: context.env };
79
- const apiKey = resolveApiKey(profileOptions);
80
- const tenantId = resolveTenantId(profileOptions);
81
- const baseUrl = resolveModaBaseUrl(profileOptions);
82
- const ingestUrl = resolveIngestUrl(profileOptions);
83
- checks.push({
84
- id: 'active_profile_resolves',
85
- group: 'config',
86
- title: 'Active profile resolves',
87
- status: activeProfile ? 'pass' : 'fail',
88
- severity: activeProfile ? 'info' : 'critical',
89
- evidence: [
90
- { label: 'active_profile', value: profileConfig.active_profile },
91
- { label: 'environment', value: activeProfile.environment },
92
- ],
93
- remediation: activeProfile ? undefined : 'Create or select a Moda profile.',
94
- nextCommand: activeProfile ? undefined : 'moda profiles list',
95
- });
96
- checks.push({
97
- id: 'api_key_resolves',
98
- group: 'config',
99
- title: 'API key resolves',
100
- status: apiKey ? 'pass' : 'fail',
101
- severity: apiKey ? 'info' : 'critical',
102
- evidence: [{ label: 'source', value: apiKey ? 'environment/profile/config' : 'none' }],
103
- remediation: apiKey ? undefined : 'Authenticate and create a tenant API key.',
104
- nextCommand: apiKey ? undefined : 'moda init',
105
- });
106
- checks.push({
107
- id: 'tenant_resolves',
108
- group: 'config',
109
- title: 'Tenant/workspace resolves',
110
- status: tenantId ? 'pass' : 'warning',
111
- severity: tenantId ? 'info' : 'warning',
112
- evidence: [{ label: 'tenant_id', value: tenantId ? 'configured' : 'missing' }],
113
- remediation: tenantId ? undefined : 'Select a tenant during init or set MODA_TENANT_ID.',
114
- nextCommand: tenantId ? undefined : 'moda init',
115
- });
116
- checks.push(authTokenCheck(context, Boolean(apiKey)));
117
- checks.push(configPermissionCheck(paths.legacyConfigPath));
118
- if (activeProfile.api_key_ref) {
119
- const secretPath = secretFilePathFromRef(activeProfile.api_key_ref);
120
- if (secretPath)
121
- checks.push(secretPermissionCheck(secretPath));
122
- checks.push({
123
- id: 'profile_api_key_ref_resolves',
124
- group: 'config',
125
- title: 'Profile API key reference resolves',
126
- status: resolveSecretReference(activeProfile.api_key_ref) ? 'pass' : 'warning',
127
- severity: 'warning',
128
- evidence: [{ label: 'api_key_ref', value: activeProfile.api_key_ref }],
129
- remediation: 'Create or update the profile API key reference.',
130
- nextCommand: 'moda profiles create',
131
- });
132
- }
133
- checks.push(urlCheck('base_url_valid', 'config', 'Base URL is valid', baseUrl));
134
- checks.push(urlCheck('ingest_url_valid', 'config', 'Ingest URL is valid', ingestUrl));
135
- checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
136
- const manifestPath = join(context.cwd, '.moda', 'harness.json');
137
- const manifestExists = existsSync(manifestPath);
138
- checks.push({
139
- id: 'harness_manifest_exists',
140
- group: 'harness',
141
- title: 'Harness manifest exists',
142
- status: manifestExists ? 'pass' : 'warning',
143
- severity: manifestExists ? 'info' : 'warning',
144
- evidence: [{ label: 'path', value: '.moda/harness.json' }],
145
- remediation: manifestExists ? undefined : 'Run `moda harness scan --summary` to scan the project and write a harness manifest.',
146
- nextCommand: manifestExists ? undefined : 'moda harness scan --summary',
147
- });
148
- checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
149
- checks.push(harnessSyncStateCheck(context.cwd));
150
- checks.push(harnessReportStateCheck(context.cwd));
151
- checks.push(discoveryCheck('prompts_discovered', 'harness', 'Prompts are discovered', context.cwd, [
152
- 'prompts',
153
- '.moda/prompts.yml',
154
- ], 'moda prompts init'));
155
- checks.push(discoveryCheck('skills_discovered', 'harness', 'Skills are discovered', context.cwd, [
156
- 'skills',
157
- '.claude/skills',
158
- '.cursor/rules',
159
- ], 'moda skills pull'));
160
- checks.push(discoveryCheck('evals_discovered', 'harness', 'Evals/tests are discovered', context.cwd, [
161
- 'evals',
162
- 'tests',
163
- '__tests__',
164
- ]));
165
- checks.push(sdkInitializationCheck(context.cwd));
166
- checks.push(validationEventPathCheck(ingestUrl));
167
- if (options.offline || !apiKey) {
168
- checks.push(...offlineCloudChecks(options.offline));
169
- }
170
- else {
171
- checks.push(...await cloudDataFlowChecks(ingestUrl, apiKey));
172
- }
173
- const nextCommands = nextCommandsForChecks(checks);
174
- const summary = summarizeChecks(checks);
175
- return {
176
- schemaVersion: 'doctor.v0.1',
177
- generatedAt: new Date().toISOString(),
178
- profile: {
179
- active: activeProfile.name,
180
- requested: context.profile,
181
- },
182
- summary,
183
- checks,
184
- nextCommands,
185
- fixPlan: checks
186
- .filter((check) => check.status === 'fail' || check.status === 'warning')
187
- .map((check) => ({
188
- checkId: check.id,
189
- command: check.nextCommand,
190
- action: check.remediation ?? 'Review this diagnostic.',
191
- })),
192
- };
193
- }
194
- function authTokenCheck(context, hasApiKey) {
195
- const session = loadAuthSession({
196
- profile: context.profile,
197
- env: context.env,
198
- includeExpired: true,
199
- });
200
- const valid = isAuthSessionValid(session);
201
- if (!valid && hasApiKey) {
202
- return {
203
- id: 'auth_token_valid',
204
- group: 'config',
205
- title: 'Browser auth token is valid',
206
- status: 'skipped',
207
- severity: 'info',
208
- evidence: [{ label: 'reason', value: 'api key configured' }],
209
- };
210
- }
211
- return {
212
- id: 'auth_token_valid',
213
- group: 'config',
214
- title: 'Browser auth token is valid',
215
- status: valid ? 'pass' : 'warning',
216
- severity: valid ? 'info' : 'warning',
217
- evidence: [
218
- { label: 'token', value: session ? 'present' : 'missing' },
219
- ...(session ? [{ label: 'expires_at', value: new Date(session.expires_at * 1000).toISOString() }] : []),
220
- ],
221
- remediation: valid ? undefined : 'Refresh browser authentication.',
222
- nextCommand: valid ? undefined : 'moda init',
223
- };
224
- }
225
- function configPermissionCheck(path) {
226
- if (!existsSync(path)) {
227
- return {
228
- id: 'config_file_permissions',
229
- group: 'config',
230
- title: 'Config file permissions',
231
- status: 'skipped',
232
- severity: 'info',
233
- evidence: [{ label: 'path', value: path }],
234
- };
235
- }
236
- const permission = ensureSecretFilePermissions(path);
237
- return {
238
- id: 'config_file_permissions',
239
- group: 'config',
240
- title: 'Config file permissions',
241
- status: permission.secure ? 'pass' : 'fail',
242
- severity: permission.secure ? 'info' : 'critical',
243
- evidence: [
244
- { label: 'path', value: path },
245
- { label: 'mode', value: permission.mode ?? 'unknown' },
246
- ],
247
- remediation: permission.secure ? undefined : 'Restrict config file permissions to the current user.',
248
- };
249
- }
250
- function secretPermissionCheck(path) {
251
- const permission = ensureSecretFilePermissions(path);
252
- return {
253
- id: 'secret_file_permissions',
254
- group: 'config',
255
- title: 'Secret file permissions',
256
- status: permission.secure ? 'pass' : 'fail',
257
- severity: permission.secure ? 'info' : 'critical',
258
- evidence: [
259
- { label: 'path', value: path },
260
- { label: 'mode', value: permission.mode ?? 'unknown' },
261
- ],
262
- remediation: permission.secure ? undefined : 'Restrict secret file permissions to the current user.',
263
- };
264
- }
265
- function urlCheck(id, group, title, value) {
266
- let valid = false;
267
- try {
268
- const parsed = new URL(value);
269
- valid = parsed.protocol === 'http:' || parsed.protocol === 'https:';
270
- }
271
- catch {
272
- valid = false;
273
- }
274
- return {
275
- id,
276
- group,
277
- title,
278
- status: valid ? 'pass' : 'fail',
279
- severity: valid ? 'info' : 'critical',
280
- evidence: [{ label: 'url', value }],
281
- remediation: valid ? undefined : 'Configure a valid http(s) URL.',
282
- nextCommand: valid ? undefined : 'moda profiles create',
283
- };
284
- }
285
- /**
286
- * Local stacks mount `/auth/cli` and `/api/*` CLI/control proxies on the
287
- * frontend (`:3000` by default). Pointing `base_url` at the backend (`:3001`)
288
- * breaks browser login even though some Nest routes still answer there.
289
- */
290
- function localBaseUrlCheck(environment, baseUrl) {
291
- // Only the `local` profile targets a loopback worktree stack. `dev` defaults to
292
- // remote `https://moda.dev`, so including it here warns valid remote configs.
293
- if (environment !== 'local') {
294
- return {
295
- id: 'local_base_url_frontend',
296
- group: 'config',
297
- title: 'Local base URL targets the frontend',
298
- status: 'skipped',
299
- severity: 'info',
300
- evidence: [{ label: 'environment', value: environment }],
301
- };
302
- }
303
- let port = null;
304
- let loopback = false;
305
- try {
306
- const parsed = new URL(baseUrl);
307
- loopback = isLoopbackBaseUrl(baseUrl);
308
- port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80');
309
- }
310
- catch {
311
- return {
312
- id: 'local_base_url_frontend',
313
- group: 'config',
314
- title: 'Local base URL targets the frontend',
315
- status: 'fail',
316
- severity: 'critical',
317
- evidence: [{ label: 'base_url', value: baseUrl }],
318
- remediation: 'Set the local profile base URL to the frontend origin (http://localhost:3000).',
319
- nextCommand: 'moda profiles create local --environment local --base-url http://localhost:3000',
320
- };
321
- }
322
- const looksLikeBackend = loopback && (port === '3001' || port === '3002');
323
- if (looksLikeBackend) {
324
- return {
325
- id: 'local_base_url_frontend',
326
- group: 'config',
327
- title: 'Local base URL targets the frontend',
328
- status: 'warning',
329
- severity: 'warning',
330
- evidence: [
331
- { label: 'base_url', value: baseUrl },
332
- { label: 'port', value: port ?? 'unknown' },
333
- ],
334
- remediation: 'Local CLI auth and control APIs are served by the frontend. ' +
335
- 'Use http://localhost:3000 (or your worktree frontend port), not the backend :3001.',
336
- nextCommand: 'moda profiles create local --environment local --base-url http://localhost:3000 --use',
337
- };
338
- }
339
- if (!loopback) {
340
- return {
341
- id: 'local_base_url_frontend',
342
- group: 'config',
343
- title: 'Local base URL targets the frontend',
344
- status: 'warning',
345
- severity: 'warning',
346
- evidence: [{ label: 'base_url', value: baseUrl }],
347
- remediation: 'Local profile should use a loopback frontend URL (http://localhost:3000). ' +
348
- 'Activate with `moda profiles use local` or set MODA_PROFILE=local.',
349
- nextCommand: 'moda profiles use local',
350
- };
351
- }
352
- return {
353
- id: 'local_base_url_frontend',
354
- group: 'config',
355
- title: 'Local base URL targets the frontend',
356
- status: 'pass',
357
- severity: 'info',
358
- evidence: [{ label: 'base_url', value: baseUrl }],
359
- };
360
- }
361
- function harnessCurrentCheck(cwd, manifestPath, manifestExists, tenantId) {
362
- if (!manifestExists) {
363
- return {
364
- id: 'harness_manifest_current',
365
- group: 'harness',
366
- title: 'Harness manifest matches scanner output',
367
- status: 'skipped',
368
- severity: 'info',
369
- evidence: [{ label: 'reason', value: 'manifest missing' }],
370
- nextCommand: 'moda harness scan --summary',
371
- };
372
- }
373
- try {
374
- const current = scanHarness({
375
- rootDir: cwd,
376
- scannerVersion: 'doctor',
377
- tenantId,
378
- tenantSource: tenantId ? 'env_or_config' : undefined,
379
- });
380
- const saved = JSON.parse(readFileSync(manifestPath, 'utf8'));
381
- const savedHash = stableGraphHash(saved);
382
- const currentHash = stableGraphHash(current);
383
- const matches = savedHash === currentHash;
384
- return {
385
- id: 'harness_manifest_current',
386
- group: 'harness',
387
- title: 'Harness manifest matches scanner output',
388
- status: matches ? 'pass' : 'warning',
389
- severity: matches ? 'info' : 'warning',
390
- evidence: [
391
- { label: 'saved_hash', value: savedHash.slice(0, 12) },
392
- { label: 'current_hash', value: currentHash.slice(0, 12) },
393
- ...(matches ? [] : [{ label: 'diff', value: 'Manifest hash differs from fresh scanner output.' }]),
394
- ],
395
- remediation: matches ? undefined : 'Refresh the local harness manifest.',
396
- nextCommand: matches ? undefined : 'moda harness scan --summary',
397
- };
398
- }
399
- catch (err) {
400
- return {
401
- id: 'harness_manifest_current',
402
- group: 'harness',
403
- title: 'Harness manifest matches scanner output',
404
- status: 'fail',
405
- severity: 'critical',
406
- evidence: [{ label: 'error', value: err instanceof Error ? err.message : String(err) }],
407
- remediation: 'Regenerate the harness manifest.',
408
- nextCommand: 'moda harness scan --summary',
409
- };
410
- }
411
- }
412
- function harnessSyncStateCheck(cwd) {
413
- const state = readHarnessSyncState(cwd);
414
- if (!state) {
415
- return {
416
- id: 'harness_sync_state_present',
417
- group: 'harness',
418
- title: 'Harness sync state exists',
419
- status: 'warning',
420
- severity: 'warning',
421
- evidence: [{ label: 'path', value: '.moda/sync-state.json' }],
422
- remediation: 'Sync the harness graph so Moda can correlate local state with cloud state.',
423
- nextCommand: 'moda harness sync --dry-run',
424
- };
425
- }
426
- return {
427
- id: 'harness_sync_state_present',
428
- group: 'harness',
429
- title: 'Harness sync state exists',
430
- status: 'pass',
431
- severity: 'info',
432
- evidence: [
433
- { label: 'updated_at', value: state.updatedAt },
434
- { label: 'profile', value: state.profile },
435
- { label: 'harness_id', value: state.harnessId },
436
- { label: 'remote_version_id', value: state.remote.versionId || 'none' },
437
- ],
438
- };
439
- }
440
- function harnessReportStateCheck(cwd) {
441
- const reportPath = join(cwd, HARNESS_REPORT_PATH);
442
- if (!existsSync(reportPath)) {
443
- return {
444
- id: 'harness_report_approved_synced',
445
- group: 'harness',
446
- title: 'Harness analyst report is approved and synced',
447
- status: 'warning',
448
- severity: 'warning',
449
- evidence: [{ label: 'path', value: HARNESS_REPORT_PATH }],
450
- remediation: 'Run a Moda Analyst pass so the harness graph has cited evidence before cloud sync.',
451
- nextCommand: 'moda harness analyze --summary',
452
- };
453
- }
454
- try {
455
- const report = readHarnessReport(reportPath);
456
- const validation = validateHarnessReport(report, cwd);
457
- const reportHash = hashHarnessReport(report);
458
- const evidence = [
459
- { label: 'path', value: HARNESS_REPORT_PATH },
460
- { label: 'report_hash', value: reportHash.slice(0, 12) },
461
- { label: 'validation', value: validation.status },
462
- ];
463
- if (validation.status !== 'pass') {
464
- return {
465
- id: 'harness_report_approved_synced',
466
- group: 'harness',
467
- title: 'Harness analyst report is approved and synced',
468
- status: 'fail',
469
- severity: 'critical',
470
- evidence: [
471
- ...evidence,
472
- { label: 'errors', value: validation.errors.slice(0, 3).join(' | ') || 'validation failed' },
473
- ],
474
- remediation: 'Regenerate or fix the harness report before approving or syncing.',
475
- nextCommand: 'moda harness analyze --summary',
476
- };
477
- }
478
- const approval = readHarnessReportApproval(cwd);
479
- const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION &&
480
- approval.reportId === report.reportId &&
481
- approval.reportHash === reportHash &&
482
- approval.approved === true;
483
- evidence.push({ label: 'approval', value: approved ? 'approved' : approval ? 'stale_or_invalid' : 'missing' });
484
- if (approval && !approved) {
485
- return {
486
- id: 'harness_report_approved_synced',
487
- group: 'harness',
488
- title: 'Harness analyst report is approved and synced',
489
- status: 'fail',
490
- severity: 'critical',
491
- evidence,
492
- remediation: 'Approval no longer matches the current report. Review and approve the report again.',
493
- nextCommand: 'moda harness approve --yes',
494
- };
495
- }
496
- if (!approved) {
497
- return {
498
- id: 'harness_report_approved_synced',
499
- group: 'harness',
500
- title: 'Harness analyst report is approved and synced',
501
- status: 'warning',
502
- severity: 'warning',
503
- evidence,
504
- remediation: 'Review and approve the validated harness report before syncing.',
505
- nextCommand: 'moda harness approve --yes',
506
- };
507
- }
508
- const state = readHarnessSyncState(cwd);
509
- const synced = state?.source === 'approved_report' && state.report?.reportHash === reportHash;
510
- evidence.push({ label: 'sync', value: synced ? 'synced' : 'not_synced_to_this_report' }, ...(state?.report?.reportHash ? [{ label: 'sync_report_hash', value: state.report.reportHash.slice(0, 12) }] : []));
511
- return {
512
- id: 'harness_report_approved_synced',
513
- group: 'harness',
514
- title: 'Harness analyst report is approved and synced',
515
- status: synced ? 'pass' : 'warning',
516
- severity: synced ? 'info' : 'warning',
517
- evidence,
518
- remediation: synced ? undefined : 'Sync the approved harness report so Moda cloud has the same harness graph.',
519
- nextCommand: synced ? undefined : 'moda harness sync --from-report .moda/harness-report.json',
520
- };
521
- }
522
- catch (err) {
523
- return {
524
- id: 'harness_report_approved_synced',
525
- group: 'harness',
526
- title: 'Harness analyst report is approved and synced',
527
- status: 'fail',
528
- severity: 'critical',
529
- evidence: [
530
- { label: 'path', value: HARNESS_REPORT_PATH },
531
- { label: 'error', value: err instanceof Error ? err.message : String(err) },
532
- ],
533
- remediation: 'Regenerate the harness report.',
534
- nextCommand: 'moda harness analyze --summary',
535
- };
536
- }
537
- }
538
- function readHarnessReportApproval(cwd) {
539
- const approvalPath = join(cwd, HARNESS_REPORT_APPROVAL_PATH);
540
- if (!existsSync(approvalPath))
541
- return null;
542
- try {
543
- return JSON.parse(readFileSync(approvalPath, 'utf8'));
544
- }
545
- catch {
546
- return {
547
- schemaVersion: HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
548
- reportId: 'unreadable',
549
- reportHash: 'unreadable',
550
- approved: true,
551
- approvedAt: '',
552
- approvedBy: 'local-human',
553
- reportPath: HARNESS_REPORT_PATH,
554
- };
555
- }
556
- }
557
- function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
558
- const found = relativePaths.filter((path) => existsSync(join(cwd, path)));
559
- return {
560
- id,
561
- group,
562
- title,
563
- status: found.length > 0 ? 'pass' : 'warning',
564
- severity: found.length > 0 ? 'info' : 'warning',
565
- evidence: [{ label: 'paths', value: found.length > 0 ? found.join(', ') : 'none' }],
566
- remediation: found.length > 0 ? undefined : `Expected one of: ${relativePaths.join(', ')}.`,
567
- nextCommand: found.length > 0 ? undefined : nextCommand,
568
- };
569
- }
570
- function sdkInitializationCheck(cwd) {
571
- const candidates = ['src', 'app', 'agent_harness'];
572
- let detected = false;
573
- for (const candidate of candidates) {
574
- const path = join(cwd, candidate);
575
- if (!existsSync(path))
576
- continue;
577
- try {
578
- const stat = statSync(path);
579
- detected ||= stat.isDirectory() || stat.isFile();
580
- }
581
- catch {
582
- // Ignore unreadable candidate paths; doctor reports higher-level gaps.
583
- }
584
- }
585
- return {
586
- id: 'sdk_initialization_detected',
587
- group: 'harness',
588
- title: 'SDK initialization is detectable',
589
- status: detected ? 'warning' : 'skipped',
590
- severity: 'warning',
591
- evidence: [{ label: 'status', value: detected ? 'code present, instrumentation not verified' : 'no source candidates' }],
592
- remediation: 'Use the generated integration guide to add Moda SDK instrumentation.',
593
- nextCommand: 'moda init',
594
- };
595
- }
596
- // Unused while coding-agent capture is disabled; kept for re-enablement.
597
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
598
- function shadowSparkWorkspaceCheck(cwd, env) {
599
- const repo = detectGitRepo(cwd);
600
- if (!repo) {
601
- return {
602
- id: 'shadow_spark_workspace_health',
603
- group: 'spark',
604
- title: 'Shadow Spark workspace health',
605
- status: 'skipped',
606
- severity: 'info',
607
- evidence: [{ label: 'reason', value: 'not inside a git repository' }],
608
- };
609
- }
610
- const workspaceId = createHash('sha256')
611
- .update(`${repo.toplevel}:${repo.originUrl || ''}`)
612
- .digest('hex')
613
- .slice(0, 16);
614
- const path = join(getModaPaths({ env }).home, '.moda', 'workspaces', workspaceId);
615
- const healthy = existsSync(join(path, '.git'));
616
- return {
617
- id: 'shadow_spark_workspace_health',
618
- group: 'spark',
619
- title: 'Shadow Spark workspace health',
620
- status: healthy ? 'pass' : 'warning',
621
- severity: healthy ? 'info' : 'warning',
622
- evidence: [
623
- { label: 'workspace_id', value: workspaceId },
624
- { label: 'path', value: path },
625
- { label: 'shadow_git', value: healthy ? 'present' : 'missing' },
626
- ],
627
- remediation: healthy ? undefined : 'Create a Shadow Spark snapshot.',
628
- nextCommand: healthy ? undefined : 'moda spark snapshot --json',
629
- };
630
- }
631
- // Unused while coding-agent capture is disabled; kept for re-enablement.
632
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
633
- function hookCheck(env) {
634
- const paths = getModaPaths({ env });
635
- const hookCandidates = [
636
- join(paths.home, '.claude', 'hooks', 'moda-snapshot.sh'),
637
- join(paths.home, '.codex', 'hooks.json'),
638
- join(paths.home, '.cursor', 'hooks.json'),
639
- ];
640
- const found = hookCandidates.filter((path) => existsSync(path));
641
- return {
642
- id: 'hooks_installed',
643
- group: 'hooks',
644
- title: 'Coding-agent hooks installed',
645
- status: found.length > 0 ? 'pass' : 'warning',
646
- severity: found.length > 0 ? 'info' : 'warning',
647
- evidence: [{ label: 'installed', value: found.length > 0 ? String(found.length) : 'none' }],
648
- remediation: found.length > 0 ? undefined : 'Install coding-agent trace hooks.',
649
- nextCommand: found.length > 0 ? undefined : 'moda hooks install --agent=all',
650
- };
651
- }
652
- // Unused while coding-agent capture is disabled; kept for re-enablement.
653
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
654
- function hookTelemetryChecks(env, ingestUrl) {
655
- const configs = agentTelemetryConfigs(env);
656
- return [
657
- hookFilesExecutableCheck(env),
658
- agentConfigJsonCheck(configs),
659
- agentConfigApiKeyCheck(configs),
660
- otlpEndpointCheck(configs, ingestUrl),
661
- agentConfigApiKeyStorageCheck(configs),
662
- ];
663
- }
664
- function agentTelemetryConfigs(env) {
665
- const paths = getModaPaths({ env });
666
- const configs = [
667
- {
668
- agent: 'claude',
669
- title: 'Claude Code',
670
- path: join(paths.home, '.claude', 'settings.json'),
671
- providerPath: 'claude',
672
- providerAttribute: 'claude_code',
673
- },
674
- {
675
- agent: 'codex',
676
- title: 'Codex',
677
- path: join(paths.home, '.codex', 'hooks.json'),
678
- providerPath: 'codex',
679
- providerAttribute: 'codex',
680
- },
681
- {
682
- agent: 'cursor',
683
- title: 'Cursor',
684
- path: join(paths.home, '.cursor', 'hooks.json'),
685
- providerPath: 'cursor',
686
- providerAttribute: 'cursor',
687
- },
688
- ];
689
- return configs.map((config) => {
690
- if (!existsSync(config.path))
691
- return { ...config, exists: false };
692
- try {
693
- const parsed = JSON.parse(readFileSync(config.path, 'utf8'));
694
- if (!isRecord(parsed)) {
695
- return { ...config, exists: true, error: 'JSON root is not an object.' };
696
- }
697
- return { ...config, exists: true, parsed };
698
- }
699
- catch (err) {
700
- return {
701
- ...config,
702
- exists: true,
703
- error: err instanceof Error ? err.message : String(err),
704
- };
705
- }
706
- });
707
- }
708
- function hookFilesExecutableCheck(env) {
709
- const paths = getModaPaths({ env });
710
- const candidates = [
711
- join(paths.home, '.claude', 'hooks', 'moda-snapshot.sh'),
712
- join(paths.home, '.moda', 'hooks', 'moda-snapshot.sh'),
713
- ];
714
- const found = candidates.filter((path) => existsSync(path));
715
- const notExecutable = found.filter((path) => {
716
- try {
717
- return (statSync(path).mode & 0o111) === 0;
718
- }
719
- catch {
720
- return true;
721
- }
722
- });
723
- return {
724
- id: 'hook_files_executable',
725
- group: 'hooks',
726
- title: 'Hook files are executable',
727
- status: found.length === 0 ? 'skipped' : notExecutable.length === 0 ? 'pass' : 'fail',
728
- severity: notExecutable.length === 0 ? 'info' : 'critical',
729
- evidence: [
730
- { label: 'found', value: found.length > 0 ? found.map(relativeHomePath(paths.home)).join(', ') : 'none' },
731
- ...(notExecutable.length > 0
732
- ? [{ label: 'not_executable', value: notExecutable.map(relativeHomePath(paths.home)).join(', ') }]
733
- : []),
734
- ],
735
- remediation: notExecutable.length > 0 ? 'Reinstall hooks so Moda can set executable permissions.' : undefined,
736
- nextCommand: notExecutable.length > 0 || found.length === 0 ? 'moda hooks install --agent=all' : undefined,
737
- };
738
- }
739
- function agentConfigJsonCheck(configs) {
740
- const existing = configs.filter((config) => config.exists);
741
- const invalid = existing.filter((config) => config.error);
742
- return {
743
- id: 'agent_config_json_valid',
744
- group: 'hooks',
745
- title: 'Agent config JSON is valid',
746
- status: existing.length === 0 ? 'skipped' : invalid.length === 0 ? 'pass' : 'fail',
747
- severity: invalid.length === 0 ? 'info' : 'critical',
748
- evidence: [
749
- { label: 'configs', value: existing.length > 0 ? existing.map((config) => config.agent).join(', ') : 'none' },
750
- ...(invalid.length > 0
751
- ? invalid.map((config) => ({ label: `${config.agent}_error`, value: config.error ?? 'invalid JSON' }))
752
- : []),
753
- ],
754
- remediation: invalid.length > 0 ? 'Fix invalid agent hook config JSON.' : undefined,
755
- nextCommand: invalid.length > 0 ? 'moda hooks install --agent=all' : undefined,
756
- };
757
- }
758
- function agentConfigApiKeyCheck(configs) {
759
- const parsed = configs.filter((config) => config.parsed);
760
- const missing = parsed.filter((config) => !telemetryHeader(config).replace(/^Authorization=Bearer\s*/, '').trim());
761
- return {
762
- id: 'agent_config_api_key_resolves',
763
- group: 'hooks',
764
- title: 'API key exists in agent telemetry config',
765
- status: parsed.length === 0 ? 'skipped' : missing.length === 0 ? 'pass' : 'warning',
766
- severity: missing.length === 0 ? 'info' : 'warning',
767
- evidence: [
768
- { label: 'configs', value: parsed.length > 0 ? parsed.map((config) => config.agent).join(', ') : 'none' },
769
- ...(missing.length > 0 ? [{ label: 'missing_or_empty', value: missing.map((config) => config.agent).join(', ') }] : []),
770
- ],
771
- remediation: missing.length > 0 ? 'Reinstall hooks after authenticating so telemetry has an API key.' : undefined,
772
- nextCommand: missing.length > 0 ? 'moda hooks install --agent=all' : undefined,
773
- };
774
- }
775
- function otlpEndpointCheck(configs, ingestUrl) {
776
- const parsed = configs.filter((config) => config.parsed);
777
- const expectedBase = ingestUrl.replace(/\/+$/, '');
778
- const mismatches = [];
779
- for (const config of parsed) {
780
- const env = telemetryEnv(config);
781
- const expected = {
782
- OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/logs`,
783
- OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/metrics`,
784
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: `${expectedBase}/v1/otel/${config.providerPath}/traces`,
785
- };
786
- for (const [key, value] of Object.entries(expected)) {
787
- if (env[key] !== value)
788
- mismatches.push(`${config.agent}:${key}`);
789
- }
790
- const attrs = String(env.OTEL_RESOURCE_ATTRIBUTES ?? '');
791
- if (!attrs.split(',').some((item) => item.trim() === `provider=${config.providerAttribute}`)) {
792
- mismatches.push(`${config.agent}:OTEL_RESOURCE_ATTRIBUTES`);
793
- }
794
- }
795
- return {
796
- id: 'otlp_endpoints_match_profile',
797
- group: 'hooks',
798
- title: 'OTLP endpoints match active ingest URL',
799
- status: parsed.length === 0 ? 'skipped' : mismatches.length === 0 ? 'pass' : 'warning',
800
- severity: mismatches.length === 0 ? 'info' : 'warning',
801
- evidence: [
802
- { label: 'ingest_url', value: expectedBase },
803
- { label: 'configs', value: parsed.length > 0 ? parsed.map((config) => config.agent).join(', ') : 'none' },
804
- ...(mismatches.length > 0 ? [{ label: 'mismatches', value: mismatches.slice(0, 8).join(', ') }] : []),
805
- ],
806
- remediation: mismatches.length > 0 ? 'Reinstall hooks so agent telemetry points at the active Moda ingest URL.' : undefined,
807
- nextCommand: mismatches.length > 0 ? 'moda hooks install --agent=all' : undefined,
808
- };
809
- }
810
- function agentConfigApiKeyStorageCheck(configs) {
811
- const withStoredKey = configs
812
- .filter((config) => config.parsed)
813
- .filter((config) => telemetryHeader(config).replace(/^Authorization=Bearer\s*/, '').trim().length > 0);
814
- return {
815
- id: 'agent_config_api_key_storage',
816
- group: 'hooks',
817
- title: 'API key storage in agent config',
818
- status: withStoredKey.length > 0 ? 'warning' : 'pass',
819
- severity: withStoredKey.length > 0 ? 'warning' : 'info',
820
- evidence: [{ label: 'configs_with_key', value: withStoredKey.length > 0 ? withStoredKey.map((config) => config.agent).join(', ') : 'none' }],
821
- remediation: withStoredKey.length > 0
822
- ? 'Agent configs contain OTLP auth headers; ensure file permissions are restricted and rotate keys if exposed.'
823
- : undefined,
824
- };
825
- }
826
- function validationEventPathCheck(ingestUrl) {
827
- return {
828
- id: 'validation_event_path_defined',
829
- group: 'cloud',
830
- title: 'Validation event path is defined',
831
- status: 'pass',
832
- severity: 'info',
833
- evidence: [{ label: 'path', value: `${ingestUrl.replace(/\/+$/, '')}/v1/otel/validation` }],
834
- };
835
- }
836
- function telemetryEnv(config) {
837
- return isRecord(config.parsed?.env) ? config.parsed.env : {};
838
- }
839
- function telemetryHeader(config) {
840
- const env = telemetryEnv(config);
841
- return typeof env.OTEL_EXPORTER_OTLP_HEADERS === 'string'
842
- ? env.OTEL_EXPORTER_OTLP_HEADERS
843
- : '';
844
- }
845
- function relativeHomePath(home) {
846
- return (path) => path.startsWith(home) ? `~/${path.slice(home.length).replace(/^\/+/, '')}` : path;
847
- }
848
- function offlineCloudChecks(isOffline) {
849
- const status = isOffline ? 'skipped' : 'warning';
850
- const evidence = [{ label: 'mode', value: isOffline ? 'offline' : 'missing api key' }];
851
- const remediation = isOffline
852
- ? 'Rerun without --offline to check Moda Cloud.'
853
- : 'Configure an API key before checking cloud data flow.';
854
- const nextCommand = isOffline ? 'moda doctor --online' : 'moda init';
855
- return [
856
- {
857
- id: 'cloud_api_reachable',
858
- group: 'cloud',
859
- title: 'Moda Cloud API is reachable',
860
- status,
861
- severity: 'warning',
862
- evidence,
863
- remediation,
864
- nextCommand,
865
- },
866
- {
867
- id: 'ingest_endpoint_reachable',
868
- group: 'cloud',
869
- title: 'Moda ingest endpoint is reachable',
870
- status,
871
- severity: 'warning',
872
- evidence,
873
- remediation,
874
- nextCommand,
875
- },
876
- {
877
- id: 'recent_data_flow',
878
- group: 'cloud',
879
- title: 'Recent traces/logs/runs exist',
880
- status,
881
- severity: 'warning',
882
- evidence,
883
- remediation,
884
- nextCommand,
885
- },
886
- {
887
- id: 'cloud_data_flow',
888
- group: 'cloud',
889
- title: 'Moda Cloud data flow',
890
- status,
891
- severity: 'warning',
892
- evidence,
893
- remediation,
894
- nextCommand,
895
- },
896
- ];
897
- }
898
- async function cloudDataFlowChecks(ingestUrl, apiKey) {
899
- let overview;
900
- let cloudApiCheck;
901
- try {
902
- overview = await callDataAPI('/overview?days_back=1');
903
- cloudApiCheck = {
904
- id: 'cloud_api_reachable',
905
- group: 'cloud',
906
- title: 'Moda Cloud API is reachable',
907
- status: 'pass',
908
- severity: 'info',
909
- evidence: [{ label: 'overview', value: 'reachable' }],
910
- };
911
- }
912
- catch (err) {
913
- const error = err instanceof Error ? err.message : String(err);
914
- return [
915
- {
916
- id: 'cloud_api_reachable',
917
- group: 'cloud',
918
- title: 'Moda Cloud API is reachable',
919
- status: 'warning',
920
- severity: 'warning',
921
- evidence: [{ label: 'error', value: error }],
922
- remediation: 'Check API key and base URL.',
923
- nextCommand: 'moda doctor --offline',
924
- },
925
- {
926
- id: 'ingest_endpoint_reachable',
927
- group: 'cloud',
928
- title: 'Moda ingest endpoint is reachable',
929
- status: 'skipped',
930
- severity: 'warning',
931
- evidence: [{ label: 'reason', value: 'cloud API unreachable' }],
932
- },
933
- {
934
- id: 'recent_data_flow',
935
- group: 'cloud',
936
- title: 'Recent traces/logs/runs exist',
937
- status: 'skipped',
938
- severity: 'warning',
939
- evidence: [{ label: 'reason', value: 'overview unavailable' }],
940
- },
941
- {
942
- id: 'cloud_data_flow',
943
- group: 'cloud',
944
- title: 'Moda Cloud data flow',
945
- status: 'warning',
946
- severity: 'warning',
947
- evidence: [{ label: 'error', value: error }],
948
- remediation: 'Check API key, base URL, and whether data has reached Moda.',
949
- nextCommand: 'moda doctor --offline',
950
- },
951
- ];
952
- }
953
- const ingestCheck = await ingestReachabilityCheck(ingestUrl, apiKey);
954
- const recentCheck = recentDataFlowCheck(overview);
955
- const cloudStatus = cloudApiCheck.status === 'pass' &&
956
- ingestCheck.status === 'pass' &&
957
- recentCheck.status === 'pass'
958
- ? 'pass'
959
- : 'warning';
960
- return [
961
- cloudApiCheck,
962
- ingestCheck,
963
- recentCheck,
964
- {
965
- id: 'cloud_data_flow',
966
- group: 'cloud',
967
- title: 'Moda Cloud data flow',
968
- status: cloudStatus,
969
- severity: cloudStatus === 'pass' ? 'info' : 'warning',
970
- evidence: [
971
- { label: 'cloud_api', value: cloudApiCheck.status },
972
- { label: 'ingest', value: ingestCheck.status },
973
- { label: 'recent_data', value: recentCheck.status },
974
- ],
975
- remediation: cloudStatus === 'pass' ? undefined : 'Check API key, ingest URL, and whether data has reached Moda.',
976
- nextCommand: cloudStatus === 'pass' ? undefined : 'moda doctor --offline',
977
- },
978
- ];
979
- }
980
- async function ingestReachabilityCheck(ingestUrl, apiKey) {
981
- const url = `${ingestUrl.replace(/\/+$/, '')}/`;
982
- try {
983
- const response = await fetch(url, {
984
- method: 'GET',
985
- headers: { Authorization: `Bearer ${apiKey}` },
986
- signal: AbortSignal.timeout(10_000),
987
- });
988
- return {
989
- id: 'ingest_endpoint_reachable',
990
- group: 'cloud',
991
- title: 'Moda ingest endpoint is reachable',
992
- status: 'pass',
993
- severity: 'info',
994
- evidence: [
995
- { label: 'url', value: url },
996
- { label: 'http_status', value: String(response.status) },
997
- ],
998
- };
999
- }
1000
- catch (err) {
1001
- return {
1002
- id: 'ingest_endpoint_reachable',
1003
- group: 'cloud',
1004
- title: 'Moda ingest endpoint is reachable',
1005
- status: 'warning',
1006
- severity: 'warning',
1007
- evidence: [{ label: 'error', value: err instanceof Error ? err.message : String(err) }],
1008
- remediation: 'Check the configured ingest URL and network reachability.',
1009
- nextCommand: 'moda profiles create',
1010
- };
1011
- }
1012
- }
1013
- function recentDataFlowCheck(overview) {
1014
- const data = isRecord(overview) ? overview : {};
1015
- const conversations = numberAt(data, ['conversations', 'total']);
1016
- const toolFailures = numberAt(data, ['tool_failures', 'total']);
1017
- const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : [];
1018
- const lastEventAt = lastActivityTimestamp(recentActivity);
1019
- const hasData = conversations > 0 || toolFailures > 0 || Boolean(lastEventAt);
1020
- return {
1021
- id: 'recent_data_flow',
1022
- group: 'cloud',
1023
- title: 'Recent traces/logs/runs exist',
1024
- status: hasData ? 'pass' : 'warning',
1025
- severity: hasData ? 'info' : 'warning',
1026
- evidence: [
1027
- { label: 'conversations', value: String(conversations) },
1028
- { label: 'tool_failures', value: String(toolFailures) },
1029
- { label: 'last_event_at', value: lastEventAt ?? 'none' },
1030
- ],
1031
- remediation: hasData ? undefined : 'No recent Moda data was visible in the overview window.',
1032
- nextCommand: hasData ? undefined : 'moda hooks install --agent=all',
1033
- };
1034
- }
1035
- function numberAt(value, path) {
1036
- let cursor = value;
1037
- for (const key of path) {
1038
- if (!isRecord(cursor))
1039
- return 0;
1040
- cursor = cursor[key];
1041
- }
1042
- return typeof cursor === 'number' && Number.isFinite(cursor) ? cursor : 0;
1043
- }
1044
- function lastActivityTimestamp(items) {
1045
- const timestamps = items
1046
- .map((item) => isRecord(item) && typeof item.timestamp === 'string' ? item.timestamp : undefined)
1047
- .filter((item) => Boolean(item))
1048
- .sort();
1049
- return timestamps.at(-1);
1050
- }
1051
- function summarizeChecks(checks) {
1052
- const fail = checks.filter((check) => check.status === 'fail').length;
1053
- const warning = checks.filter((check) => check.status === 'warning').length;
1054
- const skipped = checks.filter((check) => check.status === 'skipped').length;
1055
- const pass = checks.filter((check) => check.status === 'pass').length;
1056
- return {
1057
- status: fail > 0 ? 'fail' : warning > 0 ? 'warning' : 'pass',
1058
- pass,
1059
- warning,
1060
- fail,
1061
- skipped,
1062
- };
1063
- }
1064
- function nextCommandsForChecks(checks) {
1065
- return [...new Set(checks
1066
- .filter((check) => check.status === 'fail' || check.status === 'warning')
1067
- .map((check) => check.nextCommand)
1068
- .filter((command) => Boolean(command)))];
1069
- }
1070
- function firstBrokenCheck(checks) {
1071
- return checks.find((check) => check.status === 'fail') ??
1072
- checks.find((check) => check.status === 'warning');
1073
- }
1074
- function checkStatus(checks, id) {
1075
- return checks.find((check) => check.id === id)?.status ?? 'skipped';
1076
- }
1077
- function doctorAgentWriteOptions(report, command) {
1078
- const broken = report.checks.filter((check) => check.status === 'fail' || check.status === 'warning');
1079
- return {
1080
- summary: `${command} completed with status ${report.summary.status}: ${report.summary.fail} failing checks and ${report.summary.warning} warnings.`,
1081
- findings: broken.slice(0, 16).map((check) => ({
1082
- id: check.id,
1083
- title: check.title,
1084
- status: check.status,
1085
- severity: check.severity,
1086
- description: check.remediation ?? `Doctor check ${check.id} is ${check.status}.`,
1087
- evidence_ref_ids: [`doctor:${check.id}`],
1088
- })),
1089
- evidenceRefs: report.checks.map((check) => ({
1090
- id: `doctor:${check.id}`,
1091
- kind: 'doctor_check',
1092
- label: check.title,
1093
- status: check.status,
1094
- })),
1095
- recommendedActions: report.fixPlan.slice(0, 16).map((item, index) => ({
1096
- id: `doctor_action_${index + 1}_${item.checkId}`,
1097
- title: item.action,
1098
- command: item.command,
1099
- rationale: `Doctor check ${item.checkId} is not passing.`,
1100
- mutability: item.command ? agentMutabilityForCommand(item.command) : 'unknown',
1101
- requires_approval: item.command ? agentCommandRequiresApproval(item.command) : true,
1102
- })),
1103
- nextCommands: report.nextCommands.slice(0, 16).map((nextCommand) => ({
1104
- command: nextCommand,
1105
- purpose: agentPurposeForCommand(nextCommand),
1106
- mutability: agentMutabilityForCommand(nextCommand),
1107
- requires_approval: agentCommandRequiresApproval(nextCommand),
1108
- })),
1109
- warnings: broken
1110
- .filter((check) => check.status === 'warning')
1111
- .slice(0, 16)
1112
- .map((check) => `${check.title}: ${check.remediation ?? check.status}`),
1113
- };
1114
- }
1115
- function agentMutabilityForCommand(command) {
1116
- if (/\bsync\b/.test(command))
1117
- return 'sync';
1118
- if (/\b(init|install|approve|pull|gen|promote|remove)\b/.test(command))
1119
- return 'write';
1120
- return 'read';
1121
- }
1122
- function agentCommandRequiresApproval(command) {
1123
- return agentMutabilityForCommand(command) !== 'read' && !command.includes('--dry-run');
1124
- }
1125
- function agentPurposeForCommand(command) {
1126
- if (command.includes('doctor'))
1127
- return 'Re-run Moda diagnostics.';
1128
- if (command.includes('harness approve'))
1129
- return 'Approve the validated harness report after review.';
1130
- if (command.includes('harness sync'))
1131
- return 'Sync harness state to Moda Cloud.';
1132
- if (command.includes('harness scan'))
1133
- return 'Refresh the local harness manifest.';
1134
- if (command.includes('init'))
1135
- return 'Run Moda onboarding.';
1136
- return 'Inspect or repair Moda setup.';
1137
- }
1138
- function detectGitRepo(cwd) {
1139
- const toplevel = spawnSync('git', ['rev-parse', '--show-toplevel'], {
1140
- cwd,
1141
- encoding: 'utf8',
1142
- });
1143
- if (toplevel.status !== 0 || !toplevel.stdout.trim())
1144
- return undefined;
1145
- const root = toplevel.stdout.trim();
1146
- const origin = spawnSync('git', ['config', '--get', 'remote.origin.url'], {
1147
- cwd: root,
1148
- encoding: 'utf8',
1149
- });
1150
- return {
1151
- toplevel: root,
1152
- originUrl: origin.status === 0 ? origin.stdout.trim() : '',
1153
- };
1154
- }
1155
- function stableGraphHash(graph) {
1156
- return createHash('sha256')
1157
- .update(JSON.stringify(sortJson({
1158
- ...graph,
1159
- scan: {
1160
- ...graph.scan,
1161
- createdAt: undefined,
1162
- scannerVersion: undefined,
1163
- skipped: undefined,
1164
- },
1165
- })))
1166
- .digest('hex');
1167
- }
1168
- function sortJson(value) {
1169
- if (Array.isArray(value))
1170
- return value.map((item) => sortJson(item));
1171
- if (value && typeof value === 'object') {
1172
- return Object.fromEntries(Object.entries(value)
1173
- .filter(([, item]) => item !== undefined)
1174
- .sort(([a], [b]) => a.localeCompare(b))
1175
- .map(([key, item]) => [key, sortJson(item)]));
1176
- }
1177
- return value;
1178
- }
1179
- function isRecord(value) {
1180
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1181
- }
1182
- //# sourceMappingURL=doctor.js.map