pumuki 6.3.73 → 6.3.75

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 (43) hide show
  1. package/VERSION +1 -1
  2. package/docs/README.md +9 -7
  3. package/docs/operations/RELEASE_NOTES.md +0 -18
  4. package/docs/validation/README.md +3 -1
  5. package/integrations/evidence/buildEvidence.ts +14 -0
  6. package/integrations/evidence/repoState.ts +3 -0
  7. package/integrations/evidence/schema.ts +18 -0
  8. package/integrations/evidence/trackingContract.ts +146 -0
  9. package/integrations/evidence/writeEvidence.ts +14 -0
  10. package/integrations/gate/evaluateAiGate.ts +166 -3
  11. package/integrations/gate/governanceActionCatalog.ts +45 -0
  12. package/integrations/gate/remediationCatalog.ts +8 -0
  13. package/integrations/git/GitService.ts +0 -25
  14. package/integrations/git/aiGateRepoPolicyFindings.ts +4 -0
  15. package/integrations/git/runPlatformGateFacts.ts +0 -1
  16. package/integrations/lifecycle/adapter.templates.json +0 -3
  17. package/integrations/lifecycle/adapter.ts +24 -0
  18. package/integrations/lifecycle/bootstrapManifest.ts +248 -0
  19. package/integrations/lifecycle/cli.ts +30 -68
  20. package/integrations/lifecycle/cliSdd.ts +4 -3
  21. package/integrations/lifecycle/doctor.ts +7 -22
  22. package/integrations/lifecycle/governanceObservationSnapshot.ts +29 -2
  23. package/integrations/lifecycle/index.ts +0 -2
  24. package/integrations/lifecycle/install.ts +21 -0
  25. package/integrations/lifecycle/state.ts +8 -1
  26. package/integrations/mcp/aiGateCheck.ts +140 -10
  27. package/integrations/mcp/alignedPlatformGate.ts +232 -0
  28. package/integrations/mcp/autoExecuteAiStart.ts +6 -1
  29. package/integrations/mcp/enterpriseServer.ts +6 -6
  30. package/integrations/mcp/preFlightCheck.ts +10 -0
  31. package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
  32. package/integrations/platform/detectPlatforms.ts +0 -37
  33. package/integrations/policy/experimentalFeatures.ts +1 -1
  34. package/package.json +1 -10
  35. package/scripts/consumer-postinstall.cjs +1 -10
  36. package/AGENTS.md +0 -269
  37. package/CHANGELOG.md +0 -686
  38. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +0 -62
  39. package/integrations/lifecycle/audit.ts +0 -101
  40. package/scripts/consumer-postinstall-resolve-args.cjs +0 -38
  41. package/scripts/pumuki-full-surface-smoke-lib.ts +0 -37
  42. package/scripts/pumuki-full-surface-smoke.ts +0 -261
  43. package/scripts/pumuki-smoke-installed-wrapper.cjs +0 -31
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.73
1
+ v6.3.64
package/docs/README.md CHANGED
@@ -36,9 +36,11 @@ Mapa corto y humano de la documentación oficial de Pumuki.
36
36
  - `docs/validation/ios-avdlee-parity-matrix.md`
37
37
 
38
38
  - Quiero saber en qué estamos ahora:
39
- - `docs/tracking/plan-activo-de-trabajo.md`
40
- - Quiero el seguimiento del curso Stack My Architecture (Pumuki), iniciativa formativa aparte del espejo operativo:
41
- - `docs/tracking/plan-curso-pumuki-stack-my-architecture.md`
39
+ - `PUMUKI-RESET-MASTER-PLAN.md` en la raíz del repo como artefacto operativo local y única fuente viva del tracking interno
40
+ - Quiero contexto histórico o materiales de seguimiento retirados:
41
+ - Referencia histórica: `docs/tracking/plan-activo-de-trabajo.md`
42
+ - Quiero el seguimiento del curso Stack My Architecture (Pumuki), iniciativa formativa aparte del espejo operativo:
43
+ - Curso Stack My Architecture: `docs/tracking/plan-curso-pumuki-stack-my-architecture.md`
42
44
 
43
45
  ## Estructura oficial
44
46
 
@@ -65,10 +67,10 @@ Mapa corto y humano de la documentación oficial de Pumuki.
65
67
  - Skills vendorizadas que el repo usa como contrato local.
66
68
 
67
69
  - `docs/tracking/`
68
- - Seguimiento permitido y solo el imprescindible.
69
- - Espejo operativo de producto y consumidores: `docs/tracking/plan-activo-de-trabajo.md` (unica fuente de verdad para ese ambito).
70
- - Iniciativa formativa (curso Pumuki en Stack My Architecture): `docs/tracking/plan-curso-pumuki-stack-my-architecture.md` (no sustituye al plan activo).
71
- - Regla hard: solo puede existir una tarea `🚧` en cada documento de seguimiento que lo use.
70
+ - Material histórico o formativo; no actúa como backlog vivo del producto.
71
+ - Fuente viva del tracking interno: `PUMUKI-RESET-MASTER-PLAN.md` en la raíz del repo.
72
+ - Documento retirado: `docs/tracking/plan-activo-de-trabajo.md`.
73
+ - Material del curso: `docs/tracking/plan-curso-pumuki-stack-my-architecture.md`.
72
74
 
