@kata-sh/cli 0.1.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kata Session Forensics — Deep analysis of pi session JSONL files
|
|
3
|
+
*
|
|
4
|
+
* Pi's SessionManager persists every entry to disk via appendFileSync as it
|
|
5
|
+
* happens. When a crash occurs, the session JSONL on disk contains every tool
|
|
6
|
+
* call, every assistant response, and every error up to the moment of death.
|
|
7
|
+
*
|
|
8
|
+
* This module reads that file and reconstructs a structured execution trace
|
|
9
|
+
* that tells the recovering agent exactly what happened, what changed, and
|
|
10
|
+
* where to resume.
|
|
11
|
+
*
|
|
12
|
+
* Used by:
|
|
13
|
+
* - Crash recovery (reading the surviving pi session file)
|
|
14
|
+
* - Stuck-retry diagnostics (reading Kata activity log copies)
|
|
15
|
+
*
|
|
16
|
+
* Entry format (verified against real pi session files):
|
|
17
|
+
* - Tool calls: { type: "toolCall", name: "bash", id: "toolu_...", arguments: { command: "..." } }
|
|
18
|
+
* - Tool results: { role: "toolResult", toolCallId: "toolu_...", toolName: "bash", isError: bool, content: ... }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
22
|
+
import { execSync } from "node:child_process";
|
|
23
|
+
import { basename, join } from "node:path";
|
|
24
|
+
|
|
25
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface ToolCall {
|
|
28
|
+
name: string;
|
|
29
|
+
input: Record<string, unknown>;
|
|
30
|
+
result?: string;
|
|
31
|
+
isError: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ExecutionTrace {
|
|
35
|
+
/** Ordered list of tool calls with results */
|
|
36
|
+
toolCalls: ToolCall[];
|
|
37
|
+
/** Files written or edited (deduplicated, ordered by first occurrence) */
|
|
38
|
+
filesWritten: string[];
|
|
39
|
+
/** Files read (deduplicated) */
|
|
40
|
+
filesRead: string[];
|
|
41
|
+
/** Shell commands executed with exit status */
|
|
42
|
+
commandsRun: { command: string; failed: boolean }[];
|
|
43
|
+
/** Tool errors encountered */
|
|
44
|
+
errors: string[];
|
|
45
|
+
/** The agent's last reasoning / text output before crash */
|
|
46
|
+
lastReasoning: string;
|
|
47
|
+
/** Total tool calls completed (have matching results) */
|
|
48
|
+
toolCallCount: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RecoveryBriefing {
|
|
52
|
+
/** What the agent was doing */
|
|
53
|
+
unitType: string;
|
|
54
|
+
unitId: string;
|
|
55
|
+
/** Structured execution trace */
|
|
56
|
+
trace: ExecutionTrace;
|
|
57
|
+
/** Git state: files modified/added/deleted since unit started */
|
|
58
|
+
gitChanges: string | null;
|
|
59
|
+
/** Formatted prompt section ready for injection */
|
|
60
|
+
prompt: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── JSONL Parsing ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function parseJSONL(raw: string): unknown[] {
|
|
66
|
+
return raw
|
|
67
|
+
.trim()
|
|
68
|
+
.split("\n")
|
|
69
|
+
.map((line) => {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(line);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.filter(Boolean) as unknown[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Find the entries belonging to the last session in a JSONL file.
|
|
81
|
+
* Auto-mode creates a new session per unit, so the last session header
|
|
82
|
+
* marks the start of the crashed unit's entries.
|
|
83
|
+
*/
|
|
84
|
+
function extractLastSession(entries: unknown[]): unknown[] {
|
|
85
|
+
let lastSessionIdx = -1;
|
|
86
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
87
|
+
const entry = entries[i] as Record<string, unknown>;
|
|
88
|
+
if (entry.type === "session") {
|
|
89
|
+
lastSessionIdx = i;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lastSessionIdx >= 0 ? entries.slice(lastSessionIdx) : entries;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Trace Extraction ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract a structured execution trace from raw session entries.
|
|
100
|
+
* Works with both pi session JSONL and Kata activity log JSONL.
|
|
101
|
+
*/
|
|
102
|
+
export function extractTrace(entries: unknown[]): ExecutionTrace {
|
|
103
|
+
const toolCalls: ToolCall[] = [];
|
|
104
|
+
const filesWritten: string[] = [];
|
|
105
|
+
const filesRead: string[] = [];
|
|
106
|
+
const commandsRun: { command: string; failed: boolean }[] = [];
|
|
107
|
+
const errors: string[] = [];
|
|
108
|
+
let lastReasoning = "";
|
|
109
|
+
|
|
110
|
+
// Track pending tool calls by ID for matching with results
|
|
111
|
+
const pendingTools = new Map<
|
|
112
|
+
string,
|
|
113
|
+
{ name: string; input: Record<string, unknown> }
|
|
114
|
+
>();
|
|
115
|
+
|
|
116
|
+
const seenWritten = new Set<string>();
|
|
117
|
+
const seenRead = new Set<string>();
|
|
118
|
+
|
|
119
|
+
for (const raw of entries) {
|
|
120
|
+
const entry = raw as Record<string, unknown>;
|
|
121
|
+
if (entry.type !== "message" || !entry.message) continue;
|
|
122
|
+
const msg = entry.message as Record<string, unknown>;
|
|
123
|
+
|
|
124
|
+
// ── Assistant messages: tool calls + reasoning ──
|
|
125
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
126
|
+
for (const part of msg.content as Record<string, unknown>[]) {
|
|
127
|
+
// Text reasoning
|
|
128
|
+
if (part.type === "text" && part.text) {
|
|
129
|
+
lastReasoning = String(part.text);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Tool call initiation
|
|
133
|
+
// Pi format: { type: "toolCall", name: "bash", id: "toolu_...", arguments: { command: "..." } }
|
|
134
|
+
if (part.type === "toolCall") {
|
|
135
|
+
const name = String(part.name || "unknown").toLowerCase();
|
|
136
|
+
const input = (part.arguments || part.input || {}) as Record<
|
|
137
|
+
string,
|
|
138
|
+
unknown
|
|
139
|
+
>;
|
|
140
|
+
const id = String(part.id || "");
|
|
141
|
+
|
|
142
|
+
if (id) {
|
|
143
|
+
pendingTools.set(id, { name, input });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Track file operations
|
|
147
|
+
const path = input.path ? String(input.path) : null;
|
|
148
|
+
if (path) {
|
|
149
|
+
if (name === "write" || name === "edit") {
|
|
150
|
+
if (!seenWritten.has(path)) {
|
|
151
|
+
seenWritten.add(path);
|
|
152
|
+
filesWritten.push(path);
|
|
153
|
+
}
|
|
154
|
+
} else if (name === "read") {
|
|
155
|
+
if (!seenRead.has(path)) {
|
|
156
|
+
seenRead.add(path);
|
|
157
|
+
filesRead.push(path);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Track shell commands
|
|
163
|
+
if ((name === "bash" || name === "bg_shell") && input.command) {
|
|
164
|
+
commandsRun.push({ command: String(input.command), failed: false });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Tool results: match with pending calls ──
|
|
171
|
+
// Pi format: { role: "toolResult", toolCallId: "toolu_...", toolName: "bash", isError: bool, content: ... }
|
|
172
|
+
if (msg.role === "toolResult") {
|
|
173
|
+
const id = String(msg.toolCallId || "");
|
|
174
|
+
const isError = !!msg.isError;
|
|
175
|
+
const resultText = extractResultText(msg);
|
|
176
|
+
|
|
177
|
+
const pending = pendingTools.get(id);
|
|
178
|
+
if (pending) {
|
|
179
|
+
toolCalls.push({
|
|
180
|
+
name: pending.name,
|
|
181
|
+
input: redactInput(pending.name, pending.input),
|
|
182
|
+
result: resultText.slice(0, 500),
|
|
183
|
+
isError,
|
|
184
|
+
});
|
|
185
|
+
pendingTools.delete(id);
|
|
186
|
+
|
|
187
|
+
// Mark failed commands
|
|
188
|
+
if (
|
|
189
|
+
isError &&
|
|
190
|
+
(pending.name === "bash" || pending.name === "bg_shell")
|
|
191
|
+
) {
|
|
192
|
+
const lastCmd = findLast(
|
|
193
|
+
commandsRun,
|
|
194
|
+
(c) => c.command === String(pending.input.command),
|
|
195
|
+
);
|
|
196
|
+
if (lastCmd) lastCmd.failed = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (isError && resultText) {
|
|
201
|
+
errors.push(resultText.slice(0, 300));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Flush any pending tool calls that never got results (crash mid-tool)
|
|
207
|
+
for (const [, pending] of pendingTools) {
|
|
208
|
+
toolCalls.push({
|
|
209
|
+
name: pending.name,
|
|
210
|
+
input: redactInput(pending.name, pending.input),
|
|
211
|
+
isError: false,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
toolCalls,
|
|
217
|
+
filesWritten,
|
|
218
|
+
filesRead,
|
|
219
|
+
commandsRun,
|
|
220
|
+
errors,
|
|
221
|
+
lastReasoning: lastReasoning.slice(-600).trim(),
|
|
222
|
+
toolCallCount: toolCalls.length,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─── Git State ────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
function getGitChanges(basePath: string): string | null {
|
|
229
|
+
try {
|
|
230
|
+
const status = execSync("git status --porcelain", {
|
|
231
|
+
cwd: basePath,
|
|
232
|
+
stdio: "pipe",
|
|
233
|
+
})
|
|
234
|
+
.toString()
|
|
235
|
+
.trim();
|
|
236
|
+
if (!status) return null;
|
|
237
|
+
|
|
238
|
+
const diffStat = execSync("git diff --stat HEAD 2>/dev/null || true", {
|
|
239
|
+
cwd: basePath,
|
|
240
|
+
stdio: "pipe",
|
|
241
|
+
})
|
|
242
|
+
.toString()
|
|
243
|
+
.trim();
|
|
244
|
+
const stagedStat = execSync(
|
|
245
|
+
"git diff --stat --cached HEAD 2>/dev/null || true",
|
|
246
|
+
{ cwd: basePath, stdio: "pipe" },
|
|
247
|
+
)
|
|
248
|
+
.toString()
|
|
249
|
+
.trim();
|
|
250
|
+
|
|
251
|
+
const parts: string[] = [];
|
|
252
|
+
if (status) parts.push(`Status:\n${status}`);
|
|
253
|
+
if (stagedStat) parts.push(`Staged:\n${stagedStat}`);
|
|
254
|
+
if (diffStat) parts.push(`Unstaged:\n${diffStat}`);
|
|
255
|
+
return parts.join("\n\n");
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Recovery Briefing ────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Synthesize a full crash recovery briefing.
|
|
265
|
+
*
|
|
266
|
+
* Reads the surviving pi session file (or falls back to the last Kata activity
|
|
267
|
+
* log), deep-parses it into an execution trace, combines with git state, and
|
|
268
|
+
* formats a structured prompt section ready for injection.
|
|
269
|
+
*/
|
|
270
|
+
export function synthesizeCrashRecovery(
|
|
271
|
+
basePath: string,
|
|
272
|
+
unitType: string,
|
|
273
|
+
unitId: string,
|
|
274
|
+
sessionFile?: string,
|
|
275
|
+
activityDir?: string,
|
|
276
|
+
): RecoveryBriefing | null {
|
|
277
|
+
try {
|
|
278
|
+
let trace: ExecutionTrace | null = null;
|
|
279
|
+
|
|
280
|
+
// Primary source: surviving pi session file
|
|
281
|
+
if (sessionFile && existsSync(sessionFile)) {
|
|
282
|
+
const raw = readFileSync(sessionFile, "utf-8");
|
|
283
|
+
const allEntries = parseJSONL(raw);
|
|
284
|
+
const sessionEntries = extractLastSession(allEntries);
|
|
285
|
+
trace = extractTrace(sessionEntries);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Fallback: last Kata activity log
|
|
289
|
+
if (!trace || trace.toolCallCount === 0) {
|
|
290
|
+
const fallbackTrace = readLastActivityLog(activityDir);
|
|
291
|
+
if (fallbackTrace && fallbackTrace.toolCallCount > 0) {
|
|
292
|
+
trace = fallbackTrace;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// If no trace from either source, still provide git state
|
|
297
|
+
if (!trace) {
|
|
298
|
+
trace = {
|
|
299
|
+
toolCalls: [],
|
|
300
|
+
filesWritten: [],
|
|
301
|
+
filesRead: [],
|
|
302
|
+
commandsRun: [],
|
|
303
|
+
errors: [],
|
|
304
|
+
lastReasoning: "",
|
|
305
|
+
toolCallCount: 0,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const gitChanges = getGitChanges(basePath);
|
|
310
|
+
const prompt = formatRecoveryPrompt(unitType, unitId, trace, gitChanges);
|
|
311
|
+
|
|
312
|
+
return { unitType, unitId, trace, gitChanges, prompt };
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Deep diagnostic from any JSONL source (activity log or session file).
|
|
320
|
+
* Replaces the old shallow getLastActivityDiagnostic().
|
|
321
|
+
*/
|
|
322
|
+
export function getDeepDiagnostic(basePath: string): string | null {
|
|
323
|
+
const activityDir = join(basePath, ".kata", "activity");
|
|
324
|
+
const trace = readLastActivityLog(activityDir);
|
|
325
|
+
if (!trace || trace.toolCallCount === 0) return null;
|
|
326
|
+
return formatTraceSummary(trace);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Formatting ───────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
function formatRecoveryPrompt(
|
|
332
|
+
unitType: string,
|
|
333
|
+
unitId: string,
|
|
334
|
+
trace: ExecutionTrace,
|
|
335
|
+
gitChanges: string | null,
|
|
336
|
+
): string {
|
|
337
|
+
const sections: string[] = [];
|
|
338
|
+
|
|
339
|
+
sections.push(
|
|
340
|
+
"## Crash Recovery Briefing",
|
|
341
|
+
"",
|
|
342
|
+
`You are resuming \`${unitType}\` for \`${unitId}\` after a crash.`,
|
|
343
|
+
`The previous session completed **${trace.toolCallCount} tool calls** before dying.`,
|
|
344
|
+
"Use this briefing to pick up exactly where it left off. Do NOT redo completed work.",
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Tool call trace — compact summary
|
|
348
|
+
if (trace.toolCalls.length > 0) {
|
|
349
|
+
sections.push("", "### Completed Tool Calls");
|
|
350
|
+
const summary = compressToolCallTrace(trace.toolCalls);
|
|
351
|
+
sections.push(summary);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Files written
|
|
355
|
+
if (trace.filesWritten.length > 0) {
|
|
356
|
+
sections.push(
|
|
357
|
+
"",
|
|
358
|
+
"### Files Already Written/Edited",
|
|
359
|
+
...trace.filesWritten.map((f) => `- \`${f}\``),
|
|
360
|
+
"",
|
|
361
|
+
"These files exist on disk from the previous run. Verify they look correct before continuing.",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Commands run
|
|
366
|
+
const significantCommands = trace.commandsRun.filter(
|
|
367
|
+
(c) => !c.command.startsWith("git ") || c.failed,
|
|
368
|
+
);
|
|
369
|
+
if (significantCommands.length > 0) {
|
|
370
|
+
sections.push("", "### Commands Already Run");
|
|
371
|
+
for (const c of significantCommands.slice(-10)) {
|
|
372
|
+
const status = c.failed ? " ❌" : " ✓";
|
|
373
|
+
sections.push(`- \`${truncate(c.command, 120)}\`${status}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Errors
|
|
378
|
+
if (trace.errors.length > 0) {
|
|
379
|
+
sections.push(
|
|
380
|
+
"",
|
|
381
|
+
"### Errors Before Crash",
|
|
382
|
+
...trace.errors.slice(-3).map((e) => `- ${truncate(e, 200)}`),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Git state
|
|
387
|
+
if (gitChanges) {
|
|
388
|
+
sections.push(
|
|
389
|
+
"",
|
|
390
|
+
"### Current Git State (filesystem truth)",
|
|
391
|
+
"```",
|
|
392
|
+
gitChanges,
|
|
393
|
+
"```",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Last reasoning
|
|
398
|
+
if (trace.lastReasoning) {
|
|
399
|
+
sections.push(
|
|
400
|
+
"",
|
|
401
|
+
"### Last Agent Reasoning Before Crash",
|
|
402
|
+
`> ${trace.lastReasoning.replace(/\n/g, "\n> ")}`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
sections.push(
|
|
407
|
+
"",
|
|
408
|
+
"### Resume Instructions",
|
|
409
|
+
"1. Check the task plan for remaining work",
|
|
410
|
+
"2. Verify files listed above exist and look correct on disk",
|
|
411
|
+
"3. Continue from where the previous session left off",
|
|
412
|
+
"4. Do NOT re-read files or re-run commands that already succeeded above",
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return sections.join("\n");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Compress a tool call trace into a readable summary.
|
|
420
|
+
* Groups consecutive reads, shows write/edit/bash individually.
|
|
421
|
+
*/
|
|
422
|
+
function compressToolCallTrace(calls: ToolCall[]): string {
|
|
423
|
+
const lines: string[] = [];
|
|
424
|
+
let readBatch: string[] = [];
|
|
425
|
+
|
|
426
|
+
function flushReads() {
|
|
427
|
+
if (readBatch.length === 0) return;
|
|
428
|
+
if (readBatch.length <= 2) {
|
|
429
|
+
for (const path of readBatch) lines.push(` read \`${path}\``);
|
|
430
|
+
} else {
|
|
431
|
+
lines.push(
|
|
432
|
+
` read ${readBatch.length} files: ${readBatch.map((p) => `\`${basename(p)}\``).join(", ")}`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
readBatch = [];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (let i = 0; i < calls.length; i++) {
|
|
439
|
+
const call = calls[i]!;
|
|
440
|
+
const num = i + 1;
|
|
441
|
+
|
|
442
|
+
if (call.name === "read" && call.input.path) {
|
|
443
|
+
readBatch.push(String(call.input.path));
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
flushReads();
|
|
448
|
+
|
|
449
|
+
const err = call.isError ? " ❌" : "";
|
|
450
|
+
|
|
451
|
+
if (call.name === "write" || call.name === "edit") {
|
|
452
|
+
lines.push(`${num}. ${call.name} \`${call.input.path || "?"}\`${err}`);
|
|
453
|
+
} else if (call.name === "bash" || call.name === "bg_shell") {
|
|
454
|
+
const cmd = truncate(String(call.input.command || ""), 80);
|
|
455
|
+
lines.push(`${num}. ${call.name}: \`${cmd}\`${err}`);
|
|
456
|
+
} else {
|
|
457
|
+
lines.push(`${num}. ${call.name}${err}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
flushReads();
|
|
462
|
+
return lines.join("\n");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function formatTraceSummary(trace: ExecutionTrace): string {
|
|
466
|
+
const parts: string[] = [];
|
|
467
|
+
parts.push(`Tool calls completed: ${trace.toolCallCount}`);
|
|
468
|
+
|
|
469
|
+
if (trace.filesWritten.length > 0) {
|
|
470
|
+
parts.push(
|
|
471
|
+
`Files written: ${trace.filesWritten.map((f) => `\`${f}\``).join(", ")}`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (trace.commandsRun.length > 0) {
|
|
475
|
+
const cmds = trace.commandsRun
|
|
476
|
+
.slice(-5)
|
|
477
|
+
.map((c) => `\`${truncate(c.command, 80)}\`${c.failed ? " ❌" : ""}`);
|
|
478
|
+
parts.push(`Commands run: ${cmds.join(", ")}`);
|
|
479
|
+
}
|
|
480
|
+
if (trace.errors.length > 0) {
|
|
481
|
+
parts.push(`Errors: ${trace.errors.slice(-3).join("; ")}`);
|
|
482
|
+
}
|
|
483
|
+
if (trace.lastReasoning) {
|
|
484
|
+
parts.push(`Last reasoning: "${trace.lastReasoning}"`);
|
|
485
|
+
}
|
|
486
|
+
return parts.join("\n");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
function readLastActivityLog(activityDir?: string): ExecutionTrace | null {
|
|
492
|
+
if (!activityDir) return null;
|
|
493
|
+
try {
|
|
494
|
+
if (!existsSync(activityDir)) return null;
|
|
495
|
+
const files = readdirSync(activityDir)
|
|
496
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
497
|
+
.sort();
|
|
498
|
+
if (files.length === 0) return null;
|
|
499
|
+
|
|
500
|
+
const lastFile = files[files.length - 1]!;
|
|
501
|
+
const raw = readFileSync(join(activityDir, lastFile), "utf-8");
|
|
502
|
+
return extractTrace(parseJSONL(raw));
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function extractResultText(msg: Record<string, unknown>): string {
|
|
509
|
+
const content = msg.content;
|
|
510
|
+
if (typeof content === "string") return content;
|
|
511
|
+
if (Array.isArray(content)) {
|
|
512
|
+
return content
|
|
513
|
+
.filter((p: Record<string, unknown>) => p.type === "text")
|
|
514
|
+
.map((p: Record<string, unknown>) => String(p.text || ""))
|
|
515
|
+
.join(" ");
|
|
516
|
+
}
|
|
517
|
+
return "";
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Redact sensitive fields from tool inputs.
|
|
522
|
+
* Keep paths and commands, drop large content bodies.
|
|
523
|
+
*/
|
|
524
|
+
function redactInput(
|
|
525
|
+
name: string,
|
|
526
|
+
input: Record<string, unknown>,
|
|
527
|
+
): Record<string, unknown> {
|
|
528
|
+
const safe: Record<string, unknown> = {};
|
|
529
|
+
for (const [key, value] of Object.entries(input)) {
|
|
530
|
+
if (key === "content" || key === "oldText" || key === "newText") {
|
|
531
|
+
safe[key] =
|
|
532
|
+
typeof value === "string" ? truncate(value, 100) : "[redacted]";
|
|
533
|
+
} else {
|
|
534
|
+
safe[key] = value;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return safe;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Array.findLast polyfill for older Node versions */
|
|
541
|
+
function findLast<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
|
|
542
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
543
|
+
if (predicate(arr[i]!)) return arr[i];
|
|
544
|
+
}
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function truncate(s: string, max: number): string {
|
|
549
|
+
return s.length > max ? s.slice(0, max) + "…" : s;
|
|
550
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kata Skill Discovery
|
|
3
|
+
*
|
|
4
|
+
* Detects skills installed during auto-mode by comparing the current
|
|
5
|
+
* skills directory against a snapshot taken at auto-mode start.
|
|
6
|
+
*
|
|
7
|
+
* New skills are injected into the system prompt via before_agent_start,
|
|
8
|
+
* making them visible to all subsequent units without requiring a reload.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
|
|
15
|
+
const SKILLS_DIR = join(getAgentDir(), "skills");
|
|
16
|
+
|
|
17
|
+
export interface DiscoveredSkill {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
location: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Snapshot of skill names at auto-mode start */
|
|
24
|
+
let baselineSkills: Set<string> | null = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Snapshot the current skills directory. Call at auto-mode start.
|
|
28
|
+
*/
|
|
29
|
+
export function snapshotSkills(): void {
|
|
30
|
+
baselineSkills = new Set(listSkillDirs());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clear the snapshot. Call when auto-mode stops.
|
|
35
|
+
*/
|
|
36
|
+
export function clearSkillSnapshot(): void {
|
|
37
|
+
baselineSkills = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a snapshot is active (auto-mode is running with discovery).
|
|
42
|
+
*/
|
|
43
|
+
export function hasSkillSnapshot(): boolean {
|
|
44
|
+
return baselineSkills !== null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Detect skills installed since the snapshot was taken.
|
|
49
|
+
* Returns skill metadata for any new skills found.
|
|
50
|
+
*/
|
|
51
|
+
export function detectNewSkills(): DiscoveredSkill[] {
|
|
52
|
+
if (!baselineSkills) return [];
|
|
53
|
+
|
|
54
|
+
const current = listSkillDirs();
|
|
55
|
+
const newSkills: DiscoveredSkill[] = [];
|
|
56
|
+
|
|
57
|
+
for (const dir of current) {
|
|
58
|
+
if (baselineSkills.has(dir)) continue;
|
|
59
|
+
|
|
60
|
+
const skillMdPath = join(SKILLS_DIR, dir, "SKILL.md");
|
|
61
|
+
if (!existsSync(skillMdPath)) continue;
|
|
62
|
+
|
|
63
|
+
const meta = parseSkillFrontmatter(skillMdPath);
|
|
64
|
+
if (meta) {
|
|
65
|
+
newSkills.push({
|
|
66
|
+
name: meta.name || dir,
|
|
67
|
+
description: meta.description || `Skill: ${dir}`,
|
|
68
|
+
location: skillMdPath,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return newSkills;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format discovered skills as an XML block matching pi's <available_skills> format.
|
|
78
|
+
* This can be appended to the system prompt so the LLM sees them naturally.
|
|
79
|
+
*/
|
|
80
|
+
export function formatSkillsXml(skills: DiscoveredSkill[]): string {
|
|
81
|
+
if (skills.length === 0) return "";
|
|
82
|
+
|
|
83
|
+
const entries = skills.map(s => ` <skill>
|
|
84
|
+
<name>${escapeXml(s.name)}</name>
|
|
85
|
+
<description>${escapeXml(s.description)}</description>
|
|
86
|
+
<location>${escapeXml(s.location)}</location>
|
|
87
|
+
</skill>`).join("\n");
|
|
88
|
+
|
|
89
|
+
return `\n<newly_discovered_skills>
|
|
90
|
+
The following skills were installed during this auto-mode session.
|
|
91
|
+
Use the read tool to load a skill's file when the task matches its description.
|
|
92
|
+
|
|
93
|
+
${entries}
|
|
94
|
+
</newly_discovered_skills>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Internals ────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function listSkillDirs(): string[] {
|
|
100
|
+
if (!existsSync(SKILLS_DIR)) return [];
|
|
101
|
+
try {
|
|
102
|
+
return readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
103
|
+
.filter(d => d.isDirectory())
|
|
104
|
+
.map(d => d.name);
|
|
105
|
+
} catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseSkillFrontmatter(path: string): { name?: string; description?: string } | null {
|
|
111
|
+
try {
|
|
112
|
+
const content = readFileSync(path, "utf-8");
|
|
113
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
114
|
+
if (!match) return null;
|
|
115
|
+
|
|
116
|
+
const fm = match[1];
|
|
117
|
+
const result: { name?: string; description?: string } = {};
|
|
118
|
+
|
|
119
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
120
|
+
if (nameMatch) result.name = nameMatch[1].trim();
|
|
121
|
+
|
|
122
|
+
const descMatch = fm.match(/^description:\s*(.+)$/m);
|
|
123
|
+
if (descMatch) result.description = descMatch[1].trim();
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function escapeXml(text: string): string {
|
|
132
|
+
return text
|
|
133
|
+
.replace(/&/g, "&")
|
|
134
|
+
.replace(/</g, "<")
|
|
135
|
+
.replace(/>/g, ">")
|
|
136
|
+
.replace(/"/g, """);
|
|
137
|
+
}
|