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.
- package/VERSION +1 -1
- package/docs/README.md +9 -7
- package/docs/operations/RELEASE_NOTES.md +0 -18
- package/docs/validation/README.md +3 -1
- package/integrations/evidence/buildEvidence.ts +14 -0
- package/integrations/evidence/repoState.ts +3 -0
- package/integrations/evidence/schema.ts +18 -0
- package/integrations/evidence/trackingContract.ts +146 -0
- package/integrations/evidence/writeEvidence.ts +14 -0
- package/integrations/gate/evaluateAiGate.ts +166 -3
- package/integrations/gate/governanceActionCatalog.ts +45 -0
- package/integrations/gate/remediationCatalog.ts +8 -0
- package/integrations/git/GitService.ts +0 -25
- package/integrations/git/aiGateRepoPolicyFindings.ts +4 -0
- package/integrations/git/runPlatformGateFacts.ts +0 -1
- package/integrations/lifecycle/adapter.templates.json +0 -3
- package/integrations/lifecycle/adapter.ts +24 -0
- package/integrations/lifecycle/bootstrapManifest.ts +248 -0
- package/integrations/lifecycle/cli.ts +30 -68
- package/integrations/lifecycle/cliSdd.ts +4 -3
- package/integrations/lifecycle/doctor.ts +7 -22
- package/integrations/lifecycle/governanceObservationSnapshot.ts +29 -2
- package/integrations/lifecycle/index.ts +0 -2
- package/integrations/lifecycle/install.ts +21 -0
- package/integrations/lifecycle/state.ts +8 -1
- package/integrations/mcp/aiGateCheck.ts +140 -10
- package/integrations/mcp/alignedPlatformGate.ts +232 -0
- package/integrations/mcp/autoExecuteAiStart.ts +6 -1
- package/integrations/mcp/enterpriseServer.ts +6 -6
- package/integrations/mcp/preFlightCheck.ts +10 -0
- package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
- package/integrations/platform/detectPlatforms.ts +0 -37
- package/integrations/policy/experimentalFeatures.ts +1 -1
- package/package.json +1 -10
- package/scripts/consumer-postinstall.cjs +1 -10
- package/AGENTS.md +0 -269
- package/CHANGELOG.md +0 -686
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +0 -62
- package/integrations/lifecycle/audit.ts +0 -101
- package/scripts/consumer-postinstall-resolve-args.cjs +0 -38
- package/scripts/pumuki-full-surface-smoke-lib.ts +0 -37
- package/scripts/pumuki-full-surface-smoke.ts +0 -261
- package/scripts/pumuki-smoke-installed-wrapper.cjs +0 -31
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
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
|
-
- `
|
|
40
|
-
- Quiero
|
|
41
|
-
- `docs/tracking/plan-
|
|
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
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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 "${
|
|
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')
|