pi-crew 0.2.3 → 0.2.5
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/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { PiTeamsConfig } from "../../config/config.ts";
|
|
3
|
-
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
4
|
-
import type { TeamToolDetails } from "../team-tool-types.ts";
|
|
5
|
-
import { toolResult, type PiTeamsToolResult } from "../tool-result.ts";
|
|
6
|
-
|
|
7
|
-
export type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "model">> & {
|
|
8
|
-
sessionId?: string;
|
|
9
|
-
modelRegistry?: unknown;
|
|
10
|
-
sessionManager?: { getBranch?: () => unknown[] };
|
|
11
|
-
events?: { emit?: (event: string, data: unknown) => void };
|
|
12
|
-
metricRegistry?: MetricRegistry;
|
|
13
|
-
signal?: AbortSignal;
|
|
14
|
-
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
15
|
-
abortForegroundRun?: (runId: string) => boolean;
|
|
16
|
-
onRunStarted?: (runId: string) => void;
|
|
17
|
-
onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
|
|
18
|
-
config?: PiTeamsConfig;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function withSessionId<T extends Pick<ExtensionContext, "sessionManager">>(ctx: T): T & { sessionId?: string } {
|
|
22
|
-
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
23
|
-
return sessionId ? { ...ctx, sessionId } : { ...ctx };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
27
|
-
return toolResult(text, details, isError);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function formatScoped(name: string, source: string, description: string): string {
|
|
31
|
-
return `- ${name} (${source}): ${description}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function extractTextContent(content: unknown): string {
|
|
35
|
-
if (typeof content === "string") return content;
|
|
36
|
-
if (!Array.isArray(content)) return "";
|
|
37
|
-
return content.map((part) => part && typeof part === "object" && !Array.isArray(part) && typeof (part as { text?: unknown }).text === "string" ? (part as { text: string }).text : "").filter(Boolean).join("\n");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function buildParentContext(ctx: TeamContext): string | undefined {
|
|
41
|
-
const branch = ctx.sessionManager?.getBranch?.();
|
|
42
|
-
if (!Array.isArray(branch) || branch.length === 0) return undefined;
|
|
43
|
-
const parts: string[] = [];
|
|
44
|
-
for (const entry of branch.slice(-20)) {
|
|
45
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
46
|
-
const record = entry as { type?: unknown; message?: unknown; summary?: unknown };
|
|
47
|
-
if (record.type === "compaction" && typeof record.summary === "string") parts.push(`[Summary]: ${record.summary}`);
|
|
48
|
-
const message = record.message && typeof record.message === "object" && !Array.isArray(record.message) ? record.message as { role?: unknown; content?: unknown } : undefined;
|
|
49
|
-
if (!message || (message.role !== "user" && message.role !== "assistant")) continue;
|
|
50
|
-
const text = extractTextContent(message.content).trim();
|
|
51
|
-
if (text) parts.push(`[${message.role === "user" ? "User" : "Assistant"}]: ${text}`);
|
|
52
|
-
}
|
|
53
|
-
if (!parts.length) return undefined;
|
|
54
|
-
return [`# Parent Conversation Context`, "The following context was inherited from the parent Pi session. Treat it as reference-only.", "", parts.join("\n\n")].join("\n");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function configRecord(config: unknown): Record<string, unknown> {
|
|
58
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) return {};
|
|
59
|
-
return config as Record<string, unknown>;
|
|
60
|
-
}
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { PiTeamsConfig } from "../../config/config.ts";
|
|
3
|
+
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
4
|
+
import type { TeamToolDetails } from "../team-tool-types.ts";
|
|
5
|
+
import { toolResult, type PiTeamsToolResult } from "../tool-result.ts";
|
|
6
|
+
|
|
7
|
+
export type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "model">> & {
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
modelRegistry?: unknown;
|
|
10
|
+
sessionManager?: { getBranch?: () => unknown[] };
|
|
11
|
+
events?: { emit?: (event: string, data: unknown) => void };
|
|
12
|
+
metricRegistry?: MetricRegistry;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
15
|
+
abortForegroundRun?: (runId: string) => boolean;
|
|
16
|
+
onRunStarted?: (runId: string) => void;
|
|
17
|
+
onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
|
|
18
|
+
config?: PiTeamsConfig;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function withSessionId<T extends Pick<ExtensionContext, "sessionManager">>(ctx: T): T & { sessionId?: string } {
|
|
22
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
23
|
+
return sessionId ? { ...ctx, sessionId } : { ...ctx };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
27
|
+
return toolResult(text, details, isError);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatScoped(name: string, source: string, description: string): string {
|
|
31
|
+
return `- ${name} (${source}): ${description}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractTextContent(content: unknown): string {
|
|
35
|
+
if (typeof content === "string") return content;
|
|
36
|
+
if (!Array.isArray(content)) return "";
|
|
37
|
+
return content.map((part) => part && typeof part === "object" && !Array.isArray(part) && typeof (part as { text?: unknown }).text === "string" ? (part as { text: string }).text : "").filter(Boolean).join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildParentContext(ctx: TeamContext): string | undefined {
|
|
41
|
+
const branch = ctx.sessionManager?.getBranch?.();
|
|
42
|
+
if (!Array.isArray(branch) || branch.length === 0) return undefined;
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
for (const entry of branch.slice(-20)) {
|
|
45
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
46
|
+
const record = entry as { type?: unknown; message?: unknown; summary?: unknown };
|
|
47
|
+
if (record.type === "compaction" && typeof record.summary === "string") parts.push(`[Summary]: ${record.summary}`);
|
|
48
|
+
const message = record.message && typeof record.message === "object" && !Array.isArray(record.message) ? record.message as { role?: unknown; content?: unknown } : undefined;
|
|
49
|
+
if (!message || (message.role !== "user" && message.role !== "assistant")) continue;
|
|
50
|
+
const text = extractTextContent(message.content).trim();
|
|
51
|
+
if (text) parts.push(`[${message.role === "user" ? "User" : "Assistant"}]: ${text}`);
|
|
52
|
+
}
|
|
53
|
+
if (!parts.length) return undefined;
|
|
54
|
+
return [`# Parent Conversation Context`, "The following context was inherited from the parent Pi session. Treat it as reference-only.", "", parts.join("\n\n")].join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function configRecord(config: unknown): Record<string, unknown> {
|
|
58
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return {};
|
|
59
|
+
return config as Record<string, unknown>;
|
|
60
|
+
}
|
|
@@ -1,242 +1,242 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
5
|
-
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
6
|
-
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
7
|
-
import { loadConfig } from "../../config/config.ts";
|
|
8
|
-
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
9
|
-
import { DEFAULT_PATHS } from "../../config/defaults.ts";
|
|
10
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
11
|
-
import { getPiSpawnCommand } from "../../runtime/pi-spawn.ts";
|
|
12
|
-
import { validateResources } from "../validate-resources.ts";
|
|
13
|
-
import { detectDrift, formatDriftReport, type DriftReport } from "../../config/drift-detector.ts";
|
|
14
|
-
import { TeamToolParams } from "../../schema/team-tool-schema.ts";
|
|
15
|
-
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
16
|
-
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
17
|
-
|
|
18
|
-
interface DoctorCheck {
|
|
19
|
-
label: string;
|
|
20
|
-
ok: boolean;
|
|
21
|
-
detail: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function firstOutputLine(stdout: string | null | undefined, stderr: string | null | undefined): string {
|
|
25
|
-
const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
|
|
26
|
-
return output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "available";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function commandExists(command: string, args: string[]): { ok: boolean; detail: string } {
|
|
30
|
-
try {
|
|
31
|
-
const output = spawnSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
32
|
-
if (output.error) {
|
|
33
|
-
return { ok: false, detail: output.error.message };
|
|
34
|
-
}
|
|
35
|
-
if (output.status !== 0) {
|
|
36
|
-
return { ok: false, detail: firstOutputLine(output.stdout, output.stderr) || `status ${output.status}` };
|
|
37
|
-
}
|
|
38
|
-
return { ok: true, detail: firstOutputLine(output.stdout, output.stderr) };
|
|
39
|
-
} catch (error) {
|
|
40
|
-
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function piCommandExists(): { ok: boolean; detail: string } {
|
|
45
|
-
const spec = getPiSpawnCommand(["--version"]);
|
|
46
|
-
const output = commandExists(spec.command, spec.args);
|
|
47
|
-
if (!output.ok) return output;
|
|
48
|
-
const executable = spec.command === "pi" ? "pi" : `${spec.command} ${spec.args[0] ?? ""}`.trim();
|
|
49
|
-
return { ok: true, detail: `${output.detail} (${executable})` };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function checkWritableDir(dir: string): { ok: boolean; detail: string } {
|
|
53
|
-
try {
|
|
54
|
-
if (!fs.existsSync(dir)) return { ok: false, detail: `${dir}: missing` };
|
|
55
|
-
if (!fs.statSync(dir).isDirectory()) return { ok: false, detail: `${dir}: not a directory` };
|
|
56
|
-
// fs.accessSync(W_OK) is unreliable on Windows; verify by writing a temp file.
|
|
57
|
-
const probePath = `${dir}/.pi-crew-write-test`;
|
|
58
|
-
try {
|
|
59
|
-
fs.writeFileSync(probePath, "ok", "utf-8");
|
|
60
|
-
fs.rmSync(probePath, { force: true });
|
|
61
|
-
} catch {
|
|
62
|
-
return { ok: false, detail: `${dir}: not writable (write test failed)` };
|
|
63
|
-
}
|
|
64
|
-
return { ok: true, detail: dir };
|
|
65
|
-
} catch (error) {
|
|
66
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
-
return { ok: false, detail: `${dir}: ${message}` };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function auditJsonSchema(schema: unknown): string[] {
|
|
72
|
-
const issues: string[] = [];
|
|
73
|
-
const walk = (node: unknown): void => {
|
|
74
|
-
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
75
|
-
const record = node as Record<string, unknown>;
|
|
76
|
-
if (Array.isArray(record.type)) issues.push("schema node uses array-valued type");
|
|
77
|
-
if (record.description && !record.type && !record.anyOf && !record.oneOf && !record.allOf && !record.properties) issues.push(`description-only schema node: ${record.description}`);
|
|
78
|
-
if (record.type === "array" && !record.items) issues.push("array schema missing items");
|
|
79
|
-
if (record.type && (record.anyOf || record.oneOf)) issues.push("schema node combines type with union keyword");
|
|
80
|
-
for (const value of Object.values(record)) {
|
|
81
|
-
if (Array.isArray(value)) for (const item of value) walk(item);
|
|
82
|
-
else walk(value);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
walk(schema);
|
|
86
|
-
return issues;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function makeLine(check: DoctorCheck): string {
|
|
90
|
-
return `- ${check.ok ? "OK" : "FAIL"} ${check.label}: ${check.detail}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function section(title: string, checks: () => DoctorCheck[]): string[] {
|
|
94
|
-
try {
|
|
95
|
-
return [title, ...checks().map(makeLine)];
|
|
96
|
-
} catch (error) {
|
|
97
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
98
|
-
return [title, `- FAIL ${title}: ${detail}`];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export interface TeamDoctorReportInput {
|
|
103
|
-
cwd: string;
|
|
104
|
-
configPath: string;
|
|
105
|
-
configErrors: string[];
|
|
106
|
-
configWarnings: string[];
|
|
107
|
-
model?: { provider: string; id: string };
|
|
108
|
-
validationErrors: number;
|
|
109
|
-
validationWarnings: number;
|
|
110
|
-
smokeChildPi?: { ok: boolean; detail: string };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface TeamDoctorReport {
|
|
114
|
-
text: string;
|
|
115
|
-
hasErrors: boolean;
|
|
116
|
-
drift?: DriftReport;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorReport {
|
|
120
|
-
// Compute drift once — reused in both Drift section and return value
|
|
121
|
-
const driftResult = detectDrift(
|
|
122
|
-
{
|
|
123
|
-
agents: allAgents(discoverAgents(input.cwd)).map((a) => a.name),
|
|
124
|
-
teams: allTeams(discoverTeams(input.cwd)).map((t) => t.name),
|
|
125
|
-
workflows: allWorkflows(discoverWorkflows(input.cwd)).map((w) => w.name),
|
|
126
|
-
},
|
|
127
|
-
loadConfig(input.cwd).config,
|
|
128
|
-
);
|
|
129
|
-
const sections = [
|
|
130
|
-
section("Runtime", () => {
|
|
131
|
-
const git = commandExists("git", ["--version"]);
|
|
132
|
-
const pi = piCommandExists();
|
|
133
|
-
return [
|
|
134
|
-
{ label: "cwd", ok: true, detail: input.cwd },
|
|
135
|
-
{ label: "platform", ok: true, detail: `${process.platform}/${process.arch} node=${process.version}` },
|
|
136
|
-
{ label: "pi command", ok: pi.ok, detail: pi.detail },
|
|
137
|
-
{ label: "git command", ok: git.ok, detail: git.detail },
|
|
138
|
-
{ label: "config", ok: input.configErrors.length === 0, detail: `${input.configPath} (${input.configErrors.length} errors)` },
|
|
139
|
-
{ label: "model", ok: true, detail: input.model ? `${input.model.provider}/${input.model.id}` : "not available in this context" },
|
|
140
|
-
{ label: "config warnings", ok: true, detail: `${input.configWarnings.length} warnings` },
|
|
141
|
-
];
|
|
142
|
-
}),
|
|
143
|
-
section("Filesystem", () => {
|
|
144
|
-
const userWritable = checkWritableDir(userCrewRoot());
|
|
145
|
-
const projectWritable = checkWritableDir(projectCrewRoot(input.cwd));
|
|
146
|
-
return [
|
|
147
|
-
{ label: "user state", ok: userWritable.ok || userWritable.detail.endsWith(": missing"), detail: userWritable.detail },
|
|
148
|
-
{ label: "project state", ok: projectWritable.ok || projectWritable.detail.endsWith(": missing"), detail: projectWritable.detail },
|
|
149
|
-
{ label: "project state root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.runsSubdir) },
|
|
150
|
-
{ label: "artifacts root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.artifactsSubdir) },
|
|
151
|
-
];
|
|
152
|
-
}),
|
|
153
|
-
section("Discovery", () => {
|
|
154
|
-
const discoveredAgents = allAgents(discoverAgents(input.cwd));
|
|
155
|
-
const discoveredTeams = allTeams(discoverTeams(input.cwd));
|
|
156
|
-
const discoveredWorkflows = allWorkflows(discoverWorkflows(input.cwd));
|
|
157
|
-
const agentModelHints = discoveredAgents.filter((agent) => agent.model || agent.fallbackModels?.length).length;
|
|
158
|
-
return [
|
|
159
|
-
{ label: "agents", ok: true, detail: `${discoveredAgents.length} discovered` },
|
|
160
|
-
{ label: "teams", ok: true, detail: `${discoveredTeams.length} discovered` },
|
|
161
|
-
{ label: "workflows", ok: true, detail: `${discoveredWorkflows.length} discovered` },
|
|
162
|
-
{ label: "resource model hints", ok: true, detail: `${agentModelHints} agents declare model/fallback preferences` },
|
|
163
|
-
];
|
|
164
|
-
}),
|
|
165
|
-
section("Resource validation", () => [{
|
|
166
|
-
label: "resource validation",
|
|
167
|
-
ok: input.validationErrors === 0,
|
|
168
|
-
detail: `${input.validationErrors} errors, ${input.validationWarnings} warnings`,
|
|
169
|
-
}]),
|
|
170
|
-
section("Drift", () => {
|
|
171
|
-
const driftErrors = driftResult.items.filter((item) => item.severity === "error").length;
|
|
172
|
-
const driftWarnings = driftResult.items.filter((item) => item.severity === "warning").length;
|
|
173
|
-
return [{
|
|
174
|
-
label: "config drift",
|
|
175
|
-
ok: !driftResult.hasDrift || driftErrors === 0,
|
|
176
|
-
detail: driftResult.hasDrift ? `${driftErrors} errors, ${driftWarnings} warnings` : "no drift detected",
|
|
177
|
-
}];
|
|
178
|
-
}),
|
|
179
|
-
section("Schema", () => {
|
|
180
|
-
const schemaIssues = auditJsonSchema(TeamToolParams);
|
|
181
|
-
return [{ label: "strict-provider schema", ok: schemaIssues.length === 0, detail: schemaIssues.length ? schemaIssues.slice(0, 3).join("; ") : "team tool schema compatible" }];
|
|
182
|
-
}),
|
|
183
|
-
section("Async/result delivery", () => [
|
|
184
|
-
{ label: "result watcher", ok: true, detail: "fs.watch with polling fallback for EMFILE/ENOSPC/EPERM" },
|
|
185
|
-
{ label: "async notifier", ok: true, detail: "session-stale guarded completion notifications enabled" },
|
|
186
|
-
]),
|
|
187
|
-
section("Worktrees", () => [
|
|
188
|
-
{ label: "leader repository", ok: true, detail: input.cwd },
|
|
189
|
-
{ label: "cleanup policy", ok: true, detail: "dirty worktrees preserved unless force is set" },
|
|
190
|
-
]),
|
|
191
|
-
];
|
|
192
|
-
if (input.smokeChildPi) {
|
|
193
|
-
sections.push([`Child check`, `- ${input.smokeChildPi.ok ? "OK" : "FAIL"} child Pi smoke: ${input.smokeChildPi.detail}`]);
|
|
194
|
-
}
|
|
195
|
-
const lines = ["pi-crew doctor report"];
|
|
196
|
-
for (const block of sections) {
|
|
197
|
-
if (block.length > 0) {
|
|
198
|
-
lines.push(...block);
|
|
199
|
-
lines.push("");
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
if (lines.at(-1) === "") lines.pop();
|
|
203
|
-
const text = lines.join("\n");
|
|
204
|
-
return { text, hasErrors: sections.some((sectionLines) => sectionLines.some((line) => line.includes("FAIL"))), drift: driftResult.hasDrift ? driftResult : undefined };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {}): PiTeamsToolResult {
|
|
208
|
-
const loadedConfig = loadConfig(ctx.cwd);
|
|
209
|
-
let smokeChildPi: { ok: boolean; detail: string } | undefined;
|
|
210
|
-
if (configRecord(params.config).smokeChildPi === true) {
|
|
211
|
-
try {
|
|
212
|
-
const spec = getPiSpawnCommand(["--mode", "json", "-p", "Reply with exactly PI-TEAMS-SMOKE-OK"]);
|
|
213
|
-
const output = execFileSync(spec.command, spec.args, {
|
|
214
|
-
cwd: ctx.cwd,
|
|
215
|
-
encoding: "utf-8",
|
|
216
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
217
|
-
timeout: 15_000,
|
|
218
|
-
}).trim();
|
|
219
|
-
smokeChildPi = { ok: output.includes("PI-TEAMS-SMOKE-OK"), detail: output.split("\n").slice(-1)[0] ?? "completed" };
|
|
220
|
-
} catch (error) {
|
|
221
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
-
smokeChildPi = { ok: false, detail: message };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
const validation = validateResources(ctx.cwd);
|
|
226
|
-
const { text, hasErrors, drift } = buildTeamDoctorReport({
|
|
227
|
-
cwd: ctx.cwd,
|
|
228
|
-
configPath: loadedConfig.path,
|
|
229
|
-
configErrors: loadedConfig.error ? [loadedConfig.error] : [],
|
|
230
|
-
configWarnings: loadedConfig.warnings ?? [],
|
|
231
|
-
model: ctx.model,
|
|
232
|
-
validationErrors: validation.issues.filter((issue) => issue.level === "error").length,
|
|
233
|
-
validationWarnings: validation.issues.filter((issue) => issue.level === "warning").length,
|
|
234
|
-
smokeChildPi,
|
|
235
|
-
});
|
|
236
|
-
// Append detailed drift section if any drift was detected
|
|
237
|
-
let finalText = text;
|
|
238
|
-
if (drift?.hasDrift) {
|
|
239
|
-
finalText = `${text}\n\nDrift details:\n${formatDriftReport(drift)}`;
|
|
240
|
-
}
|
|
241
|
-
return result(finalText, { action: "doctor", status: hasErrors ? "error" : "ok" }, hasErrors);
|
|
242
|
-
}
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
5
|
+
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
6
|
+
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
7
|
+
import { loadConfig } from "../../config/config.ts";
|
|
8
|
+
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
9
|
+
import { DEFAULT_PATHS } from "../../config/defaults.ts";
|
|
10
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
11
|
+
import { getPiSpawnCommand } from "../../runtime/pi-spawn.ts";
|
|
12
|
+
import { validateResources } from "../validate-resources.ts";
|
|
13
|
+
import { detectDrift, formatDriftReport, type DriftReport } from "../../config/drift-detector.ts";
|
|
14
|
+
import { TeamToolParams } from "../../schema/team-tool-schema.ts";
|
|
15
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
16
|
+
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
17
|
+
|
|
18
|
+
interface DoctorCheck {
|
|
19
|
+
label: string;
|
|
20
|
+
ok: boolean;
|
|
21
|
+
detail: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function firstOutputLine(stdout: string | null | undefined, stderr: string | null | undefined): string {
|
|
25
|
+
const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
|
|
26
|
+
return output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "available";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function commandExists(command: string, args: string[]): { ok: boolean; detail: string } {
|
|
30
|
+
try {
|
|
31
|
+
const output = spawnSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
32
|
+
if (output.error) {
|
|
33
|
+
return { ok: false, detail: output.error.message };
|
|
34
|
+
}
|
|
35
|
+
if (output.status !== 0) {
|
|
36
|
+
return { ok: false, detail: firstOutputLine(output.stdout, output.stderr) || `status ${output.status}` };
|
|
37
|
+
}
|
|
38
|
+
return { ok: true, detail: firstOutputLine(output.stdout, output.stderr) };
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function piCommandExists(): { ok: boolean; detail: string } {
|
|
45
|
+
const spec = getPiSpawnCommand(["--version"]);
|
|
46
|
+
const output = commandExists(spec.command, spec.args);
|
|
47
|
+
if (!output.ok) return output;
|
|
48
|
+
const executable = spec.command === "pi" ? "pi" : `${spec.command} ${spec.args[0] ?? ""}`.trim();
|
|
49
|
+
return { ok: true, detail: `${output.detail} (${executable})` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function checkWritableDir(dir: string): { ok: boolean; detail: string } {
|
|
53
|
+
try {
|
|
54
|
+
if (!fs.existsSync(dir)) return { ok: false, detail: `${dir}: missing` };
|
|
55
|
+
if (!fs.statSync(dir).isDirectory()) return { ok: false, detail: `${dir}: not a directory` };
|
|
56
|
+
// fs.accessSync(W_OK) is unreliable on Windows; verify by writing a temp file.
|
|
57
|
+
const probePath = `${dir}/.pi-crew-write-test`;
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(probePath, "ok", "utf-8");
|
|
60
|
+
fs.rmSync(probePath, { force: true });
|
|
61
|
+
} catch {
|
|
62
|
+
return { ok: false, detail: `${dir}: not writable (write test failed)` };
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, detail: dir };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
return { ok: false, detail: `${dir}: ${message}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function auditJsonSchema(schema: unknown): string[] {
|
|
72
|
+
const issues: string[] = [];
|
|
73
|
+
const walk = (node: unknown): void => {
|
|
74
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
75
|
+
const record = node as Record<string, unknown>;
|
|
76
|
+
if (Array.isArray(record.type)) issues.push("schema node uses array-valued type");
|
|
77
|
+
if (record.description && !record.type && !record.anyOf && !record.oneOf && !record.allOf && !record.properties) issues.push(`description-only schema node: ${record.description}`);
|
|
78
|
+
if (record.type === "array" && !record.items) issues.push("array schema missing items");
|
|
79
|
+
if (record.type && (record.anyOf || record.oneOf)) issues.push("schema node combines type with union keyword");
|
|
80
|
+
for (const value of Object.values(record)) {
|
|
81
|
+
if (Array.isArray(value)) for (const item of value) walk(item);
|
|
82
|
+
else walk(value);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
walk(schema);
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function makeLine(check: DoctorCheck): string {
|
|
90
|
+
return `- ${check.ok ? "OK" : "FAIL"} ${check.label}: ${check.detail}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function section(title: string, checks: () => DoctorCheck[]): string[] {
|
|
94
|
+
try {
|
|
95
|
+
return [title, ...checks().map(makeLine)];
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
98
|
+
return [title, `- FAIL ${title}: ${detail}`];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TeamDoctorReportInput {
|
|
103
|
+
cwd: string;
|
|
104
|
+
configPath: string;
|
|
105
|
+
configErrors: string[];
|
|
106
|
+
configWarnings: string[];
|
|
107
|
+
model?: { provider: string; id: string };
|
|
108
|
+
validationErrors: number;
|
|
109
|
+
validationWarnings: number;
|
|
110
|
+
smokeChildPi?: { ok: boolean; detail: string };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface TeamDoctorReport {
|
|
114
|
+
text: string;
|
|
115
|
+
hasErrors: boolean;
|
|
116
|
+
drift?: DriftReport;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorReport {
|
|
120
|
+
// Compute drift once — reused in both Drift section and return value
|
|
121
|
+
const driftResult = detectDrift(
|
|
122
|
+
{
|
|
123
|
+
agents: allAgents(discoverAgents(input.cwd)).map((a) => a.name),
|
|
124
|
+
teams: allTeams(discoverTeams(input.cwd)).map((t) => t.name),
|
|
125
|
+
workflows: allWorkflows(discoverWorkflows(input.cwd)).map((w) => w.name),
|
|
126
|
+
},
|
|
127
|
+
loadConfig(input.cwd).config,
|
|
128
|
+
);
|
|
129
|
+
const sections = [
|
|
130
|
+
section("Runtime", () => {
|
|
131
|
+
const git = commandExists("git", ["--version"]);
|
|
132
|
+
const pi = piCommandExists();
|
|
133
|
+
return [
|
|
134
|
+
{ label: "cwd", ok: true, detail: input.cwd },
|
|
135
|
+
{ label: "platform", ok: true, detail: `${process.platform}/${process.arch} node=${process.version}` },
|
|
136
|
+
{ label: "pi command", ok: pi.ok, detail: pi.detail },
|
|
137
|
+
{ label: "git command", ok: git.ok, detail: git.detail },
|
|
138
|
+
{ label: "config", ok: input.configErrors.length === 0, detail: `${input.configPath} (${input.configErrors.length} errors)` },
|
|
139
|
+
{ label: "model", ok: true, detail: input.model ? `${input.model.provider}/${input.model.id}` : "not available in this context" },
|
|
140
|
+
{ label: "config warnings", ok: true, detail: `${input.configWarnings.length} warnings` },
|
|
141
|
+
];
|
|
142
|
+
}),
|
|
143
|
+
section("Filesystem", () => {
|
|
144
|
+
const userWritable = checkWritableDir(userCrewRoot());
|
|
145
|
+
const projectWritable = checkWritableDir(projectCrewRoot(input.cwd));
|
|
146
|
+
return [
|
|
147
|
+
{ label: "user state", ok: userWritable.ok || userWritable.detail.endsWith(": missing"), detail: userWritable.detail },
|
|
148
|
+
{ label: "project state", ok: projectWritable.ok || projectWritable.detail.endsWith(": missing"), detail: projectWritable.detail },
|
|
149
|
+
{ label: "project state root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.runsSubdir) },
|
|
150
|
+
{ label: "artifacts root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.artifactsSubdir) },
|
|
151
|
+
];
|
|
152
|
+
}),
|
|
153
|
+
section("Discovery", () => {
|
|
154
|
+
const discoveredAgents = allAgents(discoverAgents(input.cwd));
|
|
155
|
+
const discoveredTeams = allTeams(discoverTeams(input.cwd));
|
|
156
|
+
const discoveredWorkflows = allWorkflows(discoverWorkflows(input.cwd));
|
|
157
|
+
const agentModelHints = discoveredAgents.filter((agent) => agent.model || agent.fallbackModels?.length).length;
|
|
158
|
+
return [
|
|
159
|
+
{ label: "agents", ok: true, detail: `${discoveredAgents.length} discovered` },
|
|
160
|
+
{ label: "teams", ok: true, detail: `${discoveredTeams.length} discovered` },
|
|
161
|
+
{ label: "workflows", ok: true, detail: `${discoveredWorkflows.length} discovered` },
|
|
162
|
+
{ label: "resource model hints", ok: true, detail: `${agentModelHints} agents declare model/fallback preferences` },
|
|
163
|
+
];
|
|
164
|
+
}),
|
|
165
|
+
section("Resource validation", () => [{
|
|
166
|
+
label: "resource validation",
|
|
167
|
+
ok: input.validationErrors === 0,
|
|
168
|
+
detail: `${input.validationErrors} errors, ${input.validationWarnings} warnings`,
|
|
169
|
+
}]),
|
|
170
|
+
section("Drift", () => {
|
|
171
|
+
const driftErrors = driftResult.items.filter((item) => item.severity === "error").length;
|
|
172
|
+
const driftWarnings = driftResult.items.filter((item) => item.severity === "warning").length;
|
|
173
|
+
return [{
|
|
174
|
+
label: "config drift",
|
|
175
|
+
ok: !driftResult.hasDrift || driftErrors === 0,
|
|
176
|
+
detail: driftResult.hasDrift ? `${driftErrors} errors, ${driftWarnings} warnings` : "no drift detected",
|
|
177
|
+
}];
|
|
178
|
+
}),
|
|
179
|
+
section("Schema", () => {
|
|
180
|
+
const schemaIssues = auditJsonSchema(TeamToolParams);
|
|
181
|
+
return [{ label: "strict-provider schema", ok: schemaIssues.length === 0, detail: schemaIssues.length ? schemaIssues.slice(0, 3).join("; ") : "team tool schema compatible" }];
|
|
182
|
+
}),
|
|
183
|
+
section("Async/result delivery", () => [
|
|
184
|
+
{ label: "result watcher", ok: true, detail: "fs.watch with polling fallback for EMFILE/ENOSPC/EPERM" },
|
|
185
|
+
{ label: "async notifier", ok: true, detail: "session-stale guarded completion notifications enabled" },
|
|
186
|
+
]),
|
|
187
|
+
section("Worktrees", () => [
|
|
188
|
+
{ label: "leader repository", ok: true, detail: input.cwd },
|
|
189
|
+
{ label: "cleanup policy", ok: true, detail: "dirty worktrees preserved unless force is set" },
|
|
190
|
+
]),
|
|
191
|
+
];
|
|
192
|
+
if (input.smokeChildPi) {
|
|
193
|
+
sections.push([`Child check`, `- ${input.smokeChildPi.ok ? "OK" : "FAIL"} child Pi smoke: ${input.smokeChildPi.detail}`]);
|
|
194
|
+
}
|
|
195
|
+
const lines = ["pi-crew doctor report"];
|
|
196
|
+
for (const block of sections) {
|
|
197
|
+
if (block.length > 0) {
|
|
198
|
+
lines.push(...block);
|
|
199
|
+
lines.push("");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (lines.at(-1) === "") lines.pop();
|
|
203
|
+
const text = lines.join("\n");
|
|
204
|
+
return { text, hasErrors: sections.some((sectionLines) => sectionLines.some((line) => line.includes("FAIL"))), drift: driftResult.hasDrift ? driftResult : undefined };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {}): PiTeamsToolResult {
|
|
208
|
+
const loadedConfig = loadConfig(ctx.cwd);
|
|
209
|
+
let smokeChildPi: { ok: boolean; detail: string } | undefined;
|
|
210
|
+
if (configRecord(params.config).smokeChildPi === true) {
|
|
211
|
+
try {
|
|
212
|
+
const spec = getPiSpawnCommand(["--mode", "json", "-p", "Reply with exactly PI-TEAMS-SMOKE-OK"]);
|
|
213
|
+
const output = execFileSync(spec.command, spec.args, {
|
|
214
|
+
cwd: ctx.cwd,
|
|
215
|
+
encoding: "utf-8",
|
|
216
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
217
|
+
timeout: 15_000,
|
|
218
|
+
}).trim();
|
|
219
|
+
smokeChildPi = { ok: output.includes("PI-TEAMS-SMOKE-OK"), detail: output.split("\n").slice(-1)[0] ?? "completed" };
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
+
smokeChildPi = { ok: false, detail: message };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const validation = validateResources(ctx.cwd);
|
|
226
|
+
const { text, hasErrors, drift } = buildTeamDoctorReport({
|
|
227
|
+
cwd: ctx.cwd,
|
|
228
|
+
configPath: loadedConfig.path,
|
|
229
|
+
configErrors: loadedConfig.error ? [loadedConfig.error] : [],
|
|
230
|
+
configWarnings: loadedConfig.warnings ?? [],
|
|
231
|
+
model: ctx.model,
|
|
232
|
+
validationErrors: validation.issues.filter((issue) => issue.level === "error").length,
|
|
233
|
+
validationWarnings: validation.issues.filter((issue) => issue.level === "warning").length,
|
|
234
|
+
smokeChildPi,
|
|
235
|
+
});
|
|
236
|
+
// Append detailed drift section if any drift was detected
|
|
237
|
+
let finalText = text;
|
|
238
|
+
if (drift?.hasDrift) {
|
|
239
|
+
finalText = `${text}\n\nDrift details:\n${formatDriftReport(drift)}`;
|
|
240
|
+
}
|
|
241
|
+
return result(finalText, { action: "doctor", status: hasErrors ? "error" : "ok" }, hasErrors);
|
|
242
|
+
}
|