@moda-ai/cli 1.1.1 → 1.2.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 (59) hide show
  1. package/README.md +36 -0
  2. package/dist/agent.d.ts +5 -0
  3. package/dist/agent.js +477 -0
  4. package/dist/analyst-tui.d.ts +81 -0
  5. package/dist/analyst-tui.js +314 -0
  6. package/dist/api-client.d.ts +4 -0
  7. package/dist/api-client.js +20 -3
  8. package/dist/auth.d.ts +22 -3
  9. package/dist/auth.js +230 -66
  10. package/dist/cli.d.ts +20 -3
  11. package/dist/cli.js +768 -93
  12. package/dist/command-context.d.ts +34 -0
  13. package/dist/command-context.js +38 -0
  14. package/dist/command-registry.d.ts +22 -0
  15. package/dist/command-registry.js +15 -0
  16. package/dist/config.d.ts +40 -1
  17. package/dist/config.js +70 -3
  18. package/dist/doctor.d.ts +79 -0
  19. package/dist/doctor.js +1102 -0
  20. package/dist/errors.d.ts +5 -0
  21. package/dist/errors.js +9 -0
  22. package/dist/harness-report.d.ts +138 -0
  23. package/dist/harness-report.js +1464 -0
  24. package/dist/harness.d.ts +233 -0
  25. package/dist/harness.js +3264 -0
  26. package/dist/init/index.d.ts +30 -1
  27. package/dist/init/index.js +383 -23
  28. package/dist/init/rules.d.ts +5 -1
  29. package/dist/init/rules.js +22 -16
  30. package/dist/init/sdk-guide.d.ts +3 -0
  31. package/dist/init/sdk-guide.js +397 -0
  32. package/dist/init/tenant.js +12 -3
  33. package/dist/keys.d.ts +47 -0
  34. package/dist/keys.js +136 -0
  35. package/dist/output.d.ts +118 -0
  36. package/dist/output.js +228 -0
  37. package/dist/production-intelligence.d.ts +134 -0
  38. package/dist/production-intelligence.js +1113 -0
  39. package/dist/profile-commands.d.ts +10 -0
  40. package/dist/profile-commands.js +252 -0
  41. package/dist/profiles.d.ts +90 -0
  42. package/dist/profiles.js +432 -0
  43. package/dist/prompts.d.ts +6 -0
  44. package/dist/prompts.js +368 -0
  45. package/dist/schemas.d.ts +4 -4
  46. package/dist/skills.d.ts +25 -0
  47. package/dist/skills.js +468 -0
  48. package/dist/sync.d.ts +5 -0
  49. package/dist/sync.js +27 -0
  50. package/dist/telemetry.d.ts +18 -0
  51. package/dist/telemetry.js +63 -0
  52. package/dist/tui.d.ts +40 -0
  53. package/dist/tui.js +387 -0
  54. package/dist/workspace.d.ts +12 -0
  55. package/dist/workspace.js +1294 -0
  56. package/hooks/moda-snapshot.sh +41 -0
  57. package/package.json +10 -7
  58. package/skills/moda-cli/SKILL.md +48 -4
  59. package/skills/integrate-moda/SKILL.md +0 -63
