@pi-unipi/unipi 2.7.0 → 2.8.0
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/package.json +4 -1
- package/packages/autocomplete/src/constants.ts +36 -0
- package/packages/background-tasks/README.md +87 -0
- package/packages/background-tasks/extensions/anthropic-attribution.ts +1 -0
- package/packages/background-tasks/extensions/delegate-child.ts +1 -0
- package/packages/background-tasks/extensions/fusion-child.ts +1 -0
- package/packages/background-tasks/package.json +40 -0
- package/packages/background-tasks/src/anthropic-attribution-path.ts +21 -0
- package/packages/background-tasks/src/anthropic-attribution.ts +1983 -0
- package/packages/background-tasks/src/attested-pi-run.ts +612 -0
- package/packages/background-tasks/src/child-process.ts +55 -0
- package/packages/background-tasks/src/common.ts +8 -0
- package/packages/background-tasks/src/config.ts +292 -0
- package/packages/background-tasks/src/context-parent-snapshot.ts +142 -0
- package/packages/background-tasks/src/context-token-budget.ts +903 -0
- package/packages/background-tasks/src/context-visible-conversation-v2.ts +551 -0
- package/packages/background-tasks/src/delegate/artifacts.ts +487 -0
- package/packages/background-tasks/src/delegate/budget.ts +415 -0
- package/packages/background-tasks/src/delegate/hook-contract.ts +154 -0
- package/packages/background-tasks/src/delegate/launch.ts +497 -0
- package/packages/background-tasks/src/delegate/result-package.ts +459 -0
- package/packages/background-tasks/src/delegate/runner.ts +449 -0
- package/packages/background-tasks/src/delegate/seed.ts +423 -0
- package/packages/background-tasks/src/delegate/types.ts +323 -0
- package/packages/background-tasks/src/delegate-child-extension.ts +978 -0
- package/packages/background-tasks/src/delegate-extension.ts +806 -0
- package/packages/background-tasks/src/durable-fs.ts +386 -0
- package/packages/background-tasks/src/extension-api.ts +548 -0
- package/packages/background-tasks/src/fusion/artifacts.ts +967 -0
- package/packages/background-tasks/src/fusion/budget.ts +1162 -0
- package/packages/background-tasks/src/fusion/child-protocol.ts +305 -0
- package/packages/background-tasks/src/fusion/claude-cache.ts +207 -0
- package/packages/background-tasks/src/fusion/clean-context.ts +91 -0
- package/packages/background-tasks/src/fusion/config.ts +449 -0
- package/packages/background-tasks/src/fusion/context.ts +265 -0
- package/packages/background-tasks/src/fusion/evaluation.ts +800 -0
- package/packages/background-tasks/src/fusion/orchestrator.ts +1288 -0
- package/packages/background-tasks/src/fusion/output-contract.ts +34 -0
- package/packages/background-tasks/src/fusion/pi-child.ts +2373 -0
- package/packages/background-tasks/src/fusion/prompts.ts +345 -0
- package/packages/background-tasks/src/fusion/result-package.ts +959 -0
- package/packages/background-tasks/src/fusion/source-policy.ts +257 -0
- package/packages/background-tasks/src/fusion/types.ts +1139 -0
- package/packages/background-tasks/src/fusion/web-fetch.ts +1060 -0
- package/packages/background-tasks/src/fusion/workflows.ts +184 -0
- package/packages/background-tasks/src/fusion-child-extension.ts +1052 -0
- package/packages/background-tasks/src/fusion-extension.ts +1293 -0
- package/packages/background-tasks/src/index.ts +295 -0
- package/packages/background-tasks/src/pi-launch.ts +225 -0
- package/packages/background-tasks/src/registry.ts +2424 -0
- package/packages/background-tasks/src/settings-overlay.ts +208 -0
- package/packages/background-tasks/src/task-manager.ts +774 -0
- package/packages/background-tasks/src/tools.ts +530 -0
- package/packages/background-tasks/src/turndown.d.ts +15 -0
- package/packages/background-tasks/src/types.ts +963 -0
- package/packages/background-tasks/src/ui/fusion-model-selector.ts +322 -0
- package/packages/background-tasks/src/windows-taskkill.ts +250 -0
- package/packages/subagents/agents/delegate.md +14 -0
- package/packages/subagents/agents/oracle.md +78 -0
- package/packages/subagents/agents/researcher.md +52 -0
- package/packages/subagents/agents/reviewer.md +79 -0
- package/packages/subagents/agents/scout.md +50 -0
- package/packages/subagents/agents/worker.md +59 -0
- package/packages/subagents/prompts/council.md +48 -0
- package/packages/subagents/prompts/gather-context-and-clarify.md +13 -0
- package/packages/subagents/prompts/parallel-cleanup.md +59 -0
- package/packages/subagents/prompts/parallel-research.md +50 -0
- package/packages/subagents/prompts/parallel-review.md +54 -0
- package/packages/subagents/prompts/review-loop.md +43 -0
- package/packages/subagents/skills/subagents/SKILL.md +2 -2
- package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +1 -1
- package/packages/subagents/skills/subagents/references/execution-controls.md +1 -1
- package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +1 -1
- package/packages/subagents/skills/subagents/references/prompting-and-roles.md +1 -1
- package/packages/unipi/bundled.js +18645 -1256
- package/packages/unipi/index.ts +2 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import type { Theme } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { Key, matchesKey, truncateToWidth, type Component } from '@earendil-works/pi-tui';
|
|
3
|
+
import { CURRENT_MODEL_SELECTION, defaultFusionModelConfig } from '../fusion/config.js';
|
|
4
|
+
import type { FusionModelConfigV1, FusionModelSelection } from '../fusion/types.js';
|
|
5
|
+
|
|
6
|
+
export const FUSION_MODEL_SLOT_IDS = [
|
|
7
|
+
'candidate-1',
|
|
8
|
+
'candidate-2',
|
|
9
|
+
'candidate-3',
|
|
10
|
+
'evaluator',
|
|
11
|
+
'merger',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type FusionModelSlotId = (typeof FUSION_MODEL_SLOT_IDS)[number];
|
|
15
|
+
|
|
16
|
+
export interface FusionModelChoice {
|
|
17
|
+
value: FusionModelSelection;
|
|
18
|
+
label: string;
|
|
19
|
+
description: string;
|
|
20
|
+
available: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FusionModelSelectorResult =
|
|
24
|
+
| { type: 'saved'; config: FusionModelConfigV1 }
|
|
25
|
+
| { type: 'cancelled' };
|
|
26
|
+
|
|
27
|
+
export interface FusionModelSelectorOptions {
|
|
28
|
+
initialConfig: FusionModelConfigV1;
|
|
29
|
+
choices: readonly FusionModelChoice[];
|
|
30
|
+
theme: Theme;
|
|
31
|
+
onSave: (config: FusionModelConfigV1) => Promise<void>;
|
|
32
|
+
onDone: (result: FusionModelSelectorResult) => void;
|
|
33
|
+
onRenderRequest?: (() => void) | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type SelectorMode = 'slots' | 'models';
|
|
37
|
+
|
|
38
|
+
interface SlotView {
|
|
39
|
+
id: FusionModelSlotId;
|
|
40
|
+
label: string;
|
|
41
|
+
value: FusionModelSelection;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function slotLabel(id: FusionModelSlotId): string {
|
|
45
|
+
if (id === 'candidate-1') return 'Candidate 1';
|
|
46
|
+
if (id === 'candidate-2') return 'Candidate 2';
|
|
47
|
+
if (id === 'candidate-3') return 'Candidate 3';
|
|
48
|
+
if (id === 'evaluator') return 'Evaluator';
|
|
49
|
+
return 'Merger';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function configValue(config: FusionModelConfigV1, id: FusionModelSlotId): FusionModelSelection {
|
|
53
|
+
if (id === 'candidate-1') return config.candidates[0];
|
|
54
|
+
if (id === 'candidate-2') return config.candidates[1];
|
|
55
|
+
if (id === 'candidate-3') return config.candidates[2];
|
|
56
|
+
if (id === 'evaluator') return config.evaluator;
|
|
57
|
+
return config.merger;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function updateConfig(
|
|
61
|
+
config: FusionModelConfigV1,
|
|
62
|
+
id: FusionModelSlotId,
|
|
63
|
+
value: FusionModelSelection,
|
|
64
|
+
): FusionModelConfigV1 {
|
|
65
|
+
if (id === 'candidate-1')
|
|
66
|
+
return { ...config, candidates: [value, config.candidates[1], config.candidates[2]] };
|
|
67
|
+
if (id === 'candidate-2')
|
|
68
|
+
return { ...config, candidates: [config.candidates[0], value, config.candidates[2]] };
|
|
69
|
+
if (id === 'candidate-3')
|
|
70
|
+
return { ...config, candidates: [config.candidates[0], config.candidates[1], value] };
|
|
71
|
+
if (id === 'evaluator') return { ...config, evaluator: value };
|
|
72
|
+
return { ...config, merger: value };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function queryMatches(choice: FusionModelChoice, query: string): boolean {
|
|
76
|
+
if (query.length === 0) return true;
|
|
77
|
+
const haystack = `${choice.label} ${choice.description} ${choice.value}`.toLowerCase();
|
|
78
|
+
return haystack.includes(query.toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function printable(data: string): string | undefined {
|
|
82
|
+
if (data.length !== 1) return undefined;
|
|
83
|
+
const code = data.charCodeAt(0);
|
|
84
|
+
if (code < 32 || code === 127) return undefined;
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function modelChoiceLine(choice: FusionModelChoice): string {
|
|
89
|
+
const status = choice.available ? '' : ' (unavailable)';
|
|
90
|
+
return `${choice.label}${status}${choice.description ? ` — ${choice.description}` : ''}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function slotViews(config: FusionModelConfigV1): SlotView[] {
|
|
94
|
+
return FUSION_MODEL_SLOT_IDS.map((id) => ({
|
|
95
|
+
id,
|
|
96
|
+
label: slotLabel(id),
|
|
97
|
+
value: configValue(config, id),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class FusionModelSelector implements Component {
|
|
102
|
+
private readonly choices: readonly FusionModelChoice[];
|
|
103
|
+
private readonly theme: Theme;
|
|
104
|
+
private readonly onSave: (config: FusionModelConfigV1) => Promise<void>;
|
|
105
|
+
private readonly onDone: (result: FusionModelSelectorResult) => void;
|
|
106
|
+
private readonly onRenderRequest: (() => void) | undefined;
|
|
107
|
+
private draft: FusionModelConfigV1;
|
|
108
|
+
private mode: SelectorMode = 'slots';
|
|
109
|
+
private selectedSlot = 0;
|
|
110
|
+
private selectedChoice = 0;
|
|
111
|
+
private search = '';
|
|
112
|
+
private saving = false;
|
|
113
|
+
private error: string | undefined;
|
|
114
|
+
private cachedWidth: number | undefined;
|
|
115
|
+
private cachedLines: string[] | undefined;
|
|
116
|
+
|
|
117
|
+
constructor(options: FusionModelSelectorOptions) {
|
|
118
|
+
this.draft = options.initialConfig;
|
|
119
|
+
this.choices = options.choices;
|
|
120
|
+
this.theme = options.theme;
|
|
121
|
+
this.onSave = options.onSave;
|
|
122
|
+
this.onDone = options.onDone;
|
|
123
|
+
this.onRenderRequest = options.onRenderRequest;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
render(width: number): string[] {
|
|
127
|
+
if (this.cachedWidth === width && this.cachedLines !== undefined) return this.cachedLines;
|
|
128
|
+
const lines = this.mode === 'slots' ? this.renderSlots(width) : this.renderChoices(width);
|
|
129
|
+
this.cachedWidth = width;
|
|
130
|
+
this.cachedLines = lines.map((line) => truncateToWidth(line, Math.max(1, width)));
|
|
131
|
+
return this.cachedLines;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
handleInput(data: string): void {
|
|
135
|
+
if (this.saving) return;
|
|
136
|
+
if (this.mode === 'slots') this.handleSlotInput(data);
|
|
137
|
+
else this.handleChoiceInput(data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
invalidate(): void {
|
|
141
|
+
this.cachedWidth = undefined;
|
|
142
|
+
this.cachedLines = undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private renderSlots(width: number): string[] {
|
|
146
|
+
const slots = slotViews(this.draft);
|
|
147
|
+
const lines = [
|
|
148
|
+
this.theme.fg('accent', this.theme.bold('Fusion models')),
|
|
149
|
+
this.theme.fg('dim', 'Select five slots. Duplicate models are allowed.'),
|
|
150
|
+
'',
|
|
151
|
+
];
|
|
152
|
+
for (const [index, slot] of slots.entries()) {
|
|
153
|
+
const marker = index === this.selectedSlot ? this.theme.fg('accent', '›') : ' ';
|
|
154
|
+
const value = this.describeSelection(slot.value);
|
|
155
|
+
lines.push(`${marker} ${slot.label.padEnd(12)} ${value}`);
|
|
156
|
+
}
|
|
157
|
+
lines.push('');
|
|
158
|
+
if (this.error !== undefined) lines.push(this.theme.fg('error', this.error));
|
|
159
|
+
const saveText = this.saving ? 'saving…' : 's save';
|
|
160
|
+
lines.push(this.theme.fg('dim', `↑↓ slot • enter choose • r reset • ${saveText} • esc cancel`));
|
|
161
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private renderChoices(width: number): string[] {
|
|
165
|
+
const selectedSlot = FUSION_MODEL_SLOT_IDS[this.selectedSlot];
|
|
166
|
+
const title =
|
|
167
|
+
selectedSlot === undefined ? 'Choose model' : `Choose model for ${slotLabel(selectedSlot)}`;
|
|
168
|
+
const filtered = this.filteredChoices();
|
|
169
|
+
const lines = [
|
|
170
|
+
this.theme.fg('accent', this.theme.bold(title)),
|
|
171
|
+
`${this.theme.fg('dim', 'Search:')} ${this.search || this.theme.fg('dim', '(type to filter)')}`,
|
|
172
|
+
'',
|
|
173
|
+
];
|
|
174
|
+
if (filtered.length === 0) {
|
|
175
|
+
lines.push(this.theme.fg('warning', 'No matching models.'));
|
|
176
|
+
} else {
|
|
177
|
+
const windowStart = Math.max(
|
|
178
|
+
0,
|
|
179
|
+
Math.min(this.selectedChoice - 6, Math.max(0, filtered.length - 12)),
|
|
180
|
+
);
|
|
181
|
+
const windowItems = filtered.slice(windowStart, windowStart + 12);
|
|
182
|
+
for (const [offset, choice] of windowItems.entries()) {
|
|
183
|
+
const index = windowStart + offset;
|
|
184
|
+
const marker = index === this.selectedChoice ? this.theme.fg('accent', '›') : ' ';
|
|
185
|
+
const label = choice.available ? choice.label : this.theme.fg('warning', choice.label);
|
|
186
|
+
const description = choice.description
|
|
187
|
+
? this.theme.fg('dim', ` — ${choice.description}`)
|
|
188
|
+
: '';
|
|
189
|
+
const stale = choice.available ? '' : this.theme.fg('warning', ' (unavailable)');
|
|
190
|
+
lines.push(`${marker} ${label}${stale}${description}`);
|
|
191
|
+
}
|
|
192
|
+
if (filtered.length > windowItems.length) {
|
|
193
|
+
lines.push(
|
|
194
|
+
this.theme.fg(
|
|
195
|
+
'dim',
|
|
196
|
+
`${String(windowStart + 1)}-${String(windowStart + windowItems.length)} of ${String(filtered.length)}`,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push(this.theme.fg('dim', '↑↓ choose • enter apply • backspace edit search • esc back'));
|
|
203
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private handleSlotInput(data: string): void {
|
|
207
|
+
if (matchesKey(data, Key.up)) {
|
|
208
|
+
this.selectedSlot = Math.max(0, this.selectedSlot - 1);
|
|
209
|
+
this.changed();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (matchesKey(data, Key.down)) {
|
|
213
|
+
this.selectedSlot = Math.min(FUSION_MODEL_SLOT_IDS.length - 1, this.selectedSlot + 1);
|
|
214
|
+
this.changed();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (matchesKey(data, Key.enter) || data === '\r') {
|
|
218
|
+
this.mode = 'models';
|
|
219
|
+
this.search = '';
|
|
220
|
+
this.selectedChoice = this.selectedChoiceForCurrentSlot();
|
|
221
|
+
this.changed();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (data === 'r') {
|
|
225
|
+
this.draft = defaultFusionModelConfig();
|
|
226
|
+
this.error = undefined;
|
|
227
|
+
this.changed();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (data === 's') {
|
|
231
|
+
void this.save();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (matchesKey(data, Key.escape) || data === 'q') {
|
|
235
|
+
this.onDone({ type: 'cancelled' });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private handleChoiceInput(data: string): void {
|
|
240
|
+
const filtered = this.filteredChoices();
|
|
241
|
+
if (matchesKey(data, Key.up)) {
|
|
242
|
+
this.selectedChoice = Math.max(0, this.selectedChoice - 1);
|
|
243
|
+
this.changed();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (matchesKey(data, Key.down)) {
|
|
247
|
+
this.selectedChoice = Math.min(Math.max(0, filtered.length - 1), this.selectedChoice + 1);
|
|
248
|
+
this.changed();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (matchesKey(data, Key.enter) || data === '\r') {
|
|
252
|
+
const choice = filtered[this.selectedChoice];
|
|
253
|
+
const slot = FUSION_MODEL_SLOT_IDS[this.selectedSlot];
|
|
254
|
+
if (choice !== undefined && slot !== undefined) {
|
|
255
|
+
this.draft = updateConfig(this.draft, slot, choice.value);
|
|
256
|
+
this.mode = 'slots';
|
|
257
|
+
this.error = undefined;
|
|
258
|
+
}
|
|
259
|
+
this.changed();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (matchesKey(data, Key.escape)) {
|
|
263
|
+
this.mode = 'slots';
|
|
264
|
+
this.changed();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (matchesKey(data, Key.backspace) || data === '\x7f') {
|
|
268
|
+
this.search = this.search.slice(0, -1);
|
|
269
|
+
this.selectedChoice = 0;
|
|
270
|
+
this.changed();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const char = printable(data);
|
|
274
|
+
if (char !== undefined) {
|
|
275
|
+
this.search += char;
|
|
276
|
+
this.selectedChoice = 0;
|
|
277
|
+
this.changed();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async save(): Promise<void> {
|
|
282
|
+
this.saving = true;
|
|
283
|
+
this.error = undefined;
|
|
284
|
+
this.changed();
|
|
285
|
+
try {
|
|
286
|
+
await this.onSave(this.draft);
|
|
287
|
+
this.onDone({ type: 'saved', config: this.draft });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
290
|
+
this.saving = false;
|
|
291
|
+
this.changed();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private changed(): void {
|
|
296
|
+
this.invalidate();
|
|
297
|
+
this.onRenderRequest?.();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private filteredChoices(): readonly FusionModelChoice[] {
|
|
301
|
+
return this.choices.filter((choice) => queryMatches(choice, this.search));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private selectedChoiceForCurrentSlot(): number {
|
|
305
|
+
const slot = FUSION_MODEL_SLOT_IDS[this.selectedSlot];
|
|
306
|
+
if (slot === undefined) return 0;
|
|
307
|
+
const current = configValue(this.draft, slot);
|
|
308
|
+
const filtered = this.filteredChoices();
|
|
309
|
+
const index = filtered.findIndex((choice) => choice.value === current);
|
|
310
|
+
return index >= 0 ? index : 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private describeSelection(value: FusionModelSelection): string {
|
|
314
|
+
if (value === CURRENT_MODEL_SELECTION) {
|
|
315
|
+
const current = this.choices.find((choice) => choice.value === CURRENT_MODEL_SELECTION);
|
|
316
|
+
return current === undefined ? CURRENT_MODEL_SELECTION : modelChoiceLine(current);
|
|
317
|
+
}
|
|
318
|
+
const found = this.choices.find((choice) => choice.value === value);
|
|
319
|
+
if (found === undefined) return `${value} (unavailable)`;
|
|
320
|
+
return modelChoiceLine(found);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { spawn as nodeSpawn, type SpawnOptions } from 'node:child_process';
|
|
2
|
+
import { win32 } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type WindowsKillPhase = 'terminate' | 'force';
|
|
5
|
+
|
|
6
|
+
export interface TaskkillOutcome {
|
|
7
|
+
readonly exitCode: number | null;
|
|
8
|
+
readonly signal: NodeJS.Signals | null;
|
|
9
|
+
readonly stdout: string;
|
|
10
|
+
readonly stderr: string;
|
|
11
|
+
readonly stdoutTruncated: boolean;
|
|
12
|
+
readonly stderrTruncated: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface TaskkillOutputStream {
|
|
16
|
+
on(event: 'data', listener: (data: Buffer | string) => void): unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface WindowsTaskkillChildProcess {
|
|
20
|
+
readonly stdout?: TaskkillOutputStream | null | undefined;
|
|
21
|
+
readonly stderr?: TaskkillOutputStream | null | undefined;
|
|
22
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
23
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
24
|
+
on(
|
|
25
|
+
event: 'close',
|
|
26
|
+
listener: (code: number | null, signal: NodeJS.Signals | null) => void,
|
|
27
|
+
): unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type WindowsTaskkillSpawn = (
|
|
31
|
+
command: string,
|
|
32
|
+
args: string[],
|
|
33
|
+
options: SpawnOptions,
|
|
34
|
+
) => WindowsTaskkillChildProcess;
|
|
35
|
+
|
|
36
|
+
export interface WindowsTaskkillOptions {
|
|
37
|
+
readonly spawn?: WindowsTaskkillSpawn;
|
|
38
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
39
|
+
readonly timeoutMs?: number;
|
|
40
|
+
readonly signal?: AbortSignal;
|
|
41
|
+
readonly maxCaptureBytes?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
45
|
+
const DEFAULT_MAX_CAPTURE_BYTES = 8 * 1024;
|
|
46
|
+
|
|
47
|
+
class BoundedCapture {
|
|
48
|
+
private readonly chunks: Buffer[] = [];
|
|
49
|
+
private capturedBytes = 0;
|
|
50
|
+
private truncated = false;
|
|
51
|
+
|
|
52
|
+
constructor(private readonly maxBytes: number) {}
|
|
53
|
+
|
|
54
|
+
append(data: Buffer | string): void {
|
|
55
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
56
|
+
if (buffer.length === 0) return;
|
|
57
|
+
const remaining = this.maxBytes - this.capturedBytes;
|
|
58
|
+
if (remaining > 0) {
|
|
59
|
+
const kept = buffer.length <= remaining ? buffer : buffer.subarray(0, remaining);
|
|
60
|
+
this.chunks.push(kept);
|
|
61
|
+
this.capturedBytes += kept.length;
|
|
62
|
+
}
|
|
63
|
+
if (buffer.length > Math.max(0, remaining)) this.truncated = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
text(): string {
|
|
67
|
+
return Buffer.concat(this.chunks, this.capturedBytes).toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isTruncated(): boolean {
|
|
71
|
+
return this.truncated;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function lookupEnv(env: NodeJS.ProcessEnv, name: string): string | undefined {
|
|
76
|
+
const direct = env[name];
|
|
77
|
+
if (direct !== undefined) return direct;
|
|
78
|
+
const lowerName = name.toLowerCase();
|
|
79
|
+
for (const key of Object.keys(env)) {
|
|
80
|
+
if (key.toLowerCase() === lowerName) return env[key];
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateWindowsRoot(raw: string, label: string): string {
|
|
86
|
+
const value = raw.trim();
|
|
87
|
+
if (value.length === 0) throw new Error(`${label} is empty; cannot resolve taskkill.exe`);
|
|
88
|
+
if (value.includes('\0')) throw new Error(`${label} contains a NUL byte; cannot resolve taskkill.exe`);
|
|
89
|
+
if (!win32.isAbsolute(value)) {
|
|
90
|
+
throw new Error(`${label} must be an absolute Windows path; cannot resolve taskkill.exe`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveTaskkillPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
96
|
+
const systemRoot = lookupEnv(env, 'SystemRoot');
|
|
97
|
+
if (systemRoot !== undefined) {
|
|
98
|
+
return win32.join(validateWindowsRoot(systemRoot, 'SystemRoot'), 'System32', 'taskkill.exe');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const windir = lookupEnv(env, 'WINDIR');
|
|
102
|
+
if (windir !== undefined) {
|
|
103
|
+
return win32.join(validateWindowsRoot(windir, 'WINDIR'), 'System32', 'taskkill.exe');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error('Cannot resolve taskkill.exe: SystemRoot is missing and WINDIR fallback is missing');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validatePid(pid: number): void {
|
|
110
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
111
|
+
throw new Error(`Invalid Windows taskkill pid ${String(pid)}; expected a positive safe integer`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validatePhase(phase: WindowsKillPhase): void {
|
|
116
|
+
if (phase !== 'terminate' && phase !== 'force') {
|
|
117
|
+
throw new Error(`Invalid Windows taskkill phase ${String(phase)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function positiveFiniteInteger(value: number | undefined, fallback: number, label: string): number {
|
|
122
|
+
const candidate = value ?? fallback;
|
|
123
|
+
if (!Number.isFinite(candidate) || candidate <= 0) {
|
|
124
|
+
throw new Error(`${label} must be a positive finite number`);
|
|
125
|
+
}
|
|
126
|
+
return Math.max(1, Math.floor(candidate));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function outcome(
|
|
130
|
+
exitCode: number | null,
|
|
131
|
+
signal: NodeJS.Signals | null,
|
|
132
|
+
stdout: BoundedCapture,
|
|
133
|
+
stderr: BoundedCapture,
|
|
134
|
+
): TaskkillOutcome {
|
|
135
|
+
return {
|
|
136
|
+
exitCode,
|
|
137
|
+
signal,
|
|
138
|
+
stdout: stdout.text(),
|
|
139
|
+
stderr: stderr.text(),
|
|
140
|
+
stdoutTruncated: stdout.isTruncated(),
|
|
141
|
+
stderrTruncated: stderr.isTruncated(),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function defaultSpawn(command: string, args: string[], options: SpawnOptions): WindowsTaskkillChildProcess {
|
|
146
|
+
return nodeSpawn(command, args, options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function runWindowsTaskkill(
|
|
150
|
+
pid: number,
|
|
151
|
+
phase: WindowsKillPhase,
|
|
152
|
+
options: WindowsTaskkillOptions = {},
|
|
153
|
+
): Promise<TaskkillOutcome> {
|
|
154
|
+
validatePid(pid);
|
|
155
|
+
validatePhase(phase);
|
|
156
|
+
const env = options.env ?? process.env;
|
|
157
|
+
const taskkill = resolveTaskkillPath(env);
|
|
158
|
+
const timeoutMs = positiveFiniteInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS, 'timeoutMs');
|
|
159
|
+
const maxCaptureBytes = positiveFiniteInteger(
|
|
160
|
+
options.maxCaptureBytes,
|
|
161
|
+
DEFAULT_MAX_CAPTURE_BYTES,
|
|
162
|
+
'maxCaptureBytes',
|
|
163
|
+
);
|
|
164
|
+
const spawn = options.spawn ?? defaultSpawn;
|
|
165
|
+
const abortSignal = options.signal;
|
|
166
|
+
|
|
167
|
+
const args = ['/PID', String(pid), '/T'];
|
|
168
|
+
if (phase === 'force') args.push('/F');
|
|
169
|
+
|
|
170
|
+
if (abortSignal?.aborted) {
|
|
171
|
+
const stdout = new BoundedCapture(maxCaptureBytes);
|
|
172
|
+
const stderr = new BoundedCapture(maxCaptureBytes);
|
|
173
|
+
stderr.append('Windows taskkill was aborted before launch');
|
|
174
|
+
return Promise.resolve(outcome(null, null, stdout, stderr));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return new Promise<TaskkillOutcome>((resolve) => {
|
|
178
|
+
const stdout = new BoundedCapture(maxCaptureBytes);
|
|
179
|
+
const stderr = new BoundedCapture(maxCaptureBytes);
|
|
180
|
+
let settled = false;
|
|
181
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
182
|
+
let child: WindowsTaskkillChildProcess | undefined;
|
|
183
|
+
let abortListener: (() => void) | undefined;
|
|
184
|
+
|
|
185
|
+
const settle = (result: TaskkillOutcome): void => {
|
|
186
|
+
if (settled) return;
|
|
187
|
+
settled = true;
|
|
188
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
189
|
+
if (abortSignal !== undefined && abortListener !== undefined) {
|
|
190
|
+
abortSignal.removeEventListener('abort', abortListener);
|
|
191
|
+
}
|
|
192
|
+
resolve(result);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const stopHelper = (reason: string): void => {
|
|
196
|
+
stderr.append(reason);
|
|
197
|
+
if (child !== undefined) {
|
|
198
|
+
try {
|
|
199
|
+
child.kill('SIGKILL');
|
|
200
|
+
} catch (error) {
|
|
201
|
+
stderr.append(`; helper kill failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
settle(outcome(null, null, stdout, stderr));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const spawnOptions: SpawnOptions = {
|
|
208
|
+
env,
|
|
209
|
+
shell: false,
|
|
210
|
+
windowsVerbatimArguments: false,
|
|
211
|
+
windowsHide: true,
|
|
212
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
child = spawn(taskkill, args, spawnOptions);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
stderr.append(`Windows taskkill spawn failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
+
settle(outcome(null, null, stdout, stderr));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
child.stdout?.on('data', (data) => {
|
|
224
|
+
stdout.append(data);
|
|
225
|
+
});
|
|
226
|
+
child.stderr?.on('data', (data) => {
|
|
227
|
+
stderr.append(data);
|
|
228
|
+
});
|
|
229
|
+
child.on('error', (error) => {
|
|
230
|
+
stderr.append(`Windows taskkill spawn error: ${error.message}`);
|
|
231
|
+
settle(outcome(null, null, stdout, stderr));
|
|
232
|
+
});
|
|
233
|
+
child.on('close', (code, signal) => {
|
|
234
|
+
settle(outcome(code, signal, stdout, stderr));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
abortListener = () => {
|
|
238
|
+
stopHelper('Windows taskkill was aborted');
|
|
239
|
+
};
|
|
240
|
+
abortSignal?.addEventListener('abort', abortListener, { once: true });
|
|
241
|
+
|
|
242
|
+
// This timeout is the settlement guarantee for a taskkill helper that never
|
|
243
|
+
// exits. It must keep the event loop alive: an unref'd timer lets the loop
|
|
244
|
+
// drain first and leaves this promise pending forever. `settle()` always
|
|
245
|
+
// clears it, so keeping it referenced cannot leak.
|
|
246
|
+
timeout = setTimeout(() => {
|
|
247
|
+
stopHelper(`Windows taskkill timed out after ${String(timeoutMs)}ms`);
|
|
248
|
+
}, timeoutMs);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delegate
|
|
3
|
+
description: Lightweight subagent that inherits the parent model with no default reads
|
|
4
|
+
systemPromptMode: append
|
|
5
|
+
inheritProjectContext: true
|
|
6
|
+
tools: read, grep, find, ls, bash, edit, write, contact_supervisor
|
|
7
|
+
inheritSkills: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
You are a delegated agent. Execute the assigned task using the provided tools. Be direct, efficient, and keep the response focused on the requested work.
|
|
11
|
+
|
|
12
|
+
The builtin delegate uses a strict tool allowlist and does not inherit ambient extension tools from the parent session. To use an extension tool, configure a custom agent with the tool name explicitly listed in `tools` and load its provider through `extensions` or `subagentOnlyExtensions`.
|
|
13
|
+
|
|
14
|
+
If runtime bridge instructions identify a safe supervisor target and you are blocked or need a decision, use `contact_supervisor` with `reason: "need_decision"` and stay alive for the reply. Use `reason: "progress_update"` only for meaningful progress or unexpected discoveries that change the plan. Do not send routine completion handoffs; return normally when no coordination is needed.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oracle
|
|
3
|
+
aliases: advisor
|
|
4
|
+
description: High-context decision-consistency oracle that protects inherited state and prevents drift
|
|
5
|
+
tools: read, grep, find, ls, bash
|
|
6
|
+
thinking: high
|
|
7
|
+
systemPromptMode: replace
|
|
8
|
+
inheritProjectContext: true
|
|
9
|
+
inheritSkills: false
|
|
10
|
+
defaultContext: fork
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
You are the oracle: a high-context decision-consistency subagent.
|
|
14
|
+
|
|
15
|
+
Your primary job is to prevent the main agent from making hidden, conflicting, or inconsistent decisions by treating the inherited forked context as the authoritative contract. You are not the primary executor. You do not silently become a second decision-maker.
|
|
16
|
+
|
|
17
|
+
Before you do anything else, reconstruct the key inherited decisions, constraints, and open questions from the forked conversation, codebase state, and task. Those decisions form your baseline contract. Preserve them unless there is strong evidence they should be overturned.
|
|
18
|
+
|
|
19
|
+
Match search scope to the question. For runtime behavior, begin with specific source symbols, types, methods, and paths. For product, plan, policy, or decision drift, treat supplied documents and inherited context as first-class evidence. If source conflicts with docs about runtime behavior, trust source and report the conflict.
|
|
20
|
+
|
|
21
|
+
If the task asks about asking or consulting the oracle, or asks to ask, consult, discuss with, or come to agreement with the oracle about a plan, design, or architecture decision, treat it as a short live consultation unless the parent explicitly requests a one-shot report. In a first response, return the strongest challenge point or focused follow-up question when a material tradeoff remains, so the parent can resume this same session for one targeted round. A one-shot response remains suitable for an explicit one-shot request, a trivial question, or a fully settled first answer. When runtime bridge instructions provide `contact_supervisor`, ask one focused question or challenge if a material unknown, contradiction, or unapproved decision would make a final recommendation guessy. If no supervisor channel is available, return the best recommendation and name the decision that still needs the main agent.
|
|
22
|
+
|
|
23
|
+
If you need clarification from the main agent and bridge instructions provide `contact_supervisor`, use it with `reason: "need_decision"` and wait for the reply. Use `reason: "progress_update"` only for concise updates when blocked, explicitly asked for progress, or when a recommendation or concern would benefit from immediate discussion. Keep coordination traffic tight and purposeful. Do not narrate your whole review through `contact_supervisor`.
|
|
24
|
+
|
|
25
|
+
Do not send routine completion handoffs. If no coordination is needed, or after needed coordination is answered, return the final oracle recommendation normally. If `contact_supervisor` is unavailable, return the best recommendation and name the decision that still needs the main agent. Use generic `intercom` only when an external intercom provider explicitly supplies that tool and the task identifies a safe target.
|
|
26
|
+
|
|
27
|
+
Core responsibilities:
|
|
28
|
+
- reconstruct inherited decisions, constraints, and open questions from the context
|
|
29
|
+
- identify drift between the current trajectory and those inherited decisions
|
|
30
|
+
- surface contradictions and hidden assumptions the main agent may be missing
|
|
31
|
+
- call out when a proposed move conflicts with an earlier decision or constraint
|
|
32
|
+
- protect consistency over novelty; prefer the path that honors existing decisions unless the context clearly supports a pivot
|
|
33
|
+
- when you do recommend a pivot, explain exactly which prior assumption or decision should be revised and why
|
|
34
|
+
- exploit your clean forked context to spot things the main agent may have missed due to context rot, accumulated reasoning, or errors in the original instruction
|
|
35
|
+
- look beyond the explicit question and suggest guidance based on the overall agent trajectory, even when not directly asked
|
|
36
|
+
|
|
37
|
+
What you do not do by default:
|
|
38
|
+
- do not edit files or write code
|
|
39
|
+
- do not propose additional parallel decision-makers or new subagent trees unless explicitly asked
|
|
40
|
+
- do not assume a `worker` implementation handoff is the default outcome
|
|
41
|
+
- do not propose broad pivots unless the context clearly supports them
|
|
42
|
+
- do not continue the user conversation directly
|
|
43
|
+
|
|
44
|
+
Working rules:
|
|
45
|
+
- Use `bash` only for inspection, verification, or read-only analysis.
|
|
46
|
+
- If information is missing and it matters, ask the main agent with `contact_supervisor` and `reason: "need_decision"` when bridge instructions provide that tool. If no supervisor channel is available, return the best recommendation and name the unresolved decision instead of guessing.
|
|
47
|
+
- If the answer depends on a decision the main agent has not made yet, stop and ask with `contact_supervisor` when bridge instructions provide that tool. If no supervisor channel is available, mark the decision as still needed in the final recommendation.
|
|
48
|
+
- When bridge instructions are present, send concise coordination messages only when a recommendation, concern, or question would benefit from immediate discussion instead of waiting silently until the final return.
|
|
49
|
+
- Prefer narrow, specific corrections to the current path over rewriting the whole plan.
|
|
50
|
+
|
|
51
|
+
Your output should follow this shape. If no executor handoff is warranted, say so plainly.
|
|
52
|
+
|
|
53
|
+
Inherited decisions:
|
|
54
|
+
- the key decisions, constraints, and assumptions already in play
|
|
55
|
+
|
|
56
|
+
Diagnosis:
|
|
57
|
+
- what is actually going on
|
|
58
|
+
- what the main agent may be missing
|
|
59
|
+
|
|
60
|
+
Drift / contradiction check:
|
|
61
|
+
- where the current trajectory conflicts with inherited decisions or constraints
|
|
62
|
+
- what assumptions have quietly changed
|
|
63
|
+
|
|
64
|
+
Recommendation:
|
|
65
|
+
- the best next move
|
|
66
|
+
- why it is the best move
|
|
67
|
+
- if recommending a pivot, which inherited decision is being revised and why
|
|
68
|
+
|
|
69
|
+
Risks:
|
|
70
|
+
- what could still go wrong
|
|
71
|
+
- what assumptions remain uncertain
|
|
72
|
+
|
|
73
|
+
Need from main agent:
|
|
74
|
+
- specific question or decision required before continuing, if any
|
|
75
|
+
|
|
76
|
+
Suggested execution prompt:
|
|
77
|
+
- a concrete prompt for `worker`, only if an implementation handoff is actually warranted
|
|
78
|
+
- if no handoff is warranted, say so explicitly
|