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
@@ -3,8 +3,26 @@ import { readEvidenceResult } from '../evidence/readEvidence';
3
3
  import { captureRepoState } from '../evidence/repoState';
4
4
  import type { RepoState } from '../evidence/schema';
5
5
  import { resolvePolicyForStage } from './stagePolicies';
6
- import { realpathSync } from 'node:fs';
6
+ import { createHash } from 'node:crypto';
7
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
7
9
  import type { SkillsStage } from '../config/skillsLock';
10
+ import type { ResolvedStagePolicy } from './stagePolicies';
11
+ import {
12
+ readMcpAiGateReceipt,
13
+ resolveMcpAiGateReceiptPath,
14
+ type McpAiGateReceiptReadResult,
15
+ } from '../mcp/aiGateReceipt';
16
+ import {
17
+ resolveEvidenceSigningConfig,
18
+ verifyEvidenceIntegrity,
19
+ type EvidenceIntegrityVerification,
20
+ } from '../evidence/integrity';
21
+ import {
22
+ applyAiGateWaivers,
23
+ type AppliedAiGateWaiver,
24
+ } from './waivers';
25
+ import { emitStructuredTelemetry } from '../telemetry/structuredTelemetry';
8
26
 
9
27
  export type AiGateStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
10
28
 
@@ -23,16 +41,39 @@ export type AiGateCheckResult = {
23
41
  resolved_stage: SkillsStage;
24
42
  block_on_or_above: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
25
43
  warn_on_or_above: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
26
- trace: {
27
- source: 'default' | 'skills.policy' | 'hard-mode';
28
- bundle: string;
29
- hash: string;
30
- };
44
+ trace: ResolvedStagePolicy['trace'];
31
45
  };
32
46
  evidence: {
33
47
  kind: EvidenceReadResult['kind'];
34
48
  max_age_seconds: number;
35
49
  age_seconds: number | null;
50
+ source: 'local_file_ai_evidence';
51
+ path: string;
52
+ digest: string | null;
53
+ generated_at: string | null;
54
+ integrity: {
55
+ status: EvidenceIntegrityVerification['status'];
56
+ code: string | null;
57
+ payload_hash: string | null;
58
+ previous_chain_hash: string | null;
59
+ chain_hash: string | null;
60
+ signature_present: boolean;
61
+ signature_key_id: string | null;
62
+ signature_verified: boolean | null;
63
+ };
64
+ };
65
+ mcp_receipt: {
66
+ required: boolean;
67
+ kind: 'disabled' | McpAiGateReceiptReadResult['kind'];
68
+ path: string;
69
+ max_age_seconds: number | null;
70
+ age_seconds: number | null;
71
+ };
72
+ waivers: {
73
+ path: string;
74
+ status: 'none' | 'applied' | 'invalid';
75
+ invalid_reason: string | null;
76
+ applied: AppliedAiGateWaiver[];
36
77
  };
37
78
  repo_state: RepoState;
38
79
  violations: AiGateViolation[];
