pumuki 6.3.39 → 6.3.40

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.
Files changed (63) hide show
  1. package/README.md +21 -12
  2. package/VERSION +1 -1
  3. package/core/gate/evaluateRules.test.ts +40 -0
  4. package/core/gate/evaluateRules.ts +7 -1
  5. package/core/rules/Consequence.ts +1 -0
  6. package/docs/CONFIGURATION.md +50 -0
  7. package/docs/INSTALLATION.md +38 -11
  8. package/docs/MCP_SERVERS.md +1 -1
  9. package/docs/README.md +1 -0
  10. package/docs/RELEASE_NOTES.md +44 -0
  11. package/docs/USAGE.md +191 -9
  12. package/docs/registro-maestro-de-seguimiento.md +2 -2
  13. package/docs/seguimiento-activo-pumuki-saas-supermercados.md +1592 -1
  14. package/docs/validation/README.md +2 -1
  15. package/docs/validation/ast-intelligence-roadmap.md +96 -0
  16. package/integrations/config/skillsCustomRules.ts +14 -0
  17. package/integrations/config/skillsDetectorRegistry.ts +11 -1
  18. package/integrations/config/skillsLock.ts +30 -0
  19. package/integrations/config/skillsMarkdownRules.ts +14 -3
  20. package/integrations/config/skillsRuleSet.ts +25 -3
  21. package/integrations/evidence/readEvidence.test.ts +3 -2
  22. package/integrations/evidence/readEvidence.ts +14 -4
  23. package/integrations/evidence/repoState.ts +10 -2
  24. package/integrations/evidence/schema.test.ts +3 -2
  25. package/integrations/evidence/schema.ts +3 -0
  26. package/integrations/evidence/writeEvidence.test.ts +3 -2
  27. package/integrations/gate/evaluateAiGate.ts +511 -2
  28. package/integrations/git/GitService.ts +5 -1
  29. package/integrations/git/astIntelligenceDualValidation.ts +275 -0
  30. package/integrations/git/gitAtomicity.ts +42 -9
  31. package/integrations/git/resolveGitRefs.ts +37 -0
  32. package/integrations/git/runPlatformGate.ts +228 -1
  33. package/integrations/git/runPlatformGateEvaluation.ts +4 -0
  34. package/integrations/git/stageRunners.ts +116 -2
  35. package/integrations/lifecycle/cli.ts +759 -22
  36. package/integrations/lifecycle/doctor.ts +62 -0
  37. package/integrations/lifecycle/index.ts +1 -0
  38. package/integrations/lifecycle/packageInfo.ts +25 -3
  39. package/integrations/lifecycle/policyReconcile.ts +304 -0
  40. package/integrations/lifecycle/preWriteAutomation.ts +42 -2
  41. package/integrations/lifecycle/watch.ts +365 -0
  42. package/integrations/mcp/aiGateCheck.ts +59 -2
  43. package/integrations/mcp/autoExecuteAiStart.ts +25 -1
  44. package/integrations/mcp/preFlightCheck.ts +13 -0
  45. package/integrations/sdd/evidenceScaffold.ts +223 -0
  46. package/integrations/sdd/index.ts +2 -0
  47. package/integrations/sdd/stateSync.ts +400 -0
  48. package/integrations/sdd/syncDocs.ts +97 -2
  49. package/package.json +4 -1
  50. package/scripts/backlog-action-reasons-lib.ts +38 -0
  51. package/scripts/backlog-id-issue-map-lib.ts +69 -0
  52. package/scripts/backlog-json-contract-lib.ts +3 -0
  53. package/scripts/framework-menu-consumer-preflight-lib.ts +6 -0
  54. package/scripts/package-install-smoke-command-resolution-lib.ts +64 -0
  55. package/scripts/package-install-smoke-consumer-npm-lib.ts +43 -0
  56. package/scripts/package-install-smoke-consumer-repo-setup-lib.ts +2 -0
  57. package/scripts/package-install-smoke-execution-steps-lib.ts +27 -9
  58. package/scripts/package-install-smoke-lifecycle-lib.ts +15 -4
  59. package/scripts/package-install-smoke-workspace-factory-lib.ts +4 -1
  60. package/scripts/reconcile-consumer-backlog-issues-lib.ts +651 -0
  61. package/scripts/reconcile-consumer-backlog-issues.ts +348 -0
  62. package/scripts/watch-consumer-backlog-lib.ts +465 -0
  63. package/scripts/watch-consumer-backlog.ts +326 -0
