pumuki 6.3.113 → 6.3.114

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 (99) hide show
  1. package/CHANGELOG.md +51 -5
  2. package/README.md +4 -2
  3. package/VERSION +1 -1
  4. package/core/facts/detectors/typescript/index.test.ts +0 -229
  5. package/core/facts/detectors/typescript/index.ts +0 -278
  6. package/core/facts/extractHeuristicFacts.ts +0 -4
  7. package/core/rules/presets/heuristics/typescript.test.ts +1 -21
  8. package/core/rules/presets/heuristics/typescript.ts +0 -72
  9. package/docs/README.md +13 -9
  10. package/docs/codex-skills/backend-enterprise-rules.md +3 -3
  11. package/docs/operations/RELEASE_NOTES.md +40 -4
  12. package/docs/product/API_REFERENCE.md +1 -1
  13. package/docs/product/HOW_IT_WORKS.md +6 -0
  14. package/docs/product/INSTALLATION.md +1 -1
  15. package/docs/product/USAGE.md +42 -5
  16. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +100 -44
  17. package/docs/validation/README.md +6 -3
  18. package/integrations/config/skillsDetectorRegistry.ts +0 -24
  19. package/integrations/config/skillsMarkdownRules.ts +0 -57
  20. package/integrations/evidence/buildEvidence.ts +0 -24
  21. package/integrations/evidence/repoState.ts +0 -3
  22. package/integrations/evidence/schema.ts +0 -18
  23. package/integrations/evidence/writeEvidence.ts +0 -24
  24. package/integrations/gate/evaluateAiGate.ts +8 -251
  25. package/integrations/gate/remediationCatalog.ts +0 -8
  26. package/integrations/git/GitService.ts +44 -5
  27. package/integrations/git/aiGateRepoPolicyFindings.ts +86 -17
  28. package/integrations/git/runPlatformGate.ts +1 -9
  29. package/integrations/git/runPlatformGateFacts.ts +19 -1
  30. package/integrations/git/runPlatformGateOutput.ts +41 -42
  31. package/integrations/lifecycle/adapter.templates.json +1 -0
  32. package/integrations/lifecycle/adapter.ts +0 -24
  33. package/integrations/lifecycle/audit.ts +101 -0
  34. package/integrations/lifecycle/cli.ts +120 -99
  35. package/integrations/lifecycle/cliSdd.ts +4 -26
  36. package/integrations/lifecycle/doctor.ts +40 -102
  37. package/integrations/lifecycle/index.ts +2 -0
  38. package/integrations/lifecycle/install.ts +0 -21
  39. package/integrations/lifecycle/packageInfo.ts +1 -118
  40. package/integrations/lifecycle/state.ts +1 -8
  41. package/integrations/lifecycle/status.ts +40 -59
  42. package/integrations/lifecycle/watch.ts +1 -1
  43. package/integrations/mcp/aiGateCheck.ts +10 -194
  44. package/integrations/mcp/autoExecuteAiStart.ts +116 -92
  45. package/integrations/mcp/enterpriseServer.ts +7 -23
  46. package/integrations/mcp/enterpriseStdioServer.cli.ts +4 -31
  47. package/integrations/mcp/preFlightCheck.ts +5 -67
  48. package/integrations/platform/detectPlatforms.ts +37 -0
  49. package/integrations/sdd/policy.ts +28 -20
  50. package/package.json +1 -1
  51. package/scripts/check-tracking-single-active.sh +1 -1
  52. package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
  53. package/scripts/consumer-postinstall-resolve-args.cjs +44 -0
  54. package/scripts/consumer-postinstall.cjs +76 -21
  55. package/scripts/framework-menu-advanced-view-lib.ts +0 -49
  56. package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
  57. package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
  58. package/scripts/framework-menu-consumer-preflight-render.ts +0 -10
  59. package/scripts/framework-menu-consumer-preflight-run.ts +0 -23
  60. package/scripts/framework-menu-consumer-preflight-types.ts +0 -12
  61. package/scripts/framework-menu-consumer-runtime-actions.ts +87 -17
  62. package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
  63. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
  64. package/scripts/framework-menu-consumer-runtime-lib.ts +2 -38
  65. package/scripts/framework-menu-consumer-runtime-menu.ts +4 -31
  66. package/scripts/framework-menu-consumer-runtime-types.ts +3 -5
  67. package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
  68. package/scripts/framework-menu-evidence-summary-read.ts +57 -5
  69. package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
  70. package/scripts/framework-menu-evidence-summary-types.ts +7 -0
  71. package/scripts/framework-menu-gate-lib.ts +9 -0
  72. package/scripts/framework-menu-layout-data.ts +5 -0
  73. package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
  74. package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
  75. package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
  76. package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
  77. package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
  78. package/scripts/framework-menu-system-notifications-cause.ts +0 -3
  79. package/scripts/framework-menu-system-notifications-macos-swift-source.ts +24 -204
  80. package/scripts/framework-menu-system-notifications-macos.ts +4 -0
  81. package/scripts/framework-menu-system-notifications-payloads-blocked.ts +1 -1
  82. package/scripts/framework-menu-system-notifications-text.ts +1 -7
  83. package/scripts/framework-menu.ts +3 -24
  84. package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -10
  85. package/scripts/package-install-smoke-consumer-npm-lib.ts +9 -46
  86. package/scripts/pumuki-full-surface-smoke-lib.ts +37 -0
  87. package/scripts/pumuki-full-surface-smoke.ts +346 -0
  88. package/scripts/pumuki-smoke-installed-wrapper.cjs +31 -0
  89. package/integrations/evidence/trackingContract.ts +0 -17
  90. package/integrations/gate/governanceActionCatalog.ts +0 -275
  91. package/integrations/lifecycle/bootstrapManifest.ts +0 -248
  92. package/integrations/lifecycle/cliGovernanceConsole.ts +0 -69
  93. package/integrations/lifecycle/governanceNextAction.ts +0 -171
  94. package/integrations/lifecycle/governanceObservationSnapshot.ts +0 -369
  95. package/integrations/lifecycle/trackingState.ts +0 -403
  96. package/integrations/mcp/alignedPlatformGate.ts +0 -232
  97. package/integrations/mcp/readMcpPrePushStdin.ts +0 -7
  98. package/scripts/build-ruralgo-s1-evidence-pack.ts +0 -85
  99. package/scripts/ruralgo-s1-evidence-pack-lib.ts +0 -200
