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,101 @@
1
+ import { readEvidence } from '../evidence/readEvidence';
2
+ import { GitService } from '../git/GitService';
3
+ import { hasAllowedExtension } from '../git/gitDiffUtils';
4
+ import { runPlatformGate } from '../git/runPlatformGate';
5
+ import { evaluatePlatformGateFindings } from '../git/runPlatformGateEvaluation';
6
+ import { DEFAULT_FACT_FILE_EXTENSIONS } from '../git/runPlatformGateFacts';
7
+ import { resolvePolicyForStage } from '../gate/stagePolicies';
8
+
9
+ export type LifecycleAuditStage = 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
10
+
11
+ export type LifecycleAuditResult = {
12
+ command: 'pumuki audit';
13
+ repo_root: string;
14
+ stage: LifecycleAuditStage;
15
+ scope: { kind: 'repo' };
16
+ audit_mode: 'gate' | 'engine';
17
+ gate_exit_code: number;
18
+ files_scanned: number | null;
19
+ untracked_matching_extensions_count: number;
20
+ snapshot_outcome: string | null;
21
+ policy_reconcile_hint: string;
22
+ };
23
+
24
+ const POLICY_RECONCILE_HINT =
25
+ 'If .pumuki/policy-as-code.json signatures drift after a pumuki upgrade, run: pumuki policy reconcile --apply';
26
+
27
+ const countUntrackedMatchingExtensions = (
28
+ git: GitService,
29
+ extensions: ReadonlyArray<string>
30
+ ): number => {
31
+ const repoRoot = git.resolveRepoRoot();
32
+ const raw = git.runGit(['ls-files', '--others', '--exclude-standard'], repoRoot);
33
+ return raw
34
+ .split('\n')
35
+ .map((line) => line.trim())
36
+ .filter((line) => line.length > 0)
37
+ .filter((path) => hasAllowedExtension(path, extensions)).length;
38
+ };
39
+
40
+ export const runLifecycleAudit = async (params: {
41
+ stage: LifecycleAuditStage;
42
+ auditMode: 'gate' | 'engine';
43
+ }): Promise<LifecycleAuditResult> => {
44
+ const git = new GitService();
45
+ const repoRoot = git.resolveRepoRoot();
46
+ const resolved = resolvePolicyForStage(params.stage, repoRoot);
47
+ const extensions = DEFAULT_FACT_FILE_EXTENSIONS;
48
+ const untrackedMatchingExtensionsCount = countUntrackedMatchingExtensions(git, extensions);
49
+
50
+ const gateParams =
51
+ params.auditMode === 'engine'
52
+ ? {
53
+ policy: resolved.policy,
54
+ policyTrace: resolved.trace,
55
+ scope: { kind: 'repo' as const },
56
+ silent: true,
57
+ auditMode: 'engine' as const,
58
+ dependencies: {
59
+ evaluatePlatformGateFindings: (
60
+ evalParams: Parameters<typeof evaluatePlatformGateFindings>[0]
61
+ ) =>
62
+ evaluatePlatformGateFindings(evalParams, {
63
+ loadHeuristicsConfig: () => ({
64
+ astSemanticEnabled: true,
65
+ typeScriptScope: 'all',
66
+ }),
67
+ }),
68
+ printGateFindings: () => {},
69
+ },
70
+ }
71
+ : {
72
+ policy: resolved.policy,
73
+ policyTrace: resolved.trace,
74
+ scope: { kind: 'repo' as const },
75
+ silent: true,
76
+ auditMode: 'gate' as const,
77
+ };
78
+
79
+ const gateExitCode = await runPlatformGate(gateParams);
80
+ const evidence = readEvidence(repoRoot);
81
+ const filesScanned =
82
+ typeof evidence?.snapshot.files_scanned === 'number' &&
83
+ Number.isFinite(evidence.snapshot.files_scanned)
84
+ ? evidence.snapshot.files_scanned
85
+ : null;
86
+ const snapshotOutcome =
87
+ typeof evidence?.snapshot.outcome === 'string' ? evidence.snapshot.outcome : null;
88
+
89
+ return {
90
+ command: 'pumuki audit',
91
+ repo_root: repoRoot,
92
+ stage: params.stage,
93
+ scope: { kind: 'repo' },
94
+ audit_mode: params.auditMode,
95
+ gate_exit_code: gateExitCode,
96
+ files_scanned: filesScanned,
97
+ untracked_matching_extensions_count: untrackedMatchingExtensionsCount,
98
+ snapshot_outcome: snapshotOutcome,
99
+ policy_reconcile_hint: POLICY_RECONCILE_HINT,
100
+ };
101
+ };
@@ -4,11 +4,16 @@ import type { GatePolicy } from '../../core/gate/GatePolicy';
4
4
  import { runPlatformGate } from '../git/runPlatformGate';
