code-ai-installer 4.1.0 โ 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/index.js +4 -6
- package/dist/mcp/audit_ledger.d.ts +10 -0
- package/dist/mcp/audit_ledger.js +15 -0
- package/dist/mcp/cli.d.ts +5 -1
- package/dist/mcp/cli.js +10 -2
- package/dist/mcp/tools/render_diff.d.ts +25 -0
- package/dist/mcp/tools/render_diff.js +138 -0
- package/dist/mcp/tools/sign_off.js +10 -5
- package/dist/mcp/tools/stubs.js +2 -0
- package/dist/mcp_setup.d.ts +9 -4
- package/dist/mcp_setup.js +40 -13
- package/dist/shared/tools.d.ts +38 -0
- package/dist/shared/tools.js +24 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +13 -0
- package/domains/analytics/agents/conductor.md +15 -1
- package/domains/analytics/locales/en/agents/conductor.md +15 -1
- package/domains/content/agents/conductor.md +15 -1
- package/domains/content/locales/en/agents/conductor.md +15 -1
- package/domains/development/.agents/skills/mcp-integration/SKILL.md +3 -1
- package/domains/development/.agents/workflows/audit.md +25 -0
- package/domains/development/.agents/workflows/pipeline-rules.md +1 -0
- package/domains/development/AGENTS.md +1 -0
- package/domains/development/agents/architect.md +1 -1
- package/domains/development/agents/auditor.md +4 -3
- package/domains/development/agents/conductor.md +4 -1
- package/domains/development/agents/devops.md +1 -1
- package/domains/development/agents/reviewer.md +2 -1
- package/domains/development/agents/senior_full_stack.md +1 -1
- package/domains/development/agents/tester.md +1 -1
- package/domains/development/locales/en/.agents/skills/mcp-integration/SKILL.md +3 -1
- package/domains/development/locales/en/.agents/workflows/audit.md +25 -0
- package/domains/development/locales/en/.agents/workflows/pipeline-rules.md +1 -0
- package/domains/development/locales/en/AGENTS.md +2 -0
- package/domains/development/locales/en/agents/architect.md +1 -1
- package/domains/development/locales/en/agents/auditor.md +4 -3
- package/domains/development/locales/en/agents/conductor.md +4 -1
- package/domains/development/locales/en/agents/devops.md +1 -1
- package/domains/development/locales/en/agents/reviewer.md +2 -1
- package/domains/development/locales/en/agents/senior_full_stack.md +1 -1
- package/domains/development/locales/en/agents/tester.md +1 -1
- package/domains/product/agents/conductor.md +15 -1
- package/domains/product/locales/en/agents/conductor.md +15 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,8 +157,11 @@ Depending on `--target`, `code-ai` restructures your project:
|
|
|
157
157
|
|
|
158
158
|
## ๐งฌ Versions & migration
|
|
159
159
|
|
|
160
|
-
`code-ai-installer` is on **v4.
|
|
160
|
+
`code-ai-installer` is on **v4.3.1**.
|
|
161
161
|
|
|
162
|
+
- **v4.3.1** โ the `code-ai-mcp` registration is now **pinned** to the installed version (`npx -p code-ai-installer@<version>`) and re-pinned on every reinstall, so an updated server actually takes effect instead of an unpinned `npx` silently reusing a stale global/cache copy. The server also logs `code-ai-mcp v<version> ยท domain=<domain>` to stderr at startup, so the live build is visible in Claude's MCP logs.
|
|
163
|
+
- **v4.3.0** โ `render_diff` MCP tool (unified diff โ a standalone HTML review page); MCP gate-flow + stop-at-user-gate sections added to the content / analytics / product conductors; Auditor trigger โ a `/audit` command plus a Release-Gate nudge that surfaces after every 3rd completed run (development pilot).
|
|
164
|
+
- **v4.1.0** โ MCP servers now register in your **global (user-scope)** config via a direct, idempotent `~/.claude.json` merge (no dependency on the `claude` CLI); the conductor halts at each user gate โ one at a time, no batching, no auto-pass on green.
|
|
162
165
|
- **v4.0.0** โ consolidated the previously separate `code-ai-mcp` and types packages into this single package with **two bins** (`code-ai` + `code-ai-mcp`). Installing the CLI now also delivers the MCP server; for Claude it is auto-registered in your global (user-scope) MCP config. Existing 3.x CLI behavior is unchanged.
|
|
163
166
|
- **v3.0.0 (breaking)** โ removed the legacy flat root layout (`AGENTS.md` + `agents/` + `.agents/` at package root); the CLI now reads exclusively from `domains/<id>/`. **Migrating from v2.x:** add `--domain <development|content|analytics|product>` to every CLI call. If you used a custom `--project-dir`, restructure it to `domains/<id>/` instead of a flat layout.
|
|
164
167
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import prompts from "prompts";
|
|
@@ -13,10 +12,9 @@ import { setupMcp, teardownMcp } from "./mcp_setup.js";
|
|
|
13
12
|
import { getPlatformAdapters } from "./platforms/adapters.js";
|
|
14
13
|
import { resolveSourceRoot } from "./sourceResolver.js";
|
|
15
14
|
import { printBanner } from "./banner.js";
|
|
15
|
+
import { readInstallerVersion } from "./version.js";
|
|
16
16
|
const program = new Command();
|
|
17
17
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
-
const requireJson = createRequire(import.meta.url);
|
|
19
|
-
const pkg = requireJson("../package.json");
|
|
20
18
|
const WIZARD_TEXT = {
|
|
21
19
|
en: {
|
|
22
20
|
cancelled: "Installation cancelled.",
|
|
@@ -76,7 +74,7 @@ const WIZARD_TEXT = {
|
|
|
76
74
|
program
|
|
77
75
|
.name("code-ai")
|
|
78
76
|
.description("Install code-ai agents and skills for AI coding assistants")
|
|
79
|
-
.version(
|
|
77
|
+
.version(readInstallerVersion());
|
|
80
78
|
program
|
|
81
79
|
.command("targets")
|
|
82
80
|
.description("List supported AI targets")
|
|
@@ -228,7 +226,7 @@ program
|
|
|
228
226
|
});
|
|
229
227
|
for (const notice of report.notices)
|
|
230
228
|
info(` ${notice}`);
|
|
231
|
-
info(` MCP (${report.registration}): registered [${report.serversRegistered.join(", ") || "โ"}], already present [${report.serversAlreadyPresent.join(", ") || "โ"}]`);
|
|
229
|
+
info(` MCP (${report.registration}): registered [${report.serversRegistered.join(", ") || "โ"}], re-pinned [${report.serversUpdated.join(", ") || "โ"}], already present [${report.serversAlreadyPresent.join(", ") || "โ"}]`);
|
|
232
230
|
info(` .code-ai/config.json: written at ${report.configPath} (decision_store=${report.mempalaceUsed ? "mempalace" : "jsonl"})`);
|
|
233
231
|
}
|
|
234
232
|
if (dryRun) {
|
|
@@ -494,7 +492,7 @@ async function runInteractiveWizard() {
|
|
|
494
492
|
});
|
|
495
493
|
for (const notice of report.notices)
|
|
496
494
|
info(` ${notice}`);
|
|
497
|
-
info(` MCP (${report.registration}): registered [${report.serversRegistered.join(", ") || "โ"}], already present [${report.serversAlreadyPresent.join(", ") || "โ"}]`);
|
|
495
|
+
info(` MCP (${report.registration}): registered [${report.serversRegistered.join(", ") || "โ"}], re-pinned [${report.serversUpdated.join(", ") || "โ"}], already present [${report.serversAlreadyPresent.join(", ") || "โ"}]`);
|
|
498
496
|
info(` .code-ai/config.json: written at ${report.configPath} (decision_store=${report.mempalaceUsed ? "mempalace" : "jsonl"})`);
|
|
499
497
|
}
|
|
500
498
|
success("Install completed.");
|
|
@@ -10,3 +10,13 @@ export declare function readLedger(): Promise<RunScorecard[]>;
|
|
|
10
10
|
* break a sign-off (telemetry is never load-bearing for the gate).
|
|
11
11
|
*/
|
|
12
12
|
export declare function recordRunScorecard(state: TaskState): Promise<void>;
|
|
13
|
+
/** Number of COMPLETED (RG-signed) runs in the ledger. */
|
|
14
|
+
export declare function countCompletedRuns(): Promise<number>;
|
|
15
|
+
/**
|
|
16
|
+
* A1 cadence (Variant A1): surface an Auditor nudge on every 3rd completed run
|
|
17
|
+
* (3, 6, 9, โฆ). Stateless โ no last-audit marker โ so it re-reminds until the
|
|
18
|
+
* user runs /audit. Surfacing only; returns undefined when no nudge is due.
|
|
19
|
+
*/
|
|
20
|
+
export declare function auditNudgeFor(completedRuns: number): {
|
|
21
|
+
runs_total: number;
|
|
22
|
+
} | undefined;
|
package/dist/mcp/audit_ledger.js
CHANGED
|
@@ -80,3 +80,18 @@ export async function recordRunScorecard(state) {
|
|
|
80
80
|
const extras = await readSideCounts(state.task_id);
|
|
81
81
|
await appendScorecard(buildScorecard(state, extras));
|
|
82
82
|
}
|
|
83
|
+
/** Number of COMPLETED (RG-signed) runs in the ledger. */
|
|
84
|
+
export async function countCompletedRuns() {
|
|
85
|
+
const cards = await readLedger();
|
|
86
|
+
return cards.filter((c) => c.completed).length;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A1 cadence (Variant A1): surface an Auditor nudge on every 3rd completed run
|
|
90
|
+
* (3, 6, 9, โฆ). Stateless โ no last-audit marker โ so it re-reminds until the
|
|
91
|
+
* user runs /audit. Surfacing only; returns undefined when no nudge is due.
|
|
92
|
+
*/
|
|
93
|
+
export function auditNudgeFor(completedRuns) {
|
|
94
|
+
return completedRuns >= 3 && completedRuns % 3 === 0
|
|
95
|
+
? { runs_total: completedRuns }
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
package/dist/mcp/cli.d.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
13
|
import { type ToolName } from "../shared/index.js";
|
|
14
|
+
declare const SERVER_INFO: {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
};
|
|
14
18
|
/**
|
|
15
19
|
* One-line description per tool. Not stored in the shared types module (src/shared) because the
|
|
16
20
|
* registry there is pure I/O contracts; descriptions live with the transport
|
|
@@ -18,4 +22,4 @@ import { type ToolName } from "../shared/index.js";
|
|
|
18
22
|
*/
|
|
19
23
|
declare const TOOL_DESCRIPTIONS: Record<ToolName, string>;
|
|
20
24
|
declare function buildServer(): McpServer;
|
|
21
|
-
export { buildServer, TOOL_DESCRIPTIONS };
|
|
25
|
+
export { buildServer, TOOL_DESCRIPTIONS, SERVER_INFO };
|
package/dist/mcp/cli.js
CHANGED
|
@@ -14,9 +14,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
14
14
|
import { TOOL_REGISTRY } from "../shared/index.js";
|
|
15
15
|
import { CodeAiMcpServer } from "./server.js";
|
|
16
16
|
import { NotImplementedError } from "./tools/stubs.js";
|
|
17
|
+
import { readInstallerVersion } from "../version.js";
|
|
18
|
+
import { resolveActiveDomain } from "./config.js";
|
|
17
19
|
const SERVER_INFO = {
|
|
18
20
|
name: "code-ai-mcp",
|
|
19
|
-
version:
|
|
21
|
+
version: readInstallerVersion(),
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
22
24
|
* One-line description per tool. Not stored in the shared types module (src/shared) because the
|
|
@@ -56,6 +58,7 @@ const TOOL_DESCRIPTIONS = {
|
|
|
56
58
|
dependency_supply_chain: "[stub] Check supply chain (npm audit, Socket integration, license check).",
|
|
57
59
|
docker_compose: "[stub] Run docker compose up/down for a target.",
|
|
58
60
|
e2e_playwright: "[stub] Run Playwright end-to-end suite.",
|
|
61
|
+
render_diff: "Render a unified diff (e.g. `git diff`) into a colored, per-file, line-numbered HTML review page written to the OS temp dir. Returns the path + a file:// URL the user opens in a browser. Use at the REV gate to present code changes for review. Informational only โ the file lives in temp (cleared on reboot), never in the project.",
|
|
59
62
|
};
|
|
60
63
|
function buildServer() {
|
|
61
64
|
const dispatch = new CodeAiMcpServer();
|
|
@@ -107,6 +110,11 @@ async function main() {
|
|
|
107
110
|
const mcp = buildServer();
|
|
108
111
|
const transport = new StdioServerTransport();
|
|
109
112
|
await mcp.connect(transport);
|
|
113
|
+
// Startup banner to STDERR โ never stdout, which carries the JSON-RPC stream.
|
|
114
|
+
// Makes the live build + active domain visible in Claude's MCP logs, so a
|
|
115
|
+
// stale-server mismatch is obvious instead of silent.
|
|
116
|
+
const domain = await resolveActiveDomain();
|
|
117
|
+
console.error(`code-ai-mcp v${SERVER_INFO.version} ยท domain=${domain}`);
|
|
110
118
|
}
|
|
111
119
|
// Allow importing buildServer in tests without starting the stdio loop.
|
|
112
120
|
const isDirectRun = (() => {
|
|
@@ -124,4 +132,4 @@ if (isDirectRun) {
|
|
|
124
132
|
process.exit(1);
|
|
125
133
|
});
|
|
126
134
|
}
|
|
127
|
-
export { buildServer, TOOL_DESCRIPTIONS };
|
|
135
|
+
export { buildServer, TOOL_DESCRIPTIONS, SERVER_INFO };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type RenderDiffInput, type RenderDiffOutput } from "../../shared/index.js";
|
|
2
|
+
type RowKind = "hunk" | "add" | "del" | "ctx";
|
|
3
|
+
interface DiffRow {
|
|
4
|
+
kind: RowKind;
|
|
5
|
+
text: string;
|
|
6
|
+
o: number | "";
|
|
7
|
+
n: number | "";
|
|
8
|
+
}
|
|
9
|
+
export interface DiffFile {
|
|
10
|
+
name: string;
|
|
11
|
+
added: number;
|
|
12
|
+
removed: number;
|
|
13
|
+
rows: DiffRow[];
|
|
14
|
+
}
|
|
15
|
+
/** Parse a unified diff into per-file rows with old/new line numbers. Pure. */
|
|
16
|
+
export declare function parseUnifiedDiff(raw: string): DiffFile[];
|
|
17
|
+
/** Render parsed diff files into a self-contained HTML page. Pure. */
|
|
18
|
+
export declare function renderDiffHtml(diff: string, title: string): {
|
|
19
|
+
html: string;
|
|
20
|
+
files: number;
|
|
21
|
+
added: number;
|
|
22
|
+
removed: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function renderDiff(input: RenderDiffInput): Promise<RenderDiffOutput>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
/**
|
|
7
|
+
* render_diff โ turn a unified diff into a colored, per-file, line-numbered HTML
|
|
8
|
+
* review page and write it to the OS temp dir.
|
|
9
|
+
*
|
|
10
|
+
* Why a tool (not a loose script): code reviews in the pipeline are presented as
|
|
11
|
+
* an HTML diff the user opens via a file:// link. Shipping it as an MCP tool keeps
|
|
12
|
+
* it version-controlled, testable, and reachable wherever the server runs.
|
|
13
|
+
*
|
|
14
|
+
* The output is INFORMATIONAL only โ it lives in the system temp dir (cleared on
|
|
15
|
+
* reboot), never in the project, so it pollutes nothing and needs no cleanup.
|
|
16
|
+
* Self-contained: no deps, no network.
|
|
17
|
+
*/
|
|
18
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
19
|
+
/** Parse a unified diff into per-file rows with old/new line numbers. Pure. */
|
|
20
|
+
export function parseUnifiedDiff(raw) {
|
|
21
|
+
const blocks = raw.split(/^diff --git /m).filter(Boolean);
|
|
22
|
+
const files = [];
|
|
23
|
+
for (const b of blocks) {
|
|
24
|
+
const lines = ("diff --git " + b).split("\n");
|
|
25
|
+
const header = lines[0];
|
|
26
|
+
const m = header.match(/a\/(.+?) b\/(.+)$/);
|
|
27
|
+
const name = m ? m[2] : header;
|
|
28
|
+
let added = 0;
|
|
29
|
+
let removed = 0;
|
|
30
|
+
const rows = [];
|
|
31
|
+
let oldLn = 0;
|
|
32
|
+
let newLn = 0;
|
|
33
|
+
for (const ln of lines) {
|
|
34
|
+
if (ln.startsWith("diff --git") ||
|
|
35
|
+
ln.startsWith("index ") ||
|
|
36
|
+
ln.startsWith("--- ") ||
|
|
37
|
+
ln.startsWith("+++ "))
|
|
38
|
+
continue;
|
|
39
|
+
const hunk = ln.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
40
|
+
if (hunk) {
|
|
41
|
+
oldLn = Number(hunk[1]);
|
|
42
|
+
newLn = Number(hunk[2]);
|
|
43
|
+
rows.push({ kind: "hunk", text: ln, o: "", n: "" });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ln.startsWith("+")) {
|
|
47
|
+
added++;
|
|
48
|
+
rows.push({ kind: "add", text: ln.slice(1), o: "", n: newLn++ });
|
|
49
|
+
}
|
|
50
|
+
else if (ln.startsWith("-")) {
|
|
51
|
+
removed++;
|
|
52
|
+
rows.push({ kind: "del", text: ln.slice(1), o: oldLn++, n: "" });
|
|
53
|
+
}
|
|
54
|
+
else if (ln.startsWith("\\")) {
|
|
55
|
+
// "" โ skip
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
rows.push({ kind: "ctx", text: ln.slice(1), o: oldLn++, n: newLn++ });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
files.push({ name, added, removed, rows });
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
/** Render parsed diff files into a self-contained HTML page. Pure. */
|
|
66
|
+
export function renderDiffHtml(diff, title) {
|
|
67
|
+
const files = parseUnifiedDiff(diff);
|
|
68
|
+
const fileSections = files
|
|
69
|
+
.map((f, i) => {
|
|
70
|
+
const rowsHtml = f.rows
|
|
71
|
+
.map((r) => {
|
|
72
|
+
if (r.kind === "hunk")
|
|
73
|
+
return `<tr class="hunk"><td class="ln"></td><td class="ln"></td><td class="code">${esc(r.text)}</td></tr>`;
|
|
74
|
+
const sign = r.kind === "add" ? "+" : r.kind === "del" ? "-" : " ";
|
|
75
|
+
return `<tr class="${r.kind}"><td class="ln">${r.o}</td><td class="ln">${r.n}</td><td class="code"><span class="sign">${sign}</span>${esc(r.text)}</td></tr>`;
|
|
76
|
+
})
|
|
77
|
+
.join("\n");
|
|
78
|
+
return `<section id="f${i}">
|
|
79
|
+
<h2>${esc(f.name)} <span class="stat"><span class="plus">+${f.added}</span> <span class="minus">-${f.removed}</span></span></h2>
|
|
80
|
+
<table>${rowsHtml}</table>
|
|
81
|
+
</section>`;
|
|
82
|
+
})
|
|
83
|
+
.join("\n");
|
|
84
|
+
const added = files.reduce((s, f) => s + f.added, 0);
|
|
85
|
+
const removed = files.reduce((s, f) => s + f.removed, 0);
|
|
86
|
+
const nav = files
|
|
87
|
+
.map((f, i) => `<a href="#f${i}">${esc(f.name.split("/").pop() ?? f.name)}</a>`)
|
|
88
|
+
.join("");
|
|
89
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
90
|
+
<title>${esc(title)}</title>
|
|
91
|
+
<style>
|
|
92
|
+
:root { color-scheme: light dark; }
|
|
93
|
+
body { font: 13px/1.5 ui-monospace, "Cascadia Code", Consolas, monospace; margin: 0; background:#0d1117; color:#c9d1d9; }
|
|
94
|
+
header { padding: 16px 24px; border-bottom: 1px solid #30363d; position: sticky; top:0; background:#0d1117; }
|
|
95
|
+
header h1 { font: 600 16px system-ui, sans-serif; margin: 0 0 4px; }
|
|
96
|
+
header .summary { font: 13px system-ui, sans-serif; color:#8b949e; }
|
|
97
|
+
.plus { color:#3fb950; } .minus { color:#f85149; }
|
|
98
|
+
section { margin: 20px 24px; border: 1px solid #30363d; border-radius: 8px; overflow:hidden; }
|
|
99
|
+
section h2 { font: 600 13px ui-monospace, monospace; margin:0; padding:10px 14px; background:#161b22; border-bottom:1px solid #30363d; }
|
|
100
|
+
.stat { float:right; font-weight:400; }
|
|
101
|
+
table { border-collapse: collapse; width:100%; }
|
|
102
|
+
td { padding: 0 6px; white-space: pre-wrap; word-break: break-word; vertical-align: top; }
|
|
103
|
+
td.ln { width:1%; text-align:right; color:#6e7681; user-select:none; border-right:1px solid #21262d; padding:0 8px; }
|
|
104
|
+
td.code { width:100%; }
|
|
105
|
+
.sign { display:inline-block; width:1ch; color:#6e7681; }
|
|
106
|
+
tr.add { background: rgba(63,185,80,.15); } tr.add .sign { color:#3fb950; }
|
|
107
|
+
tr.del { background: rgba(248,81,73,.15); } tr.del .sign { color:#f85149; }
|
|
108
|
+
tr.hunk td { background:#161b22; color:#8b949e; padding:4px 14px; }
|
|
109
|
+
nav { padding: 0 24px; font: 13px system-ui, sans-serif; }
|
|
110
|
+
nav a { color:#58a6ff; text-decoration:none; display:inline-block; margin:2px 12px 2px 0; }
|
|
111
|
+
</style></head><body>
|
|
112
|
+
<header>
|
|
113
|
+
<h1>${esc(title)}</h1>
|
|
114
|
+
<div class="summary">${files.length} files ยท <span class="plus">+${added}</span> <span class="minus">-${removed}</span></div>
|
|
115
|
+
</header>
|
|
116
|
+
<nav>${nav}</nav>
|
|
117
|
+
${fileSections}
|
|
118
|
+
</body></html>`;
|
|
119
|
+
return { html, files: files.length, added, removed };
|
|
120
|
+
}
|
|
121
|
+
/** Slugify a label for use in a temp filename. */
|
|
122
|
+
function slug(s) {
|
|
123
|
+
return (s
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
126
|
+
.replace(/^-+|-+$/g, "")
|
|
127
|
+
.slice(0, 40) || "review");
|
|
128
|
+
}
|
|
129
|
+
export async function renderDiff(input) {
|
|
130
|
+
const title = input.title ?? "Diff review";
|
|
131
|
+
const { html, files, added, removed } = renderDiffHtml(input.diff, title);
|
|
132
|
+
// Content-hashed name โ same diff reuses one file; lands in the OS temp dir
|
|
133
|
+
// (cleared on reboot), never in the project.
|
|
134
|
+
const hash = createHash("sha1").update(input.diff).digest("hex").slice(0, 8);
|
|
135
|
+
const path = join(tmpdir(), `code-ai-diff-${slug(input.task_id ?? title)}-${hash}.html`);
|
|
136
|
+
await writeFile(path, html, "utf8");
|
|
137
|
+
return { path, file_url: pathToFileURL(path).href, files, added, removed };
|
|
138
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getGateConfig, loadPipeline } from "../pipeline.js";
|
|
2
2
|
import { readTaskState, writeTaskState } from "../task_state.js";
|
|
3
|
-
import { recordRunScorecard } from "../audit_ledger.js";
|
|
3
|
+
import { recordRunScorecard, countCompletedRuns, auditNudgeFor } from "../audit_ledger.js";
|
|
4
4
|
import { resolveActiveDomain } from "../config.js";
|
|
5
5
|
/**
|
|
6
6
|
* Records a sign-off event for the task's current gate. Validates that:
|
|
@@ -27,7 +27,9 @@ export async function signOff(input) {
|
|
|
27
27
|
switch (gateCfg.sign_off_policy) {
|
|
28
28
|
case "user":
|
|
29
29
|
if (input.signer !== "user") {
|
|
30
|
-
throw new Error(`sign_off: gate ${input.gate} requires signer 'user' (policy=user), got '${input.signer}'`
|
|
30
|
+
throw new Error(`sign_off: gate ${input.gate} requires signer 'user' (policy=user), got '${input.signer}'. ` +
|
|
31
|
+
`This is a USER gate โ HALT: present the ${input.gate} artifact and request the user's sign_off(signer='user') for THIS gate. ` +
|
|
32
|
+
`Do not auto-pass it, do not batch it with other gates, and do not begin any downstream-gate work until it is signed.`);
|
|
31
33
|
}
|
|
32
34
|
break;
|
|
33
35
|
case "mcp_auto_pass":
|
|
@@ -56,15 +58,18 @@ export async function signOff(input) {
|
|
|
56
58
|
});
|
|
57
59
|
await writeTaskState(state);
|
|
58
60
|
// Auditor telemetry: when the terminal gate is signed, the run is complete โ
|
|
59
|
-
// append a scorecard to the local ledger
|
|
60
|
-
//
|
|
61
|
+
// append a scorecard to the local ledger, then compute the /audit nudge.
|
|
62
|
+
// Best-effort: a ledger failure (or nudge failure) must NEVER break a
|
|
63
|
+
// sign-off (telemetry is not load-bearing for the gate).
|
|
64
|
+
let audit_nudge;
|
|
61
65
|
if (input.gate === "RG") {
|
|
62
66
|
try {
|
|
63
67
|
await recordRunScorecard(state);
|
|
68
|
+
audit_nudge = auditNudgeFor(await countCompletedRuns());
|
|
64
69
|
}
|
|
65
70
|
catch {
|
|
66
71
|
/* swallow โ see contract in audit_ledger.recordRunScorecard */
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
|
-
return { signed: true, signer: input.signer, timestamp };
|
|
74
|
+
return { signed: true, signer: input.signer, timestamp, ...(audit_nudge ? { audit_nudge } : {}) };
|
|
70
75
|
}
|
package/dist/mcp/tools/stubs.js
CHANGED
|
@@ -31,6 +31,7 @@ import { reviewProposal } from "./review_proposal.js";
|
|
|
31
31
|
import { e2ePlaywright } from "./e2e_playwright.js";
|
|
32
32
|
import { dockerCompose } from "./docker_compose.js";
|
|
33
33
|
import { dependencySupplyChain } from "./dependency_supply_chain.js";
|
|
34
|
+
import { renderDiff } from "./render_diff.js";
|
|
34
35
|
/** Thrown by every stub until the real implementation lands. */
|
|
35
36
|
export class NotImplementedError extends Error {
|
|
36
37
|
constructor(tool) {
|
|
@@ -83,4 +84,5 @@ export const DEFAULT_HANDLERS = {
|
|
|
83
84
|
dependency_supply_chain: dependencySupplyChain,
|
|
84
85
|
docker_compose: dockerCompose,
|
|
85
86
|
e2e_playwright: e2ePlaywright,
|
|
87
|
+
render_diff: renderDiff,
|
|
86
88
|
};
|
package/dist/mcp_setup.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ export interface McpSetupReport {
|
|
|
69
69
|
registration: "user-scope" | "manual-fallback";
|
|
70
70
|
/** Server names freshly added to the user config this run. */
|
|
71
71
|
serversRegistered: string[];
|
|
72
|
+
/** Installer-owned server names whose entry was refreshed (e.g. re-pinned to a new version). */
|
|
73
|
+
serversUpdated: string[];
|
|
72
74
|
/** Server names already present in the user config (left untouched). */
|
|
73
75
|
serversAlreadyPresent: string[];
|
|
74
76
|
/** Server names whose registration failed (config unwritable). */
|
|
@@ -128,13 +130,16 @@ export declare function userConfigPath(): string;
|
|
|
128
130
|
export declare function createUserConfigIO(configPath: string): UserConfigIO;
|
|
129
131
|
/**
|
|
130
132
|
* Idempotently merge `servers` into the config's top-level `mcpServers`. Pure โ
|
|
131
|
-
* returns a new config object plus which names were
|
|
132
|
-
* present.
|
|
133
|
-
* `mempalace` is preserved
|
|
133
|
+
* returns a new config object plus which names were added / updated / already
|
|
134
|
+
* present. A FOREIGN existing key is never overwritten (so a pre-existing global
|
|
135
|
+
* `mempalace` is preserved). An OWNED server (named in `ownedServers`) whose
|
|
136
|
+
* entry differs is refreshed in place โ that is how a reinstall re-pins
|
|
137
|
+
* `code-ai-mcp` to the new version. Exported for unit testing.
|
|
134
138
|
*/
|
|
135
|
-
export declare function mergeUserScopeServers(config: Record<string, unknown>, servers: Record<string, McpServerEntry
|
|
139
|
+
export declare function mergeUserScopeServers(config: Record<string, unknown>, servers: Record<string, McpServerEntry>, ownedServers?: readonly string[]): {
|
|
136
140
|
config: Record<string, unknown>;
|
|
137
141
|
registered: string[];
|
|
142
|
+
updated: string[];
|
|
138
143
|
alreadyPresent: string[];
|
|
139
144
|
};
|
|
140
145
|
/**
|
package/dist/mcp_setup.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { readInstallerVersion } from "./version.js";
|
|
5
6
|
/**
|
|
6
7
|
* Try `mempalace-mcp --help` โ the dedicated MCP-server bin, which is exactly
|
|
7
8
|
* what we register. Resolves true on exit code 0, false otherwise (including
|
|
@@ -120,25 +121,38 @@ function readMcpServers(config) {
|
|
|
120
121
|
}
|
|
121
122
|
/**
|
|
122
123
|
* Idempotently merge `servers` into the config's top-level `mcpServers`. Pure โ
|
|
123
|
-
* returns a new config object plus which names were
|
|
124
|
-
* present.
|
|
125
|
-
* `mempalace` is preserved
|
|
124
|
+
* returns a new config object plus which names were added / updated / already
|
|
125
|
+
* present. A FOREIGN existing key is never overwritten (so a pre-existing global
|
|
126
|
+
* `mempalace` is preserved). An OWNED server (named in `ownedServers`) whose
|
|
127
|
+
* entry differs is refreshed in place โ that is how a reinstall re-pins
|
|
128
|
+
* `code-ai-mcp` to the new version. Exported for unit testing.
|
|
126
129
|
*/
|
|
127
|
-
export function mergeUserScopeServers(config, servers) {
|
|
130
|
+
export function mergeUserScopeServers(config, servers, ownedServers = []) {
|
|
128
131
|
const next = { ...config };
|
|
129
132
|
const mcpServers = readMcpServers(config);
|
|
133
|
+
const owned = new Set(ownedServers);
|
|
130
134
|
const registered = [];
|
|
135
|
+
const updated = [];
|
|
131
136
|
const alreadyPresent = [];
|
|
132
137
|
for (const [name, entry] of Object.entries(servers)) {
|
|
138
|
+
const desired = toUserScopeEntry(entry);
|
|
133
139
|
if (Object.prototype.hasOwnProperty.call(mcpServers, name)) {
|
|
134
|
-
|
|
140
|
+
// Refresh ONLY our own servers (e.g. re-pin to a new version). A foreign
|
|
141
|
+
// server we also register (mempalace) is never overwritten.
|
|
142
|
+
if (owned.has(name) && JSON.stringify(mcpServers[name]) !== JSON.stringify(desired)) {
|
|
143
|
+
mcpServers[name] = desired;
|
|
144
|
+
updated.push(name);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
alreadyPresent.push(name);
|
|
148
|
+
}
|
|
135
149
|
continue;
|
|
136
150
|
}
|
|
137
|
-
mcpServers[name] =
|
|
151
|
+
mcpServers[name] = desired;
|
|
138
152
|
registered.push(name);
|
|
139
153
|
}
|
|
140
154
|
next.mcpServers = mcpServers;
|
|
141
|
-
return { config: next, registered, alreadyPresent };
|
|
155
|
+
return { config: next, registered, updated, alreadyPresent };
|
|
142
156
|
}
|
|
143
157
|
/**
|
|
144
158
|
* Idempotently remove `names` from the config's `mcpServers`. Pure โ returns a
|
|
@@ -184,10 +198,14 @@ function buildServerEntries(mempalaceUsed) {
|
|
|
184
198
|
// DEV-100 consolidation: the code-ai-mcp bin lives inside the
|
|
185
199
|
// `code-ai-installer` npm package. `npx -p <package> <bin>` tells npm to
|
|
186
200
|
// install that package and run the named bin from it.
|
|
201
|
+
// Pin to THIS installer's version so the spawned server is explicit and
|
|
202
|
+
// reproducible. An unpinned `npx` silently runs whatever global/cache copy
|
|
203
|
+
// exists โ that is how a stale, pre-fix server kept running after a reinstall.
|
|
204
|
+
// A reinstall re-pins this (mergeUserScopeServers refreshes owned servers).
|
|
187
205
|
const servers = {
|
|
188
206
|
"code-ai-mcp": {
|
|
189
207
|
command: "npx",
|
|
190
|
-
args: ["-y", "-p",
|
|
208
|
+
args: ["-y", "-p", `code-ai-installer@${readInstallerVersion()}`, "code-ai-mcp"],
|
|
191
209
|
},
|
|
192
210
|
};
|
|
193
211
|
if (mempalaceUsed) {
|
|
@@ -249,6 +267,7 @@ export async function setupMcp(opts, io = defaultUserConfigIO) {
|
|
|
249
267
|
}
|
|
250
268
|
const servers = buildServerEntries(mempalaceUsed);
|
|
251
269
|
const serversRegistered = [];
|
|
270
|
+
const serversUpdated = [];
|
|
252
271
|
const serversAlreadyPresent = [];
|
|
253
272
|
const serversFailed = [];
|
|
254
273
|
let registration;
|
|
@@ -268,12 +287,13 @@ export async function setupMcp(opts, io = defaultUserConfigIO) {
|
|
|
268
287
|
notices.push(manualEntryLine(name, entry));
|
|
269
288
|
}
|
|
270
289
|
else {
|
|
271
|
-
const merged = mergeUserScopeServers(read.data, servers);
|
|
290
|
+
const merged = mergeUserScopeServers(read.data, servers, INSTALLER_OWNED_SERVERS);
|
|
272
291
|
serversAlreadyPresent.push(...merged.alreadyPresent);
|
|
273
292
|
for (const name of merged.alreadyPresent) {
|
|
274
293
|
notices.push(`MCP server '${name}' already in user config โ left untouched.`);
|
|
275
294
|
}
|
|
276
|
-
|
|
295
|
+
const changed = [...merged.registered, ...merged.updated];
|
|
296
|
+
if (changed.length === 0) {
|
|
277
297
|
registration = "user-scope";
|
|
278
298
|
}
|
|
279
299
|
else {
|
|
@@ -281,16 +301,22 @@ export async function setupMcp(opts, io = defaultUserConfigIO) {
|
|
|
281
301
|
if (written.ok) {
|
|
282
302
|
registration = "user-scope";
|
|
283
303
|
serversRegistered.push(...merged.registered);
|
|
284
|
-
|
|
304
|
+
serversUpdated.push(...merged.updated);
|
|
305
|
+
const parts = [];
|
|
306
|
+
if (merged.registered.length)
|
|
307
|
+
parts.push(`registered [${merged.registered.join(", ")}]`);
|
|
308
|
+
if (merged.updated.length)
|
|
309
|
+
parts.push(`re-pinned [${merged.updated.join(", ")}]`);
|
|
310
|
+
notices.push(`MCP server(s) ${parts.join(", ")} in Claude user config ${io.configPath}` +
|
|
285
311
|
(written.backupPath ? ` (backup: ${written.backupPath})` : "") +
|
|
286
312
|
". Restart your Claude Code session to load them.");
|
|
287
313
|
}
|
|
288
314
|
else {
|
|
289
315
|
registration = "manual-fallback";
|
|
290
|
-
serversFailed.push(...
|
|
316
|
+
serversFailed.push(...changed);
|
|
291
317
|
notices.push(`Failed to write Claude user config at ${io.configPath} (${written.error ?? "unknown error"}). ` +
|
|
292
318
|
`No changes made โ add these entries to its "mcpServers" object by hand:`);
|
|
293
|
-
for (const name of
|
|
319
|
+
for (const name of changed)
|
|
294
320
|
notices.push(manualEntryLine(name, servers[name]));
|
|
295
321
|
}
|
|
296
322
|
}
|
|
@@ -308,6 +334,7 @@ export async function setupMcp(opts, io = defaultUserConfigIO) {
|
|
|
308
334
|
userConfigPath: io.configPath,
|
|
309
335
|
registration,
|
|
310
336
|
serversRegistered,
|
|
337
|
+
serversUpdated,
|
|
311
338
|
serversAlreadyPresent,
|
|
312
339
|
serversFailed,
|
|
313
340
|
configPath: cfg.path,
|
package/dist/shared/tools.d.ts
CHANGED
|
@@ -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;
|
package/dist/shared/tools.js
CHANGED
|
@@ -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
|
};
|