@moda-ai/cli 1.22.1 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/cli-c4se7a5d.js +762 -0
  2. package/dist/cli-mnqa4rp6.js +1563 -0
  3. package/dist/cli-stmjdgvy.js +2842 -0
  4. package/dist/cli-y55bgg14.js +6980 -0
  5. package/dist/cli-ysegdrm5.js +286 -0
  6. package/dist/cli.js +4779 -1159
  7. package/dist/harness-github-actions-7t3cncnt.js +95 -0
  8. package/dist/index-4ser01zr.js +2769 -0
  9. package/dist/provision-hm1ppwj6.js +74 -0
  10. package/package.json +3 -2
  11. package/skills/integration/index.json +2 -2
  12. package/dist/agent-schemas.d.ts +0 -3
  13. package/dist/agent-schemas.js +0 -358
  14. package/dist/agent.d.ts +0 -5
  15. package/dist/agent.js +0 -477
  16. package/dist/analyst-tui.d.ts +0 -81
  17. package/dist/analyst-tui.js +0 -314
  18. package/dist/api-client.d.ts +0 -46
  19. package/dist/api-client.js +0 -120
  20. package/dist/ask-stream.d.ts +0 -146
  21. package/dist/ask-stream.js +0 -473
  22. package/dist/auth-storage.d.ts +0 -35
  23. package/dist/auth-storage.js +0 -159
  24. package/dist/auth.d.ts +0 -50
  25. package/dist/auth.js +0 -694
  26. package/dist/cli-session.d.ts +0 -26
  27. package/dist/cli-session.js +0 -169
  28. package/dist/cli.d.ts +0 -44
  29. package/dist/command-context.d.ts +0 -62
  30. package/dist/command-context.js +0 -57
  31. package/dist/command-registry.d.ts +0 -22
  32. package/dist/command-registry.js +0 -15
  33. package/dist/config.d.ts +0 -60
  34. package/dist/config.js +0 -110
  35. package/dist/doctor.d.ts +0 -79
  36. package/dist/doctor.js +0 -1182
  37. package/dist/errors.d.ts +0 -50
  38. package/dist/errors.js +0 -64
  39. package/dist/exit-funnel.d.ts +0 -14
  40. package/dist/exit-funnel.js +0 -53
  41. package/dist/harness-artifact-bodies.d.ts +0 -28
  42. package/dist/harness-artifact-bodies.js +0 -276
  43. package/dist/harness-candidates.d.ts +0 -77
  44. package/dist/harness-candidates.js +0 -482
  45. package/dist/harness-remote.d.ts +0 -135
  46. package/dist/harness-remote.js +0 -540
  47. package/dist/harness-report.d.ts +0 -145
  48. package/dist/harness-report.js +0 -1579
  49. package/dist/harness-sync-limits.d.ts +0 -8
  50. package/dist/harness-sync-limits.js +0 -21
  51. package/dist/harness.d.ts +0 -463
  52. package/dist/harness.js +0 -4009
  53. package/dist/init/index.d.ts +0 -78
  54. package/dist/init/index.js +0 -1442
  55. package/dist/init/mcp.d.ts +0 -2
  56. package/dist/init/mcp.js +0 -27
  57. package/dist/init/progress-board.d.ts +0 -80
  58. package/dist/init/progress-board.js +0 -225
  59. package/dist/init/rules.d.ts +0 -6
  60. package/dist/init/rules.js +0 -171
  61. package/dist/init/sdk-catalog.d.ts +0 -10
  62. package/dist/init/sdk-catalog.js +0 -80
  63. package/dist/init/sdk-guide.d.ts +0 -19
  64. package/dist/init/sdk-guide.js +0 -185
  65. package/dist/init/sdk-integration.d.ts +0 -53
  66. package/dist/init/sdk-integration.js +0 -835
  67. package/dist/init/sdk-protocol.d.ts +0 -135
  68. package/dist/init/sdk-protocol.js +0 -152
  69. package/dist/init/sdk-verification.d.ts +0 -27
  70. package/dist/init/sdk-verification.js +0 -123
  71. package/dist/init/tenant.d.ts +0 -67
  72. package/dist/init/tenant.js +0 -421
  73. package/dist/intelligence-schema.d.ts +0 -349
  74. package/dist/intelligence-schema.js +0 -174
  75. package/dist/keys.d.ts +0 -47
  76. package/dist/keys.js +0 -136
  77. package/dist/manifest.d.ts +0 -5
  78. package/dist/manifest.js +0 -114
  79. package/dist/markdown-terminal.d.ts +0 -49
  80. package/dist/markdown-terminal.js +0 -212
  81. package/dist/output.d.ts +0 -191
  82. package/dist/output.js +0 -624
  83. package/dist/production-intelligence.d.ts +0 -175
  84. package/dist/production-intelligence.js +0 -1430
  85. package/dist/profile-commands.d.ts +0 -10
  86. package/dist/profile-commands.js +0 -342
  87. package/dist/profiles.d.ts +0 -112
  88. package/dist/profiles.js +0 -573
  89. package/dist/prompts-ab.d.ts +0 -71
  90. package/dist/prompts-ab.js +0 -313
  91. package/dist/prompts-propose.d.ts +0 -15
  92. package/dist/prompts-propose.js +0 -212
  93. package/dist/prompts.d.ts +0 -58
  94. package/dist/prompts.js +0 -418
  95. package/dist/provision.d.ts +0 -28
  96. package/dist/provision.js +0 -104
  97. package/dist/redact.d.ts +0 -2
  98. package/dist/redact.js +0 -90
  99. package/dist/schemas.d.ts +0 -225
  100. package/dist/schemas.js +0 -126
  101. package/dist/skills-bundled.d.ts +0 -35
  102. package/dist/skills-bundled.js +0 -157
  103. package/dist/skills-manifest.d.ts +0 -33
  104. package/dist/skills-manifest.js +0 -244
  105. package/dist/skills.d.ts +0 -57
  106. package/dist/skills.js +0 -783
  107. package/dist/sync.d.ts +0 -5
  108. package/dist/sync.js +0 -36
  109. package/dist/telemetry.d.ts +0 -56
  110. package/dist/telemetry.js +0 -167
  111. package/dist/transforms.d.ts +0 -25
  112. package/dist/transforms.js +0 -102
  113. package/dist/tui.d.ts +0 -48
  114. package/dist/tui.js +0 -393
  115. package/dist/update-check.d.ts +0 -31
  116. package/dist/update-check.js +0 -141
  117. package/dist/workspace.d.ts +0 -12
  118. package/dist/workspace.js +0 -1294