73
75
  ## Fuera de `docs/`
74
76
 
@@ -6,24 +6,6 @@ This file keeps only the operational highlights and rollout notes that matter wh
6
6
 
7
7
  ## 2026-04 (CLI stability and macOS notifications)
8
8
 
9
- ### 2026-04-14 (v6.3.73)
10
-
11
- - **S1 governance console**: `status`, `doctor`, menú consumer, hooks y MCP comparten ya la misma semántica de governance (`governance truth`, `reason_code`, `instruction`, `next_action`).
12
- - **RuralGo evidence pack**: nuevo `npm run validation:ruralgo-s1-evidence-pack -- --consumer-root <repo> --package-version <semver>` para preparar la validación real de `PUMUKI-INC-071/073/076`.
13
- - **Sin regresión de 6.3.72**: esta release preserva `lifecycle audit`, `consumer postinstall resolve args`, `detectPlatforms` y `full surface smoke`, en vez de volver a una base `6.3.71`.
14
- - **Rollout**: publicar `pumuki@6.3.73`, repinear primero RuralGo y ejecutar el evidence pack con la semver publicada antes de mover ningún `INC` a `FIXED`.
15
-
16
- ### 2026-04-11 (v6.3.72)
17
-
18
- - **Tarball npm**: `package.json` → `files` incluye `AGENTS.md`, `CHANGELOG.md` y `docs/tracking/plan-curso-pumuki-stack-my-architecture.md` para lectura canónica vía npm / jsDelivr sin depender solo del repo Git.
19
- - **`gate.blocked` (macOS)**: banner de Notification Center **y** modal por defecto (evita cero notificaciones si el modal no llega a mostrarse desde un hook); dedupe opcional: `PUMUKI_MACOS_GATE_BLOCKED_BANNER_DEDUPE=1`.
20
- - **Modal Swift**: `NSAlert.runModal()` en lugar de panel flotante para que los botones del diálogo respondan de forma fiable.
21
- - **Postinstall consumer**: por defecto `pumuki install --with-mcp --agent=repo` y fusión conservadora en `.pumuki/adapter.json` (`json-merge`); opt-out `PUMUKI_POSTINSTALL_SKIP_MCP=1`.
22
- - **Validación local**: `smoke:pumuki-surface` / `smoke:pumuki-surface-installed` y `validation:local-merge-bar` (sin depender de minutos de Actions). Detalle en `docs/validation/README.md`.
23
- - **Tests en macOS:** `integrations/lifecycle/__tests__/cli.test.ts` evita notificaciones reales del sistema (`PUMUKI_DISABLE_SYSTEM_NOTIFICATIONS` en hooks) para que `npm test` / la barra local no queden colgados en PRE_WRITE strict.
24
- - **Menú / matriz consumer**: opciones motor `11–14`, matriz baseline alineada, vista classic opcional, etc. (ver `CHANGELOG.md`).
25
- - **Rollout**: `pumuki@6.3.72`; `npm publish` cuando el tarball incluya lo anterior; luego `pumuki doctor --json` + repin en consumidores (p. ej. RuralGO).
26
-
27
9
  ### 2026-04-06 (v6.3.71)
28
10
 
29
11
  - **Evidencia v2.1**: bloque `operational_hints` (`requires_second_pass`, resumen operativo, desglose por severidad de reglas). Alineado con PRE_COMMIT solo-docs + evidencia trackeada (INC-069) cuando no se re-stagea el JSON automáticamente.
@@ -14,7 +14,9 @@ Este directorio contiene solo documentación estable de validación y runbooks o
14
14
 
15
15
  ## Estado de seguimiento
16
16
 
17
- - Única fuente de seguimiento: `docs/tracking/plan-activo-de-trabajo.md`
17
+ - `docs/validation/` no gobierna backlog.
18
+ - La única fuente viva del tracking interno es `PUMUKI-RESET-MASTER-PLAN.md` en la raíz del repo.
19
+ - `docs/tracking/plan-activo-de-trabajo.md` queda retirado como espejo operativo y solo conserva valor histórico.
18
20
 
19
21
  ## Política de higiene
20
22
 
@@ -716,6 +716,20 @@ const normalizeRepoState = (repoState?: RepoState): RepoState | undefined => {
716
716
  config_path: repoState.lifecycle.hard_mode.config_path,
717
717
  }
718
718
  : undefined,
