takomi 2.1.36 → 2.1.38

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 (32) hide show
  1. package/.pi/README.md +6 -3
  2. package/.pi/agents/orchestrator.md +9 -5
  3. package/.pi/extensions/oauth-router/commands.ts +36 -35
  4. package/.pi/extensions/oauth-router/index.ts +8 -8
  5. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +40 -7
  6. package/.pi/extensions/takomi-context-manager/diagnostics.ts +534 -55
  7. package/.pi/extensions/takomi-context-manager/index.ts +14 -2
  8. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +16 -4
  9. package/.pi/extensions/takomi-context-manager/policy-tools.ts +7 -0
  10. package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +2 -0
  11. package/.pi/extensions/takomi-context-manager/session-state.ts +160 -0
  12. package/.pi/extensions/takomi-context-manager/skill-registry.ts +120 -0
  13. package/.pi/extensions/takomi-context-manager/skill-tools.ts +26 -3
  14. package/.pi/extensions/takomi-context-manager/state.ts +11 -1
  15. package/.pi/extensions/takomi-context-manager/types.ts +9 -1
  16. package/.pi/extensions/takomi-runtime/command-text.ts +2 -1
  17. package/.pi/extensions/takomi-runtime/commands.ts +13 -1
  18. package/.pi/extensions/takomi-subagents/index.ts +30 -3
  19. package/.pi/extensions/takomi-subagents/native-render.ts +3 -1
  20. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +18 -3
  21. package/.pi/extensions/takomi-subagents/tool-runner.ts +38 -3
  22. package/.pi/prompts/takomi-prompt.md +6 -4
  23. package/.pi/takomi/policies/subagent-routing.md +9 -1
  24. package/.pi/takomi/policies/takomi-lifecycle-routing.md +3 -1
  25. package/README.md +22 -11
  26. package/assets/.agent/skills/exam-creator-skill/SKILL.md +182 -0
  27. package/assets/.agent/skills/exam-creator-skill/references/model-routing-policy.md +48 -0
  28. package/assets/.agent/skills/exam-creator-skill/references/workflow-rulebook.md +115 -0
  29. package/package.json +2 -2
  30. package/src/cli.js +38 -1
  31. package/src/doctor.js +7 -1
  32. package/src/pi-harness.js +96 -0
@@ -6,6 +6,7 @@ import type { ContextManagerState } from "./state";
6
6
  import { recordBlocked } from "./state";
7
7
  import { resolveTakomiRoutingPolicy } from "../takomi-runtime/routing-policy";
8
8
  import { approvedModelEquivalent, isTakomiModelApproved } from "../takomi-runtime/model-routing-defaults";
9
+ import { persistReportSnapshot } from "./session-state";
9
10
 
