pumuki 6.3.71 → 6.3.73

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 (36) hide show
  1. package/AGENTS.md +269 -0
  2. package/CHANGELOG.md +686 -0
  3. package/README.md +32 -0
  4. package/VERSION +1 -1
  5. package/docs/README.md +7 -2
  6. package/docs/operations/RELEASE_NOTES.md +18 -0
  7. package/docs/product/USAGE.md +10 -0
  8. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +62 -0
  9. package/integrations/gate/governanceActionCatalog.ts +230 -0
  10. package/integrations/git/GitService.ts +25 -0
  11. package/integrations/git/runPlatformGate.ts +9 -1
  12. package/integrations/git/runPlatformGateFacts.ts +1 -0
  13. package/integrations/git/runPlatformGateOutput.ts +36 -27
  14. package/integrations/lifecycle/adapter.templates.json +3 -0
  15. package/integrations/lifecycle/audit.ts +101 -0
  16. package/integrations/lifecycle/cli.ts +80 -8
  17. package/integrations/lifecycle/doctor.ts +64 -1
  18. package/integrations/lifecycle/governanceNextAction.ts +164 -0
  19. package/integrations/lifecycle/governanceObservationSnapshot.ts +288 -0
  20. package/integrations/lifecycle/index.ts +2 -0
  21. package/integrations/lifecycle/status.ts +29 -2
  22. package/integrations/mcp/autoExecuteAiStart.ts +86 -84
  23. package/integrations/mcp/preFlightCheck.ts +40 -3
  24. package/integrations/platform/detectPlatforms.ts +37 -0
  25. package/integrations/sdd/openSpecCli.ts +12 -3
  26. package/package.json +11 -1
  27. package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
  28. package/scripts/consumer-postinstall-resolve-args.cjs +38 -0
  29. package/scripts/consumer-postinstall.cjs +10 -1
  30. package/scripts/framework-menu-consumer-preflight-render.ts +6 -0
  31. package/scripts/framework-menu-consumer-preflight-run.ts +19 -0
  32. package/scripts/framework-menu-consumer-preflight-types.ts +8 -0
  33. package/scripts/pumuki-full-surface-smoke-lib.ts +37 -0
  34. package/scripts/pumuki-full-surface-smoke.ts +261 -0
  35. package/scripts/pumuki-smoke-installed-wrapper.cjs +31 -0
  36. package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