5
5
  import { collectWorktreeAtomicSlices } from '../git/worktreeAtomicSlices';
6
6
  import {
7
+ doctorCommandShouldFailExit,
8
+ doctorCommandShouldWarnHuman,
7
9
  doctorHasBlockingIssues,
10
+ doctorHasGovernanceAttention,
8
11
  doctorHasParityMismatch,
9
12
  runLifecycleDoctor,
10
13
  type LifecycleDoctorReport,
11
14
  } from './doctor';
15
+ import { printGovernanceObservationHuman } from './governanceObservationSnapshot';
16
+ import { printGovernanceNextActionHuman } from './governanceNextAction';
12
17
  import { runLifecycleInstall } from './install';
13
18
  import { runLifecycleRemove } from './remove';
14
19
  import { readLifecycleStatus } from './status';
@@ -72,6 +77,7 @@ import {
72
77
  type RemoteCiDiagnostics,
73
78
  } from './remoteCiDiagnostics';
74
79
  import { runPolicyReconcile } from './policyReconcile';
80
+ import { runLifecycleAudit, type LifecycleAuditStage } from './audit';
75
81
  import { resolvePreWriteEnforcement, type PreWriteEnforcementResolution } from '../policy/preWriteEnforcement';
76
82
 
77
83
  type LifecycleCommand =
@@ -87,7 +93,8 @@ type LifecycleCommand =
87
93
  | 'sdd'
88
94
  | 'adapter'
89
95
  | 'analytics'
90
- | 'policy';
96
+ | 'policy'
97
+ | 'audit';
91
98
 
92
99
  type SddCommand =
93
100
  | 'status'
@@ -173,6 +180,8 @@ export type ParsedArgs = {
173
180
  policyCommand?: PolicyCommand;
174
181
  policyStrict?: boolean;
175
182
  policyApply?: boolean;
183
+ auditStage?: LifecycleAuditStage;
184
+ auditEngine?: boolean;
176
185
  };
177
186
 
