@nanobpm/nano-workforce 0.177.0 → 0.178.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.
@@ -0,0 +1,19 @@
1
+ // Drift guard for the curated MCP tool block in the runbook (issue #715).
2
+ //
3
+ // The curated `"tools"` allowlist is authored once in `app/mcpToolSurface.ts` and rendered into
4
+ // `docs/mcp-runbook.md` by `scripts/sync-mcp-curated.ts`. This test — run under `npm test`, which CI
5
+ // already gates — fails if the runbook block drifts from the source of truth, so the enforcement does
6
+ // not depend on a separate workflow step (AGENTS.md: "Derivation over duplication: no drift
7
+ // surfaces"). Run `npm run sync:mcp-curated` to reconcile. Mirrors `scripts/sync-nav.test.ts`.
8
+ import { test } from "node:test";
9
+ import { assert } from "#test-assert";
10
+ import { reconciledRunbook } from "./sync-mcp-curated.ts";
11
+
12
+ test("docs/mcp-runbook.md curated-tools block is in sync with app/mcpToolSurface.ts", () => {
13
+ const { current, next } = reconciledRunbook();
14
+ assert(
15
+ current === next,
16
+ "docs/mcp-runbook.md curated-tools block is STALE vs app/mcpToolSurface.ts — " +
17
+ "run `npm run sync:mcp-curated` and commit the result.",
18
+ );
19
+ });
@@ -0,0 +1,77 @@
1
+ // Render the curated MCP tool allowlist (`CURATED_MCP_TOOLS`, app/mcpToolSurface.ts) into the runbook
2
+ // (docs/mcp-runbook.md) between its `curated-tools` sentinels (issue #715).
3
+ //
4
+ // The curated `"tools"` allowlist an MCP client imports instead of `["*"]` (the tractable-surface
5
+ // subset) is authored ONCE in `app/mcpToolSurface.ts`. The runbook shows it as a copyable JSON block;
6
+ // this script derives that block from the source so the two can never drift (AGENTS.md "no drift
7
+ // surfaces"), mirroring the repo's other derive/verify pairs (sync-nav, inline-mcp-bodies, layout-bpmn).
8
+ //
9
+ // node --experimental-strip-types scripts/sync-mcp-curated.ts # write the runbook block
10
+ // node --experimental-strip-types scripts/sync-mcp-curated.ts --check # verify (CI) — non-zero on drift
11
+ import { readFileSync, writeFileSync } from "node:fs";
12
+ import process from "node:process";
13
+ import { pathToFileURL } from "node:url";
14
+ import { CURATED_MCP_TOOLS } from "../app/mcpToolSurface.ts";
15
+
16
+ const ROOT = decodeURIComponent(new URL("../", import.meta.url).pathname);
17
+ export const RUNBOOK_PATH = `${ROOT}docs/mcp-runbook.md`;
18
+
19
+ const BEGIN = "<!-- BEGIN GENERATED: curated-tools (npm run sync:mcp-curated) -->";
20
+ const END = "<!-- END GENERATED: curated-tools -->";
21
+
22
+ /** The generated block: a fenced JSON array of curated tool names, one per line, in authored order. */
23
+ export function renderBlock(): string {
24
+ const lines = CURATED_MCP_TOOLS.map((name, i) => {
25
+ const comma = i === CURATED_MCP_TOOLS.length - 1 ? "" : ",";
26
+ return ` ${JSON.stringify(name)}${comma}`;
27
+ });
28
+ return ["```json", "[", ...lines, "]", "```"].join("\n");
29
+ }
30
+
31
+ export function replaceBetweenSentinels(source: string, block: string): string {
32
+ const begin = source.indexOf(BEGIN);
33
+ const end = source.indexOf(END);
34
+ if (begin === -1 || end === -1 || end < begin) {
35
+ throw new Error(
36
+ `docs/mcp-runbook.md is missing the curated-tools sentinels (${BEGIN} … ${END}). ` +
37
+ "Restore them so the generated block has a home.",
38
+ );
39
+ }
40
+ const before = source.slice(0, begin + BEGIN.length);
41
+ const after = source.slice(end);
42
+ return `${before}\n${block}\n${after}`;
43
+ }
44
+
45
+ /** The runbook content the generator WOULD write given its current on-disk state. */
46
+ export function reconciledRunbook(): { current: string; next: string } {
47
+ const current = readFileSync(RUNBOOK_PATH, "utf8");
48
+ return { current, next: replaceBetweenSentinels(current, renderBlock()) };
49
+ }
50
+
51
+ function main(): void {
52
+ const check = process.argv.includes("--check");
53
+ const { current, next } = reconciledRunbook();
54
+
55
+ if (check) {
56
+ if (current !== next) {
57
+ console.error(
58
+ "docs/mcp-runbook.md curated-tools block is STALE vs app/mcpToolSurface.ts. " +
59
+ "Run `npm run sync:mcp-curated` and commit the result.",
60
+ );
61
+ process.exit(1);
62
+ }
63
+ console.log("sync:mcp-curated: runbook curated-tools block is up to date.");
64
+ return;
65
+ }
66
+ if (current !== next) {
67
+ writeFileSync(RUNBOOK_PATH, next);
68
+ console.log("sync:mcp-curated: wrote curated-tools block into docs/mcp-runbook.md.");
69
+ } else {
70
+ console.log("sync:mcp-curated: runbook curated-tools block already up to date.");
71
+ }
72
+ }
73
+
74
+ // Run as a CLI only when invoked directly (not when imported by the drift test).
75
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
76
+ main();
77
+ }