pumuki 6.3.73 → 6.3.76
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/README.md +9 -7
- package/docs/operations/RELEASE_NOTES.md +0 -18
- 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 +45 -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/runPlatformGateFacts.ts +0 -1
- package/integrations/lifecycle/adapter.templates.json +0 -3
- package/integrations/lifecycle/adapter.ts +24 -0
- package/integrations/lifecycle/bootstrapManifest.ts +248 -0
- package/integrations/lifecycle/cli.ts +30 -68
- package/integrations/lifecycle/cliSdd.ts +4 -3
- package/integrations/lifecycle/doctor.ts +7 -22
- package/integrations/lifecycle/governanceObservationSnapshot.ts +29 -2
- package/integrations/lifecycle/index.ts +0 -2
- package/integrations/lifecycle/install.ts +21 -0
- package/integrations/lifecycle/state.ts +8 -1
- package/integrations/mcp/aiGateCheck.ts +140 -10
- package/integrations/mcp/alignedPlatformGate.ts +232 -0
- package/integrations/mcp/autoExecuteAiStart.ts +6 -1
- package/integrations/mcp/enterpriseServer.ts +23 -7
- package/integrations/mcp/enterpriseStdioServer.cli.ts +32 -5
- package/integrations/mcp/preFlightCheck.ts +10 -0
- package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
- package/integrations/platform/detectPlatforms.ts +0 -37
- package/integrations/policy/experimentalFeatures.ts +1 -1
- package/package.json +1 -10
- package/scripts/consumer-postinstall.cjs +1 -10
- package/AGENTS.md +0 -269
- package/CHANGELOG.md +0 -686
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +0 -62
- package/integrations/lifecycle/audit.ts +0 -101
- package/scripts/consumer-postinstall-resolve-args.cjs +0 -38
- package/scripts/pumuki-full-surface-smoke-lib.ts +0 -37
- package/scripts/pumuki-full-surface-smoke.ts +0 -261
- package/scripts/pumuki-smoke-installed-wrapper.cjs +0 -31
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readEvidenceResult } from '../evidence/readEvidence';
|
|
4
|
+
import { readRepoTrackingState } from '../evidence/trackingContract';
|
|
5
|
+
import type { RepoTrackingState } from '../evidence/schema';
|
|
4
6
|
import { readSddStatus } from '../sdd';
|
|
5
7
|
import type { SddStatusPayload } from '../sdd/types';
|
|
6
8
|
import type { LifecycleExperimentalFeaturesSnapshot } from './experimentalFeaturesSnapshot';
|
|
@@ -57,6 +59,7 @@ export type GovernanceObservationSnapshot = {
|
|
|
57
59
|
on_protected_branch_hint: boolean;
|
|
58
60
|
};
|
|
59
61
|
contract_surface: GovernanceContractSurface;
|
|
62
|
+
tracking: RepoTrackingState;
|
|
60
63
|
attention_codes: ReadonlyArray<string>;
|
|
61
64
|
governance_effective: 'green' | 'attention' | 'blocked';
|
|
62
65
|
agent_bootstrap_hints: ReadonlyArray<string>;
|
|
@@ -142,7 +145,8 @@ const summarizeEvidence = (repoRoot: string): GovernanceEvidenceSummary => {
|
|
|
142
145
|
const buildHints = (
|
|
143
146
|
surface: GovernanceContractSurface,
|
|
144
147
|
branch: string | null,
|
|
145
|
-
protectedBranchHint: boolean
|
|
148
|
+
protectedBranchHint: boolean,
|
|
149
|
+
tracking: RepoTrackingState
|
|
146
150
|
): string[] => {
|
|
147
151
|
const hints: string[] = [];
|
|
148
152
|
if (surface.agents_md) {
|
|
@@ -157,6 +161,17 @@ const buildHints = (
|
|
|
157
161
|
if (protectedBranchHint && branch) {
|
|
158
162
|
hints.push(`La rama "${branch}" cae en el set protegido por defecto: usa feature/* o refactor/*.`);
|
|
159
163
|
}
|
|
164
|
+
if (tracking.conflict) {
|
|
165
|
+
hints.push('Tracking canónico en conflicto: AGENTS.md y los README del repo no apuntan al mismo MD.');
|
|
166
|
+
}
|
|
167
|
+
if (tracking.enforced && !tracking.canonical_present) {
|
|
168
|
+
hints.push(`Falta el tracking canónico declarado (${tracking.canonical_path ?? 'sin resolver'}).`);
|
|
169
|
+
}
|
|
170
|
+
if (tracking.enforced && tracking.single_in_progress_valid === false) {
|
|
171
|
+
hints.push(
|
|
172
|
+
`El tracking canónico debe dejar exactamente una 🚧 (actual=${tracking.in_progress_count ?? 'n/a'}).`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
160
175
|
hints.push('SDD/OpenSpec: usa PUMUKI_EXPERIMENTAL_SDD=advisory|strict cuando el loop SDD esté activo.');
|
|
161
176
|
hints.push('WARN-as-BLOCK: activa PUMUKI_ENTERPRISE_STRICT_WARN_AS_BLOCK=1 si el repo exige promoción dura.');
|
|
162
177
|
return hints;
|
|
@@ -176,6 +191,7 @@ export const readGovernanceObservationSnapshot = (params: {
|
|
|
176
191
|
const branch = readCurrentBranch(git, repoRoot);
|
|
177
192
|
const onProtected = typeof branch === 'string' && DEFAULT_PROTECTED_BRANCHES.has(branch.trim().toLowerCase());
|
|
178
193
|
const surface = buildContractSurface(repoRoot);
|
|
194
|
+
const tracking = readRepoTrackingState(repoRoot);
|
|
179
195
|
const warnAsBlock = truthyEnv(process.env.PUMUKI_ENTERPRISE_STRICT_WARN_AS_BLOCK);
|
|
180
196
|
|
|
181
197
|
const attention: string[] = [];
|
|
@@ -209,6 +225,15 @@ export const readGovernanceObservationSnapshot = (params: {
|
|
|
209
225
|
if (onProtected) {
|
|
210
226
|
attention.push('GITFLOW_PROTECTED_BRANCH_CONTEXT');
|
|
211
227
|
}
|
|
228
|
+
if (tracking.conflict) {
|
|
229
|
+
attention.push('TRACKING_CANONICAL_SOURCE_CONFLICT');
|
|
230
|
+
}
|
|
231
|
+
if (tracking.enforced && !tracking.canonical_present) {
|
|
232
|
+
attention.push('TRACKING_CANONICAL_FILE_MISSING');
|
|
233
|
+
}
|
|
234
|
+
if (tracking.enforced && tracking.single_in_progress_valid === false) {
|
|
235
|
+
attention.push('TRACKING_CANONICAL_IN_PROGRESS_INVALID');
|
|
236
|
+
}
|
|
212
237
|
|
|
213
238
|
let governanceEffective: GovernanceObservationSnapshot['governance_effective'] = 'green';
|
|
214
239
|
if (
|
|
@@ -248,9 +273,10 @@ export const readGovernanceObservationSnapshot = (params: {
|
|
|
248
273
|
on_protected_branch_hint: onProtected,
|
|
249
274
|
},
|
|
250
275
|
contract_surface: surface,
|
|
276
|
+
tracking,
|
|
251
277
|
attention_codes: attention,
|
|
252
278
|
governance_effective: governanceEffective,
|
|
253
|
-
agent_bootstrap_hints: buildHints(surface, branch, onProtected),
|
|
279
|
+
agent_bootstrap_hints: buildHints(surface, branch, onProtected, tracking),
|
|
254
280
|
};
|
|
255
281
|
};
|
|
256
282
|
|
|
@@ -263,6 +289,7 @@ export const buildGovernanceObservationSummaryLines = (
|
|
|
263
289
|
`SDD: env=${snapshot.sdd.experimental_raw ?? '(unset)'} effective=${snapshot.sdd.effective_mode} session_active=${snapshot.sdd_session.active} session_valid=${snapshot.sdd_session.valid} change=${snapshot.sdd_session.change_id ?? 'none'}`,
|
|
264
290
|
`Evidence: readable=${snapshot.evidence.readable} stage=${snapshot.evidence.snapshot_stage ?? 'n/a'} outcome=${snapshot.evidence.snapshot_outcome ?? 'n/a'} ai_gate=${snapshot.evidence.ai_gate_status ?? 'n/a'} findings=${snapshot.evidence.findings_count ?? 'n/a'}`,
|
|
265
291
|
`GitFlow: branch=${snapshot.git.current_branch ?? 'unknown'} protected_hint=${snapshot.git.on_protected_branch_hint ? 'yes' : 'no'}`,
|
|
292
|
+
`Tracking: enforced=${snapshot.tracking.enforced} canonical=${snapshot.tracking.canonical_path ?? 'none'} present=${snapshot.tracking.canonical_present} single_active=${snapshot.tracking.single_in_progress_valid ?? 'n/a'} count=${snapshot.tracking.in_progress_count ?? 'n/a'} conflict=${snapshot.tracking.conflict}`,
|
|
266
293
|
`Policy strict: PRE_WRITE=${snapshot.policy_strict.pre_write} PRE_COMMIT=${snapshot.policy_strict.pre_commit} PRE_PUSH=${snapshot.policy_strict.pre_push} CI=${snapshot.policy_strict.ci}`,
|
|
267
294
|
];
|
|
268
295
|
if (snapshot.attention_codes.length > 0) {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export { runLifecycleDoctor, doctorHasBlockingIssues } from './doctor';
|
|
2
|
-
export { runLifecycleAudit } from './audit';
|
|
3
|
-
export type { LifecycleAuditResult, LifecycleAuditStage } from './audit';
|
|
4
2
|
export { runLifecycleInstall } from './install';
|
|
5
3
|
export { runLifecycleUninstall } from './uninstall';
|
|
6
4
|
export { runLifecycleRemove } from './remove';
|
|
@@ -13,6 +13,7 @@ import { createEmptyEvaluationMetrics } from '../evidence/evaluationMetrics';
|
|
|
13
13
|
import { readOpenSpecManagedArtifacts, writeLifecycleState } from './state';
|
|
14
14
|
import { ensureRuntimeArtifactsIgnored } from './artifacts';
|
|
15
15
|
import { runLifecycleAdapterInstall } from './adapter';
|
|
16
|
+
import { writeLifecycleBootstrapManifest } from './bootstrapManifest';
|
|
16
17
|
|
|
17
18
|
export type LifecycleInstallResult = {
|
|
18
19
|
repoRoot: string;
|
|
@@ -20,6 +21,10 @@ export type LifecycleInstallResult = {
|
|
|
20
21
|
changedHooks: ReadonlyArray<string>;
|
|
21
22
|
openSpecBootstrap?: OpenSpecBootstrapResult;
|
|
22
23
|
degradedDoctorBypass?: boolean;
|
|
24
|
+
bootstrapManifest: {
|
|
25
|
+
path: string;
|
|
26
|
+
changed: boolean;
|
|
27
|
+
};
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
const shouldBootstrapEvidence = (repoRoot: string): boolean =>
|
|
@@ -103,12 +108,20 @@ export const runLifecycleInstall = (params?: {
|
|
|
103
108
|
openSpecManagedArtifacts: priorArtifacts.length > 0 ? priorArtifacts : undefined,
|
|
104
109
|
});
|
|
105
110
|
ensureRepoBaselineAdapter(report.repoRoot);
|
|
111
|
+
const bootstrapManifest = writeLifecycleBootstrapManifest({
|
|
112
|
+
git,
|
|
113
|
+
repoRoot: report.repoRoot,
|
|
114
|
+
});
|
|
106
115
|
return {
|
|
107
116
|
repoRoot: report.repoRoot,
|
|
108
117
|
version,
|
|
109
118
|
changedHooks,
|
|
110
119
|
openSpecBootstrap: undefined,
|
|
111
120
|
degradedDoctorBypass: true,
|
|
121
|
+
bootstrapManifest: {
|
|
122
|
+
path: bootstrapManifest.path,
|
|
123
|
+
changed: bootstrapManifest.changed,
|
|
124
|
+
},
|
|
112
125
|
};
|
|
113
126
|
}
|
|
114
127
|
const renderedIssues = report.issues.map((issue) => `- [${issue.severity}] ${issue.message}`).join('\n');
|
|
@@ -142,11 +155,19 @@ export const runLifecycleInstall = (params?: {
|
|
|
142
155
|
openSpecManagedArtifacts: Array.from(mergedOpenSpecArtifacts),
|
|
143
156
|
});
|
|
144
157
|
ensureRepoBaselineAdapter(report.repoRoot);
|
|
158
|
+
const bootstrapManifest = writeLifecycleBootstrapManifest({
|
|
159
|
+
git,
|
|
160
|
+
repoRoot: report.repoRoot,
|
|
161
|
+
});
|
|
145
162
|
|
|
146
163
|
return {
|
|
147
164
|
repoRoot: report.repoRoot,
|
|
148
165
|
version,
|
|
149
166
|
changedHooks,
|
|
150
167
|
openSpecBootstrap,
|
|
168
|
+
bootstrapManifest: {
|
|
169
|
+
path: bootstrapManifest.path,
|
|
170
|
+
changed: bootstrapManifest.changed,
|
|
171
|
+
},
|
|
151
172
|
};
|
|
152
173
|
};
|
|
@@ -49,10 +49,17 @@ export const writeLifecycleState = (params: {
|
|
|
49
49
|
openSpecManagedArtifacts?: ReadonlyArray<string>;
|
|
50
50
|
}): void => {
|
|
51
51
|
const { git, repoRoot, version } = params;
|
|
52
|
+
const existingInstalledAt = git.localConfig(repoRoot, PUMUKI_CONFIG_KEYS.installedAt);
|
|
52
53
|
git.applyLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.installed, 'true');
|
|
53
54
|
git.applyLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.version, version);
|
|
54
55
|
git.applyLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.hooks, PUMUKI_MANAGED_HOOKS.join(','));
|
|
55
|
-
git.applyLocalConfig(
|
|
56
|
+
git.applyLocalConfig(
|
|
57
|
+
repoRoot,
|
|
58
|
+
PUMUKI_CONFIG_KEYS.installedAt,
|
|
59
|
+
typeof existingInstalledAt === 'string' && existingInstalledAt.trim().length > 0
|
|
60
|
+
? existingInstalledAt
|
|
61
|
+
: new Date().toISOString()
|
|
62
|
+
);
|
|
56
63
|
if (params.openSpecManagedArtifacts) {
|
|
57
64
|
const serialized = serializeManagedArtifacts(params.openSpecManagedArtifacts);
|
|
58
65
|
if (serialized) {
|
|
@@ -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
|
+
};
|
|
@@ -54,7 +54,10 @@ const confidenceFromViolation = (violationCode: string | null): number => {
|
|
|
54
54
|
if (isEvidenceCode(violationCode)) {
|
|
55
55
|
return 65;
|
|
56
56
|
}
|
|
57
|
-
if (
|
|
57
|
+
if (
|
|
58
|
+
violationCode === 'GITFLOW_PROTECTED_BRANCH'
|
|
59
|
+
|| violationCode === 'GITFLOW_BRANCH_NAMING_INVALID'
|
|
60
|
+
) {
|
|
58
61
|
return 40;
|
|
59
62
|
}
|
|
60
63
|
return 50;
|
|
@@ -67,6 +70,8 @@ const normalizeGovernanceCatalogCode = (code: string): string => {
|
|
|
67
70
|
return 'EVIDENCE_INVALID_OR_CHAIN';
|
|
68
71
|
case 'GITFLOW_PROTECTED_BRANCH':
|
|
69
72
|
return 'GITFLOW_PROTECTED_BRANCH_CONTEXT';
|
|
73
|
+
case 'GITFLOW_BRANCH_NAMING_INVALID':
|
|
74
|
+
return 'GITFLOW_BRANCH_NAMING_INVALID_CONTEXT';
|
|
70
75
|
default:
|
|
71
76
|
return code;
|
|
72
77
|
}
|