@oh-my-pi/pi-coding-agent 3.24.0 → 3.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Generate memorable two-word task identifiers.
3
+ * Format: AdjectiveNoun (e.g., "SwiftFalcon", "CalmPanda")
4
+ *
5
+ * Dictionaries sourced from unique-names-generator (MIT license).
6
+ * 1202 adjectives × 355 animals = 426,710 combinations.
7
+ */
8
+
9
+ const ADJECTIVES = [
10
+ "able", "above", "absent", "absolute", "abstract", "abundant", "academic", "acceptable",
11
+ "accepted", "accessible", "accurate", "accused", "active", "actual", "acute", "added",
12
+ "additional", "adequate", "adjacent", "administrative", "adorable", "advanced", "adverse",
13
+ "advisory", "aesthetic", "afraid", "aggregate", "aggressive", "agreeable", "agreed",
14
+ "agricultural", "alert", "alive", "alleged", "allied", "alone", "alright", "alternative",
15
+ "amateur", "amazing", "ambitious", "amused", "ancient", "angry", "annoyed", "annual",
16
+ "anonymous", "anxious", "appalling", "apparent", "applicable", "appropriate", "arbitrary",
17
+ "architectural", "armed", "arrogant", "artificial", "artistic", "ashamed", "asleep",
18
+ "assistant", "associated", "atomic", "attractive", "automatic", "autonomous", "available",
19
+ "average", "awake", "aware", "awful", "awkward", "back", "bad", "balanced", "bare", "basic",
20
+ "beautiful", "beneficial", "better", "bewildered", "big", "binding", "biological", "bitter",
21
+ "bizarre", "blank", "blind", "blonde", "bloody", "blushing", "boiling", "bold", "bored",
22
+ "boring", "bottom", "brainy", "brave", "breakable", "breezy", "brief", "bright", "brilliant",
23
+ "broad", "broken", "bumpy", "burning", "busy", "calm", "capable", "capitalist", "careful",
24
+ "casual", "causal", "cautious", "central", "certain", "changing", "characteristic", "charming",
25
+ "cheap", "cheerful", "chemical", "chief", "chilly", "chosen", "chronic", "chubby", "circular",
26
+ "civic", "civil", "civilian", "classic", "classical", "clean", "clear", "clever", "clinical",
27
+ "close", "closed", "cloudy", "clumsy", "coastal", "cognitive", "coherent", "cold", "collective",
28
+ "colonial", "colorful", "colossal", "coloured", "colourful", "combative", "combined",
29
+ "comfortable", "coming", "commercial", "common", "communist", "compact", "comparable",
30
+ "comparative", "compatible", "competent", "competitive", "complete", "complex", "complicated",
31
+ "comprehensive", "compulsory", "conceptual", "concerned", "concrete", "condemned", "confident",
32
+ "confidential", "confused", "conscious", "conservation", "conservative", "considerable",
33
+ "consistent", "constant", "constitutional", "contemporary", "content", "continental",
34
+ "continued", "continuing", "continuous", "controlled", "controversial", "convenient",
35
+ "conventional", "convinced", "convincing", "cooing", "cool", "cooperative", "corporate",
36
+ "correct", "corresponding", "costly", "courageous", "crazy", "creative", "creepy", "criminal",
37
+ "critical", "crooked", "crowded", "crucial", "crude", "cruel", "cuddly", "cultural", "curious",
38
+ "curly", "current", "curved", "cute", "daily", "damaged", "damp", "dangerous", "dark", "dead",
39
+ "deaf", "deafening", "dear", "decent", "decisive", "deep", "defeated", "defensive", "defiant",
40
+ "definite", "deliberate", "delicate", "delicious", "delighted", "delightful", "democratic",
41
+ "dependent", "depressed", "desirable", "desperate", "detailed", "determined", "developed",
42
+ "developing", "devoted", "different", "difficult", "digital", "diplomatic", "direct", "dirty",
43
+ "disabled", "disappointed", "disastrous", "disciplinary", "disgusted", "distant", "distinct",
44
+ "distinctive", "distinguished", "disturbed", "disturbing", "diverse", "divine", "dizzy",
45
+ "domestic", "dominant", "double", "doubtful", "drab", "dramatic", "dreadful", "driving",
46
+ "drunk", "dry", "dual", "due", "dull", "dusty", "dying", "dynamic", "eager", "early", "eastern",
47
+ "easy", "economic", "educational", "eerie", "effective", "efficient", "elaborate", "elated",
48
+ "elderly", "eldest", "electoral", "electric", "electrical", "electronic", "elegant", "eligible",
49
+ "embarrassed", "embarrassing", "emotional", "empirical", "empty", "enchanting", "encouraging",
50
+ "endless", "energetic", "enormous", "enthusiastic", "entire", "entitled", "envious",
51
+ "environmental", "equal", "equivalent", "essential", "established", "estimated", "ethical",
52
+ "ethnic", "eventual", "everyday", "evident", "evil", "evolutionary", "exact", "excellent",
53
+ "exceptional", "excess", "excessive", "excited", "exciting", "exclusive", "existing", "exotic",
54
+ "expected", "expensive", "experienced", "experimental", "explicit", "extended", "extensive",
55
+ "external", "extra", "extraordinary", "extreme", "exuberant", "faint", "fair", "faithful",
56
+ "familiar", "famous", "fancy", "fantastic", "far", "fascinating", "fashionable", "fast", "fat",
57
+ "fatal", "favourable", "favourite", "federal", "fellow", "female", "feminist", "few", "fierce",
58
+ "filthy", "final", "financial", "fine", "firm", "fiscal", "fit", "fixed", "flaky", "flat",
59
+ "flexible", "fluffy", "fluttering", "flying", "following", "fond", "foolish", "foreign",
60
+ "formal", "formidable", "forthcoming", "fortunate", "forward", "fragile", "frail", "frantic",
61
+ "free", "frequent", "fresh", "friendly", "frightened", "front", "frozen", "full", "fun",
62
+ "functional", "fundamental", "funny", "furious", "future", "fuzzy", "gastric", "gay", "general",
63
+ "generous", "genetic", "gentle", "genuine", "geographical", "giant", "gigantic", "given",
64
+ "glad", "glamorous", "gleaming", "global", "glorious", "golden", "good", "gorgeous", "gothic",
65
+ "governing", "graceful", "gradual", "grand", "grateful", "greasy", "great", "grieving", "grim",
66
+ "gross", "grotesque", "growing", "grubby", "grumpy", "guilty", "handicapped", "handsome",
67
+ "happy", "hard", "harsh", "head", "healthy", "heavy", "helpful", "helpless", "hidden", "high",
68
+ "hilarious", "hissing", "historic", "historical", "hollow", "holy", "homeless", "homely",
69
+ "honest", "horizontal", "horrible", "hostile", "hot", "huge", "human", "hungry", "hurt",
70
+ "hushed", "husky", "icy", "ideal", "identical", "ideological", "ill", "illegal", "imaginative",
71
+ "immediate", "immense", "imperial", "implicit", "important", "impossible", "impressed",
72
+ "impressive", "improved", "inadequate", "inappropriate", "inclined", "increased", "increasing",
73
+ "incredible", "independent", "indirect", "individual", "industrial", "inevitable", "influential",
74
+ "informal", "inherent", "initial", "injured", "inland", "inner", "innocent", "innovative",
75
+ "inquisitive", "instant", "institutional", "insufficient", "intact", "integral", "integrated",
76
+ "intellectual", "intelligent", "intense", "intensive", "interested", "interesting", "interim",
77
+ "interior", "intermediate", "internal", "international", "intimate", "invisible", "involved",
78
+ "irrelevant", "isolated", "itchy", "jealous", "jittery", "joint", "jolly", "joyous", "judicial",
79
+ "juicy", "junior", "just", "keen", "key", "kind", "known", "labour", "large", "late", "latin",
80
+ "lazy", "leading", "left", "legal", "legislative", "legitimate", "lengthy", "lesser", "level",
81
+ "lexical", "liable", "liberal", "light", "like", "likely", "limited", "linear", "linguistic",
82
+ "liquid", "literary", "little", "live", "lively", "living", "local", "logical", "lonely",
83
+ "long", "loose", "lost", "loud", "lovely", "low", "loyal", "lucky", "mad", "magic", "magnetic",
84
+ "magnificent", "main", "major", "male", "mammoth", "managerial", "managing", "manual", "many",
85
+ "marginal", "marine", "marked", "married", "marvellous", "marxist", "mass", "massive",
86
+ "mathematical", "mature", "maximum", "mean", "meaningful", "mechanical", "medical", "medieval",
87
+ "melodic", "melted", "mental", "mere", "metropolitan", "mid", "middle", "mighty", "mild",
88
+ "military", "miniature", "minimal", "minimum", "ministerial", "minor", "miserable", "misleading",
89
+ "missing", "misty", "mixed", "moaning", "mobile", "moderate", "modern", "modest", "molecular",
90
+ "monetary", "monthly", "moral", "motionless", "muddy", "multiple", "mushy", "musical", "mute",
91
+ "mutual", "mysterious", "naked", "narrow", "nasty", "national", "native", "natural", "naughty",
92
+ "naval", "near", "nearby", "neat", "necessary", "negative", "neighbouring", "nervous", "net",
93
+ "neutral", "new", "nice", "noble", "noisy", "normal", "northern", "nosy", "notable", "novel",
94
+ "nuclear", "numerous", "nursing", "nutritious", "nutty", "obedient", "objective", "obliged",
95
+ "obnoxious", "obvious", "occasional", "occupational", "odd", "official", "old", "olympic",
96
+ "only", "open", "operational", "opposite", "optimistic", "oral", "ordinary", "organic",
97
+ "organisational", "original", "orthodox", "other", "outdoor", "outer", "outrageous", "outside",
98
+ "outstanding", "overall", "overseas", "overwhelming", "painful", "pale", "panicky", "parallel",
99
+ "parental", "parliamentary", "partial", "particular", "passing", "passive", "past", "patient",
100
+ "payable", "peaceful", "peculiar", "perfect", "permanent", "persistent", "personal", "petite",
101
+ "philosophical", "physical", "plain", "planned", "plastic", "pleasant", "pleased", "poised",
102
+ "polite", "political", "poor", "popular", "positive", "possible", "potential", "powerful",
103
+ "practical", "precious", "precise", "preferred", "pregnant", "preliminary", "premier",
104
+ "prepared", "present", "presidential", "pretty", "previous", "prickly", "primary", "prime",
105
+ "primitive", "principal", "printed", "prior", "private", "probable", "productive", "professional",
106
+ "profitable", "profound", "progressive", "prominent", "promising", "proper", "proposed",
107
+ "prospective", "protective", "proud", "provincial", "psychiatric", "psychological", "public",
108
+ "puny", "pure", "purring", "puzzled", "quaint", "qualified", "quarrelsome", "querulous",
109
+ "quick", "quickest", "quiet", "quintessential", "quixotic", "racial", "radical", "rainy",
110
+ "random", "rapid", "rare", "raspy", "rational", "ratty", "raw", "ready", "real", "realistic",
111
+ "rear", "reasonable", "recent", "reduced", "redundant", "regional", "registered", "regular",
112
+ "regulatory", "related", "relative", "relaxed", "relevant", "reliable", "relieved", "religious",
113
+ "reluctant", "remaining", "remarkable", "remote", "renewed", "representative", "repulsive",
114
+ "required", "resident", "residential", "resonant", "respectable", "respective", "responsible",
115
+ "resulting", "retail", "retired", "revolutionary", "rich", "ridiculous", "right", "rigid",
116
+ "ripe", "rising", "rival", "roasted", "robust", "rolling", "romantic", "rotten", "rough",
117
+ "round", "royal", "rubber", "rude", "ruling", "running", "rural", "sacred", "sad", "safe",
118
+ "salty", "satisfactory", "satisfied", "scared", "scary", "scattered", "scientific", "scornful",
119
+ "scrawny", "screeching", "secondary", "secret", "secure", "select", "selected", "selective",
120
+ "selfish", "semantic", "senior", "sensible", "sensitive", "separate", "serious", "severe",
121
+ "sexual", "shaggy", "shaky", "shallow", "shared", "sharp", "sheer", "shiny", "shivering",
122
+ "shocked", "short", "shrill", "shy", "sick", "significant", "silent", "silky", "silly",
123
+ "similar", "simple", "single", "skilled", "skinny", "sleepy", "slight", "slim", "slimy",
124
+ "slippery", "slow", "small", "smart", "smiling", "smoggy", "smooth", "social", "socialist",
125
+ "soft", "solar", "sole", "solid", "sophisticated", "sore", "sorry", "sound", "sour", "southern",
126
+ "soviet", "spare", "sparkling", "spatial", "special", "specific", "specified", "spectacular",
127
+ "spicy", "spiritual", "splendid", "spontaneous", "sporting", "spotless", "spotty", "square",
128
+ "squealing", "stable", "stale", "standard", "static", "statistical", "statutory", "steady",
129
+ "steep", "sticky", "stiff", "still", "stingy", "stormy", "straight", "straightforward",
130
+ "strange", "strategic", "strict", "striking", "striped", "strong", "structural", "stuck",
131
+ "stupid", "subjective", "subsequent", "substantial", "subtle", "successful", "successive",
132
+ "sudden", "sufficient", "suitable", "sunny", "super", "superb", "superior", "supporting",
133
+ "supposed", "supreme", "sure", "surprised", "surprising", "surrounding", "surviving",
134
+ "suspicious", "sweet", "swift", "symbolic", "sympathetic", "systematic", "tall", "tame",
135
+ "tart", "tasteless", "tasty", "technical", "technological", "teenage", "temporary", "tender",
136
+ "tense", "terrible", "territorial", "testy", "theoretical", "thick", "thin", "thirsty",
137
+ "thorough", "thoughtful", "thoughtless", "thundering", "tight", "tiny", "tired", "top", "total",
138
+ "tough", "toxic", "traditional", "tragic", "tremendous", "tricky", "tropical", "troubled",
139
+ "typical", "ugliest", "ugly", "ultimate", "unable", "unacceptable", "unaware", "uncertain",
140
+ "unchanged", "uncomfortable", "unconscious", "underground", "underlying", "unemployed",
141
+ "uneven", "unexpected", "unfair", "unfortunate", "unhappy", "uniform", "uninterested", "unique",
142
+ "united", "universal", "unknown", "unlikely", "unnecessary", "unpleasant", "unsightly",
143
+ "unusual", "unwilling", "upper", "upset", "uptight", "urban", "urgent", "used", "useful",
144
+ "useless", "usual", "vague", "valid", "valuable", "variable", "varied", "various", "varying",
145
+ "vast", "verbal", "vertical", "vicarious", "vicious", "victorious", "violent", "visible",
146
+ "visiting", "visual", "vital", "vivacious", "vivid", "vocal", "vocational", "voiceless",
147
+ "voluminous", "voluntary", "vulnerable", "wandering", "warm", "wasteful", "watery", "weak",
148
+ "wealthy", "weary", "weekly", "weird", "welcome", "well", "western", "wet", "whispering",
149
+ "whole", "wicked", "wide", "widespread", "wild", "willing", "willowy", "wily", "wise", "wispy",
150
+ "witty", "wonderful", "wooden", "working", "worldwide", "worried", "worrying", "worthwhile",
151
+ "worthy", "written", "wrong", "xenial", "xeric", "yawning", "yearning", "young", "youngest",
152
+ "youthful", "zany", "zealous", "zesty", "zippy",
153
+ ];
154
+
155
+ const NOUNS = [
156
+ "aardvark", "aardwolf", "albatross", "alligator", "alpaca", "amphibian", "anaconda",
157
+ "angelfish", "anglerfish", "ant", "anteater", "antelope", "antlion", "ape", "aphid",
158
+ "armadillo", "asp", "baboon", "badger", "bandicoot", "barnacle", "barracuda", "basilisk",
159
+ "bass", "bat", "bear", "beaver", "bedbug", "bee", "beetle", "bird", "bison", "blackbird",
160
+ "boa", "boar", "bobcat", "bobolink", "bonobo", "booby", "bovid", "bug", "butterfly", "buzzard",
161
+ "camel", "canid", "capybara", "cardinal", "caribou", "carp", "cat", "caterpillar", "catfish",
162
+ "catshark", "cattle", "centipede", "cephalopod", "chameleon", "cheetah", "chickadee", "chicken",
163
+ "chimpanzee", "chinchilla", "chipmunk", "cicada", "clam", "clownfish", "cobra", "cockroach",
164
+ "cod", "condor", "constrictor", "coral", "cougar", "cow", "coyote", "crab", "crane", "crawdad",
165
+ "crayfish", "cricket", "crocodile", "crow", "cuckoo", "damselfly", "deer", "dingo", "dinosaur",
166
+ "dog", "dolphin", "donkey", "dormouse", "dove", "dragon", "dragonfly", "duck", "eagle",
167
+ "earthworm", "earwig", "echidna", "eel", "egret", "elephant", "elk", "emu", "ermine", "falcon",
168
+ "ferret", "finch", "firefly", "fish", "flamingo", "flea", "fly", "flyingfish", "fowl", "fox",
169
+ "frog", "gamefowl", "gazelle", "gecko", "gerbil", "gibbon", "giraffe", "goat", "goldfish",
170
+ "goose", "gopher", "gorilla", "grasshopper", "grouse", "guan", "guanaco", "guineafowl", "gull",
171
+ "guppy", "haddock", "halibut", "hamster", "hare", "harrier", "hawk", "hedgehog", "heron",
172
+ "herring", "hippopotamus", "hookworm", "hornet", "horse", "hoverfly", "hummingbird", "hyena",
173
+ "iguana", "impala", "jackal", "jaguar", "jay", "jellyfish", "kangaroo", "kingfisher", "kite",
174
+ "kiwi", "koala", "koi", "krill", "ladybug", "lamprey", "lark", "leech", "lemming", "lemur",
175
+ "leopard", "leopon", "limpet", "lion", "lizard", "llama", "lobster", "locust", "loon", "louse",
176
+ "lungfish", "lynx", "macaw", "mackerel", "magpie", "mammal", "manatee", "mandrill", "marlin",
177
+ "marmoset", "marmot", "marsupial", "marten", "mastodon", "meadowlark", "meerkat", "mink",
178
+ "minnow", "mite", "mockingbird", "mole", "mollusk", "mongoose", "monkey", "moose", "mosquito",
179
+ "moth", "mouse", "mule", "muskox", "narwhal", "newt", "nightingale", "ocelot", "octopus",
180
+ "opossum", "orangutan", "orca", "ostrich", "otter", "owl", "ox", "panda", "panther", "parakeet",
181
+ "parrot", "parrotfish", "partridge", "peacock", "peafowl", "pelican", "penguin", "perch",
182
+ "pheasant", "pig", "pigeon", "pike", "pinniped", "piranha", "planarian", "platypus", "pony",
183
+ "porcupine", "porpoise", "possum", "prawn", "primate", "ptarmigan", "puffin", "puma", "python",
184
+ "quail", "quelea", "quokka", "rabbit", "raccoon", "rat", "rattlesnake", "raven", "reindeer",
185
+ "reptile", "rhinoceros", "roadrunner", "rodent", "rook", "rooster", "roundworm", "sailfish",
186
+ "salamander", "salmon", "sawfish", "scallop", "scorpion", "seahorse", "shark", "sheep", "shrew",
187
+ "shrimp", "silkworm", "silverfish", "skink", "skunk", "sloth", "slug", "smelt", "snail",
188
+ "snake", "snipe", "sole", "sparrow", "spider", "spoonbill", "squid", "squirrel", "starfish",
189
+ "stingray", "stoat", "stork", "sturgeon", "swallow", "swan", "swift", "swordfish", "swordtail",
190
+ "tahr", "takin", "tapir", "tarantula", "tarsier", "termite", "tern", "thrush", "tick", "tiger",
191
+ "tiglon", "toad", "tortoise", "toucan", "trout", "tuna", "turkey", "turtle", "tyrannosaurus",
192
+ "unicorn", "urial", "vicuna", "viper", "vole", "vulture", "wallaby", "walrus", "warbler",
193
+ "wasp", "weasel", "whale", "whippet", "whitefish", "wildcat", "wildebeest", "wildfowl", "wolf",
194
+ "wolverine", "wombat", "woodpecker", "worm", "wren", "yak", "zebra",
195
+ ];
196
+
197
+ function capitalize(s: string): string {
198
+ return s.charAt(0).toUpperCase() + s.slice(1);
199
+ }
200
+
201
+ let usedNames = new Set<string>();
202
+
203
+ /**
204
+ * Generate a unique two-word identifier (e.g., "SwiftFalcon").
205
+ * Falls back to numeric suffix if all combinations exhausted.
206
+ */
207
+ export function generateTaskName(): string {
208
+ // Try random combinations first (50 attempts)
209
+ for (let attempt = 0; attempt < 50; attempt++) {
210
+ const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
211
+ const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
212
+ const name = `${capitalize(adj)}${capitalize(noun)}`;
213
+ if (!usedNames.has(name)) {
214
+ usedNames.add(name);
215
+ return name;
216
+ }
217
+ }
218
+
219
+ // Exhaustive search if random fails
220
+ for (const adj of ADJECTIVES) {
221
+ for (const noun of NOUNS) {
222
+ const name = `${capitalize(adj)}${capitalize(noun)}`;
223
+ if (!usedNames.has(name)) {
224
+ usedNames.add(name);
225
+ return name;
226
+ }
227
+ }
228
+ }
229
+
230
+ // All 426k combinations used, add numeric suffix
231
+ let counter = 0;
232
+ while (true) {
233
+ const name = `Task${counter}`;
234
+ if (!usedNames.has(name)) {
235
+ usedNames.add(name);
236
+ return name;
237
+ }
238
+ counter++;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Reset name generator state (for testing).
244
+ */
245
+ export function resetTaskNames(): void {
246
+ usedNames = new Set<string>();
247
+ }
@@ -69,6 +69,118 @@ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): s
69
69
  return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
70
70
  }