@@ -0,0 +1,288 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { readEvidenceResult } from '../evidence/readEvidence';
4
+ import { readSddStatus } from '../sdd';
5
+ import type { SddStatusPayload } from '../sdd/types';
6
+ import type { LifecycleExperimentalFeaturesSnapshot } from './experimentalFeaturesSnapshot';
7
+ import type { ILifecycleGitService } from './gitService';
8
+ import { LifecycleGitService } from './gitService';
9
+ import type { LifecyclePolicyValidationSnapshot } from './policyValidationSnapshot';
10
+ import { writeInfo } from './cliOutputs';
11
+
12
+ const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
13
+
14
+ export type GovernanceEvidenceSummary = {
15
+ path: string;
16
+ readable: 'missing' | 'invalid' | 'valid';
17
+ snapshot_stage?: string;
18
+ snapshot_outcome?: 'PASS' | 'WARN' | 'BLOCK';
19
+ matched_warn_count?: number;
20
+ matched_blocking_count?: number;
21
+ findings_count?: number;
22
+ ai_gate_status?: 'ALLOWED' | 'BLOCKED';
23
+ human_summary_preview: string[];
24
+ };
25
+
26
+ export type GovernanceContractSurface = {
27
+ agents_md: boolean;
28
+ skills_lock_json: boolean;
29
+ skills_sources_json: boolean;
30
+ vendor_skills_dir: boolean;
31
+ pumuki_adapter_json: boolean;
32
+ };
33
+
34
+ export type GovernanceObservationSnapshot = {
35
+ schema_version: '1';
36
+ sdd: {
37
+ experimental_raw: string | null;
38
+ effective_mode: 'off' | 'advisory' | 'strict';
39
+ experimental_source: string;
40
+ };
41
+ sdd_session: {
42
+ active: boolean;
43
+ valid: boolean;
44
+ change_id: string | null;
45
+ remaining_seconds: number | null;
46
+ };
47
+ policy_strict: {
48
+ pre_write: boolean;
49
+ pre_commit: boolean;
50
+ pre_push: boolean;
51
+ ci: boolean;
52
+ };
53
+ enterprise_warn_as_block_env: boolean;
54
+ evidence: GovernanceEvidenceSummary;
55
+ git: {
56
+ current_branch: string | null;
57
+ on_protected_branch_hint: boolean;
58
+ };
59
+ contract_surface: GovernanceContractSurface;
60
+ attention_codes: ReadonlyArray<string>;
61
+ governance_effective: 'green' | 'attention' | 'blocked';
62
+ agent_bootstrap_hints: ReadonlyArray<string>;
63
+ };
64
+
65
+ const truthyEnv = (value: string | undefined): boolean => {
66
+ if (typeof value !== 'string') {
67
+ return false;
68
+ }
69
+ const normalized = value.trim().toLowerCase();
70
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'strict';
71
+ };
72
+
73
+ const readCurrentBranch = (git: ILifecycleGitService, repoRoot: string): string | null => {
74
+ try {
75
+ const branch = git.runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot).trim();
76
+ return branch.length > 0 ? branch : null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ };
81
+
82
+ const readSddStatusSafe = (repoRoot: string): SddStatusPayload => {
83
+ try {
84
+ return readSddStatus(repoRoot);
85
+ } catch {
86
+ return {
87
+ repoRoot,
88
+ openspec: {
89
+ installed: false,
90
+ projectInitialized: false,
91
+ minimumVersion: '0.0.0',
92
+ recommendedVersion: '0.0.0',
93
+ compatible: false,
94
+ },
95
+ session: {
96
+ repoRoot,
97
+ active: false,
98
+ valid: false,
99
+ },
100
+ };
101
+ }
102
+ };
103
+
104
+ const buildContractSurface = (repoRoot: string): GovernanceContractSurface => ({
105
+ agents_md: existsSync(join(repoRoot, 'AGENTS.md')),
106
+ skills_lock_json: existsSync(join(repoRoot, 'skills.lock.json')),
107
+ skills_sources_json: existsSync(join(repoRoot, 'skills.sources.json')),
108
+ vendor_skills_dir: existsSync(join(repoRoot, 'vendor', 'skills')),
109
+ pumuki_adapter_json: existsSync(join(repoRoot, '.pumuki', 'adapter.json')),
110
+ });
111
+
112
+ const summarizeEvidence = (repoRoot: string): GovernanceEvidenceSummary => {
113
+ const evidenceResult = readEvidenceResult(repoRoot);
114
+ const path = evidenceResult.source_descriptor.path;
115
+ if (evidenceResult.kind === 'missing') {
116
+ return { path, readable: 'missing', human_summary_preview: [] };
117
+ }
118
+ if (evidenceResult.kind === 'invalid') {
119
+ return {
120
+ path,
121
+ readable: 'invalid',
122
+ human_summary_preview: [evidenceResult.detail ?? evidenceResult.reason],
123
+ };
124
+ }
125
+
126
+ const snapshot = evidenceResult.evidence.snapshot;
127
+ const hints = evidenceResult.evidence.operational_hints?.human_summary_lines ?? [];
128
+ const breakdown = evidenceResult.evidence.operational_hints?.rule_execution_breakdown;
129
+ return {
130
+ path,
131
+ readable: 'valid',
132
+ snapshot_stage: snapshot.stage,
133
+ snapshot_outcome: snapshot.outcome,
134
+ matched_warn_count: breakdown?.matched_warn_count,
135
+ matched_blocking_count: breakdown?.matched_blocking_count,
136
+ findings_count: Array.isArray(snapshot.findings) ? snapshot.findings.length : 0,
137
+ ai_gate_status: evidenceResult.evidence.ai_gate.status,
138
+ human_summary_preview: hints.slice(0, 5),
139
+ };
140
+ };
141
+
142
+ const buildHints = (
143
+ surface: GovernanceContractSurface,
144
+ branch: string | null,
145
+ protectedBranchHint: boolean
146
+ ): string[] => {
147
+ const hints: string[] = [];
148
+ if (surface.agents_md) {
149
+ hints.push('AGENTS.md presente: aplica el contrato del repo antes de dar governance en verde.');
150
+ }
151
+ if (!surface.skills_lock_json) {
152
+ hints.push('Falta skills.lock.json: genera lock canónico de skills antes de cerrar la gobernanza.');
153
+ }
154
+ if (!surface.pumuki_adapter_json) {
155
+ hints.push('Falta .pumuki/adapter.json: instala el adaptador si quieres wiring IDE/MCP explícito.');
156
+ }
157
+ if (protectedBranchHint && branch) {
158
+ hints.push(`La rama "${branch}" cae en el set protegido por defecto: usa feature/* o refactor/*.`);
159
+ }
160
+ hints.push('SDD/OpenSpec: usa PUMUKI_EXPERIMENTAL_SDD=advisory|strict cuando el loop SDD esté activo.');
161
+ hints.push('WARN-as-BLOCK: activa PUMUKI_ENTERPRISE_STRICT_WARN_AS_BLOCK=1 si el repo exige promoción dura.');
162
+ return hints;
163
+ };
164
+
165
+ export const readGovernanceObservationSnapshot = (params: {
166
+ repoRoot: string;
167
+ experimentalFeatures: LifecycleExperimentalFeaturesSnapshot;
168
+ policyValidation: LifecyclePolicyValidationSnapshot;
169
+ git?: ILifecycleGitService;
170
+ }): GovernanceObservationSnapshot => {
171
+ const git = params.git ?? new LifecycleGitService();
172
+ const { repoRoot, experimentalFeatures, policyValidation } = params;
173
+ const rawSdd = process.env.PUMUKI_EXPERIMENTAL_SDD?.trim();
174
+ const sddStatus = readSddStatusSafe(repoRoot);
175
+ const evidence = summarizeEvidence(repoRoot);
176
+ const branch = readCurrentBranch(git, repoRoot);
177
+ const onProtected = typeof branch === 'string' && DEFAULT_PROTECTED_BRANCHES.has(branch.trim().toLowerCase());
178
+ const surface = buildContractSurface(repoRoot);
179
+ const warnAsBlock = truthyEnv(process.env.PUMUKI_ENTERPRISE_STRICT_WARN_AS_BLOCK);
180
+
181
+ const attention: string[] = [];
182
+ if (evidence.readable === 'invalid') {
183
+ attention.push('EVIDENCE_INVALID_OR_CHAIN');
184
+ }
185
+ if (evidence.readable === 'valid' && evidence.ai_gate_status === 'BLOCKED') {
186
+ attention.push('AI_GATE_BLOCKED');
187
+ }
188
+ if (evidence.readable === 'valid' && evidence.snapshot_outcome === 'WARN') {
189
+ attention.push('EVIDENCE_SNAPSHOT_WARN');
190
+ }
191
+ if (evidence.readable === 'valid' && evidence.snapshot_outcome === 'BLOCK') {
192
+ attention.push('EVIDENCE_SNAPSHOT_BLOCK');
193
+ }
194
+ if (sddStatus.session.active === true && sddStatus.session.valid !== true) {
195
+ attention.push('SDD_SESSION_INVALID_OR_EXPIRED');
196
+ }
197
+ if (!policyValidation.stages.PRE_WRITE.strict) {
198
+ attention.push('POLICY_PRE_WRITE_NOT_STRICT');
199
+ }
200
+ if (!policyValidation.stages.PRE_COMMIT.strict) {
201
+ attention.push('POLICY_PRE_COMMIT_NOT_STRICT');
202
+ }
203
+ if (!policyValidation.stages.PRE_PUSH.strict) {
204
+ attention.push('POLICY_PRE_PUSH_NOT_STRICT');
205
+ }
206
+ if (!policyValidation.stages.CI.strict) {
207
+ attention.push('POLICY_CI_NOT_STRICT');
208
+ }
209
+ if (onProtected) {
210
+ attention.push('GITFLOW_PROTECTED_BRANCH_CONTEXT');
211
+ }
212
+
213
+ let governanceEffective: GovernanceObservationSnapshot['governance_effective'] = 'green';
214
+ if (
215
+ evidence.readable === 'invalid'
216
+ || (evidence.readable === 'valid' && evidence.ai_gate_status === 'BLOCKED')
217
+ || (evidence.readable === 'valid' && evidence.snapshot_outcome === 'BLOCK')
218
+ ) {
219
+ governanceEffective = 'blocked';
220
+ } else if (attention.length > 0) {
221
+ governanceEffective = 'attention';
222
+ }
223
+
224
+ return {
225
+ schema_version: '1',
226
+ sdd: {
227
+ experimental_raw: rawSdd && rawSdd.length > 0 ? rawSdd : null,
228
+ effective_mode: experimentalFeatures.features.sdd.mode,
229
+ experimental_source: experimentalFeatures.features.sdd.source,
230
+ },
231
+ sdd_session: {
232
+ active: sddStatus.session.active,
233
+ valid: sddStatus.session.valid,
234
+ change_id: sddStatus.session.changeId ?? null,
235
+ remaining_seconds:
236
+ typeof sddStatus.session.remainingSeconds === 'number' ? sddStatus.session.remainingSeconds : null,
237
+ },
238
+ policy_strict: {
239
+ pre_write: policyValidation.stages.PRE_WRITE.strict,
240
+ pre_commit: policyValidation.stages.PRE_COMMIT.strict,
241
+ pre_push: policyValidation.stages.PRE_PUSH.strict,
242
+ ci: policyValidation.stages.CI.strict,
243
+ },
244
+ enterprise_warn_as_block_env: warnAsBlock,
245
+ evidence,
246
+ git: {
247
+ current_branch: branch,
248
+ on_protected_branch_hint: onProtected,
249
+ },
250
+ contract_surface: surface,
251
+ attention_codes: attention,
252
+ governance_effective: governanceEffective,
253
+ agent_bootstrap_hints: buildHints(surface, branch, onProtected),
254
+ };
255
+ };
256
+
257
+ export const buildGovernanceObservationSummaryLines = (
258
+ snapshot: GovernanceObservationSnapshot
259
+ ): string[] => {
260
+ const lines = [
261
+ `Governance: ${snapshot.governance_effective.toUpperCase()}`,
262
+ `Contract: AGENTS=${snapshot.contract_surface.agents_md ? 'yes' : 'no'} skills.lock=${snapshot.contract_surface.skills_lock_json ? 'yes' : 'no'} skills.sources=${snapshot.contract_surface.skills_sources_json ? 'yes' : 'no'} vendor/skills=${snapshot.contract_surface.vendor_skills_dir ? 'yes' : 'no'} adapter=${snapshot.contract_surface.pumuki_adapter_json ? 'yes' : 'no'}`,
263
+ `SDD: env=${snapshot.sdd.experimental_raw ?? '(unset)'} effective=${snapshot.sdd.effective_mode} session_active=${snapshot.sdd_session.active} session_valid=${snapshot.sdd_session.valid} change=${snapshot.sdd_session.change_id ?? 'none'}`,
264
+ `Evidence: readable=${snapshot.evidence.readable} stage=${snapshot.evidence.snapshot_stage ?? 'n/a'} outcome=${snapshot.evidence.snapshot_outcome ?? 'n/a'} ai_gate=${snapshot.evidence.ai_gate_status ?? 'n/a'} findings=${snapshot.evidence.findings_count ?? 'n/a'}`,
265
+ `GitFlow: branch=${snapshot.git.current_branch ?? 'unknown'} protected_hint=${snapshot.git.on_protected_branch_hint ? 'yes' : 'no'}`,
266
+ `Policy strict: PRE_WRITE=${snapshot.policy_strict.pre_write} PRE_COMMIT=${snapshot.policy_strict.pre_commit} PRE_PUSH=${snapshot.policy_strict.pre_push} CI=${snapshot.policy_strict.ci}`,
267
+ ];
268
+ if (snapshot.attention_codes.length > 0) {
269
+ lines.push(`Attention: ${snapshot.attention_codes.join(', ')}`);
270
+ }
271
+ return lines;
272
+ };
273
+
274
+ export const printGovernanceObservationHuman = (snapshot: GovernanceObservationSnapshot): void => {
275
+ writeInfo('[pumuki] governance truth (S1 / governance console baseline):');
276
+ for (const line of buildGovernanceObservationSummaryLines(snapshot)) {
277
+ writeInfo(`[pumuki] ${line}`);
278
+ }
279
+ for (const hint of snapshot.evidence.human_summary_preview) {
280
+ writeInfo(`[pumuki] evidence hint: ${hint}`);
281
+ }
282
+ };
283
+
284
+ export const doctorGovernanceIsBlocking = (snapshot: GovernanceObservationSnapshot): boolean =>
285
+ snapshot.governance_effective === 'blocked';
286
+
287
+ export const doctorGovernanceNeedsAttention = (snapshot: GovernanceObservationSnapshot): boolean =>
288
+ snapshot.governance_effective !== 'green';
@@ -1,4 +1,6 @@
1
1
  export { runLifecycleDoctor, doctorHasBlockingIssues } from './doctor';