719
+ tracking: {
720
+ enforced: repoState.lifecycle.tracking.enforced,
721
+ canonical_path: repoState.lifecycle.tracking.canonical_path,
722
+ canonical_present: repoState.lifecycle.tracking.canonical_present,
723
+ source_file: repoState.lifecycle.tracking.source_file,
724
+ in_progress_count: repoState.lifecycle.tracking.in_progress_count,
725
+ single_in_progress_valid: repoState.lifecycle.tracking.single_in_progress_valid,
726
+ conflict: repoState.lifecycle.tracking.conflict,
727
+ declarations: repoState.lifecycle.tracking.declarations.map((entry) => ({
728
+ source_file: entry.source_file,
729
+ declared_path: entry.declared_path,
730
+ resolved_path: entry.resolved_path,
731
+ })),
732
+ },
719
733
  },
720
734
  };
721
735
  };
@@ -5,6 +5,7 @@ import { readLifecycleStatus } from '../lifecycle/status';
5
5
  import { resolvePumukiVersionMetadata } from '../lifecycle/packageInfo';
6
6
  import { readPersistedHardModeConfig } from '../policy/policyProfiles';
7
7
  import type { RepoHardModeState, RepoHookState, RepoState } from './schema';
8
+ import { readRepoTrackingState } from './trackingContract';
8
9
 
9
10
  type HookStateShape = { exists: boolean; managedBlockPresent: boolean };
10
11
 
@@ -123,6 +124,7 @@ export const captureRepoState = (repoRoot: string): RepoState => {
123
124
  const consumerFacingVersion = versionMetadata.resolvedVersion;
124
125
  const installedVersion = versionMetadata.consumerInstalledVersion;
125
126
  const hardModeState = readHardModeState(repoRoot);
127
+ const trackingState = readRepoTrackingState(repoRoot);
126
128
 
127
129
  return {
128
130
  repo_root: repoRoot,
@@ -150,6 +152,7 @@ export const captureRepoState = (repoRoot: string): RepoState => {
150
152
  pre_push: toHookState(lifecycle.hookStatus['pre-push']),
151
153
  },
152
154
  hard_mode: hardModeState,
155
+ tracking: trackingState,
153
156
  },
154
157
  };
155
158
  };
@@ -171,6 +171,23 @@ export type RepoHardModeState = {
171
171
  config_path: string;
172
172
  };
173
173
 
174
+ export type RepoTrackingDeclaration = {
175
+ source_file: string;
176
+ declared_path: string;
177
+ resolved_path: string;
178
+ };
179
+
180
+ export type RepoTrackingState = {
181
+ enforced: boolean;
182
+ canonical_path: string | null;
183
+ canonical_present: boolean;
184
+ source_file: string | null;
185
+ in_progress_count: number | null;
186
+ single_in_progress_valid: boolean | null;
187
+ conflict: boolean;
188
+ declarations: ReadonlyArray<RepoTrackingDeclaration>;
189
+ };
190
+
174
191
  export type RepoState = {
175
192
  repo_root: string;
176
193
  git: {
@@ -196,6 +213,7 @@ export type RepoState = {
196
213
  pre_push: RepoHookState;
197
214
  };
198
215
  hard_mode?: RepoHardModeState;
216
+ tracking: RepoTrackingState;
199
217
  };
200
218
  };
201
219
 