71
71
 
72
+ function formatJsonScalar(value: unknown): string {
73
+ if (value === null) return "null";
74
+ if (typeof value === "string") return `"${value}"`;
75
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
76
+ return "";
77
+ }
78
+
79
+ function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
80
+ return ancestors.map((hasNext) => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
81
+ }
82
+
83
+ function renderJsonTreeLines(
84
+ value: unknown,
85
+ theme: Theme,
86
+ maxDepth: number,
87
+ maxLines: number,
88
+ ): { lines: string[]; truncated: boolean } {
89
+ const lines: string[] = [];
90
+ let truncated = false;
91
+
92
+ const iconObject = theme.styledSymbol("icon.folder", "muted");
93
+ const iconArray = theme.styledSymbol("icon.package", "muted");
94
+ const iconScalar = theme.styledSymbol("icon.file", "muted");
95
+
96
+ const pushLine = (line: string) => {
97
+ if (lines.length >= maxLines) {
98
+ truncated = true;
99
+ return false;
100
+ }
101
+ lines.push(line);
102
+ return true;
103
+ };
104
+
105
+ const renderNode = (val: unknown, key: string | undefined, ancestors: boolean[], isLast: boolean, depth: number) => {
106
+ if (lines.length >= maxLines) {
107
+ truncated = true;
108
+ return;
109
+ }
110
+
111
+ const connector = isLast ? theme.tree.last : theme.tree.branch;
112
+ const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `;
113
+ const scalar = formatJsonScalar(val);
114
+
115
+ if (scalar) {
116
+ const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
117
+ pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
118
+ return;
119
+ }
120
+
121
+ if (Array.isArray(val)) {
122
+ const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
123
+ pushLine(`${prefix}${iconArray} ${header}`);
124
+ if (val.length === 0) {
125
+ pushLine(
126
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", "[]")}`,
127
+ );
128
+ return;
129
+ }
130
+ if (depth >= maxDepth) {
131
+ pushLine(
132
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", theme.format.ellipsis)}`,
133
+ );
134
+ return;
135
+ }
136
+ const nextAncestors = [...ancestors, !isLast];
137
+ for (let i = 0; i < val.length; i++) {
138
+ renderNode(val[i], `[${i}]`, nextAncestors, i === val.length - 1, depth + 1);
139
+ if (lines.length >= maxLines) {
140
+ truncated = true;
141
+ return;
142
+ }
143
+ }
144
+ return;
145
+ }
146
+
147
+ if (val && typeof val === "object") {
148
+ const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
149
+ pushLine(`${prefix}${iconObject} ${header}`);
150
+ const entries = Object.entries(val as Record<string, unknown>);
151
+ if (entries.length === 0) {
152
+ pushLine(
153
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", "{}")}`,
154
+ );
155
+ return;
156
+ }
157
+ if (depth >= maxDepth) {
158
+ pushLine(
159
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", theme.format.ellipsis)}`,
160
+ );
161
+ return;
162
+ }
163
+ const nextAncestors = [...ancestors, !isLast];
164
+ for (let i = 0; i < entries.length; i++) {
165
+ const [childKey, child] = entries[i];
166
+ renderNode(child, childKey, nextAncestors, i === entries.length - 1, depth + 1);
167
+ if (lines.length >= maxLines) {
168
+ truncated = true;
169
+ return;
170
+ }
171
+ }
172
+ return;
173
+ }
174
+
175
+ const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
176
+ pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", String(val))}`);
177
+ };
178
+
179
+ renderNode(value, undefined, [], true, 0);
180
+
181
+ return { lines, truncated };
182
+ }
183
+
72
184
  function renderOutputSection(
73
185
  output: string,
74
186
  continuePrefix: string,
@@ -78,11 +190,30 @@ function renderOutputSection(
78
190
  maxExpanded = 10,
79
191
  ): string[] {
80
192
  const lines: string[] = [];
81
- const outputLines = output.split("\n").filter((line) => line.trim());
82
- if (outputLines.length === 0) return lines;
193
+ const trimmedOutput = output.trim();
194
+ if (!trimmedOutput) return lines;
83
195
 
84
196
  lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
85
197
 
198
+ if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
199
+ try {
200
+ const parsed = JSON.parse(trimmedOutput);
201
+ const tree = renderJsonTreeLines(parsed, theme, expanded ? 6 : 2, expanded ? 24 : 6);
202
+ if (tree.lines.length > 0) {
203
+ for (const line of tree.lines) {
204
+ lines.push(`${continuePrefix} ${line}`);
205
+ }
206
+ if (tree.truncated) {
207
+ lines.push(`${continuePrefix} ${theme.fg("dim", theme.format.ellipsis)}`);
208
+ }
209
+ return lines;
210
+ }
211
+ } catch {
212
+ // Fall back to raw output
213
+ }
214
+ }
215
+
216
+ const outputLines = output.split("\n").filter((line) => line.trim());
86
217
  const previewCount = expanded ? maxExpanded : maxCollapsed;
87
218
  for (const line of outputLines.slice(0, previewCount)) {
88
219
  lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 70, theme.format.ellipsis))}`);
@@ -144,9 +275,8 @@ function renderAgentProgress(
144
275
  ? "error"
145
276
  : "accent";
146
277
 
147
- // Main status line - include index for Output tool ID derivation
148
- const agentId = `${progress.agent}(${progress.index})`;
149
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)}`;
278
+ // Main status line - use taskId for Output tool
279
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", progress.taskId)}`;
150
280
  const description = progress.description?.trim();
