@moda-ai/cli 1.44.1 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/dist/cli.js +742 -34
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +92 -26
package/README.md
CHANGED
|
@@ -274,24 +274,36 @@ rendered = moda.prompt("support.triage").render({
|
|
|
274
274
|
})
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
-
##
|
|
277
|
+
## Register your harness for replays
|
|
278
278
|
|
|
279
279
|
When the GitHub App or `moda harness analyze` isn't an option, have your coding
|
|
280
280
|
agent write `.moda/registry.json` from the codebase and push it. Only what the
|
|
281
281
|
manifest lists is uploaded. It needs just an API key.
|
|
282
282
|
|
|
283
283
|
```bash
|
|
284
|
-
moda registry
|
|
284
|
+
moda registry prompt # brief to hand your coding agent
|
|
285
|
+
moda registry template --out=.moda/registry.json # starter manifest (moda_registry.v2)
|
|
285
286
|
moda registry validate # local validation, no network
|
|
286
287
|
moda registry push --dry-run # server-side preview
|
|
287
|
-
moda registry push # prompts, skills,
|
|
288
|
+
moda registry push # prompts, skills, tools, then agent wiring
|
|
289
|
+
moda registry status # registered harness and version
|
|
290
|
+
moda registry coverage --days=7 # recent traces vs the registry
|
|
288
291
|
```
|
|
289
292
|
|
|
290
293
|
Prompts go to the prompt registry, skills to the skill library (pass `--live`
|
|
291
294
|
or set `"live": true` to serve them), and tools to the tool registry as OpenAI
|
|
292
|
-
function schemas. The
|
|
293
|
-
|
|
294
|
-
|
|
295
|
+
function schemas. The v2 sections describe how the harness runs:
|
|
296
|
+
|
|
297
|
+
- `agents`: each agent's prompt, model and params, tools, skills, handoffs,
|
|
298
|
+
and trace names.
|
|
299
|
+
- `routing`: how the harness switches prompts.
|
|
300
|
+
- `context`: what the runtime injects into prompts.
|
|
301
|
+
- `runtime` and `mcpServers`.
|
|
302
|
+
|
|
303
|
+
Replay rebuilds the agent that answered each replayed turn from these, and
|
|
304
|
+
switches agents when the model hands off. The format is in the bundled
|
|
305
|
+
`moda-cli` skill. The analyzer's citation graph (the dashboard Harness map)
|
|
306
|
+
still comes from `moda harness analyze`, the GitHub App, or an approved report.
|
|
295
307
|
|
|
296
308
|
## Anchors and message windows
|
|
297
309
|
|
package/dist/cli.js
CHANGED
|
@@ -2062,9 +2062,539 @@ async function runSyncCommand(context) {
|
|
|
2062
2062
|
}
|
|
2063
2063
|
|
|
2064
2064
|
// src/registry.ts
|
|
2065
|
+
import { execFileSync } from "node:child_process";
|
|
2065
2066
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2066
2067
|
import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "node:path";
|
|
2067
|
-
|
|
2068
|
+
|
|
2069
|
+
// src/registry-harness.ts
|
|
2070
|
+
var HARNESS_SECTIONS = ["runtime", "agents", "routing", "context", "mcpServers"];
|
|
2071
|
+
var ROUTING_STRATEGIES = ["single", "trace_attribute", "handoff", "router", "rules"];
|
|
2072
|
+
var HISTORY_STRATEGIES = ["full", "window", "summary", "truncate_tokens"];
|
|
2073
|
+
var SKILL_LOADING_MODES = ["tool", "system_prompt", "none"];
|
|
2074
|
+
var CONTEXT_SOURCES = ["static", "clock", "trace", "tool", "memory", "retrieval", "unknown"];
|
|
2075
|
+
var CONTEXT_TARGETS = ["prompt_variable", "system_append"];
|
|
2076
|
+
var CLOCK_FORMATS = ["date", "datetime", "iso"];
|
|
2077
|
+
var MCP_TRANSPORTS = ["stdio", "http", "sse", "streamable_http"];
|
|
2078
|
+
var AGENT_KEY_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
2079
|
+
var TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
2080
|
+
var VARIABLE_PATTERN = /^[A-Za-z_][A-Za-z0-9_.]{0,127}$/;
|
|
2081
|
+
var MAX_AGENTS = 100;
|
|
2082
|
+
var MAX_CONTEXT = 200;
|
|
2083
|
+
var MAX_MCP_SERVERS = 50;
|
|
2084
|
+
var MAX_HARNESS_DOCUMENT_BYTES = 1e6;
|
|
2085
|
+
var AGENT_FIELDS = {
|
|
2086
|
+
key: "string",
|
|
2087
|
+
name: "string",
|
|
2088
|
+
description: "string",
|
|
2089
|
+
entry: "boolean",
|
|
2090
|
+
prompt: "string",
|
|
2091
|
+
model: "object",
|
|
2092
|
+
tools: "string[]",
|
|
2093
|
+
skills: "string[]",
|
|
2094
|
+
handoffs: "object[]",
|
|
2095
|
+
subagents: "object[]",
|
|
2096
|
+
context: "string[]",
|
|
2097
|
+
maxToolRoundsPerTurn: "number",
|
|
2098
|
+
responseSchema: "object",
|
|
2099
|
+
match: "object"
|
|
2100
|
+
};
|
|
2101
|
+
var MODEL_FIELDS = {
|
|
2102
|
+
id: "string",
|
|
2103
|
+
provider: "string",
|
|
2104
|
+
temperature: "number",
|
|
2105
|
+
maxTokens: "number",
|
|
2106
|
+
topP: "number",
|
|
2107
|
+
reasoningEffort: "string"
|
|
2108
|
+
};
|
|
2109
|
+
var HANDOFF_FIELDS = { to: "string", tool: "string", description: "string" };
|
|
2110
|
+
var SUBAGENT_FIELDS = { agent: "string", tool: "string", description: "string" };
|
|
2111
|
+
var MATCH_FIELDS = { agentNames: "string[]", promptKeys: "string[]" };
|
|
2112
|
+
var ROUTING_FIELDS = { strategy: "string", default: "string", notes: "string" };
|
|
2113
|
+
var RUNTIME_FIELDS = {
|
|
2114
|
+
framework: "string",
|
|
2115
|
+
language: "string",
|
|
2116
|
+
maxToolRoundsPerTurn: "number",
|
|
2117
|
+
parallelToolCalls: "boolean",
|
|
2118
|
+
history: "object",
|
|
2119
|
+
skillLoading: "string",
|
|
2120
|
+
notes: "string"
|
|
2121
|
+
};
|
|
2122
|
+
var HISTORY_FIELDS = { strategy: "string", maxMessages: "number", maxTokens: "number" };
|
|
2123
|
+
var CONTEXT_FIELDS = {
|
|
2124
|
+
key: "string",
|
|
2125
|
+
variable: "string",
|
|
2126
|
+
source: "string",
|
|
2127
|
+
value: "string",
|
|
2128
|
+
format: "string",
|
|
2129
|
+
target: "string",
|
|
2130
|
+
description: "string"
|
|
2131
|
+
};
|
|
2132
|
+
var MCP_FIELDS = {
|
|
2133
|
+
name: "string",
|
|
2134
|
+
transport: "string",
|
|
2135
|
+
url: "string",
|
|
2136
|
+
command: "string",
|
|
2137
|
+
tools: "string[]",
|
|
2138
|
+
description: "string"
|
|
2139
|
+
};
|
|
2140
|
+
function handoffToolName(handoff) {
|
|
2141
|
+
return handoff.tool?.trim() || `transfer_to_${handoff.to.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 64);
|
|
2142
|
+
}
|
|
2143
|
+
function hasHarnessSections(input) {
|
|
2144
|
+
return HARNESS_SECTIONS.some((section) => input[section] !== undefined);
|
|
2145
|
+
}
|
|
2146
|
+
function validateHarnessSections(input, known, errors, warnings) {
|
|
2147
|
+
if (!hasHarnessSections(input))
|
|
2148
|
+
return;
|
|
2149
|
+
const document = {};
|
|
2150
|
+
if (input.runtime !== undefined) {
|
|
2151
|
+
if (checkObject("runtime", input.runtime, RUNTIME_FIELDS, errors)) {
|
|
2152
|
+
const runtime = input.runtime;
|
|
2153
|
+
checkPositiveInt("runtime.maxToolRoundsPerTurn", runtime.maxToolRoundsPerTurn, errors);
|
|
2154
|
+
checkEnum("runtime.skillLoading", runtime.skillLoading, SKILL_LOADING_MODES, errors);
|
|
2155
|
+
if (runtime.history !== undefined && checkObject("runtime.history", runtime.history, HISTORY_FIELDS, errors)) {
|
|
2156
|
+
checkEnum("runtime.history.strategy", runtime.history.strategy, HISTORY_STRATEGIES, errors);
|
|
2157
|
+
checkPositiveInt("runtime.history.maxMessages", runtime.history.maxMessages, errors);
|
|
2158
|
+
checkPositiveInt("runtime.history.maxTokens", runtime.history.maxTokens, errors);
|
|
2159
|
+
}
|
|
2160
|
+
document.runtime = runtime;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
const contextKeys = new Set;
|
|
2164
|
+
if (input.context !== undefined) {
|
|
2165
|
+
const entries = listOf("context", input.context, MAX_CONTEXT, errors);
|
|
2166
|
+
document.context = [];
|
|
2167
|
+
entries.forEach((raw, index) => {
|
|
2168
|
+
const label = `context[${index}]`;
|
|
2169
|
+
if (!checkObject(label, raw, CONTEXT_FIELDS, errors))
|
|
2170
|
+
return;
|
|
2171
|
+
const entry = raw;
|
|
2172
|
+
if (!nonEmpty(entry.key))
|
|
2173
|
+
errors.push(`${label}.key is required`);
|
|
2174
|
+
else if (contextKeys.has(entry.key))
|
|
2175
|
+
errors.push(`${label}: duplicate context key "${entry.key}"`);
|
|
2176
|
+
else
|
|
2177
|
+
contextKeys.add(entry.key);
|
|
2178
|
+
if (!nonEmpty(entry.source))
|
|
2179
|
+
errors.push(`${label}.source is required (one of ${CONTEXT_SOURCES.join(", ")})`);
|
|
2180
|
+
else
|
|
2181
|
+
checkEnum(`${label}.source`, entry.source, CONTEXT_SOURCES, errors);
|
|
2182
|
+
checkEnum(`${label}.target`, entry.target, CONTEXT_TARGETS, errors);
|
|
2183
|
+
checkEnum(`${label}.format`, entry.format, CLOCK_FORMATS, errors);
|
|
2184
|
+
const target = entry.target ?? "prompt_variable";
|
|
2185
|
+
if (target === "prompt_variable") {
|
|
2186
|
+
if (!nonEmpty(entry.variable))
|
|
2187
|
+
errors.push(`${label}.variable is required for target "prompt_variable"`);
|
|
2188
|
+
else if (!VARIABLE_PATTERN.test(entry.variable))
|
|
2189
|
+
errors.push(`${label}.variable "${entry.variable}" is not a valid template variable name`);
|
|
2190
|
+
}
|
|
2191
|
+
if (entry.source === "static" && typeof entry.value !== "string")
|
|
2192
|
+
errors.push(`${label}.value is required for source "static"`);
|
|
2193
|
+
if (entry.source !== "static" && entry.value !== undefined) {
|
|
2194
|
+
errors.push(`${label}.value is only allowed for source "static" (record runtime values in traces instead)`);
|
|
2195
|
+
}
|
|
2196
|
+
document.context.push(entry);
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
const agentKeys = new Set;
|
|
2200
|
+
const agents = input.agents === undefined ? [] : listOf("agents", input.agents, MAX_AGENTS, errors);
|
|
2201
|
+
for (const raw of agents) {
|
|
2202
|
+
if (isRecord(raw) && nonEmpty(raw.key) && AGENT_KEY_PATTERN.test(raw.key))
|
|
2203
|
+
agentKeys.add(raw.key);
|
|
2204
|
+
}
|
|
2205
|
+
if (input.agents !== undefined) {
|
|
2206
|
+
document.agents = [];
|
|
2207
|
+
const seen = new Set;
|
|
2208
|
+
let entries = 0;
|
|
2209
|
+
agents.forEach((raw, index) => {
|
|
2210
|
+
const label = `agents[${index}]`;
|
|
2211
|
+
if (!checkObject(label, raw, AGENT_FIELDS, errors))
|
|
2212
|
+
return;
|
|
2213
|
+
const agent = raw;
|
|
2214
|
+
if (!nonEmpty(agent.key))
|
|
2215
|
+
errors.push(`${label}.key is required`);
|
|
2216
|
+
else if (!AGENT_KEY_PATTERN.test(agent.key))
|
|
2217
|
+
errors.push(`${label}.key "${agent.key}" may only contain letters, digits, ".", "_" and "-" (max 128)`);
|
|
2218
|
+
else if (seen.has(agent.key))
|
|
2219
|
+
errors.push(`${label}: duplicate agent key "${agent.key}"`);
|
|
2220
|
+
else
|
|
2221
|
+
seen.add(agent.key);
|
|
2222
|
+
if (agent.entry === true)
|
|
2223
|
+
entries += 1;
|
|
2224
|
+
if (agent.model !== undefined && checkObject(`${label}.model`, agent.model, MODEL_FIELDS, errors)) {
|
|
2225
|
+
const model = agent.model;
|
|
2226
|
+
if (typeof model.temperature === "number" && (model.temperature < 0 || model.temperature > 2)) {
|
|
2227
|
+
errors.push(`${label}.model.temperature must be between 0 and 2`);
|
|
2228
|
+
}
|
|
2229
|
+
if (typeof model.topP === "number" && (model.topP <= 0 || model.topP > 1))
|
|
2230
|
+
errors.push(`${label}.model.topP must be in (0, 1]`);
|
|
2231
|
+
checkPositiveInt(`${label}.model.maxTokens`, model.maxTokens, errors);
|
|
2232
|
+
}
|
|
2233
|
+
checkPositiveInt(`${label}.maxToolRoundsPerTurn`, agent.maxToolRoundsPerTurn, errors);
|
|
2234
|
+
if (agent.match !== undefined)
|
|
2235
|
+
checkObject(`${label}.match`, agent.match, MATCH_FIELDS, errors);
|
|
2236
|
+
const handoffTools = new Set;
|
|
2237
|
+
(Array.isArray(agent.handoffs) ? agent.handoffs : []).forEach((handoff, hIndex) => {
|
|
2238
|
+
const hLabel = `${label}.handoffs[${hIndex}]`;
|
|
2239
|
+
if (!checkObject(hLabel, handoff, HANDOFF_FIELDS, errors))
|
|
2240
|
+
return;
|
|
2241
|
+
if (!nonEmpty(handoff.to)) {
|
|
2242
|
+
errors.push(`${hLabel}.to is required`);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
if (!agentKeys.has(handoff.to))
|
|
2246
|
+
errors.push(`${hLabel}.to "${handoff.to}" is not an agent key in this manifest`);
|
|
2247
|
+
if (handoff.to === agent.key)
|
|
2248
|
+
errors.push(`${hLabel}: an agent cannot hand off to itself`);
|
|
2249
|
+
const tool = handoffToolName(handoff);
|
|
2250
|
+
if (!TOOL_NAME_PATTERN.test(tool))
|
|
2251
|
+
errors.push(`${hLabel}.tool "${tool}" must match ${TOOL_NAME_PATTERN.source}`);
|
|
2252
|
+
else if (handoffTools.has(tool))
|
|
2253
|
+
errors.push(`${hLabel}: duplicate handoff tool "${tool}"`);
|
|
2254
|
+
else if ((agent.tools ?? []).includes(tool))
|
|
2255
|
+
errors.push(`${hLabel}: handoff tool "${tool}" collides with a tool in ${label}.tools`);
|
|
2256
|
+
handoffTools.add(tool);
|
|
2257
|
+
});
|
|
2258
|
+
(Array.isArray(agent.subagents) ? agent.subagents : []).forEach((subagent, sIndex) => {
|
|
2259
|
+
const sLabel = `${label}.subagents[${sIndex}]`;
|
|
2260
|
+
if (!checkObject(sLabel, subagent, SUBAGENT_FIELDS, errors))
|
|
2261
|
+
return;
|
|
2262
|
+
if (!nonEmpty(subagent.agent))
|
|
2263
|
+
errors.push(`${sLabel}.agent is required`);
|
|
2264
|
+
else if (!agentKeys.has(subagent.agent))
|
|
2265
|
+
errors.push(`${sLabel}.agent "${subagent.agent}" is not an agent key in this manifest`);
|
|
2266
|
+
if (!nonEmpty(subagent.tool))
|
|
2267
|
+
errors.push(`${sLabel}.tool is required (the tool name the parent calls)`);
|
|
2268
|
+
else if (!(agent.tools ?? []).includes(subagent.tool)) {
|
|
2269
|
+
errors.push(`${sLabel}.tool "${subagent.tool}" must also be listed in ${label}.tools (with its schema in tools[])`);
|
|
2270
|
+
}
|
|
2271
|
+
});
|
|
2272
|
+
for (const key of agent.context ?? []) {
|
|
2273
|
+
if (!contextKeys.has(key))
|
|
2274
|
+
errors.push(`${label}.context "${key}" is not a key in context[]`);
|
|
2275
|
+
}
|
|
2276
|
+
if (nonEmpty(agent.prompt) && !known.prompts.has(agent.prompt)) {
|
|
2277
|
+
warnings.push(`${label}.prompt "${agent.prompt}" is not in prompts[]; it must already exist in Moda`);
|
|
2278
|
+
}
|
|
2279
|
+
for (const tool of agent.tools ?? []) {
|
|
2280
|
+
if (!known.tools.has(tool))
|
|
2281
|
+
warnings.push(`${label}.tools "${tool}" is not in tools[]; it must already exist in Moda`);
|
|
2282
|
+
}
|
|
2283
|
+
for (const skill of agent.skills ?? []) {
|
|
2284
|
+
if (!known.skills.has(skill))
|
|
2285
|
+
warnings.push(`${label}.skills "${skill}" is not in skills[]; it must already exist in Moda`);
|
|
2286
|
+
}
|
|
2287
|
+
document.agents.push(agent);
|
|
2288
|
+
});
|
|
2289
|
+
if (entries > 1)
|
|
2290
|
+
errors.push('agents: at most one agent may set "entry": true');
|
|
2291
|
+
}
|
|
2292
|
+
if (input.routing !== undefined) {
|
|
2293
|
+
if (checkObject("routing", input.routing, ROUTING_FIELDS, errors)) {
|
|
2294
|
+
const routing = input.routing;
|
|
2295
|
+
checkEnum("routing.strategy", routing.strategy, ROUTING_STRATEGIES, errors);
|
|
2296
|
+
if (routing.default !== undefined && !agentKeys.has(routing.default)) {
|
|
2297
|
+
errors.push(`routing.default "${routing.default}" is not an agent key in agents[]`);
|
|
2298
|
+
}
|
|
2299
|
+
document.routing = routing;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (input.mcpServers !== undefined) {
|
|
2303
|
+
document.mcpServers = [];
|
|
2304
|
+
const names = new Set;
|
|
2305
|
+
listOf("mcpServers", input.mcpServers, MAX_MCP_SERVERS, errors).forEach((raw, index) => {
|
|
2306
|
+
const label = `mcpServers[${index}]`;
|
|
2307
|
+
if (!checkObject(label, raw, MCP_FIELDS, errors))
|
|
2308
|
+
return;
|
|
2309
|
+
const server = raw;
|
|
2310
|
+
if (!nonEmpty(server.name))
|
|
2311
|
+
errors.push(`${label}.name is required`);
|
|
2312
|
+
else if (names.has(server.name))
|
|
2313
|
+
errors.push(`${label}: duplicate MCP server name "${server.name}"`);
|
|
2314
|
+
else
|
|
2315
|
+
names.add(server.name);
|
|
2316
|
+
checkEnum(`${label}.transport`, server.transport, MCP_TRANSPORTS, errors);
|
|
2317
|
+
if (server.url !== undefined && hasUrlCredentials(server.url)) {
|
|
2318
|
+
errors.push(`${label}.url must not contain credentials (user info or token query parameters)`);
|
|
2319
|
+
}
|
|
2320
|
+
document.mcpServers.push(server);
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
if (Buffer.byteLength(JSON.stringify(document), "utf8") > MAX_HARNESS_DOCUMENT_BYTES) {
|
|
2324
|
+
errors.push(`harness sections exceed ${MAX_HARNESS_DOCUMENT_BYTES} bytes`);
|
|
2325
|
+
}
|
|
2326
|
+
return document;
|
|
2327
|
+
}
|
|
2328
|
+
function hasUrlCredentials(raw) {
|
|
2329
|
+
try {
|
|
2330
|
+
const url = new URL(raw);
|
|
2331
|
+
if (url.username || url.password)
|
|
2332
|
+
return true;
|
|
2333
|
+
return [...url.searchParams.keys()].some((key) => /token|key|secret|password|auth/i.test(key));
|
|
2334
|
+
} catch {
|
|
2335
|
+
return false;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function listOf(label, value, max, errors) {
|
|
2339
|
+
if (!Array.isArray(value)) {
|
|
2340
|
+
errors.push(`${label} must be an array`);
|
|
2341
|
+
return [];
|
|
2342
|
+
}
|
|
2343
|
+
if (value.length > max)
|
|
2344
|
+
errors.push(`${label} has ${value.length} entries; the limit is ${max}`);
|
|
2345
|
+
return value;
|
|
2346
|
+
}
|
|
2347
|
+
function checkObject(label, value, spec, errors) {
|
|
2348
|
+
if (!isRecord(value)) {
|
|
2349
|
+
errors.push(`${label} must be an object`);
|
|
2350
|
+
return false;
|
|
2351
|
+
}
|
|
2352
|
+
let typed = true;
|
|
2353
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2354
|
+
const expected = spec[key];
|
|
2355
|
+
if (!expected) {
|
|
2356
|
+
errors.push(`${label}: unknown field "${key}" (allowed: ${Object.keys(spec).join(", ")})`);
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
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;
|
|
2360
|
+
if (!ok) {
|
|
2361
|
+
errors.push(`${label}.${key} must be ${describe(expected)}`);
|
|
2362
|
+
typed = false;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return typed;
|
|
2366
|
+
}
|
|
2367
|
+
function describe(type) {
|
|
2368
|
+
if (type === "string[]")
|
|
2369
|
+
return "an array of strings";
|
|
2370
|
+
if (type === "object[]")
|
|
2371
|
+
return "an array of objects";
|
|
2372
|
+
return type === "object" ? "an object" : `a ${type}`;
|
|
2373
|
+
}
|
|
2374
|
+
function checkEnum(label, value, allowed, errors) {
|
|
2375
|
+
if (value === undefined || typeof value !== "string")
|
|
2376
|
+
return;
|
|
2377
|
+
if (!allowed.includes(value))
|
|
2378
|
+
errors.push(`${label} must be one of ${allowed.join(", ")} (got "${value}")`);
|
|
2379
|
+
}
|
|
2380
|
+
function checkPositiveInt(label, value, errors) {
|
|
2381
|
+
if (value === undefined || typeof value !== "number")
|
|
2382
|
+
return;
|
|
2383
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
2384
|
+
errors.push(`${label} must be a positive integer`);
|
|
2385
|
+
}
|
|
2386
|
+
function isRecord(value) {
|
|
2387
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2388
|
+
}
|
|
2389
|
+
function nonEmpty(value) {
|
|
2390
|
+
return typeof value === "string" && value.trim() !== "";
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/registry-prompt.ts
|
|
2394
|
+
var REGISTRY_AGENT_BRIEF = `# Register this repo's AI harness with Moda for faithful replays
|
|
2395
|
+
|
|
2396
|
+
You are working in the repository that runs our production AI agent. Moda
|
|
2397
|
+
replays recorded production conversations against our agent to test prompt,
|
|
2398
|
+
tool, and skill changes. A replay is only as faithful as what Moda knows about
|
|
2399
|
+
how our agent actually runs. Your job: describe our harness to Moda exactly,
|
|
2400
|
+
push it with the \`moda\` CLI, and make sure our production traces say which
|
|
2401
|
+
agent and prompt handled each LLM call.
|
|
2402
|
+
|
|
2403
|
+
## Ground rules
|
|
2404
|
+
|
|
2405
|
+
- Read the code; do not guess. Every value you register must come from the
|
|
2406
|
+
repo (cite the file in \`sourcePath\` or in a note). If something is decided
|
|
2407
|
+
at runtime and you can't find it, say so in your final report.
|
|
2408
|
+
- Copy prompt text, tool names, tool descriptions, and parameter schemas
|
|
2409
|
+
verbatim. For templated prompts, keep the template with its placeholders
|
|
2410
|
+
(\`{{name}}\`, \`{name}\`, \`\${name}\`). Never register a rendered example.
|
|
2411
|
+
- Never put secrets in the manifest: no API keys, tokens, auth headers, env var
|
|
2412
|
+
values, or customer data.
|
|
2413
|
+
- Do not change runtime behaviour. Part 5 adds trace metadata only. Show the
|
|
2414
|
+
diff and get my OK before committing it.
|
|
2415
|
+
- Only \`.moda/registry.json\` (plus the files it references) is uploaded.
|
|
2416
|
+
Nothing else in the repo leaves this machine.
|
|
2417
|
+
|
|
2418
|
+
## Part 0: setup
|
|
2419
|
+
|
|
2420
|
+
1. Run \`moda --version\`. You need 1.44.0 or newer. If it's older, run
|
|
2421
|
+
\`npm install -g @moda-ai/cli@latest\`.
|
|
2422
|
+
2. Auth: \`MODA_API_KEY\` must be set, or run \`moda auth login --api-key=<key>\`.
|
|
2423
|
+
Ask me for the key if neither is available. Check with \`moda doctor\`.
|
|
2424
|
+
3. Run \`moda registry template\` to see a complete example manifest, and
|
|
2425
|
+
\`moda registry status\` to see what is already registered.
|
|
2426
|
+
|
|
2427
|
+
## Part 1: inventory the harness
|
|
2428
|
+
|
|
2429
|
+
Find every place the code calls an LLM: direct SDK calls (OpenAI, Anthropic,
|
|
2430
|
+
Gemini, Bedrock, OpenRouter), framework agents, and anything else. For each
|
|
2431
|
+
call site, write down:
|
|
2432
|
+
|
|
2433
|
+
- **Agent**: the logical agent it belongs to, with a stable key such as
|
|
2434
|
+
\`triage\` or \`billing\`. Different system prompts, tool sets, or models mean
|
|
2435
|
+
different agents. A one-off call (summariser, classifier, title generator)
|
|
2436
|
+
is its own agent if it runs during a conversation.
|
|
2437
|
+
- **Prompt**: the system prompt or messages it sends, how they're built
|
|
2438
|
+
(static text, template plus variables, or code that assembles them), and
|
|
2439
|
+
the file.
|
|
2440
|
+
- **Model and settings**: model id, provider, temperature, max tokens, top_p,
|
|
2441
|
+
and reasoning effort, wherever they're set (code, config, env defaults).
|
|
2442
|
+
- **Tools**: every tool the model can call at that site, with the exact JSON
|
|
2443
|
+
schema the model sees. Include MCP tools and agents exposed as tools.
|
|
2444
|
+
- **Skills**: SKILL.md files or instruction docs the agent loads, and HOW it
|
|
2445
|
+
loads them (a tool such as skill_read or read_file, inlined into the system
|
|
2446
|
+
prompt, or not at all).
|
|
2447
|
+
- **Loop limits**: max tool rounds or steps per turn, parallel tool calls, and
|
|
2448
|
+
how history is truncated or summarised.
|
|
2449
|
+
|
|
2450
|
+
Framework hints:
|
|
2451
|
+
|
|
2452
|
+
- **OpenAI Agents SDK**: \`Agent(name, instructions, model, model_settings,
|
|
2453
|
+
tools, handoffs)\`. \`name\` is the trace agent name. Handoffs become
|
|
2454
|
+
\`transfer_to_<agent>\` tools.
|
|
2455
|
+
- **Claude Agent SDK / Claude Code SDK**: \`ClaudeAgentOptions(system_prompt,
|
|
2456
|
+
model, allowed_tools, mcp_servers, agents=...)\`. Subagents are \`agents\`,
|
|
2457
|
+
and skills live in \`.claude/skills/*/SKILL.md\`.
|
|
2458
|
+
- **LangGraph / LangChain**: each node that binds an LLM (\`bind_tools\`,
|
|
2459
|
+
\`create_react_agent\`) is an agent. Conditional edges are the routing.
|
|
2460
|
+
- **Vercel AI SDK**: \`generateText\` / \`streamText({ system, tools, model,
|
|
2461
|
+
maxSteps | stopWhen })\`.
|
|
2462
|
+
- **Raw SDK calls**: the system message, \`tools=\`, and \`model=\` arguments.
|
|
2463
|
+
|
|
2464
|
+
## Part 2: work out prompt switching
|
|
2465
|
+
|
|
2466
|
+
Work out how the harness decides which agent and prompt answers a given turn.
|
|
2467
|
+
That becomes \`routing\` and the agents' \`handoffs\`:
|
|
2468
|
+
|
|
2469
|
+
- **single**: one agent always answers. Prompt versions still change over
|
|
2470
|
+
time; Part 5 handles that.
|
|
2471
|
+
- **handoff**: the model transfers control by calling a tool (OpenAI Agents
|
|
2472
|
+
handoffs, a "transfer_to_*" function, or a supervisor delegating). Add
|
|
2473
|
+
\`handoffs: [{ "to": "<agent key>", "tool": "<exact tool name>",
|
|
2474
|
+
"description": "<when>" }]\` to the source agent.
|
|
2475
|
+
- **router / rules**: code picks the agent (by channel, intent classifier,
|
|
2476
|
+
conversation stage, user plan, or feature flag). Describe the rule in
|
|
2477
|
+
\`routing.notes\` and make sure Part 5's attribution names the chosen agent
|
|
2478
|
+
on every call.
|
|
2479
|
+
- **trace_attribute**: the agent is already identified on every trace.
|
|
2480
|
+
- **subagents** (agents as tools): the parent calls a tool that runs another
|
|
2481
|
+
agent and returns its result. Register that tool's schema in \`tools[]\`,
|
|
2482
|
+
list it in the parent's \`tools\`, and add \`subagents: [{ "agent": "<child
|
|
2483
|
+
key>", "tool": "<tool name>" }]\`.
|
|
2484
|
+
|
|
2485
|
+
Set \`routing.default\` to the agent that answers when nothing else decides,
|
|
2486
|
+
and mark the first agent a conversation reaches with \`"entry": true\`.
|
|
2487
|
+
|
|
2488
|
+
## Part 3: list the injected context
|
|
2489
|
+
|
|
2490
|
+
For every placeholder in every prompt, and anything appended to the prompt at
|
|
2491
|
+
runtime, add a \`context[]\` entry:
|
|
2492
|
+
|
|
2493
|
+
- \`source: "static"\`: the same value every time (put it in \`value\`).
|
|
2494
|
+
- \`source: "clock"\`: the current date or time (\`format\`: date, datetime, or
|
|
2495
|
+
iso). Replay uses the original conversation's time.
|
|
2496
|
+
- \`source: "trace"\`: a per-user or per-conversation value, such as the
|
|
2497
|
+
profile, account, plan, or locale.
|
|
2498
|
+
- \`source: "memory" | "retrieval" | "tool"\`: memory recall, RAG results, or a
|
|
2499
|
+
tool fetched before the call.
|
|
2500
|
+
- \`target: "prompt_variable"\` (with \`variable\` set to the placeholder name,
|
|
2501
|
+
without braces) for template variables, or \`target: "system_append"\` for
|
|
2502
|
+
blocks the code appends.
|
|
2503
|
+
|
|
2504
|
+
List context keys on an agent with \`context: [...]\` when only some agents get
|
|
2505
|
+
them.
|
|
2506
|
+
|
|
2507
|
+
## Part 4: write, validate, and push the manifest
|
|
2508
|
+
|
|
2509
|
+
Write \`.moda/registry.json\` with \`"schema": "moda_registry.v2"\`:
|
|
2510
|
+
|
|
2511
|
+
- \`prompts[]\`: \`{ key, file | content | systemPrompt | messages, name,
|
|
2512
|
+
description, variables, modelConfig, responseSchema, sourcePath }\`. If the
|
|
2513
|
+
repo already uses \`moda prompts\` (it has a \`.moda/prompts.lock.json\`),
|
|
2514
|
+
reuse those keys.
|
|
2515
|
+
- \`tools[]\`: \`{ schema: { name, description, parameters } }\`, exactly as
|
|
2516
|
+
sent to the model.
|
|
2517
|
+
- \`skills[]\`: \`{ key, file: "path/to/SKILL.md" }\`. Set \`"live": true\` only
|
|
2518
|
+
if I ask.
|
|
2519
|
+
- \`agents[]\`: \`{ key, name, entry, prompt, model: { id, provider,
|
|
2520
|
+
temperature, maxTokens, topP, reasoningEffort }, tools, skills, handoffs,
|
|
2521
|
+
subagents, context, maxToolRoundsPerTurn, responseSchema, match: {
|
|
2522
|
+
agentNames, promptKeys } }\`. \`match.agentNames\` are the names this agent
|
|
2523
|
+
carries in traces; \`match.promptKeys\` are prompt keys that identify it.
|
|
2524
|
+
- \`routing\`: \`{ strategy, default, notes }\`.
|
|
2525
|
+
- \`runtime\`: \`{ framework, language, maxToolRoundsPerTurn,
|
|
2526
|
+
parallelToolCalls, history: { strategy, maxMessages, maxTokens },
|
|
2527
|
+
skillLoading: "tool" | "system_prompt" | "none" }\`.
|
|
2528
|
+
- \`context[]\`: from Part 3.
|
|
2529
|
+
- \`mcpServers[]\`: \`{ name, transport, url | command, tools, description }\`,
|
|
2530
|
+
with no credentials. Their tools still go in \`tools[]\`.
|
|
2531
|
+
|
|
2532
|
+
Then run:
|
|
2533
|
+
|
|
2534
|
+
moda registry validate # local; fix every error and read every warning
|
|
2535
|
+
moda registry push --dry-run # server-side preview, writes nothing
|
|
2536
|
+
moda registry push --message="<what you registered or changed>"
|
|
2537
|
+
|
|
2538
|
+
Re-pushing an unchanged manifest is a no-op. A changed prompt adds a new
|
|
2539
|
+
version, and a changed harness adds a new harness version.
|
|
2540
|
+
|
|
2541
|
+
## Part 5: make traces name the agent and prompt version
|
|
2542
|
+
|
|
2543
|
+
Replay picks the agent and prompt version that handled the replayed turn from
|
|
2544
|
+
attributes on each LLM call. Auto-instrumented calls don't carry them, so add
|
|
2545
|
+
them at each call site from Part 1:
|
|
2546
|
+
|
|
2547
|
+
| What | OTel span attribute | Vercel AI SDK \`experimental_telemetry.metadata\` | \`/v1/ingest\` event field |
|
|
2548
|
+
|---|---|---|---|
|
|
2549
|
+
| Agent | \`gen_ai.agent.name\` (or \`moda.agent_name\`) | \`moda.agent_name\` | \`agent_name\` |
|
|
2550
|
+
| Prompt key | \`moda.prompt_key\` | \`moda.prompt_key\` | \`prompt_name\` |
|
|
2551
|
+
| Prompt version | \`moda.prompt_version_id\` | \`moda.prompt_version_id\` | \`prompt_version_id\` |
|
|
2552
|
+
|
|
2553
|
+
- The agent value must be the agent's \`key\` or one of its
|
|
2554
|
+
\`match.agentNames\`. Frameworks that already set \`gen_ai.agent.name\`
|
|
2555
|
+
(OpenAI Agents SDK, many OpenInference/OpenLLMetry instrumentations) only
|
|
2556
|
+
need \`match.agentNames\` to list those names.
|
|
2557
|
+
- Get the prompt version id from \`.moda/prompts.lock.json\` (written by
|
|
2558
|
+
\`moda prompts sync\`), or leave it out. Replay then uses the prompt's prod
|
|
2559
|
+
version.
|
|
2560
|
+
- Node with the Moda SDK: \`Moda.withLLMCall(..., ({ span }) =>
|
|
2561
|
+
span.rawSpan.setAttribute(...))\`, or pass Vercel metadata through
|
|
2562
|
+
\`Moda.getVercelAITelemetry({ metadata })\`.
|
|
2563
|
+
- Python: set the attributes in your OpenTelemetry instrumentation on the LLM
|
|
2564
|
+
span, or send \`agent_name\` / \`prompt_*\` on \`/v1/ingest\` events.
|
|
2565
|
+
- Show me the diff before committing it.
|
|
2566
|
+
|
|
2567
|
+
## Part 6: verify
|
|
2568
|
+
|
|
2569
|
+
moda registry status
|
|
2570
|
+
moda registry coverage --days=7
|
|
2571
|
+
moda registry log # each push is a commit; review what changed
|
|
2572
|
+
|
|
2573
|
+
Coverage compares the last N days of production traces with what you
|
|
2574
|
+
registered: the share of LLM calls naming an agent or prompt, agent names and
|
|
2575
|
+
prompt keys that match no registered agent, tools used in production but not
|
|
2576
|
+
registered, and registered agents never seen. Fix the manifest (or the
|
|
2577
|
+
attribution) and push again until there is nothing unexplained left. Attribution
|
|
2578
|
+
added in Part 5 only shows up on traces created after it ships.
|
|
2579
|
+
|
|
2580
|
+
## Final report
|
|
2581
|
+
|
|
2582
|
+
Reply with:
|
|
2583
|
+
|
|
2584
|
+
1. The agents you registered: key, prompt, model, tool count, skill count, and
|
|
2585
|
+
handoffs.
|
|
2586
|
+
2. The routing strategy, and how you verified it.
|
|
2587
|
+
3. Context entries whose values replay can't reproduce (\`trace\`, \`memory\`,
|
|
2588
|
+
\`retrieval\`, \`tool\`).
|
|
2589
|
+
4. The attribution diff (or that it's pending my OK).
|
|
2590
|
+
5. The \`moda registry coverage\` result, plus anything unexplained you
|
|
2591
|
+
couldn't resolve.
|
|
2592
|
+
6. Anything you found but couldn't express in the manifest.
|
|
2593
|
+
`;
|
|
2594
|
+
|
|
2595
|
+
// src/registry.ts
|
|
2596
|
+
var REGISTRY_SCHEMA = "moda_registry.v2";
|
|
2597
|
+
var REGISTRY_SCHEMA_V1 = "moda_registry.v1";
|
|
2068
2598
|
var DEFAULT_REGISTRY_PATH = ".moda/registry.json";
|
|
2069
2599
|
var SKILL_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
2070
2600
|
var MAX_ITEMS_PER_SECTION = 500;
|
|
@@ -2075,7 +2605,7 @@ var REGISTRY_TEMPLATE = {
|
|
|
2075
2605
|
key: "support.triage",
|
|
2076
2606
|
name: "Support triage system prompt",
|
|
2077
2607
|
description: "Routes inbound support messages",
|
|
2078
|
-
content: "You are a support triage agent. Classify the request and ...",
|
|
2608
|
+
content: "You are a support triage agent for {{company_name}}. Today is {{current_date}}. Classify the request and ...",
|
|
2079
2609
|
sourcePath: "src/agents/triage/prompt.ts"
|
|
2080
2610
|
},
|
|
2081
2611
|
{
|
|
@@ -2108,12 +2638,49 @@ description: How to process refund requests
|
|
|
2108
2638
|
}
|
|
2109
2639
|
}
|
|
2110
2640
|
}
|
|
2641
|
+
],
|
|
2642
|
+
runtime: {
|
|
2643
|
+
framework: "openai-agents",
|
|
2644
|
+
language: "python",
|
|
2645
|
+
maxToolRoundsPerTurn: 6,
|
|
2646
|
+
history: { strategy: "window", maxMessages: 40 },
|
|
2647
|
+
skillLoading: "tool"
|
|
2648
|
+
},
|
|
2649
|
+
agents: [
|
|
2650
|
+
{
|
|
2651
|
+
key: "triage",
|
|
2652
|
+
name: "Triage",
|
|
2653
|
+
entry: true,
|
|
2654
|
+
prompt: "support.triage",
|
|
2655
|
+
model: { id: "openai/gpt-5.1", temperature: 0.2, maxTokens: 1024 },
|
|
2656
|
+
tools: ["lookup_customer"],
|
|
2657
|
+
handoffs: [{ to: "reply", description: "Hand off once the request is classified" }],
|
|
2658
|
+
match: { agentNames: ["TriageAgent"] }
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
key: "reply",
|
|
2662
|
+
name: "Reply writer",
|
|
2663
|
+
prompt: "support.reply",
|
|
2664
|
+
model: { id: "anthropic/claude-sonnet-5-5", temperature: 0.4, maxTokens: 2048 },
|
|
2665
|
+
skills: ["refund-policy"],
|
|
2666
|
+
match: { agentNames: ["ReplyAgent"] }
|
|
2667
|
+
}
|
|
2668
|
+
],
|
|
2669
|
+
routing: { strategy: "handoff", default: "triage" },
|
|
2670
|
+
context: [
|
|
2671
|
+
{ key: "today", variable: "current_date", source: "clock", format: "date" },
|
|
2672
|
+
{ key: "company", variable: "company_name", source: "static", value: "Acme" }
|
|
2111
2673
|
]
|
|
2112
2674
|
};
|
|
2113
2675
|
var REGISTRY_SUBCOMMAND_FLAGS = {
|
|
2114
2676
|
template: ["out", "dry-run"],
|
|
2115
2677
|
validate: ["file", "manifest"],
|
|
2116
|
-
push: ["file", "manifest", "live", "dry-run"]
|
|
2678
|
+
push: ["file", "manifest", "live", "dry-run", "message"],
|
|
2679
|
+
log: ["limit"],
|
|
2680
|
+
show: [],
|
|
2681
|
+
status: [],
|
|
2682
|
+
coverage: ["days"],
|
|
2683
|
+
prompt: []
|
|
2117
2684
|
};
|
|
2118
2685
|
async function runRegistryCommand(context) {
|
|
2119
2686
|
const subcommand = context.positionals[0] ?? "push";
|
|
@@ -2123,18 +2690,50 @@ async function runRegistryCommand(context) {
|
|
|
2123
2690
|
return writeTemplate(context);
|
|
2124
2691
|
case "validate": {
|
|
2125
2692
|
const registry = loadRegistry(context);
|
|
2693
|
+
printWarnings(context, registry.warnings);
|
|
2126
2694
|
context.output.writeData({
|
|
2127
|
-
schema:
|
|
2695
|
+
schema: registry.schema,
|
|
2128
2696
|
manifest: displayPath(context, registry.manifestPath),
|
|
2129
2697
|
valid: true,
|
|
2130
|
-
counts: countsOf(registry)
|
|
2698
|
+
counts: countsOf(registry),
|
|
2699
|
+
warnings: registry.warnings
|
|
2131
2700
|
});
|
|
2132
2701
|
return { exitCode: 0 };
|
|
2133
2702
|
}
|
|
2134
2703
|
case "push":
|
|
2135
2704
|
return pushRegistry(context);
|
|
2705
|
+
case "status":
|
|
2706
|
+
return readRegistry(context, "/harness-registry");
|
|
2707
|
+
case "log": {
|
|
2708
|
+
const limit = context.flags.limit;
|
|
2709
|
+
if (limit !== undefined && !/^\d+$/.test(limit)) {
|
|
2710
|
+
throw new CliInputError("--limit must be a whole number.", "Usage: moda registry log [--limit=20]");
|
|
2711
|
+
}
|
|
2712
|
+
return readRegistry(context, `/registry/commits${limit ? `?limit=${limit}` : ""}`);
|
|
2713
|
+
}
|
|
2714
|
+
case "show": {
|
|
2715
|
+
const ref = context.positionals[1];
|
|
2716
|
+
if (!ref) {
|
|
2717
|
+
throw new CliInputError("Missing commit.", 'Usage: moda registry show <commit> (a hash from `moda registry log`, or "working")');
|
|
2718
|
+
}
|
|
2719
|
+
return readRegistry(context, ref === "working" ? "/registry/compare?to=working" : `/registry/commits/${encodeURIComponent(ref)}`);
|
|
2720
|
+
}
|
|
2721
|
+
case "coverage": {
|
|
2722
|
+
const days = context.flags.days;
|
|
2723
|
+
if (days !== undefined && !/^\d+$/.test(days)) {
|
|
2724
|
+
throw new CliInputError("--days must be a whole number of days.", "Usage: moda registry coverage [--days=7]");
|
|
2725
|
+
}
|
|
2726
|
+
return readRegistry(context, `/harness-registry/coverage${days ? `?days=${days}` : ""}`);
|
|
2727
|
+
}
|
|
2728
|
+
case "prompt":
|
|
2729
|
+
if (context.outputMode === "human")
|
|
2730
|
+
process.stdout.write(`${REGISTRY_AGENT_BRIEF}
|
|
2731
|
+
`);
|
|
2732
|
+
else
|
|
2733
|
+
context.output.writeData({ prompt: REGISTRY_AGENT_BRIEF });
|
|
2734
|
+
return { exitCode: 0 };
|
|
2136
2735
|
default:
|
|
2137
|
-
throw new CliInputError(`Unknown registry command '${subcommand}'.`, "Usage: moda registry template|validate|push [--file=.moda/registry.json] [--dry-run] [--live]");
|
|
2736
|
+
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
2737
|
}
|
|
2139
2738
|
}
|
|
2140
2739
|
function writeTemplate(context) {
|
|
@@ -2159,6 +2758,9 @@ function writeTemplate(context) {
|
|
|
2159
2758
|
return { exitCode: 0 };
|
|
2160
2759
|
}
|
|
2161
2760
|
async function pushRegistry(context) {
|
|
2761
|
+
const message = context.flags.message;
|
|
2762
|
+
if (message === "true")
|
|
2763
|
+
throw new CliInputError("Missing text for --message.", 'Usage: moda registry push --message="Add billing agent"');
|
|
2162
2764
|
const registry = loadRegistry(context);
|
|
2163
2765
|
const profileOptions = { profile: context.profile, env: context.env };
|
|
2164
2766
|
const hasKey = Boolean(resolveApiKey(profileOptions));
|
|
@@ -2191,7 +2793,20 @@ async function pushRegistry(context) {
|
|
|
2191
2793
|
await push("prompts", "/prompts/sync", registry.prompts.length, { prompts: registry.prompts });
|
|
2192
2794
|
await push("skills", "/skills/sync", registry.skills.length, { skills: registry.skills });
|
|
2193
2795
|
await push("tools", "/tool-definitions/sync", registry.tools.length, { tools: registry.tools });
|
|
2796
|
+
if (registry.harness) {
|
|
2797
|
+
await push("harness", "/harness-registry/sync", 1, { schema: registry.schema, document: registry.harness });
|
|
2798
|
+
}
|
|
2799
|
+
let commit;
|
|
2800
|
+
let commitError;
|
|
2801
|
+
if (!context.dryRun && sections.some((section) => section.status === "synced")) {
|
|
2802
|
+
try {
|
|
2803
|
+
commit = await callControlAPI("/registry/commits", { method: "POST", body: JSON.stringify({ ...message ? { message } : {}, ...gitInfo(context.cwd) }) }, profileOptions);
|
|
2804
|
+
} catch (error) {
|
|
2805
|
+
commitError = error instanceof Error ? error.message : String(error);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2194
2808
|
const failed = sections.filter((section) => section.status === "error");
|
|
2809
|
+
printWarnings(context, registry.warnings);
|
|
2195
2810
|
if (context.outputMode === "human" && !context.quiet) {
|
|
2196
2811
|
for (const section of sections) {
|
|
2197
2812
|
const detail = section.status === "error" ? ` — ${section.error}` : describeActions(section.response);
|
|
@@ -2200,13 +2815,26 @@ async function pushRegistry(context) {
|
|
|
2200
2815
|
if (sections.some((section) => section.section === "tools" && section.status === "synced")) {
|
|
2201
2816
|
console.error(" Replays now give your agent these tools with their real schemas and descriptions.");
|
|
2202
2817
|
}
|
|
2818
|
+
if (sections.some((section) => section.section === "harness" && section.status === "synced")) {
|
|
2819
|
+
console.error(" Replays now rebuild the agent live at each replayed turn. Check `moda registry coverage`.");
|
|
2820
|
+
}
|
|
2821
|
+
const recorded = isRecord2(commit) && isRecord2(commit.commit) ? commit.commit : undefined;
|
|
2822
|
+
if (recorded) {
|
|
2823
|
+
const label = commit && isRecord2(commit) && commit.action === "unchanged" ? "no registry changes since" : "commit";
|
|
2824
|
+
console.error(` ${label} ${String(recorded.id).slice(0, 7)} (#${String(recorded.seq)}) ${String(recorded.summary ?? "")}`);
|
|
2825
|
+
} else if (commitError) {
|
|
2826
|
+
console.error(` commit: not recorded — ${commitError}`);
|
|
2827
|
+
}
|
|
2203
2828
|
}
|
|
2204
2829
|
context.output.writeData({
|
|
2205
|
-
schema:
|
|
2830
|
+
schema: registry.schema,
|
|
2206
2831
|
manifest: displayPath(context, registry.manifestPath),
|
|
2207
2832
|
dryRun: context.dryRun,
|
|
2208
2833
|
counts: countsOf(registry),
|
|
2209
|
-
sections
|
|
2834
|
+
sections,
|
|
2835
|
+
warnings: registry.warnings,
|
|
2836
|
+
...commit !== undefined ? { commit } : {},
|
|
2837
|
+
...commitError ? { commitError } : {}
|
|
2210
2838
|
}, {
|
|
2211
2839
|
status: failed.length > 0 ? "error" : "success",
|
|
2212
2840
|
...failed.length > 0 ? {
|
|
@@ -2219,11 +2847,39 @@ async function pushRegistry(context) {
|
|
|
2219
2847
|
});
|
|
2220
2848
|
return { exitCode: failed.length > 0 ? 1 : 0 };
|
|
2221
2849
|
}
|
|
2850
|
+
function gitInfo(cwd) {
|
|
2851
|
+
const git = (...args) => execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).toString().trim();
|
|
2852
|
+
try {
|
|
2853
|
+
const gitCommit = git("rev-parse", "HEAD");
|
|
2854
|
+
const branch = git("rev-parse", "--abbrev-ref", "HEAD");
|
|
2855
|
+
return {
|
|
2856
|
+
gitCommit,
|
|
2857
|
+
...branch && branch !== "HEAD" ? { gitBranch: branch } : {},
|
|
2858
|
+
gitDirty: git("status", "--porcelain").length > 0
|
|
2859
|
+
};
|
|
2860
|
+
} catch {
|
|
2861
|
+
return {};
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
async function readRegistry(context, endpoint) {
|
|
2865
|
+
const profileOptions = { profile: context.profile, env: context.env };
|
|
2866
|
+
if (!resolveApiKey(profileOptions)) {
|
|
2867
|
+
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.");
|
|
2868
|
+
}
|
|
2869
|
+
context.output.writeData(await callControlAPI(endpoint, { method: "GET" }, profileOptions));
|
|
2870
|
+
return { exitCode: 0 };
|
|
2871
|
+
}
|
|
2872
|
+
function printWarnings(context, warnings) {
|
|
2873
|
+
if (context.outputMode !== "human" || context.quiet)
|
|
2874
|
+
return;
|
|
2875
|
+
for (const warning of warnings)
|
|
2876
|
+
console.error(` warning: ${warning}`);
|
|
2877
|
+
}
|
|
2222
2878
|
function describeActions(response) {
|
|
2223
|
-
const synced =
|
|
2879
|
+
const synced = isRecord2(response) && Array.isArray(response.synced) ? response.synced : [];
|
|
2224
2880
|
const counts = new Map;
|
|
2225
2881
|
for (const item of synced) {
|
|
2226
|
-
if (!
|
|
2882
|
+
if (!isRecord2(item))
|
|
2227
2883
|
continue;
|
|
2228
2884
|
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
2885
|
counts.set(action, (counts.get(action) ?? 0) + 1);
|
|
@@ -2298,13 +2954,17 @@ function checkFields(label, item, spec, errors) {
|
|
|
2298
2954
|
}
|
|
2299
2955
|
function resolveRegistry(input, options) {
|
|
2300
2956
|
const errors = [];
|
|
2301
|
-
|
|
2957
|
+
const warnings = [];
|
|
2958
|
+
if (!isRecord2(input)) {
|
|
2302
2959
|
throw new CliInputError("Registry manifest must be a JSON object with prompts, skills, and/or tools arrays.");
|
|
2303
2960
|
}
|
|
2304
|
-
if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA) {
|
|
2305
|
-
errors.push(`schema must be "${REGISTRY_SCHEMA}" (got ${JSON.stringify(input.schema)})`);
|
|
2961
|
+
if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA && input.schema !== REGISTRY_SCHEMA_V1) {
|
|
2962
|
+
errors.push(`schema must be "${REGISTRY_SCHEMA}" or "${REGISTRY_SCHEMA_V1}" (got ${JSON.stringify(input.schema)})`);
|
|
2963
|
+
}
|
|
2964
|
+
if (input.schema === REGISTRY_SCHEMA_V1 && hasHarnessSections(input)) {
|
|
2965
|
+
errors.push(`${HARNESS_SECTIONS.join(", ")} need schema "${REGISTRY_SCHEMA}"`);
|
|
2306
2966
|
}
|
|
2307
|
-
const known = new Set(["schema", "prompts", "skills", "tools"]);
|
|
2967
|
+
const known = new Set(["schema", "prompts", "skills", "tools", ...HARNESS_SECTIONS]);
|
|
2308
2968
|
for (const key of Object.keys(input)) {
|
|
2309
2969
|
if (!known.has(key))
|
|
2310
2970
|
errors.push(`unknown top-level field "${key}"`);
|
|
@@ -2336,7 +2996,7 @@ function resolveRegistry(input, options) {
|
|
|
2336
2996
|
const sourcePathFor = (file) => relative(options.cwd, resolve2(options.cwd, file)).split("\\").join("/");
|
|
2337
2997
|
const prompts = arrayOf(input.prompts, "prompts", errors).map((raw, index) => {
|
|
2338
2998
|
const label = `prompts[${index}]`;
|
|
2339
|
-
if (!
|
|
2999
|
+
if (!isRecord2(raw)) {
|
|
2340
3000
|
errors.push(`${label} must be an object`);
|
|
2341
3001
|
return;
|
|
2342
3002
|
}
|
|
@@ -2361,7 +3021,7 @@ function resolveRegistry(input, options) {
|
|
|
2361
3021
|
});
|
|
2362
3022
|
const skills = arrayOf(input.skills, "skills", errors).map((raw, index) => {
|
|
2363
3023
|
const label = `skills[${index}]`;
|
|
2364
|
-
if (!
|
|
3024
|
+
if (!isRecord2(raw)) {
|
|
2365
3025
|
errors.push(`${label} must be an object`);
|
|
2366
3026
|
return;
|
|
2367
3027
|
}
|
|
@@ -2403,13 +3063,13 @@ function resolveRegistry(input, options) {
|
|
|
2403
3063
|
const toolNames = new Set;
|
|
2404
3064
|
const tools = arrayOf(input.tools, "tools", errors).map((raw, index) => {
|
|
2405
3065
|
const label = `tools[${index}]`;
|
|
2406
|
-
if (!
|
|
3066
|
+
if (!isRecord2(raw) || !isRecord2(raw.schema)) {
|
|
2407
3067
|
errors.push(`${label}.schema must be an object (OpenAI function schema)`);
|
|
2408
3068
|
return;
|
|
2409
3069
|
}
|
|
2410
3070
|
checkFields(label, raw, TOOL_FIELDS, errors);
|
|
2411
3071
|
const tool = raw;
|
|
2412
|
-
const inner = tool.schema.type === "function" &&
|
|
3072
|
+
const inner = tool.schema.type === "function" && isRecord2(tool.schema.function) ? tool.schema.function : tool.schema;
|
|
2413
3073
|
const schemaName = typeof inner.name === "string" ? inner.name.trim() : undefined;
|
|
2414
3074
|
const name = (typeof tool.name === "string" ? tool.name.trim() : "") || schemaName;
|
|
2415
3075
|
if (typeof tool.name === "string" && tool.name.trim() && schemaName && tool.name.trim() !== schemaName) {
|
|
@@ -2428,9 +3088,9 @@ function resolveRegistry(input, options) {
|
|
|
2428
3088
|
errors.push(`${label}: schema.description must be a string`);
|
|
2429
3089
|
}
|
|
2430
3090
|
if (inner.parameters !== undefined) {
|
|
2431
|
-
if (!
|
|
3091
|
+
if (!isRecord2(inner.parameters) || inner.parameters.type !== "object") {
|
|
2432
3092
|
errors.push(`${label}: schema.parameters.type must be "object"`);
|
|
2433
|
-
} else if (inner.parameters.properties !== undefined && !
|
|
3093
|
+
} else if (inner.parameters.properties !== undefined && !isRecord2(inner.parameters.properties)) {
|
|
2434
3094
|
errors.push(`${label}: schema.parameters.properties must be an object`);
|
|
2435
3095
|
}
|
|
2436
3096
|
}
|
|
@@ -2452,6 +3112,7 @@ function resolveRegistry(input, options) {
|
|
|
2452
3112
|
skillKeys.add(skill.key);
|
|
2453
3113
|
}
|
|
2454
3114
|
}
|
|
3115
|
+
const harness = validateHarnessSections(input, { prompts: promptKeys, tools: toolNames, skills: skillKeys }, errors, warnings);
|
|
2455
3116
|
if (errors.length > 0) {
|
|
2456
3117
|
throw new CliInputError(`Registry manifest is invalid:
|
|
2457
3118
|
- ${errors.join(`
|
|
@@ -2459,12 +3120,15 @@ function resolveRegistry(input, options) {
|
|
|
2459
3120
|
}
|
|
2460
3121
|
const resolved = {
|
|
2461
3122
|
manifestPath: options.manifestPath,
|
|
3123
|
+
schema: typeof input.schema === "string" ? input.schema : harness ? REGISTRY_SCHEMA : REGISTRY_SCHEMA_V1,
|
|
2462
3124
|
prompts: prompts.filter(isDefined),
|
|
2463
3125
|
skills: skills.filter(isDefined),
|
|
2464
|
-
tools: tools.filter(isDefined)
|
|
3126
|
+
tools: tools.filter(isDefined),
|
|
3127
|
+
...harness ? { harness } : {},
|
|
3128
|
+
warnings
|
|
2465
3129
|
};
|
|
2466
|
-
if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0) {
|
|
2467
|
-
throw new CliInputError("Registry manifest has no prompts, skills, or
|
|
3130
|
+
if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0 && !harness) {
|
|
3131
|
+
throw new CliInputError("Registry manifest has no prompts, skills, tools, or agents to push.");
|
|
2468
3132
|
}
|
|
2469
3133
|
return resolved;
|
|
2470
3134
|
}
|
|
@@ -2481,7 +3145,16 @@ function arrayOf(value, label, errors) {
|
|
|
2481
3145
|
return value;
|
|
2482
3146
|
}
|
|
2483
3147
|
function countsOf(registry) {
|
|
2484
|
-
return {
|
|
3148
|
+
return {
|
|
3149
|
+
prompts: registry.prompts.length,
|
|
3150
|
+
skills: registry.skills.length,
|
|
3151
|
+
tools: registry.tools.length,
|
|
3152
|
+
...registry.harness ? {
|
|
3153
|
+
agents: registry.harness.agents?.length ?? 0,
|
|
3154
|
+
context: registry.harness.context?.length ?? 0,
|
|
3155
|
+
mcpServers: registry.harness.mcpServers?.length ?? 0
|
|
3156
|
+
} : {}
|
|
3157
|
+
};
|
|
2485
3158
|
}
|
|
2486
3159
|
function displayPath(context, path) {
|
|
2487
3160
|
const rel = relative(context.cwd, path);
|
|
@@ -2491,7 +3164,7 @@ function isInside(root, target) {
|
|
|
2491
3164
|
const rel = relative(resolve2(root), target);
|
|
2492
3165
|
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
2493
3166
|
}
|
|
2494
|
-
function
|
|
3167
|
+
function isRecord2(value) {
|
|
2495
3168
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2496
3169
|
}
|
|
2497
3170
|
function nonEmptyString(value) {
|
|
@@ -7198,7 +7871,7 @@ Commands:
|
|
|
7198
7871
|
skills sync Push local .claude/skills/**/SKILL.md up to Moda
|
|
7199
7872
|
skills sync --dry-run Show what would be pushed without writing
|
|
7200
7873
|
skills list|show|policy List pushed skills, show one, or set --live/--local
|
|
7201
|
-
registry push Push prompts, skills, and
|
|
7874
|
+
registry push Push prompts, skills, tools, and agent wiring from one JSON manifest
|
|
7202
7875
|
|
|
7203
7876
|
Options:
|
|
7204
7877
|
--help Show this help message
|
|
@@ -7993,7 +8666,7 @@ async function searchFallbackToTraces(params) {
|
|
|
7993
8666
|
last_timestamp: row.last_timestamp ?? null
|
|
7994
8667
|
}));
|
|
7995
8668
|
const first = asString(rows[0].conversation_id);
|
|
7996
|
-
const
|
|
8669
|
+
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
8670
|
const nextCommands = [];
|
|
7998
8671
|
if (first) {
|
|
7999
8672
|
nextCommands.push({ command: `moda audit ${first}`, purpose: "Open the top trace to locate the relevant message.", mutability: "read", requires_approval: false });
|
|
@@ -8018,7 +8691,7 @@ async function searchFallbackToTraces(params) {
|
|
|
8018
8691
|
...hasMore ? { next_offset: nextOffset } : {}
|
|
8019
8692
|
}
|
|
8020
8693
|
},
|
|
8021
|
-
warning: `Message-level search returned 0 results, so these are ${
|
|
8694
|
+
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
8695
|
nextCommands
|
|
8023
8696
|
};
|
|
8024
8697
|
}
|
|
@@ -9681,17 +10354,19 @@ var commandRegistry = createCommandRegistry([
|
|
|
9681
10354
|
},
|
|
9682
10355
|
{
|
|
9683
10356
|
name: "registry",
|
|
9684
|
-
description: "Push prompts, skills, and
|
|
10357
|
+
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
10358
|
examples: [
|
|
10359
|
+
"moda registry prompt",
|
|
9686
10360
|
"moda registry template --out=.moda/registry.json",
|
|
9687
10361
|
"moda registry validate",
|
|
9688
10362
|
"moda registry push --dry-run",
|
|
9689
|
-
"moda registry push --file=.moda/registry.json"
|
|
10363
|
+
"moda registry push --file=.moda/registry.json",
|
|
10364
|
+
"moda registry coverage --days=7"
|
|
9690
10365
|
],
|
|
9691
10366
|
subcommands: [
|
|
9692
10367
|
{
|
|
9693
10368
|
name: "template",
|
|
9694
|
-
description: "Print (or write with --out) an example moda_registry.
|
|
10369
|
+
description: "Print (or write with --out) an example moda_registry.v2 manifest",
|
|
9695
10370
|
usage: "moda registry template [--out=<path>] [--yes]",
|
|
9696
10371
|
flags: [
|
|
9697
10372
|
{ name: "--out=<path>", description: "Write the template to a file instead of stdout (refuses to overwrite without --yes)." }
|
|
@@ -9707,18 +10382,51 @@ var commandRegistry = createCommandRegistry([
|
|
|
9707
10382
|
},
|
|
9708
10383
|
{
|
|
9709
10384
|
name: "push",
|
|
9710
|
-
description: "Upload the manifest's prompts, skills, and
|
|
9711
|
-
usage: "moda registry push [--file=<path>] [--dry-run] [--live]",
|
|
10385
|
+
description: "Upload the manifest's prompts, skills, tools, and harness sections to the API key's tenant",
|
|
10386
|
+
usage: "moda registry push [--file=<path>] [--dry-run] [--live] [--message=<text>]",
|
|
9712
10387
|
flags: [
|
|
9713
10388
|
{ name: "--file=<path>", description: "Manifest path (default .moda/registry.json)." },
|
|
10389
|
+
{ name: "--message=<text>", description: "Message for the registry commit this push records (shown in the dashboard Registry history)." },
|
|
9714
10390
|
{ name: "--dry-run", description: "Server-side preview; nothing is written. Without an API key, validates locally only." },
|
|
9715
10391
|
{ name: "--live", description: "Mark every pushed skill live (included in the served skill surface)." }
|
|
9716
10392
|
],
|
|
9717
10393
|
examples: ["moda registry push --dry-run", "moda registry push --live"]
|
|
10394
|
+
},
|
|
10395
|
+
{
|
|
10396
|
+
name: "status",
|
|
10397
|
+
description: "Show the registered harness (agents, routing, context) and its version",
|
|
10398
|
+
usage: "moda registry status",
|
|
10399
|
+
examples: ["moda registry status"]
|
|
10400
|
+
},
|
|
10401
|
+
{
|
|
10402
|
+
name: "log",
|
|
10403
|
+
description: "Registry history: one commit per push, with what changed",
|
|
10404
|
+
usage: "moda registry log [--limit=20]",
|
|
10405
|
+
flags: [{ name: "--limit=<n>", description: "Commits to list (default 50)." }],
|
|
10406
|
+
examples: ["moda registry log --limit=10"]
|
|
10407
|
+
},
|
|
10408
|
+
{
|
|
10409
|
+
name: "show",
|
|
10410
|
+
description: `A registry commit's file diffs, or "working" for changes since the last push`,
|
|
10411
|
+
usage: "moda registry show <commit|working>",
|
|
10412
|
+
examples: ["moda registry show 3f9a2c1", "moda registry show working"]
|
|
10413
|
+
},
|
|
10414
|
+
{
|
|
10415
|
+
name: "coverage",
|
|
10416
|
+
description: "Compare recent production traces with the registry: attribution rate, unmatched agents and prompts, unregistered tools",
|
|
10417
|
+
usage: "moda registry coverage [--days=7]",
|
|
10418
|
+
flags: [{ name: "--days=<n>", description: "Look-back window in days (default 7, max 30)." }],
|
|
10419
|
+
examples: ["moda registry coverage --days=7"]
|
|
10420
|
+
},
|
|
10421
|
+
{
|
|
10422
|
+
name: "prompt",
|
|
10423
|
+
description: "Print the brief to hand your coding agent so it inventories and registers your harness",
|
|
10424
|
+
usage: "moda registry prompt",
|
|
10425
|
+
examples: ["moda registry prompt | pbcopy", 'claude "$(moda registry prompt)"']
|
|
9718
10426
|
}
|
|
9719
10427
|
],
|
|
9720
10428
|
auth: "api-key",
|
|
9721
|
-
authNote: "template and
|
|
10429
|
+
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
10430
|
outputModes: ["human", "json", "agent"],
|
|
9723
10431
|
mutability: "write",
|
|
9724
10432
|
validateConfigBeforeRun: false,
|
package/package.json
CHANGED
package/skills/moda-cli/SKILL.md
CHANGED
|
@@ -39,8 +39,9 @@ Activate this skill when the user wants to:
|
|
|
39
39
|
- List/filter traces by user, environment, cluster, outcome, or time
|
|
40
40
|
- Manage code-first prompt versions when the user explicitly asks for prompt
|
|
41
41
|
sync, prompt status, or prompt promotion
|
|
42
|
-
- Push their prompts, skills,
|
|
43
|
-
|
|
42
|
+
- Push their prompts, skills, tool definitions, and agent wiring (agents,
|
|
43
|
+
routing, injected context) to Moda without a repo scan, so replays rebuild
|
|
44
|
+
their harness — `moda registry push` (section 7b)
|
|
44
45
|
|
|
45
46
|
**Do not** activate for: writing code that calls the Moda Data API directly
|
|
46
47
|
(use the API docs instead), changing application instrumentation unless the
|
|
@@ -832,41 +833,66 @@ Page with `--cursor=<nextCursor>`. Playouts exist only for runs whose
|
|
|
832
833
|
playouts were captured; `playoutCount: 0` on `run` means there is no
|
|
833
834
|
transcript to read.
|
|
834
835
|
|
|
835
|
-
### 7b.
|
|
836
|
+
### 7b. Register the harness for replays (`moda registry`)
|
|
836
837
|
|
|
837
|
-
Use when the user wants their prompts, skills,
|
|
838
|
-
|
|
839
|
-
analyze`.
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
repo is uploaded.
|
|
838
|
+
Use when the user wants their prompts, skills, tools, or agent wiring in Moda
|
|
839
|
+
so replays rebuild their harness faithfully, without the GitHub App or `moda
|
|
840
|
+
harness analyze`. **You** are the analyst: read the codebase you already have,
|
|
841
|
+
write one manifest, and push exactly what it lists. Nothing else in the repo is
|
|
842
|
+
uploaded. For a full guided pass (inventory, prompt switching, context,
|
|
843
|
+
attribution, and verification), run `moda registry prompt` and follow the
|
|
844
|
+
brief it prints.
|
|
845
845
|
|
|
846
846
|
```bash
|
|
847
|
-
moda registry
|
|
847
|
+
moda registry prompt # the step-by-step brief for this job
|
|
848
|
+
moda registry template --out=.moda/registry.json # starter manifest (moda_registry.v2)
|
|
848
849
|
# ...fill in .moda/registry.json from the codebase...
|
|
849
850
|
moda registry validate # local only, no network
|
|
850
851
|
moda registry push --dry-run # server-side preview, writes nothing
|
|
851
852
|
moda registry push # upload
|
|
853
|
+
moda registry status # what is registered (harness version)
|
|
854
|
+
moda registry coverage --days=7 # production traces vs the registry
|
|
855
|
+
moda registry log # history: one commit per push
|
|
856
|
+
moda registry show <commit|working> # a commit's file diffs / changes since the last push
|
|
852
857
|
```
|
|
853
858
|
|
|
854
|
-
|
|
859
|
+
Every successful `push` records a registry **commit**: the whole registry
|
|
860
|
+
(prompts, tools, skills, agents, harness settings) snapshotted as files, with
|
|
861
|
+
your repo's git SHA, branch, and dirty flag. Pass `--message="..."` to label
|
|
862
|
+
it. The dashboard **Registry** page shows these commits git-style: a log,
|
|
863
|
+
per-file diffs, a file browser, and uncommitted changes made outside a push
|
|
864
|
+
(dashboard edits, MCP discovery, `moda prompts sync`).
|
|
865
|
+
|
|
866
|
+
Manifest (`moda_registry.v2`; `moda_registry.v1` with only prompts, skills, and
|
|
867
|
+
tools still works; every section optional, ≤500 entries in prompts, skills, and tools):
|
|
855
868
|
|
|
856
869
|
```json
|
|
857
870
|
{
|
|
858
|
-
"schema": "moda_registry.
|
|
871
|
+
"schema": "moda_registry.v2",
|
|
859
872
|
"prompts": [
|
|
860
|
-
{ "key": "support.triage", "
|
|
861
|
-
|
|
862
|
-
{ "key": "support.reply", "file": "prompts/reply.md" }
|
|
873
|
+
{ "key": "support.triage", "file": "prompts/triage.md" },
|
|
874
|
+
{ "key": "support.billing", "content": "You handle billing for {{company_name}}..." }
|
|
863
875
|
],
|
|
864
876
|
"skills": [
|
|
865
|
-
{ "key": "refund-policy", "
|
|
877
|
+
{ "key": "refund-policy", "file": ".claude/skills/refund-policy/SKILL.md" }
|
|
866
878
|
],
|
|
867
879
|
"tools": [
|
|
868
880
|
{ "schema": { "name": "lookup_customer", "description": "...",
|
|
869
881
|
"parameters": { "type": "object", "properties": { "email": { "type": "string" } } } } }
|
|
882
|
+
],
|
|
883
|
+
"runtime": { "framework": "openai-agents", "maxToolRoundsPerTurn": 6, "skillLoading": "tool" },
|
|
884
|
+
"agents": [
|
|
885
|
+
{ "key": "triage", "entry": true, "prompt": "support.triage",
|
|
886
|
+
"model": { "id": "openai/gpt-5.1", "temperature": 0.2, "maxTokens": 1024 },
|
|
887
|
+
"tools": ["lookup_customer"],
|
|
888
|
+
"handoffs": [{ "to": "billing", "tool": "transfer_to_billing" }],
|
|
889
|
+
"match": { "agentNames": ["TriageAgent"] } },
|
|
890
|
+
{ "key": "billing", "prompt": "support.billing", "skills": ["refund-policy"] }
|
|
891
|
+
],
|
|
892
|
+
"routing": { "strategy": "handoff", "default": "triage" },
|
|
893
|
+
"context": [
|
|
894
|
+
{ "key": "company", "variable": "company_name", "source": "static", "value": "Acme" },
|
|
895
|
+
{ "key": "today", "variable": "current_date", "source": "clock", "format": "date" }
|
|
870
896
|
]
|
|
871
897
|
}
|
|
872
898
|
```
|
|
@@ -881,24 +907,64 @@ Manifest (`moda_registry.v1`; every section optional, ≤500 entries each):
|
|
|
881
907
|
- **Skills:** `key` (letters, digits, `_`, `-`) plus `skillMd` or `file` (full
|
|
882
908
|
SKILL.md). Pushed skills are *local* by default. Set `"live": true`, or pass
|
|
883
909
|
`--live`, to include them in the served skill surface. Leaving `live` out
|
|
884
|
-
never demotes a skill that is already live.
|
|
910
|
+
never demotes a skill that is already live. Replays load the skills an agent
|
|
911
|
+
lists in `agents[].skills`, whether or not they are live.
|
|
885
912
|
- **Tools:** an OpenAI function `schema` (a wrapped
|
|
886
913
|
`{"type":"function","function":{...}}` is accepted), plus optional
|
|
887
914
|
`isActive`. Copy names and parameter schemas exactly as the agent registers
|
|
888
915
|
them with the model. Tools are upserted by name and never overwrite
|
|
889
916
|
MCP-server-managed tools (those report `skipped`).
|
|
917
|
+
- **Agents:** one per distinct prompt, tool set, or model. Fields: `key`,
|
|
918
|
+
`name`, `description`, `entry`, `prompt` (prompt key), `model` (`id`,
|
|
919
|
+
`provider`, `temperature`, `maxTokens`, `topP`, `reasoningEffort`), `tools`
|
|
920
|
+
(tool names), `skills` (skill keys), `handoffs` (`to`, `tool`,
|
|
921
|
+
`description`), `subagents` (`agent`, `tool`; the tool must also be in
|
|
922
|
+
`tools`), `context` (context keys), `maxToolRoundsPerTurn`,
|
|
923
|
+
`responseSchema`, `match` (`agentNames` as they appear in traces,
|
|
924
|
+
`promptKeys`).
|
|
925
|
+
- **Routing (prompt switching):** `strategy` is `single`, `handoff`, `router`,
|
|
926
|
+
`rules`, or `trace_attribute`; `default` is an agent key; put code-level
|
|
927
|
+
rules in `notes`. Replay runs the agent named on the replayed turn's trace
|
|
928
|
+
(agent name, then prompt key), else `default`, else the `entry` agent. When
|
|
929
|
+
the replayed model calls a handoff tool, replay switches to the target
|
|
930
|
+
agent's prompt, tools, model, and params.
|
|
931
|
+
- **Context:** what the runtime injects. `source` is `static` (with `value`),
|
|
932
|
+
`clock` (`format`: date, datetime, or iso; replay uses the source
|
|
933
|
+
conversation's time), or `trace`, `memory`, `retrieval`, `tool`, or
|
|
934
|
+
`unknown`. Those last ones are reported as unresolved in replay fidelity,
|
|
935
|
+
never invented. `target` is `prompt_variable` (needs `variable`) or
|
|
936
|
+
`system_append`.
|
|
937
|
+
- **Runtime:** `framework`, `language`, `maxToolRoundsPerTurn`,
|
|
938
|
+
`parallelToolCalls`, `history` (`strategy`, `maxMessages`, `maxTokens`),
|
|
939
|
+
and `skillLoading` (`tool`, `system_prompt`, or `none`).
|
|
940
|
+
- **MCP servers:** `name`, `transport`, `url` or `command`, `tools`, and
|
|
941
|
+
`description`, with no credentials. List their tools in `tools[]` too.
|
|
890
942
|
- `file` paths are relative to the directory you run the command from and must
|
|
891
943
|
stay inside it (symlinks included).
|
|
892
944
|
|
|
945
|
+
**Traces must name the agent.** For per-turn prompt switching, each LLM call
|
|
946
|
+
should carry `gen_ai.agent.name` (or `moda.agent_name`; with the Vercel AI SDK
|
|
947
|
+
use `experimental_telemetry.metadata["moda.agent_name"]`; `agent_name` on
|
|
948
|
+
`/v1/ingest`), set to an agent `key` or one of its `match.agentNames`. It
|
|
949
|
+
should also carry `moda.prompt_key` / `moda.prompt_version_id`. Only add these
|
|
950
|
+
to application code when the user asks for it or approves the diff.
|
|
951
|
+
`moda registry coverage` shows how many recent calls resolve to a registered
|
|
952
|
+
agent, which agent names, prompt keys, and tools are unregistered, and which
|
|
953
|
+
agents were never seen.
|
|
954
|
+
|
|
893
955
|
`push` needs an API key. `push --dry-run` without one only validates locally
|
|
894
956
|
(status `validated`). Each section goes to its own endpoint and reports
|
|
895
957
|
`sections[].status` (`synced`, `dry_run`, `validated`, `skipped`, or `error`).
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
958
|
+
The harness sections (`runtime`, `agents`, `routing`, `context`, and
|
|
959
|
+
`mcpServers`) are pushed last, as one versioned document. Re-pushing an
|
|
960
|
+
unchanged one reports `unchanged`. If any section fails, `push` exits `1`,
|
|
961
|
+
lists the failures in `errors[]`, and still pushes the other sections. The
|
|
962
|
+
whole manifest is validated before any request, with every error listed at
|
|
963
|
+
once. `warnings[]` names agent references to prompts, tools, or skills that
|
|
964
|
+
the manifest doesn't define; they must already exist in Moda. Unknown fields
|
|
965
|
+
and wrong types are rejected, matching what the server accepts. Skill keys are
|
|
966
|
+
stricter than the server (letters, digits, `_`, `-`) because they double as
|
|
967
|
+
`.claude/skills/<key>/` directory names.
|
|
902
968
|
|
|
903
969
|
### 8. Fixing detected Problems
|
|
904
970
|
|
|
@@ -1226,7 +1292,7 @@ npx: `npx -p @moda-ai/cli moda <command>`.
|
|
|
1226
1292
|
| `moda prompts diff` | Read-only: only prompts changed/new/deleted since last sync (hash-level, not a text diff) |
|
|
1227
1293
|
| `moda prompts sync` | Upload changed prompt versions and update lockfile |
|
|
1228
1294
|
| `moda prompts promote <key>` | Move a remote prompt label |
|
|
1229
|
-
| `moda registry template\|validate\|push` |
|
|
1295
|
+
| `moda registry template\|validate\|push\|status\|coverage\|log\|show\|prompt` | Register prompts, skills, tools, and agent wiring from one agent-written manifest (`--file`, `--dry-run`, `--live`, `--message`, `--days`); every push is a commit in the Registry history; no repo scan |
|
|
1230
1296
|
| `moda users` / `moda user <id>` | End users with health/activity; one user's profile |
|
|
1231
1297
|
| `moda problem-reopen <pid>` | Reopen a Problem marked fixed |
|
|
1232
1298
|
| `moda signals <create\|update\|activate\|pause\|delete\|examples\|refine\|revise\|proposals\|accept\|reject\|search\|preview>` | Manage custom signals (writes take `--dry-run`; LLM-backed ones are rate-limited per key) |
|