@moda-ai/cli 1.44.1 → 1.45.1

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.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  runPromptsCommand,
9
9
  runSkillsCommand,
10
10
  runStatusCommand
11
- } from "./cli-j2rdgd3p.js";
11
+ } from "./cli-nnwegmzh.js";
12
12
  import {
13
13
  ApiError,
14
14
  HARNESS_REPORT_APPROVAL_PATH,
@@ -37,7 +37,7 @@ import {
37
37
  terminalStyles,
38
38
  unknownSignalsSubcommandError,
39
39
  validateHarnessReport
40
- } from "./cli-5tsqk3jt.js";
40
+ } from "./cli-3xh2w7hb.js";
41
41
  import"./cli-ssfq84k5.js";
42
42
  import {
43
43
  authFetch,
@@ -46,7 +46,7 @@ import {
46
46
  loadAuthSession,
47
47
  login,
48
48
  revokeCliSessionToken
49
- } from "./cli-rycpwqzm.js";
49
+ } from "./cli-dy2aavbe.js";
50
50
  import {
51
51
  AGENT_ERROR_CODES,
52
52
  CliAccessError,
@@ -72,6 +72,7 @@ import {
72
72
  describeBaseUrlSource,
73
73
  embeddedTenantIdFromApiKey,
74
74
  ensureSecretFilePermissions,
75
+ explicitOutputMode,
75
76
  getActiveProfile,
76
77
  getModaDir,
77
78
  getModaPaths,
@@ -95,7 +96,7 @@ import {
95
96
  validateConfig,
96
97
  wasOutputCancelled,
97
98
  writeSecretJsonFile
98
- } from "./cli-8c60k2p4.js";
99
+ } from "./cli-bvnw7585.js";
99
100
  import {
100
101
  __require
101
102
  } from "./cli-0v6na3yp.js";
@@ -2062,9 +2063,539 @@ async function runSyncCommand(context) {
2062
2063
  }
2063
2064
 
2064
2065
  // src/registry.ts
2066
+ import { execFileSync } from "node:child_process";
2065
2067
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
2066
2068
  import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "node:path";
2067
- var REGISTRY_SCHEMA = "moda_registry.v1";
2069
+
2070
+ // src/registry-harness.ts
2071
+ var HARNESS_SECTIONS = ["runtime", "agents", "routing", "context", "mcpServers"];
2072
+ var ROUTING_STRATEGIES = ["single", "trace_attribute", "handoff", "router", "rules"];
2073
+ var HISTORY_STRATEGIES = ["full", "window", "summary", "truncate_tokens"];
2074
+ var SKILL_LOADING_MODES = ["tool", "system_prompt", "none"];
2075
+ var CONTEXT_SOURCES = ["static", "clock", "trace", "tool", "memory", "retrieval", "unknown"];
2076
+ var CONTEXT_TARGETS = ["prompt_variable", "system_append"];
2077
+ var CLOCK_FORMATS = ["date", "datetime", "iso"];
2078
+ var MCP_TRANSPORTS = ["stdio", "http", "sse", "streamable_http"];
2079
+ var AGENT_KEY_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
2080
+ var TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
2081
+ var VARIABLE_PATTERN = /^[A-Za-z_][A-Za-z0-9_.]{0,127}$/;
2082
+ var MAX_AGENTS = 100;
2083
+ var MAX_CONTEXT = 200;
2084
+ var MAX_MCP_SERVERS = 50;
2085
+ var MAX_HARNESS_DOCUMENT_BYTES = 1e6;
2086
+ var AGENT_FIELDS = {
2087
+ key: "string",
2088
+ name: "string",
2089
+ description: "string",
2090
+ entry: "boolean",
2091
+ prompt: "string",
2092
+ model: "object",
2093
+ tools: "string[]",
2094
+ skills: "string[]",
2095
+ handoffs: "object[]",
2096
+ subagents: "object[]",
2097
+ context: "string[]",
2098
+ maxToolRoundsPerTurn: "number",
2099
+ responseSchema: "object",
2100
+ match: "object"
2101
+ };
2102
+ var MODEL_FIELDS = {
2103
+ id: "string",
2104
+ provider: "string",
2105
+ temperature: "number",
2106
+ maxTokens: "number",
2107
+ topP: "number",
2108
+ reasoningEffort: "string"
2109
+ };
2110
+ var HANDOFF_FIELDS = { to: "string", tool: "string", description: "string" };
2111
+ var SUBAGENT_FIELDS = { agent: "string", tool: "string", description: "string" };
2112
+ var MATCH_FIELDS = { agentNames: "string[]", promptKeys: "string[]" };
2113
+ var ROUTING_FIELDS = { strategy: "string", default: "string", notes: "string" };
2114
+ var RUNTIME_FIELDS = {
2115
+ framework: "string",
2116
+ language: "string",
2117
+ maxToolRoundsPerTurn: "number",
2118
+ parallelToolCalls: "boolean",
2119
+ history: "object",
2120
+ skillLoading: "string",
2121
+ notes: "string"
2122
+ };
2123
+ var HISTORY_FIELDS = { strategy: "string", maxMessages: "number", maxTokens: "number" };
2124
+ var CONTEXT_FIELDS = {
2125
+ key: "string",
2126
+ variable: "string",
2127
+ source: "string",
2128
+ value: "string",
2129
+ format: "string",
2130
+ target: "string",
2131
+ description: "string"
2132
+ };
2133
+ var MCP_FIELDS = {
2134
+ name: "string",
2135
+ transport: "string",
2136
+ url: "string",
2137
+ command: "string",
2138
+ tools: "string[]",
2139
+ description: "string"
2140
+ };
2141
+ function handoffToolName(handoff) {
2142
+ return handoff.tool?.trim() || `transfer_to_${handoff.to.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 64);
2143
+ }
2144
+ function hasHarnessSections(input) {
2145
+ return HARNESS_SECTIONS.some((section) => input[section] !== undefined);
2146
+ }
2147
+ function validateHarnessSections(input, known, errors, warnings) {
2148
+ if (!hasHarnessSections(input))
2149
+ return;
2150
+ const document = {};
2151
+ if (input.runtime !== undefined) {
2152
+ if (checkObject("runtime", input.runtime, RUNTIME_FIELDS, errors)) {
2153
+ const runtime = input.runtime;
2154
+ checkPositiveInt("runtime.maxToolRoundsPerTurn", runtime.maxToolRoundsPerTurn, errors);
2155
+ checkEnum("runtime.skillLoading", runtime.skillLoading, SKILL_LOADING_MODES, errors);
2156
+ if (runtime.history !== undefined && checkObject("runtime.history", runtime.history, HISTORY_FIELDS, errors)) {
2157
+ checkEnum("runtime.history.strategy", runtime.history.strategy, HISTORY_STRATEGIES, errors);
2158
+ checkPositiveInt("runtime.history.maxMessages", runtime.history.maxMessages, errors);
2159
+ checkPositiveInt("runtime.history.maxTokens", runtime.history.maxTokens, errors);
2160
+ }
2161
+ document.runtime = runtime;
2162
+ }
2163
+ }
2164
+ const contextKeys = new Set;
2165
+ if (input.context !== undefined) {
2166
+ const entries = listOf("context", input.context, MAX_CONTEXT, errors);
2167
+ document.context = [];
2168
+ entries.forEach((raw, index) => {
2169
+ const label = `context[${index}]`;
2170
+ if (!checkObject(label, raw, CONTEXT_FIELDS, errors))
2171
+ return;
2172
+ const entry = raw;
2173
+ if (!nonEmpty(entry.key))
2174
+ errors.push(`${label}.key is required`);
2175
+ else if (contextKeys.has(entry.key))
2176
+ errors.push(`${label}: duplicate context key "${entry.key}"`);
2177
+ else
2178
+ contextKeys.add(entry.key);
2179
+ if (!nonEmpty(entry.source))
2180
+ errors.push(`${label}.source is required (one of ${CONTEXT_SOURCES.join(", ")})`);
2181
+ else
2182
+ checkEnum(`${label}.source`, entry.source, CONTEXT_SOURCES, errors);
2183
+ checkEnum(`${label}.target`, entry.target, CONTEXT_TARGETS, errors);
2184
+ checkEnum(`${label}.format`, entry.format, CLOCK_FORMATS, errors);
2185
+ const target = entry.target ?? "prompt_variable";
2186
+ if (target === "prompt_variable") {
2187
+ if (!nonEmpty(entry.variable))
2188
+ errors.push(`${label}.variable is required for target "prompt_variable"`);
2189
+ else if (!VARIABLE_PATTERN.test(entry.variable))
2190
+ errors.push(`${label}.variable "${entry.variable}" is not a valid template variable name`);
2191
+ }
2192
+ if (entry.source === "static" && typeof entry.value !== "string")
2193
+ errors.push(`${label}.value is required for source "static"`);
2194
+ if (entry.source !== "static" && entry.value !== undefined) {
2195
+ errors.push(`${label}.value is only allowed for source "static" (record runtime values in traces instead)`);
2196
+ }
2197
+ document.context.push(entry);
2198
+ });
2199
+ }
2200
+ const agentKeys = new Set;
2201
+ const agents = input.agents === undefined ? [] : listOf("agents", input.agents, MAX_AGENTS, errors);
2202
+ for (const raw of agents) {
2203
+ if (isRecord(raw) && nonEmpty(raw.key) && AGENT_KEY_PATTERN.test(raw.key))
2204
+ agentKeys.add(raw.key);
2205
+ }
2206
+ if (input.agents !== undefined) {
2207
+ document.agents = [];
2208
+ const seen = new Set;
2209
+ let entries = 0;
2210
+ agents.forEach((raw, index) => {
2211
+ const label = `agents[${index}]`;
2212
+ if (!checkObject(label, raw, AGENT_FIELDS, errors))
2213
+ return;
2214
+ const agent = raw;
2215
+ if (!nonEmpty(agent.key))
2216
+ errors.push(`${label}.key is required`);
2217
+ else if (!AGENT_KEY_PATTERN.test(agent.key))
2218
+ errors.push(`${label}.key "${agent.key}" may only contain letters, digits, ".", "_" and "-" (max 128)`);
2219
+ else if (seen.has(agent.key))
2220
+ errors.push(`${label}: duplicate agent key "${agent.key}"`);
2221
+ else
2222
+ seen.add(agent.key);
2223
+ if (agent.entry === true)
2224
+ entries += 1;
2225
+ if (agent.model !== undefined && checkObject(`${label}.model`, agent.model, MODEL_FIELDS, errors)) {
2226
+ const model = agent.model;
2227
+ if (typeof model.temperature === "number" && (model.temperature < 0 || model.temperature > 2)) {
2228
+ errors.push(`${label}.model.temperature must be between 0 and 2`);
2229
+ }
2230
+ if (typeof model.topP === "number" && (model.topP <= 0 || model.topP > 1))
2231
+ errors.push(`${label}.model.topP must be in (0, 1]`);
2232
+ checkPositiveInt(`${label}.model.maxTokens`, model.maxTokens, errors);
2233
+ }
2234
+ checkPositiveInt(`${label}.maxToolRoundsPerTurn`, agent.maxToolRoundsPerTurn, errors);
2235
+ if (agent.match !== undefined)
2236
+ checkObject(`${label}.match`, agent.match, MATCH_FIELDS, errors);
2237
+ const handoffTools = new Set;
2238
+ (Array.isArray(agent.handoffs) ? agent.handoffs : []).forEach((handoff, hIndex) => {
2239
+ const hLabel = `${label}.handoffs[${hIndex}]`;
2240
+ if (!checkObject(hLabel, handoff, HANDOFF_FIELDS, errors))
2241
+ return;
2242
+ if (!nonEmpty(handoff.to)) {
2243
+ errors.push(`${hLabel}.to is required`);
2244
+ return;
2245
+ }
2246
+ if (!agentKeys.has(handoff.to))
2247
+ errors.push(`${hLabel}.to "${handoff.to}" is not an agent key in this manifest`);
2248
+ if (handoff.to === agent.key)
2249
+ errors.push(`${hLabel}: an agent cannot hand off to itself`);
2250
+ const tool = handoffToolName(handoff);
2251
+ if (!TOOL_NAME_PATTERN.test(tool))
2252
+ errors.push(`${hLabel}.tool "${tool}" must match ${TOOL_NAME_PATTERN.source}`);
2253
+ else if (handoffTools.has(tool))
2254
+ errors.push(`${hLabel}: duplicate handoff tool "${tool}"`);
2255
+ else if ((agent.tools ?? []).includes(tool))
2256
+ errors.push(`${hLabel}: handoff tool "${tool}" collides with a tool in ${label}.tools`);
2257
+ handoffTools.add(tool);
2258
+ });
2259
+ (Array.isArray(agent.subagents) ? agent.subagents : []).forEach((subagent, sIndex) => {
2260
+ const sLabel = `${label}.subagents[${sIndex}]`;
2261
+ if (!checkObject(sLabel, subagent, SUBAGENT_FIELDS, errors))
2262
+ return;
2263
+ if (!nonEmpty(subagent.agent))
2264
+ errors.push(`${sLabel}.agent is required`);
2265
+ else if (!agentKeys.has(subagent.agent))
2266
+ errors.push(`${sLabel}.agent "${subagent.agent}" is not an agent key in this manifest`);
2267
+ if (!nonEmpty(subagent.tool))
2268
+ errors.push(`${sLabel}.tool is required (the tool name the parent calls)`);
2269
+ else if (!(agent.tools ?? []).includes(subagent.tool)) {
2270
+ errors.push(`${sLabel}.tool "${subagent.tool}" must also be listed in ${label}.tools (with its schema in tools[])`);
2271
+ }
2272
+ });
2273
+ for (const key of agent.context ?? []) {
2274
+ if (!contextKeys.has(key))
2275
+ errors.push(`${label}.context "${key}" is not a key in context[]`);
2276
+ }
2277
+ if (nonEmpty(agent.prompt) && !known.prompts.has(agent.prompt)) {
2278
+ warnings.push(`${label}.prompt "${agent.prompt}" is not in prompts[]; it must already exist in Moda`);
2279
+ }
2280
+ for (const tool of agent.tools ?? []) {
2281
+ if (!known.tools.has(tool))
2282
+ warnings.push(`${label}.tools "${tool}" is not in tools[]; it must already exist in Moda`);
2283
+ }
2284
+ for (const skill of agent.skills ?? []) {
2285
+ if (!known.skills.has(skill))
2286
+ warnings.push(`${label}.skills "${skill}" is not in skills[]; it must already exist in Moda`);
2287
+ }
2288
+ document.agents.push(agent);
2289
+ });
2290
+ if (entries > 1)
2291
+ errors.push('agents: at most one agent may set "entry": true');
2292
+ }
2293
+ if (input.routing !== undefined) {
2294
+ if (checkObject("routing", input.routing, ROUTING_FIELDS, errors)) {
2295
+ const routing = input.routing;
2296
+ checkEnum("routing.strategy", routing.strategy, ROUTING_STRATEGIES, errors);
2297
+ if (routing.default !== undefined && !agentKeys.has(routing.default)) {
2298
+ errors.push(`routing.default "${routing.default}" is not an agent key in agents[]`);
2299
+ }
2300
+ document.routing = routing;
2301
+ }
2302
+ }
2303
+ if (input.mcpServers !== undefined) {
2304
+ document.mcpServers = [];
2305
+ const names = new Set;
2306
+ listOf("mcpServers", input.mcpServers, MAX_MCP_SERVERS, errors).forEach((raw, index) => {
2307
+ const label = `mcpServers[${index}]`;
2308
+ if (!checkObject(label, raw, MCP_FIELDS, errors))
2309
+ return;
2310
+ const server = raw;
2311
+ if (!nonEmpty(server.name))
2312
+ errors.push(`${label}.name is required`);
2313
+ else if (names.has(server.name))
2314
+ errors.push(`${label}: duplicate MCP server name "${server.name}"`);
2315
+ else
2316
+ names.add(server.name);
2317
+ checkEnum(`${label}.transport`, server.transport, MCP_TRANSPORTS, errors);
2318
+ if (server.url !== undefined && hasUrlCredentials(server.url)) {
2319
+ errors.push(`${label}.url must not contain credentials (user info or token query parameters)`);
2320
+ }
2321
+ document.mcpServers.push(server);
2322
+ });
2323
+ }
2324
+ if (Buffer.byteLength(JSON.stringify(document), "utf8") > MAX_HARNESS_DOCUMENT_BYTES) {
2325
+ errors.push(`harness sections exceed ${MAX_HARNESS_DOCUMENT_BYTES} bytes`);
2326
+ }
2327
+ return document;
2328
+ }
2329
+ function hasUrlCredentials(raw) {
2330
+ try {
2331
+ const url = new URL(raw);
2332
+ if (url.username || url.password)
2333
+ return true;
2334
+ return [...url.searchParams.keys()].some((key) => /token|key|secret|password|auth/i.test(key));
2335
+ } catch {
2336
+ return false;
2337
+ }
2338
+ }
2339
+ function listOf(label, value, max, errors) {
2340
+ if (!Array.isArray(value)) {
2341
+ errors.push(`${label} must be an array`);
2342
+ return [];
2343
+ }
2344
+ if (value.length > max)
2345
+ errors.push(`${label} has ${value.length} entries; the limit is ${max}`);
2346
+ return value;
2347
+ }
2348
+ function checkObject(label, value, spec, errors) {
2349
+ if (!isRecord(value)) {
2350
+ errors.push(`${label} must be an object`);
2351
+ return false;
2352
+ }
2353
+ let typed = true;
2354
+ for (const [key, item] of Object.entries(value)) {
2355
+ const expected = spec[key];
2356
+ if (!expected) {
2357
+ errors.push(`${label}: unknown field "${key}" (allowed: ${Object.keys(spec).join(", ")})`);
2358
+ continue;
2359
+ }
2360
+ const ok = expected === "string[]" ? Array.isArray(item) && item.every((entry) => typeof entry === "string") : expected === "object[]" ? Array.isArray(item) && item.every(isRecord) : expected === "object" ? isRecord(item) : expected === "number" ? typeof item === "number" && Number.isFinite(item) : typeof item === expected;
2361
+ if (!ok) {
2362
+ errors.push(`${label}.${key} must be ${describe(expected)}`);
2363
+ typed = false;
2364
+ }
2365
+ }
2366
+ return typed;
2367
+ }
2368
+ function describe(type) {
2369
+ if (type === "string[]")
2370
+ return "an array of strings";
2371
+ if (type === "object[]")
2372
+ return "an array of objects";
2373
+ return type === "object" ? "an object" : `a ${type}`;
2374
+ }
2375
+ function checkEnum(label, value, allowed, errors) {
2376
+ if (value === undefined || typeof value !== "string")
2377
+ return;
2378
+ if (!allowed.includes(value))
2379
+ errors.push(`${label} must be one of ${allowed.join(", ")} (got "${value}")`);
2380
+ }
2381
+ function checkPositiveInt(label, value, errors) {
2382
+ if (value === undefined || typeof value !== "number")
2383
+ return;
2384
+ if (!Number.isInteger(value) || value <= 0)
2385
+ errors.push(`${label} must be a positive integer`);
2386
+ }
2387
+ function isRecord(value) {
2388
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2389
+ }
2390
+ function nonEmpty(value) {
2391
+ return typeof value === "string" && value.trim() !== "";
2392
+ }
2393
+
2394
+ // src/registry-prompt.ts
2395
+ var REGISTRY_AGENT_BRIEF = `# Register this repo's AI harness with Moda for faithful replays
2396
+
2397
+ You are working in the repository that runs our production AI agent. Moda
2398
+ replays recorded production conversations against our agent to test prompt,
2399
+ tool, and skill changes. A replay is only as faithful as what Moda knows about
2400
+ how our agent actually runs. Your job: describe our harness to Moda exactly,
2401
+ push it with the \`moda\` CLI, and make sure our production traces say which
2402
+ agent and prompt handled each LLM call.
2403
+
2404
+ ## Ground rules
2405
+
2406
+ - Read the code; do not guess. Every value you register must come from the
2407
+ repo (cite the file in \`sourcePath\` or in a note). If something is decided
2408
+ at runtime and you can't find it, say so in your final report.
2409
+ - Copy prompt text, tool names, tool descriptions, and parameter schemas
2410
+ verbatim. For templated prompts, keep the template with its placeholders
2411
+ (\`{{name}}\`, \`{name}\`, \`\${name}\`). Never register a rendered example.
2412
+ - Never put secrets in the manifest: no API keys, tokens, auth headers, env var
2413
+ values, or customer data.
2414
+ - Do not change runtime behaviour. Part 5 adds trace metadata only. Show the
2415
+ diff and get my OK before committing it.
2416
+ - Only \`.moda/registry.json\` (plus the files it references) is uploaded.
2417
+ Nothing else in the repo leaves this machine.
2418
+
2419
+ ## Part 0: setup
2420
+
2421
+ 1. Run \`moda --version\`. You need 1.44.0 or newer. If it's older, run
2422
+ \`npm install -g @moda-ai/cli@latest\`.
2423
+ 2. Auth: \`MODA_API_KEY\` must be set, or run \`moda auth login --api-key=<key>\`.
2424
+ Ask me for the key if neither is available. Check with \`moda doctor\`.
2425
+ 3. Run \`moda registry template\` to see a complete example manifest, and
2426
+ \`moda registry status\` to see what is already registered.
2427
+
2428
+ ## Part 1: inventory the harness
2429
+
2430
+ Find every place the code calls an LLM: direct SDK calls (OpenAI, Anthropic,
2431
+ Gemini, Bedrock, OpenRouter), framework agents, and anything else. For each
2432
+ call site, write down:
2433
+
2434
+ - **Agent**: the logical agent it belongs to, with a stable key such as
2435
+ \`triage\` or \`billing\`. Different system prompts, tool sets, or models mean
2436
+ different agents. A one-off call (summariser, classifier, title generator)
2437
+ is its own agent if it runs during a conversation.
2438
+ - **Prompt**: the system prompt or messages it sends, how they're built
2439
+ (static text, template plus variables, or code that assembles them), and
2440
+ the file.
2441
+ - **Model and settings**: model id, provider, temperature, max tokens, top_p,
2442
+ and reasoning effort, wherever they're set (code, config, env defaults).
2443
+ - **Tools**: every tool the model can call at that site, with the exact JSON
2444
+ schema the model sees. Include MCP tools and agents exposed as tools.
2445
+ - **Skills**: SKILL.md files or instruction docs the agent loads, and HOW it
2446
+ loads them (a tool such as skill_read or read_file, inlined into the system
2447
+ prompt, or not at all).
2448
+ - **Loop limits**: max tool rounds or steps per turn, parallel tool calls, and
2449
+ how history is truncated or summarised.
2450
+
2451
+ Framework hints:
2452
+
2453
+ - **OpenAI Agents SDK**: \`Agent(name, instructions, model, model_settings,
2454
+ tools, handoffs)\`. \`name\` is the trace agent name. Handoffs become
2455
+ \`transfer_to_<agent>\` tools.
2456
+ - **Claude Agent SDK / Claude Code SDK**: \`ClaudeAgentOptions(system_prompt,
2457
+ model, allowed_tools, mcp_servers, agents=...)\`. Subagents are \`agents\`,
2458
+ and skills live in \`.claude/skills/*/SKILL.md\`.
2459
+ - **LangGraph / LangChain**: each node that binds an LLM (\`bind_tools\`,
2460
+ \`create_react_agent\`) is an agent. Conditional edges are the routing.
2461
+ - **Vercel AI SDK**: \`generateText\` / \`streamText({ system, tools, model,
2462
+ maxSteps | stopWhen })\`.
2463
+ - **Raw SDK calls**: the system message, \`tools=\`, and \`model=\` arguments.
2464
+
2465
+ ## Part 2: work out prompt switching
2466
+
2467
+ Work out how the harness decides which agent and prompt answers a given turn.
2468
+ That becomes \`routing\` and the agents' \`handoffs\`:
2469
+
2470
+ - **single**: one agent always answers. Prompt versions still change over
2471
+ time; Part 5 handles that.
2472
+ - **handoff**: the model transfers control by calling a tool (OpenAI Agents
2473
+ handoffs, a "transfer_to_*" function, or a supervisor delegating). Add
2474
+ \`handoffs: [{ "to": "<agent key>", "tool": "<exact tool name>",
2475
+ "description": "<when>" }]\` to the source agent.
2476
+ - **router / rules**: code picks the agent (by channel, intent classifier,
2477
+ conversation stage, user plan, or feature flag). Describe the rule in
2478
+ \`routing.notes\` and make sure Part 5's attribution names the chosen agent
2479
+ on every call.
2480
+ - **trace_attribute**: the agent is already identified on every trace.
2481
+ - **subagents** (agents as tools): the parent calls a tool that runs another
2482
+ agent and returns its result. Register that tool's schema in \`tools[]\`,
2483
+ list it in the parent's \`tools\`, and add \`subagents: [{ "agent": "<child
2484
+ key>", "tool": "<tool name>" }]\`.
2485
+
2486
+ Set \`routing.default\` to the agent that answers when nothing else decides,
2487
+ and mark the first agent a conversation reaches with \`"entry": true\`.
2488
+
2489
+ ## Part 3: list the injected context
2490
+
2491
+ For every placeholder in every prompt, and anything appended to the prompt at
2492
+ runtime, add a \`context[]\` entry:
2493
+
2494
+ - \`source: "static"\`: the same value every time (put it in \`value\`).
2495
+ - \`source: "clock"\`: the current date or time (\`format\`: date, datetime, or
2496
+ iso). Replay uses the original conversation's time.
2497
+ - \`source: "trace"\`: a per-user or per-conversation value, such as the
2498
+ profile, account, plan, or locale.
2499
+ - \`source: "memory" | "retrieval" | "tool"\`: memory recall, RAG results, or a
2500
+ tool fetched before the call.
2501
+ - \`target: "prompt_variable"\` (with \`variable\` set to the placeholder name,
2502
+ without braces) for template variables, or \`target: "system_append"\` for
2503
+ blocks the code appends.
2504
+
2505
+ List context keys on an agent with \`context: [...]\` when only some agents get
2506
+ them.
2507
+
2508
+ ## Part 4: write, validate, and push the manifest
2509
+
2510
+ Write \`.moda/registry.json\` with \`"schema": "moda_registry.v2"\`:
2511
+
2512
+ - \`prompts[]\`: \`{ key, file | content | systemPrompt | messages, name,
2513
+ description, variables, modelConfig, responseSchema, sourcePath }\`. If the
2514
+ repo already uses \`moda prompts\` (it has a \`.moda/prompts.lock.json\`),
2515
+ reuse those keys.
2516
+ - \`tools[]\`: \`{ schema: { name, description, parameters } }\`, exactly as
2517
+ sent to the model.
2518
+ - \`skills[]\`: \`{ key, file: "path/to/SKILL.md" }\`. Set \`"live": true\` only
2519
+ if I ask.
2520
+ - \`agents[]\`: \`{ key, name, entry, prompt, model: { id, provider,
2521
+ temperature, maxTokens, topP, reasoningEffort }, tools, skills, handoffs,
2522
+ subagents, context, maxToolRoundsPerTurn, responseSchema, match: {
2523
+ agentNames, promptKeys } }\`. \`match.agentNames\` are the names this agent
2524
+ carries in traces; \`match.promptKeys\` are prompt keys that identify it.
2525
+ - \`routing\`: \`{ strategy, default, notes }\`.
2526
+ - \`runtime\`: \`{ framework, language, maxToolRoundsPerTurn,
2527
+ parallelToolCalls, history: { strategy, maxMessages, maxTokens },
2528
+ skillLoading: "tool" | "system_prompt" | "none" }\`.
2529
+ - \`context[]\`: from Part 3.
2530
+ - \`mcpServers[]\`: \`{ name, transport, url | command, tools, description }\`,
2531
+ with no credentials. Their tools still go in \`tools[]\`.
2532
+
2533
+ Then run:
2534
+
2535
+ moda registry validate # local; fix every error and read every warning
2536
+ moda registry push --dry-run # server-side preview, writes nothing
2537
+ moda registry push --message="<what you registered or changed>"
2538
+
2539
+ Re-pushing an unchanged manifest is a no-op. A changed prompt adds a new
2540
+ version, and a changed harness adds a new harness version.
2541
+
2542
+ ## Part 5: make traces name the agent and prompt version
2543
+
2544
+ Replay picks the agent and prompt version that handled the replayed turn from
2545
+ attributes on each LLM call. Auto-instrumented calls don't carry them, so add
2546
+ them at each call site from Part 1:
2547
+
2548
+ | What | OTel span attribute | Vercel AI SDK \`experimental_telemetry.metadata\` | \`/v1/ingest\` event field |
2549
+ |---|---|---|---|
2550
+ | Agent | \`gen_ai.agent.name\` (or \`moda.agent_name\`) | \`moda.agent_name\` | \`agent_name\` |
2551
+ | Prompt key | \`moda.prompt_key\` | \`moda.prompt_key\` | \`prompt_name\` |
2552
+ | Prompt version | \`moda.prompt_version_id\` | \`moda.prompt_version_id\` | \`prompt_version_id\` |
2553
+
2554
+ - The agent value must be the agent's \`key\` or one of its
2555
+ \`match.agentNames\`. Frameworks that already set \`gen_ai.agent.name\`
2556
+ (OpenAI Agents SDK, many OpenInference/OpenLLMetry instrumentations) only
2557
+ need \`match.agentNames\` to list those names.
2558
+ - Get the prompt version id from \`.moda/prompts.lock.json\` (written by
2559
+ \`moda prompts sync\`), or leave it out. Replay then uses the prompt's prod
2560
+ version.
2561
+ - Node with the Moda SDK: \`Moda.withLLMCall(..., ({ span }) =>
2562
+ span.rawSpan.setAttribute(...))\`, or pass Vercel metadata through
2563
+ \`Moda.getVercelAITelemetry({ metadata })\`.
2564
+ - Python: set the attributes in your OpenTelemetry instrumentation on the LLM
2565
+ span, or send \`agent_name\` / \`prompt_*\` on \`/v1/ingest\` events.
2566
+ - Show me the diff before committing it.
2567
+
2568
+ ## Part 6: verify
2569
+
2570
+ moda registry status
2571
+ moda registry coverage --days=7
2572
+ moda registry log # each push is a commit; review what changed
2573
+
2574
+ Coverage compares the last N days of production traces with what you
2575
+ registered: the share of LLM calls naming an agent or prompt, agent names and
2576
+ prompt keys that match no registered agent, tools used in production but not
2577
+ registered, and registered agents never seen. Fix the manifest (or the
2578
+ attribution) and push again until there is nothing unexplained left. Attribution
2579
+ added in Part 5 only shows up on traces created after it ships.
2580
+
2581
+ ## Final report
2582
+
2583
+ Reply with:
2584
+
2585
+ 1. The agents you registered: key, prompt, model, tool count, skill count, and
2586
+ handoffs.
2587
+ 2. The routing strategy, and how you verified it.
2588
+ 3. Context entries whose values replay can't reproduce (\`trace\`, \`memory\`,
2589
+ \`retrieval\`, \`tool\`).
2590
+ 4. The attribution diff (or that it's pending my OK).
2591
+ 5. The \`moda registry coverage\` result, plus anything unexplained you
2592
+ couldn't resolve.
2593
+ 6. Anything you found but couldn't express in the manifest.
2594
+ `;
2595
+
2596
+ // src/registry.ts
2597
+ var REGISTRY_SCHEMA = "moda_registry.v2";
2598
+ var REGISTRY_SCHEMA_V1 = "moda_registry.v1";
2068
2599
  var DEFAULT_REGISTRY_PATH = ".moda/registry.json";
2069
2600
  var SKILL_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
2070
2601
  var MAX_ITEMS_PER_SECTION = 500;
@@ -2075,7 +2606,7 @@ var REGISTRY_TEMPLATE = {
2075
2606
  key: "support.triage",
2076
2607
  name: "Support triage system prompt",
2077
2608
  description: "Routes inbound support messages",
2078
- content: "You are a support triage agent. Classify the request and ...",
2609
+ content: "You are a support triage agent for {{company_name}}. Today is {{current_date}}. Classify the request and ...",
2079
2610
  sourcePath: "src/agents/triage/prompt.ts"
2080
2611
  },
2081
2612
  {
@@ -2108,12 +2639,49 @@ description: How to process refund requests
2108
2639
  }
2109
2640
  }
2110
2641
  }
