pi-harness-runtime 1.1.349 → 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.
Files changed (128) hide show
  1. package/README.md +3 -0
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/dist/delegation-assessor.d.ts +27 -0
  25. package/packages/contracts/dist/delegation-assessor.js +210 -0
  26. package/packages/contracts/dist/delegation-store.d.ts +35 -0
  27. package/packages/contracts/dist/delegation-store.js +172 -0
  28. package/packages/contracts/dist/delegation.d.ts +142 -0
  29. package/packages/contracts/dist/delegation.js +33 -0
  30. package/packages/contracts/dist/index.d.ts +5 -0
  31. package/packages/contracts/dist/index.js +5 -0
  32. package/packages/contracts/dist/scope-discovery.d.ts +65 -0
  33. package/packages/contracts/dist/scope-discovery.js +247 -0
  34. package/packages/contracts/dist/work-partitioner.d.ts +57 -0
  35. package/packages/contracts/dist/work-partitioner.js +165 -0
  36. package/packages/contracts/package.json +1 -1
  37. package/packages/contracts/src/delegation-assessor.ts +268 -0
  38. package/packages/contracts/src/delegation-store.ts +204 -0
  39. package/packages/contracts/src/delegation.ts +174 -0
  40. package/packages/contracts/src/index.ts +5 -0
  41. package/packages/contracts/src/scope-discovery.ts +352 -0
  42. package/packages/contracts/src/work-partitioner.ts +251 -0
  43. package/packages/contracts/test/delegation.test.ts +350 -0
  44. package/packages/cookie-sanitizer/package.json +1 -1
  45. package/packages/cost-optimizer/package.json +1 -1
  46. package/packages/dependency-analyzer/package.json +1 -1
  47. package/packages/django-plugin/package.json +1 -1
  48. package/packages/doc-generator/package.json +1 -1
  49. package/packages/durable-review/package.json +1 -1
  50. package/packages/e2e-tester/package.json +1 -1
  51. package/packages/evaluation-engine/package.json +1 -1
  52. package/packages/evaluation-runner/package.json +1 -1
  53. package/packages/event-bus/package.json +1 -1
  54. package/packages/event-sink/package.json +1 -1
  55. package/packages/event-store/package.json +1 -1
  56. package/packages/experience-replay/package.json +1 -1
  57. package/packages/feedback-collector/package.json +1 -1
  58. package/packages/file-copy-helper/package.json +1 -1
  59. package/packages/framework-detector/package.json +1 -1
  60. package/packages/framework-plugin-sdk/package.json +1 -1
  61. package/packages/frappe-plugin/package.json +1 -1
  62. package/packages/generic-web-plugin/package.json +1 -1
  63. package/packages/health-monitor/package.json +1 -1
  64. package/packages/intent-analyzer/package.json +1 -1
  65. package/packages/jev-judge/package.json +1 -1
  66. package/packages/knowledge-graph/package.json +1 -1
  67. package/packages/knowledge-retrieval/package.json +1 -1
  68. package/packages/laravel-plugin/package.json +1 -1
  69. package/packages/learning-engine/package.json +1 -1
  70. package/packages/mcp-adapter/package.json +1 -1
  71. package/packages/memory-engine/package.json +1 -1
  72. package/packages/milestone-manager/package.json +1 -1
  73. package/packages/model-registry/package.json +1 -1
  74. package/packages/nextjs-plugin/package.json +1 -1
  75. package/packages/notification/package.json +1 -1
  76. package/packages/observability/package.json +1 -1
  77. package/packages/okf-indexer/package.json +1 -1
  78. package/packages/paperclip-integration/package.json +1 -1
  79. package/packages/performance-optimizer/package.json +1 -1
  80. package/packages/permission-mode/package.json +1 -1
  81. package/packages/privilege-broker/package.json +1 -1
  82. package/packages/project-analyzer/package.json +1 -1
  83. package/packages/project-bootstrap/package.json +1 -1
  84. package/packages/projection-engine/package.json +1 -1
  85. package/packages/prompt-compiler/package.json +1 -1
  86. package/packages/prompt-versioning/package.json +1 -1
  87. package/packages/provider-adapter-sdk/package.json +1 -1
  88. package/packages/provider-router/package.json +1 -1
  89. package/packages/provider-selector/package.json +1 -1
  90. package/packages/providers/package.json +1 -1
  91. package/packages/qdrant-skills/package.json +1 -1
  92. package/packages/quota-manager/package.json +1 -1
  93. package/packages/rate-limiter/package.json +1 -1
  94. package/packages/react-vite-plugin/package.json +1 -1
  95. package/packages/relay/package.json +1 -1
  96. package/packages/release-manager/package.json +1 -1
  97. package/packages/requirement-compiler/package.json +1 -1
  98. package/packages/runtime/package.json +1 -1
  99. package/packages/scheduler/package.json +1 -1
  100. package/packages/scheduler-adapter/package.json +1 -1
  101. package/packages/session/package.json +1 -1
  102. package/packages/session-api/package.json +1 -1
  103. package/packages/session-export/package.json +1 -1
  104. package/packages/shared-context/package.json +1 -1
  105. package/packages/skill-mcp-client/package.json +1 -1
  106. package/packages/skill-registry/package.json +1 -1
  107. package/packages/skills/package.json +1 -1
  108. package/packages/sprint-planner/package.json +1 -1
  109. package/packages/subscription-engine/package.json +1 -1
  110. package/packages/task-compiler/package.json +1 -1
  111. package/packages/telegram/package.json +1 -1
  112. package/packages/tencentdb-memory/package.json +1 -1
  113. package/packages/tencentdb-sync/package.json +1 -1
  114. package/packages/test-data-generator/package.json +1 -1
  115. package/packages/test-generator/package.json +1 -1
  116. package/packages/todo-bd-sync/package.json +1 -1
  117. package/packages/token-estimation/package.json +1 -1
  118. package/packages/token-optimizer/package.json +1 -1
  119. package/packages/trajectory/package.json +1 -1
  120. package/packages/tui/package.json +1 -1
  121. package/packages/types/package.json +1 -1
  122. package/packages/voice-api/package.json +1 -1
  123. package/packages/webmcp-tester/package.json +1 -1
  124. package/packages/wiki/package.json +1 -1
  125. package/packages/workflow-events/package.json +1 -1
  126. package/packages/workspace-scanner/package.json +1 -1
  127. package/packages/worktree/package.json +1 -1
  128. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Work Partitioner