@@ -0,0 +1,146 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, relative, resolve, sep } from 'node:path';
3
+ import type { RepoTrackingDeclaration, RepoTrackingState } from './schema';
4
+
5
+ const TRACKING_DECLARATION_SOURCES = [
6
+ 'AGENTS.md',
7
+ 'docs/README.md',
8
+ 'docs/validation/README.md',
9
+ 'docs/strategy/README.md',
10
+ ] as const;
11
+
12
+ const TRACKING_KEYWORDS =
13
+ /(seguimiento|tracking interno|tracking can[oó]nico|canonical tracking|fuente viva|fuente can[oó]nica|source of truth|single source)/i;
14
+ const TRACKING_PRIORITY_SOURCE = new Set(['AGENTS.md']);
15
+ const IN_PROGRESS_PATTERNS = [
16
+ /^- Estado:\s*🚧/m,
17
+ /^- 🚧 (\`?P[0-9A-Za-z.-]+\`?)/m,
18
+ /^\|[^|\n]+\|\s*🚧(?:\s|\|)/m,
19
+ ] as const;
20
+
21
+ const toRepoRelativePath = (repoRoot: string, absolutePath: string): string => {
22
+ const relativePath = relative(repoRoot, absolutePath).replace(/\\/g, '/');
23
+ return relativePath.length > 0 && !relativePath.startsWith('..') ? relativePath : absolutePath.replace(/\\/g, '/');
24
+ };
25
+
26
+ const collectDeclaredPathsFromLine = (line: string): string[] => {
27
+ const matches = new Set<string>();
28
+ for (const regex of [/`([^`]+\.md)`/g, /\[[^\]]+\]\(([^)]+\.md)\)/g]) {
29
+ let match: RegExpExecArray | null;
30
+ while ((match = regex.exec(line)) !== null) {
31
+ const candidate = match[1]?.trim();
32
+ if (candidate) {
33
+ matches.add(candidate);
34
+ }
35
+ }
36
+ }
37
+ return [...matches];
38
+ };
39
+
40
+ const collectTrackingDeclarations = (repoRoot: string): ReadonlyArray<RepoTrackingDeclaration> => {
41
+ const declarations: RepoTrackingDeclaration[] = [];
42
+ for (const sourceFile of TRACKING_DECLARATION_SOURCES) {
43
+ const absoluteSourcePath = resolve(repoRoot, sourceFile);
44
+ if (!existsSync(absoluteSourcePath)) {
45
+ continue;
46
+ }
47
+ const content = readFileSync(absoluteSourcePath, 'utf8');
48
+ for (const line of content.split(/\r?\n/)) {
49
+ if (!TRACKING_KEYWORDS.test(line)) {
50
+ continue;
51
+ }
52
+ for (const declaredPath of collectDeclaredPathsFromLine(line)) {
53
+ const resolvedPath = declaredPath.startsWith('./') || declaredPath.startsWith('../')
54
+ ? resolve(repoRoot, dirname(sourceFile), declaredPath)
55
+ : resolve(repoRoot, declaredPath);
56
+ declarations.push({
57
+ source_file: sourceFile,
58
+ declared_path: declaredPath,
59
+ resolved_path: toRepoRelativePath(repoRoot, resolvedPath),
60
+ });
61
+ }
62
+ }
63
+ }
64
+ const deduped = new Map<string, RepoTrackingDeclaration>();
65
+ for (const declaration of declarations) {
66
+ deduped.set(
67
+ `${declaration.source_file}::${declaration.resolved_path}`,
68
+ declaration
69
+ );
70
+ }
71
+ return [...deduped.values()];
72
+ };
73
+
74
+ const resolveCanonicalTrackingPath = (
75
+ declarations: ReadonlyArray<RepoTrackingDeclaration>
76
+ ): { canonicalPath: string | null; sourceFile: string | null; conflict: boolean } => {
77
+ if (declarations.length === 0) {
78
+ return {
79
+ canonicalPath: null,
80
+ sourceFile: null,
81
+ conflict: false,
82
+ };
83
+ }
84
+
85
+ const agentsDeclarations = declarations.filter((entry) => TRACKING_PRIORITY_SOURCE.has(entry.source_file));
86
+ const preferred = agentsDeclarations.length > 0 ? agentsDeclarations : declarations;
87
+ const preferredPaths = [...new Set(preferred.map((entry) => entry.resolved_path))];
88
+ const allPaths = [...new Set(declarations.map((entry) => entry.resolved_path))];
89
+
90
+ if (preferredPaths.length === 0) {
91
+ return {
92
+ canonicalPath: null,
93
+ sourceFile: null,
94
+ conflict: allPaths.length > 1,
95
+ };
96
+ }
97
+
98
+ const canonicalPath = preferredPaths[0] ?? null;
99
+ const sourceFile =
100
+ preferred.find((entry) => entry.resolved_path === canonicalPath)?.source_file
101
+ ?? null;
102
+ return {
103
+ canonicalPath,
104
+ sourceFile,
105
+ conflict: allPaths.length > 1 || preferredPaths.length > 1,
106
+ };
107
+ };
108
+
109
+ const countInProgressMarkers = (absolutePath: string): number => {
110
+ if (!existsSync(absolutePath)) {
111
+ return 0;
112
+ }
113
+ const content = readFileSync(absolutePath, 'utf8');
114
+ const matchedLines = new Set<number>();
115
+ const lines = content.split(/\r?\n/);
116
+ lines.forEach((line, index) => {
117
+ if (IN_PROGRESS_PATTERNS.some((pattern) => pattern.test(line))) {
118
+ matchedLines.add(index);
119
+ }
120
+ });
121
+ return matchedLines.size;
122
+ };
123
+
124
+ export const readRepoTrackingState = (repoRoot: string): RepoTrackingState => {
125
+ const declarations = collectTrackingDeclarations(repoRoot);
126
+ const enforced = declarations.length > 0;
127
+ const resolution = resolveCanonicalTrackingPath(declarations);
128
+ const canonicalAbsolutePath = resolution.canonicalPath
129
+ ? resolve(repoRoot, resolution.canonicalPath.split('/').join(sep))
130
+ : null;
131
+ const canonicalPresent = canonicalAbsolutePath ? existsSync(canonicalAbsolutePath) : false;
132
+ const inProgressCount = canonicalPresent && canonicalAbsolutePath
133
+ ? countInProgressMarkers(canonicalAbsolutePath)
134
+ : null;
135
+
136
+ return {
137
+ enforced,
138
+ canonical_path: resolution.canonicalPath,
139
+ canonical_present: canonicalPresent,
140
+ source_file: resolution.sourceFile,
141
+ in_progress_count: inProgressCount,
142
+ single_in_progress_valid: inProgressCount === null ? null : inProgressCount === 1,
143
+ conflict: resolution.conflict,
144
+ declarations,
145
+ };
146
+ };
@@ -217,6 +217,20 @@ const normalizeRepoState = (
217
217
  config_path: toRelativeRepoPath(repoRoot, repoState.lifecycle.hard_mode.config_path),
218
218
  }
