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
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
196
|
-
|
|
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(
|
|
204
|
-
const autoFixes = buildAutoFixes(
|
|
205
|
-
const message = buildMessage(
|
|
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:
|
|
256
|
+
success: normalizedEvaluation.allowed,
|
|
212
257
|
result: {
|
|
213
|
-
allowed:
|
|
214
|
-
status:
|
|
258
|
+
allowed: normalizedEvaluation.allowed,
|
|
259
|
+
status: normalizedEvaluation.status,
|
|
215
260
|
timestamp,
|
|
216
261
|
branch,
|
|
217
262
|
message,
|
|
218
|
-
stage:
|
|
219
|
-
policy:
|
|
220
|
-
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:
|
|
225
|
-
mcp_receipt:
|
|
226
|
-
skills_contract:
|
|
227
|
-
repo_state:
|
|
228
|
-
consistency_hint: buildConsistencyHint(
|
|
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 =
|
|
310
|
+
const allowed = normalizedEvaluation.allowed && !platformBlocks;
|
|
261
311
|
const status: 'ALLOWED' | 'BLOCKED' = allowed ? 'ALLOWED' : 'BLOCKED';
|
|
262
312
|
const violations = platformBlocks && platform
|
|
263
313
|
? [
|
|
264
|
-
...
|
|
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
|
-
:
|
|
274
|
-
const branch =
|
|
275
|
-
const timestamp =
|
|
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 = { ...
|
|
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:
|
|
299
|
-
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:
|
|
305
|
-
mcp_receipt:
|
|
306
|
-
skills_contract:
|
|
307
|
-
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.
|
|
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": {
|