@@ -1,540 +0,0 @@
1
- /**
2
- * Server-side ("Moda-hosted") harness analyze client.
3
- *
4
- * `moda harness analyze --remote` builds a safe source snapshot with
5
- * `buildSourceSnapshot` (harness.ts), uploads it to the ingest worker
6
- * (`POST /v1/harness/analyze`), polls `GET /v1/harness/analyze/:id` for the
7
- * validated report, then writes the same local artifacts and emits the same
8
- * lifecycle events as the local analyze paths.
9
- *
10
- * The flow is split into composable pieces so callers can overlap or detach it:
11
- * `startRemoteAnalyze` (snapshot + upload + POST), `pollRemoteAnalyzeUntilDone`
12
- * (status loop with an `onProgress` hook), and `finalizeRemoteAnalyzeResult`
13
- * (repair + local validation + artifact writes). `runRemoteHarnessAnalyst`
14
- * composes all three for `moda harness analyze --remote`, and `moda harness
15
- * pull` (runHarnessPull) resumes a detached run from the
16
- * `.moda/harness-remote-run.json` state file.
17
- */
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
19
- import { join } from 'node:path';
20
- import { createCommandContext, stringOption } from './command-context.js';
21
- import { resolveApiKey, resolveIngestUrl } from './config.js';
22
- import { CliInputError } from './errors.js';
23
- import { stripAnsi } from './markdown-terminal.js';
24
- import { normalizeHarnessReport, stampRemoteSnapshotTruncation, validateHarnessReport, writeHarnessReportArtifacts, } from './harness-report.js';
25
- import { buildSourceSnapshot, emitWrittenArtifacts, numericEnv, numericEnvAllowZero, repairExternalAnalystReport, runHarnessCommand, sleep, summarizeHarness, summarizeInvalidAnalystReport, truncateText, writeHarnessAnalyzeResult, writeHarnessArtifacts, writeHumanProgress, } from './harness.js';
26
- /**
27
- * Client poll deadline. Must exceed the server's total analysis budget
28
- * (MODA_HARNESS_ANALYZE_TOTAL_BUDGET_MS, default 30m: full attempts plus the
29
- * sharded fallback), or the client reports a bogus "did not finish" while the
30
- * hosted run is still working; polling also keeps the hosted container awake.
31
- */
32
- const DEFAULT_REMOTE_ANALYZE_TIMEOUT_MS = 2_100_000;
33
- const DEFAULT_REMOTE_POLL_INITIAL_MS = 3_000;
34
- const DEFAULT_REMOTE_POLL_MAX_MS = 15_000;
35
- /** Lower bound on the inter-poll delay so a 0 interval can't busy-loop the status endpoint. */
36
- const MIN_REMOTE_POLL_DELAY_MS = 250;
37
- /**
38
- * Consecutive status-poll failures tolerated before the poll gives up. A
39
- * hosted run stays healthy across a transient blip (a network hiccup, a proxy
40
- * 5xx, or a GET racing the container's run-state write), and aborting a
41
- * 30-minute analysis over one failed GET would waste the whole run.
42
- */
43
- const REMOTE_POLL_MAX_CONSECUTIVE_FAILURES = 4;
44
- const REMOTE_UPLOAD_TIMEOUT_MS = 120_000;
45
- const REMOTE_STATUS_TIMEOUT_MS = 30_000;
46
- /** Per-repo pointer to the most recent server-side analysis run. */
47
- export const HARNESS_REMOTE_RUN_STATE_PATH = join('.moda', 'harness-remote-run.json');
48
- /**
49
- * True when `moda harness analyze` should run server-side (Moda-hosted) instead
50
- * of spawning a local coding agent: `--remote` (flags.remote === 'true') or the
51
- * `MODA_HARNESS_REMOTE` env opt-in.
52
- */
53
- export function isRemoteHarnessAnalyze(context) {
54
- // The CLI normalizes numeric flag values to numbers, so `--remote=1` arrives
55
- // as the number 1 rather than the string '1'; accept every truthy spelling.
56
- const flag = context.flags.remote;
57
- // An explicit local opt-out (`--local` / `--no-remote` / `--remote=false|0`)
58
- // wins over the `MODA_HARNESS_REMOTE` env, so callers that force local (the
59
- // `moda init` local path and its remote-failure fallback) stay local even
60
- // when the env is set. `no-remote` may arrive kebab- or snake-cased depending
61
- // on the caller (real CLI parsing snake-cases; internal callers do not).
62
- const noRemote = context.flags['no-remote'] ?? context.flags.no_remote;
63
- const local = context.flags.local;
64
- const isTruthy = (v) => v === true || v === 1 || v === 'true' || v === '1' || v === 'yes';
65
- const isFalsy = (v) => v === false || v === 0 || v === 'false' || v === '0' || v === 'no';
66
- if (isFalsy(flag) || isTruthy(noRemote) || isTruthy(local))
67
- return false;
68
- if (isTruthy(flag))
69
- return true;
70
- const env = (context.env.MODA_HARNESS_REMOTE ?? '').trim().toLowerCase();
71
- return env !== '' && env !== '0' && env !== 'false';
72
- }
73
- /** Read `.moda/harness-remote-run.json`; undefined when absent or unparseable. */
74
- export function readRemoteRunState(rootDir) {
75
- const path = join(rootDir, HARNESS_REMOTE_RUN_STATE_PATH);
76
- if (!existsSync(path))
77
- return undefined;
78
- try {
79
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
80
- if (!parsed || typeof parsed.run_id !== 'string' || parsed.run_id.length === 0)
81
- return undefined;
82
- return {
83
- run_id: parsed.run_id,
84
- started_at_iso: typeof parsed.started_at_iso === 'string' ? parsed.started_at_iso : '',
85
- status: parsed.status === 'completed' || parsed.status === 'failed' ? parsed.status : 'running',
86
- };
87
- }
88
- catch {
89
- return undefined;
90
- }
91
- }
92
- /** Best-effort write: run tracking must never break the analyze pipeline itself. */
93
- function writeRemoteRunState(rootDir, state) {
94
- try {
95
- mkdirSync(join(rootDir, '.moda'), { recursive: true });
96
- writeFileSync(join(rootDir, HARNESS_REMOTE_RUN_STATE_PATH), `${JSON.stringify(state, null, 2)}\n`);
97
- }
98
- catch {
99
- // Ignore: a read-only tree loses run tracking, not the analysis result.
100
- }
101
- }
102
- /** Mark the run terminal, preserving the original started_at when it is the same run. */
103
- function markRemoteRunTerminal(rootDir, runId, status) {
104
- const existing = readRemoteRunState(rootDir);
105
- writeRemoteRunState(rootDir, {
106
- run_id: runId,
107
- started_at_iso: existing && existing.run_id === runId && existing.started_at_iso
108
- ? existing.started_at_iso
109
- : new Date().toISOString(),
110
- status,
111
- });
112
- }
113
- /** Resolve the ingest endpoint + API key or fail with the standard remote-analyze guidance. */
114
- function resolveRemoteAnalyzeEndpoint(context) {
115
- const apiKey = resolveApiKey({ profile: context.profile, env: context.env });
116
- if (!apiKey) {
117
- throw new CliInputError('No Moda API key found for remote harness analysis.', 'Run `moda init` first (or set MODA_API_KEY), then rerun `moda harness analyze --remote`.');
118
- }
119
- const ingestUrl = resolveIngestUrl({ profile: context.profile, env: context.env }).replace(/\/+$/, '');
120
- return { apiKey, ingestUrl };
121
- }
122
- /**
123
- * Snapshot + upload + POST. Emits the inventory/upload lifecycle events,
124
- * persists `.moda/harness-remote-run.json` with status `running`, and returns
125
- * the accepted run id without waiting for the analysis.
126
- */
127
- export async function startRemoteAnalyze(options) {
128
- const { context, rootDir } = options;
129
- if (context.dryRun) {
130
- throw new CliInputError('Remote harness analysis cannot run in dry-run mode.', 'Use `moda harness analyze --analyst=local-scan --dry-run` for deterministic local inspection.');
131
- }
132
- const { apiKey, ingestUrl } = resolveRemoteAnalyzeEndpoint(context);
133
- context.output.writeEvent({
134
- event: 'progress',
135
- phase: 'inventory',
136
- message: 'Inventorying local harness files for upload.',
137
- elapsed_ms: Date.now() - context.startedAt,
138
- });
139
- writeHumanProgress(context, 'Moda Analyst (remote): inventorying local harness files...');
140
- const snapshot = buildSourceSnapshot(rootDir);
141
- context.output.writeEvent({
142
- event: 'progress',
143
- phase: 'remote_upload',
144
- message: `Uploading ${snapshot.fileCount} file(s) (${snapshot.byteCount} bytes) to Moda for server-side analysis.`,
145
- elapsed_ms: Date.now() - context.startedAt,
146
- });
147
- writeHumanProgress(context, `Moda Analyst (remote): uploading ${snapshot.fileCount} file(s) for server-side analysis...`);
148
- if (snapshot.truncated) {
149
- context.output.writeEvent({
150
- event: 'warning',
151
- phase: 'remote_upload',
152
- message: 'Source snapshot is truncated; server-side analysis may be incomplete.',
153
- elapsed_ms: Date.now() - context.startedAt,
154
- });
155
- writeHumanProgress(context, 'Warning: source snapshot is truncated; analysis may be incomplete.');
156
- }
157
- const accepted = await postRemoteAnalyze(ingestUrl, apiKey, {
158
- snapshot,
159
- options: buildRemoteAnalyzeOptions(context.flags),
160
- });
161
- const runId = accepted.analysis_run_id;
162
- writeRemoteRunState(rootDir, {
163
- run_id: runId,
164
- started_at_iso: new Date().toISOString(),
165
- status: 'running',
166
- });
167
- context.output.writeEvent({
168
- event: 'progress',
169
- phase: 'remote_wait',
170
- message: `Moda accepted analysis run ${runId}; waiting for the server-side agent.`,
171
- elapsed_ms: Date.now() - context.startedAt,
172
- });
173
- writeHumanProgress(context, `Moda Analyst (remote): analysis run ${runId} accepted; waiting for results...`);
174
- return { runId, snapshotTruncated: snapshot.truncated };
175
- }
176
- /**
177
- * Poll `GET /v1/harness/analyze/:id` until the run is terminal, the deadline
178
- * passes, or `signal` aborts. `onProgress` fires after every poll with the
179
- * latest status and any server-attached progress, so callers can render a live
180
- * board; the poll cadence (3s doubling to 15s) is unchanged.
181
- */
182
- export async function pollRemoteAnalyzeUntilDone(options) {
183
- const { context, runId, onProgress, signal } = options;
184
- const { apiKey, ingestUrl } = resolveRemoteAnalyzeEndpoint(context);
185
- const timeoutMs = options.timeoutMs ?? numericEnv('MODA_HARNESS_REMOTE_TIMEOUT_MS', DEFAULT_REMOTE_ANALYZE_TIMEOUT_MS);
186
- const initialMs = numericEnvAllowZero('MODA_HARNESS_REMOTE_POLL_INITIAL_MS', DEFAULT_REMOTE_POLL_INITIAL_MS);
187
- const maxMs = numericEnvAllowZero('MODA_HARNESS_REMOTE_POLL_MAX_MS', DEFAULT_REMOTE_POLL_MAX_MS);
188
- const deadline = Date.now() + timeoutMs;
189
- const url = `${ingestUrl}/v1/harness/analyze/${encodeURIComponent(runId)}`;
190
- // Floor the inter-poll delay so a 0 (or unset) interval can't turn a long
191
- // hosted run into a tight loop hammering the status endpoint.
192
- const capMs = Math.max(maxMs, MIN_REMOTE_POLL_DELAY_MS);
193
- let delay = Math.min(Math.max(initialMs, MIN_REMOTE_POLL_DELAY_MS), capMs);
194
- let last = { success: false, status: 'accepted' };
195
- let consecutiveFailures = 0;
196
- while (true) {
197
- try {
198
- last = await remoteAnalyzeFetch(url, apiKey, { method: 'GET' }, REMOTE_STATUS_TIMEOUT_MS);
199
- consecutiveFailures = 0;
200
- }
201
- catch (error) {
202
- // Tolerate a bounded run of consecutive poll failures instead of
203
- // aborting the (still healthy) hosted analysis on the first blip.
204
- consecutiveFailures += 1;
205
- if (consecutiveFailures >= REMOTE_POLL_MAX_CONSECUTIVE_FAILURES || signal?.aborted || Date.now() >= deadline) {
206
- throw error;
207
- }
208
- context.output.writeEvent({
209
- event: 'progress',
210
- phase: 'remote_wait',
211
- message: `Status poll failed (${consecutiveFailures}/${REMOTE_POLL_MAX_CONSECUTIVE_FAILURES}); retrying.`,
212
- elapsed_ms: Date.now() - context.startedAt,
213
- });
214
- await sleep(Math.min(delay, Math.max(deadline - Date.now(), MIN_REMOTE_POLL_DELAY_MS)));
215
- if (signal?.aborted)
216
- return last;
217
- delay = Math.min(delay * 2, capMs);
218
- continue;
219
- }
220
- onProgress?.({ status: last?.status ?? 'running', progress: normalizeRemoteProgress(last?.progress) });
221
- if (last?.status === 'completed' || last?.status === 'failed')
222
- return last;
223
- const remaining = deadline - Date.now();
224
- if (remaining <= 0)
225
- return last ?? { success: false, status: 'running' };
226
- if (signal?.aborted)
227
- return last ?? { success: false, status: 'running' };
228
- context.output.writeEvent({
229
- event: 'progress',
230
- phase: 'remote_wait',
231
- message: `Server-side analysis status: ${last?.status ?? 'running'}.`,
232
- elapsed_ms: Date.now() - context.startedAt,
233
- });
234
- await sleep(Math.min(delay, remaining));
235
- if (signal?.aborted)
236
- return last;
237
- delay = Math.min(delay * 2, capMs);
238
- }
239
- }
240
- /**
241
- * Turn a terminal (or timed-out) status payload into the local result: repair
242
- * the report from local source, re-validate, write `.moda` artifacts, emit the
243
- * artifact/validation events, and update the run state file. Shared by
244
- * `moda harness analyze --remote` and `moda harness pull`. Throws for runs the
245
- * client cannot recover; a timed-out non-terminal run keeps the state file at
246
- * `running` so `moda harness pull` can resume it later.
247
- */
248
- export function finalizeRemoteAnalyzeResult(options) {
249
- const { context, rootDir, runId, final } = options;
250
- // A run that returned no report at all is a hard failure (timeout, or the
251
- // hosted agent crashed before producing anything); nothing to recover.
252
- if (!final.report) {
253
- if (final.status === 'failed')
254
- markRemoteRunTerminal(rootDir, runId, 'failed');
255
- throw remoteAnalyzeHardFailure(final, runId);
256
- }
257
- // A `failed` run only qualifies for client-side recovery when the server tells
258
- // us the failure was its own REPORT VALIDATION (it produced a report but the
259
- // report-contract check failed). That is the one class the client can recover:
260
- // the uploaded snapshot is truncated/sampled, so the hosted analyst commonly
261
- // leaves prompt/tool `body` (or citation lines) empty, and the client (which
262
- // has the full local repo) can backfill those source-dependent gaps and
263
- // re-validate. Any other `failed` run (a crashed/aborted agent, which carries
264
- // an `error` string and no structured validation verdict) stays a hard failure
265
- // even if a partial report leaked through, so we never write or sync a report
266
- // from a hosted run the server aborted for a reason the client cannot repair.
267
- if (final.status === 'failed' && !serverFailedReportValidation(final)) {
268
- markRemoteRunTerminal(rootDir, runId, 'failed');
269
- throw remoteAnalyzeHardFailure(final, runId);
270
- }
271
- // The hosted analyst returned a report that failed SERVER-side validation,
272
- // most often because it enumerated prompt/tool artifacts but left their `body`
273
- // empty, or because the uploaded snapshot was truncated so the server never
274
- // saw the source. The client has the full local repo, so `repairExternalAnalystReport`
275
- // backfills those bodies from disk here and we re-validate locally before
276
- // deciding the outcome (a report that stays invalid after repair still exits 1
277
- // and is never synced). This lets `--remote` succeed even when the server
278
- // could not complete the report.
279
- if (final.status === 'failed') {
280
- writeHumanProgress(context, 'Moda Analyst (remote): server validation incomplete; completing artifact bodies from local source...');
281
- }
282
- let report = repairExternalAnalystReport(normalizeHarnessReport(final.report), rootDir);
283
- if (options.snapshotTruncated) {
284
- report = stampRemoteSnapshotTruncation(report);
285
- }
286
- const validation = validateHarnessReport(report, rootDir);
287
- context.output.writeEvent({
288
- event: 'progress',
289
- phase: 'validation',
290
- message: `Harness report validation ${validation.status}.`,
291
- elapsed_ms: Date.now() - context.startedAt,
292
- });
293
- writeHumanProgress(context, `Moda Analyst (remote): validation ${validation.status}.`);
294
- const written = [
295
- ...writeHarnessReportArtifacts({
296
- rootDir,
297
- report,
298
- validation,
299
- markdown: context.flags.summary === 'true' || context.outputMode === 'human',
300
- }),
301
- ];
302
- if (validation.status === 'pass') {
303
- written.push(...writeHarnessArtifacts(report.graph, rootDir, {
304
- summary: context.flags.summary === 'true',
305
- }));
306
- }
307
- emitWrittenArtifacts(context, written, 'Written by Moda Analyst (remote).');
308
- markRemoteRunTerminal(rootDir, runId, validation.status === 'pass' ? 'completed' : 'failed');
309
- writeHarnessAnalyzeResult(context, {
310
- scanPlan: options.scanPlan,
311
- summary: validation.status === 'pass'
312
- ? summarizeHarness(report.graph)
313
- : summarizeInvalidAnalystReport(report),
314
- report,
315
- validation,
316
- written,
317
- dryRun: false,
318
- });
319
- return { exitCode: validation.status === 'pass' ? 0 : 1 };
320
- }
321
- /**
322
- * Server-side ("Moda-hosted") harness analysis. Builds a safe source snapshot,
323
- * uploads it to the ingest worker, polls for the validated report, then writes
324
- * the same local artifacts the local analyze path writes. Emits the same
325
- * lifecycle events so `--agent --stream` behaves consistently with the local
326
- * path.
327
- */
328
- export async function runRemoteHarnessAnalyst(options) {
329
- const { context, rootDir } = options;
330
- const { runId, snapshotTruncated } = await startRemoteAnalyze({ context, rootDir });
331
- const final = await pollRemoteAnalyzeUntilDone({ context, runId });
332
- return finalizeRemoteAnalyzeResult({
333
- context,
334
- rootDir,
335
- scanPlan: options.scanPlan,
336
- runId,
337
- final,
338
- snapshotTruncated,
339
- });
340
- }
341
- /**
342
- * `moda harness pull [--run-id=<id>] [--sync]`: fetch the status of a
343
- * server-side analysis run once (default run id from
344
- * `.moda/harness-remote-run.json`). Still running: print status + progress and
345
- * exit 0. Completed: finalize exactly like `harness analyze --remote` (repair,
346
- * validate, write `.moda` artifacts); with `--sync` also approve + `harness
347
- * sync --from-report` best-effort, the way `moda init` does. Failed: error,
348
- * exit 1.
349
- */
350
- export async function runHarnessPull(options) {
351
- const { context, rootDir } = options;
352
- const state = readRemoteRunState(rootDir);
353
- const flagRunId = stringOption(context.flags['run-id'] ?? context.flags.run_id);
354
- const runId = flagRunId ?? state?.run_id;
355
- if (!runId) {
356
- throw new CliInputError('No server-side harness analysis run to pull.', 'Start one with `moda harness analyze --remote` (or `moda init`), or pass --run-id=<id>.');
357
- }
358
- const { apiKey, ingestUrl } = resolveRemoteAnalyzeEndpoint(context);
359
- const url = `${ingestUrl}/v1/harness/analyze/${encodeURIComponent(runId)}`;
360
- const status = await remoteAnalyzeFetch(url, apiKey, { method: 'GET' }, REMOTE_STATUS_TIMEOUT_MS);
361
- if (status?.status === 'accepted' || status?.status === 'running') {
362
- const progress = normalizeRemoteProgress(status.progress);
363
- writeHumanProgress(context, `Moda Analyst (remote): run ${runId} is still ${status.status}${progress?.message ? ` (${progress.message})` : ''}.`);
364
- context.output.writeData({
365
- runId,
366
- status: status.status,
367
- progress,
368
- startedAt: state && state.run_id === runId && state.started_at_iso ? state.started_at_iso : undefined,
369
- hint: 'Run `moda harness pull` again in a few minutes.',
370
- });
371
- return { exitCode: 0 };
372
- }
373
- const result = finalizeRemoteAnalyzeResult({
374
- context,
375
- rootDir,
376
- scanPlan: options.scanPlan,
377
- runId,
378
- final: status,
379
- });
380
- if (result.exitCode === 0 && isTruthyFlagValue(context.flags.sync)) {
381
- await approveAndSyncPulledReport(context);
382
- }
383
- return result;
384
- }
385
- /** Accept every truthy spelling a bare or internal `--sync` flag can arrive as. */
386
- function isTruthyFlagValue(value) {
387
- return value === true || value === 1 || value === 'true' || value === '1' || value === 'yes';
388
- }
389
- /**
390
- * Best-effort approve + `harness sync --from-report`, the same follow-up calls
391
- * `moda init` makes after a successful analyze. A network blip must not fail
392
- * the pull; the report artifacts are already on disk, and the user gets the
393
- * manual recovery command instead.
394
- */
395
- async function approveAndSyncPulledReport(context) {
396
- const machine = context.outputMode !== 'human';
397
- const agentMode = context.outputMode === 'agent' || context.outputMode === 'agent-stream';
398
- const subFlags = {
399
- yes: 'true',
400
- quiet: 'true',
401
- 'no-telemetry': 'true',
402
- ...(machine ? { json: 'true' } : {}),
403
- ...(agentMode ? { agent: 'true' } : {}),
404
- ...(context.profile ? { profile: context.profile } : {}),
405
- };
406
- try {
407
- await runHarnessCommand(createCommandContext({
408
- command: 'harness',
409
- positional: 'approve',
410
- positionals: ['approve'],
411
- flags: subFlags,
412
- }, { cwd: context.cwd, env: context.env }));
413
- await runHarnessCommand(createCommandContext({
414
- command: 'harness',
415
- positional: 'sync',
416
- positionals: ['sync'],
417
- flags: { ...subFlags, 'from-report': 'true' },
418
- }, { cwd: context.cwd, env: context.env }));
419
- writeHumanProgress(context, 'Moda Analyst (remote): harness graph synced to Moda.');
420
- }
421
- catch (error) {
422
- const message = error instanceof Error ? error.message : String(error);
423
- context.output.writeEvent({
424
- event: 'warning',
425
- phase: 'remote_sync',
426
- message: `Harness sync skipped: ${message}`,
427
- elapsed_ms: Date.now() - context.startedAt,
428
- });
429
- writeHumanProgress(context, `Harness sync skipped: ${message}. Run \`moda harness approve --yes && moda harness sync --from-report\` later to upload.`);
430
- }
431
- }
432
- /** Build the terminal error for a remote run that cannot be recovered client-side. */
433
- function remoteAnalyzeHardFailure(final, runId) {
434
- const detail = final.status === 'failed'
435
- ? `Remote harness analysis failed: ${final.error ?? 'unknown error'}.`
436
- : `Remote harness analysis did not finish before the timeout (last status: ${final.status}).`;
437
- return new CliInputError(detail, [
438
- `Retry \`moda harness analyze --remote\`${runId ? ` (run ${runId})` : ''}.`,
439
- 'Or run `moda harness analyze --analyst=local-scan` to analyze locally.',
440
- 'Increase MODA_HARNESS_REMOTE_TIMEOUT_MS if large repos need longer.',
441
- ].join('\n'));
442
- }
443
- /**
444
- * True when a `failed` hosted run failed specifically because its own report
445
- * validation failed, the only server failure the client can recover. The hosted
446
- * service attaches a structured validation verdict (`{ status: 'fail', ... }`)
447
- * only when it produced a report that failed the report-contract check
448
- * (`apps/moda-harness-analyze` server.ts); an operational failure (crashed agent)
449
- * reports an `error` string with no validation object. Gating recovery on this
450
- * verdict keeps crashed/aborted runs as hard failures, and the mandatory
451
- * client-side re-validation still rejects any report that stays invalid after
452
- * body/citation repair.
453
- */
454
- function serverFailedReportValidation(final) {
455
- const { validation } = final;
456
- return typeof validation === 'object' && validation !== null && !Array.isArray(validation)
457
- && validation.status === 'fail';
458
- }
459
- /**
460
- * Strip ANSI/OSC escapes and bare control characters from a server-controlled
461
- * progress string before it can reach a terminal. The hosted-analyze `phase`/
462
- * `message` originate from LLM-agent tool inputs (file paths, commands) that a
463
- * hostile repo or prompt injection can lace with escape sequences; the container
464
- * relays them unsanitized. `moda init` scrubs them via the progress board's
465
- * `oneLine`, but the standalone `moda harness pull` human path prints them
466
- * directly — so sanitize once, here, where every consumer flows through. Mirrors
467
- * `oneLine`'s ANSI + control-char stripping (without the display truncation).
468
- */
469
- function sanitizeProgressText(text) {
470
- return stripAnsi(text)
471
- .replace(/[\u0000-\u001f\u007f]+/g, ' ')
472
- .replace(/\s+/g, ' ')
473
- .trim();
474
- }
475
- /** Keep only the documented progress fields; drop anything else a server sends. */
476
- function normalizeRemoteProgress(value) {
477
- if (typeof value !== 'object' || value === null || Array.isArray(value))
478
- return undefined;
479
- const raw = value;
480
- const progress = {};
481
- if (typeof raw.phase === 'string')
482
- progress.phase = sanitizeProgressText(raw.phase);
483
- if (typeof raw.message === 'string')
484
- progress.message = sanitizeProgressText(raw.message);
485
- if (typeof raw.tool_calls === 'number')
486
- progress.tool_calls = raw.tool_calls;
487
- if (typeof raw.updated_ms === 'number')
488
- progress.updated_ms = raw.updated_ms;
489
- return Object.keys(progress).length > 0 ? progress : undefined;
490
- }
491
- /** Map CLI flags to the optional server-side analyze options in the wire contract. */
492
- function buildRemoteAnalyzeOptions(flags) {
493
- const options = {};
494
- const analyst = flags.analyst;
495
- if (analyst === 'claude' || analyst === 'codex' || analyst === 'cursor' || analyst === 'auto') {
496
- options.agent = analyst;
497
- }
498
- const shards = Number(flags.shards);
499
- if (Number.isFinite(shards) && shards > 0)
500
- options.shards = Math.floor(shards);
501
- return Object.keys(options).length > 0 ? options : undefined;
502
- }
503
- async function postRemoteAnalyze(ingestUrl, apiKey, body) {
504
- const parsed = await remoteAnalyzeFetch(`${ingestUrl}/v1/harness/analyze`, apiKey, {
505
- method: 'POST',
506
- headers: { 'Content-Type': 'application/json' },
507
- body: JSON.stringify(body),
508
- }, REMOTE_UPLOAD_TIMEOUT_MS);
509
- if (!parsed || parsed.success !== true || typeof parsed.analysis_run_id !== 'string' || parsed.analysis_run_id.length === 0) {
510
- throw new CliInputError('Moda did not accept the harness analysis request.', 'The ingest worker returned an unexpected response. Retry, or run `moda harness analyze --analyst=local-scan`.');
511
- }
512
- return parsed;
513
- }
514
- async function remoteAnalyzeFetch(url, apiKey, init, timeoutMs) {
515
- const controller = new AbortController();
516
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
517
- let response;
518
- try {
519
- response = await fetch(url, {
520
- ...init,
521
- headers: { Authorization: `Bearer ${apiKey}`, ...(init.headers ?? {}) },
522
- signal: controller.signal,
523
- });
524
- }
525
- catch (error) {
526
- const isAbort = error instanceof DOMException && error.name === 'AbortError';
527
- throw new CliInputError(isAbort
528
- ? `Remote harness analysis request timed out after ${timeoutMs}ms.`
529
- : `Network error reaching Moda ingest: ${error instanceof Error ? error.message : String(error)}.`, 'Check your connection and MODA_INGEST_URL, then retry `moda harness analyze --remote`.');
530
- }
531
- finally {
532
- clearTimeout(timeout);
533
- }
534
- if (!response.ok) {
535
- const text = await response.text().catch(() => '');
536
- throw new CliInputError(`Remote harness analysis request failed (HTTP ${response.status}).`, text ? truncateText(text, 500) : 'Retry, or run `moda harness analyze --analyst=local-scan`.');
537
- }
538
- return response.json();
539
- }
540
- //# sourceMappingURL=harness-remote.js.map
@@ -1,145 +0,0 @@
1
- import type { HarnessGraph, HarnessScanReadPlan } from './harness.js';
2
- export declare const HARNESS_REPORT_SCHEMA_VERSION = "harness_report.v0.1";
3
- export declare const HARNESS_REPORT_VALIDATION_SCHEMA_VERSION = "harness_report_validation.v0.1";
4
- export declare const HARNESS_REPORT_APPROVAL_SCHEMA_VERSION = "harness_report_approval.v0.1";
5
- export declare const HARNESS_REPORT_PATH: string;
6
- export declare const HARNESS_REPORT_MARKDOWN_PATH: string;
7
- export declare const HARNESS_REPORT_VALIDATION_PATH: string;
8
- export declare const HARNESS_REPORT_APPROVAL_PATH: string;
9
- type HarnessReportClaimKind = 'runtime_agent' | 'agent_family' | 'developer_agent_adapter' | 'artifact' | 'channel_adapter' | 'identity_namespace' | 'environment' | 'agent_framework' | 'model_provider' | 'deployment' | 'relationship' | 'unknown';
10
- export interface HarnessReportCitation {
11
- id: string;
12
- type: 'file' | 'generated_doc' | 'command_output' | 'production_evidence';
13
- path?: string;
14
- line?: number;
15
- reason: string;
16
- excerpt?: string;
17
- }
18
- export interface HarnessReportClaim {
19
- id: string;
20
- kind: HarnessReportClaimKind;
21
- subjectId: string;
22
- title: string;
23
- summary: string;
24
- confidence: number;
25
- inference: 'direct' | 'inferred' | 'mixed';
26
- citationIds: string[];
27
- }
28
- export interface HarnessReport {
29
- schemaVersion: typeof HARNESS_REPORT_SCHEMA_VERSION;
30
- reportId: string;
31
- harnessId: string;
32
- createdAt: string;
33
- analyst: {
34
- id: string;
35
- adapter: 'claude' | 'codex' | 'cursor' | 'local-scan' | 'moda' | string;
36
- mode: 'agent' | 'local';
37
- version: string;
38
- };
39
- repo: HarnessGraph['repo'];
40
- tenant?: HarnessGraph['tenant'];
41
- source: {
42
- graphSchemaVersion: string;
43
- graphHash: string;
44
- sourceCommit?: string;
45
- readPlan: HarnessScanReadPlan;
46
- deterministicInventory: boolean;
47
- };
48
- summary: HarnessReportSummary;
49
- graph: HarnessGraph;
50
- claims: HarnessReportClaim[];
51
- citations: HarnessReportCitation[];
52
- syncPlan: {
53
- command: string;
54
- dryRunCommand: string;
55
- uploads: string[];
56
- privacy: string[];
57
- };
58
- unknowns: Array<{
59
- id: string;
60
- question: string;
61
- citationIds: string[];
62
- }>;
63
- }
64
- export interface HarnessReportSummary {
65
- runtimeAgentCount: number;
66
- agentFamilyCount: number;
67
- artifactCount: number;
68
- sharedArtifactCount: number;
69
- channelAdapterCount: number;
70
- identityNamespaceCount: number;
71
- environmentCount: number;
72
- agentFrameworkCount: number;
73
- modelProviderCount: number;
74
- deploymentCount: number;
75
- relationshipCount: number;
76
- unknownCount: number;
77
- }
78
- export interface HarnessReportValidation {
79
- schemaVersion: typeof HARNESS_REPORT_VALIDATION_SCHEMA_VERSION;
80
- reportId: string;
81
- reportHash: string;
82
- status: 'pass' | 'fail';
83
- checkedAt: string;
84
- checks: Array<{
85
- id: string;
86
- status: 'pass' | 'fail' | 'warning';
87
- message: string;
88
- }>;
89
- errors: string[];
90
- warnings: string[];
91
- }
92
- export interface HarnessReportApproval {
93
- schemaVersion: typeof HARNESS_REPORT_APPROVAL_SCHEMA_VERSION;
94
- reportId: string;
95
- reportHash: string;
96
- approved: true;
97
- approvedAt: string;
98
- approvedBy: 'local-human' | '--yes';
99
- reportPath: string;
100
- }
101
- export declare function createHarnessReport(options: {
102
- graph: HarnessGraph;
103
- rootDir: string;
104
- sourceCommit?: string;
105
- analystAdapter?: string;
106
- }): HarnessReport;
107
- export declare function validateHarnessReport(report: HarnessReport, rootDir: string): HarnessReportValidation;
108
- export declare function approveHarnessReport(options: {
109
- rootDir: string;
110
- reportPath: string;
111
- yes: boolean;
112
- interactive: boolean;
113
- }): Promise<{
114
- report: HarnessReport;
115
- validation: HarnessReportValidation;
116
- approval: HarnessReportApproval;
117
- }>;
118
- export declare function loadHarnessReportForSync(rootDir: string, reportPath: string): {
119
- report: HarnessReport;
120
- validation: HarnessReportValidation;
121
- approval: HarnessReportApproval | null;
122
- approved: boolean;
123
- };
124
- export declare function readHarnessReport(reportPath: string): HarnessReport;
125
- export declare function normalizeHarnessReport(value: unknown): HarnessReport;
126
- /** prompt/tool artifacts must carry full usable content for downstream optimization. */
127
- export declare function requiresArtifactBody(type: unknown): boolean;
128
- export declare function artifactHasBody(item: Record<string, unknown>): boolean;
129
- /** Canonical evidence path for graph items: prefer explicit sourcePath, then evidence. */
130
- export declare function firstEvidencePath(item: Record<string, unknown>): string | undefined;
131
- /** Stamp remote snapshot truncation onto a validated report graph scan metadata. */
132
- export declare function stampRemoteSnapshotTruncation(report: HarnessReport): HarnessReport;
133
- export declare function resolveHarnessReportPath(rootDir: string, value?: string): string;
134
- export declare function writeHarnessReportArtifacts(options: {
135
- rootDir: string;
136
- report: HarnessReport;
137
- validation: HarnessReportValidation;
138
- markdown?: boolean;
139
- }): string[];
140
- export declare function renderHarnessReportMarkdown(report: HarnessReport, validation: HarnessReportValidation): string;
141
- export declare function hashHarnessReport(report: HarnessReport): string;
142
- export declare function hashStableJson(value: unknown): string;
143
- export declare function graphForReport(graph: HarnessGraph): HarnessGraph;
144
- export {};
145
- //# sourceMappingURL=harness-report.d.ts.map