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.
- package/docs/README.md +9 -7
- package/docs/operations/RELEASE_NOTES.md +0 -7
- package/docs/product/USAGE.md +2 -5
- 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 +275 -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/runPlatformGate.ts +9 -1
- package/integrations/git/runPlatformGateFacts.ts +0 -7
- package/integrations/git/runPlatformGateOutput.ts +36 -27
- package/integrations/lifecycle/adapter.ts +24 -0
- package/integrations/lifecycle/bootstrapManifest.ts +248 -0
- package/integrations/lifecycle/cli.ts +45 -11
- package/integrations/lifecycle/cliSdd.ts +4 -3
- package/integrations/lifecycle/doctor.ts +49 -1
- package/integrations/lifecycle/governanceNextAction.ts +164 -0
- package/integrations/lifecycle/governanceObservationSnapshot.ts +315 -0
- package/integrations/lifecycle/install.ts +21 -0
- package/integrations/lifecycle/state.ts +8 -1
- package/integrations/lifecycle/status.ts +29 -2
- package/integrations/mcp/aiGateCheck.ts +140 -10
- package/integrations/mcp/alignedPlatformGate.ts +232 -0
- package/integrations/mcp/autoExecuteAiStart.ts +92 -85
- package/integrations/mcp/enterpriseServer.ts +6 -6
- package/integrations/mcp/preFlightCheck.ts +51 -5
- package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
- package/integrations/policy/experimentalFeatures.ts +1 -1
- package/package.json +2 -4
- package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
- package/scripts/consumer-menu-matrix-baseline-report-lib.ts +38 -13
- package/scripts/framework-menu-consumer-actions-lib.ts +4 -28
- package/scripts/framework-menu-consumer-preflight-hints.ts +2 -5
- package/scripts/framework-menu-consumer-preflight-render.ts +6 -0
- package/scripts/framework-menu-consumer-preflight-run.ts +19 -0
- package/scripts/framework-menu-consumer-preflight-types.ts +8 -0
- package/scripts/framework-menu-consumer-runtime-actions.ts +6 -86
- package/scripts/framework-menu-consumer-runtime-audit.ts +2 -36
- package/scripts/framework-menu-consumer-runtime-lib.ts +0 -2
- package/scripts/framework-menu-consumer-runtime-types.ts +1 -3
- package/scripts/framework-menu-evidence-summary-lib.ts +0 -1
- package/scripts/framework-menu-evidence-summary-read.ts +5 -57
- package/scripts/framework-menu-evidence-summary-severity.ts +1 -3
- package/scripts/framework-menu-evidence-summary-types.ts +0 -7
- package/scripts/framework-menu-gate-lib.ts +0 -9
- package/scripts/framework-menu-layout-data.ts +0 -5
- package/scripts/framework-menu-matrix-baseline-lib.ts +14 -15
- package/scripts/framework-menu-matrix-canary-lib.ts +1 -22
- package/scripts/framework-menu-matrix-evidence-lib.ts +0 -1
- package/scripts/framework-menu-matrix-evidence-types.ts +1 -13
- package/scripts/framework-menu-matrix-runner-lib.ts +0 -35
- package/scripts/framework-menu-system-notifications-macos.ts +0 -4
- package/scripts/framework-menu.ts +0 -3
- package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
- package/AGENTS.md +0 -269
- package/CHANGELOG.md +0 -666
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +0 -111
- 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
|
-
- `
|
|
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,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.
|
package/docs/product/USAGE.md
CHANGED
|
@@ -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
|
|
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`.
|
|
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
|
-
-
|
|
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');
|