jeo-code 0.7.33 → 0.7.37

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.
@@ -9,14 +9,21 @@ import {
9
9
  import { PlanSchema, normalizePlanShape, parseYaml } from "../agent/plan";
10
10
  import { getSubagentRole, subagentRoleIds } from "../agent/subagents";
11
11
 
12
- export async function runApproveCommand(args: string[] = []): Promise<void> {
13
- const cwd = process.cwd();
14
- const planPathInput = args[0];
12
+ export interface ApprovePlanResult {
13
+ ok: boolean;
14
+ message: string;
15
+ }
15
16
 
17
+ /** Core approval gate, shared by the `jeo approve` CLI command and the agent-facing
18
+ * `approve` tool (src/agent/approve-tool.ts). ALL of the plan-quality checks below
19
+ * (schema shape, known roles, persisted [OKAY] consensus verdict, hash-vs-consensus
20
+ * match) still apply no matter who calls this — approval is a content gate, not an
21
+ * identity gate. 2026-07: the identity gate (only a human running `jeo approve` in
22
+ * their own terminal could flip `approved: true`) was removed per explicit user
23
+ * direction — the agent can now call this itself via the `approve` tool. */
24
+ export async function approvePlan(planPathInput: string, cwd: string): Promise<ApprovePlanResult> {
16
25
  if (!planPathInput) {
17
- console.log("[ERROR] Plan path argument is required.");
18
- process.exitCode = 1;
19
- return;
26
+ return { ok: false, message: "[ERROR] Plan path argument is required." };
20
27
  }
21
28
 
22
29
  const resolvedInputPath = path.resolve(cwd, planPathInput);
@@ -25,23 +32,17 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
25
32
  try {
26
33
  await fs.access(resolvedInputPath);
27
34
  } catch {
28
- console.log(`[ERROR] Plan file not found: ${resolvedInputPath}`);
29
- process.exitCode = 1;
30
- return;
35
+ return { ok: false, message: `[ERROR] Plan file not found: ${resolvedInputPath}` };
31
36
  }
32
37
 
33
38
  // Read ralplan state
34
39
  const ralplanState = await readWorkflowState("ralplan", cwd);
35
40
  if (!ralplanState) {
36
- console.log(`[ERROR] No ralplan workflow state found. Please run 'jeo ralplan' first.`);
37
- process.exitCode = 1;
38
- return;
41
+ return { ok: false, message: `[ERROR] No ralplan workflow state found. Please run 'jeo ralplan' first.` };
39
42
  }
40
43
 
41
44
  if (!ralplanState.plan_path) {
42
- console.log(`[ERROR] No plan path associated with the current ralplan state.`);
43
- process.exitCode = 1;
44
- return;
45
+ return { ok: false, message: `[ERROR] No plan path associated with the current ralplan state.` };
45
46
  }
46
47
 
47
48
  // Compare canonical (symlink-resolved) paths so a relative arg, an absolute arg,
@@ -56,20 +57,19 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
56
57
  const resolvedStatePath = path.resolve(cwd, ralplanState.plan_path);
57
58
  const [canonInput, canonState] = await Promise.all([canonical(resolvedInputPath), canonical(resolvedStatePath)]);
58
59
  if (canonInput !== canonState) {
59
- console.log(
60
- `[ERROR] Provided plan path does not match the active plan in the ralplan state.\n` +
61
- ` provided: ${resolvedInputPath}\n` +
62
- ` active: ${resolvedStatePath}\n` +
63
- ` Run 'jeo approve "${resolvedStatePath}"' to approve the active plan.`
64
- );
65
- process.exitCode = 1;
66
- return;
60
+ return {
61
+ ok: false,
62
+ message:
63
+ `[ERROR] Provided plan path does not match the active plan in the ralplan state.\n` +
64
+ ` provided: ${resolvedInputPath}\n` +
65
+ ` active: ${resolvedStatePath}\n` +
66
+ ` Run 'jeo approve "${resolvedStatePath}"' to approve the active plan.`,
67
+ };
67
68
  }
68
69
 
69
70
  // Idempotency: check if already approved
70
71
  if (ralplanState.approved) {
71
- console.log(`[SUCCESS] Plan is already approved.`);
72
- return;
72
+ return { ok: true, message: `[SUCCESS] Plan is already approved.` };
73
73
  }
74
74
 
75
75
  // Round-10 #4 (architect ref 8-Round10Planning): approval is a GATE, not a
@@ -81,52 +81,53 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
81
81
  planContent = await fs.readFile(resolvedInputPath, "utf-8");
82
82
  const parsed = PlanSchema.safeParse(normalizePlanShape(parseYaml(planContent)));
83
83
  if (!parsed.success) {
84
- console.log(
85
- `[ERROR] Refusing to approve: the plan is not in the shape 'jeo team' executes (top-level 'steps:' list of { name, role? }).\n` +
86
- ` ${parsed.error.issues[0]?.message ?? "schema mismatch"}\n` +
87
- ` Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`,
88
- );
89
- process.exitCode = 1;
90
- return;
84
+ return {
85
+ ok: false,
86
+ message:
87
+ `[ERROR] Refusing to approve: the plan is not in the shape 'jeo team' executes (top-level 'steps:' list of { name, role? }).\n` +
88
+ ` ${parsed.error.issues[0]?.message ?? "schema mismatch"}\n` +
89
+ ` Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`,
90
+ };
91
91
  }
92
92
  const cfg = await readGlobalConfig();
93
93
  const unknown = [...new Set(parsed.data.steps.map(s => s.role?.trim()).filter((r): r is string => !!r && !getSubagentRole(r, cfg)))];
94
94
  if (unknown.length > 0) {
95
- console.log(
96
- `[ERROR] Refusing to approve: plan references unknown subagent role(s): ${unknown.join(", ")}.\n` +
97
- ` Known roles: ${subagentRoleIds(cfg).join(", ")}. Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`,
98
- );
99
- process.exitCode = 1;
100
- return;
95
+ return {
96
+ ok: false,
97
+ message:
98
+ `[ERROR] Refusing to approve: plan references unknown subagent role(s): ${unknown.join(", ")}.\n` +
99
+ ` Known roles: ${subagentRoleIds(cfg).join(", ")}. Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`,
100
+ };
101
101
  }
102
102
  } catch (err: any) {
103
- console.log(`[ERROR] Refusing to approve: the plan file is not parseable YAML (${err.message}). Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`);
104
- process.exitCode = 1;
105
- return;
103
+ return {
104
+ ok: false,
105
+ message: `[ERROR] Refusing to approve: the plan file is not parseable YAML (${err.message}). Fix ${resolvedInputPath} or re-run 'jeo ralplan'.`,
106
+ };
106
107
  }
107
108
 
108
109
  // Round-11: approval also requires the PERSISTED consensus verdict — a plan
109
110
  // that never passed (or failed) the critic gate cannot be approved. States
110
111
  // from older ralplan runs lack the field; re-running ralplan heals them.
111
112
  if (ralplanState.consensus !== "okay") {
112
- console.log(
113
- `[ERROR] Refusing to approve: the plan lacks an [OKAY] consensus verdict (recorded: ${ralplanState.consensus ?? "none"}).\n` +
114
- ` Re-run 'jeo ralplan' so the consensus critic can review the plan, then approve again.`,
115
- );
116
- process.exitCode = 1;
117
- return;
113
+ return {
114
+ ok: false,
115
+ message:
116
+ `[ERROR] Refusing to approve: the plan lacks an [OKAY] consensus verdict (recorded: ${ralplanState.consensus ?? "none"}).\n` +
117
+ ` Re-run 'jeo ralplan' so the consensus critic can review the plan, then approve again.`,
118
+ };
118
119
  }
119
120
 
120
121
  // Round-13: verify the plan's hash matches the consensus hash to prevent silent edits
121
122
  if (ralplanState.consensus_hash) {
122
123
  const currentHash = createHash("sha256").update(planContent).digest("hex");
123
124
  if (currentHash !== ralplanState.consensus_hash) {
124
- console.log(
125
- `[ERROR] Refusing to approve: the plan file has been modified since the consensus critic reviewed it.\n` +
126
- ` Re-run 'jeo ralplan' to let the critic review the updated plan, then approve again.`
127
- );
128
- process.exitCode = 1;
129
- return;
125
+ return {
126
+ ok: false,
127
+ message:
128
+ `[ERROR] Refusing to approve: the plan file has been modified since the consensus critic reviewed it.\n` +
129
+ ` Re-run 'jeo ralplan' to let the critic review the updated plan, then approve again.`,
130
+ };
130
131
  }
131
132
  }
132
133
 
@@ -134,5 +135,13 @@ export async function runApproveCommand(args: string[] = []): Promise<void> {
134
135
  ralplanState.approved = true;
135
136
  await writeWorkflowState("ralplan", ralplanState, cwd);
136
137
 
137
- console.log(`[SUCCESS] Plan approved successfully.`);
138
+ return { ok: true, message: `[SUCCESS] Plan approved successfully.` };
139
+ }
140
+
141
+ export async function runApproveCommand(args: string[] = []): Promise<void> {
142
+ const cwd = process.cwd();
143
+ const planPathInput = args[0];
144
+ const { ok, message } = await approvePlan(planPathInput, cwd);
145
+ console.log(message);
146
+ if (!ok) process.exitCode = 1;
138
147
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `jeo daemon status|start|stop|reload` — manage the background Telegram
3
+ * notification/subagent-control daemon (gjc `gjc daemon` parity, scoped to
4
+ * jeo's one daemon kind). See `src/agent/notify/daemon-control.ts`.
5
+ */
6
+ import { daemonStatus, startDaemon, stopDaemon, reloadDaemon } from "../agent/notify/daemon-control";
7
+
8
+ async function printStatus(): Promise<void> {
9
+ const status = await daemonStatus();
10
+ if (!status.configured) {
11
+ console.log("notifications not configured — run 'jeo notify setup' first.");
12
+ }
13
+ if (status.running) {
14
+ console.log(`running (pid ${status.pid}, started ${new Date(status.startedAt!).toISOString()})`);
15
+ } else if (status.stale) {
16
+ console.log("stale — a lock file exists but its pid is dead (previous daemon crashed without cleanup). 'jeo daemon start' will reclaim it.");
17
+ } else {
18
+ console.log("stopped");
19
+ }
20
+ }
21
+
22
+ export async function runDaemonCommand(args: string[]): Promise<void> {
23
+ const action = args[0] ?? "status";
24
+ if (action === "status" || action === "list") return printStatus();
25
+ if (action === "start") {
26
+ const res = await startDaemon();
27
+ console.log(res.message);
28
+ if (!res.ok) process.exitCode = 1;
29
+ return;
30
+ }
31
+ if (action === "stop") {
32
+ const res = await stopDaemon();
33
+ console.log(res.message);
34
+ if (!res.ok) process.exitCode = 1;
35
+ return;
36
+ }
37
+ if (action === "reload") {
38
+ const res = await reloadDaemon();
39
+ console.log(res.message);
40
+ if (!res.ok) process.exitCode = 1;
41
+ return;
42
+ }
43
+ console.log(`Unknown 'jeo daemon' action '${action}'. Usage: jeo daemon [status|start|stop|reload]`);
44
+ process.exitCode = 1;
45
+ }
@@ -8,11 +8,14 @@ import { memoryPromptSection, spawnDetachedDistill, recordFailedAttempt } from "
8
8
  import { createTaskTool, taskToolProtocolLine, type TaskSubEvent } from "../agent/task-tool";
9
9
  import { createSubagentTool, SUBAGENT_TOOL_PROTOCOL_LINE } from "../agent/subagent-tool";
10
10
  import { SubagentRegistry } from "../agent/subagent-registry";
11
+ import { stopSessionNotifyEndpoint } from "../agent/notify/session-endpoint";
12
+
11
13
  import { createTodoTool, TODO_TOOL_PROTOCOL_LINE } from "../agent/todo-tool";
12
14
  import { createJobTool, JOB_TOOL_PROTOCOL_LINE } from "../agent/job-tool";
13
15
  import { JobRegistry } from "../agent/job-registry";
14
16
  import { createIrcTool, IRC_TOOL_PROTOCOL_LINE } from "../agent/irc-tool";
15
17
  import { createGoalTool, GOAL_TOOL_PROTOCOL_LINE } from "../agent/goal-tool";
18
+ import { createApproveTool, APPROVE_TOOL_PROTOCOL_LINE } from "../agent/approve-tool";
16
19
  import { LaunchTui } from "../tui/app";
17
20
  import { runDeepInterviewEngine, type DeepInterviewEngineOptions } from "./deep-interview";
18
21
  import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
@@ -525,7 +528,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
525
528
  // pi-style: load project context (JEO.md / AGENTS.md / .jeo/context.md / CLAUDE.md) into the prompt.
526
529
  const contextFiles = await loadProjectContext(cwd);
527
530
 
528
- const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "todo", "subagent", "job", "irc", "goal", "ast_grep", "ast_edit", "computer", "lsp", "lsp_rename", "debug", "browser"]);
531
+ const KNOWN_TOOLS = new Set(["read", "write", "edit", "bash", "find", "search", "ls", "task", "todo", "subagent", "job", "irc", "goal", "approve", "ast_grep", "ast_edit", "computer", "lsp", "lsp_rename", "debug", "browser"]);
529
532
  let allowedTools = new Set(KNOWN_TOOLS);
530
533
 
531
534
  if (flags.noTools) {
@@ -599,6 +602,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
599
602
  (allowedTools.has("job") ? "\n\nBackground jobs: " + JOB_TOOL_PROTOCOL_LINE : "") +
600
603
  (allowedTools.has("irc") ? "\n\nPeer messaging: " + IRC_TOOL_PROTOCOL_LINE : "") +
601
604
  (allowedTools.has("goal") ? "\n\nGoal tracking: " + GOAL_TOOL_PROTOCOL_LINE : "") +
605
+ (allowedTools.has("approve") ? "\n\nPlan approval: " + APPROVE_TOOL_PROTOCOL_LINE : "") +
602
606
  (effectiveNoSkills ? "" :
603
607
  "\n\nJEO workflow routing:\n" +
604
608
  "- Answer the user's request DIRECTLY. Never reply with a catalog, list, or summary of skills unless the user explicitly asks what skills exist.\n" +
@@ -880,10 +884,33 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
880
884
  // Refresh injected OKF memory with THIS turn's query before the model call so
881
885
  // failure-first ranking fires and same-session dead ends resurface.
882
886
  await refreshSessionMemory(userInput);
883
- const compRes = await maybeCompact(history, {
884
- model: activeModel,
885
- contextTokens,
887
+ // Esc-cancellable turn-boundary compaction preflight: a slow/hung summarization
888
+ // LLM call must not be un-cancellable just because it runs before the main
889
+ // per-turn abort harness (created below) exists yet. A short-lived harness scoped
890
+ // to just this call gives Esc/Ctrl+C the same abort path without wiring the full
891
+ // mid-turn steering machinery this early (which needs turn-scoped vars not
892
+ // constructed until after compaction/history mutation settles).
893
+ const compactHarness = createInFlightAbortHarness({
894
+ captureEsc: !!tui,
895
+ onAbortNotice: msg => {
896
+ if (tui) tui.events().onNotice?.(msg);
897
+ else console.log(msg);
898
+ },
899
+ onHardExit: () => {
900
+ if (tui) tui.finish("Cancelled.", { ok: false });
901
+ process.exit(130);
902
+ },
886
903
  });
904
+ let compRes: Awaited<ReturnType<typeof maybeCompact>>;
905
+ try {
906
+ compRes = await maybeCompact(history, {
907
+ model: activeModel,
908
+ contextTokens,
909
+ signal: compactHarness.controller.signal,
910
+ });
911
+ } finally {
912
+ compactHarness.dispose();
913
+ }
887
914
  if (compRes.error) {
888
915
  throw new Error(compRes.error);
889
916
  }
@@ -1085,6 +1112,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1085
1112
  job: createJobTool(jobRegistry),
1086
1113
  irc: createIrcTool(subagentRegistry),
1087
1114
  goal: createGoalTool(),
1115
+ approve: createApproveTool(),
1088
1116
  };
1089
1117
  const tools = filterToolMap(fullTools, Array.from(allowedTools));
1090
1118
  // Opik observability (opt-in via JEO_OPIK): one trace per turn, spans per
@@ -1143,7 +1171,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1143
1171
  } finally {
1144
1172
  harness.dispose();
1145
1173
  subagentRegistry.cancelAll(); // #9: no detached run leaks past the turn
1174
+ await stopSessionNotifyEndpoint(subagentRegistry); // tear down the Telegram remote-control endpoint, if one was started
1146
1175
  jobRegistry.cancelAll(); // background jobs are turn-scoped too — no orphaned processes
1176
+
1147
1177
  // Steering typed but never drained (e.g. entered just after the final step)
1148
1178
  // must not be lost — fold it into the next prompt draft so it runs next.
1149
1179
  const leftover = steerInbox.splice(0, steerInbox.length).map(s => s.trim()).filter(Boolean);
@@ -3587,6 +3617,26 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3587
3617
  }
3588
3618
  continue;
3589
3619
  }
3620
+ if (input === "/wiki" || input.startsWith("/wiki ")) {
3621
+ const decision = decideWikiSlash(
3622
+ input,
3623
+ resolveWikiRoot(await readGlobalConfig()),
3624
+ !!jeoEnv("WIKI_ROOT"),
3625
+ );
3626
+ for (const line of decision.lines) console.log(line);
3627
+ if (decision.kind === "clear") {
3628
+ await saveConfigPatch(() => ({ wikiRoot: undefined }));
3629
+ delete process.env.JEO_WIKI_ROOT;
3630
+ sessionWikiRoot = undefined;
3631
+ await refreshSessionMemory("");
3632
+ } else if (decision.kind === "set") {
3633
+ await saveConfigPatch(() => ({ wikiRoot: decision.persistArg }));
3634
+ process.env.JEO_WIKI_ROOT = decision.root;
3635
+ sessionWikiRoot = decision.root;
3636
+ await refreshSessionMemory("");
3637
+ }
3638
+ continue;
3639
+ }
3590
3640
  if (input === "/evolve") {
3591
3641
  await runEvolveSimulation(animateAsciiArt);
3592
3642
  continue;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `jeo notify setup|status` — configure and inspect the remote subagent
3
+ * notification/control channel (gjc `gjc notify setup`/`gjc notify status`
4
+ * parity, scoped to jeo's Telegram + subagent surface; see `src/agent/notify/`).
5
+ */
6
+ import { createInterface } from "node:readline/promises";
7
+ import { readGlobalConfig, saveConfigPatch } from "../agent/state";
8
+ import { TelegramApi, maskToken } from "../agent/notify/telegram-api";
9
+ import { daemonStatus } from "../agent/notify/daemon-control";
10
+
11
+ function parseFlags(args: string[]): { token?: string; chatId?: string } {
12
+ const out: { token?: string; chatId?: string } = {};
13
+ for (let i = 0; i < args.length; i++) {
14
+ if (args[i] === "--token" && args[i + 1]) out.token = args[++i];
15
+ else if (args[i] === "--chat-id" && args[i + 1]) out.chatId = args[++i];
16
+ }
17
+ return out;
18
+ }
19
+
20
+ async function runSetup(args: string[]): Promise<void> {
21
+ const flags = parseFlags(args);
22
+ let token = flags.token;
23
+ let chatId = flags.chatId;
24
+
25
+ if (!token || !chatId) {
26
+ if (!process.stdin.isTTY) {
27
+ console.log(
28
+ "jeo notify setup needs an interactive terminal (TTY) unless both --token and --chat-id are given.\n" +
29
+ "Non-interactive: jeo notify setup --token <botToken> --chat-id <chatId>",
30
+ );
31
+ return;
32
+ }
33
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
34
+ try {
35
+ console.log("\n=== jeo notify setup (Telegram) ===");
36
+ console.log("Create a bot with @BotFather (https://core.telegram.org/bots/features#botfather), then paste its token below.\n");
37
+ if (!token) token = (await rl.question("Telegram BotFather token: ")).trim();
38
+ if (!token) {
39
+ console.log("No token given — aborting.");
40
+ return;
41
+ }
42
+ const api = new TelegramApi(token);
43
+ const me = await api.getMe();
44
+ if (!me.ok || !me.result) {
45
+ console.log(`Telegram getMe failed: ${me.description ?? "invalid token"}. Re-copy the token from BotFather and retry.`);
46
+ return;
47
+ }
48
+ console.log(`Bot verified: @${me.result.username}`);
49
+
50
+ if (!chatId) {
51
+ console.log(`\nMessage @${me.result.username} from your Telegram account now (any text). Waiting...`);
52
+ let offset: number | undefined;
53
+ const deadline = Date.now() + 120_000;
54
+ while (Date.now() < deadline && !chatId) {
55
+ const updates = await api.getUpdates(offset, 5);
56
+ if (updates.ok) {
57
+ for (const u of updates.result) {
58
+ offset = u.update_id + 1;
59
+ const chat = u.message?.chat;
60
+ if (chat && chat.type === "private") {
61
+ chatId = String(chat.id);
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ }
67
+ if (!chatId) {
68
+ console.log("Timed out waiting for a private message. Re-run 'jeo notify setup' and message the bot promptly.");
69
+ return;
70
+ }
71
+ console.log(`Paired private chat id ${chatId}.`);
72
+ }
73
+ } finally {
74
+ rl.close();
75
+ }
76
+ } else {
77
+ const api = new TelegramApi(token);
78
+ const me = await api.getMe();
79
+ if (!me.ok) {
80
+ console.log(`Telegram getMe failed: ${me.description ?? "invalid token"}.`);
81
+ return;
82
+ }
83
+ }
84
+
85
+ await saveConfigPatch(raw => ({
86
+ notifications: { ...raw.notifications, enabled: true, telegram: { botToken: token, chatId } },
87
+ }));
88
+ console.log(`\nNotifications enabled. botToken=${maskToken(token!)} chatId=${chatId}`);
89
+ console.log("Start the daemon with 'jeo daemon start', then run a detached subagent (task {detached:true}) to see it appear in Telegram.");
90
+ }
91
+
92
+ async function runStatus(): Promise<void> {
93
+ const config = await readGlobalConfig();
94
+ const enabled = Boolean(config.notifications?.enabled);
95
+ const botToken = config.notifications?.telegram?.botToken;
96
+ const chatId = config.notifications?.telegram?.chatId;
97
+ const status = await daemonStatus();
98
+ console.log(`enabled=${enabled}`);
99
+ console.log(`botToken=${botToken ? maskToken(botToken) : "(not set)"}`);
100
+ console.log(`chatId=${chatId ?? "(not set)"}`);
101
+ console.log(`daemon=${status.running ? `running (pid ${status.pid})` : status.stale ? "stale (crashed without cleanup)" : "stopped"}`);
102
+ }
103
+
104
+ export async function runNotifyCommand(args: string[]): Promise<void> {
105
+ const sub = args[0] ?? "status";
106
+ if (sub === "setup") return runSetup(args.slice(1));
107
+ if (sub === "status") return runStatus();
108
+ console.log(`Unknown 'jeo notify' subcommand '${sub}'. Usage: jeo notify [setup|status]`);
109
+ }
@@ -142,7 +142,17 @@ export async function runRalplanEngine(opts: RalplanEngineOptions = {}): Promise
142
142
  ` - name: "<imperative task, e.g. Implement reverse() in src/reverse.ts>"\n` +
143
143
  ` role: executor # one of: executor | planner | architect | critic\n` +
144
144
  ` target: "<primary file path>"\n` +
145
- `Provide 3-8 concrete, ordered steps. Output ONLY the YAML.`;
145
+ ` parallel_group: "<optional short id, e.g. g1>"\n` +
146
+ `Provide 3-8 concrete, ordered steps. Output ONLY the YAML.\n\n` +
147
+ `parallel_group is OPTIONAL and OFF by default — omit it unless you are certain. ` +
148
+ `'jeo team' executes steps strictly in array order, EXCEPT a contiguous (adjacent, ` +
149
+ `no other step in between) run of steps sharing the SAME non-empty parallel_group value, ` +
150
+ `which it runs CONCURRENTLY in isolated git worktrees and merges back. Only mark steps ` +
151
+ `parallel_group when they are TRULY independent: each touches DISJOINT files with no step ` +
152
+ `reading or building on another's output. Never mark two steps parallel if one depends on, ` +
153
+ `reads, or extends a file another writes. When unsure, leave parallel_group out — a plain ` +
154
+ `serial step is always correct; a wrongly-parallel step risks a merge conflict that halts ` +
155
+ `execution.`;
146
156
 
147
157
  const PLANNER = `You are the PLANNER. From the crystallized spec, sequence the work into a logical, outcome-based progression of concrete, ordered tasks.\n` + SCHEMA_SPEC;
148
158
  const ARCHITECT = `You are the ARCHITECT. Review the Planner's draft for technical feasibility, correct file targets, directory structure, and any missing setup/wiring/test steps. Return an improved plan (same shape).\n` + SCHEMA_SPEC;