pi-harness-runtime 1.1.350 → 1.1.351
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/delegation-store.d.ts +35 -0
- package/packages/contracts/dist/delegation-store.js +172 -0
- package/packages/contracts/dist/index.d.ts +2 -0
- package/packages/contracts/dist/index.js +2 -0
- package/packages/contracts/dist/work-partitioner.d.ts +57 -0
- package/packages/contracts/dist/work-partitioner.js +165 -0
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/src/delegation-store.ts +204 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/work-partitioner.ts +251 -0
- package/packages/contracts/test/delegation.test.ts +350 -0
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation Store
|
|
3
|
+
*
|
|
4
|
+
* Durable storage for delegation receipts.
|
|
5
|
+
* Stores in ignored harness state directory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import type { DelegationReceipt } from "./delegation.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Store directory path
|
|
14
|
+
*/
|
|
15
|
+
const STORE_DIR = join(process.env.HOME || "/home/frappe", ".pi-harness-runtime", "delegation-state");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure store directory exists
|
|
19
|
+
*/
|
|
20
|
+
function ensureStoreDir(): void {
|
|
21
|
+
if (!existsSync(STORE_DIR)) {
|
|
22
|
+
mkdirSync(STORE_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get receipt file path for workspace + task
|
|
28
|
+
*/
|
|
29
|
+
function getReceiptPath(workspacePath: string, taskId: string): string {
|
|
30
|
+
// Sanitize path for use as filename
|
|
31
|
+
const safePath = workspacePath.replace(/[^a-zA-Z0-9_-]/g, "_").slice(-100);
|
|
32
|
+
const safeTask = taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
33
|
+
return join(STORE_DIR, `${safePath}__${safeTask}.json`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Save delegation receipt
|
|
38
|
+
*/
|
|
39
|
+
export function saveDelegationReceipt(receipt: DelegationReceipt): void {
|
|
40
|
+
ensureStoreDir();
|
|
41
|
+
const path = getReceiptPath(receipt.workspacePath, receipt.parentTaskId);
|
|
42
|
+
|
|
43
|
+
// Atomic write: write to temp, then rename
|
|
44
|
+
const tempPath = path + ".tmp";
|
|
45
|
+
writeFileSync(tempPath, JSON.stringify(receipt, null, 2), "utf8");
|
|
46
|
+
|
|
47
|
+
// Rename is atomic on POSIX
|
|
48
|
+
try {
|
|
49
|
+
const { renameSync } = require("fs");
|
|
50
|
+
renameSync(tempPath, path);
|
|
51
|
+
} catch {
|
|
52
|
+
// Fallback: just move the file
|
|
53
|
+
try {
|
|
54
|
+
writeFileSync(path, JSON.stringify(receipt, null, 2), "utf8");
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore write errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load delegation receipt
|
|
63
|
+
*/
|
|
64
|
+
export function loadDelegationReceipt(
|
|
65
|
+
workspacePath: string,
|
|
66
|
+
taskId: string
|
|
67
|
+
): DelegationReceipt | null {
|
|
68
|
+
const path = getReceiptPath(workspacePath, taskId);
|
|
69
|
+
|
|
70
|
+
if (!existsSync(path)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const content = readFileSync(path, "utf8");
|
|
76
|
+
const receipt = JSON.parse(content) as DelegationReceipt;
|
|
77
|
+
|
|
78
|
+
// Validate version
|
|
79
|
+
if (receipt.version !== 1) {
|
|
80
|
+
// Future migration logic would go here
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return receipt;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Delete delegation receipt
|
|
92
|
+
*/
|
|
93
|
+
export function deleteDelegationReceipt(
|
|
94
|
+
workspacePath: string,
|
|
95
|
+
taskId: string
|
|
96
|
+
): void {
|
|
97
|
+
const path = getReceiptPath(workspacePath, taskId);
|
|
98
|
+
|
|
99
|
+
if (existsSync(path)) {
|
|
100
|
+
try {
|
|
101
|
+
const { unlinkSync } = require("fs");
|
|
102
|
+
unlinkSync(path);
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore delete errors
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* List all receipts for a workspace
|
|
111
|
+
*/
|
|
112
|
+
export function listDelegationReceipts(workspacePath: string): DelegationReceipt[] {
|
|
113
|
+
ensureStoreDir();
|
|
114
|
+
const receipts: DelegationReceipt[] = [];
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const { readdirSync } = require("fs");
|
|
118
|
+
const files = readdirSync(STORE_DIR);
|
|
119
|
+
|
|
120
|
+
const safePathPrefix = workspacePath.replace(/[^a-zA-Z0-9_-]/g, "_").slice(-100) + "__";
|
|
121
|
+
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
if (!file.startsWith(safePathPrefix) || !file.endsWith(".json")) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const content = readFileSync(join(STORE_DIR, file), "utf8");
|
|
129
|
+
const receipt = JSON.parse(content) as DelegationReceipt;
|
|
130
|
+
if (receipt.workspacePath === workspacePath) {
|
|
131
|
+
receipts.push(receipt);
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// Skip invalid files
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Directory doesn't exist or not readable
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return receipts;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Update receipt status
|
|
146
|
+
*/
|
|
147
|
+
export function updateReceiptStatus(
|
|
148
|
+
workspacePath: string,
|
|
149
|
+
taskId: string,
|
|
150
|
+
status: DelegationReceipt["status"],
|
|
151
|
+
updates?: Partial<DelegationReceipt>
|
|
152
|
+
): DelegationReceipt | null {
|
|
153
|
+
const receipt = loadDelegationReceipt(workspacePath, taskId);
|
|
154
|
+
|
|
155
|
+
if (!receipt) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const updated: DelegationReceipt = {
|
|
160
|
+
...receipt,
|
|
161
|
+
...updates,
|
|
162
|
+
status,
|
|
163
|
+
updatedAt: Date.now(),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
saveDelegationReceipt(updated);
|
|
167
|
+
return updated;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check if receipt exists for task
|
|
172
|
+
*/
|
|
173
|
+
export function hasReceipt(workspacePath: string, taskId: string): boolean {
|
|
174
|
+
const path = getReceiptPath(workspacePath, taskId);
|
|
175
|
+
return existsSync(path);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Compact receipt for storage after compaction
|
|
180
|
+
*/
|
|
181
|
+
export function compactReceipt(receipt: DelegationReceipt): DelegationReceipt {
|
|
182
|
+
return {
|
|
183
|
+
version: receipt.version,
|
|
184
|
+
workspacePath: receipt.workspacePath,
|
|
185
|
+
sessionId: receipt.sessionId,
|
|
186
|
+
parentTaskId: receipt.parentTaskId,
|
|
187
|
+
scopeFingerprint: receipt.scopeFingerprint,
|
|
188
|
+
decision: {
|
|
189
|
+
strategy: receipt.decision.strategy,
|
|
190
|
+
reason: receipt.decision.reason,
|
|
191
|
+
workGroups: receipt.decision.workGroups.filter((g) => g.status === "pending" || g.status === "dispatched" || g.status === "running"),
|
|
192
|
+
validationExpectations: receipt.decision.validationExpectations,
|
|
193
|
+
confidence: receipt.decision.confidence,
|
|
194
|
+
jevUsed: receipt.decision.jevUsed,
|
|
195
|
+
reassessmentTriggers: receipt.decision.reassessmentTriggers,
|
|
196
|
+
},
|
|
197
|
+
createdAt: receipt.createdAt,
|
|
198
|
+
updatedAt: Date.now(),
|
|
199
|
+
status: receipt.status,
|
|
200
|
+
childIds: receipt.childIds,
|
|
201
|
+
pendingValidation: receipt.pendingValidation,
|
|
202
|
+
userConstraints: receipt.userConstraints,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -21,4 +21,6 @@ export * from './notifications.js';
|
|
|
21
21
|
export * from './approvals.js';
|
|
22
22
|
export * from './delegation.js';
|
|
23
23
|
export * from './delegation-assessor.js';
|
|
24
|
+
export * from './delegation-store.js';
|
|
24
25
|
export * from './scope-discovery.js';
|
|
26
|
+
export * from './work-partitioner.js';
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Partitioner
|
|
3
|
+
*
|
|
4
|
+
* Partitions work into disjoint file ownership groups.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WorkGroup, ScopeFacts } from "./delegation.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* File dependency info
|
|
11
|
+
*/
|
|
12
|
+
export interface FileDependency {
|
|
13
|
+
path: string;
|
|
14
|
+
imports: string[];
|
|
15
|
+
exports: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Partition configuration
|
|
20
|
+
*/
|
|
21
|
+
export interface PartitionConfig {
|
|
22
|
+
/** Maximum files per group */
|
|
23
|
+
maxFilesPerGroup: number;
|
|
24
|
+
/** Whether to group by directory */
|
|
25
|
+
groupByDirectory: boolean;
|
|
26
|
+
/** Shared files that shouldn't be partitioned */
|
|
27
|
+
sharedPaths: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default partition config
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_PARTITION_CONFIG: PartitionConfig = {
|
|
34
|
+
maxFilesPerGroup: 15,
|
|
35
|
+
groupByDirectory: true,
|
|
36
|
+
sharedPaths: [
|
|
37
|
+
"index.ts",
|
|
38
|
+
"index.js",
|
|
39
|
+
"main.ts",
|
|
40
|
+
"main.js",
|
|
41
|
+
"package.json",
|
|
42
|
+
"tsconfig.json",
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Partition files into ownership groups
|
|
48
|
+
*/
|
|
49
|
+
export function partitionWork(
|
|
50
|
+
files: string[],
|
|
51
|
+
facts: ScopeFacts,
|
|
52
|
+
config: Partial<PartitionConfig> = {}
|
|
53
|
+
): WorkGroup[] {
|
|
54
|
+
const cfg = { ...DEFAULT_PARTITION_CONFIG, ...config };
|
|
55
|
+
|
|
56
|
+
// Filter out shared paths
|
|
57
|
+
const ownedFiles = files.filter(
|
|
58
|
+
(f) => !cfg.sharedPaths.some((s) => f.endsWith(s))
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Group files by directory
|
|
62
|
+
const dirGroups = groupByDirectory(ownedFiles);
|
|
63
|
+
|
|
64
|
+
// Further partition large groups
|
|
65
|
+
const groups: WorkGroup[] = [];
|
|
66
|
+
let groupId = 1;
|
|
67
|
+
|
|
68
|
+
for (const [dir, dirFiles] of Object.entries(dirGroups)) {
|
|
69
|
+
if (dirFiles.length <= cfg.maxFilesPerGroup) {
|
|
70
|
+
groups.push(createGroup(groupId++, dir, dirFiles, facts.isMechanical));
|
|
71
|
+
} else {
|
|
72
|
+
// Split large directories
|
|
73
|
+
const subGroups = splitGroup(dirFiles, cfg.maxFilesPerGroup);
|
|
74
|
+
for (const subFiles of subGroups) {
|
|
75
|
+
groups.push(createGroup(groupId++, dir, subFiles, facts.isMechanical));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return groups;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Group files by directory
|
|
85
|
+
*/
|
|
86
|
+
function groupByDirectory(files: string[]): Record<string, string[]> {
|
|
87
|
+
const groups: Record<string, string[]> = {};
|
|
88
|
+
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
// Get directory part of path
|
|
91
|
+
const parts = file.split("/");
|
|
92
|
+
const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
|
|
93
|
+
|
|
94
|
+
if (!groups[dir]) {
|
|
95
|
+
groups[dir] = [];
|
|
96
|
+
}
|
|
97
|
+
groups[dir].push(file);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return groups;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a work group
|
|
105
|
+
*/
|
|
106
|
+
function createGroup(
|
|
107
|
+
id: number,
|
|
108
|
+
description: string,
|
|
109
|
+
files: string[],
|
|
110
|
+
isMechanical: boolean
|
|
111
|
+
): WorkGroup {
|
|
112
|
+
return {
|
|
113
|
+
groupId: `group-${id}`,
|
|
114
|
+
description: `${description} (${files.length} files)`,
|
|
115
|
+
ownedPaths: files,
|
|
116
|
+
isMechanical,
|
|
117
|
+
status: "pending",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Split a group into smaller subgroups
|
|
123
|
+
*/
|
|
124
|
+
function splitGroup(files: string[], maxSize: number): string[][] {
|
|
125
|
+
const subgroups: string[][] = [];
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < files.length; i += maxSize) {
|
|
128
|
+
subgroups.push(files.slice(i, i + maxSize));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return subgroups;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Detect ownership conflicts
|
|
136
|
+
*/
|
|
137
|
+
export function detectOwnershipConflicts(
|
|
138
|
+
groups: WorkGroup[],
|
|
139
|
+
newPaths: string[]
|
|
140
|
+
): string[] {
|
|
141
|
+
const conflicts: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (const path of newPaths) {
|
|
144
|
+
for (const group of groups) {
|
|
145
|
+
if (
|
|
146
|
+
group.status !== "cancelled" &&
|
|
147
|
+
group.status !== "failed" &&
|
|
148
|
+
group.ownedPaths.includes(path)
|
|
149
|
+
) {
|
|
150
|
+
conflicts.push(`${path} already owned by ${group.groupId}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return conflicts;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Assign work to group
|
|
160
|
+
*/
|
|
161
|
+
export function assignWorkToGroup(
|
|
162
|
+
groups: WorkGroup[],
|
|
163
|
+
groupId: string,
|
|
164
|
+
paths: string[]
|
|
165
|
+
): WorkGroup | null {
|
|
166
|
+
const group = groups.find((g) => g.groupId === groupId);
|
|
167
|
+
|
|
168
|
+
if (!group) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check for conflicts
|
|
173
|
+
const conflicts = detectOwnershipConflicts(
|
|
174
|
+
groups.filter((g) => g.groupId !== groupId),
|
|
175
|
+
paths
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (conflicts.length > 0) {
|
|
179
|
+
// Return group with conflict info
|
|
180
|
+
return {
|
|
181
|
+
...group,
|
|
182
|
+
status: "failed",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
...group,
|
|
188
|
+
ownedPaths: [...new Set([...group.ownedPaths, ...paths])],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get unowned files
|
|
194
|
+
*/
|
|
195
|
+
export function getUnownedFiles(
|
|
196
|
+
allFiles: string[],
|
|
197
|
+
groups: WorkGroup[]
|
|
198
|
+
): string[] {
|
|
199
|
+
const ownedFiles = new Set<string>();
|
|
200
|
+
|
|
201
|
+
for (const group of groups) {
|
|
202
|
+
if (group.status !== "cancelled" && group.status !== "failed") {
|
|
203
|
+
for (const path of group.ownedPaths) {
|
|
204
|
+
ownedFiles.add(path);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return allFiles.filter((f) => !ownedFiles.has(f));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Update group status
|
|
214
|
+
*/
|
|
215
|
+
export function updateGroupStatus(
|
|
216
|
+
groups: WorkGroup[],
|
|
217
|
+
groupId: string,
|
|
218
|
+
status: WorkGroup["status"],
|
|
219
|
+
childId?: string
|
|
220
|
+
): WorkGroup[] {
|
|
221
|
+
return groups.map((g) => {
|
|
222
|
+
if (g.groupId !== groupId) {
|
|
223
|
+
return g;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...g,
|
|
228
|
+
status,
|
|
229
|
+
childId: childId || g.childId,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if all groups are complete
|
|
236
|
+
*/
|
|
237
|
+
export function areAllGroupsComplete(groups: WorkGroup[]): boolean {
|
|
238
|
+
return groups.every(
|
|
239
|
+
(g) =>
|
|
240
|
+
g.status === "completed" ||
|
|
241
|
+
g.status === "failed" ||
|
|
242
|
+
g.status === "cancelled"
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get pending groups
|
|
248
|
+
*/
|
|
249
|
+
export function getPendingGroups(groups: WorkGroup[]): WorkGroup[] {
|
|
250
|
+
return groups.filter((g) => g.status === "pending");
|
|
251
|
+
}
|