pumuki 6.3.44 → 6.3.46
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/RELEASE_NOTES.md +31 -0
- package/docs/USAGE.md +19 -0
- package/docs/seguimiento-activo-pumuki-saas-supermercados.md +287 -5
- package/integrations/git/runPlatformGate.ts +30 -19
- package/integrations/git/stageRunners.ts +128 -30
- package/integrations/lifecycle/watch.ts +26 -0
- package/integrations/mcp/aiGateCheck.ts +16 -2
- package/integrations/mcp/autoExecuteAiStart.ts +12 -2
- package/integrations/mcp/preFlightCheck.ts +16 -0
- package/integrations/sdd/learningInsights.ts +91 -0
- package/integrations/sdd/syncDocs.ts +317 -28
- package/package.json +5 -2
- package/scripts/backlog-id-issue-map-lib.ts +1 -1
- package/scripts/reconcile-consumer-backlog-issues-lib.ts +2 -2
- package/scripts/watch-consumer-backlog-fleet-tick.ts +225 -0
- package/scripts/watch-consumer-backlog-fleet.ts +200 -0
- package/scripts/watch-consumer-backlog-lib.ts +2 -2
|
@@ -22,6 +22,7 @@ import { readFileSync } from 'node:fs';
|
|
|
22
22
|
import { readEvidence, readEvidenceResult } from '../evidence/readEvidence';
|
|
23
23
|
import type { EvidenceReadResult } from '../evidence/readEvidence';
|
|
24
24
|
import { ensureRuntimeArtifactsIgnored } from '../lifecycle/artifacts';
|
|
25
|
+
import { runPolicyReconcile } from '../lifecycle/policyReconcile';
|
|
25
26
|
|
|
26
27
|
const PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE =
|
|
27
28
|
'pumuki pre-push blocked: branch has no upstream tracking reference. Configure upstream first (for example: git push --set-upstream origin <branch>) and retry.';
|
|
@@ -54,6 +55,14 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
54
55
|
'Alinea upstream con la rama actual: git branch --unset-upstream && git push --set-upstream origin <branch>',
|
|
55
56
|
};
|
|
56
57
|
|
|
58
|
+
const HOOK_POLICY_RECONCILE_CODES = new Set<string>([
|
|
59
|
+
'SKILLS_PLATFORM_COVERAGE_INCOMPLETE_HIGH',
|
|
60
|
+
'SKILLS_SCOPE_COMPLIANCE_INCOMPLETE_HIGH',
|
|
61
|
+
'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
|
|
62
|
+
'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
|
|
63
|
+
'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
|
|
64
|
+
]);
|
|
65
|
+
|
|
57
66
|
type StageRunnerDependencies = {
|
|
58
67
|
resolvePolicyForStage: typeof resolvePolicyForStage;
|
|
59
68
|
resolveUpstreamRef: typeof resolveUpstreamRef;
|
|
@@ -89,6 +98,7 @@ type StageRunnerDependencies = {
|
|
|
89
98
|
writeHookGateSummary: (message: string) => void;
|
|
90
99
|
isQuietMode: () => boolean;
|
|
91
100
|
ensureRuntimeArtifactsIgnored: (repoRoot: string) => void;
|
|
101
|
+
runPolicyReconcile: typeof runPolicyReconcile;
|
|
92
102
|
};
|
|
93
103
|
|
|
94
104
|
const defaultDependencies: StageRunnerDependencies = {
|
|
@@ -144,6 +154,7 @@ const defaultDependencies: StageRunnerDependencies = {
|
|
|
144
154
|
} catch {
|
|
145
155
|
}
|
|
146
156
|
},
|
|
157
|
+
runPolicyReconcile,
|
|
147
158
|
};
|
|
148
159
|
|
|
149
160
|
const getDependencies = (
|
|
@@ -196,6 +207,93 @@ const notifyGateBlockedForStage = (params: {
|
|
|
196
207
|
});
|
|
197
208
|
};
|
|
198
209
|
|
|
210
|
+
const isHookPolicyAutoReconcileEnabled = (): boolean =>
|
|
211
|
+
process.env.PUMUKI_HOOK_POLICY_AUTO_RECONCILE !== '0';
|
|
212
|
+
|
|
213
|
+
const shouldRetryAfterPolicyReconcile = (params: {
|
|
214
|
+
dependencies: StageRunnerDependencies;
|
|
215
|
+
repoRoot: string;
|
|
216
|
+
stage: 'PRE_COMMIT' | 'PRE_PUSH';
|
|
217
|
+
}): boolean => {
|
|
218
|
+
const evidence = params.dependencies.readEvidence(params.repoRoot);
|
|
219
|
+
if (!evidence) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const stageCodes = new Set<string>();
|
|
223
|
+
if (evidence.snapshot.stage === params.stage) {
|
|
224
|
+
for (const finding of evidence.snapshot.findings) {
|
|
225
|
+
stageCodes.add(finding.code);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const violation of evidence.ai_gate.violations) {
|
|
229
|
+
stageCodes.add(violation.code);
|
|
230
|
+
}
|
|
231
|
+
for (const code of stageCodes) {
|
|
232
|
+
if (HOOK_POLICY_RECONCILE_CODES.has(code)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
type HookStage = 'PRE_COMMIT' | 'PRE_PUSH';
|
|
240
|
+
type HookPolicyTrace = NonNullable<ReturnType<typeof resolvePolicyForStage>['trace']>;
|
|
241
|
+
|
|
242
|
+
const runHookGateAttempt = async (params: {
|
|
243
|
+
dependencies: StageRunnerDependencies;
|
|
244
|
+
stage: HookStage;
|
|
245
|
+
scope: Parameters<typeof runPlatformGate>[0]['scope'];
|
|
246
|
+
}): Promise<{ exitCode: number; policyTrace: HookPolicyTrace }> => {
|
|
247
|
+
const resolved = params.dependencies.resolvePolicyForStage(params.stage);
|
|
248
|
+
const exitCode = await params.dependencies.runPlatformGate({
|
|
249
|
+
policy: resolved.policy,
|
|
250
|
+
policyTrace: resolved.trace,
|
|
251
|
+
scope: params.scope,
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
exitCode,
|
|
255
|
+
policyTrace: resolved.trace,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const runHookGateWithPolicyRetry = async (params: {
|
|
260
|
+
dependencies: StageRunnerDependencies;
|
|
261
|
+
repoRoot: string;
|
|
262
|
+
stage: HookStage;
|
|
263
|
+
scope: Parameters<typeof runPlatformGate>[0]['scope'];
|
|
264
|
+
}): Promise<{ exitCode: number; policyTrace: HookPolicyTrace }> => {
|
|
265
|
+
const firstAttempt = await runHookGateAttempt({
|
|
266
|
+
dependencies: params.dependencies,
|
|
267
|
+
stage: params.stage,
|
|
268
|
+
scope: params.scope,
|
|
269
|
+
});
|
|
270
|
+
if (firstAttempt.exitCode === 0) {
|
|
271
|
+
return firstAttempt;
|
|
272
|
+
}
|
|
273
|
+
if (!isHookPolicyAutoReconcileEnabled()) {
|
|
274
|
+
return firstAttempt;
|
|
275
|
+
}
|
|
276
|
+
if (
|
|
277
|
+
!shouldRetryAfterPolicyReconcile({
|
|
278
|
+
dependencies: params.dependencies,
|
|
279
|
+
repoRoot: params.repoRoot,
|
|
280
|
+
stage: params.stage,
|
|
281
|
+
})
|
|
282
|
+
) {
|
|
283
|
+
return firstAttempt;
|
|
284
|
+
}
|
|
285
|
+
params.dependencies.runPolicyReconcile({
|
|
286
|
+
repoRoot: params.repoRoot,
|
|
287
|
+
strict: true,
|
|
288
|
+
apply: true,
|
|
289
|
+
});
|
|
290
|
+
return runHookGateAttempt({
|
|
291
|
+
dependencies: params.dependencies,
|
|
292
|
+
stage: params.stage,
|
|
293
|
+
scope: params.scope,
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
|
|
199
297
|
const ZERO_HASH = /^0+$/;
|
|
200
298
|
|
|
201
299
|
const toEvidenceAgeSeconds = (
|
|
@@ -367,10 +465,10 @@ export async function runPreCommitStage(
|
|
|
367
465
|
) {
|
|
368
466
|
return 1;
|
|
369
467
|
}
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
468
|
+
const result = await runHookGateWithPolicyRetry({
|
|
469
|
+
dependencies: activeDependencies,
|
|
470
|
+
repoRoot,
|
|
471
|
+
stage: 'PRE_COMMIT',
|
|
374
472
|
scope: {
|
|
375
473
|
kind: 'staged',
|
|
376
474
|
},
|
|
@@ -378,10 +476,10 @@ export async function runPreCommitStage(
|
|
|
378
476
|
emitSuccessfulHookGateSummary({
|
|
379
477
|
dependencies: activeDependencies,
|
|
380
478
|
stage: 'PRE_COMMIT',
|
|
381
|
-
policyTrace:
|
|
382
|
-
exitCode,
|
|
479
|
+
policyTrace: result.policyTrace,
|
|
480
|
+
exitCode: result.exitCode,
|
|
383
481
|
});
|
|
384
|
-
if (exitCode !== 0) {
|
|
482
|
+
if (result.exitCode !== 0) {
|
|
385
483
|
notifyGateBlockedForStage({
|
|
386
484
|
dependencies: activeDependencies,
|
|
387
485
|
stage: 'PRE_COMMIT',
|
|
@@ -391,7 +489,7 @@ export async function runPreCommitStage(
|
|
|
391
489
|
});
|
|
392
490
|
}
|
|
393
491
|
notifyAuditSummaryForStage(activeDependencies, 'PRE_COMMIT');
|
|
394
|
-
return exitCode;
|
|
492
|
+
return result.exitCode;
|
|
395
493
|
}
|
|
396
494
|
|
|
397
495
|
export async function runPrePushStage(
|
|
@@ -427,10 +525,10 @@ export async function runPrePushStage(
|
|
|
427
525
|
) {
|
|
428
526
|
return 1;
|
|
429
527
|
}
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
528
|
+
const result = await runHookGateWithPolicyRetry({
|
|
529
|
+
dependencies: activeDependencies,
|
|
530
|
+
repoRoot,
|
|
531
|
+
stage: 'PRE_PUSH',
|
|
434
532
|
scope: {
|
|
435
533
|
kind: 'range',
|
|
436
534
|
fromRef: bootstrapBaseRef,
|
|
@@ -440,18 +538,18 @@ export async function runPrePushStage(
|
|
|
440
538
|
emitSuccessfulHookGateSummary({
|
|
441
539
|
dependencies: activeDependencies,
|
|
442
540
|
stage: 'PRE_PUSH',
|
|
443
|
-
policyTrace:
|
|
444
|
-
exitCode,
|
|
541
|
+
policyTrace: result.policyTrace,
|
|
542
|
+
exitCode: result.exitCode,
|
|
445
543
|
});
|
|
446
544
|
notifyAuditSummaryForStage(activeDependencies, 'PRE_PUSH');
|
|
447
|
-
return exitCode;
|
|
545
|
+
return result.exitCode;
|
|
448
546
|
}
|
|
449
547
|
if (manualInvocationFallback) {
|
|
450
548
|
process.stderr.write(`${PRE_PUSH_MANUAL_FALLBACK_MESSAGE}\n`);
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
549
|
+
const result = await runHookGateWithPolicyRetry({
|
|
550
|
+
dependencies: activeDependencies,
|
|
551
|
+
repoRoot,
|
|
552
|
+
stage: 'PRE_PUSH',
|
|
455
553
|
scope: {
|
|
456
554
|
kind: 'workingTree',
|
|
457
555
|
},
|
|
@@ -459,11 +557,11 @@ export async function runPrePushStage(
|
|
|
459
557
|
emitSuccessfulHookGateSummary({
|
|
460
558
|
dependencies: activeDependencies,
|
|
461
559
|
stage: 'PRE_PUSH',
|
|
462
|
-
policyTrace:
|
|
463
|
-
exitCode,
|
|
560
|
+
policyTrace: result.policyTrace,
|
|
561
|
+
exitCode: result.exitCode,
|
|
464
562
|
});
|
|
465
563
|
notifyAuditSummaryForStage(activeDependencies, 'PRE_PUSH');
|
|
466
|
-
return exitCode;
|
|
564
|
+
return result.exitCode;
|
|
467
565
|
}
|
|
468
566
|
process.stderr.write(`${PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE}\n`);
|
|
469
567
|
notifyGateBlockedForStage({
|
|
@@ -506,10 +604,10 @@ export async function runPrePushStage(
|
|
|
506
604
|
return 1;
|
|
507
605
|
}
|
|
508
606
|
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
607
|
+
const result = await runHookGateWithPolicyRetry({
|
|
608
|
+
dependencies: activeDependencies,
|
|
609
|
+
repoRoot,
|
|
610
|
+
stage: 'PRE_PUSH',
|
|
513
611
|
scope: {
|
|
514
612
|
kind: 'range',
|
|
515
613
|
fromRef: upstreamRef,
|
|
@@ -519,10 +617,10 @@ export async function runPrePushStage(
|
|
|
519
617
|
emitSuccessfulHookGateSummary({
|
|
520
618
|
dependencies: activeDependencies,
|
|
521
619
|
stage: 'PRE_PUSH',
|
|
522
|
-
policyTrace:
|
|
523
|
-
exitCode,
|
|
620
|
+
policyTrace: result.policyTrace,
|
|
621
|
+
exitCode: result.exitCode,
|
|
524
622
|
});
|
|
525
|
-
if (exitCode !== 0) {
|
|
623
|
+
if (result.exitCode !== 0) {
|
|
526
624
|
notifyGateBlockedForStage({
|
|
527
625
|
dependencies: activeDependencies,
|
|
528
626
|
stage: 'PRE_PUSH',
|
|
@@ -532,7 +630,7 @@ export async function runPrePushStage(
|
|
|
532
630
|
});
|
|
533
631
|
}
|
|
534
632
|
notifyAuditSummaryForStage(activeDependencies, 'PRE_PUSH');
|
|
535
|
-
return exitCode;
|
|
633
|
+
return result.exitCode;
|
|
536
634
|
}
|
|
537
635
|
|
|
538
636
|
export async function runCiStage(
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
emitGateBlockedNotification,
|
|
16
16
|
} from '../notifications/emitAuditSummaryNotification';
|
|
17
17
|
import { runPolicyReconcile } from './policyReconcile';
|
|
18
|
+
import { resolvePumukiVersionMetadata, type PumukiVersionMetadata } from './packageInfo';
|
|
18
19
|
|
|
19
20
|
export type LifecycleWatchStage = 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
20
21
|
export type LifecycleWatchScope = 'workingTree' | 'staged' | 'repoAndStaged' | 'repo';
|
|
@@ -49,6 +50,14 @@ export type LifecycleWatchTickResult = {
|
|
|
49
50
|
export type LifecycleWatchResult = {
|
|
50
51
|
command: 'pumuki watch';
|
|
51
52
|
repoRoot: string;
|
|
53
|
+
version: {
|
|
54
|
+
effective: string;
|
|
55
|
+
runtime: string;
|
|
56
|
+
consumerInstalled: string | null;
|
|
57
|
+
source: PumukiVersionMetadata['source'];
|
|
58
|
+
driftFromRuntime: boolean;
|
|
59
|
+
driftWarning: string | null;
|
|
60
|
+
};
|
|
52
61
|
stage: LifecycleWatchStage;
|
|
53
62
|
scope: LifecycleWatchScope;
|
|
54
63
|
intervalMs: number;
|
|
@@ -73,6 +82,7 @@ type LifecycleWatchDependencies = {
|
|
|
73
82
|
emitAuditSummaryNotificationFromEvidence: typeof emitAuditSummaryNotificationFromEvidence;
|
|
74
83
|
emitGateBlockedNotification: typeof emitGateBlockedNotification;
|
|
75
84
|
runPolicyReconcile: typeof runPolicyReconcile;
|
|
85
|
+
resolvePumukiVersionMetadata: (params?: { repoRoot?: string }) => PumukiVersionMetadata;
|
|
76
86
|
nowMs: () => number;
|
|
77
87
|
sleep: (ms: number) => Promise<void>;
|
|
78
88
|
};
|
|
@@ -96,6 +106,7 @@ const defaultDependencies: LifecycleWatchDependencies = {
|
|
|
96
106
|
emitAuditSummaryNotificationFromEvidence,
|
|
97
107
|
emitGateBlockedNotification,
|
|
98
108
|
runPolicyReconcile,
|
|
109
|
+
resolvePumukiVersionMetadata,
|
|
99
110
|
nowMs: () => Date.now(),
|
|
100
111
|
sleep: async (ms) => {
|
|
101
112
|
await sleepTimer(ms);
|
|
@@ -231,6 +242,12 @@ export const runLifecycleWatch = async (
|
|
|
231
242
|
...dependencies,
|
|
232
243
|
};
|
|
233
244
|
const repoRoot = params?.repoRoot ?? activeDependencies.resolveRepoRoot();
|
|
245
|
+
const versionMetadata = activeDependencies.resolvePumukiVersionMetadata({ repoRoot });
|
|
246
|
+
const driftFromRuntime = versionMetadata.resolvedVersion !== versionMetadata.runtimeVersion;
|
|
247
|
+
const driftWarning = driftFromRuntime
|
|
248
|
+
? `Version drift detectado: effective=${versionMetadata.resolvedVersion} runtime=${versionMetadata.runtimeVersion}. ` +
|
|
249
|
+
'Actualiza el consumer para alinear el binario local con @latest y evitar diagnósticos inconsistentes.'
|
|
250
|
+
: null;
|
|
234
251
|
const stage = params?.stage ?? 'PRE_COMMIT';
|
|
235
252
|
const scope = params?.scope ?? 'workingTree';
|
|
236
253
|
const intervalMs = Math.max(250, Math.trunc(params?.intervalMs ?? 3000));
|
|
@@ -309,6 +326,7 @@ export const runLifecycleWatch = async (
|
|
|
309
326
|
policy: resolvedPolicy.policy,
|
|
310
327
|
policyTrace: resolvedPolicy.trace,
|
|
311
328
|
scope: gateScope,
|
|
329
|
+
silent: true,
|
|
312
330
|
});
|
|
313
331
|
const evidence = activeDependencies.readEvidence(repoRoot);
|
|
314
332
|
const allFindings = evidence?.snapshot.findings ?? [];
|
|
@@ -449,6 +467,14 @@ export const runLifecycleWatch = async (
|
|
|
449
467
|
return {
|
|
450
468
|
command: 'pumuki watch',
|
|
451
469
|
repoRoot,
|
|
470
|
+
version: {
|
|
471
|
+
effective: versionMetadata.resolvedVersion,
|
|
472
|
+
runtime: versionMetadata.runtimeVersion,
|
|
473
|
+
consumerInstalled: versionMetadata.consumerInstalledVersion,
|
|
474
|
+
source: versionMetadata.source,
|
|
475
|
+
driftFromRuntime,
|
|
476
|
+
driftWarning,
|
|
477
|
+
},
|
|
452
478
|
stage,
|
|
453
479
|
scope,
|
|
454
480
|
intervalMs,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { evaluateAiGate, type AiGateStage } from '../gate/evaluateAiGate';
|
|
2
|
+
import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
|
|
2
3
|
|
|
3
4
|
const AUTO_FIX_BY_CODE: Readonly<Record<string, string>> = {
|
|
4
5
|
EVIDENCE_MISSING: 'Ejecuta una auditoría para generar .ai_evidence.json.',
|
|
@@ -28,6 +29,7 @@ export type EnterpriseAiGateCheckResult = {
|
|
|
28
29
|
violations: ReturnType<typeof evaluateAiGate>['violations'];
|
|
29
30
|
warnings: ReadonlyArray<string>;
|
|
30
31
|
auto_fixes: ReadonlyArray<string>;
|
|
32
|
+
learning_context: SddLearningContext | null;
|
|
31
33
|
evidence: ReturnType<typeof evaluateAiGate>['evidence'];
|
|
32
34
|
mcp_receipt: ReturnType<typeof evaluateAiGate>['mcp_receipt'];
|
|
33
35
|
skills_contract: ReturnType<typeof evaluateAiGate>['skills_contract'];
|
|
@@ -99,7 +101,10 @@ const buildWarnings = (evaluation: ReturnType<typeof evaluateAiGate>): ReadonlyA
|
|
|
99
101
|
return warnings;
|
|
100
102
|
};
|
|
101
103
|
|
|
102
|
-
const buildAutoFixes = (
|
|
104
|
+
const buildAutoFixes = (
|
|
105
|
+
evaluation: ReturnType<typeof evaluateAiGate>,
|
|
106
|
+
learningContext: SddLearningContext | null
|
|
107
|
+
): ReadonlyArray<string> => {
|
|
103
108
|
const fixes: string[] = [];
|
|
104
109
|
const emittedCodes = new Set<string>();
|
|
105
110
|
for (const violation of evaluation.violations) {
|
|
@@ -113,6 +118,11 @@ const buildAutoFixes = (evaluation: ReturnType<typeof evaluateAiGate>): Readonly
|
|
|
113
118
|
fixes.push(fix);
|
|
114
119
|
emittedCodes.add(violation.code);
|
|
115
120
|
}
|
|
121
|
+
for (const recommendation of learningContext?.recommended_actions ?? []) {
|
|
122
|
+
if (!fixes.includes(recommendation)) {
|
|
123
|
+
fixes.push(recommendation);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
116
126
|
return fixes;
|
|
117
127
|
};
|
|
118
128
|
|
|
@@ -143,8 +153,11 @@ export const runEnterpriseAiGateCheck = (params: {
|
|
|
143
153
|
});
|
|
144
154
|
const branch = evaluation.repo_state.git.branch;
|
|
145
155
|
const timestamp = evaluation.evidence.source.generated_at;
|
|
156
|
+
const learningContext = readSddLearningContext({
|
|
157
|
+
repoRoot: params.repoRoot,
|
|
158
|
+
});
|
|
146
159
|
const warnings = buildWarnings(evaluation);
|
|
147
|
-
const autoFixes = buildAutoFixes(evaluation);
|
|
160
|
+
const autoFixes = buildAutoFixes(evaluation, learningContext);
|
|
148
161
|
const message = buildMessage(evaluation);
|
|
149
162
|
|
|
150
163
|
return {
|
|
@@ -163,6 +176,7 @@ export const runEnterpriseAiGateCheck = (params: {
|
|
|
163
176
|
violations: evaluation.violations,
|
|
164
177
|
warnings,
|
|
165
178
|
auto_fixes: autoFixes,
|
|
179
|
+
learning_context: learningContext,
|
|
166
180
|
evidence: evaluation.evidence,
|
|
167
181
|
mcp_receipt: evaluation.mcp_receipt,
|
|
168
182
|
skills_contract: evaluation.skills_contract,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
|
|
2
2
|
import { collectWorktreeAtomicSlices } from '../git/worktreeAtomicSlices';
|
|
3
|
+
import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
|
|
3
4
|
|
|
4
5
|
type AutoExecuteAction = 'proceed' | 'ask';
|
|
5
6
|
type AutoExecutePhase = 'GREEN' | 'RED';
|
|
@@ -154,6 +155,7 @@ export type EnterpriseAutoExecuteAiStartResult = {
|
|
|
154
155
|
confidence_pct: number;
|
|
155
156
|
reason_code: string;
|
|
156
157
|
next_action: AutoExecuteNextAction;
|
|
158
|
+
learning_context: SddLearningContext | null;
|
|
157
159
|
gate: {
|
|
158
160
|
stage: ReturnType<typeof evaluateAiGate>['stage'];
|
|
159
161
|
status: ReturnType<typeof evaluateAiGate>['status'];
|
|
@@ -174,6 +176,9 @@ export const runEnterpriseAutoExecuteAiStart = (params: {
|
|
|
174
176
|
stage,
|
|
175
177
|
requireMcpReceipt: params.requireMcpReceipt ?? false,
|
|
176
178
|
});
|
|
179
|
+
const learningContext = readSddLearningContext({
|
|
180
|
+
repoRoot: params.repoRoot,
|
|
181
|
+
});
|
|
177
182
|
const firstViolation = evaluation.violations[0];
|
|
178
183
|
const reasonCode = firstViolation?.code ?? 'READY';
|
|
179
184
|
const action: AutoExecuteAction = evaluation.allowed ? 'proceed' : 'ask';
|
|
@@ -186,12 +191,16 @@ export const runEnterpriseAutoExecuteAiStart = (params: {
|
|
|
186
191
|
}
|
|
187
192
|
: nextActionFromViolation(firstViolation, params.repoRoot);
|
|
188
193
|
|
|
189
|
-
|
|
194
|
+
let message = toHumanMessage({
|
|
190
195
|
action,
|
|
191
196
|
confidencePct,
|
|
192
197
|
reasonCode,
|
|
193
198
|
});
|
|
194
|
-
|
|
199
|
+
let instruction = nextAction.message;
|
|
200
|
+
if (learningContext?.recommended_actions[0]) {
|
|
201
|
+
message = `${message} Learning: ${learningContext.recommended_actions[0]}`;
|
|
202
|
+
instruction = `${instruction} Learning: ${learningContext.recommended_actions[0]}`;
|
|
203
|
+
}
|
|
195
204
|
const force = action === 'ask' && confidencePct < 50;
|
|
196
205
|
|
|
197
206
|
return {
|
|
@@ -210,6 +219,7 @@ export const runEnterpriseAutoExecuteAiStart = (params: {
|
|
|
210
219
|
confidence_pct: confidencePct,
|
|
211
220
|
reason_code: reasonCode,
|
|
212
221
|
next_action: nextAction,
|
|
222
|
+
learning_context: learningContext,
|
|
213
223
|
gate: {
|
|
214
224
|
stage: evaluation.stage,
|
|
215
225
|
status: evaluation.status,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
|
|
2
2
|
import { collectWorktreeAtomicSlices } from '../git/worktreeAtomicSlices';
|
|
3
|
+
import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
|
|
3
4
|
|
|
4
5
|
const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
|
|
5
6
|
EVIDENCE_MISSING: 'Ejecuta una auditoría (1/2/3/4) para regenerar .ai_evidence.json.',
|
|
@@ -44,6 +45,7 @@ const buildPreFlightHints = (params: {
|
|
|
44
45
|
status: ReturnType<typeof evaluateAiGate>['status'];
|
|
45
46
|
violations: ReadonlyArray<AiGateViolation>;
|
|
46
47
|
upstream: string | null;
|
|
48
|
+
learningContext: SddLearningContext | null;
|
|
47
49
|
}): ReadonlyArray<string> => {
|
|
48
50
|
const hints: string[] = [];
|
|
49
51
|
const emittedCodes = new Set<string>();
|
|
@@ -89,6 +91,14 @@ const buildPreFlightHints = (params: {
|
|
|
89
91
|
hints.push('Corrige la causa bloqueante y vuelve a ejecutar el pre-flight.');
|
|
90
92
|
}
|
|
91
93
|
}
|
|
94
|
+
if (params.learningContext) {
|
|
95
|
+
hints.push(
|
|
96
|
+
`LEARNING_CONTEXT: change=${params.learningContext.change} file=${params.learningContext.path}`
|
|
97
|
+
);
|
|
98
|
+
if (params.learningContext.recommended_actions[0]) {
|
|
99
|
+
hints.push(`LEARNING_NEXT_ACTION: ${params.learningContext.recommended_actions[0]}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
92
102
|
return hints;
|
|
93
103
|
};
|
|
94
104
|
|
|
@@ -111,6 +121,7 @@ export type EnterprisePreFlightCheckResult = {
|
|
|
111
121
|
skills_contract: ReturnType<typeof evaluateAiGate>['skills_contract'];
|
|
112
122
|
repo_state: ReturnType<typeof evaluateAiGate>['repo_state'];
|
|
113
123
|
hints: ReadonlyArray<string>;
|
|
124
|
+
learning_context: SddLearningContext | null;
|
|
114
125
|
ast_analysis: null;
|
|
115
126
|
tdd_status: null;
|
|
116
127
|
};
|
|
@@ -126,6 +137,9 @@ export const runEnterprisePreFlightCheck = (params: {
|
|
|
126
137
|
stage: params.stage,
|
|
127
138
|
requireMcpReceipt: params.requireMcpReceipt ?? false,
|
|
128
139
|
});
|
|
140
|
+
const learningContext = readSddLearningContext({
|
|
141
|
+
repoRoot: params.repoRoot,
|
|
142
|
+
});
|
|
129
143
|
|
|
130
144
|
const hints = buildPreFlightHints({
|
|
131
145
|
repoRoot: params.repoRoot,
|
|
@@ -133,6 +147,7 @@ export const runEnterprisePreFlightCheck = (params: {
|
|
|
133
147
|
status: evaluation.status,
|
|
134
148
|
violations: evaluation.violations,
|
|
135
149
|
upstream: evaluation.repo_state.git.upstream,
|
|
150
|
+
learningContext,
|
|
136
151
|
});
|
|
137
152
|
const phase: 'GREEN' | 'RED' = evaluation.allowed ? 'GREEN' : 'RED';
|
|
138
153
|
const message = evaluation.allowed
|
|
@@ -161,6 +176,7 @@ export const runEnterprisePreFlightCheck = (params: {
|
|
|
161
176
|
skills_contract: evaluation.skills_contract,
|
|
162
177
|
repo_state: evaluation.repo_state,
|
|
163
178
|
hints,
|
|
179
|
+
learning_context: learningContext,
|
|
164
180
|
ast_analysis: null,
|
|
165
181
|
tdd_status: null,
|
|
166
182
|
},
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { readSddSession } from './sessionStore';
|
|
4
|
+
|
|
5
|
+
type LearningArtifact = {
|
|
6
|
+
generated_at?: unknown;
|
|
7
|
+
failed_patterns?: unknown;
|
|
8
|
+
successful_patterns?: unknown;
|
|
9
|
+
rule_updates?: unknown;
|
|
10
|
+
gate_anomalies?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type SddLearningContext = {
|
|
14
|
+
change: string;
|
|
15
|
+
path: string;
|
|
16
|
+
generated_at: string | null;
|
|
17
|
+
failed_patterns: string[];
|
|
18
|
+
successful_patterns: string[];
|
|
19
|
+
rule_updates: string[];
|
|
20
|
+
gate_anomalies: string[];
|
|
21
|
+
recommended_actions: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const toStringArray = (value: unknown): string[] => {
|
|
25
|
+
if (!Array.isArray(value)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return value.filter((item): item is string => typeof item === 'string');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const toRecommendedActions = (ruleUpdates: string[]): string[] => {
|
|
32
|
+
const actions: string[] = [];
|
|
33
|
+
for (const rule of ruleUpdates) {
|
|
34
|
+
if (rule === 'evidence.bootstrap.required' || rule === 'evidence.rebuild.required') {
|
|
35
|
+
actions.push(
|
|
36
|
+
'Regenera evidencia y vuelve a validar PRE_WRITE para estabilizar el gate.'
|
|
37
|
+
);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (rule.startsWith('ai-gate.violation.')) {
|
|
41
|
+
actions.push('Corrige la violación de gate indicada y reejecuta validate/hook.');
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (rule.startsWith('sdd.')) {
|
|
45
|
+
actions.push('Completa el contrato SDD del cambio activo antes de cerrar stage.');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [...new Set(actions)];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const readSddLearningContext = (params: {
|
|
53
|
+
repoRoot: string;
|
|
54
|
+
change?: string | null;
|
|
55
|
+
}): SddLearningContext | null => {
|
|
56
|
+
const repoRoot = resolve(params.repoRoot);
|
|
57
|
+
const explicitChange = params.change?.trim().toLowerCase() ?? null;
|
|
58
|
+
const session = readSddSession(repoRoot);
|
|
59
|
+
const change = explicitChange ?? session.changeId ?? null;
|
|
60
|
+
if (!change) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const relativePath = `openspec/changes/${change}/learning.json`;
|
|
65
|
+
const absolutePath = resolve(repoRoot, relativePath);
|
|
66
|
+
if (!existsSync(absolutePath)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let parsed: LearningArtifact;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(readFileSync(absolutePath, 'utf8')) as LearningArtifact;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ruleUpdates = toStringArray(parsed.rule_updates);
|
|
78
|
+
return {
|
|
79
|
+
change,
|
|
80
|
+
path: relativePath,
|
|
81
|
+
generated_at:
|
|
82
|
+
typeof parsed.generated_at === 'string' && parsed.generated_at.trim().length > 0
|
|
83
|
+
? parsed.generated_at
|
|
84
|
+
: null,
|
|
85
|
+
failed_patterns: toStringArray(parsed.failed_patterns),
|
|
86
|
+
successful_patterns: toStringArray(parsed.successful_patterns),
|
|
87
|
+
rule_updates: ruleUpdates,
|
|
88
|
+
gate_anomalies: toStringArray(parsed.gate_anomalies),
|
|
89
|
+
recommended_actions: toRecommendedActions(ruleUpdates),
|
|
90
|
+
};
|
|
91
|
+
};
|