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,37 +1,73 @@
1
1
  import * as fs from "node:fs/promises";
2
- import { z } from "zod";
2
+ import chalk from "chalk";
3
+ import { PlanSchema, normalizePlanShape, parseYaml } from "../agent/plan";
3
4
  import {
4
5
  readWorkflowState,
6
+ readWorkflowStateStrict,
5
7
  writeWorkflowState,
8
+ acquireWorkflowRunLock,
9
+ type WorkflowState,
6
10
  } from "../agent/state";
7
11
  import { runAgentLoop } from "../agent/engine";
12
+ import { maybeCompact } from "../agent/compaction";
13
+ import { catalogMetadata } from "../ai";
8
14
  import { readGlobalConfig } from "../agent/state";
9
15
  import {
10
16
  defaultSubagentRole,
17
+ getSubagentRole,
11
18
  resolveSubagentModel,
12
19
  resolveSubagentMaxSteps,
13
20
  subagentSystemPrompt,
14
21
  subagentToolset,
22
+ subagentRoleIds,
23
+ validateSubagentDoneReason,
15
24
  } from "../agent/subagents";
16
25
  import type { Message } from "../agent/loop";
26
+ import { loadProjectContext, withProjectContext } from "../agent/context-files";
27
+ import { categoryBadge } from "../tui/components/category-index";
17
28
 
18
29
  export type RalphStreamKind = "step" | "complete" | "error";
19
30
 
