jeo-code 0.1.0 → 0.4.4

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 (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +804 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +562 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. package/src/commands/models.ts +0 -104
@@ -1,41 +1,55 @@
1
+ import deepInterviewSkillRaw from "../prompts/skills/deep-interview/SKILL.md" with { type: "text" };
2
+ import deepDiveSkillRaw from "../prompts/skills/deep-dive/SKILL.md" with { type: "text" };
3
+ import ralplanSkillRaw from "../prompts/skills/ralplan/SKILL.md" with { type: "text" };
4
+ import teamSkillRaw from "../prompts/skills/team/SKILL.md" with { type: "text" };
5
+ import ultragoalSkillRaw from "../prompts/skills/ultragoal/SKILL.md" with { type: "text" };
6
+ import gjcSkillRaw from "../prompts/skills/gjc/SKILL.md" with { type: "text" };
7
+
8
+ const MAX_SKILL_SUMMARY_CHARS = 180;
9
+ const MAX_SKILL_DETAILS_CHARS = 8_000;
10
+ const MAX_SKILLS_PROMPT_LINES = 40;
11
+ const MAX_SKILLS_PROMPT_CHARS = 6_000;
12
+
1
13
  export interface SkillDoc {
2
14
  name: string; // e.g. "deep-interview"
3
- command: string; // e.g. "joc deep-interview \"<idea>\""
15
+ command: string; // e.g. "jeo deep-interview \"<idea>\""
4
16
  summary: string; // one line
5
17
  whenToUse: string; // one line
6
18
  details: string; // 2-5 lines of guidance
19
+ /** Slash aliases that invoke this skill directly, e.g. `/speckit.plan`. */
20
+ aliases?: string[];
21
+ /** Source SKILL.md path for discovered skills; absent for bundled skills. */
22
+ sourcePath?: string;
23
+ raw?: string;
7
24
  }
8
25
 
9
26
  export const SKILLS: SkillDoc[] = [
10
- {
11
- name: "deep-interview",
12
- command: 'joc deep-interview "<idea>"',
13
- summary: "Socratic ambiguity gate, freezes a seed at ambiguity ≤ 20%; --auto for non-interactive.",
14
- whenToUse: "When an idea is vague and needs requirement gathering and refinement before planning.",
15
- details: "Initiates a Socratic dialogue to ask clarifying questions about a vague idea.\nScores the ambiguity of the proposal and iterates until it is under 20%.\nSaves a structured requirements seed that can be used by subsequent workflows.\nSupports an --auto flag to skip interaction."
16
- },
17
- {
18
- name: "ralplan",
19
- command: "joc ralplan",
20
- summary: "Planner/Architect/Critic blueprint from the seed.",
21
- whenToUse: "When requirements are clear (e.g. from deep-interview) and you need a robust execution blueprint.",
22
- details: "Executes a multi-agent critique and planning process to generate a structured implementation plan.\nCombines views from a Planner, an Architect, and a Critic to identify risks, define tasks, and specify files.\nSaves the blueprint for execution."
23
- },
24
- {
25
- name: "team",
26
- command: "joc team",
27
- summary: "Per-task executor loop against the plan.",
28
- whenToUse: "When you have a blueprint/plan and need to execute the concrete implementation tasks.",
29
- details: "Coordinates execution of individual tasks defined in the blueprint.\nSpawns per-task executor subagents or loops to implement code changes.\nEnsures task-level isolation and tracks implementation status."
30
- },
31
- {
32
- name: "ultragoal",
33
- command: "joc ultragoal",
34
- summary: "Verify acceptance criteria, write report.",
35
- whenToUse: "When tasks are implemented and you need a final, high-level verification and summary report.",
36
- details: "Verifies the implementation against the acceptance criteria specified in the plan.\nRuns checks, tests, or validations to ensure correctness.\nGenerates a final completion report outlining the changes and verification evidence."
37
- }
27
+ parseSkillMarkdown("deep-interview", deepInterviewSkillRaw),
28
+ parseSkillMarkdown("deep-dive", deepDiveSkillRaw),
29
+ parseSkillMarkdown("ralplan", ralplanSkillRaw),
30
+ parseSkillMarkdown("team", teamSkillRaw),
31
+ parseSkillMarkdown("ultragoal", ultragoalSkillRaw),
32
+ parseSkillMarkdown("gjc", gjcSkillRaw),
38
33
  ];
34
+ export const BUILTIN_SKILL_NAMES = SKILLS.map(s => s.name.toLowerCase());
35
+
36
+ export function workflowSkillsForPrompt(skills?: SkillDoc[]): SkillDoc[] {
37
+ if (!skills) {
38
+ return SKILLS;
39
+ }
40
+ const result: SkillDoc[] = [];
41
+ const bundleNames = new Set(BUILTIN_SKILL_NAMES);
42
+
43
+ result.push(...SKILLS);
44
+
45
+ for (const s of skills) {
46
+ if (!bundleNames.has(s.name.toLowerCase())) {
47
+ result.push(s);
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+
39
53
 
40
54
  export function getSkill(name: string): SkillDoc | undefined {
41
55
  return SKILLS.find(s => s.name.toLowerCase() === name.toLowerCase());
@@ -46,16 +60,536 @@ export function skillNames(): string[] {
46
60
  }
47
61
 
48
62
  export function formatSkill(s: SkillDoc): string {
63
+ const aliases = skillSlashAliases(s);
49
64
  return [
50
65
  `Skill: ${s.name}`,
51
66
  `Command: ${s.command}`,
67
+ aliases.length ? `Slash aliases: ${aliases.join(", ")}` : undefined,
52
68
  `Summary: ${s.summary}`,
53
69
  `When to use: ${s.whenToUse}`,
54
70
  `Details:`,
55
71
  s.details.split("\n").map(line => ` ${line}`).join("\n")
72
+ ].filter(Boolean).join("\n");
73
+ }
74
+
75
+ function compactSkillExecutionBrief(skill: SkillDoc): string {
76
+ const aliases = skillSlashAliases(skill);
77
+ let details = skill.details;
78
+ if (details.length > 2400) {
79
+ details = details.slice(0, 2400) + "…";
80
+ }
81
+ return [
82
+ `Name: ${skill.name}`,
83
+ aliases.length ? `Slash aliases: ${aliases.join(", ")}` : undefined,
84
+ `Summary: ${skill.summary}`,
85
+ skill.whenToUse ? `When to use: ${skill.whenToUse}` : undefined,
86
+ "Guidance:",
87
+ ...details.split("\n").map(line => ` ${line}`),
88
+ ].filter(Boolean).join("\n");
89
+ }
90
+
91
+ /**
92
+ * Build the agent task that EXECUTES a skill's workflow (rather than merely echoing
93
+ * the doc or — as weak models do — calling a bogus tool named after the skill). The
94
+ * skill text is injected as GUIDANCE and the agent is told to use its real tools.
95
+ */
96
+ export function buildSkillTask(skill: SkillDoc, intent: string, invokedAs?: string): string {
97
+ const requested = invokedAs ? `Invoked as: ${invokedAs}\n` : "";
98
+ return [
99
+ `You are now executing the "${skill.name}" workflow skill in this repository.`,
100
+ `IMPORTANT: this skill is GUIDANCE for you — it is NOT a callable tool. Do NOT emit a tool call named "${skill.name}". Use your real tools (read, write, edit, bash, find, search, ls, task, todo) to carry out the work, then call done with a short summary.`,
101
+ `You must never quote or recite the guidance text as your reply; the done reason must describe actual work/outcome.`,
102
+ "",
103
+ `<skill_guidance name="${skill.name}">`,
104
+ compactSkillExecutionBrief(skill),
105
+ `</skill_guidance>`,
106
+ "",
107
+ requested +
108
+ (intent
109
+ ? `User intent: ${intent}`
110
+ : "Carry out this skill's workflow now. If it needs a concrete target you weren't given, make a reasonable assumption or ask the user via done."),
56
111
  ].join("\n");
57
112
  }
113
+ export function skillsPromptSection(skills: SkillDoc[] = SKILLS): string {
114
+ const bundleSkills: SkillDoc[] = [];
115
+ const configuredSkills: SkillDoc[] = [];
116
+
117
+ const bundleNames = new Set(BUILTIN_SKILL_NAMES);
118
+ for (const s of skills) {
119
+ if (bundleNames.has(s.name.toLowerCase())) {
120
+ bundleSkills.push(s);
121
+ } else {
122
+ configuredSkills.push(s);
123
+ }
124
+ }
125
+
126
+ const lines: string[] = [];
127
+ let used = 0;
128
+
129
+ function tryAppend(line: string): boolean {
130
+ if (lines.length >= MAX_SKILLS_PROMPT_LINES) return false;
131
+ if (used + line.length + 1 > MAX_SKILLS_PROMPT_CHARS) return false;
132
+ lines.push(line);
133
+ used += line.length + 1;
134
+ return true;
135
+ }
136
+
137
+ // 1. Bundled workflow skills
138
+ if (bundleSkills.length > 0) {
139
+ if (tryAppend("Bundled workflow skills:")) {
140
+ for (const [i, s] of bundleSkills.entries()) {
141
+ const aliases = skillSlashAliases(s);
142
+ const line = `- ${s.name}${aliases.length ? ` (${aliases.join(", ")})` : ""} — ${s.summary}`;
143
+ if (!tryAppend(line)) {
144
+ const remaining = bundleSkills.length - i;
145
+ lines.push(`- … ${remaining} more skill(s) omitted for brevity`);
146
+ break;
147
+ }
148
+ }
149
+ } else {
150
+ lines.push(`- … ${bundleSkills.length} more skill(s) omitted for brevity`);
151
+ }
152
+ }
153
+
154
+ // 2. Configured skills
155
+ if (configuredSkills.length > 0) {
156
+ const isBudgetFull = lines.length >= MAX_SKILLS_PROMPT_LINES || used >= MAX_SKILLS_PROMPT_CHARS;
157
+ if (isBudgetFull) {
158
+ lines.push(`- … ${configuredSkills.length} more skill(s) omitted for brevity`);
159
+ } else {
160
+ const spaceAdded = tryAppend("");
161
+ const titleAdded = tryAppend("Configured skills:");
162
+
163
+ if (titleAdded) {
164
+ for (const [i, s] of configuredSkills.entries()) {
165
+ const aliases = skillSlashAliases(s);
166
+ const line = `- ${s.name}${aliases.length ? ` (${aliases.join(", ")})` : ""} — ${s.summary}`;
167
+ if (!tryAppend(line)) {
168
+ const remaining = configuredSkills.length - i;
169
+ lines.push(`- … ${remaining} more skill(s) omitted for brevity`);
170
+ break;
171
+ }
172
+ }
173
+ } else {
174
+ if (spaceAdded && lines[lines.length - 1] === "") {
175
+ lines.pop();
176
+ }
177
+ lines.push(`- … ${configuredSkills.length} more skill(s) omitted for brevity`);
178
+ }
179
+ }
180
+ }
181
+
182
+ return lines.join("\n");
183
+ }
184
+
185
+ import * as fs from "node:fs/promises";
186
+ import * as path from "node:path";
187
+ import * as os from "node:os";
188
+ import { existsSync, statSync, readFileSync } from "node:fs";
189
+ import { jeoEnv } from "../util/env";
190
+
191
+ export function tryResolveSkillFromFilePath(filePath: string): SkillDoc | null {
192
+ try {
193
+ let targetPath = path.resolve(filePath);
194
+ if (!existsSync(targetPath)) {
195
+ return null;
196
+ }
197
+ const stat = statSync(targetPath);
198
+ if (stat.isDirectory()) {
199
+ const skillMd = path.join(targetPath, "SKILL.md");
200
+ if (existsSync(skillMd) && statSync(skillMd).isFile()) {
201
+ targetPath = skillMd;
202
+ } else {
203
+ return null;
204
+ }
205
+ } else if (!stat.isFile() || !targetPath.endsWith(".md")) {
206
+ return null;
207
+ }
208
+
209
+ const content = readFileSync(targetPath, "utf-8");
210
+ // Determine a name for this skill
211
+ let skillName = path.basename(targetPath, ".md");
212
+ if (skillName.toLowerCase() === "skill" || skillName.toLowerCase() === "readme") {
213
+ // Use the directory name if the filename is generic
214
+ skillName = path.basename(path.dirname(targetPath));
215
+ }
216
+ const parsed = parseSkillMarkdown(skillName, content, { preferMetaName: true });
217
+ return isSupportedExternalSkill(parsed) ? parsed : null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+ const BUILTIN_SLASH_ALIASES = new Set([
223
+ "/help", "/clear", "/compact", "/model", "/fast", "/provider", "/logout",
224
+ "/agents", "/config", "/roles", "/thinking",
225
+ "/view", "/diff", "/find", "/search", "/sessions", "/skill", "/evolve",
226
+ "/exit", "/quit",
227
+ ]);
228
+
229
+ const RESERVED_SKILL_NAMES = new Set(
230
+ [...BUILTIN_SLASH_ALIASES].map(alias => alias.slice(1).toLowerCase())
231
+ );
232
+
233
+ function normalizeSlashAlias(raw: string): string | undefined {
234
+ const m = raw.trim().match(/^\/[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*$/);
235
+ if (!m) return undefined;
236
+ const alias = m[0];
237
+ return BUILTIN_SLASH_ALIASES.has(alias.toLowerCase()) ? undefined : alias;
238
+ }
239
+
240
+ function splitAliasHeader(value: string): string[] {
241
+ return value.split(/[,\s]+/).map(normalizeSlashAlias).filter((a): a is string => !!a);
242
+ }
243
+
244
+ function aliasOwner(alias: string): string {
245
+ return alias.slice(1).split(".", 1)[0]?.toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
246
+ }
247
+
248
+ function skillAliasOwners(name: string): Set<string> {
249
+ const sanitize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
250
+ const owners = new Set([sanitize(name)]);
251
+ // Namespaced skills (e.g. "oh-my-claudecode:team") also own aliases for their
252
+ // last segment, so "/team" inside that skill's doc is still self-owned.
253
+ const last = name.split(/[:/]/).pop();
254
+ if (last) owners.add(sanitize(last));
255
+ owners.delete("");
256
+ return owners;
257
+ }
258
+
259
+ function inferSlashAliases(content: string, skillName: string): string[] {
260
+ const aliases: string[] = [];
261
+ const owners = skillAliasOwners(skillName);
262
+ const re = /(?:^|[\s`([{])((\/[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*))/g;
263
+ let match: RegExpExecArray | null;
264
+ while ((match = re.exec(content))) {
265
+ const alias = normalizeSlashAlias(match[1] ?? "");
266
+ if (!alias || !owners.has(aliasOwner(alias))) continue;
267
+ if (!aliases.some(a => a.toLowerCase() === alias.toLowerCase())) aliases.push(alias);
268
+ }
269
+ return aliases;
270
+ }
271
+
272
+ function dedupeAliases(aliases: string[]): string[] {
273
+ const seen = new Set<string>();
274
+ const out: string[] = [];
275
+ for (const alias of aliases) {
276
+ const key = alias.toLowerCase();
277
+ if (seen.has(key)) continue;
278
+ seen.add(key);
279
+ out.push(alias);
280
+ }
281
+ return out;
282
+ }
283
+
284
+ export function skillSlashAliases(skill: SkillDoc): string[] {
285
+ return dedupeAliases(skill.aliases ?? []);
286
+ }
287
+
288
+
289
+ /** Global + per-project skill-doc directories (user-configurable SKILL.md files).
290
+ * Ordered lowest → highest precedence (a later dir's skill overrides an earlier
291
+ * one with the same name): foreign-ecosystem roots first, jeo-native last.
292
+ * Covered install layouts:
293
+ * - Vercel `npx skills add [-g]` canonical store: `.agents/skills/` (project + ~)
294
+ * - Vercel agent-targeted installs (`-a claude-code`): `.claude/skills/` (project + ~)
295
+ * - jeo agent skills (self-contained `.jeo` namespace, gjc-structure parity):
296
+ * `<config>/agent/skills/` (+ project `<cwd>/.jeo/agent/skills/`)
297
+ * - jeo-native flat: `<config>/skills/`, `<cwd>/.jeo/skills/`, then `JEO_SKILLS_DIR`. */
298
+ export function skillDirs(cwd: string = process.cwd()): string[] {
299
+ // $HOME wins over os.homedir(): Bun caches the system home, so tests (and
300
+ // sandboxed runs) that re-point HOME would otherwise still scan the real one.
301
+ const userHome = process.env.HOME || os.homedir();
302
+ const home = jeoEnv("CONFIG_DIR") || path.join(userHome, ".jeo");
303
+ const configured = (jeoEnv("SKILLS_DIR") ?? "")
304
+ .split(path.delimiter)
305
+ .map(s => s.trim())
306
+ .filter(Boolean);
307
+ return [
308
+ path.join(userHome, ".claude", "skills"),
309
+ path.join(home, "agent", "skills"),
310
+ path.join(userHome, ".agents", "skills"),
311
+ path.join(home, "skills"),
312
+ path.join(cwd, ".claude", "skills"),
313
+ path.join(cwd, ".agents", "skills"),
314
+ path.join(cwd, ".jeo", "skills"),
315
+ path.join(cwd, ".jeo", "agent", "skills"),
316
+ ...configured,
317
+ ];
318
+ }
319
+
320
+ /** A frontmatter `name:` usable as a skill identity: one bare token, no spaces. */
321
+ function sanitizeSkillName(raw: string | undefined): string | undefined {
322
+ const m = (raw ?? "").trim();
323
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(m) ? m : undefined;
324
+ }
325
+
326
+ /** Parse a user skill markdown file into a SkillDoc. Recognizes both the documented
327
+ * `key: value` header grammar (summary / command / when[ to use] / use) AND the
328
+ * decorated form emitted by `jeo skills --write` (`Skill:` / `When to use:` /
329
+ * `Details:`), tolerating a leading `# title` and blank separators. Falls back to
330
+ * inferring the summary from the first body line. */
331
+ export function parseSkillMarkdown(name: string, content: string, opts?: { preferMetaName?: boolean }): SkillDoc {
332
+ const meta: Record<string, string> = {};
333
+ const lines = content.split(/\r?\n/);
334
+ const HEADER = /^(summary|command|when to use|when|whentouse|use|skill|alias|aliases|slash|slashes)\s*:\s*(.*)$/i;
335
+ let idx = 0;
336
+ // YAML-style frontmatter (the standard agent SKILL.md format): a `---` … `---`
337
+ // block at the very top. `description:` maps to the summary so real skill files
338
+ // never surface a literal "---" summary into the prompt or /skill list.
339
+ while (idx < lines.length && lines[idx]!.trim() === "") idx++;
340
+ if (idx < lines.length && lines[idx]!.trim() === "---") {
341
+ idx++;
342
+ for (; idx < lines.length && lines[idx]!.trim() !== "---"; idx++) {
343
+ const fm = lines[idx]!.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
344
+ if (!fm) continue;
345
+ const key = fm[1]!.toLowerCase().replace(/[\s_-]+/g, "");
346
+ let value = fm[2]!.trim().replace(/^["']|["']$/g, "");
347
+ // YAML block scalar indicator `>` / `|` (with optional chomping `+`/`-`). Also fold in
348
+ // the invalid-but-ubiquitous lead-in form real SKILL.md files use, e.g.
349
+ // `description: Use this skill when >` followed by an indented continuation block —
350
+ // otherwise the summary is the truncated nonsense "Use this skill when >".
351
+ // Require whitespace before the indicator (or the whole value) so a description that
352
+ // merely ENDS in `>` (e.g. "returns <T>") is left intact.
353
+ if (/(?:^|\s)[>|][+-]?$/.test(value)) {
354
+ const lead = value.replace(/\s*[>|][+-]?$/, "").trim();
355
+ const block: string[] = [];
356
+ for (idx++; idx < lines.length && /^\s+/.test(lines[idx] ?? ""); idx++) block.push(lines[idx]!.trim());
357
+ idx--;
358
+ value = [lead, block.join(" ")].filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
359
+ }
360
+ meta[key] = value;
361
+ }
362
+ if (idx < lines.length) idx++; // consume the closing ---
363
+ }
364
+ // Skip leading blank lines and a single leading markdown title.
365
+ while (idx < lines.length && lines[idx]!.trim() === "") idx++;
366
+ if (idx < lines.length && lines[idx]!.startsWith("# ")) {
367
+ idx++;
368
+ while (idx < lines.length && lines[idx]!.trim() === "") idx++;
369
+ }
370
+ // Parse a leading header block (tolerating blank separators); `Details:` (or the
371
+ // first free line) begins the body.
372
+ let detailsBlock: string[] | null = null;
373
+ for (; idx < lines.length; idx++) {
374
+ const raw = lines[idx]!;
375
+ if (raw.trim() === "") continue;
376
+ const dm = raw.match(/^details\s*:\s*(.*)$/i);
377
+ if (dm) {
378
+ const inline = dm[1]!.trim();
379
+ detailsBlock = inline ? [inline] : [];
380
+ for (idx++; idx < lines.length; idx++) detailsBlock.push(lines[idx]!.replace(/^ {2}/, ""));
381
+ break;
382
+ }
383
+ const m = raw.match(HEADER);
384
+ if (m) { meta[m[1]!.toLowerCase().replace(/\s+/g, "")] = m[2]!.trim(); continue; }
385
+ break;
386
+ }
387
+ const body = (detailsBlock ? detailsBlock.join("\n") : lines.slice(idx).join("\n")).trim();
388
+ const firstLine = body.split("\n").find(l => l.trim())?.trim() ?? "";
389
+ const explicitAliases = [
390
+ ...splitAliasHeader(meta.alias ?? ""),
391
+ ...splitAliasHeader(meta.aliases ?? ""),
392
+ ...splitAliasHeader(meta.slash ?? ""),
393
+ ...splitAliasHeader(meta.slashes ?? ""),
394
+ ];
395
+ const rawSummary = meta.summary ?? meta.description ?? firstLine;
396
+ const summary = rawSummary ? (rawSummary.length > MAX_SKILL_SUMMARY_CHARS ? rawSummary.slice(0, MAX_SKILL_SUMMARY_CHARS - 1) + "…" : rawSummary) : name;
397
+ const details = body
398
+ ? (body.length > MAX_SKILL_DETAILS_CHARS ? body.slice(0, MAX_SKILL_DETAILS_CHARS - 1) + "…" : body)
399
+ : "(no details)";
400
+ // External (Vercel/agent-skills standard) SKILL.md files carry their identity in
401
+ // the frontmatter `name:`; honor it over the directory/file name when the caller
402
+ // opts in — bundled skills keep their fixed constructor names.
403
+ const docName = (opts?.preferMetaName ? sanitizeSkillName(meta.name) : undefined) ?? name;
404
+ return {
405
+ name: docName,
406
+ command: meta.command ?? `/skill ${docName}`,
407
+ summary,
408
+ whenToUse: meta.whentouse ?? meta.when ?? meta.use ?? "",
409
+ details,
410
+ aliases: dedupeAliases([...explicitAliases, ...inferSlashAliases(content, docName)]),
411
+ raw: content,
412
+ };
413
+ }
414
+
415
+ function isSupportedExternalSkill(doc: SkillDoc): boolean {
416
+ return !RESERVED_SKILL_NAMES.has(doc.name.toLowerCase());
417
+ }
418
+
419
+ /** Bundled skills merged with user skill docs from {@link skillDirs} (user overrides by name). */
420
+ export async function loadSkills(cwd: string = process.cwd()): Promise<SkillDoc[]> {
421
+ const byName = new Map<string, SkillDoc>(SKILLS.map(s => [s.name.toLowerCase(), s]));
422
+ for (const dir of skillDirs(cwd)) {
423
+ let entries: import("node:fs").Dirent[] = [];
424
+ try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { continue; }
425
+ for (const entry of entries) {
426
+ if (entry.name.startsWith(".")) continue;
427
+ let isFile = entry.isFile();
428
+ let isDir = entry.isDirectory();
429
+ // Vercel `npx skills add` links agent dirs (.claude/skills/x) to its canonical
430
+ // store — a symlinked skill reports NEITHER isFile nor isDirectory on the
431
+ // Dirent, which silently dropped every linked skill. Follow the link.
432
+ if (entry.isSymbolicLink()) {
433
+ try {
434
+ const st = await fs.stat(path.join(dir, entry.name));
435
+ isFile = st.isFile();
436
+ isDir = st.isDirectory();
437
+ } catch { continue; /* broken symlink */ }
438
+ }
439
+ if (isFile && entry.name.endsWith(".md")) {
440
+ const nm = entry.name.slice(0, -3);
441
+ try {
442
+ const parsed = parseSkillMarkdown(nm, await fs.readFile(path.join(dir, entry.name), "utf-8"), { preferMetaName: true });
443
+ parsed.sourcePath = path.join(dir, entry.name);
444
+ if (isSupportedExternalSkill(parsed)) byName.set(parsed.name.toLowerCase(), parsed);
445
+ } catch { /* skip unreadable file */ }
446
+ continue;
447
+ }
448
+ if (isDir) {
449
+ const skillPath = path.join(dir, entry.name, "SKILL.md");
450
+ try {
451
+ const parsed = parseSkillMarkdown(entry.name, await fs.readFile(skillPath, "utf-8"), { preferMetaName: true });
452
+ parsed.sourcePath = skillPath;
453
+ if (isSupportedExternalSkill(parsed)) byName.set(parsed.name.toLowerCase(), parsed);
454
+ } catch { /* skip dirs without SKILL.md or unreadable files */ }
455
+ }
456
+ }
457
+ }
458
+ return [...byName.values()];
459
+ }
460
+
461
+ /** gjc-style skill-invocation card body (the `[skill]` block shown in the TUI
462
+ * when `$name`//skill runs): name, resolved SKILL.md path (or the bundled
463
+ * module path), and the prompt size actually injected. Pure — testable. */
464
+ export function skillInvocationCard(skill: SkillDoc): string[] {
465
+ const promptLines = (skill.raw ?? skill.details ?? "").split("\n").filter(l => l.trim().length > 0).length;
466
+ // jeo-ref tree-connector detail: the skill name leads, resolved metadata hangs
467
+ // off ├─/└─ connectors so the card scans like the reference's Skill panel.
468
+ return [
469
+ `Skill: ${skill.name}`,
470
+ `├─ path: ${skill.sourcePath ?? `(bundled) src/prompts/skills/${skill.name}/SKILL.md`}`,
471
+ `└─ prompt: ${promptLines} lines`,
472
+ ];
473
+ }
474
+
475
+ /** Case-insensitive lookup within a resolved skill list. */
476
+ export function getSkillFrom(skills: SkillDoc[], name: string): SkillDoc | undefined {
477
+ return skills.find(s => s.name.toLowerCase() === name.toLowerCase());
478
+ }
479
+
480
+ /** Case-insensitive lookup by direct slash alias, e.g. `/speckit.plan`. */
481
+ export function getSkillBySlash(skills: SkillDoc[], command: string): SkillDoc | undefined {
482
+ const q = command.toLowerCase();
483
+ return skills.find(s => skillSlashAliases(s).some(a => a.toLowerCase() === q));
484
+ }
485
+
486
+ export interface SkillInvocation {
487
+ skill: SkillDoc;
488
+ intent: string;
489
+ invokedAs?: string;
490
+ }
491
+
492
+ /** Parse only explicit skill invocations. Ambient mentions of skill names or slash
493
+ * aliases inside a broader prompt are deliberately ignored so pasted SKILL.md files
494
+ * cannot hijack an ordinary coding request. */
495
+ export function parseSkillInvocation(input: string, skills: SkillDoc[]): SkillInvocation | null {
496
+ const trimmed = input.trim();
497
+ if (!trimmed) return null;
498
+
499
+ const explicitEntrypoint = trimmed.startsWith("/skill:")
500
+ ? "/skill:"
501
+ : (trimmed === "/skill" || trimmed.startsWith("/skill ")) ? "/skill" : "";
502
+ if (explicitEntrypoint) {
503
+ const rest = trimmed.substring(explicitEntrypoint.length).trim();
504
+ if (!rest) return null;
505
+ const [name, ...intentParts] = rest.split(/\s+/);
506
+ let skill = getSkillFrom(skills, name ?? "");
507
+ if (!skill && name) {
508
+ skill = tryResolveSkillFromFilePath(name) ?? undefined;
509
+ }
510
+ return skill ? { skill, intent: intentParts.join(" ").trim() } : null;
511
+ }
512
+
513
+ const command = trimmed.split(/\s+/, 1)[0] ?? "";
514
+ // Codex/gjc-style exact-name entrypoint: `$team [intent]` invokes the skill
515
+ // named "team" directly (case-insensitive). Only the FIRST token counts, and
516
+ // only when a skill with that exact name is loaded — `$HOME is what?` or any
517
+ // unknown `$word` falls through to the model as an ordinary prompt.
518
+ if (command.length > 1 && command.startsWith("$")) {
519
+ const dollarSkill = getSkillFrom(skills, command.slice(1));
520
+ if (dollarSkill) {
521
+ return { skill: dollarSkill, intent: trimmed.slice(command.length).trim(), invokedAs: command };
522
+ }
523
+ }
524
+ let skill = getSkillBySlash(skills, command);
525
+ if (!skill) {
526
+ if (command.startsWith("/") || command.startsWith(".") || command.includes("/")) {
527
+ const resolved = tryResolveSkillFromFilePath(command);
528
+ if (resolved) {
529
+ return { skill: resolved, intent: trimmed.slice(command.length).trim(), invokedAs: command };
530
+ }
531
+ }
532
+ }
533
+ return skill ? { skill, intent: trimmed.slice(command.length).trim(), invokedAs: command } : null;
534
+ }
535
+ export function looksLikeSkillEcho(reply: string, skills: SkillDoc[]): boolean {
536
+ if (reply.length < 80) {
537
+ return false;
538
+ }
539
+
540
+ const lines = reply.split(/\r?\n/);
541
+
542
+ // Heuristic 1: Contains <skill_guidance or a line starting with "Skill: " AND a line starting with "When to use:"
543
+ if (reply.includes("<skill_guidance")) {
544
+ return true;
545
+ }
546
+ const hasSkillLine = lines.some(l => l.trim().startsWith("Skill: "));
547
+ const hasWhenToUseLine = lines.some(l => l.trim().startsWith("When to use:"));
548
+ if (hasSkillLine && hasWhenToUseLine) {
549
+ return true;
550
+ }
551
+
552
+ // Heuristic 2: >= 3 reply lines are near-verbatim matches (trimmed, case-insensitive)
553
+ // of skill summary lines or of "- <name> — <summary>" lines that skillsPromptSection would emit
554
+ const targets = new Set<string>();
555
+ for (const s of skills) {
556
+ if (s.summary) {
557
+ targets.add(s.summary.trim().toLowerCase());
558
+ }
559
+ const aliases = skillSlashAliases(s);
560
+ const promptLine = `- ${s.name}${aliases.length ? ` (${aliases.join(", ")})` : ""} — ${s.summary}`;
561
+ targets.add(promptLine.trim().toLowerCase());
562
+ }
563
+
564
+ let matchCount = 0;
565
+ for (const line of lines) {
566
+ const trimmedLine = line.trim().toLowerCase();
567
+ if (targets.has(trimmedLine)) {
568
+ matchCount++;
569
+ }
570
+ }
571
+ if (matchCount >= 3) {
572
+ return true;
573
+ }
574
+
575
+ // Heuristic 3: Contains a verbatim chunk (>= 160 consecutive chars) of any skill's details
576
+ // (only check first 50 skills, and only those with details length >= 160, using start/middle/end probes)
577
+ const checkedSkills = skills.slice(0, 50);
578
+ for (const s of checkedSkills) {
579
+ const details = s.details;
580
+ if (!details || details.length < 160) {
581
+ continue;
582
+ }
583
+ const len = details.length;
584
+ const startProbe = details.slice(0, 160);
585
+ const midStart = Math.floor((len - 160) / 2);
586
+ const midProbe = details.slice(midStart, midStart + 160);
587
+ const endProbe = details.slice(len - 160);
588
+
589
+ if (reply.includes(startProbe) || reply.includes(midProbe) || reply.includes(endProbe)) {
590
+ return true;
591
+ }
592
+ }
58
593
 
59
- export function skillsPromptSection(): string {
60
- return SKILLS.map(s => `- ${s.name} — ${s.summary}`).join("\n");
594
+ return false;
61
595
  }
@@ -0,0 +1,43 @@
1
+ <!-- Parent: ../AGENTS.md -->
2
+ <!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
3
+
4
+ # tui
5
+
6
+ ## Purpose
7
+ Terminal User Interface logic, layout systems, and the differential renderer. Responsible for visualizing the agent's state, tools, and streams.
8
+
9
+ ## Key Files
10
+ | File | Description |
11
+ |------|-------------|
12
+ | `app.ts` | High-level TUI orchestrator (`LaunchTui`) |
13
+ | `renderer.ts` | The differential, atomic terminal renderer (handles scrollback, resizing, DECSET 2026) |
14
+ | `terminal.ts` | Low-level ANSI escape codes and terminal size utilities |
15
+
16
+ ## Subdirectories
17
+ | Directory | Purpose |
18
+ |-----------|---------|
19
+ | `components/` | Reusable UI widgets (footer, forge boxes, timeline, layouts) (see `components/AGENTS.md`) |
20
+ | `monitoring/` | Specialized HUD views (see `monitoring/AGENTS.md`) |
21
+
22
+ ## For AI Agents
23
+
24
+ ### Working In This Directory
25
+ - The differential renderer (`renderer.ts`) is critical for the "scrollback-friendly inline live turn" feature. DO NOT introduce full-screen clears (`\x1b[0J` mid-turn) as they flood tmux scrollback.
26
+ - Maintain visual parity with `gjc` design patterns (e.g., shadcn-inspired stage-grouped card layouts, muted card headers).
27
+
28
+ ### Testing Requirements
29
+ - Test rendering logic using mocked stdout.
30
+ - Verify resize behavior and atomic flushes.
31
+
32
+ ### Common Patterns
33
+ - Separation of UI from engine: `app.ts` listens to `AgentLoopEvents` but does not import engine loop directly.
34
+
35
+ ## Dependencies
36
+
37
+ ### Internal
38
+ - Driven by events from `src/agent/loop.ts`.
39
+
40
+ ### External
41
+ - ANSI escape sequence management.
42
+
43
+ <!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->