pumuki 6.3.70 → 6.3.72

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 (45) hide show
  1. package/AGENTS.md +269 -0
  2. package/CHANGELOG.md +666 -0
  3. package/README.md +32 -0
  4. package/docs/README.md +7 -2
  5. package/docs/operations/RELEASE_NOTES.md +15 -0
  6. package/docs/product/CONFIGURATION.md +12 -0
  7. package/docs/product/USAGE.md +24 -3
  8. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +111 -0
  9. package/integrations/evidence/buildEvidence.ts +15 -0
  10. package/integrations/evidence/operationalHints.ts +110 -0
  11. package/integrations/evidence/schema.ts +16 -0
  12. package/integrations/evidence/writeEvidence.ts +3 -0
  13. package/integrations/gate/remediationCatalog.ts +40 -0
  14. package/integrations/git/GitService.ts +25 -0
  15. package/integrations/git/filterFactsByPathPrefixes.ts +61 -0
  16. package/integrations/git/runPlatformGate.ts +12 -4
  17. package/integrations/git/runPlatformGateFacts.ts +7 -0
  18. package/integrations/git/stageRunners.ts +82 -28
  19. package/integrations/lifecycle/cli.ts +32 -3
  20. package/integrations/lifecycle/doctor.ts +112 -0
  21. package/integrations/mcp/aiGateCheck.ts +2 -11
  22. package/integrations/mcp/preFlightCheck.ts +2 -1
  23. package/integrations/sdd/openSpecCli.ts +12 -3
  24. package/package.json +4 -1
  25. package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
  26. package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
  27. package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
  28. package/scripts/framework-menu-consumer-runtime-actions.ts +86 -6
  29. package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
  30. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
  31. package/scripts/framework-menu-consumer-runtime-lib.ts +2 -0
  32. package/scripts/framework-menu-consumer-runtime-types.ts +3 -1
  33. package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
  34. package/scripts/framework-menu-evidence-summary-read.ts +57 -5
  35. package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
  36. package/scripts/framework-menu-evidence-summary-types.ts +7 -0
  37. package/scripts/framework-menu-gate-lib.ts +9 -0
  38. package/scripts/framework-menu-layout-data.ts +5 -0
  39. package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
  40. package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
  41. package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
  42. package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
  43. package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
  44. package/scripts/framework-menu-system-notifications-macos.ts +4 -0
  45. package/scripts/framework-menu.ts +3 -0
