pumuki 6.3.72 → 6.3.75
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/docs/README.md +9 -7
- package/docs/operations/RELEASE_NOTES.md +0 -7
- package/docs/product/USAGE.md +2 -5
- package/docs/validation/README.md +3 -1
- package/integrations/evidence/buildEvidence.ts +14 -0
- package/integrations/evidence/repoState.ts +3 -0
- package/integrations/evidence/schema.ts +18 -0
- package/integrations/evidence/trackingContract.ts +146 -0
- package/integrations/evidence/writeEvidence.ts +14 -0
- package/integrations/gate/evaluateAiGate.ts +166 -3
- package/integrations/gate/governanceActionCatalog.ts +275 -0
- package/integrations/gate/remediationCatalog.ts +8 -0
- package/integrations/git/GitService.ts +0 -25
- package/integrations/git/aiGateRepoPolicyFindings.ts +4 -0
- package/integrations/git/runPlatformGate.ts +9 -1
- package/integrations/git/runPlatformGateFacts.ts +0 -7
- package/integrations/git/runPlatformGateOutput.ts +36 -27
- package/integrations/lifecycle/adapter.ts +24 -0
- package/integrations/lifecycle/bootstrapManifest.ts +248 -0
- package/integrations/lifecycle/cli.ts +45 -11
- package/integrations/lifecycle/cliSdd.ts +4 -3
- package/integrations/lifecycle/doctor.ts +49 -1
- package/integrations/lifecycle/governanceNextAction.ts +164 -0
- package/integrations/lifecycle/governanceObservationSnapshot.ts +315 -0
- package/integrations/lifecycle/install.ts +21 -0
- package/integrations/lifecycle/state.ts +8 -1
- package/integrations/lifecycle/status.ts +29 -2
- package/integrations/mcp/aiGateCheck.ts +140 -10
- package/integrations/mcp/alignedPlatformGate.ts +232 -0
- package/integrations/mcp/autoExecuteAiStart.ts +92 -85
- package/integrations/mcp/enterpriseServer.ts +6 -6
- package/integrations/mcp/preFlightCheck.ts +51 -5
- package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
- package/integrations/policy/experimentalFeatures.ts +1 -1
- package/package.json +2 -4
- package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
- package/scripts/consumer-menu-matrix-baseline-report-lib.ts +38 -13
- package/scripts/framework-menu-consumer-actions-lib.ts +4 -28
- package/scripts/framework-menu-consumer-preflight-hints.ts +2 -5
- package/scripts/framework-menu-consumer-preflight-render.ts +6 -0
- package/scripts/framework-menu-consumer-preflight-run.ts +19 -0
- package/scripts/framework-menu-consumer-preflight-types.ts +8 -0
- package/scripts/framework-menu-consumer-runtime-actions.ts +6 -86
- package/scripts/framework-menu-consumer-runtime-audit.ts +2 -36
- package/scripts/framework-menu-consumer-runtime-lib.ts +0 -2
- package/scripts/framework-menu-consumer-runtime-types.ts +1 -3
- package/scripts/framework-menu-evidence-summary-lib.ts +0 -1
- package/scripts/framework-menu-evidence-summary-read.ts +5 -57
- package/scripts/framework-menu-evidence-summary-severity.ts +1 -3
- package/scripts/framework-menu-evidence-summary-types.ts +0 -7
- package/scripts/framework-menu-gate-lib.ts +0 -9
- package/scripts/framework-menu-layout-data.ts +0 -5
- package/scripts/framework-menu-matrix-baseline-lib.ts +14 -15
- package/scripts/framework-menu-matrix-canary-lib.ts +1 -22
- package/scripts/framework-menu-matrix-evidence-lib.ts +0 -1
- package/scripts/framework-menu-matrix-evidence-types.ts +1 -13
- package/scripts/framework-menu-matrix-runner-lib.ts +0 -35
- package/scripts/framework-menu-system-notifications-macos.ts +0 -4
- package/scripts/framework-menu.ts +0 -3
- package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
- package/AGENTS.md +0 -269
- package/CHANGELOG.md +0 -666
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +0 -111
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +0 -140
|
@@ -2,9 +2,17 @@ import { evaluateAiGate, type AiGateStage } from '../gate/evaluateAiGate';
|
|
|
2
2
|
import { resolveRemediationHintForViolationCode } from '../gate/remediationCatalog';
|
|
3
3
|
import { resolveLearningContextExperimentalFeature } from '../policy/experimentalFeatures';
|
|
4
4
|
import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
|
|
5
|
+
import { runMcpAlignedPlatformGate } from './alignedPlatformGate';
|
|
5
6
|
|
|
6
7
|
const PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
|
|
7
8
|
|
|
9
|
+
type PlatformGateAlignment = {
|
|
10
|
+
mode: 'full' | 'policy';
|
|
11
|
+
exit_code: number;
|
|
12
|
+
aligned: boolean;
|
|
13
|
+
skip_reason: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
8
16
|
export type EnterpriseAiGateCheckResult = {
|
|
9
17
|
tool: 'ai_gate_check';
|
|
10
18
|
dryRun: true;
|
|
@@ -31,6 +39,7 @@ export type EnterpriseAiGateCheckResult = {
|
|
|
31
39
|
reason_code: 'HOOK_RUNNER_CAN_REFRESH_EVIDENCE' | null;
|
|
32
40
|
message: string;
|
|
33
41
|
};
|
|
42
|
+
platform_gate_alignment?: PlatformGateAlignment;
|
|
34
43
|
};
|
|
35
44
|
};
|
|
36
45
|
|
|
@@ -41,23 +50,18 @@ const isHookRefreshableEvidenceCode = (code: string): boolean =>
|
|
|
41
50
|
|
|
42
51
|
type AiGateCheckDependencies = {
|
|
43
52
|
evaluateAiGate: typeof evaluateAiGate;
|
|
53
|
+
runMcpAlignedPlatformGate: typeof runMcpAlignedPlatformGate;
|
|
44
54
|
};
|
|
45
55
|
|
|
46
56
|
const defaultDependencies: AiGateCheckDependencies = {
|
|
47
57
|
evaluateAiGate,
|
|
58
|
+
runMcpAlignedPlatformGate,
|
|
48
59
|
};
|
|
49
60
|
|
|
50
61
|
const buildConsistencyHint = (
|
|
51
|
-
evaluation: ReturnType<typeof evaluateAiGate
|
|
62
|
+
evaluation: ReturnType<typeof evaluateAiGate>,
|
|
63
|
+
platform?: { exitCode: number; aligned: boolean; skipReason: string | null }
|
|
52
64
|
): EnterpriseAiGateCheckResult['result']['consistency_hint'] => {
|
|
53
|
-
if (!HOOK_STAGE_SET.has(evaluation.stage)) {
|
|
54
|
-
return {
|
|
55
|
-
comparable_with_hook_runner: true,
|
|
56
|
-
reason_code: null,
|
|
57
|
-
message: 'Stage is directly comparable with ai_gate_check semantics.',
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
65
|
const hasRefreshableEvidenceViolation = evaluation.violations.some((violation) =>
|
|
62
66
|
isHookRefreshableEvidenceCode(violation.code)
|
|
63
67
|
);
|
|
@@ -72,6 +76,24 @@ const buildConsistencyHint = (
|
|
|
72
76
|
};
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
if (platform?.aligned) {
|
|
80
|
+
return {
|
|
81
|
+
comparable_with_hook_runner: true,
|
|
82
|
+
reason_code: null,
|
|
83
|
+
message:
|
|
84
|
+
`ai_gate_check ejecutó runPlatformGate después de leer la evidencia actual (exit_code=${platform.exitCode}); ` +
|
|
85
|
+
'alineación hook-like habilitada explícitamente para este stage.',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!HOOK_STAGE_SET.has(evaluation.stage)) {
|
|
90
|
+
return {
|
|
91
|
+
comparable_with_hook_runner: true,
|
|
92
|
+
reason_code: null,
|
|
93
|
+
message: 'Stage is directly comparable with ai_gate_check semantics.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
return {
|
|
76
98
|
comparable_with_hook_runner: true,
|
|
77
99
|
reason_code: null,
|
|
@@ -118,7 +140,14 @@ const buildAutoFixes = (
|
|
|
118
140
|
return fixes;
|
|
119
141
|
};
|
|
120
142
|
|
|
121
|
-
const buildMessage = (
|
|
143
|
+
const buildMessage = (
|
|
144
|
+
evaluation: ReturnType<typeof evaluateAiGate>,
|
|
145
|
+
platform?: { exitCode: number; skipReason: string | null }
|
|
146
|
+
): string => {
|
|
147
|
+
if (platform && platform.exitCode !== 0) {
|
|
148
|
+
const suffix = platform.skipReason ? ` (${platform.skipReason})` : '';
|
|
149
|
+
return `🔴 runPlatformGate exit_code=${platform.exitCode}${suffix}.`;
|
|
150
|
+
}
|
|
122
151
|
if (evaluation.allowed) {
|
|
123
152
|
return `✅ Gate ${evaluation.stage} ALLOWED.`;
|
|
124
153
|
}
|
|
@@ -129,6 +158,26 @@ const buildMessage = (evaluation: ReturnType<typeof evaluateAiGate>): string =>
|
|
|
129
158
|
return `🔴 ${firstViolation.code}: ${firstViolation.message}`;
|
|
130
159
|
};
|
|
131
160
|
|
|
161
|
+
const resolveAiGateCheckMode = (): PlatformGateAlignment['mode'] => {
|
|
162
|
+
const raw = process.env.PUMUKI_MCP_AI_GATE_CHECK_MODE?.trim().toLowerCase();
|
|
163
|
+
return raw === 'full' || raw === 'aligned' ? 'full' : 'policy';
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const toPlatformGateAlignment = (
|
|
167
|
+
mode: PlatformGateAlignment['mode'],
|
|
168
|
+
platform?: { exitCode: number; aligned: boolean; skipReason: string | null }
|
|
169
|
+
): PlatformGateAlignment | undefined => {
|
|
170
|
+
if (mode !== 'full' || !platform) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
mode,
|
|
175
|
+
exit_code: platform.exitCode,
|
|
176
|
+
aligned: platform.aligned,
|
|
177
|
+
skip_reason: platform.skipReason,
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
132
181
|
export const runEnterpriseAiGateCheck = (params: {
|
|
133
182
|
repoRoot: string;
|
|
134
183
|
stage: AiGateStage;
|
|
@@ -180,3 +229,84 @@ export const runEnterpriseAiGateCheck = (params: {
|
|
|
180
229
|
},
|
|
181
230
|
};
|
|
182
231
|
};
|
|
232
|
+
|
|
233
|
+
export const runEnterpriseAiGateCheckAsync = async (params: {
|
|
234
|
+
repoRoot: string;
|
|
235
|
+
stage: AiGateStage;
|
|
236
|
+
requireMcpReceipt?: boolean;
|
|
237
|
+
}, dependencies: Partial<AiGateCheckDependencies> = {}): Promise<EnterpriseAiGateCheckResult> => {
|
|
238
|
+
const mode = resolveAiGateCheckMode();
|
|
239
|
+
const activeDependencies: AiGateCheckDependencies = {
|
|
240
|
+
...defaultDependencies,
|
|
241
|
+
...dependencies,
|
|
242
|
+
};
|
|
243
|
+
const evaluation = activeDependencies.evaluateAiGate({
|
|
244
|
+
repoRoot: params.repoRoot,
|
|
245
|
+
stage: params.stage,
|
|
246
|
+
requireMcpReceipt: params.requireMcpReceipt ?? false,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
let platform:
|
|
250
|
+
| { exitCode: number; aligned: boolean; skipReason: string | null }
|
|
251
|
+
| undefined;
|
|
252
|
+
if (mode === 'full') {
|
|
253
|
+
platform = await activeDependencies.runMcpAlignedPlatformGate({
|
|
254
|
+
repoRoot: params.repoRoot,
|
|
255
|
+
stage: params.stage,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const platformBlocks = Boolean(platform && platform.exitCode !== 0);
|
|
260
|
+
const allowed = evaluation.allowed && !platformBlocks;
|
|
261
|
+
const status: 'ALLOWED' | 'BLOCKED' = allowed ? 'ALLOWED' : 'BLOCKED';
|
|
262
|
+
const violations = platformBlocks && platform
|
|
263
|
+
? [
|
|
264
|
+
...evaluation.violations,
|
|
265
|
+
{
|
|
266
|
+
code: 'PLATFORM_GATE_EXIT_NON_ZERO',
|
|
267
|
+
message:
|
|
268
|
+
`runPlatformGate devolvió exit_code=${platform.exitCode}` +
|
|
269
|
+
(platform.skipReason ? ` (${platform.skipReason})` : ''),
|
|
270
|
+
severity: 'ERROR' as const,
|
|
271
|
+
},
|
|
272
|
+
]
|
|
273
|
+
: evaluation.violations;
|
|
274
|
+
const branch = evaluation.repo_state.git.branch;
|
|
275
|
+
const timestamp = evaluation.evidence.source.generated_at;
|
|
276
|
+
const learningContextFeature = resolveLearningContextExperimentalFeature();
|
|
277
|
+
const learningContext = learningContextFeature.mode === 'off'
|
|
278
|
+
? null
|
|
279
|
+
: readSddLearningContext({
|
|
280
|
+
repoRoot: params.repoRoot,
|
|
281
|
+
});
|
|
282
|
+
const evaluationForHints = { ...evaluation, allowed, status, violations };
|
|
283
|
+
const warnings = buildWarnings(evaluationForHints);
|
|
284
|
+
const autoFixes = buildAutoFixes(evaluationForHints, learningContext);
|
|
285
|
+
const message = buildMessage(evaluationForHints, platform);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
tool: 'ai_gate_check',
|
|
289
|
+
dryRun: true,
|
|
290
|
+
executed: true,
|
|
291
|
+
success: allowed,
|
|
292
|
+
result: {
|
|
293
|
+
allowed,
|
|
294
|
+
status,
|
|
295
|
+
timestamp,
|
|
296
|
+
branch,
|
|
297
|
+
message,
|
|
298
|
+
stage: evaluation.stage,
|
|
299
|
+
policy: evaluation.policy,
|
|
300
|
+
violations,
|
|
301
|
+
warnings,
|
|
302
|
+
auto_fixes: autoFixes,
|
|
303
|
+
learning_context: learningContext,
|
|
304
|
+
evidence: evaluation.evidence,
|
|
305
|
+
mcp_receipt: evaluation.mcp_receipt,
|
|
306
|
+
skills_contract: evaluation.skills_contract,
|
|
307
|
+
repo_state: evaluation.repo_state,
|
|
308
|
+
consistency_hint: buildConsistencyHint(evaluationForHints, platform),
|
|
309
|
+
platform_gate_alignment: toPlatformGateAlignment(mode, platform),
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { AiGateStage } from '../gate/evaluateAiGate';
|
|
2
|
+
import { resolvePolicyForStage } from '../gate/stagePolicies';
|
|
3
|
+
import type { SddDecision } from '../sdd';
|
|
4
|
+
import { GitService } from '../git/GitService';
|
|
5
|
+
import { runPlatformGate } from '../git/runPlatformGate';
|
|
6
|
+
import type { GateScope } from '../git/runPlatformGateFacts';
|
|
7
|
+
import { readMcpPrePushStdin } from './readMcpPrePushStdin';
|
|
8
|
+
|
|
9
|
+
const ZERO_HASH = /^0+$/;
|
|
10
|
+
|
|
11
|
+
const runGit = (repoRoot: string, args: ReadonlyArray<string>): string | null => {
|
|
12
|
+
try {
|
|
13
|
+
return new GitService().runGit(args, repoRoot).trim();
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const resolveUpstreamRefInRepo = (repoRoot: string): string | null =>
|
|
20
|
+
runGit(repoRoot, ['rev-parse', '@{u}']);
|
|
21
|
+
|
|
22
|
+
const resolveHeadOidInRepo = (repoRoot: string): string | null =>
|
|
23
|
+
runGit(repoRoot, ['rev-parse', 'HEAD']);
|
|
24
|
+
|
|
25
|
+
const resolveCiBaseRefInRepo = (repoRoot: string): string => {
|
|
26
|
+
const fromEnv = process.env.GITHUB_BASE_REF?.trim();
|
|
27
|
+
if (fromEnv) {
|
|
28
|
+
if (runGit(repoRoot, ['rev-parse', '--verify', fromEnv])) {
|
|
29
|
+
return fromEnv;
|
|
30
|
+
}
|
|
31
|
+
const remoteRef = `origin/${fromEnv}`;
|
|
32
|
+
if (runGit(repoRoot, ['rev-parse', '--verify', remoteRef])) {
|
|
33
|
+
return remoteRef;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const candidate of ['origin/main', 'main', 'HEAD']) {
|
|
38
|
+
if (runGit(repoRoot, ['rev-parse', '--verify', candidate])) {
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return 'HEAD';
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const resolvePrePushBootstrapBaseRefInRepo = (repoRoot: string): string => {
|
|
47
|
+
const candidates = ['origin/develop', 'develop', resolveCiBaseRefInRepo(repoRoot)];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (runGit(repoRoot, ['rev-parse', '--verify', candidate])) {
|
|
50
|
+
return candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return 'HEAD';
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const shouldAllowBootstrapPrePush = (rawInput: string): boolean => {
|
|
58
|
+
const lines = rawInput
|
|
59
|
+
.split('\n')
|
|
60
|
+
.map((line) => line.trim())
|
|
61
|
+
.filter((line) => line.length > 0);
|
|
62
|
+
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const [localRef, localOid, remoteRef, remoteOid] = line.split(/\s+/);
|
|
65
|
+
if (!localRef || !localOid || !remoteRef || !remoteOid) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const localIsBranch = localRef.startsWith('refs/heads/');
|
|
69
|
+
const remoteIsBranch = remoteRef.startsWith('refs/heads/');
|
|
70
|
+
const localIsDeletion = ZERO_HASH.test(localOid);
|
|
71
|
+
const remoteIsNewBranch = ZERO_HASH.test(remoteOid);
|
|
72
|
+
|
|
73
|
+
if (localIsBranch && remoteIsBranch && !localIsDeletion && remoteIsNewBranch) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const resolveExplicitPrePushRange = (
|
|
82
|
+
rawInput: string
|
|
83
|
+
): { fromRef: string; toRef: string } | undefined => {
|
|
84
|
+
const lines = rawInput
|
|
85
|
+
.split('\n')
|
|
86
|
+
.map((line) => line.trim())
|
|
87
|
+
.filter((line) => line.length > 0);
|
|
88
|
+
|
|
89
|
+
const eligibleRanges = lines
|
|
90
|
+
.map((line) => {
|
|
91
|
+
const [localRef, localOid, remoteRef, remoteOid] = line.split(/\s+/);
|
|
92
|
+
if (!localRef || !localOid || !remoteRef || !remoteOid) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const localIsDeletion = ZERO_HASH.test(localOid);
|
|
96
|
+
const remoteIsNewBranch = ZERO_HASH.test(remoteOid);
|
|
97
|
+
if (localIsDeletion || remoteIsNewBranch) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
fromRef: remoteOid,
|
|
102
|
+
toRef: localOid,
|
|
103
|
+
};
|
|
104
|
+
})
|
|
105
|
+
.filter((value): value is { fromRef: string; toRef: string } => Boolean(value));
|
|
106
|
+
|
|
107
|
+
if (eligibleRanges.length !== 1) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return eligibleRanges[0];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type PrePushScopeResolution =
|
|
115
|
+
| { kind: 'scope'; scope: GateScope; sddDecisionOverride?: Pick<SddDecision, 'allowed' | 'code' | 'message'> }
|
|
116
|
+
| { kind: 'upstream_missing' };
|
|
117
|
+
|
|
118
|
+
const resolvePrePushScopeForMcp = (params: { repoRoot: string }): PrePushScopeResolution => {
|
|
119
|
+
const prePushInput = readMcpPrePushStdin();
|
|
120
|
+
const upstreamRef = resolveUpstreamRefInRepo(params.repoRoot);
|
|
121
|
+
if (!upstreamRef) {
|
|
122
|
+
const bootstrapBaseRef = resolvePrePushBootstrapBaseRefInRepo(params.repoRoot);
|
|
123
|
+
const bootstrapByPrePushStdIn = shouldAllowBootstrapPrePush(prePushInput);
|
|
124
|
+
const bootstrapByFallbackBase = !bootstrapByPrePushStdIn && bootstrapBaseRef !== 'HEAD';
|
|
125
|
+
const manualInvocationFallback =
|
|
126
|
+
!bootstrapByPrePushStdIn &&
|
|
127
|
+
!bootstrapByFallbackBase &&
|
|
128
|
+
prePushInput.trim().length === 0;
|
|
129
|
+
if (bootstrapByPrePushStdIn || bootstrapByFallbackBase) {
|
|
130
|
+
return {
|
|
131
|
+
kind: 'scope',
|
|
132
|
+
scope: {
|
|
133
|
+
kind: 'range',
|
|
134
|
+
fromRef: bootstrapBaseRef,
|
|
135
|
+
toRef: 'HEAD',
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (manualInvocationFallback) {
|
|
140
|
+
return { kind: 'scope', scope: { kind: 'workingTree' } };
|
|
141
|
+
}
|
|
142
|
+
return { kind: 'upstream_missing' };
|
|
143
|
+
}
|
|
144
|
+
const explicitPrePushRange = resolveExplicitPrePushRange(prePushInput);
|
|
145
|
+
const prePushFromRef = explicitPrePushRange?.fromRef ?? upstreamRef;
|
|
146
|
+
const prePushToRef = explicitPrePushRange?.toRef ?? 'HEAD';
|
|
147
|
+
const headOid = resolveHeadOidInRepo(params.repoRoot);
|
|
148
|
+
const sddDecisionOverride =
|
|
149
|
+
explicitPrePushRange && headOid && explicitPrePushRange.toRef !== headOid
|
|
150
|
+
? ({
|
|
151
|
+
allowed: true,
|
|
152
|
+
code: 'ALLOWED',
|
|
153
|
+
message:
|
|
154
|
+
`SDD enforcement suspended for PRE_PUSH historical publish targeting ${explicitPrePushRange.toRef.slice(0, 12)} ` +
|
|
155
|
+
`instead of current HEAD ${headOid.slice(0, 12)}.`,
|
|
156
|
+
} as Pick<SddDecision, 'allowed' | 'code' | 'message'>)
|
|
157
|
+
: undefined;
|
|
158
|
+
return {
|
|
159
|
+
kind: 'scope',
|
|
160
|
+
scope: {
|
|
161
|
+
kind: 'range',
|
|
162
|
+
fromRef: prePushFromRef,
|
|
163
|
+
toRef: prePushToRef,
|
|
164
|
+
},
|
|
165
|
+
sddDecisionOverride,
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
type RunAlignedParams = {
|
|
170
|
+
repoRoot: string;
|
|
171
|
+
stage: AiGateStage;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const runMcpAlignedPlatformGate = async (
|
|
175
|
+
params: RunAlignedParams
|
|
176
|
+
): Promise<{ exitCode: number; aligned: boolean; skipReason: string | null }> => {
|
|
177
|
+
const git = new GitService();
|
|
178
|
+
const resolved = resolvePolicyForStage(params.stage, params.repoRoot);
|
|
179
|
+
if (params.stage === 'PRE_WRITE') {
|
|
180
|
+
const exitCode = await runPlatformGate({
|
|
181
|
+
policy: resolved.policy,
|
|
182
|
+
policyTrace: resolved.trace,
|
|
183
|
+
scope: { kind: 'workingTree' },
|
|
184
|
+
silent: true,
|
|
185
|
+
services: { git },
|
|
186
|
+
});
|
|
187
|
+
return { exitCode, aligned: true, skipReason: null };
|
|
188
|
+
}
|
|
189
|
+
if (params.stage === 'PRE_COMMIT') {
|
|
190
|
+
const exitCode = await runPlatformGate({
|
|
191
|
+
policy: resolved.policy,
|
|
192
|
+
policyTrace: resolved.trace,
|
|
193
|
+
scope: { kind: 'staged' },
|
|
194
|
+
silent: true,
|
|
195
|
+
services: { git },
|
|
196
|
+
});
|
|
197
|
+
return { exitCode, aligned: true, skipReason: null };
|
|
198
|
+
}
|
|
199
|
+
if (params.stage === 'CI') {
|
|
200
|
+
const ciBaseRef = resolveCiBaseRefInRepo(params.repoRoot);
|
|
201
|
+
const exitCode = await runPlatformGate({
|
|
202
|
+
policy: resolved.policy,
|
|
203
|
+
policyTrace: resolved.trace,
|
|
204
|
+
scope: {
|
|
205
|
+
kind: 'range',
|
|
206
|
+
fromRef: ciBaseRef,
|
|
207
|
+
toRef: 'HEAD',
|
|
208
|
+
},
|
|
209
|
+
silent: true,
|
|
210
|
+
services: { git },
|
|
211
|
+
});
|
|
212
|
+
return { exitCode, aligned: true, skipReason: null };
|
|
213
|
+
}
|
|
214
|
+
if (params.stage === 'PRE_PUSH') {
|
|
215
|
+
const scopeResolution = resolvePrePushScopeForMcp({ repoRoot: params.repoRoot });
|
|
216
|
+
if (scopeResolution.kind === 'upstream_missing') {
|
|
217
|
+
return { exitCode: 1, aligned: false, skipReason: 'PRE_PUSH_UPSTREAM_MISSING' };
|
|
218
|
+
}
|
|
219
|
+
const exitCode = await runPlatformGate({
|
|
220
|
+
policy: resolved.policy,
|
|
221
|
+
policyTrace: resolved.trace,
|
|
222
|
+
scope: scopeResolution.scope,
|
|
223
|
+
silent: true,
|
|
224
|
+
services: { git },
|
|
225
|
+
...(scopeResolution.sddDecisionOverride
|
|
226
|
+
? { sddDecisionOverride: scopeResolution.sddDecisionOverride }
|
|
227
|
+
: {}),
|
|
228
|
+
});
|
|
229
|
+
return { exitCode, aligned: true, skipReason: null };
|
|
230
|
+
}
|
|
231
|
+
throw new Error(`Unsupported MCP aligned stage: ${String(params.stage)}`);
|
|
232
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildGovernanceValidateCommand,
|
|
3
|
+
resolveGovernanceCatalogAction,
|
|
4
|
+
} from '../gate/governanceActionCatalog';
|
|
1
5
|
import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
|
|
2
6
|
import { collectWorktreeAtomicSlices } from '../git/worktreeAtomicSlices';
|
|
3
7
|
import { resolveLearningContextExperimentalFeature } from '../policy/experimentalFeatures';
|
|
@@ -50,96 +54,98 @@ const confidenceFromViolation = (violationCode: string | null): number => {
|
|
|
50
54
|
if (isEvidenceCode(violationCode)) {
|
|
51
55
|
return 65;
|
|
52
56
|
}
|
|
53
|
-
if (
|
|
57
|
+
if (
|
|
58
|
+
violationCode === 'GITFLOW_PROTECTED_BRANCH'
|
|
59
|
+
|| violationCode === 'GITFLOW_BRANCH_NAMING_INVALID'
|
|
60
|
+
) {
|
|
54
61
|
return 40;
|
|
55
62
|
}
|
|
56
63
|
return 50;
|
|
57
64
|
};
|
|
58
65
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
repoRoot: string
|
|
62
|
-
): AutoExecuteNextAction => {
|
|
63
|
-
if (!violation) {
|
|
64
|
-
return {
|
|
65
|
-
kind: 'info',
|
|
66
|
-
message: 'Gate listo. Puedes continuar con implementación.',
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
switch (violation.code) {
|
|
70
|
-
case 'EVIDENCE_MISSING':
|
|
66
|
+
const normalizeGovernanceCatalogCode = (code: string): string => {
|
|
67
|
+
switch (code) {
|
|
71
68
|
case 'EVIDENCE_INVALID':
|
|
72
69
|
case 'EVIDENCE_CHAIN_INVALID':
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
kind: 'run_command',
|
|
76
|
-
message: 'Regenera o refresca evidencia y vuelve a evaluar PRE_WRITE.',
|
|
77
|
-
command: 'npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
|
|
78
|
-
};
|
|
79
|
-
case 'EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES':
|
|
80
|
-
return {
|
|
81
|
-
kind: 'run_command',
|
|
82
|
-
message:
|
|
83
|
-
'No hay active_rule_ids para plataforma de código detectada. Reconciliación strict de policy/skills y revalidación PRE_WRITE.',
|
|
84
|
-
command:
|
|
85
|
-
'npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
|
|
86
|
-
};
|
|
87
|
-
case 'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE':
|
|
88
|
-
case 'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING':
|
|
89
|
-
case 'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE':
|
|
90
|
-
return {
|
|
91
|
-
kind: 'run_command',
|
|
92
|
-
message:
|
|
93
|
-
'Completa cobertura de skills por plataforma (prefijos + bundles) y revalida PRE_WRITE.',
|
|
94
|
-
command: 'npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
|
|
95
|
-
};
|
|
96
|
-
case 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING':
|
|
97
|
-
case 'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE':
|
|
98
|
-
return {
|
|
99
|
-
kind: 'run_command',
|
|
100
|
-
message:
|
|
101
|
-
'Reconcilia policy/skills en modo estricto (incluida skills.ios.critical-test-quality cuando aplique) y revalida PRE_WRITE.',
|
|
102
|
-
command:
|
|
103
|
-
'npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
|
|
104
|
-
};
|
|
105
|
-
case 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT':
|
|
106
|
-
case 'EVIDENCE_PREWRITE_WORKTREE_WARN':
|
|
107
|
-
{
|
|
108
|
-
const plan = collectWorktreeAtomicSlices({
|
|
109
|
-
repoRoot,
|
|
110
|
-
maxSlices: 3,
|
|
111
|
-
maxFilesPerSlice: 4,
|
|
112
|
-
});
|
|
113
|
-
if (plan.slices.length > 0) {
|
|
114
|
-
const firstSlice = plan.slices[0];
|
|
115
|
-
return {
|
|
116
|
-
kind: 'run_command',
|
|
117
|
-
message:
|
|
118
|
-
`Particiona el worktree en slices atómicos por scope. Primer lote sugerido: ${firstSlice?.scope ?? 'scope-desconocido'}.`,
|
|
119
|
-
command:
|
|
120
|
-
`${firstSlice?.staged_command ?? 'git add -p'} && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json`,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
kind: 'run_command',
|
|
126
|
-
message:
|
|
127
|
-
'Particiona el worktree en slices atómicos y revalida PRE_WRITE para continuar sin fricción.',
|
|
128
|
-
command:
|
|
129
|
-
'git status --short && git add -p && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
|
|
130
|
-
};
|
|
70
|
+
return 'EVIDENCE_INVALID_OR_CHAIN';
|
|
131
71
|
case 'GITFLOW_PROTECTED_BRANCH':
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
command: 'git checkout -b feature/<descripcion-kebab-case>',
|
|
136
|
-
};
|
|
72
|
+
return 'GITFLOW_PROTECTED_BRANCH_CONTEXT';
|
|
73
|
+
case 'GITFLOW_BRANCH_NAMING_INVALID':
|
|
74
|
+
return 'GITFLOW_BRANCH_NAMING_INVALID_CONTEXT';
|
|
137
75
|
default:
|
|
76
|
+
return code;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const resolveAutoExecuteRemediation = (params: {
|
|
81
|
+
violation: AiGateViolation | undefined;
|
|
82
|
+
repoRoot: string;
|
|
83
|
+
stage: AiGateStage;
|
|
84
|
+
allowed: boolean;
|
|
85
|
+
}): {
|
|
86
|
+
instruction: string;
|
|
87
|
+
nextAction: AutoExecuteNextAction;
|
|
88
|
+
} => {
|
|
89
|
+
if (!params.violation) {
|
|
90
|
+
const readyAction = resolveGovernanceCatalogAction({
|
|
91
|
+
code: 'READY',
|
|
92
|
+
stage: params.stage,
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
instruction: readyAction.instruction,
|
|
96
|
+
nextAction: readyAction.next_action,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
params.violation.code === 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT'
|
|
102
|
+
|| params.violation.code === 'EVIDENCE_PREWRITE_WORKTREE_WARN'
|
|
103
|
+
) {
|
|
104
|
+
const validateCommand = buildGovernanceValidateCommand(params.stage);
|
|
105
|
+
const plan = collectWorktreeAtomicSlices({
|
|
106
|
+
repoRoot: params.repoRoot,
|
|
107
|
+
maxSlices: 3,
|
|
108
|
+
maxFilesPerSlice: 4,
|
|
109
|
+
});
|
|
110
|
+
if (plan.slices.length > 0) {
|
|
111
|
+
const firstSlice = plan.slices[0];
|
|
138
112
|
return {
|
|
139
|
-
|
|
140
|
-
|
|
113
|
+
instruction: 'Particiona el worktree en slices atómicos antes de continuar.',
|
|
114
|
+
nextAction: {
|
|
115
|
+
kind: params.allowed ? 'info' : 'run_command',
|
|
116
|
+
message:
|
|
117
|
+
`Particiona el worktree en slices atómicos por scope. Primer lote sugerido: ${firstSlice?.scope ?? 'scope-desconocido'}.`,
|
|
118
|
+
command: params.allowed
|
|
119
|
+
? undefined
|
|
120
|
+
: `${firstSlice?.staged_command ?? 'git add -p'} && ${validateCommand}`,
|
|
121
|
+
},
|
|
141
122
|
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
instruction: 'Particiona el worktree en slices atómicos antes de continuar.',
|
|
126
|
+
nextAction: {
|
|
127
|
+
kind: params.allowed ? 'info' : 'run_command',
|
|
128
|
+
message: 'Particiona el worktree en slices atómicos y revalida PRE_WRITE para continuar sin fricción.',
|
|
129
|
+
command: params.allowed
|
|
130
|
+
? undefined
|
|
131
|
+
: `git status --short && git add -p && ${validateCommand}`,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
142
134
|
}
|
|
135
|
+
|
|
136
|
+
const governanceAction = resolveGovernanceCatalogAction({
|
|
137
|
+
code: normalizeGovernanceCatalogCode(params.violation.code),
|
|
138
|
+
stage: params.stage,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
instruction: governanceAction.instruction,
|
|
142
|
+
nextAction: params.allowed
|
|
143
|
+
? {
|
|
144
|
+
kind: 'info',
|
|
145
|
+
message: governanceAction.next_action.message,
|
|
146
|
+
}
|
|
147
|
+
: governanceAction.next_action,
|
|
148
|
+
};
|
|
143
149
|
};
|
|
144
150
|
|
|
145
151
|
export type EnterpriseAutoExecuteAiStartResult = {
|
|
@@ -190,19 +196,20 @@ export const runEnterpriseAutoExecuteAiStart = (params: {
|
|
|
190
196
|
const action: AutoExecuteAction = evaluation.allowed ? 'proceed' : 'ask';
|
|
191
197
|
const phase = toAutoExecutePhase(action);
|
|
192
198
|
const confidencePct = confidenceFromViolation(firstViolation?.code ?? null);
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
const remediation = resolveAutoExecuteRemediation({
|
|
200
|
+
violation: firstViolation,
|
|
201
|
+
repoRoot: params.repoRoot,
|
|
202
|
+
stage,
|
|
203
|
+
allowed: evaluation.allowed,
|
|
204
|
+
});
|
|
205
|
+
const nextAction = remediation.nextAction;
|
|
199
206
|
|
|
200
207
|
let message = toHumanMessage({
|
|
201
208
|
action,
|
|
202
209
|
confidencePct,
|
|
203
210
|
reasonCode,
|
|
204
211
|
});
|
|
205
|
-
let instruction =
|
|
212
|
+
let instruction = remediation.instruction;
|
|
206
213
|
if (learningContext?.recommended_actions[0]) {
|
|
207
214
|
message = `${message} Learning: ${learningContext.recommended_actions[0]}`;
|
|
208
215
|
instruction = `${instruction} Learning: ${learningContext.recommended_actions[0]}`;
|
|
@@ -9,7 +9,7 @@ import { resolveMcpEnterpriseExperimentalFeature } from '../policy/experimentalF
|
|
|
9
9
|
import { evaluateSddPolicy, readSddStatus } from '../sdd';
|
|
10
10
|
import type { SddStage } from '../sdd';
|
|
11
11
|
import { toStatusPayload } from './evidencePayloads';
|
|
12
|
-
import {
|
|
12
|
+
import { runEnterpriseAiGateCheckAsync } from './aiGateCheck';
|
|
13
13
|
import { runEnterprisePreFlightCheck } from './preFlightCheck';
|
|
14
14
|
import { runEnterpriseAutoExecuteAiStart } from './autoExecuteAiStart';
|
|
15
15
|
import { writeMcpAiGateReceipt } from './aiGateReceipt';
|
|
@@ -382,16 +382,16 @@ const evaluateCriticalToolGuard = (
|
|
|
382
382
|
}
|
|
383
383
|
};
|
|
384
384
|
|
|
385
|
-
const executeEnterpriseTool = (
|
|
385
|
+
const executeEnterpriseTool = async (
|
|
386
386
|
repoRoot: string,
|
|
387
387
|
toolName: EnterpriseToolName,
|
|
388
388
|
args: Record<string, string | number | boolean | bigint | symbol | null | Date | object>,
|
|
389
389
|
dryRun: boolean
|
|
390
|
-
): EnterpriseToolExecution => {
|
|
390
|
+
): Promise<EnterpriseToolExecution> => {
|
|
391
391
|
switch (toolName) {
|
|
392
392
|
case 'ai_gate_check': {
|
|
393
393
|
const stage = toSddStage(args.stage, 'PRE_COMMIT');
|
|
394
|
-
const execution =
|
|
394
|
+
const execution = await runEnterpriseAiGateCheckAsync({
|
|
395
395
|
repoRoot,
|
|
396
396
|
stage,
|
|
397
397
|
});
|
|
@@ -741,7 +741,7 @@ export const startEnterpriseMcpServer = (
|
|
|
741
741
|
return;
|
|
742
742
|
}
|
|
743
743
|
void readJsonBody(req)
|
|
744
|
-
.then((body) => {
|
|
744
|
+
.then(async (body) => {
|
|
745
745
|
if (typeof body !== 'object' || body === null) {
|
|
746
746
|
sendJson(res, 400, {
|
|
747
747
|
error: 'Invalid request body.',
|
|
@@ -814,7 +814,7 @@ export const startEnterpriseMcpServer = (
|
|
|
814
814
|
}
|
|
815
815
|
let result: EnterpriseToolExecution;
|
|
816
816
|
try {
|
|
817
|
-
result = executeEnterpriseTool(
|
|
817
|
+
result = await executeEnterpriseTool(
|
|
818
818
|
repoRoot,
|
|
819
819
|
toolName,
|
|
820
820
|
args,
|