@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,1442 +0,0 @@
1
- // src/init/index.ts. Full moda init wizard orchestrator.
2
- //
3
- // Five visible steps: Sign in, Workspace, Plan, Set up, Verify. Every prompt
4
- // lives in Step 3 (Plan); Step 4 then runs unattended, firing the hosted
5
- // harness analysis first and overlapping it with the fast local tasks and the
6
- // SDK integration agent while a live progress board (progress-board.ts)
7
- // renders each lane.
8
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
9
- import { dirname, join } from 'node:path';
10
- import { checkbox, confirm, select } from '@inquirer/prompts';
11
- import { login, loadToken } from '../auth.js';
12
- import { selectTenantAndCreateKey } from './tenant.js';
13
- import { detectAgents } from './mcp.js';
14
- import { installRules } from './rules.js';
15
- import { initPrompts, runPromptSync } from '../prompts.js';
16
- import { runSkillSync } from '../skills.js';
17
- import { SDK_GUIDE_PATH, writeSdkIntegrationGuide } from './sdk-guide.js';
18
- import { runSdkIntegration } from './sdk-integration.js';
19
- import { fetchAuditWithApiKey } from './sdk-verification.js';
20
- import { InitProgressBoard } from './progress-board.js';
21
- import { createCommandContext } from '../command-context.js';
22
- import { resolveModaBaseUrl, resolveIngestUrl, VERSION } from '../config.js';
23
- import { CliAuthError, CliInputError } from '../errors.js';
24
- import { describeSyncIdentity, describeSyncTenantLabel, detectAvailableCodingAgents, isDeterministicLocalAnalyst, runHarnessCommand, } from '../harness.js';
25
- import { pollRemoteAnalyzeUntilDone, startRemoteAnalyze, } from '../harness-remote.js';
26
- import { buildDoctorReport } from '../doctor.js';
27
- import { renderInitBanner, renderInitComplete, renderInitNote, renderInitSetupPlan, renderInitStep, } from '../tui.js';
28
- /** Recovery command printed whenever init detaches from a hosted analysis run. */
29
- const HARNESS_PULL_RECOVERY = 'moda harness pull --sync';
30
- /** Minimum gap between board-driven progress events per lane in machine modes. */
31
- const LANE_EVENT_THROTTLE_MS = 2_000;
32
- export async function runInit(flags, options = {}) {
33
- const explicitYes = flags.yes === true ||
34
- flags.yes === 'true' ||
35
- flags.y === true ||
36
- flags.y === 'true';
37
- const json = flags.json === true || flags.json === 'true';
38
- const agent = flags.agent === true || flags.agent === 'true';
39
- const human = !json && !agent;
40
- const yes = explicitYes || json || agent;
41
- const phases = [];
42
- const log = (message) => {
43
- if (human)
44
- console.log(message);
45
- };
46
- const warn = (message) => {
47
- if (human)
48
- console.error(message);
49
- };
50
- const emit = (phase, message, status) => {
51
- options.onEvent?.({ phase, message, status });
52
- };
53
- // Board-driven lane updates arrive as fast as the underlying stream; machine
54
- // modes get them as progress events throttled to one per lane per 2s so
55
- // agent NDJSON stays readable (the human board repaints at 150ms instead).
56
- const laneEventAt = new Map();
57
- const emitLaneProgress = (phase, laneId, message) => {
58
- if (!options.onEvent || human)
59
- return;
60
- const now = Date.now();
61
- if (now - (laneEventAt.get(laneId) ?? 0) < LANE_EVENT_THROTTLE_MS)
62
- return;
63
- laneEventAt.set(laneId, now);
64
- emit(phase, message);
65
- };
66
- log(renderInitBanner());
67
- emit('welcome', 'Onboarding this project as a Moda agent harness.', 'completed');
68
- phases.push({ id: 'welcome', title: 'Welcome', status: 'completed' });
69
- // Step 1: Sign in.
70
- // Every auth touchpoint below (cached-token check, browser login, tenant
71
- // setup) must share the same --profile context, or `moda init --profile X`
72
- // logs in / stores under the default profile while the rest of init reads
73
- // from X, and the session then looks unauthenticated or uses the wrong key.
74
- const profile = stringFlag(flags.profile);
75
- log(renderInitStep(1, 5, 'Sign in', 'No environment variable files are written. File changes are listed before completion.'));
76
- let token = loadToken({ profile });
77
- if (!token) {
78
- if (json || agent) {
79
- throw new CliAuthError('Browser authentication is required before non-interactive init can continue.', {
80
- reason: 'no_stored_credentials',
81
- login_command: ['moda', 'auth', 'login'],
82
- resume_command: ['moda', 'init', ...resumeFlagsForInit(flags)],
83
- }, 'Run `moda auth login`, then re-run `moda init`.');
84
- }
85
- let action = 'login';
86
- if (!yes) {
87
- action = await select({
88
- message: 'Get started',
89
- choices: [
90
- { name: 'Sign up', value: 'signup' },
91
- { name: 'Login', value: 'login' },
92
- ],
93
- });
94
- }
95
- log(renderInitNote(`Opening browser to ${action === 'signup' ? 'sign up' : 'log in'}...`, 'warning'));
96
- emit('auth', `Opening browser to ${action}.`);
97
- token = await login(action, { profile });
98
- log(renderInitNote('Authentication successful'));
99
- }
100
- else {
101
- log(renderInitNote('Already authenticated (cached token)'));
102
- }
103
- emit('auth', 'Authentication complete.', 'completed');
104
- phases.push({ id: 'auth', title: 'Sign in', status: 'completed' });
105
- // Step 2: Workspace (tenant + API key).
106
- log(renderInitStep(2, 5, 'Workspace'));
107
- emit('workspace', 'Selecting workspace and creating API key.');
108
- // --tenant-id flag wins over MODA_TENANT_ID; the source drives the
109
- // env-shadow warning so we don't warn when the env var was the selection.
110
- const tenantIdFlag = stringFlag(flagValue(flags, 'tenant-id', 'tenant_id'));
111
- // Normalize the env var the same way as the flag: an empty/whitespace
112
- // MODA_TENANT_ID (e.g. `export MODA_TENANT_ID=`, or a harness that blanks it
113
- // to clear an inherited value) means "unset", not an explicit empty-id
114
- // selection; otherwise the exact-id match below fails with
115
- // "No organization with id ''" instead of auto-selecting a sole org.
116
- const tenantIdEnv = stringFlag(process.env.MODA_TENANT_ID);
117
- const { apiKey, tenantId: selectedTenantId, tenantName } = await selectTenantAndCreateKey({
118
- tenantId: tenantIdFlag ?? tenantIdEnv,
119
- tenantIdSource: tenantIdFlag ? 'flag' : tenantIdEnv ? 'env' : undefined,
120
- nonInteractive: json || agent,
121
- silent: !human,
122
- resumeCommand: ['moda', 'init', ...resumeFlagsForInit(flags)],
123
- profile,
124
- });
125
- log(renderInitNote(`Active workspace: ${tenantName}`));
126
- emit('workspace', `Active workspace: ${tenantName}`, 'completed');
127
- phases.push({ id: 'workspace', title: 'Workspace', status: 'completed', details: tenantName });
128
- // Step 3: Plan. Every remaining decision lives in this single prompt beat;
129
- // Step 4 then runs unattended with zero prompts. Explicit flags keep
130
- // overriding exactly as before, and --yes/--json/--agent never prompt
131
- // (optional items default off, snapshot consent implied).
132
- log(renderInitStep(3, 5, 'Plan', 'One confirmation covers everything below; setup then runs unattended.'));
133
- let remote = resolveHarnessRemote(flags);
134
- const remotePinned = hasExplicitRemoteChoice(flags);
135
- const sdkFlag = resolveSdkIntegrateFlag(flags);
136
- const ciCdFlag = resolveCiCdFlag(flags);
137
- const autosyncFlag = resolveAutosyncFlag(flags);
138
- const harnessRescanFlag = resolveHarnessRescanFlag(flags);
139
- const harnessRescanPaths = parseHarnessRescanPaths(flagValue(flags, 'harness-rescan-paths', 'harness_rescan_paths'));
140
- // Passing --harness-rescan-paths alone is an explicit opt-in: scoping the
141
- // rescan only makes sense when the workflow is being written.
142
- const harnessRescanChoice = harnessRescanFlag ?? (harnessRescanPaths !== undefined ? true : undefined);
143
- // SDK integration installs packages and edits source via a coding agent, so
144
- // it defaults ON only when an agent CLI is installed to do the work; it
145
- // never runs unattended without an explicit --sdk-integrate.
146
- const codingAgentAvailable = detectAvailableCodingAgents().length > 0;
147
- let integrateSdk = sdkFlag !== undefined ? sdkFlag : yes ? false : codingAgentAvailable;
148
- let configureCiCd = ciCdFlag ?? false;
149
- let configureAutosync = autosyncFlag ?? false;
150
- let configureHarnessRescan = harnessRescanChoice ?? false;
151
- let setupPlan = [];
152
- const renderPlan = () => {
153
- setupPlan = buildSetupPlan({
154
- installHooks: false,
155
- integrateSdk,
156
- configureCiCd,
157
- configureAutosync,
158
- configureHarnessRescan,
159
- remote,
160
- });
161
- log(renderInitSetupPlan(setupPlan, { snapshotConsent: remote }));
162
- };
163
- renderPlan();
164
- if (!yes) {
165
- const proceed = await confirm({ message: 'Proceed with this plan?', default: true });
166
- if (!proceed) {
167
- const choices = [];
168
- if (sdkFlag === undefined) {
169
- choices.push({
170
- name: 'SDK integration via your coding agent (installs packages, edits source)',
171
- value: 'sdk',
172
- checked: integrateSdk,
173
- });
174
- }
175
- if (!remotePinned) {
176
- choices.push({
177
- name: "Harness analysis on Moda's servers (uncheck to run a local coding agent analyst)",
178
- value: 'remote',
179
- checked: remote,
180
- });
181
- }
182
- if (ciCdFlag === undefined) {
183
- choices.push({
184
- name: 'CI/CD validation workflow (.github/workflows/moda-validation.yml)',
185
- value: 'ci-cd',
186
- checked: configureCiCd,
187
- });
188
- }
189
- if (autosyncFlag === undefined) {
190
- choices.push({
191
- name: 'Autosync prompts and skills on push to main (.github/workflows/moda-autosync.yml)',
192
- value: 'autosync',
193
- checked: configureAutosync,
194
- });
195
- }
196
- if (harnessRescanChoice === undefined) {
197
- choices.push({
198
- name: 'Re-scan the harness on push to main (.github/workflows/moda-harness-rescan.yml)',
199
- value: 'harness-rescan',
200
- checked: configureHarnessRescan,
201
- });
202
- }
203
- if (choices.length > 0) {
204
- const selected = await checkbox({
205
- message: 'Choose what runs (space toggles, enter confirms)',
206
- choices,
207
- });
208
- if (sdkFlag === undefined)
209
- integrateSdk = selected.includes('sdk');
210
- if (!remotePinned)
211
- remote = selected.includes('remote');
212
- if (ciCdFlag === undefined)
213
- configureCiCd = selected.includes('ci-cd');
214
- if (autosyncFlag === undefined)
215
- configureAutosync = selected.includes('autosync');
216
- if (harnessRescanChoice === undefined)
217
- configureHarnessRescan = selected.includes('harness-rescan');
218
- renderPlan();
219
- }
220
- }
221
- }
222
- if (json || agent) {
223
- const skipped = setupPlan
224
- .filter((action) => (action.category === 'sdk' || action.category === 'ci-cd' || action.category === 'autosync' ||
225
- action.category === 'harness-rescan') &&
226
- action.approved === false)
227
- .map((action) => `${action.category}: ${action.command ?? 'explicit flag required'}`);
228
- if (skipped.length > 0) {
229
- emit('plan', `Non-interactive init skipped optional actions (${skipped.join('; ')}).`, 'warning');
230
- }
231
- }
232
- emit('plan', 'Setup plan ready.', 'completed');
233
- phases.push({ id: 'plan', title: 'Plan', status: 'completed' });
234
- // Step 4: Set up. Zero prompts from here on. The hosted harness analysis is
235
- // fired FIRST and polled concurrently while the fast local tasks and the SDK
236
- // integration agent run; a live progress board renders each lane in human
237
- // mode. Local-analyst mode stays sequential (AnalystTui provides its own
238
- // live progress).
239
- log(renderInitStep(4, 5, 'Set up', remote
240
- ? 'Harness analysis runs on Moda while local setup continues. No further input is needed.'
241
- : 'Runs unattended. No further input is needed.'));
242
- const rootDir = process.cwd();
243
- // An explicit --remote pin means "never fall back to a local coding agent".
244
- const allowLocalFallback = !isTruthyFlag(flagValue(flags, 'remote'));
245
- const nestedHarnessFlags = (extra) => commandFlags({
246
- summary: true,
247
- // The Step 3 plan confirm covers the broad local read and (for remote)
248
- // the snapshot upload, so nested harness commands must never re-prompt.
249
- yes: true,
250
- json: json || agent,
251
- agent,
252
- quiet: true,
253
- 'no-telemetry': true,
254
- profile,
255
- ...extra,
256
- });
257
- const runAnalyze = (useRemote) => runHarnessCommand(createCommandContext({
258
- command: 'harness',
259
- positional: 'analyze',
260
- positionals: ['analyze'],
261
- flags: nestedHarnessFlags({
262
- analyst: stringFlag(flags.analyst) ?? 'auto',
263
- // Pass the remote/local decision EXPLICITLY. `commandFlags` drops
264
- // `remote: false`, so a bare `remote: useRemote` would leave the child
265
- // with no signal and it would re-derive remoteness from
266
- // `MODA_HARNESS_REMOTE`, silently overriding `--local` and the
267
- // remote-failure fallback. `--no-remote` survives (it's `true`).
268
- remote: useRemote ? true : undefined,
269
- 'no-remote': useRemote ? undefined : true,
270
- progress: true,
271
- }),
272
- }));
273
- let board;
274
- const startBoard = () => {
275
- if (!human)
276
- return undefined;
277
- board ??= new InitProgressBoard();
278
- return board;
279
- };
280
- const validationFailure = () => new CliInputError('Moda Analyst produced a harness report that did not pass validation.', 'Review .moda/harness-report-validation.json, then rerun `moda init --local --analyst=claude|cursor|codex` or `moda harness analyze --analyst=local-scan --summary`.');
281
- let harnessPhase = { id: 'harness', title: 'Harness discovery', status: 'completed' };
282
- let harnessDeferred = false;
283
- // Upload the cited harness graph so Moda's backend has the understanding,
284
- // not just local artifacts. `harness sync --from-report` refuses an
285
- // unapproved report, so approve it first. Best-effort: a network blip on
286
- // either call must not abort onboarding.
287
- const approveAndSyncHarness = async () => {
288
- // Quality gate: a result with ZERO runtime agents on a repo that clearly
289
- // uses LLM frameworks/providers almost always means discovery failed (bad
290
- // snapshot, analyst error), not an agent-free repo. Auto-approving and
291
- // syncing it would overwrite the tenant's harness with an empty graph, so
292
- // withhold the upload and hand the user the retry commands instead.
293
- // The framework/provider evidence is the union of what the discovered
294
- // graph self-reports AND a deterministic repo-side dependency scan — a
295
- // TOTAL discovery failure (0 agents, 0 frameworks, 0 providers) must not
296
- // bypass the gate just because the artifact being judged is empty.
297
- const graphSignals = readHarnessQualitySignals(rootDir);
298
- const repoSignals = detectRepoLlmSignals(rootDir);
299
- const signals = graphSignals
300
- ? {
301
- runtimeAgents: graphSignals.runtimeAgents,
302
- frameworks: [...new Set([...graphSignals.frameworks, ...repoSignals.frameworks])],
303
- providers: [...new Set([...graphSignals.providers, ...repoSignals.providers])],
304
- }
305
- : undefined;
306
- if (signals && harnessLooksIncomplete(signals)) {
307
- const names = [...signals.frameworks, ...signals.providers].join(', ');
308
- const retryCommand = remote
309
- ? 'moda harness analyze --remote --summary'
310
- : 'moda harness analyze --analyst=claude|cursor|codex --summary';
311
- warn(renderInitNote(`Harness discovery found 0 runtime agents, but this repo has LLM framework signals (${names}). ` +
312
- 'Discovery likely failed, so the result was NOT approved or synced.', 'warning'));
313
- warn(` Retry with \`${retryCommand}\`, then \`moda harness approve --yes && moda harness sync --from-report\`.`);
314
- emit('harness', 'Harness discovery found 0 agents despite LLM framework signals; sync withheld.', 'warning');
315
- harnessPhase = {
316
- id: 'harness',
317
- title: 'Harness discovery',
318
- status: 'warning',
319
- details: '0 agents detected; sync withheld',
320
- };
321
- return;
322
- }
323
- // Identity banner: the credential is resolved with the same precedence
324
- // the upload uses (MODA_API_KEY env wins) and the tenant comes from the
325
- // WINNING KEY itself (or is marked unverified), so a shadowed/stale key
326
- // is visible — and never mislabeled — BEFORE the graph lands in the
327
- // wrong tenant. Best-effort: must not abort onboarding.
328
- try {
329
- const identity = describeSyncIdentity({ profile, env: process.env });
330
- if (identity) {
331
- // The friendly workspace name only decorates the id when the winning
332
- // key's embedded tenant IS the tenant init just selected — otherwise
333
- // the raw (possibly surprising) destination id stands alone.
334
- const tenantLabel = identity.tenantSource === 'api-key' && identity.tenantId
335
- ? `${identity.tenantId}${identity.tenantId === selectedTenantId ? ` (${tenantName})` : ''}`
336
- : describeSyncTenantLabel(identity);
337
- log(renderInitNote(`Syncing as ${identity.credential} → tenant ${tenantLabel}`, 'info'));
338
- }
339
- }
340
- catch {
341
- // Advisory banner only.
342
- }
343
- try {
344
- await runHarnessCommand(createCommandContext({
345
- command: 'harness',
346
- positional: 'approve',
347
- positionals: ['approve'],
348
- flags: nestedHarnessFlags({}),
349
- }));
350
- await runHarnessCommand(createCommandContext({
351
- command: 'harness',
352
- positional: 'sync',
353
- positionals: ['sync'],
354
- flags: nestedHarnessFlags({ 'from-report': true }),
355
- }));
356
- log(renderInitNote('Synced harness graph to Moda'));
357
- emit('harness', 'Harness graph synced to Moda.', 'completed');
358
- }
359
- catch (err) {
360
- const message = err instanceof Error ? err.message : String(err);
361
- warn(renderInitNote(`Harness sync skipped: ${message}`, 'warning'));
362
- warn(' Run `moda harness approve --yes && moda harness sync --from-report` later to upload.');
363
- emit('harness', 'Harness sync skipped.', 'warning');
364
- harnessPhase = { id: 'harness', title: 'Harness discovery', status: 'warning', details: 'sync skipped' };
365
- }
366
- };
367
- const finishLocalAnalyze = async () => {
368
- emit('harness', 'Running Moda Analyst harness discovery.');
369
- const result = await runAnalyze(false);
370
- if (result.exitCode !== 0)
371
- throw validationFailure();
372
- log(renderInitNote('Wrote or verified .moda/harness-report.json, .moda/harness.json, and .moda/HARNESS.md'));
373
- if (human)
374
- printHarnessDetectionSummary();
375
- emit('harness', 'Harness discovery complete.', 'completed');
376
- await approveAndSyncHarness();
377
- };
378
- // Fire the hosted analysis first so it overlaps everything below.
379
- let pollPromise;
380
- let pollError;
381
- let remoteRunId;
382
- // Covers the whole overlapped window (fast tasks, SDK agent, final await):
383
- // a Ctrl-C leaves the hosted run resumable and prints the recovery command.
384
- const onRemoteSigint = () => {
385
- board?.stop();
386
- process.stderr.write(`\nHarness analysis keeps running on Moda. Resume anytime with \`${HARNESS_PULL_RECOVERY}\`.\n`);
387
- process.exit(130);
388
- };
389
- if (remote) {
390
- emit('harness', 'Running Moda-hosted harness discovery.');
391
- try {
392
- const harnessContext = createCommandContext({
393
- command: 'harness',
394
- positional: 'analyze',
395
- positionals: ['analyze'],
396
- flags: nestedHarnessFlags({
397
- analyst: stringFlag(flags.analyst) ?? 'auto',
398
- remote: true,
399
- }),
400
- });
401
- const started = await startRemoteAnalyze({ context: harnessContext, rootDir });
402
- remoteRunId = started.runId;
403
- // From this point Ctrl-C is safe and resumable: the state file already
404
- // says `running`, so exiting early just detaches from the hosted run.
405
- process.once('SIGINT', onRemoteSigint);
406
- emit('harness', `Moda accepted harness analysis run ${started.runId}.`);
407
- startBoard()?.startLane('harness', 'Harness analysis on Moda', {
408
- detail: 'waiting for the hosted analyst',
409
- expectation: 'usually 2-4 min',
410
- });
411
- pollPromise = pollRemoteAnalyzeUntilDone({
412
- context: harnessContext,
413
- runId: started.runId,
414
- onProgress: ({ status, progress }) => {
415
- if (status === 'completed' || status === 'failed') {
416
- // The hosted run just turned terminal, but the lane is only
417
- // finalized after the SDK lane below. Freeze it now so the board
418
- // does not keep a live spinner and climbing elapsed for minutes
419
- // after the analysis actually finished; the post-setup pull
420
- // refines the note (the board allows terminal refinements).
421
- if (status === 'completed') {
422
- board?.complete('harness', 'analysis finished on Moda; finalizing after setup');
423
- }
424
- else {
425
- board?.warn('harness', 'hosted run finished with issues; finalizing after setup');
426
- }
427
- emitLaneProgress('harness', 'harness', `Hosted analysis ${status}; finalizing after setup.`);
428
- return;
429
- }
430
- const message = progress?.message
431
- ?? (typeof progress?.tool_calls === 'number'
432
- ? `${progress.tool_calls} tool call(s) so far`
433
- : `analysis ${status}`);
434
- board?.update('harness', message);
435
- emitLaneProgress('harness', 'harness', message);
436
- },
437
- }).catch((error) => {
438
- // Captured, not thrown: an unhandled rejection here would crash init
439
- // in the middle of the fast tasks. Inspected after the SDK lane.
440
- pollError = error;
441
- return undefined;
442
- });
443
- }
444
- catch (error) {
445
- // A remote failure (service down, upload error) must not sink
446
- // onboarding; fall back to the local analyst path unless --remote was
447
- // pinned explicitly.
448
- if (!allowLocalFallback)
449
- throw error;
450
- warn(renderInitNote(`Server-side harness discovery unavailable (${error instanceof Error ? error.message : String(error)}); falling back to local analyst.`, 'warning'));
451
- emit('harness', 'Server-side discovery failed; falling back to local analyst.', 'warning');
452
- remote = false;
453
- }
454
- }
455
- // Local-analyst mode keeps today's sequential behavior: analyze (AnalystTui
456
- // renders live progress), then approve + sync, then the fast tasks below.
457
- if (!remote) {
458
- await finishLocalAnalyze();
459
- }
460
- emit('apply', 'Applying approved setup changes.');
461
- const laneBoard = startBoard();
462
- const ingestCodingAgentTraces = false;
463
- const agents = detectAgents();
464
- laneBoard?.startLane('agent-skills', 'Agent skills');
465
- if (agents.length > 0) {
466
- // The Step 3 plan already covered the skill install; never re-prompt.
467
- await installRules(agents, { yes: true, silent: true });
468
- laneBoard?.complete('agent-skills', `moda-cli skill installed for ${agents.join(', ')}`);
469
- }
470
- else {
471
- laneBoard?.complete('agent-skills', 'no coding agents detected');
472
- }
473
- await initPrompts({ silent: true });
474
- laneBoard?.startLane('prompts', 'Prompts', { detail: 'Created or verified .moda/prompts.yml' });
475
- // Push the manifest's prompt files to Moda so production traces can attribute
476
- // managed prompt versions. A fresh repo has none yet; that's a harmless no-op.
477
- try {
478
- const promptSync = await runPromptSync({}, { profile });
479
- laneBoard?.complete('prompts', promptSync.synced.length > 0
480
- ? `${promptSync.synced.length} prompt(s) synced`
481
- : 'manifest ready (no prompt files to sync yet)');
482
- }
483
- catch (err) {
484
- const message = err instanceof Error ? err.message : String(err);
485
- laneBoard?.warn('prompts', `sync skipped: ${message}`);
486
- }
487
- // Push locally-authored skills (`.claude/skills/**/SKILL.md`) to Moda, in
488
- // parallel intent with prompt sync. Best-effort: a network blip must not
489
- // abort onboarding.
490
- laneBoard?.startLane('skills', 'Skills');
491
- try {
492
- const skillSync = await runSkillSync({}, { profile });
493
- laneBoard?.complete('skills', skillSync.synced.length > 0
494
- ? `${skillSync.synced.length} skill(s) synced`
495
- : 'no local skills to sync yet');
496
- }
497
- catch (err) {
498
- const message = err instanceof Error ? err.message : String(err);
499
- laneBoard?.warn('skills', `sync skipped: ${message}`);
500
- }
501
- if (setupPlan.find((action) => action.category === 'ci-cd')?.approved === true) {
502
- const workflowPath = writeCiCdWorkflow();
503
- laneBoard?.startLane('ci-cd', 'CI/CD workflow');
504
- laneBoard?.complete('ci-cd', `wrote ${workflowPath}`);
505
- markApplied(setupPlan, 'ci-cd');
506
- }
507
- if (setupPlan.find((action) => action.category === 'autosync')?.approved === true) {
508
- const workflowPath = writeAutosyncWorkflow();
509
- laneBoard?.startLane('autosync', 'Autosync workflow');
510
- laneBoard?.complete('autosync', `wrote ${workflowPath}`);
511
- markApplied(setupPlan, 'autosync');
512
- }
513
- if (setupPlan.find((action) => action.category === 'harness-rescan')?.approved === true) {
514
- const workflowPath = writeHarnessRescanWorkflow(process.cwd(), harnessRescanPaths);
515
- laneBoard?.startLane('harness-rescan', 'Harness rescan workflow');
516
- laneBoard?.complete('harness-rescan', `wrote ${workflowPath}`);
517
- markApplied(setupPlan, 'harness-rescan');
518
- }
519
- const sdkAction = setupPlan.find((action) => action.category === 'sdk');
520
- let sdkIntegration;
521
- if (sdkAction?.approved === true) {
522
- laneBoard?.startLane('sdk', 'SDK integration', {
523
- detail: 'launching your coding agent',
524
- expectation: 'usually 3-10 min',
525
- });
526
- emit('apply', 'Running your coding agent to integrate the Moda SDK.');
527
- const verificationBaseUrl = resolveModaBaseUrl({ profile });
528
- sdkIntegration = await runSdkIntegration({
529
- apiKey,
530
- ingestUrl: resolveIngestUrl({ profile }),
531
- fetchAudit: (id) => fetchAuditWithApiKey(id, apiKey, verificationBaseUrl),
532
- onProgress: (message) => {
533
- laneBoard?.update('sdk', message);
534
- emitLaneProgress('apply', 'sdk', message);
535
- },
536
- onPhase: (phase, message) => {
537
- laneBoard?.update('sdk', message);
538
- emitLaneProgress('apply', 'sdk', message);
539
- },
540
- // Presence of onEvent selects the streaming agent invocation so tool
541
- // cards arrive incrementally; onProgress above receives the rendered
542
- // cards (describeCodingAgentEvent), which is all the lane needs.
543
- onEvent: () => { },
544
- });
545
- applySdkIntegrationOutcome(setupPlan, sdkIntegration, {
546
- laneBoard,
547
- writeGuide: () => writeSdkIntegrationGuide(rootDir),
548
- });
549
- }
550
- else {
551
- markApplied(setupPlan, 'sdk', 'User skipped SDK integration.');
552
- }
553
- markApplied(setupPlan, 'hooks', ingestCodingAgentTraces ? undefined : 'Coding-agent trace capture is temporarily unavailable.');
554
- markApplied(setupPlan, 'prompts');
555
- markApplied(setupPlan, 'skills');
556
- // Await the hosted analysis last: it has been polling concurrently since the
557
- // top of this step. Ctrl-C or a poll deadline leaves the run resumable
558
- // (state file stays `running`) and prints the pull recovery command.
559
- if (remote && pollPromise && remoteRunId) {
560
- const final = await pollPromise;
561
- process.removeListener('SIGINT', onRemoteSigint);
562
- const fallBackToLocal = async (error) => {
563
- board?.fail('harness', 'hosted analysis failed');
564
- board?.stop();
565
- if (!allowLocalFallback)
566
- throw error;
567
- warn(renderInitNote(`Server-side harness discovery failed (${error instanceof Error ? error.message : String(error)}); falling back to local analyst.`, 'warning'));
568
- emit('harness', 'Server-side discovery failed; falling back to local analyst.', 'warning');
569
- await finishLocalAnalyze();
570
- };
571
- if (pollError !== undefined) {
572
- await fallBackToLocal(pollError);
573
- }
574
- else if (!final || (final.status !== 'completed' && final.status !== 'failed')) {
575
- // The poll deadline passed while the hosted run kept going. Expected on
576
- // very large repos, and safe: the run state file stays `running`.
577
- harnessDeferred = true;
578
- harnessPhase = { id: 'harness', title: 'Harness discovery', status: 'warning', details: 'still running on Moda' };
579
- markApplied(setupPlan, 'harness', `Analysis still running on Moda; run \`${HARNESS_PULL_RECOVERY}\` to fetch and sync it.`);
580
- board?.warn('harness', `still running on Moda; run \`${HARNESS_PULL_RECOVERY}\` in a few minutes`);
581
- board?.stop();
582
- log(renderInitNote(`Harness analysis is still running on Moda. Run \`${HARNESS_PULL_RECOVERY}\` in a few minutes to fetch and sync the result.`, 'info'));
583
- emit('harness', `Harness analysis still running on Moda; resume with \`${HARNESS_PULL_RECOVERY}\`.`, 'warning');
584
- }
585
- else {
586
- board?.update('harness', 'writing local artifacts');
587
- let pullResult;
588
- try {
589
- // `harness pull` finalizes exactly like `harness analyze --remote`
590
- // (repair, local re-validation, .moda artifact writes) from the
591
- // already-terminal hosted run.
592
- pullResult = await runHarnessCommand(createCommandContext({
593
- command: 'harness',
594
- positional: 'pull',
595
- positionals: ['pull'],
596
- flags: nestedHarnessFlags({ 'run-id': remoteRunId }),
597
- }));
598
- }
599
- catch (error) {
600
- await fallBackToLocal(error);
601
- }
602
- if (pullResult) {
603
- if (pullResult.exitCode !== 0) {
604
- board?.fail('harness', 'report validation failed');
605
- board?.stop();
606
- throw validationFailure();
607
- }
608
- board?.complete('harness', 'report written to .moda/');
609
- board?.stop();
610
- log(renderInitNote('Wrote or verified .moda/harness-report.json, .moda/harness.json, and .moda/HARNESS.md'));
611
- if (human)
612
- printHarnessDetectionSummary();
613
- emit('harness', 'Harness discovery complete.', 'completed');
614
- await approveAndSyncHarness();
615
- }
616
- }
617
- }
618
- board?.stop();
619
- if (!harnessDeferred)
620
- markApplied(setupPlan, 'harness');
621
- phases.push(harnessPhase);
622
- emit('apply', 'Approved changes applied.', 'completed');
623
- phases.push({ id: 'apply', title: 'Set up', status: 'completed' });
624
- // Step 5: Verify.
625
- const offlineValidation = isTruthyFlag(flagValue(flags, 'offline'));
626
- log(renderInitStep(5, 5, 'Verify', offlineValidation
627
- ? 'Running local doctor checks so the setup leaves a clear state.'
628
- : 'Running Moda doctor online as a tenant health check (not proof of SDK integration).'));
629
- emit('validate', `Running Moda doctor diagnostics (${offlineValidation ? 'offline' : 'online'}).`);
630
- const doctorReport = await buildDoctorReport(createCommandContext({
631
- command: 'doctor',
632
- positional: undefined,
633
- positionals: [],
634
- flags: commandFlags({ json: true, quiet: true, 'no-telemetry': true }),
635
- }), { offline: offlineValidation });
636
- markApplied(setupPlan, 'validation');
637
- log(renderInitNote(`Validation checks completed: ${doctorReport.summary.pass} passing, ${doctorReport.summary.warning} warning, ${doctorReport.summary.fail} failing`));
638
- let telemetrySeen;
639
- if (!offlineValidation) {
640
- const cloud = doctorReport.checks.find((check) => check.id === 'cloud_data_flow');
641
- if (cloud) {
642
- telemetrySeen = cloud.status === 'pass';
643
- if (telemetrySeen) {
644
- log(renderInitNote('Moda Cloud telemetry: flowing'));
645
- }
646
- else {
647
- // Expected before the app has run with the SDK installed; surfaced as
648
- // a next step on the completion screen, not a warning badge.
649
- log(renderInitNote('No telemetry yet. Expected until your app runs with the SDK; see next steps.', 'info'));
650
- }
651
- }
652
- }
653
- emit('validate', 'Validation checks complete.', doctorReport.summary.fail > 0 ? 'warning' : 'completed');
654
- phases.push({ id: 'validate', title: 'Verify', status: 'completed' });
655
- // Done: end on the payoff.
656
- log(renderInitComplete({
657
- tenantName,
658
- dashboardUrl: `${resolveModaBaseUrl({ profile })}/dashboard`,
659
- harness: harnessDeferred
660
- ? { state: 'pending' }
661
- : { state: 'mapped', ...readHarnessGraphCounts() },
662
- filesChanged: changedFiles(setupPlan),
663
- skipped: setupPlan.filter((item) => item.status === 'skipped' || item.status === 'manual'),
664
- telemetrySeen,
665
- }));
666
- return { apiKey, tenantName, phases, setupPlan, ...(sdkIntegration ? { sdk_integration: sdkIntegration } : {}) };
667
- }
668
- function buildSetupPlan(options) {
669
- return [
670
- {
671
- category: 'harness',
672
- title: options.remote
673
- ? "Analyze the harness on Moda's servers, write the cited report, and sync the graph to Moda"
674
- : 'Analyze the harness locally, write the cited report, and sync the graph to Moda',
675
- status: 'planned',
676
- approved: true,
677
- files: [
678
- '.moda/harness-report.json',
679
- '.moda/harness-report-validation.json',
680
- '.moda/HARNESS_REPORT.md',
681
- '.moda/harness.json',
682
- '.moda/HARNESS.md',
683
- ],
684
- command: options.remote
685
- ? 'moda harness analyze --remote --summary && moda harness approve --yes && moda harness sync --from-report'
686
- : 'moda harness analyze --analyst=auto --summary && moda harness approve --yes && moda harness sync --from-report',
687
- },
688
- {
689
- category: 'hooks',
690
- title: 'Install coding-agent trace hooks',
691
- status: options.installHooks ? 'planned' : 'skipped',
692
- approved: options.installHooks,
693
- files: ['~/.claude/settings.json', '~/.codex/hooks.json', '~/.cursor/hooks.json'],
694
- command: 'moda hooks install --agent=all',
695
- reason: options.installHooks ? undefined : 'Coding-agent trace capture is temporarily unavailable.',
696
- },
697
- {
698
- category: 'prompts',
699
- title: 'Initialize the prompt registry manifest and sync prompts to Moda',
700
- status: 'planned',
701
- approved: true,
702
- files: ['.moda/prompts.yml'],
703
- command: 'moda prompts init && moda prompts sync',
704
- },
705
- {
706
- category: 'skills',
707
- title: 'Install the moda-cli skill into detected coding agents and sync local skills to Moda',
708
- status: 'planned',
709
- approved: true,
710
- files: ['.claude/skills', '.cursor/rules'],
711
- // Skill install into detected agents happens inside `moda init`; there is no
712
- // standalone install command, so surface only the re-runnable sync here.
713
- command: 'moda skills sync',
714
- },
715
- {
716
- category: 'sdk',
717
- title: 'Launch your coding agent to integrate the Moda SDK',
718
- status: options.integrateSdk ? 'planned' : 'manual',
719
- approved: options.integrateSdk,
720
- files: [],
721
- command: 'moda init --sdk-integrate',
722
- reason: options.integrateSdk
723
- ? 'Your agent investigates the repo, applies matching integration skill(s), installs packages, runs repo checks, sends one verification request, and Moda verifies the exact trace. Your Moda API key is provided to the agent\'s environment for the verification run.'
724
- : `SDK integration stays manual; see ${SDK_GUIDE_PATH} if you generate the guide.`,
725
- },
726
- {
727
- category: 'ci-cd',
728
- title: 'CI/CD validation workflow',
729
- status: options.configureCiCd ? 'planned' : 'manual',
730
- approved: options.configureCiCd,
731
- files: ['.github/workflows/moda-validation.yml'],
732
- command: 'moda doctor --online --json',
733
- reason: options.configureCiCd ? undefined : 'No CI/CD files are changed until a project-specific plan is approved.',
734
- },
735
- {
736
- category: 'autosync',
737
- title: 'Autosync prompts and skills to Moda on push to main',
738
- status: options.configureAutosync ? 'planned' : 'manual',
739
- approved: options.configureAutosync,
740
- files: ['.github/workflows/moda-autosync.yml'],
741
- command: 'moda prompts sync && moda skills sync',
742
- reason: options.configureAutosync ? undefined : 'No autosync workflow is written until it is explicitly approved (--autosync).',
743
- },
744
- {
745
- category: 'harness-rescan',
746
- title: 'Re-scan the harness on push to main so the synced graph never drifts from the code',
747
- status: options.configureHarnessRescan ? 'planned' : 'manual',
748
- approved: options.configureHarnessRescan,
749
- files: ['.github/workflows/moda-harness-rescan.yml'],
750
- command: 'moda harness analyze --remote --yes && moda harness approve --yes && moda harness sync --from-report',
751
- reason: options.configureHarnessRescan
752
- ? undefined
753
- : 'No harness rescan workflow is written until it is explicitly approved (--harness-rescan).',
754
- },
755
- {
756
- category: 'validation',
757
- title: 'Run Moda doctor online as a tenant health check',
758
- status: 'planned',
759
- approved: true,
760
- files: [],
761
- command: 'moda doctor --online',
762
- },
763
- ];
764
- }
765
- function markApplied(setupPlan, category, skipReason) {
766
- const action = setupPlan.find((item) => item.category === category);
767
- if (!action)
768
- return;
769
- if (skipReason) {
770
- action.status = 'skipped';
771
- action.reason = skipReason;
772
- return;
773
- }
774
- if (action.approved)
775
- action.status = 'applied';
776
- }
777
- function markManual(setupPlan, category, reason) {
778
- const action = setupPlan.find((item) => item.category === category);
779
- if (!action)
780
- return;
781
- action.status = 'manual';
782
- action.reason = reason;
783
- }
784
- function applySdkIntegrationOutcome(setupPlan, outcome, deps) {
785
- const sdkAction = setupPlan.find((action) => action.category === 'sdk');
786
- if (!sdkAction)
787
- return;
788
- const { laneBoard, writeGuide } = deps;
789
- const verificationId = outcome.verification_id ?? 'unknown';
790
- const firstRemaining = outcome.agent_result?.remaining_actions?.[0];
791
- switch (outcome.status) {
792
- case 'integrated_verified': {
793
- const llmSpans = outcome.verification?.llm_spans ?? 0;
794
- laneBoard?.complete('sdk', `verified — trace reached Moda (${llmSpans} llm span(s), conversation ${verificationId})`);
795
- markApplied(setupPlan, 'sdk');
796
- if (outcome.agent_result?.files_changed?.length) {
797
- sdkAction.files = [...new Set(outcome.agent_result.files_changed)].slice(0, 20);
798
- }
799
- break;
800
- }
801
- case 'telemetry_not_observed':
802
- laneBoard?.warn('sdk', `integration applied but the verification trace was not observed; check MODA_API_KEY at runtime, then moda audit ${verificationId}`);
803
- markManual(setupPlan, 'sdk', outcome.detail ?? 'Verification trace was not observed.');
804
- break;
805
- case 'needs_human_input': {
806
- const securityLead = outcome.security_findings?.length
807
- ? `Security: possible API key leak in ${outcome.security_findings.join(', ')} — remove the key, rotate it, and keep secrets in the environment. `
808
- : '';
809
- const note = firstRemaining ?? outcome.detail ?? 'Additional human input required.';
810
- laneBoard?.warn('sdk', `${securityLead}${note}`);
811
- markManual(setupPlan, 'sdk', `${securityLead}${note}`.trim());
812
- break;
813
- }
814
- case 'nothing_to_integrate': {
815
- if (outcome.verification?.observed === true) {
816
- laneBoard?.complete('sdk', outcome.detail ?? 'existing integration verified — trace reached Moda');
817
- markApplied(setupPlan, 'sdk');
818
- sdkAction.reason = outcome.detail ?? 'Existing Moda integration verified.';
819
- break;
820
- }
821
- // With a smoke attempt the verification id is real and auditable (the
822
- // trace may land late); without one the user must pick their own id.
823
- const reason = outcome.verification
824
- ? 'agent reports Moda is already integrated but the verification trace was not observed; ' +
825
- `check MODA_API_KEY at runtime, then \`moda audit ${verificationId}\``
826
- : 'agent reports Moda is already integrated — not independently verified; ' +
827
- 'send a real request through your app with a conversation id you choose, then run `moda audit` with that id';
828
- laneBoard?.warn('sdk', reason);
829
- markManual(setupPlan, 'sdk', reason);
830
- break;
831
- }
832
- case 'agent_unavailable': {
833
- const guidePath = writeGuide();
834
- sdkAction.files = [guidePath];
835
- laneBoard?.warn('sdk', `no coding agent CLI found; wrote ${guidePath} to integrate by hand`);
836
- markManual(setupPlan, 'sdk', `No coding agent CLI found; wrote ${guidePath}.`);
837
- break;
838
- }
839
- case 'cancelled':
840
- laneBoard?.fail('sdk', outcome.detail ?? 'SDK integration cancelled');
841
- markManual(setupPlan, 'sdk', outcome.detail ?? 'SDK integration cancelled.');
842
- break;
843
- case 'repository_checks_failed':
844
- case 'smoke_execution_failed':
845
- case 'agent_failed':
846
- default: {
847
- const guidePath = writeGuide();
848
- sdkAction.files = [guidePath];
849
- const detail = outcome.detail ?? 'SDK integration did not complete.';
850
- laneBoard?.warn('sdk', `${detail}; wrote ${guidePath} to finish manually`);
851
- markManual(setupPlan, 'sdk', `${detail}; wrote ${guidePath}.`);
852
- break;
853
- }
854
- }
855
- }
856
- function changedFiles(setupPlan) {
857
- return [...new Set(setupPlan
858
- .filter((action) => action.status === 'applied')
859
- .flatMap((action) => action.files))].filter((file) => !file.includes('.env'));
860
- }
861
- // The CLI version generated workflows pin to. Pinning (rather than `@latest`,
862
- // which resolves at CI run time) keeps a future compromised `@moda-ai/cli`
863
- // publish from silently executing in adopters' CI with their MODA_API_KEY /
864
- // MODA_TENANT_ID present. Especially important for autosync, which *writes* to
865
- // the Moda registry. The pin tracks the CLI that generated the file, so each
866
- // `moda init` upgrade is an explicit, reviewable bump of the workflow.
867
- const GENERATED_WORKFLOW_CLI = `@moda-ai/cli@${VERSION}`;
868
- // npx dispatch shape matters: `npx -y @moda-ai/cli@X moda harness ...` resolves
869
- // the package's default bin and passes the literal `moda` token through as the
870
- // CLI's first argument, so every step would exit 1 with "Unknown command
871
- // 'moda'". `--package=<pkg> moda` pins the package AND names the `moda` bin
872
- // explicitly, so the CLI receives the real subcommand first.
873
- const GENERATED_WORKFLOW_NPX = `npx -y --package=${GENERATED_WORKFLOW_CLI} moda`;
874
- // Exported for tests (generated-workflows.test.ts validates npx dispatch shape).
875
- export function writeCiCdWorkflow(cwd = process.cwd()) {
876
- const relativePath = '.github/workflows/moda-validation.yml';
877
- const path = join(cwd, relativePath);
878
- mkdirSync(dirname(path), { recursive: true });
879
- writeFileSync(path, `${[
880
- 'name: Moda Validation',
881
- '',
882
- 'on:',
883
- ' pull_request:',
884
- ' push:',
885
- ' branches: [main]',
886
- '',
887
- '# Least privilege: these jobs only read the checked-out repo and talk to',
888
- '# Moda over the API key; they never write back to the repository.',
889
- 'permissions:',
890
- ' contents: read',
891
- '',
892
- 'jobs:',
893
- ' moda-doctor:',
894
- ' runs-on: ubuntu-latest',
895
- ' env:',
896
- ' MODA_API_KEY: ${{ secrets.MODA_API_KEY }}',
897
- ' MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }}',
898
- ' steps:',
899
- ' - uses: actions/checkout@v4',
900
- ' - uses: actions/setup-node@v4',
901
- ' with:',
902
- ' node-version: 22',
903
- ' - name: Validate Moda setup',
904
- ` run: ${GENERATED_WORKFLOW_NPX} doctor --online --json`,
905
- '',
906
- ].join('\n')}`);
907
- return relativePath;
908
- }
909
- // Generates a push-to-main autosync workflow that keeps the Moda registry in
910
- // sync with the repo: it pushes prompt and skill changes after they merge. It
911
- // only runs on `main` (never on pull_request) so unmerged versions are never
912
- // published. Keep its env in step with writeCiCdWorkflow().
913
- export function writeAutosyncWorkflow(cwd = process.cwd()) {
914
- const relativePath = '.github/workflows/moda-autosync.yml';
915
- const path = join(cwd, relativePath);
916
- mkdirSync(dirname(path), { recursive: true });
917
- writeFileSync(path, `${[
918
- 'name: Moda Autosync',
919
- '',
920
- 'on:',
921
- ' push:',
922
- ' branches: [main]',
923
- '',
924
- '# Least privilege: this job only reads the checked-out repo and writes to',
925
- '# Moda over the API key; it never writes back to the repository.',
926
- 'permissions:',
927
- ' contents: read',
928
- '',
929
- '# A single in-flight sync at a time; newer pushes supersede older runs.',
930
- 'concurrency:',
931
- ' group: moda-autosync',
932
- ' cancel-in-progress: true',
933
- '',
934
- 'jobs:',
935
- ' moda-sync:',
936
- ' runs-on: ubuntu-latest',
937
- ' env:',
938
- ' MODA_API_KEY: ${{ secrets.MODA_API_KEY }}',
939
- ' MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }}',
940
- ' steps:',
941
- ' - uses: actions/checkout@v4',
942
- ' - uses: actions/setup-node@v4',
943
- ' with:',
944
- ' node-version: 22',
945
- ` - name: Sync prompts to Moda`,
946
- ` run: ${GENERATED_WORKFLOW_NPX} prompts sync`,
947
- ` - name: Sync skills to Moda`,
948
- ` run: ${GENERATED_WORKFLOW_NPX} skills sync`,
949
- '',
950
- ].join('\n')}`);
951
- return relativePath;
952
- }
953
- // Generates the "living harness" rescan workflow: after every merge to main
954
- // the hosted analyst re-analyzes the repo, the refreshed report is approved
955
- // non-interactively, and the cited graph is synced to Moda — so the harness
956
- // Moda serves never drifts from the code. It runs only on push to `main`
957
- // (plus manual workflow_dispatch), never on pull_request, so unmerged agent
958
- // changes are never synced. An optional path scope limits pushes that trigger
959
- // a rescan. Keep its env in step with writeCiCdWorkflow().
960
- export function writeHarnessRescanWorkflow(cwd = process.cwd(), paths) {
961
- const relativePath = '.github/workflows/moda-harness-rescan.yml';
962
- const path = join(cwd, relativePath);
963
- mkdirSync(dirname(path), { recursive: true });
964
- const pathScope = paths !== undefined && paths.length > 0
965
- ? [
966
- ' # Only pushes touching these paths trigger a rescan.',
967
- ' paths:',
968
- ...paths.map((glob) => ` - '${glob.replace(/'/g, "''")}'`),
969
- ]
970
- : [
971
- ' # Optional: scope rescans to agent-relevant paths, e.g.',
972
- ' # paths:',
973
- " # - 'src/agents/**'",
974
- ];
975
- writeFileSync(path, `${[
976
- 'name: Moda Harness Rescan',
977
- '',
978
- 'on:',
979
- ' push:',
980
- ' branches: [main]',
981
- ...pathScope,
982
- ' workflow_dispatch:',
983
- '',
984
- '# Least privilege: this job reads the checked-out repo, uploads a safe',
985
- '# source snapshot to Moda over the API key, and syncs the refreshed harness',
986
- '# graph; it never writes back to the repository.',
987
- 'permissions:',
988
- ' contents: read',
989
- '',
990
- '# One in-flight rescan at a time; newer pushes supersede older runs.',
991
- 'concurrency:',
992
- ' group: moda-harness-rescan',
993
- ' cancel-in-progress: true',
994
- '',
995
- 'jobs:',
996
- ' moda-harness-rescan:',
997
- ' runs-on: ubuntu-latest',
998
- ' env:',
999
- ' MODA_API_KEY: ${{ secrets.MODA_API_KEY }}',
1000
- ' MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }}',
1001
- ' steps:',
1002
- ' - uses: actions/checkout@v4',
1003
- ' - uses: actions/setup-node@v4',
1004
- ' with:',
1005
- ' node-version: 22',
1006
- ' - name: Re-analyze the harness on Moda',
1007
- ` run: ${GENERATED_WORKFLOW_NPX} harness analyze --remote --yes`,
1008
- ' - name: Approve the refreshed harness report',
1009
- ` run: ${GENERATED_WORKFLOW_NPX} harness approve --yes`,
1010
- ' - name: Sync the harness graph to Moda',
1011
- ` run: ${GENERATED_WORKFLOW_NPX} harness sync --from-report`,
1012
- '',
1013
- ].join('\n')}`);
1014
- return relativePath;
1015
- }
1016
- function printHarnessDetectionSummary(cwd = process.cwd()) {
1017
- const manifestPath = join(cwd, '.moda', 'harness.json');
1018
- if (!existsSync(manifestPath))
1019
- return;
1020
- try {
1021
- const graph = JSON.parse(readFileSync(manifestPath, 'utf8'));
1022
- const frameworkNames = (graph.agentFrameworks ?? [])
1023
- .map((framework) => typeof framework.name === 'string' ? framework.name : undefined)
1024
- .filter((name) => Boolean(name));
1025
- const providerNames = (graph.modelProviders ?? [])
1026
- .map((provider) => typeof provider.name === 'string' ? provider.name : undefined)
1027
- .filter((name) => Boolean(name));
1028
- console.log(` Detected harness: ${(graph.runtimeAgents ?? []).length} runtime agent(s), ` +
1029
- `${(graph.artifacts ?? []).length} artifact(s), ${(graph.relationships ?? []).length} relationship(s)`);
1030
- if (frameworkNames.length > 0)
1031
- console.log(` Frameworks: ${frameworkNames.join(', ')}`);
1032
- if (providerNames.length > 0)
1033
- console.log(` Model providers: ${providerNames.join(', ')}`);
1034
- }
1035
- catch {
1036
- console.log(' Detected harness summary is available in .moda/HARNESS.md');
1037
- }
1038
- }
1039
- function signalsFromGraph(graph) {
1040
- const names = (items) => (items ?? [])
1041
- .map((item) => (typeof item.name === 'string' ? item.name : undefined))
1042
- .filter((name) => Boolean(name));
1043
- return {
1044
- runtimeAgents: Array.isArray(graph.runtimeAgents) ? graph.runtimeAgents.length : 0,
1045
- frameworks: names(graph.agentFrameworks),
1046
- providers: names(graph.modelProviders),
1047
- };
1048
- }
1049
- /**
1050
- * Read the gate signals from the SAME graph the auto-sync uploads: the cited
1051
- * report (`.moda/harness-report.json`, consumed by `harness sync --from-report`)
1052
- * is authoritative, so read its `graph` first. Only fall back to the standalone
1053
- * manifest (`.moda/harness.json`) when no report exists, otherwise a stale or
1054
- * missing manifest could let an empty report slip past the gate and overwrite
1055
- * the tenant's harness. Returns undefined when neither is readable.
1056
- */
1057
- export function readHarnessQualitySignals(cwd = process.cwd()) {
1058
- const reportPath = join(cwd, '.moda', 'harness-report.json');
1059
- if (existsSync(reportPath)) {
1060
- try {
1061
- const report = JSON.parse(readFileSync(reportPath, 'utf8'));
1062
- if (report.graph && typeof report.graph === 'object') {
1063
- return signalsFromGraph(report.graph);
1064
- }
1065
- }
1066
- catch {
1067
- // Fall through to the manifest below.
1068
- }
1069
- }
1070
- const manifestPath = join(cwd, '.moda', 'harness.json');
1071
- if (!existsSync(manifestPath))
1072
- return undefined;
1073
- try {
1074
- return signalsFromGraph(JSON.parse(readFileSync(manifestPath, 'utf8')));
1075
- }
1076
- catch {
1077
- return undefined;
1078
- }
1079
- }
1080
- /**
1081
- * True when discovery found no runtime agents even though the repo carries
1082
- * LLM framework/provider signals — the signature of a failed discovery run,
1083
- * not an agent-free repo.
1084
- */
1085
- export function harnessLooksIncomplete(signals) {
1086
- return signals.runtimeAgents === 0 &&
1087
- (signals.frameworks.length > 0 || signals.providers.length > 0);
1088
- }
1089
- // --- Repo-side LLM dependency scan (0-agent gate evidence) -----------------
1090
- //
1091
- // The gate must not read its "repo has LLM signals" evidence solely from the
1092
- // artifact it is gating: a TOTAL discovery failure emits a graph with 0
1093
- // agents AND 0 frameworks/providers, which would sail past a graph-only
1094
- // check and overwrite the tenant's harness with an empty graph. This scan is
1095
- // deterministic and independent of the analyst: it looks for well-known LLM
1096
- // packages in dependency manifests (package.json, requirements.txt,
1097
- // pyproject.toml) via a bounded walk.
1098
- const REPO_SIGNAL_MAX_DEPTH = 3;
1099
- const REPO_SIGNAL_MAX_DIRS = 500;
1100
- const REPO_SIGNAL_SKIPPED_DIRS = new Set([
1101
- 'node_modules', 'dist', 'build', 'out', 'coverage', 'vendor', 'target',
1102
- 'venv', 'env', '__pycache__', 'site-packages',
1103
- ]);
1104
- /** Exact npm package name → signal. */
1105
- const JS_LLM_DEPENDENCIES = {
1106
- openai: { name: 'OpenAI', kind: 'provider' },
1107
- '@anthropic-ai/sdk': { name: 'Anthropic', kind: 'provider' },
1108
- '@google/generative-ai': { name: 'Google Gemini', kind: 'provider' },
1109
- '@google/genai': { name: 'Google Gemini', kind: 'provider' },
1110
- 'cohere-ai': { name: 'Cohere', kind: 'provider' },
1111
- 'groq-sdk': { name: 'Groq', kind: 'provider' },
1112
- '@mistralai/mistralai': { name: 'Mistral', kind: 'provider' },
1113
- ollama: { name: 'Ollama', kind: 'provider' },
1114
- 'together-ai': { name: 'Together', kind: 'provider' },
1115
- '@aws-sdk/client-bedrock-runtime': { name: 'AWS Bedrock', kind: 'provider' },
1116
- ai: { name: 'Vercel AI SDK', kind: 'framework' },
1117
- langchain: { name: 'LangChain', kind: 'framework' },
1118
- llamaindex: { name: 'LlamaIndex', kind: 'framework' },
1119
- '@anthropic-ai/claude-agent-sdk': { name: 'Claude Agent SDK', kind: 'framework' },
1120
- '@openai/agents': { name: 'OpenAI Agents', kind: 'framework' },
1121
- '@mastra/core': { name: 'Mastra', kind: 'framework' },
1122
- };
1123
- /** npm scope/prefix → signal (monorepo-style split packages). */
1124
- const JS_LLM_DEPENDENCY_PREFIXES = [
1125
- ['@langchain/', { name: 'LangChain', kind: 'framework' }],
1126
- ['@ai-sdk/', { name: 'Vercel AI SDK', kind: 'framework' }],
1127
- ['@llamaindex/', { name: 'LlamaIndex', kind: 'framework' }],
1128
- ];
1129
- /** Normalized (lowercase, `_`→`-`) Python distribution name → signal. */
1130
- const PY_LLM_DEPENDENCIES = {
1131
- openai: { name: 'OpenAI', kind: 'provider' },
1132
- anthropic: { name: 'Anthropic', kind: 'provider' },
1133
- 'google-generativeai': { name: 'Google Gemini', kind: 'provider' },
1134
- 'google-genai': { name: 'Google Gemini', kind: 'provider' },
1135
- cohere: { name: 'Cohere', kind: 'provider' },
1136
- groq: { name: 'Groq', kind: 'provider' },
1137
- mistralai: { name: 'Mistral', kind: 'provider' },
1138
- ollama: { name: 'Ollama', kind: 'provider' },
1139
- langgraph: { name: 'LangGraph', kind: 'framework' },
1140
- litellm: { name: 'LiteLLM', kind: 'framework' },
1141
- crewai: { name: 'CrewAI', kind: 'framework' },
1142
- autogen: { name: 'AutoGen', kind: 'framework' },
1143
- pyautogen: { name: 'AutoGen', kind: 'framework' },
1144
- ag2: { name: 'AutoGen', kind: 'framework' },
1145
- 'pydantic-ai': { name: 'Pydantic AI', kind: 'framework' },
1146
- 'openai-agents': { name: 'OpenAI Agents', kind: 'framework' },
1147
- 'semantic-kernel': { name: 'Semantic Kernel', kind: 'framework' },
1148
- dspy: { name: 'DSPy', kind: 'framework' },
1149
- 'haystack-ai': { name: 'Haystack', kind: 'framework' },
1150
- };
1151
- /** Python prefix matches (langchain, langchain-openai, llama-index-core, ...). */
1152
- const PY_LLM_DEPENDENCY_PREFIXES = [
1153
- ['langchain', { name: 'LangChain', kind: 'framework' }],
1154
- ['llama-index', { name: 'LlamaIndex', kind: 'framework' }],
1155
- ];
1156
- function jsDependencySignal(dep) {
1157
- const exact = JS_LLM_DEPENDENCIES[dep];
1158
- if (exact)
1159
- return exact;
1160
- for (const [prefix, signal] of JS_LLM_DEPENDENCY_PREFIXES) {
1161
- if (dep.startsWith(prefix))
1162
- return signal;
1163
- }
1164
- return undefined;
1165
- }
1166
- function pyDependencySignal(dep) {
1167
- const normalized = dep.toLowerCase().replace(/_/g, '-');
1168
- const exact = PY_LLM_DEPENDENCIES[normalized];
1169
- if (exact)
1170
- return exact;
1171
- for (const [prefix, signal] of PY_LLM_DEPENDENCY_PREFIXES) {
1172
- if (normalized === prefix || normalized.startsWith(`${prefix}-`))
1173
- return signal;
1174
- }
1175
- return undefined;
1176
- }
1177
- /** Requirement names from requirements.txt lines / pyproject.toml specifiers. */
1178
- function pythonRequirementNames(text, file) {
1179
- const names = [];
1180
- if (file === 'requirements.txt') {
1181
- for (const line of text.split('\n')) {
1182
- const match = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line);
1183
- if (match)
1184
- names.push(match[1]);
1185
- }
1186
- return names;
1187
- }
1188
- // pyproject.toml: dependency specifiers are quoted strings whose leading
1189
- // token is the distribution name ("openai>=1.0", "langchain-openai", ...).
1190
- for (const match of text.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)[^"']*["']/g)) {
1191
- names.push(match[1]);
1192
- }
1193
- return names;
1194
- }
1195
- /**
1196
- * Deterministic repo-side LLM signals: well-known LLM package names found in
1197
- * dependency manifests. Independent of the discovery output, so a fully
1198
- * failed discovery (empty graph) cannot bypass the 0-agent gate.
1199
- */
1200
- export function detectRepoLlmSignals(cwd = process.cwd()) {
1201
- const frameworks = new Set();
1202
- const providers = new Set();
1203
- const record = (signal) => {
1204
- if (!signal)
1205
- return;
1206
- (signal.kind === 'framework' ? frameworks : providers).add(signal.name);
1207
- };
1208
- const scanDir = (dir) => {
1209
- try {
1210
- const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
1211
- for (const dep of [
1212
- ...Object.keys(pkg.dependencies ?? {}),
1213
- ...Object.keys(pkg.devDependencies ?? {}),
1214
- ...Object.keys(pkg.peerDependencies ?? {}),
1215
- ]) {
1216
- record(jsDependencySignal(dep));
1217
- }
1218
- }
1219
- catch {
1220
- // No/invalid package.json here.
1221
- }
1222
- for (const file of ['requirements.txt', 'pyproject.toml']) {
1223
- try {
1224
- const text = readFileSync(join(dir, file), 'utf8');
1225
- for (const name of pythonRequirementNames(text, file)) {
1226
- record(pyDependencySignal(name));
1227
- }
1228
- }
1229
- catch {
1230
- // No such manifest here.
1231
- }
1232
- }
1233
- };
1234
- let visited = 0;
1235
- const walk = (dir, depth) => {
1236
- if (depth > REPO_SIGNAL_MAX_DEPTH || visited >= REPO_SIGNAL_MAX_DIRS)
1237
- return;
1238
- visited += 1;
1239
- scanDir(dir);
1240
- let entries;
1241
- try {
1242
- entries = readdirSync(dir, { withFileTypes: true });
1243
- }
1244
- catch {
1245
- return;
1246
- }
1247
- for (const entry of entries) {
1248
- if (!entry.isDirectory() || entry.name.startsWith('.'))
1249
- continue;
1250
- if (REPO_SIGNAL_SKIPPED_DIRS.has(entry.name))
1251
- continue;
1252
- walk(join(dir, entry.name), depth + 1);
1253
- }
1254
- };
1255
- walk(cwd, 0);
1256
- return { frameworks: [...frameworks].sort(), providers: [...providers].sort() };
1257
- }
1258
- /** Agent/artifact counts for the completion screen's harness payoff line. */
1259
- function readHarnessGraphCounts(cwd = process.cwd()) {
1260
- const manifestPath = join(cwd, '.moda', 'harness.json');
1261
- if (!existsSync(manifestPath))
1262
- return {};
1263
- try {
1264
- const graph = JSON.parse(readFileSync(manifestPath, 'utf8'));
1265
- return {
1266
- agents: Array.isArray(graph.runtimeAgents) ? graph.runtimeAgents.length : 0,
1267
- artifacts: Array.isArray(graph.artifacts) ? graph.artifacts.length : 0,
1268
- };
1269
- }
1270
- catch {
1271
- return {};
1272
- }
1273
- }
1274
- function commandFlags(flags) {
1275
- return Object.fromEntries(Object.entries(flags)
1276
- .filter(([, value]) => value !== undefined && value !== false)
1277
- .map(([key, value]) => [key, value === true ? 'true' : String(value)]));
1278
- }
1279
- function stringFlag(value) {
1280
- return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
1281
- }
1282
- export function resumeFlagsForInit(flags) {
1283
- const passthrough = [
1284
- 'profile',
1285
- 'tenant-id',
1286
- 'offline',
1287
- 'analyst',
1288
- 'sdk-integrate',
1289
- 'ci-cd',
1290
- 'autosync',
1291
- 'harness-rescan',
1292
- 'harness-rescan-paths',
1293
- 'no-sdk-integrate',
1294
- 'no-ci-cd',
1295
- 'no-autosync',
1296
- 'no-harness-rescan',
1297
- 'yes',
1298
- 'agent',
1299
- 'json',
1300
- 'stream',
1301
- ];
1302
- const out = [];
1303
- for (const key of passthrough) {
1304
- const value = flagValue(flags, key, key.replace(/-/g, '_'));
1305
- if (value === undefined || value === false || value === 'false')
1306
- continue;
1307
- const flag = `--${key}`;
1308
- if (value === true || value === 'true')
1309
- out.push(flag);
1310
- else
1311
- out.push(flag, String(value));
1312
- }
1313
- // The rescan choice is derived as `flag ?? (paths ? true : ...)`, so a bare
1314
- // `--harness-rescan-paths` re-implies opt-in. If the original run explicitly
1315
- // disabled rescan (via `--harness-rescan=false`, whose false value the loop
1316
- // above drops), carry the negative through so a resume can't silently write
1317
- // the workflow the user declined.
1318
- if (resolveHarnessRescanFlag(flags) === false && !out.includes('--no-harness-rescan')) {
1319
- out.push('--no-harness-rescan');
1320
- }
1321
- return out;
1322
- }
1323
- /**
1324
- * Explicit CI/CD decision from flags, or undefined when the Plan beat decides
1325
- * (interactive default off; --yes/--json/--agent default off).
1326
- */
1327
- function resolveCiCdFlag(flags) {
1328
- const explicit = parseOptionalBoolean(flagValue(flags, 'ci-cd', 'ci_cd', 'ci'));
1329
- if (explicit !== undefined)
1330
- return explicit;
1331
- if (isTruthyFlag(flagValue(flags, 'no-ci-cd', 'no_ci_cd')))
1332
- return false;
1333
- return undefined;
1334
- }
1335
- /** Explicit autosync decision from flags, or undefined when the Plan beat decides. */
1336
- function resolveAutosyncFlag(flags) {
1337
- const explicit = parseOptionalBoolean(flagValue(flags, 'autosync', 'auto-sync', 'auto_sync'));
1338
- if (explicit !== undefined)
1339
- return explicit;
1340
- if (isTruthyFlag(flagValue(flags, 'no-autosync', 'no_autosync', 'no-auto-sync', 'no_auto_sync')))
1341
- return false;
1342
- return undefined;
1343
- }
1344
- /** Explicit harness-rescan decision from flags, or undefined when the Plan beat decides. */
1345
- function resolveHarnessRescanFlag(flags) {
1346
- const explicit = parseOptionalBoolean(flagValue(flags, 'harness-rescan', 'harness_rescan'));
1347
- if (explicit !== undefined)
1348
- return explicit;
1349
- if (isTruthyFlag(flagValue(flags, 'no-harness-rescan', 'no_harness_rescan')))
1350
- return false;
1351
- return undefined;
1352
- }
1353
- /**
1354
- * Comma-separated path globs for the rescan workflow's `on.push.paths` scope.
1355
- * `undefined` when the flag is absent (or carries no usable globs), so a bare
1356
- * `--harness-rescan` still writes an unscoped rescan-on-every-push workflow.
1357
- */
1358
- function parseHarnessRescanPaths(value) {
1359
- if (typeof value !== 'string')
1360
- return undefined;
1361
- const globs = value
1362
- .split(',')
1363
- .map((glob) => glob.trim())
1364
- .filter((glob) => glob.length > 0 && glob !== 'true');
1365
- return globs.length > 0 ? globs : undefined;
1366
- }
1367
- /**
1368
- * Explicit SDK-integration decision from flags, or undefined when the Plan
1369
- * beat decides (interactive default: on when a coding-agent CLI is installed;
1370
- * --yes/--json/--agent default off because the agent edits source and installs
1371
- * packages, which must never happen unattended without an explicit opt-in).
1372
- */
1373
- function resolveSdkIntegrateFlag(flags) {
1374
- const explicit = parseOptionalBoolean(flagValue(flags, 'sdk-integrate', 'sdk_integrate', 'sdk-guide', 'sdk_guide', 'sdk'));
1375
- if (explicit !== undefined)
1376
- return explicit;
1377
- if (isTruthyFlag(flagValue(flags, 'no-sdk-integrate', 'no_sdk_integrate', 'no-sdk-guide', 'no_sdk_guide', 'no-sdk', 'no_sdk'))) {
1378
- return false;
1379
- }
1380
- return undefined;
1381
- }
1382
- function parseOptionalBoolean(value) {
1383
- if (value === undefined)
1384
- return undefined;
1385
- // The CLI parser normalizes numeric-looking flag values to numbers, so
1386
- // `--remote=1` arrives as number 1 and `--remote=0` as number 0 (not the
1387
- // string spellings); accept both alongside the string/boolean forms.
1388
- if (value === true || value === 1 || value === 'true' || value === '1' || value === 'yes')
1389
- return true;
1390
- if (value === false || value === 0 || value === 'false' || value === '0' || value === 'no')
1391
- return false;
1392
- return undefined;
1393
- }
1394
- function isTruthyFlag(value) {
1395
- // The CLI normalizes numeric-looking flag values to numbers (`--remote=1` →
1396
- // number 1), so accept the numeric `1` alongside the string spellings.
1397
- return value === true || value === 1 || value === 'true' || value === '1' || value === 'yes';
1398
- }
1399
- /**
1400
- * Onboarding runs harness discovery SERVER-SIDE by default so the customer
1401
- * needs no local coding agent. Opt out with `--local`, `--no-remote`,
1402
- * `--remote=false`, or an explicit deterministic local analyst
1403
- * (`--analyst=local-scan|local|scripted|moda`).
1404
- */
1405
- export function resolveHarnessRemote(flags) {
1406
- if (isTruthyFlag(flagValue(flags, 'local')))
1407
- return false;
1408
- if (isTruthyFlag(flagValue(flags, 'no-remote', 'no_remote')))
1409
- return false;
1410
- const remote = parseOptionalBoolean(flagValue(flags, 'remote'));
1411
- if (remote !== undefined)
1412
- return remote;
1413
- // Scenarios and users that pin `--analyst=local-scan` mean "run the local
1414
- // deterministic scanner", not "upload to hosted analyze and poll". Treat that
1415
- // as an implicit local opt-out so moda-test live/onboarding suites and
1416
- // developer workflows do not hang waiting on remote analyze.
1417
- if (isDeterministicLocalAnalyst(stringFlag(flagValue(flags, 'analyst')))) {
1418
- return false;
1419
- }
1420
- return true;
1421
- }
1422
- /**
1423
- * True when the user pinned the remote/local decision (flags or a
1424
- * deterministic analyst), so the Plan checkbox must not offer to re-toggle it.
1425
- */
1426
- function hasExplicitRemoteChoice(flags) {
1427
- if (isTruthyFlag(flagValue(flags, 'local')))
1428
- return true;
1429
- if (isTruthyFlag(flagValue(flags, 'no-remote', 'no_remote')))
1430
- return true;
1431
- if (parseOptionalBoolean(flagValue(flags, 'remote')) !== undefined)
1432
- return true;
1433
- return isDeterministicLocalAnalyst(stringFlag(flagValue(flags, 'analyst')));
1434
- }
1435
- function flagValue(flags, ...keys) {
1436
- for (const key of keys) {
1437
- if (flags[key] !== undefined)
1438
- return flags[key];
1439
- }
1440
- return undefined;
1441
- }
1442
- //# sourceMappingURL=index.js.map