2
+ export { runLifecycleAudit } from './audit';
3
+ export type { LifecycleAuditResult, LifecycleAuditStage } from './audit';
2
4
  export { runLifecycleInstall } from './install';
3
5
  export { runLifecycleUninstall } from './uninstall';
4
6
  export { runLifecycleRemove } from './remove';
@@ -9,6 +9,15 @@ import {
9
9
  readLifecyclePolicyValidationSnapshot,
10
10
  type LifecyclePolicyValidationSnapshot,
11
11
  } from './policyValidationSnapshot';
12
+ import {
13
+ readGovernanceObservationSnapshot,
14
+ type GovernanceObservationSnapshot,
15
+ } from './governanceObservationSnapshot';
16
+ import {
17
+ readGovernanceNextAction,
18
+ type GovernanceNextActionReader,
19
+ type GovernanceNextActionSummary,
20
+ } from './governanceNextAction';
12
21
  import { readLifecycleState, type LifecycleState } from './state';
13
22
 
14
23
  export type LifecycleStatus = {
@@ -22,11 +31,14 @@ export type LifecycleStatus = {
22
31
  trackedNodeModulesCount: number;
23
32
  policyValidation: LifecyclePolicyValidationSnapshot;
24
33
  experimentalFeatures: LifecycleExperimentalFeaturesSnapshot;
34
+ governanceObservation: GovernanceObservationSnapshot;
35
+ governanceNextAction: GovernanceNextActionSummary;
25
36
  };
26
37
 
27
38
  export const readLifecycleStatus = (params?: {
28
39
  cwd?: string;
29
40
  git?: ILifecycleGitService;
41
+ governanceNextActionReader?: GovernanceNextActionReader;
30
42
  }): LifecycleStatus => {
31
43
  const git = params?.git ?? new LifecycleGitService();
32
44
  const cwd = params?.cwd ?? process.cwd();
@@ -38,6 +50,19 @@ export const readLifecycleStatus = (params?: {
38
50
  repoRoot,
39
51
  lifecycleVersion: lifecycleState.version,
40
52
  });
53
+ const policyValidation = readLifecyclePolicyValidationSnapshot(repoRoot);
54
+ const experimentalFeatures = readLifecycleExperimentalFeaturesSnapshot();
55
+ const governanceObservation = readGovernanceObservationSnapshot({
56
+ repoRoot,
57
+ experimentalFeatures,
58
+ policyValidation,
59
+ git,
60
+ });
61
+ const governanceNextAction = (params?.governanceNextActionReader ?? readGovernanceNextAction)({
62
+ repoRoot,
63
+ stage: 'PRE_WRITE',
64
+ governanceObservation,
65
+ });
41
66
 
42
67
  return {
43
68
  repoRoot,
@@ -48,7 +73,9 @@ export const readLifecycleStatus = (params?: {
48
73
  hooksDirectory: hooksDirectory.path,
49
74
  hooksDirectoryResolution: hooksDirectory.source,
50
75
  trackedNodeModulesCount,
51
- policyValidation: readLifecyclePolicyValidationSnapshot(repoRoot),
52
- experimentalFeatures: readLifecycleExperimentalFeaturesSnapshot(),
76
+ policyValidation,
77
+ experimentalFeatures,
78
+ governanceNextAction,
79
+ governanceObservation,
53
80
  };
54
81
  };
@@ -1,3 +1,7 @@
1
+ import {
2
+ buildGovernanceValidateCommand,
3
+ resolveGovernanceCatalogAction,
4
+ } from '../gate/governanceActionCatalog';
1
5
  import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
2
6
  import { collectWorktreeAtomicSlices } from '../git/worktreeAtomicSlices';
3
7
  import { resolveLearningContextExperimentalFeature } from '../policy/experimentalFeatures';
@@ -56,90 +60,87 @@ const confidenceFromViolation = (violationCode: string | null): number => {
56
60
  return 50;
57
61
  };
58
62
 
59
- const nextActionFromViolation = (
60
- violation: AiGateViolation | undefined,
61
- repoRoot: string
62
- ): AutoExecuteNextAction => {
63
- if (!violation) {
64
- return {
65
- kind: 'info',
66
- message: 'Gate listo. Puedes continuar con implementación.',
67
- };
68
- }
69
- switch (violation.code) {
70
- case 'EVIDENCE_MISSING':
63
+ const normalizeGovernanceCatalogCode = (code: string): string => {
64
+ switch (code) {
71
65
  case 'EVIDENCE_INVALID':
72
66
  case 'EVIDENCE_CHAIN_INVALID':
73
- case 'EVIDENCE_STALE':
74
- return {
75
- kind: 'run_command',
76
- message: 'Regenera o refresca evidencia y vuelve a evaluar PRE_WRITE.',
77
- command: 'npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
78
- };
79
- case 'EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES':
80
- return {
81
- kind: 'run_command',
82
- message:
83
- 'No hay active_rule_ids para plataforma de código detectada. Reconciliación strict de policy/skills y revalidación PRE_WRITE.',
84
- command:
85
- 'npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
86
- };
87
- case 'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE':
88
- case 'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING':
89
- case 'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE':
90
- return {
91
- kind: 'run_command',
92
- message:
93
- 'Completa cobertura de skills por plataforma (prefijos + bundles) y revalida PRE_WRITE.',
94
- command: 'npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
95
- };
96
- case 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING':
97
- case 'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE':
98
- return {
99
- kind: 'run_command',
100
- message:
101
- 'Reconcilia policy/skills en modo estricto (incluida skills.ios.critical-test-quality cuando aplique) y revalida PRE_WRITE.',
102
- command:
103
- 'npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
104
- };
105
- case 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT':
106
- case 'EVIDENCE_PREWRITE_WORKTREE_WARN':
107
- {
108
- const plan = collectWorktreeAtomicSlices({
109
- repoRoot,
110
- maxSlices: 3,
111
- maxFilesPerSlice: 4,
112
- });
113
- if (plan.slices.length > 0) {
114
- const firstSlice = plan.slices[0];
115
- return {
116
- kind: 'run_command',
117
- message:
118
- `Particiona el worktree en slices atómicos por scope. Primer lote sugerido: ${firstSlice?.scope ?? 'scope-desconocido'}.`,
119
- command:
120
- `${firstSlice?.staged_command ?? 'git add -p'} && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json`,
121
- };
122
- }
123
- }
124
- return {
125
- kind: 'run_command',
126
- message:
127
- 'Particiona el worktree en slices atómicos y revalida PRE_WRITE para continuar sin fricción.',
128
- command:
129
- 'git status --short && git add -p && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
130
- };
67
+ return 'EVIDENCE_INVALID_OR_CHAIN';
131
68
  case 'GITFLOW_PROTECTED_BRANCH':
132
- return {
133
- kind: 'run_command',
134
- message: 'Cambia a una rama feature/* antes de continuar.',
135
- command: 'git checkout -b feature/<descripcion-kebab-case>',
136
- };
69
+ return 'GITFLOW_PROTECTED_BRANCH_CONTEXT';
137
70
  default:
71
+ return code;
72
+ }
73
+ };
74
+
75
+ const resolveAutoExecuteRemediation = (params: {
76
+ violation: AiGateViolation | undefined;
77
+ repoRoot: string;
78
+ stage: AiGateStage;
79
+ allowed: boolean;
80
+ }): {
81
+ instruction: string;
82
+ nextAction: AutoExecuteNextAction;
83
+ } => {
84
+ if (!params.violation) {
85
+ const readyAction = resolveGovernanceCatalogAction({
86
+ code: 'READY',
87
+ stage: params.stage,
88
+ });
89
+ return {
90
+ instruction: readyAction.instruction,
91
+ nextAction: readyAction.next_action,
92
+ };
93
+ }
94
+
95
+ if (
96
+ params.violation.code === 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT'
97
+ || params.violation.code === 'EVIDENCE_PREWRITE_WORKTREE_WARN'
98
+ ) {
99
+ const validateCommand = buildGovernanceValidateCommand(params.stage);
100
+ const plan = collectWorktreeAtomicSlices({
101
+ repoRoot: params.repoRoot,
102
+ maxSlices: 3,
103
+ maxFilesPerSlice: 4,
104
+ });
105
+ if (plan.slices.length > 0) {
106
+ const firstSlice = plan.slices[0];
138
107
  return {
139
- kind: 'info',
140
- message: 'Corrige la violación bloqueante y vuelve a ejecutar auto_execute_ai_start.',
108
+ instruction: 'Particiona el worktree en slices atómicos antes de continuar.',
109
+ nextAction: {
110
+ kind: params.allowed ? 'info' : 'run_command',
111
+ message:
112
+ `Particiona el worktree en slices atómicos por scope. Primer lote sugerido: ${firstSlice?.scope ?? 'scope-desconocido'}.`,
113
+ command: params.allowed
114
+ ? undefined
115
+ : `${firstSlice?.staged_command ?? 'git add -p'} && ${validateCommand}`,
116
+ },
141
117
  };
118
+ }
119
+ return {
120
+ instruction: 'Particiona el worktree en slices atómicos antes de continuar.',
121
+ nextAction: {
122
+ kind: params.allowed ? 'info' : 'run_command',
123
+ message: 'Particiona el worktree en slices atómicos y revalida PRE_WRITE para continuar sin fricción.',
124
+ command: params.allowed
125
+ ? undefined
126
+ : `git status --short && git add -p && ${validateCommand}`,
127
+ },
128
+ };
142
129
  }
130
+
131
+ const governanceAction = resolveGovernanceCatalogAction({
132
+ code: normalizeGovernanceCatalogCode(params.violation.code),
133
+ stage: params.stage,
134
+ });
135
+ return {
136
+ instruction: governanceAction.instruction,
137
+ nextAction: params.allowed
138
+ ? {
139
+ kind: 'info',
140
+ message: governanceAction.next_action.message,
141
+ }
142
+ : governanceAction.next_action,
143
+ };
143
144
  };
144
145
 
145
146
  export type EnterpriseAutoExecuteAiStartResult = {
@@ -190,19 +191,20 @@ export const runEnterpriseAutoExecuteAiStart = (params: {
190
191
  const action: AutoExecuteAction = evaluation.allowed ? 'proceed' : 'ask';
191
192
  const phase = toAutoExecutePhase(action);
192
193
  const confidencePct = confidenceFromViolation(firstViolation?.code ?? null);
193
- const nextAction = evaluation.allowed
194
- ? {
195
- kind: 'info' as const,
196
- message: 'Gate en verde. Continúa con la implementación.',
197
- }
198
- : nextActionFromViolation(firstViolation, params.repoRoot);
194
+ const remediation = resolveAutoExecuteRemediation({
195
+ violation: firstViolation,
196
+ repoRoot: params.repoRoot,
197
+ stage,
198
+ allowed: evaluation.allowed,
199
+ });
200
+ const nextAction = remediation.nextAction;
199
201
 
200
202
  let message = toHumanMessage({
201
203
  action,
202
204
  confidencePct,
203
205
  reasonCode,
204
206
  });
205
- let instruction = nextAction.message;
207
+ let instruction = remediation.instruction;
206
208
  if (learningContext?.recommended_actions[0]) {
207
209
  message = `${message} Learning: ${learningContext.recommended_actions[0]}`;
208
210
  instruction = `${instruction} Learning: ${learningContext.recommended_actions[0]}`;
@@ -1,3 +1,7 @@
1
+ import {
2
+ resolveGovernanceCatalogAction,
3
+ type GovernanceCatalogNextAction,
4
+ } from '../gate/governanceActionCatalog';
1
5
  import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
2
6
  import { collectWorktreeAtomicSlices } from '../git/worktreeAtomicSlices';
3
7
  import { resolveLearningContextExperimentalFeature } from '../policy/experimentalFeatures';
@@ -40,6 +44,18 @@ const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
40
44
  GITFLOW_PROTECTED_BRANCH: 'Evita trabajo directo en ramas protegidas (usa feature/*).',
41
45
  };
42
46
 
47
+ const normalizeGovernanceCatalogCode = (code: string): string => {
48
+ switch (code) {
49
+ case 'EVIDENCE_INVALID':
50
+ case 'EVIDENCE_CHAIN_INVALID':
51
+ return 'EVIDENCE_INVALID_OR_CHAIN';
52
+ case 'GITFLOW_PROTECTED_BRANCH':
53
+ return 'GITFLOW_PROTECTED_BRANCH_CONTEXT';
54
+ default:
55
+ return code;
56
+ }
57
+ };
58
+
43
59
  const buildPreFlightHints = (params: {
44
60
  repoRoot: string;
45
61
  stage: AiGateStage;
@@ -114,6 +130,8 @@ export type EnterprisePreFlightCheckResult = {
114
130
  phase: 'GREEN' | 'RED';
115
131
  message: string;
116
132
  instruction: string;
133
+ reason_code: string;
134
+ next_action: GovernanceCatalogNextAction;
117
135
  stage: ReturnType<typeof evaluateAiGate>['stage'];
118
136
  policy: ReturnType<typeof evaluateAiGate>['policy'];
119
137
  violations: ReturnType<typeof evaluateAiGate>['violations'];
@@ -153,13 +171,30 @@ export const runEnterprisePreFlightCheck = (params: {
153
171
  upstream: evaluation.repo_state.git.upstream,
154
172
  learningContext,
155
173
  });
174
+ const firstViolation = evaluation.violations[0];
175
+ const reasonCode = firstViolation?.code ?? 'READY';
176
+ const governanceAction = firstViolation
177
+ ? resolveGovernanceCatalogAction({
178
+ code: normalizeGovernanceCatalogCode(firstViolation.code),
179
+ stage: evaluation.stage,
180
+ })
181
+ : resolveGovernanceCatalogAction({
182
+ code: 'READY',
183
+ stage: evaluation.stage,
184
+ });
156
185
  const phase: 'GREEN' | 'RED' = evaluation.allowed ? 'GREEN' : 'RED';
157
186
  const message = evaluation.allowed
158
187
  ? '✅ Pre-flight aprobado: puedes continuar con la implementación.'
159
- : `🔴 Pre-flight bloqueado: corrige ${evaluation.violations[0]?.code ?? 'la causa'} y vuelve a ejecutar.`;
160
- const instruction = evaluation.allowed
188
+ : `🔴 Pre-flight bloqueado: corrige ${reasonCode} y vuelve a ejecutar.`;
189
+ const instruction = !firstViolation && evaluation.allowed
161
190
  ? 'Implementa el cambio mínimo para pasar en verde y vuelve a validar.'
162
- : hints[0] ?? 'Corrige la causa bloqueante y vuelve a ejecutar el pre-flight.';
191
+ : governanceAction.instruction;
192
+ const nextAction: GovernanceCatalogNextAction = evaluation.allowed
193
+ ? {
194
+ kind: 'info',
195
+ message: governanceAction.next_action.message,
196
+ }
197
+ : governanceAction.next_action;
163
198
 
164
199
  return {
165
200
  tool: 'pre_flight_check',
@@ -172,6 +207,8 @@ export const runEnterprisePreFlightCheck = (params: {
172
207
  phase,
173
208
  message,
174
209
  instruction,
210
+ reason_code: reasonCode,
211
+ next_action: nextAction,
175
212
  stage: evaluation.stage,
176
213
  policy: evaluation.policy,
177
214
  violations: evaluation.violations,
@@ -51,6 +51,38 @@ const collectFilePaths = (facts: ReadonlyArray<Fact>): string[] => {
51
51
  .filter((path): path is string => typeof path === 'string');
52
52
  };
53
53
 
54
+ const ALLOWED_PIN_KEYS = new Set(['ios', 'android', 'backend', 'frontend']);
55
+
56
+ const readPinnedPlatformsFromEnv = (): ReadonlySet<keyof DetectedPlatforms> | null => {
57
+ const raw = process.env.PUMUKI_PIN_PLATFORMS?.trim().toLowerCase();
58
+ if (!raw) {
59
+ return null;
60
+ }
61
+ const tokens = raw
62
+ .split(',')
63
+ .map((token) => token.trim())
64
+ .filter((token) => token.length > 0)
65
+ .filter((token) => ALLOWED_PIN_KEYS.has(token)) as Array<keyof DetectedPlatforms>;
66
+ if (tokens.length === 0) {
67
+ return null;
68
+ }
69
+ return new Set(tokens);
70
+ };
71
+
72
+ const applyPinnedPlatformsFilter = (
73
+ detected: DetectedPlatforms,
74
+ pin: ReadonlySet<keyof DetectedPlatforms>
75
+ ): DetectedPlatforms => {
76
+ const next: DetectedPlatforms = {};
77
+ for (const key of pin) {
78
+ const state = detected[key];
79
+ if (state) {
80
+ next[key] = state;
81
+ }
82
+ }
83
+ return next;
84
+ };
85
+
54
86
  export const detectPlatformsFromFacts = (
55
87
  facts: ReadonlyArray<Fact>
56
88
  ): DetectedPlatforms => {
@@ -102,5 +134,10 @@ export const detectPlatformsFromFacts = (
102
134
  };
103
135
  }
104
136
 
137
+ const pin = readPinnedPlatformsFromEnv();
138
+ if (pin && pin.size > 0) {
139
+ return applyPinnedPlatformsFilter(result, pin);
140
+ }
141
+
105
142
  return result;
106
143
  };