10
11
  type Settings = {
11
12
  takomi?: { modelRoutingPolicyFile?: string };
@@ -183,19 +184,19 @@ export function installModelPolicyGate(pi: ExtensionAPI, state: ContextManagerSt
183
184
  if (approved.length === 0) return;
184
185
 
185
186
  const refs = collectRequestedModelRefs(event.input);
186
- const corrections: string[] = [];
187
+ const corrections: Array<{ from: string; to: string; recovery?: string }> = [];
187
188
  for (const ref of refs) {
188
189
  if (isTakomiModelApproved(ref.value, approved)) continue;
189
190
  const equivalent = approvedModelEquivalent(ref.value, approved);
190
191
  if (equivalent) {
191
192
  setModelRef(ref, equivalent);
192
- corrections.push(`${ref.value} -> ${equivalent}`);
193
+ corrections.push({ from: ref.value, to: equivalent });
193
194
  continue;
194
195
  }
195
196
  const recovery = await askForInvalidModelRecovery(ctx, ref.value, approved);
196
197
  if (recovery.action === "retry") {
197
198
  setModelRef(ref, recovery.model);
198
- corrections.push(`${ref.value} -> ${recovery.model} (user selected recovery)`);
199
+ corrections.push({ from: ref.value, to: recovery.model, recovery: "user selected recovery" });
199
200
  continue;
200
201
  }
201
202
  const reason = [
@@ -204,12 +205,23 @@ export function installModelPolicyGate(pi: ExtensionAPI, state: ContextManagerSt
204
205
  "Human selected stop. The agent turn has been aborted; wait for the user's next prompt.",
205
206
  ].join("\n");
206
207
  recordBlocked(state, event.toolName, reason);
208
+ persistReportSnapshot(pi, state, "model-policy-gate-block");
207
209
  ctx.abort?.();
208
210
  return { block: true, reason };
209
211
  }
210
212
 
211
213
  if (corrections.length > 0) {
212
- ctx.ui.notify(`Takomi context manager corrected subagent model routing:\n- ${corrections.join("\n- ")}\n\nBe careful to follow /takomi routing policy next time.`, "warning");
214
+ const timestamp = new Date().toISOString();
215
+ state.report.timestamp = timestamp;
216
+ state.report.modelRoutingCorrections.push(...corrections.map((correction) => ({
217
+ toolName: event.toolName,
218
+ from: correction.from,
219
+ to: correction.to,
220
+ recovery: correction.recovery,
221
+ timestamp,
222
+ })));
223
+ persistReportSnapshot(pi, state, "model-policy-correction");
224
+ ctx.ui.notify(`Takomi context manager corrected subagent model routing:\n- ${corrections.map((correction) => `${correction.from} -> ${correction.to}${correction.recovery ? ` (${correction.recovery})` : ""}`).join("\n- ")}\n\nBe careful to follow /takomi routing policy next time.`, "warning");
213
225
  }
214
226
  });
215
227
 
@@ -3,6 +3,7 @@ import { Type } from "typebox";
3
3
  import type { ContextManagerState } from "./state";
4
4
  import { syncReportLedger } from "./state";
5
5
  import { renderPolicies, renderPolicyManifest } from "./policy-registry";
6
+ import { persistReportSnapshot, restoreReportFromSession } from "./session-state";
6
7
 
7
8
  export function registerPolicyTools(pi: ExtensionAPI, state: ContextManagerState): void {
8
9
  pi.registerTool({
@@ -12,8 +13,11 @@ export function registerPolicyTools(pi: ExtensionAPI, state: ContextManagerState
12
13
  promptSnippet: "Show available context policy pack descriptions",
13
14
  parameters: Type.Object({ policies: Type.Optional(Type.Array(Type.String({ description: "Policy name to inspect" }))) }),
14
15
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
16
+ restoreReportFromSession(state, ctx);
17
+ state.report.timestamp = new Date().toISOString();
15
18
  state.report.cwd = ctx.cwd;
16
19
  state.report.toolCalls.policyManifest += 1;
20
+ persistReportSnapshot(pi, state, "policy_manifest");
17
21
  return { content: [{ type: "text", text: renderPolicyManifest(state.policies, params.policies ?? []) }], details: { requested: params.policies ?? [...state.policies.keys()] } };
18
22
  },
19
23
  });
@@ -25,10 +29,13 @@ export function registerPolicyTools(pi: ExtensionAPI, state: ContextManagerState
25
29
  promptSnippet: "Load policy packs required before sensitive tool calls",
26
30
  parameters: Type.Object({ policies: Type.Array(Type.String({ description: "Policy pack name to load" })) }),
27
31
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
32
+ restoreReportFromSession(state, ctx);
33
+ state.report.timestamp = new Date().toISOString();
28
34
  state.report.cwd = ctx.cwd;
29
35
  state.report.toolCalls.policyLoad += 1;
30
36
  const text = renderPolicies(state.policies, state.loadedPolicies, params.policies);
31
37
  syncReportLedger(state);
38
+ persistReportSnapshot(pi, state, "policy_load");
32
39
  return { content: [{ type: "text", text }], details: { requested: params.policies, loadedPolicies: [...state.loadedPolicies].sort() } };
33
40
  },
34
41
  });
@@ -3,6 +3,7 @@ import type { ContextManagerConfig } from "./types";
3
3
  import type { ContextManagerState } from "./state";
4
4
  import { recordBlocked, syncReportLedger } from "./state";
5
5
  import { renderPolicies } from "./policy-registry";
6
+ import { persistReportSnapshot } from "./session-state";
6
7
 
7
8
  function renderPolicyGateBlock(toolName: string, missing: string[], policyText: string): string {
8
9
  return [
@@ -33,6 +34,7 @@ export function installPrerequisiteGates(pi: ExtensionAPI, state: ContextManager
33
34
  syncReportLedger(state);
34
35
  const reason = renderPolicyGateBlock(event.toolName, missing, policyText);
35
36
  recordBlocked(state, event.toolName, reason);
37
+ persistReportSnapshot(pi, state, "prerequisite-gate-block");
36
38
  return { block: true, reason };
37
39
  }
38
40
  });