@@ -20,6 +20,35 @@ export type AiGateViolation = {
20
20
  severity: 'ERROR' | 'WARN';
21
21
  };
22
22
 
23
+ type PreWriteWorktreeHygienePolicy = {
24
+ enabled: boolean;
25
+ warnThreshold: number;
26
+ blockThreshold: number;
27
+ };
28
+
29
+ export type AiGateSkillsContractPlatformRequirement = {
30
+ platform: PreWriteSkillsPlatform;
31
+ required_rule_prefix: string;
32
+ required_bundles: ReadonlyArray<string>;
33
+ required_critical_rule_ids: ReadonlyArray<string>;
34
+ required_any_transversal_critical_rule_ids: ReadonlyArray<string>;
35
+ active_prefix_covered: boolean;
36
+ evaluated_prefix_covered: boolean;
37
+ missing_bundles: ReadonlyArray<string>;
38
+ missing_critical_rule_ids: ReadonlyArray<string>;
39
+ transversal_critical_covered: boolean;
40
+ missing_any_transversal_critical_rule_ids: ReadonlyArray<string>;
41
+ };
42
+
43
+ export type AiGateSkillsContractAssessment = {
44
+ stage: AiGateStage;
45
+ enforced: boolean;
46
+ status: 'PASS' | 'FAIL' | 'NOT_APPLICABLE';
47
+ detected_platforms: ReadonlyArray<PreWriteSkillsPlatform>;
48
+ requirements: ReadonlyArray<AiGateSkillsContractPlatformRequirement>;
49
+ violations: ReadonlyArray<AiGateViolation>;
50
+ };
51
+
23
52
  export type AiGateCheckResult = {
24
53
  stage: AiGateStage;
25
54
  status: 'ALLOWED' | 'BLOCKED';
@@ -53,6 +82,7 @@ export type AiGateCheckResult = {
53
82
  max_age_seconds: number | null;
54
83
  age_seconds: number | null;
55
84
  };
85
+ skills_contract: AiGateSkillsContractAssessment;
56
86
  repo_state: RepoState;
57
87
  violations: AiGateViolation[];
58
88
  };
@@ -79,8 +109,46 @@ const DEFAULT_MAX_AGE_SECONDS: Readonly<Record<AiGateStage, number>> = {
79
109
  PRE_PUSH: 1800,
80
110
  CI: 7200,
81
111
  };
112
+ const DEFAULT_PREWRITE_WORKTREE_HYGIENE: PreWriteWorktreeHygienePolicy = {
113
+ enabled: true,
114
+ warnThreshold: 12,
115
+ blockThreshold: 24,
116
+ };
117
+ const PREWRITE_WORKTREE_HYGIENE_ENABLED_ENV = 'PUMUKI_PREWRITE_WORKTREE_HYGIENE_ENABLED';
118
+ const PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_WARN_THRESHOLD';
119
+ const PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_BLOCK_THRESHOLD';
82
120
 
83
121
  const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