2642
+ ],
2643
+ runtime: {
2644
+ framework: "openai-agents",
2645
+ language: "python",
2646
+ maxToolRoundsPerTurn: 6,
2647
+ history: { strategy: "window", maxMessages: 40 },
2648
+ skillLoading: "tool"
2649
+ },
2650
+ agents: [
2651
+ {
2652
+ key: "triage",
2653
+ name: "Triage",
2654
+ entry: true,
2655
+ prompt: "support.triage",
2656
+ model: { id: "openai/gpt-5.1", temperature: 0.2, maxTokens: 1024 },
2657
+ tools: ["lookup_customer"],
2658
+ handoffs: [{ to: "reply", description: "Hand off once the request is classified" }],
2659
+ match: { agentNames: ["TriageAgent"] }
2660
+ },
2661
+ {
2662
+ key: "reply",
2663
+ name: "Reply writer",
2664
+ prompt: "support.reply",
2665
+ model: { id: "anthropic/claude-sonnet-5-5", temperature: 0.4, maxTokens: 2048 },
2666
+ skills: ["refund-policy"],
2667
+ match: { agentNames: ["ReplyAgent"] }
2668
+ }
2669
+ ],
2670
+ routing: { strategy: "handoff", default: "triage" },
2671
+ context: [
2672
+ { key: "today", variable: "current_date", source: "clock", format: "date" },
2673
+ { key: "company", variable: "company_name", source: "static", value: "Acme" }
2111
2674
  ]
