pumuki 6.3.44 → 6.3.45

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.
@@ -5,6 +5,24 @@ Detailed commit history remains available through Git history (`git log` / `git
5
5
 
6
6
  ## 2026-03 (enterprise hardening updates)
7
7
 
8
+ ### 2026-03-05 (v6.3.45)
9
+
10
+ - SDD sync canónico ampliado por defecto en consumer:
11
+ - `pumuki sdd sync-docs` ahora sincroniza, cuando existen, los 3 documentos base:
12
+ - `docs/strategy/ruralgo-tracking-hub.md`
13
+ - `docs/technical/08-validation/refactor/operational-summary.md`
14
+ - `docs/validation/refactor/last-run.json`
15
+ - Auto-sync OpenSpec integral por cambio:
16
+ - `pumuki sdd auto-sync` incluye por defecto:
17
+ - `openspec/changes/<change>/tasks.md`
18
+ - `openspec/changes/<change>/design.md`
19
+ - `openspec/changes/<change>/retrospective.md`
20
+ - Consumo automático universal de aprendizaje:
21
+ - `ai_gate_check`, `pre_flight_check` y `auto_execute_ai_start` exponen `learning_context` cuando existe `openspec/changes/<change>/learning.json`.
22
+ - Evidencia de validación:
23
+ - `npx --yes tsx@4.21.0 --test integrations/mcp/__tests__/aiGateCheck.test.ts integrations/mcp/__tests__/preFlightCheck.test.ts integrations/mcp/__tests__/autoExecuteAiStart.test.ts integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` (`78 pass / 0 fail`)
24
+ - `npm run -s typecheck` (`PASS`)
25
+
8
26
  ### 2026-03-05 (v6.3.43)
9
27
 
10
28
  - `pumuki sdd evidence` alinea su salida con el contrato TDD/BDD del gate:
@@ -2099,12 +2099,94 @@
2099
2099
  - `npx --yes tsx@4.21.0 --test integrations/lifecycle/__tests__/watch.test.ts integrations/lifecycle/__tests__/cli.test.ts integrations/lifecycle/__tests__/policyReconcile.test.ts` -> `53 pass / 0 fail`.
2100
2100
  - `npm run -s typecheck` -> `PASS`.
2101
2101
 
2102
- - 🚧 PUMUKI-162: Ejecutar siguiente pendiente activo de Flux (`PUM-011`) para cerrar paridad consumer de `watch --once --json` con `lastTick.changedFiles[]` y `lastTick.evaluatedFiles[]`.
2102
+ - PUMUKI-162: Ejecutar siguiente pendiente activo de Flux (`PUM-011`) para cerrar paridad consumer de `watch --once --json` con `lastTick.changedFiles[]` y `lastTick.evaluatedFiles[]`.
2103
2103
  - Alcance:
2104
2104
  - verificar contrato JSON final en paquete publicado vs consumer runtime,
2105
2105
  - eliminar drift de payload entre core y distribución npm,
2106
2106
  - cerrar con validación reproducible en consumer (sin tocar código funcional del consumer).
2107
- - Avance actual (2026-03-05):
2108
- - verificado en core local (`bin/pumuki.js`): `lastTick.changedFiles[]` y `lastTick.evaluatedFiles[]` sí aparecen.
2109
- - verificado en paquete publicado (`pumuki@latest`): esos campos aún no aparecen en JSON de `watch`.
2110
- - conclusión: bug activo de rollout/distribución (no de lógica local), pendiente de corte/release para cerrar `PUM-011`.
2107
+ - Resultado (2026-03-05):
2108
+ - release publicado: `pumuki@6.3.44`.
2109
+ - verificación `@latest` en repo temporal:
2110
+ - `lastTick.changedFiles[]` presente.
2111
+ - `lastTick.evaluatedFiles[]` presente.
2112
+ - leyenda Flux actualizada:
2113
+ - `PUM-011` -> `✅ Cerrado`.
2114
+
2115
+ - ✅ PUMUKI-163: Ejecutar siguiente pendiente activo de Flux (`PUM-012`) para garantizar contrato JSON estricto (`stdout` solo JSON) cuando se usa `--json`.
2116
+ - Resultado (2026-03-05):
2117
+ - `integrations/git/runPlatformGate.ts` añade `silent?: boolean` para suprimir salida humana en `stdout` al ejecutar en flujos machine-readable.
2118
+ - `integrations/lifecycle/watch.ts` invoca `runPlatformGate(..., { silent: true })` en `watch --json` para mantener contrato estricto de parseo.
2119
+ - cobertura de regresión añadida en `integrations/git/__tests__/runPlatformGate.test.ts`:
2120
+ - `runPlatformGate silent evita salida humana en stdout para contratos JSON`.
2121
+ - validación funcional de contrato:
2122
+ - `node bin/pumuki.js watch --once --stage=PRE_COMMIT --scope=workingTree --json | jq -e '.status'` -> `JSON_PIPE_OK`.
2123
+ - leyenda Flux actualizada:
2124
+ - `PUM-012` -> `✅ Cerrado`.
2125
+ - backlog Flux externo queda en `✅ 100% cerrado`.
2126
+ - Evidencia:
2127
+ - `npx --yes tsx@4.21.0 --test integrations/git/__tests__/runPlatformGate.test.ts integrations/lifecycle/__tests__/watch.test.ts integrations/lifecycle/__tests__/cli.test.ts` -> `81 pass / 0 fail`.
2128
+ - `npm run -s typecheck` -> `PASS`.
2129
+
2130
+ - ✅ PUMUKI-164: Priorizar cierre SDD pendiente de RuralGo para `sync-docs` completo (3 docs canónicos por defecto + aprendizaje operativo) y dejar contrato enterprise sin ambigüedad.
2131
+ - Resultado (2026-03-05):
2132
+ - `integrations/sdd/syncDocs.ts` amplía targets por defecto de `sync-docs`:
2133
+ - `docs/strategy/ruralgo-tracking-hub.md` (sección managed auto-creable),
2134
+ - `docs/technical/08-validation/refactor/operational-summary.md` (sección managed auto-creable),
2135
+ - `docs/validation/refactor/last-run.json` (merge JSON determinista con `pumuki_sdd_status`),
2136
+ - manteniendo compatibilidad con target previo (`pumuki-integration-feedback.md`) y sin romper repos que no tengan esos archivos (targets opcionales por existencia).
2137
+ - `applyManagedSection` ahora soporta `createIfMissing` (solo cuando faltan ambos markers) para evitar conflicto falso en docs canónicos nuevos.
2138
+ - test de regresión añadido:
2139
+ - `integrations/sdd/__tests__/syncDocs.test.ts` -> `runSddSyncDocs por defecto sincroniza 3 docs canónicos SDD cuando existen en el repo consumer`.
2140
+ - estado RuralGo actualizado:
2141
+ - `/Users/juancarlosmerlosalbarracin/Developer/Projects/R_GO/docs/technical/08-validation/refactor/pumuki-integration-feedback.md`
2142
+ - bloque “Actualizar 3 docs canónicos del consumer por defecto” -> `✅ Implementado`.
2143
+ - Evidencia:
2144
+ - `npx --yes tsx@4.21.0 --test integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` -> `59 pass / 0 fail`.
2145
+ - `npm run -s typecheck` -> `PASS`.
2146
+
2147
+ - ✅ PUMUKI-165: Priorizar siguiente SDD pendiente de RuralGo para auto-sync integral de artefactos OpenSpec (`tasks.md/design.md/retrospective.md`) por contrato default.
2148
+ - Resultado (2026-03-05):
2149
+ - `integrations/sdd/syncDocs.ts` amplía `runSddAutoSync` para incluir por defecto targets OpenSpec por cambio:
2150
+ - `openspec/changes/<change>/tasks.md`
2151
+ - `openspec/changes/<change>/design.md`
2152
+ - `openspec/changes/<change>/retrospective.md`
2153
+ - comportamiento idempotente y compatible:
2154
+ - crea archivo si no existe (`bootstrapIfMissing`),
2155
+ - inserta/actualiza bloque managed `AUTO_SYNC_STATUS`,
2156
+ - no rompe consumers existentes (targets opcionales/canónicos previos se mantienen).
2157
+ - cobertura actualizada:
2158
+ - `runSddAutoSync dry-run orquesta sync-docs + learning sin modificar archivos` ahora valida 4 archivos sincronizados (canónico + 3 OpenSpec),
2159
+ - `runSddAutoSync aplica sync-docs y persiste learning en modo escritura` valida creación real de `tasks/design/retrospective` con markers.
2160
+ - estado RuralGo actualizado:
2161
+ - `/Users/juancarlosmerlosalbarracin/Developer/Projects/R_GO/docs/technical/08-validation/refactor/pumuki-integration-feedback.md`
2162
+ - bloque “Auto-sync integral de tasks.md/design.md/retrospective.md por defecto” -> `✅ Implementado`.
2163
+ - Evidencia:
2164
+ - `npx --yes tsx@4.21.0 --test integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` -> `59 pass / 0 fail`.
2165
+ - `npm run -s typecheck` -> `PASS`.
2166
+
2167
+ - ✅ PUMUKI-166: Priorizar último pendiente SDD de RuralGo para consumo automático universal de `learning.json` por agente/orquestador.
2168
+ - Resultado (2026-03-05):
2169
+ - nuevo helper `integrations/sdd/learningInsights.ts` para lectura robusta de `openspec/changes/<change>/learning.json` (cambio activo) y derivación de recomendaciones accionables.
2170
+ - integración automática en herramientas MCP:
2171
+ - `integrations/mcp/aiGateCheck.ts`
2172
+ - `integrations/mcp/preFlightCheck.ts`
2173
+ - `integrations/mcp/autoExecuteAiStart.ts`
2174
+ - salida extendida sin romper contrato:
2175
+ - campo `learning_context` en cada tool,
2176
+ - hints/auto_fixes/instruction enriquecidos con recomendaciones de learning cuando existen.
2177
+ - cobertura añadida:
2178
+ - `integrations/mcp/__tests__/aiGateCheck.test.ts` (ingesta + auto_fix learning),
2179
+ - `integrations/mcp/__tests__/preFlightCheck.test.ts` (learning_context en preflight),
2180
+ - `integrations/mcp/__tests__/autoExecuteAiStart.test.ts` (mensaje/instrucción con learning).
2181
+ - estado RuralGo actualizado:
2182
+ - `/Users/juancarlosmerlosalbarracin/Developer/Projects/R_GO/docs/technical/08-validation/refactor/pumuki-integration-feedback.md`
2183
+ - bloque “Consumo automático universal de learning.json por cualquier agente” -> `✅ Implementado`.
2184
+ - Evidencia:
2185
+ - `npx --yes tsx@4.21.0 --test integrations/mcp/__tests__/aiGateCheck.test.ts integrations/mcp/__tests__/preFlightCheck.test.ts integrations/mcp/__tests__/autoExecuteAiStart.test.ts integrations/sdd/__tests__/syncDocs.test.ts integrations/lifecycle/__tests__/cli.test.ts` -> `78 pass / 0 fail`.
2186
+ - `npm run -s typecheck` -> `PASS`.
2187
+
2188
+ - 🚧 PUMUKI-167: Preparar release + rollout consumidor tras cierre de backlog SDD/Flux/RuralGo.
2189
+ - Alcance:
2190
+ - consolidar changelog de fixes/mejoras cerradas en este bloque,
2191
+ - publicar nueva versión npm con evidencia de tests en verde,
2192
+ - ejecutar upgrade/update en repos consumidores (`SAAS`, `R_GO`) y validar smoke mínimo post-upgrade.
@@ -769,6 +769,7 @@ export async function runPlatformGate(params: {
769
769
  auditMode?: 'gate' | 'engine';
770
770
  policyTrace?: ResolvedStagePolicy['trace'];
771
771
  scope: GateScope;
772
+ silent?: boolean;
772
773
  sddShortCircuit?: boolean;
773
774
  services?: Partial<GateServices>;
774
775
  dependencies?: Partial<GateDependencies>;
@@ -797,7 +798,9 @@ export async function runPlatformGate(params: {
797
798
  repoRoot
798
799
  );
799
800
  if (!sddDecision.allowed) {
800
- process.stdout.write(`[pumuki][sdd] ${sddDecision.code}: ${sddDecision.message}\n`);
801
+ if (params.silent !== true) {
802
+ process.stdout.write(`[pumuki][sdd] ${sddDecision.code}: ${sddDecision.message}\n`);
803
+ }
801
804
  sddBlockingFinding = toSddBlockingFinding(sddDecision);
802
805
  if (shouldShortCircuitSdd) {
803
806
  const emptyDetectedPlatforms: DetectedPlatforms = {};
@@ -972,16 +975,18 @@ export async function runPlatformGate(params: {
972
975
  const astIntelligenceDualFinding = astIntelligenceDualValidation?.finding;
973
976
  if (astIntelligenceDualValidation && astIntelligenceDualValidation.mode !== 'off') {
974
977
  const summary = astIntelligenceDualValidation.summary;
975
- process.stdout.write(
976
- `[pumuki][ast-intelligence] mode=${astIntelligenceDualValidation.mode}` +
977
- ` mapped_rules=${summary.mapped_rules}` +
978
- ` compared_rules=${summary.compared_rules}` +
979
- ` divergences=${summary.divergences}` +
980
- ` false_positives=${summary.false_positives}` +
981
- ` false_negatives=${summary.false_negatives}` +
982
- ` latency_ms=${summary.latency_ms}` +
983
- ` languages=[${summary.languages.join(',') || 'none'}]\n`
984
- );
978
+ if (params.silent !== true) {
979
+ process.stdout.write(
980
+ `[pumuki][ast-intelligence] mode=${astIntelligenceDualValidation.mode}` +
981
+ ` mapped_rules=${summary.mapped_rules}` +
982
+ ` compared_rules=${summary.compared_rules}` +
983
+ ` divergences=${summary.divergences}` +
984
+ ` false_positives=${summary.false_positives}` +
985
+ ` false_negatives=${summary.false_negatives}` +
986
+ ` latency_ms=${summary.latency_ms}` +
987
+ ` languages=[${summary.languages.join(',') || 'none'}]\n`
988
+ );
989
+ }
985
990
  }
986
991
  const degradedModeBlocks = params.policyTrace?.degraded?.action === 'block';
987
992
  const rulesCoverage = coverage
@@ -1170,9 +1175,11 @@ export async function runPlatformGate(params: {
1170
1175
  } catch (error) {
1171
1176
  const rawReason = error instanceof Error ? error.message : String(error);
1172
1177
  const reason = rawReason.trim().replace(/\s+/g, ' ');
1173
- process.stdout.write(
1174
- `[pumuki][memory-shadow] unavailable reason=${reason.length > 0 ? reason : 'unknown_error'}\n`
1175
- );
1178
+ if (params.silent !== true) {
1179
+ process.stdout.write(
1180
+ `[pumuki][memory-shadow] unavailable reason=${reason.length > 0 ? reason : 'unknown_error'}\n`
1181
+ );
1182
+ }
1176
1183
  }
1177
1184
  }
1178
1185
  const memoryShadow:
@@ -1196,11 +1203,13 @@ export async function runPlatformGate(params: {
1196
1203
  : undefined;
1197
1204
 
1198
1205
  if (memoryShadowRecommendation) {
1199
- process.stdout.write(
1200
- `[pumuki][memory-shadow] recommended=${memoryShadowRecommendation.recommendedOutcome}` +
1201
- ` confidence=${memoryShadowRecommendation.confidence.toFixed(2)}` +
1202
- ` reasons=${memoryShadowRecommendation.reasonCodes.join(',')}\n`
1203
- );
1206
+ if (params.silent !== true) {
1207
+ process.stdout.write(
1208
+ `[pumuki][memory-shadow] recommended=${memoryShadowRecommendation.recommendedOutcome}` +
1209
+ ` confidence=${memoryShadowRecommendation.confidence.toFixed(2)}` +
1210
+ ` reasons=${memoryShadowRecommendation.reasonCodes.join(',')}\n`
1211
+ );
1212
+ }
1204
1213
  }
1205
1214
 
1206
1215
  dependencies.emitPlatformGateEvidence({
@@ -1224,7 +1233,9 @@ export async function runPlatformGate(params: {
1224
1233
  });
1225
1234
 
1226
1235
  if (gateOutcome === 'BLOCK') {
1236
+ if (params.silent !== true) {
1227
1237
  dependencies.printGateFindings(findingsWithWaiver);
1238
+ }
1228
1239
  return 1;
1229
1240
  }
1230
1241
 
@@ -309,6 +309,7 @@ export const runLifecycleWatch = async (
309
309
  policy: resolvedPolicy.policy,
310
310
  policyTrace: resolvedPolicy.trace,
311
311
  scope: gateScope,
312
+ silent: true,
312
313
  });
313
314
  const evidence = activeDependencies.readEvidence(repoRoot);
314
315
  const allFindings = evidence?.snapshot.findings ?? [];
@@ -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 = (evaluation: ReturnType<typeof evaluateAiGate>): ReadonlyArray<string> => {
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
- const message = toHumanMessage({
194
+ let message = toHumanMessage({
190
195
  action,
191
196
  confidencePct,
192
197
  reasonCode,
193
198
  });
194
- const instruction = nextAction.message;
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
+ };
@@ -11,6 +11,18 @@ const SDD_STATUS_SECTION = {
11
11
  end: '<!-- PUMUKI:END SDD_STATUS -->',
12
12
  } as const;
13
13
 
14
+ const TRACKING_HUB_SECTION = {
15
+ id: 'pumuki-sdd-sync',
16
+ begin: '<!-- PUMUKI:BEGIN SDD_SYNC_STATUS -->',
17
+ end: '<!-- PUMUKI:END SDD_SYNC_STATUS -->',
18
+ } as const;
19
+
20
+ const OPERATIONAL_SUMMARY_SECTION = {
21
+ id: 'pumuki-sdd-sync',
22
+ begin: '<!-- PUMUKI:BEGIN SDD_SYNC_STATUS -->',
23
+ end: '<!-- PUMUKI:END SDD_SYNC_STATUS -->',
24
+ } as const;
25
+
14
26
  type ManagedSectionSyncResult = {
15
27
  sectionId: string;
16
28
  updated: boolean;
@@ -24,11 +36,15 @@ export type SddSyncDocsManagedSection = {
24
36
  beginMarker: string;
25
37
  endMarker: string;
26
38
  renderBody: (repoRoot: string) => string;
39
+ createIfMissing?: boolean;
27
40
  };
28
41
 
29
42
  export type SddSyncDocsTarget = {
30
43
  path: string;
31
- sections: ReadonlyArray<SddSyncDocsManagedSection>;
44
+ sections?: ReadonlyArray<SddSyncDocsManagedSection>;
45
+ renderWholeFile?: (repoRoot: string, currentSource: string) => string;
46
+ optional?: boolean;
47
+ bootstrapIfMissing?: string | ((repoRoot: string) => string);
32
48
  };
33
49
 
34
50
  export type SddSyncDocsFileResult = {
@@ -182,7 +198,51 @@ const formatSddStatusManagedBody = (repoRoot: string): string => {
182
198
  ].join('\n');
183
199
  };
184
200
 
185
- const DEFAULT_SYNC_DOCS_TARGETS: ReadonlyArray<SddSyncDocsTarget> = [
201
+ const formatTrackingManagedBody = (repoRoot: string): string => {
202
+ const status = readSddStatus(repoRoot);
203
+ return [
204
+ `- source: pumuki sdd sync-docs`,
205
+ `- repo_root: ${status.repoRoot}`,
206
+ `- openspec_installed: ${status.openspec.installed ? 'yes' : 'no'}`,
207
+ `- openspec_version: ${status.openspec.version ?? 'unknown'}`,
208
+ `- sdd_session_active: ${status.session.active ? 'yes' : 'no'}`,
209
+ `- sdd_session_valid: ${status.session.valid ? 'yes' : 'no'}`,
210
+ `- sdd_session_change: ${status.session.changeId ?? 'none'}`,
211
+ ].join('\n');
212
+ };
213
+
214
+ const renderLastRunJson = (repoRoot: string, currentSource: string): string => {
215
+ let parsed: unknown;
216
+ try {
217
+ parsed = JSON.parse(currentSource);
218
+ } catch {
219
+ throw new Error('[pumuki][sdd] sync-docs invalid JSON in docs/validation/refactor/last-run.json');
220
+ }
221
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object') {
222
+ throw new Error('[pumuki][sdd] sync-docs expected object JSON in docs/validation/refactor/last-run.json');
223
+ }
224
+
225
+ const status = readSddStatus(repoRoot);
226
+ const payload = parsed as Record<string, unknown>;
227
+ const next = {
228
+ ...payload,
229
+ pumuki_sdd_status: {
230
+ source: 'pumuki sdd sync-docs',
231
+ repo_root: status.repoRoot,
232
+ openspec_installed: status.openspec.installed,
233
+ openspec_version: status.openspec.version ?? null,
234
+ openspec_project_initialized: status.openspec.projectInitialized,
235
+ openspec_compatible: status.openspec.compatible,
236
+ session_active: status.session.active,
237
+ session_valid: status.session.valid,
238
+ session_change: status.session.changeId ?? null,
239
+ },
240
+ };
241
+
242
+ return `${JSON.stringify(next, null, 2)}\n`;
243
+ };
244
+
245
+ const PRIMARY_SYNC_DOCS_TARGETS: ReadonlyArray<SddSyncDocsTarget> = [
186
246
  {
187
247
  path: 'docs/technical/08-validation/refactor/pumuki-integration-feedback.md',
188
248
  sections: [
@@ -196,10 +256,151 @@ const DEFAULT_SYNC_DOCS_TARGETS: ReadonlyArray<SddSyncDocsTarget> = [
196
256
  },
197
257
  ];
198
258
 
259
+ const OPTIONAL_SYNC_DOCS_TARGETS: ReadonlyArray<SddSyncDocsTarget> = [
260
+ {
261
+ path: 'docs/strategy/ruralgo-tracking-hub.md',
262
+ optional: true,
263
+ sections: [
264
+ {
265
+ id: TRACKING_HUB_SECTION.id,
266
+ beginMarker: TRACKING_HUB_SECTION.begin,
267
+ endMarker: TRACKING_HUB_SECTION.end,
268
+ renderBody: formatTrackingManagedBody,
269
+ createIfMissing: true,
270
+ },
271
+ ],
272
+ },
273
+ {
274
+ path: 'docs/technical/08-validation/refactor/operational-summary.md',
275
+ optional: true,
276
+ sections: [
277
+ {
278
+ id: OPERATIONAL_SUMMARY_SECTION.id,
279
+ beginMarker: OPERATIONAL_SUMMARY_SECTION.begin,
280
+ endMarker: OPERATIONAL_SUMMARY_SECTION.end,
281
+ renderBody: formatTrackingManagedBody,
282
+ createIfMissing: true,
283
+ },
284
+ ],
285
+ },
286
+ {
287
+ path: 'docs/validation/refactor/last-run.json',
288
+ optional: true,
289
+ renderWholeFile: renderLastRunJson,
290
+ },
291
+ ];
292
+
293
+ const DEFAULT_SYNC_DOCS_TARGETS: ReadonlyArray<SddSyncDocsTarget> = [
294
+ ...PRIMARY_SYNC_DOCS_TARGETS,
295
+ ...OPTIONAL_SYNC_DOCS_TARGETS,
296
+ ];
297
+
199
298
  export const SDD_SYNC_DOCS_CANONICAL_FILES = DEFAULT_SYNC_DOCS_TARGETS.map(
200
299
  (target) => target.path
201
300
  );
202
301
 
302
+ const resolveSyncDocsTargets = (
303
+ repoRoot: string,
304
+ targets?: ReadonlyArray<SddSyncDocsTarget>
305
+ ): ReadonlyArray<SddSyncDocsTarget> => {
306
+ if (targets) {
307
+ return targets;
308
+ }
309
+ return DEFAULT_SYNC_DOCS_TARGETS.filter((target) => {
310
+ if (!target.optional) {
311
+ return true;
312
+ }
313
+ return existsSync(resolve(repoRoot, target.path));
314
+ });
315
+ };
316
+
317
+ const OPENSPEC_AUTO_SYNC_SECTION = {
318
+ id: 'pumuki-auto-sync',
319
+ begin: '<!-- PUMUKI:BEGIN AUTO_SYNC_STATUS -->',
320
+ end: '<!-- PUMUKI:END AUTO_SYNC_STATUS -->',
321
+ } as const;
322
+
323
+ const formatOpenSpecAutoSyncBody = (params: {
324
+ change: string;
325
+ stage: SddStage | null;
326
+ task: string | null;
327
+ now: () => Date;
328
+ }): string =>
329
+ [
330
+ `- source: pumuki sdd auto-sync`,
331
+ `- change: ${params.change}`,
332
+ `- stage: ${params.stage ?? 'none'}`,
333
+ `- task: ${params.task ?? 'none'}`,
334
+ `- updated_at: ${params.now().toISOString()}`,
335
+ ].join('\n');
336
+
337
+ const buildOpenSpecAutoSyncTargets = (params: {
338
+ change: string;
339
+ stage: SddStage | null;
340
+ task: string | null;
341
+ now: () => Date;
342
+ }): ReadonlyArray<SddSyncDocsTarget> => {
343
+ const buildDoc = (title: string): string =>
344
+ [
345
+ `# ${title}`,
346
+ '',
347
+ OPENSPEC_AUTO_SYNC_SECTION.begin,
348
+ '- source: bootstrap',
349
+ OPENSPEC_AUTO_SYNC_SECTION.end,
350
+ '',
351
+ ].join('\n');
352
+
353
+ const renderBody = () =>
354
+ formatOpenSpecAutoSyncBody({
355
+ change: params.change,
356
+ stage: params.stage,
357
+ task: params.task,
358
+ now: params.now,
359
+ });
360
+
361
+ return [
362
+ {
363
+ path: `openspec/changes/${params.change}/tasks.md`,
364
+ sections: [
365
+ {
366
+ id: OPENSPEC_AUTO_SYNC_SECTION.id,
367
+ beginMarker: OPENSPEC_AUTO_SYNC_SECTION.begin,
368
+ endMarker: OPENSPEC_AUTO_SYNC_SECTION.end,
369
+ renderBody,
370
+ createIfMissing: true,
371
+ },
372
+ ],
373
+ bootstrapIfMissing: buildDoc('Tasks'),
374
+ },
375
+ {
376
+ path: `openspec/changes/${params.change}/design.md`,
377
+ sections: [
378
+ {
379
+ id: OPENSPEC_AUTO_SYNC_SECTION.id,
380
+ beginMarker: OPENSPEC_AUTO_SYNC_SECTION.begin,
381
+ endMarker: OPENSPEC_AUTO_SYNC_SECTION.end,
382
+ renderBody,
383
+ createIfMissing: true,
384
+ },
385
+ ],
386
+ bootstrapIfMissing: buildDoc('Design'),
387
+ },
388
+ {
389
+ path: `openspec/changes/${params.change}/retrospective.md`,
390
+ sections: [
391
+ {
392
+ id: OPENSPEC_AUTO_SYNC_SECTION.id,
393
+ beginMarker: OPENSPEC_AUTO_SYNC_SECTION.begin,
394
+ endMarker: OPENSPEC_AUTO_SYNC_SECTION.end,
395
+ renderBody,
396
+ createIfMissing: true,
397
+ },
398
+ ],
399
+ bootstrapIfMissing: buildDoc('Retrospective'),
400
+ },
401
+ ];
402
+ };
403
+
203
404
  const applyManagedSection = (params: {
204
405
  filePath: string;
205
406
  source: string;
@@ -207,14 +408,46 @@ const applyManagedSection = (params: {
207
408
  endMarker: string;
208
409
  renderedBody: string;
209
410
  sectionId: string;
411
+ createIfMissing?: boolean;
210
412
  }): {
211
413
  nextSource: string;
212
414
  result: ManagedSectionSyncResult;
213
415
  } => {
214
416
  const beginIndex = params.source.indexOf(params.beginMarker);
215
417
  const endIndex = params.source.indexOf(params.endMarker);
216
-
217
- if (beginIndex === -1 || endIndex === -1 || endIndex < beginIndex) {
418
+ const missingBegin = beginIndex === -1;
419
+ const missingEnd = endIndex === -1;
420
+
421
+ if (missingBegin || missingEnd) {
422
+ if (params.createIfMissing === true && missingBegin && missingEnd) {
423
+ const beforeBody = '';
424
+ const afterBody = normalizeSectionBody(params.renderedBody);
425
+ const block = `${params.beginMarker}\n${params.renderedBody}\n${params.endMarker}`;
426
+ const sourceTrimmedEnd = params.source.replace(/\s*$/, '');
427
+ const nextSource =
428
+ sourceTrimmedEnd.length === 0
429
+ ? `${block}\n`
430
+ : `${sourceTrimmedEnd}\n\n${block}\n`;
431
+ return {
432
+ nextSource,
433
+ result: {
434
+ sectionId: params.sectionId,
435
+ updated: true,
436
+ before: beforeBody,
437
+ after: afterBody,
438
+ diffMarkdown: buildSectionDiffMarkdown({
439
+ sectionId: params.sectionId,
440
+ before: beforeBody,
441
+ after: afterBody,
442
+ }),
443
+ },
444
+ };
445
+ }
446
+ throw new Error(
447
+ `[pumuki][sdd] sync-docs conflict in ${params.filePath}: expected managed markers ${params.beginMarker} ... ${params.endMarker}`
448
+ );
449
+ }
450
+ if (endIndex < beginIndex) {
218
451
  throw new Error(
219
452
  `[pumuki][sdd] sync-docs conflict in ${params.filePath}: expected managed markers ${params.beginMarker} ... ${params.endMarker}`
220
453
  );
@@ -369,7 +602,7 @@ export const runSddSyncDocs = (params?: {
369
602
  flagName: '--from-evidence',
370
603
  })
371
604
  : null;
372
- const targets = params?.targets ?? DEFAULT_SYNC_DOCS_TARGETS;
605
+ const targets = resolveSyncDocsTargets(repoRoot, params?.targets);
373
606
  const now = params?.now ?? (() => new Date());
374
607
  const evidenceReader =
375
608
  params?.evidenceReader ??
@@ -381,45 +614,88 @@ export const runSddSyncDocs = (params?: {
381
614
  : undefined
382
615
  ));
383
616
 
384
- const updates = targets.map((target) => {
617
+ const updates: Array<{
618
+ relativePath: string;
619
+ absolutePath: string;
620
+ currentSource: string;
621
+ nextSource: string;
622
+ sections: ManagedSectionSyncResult[];
623
+ }> = [];
624
+
625
+ for (const target of targets) {
385
626
  const absolutePath = resolve(repoRoot, target.path);
386
- if (!existsSync(absolutePath)) {
387
- throw new Error(
388
- `[pumuki][sdd] sync-docs missing canonical file: ${target.path}`
389
- );
627
+ const exists = existsSync(absolutePath);
628
+ let currentSource = '';
629
+
630
+ if (!exists) {
631
+ if (target.bootstrapIfMissing !== undefined) {
632
+ currentSource =
633
+ typeof target.bootstrapIfMissing === 'function'
634
+ ? target.bootstrapIfMissing(repoRoot)
635
+ : target.bootstrapIfMissing;
636
+ } else if (target.optional) {
637
+ continue;
638
+ } else {
639
+ throw new Error(
640
+ `[pumuki][sdd] sync-docs missing canonical file: ${target.path}`
641
+ );
642
+ }
643
+ } else {
644
+ currentSource = readFileSync(absolutePath, 'utf8');
390
645
  }
391
646
 
392
- const currentSource = readFileSync(absolutePath, 'utf8');
393
647
  let nextSource = currentSource;
394
648
  const sectionUpdates: ManagedSectionSyncResult[] = [];
395
649
 
396
- for (const section of target.sections) {
397
- const update = applyManagedSection({
398
- filePath: target.path,
399
- source: nextSource,
400
- beginMarker: section.beginMarker,
401
- endMarker: section.endMarker,
402
- renderedBody: section.renderBody(repoRoot),
403
- sectionId: section.id,
650
+ if (target.renderWholeFile) {
651
+ nextSource = target.renderWholeFile(repoRoot, nextSource);
652
+ sectionUpdates.push({
653
+ sectionId: 'file-content',
654
+ updated: normalizeSectionBody(currentSource) !== normalizeSectionBody(nextSource),
655
+ before: normalizeSectionBody(currentSource),
656
+ after: normalizeSectionBody(nextSource),
657
+ diffMarkdown: buildSectionDiffMarkdown({
658
+ sectionId: 'file-content',
659
+ before: normalizeSectionBody(currentSource),
660
+ after: normalizeSectionBody(nextSource),
661
+ }),
404
662
  });
405
- nextSource = update.nextSource;
406
- sectionUpdates.push(update.result);
663
+ } else {
664
+ if (!target.sections || target.sections.length === 0) {
665
+ throw new Error(
666
+ `[pumuki][sdd] sync-docs invalid target configuration for ${target.path}: expected sections or renderWholeFile`
667
+ );
668
+ }
669
+ for (const section of target.sections) {
670
+ const update = applyManagedSection({
671
+ filePath: target.path,
672
+ source: nextSource,
673
+ beginMarker: section.beginMarker,
674
+ endMarker: section.endMarker,
675
+ renderedBody: section.renderBody(repoRoot),
676
+ sectionId: section.id,
677
+ createIfMissing: section.createIfMissing,
678
+ });
679
+ nextSource = update.nextSource;
680
+ sectionUpdates.push(update.result);
681
+ }
407
682
  }
408
683
 
409
- return {
684
+ updates.push({
410
685
  relativePath: target.path,
411
686
  absolutePath,
412
687
  currentSource,
413
688
  nextSource,
414
689
  sections: sectionUpdates,
415
- };
416
- });
690
+ });
691
+ }
417
692
 
418
693
  if (!dryRun) {
419
694
  for (const update of updates) {
420
- if (update.currentSource === update.nextSource) {
695
+ if (update.currentSource === update.nextSource && existsSync(update.absolutePath)) {
421
696
  continue;
422
697
  }
698
+ mkdirSync(dirname(update.absolutePath), { recursive: true });
423
699
  writeFileSync(update.absolutePath, update.nextSource, 'utf8');
424
700
  }
425
701
  }
@@ -557,17 +833,30 @@ export const runSddAutoSync = (params?: {
557
833
  if (!change) {
558
834
  throw new Error('[pumuki][sdd] auto-sync requires --change=<change-id>.');
559
835
  }
836
+ const repoRoot = resolve(params?.repoRoot ?? process.cwd());
837
+ const now = params?.now ?? (() => new Date());
838
+ const targets =
839
+ params?.targets ??
840
+ [
841
+ ...resolveSyncDocsTargets(repoRoot),
842
+ ...buildOpenSpecAutoSyncTargets({
843
+ change,
844
+ stage: params?.stage ?? null,
845
+ task: params?.task?.trim() ? params.task.trim() : null,
846
+ now,
847
+ }),
848
+ ];
560
849
 
561
850
  const syncResult = runSddSyncDocs({
562
- repoRoot: params?.repoRoot,
851
+ repoRoot,
563
852
  dryRun: params?.dryRun,
564
853
  change,
565
854
  stage: params?.stage,
566
855
  task: params?.task,
567
856
  fromEvidencePath: params?.fromEvidencePath,
568
- now: params?.now,
857
+ now,
569
858
  evidenceReader: params?.evidenceReader,
570
- targets: params?.targets,
859
+ targets,
571
860
  });
572
861
 
573
862
  if (!syncResult.learning) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.44",
3
+ "version": "6.3.45",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {