pumuki 6.3.72 → 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.
- package/CHANGELOG.md +20 -0
- package/VERSION +1 -1
- package/docs/README.md +1 -1
- package/docs/operations/RELEASE_NOTES.md +12 -1
- package/docs/product/USAGE.md +2 -5
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +56 -105
- package/integrations/gate/governanceActionCatalog.ts +230 -0
- package/integrations/git/runPlatformGate.ts +9 -1
- package/integrations/git/runPlatformGateFacts.ts +1 -7
- package/integrations/git/runPlatformGateOutput.ts +36 -27
- package/integrations/lifecycle/adapter.templates.json +3 -0
- package/integrations/lifecycle/audit.ts +101 -0
- package/integrations/lifecycle/cli.ts +80 -8
- package/integrations/lifecycle/doctor.ts +64 -1
- package/integrations/lifecycle/governanceNextAction.ts +164 -0
- package/integrations/lifecycle/governanceObservationSnapshot.ts +288 -0
- package/integrations/lifecycle/index.ts +2 -0
- package/integrations/lifecycle/status.ts +29 -2
- package/integrations/mcp/autoExecuteAiStart.ts +86 -84
- package/integrations/mcp/preFlightCheck.ts +41 -5
- package/integrations/platform/detectPlatforms.ts +37 -0
- package/package.json +8 -1
- package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
- package/scripts/consumer-menu-matrix-baseline-report-lib.ts +38 -13
- package/scripts/consumer-postinstall-resolve-args.cjs +38 -0
- package/scripts/consumer-postinstall.cjs +10 -1
- package/scripts/framework-menu-consumer-actions-lib.ts +4 -28
- package/scripts/framework-menu-consumer-preflight-hints.ts +2 -5
- package/scripts/framework-menu-consumer-preflight-render.ts +6 -0
- package/scripts/framework-menu-consumer-preflight-run.ts +19 -0
- package/scripts/framework-menu-consumer-preflight-types.ts +8 -0
- package/scripts/framework-menu-consumer-runtime-actions.ts +6 -86
- package/scripts/framework-menu-consumer-runtime-audit.ts +2 -36
- package/scripts/framework-menu-consumer-runtime-lib.ts +0 -2
- package/scripts/framework-menu-consumer-runtime-types.ts +1 -3
- package/scripts/framework-menu-evidence-summary-lib.ts +0 -1
- package/scripts/framework-menu-evidence-summary-read.ts +5 -57
- package/scripts/framework-menu-evidence-summary-severity.ts +1 -3
- package/scripts/framework-menu-evidence-summary-types.ts +0 -7
- package/scripts/framework-menu-gate-lib.ts +0 -9
- package/scripts/framework-menu-layout-data.ts +0 -5
- package/scripts/framework-menu-matrix-baseline-lib.ts +14 -15
- package/scripts/framework-menu-matrix-canary-lib.ts +1 -22
- package/scripts/framework-menu-matrix-evidence-lib.ts +0 -1
- package/scripts/framework-menu-matrix-evidence-types.ts +1 -13
- package/scripts/framework-menu-matrix-runner-lib.ts +0 -35
- package/scripts/framework-menu-system-notifications-macos.ts +0 -4
- package/scripts/framework-menu.ts +0 -3
- package/scripts/pumuki-full-surface-smoke-lib.ts +37 -0
- package/scripts/pumuki-full-surface-smoke.ts +261 -0
- package/scripts/pumuki-smoke-installed-wrapper.cjs +31 -0
- package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +0 -140
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
import type { Finding } from '../../core/gate/Finding';
|
|
2
|
-
|
|
3
|
-
const BLOCK_NEXT_ACTION_BY_CODE: Readonly<Record<string, string>> = {
|
|
4
|
-
SDD_SESSION_MISSING:
|
|
5
|
-
'npx --yes --package pumuki@latest pumuki sdd session --open --change=<id>',
|
|
6
|
-
SDD_SESSION_INVALID:
|
|
7
|
-
'npx --yes --package pumuki@latest pumuki sdd session --refresh --ttl-minutes=90',
|
|
8
|
-
PRE_PUSH_UPSTREAM_MISSING:
|
|
9
|
-
'git push --set-upstream origin <branch>',
|
|
10
|
-
PRE_PUSH_UPSTREAM_MISALIGNED:
|
|
11
|
-
'git branch --unset-upstream && git push --set-upstream origin <branch>',
|
|
12
|
-
EVIDENCE_STALE:
|
|
13
|
-
'npx --yes --package pumuki@latest pumuki-pre-commit',
|
|
14
|
-
EVIDENCE_BRANCH_MISMATCH:
|
|
15
|
-
'npx --yes --package pumuki@latest pumuki-pre-commit',
|
|
16
|
-
GIT_ATOMICITY_TOO_MANY_FILES:
|
|
17
|
-
'git restore --staged . && separa cambios en commits más pequeños',
|
|
18
|
-
GIT_ATOMICITY_TOO_MANY_SCOPES:
|
|
19
|
-
'Revisa scope_files del bloqueo y aplica: git restore --staged . && git add <scope>/ && git commit -m "<tipo>: <scope>"',
|
|
20
|
-
ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
|
|
21
|
-
'Reconcilia policy/skills y reintenta PRE_COMMIT: npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki-pre-commit',
|
|
22
|
-
SKILLS_SKILLS_FRONTEND_NO_SOLID_VIOLATIONS:
|
|
23
|
-
'Aplica refactor incremental: extrae 1 componente/hook por commit y vuelve a ejecutar PRE_COMMIT.',
|
|
24
|
-
};
|
|
2
|
+
import { resolveGovernanceCatalogAction } from '../gate/governanceActionCatalog';
|
|
25
3
|
|
|
26
4
|
const severityWeight = (severity: string): number => {
|
|
27
5
|
switch (severity.toUpperCase()) {
|
|
@@ -94,18 +72,49 @@ const formatFinding = (finding: Finding): string => {
|
|
|
94
72
|
return `[${severity}] ${finding.ruleId}: ${finding.message} -> ${location}`;
|
|
95
73
|
};
|
|
96
74
|
|
|
97
|
-
|
|
75
|
+
const resolveAtomicityFallback = (code: string): string | null => {
|
|
76
|
+
switch (code) {
|
|
77
|
+
case 'GIT_ATOMICITY_TOO_MANY_FILES':
|
|
78
|
+
return 'git restore --staged . && separa cambios en commits más pequeños';
|
|
79
|
+
case 'GIT_ATOMICITY_TOO_MANY_SCOPES':
|
|
80
|
+
return 'Revisa scope_files del bloqueo y aplica: git restore --staged . && git add <scope>/ && git commit -m "<tipo>: <scope>"';
|
|
81
|
+
default:
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const printGateFindings = (
|
|
87
|
+
findings: ReadonlyArray<Finding>,
|
|
88
|
+
params?: { stage?: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI' }
|
|
89
|
+
): void => {
|
|
98
90
|
if (findings.length === 0) {
|
|
99
91
|
return;
|
|
100
92
|
}
|
|
101
93
|
const primary = resolvePrimaryFinding(findings);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
const action = resolveGovernanceCatalogAction({
|
|
95
|
+
code: primary.code,
|
|
96
|
+
stage: params?.stage ?? 'PRE_COMMIT',
|
|
97
|
+
fallback: {
|
|
98
|
+
reason_code: primary.code,
|
|
99
|
+
instruction: 'Corrige el bloqueante primario y vuelve a ejecutar la validación del stage actual.',
|
|
100
|
+
next_action: {
|
|
101
|
+
kind: 'info',
|
|
102
|
+
message:
|
|
103
|
+
resolveAtomicityFallback(primary.code)
|
|
104
|
+
?? 'Corrige el bloqueante primario y vuelve a ejecutar el mismo comando.',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const nextAction = action.next_action.command ?? action.next_action.message;
|
|
105
109
|
process.stdout.write(
|
|
106
110
|
`[pumuki][block-summary] primary=${primary.code} severity=${primary.severity.toUpperCase()} rule=${primary.ruleId}\n`
|
|
107
111
|
);
|
|
112
|
+
process.stdout.write(`[pumuki][block-summary] reason_code=${action.reason_code}\n`);
|
|
113
|
+
process.stdout.write(`[pumuki][block-summary] instruction=${action.instruction}\n`);
|
|
108
114
|
process.stdout.write(`[pumuki][block-summary] next_action=${nextAction}\n`);
|
|
115
|
+
if (action.next_action.command) {
|
|
116
|
+
process.stdout.write(`[pumuki][block-summary] command=${action.next_action.command}\n`);
|
|
117
|
+
}
|
|
109
118
|
for (const finding of findings) {
|
|
110
119
|
process.stdout.write(`${formatFinding(finding)}\n`);
|
|
111
120
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"codex": [
|
|
3
3
|
{
|
|
4
4
|
"path": ".pumuki/adapter.json",
|
|
5
|
+
"mode": "json-merge",
|
|
5
6
|
"payload": {
|
|
6
7
|
"hooks": {
|
|
7
8
|
"pre_write": {
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"repo": [
|
|
32
33
|
{
|
|
33
34
|
"path": ".pumuki/adapter.json",
|
|
35
|
+
"mode": "json-merge",
|
|
34
36
|
"payload": {
|
|
35
37
|
"hooks": {
|
|
36
38
|
"pre_write": {
|
|
@@ -143,6 +145,7 @@
|
|
|
143
145
|
},
|
|
144
146
|
{
|
|
145
147
|
"path": ".pumuki/adapter.json",
|
|
148
|
+
"mode": "json-merge",
|
|
146
149
|
"payload": {
|
|
147
150
|
"hooks": {
|
|
148
151
|
"pre_write": {
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
+
};
|