@fradser/pi-agent-teams 0.6.0 → 0.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/.memory/team-centric-design.md +1 -1
- package/README.md +33 -13
- package/package.json +3 -3
- package/references/agent-roles.md +77 -0
- package/src/agents.ts +141 -15
- package/src/follow-up-queue.ts +1 -0
- package/src/guidance.ts +46 -20
- package/src/index.ts +6 -4
- package/src/picker-keys.ts +30 -0
- package/src/state.ts +42 -5
- package/src/team-machine.ts +406 -54
- package/src/tools.ts +79 -93
- package/src/types.ts +23 -4
- package/src/ui.ts +369 -69
- package/src/worker.ts +3 -1
- package/agents/observer.md +0 -10
- package/agents/reviewer.md +0 -13
- package/agents/specialist.md +0 -10
- package/agents/worker.md +0 -11
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keystroke routing for the teammate-model picker. Pure mapping from raw
|
|
3
|
+
* terminal input to picker actions so the collision-prone decision table is
|
|
4
|
+
* unit-testable without a TUI: every printable character (letters included)
|
|
5
|
+
* must route to typing, never to a shortcut.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
export type PickerKeyAction =
|
|
11
|
+
| { kind: "type"; text: string }
|
|
12
|
+
| { kind: "backspace" }
|
|
13
|
+
| { kind: "up" }
|
|
14
|
+
| { kind: "down" }
|
|
15
|
+
| { kind: "confirm" }
|
|
16
|
+
| { kind: "cancel" };
|
|
17
|
+
|
|
18
|
+
/** Map one raw input event to a picker action, or undefined when the event
|
|
19
|
+
* is not a picker key (multi-byte pastes, unhandled control sequences). */
|
|
20
|
+
export function mapPickerKey(data: string): PickerKeyAction | undefined {
|
|
21
|
+
if (matchesKey(data, Key.escape)) return { kind: "cancel" };
|
|
22
|
+
if (matchesKey(data, Key.enter)) return { kind: "confirm" };
|
|
23
|
+
if (matchesKey(data, Key.up)) return { kind: "up" };
|
|
24
|
+
if (matchesKey(data, Key.down)) return { kind: "down" };
|
|
25
|
+
if (matchesKey(data, Key.backspace) || matchesKey(data, Key.delete)) return { kind: "backspace" };
|
|
26
|
+
// Printable characters are always search text — letters like "c" must
|
|
27
|
+
// never double as shortcuts in a type-to-filter list.
|
|
28
|
+
if (data.length === 1 && data >= " ") return { kind: "type", text: data };
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type TeamState,
|
|
14
14
|
type WorkerReportEvent,
|
|
15
15
|
} from "./types.ts";
|
|
16
|
+
import { nonEmpty } from "@fradser/pi-kit";
|
|
16
17
|
|
|
17
18
|
export const MAX_LEADER_MAILBOX_MESSAGES = 4096;
|
|
18
19
|
/** FIFO cap of remembered peer message ids per inbox (dedup guard). */
|
|
@@ -22,10 +23,9 @@ export const MAX_TASK_DEPENDENCIES = 32;
|
|
|
22
23
|
function emptyState(): TeamState {
|
|
23
24
|
return {
|
|
24
25
|
teammates: {},
|
|
25
|
-
tasks:
|
|
26
|
+
tasks: emptyTaskMap(),
|
|
26
27
|
leaderMailbox: [],
|
|
27
28
|
messageCounter: 0,
|
|
28
|
-
taskCounter: 0,
|
|
29
29
|
workerEventOffsets: {},
|
|
30
30
|
workerEventIds: {},
|
|
31
31
|
peerInboxOffsets: {},
|
|
@@ -58,11 +58,30 @@ function nextMessageId(): string {
|
|
|
58
58
|
return `msg_${++state.messageCounter}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Task maps must not inherit Object.prototype: slug ids like "constructor"
|
|
62
|
+
* are legal task ids and must never alias inherited properties. */
|
|
63
|
+
function emptyTaskMap(): Record<string, BoardTask> {
|
|
64
|
+
return Object.create(null);
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
export function resetState(): void {
|
|
62
68
|
state = emptyState();
|
|
63
69
|
markStateDirty();
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
// ── Team default model ──────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/** The unified teammate model for this session, or undefined when Pi picks. */
|
|
75
|
+
export function getTeamDefaultModel(): string | undefined {
|
|
76
|
+
return state.defaultModel;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Set (or clear with undefined) the unified teammate model for later spawns. */
|
|
80
|
+
export function setTeamDefaultModel(ref: string | undefined): void {
|
|
81
|
+
state.defaultModel = nonEmpty(ref);
|
|
82
|
+
markStateDirty();
|
|
83
|
+
}
|
|
84
|
+
|
|
66
85
|
// ── Roster queries ────────────────────────────────────────────────
|
|
67
86
|
|
|
68
87
|
const NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/i;
|
|
@@ -204,6 +223,26 @@ export function setPeerInboxOffset(inboxName: string, offset: number): void {
|
|
|
204
223
|
|
|
205
224
|
// ── Board: creation and queries ───────────────────────────────────
|
|
206
225
|
|
|
226
|
+
/** Maximum characters in a generated slug task id (suffix included). */
|
|
227
|
+
const MAX_TASK_ID_LENGTH = 48;
|
|
228
|
+
|
|
229
|
+
/** A readable, filesystem-safe id derived from the subject:
|
|
230
|
+
* "Polish login flow" -> "polish-login-flow"; duplicates get -2, -3, ... */
|
|
231
|
+
export function taskIdFromSubject(subject: string, taken: ReadonlySet<string>): string {
|
|
232
|
+
const base = subject
|
|
233
|
+
.toLowerCase()
|
|
234
|
+
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
|
235
|
+
.replace(/^-+|-+$/g, "")
|
|
236
|
+
.slice(0, MAX_TASK_ID_LENGTH)
|
|
237
|
+
.replace(/-+$/g, "") || "task";
|
|
238
|
+
let id = base;
|
|
239
|
+
for (let n = 2; taken.has(id); n++) {
|
|
240
|
+
const suffix = `-${n}`;
|
|
241
|
+
id = base.slice(0, Math.max(1, MAX_TASK_ID_LENGTH - suffix.length)).replace(/-+$/g, "") + suffix;
|
|
242
|
+
}
|
|
243
|
+
return id;
|
|
244
|
+
}
|
|
245
|
+
|
|
207
246
|
export function createTask(input: {
|
|
208
247
|
subject: string;
|
|
209
248
|
description?: string;
|
|
@@ -219,7 +258,7 @@ export function createTask(input: {
|
|
|
219
258
|
for (const dep of dependsOn) {
|
|
220
259
|
if (!state.tasks[dep]) return { ok: false, error: `Task depends on unknown task "${dep}".` };
|
|
221
260
|
}
|
|
222
|
-
const id =
|
|
261
|
+
const id = taskIdFromSubject(subject, new Set(Object.keys(state.tasks)));
|
|
223
262
|
const task: BoardTask = {
|
|
224
263
|
id,
|
|
225
264
|
subject,
|
|
@@ -350,8 +389,6 @@ export function loadBoard(tasks: Record<string, BoardTask>): number {
|
|
|
350
389
|
updatedAt: Date.now(),
|
|
351
390
|
};
|
|
352
391
|
state.tasks[restored.id] = restored;
|
|
353
|
-
const numeric = Number(restored.id.slice(2));
|
|
354
|
-
if (Number.isFinite(numeric)) state.taskCounter = Math.max(state.taskCounter, numeric);
|
|
355
392
|
reloaded++;
|
|
356
393
|
}
|
|
357
394
|
if (reloaded > 0) markStateDirty();
|