@dungle-scrubs/tallow 0.8.25 → 0.8.26

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 (52) hide show
  1. package/dist/auth-hardening.d.ts +12 -0
  2. package/dist/auth-hardening.d.ts.map +1 -1
  3. package/dist/auth-hardening.js +30 -7
  4. package/dist/auth-hardening.js.map +1 -1
  5. package/dist/cli.js +5 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.js +1 -1
  9. package/dist/install.js +2 -2
  10. package/dist/install.js.map +1 -1
  11. package/dist/interactive-mode-patch.d.ts.map +1 -1
  12. package/dist/interactive-mode-patch.js +119 -7
  13. package/dist/interactive-mode-patch.js.map +1 -1
  14. package/dist/model-metadata-overrides.d.ts +19 -0
  15. package/dist/model-metadata-overrides.d.ts.map +1 -0
  16. package/dist/model-metadata-overrides.js +38 -0
  17. package/dist/model-metadata-overrides.js.map +1 -0
  18. package/dist/sdk.d.ts +2 -0
  19. package/dist/sdk.d.ts.map +1 -1
  20. package/dist/sdk.js +28 -1
  21. package/dist/sdk.js.map +1 -1
  22. package/extensions/__integration__/teams-runtime.test.ts +22 -1
  23. package/extensions/_shared/__tests__/shell-policy.test.ts +197 -0
  24. package/extensions/_shared/shell-policy.ts +27 -0
  25. package/extensions/background-task-tool/index.ts +2 -1
  26. package/extensions/bash-tool-enhanced/index.ts +2 -1
  27. package/extensions/custom-footer/__tests__/index.test.ts +29 -0
  28. package/extensions/custom-footer/context-display.ts +49 -0
  29. package/extensions/custom-footer/index.ts +10 -23
  30. package/extensions/permissions/index.ts +31 -10
  31. package/extensions/plan-mode-tool/__tests__/index.test.ts +32 -2
  32. package/extensions/plan-mode-tool/index.ts +6 -1
  33. package/extensions/slash-command-bridge/index.ts +30 -1
  34. package/extensions/subagent-tool/__tests__/process-liveness.test.ts +42 -3
  35. package/extensions/subagent-tool/process.ts +132 -21
  36. package/extensions/tasks/__tests__/store.test.ts +26 -2
  37. package/extensions/tasks/commands/register-tasks-extension.ts +2 -2
  38. package/extensions/tasks/index.ts +5 -5
  39. package/extensions/tasks/state/index.ts +90 -36
  40. package/extensions/teams-tool/__tests__/archive-store.test.ts +98 -0
  41. package/extensions/teams-tool/__tests__/peer-messaging.test.ts +26 -0
  42. package/extensions/teams-tool/archive-store.ts +200 -0
  43. package/extensions/teams-tool/sessions/spawn.ts +244 -71
  44. package/extensions/teams-tool/tools/register-extension.ts +146 -105
  45. package/extensions/teams-tool/tools/teammate-tools.ts +43 -1
  46. package/package.json +4 -4
  47. package/skills/tallow-expert/SKILL.md +1 -1
  48. package/templates/agents/architect.md +13 -5
  49. package/templates/agents/debug.md +3 -3
  50. package/templates/agents/explore.md +9 -2
  51. package/templates/agents/refactor.md +2 -2
  52. package/templates/agents/scout.md +3 -2
@@ -10,70 +10,100 @@ import {
10
10
  AuthStorage,
11
11
  createAgentSession,
12
12
  createExtensionRuntime,
13
+ DefaultResourceLoader,
13
14
  ModelRegistry,
14
- type ResourceLoader,
15
15
  SessionManager,
16
16
  SettingsManager,
17
17
  } from "@mariozechner/pi-coding-agent";