2112
2675
  };
2113
2676
  var REGISTRY_SUBCOMMAND_FLAGS = {
2114
2677
  template: ["out", "dry-run"],
2115
2678
  validate: ["file", "manifest"],
2116
- push: ["file", "manifest", "live", "dry-run"]
2679
+ push: ["file", "manifest", "live", "dry-run", "message"],
2680
+ log: ["limit"],
2681
+ show: [],
2682
+ status: [],
2683
+ coverage: ["days"],
2684
+ prompt: []
2117
2685
  };
2118
2686
  async function runRegistryCommand(context) {
2119
2687
  const subcommand = context.positionals[0] ?? "push";
@@ -2123,18 +2691,52 @@ async function runRegistryCommand(context) {
2123
2691
  return writeTemplate(context);
2124
2692
  case "validate": {
2125
2693
  const registry = loadRegistry(context);
2694
+ printWarnings(context, registry.warnings);
2126
2695
  context.output.writeData({
2127
- schema: REGISTRY_SCHEMA,
2696
+ schema: registry.schema,
2128
2697
  manifest: displayPath(context, registry.manifestPath),
2129
2698
  valid: true,
2130
- counts: countsOf(registry)
2699
+ counts: countsOf(registry),
2700
+ warnings: registry.warnings
2131
2701
  });
2132
2702
  return { exitCode: 0 };
2133
2703
  }
2134
2704
  case "push":
2135
2705
  return pushRegistry(context);
2706
+ case "status":
2707
+ return readRegistry(context, "/harness-registry");
2708
+ case "log": {
2709
+ const limit = context.flags.limit;
2710
+ if (limit !== undefined && !/^\d+$/.test(limit)) {
2711
+ throw new CliInputError("--limit must be a whole number.", "Usage: moda registry log [--limit=20]");
2712
+ }
2713
+ return readRegistry(context, `/registry/commits${limit ? `?limit=${limit}` : ""}`);
2714
+ }
2715
+ case "show": {
2716
+ const ref = context.positionals[1];
2717
+ if (!ref) {
2718
+ throw new CliInputError("Missing commit.", 'Usage: moda registry show <commit> (a hash from `moda registry log`, or "working")');
2719
+ }
2720
+ return readRegistry(context, ref === "working" ? "/registry/compare?to=working" : `/registry/commits/${encodeURIComponent(ref)}`);
2721
+ }
2722
+ case "coverage": {
2723
+ const days = context.flags.days;
2724
+ if (days !== undefined && !/^\d+$/.test(days)) {
2725
+ throw new CliInputError("--days must be a whole number of days.", "Usage: moda registry coverage [--days=7]");
2726
+ }
2727
+ return readRegistry(context, `/harness-registry/coverage${days ? `?days=${days}` : ""}`);
2728
+ }
2729
+ case "prompt": {
2730
+ const mode = explicitOutputMode(context.flags, context.env);
2731
+ if (mode && mode !== "human")
2732
+ context.output.writeData({ prompt: REGISTRY_AGENT_BRIEF });
2733
+ else if (!context.quiet)
2734
+ process.stdout.write(`${REGISTRY_AGENT_BRIEF}
2735
+ `);
2736
+ return { exitCode: 0 };
2737
+ }
2136
2738
  default:
2137
- throw new CliInputError(`Unknown registry command '${subcommand}'.`, "Usage: moda registry template|validate|push [--file=.moda/registry.json] [--dry-run] [--live]");
2739
+ throw new CliInputError(`Unknown registry command '${subcommand}'.`, "Usage: moda registry template|validate|push|status|coverage|log|show|prompt [--file=.moda/registry.json] [--dry-run] [--live]");
2138
2740
  }
2139
2741
  }