122
+ const PREWRITE_SKILLS_PLATFORMS = ['ios', 'android', 'backend', 'frontend'] as const;
123
+ type PreWriteSkillsPlatform = (typeof PREWRITE_SKILLS_PLATFORMS)[number];
124
+ const PLATFORM_SKILLS_RULE_PREFIXES: Readonly<Record<PreWriteSkillsPlatform, string>> = {
125
+ ios: 'skills.ios.',
126
+ android: 'skills.android.',
127
+ backend: 'skills.backend.',
128
+ frontend: 'skills.frontend.',
129
+ };
130
+ const PLATFORM_REQUIRED_SKILLS_BUNDLES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
131
+ ios: [
132
+ 'ios-guidelines',
133
+ 'ios-concurrency-guidelines',
134
+ 'ios-swiftui-expert-guidelines',
135
+ ],
136
+ android: ['android-guidelines'],
137
+ backend: ['backend-guidelines'],
138
+ frontend: ['frontend-guidelines'],
139
+ };
140
+ const PREWRITE_CRITICAL_SKILLS_RULES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
141
+ ios: ['skills.ios.critical-test-quality'],
142
+ android: [],
143
+ backend: [],
144
+ frontend: [],
145
+ };
146
+ const PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
147
+ ios: [],
148
+ android: ['skills.android.no-runblocking', 'skills.android.no-thread-sleep'],
149
+ backend: ['skills.backend.no-empty-catch', 'skills.backend.avoid-explicit-any'],
150
+ frontend: ['skills.frontend.no-empty-catch', 'skills.frontend.avoid-explicit-any'],
151
+ };
84
152
  const MCP_RECEIPT_STAGE_ORDER: Readonly<Record<AiGateStage, number>> = {
85
153
  PRE_WRITE: 0,
86
154
  PRE_COMMIT: 1,
@@ -94,6 +162,66 @@ const toErrorViolation = (code: string, message: string): AiGateViolation => ({
94
162
  message,
95
163
  });
96
164
 
165
+ const toWarnViolation = (code: string, message: string): AiGateViolation => ({
166
+ code,
167
+ severity: 'WARN',
168
+ message,
169
+ });
170
+
171
+ const toPositiveInteger = (value: unknown, fallback: number): number => {
172
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
173
+ return fallback;
174
+ }
175
+ const normalized = Math.trunc(value);
176
+ return normalized > 0 ? normalized : fallback;
177
+ };
178
+
179
+ const toBooleanFromEnv = (value: string | undefined, fallback: boolean): boolean => {
180
+ if (typeof value !== 'string') {
181
+ return fallback;
182
+ }
183
+ const normalized = value.trim().toLowerCase();
184
+ if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
185
+ return true;
186
+ }
187
+ if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') {
188
+ return false;
189
+ }
190
+ return fallback;
191
+ };
192
+
193
+ const resolvePreWriteWorktreeHygienePolicy = (
194
+ input?: Partial<PreWriteWorktreeHygienePolicy>
195
+ ): PreWriteWorktreeHygienePolicy => {
196
+ const enabled = input?.enabled
197
+ ?? toBooleanFromEnv(
198
+ process.env[PREWRITE_WORKTREE_HYGIENE_ENABLED_ENV],
199
+ DEFAULT_PREWRITE_WORKTREE_HYGIENE.enabled
200
+ );
201
+ const warnThreshold = toPositiveInteger(
202
+ input?.warnThreshold
203
+ ?? (process.env[PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV]
204
+ ? Number(process.env[PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV])
205
+ : undefined),
206
+ DEFAULT_PREWRITE_WORKTREE_HYGIENE.warnThreshold
207
+ );
208
+ const requestedBlockThreshold = toPositiveInteger(
209
+ input?.blockThreshold
210
+ ?? (process.env[PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV]
211
+ ? Number(process.env[PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV])
212
+ : undefined),
213
+ DEFAULT_PREWRITE_WORKTREE_HYGIENE.blockThreshold
214
+ );
215
+ const blockThreshold =
216
+ requestedBlockThreshold >= warnThreshold ? requestedBlockThreshold : warnThreshold;
217
+
218
+ return {
219
+ enabled,
220
+ warnThreshold,
221
+ blockThreshold,
222
+ };
223
+ };
224
+
97
225
  const toTimestampAgeSeconds = (
98
226
  timestamp: string,
99
227
  nowMs: number
@@ -123,11 +251,330 @@ const toCanonicalPath = (value: string): string => {
123
251
  }
124
252
  };
125
253
 