18
- import { getTallowPath } from "../../_shared/tallow-paths.js";
18
+ import { applyKnownModelMetadataOverrides } from "../../../src/model-metadata-overrides.js";
19
+ import { getTallowHomeDir, getTallowPath } from "../../_shared/tallow-paths.js";
20
+ import {
21
+ type AgentConfig,
22
+ computeEffectiveTools,
23
+ discoverAgents,
24
+ resolveAgentForExecution,
25
+ } from "../../subagent-tool/agents.js";
19
26
  import { type RoutingHints, routeModel } from "../../subagent-tool/model-router.js";
20
27
  import { resolveStandardTools } from "../state/team-view.js";
21
28
  import type { Teammate } from "../state/types.js";
22
29
  import type { Team } from "../store.js";
23
30
  import { createTeammateTools } from "../tools/teammate-tools.js";
24
31
 
32
+ interface SpawnTeammateSessionOptions {
33
+ readonly agentName?: string;
34
+ readonly cwd: string;
35
+ readonly hints?: RoutingHints;
36
+ readonly modelOverride?: string;
37
+ readonly name: string;
38
+ readonly parentModelId?: string;
39
+ readonly piEvents?: ExtensionAPI["events"];
40
+ readonly role?: string;
41
+ readonly team: Team<Teammate>;
42
+ readonly thinkingLevel?: string;
43
+ readonly toolNames?: string[];
44
+ }
45
+
46
+ type TeammateThinkingLevel = "high" | "low" | "medium" | "off";
47
+
25
48
  /**
26
- * Spawn a teammate as an in-process AgentSession with shared team tools.
49
+ * Coerce an arbitrary string into a supported teammate thinking level.
27
50
  *
28
- * Model selection follows the same routing as subagents:
29
- * - modelOverride set explicit fuzzy resolution (best match)
30
- * - modelScope set → auto-route within that model family
31
- * - neither → full auto-route based on role complexity and cost preference
32
- *
33
- * @param cwd - Working directory
34
- * @param team - Team to add the teammate to
35
- * @param name - Teammate name
36
- * @param role - Role description (used for task classification + system prompt)
37
- * @param modelOverride - Explicit model name (fuzzy matched). Skips auto-routing.
38
- * @param toolNames - Standard tool names (defaults to all coding tools)
39
- * @param piEvents - Event emitter for lifecycle events
40
- * @param hints - Optional routing hints (modelScope, costPreference, etc.)
41
- * @param parentModelId - Parent model ID for fallback inheritance
42
- * @returns The created Teammate
43
- * @throws If model not found or session creation fails
51
+ * @param value - Raw level string from caller context or tool params
52
+ * @returns Supported thinking level, or undefined when unsupported
44
53
  */
