pi-harness-runtime 1.1.451 → 1.1.452

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 (93) hide show
  1. package/AGENTS.md +53 -0
  2. package/CHANGELOG.md +24 -0
  3. package/index.ts +172 -21
  4. package/package.json +1 -1
  5. package/packages/agent-policy/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/checkpoint/package.json +1 -1
  9. package/packages/clipboard/package.json +1 -1
  10. package/packages/codex-watcher/package.json +1 -1
  11. package/packages/config-capture/package.json +1 -1
  12. package/packages/contracts/package.json +1 -1
  13. package/packages/cookie-sanitizer/package.json +1 -1
  14. package/packages/evaluation-engine/package.json +1 -1
  15. package/packages/event-bus/package.json +1 -1
  16. package/packages/event-store/package.json +1 -1
  17. package/packages/experience-replay/package.json +1 -1
  18. package/packages/file-copy-helper/package.json +1 -1
  19. package/packages/intent-analyzer/package.json +1 -1
  20. package/packages/jev-judge/dist/continuation-status.d.ts +31 -0
  21. package/packages/jev-judge/dist/continuation-status.d.ts.map +1 -0
  22. package/packages/jev-judge/dist/continuation-status.js +40 -0
  23. package/packages/jev-judge/dist/continuation-status.js.map +1 -0
  24. package/packages/jev-judge/dist/delegation-harness.d.ts +74 -1
  25. package/packages/jev-judge/dist/delegation-harness.d.ts.map +1 -1
  26. package/packages/jev-judge/dist/delegation-harness.js +183 -18
  27. package/packages/jev-judge/dist/delegation-harness.js.map +1 -1
  28. package/packages/jev-judge/dist/delegation-scope.d.ts +88 -0
  29. package/packages/jev-judge/dist/delegation-scope.d.ts.map +1 -0
  30. package/packages/jev-judge/dist/delegation-scope.js +218 -0
  31. package/packages/jev-judge/dist/delegation-scope.js.map +1 -0
  32. package/packages/jev-judge/package.json +1 -1
  33. package/packages/jev-judge/src/continuation-status.ts +41 -0
  34. package/packages/jev-judge/src/delegation-harness.ts +254 -45
  35. package/packages/jev-judge/src/delegation-scope.ts +264 -0
  36. package/packages/knowledge-retrieval/package.json +1 -1
  37. package/packages/learning-engine/package.json +1 -1
  38. package/packages/logging/package.json +1 -1
  39. package/packages/memory-engine/package.json +1 -1
  40. package/packages/notification/package.json +1 -1
  41. package/packages/paperclip-integration/package.json +1 -1
  42. package/packages/performance-optimizer/package.json +1 -1
  43. package/packages/privilege-broker/package.json +1 -1
  44. package/packages/prompt-compiler/package.json +1 -1
  45. package/packages/providers/package.json +1 -1
  46. package/packages/qdrant-skills/package.json +1 -1
  47. package/packages/quota-manager/package.json +1 -1
  48. package/packages/relay/package.json +1 -1
  49. package/packages/skill-mcp-client/package.json +1 -1
  50. package/packages/skills/package.json +1 -1
  51. package/packages/todo-bd-sync/package.json +1 -1
  52. package/packages/token-estimation/package.json +1 -1
  53. package/packages/tooling-preference/package.json +1 -1
  54. package/packages/trajectory/package.json +1 -1
  55. package/packages/tui/package.json +1 -1
  56. package/packages/two-phase-review/dist/classify.d.ts +22 -1
  57. package/packages/two-phase-review/dist/classify.d.ts.map +1 -1
  58. package/packages/two-phase-review/dist/classify.js +37 -1
  59. package/packages/two-phase-review/dist/classify.js.map +1 -1
  60. package/packages/two-phase-review/dist/index.d.ts +4 -2
  61. package/packages/two-phase-review/dist/index.d.ts.map +1 -1
  62. package/packages/two-phase-review/dist/index.js +3 -2
  63. package/packages/two-phase-review/dist/index.js.map +1 -1
  64. package/packages/two-phase-review/dist/rules.d.ts +8 -3
  65. package/packages/two-phase-review/dist/rules.d.ts.map +1 -1
  66. package/packages/two-phase-review/dist/rules.js +36 -5
  67. package/packages/two-phase-review/dist/rules.js.map +1 -1
  68. package/packages/two-phase-review/dist/runtime.d.ts +9 -0
  69. package/packages/two-phase-review/dist/runtime.d.ts.map +1 -1
  70. package/packages/two-phase-review/dist/runtime.js +68 -17
  71. package/packages/two-phase-review/dist/runtime.js.map +1 -1
  72. package/packages/two-phase-review/dist/state.d.ts +15 -0
  73. package/packages/two-phase-review/dist/state.d.ts.map +1 -1
  74. package/packages/two-phase-review/dist/state.js +37 -2
  75. package/packages/two-phase-review/dist/state.js.map +1 -1
  76. package/packages/two-phase-review/dist/types.d.ts +2 -2
  77. package/packages/two-phase-review/dist/types.d.ts.map +1 -1
  78. package/packages/two-phase-review/dist/validation.d.ts +50 -0
  79. package/packages/two-phase-review/dist/validation.d.ts.map +1 -0
  80. package/packages/two-phase-review/dist/validation.js +54 -0
  81. package/packages/two-phase-review/dist/validation.js.map +1 -0
  82. package/packages/two-phase-review/package.json +1 -1
  83. package/packages/two-phase-review/src/classify.ts +38 -1
  84. package/packages/two-phase-review/src/index.ts +16 -0
  85. package/packages/two-phase-review/src/rules.ts +48 -4
  86. package/packages/two-phase-review/src/runtime.ts +88 -17
  87. package/packages/two-phase-review/src/state.ts +38 -2
  88. package/packages/two-phase-review/src/types.ts +2 -1
  89. package/packages/two-phase-review/src/validation.ts +86 -0
  90. package/packages/types/package.json +1 -1
  91. package/packages/webmcp-tester/package.json +1 -1
  92. package/packages/wiki/package.json +1 -1
  93. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,218 @@
