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.
Files changed (76) hide show
  1. package/README.md +3 -1
  2. package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
  3. package/bin/pumuki-mcp-evidence-stdio.js +5 -0
  4. package/core/gate/conditionMatches.ts +1 -21
  5. package/core/gate/evaluateGate.js +5 -0
  6. package/core/gate/evaluateRules.js +5 -0
  7. package/core/gate/evaluateRules.ts +1 -24
  8. package/core/gate/scopeMatcher.ts +84 -0
  9. package/docs/EXECUTION_BOARD.md +749 -376
  10. package/docs/MCP_SERVERS.md +41 -2
  11. package/docs/README.md +6 -2
  12. package/docs/REFRACTOR_PROGRESS.md +374 -6
  13. package/docs/validation/README.md +11 -1
  14. package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
  15. package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
  16. package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
  17. package/integrations/config/skillsCompliance.ts +212 -0
  18. package/integrations/evidence/integrity.ts +352 -0
  19. package/integrations/evidence/rulesCoverage.ts +94 -0
  20. package/integrations/evidence/schema.test.ts +16 -0
  21. package/integrations/evidence/schema.ts +41 -0
  22. package/integrations/evidence/writeEvidence.test.ts +68 -0
  23. package/integrations/evidence/writeEvidence.ts +23 -2
  24. package/integrations/gate/evaluateAiGate.ts +382 -15
  25. package/integrations/gate/stagePolicies.ts +70 -15
  26. package/integrations/gate/waivers.ts +209 -0
  27. package/integrations/git/findingTraceability.ts +3 -23
  28. package/integrations/git/index.js +5 -0
  29. package/integrations/git/runCliCommand.ts +16 -0
  30. package/integrations/git/runPlatformGate.ts +53 -1
  31. package/integrations/git/runPlatformGateEvaluation.ts +13 -0
  32. package/integrations/git/stageRunners.ts +168 -5
  33. package/integrations/lifecycle/adapter.templates.json +72 -5
  34. package/integrations/lifecycle/adapter.ts +78 -4
  35. package/integrations/lifecycle/cli.ts +384 -14
  36. package/integrations/lifecycle/doctor.ts +534 -0
  37. package/integrations/lifecycle/hookBlock.ts +2 -1
  38. package/integrations/lifecycle/index.js +5 -0
  39. package/integrations/lifecycle/install.ts +115 -3
  40. package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
  41. package/integrations/lifecycle/preWriteAutomation.ts +142 -0
  42. package/integrations/mcp/aiGateCheck.ts +6 -0
  43. package/integrations/mcp/aiGateReceipt.ts +188 -0
  44. package/integrations/mcp/enterpriseServer.ts +14 -1
  45. package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
  46. package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
  47. package/integrations/mcp/index.js +5 -0
  48. package/integrations/sdd/index.js +5 -0
  49. package/integrations/sdd/index.ts +2 -0
  50. package/integrations/sdd/policy.ts +191 -2
  51. package/integrations/sdd/sessionStore.ts +139 -19
  52. package/integrations/sdd/syncDocs.ts +180 -0
  53. package/integrations/sdd/types.ts +4 -1
  54. package/integrations/telemetry/structuredTelemetry.ts +197 -0
  55. package/package.json +27 -8
  56. package/scripts/build-p9-validation-manifests.ts +53 -0
  57. package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
  58. package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
  59. package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
  60. package/scripts/check-p9-ruralgo-install-health.ts +288 -0
  61. package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
  62. package/scripts/check-package-manifest.ts +49 -0
  63. package/scripts/check-tracking-single-active.sh +40 -0
  64. package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
  65. package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
  66. package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
  67. package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
  68. package/scripts/manage-library.sh +1 -1
  69. package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
  70. package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
  71. package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
  72. package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
  73. package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
  74. package/scripts/p9-validation-manifests-lib.ts +366 -0
  75. package/scripts/package-manifest-lib.ts +9 -0
  76. 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 { getCurrentPumukiVersion } from './packageInfo';
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: params?.npm ?? new LifecycleNpmService(),
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
- npm.runNpm(
175
- ['install', '--save-dev', '--save-exact', `${OPENSPEC_NPM_PACKAGE_NAME}@latest`],
176
- params.repoRoot
177
- );
178
- packageInstalled = true;
179
- actions.push(`npm-install:${OPENSPEC_NPM_PACKAGE_NAME}@latest`);
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: execution.result,
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': {