@moda-ai/cli 1.22.0 → 1.22.2
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-g8qs2tj9.js +9777 -0
- package/dist/cli-pwbvtes8.js +2312 -0
- package/dist/cli-qszcpda3.js +284 -0
- package/dist/cli.js +4775 -1159
- package/dist/index-6h0nc1r5.js +2765 -0
- package/dist/provision-j5gq8cvx.js +72 -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 -777
- 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
|
@@ -0,0 +1,2765 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SKILL_SOURCE_INTEGRATION_BUNDLED,
|
|
3
|
+
buildDoctorReport,
|
|
4
|
+
claudeSkillToCursorRule,
|
|
5
|
+
codingAgentDisplayName,
|
|
6
|
+
describeCodingAgentEvent,
|
|
7
|
+
describeSyncIdentity,
|
|
8
|
+
describeSyncTenantLabel,
|
|
9
|
+
detectAgents,
|
|
10
|
+
detectAvailableCodingAgents,
|
|
11
|
+
initPrompts,
|
|
12
|
+
installRules,
|
|
13
|
+
isDeterministicLocalAnalyst,
|
|
14
|
+
pollRemoteAnalyzeUntilDone,
|
|
15
|
+
readSkillsManifest,
|
|
16
|
+
renderInitBanner,
|
|
17
|
+
renderInitComplete,
|
|
18
|
+
renderInitNote,
|
|
19
|
+
renderInitSetupPlan,
|
|
20
|
+
renderInitStep,
|
|
21
|
+
runCodingAgent,
|
|
22
|
+
runHarnessCommand,
|
|
23
|
+
runPromptSync,
|
|
24
|
+
runSkillSync,
|
|
25
|
+
startRemoteAnalyze,
|
|
26
|
+
stripAnsi,
|
|
27
|
+
upsertSkillManifestRecord
|
|
28
|
+
} from "./cli-g8qs2tj9.js";
|
|
29
|
+
import {
|
|
30
|
+
selectTenantAndCreateKey
|
|
31
|
+
} from "./cli-qszcpda3.js";
|
|
32
|
+
import {
|
|
33
|
+
CliAuthError,
|
|
34
|
+
CliInputError,
|
|
35
|
+
VERSION,
|
|
36
|
+
createCommandContext,
|
|
37
|
+
loadToken,
|
|
38
|
+
login,
|
|
39
|
+
resolveIngestUrl,
|
|
40
|
+
resolveModaBaseUrl
|
|
41
|
+
} from "./cli-pwbvtes8.js";
|
|
42
|
+
|
|
43
|
+
// src/init/index.ts
|
|
44
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
45
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
46
|
+
import { checkbox, confirm, select } from "@inquirer/prompts";
|
|
47
|
+
|
|
48
|
+
// src/init/sdk-guide.ts
|
|
49
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
50
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
51
|
+
|
|
52
|
+
// src/skills-bundled.ts
|
|
53
|
+
import { createHash } from "node:crypto";
|
|
54
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
55
|
+
import { dirname, join, relative } from "node:path";
|
|
56
|
+
import { fileURLToPath } from "node:url";
|
|
57
|
+
var PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
58
|
+
var BUNDLED_SKILLS_DIR = join(PACKAGE_ROOT, "skills", "integration");
|
|
59
|
+
function bundledSkillsDir() {
|
|
60
|
+
return process.env.MODA_BUNDLED_SKILLS_DIR || BUNDLED_SKILLS_DIR;
|
|
61
|
+
}
|
|
62
|
+
function readBundledIndex() {
|
|
63
|
+
const indexPath = join(bundledSkillsDir(), "index.json");
|
|
64
|
+
if (!existsSync(indexPath))
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(readFileSync(indexPath, "utf8"));
|
|
68
|
+
if (!Array.isArray(parsed.skills))
|
|
69
|
+
return null;
|
|
70
|
+
return parsed;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function installIntegrationSkill(skillId, cwd = process.cwd()) {
|
|
76
|
+
if (!/^[a-z0-9-]+$/.test(skillId)) {
|
|
77
|
+
throw new Error(`Invalid integration skill id: ${JSON.stringify(skillId).slice(0, 80)}`);
|
|
78
|
+
}
|
|
79
|
+
const sourceDir = join(bundledSkillsDir(), skillId);
|
|
80
|
+
if (!existsSync(join(sourceDir, "SKILL.md"))) {
|
|
81
|
+
const bundled = readBundledIndex()?.skills.map((skill) => skill.id) ?? [];
|
|
82
|
+
throw new Error(`Integration skill '${skillId}' is not bundled with this CLI.${bundled.length > 0 ? ` Bundled skills: ${bundled.join(", ")}` : ""}`);
|
|
83
|
+
}
|
|
84
|
+
const files = listFilesRecursive(sourceDir).sort();
|
|
85
|
+
const plannedWrites = [];
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
const relPath = relative(sourceDir, file).replace(/\\/g, "/");
|
|
88
|
+
plannedWrites.push({
|
|
89
|
+
targetRelPath: `.claude/skills/${skillId}/${relPath}`,
|
|
90
|
+
targetPath: join(cwd, ".claude", "skills", skillId, relPath),
|
|
91
|
+
content: readFileSync(file, "utf8")
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const writeCursor = detectAgents().includes("Cursor") || existsSync(join(cwd, ".cursor"));
|
|
95
|
+
if (writeCursor) {
|
|
96
|
+
const skillMd = readFileSync(join(sourceDir, "SKILL.md"), "utf8");
|
|
97
|
+
plannedWrites.push({
|
|
98
|
+
targetRelPath: `.cursor/rules/${skillId}.mdc`,
|
|
99
|
+
targetPath: join(cwd, ".cursor", "rules", `${skillId}.mdc`),
|
|
100
|
+
content: claudeSkillToCursorRule(skillMd)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const ownedPaths = new Set((readSkillsManifest(cwd)?.records ?? []).flatMap((record) => record.paths));
|
|
104
|
+
const collisions = plannedWrites.filter((write) => existsSync(write.targetPath) && !ownedPaths.has(write.targetRelPath));
|
|
105
|
+
if (collisions.length > 0) {
|
|
106
|
+
throw new Error(`Refusing to overwrite ${collisions.length} existing file(s) not managed by Moda: ` + `${collisions.map((write) => write.targetRelPath).join(", ")}. ` + "Move or remove them, then re-run to install the bundled integration skill.");
|
|
107
|
+
}
|
|
108
|
+
const paths = [];
|
|
109
|
+
let wroteAny = false;
|
|
110
|
+
for (const write of plannedWrites) {
|
|
111
|
+
if (writeIfChanged(write.targetPath, write.content))
|
|
112
|
+
wroteAny = true;
|
|
113
|
+
paths.push(write.targetRelPath);
|
|
114
|
+
}
|
|
115
|
+
upsertSkillManifestRecord(cwd, {
|
|
116
|
+
skillId,
|
|
117
|
+
contentHash: hashSkillDir(sourceDir, files),
|
|
118
|
+
source: SKILL_SOURCE_INTEGRATION_BUNDLED,
|
|
119
|
+
paths
|
|
120
|
+
});
|
|
121
|
+
return { skillId, status: wroteAny ? "installed" : "unchanged", paths };
|
|
122
|
+
}
|
|
123
|
+
function writeIfChanged(path, content) {
|
|
124
|
+
if (existsSync(path)) {
|
|
125
|
+
try {
|
|
126
|
+
if (readFileSync(path, "utf8") === content)
|
|
127
|
+
return false;
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
131
|
+
writeFileSync(path, content);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
function listFilesRecursive(dir) {
|
|
135
|
+
const files = [];
|
|
136
|
+
const stack = [dir];
|
|
137
|
+
while (stack.length > 0) {
|
|
138
|
+
const current = stack.pop();
|
|
139
|
+
if (!current)
|
|
140
|
+
continue;
|
|
141
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
142
|
+
const path = join(current, entry.name);
|
|
143
|
+
if (entry.isDirectory())
|
|
144
|
+
stack.push(path);
|
|
145
|
+
else if (entry.isFile())
|
|
146
|
+
files.push(path);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
function hashSkillDir(sourceDir, files) {
|
|
152
|
+
const hash = createHash("sha256");
|
|
153
|
+
for (const file of files) {
|
|
154
|
+
hash.update(relative(sourceDir, file).replace(/\\/g, "/"));
|
|
155
|
+
hash.update("\x00");
|
|
156
|
+
hash.update(readFileSync(file));
|
|
157
|
+
hash.update("\x00");
|
|
158
|
+
}
|
|
159
|
+
return hash.digest("hex");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/init/sdk-catalog.ts
|
|
163
|
+
import {
|
|
164
|
+
copyFileSync,
|
|
165
|
+
existsSync as existsSync2,
|
|
166
|
+
mkdirSync as mkdirSync2,
|
|
167
|
+
readdirSync as readdirSync2,
|
|
168
|
+
rmSync,
|
|
169
|
+
statSync
|
|
170
|
+
} from "node:fs";
|
|
171
|
+
import { join as join2, relative as relative2, resolve } from "node:path";
|
|
172
|
+
function materializeSdkCatalog(cwd, runId) {
|
|
173
|
+
if (!/^[a-z0-9-]+$/.test(runId)) {
|
|
174
|
+
throw new Error(`Invalid SDK catalog run id: ${JSON.stringify(runId).slice(0, 80)}`);
|
|
175
|
+
}
|
|
176
|
+
const sourceDir = bundledSkillsDir();
|
|
177
|
+
const indexPath = join2(sourceDir, "index.json");
|
|
178
|
+
if (!existsSync2(sourceDir) || !existsSync2(indexPath)) {
|
|
179
|
+
throw missingBundleError(sourceDir);
|
|
180
|
+
}
|
|
181
|
+
const index = readBundledIndex();
|
|
182
|
+
if (!index) {
|
|
183
|
+
throw missingBundleError(sourceDir);
|
|
184
|
+
}
|
|
185
|
+
const dir = join2(cwd, ".moda", "tmp", `sdk-catalog-${runId}`);
|
|
186
|
+
cleanupSdkCatalog(dir);
|
|
187
|
+
copyDirectoryRecursive(sourceDir, dir);
|
|
188
|
+
return {
|
|
189
|
+
dir,
|
|
190
|
+
relativeDir: relative2(cwd, dir).replace(/\\/g, "/"),
|
|
191
|
+
index
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function cleanupSdkCatalog(dir) {
|
|
195
|
+
const normalized = resolve(dir).replace(/\\/g, "/");
|
|
196
|
+
if (!normalized.includes(".moda/tmp/sdk-catalog-"))
|
|
197
|
+
return;
|
|
198
|
+
try {
|
|
199
|
+
rmSync(dir, { recursive: true, force: true });
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
function renderCatalogListing(index) {
|
|
203
|
+
return index.skills.map((skill) => {
|
|
204
|
+
const targets = [`language: ${skill.targets.language}`];
|
|
205
|
+
if (skill.targets.frameworks.length > 0) {
|
|
206
|
+
targets.push(`frameworks: ${skill.targets.frameworks.join(",")}`);
|
|
207
|
+
}
|
|
208
|
+
if (skill.targets.providers.length > 0) {
|
|
209
|
+
targets.push(`providers: ${skill.targets.providers.join(",")}`);
|
|
210
|
+
}
|
|
211
|
+
return `- ${skill.id} — ${skill.summary} [${targets.join("; ")}]`;
|
|
212
|
+
}).join(`
|
|
213
|
+
`);
|
|
214
|
+
}
|
|
215
|
+
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
216
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
217
|
+
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
218
|
+
const source = join2(sourceDir, entry.name);
|
|
219
|
+
const target = join2(targetDir, entry.name);
|
|
220
|
+
if (entry.isDirectory()) {
|
|
221
|
+
copyDirectoryRecursive(source, target);
|
|
222
|
+
} else if (entry.isFile()) {
|
|
223
|
+
copyFileSync(source, target);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function missingBundleError(sourceDir) {
|
|
228
|
+
let detail = "is absent";
|
|
229
|
+
try {
|
|
230
|
+
if (existsSync2(sourceDir) && !statSync(sourceDir).isDirectory()) {
|
|
231
|
+
detail = "is not a directory";
|
|
232
|
+
} else if (existsSync2(sourceDir)) {
|
|
233
|
+
detail = "has no readable index.json";
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
detail = "is not readable";
|
|
237
|
+
}
|
|
238
|
+
return new Error(`Bundled SDK skill catalog ${detail} at ${sourceDir}. ` + "Set MODA_BUNDLED_SKILLS_DIR to a built bundle, or run the integration-skill bundle build in a dev tree.");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/init/sdk-guide.ts
|
|
242
|
+
var SDK_GUIDE_PATH = ".moda/SDK_INTEGRATION.md";
|
|
243
|
+
function writeSdkIntegrationGuide(cwd = process.cwd()) {
|
|
244
|
+
const relativePath = SDK_GUIDE_PATH;
|
|
245
|
+
const absolutePath = join3(cwd, relativePath);
|
|
246
|
+
const content = renderSdkIntegrationGuideContent();
|
|
247
|
+
mkdirSync3(dirname2(absolutePath), { recursive: true });
|
|
248
|
+
writeFileSync2(absolutePath, content);
|
|
249
|
+
return relativePath;
|
|
250
|
+
}
|
|
251
|
+
function renderSdkIntegrationGuideContent() {
|
|
252
|
+
const index = readBundledIndex();
|
|
253
|
+
const catalogSection = index ? [
|
|
254
|
+
"## Available integration skills",
|
|
255
|
+
"",
|
|
256
|
+
renderCatalogListing(index),
|
|
257
|
+
"",
|
|
258
|
+
"During `moda init`, Moda materializes these skills into a temporary catalog for your",
|
|
259
|
+
"coding agent. To integrate by hand, read the matching skill inside the installed CLI",
|
|
260
|
+
"package (`.../@moda-ai/cli/skills/integration/<id>/SKILL.md`), re-run `moda init` with",
|
|
261
|
+
"a coding agent CLI installed, or follow https://docs.moda.dev/ingestion.",
|
|
262
|
+
""
|
|
263
|
+
] : [
|
|
264
|
+
"## Available integration skills",
|
|
265
|
+
"",
|
|
266
|
+
"_The bundled skill index is unavailable in this install; use the generic steps below",
|
|
267
|
+
"and https://docs.moda.dev/ingestion for stack-specific guidance._",
|
|
268
|
+
""
|
|
269
|
+
];
|
|
270
|
+
return [
|
|
271
|
+
"# Moda SDK Integration Guide",
|
|
272
|
+
"",
|
|
273
|
+
"Generated by `moda init`. **The SDK integration was not applied automatically.**",
|
|
274
|
+
"This checklist gets you there by hand or with your coding agent.",
|
|
275
|
+
"",
|
|
276
|
+
...catalogSection,
|
|
277
|
+
"## 1. Install the SDK",
|
|
278
|
+
"",
|
|
279
|
+
"Install the Moda SDK for your stack with **your** package manager. If a catalog skill",
|
|
280
|
+
"above matches your repo, use its exact package name and install command.",
|
|
281
|
+
"",
|
|
282
|
+
"Examples (adapt to your stack):",
|
|
283
|
+
"",
|
|
284
|
+
"Node.js / TypeScript:",
|
|
285
|
+
"",
|
|
286
|
+
"```bash",
|
|
287
|
+
"npm install moda-ai # or: bun add / pnpm add / yarn add",
|
|
288
|
+
"```",
|
|
289
|
+
"",
|
|
290
|
+
"Python:",
|
|
291
|
+
"",
|
|
292
|
+
"```bash",
|
|
293
|
+
"pip install moda-ai # or: uv add / poetry add",
|
|
294
|
+
"```",
|
|
295
|
+
"",
|
|
296
|
+
"Do not paste `MODA_API_KEY` into source or `.env` files. Keep it in your deployment",
|
|
297
|
+
"environment, secret manager, or local shell.",
|
|
298
|
+
"",
|
|
299
|
+
"## 2. Initialize Moda before LLM clients",
|
|
300
|
+
"",
|
|
301
|
+
"Initialize before any LLM client is created.",
|
|
302
|
+
"",
|
|
303
|
+
"Node.js / TypeScript (example):",
|
|
304
|
+
"",
|
|
305
|
+
"```ts",
|
|
306
|
+
"import { Moda } from 'moda-ai';",
|
|
307
|
+
"",
|
|
308
|
+
"await Moda.init(process.env.MODA_API_KEY!);",
|
|
309
|
+
"```",
|
|
310
|
+
"",
|
|
311
|
+
"Python (example):",
|
|
312
|
+
"",
|
|
313
|
+
"```python",
|
|
314
|
+
"import os",
|
|
315
|
+
"",
|
|
316
|
+
"import moda",
|
|
317
|
+
"",
|
|
318
|
+
'moda.init(os.environ["MODA_API_KEY"])',
|
|
319
|
+
"```",
|
|
320
|
+
"",
|
|
321
|
+
"## 3. Set conversation and user context",
|
|
322
|
+
"",
|
|
323
|
+
"Set a stable conversation id before each LLM call, and a user id when one user can be",
|
|
324
|
+
"identified. For concurrent request handlers, use scoped context helpers from the SDK",
|
|
325
|
+
"instead of setting global context across overlapping requests.",
|
|
326
|
+
"",
|
|
327
|
+
"Node.js / TypeScript (example):",
|
|
328
|
+
"",
|
|
329
|
+
"```ts",
|
|
330
|
+
"Moda.conversationId = sessionId;",
|
|
331
|
+
"Moda.userId = userId;",
|
|
332
|
+
"```",
|
|
333
|
+
"",
|
|
334
|
+
"Python (example):",
|
|
335
|
+
"",
|
|
336
|
+
"```python",
|
|
337
|
+
"moda.conversation_id = session_id",
|
|
338
|
+
"moda.user_id = user_id",
|
|
339
|
+
"```",
|
|
340
|
+
"",
|
|
341
|
+
"## 4. Flush before exit",
|
|
342
|
+
"",
|
|
343
|
+
"Node.js / TypeScript: `await Moda.flush();` — Python: `moda.flush()`.",
|
|
344
|
+
"",
|
|
345
|
+
"## 5. Validate",
|
|
346
|
+
"",
|
|
347
|
+
"Tenant health (does not prove this integration by itself):",
|
|
348
|
+
"",
|
|
349
|
+
"```bash",
|
|
350
|
+
"moda doctor --online --json",
|
|
351
|
+
"```",
|
|
352
|
+
"",
|
|
353
|
+
"Exact-trace confirmation: send one request through your app with a known conversation id,",
|
|
354
|
+
"then run `moda audit <that-id>` — you should see your llm span(s).",
|
|
355
|
+
""
|
|
356
|
+
].join(`
|
|
357
|
+
`);
|
|
358
|
+
}
|
|
359
|
+
function readRuntimeSummary(cwd) {
|
|
360
|
+
const empty = {
|
|
361
|
+
runtimeAgents: 0,
|
|
362
|
+
agentFamilies: 0,
|
|
363
|
+
frameworks: [],
|
|
364
|
+
providers: [],
|
|
365
|
+
runtimeAgentNames: []
|
|
366
|
+
};
|
|
367
|
+
const path = join3(cwd, ".moda", "harness.json");
|
|
368
|
+
if (!existsSync3(path))
|
|
369
|
+
return empty;
|
|
370
|
+
try {
|
|
371
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
372
|
+
if (!isRecord(parsed))
|
|
373
|
+
return empty;
|
|
374
|
+
const runtimeAgents = Array.isArray(parsed.runtimeAgents) ? parsed.runtimeAgents : [];
|
|
375
|
+
const agentFamilies = Array.isArray(parsed.agentFamilies) ? parsed.agentFamilies : [];
|
|
376
|
+
const frameworks = Array.isArray(parsed.agentFrameworks) ? parsed.agentFrameworks : [];
|
|
377
|
+
const providers = Array.isArray(parsed.modelProviders) ? parsed.modelProviders : [];
|
|
378
|
+
return {
|
|
379
|
+
runtimeAgents: runtimeAgents.length,
|
|
380
|
+
agentFamilies: agentFamilies.length,
|
|
381
|
+
runtimeAgentNames: runtimeAgents.map((agent) => isRecord(agent) && typeof agent.name === "string" ? agent.name : undefined).filter((name) => Boolean(name)),
|
|
382
|
+
frameworks: frameworks.map((framework) => isRecord(framework) && typeof framework.name === "string" ? framework.name : undefined).filter((name) => Boolean(name)),
|
|
383
|
+
providers: providers.map((provider) => isRecord(provider) && typeof provider.name === "string" ? provider.name : undefined).filter((name) => Boolean(name))
|
|
384
|
+
};
|
|
385
|
+
} catch {
|
|
386
|
+
return empty;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function renderRuntimeSummaryBlock(summary) {
|
|
390
|
+
const runtimeNames = summary.runtimeAgentNames.slice(0, 8);
|
|
391
|
+
const lines = [
|
|
392
|
+
`- Runtime agents: ${summary.runtimeAgents}`,
|
|
393
|
+
`- Agent families: ${summary.agentFamilies}`
|
|
394
|
+
];
|
|
395
|
+
if (summary.frameworks.length > 0) {
|
|
396
|
+
lines.push(`- Frameworks: ${summary.frameworks.join(", ")}`);
|
|
397
|
+
}
|
|
398
|
+
if (summary.providers.length > 0) {
|
|
399
|
+
lines.push(`- Providers: ${summary.providers.join(", ")}`);
|
|
400
|
+
}
|
|
401
|
+
if (runtimeNames.length > 0) {
|
|
402
|
+
lines.push(`- Runtime agent names: ${runtimeNames.join(", ")}`);
|
|
403
|
+
}
|
|
404
|
+
return lines.join(`
|
|
405
|
+
`);
|
|
406
|
+
}
|
|
407
|
+
function isRecord(value) {
|
|
408
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/init/sdk-integration.ts
|
|
412
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
413
|
+
import {
|
|
414
|
+
closeSync,
|
|
415
|
+
existsSync as existsSync5,
|
|
416
|
+
mkdirSync as mkdirSync4,
|
|
417
|
+
openSync,
|
|
418
|
+
readFileSync as readFileSync4,
|
|
419
|
+
readSync,
|
|
420
|
+
readdirSync as readdirSync3,
|
|
421
|
+
rmSync as rmSync2,
|
|
422
|
+
statSync as statSync2,
|
|
423
|
+
writeFileSync as writeFileSync3
|
|
424
|
+
} from "node:fs";
|
|
425
|
+
import { dirname as dirname3, join as join5, relative as relative3, resolve as resolve2 } from "node:path";
|
|
426
|
+
import { spawnSync } from "node:child_process";
|
|
427
|
+
|
|
428
|
+
// src/init/sdk-protocol.ts
|
|
429
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
430
|
+
import { join as join4 } from "node:path";
|
|
431
|
+
import { z } from "zod";
|
|
432
|
+
var SDK_RESULT_SCHEMA_VERSION = "moda.sdk_integration_result.v1";
|
|
433
|
+
var SDK_RESULT_JSON_BEGIN = "MODA_SDK_RESULT_JSON_BEGIN";
|
|
434
|
+
var SDK_RESULT_JSON_END = "MODA_SDK_RESULT_JSON_END";
|
|
435
|
+
var SDK_RESULT_FILE = ".moda/sdk-integration-result.json";
|
|
436
|
+
var nonemptyString = z.preprocess((value) => {
|
|
437
|
+
if (typeof value === "string")
|
|
438
|
+
return value.trim();
|
|
439
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
440
|
+
return String(value);
|
|
441
|
+
}
|
|
442
|
+
return value;
|
|
443
|
+
}, z.string().min(1));
|
|
444
|
+
var optionalString = z.preprocess((value) => {
|
|
445
|
+
if (value === undefined)
|
|
446
|
+
return;
|
|
447
|
+
if (typeof value === "string") {
|
|
448
|
+
const trimmed = value.trim();
|
|
449
|
+
return trimmed || undefined;
|
|
450
|
+
}
|
|
451
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
452
|
+
return String(value);
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}, z.string().min(1).optional());
|
|
456
|
+
function tolerantArray(itemSchema) {
|
|
457
|
+
return z.preprocess((value) => {
|
|
458
|
+
if (!Array.isArray(value))
|
|
459
|
+
return [];
|
|
460
|
+
const accepted = [];
|
|
461
|
+
for (const item of value) {
|
|
462
|
+
const parsed = itemSchema.safeParse(item);
|
|
463
|
+
if (parsed.success)
|
|
464
|
+
accepted.push(parsed.data);
|
|
465
|
+
}
|
|
466
|
+
return accepted;
|
|
467
|
+
}, z.array(itemSchema)).default([]);
|
|
468
|
+
}
|
|
469
|
+
var callSiteSchema = z.union([
|
|
470
|
+
nonemptyString.transform((file) => ({ file })),
|
|
471
|
+
z.object({
|
|
472
|
+
file: nonemptyString,
|
|
473
|
+
detail: optionalString
|
|
474
|
+
})
|
|
475
|
+
]);
|
|
476
|
+
var exitCodeSchema = z.preprocess((value) => {
|
|
477
|
+
if (value === null)
|
|
478
|
+
return null;
|
|
479
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
480
|
+
const numeric = Number(value);
|
|
481
|
+
return Number.isFinite(numeric) ? numeric : value;
|
|
482
|
+
}
|
|
483
|
+
return value;
|
|
484
|
+
}, z.union([z.number().finite(), z.null()]));
|
|
485
|
+
var commandSchema = z.object({
|
|
486
|
+
command: nonemptyString,
|
|
487
|
+
exit_code: exitCodeSchema,
|
|
488
|
+
kind: optionalString
|
|
489
|
+
});
|
|
490
|
+
var smokeSchema = z.object({
|
|
491
|
+
method: nonemptyString,
|
|
492
|
+
command: optionalString,
|
|
493
|
+
verification_id: nonemptyString
|
|
494
|
+
});
|
|
495
|
+
var failureSchema = z.object({
|
|
496
|
+
class: nonemptyString,
|
|
497
|
+
detail: nonemptyString
|
|
498
|
+
});
|
|
499
|
+
var SdkAgentResultSchema = z.object({
|
|
500
|
+
schema_version: z.string().trim().pipe(z.literal(SDK_RESULT_SCHEMA_VERSION)),
|
|
501
|
+
status: z.preprocess((value) => typeof value === "string" ? value.trim() : value, z.enum([
|
|
502
|
+
"integrated",
|
|
503
|
+
"needs_human_input",
|
|
504
|
+
"repository_checks_failed",
|
|
505
|
+
"smoke_execution_failed",
|
|
506
|
+
"nothing_to_integrate",
|
|
507
|
+
"failed"
|
|
508
|
+
])),
|
|
509
|
+
summary: nonemptyString,
|
|
510
|
+
skills_consulted: tolerantArray(nonemptyString),
|
|
511
|
+
skills_used: tolerantArray(nonemptyString),
|
|
512
|
+
packages_installed: tolerantArray(nonemptyString),
|
|
513
|
+
files_changed: tolerantArray(nonemptyString),
|
|
514
|
+
call_sites_instrumented: tolerantArray(callSiteSchema),
|
|
515
|
+
commands_run: tolerantArray(commandSchema),
|
|
516
|
+
smoke: z.preprocess((value) => value ?? null, smokeSchema.nullable()).default(null),
|
|
517
|
+
remaining_actions: tolerantArray(nonemptyString),
|
|
518
|
+
failure: z.preprocess((value) => value ?? null, failureSchema.nullable()).default(null)
|
|
519
|
+
});
|
|
520
|
+
function extractSdkAgentResult(opts) {
|
|
521
|
+
const diagnostics = [];
|
|
522
|
+
const resultPath = join4(opts.cwd, SDK_RESULT_FILE);
|
|
523
|
+
if (existsSync4(resultPath)) {
|
|
524
|
+
try {
|
|
525
|
+
const parsed = parseCandidate(readFileSync3(resultPath, "utf8"));
|
|
526
|
+
if (parsed.result)
|
|
527
|
+
return { result: parsed.result, source: "file" };
|
|
528
|
+
diagnostics.push(`result file: ${parsed.detail}`);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
diagnostics.push(`result file: ${errorMessage(error)}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const output = [opts.stdout, opts.stderr].filter(Boolean).join(`
|
|
534
|
+
`);
|
|
535
|
+
const candidates = markerCandidates(output);
|
|
536
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
537
|
+
const parsed = parseCandidate(candidate);
|
|
538
|
+
if (parsed.result)
|
|
539
|
+
return { result: parsed.result, source: "markers" };
|
|
540
|
+
diagnostics.push(`marker block ${index + 1}: ${parsed.detail}`);
|
|
541
|
+
}
|
|
542
|
+
if (candidates.length === 0) {
|
|
543
|
+
diagnostics.push(`no ${SDK_RESULT_JSON_BEGIN}/${SDK_RESULT_JSON_END} marker block found`);
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
error: "SDK integration result protocol violation.",
|
|
547
|
+
detail: diagnostics.join("; ")
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function markerCandidates(output) {
|
|
551
|
+
const pattern = new RegExp(`${SDK_RESULT_JSON_BEGIN}\\s*([\\s\\S]*?)\\s*${SDK_RESULT_JSON_END}`, "g");
|
|
552
|
+
const candidates = [];
|
|
553
|
+
for (const match of output.matchAll(pattern)) {
|
|
554
|
+
if (match[1])
|
|
555
|
+
candidates.push(match[1]);
|
|
556
|
+
}
|
|
557
|
+
return candidates;
|
|
558
|
+
}
|
|
559
|
+
function parseCandidate(candidate) {
|
|
560
|
+
let value;
|
|
561
|
+
try {
|
|
562
|
+
value = JSON.parse(candidate.trim());
|
|
563
|
+
} catch (error) {
|
|
564
|
+
return { detail: `invalid JSON (${errorMessage(error)})` };
|
|
565
|
+
}
|
|
566
|
+
const parsed = SdkAgentResultSchema.safeParse(value);
|
|
567
|
+
if (parsed.success)
|
|
568
|
+
return { result: parsed.data };
|
|
569
|
+
const detail = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join(", ");
|
|
570
|
+
return { detail: `validation failed (${detail})` };
|
|
571
|
+
}
|
|
572
|
+
function errorMessage(error) {
|
|
573
|
+
return error instanceof Error ? error.message : String(error);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/init/sdk-verification.ts
|
|
577
|
+
import { randomBytes } from "node:crypto";
|
|
578
|
+
var DEFAULT_TIMEOUT_MS = 90000;
|
|
579
|
+
var DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
580
|
+
var MIN_TIMEOUT_MS = 1e4;
|
|
581
|
+
var MAX_TIMEOUT_MS = 600000;
|
|
582
|
+
function generateVerificationId() {
|
|
583
|
+
return `moda-verify-${randomBytes(16).toString("hex")}`;
|
|
584
|
+
}
|
|
585
|
+
async function verifyExactTrace(opts) {
|
|
586
|
+
const now = opts.now ?? Date.now;
|
|
587
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
588
|
+
const fetchAudit = opts.fetchAudit;
|
|
589
|
+
const timeoutMs = opts.timeoutMs === undefined || !Number.isFinite(opts.timeoutMs) ? resolveTimeoutFromEnv() : Math.max(0, opts.timeoutMs);
|
|
590
|
+
const requestedPollInterval = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
591
|
+
const pollIntervalMs = Number.isFinite(requestedPollInterval) ? Math.max(1, requestedPollInterval) : DEFAULT_POLL_INTERVAL_MS;
|
|
592
|
+
const startedAt = now();
|
|
593
|
+
const deadline = startedAt + Math.max(0, timeoutMs);
|
|
594
|
+
let polls = 0;
|
|
595
|
+
let lastError;
|
|
596
|
+
let counts = {
|
|
597
|
+
total_spans: 0,
|
|
598
|
+
llm_spans: 0,
|
|
599
|
+
tool_spans: 0,
|
|
600
|
+
trace_ids: [],
|
|
601
|
+
orphan_count: 0,
|
|
602
|
+
duplicate_count: 0,
|
|
603
|
+
ingest_rows: 0
|
|
604
|
+
};
|
|
605
|
+
if (!fetchAudit) {
|
|
606
|
+
lastError = "verification requires an explicit audit fetcher bound to the provisioned API key";
|
|
607
|
+
return buildEvidence(false);
|
|
608
|
+
}
|
|
609
|
+
while (true) {
|
|
610
|
+
polls++;
|
|
611
|
+
try {
|
|
612
|
+
const response = await fetchAudit(opts.verificationId);
|
|
613
|
+
const parsed = readAuditCounts(response);
|
|
614
|
+
if (parsed) {
|
|
615
|
+
counts = parsed;
|
|
616
|
+
if (counts.total_spans >= 1 && counts.llm_spans >= 1) {
|
|
617
|
+
return buildEvidence(true);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
622
|
+
}
|
|
623
|
+
const currentTime = now();
|
|
624
|
+
if (currentTime >= deadline)
|
|
625
|
+
return buildEvidence(false);
|
|
626
|
+
await sleep(Math.min(pollIntervalMs, deadline - currentTime));
|
|
627
|
+
}
|
|
628
|
+
function buildEvidence(observed) {
|
|
629
|
+
return {
|
|
630
|
+
observed,
|
|
631
|
+
verification_id: opts.verificationId,
|
|
632
|
+
...counts,
|
|
633
|
+
elapsed_ms: Math.max(0, now() - startedAt),
|
|
634
|
+
polls,
|
|
635
|
+
...lastError ? { last_error: lastError } : {}
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function resolveTimeoutFromEnv() {
|
|
640
|
+
const raw = process.env.MODA_SDK_VERIFY_TIMEOUT_MS;
|
|
641
|
+
if (!raw)
|
|
642
|
+
return DEFAULT_TIMEOUT_MS;
|
|
643
|
+
const parsed = Number(raw);
|
|
644
|
+
if (!Number.isFinite(parsed))
|
|
645
|
+
return DEFAULT_TIMEOUT_MS;
|
|
646
|
+
return Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.trunc(parsed)));
|
|
647
|
+
}
|
|
648
|
+
function readAuditCounts(value) {
|
|
649
|
+
if (!isRecord2(value) || !isRecord2(value.summary))
|
|
650
|
+
return;
|
|
651
|
+
const summary = value.summary;
|
|
652
|
+
if (!isNonNegativeNumber(summary.total_spans) || !isRecord2(summary.by_type))
|
|
653
|
+
return;
|
|
654
|
+
return {
|
|
655
|
+
total_spans: summary.total_spans,
|
|
656
|
+
llm_spans: isNonNegativeNumber(summary.by_type.llm) ? summary.by_type.llm : 0,
|
|
657
|
+
tool_spans: isNonNegativeNumber(summary.by_type.tool) ? summary.by_type.tool : 0,
|
|
658
|
+
trace_ids: Array.isArray(summary.trace_ids) ? summary.trace_ids.filter((item) => typeof item === "string") : [],
|
|
659
|
+
orphan_count: isNonNegativeNumber(summary.orphan_count) ? summary.orphan_count : 0,
|
|
660
|
+
duplicate_count: isNonNegativeNumber(summary.duplicate_count) ? summary.duplicate_count : 0,
|
|
661
|
+
ingest_rows: isNonNegativeNumber(summary.ingest_rows) ? summary.ingest_rows : 0
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function isRecord2(value) {
|
|
665
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
666
|
+
}
|
|
667
|
+
function isNonNegativeNumber(value) {
|
|
668
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
669
|
+
}
|
|
670
|
+
async function fetchAuditWithApiKey(id, apiKey, baseUrl) {
|
|
671
|
+
const url = `${baseUrl}/api/v1/data/audit/${encodeURIComponent(id)}?kind=conversation`;
|
|
672
|
+
const response = await fetch(url, {
|
|
673
|
+
method: "GET",
|
|
674
|
+
headers: { "x-api-key": apiKey },
|
|
675
|
+
signal: AbortSignal.timeout(1e4)
|
|
676
|
+
});
|
|
677
|
+
const body = await response.text();
|
|
678
|
+
if (!response.ok) {
|
|
679
|
+
throw new Error(`Audit fetch failed (${response.status}): ${body.slice(0, 200)}`);
|
|
680
|
+
}
|
|
681
|
+
return JSON.parse(body);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/init/sdk-integration.ts
|
|
685
|
+
var OUTCOME_SCHEMA_VERSION = "moda.sdk_integration_outcome.v1";
|
|
686
|
+
var MAX_SCAN_FILE_BYTES = 1048576;
|
|
687
|
+
var MAX_SCANNED_FILES = 2000;
|
|
688
|
+
var MAX_SCAN_DIRECTORIES = 2000;
|
|
689
|
+
var SECRET_KEY_PATTERN = /moda_sk_[A-Za-z0-9]{8,}/g;
|
|
690
|
+
var DEGRADED_GUARDS_NOTE = "changed-file guards degraded: not a git repository";
|
|
691
|
+
async function runSdkIntegration(options = {}) {
|
|
692
|
+
const cwd = options.cwd ?? process.cwd();
|
|
693
|
+
const available = detectAvailableCodingAgents();
|
|
694
|
+
if (available.length === 0) {
|
|
695
|
+
return { status: "agent_unavailable", repair_attempts: 0 };
|
|
696
|
+
}
|
|
697
|
+
const adapter = options.adapter && available.some((item) => item.adapter === options.adapter) ? options.adapter : available[0].adapter;
|
|
698
|
+
const human = options.human ?? false;
|
|
699
|
+
const emit = (message) => {
|
|
700
|
+
const redacted = redactSecretText(message, options.apiKey);
|
|
701
|
+
options.onProgress?.(redacted);
|
|
702
|
+
if (human)
|
|
703
|
+
process.stderr.write(` ${codingAgentDisplayName(adapter)}: ${redacted}
|
|
704
|
+
`);
|
|
705
|
+
};
|
|
706
|
+
const runAgent = options.runAgent ?? runCodingAgent;
|
|
707
|
+
const verify = options.verify ?? verifyExactTrace;
|
|
708
|
+
const maxRepairAttempts = resolveMaxRepairAttempts(options.maxRepairAttempts);
|
|
709
|
+
const runId = randomBytes2(4).toString("hex");
|
|
710
|
+
const verificationId = generateVerificationId();
|
|
711
|
+
let catalog;
|
|
712
|
+
let gitBaseline = null;
|
|
713
|
+
let runStartedAt = Date.now();
|
|
714
|
+
let guardsDegraded = false;
|
|
715
|
+
let degradedGuardsEmitted = false;
|
|
716
|
+
const markGuardsDegraded = () => {
|
|
717
|
+
guardsDegraded = true;
|
|
718
|
+
if (degradedGuardsEmitted)
|
|
719
|
+
return;
|
|
720
|
+
degradedGuardsEmitted = true;
|
|
721
|
+
emit(DEGRADED_GUARDS_NOTE);
|
|
722
|
+
};
|
|
723
|
+
try {
|
|
724
|
+
catalog = materializeSdkCatalog(cwd, runId);
|
|
725
|
+
gitBaseline = snapshotGitStatus(cwd);
|
|
726
|
+
runStartedAt = Date.now();
|
|
727
|
+
if (gitBaseline === null)
|
|
728
|
+
markGuardsDegraded();
|
|
729
|
+
} catch (error) {
|
|
730
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
731
|
+
return {
|
|
732
|
+
status: "agent_failed",
|
|
733
|
+
adapter,
|
|
734
|
+
verification_id: verificationId,
|
|
735
|
+
repair_attempts: 0,
|
|
736
|
+
detail: redactSecretText(`Could not materialize the SDK skill catalog: ${detail}`, options.apiKey)
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const attemptCtx = {
|
|
740
|
+
cwd,
|
|
741
|
+
adapter,
|
|
742
|
+
verificationId,
|
|
743
|
+
catalog,
|
|
744
|
+
gitBaseline,
|
|
745
|
+
apiKey: options.apiKey,
|
|
746
|
+
ingestUrl: options.ingestUrl,
|
|
747
|
+
human,
|
|
748
|
+
emit,
|
|
749
|
+
onEvent: options.onEvent,
|
|
750
|
+
onPhase: options.onPhase,
|
|
751
|
+
runAgent,
|
|
752
|
+
verify,
|
|
753
|
+
fetchAudit: options.fetchAudit,
|
|
754
|
+
verifyTimeoutMs: options.verifyTimeoutMs
|
|
755
|
+
};
|
|
756
|
+
let repairAttemptsUsed = 0;
|
|
757
|
+
let lastAgentResult;
|
|
758
|
+
let lastVerification;
|
|
759
|
+
let lastSecurityFindings;
|
|
760
|
+
let lastOutcome = { status: "agent_failed", detail: "No agent attempt completed." };
|
|
761
|
+
let installedSkillIds;
|
|
762
|
+
let protocolRetryUsed = false;
|
|
763
|
+
try {
|
|
764
|
+
options.onPhase?.("investigating", "Investigating repository and integrating the Moda SDK…");
|
|
765
|
+
emit("Starting SDK integration…");
|
|
766
|
+
while (true) {
|
|
767
|
+
const isRepair = repairAttemptsUsed > 0;
|
|
768
|
+
const prompt = isRepair ? renderSdkRepairPrompt({
|
|
769
|
+
catalogDir: catalog.relativeDir,
|
|
770
|
+
catalogListing: renderCatalogListing(catalog.index),
|
|
771
|
+
verificationId,
|
|
772
|
+
attempt: repairAttemptsUsed,
|
|
773
|
+
maxAttempts: maxRepairAttempts,
|
|
774
|
+
classification: lastOutcome.repairable ?? "telemetry_not_observed",
|
|
775
|
+
previousResult: lastAgentResult,
|
|
776
|
+
verification: lastVerification,
|
|
777
|
+
securityFindings: lastSecurityFindings,
|
|
778
|
+
harnessSummary: readRuntimeSummary(cwd)
|
|
779
|
+
}) : renderSdkIntegrationPrompt({
|
|
780
|
+
catalogDir: catalog.relativeDir,
|
|
781
|
+
catalogListing: renderCatalogListing(catalog.index),
|
|
782
|
+
verificationId,
|
|
783
|
+
harnessSummary: readRuntimeSummary(cwd)
|
|
784
|
+
});
|
|
785
|
+
const run = await invokeAgent(attemptCtx, prompt);
|
|
786
|
+
const extraction = await extractResultWithProtocolRetry(attemptCtx, run, protocolRetryUsed, () => {
|
|
787
|
+
protocolRetryUsed = true;
|
|
788
|
+
});
|
|
789
|
+
if ("agentFailed" in extraction) {
|
|
790
|
+
lastOutcome = extraction.agentFailed;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
lastAgentResult = sanitizeAgentResult(extraction.result, options.apiKey);
|
|
794
|
+
removeAgentResultFile(cwd);
|
|
795
|
+
const guards = applyDeterministicGuards(cwd, gitBaseline, lastAgentResult, runStartedAt, options.apiKey);
|
|
796
|
+
if (guards.guardsDegraded)
|
|
797
|
+
markGuardsDegraded();
|
|
798
|
+
lastSecurityFindings = guards.securityFindings.length > 0 ? guards.securityFindings : undefined;
|
|
799
|
+
if (guards.securityFindings.length > 0) {
|
|
800
|
+
lastOutcome = {
|
|
801
|
+
status: "needs_human_input",
|
|
802
|
+
detail: "remove the leaked key, rotate it, and keep secrets in the environment"
|
|
803
|
+
};
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
if (guards.dishonestIntegrated) {
|
|
807
|
+
lastOutcome = {
|
|
808
|
+
status: "agent_failed",
|
|
809
|
+
detail: "agent reported integration but no files changed"
|
|
810
|
+
};
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
lastOutcome = await mapAgentResultToOutcome(attemptCtx, lastAgentResult);
|
|
814
|
+
if (lastOutcome.verification) {
|
|
815
|
+
lastVerification = lastOutcome.verification;
|
|
816
|
+
}
|
|
817
|
+
if (!lastOutcome.repairable || repairAttemptsUsed >= maxRepairAttempts) {
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
repairAttemptsUsed += 1;
|
|
821
|
+
options.onPhase?.("repairing", `Repairing SDK integration (attempt ${repairAttemptsUsed} of ${maxRepairAttempts})…`);
|
|
822
|
+
emit(`Repair attempt ${repairAttemptsUsed} of ${maxRepairAttempts}…`);
|
|
823
|
+
}
|
|
824
|
+
if (lastOutcome.status === "integrated_verified" && lastAgentResult) {
|
|
825
|
+
installedSkillIds = installUsedSkills(lastAgentResult, catalog, cwd, emit);
|
|
826
|
+
}
|
|
827
|
+
const finalStatus = lastOutcome.status;
|
|
828
|
+
const finalDetail = composeFinalDetail(finalStatus, lastOutcome.detail, lastVerification, options.apiKey, guardsDegraded);
|
|
829
|
+
writeFinalOutcomeArtifact(cwd, {
|
|
830
|
+
status: finalStatus,
|
|
831
|
+
verification_id: verificationId,
|
|
832
|
+
verification: lastVerification,
|
|
833
|
+
agent_result: lastAgentResult,
|
|
834
|
+
repair_attempts: repairAttemptsUsed,
|
|
835
|
+
security_findings: lastSecurityFindings
|
|
836
|
+
});
|
|
837
|
+
return {
|
|
838
|
+
status: finalStatus,
|
|
839
|
+
adapter,
|
|
840
|
+
verification_id: verificationId,
|
|
841
|
+
verification: lastVerification,
|
|
842
|
+
agent_result: lastAgentResult,
|
|
843
|
+
repair_attempts: repairAttemptsUsed,
|
|
844
|
+
...lastSecurityFindings ? { security_findings: lastSecurityFindings } : {},
|
|
845
|
+
...installedSkillIds ? { installed_skill_ids: installedSkillIds } : {},
|
|
846
|
+
...finalDetail ? { detail: finalDetail } : {}
|
|
847
|
+
};
|
|
848
|
+
} finally {
|
|
849
|
+
cleanupSdkCatalog(catalog.dir);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function renderSdkIntegrationPrompt(opts) {
|
|
853
|
+
const summary = opts.harnessSummary;
|
|
854
|
+
const hasHarnessSignals = Boolean(summary && (summary.runtimeAgents > 0 || summary.frameworks.length > 0 || summary.providers.length > 0));
|
|
855
|
+
const harnessBlock = summary && hasHarnessSignals ? [
|
|
856
|
+
"Context from Moda's earlier harness discovery (may be incomplete):",
|
|
857
|
+
renderRuntimeSummaryBlock(summary),
|
|
858
|
+
""
|
|
859
|
+
] : [];
|
|
860
|
+
return [
|
|
861
|
+
"You are running inside `moda init` as a one-shot SDK integration task. The user has",
|
|
862
|
+
"approved letting you install packages and edit source files in THIS repository to add",
|
|
863
|
+
"Moda LLM observability. Do the integration now — do not just describe it.",
|
|
864
|
+
"",
|
|
865
|
+
...SAFETY_RULES,
|
|
866
|
+
"",
|
|
867
|
+
"Investigation:",
|
|
868
|
+
"- Inspect this repository yourself: workspace/monorepo layout, package manifests and",
|
|
869
|
+
" lockfiles, scripts, app entry points, LLM clients/wrappers, existing telemetry or",
|
|
870
|
+
" existing Moda setup, tests/build/typecheck/lint commands, and runtime requirements.",
|
|
871
|
+
"- If Moda is already integrated, do not duplicate it. You MUST still run the smoke",
|
|
872
|
+
" through the EXISTING integration with the provided verification id, then report",
|
|
873
|
+
" `nothing_to_integrate` with an honest summary and the `smoke` field set.",
|
|
874
|
+
"",
|
|
875
|
+
...harnessBlock,
|
|
876
|
+
"Skill catalog (read relevant skills; multiple may apply):",
|
|
877
|
+
opts.catalogListing,
|
|
878
|
+
"",
|
|
879
|
+
`Read \`${opts.catalogDir}/<id>/SKILL.md\` for every skill relevant to this repo`,
|
|
880
|
+
"(EXAMPLE.md beside it shows the finished integration). Multiple skills may apply",
|
|
881
|
+
"(e.g. a monorepo with a Node service and a Python service, or two providers) —",
|
|
882
|
+
"apply each relevant one. The catalog is guidance, not a constraint: if the repo's",
|
|
883
|
+
"stack has no matching skill, use the closest skill plus the docs pattern, and say",
|
|
884
|
+
"so in the result.",
|
|
885
|
+
"",
|
|
886
|
+
"Repository checks:",
|
|
887
|
+
"- Discover and run this repo's own build/typecheck/lint/test commands relevant to",
|
|
888
|
+
" your changes; record each in commands_run with exit codes.",
|
|
889
|
+
"- If they fail and you cannot fix within the integration scope → status",
|
|
890
|
+
" repository_checks_failed.",
|
|
891
|
+
"",
|
|
892
|
+
"Smoke + verification id:",
|
|
893
|
+
`- Run one real request through an instrumented application path that makes an actual`,
|
|
894
|
+
" LLM call, with the Moda conversation id set to EXACTLY",
|
|
895
|
+
` \`${opts.verificationId}\` (each skill documents the conversation-id API, e.g.`,
|
|
896
|
+
" Moda.conversationId / moda.conversation_id).",
|
|
897
|
+
"- Prefer a minimal temporary script under .moda/tmp/ (delete it after) or an existing",
|
|
898
|
+
" safe entry point — never a destructive path (no prod mutations, no long-running",
|
|
899
|
+
" servers left up). Flush before exit.",
|
|
900
|
+
"- MODA_API_KEY and MODA_INGEST_URL are already exported in your environment for the",
|
|
901
|
+
" smoke run — reference them, never print or copy their values.",
|
|
902
|
+
"- If the smoke cannot run (missing provider key, needs a running service you cannot",
|
|
903
|
+
" start safely) → status smoke_execution_failed or needs_human_input with",
|
|
904
|
+
" remaining_actions.",
|
|
905
|
+
"",
|
|
906
|
+
renderResultProtocolSection()
|
|
907
|
+
].join(`
|
|
908
|
+
`);
|
|
909
|
+
}
|
|
910
|
+
function renderSdkRepairPrompt(opts) {
|
|
911
|
+
const evidence = {
|
|
912
|
+
previous_agent_result: opts.previousResult ?? null,
|
|
913
|
+
verification: opts.verification ?? null,
|
|
914
|
+
security_findings: opts.securityFindings ?? []
|
|
915
|
+
};
|
|
916
|
+
return [
|
|
917
|
+
`Repair attempt ${opts.attempt} of ${opts.maxAttempts} failed: ${opts.classification}`,
|
|
918
|
+
"",
|
|
919
|
+
"Structured evidence from the previous attempt:",
|
|
920
|
+
JSON.stringify(evidence, null, 2),
|
|
921
|
+
"",
|
|
922
|
+
...SAFETY_RULES,
|
|
923
|
+
"",
|
|
924
|
+
`Skill catalog directory: ${opts.catalogDir}`,
|
|
925
|
+
opts.catalogListing,
|
|
926
|
+
"",
|
|
927
|
+
`Use the SAME verification id for the smoke run: \`${opts.verificationId}\`.`,
|
|
928
|
+
"Re-run only what is needed; report honestly via the result protocol.",
|
|
929
|
+
"",
|
|
930
|
+
renderResultProtocolSection()
|
|
931
|
+
].join(`
|
|
932
|
+
`);
|
|
933
|
+
}
|
|
934
|
+
var SAFETY_RULES = [
|
|
935
|
+
"Scope and safety rules:",
|
|
936
|
+
"- Only touch this repository (the working directory you were launched in).",
|
|
937
|
+
"- Install the Moda SDK with the package manager this repo already uses.",
|
|
938
|
+
"- Instrument the real LLM call sites; do not invent files or frameworks that are not here.",
|
|
939
|
+
"- NEVER write MODA_API_KEY or any secret into source files, and never create or edit",
|
|
940
|
+
" .env files. The key is already exported in your environment as MODA_API_KEY for the",
|
|
941
|
+
" smoke run — reference it, never print or copy its value.",
|
|
942
|
+
"- Make the smallest correct change. Match the existing code style and imports.",
|
|
943
|
+
"- Do not modify anything under the ephemeral skill catalog directory.",
|
|
944
|
+
"- Do not run `git commit`.",
|
|
945
|
+
"- Do not edit `.moda/` state files except `.moda/sdk-integration-result.json`.",
|
|
946
|
+
"- If the repo has no LLM calls or something is ambiguous, stop and report honestly",
|
|
947
|
+
" via the result protocol."
|
|
948
|
+
];
|
|
949
|
+
function renderResultProtocolSection() {
|
|
950
|
+
return [
|
|
951
|
+
"Result protocol (REQUIRED — write file AND print markers as your last output):",
|
|
952
|
+
`- Write \`${SDK_RESULT_FILE}\` with schema_version \`${SDK_RESULT_SCHEMA_VERSION}\`.`,
|
|
953
|
+
`- Print the same JSON between ${SDK_RESULT_JSON_BEGIN} and ${SDK_RESULT_JSON_END}.`,
|
|
954
|
+
"Fields:",
|
|
955
|
+
"- schema_version (required): must be moda.sdk_integration_result.v1",
|
|
956
|
+
"- status (required): integrated | needs_human_input | repository_checks_failed |",
|
|
957
|
+
" smoke_execution_failed | nothing_to_integrate | failed",
|
|
958
|
+
"- summary (required): one-line human summary",
|
|
959
|
+
"- skills_consulted: string[] of catalog skill ids you read",
|
|
960
|
+
"- skills_used: string[] of catalog skill ids you applied",
|
|
961
|
+
"- packages_installed: string[]",
|
|
962
|
+
"- files_changed: string[] of repo-relative paths",
|
|
963
|
+
"- call_sites_instrumented: { file, detail? }[] (plain strings accepted as {file})",
|
|
964
|
+
"- commands_run: { command, exit_code, kind? }[]",
|
|
965
|
+
`- smoke: { method, command?, verification_id } | null — verification_id must be exactly`,
|
|
966
|
+
" the id provided above when smoke ran",
|
|
967
|
+
"- remaining_actions: string[] for the human when blocked",
|
|
968
|
+
"- failure: { class, detail } | null",
|
|
969
|
+
"",
|
|
970
|
+
"Report honestly: `integrated` means you completed integration, checks passed, and the",
|
|
971
|
+
"smoke ran — Moda will independently verify the trace; false claims are detected."
|
|
972
|
+
].join(`
|
|
973
|
+
`);
|
|
974
|
+
}
|
|
975
|
+
function resolveMaxRepairAttempts(override) {
|
|
976
|
+
if (override !== undefined && Number.isFinite(override)) {
|
|
977
|
+
return Math.min(3, Math.max(0, Math.trunc(override)));
|
|
978
|
+
}
|
|
979
|
+
const raw = process.env.MODA_SDK_REPAIR_ATTEMPTS;
|
|
980
|
+
if (!raw)
|
|
981
|
+
return 1;
|
|
982
|
+
const parsed = Number(raw);
|
|
983
|
+
if (!Number.isFinite(parsed))
|
|
984
|
+
return 1;
|
|
985
|
+
return Math.min(3, Math.max(0, Math.trunc(parsed)));
|
|
986
|
+
}
|
|
987
|
+
function snapshotGitStatus(cwd) {
|
|
988
|
+
const result = spawnSync("git", ["-c", "core.quotePath=off", "status", "--porcelain"], {
|
|
989
|
+
cwd,
|
|
990
|
+
encoding: "utf8"
|
|
991
|
+
});
|
|
992
|
+
if (result.status !== 0)
|
|
993
|
+
return null;
|
|
994
|
+
return result.stdout ?? "";
|
|
995
|
+
}
|
|
996
|
+
function gitDelta(baseline, cwd) {
|
|
997
|
+
const after = snapshotGitStatus(cwd);
|
|
998
|
+
if (baseline === null || after === null)
|
|
999
|
+
return { lines: [], degraded: true };
|
|
1000
|
+
const beforeLines = new Set(baseline.split(`
|
|
1001
|
+
`).filter(Boolean));
|
|
1002
|
+
return {
|
|
1003
|
+
lines: after.split(`
|
|
1004
|
+
`).filter((line) => line && !beforeLines.has(line)),
|
|
1005
|
+
degraded: false
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function parseGitDeltaPaths(deltaLines) {
|
|
1009
|
+
const paths = [];
|
|
1010
|
+
for (const line of deltaLines) {
|
|
1011
|
+
if (!line.trim())
|
|
1012
|
+
continue;
|
|
1013
|
+
const body = line.slice(3).trim();
|
|
1014
|
+
if (!body)
|
|
1015
|
+
continue;
|
|
1016
|
+
const renameAt = body.lastIndexOf(" -> ");
|
|
1017
|
+
const rawPath = renameAt >= 0 ? body.slice(renameAt + 4) : body;
|
|
1018
|
+
paths.push(unquoteGitPath(rawPath));
|
|
1019
|
+
}
|
|
1020
|
+
return paths;
|
|
1021
|
+
}
|
|
1022
|
+
var GIT_QUOTE_ESCAPES = {
|
|
1023
|
+
'"': 34,
|
|
1024
|
+
"\\": 92,
|
|
1025
|
+
a: 7,
|
|
1026
|
+
b: 8,
|
|
1027
|
+
f: 12,
|
|
1028
|
+
n: 10,
|
|
1029
|
+
r: 13,
|
|
1030
|
+
t: 9,
|
|
1031
|
+
v: 11
|
|
1032
|
+
};
|
|
1033
|
+
function unquoteGitPath(path) {
|
|
1034
|
+
const trimmed = path.trim();
|
|
1035
|
+
if (trimmed.length < 2 || trimmed[0] !== '"' || trimmed.at(-1) !== '"')
|
|
1036
|
+
return trimmed;
|
|
1037
|
+
const inner = trimmed.slice(1, -1);
|
|
1038
|
+
const bytes = [];
|
|
1039
|
+
for (let i = 0;i < inner.length; i++) {
|
|
1040
|
+
const ch = inner[i];
|
|
1041
|
+
if (ch !== "\\") {
|
|
1042
|
+
for (const byte of Buffer.from(ch, "utf8"))
|
|
1043
|
+
bytes.push(byte);
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const octal = inner.slice(i + 1, i + 4);
|
|
1047
|
+
if (/^[0-7]{3}$/.test(octal)) {
|
|
1048
|
+
bytes.push(parseInt(octal, 8));
|
|
1049
|
+
i += 3;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const next = inner[i + 1];
|
|
1053
|
+
if (next !== undefined && GIT_QUOTE_ESCAPES[next] !== undefined) {
|
|
1054
|
+
bytes.push(GIT_QUOTE_ESCAPES[next]);
|
|
1055
|
+
i += 1;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
bytes.push(92);
|
|
1059
|
+
}
|
|
1060
|
+
return Buffer.from(bytes).toString("utf8");
|
|
1061
|
+
}
|
|
1062
|
+
var MTIME_SLACK_MS = 2000;
|
|
1063
|
+
function filesTouchedSince(cwd, paths, sinceMs) {
|
|
1064
|
+
const resolvedCwd = resolve2(cwd);
|
|
1065
|
+
return paths.some((relPath) => {
|
|
1066
|
+
const absPath = resolve2(cwd, relPath.replace(/\\/g, "/"));
|
|
1067
|
+
if (!absPath.startsWith(resolvedCwd + "/") && absPath !== resolvedCwd)
|
|
1068
|
+
return false;
|
|
1069
|
+
try {
|
|
1070
|
+
return statSync2(absPath).mtimeMs >= sinceMs - MTIME_SLACK_MS;
|
|
1071
|
+
} catch {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
function applyDeterministicGuards(cwd, gitBaseline, result, runStartedAt, apiKey) {
|
|
1077
|
+
const delta = gitDelta(gitBaseline, cwd);
|
|
1078
|
+
const deltaPaths = parseGitDeltaPaths(delta.lines);
|
|
1079
|
+
const candidatePaths = uniquePaths([...deltaPaths, ...result.files_changed]);
|
|
1080
|
+
const securityFindings = scanForSecrets(cwd, candidatePaths, apiKey);
|
|
1081
|
+
const claimCorroborated = deltaPaths.length > 0 || filesTouchedSince(cwd, result.files_changed, runStartedAt);
|
|
1082
|
+
const dishonestIntegrated = result.status === "integrated" && (delta.degraded ? result.files_changed.length === 0 : !claimCorroborated);
|
|
1083
|
+
return { securityFindings, dishonestIntegrated, guardsDegraded: delta.degraded };
|
|
1084
|
+
}
|
|
1085
|
+
function uniquePaths(paths) {
|
|
1086
|
+
return [...new Set(paths.map((p) => p.replace(/\\/g, "/")).filter(Boolean))];
|
|
1087
|
+
}
|
|
1088
|
+
function scanForSecrets(cwd, paths, apiKey) {
|
|
1089
|
+
const findings = [];
|
|
1090
|
+
const resolvedCwd = resolve2(cwd);
|
|
1091
|
+
const scanPaths = collectSecretScanFiles(cwd, paths);
|
|
1092
|
+
for (const relPath of scanPaths) {
|
|
1093
|
+
const absPath = resolve2(cwd, relPath);
|
|
1094
|
+
if (!absPath.startsWith(resolvedCwd + "/") && absPath !== resolvedCwd)
|
|
1095
|
+
continue;
|
|
1096
|
+
if (!existsSync5(absPath))
|
|
1097
|
+
continue;
|
|
1098
|
+
let stat;
|
|
1099
|
+
try {
|
|
1100
|
+
stat = statSync2(absPath);
|
|
1101
|
+
} catch {
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (!stat.isFile())
|
|
1105
|
+
continue;
|
|
1106
|
+
let content;
|
|
1107
|
+
try {
|
|
1108
|
+
content = stat.size > MAX_SCAN_FILE_BYTES ? readFilePrefix(absPath, MAX_SCAN_FILE_BYTES) : readFileSync4(absPath, "utf8");
|
|
1109
|
+
} catch {
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (content.includes("\x00"))
|
|
1113
|
+
continue;
|
|
1114
|
+
let leaked = false;
|
|
1115
|
+
if (apiKey && content.includes(apiKey))
|
|
1116
|
+
leaked = true;
|
|
1117
|
+
SECRET_KEY_PATTERN.lastIndex = 0;
|
|
1118
|
+
if (SECRET_KEY_PATTERN.test(content))
|
|
1119
|
+
leaked = true;
|
|
1120
|
+
if (leaked)
|
|
1121
|
+
findings.push(relPath.replace(/\\/g, "/"));
|
|
1122
|
+
}
|
|
1123
|
+
return findings;
|
|
1124
|
+
}
|
|
1125
|
+
function readFilePrefix(absPath, maxBytes) {
|
|
1126
|
+
const fd = openSync(absPath, "r");
|
|
1127
|
+
try {
|
|
1128
|
+
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
1129
|
+
const bytesRead = readSync(fd, buffer, 0, maxBytes, 0);
|
|
1130
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
1131
|
+
} finally {
|
|
1132
|
+
closeSync(fd);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function collectSecretScanFiles(cwd, paths) {
|
|
1136
|
+
const resolvedCwd = resolve2(cwd);
|
|
1137
|
+
const files = [];
|
|
1138
|
+
const seenFiles = new Set;
|
|
1139
|
+
let visitedDirectories = 0;
|
|
1140
|
+
const isInsideRepo = (path) => path === resolvedCwd || path.startsWith(`${resolvedCwd}/`);
|
|
1141
|
+
const addFile = (path) => {
|
|
1142
|
+
if (files.length >= MAX_SCANNED_FILES || !isInsideRepo(path))
|
|
1143
|
+
return;
|
|
1144
|
+
const relPath = relative3(resolvedCwd, path).replace(/\\/g, "/");
|
|
1145
|
+
if (!relPath || seenFiles.has(relPath))
|
|
1146
|
+
return;
|
|
1147
|
+
seenFiles.add(relPath);
|
|
1148
|
+
files.push(relPath);
|
|
1149
|
+
};
|
|
1150
|
+
const addEnvFiles = (dir) => {
|
|
1151
|
+
if (files.length >= MAX_SCANNED_FILES || !isInsideRepo(dir))
|
|
1152
|
+
return;
|
|
1153
|
+
let entries;
|
|
1154
|
+
try {
|
|
1155
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
1156
|
+
} catch {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
for (const entry of entries) {
|
|
1160
|
+
if (entry.isFile() && (entry.name === ".env" || entry.name.startsWith(".env."))) {
|
|
1161
|
+
addFile(join5(dir, entry.name));
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
const addDirectoryFiles = (rootDir) => {
|
|
1166
|
+
const stack = [rootDir];
|
|
1167
|
+
while (stack.length > 0 && files.length < MAX_SCANNED_FILES && visitedDirectories < MAX_SCAN_DIRECTORIES) {
|
|
1168
|
+
const dir = stack.pop();
|
|
1169
|
+
if (!dir || !isInsideRepo(dir))
|
|
1170
|
+
continue;
|
|
1171
|
+
visitedDirectories += 1;
|
|
1172
|
+
let entries;
|
|
1173
|
+
try {
|
|
1174
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
1175
|
+
} catch {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
for (const entry of entries) {
|
|
1179
|
+
if (files.length >= MAX_SCANNED_FILES)
|
|
1180
|
+
break;
|
|
1181
|
+
const path = join5(dir, entry.name);
|
|
1182
|
+
if (entry.isDirectory())
|
|
1183
|
+
stack.push(path);
|
|
1184
|
+
else if (entry.isFile())
|
|
1185
|
+
addFile(path);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
addEnvFiles(resolvedCwd);
|
|
1190
|
+
const candidates = uniquePaths(paths).slice(0, MAX_SCANNED_FILES);
|
|
1191
|
+
for (const relPath of candidates) {
|
|
1192
|
+
if (files.length >= MAX_SCANNED_FILES)
|
|
1193
|
+
break;
|
|
1194
|
+
const absPath = resolve2(cwd, relPath);
|
|
1195
|
+
if (!isInsideRepo(absPath))
|
|
1196
|
+
continue;
|
|
1197
|
+
addEnvFiles(dirname3(absPath));
|
|
1198
|
+
if (!existsSync5(absPath))
|
|
1199
|
+
continue;
|
|
1200
|
+
let stat;
|
|
1201
|
+
try {
|
|
1202
|
+
stat = statSync2(absPath);
|
|
1203
|
+
} catch {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (stat.isFile())
|
|
1207
|
+
addFile(absPath);
|
|
1208
|
+
else if (stat.isDirectory() && relPath.replace(/\\/g, "/").endsWith("/")) {
|
|
1209
|
+
addDirectoryFiles(absPath);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return files;
|
|
1213
|
+
}
|
|
1214
|
+
async function invokeAgent(ctx, prompt) {
|
|
1215
|
+
const env = { ...process.env };
|
|
1216
|
+
if (ctx.apiKey)
|
|
1217
|
+
env.MODA_API_KEY = ctx.apiKey;
|
|
1218
|
+
if (ctx.ingestUrl)
|
|
1219
|
+
env.MODA_INGEST_URL = ctx.ingestUrl;
|
|
1220
|
+
return ctx.runAgent({
|
|
1221
|
+
adapter: ctx.adapter,
|
|
1222
|
+
prompt,
|
|
1223
|
+
rootDir: ctx.cwd,
|
|
1224
|
+
mode: "write",
|
|
1225
|
+
env,
|
|
1226
|
+
stream: ctx.human || Boolean(ctx.onEvent),
|
|
1227
|
+
progressToStderr: false,
|
|
1228
|
+
onProgress: (message) => ctx.emit(message),
|
|
1229
|
+
onAgentEvent: (event) => {
|
|
1230
|
+
ctx.onEvent?.(event);
|
|
1231
|
+
const card = describeCodingAgentEvent(event);
|
|
1232
|
+
if (card)
|
|
1233
|
+
ctx.emit(card);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
async function extractResultWithProtocolRetry(ctx, run, protocolRetryUsed, onProtocolRetry) {
|
|
1238
|
+
if (run.failedToStart) {
|
|
1239
|
+
return { agentFailed: { status: "agent_failed", detail: "CLI failed to start" } };
|
|
1240
|
+
}
|
|
1241
|
+
if (run.timedOut) {
|
|
1242
|
+
return { agentFailed: { status: "agent_failed", detail: "timed out" } };
|
|
1243
|
+
}
|
|
1244
|
+
let extraction = extractSdkAgentResult({ cwd: ctx.cwd, stdout: run.stdout, stderr: run.stderr });
|
|
1245
|
+
if ("error" in extraction && !protocolRetryUsed) {
|
|
1246
|
+
onProtocolRetry();
|
|
1247
|
+
ctx.emit("Agent did not emit a valid result; requesting protocol retry…");
|
|
1248
|
+
const retryRun = await invokeAgent(ctx, renderProtocolRetryPrompt(ctx.catalog.relativeDir));
|
|
1249
|
+
if (retryRun.failedToStart) {
|
|
1250
|
+
return { agentFailed: { status: "agent_failed", detail: "CLI failed to start" } };
|
|
1251
|
+
}
|
|
1252
|
+
if (retryRun.timedOut) {
|
|
1253
|
+
return { agentFailed: { status: "agent_failed", detail: "timed out" } };
|
|
1254
|
+
}
|
|
1255
|
+
extraction = extractSdkAgentResult({
|
|
1256
|
+
cwd: ctx.cwd,
|
|
1257
|
+
stdout: retryRun.stdout,
|
|
1258
|
+
stderr: retryRun.stderr
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if ("error" in extraction) {
|
|
1262
|
+
const tail = redactOutputTail(`${run.stdout}
|
|
1263
|
+
${run.stderr}`, ctx.apiKey);
|
|
1264
|
+
const exitNote = run.status !== 0 ? ` exited with status ${run.status ?? "unknown"}.` : ".";
|
|
1265
|
+
return {
|
|
1266
|
+
agentFailed: {
|
|
1267
|
+
status: "agent_failed",
|
|
1268
|
+
detail: `SDK integration result protocol violation${exitNote}${tail ? `
|
|
1269
|
+
${tail}` : ""}`
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
return { result: extraction.result };
|
|
1274
|
+
}
|
|
1275
|
+
function renderProtocolRetryPrompt(catalogDir) {
|
|
1276
|
+
return [
|
|
1277
|
+
"Your previous run did not emit a valid SDK integration result.",
|
|
1278
|
+
"Write `.moda/sdk-integration-result.json` AND print the same JSON between",
|
|
1279
|
+
`${SDK_RESULT_JSON_BEGIN} and ${SDK_RESULT_JSON_END} as your last output.`,
|
|
1280
|
+
`Do not modify anything under ${catalogDir}.`,
|
|
1281
|
+
"",
|
|
1282
|
+
renderResultProtocolSection()
|
|
1283
|
+
].join(`
|
|
1284
|
+
`);
|
|
1285
|
+
}
|
|
1286
|
+
function removeAgentResultFile(cwd) {
|
|
1287
|
+
const path = join5(cwd, SDK_RESULT_FILE);
|
|
1288
|
+
try {
|
|
1289
|
+
if (existsSync5(path))
|
|
1290
|
+
rmSync2(path);
|
|
1291
|
+
} catch {}
|
|
1292
|
+
}
|
|
1293
|
+
async function mapAgentResultToOutcome(ctx, result) {
|
|
1294
|
+
const agentDetail = (detail) => redactSecretText(detail, ctx.apiKey);
|
|
1295
|
+
switch (result.status) {
|
|
1296
|
+
case "needs_human_input":
|
|
1297
|
+
return {
|
|
1298
|
+
status: "needs_human_input",
|
|
1299
|
+
detail: agentDetail(result.failure?.detail ?? result.summary)
|
|
1300
|
+
};
|
|
1301
|
+
case "nothing_to_integrate": {
|
|
1302
|
+
if (!result.smoke) {
|
|
1303
|
+
return {
|
|
1304
|
+
status: "nothing_to_integrate",
|
|
1305
|
+
detail: "agent reports Moda is already integrated — not independently verified"
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
const verification = await verifyAgentSmoke(ctx);
|
|
1309
|
+
if (verification.observed) {
|
|
1310
|
+
return {
|
|
1311
|
+
status: "nothing_to_integrate",
|
|
1312
|
+
detail: `existing integration verified — trace reached Moda (${verification.llm_spans} llm span(s))`,
|
|
1313
|
+
verification
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
status: "nothing_to_integrate",
|
|
1318
|
+
detail: "agent reports Moda is already integrated — verification trace was not observed",
|
|
1319
|
+
verification
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
case "repository_checks_failed":
|
|
1323
|
+
return {
|
|
1324
|
+
status: "repository_checks_failed",
|
|
1325
|
+
detail: agentDetail(result.failure?.detail ?? result.summary),
|
|
1326
|
+
repairable: "repository_checks_failed"
|
|
1327
|
+
};
|
|
1328
|
+
case "smoke_execution_failed":
|
|
1329
|
+
return {
|
|
1330
|
+
status: "smoke_execution_failed",
|
|
1331
|
+
detail: agentDetail(result.failure?.detail ?? result.summary),
|
|
1332
|
+
repairable: "smoke_execution_failed"
|
|
1333
|
+
};
|
|
1334
|
+
case "failed":
|
|
1335
|
+
return {
|
|
1336
|
+
status: "agent_failed",
|
|
1337
|
+
detail: agentDetail(result.failure?.detail ?? result.summary),
|
|
1338
|
+
repairable: "failed"
|
|
1339
|
+
};
|
|
1340
|
+
case "integrated": {
|
|
1341
|
+
const verification = await verifyAgentSmoke(ctx);
|
|
1342
|
+
if (verification.observed) {
|
|
1343
|
+
let detail = `trace reached Moda (${verification.llm_spans} llm span(s))`;
|
|
1344
|
+
if (verification.duplicate_count > 0) {
|
|
1345
|
+
detail += `; warning: duplicate_count=${verification.duplicate_count}`;
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
status: "integrated_verified",
|
|
1349
|
+
detail,
|
|
1350
|
+
verification
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
status: "telemetry_not_observed",
|
|
1355
|
+
detail: "integration applied but the verification trace was not observed",
|
|
1356
|
+
verification,
|
|
1357
|
+
repairable: "telemetry_not_observed"
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
default:
|
|
1361
|
+
return { status: "agent_failed", detail: agentDetail(result.summary) };
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
async function verifyAgentSmoke(ctx) {
|
|
1365
|
+
ctx.onPhase?.("verifying", "Verifying trace in Moda…");
|
|
1366
|
+
ctx.emit("Verifying trace in Moda…");
|
|
1367
|
+
const evidence = await ctx.verify({
|
|
1368
|
+
verificationId: ctx.verificationId,
|
|
1369
|
+
timeoutMs: ctx.verifyTimeoutMs,
|
|
1370
|
+
fetchAudit: ctx.fetchAudit
|
|
1371
|
+
});
|
|
1372
|
+
return evidence.last_error !== undefined ? { ...evidence, last_error: redactSecretText(evidence.last_error, ctx.apiKey) } : evidence;
|
|
1373
|
+
}
|
|
1374
|
+
function installUsedSkills(result, catalog, cwd, emit) {
|
|
1375
|
+
const validIds = new Set(catalog.index.skills.map((skill) => skill.id));
|
|
1376
|
+
const installed = [];
|
|
1377
|
+
const warnings = [];
|
|
1378
|
+
for (const skillId of result.skills_used) {
|
|
1379
|
+
if (!validIds.has(skillId)) {
|
|
1380
|
+
warnings.push(`unknown skill id ${skillId}`);
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
installIntegrationSkill(skillId, cwd);
|
|
1385
|
+
installed.push(skillId);
|
|
1386
|
+
emit(`Installed integration skill ${skillId} to .claude/skills/${skillId}/`);
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1389
|
+
warnings.push(`could not install ${skillId}: ${message}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (warnings.length > 0) {
|
|
1393
|
+
emit(warnings.join("; "));
|
|
1394
|
+
}
|
|
1395
|
+
return installed;
|
|
1396
|
+
}
|
|
1397
|
+
function composeFinalDetail(status, detail, verification, apiKey, guardsDegraded) {
|
|
1398
|
+
const baseDetail = detail ?? (status === "integrated_verified" && verification ? `verified — trace reached Moda (${verification.llm_spans} llm span(s))` : undefined);
|
|
1399
|
+
const combined = guardsDegraded ? [baseDetail, DEGRADED_GUARDS_NOTE].filter(Boolean).join("; ") : baseDetail;
|
|
1400
|
+
return combined ? redactSecretText(combined, apiKey) : undefined;
|
|
1401
|
+
}
|
|
1402
|
+
function writeFinalOutcomeArtifact(cwd, payload) {
|
|
1403
|
+
const path = join5(cwd, SDK_RESULT_FILE);
|
|
1404
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
1405
|
+
writeFileSync3(path, `${JSON.stringify({
|
|
1406
|
+
schema_version: OUTCOME_SCHEMA_VERSION,
|
|
1407
|
+
status: payload.status,
|
|
1408
|
+
verification_id: payload.verification_id,
|
|
1409
|
+
verification: payload.verification,
|
|
1410
|
+
agent_result: payload.agent_result,
|
|
1411
|
+
repair_attempts: payload.repair_attempts,
|
|
1412
|
+
...payload.security_findings ? { security_findings: payload.security_findings } : {},
|
|
1413
|
+
finished_at: new Date().toISOString()
|
|
1414
|
+
}, null, 2)}
|
|
1415
|
+
`);
|
|
1416
|
+
}
|
|
1417
|
+
function redactSecretText(text, apiKey) {
|
|
1418
|
+
let redacted = text;
|
|
1419
|
+
for (const secret of [apiKey, process.env.MODA_API_KEY]) {
|
|
1420
|
+
if (secret)
|
|
1421
|
+
redacted = redacted.split(secret).join("[REDACTED]");
|
|
1422
|
+
}
|
|
1423
|
+
SECRET_KEY_PATTERN.lastIndex = 0;
|
|
1424
|
+
return redacted.replace(SECRET_KEY_PATTERN, "[REDACTED]");
|
|
1425
|
+
}
|
|
1426
|
+
function sanitizeAgentResult(result, apiKey) {
|
|
1427
|
+
return JSON.parse(redactSecretText(JSON.stringify(result), apiKey));
|
|
1428
|
+
}
|
|
1429
|
+
function redactOutputTail(output, apiKey) {
|
|
1430
|
+
return redactSecretText(output, apiKey).trim().slice(-800);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/init/progress-board.ts
|
|
1434
|
+
var SPINNER_FRAMES = ["◐", "◓", "◑", "◒"];
|
|
1435
|
+
var GLYPHS = {
|
|
1436
|
+
done: "✓",
|
|
1437
|
+
warning: "!",
|
|
1438
|
+
failed: "✗"
|
|
1439
|
+
};
|
|
1440
|
+
var GLYPH_COLORS = {
|
|
1441
|
+
done: 32,
|
|
1442
|
+
warning: 33,
|
|
1443
|
+
failed: 31
|
|
1444
|
+
};
|
|
1445
|
+
var DETAIL_MAX = 100;
|
|
1446
|
+
var RENDER_INTERVAL_MS = 150;
|
|
1447
|
+
var NON_TTY_LANE_INTERVAL_MS = 1e4;
|
|
1448
|
+
function renderProgressBoardFrame(lanes, options = {}) {
|
|
1449
|
+
const now = options.now ?? Date.now();
|
|
1450
|
+
const color = options.color ?? false;
|
|
1451
|
+
const width = Math.max(48, Math.min(options.width ?? 120, 200));
|
|
1452
|
+
const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
1453
|
+
const titleWidth = Math.min(32, lanes.reduce((max, lane) => Math.max(max, lane.title.length), 0));
|
|
1454
|
+
return lanes.map((lane) => {
|
|
1455
|
+
const title = lane.title.padEnd(titleWidth);
|
|
1456
|
+
if (lane.status === "running") {
|
|
1457
|
+
const elapsed = formatDuration(Math.max(0, Math.round((now - lane.startedAt) / 1000)));
|
|
1458
|
+
const detail = lane.detail ? ` ${oneLine(lane.detail)}` : "";
|
|
1459
|
+
const expectation = lane.expectation ? ` ${paint(90, `(${lane.expectation})`)}` : "";
|
|
1460
|
+
const glyph2 = paint(36, options.spinnerFrame ?? SPINNER_FRAMES[0]);
|
|
1461
|
+
return truncateVisible(` ${glyph2} ${title} ${paint(90, elapsed.padStart(6))}${detail}${expectation}`, width);
|
|
1462
|
+
}
|
|
1463
|
+
const glyph = paint(GLYPH_COLORS[lane.status], GLYPHS[lane.status]);
|
|
1464
|
+
const note = lane.detail ? oneLine(lane.detail) : formatDuration(Math.max(0, Math.round(((lane.finishedAt ?? now) - lane.startedAt) / 1000)));
|
|
1465
|
+
return truncateVisible(` ${glyph} ${title} ${note}`, width);
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
class InitProgressBoard {
|
|
1470
|
+
stream;
|
|
1471
|
+
tty;
|
|
1472
|
+
color;
|
|
1473
|
+
now;
|
|
1474
|
+
renderIntervalMs;
|
|
1475
|
+
fallbackIntervalMs;
|
|
1476
|
+
laneList = [];
|
|
1477
|
+
lanesById = new Map;
|
|
1478
|
+
lastFallbackPrint = new Map;
|
|
1479
|
+
timer;
|
|
1480
|
+
renderedLines = 0;
|
|
1481
|
+
spinnerIndex = 0;
|
|
1482
|
+
stopped = false;
|
|
1483
|
+
constructor(options = {}) {
|
|
1484
|
+
this.stream = options.stream ?? process.stderr;
|
|
1485
|
+
const dumbTerm = process.env.TERM === "dumb";
|
|
1486
|
+
this.tty = options.isTTY ?? (this.stream.isTTY === true && !dumbTerm);
|
|
1487
|
+
this.color = options.color ?? (this.tty && process.env.NO_COLOR === undefined);
|
|
1488
|
+
this.now = options.now ?? Date.now;
|
|
1489
|
+
this.renderIntervalMs = options.renderIntervalMs ?? RENDER_INTERVAL_MS;
|
|
1490
|
+
this.fallbackIntervalMs = options.fallbackIntervalMs ?? NON_TTY_LANE_INTERVAL_MS;
|
|
1491
|
+
}
|
|
1492
|
+
get lanes() {
|
|
1493
|
+
return this.laneList;
|
|
1494
|
+
}
|
|
1495
|
+
startLane(id, title, options = {}) {
|
|
1496
|
+
if (this.stopped || this.lanesById.has(id))
|
|
1497
|
+
return;
|
|
1498
|
+
const lane = {
|
|
1499
|
+
id,
|
|
1500
|
+
title,
|
|
1501
|
+
status: "running",
|
|
1502
|
+
detail: options.detail,
|
|
1503
|
+
expectation: options.expectation,
|
|
1504
|
+
startedAt: this.now()
|
|
1505
|
+
};
|
|
1506
|
+
this.laneList.push(lane);
|
|
1507
|
+
this.lanesById.set(id, lane);
|
|
1508
|
+
if (this.tty) {
|
|
1509
|
+
this.ensureTimer();
|
|
1510
|
+
this.render();
|
|
1511
|
+
} else {
|
|
1512
|
+
this.lastFallbackPrint.set(id, this.now());
|
|
1513
|
+
this.writeLine(` ${title}: ${lane.detail ? oneLine(lane.detail) : "started"}${lane.expectation ? ` (${lane.expectation})` : ""}`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
update(id, detail) {
|
|
1517
|
+
const lane = this.lanesById.get(id);
|
|
1518
|
+
if (!lane || this.stopped || lane.status !== "running")
|
|
1519
|
+
return;
|
|
1520
|
+
lane.detail = detail;
|
|
1521
|
+
if (this.tty)
|
|
1522
|
+
return;
|
|
1523
|
+
const last = this.lastFallbackPrint.get(id) ?? 0;
|
|
1524
|
+
if (this.now() - last < this.fallbackIntervalMs)
|
|
1525
|
+
return;
|
|
1526
|
+
this.lastFallbackPrint.set(id, this.now());
|
|
1527
|
+
const elapsed = formatDuration(Math.max(0, Math.round((this.now() - lane.startedAt) / 1000)));
|
|
1528
|
+
this.writeLine(` ${lane.title}: ${oneLine(detail)} (${elapsed} elapsed)`);
|
|
1529
|
+
}
|
|
1530
|
+
complete(id, note) {
|
|
1531
|
+
this.finish(id, "done", note);
|
|
1532
|
+
}
|
|
1533
|
+
warn(id, note) {
|
|
1534
|
+
this.finish(id, "warning", note);
|
|
1535
|
+
}
|
|
1536
|
+
fail(id, note) {
|
|
1537
|
+
this.finish(id, "failed", note);
|
|
1538
|
+
}
|
|
1539
|
+
stop() {
|
|
1540
|
+
if (this.stopped)
|
|
1541
|
+
return;
|
|
1542
|
+
this.stopped = true;
|
|
1543
|
+
if (this.timer) {
|
|
1544
|
+
clearInterval(this.timer);
|
|
1545
|
+
this.timer = undefined;
|
|
1546
|
+
}
|
|
1547
|
+
if (this.tty && this.laneList.length > 0)
|
|
1548
|
+
this.render();
|
|
1549
|
+
}
|
|
1550
|
+
finish(id, status, note) {
|
|
1551
|
+
const lane = this.lanesById.get(id);
|
|
1552
|
+
if (!lane || this.stopped)
|
|
1553
|
+
return;
|
|
1554
|
+
if (lane.status !== "running" && lane.status === status && lane.detail === note)
|
|
1555
|
+
return;
|
|
1556
|
+
lane.status = status;
|
|
1557
|
+
lane.detail = note;
|
|
1558
|
+
lane.finishedAt ??= this.now();
|
|
1559
|
+
if (this.tty) {
|
|
1560
|
+
this.render();
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const label = status === "done" ? "done" : status === "warning" ? "warning" : "failed";
|
|
1564
|
+
this.writeLine(` ${lane.title}: ${label}${note ? ` (${oneLine(note)})` : ""}`);
|
|
1565
|
+
}
|
|
1566
|
+
ensureTimer() {
|
|
1567
|
+
if (this.timer || !this.tty)
|
|
1568
|
+
return;
|
|
1569
|
+
this.timer = setInterval(() => this.render(), this.renderIntervalMs);
|
|
1570
|
+
this.timer.unref?.();
|
|
1571
|
+
}
|
|
1572
|
+
render() {
|
|
1573
|
+
if (!this.tty || this.laneList.length === 0)
|
|
1574
|
+
return;
|
|
1575
|
+
const lines = renderProgressBoardFrame(this.laneList, {
|
|
1576
|
+
now: this.now(),
|
|
1577
|
+
spinnerFrame: SPINNER_FRAMES[this.spinnerIndex++ % SPINNER_FRAMES.length],
|
|
1578
|
+
color: this.color,
|
|
1579
|
+
width: this.stream.columns ?? 120
|
|
1580
|
+
});
|
|
1581
|
+
const up = this.renderedLines > 0 ? `\x1B[${this.renderedLines}A` : "";
|
|
1582
|
+
this.stream.write(`${up}${lines.map((line) => `\x1B[2K${line}`).join(`
|
|
1583
|
+
`)}
|
|
1584
|
+
`);
|
|
1585
|
+
this.renderedLines = lines.length;
|
|
1586
|
+
}
|
|
1587
|
+
writeLine(line) {
|
|
1588
|
+
this.stream.write(`${line}
|
|
1589
|
+
`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function oneLine(text) {
|
|
1593
|
+
const collapsed = stripAnsi(text).replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
1594
|
+
return collapsed.length > DETAIL_MAX ? `${collapsed.slice(0, DETAIL_MAX - 1)}…` : collapsed;
|
|
1595
|
+
}
|
|
1596
|
+
function formatDuration(seconds) {
|
|
1597
|
+
if (seconds < 60)
|
|
1598
|
+
return `${seconds}s`;
|
|
1599
|
+
const minutes = Math.floor(seconds / 60);
|
|
1600
|
+
const remaining = seconds % 60;
|
|
1601
|
+
return `${minutes}m ${remaining}s`;
|
|
1602
|
+
}
|
|
1603
|
+
function truncateVisible(value, width) {
|
|
1604
|
+
if (visibleLength(value) <= width)
|
|
1605
|
+
return value;
|
|
1606
|
+
const stripped = stripAnsi(value);
|
|
1607
|
+
return `${stripped.slice(0, Math.max(0, width - 3))}...`;
|
|
1608
|
+
}
|
|
1609
|
+
function visibleLength(value) {
|
|
1610
|
+
return stripAnsi(value).length;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/init/index.ts
|
|
1614
|
+
var HARNESS_PULL_RECOVERY = "moda harness pull --sync";
|
|
1615
|
+
var LANE_EVENT_THROTTLE_MS = 2000;
|
|
1616
|
+
async function runInit(flags, options = {}) {
|
|
1617
|
+
const explicitYes = flags.yes === true || flags.yes === "true" || flags.y === true || flags.y === "true";
|
|
1618
|
+
const json = flags.json === true || flags.json === "true";
|
|
1619
|
+
const agent = flags.agent === true || flags.agent === "true";
|
|
1620
|
+
const human = !json && !agent;
|
|
1621
|
+
const yes = explicitYes || json || agent;
|
|
1622
|
+
const phases = [];
|
|
1623
|
+
const log = (message) => {
|
|
1624
|
+
if (human)
|
|
1625
|
+
console.log(message);
|
|
1626
|
+
};
|
|
1627
|
+
const warn = (message) => {
|
|
1628
|
+
if (human)
|
|
1629
|
+
console.error(message);
|
|
1630
|
+
};
|
|
1631
|
+
const emit = (phase, message, status) => {
|
|
1632
|
+
options.onEvent?.({ phase, message, status });
|
|
1633
|
+
};
|
|
1634
|
+
const laneEventAt = new Map;
|
|
1635
|
+
const emitLaneProgress = (phase, laneId, message) => {
|
|
1636
|
+
if (!options.onEvent || human)
|
|
1637
|
+
return;
|
|
1638
|
+
const now = Date.now();
|
|
1639
|
+
if (now - (laneEventAt.get(laneId) ?? 0) < LANE_EVENT_THROTTLE_MS)
|
|
1640
|
+
return;
|
|
1641
|
+
laneEventAt.set(laneId, now);
|
|
1642
|
+
emit(phase, message);
|
|
1643
|
+
};
|
|
1644
|
+
log(renderInitBanner());
|
|
1645
|
+
emit("welcome", "Onboarding this project as a Moda agent harness.", "completed");
|
|
1646
|
+
phases.push({ id: "welcome", title: "Welcome", status: "completed" });
|
|
1647
|
+
const profile = stringFlag(flags.profile);
|
|
1648
|
+
log(renderInitStep(1, 5, "Sign in", "No environment variable files are written. File changes are listed before completion."));
|
|
1649
|
+
let token = loadToken({ profile });
|
|
1650
|
+
if (!token) {
|
|
1651
|
+
if (json || agent) {
|
|
1652
|
+
throw new CliAuthError("Browser authentication is required before non-interactive init can continue.", {
|
|
1653
|
+
reason: "no_stored_credentials",
|
|
1654
|
+
login_command: ["moda", "auth", "login"],
|
|
1655
|
+
resume_command: ["moda", "init", ...resumeFlagsForInit(flags)]
|
|
1656
|
+
}, "Run `moda auth login`, then re-run `moda init`.");
|
|
1657
|
+
}
|
|
1658
|
+
let action = "login";
|
|
1659
|
+
if (!yes) {
|
|
1660
|
+
action = await select({
|
|
1661
|
+
message: "Get started",
|
|
1662
|
+
choices: [
|
|
1663
|
+
{ name: "Sign up", value: "signup" },
|
|
1664
|
+
{ name: "Login", value: "login" }
|
|
1665
|
+
]
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
log(renderInitNote(`Opening browser to ${action === "signup" ? "sign up" : "log in"}...`, "warning"));
|
|
1669
|
+
emit("auth", `Opening browser to ${action}.`);
|
|
1670
|
+
token = await login(action, { profile });
|
|
1671
|
+
log(renderInitNote("Authentication successful"));
|
|
1672
|
+
} else {
|
|
1673
|
+
log(renderInitNote("Already authenticated (cached token)"));
|
|
1674
|
+
}
|
|
1675
|
+
emit("auth", "Authentication complete.", "completed");
|
|
1676
|
+
phases.push({ id: "auth", title: "Sign in", status: "completed" });
|
|
1677
|
+
log(renderInitStep(2, 5, "Workspace"));
|
|
1678
|
+
emit("workspace", "Selecting workspace and creating API key.");
|
|
1679
|
+
const tenantIdFlag = stringFlag(flagValue(flags, "tenant-id", "tenant_id"));
|
|
1680
|
+
const tenantIdEnv = stringFlag(process.env.MODA_TENANT_ID);
|
|
1681
|
+
const { apiKey, tenantId: selectedTenantId, tenantName } = await selectTenantAndCreateKey({
|
|
1682
|
+
tenantId: tenantIdFlag ?? tenantIdEnv,
|
|
1683
|
+
tenantIdSource: tenantIdFlag ? "flag" : tenantIdEnv ? "env" : undefined,
|
|
1684
|
+
nonInteractive: json || agent,
|
|
1685
|
+
silent: !human,
|
|
1686
|
+
resumeCommand: ["moda", "init", ...resumeFlagsForInit(flags)],
|
|
1687
|
+
profile
|
|
1688
|
+
});
|
|
1689
|
+
log(renderInitNote(`Active workspace: ${tenantName}`));
|
|
1690
|
+
emit("workspace", `Active workspace: ${tenantName}`, "completed");
|
|
1691
|
+
phases.push({ id: "workspace", title: "Workspace", status: "completed", details: tenantName });
|
|
1692
|
+
log(renderInitStep(3, 5, "Plan", "One confirmation covers everything below; setup then runs unattended."));
|
|
1693
|
+
let remote = resolveHarnessRemote(flags);
|
|
1694
|
+
const remotePinned = hasExplicitRemoteChoice(flags);
|
|
1695
|
+
const sdkFlag = resolveSdkIntegrateFlag(flags);
|
|
1696
|
+
const ciCdFlag = resolveCiCdFlag(flags);
|
|
1697
|
+
const autosyncFlag = resolveAutosyncFlag(flags);
|
|
1698
|
+
const harnessRescanFlag = resolveHarnessRescanFlag(flags);
|
|
1699
|
+
const harnessRescanPaths = parseHarnessRescanPaths(flagValue(flags, "harness-rescan-paths", "harness_rescan_paths"));
|
|
1700
|
+
const harnessRescanChoice = harnessRescanFlag ?? (harnessRescanPaths !== undefined ? true : undefined);
|
|
1701
|
+
const codingAgentAvailable = detectAvailableCodingAgents().length > 0;
|
|
1702
|
+
let integrateSdk = sdkFlag !== undefined ? sdkFlag : yes ? false : codingAgentAvailable;
|
|
1703
|
+
let configureCiCd = ciCdFlag ?? false;
|
|
1704
|
+
let configureAutosync = autosyncFlag ?? false;
|
|
1705
|
+
let configureHarnessRescan = harnessRescanChoice ?? false;
|
|
1706
|
+
let setupPlan = [];
|
|
1707
|
+
const renderPlan = () => {
|
|
1708
|
+
setupPlan = buildSetupPlan({
|
|
1709
|
+
installHooks: false,
|
|
1710
|
+
integrateSdk,
|
|
1711
|
+
configureCiCd,
|
|
1712
|
+
configureAutosync,
|
|
1713
|
+
configureHarnessRescan,
|
|
1714
|
+
remote
|
|
1715
|
+
});
|
|
1716
|
+
log(renderInitSetupPlan(setupPlan, { snapshotConsent: remote }));
|
|
1717
|
+
};
|
|
1718
|
+
renderPlan();
|
|
1719
|
+
if (!yes) {
|
|
1720
|
+
const proceed = await confirm({ message: "Proceed with this plan?", default: true });
|
|
1721
|
+
if (!proceed) {
|
|
1722
|
+
const choices = [];
|
|
1723
|
+
if (sdkFlag === undefined) {
|
|
1724
|
+
choices.push({
|
|
1725
|
+
name: "SDK integration via your coding agent (installs packages, edits source)",
|
|
1726
|
+
value: "sdk",
|
|
1727
|
+
checked: integrateSdk
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
if (!remotePinned) {
|
|
1731
|
+
choices.push({
|
|
1732
|
+
name: "Harness analysis on Moda's servers (uncheck to run a local coding agent analyst)",
|
|
1733
|
+
value: "remote",
|
|
1734
|
+
checked: remote
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
if (ciCdFlag === undefined) {
|
|
1738
|
+
choices.push({
|
|
1739
|
+
name: "CI/CD validation workflow (.github/workflows/moda-validation.yml)",
|
|
1740
|
+
value: "ci-cd",
|
|
1741
|
+
checked: configureCiCd
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
if (autosyncFlag === undefined) {
|
|
1745
|
+
choices.push({
|
|
1746
|
+
name: "Autosync prompts and skills on push to main (.github/workflows/moda-autosync.yml)",
|
|
1747
|
+
value: "autosync",
|
|
1748
|
+
checked: configureAutosync
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
if (harnessRescanChoice === undefined) {
|
|
1752
|
+
choices.push({
|
|
1753
|
+
name: "Re-scan the harness on push to main (.github/workflows/moda-harness-rescan.yml)",
|
|
1754
|
+
value: "harness-rescan",
|
|
1755
|
+
checked: configureHarnessRescan
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
if (choices.length > 0) {
|
|
1759
|
+
const selected = await checkbox({
|
|
1760
|
+
message: "Choose what runs (space toggles, enter confirms)",
|
|
1761
|
+
choices
|
|
1762
|
+
});
|
|
1763
|
+
if (sdkFlag === undefined)
|
|
1764
|
+
integrateSdk = selected.includes("sdk");
|
|
1765
|
+
if (!remotePinned)
|
|
1766
|
+
remote = selected.includes("remote");
|
|
1767
|
+
if (ciCdFlag === undefined)
|
|
1768
|
+
configureCiCd = selected.includes("ci-cd");
|
|
1769
|
+
if (autosyncFlag === undefined)
|
|
1770
|
+
configureAutosync = selected.includes("autosync");
|
|
1771
|
+
if (harnessRescanChoice === undefined)
|
|
1772
|
+
configureHarnessRescan = selected.includes("harness-rescan");
|
|
1773
|
+
renderPlan();
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
if (json || agent) {
|
|
1778
|
+
const skipped = setupPlan.filter((action) => (action.category === "sdk" || action.category === "ci-cd" || action.category === "autosync" || action.category === "harness-rescan") && action.approved === false).map((action) => `${action.category}: ${action.command ?? "explicit flag required"}`);
|
|
1779
|
+
if (skipped.length > 0) {
|
|
1780
|
+
emit("plan", `Non-interactive init skipped optional actions (${skipped.join("; ")}).`, "warning");
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
emit("plan", "Setup plan ready.", "completed");
|
|
1784
|
+
phases.push({ id: "plan", title: "Plan", status: "completed" });
|
|
1785
|
+
log(renderInitStep(4, 5, "Set up", remote ? "Harness analysis runs on Moda while local setup continues. No further input is needed." : "Runs unattended. No further input is needed."));
|
|
1786
|
+
const rootDir = process.cwd();
|
|
1787
|
+
const allowLocalFallback = !isTruthyFlag(flagValue(flags, "remote"));
|
|
1788
|
+
const nestedHarnessFlags = (extra) => commandFlags({
|
|
1789
|
+
summary: true,
|
|
1790
|
+
yes: true,
|
|
1791
|
+
json: json || agent,
|
|
1792
|
+
agent,
|
|
1793
|
+
quiet: true,
|
|
1794
|
+
"no-telemetry": true,
|
|
1795
|
+
profile,
|
|
1796
|
+
...extra
|
|
1797
|
+
});
|
|
1798
|
+
const runAnalyze = (useRemote) => runHarnessCommand(createCommandContext({
|
|
1799
|
+
command: "harness",
|
|
1800
|
+
positional: "analyze",
|
|
1801
|
+
positionals: ["analyze"],
|
|
1802
|
+
flags: nestedHarnessFlags({
|
|
1803
|
+
analyst: stringFlag(flags.analyst) ?? "auto",
|
|
1804
|
+
remote: useRemote ? true : undefined,
|
|
1805
|
+
"no-remote": useRemote ? undefined : true,
|
|
1806
|
+
progress: true
|
|
1807
|
+
})
|
|
1808
|
+
}));
|
|
1809
|
+
let board;
|
|
1810
|
+
const startBoard = () => {
|
|
1811
|
+
if (!human)
|
|
1812
|
+
return;
|
|
1813
|
+
board ??= new InitProgressBoard;
|
|
1814
|
+
return board;
|
|
1815
|
+
};
|
|
1816
|
+
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`.");
|
|
1817
|
+
let harnessPhase = { id: "harness", title: "Harness discovery", status: "completed" };
|
|
1818
|
+
let harnessDeferred = false;
|
|
1819
|
+
const approveAndSyncHarness = async () => {
|
|
1820
|
+
const graphSignals = readHarnessQualitySignals(rootDir);
|
|
1821
|
+
const repoSignals = detectRepoLlmSignals(rootDir);
|
|
1822
|
+
const signals = graphSignals ? {
|
|
1823
|
+
runtimeAgents: graphSignals.runtimeAgents,
|
|
1824
|
+
frameworks: [...new Set([...graphSignals.frameworks, ...repoSignals.frameworks])],
|
|
1825
|
+
providers: [...new Set([...graphSignals.providers, ...repoSignals.providers])]
|
|
1826
|
+
} : undefined;
|
|
1827
|
+
if (signals && harnessLooksIncomplete(signals)) {
|
|
1828
|
+
const names = [...signals.frameworks, ...signals.providers].join(", ");
|
|
1829
|
+
const retryCommand = remote ? "moda harness analyze --remote --summary" : "moda harness analyze --analyst=claude|cursor|codex --summary";
|
|
1830
|
+
warn(renderInitNote(`Harness discovery found 0 runtime agents, but this repo has LLM framework signals (${names}). ` + "Discovery likely failed, so the result was NOT approved or synced.", "warning"));
|
|
1831
|
+
warn(` Retry with \`${retryCommand}\`, then \`moda harness approve --yes && moda harness sync --from-report\`.`);
|
|
1832
|
+
emit("harness", "Harness discovery found 0 agents despite LLM framework signals; sync withheld.", "warning");
|
|
1833
|
+
harnessPhase = {
|
|
1834
|
+
id: "harness",
|
|
1835
|
+
title: "Harness discovery",
|
|
1836
|
+
status: "warning",
|
|
1837
|
+
details: "0 agents detected; sync withheld"
|
|
1838
|
+
};
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
try {
|
|
1842
|
+
const identity = describeSyncIdentity({ profile, env: process.env });
|
|
1843
|
+
if (identity) {
|
|
1844
|
+
const tenantLabel = identity.tenantSource === "api-key" && identity.tenantId ? `${identity.tenantId}${identity.tenantId === selectedTenantId ? ` (${tenantName})` : ""}` : describeSyncTenantLabel(identity);
|
|
1845
|
+
log(renderInitNote(`Syncing as ${identity.credential} → tenant ${tenantLabel}`, "info"));
|
|
1846
|
+
}
|
|
1847
|
+
} catch {}
|
|
1848
|
+
try {
|
|
1849
|
+
await runHarnessCommand(createCommandContext({
|
|
1850
|
+
command: "harness",
|
|
1851
|
+
positional: "approve",
|
|
1852
|
+
positionals: ["approve"],
|
|
1853
|
+
flags: nestedHarnessFlags({})
|
|
1854
|
+
}));
|
|
1855
|
+
await runHarnessCommand(createCommandContext({
|
|
1856
|
+
command: "harness",
|
|
1857
|
+
positional: "sync",
|
|
1858
|
+
positionals: ["sync"],
|
|
1859
|
+
flags: nestedHarnessFlags({ "from-report": true })
|
|
1860
|
+
}));
|
|
1861
|
+
log(renderInitNote("Synced harness graph to Moda"));
|
|
1862
|
+
emit("harness", "Harness graph synced to Moda.", "completed");
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1865
|
+
warn(renderInitNote(`Harness sync skipped: ${message}`, "warning"));
|
|
1866
|
+
warn(" Run `moda harness approve --yes && moda harness sync --from-report` later to upload.");
|
|
1867
|
+
emit("harness", "Harness sync skipped.", "warning");
|
|
1868
|
+
harnessPhase = { id: "harness", title: "Harness discovery", status: "warning", details: "sync skipped" };
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
const finishLocalAnalyze = async () => {
|
|
1872
|
+
emit("harness", "Running Moda Analyst harness discovery.");
|
|
1873
|
+
const result = await runAnalyze(false);
|
|
1874
|
+
if (result.exitCode !== 0)
|
|
1875
|
+
throw validationFailure();
|
|
1876
|
+
log(renderInitNote("Wrote or verified .moda/harness-report.json, .moda/harness.json, and .moda/HARNESS.md"));
|
|
1877
|
+
if (human)
|
|
1878
|
+
printHarnessDetectionSummary();
|
|
1879
|
+
emit("harness", "Harness discovery complete.", "completed");
|
|
1880
|
+
await approveAndSyncHarness();
|
|
1881
|
+
};
|
|
1882
|
+
let pollPromise;
|
|
1883
|
+
let pollError;
|
|
1884
|
+
let remoteRunId;
|
|
1885
|
+
const onRemoteSigint = () => {
|
|
1886
|
+
board?.stop();
|
|
1887
|
+
process.stderr.write(`
|
|
1888
|
+
Harness analysis keeps running on Moda. Resume anytime with \`${HARNESS_PULL_RECOVERY}\`.
|
|
1889
|
+
`);
|
|
1890
|
+
process.exit(130);
|
|
1891
|
+
};
|
|
1892
|
+
if (remote) {
|
|
1893
|
+
emit("harness", "Running Moda-hosted harness discovery.");
|
|
1894
|
+
try {
|
|
1895
|
+
const harnessContext = createCommandContext({
|
|
1896
|
+
command: "harness",
|
|
1897
|
+
positional: "analyze",
|
|
1898
|
+
positionals: ["analyze"],
|
|
1899
|
+
flags: nestedHarnessFlags({
|
|
1900
|
+
analyst: stringFlag(flags.analyst) ?? "auto",
|
|
1901
|
+
remote: true
|
|
1902
|
+
})
|
|
1903
|
+
});
|
|
1904
|
+
const started = await startRemoteAnalyze({ context: harnessContext, rootDir });
|
|
1905
|
+
remoteRunId = started.runId;
|
|
1906
|
+
process.once("SIGINT", onRemoteSigint);
|
|
1907
|
+
emit("harness", `Moda accepted harness analysis run ${started.runId}.`);
|
|
1908
|
+
startBoard()?.startLane("harness", "Harness analysis on Moda", {
|
|
1909
|
+
detail: "waiting for the hosted analyst",
|
|
1910
|
+
expectation: "usually 2-4 min"
|
|
1911
|
+
});
|
|
1912
|
+
pollPromise = pollRemoteAnalyzeUntilDone({
|
|
1913
|
+
context: harnessContext,
|
|
1914
|
+
runId: started.runId,
|
|
1915
|
+
onProgress: ({ status, progress }) => {
|
|
1916
|
+
if (status === "completed" || status === "failed") {
|
|
1917
|
+
if (status === "completed") {
|
|
1918
|
+
board?.complete("harness", "analysis finished on Moda; finalizing after setup");
|
|
1919
|
+
} else {
|
|
1920
|
+
board?.warn("harness", "hosted run finished with issues; finalizing after setup");
|
|
1921
|
+
}
|
|
1922
|
+
emitLaneProgress("harness", "harness", `Hosted analysis ${status}; finalizing after setup.`);
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
const message = progress?.message ?? (typeof progress?.tool_calls === "number" ? `${progress.tool_calls} tool call(s) so far` : `analysis ${status}`);
|
|
1926
|
+
board?.update("harness", message);
|
|
1927
|
+
emitLaneProgress("harness", "harness", message);
|
|
1928
|
+
}
|
|
1929
|
+
}).catch((error) => {
|
|
1930
|
+
pollError = error;
|
|
1931
|
+
return;
|
|
1932
|
+
});
|
|
1933
|
+
} catch (error) {
|
|
1934
|
+
if (!allowLocalFallback)
|
|
1935
|
+
throw error;
|
|
1936
|
+
warn(renderInitNote(`Server-side harness discovery unavailable (${error instanceof Error ? error.message : String(error)}); falling back to local analyst.`, "warning"));
|
|
1937
|
+
emit("harness", "Server-side discovery failed; falling back to local analyst.", "warning");
|
|
1938
|
+
remote = false;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
if (!remote) {
|
|
1942
|
+
await finishLocalAnalyze();
|
|
1943
|
+
}
|
|
1944
|
+
emit("apply", "Applying approved setup changes.");
|
|
1945
|
+
const laneBoard = startBoard();
|
|
1946
|
+
const ingestCodingAgentTraces = false;
|
|
1947
|
+
const agents = detectAgents();
|
|
1948
|
+
laneBoard?.startLane("agent-skills", "Agent skills");
|
|
1949
|
+
if (agents.length > 0) {
|
|
1950
|
+
await installRules(agents, { yes: true, silent: true });
|
|
1951
|
+
laneBoard?.complete("agent-skills", `moda-cli skill installed for ${agents.join(", ")}`);
|
|
1952
|
+
} else {
|
|
1953
|
+
laneBoard?.complete("agent-skills", "no coding agents detected");
|
|
1954
|
+
}
|
|
1955
|
+
await initPrompts({ silent: true });
|
|
1956
|
+
laneBoard?.startLane("prompts", "Prompts", { detail: "Created or verified .moda/prompts.yml" });
|
|
1957
|
+
try {
|
|
1958
|
+
const promptSync = await runPromptSync({}, { profile });
|
|
1959
|
+
laneBoard?.complete("prompts", promptSync.synced.length > 0 ? `${promptSync.synced.length} prompt(s) synced` : "manifest ready (no prompt files to sync yet)");
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1962
|
+
laneBoard?.warn("prompts", `sync skipped: ${message}`);
|
|
1963
|
+
}
|
|
1964
|
+
laneBoard?.startLane("skills", "Skills");
|
|
1965
|
+
try {
|
|
1966
|
+
const skillSync = await runSkillSync({}, { profile });
|
|
1967
|
+
laneBoard?.complete("skills", skillSync.synced.length > 0 ? `${skillSync.synced.length} skill(s) synced` : "no local skills to sync yet");
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1970
|
+
laneBoard?.warn("skills", `sync skipped: ${message}`);
|
|
1971
|
+
}
|
|
1972
|
+
if (setupPlan.find((action) => action.category === "ci-cd")?.approved === true) {
|
|
1973
|
+
const workflowPath = writeCiCdWorkflow();
|
|
1974
|
+
laneBoard?.startLane("ci-cd", "CI/CD workflow");
|
|
1975
|
+
laneBoard?.complete("ci-cd", `wrote ${workflowPath}`);
|
|
1976
|
+
markApplied(setupPlan, "ci-cd");
|
|
1977
|
+
}
|
|
1978
|
+
if (setupPlan.find((action) => action.category === "autosync")?.approved === true) {
|
|
1979
|
+
const workflowPath = writeAutosyncWorkflow();
|
|
1980
|
+
laneBoard?.startLane("autosync", "Autosync workflow");
|
|
1981
|
+
laneBoard?.complete("autosync", `wrote ${workflowPath}`);
|
|
1982
|
+
markApplied(setupPlan, "autosync");
|
|
1983
|
+
}
|
|
1984
|
+
if (setupPlan.find((action) => action.category === "harness-rescan")?.approved === true) {
|
|
1985
|
+
const workflowPath = writeHarnessRescanWorkflow(process.cwd(), harnessRescanPaths);
|
|
1986
|
+
laneBoard?.startLane("harness-rescan", "Harness rescan workflow");
|
|
1987
|
+
laneBoard?.complete("harness-rescan", `wrote ${workflowPath}`);
|
|
1988
|
+
markApplied(setupPlan, "harness-rescan");
|
|
1989
|
+
}
|
|
1990
|
+
const sdkAction = setupPlan.find((action) => action.category === "sdk");
|
|
1991
|
+
let sdkIntegration;
|
|
1992
|
+
if (sdkAction?.approved === true) {
|
|
1993
|
+
laneBoard?.startLane("sdk", "SDK integration", {
|
|
1994
|
+
detail: "launching your coding agent",
|
|
1995
|
+
expectation: "usually 3-10 min"
|
|
1996
|
+
});
|
|
1997
|
+
emit("apply", "Running your coding agent to integrate the Moda SDK.");
|
|
1998
|
+
const verificationBaseUrl = resolveModaBaseUrl({ profile });
|
|
1999
|
+
sdkIntegration = await runSdkIntegration({
|
|
2000
|
+
apiKey,
|
|
2001
|
+
ingestUrl: resolveIngestUrl({ profile }),
|
|
2002
|
+
fetchAudit: (id) => fetchAuditWithApiKey(id, apiKey, verificationBaseUrl),
|
|
2003
|
+
onProgress: (message) => {
|
|
2004
|
+
laneBoard?.update("sdk", message);
|
|
2005
|
+
emitLaneProgress("apply", "sdk", message);
|
|
2006
|
+
},
|
|
2007
|
+
onPhase: (phase, message) => {
|
|
2008
|
+
laneBoard?.update("sdk", message);
|
|
2009
|
+
emitLaneProgress("apply", "sdk", message);
|
|
2010
|
+
},
|
|
2011
|
+
onEvent: () => {}
|
|
2012
|
+
});
|
|
2013
|
+
applySdkIntegrationOutcome(setupPlan, sdkIntegration, {
|
|
2014
|
+
laneBoard,
|
|
2015
|
+
writeGuide: () => writeSdkIntegrationGuide(rootDir)
|
|
2016
|
+
});
|
|
2017
|
+
} else {
|
|
2018
|
+
markApplied(setupPlan, "sdk", "User skipped SDK integration.");
|
|
2019
|
+
}
|
|
2020
|
+
markApplied(setupPlan, "hooks", ingestCodingAgentTraces ? undefined : "Coding-agent trace capture is temporarily unavailable.");
|
|
2021
|
+
markApplied(setupPlan, "prompts");
|
|
2022
|
+
markApplied(setupPlan, "skills");
|
|
2023
|
+
if (remote && pollPromise && remoteRunId) {
|
|
2024
|
+
const final = await pollPromise;
|
|
2025
|
+
process.removeListener("SIGINT", onRemoteSigint);
|
|
2026
|
+
const fallBackToLocal = async (error) => {
|
|
2027
|
+
board?.fail("harness", "hosted analysis failed");
|
|
2028
|
+
board?.stop();
|
|
2029
|
+
if (!allowLocalFallback)
|
|
2030
|
+
throw error;
|
|
2031
|
+
warn(renderInitNote(`Server-side harness discovery failed (${error instanceof Error ? error.message : String(error)}); falling back to local analyst.`, "warning"));
|
|
2032
|
+
emit("harness", "Server-side discovery failed; falling back to local analyst.", "warning");
|
|
2033
|
+
await finishLocalAnalyze();
|
|
2034
|
+
};
|
|
2035
|
+
if (pollError !== undefined) {
|
|
2036
|
+
await fallBackToLocal(pollError);
|
|
2037
|
+
} else if (!final || final.status !== "completed" && final.status !== "failed") {
|
|
2038
|
+
harnessDeferred = true;
|
|
2039
|
+
harnessPhase = { id: "harness", title: "Harness discovery", status: "warning", details: "still running on Moda" };
|
|
2040
|
+
markApplied(setupPlan, "harness", `Analysis still running on Moda; run \`${HARNESS_PULL_RECOVERY}\` to fetch and sync it.`);
|
|
2041
|
+
board?.warn("harness", `still running on Moda; run \`${HARNESS_PULL_RECOVERY}\` in a few minutes`);
|
|
2042
|
+
board?.stop();
|
|
2043
|
+
log(renderInitNote(`Harness analysis is still running on Moda. Run \`${HARNESS_PULL_RECOVERY}\` in a few minutes to fetch and sync the result.`, "info"));
|
|
2044
|
+
emit("harness", `Harness analysis still running on Moda; resume with \`${HARNESS_PULL_RECOVERY}\`.`, "warning");
|
|
2045
|
+
} else {
|
|
2046
|
+
board?.update("harness", "writing local artifacts");
|
|
2047
|
+
let pullResult;
|
|
2048
|
+
try {
|
|
2049
|
+
pullResult = await runHarnessCommand(createCommandContext({
|
|
2050
|
+
command: "harness",
|
|
2051
|
+
positional: "pull",
|
|
2052
|
+
positionals: ["pull"],
|
|
2053
|
+
flags: nestedHarnessFlags({ "run-id": remoteRunId })
|
|
2054
|
+
}));
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
await fallBackToLocal(error);
|
|
2057
|
+
}
|
|
2058
|
+
if (pullResult) {
|
|
2059
|
+
if (pullResult.exitCode !== 0) {
|
|
2060
|
+
board?.fail("harness", "report validation failed");
|
|
2061
|
+
board?.stop();
|
|
2062
|
+
throw validationFailure();
|
|
2063
|
+
}
|
|
2064
|
+
board?.complete("harness", "report written to .moda/");
|
|
2065
|
+
board?.stop();
|
|
2066
|
+
log(renderInitNote("Wrote or verified .moda/harness-report.json, .moda/harness.json, and .moda/HARNESS.md"));
|
|
2067
|
+
if (human)
|
|
2068
|
+
printHarnessDetectionSummary();
|
|
2069
|
+
emit("harness", "Harness discovery complete.", "completed");
|
|
2070
|
+
await approveAndSyncHarness();
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
board?.stop();
|
|
2075
|
+
if (!harnessDeferred)
|
|
2076
|
+
markApplied(setupPlan, "harness");
|
|
2077
|
+
phases.push(harnessPhase);
|
|
2078
|
+
emit("apply", "Approved changes applied.", "completed");
|
|
2079
|
+
phases.push({ id: "apply", title: "Set up", status: "completed" });
|
|
2080
|
+
const offlineValidation = isTruthyFlag(flagValue(flags, "offline"));
|
|
2081
|
+
log(renderInitStep(5, 5, "Verify", offlineValidation ? "Running local doctor checks so the setup leaves a clear state." : "Running Moda doctor online as a tenant health check (not proof of SDK integration)."));
|
|
2082
|
+
emit("validate", `Running Moda doctor diagnostics (${offlineValidation ? "offline" : "online"}).`);
|
|
2083
|
+
const doctorReport = await buildDoctorReport(createCommandContext({
|
|
2084
|
+
command: "doctor",
|
|
2085
|
+
positional: undefined,
|
|
2086
|
+
positionals: [],
|
|
2087
|
+
flags: commandFlags({ json: true, quiet: true, "no-telemetry": true })
|
|
2088
|
+
}), { offline: offlineValidation });
|
|
2089
|
+
markApplied(setupPlan, "validation");
|
|
2090
|
+
log(renderInitNote(`Validation checks completed: ${doctorReport.summary.pass} passing, ${doctorReport.summary.warning} warning, ${doctorReport.summary.fail} failing`));
|
|
2091
|
+
let telemetrySeen;
|
|
2092
|
+
if (!offlineValidation) {
|
|
2093
|
+
const cloud = doctorReport.checks.find((check) => check.id === "cloud_data_flow");
|
|
2094
|
+
if (cloud) {
|
|
2095
|
+
telemetrySeen = cloud.status === "pass";
|
|
2096
|
+
if (telemetrySeen) {
|
|
2097
|
+
log(renderInitNote("Moda Cloud telemetry: flowing"));
|
|
2098
|
+
} else {
|
|
2099
|
+
log(renderInitNote("No telemetry yet. Expected until your app runs with the SDK; see next steps.", "info"));
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
emit("validate", "Validation checks complete.", doctorReport.summary.fail > 0 ? "warning" : "completed");
|
|
2104
|
+
phases.push({ id: "validate", title: "Verify", status: "completed" });
|
|
2105
|
+
log(renderInitComplete({
|
|
2106
|
+
tenantName,
|
|
2107
|
+
dashboardUrl: `${resolveModaBaseUrl({ profile })}/dashboard`,
|
|
2108
|
+
harness: harnessDeferred ? { state: "pending" } : { state: "mapped", ...readHarnessGraphCounts() },
|
|
2109
|
+
filesChanged: changedFiles(setupPlan),
|
|
2110
|
+
skipped: setupPlan.filter((item) => item.status === "skipped" || item.status === "manual"),
|
|
2111
|
+
telemetrySeen
|
|
2112
|
+
}));
|
|
2113
|
+
return { apiKey, tenantName, phases, setupPlan, ...sdkIntegration ? { sdk_integration: sdkIntegration } : {} };
|
|
2114
|
+
}
|
|
2115
|
+
function buildSetupPlan(options) {
|
|
2116
|
+
return [
|
|
2117
|
+
{
|
|
2118
|
+
category: "harness",
|
|
2119
|
+
title: options.remote ? "Analyze the harness on Moda's servers, write the cited report, and sync the graph to Moda" : "Analyze the harness locally, write the cited report, and sync the graph to Moda",
|
|
2120
|
+
status: "planned",
|
|
2121
|
+
approved: true,
|
|
2122
|
+
files: [
|
|
2123
|
+
".moda/harness-report.json",
|
|
2124
|
+
".moda/harness-report-validation.json",
|
|
2125
|
+
".moda/HARNESS_REPORT.md",
|
|
2126
|
+
".moda/harness.json",
|
|
2127
|
+
".moda/HARNESS.md"
|
|
2128
|
+
],
|
|
2129
|
+
command: options.remote ? "moda harness analyze --remote --summary && moda harness approve --yes && moda harness sync --from-report" : "moda harness analyze --analyst=auto --summary && moda harness approve --yes && moda harness sync --from-report"
|
|
2130
|
+
},
|
|
2131
|
+
{
|
|
2132
|
+
category: "hooks",
|
|
2133
|
+
title: "Install coding-agent trace hooks",
|
|
2134
|
+
status: options.installHooks ? "planned" : "skipped",
|
|
2135
|
+
approved: options.installHooks,
|
|
2136
|
+
files: ["~/.claude/settings.json", "~/.codex/hooks.json", "~/.cursor/hooks.json"],
|
|
2137
|
+
command: "moda hooks install --agent=all",
|
|
2138
|
+
reason: options.installHooks ? undefined : "Coding-agent trace capture is temporarily unavailable."
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
category: "prompts",
|
|
2142
|
+
title: "Initialize the prompt registry manifest and sync prompts to Moda",
|
|
2143
|
+
status: "planned",
|
|
2144
|
+
approved: true,
|
|
2145
|
+
files: [".moda/prompts.yml"],
|
|
2146
|
+
command: "moda prompts init && moda prompts sync"
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
category: "skills",
|
|
2150
|
+
title: "Install the moda-cli skill into detected coding agents and sync local skills to Moda",
|
|
2151
|
+
status: "planned",
|
|
2152
|
+
approved: true,
|
|
2153
|
+
files: [".claude/skills", ".cursor/rules"],
|
|
2154
|
+
command: "moda skills sync"
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
category: "sdk",
|
|
2158
|
+
title: "Launch your coding agent to integrate the Moda SDK",
|
|
2159
|
+
status: options.integrateSdk ? "planned" : "manual",
|
|
2160
|
+
approved: options.integrateSdk,
|
|
2161
|
+
files: [],
|
|
2162
|
+
command: "moda init --sdk-integrate",
|
|
2163
|
+
reason: options.integrateSdk ? "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." : `SDK integration stays manual; see ${SDK_GUIDE_PATH} if you generate the guide.`
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
category: "ci-cd",
|
|
2167
|
+
title: "CI/CD validation workflow",
|
|
2168
|
+
status: options.configureCiCd ? "planned" : "manual",
|
|
2169
|
+
approved: options.configureCiCd,
|
|
2170
|
+
files: [".github/workflows/moda-validation.yml"],
|
|
2171
|
+
command: "moda doctor --online --json",
|
|
2172
|
+
reason: options.configureCiCd ? undefined : "No CI/CD files are changed until a project-specific plan is approved."
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
category: "autosync",
|
|
2176
|
+
title: "Autosync prompts and skills to Moda on push to main",
|
|
2177
|
+
status: options.configureAutosync ? "planned" : "manual",
|
|
2178
|
+
approved: options.configureAutosync,
|
|
2179
|
+
files: [".github/workflows/moda-autosync.yml"],
|
|
2180
|
+
command: "moda prompts sync && moda skills sync",
|
|
2181
|
+
reason: options.configureAutosync ? undefined : "No autosync workflow is written until it is explicitly approved (--autosync)."
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
category: "harness-rescan",
|
|
2185
|
+
title: "Re-scan the harness on push to main so the synced graph never drifts from the code",
|
|
2186
|
+
status: options.configureHarnessRescan ? "planned" : "manual",
|
|
2187
|
+
approved: options.configureHarnessRescan,
|
|
2188
|
+
files: [".github/workflows/moda-harness-rescan.yml"],
|
|
2189
|
+
command: "moda harness analyze --remote --yes && moda harness approve --yes && moda harness sync --from-report",
|
|
2190
|
+
reason: options.configureHarnessRescan ? undefined : "No harness rescan workflow is written until it is explicitly approved (--harness-rescan)."
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
category: "validation",
|
|
2194
|
+
title: "Run Moda doctor online as a tenant health check",
|
|
2195
|
+
status: "planned",
|
|
2196
|
+
approved: true,
|
|
2197
|
+
files: [],
|
|
2198
|
+
command: "moda doctor --online"
|
|
2199
|
+
}
|
|
2200
|
+
];
|
|
2201
|
+
}
|
|
2202
|
+
function markApplied(setupPlan, category, skipReason) {
|
|
2203
|
+
const action = setupPlan.find((item) => item.category === category);
|
|
2204
|
+
if (!action)
|
|
2205
|
+
return;
|
|
2206
|
+
if (skipReason) {
|
|
2207
|
+
action.status = "skipped";
|
|
2208
|
+
action.reason = skipReason;
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (action.approved)
|
|
2212
|
+
action.status = "applied";
|
|
2213
|
+
}
|
|
2214
|
+
function markManual(setupPlan, category, reason) {
|
|
2215
|
+
const action = setupPlan.find((item) => item.category === category);
|
|
2216
|
+
if (!action)
|
|
2217
|
+
return;
|
|
2218
|
+
action.status = "manual";
|
|
2219
|
+
action.reason = reason;
|
|
2220
|
+
}
|
|
2221
|
+
function applySdkIntegrationOutcome(setupPlan, outcome, deps) {
|
|
2222
|
+
const sdkAction = setupPlan.find((action) => action.category === "sdk");
|
|
2223
|
+
if (!sdkAction)
|
|
2224
|
+
return;
|
|
2225
|
+
const { laneBoard, writeGuide } = deps;
|
|
2226
|
+
const verificationId = outcome.verification_id ?? "unknown";
|
|
2227
|
+
const firstRemaining = outcome.agent_result?.remaining_actions?.[0];
|
|
2228
|
+
switch (outcome.status) {
|
|
2229
|
+
case "integrated_verified": {
|
|
2230
|
+
const llmSpans = outcome.verification?.llm_spans ?? 0;
|
|
2231
|
+
laneBoard?.complete("sdk", `verified — trace reached Moda (${llmSpans} llm span(s), conversation ${verificationId})`);
|
|
2232
|
+
markApplied(setupPlan, "sdk");
|
|
2233
|
+
if (outcome.agent_result?.files_changed?.length) {
|
|
2234
|
+
sdkAction.files = [...new Set(outcome.agent_result.files_changed)].slice(0, 20);
|
|
2235
|
+
}
|
|
2236
|
+
break;
|
|
2237
|
+
}
|
|
2238
|
+
case "telemetry_not_observed":
|
|
2239
|
+
laneBoard?.warn("sdk", `integration applied but the verification trace was not observed; check MODA_API_KEY at runtime, then moda audit ${verificationId}`);
|
|
2240
|
+
markManual(setupPlan, "sdk", outcome.detail ?? "Verification trace was not observed.");
|
|
2241
|
+
break;
|
|
2242
|
+
case "needs_human_input": {
|
|
2243
|
+
const securityLead = outcome.security_findings?.length ? `Security: possible API key leak in ${outcome.security_findings.join(", ")} — remove the key, rotate it, and keep secrets in the environment. ` : "";
|
|
2244
|
+
const note = firstRemaining ?? outcome.detail ?? "Additional human input required.";
|
|
2245
|
+
laneBoard?.warn("sdk", `${securityLead}${note}`);
|
|
2246
|
+
markManual(setupPlan, "sdk", `${securityLead}${note}`.trim());
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
case "nothing_to_integrate": {
|
|
2250
|
+
if (outcome.verification?.observed === true) {
|
|
2251
|
+
laneBoard?.complete("sdk", outcome.detail ?? "existing integration verified — trace reached Moda");
|
|
2252
|
+
markApplied(setupPlan, "sdk");
|
|
2253
|
+
sdkAction.reason = outcome.detail ?? "Existing Moda integration verified.";
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
const reason = outcome.verification ? "agent reports Moda is already integrated but the verification trace was not observed; " + `check MODA_API_KEY at runtime, then \`moda audit ${verificationId}\`` : "agent reports Moda is already integrated — not independently verified; " + "send a real request through your app with a conversation id you choose, then run `moda audit` with that id";
|
|
2257
|
+
laneBoard?.warn("sdk", reason);
|
|
2258
|
+
markManual(setupPlan, "sdk", reason);
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2261
|
+
case "agent_unavailable": {
|
|
2262
|
+
const guidePath = writeGuide();
|
|
2263
|
+
sdkAction.files = [guidePath];
|
|
2264
|
+
laneBoard?.warn("sdk", `no coding agent CLI found; wrote ${guidePath} to integrate by hand`);
|
|
2265
|
+
markManual(setupPlan, "sdk", `No coding agent CLI found; wrote ${guidePath}.`);
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
case "cancelled":
|
|
2269
|
+
laneBoard?.fail("sdk", outcome.detail ?? "SDK integration cancelled");
|
|
2270
|
+
markManual(setupPlan, "sdk", outcome.detail ?? "SDK integration cancelled.");
|
|
2271
|
+
break;
|
|
2272
|
+
case "repository_checks_failed":
|
|
2273
|
+
case "smoke_execution_failed":
|
|
2274
|
+
case "agent_failed":
|
|
2275
|
+
default: {
|
|
2276
|
+
const guidePath = writeGuide();
|
|
2277
|
+
sdkAction.files = [guidePath];
|
|
2278
|
+
const detail = outcome.detail ?? "SDK integration did not complete.";
|
|
2279
|
+
laneBoard?.warn("sdk", `${detail}; wrote ${guidePath} to finish manually`);
|
|
2280
|
+
markManual(setupPlan, "sdk", `${detail}; wrote ${guidePath}.`);
|
|
2281
|
+
break;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
function changedFiles(setupPlan) {
|
|
2286
|
+
return [...new Set(setupPlan.filter((action) => action.status === "applied").flatMap((action) => action.files))].filter((file) => !file.includes(".env"));
|
|
2287
|
+
}
|
|
2288
|
+
var GENERATED_WORKFLOW_CLI = `@moda-ai/cli@${VERSION}`;
|
|
2289
|
+
var GENERATED_WORKFLOW_NPX = `npx -y --package=${GENERATED_WORKFLOW_CLI} moda`;
|
|
2290
|
+
function writeCiCdWorkflow(cwd = process.cwd()) {
|
|
2291
|
+
const relativePath = ".github/workflows/moda-validation.yml";
|
|
2292
|
+
const path = join6(cwd, relativePath);
|
|
2293
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
2294
|
+
writeFileSync4(path, `${[
|
|
2295
|
+
"name: Moda Validation",
|
|
2296
|
+
"",
|
|
2297
|
+
"on:",
|
|
2298
|
+
" pull_request:",
|
|
2299
|
+
" push:",
|
|
2300
|
+
" branches: [main]",
|
|
2301
|
+
"",
|
|
2302
|
+
"# Least privilege: these jobs only read the checked-out repo and talk to",
|
|
2303
|
+
"# Moda over the API key; they never write back to the repository.",
|
|
2304
|
+
"permissions:",
|
|
2305
|
+
" contents: read",
|
|
2306
|
+
"",
|
|
2307
|
+
"jobs:",
|
|
2308
|
+
" moda-doctor:",
|
|
2309
|
+
" runs-on: ubuntu-latest",
|
|
2310
|
+
" env:",
|
|
2311
|
+
" MODA_API_KEY: ${{ secrets.MODA_API_KEY }}",
|
|
2312
|
+
" MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }}",
|
|
2313
|
+
" steps:",
|
|
2314
|
+
" - uses: actions/checkout@v4",
|
|
2315
|
+
" - uses: actions/setup-node@v4",
|
|
2316
|
+
" with:",
|
|
2317
|
+
" node-version: 22",
|
|
2318
|
+
" - name: Validate Moda setup",
|
|
2319
|
+
` run: ${GENERATED_WORKFLOW_NPX} doctor --online --json`,
|
|
2320
|
+
""
|
|
2321
|
+
].join(`
|
|
2322
|
+
`)}`);
|
|
2323
|
+
return relativePath;
|
|
2324
|
+
}
|
|
2325
|
+
function writeAutosyncWorkflow(cwd = process.cwd()) {
|
|
2326
|
+
const relativePath = ".github/workflows/moda-autosync.yml";
|
|
2327
|
+
const path = join6(cwd, relativePath);
|
|
2328
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
2329
|
+
writeFileSync4(path, `${[
|
|
2330
|
+
"name: Moda Autosync",
|
|
2331
|
+
"",
|
|
2332
|
+
"on:",
|
|
2333
|
+
" push:",
|
|
2334
|
+
" branches: [main]",
|
|
2335
|
+
"",
|
|
2336
|
+
"# Least privilege: this job only reads the checked-out repo and writes to",
|
|
2337
|
+
"# Moda over the API key; it never writes back to the repository.",
|
|
2338
|
+
"permissions:",
|
|
2339
|
+
" contents: read",
|
|
2340
|
+
"",
|
|
2341
|
+
"# A single in-flight sync at a time; newer pushes supersede older runs.",
|
|
2342
|
+
"concurrency:",
|
|
2343
|
+
" group: moda-autosync",
|
|
2344
|
+
" cancel-in-progress: true",
|
|
2345
|
+
"",
|
|
2346
|
+
"jobs:",
|
|
2347
|
+
" moda-sync:",
|
|
2348
|
+
" runs-on: ubuntu-latest",
|
|
2349
|
+
" env:",
|
|
2350
|
+
" MODA_API_KEY: ${{ secrets.MODA_API_KEY }}",
|
|
2351
|
+
" MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }}",
|
|
2352
|
+
" steps:",
|
|
2353
|
+
" - uses: actions/checkout@v4",
|
|
2354
|
+
" - uses: actions/setup-node@v4",
|
|
2355
|
+
" with:",
|
|
2356
|
+
" node-version: 22",
|
|
2357
|
+
` - name: Sync prompts to Moda`,
|
|
2358
|
+
` run: ${GENERATED_WORKFLOW_NPX} prompts sync`,
|
|
2359
|
+
` - name: Sync skills to Moda`,
|
|
2360
|
+
` run: ${GENERATED_WORKFLOW_NPX} skills sync`,
|
|
2361
|
+
""
|
|
2362
|
+
].join(`
|
|
2363
|
+
`)}`);
|
|
2364
|
+
return relativePath;
|
|
2365
|
+
}
|
|
2366
|
+
function writeHarnessRescanWorkflow(cwd = process.cwd(), paths) {
|
|
2367
|
+
const relativePath = ".github/workflows/moda-harness-rescan.yml";
|
|
2368
|
+
const path = join6(cwd, relativePath);
|
|
2369
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
2370
|
+
const pathScope = paths !== undefined && paths.length > 0 ? [
|
|
2371
|
+
" # Only pushes touching these paths trigger a rescan.",
|
|
2372
|
+
" paths:",
|
|
2373
|
+
...paths.map((glob) => ` - '${glob.replace(/'/g, "''")}'`)
|
|
2374
|
+
] : [
|
|
2375
|
+
" # Optional: scope rescans to agent-relevant paths, e.g.",
|
|
2376
|
+
" # paths:",
|
|
2377
|
+
" # - 'src/agents/**'"
|
|
2378
|
+
];
|
|
2379
|
+
writeFileSync4(path, `${[
|
|
2380
|
+
"name: Moda Harness Rescan",
|
|
2381
|
+
"",
|
|
2382
|
+
"on:",
|
|
2383
|
+
" push:",
|
|
2384
|
+
" branches: [main]",
|
|
2385
|
+
...pathScope,
|
|
2386
|
+
" workflow_dispatch:",
|
|
2387
|
+
"",
|
|
2388
|
+
"# Least privilege: this job reads the checked-out repo, uploads a safe",
|
|
2389
|
+
"# source snapshot to Moda over the API key, and syncs the refreshed harness",
|
|
2390
|
+
"# graph; it never writes back to the repository.",
|
|
2391
|
+
"permissions:",
|
|
2392
|
+
" contents: read",
|
|
2393
|
+
"",
|
|
2394
|
+
"# One in-flight rescan at a time; newer pushes supersede older runs.",
|
|
2395
|
+
"concurrency:",
|
|
2396
|
+
" group: moda-harness-rescan",
|
|
2397
|
+
" cancel-in-progress: true",
|
|
2398
|
+
"",
|
|
2399
|
+
"jobs:",
|
|
2400
|
+
" moda-harness-rescan:",
|
|
2401
|
+
" runs-on: ubuntu-latest",
|
|
2402
|
+
" env:",
|
|
2403
|
+
" MODA_API_KEY: ${{ secrets.MODA_API_KEY }}",
|
|
2404
|
+
" MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }}",
|
|
2405
|
+
" steps:",
|
|
2406
|
+
" - uses: actions/checkout@v4",
|
|
2407
|
+
" - uses: actions/setup-node@v4",
|
|
2408
|
+
" with:",
|
|
2409
|
+
" node-version: 22",
|
|
2410
|
+
" - name: Re-analyze the harness on Moda",
|
|
2411
|
+
` run: ${GENERATED_WORKFLOW_NPX} harness analyze --remote --yes`,
|
|
2412
|
+
" - name: Approve the refreshed harness report",
|
|
2413
|
+
` run: ${GENERATED_WORKFLOW_NPX} harness approve --yes`,
|
|
2414
|
+
" - name: Sync the harness graph to Moda",
|
|
2415
|
+
` run: ${GENERATED_WORKFLOW_NPX} harness sync --from-report`,
|
|
2416
|
+
""
|
|
2417
|
+
].join(`
|
|
2418
|
+
`)}`);
|
|
2419
|
+
return relativePath;
|
|
2420
|
+
}
|
|
2421
|
+
function printHarnessDetectionSummary(cwd = process.cwd()) {
|
|
2422
|
+
const manifestPath = join6(cwd, ".moda", "harness.json");
|
|
2423
|
+
if (!existsSync6(manifestPath))
|
|
2424
|
+
return;
|
|
2425
|
+
try {
|
|
2426
|
+
const graph = JSON.parse(readFileSync5(manifestPath, "utf8"));
|
|
2427
|
+
const frameworkNames = (graph.agentFrameworks ?? []).map((framework) => typeof framework.name === "string" ? framework.name : undefined).filter((name) => Boolean(name));
|
|
2428
|
+
const providerNames = (graph.modelProviders ?? []).map((provider) => typeof provider.name === "string" ? provider.name : undefined).filter((name) => Boolean(name));
|
|
2429
|
+
console.log(` Detected harness: ${(graph.runtimeAgents ?? []).length} runtime agent(s), ` + `${(graph.artifacts ?? []).length} artifact(s), ${(graph.relationships ?? []).length} relationship(s)`);
|
|
2430
|
+
if (frameworkNames.length > 0)
|
|
2431
|
+
console.log(` Frameworks: ${frameworkNames.join(", ")}`);
|
|
2432
|
+
if (providerNames.length > 0)
|
|
2433
|
+
console.log(` Model providers: ${providerNames.join(", ")}`);
|
|
2434
|
+
} catch {
|
|
2435
|
+
console.log(" Detected harness summary is available in .moda/HARNESS.md");
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
function signalsFromGraph(graph) {
|
|
2439
|
+
const names = (items) => (items ?? []).map((item) => typeof item.name === "string" ? item.name : undefined).filter((name) => Boolean(name));
|
|
2440
|
+
return {
|
|
2441
|
+
runtimeAgents: Array.isArray(graph.runtimeAgents) ? graph.runtimeAgents.length : 0,
|
|
2442
|
+
frameworks: names(graph.agentFrameworks),
|
|
2443
|
+
providers: names(graph.modelProviders)
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
function readHarnessQualitySignals(cwd = process.cwd()) {
|
|
2447
|
+
const reportPath = join6(cwd, ".moda", "harness-report.json");
|
|
2448
|
+
if (existsSync6(reportPath)) {
|
|
2449
|
+
try {
|
|
2450
|
+
const report = JSON.parse(readFileSync5(reportPath, "utf8"));
|
|
2451
|
+
if (report.graph && typeof report.graph === "object") {
|
|
2452
|
+
return signalsFromGraph(report.graph);
|
|
2453
|
+
}
|
|
2454
|
+
} catch {}
|
|
2455
|
+
}
|
|
2456
|
+
const manifestPath = join6(cwd, ".moda", "harness.json");
|
|
2457
|
+
if (!existsSync6(manifestPath))
|
|
2458
|
+
return;
|
|
2459
|
+
try {
|
|
2460
|
+
return signalsFromGraph(JSON.parse(readFileSync5(manifestPath, "utf8")));
|
|
2461
|
+
} catch {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
function harnessLooksIncomplete(signals) {
|
|
2466
|
+
return signals.runtimeAgents === 0 && (signals.frameworks.length > 0 || signals.providers.length > 0);
|
|
2467
|
+
}
|
|
2468
|
+
var REPO_SIGNAL_MAX_DEPTH = 3;
|
|
2469
|
+
var REPO_SIGNAL_MAX_DIRS = 500;
|
|
2470
|
+
var REPO_SIGNAL_SKIPPED_DIRS = new Set([
|
|
2471
|
+
"node_modules",
|
|
2472
|
+
"dist",
|
|
2473
|
+
"build",
|
|
2474
|
+
"out",
|
|
2475
|
+
"coverage",
|
|
2476
|
+
"vendor",
|
|
2477
|
+
"target",
|
|
2478
|
+
"venv",
|
|
2479
|
+
"env",
|
|
2480
|
+
"__pycache__",
|
|
2481
|
+
"site-packages"
|
|
2482
|
+
]);
|
|
2483
|
+
var JS_LLM_DEPENDENCIES = {
|
|
2484
|
+
openai: { name: "OpenAI", kind: "provider" },
|
|
2485
|
+
"@anthropic-ai/sdk": { name: "Anthropic", kind: "provider" },
|
|
2486
|
+
"@google/generative-ai": { name: "Google Gemini", kind: "provider" },
|
|
2487
|
+
"@google/genai": { name: "Google Gemini", kind: "provider" },
|
|
2488
|
+
"cohere-ai": { name: "Cohere", kind: "provider" },
|
|
2489
|
+
"groq-sdk": { name: "Groq", kind: "provider" },
|
|
2490
|
+
"@mistralai/mistralai": { name: "Mistral", kind: "provider" },
|
|
2491
|
+
ollama: { name: "Ollama", kind: "provider" },
|
|
2492
|
+
"together-ai": { name: "Together", kind: "provider" },
|
|
2493
|
+
"@aws-sdk/client-bedrock-runtime": { name: "AWS Bedrock", kind: "provider" },
|
|
2494
|
+
ai: { name: "Vercel AI SDK", kind: "framework" },
|
|
2495
|
+
langchain: { name: "LangChain", kind: "framework" },
|
|
2496
|
+
llamaindex: { name: "LlamaIndex", kind: "framework" },
|
|
2497
|
+
"@anthropic-ai/claude-agent-sdk": { name: "Claude Agent SDK", kind: "framework" },
|
|
2498
|
+
"@openai/agents": { name: "OpenAI Agents", kind: "framework" },
|
|
2499
|
+
"@mastra/core": { name: "Mastra", kind: "framework" }
|
|
2500
|
+
};
|
|
2501
|
+
var JS_LLM_DEPENDENCY_PREFIXES = [
|
|
2502
|
+
["@langchain/", { name: "LangChain", kind: "framework" }],
|
|
2503
|
+
["@ai-sdk/", { name: "Vercel AI SDK", kind: "framework" }],
|
|
2504
|
+
["@llamaindex/", { name: "LlamaIndex", kind: "framework" }]
|
|
2505
|
+
];
|
|
2506
|
+
var PY_LLM_DEPENDENCIES = {
|
|
2507
|
+
openai: { name: "OpenAI", kind: "provider" },
|
|
2508
|
+
anthropic: { name: "Anthropic", kind: "provider" },
|
|
2509
|
+
"google-generativeai": { name: "Google Gemini", kind: "provider" },
|
|
2510
|
+
"google-genai": { name: "Google Gemini", kind: "provider" },
|
|
2511
|
+
cohere: { name: "Cohere", kind: "provider" },
|
|
2512
|
+
groq: { name: "Groq", kind: "provider" },
|
|
2513
|
+
mistralai: { name: "Mistral", kind: "provider" },
|
|
2514
|
+
ollama: { name: "Ollama", kind: "provider" },
|
|
2515
|
+
langgraph: { name: "LangGraph", kind: "framework" },
|
|
2516
|
+
litellm: { name: "LiteLLM", kind: "framework" },
|
|
2517
|
+
crewai: { name: "CrewAI", kind: "framework" },
|
|
2518
|
+
autogen: { name: "AutoGen", kind: "framework" },
|
|
2519
|
+
pyautogen: { name: "AutoGen", kind: "framework" },
|
|
2520
|
+
ag2: { name: "AutoGen", kind: "framework" },
|
|
2521
|
+
"pydantic-ai": { name: "Pydantic AI", kind: "framework" },
|
|
2522
|
+
"openai-agents": { name: "OpenAI Agents", kind: "framework" },
|
|
2523
|
+
"semantic-kernel": { name: "Semantic Kernel", kind: "framework" },
|
|
2524
|
+
dspy: { name: "DSPy", kind: "framework" },
|
|
2525
|
+
"haystack-ai": { name: "Haystack", kind: "framework" }
|
|
2526
|
+
};
|
|
2527
|
+
var PY_LLM_DEPENDENCY_PREFIXES = [
|
|
2528
|
+
["langchain", { name: "LangChain", kind: "framework" }],
|
|
2529
|
+
["llama-index", { name: "LlamaIndex", kind: "framework" }]
|
|
2530
|
+
];
|
|
2531
|
+
function jsDependencySignal(dep) {
|
|
2532
|
+
const exact = JS_LLM_DEPENDENCIES[dep];
|
|
2533
|
+
if (exact)
|
|
2534
|
+
return exact;
|
|
2535
|
+
for (const [prefix, signal] of JS_LLM_DEPENDENCY_PREFIXES) {
|
|
2536
|
+
if (dep.startsWith(prefix))
|
|
2537
|
+
return signal;
|
|
2538
|
+
}
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
function pyDependencySignal(dep) {
|
|
2542
|
+
const normalized = dep.toLowerCase().replace(/_/g, "-");
|
|
2543
|
+
const exact = PY_LLM_DEPENDENCIES[normalized];
|
|
2544
|
+
if (exact)
|
|
2545
|
+
return exact;
|
|
2546
|
+
for (const [prefix, signal] of PY_LLM_DEPENDENCY_PREFIXES) {
|
|
2547
|
+
if (normalized === prefix || normalized.startsWith(`${prefix}-`))
|
|
2548
|
+
return signal;
|
|
2549
|
+
}
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
function pythonRequirementNames(text, file) {
|
|
2553
|
+
const names = [];
|
|
2554
|
+
if (file === "requirements.txt") {
|
|
2555
|
+
for (const line of text.split(`
|
|
2556
|
+
`)) {
|
|
2557
|
+
const match = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line);
|
|
2558
|
+
if (match)
|
|
2559
|
+
names.push(match[1]);
|
|
2560
|
+
}
|
|
2561
|
+
return names;
|
|
2562
|
+
}
|
|
2563
|
+
for (const match of text.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)[^"']*["']/g)) {
|
|
2564
|
+
names.push(match[1]);
|
|
2565
|
+
}
|
|
2566
|
+
return names;
|
|
2567
|
+
}
|
|
2568
|
+
function detectRepoLlmSignals(cwd = process.cwd()) {
|
|
2569
|
+
const frameworks = new Set;
|
|
2570
|
+
const providers = new Set;
|
|
2571
|
+
const record = (signal) => {
|
|
2572
|
+
if (!signal)
|
|
2573
|
+
return;
|
|
2574
|
+
(signal.kind === "framework" ? frameworks : providers).add(signal.name);
|
|
2575
|
+
};
|
|
2576
|
+
const scanDir = (dir) => {
|
|
2577
|
+
try {
|
|
2578
|
+
const pkg = JSON.parse(readFileSync5(join6(dir, "package.json"), "utf8"));
|
|
2579
|
+
for (const dep of [
|
|
2580
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
2581
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
2582
|
+
...Object.keys(pkg.peerDependencies ?? {})
|
|
2583
|
+
]) {
|
|
2584
|
+
record(jsDependencySignal(dep));
|
|
2585
|
+
}
|
|
2586
|
+
} catch {}
|
|
2587
|
+
for (const file of ["requirements.txt", "pyproject.toml"]) {
|
|
2588
|
+
try {
|
|
2589
|
+
const text = readFileSync5(join6(dir, file), "utf8");
|
|
2590
|
+
for (const name of pythonRequirementNames(text, file)) {
|
|
2591
|
+
record(pyDependencySignal(name));
|
|
2592
|
+
}
|
|
2593
|
+
} catch {}
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
let visited = 0;
|
|
2597
|
+
const walk = (dir, depth) => {
|
|
2598
|
+
if (depth > REPO_SIGNAL_MAX_DEPTH || visited >= REPO_SIGNAL_MAX_DIRS)
|
|
2599
|
+
return;
|
|
2600
|
+
visited += 1;
|
|
2601
|
+
scanDir(dir);
|
|
2602
|
+
let entries;
|
|
2603
|
+
try {
|
|
2604
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2605
|
+
} catch {
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
for (const entry of entries) {
|
|
2609
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
2610
|
+
continue;
|
|
2611
|
+
if (REPO_SIGNAL_SKIPPED_DIRS.has(entry.name))
|
|
2612
|
+
continue;
|
|
2613
|
+
walk(join6(dir, entry.name), depth + 1);
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
walk(cwd, 0);
|
|
2617
|
+
return { frameworks: [...frameworks].sort(), providers: [...providers].sort() };
|
|
2618
|
+
}
|
|
2619
|
+
function readHarnessGraphCounts(cwd = process.cwd()) {
|
|
2620
|
+
const manifestPath = join6(cwd, ".moda", "harness.json");
|
|
2621
|
+
if (!existsSync6(manifestPath))
|
|
2622
|
+
return {};
|
|
2623
|
+
try {
|
|
2624
|
+
const graph = JSON.parse(readFileSync5(manifestPath, "utf8"));
|
|
2625
|
+
return {
|
|
2626
|
+
agents: Array.isArray(graph.runtimeAgents) ? graph.runtimeAgents.length : 0,
|
|
2627
|
+
artifacts: Array.isArray(graph.artifacts) ? graph.artifacts.length : 0
|
|
2628
|
+
};
|
|
2629
|
+
} catch {
|
|
2630
|
+
return {};
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
function commandFlags(flags) {
|
|
2634
|
+
return Object.fromEntries(Object.entries(flags).filter(([, value]) => value !== undefined && value !== false).map(([key, value]) => [key, value === true ? "true" : String(value)]));
|
|
2635
|
+
}
|
|
2636
|
+
function stringFlag(value) {
|
|
2637
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
2638
|
+
}
|
|
2639
|
+
function resumeFlagsForInit(flags) {
|
|
2640
|
+
const passthrough = [
|
|
2641
|
+
"profile",
|
|
2642
|
+
"tenant-id",
|
|
2643
|
+
"offline",
|
|
2644
|
+
"analyst",
|
|
2645
|
+
"sdk-integrate",
|
|
2646
|
+
"ci-cd",
|
|
2647
|
+
"autosync",
|
|
2648
|
+
"harness-rescan",
|
|
2649
|
+
"harness-rescan-paths",
|
|
2650
|
+
"no-sdk-integrate",
|
|
2651
|
+
"no-ci-cd",
|
|
2652
|
+
"no-autosync",
|
|
2653
|
+
"no-harness-rescan",
|
|
2654
|
+
"yes",
|
|
2655
|
+
"agent",
|
|
2656
|
+
"json",
|
|
2657
|
+
"stream"
|
|
2658
|
+
];
|
|
2659
|
+
const out = [];
|
|
2660
|
+
for (const key of passthrough) {
|
|
2661
|
+
const value = flagValue(flags, key, key.replace(/-/g, "_"));
|
|
2662
|
+
if (value === undefined || value === false || value === "false")
|
|
2663
|
+
continue;
|
|
2664
|
+
const flag = `--${key}`;
|
|
2665
|
+
if (value === true || value === "true")
|
|
2666
|
+
out.push(flag);
|
|
2667
|
+
else
|
|
2668
|
+
out.push(flag, String(value));
|
|
2669
|
+
}
|
|
2670
|
+
if (resolveHarnessRescanFlag(flags) === false && !out.includes("--no-harness-rescan")) {
|
|
2671
|
+
out.push("--no-harness-rescan");
|
|
2672
|
+
}
|
|
2673
|
+
return out;
|
|
2674
|
+
}
|
|
2675
|
+
function resolveCiCdFlag(flags) {
|
|
2676
|
+
const explicit = parseOptionalBoolean(flagValue(flags, "ci-cd", "ci_cd", "ci"));
|
|
2677
|
+
if (explicit !== undefined)
|
|
2678
|
+
return explicit;
|
|
2679
|
+
if (isTruthyFlag(flagValue(flags, "no-ci-cd", "no_ci_cd")))
|
|
2680
|
+
return false;
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
function resolveAutosyncFlag(flags) {
|
|
2684
|
+
const explicit = parseOptionalBoolean(flagValue(flags, "autosync", "auto-sync", "auto_sync"));
|
|
2685
|
+
if (explicit !== undefined)
|
|
2686
|
+
return explicit;
|
|
2687
|
+
if (isTruthyFlag(flagValue(flags, "no-autosync", "no_autosync", "no-auto-sync", "no_auto_sync")))
|
|
2688
|
+
return false;
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
function resolveHarnessRescanFlag(flags) {
|
|
2692
|
+
const explicit = parseOptionalBoolean(flagValue(flags, "harness-rescan", "harness_rescan"));
|
|
2693
|
+
if (explicit !== undefined)
|
|
2694
|
+
return explicit;
|
|
2695
|
+
if (isTruthyFlag(flagValue(flags, "no-harness-rescan", "no_harness_rescan")))
|
|
2696
|
+
return false;
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
function parseHarnessRescanPaths(value) {
|
|
2700
|
+
if (typeof value !== "string")
|
|
2701
|
+
return;
|
|
2702
|
+
const globs = value.split(",").map((glob) => glob.trim()).filter((glob) => glob.length > 0 && glob !== "true");
|
|
2703
|
+
return globs.length > 0 ? globs : undefined;
|
|
2704
|
+
}
|
|
2705
|
+
function resolveSdkIntegrateFlag(flags) {
|
|
2706
|
+
const explicit = parseOptionalBoolean(flagValue(flags, "sdk-integrate", "sdk_integrate", "sdk-guide", "sdk_guide", "sdk"));
|
|
2707
|
+
if (explicit !== undefined)
|
|
2708
|
+
return explicit;
|
|
2709
|
+
if (isTruthyFlag(flagValue(flags, "no-sdk-integrate", "no_sdk_integrate", "no-sdk-guide", "no_sdk_guide", "no-sdk", "no_sdk"))) {
|
|
2710
|
+
return false;
|
|
2711
|
+
}
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
function parseOptionalBoolean(value) {
|
|
2715
|
+
if (value === undefined)
|
|
2716
|
+
return;
|
|
2717
|
+
if (value === true || value === 1 || value === "true" || value === "1" || value === "yes")
|
|
2718
|
+
return true;
|
|
2719
|
+
if (value === false || value === 0 || value === "false" || value === "0" || value === "no")
|
|
2720
|
+
return false;
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
function isTruthyFlag(value) {
|
|
2724
|
+
return value === true || value === 1 || value === "true" || value === "1" || value === "yes";
|
|
2725
|
+
}
|
|
2726
|
+
function resolveHarnessRemote(flags) {
|
|
2727
|
+
if (isTruthyFlag(flagValue(flags, "local")))
|
|
2728
|
+
return false;
|
|
2729
|
+
if (isTruthyFlag(flagValue(flags, "no-remote", "no_remote")))
|
|
2730
|
+
return false;
|
|
2731
|
+
const remote = parseOptionalBoolean(flagValue(flags, "remote"));
|
|
2732
|
+
if (remote !== undefined)
|
|
2733
|
+
return remote;
|
|
2734
|
+
if (isDeterministicLocalAnalyst(stringFlag(flagValue(flags, "analyst")))) {
|
|
2735
|
+
return false;
|
|
2736
|
+
}
|
|
2737
|
+
return true;
|
|
2738
|
+
}
|
|
2739
|
+
function hasExplicitRemoteChoice(flags) {
|
|
2740
|
+
if (isTruthyFlag(flagValue(flags, "local")))
|
|
2741
|
+
return true;
|
|
2742
|
+
if (isTruthyFlag(flagValue(flags, "no-remote", "no_remote")))
|
|
2743
|
+
return true;
|
|
2744
|
+
if (parseOptionalBoolean(flagValue(flags, "remote")) !== undefined)
|
|
2745
|
+
return true;
|
|
2746
|
+
return isDeterministicLocalAnalyst(stringFlag(flagValue(flags, "analyst")));
|
|
2747
|
+
}
|
|
2748
|
+
function flagValue(flags, ...keys) {
|
|
2749
|
+
for (const key of keys) {
|
|
2750
|
+
if (flags[key] !== undefined)
|
|
2751
|
+
return flags[key];
|
|
2752
|
+
}
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
export {
|
|
2756
|
+
writeHarnessRescanWorkflow,
|
|
2757
|
+
writeCiCdWorkflow,
|
|
2758
|
+
writeAutosyncWorkflow,
|
|
2759
|
+
runInit,
|
|
2760
|
+
resumeFlagsForInit,
|
|
2761
|
+
resolveHarnessRemote,
|
|
2762
|
+
readHarnessQualitySignals,
|
|
2763
|
+
harnessLooksIncomplete,
|
|
2764
|
+
detectRepoLlmSignals
|
|
2765
|
+
};
|