pumuki 6.3.89 → 6.3.91

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,7 @@
1
+ ## 2026-04-20 (v6.3.91)
2
+ - **Observabilidad de skills alineada**: `status --json` y `doctor --json` dejan visible `governanceObservation.skills_contract` cuando la evidencia y el lock efectivo de skills muestran un contrato activo; el lifecycle ya no aparenta `NOT_APPLICABLE` mientras el gate real bloquea violaciones frontend/backend.
3
+ - **Rollout recomendado**: publicar `pumuki@6.3.91`, repin inmediato en `Flux_training` y repetir la repro con rojo frontend staged para confirmar `skills_contract.enforced=true`, `skills_contract.status=FAIL` y `attention_codes` con `SKILLS_CONTRACT_INCOMPLETE`.
4
+
1
5
  ## 2026-04-20 (v6.3.89)
2
6
  - **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
7
  - **Rollback limpio**: `pumuki uninstall` retira ese `core.hooksPath` solo si fue creado por Pumuki para el worktree.
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { readEvidenceResult } from '../evidence/readEvidence';
4
4
  import { readRepoTrackingState } from '../evidence/trackingContract';
5
5
  import type { RepoTrackingState } from '../evidence/schema';
6
+ import { loadRequiredSkillsLock } from '../config/skillsEffectiveLock';
6
7
  import { readSddStatus } from '../sdd';
7
8
  import type { SddStatusPayload } from '../sdd/types';
8
9
  import type { LifecycleExperimentalFeaturesSnapshot } from './experimentalFeaturesSnapshot';
@@ -25,6 +26,37 @@ export type GovernanceEvidenceSummary = {
25
26
  human_summary_preview: string[];
26
27
  };
27
28
 
