@rockhopper-co/mcp-server 0.7.0 → 0.10.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/CHANGELOG.md +68 -0
- package/README.md +31 -0
- package/dist/api-client.d.ts +83 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +171 -10
- package/dist/api-client.js.map +1 -1
- package/dist/auth/device-grant-client.d.ts.map +1 -1
- package/dist/auth/device-grant-client.js +3 -1
- package/dist/auth/device-grant-client.js.map +1 -1
- package/dist/cli.js +39 -1
- package/dist/cli.js.map +1 -1
- package/dist/correlation.d.ts +9 -0
- package/dist/correlation.d.ts.map +1 -0
- package/dist/correlation.js +26 -0
- package/dist/correlation.js.map +1 -0
- package/dist/logger.d.ts +42 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +159 -0
- package/dist/logger.js.map +1 -0
- package/dist/not-ready.d.ts +95 -0
- package/dist/not-ready.d.ts.map +1 -0
- package/dist/not-ready.js +151 -0
- package/dist/not-ready.js.map +1 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +8 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/resources/changes.d.ts.map +1 -1
- package/dist/resources/changes.js +6 -0
- package/dist/resources/changes.js.map +1 -1
- package/dist/resources/orchestration-guide.md +23 -3
- package/dist/resources/teams.d.ts.map +1 -1
- package/dist/resources/teams.js +7 -1
- package/dist/resources/teams.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +40 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/get-cell-history.d.ts.map +1 -1
- package/dist/tools/get-cell-history.js +28 -4
- package/dist/tools/get-cell-history.js.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +13 -1
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/write-reviews.d.ts.map +1 -1
- package/dist/tools/write-reviews.js +58 -4
- package/dist/tools/write-reviews.js.map +1 -1
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/zod-schemas.d.ts +30 -54
- package/dist/zod-schemas.d.ts.map +1 -1
- package/dist/zod-schemas.js +24 -0
- package/dist/zod-schemas.js.map +1 -1
- package/package.json +22 -10
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { ApiClient } from './api-client.js';
|
|
4
4
|
import { AuthResolutionError, resolveAuth, } from './auth/resolve-auth.js';
|
|
5
|
+
import { flushLoggerSync, initLogger, log, serviceVersion, } from './logger.js';
|
|
5
6
|
import { createServer } from './server.js';
|
|
6
7
|
const ROCKHOPPER_API_URL = process.env.ROCKHOPPER_API_URL || 'https://api.rockhopper.co';
|
|
8
|
+
// KI-225: start the local rotating diagnostic logfile before anything else,
|
|
9
|
+
// so startup-path failures (auth, preflight, crashes) are captured. Never
|
|
10
|
+
// throws and never writes to stdout — falls back to a no-op logger.
|
|
11
|
+
await initLogger();
|
|
12
|
+
// Capture client-side crashes that would otherwise vanish (the backend can't
|
|
13
|
+
// see them). Registering these handlers suppresses Node's default crash, so
|
|
14
|
+
// uncaughtException re-exits 1 to PRESERVE the existing exit behavior; the
|
|
15
|
+
// fatal line is flushed to disk synchronously first.
|
|
16
|
+
process.on('uncaughtException', (err) => {
|
|
17
|
+
log.fatal({ event: 'uncaught_exception', err }, 'uncaught_exception');
|
|
18
|
+
flushLoggerSync();
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
process.on('unhandledRejection', (reason) => {
|
|
22
|
+
log.error({ event: 'unhandled_rejection', err: reason }, 'unhandled_rejection');
|
|
23
|
+
});
|
|
7
24
|
// ENG-1444: auth resolution order is
|
|
8
25
|
// 1. ROCKHOPPER_TOKEN env var (Personal Access Token — headless / CI)
|
|
9
26
|
// 2. Stored OAuth bundle in the OS keychain (prior device-grant flow)
|
|
@@ -17,6 +34,8 @@ try {
|
|
|
17
34
|
}
|
|
18
35
|
catch (err) {
|
|
19
36
|
if (err instanceof AuthResolutionError) {
|
|
37
|
+
// KI-225: local auth failure before any API call (bad token / sign-in).
|
|
38
|
+
log.warn({ event: 'auth_failed', reason: err.code }, 'auth_failed');
|
|
20
39
|
if (err.code === 'pat_malformed') {
|
|
21
40
|
console.error(`Error: ${err.message}`);
|
|
22
41
|
console.error('Tokens start with "rh_pat_". Check that the full token was copied correctly.');
|
|
@@ -34,16 +53,33 @@ catch (err) {
|
|
|
34
53
|
}
|
|
35
54
|
process.exit(1);
|
|
36
55
|
}
|
|
56
|
+
// Every catch branch above ends in `process.exit(1)`, so `resolved` is always
|
|
57
|
+
// assigned past this point. TS 6.0's stricter control-flow analysis can't prove
|
|
58
|
+
// that across the implicit-typed `let`, so narrow it explicitly here.
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
37
62
|
const apiClient = new ApiClient({
|
|
38
63
|
baseUrl: ROCKHOPPER_API_URL,
|
|
39
64
|
token: resolved.accessToken,
|
|
40
65
|
});
|
|
41
66
|
try {
|
|
42
|
-
await apiClient.getMe();
|
|
67
|
+
const me = await apiClient.getMe();
|
|
68
|
+
// ENG-1756 (plan §9 decision 15): the PAT owner IS the human driving this
|
|
69
|
+
// local agent — reuse the preflight to declare them, so every write carries
|
|
70
|
+
// `X-Driving-Human` and the backend's anonymous-agent-write admission
|
|
71
|
+
// never fires for this client. Best-effort: while unset, the backend
|
|
72
|
+
// resolves the PAT owner server-side (same human).
|
|
73
|
+
apiClient.setDrivingHuman(me?.msId ?? me?.googleId ?? null);
|
|
43
74
|
}
|
|
44
75
|
catch (err) {
|
|
45
76
|
const msg = err instanceof Error ? err.message : String(err);
|
|
46
77
|
if (msg.includes('401') || msg.includes('403')) {
|
|
78
|
+
// KI-225: preflight token rejection — the local auth-fail signal.
|
|
79
|
+
log.warn({
|
|
80
|
+
event: 'auth_failed',
|
|
81
|
+
reason: resolved.source === 'pat' ? 'pat_invalid' : 'oauth_invalid',
|
|
82
|
+
}, 'auth_failed');
|
|
47
83
|
if (resolved.source === 'pat') {
|
|
48
84
|
console.error('Error: ROCKHOPPER_TOKEN is invalid or expired.\n' +
|
|
49
85
|
'Create a new Personal Access Token in Rockhopper Settings and set it as ROCKHOPPER_TOKEN.');
|
|
@@ -60,6 +96,8 @@ catch (err) {
|
|
|
60
96
|
process.exit(1);
|
|
61
97
|
}
|
|
62
98
|
const server = createServer(apiClient);
|
|
99
|
+
// KI-225: one line per launch — anchors a session in the file.
|
|
100
|
+
log.info({ event: 'mcp_server_start', version: serviceVersion }, 'mcp_server_start');
|
|
63
101
|
const transport = new StdioServerTransport();
|
|
64
102
|
await server.connect(transport);
|
|
65
103
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EACL,mBAAmB,EACnB,WAAW,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `fn` inside an AsyncLocalStorage scope carrying `id`. Reads via
|
|
3
|
+
* {@link getCorrelationId} anywhere in the (sync or async) continuation
|
|
4
|
+
* return the same id. Mints a fresh UUID v4 when `id` is omitted.
|
|
5
|
+
*/
|
|
6
|
+
export declare const runWithCorrelationId: <T>(fn: () => T, id?: string) => T;
|
|
7
|
+
/** Current correlation id, or `undefined` when called outside a scope. */
|
|
8
|
+
export declare const getCorrelationId: () => string | undefined;
|
|
9
|
+
//# sourceMappingURL=correlation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.d.ts","sourceRoot":"","sources":["../src/correlation.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EACpC,IAAI,MAAM,CAAC,EACX,KAAI,MAAqB,KACxB,CAAoB,CAAC;AAExB,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,QAAO,MAAM,GAAG,SAA2B,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
/**
|
|
4
|
+
* Phase 1.1 / KI-226 — correlation id propagation.
|
|
5
|
+
*
|
|
6
|
+
* Per-tool-call scope: a single correlation id covers every outbound
|
|
7
|
+
* Rockhopper API call a tool invocation makes (including multi-call
|
|
8
|
+
* fan-outs like search). The id rides the `X-Correlation-Id` header out of
|
|
9
|
+
* {@link ApiClient.request}, so an mcp-server-originated request is
|
|
10
|
+
* traceable as a group in backend logs.
|
|
11
|
+
*
|
|
12
|
+
* The id is a non-sensitive UUID v4 — never co-log the bearer token with it.
|
|
13
|
+
* `randomUUID` comes from `node:crypto` (NOT the global `crypto`) because the
|
|
14
|
+
* package supports Node 18, where global `crypto.randomUUID` is not
|
|
15
|
+
* guaranteed.
|
|
16
|
+
*/
|
|
17
|
+
const als = new AsyncLocalStorage();
|
|
18
|
+
/**
|
|
19
|
+
* Runs `fn` inside an AsyncLocalStorage scope carrying `id`. Reads via
|
|
20
|
+
* {@link getCorrelationId} anywhere in the (sync or async) continuation
|
|
21
|
+
* return the same id. Mints a fresh UUID v4 when `id` is omitted.
|
|
22
|
+
*/
|
|
23
|
+
export const runWithCorrelationId = (fn, id = randomUUID()) => als.run(id, fn);
|
|
24
|
+
/** Current correlation id, or `undefined` when called outside a scope. */
|
|
25
|
+
export const getCorrelationId = () => als.getStore();
|
|
26
|
+
//# sourceMappingURL=correlation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.js","sourceRoot":"","sources":["../src/correlation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;;;;;;;;GAaG;AACH,MAAM,GAAG,GAAG,IAAI,iBAAiB,EAAU,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,EAAW,EACX,KAAa,UAAU,EAAE,EACtB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;AAExB,0EAA0E;AAC1E,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAuB,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import pinoRoll from 'pino-roll';
|
|
3
|
+
/** The 5 log methods we expose. A real `pino.Logger` satisfies this. */
|
|
4
|
+
export interface DiagnosticLogger {
|
|
5
|
+
debug: pino.LogFn;
|
|
6
|
+
info: pino.LogFn;
|
|
7
|
+
warn: pino.LogFn;
|
|
8
|
+
error: pino.LogFn;
|
|
9
|
+
fatal: pino.LogFn;
|
|
10
|
+
}
|
|
11
|
+
export declare const serviceVersion: string;
|
|
12
|
+
/**
|
|
13
|
+
* Builds a file-only diagnostic logger. NEVER throws and NEVER writes to
|
|
14
|
+
* stdout — on opt-out or any failure it returns a no-op logger. Exposed
|
|
15
|
+
* (vs. only the {@link log} singleton) so tests can drive an isolated
|
|
16
|
+
* instance pointed at a tmpdir.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createDiagnosticLogger(opts?: {
|
|
19
|
+
dir?: string;
|
|
20
|
+
disable?: boolean;
|
|
21
|
+
level?: string;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
logger: DiagnosticLogger;
|
|
24
|
+
flush: () => Promise<void>;
|
|
25
|
+
destination?: Awaited<ReturnType<typeof pinoRoll>>;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Process-wide singleton. Starts as a no-op (so importing this module —
|
|
29
|
+
* including from the library entry / mcp-gateway — has no side effect and
|
|
30
|
+
* opens no file) and is swapped to the real file logger by
|
|
31
|
+
* {@link initLogger}, which the stdio CLI calls at startup.
|
|
32
|
+
*/
|
|
33
|
+
export declare let log: DiagnosticLogger;
|
|
34
|
+
/** Idempotently constructs the singleton from env config. Never rejects. */
|
|
35
|
+
export declare function initLogger(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort synchronous flush — for the crash path, where the last
|
|
38
|
+
* `fatal` line must hit disk before {@link process.exit}. No-op when the
|
|
39
|
+
* logger is disabled / not yet ready.
|
|
40
|
+
*/
|
|
41
|
+
export declare function flushLoggerSync(): void;
|
|
42
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AA2BA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,QAAQ,MAAM,WAAW,CAAC;AAGjC,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;IAClB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC;IACjB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;IAClB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;CACnB;AA0BD,eAAO,MAAM,cAAc,QAAkB,CAAC;AAuB9C;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7D,OAAO,CAAC;IACT,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC;CACpD,CAAC,CAmDD;AAED;;;;;GAKG;AACH,eAAO,IAAI,GAAG,EAAE,gBAA6B,CAAC;AAK9C,4EAA4E;AAC5E,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAQhD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAMtC"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 1.5 / KI-225 — local, rotating diagnostic logfile.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the backend already logs everything that *reaches* the
|
|
5
|
+
* API. The unique value of a client-side log is the failures that NEVER
|
|
6
|
+
* reach the backend (network-unreachable, local auth rejection, schema
|
|
7
|
+
* drift, uncaught crashes) plus a local request-latency view. The file is
|
|
8
|
+
* the customer's to keep and hand to support — there is NO remote
|
|
9
|
+
* transmission.
|
|
10
|
+
*
|
|
11
|
+
* 🚨 HARD CONSTRAINT — stdout is the MCP stdio transport. Logs MUST go to
|
|
12
|
+
* the FILE ONLY, never stdout. We therefore:
|
|
13
|
+
* - build pino against a {@link pino-roll} Sonic-boom file destination
|
|
14
|
+
* (NOT pino's default stdout destination, NOT a worker transport), and
|
|
15
|
+
* - fall back to a NO-OP logger on ANY construction failure — never
|
|
16
|
+
* crash the server, never write to stdout.
|
|
17
|
+
*
|
|
18
|
+
* Redaction (SOC2-adjacent, runs on customer machines): callers must only
|
|
19
|
+
* ever pass safe fields — event, method, URL pathname (no query/token),
|
|
20
|
+
* status, durationMs, tool name, correlationId, version, error
|
|
21
|
+
* type/message. Never tokens, Authorization headers, request/response
|
|
22
|
+
* bodies, tool arguments, file contents, or cell data.
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import pino from 'pino';
|
|
29
|
+
import pinoRoll from 'pino-roll';
|
|
30
|
+
import { getCorrelationId } from './correlation.js';
|
|
31
|
+
const SERVICE_NAME = 'mcp-server';
|
|
32
|
+
const LOG_FILE_BASENAME = 'mcp-server.log';
|
|
33
|
+
const LOG_MAX_SIZE = '5m';
|
|
34
|
+
const LOG_FILE_COUNT = 5;
|
|
35
|
+
const DEFAULT_LOG_DIR = path.join(os.homedir(), '.rockhopper', 'mcp-server');
|
|
36
|
+
const VALID_LEVELS = new Set([
|
|
37
|
+
'fatal',
|
|
38
|
+
'error',
|
|
39
|
+
'warn',
|
|
40
|
+
'info',
|
|
41
|
+
'debug',
|
|
42
|
+
'trace',
|
|
43
|
+
'silent',
|
|
44
|
+
]);
|
|
45
|
+
/** Package version stamped into every line's `base`. Best-effort read. */
|
|
46
|
+
const requireJson = createRequire(import.meta.url);
|
|
47
|
+
let resolvedVersion = '0.0.0';
|
|
48
|
+
try {
|
|
49
|
+
const pkg = requireJson('../package.json');
|
|
50
|
+
if (pkg.version)
|
|
51
|
+
resolvedVersion = pkg.version;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// package.json unreadable — keep the sentinel; never crash over a version.
|
|
55
|
+
}
|
|
56
|
+
export const serviceVersion = resolvedVersion;
|
|
57
|
+
/** Shared no-op so disabled/failed logging is a single cheap call site. */
|
|
58
|
+
const noopFn = () => { };
|
|
59
|
+
const noopLogger = {
|
|
60
|
+
debug: noopFn,
|
|
61
|
+
info: noopFn,
|
|
62
|
+
warn: noopFn,
|
|
63
|
+
error: noopFn,
|
|
64
|
+
fatal: noopFn,
|
|
65
|
+
};
|
|
66
|
+
function isTruthyEnv(value) {
|
|
67
|
+
if (!value)
|
|
68
|
+
return false;
|
|
69
|
+
const v = value.trim().toLowerCase();
|
|
70
|
+
return v !== '' && v !== '0' && v !== 'false' && v !== 'no';
|
|
71
|
+
}
|
|
72
|
+
function normalizeLevel(level) {
|
|
73
|
+
const v = level?.trim().toLowerCase();
|
|
74
|
+
return v && VALID_LEVELS.has(v) ? v : 'info';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds a file-only diagnostic logger. NEVER throws and NEVER writes to
|
|
78
|
+
* stdout — on opt-out or any failure it returns a no-op logger. Exposed
|
|
79
|
+
* (vs. only the {@link log} singleton) so tests can drive an isolated
|
|
80
|
+
* instance pointed at a tmpdir.
|
|
81
|
+
*/
|
|
82
|
+
export async function createDiagnosticLogger(opts = {}) {
|
|
83
|
+
const disable = opts.disable ?? isTruthyEnv(process.env.ROCKHOPPER_MCP_LOG_DISABLE);
|
|
84
|
+
if (disable) {
|
|
85
|
+
return { logger: noopLogger, flush: async () => { } };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const envDir = process.env.ROCKHOPPER_MCP_LOG_DIR?.trim();
|
|
89
|
+
const dir = opts.dir ?? (envDir && envDir.length ? envDir : DEFAULT_LOG_DIR);
|
|
90
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
91
|
+
const destination = await pinoRoll({
|
|
92
|
+
file: path.join(dir, LOG_FILE_BASENAME),
|
|
93
|
+
size: LOG_MAX_SIZE,
|
|
94
|
+
limit: { count: LOG_FILE_COUNT },
|
|
95
|
+
mkdir: true,
|
|
96
|
+
});
|
|
97
|
+
// A logging IO error must never crash the server (or escape as an
|
|
98
|
+
// unhandled 'error' event). Swallow it — diagnostics are best-effort.
|
|
99
|
+
destination.on('error', () => { });
|
|
100
|
+
const logger = pino({
|
|
101
|
+
level: normalizeLevel(opts.level ?? process.env.ROCKHOPPER_MCP_LOG_LEVEL),
|
|
102
|
+
base: { service: SERVICE_NAME, version: serviceVersion },
|
|
103
|
+
serializers: { err: pino.stdSerializers.err },
|
|
104
|
+
// Every line auto-carries the per-tool-call correlationId (Phase 1.1
|
|
105
|
+
// ALS scope) when one is active — no call site has to thread it.
|
|
106
|
+
mixin() {
|
|
107
|
+
const id = getCorrelationId();
|
|
108
|
+
return id ? { correlationId: id } : {};
|
|
109
|
+
},
|
|
110
|
+
}, destination);
|
|
111
|
+
const flush = () => new Promise((resolve) => {
|
|
112
|
+
try {
|
|
113
|
+
destination.flush(() => resolve());
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
resolve();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return { logger, flush, destination };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// fs / pino-roll / pino failure — degrade to no-op, never to stdout.
|
|
123
|
+
return { logger: noopLogger, flush: async () => { } };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Process-wide singleton. Starts as a no-op (so importing this module —
|
|
128
|
+
* including from the library entry / mcp-gateway — has no side effect and
|
|
129
|
+
* opens no file) and is swapped to the real file logger by
|
|
130
|
+
* {@link initLogger}, which the stdio CLI calls at startup.
|
|
131
|
+
*/
|
|
132
|
+
export let log = noopLogger;
|
|
133
|
+
let activeDestination;
|
|
134
|
+
let initPromise;
|
|
135
|
+
/** Idempotently constructs the singleton from env config. Never rejects. */
|
|
136
|
+
export async function initLogger() {
|
|
137
|
+
if (initPromise)
|
|
138
|
+
return initPromise;
|
|
139
|
+
initPromise = (async () => {
|
|
140
|
+
const built = await createDiagnosticLogger();
|
|
141
|
+
log = built.logger;
|
|
142
|
+
activeDestination = built.destination;
|
|
143
|
+
})();
|
|
144
|
+
return initPromise;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Best-effort synchronous flush — for the crash path, where the last
|
|
148
|
+
* `fatal` line must hit disk before {@link process.exit}. No-op when the
|
|
149
|
+
* logger is disabled / not yet ready.
|
|
150
|
+
*/
|
|
151
|
+
export function flushLoggerSync() {
|
|
152
|
+
try {
|
|
153
|
+
activeDestination?.flushSync();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Stream not ready or already closed — nothing more we can do pre-exit.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan 02 ruling 5 (David, 2026-08-04) — STRICT no-partial, on the machine
|
|
3
|
+
* surfaces.
|
|
4
|
+
*
|
|
5
|
+
* The rule David landed is "nothing serves change history until it is
|
|
6
|
+
* complete", and he chose STRICT specifically because THIS server is a
|
|
7
|
+
* consumer: rows leave here for Claude Desktop / Cursor, which get no banner,
|
|
8
|
+
* no colour and no human in the loop, and will narrate whatever arrives as
|
|
9
|
+
* fact. An empty list is the dangerous answer — "there are no changes" is a
|
|
10
|
+
* factual claim, and it is the claim an assistant makes when handed zero rows.
|
|
11
|
+
*
|
|
12
|
+
* So a not-ready answer here is a REFUSAL, not a value: `isError: true`, a
|
|
13
|
+
* shouting marker, an explicit "this is not an empty result", and a JSON
|
|
14
|
+
* object an assistant can branch on. A resource or prompt THROWS for the same
|
|
15
|
+
* reason a tool cannot return rows — a protocol error cannot be summarised as
|
|
16
|
+
* data.
|
|
17
|
+
*
|
|
18
|
+
* Vocabulary is deliberately the backend's, not a second one: the reasons
|
|
19
|
+
* mirror the SP02 `ParsedOutputNotReadyReason` family
|
|
20
|
+
* (`backend/src/common/parsed-output/parsed-output-not-ready.error.ts`), and
|
|
21
|
+
* `isNotReady` matches both the typed error and a wrapper carrying it as
|
|
22
|
+
* `cause`, exactly like the backend's `isParsedOutputNotReady`.
|
|
23
|
+
*/
|
|
24
|
+
/** Why a change-history answer is being refused. */
|
|
25
|
+
export type NotReadyReason =
|
|
26
|
+
/** A commit-diff fold is still rewriting this file's change-log window. */
|
|
27
|
+
'change_history_incomplete'
|
|
28
|
+
/** The backend answered 429/503: parsed outputs are still being produced. */
|
|
29
|
+
| 'still_producing'
|
|
30
|
+
/**
|
|
31
|
+
* The completeness probe itself could not answer. Fail CLOSED: under STRICT
|
|
32
|
+
* an unknown completeness state is not permission to serve rows. This is the
|
|
33
|
+
* one reason that is NOT a statement about the file.
|
|
34
|
+
*/
|
|
35
|
+
| 'completeness_unknown';
|
|
36
|
+
/** Grep marker; also the first token of every refusal an assistant sees. */
|
|
37
|
+
export declare const NOT_READY_MARKER = "CHANGE_HISTORY_NOT_READY";
|
|
38
|
+
/** Poll hint when the backend gave none — mirrors the backend's own default. */
|
|
39
|
+
export declare const DEFAULT_RETRY_AFTER_SECONDS = 15;
|
|
40
|
+
export declare class ChangeHistoryNotReadyError extends Error {
|
|
41
|
+
/** Structural marker, so a wrapper can be recognised without importing. */
|
|
42
|
+
readonly notReady: true;
|
|
43
|
+
readonly reason: NotReadyReason;
|
|
44
|
+
readonly retryAfterSeconds: number;
|
|
45
|
+
readonly fileMsId: string | null;
|
|
46
|
+
constructor(ctx: {
|
|
47
|
+
reason: NotReadyReason;
|
|
48
|
+
retryAfterSeconds?: number | null;
|
|
49
|
+
fileMsId?: string | null;
|
|
50
|
+
detail?: string;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Matches the typed error AND any error carrying one as `cause`. */
|
|
54
|
+
export declare function isNotReady(err: unknown): err is ChangeHistoryNotReadyError;
|
|
55
|
+
export interface NotReadyToolResult {
|
|
56
|
+
/** The SDK's `CallToolResult` carries an index signature; mirror it so this
|
|
57
|
+
* shape is assignable to a tool handler's return type. */
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
content: Array<{
|
|
60
|
+
type: 'text';
|
|
61
|
+
text: string;
|
|
62
|
+
}>;
|
|
63
|
+
isError: true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The tool answer for a refusal. Three defences, because one is not enough
|
|
67
|
+
* against a model that wants to be helpful: `isError`, prose that names the
|
|
68
|
+
* wrong inference explicitly, and a JSON object to branch on.
|
|
69
|
+
*/
|
|
70
|
+
export declare function notReadyToolResult(err: unknown): NotReadyToolResult;
|
|
71
|
+
/** The completeness probe this module needs from the API client. */
|
|
72
|
+
export interface CompletenessProbe {
|
|
73
|
+
getFoldStatus(fileMsId: string): Promise<{
|
|
74
|
+
foldPending: boolean;
|
|
75
|
+
foldTargetVersionId: number | null;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Throws {@link ChangeHistoryNotReadyError} unless the file's change history is
|
|
80
|
+
* complete.
|
|
81
|
+
*
|
|
82
|
+
* `foldPending` is the backend's own authoritative queue read
|
|
83
|
+
* (`GET /file-versions/file/:fileMsId/fold-status`, KI-1399) — while it is
|
|
84
|
+
* true a commit-diff fold is queued, retrying or running, and the change-log
|
|
85
|
+
* window is mid-rewrite. That is precisely the incomplete state, and after
|
|
86
|
+
* plan 02's write-path decoupling (David Q3, 2026-08-03: save the version row
|
|
87
|
+
* first, defer the fold) it is the NORMAL state immediately after any write.
|
|
88
|
+
*
|
|
89
|
+
* A probe that cannot answer refuses. The backend's own probe fails OPEN to
|
|
90
|
+
* not-pending on purpose — a UI lock that can hang is worse than a stale row —
|
|
91
|
+
* but that trade does not transfer here: for a machine consumer a wrong
|
|
92
|
+
* "complete" is a fabricated fact, so the client-side default is the opposite.
|
|
93
|
+
*/
|
|
94
|
+
export declare function assertChangeHistoryComplete(api: CompletenessProbe, fileMsId: string): Promise<void>;
|
|
95
|
+
//# sourceMappingURL=not-ready.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"not-ready.d.ts","sourceRoot":"","sources":["../src/not-ready.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,oDAAoD;AACpD,MAAM,MAAM,cAAc;AACxB,2EAA2E;AACzE,2BAA2B;AAC7B,6EAA6E;GAC3E,iBAAiB;AACnB;;;;GAIG;GACD,sBAAsB,CAAC;AAE3B,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D,gFAAgF;AAChF,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAG,IAAI,CAAU;IAClC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;gBAErB,GAAG,EAAE;QACf,MAAM,EAAE,cAAc,CAAC;QACvB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAClC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB;CAeF;AAED,qEAAqE;AACrE,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,0BAA0B,CAI1E;AAyBD,MAAM,WAAW,kBAAkB;IACjC;8DAC0D;IAC1D,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,IAAI,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAwBnE;AAED,oEAAoE;AACpE,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QACvC,WAAW,EAAE,OAAO,CAAC;QACrB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;KACpC,CAAC,CAAC;CACJ;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,iBAAiB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan 02 ruling 5 (David, 2026-08-04) — STRICT no-partial, on the machine
|
|
3
|
+
* surfaces.
|
|
4
|
+
*
|
|
5
|
+
* The rule David landed is "nothing serves change history until it is
|
|
6
|
+
* complete", and he chose STRICT specifically because THIS server is a
|
|
7
|
+
* consumer: rows leave here for Claude Desktop / Cursor, which get no banner,
|
|
8
|
+
* no colour and no human in the loop, and will narrate whatever arrives as
|
|
9
|
+
* fact. An empty list is the dangerous answer — "there are no changes" is a
|
|
10
|
+
* factual claim, and it is the claim an assistant makes when handed zero rows.
|
|
11
|
+
*
|
|
12
|
+
* So a not-ready answer here is a REFUSAL, not a value: `isError: true`, a
|
|
13
|
+
* shouting marker, an explicit "this is not an empty result", and a JSON
|
|
14
|
+
* object an assistant can branch on. A resource or prompt THROWS for the same
|
|
15
|
+
* reason a tool cannot return rows — a protocol error cannot be summarised as
|
|
16
|
+
* data.
|
|
17
|
+
*
|
|
18
|
+
* Vocabulary is deliberately the backend's, not a second one: the reasons
|
|
19
|
+
* mirror the SP02 `ParsedOutputNotReadyReason` family
|
|
20
|
+
* (`backend/src/common/parsed-output/parsed-output-not-ready.error.ts`), and
|
|
21
|
+
* `isNotReady` matches both the typed error and a wrapper carrying it as
|
|
22
|
+
* `cause`, exactly like the backend's `isParsedOutputNotReady`.
|
|
23
|
+
*/
|
|
24
|
+
/** Grep marker; also the first token of every refusal an assistant sees. */
|
|
25
|
+
export const NOT_READY_MARKER = 'CHANGE_HISTORY_NOT_READY';
|
|
26
|
+
/** Poll hint when the backend gave none — mirrors the backend's own default. */
|
|
27
|
+
export const DEFAULT_RETRY_AFTER_SECONDS = 15;
|
|
28
|
+
export class ChangeHistoryNotReadyError extends Error {
|
|
29
|
+
/** Structural marker, so a wrapper can be recognised without importing. */
|
|
30
|
+
notReady = true;
|
|
31
|
+
reason;
|
|
32
|
+
retryAfterSeconds;
|
|
33
|
+
fileMsId;
|
|
34
|
+
constructor(ctx) {
|
|
35
|
+
super(`${NOT_READY_MARKER}: reason=${ctx.reason} ` +
|
|
36
|
+
`retryAfterSeconds=${ctx.retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS}` +
|
|
37
|
+
(ctx.detail ? ` — ${ctx.detail}` : '') +
|
|
38
|
+
' — Rockhopper has not finished computing this change history. ' +
|
|
39
|
+
'This is NOT an empty result: no rows can be served yet, and nothing ' +
|
|
40
|
+
'may be inferred about whether the file changed.');
|
|
41
|
+
this.name = 'ChangeHistoryNotReadyError';
|
|
42
|
+
this.reason = ctx.reason;
|
|
43
|
+
this.retryAfterSeconds =
|
|
44
|
+
ctx.retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS;
|
|
45
|
+
this.fileMsId = ctx.fileMsId ?? null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Matches the typed error AND any error carrying one as `cause`. */
|
|
49
|
+
export function isNotReady(err) {
|
|
50
|
+
if (err instanceof ChangeHistoryNotReadyError)
|
|
51
|
+
return true;
|
|
52
|
+
const cause = err?.cause;
|
|
53
|
+
return cause instanceof ChangeHistoryNotReadyError;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* HTTP statuses that answer the question DEFINITIVELY. A permanently-refused
|
|
57
|
+
* request is not "still producing", and the two must not share an answer: a
|
|
58
|
+
* probe that 404s (no such file) or 403s (no access) has told us something
|
|
59
|
+
* true, and dressing it up as a capacity signal would send an assistant into a
|
|
60
|
+
* retry loop against a wall. Matched structurally (`err.status`) so this module
|
|
61
|
+
* stays free of an import cycle with the API client.
|
|
62
|
+
*/
|
|
63
|
+
const DEFINITIVE_HTTP_STATUSES = new Set([400, 401, 403, 404, 405, 410, 422]);
|
|
64
|
+
function isDefinitiveRejection(err) {
|
|
65
|
+
const status = err?.status;
|
|
66
|
+
return typeof status === 'number' && DEFINITIVE_HTTP_STATUSES.has(status);
|
|
67
|
+
}
|
|
68
|
+
/** Unwrap to the typed error (accepts the `cause` shape). */
|
|
69
|
+
function unwrap(err) {
|
|
70
|
+
if (err instanceof ChangeHistoryNotReadyError)
|
|
71
|
+
return err;
|
|
72
|
+
const cause = err.cause;
|
|
73
|
+
if (cause instanceof ChangeHistoryNotReadyError)
|
|
74
|
+
return cause;
|
|
75
|
+
return new ChangeHistoryNotReadyError({ reason: 'completeness_unknown' });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The tool answer for a refusal. Three defences, because one is not enough
|
|
79
|
+
* against a model that wants to be helpful: `isError`, prose that names the
|
|
80
|
+
* wrong inference explicitly, and a JSON object to branch on.
|
|
81
|
+
*/
|
|
82
|
+
export function notReadyToolResult(err) {
|
|
83
|
+
const e = unwrap(err);
|
|
84
|
+
const payload = {
|
|
85
|
+
status: 'not_ready',
|
|
86
|
+
reason: e.reason,
|
|
87
|
+
retryAfterSeconds: e.retryAfterSeconds,
|
|
88
|
+
fileMsId: e.fileMsId,
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: `${NOT_READY_MARKER} — this is NOT a result and NOT an empty result.\n` +
|
|
95
|
+
`Rockhopper has not finished computing this file's change history, ` +
|
|
96
|
+
`so no rows can be served.\n` +
|
|
97
|
+
`Do NOT say there are no changes, that nothing changed, or that the ` +
|
|
98
|
+
`history is empty — none of that is known.\n` +
|
|
99
|
+
`Retry in ${e.retryAfterSeconds} seconds.\n` +
|
|
100
|
+
JSON.stringify(payload),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
isError: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Throws {@link ChangeHistoryNotReadyError} unless the file's change history is
|
|
108
|
+
* complete.
|
|
109
|
+
*
|
|
110
|
+
* `foldPending` is the backend's own authoritative queue read
|
|
111
|
+
* (`GET /file-versions/file/:fileMsId/fold-status`, KI-1399) — while it is
|
|
112
|
+
* true a commit-diff fold is queued, retrying or running, and the change-log
|
|
113
|
+
* window is mid-rewrite. That is precisely the incomplete state, and after
|
|
114
|
+
* plan 02's write-path decoupling (David Q3, 2026-08-03: save the version row
|
|
115
|
+
* first, defer the fold) it is the NORMAL state immediately after any write.
|
|
116
|
+
*
|
|
117
|
+
* A probe that cannot answer refuses. The backend's own probe fails OPEN to
|
|
118
|
+
* not-pending on purpose — a UI lock that can hang is worse than a stale row —
|
|
119
|
+
* but that trade does not transfer here: for a machine consumer a wrong
|
|
120
|
+
* "complete" is a fabricated fact, so the client-side default is the opposite.
|
|
121
|
+
*/
|
|
122
|
+
export async function assertChangeHistoryComplete(api, fileMsId) {
|
|
123
|
+
let status;
|
|
124
|
+
try {
|
|
125
|
+
status = await api.getFoldStatus(fileMsId);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (isNotReady(err))
|
|
129
|
+
throw unwrap(err);
|
|
130
|
+
// A definitive rejection is the caller's real answer — let it through so
|
|
131
|
+
// the tool reports "not found" / "no access" instead of "retry in 15s".
|
|
132
|
+
if (isDefinitiveRejection(err))
|
|
133
|
+
throw err;
|
|
134
|
+
throw new ChangeHistoryNotReadyError({
|
|
135
|
+
reason: 'completeness_unknown',
|
|
136
|
+
fileMsId,
|
|
137
|
+
detail: `fold-status probe failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (status.foldPending) {
|
|
141
|
+
throw new ChangeHistoryNotReadyError({
|
|
142
|
+
reason: 'change_history_incomplete',
|
|
143
|
+
fileMsId,
|
|
144
|
+
detail: `a commit-diff fold is still pending` +
|
|
145
|
+
(status.foldTargetVersionId == null
|
|
146
|
+
? ''
|
|
147
|
+
: ` for version ${status.foldTargetVersionId}`),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=not-ready.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"not-ready.js","sourceRoot":"","sources":["../src/not-ready.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/prompts/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,IAAI,CAgOvE"}
|
package/dist/prompts/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { assertChangeHistoryComplete } from '../not-ready.js';
|
|
2
3
|
export function registerPrompts(server, api) {
|
|
3
4
|
server.registerPrompt('summarize-file-changes', {
|
|
4
5
|
title: 'Summarize Recent Changes',
|
|
@@ -7,6 +8,10 @@ export function registerPrompts(server, api) {
|
|
|
7
8
|
fileMsId: z.string().describe('Platform ID of the enrolled file'),
|
|
8
9
|
},
|
|
9
10
|
}, async ({ fileMsId }) => {
|
|
11
|
+
// Plan 02 ruling 5 (STRICT) — a prompt is the highest-risk surface: its
|
|
12
|
+
// whole product is a model narrating these rows as fact. Refuse before
|
|
13
|
+
// assembling anything; a prompt has no error channel but its own throw.
|
|
14
|
+
await assertChangeHistoryComplete(api, fileMsId);
|
|
10
15
|
const [file, versions, changesPage] = await Promise.all([
|
|
11
16
|
api.getEnrolledFile(fileMsId),
|
|
12
17
|
api.getFileVersions(fileMsId),
|
|
@@ -118,6 +123,9 @@ export function registerPrompts(server, api) {
|
|
|
118
123
|
fileMsId: z.string().describe('Platform ID of the enrolled file'),
|
|
119
124
|
},
|
|
120
125
|
}, async ({ fileMsId }) => {
|
|
126
|
+
// Plan 02 ruling 5 (STRICT) — this prompt reports a change COUNT, which
|
|
127
|
+
// is the same factual claim in one number instead of many rows.
|
|
128
|
+
await assertChangeHistoryComplete(api, fileMsId);
|
|
121
129
|
const [file, versions, comments, changesPage] = await Promise.all([
|
|
122
130
|
api.getEnrolledFile(fileMsId),
|
|
123
131
|
api.getFileVersions(fileMsId),
|