151
281
  if (description) {
152
282
  statusLine += ` ${theme.fg("muted", truncate(description, 40, theme.format.ellipsis))}`;
@@ -342,9 +472,8 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
342
472
  const iconColor = success ? "success" : "error";
343
473
  const statusText = aborted ? "aborted" : success ? "done" : "failed";
344
474
 
345
- // Main status line - include index for Output tool ID derivation
346
- const agentId = `${result.agent}(${result.index})`;
347
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)} ${formatBadge(statusText, iconColor, theme)}`;
475
+ // Main status line - use taskId for Output tool
476
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", result.taskId)} ${formatBadge(statusText, iconColor, theme)}`;
348
477
  const description = result.description?.trim();
349
478
  if (description) {
350
479
  statusLine += ` ${theme.fg("muted", truncate(description, 40, theme.format.ellipsis))}`;
@@ -38,6 +38,11 @@ export type TaskItem = Static<typeof taskItemSchema>;
38
38
  /** Task tool parameters */
39
39
  export const taskSchema = Type.Object({
40
40
  context: Type.Optional(Type.String({ description: "Shared context prepended to all task prompts" })),
41
+ output_schema: Type.Optional(
42
+ Type.Any({
43
+ description: "JSON schema for structured subagent output (used by the complete tool)",
44
+ }),
45
+ ),
41
46
  tasks: Type.Array(taskItemSchema, {
42
47
  description: "Tasks to run in parallel",
43
48
  maxItems: MAX_PARALLEL_TASKS,
@@ -85,6 +90,7 @@ export interface AgentDefinition {
85
90
  /** Progress tracking for a single agent */
86
91
  export interface AgentProgress {
87
92
  index: number;
93
+ taskId: string;
88
94
  agent: string;
89
95
  agentSource: AgentSource;
90
96
  status: "pending" | "running" | "completed" | "failed" | "aborted";
@@ -106,6 +112,7 @@ export interface AgentProgress {
106
112
  /** Result from a single agent execution */
107
113
  export interface SingleResult {
108
114
  index: number;
115
+ taskId: string;
109
116
  agent: string;
110
117
  agentSource: AgentSource;
111
118
  task: string;
@@ -6,6 +6,7 @@ export interface SubagentWorkerStartPayload {
6
6
  systemPrompt: string;
7
7
  model?: string;
8
8
  toolNames?: string[];
9
+ outputSchema?: unknown;
9
10
  sessionFile?: string | null;
10
11
  spawnsEnv?: string;
11
12
  }
@@ -123,6 +123,9 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
123
123
 
124
124
  // Create agent session (equivalent to CLI's createAgentSession)
125
125
  // Note: hasUI: false disables interactive features
126
+ const completionInstruction =
127
+ "When finished, call the complete tool exactly once. Do not end with a plain-text final answer.";
128
+
126
129
  const { session } = await createAgentSession({
127
130
  cwd: payload.cwd,
128
131
  authStorage,
@@ -130,8 +133,10 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
130
133
  model,
131
134
  thinkingLevel,
132
135
  toolNames: payload.toolNames,
136
+ outputSchema: payload.outputSchema,
137
+ requireCompleteTool: true,
133
138
  // Append system prompt (equivalent to CLI's --append-system-prompt)
134
- systemPrompt: (defaultPrompt) => `${defaultPrompt}\n\n${payload.systemPrompt}`,
139
+ systemPrompt: (defaultPrompt) => `${defaultPrompt}\n\n${payload.systemPrompt}\n\n${completionInstruction}`,
135
140
  sessionManager,
136
141
  hasUI: false,
137
142
  // Pass spawn restrictions to nested tasks
@@ -164,16 +169,43 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
164
169
  await extensionRunner.emit({ type: "session_start" });
165
170
  }
166
171
 
172
+ // Track complete tool calls
173
+ const MAX_COMPLETE_RETRIES = 3;
174
+ let completeCalled = false;
175
+
167
176
  // Subscribe to events and forward to parent (equivalent to --mode json output)
168
177
  session.subscribe((event: AgentSessionEvent) => {
169
178
  if (isAgentEvent(event)) {
170
179
  postMessageSafe({ type: "event", event });
180
+ // Track when complete tool is called
181
+ if (event.type === "tool_execution_end" && event.toolName === "complete") {
182
+ completeCalled = true;
183
+ }
171
184
  }
172
185
  });
173
186
 
174
187
  // Run the prompt (equivalent to --prompt flag)
175
188
  await session.prompt(payload.task);
176
189
 
190
+ // Retry loop if complete was not called
191
+ let retryCount = 0;
192
+ while (!completeCalled && retryCount < MAX_COMPLETE_RETRIES && !abortRequested) {
193
+ retryCount++;
194
+ const reminder = `<system-reminder>
195
+ CRITICAL: You stopped without calling the complete tool. This is reminder ${retryCount} of ${MAX_COMPLETE_RETRIES}.
196
+
197
+ You MUST call the complete tool to finish your task. Options:
198
+ 1. Call complete with your result data if you have completed the task
199
+ 2. Call complete with status="aborted" and an error message if you cannot complete the task
200
+
201
+ Failure to call complete after ${MAX_COMPLETE_RETRIES} reminders will result in task failure.
202
+ </system-reminder>
203
+
204
+ Call complete now.`;
205
+
206
+ await session.prompt(reminder);
207
+ }
208
+
177
209
  // Check if aborted during execution
178
210
  const lastMessage = session.state.messages[session.state.messages.length - 1];
179
211
  if (lastMessage?.role === "assistant" && lastMessage.stopReason === "aborted") {
@@ -1,50 +1,14 @@
1
- You are a worker agent for delegated tasks. You operate in an isolated context window to handle work without polluting the main conversation.
2
-
3
- Do what has been asked; nothing more, nothing less. Work autonomously using all available tools.
4
-
5
- Your strengths:
6
-
7
- - Searching for code, configurations, and patterns across large codebases
8
- - Analyzing multiple files to understand system architecture
9
- - Investigating complex questions that require exploring many files
10
- - Performing multi-step research and implementation tasks
11
-
12
- Guidelines:
13
-
14
- - Persist until the task is fully resolved end-to-end when feasible.
15
- - Verify with tools; ask for clarification when required.
16
- - For file searches: Use grep/glob when you need to search broadly. Use read when you know the specific file path.
17
- - For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.
18
- - Be thorough: Check multiple locations, consider different naming conventions, look for related files.
19
- - When spawning subagents with the Task tool, include a short, user-facing `description` for each task (5-8 words) that summarizes the approach.
20
- - NEVER create files unless absolutely necessary. ALWAYS prefer editing existing files.
21
- - NEVER proactively create documentation files (\*.md) or README files unless explicitly requested.
22
- - Any file paths in your response MUST be absolute. Do NOT use relative paths.
23
- - Include relevant code snippets in your final response.
24
-
25
- Output format when finished:
26
-
27
- ## Completed
28
-
29
- What was done.
30
-
31
- ## Files Changed
32
-
33
- - `/absolute/path/to/file.ts` - what changed
34
-
35
- ## Key Code
36
-
37
- Relevant snippets or signatures touched:
38
-
39
- ```language
40
- // actual code
41
- ```
42
-
43
- ## Notes (if any)
44
-
45
- Anything the main agent should know.
46
-
47
- If handing off to another agent (e.g. reviewer), include:
48
-
49
- - Exact file paths changed
50
- - Key functions/types touched (short list)
1
+ You are a worker agent for delegated tasks in an isolated context. Finish only the assigned work and return the minimum useful result.
2
+
3
+ Principles:
4
+
5
+ - Be concise. No filler, repetition, or tool transcripts.
6
+ - If blocked, ask a single focused question; otherwise proceed autonomously.
7
+ - Prefer narrow search (grep/glob) then read only needed ranges.
8
+ - Avoid full-file reads unless necessary.
9
+ - NEVER create files unless absolutely required. Prefer edits to existing files.
10
+ - NEVER create documentation files (\*.md) unless explicitly requested.
11
+ - Any file paths in your response MUST be absolute.
12
+ - When spawning subagents with the Task tool, include a 5-8 word user-facing description.
13
+ - Include the smallest relevant code snippet when discussing code or config.
14
+ - Follow the main agent's instructions.
@@ -21,7 +21,8 @@ Do NOT use when:
21
21
  - `"raw"` (default): Full output with ANSI codes preserved
22
22
  - `"json"`: Structured object with metadata
23
23
  - `"stripped"`: Plain text with ANSI codes removed for parsing
24
+ - `query` (optional): jq-like query for JSON outputs (e.g., `.result.items[0].name`)
24
25
  - `offset` (optional): Line number to start reading from (1-indexed)
25
26
  - `limit` (optional): Maximum number of lines to read
26
27
 
27
- Use offset/limit for line ranges to reduce context usage on large outputs.
28
+ Use offset/limit for line ranges to reduce context usage on large outputs. Use `query` for JSON outputs (for example, subagent `complete` results).