code-ai-installer 4.0.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +5 -3
  2. package/dist/mcp/audit_ledger.d.ts +10 -0
  3. package/dist/mcp/audit_ledger.js +15 -0
  4. package/dist/mcp/cli.js +1 -0
  5. package/dist/mcp/tools/render_diff.d.ts +25 -0
  6. package/dist/mcp/tools/render_diff.js +138 -0
  7. package/dist/mcp/tools/sign_off.js +10 -5
  8. package/dist/mcp/tools/stubs.js +2 -0
  9. package/dist/mcp_setup.d.ts +79 -48
  10. package/dist/mcp_setup.js +215 -107
  11. package/dist/shared/tools.d.ts +38 -0
  12. package/dist/shared/tools.js +24 -0
  13. package/domains/analytics/agents/conductor.md +15 -1
  14. package/domains/analytics/locales/en/agents/conductor.md +15 -1
  15. package/domains/content/agents/conductor.md +15 -1
  16. package/domains/content/locales/en/agents/conductor.md +15 -1
  17. package/domains/development/.agents/skills/mcp-integration/SKILL.md +3 -1
  18. package/domains/development/.agents/workflows/audit.md +25 -0
  19. package/domains/development/.agents/workflows/pipeline-rules.md +1 -0
  20. package/domains/development/AGENTS.md +1 -0
  21. package/domains/development/agents/architect.md +1 -1
  22. package/domains/development/agents/auditor.md +4 -3
  23. package/domains/development/agents/conductor.md +4 -1
  24. package/domains/development/agents/devops.md +1 -1
  25. package/domains/development/agents/reviewer.md +2 -1
  26. package/domains/development/agents/senior_full_stack.md +1 -1
  27. package/domains/development/agents/tester.md +1 -1
  28. package/domains/development/locales/en/.agents/skills/mcp-integration/SKILL.md +3 -1
  29. package/domains/development/locales/en/.agents/workflows/audit.md +25 -0
  30. package/domains/development/locales/en/.agents/workflows/pipeline-rules.md +1 -0
  31. package/domains/development/locales/en/AGENTS.md +2 -0
  32. package/domains/development/locales/en/agents/architect.md +1 -1
  33. package/domains/development/locales/en/agents/auditor.md +4 -3
  34. package/domains/development/locales/en/agents/conductor.md +4 -1
  35. package/domains/development/locales/en/agents/devops.md +1 -1
  36. package/domains/development/locales/en/agents/reviewer.md +2 -1
  37. package/domains/development/locales/en/agents/senior_full_stack.md +1 -1
  38. package/domains/development/locales/en/agents/tester.md +1 -1
  39. package/domains/product/agents/conductor.md +15 -1
  40. package/domains/product/locales/en/agents/conductor.md +15 -1
  41. package/package.json +1 -1
package/dist/mcp_setup.js CHANGED
@@ -1,9 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
3
4
  import { join } from "node:path";
4
- const defaultClaudeCli = {
5
- run: (args) => spawnCapture("claude", args),
6
- };
7
5
  /**
8
6
  * Try `mempalace-mcp --help` — the dedicated MCP-server bin, which is exactly
9
7
  * what we register. Resolves true on exit code 0, false otherwise (including
@@ -14,11 +12,6 @@ const defaultClaudeCli = {
14
12
  export async function detectMemPalace() {
15
13
  return spawnExitZero("mempalace-mcp", ["--help"]);
16
14
  }
17
- /** True when the `claude` CLI is on PATH (probed via `claude --version`). */
18
- export async function detectClaudeCli(cli = defaultClaudeCli) {
19
- const res = await cli.run(["--version"]);
20
- return res.ok;
21
- }
22
15
  /**
23
16
  * Find the first available Python install runtime, in preference order:
24
17
  * uv → pipx → pip. Returns null if none available.
@@ -56,55 +49,118 @@ function installCommandArgs(runtime) {
56
49
  }
57
50
  }
58
51
  /**
59
- * Build the argv for `claude mcp add --scope user <name> -- <command> [args]`.
60
- * Pure — exported for testing. Env vars become `-e KEY=VALUE` flags placed
61
- * before the server name (as `claude mcp add` expects options first).
52
+ * Resolve the Claude user config path. Honours `CLAUDE_CONFIG_DIR` when set,
53
+ * else `~/.claude.json`.
62
54
  */
