pumuki 6.3.169 → 6.3.171
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 +0 -37
- package/VERSION +1 -1
- package/core/facts/detectors/text/ios.test.ts +0 -36
- package/core/facts/detectors/text/ios.ts +0 -24
- package/core/facts/detectors/typescript/index.ts +1 -6
- package/integrations/config/coreSkillsLock.ts +1 -11
- package/integrations/config/skillsRuleSet.ts +1 -0
- package/integrations/git/stageRunners.ts +0 -20
- package/integrations/lifecycle/audit.ts +35 -5
- package/integrations/mcp/evidenceStdioServer.cli.ts +33 -76
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,43 +6,6 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [6.3.169] - 2026-05-06
|
|
10
|
-
|
|
11
|
-
### Fixed
|
|
12
|
-
|
|
13
|
-
- **Tracked evidence working-tree hygiene:** successful `pumuki-pre-commit` now restores tracked `.ai_evidence.json` from `HEAD` when the file was not staged at hook start, preventing pre-commit frameworks from failing with “files were modified by this hook” while still excluding evidence from code commits.
|
|
14
|
-
|
|
15
|
-
## [6.3.168] - 2026-05-06
|
|
16
|
-
|
|
17
|
-
### Fixed
|
|
18
|
-
|
|
19
|
-
- **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.
|
|
20
|
-
|
|
21
|
-
## [6.3.167] - 2026-05-06
|
|
22
|
-
|
|
23
|
-
### Fixed
|
|
24
|
-
|
|
25
|
-
- **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.
|
|
26
|
-
|
|
27
|
-
## [6.3.166] - 2026-05-06
|
|
28
|
-
|
|
29
|
-
### Fixed
|
|
30
|
-
|
|
31
|
-
- **Backend heuristic false positives:** JSON-RPC protocol codes and MCP listener resolution no longer trigger backend hardcoded config or magic-number blockers during `PRE_PUSH`.
|
|
32
|
-
|
|
33
|
-
## [6.3.165] - 2026-05-06
|
|
34
|
-
|
|
35
|
-
### Fixed
|
|
36
|
-
|
|
37
|
-
- **MCP evidence stdio PRE_PUSH:** los defaults de host, ruta, puerto y timeout quedan nombrados como constantes para cumplir reglas backend de configuración y evitar bloqueos del hook al publicar ramas de release.
|
|
38
|
-
|
|
39
|
-
## [6.3.164] - 2026-05-06
|
|
40
|
-
|
|
41
|
-
### Fixed
|
|
42
|
-
|
|
43
|
-
- **PUMUKI-INC-130 XCTest helpers brownfield:** `trackForMemoryLeaks`/factory helpers that import `XCTest` but do not define `XCTestCase` suites or `test...` methods no longer trigger `skills.ios.critical-test-quality`.
|
|
44
|
-
- **Skills runtime enforcement:** declarative skill registry rules stay visible in coverage metadata without being counted as missing runtime AST detectors; AUTO rules without detector mapping still fail closed.
|
|
45
|
-
|
|
46
9
|
## [6.3.143] - 2026-05-05
|
|
47
10
|
|
|
48
11
|
### Fixed
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.171
|
|
@@ -683,46 +683,10 @@ func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #filePath,
|
|
|
683
683
|
addTeardownBlock { _ = instance }
|
|
684
684
|
}
|
|
685
685
|
`;
|
|
686
|
-
const ruralGoHelperSource = `
|
|
687
|
-
import XCTest
|
|
688
|
-
|
|
689
|
-
final class WeakReference<T: AnyObject> {
|
|
690
|
-
weak var value: T?
|
|
691
|
-
|
|
692
|
-
init(_ value: T) {
|
|
693
|
-
self.value = value
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
func trackForMemoryLeaks(
|
|
698
|
-
_ instance: AnyObject,
|
|
699
|
-
testCase: XCTestCase,
|
|
700
|
-
file: StaticString = #filePath,
|
|
701
|
-
line: UInt = #line
|
|
702
|
-
) {
|
|
703
|
-
nonisolated(unsafe) let reference = makeSUT(instance)
|
|
704
|
-
testCase.addTeardownBlock {
|
|
705
|
-
XCTAssertNil(
|
|
706
|
-
reference.value,
|
|
707
|
-
"Instance should have been deallocated. Potential memory leak.",
|
|
708
|
-
file: file,
|
|
709
|
-
line: line
|
|
710
|
-
)
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
private func makeSUT<T: AnyObject>(_ instance: T) -> WeakReference<T> {
|
|
715
|
-
WeakReference(instance)
|
|
716
|
-
}
|
|
717
|
-
`;
|
|
718
686
|
|
|
719
687
|
assert.equal(hasSwiftLegacyXCTestImportUsage(helperSource), false);
|
|
720
688
|
assert.equal(hasSwiftModernizableXCTestSuiteUsage(helperSource), false);
|
|
721
689
|
assert.equal(hasSwiftXCTestAssertionUsage(helperSource), false);
|
|
722
|
-
assert.equal(hasSwiftLegacyXCTestImportUsage(ruralGoHelperSource), false);
|
|
723
|
-
assert.equal(hasSwiftModernizableXCTestSuiteUsage(ruralGoHelperSource), false);
|
|
724
|
-
assert.equal(hasSwiftXCTestAssertionUsage(ruralGoHelperSource), false);
|
|
725
|
-
assert.equal(hasSwiftXCTUnwrapUsage(ruralGoHelperSource), false);
|
|
726
690
|
});
|
|
727
691
|
|
|
728
692
|
test('hasSwiftXCTUnwrapUsage detecta XCTUnwrap real y evita strings', () => {
|
|
@@ -840,14 +840,6 @@ const hasSwiftXCTestOnlyBrownfieldSuiteUsage = (source: string): boolean => {
|
|
|
840
840
|
return !hasSwiftTestingImportUsage(source) && !hasSwiftTestingSuiteAttributeUsage(source);
|
|
841
841
|
};
|
|
842
842
|
|
|
843
|
-
const hasSwiftXCTestHelperOrFactoryUsage = (source: string): boolean => {
|
|
844
|
-
return (
|
|
845
|
-
hasSwiftXCTestImportUsage(source) &&
|
|
846
|
-
!hasSwiftXCTestCaseSubclassUsage(source) &&
|
|
847
|
-
!hasSwiftLegacyXCTestMethodUsage(source)
|
|
848
|
-
);
|
|
849
|
-
};
|
|
850
|
-
|
|
851
843
|
export const hasSwiftLegacyXCTestImportUsage = (source: string): boolean => {
|
|
852
844
|
if (!hasSwiftXCTestImportUsage(source)) {
|
|
853
845
|
return false;
|
|
@@ -893,10 +885,6 @@ export const hasSwiftXCTestAssertionUsage = (source: string): boolean => {
|
|
|
893
885
|
return false;
|
|
894
886
|
}
|
|
895
887
|
|
|
896
|
-
if (hasSwiftXCTestHelperOrFactoryUsage(source)) {
|
|
897
|
-
return false;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
888
|
if (hasSwiftXCTestOnlyBrownfieldSuiteUsage(source)) {
|
|
901
889
|
return false;
|
|
902
890
|
}
|
|
@@ -912,10 +900,6 @@ export const hasSwiftXCTUnwrapUsage = (source: string): boolean => {
|
|
|
912
900
|
return false;
|
|
913
901
|
}
|
|
914
902
|
|
|
915
|
-
if (hasSwiftXCTestHelperOrFactoryUsage(source)) {
|
|
916
|
-
return false;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
903
|
if (hasSwiftXCTestOnlyBrownfieldSuiteUsage(source)) {
|
|
920
904
|
return false;
|
|
921
905
|
}
|
|
@@ -932,10 +916,6 @@ const hasSwiftConfirmationUsage = (source: string): boolean => {
|
|
|
932
916
|
};
|
|
933
917
|
|
|
934
918
|
export const hasSwiftWaitForExpectationsUsage = (source: string): boolean => {
|
|
935
|
-
if (hasSwiftXCTestHelperOrFactoryUsage(source)) {
|
|
936
|
-
return false;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
919
|
const legacyWaitPattern = /\bwait\s*\(\s*for\s*:|\bwaitForExpectations\s*\(/;
|
|
940
920
|
return collectSwiftFunctionDeclarations(source).some((declaration) => {
|
|
941
921
|
if (!/\basync\b/.test(declaration.signature)) {
|
|
@@ -947,10 +927,6 @@ export const hasSwiftWaitForExpectationsUsage = (source: string): boolean => {
|
|
|
947
927
|
};
|
|
948
928
|
|
|
949
929
|
export const hasSwiftLegacyExpectationDescriptionUsage = (source: string): boolean => {
|
|
950
|
-
if (hasSwiftXCTestHelperOrFactoryUsage(source)) {
|
|
951
|
-
return false;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
930
|
return collectSwiftFunctionDeclarations(source).some((declaration) => {
|
|
955
931
|
if (!/\basync\b/.test(declaration.signature)) {
|
|
956
932
|
return false;
|
|
@@ -4845,12 +4845,7 @@ const isRuntimeApiLiteral = (node: AstNode): boolean => {
|
|
|
4845
4845
|
};
|
|
4846
4846
|
|
|
4847
4847
|
const isNeutralHardcodedNumericLiteral = (node: AstNode): boolean => {
|
|
4848
|
-
return (
|
|
4849
|
-
node.type === 'NumericLiteral' &&
|
|
4850
|
-
(node.value === 0 ||
|
|
4851
|
-
node.value === 1 ||
|
|
4852
|
-
(typeof node.value === 'number' && node.value >= -32768 && node.value <= -32000))
|
|
4853
|
-
);
|
|
4848
|
+
return node.type === 'NumericLiteral' && (node.value === 0 || node.value === 1);
|
|
4854
4849
|
};
|
|
4855
4850
|
|
|
4856
4851
|
const isBenignHardcodedConfigLiteral = (node: AstNode): boolean => {
|
|
@@ -8,15 +8,6 @@ const PACKAGE_ROOT = resolve(__dirname, '..', '..');
|
|
|
8
8
|
|
|
9
9
|
let cachedCoreSkillsLock: SkillsLockV1 | undefined;
|
|
10
10
|
|
|
11
|
-
const writeDebugFallback = (error: unknown): void => {
|
|
12
|
-
if (process.env.PUMUKI_DEBUG !== '1') {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
-
process.stderr.write(`[pumuki][skills-lock] compile fallback: ${message}\n`);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
11
|
export const resolveCoreSkillsLockForPackageRoot = (
|
|
21
12
|
packageRoot: string
|
|
22
13
|
): SkillsLockV1 | undefined => {
|
|
@@ -29,8 +20,7 @@ export const resolveCoreSkillsLockForPackageRoot = (
|
|
|
29
20
|
if (compiledLock.bundles.length > 0) {
|
|
30
21
|
return compiledLock;
|
|
31
22
|
}
|
|
32
|
-
} catch
|
|
33
|
-
writeDebugFallback(error);
|
|
23
|
+
} catch {
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
return loadSkillsLock(packageRoot);
|
|
@@ -77,9 +77,6 @@ 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
|
-
|
|
83
80
|
const PRE_COMMIT_EVIDENCE_MAX_AGE_SECONDS = 900;
|
|
84
81
|
const PRE_PUSH_EVIDENCE_MAX_AGE_SECONDS = 1800;
|
|
85
82
|
const HOOK_GATE_PROGRESS_REMINDER_MS = 2000;
|
|
@@ -523,7 +520,6 @@ const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
|
|
|
523
520
|
dependencies: StageRunnerDependencies;
|
|
524
521
|
repoRoot: string;
|
|
525
522
|
gateBlocked: boolean;
|
|
526
|
-
evidenceWasStagedAtStart: boolean;
|
|
527
523
|
}): boolean => {
|
|
528
524
|
const evidenceAbsolutePath = join(params.repoRoot, EVIDENCE_FILE_PATH);
|
|
529
525
|
if (!existsSync(evidenceAbsolutePath)) {
|
|
@@ -547,20 +543,6 @@ const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
|
|
|
547
543
|
params.dependencies.restorePathFromHead(params.repoRoot, EVIDENCE_FILE_PATH);
|
|
548
544
|
return false;
|
|
549
545
|
}
|
|
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} restored because it was not staged before PRE_COMMIT. ` +
|
|
558
|
-
`Force previous behavior: PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE=1\n`
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
params.dependencies.restorePathFromHead(params.repoRoot, EVIDENCE_FILE_PATH);
|
|
562
|
-
return false;
|
|
563
|
-
}
|
|
564
546
|
try {
|
|
565
547
|
params.dependencies.stagePath(params.repoRoot, EVIDENCE_FILE_PATH);
|
|
566
548
|
return false;
|
|
@@ -805,7 +787,6 @@ export async function runPreCommitStage(
|
|
|
805
787
|
const activeDependencies = getDependencies(dependencies);
|
|
806
788
|
const repoRoot = activeDependencies.resolveRepoRoot();
|
|
807
789
|
const manifestSnapshot = captureManifestGuardSnapshot(repoRoot);
|
|
808
|
-
const initiallyStagedPaths = activeDependencies.listStagedIndexPaths(repoRoot);
|
|
809
790
|
activeDependencies.ensureRuntimeArtifactsIgnored(repoRoot);
|
|
810
791
|
if (
|
|
811
792
|
enforceGitAtomicityGate({
|
|
@@ -839,7 +820,6 @@ export async function runPreCommitStage(
|
|
|
839
820
|
dependencies: activeDependencies,
|
|
840
821
|
repoRoot,
|
|
841
822
|
gateBlocked: result.exitCode !== 0,
|
|
842
|
-
evidenceWasStagedAtStart: hasStagedEvidencePath(initiallyStagedPaths),
|
|
843
823
|
})
|
|
844
824
|
) {
|
|
845
825
|
return 1;
|
|
@@ -4,7 +4,7 @@ import { GitService, type IGitService } from '../git/GitService';
|
|
|
4
4
|
import { hasAllowedExtension } from '../git/gitDiffUtils';
|
|
5
5
|
import { runPlatformGate } from '../git/runPlatformGate';
|
|
6
6
|
import { evaluatePlatformGateFindings } from '../git/runPlatformGateEvaluation';
|
|
7
|
-
import { DEFAULT_FACT_FILE_EXTENSIONS } from '../git/runPlatformGateFacts';
|
|
7
|
+
import { DEFAULT_FACT_FILE_EXTENSIONS, type GateScope } from '../git/runPlatformGateFacts';
|
|
8
8
|
import { resolvePolicyForStage, type ResolvedStagePolicy } from '../gate/stagePolicies';
|
|
9
9
|
|
|
10
10
|
export type LifecycleAuditStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
@@ -23,7 +23,7 @@ export type LifecycleAuditResult = {
|
|
|
23
23
|
command: 'pumuki audit';
|
|
24
24
|
repo_root: string;
|
|
25
25
|
stage: LifecycleAuditStage;
|
|
26
|
-
scope: { kind: 'repo' };
|
|
26
|
+
scope: { kind: 'repo' | 'staged'; staged_matching_extensions_count: number };
|
|
27
27
|
audit_mode: 'gate' | 'engine';
|
|
28
28
|
gate_exit_code: number;
|
|
29
29
|
files_scanned: number | null;
|
|
@@ -70,6 +70,28 @@ const countUntrackedMatchingExtensions = (
|
|
|
70
70
|
.filter((path) => hasAllowedExtension(path, extensions)).length;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
const collectStagedMatchingExtensions = (
|
|
74
|
+
git: Pick<IGitService, 'resolveRepoRoot' | 'runGit'>,
|
|
75
|
+
extensions: ReadonlyArray<string>
|
|
76
|
+
): string[] => {
|
|
77
|
+
const repoRoot = git.resolveRepoRoot();
|
|
78
|
+
return git.runGit(['diff', '--cached', '--name-only'], repoRoot)
|
|
79
|
+
.split('\n')
|
|
80
|
+
.map((line) => line.trim())
|
|
81
|
+
.filter((line) => line.length > 0)
|
|
82
|
+
.filter((path) => hasAllowedExtension(path, extensions));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const resolveLifecycleAuditScope = (params: {
|
|
86
|
+
stage: LifecycleAuditStage;
|
|
87
|
+
stagedMatchingExtensions: ReadonlyArray<string>;
|
|
88
|
+
}): GateScope => {
|
|
89
|
+
if (params.stage === 'PRE_WRITE' && params.stagedMatchingExtensions.length > 0) {
|
|
90
|
+
return { kind: 'staged' };
|
|
91
|
+
}
|
|
92
|
+
return { kind: 'repo' };
|
|
93
|
+
};
|
|
94
|
+
|
|
73
95
|
const isFindingBlocking = (finding: SnapshotFinding): boolean => {
|
|
74
96
|
return finding.severity === 'CRITICAL' ||
|
|
75
97
|
finding.severity === 'ERROR' ||
|
|
@@ -179,13 +201,18 @@ export const runLifecycleAudit = async (params: {
|
|
|
179
201
|
);
|
|
180
202
|
const extensions = DEFAULT_FACT_FILE_EXTENSIONS;
|
|
181
203
|
const untrackedMatchingExtensionsCount = countUntrackedMatchingExtensions(git, extensions);
|
|
204
|
+
const stagedMatchingExtensions = collectStagedMatchingExtensions(git, extensions);
|
|
205
|
+
const scope = resolveLifecycleAuditScope({
|
|
206
|
+
stage: params.stage,
|
|
207
|
+
stagedMatchingExtensions,
|
|
208
|
+
});
|
|
182
209
|
|
|
183
210
|
const gateParams =
|
|
184
211
|
params.auditMode === 'engine'
|
|
185
212
|
? {
|
|
186
213
|
policy: resolved.policy,
|
|
187
214
|
policyTrace: resolved.trace,
|
|
188
|
-
scope
|
|
215
|
+
scope,
|
|
189
216
|
silent: true,
|
|
190
217
|
auditMode: 'engine' as const,
|
|
191
218
|
dependencies: {
|
|
@@ -204,7 +231,7 @@ export const runLifecycleAudit = async (params: {
|
|
|
204
231
|
: {
|
|
205
232
|
policy: resolved.policy,
|
|
206
233
|
policyTrace: resolved.trace,
|
|
207
|
-
scope
|
|
234
|
+
scope,
|
|
208
235
|
silent: true,
|
|
209
236
|
auditMode: 'gate' as const,
|
|
210
237
|
};
|
|
@@ -229,7 +256,10 @@ export const runLifecycleAudit = async (params: {
|
|
|
229
256
|
command: 'pumuki audit',
|
|
230
257
|
repo_root: repoRoot,
|
|
231
258
|
stage: params.stage,
|
|
232
|
-
scope: {
|
|
259
|
+
scope: {
|
|
260
|
+
kind: scope.kind === 'staged' ? 'staged' : 'repo',
|
|
261
|
+
staged_matching_extensions_count: stagedMatchingExtensions.length,
|
|
262
|
+
},
|
|
233
263
|
audit_mode: params.auditMode,
|
|
234
264
|
gate_exit_code: gateExitCode,
|
|
235
265
|
files_scanned: filesScanned,
|
|
@@ -21,31 +21,6 @@ type JsonRpcResponse = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
24
|
-
const JSON_RPC_VERSION = '2.0';
|
|
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
|
-
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';
|
|
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
|
-
};
|
|
49
24
|
|
|
50
25
|
const toJsonRpcId = (value: unknown): JsonRpcId => {
|
|
51
26
|
if (typeof value === 'string' || typeof value === 'number' || value === null) {
|
|
@@ -60,7 +35,7 @@ const sendMessage = (message: JsonRpcResponse): void => {
|
|
|
60
35
|
|
|
61
36
|
const sendResult = (id: JsonRpcId, result: unknown): void => {
|
|
62
37
|
sendMessage({
|
|
63
|
-
jsonrpc:
|
|
38
|
+
jsonrpc: '2.0',
|
|
64
39
|
id,
|
|
65
40
|
result,
|
|
66
41
|
});
|
|
@@ -68,7 +43,7 @@ const sendResult = (id: JsonRpcId, result: unknown): void => {
|
|
|
68
43
|
|
|
69
44
|
const sendError = (id: JsonRpcId, code: number, message: string): void => {
|
|
70
45
|
sendMessage({
|
|
71
|
-
jsonrpc:
|
|
46
|
+
jsonrpc: '2.0',
|
|
72
47
|
id,
|
|
73
48
|
error: {
|
|
74
49
|
code,
|
|
@@ -77,10 +52,10 @@ const sendError = (id: JsonRpcId, code: number, message: string): void => {
|
|
|
77
52
|
});
|
|
78
53
|
};
|
|
79
54
|
|
|
80
|
-
const
|
|
55
|
+
const isPortInUse = async (host: string, port: number): Promise<boolean> =>
|
|
81
56
|
await new Promise((resolve) => {
|
|
82
57
|
const socket = new Socket();
|
|
83
|
-
socket.setTimeout(
|
|
58
|
+
socket.setTimeout(600);
|
|
84
59
|
socket.once('connect', () => {
|
|
85
60
|
socket.destroy();
|
|
86
61
|
resolve(true);
|
|
@@ -96,14 +71,13 @@ const canConnectToAddress = async (host: string, port: number): Promise<boolean>
|
|
|
96
71
|
socket.connect(port, host);
|
|
97
72
|
});
|
|
98
73
|
|
|
99
|
-
const
|
|
74
|
+
const findEphemeralPort = async (host: string): Promise<number> =>
|
|
100
75
|
await new Promise((resolve, reject) => {
|
|
101
76
|
const probe = createServer();
|
|
102
77
|
probe.once('error', reject);
|
|
103
|
-
probe.listen(
|
|
78
|
+
probe.listen(0, host, () => {
|
|
104
79
|
const address = probe.address();
|
|
105
|
-
const port =
|
|
106
|
-
address && typeof address === 'object' ? address.port : EPHEMERAL_LISTENER_PORT;
|
|
80
|
+
const port = address && typeof address === 'object' ? address.port : 0;
|
|
107
81
|
probe.close(() => resolve(port));
|
|
108
82
|
});
|
|
109
83
|
});
|
|
@@ -111,62 +85,45 @@ const findAvailableListenerNumber = async (host: string): Promise<number> =>
|
|
|
111
85
|
const fetchJson = async (url: string): Promise<unknown> => {
|
|
112
86
|
const response = await fetch(url);
|
|
113
87
|
const text = await response.text();
|
|
114
|
-
if (text.trim().length ===
|
|
88
|
+
if (text.trim().length === 0) {
|
|
115
89
|
return {};
|
|
116
90
|
}
|
|
117
91
|
return JSON.parse(text) as unknown;
|
|
118
92
|
};
|
|
119
93
|
|
|
120
|
-
const writeDebugHealthProbeFailure = (error: unknown): void => {
|
|
121
|
-
if (process.env.PUMUKI_DEBUG !== '1') {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
126
|
-
process.stderr.write(`[pumuki-mcp-evidence-stdio] health probe fallback: ${message}\n`);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
94
|
const startOrReuseEvidenceHttp = async (): Promise<{
|
|
130
95
|
host: string;
|
|
131
96
|
port: number;
|
|
132
97
|
route: string;
|
|
133
98
|
}> => {
|
|
134
|
-
const host =
|
|
135
|
-
const route =
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
const preferredListener = parsedListener;
|
|
141
|
-
const requestedListener =
|
|
142
|
-
preferredListener > EPHEMERAL_LISTENER_PORT
|
|
143
|
-
? preferredListener
|
|
144
|
-
: await findAvailableListenerNumber(host);
|
|
145
|
-
const healthUrl = `http://${host}:${requestedListener}/health`;
|
|
99
|
+
const host = process.env.PUMUKI_EVIDENCE_HOST ?? '127.0.0.1';
|
|
100
|
+
const route = process.env.PUMUKI_EVIDENCE_ROUTE ?? '/ai-evidence';
|
|
101
|
+
const parsedPort = Number.parseInt(process.env.PUMUKI_EVIDENCE_PORT ?? '', 10);
|
|
102
|
+
const preferredPort = Number.isFinite(parsedPort) ? parsedPort : 7341;
|
|
103
|
+
const requestedPort = preferredPort > 0 ? preferredPort : await findEphemeralPort(host);
|
|
104
|
+
const healthUrl = `http://${host}:${requestedPort}/health`;
|
|
146
105
|
|
|
147
106
|
try {
|
|
148
107
|
const health = (await fetchJson(healthUrl)) as { status?: string };
|
|
149
108
|
if (health.status === 'ok') {
|
|
150
|
-
return { host, port:
|
|
109
|
+
return { host, port: requestedPort, route };
|
|
151
110
|
}
|
|
152
|
-
} catch
|
|
153
|
-
|
|
111
|
+
} catch {
|
|
112
|
+
// ignored
|
|
154
113
|
}
|
|
155
114
|
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
? await findAvailableListenerNumber(host)
|
|
159
|
-
: requestedListener;
|
|
115
|
+
const portInUse = await isPortInUse(host, requestedPort);
|
|
116
|
+
const resolvedPort = portInUse ? await findEphemeralPort(host) : requestedPort;
|
|
160
117
|
startEvidenceContextServer({
|
|
161
118
|
host,
|
|
162
|
-
port:
|
|
119
|
+
port: resolvedPort,
|
|
163
120
|
route,
|
|
164
121
|
repoRoot: process.cwd(),
|
|
165
122
|
});
|
|
166
123
|
|
|
167
124
|
return {
|
|
168
125
|
host,
|
|
169
|
-
port:
|
|
126
|
+
port: resolvedPort,
|
|
170
127
|
route,
|
|
171
128
|
};
|
|
172
129
|
};
|
|
@@ -234,8 +191,8 @@ const run = async (): Promise<void> => {
|
|
|
234
191
|
] as const;
|
|
235
192
|
|
|
236
193
|
const handleRequest = async (request: JsonRpcRequest): Promise<void> => {
|
|
237
|
-
if (request.jsonrpc !==
|
|
238
|
-
sendError(toJsonRpcId(request.id),
|
|
194
|
+
if (request.jsonrpc !== '2.0') {
|
|
195
|
+
sendError(toJsonRpcId(request.id), -32600, 'Invalid JSON-RPC version.');
|
|
239
196
|
return;
|
|
240
197
|
}
|
|
241
198
|
|
|
@@ -251,7 +208,7 @@ const run = async (): Promise<void> => {
|
|
|
251
208
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
252
209
|
serverInfo: {
|
|
253
210
|
name: 'pumuki-evidence-stdio',
|
|
254
|
-
version:
|
|
211
|
+
version: '1.0.0',
|
|
255
212
|
},
|
|
256
213
|
capabilities: {
|
|
257
214
|
tools: {
|
|
@@ -292,7 +249,7 @@ const run = async (): Promise<void> => {
|
|
|
292
249
|
const name = typeof params.name === 'string' ? params.name : '';
|
|
293
250
|
const tool = toolsCatalog.find((entry) => entry.name === name);
|
|
294
251
|
if (!tool) {
|
|
295
|
-
sendError(id,
|
|
252
|
+
sendError(id, -32602, `Unknown tool: ${name}`);
|
|
296
253
|
return;
|
|
297
254
|
}
|
|
298
255
|
const payload = await fetchJson(`${baseUrl}${tool.path}`);
|
|
@@ -327,7 +284,7 @@ const run = async (): Promise<void> => {
|
|
|
327
284
|
const uri = typeof params.uri === 'string' ? params.uri : '';
|
|
328
285
|
const resource = resourcesCatalog.find((entry) => entry.uri === uri);
|
|
329
286
|
if (!resource) {
|
|
330
|
-
sendError(id,
|
|
287
|
+
sendError(id, -32602, `Unknown resource URI: ${uri}`);
|
|
331
288
|
return;
|
|
332
289
|
}
|
|
333
290
|
const payload = await fetchJson(`${baseUrl}${resource.path}`);
|
|
@@ -343,31 +300,31 @@ const run = async (): Promise<void> => {
|
|
|
343
300
|
return;
|
|
344
301
|
}
|
|
345
302
|
|
|
346
|
-
sendError(id,
|
|
303
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
347
304
|
};
|
|
348
305
|
|
|
349
306
|
const processBuffer = (): void => {
|
|
350
307
|
while (true) {
|
|
351
308
|
const lineEnd = textBuffer.indexOf('\n');
|
|
352
|
-
if (lineEnd ===
|
|
309
|
+
if (lineEnd === -1) {
|
|
353
310
|
return;
|
|
354
311
|
}
|
|
355
312
|
const rawLine = textBuffer.slice(0, lineEnd).trim();
|
|
356
|
-
textBuffer = textBuffer.slice(lineEnd +
|
|
357
|
-
if (rawLine.length ===
|
|
313
|
+
textBuffer = textBuffer.slice(lineEnd + 1);
|
|
314
|
+
if (rawLine.length === 0) {
|
|
358
315
|
continue;
|
|
359
316
|
}
|
|
360
317
|
let payload: JsonRpcRequest;
|
|
361
318
|
try {
|
|
362
319
|
payload = JSON.parse(rawLine) as JsonRpcRequest;
|
|
363
320
|
} catch {
|
|
364
|
-
sendError(null,
|
|
321
|
+
sendError(null, -32700, 'Parse error');
|
|
365
322
|
continue;
|
|
366
323
|
}
|
|
367
324
|
void handleRequest(payload).catch((error) => {
|
|
368
325
|
const id = toJsonRpcId(payload.id);
|
|
369
326
|
const message = error instanceof Error ? error.message : 'Internal error';
|
|
370
|
-
sendError(id,
|
|
327
|
+
sendError(id, -32603, message);
|
|
371
328
|
});
|
|
372
329
|
}
|
|
373
330
|
};
|
|
@@ -381,5 +338,5 @@ const run = async (): Promise<void> => {
|
|
|
381
338
|
void run().catch((error) => {
|
|
382
339
|
const message = error instanceof Error ? error.message : 'Unknown MCP stdio bridge error';
|
|
383
340
|
process.stderr.write(`[pumuki-mcp-evidence-stdio] ${message}\n`);
|
|
384
|
-
process.exit(
|
|
341
|
+
process.exit(1);
|
|
385
342
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.171",
|
|
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": {
|