pumuki 6.3.57 → 6.3.58

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/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.57
1
+ v6.3.58
@@ -6,6 +6,25 @@ This file keeps only the operational highlights and rollout notes that matter wh
6
6
 
7
7
  ## 2026-03 (enterprise hardening updates)
8
8
 
9
+ ### 2026-03-14 (v6.3.58)
10
+
11
+ - PRE_WRITE no-op stability for consumer slices:
12
+ - clean slices with `pending_changes=0` no longer fail by undetected multi-platform skills contract noise,
13
+ - the resulting contract is now `NOT_APPLICABLE` when there is no materialized active platform scope.
14
+ - PRE_WRITE freshness autocure:
15
+ - stale `ai_evidence` and stale MCP receipts are refreshed even if the SDD session itself is invalid,
16
+ - operational noise is removed before the final decision is reported.
17
+ - Deep doctor de-escalation:
18
+ - missing/stale evidence is now treated as operational drift (`warning`) instead of a hard diagnostic block.
19
+ - Operational impact:
20
+ - `SAAS · PUMUKI-021` is left with only release/adoption work; the core no longer has a technical residual for that bug.
21
+ - Validation evidence:
22
+ - `npm run -s typecheck` (`PASS`)
23
+ - `node --import tsx --test integrations/gate/__tests__/evaluateAiGate.test.ts` (`40 pass / 0 fail`)
24
+ - `node --import tsx --test --test-name-pattern "autocura evidence y receipt stale aunque la sesión SDD esté inválida|blocks AI gate violations in strict enforcement mode|blocks missing OpenSpec in strict enforcement mode|expone next_action de reconcile cuando active_rule_ids está vacío para código|expone next_action con slice atómico" integrations/lifecycle/__tests__/cli.test.ts` (`4 pass / 0 fail`)
25
+ - `npm run -s validation:package-manifest` (`PASS`)
26
+ - `npm pack --json --dry-run` (`PASS`)
27
+
9
28
  ### 2026-03-11 (v6.3.57)
10
29
 
11
30
  - AST intelligence and gate payload enrichment:
@@ -188,7 +188,7 @@ export type RepoState = {
188
188
  installed: boolean;
189
189
  package_version: string | null;
190
190
  lifecycle_version: string | null;
191
- package_version_source?: 'consumer-node-modules' | 'runtime-package';
191
+ package_version_source?: 'consumer-node-modules' | 'runtime-package' | 'source-bin';
192
192
  package_version_runtime?: string | null;
193
193
  package_version_installed?: string | null;
194
194
  hooks: {
@@ -10,6 +10,10 @@ import {
10
10
  loadEffectiveSkillsLock,
11
11
  loadRequiredSkillsLock,
12
12
  } from '../config/skillsEffectiveLock';
13
+ import {
14
+ resolveSkillsEnforcement,
15
+ type SkillsEnforcementResolution,
16
+ } from '../policy/skillsEnforcement';
13
17
  import {
14
18
  readMcpAiGateReceipt,
15
19
  resolveMcpAiGateReceiptPath,
@@ -169,6 +173,13 @@ const MCP_RECEIPT_STAGE_ORDER: Readonly<Record<AiGateStage, number>> = {
169
173
  PRE_PUSH: 2,
170
174
  CI: 3,
171
175
  };
176
+ const SKILLS_CONTRACT_SUPPRESSED_EVIDENCE_CODES = new Set([
177
+ 'EVIDENCE_MISSING',
178
+ 'EVIDENCE_INVALID',
179
+ 'EVIDENCE_CHAIN_INVALID',
180
+ 'EVIDENCE_TIMESTAMP_INVALID',
181
+ 'EVIDENCE_STALE',
182
+ ]);
172
183
 
173
184
  const toErrorViolation = (code: string, message: string): AiGateViolation => ({
174
185
  code,
@@ -182,6 +193,16 @@ const toWarnViolation = (code: string, message: string): AiGateViolation => ({
182
193
  message,
183
194
  });
184
195
 
196
+ const toSkillsViolation = (
197
+ resolution: SkillsEnforcementResolution,
198
+ code: string,
199
+ message: string
200
+ ): AiGateViolation => (
201
+ resolution.blocking
202
+ ? toErrorViolation(code, message)
203
+ : toWarnViolation(code, message)
204
+ );
205
+
185
206
  const normalizeRepoStateLifecycleVersions = (repoState: RepoState): RepoState => {
186
207
  const packageVersion = repoState.lifecycle.package_version;
187
208
  const lifecycleVersion = repoState.lifecycle.lifecycle_version;
@@ -426,6 +447,7 @@ const collectActiveRuleIdsCoverageViolations = (params: {
426
447
  const collectPreWritePlatformSkillsViolations = (params: {
427
448
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
428
449
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
450
+ skillsEnforcement: SkillsEnforcementResolution;
429
451
  }): AiGateViolation[] => {
430
452
  const detectedPlatforms = toPreWriteDetectedSkillsPlatforms({
431
453
  platforms: params.evidence.platforms,
@@ -460,7 +482,8 @@ const collectPreWritePlatformSkillsViolations = (params: {
460
482
 
461
483
  if (missingScopeCoverage.length > 0) {
462
484
  violations.push(
463
- toErrorViolation(
485
+ toSkillsViolation(
486
+ params.skillsEnforcement,
464
487
  'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
465
488
  `Detected platforms missing skill-rule coverage in PRE_WRITE: ${missingScopeCoverage.join(' | ')}.`
466
489
  )
@@ -485,7 +508,8 @@ const collectPreWritePlatformSkillsViolations = (params: {
485
508
 
486
509
  if (missingBundlesByPlatform.length > 0) {
487
510
  violations.push(
488
- toErrorViolation(
511
+ toSkillsViolation(
512
+ params.skillsEnforcement,
489
513
  'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
490
514
  `Detected platforms missing required skill bundles in PRE_WRITE: ${missingBundlesByPlatform.join(' | ')}.`
491
515
  )
@@ -513,7 +537,8 @@ const collectPreWritePlatformSkillsViolations = (params: {
513
537
 
514
538
  if (missingCriticalRulesByPlatform.length > 0) {
515
539
  violations.push(
516
- toErrorViolation(
540
+ toSkillsViolation(
541
+ params.skillsEnforcement,
517
542
  'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
518
543
  `Detected platforms missing critical skill-rule enforcement in PRE_WRITE: ${missingCriticalRulesByPlatform.join(' | ')}.`
519
544
  )
@@ -526,6 +551,7 @@ const collectPreWritePlatformSkillsViolations = (params: {
526
551
  const collectPreWriteCrossPlatformCriticalViolations = (params: {
527
552
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
528
553
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
554
+ skillsEnforcement: SkillsEnforcementResolution;
529
555
  }): AiGateViolation[] => {
530
556
  const detectedPlatforms = toPreWriteDetectedSkillsPlatforms({
531
557
  platforms: params.evidence.platforms,
@@ -561,7 +587,8 @@ const collectPreWriteCrossPlatformCriticalViolations = (params: {
561
587
  }
562
588
 
563
589
  return [
564
- toErrorViolation(
590
+ toSkillsViolation(
591
+ params.skillsEnforcement,
565
592
  'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
566
593
  `Cross-platform critical enforcement incomplete in PRE_WRITE: ${missingCriticalCoverage.join(' | ')}.`
567
594
  ),
@@ -571,8 +598,10 @@ const collectPreWriteCrossPlatformCriticalViolations = (params: {
571
598
  const toSkillsContractAssessment = (params: {
572
599
  stage: AiGateStage;
573
600
  repoRoot: string;
601
+ repoState: RepoState;
574
602
  evidenceResult: EvidenceReadResult;
575
603
  requiredLock?: SkillsLockV1;
604
+ skillsEnforcement: SkillsEnforcementResolution;
576
605
  }): AiGateSkillsContractAssessment => {
577
606
  const requiredPlatforms = toLockRequiredPlatforms(params.requiredLock);
578
607
 
@@ -602,7 +631,8 @@ const toSkillsContractAssessment = (params: {
602
631
  violations:
603
632
  requiredPlatforms.length > 0
604
633
  ? [
605
- toErrorViolation(
634
+ toSkillsViolation(
635
+ params.skillsEnforcement,
606
636
  'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
607
637
  `Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
608
638
  ),
@@ -615,7 +645,7 @@ const toSkillsContractAssessment = (params: {
615
645
  const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.evidenceResult.evidence.platforms);
616
646
  const inferredPlatforms = toCoverageInferredPlatforms(coverage);
617
647
  const repoTreeDetectedPlatforms =
618
- requiredPlatforms.length > 0
648
+ params.stage !== 'PRE_WRITE' && requiredPlatforms.length > 0
619
649
  ? toRepoTreeDetectedPlatforms({
620
650
  repoRoot: params.repoRoot,
621
651
  platforms: requiredPlatforms,
@@ -634,12 +664,26 @@ const toSkillsContractAssessment = (params: {
634
664
  : inferredPlatforms.length > 0
635
665
  ? inferredPlatforms
636
666
  : repoTreeDetectedPlatforms;
667
+ const pendingChanges = resolvePendingChanges(params.repoState);
668
+ const detectedPlatformSet = new Set(detectedPlatforms);
637
669
  const assessmentPlatforms =
638
670
  requiredPlatforms.length > 0
639
- ? requiredPlatforms
671
+ ? params.stage === 'PRE_WRITE' && detectedPlatforms.length > 0
672
+ ? requiredPlatforms.filter((platform) => detectedPlatformSet.has(platform))
673
+ : requiredPlatforms
640
674
  : detectedPlatforms;
641
675
 
642
676
  if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) {
677
+ if (params.stage === 'PRE_WRITE' && pendingChanges === 0) {
678
+ return {
679
+ stage: params.stage,
680
+ enforced: false,
681
+ status: 'NOT_APPLICABLE',
682
+ detected_platforms: [],
683
+ requirements: [],
684
+ violations: [],
685
+ };
686
+ }
643
687
  const requirements: AiGateSkillsContractPlatformRequirement[] = requiredPlatforms.map((platform) => ({
644
688
  platform,
645
689
  required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
@@ -665,7 +709,8 @@ const toSkillsContractAssessment = (params: {
665
709
  detected_platforms: [],
666
710
  requirements,
667
711
  violations: [
668
- toErrorViolation(
712
+ toSkillsViolation(
713
+ params.skillsEnforcement,
669
714
  'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
670
715
  `Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
671
716
  ),
@@ -694,7 +739,8 @@ const toSkillsContractAssessment = (params: {
694
739
  const violations: AiGateViolation[] = [];
695
740
  if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) {
696
741
  violations.push(
697
- toErrorViolation(
742
+ toSkillsViolation(
743
+ params.skillsEnforcement,
698
744
  'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
699
745
  `Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
700
746
  )
@@ -759,7 +805,8 @@ const toSkillsContractAssessment = (params: {
759
805
  missingParts.push('evaluated_prefix');
760
806
  }
761
807
  violations.push(
762
- toErrorViolation(
808
+ toSkillsViolation(
809
+ params.skillsEnforcement,
763
810
  'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
764
811
  `Skills contract scope coverage missing for ${platform}: ${missingParts.join(', ')} (${requiredRulePrefix}).`
765
812
  )
@@ -767,7 +814,8 @@ const toSkillsContractAssessment = (params: {
767
814
  }
768
815
  if (missingBundles.length > 0) {
769
816
  violations.push(
770
- toErrorViolation(
817
+ toSkillsViolation(
818
+ params.skillsEnforcement,
771
819
  'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
772
820
  `Skills contract missing bundles for ${platform}: [${missingBundles.join(', ')}].`
773
821
  )
@@ -775,7 +823,8 @@ const toSkillsContractAssessment = (params: {
775
823
  }
776
824
  if (missingCriticalRuleIds.length > 0) {
777
825
  violations.push(
778
- toErrorViolation(
826
+ toSkillsViolation(
827
+ params.skillsEnforcement,
779
828
  'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
780
829
  `Skills contract missing critical rule coverage for ${platform}: [${missingCriticalRuleIds.join(', ')}].`
781
830
  )
@@ -783,7 +832,8 @@ const toSkillsContractAssessment = (params: {
783
832
  }
784
833
  if (!transversalCriticalCovered && requiredAnyTransversalCriticalRuleIds.length > 0) {
785
834
  violations.push(
786
- toErrorViolation(
835
+ toSkillsViolation(
836
+ params.skillsEnforcement,
787
837
  'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
788
838
  `Skills contract missing transversal critical coverage for ${platform}: required_any=[${requiredAnyTransversalCriticalRuleIds.join(', ')}].`
789
839
  )
@@ -867,6 +917,7 @@ const collectPreWriteCoherenceViolations = (params: {
867
917
  )
868
918
  );
869
919
  } else {
920
+ const skillsEnforcement = resolveSkillsEnforcement();
870
921
  if (coverage.stage !== params.evidence.snapshot.stage) {
871
922
  violations.push(
872
923
  toErrorViolation(
@@ -908,12 +959,14 @@ const collectPreWriteCoherenceViolations = (params: {
908
959
  ...collectPreWritePlatformSkillsViolations({
909
960
  evidence: params.evidence,
910
961
  coverage,
962
+ skillsEnforcement,
911
963
  })
912
964
  );
913
965
  violations.push(
914
966
  ...collectPreWriteCrossPlatformCriticalViolations({
915
967
  evidence: params.evidence,
916
968
  coverage,
969
+ skillsEnforcement,
917
970
  })
918
971
  );
919
972
  }
@@ -928,9 +981,7 @@ const collectPreWriteCoherenceViolations = (params: {
928
981
  }
929
982
 
930
983
  if (params.preWriteWorktreeHygiene.enabled && params.repoState.git.available) {
931
- const pendingChanges =
932
- params.repoState.git.pending_changes
933
- ?? (params.repoState.git.staged + params.repoState.git.unstaged);
984
+ const pendingChanges = resolvePendingChanges(params.repoState) ?? 0;
934
985
  if (pendingChanges >= params.preWriteWorktreeHygiene.blockThreshold) {
935
986
  violations.push(
936
987
  toErrorViolation(
@@ -1057,6 +1108,13 @@ const collectGitflowViolations = (
1057
1108
  return violations;
1058
1109
  };
1059
1110
 
1111
+ const resolvePendingChanges = (repoState: RepoState): number | null => {
1112
+ if (!repoState.git.available) {
1113
+ return null;
1114
+ }
1115
+ return repoState.git.pending_changes ?? (repoState.git.staged + repoState.git.unstaged);
1116
+ };
1117
+
1060
1118
  const toPolicyStage = (stage: AiGateStage): SkillsStage => {
1061
1119
  if (stage === 'PRE_WRITE') {
1062
1120
  return 'PRE_COMMIT';
@@ -1222,6 +1280,7 @@ export const evaluateAiGate = (
1222
1280
  policyStage,
1223
1281
  params.repoRoot
1224
1282
  );
1283
+ const skillsEnforcement = resolveSkillsEnforcement();
1225
1284
  const evidenceAssessment = collectEvidenceViolations(
1226
1285
  evidenceResult,
1227
1286
  params.repoRoot,
@@ -1243,15 +1302,22 @@ export const evaluateAiGate = (
1243
1302
  const skillsContract = toSkillsContractAssessment({
1244
1303
  stage: params.stage,
1245
1304
  repoRoot: params.repoRoot,
1305
+ repoState,
1246
1306
  evidenceResult,
1247
1307
  requiredLock: requiredSkillsLock,
1308
+ skillsEnforcement,
1248
1309
  });
1310
+ const suppressSkillsContractViolation = evidenceAssessment.violations.some((violation) =>
1311
+ SKILLS_CONTRACT_SUPPRESSED_EVIDENCE_CODES.has(violation.code)
1312
+ );
1249
1313
  const stageSkillsContractViolations =
1250
- skillsContract.status !== 'FAIL'
1314
+ suppressSkillsContractViolation
1315
+ || skillsContract.status !== 'FAIL'
1251
1316
  || (params.stage === 'PRE_WRITE' && requiredSkillsPlatforms.length === 0)
1252
- ? []
1317
+ ? []
1253
1318
  : [
1254
- toErrorViolation(
1319
+ toSkillsViolation(
1320
+ skillsEnforcement,
1255
1321
  'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE',
1256
1322
  `Skills contract incomplete for ${params.stage}: ${skillsContract.violations.map((violation) => violation.code).join(', ')}.`
1257
1323
  ),
@@ -25,8 +25,12 @@ import type { EvidenceReadResult } from '../evidence/readEvidence';
25
25
  import type { SnapshotFinding } from '../evidence/schema';
26
26
  import { ensureRuntimeArtifactsIgnored } from '../lifecycle/artifacts';
27
27
  import { runPolicyReconcile } from '../lifecycle/policyReconcile';
28
- import { isSeverityAtLeast } from '../../core/rules/Severity';
28
+ import { isSeverityAtLeast, type Severity } from '../../core/rules/Severity';
29
29
  import type { SddDecision } from '../sdd';
30
+ import {
31
+ resolveGitAtomicityEnforcement,
32
+ type GitAtomicityEnforcementResolution,
33
+ } from '../policy/gitAtomicityEnforcement';
30
34
 
31
35
  const PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE =
32
36
  'pumuki pre-push blocked: branch has no upstream tracking reference. Configure upstream first (for example: git push --set-upstream origin <branch>) and retry.';
@@ -38,6 +42,7 @@ const PRE_PUSH_UPSTREAM_MISALIGNED_AHEAD_THRESHOLD = 5;
38
42
 
39
43
  const PRE_COMMIT_EVIDENCE_MAX_AGE_SECONDS = 900;
40
44
  const PRE_PUSH_EVIDENCE_MAX_AGE_SECONDS = 1800;
45
+ const HOOK_GATE_PROGRESS_REMINDER_MS = 2000;
41
46
  const DEFAULT_BLOCKED_REMEDIATION = 'Corrige la causa del bloqueo y vuelve a ejecutar el gate.';
42
47
  const EVIDENCE_FILE_PATH = '.ai_evidence.json';
43
48
 
@@ -106,11 +111,17 @@ type StageRunnerDependencies = {
106
111
  now: () => number;
107
112
  writeHookGateSummary: (message: string) => void;
108
113
  isQuietMode: () => boolean;
114
+ scheduleHookGateProgressReminder: (params: {
115
+ stage: HookStage;
116
+ delayMs: number;
117
+ onProgress: () => void;
118
+ }) => () => void;
109
119
  ensureRuntimeArtifactsIgnored: (repoRoot: string) => void;
110
120
  runPolicyReconcile: typeof runPolicyReconcile;
111
121
  isPathTracked: (repoRoot: string, relativePath: string) => boolean;
112
122
  stagePath: (repoRoot: string, relativePath: string) => void;
113
123
  resolveHeadOid: (repoRoot: string) => string | null;
124
+ resolveGitAtomicityEnforcement: () => GitAtomicityEnforcementResolution;
114
125
  };
115
126
 
116
127
  const defaultDependencies: StageRunnerDependencies = {
@@ -160,6 +171,17 @@ const defaultDependencies: StageRunnerDependencies = {
160
171
  process.stdout.write(`${message}\n`);
161
172
  },
162
173
  isQuietMode: () => process.argv.includes('--quiet'),
174
+ scheduleHookGateProgressReminder: ({ delayMs, onProgress }) => {
175
+ const timer = setTimeout(() => {
176
+ onProgress();
177
+ }, delayMs);
178
+ if (typeof timer === 'object' && timer !== null && 'unref' in timer) {
179
+ timer.unref();
180
+ }
181
+ return () => {
182
+ clearTimeout(timer);
183
+ };
184
+ },
163
185
  ensureRuntimeArtifactsIgnored: (repoRoot) => {
164
186
  try {
165
187
  ensureRuntimeArtifactsIgnored(repoRoot);
@@ -185,6 +207,7 @@ const defaultDependencies: StageRunnerDependencies = {
185
207
  return null;
186
208
  }
187
209
  },
210
+ resolveGitAtomicityEnforcement,
188
211
  };
189
212
 
190
213
  const getDependencies = (
@@ -253,6 +276,25 @@ const shouldRetryAfterPolicyReconcile = (params: {
253
276
  repoRoot: string;
254
277
  stage: 'PRE_COMMIT' | 'PRE_PUSH';
255
278
  }): boolean => {
279
+ const toEvidenceViolationSeverity = (violation: {
280
+ severity?: string | null;
281
+ level?: string | null;
282
+ }): Severity => {
283
+ const candidate = typeof violation.severity === 'string'
284
+ ? violation.severity
285
+ : typeof violation.level === 'string'
286
+ ? violation.level
287
+ : null;
288
+ if (
289
+ candidate === 'INFO' ||
290
+ candidate === 'WARN' ||
291
+ candidate === 'ERROR' ||
292
+ candidate === 'CRITICAL'
293
+ ) {
294
+ return candidate;
295
+ }
296
+ return 'INFO';
297
+ };
256
298
  const evidence = params.dependencies.readEvidence(params.repoRoot);
257
299
  if (!evidence) {
258
300
  return false;
@@ -260,11 +302,15 @@ const shouldRetryAfterPolicyReconcile = (params: {
260
302
  const stageCodes = new Set<string>();
261
303
  if (evidence.snapshot.stage === params.stage) {
262
304
  for (const finding of evidence.snapshot.findings) {
263
- stageCodes.add(finding.code);
305
+ if (isSeverityAtLeast(finding.severity, 'ERROR')) {
306
+ stageCodes.add(finding.code);
307
+ }
264
308
  }
265
309
  }
266
310
  for (const violation of evidence.ai_gate.violations) {
267
- stageCodes.add(violation.code);
311
+ if (isSeverityAtLeast(toEvidenceViolationSeverity(violation), 'ERROR')) {
312
+ stageCodes.add(violation.code);
313
+ }
268
314
  }
269
315
  for (const code of stageCodes) {
270
316
  if (HOOK_POLICY_RECONCILE_CODES.has(code)) {
@@ -384,38 +430,58 @@ const runHookGateWithPolicyRetry = async (params: {
384
430
  scope: Parameters<typeof runPlatformGate>[0]['scope'];
385
431
  sddDecisionOverride?: Pick<SddDecision, 'allowed' | 'code' | 'message'>;
386
432
  }): Promise<{ exitCode: number; policyTrace: HookPolicyTrace }> => {
387
- const firstAttempt = await runHookGateAttempt({
388
- dependencies: params.dependencies,
389
- stage: params.stage,
390
- scope: params.scope,
391
- sddDecisionOverride: params.sddDecisionOverride,
392
- });
393
- if (firstAttempt.exitCode === 0) {
394
- return firstAttempt;
395
- }
396
- if (!isHookPolicyAutoReconcileEnabled()) {
397
- return firstAttempt;
433
+ if (!params.dependencies.isQuietMode()) {
434
+ params.dependencies.writeHookGateSummary(
435
+ `[pumuki][hook-gate] stage=${params.stage} decision=PENDING status=STARTED`
436
+ );
398
437
  }
399
- if (
400
- !shouldRetryAfterPolicyReconcile({
438
+ const cancelProgressReminder = params.dependencies.isQuietMode()
439
+ ? () => {}
440
+ : params.dependencies.scheduleHookGateProgressReminder({
441
+ stage: params.stage,
442
+ delayMs: HOOK_GATE_PROGRESS_REMINDER_MS,
443
+ onProgress: () => {
444
+ params.dependencies.writeHookGateSummary(
445
+ `[pumuki][hook-gate] stage=${params.stage} decision=PENDING status=RUNNING`
446
+ );
447
+ },
448
+ });
449
+ try {
450
+ const firstAttempt = await runHookGateAttempt({
401
451
  dependencies: params.dependencies,
452
+ stage: params.stage,
453
+ scope: params.scope,
454
+ sddDecisionOverride: params.sddDecisionOverride,
455
+ });
456
+ if (firstAttempt.exitCode === 0) {
457
+ return firstAttempt;
458
+ }
459
+ if (!isHookPolicyAutoReconcileEnabled()) {
460
+ return firstAttempt;
461
+ }
462
+ if (
463
+ !shouldRetryAfterPolicyReconcile({
464
+ dependencies: params.dependencies,
465
+ repoRoot: params.repoRoot,
466
+ stage: params.stage,
467
+ })
468
+ ) {
469
+ return firstAttempt;
470
+ }
471
+ params.dependencies.runPolicyReconcile({
402
472
  repoRoot: params.repoRoot,
473
+ strict: true,
474
+ apply: true,
475
+ });
476
+ return runHookGateAttempt({
477
+ dependencies: params.dependencies,
403
478
  stage: params.stage,
404
- })
405
- ) {
406
- return firstAttempt;
479
+ scope: params.scope,
480
+ sddDecisionOverride: params.sddDecisionOverride,
481
+ });
482
+ } finally {
483
+ cancelProgressReminder();
407
484
  }
408
- params.dependencies.runPolicyReconcile({
409
- repoRoot: params.repoRoot,
410
- strict: true,
411
- apply: true,
412
- });
413
- return runHookGateAttempt({
414
- dependencies: params.dependencies,
415
- stage: params.stage,
416
- scope: params.scope,
417
- sddDecisionOverride: params.sddDecisionOverride,
418
- });
419
485
  };
420
486
 
421
487
  const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
@@ -643,7 +709,8 @@ const enforceGitAtomicityGate = (params: {
643
709
  fromRef: params.fromRef,
644
710
  toRef: params.toRef,
645
711
  });
646
- if (!atomicity.enabled || atomicity.allowed) {
712
+ const enforcement = params.dependencies.resolveGitAtomicityEnforcement();
713
+ if (!atomicity.enabled || atomicity.allowed || !enforcement.blocking) {
647
714
  return false;
648
715
  }
649
716
  const firstViolation = atomicity.violations[0];
@@ -70,6 +70,7 @@ import {
70
70
  type RemoteCiDiagnostics,
71
71
  } from './remoteCiDiagnostics';
72
72
  import { runPolicyReconcile } from './policyReconcile';
73
+ import { resolvePreWriteEnforcement, type PreWriteEnforcementResolution } from '../policy/preWriteEnforcement';
73
74
 
74
75
  type LifecycleCommand =
75
76
  | 'bootstrap'
@@ -567,6 +568,9 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
567
568
  if (!isLifecycleCommand(commandRaw)) {
568
569
  throw new Error(`Unknown command "${commandRaw}".\n\n${HELP_TEXT}`);
569
570
  }
571
+ if (argv.slice(1).some((arg) => arg === '--help' || arg === '-h')) {
572
+ throw new Error(HELP_TEXT);
573
+ }
570
574
 
571
575
  let purgeArtifacts = false;
572
576
  let updateSpec: ParsedArgs['updateSpec'];
@@ -1562,6 +1566,7 @@ const PRE_WRITE_POLICY_RECONCILE_COMMAND =
1562
1566
  type PreWriteValidationEnvelope = {
1563
1567
  sdd: ReturnType<typeof evaluateSddPolicy>;
1564
1568
  ai_gate: ReturnType<typeof evaluateAiGate>;
1569
+ pre_write_enforcement: PreWriteEnforcementResolution;
1565
1570
  policy_validation: LifecyclePolicyValidationSnapshot;
1566
1571
  automation: PreWriteAutomationTrace;
1567
1572
  bootstrap: {
@@ -1841,6 +1846,7 @@ const resolveAiGateViolationLocation = (code: string) => {
1841
1846
  const buildPreWriteValidationEnvelope = (
1842
1847
  result: ReturnType<typeof evaluateSddPolicy>,
1843
1848
  aiGate: ReturnType<typeof evaluateAiGate>,
1849
+ preWriteEnforcement: PreWriteEnforcementResolution,
1844
1850
  policyValidation: LifecyclePolicyValidationSnapshot,
1845
1851
  automation: PreWriteAutomationTrace,
1846
1852
  bootstrap: PreWriteOpenSpecBootstrapTrace,
@@ -1848,6 +1854,7 @@ const buildPreWriteValidationEnvelope = (
1848
1854
  ): PreWriteValidationEnvelope => ({
1849
1855
  sdd: result,
1850
1856
  ai_gate: aiGate,
1857
+ pre_write_enforcement: preWriteEnforcement,
1851
1858
  policy_validation: policyValidation,
1852
1859
  automation: {
1853
1860
  attempted: automation.attempted,
@@ -1891,6 +1898,50 @@ const writeLoopAttemptEvidence = (params: {
1891
1898
  return relativePath;
1892
1899
  };
1893
1900
 
1901
+ const withTemporaryEnvOverrides = <T>(
1902
+ overrides: Readonly<Record<string, string | undefined>>,
1903
+ callback: () => T
1904
+ ): T => {
1905
+ const previous = new Map<string, string | undefined>();
1906
+ for (const [key, value] of Object.entries(overrides)) {
1907
+ previous.set(key, process.env[key]);
1908
+ if (typeof value === 'undefined') {
1909
+ delete process.env[key];
1910
+ } else {
1911
+ process.env[key] = value;
1912
+ }
1913
+ }
1914
+ try {
1915
+ return callback();
1916
+ } finally {
1917
+ for (const [key, value] of previous.entries()) {
1918
+ if (typeof value === 'undefined') {
1919
+ delete process.env[key];
1920
+ } else {
1921
+ process.env[key] = value;
1922
+ }
1923
+ }
1924
+ }
1925
+ };
1926
+
1927
+ const runRawPreWriteAiGateCheck = (params: {
1928
+ repoRoot: string;
1929
+ requireMcpReceipt: boolean;
1930
+ }): ReturnType<typeof evaluateAiGate> =>
1931
+ withTemporaryEnvOverrides(
1932
+ {
1933
+ PUMUKI_SKILLS_ENFORCEMENT: process.env.PUMUKI_SKILLS_ENFORCEMENT ?? 'strict',
1934
+ PUMUKI_TDD_BDD_ENFORCEMENT: process.env.PUMUKI_TDD_BDD_ENFORCEMENT ?? 'strict',
1935
+ PUMUKI_HEURISTICS_ENFORCEMENT: process.env.PUMUKI_HEURISTICS_ENFORCEMENT ?? 'strict',
1936
+ },
1937
+ () =>
1938
+ runEnterpriseAiGateCheck({
1939
+ repoRoot: params.repoRoot,
1940
+ stage: 'PRE_WRITE',
1941
+ requireMcpReceipt: params.requireMcpReceipt,
1942
+ }).result
1943
+ );
1944
+
1894
1945
  export const runLifecycleCli = async (
1895
1946
  argv: ReadonlyArray<string>,
1896
1947
  dependencies: Partial<LifecycleCliDependencies> = {}
@@ -2473,6 +2524,13 @@ export const runLifecycleCli = async (
2473
2524
  let result = evaluateSddPolicy({
2474
2525
  stage: parsed.sddStage ?? 'PRE_COMMIT',
2475
2526
  });
2527
+ const preWriteEnforcement = result.stage === 'PRE_WRITE'
2528
+ ? resolvePreWriteEnforcement()
2529
+ : {
2530
+ mode: 'strict',
2531
+ source: 'default',
2532
+ blocking: true,
2533
+ } satisfies PreWriteEnforcementResolution;
2476
2534
  const policyValidation = readLifecyclePolicyValidationSnapshot(process.cwd());
2477
2535
  const preWriteAutoBootstrapEnabled = process.env.PUMUKI_PREWRITE_AUTO_BOOTSTRAP !== '0';
2478
2536
  const preWriteBootstrapTrace: PreWriteOpenSpecBootstrapTrace = {
@@ -2534,17 +2592,24 @@ export const runLifecycleCli = async (
2534
2592
  automationTrace.attempted = auto.trace.attempted;
2535
2593
  automationTrace.actions = auto.trace.actions;
2536
2594
  }
2595
+ const rawPreWriteAiGate = result.stage === 'PRE_WRITE' && aiGate
2596
+ ? runRawPreWriteAiGateCheck({
2597
+ repoRoot: process.cwd(),
2598
+ requireMcpReceipt: true,
2599
+ })
2600
+ : null;
2537
2601
  const nextAction = resolvePreWriteNextAction({
2538
2602
  sdd: result,
2539
- aiGate,
2603
+ aiGate: rawPreWriteAiGate ?? aiGate,
2540
2604
  });
2541
2605
  if (parsed.json) {
2542
2606
  writeInfo(
2543
2607
  JSON.stringify(
2544
- aiGate
2608
+ (rawPreWriteAiGate ?? aiGate)
2545
2609
  ? buildPreWriteValidationEnvelope(
2546
2610
  result,
2547
- aiGate,
2611
+ rawPreWriteAiGate ?? aiGate!,
2612
+ preWriteEnforcement,
2548
2613
  policyValidation,
2549
2614
  automationTrace,
2550
2615
  preWriteBootstrapTrace,
@@ -2582,6 +2647,9 @@ export const runLifecycleCli = async (
2582
2647
  writeInfo(
2583
2648
  `[pumuki][sdd] openspec auto-bootstrap: enabled=${preWriteBootstrapTrace.enabled ? 'yes' : 'no'} attempted=${preWriteBootstrapTrace.attempted ? 'yes' : 'no'} status=${preWriteBootstrapTrace.status} actions=${preWriteBootstrapTrace.actions.join(',') || 'none'}`
2584
2649
  );
2650
+ writeInfo(
2651
+ `[pumuki][sdd] pre-write enforcement: mode=${preWriteEnforcement.mode} source=${preWriteEnforcement.source} blocking=${preWriteEnforcement.blocking ? 'yes' : 'no'}`
2652
+ );
2585
2653
  if (preWriteBootstrapTrace.details) {
2586
2654
  writeInfo(
2587
2655
  `[pumuki][sdd] openspec auto-bootstrap details: ${preWriteBootstrapTrace.details}`
@@ -2617,7 +2685,7 @@ export const runLifecycleCli = async (
2617
2685
  aiGateResult: aiGate,
2618
2686
  });
2619
2687
  }
2620
- if (!result.decision.allowed) {
2688
+ if (!result.decision.allowed && preWriteEnforcement.blocking) {
2621
2689
  activeDependencies.emitGateBlockedNotification({
2622
2690
  repoRoot: process.cwd(),
2623
2691
  stage: result.stage,
@@ -2631,7 +2699,7 @@ export const runLifecycleCli = async (
2631
2699
  });
2632
2700
  return 1;
2633
2701
  }
2634
- if (aiGate && !aiGate.allowed) {
2702
+ if (aiGate && !aiGate.allowed && preWriteEnforcement.blocking) {
2635
2703
  const firstViolation = aiGate.violations[0];
2636
2704
  const causeCode = firstViolation?.code ?? 'AI_GATE_BLOCKED';
2637
2705
  const causeMessage = firstViolation?.message ?? 'AI gate blocked PRE_WRITE stage.';
@@ -423,9 +423,9 @@ const evaluateEvidenceSourceDriftCheck = (params: {
423
423
  return buildDeepCheck({
424
424
  id: 'evidence-source-drift',
425
425
  status: 'fail',
426
- severity: 'error',
426
+ severity: 'warning',
427
427
  message: '.ai_evidence.json is missing.',
428
- remediation: 'Regenerate evidence with a full audit before continuing with enterprise checks.',
428
+ remediation: 'Regenerate evidence with a full audit before relying on enterprise diagnostics or gates.',
429
429
  metadata: {
430
430
  path: evidenceResult.source_descriptor.path,
431
431
  },
@@ -456,15 +456,17 @@ const evaluateEvidenceSourceDriftCheck = (params: {
456
456
  const ageSeconds = Number.isFinite(timestampMs)
457
457
  ? Math.max(0, Math.floor((nowMs - timestampMs) / 1000))
458
458
  : null;
459
+ let operationalDriftOnly = true;
459
460
 
460
461
  if (!Number.isFinite(timestampMs)) {
461
462
  toError();
463
+ operationalDriftOnly = false;
462
464
  violations.push('Evidence timestamp is invalid.');
463
465
  } else if (timestampMs > nowMs) {
464
466
  toError();
467
+ operationalDriftOnly = false;
465
468
  violations.push('Evidence timestamp is in the future.');
466
469
  } else if (ageSeconds !== null && ageSeconds > DEEP_EVIDENCE_MAX_AGE_SECONDS) {
467
- toError();
468
470
  violations.push(
469
471
  `Evidence is stale (${ageSeconds}s > ${DEEP_EVIDENCE_MAX_AGE_SECONDS}s).`
470
472
  );
@@ -488,6 +490,7 @@ const evaluateEvidenceSourceDriftCheck = (params: {
488
490
  toCanonicalPath(expectedEvidencePath)
489
491
  ) {
490
492
  toError();
493
+ operationalDriftOnly = false;
491
494
  violations.push(
492
495
  `Evidence source path mismatch (${evidenceResult.source_descriptor.path} != ${expectedEvidencePath}).`
493
496
  );
@@ -498,6 +501,7 @@ const evaluateEvidenceSourceDriftCheck = (params: {
498
501
  !/^sha256:[0-9a-f]{64}$/i.test(evidenceResult.source_descriptor.digest)
499
502
  ) {
500
503
  toError();
504
+ operationalDriftOnly = false;
501
505
  violations.push('Evidence digest format is invalid.');
502
506
  }
503
507
 
@@ -508,6 +512,7 @@ const evaluateEvidenceSourceDriftCheck = (params: {
508
512
  toCanonicalPath(evidenceRepoRoot) !== toCanonicalPath(params.repoRoot)
509
513
  ) {
510
514
  toError();
515
+ operationalDriftOnly = false;
511
516
  violations.push(`Evidence repo_root mismatch (${evidenceRepoRoot} != ${params.repoRoot}).`);
512
517
  }
513
518
 
@@ -520,6 +525,7 @@ const evaluateEvidenceSourceDriftCheck = (params: {
520
525
  evidenceBranch !== currentBranch
521
526
  ) {
522
527
  toError();
528
+ operationalDriftOnly = false;
523
529
  violations.push(`Evidence branch mismatch (${evidenceBranch} != ${currentBranch}).`);
524
530
  }
525
531
 
@@ -535,6 +541,9 @@ const evaluateEvidenceSourceDriftCheck = (params: {
535
541
  }
536
542
 
537
543
  if (violations.length > 0) {
544
+ if (operationalDriftOnly) {
545
+ severity = 'warning';
546
+ }
538
547
  return buildDeepCheck({
539
548
  id: 'evidence-source-drift',
540
549
  status: 'fail',
@@ -98,7 +98,7 @@ export const buildPreWriteAutomationTrace = async (params: {
98
98
  attempted: false,
99
99
  actions: [],
100
100
  };
101
- if (params.sdd.stage !== 'PRE_WRITE' || !params.sdd.decision.allowed || params.aiGate.allowed) {
101
+ if (params.sdd.stage !== 'PRE_WRITE' || params.aiGate.allowed) {
102
102
  return {
103
103
  aiGate: params.aiGate,
104
104
  trace,
@@ -11,6 +11,16 @@ import { readEvidence } from '../evidence/readEvidence';
11
11
  import type { AiEvidenceV2_1, SnapshotFinding } from '../evidence/schema';
12
12
  import { GitService, type IGitService } from '../git/GitService';
13
13
  import type { Fact } from '../../core/facts/Fact';
14
+ import {
15
+ evaluateGitAtomicity,
16
+ type GitAtomicityEvaluation,
17
+ type GitAtomicityViolation,
18
+ } from '../git/gitAtomicity';
19
+ import {
20
+ resolveCiBaseRef,
21
+ resolvePrePushBootstrapBaseRef,
22
+ resolveUpstreamRef,
23
+ } from '../git/resolveGitRefs';
14
24
  import { ensureRuntimeArtifactsIgnored } from './artifacts';
15
25
  import {
16
26
  emitAuditSummaryNotificationFromEvidence,
@@ -22,6 +32,10 @@ import {
22
32
  resolvePumukiVersionMetadata,
23
33
  type PumukiVersionMetadata,
24
34
  } from './packageInfo';
35
+ import {
36
+ resolveGitAtomicityEnforcement,
37
+ type GitAtomicityEnforcementResolution,
38
+ } from '../policy/gitAtomicityEnforcement';
25
39
 
26
40
  export type LifecycleWatchStage = 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
27
41
  export type LifecycleWatchScope = 'workingTree' | 'staged' | 'repoAndStaged' | 'repo';
@@ -82,6 +96,15 @@ type LifecycleWatchDependencies = {
82
96
  resolveRepoRoot: () => string;
83
97
  readChangeToken: (repoRoot: string) => string;
84
98
  resolvePolicyForStage: (stage: LifecycleWatchStage) => ResolvedStagePolicy;
99
+ resolveUpstreamRef: typeof resolveUpstreamRef;
100
+ resolvePrePushBootstrapBaseRef: typeof resolvePrePushBootstrapBaseRef;
101
+ resolveCiBaseRef: typeof resolveCiBaseRef;
102
+ evaluateGitAtomicity: (params: {
103
+ repoRoot: string;
104
+ stage: LifecycleWatchStage;
105
+ fromRef?: string;
106
+ toRef?: string;
107
+ }) => GitAtomicityEvaluation;
85
108
  runPlatformGate: typeof runPlatformGate;
86
109
  resolveFactsForGateScope: typeof resolveFactsForGateScope;
87
110
  readEvidence: (repoRoot: string) => AiEvidenceV2_1 | undefined;
@@ -90,6 +113,7 @@ type LifecycleWatchDependencies = {
90
113
  emitGateBlockedNotification: typeof emitGateBlockedNotification;
91
114
  runPolicyReconcile: typeof runPolicyReconcile;
92
115
  resolvePumukiVersionMetadata: (params?: { repoRoot?: string }) => PumukiVersionMetadata;
116
+ resolveGitAtomicityEnforcement: () => GitAtomicityEnforcementResolution;
93
117
  nowMs: () => number;
94
118
  sleep: (ms: number) => Promise<void>;
95
119
  };
@@ -115,6 +139,16 @@ const defaultDependencies: LifecycleWatchDependencies = {
115
139
  readChangeToken: (repoRoot) =>
116
140
  defaultGitService.runGit(['status', '--porcelain=v1', '--untracked-files=all'], repoRoot),
117
141
  resolvePolicyForStage: (stage) => resolvePolicyForStage(stage),
142
+ resolveUpstreamRef,
143
+ resolvePrePushBootstrapBaseRef,
144
+ resolveCiBaseRef,
145
+ evaluateGitAtomicity: (params) =>
146
+ evaluateGitAtomicity({
147
+ repoRoot: params.repoRoot,
148
+ stage: params.stage,
149
+ fromRef: params.fromRef,
150
+ toRef: params.toRef,
151
+ }),
118
152
  runPlatformGate,
119
153
  resolveFactsForGateScope,
120
154
  readEvidence,
@@ -128,6 +162,7 @@ const defaultDependencies: LifecycleWatchDependencies = {
128
162
  emitGateBlockedNotification,
129
163
  runPolicyReconcile,
130
164
  resolvePumukiVersionMetadata,
165
+ resolveGitAtomicityEnforcement,
131
166
  nowMs: () => Date.now(),
132
167
  sleep: async (ms) => {
133
168
  await sleepTimer(ms);
@@ -280,6 +315,56 @@ const collectEvaluatedFilesFromFacts = (facts: ReadonlyArray<Fact>): ReadonlyArr
280
315
  return collectChangedFilesFromFacts(facts);
281
316
  };
282
317
 
318
+ const resolveWatchAtomicityRange = (params: {
319
+ stage: LifecycleWatchStage;
320
+ scope: LifecycleWatchScope;
321
+ dependencies: LifecycleWatchDependencies;
322
+ }): { fromRef?: string; toRef?: string } => {
323
+ if (params.stage === 'PRE_COMMIT') {
324
+ return {};
325
+ }
326
+ if (params.stage === 'CI') {
327
+ return {
328
+ fromRef: params.dependencies.resolveCiBaseRef(),
329
+ toRef: 'HEAD',
330
+ };
331
+ }
332
+ if (params.scope === 'workingTree') {
333
+ return {};
334
+ }
335
+ const upstreamRef = params.dependencies.resolveUpstreamRef();
336
+ if (typeof upstreamRef === 'string' && upstreamRef.trim().length > 0) {
337
+ return {
338
+ fromRef: upstreamRef,
339
+ toRef: 'HEAD',
340
+ };
341
+ }
342
+ const bootstrapBaseRef = params.dependencies.resolvePrePushBootstrapBaseRef();
343
+ if (bootstrapBaseRef !== 'HEAD') {
344
+ return {
345
+ fromRef: bootstrapBaseRef,
346
+ toRef: 'HEAD',
347
+ };
348
+ }
349
+ return {};
350
+ };
351
+
352
+ const toAtomicitySnapshotFindings = (
353
+ violations: ReadonlyArray<GitAtomicityViolation>,
354
+ enforcement: GitAtomicityEnforcementResolution
355
+ ): ReadonlyArray<SnapshotFinding> =>
356
+ violations.map((violation) => ({
357
+ ruleId: 'governance.git.atomicity',
358
+ severity: enforcement.blocking ? 'ERROR' : 'WARN',
359
+ code: violation.code,
360
+ message: violation.message,
361
+ file: '.pumuki/git-atomicity.json',
362
+ matchedBy: 'LifecycleWatch',
363
+ source: 'skills.backend.runtime-hygiene',
364
+ blocking: enforcement.blocking,
365
+ expected_fix: violation.remediation,
366
+ }));
367
+
283
368
  const toFirstCause = (params: {
284
369
  evidence: AiEvidenceV2_1 | undefined;
285
370
  matchedFindings: ReadonlyArray<SnapshotFinding>;
@@ -403,6 +488,36 @@ export const runLifecycleWatch = async (
403
488
  gateOutcome: 'BLOCK' | 'WARN' | 'ALLOW' | 'NO_EVIDENCE';
404
489
  topCodes: ReadonlyArray<string>;
405
490
  }> => {
491
+ const atomicityRange = resolveWatchAtomicityRange({
492
+ stage,
493
+ scope,
494
+ dependencies: activeDependencies,
495
+ });
496
+ const atomicity = activeDependencies.evaluateGitAtomicity({
497
+ repoRoot,
498
+ stage,
499
+ fromRef: atomicityRange.fromRef,
500
+ toRef: atomicityRange.toRef,
501
+ });
502
+ const atomicityEnforcement = activeDependencies.resolveGitAtomicityEnforcement();
503
+ const atomicityFindings =
504
+ atomicity.enabled && !atomicity.allowed
505
+ ? toAtomicitySnapshotFindings(atomicity.violations, atomicityEnforcement)
506
+ : [];
507
+ if (atomicityFindings.length > 0 && atomicityEnforcement.blocking) {
508
+ const allFindings = atomicityFindings;
509
+ const matchedFindings = allFindings.filter((finding) =>
510
+ isSeverityAtLeast(finding.severity, thresholdSeverity)
511
+ );
512
+ return {
513
+ gateExitCode: 1,
514
+ evidence: undefined,
515
+ allFindings,
516
+ matchedFindings,
517
+ gateOutcome: 'BLOCK',
518
+ topCodes: toTopCodes(matchedFindings),
519
+ };
520
+ }
406
521
  const manifestSnapshot = captureWatchManifestGuardSnapshot(repoRoot);
407
522
  let gateExitCode = await activeDependencies.runPlatformGate({
408
523
  policy: resolvedPolicy.policy,
@@ -432,20 +547,28 @@ export const runLifecycleWatch = async (
432
547
  source: 'skills.backend.runtime-hygiene',
433
548
  }
434
549
  : null;
550
+ const allFindingsWithAtomicity = [...allFindingsBase, ...atomicityFindings];
551
+ const matchedFindingsWithAtomicity = allFindingsWithAtomicity.filter((finding) =>
552
+ isSeverityAtLeast(finding.severity, thresholdSeverity)
553
+ );
435
554
  const allFindings = manifestMutationFinding
436
- ? [...allFindingsBase, manifestMutationFinding]
437
- : allFindingsBase;
555
+ ? [...allFindingsWithAtomicity, manifestMutationFinding]
556
+ : allFindingsWithAtomicity;
438
557
  const matchedFindings = manifestMutationFinding
439
- ? [...matchedFindingsBase, manifestMutationFinding]
440
- : matchedFindingsBase;
558
+ ? [...matchedFindingsWithAtomicity, manifestMutationFinding]
559
+ : matchedFindingsWithAtomicity;
441
560
  const rawGateOutcome =
442
561
  evidence?.snapshot.outcome ??
443
562
  (gateExitCode !== 0 ? 'BLOCK' : 'NO_EVIDENCE');
444
- const gateOutcome = manifestMutationFinding
445
- ? 'BLOCK'
446
- : rawGateOutcome === 'PASS'
563
+ const normalizedGateOutcome =
564
+ rawGateOutcome === 'PASS'
447
565
  ? 'ALLOW'
448
566
  : rawGateOutcome;
567
+ const gateOutcome = manifestMutationFinding
568
+ ? 'BLOCK'
569
+ : normalizedGateOutcome === 'ALLOW' && atomicityFindings.length > 0
570
+ ? 'WARN'
571
+ : normalizedGateOutcome;
449
572
  if (manifestMutationFinding) {
450
573
  gateExitCode = 1;
451
574
  process.stderr.write(
@@ -465,9 +588,32 @@ export const runLifecycleWatch = async (
465
588
 
466
589
  let evaluation = await runEvaluation(activeDependencies.resolvePolicyForStage(stage));
467
590
  if (autoPolicyReconcileEnabled && evaluation.gateExitCode !== 0) {
591
+ const toEvidenceViolationSeverity = (violation: {
592
+ severity?: string | null;
593
+ level?: string | null;
594
+ }): Severity => {
595
+ const candidate = typeof violation.severity === 'string'
596
+ ? violation.severity
597
+ : typeof violation.level === 'string'
598
+ ? violation.level
599
+ : null;
600
+ if (
601
+ candidate === 'INFO' ||
602
+ candidate === 'WARN' ||
603
+ candidate === 'ERROR' ||
604
+ candidate === 'CRITICAL'
605
+ ) {
606
+ return candidate;
607
+ }
608
+ return 'INFO';
609
+ };
468
610
  const findingCodes = new Set<string>([
469
- ...evaluation.allFindings.map((finding) => finding.code),
470
- ...((evaluation.evidence?.ai_gate.violations ?? []).map((violation) => violation.code)),
611
+ ...evaluation.allFindings
612
+ .filter((finding) => isSeverityAtLeast(finding.severity, 'ERROR'))
613
+ .map((finding) => finding.code),
614
+ ...((evaluation.evidence?.ai_gate.violations ?? [])
615
+ .filter((violation) => isSeverityAtLeast(toEvidenceViolationSeverity(violation), 'ERROR'))
616
+ .map((violation) => violation.code)),
471
617
  ]);
472
618
  const shouldAttemptAutoReconcile = [...findingCodes].some((code) =>
473
619
  WATCH_POLICY_RECONCILE_CODES.has(code)
@@ -0,0 +1,57 @@
1
+ export type GitAtomicityEnforcementMode = 'advisory' | 'strict';
2
+
3
+ export type GitAtomicityEnforcementResolution = {
4
+ mode: GitAtomicityEnforcementMode;
5
+ source: 'default' | 'env';
6
+ blocking: boolean;
7
+ };
8
+
9
+ const GIT_ATOMICITY_ENFORCEMENT_ENV = 'PUMUKI_GIT_ATOMICITY_ENFORCEMENT';
10
+
11
+ const toGitAtomicityEnforcementMode = (
12
+ value: string | undefined
13
+ ): GitAtomicityEnforcementMode | null => {
14
+ if (typeof value !== 'string') {
15
+ return null;
16
+ }
17
+ const normalized = value.trim().toLowerCase();
18
+ if (
19
+ normalized === 'strict'
20
+ || normalized === '1'
21
+ || normalized === 'true'
22
+ || normalized === 'yes'
23
+ || normalized === 'on'
24
+ ) {
25
+ return 'strict';
26
+ }
27
+ if (
28
+ normalized === 'advisory'
29
+ || normalized === 'warn'
30
+ || normalized === 'warning'
31
+ || normalized === '0'
32
+ || normalized === 'false'
33
+ || normalized === 'no'
34
+ || normalized === 'off'
35
+ ) {
36
+ return 'advisory';
37
+ }
38
+ return null;
39
+ };
40
+
41
+ export const resolveGitAtomicityEnforcement = (): GitAtomicityEnforcementResolution => {
42
+ const modeFromEnv = toGitAtomicityEnforcementMode(
43
+ process.env[GIT_ATOMICITY_ENFORCEMENT_ENV]
44
+ );
45
+ if (modeFromEnv) {
46
+ return {
47
+ mode: modeFromEnv,
48
+ source: 'env',
49
+ blocking: modeFromEnv === 'strict',
50
+ };
51
+ }
52
+ return {
53
+ mode: 'advisory',
54
+ source: 'default',
55
+ blocking: false,
56
+ };
57
+ };
@@ -0,0 +1,41 @@
1
+ export type PreWriteEnforcementMode = 'advisory' | 'strict';
2
+
3
+ export type PreWriteEnforcementResolution = {
4
+ mode: PreWriteEnforcementMode;
5
+ source: 'default' | 'env';
6
+ blocking: boolean;
7
+ };
8
+
9
+ const PRE_WRITE_ENFORCEMENT_ENV = 'PUMUKI_PREWRITE_ENFORCEMENT';
10
+
11
+ const toPreWriteEnforcementMode = (
12
+ value: string | undefined
13
+ ): PreWriteEnforcementMode | null => {
14
+ if (typeof value !== 'string') {
15
+ return null;
16
+ }
17
+ const normalized = value.trim().toLowerCase();
18
+ if (normalized === 'strict') {
19
+ return 'strict';
20
+ }
21
+ if (normalized === 'advisory' || normalized === 'warn' || normalized === 'warning') {
22
+ return 'advisory';
23
+ }
24
+ return null;
25
+ };
26
+
27
+ export const resolvePreWriteEnforcement = (): PreWriteEnforcementResolution => {
28
+ const modeFromEnv = toPreWriteEnforcementMode(process.env[PRE_WRITE_ENFORCEMENT_ENV]);
29
+ if (modeFromEnv) {
30
+ return {
31
+ mode: modeFromEnv,
32
+ source: 'env',
33
+ blocking: modeFromEnv === 'strict',
34
+ };
35
+ }
36
+ return {
37
+ mode: 'advisory',
38
+ source: 'default',
39
+ blocking: false,
40
+ };
41
+ };
@@ -0,0 +1,64 @@
1
+ export type SkillsEnforcementMode = 'advisory' | 'strict';
2
+
3
+ export type SkillsEnforcementResolution = {
4
+ mode: SkillsEnforcementMode;
5
+ source: 'default' | 'env' | 'prewrite';
6
+ blocking: boolean;
7
+ };
8
+
9
+ const SKILLS_ENFORCEMENT_ENV = 'PUMUKI_SKILLS_ENFORCEMENT';
10
+ const PRE_WRITE_ENFORCEMENT_ENV = 'PUMUKI_PREWRITE_ENFORCEMENT';
11
+
12
+ const toSkillsEnforcementMode = (
13
+ value: string | undefined
14
+ ): SkillsEnforcementMode | null => {
15
+ if (typeof value !== 'string') {
16
+ return null;
17
+ }
18
+ const normalized = value.trim().toLowerCase();
19
+ if (
20
+ normalized === 'strict'
21
+ || normalized === '1'
22
+ || normalized === 'true'
23
+ || normalized === 'yes'
24
+ || normalized === 'on'
25
+ ) {
26
+ return 'strict';
27
+ }
28
+ if (
29
+ normalized === 'advisory'
30
+ || normalized === 'warn'
31
+ || normalized === 'warning'
32
+ || normalized === '0'
33
+ || normalized === 'false'
34
+ || normalized === 'no'
35
+ || normalized === 'off'
36
+ ) {
37
+ return 'advisory';
38
+ }
39
+ return null;
40
+ };
41
+
42
+ export const resolveSkillsEnforcement = (): SkillsEnforcementResolution => {
43
+ const modeFromEnv = toSkillsEnforcementMode(process.env[SKILLS_ENFORCEMENT_ENV]);
44
+ if (modeFromEnv) {
45
+ return {
46
+ mode: modeFromEnv,
47
+ source: 'env',
48
+ blocking: modeFromEnv === 'strict',
49
+ };
50
+ }
51
+ const preWriteMode = process.env[PRE_WRITE_ENFORCEMENT_ENV]?.trim().toLowerCase();
52
+ if (preWriteMode === 'strict') {
53
+ return {
54
+ mode: 'strict',
55
+ source: 'prewrite',
56
+ blocking: true,
57
+ };
58
+ }
59
+ return {
60
+ mode: 'advisory',
61
+ source: 'default',
62
+ blocking: false,
63
+ };
64
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.57",
3
+ "version": "6.3.58",
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": {
@@ -95,9 +95,8 @@
95
95
  "validation:phase8:mark-followup-state": "bash scripts/mark-phase8-support-followup-state.sh",
96
96
  "validation:phase8:mark-followup-posted-now": "bash scripts/mark-phase8-followup-posted-now.sh",
97
97
  "validation:phase8:mark-followup-replied-now": "bash scripts/mark-phase8-followup-replied-now.sh",
98
- "validation:phase8:ready-handoff": "bash scripts/build-phase8-ready-handoff-summary.sh",
99
98
  "validation:phase8:close-ready": "bash scripts/run-phase8-close-ready.sh",
100
- "validation:progress-single-active": "bash scripts/check-refactor-progress-single-active.sh",
99
+ "validation:progress-single-active": "bash scripts/check-tracking-single-active.sh PUMUKI-RESET-MASTER-PLAN.md",
101
100
  "validation:self-worktree-hygiene": "node --import tsx scripts/check-self-worktree-hygiene.ts",
102
101
  "validation:tracking-single-active": "bash scripts/check-tracking-single-active.sh",
103
102
  "validation:backlog-reconcile": "node --import tsx scripts/reconcile-consumer-backlog-issues.ts",
@@ -107,7 +106,6 @@
107
106
  "validation:backlog-watch:gate": "node --import tsx scripts/watch-consumer-backlog-fleet-tick.ts --json",
108
107
  "validation:phase5-escalation:ready-to-submit": "bash scripts/check-phase5-escalation-ready-to-submit.sh",
109
108
  "validation:phase5-escalation:prepare": "bash scripts/prepare-phase5-escalation-submission.sh",
110
- "validation:phase5-escalation:close-submission": "bash scripts/close-phase5-escalation-submission.sh",
111
109
  "validation:phase5-escalation:mark-submitted": "bash scripts/mark-phase5-escalation-submitted.sh",
112
110
  "validation:phase5-escalation:payload": "bash scripts/build-phase5-support-portal-payload.sh",
113
111
  "validation:architecture-guardrails": "npx --yes tsx@4.21.0 --test scripts/__tests__/architecture-file-size-guardrails.test.ts",
@@ -203,6 +201,7 @@
203
201
  "integrations/evidence/*.ts",
204
202
  "integrations/gate/*.ts",
205
203
  "integrations/git/*.ts",
204
+ "integrations/policy/*.ts",
206
205
  "integrations/tdd/*.ts",
207
206
  "integrations/lifecycle/*.ts",
208
207
  "integrations/lifecycle/*.json",