254
+ const toNormalizedSkillsBundleName = (bundle: string): string => {
255
+ const separatorIndex = bundle.lastIndexOf('@');
256
+ if (separatorIndex <= 0) {
257
+ return bundle.trim();
258
+ }
259
+ return bundle.slice(0, separatorIndex).trim();
260
+ };
261
+
262
+ const toDetectedSkillsPlatforms = (
263
+ platforms: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['platforms'] | undefined
264
+ ): ReadonlyArray<PreWriteSkillsPlatform> => {
265
+ const platformsState = platforms ?? {};
266
+ const detected: PreWriteSkillsPlatform[] = [];
267
+ for (const platform of PREWRITE_SKILLS_PLATFORMS) {
268
+ if (platformsState[platform]?.detected === true) {
269
+ detected.push(platform);
270
+ }
271
+ }
272
+ return detected;
273
+ };
274
+
275
+ const collectActiveRuleIdsCoverageViolations = (params: {
276
+ stage: AiGateStage;
277
+ evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
278
+ coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
279
+ }): AiGateViolation[] => {
280
+ if (params.coverage.active_rule_ids.length > 0) {
281
+ return [];
282
+ }
283
+ const detectedPlatforms = toDetectedSkillsPlatforms(params.evidence.platforms);
284
+ if (detectedPlatforms.length === 0) {
285
+ return [];
286
+ }
287
+ return [
288
+ toErrorViolation(
289
+ 'EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES',
290
+ `Active rules coverage is empty at ${params.stage} with detected code platforms=[${detectedPlatforms.join(', ')}].`
291
+ ),
292
+ ];
293
+ };
294
+
295
+ const collectPreWritePlatformSkillsViolations = (params: {
296
+ evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
297
+ coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
298
+ }): AiGateViolation[] => {
299
+ const detectedPlatforms = toDetectedSkillsPlatforms(params.evidence.platforms);
300
+ if (detectedPlatforms.length === 0) {
301
+ return [];
302
+ }
303
+
304
+ const violations: AiGateViolation[] = [];
305
+ const missingScopeCoverage: string[] = [];
306
+ const missingBundlesByPlatform: string[] = [];
307
+
308
+ for (const platform of detectedPlatforms) {
309
+ const prefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
310
+ const hasActivePrefix = params.coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(prefix));
311
+ const hasEvaluatedPrefix = params.coverage.evaluated_rule_ids.some((ruleId) =>
312
+ ruleId.startsWith(prefix)
313
+ );
314
+
315
+ if (!hasActivePrefix || !hasEvaluatedPrefix) {
316
+ const reasons: string[] = [];
317
+ if (!hasActivePrefix) {
318
+ reasons.push(`active_rules_prefix=${prefix} missing`);
319
+ }
320
+ if (!hasEvaluatedPrefix) {
321
+ reasons.push(`evaluated_rules_prefix=${prefix} missing`);
322
+ }
323
+ missingScopeCoverage.push(`${platform}{${reasons.join('; ')}}`);
324
+ }
325
+ }
326
+
327
+ if (missingScopeCoverage.length > 0) {
328
+ violations.push(
329
+ toErrorViolation(
330
+ 'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
331
+ `Detected platforms missing skill-rule coverage in PRE_WRITE: ${missingScopeCoverage.join(' | ')}.`
332
+ )
333
+ );
334
+ }
335
+
336
+ const activeSkillsBundles = new Set(
337
+ params.evidence.rulesets
338
+ .filter((ruleset) => ruleset.platform === 'skills')
339
+ .map((ruleset) => toNormalizedSkillsBundleName(ruleset.bundle))
340
+ .filter((bundle) => bundle.length > 0)
341
+ );
342
+
343
+ for (const platform of detectedPlatforms) {
344
+ const requiredBundles = PLATFORM_REQUIRED_SKILLS_BUNDLES[platform];
345
+ const missingBundles = requiredBundles.filter((bundleName) => !activeSkillsBundles.has(bundleName));
346
+ if (missingBundles.length === 0) {
347
+ continue;
348
+ }
349
+ missingBundlesByPlatform.push(`${platform}{missing_bundles=[${missingBundles.join(', ')}]}`);
350
+ }
351
+
352
+ if (missingBundlesByPlatform.length > 0) {
353
+ violations.push(
354
+ toErrorViolation(
355
+ 'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
356
+ `Detected platforms missing required skill bundles in PRE_WRITE: ${missingBundlesByPlatform.join(' | ')}.`
357
+ )
358
+ );
359
+ }
360
+
361
+ const missingCriticalRulesByPlatform: string[] = [];
362
+ for (const platform of detectedPlatforms) {
363
+ const requiredCriticalRuleIds = PREWRITE_CRITICAL_SKILLS_RULES[platform];
364
+ if (requiredCriticalRuleIds.length === 0) {
365
+ continue;
366
+ }
367
+ const missingCriticalRuleIds = requiredCriticalRuleIds.filter((ruleId) => {
368
+ const hasActive = params.coverage.active_rule_ids.includes(ruleId);
369
+ const hasEvaluated = params.coverage.evaluated_rule_ids.includes(ruleId);
370
+ return !hasActive || !hasEvaluated;
371
+ });
372
+ if (missingCriticalRuleIds.length === 0) {
373
+ continue;
374
+ }
375
+ missingCriticalRulesByPlatform.push(
376
+ `${platform}{missing_critical_rule_ids=[${missingCriticalRuleIds.join(', ')}]}`
377
+ );
378
+ }
379
+
380
+ if (missingCriticalRulesByPlatform.length > 0) {
381
+ violations.push(
382
+ toErrorViolation(
383
+ 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
384
+ `Detected platforms missing critical skill-rule enforcement in PRE_WRITE: ${missingCriticalRulesByPlatform.join(' | ')}.`
385
+ )
386
+ );
387
+ }
388
+
389
+ return violations;
390
+ };
391
+
392
+ const collectPreWriteCrossPlatformCriticalViolations = (params: {
393
+ evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
394
+ coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
395
+ }): AiGateViolation[] => {
396
+ const detectedPlatforms = toDetectedSkillsPlatforms(params.evidence.platforms);
397
+ if (detectedPlatforms.length === 0) {
398
+ return [];
399
+ }
400
+
401
+ const missingCriticalCoverage: string[] = [];
402
+ for (const platform of detectedPlatforms) {
403
+ const requiredRuleIds = PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform];
404
+ if (requiredRuleIds.length === 0) {
405
+ continue;
406
+ }
407
+
408
+ const hasCoverage = requiredRuleIds.some((ruleId) =>
409
+ params.coverage.active_rule_ids.includes(ruleId) &&
410
+ params.coverage.evaluated_rule_ids.includes(ruleId)
411
+ );
412
+
413
+ if (hasCoverage) {
414
+ continue;
415
+ }
416
+
417
+ missingCriticalCoverage.push(
418
+ `${platform}{required_any=[${requiredRuleIds.join(', ')}]}`
419
+ );
420
+ }
421
+
422
+ if (missingCriticalCoverage.length === 0) {
423
+ return [];
424
+ }
425
+
426
+ return [
427
+ toErrorViolation(
428
+ 'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
429
+ `Cross-platform critical enforcement incomplete in PRE_WRITE: ${missingCriticalCoverage.join(' | ')}.`
430
+ ),
431
+ ];
432
+ };
433
+
434
+ const toSkillsContractAssessment = (params: {
435
+ stage: AiGateStage;
436
+ evidenceResult: EvidenceReadResult;
437
+ }): AiGateSkillsContractAssessment => {
438
+ if (params.evidenceResult.kind !== 'valid') {
439
+ return {
440
+ stage: params.stage,
441
+ enforced: false,
442
+ status: 'NOT_APPLICABLE',
443
+ detected_platforms: [],
444
+ requirements: [],
445
+ violations: [],
446
+ };
447
+ }
448
+
449
+ const coverage = params.evidenceResult.evidence.snapshot.rules_coverage;
450
+ const detectedPlatforms = toDetectedSkillsPlatforms(params.evidenceResult.evidence.platforms);
451
+ if (detectedPlatforms.length === 0) {
452
+ return {
453
+ stage: params.stage,
454
+ enforced: false,
455
+ status: 'NOT_APPLICABLE',
456
+ detected_platforms: [],
457
+ requirements: [],
458
+ violations: [],
459
+ };
460
+ }
461
+
462
+ const activeSkillsBundles = new Set(
463
+ params.evidenceResult.evidence.rulesets
464
+ .filter((ruleset) => ruleset.platform === 'skills')
465
+ .map((ruleset) => toNormalizedSkillsBundleName(ruleset.bundle))
466
+ .filter((bundle) => bundle.length > 0)
467
+ );
468
+
469
+ const requirements: AiGateSkillsContractPlatformRequirement[] = [];
470
+ const violations: AiGateViolation[] = [];
471
+ for (const platform of detectedPlatforms) {
472
+ const requiredRulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
473
+ const requiredBundles = [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]];
474
+ const requiredCriticalRuleIds = [...PREWRITE_CRITICAL_SKILLS_RULES[platform]];
475
+ const requiredAnyTransversalCriticalRuleIds = [
476
+ ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
477
+ ];
478
+ const activePrefixCovered = coverage
479
+ ? coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix))
480
+ : false;
481
+ const evaluatedPrefixCovered = coverage
482
+ ? coverage.evaluated_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix))
483
+ : false;
484
+ const missingBundles = requiredBundles.filter(
485
+ (bundleName) => !activeSkillsBundles.has(bundleName)
486
+ );
487
+ const missingCriticalRuleIds = coverage
488
+ ? requiredCriticalRuleIds.filter((ruleId) => {
489
+ const hasActive = coverage.active_rule_ids.includes(ruleId);
490
+ const hasEvaluated = coverage.evaluated_rule_ids.includes(ruleId);
491
+ return !hasActive || !hasEvaluated;
492
+ })
493
+ : [...requiredCriticalRuleIds];
494
+ const transversalCriticalCovered =
495
+ requiredAnyTransversalCriticalRuleIds.length === 0
496
+ ? true
497
+ : coverage
498
+ ? requiredAnyTransversalCriticalRuleIds.some((ruleId) =>
499
+ coverage.active_rule_ids.includes(ruleId)
500
+ && coverage.evaluated_rule_ids.includes(ruleId)
501
+ )
502
+ : false;
503
+ const missingAnyTransversalCriticalRuleIds = transversalCriticalCovered
504
+ ? []
505
+ : [...requiredAnyTransversalCriticalRuleIds];
506
+
507
+ requirements.push({
508
+ platform,
509
+ required_rule_prefix: requiredRulePrefix,
510
+ required_bundles: requiredBundles,
511
+ required_critical_rule_ids: requiredCriticalRuleIds,
512
+ required_any_transversal_critical_rule_ids: requiredAnyTransversalCriticalRuleIds,
513
+ active_prefix_covered: activePrefixCovered,
514
+ evaluated_prefix_covered: evaluatedPrefixCovered,
515
+ missing_bundles: missingBundles,
516
+ missing_critical_rule_ids: missingCriticalRuleIds,
517
+ transversal_critical_covered: transversalCriticalCovered,
518
+ missing_any_transversal_critical_rule_ids: missingAnyTransversalCriticalRuleIds,
519
+ });
520
+
521
+ if (!activePrefixCovered || !evaluatedPrefixCovered) {
522
+ const missingParts: string[] = [];
523
+ if (!activePrefixCovered) {
524
+ missingParts.push('active_prefix');
525
+ }
526
+ if (!evaluatedPrefixCovered) {
527
+ missingParts.push('evaluated_prefix');
528
+ }
529
+ violations.push(
530
+ toErrorViolation(
531
+ 'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
532
+ `Skills contract scope coverage missing for ${platform}: ${missingParts.join(', ')} (${requiredRulePrefix}).`
533
+ )
534
+ );
535
+ }
536
+ if (missingBundles.length > 0) {
537
+ violations.push(
538
+ toErrorViolation(
539
+ 'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
540
+ `Skills contract missing bundles for ${platform}: [${missingBundles.join(', ')}].`
541
+ )
542
+ );
543
+ }
544
+ if (missingCriticalRuleIds.length > 0) {
545
+ violations.push(
546
+ toErrorViolation(
547
+ 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
548
+ `Skills contract missing critical rule coverage for ${platform}: [${missingCriticalRuleIds.join(', ')}].`
549
+ )
550
+ );
551
+ }
552
+ if (!transversalCriticalCovered && requiredAnyTransversalCriticalRuleIds.length > 0) {
553
+ violations.push(
554
+ toErrorViolation(
555
+ 'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
556
+ `Skills contract missing transversal critical coverage for ${platform}: required_any=[${requiredAnyTransversalCriticalRuleIds.join(', ')}].`
557
+ )
558
+ );
559
+ }
560
+ }
561
+
562
+ return {
563
+ stage: params.stage,
564
+ enforced: true,
565
+ status: violations.length === 0 ? 'PASS' : 'FAIL',
566
+ detected_platforms: detectedPlatforms,
567
+ requirements,
568
+ violations,
569
+ };
570
+ };
571
+
126
572
  const collectPreWriteCoherenceViolations = (params: {
127
573
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
128
574
  repoRoot: string;
129
575
  repoState: RepoState;
130
576
  nowMs: number;
577
+ preWriteWorktreeHygiene: PreWriteWorktreeHygienePolicy;
131
578
  }): AiGateViolation[] => {
132
579
  const violations: AiGateViolation[] = [];
133
580
  const evidenceRepoRoot = params.evidence.repo_state?.repo_root;
@@ -216,6 +663,27 @@ const collectPreWriteCoherenceViolations = (params: {
216
663
  )
217
664
  );
218
665
  }
