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 +1 -1
- package/docs/operations/RELEASE_NOTES.md +19 -0
- package/integrations/evidence/schema.ts +1 -1
- package/integrations/gate/evaluateAiGate.ts +85 -19
- package/integrations/git/stageRunners.ts +98 -31
- package/integrations/lifecycle/cli.ts +73 -5
- package/integrations/lifecycle/doctor.ts +12 -3
- package/integrations/lifecycle/preWriteAutomation.ts +1 -1
- package/integrations/lifecycle/watch.ts +155 -9
- package/integrations/policy/gitAtomicityEnforcement.ts +57 -0
- package/integrations/policy/preWriteEnforcement.ts +41 -0
- package/integrations/policy/skillsEnforcement.ts +64 -0
- package/package.json +3 -4
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1314
|
+
suppressSkillsContractViolation
|
|
1315
|
+
|| skillsContract.status !== 'FAIL'
|
|
1251
1316
|
|| (params.stage === 'PRE_WRITE' && requiredSkillsPlatforms.length === 0)
|
|
1252
|
-
|
|
1317
|
+
? []
|
|
1253
1318
|
: [
|
|
1254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
426
|
+
severity: 'warning',
|
|
427
427
|
message: '.ai_evidence.json is missing.',
|
|
428
|
-
remediation: 'Regenerate evidence with a full audit before
|
|
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' ||
|
|
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
|
-
? [...
|
|
437
|
-
:
|
|
555
|
+
? [...allFindingsWithAtomicity, manifestMutationFinding]
|
|
556
|
+
: allFindingsWithAtomicity;
|
|
438
557
|
const matchedFindings = manifestMutationFinding
|
|
439
|
-
? [...
|
|
440
|
-
:
|
|
558
|
+
? [...matchedFindingsWithAtomicity, manifestMutationFinding]
|
|
559
|
+
: matchedFindingsWithAtomicity;
|
|
441
560
|
const rawGateOutcome =
|
|
442
561
|
evidence?.snapshot.outcome ??
|
|
443
562
|
(gateExitCode !== 0 ? 'BLOCK' : 'NO_EVIDENCE');
|
|
444
|
-
const
|
|
445
|
-
|
|
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
|
|
470
|
-
|
|
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.
|
|
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-
|
|
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",
|