@@ -1,403 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { dirname, relative, resolve } from 'node:path';
3
-
4
- export type TrackingDeclaration = {
5
- source_file: string;
6
- declared_path: string;
7
- resolved_path: string;
8
- };
9
-
10
- export type RepoTrackingState = {
11
- enforced: boolean;
12
- canonical_path: string | null;
13
- canonical_present: boolean;
14
- source_file: string | null;
15
- in_progress_count: number;
16
- single_in_progress_valid: boolean;
17
- conflict: boolean;
18
- declarations: ReadonlyArray<TrackingDeclaration>;
19
- in_progress_entries?: ReadonlyArray<{
20
- line_number: number;
21
- task_id: string | null;
22
- }>;
23
- active_task_id?: string | null;
24
- last_run_path?: string | null;
25
- last_run_status?: string | null;
26
- };
27
-
28
- export const formatTrackingActionableContext = (tracking: RepoTrackingState): string | null => {
29
- const activeEntries = (tracking.in_progress_entries ?? [])
30
- .map((entry) => `${entry.task_id ?? 'UNKNOWN'}@L${entry.line_number}`)
31
- .join(', ');
32
- if (!activeEntries) {
33
- return null;
34
- }
35
- const lastRunStatus = tracking.last_run_status ?? 'absent';
36
- return `active_entries=${activeEntries} last_run_status=${lastRunStatus}`;
37
- };
38
-
39
- type TrackingDeclarationSource = {
40
- sourceFile: string;
41
- priority: number;
42
- patterns: ReadonlyArray<RegExp>;
43
- };
44
-
45
- type TrackingCandidate = TrackingDeclaration & {
46
- priority: number;
47
- };
48
-
49
- type InProgressEntry = {
50
- lineNumber: number;
51
- taskId: string | null;
52
- };
53
-
54
- const TRACKING_DECLARATION_SOURCES: ReadonlyArray<TrackingDeclarationSource> = [
55
- {
56
- sourceFile: 'AGENTS.md',
57
- priority: 100,
58
- patterns: [
59
- /[uú]nica fuente viva del tracking interno es [`']([^`'\n]+)[`']/i,
60
- /ningun documento de seguimiento fuera de [`']([^`'\n]+)[`']/i,
61
- ],
62
- },
63
- {
64
- sourceFile: 'docs/README.md',
65
- priority: 80,
66
- patterns: [
67
- /todo el seguimiento operativo[\s\S]{0,240}?\[[^\]]+\]\(([^)]+)\)/i,
68
- /[uú]nica fuente de verdad[^`\n]*[`']([^`'\n]+)[`']/i,
69
- /fuente viva del tracking interno:\s*[`']([^`'\n]+)[`']/i,
70
- /quiero saber en qué estamos ahora:\s*[\s\S]{0,80}[-*]\s*[`']([^`'\n]+)[`']/i,
71
- ],
72
- },
73
- {
74
- sourceFile: 'docs/validation/README.md',
75
- priority: 60,
76
- patterns: [/[uú]nica fuente de seguimiento:\s*[`']([^`'\n]+)[`']/i],
77
- },
78
- ];
79
-
80
- const toRepoRelativePath = (repoRoot: string, candidatePath: string): string | null => {
81
- const repoRootAbsolute = resolve(repoRoot);
82
- const resolved = resolve(candidatePath);
83
- const rel = relative(repoRootAbsolute, resolved);
84
- if (
85
- rel === '..' ||
86
- rel.startsWith('../') ||
87
- rel.startsWith('..\\')
88
- ) {
89
- return null;
90
- }
91
- return rel.length > 0 ? rel.split('\\').join('/') : '.';
92
- };
93
-
94
- const normalizeDeclaredPath = (value: string): string => value.trim().replace(/[#?].*$/, '');
95
-
96
- const resolveDeclaredPath = (params: {
97
- repoRoot: string;
98
- sourceFile: string;
99
- declaredPath: string;
100
- }): string | null => {
101
- const sourceAbsolutePath = resolve(params.repoRoot, params.sourceFile);
102
- const candidate = normalizeDeclaredPath(params.declaredPath);
103
- if (candidate.length === 0) {
104
- return null;
105
- }
106
- const resolved = candidate.startsWith('./') || candidate.startsWith('../')
107
- ? resolve(dirname(sourceAbsolutePath), candidate)
108
- : resolve(params.repoRoot, candidate);
109
- return toRepoRelativePath(params.repoRoot, resolved);
110
- };
111
-
112
- const collectTrackingCandidates = (repoRoot: string): ReadonlyArray<TrackingCandidate> => {
113
- const candidates = new Map<string, TrackingCandidate>();
114
-
115
- for (const source of TRACKING_DECLARATION_SOURCES) {
116
- const sourcePath = resolve(repoRoot, source.sourceFile);
117
- if (!existsSync(sourcePath)) {
118
- continue;
119
- }
120
- const markdown = readFileSync(sourcePath, 'utf8');
121
- for (const pattern of source.patterns) {
122
- const match = pattern.exec(markdown);
123
- const declaredPath = match?.[1]?.trim();
124
- if (!declaredPath) {
125
- continue;
126
- }
127
- const resolvedPath = resolveDeclaredPath({
128
- repoRoot,
129
- sourceFile: source.sourceFile,
130
- declaredPath,
131
- });
132
- if (!resolvedPath) {
133
- continue;
134
- }
135
- const key = `${source.sourceFile}::${resolvedPath}`;
136
- if (candidates.has(key)) {
137
- continue;
138
- }
139
- candidates.set(key, {
140
- source_file: source.sourceFile,
141
- declared_path: declaredPath,
142
- resolved_path: resolvedPath,
143
- priority: source.priority,
144
- });
145
- }
146
- }
147
-
148
- return Array.from(candidates.values()).sort((left, right) => {
149
- if (right.priority !== left.priority) {
150
- return right.priority - left.priority;
151
- }
152
- const bySource = left.source_file.localeCompare(right.source_file);
153
- return bySource !== 0 ? bySource : left.resolved_path.localeCompare(right.resolved_path);
154
- });
155
- };
156
-
157
- const extractTaskId = (value: string): string | null => {
158
- const directMatch = value.match(/\b([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\b/);
159
- if (directMatch?.[1]) {
160
- return directMatch[1];
161
- }
162
- const dottedMatch = value.match(/\b([A-Z][A-Z0-9]*(?:[.-][A-Z0-9]+)+)\b/);
163
- return dottedMatch?.[1] ?? null;
164
- };
165
-
166
- const collectInProgressEntries = (markdown: string): ReadonlyArray<InProgressEntry> => {
167
- const lines = markdown.split(/\r?\n/);
168
- const entries: InProgressEntry[] = [];
169
-
170
- lines.forEach((line, index) => {
171
- const trimmed = line.trim();
172
- if (trimmed.length === 0) {
173
- return;
174
- }
175
-
176
- if (trimmed.startsWith('|')) {
177
- const cells = trimmed
178
- .split('|')
179
- .map((cell) => cell.trim())
180
- .filter((cell) => cell.length > 0);
181
- const firstCell = cells[0] ?? '';
182
- const secondCell = cells[1] ?? '';
183
- if (firstCell === '🚧') {
184
- entries.push({
185
- lineNumber: index + 1,
186
- taskId: extractTaskId(secondCell),
187
- });
188
- return;
189
- }
190
- const bracketCell = cells.find((cell) => /\[\s*🚧\s*\]\s*-/.test(cell));
191
- if (bracketCell) {
192
- entries.push({
193
- lineNumber: index + 1,
194
- taskId: extractTaskId(bracketCell),
195
- });
196
- return;
197
- }
198
- const emojiCell = cells.find((cell) => /^🚧(?:\b|\s|[*_`])/.test(cell));
199
- if (emojiCell) {
200
- const taskCell = cells.find((cell) => cell !== emojiCell && !/^[:\- ]+$/.test(cell)) ?? emojiCell;
201
- entries.push({
202
- lineNumber: index + 1,
203
- taskId: extractTaskId(taskCell),
204
- });
205
- return;
206
- }
207
- }
208
-
209
- if (/^- Estado:\s*🚧(?:\s|$)/.test(trimmed) || /\[\s*🚧\s*\]\s*-/.test(trimmed)) {
210
- entries.push({
211
- lineNumber: index + 1,
212
- taskId: extractTaskId(trimmed),
213
- });
214
- }
215
- });
216
-
217
- return entries;
218
- };
219
-
220
- const readActiveTaskHeader = (markdown: string): string | null => {
221
- const explicitHeader = markdown.match(/- Tarea activa principal:\s*`([^`]+)`/i)?.[1]?.trim();
222
- if (explicitHeader && explicitHeader.length > 0) {
223
- return extractTaskId(explicitHeader) ?? explicitHeader;
224
- }
225
- const bracketTask = markdown.match(/\[\s*🚧\s*\]\s*-\s*(.+)$/m)?.[1]?.trim();
226
- if (!bracketTask || bracketTask.length === 0) {
227
- return null;
228
- }
229
- return extractTaskId(bracketTask) ?? bracketTask;
230
- };
231
-
232
- const readLastRunAlignment = (params: {
233
- repoRoot: string;
234
- canonicalPath: string;
235
- activeTaskId: string | null;
236
- inProgressCount: number;
237
- activeTaskHeader: string | null;
238
- }): {
239
- path: string | null;
240
- status: string | null;
241
- valid: boolean;
242
- } => {
243
- const lastRunPath = resolve(params.repoRoot, 'docs', 'validation', 'refactor', 'last-run.json');
244
- if (!existsSync(lastRunPath)) {
245
- return {
246
- path: null,
247
- status: null,
248
- valid: params.inProgressCount === 1,
249
- };
250
- }
251
-
252
- let parsed: unknown;
253
- try {
254
- parsed = JSON.parse(readFileSync(lastRunPath, 'utf8')) as unknown;
255
- } catch {
256
- return {
257
- path: toRepoRelativePath(params.repoRoot, lastRunPath),
258
- status: null,
259
- valid: false,
260
- };
261
- }
262
-
263
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
264
- return {
265
- path: toRepoRelativePath(params.repoRoot, lastRunPath),
266
- status: null,
267
- valid: false,
268
- };
269
- }
270
-
271
- const payload = parsed as Record<string, unknown>;
272
- const status = typeof payload.status === 'string' ? payload.status.trim().toUpperCase() : null;
273
- const planFile = typeof payload.plan_file === 'string' ? payload.plan_file.trim() : null;
274
- const nextValue = typeof payload.next === 'string' ? payload.next : '';
275
- const lastRunRelativePath = toRepoRelativePath(params.repoRoot, lastRunPath);
276
- const planMatches = planFile === params.canonicalPath;
277
-
278
- if (status === 'IN_PROGRESS') {
279
- const headerMatches =
280
- !params.activeTaskHeader ||
281
- params.activeTaskHeader === params.activeTaskId ||
282
- (params.activeTaskHeader === 'NINGUNA' && params.activeTaskId === null);
283
- const nextMatches = !params.activeTaskId || nextValue.includes(params.activeTaskId);
284
- return {
285
- path: lastRunRelativePath,
286
- status,
287
- valid:
288
- planMatches &&
289
- params.inProgressCount === 1 &&
290
- headerMatches &&
291
- nextMatches,
292
- };
293
- }
294
-
295
- if (status === 'DONE') {
296
- return {
297
- path: lastRunRelativePath,
298
- status,
299
- valid:
300
- planMatches &&
301
- params.inProgressCount === 0 &&
302
- (params.activeTaskHeader === null || params.activeTaskHeader === 'NINGUNA'),
303
- };
304
- }
305
-
306
- return {
307
- path: lastRunRelativePath,
308
- status,
309
- valid: false,
310
- };
311
- };
312
-
313
- export const resolveRepoTrackingState = (repoRoot: string): RepoTrackingState => {
314
- const declarations = collectTrackingCandidates(repoRoot);
315
- const enforced = declarations.length > 0;
316
- if (!enforced) {
317
- return {
318
- enforced: false,
319
- canonical_path: null,
320
- canonical_present: false,
321
- source_file: null,
322
- in_progress_count: 0,
323
- single_in_progress_valid: false,
324
- conflict: false,
325
- declarations: [],
326
- in_progress_entries: [],
327
- active_task_id: null,
328
- last_run_path: null,
329
- last_run_status: null,
330
- };
331
- }
332
-
333
- const highestPriority = declarations[0]?.priority ?? 0;
334
- const highestPriorityDeclarations = declarations.filter(
335
- (declaration) => declaration.priority === highestPriority
336
- );
337
- const uniqueCanonicalPaths = Array.from(
338
- new Set(highestPriorityDeclarations.map((declaration) => declaration.resolved_path))
339
- );
340
- const allDeclaredPaths = Array.from(
341
- new Set(declarations.map((declaration) => declaration.resolved_path))
342
- );
343
- const canonicalPath = uniqueCanonicalPaths.length === 1 ? uniqueCanonicalPaths[0] : null;
344
- const canonicalPresent =
345
- typeof canonicalPath === 'string' && canonicalPath.length > 0
346
- ? existsSync(resolve(repoRoot, canonicalPath))
347
- : false;
348
- const sourceFile =
349
- canonicalPath === null
350
- ? null
351
- : highestPriorityDeclarations.find(
352
- (declaration) => declaration.resolved_path === canonicalPath
353
- )?.source_file ?? null;
354
-
355
- if (!canonicalPath || !canonicalPresent) {
356
- return {
357
- enforced: true,
358
- canonical_path: canonicalPath,
359
- canonical_present: canonicalPresent,
360
- source_file: sourceFile,
361
- in_progress_count: 0,
362
- single_in_progress_valid: false,
363
- conflict: allDeclaredPaths.length > 1,
364
- declarations: declarations.map(({ priority: _priority, ...declaration }) => declaration),
365
- in_progress_entries: [],
366
- active_task_id: null,
367
- last_run_path: null,
368
- last_run_status: null,
369
- };
370
- }
371
-
372
- const markdown = readFileSync(resolve(repoRoot, canonicalPath), 'utf8');
373
- const inProgressEntries = collectInProgressEntries(markdown);
374
- const inProgressCount = inProgressEntries.length;
375
- const activeTaskId = inProgressEntries[0]?.taskId ?? null;
376
- const activeTaskHeader = readActiveTaskHeader(markdown);
377
- const lastRunAlignment = readLastRunAlignment({
378
- repoRoot,
379
- canonicalPath,
380
- activeTaskId,
381
- inProgressCount,
382
- activeTaskHeader,
383
- });
384
-
385
- return {
386
- enforced: true,
387
- canonical_path: canonicalPath,
388
- canonical_present: true,
389
- source_file: sourceFile,
390
- in_progress_count: inProgressCount,
391
- single_in_progress_valid:
392
- uniqueCanonicalPaths.length === 1 && lastRunAlignment.valid,
393
- conflict: allDeclaredPaths.length > 1,
394
- declarations: declarations.map(({ priority: _priority, ...declaration }) => declaration),
395
- in_progress_entries: inProgressEntries.map((entry) => ({
396
- line_number: entry.lineNumber,
397
- task_id: entry.taskId,
398
- })),
399
- active_task_id: activeTaskId,
400
- last_run_path: lastRunAlignment.path,
401
- last_run_status: lastRunAlignment.status,
402
- };
403
- };
@@ -1,232 +0,0 @@
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,7 +0,0 @@
1
- export const readMcpPrePushStdin = (): string => {
2
- const envInput = process.env.PUMUKI_PRE_PUSH_STDIN;
3
- if (typeof envInput === 'string' && envInput.trim().length > 0) {
4
- return envInput;
5
- }
6
- return '';
7
- };