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.
- package/README.md +3 -1
- package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
- package/bin/pumuki-mcp-evidence-stdio.js +5 -0
- package/core/gate/conditionMatches.ts +1 -21
- package/core/gate/evaluateGate.js +5 -0
- package/core/gate/evaluateRules.js +5 -0
- package/core/gate/evaluateRules.ts +1 -24
- package/core/gate/scopeMatcher.ts +84 -0
- package/docs/EXECUTION_BOARD.md +749 -376
- package/docs/MCP_SERVERS.md +41 -2
- package/docs/README.md +6 -2
- package/docs/REFRACTOR_PROGRESS.md +374 -6
- package/docs/validation/README.md +11 -1
- package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
- package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
- package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
- package/integrations/config/skillsCompliance.ts +212 -0
- package/integrations/evidence/integrity.ts +352 -0
- package/integrations/evidence/rulesCoverage.ts +94 -0
- package/integrations/evidence/schema.test.ts +16 -0
- package/integrations/evidence/schema.ts +41 -0
- package/integrations/evidence/writeEvidence.test.ts +68 -0
- package/integrations/evidence/writeEvidence.ts +23 -2
- package/integrations/gate/evaluateAiGate.ts +382 -15
- package/integrations/gate/stagePolicies.ts +70 -15
- package/integrations/gate/waivers.ts +209 -0
- package/integrations/git/findingTraceability.ts +3 -23
- package/integrations/git/index.js +5 -0
- package/integrations/git/runCliCommand.ts +16 -0
- package/integrations/git/runPlatformGate.ts +53 -1
- package/integrations/git/runPlatformGateEvaluation.ts +13 -0
- package/integrations/git/stageRunners.ts +168 -5
- package/integrations/lifecycle/adapter.templates.json +72 -5
- package/integrations/lifecycle/adapter.ts +78 -4
- package/integrations/lifecycle/cli.ts +384 -14
- package/integrations/lifecycle/doctor.ts +534 -0
- package/integrations/lifecycle/hookBlock.ts +2 -1
- package/integrations/lifecycle/index.js +5 -0
- package/integrations/lifecycle/install.ts +115 -3
- package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
- package/integrations/lifecycle/preWriteAutomation.ts +142 -0
- package/integrations/mcp/aiGateCheck.ts +6 -0
- package/integrations/mcp/aiGateReceipt.ts +188 -0
- package/integrations/mcp/enterpriseServer.ts +14 -1
- package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
- package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
- package/integrations/mcp/index.js +5 -0
- package/integrations/sdd/index.js +5 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/policy.ts +191 -2
- package/integrations/sdd/sessionStore.ts +139 -19
- package/integrations/sdd/syncDocs.ts +180 -0
- package/integrations/sdd/types.ts +4 -1
- package/integrations/telemetry/structuredTelemetry.ts +197 -0
- package/package.json +27 -8
- package/scripts/build-p9-validation-manifests.ts +53 -0
- package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
- package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
- package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
- package/scripts/check-p9-ruralgo-install-health.ts +288 -0
- package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
- package/scripts/check-package-manifest.ts +49 -0
- package/scripts/check-tracking-single-active.sh +40 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
- package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
- package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
- package/scripts/manage-library.sh +1 -1
- package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
- package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
- package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
- package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
- package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
- package/scripts/p9-validation-manifests-lib.ts +366 -0
- package/scripts/package-manifest-lib.ts +9 -0
- 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"',
|