1
+ /**
2
+ * The measured scope of a task.
3
+ *
4
+ * A delegation decision is only worth something when it is based on the
5
+ * work that really has to be done. The numbers of a request are not a
6
+ * measurement: "refactor everything" says nothing about how many files
7
+ * are affected. This module measures the scope in the workspace:
8
+ *
9
+ * - The files of the workspace that the request actually names.
10
+ * - The files the session has already changed.
11
+ * - The groups those files form (one group per package or directory).
12
+ * - The tracked files that reference the measured groups.
13
+ *
14
+ * Every number in the result is a count of something real. When nothing
15
+ * can be measured the result is empty, and the caller has to say so
16
+ * instead of inventing a scope.
17
+ */
18
+ import { execFileSync } from "node:child_process";
19
+ import { createHash } from "node:crypto";
20
+ /** The default upper bound of the files of one measurement. */
21
+ const DEFAULT_LIMIT = 400;
22
+ /** The maximum number of groups that are searched for references. */
23
+ const MAX_REFERENCE_GROUPS = 10;
24
+ /**
25
+ * Run a git command in the workspace and return its output.
26
+ *
27
+ * A command that fails is not a measurement: the caller gets an empty
28
+ * result instead of a guess.
29
+ */
30
+ function git(workspacePath, args) {
31
+ try {
32
+ return execFileSync("git", args, {
33
+ cwd: workspacePath,
34
+ encoding: "utf8",
35
+ maxBuffer: 64 * 1024 * 1024,
36
+ stdio: ["ignore", "pipe", "ignore"],
37
+ });
38
+ }
39
+ catch {
40
+ return "";
41
+ }
42
+ }
43
+ /**
44
+ * The tracked files of the workspace.
45
+ */
46
+ function trackedFiles(workspacePath) {
47
+ const out = git(workspacePath, ["ls-files", "-z"]);
48
+ if (!out)
49
+ return [];
50
+ return out.split("\0").filter(Boolean);
51
+ }
52
+ /**
53
+ * The files the working tree has changed.
54
+ */
55
+ export function changedFiles(workspacePath) {
56
+ const out = git(workspacePath, ["status", "--porcelain", "-z"]);
57
+ if (!out)
58
+ return [];
59
+ const files = [];
60
+ for (const entry of out.split("\0")) {
61
+ if (!entry)
62
+ continue;
63
+ // The porcelain format is "XY path"; a rename carries two paths.
64
+ const path = entry.slice(3).trim();
65
+ if (!path)
66
+ continue;
67
+ if (path.includes(" -> ")) {
68
+ const parts = path.split(" -> ");
69
+ files.push(parts[0].trim(), parts[1].trim());
70
+ continue;
71
+ }
72
+ files.push(path);
73
+ }
74
+ return files.filter(Boolean);
75
+ }
76
+ /**
77
+ * The group a file belongs to: its package, or its top directory.
78
+ */
79
+ export function moduleOf(file) {
80
+ const parts = file.split("/");
81
+ if (parts[0] === "packages" && parts.length > 1)
82
+ return `packages/${parts[1]}`;
83
+ return parts[0];
84
+ }
85
+ /**
86
+ * The extension of a file, without the dot.
87
+ */
88
+ export function extensionOf(file) {
89
+ const name = file.split("/").pop() ?? file;
90
+ const dot = name.lastIndexOf(".");
91
+ if (dot <= 0)
92
+ return "";
93
+ return name.slice(dot + 1).toLowerCase();
94
+ }
95
+ /**
96
+ * Compare two paths.
97
+ */
98
+ function comparePaths(a, b) {
99
+ if (a === b)
100
+ return 0;
101
+ return a < b ? -1 : 1;
102
+ }
103
+ /**
104
+ * Group the measured files.
105
+ */
106
+ export function groupFiles(files) {
107
+ const groups = new Map();
108
+ for (const file of files) {
109
+ const id = moduleOf(file);
110
+ const list = groups.get(id) ?? [];
111
+ list.push(file);
112
+ groups.set(id, list);
113
+ }
114
+ return [...groups.entries()]
115
+ .map(([id, groupFiles]) => ({ id, files: groupFiles.sort(comparePaths) }))
116
+ .sort((a, b) => a.id.localeCompare(b.id));
117
+ }
118
+ /**
119
+ * The number of tracked files that reference the groups.
120
+ *
121
+ * The count is a real count of the files that mention the path of the
122
+ * group, so it is a lower bound of the coupling, not a guess.
123
+ */
124
+ function countCallSites(workspacePath, groups) {
125
+ let count = 0;
126
+ for (const group of groups.slice(0, MAX_REFERENCE_GROUPS)) {
127
+ const out = git(workspacePath, ["grep", "-l", "-F", "--", group.id]);
128
+ if (!out)
129
+ continue;
130
+ const files = out.split("\n").filter(Boolean);
131
+ // The files of the group mention their own path: they are not
132
+ // references to the group.
133
+ count += files.filter((file) => moduleOf(file) !== group.id).length;
134
+ }
135
+ return count;
136
+ }
137
+ /**
138
+ * A stable fingerprint of a set of files.
139
+ */
140
+ export function fingerprintOf(files) {
141
+ const hash = createHash("sha256");
142
+ hash.update([...files].sort(comparePaths).join("\n"));
143
+ return hash.digest("hex").slice(0, 16);
144
+ }
145
+ /**
146
+ * The path fragments of a request.
147
+ *
148
+ * A hint is a place in the repository that the request names: a path with
149
+ * a separator, or a file name with a known extension. The measurement
150
+ * checks every hint against the tracked files, so a hint that names
151
+ * nothing is simply not used.
152
+ */
153
+ export function scopeHintsFromText(text) {
154
+ const hints = new Set();
155
+ for (const match of text.match(/[\w.-]+(?:\/[\w.-]+)+/g) ?? []) {
156
+ hints.add(match.replace(/^\.\//, ""));
157
+ }
158
+ for (const match of text.match(/[\w-]+\.(?:ts|tsx|mts|cts|js|mjs|cjs|json|md|py|sh|yaml|yml)\b/g) ?? []) {
159
+ hints.add(match);
160
+ }
161
+ return [...hints].slice(0, 8);
162
+ }
163
+ /**
164
+ * Measure the scope of a task in the workspace.
165
+ */
166
+ export function measureScope(options) {
167
+ const workspacePath = options.workspacePath;
168
+ const limit = options.limit ?? DEFAULT_LIMIT;
169
+ const tracked = trackedFiles(workspacePath);
170
+ const hints = (options.hints ?? []).map((hint) => hint.trim()).filter(Boolean);
171
+ // The files the request names: a real match against the tracked paths.
172
+ const matchedHints = [];
173
+ const named = new Set();
174
+ for (const hint of hints) {
175
+ const needle = hint.toLowerCase();
176
+ let matched = false;
177
+ for (const file of tracked) {
178
+ if (!file.toLowerCase().includes(needle))
179
+ continue;
180
+ matched = true;
181
+ if (named.size < limit)
182
+ named.add(file);
183
+ }
184
+ if (matched)
185
+ matchedHints.push(hint);
186
+ }
187
+ // The files the session has changed: the scope that is already real.
188
+ const changed = (options.changedFiles ?? changedFiles(workspacePath)).filter((file) => tracked.includes(file));
189
+ let files = [];
190
+ let source = "none";
191
+ if (named.size > 0) {
192
+ files = [...named];
193
+ source = "request";
194
+ }
195
+ else if (changed.length > 0) {
196
+ files = changed.slice(0, limit);
197
+ source = "working-tree";
198
+ }
199
+ const groups = groupFiles(files);
200
+ const fileTypes = {};
201
+ for (const file of files) {
202
+ const extension = extensionOf(file);
203
+ if (!extension)
204
+ continue;
205
+ fileTypes[extension] = (fileTypes[extension] ?? 0) + 1;
206
+ }
207
+ return {
208
+ files: files.sort(comparePaths),
209
+ groups,
210
+ workspaceFiles: tracked.length,
211
+ callSites: countCallSites(workspacePath, groups),
212
+ fileTypes,
213
+ fingerprint: fingerprintOf(files),
214
+ matchedHints,
215
+ source,
216
+ };
217
+ }
218
+ //# sourceMappingURL=delegation-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delegation-scope.js","sourceRoot":"","sources":["../src/delegation-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA0CzC,+DAA+D;AAC/D,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B,qEAAqE;AACrE,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;GAKG;AACH,SAAS,GAAG,CAAC,aAAqB,EAAE,IAAc;IACjD,IAAI,CAAC;QACJ,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;YAChC,GAAG,EAAE,aAAa;YAClB,QAAQ,EAAE,MAAM;YAChB,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;YAC3B,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACnC,CAAC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,aAAqB;IAC1C,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;IACnD,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACxC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,aAAqB;IACjD,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC;IAChE,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,iEAAiE;QACjE,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACjC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YAC7C,SAAS;QACV,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAC9B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,UAAU,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/E,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;AAC1C,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,CAAS,EAAE,CAAS;IACzC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,KAAe;IACzC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChB,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SAC1B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;SACzE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,aAAqB,EAAE,MAAwB;IACtE,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,EAAE,CAAC;QAC3D,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;QACrE,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC9C,8DAA8D;QAC9D,2BAA2B;QAC3B,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC;IACrE,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,KAAe;IAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC9C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,IAAI,EAAE,EAAE,CAAC;QAChE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,iEAAiE,CAAC,IAAI,EAAE,EAAE,CAAC;QACzG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,OAA4B;IACxD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC;IAC7C,MAAM,OAAO,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAE/E,uEAAuE;IACvE,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAClC,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,SAAS;YACnD,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,KAAK,CAAC,IAAI,GAAG,KAAK;gBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QACD,IAAI,OAAO;YAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,qEAAqE;IACrE,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACrF,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CACtB,CAAC;IAEF,IAAI,KAAK,GAAa,EAAE,CAAC;IACzB,IAAI,MAAM,GAA+B,MAAM,CAAC;IAChD,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACpB,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;QACnB,MAAM,GAAG,SAAS,CAAC;IACpB,CAAC;SAAM,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAChC,MAAM,GAAG,cAAc,CAAC;IACzB,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,SAAS,GAA2B,EAAE,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,SAAS;YAAE,SAAS;QACzB,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACxD,CAAC;IAED,OAAO;QACN,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC;QAC/B,MAAM;QACN,cAAc,EAAE,OAAO,CAAC,MAAM;QAC9B,SAAS,EAAE,cAAc,CAAC,aAAa,EAAE,MAAM,CAAC;QAChD,SAAS;QACT,WAAW,EAAE,aAAa,CAAC,KAAK,CAAC;QACjC,YAAY;QACZ,MAAM;KACN,CAAC;AACH,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/jev-judge",
3
- "version": "1.1.451",
3
+ "version": "1.1.452",
4
4
  "description": "Jev Judge - Structured decision model for agent routing, E2E testing, and automated decisions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The lifecycle of a continuation that the harness dispatched.
3
+ *
4
+ * A continuation is a turn that the harness started itself. While that
5
+ * turn runs, the status says so: a timer that clears the status while the
6
+ * agent is still working would claim that nothing runs any more.
7
+ *
8
+ * The gate is the only thing that decides whether a continuation is still
9
+ * pending, so it can be read and tested without a session.
10
+ */
11
+ export class ContinuationGate {
12
+ private inFlight = false;
13
+
14
+ /**
15
+ * A continuation was dispatched.
16
+ *
17
+ * The status stays "RUN" from here until the turn settles.
18
+ */
19
+ begin(): void {
20
+ this.inFlight = true;
21
+ }
22
+
23
+ /**
24
+ * The turn settled.
25
+ *
26
+ * @returns True when a continuation was still pending, so the caller
27
+ * has to clear the status. False when there was nothing to clear.
28
+ */
29
+ settle(): boolean {
30
+ if (!this.inFlight) return false;
31
+ this.inFlight = false;
32
+ return true;
33
+ }
34
+
35
+ /**
36
+ * Whether a continuation is still running.
37
+ */
38
+ get pending(): boolean {
39
+ return this.inFlight;
40
+ }
41
+ }
@@ -9,17 +9,34 @@ import { homedir as osHomedir } from "node:os";
9
9
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
11
11
  import { join } from "path";
12
+ import type { ScopeMeasurement, ScopeWorkGroup } from "./delegation-scope.js";
12
13
 
13
14
  /**
14
15
  * Extension capability for delegation (inline to avoid dependency)
15
16
  */
16
17
  export interface ExtensionCapability {
17
- name: string;
18
- supportsSubagents: boolean;
19
- supportsWorkIsolation: boolean;
20
- supportsCancellation: boolean;
21
- availableTools: string[];
22
- verified: boolean;
18
+ name: string;
19
+ /** The tool that can dispatch a child. */
20
+ toolName: string;
21
+ supportsSubagents: boolean;
22
+ /**
23
+ * The schema of the tool carries a parameter that isolates the work of
24
+ * a child. Without such a parameter the claim is not made.
25
+ */
26
+ supportsWorkIsolation: boolean;
27
+ /**
28
+ * The schema of the tool carries a parameter that stops a child. Without
29
+ * such a parameter the claim is not made.
30
+ */
31
+ supportsCancellation: boolean;
32
+ availableTools: string[];
33
+ /**
34
+ * The schema of the tool was read and the tool is active.
35
+ *
36
+ * This is not a proof that a child ever ran: it says that the session
37
+ * can ask for one. Only a dispatch that was really sent proves that.
38
+ */
39
+ verified: boolean;
23
40
  }
24
41
 
25
42
  /**
@@ -36,29 +53,52 @@ export type DelegationStrategy =
36
53
  * Delegation decision
37
54
  */
38
55
  export interface DelegationDecision {
39
- strategy: DelegationStrategy;
40
- reason: string;
41
- workGroups: unknown[];
42
- validationExpectations: string;
43
- confidence: "high" | "medium" | "low";
44
- jevUsed: boolean;
45
- reassessmentTriggers: string[];
56
+ strategy: DelegationStrategy;
57
+ reason: string;
58
+ /**
59
+ * The groups the work falls into. They are empty unless the scope was
60
+ * measured: a decision without groups is not a delegation plan.
61
+ */
62
+ workGroups: ScopeWorkGroup[];
63
+ validationExpectations: string;
64
+ confidence: "high" | "medium" | "low";
65
+ jevUsed: boolean;
66
+ /**
67
+ * Whether a child was really dispatched.
68
+ *
69
+ * The harness assesses the work, it does not run it: the agent is the
70
+ * one that sends the groups to the sub-agent tool.
71
+ */
72
+ dispatched: boolean;
73
+ /**
74
+ * The name of the active tool that can dispatch a child.
75
+ *
76
+ * It is empty when no such tool is active. The name is the proof that
77
+ * the dispatch is possible, not the proof that it happened.
78
+ */
79
+ dispatchTool?: string;
80
+ reassessmentTriggers: string[];
46
81
  }
47
82
 
48
83
  /**
49
84
  * Scope facts
50
85
  */
51
86
  export interface ScopeFacts {
52
- estimatedFiles: number;
53
- observedFiles: number;
54
- estimatedCallSites: number;
55
- observedCallSites: number;
56
- independentComponents: number;
57
- tightlyCoupled: boolean;
58
- isMechanical: boolean;
59
- isComplex: boolean;
60
- fileTypes: Record<string, number>;
61
- fingerprint: string;
87
+ estimatedFiles: number;
88
+ observedFiles: number;
89
+ estimatedCallSites: number;
90
+ observedCallSites: number;
91
+ independentComponents: number;
92
+ tightlyCoupled: boolean;
93
+ isMechanical: boolean;
94
+ isComplex: boolean;
95
+ fileTypes: Record<string, number>;
96
+ fingerprint: string;
97
+ /**
98
+ * The measured groups of the work. A delegation decision needs them:
99
+ * without groups there is nothing to hand to a child.
100
+ */
101
+ workGroups: ScopeWorkGroup[];
62
102
  }
63
103
 
64
104
  /**
@@ -150,9 +190,149 @@ interface DelegationState {
150
190
  }
151
191
 
152
192
  /**
153
- * Sub-agent tool names
193
+ * The number of groups that can be handed to children at once.
194
+ *
195
+ * More groups than that is not a delegation plan: it is a list of files
196
+ * that still needs a decision about the order.
154
197
  */
155
- const SUBAGENT_TOOLS = ["subagent", "spawn", "delegate", "run_subagent"];
198
+ const MAX_DELEGATED_GROUPS = 8;
199
+
200
+ /**
201
+ * Build the scope facts from a measurement.
202
+ *
203
+ * Every number is a count of something the measurement found. The
204
+ * estimate is the size of the workspace: it is a real number, and it says
205
+ * how much of the repository the measured files are.
206
+ */
207
+ export function scopeFactsFromMeasurement(measurement: ScopeMeasurement): ScopeFacts {
208
+ const groups = measurement.groups;
209
+ const extensions = Object.keys(measurement.fileTypes);
210
+ return {
211
+ estimatedFiles: measurement.workspaceFiles,
212
+ observedFiles: measurement.files.length,
213
+ estimatedCallSites: measurement.callSites,
214
+ observedCallSites: measurement.callSites,
215
+ independentComponents: groups.length,
216
+ tightlyCoupled: groups.length <= 1,
217
+ // A mechanical change touches one kind of file, and there is more
218
+ // than one of them: a rename across many files is mechanical, a
219
+ // single file is just work.
220
+ isMechanical: extensions.length === 1 && measurement.files.length >= 3,
221
+ // A complex change spans more than one module.
222
+ isComplex: groups.length > 1,
223
+ fileTypes: measurement.fileTypes,
224
+ fingerprint: measurement.fingerprint,
225
+ workGroups: groups,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * The reason why the work stays in the session.
231
+ */
232
+ function singleAgentReason(groups: ScopeWorkGroup[]): string {
233
+ if (groups.length === 1) {
234
+ return `One measured group (${groups[0].id}): the work stays in this session`;
235
+ }
236
+ if (groups.length > MAX_DELEGATED_GROUPS) {
237
+ return `${groups.length} measured groups: too many to hand out at once, the work stays in this session`;
238
+ }
239
+ return "No measured group of work: the work stays in this session";
240
+ }
241
+
242
+ /**
243
+ * Build the message that hands a delegation plan to the agent.
244
+ *
245
+ * The message names the measured groups and the tool that can dispatch a
246
+ * child. It does not claim that anything was dispatched: the harness
247
+ * measured the work, the agent is the one that sends the groups out.
248
+ *
249
+ * @param decision The decision of the assessment.
250
+ * @returns The message, or null when there is nothing to hand out.
251
+ */
252
+ export function buildDelegationSteer(
253
+ decision: {
254
+ strategy: string;
255
+ workGroups?: { id: string; files: string[] }[];
256
+ dispatchTool?: string;
257
+ } | null,
258
+ ): string | null {
259
+ if (!decision) return null;
260
+ if (decision.strategy !== "delegate") return null;
261
+ const groups = decision.workGroups ?? [];
262
+ if (groups.length === 0) return null;
263
+
264
+ const lines = [
265
+ `[Delegation plan] The measured scope of this request falls into ${groups.length} independent group(s):`,
266
+ ...groups.map((group) => `- ${group.id} (${group.files.length} file(s))`),
267
+ ];
268
+ if (decision.dispatchTool) {
269
+ lines.push(
270
+ `Send each group to a child agent with the ${decision.dispatchTool} tool, then integrate the results and validate them.`,
271
+ );
272
+ } else {
273
+ lines.push("Send each group to a child agent, then integrate the results and validate them.");
274
+ }
275
+ lines.push("The harness measured the scope; it did not dispatch the groups itself.");
276
+ return lines.join("\n");
277
+ }
278
+
279
+ /**
280
+ * The parameters of a tool that dispatch a child.
281
+ *
282
+ * A tool that takes the task of a child can dispatch one. The name of the
283
+ * tool says nothing about that: only the schema does.
284
+ */
285
+ const DISPATCH_PARAMETERS = ["task", "prompt", "instructions", "description"];
286
+
287
+ /**
288
+ * The parameters of a tool that isolate the work of a child.
289
+ */
290
+ const ISOLATION_PARAMETERS = ["cwd", "worktree", "isolation", "sandbox", "branch"];
291
+
292
+ /**
293
+ * The parameters of a tool that stop a child.
294
+ */
295
+ const CANCELLATION_PARAMETERS = ["timeout", "timeoutMs", "cancel", "signal", "abort"];
296
+
297
+ /**
298
+ * The words that name a child agent.
299
+ */
300
+ const CHILD_WORDS = ["subagent", "sub-agent", "child agent", "delegate", "spawn"];
301
+
302
+ /**
303
+ * The property names of a tool schema.
304
+ *
305
+ * A schema is a plain object: the properties of the object, of every
306
+ * branch of a union, and of every item of an array.
307
+ */
308
+ export function schemaPropertyNames(schema: unknown): Set<string> {
309
+ const names = new Set<string>();
310
+ const visit = (node: unknown): void => {
311
+ if (!node || typeof node !== "object") return;
312
+ const record = node as Record<string, unknown>;
313
+ const properties = record.properties;
314
+ if (properties && typeof properties === "object") {
315
+ for (const name of Object.keys(properties as Record<string, unknown>)) {
316
+ names.add(name);
317
+ }
318
+ }
319
+ for (const branch of ["anyOf", "oneOf", "allOf"]) {
320
+ const list = record[branch];
321
+ if (Array.isArray(list)) for (const entry of list) visit(entry);
322
+ }
323
+ const items = record.items;
324
+ if (items) visit(items);
325
+ };
326
+ visit(schema);
327
+ return names;
328
+ }
329
+
330
+ /**
331
+ * The text of a tool that names what the tool is for.
332
+ */
333
+ function toolText(tool: { name?: string; description?: string }): string {
334
+ return `${tool.name ?? ""} ${tool.description ?? ""}`.toLowerCase();
335
+ }
156
336
 
157
337
  /**
158
338
  * Create delegation harness integration
@@ -173,29 +353,42 @@ export function createDelegationHarness(
173
353
 
174
354
  /**
175
355
  * Discover and cache delegation capability
356
+ *
357
+ * The capability is read from the schema of the tools, not from their
358
+ * names: a tool that carries the task of a child can dispatch one, and
359
+ * only a schema that carries a working directory or a timeout proves
360
+ * that the work can be isolated or stopped.
176
361
  */
177
362
  async function discoverCapability(): Promise<ExtensionCapability | null> {
178
363
  if (state.capability) return state.capability;
179
364
 
180
365
  try {
181
366
  const allTools = pi.getAllTools();
182
- const toolNames = allTools.map((t) => t.name);
183
- const activeTools = pi.getActiveTools();
367
+ const activeTools = new Set(pi.getActiveTools());
184
368
 
185
- const hasSubagentTool = SUBAGENT_TOOLS.some(
186
- (tool) => toolNames.includes(tool) || activeTools.includes(tool)
187
- );
369
+ for (const tool of allTools) {
370
+ const properties = schemaPropertyNames(tool.parameters);
371
+ const dispatches = DISPATCH_PARAMETERS.some((parameter) => properties.has(parameter));
372
+ if (!dispatches) continue;
373
+ // The tool has to be about a child agent: a schema that takes a
374
+ // task is not enough when the tool is something else entirely.
375
+ const text = toolText(tool);
376
+ if (!CHILD_WORDS.some((word) => text.includes(word))) continue;
377
+
378
+ const isolation = ISOLATION_PARAMETERS.find((parameter) => properties.has(parameter));
379
+ const cancellation = CANCELLATION_PARAMETERS.find((parameter) => properties.has(parameter));
380
+ const isActive = activeTools.has(tool.name);
188
381
 
189
- if (hasSubagentTool) {
190
382
  state.capability = {
191
- name: "subagent-extension",
383
+ name: tool.name,
384
+ toolName: tool.name,
192
385
  supportsSubagents: true,
193
- supportsWorkIsolation: true,
194
- supportsCancellation: true,
195
- availableTools: activeTools.filter((t) =>
196
- SUBAGENT_TOOLS.some((st) => t.includes(st))
197
- ),
198
- verified: true,
386
+ supportsWorkIsolation: isolation !== undefined,
387
+ supportsCancellation: cancellation !== undefined,
388
+ availableTools: isActive ? [tool.name] : [],
389
+ // The capability is verified when the tool is active: an inactive
390
+ // tool cannot dispatch anything.
391
+ verified: isActive,
199
392
  };
200
393
  return state.capability;
201
394
  }
@@ -208,8 +401,13 @@ export function createDelegationHarness(
208
401
 
209
402
  /**
210
403
  * Check if delegation assessment should be triggered
404
+ *
405
+ * The thresholds are compared with the measured scope. A scope that was
406
+ * not measured is never assessed: an empty measurement is not a small
407
+ * task, it is an unknown one.
211
408
  */
212
409
  function shouldAssess(facts: ScopeFacts): boolean {
410
+ if (facts.observedFiles <= 0) return false;
213
411
  if (facts.observedFiles >= minFiles) return true;
214
412
  if (facts.observedCallSites >= minCallSites) return true;
215
413
  if (facts.independentComponents >= minComponents) return true;
@@ -218,6 +416,10 @@ export function createDelegationHarness(
218
416
 
219
417
  /**
220
418
  * Make deterministic assessment
419
+ *
420
+ * The decision is built from the measured scope and from the schema of
421
+ * the tools. A strategy that claims a delegation needs the groups of the
422
+ * measurement: without them there is nothing to hand to a child.
221
423
  */
222
424
  function makeDeterministicAssessment(
223
425
  facts: ScopeFacts,
@@ -226,11 +428,12 @@ export function createDelegationHarness(
226
428
  if (!capability?.verified) {
227
429
  return {
228
430
  strategy: "unavailable",
229
- reason: "No verified sub-agent capability",
431
+ reason: "No active tool can dispatch a child agent",
230
432
  workGroups: [],
231
433
  validationExpectations: "Parent handles validation",
232
434
  confidence: "high",
233
435
  jevUsed: false,
436
+ dispatched: false,
234
437
  reassessmentTriggers: ["Extension available"],
235
438
  };
236
439
  }
@@ -238,34 +441,40 @@ export function createDelegationHarness(
238
441
  if (facts.isMechanical && !facts.isComplex) {
239
442
  return {
240
443
  strategy: "script_only",
241
- reason: "Mechanical transformation",
444
+ reason: `Mechanical transformation of ${facts.observedFiles} measured file(s)`,
242
445
  workGroups: [],
243
446
  validationExpectations: "Script output validation",
244
447
  confidence: "high",
245
448
  jevUsed: false,
449
+ dispatched: false,
246
450
  reassessmentTriggers: ["Transformation unreliable"],
247
451
  };
248
452
  }
249
453
 
250
- if (facts.independentComponents >= 2 && !facts.tightlyCoupled) {
454
+ const groups = facts.workGroups ?? [];
455
+ if (groups.length >= 2 && groups.length <= MAX_DELEGATED_GROUPS && !facts.tightlyCoupled) {
251
456
  return {
252
457
  strategy: "delegate",
253
- reason: `${facts.independentComponents} independent components`,
254
- workGroups: [],
458
+ reason: `${groups.length} measured groups over ${facts.observedFiles} file(s); the agent dispatches the groups`,
459
+ workGroups: groups,
255
460
  validationExpectations: "Each group validated, then integrated",
256
461
  confidence: "medium",
257
462
  jevUsed: false,
463
+ // The harness measured the groups, it did not dispatch them.
464
+ dispatched: false,
465
+ dispatchTool: capability.toolName,
258
466
  reassessmentTriggers: ["Scope expands", "Coupling discovered"],
259
467
  };
260
468
  }
261
469
 
262
470
  return {
263
471
  strategy: "single_agent",
264
- reason: "Default to single agent",
265
- workGroups: [],
472
+ reason: singleAgentReason(groups),
473
+ workGroups: groups,
266
474
  validationExpectations: "Parent validates",
267
475
  confidence: "low",
268
476
  jevUsed: false,
477
+ dispatched: false,
269
478
  reassessmentTriggers: ["Scope exceeds threshold"],
270
479
  };
271
480
  }