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
@@ -22,11 +22,17 @@ import {
22
22
  openSddSession,
23
23
  readSddStatus,
24
24
  refreshSddSession,
25
+ runSddSyncDocs,
25
26
  type SddStage,
27
+ type SddSessionState,
26
28
  } from '../sdd';
27
29
  import { evaluateAiGate } from '../gate/evaluateAiGate';
28
30
  import { runEnterpriseAiGateCheck } from '../mcp/aiGateCheck';
29
31
  import { emitAuditSummaryNotificationFromAiGate } from '../notifications/emitAuditSummaryNotification';
32
+ import {
33
+ buildPreWriteAutomationTrace,
34
+ type PreWriteAutomationTrace,
35
+ } from './preWriteAutomation';
30
36
  import { buildLocalHotspotsReport, type LocalHotspotsReport } from './analyticsHotspots';
31
37
  import { resolveHotspotsSaasIngestionAuditPath } from './saasIngestionAudit';
32
38
  import { readHotspotsSaasIngestionPayload } from './saasIngestionContract';
@@ -48,7 +54,7 @@ type LifecycleCommand =
48
54
  | 'adapter'
49
55
  | 'analytics';
50
56
 
51
- type SddCommand = 'status' | 'validate' | 'session';
57
+ type SddCommand = 'status' | 'validate' | 'session' | 'sync-docs';
52
58
  type LoopCommand = 'run' | 'status' | 'stop' | 'resume' | 'list' | 'export';
53
59
  type AnalyticsCommand = 'hotspots';
54
60
  type AnalyticsHotspotsCommand = 'report' | 'diagnose';
