pumuki 6.3.39 → 6.3.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -12
- package/VERSION +1 -1
- package/core/gate/evaluateRules.test.ts +40 -0
- package/core/gate/evaluateRules.ts +7 -1
- package/core/rules/Consequence.ts +1 -0
- package/docs/CONFIGURATION.md +50 -0
- package/docs/INSTALLATION.md +38 -11
- package/docs/MCP_SERVERS.md +1 -1
- package/docs/README.md +1 -0
- package/docs/RELEASE_NOTES.md +44 -0
- package/docs/USAGE.md +191 -9
- package/docs/registro-maestro-de-seguimiento.md +2 -2
- package/docs/seguimiento-activo-pumuki-saas-supermercados.md +1592 -1
- package/docs/validation/README.md +2 -1
- package/docs/validation/ast-intelligence-roadmap.md +96 -0
- package/integrations/config/skillsCustomRules.ts +14 -0
- package/integrations/config/skillsDetectorRegistry.ts +11 -1
- package/integrations/config/skillsLock.ts +30 -0
- package/integrations/config/skillsMarkdownRules.ts +14 -3
- package/integrations/config/skillsRuleSet.ts +25 -3
- package/integrations/evidence/readEvidence.test.ts +3 -2
- package/integrations/evidence/readEvidence.ts +14 -4
- package/integrations/evidence/repoState.ts +10 -2
- package/integrations/evidence/schema.test.ts +3 -2
- package/integrations/evidence/schema.ts +3 -0
- package/integrations/evidence/writeEvidence.test.ts +3 -2
- package/integrations/gate/evaluateAiGate.ts +511 -2
- package/integrations/git/GitService.ts +5 -1
- package/integrations/git/astIntelligenceDualValidation.ts +275 -0
- package/integrations/git/gitAtomicity.ts +42 -9
- package/integrations/git/resolveGitRefs.ts +37 -0
- package/integrations/git/runPlatformGate.ts +228 -1
- package/integrations/git/runPlatformGateEvaluation.ts +4 -0
- package/integrations/git/stageRunners.ts +116 -2
- package/integrations/lifecycle/cli.ts +759 -22
- package/integrations/lifecycle/doctor.ts +62 -0
- package/integrations/lifecycle/index.ts +1 -0
- package/integrations/lifecycle/packageInfo.ts +25 -3
- package/integrations/lifecycle/policyReconcile.ts +304 -0
- package/integrations/lifecycle/preWriteAutomation.ts +42 -2
- package/integrations/lifecycle/watch.ts +365 -0
- package/integrations/mcp/aiGateCheck.ts +59 -2
- package/integrations/mcp/autoExecuteAiStart.ts +25 -1
- package/integrations/mcp/preFlightCheck.ts +13 -0
- package/integrations/sdd/evidenceScaffold.ts +223 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/stateSync.ts +400 -0
- package/integrations/sdd/syncDocs.ts +97 -2
- package/package.json +4 -1
- package/scripts/backlog-action-reasons-lib.ts +38 -0
- package/scripts/backlog-id-issue-map-lib.ts +69 -0
- package/scripts/backlog-json-contract-lib.ts +3 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +6 -0
- package/scripts/package-install-smoke-command-resolution-lib.ts +64 -0
- package/scripts/package-install-smoke-consumer-npm-lib.ts +43 -0
- package/scripts/package-install-smoke-consumer-repo-setup-lib.ts +2 -0
- package/scripts/package-install-smoke-execution-steps-lib.ts +27 -9
- package/scripts/package-install-smoke-lifecycle-lib.ts +15 -4
- package/scripts/package-install-smoke-workspace-factory-lib.ts +4 -1
- package/scripts/reconcile-consumer-backlog-issues-lib.ts +651 -0
- package/scripts/reconcile-consumer-backlog-issues.ts +348 -0
- package/scripts/watch-consumer-backlog-lib.ts +465 -0
- package/scripts/watch-consumer-backlog.ts +326 -0
|
@@ -169,6 +169,34 @@ const readNestedString = (
|
|
|
169
169
|
return typeof cursor === 'string' && cursor.trim().length > 0 ? cursor : undefined;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
+
const hasRobustPumukiCommandResolution = (command: string): boolean => {
|
|
173
|
+
const normalized = command.trim();
|
|
174
|
+
if (normalized.length === 0) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (/node_modules[\\/]\.bin[\\/]pumuki/i.test(normalized)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
/node\s+.*node_modules[\\/]pumuki[\\/]bin[\\/]pumuki(?:-[a-z0-9-]+)?\.js/i.test(
|
|
184
|
+
normalized
|
|
185
|
+
)
|
|
186
|
+
) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (/--package\s+pumuki(?:@[^\s]+)?/i.test(normalized)) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return false;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const mutatesPathForCommandExecution = (command: string): boolean =>
|
|
198
|
+
/\bPATH\s*=\s*[^\n]*\$PATH/i.test(command);
|
|
199
|
+
|
|
172
200
|
const buildDeepCheck = (
|
|
173
201
|
check: Omit<DoctorDeepCheck, 'status' | 'severity'> & {
|
|
174
202
|
status: DoctorDeepCheck['status'];
|
|
@@ -283,6 +311,40 @@ const evaluateAdapterWiringCheck = (repoRoot: string): DoctorDeepCheck => {
|
|
|
283
311
|
});
|
|
284
312
|
}
|
|
285
313
|
|
|
314
|
+
const weakResolutionPaths = requiredCommandPaths
|
|
315
|
+
.map((path) => {
|
|
316
|
+
const command = readNestedString(parsed, path);
|
|
317
|
+
return {
|
|
318
|
+
path: path.join('.'),
|
|
319
|
+
command,
|
|
320
|
+
};
|
|
321
|
+
})
|
|
322
|
+
.filter(
|
|
323
|
+
(entry) =>
|
|
324
|
+
entry.command &&
|
|
325
|
+
(!hasRobustPumukiCommandResolution(entry.command) ||
|
|
326
|
+
mutatesPathForCommandExecution(entry.command))
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
if (weakResolutionPaths.length > 0) {
|
|
330
|
+
const pathMutationCount = weakResolutionPaths.filter(
|
|
331
|
+
(entry) => entry.command && mutatesPathForCommandExecution(entry.command)
|
|
332
|
+
).length;
|
|
333
|
+
return buildDeepCheck({
|
|
334
|
+
id: 'adapter-wiring',
|
|
335
|
+
status: 'fail',
|
|
336
|
+
severity: 'warning',
|
|
337
|
+
message: `Adapter wiring commands are present but use fragile binary resolution or PATH mutation (${weakResolutionPaths.map((entry) => entry.path).join(', ')}).`,
|
|
338
|
+
remediation:
|
|
339
|
+
'Re-run "pumuki adapter install --agent=codex" to restore robust local/bin-or-package command resolution.',
|
|
340
|
+
metadata: {
|
|
341
|
+
path: adapterPath,
|
|
342
|
+
weak_resolution_count: weakResolutionPaths.length,
|
|
343
|
+
path_mutation_count: pathMutationCount,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
286
348
|
return buildDeepCheck({
|
|
287
349
|
id: 'adapter-wiring',
|
|
288
350
|
status: 'pass',
|
|
@@ -17,15 +17,37 @@ const readConsumerInstalledVersion = (repoRoot: string): string | null => {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export type PumukiVersionMetadata = {
|
|
21
|
+
resolvedVersion: string;
|
|
22
|
+
runtimeVersion: string;
|
|
23
|
+
consumerInstalledVersion: string | null;
|
|
24
|
+
source: 'consumer-node-modules' | 'runtime-package';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const resolvePumukiVersionMetadata = (params?: { repoRoot?: string }): PumukiVersionMetadata => {
|
|
28
|
+
const runtimeVersion = packageJson.version;
|
|
21
29
|
const repoRoot = params?.repoRoot;
|
|
22
30
|
if (typeof repoRoot === 'string' && repoRoot.trim().length > 0) {
|
|
23
31
|
const installedVersion = readConsumerInstalledVersion(repoRoot.trim());
|
|
24
32
|
if (installedVersion) {
|
|
25
|
-
return
|
|
33
|
+
return {
|
|
34
|
+
resolvedVersion: installedVersion,
|
|
35
|
+
runtimeVersion,
|
|
36
|
+
consumerInstalledVersion: installedVersion,
|
|
37
|
+
source: 'consumer-node-modules',
|
|
38
|
+
};
|
|
26
39
|
}
|
|
27
40
|
}
|
|
28
|
-
return
|
|
41
|
+
return {
|
|
42
|
+
resolvedVersion: runtimeVersion,
|
|
43
|
+
runtimeVersion,
|
|
44
|
+
consumerInstalledVersion: null,
|
|
45
|
+
source: 'runtime-package',
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const getCurrentPumukiVersion = (params?: { repoRoot?: string }): string => {
|
|
50
|
+
return resolvePumukiVersionMetadata(params).resolvedVersion;
|
|
29
51
|
};
|
|
30
52
|
|
|
31
53
|
export const getCurrentPumukiPackageName = (): string => packageJson.name;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
readLifecyclePolicyValidationSnapshot,
|
|
5
|
+
type LifecyclePolicyValidationSnapshot,
|
|
6
|
+
} from './policyValidationSnapshot';
|
|
7
|
+
|
|
8
|
+
type PolicyReconcileSeverity = 'INFO' | 'WARN' | 'ERROR';
|
|
9
|
+
|
|
10
|
+
type PolicyReconcileDriftCode =
|
|
11
|
+
| 'AGENTS_FILE_MISSING'
|
|
12
|
+
| 'AGENTS_REQUIRED_SKILLS_EMPTY'
|
|
13
|
+
| 'SKILLS_LOCK_MISSING'
|
|
14
|
+
| 'SKILLS_LOCK_INVALID'
|
|
15
|
+
| 'AGENTS_REQUIRED_SKILL_MISSING_IN_LOCK'
|
|
16
|
+
| 'POLICY_STAGE_INVALID'
|
|
17
|
+
| 'POLICY_STAGE_UNSIGNED_OR_COMPUTED'
|
|
18
|
+
| 'POLICY_STAGE_SIGNATURE_MISSING'
|
|
19
|
+
| 'POLICY_HASH_DIVERGENCE'
|
|
20
|
+
| 'POLICY_STAGE_NON_STRICT';
|
|
21
|
+
|
|
22
|
+
export type PolicyReconcileDrift = {
|
|
23
|
+
code: PolicyReconcileDriftCode;
|
|
24
|
+
severity: PolicyReconcileSeverity;
|
|
25
|
+
blocking: boolean;
|
|
26
|
+
message: string;
|
|
27
|
+
remediation: string | null;
|
|
28
|
+
context?: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PolicyReconcileReport = {
|
|
32
|
+
command: 'pumuki policy reconcile';
|
|
33
|
+
repoRoot: string;
|
|
34
|
+
generatedAt: string;
|
|
35
|
+
strictRequested: boolean;
|
|
36
|
+
summary: {
|
|
37
|
+
total: number;
|
|
38
|
+
blocking: number;
|
|
39
|
+
errors: number;
|
|
40
|
+
warnings: number;
|
|
41
|
+
infos: number;
|
|
42
|
+
status: 'PASS' | 'BLOCKED';
|
|
43
|
+
};
|
|
44
|
+
requiredSkills: string[];
|
|
45
|
+
stages: LifecyclePolicyValidationSnapshot['stages'];
|
|
46
|
+
drifts: PolicyReconcileDrift[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const REQUIRED_SKILL_IDS = [
|
|
50
|
+
'windsurf-rules-ios',
|
|
51
|
+
'swift-concurrency',
|
|
52
|
+
'swiftui-expert-skill',
|
|
53
|
+
'windsurf-rules-frontend',
|
|
54
|
+
'windsurf-rules-backend',
|
|
55
|
+
'windsurf-rules-android',
|
|
56
|
+
] as const;
|
|
57
|
+
|
|
58
|
+
type SkillsLockBundle = {
|
|
59
|
+
name?: string;
|
|
60
|
+
source?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const addDrift = (
|
|
64
|
+
drifts: PolicyReconcileDrift[],
|
|
65
|
+
drift: PolicyReconcileDrift
|
|
66
|
+
): void => {
|
|
67
|
+
drifts.push(drift);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const hasRequiredSkillInLock = (params: {
|
|
71
|
+
requiredSkill: string;
|
|
72
|
+
bundles: SkillsLockBundle[];
|
|
73
|
+
}): boolean => {
|
|
74
|
+
const required = params.requiredSkill.toLowerCase();
|
|
75
|
+
return params.bundles.some((bundle) => {
|
|
76
|
+
const name = typeof bundle.name === 'string' ? bundle.name.toLowerCase() : '';
|
|
77
|
+
const source = typeof bundle.source === 'string' ? bundle.source.toLowerCase() : '';
|
|
78
|
+
return name.includes(required) || source.includes(required);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const runPolicyReconcile = (params?: {
|
|
83
|
+
repoRoot?: string;
|
|
84
|
+
now?: () => Date;
|
|
85
|
+
strict?: boolean;
|
|
86
|
+
}): PolicyReconcileReport => {
|
|
87
|
+
const repoRoot = resolve(params?.repoRoot ?? process.cwd());
|
|
88
|
+
const now = params?.now ?? (() => new Date());
|
|
89
|
+
const strictRequested = params?.strict === true;
|
|
90
|
+
const drifts: PolicyReconcileDrift[] = [];
|
|
91
|
+
|
|
92
|
+
const agentsPath = resolve(repoRoot, 'AGENTS.md');
|
|
93
|
+
const agentsExists = existsSync(agentsPath);
|
|
94
|
+
let agentsContent = '';
|
|
95
|
+
if (!agentsExists) {
|
|
96
|
+
addDrift(drifts, {
|
|
97
|
+
code: 'AGENTS_FILE_MISSING',
|
|
98
|
+
severity: 'ERROR',
|
|
99
|
+
blocking: true,
|
|
100
|
+
message: 'AGENTS.md is missing. Hard rules cannot be reconciled.',
|
|
101
|
+
remediation: 'Restore AGENTS.md at repository root before continuing.',
|
|
102
|
+
context: {
|
|
103
|
+
path: agentsPath,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
agentsContent = readFileSync(agentsPath, 'utf8');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const requiredSkills = REQUIRED_SKILL_IDS.filter((skillId) =>
|
|
111
|
+
agentsContent.includes(skillId)
|
|
112
|
+
);
|
|
113
|
+
if (agentsExists && requiredSkills.length === 0) {
|
|
114
|
+
addDrift(drifts, {
|
|
115
|
+
code: 'AGENTS_REQUIRED_SKILLS_EMPTY',
|
|
116
|
+
severity: 'WARN',
|
|
117
|
+
blocking: false,
|
|
118
|
+
message:
|
|
119
|
+
'AGENTS.md does not declare any known required skills from hard contract list.',
|
|
120
|
+
remediation: 'Declare required skills explicitly in AGENTS.md to keep policy reconciliation deterministic.',
|
|
121
|
+
context: {
|
|
122
|
+
skills: [...REQUIRED_SKILL_IDS],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const skillsLockPath = resolve(repoRoot, 'skills.lock.json');
|
|
128
|
+
const skillsLockExists = existsSync(skillsLockPath);
|
|
129
|
+
let bundles: SkillsLockBundle[] = [];
|
|
130
|
+
if (!skillsLockExists) {
|
|
131
|
+
addDrift(drifts, {
|
|
132
|
+
code: 'SKILLS_LOCK_MISSING',
|
|
133
|
+
severity: 'ERROR',
|
|
134
|
+
blocking: true,
|
|
135
|
+
message: 'skills.lock.json is missing.',
|
|
136
|
+
remediation: 'Regenerate skills lock before running strict policy reconciliation.',
|
|
137
|
+
context: {
|
|
138
|
+
path: skillsLockPath,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(readFileSync(skillsLockPath, 'utf8')) as {
|
|
144
|
+
bundles?: SkillsLockBundle[];
|
|
145
|
+
};
|
|
146
|
+
bundles = Array.isArray(parsed.bundles) ? parsed.bundles : [];
|
|
147
|
+
if (!Array.isArray(parsed.bundles)) {
|
|
148
|
+
addDrift(drifts, {
|
|
149
|
+
code: 'SKILLS_LOCK_INVALID',
|
|
150
|
+
severity: 'ERROR',
|
|
151
|
+
blocking: true,
|
|
152
|
+
message: 'skills.lock.json does not contain a valid bundles array.',
|
|
153
|
+
remediation: 'Rebuild skills lock with deterministic compiler output.',
|
|
154
|
+
context: {
|
|
155
|
+
path: skillsLockPath,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
addDrift(drifts, {
|
|
161
|
+
code: 'SKILLS_LOCK_INVALID',
|
|
162
|
+
severity: 'ERROR',
|
|
163
|
+
blocking: true,
|
|
164
|
+
message: 'skills.lock.json is not valid JSON.',
|
|
165
|
+
remediation: 'Regenerate skills lock and retry policy reconciliation.',
|
|
166
|
+
context: {
|
|
167
|
+
path: skillsLockPath,
|
|
168
|
+
error: error instanceof Error ? error.message : String(error),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (bundles.length > 0) {
|
|
175
|
+
for (const requiredSkill of requiredSkills) {
|
|
176
|
+
if (!hasRequiredSkillInLock({ requiredSkill, bundles })) {
|
|
177
|
+
addDrift(drifts, {
|
|
178
|
+
code: 'AGENTS_REQUIRED_SKILL_MISSING_IN_LOCK',
|
|
179
|
+
severity: 'ERROR',
|
|
180
|
+
blocking: true,
|
|
181
|
+
message: `Required skill "${requiredSkill}" is not mapped in skills.lock.json bundles.`,
|
|
182
|
+
remediation: 'Recompile skills lock and ensure required skill source is included.',
|
|
183
|
+
context: {
|
|
184
|
+
required_skill: requiredSkill,
|
|
185
|
+
lock_path: skillsLockPath,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const snapshot = readLifecyclePolicyValidationSnapshot(repoRoot);
|
|
193
|
+
const stageHashes = new Set<string>();
|
|
194
|
+
for (const [stage, stageSnapshot] of Object.entries(snapshot.stages)) {
|
|
195
|
+
stageHashes.add(stageSnapshot.hash);
|
|
196
|
+
const validationStatus = stageSnapshot.validationStatus;
|
|
197
|
+
const validationCode = stageSnapshot.validationCode;
|
|
198
|
+
if (validationStatus !== 'valid' || validationCode !== 'POLICY_AS_CODE_VALID') {
|
|
199
|
+
addDrift(drifts, {
|
|
200
|
+
code: 'POLICY_STAGE_INVALID',
|
|
201
|
+
severity: 'ERROR',
|
|
202
|
+
blocking: true,
|
|
203
|
+
message: `Stage ${stage} policy is not valid (${validationCode ?? 'UNKNOWN'}).`,
|
|
204
|
+
remediation: 'Fix policy-as-code contract and rerun reconciliation.',
|
|
205
|
+
context: {
|
|
206
|
+
stage,
|
|
207
|
+
validation_status: validationStatus,
|
|
208
|
+
validation_code: validationCode,
|
|
209
|
+
source: stageSnapshot.source,
|
|
210
|
+
hash: stageSnapshot.hash,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (!stageSnapshot.strict) {
|
|
215
|
+
addDrift(drifts, {
|
|
216
|
+
code: 'POLICY_STAGE_NON_STRICT',
|
|
217
|
+
severity: strictRequested ? 'ERROR' : 'WARN',
|
|
218
|
+
blocking: strictRequested,
|
|
219
|
+
message: strictRequested
|
|
220
|
+
? `Stage ${stage} runs with strict=false under strict reconcile mode.`
|
|
221
|
+
: `Stage ${stage} runs with strict=false.`,
|
|
222
|
+
remediation: strictRequested
|
|
223
|
+
? 'Enable strict mode (PUMUKI_POLICY_STRICT=1) and rerun reconciliation.'
|
|
224
|
+
: 'Enable strict mode for enterprise fail-closed behavior.',
|
|
225
|
+
context: {
|
|
226
|
+
stage,
|
|
227
|
+
hash: stageSnapshot.hash,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
strictRequested &&
|
|
233
|
+
(typeof stageSnapshot.policySource !== 'string' ||
|
|
234
|
+
!stageSnapshot.policySource.startsWith('file:'))
|
|
235
|
+
) {
|
|
236
|
+
addDrift(drifts, {
|
|
237
|
+
code: 'POLICY_STAGE_UNSIGNED_OR_COMPUTED',
|
|
238
|
+
severity: 'ERROR',
|
|
239
|
+
blocking: true,
|
|
240
|
+
message: `Stage ${stage} is not backed by a file-based policy-as-code contract.`,
|
|
241
|
+
remediation:
|
|
242
|
+
'Provide .pumuki/policy-as-code.json with valid signatures and rerun strict reconcile.',
|
|
243
|
+
context: {
|
|
244
|
+
stage,
|
|
245
|
+
policy_source: stageSnapshot.policySource,
|
|
246
|
+
validation_code: stageSnapshot.validationCode,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (
|
|
251
|
+
strictRequested &&
|
|
252
|
+
(typeof stageSnapshot.signature !== 'string' || stageSnapshot.signature.trim().length === 0)
|
|
253
|
+
) {
|
|
254
|
+
addDrift(drifts, {
|
|
255
|
+
code: 'POLICY_STAGE_SIGNATURE_MISSING',
|
|
256
|
+
severity: 'ERROR',
|
|
257
|
+
blocking: true,
|
|
258
|
+
message: `Stage ${stage} policy signature is missing.`,
|
|
259
|
+
remediation:
|
|
260
|
+
'Regenerate signed policy-as-code contract and rerun strict reconcile.',
|
|
261
|
+
context: {
|
|
262
|
+
stage,
|
|
263
|
+
policy_source: stageSnapshot.policySource,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (stageHashes.size > 1) {
|
|
270
|
+
addDrift(drifts, {
|
|
271
|
+
code: 'POLICY_HASH_DIVERGENCE',
|
|
272
|
+
severity: 'WARN',
|
|
273
|
+
blocking: false,
|
|
274
|
+
message: 'Policy hashes differ across PRE_COMMIT/PRE_PUSH/CI stages.',
|
|
275
|
+
remediation: 'Align stage policy bundles to reduce contract drift.',
|
|
276
|
+
context: {
|
|
277
|
+
hashes: [...stageHashes],
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const blocking = drifts.filter((drift) => drift.blocking).length;
|
|
283
|
+
const errors = drifts.filter((drift) => drift.severity === 'ERROR').length;
|
|
284
|
+
const warnings = drifts.filter((drift) => drift.severity === 'WARN').length;
|
|
285
|
+
const infos = drifts.filter((drift) => drift.severity === 'INFO').length;
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
command: 'pumuki policy reconcile',
|
|
289
|
+
repoRoot,
|
|
290
|
+
generatedAt: now().toISOString(),
|
|
291
|
+
strictRequested,
|
|
292
|
+
summary: {
|
|
293
|
+
total: drifts.length,
|
|
294
|
+
blocking,
|
|
295
|
+
errors,
|
|
296
|
+
warnings,
|
|
297
|
+
infos,
|
|
298
|
+
status: blocking > 0 ? 'BLOCKED' : 'PASS',
|
|
299
|
+
},
|
|
300
|
+
requiredSkills,
|
|
301
|
+
stages: snapshot.stages,
|
|
302
|
+
drifts,
|
|
303
|
+
};
|
|
304
|
+
};
|
|
@@ -11,6 +11,7 @@ const PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES = new Set<string>([
|
|
|
11
11
|
'EVIDENCE_INVALID',
|
|
12
12
|
'EVIDENCE_CHAIN_INVALID',
|
|
13
13
|
'EVIDENCE_STALE',
|
|
14
|
+
'EVIDENCE_GATE_BLOCKED',
|
|
14
15
|
'EVIDENCE_REPO_ROOT_MISMATCH',
|
|
15
16
|
'EVIDENCE_BRANCH_MISMATCH',
|
|
16
17
|
'EVIDENCE_RULES_COVERAGE_MISSING',
|
|
@@ -64,6 +65,9 @@ const defaultDependencies: PreWriteAutomationDependencies = {
|
|
|
64
65
|
const hasAutoFixableEvidenceViolation = (aiGate: ReturnType<typeof evaluateAiGate>): boolean =>
|
|
65
66
|
aiGate.violations.some((violation) => PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES.has(violation.code));
|
|
66
67
|
|
|
68
|
+
const hasEvidenceGateBlockedViolation = (aiGate: ReturnType<typeof evaluateAiGate>): boolean =>
|
|
69
|
+
aiGate.violations.some((violation) => violation.code === 'EVIDENCE_GATE_BLOCKED');
|
|
70
|
+
|
|
67
71
|
const hasAutoFixableMcpReceiptViolation = (aiGate: ReturnType<typeof evaluateAiGate>): boolean =>
|
|
68
72
|
aiGate.violations.some((violation) => PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES.has(violation.code));
|
|
69
73
|
|
|
@@ -122,7 +126,43 @@ export const buildPreWriteAutomationTrace = async (params: {
|
|
|
122
126
|
trace.actions.push({
|
|
123
127
|
action: 'refresh_evidence',
|
|
124
128
|
status: 'OK',
|
|
125
|
-
details: `runPlatformGate exit_code=${gateExitCode}`,
|
|
129
|
+
details: `stage=PRE_COMMIT runPlatformGate exit_code=${gateExitCode}`,
|
|
130
|
+
});
|
|
131
|
+
aiGate = activeDependencies.runEnterpriseAiGateCheck({
|
|
132
|
+
repoRoot: params.repoRoot,
|
|
133
|
+
stage: 'PRE_WRITE',
|
|
134
|
+
requireMcpReceipt: true,
|
|
135
|
+
}).result;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
trace.actions.push({
|
|
138
|
+
action: 'refresh_evidence',
|
|
139
|
+
status: 'FAILED',
|
|
140
|
+
details: error instanceof Error ? error.message : 'Unknown PRE_COMMIT evidence refresh error',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!aiGate.allowed && hasEvidenceGateBlockedViolation(aiGate)) {
|
|
146
|
+
trace.attempted = true;
|
|
147
|
+
try {
|
|
148
|
+
const gateExitCode = await params.runPlatformGate({
|
|
149
|
+
policy: {
|
|
150
|
+
stage: 'PRE_PUSH',
|
|
151
|
+
blockOnOrAbove: 'ERROR',
|
|
152
|
+
warnOnOrAbove: 'WARN',
|
|
153
|
+
},
|
|
154
|
+
scope: {
|
|
155
|
+
kind: 'workingTree',
|
|
156
|
+
},
|
|
157
|
+
auditMode: 'gate',
|
|
158
|
+
dependencies: {
|
|
159
|
+
printGateFindings: () => {},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
trace.actions.push({
|
|
163
|
+
action: 'refresh_evidence',
|
|
164
|
+
status: 'OK',
|
|
165
|
+
details: `stage=PRE_PUSH runPlatformGate exit_code=${gateExitCode}`,
|
|
126
166
|
});
|
|
127
167
|
aiGate = activeDependencies.runEnterpriseAiGateCheck({
|
|
128
168
|
repoRoot: params.repoRoot,
|
|
@@ -133,7 +173,7 @@ export const buildPreWriteAutomationTrace = async (params: {
|
|
|
133
173
|
trace.actions.push({
|
|
134
174
|
action: 'refresh_evidence',
|
|
135
175
|
status: 'FAILED',
|
|
136
|
-
details: error instanceof Error ? error.message : 'Unknown evidence refresh error',
|
|
176
|
+
details: error instanceof Error ? error.message : 'Unknown PRE_PUSH evidence refresh error',
|
|
137
177
|
});
|
|
138
178
|
}
|
|
139
179
|
}
|