@@ -0,0 +1,160 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { ContextManagerState } from "./state";
3
+ import type { ContextReport } from "./types";
4
+
5
+ const SNAPSHOT_TYPE = "takomi-context-manager-report";
6
+ const TOOL_NAMES = ["skill_index", "skill_manifest", "skill_load", "policy_manifest", "policy_load", "context_report"] as const;
7
+ type ToolName = typeof TOOL_NAMES[number];
8
+
9
+ type RestoreStats = {
10
+ snapshotCount: number;
11
+ toolResultCount: number;
12
+ };
13
+
14
+ function asRecord(value: unknown): Record<string, unknown> {
15
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
16
+ }
17
+
18
+ function uniqueSorted(values: Iterable<string>): string[] {
19
+ return [...new Set([...values].filter(Boolean))].sort((a, b) => a.localeCompare(b));
20
+ }
21
+
22
+ function mergeReports(base: ContextReport, restored: ContextReport | undefined): ContextReport {
23
+ if (!restored) return base;
24
+ return {
25
+ ...base,
26
+ ...restored,
27
+ toolCalls: { ...base.toolCalls, ...restored.toolCalls },
28
+ promptRewrite: { ...base.promptRewrite, ...restored.promptRewrite },
29
+ sessionRestore: { ...base.sessionRestore, ...restored.sessionRestore },
30
+ };
31
+ }
32
+
33
+ function maxToolCalls(a: ContextReport["toolCalls"], b: ContextReport["toolCalls"]): ContextReport["toolCalls"] {
34
+ return {
35
+ skillIndex: Math.max(a.skillIndex, b.skillIndex),
36
+ skillManifest: Math.max(a.skillManifest, b.skillManifest),
37
+ skillLoad: Math.max(a.skillLoad, b.skillLoad),
38
+ policyManifest: Math.max(a.policyManifest, b.policyManifest),
39
+ policyLoad: Math.max(a.policyLoad, b.policyLoad),
40
+ contextReport: Math.max(a.contextReport, b.contextReport),
41
+ };
42
+ }
43
+
44
+ function incrementTool(calls: ContextReport["toolCalls"], toolName: ToolName): void {
45
+ if (toolName === "skill_index") calls.skillIndex += 1;
46
+ if (toolName === "skill_manifest") calls.skillManifest += 1;
47
+ if (toolName === "skill_load") calls.skillLoad += 1;
48
+ if (toolName === "policy_manifest") calls.policyManifest += 1;
49
+ if (toolName === "policy_load") calls.policyLoad += 1;
50
+ if (toolName === "context_report") calls.contextReport += 1;
51
+ }
52
+
53
+ function getEntryData(entry: unknown): unknown {
54
+ const record = asRecord(entry);
55
+ if (record.type === "custom" && record.customType === SNAPSHOT_TYPE) return record.data;
56
+ const message = asRecord(record.message);
57
+ if (message.role === "custom" && message.customType === SNAPSHOT_TYPE) return message.details ?? message.content;
58
+ return undefined;
59
+ }
60
+
61
+ function getToolResult(entry: unknown): { toolName?: string; details?: Record<string, unknown> } | undefined {
62
+ const record = asRecord(entry);
63
+ if (record.type !== "message") return undefined;
64
+ const message = asRecord(record.message);
65
+ if (message.role !== "toolResult") return undefined;
66
+ return { toolName: typeof message.toolName === "string" ? message.toolName : undefined, details: asRecord(message.details) };
67
+ }
68
+
69
+ function getEntries(ctx: unknown): unknown[] {
70
+ const manager = asRecord(ctx).sessionManager as { getEntries?: () => unknown[]; getBranch?: () => unknown[] } | undefined;
71
+ try {
72
+ return manager?.getEntries?.() ?? manager?.getBranch?.() ?? [];
73
+ } catch {
74
+ return [];
75
+ }
76
+ }
77
+
78
+ function extractSnapshot(data: unknown): ContextReport | undefined {
79
+ const record = asRecord(data);
80
+ const report = asRecord(record.report);
81
+ if (!report || typeof report.timestamp !== "string") return undefined;
82
+ return report as ContextReport;
83
+ }
84
+
85
+ export function restoreReportFromSession(state: ContextManagerState, ctx: unknown): RestoreStats {
86
+ const entries = getEntries(ctx);
87
+ let latestSnapshot: ContextReport | undefined;
88
+ let snapshotCount = 0;
89
+ let toolResultCount = 0;
90
+ const countedToolCalls: ContextReport["toolCalls"] = { skillIndex: 0, skillManifest: 0, skillLoad: 0, policyManifest: 0, policyLoad: 0, contextReport: 0 };
91
+ const loadedSkills = new Set(state.report.loadedByTool);
92
+ const loadedPolicies = new Set(state.report.loadedPolicies);
93
+
94
+ for (const entry of entries) {
95
+ const snapshot = extractSnapshot(getEntryData(entry));
96
+ if (snapshot) {
97
+ snapshotCount += 1;
98
+ if (!latestSnapshot || snapshot.timestamp >= latestSnapshot.timestamp) latestSnapshot = snapshot;
99
+ }
100
+
101
+ const toolResult = getToolResult(entry);
102
+ if (!toolResult?.toolName || !(TOOL_NAMES as readonly string[]).includes(toolResult.toolName)) continue;
103
+ toolResultCount += 1;
104
+ incrementTool(countedToolCalls, toolResult.toolName as ToolName);
105
+ if (toolResult.toolName === "skill_load" && typeof toolResult.details?.skill === "string") loadedSkills.add(toolResult.details.skill);
106
+ if (toolResult.toolName === "policy_load" && Array.isArray(toolResult.details?.loadedPolicies)) {
107
+ for (const policy of toolResult.details.loadedPolicies) if (typeof policy === "string") loadedPolicies.add(policy);
108
+ }
109
+ }
110
+
111
+ state.report = mergeReports(state.report, latestSnapshot);
112
+ state.report.loadedByTool ??= [];
113
+ state.report.loadedPolicies ??= [];
114
+ state.report.blockedActions ??= [];
115
+ state.report.modelRoutingCorrections ??= [];
116
+ state.report.duplicateExtensionWarnings ??= [];
117
+ for (const skill of state.report.loadedByTool) loadedSkills.add(skill);
118
+ for (const policy of state.report.loadedPolicies) loadedPolicies.add(policy);
119
+ state.report.toolCalls = maxToolCalls(state.report.toolCalls, countedToolCalls);
120
+ state.report.loadedByTool = uniqueSorted(loadedSkills);
121
+ state.report.loadedPolicies = uniqueSorted(loadedPolicies);
122
+ state.loadedPolicies = new Set(state.report.loadedPolicies);
123
+ state.report.sessionRestore = {
124
+ attempted: true,
125
+ restored: Boolean(latestSnapshot) || toolResultCount > 0,
126
+ snapshotCount,
127
+ toolResultCount,
128
+ note: latestSnapshot
129
+ ? "Restored from Takomi context-manager snapshots and Pi tool-result history in the current session file."
130
+ : toolResultCount > 0
131
+ ? "Restored from Pi tool-result history in the current session file; no context-manager snapshot was found."
132
+ : "No prior context-manager snapshots or tool results were found in the current session file.",
133
+ };
134
+ return { snapshotCount, toolResultCount };
135
+ }
136
+
137
+ function compactReason(reason: string): string {
138
+ const firstMeaningfulLine = reason.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? reason;
139
+ if (/Loaded policy context:/i.test(reason)) return `${firstMeaningfulLine}\n[policy content omitted from persisted context-manager snapshot]`;
140
+ return reason.length > 1200 ? `${reason.slice(0, 1200)}…\n[truncated in persisted context-manager snapshot]` : reason;
141
+ }
142
+
143
+ function snapshotReport(report: ContextReport): ContextReport {
144
+ return {
145
+ ...report,
146
+ blockedActions: report.blockedActions.map((action) => ({ ...action, reason: compactReason(action.reason) })),
147
+ };
148
+ }
149
+
150
+ export function persistReportSnapshot(pi: ExtensionAPI, state: ContextManagerState, reason: string): void {
151
+ try {
152
+ pi.appendEntry(SNAPSHOT_TYPE, {
153
+ version: 1,
154
+ reason,
155
+ report: snapshotReport(state.report),
156
+ });
157
+ } catch {
158
+ // Persistence should never break agent startup or tool execution.
159
+ }
160
+ }
@@ -1,3 +1,6 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
1
4
  import type { SkillRecord } from "./types";
