pumuki 6.3.65 → 6.3.69
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/README.md +1 -1
- package/docs/operations/RELEASE_NOTES.md +18 -0
- package/docs/product/CONFIGURATION.md +2 -2
- package/docs/product/INSTALLATION.md +2 -0
- package/docs/product/USAGE.md +1 -1
- package/integrations/gate/evaluateAiGate.ts +29 -19
- package/integrations/git/aiGateRepoPolicyFindings.ts +46 -0
- package/integrations/git/runPlatformGate.ts +23 -10
- package/integrations/git/stageRunners.ts +5 -0
- package/integrations/lifecycle/adapter.templates.json +57 -0
- package/integrations/lifecycle/hookBlock.ts +56 -34
- package/integrations/lifecycle/install.ts +20 -0
- package/package.json +2 -1
- package/scripts/framework-menu-system-notifications-config-choice.ts +22 -3
- package/scripts/framework-menu-system-notifications-config-state.ts +10 -3
- package/scripts/framework-menu-system-notifications-dispatch.ts +19 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-effect.ts +2 -1
- package/scripts/framework-menu-system-notifications-macos-runner-parse.ts +8 -3
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ npx --yes pumuki status
|
|
|
37
37
|
npx --yes pumuki doctor --json
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta
|
|
40
|
+
Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta **`pumuki install` solo** (hooks `pre-commit` / `pre-push`, lifecycle, evidencia cuando aplica). **Pumuki no depende de ningún IDE** para el baseline: no toca `.cursor/` ni otros ficheros de editor por defecto. Desde **6.3.68**, cada hook gestionado ejecuta **`pumuki-pre-write` antes** de `pumuki-pre-commit` / `pumuki-pre-push` (stage **PRE_WRITE** vía Git). Saltar solo PRE_WRITE en hooks: `PUMUKI_SKIP_CHAINED_PRE_WRITE=1`. Desde **6.3.69**, esos mismos hooks aplican también **git-flow en ramas protegidas** (`GITFLOW_PROTECTED_BRANCH`) e **higiene de worktree** (`PUMUKI_PREWRITE_WORKTREE_*` / códigos `EVIDENCE_PREWRITE_WORKTREE_*`) cuando la evidencia es válida; el **modal macOS** de bloqueo (Desactivar / Silenciar 30 min / Mantener activas) queda **activo por defecto** si las notificaciones están habilitadas (`"blockedDialogEnabled": false` o `PUMUKI_MACOS_BLOCKED_DIALOG=0` para apagarlo). Tras install, si no existía, aparece **`.pumuki/adapter.json`** con los comandos de hooks y **MCP stdio** para referencia o para clientes que los registren manualmente. Para generar también config de IDE: **`pumuki install --with-mcp --agent=cursor`** (u otro) o **`pumuki bootstrap --enterprise --agent=…`**. Desactivar el postinstall: `PUMUKI_SKIP_POSTINSTALL=1`. En CI suele saltarse solo (`CI=true`). En **6.3.64+**, las notificaciones del sistema en plataformas sin banner nativo se reflejan en **stderr** por defecto (`PUMUKI_DISABLE_STDERR_NOTIFICATIONS=1` para silenciarlas). En **6.3.69+**, un `gate.blocked` en macOS también deja una copia en **stderr** por defecto (`PUMUKI_DISABLE_GATE_BLOCKED_STDERR_MIRROR=1` para desactivar solo eso).
|
|
41
41
|
|
|
42
42
|
Fallback (equivalent in pasos separados):
|
|
43
43
|
|
|
@@ -6,6 +6,24 @@ 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-05 (v6.3.69)
|
|
10
|
+
|
|
11
|
+
- **Hooks = política de repo**: `PRE_COMMIT` / `PRE_PUSH` / `CI` / `PRE_WRITE` incorporan **`GITFLOW_PROTECTED_BRANCH`** y **higiene de worktree** (`EVIDENCE_PREWRITE_WORKTREE_*`, env `PUMUKI_PREWRITE_WORKTREE_*`) vía fusión con `evaluateAiGate` en `runPlatformGate`.
|
|
12
|
+
- **macOS bloqueos**: modal **Desactivar / Silenciar 30 min / Mantener activas** **on by default** si las notificaciones están habilitadas; normalización de etiquetas y parseo `osascript` más robusto; `gate.blocked` duplica payload en **stderr** por defecto (`PUMUKI_DISABLE_GATE_BLOCKED_STDERR_MIRROR=1` para opt-out).
|
|
13
|
+
- **Rollout**: `pumuki@6.3.69`, **`pumuki install`** en consumidores si quieren hooks reescritos tras cambios previos; revisar `.pumuki/system-notifications.json` si asumías modal apagado sin clave `blockedDialogEnabled`.
|
|
14
|
+
|
|
15
|
+
### 2026-04-06 (v6.3.68)
|
|
16
|
+
|
|
17
|
+
- **PRE_WRITE sin depender del IDE**: `pre-commit` y `pre-push` gestionados ejecutan **`pumuki-pre-write`** antes del gate del stage principal. Opt-out: `PUMUKI_SKIP_CHAINED_PRE_WRITE=1`.
|
|
18
|
+
- **`.pumuki/adapter.json`**: se crea en `pumuki install` si faltaba, con comandos de **MCP stdio** y hooks (plantilla `repo`); sigue sin imponer Cursor ni otros IDEs.
|
|
19
|
+
- Rollout: `pumuki@6.3.68` y **`pumuki install`** en consumidores para reescribir hooks.
|
|
20
|
+
|
|
21
|
+
### 2026-04-06 (v6.3.67)
|
|
22
|
+
|
|
23
|
+
- **Corrección de producto**: el `postinstall` **no** acopla Pumuki a Cursor ni a ningún IDE. Vuelve a ejecutar solo **`pumuki install`** (baseline Git + lifecycle). IDE/MCP: opt-in con `pumuki install --with-mcp --agent=…` o `bootstrap --enterprise`.
|
|
24
|
+
- La plantilla adaptador **Cursor** sigue pudiendo fusionar `.cursor/mcp.json` y escribir `.pumuki/adapter.json` cuando el equipo elige ese agente explícitamente.
|
|
25
|
+
- Rollout: `pumuki@6.3.67` si **6.3.66** llegó a publicarse; si no, repin directo a **6.3.67**.
|
|
26
|
+
|
|
9
27
|
### 2026-04-06 (v6.3.65)
|
|
10
28
|
|
|
11
29
|
- **pre-commit.com + `exec`**: Pumuki ya no inserta su bloque gestionado *después* del `exec` del hook generado por pre-commit (código inalcanzable). Tras actualizar a **6.3.65**, ejecutar `pumuki install` en el consumer para reordenar `.git/hooks/pre-commit`.
|
|
@@ -307,9 +307,9 @@ Blocking code:
|
|
|
307
307
|
|
|
308
308
|
- `EVIDENCE_SKILLS_CONTRACT_INCOMPLETE` (when contract is incomplete outside PRE_WRITE)
|
|
309
309
|
|
|
310
|
-
## PRE_WRITE
|
|
310
|
+
## Worktree hygiene guard (PRE_WRITE + Git hooks)
|
|
311
311
|
|
|
312
|
-
AI Gate
|
|
312
|
+
AI Gate enforces worktree hygiene using **pending_changes** (or `staged + unstaged`) to reduce oversized batches before commit/push. The same thresholds apply to **`PRE_WRITE`**, **`PRE_COMMIT`**, **`PRE_PUSH`**, and **`CI`** when `.ai_evidence.json` is readable and valid (hooks merge these violations into `runPlatformGate`). Git-flow protected-branch checks (`GITFLOW_PROTECTED_BRANCH`) are also merged into hook runs.
|
|
313
313
|
|
|
314
314
|
Environment variables:
|
|
315
315
|
|
|
@@ -51,6 +51,8 @@ If both commands pass, the workspace is ready.
|
|
|
51
51
|
npm install --save-exact pumuki
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
The package `postinstall` runs **`pumuki install` only** (Git hooks + lifecycle wiring). Hooks chain **`pumuki-pre-write`** then **`pumuki-pre-commit`** / **`pumuki-pre-push`** (IDE-agnostic). When missing, **`.pumuki/adapter.json`** is created with hook + **MCP stdio** command lines (any MCP client can use them). It does **not** write IDE-specific files; use step 2 or `pumuki install --with-mcp --agent=<name>` for that.
|
|
55
|
+
|
|
54
56
|
### 2) Bootstrap managed lifecycle (recommended single command)
|
|
55
57
|
|
|
56
58
|
```bash
|
package/docs/product/USAGE.md
CHANGED
|
@@ -242,7 +242,7 @@ Stage mapping:
|
|
|
242
242
|
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.
|
|
243
243
|
|
|
244
244
|
System notifications (macOS) can be enabled from advanced menu option `31` (persisted in `.pumuki/system-notifications.json`).
|
|
245
|
-
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
|
|
245
|
+
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**.
|
|
246
246
|
Blocked notifications now use a native Swift floating modal (bottom-right) by default, with AppleScript fallback.
|
|
247
247
|
Override mode with `PUMUKI_MACOS_BLOCKED_DIALOG_MODE=auto|swift-floating|applescript`.
|
|
248
248
|
Custom skills import is available in advanced menu option `33` (writes `/.pumuki/custom-rules.json`).
|
|
@@ -1093,25 +1093,6 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
1093
1093
|
);
|
|
1094
1094
|
}
|
|
1095
1095
|
|
|
1096
|
-
if (params.preWriteWorktreeHygiene.enabled && params.repoState.git.available) {
|
|
1097
|
-
const pendingChanges = resolvePendingChanges(params.repoState) ?? 0;
|
|
1098
|
-
if (pendingChanges >= params.preWriteWorktreeHygiene.blockThreshold) {
|
|
1099
|
-
violations.push(
|
|
1100
|
-
toErrorViolation(
|
|
1101
|
-
'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT',
|
|
1102
|
-
`PRE_WRITE hygiene exceeded: pending_changes=${pendingChanges} (block_threshold=${params.preWriteWorktreeHygiene.blockThreshold}). Split worktree into atomic slices.`
|
|
1103
|
-
)
|
|
1104
|
-
);
|
|
1105
|
-
} else if (pendingChanges >= params.preWriteWorktreeHygiene.warnThreshold) {
|
|
1106
|
-
violations.push(
|
|
1107
|
-
toWarnViolation(
|
|
1108
|
-
'EVIDENCE_PREWRITE_WORKTREE_WARN',
|
|
1109
|
-
`PRE_WRITE hygiene warning: pending_changes=${pendingChanges} (warn_threshold=${params.preWriteWorktreeHygiene.warnThreshold}). Consider splitting worktree into smaller slices.`
|
|
1110
|
-
)
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
1096
|
return violations;
|
|
1116
1097
|
};
|
|
1117
1098
|
|
|
@@ -1178,6 +1159,8 @@ const collectEvidenceViolations = (
|
|
|
1178
1159
|
);
|
|
1179
1160
|
}
|
|
1180
1161
|
|
|
1162
|
+
appendWorktreeHygieneViolations(violations, repoState, preWriteWorktreeHygiene, stage);
|
|
1163
|
+
|
|
1181
1164
|
return { violations, ageSeconds };
|
|
1182
1165
|
};
|
|
1183
1166
|
|
|
@@ -1228,6 +1211,33 @@ const resolvePendingChanges = (repoState: RepoState): number | null => {
|
|
|
1228
1211
|
return repoState.git.pending_changes ?? (repoState.git.staged + repoState.git.unstaged);
|
|
1229
1212
|
};
|
|
1230
1213
|
|
|
1214
|
+
const appendWorktreeHygieneViolations = (
|
|
1215
|
+
violations: AiGateViolation[],
|
|
1216
|
+
repoState: RepoState,
|
|
1217
|
+
policy: PreWriteWorktreeHygienePolicy,
|
|
1218
|
+
stageLabel: AiGateStage
|
|
1219
|
+
): void => {
|
|
1220
|
+
if (!policy.enabled || !repoState.git.available) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const pendingChanges = resolvePendingChanges(repoState) ?? 0;
|
|
1224
|
+
if (pendingChanges >= policy.blockThreshold) {
|
|
1225
|
+
violations.push(
|
|
1226
|
+
toErrorViolation(
|
|
1227
|
+
'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT',
|
|
1228
|
+
`${stageLabel} worktree hygiene exceeded: pending_changes=${pendingChanges} (block_threshold=${policy.blockThreshold}). Split worktree into atomic slices.`
|
|
1229
|
+
)
|
|
1230
|
+
);
|
|
1231
|
+
} else if (pendingChanges >= policy.warnThreshold) {
|
|
1232
|
+
violations.push(
|
|
1233
|
+
toWarnViolation(
|
|
1234
|
+
'EVIDENCE_PREWRITE_WORKTREE_WARN',
|
|
1235
|
+
`${stageLabel} worktree hygiene warning: pending_changes=${pendingChanges} (warn_threshold=${policy.warnThreshold}). Consider smaller staged/unstaged batches.`
|
|
1236
|
+
)
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1231
1241
|
const toPolicyStage = (stage: AiGateStage): SkillsStage => {
|
|
1232
1242
|
if (stage === 'PRE_WRITE') {
|
|
1233
1243
|
return 'PRE_COMMIT';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Finding } from '../../core/gate/Finding';
|
|
2
|
+
import type { GateStage } from '../../core/gate/GateStage';
|
|
3
|
+
import { evaluateAiGate, type AiGateStage } from '../gate/evaluateAiGate';
|
|
4
|
+
|
|
5
|
+
const AI_GATE_STAGES = new Set<AiGateStage>(['PRE_WRITE', 'PRE_COMMIT', 'PRE_PUSH', 'CI']);
|
|
6
|
+
|
|
7
|
+
const REPO_POLICY_CODES = new Set<string>([
|
|
8
|
+
'GITFLOW_PROTECTED_BRANCH',
|
|
9
|
+
'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT',
|
|
10
|
+
'EVIDENCE_PREWRITE_WORKTREE_WARN',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const toRepoPolicyFinding = (params: {
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
severity: 'ERROR' | 'WARN';
|
|
17
|
+
}): Finding => ({
|
|
18
|
+
ruleId: `ai_gate.repo_policy.${params.code}`,
|
|
19
|
+
severity: params.severity,
|
|
20
|
+
code: params.code,
|
|
21
|
+
message: params.message,
|
|
22
|
+
matchedBy: 'RepoPolicy',
|
|
23
|
+
source: 'ai_gate:repo_policy',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const collectAiGateRepoPolicyFindings = (params: {
|
|
27
|
+
repoRoot: string;
|
|
28
|
+
stage: GateStage;
|
|
29
|
+
}): Finding[] => {
|
|
30
|
+
if (!AI_GATE_STAGES.has(params.stage as AiGateStage)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const evaluation = evaluateAiGate({
|
|
34
|
+
repoRoot: params.repoRoot,
|
|
35
|
+
stage: params.stage as AiGateStage,
|
|
36
|
+
});
|
|
37
|
+
return evaluation.violations
|
|
38
|
+
.filter((v) => REPO_POLICY_CODES.has(v.code))
|
|
39
|
+
.map((v) =>
|
|
40
|
+
toRepoPolicyFinding({
|
|
41
|
+
code: v.code,
|
|
42
|
+
message: v.message,
|
|
43
|
+
severity: v.severity === 'ERROR' ? 'ERROR' : 'WARN',
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -34,6 +34,7 @@ import { enforceTddBddPolicy } from '../tdd/enforcement';
|
|
|
34
34
|
import type { TddBddSnapshot } from '../tdd/types';
|
|
35
35
|
import { resolveSkillsEnforcement } from '../policy/skillsEnforcement';
|
|
36
36
|
import { applyTddBddEnforcement } from '../policy/tddBddEnforcement';
|
|
37
|
+
import { collectAiGateRepoPolicyFindings } from './aiGateRepoPolicyFindings';
|
|
37
38
|
|
|
38
39
|
export type OperationalMemoryShadowRecommendation = {
|
|
39
40
|
recommendedOutcome: 'ALLOW' | 'WARN' | 'BLOCK';
|
|
@@ -151,13 +152,19 @@ const defaultDependencies: GateDependencies = {
|
|
|
151
152
|
}),
|
|
152
153
|
};
|
|
153
154
|
|
|
154
|
-
const
|
|
155
|
+
const readSymbolicBranchRef = (git: IGitService, repoRoot: string): string | null => {
|
|
155
156
|
try {
|
|
156
157
|
const symbolicBranch = git.runGit(['symbolic-ref', '--short', 'HEAD'], repoRoot).trim();
|
|
157
|
-
|
|
158
|
-
return symbolicBranch;
|
|
159
|
-
}
|
|
158
|
+
return symbolicBranch.length > 0 ? symbolicBranch : null;
|
|
160
159
|
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const resolveCurrentBranch = (git: IGitService, repoRoot: string): string | null => {
|
|
165
|
+
const symbolic = readSymbolicBranchRef(git, repoRoot);
|
|
166
|
+
if (symbolic !== null) {
|
|
167
|
+
return symbolic;
|
|
161
168
|
}
|
|
162
169
|
try {
|
|
163
170
|
const branch = git.runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot).trim();
|
|
@@ -927,6 +934,15 @@ export async function runPlatformGate(params: {
|
|
|
927
934
|
const filesScanned = countScannedFilesFromFacts(factsForPlatformEvaluation);
|
|
928
935
|
const observedCodePaths = collectObservedCodePathsFromFacts(facts);
|
|
929
936
|
|
|
937
|
+
const platformEvaluation = dependencies.evaluatePlatformGateFindings({
|
|
938
|
+
facts: factsForPlatformEvaluation,
|
|
939
|
+
stage: params.policy.stage,
|
|
940
|
+
repoRoot,
|
|
941
|
+
});
|
|
942
|
+
const aiGateRepoPolicyFindings = collectAiGateRepoPolicyFindings({
|
|
943
|
+
repoRoot,
|
|
944
|
+
stage: params.policy.stage,
|
|
945
|
+
});
|
|
930
946
|
const {
|
|
931
947
|
detectedPlatforms,
|
|
932
948
|
skillsRuleSet,
|
|
@@ -934,12 +950,9 @@ export async function runPlatformGate(params: {
|
|
|
934
950
|
heuristicRules,
|
|
935
951
|
coverage,
|
|
936
952
|
evaluationFacts = factsForPlatformEvaluation,
|
|
937
|
-
findings,
|
|
938
|
-
} =
|
|
939
|
-
|
|
940
|
-
stage: params.policy.stage,
|
|
941
|
-
repoRoot,
|
|
942
|
-
});
|
|
953
|
+
findings: ruleEngineFindings,
|
|
954
|
+
} = platformEvaluation;
|
|
955
|
+
const findings = [...aiGateRepoPolicyFindings, ...ruleEngineFindings];
|
|
943
956
|
const evaluationMetrics: SnapshotEvaluationMetrics = coverage
|
|
944
957
|
? {
|
|
945
958
|
facts_total: coverage.factsTotal,
|
|
@@ -62,6 +62,10 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
62
62
|
EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES:
|
|
63
63
|
'Reconcilia policy/skills y revalida PRE_WRITE: npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
|
|
64
64
|
GITFLOW_PROTECTED_BRANCH: 'Trabaja en feature/* y evita ramas protegidas.',
|
|
65
|
+
EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT:
|
|
66
|
+
'Reduce archivos staged/unstaged por debajo del umbral (o ajusta PUMUKI_PREWRITE_WORKTREE_*); divide el trabajo en commits más pequeños.',
|
|
67
|
+
EVIDENCE_PREWRITE_WORKTREE_WARN:
|
|
68
|
+
'El worktree supera el umbral de aviso; reduce alcance antes del siguiente commit/push.',
|
|
65
69
|
PRE_PUSH_UPSTREAM_MISSING: 'Ejecuta: git push --set-upstream origin <branch>',
|
|
66
70
|
PRE_PUSH_UPSTREAM_MISALIGNED:
|
|
67
71
|
'Alinea upstream con la rama actual: git branch --unset-upstream && git push --set-upstream origin <branch>',
|
|
@@ -186,6 +190,7 @@ const defaultDependencies: StageRunnerDependencies = {
|
|
|
186
190
|
try {
|
|
187
191
|
ensureRuntimeArtifactsIgnored(repoRoot);
|
|
188
192
|
} catch {
|
|
193
|
+
undefined;
|
|
189
194
|
}
|
|
190
195
|
},
|
|
191
196
|
runPolicyReconcile,
|
|
@@ -28,6 +28,35 @@
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
|
+
"repo": [
|
|
32
|
+
{
|
|
33
|
+
"path": ".pumuki/adapter.json",
|
|
34
|
+
"payload": {
|
|
35
|
+
"hooks": {
|
|
36
|
+
"pre_write": {
|
|
37
|
+
"command": "npx --yes --package pumuki@latest pumuki-pre-write"
|
|
38
|
+
},
|
|
39
|
+
"pre_commit": {
|
|
40
|
+
"command": "npx --yes --package pumuki@latest pumuki-pre-commit"
|
|
41
|
+
},
|
|
42
|
+
"pre_push": {
|
|
43
|
+
"command": "npx --yes --package pumuki@latest pumuki-pre-push"
|
|
44
|
+
},
|
|
45
|
+
"ci": {
|
|
46
|
+
"command": "npx --yes --package pumuki@latest pumuki-ci"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"mcp": {
|
|
50
|
+
"enterprise": {
|
|
51
|
+
"command": "npx --yes --package pumuki@latest pumuki-mcp-enterprise-stdio"
|
|
52
|
+
},
|
|
53
|
+
"evidence": {
|
|
54
|
+
"command": "npx --yes --package pumuki@latest pumuki-mcp-evidence-stdio"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
],
|
|
31
60
|
"claude": [
|
|
32
61
|
{
|
|
33
62
|
"path": ".claude/settings.json",
|
|
@@ -74,6 +103,7 @@
|
|
|
74
103
|
"cursor": [
|
|
75
104
|
{
|
|
76
105
|
"path": ".cursor/mcp.json",
|
|
106
|
+
"mode": "json-merge",
|
|
77
107
|
"payload": {
|
|
78
108
|
"mcpServers": {
|
|
79
109
|
"pumuki-enterprise": {
|
|
@@ -110,6 +140,33 @@
|
|
|
110
140
|
}
|
|
111
141
|
}
|
|
112
142
|
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"path": ".pumuki/adapter.json",
|
|
146
|
+
"payload": {
|
|
147
|
+
"hooks": {
|
|
148
|
+
"pre_write": {
|
|
149
|
+
"command": "npx --yes --package pumuki@latest pumuki-pre-write"
|
|
150
|
+
},
|
|
151
|
+
"pre_commit": {
|
|
152
|
+
"command": "npx --yes --package pumuki@latest pumuki-pre-commit"
|
|
153
|
+
},
|
|
154
|
+
"pre_push": {
|
|
155
|
+
"command": "npx --yes --package pumuki@latest pumuki-pre-push"
|
|
156
|
+
},
|
|
157
|
+
"ci": {
|
|
158
|
+
"command": "npx --yes --package pumuki@latest pumuki-ci"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"mcp": {
|
|
162
|
+
"enterprise": {
|
|
163
|
+
"command": "npx --yes --package pumuki@latest pumuki-mcp-enterprise-stdio"
|
|
164
|
+
},
|
|
165
|
+
"evidence": {
|
|
166
|
+
"command": "npx --yes --package pumuki@latest pumuki-mcp-evidence-stdio"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
113
170
|
}
|
|
114
171
|
],
|
|
115
172
|
"windsurf": [
|
|
@@ -9,6 +9,10 @@ const HOOK_COMMANDS: Record<PumukiManagedHook, string> = {
|
|
|
9
9
|
'pre-push': 'pumuki-pre-push',
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
const PRE_WRITE_CLI = 'pumuki-pre-write';
|
|
13
|
+
|
|
14
|
+
type ResolverPhase = 'pre-write' | 'main';
|
|
15
|
+
|
|
12
16
|
const trimTrailingWhitespace = (value: string): string =>
|
|
13
17
|
value.replace(/[ \t]+\n/g, '\n').trimEnd();
|
|
14
18
|
|
|
@@ -24,54 +28,72 @@ const managedBlockPattern = new RegExp(
|
|
|
24
28
|
'g'
|
|
25
29
|
);
|
|
26
30
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const runnerLine = (
|
|
32
|
+
parentHook: PumukiManagedHook,
|
|
33
|
+
phase: ResolverPhase,
|
|
34
|
+
runner: string
|
|
35
|
+
): string => {
|
|
36
|
+
if (parentHook === 'pre-push' && phase === 'main') {
|
|
37
|
+
return ` PUMUKI_PRE_PUSH_STDIN="$PUMUKI_PRE_PUSH_STDIN" ${runner} "$@"`;
|
|
33
38
|
}
|
|
34
|
-
return ` ${
|
|
39
|
+
return ` ${runner}`;
|
|
35
40
|
};
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
const buildCliResolver = (params: {
|
|
43
|
+
cli: string;
|
|
44
|
+
parentHook: PumukiManagedHook;
|
|
45
|
+
phase: ResolverPhase;
|
|
46
|
+
}): string[] => {
|
|
47
|
+
const { cli, parentHook, phase } = params;
|
|
39
48
|
const localBinPath = `./node_modules/.bin/${cli}`;
|
|
40
49
|
const localNodeEntry = `./node_modules/pumuki/bin/${cli}.js`;
|
|
41
|
-
|
|
42
50
|
return [
|
|
43
|
-
PUMUKI_MANAGED_BLOCK_START,
|
|
44
|
-
...(hook === 'pre-push' ? ['PUMUKI_PRE_PUSH_STDIN="$(cat)"'] : []),
|
|
45
51
|
`if [ -x "${localBinPath}" ]; then`,
|
|
46
|
-
|
|
47
|
-
hook,
|
|
48
|
-
runner: localBinPath,
|
|
49
|
-
}),
|
|
52
|
+
runnerLine(parentHook, phase, localBinPath),
|
|
50
53
|
`elif [ -f "${localNodeEntry}" ] && command -v node >/dev/null 2>&1; then`,
|
|
51
|
-
|
|
52
|
-
hook,
|
|
53
|
-
runner: `node ${localNodeEntry}`,
|
|
54
|
-
}),
|
|
54
|
+
runnerLine(parentHook, phase, `node ${localNodeEntry}`),
|
|
55
55
|
`elif command -v ${cli} >/dev/null 2>&1; then`,
|
|
56
|
-
|
|
57
|
-
hook,
|
|
58
|
-
runner: cli,
|
|
59
|
-
}),
|
|
56
|
+
runnerLine(parentHook, phase, cli),
|
|
60
57
|
'elif command -v npx >/dev/null 2>&1; then',
|
|
61
|
-
|
|
62
|
-
hook,
|
|
63
|
-
runner: `npx --yes --package pumuki@latest ${cli}`,
|
|
64
|
-
}),
|
|
58
|
+
runnerLine(parentHook, phase, `npx --yes --package pumuki@latest ${cli}`),
|
|
65
59
|
'else',
|
|
66
60
|
` echo "[pumuki] unable to resolve ${cli} runner. Install dependencies or ensure npx is available." >&2`,
|
|
67
61
|
' exit 1',
|
|
68
62
|
'fi',
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
]
|
|
63
|
+
];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const buildPumukiManagedHookBlock = (hook: PumukiManagedHook): string => {
|
|
67
|
+
const mainCli = HOOK_COMMANDS[hook];
|
|
68
|
+
const lines: string[] = [PUMUKI_MANAGED_BLOCK_START];
|
|
69
|
+
|
|
70
|
+
lines.push('if [ "${PUMUKI_SKIP_CHAINED_PRE_WRITE:-}" != "1" ]; then');
|
|
71
|
+
lines.push(...buildCliResolver({
|
|
72
|
+
cli: PRE_WRITE_CLI,
|
|
73
|
+
parentHook: hook,
|
|
74
|
+
phase: 'pre-write',
|
|
75
|
+
}));
|
|
76
|
+
lines.push(' status=$?');
|
|
77
|
+
lines.push(' if [ "$status" -ne 0 ]; then');
|
|
78
|
+
lines.push(' exit "$status"');
|
|
79
|
+
lines.push(' fi');
|
|
80
|
+
lines.push('fi');
|
|
81
|
+
|
|
82
|
+
if (hook === 'pre-push') {
|
|
83
|
+
lines.push('PUMUKI_PRE_PUSH_STDIN="$(cat)"');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push(...buildCliResolver({
|
|
87
|
+
cli: mainCli,
|
|
88
|
+
parentHook: hook,
|
|
89
|
+
phase: 'main',
|
|
90
|
+
}));
|
|
91
|
+
lines.push(' status=$?');
|
|
92
|
+
lines.push(' if [ "$status" -ne 0 ]; then');
|
|
93
|
+
lines.push(' exit "$status"');
|
|
94
|
+
lines.push(' fi');
|
|
95
|
+
lines.push(PUMUKI_MANAGED_BLOCK_END);
|
|
96
|
+
return lines.join('\n');
|
|
75
97
|
};
|
|
76
98
|
|
|
77
99
|
const ensureExecutableHeader = (contents: string): string => {
|
|
@@ -12,6 +12,7 @@ import { captureRepoState } from '../evidence/repoState';
|
|
|
12
12
|
import { createEmptyEvaluationMetrics } from '../evidence/evaluationMetrics';
|
|
13
13
|
import { readOpenSpecManagedArtifacts, writeLifecycleState } from './state';
|
|
14
14
|
import { ensureRuntimeArtifactsIgnored } from './artifacts';
|
|
15
|
+
import { runLifecycleAdapterInstall } from './adapter';
|
|
15
16
|
|
|
16
17
|
export type LifecycleInstallResult = {
|
|
17
18
|
repoRoot: string;
|
|
@@ -59,6 +60,23 @@ const wireHooksLifecycleAndBootstrapEvidence = (params: {
|
|
|
59
60
|
return hookResult.changedHooks;
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
const ensureRepoBaselineAdapter = (repoRoot: string): void => {
|
|
64
|
+
const adapterPath = join(repoRoot, '.pumuki', 'adapter.json');
|
|
65
|
+
if (existsSync(adapterPath)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
runLifecycleAdapterInstall({
|
|
70
|
+
cwd: repoRoot,
|
|
71
|
+
agent: 'repo',
|
|
72
|
+
});
|
|
73
|
+
} catch (cause: unknown) {
|
|
74
|
+
if (process.env.PUMUKI_VERBOSE_INSTALL === '1') {
|
|
75
|
+
console.debug('[pumuki] adapter scaffold skipped', cause);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
62
80
|
export const runLifecycleInstall = (params?: {
|
|
63
81
|
cwd?: string;
|
|
64
82
|
git?: ILifecycleGitService;
|
|
@@ -84,6 +102,7 @@ export const runLifecycleInstall = (params?: {
|
|
|
84
102
|
version,
|
|
85
103
|
openSpecManagedArtifacts: priorArtifacts.length > 0 ? priorArtifacts : undefined,
|
|
86
104
|
});
|
|
105
|
+
ensureRepoBaselineAdapter(report.repoRoot);
|
|
87
106
|
return {
|
|
88
107
|
repoRoot: report.repoRoot,
|
|
89
108
|
version,
|
|
@@ -122,6 +141,7 @@ export const runLifecycleInstall = (params?: {
|
|
|
122
141
|
version,
|
|
123
142
|
openSpecManagedArtifacts: Array.from(mergedOpenSpecArtifacts),
|
|
124
143
|
});
|
|
144
|
+
ensureRepoBaselineAdapter(report.repoRoot);
|
|
125
145
|
|
|
126
146
|
return {
|
|
127
147
|
repoRoot: report.repoRoot,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.69",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -212,6 +212,7 @@
|
|
|
212
212
|
"@babel/preset-env": "^7.29.0",
|
|
213
213
|
"@babel/preset-typescript": "^7.28.5",
|
|
214
214
|
"@babel/traverse": "^7.28.5",
|
|
215
|
+
"@fission-ai/openspec": "1.2.0",
|
|
215
216
|
"@types/node": "^20.10.0",
|
|
216
217
|
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
217
218
|
"@typescript-eslint/parser": "^8.55.0",
|
|
@@ -6,16 +6,35 @@ import {
|
|
|
6
6
|
} from './framework-menu-system-notifications-types';
|
|
7
7
|
import { writeSystemNotificationsConfigFile } from './framework-menu-system-notifications-config-file';
|
|
8
8
|
|
|
9
|
+
export const normalizeBlockedDialogButtonLabel = (raw: string): string => {
|
|
10
|
+
const t = raw.replace(/\r/g, '').trim();
|
|
11
|
+
const lower = t.toLowerCase();
|
|
12
|
+
if (t === BLOCKED_DIALOG_DISABLE || lower.includes('desactivar')) {
|
|
13
|
+
return BLOCKED_DIALOG_DISABLE;
|
|
14
|
+
}
|
|
15
|
+
if (
|
|
16
|
+
t === BLOCKED_DIALOG_MUTE_30
|
|
17
|
+
|| (lower.includes('silenciar') && lower.includes('30'))
|
|
18
|
+
) {
|
|
19
|
+
return BLOCKED_DIALOG_MUTE_30;
|
|
20
|
+
}
|
|
21
|
+
if (t === BLOCKED_DIALOG_KEEP || (lower.includes('mantener') && lower.includes('activ'))) {
|
|
22
|
+
return BLOCKED_DIALOG_KEEP;
|
|
23
|
+
}
|
|
24
|
+
return t;
|
|
25
|
+
};
|
|
26
|
+
|
|
9
27
|
export const applyDialogChoice = (params: {
|
|
10
28
|
repoRoot: string;
|
|
11
29
|
config: SystemNotificationsConfig;
|
|
12
30
|
button: string;
|
|
13
31
|
nowMs: number;
|
|
14
32
|
}): void => {
|
|
15
|
-
|
|
33
|
+
const button = normalizeBlockedDialogButtonLabel(params.button);
|
|
34
|
+
if (button === BLOCKED_DIALOG_KEEP) {
|
|
16
35
|
return;
|
|
17
36
|
}
|
|
18
|
-
if (
|
|
37
|
+
if (button === BLOCKED_DIALOG_DISABLE) {
|
|
19
38
|
writeSystemNotificationsConfigFile(params.repoRoot, {
|
|
20
39
|
enabled: false,
|
|
21
40
|
channel: params.config.channel,
|
|
@@ -23,7 +42,7 @@ export const applyDialogChoice = (params: {
|
|
|
23
42
|
});
|
|
24
43
|
return;
|
|
25
44
|
}
|
|
26
|
-
if (
|
|
45
|
+
if (button === BLOCKED_DIALOG_MUTE_30) {
|
|
27
46
|
const muteUntil = new Date(params.nowMs + 30 * 60_000).toISOString();
|
|
28
47
|
writeSystemNotificationsConfigFile(params.repoRoot, {
|
|
29
48
|
enabled: true,
|
|
@@ -13,16 +13,23 @@ export const buildSystemNotificationsConfigFromSelection = (
|
|
|
13
13
|
): SystemNotificationsConfig => ({
|
|
14
14
|
enabled,
|
|
15
15
|
channel: 'macos',
|
|
16
|
-
blockedDialogEnabled:
|
|
16
|
+
blockedDialogEnabled: enabled,
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
export const normalizeSystemNotificationsConfig = (
|
|
20
20
|
raw: RawSystemNotificationsConfig
|
|
21
21
|
): SystemNotificationsConfig => {
|
|
22
|
+
const enabled = raw.enabled === true;
|
|
23
|
+
const blockedDialogEnabled =
|
|
24
|
+
raw.blockedDialogEnabled === true
|
|
25
|
+
? true
|
|
26
|
+
: raw.blockedDialogEnabled === false
|
|
27
|
+
? false
|
|
28
|
+
: enabled;
|
|
22
29
|
const config: SystemNotificationsConfig = {
|
|
23
|
-
enabled
|
|
30
|
+
enabled,
|
|
24
31
|
channel: 'macos',
|
|
25
|
-
blockedDialogEnabled
|
|
32
|
+
blockedDialogEnabled,
|
|
26
33
|
};
|
|
27
34
|
if (typeof raw.muteUntil === 'string' && raw.muteUntil.trim().length > 0) {
|
|
28
35
|
config.muteUntil = raw.muteUntil;
|
|
@@ -14,6 +14,19 @@ import {
|
|
|
14
14
|
isStderrNotificationFallbackDisabled,
|
|
15
15
|
} from './framework-menu-system-notifications-stdio-fallback';
|
|
16
16
|
|
|
17
|
+
const shouldMirrorGateBlockedToStderr = (
|
|
18
|
+
event: PumukiCriticalNotificationEvent,
|
|
19
|
+
env: NodeJS.ProcessEnv
|
|
20
|
+
): boolean => {
|
|
21
|
+
if (event.kind !== 'gate.blocked') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (isTruthyEnvValue(env.PUMUKI_DISABLE_GATE_BLOCKED_STDERR_MIRROR)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
};
|
|
29
|
+
|
|
17
30
|
export const dispatchSystemNotification = (params: {
|
|
18
31
|
event: PumukiCriticalNotificationEvent;
|
|
19
32
|
payload: SystemNotificationPayload;
|
|
@@ -46,7 +59,12 @@ export const dispatchSystemNotification = (params: {
|
|
|
46
59
|
applyDialogChoice,
|
|
47
60
|
});
|
|
48
61
|
|
|
49
|
-
|
|
62
|
+
const mirrorStderrForVisibility =
|
|
63
|
+
macResult.delivered &&
|
|
64
|
+
!stderrOff &&
|
|
65
|
+
(isTruthyEnvValue(params.env.PUMUKI_NOTIFICATION_STDERR_MIRROR) ||
|
|
66
|
+
shouldMirrorGateBlockedToStderr(params.event, params.env));
|
|
67
|
+
if (mirrorStderrForVisibility) {
|
|
50
68
|
deliverStderrNotificationBanner({ payload: params.payload });
|
|
51
69
|
}
|
|
52
70
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SystemNotificationsConfig } from './framework-menu-system-notifications-types';
|
|
2
|
+
import { normalizeBlockedDialogButtonLabel } from './framework-menu-system-notifications-config-choice';
|
|
2
3
|
|
|
3
4
|
export const applyBlockedDialogSelection = (params: {
|
|
4
5
|
repoRoot: string;
|
|
@@ -19,7 +20,7 @@ export const applyBlockedDialogSelection = (params: {
|
|
|
19
20
|
params.applyDialogChoice({
|
|
20
21
|
repoRoot: params.repoRoot,
|
|
21
22
|
config: params.config,
|
|
22
|
-
button: params.selectedButton,
|
|
23
|
+
button: normalizeBlockedDialogButtonLabel(params.selectedButton),
|
|
23
24
|
nowMs: params.nowMs,
|
|
24
25
|
});
|
|
25
26
|
};
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export const extractDialogButton = (stdout: string): string | null => {
|
|
2
|
-
const
|
|
3
|
-
if (
|
|
2
|
+
const matches = [...stdout.matchAll(/button returned:\s*(.+)/gi)];
|
|
3
|
+
if (matches.length === 0) {
|
|
4
4
|
return null;
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
const raw = matches[matches.length - 1][1]?.trim() ?? '';
|
|
7
|
+
const cleaned = raw
|
|
8
|
+
.replace(/[,}]\s*$/g, '')
|
|
9
|
+
.replace(/^["'\u201c]|[\u201d"']$/g, '')
|
|
10
|
+
.trim();
|
|
11
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
7
12
|
};
|