178
187
  const HELP_TEXT = `
@@ -183,6 +192,7 @@ Pumuki lifecycle commands:
183
192
  pumuki remove
184
193
  pumuki update [--latest|--spec=<package-spec>]
185
194
  pumuki doctor [--remote-checks] [--deep] [--parity] [--json]
195
+ pumuki audit [--stage=PRE_COMMIT|PRE_PUSH|CI] [--engine] [--json]
186
196
  pumuki status [--json] [--remote-checks]
187
197
  pumuki watch [--stage=PRE_COMMIT|PRE_PUSH|CI] [--scope=workingTree|staged|repoAndStaged|repo] [--severity=critical|high|medium|low] [--interval-ms=<n>] [--notify-cooldown-ms=<n>] [--no-notify] [--once|--iterations=<n>] [--json]
188
198
  pumuki loop run --objective=<text> [--max-attempts=<n>] [--json]
@@ -228,7 +238,8 @@ const isLifecycleCommand = (value: string): value is LifecycleCommand =>
228
238
  value === 'sdd' ||
229
239
  value === 'adapter' ||
230
240
  value === 'analytics' ||
231
- value === 'policy';
241
+ value === 'policy' ||
242
+ value === 'audit';
232
243
 
233
244
  const parseAdapterAgent = (value?: string): AdapterAgent => {
234
245
  const normalized = (value ?? '').trim();
@@ -258,6 +269,16 @@ const parseSddStage = (value?: string): SddStage => {
258
269
  throw new Error(`Unsupported SDD stage "${value}". Use PRE_WRITE, PRE_COMMIT, PRE_PUSH or CI.`);
259
270
  };
260
271
 
272
+ const parseAuditStage = (value?: string): LifecycleAuditStage => {
273
+ const stage = parseSddStage(value);
274
+ if (stage === 'PRE_WRITE') {
275
+ throw new Error(
276
+ 'PRE_WRITE is not supported for "pumuki audit". Use PRE_COMMIT, PRE_PUSH or CI (aliases GREEN, REFACTOR, CLOSE).'
277
+ );
278
+ }
279
+ return stage;
280
+ };
281
+
261
282
  const parseSddEvidencePath = (value: string): string => {
262
283
  const normalized = value.trim();
263
284
  if (normalized.length === 0) {
@@ -623,6 +644,8 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
623
644
  let analyticsSinceDays: ParsedArgs['analyticsSinceDays'];
624
645
  let analyticsJsonOutputPath: ParsedArgs['analyticsJsonOutputPath'];
625
646
  let analyticsMarkdownOutputPath: ParsedArgs['analyticsMarkdownOutputPath'];
647
+ let auditStage: LifecycleAuditStage | undefined;
648
+ let auditEngine = false;
626
649
 
627
650
  if (commandRaw === 'watch') {
628
651
  for (const arg of argv.slice(1)) {
@@ -806,6 +829,31 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
806
829
  };
807
830
  }
808
831
 
832
+ if (commandRaw === 'audit') {
833
+ for (const arg of argv.slice(1)) {
834
+ if (arg === '--json') {
835
+ json = true;
836
+ continue;
837
+ }
838
+ if (arg === '--engine') {
839
+ auditEngine = true;
840
+ continue;
841
+ }
842
+ if (arg.startsWith('--stage=')) {
843
+ auditStage = parseAuditStage(arg.slice('--stage='.length));
844
+ continue;
845
+ }
846
+ throw new Error(`Unsupported argument "${arg}".\n\n${HELP_TEXT}`);
847
+ }
848
+ return {
849
+ command: commandRaw,
850
+ purgeArtifacts: false,
851
+ json,
852
+ auditStage: auditStage ?? 'PRE_COMMIT',
853
+ ...(auditEngine ? { auditEngine: true } : {}),
854
+ };
855
+ }
856
+
809
857
  if (commandRaw === 'loop') {
810
858
  const subcommandRaw = argv[1] ?? '';
811
859
  if (
@@ -1517,6 +1565,8 @@ const printDoctorReport = (
1517
1565
  writeInfo(
1518
1566
  `[pumuki] hook pre-push: ${report.hookStatus['pre-push'].managedBlockPresent ? 'managed' : 'missing'}`
1519
1567
  );
1568
+ printGovernanceObservationHuman(report.governanceObservation);
1569
+ printGovernanceNextActionHuman(report.governanceNextAction);
1520
1570
  writeInfo(
1521
1571
  `[pumuki] policy-as-code: PRE_COMMIT=${report.policyValidation.stages.PRE_COMMIT.validationCode ?? 'n/a'} strict=${report.policyValidation.stages.PRE_COMMIT.strict ? 'yes' : 'no'} ` +
1522
1572
  `PRE_PUSH=${report.policyValidation.stages.PRE_PUSH.validationCode ?? 'n/a'} strict=${report.policyValidation.stages.PRE_PUSH.strict ? 'yes' : 'no'} ` +
@@ -1554,17 +1604,19 @@ const printDoctorReport = (
1554
1604
  }
1555
1605
  }
1556
1606
 
1557
- const hasBlocking =
1558
- doctorHasBlockingIssues(report) || doctorHasParityMismatch(report);
1559
- const hasWarnings =
1560
- report.issues.length > 0 ||
1561
- report.deep?.checks.some((check) => check.status !== 'pass') === true;
1607
+ const hasBlocking = doctorCommandShouldFailExit(report);
1608
+ const hasWarnings = doctorCommandShouldWarnHuman(report);
1562
1609
 
1563
1610
  if (!hasWarnings && !hasBlocking) {
1564
1611
  writeInfo('[pumuki] doctor verdict: PASS');
1565
1612
  } else {
1566
1613
  writeInfo(`[pumuki] doctor verdict: ${hasBlocking ? 'BLOCKED' : 'WARN'}`);
1567
1614
  }
1615
+ if (!hasBlocking && doctorHasGovernanceAttention(report)) {
1616
+ writeInfo(
1617
+ '[pumuki] doctor: governance_effective is not GREEN (see governance truth).'
1618
+ );
1619
+ }
1568
1620
 
1569
1621
  if (remoteCiDiagnostics) {
1570
1622
  printRemoteCiDiagnostics(remoteCiDiagnostics);
@@ -2250,6 +2302,24 @@ export const runLifecycleCli = async (
2250
2302
  );
2251
2303
  return 0;
2252
2304
  }
2305
+ case 'audit': {
2306
+ const result = await runLifecycleAudit({
2307
+ stage: parsed.auditStage ?? 'PRE_COMMIT',
2308
+ auditMode: parsed.auditEngine === true ? 'engine' : 'gate',
2309
+ });
2310
+ if (parsed.json) {
2311
+ writeInfo(JSON.stringify(result, null, 2));
2312
+ } else {
2313
+ writeInfo(
2314
+ `[pumuki] audit: repo=${result.repo_root} stage=${result.stage} mode=${result.audit_mode} exit=${result.gate_exit_code}`
2315
+ );
2316
+ writeInfo(
2317
+ `[pumuki] audit: files_scanned=${result.files_scanned ?? 'n/a'} untracked_matching_extensions=${result.untracked_matching_extensions_count} outcome=${result.snapshot_outcome ?? 'n/a'}`
2318
+ );
2319
+ writeInfo(`[pumuki] audit: hint=${result.policy_reconcile_hint}`);
2320
+ }
2321
+ return result.gate_exit_code;
2322
+ }
2253
2323
  case 'doctor': {
2254
2324
  const report = runLifecycleDoctor({
2255
2325
  deep: parsed.doctorDeep === true,
@@ -2276,7 +2346,7 @@ export const runLifecycleCli = async (
2276
2346
  } else {
2277
2347
  printDoctorReport(report, remoteCiDiagnostics);
2278
2348
  }
2279
- return doctorHasBlockingIssues(report) || doctorHasParityMismatch(report) ? 1 : 0;
2349
+ return doctorCommandShouldFailExit(report) ? 1 : 0;
2280
2350
  }
2281
2351
  case 'status': {
2282
2352
  const status = readLifecycleStatus();
@@ -2329,6 +2399,8 @@ export const runLifecycleCli = async (
2329
2399
  writeInfo(
2330
2400
  `[pumuki] hooks: pre-commit=${status.hookStatus['pre-commit'].managedBlockPresent ? 'managed' : 'missing'}, pre-push=${status.hookStatus['pre-push'].managedBlockPresent ? 'managed' : 'missing'}`
2331
2401
  );
2402
+ printGovernanceObservationHuman(status.governanceObservation);
2403
+ printGovernanceNextActionHuman(status.governanceNextAction);
2332
2404
  writeInfo(
2333
2405
  `[pumuki] tracked node_modules paths: ${status.trackedNodeModulesCount}`
2334
2406
  );
@@ -5,10 +5,25 @@ import { resolvePolicyForStage } from '../gate/stagePolicies';
5
5
  import { getPumukiHooksStatus, resolvePumukiHooksDirectory } from './hookManager';
6
6
  import { LifecycleGitService, type ILifecycleGitService } from './gitService';
7
7
  import { buildLifecycleVersionReport, getCurrentPumukiVersion } from './packageInfo';
8
+ import {
9
+ readLifecycleExperimentalFeaturesSnapshot,
10
+ type LifecycleExperimentalFeaturesSnapshot,
11
+ } from './experimentalFeaturesSnapshot';
8
12
  import {
9
13
  readLifecyclePolicyValidationSnapshot,
10
14
  type LifecyclePolicyValidationSnapshot,
11
15
  } from './policyValidationSnapshot';
16
+ import {
17
+ doctorGovernanceIsBlocking,
18
+ doctorGovernanceNeedsAttention,
19
+ readGovernanceObservationSnapshot,
20
+ type GovernanceObservationSnapshot,
21
+ } from './governanceObservationSnapshot';
22
+ import {
23
+ readGovernanceNextAction,
24
+ type GovernanceNextActionReader,
25
+ type GovernanceNextActionSummary,
26
+ } from './governanceNextAction';
12
27
  import { readLifecycleState, type LifecycleState } from './state';
13
28
  import {
14
29
  detectOpenSpecInstallation,
@@ -95,6 +110,10 @@ export type LifecycleDoctorReport = {
95
110
  hooksDirectory: string;
96
111
  hooksDirectoryResolution: 'git-rev-parse' | 'git-config' | 'default';
97
112
  policyValidation: LifecyclePolicyValidationSnapshot;
113
+ experimentalFeatures: LifecycleExperimentalFeaturesSnapshot;
114
+ governanceObservation: GovernanceObservationSnapshot;
115
+ governanceNextAction: GovernanceNextActionSummary;
116
+ policy_signature_remediation?: string;
98
117
  issues: ReadonlyArray<DoctorIssue>;
99
118
  deep?: DoctorDeepReport;
100
119
  parity_profile?: DoctorParityProfile;
@@ -792,11 +811,21 @@ const compareDoctorParityProfile = (params: {
792
811
  };
793
812
  };
794
813
 
814
+ const buildPolicySignatureRemediation = (
815
+ policyValidation: LifecyclePolicyValidationSnapshot
816
+ ): string | undefined => {
817
+ const mismatch = Object.values(policyValidation.stages).some(
818
+ (stage) => stage.validationCode === 'POLICY_AS_CODE_SIGNATURE_MISMATCH'
819
+ );
820
+ return mismatch ? 'pumuki policy reconcile --apply' : undefined;
821
+ };
822
+
795
823
  export const runLifecycleDoctor = (params?: {
796
824
  cwd?: string;
797
825
  git?: ILifecycleGitService;
798
826
  deep?: boolean;
799
827
  parity?: boolean;
828
+ governanceNextActionReader?: GovernanceNextActionReader;
800
829
  }): LifecycleDoctorReport => {
801
830
  const git = params?.git ?? new LifecycleGitService();
802
831
  const cwd = params?.cwd ?? process.cwd();
@@ -824,6 +853,19 @@ export const runLifecycleDoctor = (params?: {
824
853
  repoRoot,
825
854
  lifecycleVersion: lifecycleState.version,
826
855
  });
856
+ const policyValidation = readLifecyclePolicyValidationSnapshot(repoRoot);
857
+ const experimentalFeatures = readLifecycleExperimentalFeaturesSnapshot();
858
+ const governanceObservation = readGovernanceObservationSnapshot({
859
+ repoRoot,
860
+ experimentalFeatures,
861
+ policyValidation,
862
+ git,
863
+ });
864
+ const governanceNextAction = (params?.governanceNextActionReader ?? readGovernanceNextAction)({
865
+ repoRoot,
866
+ stage: 'PRE_WRITE',
867
+ governanceObservation,
868
+ });
827
869
 
828
870
  const parity_profile =
829
871
  params?.parity === true
@@ -837,6 +879,8 @@ export const runLifecycleDoctor = (params?: {
837
879
  ? compareDoctorParityProfile({ repoRoot, actual: parity_profile })
838
880
  : undefined;
839
881
 
882
+ const policySignatureRemediation = buildPolicySignatureRemediation(policyValidation);
883
+
840
884
  return {
841
885
  repoRoot,
842
886
  packageVersion: version.effective,
@@ -846,7 +890,13 @@ export const runLifecycleDoctor = (params?: {
846
890
  hookStatus,
847
891
  hooksDirectory: hooksDirectory.path,
848
892
  hooksDirectoryResolution: hooksDirectory.source,
849
- policyValidation: readLifecyclePolicyValidationSnapshot(repoRoot),
893
+ policyValidation,
894
+ experimentalFeatures,
895
+ governanceObservation,
896
+ governanceNextAction,
897
+ ...(policySignatureRemediation
898
+ ? { policy_signature_remediation: policySignatureRemediation }
899
+ : {}),
850
900
  issues,
851
901
  deep,
852
902
  parity_profile,
@@ -859,3 +909,16 @@ export const doctorHasBlockingIssues = (report: LifecycleDoctorReport): boolean
859
909
 
860
910
  export const doctorHasParityMismatch = (report: LifecycleDoctorReport): boolean =>
861
911
  typeof report.parity_comparison !== 'undefined' && report.parity_comparison.matches === false;
912
+
913
+ export const doctorHasGovernanceAttention = (report: LifecycleDoctorReport): boolean =>
914
+ doctorGovernanceNeedsAttention(report.governanceObservation);
915
+
916
+ export const doctorCommandShouldWarnHuman = (report: LifecycleDoctorReport): boolean =>
917
+ report.issues.length > 0 ||
918
+ report.deep?.checks.some((check) => check.status !== 'pass') === true ||
919
+ doctorHasGovernanceAttention(report);
920
+
921
+ export const doctorCommandShouldFailExit = (report: LifecycleDoctorReport): boolean =>
922
+ doctorHasBlockingIssues(report) ||
923
+ doctorHasParityMismatch(report) ||
924
+ doctorGovernanceIsBlocking(report.governanceObservation);
@@ -0,0 +1,164 @@
1
+ import type { AiGateStage } from '../gate/evaluateAiGate';
2
+ import { resolveGovernanceCatalogAction } from '../gate/governanceActionCatalog';
3
+ import type { GovernanceObservationSnapshot } from './governanceObservationSnapshot';
4
+ import { writeInfo } from './cliOutputs';
5
+
6
+ export type GovernanceNextActionSummary = {
7
+ stage: AiGateStage;
8
+ phase: 'GREEN' | 'RED';
9
+ action: 'proceed' | 'ask';
10
+ confidence_pct: number;
11
+ reason_code: string;
12
+ instruction: string;
13
+ message: string;
14
+ next_action: {
15
+ kind: 'info' | 'run_command';
16
+ message: string;
17
+ command?: string;
18
+ };
19
+ };
20
+
21
+ export type GovernanceNextActionReader = (params: {
22
+ repoRoot: string;
23
+ stage?: AiGateStage;
24
+ governanceObservation: GovernanceObservationSnapshot;
25
+ }) => GovernanceNextActionSummary;
26
+
27
+ const resolveBlockedAction = (
28
+ snapshot: GovernanceObservationSnapshot,
29
+ stage: AiGateStage
30
+ ): GovernanceNextActionSummary => {
31
+ if (snapshot.attention_codes.includes('EVIDENCE_INVALID_OR_CHAIN')) {
32
+ return {
33
+ stage,
34
+ phase: 'RED',
35
+ action: 'ask',
36
+ confidence_pct: 80,
37
+ ...resolveGovernanceCatalogAction({ code: 'EVIDENCE_INVALID_OR_CHAIN', stage }),
38
+ message: 'La evidencia actual no es fiable; detén la ejecución automática hasta regenerarla.',
39
+ };
40
+ }
41
+ if (
42
+ snapshot.attention_codes.includes('AI_GATE_BLOCKED')
43
+ || snapshot.attention_codes.includes('EVIDENCE_SNAPSHOT_BLOCK')
44
+ ) {
45
+ return {
46
+ stage,
47
+ phase: 'RED',
48
+ action: 'ask',
49
+ confidence_pct: 75,
50
+ ...resolveGovernanceCatalogAction({ code: 'AI_GATE_BLOCKED', stage }),
51
+ message: 'El gate efectivo sigue bloqueado; Pumuki no debe marcar verde ni dejar continuar.',
52
+ };
53
+ }
54
+ if (snapshot.attention_codes.includes('SDD_SESSION_INVALID_OR_EXPIRED')) {
55
+ return {
56
+ stage,
57
+ phase: 'RED',
58
+ action: 'ask',
59
+ confidence_pct: 70,
60
+ ...resolveGovernanceCatalogAction({ code: 'SDD_SESSION_INVALID_OR_EXPIRED', stage }),
61
+ message: 'Hay una sesión SDD activa pero inválida; eso rompe el loop documental esperado.',
62
+ };
63
+ }
64
+ if (snapshot.attention_codes.includes('GITFLOW_PROTECTED_BRANCH_CONTEXT')) {
65
+ return {
66
+ stage,
67
+ phase: 'RED',
68
+ action: 'ask',
69
+ confidence_pct: 65,
70
+ ...resolveGovernanceCatalogAction({ code: 'GITFLOW_PROTECTED_BRANCH_CONTEXT', stage }),
71
+ message: 'El contexto actual cae sobre una rama protegida; el flujo enterprise no debe continuar ahí.',
72
+ };
73
+ }
74
+ if (
75
+ snapshot.attention_codes.some((code) => code.startsWith('POLICY_'))
76
+ || snapshot.enterprise_warn_as_block_env
77
+ ) {
78
+ return {
79
+ stage,
80
+ phase: 'RED',
81
+ action: 'ask',
82
+ confidence_pct: 60,
83
+ ...resolveGovernanceCatalogAction({ code: 'POLICY_STAGE_NOT_STRICT', stage }),
84
+ message: 'La política efectiva todavía no es estricta en todos los stages requeridos.',
85
+ };
86
+ }
87
+ if (!snapshot.contract_surface.skills_lock_json || !snapshot.contract_surface.skills_sources_json) {
88
+ return {
89
+ stage,
90
+ phase: 'RED',
91
+ action: 'ask',
92
+ confidence_pct: 55,
93
+ ...resolveGovernanceCatalogAction({ code: 'SKILLS_CONTRACT_SURFACE_INCOMPLETE', stage }),
94
+ message: 'El contrato de skills todavía no está completamente materializado en el repo.',
95
+ };
96
+ }
97
+ if (!snapshot.contract_surface.pumuki_adapter_json) {
98
+ return {
99
+ stage,
100
+ phase: 'RED',
101
+ action: 'ask',
102
+ confidence_pct: 50,
103
+ ...resolveGovernanceCatalogAction({ code: 'ADAPTER_WIRING_MISSING', stage }),
104
+ message: 'La línea base Git puede operar, pero el wiring adaptador aún no está materializado.',
105
+ };
106
+ }
107
+ if (snapshot.attention_codes.includes('EVIDENCE_SNAPSHOT_WARN')) {
108
+ return {
109
+ stage,
110
+ phase: 'RED',
111
+ action: 'ask',
112
+ confidence_pct: 50,
113
+ ...resolveGovernanceCatalogAction({ code: 'EVIDENCE_SNAPSHOT_WARN', stage }),
114
+ message: 'La evidencia está en WARN; no conviene tratar el repo como completamente verde.',
115
+ };
116
+ }
117
+ return {
118
+ stage,
119
+ phase: 'RED',
120
+ action: 'ask',
121
+ confidence_pct: 40,
122
+ ...resolveGovernanceCatalogAction({ code: 'GOVERNANCE_ATTENTION', stage }),
123
+ message: 'Todavía hay señales de governance no resueltas.',
124
+ };
125
+ };
126
+
127
+ export const readGovernanceNextAction: GovernanceNextActionReader = (params) => {
128
+ const stage = params.stage ?? 'PRE_WRITE';
129
+ const snapshot = params.governanceObservation;
130
+ if (snapshot.governance_effective === 'green') {
131
+ return {
132
+ stage,
133
+ phase: 'GREEN',
134
+ action: 'proceed',
135
+ confidence_pct: 90,
136
+ ...resolveGovernanceCatalogAction({ code: 'READY', stage }),
137
+ message: 'Governance efectiva en verde: continúa con la implementación mínima.',
138
+ };
139
+ }
140
+ return resolveBlockedAction(snapshot, stage);
141
+ };
142
+
143
+ export const buildGovernanceNextActionSummaryLines = (
144
+ snapshot: GovernanceNextActionSummary
145
+ ): string[] => {
146
+ const lines = [
147
+ `Next action: stage=${snapshot.stage} phase=${snapshot.phase} action=${snapshot.action} confidence=${snapshot.confidence_pct}% reason=${snapshot.reason_code}`,
148
+ `Instruction: ${snapshot.instruction}`,
149
+ `Action detail: ${snapshot.next_action.message}`,
150
+ ];
151
+ if (snapshot.next_action.command) {
152
+ lines.push(`Command: ${snapshot.next_action.command}`);
153
+ }
154
+ return lines;
155
+ };
156
+
157
+ export const printGovernanceNextActionHuman = (
158
+ snapshot: GovernanceNextActionSummary
159
+ ): void => {
160
+ writeInfo('[pumuki] governance next action (S1 / governance console baseline):');
161
+ for (const line of buildGovernanceNextActionSummaryLines(snapshot)) {
162
+ writeInfo(`[pumuki] ${line}`);
163
+ }
164
+ };