pumuki 6.3.72 → 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 (64) hide show
  1. package/docs/README.md +9 -7
  2. package/docs/operations/RELEASE_NOTES.md +0 -7
  3. package/docs/product/USAGE.md +2 -5
  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 +275 -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/runPlatformGate.ts +9 -1
  16. package/integrations/git/runPlatformGateFacts.ts +0 -7
  17. package/integrations/git/runPlatformGateOutput.ts +36 -27
  18. package/integrations/lifecycle/adapter.ts +24 -0
  19. package/integrations/lifecycle/bootstrapManifest.ts +248 -0
  20. package/integrations/lifecycle/cli.ts +45 -11
  21. package/integrations/lifecycle/cliSdd.ts +4 -3
  22. package/integrations/lifecycle/doctor.ts +49 -1
  23. package/integrations/lifecycle/governanceNextAction.ts +164 -0
  24. package/integrations/lifecycle/governanceObservationSnapshot.ts +315 -0
  25. package/integrations/lifecycle/install.ts +21 -0
  26. package/integrations/lifecycle/state.ts +8 -1
  27. package/integrations/lifecycle/status.ts +29 -2
  28. package/integrations/mcp/aiGateCheck.ts +140 -10
  29. package/integrations/mcp/alignedPlatformGate.ts +232 -0
  30. package/integrations/mcp/autoExecuteAiStart.ts +92 -85
  31. package/integrations/mcp/enterpriseServer.ts +6 -6
  32. package/integrations/mcp/preFlightCheck.ts +51 -5
  33. package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
  34. package/integrations/policy/experimentalFeatures.ts +1 -1
  35. package/package.json +2 -4
  36. package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
  37. package/scripts/consumer-menu-matrix-baseline-report-lib.ts +38 -13
  38. package/scripts/framework-menu-consumer-actions-lib.ts +4 -28
  39. package/scripts/framework-menu-consumer-preflight-hints.ts +2 -5
  40. package/scripts/framework-menu-consumer-preflight-render.ts +6 -0
  41. package/scripts/framework-menu-consumer-preflight-run.ts +19 -0
  42. package/scripts/framework-menu-consumer-preflight-types.ts +8 -0
  43. package/scripts/framework-menu-consumer-runtime-actions.ts +6 -86
  44. package/scripts/framework-menu-consumer-runtime-audit.ts +2 -36
  45. package/scripts/framework-menu-consumer-runtime-lib.ts +0 -2
  46. package/scripts/framework-menu-consumer-runtime-types.ts +1 -3
  47. package/scripts/framework-menu-evidence-summary-lib.ts +0 -1
  48. package/scripts/framework-menu-evidence-summary-read.ts +5 -57
  49. package/scripts/framework-menu-evidence-summary-severity.ts +1 -3
  50. package/scripts/framework-menu-evidence-summary-types.ts +0 -7
  51. package/scripts/framework-menu-gate-lib.ts +0 -9
  52. package/scripts/framework-menu-layout-data.ts +0 -5
  53. package/scripts/framework-menu-matrix-baseline-lib.ts +14 -15
  54. package/scripts/framework-menu-matrix-canary-lib.ts +1 -22
  55. package/scripts/framework-menu-matrix-evidence-lib.ts +0 -1
  56. package/scripts/framework-menu-matrix-evidence-types.ts +1 -13
  57. package/scripts/framework-menu-matrix-runner-lib.ts +0 -35
  58. package/scripts/framework-menu-system-notifications-macos.ts +0 -4
  59. package/scripts/framework-menu.ts +0 -3
  60. package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
  61. package/AGENTS.md +0 -269
  62. package/CHANGELOG.md +0 -666
  63. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +0 -111
  64. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +0 -140
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
- - Curso Pumuki (Stack My Architecture): diseño pedagógico + seguimiento de entrega en `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,13 +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-11 (v6.3.72)
10
-
11
- - **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.
12
- - **`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`.
13
- - **Menú / matriz consumer**: opciones motor `11–14`, matriz baseline alineada, vista classic opcional, etc. (ver `CHANGELOG.md`).
14
- - **Rollout**: `pumuki@6.3.72`; `pumuki doctor --json` + repin en consumidores (p. ej. RuralGO).
15
-
16
9
  ### 2026-04-06 (v6.3.71)
17
10
 
18
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.
@@ -130,10 +130,7 @@ Use `A` to switch to `Advanced` mode (full options), and `C` to return to `Consu
130
130
  Advanced mode options include short inline contextual help.
131
131
  Consumer mode is now a minimal read-only shell:
132
132
 
