pumuki 6.3.166 → 6.3.168

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 CHANGED
@@ -6,6 +6,18 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [6.3.168] - 2026-05-06
10
+
11
+ ### Fixed
12
+
13
+ - **Tracked evidence commit hygiene:** `pumuki-pre-commit` now respects tracked `.ai_evidence.json` when it was not staged by the developer before a successful code commit. Documentation-only commits still restore evidence drift, and `PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE=1` preserves the previous forced restage behavior.
14
+
15
+ ## [6.3.167] - 2026-05-06
16
+
17
+ ### Fixed
18
+
19
+ - **MCP evidence stdio strict backend gate:** the MCP bridge now uses explicit JSON-RPC constants and requires adapter-provided evidence env vars instead of production defaults, keeping `PRE_PUSH` fail-closed without false backend blockers.
20
+
9
21
  ## [6.3.166] - 2026-05-06
10
22
 
11
23
  ### Fixed
@@ -77,6 +77,9 @@ const shouldSkipRestagingTrackedEvidenceForDocumentationOnlyScope = (params: {
77
77
  return paths.every(isDocumentationOnlyStagedPath);
78
78
  };
79
79
 
80
+ const hasStagedEvidencePath = (paths: ReadonlyArray<string>): boolean =>
81
+ paths.some((path) => path === '.ai_evidence.json' || path === '.AI_EVIDENCE.json');
82
+
80
83
  const PRE_COMMIT_EVIDENCE_MAX_AGE_SECONDS = 900;
81
84
  const PRE_PUSH_EVIDENCE_MAX_AGE_SECONDS = 1800;
82
85
  const HOOK_GATE_PROGRESS_REMINDER_MS = 2000;
@@ -520,6 +523,7 @@ const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
520
523
  dependencies: StageRunnerDependencies;
521
524
  repoRoot: string;
522
525
  gateBlocked: boolean;
526
+ evidenceWasStagedAtStart: boolean;
523
527
  }): boolean => {
524
528
  const evidenceAbsolutePath = join(params.repoRoot, EVIDENCE_FILE_PATH);
525
529
  if (!existsSync(evidenceAbsolutePath)) {
@@ -543,6 +547,19 @@ const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
543
547
  params.dependencies.restorePathFromHead(params.repoRoot, EVIDENCE_FILE_PATH);
544
548
  return false;
545
549
  }
550
+ if (
551
+ !params.gateBlocked &&
552
+ !params.evidenceWasStagedAtStart &&
553
+ !isTruthyEnvFlag(process.env.PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE)
554
+ ) {
555
+ if (!params.dependencies.isQuietMode()) {
556
+ process.stderr.write(
557
+ `[pumuki][evidence-sync] tracked ${EVIDENCE_FILE_PATH} left unstaged because it was not staged before PRE_COMMIT. ` +
558
+ `Force previous behavior: PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE=1\n`
559
+ );
560
+ }
561
+ return false;
562
+ }
546
563
  try {
547
564
  params.dependencies.stagePath(params.repoRoot, EVIDENCE_FILE_PATH);
548
565
  return false;
@@ -787,6 +804,7 @@ export async function runPreCommitStage(
787
804
  const activeDependencies = getDependencies(dependencies);
788
805
  const repoRoot = activeDependencies.resolveRepoRoot();
789
806
  const manifestSnapshot = captureManifestGuardSnapshot(repoRoot);
807
+ const initiallyStagedPaths = activeDependencies.listStagedIndexPaths(repoRoot);
790
808
  activeDependencies.ensureRuntimeArtifactsIgnored(repoRoot);
791
809
  if (
792
810
  enforceGitAtomicityGate({
@@ -820,6 +838,7 @@ export async function runPreCommitStage(
820
838
  dependencies: activeDependencies,
821
839
  repoRoot,
822
840
  gateBlocked: result.exitCode !== 0,
841
+ evidenceWasStagedAtStart: hasStagedEvidencePath(initiallyStagedPaths),
823
842
  })
824
843
  ) {
825
844
  return 1;
@@ -22,11 +22,30 @@ type JsonRpcResponse = {
22
22
 
23
23
  const MCP_PROTOCOL_VERSION = '2024-11-05';
24
24
  const JSON_RPC_VERSION = '2.0';
25
- const DEFAULT_EVIDENCE_HOST = '127.0.0.1';
26
- const DEFAULT_EVIDENCE_ROUTE = '/ai-evidence';
27
- const DEFAULT_EVIDENCE_PORT = 7341;
25
+ const EVIDENCE_HOST_ENV = 'PUMUKI_EVIDENCE_HOST';
26
+ const EVIDENCE_ROUTE_ENV = 'PUMUKI_EVIDENCE_ROUTE';
27
+ const EVIDENCE_PORT_ENV = 'PUMUKI_EVIDENCE_PORT';
28
28
  const PORT_PROBE_TIMEOUT_MS = 600;
29
+ const EPHEMERAL_LISTENER_PORT = 0;
30
+ const EMPTY_TEXT_LENGTH = 0;
31
+ const DECIMAL_RADIX = 10;
32
+ const PROCESS_FAILURE_EXIT_CODE = 1;
33
+ const LINE_BREAK_WIDTH = 1;
34
+ const TOOL_IMPLEMENTATION_VERSION = '1.0.0';
29
35
  const JSON_RPC_INVALID_REQUEST = -32600;
36
+ const JSON_RPC_METHOD_NOT_FOUND = -32601;
37
+ const JSON_RPC_INVALID_PARAMS = -32602;
38
+ const JSON_RPC_INTERNAL_ERROR = -32603;
39
+ const JSON_RPC_PARSE_ERROR = -32700;
40
+ const LINE_BREAK_NOT_FOUND = -1;
41
+
42
+ const readRequiredEnv = (name: string): string => {
43
+ const value = process.env[name];
44
+ if (typeof value === 'string' && value.trim().length > EMPTY_TEXT_LENGTH) {
45
+ return value;
46
+ }
47
+ throw new Error(`${name} is required for pumuki MCP evidence stdio bridge.`);
48
+ };
30
49
 
31
50
  const toJsonRpcId = (value: unknown): JsonRpcId => {
32
51
  if (typeof value === 'string' || typeof value === 'number' || value === null) {
@@ -49,7 +68,7 @@ const sendResult = (id: JsonRpcId, result: unknown): void => {
49
68
 
50
69
  const sendError = (id: JsonRpcId, code: number, message: string): void => {
51
70
  sendMessage({
52
- jsonrpc: '2.0',
71
+ jsonrpc: JSON_RPC_VERSION,
53
72
  id,
54
73
  error: {
55
74
  code,
@@ -81,9 +100,10 @@ const findAvailableListenerNumber = async (host: string): Promise<number> =>
81
100
  await new Promise((resolve, reject) => {
82
101
  const probe = createServer();
83
102
  probe.once('error', reject);
84
- probe.listen(0, host, () => {
103
+ probe.listen(EPHEMERAL_LISTENER_PORT, host, () => {
85
104
  const address = probe.address();
86
- const port = address && typeof address === 'object' ? address.port : 0;
105
+ const port =
106
+ address && typeof address === 'object' ? address.port : EPHEMERAL_LISTENER_PORT;
87
107
  probe.close(() => resolve(port));
88
108
  });
89
109
  });
@@ -91,7 +111,7 @@ const findAvailableListenerNumber = async (host: string): Promise<number> =>
91
111
  const fetchJson = async (url: string): Promise<unknown> => {
92
112
  const response = await fetch(url);
93
113
  const text = await response.text();
94
- if (text.trim().length === 0) {
114
+ if (text.trim().length === EMPTY_TEXT_LENGTH) {
95
115
  return {};
96
116
  }
97
117
  return JSON.parse(text) as unknown;
@@ -111,14 +131,17 @@ const startOrReuseEvidenceHttp = async (): Promise<{
111
131
  port: number;
112
132
  route: string;
113
133
  }> => {
114
- const host = process.env.PUMUKI_EVIDENCE_HOST ?? DEFAULT_EVIDENCE_HOST;
115
- const route = process.env.PUMUKI_EVIDENCE_ROUTE ?? DEFAULT_EVIDENCE_ROUTE;
116
- const parsedListener = Number.parseInt(process.env.PUMUKI_EVIDENCE_PORT ?? '', 10);
117
- const preferredListener = Number.isFinite(parsedListener)
118
- ? parsedListener
119
- : DEFAULT_EVIDENCE_PORT;
134
+ const host = readRequiredEnv(EVIDENCE_HOST_ENV);
135
+ const route = readRequiredEnv(EVIDENCE_ROUTE_ENV);
136
+ const parsedListener = Number.parseInt(readRequiredEnv(EVIDENCE_PORT_ENV), DECIMAL_RADIX);
137
+ if (!Number.isFinite(parsedListener)) {
138
+ throw new Error(`${EVIDENCE_PORT_ENV} must be a valid listener number.`);
139
+ }
140
+ const preferredListener = parsedListener;
120
141
  const requestedListener =
121
- preferredListener > 0 ? preferredListener : await findAvailableListenerNumber(host);
142
+ preferredListener > EPHEMERAL_LISTENER_PORT
143
+ ? preferredListener
144
+ : await findAvailableListenerNumber(host);
122
145
  const healthUrl = `http://${host}:${requestedListener}/health`;
123
146
 
124
147
  try {
@@ -228,7 +251,7 @@ const run = async (): Promise<void> => {
228
251
  protocolVersion: MCP_PROTOCOL_VERSION,
229
252
  serverInfo: {
230
253
  name: 'pumuki-evidence-stdio',
231
- version: '1.0.0',
254
+ version: TOOL_IMPLEMENTATION_VERSION,
232
255
  },
233
256
  capabilities: {
234
257
  tools: {
@@ -269,7 +292,7 @@ const run = async (): Promise<void> => {
269
292
  const name = typeof params.name === 'string' ? params.name : '';
270
293
  const tool = toolsCatalog.find((entry) => entry.name === name);
271
294
  if (!tool) {
272
- sendError(id, -32602, `Unknown tool: ${name}`);
295
+ sendError(id, JSON_RPC_INVALID_PARAMS, `Unknown tool: ${name}`);
273
296
  return;
274
297
  }
275
298
  const payload = await fetchJson(`${baseUrl}${tool.path}`);
@@ -304,7 +327,7 @@ const run = async (): Promise<void> => {
304
327
  const uri = typeof params.uri === 'string' ? params.uri : '';
305
328
  const resource = resourcesCatalog.find((entry) => entry.uri === uri);
306
329
  if (!resource) {
307
- sendError(id, -32602, `Unknown resource URI: ${uri}`);
330
+ sendError(id, JSON_RPC_INVALID_PARAMS, `Unknown resource URI: ${uri}`);
308
331
  return;
309
332
  }
310
333
  const payload = await fetchJson(`${baseUrl}${resource.path}`);
@@ -320,31 +343,31 @@ const run = async (): Promise<void> => {
320
343
  return;
321
344
  }
322
345
 
323
- sendError(id, -32601, `Method not found: ${method}`);
346
+ sendError(id, JSON_RPC_METHOD_NOT_FOUND, `Method not found: ${method}`);
324
347
  };
325
348
 
326
349
  const processBuffer = (): void => {
327
350
  while (true) {
328
351
  const lineEnd = textBuffer.indexOf('\n');
329
- if (lineEnd === -1) {
352
+ if (lineEnd === LINE_BREAK_NOT_FOUND) {
330
353
  return;
331
354
  }
332
355
  const rawLine = textBuffer.slice(0, lineEnd).trim();
333
- textBuffer = textBuffer.slice(lineEnd + 1);
334
- if (rawLine.length === 0) {
356
+ textBuffer = textBuffer.slice(lineEnd + LINE_BREAK_WIDTH);
357
+ if (rawLine.length === EMPTY_TEXT_LENGTH) {
335
358
  continue;
336
359
  }
337
360
  let payload: JsonRpcRequest;
338
361
  try {
339
362
  payload = JSON.parse(rawLine) as JsonRpcRequest;
340
363
  } catch {
341
- sendError(null, -32700, 'Parse error');
364
+ sendError(null, JSON_RPC_PARSE_ERROR, 'Parse error');
342
365
  continue;
343
366
  }
344
367
  void handleRequest(payload).catch((error) => {
345
368
  const id = toJsonRpcId(payload.id);
346
369
  const message = error instanceof Error ? error.message : 'Internal error';
347
- sendError(id, -32603, message);
370
+ sendError(id, JSON_RPC_INTERNAL_ERROR, message);
348
371
  });
349
372
  }
350
373
  };
@@ -358,5 +381,5 @@ const run = async (): Promise<void> => {
358
381
  void run().catch((error) => {
359
382
  const message = error instanceof Error ? error.message : 'Unknown MCP stdio bridge error';
360
383
  process.stderr.write(`[pumuki-mcp-evidence-stdio] ${message}\n`);
361
- process.exit(1);
384
+ process.exit(PROCESS_FAILURE_EXIT_CODE);
362
385
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.166",
3
+ "version": "6.3.168",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {