@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.
- package/dist/cli-c4se7a5d.js +762 -0
- package/dist/cli-mnqa4rp6.js +1563 -0
- package/dist/cli-stmjdgvy.js +2842 -0
- package/dist/cli-y55bgg14.js +6980 -0
- package/dist/cli-ysegdrm5.js +286 -0
- package/dist/cli.js +4779 -1159
- package/dist/harness-github-actions-7t3cncnt.js +95 -0
- package/dist/index-4ser01zr.js +2769 -0
- package/dist/provision-hm1ppwj6.js +74 -0
- package/package.json +3 -2
- package/skills/integration/index.json +2 -2
- package/dist/agent-schemas.d.ts +0 -3
- package/dist/agent-schemas.js +0 -358
- package/dist/agent.d.ts +0 -5
- package/dist/agent.js +0 -477
- package/dist/analyst-tui.d.ts +0 -81
- package/dist/analyst-tui.js +0 -314
- package/dist/api-client.d.ts +0 -46
- package/dist/api-client.js +0 -120
- package/dist/ask-stream.d.ts +0 -146
- package/dist/ask-stream.js +0 -473
- package/dist/auth-storage.d.ts +0 -35
- package/dist/auth-storage.js +0 -159
- package/dist/auth.d.ts +0 -50
- package/dist/auth.js +0 -694
- package/dist/cli-session.d.ts +0 -26
- package/dist/cli-session.js +0 -169
- package/dist/cli.d.ts +0 -44
- package/dist/command-context.d.ts +0 -62
- package/dist/command-context.js +0 -57
- package/dist/command-registry.d.ts +0 -22
- package/dist/command-registry.js +0 -15
- package/dist/config.d.ts +0 -60
- package/dist/config.js +0 -110
- package/dist/doctor.d.ts +0 -79
- package/dist/doctor.js +0 -1182
- package/dist/errors.d.ts +0 -50
- package/dist/errors.js +0 -64
- package/dist/exit-funnel.d.ts +0 -14
- package/dist/exit-funnel.js +0 -53
- package/dist/harness-artifact-bodies.d.ts +0 -28
- package/dist/harness-artifact-bodies.js +0 -276
- package/dist/harness-candidates.d.ts +0 -77
- package/dist/harness-candidates.js +0 -482
- package/dist/harness-remote.d.ts +0 -135
- package/dist/harness-remote.js +0 -540
- package/dist/harness-report.d.ts +0 -145
- package/dist/harness-report.js +0 -1579
- package/dist/harness-sync-limits.d.ts +0 -8
- package/dist/harness-sync-limits.js +0 -21
- package/dist/harness.d.ts +0 -463
- package/dist/harness.js +0 -4009
- package/dist/init/index.d.ts +0 -78
- package/dist/init/index.js +0 -1442
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -27
- package/dist/init/progress-board.d.ts +0 -80
- package/dist/init/progress-board.js +0 -225
- package/dist/init/rules.d.ts +0 -6
- package/dist/init/rules.js +0 -171
- package/dist/init/sdk-catalog.d.ts +0 -10
- package/dist/init/sdk-catalog.js +0 -80
- package/dist/init/sdk-guide.d.ts +0 -19
- package/dist/init/sdk-guide.js +0 -185
- package/dist/init/sdk-integration.d.ts +0 -53
- package/dist/init/sdk-integration.js +0 -835
- package/dist/init/sdk-protocol.d.ts +0 -135
- package/dist/init/sdk-protocol.js +0 -152
- package/dist/init/sdk-verification.d.ts +0 -27
- package/dist/init/sdk-verification.js +0 -123
- package/dist/init/tenant.d.ts +0 -67
- package/dist/init/tenant.js +0 -421
- package/dist/intelligence-schema.d.ts +0 -349
- package/dist/intelligence-schema.js +0 -174
- package/dist/keys.d.ts +0 -47
- package/dist/keys.js +0 -136
- package/dist/manifest.d.ts +0 -5
- package/dist/manifest.js +0 -114
- package/dist/markdown-terminal.d.ts +0 -49
- package/dist/markdown-terminal.js +0 -212
- package/dist/output.d.ts +0 -191
- package/dist/output.js +0 -624
- package/dist/production-intelligence.d.ts +0 -175
- package/dist/production-intelligence.js +0 -1430
- package/dist/profile-commands.d.ts +0 -10
- package/dist/profile-commands.js +0 -342
- package/dist/profiles.d.ts +0 -112
- package/dist/profiles.js +0 -573
- package/dist/prompts-ab.d.ts +0 -71
- package/dist/prompts-ab.js +0 -313
- package/dist/prompts-propose.d.ts +0 -15
- package/dist/prompts-propose.js +0 -212
- package/dist/prompts.d.ts +0 -58
- package/dist/prompts.js +0 -418
- package/dist/provision.d.ts +0 -28
- package/dist/provision.js +0 -104
- package/dist/redact.d.ts +0 -2
- package/dist/redact.js +0 -90
- package/dist/schemas.d.ts +0 -225
- package/dist/schemas.js +0 -126
- package/dist/skills-bundled.d.ts +0 -35
- package/dist/skills-bundled.js +0 -157
- package/dist/skills-manifest.d.ts +0 -33
- package/dist/skills-manifest.js +0 -244
- package/dist/skills.d.ts +0 -57
- package/dist/skills.js +0 -783
- package/dist/sync.d.ts +0 -5
- package/dist/sync.js +0 -36
- package/dist/telemetry.d.ts +0 -56
- package/dist/telemetry.js +0 -167
- package/dist/transforms.d.ts +0 -25
- package/dist/transforms.js +0 -102
- package/dist/tui.d.ts +0 -48
- package/dist/tui.js +0 -393
- package/dist/update-check.d.ts +0 -31
- package/dist/update-check.js +0 -141
- package/dist/workspace.d.ts +0 -12
- package/dist/workspace.js +0 -1294
package/dist/init/index.js
DELETED
|
@@ -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
|