2
5
 
3
6
  export function normalizeText(value: string): string {
@@ -85,3 +88,120 @@ export function findSkill(skills: Map<string, SkillRecord>, name: string): Skill
85
88
  const matches = sortedSkills(skills).filter((skill) => normalizeName(skill.name).includes(key));
86
89
  return matches.length === 1 ? matches[0] : undefined;
87
90
  }
91
+
92
+ function parseFrontmatter(text: string): Record<string, string> {
93
+ const match = text.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
94
+ if (!match) return {};
95
+ const data: Record<string, string> = {};
96
+ for (const line of match[1].split(/\r?\n/)) {
97
+ const parsed = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
98
+ if (!parsed) continue;
99
+ data[parsed[1]] = parsed[2].replace(/^['\"]|['\"]$/g, "").trim();
100
+ }
101
+ return data;
102
+ }
103
+
104
+ async function pathExists(filePath: string): Promise<boolean> {
105
+ try {
106
+ await stat(filePath);
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ async function readSkillFile(filePath: string): Promise<SkillRecord | undefined> {
114
+ try {
115
+ const text = await readFile(filePath, "utf8");
116
+ const frontmatter = parseFrontmatter(text);
117
+ const name = frontmatter.name?.trim() || path.basename(path.dirname(filePath));
118
+ const description = frontmatter.description?.trim();
119
+ if (!name || !description) return undefined;
120
+ return { name, description, location: filePath, source: "filesystem" };
121
+ } catch {
122
+ return undefined;
123
+ }
124
+ }
125
+
126
+ async function collectSkillFiles(root: string, directMarkdownFiles = false, depth = 0, maxDepth = 8): Promise<string[]> {
127
+ if (depth > maxDepth || !await pathExists(root)) return [];
128
+ let entries: Array<{ name: string; isDirectory(): boolean; isFile(): boolean }> = [];
129
+ try {
130
+ entries = await readdir(root, { withFileTypes: true });
131
+ } catch {
132
+ return [];
133
+ }
134
+
135
+ const files: string[] = [];
136
+ if (await pathExists(path.join(root, "SKILL.md"))) files.push(path.join(root, "SKILL.md"));
137
+ if (directMarkdownFiles && depth === 0) {
138
+ for (const entry of entries) {
139
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) files.push(path.join(root, entry.name));
140
+ }
141
+ }
142
+
143
+ for (const entry of entries) {
144
+ if (!entry.isDirectory()) continue;
145
+ if ([".git", "node_modules"].includes(entry.name)) continue;
146
+ files.push(...await collectSkillFiles(path.join(root, entry.name), false, depth + 1, maxDepth));
147
+ }
148
+ return [...new Set(files)];
149
+ }
150
+
151
+ async function collectPackageSkillRoots(nodeModulesRoot: string): Promise<string[]> {
152
+ if (!await pathExists(nodeModulesRoot)) return [];
153
+ const roots: string[] = [];
154
+ let packages: Array<{ name: string; isDirectory(): boolean }> = [];
155
+ try {
156
+ packages = await readdir(nodeModulesRoot, { withFileTypes: true });
157
+ } catch {
158
+ return [];
159
+ }
160
+ for (const pkg of packages) {
161
+ if (!pkg.isDirectory()) continue;
162
+ if (pkg.name.startsWith("@")) {
163
+ const scopeRoot = path.join(nodeModulesRoot, pkg.name);
164
+ let scoped: Array<{ name: string; isDirectory(): boolean }> = [];
165
+ try {
166
+ scoped = await readdir(scopeRoot, { withFileTypes: true });
167
+ } catch {
168
+ continue;
169
+ }
170
+ for (const scopedPkg of scoped) if (scopedPkg.isDirectory()) roots.push(path.join(scopeRoot, scopedPkg.name, "skills"));
171
+ continue;
172
+ }
173
+ roots.push(path.join(nodeModulesRoot, pkg.name, "skills"));
174
+ }
175
+ return roots;
176
+ }
177
+
178
+ function ancestorSkillRoots(cwd: string): string[] {
179
+ const roots: string[] = [];
180
+ let current = path.resolve(cwd || process.cwd());
181
+ const seen = new Set<string>();
182
+ while (!seen.has(current)) {
183
+ seen.add(current);
184
+ roots.push(path.join(current, ".agents", "skills"));
185
+ const parent = path.dirname(current);
186
+ if (parent === current) break;
187
+ current = parent;
188
+ }
189
+ roots.push(path.resolve(cwd || process.cwd(), ".pi", "skills"));
190
+ return roots;
191
+ }
192
+
193
+ export async function discoverSkillsFromFilesystem(cwd = process.cwd()): Promise<SkillRecord[]> {
194
+ const home = os.homedir();
195
+ const roots = [
196
+ { root: path.join(home, ".pi", "agent", "skills"), directMarkdownFiles: true },
197
+ { root: path.join(home, ".agents", "skills"), directMarkdownFiles: false },
198
+ ...ancestorSkillRoots(cwd).map((root) => ({ root, directMarkdownFiles: root.endsWith(`${path.sep}.pi${path.sep}skills`) })),
199
+ ...(await collectPackageSkillRoots(path.join(home, ".pi", "agent", "npm", "node_modules"))).map((root) => ({ root, directMarkdownFiles: false })),
200
+ ];
201
+ const skillFiles = new Set<string>();
202
+ for (const { root, directMarkdownFiles } of roots) {
203
+ for (const file of await collectSkillFiles(root, directMarkdownFiles)) skillFiles.add(file);
204
+ }
205
+ const skills = await Promise.all([...skillFiles].map(readSkillFile));
206
+ return skills.filter((skill): skill is SkillRecord => Boolean(skill));
207
+ }
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { Type } from "typebox";
5
5
  import type { ContextManagerState } from "./state";
6
- import { findSkill, normalizeName, sortedSkills } from "./skill-registry";
6
+ import { discoverSkillsFromFilesystem, findSkill, mergeSkills, normalizeName, sortedSkills } from "./skill-registry";
7
+ import { persistReportSnapshot, restoreReportFromSession } from "./session-state";
7
8
 
8
9
  function renderSkillIndex(state: ContextManagerState): string {
9
10
  const skills = sortedSkills(state.skills);
@@ -29,6 +30,12 @@ async function loadSkillContent(location: string): Promise<string> {
29
30
  return readFile(location, "utf8");
30
31
  }
31
32
 
33
+ async function ensureSkillsDiscovered(state: ContextManagerState, cwd: string): Promise<void> {
34
+ if (state.skills.size > 0) return;
35
+ state.skills = mergeSkills(await discoverSkillsFromFilesystem(cwd));
36
+ state.report.skillCount = state.skills.size;
37
+ }
38
+
32
39
  export function registerSkillTools(pi: ExtensionAPI, state: ContextManagerState): void {
33
40
  pi.registerTool({
34
41
  name: "skill_index",
@@ -37,8 +44,12 @@ export function registerSkillTools(pi: ExtensionAPI, state: ContextManagerState)
37
44
  promptSnippet: "List available skill names only for progressive skill loading",
38
45
  parameters: Type.Object({}),
39
46
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
47
+ restoreReportFromSession(state, ctx);
48
+ await ensureSkillsDiscovered(state, ctx.cwd);
49
+ state.report.timestamp = new Date().toISOString();
40
50
  state.report.cwd = ctx.cwd;
41
51
  state.report.toolCalls.skillIndex += 1;
52
+ persistReportSnapshot(pi, state, "skill_index");
42
53
  return { content: [{ type: "text", text: renderSkillIndex(state) }], details: { skillCount: state.skills.size } };
43
54
  },
44
55
  });
@@ -50,8 +61,12 @@ export function registerSkillTools(pi: ExtensionAPI, state: ContextManagerState)
50
61
  promptSnippet: "Show selected skill descriptions and locations without full instructions",
51
62
  parameters: Type.Object({ skills: Type.Array(Type.String({ description: "Skill name to inspect" })) }),
52
63
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
64
+ restoreReportFromSession(state, ctx);
65
+ await ensureSkillsDiscovered(state, ctx.cwd);
66
+ state.report.timestamp = new Date().toISOString();
53
67
  state.report.cwd = ctx.cwd;
54
68
  state.report.toolCalls.skillManifest += 1;
69
+ persistReportSnapshot(pi, state, "skill_manifest");
55
70
  return { content: [{ type: "text", text: renderManifest(state, params.skills) }], details: { requested: params.skills } };
56
71
  },
57
72
  });
@@ -63,16 +78,24 @@ export function registerSkillTools(pi: ExtensionAPI, state: ContextManagerState)
63
78
  promptSnippet: "Load full SKILL.md instructions for one selected skill",
64
79
  parameters: Type.Object({ skill: Type.String({ description: "Exact skill name to load" }) }),
65
80
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
81
+ restoreReportFromSession(state, ctx);
82
+ await ensureSkillsDiscovered(state, ctx.cwd);
83
+ state.report.timestamp = new Date().toISOString();
66
84
  state.report.cwd = ctx.cwd;
67
85
  state.report.toolCalls.skillLoad += 1;
68
86
  const skill = findSkill(state.skills, params.skill);
69
- if (!skill?.location) return { content: [{ type: "text", text: renderManifest(state, [params.skill]) }], details: { found: false, requested: params.skill }, isError: true };
87
+ if (!skill?.location) {
88
+ persistReportSnapshot(pi, state, "skill_load_not_found");
89
+ return { content: [{ type: "text", text: renderManifest(state, [params.skill]) }], details: { found: false, requested: params.skill }, isError: true };
90
+ }
70
91
  try {
71
92
  const content = await loadSkillContent(skill.location);
72
- state.report.loadedByTool.push(skill.name);
93
+ state.report.loadedByTool = [...new Set([...state.report.loadedByTool, skill.name])].sort();
94
+ persistReportSnapshot(pi, state, "skill_load");
73
95
  return { content: [{ type: "text", text: [`Skill: ${skill.name}`, `Location: ${skill.location}`, "", content].join("\n") }], details: { found: true, skill: skill.name, location: skill.location } };
74
96
  } catch (error) {
75
97
  const message = error instanceof Error ? error.message : String(error);
98
+ persistReportSnapshot(pi, state, "skill_load_error");
76
99
  return { content: [{ type: "text", text: message }], details: { found: true, skill: skill.name, error: message }, isError: true };
77
100
  }
78
101
  },