133
- - `1/2/3/4` are the canonical gate flows with **consumer preflight** before evaluation (labels state scope and PRE_COMMIT vs PRE_PUSH).
134
- - `11/12/13/14` run the **engine** with **no preflight**: staged only, unstaged only (index→working tree + untracked), full working tree under **PRE_COMMIT**, or **all tracked files** (full repo). They write `.ai_evidence.json` on **PRE_COMMIT** engine runs like other menu audits.
135
- - After each successful evidence read, the menu prints a **second panel** (“Classic evidence view”) with **ANSI-colored** enterprise/legacy severity counts, optional **platform** rows from `snapshot.platforms`, and a longer ranked violation list. Disable with `PUMUKI_MENU_VINTAGE_REPORT=0`.
136
- - Options **2** and **4** (PRE_PUSH): if outcome is **PASS** or **WARN**, a short hint explains that a **tracked** `.ai_evidence.json` may **not** be rewritten on disk; use `PUMUKI_PRE_PUSH_ALWAYS_WRITE_TRACKED_EVIDENCE=1` for local debugging.
133
+ - `1/2/3/4` are the canonical read-only gate flows
137
134
  - `8` exports the same evidence snapshot in markdown form
138
135
  - `5/6/7/9` remain available only as `Legacy Read-Only Diagnostics`
139
136
 
@@ -252,7 +249,7 @@ Stage mapping:
252
249
  If a scope is empty, the menu prints an explicit operational hint (`Scope vacío`), so `PASS` with zero findings is distinguishable from a clean repository scan.
253
250
 
254
251
  System notifications (macOS) can be enabled from advanced menu option `31` (persisted in `.pumuki/system-notifications.json`).
255
- On non-macOS platforms, the same payloads are written to **stderr** by default (visible in the terminal) because there is no native banner API. Set `PUMUKI_DISABLE_STDERR_NOTIFICATIONS=1` to silence that path (delivery reports `unsupported-platform` on those OSes). On macOS, set `PUMUKI_NOTIFICATION_STDERR_MIRROR=1` to duplicate **any** notification payload to stderr in addition to the system notification. For **`gate.blocked`** specifically, stderr mirroring is **on by default** when the macOS path reports success (so a failed push/commit still prints a `[pumuki]` block in the terminal even if the banner does not appear); disable only that default with `PUMUKI_DISABLE_GATE_BLOCKED_STDERR_MIRROR=1`. The **blocked modal** (Swift floating / AppleScript) with **Desactivar / Silenciar 30 min / Mantener activas** is **on by default** whenever notifications are enabled and `blockedDialogEnabled` is omitted in `.pumuki/system-notifications.json`. Turn it off with `"blockedDialogEnabled": false` or `PUMUKI_MACOS_BLOCKED_DIALOG=0`. **`gate.blocked`** now emits **both** the Notification Center banner (`osascript`) **and** the interactive modal by default, so consumers (e.g. hooks in other repos) still get a visible banner if the modal cannot attach to a GUI session. To restore the previous “modal only” behaviour and suppress the duplicate banner, set `PUMUKI_MACOS_GATE_BLOCKED_BANNER_DEDUPE=1`. If you see **no** notifications at all: confirm `PUMUKI_DISABLE_SYSTEM_NOTIFICATIONS` is unset, delete or fix `.pumuki/system-notifications.json` (absent file defaults to enabled), and ensure the terminal app is allowed to show notifications in **System Settings → Notifications**.
252
+ On non-macOS platforms, the same payloads are written to **stderr** by default (visible in the terminal) because there is no native banner API. Set `PUMUKI_DISABLE_STDERR_NOTIFICATIONS=1` to silence that path (delivery reports `unsupported-platform` on those OSes). On macOS, set `PUMUKI_NOTIFICATION_STDERR_MIRROR=1` to duplicate **any** notification payload to stderr in addition to the system notification. For **`gate.blocked`** specifically, stderr mirroring is **on by default** when the macOS path reports success (so a failed push/commit still prints a `[pumuki]` block in the terminal even if the banner does not appear); disable only that default with `PUMUKI_DISABLE_GATE_BLOCKED_STDERR_MIRROR=1`. The **blocked modal** (Swift floating / AppleScript) with **Desactivar / Silenciar 30 min / Mantener activas** is **on by default** whenever notifications are enabled and `blockedDialogEnabled` is omitted in `.pumuki/system-notifications.json`. Turn it off with `"blockedDialogEnabled": false` or `PUMUKI_MACOS_BLOCKED_DIALOG=0`. Ensure the terminal app is allowed to show notifications in **System Settings → Notifications**.
256
253
  Blocked notifications now use a native Swift floating modal (bottom-right) by default, with AppleScript fallback.
257
254
  Override mode with `PUMUKI_MACOS_BLOCKED_DIALOG_MODE=auto|swift-floating|applescript`.
258
255
  Custom skills import is available in advanced menu option `33` (writes `/.pumuki/custom-rules.json`).
@@ -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');