pi-harness-runtime 1.1.349 → 1.1.350

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 (120) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/dist/delegation-assessor.d.ts +27 -0
  24. package/packages/contracts/dist/delegation-assessor.js +210 -0
  25. package/packages/contracts/dist/delegation.d.ts +142 -0
  26. package/packages/contracts/dist/delegation.js +33 -0
  27. package/packages/contracts/dist/index.d.ts +3 -0
  28. package/packages/contracts/dist/index.js +3 -0
  29. package/packages/contracts/dist/scope-discovery.d.ts +65 -0
  30. package/packages/contracts/dist/scope-discovery.js +247 -0
  31. package/packages/contracts/package.json +1 -1
  32. package/packages/contracts/src/delegation-assessor.ts +268 -0
  33. package/packages/contracts/src/delegation.ts +174 -0
  34. package/packages/contracts/src/index.ts +3 -0
  35. package/packages/contracts/src/scope-discovery.ts +352 -0
  36. package/packages/cookie-sanitizer/package.json +1 -1
  37. package/packages/cost-optimizer/package.json +1 -1
  38. package/packages/dependency-analyzer/package.json +1 -1
  39. package/packages/django-plugin/package.json +1 -1
  40. package/packages/doc-generator/package.json +1 -1
  41. package/packages/durable-review/package.json +1 -1
  42. package/packages/e2e-tester/package.json +1 -1
  43. package/packages/evaluation-engine/package.json +1 -1
  44. package/packages/evaluation-runner/package.json +1 -1
  45. package/packages/event-bus/package.json +1 -1
  46. package/packages/event-sink/package.json +1 -1
  47. package/packages/event-store/package.json +1 -1
  48. package/packages/experience-replay/package.json +1 -1
  49. package/packages/feedback-collector/package.json +1 -1
  50. package/packages/file-copy-helper/package.json +1 -1
  51. package/packages/framework-detector/package.json +1 -1
  52. package/packages/framework-plugin-sdk/package.json +1 -1
  53. package/packages/frappe-plugin/package.json +1 -1
  54. package/packages/generic-web-plugin/package.json +1 -1
  55. package/packages/health-monitor/package.json +1 -1
  56. package/packages/intent-analyzer/package.json +1 -1
  57. package/packages/jev-judge/package.json +1 -1
  58. package/packages/knowledge-graph/package.json +1 -1
  59. package/packages/knowledge-retrieval/package.json +1 -1
  60. package/packages/laravel-plugin/package.json +1 -1
  61. package/packages/learning-engine/package.json +1 -1
  62. package/packages/mcp-adapter/package.json +1 -1
  63. package/packages/memory-engine/package.json +1 -1
  64. package/packages/milestone-manager/package.json +1 -1
  65. package/packages/model-registry/package.json +1 -1
  66. package/packages/nextjs-plugin/package.json +1 -1
  67. package/packages/notification/package.json +1 -1
  68. package/packages/observability/package.json +1 -1
  69. package/packages/okf-indexer/package.json +1 -1
  70. package/packages/paperclip-integration/package.json +1 -1
  71. package/packages/performance-optimizer/package.json +1 -1
  72. package/packages/permission-mode/package.json +1 -1
  73. package/packages/privilege-broker/package.json +1 -1
  74. package/packages/project-analyzer/package.json +1 -1
  75. package/packages/project-bootstrap/package.json +1 -1
  76. package/packages/projection-engine/package.json +1 -1
  77. package/packages/prompt-compiler/package.json +1 -1
  78. package/packages/prompt-versioning/package.json +1 -1
  79. package/packages/provider-adapter-sdk/package.json +1 -1
  80. package/packages/provider-router/package.json +1 -1
  81. package/packages/provider-selector/package.json +1 -1
  82. package/packages/providers/package.json +1 -1
  83. package/packages/qdrant-skills/package.json +1 -1
  84. package/packages/quota-manager/package.json +1 -1
  85. package/packages/rate-limiter/package.json +1 -1
  86. package/packages/react-vite-plugin/package.json +1 -1
  87. package/packages/relay/package.json +1 -1
  88. package/packages/release-manager/package.json +1 -1
  89. package/packages/requirement-compiler/package.json +1 -1
  90. package/packages/runtime/package.json +1 -1
  91. package/packages/scheduler/package.json +1 -1
  92. package/packages/scheduler-adapter/package.json +1 -1
  93. package/packages/session/package.json +1 -1
  94. package/packages/session-api/package.json +1 -1
  95. package/packages/session-export/package.json +1 -1
  96. package/packages/shared-context/package.json +1 -1
  97. package/packages/skill-mcp-client/package.json +1 -1
  98. package/packages/skill-registry/package.json +1 -1
  99. package/packages/skills/package.json +1 -1
  100. package/packages/sprint-planner/package.json +1 -1
  101. package/packages/subscription-engine/package.json +1 -1
  102. package/packages/task-compiler/package.json +1 -1
  103. package/packages/telegram/package.json +1 -1
  104. package/packages/tencentdb-memory/package.json +1 -1
  105. package/packages/tencentdb-sync/package.json +1 -1
  106. package/packages/test-data-generator/package.json +1 -1
  107. package/packages/test-generator/package.json +1 -1
  108. package/packages/todo-bd-sync/package.json +1 -1
  109. package/packages/token-estimation/package.json +1 -1
  110. package/packages/token-optimizer/package.json +1 -1
  111. package/packages/trajectory/package.json +1 -1
  112. package/packages/tui/package.json +1 -1
  113. package/packages/types/package.json +1 -1
  114. package/packages/voice-api/package.json +1 -1
  115. package/packages/webmcp-tester/package.json +1 -1
  116. package/packages/wiki/package.json +1 -1
  117. package/packages/workflow-events/package.json +1 -1
  118. package/packages/workspace-scanner/package.json +1 -1
  119. package/packages/worktree/package.json +1 -1
  120. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Scope Discovery
