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,89 +1,89 @@
|
|
|
1
|
-
import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
-
import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
|
|
3
|
-
import type { TeamEvent } from "../state/event-log.ts";
|
|
4
|
-
import type { ExportedRunBundle } from "./run-export.ts";
|
|
5
|
-
|
|
6
|
-
export interface BundleValidationResult {
|
|
7
|
-
ok: boolean;
|
|
8
|
-
errors: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
|
|
16
|
-
if (!isRecord(value)) {
|
|
17
|
-
errors.push(`manifest.artifacts[${index}] must be an object.`);
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
const before = errors.length;
|
|
21
|
-
if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
|
|
22
|
-
if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
|
|
23
|
-
if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
|
|
24
|
-
if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
|
|
25
|
-
if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
|
|
26
|
-
return errors.length === before;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
|
|
30
|
-
if (!isRecord(value)) {
|
|
31
|
-
errors.push("manifest must be an object.");
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
const before = errors.length;
|
|
35
|
-
if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
|
|
36
|
-
for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
|
|
37
|
-
if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
|
|
38
|
-
}
|
|
39
|
-
if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
|
|
40
|
-
if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
|
|
41
|
-
if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
|
|
42
|
-
else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
|
|
43
|
-
return errors.length === before;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
|
|
47
|
-
if (!isRecord(value)) {
|
|
48
|
-
errors.push(`tasks[${index}] must be an object.`);
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
const before = errors.length;
|
|
52
|
-
for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
|
|
53
|
-
if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
|
|
54
|
-
}
|
|
55
|
-
if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
|
|
56
|
-
if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
|
|
57
|
-
return errors.length === before;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
|
|
61
|
-
if (!isRecord(value)) {
|
|
62
|
-
errors.push(`events[${index}] must be an object.`);
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
const before = errors.length;
|
|
66
|
-
for (const field of ["time", "type", "runId"] as const) {
|
|
67
|
-
if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
|
|
68
|
-
}
|
|
69
|
-
return errors.length === before;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function validateRunBundle(value: unknown): BundleValidationResult {
|
|
73
|
-
const errors: string[] = [];
|
|
74
|
-
if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
|
|
75
|
-
if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
|
|
76
|
-
if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
|
|
77
|
-
validateManifest(value.manifest, errors);
|
|
78
|
-
if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
|
|
79
|
-
else value.tasks.forEach((task, index) => validateTask(task, index, errors));
|
|
80
|
-
if (!Array.isArray(value.events)) errors.push("events must be an array.");
|
|
81
|
-
else value.events.forEach((event, index) => validateEvent(event, index, errors));
|
|
82
|
-
if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
|
|
83
|
-
return { ok: errors.length === 0, errors };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
|
|
87
|
-
const validation = validateRunBundle(value);
|
|
88
|
-
if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
|
|
89
|
-
}
|
|
1
|
+
import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
+
import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
|
|
3
|
+
import type { TeamEvent } from "../state/event-log.ts";
|
|
4
|
+
import type { ExportedRunBundle } from "./run-export.ts";
|
|
5
|
+
|
|
6
|
+
export interface BundleValidationResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
|
|
16
|
+
if (!isRecord(value)) {
|
|
17
|
+
errors.push(`manifest.artifacts[${index}] must be an object.`);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const before = errors.length;
|
|
21
|
+
if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
|
|
22
|
+
if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
|
|
23
|
+
if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
|
|
24
|
+
if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
|
|
25
|
+
if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
|
|
26
|
+
return errors.length === before;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
|
|
30
|
+
if (!isRecord(value)) {
|
|
31
|
+
errors.push("manifest must be an object.");
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const before = errors.length;
|
|
35
|
+
if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
|
|
36
|
+
for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
|
|
37
|
+
if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
|
|
38
|
+
}
|
|
39
|
+
if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
|
|
40
|
+
if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
|
|
41
|
+
if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
|
|
42
|
+
else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
|
|
43
|
+
return errors.length === before;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
|
|
47
|
+
if (!isRecord(value)) {
|
|
48
|
+
errors.push(`tasks[${index}] must be an object.`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const before = errors.length;
|
|
52
|
+
for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
|
|
53
|
+
if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
|
|
54
|
+
}
|
|
55
|
+
if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
|
|
56
|
+
if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
|
|
57
|
+
return errors.length === before;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
|
|
61
|
+
if (!isRecord(value)) {
|
|
62
|
+
errors.push(`events[${index}] must be an object.`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const before = errors.length;
|
|
66
|
+
for (const field of ["time", "type", "runId"] as const) {
|
|
67
|
+
if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
|
|
68
|
+
}
|
|
69
|
+
return errors.length === before;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validateRunBundle(value: unknown): BundleValidationResult {
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
|
|
75
|
+
if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
|
|
76
|
+
if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
|
|
77
|
+
validateManifest(value.manifest, errors);
|
|
78
|
+
if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
|
|
79
|
+
else value.tasks.forEach((task, index) => validateTask(task, index, errors));
|
|
80
|
+
if (!Array.isArray(value.events)) errors.push("events must be an array.");
|
|
81
|
+
else value.events.forEach((event, index) => validateEvent(event, index, errors));
|
|
82
|
+
if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
|
|
83
|
+
return { ok: errors.length === 0, errors };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
|
|
87
|
+
const validation = validateRunBundle(value);
|
|
88
|
+
if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
|
|
89
|
+
}
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
|
-
import { writeArtifact } from "../state/artifact-store.ts";
|
|
6
|
-
import { readEvents, type TeamEvent } from "../state/event-log.ts";
|
|
7
|
-
import { redactSecrets } from "../utils/redaction.ts";
|
|
8
|
-
|
|
9
|
-
/** Replace absolute paths containing home directory with ~/ */
|
|
10
|
-
function redactHomePaths<T>(obj: T): T {
|
|
11
|
-
const home = os.homedir();
|
|
12
|
-
if (!home) return redactSecrets(obj) as T;
|
|
13
|
-
const json = JSON.stringify(obj);
|
|
14
|
-
const safe = json.split(home).join("~");
|
|
15
|
-
return redactSecrets(JSON.parse(safe)) as T;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ExportedRunBundle {
|
|
19
|
-
schemaVersion: 1;
|
|
20
|
-
exportedAt: string;
|
|
21
|
-
manifest: TeamRunManifest;
|
|
22
|
-
tasks: TeamTaskState[];
|
|
23
|
-
events: TeamEvent[];
|
|
24
|
-
artifactPaths: string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function exportRunBundle(manifest: TeamRunManifest, tasks: TeamTaskState[]): { jsonPath: string; markdownPath: string } {
|
|
28
|
-
const events = readEvents(manifest.eventsPath);
|
|
29
|
-
const safeManifest = redactHomePaths(manifest);
|
|
30
|
-
const safeTasks = redactHomePaths(tasks);
|
|
31
|
-
const safeEvents = redactHomePaths(events);
|
|
32
|
-
const bundle: ExportedRunBundle = {
|
|
33
|
-
schemaVersion: 1,
|
|
34
|
-
exportedAt: new Date().toISOString(),
|
|
35
|
-
manifest: safeManifest as TeamRunManifest,
|
|
36
|
-
tasks: safeTasks as TeamTaskState[],
|
|
37
|
-
events: safeEvents as TeamEvent[],
|
|
38
|
-
artifactPaths: safeManifest.artifacts.map((artifact) => artifact.path),
|
|
39
|
-
};
|
|
40
|
-
const json = writeArtifact(manifest.artifactsRoot, {
|
|
41
|
-
kind: "metadata",
|
|
42
|
-
relativePath: "export/run-export.json",
|
|
43
|
-
producer: "run-export",
|
|
44
|
-
content: `${JSON.stringify(bundle, null, 2)}\n`,
|
|
45
|
-
});
|
|
46
|
-
const markdown = writeArtifact(manifest.artifactsRoot, {
|
|
47
|
-
kind: "summary",
|
|
48
|
-
relativePath: "export/run-export.md",
|
|
49
|
-
producer: "run-export",
|
|
50
|
-
content: [
|
|
51
|
-
`# pi-crew export ${safeManifest.runId}`,
|
|
52
|
-
"",
|
|
53
|
-
`Exported: ${bundle.exportedAt}`,
|
|
54
|
-
`Status: ${safeManifest.status}`,
|
|
55
|
-
`Team: ${safeManifest.team}`,
|
|
56
|
-
`Workflow: ${safeManifest.workflow ?? "(none)"}`,
|
|
57
|
-
`Goal: ${safeManifest.goal}`,
|
|
58
|
-
"",
|
|
59
|
-
"## Tasks",
|
|
60
|
-
...safeTasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
61
|
-
"",
|
|
62
|
-
"## Artifacts",
|
|
63
|
-
...(safeManifest.artifacts.length ? safeManifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}`) : ["- (none)"]),
|
|
64
|
-
"",
|
|
65
|
-
"## Recent Events",
|
|
66
|
-
...(safeEvents.slice(-20).map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`)),
|
|
67
|
-
"",
|
|
68
|
-
].join("\n"),
|
|
69
|
-
});
|
|
70
|
-
// Ensure artifact dirs are materialized before returning paths on filesystems with delayed metadata.
|
|
71
|
-
fs.statSync(path.dirname(json.path));
|
|
72
|
-
return { jsonPath: json.path, markdownPath: markdown.path };
|
|
73
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
|
+
import { writeArtifact } from "../state/artifact-store.ts";
|
|
6
|
+
import { readEvents, type TeamEvent } from "../state/event-log.ts";
|
|
7
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
8
|
+
|
|
9
|
+
/** Replace absolute paths containing home directory with ~/ */
|
|
10
|
+
function redactHomePaths<T>(obj: T): T {
|
|
11
|
+
const home = os.homedir();
|
|
12
|
+
if (!home) return redactSecrets(obj) as T;
|
|
13
|
+
const json = JSON.stringify(obj);
|
|
14
|
+
const safe = json.split(home).join("~");
|
|
15
|
+
return redactSecrets(JSON.parse(safe)) as T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ExportedRunBundle {
|
|
19
|
+
schemaVersion: 1;
|
|
20
|
+
exportedAt: string;
|
|
21
|
+
manifest: TeamRunManifest;
|
|
22
|
+
tasks: TeamTaskState[];
|
|
23
|
+
events: TeamEvent[];
|
|
24
|
+
artifactPaths: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function exportRunBundle(manifest: TeamRunManifest, tasks: TeamTaskState[]): { jsonPath: string; markdownPath: string } {
|
|
28
|
+
const events = readEvents(manifest.eventsPath);
|
|
29
|
+
const safeManifest = redactHomePaths(manifest);
|
|
30
|
+
const safeTasks = redactHomePaths(tasks);
|
|
31
|
+
const safeEvents = redactHomePaths(events);
|
|
32
|
+
const bundle: ExportedRunBundle = {
|
|
33
|
+
schemaVersion: 1,
|
|
34
|
+
exportedAt: new Date().toISOString(),
|
|
35
|
+
manifest: safeManifest as TeamRunManifest,
|
|
36
|
+
tasks: safeTasks as TeamTaskState[],
|
|
37
|
+
events: safeEvents as TeamEvent[],
|
|
38
|
+
artifactPaths: safeManifest.artifacts.map((artifact) => artifact.path),
|
|
39
|
+
};
|
|
40
|
+
const json = writeArtifact(manifest.artifactsRoot, {
|
|
41
|
+
kind: "metadata",
|
|
42
|
+
relativePath: "export/run-export.json",
|
|
43
|
+
producer: "run-export",
|
|
44
|
+
content: `${JSON.stringify(bundle, null, 2)}\n`,
|
|
45
|
+
});
|
|
46
|
+
const markdown = writeArtifact(manifest.artifactsRoot, {
|
|
47
|
+
kind: "summary",
|
|
48
|
+
relativePath: "export/run-export.md",
|
|
49
|
+
producer: "run-export",
|
|
50
|
+
content: [
|
|
51
|
+
`# pi-crew export ${safeManifest.runId}`,
|
|
52
|
+
"",
|
|
53
|
+
`Exported: ${bundle.exportedAt}`,
|
|
54
|
+
`Status: ${safeManifest.status}`,
|
|
55
|
+
`Team: ${safeManifest.team}`,
|
|
56
|
+
`Workflow: ${safeManifest.workflow ?? "(none)"}`,
|
|
57
|
+
`Goal: ${safeManifest.goal}`,
|
|
58
|
+
"",
|
|
59
|
+
"## Tasks",
|
|
60
|
+
...safeTasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
61
|
+
"",
|
|
62
|
+
"## Artifacts",
|
|
63
|
+
...(safeManifest.artifacts.length ? safeManifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}`) : ["- (none)"]),
|
|
64
|
+
"",
|
|
65
|
+
"## Recent Events",
|
|
66
|
+
...(safeEvents.slice(-20).map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`)),
|
|
67
|
+
"",
|
|
68
|
+
].join("\n"),
|
|
69
|
+
});
|
|
70
|
+
// Ensure artifact dirs are materialized before returning paths on filesystems with delayed metadata.
|
|
71
|
+
fs.statSync(path.dirname(json.path));
|
|
72
|
+
return { jsonPath: json.path, markdownPath: markdown.path };
|
|
73
|
+
}
|
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { assertRunBundle } from "./run-bundle-schema.ts";
|
|
4
|
-
import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
5
|
-
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
6
|
-
import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
7
|
-
import { detectImportConflicts, type ConflictReport } from "../runtime/delta-conflict.ts";
|
|
8
|
-
|
|
9
|
-
export interface ImportedRunBundleInfo {
|
|
10
|
-
runId: string;
|
|
11
|
-
importedAt: string;
|
|
12
|
-
bundlePath: string;
|
|
13
|
-
summaryPath: string;
|
|
14
|
-
conflictReport?: ConflictReport;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function importRoot(cwd: string, scope: "project" | "user"): string {
|
|
18
|
-
const base = scope === "project" ? projectCrewRoot(cwd) : userCrewRoot();
|
|
19
|
-
return path.join(base, DEFAULT_PATHS.state.importsSubdir);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function importRunBundle(cwd: string, bundlePath: string, scope: "project" | "user" = "project"): ImportedRunBundleInfo {
|
|
23
|
-
const resolvedPath = path.isAbsolute(bundlePath) ? bundlePath : path.resolve(cwd, bundlePath);
|
|
24
|
-
// Path containment: only allow reading bundles from cwd or user home
|
|
25
|
-
const allowedBases = [cwd];
|
|
26
|
-
try { allowedBases.push(userCrewRoot()); } catch { /* ignore */ }
|
|
27
|
-
try { allowedBases.push(projectCrewRoot(cwd)); } catch { /* ignore */ }
|
|
28
|
-
const isContained = allowedBases.some((base) => resolvedPath.startsWith(base + path.sep) || resolvedPath === base);
|
|
29
|
-
if (!isContained) throw new Error(`Import path must be within project directory or crew root: ${resolvedPath}`);
|
|
30
|
-
const raw = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
|
|
31
|
-
assertRunBundle(raw);
|
|
32
|
-
const runId = assertSafePathId("runId", raw.manifest.runId);
|
|
33
|
-
const importedAt = new Date().toISOString();
|
|
34
|
-
|
|
35
|
-
// Non-blocking conflict detection: compare incoming bundle against any existing state.
|
|
36
|
-
let conflictReport: ConflictReport | undefined;
|
|
37
|
-
try {
|
|
38
|
-
const existingManifestPath = path.join(importRoot(cwd, scope), runId, "run-export.json");
|
|
39
|
-
if (fs.existsSync(existingManifestPath)) {
|
|
40
|
-
const existingRaw = JSON.parse(fs.readFileSync(existingManifestPath, "utf-8")) as { manifest?: Record<string, unknown>; tasks?: unknown[] };
|
|
41
|
-
conflictReport = detectImportConflicts(
|
|
42
|
-
{ manifest: raw.manifest as unknown as Record<string, unknown>, tasks: raw.tasks as unknown[] },
|
|
43
|
-
{ manifest: existingRaw.manifest, tasks: existingRaw.tasks },
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Conflict detection is best-effort; do not block import on failure.
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const importsRoot = importRoot(cwd, scope);
|
|
51
|
-
fs.mkdirSync(importsRoot, { recursive: true });
|
|
52
|
-
if (fs.lstatSync(importsRoot).isSymbolicLink()) throw new Error(`Invalid import root: ${importsRoot}`);
|
|
53
|
-
resolveRealContainedPath(path.dirname(importsRoot), path.basename(importsRoot));
|
|
54
|
-
const root = resolveContainedRelativePath(importsRoot, runId, "runId");
|
|
55
|
-
fs.mkdirSync(root, { recursive: true });
|
|
56
|
-
// TOCTOU note: mkdirSync would throw EEXIST if a symlink already existed.
|
|
57
|
-
// The lstatSync check catches a symlink swapped in between mkdirSync and the check
|
|
58
|
-
// (theoretically possible but requires local attacker with exact timing).
|
|
59
|
-
// resolveRealContainedPath provides an additional real-path containment barrier.
|
|
60
|
-
if (fs.lstatSync(root).isSymbolicLink()) throw new Error(`Invalid import directory: ${root}`);
|
|
61
|
-
resolveRealContainedPath(importsRoot, runId);
|
|
62
|
-
const targetJson = path.join(root, "run-export.json");
|
|
63
|
-
const targetSummary = path.join(root, "README.md");
|
|
64
|
-
for (const target of [targetJson, targetSummary]) {
|
|
65
|
-
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) throw new Error(`Invalid import target: ${target}`);
|
|
66
|
-
}
|
|
67
|
-
fs.writeFileSync(targetJson, `${JSON.stringify({ ...raw, importedAt, importedFrom: resolvedPath }, null, 2)}\n`, "utf-8");
|
|
68
|
-
fs.writeFileSync(targetSummary, [
|
|
69
|
-
`# Imported pi-crew run ${runId}`,
|
|
70
|
-
"",
|
|
71
|
-
`Imported: ${importedAt}`,
|
|
72
|
-
`Source: ${resolvedPath}`,
|
|
73
|
-
`Original export: ${raw.exportedAt}`,
|
|
74
|
-
`Status: ${raw.manifest.status}`,
|
|
75
|
-
`Team: ${raw.manifest.team}`,
|
|
76
|
-
`Workflow: ${raw.manifest.workflow ?? "(none)"}`,
|
|
77
|
-
`Goal: ${raw.manifest.goal}`,
|
|
78
|
-
"",
|
|
79
|
-
"## Tasks",
|
|
80
|
-
...raw.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
81
|
-
"",
|
|
82
|
-
].join("\n"), "utf-8");
|
|
83
|
-
return { runId, importedAt, bundlePath: targetJson, summaryPath: targetSummary, ...(conflictReport?.hasConflicts ? { conflictReport } : {}) };
|
|
84
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { assertRunBundle } from "./run-bundle-schema.ts";
|
|
4
|
+
import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
5
|
+
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
6
|
+
import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
7
|
+
import { detectImportConflicts, type ConflictReport } from "../runtime/delta-conflict.ts";
|
|
8
|
+
|
|
9
|
+
export interface ImportedRunBundleInfo {
|
|
10
|
+
runId: string;
|
|
11
|
+
importedAt: string;
|
|
12
|
+
bundlePath: string;
|
|
13
|
+
summaryPath: string;
|
|
14
|
+
conflictReport?: ConflictReport;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function importRoot(cwd: string, scope: "project" | "user"): string {
|
|
18
|
+
const base = scope === "project" ? projectCrewRoot(cwd) : userCrewRoot();
|
|
19
|
+
return path.join(base, DEFAULT_PATHS.state.importsSubdir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function importRunBundle(cwd: string, bundlePath: string, scope: "project" | "user" = "project"): ImportedRunBundleInfo {
|
|
23
|
+
const resolvedPath = path.isAbsolute(bundlePath) ? bundlePath : path.resolve(cwd, bundlePath);
|
|
24
|
+
// Path containment: only allow reading bundles from cwd or user home
|
|
25
|
+
const allowedBases = [cwd];
|
|
26
|
+
try { allowedBases.push(userCrewRoot()); } catch { /* ignore */ }
|
|
27
|
+
try { allowedBases.push(projectCrewRoot(cwd)); } catch { /* ignore */ }
|
|
28
|
+
const isContained = allowedBases.some((base) => resolvedPath.startsWith(base + path.sep) || resolvedPath === base);
|
|
29
|
+
if (!isContained) throw new Error(`Import path must be within project directory or crew root: ${resolvedPath}`);
|
|
30
|
+
const raw = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
|
|
31
|
+
assertRunBundle(raw);
|
|
32
|
+
const runId = assertSafePathId("runId", raw.manifest.runId);
|
|
33
|
+
const importedAt = new Date().toISOString();
|
|
34
|
+
|
|
35
|
+
// Non-blocking conflict detection: compare incoming bundle against any existing state.
|
|
36
|
+
let conflictReport: ConflictReport | undefined;
|
|
37
|
+
try {
|
|
38
|
+
const existingManifestPath = path.join(importRoot(cwd, scope), runId, "run-export.json");
|
|
39
|
+
if (fs.existsSync(existingManifestPath)) {
|
|
40
|
+
const existingRaw = JSON.parse(fs.readFileSync(existingManifestPath, "utf-8")) as { manifest?: Record<string, unknown>; tasks?: unknown[] };
|
|
41
|
+
conflictReport = detectImportConflicts(
|
|
42
|
+
{ manifest: raw.manifest as unknown as Record<string, unknown>, tasks: raw.tasks as unknown[] },
|
|
43
|
+
{ manifest: existingRaw.manifest, tasks: existingRaw.tasks },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Conflict detection is best-effort; do not block import on failure.
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const importsRoot = importRoot(cwd, scope);
|
|
51
|
+
fs.mkdirSync(importsRoot, { recursive: true });
|
|
52
|
+
if (fs.lstatSync(importsRoot).isSymbolicLink()) throw new Error(`Invalid import root: ${importsRoot}`);
|
|
53
|
+
resolveRealContainedPath(path.dirname(importsRoot), path.basename(importsRoot));
|
|
54
|
+
const root = resolveContainedRelativePath(importsRoot, runId, "runId");
|
|
55
|
+
fs.mkdirSync(root, { recursive: true });
|
|
56
|
+
// TOCTOU note: mkdirSync would throw EEXIST if a symlink already existed.
|
|
57
|
+
// The lstatSync check catches a symlink swapped in between mkdirSync and the check
|
|
58
|
+
// (theoretically possible but requires local attacker with exact timing).
|
|
59
|
+
// resolveRealContainedPath provides an additional real-path containment barrier.
|
|
60
|
+
if (fs.lstatSync(root).isSymbolicLink()) throw new Error(`Invalid import directory: ${root}`);
|
|
61
|
+
resolveRealContainedPath(importsRoot, runId);
|
|
62
|
+
const targetJson = path.join(root, "run-export.json");
|
|
63
|
+
const targetSummary = path.join(root, "README.md");
|
|
64
|
+
for (const target of [targetJson, targetSummary]) {
|
|
65
|
+
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) throw new Error(`Invalid import target: ${target}`);
|
|
66
|
+
}
|
|
67
|
+
fs.writeFileSync(targetJson, `${JSON.stringify({ ...raw, importedAt, importedFrom: resolvedPath }, null, 2)}\n`, "utf-8");
|
|
68
|
+
fs.writeFileSync(targetSummary, [
|
|
69
|
+
`# Imported pi-crew run ${runId}`,
|
|
70
|
+
"",
|
|
71
|
+
`Imported: ${importedAt}`,
|
|
72
|
+
`Source: ${resolvedPath}`,
|
|
73
|
+
`Original export: ${raw.exportedAt}`,
|
|
74
|
+
`Status: ${raw.manifest.status}`,
|
|
75
|
+
`Team: ${raw.manifest.team}`,
|
|
76
|
+
`Workflow: ${raw.manifest.workflow ?? "(none)"}`,
|
|
77
|
+
`Goal: ${raw.manifest.goal}`,
|
|
78
|
+
"",
|
|
79
|
+
"## Tasks",
|
|
80
|
+
...raw.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
81
|
+
"",
|
|
82
|
+
].join("\n"), "utf-8");
|
|
83
|
+
return { runId, importedAt, bundlePath: targetJson, summaryPath: targetSummary, ...(conflictReport?.hasConflicts ? { conflictReport } : {}) };
|
|
84
|
+
}
|