20
- export function formatRalphTodoGuide(tasks: string[], activeIndex = 0, completed: readonly string[] = []): string[] {
31
+ export interface RalphRenderOptions {
32
+ color?: boolean;
33
+ indexed?: boolean;
34
+ }
35
+
36
+ export function formatRalphTodoGuide(
37
+ tasks: string[],
38
+ activeIndex = 0,
39
+ completed: readonly string[] = [],
40
+ opts: RalphRenderOptions = {},
41
+ ): string[] {
21
42
  const done = new Set(completed);
43
+ const color = opts.color === true;
44
+ const badge = opts.indexed || color ? `${categoryBadge("subagent", { color })} ` : "";
45
+ const green = color ? chalk.green.bold : (s: string) => s;
46
+ const yellow = color ? chalk.yellow.bold : (s: string) => s;
47
+ const gray = color ? chalk.gray : (s: string) => s;
22
48
  const lines = [
23
- "[RALPH] Subagent guidance: follow todos in order; stream every step, complete, and error event.",
49
+ `${badge || "[RALPH] " }Subagent guidance: follow todos in order; stream every step, complete, and error event.`,
24
50
  ];
25
51
  tasks.forEach((task, index) => {
26
- const mark = done.has(task) ? "x" : index === activeIndex ? ">" : " ";
52
+ const mark = done.has(task) ? green("x") : index === activeIndex ? yellow(">") : gray(" ");
27
53
  lines.push(`[TODO] ${index + 1}/${tasks.length} [${mark}] ${task}`);
28
54
  });
29
55
  return lines;
30
56
  }
31
57
 
32
- export function formatRalphStreamEvent(kind: RalphStreamKind, message: string): string {
58
+ export function formatRalphStreamEvent(kind: RalphStreamKind, message: string, opts: RalphRenderOptions = {}): string {
33
59
  const label = kind === "complete" ? "complete" : kind === "error" ? "error" : "step";
34
- return ` └─ stream:${label} ${message}`;
60
+ if (!opts.color && !opts.indexed) return ` └─ stream:${label} ${message}`;
61
+ const color = opts.color === true;
62
+ const badge = `${categoryBadge("subagent", { color })} `;
63
+ const tint = color
64
+ ? kind === "complete"
65
+ ? chalk.green.bold
66
+ : kind === "error"
67
+ ? chalk.red.bold
68
+ : chalk.cyan.bold
69
+ : (s: string) => s;
70
+ return ` ${badge}${tint(`stream:${label}`)} ${message}`;
35
71
  }
36
72
 
37
73
  export interface RalphSubagentPromptContext {
@@ -58,280 +94,372 @@ export function buildRalphSubagentPrompt(ctx: RalphSubagentPromptContext): strin
58
94
  ].join("\n");
59
95
  }
60
96
 
97
+ export function activeStepIndex(totalTasks: number, pendingTasks: readonly string[] | undefined): number {
98
+ if (totalTasks <= 0) return 0;
99
+ const pending = pendingTasks?.length ?? totalTasks;
100
+ return Math.max(0, Math.min(totalTasks - pending, totalTasks - 1));
101
+ }
61
102
 
62
- export async function runTeamCommand(): Promise<void> {
63
- const cwd = process.cwd();
103
+ const ARCHITECT_STATUS_VALUES = new Set(["CLEAR", "WATCH", "BLOCK"]);
104
+ const ARCHITECT_REVIEW_VALUES = new Set(["APPROVE", "COMMENT", "REQUEST CHANGES"]);
105
+
106
+ function extractLineValue(reason: string, label: string): string | undefined {
107
+ // Strip leading/trailing markdown emphasis/quoting/heading chars so the gate
108
+ // accepts e.g. `**Architectural Status:** CLEAR` or `> Architectural Status: CLEAR`.
109
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
110
+ for (const rawLine of reason.split(/\r?\n/)) {
111
+ const stripped = rawLine.replace(/[*_`>#]+/g, "").trim();
112
+ const m = stripped.match(new RegExp(`^${escaped}\\s*:\\s*(.+)$`, "i"));
113
+ if (m) return m[1]!.trim();
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ function normalizeArchitectVerdict(raw: string): string {
119
+ // Drop trailing `(caveats)` or `- comments`; collapse whitespace; uppercase.
120
+ return raw.split(/\s*[(\-—–]/, 1)[0]!.replace(/\s+/g, " ").trim().toUpperCase();
121
+ }
122
+
123
+ export function parseRoleGateVerdict(roleId: string, reason: string): { ok: boolean; message?: string } {
124
+ const trimmed = reason.trim();
125
+ if (roleId === "architect") {
126
+ const statusRaw = extractLineValue(trimmed, "Architectural Status");
127
+ const reviewRaw = extractLineValue(trimmed, "Code Review Recommendation");
128
+ if (!statusRaw || !reviewRaw) {
129
+ return { ok: false, message: "architect report missing Architectural Status or Code Review Recommendation" };
130
+ }
131
+ const status = normalizeArchitectVerdict(statusRaw);
132
+ const review = normalizeArchitectVerdict(reviewRaw);
133
+ if (!ARCHITECT_STATUS_VALUES.has(status)) {
134
+ return { ok: false, message: `architect Architectural Status invalid (expected CLEAR|WATCH|BLOCK, got ${JSON.stringify(statusRaw)})` };
135
+ }
136
+ if (!ARCHITECT_REVIEW_VALUES.has(review)) {
137
+ return { ok: false, message: `architect Code Review Recommendation invalid (expected APPROVE|COMMENT|REQUEST CHANGES, got ${JSON.stringify(reviewRaw)})` };
138
+ }
139
+ if (status === "BLOCK" || review === "REQUEST CHANGES") {
140
+ return { ok: false, message: `architect gated execution (${status} / ${review})` };
141
+ }
142
+ return { ok: true };
143
+ }
144
+ if (roleId === "critic") {
145
+ // Fail-closed: only an explicit [OKAY] first line approves. Anything else
146
+ // (malformed, missing verdict, wrong case) gates so a buggy/spoofed reason
147
+ // cannot silently pass review.
148
+ const firstLine = trimmed.split(/\r?\n/, 1)[0]?.trim() ?? "";
149
+ if (firstLine === "[OKAY]") return { ok: true };
150
+ if (firstLine === "[REJECT]" || firstLine === "[ITERATE]") {
151
+ return { ok: false, message: `critic gated execution (${firstLine})` };
152
+ }
153
+ return { ok: false, message: `critic verdict missing or malformed (expected [OKAY]|[ITERATE]|[REJECT], got ${JSON.stringify(firstLine.slice(0, 40))})` };
154
+ }
155
+ return { ok: true };
156
+ }
157
+
158
+
159
+ export interface TeamEngineOptions {
160
+ cwd?: string;
161
+ signal?: AbortSignal;
162
+ onProgress?: (e: { skill: string; phase: string; detail?: string }) => void;
163
+ io?: {
164
+ output?: (line: string) => void;
165
+ };
166
+ }
167
+
168
+ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok: boolean; reason?: string }> {
169
+ const cwd = opts.cwd ?? process.cwd();
170
+
171
+ const log = (msg?: any) => {
172
+ const str = msg !== undefined ? String(msg) : "";
173
+ if (opts.io?.output) {
174
+ const lines = str.split("\n");
175
+ for (const line of lines) {
176
+ opts.io.output(line);
177
+ }
178
+ } else {
179
+ console.log(str);
180
+ }
181
+ };
182
+
183
+ if (opts.onProgress) {
184
+ opts.onProgress({ skill: "team", phase: "start" });
185
+ }
186
+
187
+ if (opts.signal?.aborted) {
188
+ return { ok: false, reason: "aborted" };
189
+ }
64
190
 
65
191
  // Read ralplan state
66
192
  const planState = await readWorkflowState("ralplan", cwd);
67
193
  if (!planState || planState.current_phase !== "complete" || !planState.plan_path) {
68
- console.log(
69
- `[ERROR] No completed plan found. Please run 'joc ralplan' to generate a plan first.`
194
+ log(
195
+ `[ERROR] No completed plan found. Please run 'jeo ralplan' to generate a plan first.`
70
196
  );
71
- return;
197
+ return { ok: false, reason: "No completed plan found" };
72
198
  }
73
199
 
74
200
  if (!planState.approved) {
75
- console.log(
201
+ log(
76
202
  `[ERROR] Plan is not approved. Please approve the plan before executing.`
77
203
  );
78
- return;
204
+ return { ok: false, reason: "Plan is not approved" };
79
205
  }
80
206
 
81
207
  const planPath = planState.plan_path;
82
- console.log(`\n=== Starting Team Execution Stage ===`);
83
- console.log(`Reading plan from: ${planPath}`);
208
+ log(`\n=== Starting Team Execution Stage ===`);
209
+ log(`Reading plan from: ${planPath}`);
84
210
 
85
211
  let planContent = "";
86
212
  try {
87
213
  planContent = await fs.readFile(planPath, "utf-8");
88
214
  } catch (err: any) {
89
- console.log(`[ERROR] Failed to read plan file: ${err.message}`);
90
- return;
215
+ log(`[ERROR] Failed to read plan file: ${err.message}`);
216
+ return { ok: false, reason: err.message };
91
217
  }
92
218
 
93
219
  let rawPlan: any;
94
220
  try {
95
221
  rawPlan = parseYaml(planContent);
96
222
  } catch (err: any) {
97
- console.log(`[ERROR] Failed to parse plan YAML: ${err.message}`);
98
- return;
223
+ log(`[ERROR] Failed to parse plan YAML: ${err.message}`);
224
+ return { ok: false, reason: err.message };
99
225
  }
100
226
 
101
- const parsed = PlanSchema.safeParse(rawPlan);
227
+ const parsed = PlanSchema.safeParse(normalizePlanShape(rawPlan));
102
228
  if (!parsed.success) {
103
- console.log(`[ERROR] Plan validation failed: ${parsed.error.message}`);
104
- return;
229
+ const shape = Array.isArray(rawPlan) ? "a top-level list" : typeof rawPlan;
230
+ log(
231
+ `[ERROR] The plan is not in the expected shape — it needs a top-level object with a 'steps:' list ` +
232
+ `(each step: { name, role?, ... }), but the plan file is ${shape}.\n` +
233
+ ` The planning model likely produced malformed YAML. Review ${planPath} or re-run 'jeo ralplan' (with a more capable model).`
234
+ );
235
+ return { ok: false, reason: "Plan is not in the expected shape" };
236
+ }
237
+
238
+ const teamCfg = await readGlobalConfig();
239
+ const unknownRoles = parsed.data.steps
240
+ .map(step => step.role?.trim())
241
+ .filter((role): role is string => !!role && !getSubagentRole(role, teamCfg));
242
+ if (unknownRoles.length > 0) {
243
+ const unique = [...new Set(unknownRoles)];
244
+ log(
245
+ `[ERROR] Plan references unknown subagent role(s): ${unique.join(", ")}. ` +
246
+ `Known roles: ${subagentRoleIds(teamCfg).join(", ")}. Fix ${planPath} or re-run 'jeo ralplan'.`
247
+ );
248
+ return { ok: false, reason: "Plan references unknown subagent role(s)" };
105
249
  }
106
250
 
107
251
  const tasks = parsed.data.steps.map(step => step.name);
252
+ const roleByIndex = parsed.data.steps.map(step => getSubagentRole(step.role, teamCfg)?.id);
108
253
 
109
- console.log(`Loaded ${tasks.length} tasks for execution.`);
254
+ log(`Loaded ${tasks.length} tasks for execution.`);
110
255
 
256
+ // Round-8 (architect ref 7-Round7Workflow): cross-process run lock — two
257
+ // concurrent `jeo team` runs would each pop pending_tasks[0] and last-writer-
258
+ // wins the state file (tasks executed twice, completions lost).
259
+ let releaseLock: () => Promise<void>;
260
+ try {
261
+ releaseLock = await acquireWorkflowRunLock("team", cwd);
262
+ } catch (err: any) {
263
+ log(`[ERROR] ${err.message}`);
264
+ return { ok: false, reason: "another team run holds the lock" };
265
+ }
266
+ try {
267
+ let teamState: WorkflowState;
268
+ try {
269
+ teamState = (await readWorkflowStateStrict("team", cwd)) ?? {
270
+ active: true,
271
+ current_phase: "executing",
272
+ skill: "team" as const,
273
+ slug: planState.slug,
274
+ plan_path: planPath,
275
+ completed_tasks: [],
276
+ pending_tasks: [...tasks],
277
+ };
278
+ } catch {
279
+ log(
280
+ `[ERROR] .jeo/state/team-state.json is corrupt. Fix or delete it before re-running 'jeo team' ` +
281
+ `(refusing to silently restart and re-run already-completed tasks).`,
282
+ );
283
+ return { ok: false, reason: "team-state.json is corrupt" };
284
+ }
111
285
 
112
- // Initialize team state
113
- let teamState = await readWorkflowState("team", cwd) || {
114
- active: true,
115
- current_phase: "executing",
116
- skill: "team" as const,
117
- slug: planState.slug,
118
- plan_path: planPath,
119
- completed_tasks: [],
120
- pending_tasks: [...tasks],
121
- };
286
+ // Round-7 #1 (architect ref 7-Round7Workflow): a team-state left over from a
287
+ // PREVIOUS plan must never be reused — pending=[] from plan A would make plan B
288
+ // no-op into a false "all executed" success, and a mid-flight leftover would run
289
+ // plan-A task text under plan-B roles. A different plan reinitializes execution.
290
+ if (teamState.plan_path !== planPath || teamState.slug !== planState.slug) {
291
+ log(`${categoryBadge("progress")} New plan detected (${planPath}) — restarting execution from its task list.`);
292
+ teamState = {
293
+ active: true,
294
+ current_phase: "executing",
295
+ skill: "team" as const,
296
+ slug: planState.slug,
297
+ plan_path: planPath,
298
+ completed_tasks: [],
299
+ pending_tasks: [...tasks],
300
+ };
301
+ }
302
+
303
+ // Round-8: a previous run halted on a task — its partial edits may still be
304
+ // on disk. Warn loudly before re-running on top of them, then clear the marker.
305
+ if (teamState.current_phase === "failed" && teamState.failed_task) {
306
+ log(
307
+ `[WARN] The previous run FAILED on "${teamState.failed_task}" and may have left partial edits on disk. ` +
308
+ `Review the working tree before trusting this re-run — executing the task again on top of partial work can duplicate changes.`,
309
+ );
310
+ teamState.current_phase = "executing";
311
+ delete teamState.failed_task;
312
+ }
313
+
314
+ await writeWorkflowState("team", teamState, cwd);
315
+ const renderOpts: RalphRenderOptions = { color: !!process.stdout.isTTY, indexed: true };
316
+ for (const line of formatRalphTodoGuide(tasks, activeStepIndex(tasks.length, teamState.pending_tasks), teamState.completed_tasks ?? [], renderOpts)) log(line);
317
+
318
+ while (teamState.pending_tasks && teamState.pending_tasks.length > 0) {
319
+ if (opts.signal?.aborted) {
320
+ return { ok: false, reason: "aborted" };
321
+ }
322
+
323
+ const currentTask = teamState.pending_tasks[0];
324
+ log(`\n${categoryBadge("progress")} Current task: "${currentTask}"`);
325
+ const activeIndex = activeStepIndex(tasks.length, teamState.pending_tasks);
326
+ for (const line of formatRalphTodoGuide(tasks, activeIndex, teamState.completed_tasks ?? [], renderOpts)) log(line);
327
+
328
+ if (opts.onProgress) {
329
+ opts.onProgress({ skill: "team", phase: "executing", detail: `Current task: ${currentTask}` });
330
+ }
331
+
332
+ const success = await executeTaskWithAgent({
333
+ task: currentTask,
334
+ tasks,
335
+ activeIndex,
336
+ completed: teamState.completed_tasks ?? [],
337
+ cwd,
338
+ roleId: roleByIndex[activeIndex],
339
+ });
340
+
341
+ if (opts.signal?.aborted) {
342
+ return { ok: false, reason: "aborted" };
343
+ }
344
+
345
+ if (success) {
346
+ teamState.completed_tasks = [...(teamState.completed_tasks ?? []), currentTask];
347
+ teamState.pending_tasks = teamState.pending_tasks.slice(1);
348
+ await writeWorkflowState("team", teamState, cwd);
349
+ log(`${categoryBadge("done")} Completed: "${currentTask}"`);
350
+ } else {
351
+ // Round-8: persist a failed marker so the NEXT run can warn about the
352
+ // partial edits this halted task may have left behind.
353
+ teamState.current_phase = "failed";
354
+ teamState.failed_task = currentTask;
355
+ await writeWorkflowState("team", teamState, cwd);
356
+ log(`${categoryBadge("error")} Failed on task: "${currentTask}". Halting execution.`);
357
+ return { ok: false, reason: `Failed on task: "${currentTask}"` };
358
+ }
359
+ }
122
360
 
123
- await writeWorkflowState("team", teamState, cwd);
124
- for (const line of formatRalphTodoGuide(tasks, Math.max(0, tasks.indexOf(teamState.pending_tasks?.[0] ?? "")), teamState.completed_tasks ?? [])) console.log(line);
125
-
126
- while (teamState.pending_tasks && teamState.pending_tasks.length > 0) {
127
- const currentTask = teamState.pending_tasks[0];
128
- console.log(`\n[TASK] Current: "${currentTask}"`);
129
- const activeIndex = tasks.indexOf(currentTask);
130
- for (const line of formatRalphTodoGuide(tasks, activeIndex, teamState.completed_tasks ?? [])) console.log(line);
131
-
132
- // Run the Executor loop
133
- const success = await executeTaskWithAgent({
134
- task: currentTask,
135
- tasks,
136
- activeIndex,
137
- completed: teamState.completed_tasks ?? [],
138
- cwd,
139
- });
140
-
141
- if (success) {
142
- teamState.completed_tasks = [...(teamState.completed_tasks ?? []), currentTask];
143
- teamState.pending_tasks = teamState.pending_tasks.slice(1);
361
+ if (teamState.pending_tasks && teamState.pending_tasks.length === 0) {
362
+ teamState.current_phase = "complete";
363
+ teamState.active = false; // execution finished — the flag must not read as "in progress"
144
364
  await writeWorkflowState("team", teamState, cwd);
145
- console.log(`[TASK SUCCESS] Completed: "${currentTask}"`);
146
- } else {
147
- console.log(`[TASK FAILED] Failed on task: "${currentTask}". Halting execution.`);
148
- break;
365
+ log(`\n${categoryBadge("done")} All tasks in the plan executed successfully!`);
366
+ log("Run 'jeo ultragoal' to run verify tests and evaluate metrics.");
367
+ if (opts.onProgress) {
368
+ opts.onProgress({ skill: "team", phase: "complete" });
369
+ }
149
370
  }
371
+ return { ok: true };
372
+ } finally {
373
+ await releaseLock();
150
374
  }
375
+ }
151
376
 
152
- if (teamState.pending_tasks && teamState.pending_tasks.length === 0) {
153
- teamState.current_phase = "complete";
154
- await writeWorkflowState("team", teamState, cwd);
155
- console.log("\n[SUCCESS] All tasks in the plan executed successfully!");
156
- console.log("Run 'joc ultragoal' to run verify tests and evaluate metrics.");
377
+ export async function runTeamCommand(): Promise<void> {
378
+ const res = await runTeamEngine();
379
+ if (!res.ok) {
380
+ process.exitCode = 1;
157
381
  }
158
382
  }
159
383
 
160
- async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string }): Promise<boolean> {
384
+ async function executeTaskWithAgent(ctx: RalphSubagentPromptContext & { cwd: string; roleId?: string }): Promise<boolean> {
161
385
  const config = await readGlobalConfig();
162
- const role = defaultSubagentRole();
386
+ const role = getSubagentRole(ctx.roleId, config) ?? defaultSubagentRole();
387
+ const renderOpts: RalphRenderOptions = { color: !!process.stdout.isTTY, indexed: true };
163
388
  const model = resolveSubagentModel(role.id, config);
164
389
  const maxSteps = resolveSubagentMaxSteps(role.id, config);
165
390
  console.log(` └─ Subagent: ${role.title} · model ${model} · ≤${maxSteps} steps`);
166
391
 
392
+ const contextTokens = catalogMetadata(model)?.contextTokens;
393
+
394
+ const projectContext = await loadProjectContext(ctx.cwd);
167
395
  const history: Message[] = [
168
- { role: "system", content: subagentSystemPrompt(role) },
396
+ { role: "system", content: withProjectContext(subagentSystemPrompt(role), projectContext) },
169
397
  { role: "user", content: buildRalphSubagentPrompt(ctx) },
170
398
  ];
171
399
 
400
+ try {
401
+ await maybeCompact(history, { model, contextTokens });
402
+ } catch (err) {
403
+ // LLM summary failure does not halt team
404
+ }
405
+
406
+ let mutationsOk = 0; // round-8 parent audit: successful write/edit/bash count
172
407
  const result = await runAgentLoop(history, {
173
408
  cwd: ctx.cwd,
174
409
  model,
175
410
  maxSteps,
411
+ // Bounded delegation: ralph/team subagents keep an exact step contract; the
412
+ // orchestrator owns retries, so the gjc step-extension flow is disabled here.
413
+ budget: { maxExtensions: 0 },
176
414
  tools: subagentToolset(role),
177
415
  events: {
178
416
  onAssistant: (_raw, invocation) => {
179
417
  if (!invocation) {
180
- console.log(formatRalphStreamEvent("error", "invalid tool-call json; retrying"));
418
+ console.log(formatRalphStreamEvent("error", "invalid tool-call json; retrying", renderOpts));
181
419
  } else if (invocation.tool !== "done") {
182
- console.log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested`));
420
+ console.log(formatRalphStreamEvent("step", `tool ${invocation.tool} requested`, renderOpts));
421
+ }
422
+ },
423
+ onStep: async step => {
424
+ console.log(formatRalphStreamEvent("step", `${role.title} thinking ${step}/${maxSteps}`, renderOpts));
425
+ try {
426
+ await maybeCompact(history, { model, contextTokens });
427
+ } catch (err) {
428
+ // LLM summary failure does not halt team
183
429
  }
184
430
  },
185
- onStep: step => console.log(formatRalphStreamEvent("step", `${role.title} thinking ${step}/${maxSteps}`)),
186
- onToolResult: (tool, ok) => console.log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`)),
187
- onError: msg => console.log(formatRalphStreamEvent("error", msg)),
431
+ onToolResult: (tool, ok) => {
432
+ if (ok && (tool === "write" || tool === "edit" || tool === "bash")) mutationsOk++;
433
+ console.log(formatRalphStreamEvent(ok ? "complete" : "error", `tool ${tool}`, renderOpts));
434
+ },
435
+ onNotice: msg => console.log(formatRalphStreamEvent("step", msg, renderOpts)),
188
436
  },
189
437
  });
190
438
 
191
- if (result.done) {
192
- console.log(formatRalphStreamEvent("complete", `${role.title} finished task`));
193
- return true;
194
- }
195
- console.log(formatRalphStreamEvent("error", `${role.title} did not converge within ${result.steps} steps`));
196
- return false;
197
- }
198
- export const StepSchema = z.object({
199
- name: z.string(),
200
- }).passthrough();
201
-
202
- export const PlanSchema = z.object({
203
- name: z.string(),
204
- steps: z.array(StepSchema),
205
- }).passthrough();
206
-
207
- function parseValue(v: string): any {
208
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
209
- return v.slice(1, -1);
439
+ const reason = result.doneReason?.trim() || `${role.title} did not converge within ${result.steps} steps`;
440
+ if (!result.done) {
441
+ console.log(formatRalphStreamEvent("error", reason, renderOpts));
442
+ return false;
210
443
  }
211
- if (v === "true") return true;
212
- if (v === "false") return false;
213
- if (v === "null") return null;
214
- if (v === "") return "";
215
- if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
216
- return v;
217
- }
218
-
219
- export function parseYaml(yamlStr: string): any {
220
- const lines = yamlStr.split(/\r?\n/).map(line => {
221
- const commentIdx = line.indexOf('#');
222
- const cleanLine = commentIdx !== -1 ? line.slice(0, commentIdx) : line;
223
- return {
224
- raw: cleanLine,
225
- trimmed: cleanLine.trim(),
226
- indent: cleanLine.length - cleanLine.trimStart().length
227
- };
228
- }).filter(l => l.trimmed !== '');
229
-
230
- let idx = 0;
231
-
232
- function parseBlock(baseIndent: number): any {
233
- let result: any = null;
234
- let isArray = false;
235
-
236
- if (idx < lines.length) {
237
- if (lines[idx].trimmed.startsWith('-')) {
238
- isArray = true;
239
- result = [];
240
- } else {
241
- result = {};
242
- }
243
- }
244
-
245
- while (idx < lines.length) {
246
- const line = lines[idx];
247
- if (line.indent < baseIndent) {
248
- break;
249
- }
250
-
251
- if (isArray) {
252
- if (!line.trimmed.startsWith('-')) {
253
- if (result.length > 0 && typeof result[result.length - 1] === 'object') {
254
- const colonIdx = line.trimmed.indexOf(':');
255
- if (colonIdx !== -1) {
256
- const k = line.trimmed.slice(0, colonIdx).trim();
257
- const rawVal = line.trimmed.slice(colonIdx + 1).trim();
258
- if (rawVal === '') {
259
- idx++;
260
- result[result.length - 1][k] = parseBlock(line.indent + 1);
261
- continue;
262
- } else {
263
- result[result.length - 1][k] = parseValue(rawVal);
264
- }
265
- } else {
266
- throw new Error(`Invalid line inside array block: "${line.trimmed}"`);
267
- }
268
- } else {
269
- throw new Error(`Invalid line in array: "${line.trimmed}"`);
270
- }
271
- idx++;
272
- continue;
273
- }
274
-
275
- const rest = line.trimmed.slice(1).trim();
276
- if (rest === '') {
277
- idx++;
278
- const nested = parseBlock(line.indent + 1);
279
- result.push(nested);
280
- } else if (rest.includes(':')) {
281
- const colonIdx = rest.indexOf(':');
282
- const k = rest.slice(0, colonIdx).trim();
283
- const rawVal = rest.slice(colonIdx + 1).trim();
284
- if (rawVal === '') {
285
- idx++;
286
- const nestedObj = { [k]: parseBlock(line.indent + 2) };
287
- result.push(nestedObj);
288
- } else {
289
- const item: any = { [k]: parseValue(rawVal) };
290
- result.push(item);
291
- idx++;
292
- while (idx < lines.length && !lines[idx].trimmed.startsWith('-') && lines[idx].indent >= line.indent + 2) {
293
- const subLine = lines[idx];
294
- const subColonIdx = subLine.trimmed.indexOf(':');
295
- if (subColonIdx !== -1) {
296
- const subK = subLine.trimmed.slice(0, subColonIdx).trim();
297
- const rawSubVal = subLine.trimmed.slice(subColonIdx + 1).trim();
298
- if (rawSubVal === '') {
299
- idx++;
300
- item[subK] = parseBlock(subLine.indent + 1);
301
- } else {
302
- item[subK] = parseValue(rawSubVal);
303
- idx++;
304
- }
305
- } else {
306
- throw new Error(`Invalid sub-line in block mapping: "${subLine.trimmed}"`);
307
- }
308
- }
309
- }
310
- } else {
311
- result.push(parseValue(rest));
312
- idx++;
313
- }
314
- } else {
315
- const colonIdx = line.trimmed.indexOf(':');
316
- if (colonIdx === -1) {
317
- throw new Error(`Invalid line: "${line.trimmed}"`);
318
- }
319
-
320
- const k = line.trimmed.slice(0, colonIdx).trim();
321
- const rawVal = line.trimmed.slice(colonIdx + 1).trim();
322
444
 
323
- if (rawVal === '') {
324
- idx++;
325
- result[k] = parseBlock(line.indent + 1);
326
- } else {
327
- result[k] = parseValue(rawVal);
328
- idx++;
329
- }
330
- }
331
- }
445
+ const contract = validateSubagentDoneReason(role, reason);
446
+ if (!contract.ok) {
447
+ console.log(formatRalphStreamEvent("error", `${role.title} report incomplete: missing ${contract.missing?.join(", ")}`, renderOpts));
448
+ return false;
449
+ }
332
450
 
333
- return result;
451
+ const gate = parseRoleGateVerdict(role.id, reason);
452
+ if (!gate.ok) {
453
+ console.log(formatRalphStreamEvent("error", gate.message ?? `${role.title} blocked execution`, renderOpts));
454
+ return false;
334
455
  }
335
456
 
336
- return parseBlock(0);
457
+ if (!role.readOnly && mutationsOk === 0) {
458
+ // Round-8: a mutating role finished without ONE successful mutation — the
459
+ // task may be legitimately read-only, but its "Changed Files:" claim is
460
+ // unverified; warn instead of silently trusting the report.
461
+ console.log(formatRalphStreamEvent("error", `${role.title} completed WITHOUT any successful write/edit/bash — treat its changed-files claim as unverified.`, renderOpts));
462
+ }
463
+ console.log(formatRalphStreamEvent("complete", `${role.title} finished task`, renderOpts));
464
+ return true;
337
465
  }