@@ -0,0 +1,3264 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { execFileSync, spawn } from 'node:child_process';
3
+ import { homedir } from 'node:os';
4
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
5
+ import { basename, delimiter, dirname, extname, join, relative, resolve, sep } from 'node:path';
6
+ import { confirm, select } from '@inquirer/prompts';
7
+ import { callControlAPI } from './api-client.js';
8
+ import { AnalystTui, shouldUseAnalystTui } from './analyst-tui.js';
9
+ import { resolveApiKey, resolveTenantId } from './config.js';
10
+ import { CliInputError } from './errors.js';
11
+ import { getActiveProfile } from './profiles.js';
12
+ import { approveHarnessReport, createHarnessReport, graphForReport, hashHarnessReport, hashStableJson as hashHarnessReportStableJson, HARNESS_REPORT_PATH, loadHarnessReportForSync, normalizeHarnessReport, readHarnessReport, resolveHarnessReportPath, validateHarnessReport, writeHarnessReportArtifacts, } from './harness-report.js';
13
+ import { renderHarnessAnalystChoice, renderHarnessAnalystLaunch, renderHarnessAnalystPreflight, renderHarnessAnalyzeHumanResult, renderHarnessProgress, } from './tui.js';
14
+ const MODA_DIR = '.moda';
15
+ const HARNESS_PATH = join(MODA_DIR, 'harness.json');
16
+ const HARNESS_SUMMARY_PATH = join(MODA_DIR, 'HARNESS.md');
17
+ const SYNC_STATE_PATH = join(MODA_DIR, 'sync-state.json');
18
+ const SCHEMA_VERSION = 'harness.v0.1';
19
+ const MAX_SCANNED_FILE_BYTES = 512 * 1024;
20
+ const DEFAULT_MAX_INVENTORY_FILES = 12_000;
21
+ const DEFAULT_MAX_INVENTORY_TEXT_BYTES = 48 * 1024 * 1024;
22
+ const MAX_RECORDED_SKIPS = 1_000;
23
+ const REPORT_JSON_BEGIN = 'MODA_HARNESS_REPORT_JSON_BEGIN';
24
+ const REPORT_JSON_END = 'MODA_HARNESS_REPORT_JSON_END';
25
+ const SKIPPED_DIRS = new Set([
26
+ '.angular',
27
+ '.cache',
28
+ '.expo',
29
+ '.git',
30
+ '.hg',
31
+ '.idea',
32
+ '.svn',
33
+ '.next',
34
+ '.nuxt',
35
+ '.output',
36
+ '.parcel-cache',
37
+ '.pytest_cache',
38
+ '.ruff_cache',
39
+ '.serverless',
40
+ '.sst',
41
+ '.turbo',
42
+ '.wrangler',
43
+ '.wrangler-shared',
44
+ '.moda',
45
+ '.moda-test',
46
+ '.moda-test-home',
47
+ '.moda-cli-workspaces',
48
+ '.moda-cli-benchmarks',
49
+ 'node_modules',
50
+ 'dist',
51
+ 'build',
52
+ 'coverage',
53
+ 'out',
54
+ 'target',
55
+ 'logs',
56
+ 'tmp',
57
+ 'temp',
58
+ '__pycache__',
59
+ '.venv',
60
+ 'venv',
61
+ ]);
62
+ const SKIPPED_PATH_PREFIXES = [
63
+ '.claude/worktrees',
64
+ '.codex/environments',
65
+ ];
66
+ const EXTERNAL_ANALYST_ADAPTERS = ['claude', 'codex', 'cursor'];
67
+ export async function runHarnessCommand(context) {
68
+ const subcommand = context.positionals[0] ?? 'scan';
69
+ const rootDir = resolve(context.cwd, context.flags.path ?? '.');
70
+ const selectedAgentId = selectedRuntimeAgentId(context.flags);
71
+ const tenant = resolveHarnessTenant(context.flags, context);
72
+ const scanPlan = buildCommandScanPlan(context, rootDir);
73
+ switch (subcommand) {
74
+ case 'scan': {
75
+ await approveScanIfNeeded(scanPlan, { context });
76
+ writeHumanProgress(context, 'Harness scan: inventorying local files...');
77
+ const graph = scanHarness({
78
+ rootDir,
79
+ dryRun: context.dryRun,
80
+ tenantId: tenant?.tenantId,
81
+ tenantSource: tenant?.source,
82
+ pathWasExplicit: context.flags.path !== undefined,
83
+ });
84
+ const written = context.dryRun ? [] : writeHarnessArtifacts(graph, rootDir, {
85
+ summary: context.flags.summary === 'true',
86
+ });
87
+ writeHumanProgress(context, 'Harness scan: complete.');
88
+ context.output.writeData({
89
+ scanPlan,
90
+ summary: summarizeHarness(graph),
91
+ graph,
92
+ written,
93
+ dryRun: context.dryRun,
94
+ });
95
+ return { exitCode: 0 };
96
+ }
97
+ case 'sync': {
98
+ if (!resolveApiKey({ profile: context.profile, env: context.env }) && !context.dryRun) {
99
+ throw new CliInputError('No Moda API key found for harness sync.', 'Run `moda init` first, set MODA_API_KEY, or select a profile with an API key.');
100
+ }
101
+ if (context.flags['from-report'] || context.flags.report) {
102
+ const reportPath = resolveHarnessReportPath(rootDir, context.flags['from-report'] ?? context.flags.report);
103
+ const { report, validation, approval, approved } = loadHarnessReportForSync(rootDir, reportPath);
104
+ const reportHash = hashHarnessReport(report);
105
+ if (!approved && !context.dryRun) {
106
+ throw new CliInputError('Harness report is not approved; refusing to sync.', 'Run `moda harness validate-report`, review the report, then run `moda harness approve --yes` before syncing.');
107
+ }
108
+ const syncGraph = graphForReport(report.graph);
109
+ const sourceCommit = report.source.sourceCommit ?? getGitCommit(rootDir);
110
+ const localGraphHash = hashHarnessReportStableJson(syncGraph);
111
+ const plan = createHarnessSyncPlan({
112
+ context,
113
+ rootDir,
114
+ graph: report.graph,
115
+ source: 'approved_report',
116
+ sourceCommit,
117
+ localGraphHash,
118
+ localWrites: plannedHarnessArtifactWrites(context),
119
+ });
120
+ const written = context.dryRun ? [] : writeHarnessArtifacts(report.graph, rootDir, {
121
+ summary: context.flags.summary === 'true',
122
+ });
123
+ const syncBody = {
124
+ graph: syncGraph,
125
+ graphHash: localGraphHash,
126
+ sourceCommit,
127
+ report,
128
+ reportHash,
129
+ reportValidationStatus: validation.status,
130
+ reportApproval: approval ? {
131
+ approved: approval.approved,
132
+ approvedAt: approval.approvedAt,
133
+ approvedBy: approval.approvedBy,
134
+ reportHash: approval.reportHash,
135
+ } : undefined,
136
+ };
137
+ const response = await syncHarnessWithRemote(context, syncBody);
138
+ const statePath = response && !context.dryRun
139
+ ? writeHarnessSyncState(rootDir, {
140
+ schemaVersion: 'sync_state.v0.1',
141
+ updatedAt: new Date().toISOString(),
142
+ resource: 'harness',
143
+ source: 'approved_report',
144
+ profile: plan.profile,
145
+ tenantId: response.tenantId ?? plan.tenantId,
146
+ rootPath: plan.rootPath,
147
+ sourceCommit,
148
+ harnessId: report.graph.harnessId,
149
+ localGraphHash,
150
+ report: {
151
+ reportId: report.reportId,
152
+ reportHash,
153
+ validationStatus: validation.status,
154
+ },
155
+ remote: response,
156
+ summary: summarizeHarness(report.graph),
157
+ uploads: plan.uploads,
158
+ privacy: plan.privacy,
159
+ written,
160
+ })
161
+ : undefined;
162
+ context.output.writeData({
163
+ plan,
164
+ report: {
165
+ reportId: report.reportId,
166
+ reportHash,
167
+ path: normalizePath(relative(rootDir, reportPath)) || '.moda/harness-report.json',
168
+ },
169
+ validation,
170
+ approval: {
171
+ approved,
172
+ approvedAt: approval?.approvedAt,
173
+ approvedBy: approval?.approvedBy,
174
+ },
175
+ summary: summarizeHarness(report.graph),
176
+ remote: response ?? plannedDryRunRemote(plan),
177
+ written,
178
+ syncState: statePath ? { path: statePath } : undefined,
179
+ dryRun: context.dryRun,
180
+ });
181
+ return { exitCode: 0 };
182
+ }
183
+ const graph = scanHarness({
184
+ rootDir,
185
+ dryRun: context.dryRun,
186
+ tenantId: tenant?.tenantId,
187
+ tenantSource: tenant?.source,
188
+ pathWasExplicit: context.flags.path !== undefined,
189
+ });
190
+ const syncGraph = graphForSync(graph);
191
+ const sourceCommit = getGitCommit(rootDir);
192
+ const localGraphHash = hashStableJson(syncGraph);
193
+ const plan = createHarnessSyncPlan({
194
+ context,
195
+ rootDir,
196
+ graph,
197
+ source: 'local_scan',
198
+ sourceCommit,
199
+ localGraphHash,
200
+ localWrites: plannedHarnessArtifactWrites(context),
201
+ });
202
+ const written = context.dryRun ? [] : writeHarnessArtifacts(graph, rootDir, {
203
+ summary: context.flags.summary === 'true',
204
+ });
205
+ const response = await syncHarnessWithRemote(context, {
206
+ graph: syncGraph,
207
+ graphHash: localGraphHash,
208
+ sourceCommit,
209
+ });
210
+ const statePath = response && !context.dryRun
211
+ ? writeHarnessSyncState(rootDir, {
212
+ schemaVersion: 'sync_state.v0.1',
213
+ updatedAt: new Date().toISOString(),
214
+ resource: 'harness',
215
+ source: 'local_scan',
216
+ profile: plan.profile,
217
+ tenantId: response.tenantId ?? plan.tenantId,
218
+ rootPath: plan.rootPath,
219
+ sourceCommit,
220
+ harnessId: graph.harnessId,
221
+ localGraphHash,
222
+ remote: response,
223
+ summary: summarizeHarness(graph),
224
+ uploads: plan.uploads,
225
+ privacy: plan.privacy,
226
+ written,
227
+ })
228
+ : undefined;
229
+ context.output.writeData({
230
+ scanPlan,
231
+ plan,
232
+ summary: summarizeHarness(graph),
233
+ remote: response ?? plannedDryRunRemote(plan),
234
+ written,
235
+ syncState: statePath ? { path: statePath } : undefined,
236
+ dryRun: context.dryRun,
237
+ });
238
+ return { exitCode: 0 };
239
+ }
240
+ case 'analyze': {
241
+ await approveScanIfNeeded(scanPlan, {
242
+ context,
243
+ message: 'Run Moda Analyst to investigate this project and produce a cited harness report?',
244
+ declinedMessage: 'Harness analysis was not approved.',
245
+ declinedHint: 'Run `moda harness analyze --path=<dir>` for a narrower investigation, or rerun with `--yes` after reviewing the read plan.',
246
+ });
247
+ context.output.writeEvent({
248
+ event: 'started',
249
+ phase: 'harness_analyze',
250
+ message: 'Starting Moda Analyst harness analysis.',
251
+ elapsed_ms: Date.now() - context.startedAt,
252
+ });
253
+ const analyst = await resolveHarnessAnalystAdapter(context);
254
+ if (analyst !== 'local-scan') {
255
+ return runExternalHarnessAnalyst({
256
+ context,
257
+ rootDir,
258
+ scanPlan,
259
+ tenant,
260
+ adapter: analyst,
261
+ });
262
+ }
263
+ context.output.writeEvent({
264
+ event: 'progress',
265
+ phase: 'inventory',
266
+ message: 'Inventorying local harness files.',
267
+ elapsed_ms: Date.now() - context.startedAt,
268
+ });
269
+ writeHumanProgress(context, 'Moda Analyst: inventorying local harness files...');
270
+ const graph = scanHarness({
271
+ rootDir,
272
+ dryRun: context.dryRun,
273
+ tenantId: tenant?.tenantId,
274
+ tenantSource: tenant?.source,
275
+ pathWasExplicit: context.flags.path !== undefined,
276
+ });
277
+ context.output.writeEvent({
278
+ event: 'progress',
279
+ phase: 'report',
280
+ message: 'Producing cited structured harness report.',
281
+ elapsed_ms: Date.now() - context.startedAt,
282
+ });
283
+ writeHumanProgress(context, 'Moda Analyst: producing cited structured harness report...');
284
+ const report = createHarnessReport({
285
+ graph,
286
+ rootDir,
287
+ sourceCommit: getGitCommit(rootDir),
288
+ analystAdapter: 'local-scan',
289
+ });
290
+ const validation = validateHarnessReport(report, rootDir);
291
+ context.output.writeEvent({
292
+ event: 'progress',
293
+ phase: 'validation',
294
+ message: `Harness report validation ${validation.status}.`,
295
+ elapsed_ms: Date.now() - context.startedAt,
296
+ });
297
+ writeHumanProgress(context, `Moda Analyst: validation ${validation.status}.`);
298
+ const written = context.dryRun
299
+ ? []
300
+ : [
301
+ ...writeHarnessReportArtifacts({
302
+ rootDir,
303
+ report,
304
+ validation,
305
+ markdown: context.flags.summary === 'true' || context.outputMode === 'human',
306
+ }),
307
+ ...writeHarnessArtifacts(report.graph, rootDir, {
308
+ summary: context.flags.summary === 'true',
309
+ }),
310
+ ];
311
+ for (const file of written) {
312
+ context.output.writeEvent({
313
+ event: 'artifact',
314
+ phase: 'write',
315
+ artifact: {
316
+ kind: file.includes('harness-report') || file.includes('HARNESS_REPORT')
317
+ ? 'harness_report_artifact'
318
+ : 'harness_graph_artifact',
319
+ path: file,
320
+ description: 'Written by Moda Analyst.',
321
+ },
322
+ elapsed_ms: Date.now() - context.startedAt,
323
+ });
324
+ }
325
+ writeHarnessAnalyzeResult(context, {
326
+ scanPlan,
327
+ summary: summarizeHarness(graph),
328
+ report,
329
+ validation,
330
+ written,
331
+ dryRun: context.dryRun,
332
+ });
333
+ return { exitCode: validation.status === 'pass' ? 0 : 1 };
334
+ }
335
+ case 'validate-report': {
336
+ const reportPath = resolveHarnessReportPath(rootDir, context.positionals[1] ?? context.flags.report);
337
+ const report = readHarnessReport(reportPath);
338
+ const validation = validateHarnessReport(report, rootDir);
339
+ if (!context.dryRun) {
340
+ writeHarnessReportArtifacts({
341
+ rootDir,
342
+ report,
343
+ validation,
344
+ markdown: context.flags.summary === 'true',
345
+ });
346
+ }
347
+ context.output.writeData(validation, { status: validation.status === 'pass' ? 'success' : 'error' });
348
+ return { exitCode: validation.status === 'pass' ? 0 : 1 };
349
+ }
350
+ case 'approve': {
351
+ const reportPath = resolveHarnessReportPath(rootDir, context.positionals[1] ?? context.flags.report);
352
+ const approval = await approveHarnessReport({
353
+ rootDir,
354
+ reportPath,
355
+ yes: context.yes,
356
+ interactive: context.interactive,
357
+ });
358
+ context.output.writeData({
359
+ report: {
360
+ reportId: approval.report.reportId,
361
+ reportHash: hashHarnessReport(approval.report),
362
+ path: normalizePath(relative(rootDir, reportPath)) || '.moda/harness-report.json',
363
+ },
364
+ validation: approval.validation,
365
+ approval: approval.approval,
366
+ });
367
+ return { exitCode: 0 };
368
+ }
369
+ case 'agents': {
370
+ const graph = loadOrScanHarness(rootDir, tenant);
371
+ context.output.writeData({
372
+ summary: summarizeHarness(graph),
373
+ agentFamilies: graph.agentFamilies,
374
+ runtimeAgents: graph.runtimeAgents,
375
+ unknowns: graph.unknowns,
376
+ });
377
+ return { exitCode: 0 };
378
+ }
379
+ case 'graph': {
380
+ const graph = loadOrScanHarness(rootDir, tenant);
381
+ context.output.writeData(graph);
382
+ return { exitCode: 0 };
383
+ }
384
+ case 'explain': {
385
+ const graph = loadOrScanHarness(rootDir, tenant);
386
+ if (selectedAgentId) {
387
+ const agent = graph.runtimeAgents.find((candidate) => candidate.id === selectedAgentId);
388
+ if (!agent) {
389
+ throw new CliInputError(`Runtime agent '${selectedAgentId}' was not found in the harness graph.`, 'Run `moda harness agents` to inspect detected runtime agents.');
390
+ }
391
+ context.output.writeData(explainAgent(graph, agent));
392
+ return { exitCode: 0 };
393
+ }
394
+ context.output.writeData({
395
+ summary: summarizeHarness(graph),
396
+ markdown: renderHarnessMarkdown(graph),
397
+ unknowns: graph.unknowns,
398
+ });
399
+ return { exitCode: 0 };
400
+ }
401
+ case 'write-summary': {
402
+ const graph = loadOrScanHarness(rootDir, tenant);
403
+ const summaryPath = writeHarnessSummary(graph, rootDir);
404
+ context.output.writeData({
405
+ summary: summarizeHarness(graph),
406
+ written: [summaryPath],
407
+ });
408
+ return { exitCode: 0 };
409
+ }
410
+ case 'status': {
411
+ const manifestPath = join(rootDir, HARNESS_PATH);
412
+ const graph = existsSync(manifestPath)
413
+ ? readHarnessGraph(manifestPath)
414
+ : scanHarness({
415
+ rootDir,
416
+ tenantId: tenant?.tenantId,
417
+ tenantSource: tenant?.source,
418
+ });
419
+ const currentGraphHash = hashStableJson(graphForSync(graph));
420
+ const syncState = readHarnessSyncState(rootDir);
421
+ context.output.writeData({
422
+ manifest: HARNESS_PATH,
423
+ exists: existsSync(manifestPath),
424
+ summary: summarizeHarness(graph),
425
+ graphHash: currentGraphHash,
426
+ sync: syncState
427
+ ? {
428
+ statePath: SYNC_STATE_PATH,
429
+ lastSyncedAt: syncState.updatedAt,
430
+ profile: syncState.profile,
431
+ tenantId: syncState.tenantId,
432
+ harnessId: syncState.harnessId,
433
+ remoteHarnessId: syncState.remote.harnessId,
434
+ remoteVersionId: syncState.remote.versionId,
435
+ remoteGraphHash: syncState.remote.graphHash,
436
+ localGraphHash: syncState.localGraphHash,
437
+ stale: syncState.localGraphHash !== currentGraphHash,
438
+ }
439
+ : {
440
+ statePath: SYNC_STATE_PATH,
441
+ lastSyncedAt: null,
442
+ stale: true,
443
+ },
444
+ stale: syncState ? syncState.localGraphHash !== currentGraphHash : true,
445
+ });
446
+ return { exitCode: 0 };
447
+ }
448
+ default:
449
+ throw new CliInputError(`Unknown harness command '${subcommand}'.`, 'Usage: moda harness analyze|validate-report|approve|scan|sync|agents|graph|explain|write-summary|status');
450
+ }
451
+ }
452
+ function writeHarnessAnalyzeResult(context, payload) {
453
+ if (context.outputMode === 'human') {
454
+ if (!context.quiet)
455
+ console.log(renderHarnessAnalyzeHumanResult(payload));
456
+ return;
457
+ }
458
+ const validationPassed = payload.validation.status === 'pass';
459
+ context.output.writeData(payload, {
460
+ status: validationPassed ? 'success' : 'error',
461
+ summary: validationPassed
462
+ ? 'Moda Analyst produced and validated a cited harness report.'
463
+ : 'Moda Analyst produced a harness report, but validation failed.',
464
+ findings: [
465
+ {
466
+ id: 'harness_analyze:validation',
467
+ title: 'Harness report validation',
468
+ status: payload.validation.status,
469
+ severity: validationPassed ? 'info' : 'error',
470
+ description: validationPassed
471
+ ? 'The report is ready for human review and approval.'
472
+ : 'Review validation errors before syncing the harness graph.',
473
+ },
474
+ ],
475
+ artifacts: payload.written.map((file) => ({
476
+ kind: file.includes('harness-report') || file.includes('HARNESS_REPORT')
477
+ ? 'harness_report_artifact'
478
+ : 'harness_graph_artifact',
479
+ path: file,
480
+ description: 'Written by Moda Analyst.',
481
+ })),
482
+ nextCommands: validationPassed
483
+ ? [
484
+ {
485
+ command: 'moda harness approve --yes',
486
+ purpose: 'Approve the cited report after human review.',
487
+ mutability: 'write',
488
+ requires_approval: true,
489
+ },
490
+ {
491
+ command: 'moda harness sync --from-report .moda/harness-report.json --summary',
492
+ purpose: 'Sync the approved harness graph to Moda Cloud.',
493
+ mutability: 'sync',
494
+ requires_approval: true,
495
+ },
496
+ ]
497
+ : [
498
+ {
499
+ command: 'moda harness validate-report --summary',
500
+ purpose: 'Re-run citation and schema validation after fixing the report.',
501
+ mutability: 'read',
502
+ requires_approval: false,
503
+ },
504
+ ],
505
+ });
506
+ }
507
+ async function runExternalHarnessAnalyst(options) {
508
+ if (options.context.dryRun) {
509
+ throw new CliInputError('External harness analysis cannot run in dry-run mode yet.', 'Use `moda harness analyze --analyst=local-scan --dry-run` for deterministic fallback inspection.');
510
+ }
511
+ const prompt = renderExternalHarnessAnalystPrompt({
512
+ adapter: options.adapter,
513
+ rootDir: options.rootDir,
514
+ tenantId: options.tenant?.tenantId,
515
+ });
516
+ const invocation = resolveExternalHarnessAnalystInvocation({
517
+ adapter: options.adapter,
518
+ flags: options.context.flags,
519
+ prompt,
520
+ rootDir: options.rootDir,
521
+ stream: options.context.outputMode === 'human',
522
+ });
523
+ options.context.output.writeEvent({
524
+ event: 'progress',
525
+ phase: 'analyst_launch',
526
+ message: `Launching ${analystDisplayName(options.adapter)} in read-only analyst mode.`,
527
+ elapsed_ms: Date.now() - options.context.startedAt,
528
+ });
529
+ const inheritedOutput = options.context.outputMode === 'human';
530
+ const analystTui = shouldUseAnalystTui(options.context.flags, options.context.outputMode) && invocation.streamEvents && !options.context.quiet
531
+ ? new AnalystTui({
532
+ adapterName: analystDisplayName(options.adapter),
533
+ command: invocation.command,
534
+ root: options.rootDir,
535
+ reportPath: HARNESS_REPORT_PATH,
536
+ startedAt: options.context.startedAt,
537
+ })
538
+ : undefined;
539
+ if (analystTui) {
540
+ analystTui.start();
541
+ }
542
+ else {
543
+ writeHumanBlock(options.context, renderHarnessAnalystLaunch({
544
+ adapterName: analystDisplayName(options.adapter),
545
+ command: invocation.command,
546
+ root: options.rootDir,
547
+ reportPath: HARNESS_REPORT_PATH,
548
+ }));
549
+ }
550
+ const timeoutMs = numericEnv('MODA_HARNESS_ANALYST_TIMEOUT_MS', 20 * 60 * 1000);
551
+ try {
552
+ const result = await runExternalAnalystWithRetries(invocation, {
553
+ cwd: options.rootDir,
554
+ env: buildExternalAnalystEnv(options.context.env),
555
+ inheritedOutput,
556
+ timeoutMs,
557
+ maxBuffer: 20 * 1024 * 1024,
558
+ progress: (inheritedOutput && !options.context.quiet) || options.context.outputMode === 'agent-stream',
559
+ progressToStderr: inheritedOutput && !options.context.quiet && !analystTui,
560
+ streamEvents: invocation.streamEvents,
561
+ adapter: invocation.adapter,
562
+ tui: analystTui,
563
+ maxAttempts: externalAnalystMaxAttempts(options.adapter),
564
+ backoffMs: numericEnvAllowZero('MODA_HARNESS_ANALYST_RETRY_BACKOFF_MS', 6_000),
565
+ onProgress: (message, phase = 'analyst') => {
566
+ options.context.output.writeEvent({
567
+ event: 'progress',
568
+ phase,
569
+ message,
570
+ elapsed_ms: Date.now() - options.context.startedAt,
571
+ });
572
+ },
573
+ });
574
+ if (result.error) {
575
+ throw new CliInputError(`Harness analyst ${options.adapter} failed to run: ${result.error.message}`, 'Check that the selected coding agent CLI is installed and authenticated, or run `moda harness analyze --analyst=local-scan` for the deterministic fallback.');
576
+ }
577
+ if (result.timedOut) {
578
+ throw new CliInputError(`Harness analyst ${options.adapter} timed out after ${Math.round(timeoutMs / 1000)}s.`, 'Run from a narrower agent project root, pass --path=<agent-dir>, or retry after the analyst CLI is responsive.');
579
+ }
580
+ if (result.maxBufferExceeded) {
581
+ throw new CliInputError(`Harness analyst ${options.adapter} produced more than 20MiB of output.`, 'Retry with a narrower --path or ask the analyst to write the report directly instead of printing large logs.');
582
+ }
583
+ if (result.status !== 0) {
584
+ const output = inheritedOutput
585
+ ? ''
586
+ : truncateText(`${result.stdout}\n${result.stderr}`, 1200);
587
+ throw new CliInputError(`Harness analyst ${options.adapter} exited with status ${result.status ?? 'unknown'}.`, output || 'Rerun with the selected coding agent CLI fixed, or choose another analyst with `--analyst=claude|codex|cursor`.');
588
+ }
589
+ const report = readExternalAnalystReport(options.rootDir, result, options.adapter);
590
+ analystTui?.setSummary({
591
+ runtimeAgentCount: report.graph.runtimeAgents.length,
592
+ agentFamilyCount: report.graph.agentFamilies.length,
593
+ artifactCount: report.graph.artifacts.length,
594
+ modelProviderCount: report.graph.modelProviders.length,
595
+ unknownCount: report.graph.unknowns.length,
596
+ claimCount: report.claims.length,
597
+ citationCount: report.citations.length,
598
+ relationshipCount: report.graph.relationships.length,
599
+ });
600
+ options.context.output.writeEvent({
601
+ event: 'progress',
602
+ phase: 'validation',
603
+ message: `Validating ${HARNESS_REPORT_PATH}.`,
604
+ elapsed_ms: Date.now() - options.context.startedAt,
605
+ });
606
+ analystTui?.setValidation('running');
607
+ const validation = validateHarnessReport(report, options.rootDir);
608
+ analystTui?.setValidation(validation.status === 'pass' ? 'pass' : 'fail');
609
+ if (!analystTui)
610
+ writeHumanProgress(options.context, `Moda Analyst: validation ${validation.status}.`);
611
+ const written = [
612
+ ...writeHarnessReportArtifacts({
613
+ rootDir: options.rootDir,
614
+ report,
615
+ validation,
616
+ markdown: options.context.flags.summary === 'true' || options.context.outputMode === 'human',
617
+ }),
618
+ ];
619
+ if (validation.status === 'pass') {
620
+ written.push(...writeHarnessArtifacts(report.graph, options.rootDir, {
621
+ summary: options.context.flags.summary === 'true',
622
+ }));
623
+ }
624
+ analystTui?.setArtifacts(written);
625
+ for (const file of written) {
626
+ options.context.output.writeEvent({
627
+ event: 'artifact',
628
+ phase: 'write',
629
+ artifact: {
630
+ kind: file.includes('harness-report') || file.includes('HARNESS_REPORT')
631
+ ? 'harness_report_artifact'
632
+ : 'harness_graph_artifact',
633
+ path: file,
634
+ description: 'Written by Moda Analyst.',
635
+ },
636
+ elapsed_ms: Date.now() - options.context.startedAt,
637
+ });
638
+ }
639
+ analystTui?.complete(validation.status === 'pass' ? 'pass' : 'fail');
640
+ analystTui?.stop();
641
+ writeHarnessAnalyzeResult(options.context, {
642
+ scanPlan: options.scanPlan,
643
+ summary: validation.status === 'pass'
644
+ ? summarizeHarness(report.graph)
645
+ : summarizeInvalidAnalystReport(report),
646
+ report,
647
+ validation,
648
+ written,
649
+ dryRun: false,
650
+ });
651
+ return { exitCode: validation.status === 'pass' ? 0 : 1 };
652
+ }
653
+ catch (error) {
654
+ analystTui?.complete('fail');
655
+ analystTui?.stop();
656
+ throw error;
657
+ }
658
+ }
659
+ function buildExternalAnalystEnv(env) {
660
+ const next = {
661
+ ...env,
662
+ MODA_CLI_TELEMETRY: '0',
663
+ DO_NOT_TRACK: '1',
664
+ };
665
+ const analystHome = env.MODA_HARNESS_ANALYST_HOME;
666
+ if (analystHome && analystHome !== 'true') {
667
+ next.HOME = analystHome === '~' ? homedir() : analystHome;
668
+ next.USERPROFILE = next.HOME;
669
+ }
670
+ return next;
671
+ }
672
+ function readExternalAnalystReport(rootDir, result, adapter) {
673
+ const reportPath = resolve(rootDir, HARNESS_REPORT_PATH);
674
+ if (existsSync(reportPath))
675
+ return repairExternalAnalystReport(readHarnessReport(reportPath), rootDir);
676
+ const output = [
677
+ result.stdout,
678
+ result.stderr,
679
+ extractAgentTextFromOutput(result.stdout),
680
+ extractAgentTextFromOutput(result.stderr),
681
+ ].filter(Boolean).join('\n');
682
+ const parsed = extractHarnessReportFromText(output, rootDir);
683
+ if (parsed)
684
+ return parsed;
685
+ throw new CliInputError(`Harness analyst ${adapter} did not return a harness report.`, [
686
+ `Expected ${HARNESS_REPORT_PATH} or final JSON between ${REPORT_JSON_BEGIN} and ${REPORT_JSON_END}.`,
687
+ 'Retry with the selected coding agent CLI fixed, choose another analyst, or run `moda harness analyze --analyst=local-scan --summary` for deterministic fallback.',
688
+ ].join('\n'));
689
+ }
690
+ function extractHarnessReportFromText(output, rootDir) {
691
+ for (const candidate of reportJsonCandidates(output)) {
692
+ const parsed = parseHarnessReportCandidate(candidate);
693
+ if (parsed)
694
+ return repairExternalAnalystReport(parsed, rootDir);
695
+ }
696
+ return undefined;
697
+ }
698
+ function reportJsonCandidates(output) {
699
+ const candidates = [];
700
+ const markerPattern = new RegExp(`${escapeRegExp(REPORT_JSON_BEGIN)}\\s*([\\s\\S]*?)\\s*${escapeRegExp(REPORT_JSON_END)}`, 'g');
701
+ for (const match of output.matchAll(markerPattern)) {
702
+ if (match[1])
703
+ candidates.push(match[1]);
704
+ }
705
+ const fencePattern = /```(?:json)?\s*([\s\S]*?)```/gi;
706
+ for (const match of output.matchAll(fencePattern)) {
707
+ if (match[1]?.includes('harness_report.v0.1'))
708
+ candidates.push(match[1]);
709
+ }
710
+ const firstBrace = output.indexOf('{');
711
+ const lastBrace = output.lastIndexOf('}');
712
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
713
+ candidates.push(output.slice(firstBrace, lastBrace + 1));
714
+ }
715
+ return candidates;
716
+ }
717
+ function parseHarnessReportCandidate(candidate) {
718
+ try {
719
+ const parsed = JSON.parse(candidate.trim());
720
+ if (isRecord(parsed) && parsed.schemaVersion === 'harness_report.v0.1') {
721
+ return parsed;
722
+ }
723
+ }
724
+ catch {
725
+ return undefined;
726
+ }
727
+ return undefined;
728
+ }
729
+ function repairExternalAnalystReport(report, rootDir) {
730
+ const normalized = normalizeHarnessReport(report);
731
+ repairCitationLines(normalized, rootDir);
732
+ repairReportSummaryCounts(normalized);
733
+ return normalized;
734
+ }
735
+ function repairReportSummaryCounts(report) {
736
+ const graph = report.graph;
737
+ report.summary = {
738
+ ...report.summary,
739
+ runtimeAgentCount: graph.runtimeAgents.length,
740
+ agentFamilyCount: graph.agentFamilies.length,
741
+ artifactCount: graph.artifacts.length,
742
+ sharedArtifactCount: graph.artifacts.filter((artifact) => artifact.scope === 'shared').length,
743
+ channelAdapterCount: graph.channelAdapters.length,
744
+ identityNamespaceCount: graph.identityNamespaces.length,
745
+ environmentCount: graph.environments.length,
746
+ agentFrameworkCount: graph.agentFrameworks.length,
747
+ modelProviderCount: graph.modelProviders.length,
748
+ deploymentCount: graph.deployments.length,
749
+ relationshipCount: graph.relationships.length,
750
+ unknownCount: graph.unknowns.length,
751
+ };
752
+ }
753
+ function repairCitationLines(report, rootDir) {
754
+ for (const citation of report.citations) {
755
+ if ((citation.type !== 'file' && citation.type !== 'generated_doc') || !citation.path || !citation.excerpt)
756
+ continue;
757
+ const excerpt = citation.excerpt;
758
+ const candidatePath = resolve(rootDir, citation.path);
759
+ if (!existsSync(candidatePath))
760
+ continue;
761
+ let text;
762
+ try {
763
+ text = readFileSync(candidatePath, 'utf8');
764
+ }
765
+ catch {
766
+ continue;
767
+ }
768
+ const lines = text.split(/\r?\n/);
769
+ const currentLine = typeof citation.line === 'number' ? lines[citation.line - 1] : undefined;
770
+ if (currentLine && citationLineMatches(currentLine, excerpt))
771
+ continue;
772
+ const matchedIndex = lines.findIndex((line) => citationLineMatches(line, excerpt));
773
+ if (matchedIndex >= 0)
774
+ citation.line = matchedIndex + 1;
775
+ }
776
+ }
777
+ function citationLineMatches(line, excerpt) {
778
+ return normalizeComparableLine(line).includes(normalizeComparableLine(excerpt));
779
+ }
780
+ function normalizeComparableLine(value) {
781
+ return value.replace(/\s+/g, ' ').trim();
782
+ }
783
+ function extractAgentTextFromOutput(output) {
784
+ const lines = [];
785
+ for (const line of output.split(/\r?\n/)) {
786
+ const trimmed = line.trim();
787
+ if (!trimmed)
788
+ continue;
789
+ try {
790
+ const event = JSON.parse(trimmed);
791
+ const text = extractTextFromAgentEvent(event);
792
+ if (text)
793
+ lines.push(text);
794
+ }
795
+ catch {
796
+ lines.push(trimmed);
797
+ }
798
+ }
799
+ return lines.join('\n');
800
+ }
801
+ function extractTextFromAgentEvent(event) {
802
+ if (!isRecord(event))
803
+ return undefined;
804
+ const directText = stringValue(event.text) ??
805
+ stringValue(event.message) ??
806
+ stringValue(event.delta) ??
807
+ stringValue(event.result);
808
+ if (directText)
809
+ return directText;
810
+ const message = isRecord(event.message) ? event.message : undefined;
811
+ const messageText = message
812
+ ? stringValue(message.text) ?? stringValue(message.content)
813
+ : undefined;
814
+ if (messageText)
815
+ return messageText;
816
+ const content = Array.isArray(message?.content)
817
+ ? message.content
818
+ : Array.isArray(event.content)
819
+ ? event.content
820
+ : [];
821
+ const textParts = content
822
+ .map((part) => {
823
+ if (!isRecord(part))
824
+ return undefined;
825
+ return stringValue(part.text) ?? stringValue(part.content);
826
+ })
827
+ .filter((part) => Boolean(part));
828
+ return textParts.length > 0 ? textParts.join('\n') : undefined;
829
+ }
830
+ function escapeRegExp(value) {
831
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
832
+ }
833
+ function runExternalAnalystProcess(invocation, options) {
834
+ return new Promise((resolveProcess) => {
835
+ const child = spawn(invocation.command, invocation.args, {
836
+ cwd: options.cwd,
837
+ env: options.env,
838
+ stdio: ['ignore', 'pipe', 'pipe'],
839
+ });
840
+ const startedAt = Date.now();
841
+ let lastOutputAt = startedAt;
842
+ let sawOutput = false;
843
+ let stdout = '';
844
+ let stderr = '';
845
+ let timedOut = false;
846
+ let maxBufferExceeded = false;
847
+ let settled = false;
848
+ let eventLineBuffer = '';
849
+ const progressMs = numericEnv('MODA_HARNESS_ANALYST_PROGRESS_MS', 8_000);
850
+ const progressTimer = options.progress
851
+ ? setInterval(() => {
852
+ const elapsed = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
853
+ const quietFor = Math.max(0, Math.round((Date.now() - lastOutputAt) / 1000));
854
+ const status = sawOutput
855
+ ? `${quietFor}s since analyst output`
856
+ : 'waiting for first analyst output';
857
+ const message = `Moda Analyst: still investigating (${elapsed}s elapsed, ${status})...`;
858
+ options.onProgress?.(message, 'analyst_wait');
859
+ options.tui?.heartbeat({ elapsedSeconds: elapsed, quietForSeconds: quietFor, sawOutput });
860
+ if (options.progressToStderr) {
861
+ process.stderr.write(`${renderHarnessProgress(message)}\n`);
862
+ }
863
+ }, progressMs)
864
+ : undefined;
865
+ const timeoutTimer = setTimeout(() => {
866
+ timedOut = true;
867
+ child.kill('SIGTERM');
868
+ setTimeout(() => {
869
+ if (!settled)
870
+ child.kill('SIGKILL');
871
+ }, 5_000).unref();
872
+ }, options.timeoutMs);
873
+ const flushEventLines = () => {
874
+ if (!eventLineBuffer)
875
+ return;
876
+ recordExternalAgentEventLine(eventLineBuffer, options);
877
+ eventLineBuffer = '';
878
+ };
879
+ const appendOutput = (stream, chunk) => {
880
+ sawOutput = true;
881
+ lastOutputAt = Date.now();
882
+ const text = chunk.toString('utf8');
883
+ if (stream === 'stdout')
884
+ stdout += text;
885
+ else
886
+ stderr += text;
887
+ if (options.inheritedOutput) {
888
+ if (options.tui && stream === 'stderr') {
889
+ recordExternalAgentTextChunk(text, options.tui);
890
+ }
891
+ else if (options.streamEvents && stream === 'stdout') {
892
+ eventLineBuffer += text;
893
+ const lines = eventLineBuffer.split(/\r?\n/);
894
+ eventLineBuffer = lines.pop() ?? '';
895
+ for (const line of lines) {
896
+ recordExternalAgentEventLine(line, options);
897
+ }
898
+ }
899
+ else {
900
+ process.stderr.write(chunk);
901
+ }
902
+ }
903
+ if (stdout.length + stderr.length > options.maxBuffer && !maxBufferExceeded) {
904
+ maxBufferExceeded = true;
905
+ child.kill('SIGTERM');
906
+ }
907
+ };
908
+ child.stdout.on('data', (chunk) => appendOutput('stdout', chunk));
909
+ child.stderr.on('data', (chunk) => appendOutput('stderr', chunk));
910
+ child.on('error', (error) => {
911
+ if (settled)
912
+ return;
913
+ settled = true;
914
+ clearTimeout(timeoutTimer);
915
+ if (progressTimer)
916
+ clearInterval(progressTimer);
917
+ flushEventLines();
918
+ resolveProcess({
919
+ status: null,
920
+ signal: null,
921
+ stdout,
922
+ stderr,
923
+ error,
924
+ timedOut,
925
+ maxBufferExceeded,
926
+ });
927
+ });
928
+ child.on('close', (status, signal) => {
929
+ if (settled)
930
+ return;
931
+ settled = true;
932
+ clearTimeout(timeoutTimer);
933
+ if (progressTimer)
934
+ clearInterval(progressTimer);
935
+ flushEventLines();
936
+ resolveProcess({
937
+ status,
938
+ signal,
939
+ stdout,
940
+ stderr,
941
+ timedOut,
942
+ maxBufferExceeded,
943
+ });
944
+ });
945
+ });
946
+ }
947
+ async function runExternalAnalystWithRetries(invocation, options) {
948
+ let lastResult;
949
+ for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
950
+ if (attempt > 1) {
951
+ const message = `Moda Analyst: retrying ${analystDisplayName(options.adapter)} after transient provider failure (${attempt}/${options.maxAttempts}).`;
952
+ options.onProgress?.(message, 'analyst_retry');
953
+ options.tui?.recordEvent({ kind: 'system', message });
954
+ if (options.progressToStderr)
955
+ process.stderr.write(`${renderHarnessProgress(message)}\n`);
956
+ if (options.backoffMs > 0)
957
+ await sleep(options.backoffMs);
958
+ }
959
+ lastResult = await runExternalAnalystProcess(invocation, options);
960
+ if (!isRetryableExternalAnalystFailure(lastResult))
961
+ return lastResult;
962
+ }
963
+ return lastResult;
964
+ }
965
+ function isRetryableExternalAnalystFailure(result) {
966
+ if (result.error || result.timedOut || result.maxBufferExceeded)
967
+ return false;
968
+ if (result.status === 0)
969
+ return false;
970
+ const output = `${result.stdout}\n${result.stderr}`;
971
+ return /\b529\b|overloaded|server-side issue|temporar(?:y|ily)|try again|rate limit|too many requests|\b429\b|service unavailable/i.test(output);
972
+ }
973
+ function externalAnalystMaxAttempts(adapter) {
974
+ const defaultRetries = adapter === 'claude' ? 2 : 1;
975
+ return 1 + numericEnvAllowZero('MODA_HARNESS_ANALYST_RETRIES', defaultRetries);
976
+ }
977
+ function sleep(ms) {
978
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
979
+ }
980
+ function recordExternalAgentTextChunk(text, tui) {
981
+ for (const line of text.split(/\r?\n/)) {
982
+ const message = line.trim();
983
+ if (!message)
984
+ continue;
985
+ tui.recordEvent({ kind: 'system', message: truncateText(message, 500) });
986
+ }
987
+ }
988
+ function recordExternalAgentEventLine(line, options) {
989
+ const event = parseExternalAgentEventLine(line);
990
+ if (!event)
991
+ return;
992
+ options.tui?.recordEvent(event);
993
+ if (options.tui)
994
+ return;
995
+ const formatted = formatExternalAgentDisplayEvent(event, options.adapter);
996
+ if (formatted)
997
+ process.stderr.write(formatted);
998
+ }
999
+ function formatExternalAgentEventLine(line, adapter) {
1000
+ const event = parseExternalAgentEventLine(line);
1001
+ return event ? formatExternalAgentDisplayEvent(event, adapter) : undefined;
1002
+ }
1003
+ function parseExternalAgentEventLine(line) {
1004
+ const trimmed = line.trim();
1005
+ if (!trimmed)
1006
+ return undefined;
1007
+ try {
1008
+ const event = JSON.parse(trimmed);
1009
+ return parseExternalAgentEvent(event);
1010
+ }
1011
+ catch {
1012
+ return { kind: 'agent_text', message: truncateText(trimmed, 500) };
1013
+ }
1014
+ }
1015
+ function formatExternalAgentDisplayEvent(event, adapter) {
1016
+ if (event.kind === 'tool')
1017
+ return undefined;
1018
+ return ` ${analystDisplayName(adapter)}: ${event.message}\n`;
1019
+ }
1020
+ function parseExternalAgentEvent(event) {
1021
+ if (!isRecord(event))
1022
+ return { kind: 'agent_text', message: truncateText(String(event), 500) };
1023
+ const msg = isRecord(event.msg) ? event.msg : undefined;
1024
+ const call = isRecord(event.call) ? event.call : undefined;
1025
+ const type = stringValue(event.type) ?? stringValue(event.event) ?? stringValue(msg?.type);
1026
+ const subtype = stringValue(event.subtype);
1027
+ if (type === 'rate_limit_event')
1028
+ return { kind: 'rate_limit', message: 'rate limit wait' };
1029
+ const toolName = stringValue(event.name) ??
1030
+ stringValue(event.tool_name) ??
1031
+ stringValue(event.toolName) ??
1032
+ stringValue(call?.name);
1033
+ if (toolName && /tool|call|exec|command|bash/i.test(`${type ?? ''} ${subtype ?? ''}`)) {
1034
+ return { kind: 'tool', toolName, message: `tool ${toolName}` };
1035
+ }
1036
+ const directText = stringValue(event.text) ??
1037
+ stringValue(event.message) ??
1038
+ stringValue(event.delta) ??
1039
+ stringValue(event.result);
1040
+ if (directText && !isLowSignalEventType(type)) {
1041
+ return { kind: 'agent_text', message: truncateText(firstMeaningfulLine(directText), 500) };
1042
+ }
1043
+ const message = isRecord(event.message) ? event.message : undefined;
1044
+ const messageText = message
1045
+ ? stringValue(message.text) ?? stringValue(message.content)
1046
+ : undefined;
1047
+ if (messageText && !isLowSignalEventType(type)) {
1048
+ return { kind: 'agent_text', message: truncateText(firstMeaningfulLine(messageText), 500) };
1049
+ }
1050
+ const content = Array.isArray(message?.content)
1051
+ ? message.content
1052
+ : Array.isArray(event.content)
1053
+ ? event.content
1054
+ : [];
1055
+ for (const part of content) {
1056
+ const parsed = parseAgentContentPart(part);
1057
+ if (parsed)
1058
+ return parsed;
1059
+ }
1060
+ const command = stringValue(event.command) ?? stringValue(event.cmd);
1061
+ if (command)
1062
+ return { kind: 'command', message: `command ${truncateText(command, 300)}` };
1063
+ if (type && !isLowSignalEventType(type)) {
1064
+ return { kind: 'system', message: subtype ? `${type}:${subtype}` : type };
1065
+ }
1066
+ return undefined;
1067
+ }
1068
+ function parseAgentContentPart(part) {
1069
+ if (!isRecord(part))
1070
+ return undefined;
1071
+ const partType = stringValue(part.type);
1072
+ if (partType === 'tool_use' || partType === 'tool_call') {
1073
+ const name = stringValue(part.name) ?? stringValue(part.tool_name);
1074
+ return name ? { kind: 'tool', toolName: name, message: `tool ${name}` } : undefined;
1075
+ }
1076
+ if (partType === 'tool_result' || partType === 'tool_output')
1077
+ return undefined;
1078
+ if (partType === 'text' || partType === 'output_text') {
1079
+ const text = stringValue(part.text) ?? stringValue(part.content);
1080
+ return text ? { kind: 'agent_text', message: truncateText(firstMeaningfulLine(text), 500) } : undefined;
1081
+ }
1082
+ const name = stringValue(part.name) ?? stringValue(part.tool_name);
1083
+ if (name)
1084
+ return { kind: 'tool', toolName: name, message: `tool ${name}` };
1085
+ return undefined;
1086
+ }
1087
+ function firstMeaningfulLine(value) {
1088
+ return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? value.trim();
1089
+ }
1090
+ function isLowSignalEventType(type) {
1091
+ return !type ||
1092
+ type === 'assistant' ||
1093
+ type === 'user' ||
1094
+ type === 'system' ||
1095
+ type === 'session' ||
1096
+ type === 'turn_start' ||
1097
+ type === 'turn_end' ||
1098
+ type === 'tool_use' ||
1099
+ type === 'tool_result' ||
1100
+ type === 'tool_call' ||
1101
+ type === 'tool_output' ||
1102
+ type === 'token_count' ||
1103
+ type === 'usage' ||
1104
+ type.endsWith('_delta');
1105
+ }
1106
+ function analystDisplayName(adapter) {
1107
+ if (adapter === 'claude')
1108
+ return 'Claude Code';
1109
+ if (adapter === 'cursor')
1110
+ return 'Cursor';
1111
+ return 'Codex';
1112
+ }
1113
+ function isRecord(value) {
1114
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1115
+ }
1116
+ function stringValue(value) {
1117
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
1118
+ }
1119
+ function summarizeInvalidAnalystReport(report) {
1120
+ if (!report || typeof report !== 'object')
1121
+ return { status: 'invalid_report' };
1122
+ const reportRecord = report;
1123
+ const graph = reportRecord.graph && typeof reportRecord.graph === 'object'
1124
+ ? reportRecord.graph
1125
+ : {};
1126
+ return {
1127
+ status: 'validation_failed',
1128
+ schemaVersion: typeof graph.schemaVersion === 'string' ? graph.schemaVersion : undefined,
1129
+ harnessId: typeof reportRecord.harnessId === 'string'
1130
+ ? reportRecord.harnessId
1131
+ : typeof graph.harnessId === 'string'
1132
+ ? graph.harnessId
1133
+ : undefined,
1134
+ runtimeAgentCount: countArray(graph.runtimeAgents),
1135
+ agentFamilyCount: countArray(graph.agentFamilies),
1136
+ artifactCount: countArray(graph.artifacts),
1137
+ channelAdapterCount: countArray(graph.channelAdapters),
1138
+ identityNamespaceCount: countArray(graph.identityNamespaces),
1139
+ agentFrameworkCount: countArray(graph.agentFrameworks),
1140
+ modelProviderCount: countArray(graph.modelProviders),
1141
+ deploymentCount: countArray(graph.deployments),
1142
+ unknownCount: countArray(graph.unknowns),
1143
+ };
1144
+ }
1145
+ function countArray(value) {
1146
+ return Array.isArray(value) ? value.length : undefined;
1147
+ }
1148
+ async function resolveHarnessAnalystAdapter(context) {
1149
+ const requested = normalizeAnalystAdapter(context.flags.analyst);
1150
+ if (requested !== 'auto')
1151
+ return requested;
1152
+ const available = rankHarnessAnalysts(detectAvailableHarnessAnalysts());
1153
+ if (available.length > 0) {
1154
+ writeHumanBlock(context, renderHarnessAnalystChoice({
1155
+ detected: available.map((item) => `${analystDisplayName(item.adapter)} (${item.command})`),
1156
+ }));
1157
+ }
1158
+ if (available.length === 1) {
1159
+ writeHumanProgress(context, `Using ${analystDisplayName(available[0].adapter)} for read-only harness investigation.`);
1160
+ return available[0].adapter;
1161
+ }
1162
+ if (available.length > 1 && shouldPromptForHarnessAnalyst(context)) {
1163
+ return select({
1164
+ message: 'Choose which coding agent should investigate this harness',
1165
+ choices: available.map((item) => ({
1166
+ name: `${analystDisplayName(item.adapter)} (${item.command})`,
1167
+ value: item.adapter,
1168
+ })),
1169
+ });
1170
+ }
1171
+ if (available.length > 1) {
1172
+ writeHumanProgress(context, `Using ${analystDisplayName(available[0].adapter)} for read-only harness investigation. Pass --analyst=claude|codex|cursor to choose explicitly.`);
1173
+ return available[0].adapter;
1174
+ }
1175
+ throw new CliInputError('No external harness analyst CLI was found.', [
1176
+ 'Install/sign into Claude Code, Codex, or Cursor, then rerun `moda harness analyze`.',
1177
+ 'Set MODA_HARNESS_ANALYST_COMMAND to an explicit analyst command.',
1178
+ 'For deterministic debugging only, run `moda harness analyze --analyst=local-scan`.',
1179
+ ].join('\n'));
1180
+ }
1181
+ function detectAvailableHarnessAnalysts() {
1182
+ return EXTERNAL_ANALYST_ADAPTERS
1183
+ .map((adapter) => ({ adapter, command: resolveAnalystCommand(adapter) }))
1184
+ .filter((item) => Boolean(item.command));
1185
+ }
1186
+ function rankHarnessAnalysts(analysts) {
1187
+ return [...analysts].sort((a, b) => analystQualityScore(b.adapter) - analystQualityScore(a.adapter));
1188
+ }
1189
+ function analystQualityScore(adapter) {
1190
+ if (adapter === 'claude')
1191
+ return 3;
1192
+ if (adapter === 'codex')
1193
+ return 2;
1194
+ return 1;
1195
+ }
1196
+ function shouldPromptForHarnessAnalyst(context) {
1197
+ return context.outputMode === 'human' &&
1198
+ !context.yes &&
1199
+ process.stdin.isTTY === true &&
1200
+ !context.flags['analyst-command'] &&
1201
+ !process.env.MODA_HARNESS_ANALYST_COMMAND;
1202
+ }
1203
+ function normalizeAnalystAdapter(raw) {
1204
+ const value = (raw && raw !== 'true' ? raw : 'auto').trim().toLowerCase();
1205
+ if (value === 'auto')
1206
+ return 'auto';
1207
+ if (value === 'claude' || value === 'claude-code' || value === 'claude_code')
1208
+ return 'claude';
1209
+ if (value === 'codex' || value === 'codex-cli' || value === 'codex_cli')
1210
+ return 'codex';
1211
+ if (value === 'cursor' || value === 'cursor-cli' || value === 'cursor_cli')
1212
+ return 'cursor';
1213
+ if (value === 'local-scan' || value === 'local' || value === 'scripted' || value === 'moda')
1214
+ return 'local-scan';
1215
+ throw new CliInputError(`Unknown harness analyst '${raw}'.`, 'Use `--analyst=claude|codex|cursor|local-scan`.');
1216
+ }
1217
+ function resolveExternalHarnessAnalystInvocation(options) {
1218
+ const override = options.flags['analyst-command'] || process.env.MODA_HARNESS_ANALYST_COMMAND;
1219
+ if (override) {
1220
+ const [command, ...args] = splitCommandLine(override);
1221
+ if (!command) {
1222
+ throw new CliInputError('Analyst command override is empty.');
1223
+ }
1224
+ return { adapter: options.adapter, command, args: [...args, options.prompt], streamEvents: false };
1225
+ }
1226
+ const command = resolveAnalystCommand(options.adapter);
1227
+ if (!command) {
1228
+ throw new CliInputError(`${titleFromId(options.adapter)} analyst CLI was not found.`, `Install/sign into ${titleFromId(options.adapter)}, set MODA_HARNESS_ANALYST_COMMAND, or choose another analyst with --analyst=claude|codex|cursor.`);
1229
+ }
1230
+ if (options.adapter === 'claude') {
1231
+ const extraArgs = externalAnalystExtraArgs(options.adapter);
1232
+ return {
1233
+ adapter: options.adapter,
1234
+ command,
1235
+ args: [
1236
+ '--print',
1237
+ ...extraArgs,
1238
+ '--permission-mode',
1239
+ 'plan',
1240
+ '--allowedTools',
1241
+ 'Read,Grep,Glob,Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)',
1242
+ ...(options.stream ? ['--verbose'] : []),
1243
+ '--output-format',
1244
+ options.stream ? 'stream-json' : 'text',
1245
+ '--no-session-persistence',
1246
+ options.prompt,
1247
+ ],
1248
+ streamEvents: options.stream,
1249
+ };
1250
+ }
1251
+ if (options.adapter === 'codex') {
1252
+ return {
1253
+ adapter: options.adapter,
1254
+ command,
1255
+ args: [
1256
+ 'exec',
1257
+ '--skip-git-repo-check',
1258
+ '--sandbox',
1259
+ 'read-only',
1260
+ ...(options.stream ? ['--json'] : []),
1261
+ '-C',
1262
+ options.rootDir,
1263
+ options.prompt,
1264
+ ],
1265
+ streamEvents: options.stream,
1266
+ };
1267
+ }
1268
+ return {
1269
+ adapter: options.adapter,
1270
+ command,
1271
+ args: cursorSubcommandArgs(command, [
1272
+ '--print',
1273
+ '--trust',
1274
+ '--mode',
1275
+ 'plan',
1276
+ '--sandbox',
1277
+ 'enabled',
1278
+ '--workspace',
1279
+ options.rootDir,
1280
+ '--output-format',
1281
+ options.stream ? 'stream-json' : 'text',
1282
+ options.prompt,
1283
+ ]),
1284
+ streamEvents: options.stream,
1285
+ };
1286
+ }
1287
+ function externalAnalystExtraArgs(adapter) {
1288
+ return splitCommandLine(process.env[`MODA_HARNESS_ANALYST_${adapter.toUpperCase()}_ARGS`] ?? '');
1289
+ }
1290
+ function renderExternalHarnessAnalystPrompt(options) {
1291
+ return [
1292
+ 'You are Moda Analyst, investigating an agent harness for Moda CLI onboarding.',
1293
+ '',
1294
+ 'Do real repository investigation. Do not call `moda harness scan`, `moda harness analyze`, or rely on deterministic Moda scanner output.',
1295
+ 'Only inspect the Repository root named below and files/directories under it. Do not inspect parent directories, sibling projects, the Moda CLI implementation, or this monorepo unless the Repository root itself is that directory.',
1296
+ 'Inspect package manifests, source files, prompt files, tool definitions, skills/rules, evals, deployment config, and README/AGENTS docs as needed.',
1297
+ 'Do not read secret-looking files such as .env files, private keys, credential files, or token stores.',
1298
+ 'You are running as a read-only analyst. Do not edit source files, configuration files, git state, dependency files, or project metadata.',
1299
+ 'Prefer printing the final report JSON for Moda CLI to validate and write. If your runtime unexpectedly allows file writes, only `.moda/harness-report.json` is permitted.',
1300
+ '',
1301
+ 'Your task:',
1302
+ renderExternalAnalystProtocol(),
1303
+ '1. Identify the actual runtime agent harnesses, agent families, prompts, tools, channel adapters, identity mappings, evals, model providers, frameworks, deployments, and open questions.',
1304
+ '2. Cite every claim with local file paths, exact line numbers, and excerpts. File citations without line numbers are invalid for agent-authored reports.',
1305
+ `3. After completing investigation, print the report JSON between ${REPORT_JSON_BEGIN} and ${REPORT_JSON_END}. Moda CLI will validate it and write the local artifacts.`,
1306
+ 'Your final response must contain the sentinel-wrapped JSON report. Do not end with commentary about being read-only, do not summarize without JSON, and do not omit the sentinel markers.',
1307
+ 'If the repository is too large to fully enumerate, stop broad exploration and produce the best conservative cited report with unknowns instead of continuing indefinitely.',
1308
+ '',
1309
+ 'Final output format:',
1310
+ REPORT_JSON_BEGIN,
1311
+ '{',
1312
+ ' "...": "valid harness_report.v0.1 JSON"',
1313
+ '}',
1314
+ REPORT_JSON_END,
1315
+ '',
1316
+ 'The report must be JSON with this top-level contract:',
1317
+ '- schemaVersion: "harness_report.v0.1"',
1318
+ '- reportId, harnessId, createdAt',
1319
+ `- analyst: { "id": "moda-${options.adapter}-analyst", "adapter": "${options.adapter}", "mode": "agent", "version": "cli" }`,
1320
+ '- repo: { root: ".", name, packageManagers, languages }',
1321
+ '- source: { graphSchemaVersion: "harness.v0.1", graphHash: "", readPlan, deterministicInventory: false }',
1322
+ '- summary: numeric fields named runtimeAgentCount, agentFamilyCount, artifactCount, sharedArtifactCount, channelAdapterCount, identityNamespaceCount, environmentCount, agentFrameworkCount, modelProviderCount, deploymentCount, relationshipCount, unknownCount',
1323
+ '- graph: an exact sanitized harness.v0.1 graph with arrays for agentFamilies, runtimeAgents, developerAgentAdapters, channelAdapters, artifacts, identityNamespaces, environments, agentFrameworks, modelProviders, deployments, relationships, unknowns',
1324
+ '- claims: cited claims about graph items. Each claim must include kind, subjectId, title, summary, confidence, inference, and citationIds; kind should be one of runtime_agent, agent_family, developer_agent_adapter, artifact, channel_adapter, identity_namespace, environment, agent_framework, model_provider, deployment, relationship, unknown. subjectId should be the id of the runtime agent, agent family, developer adapter, artifact, provider, adapter, deployment, relationship, or unknown being claimed.',
1325
+ '- citations: evidence records with id, type, reason, and excerpt. For type:"file" or type:"generated_doc", include path, line, reason, and excerpt matching that exact line. You may also use type:"command_output" or type:"production_evidence" with excerpt when evidence came from a command or production source.',
1326
+ `- syncPlan: { command: "moda harness sync --from-report ${HARNESS_REPORT_PATH}", dryRunCommand: "moda harness sync --from-report ${HARNESS_REPORT_PATH} --dry-run", uploads: string[], privacy: string[] }`,
1327
+ '- unknowns: report-level open questions with id, question, and citationIds',
1328
+ '',
1329
+ 'Use exact field names and numeric confidence values from 0 to 1. Do not write string confidence values like "high". Do not use confidence >= 0.90 unless the claim is direct evidence from the cited line(s).',
1330
+ 'Each graph item with evidence must use evidence: [{ path, reason }], not citationIds/citations.',
1331
+ 'Runtime agents must include id, name, kind:"runtime_agent", root, entrypointIds, channelAdapterIds, artifactIds, identityNamespaceIds, deploymentIds, frameworkIds, modelProviderIds, confidence, evidence.',
1332
+ 'Artifacts must include id, type, name, scope, usedByAgentIds, confidence, evidence, and optional sourcePath/contentHash/ownedByAgentId.',
1333
+ 'Agent families must include id, name, runtimeAgentIds, sharedArtifactIds, confidence, evidence.',
1334
+ 'Frameworks/providers must include packageNames and runtimeAgentIds arrays; providers also include envVars and modelHints arrays.',
1335
+ 'Unknowns in graph must include id, question, candidateIds, evidence. Report-level unknowns must include id, question, citationIds.',
1336
+ 'Do not make every claim about the repo or harness id. Claims should name the specific graph item they support.',
1337
+ '',
1338
+ 'Set source.graphHash to an empty string if you cannot compute Moda stable graph hashing; Moda CLI will still validate citations and schema.',
1339
+ options.tenantId ? `Tenant ID to include in graph.tenant: ${options.tenantId}` : 'No tenant ID was provided for this analysis.',
1340
+ `Repository root: ${options.rootDir}`,
1341
+ renderAnalystPreflightSection(options.rootDir),
1342
+ '',
1343
+ 'Be conservative. If something is unclear, put it in unknowns instead of inventing an agent.',
1344
+ ].filter(Boolean).join('\n');
1345
+ }
1346
+ function renderExternalAnalystProtocol() {
1347
+ const protocol = process.env.MODA_HARNESS_ANALYST_PROTOCOL?.trim().toLowerCase();
1348
+ if (protocol === 'classic' || protocol === 'none' || protocol === '0')
1349
+ return '';
1350
+ return [
1351
+ 'Focused investigation protocol:',
1352
+ '- First build a quick repo map with read-only commands such as pwd, git ls-files, rg, and targeted manifest/doc reads.',
1353
+ '- Prioritize runtime entrypoints, prompt/tool definitions, framework/provider calls, channel adapters, evals, and deploy config over exhaustive file enumeration.',
1354
+ '- If the repository is a framework, SDK, or examples collection, do not enumerate every example as a production runtime agent. Model representative harnesses, reusable frameworks, and unknowns conservatively with citations.',
1355
+ '- For every claimed runtime agent, read and cite at least one concrete entrypoint or orchestration line. Add prompt/tool/config citations when available.',
1356
+ '- Stop exploring once the cited graph is sufficiently supported. Use unknowns for unresolved questions instead of continuing broad exploration.',
1357
+ '- Prefer a compact report: target 8-16 high-signal claims and 12-40 citations unless the repo clearly needs more.',
1358
+ ].join('\n');
1359
+ }
1360
+ function renderAnalystPreflightSection(rootDir) {
1361
+ if (process.env.MODA_HARNESS_ANALYST_PREFLIGHT !== '1')
1362
+ return '';
1363
+ const paths = listAnalystPreflightPaths(rootDir);
1364
+ if (paths.length === 0)
1365
+ return '';
1366
+ const groups = {
1367
+ manifests: paths.filter((path) => isManifestLikePath(path)).slice(0, 60),
1368
+ docs: paths.filter((path) => isDocLikePath(path)).slice(0, 40),
1369
+ source: paths.filter((path) => isAgentNavigationPath(path)).slice(0, 160),
1370
+ deploy: paths.filter((path) => isPreflightDeployLikePath(path)).slice(0, 50),
1371
+ evals: paths.filter((path) => isPreflightEvalLikePath(path)).slice(0, 50),
1372
+ };
1373
+ const lines = [
1374
+ '',
1375
+ 'Navigation aid (not evidence):',
1376
+ 'Moda generated this path-only map to help you navigate faster. Do not cite it and do not trust it for claims. Read the real files and cite exact lines before asserting anything.',
1377
+ ...renderPreflightGroup('Manifests/config', groups.manifests),
1378
+ ...renderPreflightGroup('Docs/rules', groups.docs),
1379
+ ...renderPreflightGroup('Likely source/prompt/tool paths', groups.source),
1380
+ ...renderPreflightGroup('Deploy/CI', groups.deploy),
1381
+ ...renderPreflightGroup('Evals/tests', groups.evals),
1382
+ ];
1383
+ return lines.join('\n');
1384
+ }
1385
+ function renderPreflightGroup(label, paths) {
1386
+ if (paths.length === 0)
1387
+ return [];
1388
+ return [`${label}:`, ...paths.map((path) => `- ${path}`)];
1389
+ }
1390
+ function listAnalystPreflightPaths(rootDir) {
1391
+ const gitPaths = readGitTrackedPaths(rootDir);
1392
+ const paths = gitPaths.length > 0 ? gitPaths : readDirectoryPreflightPaths(rootDir);
1393
+ return [...new Set(paths.map(normalizePath))]
1394
+ .filter((path) => path && !shouldSkipPreflightPath(path))
1395
+ .sort();
1396
+ }
1397
+ function readGitTrackedPaths(rootDir) {
1398
+ try {
1399
+ return execFileSync('git', ['ls-files', '-co', '--exclude-standard'], {
1400
+ cwd: rootDir,
1401
+ encoding: 'utf8',
1402
+ maxBuffer: 4 * 1024 * 1024,
1403
+ }).split(/\r?\n/).filter(Boolean);
1404
+ }
1405
+ catch {
1406
+ return [];
1407
+ }
1408
+ }
1409
+ function readDirectoryPreflightPaths(rootDir) {
1410
+ const paths = [];
1411
+ const stack = ['.'];
1412
+ while (stack.length > 0 && paths.length < 2_000) {
1413
+ const current = stack.pop() ?? '.';
1414
+ const absolute = current === '.' ? rootDir : join(rootDir, current);
1415
+ let entries;
1416
+ try {
1417
+ entries = readdirSync(absolute);
1418
+ }
1419
+ catch {
1420
+ continue;
1421
+ }
1422
+ for (const entry of entries) {
1423
+ const child = current === '.' ? entry : `${current}/${entry}`;
1424
+ if (shouldSkipPreflightPath(child))
1425
+ continue;
1426
+ const childAbsolute = join(rootDir, child);
1427
+ let stat;
1428
+ try {
1429
+ stat = lstatSync(childAbsolute);
1430
+ }
1431
+ catch {
1432
+ continue;
1433
+ }
1434
+ if (stat.isSymbolicLink())
1435
+ continue;
1436
+ if (stat.isDirectory()) {
1437
+ stack.push(child);
1438
+ }
1439
+ else {
1440
+ paths.push(child);
1441
+ }
1442
+ }
1443
+ }
1444
+ return paths;
1445
+ }
1446
+ function shouldSkipPreflightPath(path) {
1447
+ const normalized = normalizePath(path);
1448
+ if (!normalized || normalized.startsWith('.moda/'))
1449
+ return true;
1450
+ const parts = normalized.split('/');
1451
+ if (parts.some((part) => SKIPPED_DIRS.has(part)))
1452
+ return true;
1453
+ return isSecretishPath(normalized);
1454
+ }
1455
+ function isManifestLikePath(path) {
1456
+ const name = basename(path).toLowerCase();
1457
+ return /^(package\.json|pyproject\.toml|requirements.*\.txt|uv\.lock|poetry\.lock|pnpm-lock\.yaml|bun\.lockb?|yarn\.lock|cargo\.toml|go\.mod|deno\.json|turbo\.json|langgraph\.json|agents\.json|config\.(ts|js|json|yaml|yml|toml))$/.test(name);
1458
+ }
1459
+ function isDocLikePath(path) {
1460
+ const lower = path.toLowerCase();
1461
+ return /(^|\/)(readme|agents|claude|cursor|codex|rules|skills?)(\.[\w-]+)?$/.test(lower) ||
1462
+ lower.includes('/skills/') ||
1463
+ lower.includes('/rules/');
1464
+ }
1465
+ function isAgentNavigationPath(path) {
1466
+ const lower = path.toLowerCase();
1467
+ return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|cs|java|kt|rb|php|md|yaml|yml|toml|json)$/.test(lower) &&
1468
+ /(agent|assistant|harness|prompt|tool|workflow|graph|model|provider|llm|chat|completion|planner|research|memory|eval|guardrail|router|orchestr|crew|swarm|bot)/.test(lower);
1469
+ }
1470
+ function isPreflightDeployLikePath(path) {
1471
+ const lower = path.toLowerCase();
1472
+ return lower.startsWith('.github/workflows/') ||
1473
+ lower.includes('/deploy') ||
1474
+ lower.includes('/docker') ||
1475
+ /(dockerfile|compose\.ya?ml|vercel\.json|netlify\.toml|wrangler\.toml|modal\.py|fly\.toml|render\.ya?ml)$/.test(lower);
1476
+ }
1477
+ function isPreflightEvalLikePath(path) {
1478
+ const lower = path.toLowerCase();
1479
+ return /(^|\/)(test|tests|eval|evals|bench|benchmarks|fixtures)(\/|$)/.test(lower) ||
1480
+ lower.includes('eval') ||
1481
+ lower.includes('benchmark');
1482
+ }
1483
+ function resolveAnalystCommand(adapter) {
1484
+ const envSpecific = process.env[`MODA_HARNESS_ANALYST_${adapter.toUpperCase()}_COMMAND`];
1485
+ if (envSpecific)
1486
+ return splitCommandLine(envSpecific)[0];
1487
+ if (adapter === 'claude')
1488
+ return findOnPath('claude');
1489
+ if (adapter === 'codex')
1490
+ return findOnPath('codex') || findOnPath('/Applications/Codex.app/Contents/Resources/codex');
1491
+ return findOnPath('cursor-agent') ||
1492
+ findOnPath('cursor-cli') ||
1493
+ findOnPath('cursor') ||
1494
+ findOnPath('/Applications/Cursor.app/Contents/Resources/app/bin/cursor');
1495
+ }
1496
+ function cursorSubcommandArgs(command, args) {
1497
+ const name = basename(command);
1498
+ return name === 'cursor' || name === 'cursor-cli' ? ['agent', ...args] : args;
1499
+ }
1500
+ function findOnPath(command) {
1501
+ if (command.includes('/') && existsSync(command))
1502
+ return command;
1503
+ for (const dir of (process.env.PATH ?? '').split(delimiter).filter(Boolean)) {
1504
+ const candidate = join(dir, command);
1505
+ if (existsSync(candidate))
1506
+ return candidate;
1507
+ }
1508
+ return undefined;
1509
+ }
1510
+ function splitCommandLine(commandLine) {
1511
+ return commandLine.trim().split(/\s+/).filter(Boolean);
1512
+ }
1513
+ function truncateText(value, maxLength) {
1514
+ const compact = value.replace(/\s+/g, ' ').trim();
1515
+ return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}...` : compact;
1516
+ }
1517
+ export function scanHarness(options = {}) {
1518
+ const rootDir = resolve(options.rootDir ?? process.cwd());
1519
+ const skipped = [];
1520
+ const files = inventoryFiles(rootDir, skipped);
1521
+ const annotations = files.flatMap((file) => parseAnnotations(file));
1522
+ const repo = detectRepo(rootDir, files);
1523
+ const readPlan = buildHarnessReadPlan(rootDir, options.pathWasExplicit === true);
1524
+ const graph = {
1525
+ schemaVersion: SCHEMA_VERSION,
1526
+ harnessId: stableId('harness', repo.name || basename(rootDir) || 'repo'),
1527
+ tenant: options.tenantId
1528
+ ? {
1529
+ tenantId: options.tenantId,
1530
+ source: options.tenantSource ?? 'scan_option',
1531
+ }
1532
+ : undefined,
1533
+ repo,
1534
+ scan: {
1535
+ createdAt: new Date().toISOString(),
1536
+ scannerVersion: options.scannerVersion ?? 'moda-cli',
1537
+ mode: options.dryRun ? 'dry-run' : 'permissioned',
1538
+ rootPaths: ['.'],
1539
+ readPlan,
1540
+ skipped,
1541
+ },
1542
+ agentFamilies: [],
1543
+ runtimeAgents: [],
1544
+ developerAgentAdapters: detectDeveloperAgentAdapters(files),
1545
+ channelAdapters: [],
1546
+ artifacts: [],
1547
+ identityNamespaces: [],
1548
+ environments: detectEnvironments(files),
1549
+ agentFrameworks: detectAgentFrameworks(files),
1550
+ modelProviders: detectModelProviders(files),
1551
+ deployments: [],
1552
+ relationships: [],
1553
+ unknowns: [],
1554
+ };
1555
+ applyAnnotations(graph, annotations);
1556
+ applyHeuristics(graph, files);
1557
+ linkFrameworksAndProviders(graph);
1558
+ finalizeGraph(graph);
1559
+ return graph;
1560
+ }
1561
+ export function summarizeHarness(graph) {
1562
+ return {
1563
+ schemaVersion: graph.schemaVersion,
1564
+ harnessId: graph.harnessId,
1565
+ tenantId: graph.tenant?.tenantId,
1566
+ runtimeAgentCount: graph.runtimeAgents.length,
1567
+ agentFamilyCount: graph.agentFamilies.length,
1568
+ sharedArtifactCount: graph.artifacts.filter((artifact) => artifact.scope === 'shared').length,
1569
+ channelAdapterCount: graph.channelAdapters.length,
1570
+ identityNamespaceCount: graph.identityNamespaces.length,
1571
+ agentFrameworkCount: graph.agentFrameworks.length,
1572
+ modelProviderCount: graph.modelProviders.length,
1573
+ deploymentCount: graph.deployments.length,
1574
+ unknownCount: graph.unknowns.length,
1575
+ };
1576
+ }
1577
+ function buildCommandScanPlan(context, rootDir) {
1578
+ const explicitPath = context.flags.path !== undefined;
1579
+ const rel = normalizePath(relative(context.cwd, rootDir)) || '.';
1580
+ const scope = explicitPath && rel !== '.' ? 'narrow' : 'broad';
1581
+ const machineReadable = context.outputMode !== 'human';
1582
+ const approvalRequired = scope === 'broad' && !context.yes && !machineReadable && process.stdin.isTTY === true;
1583
+ return {
1584
+ scope,
1585
+ root: rel,
1586
+ approval: {
1587
+ required: approvalRequired,
1588
+ granted: !approvalRequired,
1589
+ reason: approvalRequired
1590
+ ? 'broad scan requires confirmation'
1591
+ : machineReadable
1592
+ ? 'machine-readable output'
1593
+ : context.yes
1594
+ ? '--yes'
1595
+ : scope === 'narrow'
1596
+ ? 'narrow path'
1597
+ : 'non-interactive terminal',
1598
+ },
1599
+ readPlan: buildHarnessReadPlan(rootDir, explicitPath),
1600
+ };
1601
+ }
1602
+ async function approveScanIfNeeded(scanPlan, options = {}) {
1603
+ if (!scanPlan.approval.required)
1604
+ return;
1605
+ if (options.context?.outputMode === 'human') {
1606
+ writeHumanBlock(options.context, renderHarnessAnalystPreflight({
1607
+ root: scanPlan.root,
1608
+ willRead: scanPlan.readPlan.willRead,
1609
+ willSkip: scanPlan.readPlan.willSkip,
1610
+ }));
1611
+ }
1612
+ const approved = await confirm({
1613
+ message: options.message ?? 'Scan this project to build a Moda harness manifest?',
1614
+ default: true,
1615
+ });
1616
+ scanPlan.approval.granted = approved;
1617
+ scanPlan.approval.reason = approved ? 'confirmed by user' : 'declined by user';
1618
+ if (!approved) {
1619
+ throw new CliInputError(options.declinedMessage ?? 'Harness scan was not approved.', options.declinedHint ?? 'Run `moda harness scan --path=<dir>` for a narrower scan, or rerun with `--yes` after reviewing the read plan.');
1620
+ }
1621
+ }
1622
+ function writeHumanProgress(context, message) {
1623
+ if (context.outputMode !== 'human')
1624
+ return;
1625
+ if (context.quiet && context.flags.progress !== 'true')
1626
+ return;
1627
+ console.error(renderHarnessProgress(message));
1628
+ }
1629
+ function writeHumanBlock(context, message) {
1630
+ if (context.outputMode !== 'human')
1631
+ return;
1632
+ if (context.quiet && context.flags.progress !== 'true')
1633
+ return;
1634
+ console.error(message);
1635
+ }
1636
+ function buildHarnessReadPlan(rootDir, pathWasExplicit) {
1637
+ return {
1638
+ scope: pathWasExplicit ? 'narrow' : 'broad',
1639
+ rootPaths: [normalizePath(relative(rootDir, rootDir)) || '.'],
1640
+ willRead: [
1641
+ 'package manifests',
1642
+ 'source files under the selected path',
1643
+ 'prompt files',
1644
+ 'tool definitions',
1645
+ 'skill/rule files',
1646
+ 'eval/test files',
1647
+ 'deployment and CI config',
1648
+ ],
1649
+ willSkip: [
1650
+ 'secret-looking files',
1651
+ 'symlinks',
1652
+ 'node_modules and build output',
1653
+ '.moda runtime files',
1654
+ 'large files over 512KB',
1655
+ ],
1656
+ skipsSecretFiles: true,
1657
+ followsSymlinks: false,
1658
+ uploadsSourceByDefault: false,
1659
+ };
1660
+ }
1661
+ function resolveHarnessTenant(flags, context) {
1662
+ const flagTenant = flags['tenant-id'] || flags.tenant;
1663
+ if (flagTenant && flagTenant !== 'true') {
1664
+ return {
1665
+ tenantId: flagTenant,
1666
+ source: 'flag',
1667
+ };
1668
+ }
1669
+ const tenantId = resolveTenantId({ profile: context?.profile, env: context?.env });
1670
+ if (!tenantId)
1671
+ return undefined;
1672
+ return {
1673
+ tenantId,
1674
+ source: 'env_or_config',
1675
+ };
1676
+ }
1677
+ function createHarnessSyncPlan(options) {
1678
+ const profile = getActiveProfile({
1679
+ profile: options.context.profile,
1680
+ env: options.context.env,
1681
+ });
1682
+ const tenantId = options.graph.tenant?.tenantId ??
1683
+ resolveTenantId({ profile: options.context.profile, env: options.context.env });
1684
+ return {
1685
+ endpoint: '/api/harnesses/sync',
1686
+ resource: 'harness',
1687
+ source: options.source,
1688
+ profile: profile.name,
1689
+ tenantId,
1690
+ rootPath: options.rootDir,
1691
+ sourceCommit: options.sourceCommit,
1692
+ harnessId: options.graph.harnessId,
1693
+ localGraphHash: options.localGraphHash,
1694
+ counts: syncCounts(options.graph),
1695
+ uploads: options.source === 'approved_report'
1696
+ ? [
1697
+ 'approved harness graph metadata',
1698
+ 'runtime agents, families, artifacts, relationships, citations-derived graph shape',
1699
+ 'graph hash and source commit',
1700
+ ]
1701
+ : [
1702
+ 'harness graph metadata',
1703
+ 'runtime agents, families, artifacts, relationships',
1704
+ 'graph hash and source commit',
1705
+ ],
1706
+ localWrites: options.localWrites,
1707
+ privacy: [
1708
+ 'No secret-looking files are read by the harness scanner.',
1709
+ 'Source file contents are not uploaded by harness sync by default.',
1710
+ 'Prompt/tool/skill artifacts sync as metadata, ids, paths, hashes, and relationships unless a separate resource command uploads content.',
1711
+ ],
1712
+ };
1713
+ }
1714
+ function plannedHarnessArtifactWrites(context) {
1715
+ return context.flags.summary === 'true'
1716
+ ? [HARNESS_PATH, HARNESS_SUMMARY_PATH, SYNC_STATE_PATH]
1717
+ : [HARNESS_PATH, SYNC_STATE_PATH];
1718
+ }
1719
+ function syncCounts(graph) {
1720
+ return {
1721
+ runtimeAgents: graph.runtimeAgents.length,
1722
+ agentFamilies: graph.agentFamilies.length,
1723
+ developerAgentAdapters: graph.developerAgentAdapters.length,
1724
+ channelAdapters: graph.channelAdapters.length,
1725
+ artifacts: graph.artifacts.length,
1726
+ identityNamespaces: graph.identityNamespaces.length,
1727
+ environments: graph.environments.length,
1728
+ agentFrameworks: graph.agentFrameworks.length,
1729
+ modelProviders: graph.modelProviders.length,
1730
+ deployments: graph.deployments.length,
1731
+ relationships: graph.relationships.length,
1732
+ unknowns: graph.unknowns.length,
1733
+ };
1734
+ }
1735
+ function plannedDryRunRemote(plan) {
1736
+ return {
1737
+ tenantId: plan.tenantId ?? 'unresolved',
1738
+ harnessId: plan.harnessId,
1739
+ harnessKey: plan.harnessId,
1740
+ versionId: '',
1741
+ graphHash: plan.localGraphHash,
1742
+ changed: false,
1743
+ dryRun: true,
1744
+ counts: plan.counts,
1745
+ };
1746
+ }
1747
+ async function syncHarnessWithRemote(context, body) {
1748
+ const profileOptions = { profile: context.profile, env: context.env };
1749
+ const hasApiKey = Boolean(resolveApiKey(profileOptions));
1750
+ if (context.dryRun && !hasApiKey)
1751
+ return undefined;
1752
+ return await callControlAPI('/harnesses/sync', {
1753
+ method: 'POST',
1754
+ body: JSON.stringify({
1755
+ ...body,
1756
+ dryRun: context.dryRun,
1757
+ }),
1758
+ }, profileOptions);
1759
+ }
1760
+ export function readHarnessSyncState(rootDir) {
1761
+ const path = join(rootDir, SYNC_STATE_PATH);
1762
+ if (!existsSync(path))
1763
+ return undefined;
1764
+ try {
1765
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
1766
+ if (!isRecord(parsed) || parsed.schemaVersion !== 'sync_state.v0.1') {
1767
+ return undefined;
1768
+ }
1769
+ return parsed;
1770
+ }
1771
+ catch {
1772
+ return undefined;
1773
+ }
1774
+ }
1775
+ function writeHarnessSyncState(rootDir, state) {
1776
+ const path = join(rootDir, SYNC_STATE_PATH);
1777
+ mkdirSync(dirname(path), { recursive: true });
1778
+ writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
1779
+ return SYNC_STATE_PATH;
1780
+ }
1781
+ function selectedRuntimeAgentId(flags) {
1782
+ if (flags['agent-id'])
1783
+ return flags['agent-id'];
1784
+ if (flags.agent && flags.agent !== 'true')
1785
+ return flags.agent;
1786
+ return undefined;
1787
+ }
1788
+ function loadOrScanHarness(rootDir, tenant) {
1789
+ const manifestPath = join(rootDir, HARNESS_PATH);
1790
+ return existsSync(manifestPath)
1791
+ ? readHarnessGraph(manifestPath)
1792
+ : scanHarness({
1793
+ rootDir,
1794
+ tenantId: tenant?.tenantId,
1795
+ tenantSource: tenant?.source,
1796
+ });
1797
+ }
1798
+ function readHarnessGraph(manifestPath) {
1799
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
1800
+ }
1801
+ function writeHarnessArtifacts(graph, rootDir, options = {}) {
1802
+ const modaDir = join(rootDir, MODA_DIR);
1803
+ mkdirSync(modaDir, { recursive: true });
1804
+ const manifestPath = join(rootDir, HARNESS_PATH);
1805
+ writeFileSync(manifestPath, `${JSON.stringify(graph, null, 2)}\n`);
1806
+ const written = [HARNESS_PATH];
1807
+ if (options.summary) {
1808
+ written.push(writeHarnessSummary(graph, rootDir));
1809
+ }
1810
+ return written;
1811
+ }
1812
+ function writeHarnessSummary(graph, rootDir) {
1813
+ const summaryPath = join(rootDir, HARNESS_SUMMARY_PATH);
1814
+ mkdirSync(dirname(summaryPath), { recursive: true });
1815
+ writeFileSync(summaryPath, renderHarnessMarkdown(graph));
1816
+ return HARNESS_SUMMARY_PATH;
1817
+ }
1818
+ function renderHarnessMarkdown(graph) {
1819
+ const summary = summarizeHarness(graph);
1820
+ const lines = [
1821
+ '# Moda Harness',
1822
+ '',
1823
+ `Harness ID: ${graph.harnessId}`,
1824
+ `Tenant ID: ${graph.tenant?.tenantId ?? 'none'}`,
1825
+ `Runtime agents: ${summary.runtimeAgentCount}`,
1826
+ `Agent families: ${summary.agentFamilyCount}`,
1827
+ `Shared artifacts: ${summary.sharedArtifactCount}`,
1828
+ `Channel adapters: ${summary.channelAdapterCount}`,
1829
+ `Identity namespaces: ${summary.identityNamespaceCount}`,
1830
+ `Agent frameworks: ${summary.agentFrameworkCount}`,
1831
+ `Model providers: ${summary.modelProviderCount}`,
1832
+ '',
1833
+ '## What This Harness Does',
1834
+ '',
1835
+ ...renderHarnessPurposeLines(graph),
1836
+ '',
1837
+ '## Frameworks And Models',
1838
+ '',
1839
+ ...renderFrameworkAndProviderLines(graph),
1840
+ '',
1841
+ '## Moda Data Flow',
1842
+ '',
1843
+ '- `moda harness scan` reads local harness structure and writes `.moda/harness.json`.',
1844
+ '- `moda harness sync` uploads the graph metadata, hashes, and relationships to Moda Cloud.',
1845
+ '- Prompt, skill, eval, deployment, framework, and provider detections become the local context Moda uses for future recommendations.',
1846
+ '- Source content and secret-bearing files are not uploaded by default during scan.',
1847
+ '',
1848
+ '## Runtime Agents',
1849
+ '',
1850
+ ];
1851
+ for (const agent of graph.runtimeAgents) {
1852
+ const artifacts = graph.artifacts.filter((artifact) => agent.artifactIds.includes(artifact.id));
1853
+ lines.push(`### ${agent.name}`, '', `- ID: ${agent.id}`, `- Root: ${agent.root}`, `- Purpose: ${agent.purpose ?? 'unknown'}`, `- Family: ${agent.familyId ?? 'none'}`, `- Channels: ${agent.channelAdapterIds.join(', ') || 'none'}`, `- Frameworks: ${(agent.frameworkIds ?? []).join(', ') || 'unknown'}`, `- Model providers: ${(agent.modelProviderIds ?? []).join(', ') || 'unknown'}`, `- Artifacts: ${artifacts.map((artifact) => artifact.name).join(', ') || 'none'}`, `- Confidence: ${agent.confidence.toFixed(2)}`, '');
1854
+ }
1855
+ if (graph.agentFamilies.length > 0) {
1856
+ lines.push('## Agent Families', '');
1857
+ for (const family of graph.agentFamilies) {
1858
+ lines.push(`### ${family.name}`, '', `- ID: ${family.id}`, `- Agents: ${family.runtimeAgentIds.join(', ') || 'none'}`, `- Shared artifacts: ${family.sharedArtifactIds.join(', ') || 'none'}`, '');
1859
+ }
1860
+ }
1861
+ if (graph.unknowns.length > 0) {
1862
+ lines.push('## Open Questions', '');
1863
+ for (const unknown of graph.unknowns) {
1864
+ lines.push(`- ${unknown.question}`);
1865
+ }
1866
+ lines.push('');
1867
+ }
1868
+ return `${lines.join('\n')}\n`;
1869
+ }
1870
+ function renderHarnessPurposeLines(graph) {
1871
+ if (graph.runtimeAgents.length === 0) {
1872
+ return ['- No runtime agents were confidently detected yet.'];
1873
+ }
1874
+ return graph.runtimeAgents.map((agent) => {
1875
+ const channels = agent.channelAdapterIds
1876
+ .map((id) => graph.channelAdapters.find((channel) => channel.id === id)?.name ?? titleFromId(id))
1877
+ .join(', ');
1878
+ const purpose = agent.purpose ?? inferPurposeFromAgentShape(graph, agent);
1879
+ return `- ${agent.name}: ${purpose}${channels ? ` (${channels})` : ''}.`;
1880
+ });
1881
+ }
1882
+ function inferPurposeFromAgentShape(graph, agent) {
1883
+ const prompts = graph.artifacts
1884
+ .filter((artifact) => agent.artifactIds.includes(artifact.id) && artifact.type === 'prompt')
1885
+ .map((artifact) => artifact.name);
1886
+ if (prompts.length > 0)
1887
+ return `Uses ${prompts.join(', ')}`;
1888
+ if (agent.channelAdapterIds.length > 0) {
1889
+ return `Handles ${agent.channelAdapterIds.map((id) => titleFromId(id)).join(', ')} traffic`;
1890
+ }
1891
+ return 'Runtime agent detected from local code';
1892
+ }
1893
+ function renderFrameworkAndProviderLines(graph) {
1894
+ const lines = [];
1895
+ if (graph.agentFrameworks.length > 0) {
1896
+ lines.push(`- Frameworks: ${graph.agentFrameworks.map((framework) => framework.name).join(', ')}`);
1897
+ }
1898
+ else {
1899
+ lines.push('- Frameworks: unknown');
1900
+ }
1901
+ if (graph.modelProviders.length > 0) {
1902
+ lines.push(`- Model providers: ${graph.modelProviders.map((provider) => provider.name).join(', ')}`);
1903
+ for (const provider of graph.modelProviders) {
1904
+ if (provider.modelHints.length > 0) {
1905
+ lines.push(`- ${provider.name} model hints: ${provider.modelHints.join(', ')}`);
1906
+ }
1907
+ }
1908
+ }
1909
+ else {
1910
+ lines.push('- Model providers: unknown');
1911
+ }
1912
+ return lines;
1913
+ }
1914
+ export function graphForSync(graph) {
1915
+ return sortJson({
1916
+ ...graph,
1917
+ scan: {
1918
+ ...graph.scan,
1919
+ createdAt: undefined,
1920
+ scannerVersion: undefined,
1921
+ skipped: undefined,
1922
+ },
1923
+ });
1924
+ }
1925
+ function hashStableJson(value) {
1926
+ return hashText(JSON.stringify(sortJson(value)));
1927
+ }
1928
+ function sortJson(value) {
1929
+ if (Array.isArray(value))
1930
+ return value.map((item) => sortJson(item));
1931
+ if (value && typeof value === 'object') {
1932
+ return Object.fromEntries(Object.entries(value)
1933
+ .sort(([a], [b]) => a.localeCompare(b))
1934
+ .map(([key, item]) => [key, sortJson(item)]));
1935
+ }
1936
+ return value;
1937
+ }
1938
+ function getGitCommit(rootDir) {
1939
+ try {
1940
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
1941
+ cwd: rootDir,
1942
+ encoding: 'utf8',
1943
+ stdio: ['ignore', 'pipe', 'ignore'],
1944
+ }).trim() || undefined;
1945
+ }
1946
+ catch {
1947
+ return undefined;
1948
+ }
1949
+ }
1950
+ function explainAgent(graph, agent) {
1951
+ return {
1952
+ agent,
1953
+ family: agent.familyId
1954
+ ? graph.agentFamilies.find((family) => family.id === agent.familyId) ?? null
1955
+ : null,
1956
+ channels: graph.channelAdapters.filter((channel) => agent.channelAdapterIds.includes(channel.id)),
1957
+ artifacts: graph.artifacts.filter((artifact) => agent.artifactIds.includes(artifact.id)),
1958
+ identities: graph.identityNamespaces.filter((identity) => agent.identityNamespaceIds.includes(identity.id)),
1959
+ deployments: graph.deployments.filter((deployment) => agent.deploymentIds.includes(deployment.id)),
1960
+ relationships: graph.relationships.filter((relationship) => relationship.from === agent.id || relationship.to === agent.id),
1961
+ };
1962
+ }
1963
+ function inventoryFiles(rootDir, skipped) {
1964
+ const files = [];
1965
+ const rootReal = resolve(rootDir);
1966
+ const maxFiles = numericEnv('MODA_HARNESS_MAX_FILES', DEFAULT_MAX_INVENTORY_FILES);
1967
+ const maxTextBytes = numericEnv('MODA_HARNESS_MAX_TEXT_BYTES', DEFAULT_MAX_INVENTORY_TEXT_BYTES);
1968
+ let textBytes = 0;
1969
+ function walk(current) {
1970
+ const rel = normalizePath(relative(rootReal, current)) || '.';
1971
+ let stat;
1972
+ try {
1973
+ stat = lstatSync(current);
1974
+ }
1975
+ catch {
1976
+ recordSkipped(skipped, { path: rel, reason: 'stat failed' });
1977
+ return;
1978
+ }
1979
+ if (stat.isSymbolicLink()) {
1980
+ recordSkipped(skipped, { path: rel, reason: 'symlink skipped for safety' });
1981
+ return;
1982
+ }
1983
+ if (stat.isDirectory()) {
1984
+ if (rel !== '.' && shouldSkipDirectory(rel, current)) {
1985
+ recordSkipped(skipped, { path: rel, reason: 'ignored directory' });
1986
+ return;
1987
+ }
1988
+ for (const entry of readdirSync(current).sort()) {
1989
+ walk(join(current, entry));
1990
+ }
1991
+ return;
1992
+ }
1993
+ if (!stat.isFile())
1994
+ return;
1995
+ if (stat.size > MAX_SCANNED_FILE_BYTES) {
1996
+ recordSkipped(skipped, { path: rel, reason: 'file too large for harness scan' });
1997
+ return;
1998
+ }
1999
+ if (isSecretishPath(rel)) {
2000
+ recordSkipped(skipped, { path: rel, reason: 'secret-bearing file skipped' });
2001
+ return;
2002
+ }
2003
+ if (!shouldInventoryFile(rel))
2004
+ return;
2005
+ if (files.length >= maxFiles) {
2006
+ throwHarnessScanTooBroad(rootReal, {
2007
+ files: files.length,
2008
+ textBytes,
2009
+ lastPath: rel,
2010
+ maxFiles,
2011
+ maxTextBytes,
2012
+ });
2013
+ }
2014
+ const item = { path: rel, absPath: current, rootDir: rootReal };
2015
+ if (isTextFile(rel)) {
2016
+ if (textBytes + stat.size > maxTextBytes) {
2017
+ recordSkipped(skipped, { path: rel, reason: 'scan text budget exceeded' });
2018
+ }
2019
+ else {
2020
+ try {
2021
+ item.text = readFileSync(current, 'utf8');
2022
+ textBytes += Buffer.byteLength(item.text, 'utf8');
2023
+ }
2024
+ catch {
2025
+ recordSkipped(skipped, { path: rel, reason: 'text read failed' });
2026
+ }
2027
+ }
2028
+ }
2029
+ files.push(item);
2030
+ }
2031
+ walk(rootReal);
2032
+ return files;
2033
+ }
2034
+ function shouldSkipDirectory(rel, absPath) {
2035
+ if (SKIPPED_DIRS.has(basename(absPath)))
2036
+ return true;
2037
+ return SKIPPED_PATH_PREFIXES.some((prefix) => rel === prefix || rel.startsWith(`${prefix}/`));
2038
+ }
2039
+ function shouldInventoryFile(path) {
2040
+ const normalized = normalizePath(path);
2041
+ const base = basename(normalized);
2042
+ const ext = extname(normalized).toLowerCase();
2043
+ if (isSourceFile(normalized))
2044
+ return true;
2045
+ if (isPackageOrLockfile(normalized))
2046
+ return true;
2047
+ if (isPromptLikePath(normalized))
2048
+ return true;
2049
+ if (isSkillLikePath(normalized))
2050
+ return true;
2051
+ if (isEvalLikePath(normalized))
2052
+ return true;
2053
+ if (isDeploymentLikePath(normalized))
2054
+ return true;
2055
+ if (isDeveloperAgentConfigPath(normalized))
2056
+ return true;
2057
+ if (base === 'README.md' || base === 'AGENTS.md')
2058
+ return true;
2059
+ return ext === '.toml' && (base === 'pyproject.toml' || base === 'wrangler.toml');
2060
+ }
2061
+ function isSourceFile(path) {
2062
+ return /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(path);
2063
+ }
2064
+ function isPackageOrLockfile(path) {
2065
+ return path === 'package.json' ||
2066
+ path === 'pyproject.toml' ||
2067
+ path === 'bun.lock' ||
2068
+ path === 'bun.lockb' ||
2069
+ path === 'pnpm-lock.yaml' ||
2070
+ path === 'yarn.lock';
2071
+ }
2072
+ function isPromptLikePath(path) {
2073
+ return /(^|\/)prompts?\//i.test(path) &&
2074
+ /\.(prompt\.)?(md|json|ya?ml)$/i.test(path);
2075
+ }
2076
+ function isSkillLikePath(path) {
2077
+ return /(^|\/)(\.claude\/skills|\.cursor\/rules|skills?)\//i.test(path) &&
2078
+ /\.(md|mdc)$/i.test(path);
2079
+ }
2080
+ function isEvalLikePath(path) {
2081
+ return /(^|\/)(evals?|tests?|__tests__)\//i.test(path) &&
2082
+ /\.(ts|tsx|js|mjs|py|json|ya?ml)$/i.test(path);
2083
+ }
2084
+ function isDeploymentLikePath(path) {
2085
+ return /(^|\/)(wrangler\.toml|vercel\.json|render\.ya?ml|railway\.json|Dockerfile|docker-compose\.ya?ml)$/i.test(path) ||
2086
+ /^\.github\/workflows\/[^/]+\.ya?ml$/i.test(path);
2087
+ }
2088
+ function isDeveloperAgentConfigPath(path) {
2089
+ return path === '.claude/settings.json' ||
2090
+ path === '.codex/hooks.json' ||
2091
+ path === '.cursor/hooks.json';
2092
+ }
2093
+ function recordSkipped(skipped, item) {
2094
+ if (skipped.length < MAX_RECORDED_SKIPS) {
2095
+ skipped.push(item);
2096
+ return;
2097
+ }
2098
+ if (skipped.length === MAX_RECORDED_SKIPS) {
2099
+ skipped.push({
2100
+ path: '...',
2101
+ reason: 'additional skipped entries omitted',
2102
+ });
2103
+ }
2104
+ }
2105
+ function throwHarnessScanTooBroad(rootDir, details) {
2106
+ const mib = (details.textBytes / 1024 / 1024).toFixed(1);
2107
+ const maxMib = (details.maxTextBytes / 1024 / 1024).toFixed(0);
2108
+ throw new CliInputError('Harness scan is too broad for safe local analysis.', [
2109
+ `Moda reached ${details.files.toLocaleString()} files while scanning ${rootDir}.`,
2110
+ `Text read budget: ${mib}MiB of ${maxMib}MiB. Last path: ${details.lastPath}.`,
2111
+ 'Run `moda init` from the actual agent project root, not a parent folder like ~/Projects.',
2112
+ 'For harness-only analysis, use `moda harness analyze --path=<agent-dir> --summary`.',
2113
+ ].join('\n'));
2114
+ }
2115
+ function numericEnv(name, fallback) {
2116
+ const raw = process.env[name];
2117
+ if (!raw)
2118
+ return fallback;
2119
+ const parsed = Number(raw);
2120
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
2121
+ }
2122
+ function numericEnvAllowZero(name, fallback) {
2123
+ const raw = process.env[name];
2124
+ if (!raw)
2125
+ return fallback;
2126
+ const parsed = Number(raw);
2127
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
2128
+ }
2129
+ function isTextFile(path) {
2130
+ const ext = extname(path).toLowerCase();
2131
+ return [
2132
+ '',
2133
+ '.cjs',
2134
+ '.js',
2135
+ '.json',
2136
+ '.jsx',
2137
+ '.md',
2138
+ '.mdc',
2139
+ '.mjs',
2140
+ '.py',
2141
+ '.toml',
2142
+ '.ts',
2143
+ '.tsx',
2144
+ '.txt',
2145
+ '.yaml',
2146
+ '.yml',
2147
+ ].includes(ext);
2148
+ }
2149
+ function isSecretishPath(path) {
2150
+ const name = basename(path).toLowerCase();
2151
+ return name === '.env' ||
2152
+ name.startsWith('.env.') ||
2153
+ name.includes('secret') ||
2154
+ name.includes('private-key') ||
2155
+ name.endsWith('.pem');
2156
+ }
2157
+ function detectRepo(rootDir, files) {
2158
+ const packageJson = files.find((file) => file.path === 'package.json');
2159
+ const pyproject = files.find((file) => file.path === 'pyproject.toml');
2160
+ const name = packageJson?.text
2161
+ ? safePackageName(packageJson.text) ?? basename(rootDir)
2162
+ : pyproject?.text
2163
+ ? safeTomlName(pyproject.text) ?? basename(rootDir)
2164
+ : basename(rootDir);
2165
+ const packageManagers = [
2166
+ files.some((file) => file.path === 'bun.lock' || file.path === 'bun.lockb') ? 'bun' : null,
2167
+ files.some((file) => file.path === 'pnpm-lock.yaml') ? 'pnpm' : null,
2168
+ files.some((file) => file.path === 'yarn.lock') ? 'yarn' : null,
2169
+ packageJson ? 'npm' : null,
2170
+ pyproject ? 'python' : null,
2171
+ ].filter((value) => Boolean(value));
2172
+ const languages = [
2173
+ files.some((file) => file.path.endsWith('.ts') || file.path.endsWith('.tsx')) ? 'typescript' : null,
2174
+ files.some((file) => file.path.endsWith('.js') || file.path.endsWith('.jsx') || file.path.endsWith('.mjs')) ? 'javascript' : null,
2175
+ files.some((file) => file.path.endsWith('.py')) ? 'python' : null,
2176
+ ].filter((value) => Boolean(value));
2177
+ return {
2178
+ root: '.',
2179
+ name,
2180
+ packageManagers: [...new Set(packageManagers)],
2181
+ languages: [...new Set(languages)],
2182
+ };
2183
+ }
2184
+ function safePackageName(text) {
2185
+ try {
2186
+ const parsed = JSON.parse(text);
2187
+ return typeof parsed.name === 'string' ? parsed.name : undefined;
2188
+ }
2189
+ catch {
2190
+ return undefined;
2191
+ }
2192
+ }
2193
+ function safeTomlName(text) {
2194
+ const match = text.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
2195
+ return match?.[1];
2196
+ }
2197
+ function parseAnnotations(file) {
2198
+ if (!file.text?.includes('moda:'))
2199
+ return [];
2200
+ const annotations = [];
2201
+ const lines = file.text.split(/\r?\n/);
2202
+ lines.forEach((line, index) => {
2203
+ const match = line.match(/\bmoda:\s*([a-z-]+)\s*(.*)$/i);
2204
+ if (!match)
2205
+ return;
2206
+ annotations.push({
2207
+ type: match[1],
2208
+ values: parseAnnotationValues(match[2] ?? ''),
2209
+ evidence: {
2210
+ path: file.path,
2211
+ reason: `Explicit moda ${match[1]} annotation on line ${index + 1}.`,
2212
+ },
2213
+ sourceAbsPath: file.absPath,
2214
+ rootDir: file.rootDir,
2215
+ });
2216
+ });
2217
+ return annotations;
2218
+ }
2219
+ function parseAnnotationValues(raw) {
2220
+ const values = {};
2221
+ const regex = /([A-Za-z0-9_-]+)=("([^"]*)"|'([^']*)'|[^\s]+)/g;
2222
+ let match;
2223
+ while ((match = regex.exec(raw))) {
2224
+ const key = match[1].replace(/-/g, '_');
2225
+ const value = match[3] ?? match[4] ?? match[2];
2226
+ if (!values[key])
2227
+ values[key] = [];
2228
+ values[key].push(value);
2229
+ }
2230
+ return values;
2231
+ }
2232
+ function applyAnnotations(graph, annotations) {
2233
+ for (const annotation of annotations) {
2234
+ switch (annotation.type) {
2235
+ case 'runtime-agent':
2236
+ case 'agent':
2237
+ upsertRuntimeAgent(graph, runtimeAgentFromAnnotation(annotation));
2238
+ break;
2239
+ case 'agent-family':
2240
+ case 'family':
2241
+ upsertAgentFamily(graph, agentFamilyFromAnnotation(annotation));
2242
+ break;
2243
+ case 'channel':
2244
+ upsertChannel(graph, channelFromAnnotation(annotation));
2245
+ break;
2246
+ case 'artifact':
2247
+ upsertArtifact(graph, artifactFromAnnotation(annotation));
2248
+ break;
2249
+ case 'identity':
2250
+ upsertIdentity(graph, identityFromAnnotation(annotation));
2251
+ break;
2252
+ case 'deployment':
2253
+ upsertDeployment(graph, deploymentFromAnnotation(annotation));
2254
+ break;
2255
+ case 'environment':
2256
+ upsertEnvironment(graph, environmentFromAnnotation(annotation));
2257
+ break;
2258
+ default:
2259
+ graph.unknowns.push({
2260
+ id: stableId('unknown', `${annotation.type}-${annotation.evidence.path}`),
2261
+ question: `Unknown moda annotation type '${annotation.type}' in ${annotation.evidence.path}.`,
2262
+ candidateIds: [],
2263
+ evidence: [annotation.evidence],
2264
+ });
2265
+ }
2266
+ }
2267
+ }
2268
+ function runtimeAgentFromAnnotation(annotation) {
2269
+ const values = annotation.values;
2270
+ const id = first(values.id) ?? stableId('agent', first(values.name) ?? annotation.evidence.path);
2271
+ return {
2272
+ id,
2273
+ name: first(values.name) ?? titleFromId(id),
2274
+ kind: 'runtime_agent',
2275
+ purpose: first(values.purpose) ?? first(values.description),
2276
+ familyId: first(values.family),
2277
+ root: normalizePath(first(values.root) ?? dirname(annotation.evidence.path)),
2278
+ entrypointIds: values.entrypoint ?? [stableId('entry', annotation.evidence.path)],
2279
+ channelAdapterIds: values.channel ?? [],
2280
+ artifactIds: [...(values.artifact ?? []), ...(values.prompt ?? []), ...(values.tool ?? [])],
2281
+ identityNamespaceIds: values.identity ?? [],
2282
+ deploymentIds: values.deployment ?? [],
2283
+ frameworkIds: values.framework ?? [],
2284
+ modelProviderIds: [...(values.provider ?? []), ...(values.model_provider ?? [])],
2285
+ confidence: numberValue(first(values.confidence), 0.9),
2286
+ evidence: [annotation.evidence],
2287
+ };
2288
+ }
2289
+ function agentFamilyFromAnnotation(annotation) {
2290
+ const values = annotation.values;
2291
+ const id = first(values.id) ?? stableId('family', first(values.name) ?? annotation.evidence.path);
2292
+ return {
2293
+ id,
2294
+ name: first(values.name) ?? titleFromId(id),
2295
+ description: first(values.description),
2296
+ runtimeAgentIds: values.agent ?? [],
2297
+ sharedArtifactIds: [...(values.artifact ?? []), ...(values.prompt ?? []), ...(values.tool ?? [])],
2298
+ confidence: numberValue(first(values.confidence), 0.86),
2299
+ evidence: [annotation.evidence],
2300
+ };
2301
+ }
2302
+ function channelFromAnnotation(annotation) {
2303
+ const values = annotation.values;
2304
+ const kind = first(values.kind) ?? first(values.type) ?? 'channel';
2305
+ const id = first(values.id) ?? stableId('channel', first(values.name) ?? kind);
2306
+ return {
2307
+ id,
2308
+ name: first(values.name) ?? titleFromId(id),
2309
+ kind,
2310
+ sourcePath: annotation.evidence.path,
2311
+ confidence: numberValue(first(values.confidence), 0.85),
2312
+ evidence: [annotation.evidence],
2313
+ };
2314
+ }
2315
+ function artifactFromAnnotation(annotation) {
2316
+ const values = annotation.values;
2317
+ const type = artifactType(first(values.kind) ?? first(values.type));
2318
+ const sourcePath = first(values.path) ?? annotation.evidence.path;
2319
+ const id = first(values.id) ?? stableId(type, first(values.name) ?? sourcePath);
2320
+ return {
2321
+ id,
2322
+ type,
2323
+ name: first(values.name) ?? titleFromId(id),
2324
+ scope: scopeValue(first(values.scope)),
2325
+ sourcePath,
2326
+ contentHash: contentHashForAnnotatedSource(sourcePath, annotation),
2327
+ ownedByAgentId: first(values.owner),
2328
+ usedByAgentIds: values.agent ?? [],
2329
+ confidence: numberValue(first(values.confidence), 0.85),
2330
+ evidence: [annotation.evidence],
2331
+ };
2332
+ }
2333
+ function identityFromAnnotation(annotation) {
2334
+ const values = annotation.values;
2335
+ const kind = first(values.kind) ?? first(values.type) ?? 'identity';
2336
+ const id = first(values.id) ?? stableId('identity', first(values.name) ?? kind);
2337
+ return {
2338
+ id,
2339
+ name: first(values.name) ?? titleFromId(id),
2340
+ kind,
2341
+ canonicalId: first(values.canonical),
2342
+ sourcePath: annotation.evidence.path,
2343
+ confidence: numberValue(first(values.confidence), 0.8),
2344
+ evidence: [annotation.evidence],
2345
+ };
2346
+ }
2347
+ function deploymentFromAnnotation(annotation) {
2348
+ const values = annotation.values;
2349
+ const id = first(values.id) ?? stableId('deploy', first(values.name) ?? annotation.evidence.path);
2350
+ return {
2351
+ id,
2352
+ name: first(values.name) ?? titleFromId(id),
2353
+ environmentId: first(values.environment),
2354
+ sourcePath: first(values.path) ?? annotation.evidence.path,
2355
+ confidence: numberValue(first(values.confidence), 0.78),
2356
+ evidence: [annotation.evidence],
2357
+ };
2358
+ }
2359
+ function environmentFromAnnotation(annotation) {
2360
+ const values = annotation.values;
2361
+ const kind = first(values.kind) ?? first(values.type) ?? 'runtime';
2362
+ const id = first(values.id) ?? stableId('env', first(values.name) ?? kind);
2363
+ return {
2364
+ id,
2365
+ name: first(values.name) ?? titleFromId(id),
2366
+ kind,
2367
+ sourcePath: annotation.evidence.path,
2368
+ confidence: numberValue(first(values.confidence), 0.75),
2369
+ evidence: [annotation.evidence],
2370
+ };
2371
+ }
2372
+ function applyHeuristics(graph, files) {
2373
+ for (const file of files) {
2374
+ maybeAddPromptArtifact(graph, file);
2375
+ maybeAddSkillArtifact(graph, file);
2376
+ maybeAddEvalArtifact(graph, file);
2377
+ maybeAddDeployment(graph, file);
2378
+ }
2379
+ if (graph.runtimeAgents.length === 0) {
2380
+ for (const file of files) {
2381
+ const agent = inferRuntimeAgent(file, graph);
2382
+ if (agent)
2383
+ upsertRuntimeAgent(graph, agent);
2384
+ }
2385
+ }
2386
+ for (const agent of graph.runtimeAgents) {
2387
+ if (agent.entrypointIds.length === 0) {
2388
+ const entryId = stableId('entry', agent.root);
2389
+ agent.entrypointIds.push(entryId);
2390
+ upsertArtifact(graph, {
2391
+ id: entryId,
2392
+ type: 'entrypoint',
2393
+ name: `${agent.name} Entrypoint`,
2394
+ scope: 'agent',
2395
+ sourcePath: agent.root,
2396
+ ownedByAgentId: agent.id,
2397
+ usedByAgentIds: [agent.id],
2398
+ confidence: 0.7,
2399
+ evidence: agent.evidence,
2400
+ });
2401
+ }
2402
+ }
2403
+ }
2404
+ function maybeAddPromptArtifact(graph, file) {
2405
+ const isPrompt = /(^|\/)prompts?\//i.test(file.path) &&
2406
+ /\.(prompt\.)?(md|json|ya?ml)$/i.test(file.path);
2407
+ if (!isPrompt)
2408
+ return;
2409
+ const id = stableId('prompt', file.path);
2410
+ upsertArtifact(graph, {
2411
+ id,
2412
+ type: 'prompt',
2413
+ name: promptNameFromPath(file.path),
2414
+ scope: 'agent',
2415
+ sourcePath: file.path,
2416
+ contentHash: hashText(file.text ?? readFileSync(file.absPath, 'utf8')),
2417
+ usedByAgentIds: [],
2418
+ confidence: 0.72,
2419
+ evidence: [{ path: file.path, reason: 'Prompt-like file found under prompts directory.' }],
2420
+ });
2421
+ }
2422
+ function maybeAddSkillArtifact(graph, file) {
2423
+ if (!/(^|\/)(\.claude\/skills|\.cursor\/rules|skills?)\//i.test(file.path))
2424
+ return;
2425
+ if (!/\.(md|mdc)$/i.test(file.path))
2426
+ return;
2427
+ upsertArtifact(graph, {
2428
+ id: stableId('skill', file.path),
2429
+ type: 'skill',
2430
+ name: titleFromId(basename(file.path).replace(/\.(md|mdc)$/i, '')),
2431
+ scope: 'global',
2432
+ sourcePath: file.path,
2433
+ contentHash: hashText(file.text ?? ''),
2434
+ usedByAgentIds: [],
2435
+ confidence: 0.7,
2436
+ evidence: [{ path: file.path, reason: 'Skill or rule file detected.' }],
2437
+ });
2438
+ }
2439
+ function maybeAddEvalArtifact(graph, file) {
2440
+ if (!/(eval|test|spec)/i.test(file.path))
2441
+ return;
2442
+ if (!/\.(ts|tsx|js|mjs|py|json|ya?ml)$/i.test(file.path))
2443
+ return;
2444
+ upsertArtifact(graph, {
2445
+ id: stableId('eval', file.path),
2446
+ type: 'eval',
2447
+ name: titleFromId(file.path),
2448
+ scope: 'global',
2449
+ sourcePath: file.path,
2450
+ contentHash: file.text ? hashText(file.text) : undefined,
2451
+ usedByAgentIds: [],
2452
+ confidence: 0.52,
2453
+ evidence: [{ path: file.path, reason: 'Eval/test-like file detected.' }],
2454
+ });
2455
+ }
2456
+ function maybeAddDeployment(graph, file) {
2457
+ if (!/(wrangler\.toml|vercel\.json|render\.yaml|railway\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/)/i.test(file.path)) {
2458
+ return;
2459
+ }
2460
+ upsertDeployment(graph, {
2461
+ id: stableId('deploy', file.path),
2462
+ name: titleFromId(file.path),
2463
+ sourcePath: file.path,
2464
+ confidence: 0.6,
2465
+ evidence: [{ path: file.path, reason: 'Deployment or CI configuration detected.' }],
2466
+ });
2467
+ }
2468
+ function inferRuntimeAgent(file, graph) {
2469
+ if (!file.text)
2470
+ return null;
2471
+ if (!/\.(ts|tsx|js|mjs|py)$/i.test(file.path))
2472
+ return null;
2473
+ if (basename(file.path) === '__init__.py')
2474
+ return null;
2475
+ const loweredPath = file.path.toLowerCase();
2476
+ const loweredText = file.text.toLowerCase();
2477
+ const pathLooksLikeAgent = /(^|\/)(agent|agents|agent_harness)\//.test(loweredPath) ||
2478
+ /(^|\/)(agent|main|index)\.(ts|tsx|js|mjs|py)$/.test(loweredPath);
2479
+ const codeLooksLikeAgent = /\b(run_agent|runagent|createagent|buildprompt|system prompt|openai|anthropic|model)\b/i.test(file.text);
2480
+ if (!pathLooksLikeAgent || !codeLooksLikeAgent)
2481
+ return null;
2482
+ const channelIds = inferChannels(file);
2483
+ const artifactIds = inferArtifactsForFile(file, graph);
2484
+ const frameworkIds = frameworkIdsForFile(file);
2485
+ const modelProviderIds = providerIdsForFile(file);
2486
+ const agentId = stableId('agent', file.path.replace(/\.(ts|tsx|js|mjs|py)$/i, ''));
2487
+ const entryId = stableId('entry', file.path);
2488
+ upsertArtifact(graph, {
2489
+ id: entryId,
2490
+ type: 'entrypoint',
2491
+ name: `${titleFromId(agentId)} Entrypoint`,
2492
+ scope: 'agent',
2493
+ sourcePath: file.path,
2494
+ contentHash: hashText(file.text),
2495
+ ownedByAgentId: agentId,
2496
+ usedByAgentIds: [agentId],
2497
+ confidence: 0.7,
2498
+ evidence: [{ path: file.path, reason: 'Agent-like entrypoint detected from path and code.' }],
2499
+ });
2500
+ return {
2501
+ id: agentId,
2502
+ name: titleFromId(file.path.replace(/\.(ts|tsx|js|mjs|py)$/i, '')),
2503
+ kind: 'runtime_agent',
2504
+ purpose: inferAgentPurpose(file),
2505
+ root: normalizePath(dirname(file.path)),
2506
+ entrypointIds: [entryId],
2507
+ channelAdapterIds: channelIds,
2508
+ artifactIds,
2509
+ identityNamespaceIds: [],
2510
+ deploymentIds: [],
2511
+ frameworkIds,
2512
+ modelProviderIds,
2513
+ confidence: loweredText.includes('run_agent') || loweredText.includes('runagent') ? 0.68 : 0.56,
2514
+ evidence: [{ path: file.path, reason: 'Agent-like entrypoint detected from path and code.' }],
2515
+ };
2516
+ }
2517
+ function inferAgentPurpose(file) {
2518
+ const text = file.text ?? '';
2519
+ const promptMatch = text.match(/(?:systemPrompt|system_prompt|SYSTEM_PROMPT)\s*[:=]\s*["'`]([^"'`]{12,180})/);
2520
+ const quoted = promptMatch?.[1];
2521
+ if (quoted)
2522
+ return titleCaseSentence(quoted.split(/[.!?]/)[0]);
2523
+ const commentMatch = text.match(/(?:purpose|description)\s*[:=]\s*["'`]([^"'`]{12,180})/i);
2524
+ if (commentMatch?.[1])
2525
+ return titleCaseSentence(commentMatch[1].split(/[.!?]/)[0]);
2526
+ const path = file.path.toLowerCase();
2527
+ if (path.includes('support'))
2528
+ return 'Support Agent';
2529
+ if (path.includes('triage'))
2530
+ return 'Triage Agent';
2531
+ if (path.includes('coding'))
2532
+ return 'Coding Agent';
2533
+ return undefined;
2534
+ }
2535
+ function titleCaseSentence(value) {
2536
+ return value
2537
+ .replace(/\s+/g, ' ')
2538
+ .trim()
2539
+ .split(' ')
2540
+ .map((word) => {
2541
+ if (!word)
2542
+ return word;
2543
+ if (/^[A-Z0-9_]+$/.test(word))
2544
+ return word;
2545
+ return word.charAt(0).toUpperCase() + word.slice(1);
2546
+ })
2547
+ .join(' ');
2548
+ }
2549
+ function inferChannels(file) {
2550
+ const text = `${file.path}\n${file.text ?? ''}`.toLowerCase();
2551
+ const channels = [
2552
+ ['whatsapp', 'channel_whatsapp'],
2553
+ ['sms', 'channel_sms'],
2554
+ ['voice', 'channel_voice'],
2555
+ ['slack', 'channel_slack'],
2556
+ ['email', 'channel_email'],
2557
+ ['web', 'channel_web'],
2558
+ ['cron', 'channel_cron'],
2559
+ ['api', 'channel_api'],
2560
+ ];
2561
+ return channels
2562
+ .filter(([needle]) => text.includes(needle))
2563
+ .map(([, id]) => id);
2564
+ }
2565
+ function inferArtifactsForFile(file, graph) {
2566
+ const text = file.text ?? '';
2567
+ const referenced = graph.artifacts.filter((artifact) => {
2568
+ if (!artifact.sourcePath)
2569
+ return false;
2570
+ const base = basename(artifact.sourcePath);
2571
+ return text.includes(base) || text.includes(artifact.sourcePath);
2572
+ });
2573
+ return referenced.map((artifact) => artifact.id);
2574
+ }
2575
+ function detectDeveloperAgentAdapters(files) {
2576
+ const adapters = [];
2577
+ if (files.some((file) => file.path.startsWith('.claude/'))) {
2578
+ adapters.push({
2579
+ id: 'developer_adapter_claude_code',
2580
+ name: 'Claude Code',
2581
+ kind: 'developer_agent_adapter',
2582
+ sourcePath: '.claude',
2583
+ confidence: 0.9,
2584
+ evidence: [{ path: '.claude', reason: 'Claude Code project markers detected.' }],
2585
+ });
2586
+ }
2587
+ if (files.some((file) => file.path.startsWith('.cursor/'))) {
2588
+ adapters.push({
2589
+ id: 'developer_adapter_cursor',
2590
+ name: 'Cursor',
2591
+ kind: 'developer_agent_adapter',
2592
+ sourcePath: '.cursor',
2593
+ confidence: 0.9,
2594
+ evidence: [{ path: '.cursor', reason: 'Cursor project markers detected.' }],
2595
+ });
2596
+ }
2597
+ if (files.some((file) => file.path.startsWith('.codex/'))) {
2598
+ adapters.push({
2599
+ id: 'developer_adapter_codex',
2600
+ name: 'Codex',
2601
+ kind: 'developer_agent_adapter',
2602
+ sourcePath: '.codex',
2603
+ confidence: 0.9,
2604
+ evidence: [{ path: '.codex', reason: 'Codex project markers detected.' }],
2605
+ });
2606
+ }
2607
+ return adapters;
2608
+ }
2609
+ function detectEnvironments(files) {
2610
+ const envs = [];
2611
+ for (const id of ['local', 'staging', 'prod']) {
2612
+ const pathMatch = files.find((file) => file.path.includes(`.${id}.`) ||
2613
+ file.path.includes(`-${id}.`) ||
2614
+ file.path.includes(`/${id}/`));
2615
+ if (pathMatch || id === 'local') {
2616
+ envs.push({
2617
+ id: `env_${id}`,
2618
+ name: titleFromId(id),
2619
+ kind: id,
2620
+ sourcePath: pathMatch?.path,
2621
+ confidence: pathMatch ? 0.65 : 0.45,
2622
+ evidence: [{
2623
+ path: pathMatch?.path ?? '.',
2624
+ reason: pathMatch ? `Environment marker for ${id} detected.` : 'Default local environment assumed.',
2625
+ }],
2626
+ });
2627
+ }
2628
+ }
2629
+ return envs;
2630
+ }
2631
+ const FRAMEWORK_CATALOG = [
2632
+ {
2633
+ id: 'framework_langchain',
2634
+ name: 'LangChain',
2635
+ kind: 'agent_framework',
2636
+ packageNames: ['langchain', '@langchain/core', '@langchain/langgraph', 'langgraph'],
2637
+ patterns: [/langchain/i, /langgraph/i],
2638
+ },
2639
+ {
2640
+ id: 'framework_vercel_ai_sdk',
2641
+ name: 'Vercel AI SDK',
2642
+ kind: 'agent_framework',
2643
+ packageNames: ['ai', '@ai-sdk/openai', '@ai-sdk/anthropic'],
2644
+ patterns: [/\bfrom\s+['"]ai['"]/, /\bgenerateText\s*\(/, /\bstreamText\s*\(/],
2645
+ },
2646
+ {
2647
+ id: 'framework_mastra',
2648
+ name: 'Mastra',
2649
+ kind: 'agent_framework',
2650
+ packageNames: ['mastra', '@mastra/core'],
2651
+ patterns: [/mastra/i],
2652
+ },
2653
+ {
2654
+ id: 'framework_openai_agents',
2655
+ name: 'OpenAI Agents SDK',
2656
+ kind: 'agent_framework',
2657
+ packageNames: ['@openai/agents', 'openai-agents', 'openai-agents-python'],
2658
+ patterns: [/openai_agents/i, /@openai\/agents/i, /\bAgent\s*\(/],
2659
+ },
2660
+ {
2661
+ id: 'framework_crewai',
2662
+ name: 'CrewAI',
2663
+ kind: 'agent_framework',
2664
+ packageNames: ['crewai'],
2665
+ patterns: [/crewai/i],
2666
+ },
2667
+ {
2668
+ id: 'framework_pydantic_ai',
2669
+ name: 'Pydantic AI',
2670
+ kind: 'agent_framework',
2671
+ packageNames: ['pydantic-ai', 'pydantic_ai'],
2672
+ patterns: [/pydantic_ai/i],
2673
+ },
2674
+ ];
2675
+ const PROVIDER_CATALOG = [
2676
+ {
2677
+ id: 'provider_openai',
2678
+ name: 'OpenAI',
2679
+ kind: 'model_provider',
2680
+ packageNames: ['openai', '@ai-sdk/openai'],
2681
+ envVars: ['OPENAI_API_KEY'],
2682
+ patterns: [/from\s+['"]openai['"]/, /\bnew\s+OpenAI\b/, /\bgpt-[\w.-]+/i, /OPENAI_API_KEY/],
2683
+ },
2684
+ {
2685
+ id: 'provider_anthropic',
2686
+ name: 'Anthropic',
2687
+ kind: 'model_provider',
2688
+ packageNames: ['@anthropic-ai/sdk', '@ai-sdk/anthropic', 'anthropic'],
2689
+ envVars: ['ANTHROPIC_API_KEY'],
2690
+ patterns: [/anthropic/i, /\bclaude-[\w.-]+/i, /ANTHROPIC_API_KEY/],
2691
+ },
2692
+ {
2693
+ id: 'provider_google',
2694
+ name: 'Google Gemini',
2695
+ kind: 'model_provider',
2696
+ packageNames: ['@google/generative-ai', '@ai-sdk/google', 'google-generativeai'],
2697
+ envVars: ['GOOGLE_GENERATIVE_AI_API_KEY', 'GEMINI_API_KEY'],
2698
+ patterns: [/gemini/i, /google.generativeai/i],
2699
+ },
2700
+ {
2701
+ id: 'provider_cohere',
2702
+ name: 'Cohere',
2703
+ kind: 'model_provider',
2704
+ packageNames: ['cohere-ai', 'cohere'],
2705
+ envVars: ['COHERE_API_KEY'],
2706
+ patterns: [/cohere/i],
2707
+ },
2708
+ {
2709
+ id: 'provider_mistral',
2710
+ name: 'Mistral',
2711
+ kind: 'model_provider',
2712
+ packageNames: ['@mistralai/mistralai', 'mistralai'],
2713
+ envVars: ['MISTRAL_API_KEY'],
2714
+ patterns: [/mistral/i],
2715
+ },
2716
+ {
2717
+ id: 'provider_ollama',
2718
+ name: 'Ollama',
2719
+ kind: 'model_provider',
2720
+ packageNames: ['ollama'],
2721
+ envVars: ['OLLAMA_HOST'],
2722
+ patterns: [/ollama/i],
2723
+ },
2724
+ ];
2725
+ function detectAgentFrameworks(files) {
2726
+ const frameworks = [];
2727
+ for (const candidate of FRAMEWORK_CATALOG) {
2728
+ const evidence = detectionEvidence(files, candidate.packageNames, candidate.patterns);
2729
+ if (evidence.length === 0)
2730
+ continue;
2731
+ frameworks.push({
2732
+ id: candidate.id,
2733
+ name: candidate.name,
2734
+ kind: candidate.kind,
2735
+ packageNames: [...candidate.packageNames],
2736
+ runtimeAgentIds: [],
2737
+ confidence: evidence.some((item) => item.path === 'package.json' || item.path === 'pyproject.toml') ? 0.82 : 0.66,
2738
+ evidence,
2739
+ });
2740
+ }
2741
+ return frameworks.sort((a, b) => a.id.localeCompare(b.id));
2742
+ }
2743
+ function detectModelProviders(files) {
2744
+ const providers = [];
2745
+ for (const candidate of PROVIDER_CATALOG) {
2746
+ const evidence = detectionEvidence(files, candidate.packageNames, candidate.patterns);
2747
+ if (evidence.length === 0)
2748
+ continue;
2749
+ const modelHints = modelHintsForProvider(candidate.id, files);
2750
+ providers.push({
2751
+ id: candidate.id,
2752
+ name: candidate.name,
2753
+ kind: candidate.kind,
2754
+ packageNames: [...candidate.packageNames],
2755
+ runtimeAgentIds: [],
2756
+ envVars: [...candidate.envVars],
2757
+ modelHints,
2758
+ confidence: evidence.some((item) => item.path === 'package.json' || item.path === 'pyproject.toml') ? 0.82 : 0.62,
2759
+ evidence,
2760
+ });
2761
+ }
2762
+ return providers.sort((a, b) => a.id.localeCompare(b.id));
2763
+ }
2764
+ function detectionEvidence(files, packageNames, patterns) {
2765
+ const evidence = [];
2766
+ for (const file of files) {
2767
+ if (file.path === 'package.json' && file.text) {
2768
+ const deps = packageDependencyNames(file.text);
2769
+ const matched = packageNames.filter((name) => deps.has(name));
2770
+ for (const name of matched) {
2771
+ evidence.push({ path: file.path, reason: `Dependency ${name} detected.` });
2772
+ }
2773
+ }
2774
+ if (file.path === 'pyproject.toml' && file.text) {
2775
+ const lowered = file.text.toLowerCase();
2776
+ for (const name of packageNames) {
2777
+ if (lowered.includes(name.toLowerCase())) {
2778
+ evidence.push({ path: file.path, reason: `Python dependency ${name} detected.` });
2779
+ }
2780
+ }
2781
+ }
2782
+ if (!file.text || !/\.(ts|tsx|js|mjs|py|json|toml)$/i.test(file.path))
2783
+ continue;
2784
+ for (const pattern of patterns) {
2785
+ if (pattern.test(file.text)) {
2786
+ evidence.push({ path: file.path, reason: `Source references ${pattern.source}.` });
2787
+ break;
2788
+ }
2789
+ }
2790
+ }
2791
+ return mergeEvidence([], evidence).slice(0, 12);
2792
+ }
2793
+ function packageDependencyNames(text) {
2794
+ try {
2795
+ const parsed = JSON.parse(text);
2796
+ return new Set([
2797
+ ...Object.keys(parsed.dependencies ?? {}),
2798
+ ...Object.keys(parsed.devDependencies ?? {}),
2799
+ ...Object.keys(parsed.optionalDependencies ?? {}),
2800
+ ...Object.keys(parsed.peerDependencies ?? {}),
2801
+ ]);
2802
+ }
2803
+ catch {
2804
+ return new Set();
2805
+ }
2806
+ }
2807
+ function modelHintsForProvider(providerId, files) {
2808
+ const hints = new Set();
2809
+ const pattern = providerId === 'provider_anthropic'
2810
+ ? /\bclaude-[A-Za-z0-9_.-]+/g
2811
+ : providerId === 'provider_openai'
2812
+ ? /\bgpt-[A-Za-z0-9_.-]+/g
2813
+ : providerId === 'provider_google'
2814
+ ? /\bgemini-[A-Za-z0-9_.-]+/g
2815
+ : null;
2816
+ if (!pattern)
2817
+ return [];
2818
+ for (const file of files) {
2819
+ if (!file.text)
2820
+ continue;
2821
+ for (const match of file.text.matchAll(pattern)) {
2822
+ hints.add(match[0]);
2823
+ }
2824
+ }
2825
+ return [...hints].sort();
2826
+ }
2827
+ function frameworkIdsForFile(file) {
2828
+ return FRAMEWORK_CATALOG
2829
+ .filter((candidate) => file.text && candidate.patterns.some((pattern) => pattern.test(file.text ?? '')))
2830
+ .map((candidate) => candidate.id);
2831
+ }
2832
+ function providerIdsForFile(file) {
2833
+ return PROVIDER_CATALOG
2834
+ .filter((candidate) => file.text && candidate.patterns.some((pattern) => pattern.test(file.text ?? '')))
2835
+ .map((candidate) => candidate.id);
2836
+ }
2837
+ function linkFrameworksAndProviders(graph) {
2838
+ for (const agent of graph.runtimeAgents) {
2839
+ const agentPaths = new Set([
2840
+ agent.root,
2841
+ ...agent.evidence.map((item) => item.path),
2842
+ ...graph.artifacts
2843
+ .filter((artifact) => agent.entrypointIds.includes(artifact.id) && artifact.sourcePath)
2844
+ .map((artifact) => artifact.sourcePath),
2845
+ ]);
2846
+ for (const framework of graph.agentFrameworks) {
2847
+ if (detectionAppliesToAgent(framework.evidence, agent, agentPaths)) {
2848
+ agent.frameworkIds = unique([...agent.frameworkIds, framework.id]).sort();
2849
+ framework.runtimeAgentIds = unique([...framework.runtimeAgentIds, agent.id]).sort();
2850
+ }
2851
+ }
2852
+ for (const provider of graph.modelProviders) {
2853
+ if (detectionAppliesToAgent(provider.evidence, agent, agentPaths)) {
2854
+ agent.modelProviderIds = unique([...agent.modelProviderIds, provider.id]).sort();
2855
+ provider.runtimeAgentIds = unique([...provider.runtimeAgentIds, agent.id]).sort();
2856
+ }
2857
+ }
2858
+ }
2859
+ }
2860
+ function detectionAppliesToAgent(evidence, agent, agentPaths) {
2861
+ return evidence.some((item) => item.path === 'package.json' ||
2862
+ item.path === 'pyproject.toml' ||
2863
+ agentPaths.has(item.path) ||
2864
+ item.path.startsWith(`${agent.root}/`));
2865
+ }
2866
+ function finalizeGraph(graph) {
2867
+ const artifactMap = new Map(graph.artifacts.map((artifact) => [artifact.id, artifact]));
2868
+ const channelMap = new Map(graph.channelAdapters.map((channel) => [channel.id, channel]));
2869
+ const identityMap = new Map(graph.identityNamespaces.map((identity) => [identity.id, identity]));
2870
+ const deploymentMap = new Map(graph.deployments.map((deployment) => [deployment.id, deployment]));
2871
+ for (const agent of graph.runtimeAgents) {
2872
+ for (const entrypointId of agent.entrypointIds) {
2873
+ if (!artifactMap.has(entrypointId)) {
2874
+ const sourcePath = agent.evidence[0]?.path ?? agent.root;
2875
+ const entrypoint = {
2876
+ id: entrypointId,
2877
+ type: 'entrypoint',
2878
+ name: `${agent.name} Entrypoint`,
2879
+ scope: 'agent',
2880
+ sourcePath,
2881
+ ownedByAgentId: agent.id,
2882
+ usedByAgentIds: [agent.id],
2883
+ confidence: Math.min(0.82, agent.confidence),
2884
+ evidence: agent.evidence,
2885
+ };
2886
+ upsertArtifact(graph, entrypoint);
2887
+ artifactMap.set(entrypointId, entrypoint);
2888
+ }
2889
+ graph.relationships.push({
2890
+ type: 'owns_artifact',
2891
+ from: agent.id,
2892
+ to: entrypointId,
2893
+ artifactId: entrypointId,
2894
+ evidence: agent.evidence,
2895
+ });
2896
+ }
2897
+ for (const channelId of agent.channelAdapterIds) {
2898
+ if (!channelMap.has(channelId)) {
2899
+ upsertChannel(graph, {
2900
+ id: channelId,
2901
+ name: titleFromId(channelId),
2902
+ kind: channelId.replace(/^channel_/, ''),
2903
+ confidence: 0.55,
2904
+ evidence: agent.evidence,
2905
+ });
2906
+ }
2907
+ graph.relationships.push({
2908
+ type: 'invoked_by_channel',
2909
+ from: agent.id,
2910
+ to: channelId,
2911
+ evidence: agent.evidence,
2912
+ });
2913
+ }
2914
+ for (const artifactId of agent.artifactIds) {
2915
+ if (!artifactMap.has(artifactId)) {
2916
+ const artifact = {
2917
+ id: artifactId,
2918
+ type: artifactTypeFromId(artifactId),
2919
+ name: titleFromId(artifactId),
2920
+ scope: 'agent',
2921
+ usedByAgentIds: [agent.id],
2922
+ confidence: 0.45,
2923
+ evidence: agent.evidence,
2924
+ };
2925
+ upsertArtifact(graph, artifact);
2926
+ artifactMap.set(artifactId, artifact);
2927
+ }
2928
+ const artifact = artifactMap.get(artifactId);
2929
+ if (artifact && !artifact.usedByAgentIds.includes(agent.id)) {
2930
+ artifact.usedByAgentIds.push(agent.id);
2931
+ }
2932
+ graph.relationships.push({
2933
+ type: 'uses_artifact',
2934
+ from: agent.id,
2935
+ to: artifactId,
2936
+ artifactId,
2937
+ evidence: artifact?.evidence ?? agent.evidence,
2938
+ });
2939
+ }
2940
+ for (const identityId of agent.identityNamespaceIds) {
2941
+ if (!identityMap.has(identityId)) {
2942
+ upsertIdentity(graph, {
2943
+ id: identityId,
2944
+ name: titleFromId(identityId),
2945
+ kind: identityId.replace(/^identity_/, ''),
2946
+ confidence: 0.5,
2947
+ evidence: agent.evidence,
2948
+ });
2949
+ }
2950
+ }
2951
+ for (const deploymentId of agent.deploymentIds) {
2952
+ if (!deploymentMap.has(deploymentId)) {
2953
+ upsertDeployment(graph, {
2954
+ id: deploymentId,
2955
+ name: titleFromId(deploymentId),
2956
+ confidence: 0.5,
2957
+ evidence: agent.evidence,
2958
+ });
2959
+ }
2960
+ graph.relationships.push({
2961
+ type: 'deployed_as',
2962
+ from: agent.id,
2963
+ to: deploymentId,
2964
+ evidence: agent.evidence,
2965
+ });
2966
+ }
2967
+ }
2968
+ const familyMap = new Map(graph.agentFamilies.map((family) => [family.id, family]));
2969
+ for (const agent of graph.runtimeAgents) {
2970
+ if (!agent.familyId)
2971
+ continue;
2972
+ const family = familyMap.get(agent.familyId);
2973
+ if (family && !family.runtimeAgentIds.includes(agent.id)) {
2974
+ family.runtimeAgentIds.push(agent.id);
2975
+ }
2976
+ }
2977
+ for (const family of graph.agentFamilies) {
2978
+ const shared = graph.artifacts
2979
+ .filter((artifact) => artifact.usedByAgentIds.filter((agentId) => family.runtimeAgentIds.includes(agentId)).length > 1)
2980
+ .map((artifact) => artifact.id);
2981
+ family.sharedArtifactIds = unique([...family.sharedArtifactIds, ...shared]);
2982
+ }
2983
+ for (const artifact of graph.artifacts) {
2984
+ artifact.usedByAgentIds = unique(artifact.usedByAgentIds).sort();
2985
+ if (artifact.usedByAgentIds.length > 1) {
2986
+ artifact.scope = 'shared';
2987
+ for (const from of artifact.usedByAgentIds) {
2988
+ for (const to of artifact.usedByAgentIds) {
2989
+ if (from === to)
2990
+ continue;
2991
+ graph.relationships.push({
2992
+ type: 'shares_artifact_with',
2993
+ from,
2994
+ to,
2995
+ artifactId: artifact.id,
2996
+ evidence: artifact.evidence,
2997
+ });
2998
+ }
2999
+ }
3000
+ }
3001
+ }
3002
+ for (const identity of graph.identityNamespaces) {
3003
+ if (identity.canonicalId && identity.canonicalId !== identity.id) {
3004
+ graph.relationships.push({
3005
+ type: 'resolves_identity_to',
3006
+ from: identity.id,
3007
+ to: identity.canonicalId,
3008
+ evidence: identity.evidence,
3009
+ });
3010
+ }
3011
+ }
3012
+ if (graph.runtimeAgents.length > 1 && graph.agentFamilies.length === 0) {
3013
+ graph.unknowns.push({
3014
+ id: 'unknown_agent_grouping',
3015
+ question: 'Multiple runtime agents were detected. Confirm whether any should be grouped into an agent family.',
3016
+ candidateIds: graph.runtimeAgents.map((agent) => agent.id),
3017
+ evidence: graph.runtimeAgents.flatMap((agent) => agent.evidence).slice(0, 5),
3018
+ });
3019
+ }
3020
+ graph.agentFamilies.sort((a, b) => a.id.localeCompare(b.id));
3021
+ graph.runtimeAgents.sort((a, b) => a.id.localeCompare(b.id));
3022
+ graph.channelAdapters.sort((a, b) => a.id.localeCompare(b.id));
3023
+ graph.artifacts.sort((a, b) => a.id.localeCompare(b.id));
3024
+ graph.identityNamespaces.sort((a, b) => a.id.localeCompare(b.id));
3025
+ graph.agentFrameworks.sort((a, b) => a.id.localeCompare(b.id));
3026
+ graph.modelProviders.sort((a, b) => a.id.localeCompare(b.id));
3027
+ graph.deployments.sort((a, b) => a.id.localeCompare(b.id));
3028
+ graph.relationships = dedupeRelationships(graph.relationships);
3029
+ }
3030
+ function upsertRuntimeAgent(graph, next) {
3031
+ const existing = graph.runtimeAgents.find((agent) => agent.id === next.id);
3032
+ if (!existing) {
3033
+ graph.runtimeAgents.push(next);
3034
+ return;
3035
+ }
3036
+ existing.evidence = mergeEvidence(existing.evidence, next.evidence);
3037
+ existing.confidence = Math.max(existing.confidence, next.confidence);
3038
+ existing.entrypointIds = unique([...existing.entrypointIds, ...next.entrypointIds]);
3039
+ existing.channelAdapterIds = unique([...existing.channelAdapterIds, ...next.channelAdapterIds]);
3040
+ existing.artifactIds = unique([...existing.artifactIds, ...next.artifactIds]);
3041
+ existing.identityNamespaceIds = unique([...existing.identityNamespaceIds, ...next.identityNamespaceIds]);
3042
+ existing.deploymentIds = unique([...existing.deploymentIds, ...next.deploymentIds]);
3043
+ existing.frameworkIds = unique([...existing.frameworkIds, ...next.frameworkIds]);
3044
+ existing.modelProviderIds = unique([...existing.modelProviderIds, ...next.modelProviderIds]);
3045
+ existing.familyId = existing.familyId ?? next.familyId;
3046
+ existing.purpose = existing.purpose ?? next.purpose;
3047
+ }
3048
+ function upsertAgentFamily(graph, next) {
3049
+ const existing = graph.agentFamilies.find((family) => family.id === next.id);
3050
+ if (!existing) {
3051
+ graph.agentFamilies.push(next);
3052
+ return;
3053
+ }
3054
+ existing.evidence = mergeEvidence(existing.evidence, next.evidence);
3055
+ existing.confidence = Math.max(existing.confidence, next.confidence);
3056
+ existing.runtimeAgentIds = unique([...existing.runtimeAgentIds, ...next.runtimeAgentIds]);
3057
+ existing.sharedArtifactIds = unique([...existing.sharedArtifactIds, ...next.sharedArtifactIds]);
3058
+ }
3059
+ function upsertChannel(graph, next) {
3060
+ const existing = graph.channelAdapters.find((channel) => channel.id === next.id);
3061
+ if (!existing) {
3062
+ graph.channelAdapters.push(next);
3063
+ return;
3064
+ }
3065
+ existing.evidence = mergeEvidence(existing.evidence, next.evidence);
3066
+ existing.confidence = Math.max(existing.confidence, next.confidence);
3067
+ }
3068
+ function upsertArtifact(graph, next) {
3069
+ const existing = graph.artifacts.find((artifact) => artifact.id === next.id);
3070
+ if (!existing) {
3071
+ graph.artifacts.push(next);
3072
+ return;
3073
+ }
3074
+ existing.evidence = mergeEvidence(existing.evidence, next.evidence);
3075
+ existing.confidence = Math.max(existing.confidence, next.confidence);
3076
+ existing.usedByAgentIds = unique([...existing.usedByAgentIds, ...next.usedByAgentIds]);
3077
+ existing.ownedByAgentId = existing.ownedByAgentId ?? next.ownedByAgentId;
3078
+ existing.contentHash = existing.contentHash ?? next.contentHash;
3079
+ existing.sourcePath = existing.sourcePath ?? next.sourcePath;
3080
+ if (existing.scope !== 'shared')
3081
+ existing.scope = next.scope;
3082
+ }
3083
+ function upsertIdentity(graph, next) {
3084
+ const existing = graph.identityNamespaces.find((identity) => identity.id === next.id);
3085
+ if (!existing) {
3086
+ graph.identityNamespaces.push(next);
3087
+ return;
3088
+ }
3089
+ existing.evidence = mergeEvidence(existing.evidence, next.evidence);
3090
+ existing.confidence = Math.max(existing.confidence, next.confidence);
3091
+ existing.canonicalId = existing.canonicalId ?? next.canonicalId;
3092
+ }
3093
+ function upsertEnvironment(graph, next) {
3094
+ const existing = graph.environments.find((env) => env.id === next.id);
3095
+ if (!existing) {
3096
+ graph.environments.push(next);
3097
+ return;
3098
+ }
3099
+ existing.evidence = mergeEvidence(existing.evidence, next.evidence);
3100
+ existing.confidence = Math.max(existing.confidence, next.confidence);
3101
+ }
3102
+ function upsertDeployment(graph, next) {
3103
+ const existing = graph.deployments.find((deployment) => deployment.id === next.id);
3104
+ if (!existing) {
3105
+ graph.deployments.push(next);
3106
+ return;
3107
+ }
3108
+ existing.evidence = mergeEvidence(existing.evidence, next.evidence);
3109
+ existing.confidence = Math.max(existing.confidence, next.confidence);
3110
+ existing.environmentId = existing.environmentId ?? next.environmentId;
3111
+ }
3112
+ function contentHashForAnnotatedSource(sourcePath, annotation) {
3113
+ const explicitSourcePath = normalizePath(sourcePath) !== normalizePath(annotation.evidence.path);
3114
+ const candidates = explicitSourcePath ? [
3115
+ resolve(annotation.rootDir, sourcePath),
3116
+ resolve(dirname(annotation.sourceAbsPath), sourcePath),
3117
+ ] : [
3118
+ resolve(annotation.rootDir, sourcePath),
3119
+ resolve(dirname(annotation.sourceAbsPath), sourcePath),
3120
+ annotation.sourceAbsPath,
3121
+ ];
3122
+ const seen = new Set();
3123
+ for (const candidate of candidates) {
3124
+ if (seen.has(candidate))
3125
+ continue;
3126
+ seen.add(candidate);
3127
+ const text = readSmallTextFileForHash(candidate, annotation.rootDir);
3128
+ if (text !== undefined)
3129
+ return hashText(text);
3130
+ }
3131
+ return undefined;
3132
+ }
3133
+ function readSmallTextFileForHash(absPath, rootDir) {
3134
+ if (!existsSync(absPath))
3135
+ return undefined;
3136
+ const rel = normalizePath(relative(rootDir, absPath));
3137
+ if (isSecretishPath(rel))
3138
+ return undefined;
3139
+ let stat;
3140
+ try {
3141
+ stat = lstatSync(absPath);
3142
+ }
3143
+ catch {
3144
+ return undefined;
3145
+ }
3146
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_SCANNED_FILE_BYTES) {
3147
+ return undefined;
3148
+ }
3149
+ if (!isTextFile(rel))
3150
+ return undefined;
3151
+ try {
3152
+ return readFileSync(absPath, 'utf8');
3153
+ }
3154
+ catch {
3155
+ return undefined;
3156
+ }
3157
+ }
3158
+ function dedupeRelationships(relationships) {
3159
+ const seen = new Set();
3160
+ const result = [];
3161
+ for (const relationship of relationships) {
3162
+ const key = [
3163
+ relationship.type,
3164
+ relationship.from,
3165
+ relationship.to,
3166
+ relationship.artifactId ?? '',
3167
+ ].join('\0');
3168
+ if (seen.has(key))
3169
+ continue;
3170
+ seen.add(key);
3171
+ result.push(relationship);
3172
+ }
3173
+ return result.sort((a, b) => `${a.type}:${a.from}:${a.to}:${a.artifactId ?? ''}`.localeCompare(`${b.type}:${b.from}:${b.to}:${b.artifactId ?? ''}`));
3174
+ }
3175
+ function mergeEvidence(left, right) {
3176
+ const seen = new Set();
3177
+ const result = [];
3178
+ for (const item of [...left, ...right]) {
3179
+ const key = `${item.path}\0${item.reason}`;
3180
+ if (seen.has(key))
3181
+ continue;
3182
+ seen.add(key);
3183
+ result.push(item);
3184
+ }
3185
+ return result;
3186
+ }
3187
+ function artifactType(value) {
3188
+ if (value === 'entrypoint' ||
3189
+ value === 'prompt' ||
3190
+ value === 'tool' ||
3191
+ value === 'skill' ||
3192
+ value === 'eval' ||
3193
+ value === 'retrieval_index' ||
3194
+ value === 'memory' ||
3195
+ value === 'guardrail' ||
3196
+ value === 'model_config') {
3197
+ return value;
3198
+ }
3199
+ return 'unknown';
3200
+ }
3201
+ function artifactTypeFromId(id) {
3202
+ if (id.startsWith('prompt_'))
3203
+ return 'prompt';
3204
+ if (id.startsWith('tool_'))
3205
+ return 'tool';
3206
+ if (id.startsWith('skill_'))
3207
+ return 'skill';
3208
+ if (id.startsWith('eval_'))
3209
+ return 'eval';
3210
+ if (id.startsWith('entry_'))
3211
+ return 'entrypoint';
3212
+ if (id.startsWith('memory_'))
3213
+ return 'memory';
3214
+ if (id.startsWith('guardrail_'))
3215
+ return 'guardrail';
3216
+ if (id.startsWith('model_config_'))
3217
+ return 'model_config';
3218
+ return 'unknown';
3219
+ }
3220
+ function scopeValue(value) {
3221
+ if (value === 'shared' || value === 'global' || value === 'agent')
3222
+ return value;
3223
+ return 'agent';
3224
+ }
3225
+ function numberValue(value, fallback) {
3226
+ if (!value)
3227
+ return fallback;
3228
+ const parsed = Number(value);
3229
+ return Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : fallback;
3230
+ }
3231
+ function first(values) {
3232
+ return values?.[0];
3233
+ }
3234
+ function unique(values) {
3235
+ return [...new Set(values.filter(Boolean))];
3236
+ }
3237
+ function stableId(prefix, raw) {
3238
+ const slug = raw
3239
+ .toLowerCase()
3240
+ .replace(/\.[^.]+$/g, '')
3241
+ .replace(/[^a-z0-9]+/g, '_')
3242
+ .replace(/^_+|_+$/g, '')
3243
+ .replace(/_+/g, '_')
3244
+ .slice(0, 72);
3245
+ return `${prefix}_${slug || hashText(raw).slice(0, 10)}`;
3246
+ }
3247
+ function titleFromId(id) {
3248
+ return id
3249
+ .replace(/^(agent|family|channel|prompt|tool|skill|eval|identity|deploy|entry|env)_/, '')
3250
+ .split(/[_/.-]+/)
3251
+ .filter(Boolean)
3252
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
3253
+ .join(' ') || id;
3254
+ }
3255
+ function promptNameFromPath(path) {
3256
+ return titleFromId(basename(path).replace(/\.(prompt\.)?(md|json|ya?ml)$/i, ''));
3257
+ }
3258
+ function normalizePath(path) {
3259
+ return path.split(sep).join('/');
3260
+ }
3261
+ function hashText(text) {
3262
+ return createHash('sha256').update(text).digest('hex');
3263
+ }
3264
+ //# sourceMappingURL=harness.js.map