219
219
  : undefined,
220
+ tracking: {
221
+ enforced: repoState.lifecycle.tracking.enforced,
222
+ canonical_path: repoState.lifecycle.tracking.canonical_path,
223
+ canonical_present: repoState.lifecycle.tracking.canonical_present,
224
+ source_file: repoState.lifecycle.tracking.source_file,
225
+ in_progress_count: repoState.lifecycle.tracking.in_progress_count,
226
+ single_in_progress_valid: repoState.lifecycle.tracking.single_in_progress_valid,
227
+ conflict: repoState.lifecycle.tracking.conflict,
228
+ declarations: repoState.lifecycle.tracking.declarations.map((entry) => ({
229
+ source_file: entry.source_file,
230
+ declared_path: entry.declared_path,
231
+ resolved_path: entry.resolved_path,
232
+ })),
233
+ },
220
234
  },
221
235
  };
222
236
  };
@@ -1,11 +1,12 @@
1
1
  import type { EvidenceReadResult } from '../evidence/readEvidence';
2
2
  import { readEvidenceResult } from '../evidence/readEvidence';
3
3
  import { captureRepoState } from '../evidence/repoState';
4
- import type { RepoState } from '../evidence/schema';
4
+ import type { RepoState, RepoTrackingState } from '../evidence/schema';
5
5
  import { resolvePolicyForStage } from './stagePolicies';
6
6
  import { execFileSync } from 'node:child_process';
7
7
  import { existsSync, realpathSync } from 'node:fs';
8
8
  import { resolve } from 'node:path';
9
+ import { isSeverityAtLeast } from '../../core/rules/Severity';
9
10
  import type { SkillsLockV1, SkillsStage } from '../config/skillsLock';