666
+
667
+ violations.push(
668
+ ...collectActiveRuleIdsCoverageViolations({
669
+ stage: 'PRE_WRITE',
670
+ evidence: params.evidence,
671
+ coverage,
672
+ })
673
+ );
674
+
675
+ violations.push(
676
+ ...collectPreWritePlatformSkillsViolations({
677
+ evidence: params.evidence,
678
+ coverage,
679
+ })
680
+ );
681
+ violations.push(
682
+ ...collectPreWriteCrossPlatformCriticalViolations({
683
+ evidence: params.evidence,
684
+ coverage,
685
+ })
686
+ );
219
687
  }
220
688
 
221
689
  if (isTimestampFuture(params.evidence.timestamp, params.nowMs)) {
@@ -227,6 +695,25 @@ const collectPreWriteCoherenceViolations = (params: {
227
695
  );
228
696
  }
229
697
 
698
+ if (params.preWriteWorktreeHygiene.enabled && params.repoState.git.available) {
699
+ const pendingChanges = params.repoState.git.staged + params.repoState.git.unstaged;
700
+ if (pendingChanges >= params.preWriteWorktreeHygiene.blockThreshold) {
701
+ violations.push(
702
+ toErrorViolation(
703
+ 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT',
704
+ `PRE_WRITE hygiene exceeded: pending_changes=${pendingChanges} (block_threshold=${params.preWriteWorktreeHygiene.blockThreshold}). Split worktree into atomic slices.`
705
+ )
706
+ );
707
+ } else if (pendingChanges >= params.preWriteWorktreeHygiene.warnThreshold) {
708
+ violations.push(
709
+ toWarnViolation(
710
+ 'EVIDENCE_PREWRITE_WORKTREE_WARN',
711
+ `PRE_WRITE hygiene warning: pending_changes=${pendingChanges} (warn_threshold=${params.preWriteWorktreeHygiene.warnThreshold}). Consider splitting worktree into smaller slices.`
712
+ )
713
+ );
714
+ }
715
+ }
716
+
230
717
  return violations;
231
718
  };
