oh-my-opencode 4.14.0 → 4.14.1
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/references/isolation.md +1 -1
- package/README.md +1 -1
- package/dist/cli/doctor/framework/types.d.ts +2 -0
- package/dist/cli/index.js +149 -1426
- package/dist/cli-node/index.js +149 -1426
- package/dist/index.js +4255 -714
- package/dist/plugin/native-skills.d.ts +9 -0
- package/dist/plugin/system-transform.d.ts +1 -1
- package/dist/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/dist/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/dist/skills/lcx-doctor/SKILL.md +48 -13
- package/dist/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/dist/skills/lcx-report-bug/SKILL.md +44 -13
- package/dist/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/dist/skills/visual-qa/SKILL.md +5 -5
- package/dist/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/dist/skills/visual-qa/scripts/cli.test.ts +7 -2
- package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
- package/dist/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/dist/tools/delegate-task/skill-resolver.d.ts +1 -0
- package/dist/tools/delegate-task/types.d.ts +2 -0
- package/dist/tools/skill/types.d.ts +1 -0
- package/package.json +26 -13
- package/packages/git-bash-mcp/dist/cli.js +2 -0
- package/packages/lsp-daemon/dist/cli.js +15 -4
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +1 -0
- package/packages/lsp-daemon/dist/ensure-daemon.js +9 -3
- package/packages/lsp-daemon/dist/index.js +15 -4
- package/packages/lsp-daemon/dist/paths.d.ts +1 -0
- package/packages/lsp-daemon/dist/paths.js +6 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -7
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +123 -10
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +123 -10
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +126 -9
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +141 -0
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +90 -25
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +2 -0
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +2 -0
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +56 -0
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +2 -5
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +2 -0
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +65 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +6 -1
- package/packages/omo-codex/plugin/components/rules/dist/cli.js +8 -106
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +4 -17
- package/packages/omo-codex/plugin/components/start-work-continuation/package.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 +17 -5
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +63 -1
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +80 -50
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +4 -12
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +7 -33
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +4 -12
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-quality-gate.json +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +3 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-doc.test.ts +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +7 -6
- package/packages/omo-codex/plugin/package-lock.json +16 -56
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +48 -13
- package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +44 -13
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +17 -5
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +63 -1
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +80 -50
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -4
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +5 -5
- package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/packages/omo-codex/plugin/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +38 -2
- package/packages/omo-codex/plugin/test/component-bin-names.test.mjs +12 -11
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +99 -1
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +62 -8
- package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/teammode-archive-ambiguity.test.mjs +102 -0
- package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +41 -1
- package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +93 -0
- package/packages/omo-codex/scripts/install-bin-links.test.mjs +6 -0
- package/packages/omo-codex/scripts/install-cache-copy.test.mjs +28 -0
- package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +43 -3
- package/packages/omo-codex/scripts/install-delegated-command.test.mjs +93 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +103 -28
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +21 -2
- package/packages/omo-codex/scripts/install-local.test.mjs +0 -1
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/lcx-doctor/SKILL.md +48 -13
- package/packages/shared-skills/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +44 -13
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/visual-qa/SKILL.md +5 -5
- package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/packages/shared-skills/skills/visual-qa/scripts/cli.test.ts +7 -2
- package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
- package/packages/shared-skills/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/dist/cli/sparkshell-appserver-websocket.d.ts +0 -4
- package/dist/cli/sparkshell-appserver.d.ts +0 -16
- package/dist/cli/sparkshell-condense.d.ts +0 -10
- package/dist/cli/sparkshell-parse.d.ts +0 -24
- package/dist/cli/sparkshell-session-context.d.ts +0 -20
- package/dist/cli/sparkshell-spark.d.ts +0 -23
- package/dist/cli/sparkshell.d.ts +0 -33
- package/dist/shared/sparkshell-awareness.d.ts +0 -5
- package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +0 -106
- package/packages/omo-codex/plugin/components/rules/test/sparkshell-awareness.test.ts +0 -355
|
@@ -68,7 +68,7 @@ node "<skill-root>/scripts/team.mjs" set-status --team <session_id> --id A --s
|
|
|
68
68
|
node "<skill-root>/scripts/team.mjs" worktree-add --team <session_id> --id A [--base-branch <branch>]
|
|
69
69
|
node "<skill-root>/scripts/team.mjs" worktree-remove --team <session_id> --id A [--force]
|
|
70
70
|
node "<skill-root>/scripts/team.mjs" integrate --team <session_id> [--id A]
|
|
71
|
-
node "<skill-root>/scripts/team.mjs" archive --team <session_id> [--id A]
|
|
71
|
+
node "<skill-root>/scripts/team.mjs" archive --team <session_id> [--id A] [--note "<...>"]
|
|
72
72
|
node "<skill-root>/scripts/team.mjs" delete --team <session_id> [--force]
|
|
73
73
|
node "<skill-root>/scripts/team.mjs" status --team <session_id>
|
|
74
74
|
```
|
|
@@ -80,6 +80,13 @@ space). `{session_id}` is the leader's Codex session id when you can pass it via
|
|
|
80
80
|
otherwise the script generates a stable handle. Re-running `init` is a safe no-op. Every mutating
|
|
81
81
|
subcommand rewrites `guide.md`, so the manual always matches the current team.
|
|
82
82
|
|
|
83
|
+
Mutating subcommands take a per-team state lock before reading and rewriting `team.json`. It is
|
|
84
|
+
safe to run independent `add-member`, `bind-thread`, `set-status`, `archive`, `delete`, `guide`,
|
|
85
|
+
and worktree mutation commands concurrently against the same team: they serialize and each command
|
|
86
|
+
reads the latest committed state before writing. If a command reports that team state is locked,
|
|
87
|
+
do not treat the intended mutation as complete; retry after the named command finishes, or inspect
|
|
88
|
+
`.omo/teams/{session_id}/.team.lock/owner.json` if the previous command crashed.
|
|
89
|
+
|
|
83
90
|
## Create the team and its threads
|
|
84
91
|
|
|
85
92
|
1. `init` the team, then `add-member` once per member.
|
|
@@ -183,12 +190,17 @@ result against that todo's acceptance criteria before you integrate.
|
|
|
183
190
|
|
|
184
191
|
DISBAND the team the moment it is no longer needed. A team exists only to do its work; once that
|
|
185
192
|
work is done, or the user no longer wants it, do not leave it lying around - archive every member,
|
|
186
|
-
then delete the team state
|
|
193
|
+
then delete the team state only after archival evidence is clean or preserved. A finished team that
|
|
194
|
+
is never disbanded is a leak.
|
|
187
195
|
|
|
188
196
|
- `archive` closes the team: notify each active member, copy anything useful into `artifacts/`,
|
|
189
|
-
archive each member thread with `codex_app.set_thread_archived
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
then try to archive each member thread with `codex_app.set_thread_archived`. Treat Codex App
|
|
198
|
+
failures such as "Ambiguous Codex thread id" or a thread id that is ambiguous across hosts as an
|
|
199
|
+
app-thread archival blocker, not as a team-state blocker: record the failure in the team log,
|
|
200
|
+
tell the user which member thread was not proven archived, and continue the team-state archive
|
|
201
|
+
with `archive --note "<blocker>"`. Never pretend a member thread was archived. Do not delete the
|
|
202
|
+
team state after an app-thread archival blocker unless the evidence has been copied elsewhere or
|
|
203
|
+
the user explicitly accepts that evidence loss.
|
|
192
204
|
- `delete` removes `.omo/teams/{session_id}` and refuses while the team is unarchived or any member
|
|
193
205
|
is still active unless `--force`.
|
|
194
206
|
- When the work wraps up, land it the way the user asked: `integrate --team <id>` for a direct merge
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// this file stays the state concern only.
|
|
10
10
|
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { lstat, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
12
|
+
import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
13
13
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
14
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
14
15
|
|
|
15
16
|
export const LENSES = ["area", "ownership", "perspective"];
|
|
16
17
|
export const MEMBER_STATUSES = ["pending", "active", "reported", "blocked", "archived"];
|
|
@@ -24,6 +25,8 @@ export function isUnderstaffed(team) {
|
|
|
24
25
|
// A team dir is a single child of .omo/teams. This pattern alone blocks "/", "\", and a
|
|
25
26
|
// leading "." so ".." and "a/b" can never name a team dir (the escape guard).
|
|
26
27
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
28
|
+
const LOCK_TIMEOUT_MS = Number.parseInt(process.env.OMO_TEAMMODE_LOCK_TIMEOUT_MS ?? "10000", 10);
|
|
29
|
+
const LOCK_RETRY_MS = Number.parseInt(process.env.OMO_TEAMMODE_LOCK_RETRY_MS ?? "25", 10);
|
|
27
30
|
|
|
28
31
|
function isoNow(now) {
|
|
29
32
|
return now ?? new Date().toISOString();
|
|
@@ -233,6 +236,65 @@ async function lstatOrNull(p) {
|
|
|
233
236
|
});
|
|
234
237
|
}
|
|
235
238
|
|
|
239
|
+
function positiveIntegerOrFallback(value, fallback) {
|
|
240
|
+
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function readLockOwner(lockDir) {
|
|
244
|
+
return readFile(join(lockDir, "owner.json"), "utf8")
|
|
245
|
+
.then((content) => JSON.parse(content))
|
|
246
|
+
.catch(() => null);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function describeLockOwner(lockDir) {
|
|
250
|
+
const owner = await readLockOwner(lockDir);
|
|
251
|
+
if (!owner || typeof owner !== "object") return "another team.mjs command";
|
|
252
|
+
const parts = [];
|
|
253
|
+
if (owner.command) parts.push(String(owner.command));
|
|
254
|
+
if (owner.pid) parts.push(`pid ${owner.pid}`);
|
|
255
|
+
if (owner.createdAt) parts.push(`since ${owner.createdAt}`);
|
|
256
|
+
return parts.length > 0 ? parts.join(", ") : "another team.mjs command";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function withTeamLock(dir, command, fn, options = {}) {
|
|
260
|
+
const timeoutMs = positiveIntegerOrFallback(options.timeoutMs ?? LOCK_TIMEOUT_MS, 10000);
|
|
261
|
+
const retryMs = positiveIntegerOrFallback(options.retryMs ?? LOCK_RETRY_MS, 25);
|
|
262
|
+
const lockDir = join(dir, ".team.lock");
|
|
263
|
+
const deadline = Date.now() + timeoutMs;
|
|
264
|
+
let acquired = false;
|
|
265
|
+
for (;;) {
|
|
266
|
+
try {
|
|
267
|
+
await mkdir(lockDir, { mode: 0o700 });
|
|
268
|
+
acquired = true;
|
|
269
|
+
await writeFile(
|
|
270
|
+
join(lockDir, "owner.json"),
|
|
271
|
+
`${JSON.stringify({ pid: process.pid, command, createdAt: new Date().toISOString() }, null, 2)}\n`,
|
|
272
|
+
{ encoding: "utf8", flag: "wx" },
|
|
273
|
+
);
|
|
274
|
+
break;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (acquired) {
|
|
277
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
if (!error || error.code !== "EEXIST") throw error;
|
|
281
|
+
const st = await lstatOrNull(lockDir);
|
|
282
|
+
if (st?.isSymbolicLink()) throw new Error(`refused: team lock path is a symlink: ${lockDir}`);
|
|
283
|
+
if (st && !st.isDirectory()) throw new Error(`refused: team lock path is not a directory: ${lockDir}`);
|
|
284
|
+
if (Date.now() >= deadline) {
|
|
285
|
+
const owner = await describeLockOwner(lockDir);
|
|
286
|
+
throw new Error(`team state is locked by ${owner}; retry after that command completes`);
|
|
287
|
+
}
|
|
288
|
+
await delay(Math.min(retryMs, Math.max(1, deadline - Date.now())));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
return await fn();
|
|
293
|
+
} finally {
|
|
294
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
236
298
|
// Create a directory chain refusing any symlinked component, so team state can never be
|
|
237
299
|
// written through a symlink that escapes the workspace.
|
|
238
300
|
async function mkdirNoSymlink(dir, stopAt) {
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
setMemberStatus,
|
|
36
36
|
setMemberWorktree,
|
|
37
37
|
teamExists,
|
|
38
|
+
withTeamLock,
|
|
38
39
|
writeGuideAtomic,
|
|
39
40
|
writeTeamAtomic,
|
|
40
41
|
} from "./team-state.mjs";
|
|
@@ -82,57 +83,71 @@ async function persist(team, dir) {
|
|
|
82
83
|
await writeGuideAtomic(team, buildGuide(team), dir);
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
async function mutateTeam(cwd, sessionId, command, fn) {
|
|
87
|
+
const dir = await assertSafeTeamDir(cwd, sessionId);
|
|
88
|
+
return withTeamLock(dir, command, async () => {
|
|
89
|
+
const team = await readTeam(dir);
|
|
90
|
+
return fn(team, dir);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
const handlers = {
|
|
86
95
|
async init(cwd, flags) {
|
|
87
96
|
const teamName = requireFlag(flags, "name");
|
|
88
97
|
const sessionName = requireFlag(flags, "session-name");
|
|
89
98
|
const sessionId = typeof flags.session === "string" ? flags.session : `team-${randomUUID().slice(0, 8)}`;
|
|
90
99
|
const dir = await ensureTeamDir(cwd, sessionId);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
await withTeamLock(dir, "init", async () => {
|
|
101
|
+
if (await teamExists(dir)) {
|
|
102
|
+
process.stdout.write(`exists: ${dir} (left untouched; re-init is a safe no-op)\n`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const team = buildTeam({
|
|
106
|
+
teamName,
|
|
107
|
+
sessionName,
|
|
108
|
+
sessionId,
|
|
109
|
+
dir,
|
|
110
|
+
worktreeEnabled: flags.worktree === true,
|
|
111
|
+
baseBranch: typeof flags["base-branch"] === "string" ? flags["base-branch"] : "dev",
|
|
112
|
+
});
|
|
113
|
+
await persist(team, dir);
|
|
114
|
+
process.stdout.write(`created: ${dir}\n`);
|
|
115
|
+
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`);
|
|
102
117
|
});
|
|
103
|
-
await persist(team, dir);
|
|
104
|
-
process.stdout.write(`created: ${dir}\n`);
|
|
105
|
-
process.stdout.write(`team.json + guide.md written; artifacts/ ready. session id: ${sessionId}\n`);
|
|
106
|
-
process.stdout.write(`next: add-member --team ${sessionId} --id A --name "<short role>" --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>"\n`);
|
|
107
118
|
},
|
|
108
119
|
|
|
109
120
|
async "add-member"(cwd, flags) {
|
|
110
121
|
const sessionId = requireFlag(flags, "team");
|
|
111
|
-
const { dir, team } = await loadTeam(cwd, sessionId);
|
|
112
122
|
const memberId = requireFlag(flags, "id").trim();
|
|
113
|
-
|
|
123
|
+
const memberInput = {
|
|
114
124
|
id: memberId,
|
|
115
125
|
name: typeof flags.name === "string" ? flags.name : null,
|
|
116
126
|
focus: requireFlag(flags, "focus"),
|
|
117
127
|
lens: requireFlag(flags, "lens"),
|
|
118
128
|
deliverable: typeof flags.deliverable === "string" ? flags.deliverable : "",
|
|
119
129
|
branch: typeof flags.branch === "string" ? flags.branch : null,
|
|
130
|
+
};
|
|
131
|
+
const { team, member } = await mutateTeam(cwd, sessionId, "add-member", async (team, dir) => {
|
|
132
|
+
addMember(team, memberInput);
|
|
133
|
+
await persist(team, dir);
|
|
134
|
+
return { team, member: team.members.find((m) => m.id === memberId) };
|
|
120
135
|
});
|
|
121
|
-
await persist(team, dir);
|
|
122
|
-
const member = team.members.find((m) => m.id === memberId);
|
|
123
136
|
process.stdout.write(`added member ${memberId} to team ${sessionId}.\n\nSend this as the new thread's first message (title the thread "${member.threadTitle}"):\n---\n${buildMemberPrompt(team, memberId)}\n---\n`);
|
|
124
137
|
},
|
|
125
138
|
|
|
126
139
|
async "bind-thread"(cwd, flags) {
|
|
127
140
|
const sessionId = requireFlag(flags, "team");
|
|
128
|
-
const
|
|
129
|
-
bindThread(team, {
|
|
141
|
+
const input = {
|
|
130
142
|
id: requireFlag(flags, "id"),
|
|
131
143
|
threadId: requireFlag(flags, "thread"),
|
|
132
144
|
cwd: typeof flags.cwd === "string" ? flags.cwd : null,
|
|
133
145
|
worktreePath: typeof flags["worktree-path"] === "string" ? flags["worktree-path"] : null,
|
|
146
|
+
};
|
|
147
|
+
await mutateTeam(cwd, sessionId, "bind-thread", async (team, dir) => {
|
|
148
|
+
bindThread(team, input);
|
|
149
|
+
await persist(team, dir);
|
|
134
150
|
});
|
|
135
|
-
await persist(team, dir);
|
|
136
151
|
process.stdout.write(`bound member ${flags.id} to thread ${flags.thread}.\n`);
|
|
137
152
|
},
|
|
138
153
|
|
|
@@ -144,25 +159,29 @@ const handlers = {
|
|
|
144
159
|
|
|
145
160
|
async "set-status"(cwd, flags) {
|
|
146
161
|
const sessionId = requireFlag(flags, "team");
|
|
147
|
-
const
|
|
148
|
-
setMemberStatus(team, {
|
|
162
|
+
const input = {
|
|
149
163
|
id: requireFlag(flags, "id"),
|
|
150
164
|
status: requireFlag(flags, "status"),
|
|
151
165
|
note: typeof flags.note === "string" ? flags.note : "",
|
|
166
|
+
};
|
|
167
|
+
await mutateTeam(cwd, sessionId, "set-status", async (team, dir) => {
|
|
168
|
+
setMemberStatus(team, input);
|
|
169
|
+
await persist(team, dir);
|
|
152
170
|
});
|
|
153
|
-
await persist(team, dir);
|
|
154
171
|
process.stdout.write(`member ${flags.id} -> ${flags.status}\n`);
|
|
155
172
|
},
|
|
156
173
|
|
|
157
174
|
async "worktree-add"(cwd, flags) {
|
|
158
175
|
const sessionId = requireFlag(flags, "team");
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const result =
|
|
162
|
-
|
|
176
|
+
const memberId = requireFlag(flags, "id");
|
|
177
|
+
const baseBranch = typeof flags["base-branch"] === "string" ? flags["base-branch"] : null;
|
|
178
|
+
const { member, result } = await mutateTeam(cwd, sessionId, "worktree-add", async (team, dir) => {
|
|
179
|
+
const member = memberOrThrow(team, memberId);
|
|
180
|
+
const result = addMemberWorktree(cwd, team, member, { baseBranch });
|
|
181
|
+
setMemberWorktree(team, { id: member.id, path: result.path, branch: result.branch });
|
|
182
|
+
await persist(team, dir);
|
|
183
|
+
return { member, result };
|
|
163
184
|
});
|
|
164
|
-
setMemberWorktree(team, { id: member.id, path: result.path, branch: result.branch });
|
|
165
|
-
await persist(team, dir);
|
|
166
185
|
const note = result.created ? "" : " (already exists)";
|
|
167
186
|
const thread = member.threadId
|
|
168
187
|
? `\nMember thread: ${codexThreadLink(member.threadId)}\nTell that member to: cd "${result.path}"`
|
|
@@ -174,11 +193,15 @@ const handlers = {
|
|
|
174
193
|
|
|
175
194
|
async "worktree-remove"(cwd, flags) {
|
|
176
195
|
const sessionId = requireFlag(flags, "team");
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
196
|
+
const memberId = requireFlag(flags, "id");
|
|
197
|
+
const force = flags.force === true;
|
|
198
|
+
const member = await mutateTeam(cwd, sessionId, "worktree-remove", async (team, dir) => {
|
|
199
|
+
const member = memberOrThrow(team, memberId);
|
|
200
|
+
removeMemberWorktree(cwd, team, member, { force });
|
|
201
|
+
clearMemberWorktree(team, { id: member.id });
|
|
202
|
+
await persist(team, dir);
|
|
203
|
+
return member;
|
|
204
|
+
});
|
|
182
205
|
process.stdout.write(`removed worktree for member ${member.id}\n`);
|
|
183
206
|
},
|
|
184
207
|
|
|
@@ -208,25 +231,30 @@ const handlers = {
|
|
|
208
231
|
|
|
209
232
|
async archive(cwd, flags) {
|
|
210
233
|
const sessionId = requireFlag(flags, "team");
|
|
211
|
-
const
|
|
212
|
-
archive(team, {
|
|
234
|
+
const input = {
|
|
213
235
|
id: typeof flags.id === "string" ? flags.id : null,
|
|
214
236
|
note: typeof flags.note === "string" ? flags.note : "",
|
|
237
|
+
};
|
|
238
|
+
await mutateTeam(cwd, sessionId, "archive", async (team, dir) => {
|
|
239
|
+
archive(team, input);
|
|
240
|
+
await persist(team, dir);
|
|
215
241
|
});
|
|
216
|
-
await persist(team, dir);
|
|
217
242
|
process.stdout.write(flags.id ? `archived member ${flags.id}\n` : `archived team ${sessionId} and closed all members\n`);
|
|
218
243
|
},
|
|
219
244
|
|
|
220
245
|
async delete(cwd, flags) {
|
|
221
246
|
const sessionId = requireFlag(flags, "team");
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
247
|
+
const dir = await assertSafeTeamDir(cwd, sessionId);
|
|
248
|
+
await withTeamLock(dir, "delete", async () => {
|
|
249
|
+
const team = await readTeam(dir);
|
|
250
|
+
const active = team.members.filter((m) => m.status !== "archived");
|
|
251
|
+
if (flags.force !== true && (team.status !== "archived" || active.length > 0)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`refused: team "${sessionId}" is not archived or still has ${active.length} active member(s). Archive first, or pass --force to delete anyway.`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
await rm(dir, { recursive: true, force: true });
|
|
257
|
+
});
|
|
230
258
|
process.stdout.write(`deleted team state: ${dir}\n`);
|
|
231
259
|
},
|
|
232
260
|
|
|
@@ -247,9 +275,11 @@ const handlers = {
|
|
|
247
275
|
|
|
248
276
|
async guide(cwd, flags) {
|
|
249
277
|
const sessionId = requireFlag(flags, "team");
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
const guidePath = await mutateTeam(cwd, sessionId, "guide", async (team, dir) => {
|
|
279
|
+
await writeGuideAtomic(team, buildGuide(team), dir);
|
|
280
|
+
return team.paths.guide;
|
|
281
|
+
});
|
|
282
|
+
process.stdout.write(`${guidePath}\n`);
|
|
253
283
|
},
|
|
254
284
|
};
|
|
255
285
|
|
|
@@ -20,7 +20,7 @@ This skill is intentionally compact. The full workflow lives in `references/full
|
|
|
20
20
|
## Non-Negotiables
|
|
21
21
|
|
|
22
22
|
- Use the ulw-loop CLI state under `.omo/ulw-loop`; do not hand-edit goal state.
|
|
23
|
-
- After any compaction or context loss, re-read brief + goals + ledger FIRST
|
|
23
|
+
- After any compaction or context loss, re-read brief + goals + ledger FIRST plus `omo ulw-loop status --json`, then resume; never re-plan from scratch.
|
|
24
24
|
- If `omo ulw-loop create-goals` says the existing aggregate is already complete, start unrelated new work with a fresh `--session-id <new-id>` instead of steering or forcing the completed default state. Use `--force` only to intentionally overwrite completed evidence.
|
|
25
25
|
- Every success criterion needs observable evidence from a real surface: a channel (tmux, HTTP, browser, computer-use) or, for CLI- or data-shaped criteria, an auxiliary surface (CLI stdout, DB diff, parsed config dump).
|
|
26
26
|
- Record evidence through the CLI only after cleanup receipts are available.
|
|
@@ -65,7 +65,7 @@ Codex subagent reliability:
|
|
|
65
65
|
- `.omo/ulw-loop/goals.json`: goals with embedded `successCriteria` per goal.
|
|
66
66
|
- `.omo/ulw-loop/ledger.jsonl`: append-only audit trail.
|
|
67
67
|
- Read artifacts before resuming, steering, or checkpointing.
|
|
68
|
-
- After compaction or context loss, re-read brief + goals + ledger FIRST
|
|
68
|
+
- After compaction or context loss, re-read brief + goals + ledger FIRST, then `omo ulw-loop status --json`. Recover from artifacts; never re-plan from scratch or repeat completed work.
|
|
69
69
|
- Never invent state outside `.omo/ulw-loop` artifacts or `omo ulw-loop status --json`.
|
|
70
70
|
|
|
71
71
|
## Bootstrap
|
|
@@ -194,9 +194,9 @@ omo ulw-loop checkpoint --goal-id <id> --status complete --evidence "<e2e eviden
|
|
|
194
194
|
`--quality-gate-json` shape:
|
|
195
195
|
```json
|
|
196
196
|
{
|
|
197
|
-
"codeReview":{"by":"lazycodex-code-reviewer","recommendation":"APPROVE","codeQualityStatus":"CLEAR","reportPath":"
|
|
198
|
-
"manualQa":{"by":"lazycodex-qa-executor","status":"passed","evidence":"CLI and data surfaces passed.","surfaceEvidence":[{"id":"surface-cli-pass","criterionRef":"C1","surface":"cli","invocation":"omo ulw-loop checkpoint --quality-gate-json sample-quality-gate.json --json","verdict":"passed","artifactRefs":["artifact-cli-pass"]},{"id":"surface-data-pass","criterionRef":"C2","surface":"data","invocation":"diff -u before-ledger.json after-ledger.json","verdict":"passed","artifactRefs":["artifact-data-diff"]}],"adversarialCases":[{"id":"adv-malformed-input","criterionRef":"C3","scenario":"malformed gate input omits manual QA evidence","expectedBehavior":"validator rejects ULW_LOOP_QUALITY_GATE_INVALID","verdict":"passed","artifactRefs":["artifact-cli-reject"]}],"artifactRefs":[{"id":"artifact-cli-pass","kind":"cli-transcript","description":"CLI pass artifact.","path":"
|
|
199
|
-
"gateReview":{"by":"lazycodex-gate-reviewer","recommendation":"APPROVE","reportPath":"
|
|
197
|
+
"codeReview":{"by":"lazycodex-code-reviewer","recommendation":"APPROVE","codeQualityStatus":"CLEAR","reportPath":"test/fixtures/artifacts/code-review.md","evidence":"Diff review passed.","blockers":[]},
|
|
198
|
+
"manualQa":{"by":"lazycodex-qa-executor","status":"passed","evidence":"CLI and data surfaces passed.","surfaceEvidence":[{"id":"surface-cli-pass","criterionRef":"C1","surface":"cli","invocation":"omo ulw-loop checkpoint --quality-gate-json sample-quality-gate.json --json","verdict":"passed","artifactRefs":["artifact-cli-pass"]},{"id":"surface-data-pass","criterionRef":"C2","surface":"data","invocation":"diff -u before-ledger.json after-ledger.json","verdict":"passed","artifactRefs":["artifact-data-diff"]}],"adversarialCases":[{"id":"adv-malformed-input","criterionRef":"C3","scenario":"malformed gate input omits manual QA evidence","expectedBehavior":"validator rejects ULW_LOOP_QUALITY_GATE_INVALID","verdict":"passed","artifactRefs":["artifact-cli-reject"]}],"artifactRefs":[{"id":"artifact-cli-pass","kind":"cli-transcript","description":"CLI pass artifact.","path":"test/fixtures/artifacts/cli-pass.txt"},{"id":"artifact-cli-reject","kind":"log","description":"Reject log artifact.","path":"test/fixtures/artifacts/rejection.txt"},{"id":"artifact-data-diff","kind":"data-diff","description":"Data diff artifact.","path":"test/fixtures/artifacts/data-diff.txt"}]},
|
|
199
|
+
"gateReview":{"by":"lazycodex-gate-reviewer","recommendation":"APPROVE","reportPath":"test/fixtures/artifacts/gate-review.md","evidence":"Gate review passed.","blockers":[]},
|
|
200
200
|
"iteration":{"fullRerun":true,"status":"passed","rerunCommands":["bunx vitest run packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-doc.test.ts"],"evidence":"Focused rerun passed."},
|
|
201
201
|
"criteriaCoverage":{"totalCriteria":3,"passCount":3,"originalIntent":"User wanted artifact-backed completion.","desiredOutcome":"Behavior ships with review and QA evidence.","userOutcomeReview":"Result matches brief and goals.","adversarialClassesCovered":["malformed_input","stale_state"]}
|
|
202
202
|
}
|
|
@@ -35,7 +35,7 @@ Verify a rendered UI against intent using objective script evidence plus two par
|
|
|
35
35
|
- Use when output must match a mock, a baseline, or a stated design intent; when you suspect a regression; when CJK (Korean/Japanese/Chinese) text may clip, misalign, or wrap awkwardly; when a claimed design system might actually be a flat image; when a terminal layout may overflow or its borders may break.
|
|
36
36
|
- Skip when there is no rendered surface (pure backend or library logic with no visual or terminal output). For broad post-implementation review use review-work; this skill is the visual specialist.
|
|
37
37
|
|
|
38
|
-
In the commands below, `$SKILL_DIR` is this skill's own directory (the folder containing this SKILL.md). The bundled
|
|
38
|
+
In the commands below, `$SKILL_DIR` is this skill's own directory (the folder containing this SKILL.md). The bundled Node evidence CLI lives at `scripts/visual-qa.mjs` inside it; the TypeScript source in `scripts/cli.ts` is for development.
|
|
39
39
|
|
|
40
40
|
## Step 1 - Detect the surface
|
|
41
41
|
|
|
@@ -66,11 +66,11 @@ Every gate runs on captures produced AFTER the last edit to the rendered source.
|
|
|
66
66
|
### Web
|
|
67
67
|
|
|
68
68
|
1. Capture a REFERENCE image: the user's mock/target, generated page snapshot, Figma export, source-site capture, or known-good baseline. Save as PNG. If the user provided overview text or annotations, save them next to the image and treat them as part of the reference packet.
|
|
69
|
-
2. Capture the ACTUAL rendered screenshot at the same viewport size. In Codex, when `browser:control-in-app-browser` is available and the page does not need an authenticated user browser session, use that Browser plugin first for navigation, page state inspection, and screenshots. If it is unavailable or lacks the needed capture action, use the project's configured browser tooling (the playwright, agent-browser, or dev-browser skill). Save as PNG. If none is configured or available, install [agent-browser](https://github.com/vercel-labs/agent-browser) (`
|
|
69
|
+
2. Capture the ACTUAL rendered screenshot at the same viewport size. In Codex, when `browser:control-in-app-browser` is available and the page does not need an authenticated user browser session, use that Browser plugin first for navigation, page state inspection, and screenshots. If it is unavailable or lacks the needed capture action, use the project's configured browser tooling (the playwright, agent-browser, or dev-browser skill). Save as PNG. If none is configured or available, install [agent-browser](https://github.com/vercel-labs/agent-browser) (`npm install -g agent-browser && agent-browser install`) and capture with it — see `$SKILL_DIR/references/agent-browser-setup.md` for the full setup, including how to shoot a fixed-viewport screenshot.
|
|
70
70
|
3. Run the diff and keep the JSON:
|
|
71
71
|
|
|
72
72
|
```
|
|
73
|
-
|
|
73
|
+
node "$SKILL_DIR/scripts/visual-qa.mjs" image-diff <reference.png> <actual.png>
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
Key fields: `dimensionsMatch`, `diffRatio` (0..1), `similarityScore` (0..100), `alphaChannelIntact`, `hotspots[]` (grid regions ranked by `diffRatio`).
|
|
@@ -104,7 +104,7 @@ metadata with cleanup receipt.
|
|
|
104
104
|
3. Run the check with the REAL terminal width and keep the JSON:
|
|
105
105
|
|
|
106
106
|
```
|
|
107
|
-
|
|
107
|
+
node "$SKILL_DIR/scripts/visual-qa.mjs" tui-check capture.txt --cols <N>
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
Key fields: `maxWidth`, `overflowLines[]`, `borderMisaligned`, `wideCharColumns[]`, `hasAnsi`.
|
|
@@ -267,7 +267,7 @@ Run this step IN ADDITION to Steps 1-4 when the original user task has a concret
|
|
|
267
267
|
1. Pixel-perfect design-compare subagent (visual oracle). Dispatch a focused, read-only design-compare reviewer (recommend `gpt-5.5` with medium reasoning). It must crop/zoom BOTH the reference (target / Figma export / source-site screenshot / generated page snapshot) and the ACTUAL screenshot into matching regions and read them **pixel-by-pixel** - header, nav, each card, spacing, type ramp, color tokens - not at a glance. It must also compare the overview text or annotations against the rendered content and DOM text. Anchor every claim with the bundled tool:
|
|
268
268
|
|
|
269
269
|
```
|
|
270
|
-
|
|
270
|
+
node "$SKILL_DIR/scripts/visual-qa.mjs" image-diff <reference.png> <actual.png>
|
|
271
271
|
```
|
|
272
272
|
|
|
273
273
|
It judges whether layout geometry, spacing, design tokens (color, type, radius, shadow), and the design itself are identical to the target, region by region. Anything off by more than rounding is a finding.
|
|
@@ -11,11 +11,12 @@ Repo: https://github.com/vercel-labs/agent-browser
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
|
|
14
|
+
npm install -g agent-browser && agent-browser install
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`
|
|
18
|
-
managed browser it drives.
|
|
17
|
+
`npm install -g agent-browser` installs the CLI globally; `agent-browser install` downloads the
|
|
18
|
+
managed browser it drives. If Bun is already available, `bun add -g agent-browser && agent-browser install`
|
|
19
|
+
is equivalent, but npm is the default because LazyCodex/Codex installations do not require Bun.
|
|
19
20
|
|
|
20
21
|
Confirm it is ready and discover the current flags:
|
|
21
22
|
|
|
@@ -36,7 +37,7 @@ agent-browser screenshot actual.png # add --full for full-page, --screensho
|
|
|
36
37
|
Then feed the PNG into the diff exactly as in Step 2:
|
|
37
38
|
|
|
38
39
|
```
|
|
39
|
-
|
|
40
|
+
node "$SKILL_DIR/scripts/visual-qa.mjs" image-diff <reference.png> actual.png
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
Match the viewport numbers to whatever the reference/mock was captured at; mismatched
|