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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// team-state.mjs - the teammode state model + atomic, symlink-guarded persistence.
|
|
2
|
+
//
|
|
3
|
+
// Zero external dependencies (node builtins only) so it runs byte-identically under
|
|
4
|
+
// `node` and `bun` on macOS, Linux, and Windows with no install step, no POSIX shell,
|
|
5
|
+
// and no python3 precondition. This is the SINGLE source of the team-state shape: the
|
|
6
|
+
// model never hand-writes team.json - it calls these helpers through scripts/team.mjs.
|
|
7
|
+
//
|
|
8
|
+
// Rendering (the member field manual + bootstrap trigger) lives in team-guide.mjs so
|
|
9
|
+
// this file stays the state concern only.
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { lstat, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
13
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
export const LENSES = ["area", "ownership", "perspective"];
|
|
16
|
+
export const MEMBER_STATUSES = ["pending", "active", "reported", "blocked", "archived"];
|
|
17
|
+
// A team is never a single member: two or more distinct slices is the minimum. One isolated
|
|
18
|
+
// job is a subagent, not a team.
|
|
19
|
+
export const MIN_MEMBERS = 2;
|
|
20
|
+
|
|
21
|
+
export function isUnderstaffed(team) {
|
|
22
|
+
return team.members.length < MIN_MEMBERS;
|
|
23
|
+
}
|
|
24
|
+
// A team dir is a single child of .omo/teams. This pattern alone blocks "/", "\", and a
|
|
25
|
+
// leading "." so ".." and "a/b" can never name a team dir (the escape guard).
|
|
26
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
27
|
+
|
|
28
|
+
function isoNow(now) {
|
|
29
|
+
return now ?? new Date().toISOString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildTeam({ teamName, sessionName, sessionId = null, dir = null, worktreeEnabled = false, baseBranch = "dev", now }) {
|
|
33
|
+
if (!teamName?.trim()) throw new Error("team name is required");
|
|
34
|
+
if (!sessionName?.trim()) throw new Error("session name is required");
|
|
35
|
+
const ts = isoNow(now);
|
|
36
|
+
return {
|
|
37
|
+
schemaVersion: 2,
|
|
38
|
+
teamId: randomUUID(),
|
|
39
|
+
teamName: teamName.trim(),
|
|
40
|
+
sessionName: sessionName.trim(),
|
|
41
|
+
sessionId,
|
|
42
|
+
threadTitleConvention: `[${teamName.trim()}] ${sessionName.trim()}`,
|
|
43
|
+
status: "active",
|
|
44
|
+
createdAt: ts,
|
|
45
|
+
updatedAt: ts,
|
|
46
|
+
archivedAt: null,
|
|
47
|
+
leader: { kind: "main-session", sessionId },
|
|
48
|
+
communication: { memberLanguage: "english", replyToUserInUserLanguage: true },
|
|
49
|
+
worktree: { enabled: Boolean(worktreeEnabled), baseBranch, root: dir ? join(dir, "worktrees") : null },
|
|
50
|
+
paths: dir
|
|
51
|
+
? { dir, team: join(dir, "team.json"), guide: join(dir, "guide.md"), artifacts: join(dir, "artifacts") }
|
|
52
|
+
: null,
|
|
53
|
+
members: [],
|
|
54
|
+
log: [{ ts, event: "created", detail: `team ${teamName.trim()}` }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function touch(team, event, detail) {
|
|
59
|
+
const ts = isoNow();
|
|
60
|
+
team.updatedAt = ts;
|
|
61
|
+
team.log.push({ ts, event, detail });
|
|
62
|
+
return team;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function memberById(team, id) {
|
|
66
|
+
const found = team.members.find((m) => m.id === id);
|
|
67
|
+
if (!found) throw new Error(`no member with id "${id}"`);
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizedFocus(focus) {
|
|
72
|
+
return focus.trim().replace(/\s+/g, " ").toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertUniqueMemberFocus(team) {
|
|
76
|
+
const seen = new Map();
|
|
77
|
+
for (const member of team.members) {
|
|
78
|
+
const key = normalizedFocus(member.focus ?? "");
|
|
79
|
+
const previous = seen.get(key);
|
|
80
|
+
if (previous) {
|
|
81
|
+
throw new Error(`member focus "${member.focus}" duplicates "${previous.focus}" (no two members may own the same thing)`);
|
|
82
|
+
}
|
|
83
|
+
seen.set(key, member);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertTeamReadyForThreadBinding(team) {
|
|
88
|
+
if (isUnderstaffed(team)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`cannot bind member threads until the team has at least ${MIN_MEMBERS} distinct members; current count is ${team.members.length}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
assertUniqueMemberFocus(team);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function addMember(team, { id, focus, lens, deliverable = "", branch = null }) {
|
|
97
|
+
if (!id?.trim()) throw new Error("member id is required");
|
|
98
|
+
if (!focus?.trim()) throw new Error("member focus is required - a concrete part, ownership area, or perspective");
|
|
99
|
+
if (!LENSES.includes(lens)) throw new Error(`invalid lens "${lens}" - use one of: ${LENSES.join(", ")}`);
|
|
100
|
+
const memberId = id.trim();
|
|
101
|
+
const memberFocus = focus.trim();
|
|
102
|
+
if (team.members.some((m) => m.id === memberId)) throw new Error(`member id "${memberId}" already exists (duplicate)`);
|
|
103
|
+
const duplicate = team.members.find((m) => normalizedFocus(m.focus) === normalizedFocus(memberFocus));
|
|
104
|
+
if (duplicate) throw new Error(`member focus "${memberFocus}" duplicates "${duplicate.focus}" (no two members may own the same thing)`);
|
|
105
|
+
team.members.push({
|
|
106
|
+
id: memberId,
|
|
107
|
+
focus: memberFocus,
|
|
108
|
+
lens,
|
|
109
|
+
deliverable: deliverable.trim(),
|
|
110
|
+
threadId: null,
|
|
111
|
+
threadTitle: team.threadTitleConvention,
|
|
112
|
+
cwd: null,
|
|
113
|
+
worktree: { path: null, branch: branch ?? null },
|
|
114
|
+
status: "pending",
|
|
115
|
+
});
|
|
116
|
+
return touch(team, "add-member", `member ${memberId} (${lens}): ${memberFocus}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function bindThread(team, { id, threadId, cwd = null, worktreePath = null }) {
|
|
120
|
+
if (!threadId?.trim()) throw new Error("thread id is required");
|
|
121
|
+
assertTeamReadyForThreadBinding(team);
|
|
122
|
+
const m = memberById(team, id);
|
|
123
|
+
m.threadId = threadId.trim();
|
|
124
|
+
m.status = "active";
|
|
125
|
+
if (cwd) m.cwd = cwd;
|
|
126
|
+
if (team.worktree.enabled) m.worktree.path = worktreePath ?? cwd ?? m.worktree.path;
|
|
127
|
+
return touch(team, "bind-thread", `member ${id} -> thread ${threadId.trim()}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function setMemberStatus(team, { id, status, note = "" }) {
|
|
131
|
+
if (!MEMBER_STATUSES.includes(status)) throw new Error(`invalid status "${status}" - use one of: ${MEMBER_STATUSES.join(", ")}`);
|
|
132
|
+
memberById(team, id).status = status;
|
|
133
|
+
return touch(team, "set-status", `member ${id} -> ${status}${note ? `: ${note}` : ""}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function archive(team, { id = null, note = "" } = {}) {
|
|
137
|
+
if (id) {
|
|
138
|
+
memberById(team, id).status = "archived";
|
|
139
|
+
return touch(team, "archive-member", `member ${id}${note ? `: ${note}` : ""}`);
|
|
140
|
+
}
|
|
141
|
+
for (const m of team.members) m.status = "archived";
|
|
142
|
+
team.status = "archived";
|
|
143
|
+
team.archivedAt = isoNow();
|
|
144
|
+
return touch(team, "archive", note || "team archived; all members closed");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function validateTeam(team) {
|
|
148
|
+
if (team?.schemaVersion !== 2) throw new Error("invalid team: schemaVersion must be 2");
|
|
149
|
+
if (!team.teamId || !team.teamName) throw new Error("invalid team: teamId and teamName are required");
|
|
150
|
+
if (team.leader?.kind !== "main-session") throw new Error("invalid team: leader.kind must be main-session");
|
|
151
|
+
if (!Array.isArray(team.members)) throw new Error("invalid team: members must be an array");
|
|
152
|
+
assertUniqueMemberFocus(team);
|
|
153
|
+
return team;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Resolve (and confine) a team dir to a single child of <cwd>/.omo/teams.
|
|
157
|
+
export function resolveTeamDir(cwd, sessionId) {
|
|
158
|
+
if (!SESSION_ID_PATTERN.test(sessionId ?? "")) throw new Error(`invalid or unsafe session id "${sessionId}"`);
|
|
159
|
+
return resolve(cwd, ".omo", "teams", sessionId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function lstatOrNull(p) {
|
|
163
|
+
return lstat(p).catch((error) => {
|
|
164
|
+
if (error && error.code === "ENOENT") return null;
|
|
165
|
+
throw error;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Create a directory chain refusing any symlinked component, so team state can never be
|
|
170
|
+
// written through a symlink that escapes the workspace.
|
|
171
|
+
async function mkdirNoSymlink(dir, stopAt) {
|
|
172
|
+
if (dir === stopAt) return;
|
|
173
|
+
const rel = relative(stopAt, dir);
|
|
174
|
+
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`refused: path escapes ${stopAt}: ${dir}`);
|
|
175
|
+
await mkdirNoSymlink(dirname(dir), stopAt);
|
|
176
|
+
const st = await lstatOrNull(dir);
|
|
177
|
+
if (st) {
|
|
178
|
+
if (st.isSymbolicLink()) throw new Error(`refused: path component is a symlink: ${dir}`);
|
|
179
|
+
if (!st.isDirectory()) throw new Error(`refused: path component is not a directory: ${dir}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await mkdir(dir);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function assertNoSymlinkComponents(dir, stopAt) {
|
|
186
|
+
if (dir === stopAt) return;
|
|
187
|
+
const rel = relative(stopAt, dir);
|
|
188
|
+
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`refused: path escapes ${stopAt}: ${dir}`);
|
|
189
|
+
await assertNoSymlinkComponents(dirname(dir), stopAt);
|
|
190
|
+
const st = await lstatOrNull(dir);
|
|
191
|
+
if (st?.isSymbolicLink()) throw new Error(`refused: path component is a symlink: ${dir}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function ensureTeamDir(cwd, sessionId) {
|
|
195
|
+
const workspaceRoot = resolve(cwd);
|
|
196
|
+
const teamsRoot = resolve(cwd, ".omo", "teams");
|
|
197
|
+
const dir = resolveTeamDir(cwd, sessionId);
|
|
198
|
+
await mkdirNoSymlink(teamsRoot, workspaceRoot);
|
|
199
|
+
await mkdirNoSymlink(dir, teamsRoot);
|
|
200
|
+
await mkdirNoSymlink(join(dir, "artifacts"), dir);
|
|
201
|
+
return dir;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function assertSafeTeamDir(cwd, sessionId) {
|
|
205
|
+
const workspaceRoot = resolve(cwd);
|
|
206
|
+
const teamsRoot = resolve(cwd, ".omo", "teams");
|
|
207
|
+
const dir = resolveTeamDir(cwd, sessionId);
|
|
208
|
+
await assertNoSymlinkComponents(teamsRoot, workspaceRoot);
|
|
209
|
+
await assertNoSymlinkComponents(dir, teamsRoot);
|
|
210
|
+
return dir;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function readTeam(dir) {
|
|
214
|
+
return validateTeam(JSON.parse(await readFile(join(dir, "team.json"), "utf8")));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function teamExists(dir) {
|
|
218
|
+
return (await lstatOrNull(join(dir, "team.json"))) !== null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function persistedFileTarget(team, pathKey, fileName, expectedDir) {
|
|
222
|
+
validateTeam(team);
|
|
223
|
+
if (!team.paths?.dir || !team.paths?.[pathKey]) throw new Error(`invalid team: paths.${pathKey} is required`);
|
|
224
|
+
const dir = resolve(expectedDir);
|
|
225
|
+
if (resolve(team.paths.dir) !== dir) throw new Error(`refused: persisted team dir does not match trusted team dir: ${team.paths.dir}`);
|
|
226
|
+
const target = resolve(team.paths[pathKey]);
|
|
227
|
+
if (target !== resolve(dir, fileName)) throw new Error(`refused: ${fileName} persist target escapes team dir: ${team.paths[pathKey]}`);
|
|
228
|
+
const dirStat = await lstatOrNull(dir);
|
|
229
|
+
if (dirStat?.isSymbolicLink()) throw new Error(`refused: team dir is a symlink: ${dir}`);
|
|
230
|
+
const st = await lstatOrNull(target);
|
|
231
|
+
if (st?.isSymbolicLink()) throw new Error(`refused: ${fileName} is a symlink: ${target}`);
|
|
232
|
+
if (st && !st.isFile()) throw new Error(`refused: ${fileName} is not a file: ${target}`);
|
|
233
|
+
return target;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function writePersistedFileAtomic(team, pathKey, fileName, content, expectedDir) {
|
|
237
|
+
const target = await persistedFileTarget(team, pathKey, fileName, expectedDir);
|
|
238
|
+
const tmp = `${target}.tmp-${process.pid}-${randomUUID()}`;
|
|
239
|
+
await writeFile(tmp, content, { encoding: "utf8", flag: "wx" });
|
|
240
|
+
await rename(tmp, target);
|
|
241
|
+
return team;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function writeTeamAtomic(team, expectedDir) {
|
|
245
|
+
return writePersistedFileAtomic(team, "team", "team.json", `${JSON.stringify(team, null, 2)}\n`, expectedDir);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function writeGuideAtomic(team, content, expectedDir) {
|
|
249
|
+
return writePersistedFileAtomic(team, "guide", "guide.md", content, expectedDir);
|
|
250
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// team.mjs - the teammode controller CLI. Thin argv dispatch over team-state.mjs (the
|
|
3
|
+
// state model) and team-guide.mjs (the member field manual). It replaces hand-written
|
|
4
|
+
// team.json / guide.md so the model never has to author those files by hand.
|
|
5
|
+
//
|
|
6
|
+
// Zero external dependencies (node builtins only): runs identically under node and bun on
|
|
7
|
+
// macOS, Linux, and Windows.
|
|
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 --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>" [--branch <b>]
|
|
11
|
+
// node "<skill-root>/scripts/team.mjs" bind-thread --team <id> --id A --thread <thread-id> [--cwd <path>]
|
|
12
|
+
// node "<skill-root>/scripts/team.mjs" member-prompt --team <id> --id A
|
|
13
|
+
// node "<skill-root>/scripts/team.mjs" set-status --team <id> --id A --status reported|blocked|active|archived [--note "<...>"]
|
|
14
|
+
// node "<skill-root>/scripts/team.mjs" archive --team <id> [--id A] [--note "<...>"]
|
|
15
|
+
// node "<skill-root>/scripts/team.mjs" delete --team <id> [--force]
|
|
16
|
+
// node "<skill-root>/scripts/team.mjs" status --team <id>
|
|
17
|
+
// node "<skill-root>/scripts/team.mjs" guide --team <id>
|
|
18
|
+
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { realpathSync } from "node:fs";
|
|
21
|
+
import { rm } from "node:fs/promises";
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
23
|
+
import { buildGuide, buildMemberPrompt } from "./team-guide.mjs";
|
|
24
|
+
import {
|
|
25
|
+
addMember,
|
|
26
|
+
archive,
|
|
27
|
+
assertSafeTeamDir,
|
|
28
|
+
bindThread,
|
|
29
|
+
buildTeam,
|
|
30
|
+
ensureTeamDir,
|
|
31
|
+
isUnderstaffed,
|
|
32
|
+
MIN_MEMBERS,
|
|
33
|
+
readTeam,
|
|
34
|
+
resolveTeamDir,
|
|
35
|
+
setMemberStatus,
|
|
36
|
+
teamExists,
|
|
37
|
+
writeGuideAtomic,
|
|
38
|
+
writeTeamAtomic,
|
|
39
|
+
} from "./team-state.mjs";
|
|
40
|
+
|
|
41
|
+
function parseFlags(args) {
|
|
42
|
+
const flags = { _: [] };
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
if (!arg.startsWith("--")) {
|
|
46
|
+
flags._.push(arg);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const key = arg.slice(2);
|
|
50
|
+
const next = args[i + 1];
|
|
51
|
+
if (next === undefined || next.startsWith("--")) {
|
|
52
|
+
flags[key] = true;
|
|
53
|
+
} else {
|
|
54
|
+
flags[key] = next;
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return flags;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function requireFlag(flags, name) {
|
|
62
|
+
const value = flags[name];
|
|
63
|
+
if (value === undefined || value === true) throw new Error(`missing required flag --${name}`);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadTeam(cwd, sessionId) {
|
|
68
|
+
const dir = await assertSafeTeamDir(cwd, sessionId);
|
|
69
|
+
return { dir, team: await readTeam(dir) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function persist(team, dir) {
|
|
73
|
+
await writeTeamAtomic(team, dir);
|
|
74
|
+
await writeGuideAtomic(team, buildGuide(team), dir);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const handlers = {
|
|
78
|
+
async init(cwd, flags) {
|
|
79
|
+
const teamName = requireFlag(flags, "name");
|
|
80
|
+
const sessionName = requireFlag(flags, "session-name");
|
|
81
|
+
const sessionId = typeof flags.session === "string" ? flags.session : `team-${randomUUID().slice(0, 8)}`;
|
|
82
|
+
const dir = await ensureTeamDir(cwd, sessionId);
|
|
83
|
+
if (await teamExists(dir)) {
|
|
84
|
+
process.stdout.write(`exists: ${dir} (left untouched; re-init is a safe no-op)\n`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const team = buildTeam({
|
|
88
|
+
teamName,
|
|
89
|
+
sessionName,
|
|
90
|
+
sessionId,
|
|
91
|
+
dir,
|
|
92
|
+
worktreeEnabled: flags.worktree === true,
|
|
93
|
+
baseBranch: typeof flags["base-branch"] === "string" ? flags["base-branch"] : "dev",
|
|
94
|
+
});
|
|
95
|
+
await persist(team, dir);
|
|
96
|
+
process.stdout.write(`created: ${dir}\n`);
|
|
97
|
+
process.stdout.write(`team.json + guide.md written; artifacts/ ready. session id: ${sessionId}\n`);
|
|
98
|
+
process.stdout.write(`next: add-member --team ${sessionId} --id A --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>"\n`);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async "add-member"(cwd, flags) {
|
|
102
|
+
const sessionId = requireFlag(flags, "team");
|
|
103
|
+
const { dir, team } = await loadTeam(cwd, sessionId);
|
|
104
|
+
const memberId = requireFlag(flags, "id").trim();
|
|
105
|
+
addMember(team, {
|
|
106
|
+
id: memberId,
|
|
107
|
+
focus: requireFlag(flags, "focus"),
|
|
108
|
+
lens: requireFlag(flags, "lens"),
|
|
109
|
+
deliverable: typeof flags.deliverable === "string" ? flags.deliverable : "",
|
|
110
|
+
branch: typeof flags.branch === "string" ? flags.branch : null,
|
|
111
|
+
});
|
|
112
|
+
await persist(team, dir);
|
|
113
|
+
process.stdout.write(`added member ${memberId} to team ${sessionId}.\n\nSend this as the new thread's first message (title it "${team.threadTitleConvention}"):\n---\n${buildMemberPrompt(team, memberId)}\n---\n`);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async "bind-thread"(cwd, flags) {
|
|
117
|
+
const sessionId = requireFlag(flags, "team");
|
|
118
|
+
const { dir, team } = await loadTeam(cwd, sessionId);
|
|
119
|
+
bindThread(team, {
|
|
120
|
+
id: requireFlag(flags, "id"),
|
|
121
|
+
threadId: requireFlag(flags, "thread"),
|
|
122
|
+
cwd: typeof flags.cwd === "string" ? flags.cwd : null,
|
|
123
|
+
worktreePath: typeof flags["worktree-path"] === "string" ? flags["worktree-path"] : null,
|
|
124
|
+
});
|
|
125
|
+
await persist(team, dir);
|
|
126
|
+
process.stdout.write(`bound member ${flags.id} to thread ${flags.thread}.\n`);
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async "member-prompt"(cwd, flags) {
|
|
130
|
+
const sessionId = requireFlag(flags, "team");
|
|
131
|
+
const { team } = await loadTeam(cwd, sessionId);
|
|
132
|
+
process.stdout.write(`${buildMemberPrompt(team, requireFlag(flags, "id"))}\n`);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async "set-status"(cwd, flags) {
|
|
136
|
+
const sessionId = requireFlag(flags, "team");
|
|
137
|
+
const { dir, team } = await loadTeam(cwd, sessionId);
|
|
138
|
+
setMemberStatus(team, {
|
|
139
|
+
id: requireFlag(flags, "id"),
|
|
140
|
+
status: requireFlag(flags, "status"),
|
|
141
|
+
note: typeof flags.note === "string" ? flags.note : "",
|
|
142
|
+
});
|
|
143
|
+
await persist(team, dir);
|
|
144
|
+
process.stdout.write(`member ${flags.id} -> ${flags.status}\n`);
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async archive(cwd, flags) {
|
|
148
|
+
const sessionId = requireFlag(flags, "team");
|
|
149
|
+
const { dir, team } = await loadTeam(cwd, sessionId);
|
|
150
|
+
archive(team, {
|
|
151
|
+
id: typeof flags.id === "string" ? flags.id : null,
|
|
152
|
+
note: typeof flags.note === "string" ? flags.note : "",
|
|
153
|
+
});
|
|
154
|
+
await persist(team, dir);
|
|
155
|
+
process.stdout.write(flags.id ? `archived member ${flags.id}\n` : `archived team ${sessionId} and closed all members\n`);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async delete(cwd, flags) {
|
|
159
|
+
const sessionId = requireFlag(flags, "team");
|
|
160
|
+
const { dir, team } = await loadTeam(cwd, sessionId);
|
|
161
|
+
const active = team.members.filter((m) => m.status !== "archived");
|
|
162
|
+
if (flags.force !== true && (team.status !== "archived" || active.length > 0)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`refused: team "${sessionId}" is not archived or still has ${active.length} active member(s). Archive first, or pass --force to delete anyway.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
await rm(dir, { recursive: true, force: true });
|
|
168
|
+
process.stdout.write(`deleted team state: ${dir}\n`);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
async status(cwd, flags) {
|
|
172
|
+
const sessionId = requireFlag(flags, "team");
|
|
173
|
+
const { team } = await loadTeam(cwd, sessionId);
|
|
174
|
+
process.stdout.write(`Team ${team.teamName} [${team.status}] - leader: main session - ${team.members.length} member(s)\n`);
|
|
175
|
+
for (const m of team.members) {
|
|
176
|
+
process.stdout.write(` ${m.id} (${m.lens}) ${m.focus} -> ${m.deliverable || "(no deliverable)"} [${m.status}]${m.threadId ? ` thread=${m.threadId}` : ""}${m.cwd ? ` cwd=${m.cwd}` : ""}\n`);
|
|
177
|
+
}
|
|
178
|
+
if (isUnderstaffed(team)) {
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`WARNING: a team needs at least ${MIN_MEMBERS} members; this team has ${team.members.length}. A single-member team is not a team - add another distinct slice or use a subagent.\n`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async guide(cwd, flags) {
|
|
186
|
+
const sessionId = requireFlag(flags, "team");
|
|
187
|
+
const { dir, team } = await loadTeam(cwd, sessionId);
|
|
188
|
+
await writeGuideAtomic(team, buildGuide(team), dir);
|
|
189
|
+
process.stdout.write(`${team.paths.guide}\n`);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
async function main() {
|
|
194
|
+
const [subcommand, ...rest] = process.argv.slice(2);
|
|
195
|
+
const handler = handlers[subcommand];
|
|
196
|
+
if (!handler) {
|
|
197
|
+
throw new Error(`unknown subcommand "${subcommand ?? ""}" - expected one of: ${Object.keys(handlers).join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
await handler(process.cwd(), parseFlags(rest));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Symlink-robust main-module check: a symlinked install invokes this file through a symlink
|
|
203
|
+
// path while node resolves import.meta.url to the realpath, so compare the resolved real paths
|
|
204
|
+
// (falling back to the literal url comparison if a path cannot be resolved).
|
|
205
|
+
function isInvokedAsScript() {
|
|
206
|
+
const entry = process.argv[1];
|
|
207
|
+
if (!entry) return false;
|
|
208
|
+
try {
|
|
209
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
|
|
210
|
+
} catch {
|
|
211
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isInvokedAsScript()) {
|
|
216
|
+
await main().catch((error) => {
|
|
217
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/// <reference path="../../node_modules/@types/node/index.d.ts" />
|
|
2
|
+
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
export type ComponentPackageJson = {
|
|
6
|
+
readonly version?: string;
|
|
7
|
+
readonly type: string;
|
|
8
|
+
readonly packageManager: string;
|
|
9
|
+
readonly bin: Readonly<Record<string, string>>;
|
|
10
|
+
readonly dependencies?: Readonly<Record<string, string>>;
|
|
11
|
+
readonly optionalDependencies?: Readonly<Record<string, string>>;
|
|
12
|
+
readonly scripts?: Readonly<Record<string, string>>;
|
|
13
|
+
readonly files?: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type HookCommand = {
|
|
17
|
+
readonly command: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type HookEntry = {
|
|
21
|
+
readonly matcher?: string;
|
|
22
|
+
readonly hooks: readonly HookCommand[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type HooksJson = {
|
|
26
|
+
readonly hooks: Record<string, readonly HookEntry[]>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type McpServer = {
|
|
30
|
+
readonly command: string;
|
|
31
|
+
readonly args: readonly string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type McpJson = {
|
|
35
|
+
readonly mcpServers: Record<string, McpServer>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type PluginJson = {
|
|
39
|
+
readonly hooks: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function readPackageJson(path: string): ComponentPackageJson {
|
|
43
|
+
const parsed = readJsonFile(path);
|
|
44
|
+
if (!isComponentPackageJson(parsed)) throw new TypeError(`Invalid package metadata: ${path}`);
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function readHooksJson(path: string): HooksJson {
|
|
49
|
+
const parsed = readJsonFile(path);
|
|
50
|
+
if (!isHooksJson(parsed)) throw new TypeError(`Invalid hooks metadata: ${path}`);
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function readMcpJson(path: string): McpJson {
|
|
55
|
+
const parsed = readJsonFile(path);
|
|
56
|
+
if (!isMcpJson(parsed)) throw new TypeError(`Invalid MCP metadata: ${path}`);
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function readPluginJson(path: string): PluginJson {
|
|
61
|
+
const parsed = readJsonFile(path);
|
|
62
|
+
if (!isPluginJson(parsed)) throw new TypeError(`Invalid plugin metadata: ${path}`);
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readJsonFile(path: string): unknown {
|
|
67
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readTextFile(path: string): string {
|
|
71
|
+
return readFileSync(path, "utf8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function listDirectoryEntries(path: string): readonly string[] {
|
|
75
|
+
return readdirSync(path);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function requireFiles(packageJson: ComponentPackageJson, path: string): readonly string[] {
|
|
79
|
+
if (packageJson.files === undefined) throw new TypeError(`Package metadata missing files: ${path}`);
|
|
80
|
+
return packageJson.files;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function requireScripts(packageJson: ComponentPackageJson, path: string): Readonly<Record<string, string>> {
|
|
84
|
+
if (packageJson.scripts === undefined) throw new TypeError(`Package metadata missing scripts: ${path}`);
|
|
85
|
+
return packageJson.scripts;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function collectHookCommandsFromValue(value: unknown): readonly string[] {
|
|
89
|
+
if (typeof value === "string") return [];
|
|
90
|
+
if (Array.isArray(value)) return value.flatMap(collectHookCommandsFromValue);
|
|
91
|
+
if (!isRecord(value)) return [];
|
|
92
|
+
const ownCommand = typeof value["command"] === "string" ? [value["command"]] : [];
|
|
93
|
+
return [...ownCommand, ...Object.values(value).flatMap(collectHookCommandsFromValue)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isComponentPackageJson(value: unknown): value is ComponentPackageJson {
|
|
97
|
+
if (!isRecord(value)) return false;
|
|
98
|
+
const dependencies = value["dependencies"];
|
|
99
|
+
const optionalDependencies = value["optionalDependencies"];
|
|
100
|
+
const scripts = value["scripts"];
|
|
101
|
+
const files = value["files"];
|
|
102
|
+
return (
|
|
103
|
+
value["type"] === "module" &&
|
|
104
|
+
value["packageManager"] === "npm@11.12.1" &&
|
|
105
|
+
isStringRecord(value["bin"]) &&
|
|
106
|
+
(dependencies === undefined || isStringRecord(dependencies)) &&
|
|
107
|
+
(optionalDependencies === undefined || isStringRecord(optionalDependencies)) &&
|
|
108
|
+
(scripts === undefined || isStringRecord(scripts)) &&
|
|
109
|
+
(files === undefined || isStringArray(files))
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isHooksJson(value: unknown): value is HooksJson {
|
|
114
|
+
if (!isRecord(value) || !isRecord(value["hooks"])) return false;
|
|
115
|
+
return Object.values(value["hooks"]).every(isHookEntries);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isHookEntries(value: unknown): value is readonly HookEntry[] {
|
|
119
|
+
return Array.isArray(value) && value.every(isHookEntry);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isHookEntry(value: unknown): value is HookEntry {
|
|
123
|
+
return isRecord(value) && Array.isArray(value["hooks"]) && value["hooks"].every(isHookCommand);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isHookCommand(value: unknown): value is HookCommand {
|
|
127
|
+
return isRecord(value) && typeof value["command"] === "string";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isMcpJson(value: unknown): value is McpJson {
|
|
131
|
+
if (!isRecord(value) || !isRecord(value["mcpServers"])) return false;
|
|
132
|
+
return Object.values(value["mcpServers"]).every(isMcpServer);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isMcpServer(value: unknown): value is McpServer {
|
|
136
|
+
return (
|
|
137
|
+
isRecord(value) &&
|
|
138
|
+
typeof value["command"] === "string" &&
|
|
139
|
+
Array.isArray(value["args"]) &&
|
|
140
|
+
value["args"].every((item) => typeof item === "string")
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isPluginJson(value: unknown): value is PluginJson {
|
|
145
|
+
return isRecord(value) && typeof value["hooks"] === "string";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isStringArray(value: unknown): value is readonly string[] {
|
|
149
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
153
|
+
return isRecord(value) && Object.values(value).every((item) => typeof item === "string");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
157
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
158
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/codex-ultrawork",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.0",
|
|
4
4
|
"description": "Codex plugin that injects the ultrawork orchestration directive and ships LazyCodex planning, review, QA, and gate agent roles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.12.1",
|