45
- export async function spawnTeammateSession(
46
- cwd: string,
47
- team: Team<Teammate>,
48
- name: string,
49
- role: string,
50
- modelOverride: string | undefined,
51
- toolNames?: string[],
52
- piEvents?: ExtensionAPI["events"],
53
- hints?: RoutingHints,
54
- parentModelId?: string
55
- ): Promise<Teammate> {
56
- const routing = await routeModel(role, modelOverride, undefined, parentModelId, role, hints, cwd);
57
- if (!routing.ok) {
58
- const available = listAvailableModels().slice(0, 20).join(", ");
59
- throw new Error(`Model not found: "${routing.query}". Available: ${available}`);
54
+ function coerceThinkingLevel(value: string | undefined): TeammateThinkingLevel | undefined {
55
+ if (!value) return undefined;
56
+ const normalized = value.trim().toLowerCase();
57
+ if (
58
+ normalized === "off" ||
59
+ normalized === "low" ||
60
+ normalized === "medium" ||
61
+ normalized === "high"
62
+ ) {
63
+ return normalized;
60
64
  }
61
- const resolved = routing.model;
62
-
63
- // Use the user's tallow auth and model config so teammates inherit
64
- // API keys and custom model definitions from the main session.
65
- const authStorage = AuthStorage.create(getTallowPath("auth.json"));
66
- const modelRegistry = new ModelRegistry(authStorage, getTallowPath("models.json"));
65
+ return undefined;
66
+ }
67
67
 
68
- const model = modelRegistry.find(resolved.provider, resolved.id);
69
- if (!model) {
68
+ /**
69
+ * Resolve an optional teammate agent template from the shared agent directories.
70
+ *
71
+ * Team teammates should not silently fall back to an ephemeral template when the
72
+ * caller explicitly requested a named agent. Typos must fail closed.
73
+ *
74
+ * @param cwd - Working directory used for project-agent discovery
75
+ * @param agentName - Optional template name requested by the caller
76
+ * @returns Resolved agent template, or undefined when no template was requested
77
+ * @throws {Error} When a named template cannot be found
78
+ */
79
+ function resolveTeammateAgentTemplate(
80
+ cwd: string,
81
+ agentName: string | undefined
82
+ ): AgentConfig | undefined {
83
+ if (!agentName) return undefined;
84
+ const discovery = discoverAgents(cwd, "both");
85
+ const resolved = resolveAgentForExecution(agentName, discovery.agents, discovery.defaults);
86
+ if (resolved.resolution === "ephemeral") {
70
87
  throw new Error(
71
- `Model resolved to "${resolved.id}" (provider: ${resolved.provider}) but not found in registry`
88
+ `Teammate agent template "${agentName}" was not found in user or project agent directories.`
72
89
  );
73
90
  }
91
+ return resolved.agent;
92
+ }
74
93
 
75
- const otherNames = Array.from(team.teammates.keys()).filter((n) => n !== name);
76
- const systemPrompt = [
94
+ /**
95
+ * Build the coordination prompt appended to every teammate.
96
+ *
97
+ * @param team - Runtime team container
98
+ * @param name - Teammate name
99
+ * @param role - Effective teammate role text
100
+ * @returns Coordination instructions shared by all teammates
101
+ */
102
+ function buildCoordinationPrompt(team: Team<Teammate>, name: string, role: string): string {
103
+ const otherNames = Array.from(team.teammates.keys()).filter(
104
+ (teammateName) => teammateName !== name
105
+ );
106
+ return [
77
107
  `You are "${name}", a teammate in team "${team.name}".`,
78
108
  `Your role: ${role}`,
79
109
  "",
@@ -95,40 +125,183 @@ export async function spawnTeammateSession(
95
125
  "",
96
126
  "Communicate with teammates via team_message when you need their input.",
97
127
  ].join("\n");
128
+ }
98
129
 
99
- const resourceLoader: ResourceLoader = {
100
- getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
101
- getSkills: () => ({ skills: [], diagnostics: [] }),
102
- getPrompts: () => ({ prompts: [], diagnostics: [] }),
103
- getThemes: () => ({ themes: [], diagnostics: [] }),
104
- getAgentsFiles: () => ({ agentsFiles: [] }),
105
- getSystemPrompt: () => systemPrompt,
106
- getAppendSystemPrompt: () => [],
107
- getPathMetadata: () => new Map(),
108
- extendResources: () => {},
109
- reload: async () => {},
110
- };
130
+ /**
131
+ * Merge teammate coordination with an optional agent template system prompt.
132
+ *
133
+ * @param team - Runtime team container
134
+ * @param name - Teammate name
135
+ * @param role - Effective teammate role text
136
+ * @param templateAgent - Optional resolved agent template
137
+ * @returns Final system prompt text for the teammate session
138
+ */
139
+ function buildTeammateSystemPrompt(
140
+ team: Team<Teammate>,
141
+ name: string,
142
+ role: string,
143
+ templateAgent: AgentConfig | undefined
144
+ ): string {
145
+ const sections = [buildCoordinationPrompt(team, name, role)];
146
+ if (templateAgent?.systemPrompt.trim()) {
147
+ sections.push(
148
+ [`Base agent template: ${templateAgent.name}`, templateAgent.systemPrompt.trim()].join("\n\n")
149
+ );
150
+ }
151
+ if (templateAgent?.maxTurns) {
152
+ sections.unshift(
153
+ `You have a maximum of ${templateAgent.maxTurns} tool-use turns for this task. Plan accordingly and return your best partial result before hitting the limit.`
154
+ );
155
+ }
156
+ return sections.join("\n\n");
157
+ }
111
158
 
112
- const teammateCustomTools = createTeammateTools(team, name, piEvents);
159
+ /**
160
+ * Resolve the standard tool allowlist for a teammate.
161
+ *
162
+ * Explicit tool names passed to `team_spawn` win. Otherwise agent-template
163
+ * allow/deny lists are applied when present.
164
+ *
165
+ * @param explicitToolNames - Tools passed directly to team_spawn
166
+ * @param templateAgent - Optional resolved agent template
167
+ * @returns Effective tool names, or undefined to allow the default coding set
168
+ */
169
+ function resolveTeammateToolNames(
170
+ explicitToolNames: string[] | undefined,
171
+ templateAgent: AgentConfig | undefined
172
+ ): string[] | undefined {
173
+ if (explicitToolNames) return explicitToolNames;
174
+ if (!templateAgent) return undefined;
175
+ return computeEffectiveTools(templateAgent.tools, templateAgent.disallowedTools);
176
+ }
113
177
 
114
- const { session } = await createAgentSession({
178
+ /**
179
+ * Create a loader that only exposes the requested teammate skills/system prompt.
180
+ *
181
+ * Extensions are disabled on purpose — teammates get only the explicitly passed
182
+ * standard tools plus the injected team coordination tools.
183
+ *
184
+ * @param cwd - Working directory
185
+ * @param settingsManager - Shared in-memory settings for the teammate session
186
+ * @param systemPrompt - Final system prompt text
187
+ * @param templateAgent - Optional resolved agent template
188
+ * @returns Reloaded resource loader ready for createAgentSession
189
+ */
190
+ async function createTeammateResourceLoader(
191
+ cwd: string,
192
+ settingsManager: SettingsManager,
193
+ systemPrompt: string,
194
+ templateAgent: AgentConfig | undefined
195
+ ): Promise<DefaultResourceLoader> {
196
+ const requestedSkills = new Set(templateAgent?.skills ?? []);
197
+ const loader = new DefaultResourceLoader({
198
+ agentDir: getTallowHomeDir(),
115
199
  cwd,
116
- agentDir: path.join(os.tmpdir(), `pi-team-${team.name}-${name}`),
117
- model,
118
- thinkingLevel: "off",
200
+ extensionsOverride: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
201
+ promptsOverride: () => ({ diagnostics: [], prompts: [] }),
202
+ settingsManager,
203
+ skillsOverride: (base) => ({
204
+ diagnostics: base.diagnostics,
205
+ skills:
206
+ requestedSkills.size === 0
207
+ ? []
208
+ : base.skills.filter((skill) => requestedSkills.has(skill.name)),
209
+ }),
210
+ systemPromptOverride: () => systemPrompt,
211
+ });
212
+ await loader.reload();
213
+ return loader;
214
+ }
215
+
216
+ /**
217
+ * Spawn a teammate as an in-process AgentSession with shared team tools.
218
+ *
219
+ * Model selection follows the same routing as subagents:
220
+ * - modelOverride set → explicit fuzzy resolution (best match)
221
+ * - template agent model set → explicit or auto-routing keyword from frontmatter
222
+ * - neither → full auto-route based on role complexity and cost preference
223
+ *
224
+ * @param options - Session spawn options
225
+ * @returns The created Teammate
226
+ * @throws {Error} If model or agent template resolution fails
227
+ */
228
+ export async function spawnTeammateSession(
229
+ options: SpawnTeammateSessionOptions
230
+ ): Promise<Teammate> {
231
+ const templateAgent = resolveTeammateAgentTemplate(options.cwd, options.agentName);
232
+ const role = options.role?.trim() || templateAgent?.description?.trim();
233
+ if (!role) {
234
+ throw new Error("team_spawn requires either a role or an agent template.");
235
+ }
236
+
237
+ const routingTask = templateAgent?.systemPrompt?.trim()
238
+ ? `${role}\n\n${templateAgent.systemPrompt.trim()}`
239
+ : role;
240
+ const routing = await routeModel(
241
+ routingTask,
242
+ options.modelOverride,
243
+ templateAgent?.model,
244
+ options.parentModelId,
245
+ role,
246
+ options.hints,
247
+ options.cwd
248
+ );
249
+ if (!routing.ok) {
250
+ const available = listAvailableModels().slice(0, 20).join(", ");
251
+ throw new Error(`Model not found: "${routing.query}". Available: ${available}`);
252
+ }
253
+ const resolvedModel = routing.model;
254
+
255
+ // Use the user's tallow auth and model config so teammates inherit
256
+ // API keys and custom model definitions from the main session.
257
+ const authStorage = AuthStorage.create(getTallowPath("auth.json"));
258
+ const modelRegistry = new ModelRegistry(authStorage, getTallowPath("models.json"));
259
+ applyKnownModelMetadataOverrides(modelRegistry);
260
+ const model = modelRegistry.find(resolvedModel.provider, resolvedModel.id);
261
+ if (!model) {
262
+ throw new Error(
263
+ `Model resolved to "${resolvedModel.id}" (provider: ${resolvedModel.provider}) but not found in registry`
264
+ );
265
+ }
266
+
267
+ const systemPrompt = buildTeammateSystemPrompt(options.team, options.name, role, templateAgent);
268
+ const settingsManager = SettingsManager.inMemory({
269
+ compaction: { enabled: true },
270
+ retry: { enabled: true, maxRetries: 2 },
271
+ });
272
+ const resourceLoader = await createTeammateResourceLoader(
273
+ options.cwd,
274
+ settingsManager,
275
+ systemPrompt,
276
+ templateAgent
277
+ );
278
+ const teammateCustomTools = createTeammateTools(options.team, options.name, options.piEvents);
279
+ const thinkingLevel = coerceThinkingLevel(options.thinkingLevel) ?? "off";
280
+ const toolNames = resolveTeammateToolNames(options.toolNames, templateAgent);
281
+ const standardTools =
282
+ toolNames && toolNames.length === 0 ? [] : resolveStandardTools(options.cwd, toolNames);
283
+
284
+ const { session } = await createAgentSession({
119
285
  authStorage,
286
+ customTools: teammateCustomTools,
287
+ cwd: options.cwd,
288
+ agentDir: path.join(os.tmpdir(), `pi-team-${options.team.name}-${options.name}`),
289
+ model,
120
290
  modelRegistry,
121
291
  resourceLoader,
122
- tools: resolveStandardTools(cwd, toolNames),
123
- customTools: teammateCustomTools,
124
292
  sessionManager: SessionManager.inMemory(),
125
- settingsManager: SettingsManager.inMemory({
126
- compaction: { enabled: true },
127
- retry: { enabled: true, maxRetries: 2 },
128
- }),
293
+ settingsManager,
294
+ thinkingLevel,
295
+ tools: standardTools,
129
296
  });
130
297
 
131
- const mate: Teammate = { name, role, model: resolved.id, session, status: "idle" };
132
- team.teammates.set(name, mate);
133
- return mate;
298
+ const teammate: Teammate = {
299
+ model: resolvedModel.id,
300
+ name: options.name,
301
+ role,
302
+ session,
303
+ status: "idle",
304
+ };
305
+ options.team.teammates.set(options.name, teammate);
306
+ return teammate;
134
307
  }