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
@@ -2,6 +2,16 @@ import { getPumukiHooksStatus } from './hookManager';
2
2
  import { LifecycleGitService, type ILifecycleGitService } from './gitService';
3
3
  import { getCurrentPumukiVersion } from './packageInfo';
4
4
  import { readLifecycleState, type LifecycleState } from './state';
5
+ import { existsSync, readFileSync, statSync } from 'node:fs';
6
+ import { join, resolve } from 'node:path';
7
+ import { PUMUKI_MANAGED_HOOKS } from './constants';
8
+ import { parseSkillsPolicy } from '../config/skillsPolicy';
9
+ import { resolvePolicyForStage } from '../gate/stagePolicies';
10
+ import { readEvidenceResult } from '../evidence/readEvidence';
11
+ import {
12
+ resolveEvidenceSigningConfig,
13
+ verifyEvidenceIntegrity,
14
+ } from '../evidence/integrity';
5
15
 
6
16
  export type DoctorIssueSeverity = 'warning' | 'error';
7
17
 
@@ -10,6 +20,27 @@ export type DoctorIssue = {
10
20
  message: string;
11
21
  };
12
22
 
23
+ export type DoctorDeepCheckStatus = 'pass' | 'warn' | 'error';
24
+
25
+ export type DoctorDeepCheckId =
26
+ | 'hooks'
27
+ | 'upstream'
28
+ | 'adapters'
29
+ | 'policy_drift'
30
+ | 'evidence_drift';
31
+
32
+ export type DoctorDeepCheck = {
33
+ id: DoctorDeepCheckId;
34
+ status: DoctorDeepCheckStatus;
35
+ message: string;
36
+ details?: Record<string, string | number | boolean | null | ReadonlyArray<string>>;
37
+ };
38
+
39
+ export type LifecycleDoctorDeepReport = {
40
+ enabled: true;
41
+ checks: ReadonlyArray<DoctorDeepCheck>;
42
+ };
43
+
13
44
  export type LifecycleDoctorReport = {
14
45
  repoRoot: string;
15
46
  packageVersion: string;
@@ -17,12 +48,494 @@ export type LifecycleDoctorReport = {
17
48
  trackedNodeModulesPaths: ReadonlyArray<string>;
18
49
  hookStatus: ReturnType<typeof getPumukiHooksStatus>;
19
50
  issues: ReadonlyArray<DoctorIssue>;
51
+ deep?: LifecycleDoctorDeepReport;
52
+ };
53
+
54
+ const DEEP_PRECOMMIT_MAX_EVIDENCE_AGE_SECONDS = 900;
55
+ const SHA256_HEX_PATTERN = /^[A-Fa-f0-9]{64}$/;
56
+ const ADAPTER_PATH_CANDIDATES = [
57
+ '.pumuki/adapter.json',
58
+ '.pumuki/adapters.json',
59
+ '.pumuki/adapter/hooks.json',
60
+ '.pumuki/mcp.json',
61
+ ] as const;
62
+
63
+ const toDeepCheckIssueSeverity = (
64
+ status: DoctorDeepCheckStatus
65
+ ): DoctorIssueSeverity | null => {
66
+ if (status === 'error') {
67
+ return 'error';
68
+ }
69
+ if (status === 'warn') {
70
+ return 'warning';
71
+ }
72
+ return null;
73
+ };
74
+
75
+ const safeRunGit = (
76
+ git: ILifecycleGitService,
77
+ repoRoot: string,
78
+ args: ReadonlyArray<string>
79
+ ): string | undefined => {
80
+ try {
81
+ const output = git.runGit(args, repoRoot).trim();
82
+ return output.length > 0 ? output : undefined;
83
+ } catch {
84
+ return undefined;
85
+ }
86
+ };
87
+
88
+ const toPositiveCount = (value: unknown): number => {
89
+ const count = typeof value === 'number' && Number.isFinite(value)
90
+ ? Math.trunc(value)
91
+ : Number.parseInt(String(value), 10);
92
+ if (!Number.isFinite(count) || Number.isNaN(count)) {
93
+ return 0;
94
+ }
95
+ return Math.max(0, count);
96
+ };
97
+
98
+ const evaluateDeepHooksCheck = (params: {
99
+ repoRoot: string;
100
+ hookStatus: ReturnType<typeof getPumukiHooksStatus>;
101
+ }): DoctorDeepCheck => {
102
+ const missingManagedHooks: string[] = [];
103
+ const nonExecutableManagedHooks: string[] = [];
104
+
105
+ for (const hook of PUMUKI_MANAGED_HOOKS) {
106
+ const status = params.hookStatus[hook];
107
+ if (!status.exists || !status.managedBlockPresent) {
108
+ missingManagedHooks.push(hook);
109
+ continue;
110
+ }
111
+ const hookPath = join(params.repoRoot, '.git', 'hooks', hook);
112
+ try {
113
+ const mode = statSync(hookPath).mode;
114
+ if ((mode & 0o111) === 0) {
115
+ nonExecutableManagedHooks.push(hook);
116
+ }
117
+ } catch {
118
+ nonExecutableManagedHooks.push(hook);
119
+ }
120
+ }
121
+
122
+ if (nonExecutableManagedHooks.length > 0) {
123
+ return {
124
+ id: 'hooks',
125
+ status: 'error',
126
+ message: `Managed hooks are not executable: ${nonExecutableManagedHooks.join(', ')}.`,
127
+ details: {
128
+ non_executable_hooks: nonExecutableManagedHooks,
129
+ },
130
+ };
131
+ }
132
+
133
+ if (missingManagedHooks.length > 0) {
134
+ return {
135
+ id: 'hooks',
136
+ status: 'warn',
137
+ message: `Managed hook blocks are missing for: ${missingManagedHooks.join(', ')}.`,
138
+ details: {
139
+ missing_hooks: missingManagedHooks,
140
+ },
141
+ };
142
+ }
143
+
144
+ return {
145
+ id: 'hooks',
146
+ status: 'pass',
147
+ message: 'Managed hooks are present and executable.',
148
+ };
149
+ };
150
+
151
+ const evaluateDeepUpstreamCheck = (params: {
152
+ repoRoot: string;
153
+ git: ILifecycleGitService;
154
+ }): DoctorDeepCheck => {
155
+ const branch = safeRunGit(params.git, params.repoRoot, [
156
+ 'rev-parse',
157
+ '--abbrev-ref',
158
+ 'HEAD',
159
+ ]);
160
+ if (!branch || branch === 'HEAD') {
161
+ return {
162
+ id: 'upstream',
163
+ status: 'warn',
164
+ message: 'Current branch could not be resolved for upstream diagnostics.',
165
+ };
166
+ }
167
+
168
+ const upstream = safeRunGit(params.git, params.repoRoot, [
169
+ 'rev-parse',
170
+ '--abbrev-ref',
171
+ '--symbolic-full-name',
172
+ '@{u}',
173
+ ]);
174
+ if (!upstream) {
175
+ return {
176
+ id: 'upstream',
177
+ status: 'warn',
178
+ message: `No upstream configured for branch "${branch}".`,
179
+ details: {
180
+ branch,
181
+ },
182
+ };
183
+ }
184
+
185
+ const aheadBehindRaw = safeRunGit(params.git, params.repoRoot, [
186
+ 'rev-list',
187
+ '--left-right',
188
+ '--count',
189
+ `${upstream}...HEAD`,
190
+ ]);
191
+ const [behindRaw, aheadRaw] = (aheadBehindRaw ?? '').split(/\s+/);
192
+ const behind = toPositiveCount(behindRaw);
193
+ const ahead = toPositiveCount(aheadRaw);
194
+
195
+ if (behind > 0) {
196
+ return {
197
+ id: 'upstream',
198
+ status: 'warn',
199
+ message: `Branch "${branch}" is behind "${upstream}" by ${behind} commit(s).`,
200
+ details: {
201
+ branch,
202
+ upstream,
203
+ ahead,
204
+ behind,
205
+ },
206
+ };
207
+ }
208
+
209
+ return {
210
+ id: 'upstream',
211
+ status: 'pass',
212
+ message: `Branch "${branch}" has upstream "${upstream}" (ahead=${ahead}, behind=${behind}).`,
213
+ details: {
214
+ branch,
215
+ upstream,
216
+ ahead,
217
+ behind,
218
+ },
219
+ };
220
+ };
221
+
222
+ const evaluateDeepAdaptersCheck = (repoRoot: string): DoctorDeepCheck => {
223
+ const discoveredPaths = ADAPTER_PATH_CANDIDATES.filter((candidate) =>
224
+ existsSync(resolve(repoRoot, candidate))
225
+ );
226
+ if (discoveredPaths.length === 0) {
227
+ return {
228
+ id: 'adapters',
229
+ status: 'warn',
230
+ message: 'No adapter configuration file was detected in the repository.',
231
+ details: {
232
+ candidates: ADAPTER_PATH_CANDIDATES as unknown as ReadonlyArray<string>,
233
+ },
234
+ };
235
+ }
236
+
237
+ const invalidJsonPaths: string[] = [];
238
+ const pumukiConfiguredPaths: string[] = [];
239
+
240
+ for (const relativePath of discoveredPaths) {
241
+ const absolutePath = resolve(repoRoot, relativePath);
242
+ try {
243
+ const parsed = JSON.parse(readFileSync(absolutePath, 'utf8')) as unknown;
244
+ const normalized = JSON.stringify(parsed).toLowerCase();
245
+ if (
246
+ normalized.includes('pumuki-pre-') ||
247
+ normalized.includes('pumuki-mcp-enterprise') ||
248
+ normalized.includes('pumuki-mcp-evidence')
249
+ ) {
250
+ pumukiConfiguredPaths.push(relativePath);
251
+ }
252
+ } catch {
253
+ invalidJsonPaths.push(relativePath);
254
+ }
255
+ }
256
+
257
+ if (invalidJsonPaths.length > 0) {
258
+ return {
259
+ id: 'adapters',
260
+ status: 'error',
261
+ message: `Adapter configuration contains invalid JSON: ${invalidJsonPaths.join(', ')}.`,
262
+ details: {
263
+ invalid_paths: invalidJsonPaths,
264
+ },
265
+ };
266
+ }
267
+
268
+ if (pumukiConfiguredPaths.length === 0) {
269
+ return {
270
+ id: 'adapters',
271
+ status: 'warn',
272
+ message: `Adapter files were found but no Pumuki commands were detected: ${discoveredPaths.join(', ')}.`,
273
+ details: {
274
+ discovered_paths: discoveredPaths,
275
+ },
276
+ };
277
+ }
278
+
279
+ return {
280
+ id: 'adapters',
281
+ status: 'pass',
282
+ message: `Adapter configuration detected with Pumuki bindings: ${pumukiConfiguredPaths.join(', ')}.`,
283
+ details: {
284
+ discovered_paths: discoveredPaths,
285
+ pumuki_paths: pumukiConfiguredPaths,
286
+ },
287
+ };
288
+ };
289
+
290
+ const evaluateDeepPolicyDriftCheck = (repoRoot: string): DoctorDeepCheck => {
291
+ const stages: ReadonlyArray<'PRE_COMMIT' | 'PRE_PUSH' | 'CI'> = [
292
+ 'PRE_COMMIT',
293
+ 'PRE_PUSH',
294
+ 'CI',
295
+ ];
296
+ const resolvedPolicies = stages.map((stage) => ({
297
+ stage,
298
+ resolved: resolvePolicyForStage(stage, repoRoot),
299
+ }));
300
+ const invalidHashStages = resolvedPolicies
301
+ .filter((entry) => !SHA256_HEX_PATTERN.test(entry.resolved.trace.hash))
302
+ .map((entry) => entry.stage);
303
+ const invalidSignatureStages = resolvedPolicies
304
+ .filter((entry) => !SHA256_HEX_PATTERN.test(entry.resolved.trace.signature ?? ''))
305
+ .map((entry) => entry.stage);
306
+ const missingVersionStages = resolvedPolicies
307
+ .filter((entry) => {
308
+ const version = entry.resolved.trace.version;
309
+ return typeof version !== 'string' || version.trim().length === 0;
310
+ })
311
+ .map((entry) => entry.stage);
312
+
313
+ if (invalidHashStages.length > 0) {
314
+ return {
315
+ id: 'policy_drift',
316
+ status: 'error',
317
+ message: `Policy trace hash is invalid for stages: ${invalidHashStages.join(', ')}.`,
318
+ details: {
319
+ invalid_hash_stages: invalidHashStages,
320
+ },
321
+ };
322
+ }
323
+ if (invalidSignatureStages.length > 0) {
324
+ return {
325
+ id: 'policy_drift',
326
+ status: 'error',
327
+ message: `Policy trace signature is invalid for stages: ${invalidSignatureStages.join(', ')}.`,
328
+ details: {
329
+ invalid_signature_stages: invalidSignatureStages,
330
+ },
331
+ };
332
+ }
333
+ if (missingVersionStages.length > 0) {
334
+ return {
335
+ id: 'policy_drift',
336
+ status: 'error',
337
+ message: `Policy trace version is missing for stages: ${missingVersionStages.join(', ')}.`,
338
+ details: {
339
+ missing_version_stages: missingVersionStages,
340
+ },
341
+ };
342
+ }
343
+
344
+ const policyPath = resolve(repoRoot, 'skills.policy.json');
345
+ const policyExists = existsSync(policyPath);
346
+ if (!policyExists) {
347
+ return {
348
+ id: 'policy_drift',
349
+ status: 'pass',
350
+ message: 'No skills.policy.json detected; default/hard-mode policy trace is consistent.',
351
+ };
352
+ }
353
+
354
+ let parsedPolicy: unknown;
355
+ try {
356
+ parsedPolicy = JSON.parse(readFileSync(policyPath, 'utf8')) as unknown;
357
+ } catch {
358
+ return {
359
+ id: 'policy_drift',
360
+ status: 'error',
361
+ message: 'skills.policy.json exists but is not valid JSON.',
362
+ details: {
363
+ policy_path: policyPath,
364
+ },
365
+ };
366
+ }
367
+
368
+ if (!parseSkillsPolicy(parsedPolicy)) {
369
+ return {
370
+ id: 'policy_drift',
371
+ status: 'error',
372
+ message: 'skills.policy.json exists but does not match the expected schema.',
373
+ details: {
374
+ policy_path: policyPath,
375
+ },
376
+ };
377
+ }
378
+
379
+ const usingHardMode = resolvedPolicies.some(
380
+ (entry) => entry.resolved.trace.source === 'hard-mode'
381
+ );
382
+ const defaultSourceStages = resolvedPolicies
383
+ .filter((entry) => entry.resolved.trace.source === 'default')
384
+ .map((entry) => entry.stage);
385
+ if (!usingHardMode && defaultSourceStages.length > 0) {
386
+ return {
387
+ id: 'policy_drift',
388
+ status: 'warn',
389
+ message: `skills.policy.json is present but default policy remains active for: ${defaultSourceStages.join(', ')}.`,
390
+ details: {
391
+ default_source_stages: defaultSourceStages,
392
+ },
393
+ };
394
+ }
395
+
396
+ return {
397
+ id: 'policy_drift',
398
+ status: 'pass',
399
+ message: 'Policy trace is consistent with the active policy bundle.',
400
+ };
401
+ };
402
+
403
+ const toTimestampAgeSeconds = (value: string): number | null => {
404
+ const parsed = Date.parse(value);
405
+ if (!Number.isFinite(parsed)) {
406
+ return null;
407
+ }
408
+ const rawAge = Math.floor((Date.now() - parsed) / 1000);
409
+ return rawAge >= 0 ? rawAge : 0;
410
+ };
411
+
412
+ const evaluateDeepEvidenceDriftCheck = (repoRoot: string): DoctorDeepCheck => {
413
+ const evidenceResult = readEvidenceResult(repoRoot);
414
+ if (evidenceResult.kind === 'missing') {
415
+ return {
416
+ id: 'evidence_drift',
417
+ status: 'warn',
418
+ message: '.ai_evidence.json is missing; evidence drift cannot be evaluated.',
419
+ };
420
+ }
421
+
422
+ if (evidenceResult.kind === 'invalid') {
423
+ return {
424
+ id: 'evidence_drift',
425
+ status: 'error',
426
+ message: `.ai_evidence.json is invalid${evidenceResult.version ? ` (version=${evidenceResult.version})` : ''}.`,
427
+ };
428
+ }
429
+
430
+ const integrity = verifyEvidenceIntegrity(evidenceResult.evidence, {
431
+ signatureConfig: resolveEvidenceSigningConfig(process.env),
432
+ enforceSignature: false,
433
+ });
434
+ if (!integrity.ok) {
435
+ return {
436
+ id: 'evidence_drift',
437
+ status: 'error',
438
+ message: `${integrity.code ?? 'EVIDENCE_INTEGRITY_INVALID'}: ${integrity.message ?? 'Evidence integrity verification failed.'}`,
439
+ details: {
440
+ payload_hash: integrity.payloadHash,
441
+ chain_hash: integrity.chainHash,
442
+ previous_chain_hash: integrity.previousChainHash,
443
+ signature_present: integrity.signature.present,
444
+ signature_key_id: integrity.signature.keyId,
445
+ },
446
+ };
447
+ }
448
+
449
+ const timestamp = (evidenceResult.evidence as { timestamp?: unknown }).timestamp;
450
+ if (typeof timestamp !== 'string' || timestamp.trim().length === 0) {
451
+ return {
452
+ id: 'evidence_drift',
453
+ status: 'error',
454
+ message: 'Evidence payload is missing a valid timestamp.',
455
+ };
456
+ }
457
+
458
+ const ageSeconds = toTimestampAgeSeconds(timestamp);
459
+ if (ageSeconds === null) {
460
+ return {
461
+ id: 'evidence_drift',
462
+ status: 'error',
463
+ message: 'Evidence timestamp is not a valid ISO date.',
464
+ };
465
+ }
466
+
467
+ const aiGateStatus = (
468
+ evidenceResult.evidence as { ai_gate?: { status?: unknown } }
469
+ ).ai_gate?.status;
470
+
471
+ if (ageSeconds > DEEP_PRECOMMIT_MAX_EVIDENCE_AGE_SECONDS) {
472
+ return {
473
+ id: 'evidence_drift',
474
+ status: 'warn',
475
+ message: `Evidence is stale (${ageSeconds}s > ${DEEP_PRECOMMIT_MAX_EVIDENCE_AGE_SECONDS}s for PRE_COMMIT baseline).`,
476
+ details: {
477
+ age_seconds: ageSeconds,
478
+ max_age_seconds: DEEP_PRECOMMIT_MAX_EVIDENCE_AGE_SECONDS,
479
+ },
480
+ };
481
+ }
482
+
483
+ if (aiGateStatus === 'BLOCKED') {
484
+ return {
485
+ id: 'evidence_drift',
486
+ status: 'warn',
487
+ message: 'Evidence gate status is BLOCKED in the latest evidence snapshot.',
488
+ details: {
489
+ age_seconds: ageSeconds,
490
+ },
491
+ };
492
+ }
493
+
494
+ return {
495
+ id: 'evidence_drift',
496
+ status: 'pass',
497
+ message: `Evidence freshness is within baseline (age=${ageSeconds}s).`,
498
+ details: {
499
+ age_seconds: ageSeconds,
500
+ max_age_seconds: DEEP_PRECOMMIT_MAX_EVIDENCE_AGE_SECONDS,
501
+ integrity_chain_hash: integrity.chainHash,
502
+ integrity_previous_chain_hash: integrity.previousChainHash,
503
+ integrity_signature_present: integrity.signature.present,
504
+ integrity_signature_verified: integrity.signature.verified,
505
+ },
506
+ };
507
+ };
508
+
509
+ const buildDeepDoctorReport = (params: {
510
+ repoRoot: string;
511
+ git: ILifecycleGitService;
512
+ hookStatus: ReturnType<typeof getPumukiHooksStatus>;
513
+ }): LifecycleDoctorDeepReport => {
514
+ const checks: DoctorDeepCheck[] = [
515
+ evaluateDeepHooksCheck({
516
+ repoRoot: params.repoRoot,
517
+ hookStatus: params.hookStatus,
518
+ }),
519
+ evaluateDeepUpstreamCheck({
520
+ repoRoot: params.repoRoot,
521
+ git: params.git,
522
+ }),
523
+ evaluateDeepAdaptersCheck(params.repoRoot),
524
+ evaluateDeepPolicyDriftCheck(params.repoRoot),
525
+ evaluateDeepEvidenceDriftCheck(params.repoRoot),
526
+ ];
527
+
528
+ return {
529
+ enabled: true,
530
+ checks,
531
+ };
20
532
  };
21
533
 
22
534
  const buildDoctorIssues = (params: {
23
535
  trackedNodeModulesPaths: ReadonlyArray<string>;
24
536
  hookStatus: ReturnType<typeof getPumukiHooksStatus>;
25
537
  lifecycleState: LifecycleState;
538
+ deepChecks?: ReadonlyArray<DoctorDeepCheck>;
26
539
  }): ReadonlyArray<DoctorIssue> => {
27
540
  const issues: DoctorIssue[] = [];
28
541
 
@@ -56,12 +569,24 @@ const buildDoctorIssues = (params: {
56
569
  });
57
570
  }
58
571
 
572
+ for (const check of params.deepChecks ?? []) {
573
+ const severity = toDeepCheckIssueSeverity(check.status);
574
+ if (!severity) {
575
+ continue;
576
+ }
577
+ issues.push({
578
+ severity,
579
+ message: `doctor --deep [${check.id}] ${check.message}`,
580
+ });
581
+ }
582
+
59
583
  return issues;
60
584
  };