@@ -60,6 +66,7 @@ type ParsedArgs = {
60
66
  purgeArtifacts: boolean;
61
67
  updateSpec?: string;
62
68
  json: boolean;
69
+ doctorDeep?: boolean;
63
70
  sddCommand?: SddCommand;
64
71
  loopCommand?: LoopCommand;
65
72
  loopSessionId?: string;
@@ -70,6 +77,7 @@ type ParsedArgs = {
70
77
  sddSessionAction?: SddSessionAction;
71
78
  sddChangeId?: string;
72
79
  sddTtlMinutes?: number;
80
+ sddDryRun?: boolean;
73
81
  adapterCommand?: 'install';
74
82
  adapterAgent?: AdapterAgent;
75
83
  adapterDryRun?: boolean;
@@ -87,7 +95,7 @@ Pumuki lifecycle commands:
87
95
  pumuki uninstall [--purge-artifacts]
88
96
  pumuki remove
89
97
  pumuki update [--latest|--spec=<package-spec>]
90
- pumuki doctor
98
+ pumuki doctor [--deep] [--json]
91
99
  pumuki status
92
100
  pumuki loop run --objective=<text> [--max-attempts=<n>] [--json]
93
101
  pumuki loop status --session=<session-id> [--json]
@@ -103,6 +111,7 @@ Pumuki lifecycle commands:
103
111
  pumuki sdd session --open --change=<change-id> [--ttl-minutes=<n>] [--json]
104
112
  pumuki sdd session --refresh [--ttl-minutes=<n>] [--json]
105
113
  pumuki sdd session --close [--json]
114
+ pumuki sdd sync-docs [--dry-run] [--json]
106
115
  `.trim();
107
116
 
108
117
  const LOOP_RUN_POLICY: GatePolicy = {
@@ -119,6 +128,24 @@ const writeError = (message: string): void => {
119
128
  process.stderr.write(`${message}\n`);
120
129
  };
121
130
 
131
+ class LifecycleCliExitError extends Error {
132
+ readonly exitCode: number;
133
+ readonly stream: 'stdout' | 'stderr';
134
+
135
+ constructor(
136
+ message: string,
137
+ options: {
138
+ exitCode: number;
139
+ stream: 'stdout' | 'stderr';
140
+ }
141
+ ) {
142
+ super(message);
143
+ this.name = 'LifecycleCliExitError';
144
+ this.exitCode = options.exitCode;
145
+ this.stream = options.stream;
146
+ }
147
+ }
148
+
122
149
  const withOptionalLocation = (message: string, location?: string): string => {
123
150
  if (!location || location.trim().length === 0) {
124
151
  return message;
@@ -126,6 +153,20 @@ const withOptionalLocation = (message: string, location?: string): string => {
126
153
  return `${message} -> ${location}`;
127
154
  };
128
155
 
156
+ const renderSddChangeDescriptor = (session: SddSessionState): string => {
157
+ const canonical = session.changeId?.trim();
158
+ if (!canonical || canonical.length === 0) {
159
+ return 'none';
160
+ }
161
+ const alias = session.changeAlias?.trim();
162
+ if (!alias || alias.length === 0 || alias === canonical) {
163
+ return canonical;
164
+ }
165
+ return `${canonical} alias=${alias}`;
166
+ };
167
+
168
+ const isHelpFlag = (value: string | undefined): boolean => value === '--help' || value === '-h';
169
+
129
170
  const isLifecycleCommand = (value: string): value is LifecycleCommand =>
130
171
  value === 'install' ||
131
172
  value === 'uninstall' ||
@@ -379,8 +420,17 @@ const printHotspotsPublishDiagnostics = (diagnostics: HotspotsPublishDiagnostics
379
420
 
380
421
  export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs => {
381
422
  const commandRaw = argv[0];
382
- if (!commandRaw || commandRaw === '--help' || commandRaw === '-h') {
383
- throw new Error(HELP_TEXT);
423
+ if (!commandRaw) {
424
+ throw new LifecycleCliExitError(HELP_TEXT, {
425
+ exitCode: 1,
426
+ stream: 'stderr',
427
+ });
428
+ }
429
+ if (isHelpFlag(commandRaw)) {
430
+ throw new LifecycleCliExitError(HELP_TEXT, {
431
+ exitCode: 0,
432
+ stream: 'stdout',
433
+ });
384
434
  }
385
435
  if (!isLifecycleCommand(commandRaw)) {
386
436
  throw new Error(`Unknown command "${commandRaw}".\n\n${HELP_TEXT}`);
@@ -389,6 +439,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
389
439
  let purgeArtifacts = false;
390
440
  let updateSpec: ParsedArgs['updateSpec'];
391
441
  let json = false;
442
+ let doctorDeep = false;
392
443
  let sddCommand: ParsedArgs['sddCommand'];
393
444
  let loopCommand: ParsedArgs['loopCommand'];
394
445
  let loopSessionId: ParsedArgs['loopSessionId'];
@@ -399,6 +450,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
399
450
  let sddSessionAction: ParsedArgs['sddSessionAction'];
400
451
  let sddChangeId: ParsedArgs['sddChangeId'];
401
452
  let sddTtlMinutes: ParsedArgs['sddTtlMinutes'];
453
+ let sddDryRun = false;
402
454
  let adapterCommand: ParsedArgs['adapterCommand'];
403
455
  let adapterAgent: ParsedArgs['adapterAgent'];
404
456
  let adapterDryRun = false;
@@ -411,11 +463,23 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
411
463
 
412
464
  if (commandRaw === 'analytics') {
413
465
  const subcommandRaw = argv[1] ?? '';
466
+ if (isHelpFlag(subcommandRaw)) {
467
+ throw new LifecycleCliExitError(HELP_TEXT, {
468
+ exitCode: 0,
469
+ stream: 'stdout',
470
+ });
471
+ }
414
472
  if (subcommandRaw !== 'hotspots') {
415
473
  throw new Error(`Unsupported analytics command "${subcommandRaw}".\n\n${HELP_TEXT}`);
416
474
  }
417
475
  analyticsCommand = 'hotspots';
418
476
  const hotspotsActionRaw = argv[2] ?? '';
477
+ if (isHelpFlag(hotspotsActionRaw)) {
478
+ throw new LifecycleCliExitError(HELP_TEXT, {
479
+ exitCode: 0,
480
+ stream: 'stdout',
481
+ });
482
+ }
419
483
  if (hotspotsActionRaw !== 'report' && hotspotsActionRaw !== 'diagnose') {
420
484
  throw new Error(
421
485
  `Unsupported analytics hotspots action "${hotspotsActionRaw}".\n\n${HELP_TEXT}`
@@ -423,6 +487,12 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
423
487
  }
424
488
  analyticsHotspotsCommand = hotspotsActionRaw;
425
489
  for (const arg of argv.slice(3)) {
490
+ if (isHelpFlag(arg)) {
491
+ throw new LifecycleCliExitError(HELP_TEXT, {
492
+ exitCode: 0,
493
+ stream: 'stdout',
494
+ });
495
+ }
426
496
  if (arg === '--json') {
427
497
  json = true;
428
498
  continue;
@@ -483,6 +553,12 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
483
553
 
484
554
  if (commandRaw === 'loop') {
485
555
  const subcommandRaw = argv[1] ?? '';
556
+ if (isHelpFlag(subcommandRaw)) {
557
+ throw new LifecycleCliExitError(HELP_TEXT, {
558
+ exitCode: 0,
559
+ stream: 'stdout',
560
+ });
561
+ }
486
562
  if (
487
563
  subcommandRaw !== 'run' &&
488
564
  subcommandRaw !== 'status' &&
@@ -495,6 +571,12 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
495
571
  }
496
572
  loopCommand = subcommandRaw;
497
573
  for (const arg of argv.slice(2)) {
574
+ if (isHelpFlag(arg)) {
575
+ throw new LifecycleCliExitError(HELP_TEXT, {
576
+ exitCode: 0,
577
+ stream: 'stdout',
578
+ });
579
+ }
498
580
  if (arg === '--json') {
499
581
  json = true;
500
582
  continue;
@@ -573,16 +655,29 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
573
655
 
574
656
  if (commandRaw === 'sdd') {
575
657
  const subcommandRaw = argv[1] ?? 'status';
658
+ if (isHelpFlag(subcommandRaw)) {
659
+ throw new LifecycleCliExitError(HELP_TEXT, {
660
+ exitCode: 0,
661
+ stream: 'stdout',
662
+ });
663
+ }
576
664
  if (
577
665
  subcommandRaw !== 'status' &&
578
666
  subcommandRaw !== 'validate' &&
579
- subcommandRaw !== 'session'
667
+ subcommandRaw !== 'session' &&
668
+ subcommandRaw !== 'sync-docs'
580
669
  ) {
581
670
  throw new Error(`Unsupported SDD subcommand "${subcommandRaw}".\n\n${HELP_TEXT}`);
582
671
  }
583
672
  sddCommand = subcommandRaw;
584
673
 
585
674
  for (const arg of argv.slice(2)) {
675
+ if (isHelpFlag(arg)) {
676
+ throw new LifecycleCliExitError(HELP_TEXT, {
677
+ exitCode: 0,
678
+ stream: 'stdout',
679
+ });
680
+ }
586
681
  if (arg === '--json') {
587
682
  json = true;
588
683
  continue;
@@ -615,6 +710,10 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
615
710
  sddTtlMinutes = minutes;
616
711
  continue;
617
712
  }
713
+ if (arg === '--dry-run') {
714
+ sddDryRun = true;
715
+ continue;
716
+ }
618
717
  throw new Error(`Unsupported argument "${arg}".\n\n${HELP_TEXT}`);
619
718
  }
620
719
 
@@ -635,6 +734,20 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
635
734
  sddStage: sddStage ?? 'PRE_COMMIT',
636
735
  };
637
736
  }
737
+ if (sddCommand === 'sync-docs') {
738
+ if (sddStage || sddSessionAction || sddChangeId || typeof sddTtlMinutes === 'number') {
739
+ throw new Error(
740
+ `"pumuki sdd sync-docs" only supports [--dry-run] [--json].\n\n${HELP_TEXT}`
741
+ );
742
+ }
743
+ return {
744
+ command: commandRaw,
745
+ purgeArtifacts: false,
746
+ json,
747
+ sddCommand,
748
+ sddDryRun,
749
+ };
750
+ }
638
751
 
639
752
  if (!sddSessionAction) {
640
753
  throw new Error(
@@ -660,12 +773,24 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
660
773
 
661
774
  if (commandRaw === 'adapter') {
662
775
  const subcommandRaw = argv[1] ?? '';
776
+ if (isHelpFlag(subcommandRaw)) {
777
+ throw new LifecycleCliExitError(HELP_TEXT, {
778
+ exitCode: 0,
779
+ stream: 'stdout',
780
+ });
781
+ }
663
782
  if (subcommandRaw !== 'install') {
664
783
  throw new Error(`Unsupported adapter subcommand "${subcommandRaw}".\n\n${HELP_TEXT}`);
665
784
  }
666
785
  adapterCommand = 'install';
667
786
 
668
787
  for (const arg of argv.slice(2)) {
788
+ if (isHelpFlag(arg)) {
789
+ throw new LifecycleCliExitError(HELP_TEXT, {
790
+ exitCode: 0,
791
+ stream: 'stdout',
792
+ });
793
+ }
669
794
  if (arg === '--json') {
670
795
  json = true;
671
796
  continue;
@@ -694,6 +819,12 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
694
819
  }
695
820
 
696
821
  for (const arg of argv.slice(1)) {
822
+ if (isHelpFlag(arg)) {
823
+ throw new LifecycleCliExitError(HELP_TEXT, {
824
+ exitCode: 0,
825
+ stream: 'stdout',
826
+ });
827
+ }
697
828
  if (arg === '--json') {
698
829
  json = true;
699
830
  continue;
@@ -710,16 +841,27 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
710
841
  updateSpec = arg.slice('--spec='.length).trim();
711
842
  continue;
712
843
  }
844
+ if (arg === '--deep') {
845
+ if (commandRaw !== 'doctor') {
846
+ throw new Error(`Unsupported argument "${arg}".\n\n${HELP_TEXT}`);
847
+ }
848
+ doctorDeep = true;
849
+ continue;
850
+ }
713
851
 
714
852
  throw new Error(`Unsupported argument "${arg}".\n\n${HELP_TEXT}`);
715
853
  }
716
854
 
717
- return {
855
+ const parsedArgs: ParsedArgs = {
718
856
  command: commandRaw,
719
857
  purgeArtifacts,
720
858
  updateSpec,
721
859
  json,
722
860
  };
861
+ if (doctorDeep) {
862
+ parsedArgs.doctorDeep = true;
863
+ }
864
+ return parsedArgs;
723
865
  };
724
866
 
725
867
  const printDoctorReport = (report: LifecycleDoctorReport): void => {
@@ -734,6 +876,14 @@ const printDoctorReport = (report: LifecycleDoctorReport): void => {
734
876
  writeInfo(
735
877
  `[pumuki] hook pre-push: ${report.hookStatus['pre-push'].managedBlockPresent ? 'managed' : 'missing'}`
736
878
  );
879
+ if (report.deep?.enabled) {
880
+ writeInfo(`[pumuki][deep] checks: ${report.deep.checks.length}`);
881
+ for (const check of report.deep.checks) {
882
+ writeInfo(
883
+ `[pumuki][deep] ${check.id}: ${check.status.toUpperCase()} - ${check.message}`
884
+ );
885
+ }
886
+ }
737
887
 
738
888
  if (report.issues.length === 0) {
739
889
  writeInfo('[pumuki] doctor verdict: PASS');
@@ -752,6 +902,14 @@ const PRE_WRITE_TELEMETRY_CHAIN = 'pumuki->mcp->ai_gate->ai_evidence';
752
902
  type PreWriteValidationEnvelope = {
753
903
  sdd: ReturnType<typeof evaluateSddPolicy>;
754
904
  ai_gate: ReturnType<typeof evaluateAiGate>;
905
+ automation: {
906
+ attempted: boolean;
907
+ actions: Array<{
908
+ action: 'refresh_evidence' | 'refresh_mcp_receipt';
909
+ status: 'OK' | 'FAILED';
910
+ details: string;
911
+ }>;
912
+ };
755
913
  telemetry: {
756
914
  chain: typeof PRE_WRITE_TELEMETRY_CHAIN;
757
915
  stage: SddStage;
@@ -769,6 +927,131 @@ const defaultLifecycleCliDependencies: LifecycleCliDependencies = {
769
927
  runPlatformGate,
770
928
  };
771
929
 
930
+ const PRE_WRITE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
931
+ EVIDENCE_MISSING: 'Regenera evidencia ejecutando una auditoría completa antes de continuar.',
932
+ EVIDENCE_INVALID: 'Corrige el contrato de .ai_evidence.json y vuelve a ejecutar el gate.',
933
+ EVIDENCE_INTEGRITY_MISSING: 'Regenera evidencia para crear la metadata de integridad criptográfica.',
934
+ EVIDENCE_INTEGRITY_UNAVAILABLE: 'Regenera evidencia desde una auditoría válida para restaurar integridad.',
935
+ EVIDENCE_INTEGRITY_SCHEMA_INVALID: 'Regenera evidencia; el bloque de integridad no cumple el contrato.',
936
+ EVIDENCE_INTEGRITY_HASH_FORMAT_INVALID: 'Regenera evidencia; los hashes de integridad son inválidos.',
937
+ EVIDENCE_INTEGRITY_PREVIOUS_CHAIN_HASH_INVALID:
938
+ 'Regenera evidencia para reparar previous_chain_hash.',
939
+ EVIDENCE_INTEGRITY_TIMESTAMP_MISMATCH: 'Regenera evidencia para alinear timestamp e integridad.',
940
+ EVIDENCE_INTEGRITY_PAYLOAD_HASH_MISMATCH: 'Regenera evidencia; el payload hash no coincide.',
941
+ EVIDENCE_INTEGRITY_CHAIN_HASH_MISMATCH: 'Regenera evidencia; la cadena criptográfica no coincide.',
942
+ EVIDENCE_INTEGRITY_SIGNATURE_REQUIRED: 'Configura firma o desactiva exigencia de firma explícita.',
943
+ EVIDENCE_INTEGRITY_SIGNATURE_FORMAT_INVALID:
944
+ 'Regenera evidencia o corrige metadatos de firma (algorithm/key_id/value).',
945
+ EVIDENCE_INTEGRITY_SIGNATURE_KEY_MISSING:
946
+ 'Configura PUMUKI_EVIDENCE_SIGNING_KEY para verificar firma localmente.',
947
+ EVIDENCE_INTEGRITY_SIGNATURE_KEY_MISMATCH:
948
+ 'Alinea PUMUKI_EVIDENCE_SIGNING_KEY_ID con el key_id usado para firmar la evidencia.',
949
+ EVIDENCE_INTEGRITY_SIGNATURE_INVALID: 'Regenera evidencia firmada; la firma actual no verifica.',
950
+ EVIDENCE_STALE: 'Refresca evidencia para este repo y rama.',
951
+ EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este repositorio.',
952
+ EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual.',
953
+ EVIDENCE_RULES_COVERAGE_MISSING: 'Ejecuta auditoría completa para recalcular rules_coverage.',
954
+ EVIDENCE_RULES_COVERAGE_INCOMPLETE: 'Asegura unevaluated=0 y coverage_ratio=1.',
955
+ GITFLOW_PROTECTED_BRANCH: 'Trabaja en feature/* y evita ramas protegidas.',
956
+ MCP_ENTERPRISE_RECEIPT_MISSING: 'Invoca ai_gate_check desde pumuki-enterprise MCP antes de PRE_WRITE.',
957
+ MCP_ENTERPRISE_RECEIPT_INVALID: 'Corrige recibo MCP y vuelve a invocar ai_gate_check.',
958
+ MCP_ENTERPRISE_RECEIPT_STALE: 'Vuelve a ejecutar ai_gate_check para emitir recibo fresco.',
959
+ MCP_ENTERPRISE_RECEIPT_STAGE_MISMATCH: 'Reejecuta ai_gate_check con stage PRE_WRITE.',
960
+ MCP_ENTERPRISE_RECEIPT_REPO_ROOT_MISMATCH: 'Genera el recibo MCP en este mismo repositorio.',
961
+ WAIVER_POLICY_INVALID: 'Corrige el archivo de waivers y vuelve a ejecutar la validación.',
962
+ };
963
+
964
+ const wrapPreWritePanelLine = (value: string, width: number): string[] => {
965
+ if (width < 20 || value.length <= width) {
966
+ return [value];
967
+ }
968
+ const words = value.split(/\s+/);
969
+ const lines: string[] = [];
970
+ let current = '';
971
+ for (const word of words) {
972
+ if (current.length === 0) {
973
+ current = word;
974
+ continue;
975
+ }
976
+ if (`${current} ${word}`.length <= width) {
977
+ current = `${current} ${word}`;
978
+ continue;
979
+ }
980
+ lines.push(current);
981
+ current = word;
982
+ }
983
+ if (current.length > 0) {
984
+ lines.push(current);
985
+ }
986
+ return lines;
987
+ };
988
+
989
+ const renderPreWritePanel = (lines: ReadonlyArray<string>): string => {
990
+ const terminalWidth = Number.isFinite(process.stdout.columns ?? NaN)
991
+ ? Number(process.stdout.columns)
992
+ : 110;
993
+ const width = Math.min(140, Math.max(86, terminalWidth - 2));
994
+ const innerWidth = width - 4;
995
+ const normalized = lines.flatMap((line) => wrapPreWritePanelLine(line, innerWidth));
996
+ const top = `╔${'═'.repeat(width - 2)}╗`;
997
+ const bottom = `╚${'═'.repeat(width - 2)}╝`;
998
+ const body = normalized.map((line) => `║ ${line.padEnd(innerWidth, ' ')} ║`);
999
+ return [top, ...body, bottom].join('\n');
1000
+ };
1001
+
1002
+ const buildPreWriteValidationPanel = (params: {
1003
+ sdd: ReturnType<typeof evaluateSddPolicy>;
1004
+ aiGate: ReturnType<typeof evaluateAiGate>;
1005
+ automation: PreWriteAutomationTrace;
1006
+ }): string => {
1007
+ const git = params.aiGate.repo_state.git;
1008
+ const receipt = params.aiGate.mcp_receipt;
1009
+ const policyTrace = params.aiGate.policy.trace;
1010
+ const lines: string[] = [
1011
+ 'PRE-FLIGHT CHECK',
1012
+ `Stage: ${params.sdd.stage} · SDD: ${params.sdd.decision.code} · AI Gate: ${params.aiGate.status}`,
1013
+ `Branch: ${git.branch ?? 'unknown'} · Upstream: ${git.upstream ?? 'none'}`,
1014
+ `Worktree: dirty=${git.dirty ? 'yes' : 'no'} staged=${git.staged} unstaged=${git.unstaged} ahead=${git.ahead} behind=${git.behind}`,
1015
+ `Policy: source=${policyTrace.source} bundle=${policyTrace.bundle} hash=${policyTrace.hash}`,
1016
+ `Policy signature: version=${policyTrace.version ?? 'n/a'} signature=${policyTrace.signature ?? 'n/a'}`,
1017
+ `Evidence: kind=${params.aiGate.evidence.kind} age=${params.aiGate.evidence.age_seconds ?? 'n/a'}s max=${params.aiGate.evidence.max_age_seconds}s`,
1018
+ `Evidence source: ${params.aiGate.evidence.source} path=${params.aiGate.evidence.path}`,
1019
+ `Evidence digest: ${params.aiGate.evidence.digest ?? 'n/a'} generated_at=${params.aiGate.evidence.generated_at ?? 'n/a'}`,
1020
+ `Evidence integrity: status=${params.aiGate.evidence.integrity.status} chain_hash=${params.aiGate.evidence.integrity.chain_hash ?? 'n/a'} prev=${params.aiGate.evidence.integrity.previous_chain_hash ?? 'n/a'} signature=${params.aiGate.evidence.integrity.signature_present ? 'present' : 'absent'} verified=${params.aiGate.evidence.integrity.signature_verified === null ? 'n/a' : params.aiGate.evidence.integrity.signature_verified ? 'yes' : 'no'}`,
1021
+ `Waivers: status=${params.aiGate.waivers.status} applied=${params.aiGate.waivers.applied.length} path=${params.aiGate.waivers.path}`,
1022
+ `MCP receipt: required=${receipt.required ? 'yes' : 'no'} kind=${receipt.kind} age=${receipt.age_seconds ?? 'n/a'}s max=${receipt.max_age_seconds ?? 'n/a'}s`,
1023
+ `Auto-heal: attempted=${params.automation.attempted ? 'yes' : 'no'} actions=${params.automation.actions.length}`,
1024
+ `Violations: ${params.aiGate.violations.length}`,
1025
+ ];
1026
+
1027
+ if (params.automation.actions.length > 0) {
1028
+ lines.push('');
1029
+ lines.push('Auto-heal actions:');
1030
+ for (const action of params.automation.actions) {
1031
+ lines.push(`- ${action.action}: ${action.status} (${action.details})`);
1032
+ }
1033
+ }
1034
+
1035
+ if (params.aiGate.violations.length > 0) {
1036
+ lines.push('');
1037
+ lines.push('Blocking causes:');
1038
+ for (const violation of params.aiGate.violations) {
1039
+ lines.push(`- ${violation.code}: ${violation.message}`);
1040
+ }
1041
+ lines.push('');
1042
+ lines.push('Operational hints:');
1043
+ for (const violation of params.aiGate.violations) {
1044
+ const hint = PRE_WRITE_HINTS_BY_CODE[violation.code];
1045
+ if (!hint) {
1046
+ continue;
1047
+ }
1048
+ lines.push(`- ${violation.code}: ${hint}`);
1049
+ }
1050
+ }
1051
+
1052
+ return renderPreWritePanel(lines);
1053
+ };
1054
+
772
1055
  const resolveSddDecisionLocation = (
773
1056
  result: ReturnType<typeof evaluateSddPolicy>
774
1057
  ) => {
@@ -779,8 +1062,10 @@ const resolveSddDecisionLocation = (
779
1062
  case 'OPENSPEC_PROJECT_MISSING':
780
1063
  case 'SDD_VALIDATION_FAILED':
781
1064
  case 'SDD_VALIDATION_ERROR':
1065
+ case 'SDD_VALIDATION_EMPTY':
782
1066
  return 'openspec/changes:1';
783
1067
  case 'SDD_CHANGE_MISSING':
1068
+ case 'SDD_CHANGE_INCOMPLETE':
784
1069
  return changeId && changeId.trim().length > 0
785
1070
  ? `openspec/changes/${changeId.trim()}:1`
786
1071
  : 'openspec/changes:1';
@@ -808,10 +1093,15 @@ const resolveAiGateViolationLocation = (code: string) => {
808
1093
 
809
1094
  const buildPreWriteValidationEnvelope = (
810
1095
  result: ReturnType<typeof evaluateSddPolicy>,
811
- aiGate: ReturnType<typeof evaluateAiGate>
1096
+ aiGate: ReturnType<typeof evaluateAiGate>,
1097
+ automation: PreWriteAutomationTrace
812
1098
  ): PreWriteValidationEnvelope => ({
813
1099
  sdd: result,
814
1100
  ai_gate: aiGate,
1101
+ automation: {
1102
+ attempted: automation.attempted,
1103
+ actions: [...automation.actions],
1104
+ },
815
1105
  telemetry: {
816
1106
  chain: PRE_WRITE_TELEMETRY_CHAIN,
817
1107
  stage: result.stage,
@@ -866,6 +1156,31 @@ export const runLifecycleCli = async (
866
1156
  if (result.openSpecBootstrap.skippedReason === 'NO_PACKAGE_JSON') {
867
1157
  writeInfo('[pumuki] openspec bootstrap skipped npm install (package.json not found)');
868
1158
  }
1159
+ if (result.openSpecBootstrap.skippedReason === 'NPM_INSTALL_FAILED') {
1160
+ writeInfo('[pumuki] openspec bootstrap npm install failed; continuing in standalone mode (hooks installed)');
1161
+ if (result.openSpecBootstrap.skippedDetails) {
1162
+ writeInfo(`[pumuki] openspec bootstrap detail: ${result.openSpecBootstrap.skippedDetails}`);
1163
+ }
1164
+ }
1165
+ if (result.openSpecBootstrap.skippedReason === 'ENGINE_MISMATCH') {
1166
+ writeInfo('[pumuki] openspec bootstrap skipped npm install due to repository engines mismatch; continuing in standalone mode (hooks installed)');
1167
+ if (result.openSpecBootstrap.skippedDetails) {
1168
+ writeInfo(`[pumuki] openspec bootstrap detail: ${result.openSpecBootstrap.skippedDetails}`);
1169
+ }
1170
+ }
1171
+ }
1172
+ const consumerBootstrap = result.consumerPackageBootstrap;
1173
+ if (consumerBootstrap.packageInstalled) {
1174
+ writeInfo(
1175
+ `[pumuki] consumer package bootstrap: installed=yes source=${consumerBootstrap.dependencySource ?? 'devDependencies'} spec=${consumerBootstrap.targetSpec ?? 'n/a'}`
1176
+ );
1177
+ } else if (consumerBootstrap.skippedReason) {
1178
+ writeInfo(
1179
+ `[pumuki] consumer package bootstrap: installed=no reason=${consumerBootstrap.skippedReason.toLowerCase()}`
1180
+ );
1181
+ if (consumerBootstrap.skippedDetails) {
1182
+ writeInfo(`[pumuki] consumer package bootstrap detail: ${consumerBootstrap.skippedDetails}`);
1183
+ }
869
1184
  }
870
1185
  return 0;
871
1186
  }
@@ -906,8 +1221,14 @@ export const runLifecycleCli = async (
906
1221
  return 0;
907
1222
  }
908
1223
  case 'doctor': {
909
- const report = runLifecycleDoctor();
910
- printDoctorReport(report);
1224
+ const report = runLifecycleDoctor({
1225
+ deep: parsed.doctorDeep,
1226
+ });
1227
+ if (parsed.json) {
1228
+ writeInfo(JSON.stringify(report, null, 2));
1229
+ } else {
1230
+ printDoctorReport(report);
1231
+ }
911
1232
  return report.issues.some((issue) => issue.severity === 'error') ? 1 : 0;
912
1233
  }
913
1234
  case 'status': {
@@ -1150,6 +1471,26 @@ export const runLifecycleCli = async (
1150
1471
  return 1;
1151
1472
  }
1152
1473
  case 'sdd': {
1474
+ if (parsed.sddCommand === 'sync-docs') {
1475
+ const syncResult = runSddSyncDocs({
1476
+ repoRoot: process.cwd(),
1477
+ dryRun: parsed.sddDryRun,
1478
+ });
1479
+ if (parsed.json) {
1480
+ writeInfo(JSON.stringify(syncResult, null, 2));
1481
+ } else {
1482
+ writeInfo(
1483
+ `[pumuki][sdd] sync-docs: change=${syncResult.changeId} dry-run=${syncResult.dryRun ? 'yes' : 'no'} changed=${syncResult.changed ? 'yes' : 'no'} file=${syncResult.targetPathRelative}`
1484
+ );
1485
+ if (syncResult.diffPreview.length > 0) {
1486
+ writeInfo('[pumuki][sdd] sync-docs diff preview:');
1487
+ writeInfo(syncResult.diffPreview);
1488
+ } else {
1489
+ writeInfo('[pumuki][sdd] sync-docs diff preview: no changes.');
1490
+ }
1491
+ }
1492
+ return 0;
1493
+ }
1153
1494
  if (parsed.sddCommand === 'status') {
1154
1495
  const sddStatus = readSddStatus();
1155
1496
  if (parsed.json) {
@@ -1166,7 +1507,7 @@ export const runLifecycleCli = async (
1166
1507
  `[pumuki][sdd] openspec project initialized: ${sddStatus.openspec.projectInitialized ? 'yes' : 'no'}`
1167
1508
  );
1168
1509
  writeInfo(
1169
- `[pumuki][sdd] session: active=${sddStatus.session.active ? 'yes' : 'no'} valid=${sddStatus.session.valid ? 'yes' : 'no'} change=${sddStatus.session.changeId ?? 'none'}`
1510
+ `[pumuki][sdd] session: active=${sddStatus.session.active ? 'yes' : 'no'} valid=${sddStatus.session.valid ? 'yes' : 'no'} change=${renderSddChangeDescriptor(sddStatus.session)}`
1170
1511
  );
1171
1512
  if (typeof sddStatus.session.remainingSeconds === 'number') {
1172
1513
  writeInfo(
@@ -1181,17 +1522,33 @@ export const runLifecycleCli = async (
1181
1522
  stage: parsed.sddStage ?? 'PRE_COMMIT',
1182
1523
  });
1183
1524
  const shouldEvaluateAiGate = result.stage === 'PRE_WRITE';
1184
- const aiGate = shouldEvaluateAiGate
1525
+ let aiGate = shouldEvaluateAiGate
1185
1526
  ? runEnterpriseAiGateCheck({
1186
1527
  repoRoot: process.cwd(),
1187
1528
  stage: result.stage,
1529
+ requireMcpReceipt: true,
1188
1530
  }).result
1189
1531
  : null;
1532
+ const automationTrace: PreWriteAutomationTrace = {
1533
+ attempted: false,
1534
+ actions: [],
1535
+ };
1536
+ if (result.stage === 'PRE_WRITE' && aiGate) {
1537
+ const auto = await buildPreWriteAutomationTrace({
1538
+ repoRoot: process.cwd(),
1539
+ sdd: result,
1540
+ aiGate,
1541
+ runPlatformGate: activeDependencies.runPlatformGate,
1542
+ });
1543
+ aiGate = auto.aiGate;
1544
+ automationTrace.attempted = auto.trace.attempted;
1545
+ automationTrace.actions = auto.trace.actions;
1546
+ }
1190
1547
  if (parsed.json) {
1191
1548
  writeInfo(
1192
1549
  JSON.stringify(
1193
1550
  aiGate
1194
- ? buildPreWriteValidationEnvelope(result, aiGate)
1551
+ ? buildPreWriteValidationEnvelope(result, aiGate, automationTrace)
1195
1552
  : result,
1196
1553
  null,
1197
1554
  2
@@ -1213,6 +1570,11 @@ export const runLifecycleCli = async (
1213
1570
  );
1214
1571
  }
1215
1572
  if (aiGate) {
1573
+ writeInfo(buildPreWriteValidationPanel({
1574
+ sdd: result,
1575
+ aiGate,
1576
+ automation: automationTrace,
1577
+ }));
1216
1578
  writeInfo(
1217
1579
  `[pumuki][ai-gate] stage=${aiGate.stage} status=${aiGate.status} violations=${aiGate.violations.length}`
1218
1580
  );
@@ -1251,7 +1613,7 @@ export const runLifecycleCli = async (
1251
1613
  writeInfo(JSON.stringify(session, null, 2));
1252
1614
  } else {
1253
1615
  writeInfo(
1254
- `[pumuki][sdd] session opened: change=${session.changeId} ttlMinutes=${session.ttlMinutes ?? 'unknown'} valid=${session.valid ? 'yes' : 'no'}`
1616
+ `[pumuki][sdd] session opened: change=${renderSddChangeDescriptor(session)} ttlMinutes=${session.ttlMinutes ?? 'unknown'} valid=${session.valid ? 'yes' : 'no'}`
1255
1617
  );
1256
1618
  }
1257
1619
  return 0;
@@ -1264,7 +1626,7 @@ export const runLifecycleCli = async (
1264
1626
  writeInfo(JSON.stringify(session, null, 2));
1265
1627
  } else {
1266
1628
  writeInfo(
1267
- `[pumuki][sdd] session refreshed: change=${session.changeId ?? 'none'} ttlMinutes=${session.ttlMinutes ?? 'unknown'} valid=${session.valid ? 'yes' : 'no'}`
1629
+ `[pumuki][sdd] session refreshed: change=${renderSddChangeDescriptor(session)} ttlMinutes=${session.ttlMinutes ?? 'unknown'} valid=${session.valid ? 'yes' : 'no'}`
1268
1630
  );
1269
1631
  }
1270
1632
  return 0;
@@ -1305,6 +1667,14 @@ export const runLifecycleCli = async (
1305
1667
  return 1;
1306
1668
  }
1307
1669
  } catch (error) {
1670
+ if (error instanceof LifecycleCliExitError) {
1671
+ if (error.stream === 'stdout') {
1672
+ writeInfo(error.message);
1673
+ } else {
1674
+ writeError(error.message);
1675
+ }
1676
+ return error.exitCode;
1677
+ }
1308
1678
  const message = error instanceof Error ? error.message : 'Unexpected lifecycle CLI error.';
1309
1679
  writeError(message);
1310
1680
  return 1;