@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2
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/LICENSE +21 -0
- package/README.md +80 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +68 -0
- package/assets/integrations/skills/pappra/references/hosts.md +30 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
- package/assets/integrations/skills/pappra/references/operations.md +14 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1650 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +143 -0
- package/dist/app/init.js +1111 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +133 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +13 -0
- package/dist/cli/commands/host.js +125 -0
- package/dist/cli/commands/init.d.ts +51 -0
- package/dist/cli/commands/init.js +668 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +597 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +28 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle.d.ts +16 -0
- package/dist/hosts/integration/bundle.js +81 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +41 -0
- package/dist/hosts/integration/service.js +1381 -0
- package/dist/hosts/integration/support.d.ts +36 -0
- package/dist/hosts/integration/support.js +41 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +70 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +937 -0
- package/dist/hosts/pi/rpc.js +1621 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1145 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +6 -0
- package/dist/package-info.js +19 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +201 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +213 -0
- package/dist/schemas/install.js +139 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +105 -4
- package/skills/pappra/SKILL.md +68 -0
- package/skills/pappra/references/hosts.md +30 -0
- package/skills/pappra/references/lifecycle.md +37 -0
- package/skills/pappra/references/operations.md +14 -0
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
4
|
+
import { DOCTOR_REPORT_SCHEMA, doctorReportSchema, } from '../schemas/doctor.js';
|
|
5
|
+
import { isPathInside } from '../schemas/paths.js';
|
|
6
|
+
import { observePath, readFileBounded, resolveRealpath } from '../infra/fs.js';
|
|
7
|
+
import { inspectMachineConfig, inspectWorkspaceConfig, } from '../workspace/config.js';
|
|
8
|
+
import { discoverWorkspace, } from '../workspace/discover.js';
|
|
9
|
+
import { resolveWorkspaceRoot } from '../workspace/resolve.js';
|
|
10
|
+
import { resolveUserDataDir, inspectRuntimeStatePassive, } from '../install/runtime.js';
|
|
11
|
+
import { ensureStatePathBoundary, inspectOwnedFile, readInstallJournal, readWorkspaceManifest, rootIdentityOf, scanJournalDirectory, workspaceInstallPaths, } from '../install/ownership.js';
|
|
12
|
+
import { detectPassiveHosts, detectPassiveTool } from '../hosts/detect.js';
|
|
13
|
+
function truncateText(value, limit) {
|
|
14
|
+
return value.length <= limit
|
|
15
|
+
? value
|
|
16
|
+
: `${value.slice(0, Math.max(0, limit - 14))}…[truncated]`;
|
|
17
|
+
}
|
|
18
|
+
function makeCheck(input) {
|
|
19
|
+
const evidence = new Map();
|
|
20
|
+
for (const ref of input.evidence ?? []) {
|
|
21
|
+
const key = `${ref.kind}\0${ref.ref}\0${ref.digest ?? ''}\0${ref.scope}`;
|
|
22
|
+
if (!evidence.has(key))
|
|
23
|
+
evidence.set(key, ref);
|
|
24
|
+
if (evidence.size >= 32)
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
const actions = [...new Set(input.nextActions ?? [])]
|
|
28
|
+
.slice(0, 16)
|
|
29
|
+
.map((action) => truncateText(action, 1024));
|
|
30
|
+
const summary = truncateText(input.summary, 4096);
|
|
31
|
+
let coverage = truncateText(input.coverage ?? '', 256);
|
|
32
|
+
if ((input.evidence?.length ?? 0) > 32) {
|
|
33
|
+
coverage = truncateText(`${coverage}; evidence truncated`, 256);
|
|
34
|
+
}
|
|
35
|
+
if ((input.nextActions?.length ?? 0) > 16) {
|
|
36
|
+
coverage = truncateText(`${coverage}; actions truncated`, 256);
|
|
37
|
+
}
|
|
38
|
+
if (summary.length < input.summary.length) {
|
|
39
|
+
coverage = truncateText(`${coverage}; summary truncated`, 256);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
id: input.id,
|
|
43
|
+
code: input.code,
|
|
44
|
+
status: input.status,
|
|
45
|
+
severity: input.severity,
|
|
46
|
+
title: input.title,
|
|
47
|
+
summary,
|
|
48
|
+
source: input.source,
|
|
49
|
+
observedAt: input.observedAt,
|
|
50
|
+
coverage: coverage === '' ? null : coverage,
|
|
51
|
+
evidence: [...evidence.values()],
|
|
52
|
+
nextActions: actions,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function fileEvidence(path, observedAt, digest, scope = 'persisted') {
|
|
56
|
+
return { kind: 'file', ref: path, digest, observedAt, scope };
|
|
57
|
+
}
|
|
58
|
+
function diagnosticsSummary(diagnostics) {
|
|
59
|
+
return diagnostics
|
|
60
|
+
.slice(0, 4)
|
|
61
|
+
.map((entry) => `${entry.code}: ${entry.message}`)
|
|
62
|
+
.join(' | ');
|
|
63
|
+
}
|
|
64
|
+
function statusForDiagnostics(diagnostics, fallback) {
|
|
65
|
+
if (diagnostics.some((entry) => entry.severity === 'error'))
|
|
66
|
+
return 'error';
|
|
67
|
+
if (diagnostics.some((entry) => entry.severity === 'warning')) {
|
|
68
|
+
return 'warning';
|
|
69
|
+
}
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
async function inspectInstallState(root, observedAt) {
|
|
73
|
+
const paths = workspaceInstallPaths(root);
|
|
74
|
+
const evidence = [];
|
|
75
|
+
const actions = [];
|
|
76
|
+
const state = { status: 'ok' };
|
|
77
|
+
const problems = [];
|
|
78
|
+
const escalate = (next) => {
|
|
79
|
+
const order = {
|
|
80
|
+
ok: 0,
|
|
81
|
+
unsupported: 1,
|
|
82
|
+
unknown: 2,
|
|
83
|
+
warning: 3,
|
|
84
|
+
partial: 4,
|
|
85
|
+
error: 5,
|
|
86
|
+
};
|
|
87
|
+
if (order[next] > order[state.status])
|
|
88
|
+
state.status = next;
|
|
89
|
+
};
|
|
90
|
+
const realRoot = await resolveRealpath(root);
|
|
91
|
+
const manifestBoundary = await ensureStatePathBoundary(root, paths.manifestPath);
|
|
92
|
+
const realManifest = await resolveRealpath(paths.manifestPath);
|
|
93
|
+
const manifestUnsafe = !manifestBoundary.ok ||
|
|
94
|
+
(realManifest !== null &&
|
|
95
|
+
(realRoot === null || !isPathInside(realRoot, realManifest)));
|
|
96
|
+
const manifest = manifestUnsafe ? null : await readWorkspaceManifest(root);
|
|
97
|
+
if (manifest === null) {
|
|
98
|
+
escalate('error');
|
|
99
|
+
problems.push('installation manifest path is unsafe or escapes the workspace');
|
|
100
|
+
actions.push('Inspect .pappra/local/install-manifest.json; doctor does not read through unsafe paths.');
|
|
101
|
+
}
|
|
102
|
+
else if (manifest.diagnostics.length > 0) {
|
|
103
|
+
escalate('error');
|
|
104
|
+
problems.push(diagnosticsSummary(manifest.diagnostics));
|
|
105
|
+
actions.push('Inspect install-manifest.json; doctor does not repair or migrate it.');
|
|
106
|
+
}
|
|
107
|
+
else if (manifest.exists && manifest.value !== null) {
|
|
108
|
+
evidence.push(fileEvidence(paths.manifestPath, observedAt, manifest.digest));
|
|
109
|
+
const identity = await rootIdentityOf(root);
|
|
110
|
+
if (identity === null ||
|
|
111
|
+
identity.dev !== manifest.value.rootIdentity.dev ||
|
|
112
|
+
identity.ino !== manifest.value.rootIdentity.ino) {
|
|
113
|
+
escalate('error');
|
|
114
|
+
problems.push('manifest root identity does not match the current workspace');
|
|
115
|
+
actions.push('Do not reuse this installation manifest for the current root.');
|
|
116
|
+
}
|
|
117
|
+
for (const entry of manifest.value.entries) {
|
|
118
|
+
const state = await inspectOwnedFile(root, entry);
|
|
119
|
+
if (state.status === 'missing') {
|
|
120
|
+
escalate('warning');
|
|
121
|
+
problems.push(`managed file missing: ${entry.path}`);
|
|
122
|
+
actions.push(`Restore or explicitly reconcile ${entry.path}; doctor does not write it.`);
|
|
123
|
+
}
|
|
124
|
+
else if (state.status !== 'match') {
|
|
125
|
+
escalate('warning');
|
|
126
|
+
problems.push(`managed file changed: ${entry.path} (${state.status})`);
|
|
127
|
+
actions.push(`Review ${entry.path}; rollback/uninstall will preserve the user-modified file.`);
|
|
128
|
+
}
|
|
129
|
+
if (entry.backupRef !== null) {
|
|
130
|
+
const backupPath = join(root, ...entry.backupRef.split('/'));
|
|
131
|
+
const backupBoundary = await ensureStatePathBoundary(root, backupPath);
|
|
132
|
+
const realBackup = await resolveRealpath(backupPath);
|
|
133
|
+
const backupEscapes = realBackup !== null &&
|
|
134
|
+
(realRoot === null || !isPathInside(realRoot, realBackup));
|
|
135
|
+
if (!backupBoundary.ok || backupEscapes) {
|
|
136
|
+
escalate('warning');
|
|
137
|
+
problems.push(`backup path is unsafe: ${entry.backupRef}`);
|
|
138
|
+
actions.push(`Inspect backup ${entry.backupRef}; doctor does not read outside the workspace.`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const backup = await readFileBounded(backupPath, 4 * 1024 * 1024);
|
|
142
|
+
if (!backup.ok) {
|
|
143
|
+
escalate('warning');
|
|
144
|
+
problems.push(`backup is missing or not a readable regular file: ${entry.backupRef} (${backup.code})`);
|
|
145
|
+
actions.push(`Inspect backup ${entry.backupRef}; rollback will preserve the current file.`);
|
|
146
|
+
}
|
|
147
|
+
else if (entry.backupDigest !== null &&
|
|
148
|
+
backup.value.digest !== entry.backupDigest) {
|
|
149
|
+
escalate('warning');
|
|
150
|
+
problems.push(`backup digest mismatch: ${entry.backupRef}`);
|
|
151
|
+
actions.push(`Inspect backup ${entry.backupRef}; doctor does not restore it.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (!manifest.exists) {
|
|
158
|
+
escalate('unknown');
|
|
159
|
+
problems.push('workspace has no P04 installation manifest; existing files are unmanaged');
|
|
160
|
+
actions.push('Use an explicit managed init/apply before expecting rollback or uninstall ownership.');
|
|
161
|
+
}
|
|
162
|
+
const journalScan = await scanJournalDirectory(paths.journalsDir);
|
|
163
|
+
if (journalScan.kind === 'unreadable') {
|
|
164
|
+
escalate('error');
|
|
165
|
+
problems.push(`cannot read journal directory: ${paths.journalsDir} (${journalScan.error ?? 'unreadable'})`);
|
|
166
|
+
actions.push(`Inspect journal directory ${paths.journalsDir}; doctor did not treat it as empty.`);
|
|
167
|
+
evidence.push(fileEvidence(paths.journalsDir, observedAt, null));
|
|
168
|
+
}
|
|
169
|
+
for (const path of journalScan.nonRegular) {
|
|
170
|
+
escalate('error');
|
|
171
|
+
problems.push(`journal candidate is not a regular file: ${path}`);
|
|
172
|
+
actions.push(`Inspect ${path}; doctor does not follow, read, migrate, or repair non-regular journal candidates.`);
|
|
173
|
+
evidence.push(fileEvidence(path, observedAt, null));
|
|
174
|
+
}
|
|
175
|
+
const journalCount = journalScan.files.length + journalScan.nonRegular.length;
|
|
176
|
+
if (journalCount > 32) {
|
|
177
|
+
problems.push(`${journalCount} journal candidates observed; evidence/actions are bounded to 32/16 entries.`);
|
|
178
|
+
}
|
|
179
|
+
for (const path of journalScan.files) {
|
|
180
|
+
const read = await readInstallJournal(path);
|
|
181
|
+
evidence.push(fileEvidence(path, observedAt, read.digest));
|
|
182
|
+
if (read.value === null) {
|
|
183
|
+
escalate('error');
|
|
184
|
+
problems.push(`unreadable/incompatible install journal: ${path}`);
|
|
185
|
+
actions.push(`Inspect journal ${path}; doctor does not migrate or retry it.`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (read.value.status !== 'verified' &&
|
|
189
|
+
read.value.status !== 'rolled-back') {
|
|
190
|
+
escalate('warning');
|
|
191
|
+
problems.push(`install journal ${read.value.installId} is ${read.value.status}`);
|
|
192
|
+
actions.push(`Inspect journal ${path}; Pappra does not resume or clean it automatically.`);
|
|
193
|
+
}
|
|
194
|
+
if (read.value.residuals.length > 0) {
|
|
195
|
+
escalate('warning');
|
|
196
|
+
problems.push(`journal ${read.value.installId} has residuals: ${read.value.residuals.join(', ')}`);
|
|
197
|
+
actions.push('Inspect residual paths manually; doctor does not delete them.');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const lockObservation = await observePath(paths.lockPath);
|
|
201
|
+
const lock = lockObservation.kind !== 'missing';
|
|
202
|
+
if (lock) {
|
|
203
|
+
escalate('warning');
|
|
204
|
+
evidence.push(fileEvidence(paths.lockPath, observedAt, null));
|
|
205
|
+
problems.push(`workspace lock exists: ${paths.lockPath}`);
|
|
206
|
+
actions.push('Inspect the lock owner; doctor does not reclaim locks by age.');
|
|
207
|
+
}
|
|
208
|
+
const finalStatus = state.status;
|
|
209
|
+
return makeCheck({
|
|
210
|
+
id: 'install.workspace',
|
|
211
|
+
code: finalStatus === 'error'
|
|
212
|
+
? diagnosticCodes.doctor_install_state_invalid
|
|
213
|
+
: finalStatus === 'warning' || finalStatus === 'partial'
|
|
214
|
+
? diagnosticCodes.doctor_install_partial
|
|
215
|
+
: finalStatus === 'ok'
|
|
216
|
+
? diagnosticCodes.doctor_install_ok
|
|
217
|
+
: diagnosticCodes.doctor_install_unknown,
|
|
218
|
+
status: finalStatus,
|
|
219
|
+
severity: finalStatus === 'error'
|
|
220
|
+
? 'error'
|
|
221
|
+
: finalStatus === 'ok'
|
|
222
|
+
? 'info'
|
|
223
|
+
: 'warning',
|
|
224
|
+
title: 'Managed workspace installation state',
|
|
225
|
+
summary: problems.length === 0
|
|
226
|
+
? 'No unresolved P04 managed-install state was found.'
|
|
227
|
+
: problems.join('; '),
|
|
228
|
+
source: paths.manifestPath,
|
|
229
|
+
observedAt,
|
|
230
|
+
coverage: 'current workspace ownership, journals, lock, backups',
|
|
231
|
+
evidence,
|
|
232
|
+
nextActions: actions,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function runtimeCheck(targetVersion, machineRuntime, passive, machinePath, observedAt, dataDir) {
|
|
236
|
+
if (targetVersion === null) {
|
|
237
|
+
return makeCheck({
|
|
238
|
+
id: 'runtime.state',
|
|
239
|
+
code: diagnosticCodes.doctor_runtime_unknown,
|
|
240
|
+
status: 'unknown',
|
|
241
|
+
severity: 'info',
|
|
242
|
+
title: 'Runtime target and installed state',
|
|
243
|
+
summary: 'Workspace configuration does not declare a runtime target.',
|
|
244
|
+
source: machinePath ?? dataDir,
|
|
245
|
+
observedAt,
|
|
246
|
+
coverage: 'workspace runtime target and machine runtime',
|
|
247
|
+
nextActions: ['Run pappra init to establish a workspace runtime target.'],
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (machineRuntime === null) {
|
|
251
|
+
if (passive === null) {
|
|
252
|
+
return makeCheck({
|
|
253
|
+
id: 'runtime.state',
|
|
254
|
+
code: diagnosticCodes.doctor_runtime_missing,
|
|
255
|
+
status: 'warning',
|
|
256
|
+
severity: 'warning',
|
|
257
|
+
title: 'Runtime target and installed state',
|
|
258
|
+
summary: `Workspace targets ${targetVersion}, but machine.json has no runtime binding and no passive state could be read.`,
|
|
259
|
+
source: machinePath ?? dataDir,
|
|
260
|
+
observedAt,
|
|
261
|
+
nextActions: [
|
|
262
|
+
'Run an explicit pappra init --apply --install-runtime with a chosen local tarball or exact version when authorized.',
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const unboundState = passive.status;
|
|
267
|
+
if (unboundState === 'locked') {
|
|
268
|
+
return makeCheck({
|
|
269
|
+
id: 'runtime.state',
|
|
270
|
+
code: diagnosticCodes.doctor_runtime_locked,
|
|
271
|
+
status: 'warning',
|
|
272
|
+
severity: 'warning',
|
|
273
|
+
title: 'Runtime target and installed state',
|
|
274
|
+
summary: `Target runtime ${targetVersion} has a leftover installation lock; machine.json is not bound yet.`,
|
|
275
|
+
source: passive.lockPath,
|
|
276
|
+
observedAt,
|
|
277
|
+
evidence: [fileEvidence(passive.lockPath, observedAt, null)],
|
|
278
|
+
nextActions: [
|
|
279
|
+
'Inspect the lock; doctor does not reclaim it or bind the runtime.',
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (unboundState === 'partial') {
|
|
284
|
+
return makeCheck({
|
|
285
|
+
id: 'runtime.state',
|
|
286
|
+
code: diagnosticCodes.doctor_runtime_partial,
|
|
287
|
+
status: 'partial',
|
|
288
|
+
severity: 'warning',
|
|
289
|
+
title: 'Runtime target and installed state',
|
|
290
|
+
summary: `Target runtime ${targetVersion} has an incomplete installation journal; machine.json is not bound yet.`,
|
|
291
|
+
source: passive.runtimeRoot,
|
|
292
|
+
observedAt,
|
|
293
|
+
evidence: [fileEvidence(passive.runtimeRoot, observedAt, null)],
|
|
294
|
+
nextActions: [
|
|
295
|
+
'Inspect the journal; doctor does not resume an interrupted first install.',
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (unboundState === 'corrupt' ||
|
|
300
|
+
unboundState === 'unreadable' ||
|
|
301
|
+
unboundState === 'version-mismatch') {
|
|
302
|
+
return makeCheck({
|
|
303
|
+
id: 'runtime.state',
|
|
304
|
+
code: unboundState === 'unreadable'
|
|
305
|
+
? diagnosticCodes.doctor_read_error
|
|
306
|
+
: unboundState === 'version-mismatch'
|
|
307
|
+
? diagnosticCodes.doctor_runtime_mismatch
|
|
308
|
+
: diagnosticCodes.doctor_runtime_corrupt,
|
|
309
|
+
status: 'error',
|
|
310
|
+
severity: 'error',
|
|
311
|
+
title: 'Runtime target and installed state',
|
|
312
|
+
summary: `Target runtime ${targetVersion} is ${unboundState} and machine.json is not bound: ${diagnosticsSummary(passive.diagnostics)}`,
|
|
313
|
+
source: passive.runtimeRoot,
|
|
314
|
+
observedAt,
|
|
315
|
+
evidence: [fileEvidence(passive.runtimeRoot, observedAt, null)],
|
|
316
|
+
nextActions: [
|
|
317
|
+
'Inspect the runtime; doctor does not reinstall or bind it.',
|
|
318
|
+
],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return makeCheck({
|
|
322
|
+
id: 'runtime.state',
|
|
323
|
+
code: diagnosticCodes.doctor_runtime_missing,
|
|
324
|
+
status: 'warning',
|
|
325
|
+
severity: 'warning',
|
|
326
|
+
title: 'Runtime target and installed state',
|
|
327
|
+
summary: unboundState === 'content-match'
|
|
328
|
+
? `Target runtime ${targetVersion} content matches its manifest, but machine.json has no runtime binding.`
|
|
329
|
+
: `Workspace targets ${targetVersion}, but machine.json has no verified runtime location.`,
|
|
330
|
+
source: machinePath ?? dataDir,
|
|
331
|
+
observedAt,
|
|
332
|
+
evidence: unboundState === 'content-match'
|
|
333
|
+
? [
|
|
334
|
+
fileEvidence(join(passive.runtimeRoot, 'install-manifest.json'), observedAt, null),
|
|
335
|
+
]
|
|
336
|
+
: [],
|
|
337
|
+
nextActions: [
|
|
338
|
+
'Run an explicit pappra init --apply --install-runtime with a chosen local tarball or exact version when authorized.',
|
|
339
|
+
],
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (machineRuntime.version !== targetVersion) {
|
|
343
|
+
return makeCheck({
|
|
344
|
+
id: 'runtime.state',
|
|
345
|
+
code: diagnosticCodes.doctor_runtime_mismatch,
|
|
346
|
+
status: 'error',
|
|
347
|
+
severity: 'error',
|
|
348
|
+
title: 'Runtime target and installed state',
|
|
349
|
+
summary: `Workspace targets ${targetVersion}, but machine.json points to ${machineRuntime.version}.`,
|
|
350
|
+
source: machinePath ?? dataDir,
|
|
351
|
+
observedAt,
|
|
352
|
+
evidence: [fileEvidence(machineRuntime.executable, observedAt, null)],
|
|
353
|
+
nextActions: [
|
|
354
|
+
'Do not switch runtime automatically. Inspect the workspace revision and machine mapping, then use an explicit update/repair plan.',
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (passive === null) {
|
|
359
|
+
return makeCheck({
|
|
360
|
+
id: 'runtime.state',
|
|
361
|
+
code: diagnosticCodes.doctor_runtime_unknown,
|
|
362
|
+
status: 'unknown',
|
|
363
|
+
severity: 'warning',
|
|
364
|
+
title: 'Runtime target and installed state',
|
|
365
|
+
summary: 'Machine runtime is recorded, but passive runtime state could not be read.',
|
|
366
|
+
source: machinePath ?? dataDir,
|
|
367
|
+
observedAt,
|
|
368
|
+
nextActions: [
|
|
369
|
+
'Inspect the runtime data directory and machine.json manually.',
|
|
370
|
+
],
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const evidence = [
|
|
374
|
+
fileEvidence(join(passive.runtimeRoot, 'install-manifest.json'), observedAt, null),
|
|
375
|
+
fileEvidence(machineRuntime.executable, observedAt, null),
|
|
376
|
+
];
|
|
377
|
+
const state = passive.status;
|
|
378
|
+
if (state === 'content-match') {
|
|
379
|
+
const executableMatches = passive.manifest !== null &&
|
|
380
|
+
machineRuntime.executable === passive.manifest.executable;
|
|
381
|
+
if (!executableMatches) {
|
|
382
|
+
return makeCheck({
|
|
383
|
+
id: 'runtime.state',
|
|
384
|
+
code: diagnosticCodes.doctor_runtime_mismatch,
|
|
385
|
+
status: 'error',
|
|
386
|
+
severity: 'error',
|
|
387
|
+
title: 'Runtime target and installed state',
|
|
388
|
+
summary: 'machine.json executable does not match the verified runtime manifest.',
|
|
389
|
+
source: machineRuntime.executable,
|
|
390
|
+
observedAt,
|
|
391
|
+
evidence,
|
|
392
|
+
nextActions: [
|
|
393
|
+
'Run an explicit runtime repair/update plan; doctor does not rewrite machine.json.',
|
|
394
|
+
],
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return makeCheck({
|
|
398
|
+
id: 'runtime.state',
|
|
399
|
+
code: diagnosticCodes.doctor_runtime_unverified,
|
|
400
|
+
status: 'partial',
|
|
401
|
+
severity: 'warning',
|
|
402
|
+
title: 'Runtime target and installed state',
|
|
403
|
+
summary: `Runtime content digest matches its manifest, but doctor did not execute smoke. Historical verifiedAt ${passive.manifest?.verifiedAt ?? 'unknown'} is evidence only, not current callability.`,
|
|
404
|
+
source: passive.runtimeRoot,
|
|
405
|
+
observedAt,
|
|
406
|
+
coverage: 'exact version, manifest, package digest, executable presence',
|
|
407
|
+
evidence,
|
|
408
|
+
nextActions: [
|
|
409
|
+
'Do not treat this as a current callable-runtime proof. Run an explicit active runtime verify/install only with authorization; doctor does not execute it.',
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (state === 'missing') {
|
|
414
|
+
return makeCheck({
|
|
415
|
+
id: 'runtime.state',
|
|
416
|
+
code: diagnosticCodes.doctor_runtime_missing,
|
|
417
|
+
status: 'warning',
|
|
418
|
+
severity: 'warning',
|
|
419
|
+
title: 'Runtime target and installed state',
|
|
420
|
+
summary: `Runtime ${targetVersion} is recorded in machine.json but missing from ${dataDir}.`,
|
|
421
|
+
source: passive.runtimeRoot,
|
|
422
|
+
observedAt,
|
|
423
|
+
evidence,
|
|
424
|
+
nextActions: [
|
|
425
|
+
'Run an explicit init/install with a chosen runtime source when authorized; doctor does not install.',
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (state === 'locked') {
|
|
430
|
+
return makeCheck({
|
|
431
|
+
id: 'runtime.state',
|
|
432
|
+
code: diagnosticCodes.doctor_runtime_locked,
|
|
433
|
+
status: 'warning',
|
|
434
|
+
severity: 'warning',
|
|
435
|
+
title: 'Runtime target and installed state',
|
|
436
|
+
summary: `Runtime ${targetVersion} has a leftover installation lock.`,
|
|
437
|
+
source: passive.lockPath,
|
|
438
|
+
observedAt,
|
|
439
|
+
evidence,
|
|
440
|
+
nextActions: [
|
|
441
|
+
'Inspect the lock owner and journal; doctor does not reclaim, rerun, or delete the lock.',
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (state === 'partial') {
|
|
446
|
+
return makeCheck({
|
|
447
|
+
id: 'runtime.state',
|
|
448
|
+
code: diagnosticCodes.doctor_runtime_partial,
|
|
449
|
+
status: 'partial',
|
|
450
|
+
severity: 'warning',
|
|
451
|
+
title: 'Runtime target and installed state',
|
|
452
|
+
summary: `Runtime ${targetVersion} has an incomplete installation journal.`,
|
|
453
|
+
source: passive.runtimeRoot,
|
|
454
|
+
observedAt,
|
|
455
|
+
evidence,
|
|
456
|
+
nextActions: [
|
|
457
|
+
'Inspect the journal and staging directory; doctor does not resume an interrupted install.',
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (state === 'version-mismatch') {
|
|
462
|
+
return makeCheck({
|
|
463
|
+
id: 'runtime.state',
|
|
464
|
+
code: diagnosticCodes.doctor_runtime_mismatch,
|
|
465
|
+
status: 'error',
|
|
466
|
+
severity: 'error',
|
|
467
|
+
title: 'Runtime target and installed state',
|
|
468
|
+
summary: `Runtime manifest does not match target ${targetVersion}.`,
|
|
469
|
+
source: passive.runtimeRoot,
|
|
470
|
+
observedAt,
|
|
471
|
+
evidence,
|
|
472
|
+
nextActions: [
|
|
473
|
+
'Inspect the runtime and machine mapping; doctor does not switch versions.',
|
|
474
|
+
],
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return makeCheck({
|
|
478
|
+
id: 'runtime.state',
|
|
479
|
+
code: state === 'unreadable'
|
|
480
|
+
? diagnosticCodes.doctor_read_error
|
|
481
|
+
: diagnosticCodes.doctor_runtime_corrupt,
|
|
482
|
+
status: 'error',
|
|
483
|
+
severity: 'error',
|
|
484
|
+
title: 'Runtime target and installed state',
|
|
485
|
+
summary: `Runtime state is ${state}: ${diagnosticsSummary(passive.diagnostics)}`,
|
|
486
|
+
source: passive.runtimeRoot,
|
|
487
|
+
observedAt,
|
|
488
|
+
evidence,
|
|
489
|
+
nextActions: [
|
|
490
|
+
'Inspect the runtime manifest/package; doctor does not reinstall or overwrite it.',
|
|
491
|
+
],
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function hostChecks(selectedHosts, homeDir, pathEnv, observedAt) {
|
|
495
|
+
const hosts = detectPassiveHosts({
|
|
496
|
+
homeDir,
|
|
497
|
+
pathEnv,
|
|
498
|
+
selectedHosts,
|
|
499
|
+
observedAt,
|
|
500
|
+
});
|
|
501
|
+
return hosts.map((host) => {
|
|
502
|
+
const selected = host.selected;
|
|
503
|
+
if (!host.discovered) {
|
|
504
|
+
return makeCheck({
|
|
505
|
+
id: `host.${host.host}`,
|
|
506
|
+
code: selected
|
|
507
|
+
? diagnosticCodes.doctor_host_missing
|
|
508
|
+
: diagnosticCodes.doctor_host_discovered_unverified,
|
|
509
|
+
status: selected ? 'warning' : 'ok',
|
|
510
|
+
severity: selected ? 'warning' : 'info',
|
|
511
|
+
title: `Host ${host.host}`,
|
|
512
|
+
summary: selected
|
|
513
|
+
? `Selected host ${host.host} executable was not discovered on PATH.`
|
|
514
|
+
: `Host ${host.host} is not selected and no executable was discovered.`,
|
|
515
|
+
source: host.host,
|
|
516
|
+
observedAt,
|
|
517
|
+
coverage: 'PATH filesystem presence only; no host started',
|
|
518
|
+
evidence: host.evidence,
|
|
519
|
+
nextActions: selected
|
|
520
|
+
? [
|
|
521
|
+
'Install or select a supported host explicitly; P09 integration/callability is not verified.',
|
|
522
|
+
]
|
|
523
|
+
: [],
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return makeCheck({
|
|
527
|
+
id: `host.${host.host}`,
|
|
528
|
+
code: host.configured
|
|
529
|
+
? diagnosticCodes.doctor_host_config_present
|
|
530
|
+
: diagnosticCodes.doctor_host_discovered_unverified,
|
|
531
|
+
status: 'unsupported',
|
|
532
|
+
severity: 'info',
|
|
533
|
+
title: `Host ${host.host}`,
|
|
534
|
+
summary: `Executable discovered${host.configured ? '; a host config path exists' : ''}. Callability, trust, and P09 integration are not verified; config presence is not Pappra wiring.`,
|
|
535
|
+
source: host.executablePath ?? host.host,
|
|
536
|
+
observedAt,
|
|
537
|
+
coverage: 'path/config existence only',
|
|
538
|
+
evidence: host.evidence,
|
|
539
|
+
nextActions: [
|
|
540
|
+
'P09 host integration and runtime probes are not implemented; no host or model was started.',
|
|
541
|
+
],
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function toolCheck(name, preference, pathEnv, observedAt) {
|
|
546
|
+
if (preference === null) {
|
|
547
|
+
return makeCheck({
|
|
548
|
+
id: `tool.${name}`,
|
|
549
|
+
code: diagnosticCodes.doctor_tool_discovered_unverified,
|
|
550
|
+
status: 'unknown',
|
|
551
|
+
severity: 'info',
|
|
552
|
+
title: `Tool ${name}`,
|
|
553
|
+
summary: 'No workspace preference is readable; tool availability was not inferred.',
|
|
554
|
+
source: name,
|
|
555
|
+
observedAt,
|
|
556
|
+
coverage: 'PATH filesystem presence only; no execution',
|
|
557
|
+
nextActions: [
|
|
558
|
+
'Run pappra init to declare code-intelligence preferences.',
|
|
559
|
+
],
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (preference === 'none' || (name === 'serena' && preference === 'auto')) {
|
|
563
|
+
return makeCheck({
|
|
564
|
+
id: `tool.${name}`,
|
|
565
|
+
code: preference === 'none'
|
|
566
|
+
? diagnosticCodes.doctor_tool_disabled
|
|
567
|
+
: diagnosticCodes.doctor_tool_discovered_unverified,
|
|
568
|
+
status: 'ok',
|
|
569
|
+
severity: 'info',
|
|
570
|
+
title: `Tool ${name}`,
|
|
571
|
+
summary: preference === 'none'
|
|
572
|
+
? `${name} is disabled by workspace policy.`
|
|
573
|
+
: 'auto does not prove a provider is available; no implicit installation or indexing is performed.',
|
|
574
|
+
source: name,
|
|
575
|
+
observedAt,
|
|
576
|
+
coverage: 'preference only',
|
|
577
|
+
nextActions: [],
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
const tool = detectPassiveTool(name, pathEnv);
|
|
581
|
+
if (!tool.discovered) {
|
|
582
|
+
return makeCheck({
|
|
583
|
+
id: `tool.${name}`,
|
|
584
|
+
code: diagnosticCodes.doctor_tool_preference_unavailable,
|
|
585
|
+
status: 'warning',
|
|
586
|
+
severity: 'warning',
|
|
587
|
+
title: `Tool ${name}`,
|
|
588
|
+
summary: `Workspace preference is ${preference}, but ${name} was not discovered on PATH.`,
|
|
589
|
+
source: name,
|
|
590
|
+
observedAt,
|
|
591
|
+
coverage: 'PATH filesystem presence only; no execution',
|
|
592
|
+
nextActions: [
|
|
593
|
+
'Install or configure the tool explicitly only if authorized; doctor does not run package managers or prepare indexes.',
|
|
594
|
+
],
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return makeCheck({
|
|
598
|
+
id: `tool.${name}`,
|
|
599
|
+
code: diagnosticCodes.doctor_tool_discovered_unverified,
|
|
600
|
+
status: 'unsupported',
|
|
601
|
+
severity: 'info',
|
|
602
|
+
title: `Tool ${name}`,
|
|
603
|
+
summary: `${name} was discovered at ${tool.executablePath}, but callability/index health is not verified by P05.`,
|
|
604
|
+
source: tool.executablePath ?? name,
|
|
605
|
+
observedAt,
|
|
606
|
+
coverage: 'path presence only',
|
|
607
|
+
evidence: [fileEvidence(tool.executablePath ?? name, observedAt, null)],
|
|
608
|
+
nextActions: [
|
|
609
|
+
'P08 code intelligence/index verification is not implemented; no index was prepared.',
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
export async function runDoctor(options = {}) {
|
|
614
|
+
const observedAt = (options.now ?? new Date()).toISOString();
|
|
615
|
+
const dataDir = options.dataDir ?? resolveUserDataDir();
|
|
616
|
+
const homeDir = options.homeDir ?? homedir();
|
|
617
|
+
const pathEnv = options.pathEnv ?? process.env['PATH'] ?? '';
|
|
618
|
+
const resolution = await resolveWorkspaceRoot({
|
|
619
|
+
startPath: options.root ?? process.cwd(),
|
|
620
|
+
});
|
|
621
|
+
const checks = [];
|
|
622
|
+
const root = resolution.root;
|
|
623
|
+
let config = null;
|
|
624
|
+
let machine = null;
|
|
625
|
+
let discovery = null;
|
|
626
|
+
let configPath = null;
|
|
627
|
+
let machinePath = null;
|
|
628
|
+
checks.push(makeCheck({
|
|
629
|
+
id: 'workspace.root',
|
|
630
|
+
code: resolution.state === 'registered'
|
|
631
|
+
? diagnosticCodes.doctor_workspace_registered
|
|
632
|
+
: resolution.state === 'missing'
|
|
633
|
+
? diagnosticCodes.doctor_workspace_not_registered
|
|
634
|
+
: resolution.state === 'incomplete'
|
|
635
|
+
? diagnosticCodes.doctor_workspace_incomplete
|
|
636
|
+
: diagnosticCodes.doctor_workspace_unavailable,
|
|
637
|
+
status: resolution.state === 'registered'
|
|
638
|
+
? 'ok'
|
|
639
|
+
: resolution.state === 'missing'
|
|
640
|
+
? 'warning'
|
|
641
|
+
: 'error',
|
|
642
|
+
severity: resolution.state === 'registered'
|
|
643
|
+
? 'info'
|
|
644
|
+
: resolution.state === 'missing'
|
|
645
|
+
? 'warning'
|
|
646
|
+
: 'error',
|
|
647
|
+
title: 'Workspace root resolution',
|
|
648
|
+
summary: resolution.state === 'registered'
|
|
649
|
+
? `Registered workspace root: ${root ?? '(unknown)'}`
|
|
650
|
+
: resolution.state === 'missing'
|
|
651
|
+
? 'No registered Pappra workspace was found above the requested path.'
|
|
652
|
+
: `Workspace root resolution is ${resolution.state}: ${diagnosticsSummary(resolution.diagnostics)}`,
|
|
653
|
+
source: resolution.startDirectory ?? resolution.requestedPath,
|
|
654
|
+
observedAt,
|
|
655
|
+
coverage: 'nearest registered .pappra marker only; no home scan',
|
|
656
|
+
evidence: root === null ? [] : [fileEvidence(root, observedAt, null)],
|
|
657
|
+
nextActions: resolution.state === 'missing'
|
|
658
|
+
? ['Run pappra init from the intended workspace root.']
|
|
659
|
+
: resolution.state === 'registered'
|
|
660
|
+
? []
|
|
661
|
+
: [
|
|
662
|
+
'Inspect the workspace marker/root path; doctor does not repair it.',
|
|
663
|
+
],
|
|
664
|
+
}));
|
|
665
|
+
if (root !== null) {
|
|
666
|
+
const workspaceInspection = await inspectWorkspaceConfig(root, {
|
|
667
|
+
missing: 'info',
|
|
668
|
+
});
|
|
669
|
+
const machineInspection = await inspectMachineConfig(root);
|
|
670
|
+
configPath = workspaceInspection.path;
|
|
671
|
+
machinePath = machineInspection.path;
|
|
672
|
+
config = workspaceInspection.value;
|
|
673
|
+
machine = machineInspection.value;
|
|
674
|
+
discovery = await discoverWorkspace(root);
|
|
675
|
+
const configDiagnostics = workspaceInspection.diagnostics;
|
|
676
|
+
const configStatus = config
|
|
677
|
+
? statusForDiagnostics(configDiagnostics, 'ok')
|
|
678
|
+
: workspaceInspection.exists
|
|
679
|
+
? 'error'
|
|
680
|
+
: 'warning';
|
|
681
|
+
checks.push(makeCheck({
|
|
682
|
+
id: 'workspace.config',
|
|
683
|
+
code: config
|
|
684
|
+
? configStatus === 'ok'
|
|
685
|
+
? diagnosticCodes.doctor_config_valid
|
|
686
|
+
: diagnosticCodes.doctor_config_invalid
|
|
687
|
+
: workspaceInspection.exists
|
|
688
|
+
? diagnosticCodes.doctor_config_invalid
|
|
689
|
+
: diagnosticCodes.doctor_config_missing,
|
|
690
|
+
status: configStatus,
|
|
691
|
+
severity: configStatus === 'error'
|
|
692
|
+
? 'error'
|
|
693
|
+
: configStatus === 'warning'
|
|
694
|
+
? 'warning'
|
|
695
|
+
: 'info',
|
|
696
|
+
title: 'Workspace configuration',
|
|
697
|
+
summary: config
|
|
698
|
+
? `Valid workspace configuration${configDiagnostics.length > 0 ? `; ${diagnosticsSummary(configDiagnostics)}` : ''}.`
|
|
699
|
+
: workspaceInspection.exists
|
|
700
|
+
? `Workspace configuration is invalid: ${diagnosticsSummary(configDiagnostics)}`
|
|
701
|
+
: 'Workspace configuration is missing.',
|
|
702
|
+
source: workspaceInspection.path,
|
|
703
|
+
observedAt,
|
|
704
|
+
coverage: 'strict schema, semantic checks, digest',
|
|
705
|
+
evidence: [
|
|
706
|
+
fileEvidence(workspaceInspection.path, observedAt, workspaceInspection.digest),
|
|
707
|
+
],
|
|
708
|
+
nextActions: config
|
|
709
|
+
? []
|
|
710
|
+
: workspaceInspection.exists
|
|
711
|
+
? [
|
|
712
|
+
'Inspect and repair the configuration manually; doctor does not overwrite it.',
|
|
713
|
+
]
|
|
714
|
+
: ['Run pappra init to create configuration.'],
|
|
715
|
+
}));
|
|
716
|
+
const machineDiagnostics = machineInspection.diagnostics;
|
|
717
|
+
const machineStatus = machine
|
|
718
|
+
? statusForDiagnostics(machineDiagnostics, 'ok')
|
|
719
|
+
: machineInspection.exists
|
|
720
|
+
? 'error'
|
|
721
|
+
: 'warning';
|
|
722
|
+
checks.push(makeCheck({
|
|
723
|
+
id: 'workspace.machine',
|
|
724
|
+
code: machineStatus === 'ok'
|
|
725
|
+
? diagnosticCodes.doctor_machine_valid
|
|
726
|
+
: machineStatus === 'error'
|
|
727
|
+
? diagnosticCodes.doctor_machine_invalid
|
|
728
|
+
: diagnosticCodes.doctor_config_missing,
|
|
729
|
+
status: machineStatus,
|
|
730
|
+
severity: machineStatus === 'error'
|
|
731
|
+
? 'error'
|
|
732
|
+
: machineStatus === 'warning'
|
|
733
|
+
? 'warning'
|
|
734
|
+
: 'info',
|
|
735
|
+
title: 'Local machine configuration',
|
|
736
|
+
summary: machine
|
|
737
|
+
? `Valid machine configuration${machineDiagnostics.length > 0 ? `; ${diagnosticsSummary(machineDiagnostics)}` : ''}.`
|
|
738
|
+
: machineInspection.exists
|
|
739
|
+
? `Machine configuration is invalid: ${diagnosticsSummary(machineDiagnostics)}`
|
|
740
|
+
: 'Machine configuration is missing; local paths/hosts/runtime are not bound.',
|
|
741
|
+
source: machineInspection.path,
|
|
742
|
+
observedAt,
|
|
743
|
+
coverage: 'strict schema and workspace identity',
|
|
744
|
+
evidence: [
|
|
745
|
+
fileEvidence(machineInspection.path, observedAt, machineInspection.digest),
|
|
746
|
+
],
|
|
747
|
+
nextActions: machine
|
|
748
|
+
? []
|
|
749
|
+
: [
|
|
750
|
+
'Run pappra init with --config (including hosts) or an interactive TTY.',
|
|
751
|
+
],
|
|
752
|
+
}));
|
|
753
|
+
const discoveryDiagnostics = discovery.diagnostics;
|
|
754
|
+
const repositoryErrors = discoveryDiagnostics.filter((entry) => entry.severity === 'error' &&
|
|
755
|
+
(entry.code === diagnosticCodes.repositoryPathMissing ||
|
|
756
|
+
entry.code === diagnosticCodes.repositoryNotGit ||
|
|
757
|
+
entry.code === diagnosticCodes.repositorySymlinkEscape ||
|
|
758
|
+
entry.code === diagnosticCodes.repositoryNestedWorkspace ||
|
|
759
|
+
entry.code === diagnosticCodes.externalRepositoryUnmapped ||
|
|
760
|
+
entry.code === diagnosticCodes.externalRepositoryInvalid));
|
|
761
|
+
const repositoryWarnings = discoveryDiagnostics.filter((entry) => entry.severity === 'warning');
|
|
762
|
+
const repoStatus = repositoryErrors.length > 0
|
|
763
|
+
? 'error'
|
|
764
|
+
: repositoryWarnings.length > 0 ||
|
|
765
|
+
discovery.unresolvedRepositories.length > 0
|
|
766
|
+
? 'warning'
|
|
767
|
+
: 'ok';
|
|
768
|
+
checks.push(makeCheck({
|
|
769
|
+
id: 'workspace.repositories',
|
|
770
|
+
code: repoStatus === 'ok'
|
|
771
|
+
? diagnosticCodes.doctor_repository_ok
|
|
772
|
+
: diagnosticCodes.doctor_repository_issues,
|
|
773
|
+
status: repoStatus,
|
|
774
|
+
severity: repoStatus === 'error'
|
|
775
|
+
? 'error'
|
|
776
|
+
: repoStatus === 'warning'
|
|
777
|
+
? 'warning'
|
|
778
|
+
: 'info',
|
|
779
|
+
title: 'Repositories and worktrees',
|
|
780
|
+
summary: `Discovered ${discovery.repositories.length} repository checkout(s).` +
|
|
781
|
+
(discovery.boundaries.length > 0
|
|
782
|
+
? ` Nested workspace boundaries observed (not entered): ${discovery.boundaries.join(', ')}.`
|
|
783
|
+
: '') +
|
|
784
|
+
(repositoryErrors.length + repositoryWarnings.length > 0
|
|
785
|
+
? ` ${diagnosticsSummary([...repositoryErrors, ...repositoryWarnings])}`
|
|
786
|
+
: ''),
|
|
787
|
+
source: root,
|
|
788
|
+
observedAt,
|
|
789
|
+
coverage: 'read-only discovery; no fetch/source scan/scripts',
|
|
790
|
+
evidence: discovery.repositories.flatMap((repository) => repository.git.evidence.slice(0, 1)),
|
|
791
|
+
nextActions: repoStatus === 'error'
|
|
792
|
+
? [
|
|
793
|
+
'Correct repository declarations or local mappings manually; doctor does not clone or move repositories.',
|
|
794
|
+
]
|
|
795
|
+
: [],
|
|
796
|
+
}));
|
|
797
|
+
const knowledgeDiagnostics = discoveryDiagnostics.filter((entry) => entry.code === diagnosticCodes.knowledgeSourceMissing ||
|
|
798
|
+
entry.code === diagnosticCodes.knowledgeSourceSymlinkEscape ||
|
|
799
|
+
entry.code === diagnosticCodes.knowledgeSourceNotReadable);
|
|
800
|
+
checks.push(makeCheck({
|
|
801
|
+
id: 'workspace.knowledge',
|
|
802
|
+
code: knowledgeDiagnostics.some((entry) => entry.severity === 'error')
|
|
803
|
+
? diagnosticCodes.doctor_knowledge_issues
|
|
804
|
+
: diagnosticCodes.doctor_knowledge_issues,
|
|
805
|
+
status: knowledgeDiagnostics.some((entry) => entry.severity === 'error')
|
|
806
|
+
? 'warning'
|
|
807
|
+
: 'ok',
|
|
808
|
+
severity: knowledgeDiagnostics.some((entry) => entry.severity === 'error')
|
|
809
|
+
? 'warning'
|
|
810
|
+
: 'info',
|
|
811
|
+
title: 'Knowledge and rules',
|
|
812
|
+
summary: `Discovered ${discovery.documents.length} rule/document candidate(s) in place.${knowledgeDiagnostics.length > 0 ? ` ${diagnosticsSummary(knowledgeDiagnostics)}` : ''}`,
|
|
813
|
+
source: configPath ?? root,
|
|
814
|
+
observedAt,
|
|
815
|
+
coverage: 'paths and provenance only; document contents not read',
|
|
816
|
+
evidence: discovery.documents
|
|
817
|
+
.slice(0, 5)
|
|
818
|
+
.map((document) => fileEvidence(document.path, observedAt, null)),
|
|
819
|
+
nextActions: knowledgeDiagnostics.length > 0
|
|
820
|
+
? [
|
|
821
|
+
'Inspect declared knowledge sources; doctor does not copy or repair documentation.',
|
|
822
|
+
]
|
|
823
|
+
: [],
|
|
824
|
+
}));
|
|
825
|
+
checks.push(makeCheck({
|
|
826
|
+
id: 'workspace.validation',
|
|
827
|
+
code: diagnosticCodes.doctor_validation_unverified,
|
|
828
|
+
status: 'ok',
|
|
829
|
+
severity: 'info',
|
|
830
|
+
title: 'Validation command candidates',
|
|
831
|
+
summary: `Discovered ${discovery.validationCandidates.length} validation candidate(s); none are executed, PATH-resolved, or proven runnable.`,
|
|
832
|
+
source: configPath ?? root,
|
|
833
|
+
observedAt,
|
|
834
|
+
coverage: 'declarations from config/package scripts only',
|
|
835
|
+
nextActions: [
|
|
836
|
+
'Select specific commands and run them only within an authorized task/environment.',
|
|
837
|
+
],
|
|
838
|
+
}));
|
|
839
|
+
checks.push(await inspectInstallState(root, observedAt));
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
for (const [id, title] of [
|
|
843
|
+
['workspace.config', 'Workspace configuration'],
|
|
844
|
+
['workspace.machine', 'Local machine configuration'],
|
|
845
|
+
['workspace.repositories', 'Repositories and worktrees'],
|
|
846
|
+
['workspace.knowledge', 'Knowledge and rules'],
|
|
847
|
+
['workspace.validation', 'Validation command candidates'],
|
|
848
|
+
['install.workspace', 'Managed workspace installation state'],
|
|
849
|
+
]) {
|
|
850
|
+
checks.push(makeCheck({
|
|
851
|
+
id,
|
|
852
|
+
code: diagnosticCodes.doctor_workspace_not_registered,
|
|
853
|
+
status: 'unknown',
|
|
854
|
+
severity: 'warning',
|
|
855
|
+
title,
|
|
856
|
+
summary: 'Not checked because no registered workspace root is available.',
|
|
857
|
+
source: resolution.startDirectory ?? resolution.requestedPath,
|
|
858
|
+
observedAt,
|
|
859
|
+
coverage: 'not checked',
|
|
860
|
+
nextActions: ['Run from a registered workspace or run pappra init.'],
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const passiveVersion = config?.runtimeVersion ?? machine?.runtime?.version ?? null;
|
|
865
|
+
const passiveRuntime = passiveVersion === null
|
|
866
|
+
? null
|
|
867
|
+
: await inspectRuntimeStatePassive(passiveVersion, dataDir);
|
|
868
|
+
checks.push(runtimeCheck(config?.runtimeVersion ?? null, machine?.runtime ?? null, passiveRuntime, machinePath, observedAt, dataDir));
|
|
869
|
+
checks.push(makeCheck({
|
|
870
|
+
id: 'runtime.references',
|
|
871
|
+
code: diagnosticCodes.doctor_references_incomplete,
|
|
872
|
+
status: 'unknown',
|
|
873
|
+
severity: 'info',
|
|
874
|
+
title: 'Runtime reference coverage',
|
|
875
|
+
summary: machine?.runtime === null || machine?.runtime === undefined
|
|
876
|
+
? 'No current workspace runtime reference. Global runtime references were not checked.'
|
|
877
|
+
: `Current workspace references ${machine.runtime.version}; other workspaces and global processes were not checked.`,
|
|
878
|
+
source: machinePath ?? dataDir,
|
|
879
|
+
observedAt,
|
|
880
|
+
coverage: 'current workspace machine runtime only',
|
|
881
|
+
evidence: machine?.runtime === null || machine?.runtime === undefined
|
|
882
|
+
? []
|
|
883
|
+
: [fileEvidence(machine.runtime.executable, observedAt, null)],
|
|
884
|
+
nextActions: [
|
|
885
|
+
'Do not remove runtime from this report alone; provide explicit known references or complete a future global reference check.',
|
|
886
|
+
],
|
|
887
|
+
}));
|
|
888
|
+
const selectedHosts = machine?.selectedHosts ?? [];
|
|
889
|
+
const hostRootBase = root ?? homeDir;
|
|
890
|
+
const installedHostBundle = (await Promise.all(selectedHosts.map(async (host) => {
|
|
891
|
+
const path = join(hostRootBase, '.pappra', 'local', 'hosts', host, 'pappra-host.json');
|
|
892
|
+
return (await observePath(path)).kind === 'file' ? path : null;
|
|
893
|
+
}))).find((path) => path !== null) ?? null;
|
|
894
|
+
const installedHostSkill = (await Promise.all(selectedHosts.map(async (host) => {
|
|
895
|
+
const path = join(hostRootBase, '.pappra', 'local', 'hosts', host, 'skills', 'pappra', 'SKILL.md');
|
|
896
|
+
return (await observePath(path)).kind === 'file' ? path : null;
|
|
897
|
+
}))).find((path) => path !== null) ?? null;
|
|
898
|
+
checks.push(...hostChecks(selectedHosts, homeDir, pathEnv, observedAt));
|
|
899
|
+
checks.push(makeCheck({
|
|
900
|
+
id: 'host.integration',
|
|
901
|
+
code: diagnosticCodes.doctor_host_integration_unsupported,
|
|
902
|
+
status: installedHostBundle === null ? 'unsupported' : 'partial',
|
|
903
|
+
severity: 'info',
|
|
904
|
+
title: 'Host integration',
|
|
905
|
+
summary: installedHostBundle === null
|
|
906
|
+
? 'No P09 host bundle manifest was observed under the selected host roots; no host, session, or model was started.'
|
|
907
|
+
: `P09 host bundle manifest exists at ${installedHostBundle}; native trust, reload and callability remain unverified and no host was started.`,
|
|
908
|
+
source: installedHostBundle ?? root ?? homeDir,
|
|
909
|
+
observedAt,
|
|
910
|
+
coverage: installedHostBundle === null ? 'unsupported' : 'file-wiring-only',
|
|
911
|
+
nextActions: installedHostBundle === null
|
|
912
|
+
? ['Install and verify a selected host explicitly when authorized.']
|
|
913
|
+
: [
|
|
914
|
+
'Run the read-only host verify path; native trust/reload/callability still require an explicit host check.',
|
|
915
|
+
],
|
|
916
|
+
}));
|
|
917
|
+
checks.push(makeCheck({
|
|
918
|
+
id: 'host.skills',
|
|
919
|
+
code: diagnosticCodes.doctor_skill_verification_unsupported,
|
|
920
|
+
status: installedHostSkill === null ? 'unsupported' : 'partial',
|
|
921
|
+
severity: 'info',
|
|
922
|
+
title: 'Skill/bundle freshness',
|
|
923
|
+
summary: installedHostSkill === null
|
|
924
|
+
? 'No packaged P09 host skill was observed under the selected host roots; no stale-copy claim is made.'
|
|
925
|
+
: `Packaged P09 skill exists at ${installedHostSkill}; native loader/trust freshness remains unverified.`,
|
|
926
|
+
source: installedHostSkill ?? root ?? homeDir,
|
|
927
|
+
observedAt,
|
|
928
|
+
coverage: installedHostSkill === null ? 'unsupported' : 'file-copy-only',
|
|
929
|
+
nextActions: installedHostSkill === null
|
|
930
|
+
? [
|
|
931
|
+
'Install and verify a selected host bundle explicitly when authorized.',
|
|
932
|
+
]
|
|
933
|
+
: [
|
|
934
|
+
'Inspect the host bundle manifest/digest; native trust and reload are separate.',
|
|
935
|
+
],
|
|
936
|
+
}));
|
|
937
|
+
checks.push(toolCheck('codegraph', config?.code.graph ?? null, pathEnv, observedAt));
|
|
938
|
+
checks.push(toolCheck('serena', config?.code.symbols ?? null, pathEnv, observedAt));
|
|
939
|
+
checks.push(makeCheck({
|
|
940
|
+
id: 'tool.index',
|
|
941
|
+
code: diagnosticCodes.doctor_tool_discovered_unverified,
|
|
942
|
+
status: 'unsupported',
|
|
943
|
+
severity: 'info',
|
|
944
|
+
title: 'Code index',
|
|
945
|
+
summary: 'P08 code-index preparation/health is not implemented; doctor did not build or query an index.',
|
|
946
|
+
source: root ?? dataDir,
|
|
947
|
+
observedAt,
|
|
948
|
+
coverage: 'unsupported',
|
|
949
|
+
nextActions: [
|
|
950
|
+
'Use P08 capabilities when available; doctor never builds an index.',
|
|
951
|
+
],
|
|
952
|
+
}));
|
|
953
|
+
const counts = {
|
|
954
|
+
ok: checks.filter((entry) => entry.status === 'ok').length,
|
|
955
|
+
warning: checks.filter((entry) => entry.status === 'warning').length,
|
|
956
|
+
error: checks.filter((entry) => entry.status === 'error').length,
|
|
957
|
+
unknown: checks.filter((entry) => entry.status === 'unknown').length,
|
|
958
|
+
partial: checks.filter((entry) => entry.status === 'partial').length,
|
|
959
|
+
unsupported: checks.filter((entry) => entry.status === 'unsupported')
|
|
960
|
+
.length,
|
|
961
|
+
};
|
|
962
|
+
const blocking = checks.some((entry) => entry.status === 'error' ||
|
|
963
|
+
entry.status === 'warning' ||
|
|
964
|
+
entry.status === 'partial' ||
|
|
965
|
+
(entry.status === 'unknown' && entry.severity !== 'info') ||
|
|
966
|
+
(entry.status === 'unsupported' && entry.severity === 'warning'));
|
|
967
|
+
const hasError = checks.some((entry) => entry.status === 'error');
|
|
968
|
+
const nextActions = [
|
|
969
|
+
...new Set(checks.flatMap((entry) => entry.nextActions)),
|
|
970
|
+
].slice(0, 16);
|
|
971
|
+
const report = {
|
|
972
|
+
schema: DOCTOR_REPORT_SCHEMA,
|
|
973
|
+
generatedAt: observedAt,
|
|
974
|
+
requestedRoot: resolution.requestedPath,
|
|
975
|
+
workspace: {
|
|
976
|
+
root,
|
|
977
|
+
state: resolution.state,
|
|
978
|
+
configPath,
|
|
979
|
+
machinePath,
|
|
980
|
+
},
|
|
981
|
+
runtime: {
|
|
982
|
+
targetVersion: config?.runtimeVersion ?? null,
|
|
983
|
+
machineRuntime: machine?.runtime === null || machine?.runtime === undefined
|
|
984
|
+
? null
|
|
985
|
+
: {
|
|
986
|
+
version: machine.runtime.version,
|
|
987
|
+
executable: machine.runtime.executable,
|
|
988
|
+
verifiedAt: machine.runtime.verifiedAt,
|
|
989
|
+
},
|
|
990
|
+
dataDir,
|
|
991
|
+
passiveStatus: passiveRuntime?.status ?? null,
|
|
992
|
+
},
|
|
993
|
+
scope: {
|
|
994
|
+
checked: [
|
|
995
|
+
'workspace-root',
|
|
996
|
+
'workspace-config',
|
|
997
|
+
'machine-config',
|
|
998
|
+
'repositories',
|
|
999
|
+
'knowledge',
|
|
1000
|
+
'validation',
|
|
1001
|
+
'workspace-install-state',
|
|
1002
|
+
'runtime-passive-state',
|
|
1003
|
+
'current-workspace-runtime-reference',
|
|
1004
|
+
'passive-host-discovery',
|
|
1005
|
+
'configured-tool-discovery',
|
|
1006
|
+
],
|
|
1007
|
+
notChecked: [
|
|
1008
|
+
'other-workspace-runtime-references',
|
|
1009
|
+
'global-runtime-removal-safety',
|
|
1010
|
+
'host-callability',
|
|
1011
|
+
'host-trust',
|
|
1012
|
+
'model-session-history',
|
|
1013
|
+
'code-index-content',
|
|
1014
|
+
'windows-platform',
|
|
1015
|
+
],
|
|
1016
|
+
},
|
|
1017
|
+
checks,
|
|
1018
|
+
summary: {
|
|
1019
|
+
status: hasError ? 'blocked' : blocking ? 'needs_attention' : 'ok',
|
|
1020
|
+
exitCode: blocking ? 1 : 0,
|
|
1021
|
+
counts,
|
|
1022
|
+
nextActions,
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
const parsed = doctorReportSchema.safeParse(report);
|
|
1026
|
+
if (!parsed.success) {
|
|
1027
|
+
throw new Error(`Internal doctor report is invalid: ${parsed.error.issues[0]?.message ?? 'unknown issue'}`);
|
|
1028
|
+
}
|
|
1029
|
+
return parsed.data;
|
|
1030
|
+
}
|