3
+ *
4
+ * Discovers and estimates the scope of refactoring work.
5
+ */
6
+ /**
7
+ * File pattern matcher
8
+ */
9
+ export interface FilePattern {
10
+ /** Glob pattern or exact extension */
11
+ pattern: string;
12
+ /** Whether to recurse into directories */
13
+ recursive?: boolean;
14
+ }
15
+ /**
16
+ * Scope discovery configuration
17
+ */
18
+ export interface ScopeDiscoveryConfig {
19
+ /** File extensions to consider relevant */
20
+ relevantExtensions: string[];
21
+ /** Directories to exclude (vendored, generated, etc.) */
22
+ excludeDirs: string[];
23
+ /** Files to exclude by name */
24
+ excludeFiles: string[];
25
+ /** Whether to scan for call sites */
26
+ scanCallSites: boolean;
27
+ /** Call site patterns to count */
28
+ callSitePatterns: string[];
29
+ }
30
+ /**
31
+ * Default scope discovery config
32
+ */
33
+ export declare const DEFAULT_SCOPE_CONFIG: ScopeDiscoveryConfig;
34
+ /**
35
+ * Discovered scope result
36
+ */
37
+ export interface DiscoveredScope {
38
+ /** Total relevant files found */
39
+ totalFiles: number;
40
+ /** Files matching the refactoring pattern */
41
+ matchingFiles: number;
42
+ /** Estimated call sites */
43
+ callSites: number;
44
+ /** Independent components found */
45
+ components: string[];
46
+ /** File breakdown by type */
47
+ fileTypes: Record<string, number>;
48
+ /** Whether shared APIs were detected */
49
+ hasSharedApis: boolean;
50
+ /** Whether transformation appears mechanical */
51
+ isMechanical: boolean;
52
+ /** Scope fingerprint for change detection */
53
+ fingerprint: string;
54
+ }
55
+ /**
56
+ * Discover scope by scanning files
57
+ */
58
+ export declare function discoverScope(workspacePath: string, searchPattern: string, config?: Partial<ScopeDiscoveryConfig>): DiscoveredScope;
59
+ /**
60
+ * Quick scope estimate using git grep
61
+ */
62
+ export declare function quickScopeEstimate(workspacePath: string, pattern: string): {
63
+ fileCount: number;
64
+ lineCount: number;
65
+ };
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Scope Discovery
3
+ *
4
+ * Discovers and estimates the scope of refactoring work.
5
+ */
6
+ import { readFileSync, readdirSync } from "fs";
7
+ import { join, extname, relative } from "path";
8
+ import { execSync } from "child_process";
9
+ /**
10
+ * Default scope discovery config
11
+ */
12
+ export const DEFAULT_SCOPE_CONFIG = {
13
+ relevantExtensions: [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"],
14
+ excludeDirs: [
15
+ "node_modules",
16
+ ".git",
17
+ "dist",
18
+ "build",
19
+ ".next",
20
+ "__pycache__",
21
+ "vendor",
22
+ "generated",
23
+ ".venv",
24
+ "coverage",
25
+ ],
26
+ excludeFiles: [
27
+ "package-lock.json",
28
+ "yarn.lock",
29
+ "pnpm-lock.yaml",
30
+ "bun.lockb",
31
+ "*.min.js",
32
+ "*.bundle.js",
33
+ ],
34
+ scanCallSites: true,
35
+ callSitePatterns: [
36
+ "TODO:",
37
+ "FIXME:",
38
+ "console.log",
39
+ "throw new",
40
+ "await this",
41
+ "// @ts-ignore",
42
+ ],
43
+ };
44
+ /**
45
+ * Discover scope by scanning files
46
+ */
47
+ export function discoverScope(workspacePath, searchPattern, config = {}) {
48
+ const cfg = { ...DEFAULT_SCOPE_CONFIG, ...config };
49
+ const files = [];
50
+ const fileTypes = {};
51
+ const callSites = [];
52
+ const components = new Set();
53
+ // Scan directory recursively
54
+ scanDirectory(workspacePath, files, fileTypes, cfg, 0);
55
+ // Filter to relevant files
56
+ const relevantFiles = files.filter((f) => cfg.relevantExtensions.includes(extname(f)));
57
+ // Count matching files and call sites
58
+ let matchingCount = 0;
59
+ for (const file of relevantFiles) {
60
+ const content = readFileSync(file, "utf8").slice(0, 10000); // First 10KB
61
+ // Check if file matches search pattern
62
+ if (content.toLowerCase().includes(searchPattern.toLowerCase())) {
63
+ matchingCount++;
64
+ // Count call sites
65
+ if (cfg.scanCallSites) {
66
+ for (const pattern of cfg.callSitePatterns) {
67
+ const matches = content.match(new RegExp(pattern, "gi"));
68
+ if (matches) {
69
+ callSites.push(...matches.map((m) => `${file}:${m}`));
70
+ }
71
+ }
72
+ }
73
+ // Identify component (directory)
74
+ const relPath = relative(workspacePath, file);
75
+ const parts = relPath.split("/");
76
+ if (parts.length > 1) {
77
+ components.add(parts.slice(0, -1).join("/"));
78
+ }
79
+ }
80
+ }
81
+ // Detect shared APIs (files that are imported by multiple components)
82
+ const hasSharedApis = detectSharedApis(relevantFiles);
83
+ // Detect if transformation is mechanical (simple replacements)
84
+ const isMechanical = detectMechanicalPattern(relevantFiles, searchPattern);
85
+ // Generate fingerprint
86
+ const fingerprint = generateFingerprint(matchingCount, callSites.length, components.size);
87
+ return {
88
+ totalFiles: relevantFiles.length,
89
+ matchingFiles: matchingCount,
90
+ callSites: callSites.slice(0, 100).length,
91
+ components: Array.from(components),
92
+ fileTypes,
93
+ hasSharedApis,
94
+ isMechanical,
95
+ fingerprint,
96
+ };
97
+ }
98
+ /**
99
+ * Recursively scan directory
100
+ */
101
+ function scanDirectory(dir, files, fileTypes, config, depth) {
102
+ if (depth > 10)
103
+ return; // Max depth guard
104
+ try {
105
+ const entries = readdirSync(dir, { withFileTypes: true });
106
+ for (const entry of entries) {
107
+ const fullPath = join(dir, entry.name);
108
+ // Skip excluded
109
+ if (config.excludeDirs.includes(entry.name))
110
+ continue;
111
+ if (config.excludeFiles.some((f) => entry.name.includes(f)))
112
+ continue;
113
+ if (entry.isDirectory()) {
114
+ scanDirectory(fullPath, files, fileTypes, config, depth + 1);
115
+ }
116
+ else if (entry.isFile()) {
117
+ files.push(fullPath);
118
+ const ext = extname(entry.name);
119
+ fileTypes[ext] = (fileTypes[ext] || 0) + 1;
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // Skip inaccessible directories
125
+ }
126
+ }
127
+ /**
128
+ * Detect if files have shared API patterns
129
+ */
130
+ function detectSharedApis(files) {
131
+ // Look for common export patterns
132
+ const exportCounts = new Map();
133
+ for (const file of files.slice(0, 50)) {
134
+ // Sample first 50 files
135
+ try {
136
+ const content = readFileSync(file, "utf8").slice(0, 5000);
137
+ // Count exports
138
+ const exportMatches = content.match(/export\s+(const|function|class|type|interface)/g);
139
+ if (exportMatches) {
140
+ exportCounts.set(file, (exportCounts.get(file) || 0) + exportMatches.length);
141
+ }
142
+ }
143
+ catch {
144
+ // Skip unreadable files
145
+ }
146
+ }
147
+ // Multiple files with exports suggests shared APIs
148
+ let filesWithExports = 0;
149
+ for (const count of exportCounts.values()) {
150
+ if (count > 0)
151
+ filesWithExports++;
152
+ }
153
+ return filesWithExports > 3;
154
+ }
155
+ /**
156
+ * Detect if transformation is mechanical (simple replacements)
157
+ */
158
+ function detectMechanicalPattern(files, searchPattern) {
159
+ // Mechanical = simple string replacement, no complex logic
160
+ // Look for consistent patterns
161
+ let mechanicalCount = 0;
162
+ let totalMatches = 0;
163
+ for (const file of files.slice(0, 20)) {
164
+ try {
165
+ const content = readFileSync(file, "utf8");
166
+ const matches = content.match(new RegExp(searchPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"));
167
+ if (matches) {
168
+ totalMatches += matches.length;
169
+ // Check if each match is a simple replacement (same context)
170
+ // Mechanical if: same indentation, same surrounding code pattern
171
+ const uniqueContexts = new Set();
172
+ for (const match of matches.slice(0, 5)) {
173
+ const idx = content.indexOf(match);
174
+ const before = content.slice(Math.max(0, idx - 20), idx);
175
+ const after = content.slice(idx + match.length, idx + match.length + 20);
176
+ uniqueContexts.add(before.trim() + "|" + after.trim());
177
+ }
178
+ // Single context = mechanical
179
+ if (uniqueContexts.size === 1) {
180
+ mechanicalCount++;
181
+ }
182
+ }
183
+ }
184
+ catch {
185
+ // Skip unreadable files
186
+ }
187
+ }
188
+ // More than half mechanical = likely mechanical
189
+ return totalMatches > 0 && mechanicalCount / totalMatches > 0.5;
190
+ }
191
+ /**
192
+ * Generate scope fingerprint
193
+ */
194
+ function generateFingerprint(fileCount, callSiteCount, componentCount) {
195
+ // Simple fingerprint based on counts
196
+ const data = `${fileCount}:${callSiteCount}:${componentCount}`;
197
+ let hash = 0;
198
+ for (let i = 0; i < data.length; i++) {
199
+ const char = data.charCodeAt(i);
200
+ hash = (hash << 5) - hash + char;
201
+ hash = hash & hash; // Convert to 32bit integer
202
+ }
203
+ return Math.abs(hash).toString(36);
204
+ }
205
+ /**
206
+ * Quick scope estimate using git grep
207
+ */
208
+ export function quickScopeEstimate(workspacePath, pattern) {
209
+ // Validate inputs to prevent command injection
210
+ const safePath = validatePath(workspacePath);
211
+ if (!safePath)
212
+ return { fileCount: 0, lineCount: 0 };
213
+ const safePattern = escapeForGrep(pattern);
214
+ if (!safePattern)
215
+ return { fileCount: 0, lineCount: 0 };
216
+ try {
217
+ const result = execSync(`grep -r "${safePattern}" "${safePath}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" -l 2>/dev/null | wc -l`, { encoding: "utf8", timeout: 10000 });
218
+ const fileCount = parseInt(result.trim(), 10) || 0;
219
+ const lineResult = execSync(`grep -r "${safePattern}" "${safePath}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" 2>/dev/null | wc -l`, { encoding: "utf8", timeout: 10000 });
220
+ const lineCount = parseInt(lineResult.trim(), 10) || 0;
221
+ return { fileCount, lineCount };
222
+ }
223
+ catch {
224
+ return { fileCount: 0, lineCount: 0 };
225
+ }
226
+ }
227
+ /**
228
+ * Validate and sanitize a file path
229
+ */
230
+ function validatePath(path) {
231
+ // Only allow alphanumeric, hyphen, underscore, dot, and forward slash
232
+ if (!/^[\w./-]+$/.test(path)) {
233
+ return null;
234
+ }
235
+ // Prevent path traversal
236
+ if (path.includes("..")) {
237
+ return null;
238
+ }
239
+ return path;
240
+ }
241
+ /**
242
+ * Escape a string for safe use in grep pattern
243
+ */
244
+ function escapeForGrep(pattern) {
245
+ // Remove any shell metacharacters
246
+ return pattern.replace(/[|;&$`\\"'<>\(\)\[\]\{\}]/g, "");
247
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/contracts",
3
- "version": "1.1.349",
3
+ "version": "1.1.350",
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
+ }