pumuki 6.3.26 → 6.3.28

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 (76) hide show
  1. package/README.md +3 -1
  2. package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
  3. package/bin/pumuki-mcp-evidence-stdio.js +5 -0
  4. package/core/gate/conditionMatches.ts +1 -21
  5. package/core/gate/evaluateGate.js +5 -0
  6. package/core/gate/evaluateRules.js +5 -0
  7. package/core/gate/evaluateRules.ts +1 -24
  8. package/core/gate/scopeMatcher.ts +84 -0
  9. package/docs/EXECUTION_BOARD.md +749 -376
  10. package/docs/MCP_SERVERS.md +41 -2
  11. package/docs/README.md +6 -2
  12. package/docs/REFRACTOR_PROGRESS.md +374 -6
  13. package/docs/validation/README.md +11 -1
  14. package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
  15. package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
  16. package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
  17. package/integrations/config/skillsCompliance.ts +212 -0
  18. package/integrations/evidence/integrity.ts +352 -0
  19. package/integrations/evidence/rulesCoverage.ts +94 -0
  20. package/integrations/evidence/schema.test.ts +16 -0
  21. package/integrations/evidence/schema.ts +41 -0
  22. package/integrations/evidence/writeEvidence.test.ts +68 -0
  23. package/integrations/evidence/writeEvidence.ts +23 -2
  24. package/integrations/gate/evaluateAiGate.ts +382 -15
  25. package/integrations/gate/stagePolicies.ts +70 -15
  26. package/integrations/gate/waivers.ts +209 -0
  27. package/integrations/git/findingTraceability.ts +3 -23
  28. package/integrations/git/index.js +5 -0
  29. package/integrations/git/runCliCommand.ts +16 -0
  30. package/integrations/git/runPlatformGate.ts +53 -1
  31. package/integrations/git/runPlatformGateEvaluation.ts +13 -0
  32. package/integrations/git/stageRunners.ts +168 -5
  33. package/integrations/lifecycle/adapter.templates.json +72 -5
  34. package/integrations/lifecycle/adapter.ts +78 -4
  35. package/integrations/lifecycle/cli.ts +384 -14
  36. package/integrations/lifecycle/doctor.ts +534 -0
  37. package/integrations/lifecycle/hookBlock.ts +2 -1
  38. package/integrations/lifecycle/index.js +5 -0
  39. package/integrations/lifecycle/install.ts +115 -3
  40. package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
  41. package/integrations/lifecycle/preWriteAutomation.ts +142 -0
  42. package/integrations/mcp/aiGateCheck.ts +6 -0
  43. package/integrations/mcp/aiGateReceipt.ts +188 -0
  44. package/integrations/mcp/enterpriseServer.ts +14 -1
  45. package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
  46. package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
  47. package/integrations/mcp/index.js +5 -0
  48. package/integrations/sdd/index.js +5 -0
  49. package/integrations/sdd/index.ts +2 -0
  50. package/integrations/sdd/policy.ts +191 -2
  51. package/integrations/sdd/sessionStore.ts +139 -19
  52. package/integrations/sdd/syncDocs.ts +180 -0
  53. package/integrations/sdd/types.ts +4 -1
  54. package/integrations/telemetry/structuredTelemetry.ts +197 -0
  55. package/package.json +27 -8
  56. package/scripts/build-p9-validation-manifests.ts +53 -0
  57. package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
  58. package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
  59. package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
  60. package/scripts/check-p9-ruralgo-install-health.ts +288 -0
  61. package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
  62. package/scripts/check-package-manifest.ts +49 -0
  63. package/scripts/check-tracking-single-active.sh +40 -0
  64. package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
  65. package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
  66. package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
  67. package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
  68. package/scripts/manage-library.sh +1 -1
  69. package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
  70. package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
  71. package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
  72. package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
  73. package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
  74. package/scripts/p9-validation-manifests-lib.ts +366 -0
  75. package/scripts/package-manifest-lib.ts +9 -0
  76. package/skills.lock.json +1 -1
