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,360 +1,360 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Delta conflict detection for pi-crew import and resume operations.
|
|
3
|
-
*
|
|
4
|
-
* Compares incoming bundles against existing state to surface conflicts
|
|
5
|
-
* (file overwrites, status mismatches, schema drift, deleted resources)
|
|
6
|
-
* without blocking the operation — only reporting for user awareness.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// ── Types ──────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
export type ConflictKind = "file_overwrite" | "state_mismatch" | "schema_drift" | "resource_deleted";
|
|
12
|
-
|
|
13
|
-
export interface Conflict {
|
|
14
|
-
kind: ConflictKind;
|
|
15
|
-
/** File or resource path that is in conflict. */
|
|
16
|
-
path: string;
|
|
17
|
-
/** Current value or summary (optional). */
|
|
18
|
-
existing?: string;
|
|
19
|
-
/** Incoming value or summary (optional). */
|
|
20
|
-
incoming?: string;
|
|
21
|
-
severity: "error" | "warning";
|
|
22
|
-
autoResolvable: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ConflictReport {
|
|
26
|
-
hasConflicts: boolean;
|
|
27
|
-
conflicts: Conflict[];
|
|
28
|
-
summary: { errors: number; warnings: number; autoResolvable: number };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type ConflictStrategy = "skip" | "overwrite" | "merge";
|
|
32
|
-
|
|
33
|
-
export interface ConflictResolution {
|
|
34
|
-
resolved: boolean;
|
|
35
|
-
action: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── Helpers ────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
interface TaskLike {
|
|
41
|
-
id: string;
|
|
42
|
-
status: string;
|
|
43
|
-
role?: string;
|
|
44
|
-
agent?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function buildReport(conflicts: Conflict[]): ConflictReport {
|
|
48
|
-
const errors = conflicts.filter((c) => c.severity === "error").length;
|
|
49
|
-
const warnings = conflicts.filter((c) => c.severity === "warning").length;
|
|
50
|
-
const autoResolvable = conflicts.filter((c) => c.autoResolvable).length;
|
|
51
|
-
return {
|
|
52
|
-
hasConflicts: conflicts.length > 0,
|
|
53
|
-
conflicts,
|
|
54
|
-
summary: { errors, warnings, autoResolvable },
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Extract task-like objects from an unknown array, filtering out non-records.
|
|
64
|
-
*/
|
|
65
|
-
function extractTaskLikes(tasks: unknown[]): TaskLike[] {
|
|
66
|
-
return tasks
|
|
67
|
-
.filter(isRecord)
|
|
68
|
-
.map((t) => ({
|
|
69
|
-
id: typeof t.id === "string" ? t.id : "",
|
|
70
|
-
status: typeof t.status === "string" ? t.status : "",
|
|
71
|
-
role: typeof t.role === "string" ? t.role : undefined,
|
|
72
|
-
agent: typeof t.agent === "string" ? t.agent : undefined,
|
|
73
|
-
}))
|
|
74
|
-
.filter((t) => t.id !== "");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── Import Conflict Detection ──────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
export interface ImportBundle {
|
|
80
|
-
manifest: Record<string, unknown>;
|
|
81
|
-
tasks: unknown[];
|
|
82
|
-
events?: unknown[];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface ExistingState {
|
|
86
|
-
manifest?: Record<string, unknown>;
|
|
87
|
-
tasks?: unknown[];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Detect conflicts between an import bundle and existing run state.
|
|
92
|
-
*
|
|
93
|
-
* Checks:
|
|
94
|
-
* - **schema_drift**: manifest `schemaVersion` differs between import and existing.
|
|
95
|
-
* - **file_overwrite**: artifact paths in the import bundle that already exist
|
|
96
|
-
* in the current manifest's artifact list.
|
|
97
|
-
* - **state_mismatch**: task statuses differ between import and existing tasks.
|
|
98
|
-
* - **resource_deleted**: referenced agent/team/workflow in import does not exist
|
|
99
|
-
* in the current state.
|
|
100
|
-
*/
|
|
101
|
-
export function detectImportConflicts(
|
|
102
|
-
importBundle: ImportBundle,
|
|
103
|
-
existingState: ExistingState,
|
|
104
|
-
): ConflictReport {
|
|
105
|
-
const conflicts: Conflict[] = [];
|
|
106
|
-
const { manifest: incoming, tasks: incomingTasks } = importBundle;
|
|
107
|
-
const { manifest: current, tasks: currentTasks } = existingState;
|
|
108
|
-
|
|
109
|
-
// ── Schema drift ─────────────────────────────────────────────────
|
|
110
|
-
if (current) {
|
|
111
|
-
const incomingVersion = incoming.schemaVersion;
|
|
112
|
-
const currentVersion = current.schemaVersion;
|
|
113
|
-
if (
|
|
114
|
-
typeof incomingVersion !== "undefined" &&
|
|
115
|
-
typeof currentVersion !== "undefined" &&
|
|
116
|
-
incomingVersion !== currentVersion
|
|
117
|
-
) {
|
|
118
|
-
conflicts.push({
|
|
119
|
-
kind: "schema_drift",
|
|
120
|
-
path: "manifest.schemaVersion",
|
|
121
|
-
existing: String(currentVersion),
|
|
122
|
-
incoming: String(incomingVersion),
|
|
123
|
-
severity: "warning",
|
|
124
|
-
autoResolvable: true,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ── File overwrite (artifact collision) ──────────────────────────
|
|
130
|
-
const incomingArtifacts = Array.isArray(incoming.artifacts) ? incoming.artifacts : [];
|
|
131
|
-
const currentArtifacts = Array.isArray(current?.artifacts) ? current?.artifacts : [];
|
|
132
|
-
|
|
133
|
-
if (current && currentArtifacts.length > 0) {
|
|
134
|
-
const currentPaths = new Set(
|
|
135
|
-
currentArtifacts
|
|
136
|
-
.filter(isRecord)
|
|
137
|
-
.map((a) => (typeof a.path === "string" ? a.path : ""))
|
|
138
|
-
.filter((p) => p !== ""),
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
for (const artifact of incomingArtifacts) {
|
|
142
|
-
if (!isRecord(artifact)) continue;
|
|
143
|
-
const artifactPath = typeof artifact.path === "string" ? artifact.path : "";
|
|
144
|
-
if (artifactPath !== "" && currentPaths.has(artifactPath)) {
|
|
145
|
-
conflicts.push({
|
|
146
|
-
kind: "file_overwrite",
|
|
147
|
-
path: artifactPath,
|
|
148
|
-
existing: "present in current run",
|
|
149
|
-
incoming: "present in import bundle",
|
|
150
|
-
severity: "warning",
|
|
151
|
-
autoResolvable: true,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── State mismatch (task status differences) ─────────────────────
|
|
158
|
-
if (currentTasks && currentTasks.length > 0) {
|
|
159
|
-
const currentTaskMap = new Map(
|
|
160
|
-
extractTaskLikes(currentTasks).map((t) => [t.id, t]),
|
|
161
|
-
);
|
|
162
|
-
const incomingTaskList = extractTaskLikes(incomingTasks);
|
|
163
|
-
|
|
164
|
-
for (const inTask of incomingTaskList) {
|
|
165
|
-
const curTask = currentTaskMap.get(inTask.id);
|
|
166
|
-
if (curTask && curTask.status !== inTask.status) {
|
|
167
|
-
conflicts.push({
|
|
168
|
-
kind: "state_mismatch",
|
|
169
|
-
path: `tasks/${inTask.id}`,
|
|
170
|
-
existing: curTask.status,
|
|
171
|
-
incoming: inTask.status,
|
|
172
|
-
severity: "error",
|
|
173
|
-
autoResolvable: false,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ── Resource deleted (agent/team/workflow no longer in current) ──
|
|
180
|
-
if (current) {
|
|
181
|
-
const currentTeam = typeof current.team === "string" ? current.team : undefined;
|
|
182
|
-
const currentWorkflow = typeof current.workflow === "string" ? current.workflow : undefined;
|
|
183
|
-
const incomingTeam = typeof incoming.team === "string" ? incoming.team : undefined;
|
|
184
|
-
const incomingWorkflow = typeof incoming.workflow === "string" ? incoming.workflow : undefined;
|
|
185
|
-
|
|
186
|
-
if (incomingTeam && currentTeam && incomingTeam !== currentTeam) {
|
|
187
|
-
conflicts.push({
|
|
188
|
-
kind: "resource_deleted",
|
|
189
|
-
path: `team/${incomingTeam}`,
|
|
190
|
-
existing: currentTeam,
|
|
191
|
-
incoming: incomingTeam,
|
|
192
|
-
severity: "warning",
|
|
193
|
-
autoResolvable: true,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (incomingWorkflow && currentWorkflow && incomingWorkflow !== currentWorkflow) {
|
|
198
|
-
conflicts.push({
|
|
199
|
-
kind: "resource_deleted",
|
|
200
|
-
path: `workflow/${incomingWorkflow}`,
|
|
201
|
-
existing: currentWorkflow,
|
|
202
|
-
incoming: incomingWorkflow,
|
|
203
|
-
severity: "warning",
|
|
204
|
-
autoResolvable: true,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Check agent references from tasks vs current tasks
|
|
209
|
-
if (currentTasks && currentTasks.length > 0) {
|
|
210
|
-
const currentAgents = new Set(
|
|
211
|
-
extractTaskLikes(currentTasks)
|
|
212
|
-
.map((t) => t.agent)
|
|
213
|
-
.filter((a): a is string => a !== undefined),
|
|
214
|
-
);
|
|
215
|
-
const incomingTaskList = extractTaskLikes(incomingTasks);
|
|
216
|
-
for (const inTask of incomingTaskList) {
|
|
217
|
-
if (
|
|
218
|
-
inTask.agent &&
|
|
219
|
-
inTask.id !== "" &&
|
|
220
|
-
currentAgents.size > 0 &&
|
|
221
|
-
!currentAgents.has(inTask.agent) &&
|
|
222
|
-
// Only flag if there's a matching task id (agent was reassigned)
|
|
223
|
-
extractTaskLikes(currentTasks).some((ct) => ct.id === inTask.id)
|
|
224
|
-
) {
|
|
225
|
-
conflicts.push({
|
|
226
|
-
kind: "resource_deleted",
|
|
227
|
-
path: `agent/${inTask.agent}`,
|
|
228
|
-
existing: "not in current run",
|
|
229
|
-
incoming: inTask.agent,
|
|
230
|
-
severity: "warning",
|
|
231
|
-
autoResolvable: true,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return buildReport(conflicts);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ── Resume Conflict Detection ──────────────────────────────────────────
|
|
242
|
-
|
|
243
|
-
export interface SuspendedState {
|
|
244
|
-
tasks: unknown[];
|
|
245
|
-
artifacts: string[];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export interface CurrentState {
|
|
249
|
-
changedFiles: string[];
|
|
250
|
-
taskStatuses: Record<string, string>;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Detect conflicts when resuming a suspended run against current filesystem state.
|
|
255
|
-
*
|
|
256
|
-
* Checks:
|
|
257
|
-
* - **file_overwrite**: files changed since suspension.
|
|
258
|
-
* - **state_mismatch**: task statuses changed externally.
|
|
259
|
-
*/
|
|
260
|
-
export function detectResumeConflicts(
|
|
261
|
-
suspendedState: SuspendedState,
|
|
262
|
-
currentState: CurrentState,
|
|
263
|
-
): ConflictReport {
|
|
264
|
-
const conflicts: Conflict[] = [];
|
|
265
|
-
const { tasks: suspendedTasks, artifacts: suspendedArtifacts } = suspendedState;
|
|
266
|
-
const { changedFiles, taskStatuses } = currentState;
|
|
267
|
-
|
|
268
|
-
// ── File overwrite ───────────────────────────────────────────────
|
|
269
|
-
const changedSet = new Set(changedFiles);
|
|
270
|
-
for (const artifactPath of suspendedArtifacts) {
|
|
271
|
-
if (changedSet.has(artifactPath)) {
|
|
272
|
-
conflicts.push({
|
|
273
|
-
kind: "file_overwrite",
|
|
274
|
-
path: artifactPath,
|
|
275
|
-
existing: "modified since suspension",
|
|
276
|
-
incoming: "expected unchanged",
|
|
277
|
-
severity: "error",
|
|
278
|
-
autoResolvable: false,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ── State mismatch ───────────────────────────────────────────────
|
|
284
|
-
const suspendedTaskList = extractTaskLikes(suspendedTasks);
|
|
285
|
-
for (const task of suspendedTaskList) {
|
|
286
|
-
const currentStatus = taskStatuses[task.id];
|
|
287
|
-
if (currentStatus !== undefined && currentStatus !== task.status) {
|
|
288
|
-
conflicts.push({
|
|
289
|
-
kind: "state_mismatch",
|
|
290
|
-
path: `tasks/${task.id}`,
|
|
291
|
-
existing: currentStatus,
|
|
292
|
-
incoming: task.status,
|
|
293
|
-
severity: "error",
|
|
294
|
-
autoResolvable: false,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return buildReport(conflicts);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── Conflict Resolution ────────────────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Apply a resolution strategy to a conflict.
|
|
306
|
-
*
|
|
307
|
-
* - **skip**: skip the conflicting item (always resolves).
|
|
308
|
-
* - **overwrite**: replace existing with incoming (resolves for `file_overwrite`
|
|
309
|
-
* and `schema_drift`; does not resolve `state_mismatch` or `resource_deleted`).
|
|
310
|
-
* - **merge**: attempt merge — resolves `state_mismatch` with merged status;
|
|
311
|
-
* resolves others conditionally.
|
|
312
|
-
*/
|
|
313
|
-
export function resolveConflict(
|
|
314
|
-
conflict: Conflict,
|
|
315
|
-
strategy: ConflictStrategy,
|
|
316
|
-
): ConflictResolution {
|
|
317
|
-
switch (strategy) {
|
|
318
|
-
case "skip":
|
|
319
|
-
return { resolved: true, action: `Skipped ${conflict.path}` };
|
|
320
|
-
|
|
321
|
-
case "overwrite":
|
|
322
|
-
if (conflict.kind === "file_overwrite" || conflict.kind === "schema_drift") {
|
|
323
|
-
return { resolved: true, action: `Overwritten ${conflict.path} with incoming value` };
|
|
324
|
-
}
|
|
325
|
-
return {
|
|
326
|
-
resolved: false,
|
|
327
|
-
action: `Cannot overwrite ${conflict.kind} at ${conflict.path}; manual resolution required`,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
case "merge":
|
|
331
|
-
if (conflict.kind === "state_mismatch") {
|
|
332
|
-
return {
|
|
333
|
-
resolved: true,
|
|
334
|
-
action: `Merged ${conflict.path}: kept existing=${conflict.existing ?? "?"}, incoming=${conflict.incoming ?? "?"}`,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
if (conflict.kind === "resource_deleted") {
|
|
338
|
-
return {
|
|
339
|
-
resolved: true,
|
|
340
|
-
action: `Merged ${conflict.path}: using incoming resource reference`,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
if (conflict.kind === "file_overwrite") {
|
|
344
|
-
return {
|
|
345
|
-
resolved: true,
|
|
346
|
-
action: `Merged ${conflict.path}: kept both versions`,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
if (conflict.kind === "schema_drift") {
|
|
350
|
-
return {
|
|
351
|
-
resolved: true,
|
|
352
|
-
action: `Merged ${conflict.path}: using incoming schema version`,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
return {
|
|
356
|
-
resolved: false,
|
|
357
|
-
action: `Cannot merge ${conflict.kind} at ${conflict.path}`,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Delta conflict detection for pi-crew import and resume operations.
|
|
3
|
+
*
|
|
4
|
+
* Compares incoming bundles against existing state to surface conflicts
|
|
5
|
+
* (file overwrites, status mismatches, schema drift, deleted resources)
|
|
6
|
+
* without blocking the operation — only reporting for user awareness.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type ConflictKind = "file_overwrite" | "state_mismatch" | "schema_drift" | "resource_deleted";
|
|
12
|
+
|
|
13
|
+
export interface Conflict {
|
|
14
|
+
kind: ConflictKind;
|
|
15
|
+
/** File or resource path that is in conflict. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Current value or summary (optional). */
|
|
18
|
+
existing?: string;
|
|
19
|
+
/** Incoming value or summary (optional). */
|
|
20
|
+
incoming?: string;
|
|
21
|
+
severity: "error" | "warning";
|
|
22
|
+
autoResolvable: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ConflictReport {
|
|
26
|
+
hasConflicts: boolean;
|
|
27
|
+
conflicts: Conflict[];
|
|
28
|
+
summary: { errors: number; warnings: number; autoResolvable: number };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ConflictStrategy = "skip" | "overwrite" | "merge";
|
|
32
|
+
|
|
33
|
+
export interface ConflictResolution {
|
|
34
|
+
resolved: boolean;
|
|
35
|
+
action: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
interface TaskLike {
|
|
41
|
+
id: string;
|
|
42
|
+
status: string;
|
|
43
|
+
role?: string;
|
|
44
|
+
agent?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildReport(conflicts: Conflict[]): ConflictReport {
|
|
48
|
+
const errors = conflicts.filter((c) => c.severity === "error").length;
|
|
49
|
+
const warnings = conflicts.filter((c) => c.severity === "warning").length;
|
|
50
|
+
const autoResolvable = conflicts.filter((c) => c.autoResolvable).length;
|
|
51
|
+
return {
|
|
52
|
+
hasConflicts: conflicts.length > 0,
|
|
53
|
+
conflicts,
|
|
54
|
+
summary: { errors, warnings, autoResolvable },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract task-like objects from an unknown array, filtering out non-records.
|
|
64
|
+
*/
|
|
65
|
+
function extractTaskLikes(tasks: unknown[]): TaskLike[] {
|
|
66
|
+
return tasks
|
|
67
|
+
.filter(isRecord)
|
|
68
|
+
.map((t) => ({
|
|
69
|
+
id: typeof t.id === "string" ? t.id : "",
|
|
70
|
+
status: typeof t.status === "string" ? t.status : "",
|
|
71
|
+
role: typeof t.role === "string" ? t.role : undefined,
|
|
72
|
+
agent: typeof t.agent === "string" ? t.agent : undefined,
|
|
73
|
+
}))
|
|
74
|
+
.filter((t) => t.id !== "");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Import Conflict Detection ──────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
export interface ImportBundle {
|
|
80
|
+
manifest: Record<string, unknown>;
|
|
81
|
+
tasks: unknown[];
|
|
82
|
+
events?: unknown[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ExistingState {
|
|
86
|
+
manifest?: Record<string, unknown>;
|
|
87
|
+
tasks?: unknown[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Detect conflicts between an import bundle and existing run state.
|
|
92
|
+
*
|
|
93
|
+
* Checks:
|
|
94
|
+
* - **schema_drift**: manifest `schemaVersion` differs between import and existing.
|
|
95
|
+
* - **file_overwrite**: artifact paths in the import bundle that already exist
|
|
96
|
+
* in the current manifest's artifact list.
|
|
97
|
+
* - **state_mismatch**: task statuses differ between import and existing tasks.
|
|
98
|
+
* - **resource_deleted**: referenced agent/team/workflow in import does not exist
|
|
99
|
+
* in the current state.
|
|
100
|
+
*/
|
|
101
|
+
export function detectImportConflicts(
|
|
102
|
+
importBundle: ImportBundle,
|
|
103
|
+
existingState: ExistingState,
|
|
104
|
+
): ConflictReport {
|
|
105
|
+
const conflicts: Conflict[] = [];
|
|
106
|
+
const { manifest: incoming, tasks: incomingTasks } = importBundle;
|
|
107
|
+
const { manifest: current, tasks: currentTasks } = existingState;
|
|
108
|
+
|
|
109
|
+
// ── Schema drift ─────────────────────────────────────────────────
|
|
110
|
+
if (current) {
|
|
111
|
+
const incomingVersion = incoming.schemaVersion;
|
|
112
|
+
const currentVersion = current.schemaVersion;
|
|
113
|
+
if (
|
|
114
|
+
typeof incomingVersion !== "undefined" &&
|
|
115
|
+
typeof currentVersion !== "undefined" &&
|
|
116
|
+
incomingVersion !== currentVersion
|
|
117
|
+
) {
|
|
118
|
+
conflicts.push({
|
|
119
|
+
kind: "schema_drift",
|
|
120
|
+
path: "manifest.schemaVersion",
|
|
121
|
+
existing: String(currentVersion),
|
|
122
|
+
incoming: String(incomingVersion),
|
|
123
|
+
severity: "warning",
|
|
124
|
+
autoResolvable: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── File overwrite (artifact collision) ──────────────────────────
|
|
130
|
+
const incomingArtifacts = Array.isArray(incoming.artifacts) ? incoming.artifacts : [];
|
|
131
|
+
const currentArtifacts = Array.isArray(current?.artifacts) ? current?.artifacts : [];
|
|
132
|
+
|
|
133
|
+
if (current && currentArtifacts.length > 0) {
|
|
134
|
+
const currentPaths = new Set(
|
|
135
|
+
currentArtifacts
|
|
136
|
+
.filter(isRecord)
|
|
137
|
+
.map((a) => (typeof a.path === "string" ? a.path : ""))
|
|
138
|
+
.filter((p) => p !== ""),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
for (const artifact of incomingArtifacts) {
|
|
142
|
+
if (!isRecord(artifact)) continue;
|
|
143
|
+
const artifactPath = typeof artifact.path === "string" ? artifact.path : "";
|
|
144
|
+
if (artifactPath !== "" && currentPaths.has(artifactPath)) {
|
|
145
|
+
conflicts.push({
|
|
146
|
+
kind: "file_overwrite",
|
|
147
|
+
path: artifactPath,
|
|
148
|
+
existing: "present in current run",
|
|
149
|
+
incoming: "present in import bundle",
|
|
150
|
+
severity: "warning",
|
|
151
|
+
autoResolvable: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── State mismatch (task status differences) ─────────────────────
|
|
158
|
+
if (currentTasks && currentTasks.length > 0) {
|
|
159
|
+
const currentTaskMap = new Map(
|
|
160
|
+
extractTaskLikes(currentTasks).map((t) => [t.id, t]),
|
|
161
|
+
);
|
|
162
|
+
const incomingTaskList = extractTaskLikes(incomingTasks);
|
|
163
|
+
|
|
164
|
+
for (const inTask of incomingTaskList) {
|
|
165
|
+
const curTask = currentTaskMap.get(inTask.id);
|
|
166
|
+
if (curTask && curTask.status !== inTask.status) {
|
|
167
|
+
conflicts.push({
|
|
168
|
+
kind: "state_mismatch",
|
|
169
|
+
path: `tasks/${inTask.id}`,
|
|
170
|
+
existing: curTask.status,
|
|
171
|
+
incoming: inTask.status,
|
|
172
|
+
severity: "error",
|
|
173
|
+
autoResolvable: false,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Resource deleted (agent/team/workflow no longer in current) ──
|
|
180
|
+
if (current) {
|
|
181
|
+
const currentTeam = typeof current.team === "string" ? current.team : undefined;
|
|
182
|
+
const currentWorkflow = typeof current.workflow === "string" ? current.workflow : undefined;
|
|
183
|
+
const incomingTeam = typeof incoming.team === "string" ? incoming.team : undefined;
|
|
184
|
+
const incomingWorkflow = typeof incoming.workflow === "string" ? incoming.workflow : undefined;
|
|
185
|
+
|
|
186
|
+
if (incomingTeam && currentTeam && incomingTeam !== currentTeam) {
|
|
187
|
+
conflicts.push({
|
|
188
|
+
kind: "resource_deleted",
|
|
189
|
+
path: `team/${incomingTeam}`,
|
|
190
|
+
existing: currentTeam,
|
|
191
|
+
incoming: incomingTeam,
|
|
192
|
+
severity: "warning",
|
|
193
|
+
autoResolvable: true,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (incomingWorkflow && currentWorkflow && incomingWorkflow !== currentWorkflow) {
|
|
198
|
+
conflicts.push({
|
|
199
|
+
kind: "resource_deleted",
|
|
200
|
+
path: `workflow/${incomingWorkflow}`,
|
|
201
|
+
existing: currentWorkflow,
|
|
202
|
+
incoming: incomingWorkflow,
|
|
203
|
+
severity: "warning",
|
|
204
|
+
autoResolvable: true,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check agent references from tasks vs current tasks
|
|
209
|
+
if (currentTasks && currentTasks.length > 0) {
|
|
210
|
+
const currentAgents = new Set(
|
|
211
|
+
extractTaskLikes(currentTasks)
|
|
212
|
+
.map((t) => t.agent)
|
|
213
|
+
.filter((a): a is string => a !== undefined),
|
|
214
|
+
);
|
|
215
|
+
const incomingTaskList = extractTaskLikes(incomingTasks);
|
|
216
|
+
for (const inTask of incomingTaskList) {
|
|
217
|
+
if (
|
|
218
|
+
inTask.agent &&
|
|
219
|
+
inTask.id !== "" &&
|
|
220
|
+
currentAgents.size > 0 &&
|
|
221
|
+
!currentAgents.has(inTask.agent) &&
|
|
222
|
+
// Only flag if there's a matching task id (agent was reassigned)
|
|
223
|
+
extractTaskLikes(currentTasks).some((ct) => ct.id === inTask.id)
|
|
224
|
+
) {
|
|
225
|
+
conflicts.push({
|
|
226
|
+
kind: "resource_deleted",
|
|
227
|
+
path: `agent/${inTask.agent}`,
|
|
228
|
+
existing: "not in current run",
|
|
229
|
+
incoming: inTask.agent,
|
|
230
|
+
severity: "warning",
|
|
231
|
+
autoResolvable: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return buildReport(conflicts);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Resume Conflict Detection ──────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
export interface SuspendedState {
|
|
244
|
+
tasks: unknown[];
|
|
245
|
+
artifacts: string[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface CurrentState {
|
|
249
|
+
changedFiles: string[];
|
|
250
|
+
taskStatuses: Record<string, string>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Detect conflicts when resuming a suspended run against current filesystem state.
|
|
255
|
+
*
|
|
256
|
+
* Checks:
|
|
257
|
+
* - **file_overwrite**: files changed since suspension.
|
|
258
|
+
* - **state_mismatch**: task statuses changed externally.
|
|
259
|
+
*/
|
|
260
|
+
export function detectResumeConflicts(
|
|
261
|
+
suspendedState: SuspendedState,
|
|
262
|
+
currentState: CurrentState,
|
|
263
|
+
): ConflictReport {
|
|
264
|
+
const conflicts: Conflict[] = [];
|
|
265
|
+
const { tasks: suspendedTasks, artifacts: suspendedArtifacts } = suspendedState;
|
|
266
|
+
const { changedFiles, taskStatuses } = currentState;
|
|
267
|
+
|
|
268
|
+
// ── File overwrite ───────────────────────────────────────────────
|
|
269
|
+
const changedSet = new Set(changedFiles);
|
|
270
|
+
for (const artifactPath of suspendedArtifacts) {
|
|
271
|
+
if (changedSet.has(artifactPath)) {
|
|
272
|
+
conflicts.push({
|
|
273
|
+
kind: "file_overwrite",
|
|
274
|
+
path: artifactPath,
|
|
275
|
+
existing: "modified since suspension",
|
|
276
|
+
incoming: "expected unchanged",
|
|
277
|
+
severity: "error",
|
|
278
|
+
autoResolvable: false,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── State mismatch ───────────────────────────────────────────────
|
|
284
|
+
const suspendedTaskList = extractTaskLikes(suspendedTasks);
|
|
285
|
+
for (const task of suspendedTaskList) {
|
|
286
|
+
const currentStatus = taskStatuses[task.id];
|
|
287
|
+
if (currentStatus !== undefined && currentStatus !== task.status) {
|
|
288
|
+
conflicts.push({
|
|
289
|
+
kind: "state_mismatch",
|
|
290
|
+
path: `tasks/${task.id}`,
|
|
291
|
+
existing: currentStatus,
|
|
292
|
+
incoming: task.status,
|
|
293
|
+
severity: "error",
|
|
294
|
+
autoResolvable: false,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return buildReport(conflicts);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Conflict Resolution ────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Apply a resolution strategy to a conflict.
|
|
306
|
+
*
|
|
307
|
+
* - **skip**: skip the conflicting item (always resolves).
|
|
308
|
+
* - **overwrite**: replace existing with incoming (resolves for `file_overwrite`
|
|
309
|
+
* and `schema_drift`; does not resolve `state_mismatch` or `resource_deleted`).
|
|
310
|
+
* - **merge**: attempt merge — resolves `state_mismatch` with merged status;
|
|
311
|
+
* resolves others conditionally.
|
|
312
|
+
*/
|
|
313
|
+
export function resolveConflict(
|
|
314
|
+
conflict: Conflict,
|
|
315
|
+
strategy: ConflictStrategy,
|
|
316
|
+
): ConflictResolution {
|
|
317
|
+
switch (strategy) {
|
|
318
|
+
case "skip":
|
|
319
|
+
return { resolved: true, action: `Skipped ${conflict.path}` };
|
|
320
|
+
|
|
321
|
+
case "overwrite":
|
|
322
|
+
if (conflict.kind === "file_overwrite" || conflict.kind === "schema_drift") {
|
|
323
|
+
return { resolved: true, action: `Overwritten ${conflict.path} with incoming value` };
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
resolved: false,
|
|
327
|
+
action: `Cannot overwrite ${conflict.kind} at ${conflict.path}; manual resolution required`,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
case "merge":
|
|
331
|
+
if (conflict.kind === "state_mismatch") {
|
|
332
|
+
return {
|
|
333
|
+
resolved: true,
|
|
334
|
+
action: `Merged ${conflict.path}: kept existing=${conflict.existing ?? "?"}, incoming=${conflict.incoming ?? "?"}`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (conflict.kind === "resource_deleted") {
|
|
338
|
+
return {
|
|
339
|
+
resolved: true,
|
|
340
|
+
action: `Merged ${conflict.path}: using incoming resource reference`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (conflict.kind === "file_overwrite") {
|
|
344
|
+
return {
|
|
345
|
+
resolved: true,
|
|
346
|
+
action: `Merged ${conflict.path}: kept both versions`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (conflict.kind === "schema_drift") {
|
|
350
|
+
return {
|
|
351
|
+
resolved: true,
|
|
352
|
+
action: `Merged ${conflict.path}: using incoming schema version`,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
resolved: false,
|
|
357
|
+
action: `Cannot merge ${conflict.kind} at ${conflict.path}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|