2140
2742
  function writeTemplate(context) {
@@ -2159,6 +2761,9 @@ function writeTemplate(context) {
2159
2761
  return { exitCode: 0 };
2160
2762
  }
2161
2763
  async function pushRegistry(context) {
2764
+ const message = context.flags.message;
2765
+ if (message === "true")
2766
+ throw new CliInputError("Missing text for --message.", 'Usage: moda registry push --message="Add billing agent"');
2162
2767
  const registry = loadRegistry(context);
2163
2768
  const profileOptions = { profile: context.profile, env: context.env };
2164
2769
  const hasKey = Boolean(resolveApiKey(profileOptions));
@@ -2191,7 +2796,20 @@ async function pushRegistry(context) {
2191
2796
  await push("prompts", "/prompts/sync", registry.prompts.length, { prompts: registry.prompts });
2192
2797
  await push("skills", "/skills/sync", registry.skills.length, { skills: registry.skills });
2193
2798
  await push("tools", "/tool-definitions/sync", registry.tools.length, { tools: registry.tools });
2799
+ if (registry.harness) {
2800
+ await push("harness", "/harness-registry/sync", 1, { schema: registry.schema, document: registry.harness });
2801
+ }
2802
+ let commit;
2803
+ let commitError;
2804
+ if (!context.dryRun && sections.some((section) => section.status === "synced")) {
2805
+ try {
2806
+ commit = await callControlAPI("/registry/commits", { method: "POST", body: JSON.stringify({ ...message ? { message } : {}, ...gitInfo(context.cwd) }) }, profileOptions);
2807
+ } catch (error) {
2808
+ commitError = error instanceof Error ? error.message : String(error);
2809
+ }
2810
+ }
2194
2811
  const failed = sections.filter((section) => section.status === "error");
2812
+ printWarnings(context, registry.warnings);
2195
2813
  if (context.outputMode === "human" && !context.quiet) {
2196
2814
  for (const section of sections) {
2197
2815
  const detail = section.status === "error" ? ` — ${section.error}` : describeActions(section.response);
@@ -2200,13 +2818,26 @@ async function pushRegistry(context) {
2200
2818
  if (sections.some((section) => section.section === "tools" && section.status === "synced")) {
2201
2819
  console.error(" Replays now give your agent these tools with their real schemas and descriptions.");
2202
2820
  }
2821
+ if (sections.some((section) => section.section === "harness" && section.status === "synced")) {
2822
+ console.error(" Replays now rebuild the agent live at each replayed turn. Check `moda registry coverage`.");
2823
+ }
2824
+ const recorded = isRecord2(commit) && isRecord2(commit.commit) ? commit.commit : undefined;
2825
+ if (recorded) {
2826
+ const label = commit && isRecord2(commit) && commit.action === "unchanged" ? "no registry changes since" : "commit";
2827
+ console.error(` ${label} ${String(recorded.id).slice(0, 7)} (#${String(recorded.seq)}) ${String(recorded.summary ?? "")}`);
2828
+ } else if (commitError) {
2829
+ console.error(` commit: not recorded — ${commitError}`);
2830
+ }
2203
2831
  }
2204
2832
  context.output.writeData({
2205
- schema: REGISTRY_SCHEMA,
2833
+ schema: registry.schema,
2206
2834
  manifest: displayPath(context, registry.manifestPath),
2207
2835
  dryRun: context.dryRun,
2208
2836
  counts: countsOf(registry),
2209
- sections
2837
+ sections,
2838
+ warnings: registry.warnings,
2839
+ ...commit !== undefined ? { commit } : {},
2840
+ ...commitError ? { commitError } : {}
2210
2841
  }, {
2211
2842
  status: failed.length > 0 ? "error" : "success",
2212
2843
  ...failed.length > 0 ? {
@@ -2219,11 +2850,39 @@ async function pushRegistry(context) {
2219
2850
  });
2220
2851
  return { exitCode: failed.length > 0 ? 1 : 0 };
2221
2852
  }
2853
+ function gitInfo(cwd) {
2854
+ const git = (...args) => execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).toString().trim();
2855
+ try {
2856
+ const gitCommit = git("rev-parse", "HEAD");
2857
+ const branch = git("rev-parse", "--abbrev-ref", "HEAD");
2858
+ return {
2859
+ gitCommit,
2860
+ ...branch && branch !== "HEAD" ? { gitBranch: branch } : {},
2861
+ gitDirty: git("status", "--porcelain").length > 0
2862
+ };
2863
+ } catch {
2864
+ return {};
2865
+ }
2866
+ }
2867
+ async function readRegistry(context, endpoint) {
2868
+ const profileOptions = { profile: context.profile, env: context.env };
2869
+ if (!resolveApiKey(profileOptions)) {
2870
+ throw new CliInputError("No Moda API key found.", "Set MODA_API_KEY, run `moda auth login --api-key=<key>`, or select a profile with an API key.");
2871
+ }
2872
+ context.output.writeData(await callControlAPI(endpoint, { method: "GET" }, profileOptions));
2873
+ return { exitCode: 0 };
2874
+ }
2875
+ function printWarnings(context, warnings) {
2876
+ if (context.outputMode !== "human" || context.quiet)
2877
+ return;
2878
+ for (const warning of warnings)
2879
+ console.error(` warning: ${warning}`);
2880
+ }
2222
2881
  function describeActions(response) {
2223
- const synced = isRecord(response) && Array.isArray(response.synced) ? response.synced : [];
2882
+ const synced = isRecord2(response) && Array.isArray(response.synced) ? response.synced : [];
2224
2883
  const counts = new Map;
2225
2884
  for (const item of synced) {
2226
- if (!isRecord(item))
2885
+ if (!isRecord2(item))
2227
2886
  continue;
2228
2887
  const action = typeof item.action === "string" ? `${item.action}${typeof item.reason === "string" ? ` (${item.reason})` : ""}` : item.changed === false ? "unchanged" : item.policyChanged === true && item.changed === true ? "changed (live setting)" : item.changed === true ? "changed" : "synced";
2229
2888
  counts.set(action, (counts.get(action) ?? 0) + 1);
@@ -2298,13 +2957,17 @@ function checkFields(label, item, spec, errors) {
2298
2957
  }
2299
2958
  function resolveRegistry(input, options) {
2300
2959
  const errors = [];
2301
- if (!isRecord(input)) {
2960
+ const warnings = [];
2961
+ if (!isRecord2(input)) {
2302
2962
  throw new CliInputError("Registry manifest must be a JSON object with prompts, skills, and/or tools arrays.");
2303
2963
  }
2304
- if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA) {
2305
- errors.push(`schema must be "${REGISTRY_SCHEMA}" (got ${JSON.stringify(input.schema)})`);
2964
+ if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA && input.schema !== REGISTRY_SCHEMA_V1) {
2965
+ errors.push(`schema must be "${REGISTRY_SCHEMA}" or "${REGISTRY_SCHEMA_V1}" (got ${JSON.stringify(input.schema)})`);
2966
+ }
2967
+ if (input.schema === REGISTRY_SCHEMA_V1 && hasHarnessSections(input)) {
2968
+ errors.push(`${HARNESS_SECTIONS.join(", ")} need schema "${REGISTRY_SCHEMA}"`);
2306
2969
  }
2307
- const known = new Set(["schema", "prompts", "skills", "tools"]);
2970
+ const known = new Set(["schema", "prompts", "skills", "tools", ...HARNESS_SECTIONS]);
2308
2971
  for (const key of Object.keys(input)) {
2309
2972
  if (!known.has(key))
2310
2973
  errors.push(`unknown top-level field "${key}"`);
@@ -2336,7 +2999,7 @@ function resolveRegistry(input, options) {
2336
2999
  const sourcePathFor = (file) => relative(options.cwd, resolve2(options.cwd, file)).split("\\").join("/");
2337
3000
  const prompts = arrayOf(input.prompts, "prompts", errors).map((raw, index) => {
2338
3001
  const label = `prompts[${index}]`;
2339
- if (!isRecord(raw)) {
3002
+ if (!isRecord2(raw)) {
2340
3003
  errors.push(`${label} must be an object`);
2341
3004
  return;
2342
3005
  }
@@ -2361,7 +3024,7 @@ function resolveRegistry(input, options) {
2361
3024
  });
2362
3025
  const skills = arrayOf(input.skills, "skills", errors).map((raw, index) => {
2363
3026
  const label = `skills[${index}]`;
2364
- if (!isRecord(raw)) {
3027
+ if (!isRecord2(raw)) {
2365
3028
  errors.push(`${label} must be an object`);
2366
3029
  return;
2367
3030
  }
@@ -2403,13 +3066,13 @@ function resolveRegistry(input, options) {
2403
3066
  const toolNames = new Set;
2404
3067
  const tools = arrayOf(input.tools, "tools", errors).map((raw, index) => {
2405
3068
  const label = `tools[${index}]`;
2406
- if (!isRecord(raw) || !isRecord(raw.schema)) {
3069
+ if (!isRecord2(raw) || !isRecord2(raw.schema)) {
2407
3070
  errors.push(`${label}.schema must be an object (OpenAI function schema)`);
2408
3071
  return;
2409
3072
  }
2410
3073
  checkFields(label, raw, TOOL_FIELDS, errors);
2411
3074
  const tool = raw;
2412
- const inner = tool.schema.type === "function" && isRecord(tool.schema.function) ? tool.schema.function : tool.schema;
3075
+ const inner = tool.schema.type === "function" && isRecord2(tool.schema.function) ? tool.schema.function : tool.schema;
2413
3076
  const schemaName = typeof inner.name === "string" ? inner.name.trim() : undefined;
2414
3077
  const name = (typeof tool.name === "string" ? tool.name.trim() : "") || schemaName;
2415
3078
  if (typeof tool.name === "string" && tool.name.trim() && schemaName && tool.name.trim() !== schemaName) {
@@ -2428,9 +3091,9 @@ function resolveRegistry(input, options) {
2428
3091
  errors.push(`${label}: schema.description must be a string`);
2429
3092
  }
2430
3093
  if (inner.parameters !== undefined) {
2431
- if (!isRecord(inner.parameters) || inner.parameters.type !== "object") {
3094
+ if (!isRecord2(inner.parameters) || inner.parameters.type !== "object") {
2432
3095
  errors.push(`${label}: schema.parameters.type must be "object"`);
2433
- } else if (inner.parameters.properties !== undefined && !isRecord(inner.parameters.properties)) {
3096
+ } else if (inner.parameters.properties !== undefined && !isRecord2(inner.parameters.properties)) {
2434
3097
  errors.push(`${label}: schema.parameters.properties must be an object`);
2435
3098
  }
2436
3099
  }
@@ -2452,6 +3115,7 @@ function resolveRegistry(input, options) {
2452
3115
  skillKeys.add(skill.key);
2453
3116
  }
2454
3117
  }
3118
+ const harness = validateHarnessSections(input, { prompts: promptKeys, tools: toolNames, skills: skillKeys }, errors, warnings);
2455
3119
  if (errors.length > 0) {
2456
3120
  throw new CliInputError(`Registry manifest is invalid:
2457
3121
  - ${errors.join(`
@@ -2459,12 +3123,15 @@ function resolveRegistry(input, options) {
2459
3123
  }
2460
3124
  const resolved = {
2461
3125
  manifestPath: options.manifestPath,
3126
+ schema: typeof input.schema === "string" ? input.schema : harness ? REGISTRY_SCHEMA : REGISTRY_SCHEMA_V1,
2462
3127
  prompts: prompts.filter(isDefined),
2463
3128
  skills: skills.filter(isDefined),
2464
- tools: tools.filter(isDefined)
3129
+ tools: tools.filter(isDefined),
3130
+ ...harness ? { harness } : {},
3131
+ warnings
2465
3132
  };
2466
- if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0) {
2467
- throw new CliInputError("Registry manifest has no prompts, skills, or tools to push.");
3133
+ if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0 && !harness) {
3134
+ throw new CliInputError("Registry manifest has no prompts, skills, tools, or agents to push.");
2468
3135
  }
2469
3136
  return resolved;
2470
3137
  }
@@ -2481,7 +3148,16 @@ function arrayOf(value, label, errors) {
2481
3148
  return value;
2482
3149
  }
2483
3150
  function countsOf(registry) {
2484
- return { prompts: registry.prompts.length, skills: registry.skills.length, tools: registry.tools.length };
3151
+ return {
3152
+ prompts: registry.prompts.length,
3153
+ skills: registry.skills.length,
3154
+ tools: registry.tools.length,
3155
+ ...registry.harness ? {
3156
+ agents: registry.harness.agents?.length ?? 0,
3157
+ context: registry.harness.context?.length ?? 0,
3158
+ mcpServers: registry.harness.mcpServers?.length ?? 0
3159
+ } : {}
3160
+ };
2485
3161
  }
2486
3162
  function displayPath(context, path) {
2487
3163
  const rel = relative(context.cwd, path);
@@ -2491,7 +3167,7 @@ function isInside(root, target) {
2491
3167
  const rel = relative(resolve2(root), target);
2492
3168
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
2493
3169
  }
2494
- function isRecord(value) {
3170
+ function isRecord2(value) {
2495
3171
  return typeof value === "object" && value !== null && !Array.isArray(value);
2496
3172
  }
2497
3173
  function nonEmptyString(value) {
@@ -7198,7 +7874,7 @@ Commands:
7198
7874
  skills sync Push local .claude/skills/**/SKILL.md up to Moda
7199
7875
  skills sync --dry-run Show what would be pushed without writing
7200
7876
  skills list|show|policy List pushed skills, show one, or set --live/--local
7201
- registry push Push prompts, skills, and tools from one JSON manifest
7877
+ registry push Push prompts, skills, tools, and agent wiring from one JSON manifest
7202
7878
 
7203
7879
  Options:
7204
7880
  --help Show this help message
@@ -7681,7 +8357,8 @@ async function runTailCommand(params, context) {
7681
8357
  message_count: messageCount,
7682
8358
  environment: conversation.environment ?? null,
7683
8359
  cluster_name: conversation.cluster_name ?? null,
7684
- summary: conversation.summary ?? null
8360
+ summary: conversation.summary ?? null,
8361
+ latest_task: conversation.latest_task ?? null
7685
8362
  });
7686
8363
  }
7687
8364
  };
@@ -7988,12 +8665,14 @@ async function searchFallbackToTraces(params) {
7988
8665
  match_grain: "trace",
7989
8666
  conversation_id: row.conversation_id,
7990
8667
  summary: row.summary ?? null,
8668
+ matched_task: row.matched_task ?? null,
8669
+ latest_task: row.latest_task ?? null,
7991
8670
  message_count: row.message_count ?? null,
7992
8671
  first_timestamp: row.first_timestamp ?? null,
7993
8672
  last_timestamp: row.last_timestamp ?? null
7994
8673
  }));
7995
8674
  const first = asString(rows[0].conversation_id);
7996
- const describe = literal ? "trace-level keyword matches (each trace contains the words)" : `trace-level ${traceMode}-ranked related traces (they may not contain the exact words; fallback.total is the candidate pool, not a match count)`;
8675
+ const describe2 = literal ? "trace-level keyword matches (each trace contains the words)" : `trace-level ${traceMode}-ranked related traces (they may not contain the exact words; fallback.total is the candidate pool, not a match count)`;
7997
8676
  const nextCommands = [];
7998
8677
  if (first) {
7999
8678
  nextCommands.push({ command: `moda audit ${first}`, purpose: "Open the top trace to locate the relevant message.", mutability: "read", requires_approval: false });
@@ -8018,7 +8697,7 @@ async function searchFallbackToTraces(params) {
8018
8697
  ...hasMore ? { next_offset: nextOffset } : {}
8019
8698
  }
8020
8699
  },
8021
- warning: `Message-level search returned 0 results, so these are ${describe}, from \`moda traces --search\`. ` + `Each result is a whole trace, not a message, with no score or message index. ` + `Open one with \`moda audit <conversation_id>\` or \`moda context <conversation_id>\` to find the matching turn.` + (hasMore ? " More exist: page with the suggested `moda traces --search ... --offset` command." : ""),
8700
+ warning: `Message-level search returned 0 results, so these are ${describe2}, from \`moda traces --search\`. ` + `Each result is a whole trace, not a message, with no score or message index. ` + `Open one with \`moda audit <conversation_id>\` or \`moda context <conversation_id>\` to find the matching turn.` + (hasMore ? " More exist: page with the suggested `moda traces --search ... --offset` command." : ""),
8022
8701
  nextCommands
8023
8702
  };
8024
8703
  }
@@ -9342,7 +10021,7 @@ var commandRegistry = createCommandRegistry([
9342
10021
  resetApiRequestCountBeforeRun: true,
9343
10022
  telemetry: "result",
9344
10023
  handler: async (context) => {
9345
- const { runInit } = await import("./index-nepr7cw7.js");
10024
+ const { runInit } = await import("./index-bcn5chmf.js");
9346
10025
  if (context.outputMode === "agent-stream") {
9347
10026
  context.output.writeEvent({
9348
10027
  event: "started",
@@ -9383,7 +10062,7 @@ var commandRegistry = createCommandRegistry([
9383
10062
  resetApiRequestCountBeforeRun: true,
9384
10063
  telemetry: "result-and-error",
9385
10064
  handler: async (context) => {
9386
- const { runProvision } = await import("./provision-02wqtkvm.js");
10065
+ const { runProvision } = await import("./provision-mqw8s0jb.js");
9387
10066
  return runProvision(context, {
9388
10067
  resumeCommand: buildResumeCommand("provision", context.rawFlags)
9389
10068
  });
@@ -9681,17 +10360,19 @@ var commandRegistry = createCommandRegistry([
9681
10360
  },
9682
10361
  {
9683
10362
  name: "registry",
9684
- description: "Push prompts, skills, and tools to Moda from one agent-written JSON manifest (no repo scan)",
10363
+ description: "Push prompts, skills, tools, and agent wiring (agents, routing, context) to Moda from one agent-written JSON manifest, so replays rebuild your harness (no repo scan)",
9685
10364
  examples: [
10365
+ "moda registry prompt",
9686
10366
  "moda registry template --out=.moda/registry.json",
9687
10367
  "moda registry validate",
9688
10368
  "moda registry push --dry-run",
9689
- "moda registry push --file=.moda/registry.json"
10369
+ "moda registry push --file=.moda/registry.json",
10370
+ "moda registry coverage --days=7"
9690
10371
  ],
9691
10372
  subcommands: [
9692
10373
  {
9693
10374
  name: "template",
9694
- description: "Print (or write with --out) an example moda_registry.v1 manifest",
10375
+ description: "Print (or write with --out) an example moda_registry.v2 manifest",
9695
10376
  usage: "moda registry template [--out=<path>] [--yes]",
9696
10377
  flags: [
9697
10378
  { name: "--out=<path>", description: "Write the template to a file instead of stdout (refuses to overwrite without --yes)." }
@@ -9707,18 +10388,52 @@ var commandRegistry = createCommandRegistry([
9707
10388
  },
9708
10389
  {
9709
10390
  name: "push",
9710
- description: "Upload the manifest's prompts, skills, and tools to the API key's tenant",
9711
- usage: "moda registry push [--file=<path>] [--dry-run] [--live]",
10391
+ description: "Upload the manifest's prompts, skills, tools, and harness sections to the API key's tenant",
10392
+ usage: "moda registry push [--file=<path>] [--dry-run] [--live] [--message=<text>]",
9712
10393
  flags: [
9713
10394
  { name: "--file=<path>", description: "Manifest path (default .moda/registry.json)." },
10395
+ { name: "--message=<text>", description: "Message for the registry commit this push records (shown in the dashboard Registry history)." },
9714
10396
  { name: "--dry-run", description: "Server-side preview; nothing is written. Without an API key, validates locally only." },
9715
10397
  { name: "--live", description: "Mark every pushed skill live (included in the served skill surface)." }
9716
10398
  ],
9717
10399
  examples: ["moda registry push --dry-run", "moda registry push --live"]
10400
+ },
10401
+ {
10402
+ name: "status",
10403
+ description: "Show the registered harness (agents, routing, context) and its version",
10404
+ usage: "moda registry status",
10405
+ examples: ["moda registry status"]
10406
+ },
10407
+ {
10408
+ name: "log",
10409
+ description: "Registry history: one commit per push, with what changed",
10410
+ usage: "moda registry log [--limit=20]",
10411
+ flags: [{ name: "--limit=<n>", description: "Commits to list (default 50)." }],
10412
+ examples: ["moda registry log --limit=10"]
10413
+ },
10414
+ {
10415
+ name: "show",
10416
+ description: `A registry commit's file diffs, or "working" for changes since the last push`,
10417
+ usage: "moda registry show <commit|working>",
10418
+ examples: ["moda registry show 3f9a2c1", "moda registry show working"]
10419
+ },
10420
+ {
10421
+ name: "coverage",
10422
+ description: "Compare recent production traces with the registry: attribution rate, unmatched agents and prompts, unregistered tools",
10423
+ usage: "moda registry coverage [--days=7]",
10424
+ flags: [{ name: "--days=<n>", description: "Look-back window in days (default 7, max 30)." }],
10425
+ examples: ["moda registry coverage --days=7"]
10426
+ },
10427
+ {
10428
+ name: "prompt",
10429
+ description: "Print the brief to hand your coding agent so it inventories and registers your harness",
10430
+ usage: "moda registry prompt [--json | --agent]",
10431
+ flags: [{ name: "--json | --agent", description: "Return the brief inside JSON / the agent envelope. Without them it prints raw text, even when piped." }],
10432
+ examples: ["moda registry prompt | pbcopy", 'claude "$(moda registry prompt)"']
9718
10433
  }
9719
10434
  ],
9720
10435
  auth: "api-key",
9721
- authNote: "template and validate are local; push needs the API key (push --dry-run without a key validates locally only).",
10436
+ authNote: "template, validate, and prompt are local; push, status, log, show, and coverage need the API key (push --dry-run without a key validates locally only).",
9722
10437
  outputModes: ["human", "json", "agent"],
9723
10438
  mutability: "write",
9724
10439
  validateConfigBeforeRun: false,