pi-crew 0.9.26 → 0.9.28
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/CHANGELOG.md +102 -0
- package/NOTICE.md +14 -0
- package/assets/crew-vibes.ttf +0 -0
- package/assets/runner-spritesheet.png +0 -0
- package/dist/build-meta.json +245 -48
- package/dist/index.mjs +1611 -243
- package/dist/index.mjs.map +4 -4
- package/docs/perf/optimization-plan-2026-07-verified.md +396 -0
- package/package.json +9 -2
- package/scripts/bench-check.mjs +96 -0
- package/scripts/bench-cold-start.mjs +216 -0
- package/scripts/build-bundle.mjs +84 -0
- package/scripts/build-crew-vibes-font.py +328 -0
- package/scripts/check-all-skills.ts +294 -0
- package/scripts/check-bundle-staleness.mjs +97 -0
- package/scripts/check-conflict-markers.mjs +91 -0
- package/scripts/check-lazy-imports.mjs +28 -0
- package/scripts/install-crew-vibes-font.mjs +91 -0
- package/scripts/postinstall.mjs +47 -0
- package/scripts/profile-startup.mjs +125 -0
- package/scripts/release-smoke.mjs +74 -0
- package/scripts/run-bench.mjs +47 -0
- package/scripts/run-real-chain.ts +41 -0
- package/scripts/test-issue-29-crash.ts +111 -0
- package/scripts/test-issue-29-e2e.ts +183 -0
- package/scripts/test-issue-29-real-runtime.ts +330 -0
- package/scripts/test-issue-29-real-tasks.ts +387 -0
- package/scripts/test-issue-29-team-tool.ts +105 -0
- package/scripts/test-runner.mjs +74 -0
- package/scripts/verify-flicker-fix.ts +109 -0
- package/scripts/verify-skill.ts +550 -0
- package/scripts/watch-bundle.mjs +259 -0
- package/scripts/watchdog-harness.ts +119 -0
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/discover-agents.ts +10 -1
- package/src/config/defaults.ts +6 -0
- package/src/extension/crew-vibes/cat-frames.ts +18 -0
- package/src/extension/crew-vibes/config.ts +195 -0
- package/src/extension/crew-vibes/figures.ts +94 -0
- package/src/extension/crew-vibes/font-detect.ts +58 -0
- package/src/extension/crew-vibes/index.ts +385 -0
- package/src/extension/crew-vibes/provider-usage.ts +396 -0
- package/src/extension/crew-vibes/render.ts +206 -0
- package/src/extension/crew-vibes/speed.ts +286 -0
- package/src/extension/knowledge-injection.ts +12 -3
- package/src/extension/management.ts +23 -3
- package/src/extension/register.ts +7 -0
- package/src/extension/team-tool.ts +11 -0
- package/src/prompt/prompt-runtime.ts +65 -0
- package/src/runtime/child-pi.ts +123 -10
- package/src/runtime/crew-agent-records.ts +10 -3
- package/src/runtime/pi-args.ts +2 -0
- package/src/runtime/retry-executor.ts +4 -1
- package/src/runtime/task-runner/state-helpers.ts +9 -30
- package/src/runtime/task-runner.ts +1 -0
- package/src/runtime/team-runner.ts +5 -3
- package/src/state/atomic-write.ts +153 -49
- package/src/state/event-log.ts +16 -10
- package/src/state/locks.ts +7 -8
- package/src/state/mailbox.ts +15 -4
- package/src/state/state-store.ts +39 -10
- package/src/teams/discover-teams.ts +56 -1
- package/src/utils/safe-paths.ts +2 -1
- package/src/workflows/discover-workflows.ts +72 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import type { SpeedConfig } from "./config.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token-speed engine ported from pi-speeed (MIT, attribution in NOTICE.md).
|
|
5
|
+
* Tracks output tok/s with a sliding window and suppresses unreliable
|
|
6
|
+
* burst-only readings. No stats/font handling — just the live + session avg.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type TokenEvent = { time: number; tokens: number };
|
|
10
|
+
|
|
11
|
+
const COMPACTION_THRESHOLD = 5000;
|
|
12
|
+
|
|
13
|
+
export type EngineConfig = Pick<SpeedConfig, "slidingWindowMs" | "minReliableDurationMs" | "maxDisplayTokS">;
|
|
14
|
+
|
|
15
|
+
export function estimateTokensFromDelta(text: string): number {
|
|
16
|
+
if (!text) return 0;
|
|
17
|
+
const matches = text.match(/\w+|[^\s\w]/g);
|
|
18
|
+
return matches ? matches.length : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class TokenSpeedEngine {
|
|
22
|
+
private _isStreaming = false;
|
|
23
|
+
private _tokenCount = 0;
|
|
24
|
+
private _startTime = 0;
|
|
25
|
+
private _endTime = 0;
|
|
26
|
+
private _events: TokenEvent[] = [];
|
|
27
|
+
private _windowStartIndex = 0;
|
|
28
|
+
private _lastStableTokS = 0;
|
|
29
|
+
private _lastUsageOutput = 0;
|
|
30
|
+
private _config: EngineConfig;
|
|
31
|
+
|
|
32
|
+
constructor(config: EngineConfig) {
|
|
33
|
+
this._config = config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
updateConfig(config: EngineConfig): void {
|
|
37
|
+
this._config = config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get isStreaming(): boolean {
|
|
41
|
+
return this._isStreaming;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get tokenCount(): number {
|
|
45
|
+
return this._tokenCount;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get elapsedMs(): number {
|
|
49
|
+
if (this._startTime === 0) return 0;
|
|
50
|
+
return this._isStreaming ? Date.now() - this._startTime : this._endTime - this._startTime;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get avgTokS(): number {
|
|
54
|
+
const elapsedSec = this.elapsedMs / 1000;
|
|
55
|
+
if (elapsedSec <= 0) return 0;
|
|
56
|
+
return this._tokenCount / elapsedSec;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
sanitizeTokS(value: number | null, durationMs = this.elapsedMs): number | null {
|
|
60
|
+
if (value === null || !Number.isFinite(value) || value <= 0) return null;
|
|
61
|
+
if (durationMs < this._config.minReliableDurationMs) return null;
|
|
62
|
+
if (value > this._config.maxDisplayTokS) return null;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get tokS(): number {
|
|
67
|
+
const candidate = this.rawTokS;
|
|
68
|
+
const stable = this.sanitizeTokS(candidate);
|
|
69
|
+
if (stable !== null) this._lastStableTokS = stable;
|
|
70
|
+
return this._lastStableTokS;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get rawTokS(): number {
|
|
74
|
+
if (this.elapsedMs < this._config.slidingWindowMs) return this.avgTokS;
|
|
75
|
+
if (!this._isStreaming) return this.avgTokS;
|
|
76
|
+
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const windowStart = now - this._config.slidingWindowMs;
|
|
79
|
+
|
|
80
|
+
while (this._windowStartIndex < this._events.length && this._events[this._windowStartIndex].time < windowStart) {
|
|
81
|
+
this._windowStartIndex++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (this._windowStartIndex >= this._events.length) return this.avgTokS;
|
|
85
|
+
|
|
86
|
+
let windowTokenCount = 0;
|
|
87
|
+
for (let i = this._windowStartIndex; i < this._events.length; i++) {
|
|
88
|
+
windowTokenCount += this._events[i].tokens;
|
|
89
|
+
}
|
|
90
|
+
if (windowTokenCount === 0) return this.avgTokS;
|
|
91
|
+
|
|
92
|
+
const windowDuration = (now - this._events[this._windowStartIndex].time) / 1000;
|
|
93
|
+
if (windowDuration <= 0) return 0;
|
|
94
|
+
|
|
95
|
+
return windowTokenCount / windowDuration;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
start(): void {
|
|
99
|
+
this._tokenCount = 0;
|
|
100
|
+
this._isStreaming = true;
|
|
101
|
+
this._startTime = Date.now();
|
|
102
|
+
this._endTime = this._startTime;
|
|
103
|
+
this._events = [];
|
|
104
|
+
this._windowStartIndex = 0;
|
|
105
|
+
this._lastStableTokS = 0;
|
|
106
|
+
this._lastUsageOutput = 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stop(): void {
|
|
110
|
+
this._isStreaming = false;
|
|
111
|
+
this._endTime = Date.now();
|
|
112
|
+
this._events = [];
|
|
113
|
+
this._windowStartIndex = 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
recordDelta(delta: string, usageOutput?: number): void {
|
|
117
|
+
if (!this._isStreaming) return;
|
|
118
|
+
if (usageOutput !== undefined && usageOutput > 0) {
|
|
119
|
+
// usageOutput may be cumulative across message_update events. Track the
|
|
120
|
+
// delta to avoid inflating _tokenCount by N× the true total.
|
|
121
|
+
const increment = usageOutput - this._lastUsageOutput;
|
|
122
|
+
this._lastUsageOutput = usageOutput;
|
|
123
|
+
this.recordTokens(Math.max(0, increment));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.recordTokens(estimateTokensFromDelta(delta));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
reconcileTotal(tokens: number): void {
|
|
130
|
+
if (tokens > 0) this._tokenCount = tokens;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
recordTokens(tokens: number): void {
|
|
134
|
+
if (!this._isStreaming || tokens <= 0) return;
|
|
135
|
+
this._tokenCount += tokens;
|
|
136
|
+
this._events.push({ time: Date.now(), tokens });
|
|
137
|
+
if (this._windowStartIndex >= COMPACTION_THRESHOLD) this.compact();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private compact(): void {
|
|
141
|
+
if (this._windowStartIndex === 0) return;
|
|
142
|
+
this._events = this._events.slice(this._windowStartIndex);
|
|
143
|
+
this._windowStartIndex = 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type CompletedMessageSpeed = {
|
|
148
|
+
outputTokens: number;
|
|
149
|
+
durationMs: number;
|
|
150
|
+
tokS: number | null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function isSuccessfulStop(stopReason: string | undefined): boolean {
|
|
154
|
+
return stopReason !== "error" && stopReason !== "aborted";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class SpeedTracker {
|
|
158
|
+
private readonly engine: TokenSpeedEngine;
|
|
159
|
+
private lastStableTokS: number | null = null;
|
|
160
|
+
private sessionOutputTokens = 0;
|
|
161
|
+
private sessionDurationMs = 0;
|
|
162
|
+
|
|
163
|
+
constructor(config: SpeedConfig) {
|
|
164
|
+
this.engine = new TokenSpeedEngine(config);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
updateConfig(config: SpeedConfig): void {
|
|
168
|
+
this.engine.updateConfig(config);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get isStreaming(): boolean {
|
|
172
|
+
return this.engine.isStreaming;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get lastTokS(): number | null {
|
|
176
|
+
return this.lastStableTokS;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
resetSession(): void {
|
|
180
|
+
this.sessionOutputTokens = 0;
|
|
181
|
+
this.sessionDurationMs = 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
startMessage(): void {
|
|
185
|
+
this.engine.start();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
recordDelta(delta: string, usageOutput?: number): void {
|
|
189
|
+
this.engine.recordDelta(delta, usageOutput);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
stopMessage(): void {
|
|
193
|
+
if (this.engine.isStreaming) this.engine.stop();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
liveTokS(): number | null {
|
|
197
|
+
const speed = this.engine.tokS;
|
|
198
|
+
return speed > 0 ? speed : this.lastStableTokS;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
sessionAvgTokS(): number | null {
|
|
202
|
+
return this.sessionDurationMs > 0 ? this.sessionOutputTokens / (this.sessionDurationMs / 1000) : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
finishMessage(outputTokens: number, stopReason: string | undefined): CompletedMessageSpeed | null {
|
|
206
|
+
if (!this.engine.isStreaming) return null;
|
|
207
|
+
|
|
208
|
+
this.engine.reconcileTotal(outputTokens);
|
|
209
|
+
const durationMs = this.engine.elapsedMs;
|
|
210
|
+
const tokens = this.engine.tokenCount;
|
|
211
|
+
const rawAvgTokS = durationMs > 0 ? tokens / (durationMs / 1000) : null;
|
|
212
|
+
const tokS = this.engine.sanitizeTokS(rawAvgTokS, durationMs);
|
|
213
|
+
this.lastStableTokS = tokS;
|
|
214
|
+
this.engine.stop();
|
|
215
|
+
|
|
216
|
+
if (tokS !== null && isSuccessfulStop(stopReason)) {
|
|
217
|
+
this.sessionOutputTokens += tokens;
|
|
218
|
+
this.sessionDurationMs += durationMs;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { outputTokens: tokens, durationMs, tokS };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class SpeedAnimator {
|
|
226
|
+
private from: number | null = null;
|
|
227
|
+
private target: number | null = null;
|
|
228
|
+
private startedAt = 0;
|
|
229
|
+
private durationMs: number;
|
|
230
|
+
|
|
231
|
+
constructor(durationMs: number) {
|
|
232
|
+
this.durationMs = durationMs;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
updateDuration(durationMs: number): void {
|
|
236
|
+
this.durationMs = durationMs;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
reset(value: number | null = null, now = Date.now()): void {
|
|
240
|
+
this.from = value;
|
|
241
|
+
this.target = value;
|
|
242
|
+
this.startedAt = now;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
setTarget(target: number | null, now = Date.now()): number | null {
|
|
246
|
+
if (target === null) {
|
|
247
|
+
this.reset(null, now);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const current = this.value(now);
|
|
252
|
+
if (current === null) {
|
|
253
|
+
this.reset(target, now);
|
|
254
|
+
return target;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (this.target !== null && Math.abs(target - this.target) < 0.05) return current;
|
|
258
|
+
|
|
259
|
+
this.from = current;
|
|
260
|
+
this.target = target;
|
|
261
|
+
this.startedAt = now;
|
|
262
|
+
return current;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
value(now = Date.now()): number | null {
|
|
266
|
+
if (this.target === null) return null;
|
|
267
|
+
if (this.from === null || this.durationMs <= 0) return this.target;
|
|
268
|
+
|
|
269
|
+
const progress = Math.max(0, Math.min(1, (now - this.startedAt) / this.durationMs));
|
|
270
|
+
if (progress >= 1) {
|
|
271
|
+
this.from = this.target;
|
|
272
|
+
return this.target;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return this.from + (this.target - this.from) * progress;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
isAnimating(now = Date.now()): boolean {
|
|
279
|
+
return (
|
|
280
|
+
this.target !== null &&
|
|
281
|
+
this.from !== null &&
|
|
282
|
+
Math.abs(this.target - this.from) >= 0.05 &&
|
|
283
|
+
now - this.startedAt < this.durationMs
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import * as fs from "node:fs";
|
|
24
24
|
import * as path from "node:path";
|
|
25
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
25
26
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
26
27
|
import type { BeforeAgentStartEvent, ExtensionAPI } from "./pi-api.ts";
|
|
27
28
|
|
|
@@ -259,6 +260,13 @@ export function readKnowledge(cwd: string, query?: KnowledgeQuery): string {
|
|
|
259
260
|
knowledgeCache.delete(p);
|
|
260
261
|
return "";
|
|
261
262
|
}
|
|
263
|
+
// Security: reject symlinks to prevent information disclosure.
|
|
264
|
+
// An attacker could replace knowledge.md with a symlink to /etc/shadow
|
|
265
|
+
// or other sensitive files.
|
|
266
|
+
if (stat.isSymbolicLink) {
|
|
267
|
+
logInternalError("knowledge-injection.symlink", new Error(`Refusing to read symlink: ${p}`));
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
262
270
|
// P5 (Round 15): mtime+size cache. readKnowledge fires on every agent
|
|
263
271
|
// start (main session + every worker), re-reading the file each time.
|
|
264
272
|
// For a run with N workers this is N redundant readFileSync of the same
|
|
@@ -322,10 +330,11 @@ export function readKnowledge(cwd: string, query?: KnowledgeQuery): string {
|
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
/** Stat helper returning undefined on error (file missing, perms, etc.). */
|
|
325
|
-
function tryStat(p: string): { mtimeMs: number; size: number } | undefined {
|
|
333
|
+
function tryStat(p: string): { mtimeMs: number; size: number; isSymbolicLink: boolean } | undefined {
|
|
326
334
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
335
|
+
// Use lstatSync to detect symlinks (statSync follows them).
|
|
336
|
+
const s = fs.lstatSync(p);
|
|
337
|
+
return { mtimeMs: s.mtimeMs, size: s.size, isSymbolicLink: s.isSymbolicLink() };
|
|
329
338
|
} catch {
|
|
330
339
|
return undefined;
|
|
331
340
|
}
|
|
@@ -3,21 +3,32 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig, ResourceSource, RoutingMetadata } from "../agents/agent-config.ts";
|
|
5
5
|
import { serializeAgent } from "../agents/agent-serializer.ts";
|
|
6
|
-
import { discoverAgents } from "../agents/discover-agents.ts";
|
|
6
|
+
import { discoverAgents, invalidateAgentDiscoveryCache } from "../agents/discover-agents.ts";
|
|
7
7
|
import type { PiTeamsConfig } from "../config/config.ts";
|
|
8
8
|
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
9
|
-
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
9
|
+
import { allTeams, discoverTeams, invalidateTeamDiscoveryCache } from "../teams/discover-teams.ts";
|
|
10
10
|
import type { TeamConfig, TeamRole } from "../teams/team-config.ts";
|
|
11
11
|
import { serializeTeam } from "../teams/team-serializer.ts";
|
|
12
12
|
import { hasOwn, parseConfigObject, requireString, sanitizeName } from "../utils/names.ts";
|
|
13
13
|
import { projectCrewRoot, userPiRoot } from "../utils/paths.ts";
|
|
14
|
-
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
14
|
+
import { allWorkflows, discoverWorkflows, invalidateWorkflowDiscoveryCache } from "../workflows/discover-workflows.ts";
|
|
15
15
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
16
16
|
import { serializeWorkflow } from "../workflows/workflow-serializer.ts";
|
|
17
17
|
import { enforceDestructiveIntent } from "./team-tool/intent-policy.ts";
|
|
18
18
|
import type { TeamToolDetails } from "./team-tool-types.ts";
|
|
19
19
|
import { type PiTeamsToolResult, toolResult } from "./tool-result.ts";
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Invalidate all discovery caches that may have read the on-disk resource
|
|
23
|
+
* file we are about to (or just did) write. Called once per create/update/delete
|
|
24
|
+
* to keep the 5 s TTL caches in sync without forcing a per-render full rescan.
|
|
25
|
+
*/
|
|
26
|
+
function invalidateResourceCaches(): void {
|
|
27
|
+
invalidateAgentDiscoveryCache();
|
|
28
|
+
invalidateTeamDiscoveryCache();
|
|
29
|
+
invalidateWorkflowDiscoveryCache();
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
interface ManagementContext {
|
|
22
33
|
cwd: string;
|
|
23
34
|
config?: PiTeamsConfig;
|
|
@@ -385,6 +396,9 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
385
396
|
true,
|
|
386
397
|
);
|
|
387
398
|
}
|
|
399
|
+
// F17/F15: bust the discovery caches so future powerbar/scheduler reads see
|
|
400
|
+
// the new file immediately (instead of after the 5 s TTL expires).
|
|
401
|
+
invalidateResourceCaches();
|
|
388
402
|
return result(`Created ${params.resource} '${name}' at ${filePath}.`);
|
|
389
403
|
}
|
|
390
404
|
|
|
@@ -507,9 +521,13 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
507
521
|
true,
|
|
508
522
|
);
|
|
509
523
|
}
|
|
524
|
+
// F17/F15: bust the discovery caches so the renamed/updated resource is
|
|
525
|
+
// visible immediately. updateReferencesForRename may have rewritten peer
|
|
526
|
+
// files too, so we invalidate AFTER that pass completes.
|
|
510
527
|
const updatedRefs = params.updateReferences
|
|
511
528
|
? updateReferencesForRename(ctx, params.resource!, current.name, nextName, source, false)
|
|
512
529
|
: [];
|
|
530
|
+
invalidateResourceCaches();
|
|
513
531
|
return result(
|
|
514
532
|
[
|
|
515
533
|
`Updated ${params.resource} at ${nextPath}. Backup: ${backupPath}.`,
|
|
@@ -546,5 +564,7 @@ export function handleDelete(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
546
564
|
true,
|
|
547
565
|
);
|
|
548
566
|
}
|
|
567
|
+
// F17/F15: bust the discovery caches so the deletion is visible immediately.
|
|
568
|
+
invalidateResourceCaches();
|
|
549
569
|
return result(`Deleted ${params.resource} at ${resolved.resource!.filePath}. Backup: ${backupPath}.`);
|
|
550
570
|
}
|
|
@@ -64,6 +64,7 @@ import { registerCrewAutocomplete } from "./crew-autocomplete.ts";
|
|
|
64
64
|
import { registerCleanupHandler } from "./crew-cleanup.ts";
|
|
65
65
|
import { registerCrewInputRouter } from "./crew-input-router.ts";
|
|
66
66
|
import { registerCrewShortcuts } from "./crew-shortcuts.ts";
|
|
67
|
+
import { registerCrewVibes } from "./crew-vibes/index.ts";
|
|
67
68
|
import { type PiCrewRpcHandle, registerPiCrewRpc } from "./cross-extension-rpc.ts";
|
|
68
69
|
import { registerKnowledgeInjection } from "./knowledge-injection.ts";
|
|
69
70
|
import { registerCrewMessageRenderers } from "./message-renderers.ts";
|
|
@@ -1545,4 +1546,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1545
1546
|
registerContextStatusInjection(pi, {
|
|
1546
1547
|
enabled: loadConfig(process.cwd()).config.reliability?.ambientStatusInjection !== false,
|
|
1547
1548
|
});
|
|
1549
|
+
|
|
1550
|
+
try {
|
|
1551
|
+
registerCrewVibes(pi);
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
console.warn("[pi-crew] crew-vibes initialization failed:", err instanceof Error ? err.message : err);
|
|
1554
|
+
}
|
|
1548
1555
|
}
|
|
@@ -498,6 +498,17 @@ export function handleSteer(params: TeamToolParamsValue, ctx: TeamContext): PiTe
|
|
|
498
498
|
}
|
|
499
499
|
task.pendingSteers.push(message);
|
|
500
500
|
saveRunTasks(loaded.manifest, loaded.tasks);
|
|
501
|
+
// Real-time steer delivery: write to steering file so child can read immediately
|
|
502
|
+
try {
|
|
503
|
+
const steeringDir = `${loaded.manifest.artifactsRoot}/steering`;
|
|
504
|
+
fs.mkdirSync(steeringDir, { recursive: true });
|
|
505
|
+
fs.appendFileSync(
|
|
506
|
+
`${steeringDir}/${taskId}.jsonl`,
|
|
507
|
+
JSON.stringify({ type: "steer", message, ts: new Date().toISOString() }) + "\n",
|
|
508
|
+
);
|
|
509
|
+
} catch {
|
|
510
|
+
// Best-effort: file write failure doesn't block the steer from pending array
|
|
511
|
+
}
|
|
501
512
|
appendEvent(loaded.manifest.eventsPath, {
|
|
502
513
|
type: "task.steer_queued",
|
|
503
514
|
runId,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
|
|
3
4
|
export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
|
|
4
5
|
export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
|
|
5
6
|
export const PI_CREW_INHERIT_PROJECT_CONTEXT_ENV = "PI_CREW_INHERIT_PROJECT_CONTEXT";
|
|
6
7
|
export const PI_CREW_INHERIT_SKILLS_ENV = "PI_CREW_INHERIT_SKILLS";
|
|
8
|
+
const PI_CREW_MAX_OUTPUT_ENV = "PI_CREW_MAX_OUTPUT";
|
|
9
|
+
const PI_CREW_STEERING_FILE_ENV = "PI_CREW_STEERING_FILE";
|
|
7
10
|
|
|
8
11
|
const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
|
|
9
12
|
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
@@ -58,6 +61,68 @@ export function rewriteTeamWorkerPrompt(prompt: string, options: { inheritProjec
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
|
|
64
|
+
// ── Feature 1: maxTokens cap ──────────────────────────────────────────
|
|
65
|
+
// Cap output tokens per API call for background workers. Reads
|
|
66
|
+
// PI_CREW_MAX_OUTPUT_TOKENS env (set by pi-args.ts from agent.maxTokens).
|
|
67
|
+
const maxTokensEnv = process.env[PI_CREW_MAX_OUTPUT_ENV];
|
|
68
|
+
const maxTokensCap = maxTokensEnv ? Number.parseInt(maxTokensEnv, 10) : undefined;
|
|
69
|
+
if (maxTokensCap && maxTokensCap > 0) {
|
|
70
|
+
pi.on("before_provider_request", (event) => {
|
|
71
|
+
const payload = event.payload as Record<string, unknown> | undefined;
|
|
72
|
+
if (!payload || typeof payload !== "object") return;
|
|
73
|
+
// Cap both OpenAI-style max_tokens and Anthropic-style max_tokens
|
|
74
|
+
if (typeof payload.max_tokens === "number" && payload.max_tokens > maxTokensCap) {
|
|
75
|
+
payload.max_tokens = maxTokensCap;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Feature 2: real-time steering ──────────────────────────────────────
|
|
81
|
+
// Poll the steering JSONL file for new steer messages. The parent (team
|
|
82
|
+
// tool) writes steers here in real-time; this reader injects them into
|
|
83
|
+
// the active session via pi.sendMessage with deliverAs:"steer".
|
|
84
|
+
const steeringFile = process.env[PI_CREW_STEERING_FILE_ENV];
|
|
85
|
+
if (steeringFile) {
|
|
86
|
+
let lastOffset = 0;
|
|
87
|
+
const pollSteering = (): void => {
|
|
88
|
+
try {
|
|
89
|
+
const stat = fs.statSync(steeringFile, { throwIfNoEntry: false });
|
|
90
|
+
if (!stat || stat.size <= lastOffset) return;
|
|
91
|
+
const fd = fs.openSync(steeringFile, "r");
|
|
92
|
+
try {
|
|
93
|
+
const buf = Buffer.alloc(stat.size - lastOffset);
|
|
94
|
+
fs.readSync(fd, buf, 0, buf.length, lastOffset);
|
|
95
|
+
lastOffset = stat.size;
|
|
96
|
+
const lines = buf.toString("utf8").split("\n").filter(Boolean);
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
try {
|
|
99
|
+
const entry = JSON.parse(line) as { type?: string; message?: string };
|
|
100
|
+
if (entry.type === "steer" && entry.message) {
|
|
101
|
+
pi.sendMessage(
|
|
102
|
+
{ customType: "crew-steer", content: entry.message, display: false },
|
|
103
|
+
{ deliverAs: "steer" },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Malformed line — skip
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
try {
|
|
112
|
+
fs.closeSync(fd);
|
|
113
|
+
} catch {
|
|
114
|
+
/* already closed */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// File doesn't exist yet or read error — will retry next tick
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const timer = setInterval(pollSteering, 500);
|
|
122
|
+
timer.unref?.();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Prompt rewriting (existing) ────────────────────────────────────────
|
|
61
126
|
pi.on("before_agent_start", (event) => {
|
|
62
127
|
const inheritProjectContext = readBooleanEnvAny(PI_CREW_INHERIT_PROJECT_CONTEXT_ENV, PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV);
|
|
63
128
|
const inheritSkills = readBooleanEnvAny(PI_CREW_INHERIT_SKILLS_ENV, PI_TEAMS_INHERIT_SKILLS_ENV);
|