pumuki 6.3.57 → 6.3.59
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 +35 -0
- package/integrations/evidence/schema.ts +1 -1
- package/integrations/gate/evaluateAiGate.ts +198 -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.59
|
|
@@ -6,6 +6,41 @@ 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.59)
|
|
10
|
+
|
|
11
|
+
- PRE_WRITE tooling-only no-op:
|
|
12
|
+
- consumer deltas that only touch package/runtime alignment files no longer materialize a fake multi-platform skills contract,
|
|
13
|
+
- the result is now `skills_contract=NOT_APPLICABLE` even with `pending_changes > 0` when no code platform is active in the worktree delta.
|
|
14
|
+
- Policy packaging alignment:
|
|
15
|
+
- the published tarball now includes the `integrations/policy/*.ts` helpers required by `evaluateAiGate`, `stageRunners`, `watch` and `cli`.
|
|
16
|
+
- Operational impact:
|
|
17
|
+
- `SAAS · PUMUKI-021` can now be revalidated against the published package on a real tooling-only adoption slice.
|
|
18
|
+
- Validation evidence:
|
|
19
|
+
- `npm run -s typecheck` (`PASS`)
|
|
20
|
+
- `node --import tsx --test integrations/gate/__tests__/evaluateAiGate.test.ts` (`41 pass / 0 fail`)
|
|
21
|
+
- `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`)
|
|
22
|
+
- `npm run -s validation:package-manifest` (`PASS`)
|
|
23
|
+
- `npm pack --json --dry-run` (`PASS`)
|
|
24
|
+
|
|
25
|
+
### 2026-03-14 (v6.3.58)
|
|
26
|
+
|
|
27
|
+
- PRE_WRITE no-op stability for consumer slices:
|
|
28
|
+
- clean slices with `pending_changes=0` no longer fail by undetected multi-platform skills contract noise,
|
|
29
|
+
- the resulting contract is now `NOT_APPLICABLE` when there is no materialized active platform scope.
|
|
30
|
+
- PRE_WRITE freshness autocure:
|
|
31
|
+
- stale `ai_evidence` and stale MCP receipts are refreshed even if the SDD session itself is invalid,
|
|
32
|
+
- operational noise is removed before the final decision is reported.
|
|
33
|
+
- Deep doctor de-escalation:
|
|
34
|
+
- missing/stale evidence is now treated as operational drift (`warning`) instead of a hard diagnostic block.
|
|
35
|
+
- Operational impact:
|
|
36
|
+
- `SAAS · PUMUKI-021` is left with only release/adoption work; the core no longer has a technical residual for that bug.
|
|
37
|
+
- Validation evidence:
|
|
38
|
+
- `npm run -s typecheck` (`PASS`)
|
|
39
|
+
- `node --import tsx --test integrations/gate/__tests__/evaluateAiGate.test.ts` (`40 pass / 0 fail`)
|
|
40
|
+
- `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`)
|
|
41
|
+
- `npm run -s validation:package-manifest` (`PASS`)
|
|
42
|
+
- `npm pack --json --dry-run` (`PASS`)
|
|
43
|
+
|
|
9
44
|
### 2026-03-11 (v6.3.57)
|
|
10
45
|
|
|
11
46
|
- 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: {
|
|
@@ -3,6 +3,7 @@ import { readEvidenceResult } from '../evidence/readEvidence';
|
|
|
3
3
|
import { captureRepoState } from '../evidence/repoState';
|
|
4
4
|
import type { RepoState } from '../evidence/schema';
|
|
5
5
|
import { resolvePolicyForStage } from './stagePolicies';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
6
7
|
import { existsSync, realpathSync } from 'node:fs';
|
|
7
8
|
import { resolve } from 'node:path';
|
|
8
9
|
import type { SkillsLockV1, SkillsStage } from '../config/skillsLock';
|
|
@@ -10,6 +11,10 @@ import {
|
|
|
10
11
|
loadEffectiveSkillsLock,
|
|
11
12
|
loadRequiredSkillsLock,
|
|
12
13
|
} from '../config/skillsEffectiveLock';
|
|
14
|
+
import {
|
|
15
|
+
resolveSkillsEnforcement,
|
|
16
|
+
type SkillsEnforcementResolution,
|
|
17
|
+
} from '../policy/skillsEnforcement';
|
|
13
18
|
import {
|
|
14
19
|
readMcpAiGateReceipt,
|
|
15
20
|
resolveMcpAiGateReceiptPath,
|
|
@@ -169,6 +174,13 @@ const MCP_RECEIPT_STAGE_ORDER: Readonly<Record<AiGateStage, number>> = {
|
|
|
169
174
|
PRE_PUSH: 2,
|
|
170
175
|
CI: 3,
|
|
171
176
|
};
|
|
177
|
+
const SKILLS_CONTRACT_SUPPRESSED_EVIDENCE_CODES = new Set([
|
|
178
|
+
'EVIDENCE_MISSING',
|
|
179
|
+
'EVIDENCE_INVALID',
|
|
180
|
+
'EVIDENCE_CHAIN_INVALID',
|
|
181
|
+
'EVIDENCE_TIMESTAMP_INVALID',
|
|
182
|
+
'EVIDENCE_STALE',
|
|
183
|
+
]);
|
|
172
184
|
|
|
173
185
|
const toErrorViolation = (code: string, message: string): AiGateViolation => ({
|
|
174
186
|
code,
|
|
@@ -182,6 +194,16 @@ const toWarnViolation = (code: string, message: string): AiGateViolation => ({
|
|
|
182
194
|
message,
|
|
183
195
|
});
|
|
184
196
|
|
|
197
|
+
const toSkillsViolation = (
|
|
198
|
+
resolution: SkillsEnforcementResolution,
|
|
199
|
+
code: string,
|
|
200
|
+
message: string
|
|
201
|
+
): AiGateViolation => (
|
|
202
|
+
resolution.blocking
|
|
203
|
+
? toErrorViolation(code, message)
|
|
204
|
+
: toWarnViolation(code, message)
|
|
205
|
+
);
|
|
206
|
+
|
|
185
207
|
const normalizeRepoStateLifecycleVersions = (repoState: RepoState): RepoState => {
|
|
186
208
|
const packageVersion = repoState.lifecycle.package_version;
|
|
187
209
|
const lifecycleVersion = repoState.lifecycle.lifecycle_version;
|
|
@@ -332,6 +354,109 @@ const toRepoTreeDetectedPlatforms = (params: {
|
|
|
332
354
|
});
|
|
333
355
|
};
|
|
334
356
|
|
|
357
|
+
const normalizeChangedPath = (value: string): string =>
|
|
358
|
+
value.replace(/\\/g, '/').replace(/^"+|"+$/g, '').trim();
|
|
359
|
+
|
|
360
|
+
const parseChangedPath = (line: string): string | null => {
|
|
361
|
+
if (line.length < 4) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const raw = line.slice(3).trim();
|
|
365
|
+
if (raw.length === 0) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
if (raw.includes(' -> ')) {
|
|
369
|
+
const renamed = raw.split(' -> ').pop();
|
|
370
|
+
if (!renamed) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const normalizedRenamed = normalizeChangedPath(renamed);
|
|
374
|
+
return normalizedRenamed.length > 0 ? normalizedRenamed : null;
|
|
375
|
+
}
|
|
376
|
+
const normalized = normalizeChangedPath(raw);
|
|
377
|
+
return normalized.length > 0 ? normalized : null;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const collectWorktreeChangedPaths = (repoRoot: string): ReadonlyArray<string> => {
|
|
381
|
+
try {
|
|
382
|
+
const output = execFileSync(
|
|
383
|
+
'git',
|
|
384
|
+
['status', '--short', '--untracked-files=all'],
|
|
385
|
+
{
|
|
386
|
+
cwd: repoRoot,
|
|
387
|
+
encoding: 'utf8',
|
|
388
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
const files = output
|
|
392
|
+
.split('\n')
|
|
393
|
+
.map((line) => parseChangedPath(line))
|
|
394
|
+
.filter((line): line is string => typeof line === 'string' && line.length > 0);
|
|
395
|
+
return [...new Set(files)];
|
|
396
|
+
} catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boolean => {
|
|
402
|
+
const normalized = normalizeChangedPath(filePath).toLowerCase();
|
|
403
|
+
if (platform === 'ios') {
|
|
404
|
+
return normalized.endsWith('.swift')
|
|
405
|
+
|| normalized.startsWith('apps/ios/')
|
|
406
|
+
|| normalized.startsWith('ios/');
|
|
407
|
+
}
|
|
408
|
+
if (platform === 'android') {
|
|
409
|
+
return normalized.endsWith('.kt')
|
|
410
|
+
|| normalized.endsWith('.kts')
|
|
411
|
+
|| normalized.startsWith('apps/android/')
|
|
412
|
+
|| normalized.startsWith('android/');
|
|
413
|
+
}
|
|
414
|
+
if (platform === 'backend') {
|
|
415
|
+
const isTypeScriptOrJavaScript =
|
|
416
|
+
normalized.endsWith('.ts')
|
|
417
|
+
|| normalized.endsWith('.js')
|
|
418
|
+
|| normalized.endsWith('.mts')
|
|
419
|
+
|| normalized.endsWith('.cts')
|
|
420
|
+
|| normalized.endsWith('.mjs')
|
|
421
|
+
|| normalized.endsWith('.cjs');
|
|
422
|
+
if (!isTypeScriptOrJavaScript) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
return normalized.startsWith('apps/backend/')
|
|
426
|
+
|| /(^|\/)(backend|server|api)(\/|$)/.test(normalized);
|
|
427
|
+
}
|
|
428
|
+
const isReactExtension = normalized.endsWith('.tsx') || normalized.endsWith('.jsx');
|
|
429
|
+
if (isReactExtension) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
const isTypeScriptOrJavaScript =
|
|
433
|
+
normalized.endsWith('.ts')
|
|
434
|
+
|| normalized.endsWith('.js')
|
|
435
|
+
|| normalized.endsWith('.mts')
|
|
436
|
+
|| normalized.endsWith('.cts')
|
|
437
|
+
|| normalized.endsWith('.mjs')
|
|
438
|
+
|| normalized.endsWith('.cjs');
|
|
439
|
+
if (!isTypeScriptOrJavaScript) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
return normalized.startsWith('apps/frontend/')
|
|
443
|
+
|| normalized.startsWith('apps/web/')
|
|
444
|
+
|| /(^|\/)(frontend|web|client)(\/|$)/.test(normalized);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const hasWorktreeCodePlatforms = (params: {
|
|
448
|
+
repoRoot: string;
|
|
449
|
+
requiredPlatforms: ReadonlyArray<PreWriteSkillsPlatform>;
|
|
450
|
+
}): boolean => {
|
|
451
|
+
const changedPaths = collectWorktreeChangedPaths(params.repoRoot);
|
|
452
|
+
if (changedPaths.length === 0) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
return changedPaths.some((filePath) =>
|
|
456
|
+
params.requiredPlatforms.some((platform) => isPlatformPath(platform, filePath))
|
|
457
|
+
);
|
|
458
|
+
};
|
|
459
|
+
|
|
335
460
|
const toLockRequiredPlatforms = (
|
|
336
461
|
requiredLock: SkillsLockV1 | undefined
|
|
337
462
|
): ReadonlyArray<PreWriteSkillsPlatform> => {
|
|
@@ -426,6 +551,7 @@ const collectActiveRuleIdsCoverageViolations = (params: {
|
|
|
426
551
|
const collectPreWritePlatformSkillsViolations = (params: {
|
|
427
552
|
evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
|
|
428
553
|
coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
|
|
554
|
+
skillsEnforcement: SkillsEnforcementResolution;
|
|
429
555
|
}): AiGateViolation[] => {
|
|
430
556
|
const detectedPlatforms = toPreWriteDetectedSkillsPlatforms({
|
|
431
557
|
platforms: params.evidence.platforms,
|
|
@@ -460,7 +586,8 @@ const collectPreWritePlatformSkillsViolations = (params: {
|
|
|
460
586
|
|
|
461
587
|
if (missingScopeCoverage.length > 0) {
|
|
462
588
|
violations.push(
|
|
463
|
-
|
|
589
|
+
toSkillsViolation(
|
|
590
|
+
params.skillsEnforcement,
|
|
464
591
|
'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
|
|
465
592
|
`Detected platforms missing skill-rule coverage in PRE_WRITE: ${missingScopeCoverage.join(' | ')}.`
|
|
466
593
|
)
|
|
@@ -485,7 +612,8 @@ const collectPreWritePlatformSkillsViolations = (params: {
|
|
|
485
612
|
|
|
486
613
|
if (missingBundlesByPlatform.length > 0) {
|
|
487
614
|
violations.push(
|
|
488
|
-
|
|
615
|
+
toSkillsViolation(
|
|
616
|
+
params.skillsEnforcement,
|
|
489
617
|
'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
|
|
490
618
|
`Detected platforms missing required skill bundles in PRE_WRITE: ${missingBundlesByPlatform.join(' | ')}.`
|
|
491
619
|
)
|
|
@@ -513,7 +641,8 @@ const collectPreWritePlatformSkillsViolations = (params: {
|
|
|
513
641
|
|
|
514
642
|
if (missingCriticalRulesByPlatform.length > 0) {
|
|
515
643
|
violations.push(
|
|
516
|
-
|
|
644
|
+
toSkillsViolation(
|
|
645
|
+
params.skillsEnforcement,
|
|
517
646
|
'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
|
|
518
647
|
`Detected platforms missing critical skill-rule enforcement in PRE_WRITE: ${missingCriticalRulesByPlatform.join(' | ')}.`
|
|
519
648
|
)
|
|
@@ -526,6 +655,7 @@ const collectPreWritePlatformSkillsViolations = (params: {
|
|
|
526
655
|
const collectPreWriteCrossPlatformCriticalViolations = (params: {
|
|
527
656
|
evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
|
|
528
657
|
coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
|
|
658
|
+
skillsEnforcement: SkillsEnforcementResolution;
|
|
529
659
|
}): AiGateViolation[] => {
|
|
530
660
|
const detectedPlatforms = toPreWriteDetectedSkillsPlatforms({
|
|
531
661
|
platforms: params.evidence.platforms,
|
|
@@ -561,7 +691,8 @@ const collectPreWriteCrossPlatformCriticalViolations = (params: {
|
|
|
561
691
|
}
|
|
562
692
|
|
|
563
693
|
return [
|
|
564
|
-
|
|
694
|
+
toSkillsViolation(
|
|
695
|
+
params.skillsEnforcement,
|
|
565
696
|
'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
|
|
566
697
|
`Cross-platform critical enforcement incomplete in PRE_WRITE: ${missingCriticalCoverage.join(' | ')}.`
|
|
567
698
|
),
|
|
@@ -571,8 +702,10 @@ const collectPreWriteCrossPlatformCriticalViolations = (params: {
|
|
|
571
702
|
const toSkillsContractAssessment = (params: {
|
|
572
703
|
stage: AiGateStage;
|
|
573
704
|
repoRoot: string;
|
|
705
|
+
repoState: RepoState;
|
|
574
706
|
evidenceResult: EvidenceReadResult;
|
|
575
707
|
requiredLock?: SkillsLockV1;
|
|
708
|
+
skillsEnforcement: SkillsEnforcementResolution;
|
|
576
709
|
}): AiGateSkillsContractAssessment => {
|
|
577
710
|
const requiredPlatforms = toLockRequiredPlatforms(params.requiredLock);
|
|
578
711
|
|
|
@@ -602,7 +735,8 @@ const toSkillsContractAssessment = (params: {
|
|
|
602
735
|
violations:
|
|
603
736
|
requiredPlatforms.length > 0
|
|
604
737
|
? [
|
|
605
|
-
|
|
738
|
+
toSkillsViolation(
|
|
739
|
+
params.skillsEnforcement,
|
|
606
740
|
'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
|
|
607
741
|
`Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
|
|
608
742
|
),
|
|
@@ -615,7 +749,7 @@ const toSkillsContractAssessment = (params: {
|
|
|
615
749
|
const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.evidenceResult.evidence.platforms);
|
|
616
750
|
const inferredPlatforms = toCoverageInferredPlatforms(coverage);
|
|
617
751
|
const repoTreeDetectedPlatforms =
|
|
618
|
-
requiredPlatforms.length > 0
|
|
752
|
+
params.stage !== 'PRE_WRITE' && requiredPlatforms.length > 0
|
|
619
753
|
? toRepoTreeDetectedPlatforms({
|
|
620
754
|
repoRoot: params.repoRoot,
|
|
621
755
|
platforms: requiredPlatforms,
|
|
@@ -634,12 +768,35 @@ const toSkillsContractAssessment = (params: {
|
|
|
634
768
|
: inferredPlatforms.length > 0
|
|
635
769
|
? inferredPlatforms
|
|
636
770
|
: repoTreeDetectedPlatforms;
|
|
771
|
+
const pendingChanges = resolvePendingChanges(params.repoState);
|
|
772
|
+
const detectedPlatformSet = new Set(detectedPlatforms);
|
|
637
773
|
const assessmentPlatforms =
|
|
638
774
|
requiredPlatforms.length > 0
|
|
639
|
-
?
|
|
775
|
+
? params.stage === 'PRE_WRITE' && detectedPlatforms.length > 0
|
|
776
|
+
? requiredPlatforms.filter((platform) => detectedPlatformSet.has(platform))
|
|
777
|
+
: requiredPlatforms
|
|
640
778
|
: detectedPlatforms;
|
|
641
779
|
|
|
642
780
|
if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) {
|
|
781
|
+
if (
|
|
782
|
+
params.stage === 'PRE_WRITE'
|
|
783
|
+
&& (
|
|
784
|
+
pendingChanges === 0
|
|
785
|
+
|| !hasWorktreeCodePlatforms({
|
|
786
|
+
repoRoot: params.repoRoot,
|
|
787
|
+
requiredPlatforms,
|
|
788
|
+
})
|
|
789
|
+
)
|
|
790
|
+
) {
|
|
791
|
+
return {
|
|
792
|
+
stage: params.stage,
|
|
793
|
+
enforced: false,
|
|
794
|
+
status: 'NOT_APPLICABLE',
|
|
795
|
+
detected_platforms: [],
|
|
796
|
+
requirements: [],
|
|
797
|
+
violations: [],
|
|
798
|
+
};
|
|
799
|
+
}
|
|
643
800
|
const requirements: AiGateSkillsContractPlatformRequirement[] = requiredPlatforms.map((platform) => ({
|
|
644
801
|
platform,
|
|
645
802
|
required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
|
|
@@ -665,7 +822,8 @@ const toSkillsContractAssessment = (params: {
|
|
|
665
822
|
detected_platforms: [],
|
|
666
823
|
requirements,
|
|
667
824
|
violations: [
|
|
668
|
-
|
|
825
|
+
toSkillsViolation(
|
|
826
|
+
params.skillsEnforcement,
|
|
669
827
|
'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
|
|
670
828
|
`Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
|
|
671
829
|
),
|
|
@@ -694,7 +852,8 @@ const toSkillsContractAssessment = (params: {
|
|
|
694
852
|
const violations: AiGateViolation[] = [];
|
|
695
853
|
if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) {
|
|
696
854
|
violations.push(
|
|
697
|
-
|
|
855
|
+
toSkillsViolation(
|
|
856
|
+
params.skillsEnforcement,
|
|
698
857
|
'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
|
|
699
858
|
`Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
|
|
700
859
|
)
|
|
@@ -759,7 +918,8 @@ const toSkillsContractAssessment = (params: {
|
|
|
759
918
|
missingParts.push('evaluated_prefix');
|
|
760
919
|
}
|
|
761
920
|
violations.push(
|
|
762
|
-
|
|
921
|
+
toSkillsViolation(
|
|
922
|
+
params.skillsEnforcement,
|
|
763
923
|
'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
|
|
764
924
|
`Skills contract scope coverage missing for ${platform}: ${missingParts.join(', ')} (${requiredRulePrefix}).`
|
|
765
925
|
)
|
|
@@ -767,7 +927,8 @@ const toSkillsContractAssessment = (params: {
|
|
|
767
927
|
}
|
|
768
928
|
if (missingBundles.length > 0) {
|
|
769
929
|
violations.push(
|
|
770
|
-
|
|
930
|
+
toSkillsViolation(
|
|
931
|
+
params.skillsEnforcement,
|
|
771
932
|
'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
|
|
772
933
|
`Skills contract missing bundles for ${platform}: [${missingBundles.join(', ')}].`
|
|
773
934
|
)
|
|
@@ -775,7 +936,8 @@ const toSkillsContractAssessment = (params: {
|
|
|
775
936
|
}
|
|
776
937
|
if (missingCriticalRuleIds.length > 0) {
|
|
777
938
|
violations.push(
|
|
778
|
-
|
|
939
|
+
toSkillsViolation(
|
|
940
|
+
params.skillsEnforcement,
|
|
779
941
|
'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
|
|
780
942
|
`Skills contract missing critical rule coverage for ${platform}: [${missingCriticalRuleIds.join(', ')}].`
|
|
781
943
|
)
|
|
@@ -783,7 +945,8 @@ const toSkillsContractAssessment = (params: {
|
|
|
783
945
|
}
|
|
784
946
|
if (!transversalCriticalCovered && requiredAnyTransversalCriticalRuleIds.length > 0) {
|
|
785
947
|
violations.push(
|
|
786
|
-
|
|
948
|
+
toSkillsViolation(
|
|
949
|
+
params.skillsEnforcement,
|
|
787
950
|
'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
|
|
788
951
|
`Skills contract missing transversal critical coverage for ${platform}: required_any=[${requiredAnyTransversalCriticalRuleIds.join(', ')}].`
|
|
789
952
|
)
|
|
@@ -867,6 +1030,7 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
867
1030
|
)
|
|
868
1031
|
);
|
|
869
1032
|
} else {
|
|
1033
|
+
const skillsEnforcement = resolveSkillsEnforcement();
|
|
870
1034
|
if (coverage.stage !== params.evidence.snapshot.stage) {
|
|
871
1035
|
violations.push(
|
|
872
1036
|
toErrorViolation(
|
|
@@ -908,12 +1072,14 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
908
1072
|
...collectPreWritePlatformSkillsViolations({
|
|
909
1073
|
evidence: params.evidence,
|
|
910
1074
|
coverage,
|
|
1075
|
+
skillsEnforcement,
|
|
911
1076
|
})
|
|
912
1077
|
);
|
|
913
1078
|
violations.push(
|
|
914
1079
|
...collectPreWriteCrossPlatformCriticalViolations({
|
|
915
1080
|
evidence: params.evidence,
|
|
916
1081
|
coverage,
|
|
1082
|
+
skillsEnforcement,
|
|
917
1083
|
})
|
|
918
1084
|
);
|
|
919
1085
|
}
|
|
@@ -928,9 +1094,7 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
928
1094
|
}
|
|
929
1095
|
|
|
930
1096
|
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);
|
|
1097
|
+
const pendingChanges = resolvePendingChanges(params.repoState) ?? 0;
|
|
934
1098
|
if (pendingChanges >= params.preWriteWorktreeHygiene.blockThreshold) {
|
|
935
1099
|
violations.push(
|
|
936
1100
|
toErrorViolation(
|
|
@@ -1057,6 +1221,13 @@ const collectGitflowViolations = (
|
|
|
1057
1221
|
return violations;
|
|
1058
1222
|
};
|
|
1059
1223
|
|
|
1224
|
+
const resolvePendingChanges = (repoState: RepoState): number | null => {
|
|
1225
|
+
if (!repoState.git.available) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
return repoState.git.pending_changes ?? (repoState.git.staged + repoState.git.unstaged);
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1060
1231
|
const toPolicyStage = (stage: AiGateStage): SkillsStage => {
|
|
1061
1232
|
if (stage === 'PRE_WRITE') {
|
|
1062
1233
|
return 'PRE_COMMIT';
|
|
@@ -1222,6 +1393,7 @@ export const evaluateAiGate = (
|
|
|
1222
1393
|
policyStage,
|
|
1223
1394
|
params.repoRoot
|
|
1224
1395
|
);
|
|
1396
|
+
const skillsEnforcement = resolveSkillsEnforcement();
|
|
1225
1397
|
const evidenceAssessment = collectEvidenceViolations(
|
|
1226
1398
|
evidenceResult,
|
|
1227
1399
|
params.repoRoot,
|
|
@@ -1243,15 +1415,22 @@ export const evaluateAiGate = (
|
|
|
1243
1415
|
const skillsContract = toSkillsContractAssessment({
|
|
1244
1416
|
stage: params.stage,
|
|
1245
1417
|
repoRoot: params.repoRoot,
|
|
1418
|
+
repoState,
|
|
1246
1419
|
evidenceResult,
|
|
1247
1420
|
requiredLock: requiredSkillsLock,
|
|
1421
|
+
skillsEnforcement,
|
|
1248
1422
|
});
|
|
1423
|
+
const suppressSkillsContractViolation = evidenceAssessment.violations.some((violation) =>
|
|
1424
|
+
SKILLS_CONTRACT_SUPPRESSED_EVIDENCE_CODES.has(violation.code)
|
|
1425
|
+
);
|
|
1249
1426
|
const stageSkillsContractViolations =
|
|
1250
|
-
|
|
1427
|
+
suppressSkillsContractViolation
|
|
1428
|
+
|| skillsContract.status !== 'FAIL'
|
|
1251
1429
|
|| (params.stage === 'PRE_WRITE' && requiredSkillsPlatforms.length === 0)
|
|
1252
|
-
|
|
1430
|
+
? []
|
|
1253
1431
|
: [
|
|
1254
|
-
|
|
1432
|
+
toSkillsViolation(
|
|
1433
|
+
skillsEnforcement,
|
|
1255
1434
|
'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE',
|
|
1256
1435
|
`Skills contract incomplete for ${params.stage}: ${skillsContract.violations.map((violation) => violation.code).join(', ')}.`
|
|
1257
1436
|
),
|
|
@@ -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.59",
|
|
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",
|