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.
Files changed (44) hide show
  1. package/VERSION +1 -1
  2. package/docs/README.md +9 -7
  3. package/docs/operations/RELEASE_NOTES.md +0 -18
  4. package/docs/validation/README.md +3 -1
  5. package/integrations/evidence/buildEvidence.ts +14 -0
  6. package/integrations/evidence/repoState.ts +3 -0
  7. package/integrations/evidence/schema.ts +18 -0
  8. package/integrations/evidence/trackingContract.ts +146 -0
  9. package/integrations/evidence/writeEvidence.ts +14 -0
  10. package/integrations/gate/evaluateAiGate.ts +166 -3
  11. package/integrations/gate/governanceActionCatalog.ts +45 -0
  12. package/integrations/gate/remediationCatalog.ts +8 -0
  13. package/integrations/git/GitService.ts +0 -25
  14. package/integrations/git/aiGateRepoPolicyFindings.ts +4 -0
  15. package/integrations/git/runPlatformGateFacts.ts +0 -1
  16. package/integrations/lifecycle/adapter.templates.json +0 -3
  17. package/integrations/lifecycle/adapter.ts +24 -0
  18. package/integrations/lifecycle/bootstrapManifest.ts +248 -0
  19. package/integrations/lifecycle/cli.ts +30 -68
  20. package/integrations/lifecycle/cliSdd.ts +4 -3
  21. package/integrations/lifecycle/doctor.ts +7 -22
  22. package/integrations/lifecycle/governanceObservationSnapshot.ts +29 -2
  23. package/integrations/lifecycle/index.ts +0 -2
  24. package/integrations/lifecycle/install.ts +21 -0
  25. package/integrations/lifecycle/state.ts +8 -1
  26. package/integrations/mcp/aiGateCheck.ts +140 -10
  27. package/integrations/mcp/alignedPlatformGate.ts +232 -0
  28. package/integrations/mcp/autoExecuteAiStart.ts +6 -1
  29. package/integrations/mcp/enterpriseServer.ts +23 -7
  30. package/integrations/mcp/enterpriseStdioServer.cli.ts +32 -5
  31. package/integrations/mcp/preFlightCheck.ts +10 -0
  32. package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
  33. package/integrations/platform/detectPlatforms.ts +0 -37
  34. package/integrations/policy/experimentalFeatures.ts +1 -1
  35. package/package.json +1 -10
  36. package/scripts/consumer-postinstall.cjs +1 -10
  37. package/AGENTS.md +0 -269
  38. package/CHANGELOG.md +0 -686
  39. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +0 -62
  40. package/integrations/lifecycle/audit.ts +0 -101
  41. package/scripts/consumer-postinstall-resolve-args.cjs +0 -38
  42. package/scripts/pumuki-full-surface-smoke-lib.ts +0 -37
  43. package/scripts/pumuki-full-surface-smoke.ts +0 -261
  44. 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(repoRoot, PUMUKI_CONFIG_KEYS.installedAt, new Date().toISOString());
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 = (evaluation: ReturnType<typeof evaluateAiGate>): string => {
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 (violationCode === 'GITFLOW_PROTECTED_BRANCH') {
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
  }