@@ -0,0 +1,198 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import process from 'node:process';
5
+ import {
6
+ evaluateP9RuralgoBaselineVersioned,
7
+ type P9RuralgoBaselineVersionedPolicy,
8
+ } from './p9-ruralgo-baseline-versioned-lib';
9
+
10
+ type CliOptions = {
11
+ repoPath: string;
12
+ json: boolean;
13
+ policy: P9RuralgoBaselineVersionedPolicy;
14
+ };
15
+
16
+ type GitRun = {
17
+ ok: boolean;
18
+ status: number;
19
+ stdout: string;
20
+ stderr: string;
21
+ };
22
+
23
+ const DEFAULT_REPO_PATH =
24
+ '/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork';
25
+
26
+ const OPENSPEC_PROJECT = 'openspec/project.md';
27
+ const OPENSPEC_ARCHIVE_GITKEEP = 'openspec/changes/archive/.gitkeep';
28
+ const OPENSPEC_SPECS_GITKEEP = 'openspec/specs/.gitkeep';
29
+ const AI_EVIDENCE_FILE = '.ai_evidence.json';
30
+
31
+ const printHelp = (): void => {
32
+ process.stdout.write(
33
+ [
34
+ 'Uso:',
35
+ ' node --import tsx scripts/check-p9-ruralgo-baseline-versioned.ts [opciones]',
36
+ '',
37
+ 'Opciones:',
38
+ ' --repo=<path> Ruta del repo ruralgo-fork (default: ~/Developer/Projects/ruralgo-fork)',
39
+ ' --allow-missing Permite ausencia de artifacts baseline',
40
+ ' --allow-untracked Permite artifacts sin trackear',
41
+ ' --json Salida JSON',
42
+ ' --help Muestra ayuda',
43
+ '',
44
+ 'Exit code:',
45
+ ' 0 => baseline versionado para F1.T4',
46
+ ' 1 => baseline no versionado',
47
+ '',
48
+ ].join('\n'),
49
+ );
50
+ };
51
+
52
+ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
53
+ const options: CliOptions = {
54
+ repoPath: DEFAULT_REPO_PATH,
55
+ json: false,
56
+ policy: {
57
+ requireExists: true,
58
+ requireTracked: true,
59
+ },
60
+ };
61
+
62
+ for (const arg of argv) {
63
+ if (arg === '--json') {
64
+ options.json = true;
65
+ continue;
66
+ }
67
+ if (arg === '--allow-missing') {
68
+ options.policy.requireExists = false;
69
+ continue;
70
+ }
71
+ if (arg === '--allow-untracked') {
72
+ options.policy.requireTracked = false;
73
+ continue;
74
+ }
75
+ if (arg.startsWith('--repo=')) {
76
+ options.repoPath = arg.slice('--repo='.length);
77
+ continue;
78
+ }
79
+ throw new Error(`Argumento no soportado: ${arg}`);
80
+ }
81
+
82
+ return options;
83
+ };
84
+
85
+ const runGit = (repoPath: string, args: ReadonlyArray<string>): GitRun => {
86
+ try {
87
+ const stdout = execFileSync('git', ['-C', repoPath, ...args], {
88
+ encoding: 'utf8',
89
+ stdio: ['ignore', 'pipe', 'pipe'],
90
+ }).trim();
91
+ return { ok: true, status: 0, stdout, stderr: '' };
92
+ } catch (error) {
93
+ const err = error as {
94
+ status?: number;
95
+ stdout?: string | Buffer;
96
+ stderr?: string | Buffer;
97
+ };
98
+ return {
99
+ ok: false,
100
+ status: typeof err.status === 'number' ? err.status : 1,
101
+ stdout: String(err.stdout ?? '').trim(),
102
+ stderr: String(err.stderr ?? '').trim(),
103
+ };
104
+ }
105
+ };
106
+
107
+ const isTrackedByGit = (repoPath: string, relativePath: string): boolean =>
108
+ runGit(repoPath, ['ls-files', '--error-unmatch', '--', relativePath]).ok;
109
+
110
+ const main = (): number => {
111
+ const argv = process.argv.slice(2);
112
+ if (argv.includes('--help')) {
113
+ printHelp();
114
+ return 0;
115
+ }
116
+
117
+ let options: CliOptions;
118
+ try {
119
+ options = parseArgs(argv);
120
+ } catch (error) {
121
+ process.stderr.write(`[p9][baseline-versioned] ${(error as Error).message}\n`);
122
+ process.stderr.write('Usa --help para ver opciones.\n');
123
+ return 1;
124
+ }
125
+
126
+ const isGitRepo = runGit(options.repoPath, ['rev-parse', '--is-inside-work-tree']).stdout === 'true';
127
+
128
+ const openspecProjectExists = existsSync(join(options.repoPath, OPENSPEC_PROJECT));
129
+ const openspecArchiveGitkeepExists = existsSync(join(options.repoPath, OPENSPEC_ARCHIVE_GITKEEP));
130
+ const openspecSpecsGitkeepExists = existsSync(join(options.repoPath, OPENSPEC_SPECS_GITKEEP));
131
+ const aiEvidenceExists = existsSync(join(options.repoPath, AI_EVIDENCE_FILE));
132
+
133
+ const result = evaluateP9RuralgoBaselineVersioned(
134
+ {
135
+ isGitRepo,
136
+ openspecProjectExists,
137
+ openspecArchiveGitkeepExists,
138
+ openspecSpecsGitkeepExists,
139
+ aiEvidenceExists,
140
+ openspecProjectTracked: isGitRepo ? isTrackedByGit(options.repoPath, OPENSPEC_PROJECT) : false,
141
+ openspecArchiveGitkeepTracked: isGitRepo
142
+ ? isTrackedByGit(options.repoPath, OPENSPEC_ARCHIVE_GITKEEP)
143
+ : false,
144
+ openspecSpecsGitkeepTracked: isGitRepo
145
+ ? isTrackedByGit(options.repoPath, OPENSPEC_SPECS_GITKEEP)
146
+ : false,
147
+ aiEvidenceTracked: isGitRepo ? isTrackedByGit(options.repoPath, AI_EVIDENCE_FILE) : false,
148
+ },
149
+ options.policy,
150
+ );
151
+
152
+ const payload = {
153
+ ready: result.ready,
154
+ repoPath: options.repoPath,
155
+ policy: options.policy,
156
+ snapshot: {
157
+ isGitRepo,
158
+ openspecProjectExists,
159
+ openspecArchiveGitkeepExists,
160
+ openspecSpecsGitkeepExists,
161
+ aiEvidenceExists,
162
+ openspecProjectTracked: isGitRepo ? isTrackedByGit(options.repoPath, OPENSPEC_PROJECT) : false,
163
+ openspecArchiveGitkeepTracked: isGitRepo
164
+ ? isTrackedByGit(options.repoPath, OPENSPEC_ARCHIVE_GITKEEP)
165
+ : false,
166
+ openspecSpecsGitkeepTracked: isGitRepo
167
+ ? isTrackedByGit(options.repoPath, OPENSPEC_SPECS_GITKEEP)
168
+ : false,
169
+ aiEvidenceTracked: isGitRepo ? isTrackedByGit(options.repoPath, AI_EVIDENCE_FILE) : false,
170
+ },
171
+ issues: result.issues,
172
+ };
173
+
174
+ if (options.json) {
175
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
176
+ } else {
177
+ process.stdout.write('[p9][baseline-versioned] PRECHECK\n');
178
+ process.stdout.write(`[p9][baseline-versioned] repo=${payload.repoPath}\n`);
179
+ process.stdout.write(
180
+ `[p9][baseline-versioned] openspec_exists(project/archive/specs)=${payload.snapshot.openspecProjectExists}/${payload.snapshot.openspecArchiveGitkeepExists}/${payload.snapshot.openspecSpecsGitkeepExists}\n`,
181
+ );
182
+ process.stdout.write(
183
+ `[p9][baseline-versioned] tracked(project/archive/specs/ai_evidence)=${payload.snapshot.openspecProjectTracked}/${payload.snapshot.openspecArchiveGitkeepTracked}/${payload.snapshot.openspecSpecsGitkeepTracked}/${payload.snapshot.aiEvidenceTracked}\n`,
184
+ );
185
+ process.stdout.write(
186
+ `[p9][baseline-versioned] status=${payload.ready ? 'READY' : 'NOT_READY'} issues=${payload.issues.length}\n`,
187
+ );
188
+ for (const issue of payload.issues) {
189
+ process.stdout.write(
190
+ `[p9][baseline-versioned][${issue.severity}] ${issue.code}: ${issue.message}\n`,
191
+ );
192
+ }
193
+ }
194
+
195
+ return payload.ready ? 0 : 1;
196
+ };
197
+
198
+ process.exitCode = main();
@@ -0,0 +1,215 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import process from 'node:process';
3
+ import {
4
+ evaluateP9RuralgoBranchReadiness,
5
+ type P9RuralgoBranchReadinessPolicy,
6
+ } from './p9-ruralgo-branch-ready-lib';
7
+
8
+ type CliOptions = {
9
+ repoPath: string;
10
+ expectedBranch: string;
11
+ baseRef: string;
12
+ json: boolean;
13
+ policy: P9RuralgoBranchReadinessPolicy;
14
+ };
15
+
16
+ type GitRun = {
17
+ ok: boolean;
18
+ status: number;
19
+ stdout: string;
20
+ stderr: string;
21
+ };
22
+
23
+ const DEFAULT_REPO_PATH =
24
+ '/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork';
25
+
26
+ const printHelp = (): void => {
27
+ process.stdout.write(
28
+ [
29
+ 'Uso:',
30
+ ' node --import tsx scripts/check-p9-ruralgo-branch-ready.ts [opciones]',
31
+ '',
32
+ 'Opciones:',
33
+ ' --repo=<path> Ruta del repo ruralgo-fork (default: ~/Developer/Projects/ruralgo-fork)',
34
+ ' --expected-branch=<name> Rama esperada (default: feature/p9-api-contact-contract)',
35
+ ' --base-ref=<git-ref> Base esperada (default: origin/develop)',
36
+ ' --allow-upstream Permite upstream ya configurado',
37
+ ' --allow-dirty Permite worktree con cambios',
38
+ ' --allow-nonfeature-name Permite nombres fuera de feature/<kebab-case>',
39
+ ' --json Salida JSON',
40
+ ' --help Muestra ayuda',
41
+ '',
42
+ 'Exit code:',
43
+ ' 0 => rama lista para P9.F0.T3.ST2',
44
+ ' 1 => no lista (revisar issues)',
45
+ '',
46
+ ].join('\n'),
47
+ );
48
+ };
49
+
50
+ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
51
+ const options: CliOptions = {
52
+ repoPath: DEFAULT_REPO_PATH,
53
+ expectedBranch: 'feature/p9-api-contact-contract',
54
+ baseRef: 'origin/develop',
55
+ json: false,
56
+ policy: {
57
+ requireNoUpstream: true,
58
+ requireCleanWorktree: true,
59
+ requireFeatureNaming: true,
60
+ },
61
+ };
62
+
63
+ for (const arg of argv) {
64
+ if (arg === '--json') {
65
+ options.json = true;
66
+ continue;
67
+ }
68
+ if (arg === '--allow-upstream') {
69
+ options.policy.requireNoUpstream = false;
70
+ continue;
71
+ }
72
+ if (arg === '--allow-dirty') {
73
+ options.policy.requireCleanWorktree = false;
74
+ continue;
75
+ }
76
+ if (arg === '--allow-nonfeature-name') {
77
+ options.policy.requireFeatureNaming = false;
78
+ continue;
79
+ }
80
+ if (arg.startsWith('--repo=')) {
81
+ options.repoPath = arg.slice('--repo='.length);
82
+ continue;
83
+ }
84
+ if (arg.startsWith('--expected-branch=')) {
85
+ options.expectedBranch = arg.slice('--expected-branch='.length);
86
+ continue;
87
+ }
88
+ if (arg.startsWith('--base-ref=')) {
89
+ options.baseRef = arg.slice('--base-ref='.length);
90
+ continue;
91
+ }
92
+ throw new Error(`Argumento no soportado: ${arg}`);
93
+ }
94
+
95
+ return options;
96
+ };
97
+
98
+ const runGit = (repoPath: string, args: ReadonlyArray<string>): GitRun => {
99
+ try {
100
+ const stdout = execFileSync('git', ['-C', repoPath, ...args], {
101
+ encoding: 'utf8',
102
+ stdio: ['ignore', 'pipe', 'pipe'],
103
+ }).trim();
104
+ return { ok: true, status: 0, stdout, stderr: '' };
105
+ } catch (error) {
106
+ const err = error as {
107
+ status?: number;
108
+ stdout?: string | Buffer;
109
+ stderr?: string | Buffer;
110
+ };
111
+ return {
112
+ ok: false,
113
+ status: typeof err.status === 'number' ? err.status : 1,
114
+ stdout: String(err.stdout ?? '').trim(),
115
+ stderr: String(err.stderr ?? '').trim(),
116
+ };
117
+ }
118
+ };
119
+
120
+ const main = (): number => {
121
+ const argv = process.argv.slice(2);
122
+ if (argv.includes('--help')) {
123
+ printHelp();
124
+ return 0;
125
+ }
126
+
127
+ let options: CliOptions;
128
+ try {
129
+ options = parseArgs(argv);
130
+ } catch (error) {
131
+ process.stderr.write(`[p9][branch-ready] ${(error as Error).message}\n`);
132
+ process.stderr.write('Usa --help para ver opciones.\n');
133
+ return 1;
134
+ }
135
+
136
+ const isGitRepoCheck = runGit(options.repoPath, ['rev-parse', '--is-inside-work-tree']);
137
+ const isGitRepo = isGitRepoCheck.ok && isGitRepoCheck.stdout === 'true';
138
+ const currentBranch = isGitRepo
139
+ ? runGit(options.repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']).stdout || null
140
+ : null;
141
+
142
+ const upstreamCheck = isGitRepo
143
+ ? runGit(
144
+ options.repoPath,
145
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'],
146
+ )
147
+ : { ok: false, status: 1, stdout: '', stderr: '' };
148
+ const upstreamRef = upstreamCheck.ok ? upstreamCheck.stdout : null;
149
+
150
+ const statusCheck = isGitRepo ? runGit(options.repoPath, ['status', '--porcelain']) : null;
151
+ const worktreeDirty = Boolean(statusCheck?.stdout);
152
+
153
+ const baseCheck = isGitRepo
154
+ ? runGit(options.repoPath, ['merge-base', '--is-ancestor', options.baseRef, 'HEAD'])
155
+ : { ok: false, status: 2, stdout: '', stderr: '' };
156
+ const baseReachable =
157
+ baseCheck.status === 0 ? true : baseCheck.status === 1 ? false : null;
158
+
159
+ const result = evaluateP9RuralgoBranchReadiness(
160
+ {
161
+ repoPath: options.repoPath,
162
+ expectedBranch: options.expectedBranch,
163
+ currentBranch,
164
+ upstreamRef,
165
+ baseRef: options.baseRef,
166
+ baseReachable,
167
+ worktreeDirty,
168
+ isGitRepo,
169
+ },
170
+ options.policy,
171
+ );
172
+
173
+ const payload = {
174
+ ready: result.ready,
175
+ repoPath: options.repoPath,
176
+ expectedBranch: options.expectedBranch,
177
+ currentBranch,
178
+ upstreamRef,
179
+ baseRef: options.baseRef,
180
+ baseReachable,
181
+ worktreeDirty,
182
+ policy: options.policy,
183
+ issues: result.issues,
184
+ };
185
+
186
+ if (options.json) {
187
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
188
+ } else {
189
+ process.stdout.write('[p9][branch-ready] PRECHECK\n');
190
+ process.stdout.write(`[p9][branch-ready] repo=${payload.repoPath}\n`);
191
+ process.stdout.write(`[p9][branch-ready] expected_branch=${payload.expectedBranch}\n`);
192
+ process.stdout.write(`[p9][branch-ready] current_branch=${payload.currentBranch ?? 'n/a'}\n`);
193
+ process.stdout.write(`[p9][branch-ready] upstream=${payload.upstreamRef ?? 'none'}\n`);
194
+ process.stdout.write(`[p9][branch-ready] base_ref=${payload.baseRef}\n`);
195
+ process.stdout.write(
196
+ `[p9][branch-ready] base_reachable=${String(payload.baseReachable)} worktree_dirty=${String(
197
+ payload.worktreeDirty,
198
+ )}\n`,
199
+ );
200
+ process.stdout.write(
201
+ `[p9][branch-ready] status=${payload.ready ? 'READY' : 'NOT_READY'} issues=${
202
+ payload.issues.length
203
+ }\n`,
204
+ );
205
+ for (const issue of payload.issues) {
206
+ process.stdout.write(
207
+ `[p9][branch-ready][${issue.severity}] ${issue.code}: ${issue.message}\n`,
208
+ );
209
+ }
210
+ }
211
+
212
+ return payload.ready ? 0 : 1;
213
+ };
214
+
215
+ process.exitCode = main();
@@ -0,0 +1,288 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import process from 'node:process';
5
+ import {
6
+ evaluateP9RuralgoInstallHealth,
7
+ type P9RuralgoInstallHealthPolicy,
8
+ } from './p9-ruralgo-install-health-lib';
9
+
10
+ type CliOptions = {
11
+ repoPath: string;
12
+ json: boolean;
13
+ policy: P9RuralgoInstallHealthPolicy;
14
+ };
15
+
16
+ type CommandResult = {
17
+ ok: boolean;
18
+ status: number;
19
+ stdout: string;
20
+ stderr: string;
21
+ };
22
+
23
+ type PumukiStatusPayload = {
24
+ lifecycleState?: {
25
+ installed?: string | boolean;
26
+ };
27
+ hookStatus?: {
28
+ 'pre-commit'?: { exists?: boolean; managedBlockPresent?: boolean };
29
+ 'pre-push'?: { exists?: boolean; managedBlockPresent?: boolean };
30
+ };
31
+ };
32
+
33
+ const DEFAULT_REPO_PATH =
34
+ '/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork';
35
+
36
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
37
+ const localPumukiCliPath = resolve(scriptDir, '../bin/pumuki.js');
38
+
39
+ const printHelp = (): void => {
40
+ process.stdout.write(
41
+ [
42
+ 'Uso:',
43
+ ' node --import tsx scripts/check-p9-ruralgo-install-health.ts [opciones]',
44
+ '',
45
+ 'Opciones:',
46
+ ' --repo=<path> Ruta del repo ruralgo-fork (default: ~/Developer/Projects/ruralgo-fork)',
47
+ ' --allow-not-installed Permite lifecycle no instalado',
48
+ ' --allow-unmanaged-hooks Permite hooks no gestionados',
49
+ ' --allow-doctor-fail No exige doctor PASS',
50
+ ' --json Salida JSON',
51
+ ' --help Muestra ayuda',
52
+ '',
53
+ 'Exit code:',
54
+ ' 0 => instalación saludable para F1.T3',
55
+ ' 1 => instalación no saludable',
56
+ '',
57
+ ].join('\n'),
58
+ );
59
+ };
60
+
61
+ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
62
+ const options: CliOptions = {
63
+ repoPath: DEFAULT_REPO_PATH,
64
+ json: false,
65
+ policy: {
66
+ requireInstalled: true,
67
+ requireManagedHooks: true,
68
+ requireDoctorPass: true,
69
+ },
70
+ };
71
+
72
+ for (const arg of argv) {
73
+ if (arg === '--json') {
74
+ options.json = true;
75
+ continue;
76
+ }
77
+ if (arg === '--allow-not-installed') {
78
+ options.policy.requireInstalled = false;
79
+ continue;
80
+ }
81
+ if (arg === '--allow-unmanaged-hooks') {
82
+ options.policy.requireManagedHooks = false;
83
+ continue;
84
+ }
85
+ if (arg === '--allow-doctor-fail') {
86
+ options.policy.requireDoctorPass = false;
87
+ continue;
88
+ }
89
+ if (arg.startsWith('--repo=')) {
90
+ options.repoPath = arg.slice('--repo='.length);
91
+ continue;
92
+ }
93
+ throw new Error(`Argumento no soportado: ${arg}`);
94
+ }
95
+
96
+ return options;
97
+ };
98
+
99
+ const runCommand = (
100
+ command: string,
101
+ args: ReadonlyArray<string>,
102
+ cwd: string,
103
+ ): CommandResult => {
104
+ try {
105
+ const stdout = execFileSync(command, args, {
106
+ cwd,
107
+ encoding: 'utf8',
108
+ stdio: ['ignore', 'pipe', 'pipe'],
109
+ });
110
+ return {
111
+ ok: true,
112
+ status: 0,
113
+ stdout: stdout.trim(),
114
+ stderr: '',
115
+ };
116
+ } catch (error) {
117
+ const err = error as {
118
+ status?: number;
119
+ stdout?: string | Buffer;
120
+ stderr?: string | Buffer;
121
+ };
122
+ return {
123
+ ok: false,
124
+ status: typeof err.status === 'number' ? err.status : 1,
125
+ stdout: String(err.stdout ?? '').trim(),
126
+ stderr: String(err.stderr ?? '').trim(),
127
+ };
128
+ }
129
+ };
130
+
131
+ const parseLifecycleInstalled = (value: string | boolean | undefined): boolean | null => {
132
+ if (typeof value === 'boolean') {
133
+ return value;
134
+ }
135
+ if (typeof value === 'string') {
136
+ if (value === 'true') {
137
+ return true;
138
+ }
139
+ if (value === 'false') {
140
+ return false;
141
+ }
142
+ }
143
+ return null;
144
+ };
145
+
146
+ const parseDoctorVerdictPass = (stdout: string): boolean | null => {
147
+ if (!stdout) {
148
+ return null;
149
+ }
150
+ const maybeJson = stdout.trim();
151
+ if (maybeJson.startsWith('{')) {
152
+ try {
153
+ const parsed = JSON.parse(maybeJson) as { verdict?: string; issues?: unknown[] };
154
+ if (typeof parsed.verdict === 'string') {
155
+ return parsed.verdict.toUpperCase() === 'PASS';
156
+ }
157
+ if (Array.isArray(parsed.issues)) {
158
+ return parsed.issues.length === 0;
159
+ }
160
+ } catch {
161
+ // fallback to regex text parsing
162
+ }
163
+ }
164
+ const match = stdout.match(/doctor verdict:\s*([A-Z]+)/i);
165
+ if (!match) {
166
+ return null;
167
+ }
168
+ return match[1]?.toUpperCase() === 'PASS';
169
+ };
170
+
171
+ const main = (): number => {
172
+ const argv = process.argv.slice(2);
173
+ if (argv.includes('--help')) {
174
+ printHelp();
175
+ return 0;
176
+ }
177
+
178
+ let options: CliOptions;
179
+ try {
180
+ options = parseArgs(argv);
181
+ } catch (error) {
182
+ process.stderr.write(`[p9][install-health] ${(error as Error).message}\n`);
183
+ process.stderr.write('Usa --help para ver opciones.\n');
184
+ return 1;
185
+ }
186
+
187
+ const isGitRepoCheck = runCommand('git', ['rev-parse', '--is-inside-work-tree'], options.repoPath);
188
+ const isGitRepo = isGitRepoCheck.ok && isGitRepoCheck.stdout === 'true';
189
+
190
+ const statusCommand = isGitRepo
191
+ ? runCommand('node', [localPumukiCliPath, 'status', '--json'], options.repoPath)
192
+ : { ok: false, status: 1, stdout: '', stderr: '' };
193
+
194
+ let statusJsonValid = false;
195
+ let statusPayload: PumukiStatusPayload = {};
196
+ if (statusCommand.ok) {
197
+ try {
198
+ statusPayload = JSON.parse(statusCommand.stdout) as PumukiStatusPayload;
199
+ statusJsonValid = true;
200
+ } catch {
201
+ statusJsonValid = false;
202
+ }
203
+ }
204
+
205
+ const doctorCommand = isGitRepo
206
+ ? runCommand('node', [localPumukiCliPath, 'doctor', '--json'], options.repoPath)
207
+ : { ok: false, status: 1, stdout: '', stderr: '' };
208
+
209
+ const lifecycleInstalled = statusJsonValid
210
+ ? parseLifecycleInstalled(statusPayload.lifecycleState?.installed)
211
+ : null;
212
+ const preCommitManaged = statusJsonValid
213
+ ? statusPayload.hookStatus?.['pre-commit']?.managedBlockPresent === true
214
+ : null;
215
+ const prePushManaged = statusJsonValid
216
+ ? statusPayload.hookStatus?.['pre-push']?.managedBlockPresent === true
217
+ : null;
218
+ const doctorVerdictPass = doctorCommand.ok
219
+ ? parseDoctorVerdictPass(doctorCommand.stdout)
220
+ : null;
221
+
222
+ const result = evaluateP9RuralgoInstallHealth(
223
+ {
224
+ isGitRepo,
225
+ statusCommandOk: statusCommand.ok,
226
+ statusJsonValid,
227
+ lifecycleInstalled,
228
+ preCommitManaged,
229
+ prePushManaged,
230
+ doctorCommandOk: doctorCommand.ok,
231
+ doctorVerdictPass,
232
+ },
233
+ options.policy,
234
+ );
235
+
236
+ const payload = {
237
+ ready: result.ready,
238
+ repoPath: options.repoPath,
239
+ policy: options.policy,
240
+ snapshot: {
241
+ isGitRepo,
242
+ statusCommandOk: statusCommand.ok,
243
+ statusJsonValid,
244
+ lifecycleInstalled,
245
+ preCommitManaged,
246
+ prePushManaged,
247
+ doctorCommandOk: doctorCommand.ok,
248
+ doctorVerdictPass,
249
+ },
250
+ raw: {
251
+ status: {
252
+ exitCode: statusCommand.status,
253
+ stdout: statusCommand.stdout,
254
+ stderr: statusCommand.stderr,
255
+ },
256
+ doctor: {
257
+ exitCode: doctorCommand.status,
258
+ stdout: doctorCommand.stdout,
259
+ stderr: doctorCommand.stderr,
260
+ },
261
+ },
262
+ issues: result.issues,
263
+ };
264
+
265
+ if (options.json) {
266
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
267
+ } else {
268
+ process.stdout.write('[p9][install-health] PRECHECK\n');
269
+ process.stdout.write(`[p9][install-health] repo=${payload.repoPath}\n`);
270
+ process.stdout.write(
271
+ `[p9][install-health] lifecycle_installed=${String(payload.snapshot.lifecycleInstalled)} managed_hooks(pre-commit/pre-push)=${String(
272
+ payload.snapshot.preCommitManaged,
273
+ )}/${String(payload.snapshot.prePushManaged)}\n`,
274
+ );
275
+ process.stdout.write(
276
+ `[p9][install-health] doctor_pass=${String(payload.snapshot.doctorVerdictPass)} status=${payload.ready ? 'READY' : 'NOT_READY'} issues=${payload.issues.length}\n`,
277
+ );
278
+ for (const issue of payload.issues) {
279
+ process.stdout.write(
280
+ `[p9][install-health][${issue.severity}] ${issue.code}: ${issue.message}\n`,
281
+ );
282
+ }
283
+ }
284
+
285
+ return payload.ready ? 0 : 1;
286
+ };
287
+
288
+ process.exitCode = main();