@@ -0,0 +1,61 @@
1
+ import type { Fact } from '../../core/facts/Fact';
2
+
3
+ const normalizePath = (value: string): string => value.replace(/\\/g, '/').replace(/^\/+/, '');
4
+
5
+ export const resolveGateScopePathPrefixesFromEnv = (): string[] => {
6
+ const raw = process.env.PUMUKI_GATE_SCOPE_PATH_PREFIXES?.trim();
7
+ if (!raw) {
8
+ return [];
9
+ }
10
+ return Array.from(
11
+ new Set(
12
+ raw
13
+ .split(/[,;]/)
14
+ .map((segment) => normalizePath(segment.trim()))
15
+ .filter((segment) => segment.length > 0)
16
+ )
17
+ ).sort((a, b) => a.localeCompare(b));
18
+ };
19
+
20
+ const primaryPathForFact = (fact: Fact): string | null => {
21
+ if (fact.kind === 'FileContent' || fact.kind === 'FileChange') {
22
+ return fact.path;
23
+ }
24
+ if (fact.kind === 'Heuristic') {
25
+ return fact.filePath ?? null;
26
+ }
27
+ if (fact.kind === 'Dependency') {
28
+ return fact.from;
29
+ }
30
+ return null;
31
+ };
32
+
33
+ const pathMatchesAnyPrefix = (path: string, prefixes: ReadonlyArray<string>): boolean => {
34
+ const normalized = normalizePath(path);
35
+ for (const prefix of prefixes) {
36
+ if (normalized === prefix) {
37
+ return true;
38
+ }
39
+ const withSlash = prefix.endsWith('/') ? prefix : `${prefix}/`;
40
+ if (normalized.startsWith(withSlash)) {
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+ };
46
+
47
+ export const filterFactsByPathPrefixes = (
48
+ facts: ReadonlyArray<Fact>,
49
+ prefixes: ReadonlyArray<string>
50
+ ): Fact[] => {
51
+ if (prefixes.length === 0) {
52
+ return [...facts];
53
+ }
54
+ return facts.filter((fact) => {
55
+ const primary = primaryPathForFact(fact);
56
+ if (primary === null) {
57
+ return true;
58
+ }
59
+ return pathMatchesAnyPrefix(primary, prefixes);
60
+ });
61
+ };
@@ -35,6 +35,10 @@ import type { TddBddSnapshot } from '../tdd/types';
35
35
  import { resolveSkillsEnforcement } from '../policy/skillsEnforcement';
36
36
  import { applyTddBddEnforcement } from '../policy/tddBddEnforcement';
37
37
  import { collectAiGateRepoPolicyFindings } from './aiGateRepoPolicyFindings';
38
+ import {
39
+ filterFactsByPathPrefixes,
40
+ resolveGateScopePathPrefixesFromEnv,
41
+ } from './filterFactsByPathPrefixes';
38
42
 
39
43
  export type OperationalMemoryShadowRecommendation = {
40
44
  recommendedOutcome: 'ALLOW' | 'WARN' | 'BLOCK';
@@ -911,10 +915,14 @@ export async function runPlatformGate(params: {
911
915
  }
912
916
  }
913
917
 
914
- const facts = await dependencies.resolveFactsForGateScope({
915
- scope: params.scope,
916
- git,
917
- });
918
+ const gateScopePathPrefixes = resolveGateScopePathPrefixesFromEnv();
919
+ const facts = filterFactsByPathPrefixes(
920
+ await dependencies.resolveFactsForGateScope({
921
+ scope: params.scope,
922
+ git,
923
+ }),
924
+ gateScopePathPrefixes
925
+ );
918
926
  const stagedPaths = collectStagedPaths(git, repoRoot);
919
927
  const factsForPlatformEvaluation = shouldAugmentStagedSkillsContractFactsWithRepoFacts({
920
928
  scope: params.scope,
@@ -19,6 +19,10 @@ export type GateScope =
19
19
  kind: 'workingTree';
20
20
  extensions?: string[];
21
21
  }
22
+ | {
23
+ kind: 'unstaged';
24
+ extensions?: string[];
25
+ }
22
26
  | {
23
27
  kind: 'range';
24
28
  fromRef: string;
@@ -66,6 +70,9 @@ export const resolveFactsForGateScope = async (params: {
66
70
  if (params.scope.kind === 'workingTree') {
67
71
  return params.git.getStagedAndUnstagedFacts(extensions);
68
72
  }
73
+ if (params.scope.kind === 'unstaged') {
74
+ return params.git.getUnstagedFacts(extensions);
75
+ }
69
76
 
70
77
  return getFactsForCommitRange({
71
78
  fromRef: params.scope.fromRef,
@@ -20,7 +20,9 @@ import {
20
20
  } from '../notifications/emitAuditSummaryNotification';
21
21
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
22
22
  import { join } from 'node:path';
23
+ import { buildEvidenceOperationalHints } from '../evidence/operationalHints';
23
24
  import { readEvidence, readEvidenceResult } from '../evidence/readEvidence';
25
+ import { writeEvidence } from '../evidence/writeEvidence';
24
26
  import type { EvidenceReadResult } from '../evidence/readEvidence';
25
27
  import type { SnapshotFinding } from '../evidence/schema';
26
28
  import { ensureRuntimeArtifactsIgnored } from '../lifecycle/artifacts';
@@ -31,6 +33,10 @@ import {
31
33
  resolveGitAtomicityEnforcement,
32
34
  type GitAtomicityEnforcementResolution,
33
35
  } from '../policy/gitAtomicityEnforcement';
36
+ import {
37
+ DEFAULT_GATE_REMEDIATION as DEFAULT_BLOCKED_REMEDIATION,
38
+ REMEDIATION_HINT_BY_CODE as BLOCKED_REMEDIATION_BY_CODE,
39
+ } from '../gate/remediationCatalog';
34
40
 
35
41
  const PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE =
36
42
  'pumuki pre-push blocked: branch has no upstream tracking reference. Configure upstream first (for example: git push --set-upstream origin <branch>) and retry.';
@@ -40,39 +46,43 @@ const PRE_PUSH_MANUAL_FALLBACK_MESSAGE =
40
46
  '[pumuki][pre-push] branch has no upstream and stdin is empty; using working-tree fallback scope.';
41
47
  const PRE_PUSH_UPSTREAM_MISALIGNED_AHEAD_THRESHOLD = 5;
42
48
 
49
+ const isTruthyEnvFlag = (value?: string): boolean => {
50
+ if (!value) {
51
+ return false;
52
+ }
53
+ const normalized = value.trim().toLowerCase();
54
+ return normalized === '1' || normalized === 'true' || normalized === 'yes';
55
+ };
56
+
57
+ const isDocumentationOnlyStagedPath = (relativePath: string): boolean => {
58
+ const normalized = relativePath.replace(/\\/g, '/').trim();
59
+ if (normalized.length === 0) {
60
+ return false;
61
+ }
62
+ return /\.(md|mdx)$/i.test(normalized);
63
+ };
64
+
65
+ const shouldSkipRestagingTrackedEvidenceForDocumentationOnlyScope = (params: {
66
+ listStagedIndexPaths: (repoRoot: string) => ReadonlyArray<string>;
67
+ repoRoot: string;
68
+ }): boolean => {
69
+ if (isTruthyEnvFlag(process.env.PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE)) {
70
+ return false;
71
+ }
72
+ const paths = params.listStagedIndexPaths(params.repoRoot).filter(
73
+ (p) => p !== '.ai_evidence.json' && p !== '.AI_EVIDENCE.json'
74
+ );
75
+ if (paths.length === 0) {
76
+ return true;
77
+ }
78
+ return paths.every(isDocumentationOnlyStagedPath);
79
+ };
80
+
43
81
  const PRE_COMMIT_EVIDENCE_MAX_AGE_SECONDS = 900;
44
82
  const PRE_PUSH_EVIDENCE_MAX_AGE_SECONDS = 1800;
45
83
  const HOOK_GATE_PROGRESS_REMINDER_MS = 2000;
46
- const DEFAULT_BLOCKED_REMEDIATION = 'Corrige la causa del bloqueo y vuelve a ejecutar el gate.';
47
84
  const EVIDENCE_FILE_PATH = '.ai_evidence.json';
48
85
 
49
- const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
50
- EVIDENCE_MISSING: 'Regenera .ai_evidence.json ejecutando una auditoría.',
51
- EVIDENCE_INVALID: 'Corrige/regenera .ai_evidence.json y vuelve a ejecutar el gate.',
52
- EVIDENCE_CHAIN_INVALID: 'Regenera evidencia para restaurar la cadena criptográfica.',
53
- EVIDENCE_STAGE_SYNC_FAILED:
54
- 'Sincroniza la evidencia trackeada y reintenta: git add -- .ai_evidence.json && git commit --amend --no-edit',
55
- EVIDENCE_STALE: 'Refresca evidencia antes de continuar.',
56
- EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este mismo repositorio.',
57
- EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual y reintenta.',
58
- EVIDENCE_RULES_COVERAGE_MISSING: 'Ejecuta auditoría completa para recalcular rules_coverage.',
59
- EVIDENCE_RULES_COVERAGE_INCOMPLETE: 'Asegura coverage_ratio=1 y unevaluated=0.',
60
- ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
61
- 'Reconcilia policy/skills y reintenta PRE_COMMIT: npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki-pre-commit',
62
- EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES:
63
- 'Reconcilia policy/skills y revalida PRE_WRITE: npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
64
- GITFLOW_PROTECTED_BRANCH: 'Trabaja en feature/* y evita ramas protegidas.',
65
- EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT:
66
- 'Reduce archivos staged/unstaged por debajo del umbral (o ajusta PUMUKI_PREWRITE_WORKTREE_*); divide el trabajo en commits más pequeños.',
67
- EVIDENCE_PREWRITE_WORKTREE_WARN:
68
- 'El worktree supera el umbral de aviso; reduce alcance antes del siguiente commit/push.',
69
- PRE_PUSH_UPSTREAM_MISSING: 'Ejecuta: git push --set-upstream origin <branch>',
70
- PRE_PUSH_UPSTREAM_MISALIGNED:
71
- 'Alinea upstream con la rama actual: git branch --unset-upstream && git push --set-upstream origin <branch>',
72
- MANIFEST_MUTATION_DETECTED:
73
- 'Los hooks/gates no deben modificar manifests. Revisa wiring y ejecuta upgrade explícito solo cuando aplique (por ejemplo: pumuki update --latest).',
74
- };
75
-
76
86
  const HOOK_POLICY_RECONCILE_CODES = new Set<string>([
77
87
  'SKILLS_PLATFORM_COVERAGE_INCOMPLETE_HIGH',
78
88
  'SKILLS_SCOPE_COMPLIANCE_INCOMPLETE_HIGH',
@@ -123,6 +133,7 @@ type StageRunnerDependencies = {
123
133
  ensureRuntimeArtifactsIgnored: (repoRoot: string) => void;
124
134
  runPolicyReconcile: typeof runPolicyReconcile;
125
135
  isPathTracked: (repoRoot: string, relativePath: string) => boolean;
136
+ listStagedIndexPaths: (repoRoot: string) => ReadonlyArray<string>;
126
137
  stagePath: (repoRoot: string, relativePath: string) => void;
127
138
  resolveHeadOid: (repoRoot: string) => string | null;
128
139
  resolveGitAtomicityEnforcement: () => GitAtomicityEnforcementResolution;
@@ -202,6 +213,13 @@ const defaultDependencies: StageRunnerDependencies = {
202
213
  return false;
203
214
  }
204
215
  },
216
+ listStagedIndexPaths: (repoRoot) => {
217
+ const raw = new GitService().runGit(['diff', '--cached', '--name-only'], repoRoot);
218
+ return raw
219
+ .split('\n')
220
+ .map((line) => line.trim())
221
+ .filter((line) => line.length > 0);
222
+ },
205
223
  stagePath: (repoRoot, relativePath) => {
206
224
  new GitService().runGit(['add', '--', relativePath], repoRoot);
207
225
  },
@@ -489,6 +507,26 @@ const runHookGateWithPolicyRetry = async (params: {
489
507
  }
490
508
  };
491
509
 
510
+ const patchOperationalHintsAfterDocumentationOnlyEvidenceSync = (repoRoot: string): void => {
511
+ const evidenceRead = readEvidenceResult(repoRoot);
512
+ if (evidenceRead.kind !== 'valid') {
513
+ return;
514
+ }
515
+ const evidence = evidenceRead.evidence;
516
+ const hints = buildEvidenceOperationalHints({
517
+ stage: evidence.snapshot.stage,
518
+ outcome: evidence.snapshot.outcome,
519
+ findings: evidence.snapshot.findings,
520
+ rulesCoverage: evidence.snapshot.rules_coverage,
521
+ evaluationMetrics: evidence.snapshot.evaluation_metrics,
522
+ extra: {
523
+ requires_second_pass: true,
524
+ second_pass_reason: 'tracked_evidence_refreshed_on_disk_not_staged_documentation_only_commit',
525
+ },
526
+ });
527
+ writeEvidence({ ...evidence, operational_hints: hints }, { repoRoot });
528
+ };
529
+
492
530
  const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
493
531
  dependencies: StageRunnerDependencies;
494
532
  repoRoot: string;
@@ -500,6 +538,22 @@ const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
500
538
  if (!params.dependencies.isPathTracked(params.repoRoot, EVIDENCE_FILE_PATH)) {
501
539
  return false;
502
540
  }
541
+ if (
542
+ shouldSkipRestagingTrackedEvidenceForDocumentationOnlyScope({
543
+ repoRoot: params.repoRoot,
544
+ listStagedIndexPaths: params.dependencies.listStagedIndexPaths,
545
+ })
546
+ ) {
547
+ if (!params.dependencies.isQuietMode()) {
548
+ process.stderr.write(
549
+ `[pumuki][evidence-sync] tracked ${EVIDENCE_FILE_PATH} updated on disk but not auto-staged (documentation-only staged paths: *.md / *.mdx). ` +
550
+ `Include in this commit if needed: git add -- ${EVIDENCE_FILE_PATH}. ` +
551
+ `Force previous behavior: PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE=1\n`
552
+ );
553
+ }
554
+ patchOperationalHintsAfterDocumentationOnlyEvidenceSync(params.repoRoot);
555
+ return false;
556
+ }
503
557
  try {
504
558
  params.dependencies.stagePath(params.repoRoot, EVIDENCE_FILE_PATH);
505
559
  return false;
@@ -5,6 +5,7 @@ import { runPlatformGate } from '../git/runPlatformGate';
5
5
  import { collectWorktreeAtomicSlices } from '../git/worktreeAtomicSlices';
6
6
  import {
7
7
  doctorHasBlockingIssues,
8
+ doctorHasParityMismatch,
8
9
  runLifecycleDoctor,
9
10
  type LifecycleDoctorReport,
10
11
  } from './doctor';
@@ -115,6 +116,7 @@ export type ParsedArgs = {
115
116
  installMcpAgent?: AdapterAgent;
116
117
  remoteChecks?: boolean;
117
118
  doctorDeep?: boolean;
119
+ doctorParity?: boolean;
118
120
  sddCommand?: SddCommand;
119
121
  loopCommand?: LoopCommand;
120
122
  loopSessionId?: string;
@@ -180,7 +182,7 @@ Pumuki lifecycle commands:
180
182
  pumuki uninstall [--purge-artifacts]
181
183
  pumuki remove
182
184
  pumuki update [--latest|--spec=<package-spec>]
183
- pumuki doctor [--remote-checks] [--deep] [--json]
185
+ pumuki doctor [--remote-checks] [--deep] [--parity] [--json]
184
186
  pumuki status [--json] [--remote-checks]
185
187
  pumuki watch [--stage=PRE_COMMIT|PRE_PUSH|CI] [--scope=workingTree|staged|repoAndStaged|repo] [--severity=critical|high|medium|low] [--interval-ms=<n>] [--notify-cooldown-ms=<n>] [--no-notify] [--once|--iterations=<n>] [--json]
186
188
  pumuki loop run --objective=<text> [--max-attempts=<n>] [--json]
@@ -567,6 +569,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
567
569
  let installMcpAgent: ParsedArgs['installMcpAgent'];
568
570
  let remoteChecks = false;
569
571
  let doctorDeep = false;
572
+ let doctorParity = false;
570
573
  let watchStage: ParsedArgs['watchStage'];
571
574
  let watchScope: ParsedArgs['watchScope'];
572
575
  let watchIntervalMs: ParsedArgs['watchIntervalMs'];
@@ -1395,6 +1398,10 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
1395
1398
  doctorDeep = true;
1396
1399
  continue;
1397
1400
  }
1401
+ if (arg === '--parity') {
1402
+ doctorParity = true;
1403
+ continue;
1404
+ }
1398
1405
  if (arg === '--purge-artifacts') {
1399
1406
  purgeArtifacts = true;
1400
1407
  continue;
@@ -1417,6 +1424,9 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
1417
1424
  if (doctorDeep && commandRaw !== 'doctor') {
1418
1425
  throw new Error(`--deep is only supported with "pumuki doctor".\n\n${HELP_TEXT}`);
1419
1426
  }
1427
+ if (doctorParity && commandRaw !== 'doctor') {
1428
+ throw new Error(`--parity is only supported with "pumuki doctor".\n\n${HELP_TEXT}`);
1429
+ }
1420
1430
  if (commandRaw !== 'bootstrap' && bootstrapEnterprise) {
1421
1431
  throw new Error(`--enterprise is only supported with "pumuki bootstrap".\n\n${HELP_TEXT}`);
1422
1432
  }
@@ -1444,6 +1454,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
1444
1454
  ...(installMcpAgent ? { installMcpAgent } : {}),
1445
1455
  ...(remoteChecks ? { remoteChecks: true } : {}),
1446
1456
  ...(doctorDeep ? { doctorDeep: true } : {}),
1457
+ ...(doctorParity ? { doctorParity: true } : {}),
1447
1458
  };
1448
1459
  };
1449
1460
 
@@ -1516,6 +1527,22 @@ const printDoctorReport = (
1516
1527
  writeInfo(`[pumuki] ${issue.severity.toUpperCase()}: ${issue.message}`);
1517
1528
  }
1518
1529
 
1530
+ if (report.parity_profile) {
1531
+ writeInfo(
1532
+ `[pumuki][doctor][parity] pumuki=${report.parity_profile.pumuki_package_version} bundle=${report.parity_profile.pre_commit_policy_bundle} hash=${report.parity_profile.pre_commit_policy_hash}`
1533
+ );
1534
+ }
1535
+ if (report.parity_comparison) {
1536
+ writeInfo(
1537
+ `[pumuki][doctor][parity] expected_file=${report.parity_comparison.expected_path} matches=${report.parity_comparison.matches ? 'yes' : 'no'}`
1538
+ );
1539
+ for (const mismatch of report.parity_comparison.mismatches) {
1540
+ writeInfo(
1541
+ `[pumuki][doctor][parity] mismatch ${mismatch.field}: expected=${mismatch.expected} actual=${mismatch.actual}`
1542
+ );
1543
+ }
1544
+ }
1545
+
1519
1546
  if (report.deep?.enabled) {
1520
1547
  for (const check of report.deep.checks) {
1521
1548
  writeInfo(
@@ -1527,7 +1554,8 @@ const printDoctorReport = (
1527
1554
  }
1528
1555
  }
1529
1556
 
1530
- const hasBlocking = doctorHasBlockingIssues(report);
1557
+ const hasBlocking =
1558
+ doctorHasBlockingIssues(report) || doctorHasParityMismatch(report);
1531
1559
  const hasWarnings =
1532
1560
  report.issues.length > 0 ||
1533
1561
  report.deep?.checks.some((check) => check.status !== 'pass') === true;
@@ -2225,6 +2253,7 @@ export const runLifecycleCli = async (
2225
2253
  case 'doctor': {
2226
2254
  const report = runLifecycleDoctor({
2227
2255
  deep: parsed.doctorDeep === true,
2256
+ parity: parsed.doctorParity === true,
2228
2257
  });
2229
2258
  const remoteCiDiagnostics = parsed.remoteChecks
2230
2259
  ? activeDependencies.collectRemoteCiDiagnostics({
@@ -2247,7 +2276,7 @@ export const runLifecycleCli = async (
2247
2276
  } else {
2248
2277
  printDoctorReport(report, remoteCiDiagnostics);
2249
2278
  }
2250
- return doctorHasBlockingIssues(report) ? 1 : 0;
2279
+ return doctorHasBlockingIssues(report) || doctorHasParityMismatch(report) ? 1 : 0;
2251
2280
  }
2252
2281
  case 'status': {
2253
2282
  const status = readLifecycleStatus();
@@ -69,6 +69,22 @@ export type DoctorCompatibilityContract = {
69
69
  };
70
70
  };
71
71
 
72
+ export type DoctorParityProfile = {
73
+ schema_version: '1';
74
+ pumuki_package_version: string;
75
+ pre_commit_policy_bundle: string;
76
+ pre_commit_policy_hash: string;
77
+ pre_commit_policy_signature: string | null;
78
+ pre_commit_policy_version: string | null;
79
+ skills_policy_present: boolean;
80
+ };
81
+
82
+ export type DoctorParityComparison = {
83
+ expected_path: string;
84
+ matches: boolean;
85
+ mismatches: ReadonlyArray<{ field: string; expected: string; actual: string }>;
86
+ };
87
+
72
88
  export type LifecycleDoctorReport = {
73
89
  repoRoot: string;
74
90
  packageVersion: string;
@@ -81,6 +97,8 @@ export type LifecycleDoctorReport = {
81
97
  policyValidation: LifecyclePolicyValidationSnapshot;
82
98
  issues: ReadonlyArray<DoctorIssue>;
83
99
  deep?: DoctorDeepReport;
100
+ parity_profile?: DoctorParityProfile;
101
+ parity_comparison?: DoctorParityComparison;
84
102
  };
85
103
 
86
104
  const buildDoctorIssues = (params: {
@@ -698,10 +716,87 @@ const buildDoctorDeepReport = (params: {
698
716
  };
699
717
  };
700
718
 
719
+ const buildDoctorParityProfile = (params: {
720
+ repoRoot: string;
721
+ packageVersion: string;
722
+ }): DoctorParityProfile => {
723
+ const policy = resolvePolicyForStage('PRE_COMMIT', params.repoRoot);
724
+ const skillsPolicyPath = join(params.repoRoot, 'skills.policy.json');
725
+ return {
726
+ schema_version: '1',
727
+ pumuki_package_version: params.packageVersion,
728
+ pre_commit_policy_bundle: policy.trace.bundle,
729
+ pre_commit_policy_hash: policy.trace.hash,
730
+ pre_commit_policy_signature: policy.trace.signature ?? null,
731
+ pre_commit_policy_version: policy.trace.version ?? null,
732
+ skills_policy_present: existsSync(skillsPolicyPath),
733
+ };
734
+ };
735
+
736
+ const compareDoctorParityProfile = (params: {
737
+ repoRoot: string;
738
+ actual: DoctorParityProfile;
739
+ }): DoctorParityComparison | undefined => {
740
+ const expectedPath = join(params.repoRoot, '.pumuki', 'ci-parity-expected.json');
741
+ if (!existsSync(expectedPath)) {
742
+ return undefined;
743
+ }
744
+ let raw: unknown;
745
+ try {
746
+ raw = JSON.parse(readFileSync(expectedPath, 'utf8')) as unknown;
747
+ } catch {
748
+ return {
749
+ expected_path: expectedPath,
750
+ matches: false,
751
+ mismatches: [
752
+ {
753
+ field: 'ci-parity-expected.json',
754
+ expected: 'valid-json',
755
+ actual: 'parse-error',
756
+ },
757
+ ],
758
+ };
759
+ }
760
+ if (!isRecord(raw)) {
761
+ return {
762
+ expected_path: expectedPath,
763
+ matches: false,
764
+ mismatches: [{ field: 'root', expected: 'object', actual: 'non-object' }],
765
+ };
766
+ }
767
+ const mismatches: Array<{ field: string; expected: string; actual: string }> = [];
768
+ const expectField = (field: string, expected: unknown, actual: string) => {
769
+ if (typeof expected === 'string' && expected.trim().length > 0 && expected !== actual) {
770
+ mismatches.push({ field, expected, actual });
771
+ }
772
+ };
773
+ expectField(
774
+ 'pumuki_package_version',
775
+ raw.pumuki_package_version,
776
+ params.actual.pumuki_package_version
777
+ );
778
+ expectField(
779
+ 'pre_commit_policy_hash',
780
+ raw.pre_commit_policy_hash,
781
+ params.actual.pre_commit_policy_hash
782
+ );
783
+ expectField(
784
+ 'pre_commit_policy_bundle',
785
+ raw.pre_commit_policy_bundle,
786
+ params.actual.pre_commit_policy_bundle
787
+ );
788
+ return {
789
+ expected_path: expectedPath,
790
+ matches: mismatches.length === 0,
791
+ mismatches,
792
+ };
793
+ };
794
+
701
795
  export const runLifecycleDoctor = (params?: {
702
796
  cwd?: string;
703
797
  git?: ILifecycleGitService;
704
798
  deep?: boolean;
799
+ parity?: boolean;
705
800
  }): LifecycleDoctorReport => {
706
801
  const git = params?.git ?? new LifecycleGitService();
707
802
  const cwd = params?.cwd ?? process.cwd();
@@ -730,6 +825,18 @@ export const runLifecycleDoctor = (params?: {
730
825
  lifecycleVersion: lifecycleState.version,
731
826
  });
732
827
 
828
+ const parity_profile =
829
+ params?.parity === true
830
+ ? buildDoctorParityProfile({
831
+ repoRoot,
832
+ packageVersion: version.effective,
833
+ })
834
+ : undefined;
835
+ const parity_comparison =
836
+ typeof parity_profile !== 'undefined'
837
+ ? compareDoctorParityProfile({ repoRoot, actual: parity_profile })
838
+ : undefined;
839
+
733
840
  return {
734
841
  repoRoot,
735
842
  packageVersion: version.effective,
@@ -742,8 +849,13 @@ export const runLifecycleDoctor = (params?: {
742
849
  policyValidation: readLifecyclePolicyValidationSnapshot(repoRoot),
743
850
  issues,
744
851
  deep,
852
+ parity_profile,
853
+ parity_comparison,
745
854
  };
746
855
  };
747
856
 
748
857
  export const doctorHasBlockingIssues = (report: LifecycleDoctorReport): boolean =>
749
858
  report.issues.some((issue) => issue.severity === 'error') || report.deep?.blocking === true;
859
+
860
+ export const doctorHasParityMismatch = (report: LifecycleDoctorReport): boolean =>
861
+ typeof report.parity_comparison !== 'undefined' && report.parity_comparison.matches === false;
@@ -1,17 +1,8 @@
1
1
  import { evaluateAiGate, type AiGateStage } from '../gate/evaluateAiGate';
2
+ import { resolveRemediationHintForViolationCode } from '../gate/remediationCatalog';
2
3
  import { resolveLearningContextExperimentalFeature } from '../policy/experimentalFeatures';
3
4
  import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
4
5
 
5
- const AUTO_FIX_BY_CODE: Readonly<Record<string, string>> = {
6
- EVIDENCE_MISSING: 'Ejecuta una auditoría para generar .ai_evidence.json.',
7
- EVIDENCE_INVALID: 'Regenera .ai_evidence.json y vuelve a evaluar.',
8
- EVIDENCE_STALE: 'Refresca evidencia antes de continuar.',
9
- EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual.',
10
- EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este repositorio.',
11
- PRE_PUSH_UPSTREAM_MISSING: 'Ejecuta git push --set-upstream origin <branch>.',
12
- GITFLOW_PROTECTED_BRANCH: 'Crea una rama feature/* y mueve el trabajo allí.',
13
- };
14
-
15
6
  const PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
16
7
 
17
8
  export type EnterpriseAiGateCheckResult = {
@@ -112,7 +103,7 @@ const buildAutoFixes = (
112
103
  if (emittedCodes.has(violation.code)) {
113
104
  continue;
114
105
  }
115
- const fix = AUTO_FIX_BY_CODE[violation.code];
106
+ const fix = resolveRemediationHintForViolationCode(violation.code);
116
107
  if (!fix) {
117
108
  continue;
118
109
  }
@@ -4,7 +4,8 @@ import { resolveLearningContextExperimentalFeature } from '../policy/experimenta
4
4
  import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
5
5
 
6
6
  const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
7
- EVIDENCE_MISSING: 'Ejecuta una auditoría (1/2/3/4) para regenerar .ai_evidence.json.',
7
+ EVIDENCE_MISSING:
8
+ 'Ejecuta una auditoría (1/2/3/4 u opciones de motor 11–14) para regenerar .ai_evidence.json.',
8
9
  EVIDENCE_INVALID: 'Regenera .ai_evidence.json desde una opción de auditoría.',
9
10
  EVIDENCE_INTEGRITY_MISSING: 'Refresca evidencia para regenerar metadatos de integridad.',
10
11
  EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES:
@@ -17,20 +17,29 @@ type OpenSpecCommandResult = {
17
17
  stderr: string;
18
18
  };
19
19
 
20
- const resolveOpenSpecBinary = (repoRoot: string): string => {
20
+ const resolveOpenSpecBinary = (repoRoot: string): string | undefined => {
21
21
  const binaryName = process.platform === 'win32' ? 'openspec.cmd' : 'openspec';
22
22
  const localBinaryPath = join(repoRoot, 'node_modules', '.bin', binaryName);
23
23
  if (existsSync(localBinaryPath)) {
24
24
  return localBinaryPath;
25
25
  }
26
- return 'openspec';
26
+ return undefined;
27
27
  };
28
28
 
29
29
  const runOpenSpecCommand = (
30
30
  args: ReadonlyArray<string>,
31
31
  cwd: string
32
32
  ): OpenSpecCommandResult => {
33
- const result = runSpawnSync(resolveOpenSpecBinary(cwd), [...args], {
33
+ const binary = resolveOpenSpecBinary(cwd);
34
+ if (!binary) {
35
+ return {
36
+ exitCode: 127,
37
+ stdout: '',
38
+ stderr:
39
+ 'OpenSpec CLI not found under repo node_modules/.bin (add @fission-ai/openspec to this repository).',
40
+ };
41
+ }
42
+ const result = runSpawnSync(binary, [...args], {
34
43
  cwd,
35
44
  encoding: 'utf8',
36
45
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.70",
3
+ "version": "6.3.72",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -259,10 +259,13 @@
259
259
  "docs/product/*.md",
260
260
  "docs/rule-packs/*.md",
261
261
  "docs/validation/*.md",
262
+ "docs/tracking/plan-curso-pumuki-stack-my-architecture.md",
262
263
  "assets/**/*",
263
264
  "vendor/skills/**/*",
264
265
  "index.js",
265
266
  "README.md",
267
+ "AGENTS.md",
268
+ "CHANGELOG.md",
266
269
  "LICENSE",
267
270
  "VERSION",
268
271
  "tsconfig.json",