@@ -23,7 +23,15 @@ export function createEmptyReport(): ContextReport {
23
23
  editedFiles: [],
24
24
  writtenFiles: [],
25
25
  blockedActions: [],
26
+ modelRoutingCorrections: [],
26
27
  duplicateExtensionWarnings: [],
28
+ sessionRestore: {
29
+ attempted: false,
30
+ restored: false,
31
+ snapshotCount: 0,
32
+ toolResultCount: 0,
33
+ note: "No session restore attempted yet.",
34
+ },
27
35
  promptRewrite: {
28
36
  attempted: false,
29
37
  changed: false,
@@ -63,6 +71,8 @@ export function syncReportLedger(state: ContextManagerState): void {
63
71
  }
64
72
 
65
73
  export function recordBlocked(state: ContextManagerState, toolName: string, reason: string): void {
66
- state.report.blockedActions.push({ toolName, reason, timestamp: new Date().toISOString() });
74
+ const timestamp = new Date().toISOString();
75
+ state.report.timestamp = timestamp;
76
+ state.report.blockedActions.push({ toolName, reason, timestamp });
67
77
  syncReportLedger(state);
68
78
  }
@@ -2,7 +2,7 @@ export type SkillRecord = {
2
2
  name: string;
3
3
  description?: string;
4
4
  location?: string;
5
- source: "systemPromptOptions" | "xml" | "tool";
5
+ source: "systemPromptOptions" | "xml" | "filesystem" | "tool";
6
6
  };
7
7
 
8
8
  export type CandidateContext = {
@@ -57,7 +57,15 @@ export type ContextReport = {
57
57
  editedFiles: string[];
58
58
  writtenFiles: string[];
59
59
  blockedActions: Array<{ toolName: string; reason: string; timestamp: string }>;
60
+ modelRoutingCorrections: Array<{ toolName: string; from: string; to: string; timestamp: string; recovery?: string }>;
60
61
  duplicateExtensionWarnings: Array<{ toolName: string; paths: string[] }>;
62
+ sessionRestore: {
63
+ attempted: boolean;
64
+ restored: boolean;
65
+ snapshotCount: number;
66
+ toolResultCount: number;
67
+ note: string;
68
+ };
61
69
  promptRewrite: {
62
70
  attempted: boolean;
63
71
  changed: boolean;
@@ -32,6 +32,7 @@ const SUBCOMMAND_COMPLETIONS: Record<string, TakomiCompletion[]> = {
32
32
  { value: "auto", label: "auto", description: "Continue approved plans automatically" },
33
33
  ],
34
34
  subagents: [
35
+ { value: "list", label: "list", description: "List available Takomi subagents" },
35
36
  { value: "status", label: "status", description: "Show active subagents" },
36
37
  { value: "on", label: "on", description: "Allow subagent delegation" },
37
38
  { value: "off", label: "off", description: "Disable subagent delegation" },
@@ -95,7 +96,7 @@ export function commandHelp(): string {
95
96
  "/takomi plan [title]",
96
97
  "/takomi mode <direct|orchestrate|review>",
97
98
  "/takomi gate <auto|review>",
98
- "/takomi subagents <on|off|status>",
99
+ "/takomi subagents <list|on|off|status>",
99
100
  "/takomi stats [overview|daily|models|projects|projects-full|sessions|sessions-full|tasks|tasks-full|tools|subagents|sources] [since 7d]",
100
101
  "/takomi routing [show|where]",
101
102
  "/takomi routing <policy text> # updates global policy",
@@ -9,6 +9,7 @@ import { commandHelp, completions, statusText, workflowPrompt } from "./command-
9
9
  import type { TakomiSubagentController } from "./subagent-types";
10
10
  import { previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy, type RoutingPolicyInstallScope } from "./routing-policy";
11
11
  import { collectTakomiStats, renderTakomiStats } from "./takomi-stats.js";
12
+ import { discoverTakomiAgents } from "../takomi-subagents/agents";
12
13
 
13
14
  export type TakomiRuntimeCommandState = {
14
15
  enabled: boolean;
@@ -202,6 +203,17 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
202
203
 
203
204
  async function handleSubagents(ctx: ExtensionCommandContext, action?: string): Promise<void> {
204
205
  const controller = options.subagentController;
206
+ if (action === "list" || action === "agents") {
207
+ const agents = discoverTakomiAgents(ctx.cwd, "both");
208
+ const lines = [
209
+ "Takomi subagents:",
210
+ ...(agents.length
211
+ ? agents.map((agent) => `- ${agent.name} (${agent.source}): ${agent.description}`)
212
+ : ["- (none discovered)"]),
213
+ ];
214
+ ctx.ui.notify(lines.join("\n"), agents.length ? "info" : "warning");
215
+ return;
216
+ }
205
217
  if (action === "on" || action === "off") {
206
218
  await options.updateState(ctx, () => {
207
219
  options.getState().subagentsEnabled = action === "on";
@@ -212,7 +224,7 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
212
224
  ctx.ui.notify(statusText(options.getState(), controller), controller.hasRuns() ? "info" : "warning");
213
225
  return;
214
226
  }
215
- ctx.ui.notify("Usage: /takomi subagents <on|off|status>", "warning");
227
+ ctx.ui.notify("Usage: /takomi subagents <list|on|off|status>", "warning");
216
228
  }
217
229
 
218
230
  pi.registerCommand("takomi", {
@@ -31,8 +31,24 @@ const TaskSchema = Type.Object({
31
31
  checklist: Type.Optional(Type.Array(Type.Union([Type.String(), ChecklistItemSchema]))),
32
32
  });
33
33
 
34
+ const ContextSchema = Type.Union([
35
+ Type.Literal("fresh"),
36
+ Type.Literal("fork"),
37
+ ]);
38
+
39
+ const ActionSchema = Type.Union([
40
+ Type.Literal("list"),
41
+ Type.Literal("get"),
42
+ Type.Literal("models"),
43
+ Type.Literal("status"),
44
+ Type.Literal("interrupt"),
45
+ Type.Literal("resume"),
46
+ Type.Literal("doctor"),
47
+ ]);
48
+
34
49
  const SubagentParameters = Type.Object({
35
- agent: Type.Optional(Type.String({ description: "Agent name for single execution" })),
50
+ action: Type.Optional(ActionSchema),
51
+ agent: Type.Optional(Type.String({ description: "Agent name for single execution, or target agent for get/models actions" })),
36
52
  task: Type.Optional(Type.String({ description: "Task for single execution" })),
37
53
  workflow: Type.Optional(Type.String({ description: "Workflow or playbook overlay for this task" })),
38
54
  skills: Type.Optional(Type.Array(Type.String(), { description: "Extra skills to apply during the task" })),
@@ -47,6 +63,14 @@ const SubagentParameters = Type.Object({
47
63
  previewOnly: Type.Optional(Type.Boolean({ description: "Return the delegation plan without launching" })),
48
64
  clarify: Type.Optional(Type.Boolean({ description: "Show the native pi-subagents TUI to preview/edit before execution. Especially useful for chains; requires an interactive Pi TUI." })),
49
65
  chain: Type.Optional(Type.Array(TaskSchema, { description: "Sequential chain of subagent tasks" })),
66
+ context: Type.Optional(ContextSchema),
67
+ async: Type.Optional(Type.Boolean({ description: "Run through native pi-subagents background/async mode" })),
68
+ concurrency: Type.Optional(Type.Number({ description: "Maximum concurrency for parallel task groups" })),
69
+ worktree: Type.Optional(Type.Boolean({ description: "Use native pi-subagents worktree isolation where supported" })),
70
+ id: Type.Optional(Type.String({ description: "Native async/control run id for status, interrupt, or resume actions" })),
71
+ message: Type.Optional(Type.String({ description: "Follow-up message for resume actions" })),
72
+ index: Type.Optional(Type.Number({ description: "Child index for resume actions when needed" })),
73
+ chainName: Type.Optional(Type.String({ description: "Saved chain name for get actions" })),
50
74
  agentScope: Type.Optional(Type.Union([Type.Literal("user"), Type.Literal("project"), Type.Literal("both")])),
51
75
  // Project-agent confirmation and hard-stop overrides are enforced server-side;
52
76
  // they are intentionally not model-callable parameters.
@@ -56,11 +80,14 @@ function registerSubagentTool(pi: ExtensionAPI): void {
56
80
  pi.registerTool({
57
81
  name: "takomi_subagent",
58
82
  label: "Takomi",
59
- description: "Run subagents with Pi-style single, parallel, or chain modes plus Takomi lifecycle metadata.",
60
- promptSnippet: "Delegate lifecycle-aware Takomi work to specialist subagents. Use single, tasks, or chain; reuse conversationId for review loops.",
83
+ description: "Run subagents with Pi-style single, parallel, chain, async, fork, and management/status modes plus Takomi lifecycle metadata.",
84
+ promptSnippet: "Delegate lifecycle-aware Takomi work to specialist subagents. Use single, tasks, chain, or action=list/status/doctor; reuse conversationId for review loops.",
61
85
  promptGuidelines: [
62
86
  "Use this tool during orchestration when a specialist should handle a task.",
87
+ "Use action=list to discover agents and action=status/doctor for native pi-subagents diagnostics/control instead of raw subagent.",
63
88
  "Use tasks for independent parallel work and chain for dependent handoffs with {previous}.",
89
+ "Set context=fork only when inherited parent-session history is needed; otherwise prefer fresh or the agent default.",
90
+ "Set async=true for long-running work when the parent can continue safely; keep active worktree writes single-threaded unless worktree isolation is enabled.",
64
91
  "Set clarify=true when the user asks to preview/edit a subagent run in the native Pi TUI before launch.",
65
92
  "Use model, fallbackModels, and thinking only when deliberate; otherwise let the agent/profile defaults apply.",
66
93
  "If review sends work back to the same agent, reuse the same conversationId for continuity.",
@@ -171,12 +171,14 @@ export function renderTakomiSubagentResult(result: ToolResult, options: { expand
171
171
  return new Text(renderPolicyGateBlock(text, options.expanded, theme), 0, 0);
172
172
  }
173
173
 
174
+ const native = renderNativeSubagentResult(result, options, theme, context);
175
+ if (options.isPartial && native) return native;
176
+
174
177
  if (options.isPartial) {
175
178
  if (options.expanded) return new Text(livePartialText(result, theme), 0, 0);
176
179
  return new Text(summarizeCollapsedResult(text, status, theme), 0, 0);
177
180
  }
178
181
 
179
- const native = renderNativeSubagentResult(result, options, theme, context);
180
182
  if (native) return native;
181
183
 
182
184
  if (!options.expanded) {