pumuki 6.3.87 → 6.3.89

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.
@@ -1,3 +1,8 @@
1
+ ## 2026-04-20 (v6.3.89)
2
+ - **Aislamiento por worktree**: `pumuki install` detecta worktrees sin `core.hooksPath` explícito y fija un `core.hooksPath` local a `.pumuki/git-hooks`, evitando que los hooks gestionados se escriban en `.git/hooks` del checkout principal compartido.
3
+ - **Rollback limpio**: `pumuki uninstall` retira ese `core.hooksPath` solo si fue creado por Pumuki para el worktree.
4
+ - **Rollout recomendado**: publicar `pumuki@6.3.89`, repin inmediato en `Flux_training` y repetir la repro de worktree (`install`, `bootstrap-manifest`, `status --json`, checksums de hooks en checkout principal).
5
+
1
6
  ## 2026-04-20 (v6.3.87)
2
7
  - Cierra el segundo tramo de PUM-026: si PRE_WRITE solo arregla el receipt MCP y el gate pasa a verde, fuerza un refresh de paridad contra PRE_COMMIT antes del veredicto final.
3
8
  - Rollout recomendado: actualizar Flux_training y repetir la repro mínima de validate/pre-commit/.ai_evidence.
@@ -814,3 +819,5 @@ This file keeps only the operational highlights and rollout notes that matter wh
814
819
 
815
820
  - Legacy 5.3.4 migration/release notes were removed from active docs to avoid drift.
816
821
  - Historical commit trace remains available in Git history.
822
+
823
+ - 6.3.88: PUM-027 cierra el caso MCP degradado; sin .pumuki/adapter.json el gate bloquea y ya no regenera mcp-ai-gate-receipt.
@@ -27,6 +27,81 @@ export type PumukiHooksDirectoryResolution = {
27
27
  };
28
28
 
29
29
  const HOOK_FILE_MODE = 0o755;