29
+ type GovernanceSkillsContractPlatform = 'ios' | 'android' | 'backend' | 'frontend';
30
+
31
+ type GovernanceSkillsContractViolation = {
32
+ code: string;
33
+ message: string;
34
+ severity: 'ERROR' | 'WARN';
35
+ };
36
+
37
+ type GovernanceSkillsContractPlatformRequirement = {
38
+ platform: GovernanceSkillsContractPlatform;
39
+ required_rule_prefix: string;
40
+ required_bundles: ReadonlyArray<string>;
41
+ required_critical_rule_ids: ReadonlyArray<string>;
42
+ required_any_transversal_critical_rule_ids: ReadonlyArray<string>;
43
+ active_prefix_covered: boolean;
44
+ evaluated_prefix_covered: boolean;
45
+ missing_bundles: ReadonlyArray<string>;
46
+ missing_critical_rule_ids: ReadonlyArray<string>;
47
+ transversal_critical_covered: boolean;
48
+ missing_any_transversal_critical_rule_ids: ReadonlyArray<string>;
49
+ };
50
+
51
+ export type GovernanceSkillsContractSummary = {
52
+ stage: 'PRE_WRITE';
53
+ enforced: boolean;
54
+ status: 'PASS' | 'FAIL' | 'NOT_APPLICABLE';
55
+ detected_platforms: ReadonlyArray<GovernanceSkillsContractPlatform>;
56
+ requirements: ReadonlyArray<GovernanceSkillsContractPlatformRequirement>;
57
+ violations: ReadonlyArray<GovernanceSkillsContractViolation>;
58
+ };
59
+
28
60
  export type GovernanceContractSurface = {
29
61
  agents_md: boolean;
30
62
  skills_lock_json: boolean;
@@ -54,6 +86,7 @@ export type GovernanceObservationSnapshot = {
54
86
  };
55
87
  enterprise_warn_as_block_env: boolean;
56
88
  evidence: GovernanceEvidenceSummary;
89
+ skills_contract: GovernanceSkillsContractSummary;
57
90
  git: {
58
91
  current_branch: string | null;
59
92
  on_protected_branch_hint: boolean;
@@ -65,6 +98,40 @@ export type GovernanceObservationSnapshot = {
65
98
  agent_bootstrap_hints: ReadonlyArray<string>;
66
99
  };
67
100
 
101
+ const GOVERNANCE_SKILLS_PLATFORMS = ['ios', 'android', 'backend', 'frontend'] as const;
102
+ const GOVERNANCE_SKILLS_RULE_PREFIXES: Readonly<
103
+ Record<GovernanceSkillsContractPlatform, string>
104
+ > = {
105
+ ios: 'skills.ios.',
106
+ android: 'skills.android.',
107
+ backend: 'skills.backend.',
108
+ frontend: 'skills.frontend.',
109
+ };
110
+ const GOVERNANCE_REQUIRED_SKILLS_BUNDLES: Readonly<
111
+ Record<GovernanceSkillsContractPlatform, ReadonlyArray<string>>
112
+ > = {
113
+ ios: ['ios-guidelines', 'ios-concurrency-guidelines', 'ios-swiftui-expert-guidelines'],
114
+ android: ['android-guidelines'],
115
+ backend: ['backend-guidelines'],
116
+ frontend: ['frontend-guidelines'],
117
+ };
118
+ const GOVERNANCE_CRITICAL_SKILLS_RULES: Readonly<
119
+ Record<GovernanceSkillsContractPlatform, ReadonlyArray<string>>
120
+ > = {
121
+ ios: ['skills.ios.critical-test-quality'],
122
+ android: [],
123
+ backend: [],
124
+ frontend: [],
125
+ };
126
+ const GOVERNANCE_TRANSVERSAL_CRITICAL_SKILLS_RULES: Readonly<
127
+ Record<GovernanceSkillsContractPlatform, ReadonlyArray<string>>
128
+ > = {
129
+ ios: [],
130
+ android: ['skills.android.no-runblocking', 'skills.android.no-thread-sleep'],
131
+ backend: ['skills.backend.no-empty-catch', 'skills.backend.avoid-explicit-any'],
132
+ frontend: ['skills.frontend.no-empty-catch', 'skills.frontend.avoid-explicit-any'],
133
+ };
134
+
68
135
  const truthyEnv = (value: string | undefined): boolean => {
69
136
  if (typeof value !== 'string') {
70
137
  return false;
@@ -112,6 +179,186 @@ const buildContractSurface = (repoRoot: string): GovernanceContractSurface => ({
112
179
  pumuki_adapter_json: existsSync(join(repoRoot, '.pumuki', 'adapter.json')),
113
180
  });
114
181
 
182
+ const toRequiredSkillsPlatforms = (repoRoot: string): GovernanceSkillsContractPlatform[] => {
183
+ const requiredLock = loadRequiredSkillsLock(repoRoot);
184
+ if (!requiredLock) {
185
+ return [];
186
+ }
187
+ const detected = new Set<GovernanceSkillsContractPlatform>();
188
+ for (const bundle of requiredLock.bundles) {
189
+ for (const rule of bundle.rules) {
190
+ if (
191
+ rule.platform === 'ios'
192
+ || rule.platform === 'android'
193
+ || rule.platform === 'backend'
194
+ || rule.platform === 'frontend'
195
+ ) {
196
+ detected.add(rule.platform);
197
+ }
198
+ }
199
+ }
200
+ return GOVERNANCE_SKILLS_PLATFORMS.filter((platform) => detected.has(platform));
201
+ };
202
+
203
+ const toCoverageDetectedPlatforms = (
204
+ evidenceResult: ReturnType<typeof readEvidenceResult>
205
+ ): GovernanceSkillsContractPlatform[] => {
206
+ if (evidenceResult.kind !== 'valid') {
207
+ return [];
208
+ }
209
+ const explicit = GOVERNANCE_SKILLS_PLATFORMS.filter((platform) => {
210
+ const candidate = evidenceResult.evidence.platforms?.[platform];
211
+ return candidate?.detected === true;
212
+ });
213
+ if (explicit.length > 0) {
214
+ return explicit;
215
+ }
216
+ const coverage = evidenceResult.evidence.snapshot.rules_coverage;
217
+ if (!coverage) {
218
+ return [];
219
+ }
220
+ return GOVERNANCE_SKILLS_PLATFORMS.filter((platform) => {
221
+ const prefix = GOVERNANCE_SKILLS_RULE_PREFIXES[platform];
222
+ return (
223
+ coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(prefix))
224
+ || coverage.evaluated_rule_ids.some((ruleId) => ruleId.startsWith(prefix))
225
+ );
226
+ });
227
+ };
228
+
229
+ const summarizeSkillsContract = (repoRoot: string): GovernanceSkillsContractSummary => {
230
+ const requiredPlatforms = toRequiredSkillsPlatforms(repoRoot);
231
+ const evidenceResult = readEvidenceResult(repoRoot);
232
+ if (evidenceResult.kind !== 'valid') {
233
+ return {
234
+ stage: 'PRE_WRITE',
235
+ enforced: false,
236
+ status: 'NOT_APPLICABLE',
237
+ detected_platforms: [],
238
+ requirements: [],
239
+ violations: [],
240
+ };
241
+ }
242
+
243
+ const coverage = evidenceResult.evidence.snapshot.rules_coverage;
244
+ const detectedPlatforms = toCoverageDetectedPlatforms(evidenceResult);
245
+ const assessmentPlatforms = requiredPlatforms.length > 0 ? requiredPlatforms : detectedPlatforms;
246
+ if (assessmentPlatforms.length === 0) {
247
+ return {
248
+ stage: 'PRE_WRITE',
249
+ enforced: false,
250
+ status: 'NOT_APPLICABLE',
251
+ detected_platforms: [],
252
+ requirements: [],
253
+ violations: [],
254
+ };
255
+ }
256
+
257
+ const activeSkillsBundles = new Set(
258
+ (evidenceResult.evidence.rulesets ?? [])
259
+ .filter((ruleset) => ruleset.platform === 'skills')
260
+ .map((ruleset) => {
261
+ const [bundleName] = ruleset.bundle.split('@');
262
+ return bundleName?.trim().toLowerCase() ?? '';
263
+ })
264
+ .filter((bundle) => bundle.length > 0)
265
+ );
266
+
267
+ const requirements: GovernanceSkillsContractPlatformRequirement[] = [];
268
+ const violations: GovernanceSkillsContractViolation[] = [];
269
+ for (const platform of assessmentPlatforms) {
270
+ const requiredRulePrefix = GOVERNANCE_SKILLS_RULE_PREFIXES[platform];
271
+ const requiredBundles = [...GOVERNANCE_REQUIRED_SKILLS_BUNDLES[platform]];
272
+ const requiredCriticalRuleIds = [...GOVERNANCE_CRITICAL_SKILLS_RULES[platform]];
273
+ const requiredAnyTransversalCriticalRuleIds = [
274
+ ...GOVERNANCE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
275
+ ];
276
+ const activePrefixCovered = coverage
277
+ ? coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix))
278
+ : false;
279
+ const evaluatedPrefixCovered = coverage
280
+ ? coverage.evaluated_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix))
281
+ : false;
282
+ const missingBundles = requiredBundles.filter(
283
+ (bundleName) => !activeSkillsBundles.has(bundleName.toLowerCase())
284
+ );
285
+ const missingCriticalRuleIds = coverage
286
+ ? requiredCriticalRuleIds.filter((ruleId) => {
287
+ const hasActive = coverage.active_rule_ids.includes(ruleId);
288
+ const hasEvaluated = coverage.evaluated_rule_ids.includes(ruleId);
289
+ return !hasActive || !hasEvaluated;
290
+ })
291
+ : [...requiredCriticalRuleIds];
292
+ const transversalCriticalCovered =
293
+ requiredAnyTransversalCriticalRuleIds.length === 0
294
+ ? true
295
+ : Boolean(
296
+ coverage
297
+ && requiredAnyTransversalCriticalRuleIds.some((ruleId) =>
298
+ coverage.active_rule_ids.includes(ruleId)
299
+ && coverage.evaluated_rule_ids.includes(ruleId)
300
+ )
301
+ );
302
+ const missingAnyTransversalCriticalRuleIds = transversalCriticalCovered
303
+ ? []
304
+ : [...requiredAnyTransversalCriticalRuleIds];
305
+
306
+ requirements.push({
307
+ platform,
308
+ required_rule_prefix: requiredRulePrefix,
309
+ required_bundles: requiredBundles,
310
+ required_critical_rule_ids: requiredCriticalRuleIds,
311
+ required_any_transversal_critical_rule_ids: requiredAnyTransversalCriticalRuleIds,
312
+ active_prefix_covered: activePrefixCovered,
313
+ evaluated_prefix_covered: evaluatedPrefixCovered,
314
+ missing_bundles: missingBundles,
315
+ missing_critical_rule_ids: missingCriticalRuleIds,
316
+ transversal_critical_covered: transversalCriticalCovered,
317
+ missing_any_transversal_critical_rule_ids: missingAnyTransversalCriticalRuleIds,
318
+ });
319
+
320
+ if (!activePrefixCovered || !evaluatedPrefixCovered) {
321
+ violations.push({
322
+ severity: 'ERROR',
323
+ code: 'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
324
+ message: `Skills contract scope coverage missing for ${platform}.`,
325
+ });
326
+ }
327
+ if (missingBundles.length > 0) {
328
+ violations.push({
329
+ severity: 'ERROR',
330
+ code: 'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
331
+ message: `Skills contract missing bundles for ${platform}: [${missingBundles.join(', ')}].`,
332
+ });
333
+ }
334
+ if (missingCriticalRuleIds.length > 0) {
335
+ violations.push({
336
+ severity: 'ERROR',
337
+ code: 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
338
+ message: `Skills contract missing critical rule coverage for ${platform}: [${missingCriticalRuleIds.join(', ')}].`,
339
+ });
340
+ }
341
+ if (!transversalCriticalCovered && requiredAnyTransversalCriticalRuleIds.length > 0) {
342
+ violations.push({
343
+ severity: 'ERROR',
344
+ code: 'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
345
+ message:
346
+ `Skills contract missing transversal critical coverage for ${platform}: ` +
347
+ `[${requiredAnyTransversalCriticalRuleIds.join(', ')}].`,
348
+ });
349
+ }
350
+ }
351
+
352
+ return {
353
+ stage: 'PRE_WRITE',
354
+ enforced: true,
355
+ status: violations.length === 0 ? 'PASS' : 'FAIL',
356
+ detected_platforms: detectedPlatforms,
357
+ requirements,
358
+ violations,
359
+ };
360
+ };
361
+
115
362
  const summarizeEvidence = (repoRoot: string): GovernanceEvidenceSummary => {
116
363
  const evidenceResult = readEvidenceResult(repoRoot);
117
364
  const path = evidenceResult.source_descriptor.path;
@@ -193,6 +440,7 @@ export const readGovernanceObservationSnapshot = (params: {
193
440
  const surface = buildContractSurface(repoRoot);
194
441
  const tracking = readRepoTrackingState(repoRoot);
195
442
  const warnAsBlock = truthyEnv(process.env.PUMUKI_ENTERPRISE_STRICT_WARN_AS_BLOCK);
443
+ const skillsContract = summarizeSkillsContract(repoRoot);
196
444
 
197
445
  const attention: string[] = [];
198
446
  if (evidence.readable === 'invalid') {
@@ -234,6 +482,9 @@ export const readGovernanceObservationSnapshot = (params: {
234
482
  if (tracking.enforced && tracking.single_in_progress_valid === false) {
235
483
  attention.push('TRACKING_CANONICAL_IN_PROGRESS_INVALID');
236
484
  }
485
+ if (skillsContract.status === 'FAIL') {
486
+ attention.push('SKILLS_CONTRACT_INCOMPLETE');
487
+ }
237
488
 
238
489
  let governanceEffective: GovernanceObservationSnapshot['governance_effective'] = 'green';
239
490
  if (
@@ -268,6 +519,7 @@ export const readGovernanceObservationSnapshot = (params: {
268
519
  },
269
520
  enterprise_warn_as_block_env: warnAsBlock,
270
521
  evidence,
522
+ skills_contract: skillsContract,
271
523
  git: {
272
524
  current_branch: branch,
273
525
  on_protected_branch_hint: onProtected,
@@ -1,21 +1,173 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import { spawnSync as runSpawnSync } from 'node:child_process';
2
4
 
3
5
  export interface ILifecycleNpmService {
4
6
  runNpm(args: ReadonlyArray<string>, cwd: string): void;
5
7
  }
6
8
 
9
+ type LifecyclePackageManager = 'npm' | 'pnpm' | 'yarn';
10
+
11
+ type PackageManagerManifest = {
12
+ packageManager?: string;
13
+ workspaces?: unknown;
14
+ };
15
+
16
+ const resolvePackageManagerFromManifest = (cwd: string): LifecyclePackageManager | undefined => {
17
+ const packageJsonPath = join(cwd, 'package.json');
18
+ if (!existsSync(packageJsonPath)) {
19
+ return undefined;
20
+ }
21
+
22
+ try {
23
+ const raw = readFileSync(packageJsonPath, 'utf8');
24
+ const manifest = JSON.parse(raw) as PackageManagerManifest;
25
+ const packageManager = manifest.packageManager?.trim().toLowerCase();
26
+ if (!packageManager) {
27
+ return undefined;
28
+ }
29
+ if (packageManager.startsWith('pnpm@')) {
30
+ return 'pnpm';
31
+ }
32
+ if (packageManager.startsWith('yarn@')) {
33
+ return 'yarn';
34
+ }
35
+ if (packageManager.startsWith('npm@')) {
36
+ return 'npm';
37
+ }
38
+ } catch {
39
+ return undefined;
40
+ }
41
+
42
+ return undefined;
43
+ };
44
+
45
+ export const resolveLifecyclePackageManager = (cwd: string): LifecyclePackageManager => {
46
+ const fromManifest = resolvePackageManagerFromManifest(cwd);
47
+ if (fromManifest) {
48
+ return fromManifest;
49
+ }
50
+ if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
51
+ return 'pnpm';
52
+ }
53
+ if (existsSync(join(cwd, 'yarn.lock'))) {
54
+ return 'yarn';
55
+ }
56
+ return 'npm';
57
+ };
58
+
59
+ const isWorkspaceRoot = (cwd: string): boolean => {
60
+ if (existsSync(join(cwd, 'pnpm-workspace.yaml'))) {
61
+ return true;
62
+ }
63
+ const packageJsonPath = join(cwd, 'package.json');
64
+ if (!existsSync(packageJsonPath)) {
65
+ return false;
66
+ }
67
+ try {
68
+ const raw = readFileSync(packageJsonPath, 'utf8');
69
+ const manifest = JSON.parse(raw) as PackageManagerManifest;
70
+ return Array.isArray(manifest.workspaces) || typeof manifest.workspaces === 'object';
71
+ } catch {
72
+ return false;
73
+ }
74
+ };
75
+
76
+ const translateInstallLikeArgs = (
77
+ packageManager: Exclude<LifecyclePackageManager, 'npm'>,
78
+ args: ReadonlyArray<string>,
79
+ cwd: string
80
+ ): ReadonlyArray<string> => {
81
+ const packageSpecs = args.filter((arg) => !arg.startsWith('-')).slice(1);
82
+ if (packageSpecs.length === 0) {
83
+ return args;
84
+ }
85
+
86
+ const useExact = args.includes('--save-exact');
87
+ const useDev = args.includes('--save-dev');
88
+
89
+ if (packageManager === 'pnpm') {
90
+ return [
91
+ 'add',
92
+ ...(isWorkspaceRoot(cwd) ? ['-w'] : []),
93
+ ...(useDev ? ['-D'] : []),
94
+ ...(useExact ? ['-E'] : []),
95
+ ...packageSpecs,
96
+ ];
97
+ }
98
+
99
+ return [
100
+ 'add',
101
+ ...(useDev ? ['--dev'] : []),
102
+ ...(useExact ? ['--exact'] : []),
103
+ ...packageSpecs,
104
+ ];
105
+ };
106
+
107
+ const translateUninstallLikeArgs = (
108
+ packageManager: Exclude<LifecyclePackageManager, 'npm'>,
109
+ args: ReadonlyArray<string>,
110
+ cwd: string
111
+ ): ReadonlyArray<string> => {
112
+ const packageSpecs = args.filter((arg) => !arg.startsWith('-')).slice(1);
113
+ if (packageSpecs.length === 0) {
114
+ return args;
115
+ }
116
+ if (packageManager === 'pnpm') {
117
+ return ['remove', ...(isWorkspaceRoot(cwd) ? ['-w'] : []), ...packageSpecs];
118
+ }
119
+ return ['remove', ...packageSpecs];
120
+ };
121
+
122
+ export const resolveLifecyclePackageManagerCommand = (
123
+ args: ReadonlyArray<string>,
124
+ cwd: string
125
+ ): {
126
+ command: LifecyclePackageManager;
127
+ args: ReadonlyArray<string>;
128
+ } => {
129
+ const packageManager = resolveLifecyclePackageManager(cwd);
130
+ if (packageManager === 'npm') {
131
+ return {
132
+ command: 'npm',
133
+ args,
134
+ };
135
+ }
136
+
137
+ const primaryCommand = args[0];
138
+ if (primaryCommand === 'install') {
139
+ return {
140
+ command: packageManager,
141
+ args: translateInstallLikeArgs(packageManager, args, cwd),
142
+ };
143
+ }
144
+ if (primaryCommand === 'uninstall') {
145
+ return {
146
+ command: packageManager,
147
+ args: translateUninstallLikeArgs(packageManager, args, cwd),
148
+ };
149
+ }
150
+
151
+ return {
152
+ command: packageManager,
153
+ args,
154
+ };
155
+ };
156
+
7
157
  export class LifecycleNpmService implements ILifecycleNpmService {
8
158
  runNpm(args: ReadonlyArray<string>, cwd: string): void {
9
- const result = runSpawnSync('npm', args, {
159
+ const execution = resolveLifecyclePackageManagerCommand(args, cwd);
160
+ const renderedCommand = `${execution.command} ${execution.args.join(' ')}`.trim();
161
+ const result = runSpawnSync(execution.command, execution.args, {
10
162
  cwd,
11
163
  stdio: 'inherit',
12
164
  env: process.env,
13
165
  });
14
166
  if (result.error) {
15
- throw new Error(`npm ${args.join(' ')} failed: ${result.error.message}`);
167
+ throw new Error(`${renderedCommand} failed: ${result.error.message}`);
16
168
  }
17
169
  if (typeof result.status !== 'number' || result.status !== 0) {
18
- throw new Error(`npm ${args.join(' ')} failed with exit code ${result.status ?? 'unknown'}`);
170
+ throw new Error(`${renderedCommand} failed with exit code ${result.status ?? 'unknown'}`);
19
171
  }
20
172
  }
21
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.89",
3
+ "version": "6.3.91",
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": {