232
719
 
@@ -236,7 +723,8 @@ const collectEvidenceViolations = (
236
723
  repoState: RepoState,
237
724
  stage: AiGateStage,
238
725
  nowMs: number,
239
- maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>
726
+ maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>,
727
+ preWriteWorktreeHygiene: PreWriteWorktreeHygienePolicy
240
728
  ): { violations: AiGateViolation[]; ageSeconds: number | null } => {
241
729
  const violations: AiGateViolation[] = [];
242
730
  const maxAgeSeconds = maxAgeSecondsByStage[stage];
@@ -287,6 +775,7 @@ const collectEvidenceViolations = (
287
775
  repoRoot,
288
776
  repoState,
289
777
  nowMs,
778
+ preWriteWorktreeHygiene,
290
779
  })
291
780
  );
292
781
  }
@@ -473,6 +962,7 @@ export const evaluateAiGate = (
473
962
  maxAgeSecondsByStage?: Readonly<Record<AiGateStage, number>>;
474
963
  protectedBranches?: ReadonlyArray<string>;
475
964
  requireMcpReceipt?: boolean;
965
+ preWriteWorktreeHygiene?: Partial<PreWriteWorktreeHygienePolicy>;
476
966
  },
477
967
  dependencies: Partial<AiGateDependencies> = {}