30
+ const PUMUKI_WORKTREE_HOOKS_PATH = '.pumuki/git-hooks';
31
+
32
+ const readLocalGitConfigValue = (repoRoot: string, key: string): string | null => {
33
+ try {
34
+ const value = execFileSync('git', ['config', '--local', '--get', key], {
35
+ cwd: repoRoot,
36
+ encoding: 'utf8',
37
+ stdio: ['ignore', 'pipe', 'ignore'],
38
+ }).trim();
39
+ return value.length > 0 ? value : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ };
44
+
45
+ const writeLocalGitConfigValue = (repoRoot: string, key: string, value: string): void => {
46
+ execFileSync('git', ['config', '--local', key, value], {
47
+ cwd: repoRoot,
48
+ stdio: ['ignore', 'ignore', 'ignore'],
49
+ });
50
+ };
51
+
52
+ const unsetLocalGitConfigValue = (repoRoot: string, key: string): void => {
53
+ try {
54
+ execFileSync('git', ['config', '--local', '--unset-all', key], {
55
+ cwd: repoRoot,
56
+ stdio: ['ignore', 'ignore', 'ignore'],
57
+ });
58
+ } catch {
59
+ // noop
60
+ }
61
+ };
62
+
63
+ const isWorktreeCheckout = (repoRoot: string): boolean => {
64
+ try {
65
+ const gitPointer = readFileSync(join(repoRoot, '.git'), 'utf8').trim();
66
+ return /^gitdir:\s+/i.test(gitPointer);
67
+ } catch {
68
+ return false;
69
+ }
70
+ };
71
+
72
+ const isPumukiManagedWorktreeHooksPath = (repoRoot: string, hooksPath: string): boolean => {
73
+ const normalizedHooksPath = hooksPath.trim();
74
+ return (
75
+ normalizedHooksPath === PUMUKI_WORKTREE_HOOKS_PATH ||
76
+ normalizedHooksPath === resolve(repoRoot, PUMUKI_WORKTREE_HOOKS_PATH)
77
+ );
78
+ };
79
+
80
+ const ensureWorktreeLocalHooksPath = (repoRoot: string): void => {
81
+ if (!isWorktreeCheckout(repoRoot)) {
82
+ return;
83
+ }
84
+
85
+ const currentHooksPath = readLocalGitConfigValue(repoRoot, 'core.hooksPath');
86
+ if (currentHooksPath) {
87
+ return;
88
+ }
89
+
90
+ writeLocalGitConfigValue(repoRoot, 'core.hooksPath', PUMUKI_WORKTREE_HOOKS_PATH);
91
+ };
92
+
93
+ const clearWorktreeLocalHooksPath = (repoRoot: string): void => {
94
+ if (!isWorktreeCheckout(repoRoot)) {
95
+ return;
96
+ }
97
+
98
+ const currentHooksPath = readLocalGitConfigValue(repoRoot, 'core.hooksPath');
99
+ if (!currentHooksPath || !isPumukiManagedWorktreeHooksPath(repoRoot, currentHooksPath)) {
100
+ return;
101
+ }
102
+
103
+ unsetLocalGitConfigValue(repoRoot, 'core.hooksPath');
104
+ };
30
105
 
31
106
  const resolveGitPath = (repoRoot: string, gitPathTarget: string): string | null => {
32
107
  try {
@@ -137,6 +212,7 @@ const ensureHooksDirectory = (repoRoot: string): void => {
137
212
  };
138
213
 
139
214
  export const installPumukiHooks = (repoRoot: string): HookInstallResult => {
215
+ ensureWorktreeLocalHooksPath(repoRoot);
140
216
  ensureHooksDirectory(repoRoot);
141
217
  const changedHooks: PumukiManagedHook[] = [];
142
218
 
@@ -180,6 +256,7 @@ export const uninstallPumukiHooks = (repoRoot: string): HookUninstallResult => {
180
256
  changedHooks.push(hook);
181
257
  }
182
258
 
259
+ clearWorktreeLocalHooksPath(repoRoot);
183
260
  return { changedHooks };
184
261
  };
185
262
 
@@ -68,16 +68,24 @@ const hasAutoFixableEvidenceViolation = (aiGate: ReturnType<typeof evaluateAiGat
68
68
  const hasEvidenceGateBlockedViolation = (aiGate: ReturnType<typeof evaluateAiGate>): boolean =>
69
69
  aiGate.violations.some((violation) => violation.code === 'EVIDENCE_GATE_BLOCKED');
70
70
 
71
+ const hasAdapterMissingViolation = (aiGate: ReturnType<typeof evaluateAiGate>): boolean =>
72
+ aiGate.violations.some((violation) => violation.code === 'MCP_ENTERPRISE_ADAPTER_MISSING');
73
+
71
74
  const hasAutoFixableMcpReceiptViolation = (aiGate: ReturnType<typeof evaluateAiGate>): boolean =>
72
- aiGate.violations.some((violation) => PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES.has(violation.code));
75
+ !hasAdapterMissingViolation(aiGate)
76
+ && aiGate.violations.some((violation) => PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES.has(violation.code));
73
77
 
74
78
  const collectAutoFixableViolationCodes = (aiGate: ReturnType<typeof evaluateAiGate>): string[] =>
75
79
  aiGate.violations
76
- .filter(
77
- (violation) =>
78
- PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES.has(violation.code)
79
- || PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES.has(violation.code)
80
- )
80
+ .filter((violation) => {
81
+ if (PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES.has(violation.code)) {
82
+ return true;
83
+ }
84
+ if (!PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES.has(violation.code)) {
85
+ return false;
86
+ }
87
+ return !hasAdapterMissingViolation(aiGate);
88
+ })
81
89
  .map((violation) => violation.code)
82
90
  .sort((left, right) => left.localeCompare(right));
83
91
 
@@ -1,3 +1,5 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import { evaluateAiGate, type AiGateStage } from '../gate/evaluateAiGate';
2
4
  import { resolveRemediationHintForViolationCode } from '../gate/remediationCatalog';
3
5
  import { resolveLearningContextExperimentalFeature } from '../policy/experimentalFeatures';
@@ -48,6 +50,8 @@ const HOOK_STAGE_SET = new Set<AiGateStage>(['PRE_COMMIT', 'PRE_PUSH', 'CI']);
48
50
  const isHookRefreshableEvidenceCode = (code: string): boolean =>
49
51
  code.startsWith('EVIDENCE_');
50
52
 
53
+ const MCP_ENTERPRISE_ADAPTER_MISSING_CODE = 'MCP_ENTERPRISE_ADAPTER_MISSING';
54
+
51
55
  type AiGateCheckDependencies = {
52
56
  evaluateAiGate: typeof evaluateAiGate;
53
57
  runMcpAlignedPlatformGate: typeof runMcpAlignedPlatformGate;
@@ -120,6 +124,9 @@ const buildAutoFixes = (
120
124
  learningContext: SddLearningContext | null
121
125
  ): ReadonlyArray<string> => {
122
126
  const fixes: string[] = [];
127
+ if (evaluation.violations.some((violation) => violation.code === MCP_ENTERPRISE_ADAPTER_MISSING_CODE)) {
128
+ fixes.push('Regenera .pumuki/adapter.json con `pnpm exec pumuki install` antes de volver a validar el gate MCP.');
129
+ }
123
130
  const emittedCodes = new Set<string>();
124
131
  for (const violation of evaluation.violations) {
125
132
  if (emittedCodes.has(violation.code)) {
@@ -163,6 +170,39 @@ const resolveAiGateCheckMode = (): PlatformGateAlignment['mode'] => {
163
170
  return raw === 'full' || raw === 'aligned' ? 'full' : 'policy';
164
171
  };
165
172
 
173
+ const hasMcpAdapter = (repoRoot: string): boolean =>
174
+ existsSync(join(repoRoot, '.pumuki', 'adapter.json'));
175
+
176
+ const withRequiredMcpAdapter = (
177
+ evaluation: ReturnType<typeof evaluateAiGate>,
178
+ repoRoot: string,
179
+ requireMcpReceipt: boolean
180
+ ): ReturnType<typeof evaluateAiGate> => {
181
+ if (!requireMcpReceipt || hasMcpAdapter(repoRoot)) {
182
+ return evaluation;
183
+ }
184
+ if (evaluation.violations.some((violation) => violation.code === MCP_ENTERPRISE_ADAPTER_MISSING_CODE)) {
185
+ return {
186
+ ...evaluation,
187
+ allowed: false,
188
+ status: 'BLOCKED',
189
+ };
190
+ }
191
+ return {
192
+ ...evaluation,
193
+ allowed: false,
194
+ status: 'BLOCKED',
195
+ violations: [
196
+ {
197
+ code: MCP_ENTERPRISE_ADAPTER_MISSING_CODE,
198
+ severity: 'ERROR',
199
+ message: 'Missing .pumuki/adapter.json. MCP-dependent validation cannot prove a real MCP invocation.',
200
+ },
201
+ ...evaluation.violations,
202
+ ],
203
+ };
204
+ };
205
+
166
206
  const toPlatformGateAlignment = (
167
207
  mode: PlatformGateAlignment['mode'],
168
208
  platform?: { exitCode: number; aligned: boolean; skipReason: string | null }
@@ -192,40 +232,45 @@ export const runEnterpriseAiGateCheck = (params: {
192
232
  stage: params.stage,
193
233
  requireMcpReceipt: params.requireMcpReceipt ?? false,
194
234
  });
195
- const branch = evaluation.repo_state.git.branch;
196
- const timestamp = evaluation.evidence.source.generated_at;
235
+ const normalizedEvaluation = withRequiredMcpAdapter(
236
+ evaluation,
237
+ params.repoRoot,
238
+ params.requireMcpReceipt ?? false
239
+ );
240
+ const branch = normalizedEvaluation.repo_state.git.branch;
241
+ const timestamp = normalizedEvaluation.evidence.source.generated_at;
197
242
  const learningContextFeature = resolveLearningContextExperimentalFeature();
198
243
  const learningContext = learningContextFeature.mode === 'off'
199
244
  ? null
200
245
  : readSddLearningContext({
201
246
  repoRoot: params.repoRoot,
202
247
  });
203
- const warnings = buildWarnings(evaluation);
204
- const autoFixes = buildAutoFixes(evaluation, learningContext);
205
- const message = buildMessage(evaluation);
248
+ const warnings = buildWarnings(normalizedEvaluation);
249
+ const autoFixes = buildAutoFixes(normalizedEvaluation, learningContext);
250
+ const message = buildMessage(normalizedEvaluation);
206
251
 
207
252
  return {
208
253
  tool: 'ai_gate_check',
209
254
  dryRun: true,
210
255
  executed: true,
211
- success: evaluation.allowed,
256
+ success: normalizedEvaluation.allowed,
212
257
  result: {
213
- allowed: evaluation.allowed,
214
- status: evaluation.status,
258
+ allowed: normalizedEvaluation.allowed,
259
+ status: normalizedEvaluation.status,
215
260
  timestamp,
216
261
  branch,
217
262
  message,
218
- stage: evaluation.stage,
219
- policy: evaluation.policy,
220
- violations: evaluation.violations,
263
+ stage: normalizedEvaluation.stage,
264
+ policy: normalizedEvaluation.policy,
265
+ violations: normalizedEvaluation.violations,
221
266
  warnings,
222
267
  auto_fixes: autoFixes,
223
268
  learning_context: learningContext,
224
- evidence: evaluation.evidence,
225
- mcp_receipt: evaluation.mcp_receipt,
226
- skills_contract: evaluation.skills_contract,
227
- repo_state: evaluation.repo_state,
228
- consistency_hint: buildConsistencyHint(evaluation),
269
+ evidence: normalizedEvaluation.evidence,
270
+ mcp_receipt: normalizedEvaluation.mcp_receipt,
271
+ skills_contract: normalizedEvaluation.skills_contract,
272
+ repo_state: normalizedEvaluation.repo_state,
273
+ consistency_hint: buildConsistencyHint(normalizedEvaluation),
229
274
  },
230
275
  };
231
276
  };
@@ -245,6 +290,11 @@ export const runEnterpriseAiGateCheckAsync = async (params: {
245
290
  stage: params.stage,
246
291
  requireMcpReceipt: params.requireMcpReceipt ?? false,
247
292
  });
293
+ const normalizedEvaluation = withRequiredMcpAdapter(
294
+ evaluation,
295
+ params.repoRoot,
296
+ params.requireMcpReceipt ?? false
297
+ );
248
298
 
249
299
  let platform:
250
300
  | { exitCode: number; aligned: boolean; skipReason: string | null }
@@ -257,11 +307,11 @@ export const runEnterpriseAiGateCheckAsync = async (params: {
257
307
  }
258
308
 
259
309
  const platformBlocks = Boolean(platform && platform.exitCode !== 0);
260
- const allowed = evaluation.allowed && !platformBlocks;
310
+ const allowed = normalizedEvaluation.allowed && !platformBlocks;
261
311
  const status: 'ALLOWED' | 'BLOCKED' = allowed ? 'ALLOWED' : 'BLOCKED';
262
312
  const violations = platformBlocks && platform
263
313
  ? [
264
- ...evaluation.violations,
314
+ ...normalizedEvaluation.violations,
265
315
  {
266
316
  code: 'PLATFORM_GATE_EXIT_NON_ZERO',
267
317
  message:
@@ -270,16 +320,16 @@ export const runEnterpriseAiGateCheckAsync = async (params: {
270
320
  severity: 'ERROR' as const,
271
321
  },
272
322
  ]
273
- : evaluation.violations;
274
- const branch = evaluation.repo_state.git.branch;
275
- const timestamp = evaluation.evidence.source.generated_at;
323
+ : normalizedEvaluation.violations;
324
+ const branch = normalizedEvaluation.repo_state.git.branch;
325
+ const timestamp = normalizedEvaluation.evidence.source.generated_at;
276
326
  const learningContextFeature = resolveLearningContextExperimentalFeature();
277
327
  const learningContext = learningContextFeature.mode === 'off'
278
328
  ? null
279
329
  : readSddLearningContext({
280
330
  repoRoot: params.repoRoot,
281
331
  });
282
- const evaluationForHints = { ...evaluation, allowed, status, violations };
332
+ const evaluationForHints = { ...normalizedEvaluation, allowed, status, violations };
283
333
  const warnings = buildWarnings(evaluationForHints);
284
334
  const autoFixes = buildAutoFixes(evaluationForHints, learningContext);
285
335
  const message = buildMessage(evaluationForHints, platform);
@@ -295,16 +345,16 @@ export const runEnterpriseAiGateCheckAsync = async (params: {
295
345
  timestamp,
296
346
  branch,
297
347
  message,
298
- stage: evaluation.stage,
299
- policy: evaluation.policy,
348
+ stage: normalizedEvaluation.stage,
349
+ policy: normalizedEvaluation.policy,
300
350
  violations,
301
351
  warnings,
302
352
  auto_fixes: autoFixes,
303
353
  learning_context: learningContext,
304
- evidence: evaluation.evidence,
305
- mcp_receipt: evaluation.mcp_receipt,
306
- skills_contract: evaluation.skills_contract,
307
- repo_state: evaluation.repo_state,
354
+ evidence: normalizedEvaluation.evidence,
355
+ mcp_receipt: normalizedEvaluation.mcp_receipt,
356
+ skills_contract: normalizedEvaluation.skills_contract,
357
+ repo_state: normalizedEvaluation.repo_state,
308
358
  consistency_hint: buildConsistencyHint(evaluationForHints, platform),
309
359
  platform_gate_alignment: toPlatformGateAlignment(mode, platform),
310
360
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.87",
3
+ "version": "6.3.89",
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": {