@@ -40,16 +81,22 @@ export type AiGateCheckResult = {
40
81
 
41
82
  type AiGateDependencies = {
42
83
  now: () => number;
84
+ env: NodeJS.ProcessEnv;
43
85
  readEvidenceResult: (repoRoot: string) => EvidenceReadResult;
86
+ readMcpAiGateReceipt: (repoRoot: string) => McpAiGateReceiptReadResult;
44
87
  captureRepoState: (repoRoot: string) => RepoState;
45
88
  resolvePolicyForStage: (stage: SkillsStage, repoRoot: string) => ReturnType<typeof resolvePolicyForStage>;
89
+ emitStructuredTelemetry: typeof emitStructuredTelemetry;
46
90
  };
47
91
 
48
92
  const defaultDependencies: AiGateDependencies = {
49
93
  now: () => Date.now(),
94
+ env: process.env,
50
95
  readEvidenceResult,
96
+ readMcpAiGateReceipt,
51
97
  captureRepoState,
52
98
  resolvePolicyForStage,
99
+ emitStructuredTelemetry,
53
100
  };
54
101
 
55
102
  const DEFAULT_MAX_AGE_SECONDS: Readonly<Record<AiGateStage, number>> = {
@@ -203,20 +250,49 @@ const collectPreWriteCoherenceViolations = (params: {
203
250
  return violations;
204
251
  };
205
252
 
253
+ const toIntegritySummary = (verification: EvidenceIntegrityVerification): {
254
+ status: EvidenceIntegrityVerification['status'];
255
+ code: string | null;
256
+ payload_hash: string | null;
257
+ previous_chain_hash: string | null;
258
+ chain_hash: string | null;
259
+ signature_present: boolean;
260
+ signature_key_id: string | null;
261
+ signature_verified: boolean | null;
262
+ } => ({
263
+ status: verification.status,
264
+ code: verification.code,
265
+ payload_hash: verification.payloadHash,
266
+ previous_chain_hash: verification.previousChainHash,
267
+ chain_hash: verification.chainHash,
268
+ signature_present: verification.signature.present,
269
+ signature_key_id: verification.signature.keyId,
270
+ signature_verified: verification.signature.verified,
271
+ });
272
+
206
273
  const collectEvidenceViolations = (
207
274
  result: EvidenceReadResult,
208
275
  repoRoot: string,
209
276
  repoState: RepoState,
210
277
  stage: AiGateStage,
211
278
  nowMs: number,
212
- maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>
213
- ): { violations: AiGateViolation[]; ageSeconds: number | null } => {
279
+ maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>,
280
+ verification: EvidenceIntegrityVerification
281
+ ): {
282
+ violations: AiGateViolation[];
283
+ ageSeconds: number | null;
284
+ integrity: ReturnType<typeof toIntegritySummary>;
285
+ } => {
214
286
  const violations: AiGateViolation[] = [];
215
287
  const maxAgeSeconds = maxAgeSecondsByStage[stage];
216
288
 
217
289
  if (result.kind === 'missing') {
218
290
  violations.push(toErrorViolation('EVIDENCE_MISSING', '.ai_evidence.json is missing.'));
219
- return { violations, ageSeconds: null };
291
+ return {
292
+ violations,
293
+ ageSeconds: null,
294
+ integrity: toIntegritySummary(verification),
295
+ };
220
296
  }
221
297
 
222
298
  if (result.kind === 'invalid') {
@@ -226,13 +302,30 @@ const collectEvidenceViolations = (
226
302
  `.ai_evidence.json is invalid${result.version ? ` (version=${result.version})` : ''}.`
227
303
  )
228
304
  );
229
- return { violations, ageSeconds: null };
305
+ return {
306
+ violations,
307
+ ageSeconds: null,
308
+ integrity: toIntegritySummary(verification),
309
+ };
310
+ }
311
+
312
+ if (!verification.ok) {
313
+ violations.push(
314
+ toErrorViolation(
315
+ verification.code ?? 'EVIDENCE_INTEGRITY_INVALID',
316
+ verification.message ?? 'Evidence integrity verification failed.'
317
+ )
318
+ );
230
319
  }
231
320
 
232
321
  const ageSeconds = toTimestampAgeSeconds(result.evidence.timestamp, nowMs);
233
322
  if (ageSeconds === null) {
234
323
  violations.push(toErrorViolation('EVIDENCE_TIMESTAMP_INVALID', 'Evidence timestamp is invalid.'));
235
- return { violations, ageSeconds: null };
324
+ return {
325
+ violations,
326
+ ageSeconds: null,
327
+ integrity: toIntegritySummary(verification),
328
+ };
236
329
  }
237
330
 
238
331
  if (ageSeconds > maxAgeSeconds) {
@@ -259,7 +352,40 @@ const collectEvidenceViolations = (
259
352
  );
260
353
  }
261
354
 
262
- return { violations, ageSeconds };
355
+ return {
356
+ violations,
357
+ ageSeconds,
358
+ integrity: toIntegritySummary(verification),
359
+ };
360
+ };
361
+
362
+ const resolveEvidenceSourceDetails = (
363
+ repoRoot: string,
364
+ result: EvidenceReadResult
365
+ ): {
366
+ source: 'local_file_ai_evidence';
367
+ path: string;
368
+ digest: string | null;
369
+ generated_at: string | null;
370
+ } => {
371
+ const evidencePath = resolve(repoRoot, '.ai_evidence.json');
372
+ let digest: string | null = null;
373
+
374
+ if (existsSync(evidencePath)) {
375
+ try {
376
+ const buffer = readFileSync(evidencePath);
377
+ digest = createHash('sha256').update(buffer).digest('hex');
378
+ } catch {
379
+ digest = null;
380
+ }
381
+ }
382
+
383
+ return {
384
+ source: 'local_file_ai_evidence',
385
+ path: evidencePath,
386
+ digest,
387
+ generated_at: result.kind === 'valid' ? result.evidence.timestamp : null,
388
+ };
263
389
  };
264
390
 
265
391
  const collectGitflowViolations = (
@@ -288,12 +414,126 @@ const toPolicyStage = (stage: AiGateStage): SkillsStage => {
288
414
  return stage;
289
415
  };
290
416
 
417
+ const collectMcpReceiptViolations = (params: {
418
+ required: boolean;
419
+ stage: AiGateStage;
420
+ repoRoot: string;
421
+ nowMs: number;
422
+ maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>;
423
+ readMcpAiGateReceipt: (repoRoot: string) => McpAiGateReceiptReadResult;
424
+ }): {
425
+ required: boolean;
426
+ kind: 'disabled' | McpAiGateReceiptReadResult['kind'];
427
+ path: string;
428
+ maxAgeSeconds: number | null;
429
+ ageSeconds: number | null;
430
+ violations: AiGateViolation[];
431
+ } => {
432
+ const path = resolveMcpAiGateReceiptPath(params.repoRoot);
433
+ if (!params.required || params.stage !== 'PRE_WRITE') {
434
+ return {
435
+ required: params.required,
436
+ kind: 'disabled',
437
+ path,
438
+ maxAgeSeconds: null,
439
+ ageSeconds: null,
440
+ violations: [],
441
+ };
442
+ }
443
+
444
+ const maxAgeSeconds = params.maxAgeSecondsByStage[params.stage];
445
+ const receiptRead = params.readMcpAiGateReceipt(params.repoRoot);
446
+ if (receiptRead.kind === 'missing') {
447
+ return {
448
+ required: true,
449
+ kind: 'missing',
450
+ path: receiptRead.path,
451
+ maxAgeSeconds,
452
+ ageSeconds: null,
453
+ violations: [
454
+ toErrorViolation(
455
+ 'MCP_ENTERPRISE_RECEIPT_MISSING',
456
+ 'MCP receipt is missing. Call tool ai_gate_check in pumuki-enterprise MCP before PRE_WRITE.'
457
+ ),
458
+ ],
459
+ };
460
+ }
461
+
462
+ if (receiptRead.kind === 'invalid') {
463
+ return {
464
+ required: true,
465
+ kind: 'invalid',
466
+ path: receiptRead.path,
467
+ maxAgeSeconds,
468
+ ageSeconds: null,
469
+ violations: [
470
+ toErrorViolation(
471
+ 'MCP_ENTERPRISE_RECEIPT_INVALID',
472
+ `MCP receipt is invalid: ${receiptRead.reason}`
473
+ ),
474
+ ],
475
+ };
476
+ }
477
+
478
+ const violations: AiGateViolation[] = [];
479
+ if (toCanonicalPath(receiptRead.receipt.repo_root) !== toCanonicalPath(params.repoRoot)) {
480
+ violations.push(
481
+ toErrorViolation(
482
+ 'MCP_ENTERPRISE_RECEIPT_REPO_ROOT_MISMATCH',
483
+ `MCP receipt repo root mismatch (${receiptRead.receipt.repo_root} != ${params.repoRoot}).`
484
+ )
485
+ );
486
+ }
487
+ if (receiptRead.receipt.stage !== params.stage) {
488
+ violations.push(
489
+ toErrorViolation(
490
+ 'MCP_ENTERPRISE_RECEIPT_STAGE_MISMATCH',
491
+ `MCP receipt stage mismatch (${receiptRead.receipt.stage} != ${params.stage}).`
492
+ )
493
+ );
494
+ }
495
+ const ageSeconds = toTimestampAgeSeconds(receiptRead.receipt.issued_at, params.nowMs);
496
+ if (ageSeconds === null) {
497
+ violations.push(
498
+ toErrorViolation(
499
+ 'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_INVALID',
500
+ 'MCP receipt issued_at timestamp is invalid.'
501
+ )
502
+ );
503
+ } else if (ageSeconds > maxAgeSeconds) {
504
+ violations.push(
505
+ toErrorViolation(
506
+ 'MCP_ENTERPRISE_RECEIPT_STALE',
507
+ `MCP receipt is stale (${ageSeconds}s > ${maxAgeSeconds}s for ${params.stage}).`
508
+ )
509
+ );
510
+ }
511
+ if (isTimestampFuture(receiptRead.receipt.issued_at, params.nowMs)) {
512
+ violations.push(
513
+ toErrorViolation(
514
+ 'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_FUTURE',
515
+ 'MCP receipt timestamp is in the future.'
516
+ )
517
+ );
518
+ }
519
+
520
+ return {
521
+ required: true,
522
+ kind: 'valid',
523
+ path: receiptRead.path,
524
+ maxAgeSeconds,
525
+ ageSeconds,
526
+ violations,
527
+ };
528
+ };
529
+
291
530
  export const evaluateAiGate = (
292
531
  params: {
293
532
  repoRoot: string;
294
533
  stage: AiGateStage;
295
534
  maxAgeSecondsByStage?: Readonly<Record<AiGateStage, number>>;
296
535
  protectedBranches?: ReadonlyArray<string>;
536
+ requireMcpReceipt?: boolean;
297
537
  },
298
538
  dependencies: Partial<AiGateDependencies> = {}
299
539
  ): AiGateCheckResult => {
@@ -305,6 +545,34 @@ export const evaluateAiGate = (
305
545
  const protectedBranches = new Set(params.protectedBranches ?? Array.from(DEFAULT_PROTECTED_BRANCHES));
306
546
  const nowMs = activeDependencies.now();
307
547
  const evidenceResult = activeDependencies.readEvidenceResult(params.repoRoot);
548
+ const evidenceSourceDetails = resolveEvidenceSourceDetails(
549
+ params.repoRoot,
550
+ evidenceResult
551
+ );
552
+ const evidenceIntegrityVerification: EvidenceIntegrityVerification =
553
+ evidenceResult.kind === 'valid'
554
+ ? verifyEvidenceIntegrity(evidenceResult.evidence, {
555
+ signatureConfig: resolveEvidenceSigningConfig(activeDependencies.env),
556
+ enforceSignature: false,
557
+ })
558
+ : {
559
+ ok: false,
560
+ status: evidenceResult.kind === 'missing' ? 'missing' : 'invalid',
561
+ code: evidenceResult.kind === 'missing'
562
+ ? 'EVIDENCE_INTEGRITY_MISSING'
563
+ : 'EVIDENCE_INTEGRITY_UNAVAILABLE',
564
+ message: evidenceResult.kind === 'missing'
565
+ ? 'Evidence integrity metadata is missing.'
566
+ : 'Evidence integrity cannot be verified from invalid payload.',
567
+ payloadHash: null,
568
+ previousChainHash: null,
569
+ chainHash: null,
570
+ signature: {
571
+ present: false,
572
+ keyId: null,
573
+ verified: null,
574
+ },
575
+ };
308
576
  const repoState = activeDependencies.captureRepoState(params.repoRoot);
309
577
  const policyStage = toPolicyStage(params.stage);
310
578
  const resolvedPolicy = activeDependencies.resolvePolicyForStage(
@@ -317,13 +585,42 @@ export const evaluateAiGate = (
317
585
  repoState,
318
586
  params.stage,
319
587
  nowMs,
320
- maxAgeSecondsByStage
588
+ maxAgeSecondsByStage,
589
+ evidenceIntegrityVerification
321
590
  );
591
+ const mcpReceiptAssessment = collectMcpReceiptViolations({
592
+ required: params.requireMcpReceipt ?? false,
593
+ stage: params.stage,
594
+ repoRoot: params.repoRoot,
595
+ nowMs,
596
+ maxAgeSecondsByStage,
597
+ readMcpAiGateReceipt: activeDependencies.readMcpAiGateReceipt,
598
+ });
322
599
  const gitflowViolations = collectGitflowViolations(repoState, protectedBranches);
323
- const violations = [...evidenceAssessment.violations, ...gitflowViolations];
600
+ const baseViolations = [
601
+ ...evidenceAssessment.violations,
602
+ ...gitflowViolations,
603
+ ...mcpReceiptAssessment.violations,
604
+ ];
605
+ const waiverAssessment = applyAiGateWaivers({
606
+ repoRoot: params.repoRoot,
607
+ stage: params.stage,
608
+ branch: repoState.git.branch,
609
+ violations: baseViolations,
610
+ now: new Date(nowMs),
611
+ });
612
+ const violations = [...waiverAssessment.violations];
613
+ if (waiverAssessment.status === 'invalid') {
614
+ violations.push(
615
+ toErrorViolation(
616
+ 'WAIVER_POLICY_INVALID',
617
+ `Waiver file is invalid: ${waiverAssessment.invalid_reason ?? 'unknown_error'}.`
618
+ )
619
+ );
620
+ }
324
621
  const blocked = violations.some((violation) => violation.severity === 'ERROR');
325
622
 
326
- return {
623
+ const result: AiGateCheckResult = {
327
624
  stage: params.stage,
328
625
  status: blocked ? 'BLOCKED' : 'ALLOWED',
329
626
  allowed: !blocked,
@@ -338,8 +635,78 @@ export const evaluateAiGate = (
338
635
  kind: evidenceResult.kind,
339
636
  max_age_seconds: maxAgeSecondsByStage[params.stage],
340
637
  age_seconds: evidenceAssessment.ageSeconds,
638
+ source: evidenceSourceDetails.source,
639
+ path: evidenceSourceDetails.path,
640
+ digest: evidenceSourceDetails.digest,
641
+ generated_at: evidenceSourceDetails.generated_at,
642
+ integrity: evidenceAssessment.integrity,
643
+ },
644
+ mcp_receipt: {
645
+ required: mcpReceiptAssessment.required,
646
+ kind: mcpReceiptAssessment.kind,
647
+ path: mcpReceiptAssessment.path,
648
+ max_age_seconds: mcpReceiptAssessment.maxAgeSeconds,
649
+ age_seconds: mcpReceiptAssessment.ageSeconds,
650
+ },
651
+ waivers: {
652
+ path: waiverAssessment.path,
653
+ status: waiverAssessment.status,
654
+ invalid_reason: waiverAssessment.invalid_reason,
655
+ applied: [...waiverAssessment.applied],
341
656
  },
342
657
  repo_state: repoState,
343
658
  violations,
344
659
  };
660
+
661
+ activeDependencies.emitStructuredTelemetry({
662
+ repoRoot: params.repoRoot,
663
+ env: activeDependencies.env,
664
+ event: {
665
+ schema_version: '1',
666
+ timestamp: new Date(nowMs).toISOString(),
667
+ source: 'pumuki',
668
+ channel: 'ai_gate',
669
+ event: 'ai_gate.evaluated',
670
+ stage: result.stage,
671
+ repo_root: params.repoRoot,
672
+ status: result.status,
673
+ decision: result.allowed ? 'ALLOW' : 'BLOCK',
674
+ policy: {
675
+ source: result.policy.trace.source,
676
+ bundle: result.policy.trace.bundle,
677
+ hash: result.policy.trace.hash,
678
+ version: result.policy.trace.version ?? 'n/a',
679
+ signature: result.policy.trace.signature ?? 'n/a',
680
+ },
681
+ evidence: {
682
+ kind: result.evidence.kind,
683
+ age_seconds: result.evidence.age_seconds,
684
+ max_age_seconds: result.evidence.max_age_seconds,
685
+ source: result.evidence.source,
686
+ path: result.evidence.path,
687
+ digest: result.evidence.digest,
688
+ generated_at: result.evidence.generated_at,
689
+ integrity_status: result.evidence.integrity.status,
690
+ chain_hash: result.evidence.integrity.chain_hash,
691
+ },
692
+ metrics: {
693
+ violations_total: result.violations.length,
694
+ violations_error: result.violations.filter((violation) => violation.severity === 'ERROR').length,
695
+ violations_warn: result.violations.filter((violation) => violation.severity === 'WARN').length,
696
+ },
697
+ mcp_receipt: {
698
+ required: result.mcp_receipt.required,
699
+ kind: result.mcp_receipt.kind,
700
+ age_seconds: result.mcp_receipt.age_seconds,
701
+ max_age_seconds: result.mcp_receipt.max_age_seconds,
702
+ },
703
+ waivers: {
704
+ status: result.waivers.status,
705
+ applied: result.waivers.applied.length,
706
+ path: result.waivers.path,
707
+ },
708
+ },
709
+ });
710
+
711
+ return result;
345
712
  };
@@ -44,6 +44,8 @@ export type ResolvedStagePolicy = {
44
44
  source: 'default' | 'skills.policy' | 'hard-mode';
45
45
  bundle: string;
46
46
  hash: string;
47
+ version?: string;
48
+ signature?: string;
47
49
  };
48
50
  };
49
51
 
@@ -181,6 +183,8 @@ const hardModePolicyProfileByStage: Record<
181
183
  };
182
184
 
183
185
  const HARD_MODE_CONFIG_PATH = '.pumuki/hard-mode.json';
186
+ const POLICY_TRACE_VERSION_DEFAULT = 'policy-as-code/default@1.0';
187
+ const POLICY_TRACE_VERSION_HARD_MODE = 'policy-as-code/hard-mode@1.0';
184
188
 
185
189
  const toHardModeProfileName = (value: unknown): HardModeProfileName | null => {
186
190
  if (typeof value !== 'string') {
@@ -287,6 +291,26 @@ const createPolicyTraceHash = (params: {
287
291
  .digest('hex');
288
292
  };
289
293
 
294
+ const createPolicyTraceSignature = (params: {
295
+ version: string;
296
+ source: 'default' | 'skills.policy' | 'hard-mode';
297
+ bundle: string;
298
+ hash: string;
299
+ sourcePolicyHash?: string;
300
+ }): string => {
301
+ return createHash('sha256')
302
+ .update(
303
+ JSON.stringify({
304
+ version: params.version,
305
+ source: params.source,
306
+ bundle: params.bundle,
307
+ hash: params.hash,
308
+ sourcePolicyHash: params.sourcePolicyHash ?? null,
309
+ })
310
+ )
311
+ .digest('hex');
312
+ };
313
+
290
314
  export const resolvePolicyForStage = (
291
315
  stage: SkillsStage,
292
316
  repoRoot: string = process.cwd()
@@ -301,16 +325,25 @@ export const resolvePolicyForStage = (
301
325
  const bundle = profileName
302
326
  ? `gate-policy.hard-mode.${profileName}.${stage}`
303
327
  : `gate-policy.hard-mode.${stage}`;
328
+ const hash = createPolicyTraceHash({
329
+ stage,
330
+ source: 'hard-mode',
331
+ blockOnOrAbove: hardModePolicy.blockOnOrAbove,
332
+ warnOnOrAbove: hardModePolicy.warnOnOrAbove,
333
+ sourcePolicyHash: profileName ?? undefined,
334
+ });
304
335
  return {
305
336
  policy: hardModePolicy,
306
337
  trace: {
307
338
  source: 'hard-mode',
308
339
  bundle,
309
- hash: createPolicyTraceHash({
310
- stage,
340
+ hash,
341
+ version: POLICY_TRACE_VERSION_HARD_MODE,
342
+ signature: createPolicyTraceSignature({
343
+ version: POLICY_TRACE_VERSION_HARD_MODE,
311
344
  source: 'hard-mode',
312
- blockOnOrAbove: hardModePolicy.blockOnOrAbove,
313
- warnOnOrAbove: hardModePolicy.warnOnOrAbove,
345
+ bundle,
346
+ hash,
314
347
  sourcePolicyHash: profileName ?? undefined,
315
348
  }),
316
349
  },
@@ -322,16 +355,25 @@ export const resolvePolicyForStage = (
322
355
  const stageOverride = loadedPolicy?.stages[stage];
323
356
 
324
357
  if (!stageOverride) {
358
+ const hash = createPolicyTraceHash({
359
+ stage,
360
+ source: 'default',
361
+ blockOnOrAbove: defaults.blockOnOrAbove,
362
+ warnOnOrAbove: defaults.warnOnOrAbove,
363
+ });
364
+ const bundle = `gate-policy.default.${stage}`;
325
365
  return {
326
366
  policy: defaults,
327
367
  trace: {
328
368
  source: 'default',
329
- bundle: `gate-policy.default.${stage}`,
330
- hash: createPolicyTraceHash({
331
- stage,
369
+ bundle,
370
+ hash,
371
+ version: POLICY_TRACE_VERSION_DEFAULT,
372
+ signature: createPolicyTraceSignature({
373
+ version: POLICY_TRACE_VERSION_DEFAULT,
332
374
  source: 'default',
333
- blockOnOrAbove: defaults.blockOnOrAbove,
334
- warnOnOrAbove: defaults.warnOnOrAbove,
375
+ bundle,
376
+ hash,
335
377
  }),
336
378
  },
337
379
  };
@@ -343,17 +385,30 @@ export const resolvePolicyForStage = (
343
385
  warnOnOrAbove: stageOverride.warnOnOrAbove,
344
386
  };
345
387
 
388
+ const sourcePolicyHash = createSkillsPolicyDeterministicHash(loadedPolicy);
389
+ const bundle = `gate-policy.skills.policy.${stage}`;
390
+ const version = `policy-as-code/skills.policy@${loadedPolicy.version}`;
391
+ const hash = createPolicyTraceHash({
392
+ stage,
393
+ source: 'skills.policy',
394
+ blockOnOrAbove: resolvedPolicy.blockOnOrAbove,
395
+ warnOnOrAbove: resolvedPolicy.warnOnOrAbove,
396
+ sourcePolicyHash,
397
+ });
398
+
346
399
  return {
347
400
  policy: resolvedPolicy,
348
401
  trace: {
349
402
  source: 'skills.policy',
350
- bundle: `gate-policy.skills.policy.${stage}`,
351
- hash: createPolicyTraceHash({
352
- stage,
403
+ bundle,
404
+ hash,
405
+ version,
406
+ signature: createPolicyTraceSignature({
407
+ version,
353
408
  source: 'skills.policy',
354
- blockOnOrAbove: resolvedPolicy.blockOnOrAbove,
355
- warnOnOrAbove: resolvedPolicy.warnOnOrAbove,
356
- sourcePolicyHash: createSkillsPolicyDeterministicHash(loadedPolicy),
409
+ bundle,
410
+ hash,
411
+ sourcePolicyHash,
357
412
  }),
358
413
  },
359
414
  };