61
585
 
62
586
  export const runLifecycleDoctor = (params?: {
63
587
  cwd?: string;
64
588
  git?: ILifecycleGitService;
589
+ deep?: boolean;
65
590
  }): LifecycleDoctorReport => {
66
591
  const git = params?.git ?? new LifecycleGitService();
67
592
  const cwd = params?.cwd ?? process.cwd();
@@ -69,11 +594,19 @@ export const runLifecycleDoctor = (params?: {
69
594
  const trackedNodeModulesPaths = git.trackedNodeModulesPaths(repoRoot);
70
595
  const hookStatus = getPumukiHooksStatus(repoRoot);
71
596
  const lifecycleState = readLifecycleState(git, repoRoot);
597
+ const deepReport = params?.deep
598
+ ? buildDeepDoctorReport({
599
+ repoRoot,
600
+ git,
601
+ hookStatus,
602
+ })
603
+ : undefined;
72
604
 
73
605
  const issues = buildDoctorIssues({
74
606
  trackedNodeModulesPaths,
75
607
  hookStatus,
76
608
  lifecycleState,
609
+ deepChecks: deepReport?.checks,
77
610
  });
78
611
 
79
612
  return {
@@ -83,6 +616,7 @@ export const runLifecycleDoctor = (params?: {
83
616
  trackedNodeModulesPaths,
84
617
  hookStatus,
85
618
  issues,
619
+ deep: deepReport,
86
620
  };
87
621
  };
88
622
 
@@ -8,6 +8,7 @@ const HOOK_COMMANDS: Record<PumukiManagedHook, string> = {
8
8
  'pre-commit': 'pumuki-pre-commit',
9
9
  'pre-push': 'pumuki-pre-push',
10
10
  };
11
+ const PUMUKI_HOOK_PACKAGE = 'pumuki@latest';
11
12
 
12
13
  const trimTrailingWhitespace = (value: string): string =>
13
14
  value.replace(/[ \t]+\n/g, '\n').trimEnd();
@@ -30,7 +31,7 @@ export const buildPumukiManagedHookBlock = (hook: PumukiManagedHook): string =>
30
31
  return [
31
32
  PUMUKI_MANAGED_BLOCK_START,
32
33
  'if command -v npx >/dev/null 2>&1; then',
33
- ` npx --yes ${cli}`,
34
+ ` npx --yes --package ${PUMUKI_HOOK_PACKAGE} ${cli}`,
34
35
  ' status=$?',
35
36
  ' if [ "$status" -ne 0 ]; then',
36
37
  ' exit "$status"',
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ require('tsx/cjs');
4
+
5
+ module.exports = require('./index.ts');