478
968
  ): AiGateCheckResult => {
@@ -481,6 +971,9 @@ export const evaluateAiGate = (
481
971
  ...dependencies,
482
972
  };
483
973
  const maxAgeSecondsByStage = params.maxAgeSecondsByStage ?? DEFAULT_MAX_AGE_SECONDS;
974
+ const preWriteWorktreeHygiene = resolvePreWriteWorktreeHygienePolicy(
975
+ params.preWriteWorktreeHygiene
976
+ );
484
977
  const protectedBranches = new Set(params.protectedBranches ?? Array.from(DEFAULT_PROTECTED_BRANCHES));
485
978
  const nowMs = activeDependencies.now();
486
979
  const evidenceResult = activeDependencies.readEvidenceResult(params.repoRoot);
@@ -496,7 +989,8 @@ export const evaluateAiGate = (
496
989
  repoState,
497
990
  params.stage,
498
991
  nowMs,
499
- maxAgeSecondsByStage
992
+ maxAgeSecondsByStage,
993
+ preWriteWorktreeHygiene
500
994
  );
501
995
  const mcpReceiptAssessment = collectMcpReceiptViolations({
502
996
  required: params.requireMcpReceipt ?? false,
@@ -507,8 +1001,22 @@ export const evaluateAiGate = (
507
1001
  readMcpAiGateReceipt: activeDependencies.readMcpAiGateReceipt,
508
1002
  });
509
1003
  const gitflowViolations = collectGitflowViolations(repoState, protectedBranches);
1004
+ const skillsContract = toSkillsContractAssessment({
1005
+ stage: params.stage,
1006
+ evidenceResult,
1007
+ });
1008
+ const stageSkillsContractViolations =
1009
+ params.stage === 'PRE_WRITE' || skillsContract.status !== 'FAIL'
1010
+ ? []
1011
+ : [
1012
+ toErrorViolation(
1013
+ 'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE',
1014
+ `Skills contract incomplete for ${params.stage}: ${skillsContract.violations.map((violation) => violation.code).join(', ')}.`
1015
+ ),
1016
+ ];
510
1017
  const violations = [
511
1018
  ...evidenceAssessment.violations,
1019
+ ...stageSkillsContractViolations,
512
1020
  ...gitflowViolations,
513
1021
  ...mcpReceiptAssessment.violations,
514
1022
  ];
@@ -538,6 +1046,7 @@ export const evaluateAiGate = (
538
1046
  max_age_seconds: mcpReceiptAssessment.maxAgeSeconds,
539
1047
  age_seconds: mcpReceiptAssessment.ageSeconds,
540
1048
  },
1049
+ skills_contract: skillsContract,
541
1050
  repo_state: repoState,
542
1051
  violations,
543
1052
  };
@@ -26,7 +26,11 @@ const assertSafeGitArgs = (args: ReadonlyArray<string>): void => {
26
26
  export class GitService implements IGitService {
27
27
  runGit(args: ReadonlyArray<string>, cwd?: string): string {
28
28
  assertSafeGitArgs(args);
29
- return runBinarySync('git', [...args], { cwd, encoding: 'utf8' });
29
+ return runBinarySync('git', [...args], {
30
+ cwd,
31
+ encoding: 'utf8',
32
+ stdio: ['ignore', 'pipe', 'pipe'],
33
+ });
30
34
  }
31
35
 
32
36
  getStagedFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact> {