3
+ *
4
+ * Partitions work into disjoint file ownership groups.
5
+ */
6
+ /**
7
+ * Default partition config
8
+ */
9
+ export const DEFAULT_PARTITION_CONFIG = {
10
+ maxFilesPerGroup: 15,
11
+ groupByDirectory: true,
12
+ sharedPaths: [
13
+ "index.ts",
14
+ "index.js",
15
+ "main.ts",
16
+ "main.js",
17
+ "package.json",
18
+ "tsconfig.json",
19
+ ],
20
+ };
21
+ /**
22
+ * Partition files into ownership groups
23
+ */
24
+ export function partitionWork(files, facts, config = {}) {
25
+ const cfg = { ...DEFAULT_PARTITION_CONFIG, ...config };
26
+ // Filter out shared paths
27
+ const ownedFiles = files.filter((f) => !cfg.sharedPaths.some((s) => f.endsWith(s)));
28
+ // Group files by directory
29
+ const dirGroups = groupByDirectory(ownedFiles);
30
+ // Further partition large groups
31
+ const groups = [];
32
+ let groupId = 1;
33
+ for (const [dir, dirFiles] of Object.entries(dirGroups)) {
34
+ if (dirFiles.length <= cfg.maxFilesPerGroup) {
35
+ groups.push(createGroup(groupId++, dir, dirFiles, facts.isMechanical));
36
+ }
37
+ else {
38
+ // Split large directories
39
+ const subGroups = splitGroup(dirFiles, cfg.maxFilesPerGroup);
40
+ for (const subFiles of subGroups) {
41
+ groups.push(createGroup(groupId++, dir, subFiles, facts.isMechanical));
42
+ }
43
+ }
44
+ }
45
+ return groups;
46
+ }
47
+ /**
48
+ * Group files by directory
49
+ */
50
+ function groupByDirectory(files) {
51
+ const groups = {};
52
+ for (const file of files) {
53
+ // Get directory part of path
54
+ const parts = file.split("/");
55
+ const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
56
+ if (!groups[dir]) {
57
+ groups[dir] = [];
58
+ }
59
+ groups[dir].push(file);
60
+ }
61
+ return groups;
62
+ }
63
+ /**
64
+ * Create a work group
65
+ */
66
+ function createGroup(id, description, files, isMechanical) {
67
+ return {
68
+ groupId: `group-${id}`,
69
+ description: `${description} (${files.length} files)`,
70
+ ownedPaths: files,
71
+ isMechanical,
72
+ status: "pending",
73
+ };
74
+ }
75
+ /**
76
+ * Split a group into smaller subgroups
77
+ */
78
+ function splitGroup(files, maxSize) {
79
+ const subgroups = [];
80
+ for (let i = 0; i < files.length; i += maxSize) {
81
+ subgroups.push(files.slice(i, i + maxSize));
82
+ }
83
+ return subgroups;
84
+ }
85
+ /**
86
+ * Detect ownership conflicts
87
+ */
88
+ export function detectOwnershipConflicts(groups, newPaths) {
89
+ const conflicts = [];
90
+ for (const path of newPaths) {
91
+ for (const group of groups) {
92
+ if (group.status !== "cancelled" &&
93
+ group.status !== "failed" &&
94
+ group.ownedPaths.includes(path)) {
95
+ conflicts.push(`${path} already owned by ${group.groupId}`);
96
+ }
97
+ }
98
+ }
99
+ return conflicts;
100
+ }
101
+ /**
102
+ * Assign work to group
103
+ */
104
+ export function assignWorkToGroup(groups, groupId, paths) {
105
+ const group = groups.find((g) => g.groupId === groupId);
106
+ if (!group) {
107
+ return null;
108
+ }
109
+ // Check for conflicts
110
+ const conflicts = detectOwnershipConflicts(groups.filter((g) => g.groupId !== groupId), paths);
111
+ if (conflicts.length > 0) {
112
+ // Return group with conflict info
113
+ return {
114
+ ...group,
115
+ status: "failed",
116
+ };
117
+ }
118
+ return {
119
+ ...group,
120
+ ownedPaths: [...new Set([...group.ownedPaths, ...paths])],
121
+ };
122
+ }
123
+ /**
124
+ * Get unowned files
125
+ */
126
+ export function getUnownedFiles(allFiles, groups) {
127
+ const ownedFiles = new Set();
128
+ for (const group of groups) {
129
+ if (group.status !== "cancelled" && group.status !== "failed") {
130
+ for (const path of group.ownedPaths) {
131
+ ownedFiles.add(path);
132
+ }
133
+ }
134
+ }
135
+ return allFiles.filter((f) => !ownedFiles.has(f));
136
+ }
137
+ /**
138
+ * Update group status
139
+ */
140
+ export function updateGroupStatus(groups, groupId, status, childId) {
141
+ return groups.map((g) => {
142
+ if (g.groupId !== groupId) {
143
+ return g;
144
+ }
145
+ return {
146
+ ...g,
147
+ status,
148
+ childId: childId || g.childId,
149
+ };
150
+ });
151
+ }
152
+ /**
153
+ * Check if all groups are complete
154
+ */
155
+ export function areAllGroupsComplete(groups) {
156
+ return groups.every((g) => g.status === "completed" ||
157
+ g.status === "failed" ||
158
+ g.status === "cancelled");
159
+ }
160
+ /**
161
+ * Get pending groups
162
+ */
163
+ export function getPendingGroups(groups) {
164
+ return groups.filter((g) => g.status === "pending");
165
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/contracts",
3
- "version": "1.1.349",
3
+ "version": "1.1.351",
4
4
  "type": "module",
5
5
  "description": "RPC schema definitions for pi-harness runtime",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Delegation Assessor
3
+ *
4
+ * Assesses whether work should be delegated to sub-agents based on scope.
5
+ */
6
+
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import type {
9
+ ScopeFacts,
10
+ DelegationDecision,
11
+ DelegationStrategy,
12
+ WorkGroup,
13
+ DelegationTriggerConfig,
14
+ ExtensionCapability,
15
+ DEFAULT_DELEGATION_TRIGGERS,
16
+ } from "./delegation.js";
17
+ import { shouldTriggerAssessment } from "./delegation.js";
18
+
19
+ /**
20
+ * Sub-agent extension names to check for
21
+ */
22
+ const SUBAGENT_EXTENSIONS = [
23
+ "pi-subagents",
24
+ "@earendil-works/pi-subagents",
25
+ "subagent",
26
+ "spawn",
27
+ "delegate",
28
+ ];
29
+
30
+ /**
31
+ * Tool names that indicate sub-agent capability
32
+ */
33
+ const SUBAGENT_TOOLS = [
34
+ "subagent",
35
+ "spawn",
36
+ "delegate",
37
+ "run_subagent",
38
+ "run_agent",
39
+ ];
40
+
41
+ /**
42
+ * Discover extension capabilities for delegation
43
+ */
44
+ export async function discoverDelegationCapability(
45
+ pi: ExtensionAPI
46
+ ): Promise<ExtensionCapability | null> {
47
+ // Get all available tools
48
+ const allTools = pi.getAllTools();
49
+ const toolNames = allTools.map((t) => t.name);
50
+ const activeTools = pi.getActiveTools();
51
+
52
+ // Check for sub-agent tools
53
+ const hasSubagentTool = SUBAGENT_TOOLS.some(
54
+ (tool) => toolNames.includes(tool) || activeTools.includes(tool)
55
+ );
56
+
57
+ // Check for subagent extension registration
58
+ // The extension might register tools under different names
59
+ const possibleCapabilities: ExtensionCapability[] = [];
60
+
61
+ // Verify by attempting a capability check
62
+ // If subagent tools exist, assume capability
63
+ if (hasSubagentTool) {
64
+ return {
65
+ name: "subagent-extension",
66
+ supportsSubagents: true,
67
+ supportsWorkIsolation: true, // Assume based on tool presence
68
+ supportsCancellation: true,
69
+ availableTools: activeTools.filter((t) =>
70
+ SUBAGENT_TOOLS.some((st) => t.includes(st))
71
+ ),
72
+ verified: true,
73
+ };
74
+ }
75
+
76
+ // Check if pi-subagents skill is mentioned in available tools
77
+ const subagentMentioned = toolNames.some(
78
+ (t) => t.includes("subagent") || t.includes("spawn")
79
+ );
80
+
81
+ if (subagentMentioned) {
82
+ return {
83
+ name: "pi-subagents",
84
+ supportsSubagents: true,
85
+ supportsWorkIsolation: true,
86
+ supportsCancellation: true,
87
+ availableTools: toolNames.filter((t) => t.includes("subagent")),
88
+ verified: false, // Not verified - just mentioned
89
+ };
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Estimate scope from file analysis
97
+ */
98
+ export async function estimateScope(
99
+ _pi: ExtensionAPI,
100
+ patterns: string[]
101
+ ): Promise<ScopeFacts> {
102
+ // TODO: Implement actual file discovery
103
+ // For now, return placeholder
104
+ return {
105
+ estimatedFiles: patterns.length,
106
+ observedFiles: 0,
107
+ estimatedCallSites: 0,
108
+ observedCallSites: 0,
109
+ independentComponents: 1,
110
+ tightlyCoupled: false,
111
+ isMechanical: true,
112
+ isComplex: false,
113
+ fileTypes: {},
114
+ fingerprint: "",
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Make deterministic assessment without Jev
120
+ */
121
+ export function makeDeterministicAssessment(
122
+ facts: ScopeFacts,
123
+ capability: ExtensionCapability | null
124
+ ): DelegationDecision {
125
+ // No capability - must use single agent
126
+ if (!capability?.verified) {
127
+ return {
128
+ strategy: "unavailable",
129
+ reason: "No verified sub-agent capability available",
130
+ workGroups: [],
131
+ validationExpectations: "Parent handles all validation",
132
+ confidence: "high",
133
+ jevUsed: false,
134
+ reassessmentTriggers: ["Extension becomes available"],
135
+ };
136
+ }
137
+
138
+ // Small scope - single agent
139
+ if (facts.observedFiles < 5 && facts.observedCallSites < 20) {
140
+ return {
141
+ strategy: "single_agent",
142
+ reason: "Small scope - delegation overhead not justified",
143
+ workGroups: [],
144
+ validationExpectations: "Standard parent validation",
145
+ confidence: "high",
146
+ jevUsed: false,
147
+ reassessmentTriggers: [],
148
+ };
149
+ }
150
+
151
+ // Mechanical transformation with reliable script
152
+ if (facts.isMechanical && !facts.isComplex) {
153
+ return {
154
+ strategy: "script_only",
155
+ reason: "Mechanical transformation - script approach is reliable",
156
+ workGroups: [],
157
+ validationExpectations: "Script output validation + test run",
158
+ confidence: "high",
159
+ jevUsed: false,
160
+ reassessmentTriggers: ["Transformation proves unreliable"],
161
+ };
162
+ }
163
+
164
+ // Multiple independent components
165
+ if (facts.independentComponents >= 2 && !facts.tightlyCoupled) {
166
+ const groups = partitionIntoGroups(facts);
167
+ return {
168
+ strategy: "delegate",
169
+ reason: `${facts.independentComponents} independent components - delegation recommended`,
170
+ workGroups: groups,
171
+ validationExpectations: "Each group validated independently, then integrated",
172
+ confidence: "medium",
173
+ jevUsed: false,
174
+ reassessmentTriggers: ["Scope expands significantly", "Component coupling discovered"],
175
+ };
176
+ }
177
+
178
+ // Tightly coupled - sequential or single agent
179
+ if (facts.tightlyCoupled) {
180
+ return {
181
+ strategy: "script_then_delegate",
182
+ reason: "Coupled components - script shared parts, delegate isolated changes",
183
+ workGroups: [],
184
+ validationExpectations: "Script validates shared API, delegate validates components",
185
+ confidence: "medium",
186
+ jevUsed: false,
187
+ reassessmentTriggers: ["Coupling reduced after refactoring"],
188
+ };
189
+ }
190
+
191
+ // Default - single agent with monitoring
192
+ return {
193
+ strategy: "single_agent",
194
+ reason: "Default to single agent for bounded scope",
195
+ workGroups: [],
196
+ validationExpectations: "Parent validates all changes",
197
+ confidence: "low",
198
+ jevUsed: false,
199
+ reassessmentTriggers: ["Scope exceeds threshold"],
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Partition work into groups
205
+ */
206
+ function partitionIntoGroups(facts: ScopeFacts): WorkGroup[] {
207
+ const groups: WorkGroup[] = [];
208
+ const filesPerGroup = Math.ceil(facts.observedFiles / facts.independentComponents);
209
+
210
+ for (let i = 0; i < facts.independentComponents; i++) {
211
+ groups.push({
212
+ groupId: `group-${i + 1}`,
213
+ description: `Component ${i + 1} of ${facts.independentComponents}`,
214
+ ownedPaths: [], // TODO: Populate with actual files
215
+ isMechanical: facts.isMechanical,
216
+ status: "pending",
217
+ });
218
+ }
219
+
220
+ return groups;
221
+ }
222
+
223
+ /**
224
+ * Assess delegation need
225
+ */
226
+ export async function assessDelegation(
227
+ pi: ExtensionAPI,
228
+ facts: ScopeFacts,
229
+ config: DelegationTriggerConfig = { minFiles: 10, minCallSites: 50, minIndependentComponents: 2, assessOnDiscovery: true }
230
+ ): Promise<DelegationDecision | null> {
231
+ // Check if assessment should trigger
232
+ if (!shouldTriggerAssessment(facts, config)) {
233
+ return null; // No assessment needed
234
+ }
235
+
236
+ // Discover capability
237
+ const capability = await discoverDelegationCapability(pi);
238
+
239
+ // Make deterministic assessment
240
+ return makeDeterministicAssessment(facts, capability);
241
+ }
242
+
243
+ /**
244
+ * Validate assessment is still valid
245
+ */
246
+ export function isAssessmentStale(
247
+ decision: DelegationDecision,
248
+ currentFacts: ScopeFacts,
249
+ originalFacts: ScopeFacts
250
+ ): boolean {
251
+ // Scope significantly expanded
252
+ if (currentFacts.observedFiles > originalFacts.observedFiles * 1.5) {
253
+ return true;
254
+ }
255
+
256
+ // New coupling discovered
257
+ if (currentFacts.tightlyCoupled && !originalFacts.tightlyCoupled) {
258
+ return true;
259
+ }
260
+
261
+ // Reassessment triggers
262
+ return decision.reassessmentTriggers.some((trigger) => {
263
+ if (trigger.includes("scope expands")) {
264
+ return currentFacts.observedFiles > originalFacts.observedFiles * 1.5;
265
+ }
266
+ return false;
267
+ });
268
+ }
@@ -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
+ }