pumuki 6.3.26 → 6.3.28
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 +3 -1
- package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
- package/bin/pumuki-mcp-evidence-stdio.js +5 -0
- package/core/gate/conditionMatches.ts +1 -21
- package/core/gate/evaluateGate.js +5 -0
- package/core/gate/evaluateRules.js +5 -0
- package/core/gate/evaluateRules.ts +1 -24
- package/core/gate/scopeMatcher.ts +84 -0
- package/docs/EXECUTION_BOARD.md +749 -376
- package/docs/MCP_SERVERS.md +41 -2
- package/docs/README.md +6 -2
- package/docs/REFRACTOR_PROGRESS.md +374 -6
- package/docs/validation/README.md +11 -1
- package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
- package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
- package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
- package/integrations/config/skillsCompliance.ts +212 -0
- package/integrations/evidence/integrity.ts +352 -0
- package/integrations/evidence/rulesCoverage.ts +94 -0
- package/integrations/evidence/schema.test.ts +16 -0
- package/integrations/evidence/schema.ts +41 -0
- package/integrations/evidence/writeEvidence.test.ts +68 -0
- package/integrations/evidence/writeEvidence.ts +23 -2
- package/integrations/gate/evaluateAiGate.ts +382 -15
- package/integrations/gate/stagePolicies.ts +70 -15
- package/integrations/gate/waivers.ts +209 -0
- package/integrations/git/findingTraceability.ts +3 -23
- package/integrations/git/index.js +5 -0
- package/integrations/git/runCliCommand.ts +16 -0
- package/integrations/git/runPlatformGate.ts +53 -1
- package/integrations/git/runPlatformGateEvaluation.ts +13 -0
- package/integrations/git/stageRunners.ts +168 -5
- package/integrations/lifecycle/adapter.templates.json +72 -5
- package/integrations/lifecycle/adapter.ts +78 -4
- package/integrations/lifecycle/cli.ts +384 -14
- package/integrations/lifecycle/doctor.ts +534 -0
- package/integrations/lifecycle/hookBlock.ts +2 -1
- package/integrations/lifecycle/index.js +5 -0
- package/integrations/lifecycle/install.ts +115 -3
- package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
- package/integrations/lifecycle/preWriteAutomation.ts +142 -0
- package/integrations/mcp/aiGateCheck.ts +6 -0
- package/integrations/mcp/aiGateReceipt.ts +188 -0
- package/integrations/mcp/enterpriseServer.ts +14 -1
- package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
- package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
- package/integrations/mcp/index.js +5 -0
- package/integrations/sdd/index.js +5 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/policy.ts +191 -2
- package/integrations/sdd/sessionStore.ts +139 -19
- package/integrations/sdd/syncDocs.ts +180 -0
- package/integrations/sdd/types.ts +4 -1
- package/integrations/telemetry/structuredTelemetry.ts +197 -0
- package/package.json +27 -8
- package/scripts/build-p9-validation-manifests.ts +53 -0
- package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
- package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
- package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
- package/scripts/check-p9-ruralgo-install-health.ts +288 -0
- package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
- package/scripts/check-package-manifest.ts +49 -0
- package/scripts/check-tracking-single-active.sh +40 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
- package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
- package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
- package/scripts/manage-library.sh +1 -1
- package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
- package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
- package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
- package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
- package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
- package/scripts/p9-validation-manifests-lib.ts +366 -0
- package/scripts/package-manifest-lib.ts +9 -0
- package/skills.lock.json +1 -1
|
@@ -1,22 +1,38 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { installPumukiHooks } from './hookManager';
|
|
4
4
|
import { LifecycleGitService, type ILifecycleGitService } from './gitService';
|
|
5
5
|
import { doctorHasBlockingIssues, runLifecycleDoctor } from './doctor';
|
|
6
6
|
import { runOpenSpecBootstrap, type OpenSpecBootstrapResult } from './openSpecBootstrap';
|
|
7
7
|
import { LifecycleNpmService, type ILifecycleNpmService } from './npmService';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveCurrentPumukiDependency } from './consumerPackage';
|
|
9
|
+
import { getCurrentPumukiPackageName, getCurrentPumukiVersion } from './packageInfo';
|
|
9
10
|
import { generateEvidence } from '../evidence/generateEvidence';
|
|
10
11
|
import { readEvidence } from '../evidence/readEvidence';
|
|
11
12
|
import { captureRepoState } from '../evidence/repoState';
|
|
12
13
|
import { createEmptyEvaluationMetrics } from '../evidence/evaluationMetrics';
|
|
13
14
|
import { readOpenSpecManagedArtifacts, writeLifecycleState } from './state';
|
|
14
15
|
|
|
16
|
+
export type LifecycleConsumerPackageBootstrapResult = {
|
|
17
|
+
packageInstalled: boolean;
|
|
18
|
+
dependencySource?: 'dependencies' | 'devDependencies';
|
|
19
|
+
targetSpec?: string;
|
|
20
|
+
skippedReason?:
|
|
21
|
+
| 'NO_PACKAGE_JSON'
|
|
22
|
+
| 'PACKAGE_JSON_INVALID'
|
|
23
|
+
| 'SELF_PACKAGE_REPO'
|
|
24
|
+
| 'ALREADY_DECLARED'
|
|
25
|
+
| 'NPM_INSTALL_FAILED'
|
|
26
|
+
| 'ENGINE_MISMATCH';
|
|
27
|
+
skippedDetails?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
15
30
|
export type LifecycleInstallResult = {
|
|
16
31
|
repoRoot: string;
|
|
17
32
|
version: string;
|
|
18
33
|
changedHooks: ReadonlyArray<string>;
|
|
19
34
|
openSpecBootstrap?: OpenSpecBootstrapResult;
|
|
35
|
+
consumerPackageBootstrap: LifecycleConsumerPackageBootstrapResult;
|
|
20
36
|
};
|
|
21
37
|
|
|
22
38
|
const shouldBootstrapEvidence = (repoRoot: string): boolean =>
|
|
@@ -37,6 +53,95 @@ const writeBootstrapEvidence = (repoRoot: string): void => {
|
|
|
37
53
|
});
|
|
38
54
|
};
|
|
39
55
|
|
|
56
|
+
const readConsumerPackageName = (
|
|
57
|
+
repoRoot: string
|
|
58
|
+
): {
|
|
59
|
+
name?: string;
|
|
60
|
+
parseError?: string;
|
|
61
|
+
} => {
|
|
62
|
+
const packageJsonPath = join(repoRoot, 'package.json');
|
|
63
|
+
if (!existsSync(packageJsonPath)) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(packageJsonPath, 'utf8');
|
|
68
|
+
const parsed = JSON.parse(raw) as { name?: unknown };
|
|
69
|
+
const name = typeof parsed.name === 'string' ? parsed.name.trim() : undefined;
|
|
70
|
+
return {
|
|
71
|
+
name: name && name.length > 0 ? name : undefined,
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
parseError: error instanceof Error ? error.message : 'Invalid package.json',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const runConsumerPackageBootstrap = (params: {
|
|
81
|
+
repoRoot: string;
|
|
82
|
+
npm: ILifecycleNpmService;
|
|
83
|
+
openSpecBootstrap?: OpenSpecBootstrapResult;
|
|
84
|
+
}): LifecycleConsumerPackageBootstrapResult => {
|
|
85
|
+
if (params.openSpecBootstrap?.skippedReason === 'ENGINE_MISMATCH') {
|
|
86
|
+
return {
|
|
87
|
+
packageInstalled: false,
|
|
88
|
+
skippedReason: 'ENGINE_MISMATCH',
|
|
89
|
+
skippedDetails: params.openSpecBootstrap.skippedDetails,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const packageJsonPath = join(params.repoRoot, 'package.json');
|
|
94
|
+
if (!existsSync(packageJsonPath)) {
|
|
95
|
+
return {
|
|
96
|
+
packageInstalled: false,
|
|
97
|
+
skippedReason: 'NO_PACKAGE_JSON',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const packageInfo = readConsumerPackageName(params.repoRoot);
|
|
102
|
+
if (packageInfo.parseError) {
|
|
103
|
+
return {
|
|
104
|
+
packageInstalled: false,
|
|
105
|
+
skippedReason: 'PACKAGE_JSON_INVALID',
|
|
106
|
+
skippedDetails: packageInfo.parseError,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const packageName = getCurrentPumukiPackageName();
|
|
111
|
+
if (packageInfo.name === packageName) {
|
|
112
|
+
return {
|
|
113
|
+
packageInstalled: false,
|
|
114
|
+
skippedReason: 'SELF_PACKAGE_REPO',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const declaredDependency = resolveCurrentPumukiDependency(params.repoRoot);
|
|
119
|
+
if (declaredDependency.source !== 'none') {
|
|
120
|
+
return {
|
|
121
|
+
packageInstalled: false,
|
|
122
|
+
dependencySource: declaredDependency.source,
|
|
123
|
+
targetSpec: declaredDependency.spec,
|
|
124
|
+
skippedReason: 'ALREADY_DECLARED',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const targetSpec = `${packageName}@latest`;
|
|
129
|
+
try {
|
|
130
|
+
params.npm.runNpm(['install', '--save-dev', '--save-exact', targetSpec], params.repoRoot);
|
|
131
|
+
return {
|
|
132
|
+
packageInstalled: true,
|
|
133
|
+
dependencySource: 'devDependencies',
|
|
134
|
+
targetSpec,
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return {
|
|
138
|
+
packageInstalled: false,
|
|
139
|
+
skippedReason: 'NPM_INSTALL_FAILED',
|
|
140
|
+
skippedDetails: error instanceof Error ? error.message : 'npm install failed',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
40
145
|
export const runLifecycleInstall = (params?: {
|
|
41
146
|
cwd?: string;
|
|
42
147
|
git?: ILifecycleGitService;
|
|
@@ -59,13 +164,19 @@ export const runLifecycleInstall = (params?: {
|
|
|
59
164
|
|
|
60
165
|
const shouldBootstrapOpenSpec =
|
|
61
166
|
params?.bootstrapOpenSpec ?? process.env.PUMUKI_SKIP_OPENSPEC_BOOTSTRAP !== '1';
|
|
167
|
+
const npm = params?.npm ?? new LifecycleNpmService();
|
|
62
168
|
|
|
63
169
|
const openSpecBootstrap = shouldBootstrapOpenSpec
|
|
64
170
|
? runOpenSpecBootstrap({
|
|
65
171
|
repoRoot: report.repoRoot,
|
|
66
|
-
npm
|
|
172
|
+
npm,
|
|
67
173
|
})
|
|
68
174
|
: undefined;
|
|
175
|
+
const consumerPackageBootstrap = runConsumerPackageBootstrap({
|
|
176
|
+
repoRoot: report.repoRoot,
|
|
177
|
+
npm,
|
|
178
|
+
openSpecBootstrap,
|
|
179
|
+
});
|
|
69
180
|
|
|
70
181
|
const hookResult = installPumukiHooks(report.repoRoot);
|
|
71
182
|
const version = getCurrentPumukiVersion();
|
|
@@ -91,5 +202,6 @@ export const runLifecycleInstall = (params?: {
|
|
|
91
202
|
version,
|
|
92
203
|
changedHooks: hookResult.changedHooks,
|
|
93
204
|
openSpecBootstrap,
|
|
205
|
+
consumerPackageBootstrap,
|
|
94
206
|
};
|
|
95
207
|
};
|
|
@@ -14,7 +14,8 @@ export type OpenSpecBootstrapResult = {
|
|
|
14
14
|
projectInitialized: boolean;
|
|
15
15
|
actions: ReadonlyArray<string>;
|
|
16
16
|
managedArtifacts: ReadonlyArray<string>;
|
|
17
|
-
skippedReason?: 'NO_PACKAGE_JSON';
|
|
17
|
+
skippedReason?: 'NO_PACKAGE_JSON' | 'NPM_INSTALL_FAILED' | 'ENGINE_MISMATCH';
|
|
18
|
+
skippedDetails?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
type PackageDependencySource = 'dependencies' | 'devDependencies' | 'none';
|
|
@@ -22,6 +23,10 @@ type PackageDependencySource = 'dependencies' | 'devDependencies' | 'none';
|
|
|
22
23
|
type PackageJson = {
|
|
23
24
|
dependencies?: Record<string, string>;
|
|
24
25
|
devDependencies?: Record<string, string>;
|
|
26
|
+
engines?: {
|
|
27
|
+
node?: string;
|
|
28
|
+
npm?: string;
|
|
29
|
+
};
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
const readPackageJson = (repoRoot: string): PackageJson | undefined => {
|
|
@@ -48,6 +53,43 @@ const resolveDependencySource = (
|
|
|
48
53
|
return 'none';
|
|
49
54
|
};
|
|
50
55
|
|
|
56
|
+
const normalizeSemverToken = (value: string): string => value.trim().replace(/^v/i, '');
|
|
57
|
+
|
|
58
|
+
const isExactSemver = (value: string): boolean => /^\d+\.\d+\.\d+$/.test(normalizeSemverToken(value));
|
|
59
|
+
|
|
60
|
+
const resolveCurrentNpmVersionFromEnv = (): string | undefined => {
|
|
61
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
62
|
+
if (!userAgent) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const match = userAgent.match(/npm\/(\d+\.\d+\.\d+)/);
|
|
66
|
+
return match ? normalizeSemverToken(match[1]) : undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const resolveEngineMismatch = (packageJson: PackageJson | undefined): string | undefined => {
|
|
70
|
+
const requiredNode = packageJson?.engines?.node;
|
|
71
|
+
if (requiredNode && isExactSemver(requiredNode)) {
|
|
72
|
+
const normalizedRequiredNode = normalizeSemverToken(requiredNode);
|
|
73
|
+
const currentNode = normalizeSemverToken(process.versions.node);
|
|
74
|
+
if (currentNode !== normalizedRequiredNode) {
|
|
75
|
+
return `node required=${normalizedRequiredNode} current=${currentNode}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const requiredNpm = packageJson?.engines?.npm;
|
|
80
|
+
if (requiredNpm && isExactSemver(requiredNpm)) {
|
|
81
|
+
const currentNpm = resolveCurrentNpmVersionFromEnv();
|
|
82
|
+
if (currentNpm) {
|
|
83
|
+
const normalizedRequiredNpm = normalizeSemverToken(requiredNpm);
|
|
84
|
+
if (currentNpm !== normalizedRequiredNpm) {
|
|
85
|
+
return `npm required=${normalizedRequiredNpm} current=${currentNpm}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return undefined;
|
|
91
|
+
};
|
|
92
|
+
|
|
51
93
|
const OPENSPEC_LEGACY_NPM_PACKAGE_NAME = 'openspec';
|
|
52
94
|
const OPENSPEC_PROJECT_MD = 'openspec/project.md';
|
|
53
95
|
const OPENSPEC_ARCHIVE_GITKEEP = 'openspec/changes/archive/.gitkeep';
|
|
@@ -163,20 +205,37 @@ export const runOpenSpecBootstrap = (params: {
|
|
|
163
205
|
}): OpenSpecBootstrapResult => {
|
|
164
206
|
const npm = params.npm ?? new LifecycleNpmService();
|
|
165
207
|
const actions: string[] = [];
|
|
208
|
+
const packageJson = readPackageJson(params.repoRoot);
|
|
166
209
|
|
|
167
210
|
const installation = detectOpenSpecInstallation(params.repoRoot);
|
|
168
211
|
const compatibility = evaluateOpenSpecCompatibility(installation);
|
|
169
212
|
let packageInstalled = installation.installed;
|
|
170
213
|
const packageJsonPath = join(params.repoRoot, 'package.json');
|
|
171
214
|
const hasPackageJson = existsSync(packageJsonPath);
|
|
215
|
+
let skippedReason: OpenSpecBootstrapResult['skippedReason'];
|
|
216
|
+
let skippedDetails: string | undefined;
|
|
172
217
|
|
|
173
218
|
if ((!packageInstalled || !compatibility.compatible) && hasPackageJson) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
219
|
+
const engineMismatch = resolveEngineMismatch(packageJson);
|
|
220
|
+
if (engineMismatch) {
|
|
221
|
+
skippedReason = 'ENGINE_MISMATCH';
|
|
222
|
+
skippedDetails = engineMismatch;
|
|
223
|
+
actions.push('npm-install-skipped:engine-mismatch');
|
|
224
|
+
} else {
|
|
225
|
+
try {
|
|
226
|
+
npm.runNpm(
|
|
227
|
+
['install', '--save-dev', '--save-exact', `${OPENSPEC_NPM_PACKAGE_NAME}@latest`],
|
|
228
|
+
params.repoRoot
|
|
229
|
+
);
|
|
230
|
+
packageInstalled = true;
|
|
231
|
+
actions.push(`npm-install:${OPENSPEC_NPM_PACKAGE_NAME}@latest`);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
skippedReason = 'NPM_INSTALL_FAILED';
|
|
234
|
+
skippedDetails =
|
|
235
|
+
error instanceof Error ? error.message : 'npm install failed during OpenSpec bootstrap';
|
|
236
|
+
actions.push(`npm-install-failed:${OPENSPEC_NPM_PACKAGE_NAME}@latest`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
180
239
|
}
|
|
181
240
|
|
|
182
241
|
const projectInitializedBefore = isOpenSpecProjectInitialized(params.repoRoot);
|
|
@@ -194,6 +253,7 @@ export const runOpenSpecBootstrap = (params: {
|
|
|
194
253
|
projectInitialized: isOpenSpecProjectInitialized(params.repoRoot),
|
|
195
254
|
actions,
|
|
196
255
|
managedArtifacts,
|
|
197
|
-
skippedReason: !packageInstalled && !hasPackageJson ? 'NO_PACKAGE_JSON' : undefined,
|
|
256
|
+
skippedReason: skippedReason ?? (!packageInstalled && !hasPackageJson ? 'NO_PACKAGE_JSON' : undefined),
|
|
257
|
+
skippedDetails,
|
|
198
258
|
};
|
|
199
259
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { evaluateAiGate } from '../gate/evaluateAiGate';
|
|
2
|
+
import { runPlatformGate } from '../git/runPlatformGate';
|
|
3
|
+
import { runEnterpriseAiGateCheck } from '../mcp/aiGateCheck';
|
|
4
|
+
import { writeMcpAiGateReceipt } from '../mcp/aiGateReceipt';
|
|
5
|
+
import { evaluateSddPolicy } from '../sdd';
|
|
6
|
+
|
|
7
|
+
const PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES = new Set<string>([
|
|
8
|
+
'EVIDENCE_MISSING',
|
|
9
|
+
'EVIDENCE_INVALID',
|
|
10
|
+
'EVIDENCE_STALE',
|
|
11
|
+
'EVIDENCE_REPO_ROOT_MISMATCH',
|
|
12
|
+
'EVIDENCE_BRANCH_MISMATCH',
|
|
13
|
+
'EVIDENCE_RULES_COVERAGE_MISSING',
|
|
14
|
+
'EVIDENCE_RULES_COVERAGE_STAGE_MISMATCH',
|
|
15
|
+
'EVIDENCE_RULES_COVERAGE_INCOMPLETE',
|
|
16
|
+
'EVIDENCE_UNSUPPORTED_AUTO_RULES',
|
|
17
|
+
'EVIDENCE_TIMESTAMP_INVALID',
|
|
18
|
+
'EVIDENCE_TIMESTAMP_FUTURE',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES = new Set<string>([
|
|
22
|
+
'MCP_ENTERPRISE_RECEIPT_MISSING',
|
|
23
|
+
'MCP_ENTERPRISE_RECEIPT_INVALID',
|
|
24
|
+
'MCP_ENTERPRISE_RECEIPT_STALE',
|
|
25
|
+
'MCP_ENTERPRISE_RECEIPT_STAGE_MISMATCH',
|
|
26
|
+
'MCP_ENTERPRISE_RECEIPT_REPO_ROOT_MISMATCH',
|
|
27
|
+
'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_INVALID',
|
|
28
|
+
'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_FUTURE',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export type PreWriteAutomationAction = {
|
|
32
|
+
action: 'refresh_evidence' | 'refresh_mcp_receipt';
|
|
33
|
+
status: 'OK' | 'FAILED';
|
|
34
|
+
details: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type PreWriteAutomationTrace = {
|
|
38
|
+
attempted: boolean;
|
|
39
|
+
actions: PreWriteAutomationAction[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const buildPreWriteAutomationTrace = async (params: {
|
|
43
|
+
repoRoot: string;
|
|
44
|
+
sdd: ReturnType<typeof evaluateSddPolicy>;
|
|
45
|
+
aiGate: ReturnType<typeof evaluateAiGate>;
|
|
46
|
+
runPlatformGate: typeof runPlatformGate;
|
|
47
|
+
}): Promise<{
|
|
48
|
+
aiGate: ReturnType<typeof evaluateAiGate>;
|
|
49
|
+
trace: PreWriteAutomationTrace;
|
|
50
|
+
}> => {
|
|
51
|
+
const trace: PreWriteAutomationTrace = {
|
|
52
|
+
attempted: false,
|
|
53
|
+
actions: [],
|
|
54
|
+
};
|
|
55
|
+
if (params.sdd.stage !== 'PRE_WRITE' || !params.sdd.decision.allowed || params.aiGate.allowed) {
|
|
56
|
+
return {
|
|
57
|
+
aiGate: params.aiGate,
|
|
58
|
+
trace,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let aiGate = params.aiGate;
|
|
63
|
+
const hasEvidenceAutoFixableViolation = aiGate.violations.some((violation) =>
|
|
64
|
+
PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES.has(violation.code)
|
|
65
|
+
);
|
|
66
|
+
if (hasEvidenceAutoFixableViolation) {
|
|
67
|
+
trace.attempted = true;
|
|
68
|
+
try {
|
|
69
|
+
const gateExitCode = await params.runPlatformGate({
|
|
70
|
+
policy: {
|
|
71
|
+
stage: 'PRE_COMMIT',
|
|
72
|
+
blockOnOrAbove: 'ERROR',
|
|
73
|
+
warnOnOrAbove: 'WARN',
|
|
74
|
+
},
|
|
75
|
+
scope: {
|
|
76
|
+
kind: 'workingTree',
|
|
77
|
+
},
|
|
78
|
+
auditMode: 'gate',
|
|
79
|
+
dependencies: {
|
|
80
|
+
printGateFindings: () => {},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
trace.actions.push({
|
|
84
|
+
action: 'refresh_evidence',
|
|
85
|
+
status: 'OK',
|
|
86
|
+
details: `runPlatformGate exit_code=${gateExitCode}`,
|
|
87
|
+
});
|
|
88
|
+
aiGate = runEnterpriseAiGateCheck({
|
|
89
|
+
repoRoot: params.repoRoot,
|
|
90
|
+
stage: 'PRE_WRITE',
|
|
91
|
+
requireMcpReceipt: true,
|
|
92
|
+
}).result;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
trace.actions.push({
|
|
95
|
+
action: 'refresh_evidence',
|
|
96
|
+
status: 'FAILED',
|
|
97
|
+
details: error instanceof Error ? error.message : 'Unknown evidence refresh error',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hasReceiptAutoFixableViolation = aiGate.violations.some((violation) =>
|
|
103
|
+
PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES.has(violation.code)
|
|
104
|
+
);
|
|
105
|
+
if (hasReceiptAutoFixableViolation) {
|
|
106
|
+
trace.attempted = true;
|
|
107
|
+
try {
|
|
108
|
+
const aiGateWithoutReceiptRequirement = evaluateAiGate({
|
|
109
|
+
repoRoot: params.repoRoot,
|
|
110
|
+
stage: 'PRE_WRITE',
|
|
111
|
+
requireMcpReceipt: false,
|
|
112
|
+
});
|
|
113
|
+
const receiptWrite = writeMcpAiGateReceipt({
|
|
114
|
+
repoRoot: params.repoRoot,
|
|
115
|
+
stage: 'PRE_WRITE',
|
|
116
|
+
status: aiGateWithoutReceiptRequirement.status,
|
|
117
|
+
allowed: aiGateWithoutReceiptRequirement.allowed,
|
|
118
|
+
});
|
|
119
|
+
trace.actions.push({
|
|
120
|
+
action: 'refresh_mcp_receipt',
|
|
121
|
+
status: 'OK',
|
|
122
|
+
details: `receipt=${receiptWrite.path}`,
|
|
123
|
+
});
|
|
124
|
+
aiGate = runEnterpriseAiGateCheck({
|
|
125
|
+
repoRoot: params.repoRoot,
|
|
126
|
+
stage: 'PRE_WRITE',
|
|
127
|
+
requireMcpReceipt: true,
|
|
128
|
+
}).result;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
trace.actions.push({
|
|
131
|
+
action: 'refresh_mcp_receipt',
|
|
132
|
+
status: 'FAILED',
|
|
133
|
+
details: error instanceof Error ? error.message : 'Unknown MCP receipt refresh error',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
aiGate,
|
|
140
|
+
trace,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
@@ -12,6 +12,8 @@ export type EnterpriseAiGateCheckResult = {
|
|
|
12
12
|
policy: ReturnType<typeof evaluateAiGate>['policy'];
|
|
13
13
|
violations: ReturnType<typeof evaluateAiGate>['violations'];
|
|
14
14
|
evidence: ReturnType<typeof evaluateAiGate>['evidence'];
|
|
15
|
+
mcp_receipt: ReturnType<typeof evaluateAiGate>['mcp_receipt'];
|
|
16
|
+
waivers: ReturnType<typeof evaluateAiGate>['waivers'];
|
|
15
17
|
repo_state: ReturnType<typeof evaluateAiGate>['repo_state'];
|
|
16
18
|
};
|
|
17
19
|
};
|
|
@@ -19,10 +21,12 @@ export type EnterpriseAiGateCheckResult = {
|
|
|
19
21
|
export const runEnterpriseAiGateCheck = (params: {
|
|
20
22
|
repoRoot: string;
|
|
21
23
|
stage: AiGateStage;
|
|
24
|
+
requireMcpReceipt?: boolean;
|
|
22
25
|
}): EnterpriseAiGateCheckResult => {
|
|
23
26
|
const evaluation = evaluateAiGate({
|
|
24
27
|
repoRoot: params.repoRoot,
|
|
25
28
|
stage: params.stage,
|
|
29
|
+
requireMcpReceipt: params.requireMcpReceipt ?? false,
|
|
26
30
|
});
|
|
27
31
|
|
|
28
32
|
return {
|
|
@@ -37,6 +41,8 @@ export const runEnterpriseAiGateCheck = (params: {
|
|
|
37
41
|
policy: evaluation.policy,
|
|
38
42
|
violations: evaluation.violations,
|
|
39
43
|
evidence: evaluation.evidence,
|
|
44
|
+
mcp_receipt: evaluation.mcp_receipt,
|
|
45
|
+
waivers: evaluation.waivers,
|
|
40
46
|
repo_state: evaluation.repo_state,
|
|
41
47
|
},
|
|
42
48
|
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type McpAiGateStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
5
|
+
|
|
6
|
+
export type McpAiGateReceipt = {
|
|
7
|
+
version: '1';
|
|
8
|
+
source: 'pumuki-enterprise-mcp';
|
|
9
|
+
tool: 'ai_gate_check';
|
|
10
|
+
repo_root: string;
|
|
11
|
+
stage: McpAiGateStage;
|
|
12
|
+
status: 'ALLOWED' | 'BLOCKED';
|
|
13
|
+
allowed: boolean;
|
|
14
|
+
issued_at: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type McpAiGateReceiptReadResult =
|
|
18
|
+
| {
|
|
19
|
+
kind: 'missing';
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
kind: 'invalid';
|
|
24
|
+
path: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
kind: 'valid';
|
|
29
|
+
path: string;
|
|
30
|
+
receipt: McpAiGateReceipt;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const RECEIPT_RELATIVE_PATH = '.pumuki/artifacts/mcp-ai-gate-receipt.json';
|
|
34
|
+
|
|
35
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
36
|
+
typeof value === 'object' && value !== null;
|
|
37
|
+
|
|
38
|
+
const isStage = (value: unknown): value is McpAiGateStage =>
|
|
39
|
+
value === 'PRE_WRITE' ||
|
|
40
|
+
value === 'PRE_COMMIT' ||
|
|
41
|
+
value === 'PRE_PUSH' ||
|
|
42
|
+
value === 'CI';
|
|
43
|
+
|
|
44
|
+
const isStatus = (value: unknown): value is 'ALLOWED' | 'BLOCKED' =>
|
|
45
|
+
value === 'ALLOWED' || value === 'BLOCKED';
|
|
46
|
+
|
|
47
|
+
const isValidIsoTimestamp = (value: string): boolean => Number.isFinite(Date.parse(value));
|
|
48
|
+
|
|
49
|
+
export const resolveMcpAiGateReceiptPath = (repoRoot: string): string =>
|
|
50
|
+
resolve(repoRoot, RECEIPT_RELATIVE_PATH);
|
|
51
|
+
|
|
52
|
+
export const writeMcpAiGateReceipt = (params: {
|
|
53
|
+
repoRoot: string;
|
|
54
|
+
stage: McpAiGateStage;
|
|
55
|
+
status: 'ALLOWED' | 'BLOCKED';
|
|
56
|
+
allowed: boolean;
|
|
57
|
+
issuedAt?: string;
|
|
58
|
+
}): { path: string; receipt: McpAiGateReceipt } => {
|
|
59
|
+
const issuedAt = params.issuedAt ?? new Date().toISOString();
|
|
60
|
+
const path = resolveMcpAiGateReceiptPath(params.repoRoot);
|
|
61
|
+
const receipt: McpAiGateReceipt = {
|
|
62
|
+
version: '1',
|
|
63
|
+
source: 'pumuki-enterprise-mcp',
|
|
64
|
+
tool: 'ai_gate_check',
|
|
65
|
+
repo_root: params.repoRoot,
|
|
66
|
+
stage: params.stage,
|
|
67
|
+
status: params.status,
|
|
68
|
+
allowed: params.allowed,
|
|
69
|
+
issued_at: issuedAt,
|
|
70
|
+
};
|
|
71
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
72
|
+
writeFileSync(path, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
|
|
73
|
+
return {
|
|
74
|
+
path,
|
|
75
|
+
receipt,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const parseReceipt = (value: unknown): { ok: true; receipt: McpAiGateReceipt } | { ok: false; reason: string } => {
|
|
80
|
+
if (!isRecord(value)) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
reason: 'Receipt payload must be an object.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (value.version !== '1') {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
reason: 'Receipt version must be "1".',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (value.source !== 'pumuki-enterprise-mcp') {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: 'Receipt source must be "pumuki-enterprise-mcp".',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (value.tool !== 'ai_gate_check') {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
reason: 'Receipt tool must be "ai_gate_check".',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (typeof value.repo_root !== 'string' || value.repo_root.trim().length === 0) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: 'Receipt repo_root must be a non-empty string.',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (!isStage(value.stage)) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: 'Receipt stage must be PRE_WRITE, PRE_COMMIT, PRE_PUSH or CI.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (!isStatus(value.status)) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: 'Receipt status must be ALLOWED or BLOCKED.',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (typeof value.allowed !== 'boolean') {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
reason: 'Receipt allowed must be boolean.',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (typeof value.issued_at !== 'string' || !isValidIsoTimestamp(value.issued_at)) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason: 'Receipt issued_at must be a valid ISO timestamp.',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if ((value.status === 'ALLOWED' && value.allowed !== true) || (value.status === 'BLOCKED' && value.allowed !== false)) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
reason: 'Receipt status and allowed must be coherent.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
receipt: {
|
|
144
|
+
version: value.version,
|
|
145
|
+
source: value.source,
|
|
146
|
+
tool: value.tool,
|
|
147
|
+
repo_root: value.repo_root,
|
|
148
|
+
stage: value.stage,
|
|
149
|
+
status: value.status,
|
|
150
|
+
allowed: value.allowed,
|
|
151
|
+
issued_at: value.issued_at,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const readMcpAiGateReceipt = (repoRoot: string): McpAiGateReceiptReadResult => {
|
|
157
|
+
const path = resolveMcpAiGateReceiptPath(repoRoot);
|
|
158
|
+
if (!existsSync(path)) {
|
|
159
|
+
return {
|
|
160
|
+
kind: 'missing',
|
|
161
|
+
path,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const raw = readFileSync(path, 'utf8');
|
|
167
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
168
|
+
const receipt = parseReceipt(parsed);
|
|
169
|
+
if (!receipt.ok) {
|
|
170
|
+
return {
|
|
171
|
+
kind: 'invalid',
|
|
172
|
+
path,
|
|
173
|
+
reason: receipt.reason,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
kind: 'valid',
|
|
178
|
+
path,
|
|
179
|
+
receipt: receipt.receipt,
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
kind: 'invalid',
|
|
184
|
+
path,
|
|
185
|
+
reason: error instanceof Error ? error.message : 'Unknown receipt parsing error.',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
@@ -9,6 +9,7 @@ import { evaluateSddPolicy, readSddStatus } from '../sdd';
|
|
|
9
9
|
import type { SddStage } from '../sdd';
|
|
10
10
|
import { toStatusPayload } from './evidencePayloads';
|
|
11
11
|
import { runEnterpriseAiGateCheck } from './aiGateCheck';
|
|
12
|
+
import { writeMcpAiGateReceipt } from './aiGateReceipt';
|
|
12
13
|
|
|
13
14
|
export interface EnterpriseServerOptions {
|
|
14
15
|
host?: string;
|
|
@@ -357,12 +358,24 @@ const executeEnterpriseTool = (
|
|
|
357
358
|
repoRoot,
|
|
358
359
|
stage,
|
|
359
360
|
});
|
|
361
|
+
const receiptWrite = writeMcpAiGateReceipt({
|
|
362
|
+
repoRoot,
|
|
363
|
+
stage: execution.result.stage,
|
|
364
|
+
status: execution.result.status,
|
|
365
|
+
allowed: execution.result.allowed,
|
|
366
|
+
});
|
|
360
367
|
return {
|
|
361
368
|
name: toolName,
|
|
362
369
|
success: execution.success,
|
|
363
370
|
dryRun: execution.dryRun,
|
|
364
371
|
executed: execution.executed,
|
|
365
|
-
data:
|
|
372
|
+
data: {
|
|
373
|
+
...execution.result,
|
|
374
|
+
receipt: {
|
|
375
|
+
path: receiptWrite.path,
|
|
376
|
+
issued_at: receiptWrite.receipt.issued_at,
|
|
377
|
+
},
|
|
378
|
+
},
|
|
366
379
|
};
|
|
367
380
|
}
|
|
368
381
|
case 'check_sdd_status': {
|