10
11
  import {
11
12
  loadEffectiveSkillsLock,
@@ -132,6 +133,10 @@ const PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_W
132
133
  const PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_BLOCK_THRESHOLD';
133
134
 
134
135
  const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
136
+ const DEFAULT_GITFLOW_BRANCH_PATTERNS = [
137
+ /^(?:feature|bugfix|hotfix|chore|refactor|docs)\/[a-z0-9]+(?:-[a-z0-9]+)*$/,
138
+ /^release\/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/,
139
+ ] as const;
135
140
  const PREWRITE_SKILLS_PLATFORMS = ['ios', 'android', 'backend', 'frontend'] as const;
136
141
  type PreWriteSkillsPlatform = (typeof PREWRITE_SKILLS_PLATFORMS)[number];
137
142
  const PLATFORM_SKILLS_RULE_PREFIXES: Readonly<Record<PreWriteSkillsPlatform, string>> = {
@@ -457,6 +462,27 @@ const hasWorktreeCodePlatforms = (params: {
457
462
  );
458
463
  };
459
464
 
465
+ const toWorktreeDetectedPlatforms = (params: {
466
+ repoRoot: string;
467
+ requiredPlatforms: ReadonlyArray<PreWriteSkillsPlatform>;
468
+ }): ReadonlyArray<PreWriteSkillsPlatform> => {
469
+ const changedPaths = collectWorktreeChangedPaths(params.repoRoot);
470
+ if (changedPaths.length === 0) {
471
+ return [];
472
+ }
473
+
474
+ const detected = new Set<PreWriteSkillsPlatform>();
475
+ for (const filePath of changedPaths) {
476
+ for (const platform of params.requiredPlatforms) {
477
+ if (isPlatformPath(platform, filePath)) {
478
+ detected.add(platform);
479
+ }
480
+ }
481
+ }
482
+
483
+ return PREWRITE_SKILLS_PLATFORMS.filter((platform) => detected.has(platform));
484
+ };
485
+
460
486
  const toLockRequiredPlatforms = (
461
487
  requiredLock: SkillsLockV1 | undefined
462
488
  ): ReadonlyArray<PreWriteSkillsPlatform> => {
@@ -748,6 +774,13 @@ const toSkillsContractAssessment = (params: {
748
774
  const coverage = params.evidenceResult.evidence.snapshot.rules_coverage;
749
775
  const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.evidenceResult.evidence.platforms);
750
776
  const inferredPlatforms = toCoverageInferredPlatforms(coverage);
777
+ const worktreeDetectedPlatforms =
778
+ params.stage === 'PRE_WRITE' && requiredPlatforms.length > 0
779
+ ? toWorktreeDetectedPlatforms({
780
+ repoRoot: params.repoRoot,
781
+ requiredPlatforms,
782
+ })
783
+ : [];
751
784
  const repoTreeDetectedPlatforms =
752
785
  params.stage !== 'PRE_WRITE' && requiredPlatforms.length > 0
753
786
  ? toRepoTreeDetectedPlatforms({
@@ -767,6 +800,8 @@ const toSkillsContractAssessment = (params: {
767
800
  ? explicitlyDetectedEffectivePlatforms
768
801
  : inferredPlatforms.length > 0
769
802
  ? inferredPlatforms
803
+ : worktreeDetectedPlatforms.length > 0
804
+ ? worktreeDetectedPlatforms
770
805
  : repoTreeDetectedPlatforms;
771
806
  const pendingChanges = resolvePendingChanges(params.repoState);
772
807
  const detectedPlatformSet = new Set(detectedPlatforms);
@@ -1164,6 +1199,63 @@ const collectEvidenceViolations = (
1164
1199
  return { violations, ageSeconds };
1165
1200
  };
1166
1201
 
1202
+ const severityOrder: ReadonlyArray<'CRITICAL' | 'ERROR' | 'WARN' | 'INFO'> = [
1203
+ 'CRITICAL',
1204
+ 'ERROR',
1205
+ 'WARN',
1206
+ 'INFO',
1207
+ ];
1208
+
1209
+ const toHighestTriggeredSeverity = (
1210
+ severityCounts: Readonly<Record<'INFO' | 'WARN' | 'ERROR' | 'CRITICAL', number>>,
1211
+ threshold: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL'
1212
+ ): 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL' | null => {
1213
+ for (const severity of severityOrder) {
1214
+ if (severityCounts[severity] > 0 && isSeverityAtLeast(severity, threshold)) {
1215
+ return severity;
1216
+ }
1217
+ }
1218
+ return null;
1219
+ };
1220
+
1221
+ const collectEvidencePolicyThresholdViolations = (params: {
1222
+ evidenceResult: EvidenceReadResult;
1223
+ policy: ReturnType<typeof resolvePolicyForStage>['policy'];
1224
+ }): AiGateViolation[] => {
1225
+ if (params.evidenceResult.kind !== 'valid') {
1226
+ return [];
1227
+ }
1228
+
1229
+ const severityCounts = params.evidenceResult.evidence.severity_metrics.by_severity;
1230
+ const blockSeverity = toHighestTriggeredSeverity(
1231
+ severityCounts,
1232
+ params.policy.blockOnOrAbove
1233
+ );
1234
+ if (blockSeverity && params.evidenceResult.evidence.ai_gate.status !== 'BLOCKED') {
1235
+ return [
1236
+ toErrorViolation(
1237
+ 'EVIDENCE_POLICY_THRESHOLD_BLOCK',
1238
+ `Evidence severities exceed block_on_or_above=${params.policy.blockOnOrAbove} (highest=${blockSeverity}).`
1239
+ ),
1240
+ ];
1241
+ }
1242
+
1243
+ const warnSeverity = toHighestTriggeredSeverity(
1244
+ severityCounts,
1245
+ params.policy.warnOnOrAbove
1246
+ );
1247
+ if (warnSeverity && params.evidenceResult.evidence.ai_gate.status === 'ALLOWED') {
1248
+ return [
1249
+ toWarnViolation(
1250
+ 'EVIDENCE_POLICY_THRESHOLD_WARN',
1251
+ `Evidence severities exceed warn_on_or_above=${params.policy.warnOnOrAbove} (highest=${warnSeverity}).`
1252
+ ),
1253
+ ];
1254
+ }
1255
+
1256
+ return [];
1257
+ };
1258
+
1167
1259
  const toEvidenceSourceDescriptor = (
1168
1260
  result: EvidenceReadResult,
1169
1261
  repoRoot: string
@@ -1193,17 +1285,81 @@ const collectGitflowViolations = (
1193
1285
  if (!repoState.git.available) {
1194
1286
  return violations;
1195
1287
  }
1196
- if (repoState.git.branch && protectedBranches.has(repoState.git.branch)) {
1288
+ const branch = repoState.git.branch?.trim() ?? null;
1289
+ const normalizedBranch = branch?.toLowerCase() ?? null;
1290
+ if (branch && normalizedBranch && protectedBranches.has(normalizedBranch)) {
1197
1291
  violations.push(
1198
1292
  toErrorViolation(
1199
1293
  'GITFLOW_PROTECTED_BRANCH',
1200
- `Direct work on protected branch "${repoState.git.branch}" is not allowed.`
1294
+ `Direct work on protected branch "${branch}" is not allowed.`
1295
+ )
1296
+ );
1297
+ return violations;
1298
+ }
1299
+ if (
1300
+ branch
1301
+ && !DEFAULT_GITFLOW_BRANCH_PATTERNS.some((pattern) => pattern.test(branch))
1302
+ ) {
1303
+ violations.push(
1304
+ toErrorViolation(
1305
+ 'GITFLOW_BRANCH_NAMING_INVALID',
1306
+ `Branch "${branch}" does not comply with GitFlow naming. Use feature/*, bugfix/*, hotfix/*, release/*, chore/*, refactor/* or docs/*.`
1201
1307
  )
1202
1308
  );
1203
1309
  }
1204
1310
  return violations;
1205
1311
  };
1206
1312
 
1313
+ const DEFAULT_TRACKING_STATE: RepoTrackingState = {
1314
+ enforced: false,
1315
+ canonical_path: null,
1316
+ canonical_present: false,
1317
+ source_file: null,
1318
+ in_progress_count: null,
1319
+ single_in_progress_valid: null,
1320
+ conflict: false,
1321
+ declarations: [],
1322
+ };
1323
+
1324
+ const collectTrackingViolations = (repoState: RepoState): AiGateViolation[] => {
1325
+ const tracking = repoState.lifecycle.tracking ?? DEFAULT_TRACKING_STATE;
1326
+ if (!tracking.enforced) {
1327
+ return [];
1328
+ }
1329
+
1330
+ if (tracking.conflict) {
1331
+ const declaredPaths = tracking.declarations
1332
+ .map((entry) => `${entry.source_file}:${entry.resolved_path}`)
1333
+ .join(', ');
1334
+ return [
1335
+ toErrorViolation(
1336
+ 'TRACKING_CANONICAL_SOURCE_CONFLICT',
1337
+ `Tracking canonical source conflict detected (${declaredPaths}).`
1338
+ ),
1339
+ ];
1340
+ }
1341
+
1342
+ if (!tracking.canonical_path || !tracking.canonical_present) {
1343
+ return [
1344
+ toErrorViolation(
1345
+ 'TRACKING_CANONICAL_FILE_MISSING',
1346
+ `Tracking canonical file is missing (${tracking.canonical_path ?? 'undeclared'}).`
1347
+ ),
1348
+ ];
1349
+ }
1350
+
1351
+ if (tracking.single_in_progress_valid === false) {
1352
+ return [
1353
+ toErrorViolation(
1354
+ 'TRACKING_CANONICAL_IN_PROGRESS_INVALID',
1355
+ `Tracking canonical file must contain exactly one in-progress task (count=${tracking.in_progress_count ?? 'n/a'}).`
1356
+ ),
1357
+ ];
1358
+ }
1359
+
1360
+ return [];
1361
+ };
1362
+
1207
1363
  const resolvePendingChanges = (repoState: RepoState): number | null => {
1208
1364
  if (!repoState.git.available) {
1209
1365
  return null;
@@ -1422,6 +1578,7 @@ export const evaluateAiGate = (
1422
1578
  readMcpAiGateReceipt: activeDependencies.readMcpAiGateReceipt,
1423
1579
  });
1424
1580
  const gitflowViolations = collectGitflowViolations(repoState, protectedBranches);
1581
+ const trackingViolations = collectTrackingViolations(repoState);
1425
1582
  const skillsContract = toSkillsContractAssessment({
1426
1583
  stage: params.stage,
1427
1584
  repoRoot: params.repoRoot,
@@ -1445,10 +1602,16 @@ export const evaluateAiGate = (
1445
1602
  `Skills contract incomplete for ${params.stage}: ${skillsContract.violations.map((violation) => violation.code).join(', ')}.`
1446
1603
  ),
1447
1604
  ];
1605
+ const policyThresholdViolations = collectEvidencePolicyThresholdViolations({
1606
+ evidenceResult,
1607
+ policy: resolvedPolicy.policy,
1608
+ });
1448
1609
  const violations = [
1449
1610
  ...evidenceAssessment.violations,
1611
+ ...policyThresholdViolations,
1450
1612
  ...stageSkillsContractViolations,
1451
1613
  ...gitflowViolations,
1614
+ ...trackingViolations,
1452
1615
  ...mcpReceiptAssessment.violations,
1453
1616
  ];
1454
1617
  const blocked = violations.some((violation) => violation.severity === 'ERROR');
@@ -119,6 +119,51 @@ export const resolveGovernanceCatalogAction = (params: {
119
119
  command: 'git checkout -b feature/<descripcion-kebab-case>',
120
120
  },
121
121
  };
122
+ case 'GITFLOW_BRANCH_NAMING_INVALID':
123
+ case 'GITFLOW_BRANCH_NAMING_INVALID_CONTEXT':
124
+ return {
125
+ reason_code: 'GITFLOW_BRANCH_NAMING_INVALID_CONTEXT',
126
+ instruction:
127
+ 'Renombra o recrea la rama actual con un prefijo GitFlow válido antes de continuar.',
128
+ next_action: {
129
+ kind: 'run_command',
130
+ message: 'Crea una rama válida y mueve el trabajo a esa rama antes de seguir.',
131
+ command: 'git checkout -b feature/<descripcion-kebab-case>',
132
+ },
133
+ };
134
+ case 'TRACKING_CANONICAL_SOURCE_CONFLICT':
135
+ return {
136
+ reason_code: 'TRACKING_CANONICAL_SOURCE_CONFLICT',
137
+ instruction:
138
+ 'Alinea AGENTS.md y los README canónicos para que todos apunten al mismo MD de seguimiento.',
139
+ next_action: {
140
+ kind: 'info',
141
+ message:
142
+ 'Deja una única fuente canónica de tracking y elimina referencias legacy o contradictorias.',
143
+ },
144
+ };
145
+ case 'TRACKING_CANONICAL_FILE_MISSING':
146
+ return {
147
+ reason_code: 'TRACKING_CANONICAL_FILE_MISSING',
148
+ instruction:
149
+ 'Crea o restaura el MD de seguimiento canónico declarado por el repo antes de continuar.',
150
+ next_action: {
151
+ kind: 'info',
152
+ message:
153
+ 'Restaura el archivo canónico de tracking y vuelve a validar governance.',
154
+ },
155
+ };
156
+ case 'TRACKING_CANONICAL_IN_PROGRESS_INVALID':
157
+ return {
158
+ reason_code: 'TRACKING_CANONICAL_IN_PROGRESS_INVALID',
159
+ instruction:
160
+ 'El tracking canónico debe tener exactamente una tarea o fase en construcción.',
161
+ next_action: {
162
+ kind: 'info',
163
+ message:
164
+ 'Corrige el MD canónico para dejar exactamente una `🚧` antes de continuar.',
165
+ },
166
+ };
122
167
  case 'POLICY_STAGE_NOT_STRICT':
123
168
  return {
124
169
  reason_code: 'POLICY_STAGE_NOT_STRICT',
@@ -17,6 +17,14 @@ export const REMEDIATION_HINT_BY_CODE: Readonly<Record<string, string>> = {
17
17
  EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES:
18
18
  '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',
19
19
  GITFLOW_PROTECTED_BRANCH: 'Trabaja en feature/* y evita ramas protegidas.',
20
+ GITFLOW_BRANCH_NAMING_INVALID:
21
+ 'Renombra o recrea la rama con un prefijo GitFlow válido (feature/*, bugfix/*, hotfix/*, release/*, chore/*, refactor/* o docs/*).',
22
+ TRACKING_CANONICAL_SOURCE_CONFLICT:
23
+ 'Alinea AGENTS.md y los README canónicos para que todos apunten al mismo MD de seguimiento.',
24
+ TRACKING_CANONICAL_FILE_MISSING:
25
+ 'Crea o restaura el archivo canónico de tracking declarado por el repo.',
26
+ TRACKING_CANONICAL_IN_PROGRESS_INVALID:
27
+ 'Deja exactamente una tarea o fase `🚧` en el MD canónico de seguimiento antes de continuar.',
20
28
  EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT:
21
29
  'Reduce archivos staged/unstaged por debajo del umbral (o ajusta PUMUKI_PREWRITE_WORKTREE_*); divide el trabajo en commits más pequeños.',
22
30
  EVIDENCE_PREWRITE_WORKTREE_WARN:
@@ -9,7 +9,6 @@ export { parseNameStatus } from './gitDiffUtils';
9
9
  export interface IGitService {
10
10
  runGit(args: ReadonlyArray<string>, cwd?: string): string;
11
11
  getStagedFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact>;
12
- getUnstagedFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact>;
13
12
  getRepoFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact>;
14
13
  getRepoAndStagedFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact>;
15
14
  getStagedAndUnstagedFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact>;
@@ -45,30 +44,6 @@ export class GitService implements IGitService {
45
44
  );
46
45
  }
47
46
 
48
- getUnstagedFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact> {
49
- const nameStatus = this.runGit(['diff', '--name-status']);
50
- const changes = parseNameStatus(nameStatus).filter((change) =>
51
- hasAllowedExtension(change.path, extensions)
52
- );
53
- const untrackedPaths = this.runGit(['ls-files', '--others', '--exclude-standard'])
54
- .split('\n')
55
- .map((line) => line.trim())
56
- .filter((line) => line.length > 0)
57
- .filter((path) => hasAllowedExtension(path, extensions));
58
- const unstagedPaths = new Set(changes.map((change) => change.path));
59
- const untrackedChanges = untrackedPaths
60
- .filter((path) => !unstagedPaths.has(path))
61
- .map((path) => ({
62
- path,
63
- changeType: 'added' as const,
64
- }));
65
- const mergedChanges = [...changes, ...untrackedChanges];
66
- const repoRoot = this.resolveRepoRoot();
67
- return buildFactsFromChanges(mergedChanges, 'git:unstaged', (filePath) =>
68
- this.readWorkingTreeFile(repoRoot, filePath)
69
- );
70
- }
71
-
72
47
  getRepoFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact> {
73
48
  const trackedFiles = this.runGit(['ls-files'])
74
49
  .split('\n')