63
- export function buildClaudeAddArgs(name, entry) {
64
- const envFlags = entry.env
65
- ? Object.entries(entry.env).flatMap(([k, v]) => ["-e", `${k}=${v}`])
66
- : [];
67
- return ["mcp", "add", ...envFlags, "--scope", "user", name, "--", entry.command, ...entry.args];
55
+ export function userConfigPath() {
56
+ const dir = process.env.CLAUDE_CONFIG_DIR?.trim();
57
+ return dir ? join(dir, ".claude.json") : join(homedir(), ".claude.json");
58
+ }
59
+ /** Real filesystem-backed UserConfigIO. Factory so tests can target a temp file. */
60
+ export function createUserConfigIO(configPath) {
61
+ return {
62
+ configPath,
63
+ async read() {
64
+ let raw;
65
+ try {
66
+ raw = await readFile(configPath, "utf8");
67
+ }
68
+ catch (err) {
69
+ const e = err;
70
+ return { ok: false, error: e.code === "ENOENT" ? "file not found" : e.message };
71
+ }
72
+ try {
73
+ const data = JSON.parse(raw);
74
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
75
+ return { ok: false, error: "config is not a JSON object" };
76
+ }
77
+ return { ok: true, data: data };
78
+ }
79
+ catch (err) {
80
+ return { ok: false, error: `JSON parse error: ${err.message}` };
81
+ }
82
+ },
83
+ async writeWithBackup(data) {
84
+ const backupPath = `${configPath}.bak`;
85
+ try {
86
+ try {
87
+ await copyFile(configPath, backupPath);
88
+ }
89
+ catch (err) {
90
+ // Only tolerate "original did not exist"; surface anything else.
91
+ if (err.code !== "ENOENT")
92
+ throw err;
93
+ }
94
+ const tmpPath = `${configPath}.code-ai.tmp`;
95
+ await writeFile(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf8");
96
+ await rename(tmpPath, configPath);
97
+ return { ok: true, backupPath };
98
+ }
99
+ catch (err) {
100
+ return { ok: false, error: err.message };
101
+ }
102
+ },
103
+ };
68
104
  }
69
- /** Build the argv for `claude mcp remove --scope user <name>`. Pure — exported for testing. */
70
- export function buildClaudeRemoveArgs(name) {
71
- return ["mcp", "remove", "--scope", "user", name];
105
+ const defaultUserConfigIO = createUserConfigIO(userConfigPath());
106
+ /** Shape a server entry the way Claude stores user-scope stdio servers. */
107
+ function toUserScopeEntry(entry) {
108
+ return {
109
+ type: "stdio",
110
+ command: entry.command,
111
+ args: [...entry.args],
112
+ env: entry.env ?? {},
113
+ };
72
114
  }
73
- /** Human-readable manual command, for the no-CLI fallback notice. */
74
- function manualAddCommand(name, entry) {
75
- return `claude ${buildClaudeAddArgs(name, entry).join(" ")}`;
115
+ function readMcpServers(config) {
116
+ const existing = config.mcpServers;
117
+ return existing && typeof existing === "object" && !Array.isArray(existing)
118
+ ? { ...existing }
119
+ : {};
76
120
  }
77
121
  /**
78
- * Is `name` already registered in Claude's USER scope? Uses `claude mcp get`,
79
- * whose output prints `Scope: User config` for user-scope servers. A server in
80
- * a different scope (local/project) returns false here we still want it in
81
- * user scope.
122
+ * Idempotently merge `servers` into the config's top-level `mcpServers`. Pure
123
+ * returns a new config object plus which names were freshly added vs already
124
+ * present. An existing key is NEVER overwritten (so a pre-existing global
125
+ * `mempalace` is preserved untouched). Exported for unit testing.
82
126
  */
83
- export async function isRegisteredInUserScope(cli, name) {
84
- const res = await cli.run(["mcp", "get", name]);
85
- return res.ok && /Scope:\s*User config/i.test(res.output);
127
+ export function mergeUserScopeServers(config, servers) {
128
+ const next = { ...config };
129
+ const mcpServers = readMcpServers(config);
130
+ const registered = [];
131
+ const alreadyPresent = [];
132
+ for (const [name, entry] of Object.entries(servers)) {
133
+ if (Object.prototype.hasOwnProperty.call(mcpServers, name)) {
134
+ alreadyPresent.push(name);
135
+ continue;
136
+ }
137
+ mcpServers[name] = toUserScopeEntry(entry);
138
+ registered.push(name);
139
+ }
140
+ next.mcpServers = mcpServers;
141
+ return { config: next, registered, alreadyPresent };
86
142
  }
87
143
  /**
88
- * Register one server in user scope, idempotently. Skips if already present in
89
- * user scope. Never throws returns the outcome + a notice for the report.
144
+ * Idempotently remove `names` from the config's `mcpServers`. Pure returns a
145
+ * new config object plus which names were removed vs were not present. Exported
146
+ * for unit testing.
90
147
  */
91
- async function addServerToUserScope(cli, name, entry) {
92
- if (await isRegisteredInUserScope(cli, name)) {
93
- return {
94
- outcome: "already-present",
95
- notice: `MCP server '${name}' already registered in user scope — left untouched.`,
96
- };
97
- }
98
- const res = await cli.run(buildClaudeAddArgs(name, entry));
99
- if (res.ok) {
100
- return { outcome: "registered", notice: `Registered MCP server '${name}' in user scope (global).` };
148
+ export function removeUserScopeServers(config, names) {
149
+ const next = { ...config };
150
+ const mcpServers = readMcpServers(config);
151
+ const removed = [];
152
+ const notPresent = [];
153
+ for (const name of names) {
154
+ if (Object.prototype.hasOwnProperty.call(mcpServers, name)) {
155
+ delete mcpServers[name];
156
+ removed.push(name);
157
+ }
158
+ else {
159
+ notPresent.push(name);
160
+ }
101
161
  }
102
- return {
103
- outcome: "failed",
104
- notice: `Failed to register '${name}' in user scope. Run it manually:\n` +
105
- ` ${manualAddCommand(name, entry)}\n` +
106
- ` Output:\n${res.output}`,
107
- };
162
+ next.mcpServers = mcpServers;
163
+ return { config: next, removed, notPresent };
108
164
  }
109
165
  /**
110
166
  * Write `<destinationDir>/.code-ai/config.json` with the chosen backend.
@@ -142,21 +198,25 @@ function buildServerEntries(mempalaceUsed) {
142
198
  }
143
199
  return servers;
144
200
  }
201
+ /** Format a single "name: entry-json" line for the manual-fallback notice. */
202
+ function manualEntryLine(name, entry) {
203
+ return ` "${name}": ${JSON.stringify(toUserScopeEntry(entry))}`;
204
+ }
145
205
  /**
146
206
  * End-to-end orchestrator. Called from the install flow when `target=claude`.
147
207
  *
148
208
  * Order:
149
209
  * 1. If user wants MemPalace: detect → install if absent → fall back if install fails.
150
210
  * 2. Build server entries (code-ai-mcp always; mempalace only when usable).
151
- * 3. Register them in Claude USER scope via `claude mcp add --scope user`
152
- * (idempotent; skip if already present). If `claude` CLI is absent, emit
153
- * manual commands instead of touching the config by hand.
211
+ * 3. Merge them into the Claude user config's `mcpServers` (idempotent; skip
212
+ * already-present). If the config can't be read/written, print the exact
213
+ * JSON to add by hand instead of guessing.
154
214
  * 4. Write project-local `.code-ai/config.json` with the backend choice.
155
215
  *
156
216
  * Reports all decisions in `McpSetupReport.notices` so callers can surface
157
- * them to the user verbatim. `cli` is injectable for testing.
217
+ * them to the user verbatim. `io` is injectable for testing.
158
218
  */
159
- export async function setupMcp(opts, cli = defaultClaudeCli) {
219
+ export async function setupMcp(opts, io = defaultUserConfigIO) {
160
220
  const notices = [];
161
221
  let mempalaceUsed = false;
162
222
  let mempalaceInstallAttempted = false;
@@ -191,33 +251,49 @@ export async function setupMcp(opts, cli = defaultClaudeCli) {
191
251
  const serversRegistered = [];
192
252
  const serversAlreadyPresent = [];
193
253
  const serversFailed = [];
194
- const claudeCliAvailable = !opts.dryRun && (await detectClaudeCli(cli));
195
254
  let registration;
196
255
  if (opts.dryRun) {
197
256
  registration = "user-scope";
198
257
  for (const [name, entry] of Object.entries(servers)) {
199
- notices.push(`Would register MCP server '${name}' in user scope: ${manualAddCommand(name, entry)}`);
200
- }
201
- }
202
- else if (claudeCliAvailable) {
203
- registration = "user-scope";
204
- for (const [name, entry] of Object.entries(servers)) {
205
- const { outcome, notice } = await addServerToUserScope(cli, name, entry);
206
- notices.push(notice);
207
- if (outcome === "registered")
208
- serversRegistered.push(name);
209
- else if (outcome === "already-present")
210
- serversAlreadyPresent.push(name);
211
- else
212
- serversFailed.push(name);
258
+ notices.push(`Would register MCP server '${name}' in Claude user config (${io.configPath}): ${JSON.stringify(toUserScopeEntry(entry))}`);
213
259
  }
214
260
  }
215
261
  else {
216
- registration = "manual-fallback";
217
- notices.push("The `claude` CLI was not found on PATH — not modifying your global config automatically. " +
218
- "Register the MCP server(s) in user scope by running:");
219
- for (const [name, entry] of Object.entries(servers)) {
220
- notices.push(` ${manualAddCommand(name, entry)}`);
262
+ const read = await io.read();
263
+ if (!read.ok || !read.data) {
264
+ registration = "manual-fallback";
265
+ notices.push(`Could not read Claude user config at ${io.configPath} (${read.error ?? "unknown error"}). ` +
266
+ `Not modifying it automatically — add these entries to its "mcpServers" object by hand:`);
267
+ for (const [name, entry] of Object.entries(servers))
268
+ notices.push(manualEntryLine(name, entry));
269
+ }
270
+ else {
271
+ const merged = mergeUserScopeServers(read.data, servers);
272
+ serversAlreadyPresent.push(...merged.alreadyPresent);
273
+ for (const name of merged.alreadyPresent) {
274
+ notices.push(`MCP server '${name}' already in user config — left untouched.`);
275
+ }
276
+ if (merged.registered.length === 0) {
277
+ registration = "user-scope";
278
+ }
279
+ else {
280
+ const written = await io.writeWithBackup(merged.config);
281
+ if (written.ok) {
282
+ registration = "user-scope";
283
+ serversRegistered.push(...merged.registered);
284
+ notices.push(`Registered MCP server(s) [${merged.registered.join(", ")}] in Claude user config ${io.configPath}` +
285
+ (written.backupPath ? ` (backup: ${written.backupPath})` : "") +
286
+ ". Restart your Claude Code session to load them.");
287
+ }
288
+ else {
289
+ registration = "manual-fallback";
290
+ serversFailed.push(...merged.registered);
291
+ notices.push(`Failed to write Claude user config at ${io.configPath} (${written.error ?? "unknown error"}). ` +
292
+ `No changes made — add these entries to its "mcpServers" object by hand:`);
293
+ for (const name of merged.registered)
294
+ notices.push(manualEntryLine(name, servers[name]));
295
+ }
296
+ }
221
297
  }
222
298
  }
223
299
  const cfg = await writeCodeAiConfig(opts.destinationDir, {
@@ -229,7 +305,7 @@ export async function setupMcp(opts, cli = defaultClaudeCli) {
229
305
  mempalaceInstallAttempted,
230
306
  mempalaceInstallSucceeded,
231
307
  pythonRuntime,
232
- claudeCliAvailable,
308
+ userConfigPath: io.configPath,
233
309
  registration,
234
310
  serversRegistered,
235
311
  serversAlreadyPresent,
@@ -246,54 +322,86 @@ export async function setupMcp(opts, cli = defaultClaudeCli) {
246
322
  */
247
323
  const INSTALLER_OWNED_SERVERS = ["code-ai-mcp"];
248
324
  /**
249
- * Remove the installer-owned MCP server(s) from Claude's USER scope via
250
- * `claude mcp remove --scope user`. Idempotent — a server that isn't registered
251
- * is reported as "nothing to remove". If the `claude` CLI is absent, prints the
252
- * manual commands instead of touching the config. `cli` is injectable for tests.
325
+ * Remove the installer-owned MCP server(s) from the Claude user config's
326
+ * `mcpServers`. Idempotent — a server that isn't present is reported as
327
+ * "nothing to remove". If the config can't be read/written, prints guidance
328
+ * instead of guessing. `mempalace` is never touched. `io` is injectable.
253
329
  *
254
330
  * NOTE: the registration is global (shared across all projects), so this removes
255
331
  * code-ai-mcp for every project. That is the chosen behaviour (symmetric with
256
332
  * install); a multi-project user re-runs the installer to restore it.
257
333
  */
258
- export async function teardownMcp(opts, cli = defaultClaudeCli) {
334
+ export async function teardownMcp(opts, io = defaultUserConfigIO) {
259
335
  const notices = [];
260
336
  const serversRemoved = [];
261
337
  const serversNotPresent = [];
262
338
  const serversFailed = [];
263
339
  if (opts.dryRun) {
264
340
  for (const name of INSTALLER_OWNED_SERVERS) {
265
- notices.push(`Would remove MCP server '${name}' from user scope: claude ${buildClaudeRemoveArgs(name).join(" ")}`);
341
+ notices.push(`Would remove MCP server '${name}' from Claude user config (${io.configPath}).`);
266
342
  }
267
- return { claudeCliAvailable: false, removal: "user-scope", serversRemoved, serversNotPresent, serversFailed, notices };
343
+ return {
344
+ userConfigPath: io.configPath,
345
+ removal: "user-scope",
346
+ serversRemoved,
347
+ serversNotPresent,
348
+ serversFailed,
349
+ notices,
350
+ };
268
351
  }
269
- const claudeCliAvailable = await detectClaudeCli(cli);
270
- if (!claudeCliAvailable) {
271
- notices.push("The `claude` CLI was not found on PATH not modifying your global config automatically. " +
272
- "Remove the MCP server(s) from user scope by running:");
273
- for (const name of INSTALLER_OWNED_SERVERS) {
274
- notices.push(` claude ${buildClaudeRemoveArgs(name).join(" ")}`);
275
- }
276
- return { claudeCliAvailable, removal: "manual-fallback", serversRemoved, serversNotPresent, serversFailed, notices };
352
+ const read = await io.read();
353
+ if (!read.ok || !read.data) {
354
+ notices.push(`Could not read Claude user config at ${io.configPath} (${read.error ?? "unknown error"}). ` +
355
+ `Remove the server(s) [${INSTALLER_OWNED_SERVERS.join(", ")}] from its "mcpServers" object by hand.`);
356
+ return {
357
+ userConfigPath: io.configPath,
358
+ removal: "manual-fallback",
359
+ serversRemoved,
360
+ serversNotPresent,
361
+ serversFailed,
362
+ notices,
363
+ };
277
364
  }
278
- for (const name of INSTALLER_OWNED_SERVERS) {
279
- if (!(await isRegisteredInUserScope(cli, name))) {
280
- serversNotPresent.push(name);
281
- notices.push(`MCP server '${name}' is not registered in user scope — nothing to remove.`);
282
- continue;
283
- }
284
- const res = await cli.run(buildClaudeRemoveArgs(name));
285
- if (res.ok) {
286
- serversRemoved.push(name);
287
- notices.push(`Removed MCP server '${name}' from user scope.`);
288
- }
289
- else {
290
- serversFailed.push(name);
291
- notices.push(`Failed to remove '${name}' from user scope. Run it manually:\n` +
292
- ` claude ${buildClaudeRemoveArgs(name).join(" ")}\n` +
293
- ` Output:\n${res.output}`);
294
- }
365
+ const result = removeUserScopeServers(read.data, INSTALLER_OWNED_SERVERS);
366
+ serversNotPresent.push(...result.notPresent);
367
+ for (const name of result.notPresent) {
368
+ notices.push(`MCP server '${name}' is not in user config — nothing to remove.`);
369
+ }
370
+ if (result.removed.length === 0) {
371
+ return {
372
+ userConfigPath: io.configPath,
373
+ removal: "user-scope",
374
+ serversRemoved,
375
+ serversNotPresent,
376
+ serversFailed,
377
+ notices,
378
+ };
379
+ }
380
+ const written = await io.writeWithBackup(result.config);
381
+ if (written.ok) {
382
+ serversRemoved.push(...result.removed);
383
+ notices.push(`Removed MCP server(s) [${result.removed.join(", ")}] from Claude user config ${io.configPath}` +
384
+ (written.backupPath ? ` (backup: ${written.backupPath})` : "") +
385
+ ".");
386
+ return {
387
+ userConfigPath: io.configPath,
388
+ removal: "user-scope",
389
+ serversRemoved,
390
+ serversNotPresent,
391
+ serversFailed,
392
+ notices,
393
+ };
295
394
  }
296
- return { claudeCliAvailable, removal: "user-scope", serversRemoved, serversNotPresent, serversFailed, notices };
395
+ serversFailed.push(...result.removed);
396
+ notices.push(`Failed to write Claude user config at ${io.configPath} (${written.error ?? "unknown error"}). No changes made.`);
397
+ return {
398
+ userConfigPath: io.configPath,
399
+ removal: "manual-fallback",
400
+ serversRemoved,
401
+ serversNotPresent,
402
+ serversFailed,
403
+ notices,
404
+ };
297
405
  }
298
406
  // ─── Subprocess helpers ─────────────────────────────────────────────────────
299
407
  async function spawnExitZero(command, args) {
@@ -687,6 +687,10 @@ export declare const SignOffInput: z.ZodObject<{
687
687
  }>;
688
688
  evidence: z.ZodOptional<z.ZodUnknown>;
689
689
  }, z.core.$strip>;
690
+ /** Surfacing-only Auditor reminder, emitted when completed runs hit the A1 cadence (every 3rd). */
691
+ export declare const AuditNudge: z.ZodObject<{
692
+ runs_total: z.ZodNumber;
693
+ }, z.core.$strip>;
690
694
  export declare const SignOffOutput: z.ZodObject<{
691
695
  signed: z.ZodLiteral<true>;
692
696
  signer: z.ZodEnum<{
@@ -695,6 +699,9 @@ export declare const SignOffOutput: z.ZodObject<{
695
699
  system: "system";
696
700
  }>;
697
701
  timestamp: z.ZodString;
702
+ audit_nudge: z.ZodOptional<z.ZodObject<{
703
+ runs_total: z.ZodNumber;
704
+ }, z.core.$strip>>;
698
705
  }, z.core.$strip>;
699
706
  export type SignOffInput = z.infer<typeof SignOffInput>;
700
707
  export type SignOffOutput = z.infer<typeof SignOffOutput>;
@@ -1578,6 +1585,20 @@ export declare const E2EPlaywrightOutput: z.ZodObject<{
1578
1585
  }, z.core.$strip>;
1579
1586
  export type E2EPlaywrightInput = z.infer<typeof E2EPlaywrightInput>;
1580
1587
  export type E2EPlaywrightOutput = z.infer<typeof E2EPlaywrightOutput>;
1588
+ export declare const RenderDiffInput: z.ZodObject<{
1589
+ diff: z.ZodString;
1590
+ title: z.ZodOptional<z.ZodString>;
1591
+ task_id: z.ZodOptional<z.ZodString>;
1592
+ }, z.core.$strip>;
1593
+ export declare const RenderDiffOutput: z.ZodObject<{
1594
+ path: z.ZodString;
1595
+ file_url: z.ZodString;
1596
+ files: z.ZodNumber;
1597
+ added: z.ZodNumber;
1598
+ removed: z.ZodNumber;
1599
+ }, z.core.$strip>;
1600
+ export type RenderDiffInput = z.infer<typeof RenderDiffInput>;
1601
+ export type RenderDiffOutput = z.infer<typeof RenderDiffOutput>;
1581
1602
  export declare const TOOL_REGISTRY: {
1582
1603
  readonly load_role: {
1583
1604
  readonly input: z.ZodObject<{
@@ -2245,6 +2266,9 @@ export declare const TOOL_REGISTRY: {
2245
2266
  system: "system";
2246
2267
  }>;
2247
2268
  timestamp: z.ZodString;
2269
+ audit_nudge: z.ZodOptional<z.ZodObject<{
2270
+ runs_total: z.ZodNumber;
2271
+ }, z.core.$strip>>;
2248
2272
  }, z.core.$strip>;
2249
2273
  };
2250
2274
  readonly submit_artifact: {
@@ -3017,5 +3041,19 @@ export declare const TOOL_REGISTRY: {
3017
3041
  }, z.core.$strip>>>;
3018
3042
  }, z.core.$strip>;
3019
3043
  };
3044
+ readonly render_diff: {
3045
+ readonly input: z.ZodObject<{
3046
+ diff: z.ZodString;
3047
+ title: z.ZodOptional<z.ZodString>;
3048
+ task_id: z.ZodOptional<z.ZodString>;
3049
+ }, z.core.$strip>;
3050
+ readonly output: z.ZodObject<{
3051
+ path: z.ZodString;
3052
+ file_url: z.ZodString;
3053
+ files: z.ZodNumber;
3054
+ added: z.ZodNumber;
3055
+ removed: z.ZodNumber;
3056
+ }, z.core.$strip>;
3057
+ };
3020
3058
  };
3021
3059
  export type ToolName = keyof typeof TOOL_REGISTRY;
@@ -159,10 +159,16 @@ export const SignOffInput = z.object({
159
159
  /** Evidence — for mcp signer this is the auto_check tool output; for user it's free-form. */
160
160
  evidence: z.unknown().optional(),
161
161
  });
162
+ /** Surfacing-only Auditor reminder, emitted when completed runs hit the A1 cadence (every 3rd). */
163
+ export const AuditNudge = z.object({
164
+ runs_total: z.number().int().nonnegative(),
165
+ });
162
166
  export const SignOffOutput = z.object({
163
167
  signed: z.literal(true),
164
168
  signer: Signer,
165
169
  timestamp: z.string(),
170
+ /** Present only when RG is signed AND the run count hits the cadence. Best-effort; never load-bearing. */
171
+ audit_nudge: AuditNudge.optional(),
166
172
  });
167
173
  // ─── submit_artifact ─────────────────────────────────────────────────────────
168
174
  export const SubmitArtifactInput = z.object({
@@ -598,6 +604,23 @@ export const E2EPlaywrightOutput = z.object({
598
604
  failed: z.number().int().nonnegative(),
599
605
  failures: z.array(ExceptionItem).default([]),
600
606
  });
607
+ export const RenderDiffInput = z.object({
608
+ /** A unified diff (e.g. the output of `git diff`). */
609
+ diff: z.string().min(1),
610
+ /** Heading shown at the top of the review page. */
611
+ title: z.string().optional(),
612
+ /** Optional task id — only used to label the output filename. */
613
+ task_id: TaskId.optional(),
614
+ });
615
+ export const RenderDiffOutput = z.object({
616
+ /** Absolute path of the written HTML file (in the OS temp dir). */
617
+ path: z.string(),
618
+ /** file:// URL the user can open in a browser. */
619
+ file_url: z.string(),
620
+ files: z.number().int().nonnegative(),
621
+ added: z.number().int().nonnegative(),
622
+ removed: z.number().int().nonnegative(),
623
+ });
601
624
  // ════════════════════════════════════════════════════════════════════════════
602
625
  // TOOL REGISTRY — name → { input, output } for runtime dispatch
603
626
  // ════════════════════════════════════════════════════════════════════════════
@@ -639,4 +662,5 @@ export const TOOL_REGISTRY = {
639
662
  },
640
663
  docker_compose: { input: DockerComposeInput, output: DockerComposeOutput },
641
664
  e2e_playwright: { input: E2EPlaywrightInput, output: E2EPlaywrightOutput },
665
+ render_diff: { input: RenderDiffInput, output: RenderDiffOutput },
642
666
  };
@@ -5,7 +5,7 @@ domain: analytics
5
5
  signs_off_at:
6
6
  - RELEASE_GATE
7
7
  tool_allowlist: role:conductor
8
- budget_lines: 459
8
+ budget_lines: 473
9
9
  schema_version: 1
10
10
  ---
11
11
 
@@ -360,6 +360,20 @@ $handoff → RES-02 (Researcher Beta).
360
360
 
361
361
  ---
362
362
 
363
+ ## MCP-интеграция и операционные гарантии
364
+
365
+ Дирижёр оркестрирует весь поток гейтов через машину состояний `code-ai` MCP — общий поток см. в `analytics-pipeline-rules.md` § «Машина гейтов (code-ai MCP)». Conductor-specific операционные гарантии:
366
+
367
+ - **Оркестрация гейтов** — Дирижёр ведёт последовательность гейтов выбранного режима через MCP: `classify_gate` (на какой гейт ложится задача), `current_gate` (где сейчас), `advance_gate` (переход к следующему гейту только после deliverable + Handoff Envelope). Переход без полного Handoff Envelope → `advance_gate` блокирует.
368
+ - **`sign_off` — все гейты `user`, Дирижёр не подписывает за пользователя** — в аналитике каждый гейт закрывает ПОЛЬЗОВАТЕЛЬ. Дирижёр ОСТАНАВЛИВАЕТСЯ, показывает артефакт гейта и запрашивает `sign_off(signer="user")` — по одному гейту, без батчинга нескольких подтверждений, и не начинает работу следующего гейта, пока текущий не подписан. Авто-пас на зелёном не применяется (домен суждения, детерминированных авто-чеков нет).
369
+ - **RELEASE_GATE** фиксируется как `sign_off(gate="RELEASE_GATE", signer="user", decision=..., evidence=<RG checklist>)` — не «прозой».
370
+ - **`request_decision` для эскалаций** — конфликты между агентами и waiver'ы обязательных пунктов: `request_decision(...)` → решает пользователь → `record_decision`. См. § Conflict Resolution Protocol.
371
+ - **`record_decision` для ADR-достойных решений** — зафиксированные конфликты и отклонения от брифа, согласованные с пользователем: `record_decision(signer="user", domain="analytics", task_id, decision_text)`.
372
+ - **Circuit breaker отключён** — в аналитике нет полосы откатов в стиле development (DEV/REV/OPS/TEST); возвраты идут через reverse `$handoff` (см. Reverse Handoff).
373
+ - **Degraded mode** — если MCP-поток недоступен: Дирижёр ведёт Master Checklist (`$board`) и статусы Handoff Envelope вручную, sign-off фиксируется явным "Approved" пользователя, эскалации — вручную.
374
+
375
+ ---
376
+
363
377
  ## Формат ответа агента
364
378
 
365
379
  ### Full Pipeline
@@ -5,7 +5,7 @@ domain: analytics
5
5
  signs_off_at:
6
6
  - RELEASE_GATE
7
7
  tool_allowlist: role:conductor
8
- budget_lines: 457
8
+ budget_lines: 471
9
9
  schema_version: 1
10
10
  ---
11
11
 
@@ -358,6 +358,20 @@ If an agent's submission encounters `$gates` failure:
358
358
 
359
359
  ---
360
360
 
361
+ ## MCP integration & operational guardrails
362
+
363
+ The Conductor orchestrates the whole gate-flow through the `code-ai` MCP state machine — for the general flow see `analytics-pipeline-rules.md` § "Gate machine (code-ai MCP)". Conductor-specific operational guardrails:
364
+
365
+ - **Gate-flow orchestration** — the Conductor drives the active mode's gate sequence through MCP: `classify_gate` (which gate the task lands on), `current_gate` (where we are now), `advance_gate` (move to the next gate only after the deliverable + Handoff Envelope). A transition without a complete Handoff Envelope → `advance_gate` blocks.
366
+ - **`sign_off` — all gates are `user`; the Conductor does not sign for the user** — in analytics every gate is closed by the USER. The Conductor STOPS, presents the gate artifact, and requests `sign_off(signer="user")` — one gate at a time, with no batching of approvals, and does NOT start next-gate work until the current one is signed. No auto-pass on green (a judgment domain, no deterministic auto-checks).
367
+ - **RELEASE_GATE** is recorded as `sign_off(gate="RELEASE_GATE", signer="user", decision=..., evidence=<RG checklist>)` — not prose approval.
368
+ - **`request_decision` for escalations** — conflicts between agents and waivers of mandatory items: `request_decision(...)` → the user decides → `record_decision`. See § Conflict Resolution Protocol.
369
+ - **`record_decision` for ADR-worthy outcomes** — recorded conflicts and brief deviations approved by the user: `record_decision(signer="user", domain="analytics", task_id, decision_text)`.
370
+ - **Circuit breaker disabled** — analytics has no development-style rollback lane (DEV/REV/OPS/TEST); returns go through reverse `$handoff` (see Reverse Handoff).
371
+ - **Degraded mode** — if the MCP flow is unavailable: the Conductor keeps the Master Checklist (`$board`) and Handoff Envelope statuses by hand, sign-off is recorded by the user's explicit "Approved", and escalations are manual.
372
+
373
+ ---
374
+
361
375
  ## Agent Response Template Format
362
376
 
363
377
  ### Full Pipeline Sequence
@@ -5,7 +5,7 @@ domain: content
5
5
  signs_off_at:
6
6
  - RELEASE_GATE
7
7
  tool_allowlist: role:conductor
8
- budget_lines: 393
8
+ budget_lines: 407
9
9
  schema_version: 1
10
10
  ---
11
11
 
@@ -267,6 +267,20 @@ Conductor отслеживает соответствие контента ис
267
267
 
268
268
  ---
269
269
 
270
+ ## MCP-интеграция и операционные гарантии
271
+
272
+ Дирижёр оркестрирует весь поток гейтов через машину состояний `code-ai` MCP — общий поток см. в `content-pipeline-rules.md` § «Машина гейтов (code-ai MCP)». Conductor-specific операционные гарантии:
273
+
274
+ - **Оркестрация гейтов** — Дирижёр ведёт последовательность гейтов выбранного режима через MCP: `classify_gate` (на какой гейт ложится задача), `current_gate` (где сейчас), `advance_gate` (переход к следующему гейту только после deliverable + Handoff Envelope). Переход без полного Handoff Envelope → `advance_gate` блокирует.
275
+ - **`sign_off` — все гейты `user`, Дирижёр не подписывает за пользователя** — в контенте каждый гейт закрывает ПОЛЬЗОВАТЕЛЬ. Дирижёр ОСТАНАВЛИВАЕТСЯ, показывает артефакт гейта и запрашивает `sign_off(signer="user")` — по одному гейту, без батчинга нескольких подтверждений, и не начинает работу следующего гейта, пока текущий не подписан. Авто-пас на зелёном не применяется (домен суждения, детерминированных авто-чеков нет).
276
+ - **RELEASE_GATE** фиксируется как `sign_off(gate="RELEASE_GATE", signer="user", decision=..., evidence=<RG checklist>)` — не «прозой».
277
+ - **`request_decision` для эскалаций** — конфликты между агентами и waiver'ы обязательных пунктов: `request_decision(...)` → решает пользователь → `record_decision`. См. § Conflict Resolution Protocol.
278
+ - **`record_decision` для ADR-достойных решений** — зафиксированные конфликты и отклонения от брифа, согласованные с пользователем: `record_decision(signer="user", domain="content", task_id, decision_text)`.
279
+ - **Circuit breaker отключён** — в контенте нет полосы откатов в стиле development (DEV/REV/OPS/TEST); возвраты идут через reverse `$handoff` (см. Reverse Handoff).
280
+ - **Degraded mode** — если MCP-поток недоступен: Дирижёр ведёт Master Checklist (`$board`) и статусы Handoff Envelope вручную, sign-off фиксируется явным "Approved" пользователя, эскалации — вручную.
281
+
282
+ ---
283
+
270
284
  ## Формат ответа Conductor (строго)
271
285
 
272
286
  ### Инициализация