oh-my-opencode 4.11.1 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/codex-qa/scripts/lib/app-server-client.mjs +132 -82
- package/.agents/skills/codex-qa/scripts/lib/app-server-client.test.js +48 -0
- package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +361 -84
- package/.agents/skills/work-with-pr/SKILL.md +19 -5
- package/.opencode/skills/work-with-pr/SKILL.md +19 -5
- package/dist/agents/types.d.ts +7 -6
- package/dist/cli/index.js +438 -306
- package/dist/cli-node/index.js +438 -306
- package/dist/features/background-agent/parent-wake-flush-runner.d.ts +1 -0
- package/dist/features/background-agent/parent-wake-history-state.d.ts +1 -0
- package/dist/features/background-agent/parent-wake-session-history.d.ts +1 -47
- package/dist/features/background-agent/parent-wake-session-message.d.ts +47 -0
- package/dist/index.js +245 -69
- package/dist/plugin/tool-execute-before.d.ts +2 -0
- package/dist/plugin-handlers/plugin-components-loader.d.ts +1 -0
- package/dist/skills/ulw-plan/SKILL.md +2 -1
- package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
- package/dist/skills/ulw-plan/references/intent-clear.md +2 -2
- package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -2
- package/dist/tools/delegate-task/sync-session-turns.d.ts +3 -0
- package/dist/tui.js +16 -7
- package/package.json +13 -13
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +22 -2
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +25 -9
- 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/dist/cli.js +10 -3
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +9 -2
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +7 -2
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +2 -1
- package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +9 -9
- package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +94 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +33 -0
- 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/comment-checker/test/package-smoke.test.ts +2 -71
- 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 +2 -2
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +26 -104
- 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/test/package-smoke.test.ts +12 -92
- package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +157 -234
- 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 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +227 -15
- package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +62 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +34 -3
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +139 -0
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +111 -0
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +250 -0
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +220 -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/test-support/package-smoke-fixture.ts +158 -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/ulw-plan/SKILL.md +2 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +76 -49
- 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/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +17 -0
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +17 -0
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +17 -0
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +17 -0
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +17 -0
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +16 -0
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +16 -0
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +16 -0
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +16 -0
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +16 -0
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +17 -0
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +16 -0
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +16 -0
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +16 -0
- package/packages/omo-codex/plugin/package-lock.json +12 -22
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +14 -11
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +15 -4
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +37 -0
- package/packages/omo-codex/plugin/skills/ast-grep/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/comment-checker/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/debugging/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/frontend/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/git-master/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/init-deep/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lsp/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/lsp-setup/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/programming/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/refactor/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/remove-ai-slops/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/review-work/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/rules/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/start-work/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +139 -0
- package/packages/omo-codex/plugin/skills/teammode/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +111 -0
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +250 -0
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +220 -0
- package/packages/omo-codex/plugin/skills/ultraresearch/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -2
- package/packages/omo-codex/plugin/skills/visual-qa/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +34 -33
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +5 -2
- package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +13 -0
- package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +14 -6
- package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +7 -3
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +3 -3
- package/packages/omo-codex/plugin/test/display-metadata.test.mjs +83 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +30 -19
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +3 -3
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +9 -4
- package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +88 -0
- package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +240 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +385 -206
- package/packages/omo-codex/scripts/install-hook-targets.test.mjs +93 -0
- package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
- package/packages/omo-codex/scripts/install-local-bun-global-update.test.mjs +63 -0
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +109 -1
- package/packages/omo-codex/scripts/install-local.mjs +1 -0
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -2
- package/packages/omo-codex/plugin/hooks/hooks.json +0 -214
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type PlanChecklist = {
|
|
5
|
+
readonly completed: number;
|
|
6
|
+
readonly remaining: number;
|
|
7
|
+
readonly total: number;
|
|
8
|
+
readonly nextTaskLabel: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type BoulderWorkStatus = "active" | "paused" | "completed" | "abandoned";
|
|
12
|
+
|
|
13
|
+
type BoulderWork = {
|
|
14
|
+
readonly activePlan: string;
|
|
15
|
+
readonly planName: string;
|
|
16
|
+
readonly status?: BoulderWorkStatus;
|
|
17
|
+
readonly startedAt?: string;
|
|
18
|
+
readonly updatedAt?: string;
|
|
19
|
+
readonly sessionIds: readonly string[];
|
|
20
|
+
readonly worktreePath?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type BoulderState = {
|
|
24
|
+
readonly works: readonly BoulderWork[];
|
|
25
|
+
readonly mirrorWork: BoulderWork | null;
|
|
26
|
+
readonly hasWorksMap: boolean;
|
|
27
|
+
};
|
|
12
28
|
|
|
13
29
|
export type ContinuationState = {
|
|
14
30
|
readonly planName: string;
|
|
@@ -19,22 +35,218 @@ export type ContinuationState = {
|
|
|
19
35
|
readonly checklist: PlanChecklist;
|
|
20
36
|
};
|
|
21
37
|
|
|
38
|
+
const TODO_HEADING = "TODOs";
|
|
39
|
+
const FINAL_VERIFICATION_HEADING = "Final Verification Wave";
|
|
40
|
+
const CHECKBOX_PREFIX_LENGTH = "- [ ] ".length;
|
|
41
|
+
const SESSION_ID_PREFIX_PATTERN = /^(codex|opencode):/;
|
|
42
|
+
|
|
22
43
|
export function readContinuationState(cwd: string, sessionId: string): ContinuationState | null {
|
|
23
|
-
const
|
|
44
|
+
const boulderPath = getBoulderFilePath(cwd);
|
|
45
|
+
const boulderState = readBoulderState(boulderPath);
|
|
46
|
+
if (boulderState === null) return null;
|
|
47
|
+
|
|
48
|
+
const work = getWorkForSession(boulderState, normalizeSessionId(sessionId, "codex"));
|
|
24
49
|
if (work === null || !isContinuableStatus(work.status)) return null;
|
|
50
|
+
|
|
25
51
|
const planPath = resolveBoulderPlanPathForWork(cwd, work);
|
|
26
52
|
const checklist = getPlanChecklist(planPath);
|
|
27
53
|
if (checklist.remaining === 0) return null;
|
|
54
|
+
|
|
28
55
|
return {
|
|
29
|
-
planName: work.
|
|
56
|
+
planName: work.planName,
|
|
30
57
|
planPath,
|
|
31
|
-
boulderPath
|
|
58
|
+
boulderPath,
|
|
32
59
|
ledgerPath: join(cwd, ".omo", "start-work", "ledger.jsonl"),
|
|
33
|
-
worktreePath: work.
|
|
60
|
+
worktreePath: work.worktreePath ?? null,
|
|
34
61
|
checklist,
|
|
35
62
|
};
|
|
36
63
|
}
|
|
37
64
|
|
|
65
|
+
export function getPlanChecklist(planPath: string): PlanChecklist {
|
|
66
|
+
if (!existsSync(planPath)) return emptyChecklist();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
return parsePlanChecklist(readFileSync(planPath, "utf8"));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof Error) return emptyChecklist();
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parsePlanChecklist(markdown: string): PlanChecklist {
|
|
77
|
+
const lines = markdown.split(/\r?\n/);
|
|
78
|
+
const hasCountedSections = lines.some((line) => isCountedHeading(parseLevelTwoHeading(line)));
|
|
79
|
+
let completed = 0;
|
|
80
|
+
let remaining = 0;
|
|
81
|
+
let nextTaskLabel: string | null = null;
|
|
82
|
+
let isCountedSection = !hasCountedSections;
|
|
83
|
+
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
const heading = parseLevelTwoHeading(line);
|
|
86
|
+
if (heading !== null) {
|
|
87
|
+
isCountedSection = isCountedHeading(heading);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!isCountedSection) continue;
|
|
91
|
+
|
|
92
|
+
const checkbox = parseTopLevelCheckbox(line);
|
|
93
|
+
if (checkbox === null) continue;
|
|
94
|
+
|
|
95
|
+
if (checkbox.checked) {
|
|
96
|
+
completed += 1;
|
|
97
|
+
} else {
|
|
98
|
+
remaining += 1;
|
|
99
|
+
nextTaskLabel = nextTaskLabel ?? checkbox.label;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { completed, remaining, total: completed + remaining, nextTaskLabel };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readBoulderState(path: string): BoulderState | null {
|
|
107
|
+
try {
|
|
108
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
109
|
+
return parseBoulderState(parsed);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof Error) return null;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseBoulderState(value: unknown): BoulderState | null {
|
|
117
|
+
if (!isRecord(value)) return null;
|
|
118
|
+
|
|
119
|
+
const works: BoulderWork[] = [];
|
|
120
|
+
const worksValue = value["works"];
|
|
121
|
+
const hasWorksMap = isRecord(worksValue);
|
|
122
|
+
if (hasWorksMap) {
|
|
123
|
+
for (const workValue of Object.values(worksValue)) {
|
|
124
|
+
const work = parseBoulderWork(workValue);
|
|
125
|
+
if (work !== null) works.push(work);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const mirrorWork = parseBoulderWork(value);
|
|
130
|
+
if (works.length === 0 && mirrorWork === null) return null;
|
|
131
|
+
return { works, mirrorWork, hasWorksMap };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseBoulderWork(value: unknown): BoulderWork | null {
|
|
135
|
+
if (!isRecord(value)) return null;
|
|
136
|
+
|
|
137
|
+
const activePlan = value["active_plan"];
|
|
138
|
+
const planName = value["plan_name"];
|
|
139
|
+
if (typeof activePlan !== "string") return null;
|
|
140
|
+
|
|
141
|
+
const status = parseBoulderWorkStatus(value["status"]);
|
|
142
|
+
const sessionIds = parseSessionIds(value["session_ids"]);
|
|
143
|
+
const worktreePath = value["worktree_path"];
|
|
144
|
+
const startedAt = value["started_at"];
|
|
145
|
+
const updatedAt = value["updated_at"];
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
activePlan,
|
|
149
|
+
planName: typeof planName === "string" ? planName : activePlan,
|
|
150
|
+
sessionIds,
|
|
151
|
+
...(status === undefined ? {} : { status }),
|
|
152
|
+
...(typeof startedAt === "string" ? { startedAt } : {}),
|
|
153
|
+
...(typeof updatedAt === "string" ? { updatedAt } : {}),
|
|
154
|
+
...(typeof worktreePath === "string" ? { worktreePath } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getWorkForSession(state: BoulderState, normalizedSessionId: string): BoulderWork | null {
|
|
159
|
+
let newestWork: BoulderWork | null = null;
|
|
160
|
+
let newestWorkMs = 0;
|
|
161
|
+
|
|
162
|
+
for (const work of state.works) {
|
|
163
|
+
if (!work.sessionIds.includes(normalizedSessionId)) continue;
|
|
164
|
+
|
|
165
|
+
const workMs = parseIsoToMs(work.updatedAt ?? work.startedAt) ?? 0;
|
|
166
|
+
if (newestWork === null || workMs > newestWorkMs) {
|
|
167
|
+
newestWork = work;
|
|
168
|
+
newestWorkMs = workMs;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (newestWork !== null) return newestWork;
|
|
173
|
+
if (state.hasWorksMap) return null;
|
|
174
|
+
if (state.mirrorWork?.sessionIds.includes(normalizedSessionId) === true) return state.mirrorWork;
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveBoulderPlanPathForWork(cwd: string, work: BoulderWork): string {
|
|
179
|
+
const absolutePlanPath = resolveTrackedPath(cwd, work.activePlan);
|
|
180
|
+
const worktreePath = work.worktreePath?.trim();
|
|
181
|
+
if (worktreePath === undefined || worktreePath.length === 0) return absolutePlanPath;
|
|
182
|
+
|
|
183
|
+
const relativePlanPath = relative(resolve(cwd), absolutePlanPath);
|
|
184
|
+
if (relativePlanPath.length === 0 || relativePlanPath.startsWith("..") || isAbsolute(relativePlanPath)) {
|
|
185
|
+
return absolutePlanPath;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const worktreePlanPath = resolve(resolveTrackedPath(cwd, worktreePath), relativePlanPath);
|
|
189
|
+
return existsSync(worktreePlanPath) ? worktreePlanPath : absolutePlanPath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveTrackedPath(baseDirectory: string, trackedPath: string): string {
|
|
193
|
+
return isAbsolute(trackedPath) ? resolve(trackedPath) : resolve(baseDirectory, trackedPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseTopLevelCheckbox(line: string): { readonly checked: boolean; readonly label: string } | null {
|
|
197
|
+
if (line.startsWith("- [ ] ")) return { checked: false, label: line.slice(CHECKBOX_PREFIX_LENGTH) };
|
|
198
|
+
if (line.startsWith("- [x] ") || line.startsWith("- [X] ")) {
|
|
199
|
+
return { checked: true, label: line.slice(CHECKBOX_PREFIX_LENGTH) };
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseLevelTwoHeading(line: string): string | null {
|
|
205
|
+
if (!line.startsWith("## ")) return null;
|
|
206
|
+
return line.slice("## ".length).trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isCountedHeading(heading: string | null): boolean {
|
|
210
|
+
return heading === TODO_HEADING || heading === FINAL_VERIFICATION_HEADING;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseBoulderWorkStatus(value: unknown): BoulderWorkStatus | undefined {
|
|
214
|
+
if (value === "active" || value === "paused" || value === "completed" || value === "abandoned") return value;
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseSessionIds(value: unknown): readonly string[] {
|
|
219
|
+
if (!Array.isArray(value)) return [];
|
|
220
|
+
const sessionIds: string[] = [];
|
|
221
|
+
for (const item of value) {
|
|
222
|
+
if (typeof item === "string") sessionIds.push(normalizeSessionId(item));
|
|
223
|
+
}
|
|
224
|
+
return sessionIds;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeSessionId(sessionId: string, platform: "codex" | "opencode" = "opencode"): string {
|
|
228
|
+
if (SESSION_ID_PREFIX_PATTERN.test(sessionId)) return sessionId;
|
|
229
|
+
return `${platform}:${sessionId}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseIsoToMs(value: string | undefined): number | null {
|
|
233
|
+
if (value === undefined) return null;
|
|
234
|
+
const parsed = Date.parse(value);
|
|
235
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
236
|
+
}
|
|
237
|
+
|
|
38
238
|
function isContinuableStatus(status: BoulderWorkStatus | undefined): boolean {
|
|
39
239
|
return status === "active" || status === "paused";
|
|
40
240
|
}
|
|
241
|
+
|
|
242
|
+
function getBoulderFilePath(cwd: string): string {
|
|
243
|
+
return join(cwd, ".omo", "boulder.json");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function emptyChecklist(): PlanChecklist {
|
|
247
|
+
return { completed: 0, remaining: 0, total: 0, nextTaskLabel: null };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
251
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
252
|
+
}
|
package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { getPlanChecklist } from "@oh-my-opencode/boulder-state";
|
|
5
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
-
import { readContinuationState } from "../src/boulder-reader.js";
|
|
5
|
+
import { getPlanChecklist, readContinuationState } from "../src/boulder-reader.js";
|
|
7
6
|
|
|
8
7
|
const cleanupRoots: string[] = [];
|
|
9
8
|
|
|
@@ -109,6 +108,21 @@ describe("start-work boulder state reader", () => {
|
|
|
109
108
|
expect(state).toBeNull();
|
|
110
109
|
});
|
|
111
110
|
|
|
111
|
+
it("#given paused codex work with remaining checklist #when state is read #then continuation is present", () => {
|
|
112
|
+
// given
|
|
113
|
+
const workspace = createWorkspace({
|
|
114
|
+
boulderJson: createBoulderJson({ status: "paused", sessionIds: ["codex:sess_abc"] }),
|
|
115
|
+
planMarkdown: "# Plan\n\n## TODOs\n- [ ] First\n",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// when
|
|
119
|
+
const state = readContinuationState(workspace, "sess_abc");
|
|
120
|
+
|
|
121
|
+
// then
|
|
122
|
+
expect(state?.planName).toBe("launch-plan");
|
|
123
|
+
expect(state?.checklist).toEqual({ completed: 0, remaining: 1, total: 1, nextTaskLabel: "First" });
|
|
124
|
+
});
|
|
125
|
+
|
|
112
126
|
it("#given no remaining top-level checklist items #when state is read #then continuation is absent", () => {
|
|
113
127
|
// given
|
|
114
128
|
const workspace = createWorkspace({
|
|
@@ -136,6 +150,52 @@ describe("start-work boulder state reader", () => {
|
|
|
136
150
|
// then
|
|
137
151
|
expect(state).toBeNull();
|
|
138
152
|
});
|
|
153
|
+
|
|
154
|
+
it("#given bare boulder session id #when codex state is read #then continuation is absent", () => {
|
|
155
|
+
// given
|
|
156
|
+
const workspace = createWorkspace({
|
|
157
|
+
boulderJson: createBoulderJson({ status: "active", sessionIds: ["sess_abc"] }),
|
|
158
|
+
planMarkdown: "# Plan\n\n## TODOs\n- [ ] First\n",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// when
|
|
162
|
+
const state = readContinuationState(workspace, "sess_abc");
|
|
163
|
+
|
|
164
|
+
// then
|
|
165
|
+
expect(state).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("#given works omit the codex session but stale mirror matches #when state is read #then continuation is absent", () => {
|
|
169
|
+
// given
|
|
170
|
+
const workspace = createWorkspace({
|
|
171
|
+
boulderJson: JSON.stringify({
|
|
172
|
+
schema_version: 2,
|
|
173
|
+
active_work_id: "work_1",
|
|
174
|
+
works: {
|
|
175
|
+
work_1: {
|
|
176
|
+
work_id: "work_1",
|
|
177
|
+
active_plan: ".omo/plans/plan.md",
|
|
178
|
+
plan_name: "current-work",
|
|
179
|
+
status: "active",
|
|
180
|
+
started_at: "2026-06-13T00:00:00.000Z",
|
|
181
|
+
session_ids: ["opencode:sess_other"],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
active_plan: ".omo/plans/plan.md",
|
|
185
|
+
plan_name: "stale-mirror",
|
|
186
|
+
status: "active",
|
|
187
|
+
started_at: "2026-06-12T00:00:00.000Z",
|
|
188
|
+
session_ids: ["codex:sess_abc"],
|
|
189
|
+
}),
|
|
190
|
+
planMarkdown: "# Plan\n\n## TODOs\n- [ ] First\n",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// when
|
|
194
|
+
const state = readContinuationState(workspace, "sess_abc");
|
|
195
|
+
|
|
196
|
+
// then
|
|
197
|
+
expect(state).toBeNull();
|
|
198
|
+
});
|
|
139
199
|
});
|
|
140
200
|
|
|
141
201
|
type WorkspaceInput = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { execPath } from "node:process";
|
|
@@ -23,7 +23,11 @@ describe("start-work continuation CLI", () => {
|
|
|
23
23
|
// then
|
|
24
24
|
if (result.error !== undefined) throw result.error;
|
|
25
25
|
expect(result.status).toBe(0);
|
|
26
|
-
|
|
26
|
+
const output = parseStopHookOutput(result.stdout);
|
|
27
|
+
expect(output.decision).toBe("block");
|
|
28
|
+
expect(output.reason).toContain("Remaining top-level checkboxes: `2` of `3`");
|
|
29
|
+
expect(output.reason).toContain("Next incomplete task: `Task one`");
|
|
30
|
+
expect(output.reason).toContain("Your session id in boulder.json: `codex:s1`");
|
|
27
31
|
});
|
|
28
32
|
|
|
29
33
|
it("#given valid SubagentStop stdin #when CLI runs #then stdout contains block JSON", () => {
|
|
@@ -80,6 +84,18 @@ describe("start-work continuation CLI", () => {
|
|
|
80
84
|
expect(result.status).toBe(0);
|
|
81
85
|
expect(result.stdout).toBe("");
|
|
82
86
|
});
|
|
87
|
+
|
|
88
|
+
it("#given built CLI bundle #when inspected #then it does not depend on the monorepo boulder package", () => {
|
|
89
|
+
// given
|
|
90
|
+
const cliPath = join(process.cwd(), "dist", "cli.js");
|
|
91
|
+
|
|
92
|
+
// when
|
|
93
|
+
const source = readFileSync(cliPath, "utf8");
|
|
94
|
+
|
|
95
|
+
// then
|
|
96
|
+
expect(source).not.toContain("@oh-my-opencode/boulder-state");
|
|
97
|
+
expect(source).not.toContain("../../boulder-state");
|
|
98
|
+
});
|
|
83
99
|
});
|
|
84
100
|
|
|
85
101
|
function runCli(subcommand: "stop" | "subagent-stop", input: string) {
|
|
@@ -90,7 +106,7 @@ function createWorkspace(sessionIds: readonly string[]): string {
|
|
|
90
106
|
const root = mkdtempSync(join(tmpdir(), "codex-continuation-cli-"));
|
|
91
107
|
cleanupRoots.push(root);
|
|
92
108
|
mkdirSync(join(root, ".omo", "plans"), { recursive: true });
|
|
93
|
-
writeFileSync(join(root, ".omo", "plans", "plan.md"), "## TODOs\n\n- [ ] Task one\n");
|
|
109
|
+
writeFileSync(join(root, ".omo", "plans", "plan.md"), "## TODOs\n\n- [ ] Task one\n- [x] Done\n- [ ] Task two\n");
|
|
94
110
|
const work = {
|
|
95
111
|
work_id: "w1",
|
|
96
112
|
active_plan: ".omo/plans/plan.md",
|
|
@@ -122,3 +138,18 @@ function makePayload(
|
|
|
122
138
|
last_assistant_message: "done",
|
|
123
139
|
};
|
|
124
140
|
}
|
|
141
|
+
|
|
142
|
+
type ParsedStopHookOutput = {
|
|
143
|
+
readonly decision: unknown;
|
|
144
|
+
readonly reason: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function parseStopHookOutput(stdout: string): ParsedStopHookOutput {
|
|
148
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
149
|
+
if (!isRecord(parsed) || typeof parsed["reason"] !== "string") throw new Error("CLI did not emit Stop hook JSON");
|
|
150
|
+
return { decision: parsed["decision"], reason: parsed["reason"] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
154
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
155
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: teammode
|
|
3
|
+
description: "Codex-only team orchestration: run a named team of cooperating Codex threads with durable, script-managed state. MUST USE when the user asks Codex to create, run, coordinate, inspect, archive, or delete a team of threads/sessions, or to work on something as a team in parallel. The main session is always the leader; members are defined by a concrete part, ownership area, or perspective - never a vague job role; a bundled cross-platform script writes the .omo/teams state plus an auto-generated member field manual. Use a team when the work is not perfectly isolated but parallelizing helps, or when a task still needs exploration under a clear goal; use plain subagents when scope is perfectly isolated or the goal is ambiguous. Triggers: team mode, teammode, make a team, run as a team, team of agents, coordinate threads, parallel Codex threads, archive the team, delete the team."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Teammode
|
|
7
|
+
|
|
8
|
+
Run a named team of cooperating Codex threads under one leader, with durable state on disk.
|
|
9
|
+
This is a Codex-only workflow. It is inspired by the lifecycle concerns in the
|
|
10
|
+
Yeachan-Heo/oh-my-codex team skill, but it does not copy that runtime model and never depends
|
|
11
|
+
on an external terminal runner - it coordinates through Codex's own thread tools plus a bundled
|
|
12
|
+
state script.
|
|
13
|
+
|
|
14
|
+
## When to use a team (and when to use plain subagents instead)
|
|
15
|
+
|
|
16
|
+
Use a TEAM when EITHER holds:
|
|
17
|
+
- the work does NOT split into perfectly isolated pieces, but doing it in parallel is clearly
|
|
18
|
+
more convenient - members will need to see and react to each other's findings; or
|
|
19
|
+
- one task still needs exploration, yet its GOAL is already clear - parallel investigation under
|
|
20
|
+
a fixed objective.
|
|
21
|
+
|
|
22
|
+
Use plain subagents (`$ulw` / `multi_agent_v1.spawn_agent`) - NOT a team - when EITHER holds:
|
|
23
|
+
- the work IS perfectly isolated, so there is no coordination cost worth paying; or
|
|
24
|
+
- the GOAL is still ambiguous, where one mind should resolve direction before any fan-out.
|
|
25
|
+
|
|
26
|
+
A team buys cross-member coordination at a real overhead cost; only spend it when coordination
|
|
27
|
+
is the thing you actually need.
|
|
28
|
+
|
|
29
|
+
## You are the leader
|
|
30
|
+
|
|
31
|
+
The main session is ALWAYS the team leader. Do NOT create a separate leader thread - you
|
|
32
|
+
orchestrate directly. Members are the threads you create; they report to you, and you own the
|
|
33
|
+
final synthesis and any integration.
|
|
34
|
+
|
|
35
|
+
## Compose by part, ownership, or perspective - not by job title
|
|
36
|
+
|
|
37
|
+
A team is ALWAYS two or more members - never a single-member team. One worker on an isolated
|
|
38
|
+
job is a subagent (`multi_agent_v1.spawn_agent`), not a team; if you end up with a single member,
|
|
39
|
+
either split off a second distinct slice or drop the team and use a subagent.
|
|
40
|
+
|
|
41
|
+
Compose the team from what you actually KNOW about the work. Ground the split in real knowledge
|
|
42
|
+
of the problem, then divide it into clear, non-overlapping responsibilities - one per aspect of
|
|
43
|
+
the work - and give each member exactly one. No two members may own the same thing. Define each
|
|
44
|
+
member by a concrete slice: a specific part of the codebase, an ownership area, or a distinct
|
|
45
|
+
perspective/lens. Assigning a vague role ("backend dev", "release analyst", "the tester") is an
|
|
46
|
+
anti-pattern - it gives the member no real boundary and invites overlap. Each member's `focus`
|
|
47
|
+
names what they own concretely; the `lens` is one of `area`, `ownership`, or `perspective`.
|
|
48
|
+
|
|
49
|
+
## Run the script - never hand-write team state
|
|
50
|
+
|
|
51
|
+
A bundled, dependency-free Node script owns all team state so you never author `team.json` or
|
|
52
|
+
the member manual by hand. Run it with `node` (or `bun`); it works on macOS, Linux, and Windows.
|
|
53
|
+
Replace `<skill-root>` with this skill's own directory.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
node "<skill-root>/scripts/team.mjs" init --name "<team>" --session-name "<session>" [--session <leader_session_id>] [--worktree] [--base-branch dev]
|
|
57
|
+
node "<skill-root>/scripts/team.mjs" add-member --team <session_id> --id A --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>" [--branch <branch>]
|
|
58
|
+
node "<skill-root>/scripts/team.mjs" bind-thread --team <session_id> --id A --thread <thread_id> [--cwd <path>]
|
|
59
|
+
node "<skill-root>/scripts/team.mjs" member-prompt --team <session_id> --id A
|
|
60
|
+
node "<skill-root>/scripts/team.mjs" set-status --team <session_id> --id A --status reported|blocked|active|archived [--note "<...>"]
|
|
61
|
+
node "<skill-root>/scripts/team.mjs" archive --team <session_id> [--id A]
|
|
62
|
+
node "<skill-root>/scripts/team.mjs" delete --team <session_id> [--force]
|
|
63
|
+
node "<skill-root>/scripts/team.mjs" status --team <session_id>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`init` creates `.omo/teams/{session_id}/` containing `team.json` (the single durable state file:
|
|
67
|
+
team id, the main-session leader, the member roster, status, worktree config, and a lifecycle
|
|
68
|
+
log), `guide.md` (the auto-generated member field manual), and `artifacts/` (a shared exchange
|
|
69
|
+
space). `{session_id}` is the leader's Codex session id when you can pass it via `--session`;
|
|
70
|
+
otherwise the script generates a stable handle. Re-running `init` is a safe no-op. Every mutating
|
|
71
|
+
subcommand rewrites `guide.md`, so the manual always matches the current team.
|
|
72
|
+
|
|
73
|
+
## Create the team and its threads
|
|
74
|
+
|
|
75
|
+
1. `init` the team, then `add-member` once per member.
|
|
76
|
+
2. Create a durable thread per member with `codex_app.create_thread` - ALWAYS this tool for every
|
|
77
|
+
member, never a spawned agent - titled EXACTLY
|
|
78
|
+
`[team name] {session name}` (keep this convention strictly). If `codex_app.create_thread`
|
|
79
|
+
accepts a working directory / cwd argument, set it to that member's worktree; otherwise the
|
|
80
|
+
member's manual tells it to `cd` there first. Use `codex_app.set_thread_title` if the title
|
|
81
|
+
did not land at creation.
|
|
82
|
+
3. `bind-thread` to record each thread id (and `--cwd`), then send that member's bootstrap
|
|
83
|
+
trigger (printed by `add-member` / `member-prompt`) as the thread's first message. The trigger
|
|
84
|
+
is short on purpose: it tells the new thread to READ its `guide.md` and `team.json` rather than
|
|
85
|
+
carrying the whole protocol inline.
|
|
86
|
+
|
|
87
|
+
Every team member is a real Codex thread created with `codex_app.create_thread` - this is strict,
|
|
88
|
+
not a preference. NEVER substitute `multi_agent_v1.spawn_agent`, or any other in-process subagent,
|
|
89
|
+
for a team member: a spawned agent is an ephemeral helper that does not show up as a team thread,
|
|
90
|
+
cannot carry the `[team name] {session name}` title, and cannot be inspected, titled, archived, or
|
|
91
|
+
re-opened with the `codex_app.*` thread tools - which defeats the entire point of a durable team.
|
|
92
|
+
A member only counts once you have `bind-thread`-ed it to a real `codex_app.create_thread` thread
|
|
93
|
+
id. If the thread-creation tool is unavailable, STOP and say so (see Stop rules); do not quietly
|
|
94
|
+
fall back to a spawned agent.
|
|
95
|
+
|
|
96
|
+
## Communication
|
|
97
|
+
|
|
98
|
+
Coordinate with `codex_app.send_message_to_thread` (leader-to-member and peer digests) and inspect
|
|
99
|
+
status with `codex_app.read_thread`. The generated manual already binds members to the hard rules,
|
|
100
|
+
so you mostly enforce them: all member-to-member and member-to-leader communication is in English;
|
|
101
|
+
when the END user addresses a member, that member replies in the user's own language. Members
|
|
102
|
+
over-communicate relentlessly - constant, small, lean updates that report every finding, hand-off,
|
|
103
|
+
and even the smallest or most trivial step as it happens rather than one final dump - and send
|
|
104
|
+
`WORKING:`/`BLOCKED:` markers so the leader always knows their state; stale, stuck, or
|
|
105
|
+
silently-blocked members are not acceptable, and a finished
|
|
106
|
+
member reports to the leader immediately. Members hand off files and
|
|
107
|
+
memos through the team `artifacts/` directory and reference them by path instead of pasting large
|
|
108
|
+
content. Wait for every required member's final report before you declare the team done.
|
|
109
|
+
|
|
110
|
+
## Worktrees (optional isolation)
|
|
111
|
+
|
|
112
|
+
When parallel members would edit overlapping files, enable isolation with `init --worktree`. Create
|
|
113
|
+
one git worktree per member off the base branch, `bind-thread --cwd <worktree>` so each member's
|
|
114
|
+
worktree is recorded in `team.json`, and rely on the manual to direct each member into its own
|
|
115
|
+
worktree. Each member commits inside its worktree so you can integrate.
|
|
116
|
+
|
|
117
|
+
## Archive, delete, and cleanup
|
|
118
|
+
|
|
119
|
+
DISBAND the team the moment it is no longer needed. A team exists only to do its work; once that
|
|
120
|
+
work is done, or the user no longer wants it, do not leave it lying around - archive every member,
|
|
121
|
+
then delete the team state. A finished team that is never disbanded is a leak.
|
|
122
|
+
|
|
123
|
+
- `archive` closes the team: notify each active member, copy anything useful into `artifacts/`,
|
|
124
|
+
archive each member thread with `codex_app.set_thread_archived`, then `archive` flips the team
|
|
125
|
+
and all members to archived. If a thread-archive tool is unavailable, record that in the team log
|
|
126
|
+
and tell the user - never pretend a member was archived.
|
|
127
|
+
- `delete` removes `.omo/teams/{session_id}` and refuses while the team is unarchived or any member
|
|
128
|
+
is still active unless `--force`.
|
|
129
|
+
- When the work wraps up, integrate each member's worktree the way the user asked - merge directly,
|
|
130
|
+
or open a fresh PR and merge it - then archive and delete. Cleanup is real work; respect the
|
|
131
|
+
user's instruction on how to land it.
|
|
132
|
+
|
|
133
|
+
## Stop rules
|
|
134
|
+
|
|
135
|
+
- Stop and ask before deleting an unarchived team while any member is still active.
|
|
136
|
+
- Member communication stays English unless the user explicitly requests otherwise; user-facing
|
|
137
|
+
replies follow the user's language.
|
|
138
|
+
- Stop if the requested operation needs Codex thread tools (create/read/send/title/archive) and
|
|
139
|
+
they are unavailable; say so instead of faking it.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// team-guide.mjs - render the member-facing field manual (guide.md) and the short
|
|
2
|
+
// bootstrap trigger a new thread reads on startup. Pure string builders, no I/O.
|
|
3
|
+
//
|
|
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 thread is told to READ it instead of
|
|
6
|
+
// receiving the rules inline each turn.
|
|
7
|
+
|
|
8
|
+
function memberLine(member) {
|
|
9
|
+
const thread = member.threadId ? ` | thread ${member.threadId}` : " | thread not created yet";
|
|
10
|
+
const wt = member.worktree?.path ? ` | worktree ${member.worktree.path}` : "";
|
|
11
|
+
return `- **${member.id}** (${member.lens}) - ${member.focus}${member.deliverable ? ` -> ${member.deliverable}` : ""}${thread}${wt} [${member.status}]`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function worktreeSection(team) {
|
|
15
|
+
if (!team.worktree?.enabled) {
|
|
16
|
+
return `## Worktrees
|
|
17
|
+
|
|
18
|
+
This team does not use isolated git worktrees. Work in the shared repository checkout
|
|
19
|
+
the leader is running from. If your changes would collide with another member's files,
|
|
20
|
+
that is a coordination signal: mark yourself \`blocked\` and tell the leader at once.`;
|
|
21
|
+
}
|
|
22
|
+
const lines = team.members
|
|
23
|
+
.filter((m) => m.worktree?.path || m.worktree?.branch)
|
|
24
|
+
.map((m) => `- **${m.id}**: worktree \`${m.worktree.path ?? "(assign with bind-thread --cwd)"}\` on branch \`${m.worktree.branch ?? team.worktree.baseBranch}\``);
|
|
25
|
+
return `## Worktrees (ISOLATION IS ON - this is not optional for you)
|
|
26
|
+
|
|
27
|
+
This team runs each member in its own git worktree branched off \`${team.worktree.baseBranch}\`.
|
|
28
|
+
**You MUST work only inside your own worktree.** The very first thing you do is \`cd\` into it;
|
|
29
|
+
every edit, command, and commit happens there. Never touch another member's worktree, and
|
|
30
|
+
never edit files outside your assigned scope. Commit your work so the leader can integrate it.
|
|
31
|
+
|
|
32
|
+
${lines.length ? lines.join("\n") : "- (worktree paths are assigned as threads are bound; read team.json for yours)"}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildGuide(team) {
|
|
36
|
+
const artifacts = team.paths?.artifacts ?? ".omo/teams/<session_id>/artifacts";
|
|
37
|
+
const teamJson = team.paths?.team ?? ".omo/teams/<session_id>/team.json";
|
|
38
|
+
const roster = team.members.length ? team.members.map(memberLine).join("\n") : "- (no members yet)";
|
|
39
|
+
return `# Team ${team.teamName} - Member Field Manual
|
|
40
|
+
|
|
41
|
+
> Auto-generated by the teammode script. This file is the single source of truth for how
|
|
42
|
+
> this team works. Re-read it whenever you are unsure - do not improvise the protocol.
|
|
43
|
+
|
|
44
|
+
- Team: **${team.teamName}** (id \`${team.teamId}\`)
|
|
45
|
+
- Team state: \`${teamJson}\`
|
|
46
|
+
- Thread/session title convention (use it EXACTLY): \`${team.threadTitleConvention}\`
|
|
47
|
+
|
|
48
|
+
## Who you are (your identity is fixed)
|
|
49
|
+
|
|
50
|
+
**You are a member of team ${team.teamName}, not a generic assistant.** You own exactly one
|
|
51
|
+
slice of this team's work - the focus and deliverable assigned to your id below - and you are
|
|
52
|
+
accountable for it end to end. Read your row, internalize it, and act as that owner for the
|
|
53
|
+
whole session. Do not drift into other members' scope.
|
|
54
|
+
|
|
55
|
+
${roster}
|
|
56
|
+
|
|
57
|
+
## Leader
|
|
58
|
+
|
|
59
|
+
**The main session is the leader. There is no other leader, and you are not it.** You report
|
|
60
|
+
to the leader, take direction from the leader, and send your results to the leader. Peers
|
|
61
|
+
coordinate with each other, but the leader owns the final decision and integration.
|
|
62
|
+
|
|
63
|
+
## Communication rules (hard rules)
|
|
64
|
+
|
|
65
|
+
- **All communication between team members, and from you to the leader, is in English.** This
|
|
66
|
+
is mandatory regardless of the language the task was given in.
|
|
67
|
+
- **Exception: when the END USER addresses you directly, reply in the user's own language.**
|
|
68
|
+
Team-internal traffic stays English; user-facing replies match the user.
|
|
69
|
+
- **Report to the leader the moment your work is complete.** A finished task that was never
|
|
70
|
+
reported does not count as done.
|
|
71
|
+
- **Over-communicate relentlessly - report even the trivial.** Message constantly and
|
|
72
|
+
proactively: every finding, decision, hand-off, and even the smallest or most trivial step, the
|
|
73
|
+
moment it happens, in short lean notes. Never save it up for one final report. When in doubt,
|
|
74
|
+
send the message - far more and far smaller updates always beats going quiet. The instant
|
|
75
|
+
anything changes a peer's assumption, push it to them. Too many messages is correct here; the
|
|
76
|
+
only failure is going quiet.
|
|
77
|
+
- **Stale, stuck, and silently-blocked states are NOT acceptable.** During long work send
|
|
78
|
+
\`WORKING: <your focus> - <phase>\` so the leader can see you are alive. If you truly cannot
|
|
79
|
+
proceed, send \`BLOCKED: <reason>\` immediately and propose the smallest unblock - never sit
|
|
80
|
+
idle and never disappear. Being quiet is a failure, not a pause.
|
|
81
|
+
|
|
82
|
+
## Artifacts (how you hand work off)
|
|
83
|
+
|
|
84
|
+
Exchange files and memos through the team artifacts directory:
|
|
85
|
+
|
|
86
|
+
\`${artifacts}\`
|
|
87
|
+
|
|
88
|
+
Drop deliverables, notes, diffs, and findings there as files, and reference them by path in
|
|
89
|
+
your messages instead of pasting large content into a thread. Treat it as the team's shared
|
|
90
|
+
desk: if a peer or the leader needs something from you, leave it in artifacts and point them to it.
|
|
91
|
+
|
|
92
|
+
${worktreeSection(team)}
|
|
93
|
+
|
|
94
|
+
## Done means reported and verified
|
|
95
|
+
|
|
96
|
+
Your slice is done only when (1) the deliverable exists, (2) you have evidence it works, and
|
|
97
|
+
(3) you have reported it to the leader. Until all three are true, keep going.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildMemberPrompt(team, id) {
|
|
101
|
+
const member = team.members.find((m) => m.id === id);
|
|
102
|
+
if (!member) throw new Error(`no member with id "${id}"`);
|
|
103
|
+
const guide = team.paths?.guide ?? ".omo/teams/<session_id>/guide.md";
|
|
104
|
+
const teamJson = team.paths?.team ?? ".omo/teams/<session_id>/team.json";
|
|
105
|
+
const where = member.cwd ? `Work inside \`${member.cwd}\`.` : "Work from the repository root unless your manual assigns a worktree.";
|
|
106
|
+
return `You are member ${member.id} of team ${team.teamName} - owner of: ${member.focus}.
|
|
107
|
+
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.
|
|
108
|
+
${where}
|
|
109
|
+
Communicate with the team and leader in English; reply to the end user in the user's own language.
|
|
110
|
+
Start now, send WORKING/BLOCKED updates so you never look stale, and report to the leader the moment you are done.`;
|
|
111
|
+
}
|