oh-my-opencode 4.16.1 → 4.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/pre-publish-review/SKILL.md +3 -0
- package/README.md +2 -2
- package/dist/agents/hephaestus/agent.d.ts +1 -1
- package/dist/agents/hephaestus/gpt-5-6.d.ts +2 -0
- package/dist/agents/momus-gpt-5-6.d.ts +1 -0
- package/dist/agents/types.d.ts +2 -0
- package/dist/cli/index.js +800 -536
- package/dist/cli-node/index.js +800 -536
- package/dist/features/background-agent/error-classifier.d.ts +5 -0
- package/dist/features/background-agent/parent-wake-dedupe.d.ts +1 -0
- package/dist/features/background-agent/parent-wake-flush-runner.d.ts +2 -0
- package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -0
- package/dist/features/builtin-commands/templates/refactor-sections/intro-and-analysis.d.ts +1 -1
- package/dist/features/builtin-commands/templates/remove-ai-slops.d.ts +1 -1
- package/dist/index.js +464 -54
- package/dist/shared/live-server-route.d.ts +1 -1
- package/dist/skills/review-work/SKILL.md +4 -2
- package/dist/skills/start-work/SKILL.md +2 -0
- package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
- package/dist/skills/ulw-research/SKILL.md +5 -3
- package/dist/skills/visual-qa/SKILL.md +1 -1
- package/dist/tui.js +125 -13
- package/package.json +13 -13
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +398 -246
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/{hephaestus.md → hephaestus/gpt-5.5.md} +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +93 -0
- package/packages/omo-codex/plugin/components/rules/dist/cli.js +43 -24
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/src/codex-hook.ts +2 -1
- package/packages/omo-codex/plugin/components/rules/src/dynamic-target-fingerprints.ts +15 -6
- package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +7 -2
- package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +2 -2
- package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +32 -0
- package/packages/omo-codex/plugin/components/rules/test/hephaestus-model-variant.test.ts +106 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +1 -1
- package/packages/omo-codex/plugin/components/teammode/AGENTS.md +12 -9
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +136 -76
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +94 -23
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +94 -14
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-transport.mjs +55 -0
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +72 -18
- package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +23 -0
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +24 -41
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-clone-fidelity-reviewer.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-executor.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +21 -49
- package/packages/omo-codex/plugin/components/ultrawork/agents/metis.toml +17 -29
- package/packages/omo-codex/plugin/components/ultrawork/agents/momus.toml +20 -50
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +18 -39
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +1 -0
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +1 -0
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -0
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +3 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/skill-contract.test.ts +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +73 -19
- package/packages/omo-codex/plugin/scripts/entry-guard.mjs +26 -0
- package/packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs +3 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +221 -20
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/subagent-limit-guard.mjs +71 -3
- package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +35 -7
- package/packages/omo-codex/plugin/scripts/migrate-omo-sot.mjs +2 -2
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +2 -2
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +7 -5
- package/packages/omo-codex/plugin/scripts/sync-version.mjs +4 -2
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +136 -76
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +94 -23
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +94 -14
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-transport.mjs +55 -0
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +72 -18
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +1 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -0
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +3 -1
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +3 -3
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +47 -1
- package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +1 -1
- package/packages/omo-codex/plugin/test/aggregate-skills.test.mjs +4 -2
- package/packages/omo-codex/plugin/test/auto-update-restart-notice.test.mjs +33 -0
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +35 -1
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +412 -19
- package/packages/omo-codex/plugin/test/multi-agent-v2-regression.test.mjs +71 -0
- package/packages/omo-codex/plugin/test/subagent-limit-migration.test.mjs +65 -0
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +13 -4
- package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +345 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +436 -284
- package/packages/omo-codex/scripts/install-generated-bundle.test.mjs +111 -1
- package/packages/shared-skills/skills/review-work/SKILL.md +4 -2
- package/packages/shared-skills/skills/start-work/SKILL.md +2 -0
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/shared-skills/skills/ulw-research/SKILL.md +5 -3
- package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs
CHANGED
|
@@ -1,23 +1,57 @@
|
|
|
1
1
|
// team-guide.mjs - render the member-facing field manual (guide.md) and the short
|
|
2
|
-
// bootstrap trigger a new
|
|
2
|
+
// bootstrap trigger a new member reads on startup. Pure string builders, no I/O.
|
|
3
3
|
//
|
|
4
4
|
// The manual is the durable, two-channel replacement for per-prompt guidance: the leader
|
|
5
|
-
// writes it once at team creation, and every member
|
|
6
|
-
// receiving the rules inline each turn.
|
|
5
|
+
// writes it once at team creation, and every member is told to READ it instead of
|
|
6
|
+
// receiving the rules inline each turn. Every rendered surface branches on the team's
|
|
7
|
+
// transport: multi_agent_v2 members are native agents addressed by agent path; codex_app
|
|
8
|
+
// members are app threads addressed by thread id.
|
|
9
|
+
|
|
10
|
+
import { isMultiAgentV2, LEADER_AGENT_PATH } from "./team-transport.mjs";
|
|
7
11
|
|
|
8
12
|
export function codexThreadLink(threadId) {
|
|
9
13
|
return `codex://threads/${threadId}`;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
function
|
|
16
|
+
function transportLabel(team) {
|
|
17
|
+
return isMultiAgentV2(team)
|
|
18
|
+
? "MultiAgentV2 (native agents: flat `spawn_agent` / `send_message` / `followup_task`)"
|
|
19
|
+
: "Codex App threads (`codex_app.*` tools)";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function memberEndpoint(member, v2) {
|
|
23
|
+
if (v2) {
|
|
24
|
+
return member.agentPath ? ` | task \`${member.taskName}\` | agent \`${member.agentPath}\`` : " | agent not spawned yet";
|
|
25
|
+
}
|
|
13
26
|
const thread = member.threadId ? ` | thread ${member.threadId} (${codexThreadLink(member.threadId)})` : " | thread not created yet";
|
|
27
|
+
const title = member.threadTitle ? ` | thread title \`${member.threadTitle}\`` : "";
|
|
28
|
+
return `${title}${thread}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function memberLine(member, v2) {
|
|
14
32
|
const wt = member.worktree?.path ? ` | worktree ${member.worktree.path}` : "";
|
|
15
33
|
const name = member.name ? ` "${member.name}"` : "";
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
return `- **${member.id}**${name} (${member.lens}) - ${member.focus}${member.deliverable ? ` -> ${member.deliverable}` : ""}${memberEndpoint(member, v2)}${wt} [${member.status}]`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function worktreeMemberEndpoint(member, v2) {
|
|
38
|
+
if (v2) return member.agentPath ? `; agent \`${member.agentPath}\`` : "";
|
|
39
|
+
return member.threadId ? `; thread ${codexThreadLink(member.threadId)}` : "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function worktreeReadinessNote(v2) {
|
|
43
|
+
if (v2) {
|
|
44
|
+
return `Your worktree is created by the leader BEFORE you spawn; the exact path is in your row
|
|
45
|
+
above and in your bootstrap. If the leader enables isolation mid-run, the new path arrives
|
|
46
|
+
as a \`followup_task\` - switch to it before your next edit.`;
|
|
47
|
+
}
|
|
48
|
+
return `If Codex returns only \`pendingWorktreeId\` while the leader is creating a worktree-backed member
|
|
49
|
+
thread, the thread is not ready yet. The leader must wait until Codex surfaces a real thread id,
|
|
50
|
+
then bind that real id and send the bootstrap.`;
|
|
18
51
|
}
|
|
19
52
|
|
|
20
53
|
function worktreeSection(team) {
|
|
54
|
+
const v2 = isMultiAgentV2(team);
|
|
21
55
|
if (!team.worktree?.enabled) {
|
|
22
56
|
return `## Worktrees
|
|
23
57
|
|
|
@@ -28,8 +62,8 @@ that is a coordination signal: mark yourself \`blocked\` and tell the leader at
|
|
|
28
62
|
const lines = team.members
|
|
29
63
|
.filter((m) => m.worktree?.path || m.worktree?.branch)
|
|
30
64
|
.map((m) => {
|
|
31
|
-
const
|
|
32
|
-
return `- **${m.id}**: worktree \`${m.worktree.path ??
|
|
65
|
+
const bindHint = v2 ? "(assign with bind-agent --cwd)" : "(assign with bind-thread --cwd)";
|
|
66
|
+
return `- **${m.id}**: worktree \`${m.worktree.path ?? bindHint}\` on branch \`${m.worktree.branch ?? team.worktree.baseBranch}\`${worktreeMemberEndpoint(m, v2)}`;
|
|
33
67
|
});
|
|
34
68
|
return `## Worktrees (ISOLATION IS ON - this is not optional for you)
|
|
35
69
|
|
|
@@ -41,25 +75,48 @@ Before editing, verify that your assigned worktree exists and contains repo file
|
|
|
41
75
|
missing, empty, or does not look like a git worktree/repository yet, send \`BLOCKED: worktree not ready\`
|
|
42
76
|
to the leader and wait instead of editing any parent checkout or empty directory.
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
thread, the thread is not ready yet. The leader must wait until Codex surfaces a real thread id,
|
|
46
|
-
then bind that real id and send the bootstrap.
|
|
78
|
+
${worktreeReadinessNote(v2)}
|
|
47
79
|
|
|
48
|
-
${lines.length ? lines.join("\n") : "- (worktree paths are assigned as
|
|
80
|
+
${lines.length ? lines.join("\n") : "- (worktree paths are assigned as members are bound; read team.json for yours)"}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addressBookRules(team) {
|
|
84
|
+
if (isMultiAgentV2(team)) {
|
|
85
|
+
return `- **Reach the leader and your peers with \`send_message\` - not by narrating in your own
|
|
86
|
+
transcript, where no one reads it.** The address book is in team.json: the leader's target
|
|
87
|
+
is \`${LEADER_AGENT_PATH}\`, and each peer's target is its \`members[].agentPath\` (built from its
|
|
88
|
+
\`members[].taskName\`).
|
|
89
|
+
- **Use \`send_message\` for updates and \`followup_task\` only when handing a peer a NEW task**
|
|
90
|
+
that should wake it. Never invent an agent path - read it from team.json.`;
|
|
91
|
+
}
|
|
92
|
+
return `- **Reach the leader and your peers with \`codex_app.send_message_to_thread\` - not by narrating in
|
|
93
|
+
your own thread, where no one reads it.** The address book is in team.json: the leader thread id
|
|
94
|
+
is \`leader.sessionId\`, and each peer thread id is its \`members[].threadId\`.
|
|
95
|
+
- **When you mention a Codex thread, include its app link too:** \`codex://threads/<threadId>\`.
|
|
96
|
+
These links are the fastest way for the leader to reopen worktree-backed member threads.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function identityHeaderLine(team) {
|
|
100
|
+
if (isMultiAgentV2(team)) {
|
|
101
|
+
return `- Member identity convention: each member is the agent path \`${LEADER_AGENT_PATH}/<task name>\` - your row below shows yours; two members never share a task name`;
|
|
102
|
+
}
|
|
103
|
+
return `- Thread title convention: \`${team.threadTitleConvention}\` - each member's thread takes its OWN title from its name (your row below shows yours); two members never share a title`;
|
|
49
104
|
}
|
|
50
105
|
|
|
51
106
|
export function buildGuide(team) {
|
|
107
|
+
const v2 = isMultiAgentV2(team);
|
|
52
108
|
const artifacts = team.paths?.artifacts ?? ".omo/teams/<session_id>/artifacts";
|
|
53
109
|
const teamJson = team.paths?.team ?? ".omo/teams/<session_id>/team.json";
|
|
54
|
-
const roster = team.members.length ? team.members.map(memberLine).join("\n") : "- (no members yet)";
|
|
110
|
+
const roster = team.members.length ? team.members.map((member) => memberLine(member, v2)).join("\n") : "- (no members yet)";
|
|
55
111
|
return `# Team ${team.teamName} - Member Field Manual
|
|
56
112
|
|
|
57
113
|
> Auto-generated by the teammode script. This file is the single source of truth for how
|
|
58
114
|
> this team works. Re-read it whenever you are unsure - do not improvise the protocol.
|
|
59
115
|
|
|
60
116
|
- Team: **${team.teamName}** (id \`${team.teamId}\`)
|
|
117
|
+
- Transport: **${transportLabel(team)}**
|
|
61
118
|
- Team state: \`${teamJson}\`
|
|
62
|
-
|
|
119
|
+
${identityHeaderLine(team)}
|
|
63
120
|
|
|
64
121
|
## Who you are (your identity is fixed)
|
|
65
122
|
|
|
@@ -82,11 +139,7 @@ coordinate with each other, but the leader owns the final decision and integrati
|
|
|
82
139
|
is mandatory regardless of the language the task was given in.
|
|
83
140
|
- **Exception: when the END USER addresses you directly, reply in the user's own language.**
|
|
84
141
|
Team-internal traffic stays English; user-facing replies match the user.
|
|
85
|
-
|
|
86
|
-
your own thread, where no one reads it.** The address book is in team.json: the leader thread id
|
|
87
|
-
is \`leader.sessionId\`, and each peer thread id is its \`members[].threadId\`.
|
|
88
|
-
- **When you mention a Codex thread, include its app link too:** \`codex://threads/<threadId>\`.
|
|
89
|
-
These links are the fastest way for the leader to reopen worktree-backed member threads.
|
|
142
|
+
${addressBookRules(team)}
|
|
90
143
|
- **Push a short message at each of these moments - never batch them into one report at the end:**
|
|
91
144
|
- to the **leader**: every finding or decision, every file or sub-task you finish, a
|
|
92
145
|
\`WORKING: <focus> - <phase>\` heartbeat every few tool calls so you never look stale, a
|
|
@@ -116,6 +169,25 @@ Your slice is done only when (1) the deliverable exists, (2) you have evidence i
|
|
|
116
169
|
(3) you have reported it to the leader. Until all three are true, keep going.`;
|
|
117
170
|
}
|
|
118
171
|
|
|
172
|
+
function memberIdentityLine(team, member) {
|
|
173
|
+
if (isMultiAgentV2(team)) {
|
|
174
|
+
return `Your task name is \`${member.taskName}\` (agent path \`${member.agentPath}\`).`;
|
|
175
|
+
}
|
|
176
|
+
return `Your thread title is \`${member.threadTitle}\`.`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function memberEndpointNote(team, member) {
|
|
180
|
+
if (isMultiAgentV2(team)) return "";
|
|
181
|
+
return member.threadId ? `\nYour Codex thread link is ${codexThreadLink(member.threadId)}. Include it when reporting handoffs or worktree status.` : "";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function pushInstruction(team) {
|
|
185
|
+
if (isMultiAgentV2(team)) {
|
|
186
|
+
return `Push updates to the leader (target \`${LEADER_AGENT_PATH}\`) and relevant peers (their team.json \`members[].agentPath\`) with \`send_message\` as you work`;
|
|
187
|
+
}
|
|
188
|
+
return "Push updates to the leader and relevant peers with `codex_app.send_message_to_thread` as you work";
|
|
189
|
+
}
|
|
190
|
+
|
|
119
191
|
export function buildMemberPrompt(team, id) {
|
|
120
192
|
const member = team.members.find((m) => m.id === id);
|
|
121
193
|
if (!member) throw new Error(`no member with id "${id}"`);
|
|
@@ -126,14 +198,13 @@ export function buildMemberPrompt(team, id) {
|
|
|
126
198
|
: team.worktree?.enabled
|
|
127
199
|
? "Wait for the leader to bind your worktree cwd before editing."
|
|
128
200
|
: "Work from the repository root unless your manual assigns a worktree.";
|
|
129
|
-
const threadLink = member.threadId ? `\nYour Codex thread link is ${codexThreadLink(member.threadId)}. Include it when reporting handoffs or worktree status.` : "";
|
|
130
201
|
const readiness =
|
|
131
202
|
team.worktree?.enabled
|
|
132
203
|
? "\nBefore editing, verify that your assigned worktree exists and contains repo files. If this prompt arrived before your Codex worktree checkout is ready, or the path is missing, empty, or not a git worktree/repository yet, report `BLOCKED: worktree not ready` to the leader and wait."
|
|
133
204
|
: "";
|
|
134
|
-
return `You are member ${member.id}${member.name ? ` (${member.name})` : ""} of team ${team.teamName} - owner of: ${member.focus}.
|
|
205
|
+
return `You are member ${member.id}${member.name ? ` (${member.name})` : ""} of team ${team.teamName} - owner of: ${member.focus}. ${memberIdentityLine(team, member)}
|
|
135
206
|
FIRST read your field manual at \`${guide}\`, then the team state at \`${teamJson}\`; they define your scope, deliverable, the leader, the artifacts directory, and the communication rules.
|
|
136
|
-
${where}${
|
|
207
|
+
${where}${memberEndpointNote(team, member)}${readiness}
|
|
137
208
|
Communicate with the team and leader in English; reply to the end user in the user's own language.
|
|
138
|
-
Start now.
|
|
209
|
+
Start now. ${pushInstruction(team)} - findings, \`WORKING:\` heartbeats, and \`BLOCKED:\` the instant you stall - never just one report at the end.`;
|
|
139
210
|
}
|
package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs
CHANGED
|
@@ -13,6 +13,16 @@ import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|
|
13
13
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
14
14
|
import { setTimeout as delay } from "node:timers/promises";
|
|
15
15
|
|
|
16
|
+
import {
|
|
17
|
+
agentPathForTaskName,
|
|
18
|
+
assertTransport,
|
|
19
|
+
isMultiAgentV2,
|
|
20
|
+
LEADER_AGENT_PATH,
|
|
21
|
+
parseTaskName,
|
|
22
|
+
parseTeamTransport,
|
|
23
|
+
TEAM_SCHEMA_VERSION,
|
|
24
|
+
} from "./team-transport.mjs";
|
|
25
|
+
|
|
16
26
|
export const LENSES = ["area", "ownership", "perspective"];
|
|
17
27
|
export const MEMBER_STATUSES = ["pending", "active", "reported", "blocked", "archived"];
|
|
18
28
|
// A team is never a single member: two or more distinct slices is the minimum. One isolated
|
|
@@ -32,22 +42,25 @@ function isoNow(now) {
|
|
|
32
42
|
return now ?? new Date().toISOString();
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
export function buildTeam({ teamName, sessionName, sessionId = null, dir = null, worktreeEnabled = false, baseBranch = "dev", now }) {
|
|
45
|
+
export function buildTeam({ teamName, sessionName, sessionId = null, dir = null, worktreeEnabled = false, baseBranch = "dev", transport, now }) {
|
|
36
46
|
if (!teamName?.trim()) throw new Error("team name is required");
|
|
37
47
|
if (!sessionName?.trim()) throw new Error("session name is required");
|
|
48
|
+
const teamTransport = parseTeamTransport(transport);
|
|
49
|
+
const v2 = teamTransport === "multi_agent_v2";
|
|
38
50
|
const ts = isoNow(now);
|
|
39
51
|
return {
|
|
40
|
-
schemaVersion:
|
|
52
|
+
schemaVersion: TEAM_SCHEMA_VERSION,
|
|
53
|
+
transport: teamTransport,
|
|
41
54
|
teamId: randomUUID(),
|
|
42
55
|
teamName: teamName.trim(),
|
|
43
56
|
sessionName: sessionName.trim(),
|
|
44
57
|
sessionId,
|
|
45
|
-
threadTitleConvention: `[${teamName.trim()}] <member name>`,
|
|
58
|
+
threadTitleConvention: v2 ? null : `[${teamName.trim()}] <member name>`,
|
|
46
59
|
status: "active",
|
|
47
60
|
createdAt: ts,
|
|
48
61
|
updatedAt: ts,
|
|
49
62
|
archivedAt: null,
|
|
50
|
-
leader: { kind: "main-session", sessionId },
|
|
63
|
+
leader: { kind: "main-session", sessionId: v2 ? null : sessionId, agentPath: v2 ? LEADER_AGENT_PATH : null },
|
|
51
64
|
communication: { memberLanguage: "english", replyToUserInUserLanguage: true },
|
|
52
65
|
worktree: { enabled: Boolean(worktreeEnabled), baseBranch, root: dir ? join(dir, "worktrees") : null },
|
|
53
66
|
paths: dir
|
|
@@ -124,21 +137,44 @@ function assertUniqueMemberThreadTitle(team) {
|
|
|
124
137
|
}
|
|
125
138
|
}
|
|
126
139
|
|
|
127
|
-
function
|
|
140
|
+
function assertUniqueMemberTaskName(team) {
|
|
141
|
+
const seen = new Map();
|
|
142
|
+
for (const member of team.members) {
|
|
143
|
+
const taskName = parseTaskName(member.taskName);
|
|
144
|
+
const previous = seen.get(taskName);
|
|
145
|
+
if (previous) {
|
|
146
|
+
throw new Error(`member taskName "${taskName}" duplicates "${previous.taskName}" (no two members may share an agent path)`);
|
|
147
|
+
}
|
|
148
|
+
if (member.agentPath !== agentPathForTaskName(taskName)) {
|
|
149
|
+
throw new Error(`member "${member.id}" agentPath "${member.agentPath}" does not match task name "${taskName}"`);
|
|
150
|
+
}
|
|
151
|
+
seen.set(taskName, member);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function assertUniqueTransportIdentity(team) {
|
|
156
|
+
if (isMultiAgentV2(team)) assertUniqueMemberTaskName(team);
|
|
157
|
+
else assertUniqueMemberThreadTitle(team);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function assertTeamReadyForBinding(team) {
|
|
128
161
|
if (isUnderstaffed(team)) {
|
|
129
162
|
throw new Error(
|
|
130
|
-
`cannot bind
|
|
163
|
+
`cannot bind members until the team has at least ${MIN_MEMBERS} distinct members; current count is ${team.members.length}`,
|
|
131
164
|
);
|
|
132
165
|
}
|
|
133
166
|
assertUniqueMemberFocus(team);
|
|
134
167
|
assertUniqueMemberName(team);
|
|
135
|
-
|
|
168
|
+
assertUniqueTransportIdentity(team);
|
|
136
169
|
}
|
|
137
170
|
|
|
138
|
-
export function addMember(team, { id, focus, lens, deliverable = "", branch = null, name = null }) {
|
|
171
|
+
export function addMember(team, { id, focus, lens, deliverable = "", branch = null, name = null, taskName = null }) {
|
|
139
172
|
if (!id?.trim()) throw new Error("member id is required");
|
|
140
173
|
if (!focus?.trim()) throw new Error("member focus is required - a concrete part, ownership area, or perspective");
|
|
141
174
|
if (!LENSES.includes(lens)) throw new Error(`invalid lens "${lens}" - use one of: ${LENSES.join(", ")}`);
|
|
175
|
+
const v2 = isMultiAgentV2(team);
|
|
176
|
+
if (!v2 && taskName !== null) throw new Error("--task-name is only valid on multi_agent_v2 teams");
|
|
177
|
+
const memberTaskName = v2 ? parseTaskName(taskName) : null;
|
|
142
178
|
const memberId = id.trim();
|
|
143
179
|
const memberFocus = focus.trim();
|
|
144
180
|
// The member name is the short role label that titles this member's thread. Fall back to the
|
|
@@ -151,14 +187,20 @@ export function addMember(team, { id, focus, lens, deliverable = "", branch = nu
|
|
|
151
187
|
if (duplicateName) {
|
|
152
188
|
throw new Error(`member name "${memberName}" duplicates "${duplicateName.name ?? duplicateName.focus}" (no two members may produce the same thread title)`);
|
|
153
189
|
}
|
|
190
|
+
if (v2) {
|
|
191
|
+
const duplicateTask = team.members.find((m) => m.taskName === memberTaskName);
|
|
192
|
+
if (duplicateTask) throw new Error(`member taskName "${memberTaskName}" duplicates "${duplicateTask.taskName}" (no two members may share an agent path)`);
|
|
193
|
+
}
|
|
154
194
|
team.members.push({
|
|
155
195
|
id: memberId,
|
|
156
196
|
name: memberName,
|
|
157
197
|
focus: memberFocus,
|
|
158
198
|
lens,
|
|
159
199
|
deliverable: deliverable.trim(),
|
|
200
|
+
taskName: memberTaskName,
|
|
201
|
+
agentPath: v2 ? agentPathForTaskName(memberTaskName) : null,
|
|
160
202
|
threadId: null,
|
|
161
|
-
threadTitle: `[${team.teamName}] ${memberName}`,
|
|
203
|
+
threadTitle: v2 ? null : `[${team.teamName}] ${memberName}`,
|
|
162
204
|
cwd: null,
|
|
163
205
|
worktree: { path: null, branch: branch ?? null },
|
|
164
206
|
status: "pending",
|
|
@@ -167,8 +209,9 @@ export function addMember(team, { id, focus, lens, deliverable = "", branch = nu
|
|
|
167
209
|
}
|
|
168
210
|
|
|
169
211
|
export function bindThread(team, { id, threadId, cwd = null, worktreePath = null }) {
|
|
212
|
+
assertTransport(team, "codex_app", "bind-thread");
|
|
170
213
|
if (!threadId?.trim()) throw new Error("thread id is required");
|
|
171
|
-
|
|
214
|
+
assertTeamReadyForBinding(team);
|
|
172
215
|
const m = memberById(team, id);
|
|
173
216
|
m.threadId = threadId.trim();
|
|
174
217
|
m.status = "active";
|
|
@@ -177,6 +220,21 @@ export function bindThread(team, { id, threadId, cwd = null, worktreePath = null
|
|
|
177
220
|
return touch(team, "bind-thread", `member ${id} -> thread ${threadId.trim()}`);
|
|
178
221
|
}
|
|
179
222
|
|
|
223
|
+
export function bindAgent(team, { id, agentPath, cwd = null, worktreePath = null }) {
|
|
224
|
+
assertTransport(team, "multi_agent_v2", "bind-agent");
|
|
225
|
+
if (!agentPath?.trim()) throw new Error("agent path is required");
|
|
226
|
+
assertTeamReadyForBinding(team);
|
|
227
|
+
const m = memberById(team, id);
|
|
228
|
+
const boundPath = agentPath.trim();
|
|
229
|
+
if (boundPath !== m.agentPath) {
|
|
230
|
+
throw new Error(`agent path mismatch for member ${m.id}: expected "${m.agentPath}" (task name "${m.taskName}"), got "${boundPath}"`);
|
|
231
|
+
}
|
|
232
|
+
m.status = "active";
|
|
233
|
+
if (cwd) m.cwd = cwd;
|
|
234
|
+
if (team.worktree.enabled) m.worktree.path = worktreePath ?? cwd ?? m.worktree.path;
|
|
235
|
+
return touch(team, "bind-agent", `member ${id} -> agent ${boundPath}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
180
238
|
export function setMemberStatus(team, { id, status, note = "" }) {
|
|
181
239
|
if (!MEMBER_STATUSES.includes(status)) throw new Error(`invalid status "${status}" - use one of: ${MEMBER_STATUSES.join(", ")}`);
|
|
182
240
|
memberById(team, id).status = status;
|
|
@@ -204,22 +262,44 @@ export function clearMemberWorktree(team, { id }) {
|
|
|
204
262
|
export function archive(team, { id = null, note = "" } = {}) {
|
|
205
263
|
if (id) {
|
|
206
264
|
memberById(team, id).status = "archived";
|
|
207
|
-
|
|
265
|
+
const memberQualifier = isMultiAgentV2(team) ? " (team state only; V2 exposes no runtime archive operation)" : "";
|
|
266
|
+
return touch(team, "archive-member", `member ${id}${memberQualifier}${note ? `: ${note}` : ""}`);
|
|
208
267
|
}
|
|
209
268
|
for (const m of team.members) m.status = "archived";
|
|
210
269
|
team.status = "archived";
|
|
211
270
|
team.archivedAt = isoNow();
|
|
212
|
-
|
|
271
|
+
// The default detail must never claim a runtime closure that did not happen: V2 exposes no
|
|
272
|
+
// runtime archive operation, so only the durable team state is archived there.
|
|
273
|
+
const defaultDetail = isMultiAgentV2(team)
|
|
274
|
+
? "team archived; V2 exposes no runtime archive operation - durable team state is the archive"
|
|
275
|
+
: "team archived; all members closed";
|
|
276
|
+
return touch(team, "archive", note || defaultDetail);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// schemaVersion 2 predates transports; that skill allowed ONLY Codex App threads as members,
|
|
280
|
+
// so a legacy team is migrated in memory as codex_app and persisted on the next mutation.
|
|
281
|
+
function migrateLegacyTeam(team) {
|
|
282
|
+
if (team?.schemaVersion !== 2) return team;
|
|
283
|
+
team.schemaVersion = TEAM_SCHEMA_VERSION;
|
|
284
|
+
team.transport = "codex_app";
|
|
285
|
+
if (team.leader && typeof team.leader === "object") team.leader.agentPath = null;
|
|
286
|
+
for (const member of team.members ?? []) {
|
|
287
|
+
member.taskName = null;
|
|
288
|
+
member.agentPath = null;
|
|
289
|
+
}
|
|
290
|
+
return team;
|
|
213
291
|
}
|
|
214
292
|
|
|
215
293
|
export function validateTeam(team) {
|
|
216
|
-
|
|
294
|
+
migrateLegacyTeam(team);
|
|
295
|
+
if (team?.schemaVersion !== TEAM_SCHEMA_VERSION) throw new Error(`invalid team: schemaVersion must be ${TEAM_SCHEMA_VERSION}`);
|
|
296
|
+
parseTeamTransport(team.transport);
|
|
217
297
|
if (!team.teamId || !team.teamName) throw new Error("invalid team: teamId and teamName are required");
|
|
218
298
|
if (team.leader?.kind !== "main-session") throw new Error("invalid team: leader.kind must be main-session");
|
|
219
299
|
if (!Array.isArray(team.members)) throw new Error("invalid team: members must be an array");
|
|
220
300
|
assertUniqueMemberFocus(team);
|
|
221
301
|
assertUniqueMemberName(team);
|
|
222
|
-
|
|
302
|
+
assertUniqueTransportIdentity(team);
|
|
223
303
|
return team;
|
|
224
304
|
}
|
|
225
305
|
|
package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-transport.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// team-transport.mjs - the single source of transport identity for teammode state.
|
|
2
|
+
//
|
|
3
|
+
// A team runs on exactly ONE transport, chosen at init and immutable afterwards:
|
|
4
|
+
// - "multi_agent_v2": members are native Codex MultiAgentV2 agents addressed by task
|
|
5
|
+
// name / canonical agent path (`/root/<task_name>`), messaged with flat `send_message`
|
|
6
|
+
// and `followup_task`.
|
|
7
|
+
// - "codex_app": members are durable Codex App threads addressed by thread id, messaged
|
|
8
|
+
// with `codex_app.send_message_to_thread` (the fallback when flat V2 tools are absent).
|
|
9
|
+
//
|
|
10
|
+
// Zero external dependencies (node builtins only), same portability contract as the
|
|
11
|
+
// sibling scripts.
|
|
12
|
+
|
|
13
|
+
export const TEAM_SCHEMA_VERSION = 3;
|
|
14
|
+
export const TEAM_TRANSPORTS = ["multi_agent_v2", "codex_app"];
|
|
15
|
+
export const DEFAULT_TEAM_TRANSPORT = "codex_app";
|
|
16
|
+
export const LEADER_AGENT_PATH = "/root";
|
|
17
|
+
|
|
18
|
+
// Codex validates V2 task names as lowercase letters, digits, and underscores; "root" is
|
|
19
|
+
// the leader's own path segment and can never name a member.
|
|
20
|
+
const TASK_NAME_PATTERN = /^[a-z0-9_]+$/;
|
|
21
|
+
|
|
22
|
+
export function parseTeamTransport(value = DEFAULT_TEAM_TRANSPORT) {
|
|
23
|
+
if (!TEAM_TRANSPORTS.includes(value)) {
|
|
24
|
+
throw new Error(`invalid transport "${value}" - use one of: ${TEAM_TRANSPORTS.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isMultiAgentV2(team) {
|
|
30
|
+
return team?.transport === "multi_agent_v2";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isCodexApp(team) {
|
|
34
|
+
return team?.transport === "codex_app";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function assertTransport(team, transport, operation) {
|
|
38
|
+
if (team?.transport !== transport) {
|
|
39
|
+
throw new Error(`${operation} is only valid on ${transport} teams; this team's transport is "${team?.transport}"`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseTaskName(taskName) {
|
|
44
|
+
const trimmed = typeof taskName === "string" ? taskName.trim() : "";
|
|
45
|
+
if (!trimmed) throw new Error("member task name is required on multi_agent_v2 teams (--task-name)");
|
|
46
|
+
if (!TASK_NAME_PATTERN.test(trimmed)) {
|
|
47
|
+
throw new Error(`invalid task name "${trimmed}" - use lowercase letters, digits, and underscores`);
|
|
48
|
+
}
|
|
49
|
+
if (trimmed === "root") throw new Error('invalid task name "root" - it names the leader, never a member');
|
|
50
|
+
return trimmed;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function agentPathForTaskName(taskName) {
|
|
54
|
+
return `${LEADER_AGENT_PATH}/${taskName}`;
|
|
55
|
+
}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
// Zero external dependencies (node builtins only): runs identically under node and bun on
|
|
7
7
|
// macOS, Linux, and Windows.
|
|
8
8
|
//
|
|
9
|
-
// node "<skill-root>/scripts/team.mjs" init --name "<team>" --session-name "<sess>" [--session <id>] [--worktree] [--base-branch dev]
|
|
10
|
-
// node "<skill-root>/scripts/team.mjs" add-member --team <id> --id A --name "<short role>" --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>" [--branch <b>]
|
|
11
|
-
// node "<skill-root>/scripts/team.mjs" bind-
|
|
9
|
+
// node "<skill-root>/scripts/team.mjs" init --name "<team>" --session-name "<sess>" [--transport multi_agent_v2|codex_app] [--session <id>] [--worktree] [--base-branch dev]
|
|
10
|
+
// node "<skill-root>/scripts/team.mjs" add-member --team <id> --id A --name "<short role>" --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>" [--task-name <v2 task name>] [--branch <b>]
|
|
11
|
+
// node "<skill-root>/scripts/team.mjs" bind-agent --team <id> --id A --agent-path /root/<task_name> [--cwd <path>] (multi_agent_v2 teams)
|
|
12
|
+
// node "<skill-root>/scripts/team.mjs" bind-thread --team <id> --id A --thread <thread-id> [--cwd <path>] (codex_app teams)
|
|
12
13
|
// node "<skill-root>/scripts/team.mjs" member-prompt --team <id> --id A
|
|
13
14
|
// node "<skill-root>/scripts/team.mjs" set-status --team <id> --id A --status reported|blocked|active|archived [--note "<...>"]
|
|
14
15
|
// node "<skill-root>/scripts/team.mjs" archive --team <id> [--id A] [--note "<...>"]
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
addMember,
|
|
26
27
|
archive,
|
|
27
28
|
assertSafeTeamDir,
|
|
29
|
+
bindAgent,
|
|
28
30
|
bindThread,
|
|
29
31
|
buildTeam,
|
|
30
32
|
clearMemberWorktree,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
writeGuideAtomic,
|
|
40
42
|
writeTeamAtomic,
|
|
41
43
|
} from "./team-state.mjs";
|
|
44
|
+
import { isMultiAgentV2, parseTeamTransport } from "./team-transport.mjs";
|
|
42
45
|
import { addMemberWorktree, integrateMemberBranch, removeMemberWorktree } from "./team-worktree.mjs";
|
|
43
46
|
|
|
44
47
|
function parseFlags(args) {
|
|
@@ -95,11 +98,20 @@ const handlers = {
|
|
|
95
98
|
async init(cwd, flags) {
|
|
96
99
|
const teamName = requireFlag(flags, "name");
|
|
97
100
|
const sessionName = requireFlag(flags, "session-name");
|
|
101
|
+
// Validate the transport BEFORE any directory exists so a bad value leaves no state behind.
|
|
102
|
+
// A valueless `--transport` parses as `true` and must fail loudly, never default silently.
|
|
103
|
+
const transport = parseTeamTransport(flags.transport === undefined ? undefined : flags.transport);
|
|
98
104
|
const sessionId = typeof flags.session === "string" ? flags.session : `team-${randomUUID().slice(0, 8)}`;
|
|
99
105
|
const dir = await ensureTeamDir(cwd, sessionId);
|
|
100
106
|
await withTeamLock(dir, "init", async () => {
|
|
101
107
|
if (await teamExists(dir)) {
|
|
102
|
-
|
|
108
|
+
const existing = await readTeam(dir);
|
|
109
|
+
if (flags.transport !== undefined && existing.transport !== transport) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`team "${sessionId}" already exists with transport "${existing.transport}" - transport is immutable; keep using it or delete the team first`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(`exists: ${dir} (transport: ${existing.transport}; left untouched; re-init is a safe no-op)\n`);
|
|
103
115
|
return;
|
|
104
116
|
}
|
|
105
117
|
const team = buildTeam({
|
|
@@ -107,13 +119,15 @@ const handlers = {
|
|
|
107
119
|
sessionName,
|
|
108
120
|
sessionId,
|
|
109
121
|
dir,
|
|
122
|
+
transport,
|
|
110
123
|
worktreeEnabled: flags.worktree === true,
|
|
111
124
|
baseBranch: typeof flags["base-branch"] === "string" ? flags["base-branch"] : "dev",
|
|
112
125
|
});
|
|
113
126
|
await persist(team, dir);
|
|
114
|
-
|
|
127
|
+
const taskNameFlag = isMultiAgentV2(team) ? ' --task-name "<lowercase_digits_underscores>"' : "";
|
|
128
|
+
process.stdout.write(`created: ${dir} (transport: ${team.transport})\n`);
|
|
115
129
|
process.stdout.write(`team.json + guide.md written; artifacts/ ready. session id: ${sessionId}\n`);
|
|
116
|
-
process.stdout.write(`next: add-member --team ${sessionId} --id A --name "<short role>" --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>"\n`);
|
|
130
|
+
process.stdout.write(`next: add-member --team ${sessionId} --id A --name "<short role>"${taskNameFlag} --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>"\n`);
|
|
117
131
|
});
|
|
118
132
|
},
|
|
119
133
|
|
|
@@ -127,13 +141,17 @@ const handlers = {
|
|
|
127
141
|
lens: requireFlag(flags, "lens"),
|
|
128
142
|
deliverable: typeof flags.deliverable === "string" ? flags.deliverable : "",
|
|
129
143
|
branch: typeof flags.branch === "string" ? flags.branch : null,
|
|
144
|
+
taskName: typeof flags["task-name"] === "string" ? flags["task-name"] : null,
|
|
130
145
|
};
|
|
131
146
|
const { team, member } = await mutateTeam(cwd, sessionId, "add-member", async (team, dir) => {
|
|
132
147
|
addMember(team, memberInput);
|
|
133
148
|
await persist(team, dir);
|
|
134
149
|
return { team, member: team.members.find((m) => m.id === memberId) };
|
|
135
150
|
});
|
|
136
|
-
|
|
151
|
+
const delivery = isMultiAgentV2(team)
|
|
152
|
+
? `Send this as spawn_agent message (task_name "${member.taskName}", fork_turns "none"), then bind-agent --agent-path "${member.agentPath}"`
|
|
153
|
+
: `Send this as the new thread's first message (title the thread "${member.threadTitle}")`;
|
|
154
|
+
process.stdout.write(`added member ${memberId} to team ${sessionId}.\n\n${delivery}:\n---\n${buildMemberPrompt(team, memberId)}\n---\n`);
|
|
137
155
|
},
|
|
138
156
|
|
|
139
157
|
async "bind-thread"(cwd, flags) {
|
|
@@ -151,6 +169,21 @@ const handlers = {
|
|
|
151
169
|
process.stdout.write(`bound member ${flags.id} to thread ${flags.thread}.\n`);
|
|
152
170
|
},
|
|
153
171
|
|
|
172
|
+
async "bind-agent"(cwd, flags) {
|
|
173
|
+
const sessionId = requireFlag(flags, "team");
|
|
174
|
+
const input = {
|
|
175
|
+
id: requireFlag(flags, "id"),
|
|
176
|
+
agentPath: requireFlag(flags, "agent-path"),
|
|
177
|
+
cwd: typeof flags.cwd === "string" ? flags.cwd : null,
|
|
178
|
+
worktreePath: typeof flags["worktree-path"] === "string" ? flags["worktree-path"] : null,
|
|
179
|
+
};
|
|
180
|
+
await mutateTeam(cwd, sessionId, "bind-agent", async (team, dir) => {
|
|
181
|
+
bindAgent(team, input);
|
|
182
|
+
await persist(team, dir);
|
|
183
|
+
});
|
|
184
|
+
process.stdout.write(`bound member ${flags.id} to agent ${flags["agent-path"]}.\n`);
|
|
185
|
+
},
|
|
186
|
+
|
|
154
187
|
async "member-prompt"(cwd, flags) {
|
|
155
188
|
const sessionId = requireFlag(flags, "team");
|
|
156
189
|
const { team } = await loadTeam(cwd, sessionId);
|
|
@@ -175,19 +208,24 @@ const handlers = {
|
|
|
175
208
|
const sessionId = requireFlag(flags, "team");
|
|
176
209
|
const memberId = requireFlag(flags, "id");
|
|
177
210
|
const baseBranch = typeof flags["base-branch"] === "string" ? flags["base-branch"] : null;
|
|
178
|
-
const { member, result } = await mutateTeam(cwd, sessionId, "worktree-add", async (team, dir) => {
|
|
211
|
+
const { team, member, result } = await mutateTeam(cwd, sessionId, "worktree-add", async (team, dir) => {
|
|
179
212
|
const member = memberOrThrow(team, memberId);
|
|
180
213
|
const result = addMemberWorktree(cwd, team, member, { baseBranch });
|
|
181
214
|
setMemberWorktree(team, { id: member.id, path: result.path, branch: result.branch });
|
|
182
215
|
await persist(team, dir);
|
|
183
|
-
return { member, result };
|
|
216
|
+
return { team, member, result };
|
|
184
217
|
});
|
|
185
218
|
const note = result.created ? "" : " (already exists)";
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
219
|
+
const v2 = isMultiAgentV2(team);
|
|
220
|
+
const followUp = v2
|
|
221
|
+
? member.status === "active"
|
|
222
|
+
? `\nMember agent: ${member.agentPath}\nSend it a followup_task to: cd "${result.path}"`
|
|
223
|
+
: `\nMember agent is not spawned yet; include this worktree path in its spawn_agent message, then bind-agent.`
|
|
224
|
+
: member.threadId
|
|
225
|
+
? `\nMember thread: ${codexThreadLink(member.threadId)}\nTell that member to: cd "${result.path}"`
|
|
226
|
+
: "\nMember thread is not bound yet; wait for the real Codex thread id, then bind-thread before sending bootstrap. After binding, send the member this worktree path.";
|
|
189
227
|
process.stdout.write(
|
|
190
|
-
`worktree for member ${member.id}${note}: ${result.path} on branch ${result.branch} (off ${result.base}).${
|
|
228
|
+
`worktree for member ${member.id}${note}: ${result.path} on branch ${result.branch} (off ${result.base}).${followUp}\n`,
|
|
191
229
|
);
|
|
192
230
|
},
|
|
193
231
|
|
|
@@ -235,11 +273,19 @@ const handlers = {
|
|
|
235
273
|
id: typeof flags.id === "string" ? flags.id : null,
|
|
236
274
|
note: typeof flags.note === "string" ? flags.note : "",
|
|
237
275
|
};
|
|
238
|
-
await mutateTeam(cwd, sessionId, "archive", async (team, dir) => {
|
|
276
|
+
const team = await mutateTeam(cwd, sessionId, "archive", async (team, dir) => {
|
|
239
277
|
archive(team, input);
|
|
240
278
|
await persist(team, dir);
|
|
279
|
+
return team;
|
|
241
280
|
});
|
|
242
|
-
|
|
281
|
+
const v2 = isMultiAgentV2(team);
|
|
282
|
+
const teamSummary = v2
|
|
283
|
+
? `archived team ${sessionId}; V2 has no runtime archive operation - the durable team state is the archive (interrupt_agent any member still mid-turn)\n`
|
|
284
|
+
: `archived team ${sessionId} and closed all members\n`;
|
|
285
|
+
const memberSummary = v2
|
|
286
|
+
? `archived member ${flags.id} in team state; V2 has no runtime archive operation (interrupt_agent it if still mid-turn)\n`
|
|
287
|
+
: `archived member ${flags.id}\n`;
|
|
288
|
+
process.stdout.write(flags.id ? memberSummary : teamSummary);
|
|
243
289
|
},
|
|
244
290
|
|
|
245
291
|
async delete(cwd, flags) {
|
|
@@ -261,10 +307,18 @@ const handlers = {
|
|
|
261
307
|
async status(cwd, flags) {
|
|
262
308
|
const sessionId = requireFlag(flags, "team");
|
|
263
309
|
const { team } = await loadTeam(cwd, sessionId);
|
|
264
|
-
process.stdout.write(
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
`Team ${team.teamName} [${team.status}] - transport=${team.transport} - leader: main session - ${team.members.length} member(s)\n`,
|
|
312
|
+
);
|
|
265
313
|
for (const m of team.members) {
|
|
266
|
-
const
|
|
267
|
-
|
|
314
|
+
const endpoint = isMultiAgentV2(team)
|
|
315
|
+
? m.agentPath
|
|
316
|
+
? ` agent=${m.agentPath}`
|
|
317
|
+
: ""
|
|
318
|
+
: m.threadId
|
|
319
|
+
? ` thread=${m.threadId} link=${codexThreadLink(m.threadId)}`
|
|
320
|
+
: "";
|
|
321
|
+
process.stdout.write(` ${m.id} (${m.lens}) ${m.focus} -> ${m.deliverable || "(no deliverable)"} [${m.status}]${endpoint}${m.cwd ? ` cwd=${m.cwd}` : ""}\n`);
|
|
268
322
|
}
|
|
269
323
|
if (isUnderstaffed(team)) {
|
|
270
324
|
process.stdout.write(
|
|
@@ -87,6 +87,29 @@ describe("thread title PostToolUse guidance", () => {
|
|
|
87
87
|
expect(actual).toBe("");
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("#given MultiAgentV2 spawn_agent completed #when the hook runs #then it leaves task-name identity untouched", () => {
|
|
91
|
+
// given
|
|
92
|
+
const output = runPostToolUseHook({
|
|
93
|
+
hook_event_name: "PostToolUse",
|
|
94
|
+
session_id: "s-team",
|
|
95
|
+
turn_id: "t-team",
|
|
96
|
+
transcript_path: null,
|
|
97
|
+
cwd: "/repo",
|
|
98
|
+
model: "gpt-5.6-sol",
|
|
99
|
+
permission_mode: "default",
|
|
100
|
+
tool_name: "spawn_agent",
|
|
101
|
+
tool_use_id: "tool-spawn-agent",
|
|
102
|
+
tool_input: { task_name: "runtime_core", message: "Inspect runtime state" },
|
|
103
|
+
tool_response: { task_name: "/root/runtime_core" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// when
|
|
107
|
+
const actual = output;
|
|
108
|
+
|
|
109
|
+
// then
|
|
110
|
+
expect(actual).toBe("");
|
|
111
|
+
});
|
|
112
|
+
|
|
90
113
|
it("#given worktree-backed thread creation is pending #when the hook runs #then it tells Codex to wait for the real thread before bootstrapping", () => {
|
|
91
114
|
// given
|
|
92
115
|
const output = runPostToolUseHook({
|