oh-my-opencode 4.16.3 → 4.17.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/command/omomomo.md +1 -1
- package/.agents/command/publish.md +31 -58
- package/.agents/skills/hyperplan/SKILL.md +3 -3
- package/.agents/skills/omomomo/SKILL.md +1 -1
- package/.agents/skills/pre-publish-review/SKILL.md +1 -1
- package/.agents/skills/publish/SKILL.md +32 -59
- package/.opencode/command/omomomo.md +1 -1
- package/.opencode/command/publish.md +31 -58
- package/.opencode/skills/hyperplan/SKILL.md +3 -3
- package/.opencode/skills/pre-publish-review/SKILL.md +4 -1
- package/README.ja.md +4 -4
- package/README.ko.md +4 -4
- package/README.md +2 -2
- package/README.ru.md +4 -4
- package/README.zh-cn.md +4 -4
- package/dist/cli/index.js +195 -76
- package/dist/cli-node/index.js +195 -76
- package/dist/features/team-mode/test-support/async-test-helpers.d.ts +2 -2
- package/dist/index.js +310 -299
- package/dist/plugin/chat-message/types.d.ts +4 -0
- package/dist/plugin/command-execute-before.d.ts +1 -0
- package/dist/plugin/stop-continuation.d.ts +17 -0
- package/dist/skills/remove-ai-slops/SKILL.md +2 -2
- package/dist/skills/review-work/SKILL.md +10 -2
- package/dist/skills/start-work/SKILL.md +6 -3
- package/dist/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
- package/dist/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/dist/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- 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-unclear.md +4 -4
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/dist/skills/visual-qa/SKILL.md +9 -5
- package/dist/tui.js +79 -32
- package/package.json +15 -14
- package/packages/git-bash-mcp/package.json +27 -0
- package/packages/lsp-daemon/dist/cli.js +7 -13
- package/packages/lsp-daemon/dist/daemon-client.js +3 -5
- package/packages/lsp-daemon/dist/index.js +12 -18
- package/packages/lsp-daemon/dist/request-routing.js +6 -8
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +3 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1080 -1017
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/AGENTS.md +2 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/cli.js +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/codex-hook.js +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/src/codex-hook.ts +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/cli.test.ts +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/codex-hook.test.ts +67 -2
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +14 -14
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +0 -13
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +2 -2
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +3 -3
- 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/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +1 -79
- package/packages/omo-codex/plugin/components/teammode/AGENTS.md +2 -2
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +33 -16
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +2 -1
- package/packages/omo-codex/plugin/components/teammode/test/v2-spawn-schema.test.ts +69 -0
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +6 -6
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +5 -5
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +26 -0
- package/packages/omo-codex/plugin/components/ultrawork/agents/{lazycodex-executor.toml → lazycodex-worker-low.toml} +6 -4
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +26 -0
- package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +76 -37
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +76 -37
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -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-unclear.md +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +25 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -68
- package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +3 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +76 -37
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +6 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +13 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +405 -25
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +16 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +12 -9
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +148 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +209 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +25 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +14 -15
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +30 -28
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +6 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +14 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +10 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +27 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +16 -9
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +138 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +208 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +12 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +4 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +8 -35
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +24 -13
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +5 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts +43 -8
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +55 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +228 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +193 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +18 -0
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +17 -0
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +2 -2
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/model-catalog.json +16 -7
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +16 -7
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +8 -2
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +18 -4
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -3
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +33 -16
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +2 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +76 -37
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +14 -15
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +30 -28
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -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-unclear.md +4 -4
- package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +11 -7
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +76 -16
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +23 -2
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +4 -4
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +4 -4
- package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +2 -2
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +4 -101
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +14 -14
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +11 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +5 -3
- package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +25 -0
- package/packages/omo-codex/plugin/test/ulw-plan-scope-contract.test.mjs +24 -0
- package/packages/omo-codex/plugin/test/ulw-plan-skill-contract.test.mjs +9 -40
- package/packages/omo-codex/plugin/test/ulw-research-skill-contract.test.mjs +4 -277
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +5 -5
- package/packages/omo-codex/scripts/install-config.test.mjs +22 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +98 -30
- package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +2 -2
- package/packages/shared-skills/skills/review-work/SKILL.md +10 -2
- package/packages/shared-skills/skills/start-work/SKILL.md +6 -3
- package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- 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-unclear.md +4 -4
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/shared-skills/skills/visual-qa/SKILL.md +9 -5
- package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
- package/packages/omo-codex/plugin/components/ulw-loop/test/skill-contract.test.ts +0 -70
- package/packages/omo-codex/plugin/test/ulw-research-epistemic-contract.test.mjs +0 -98
- package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md
CHANGED
|
@@ -23,17 +23,15 @@ Run each criterion's real-surface proof yourself through the channel that faithf
|
|
|
23
23
|
3. **Browser use** — in Codex, use `browser:control-in-app-browser` first when available and the scenario does not need an authenticated or persistent user browser profile. Otherwise use Chrome to drive the REAL page; if unavailable, use agent-browser. Capture action log + screenshot path. Never downgrade a browser-facing criterion.
|
|
24
24
|
4. **Computer use** — for desktop/GUI apps, drive the running app via OS automation (computer-use, AppleScript, xdotool, etc.); capture action log + screenshot.
|
|
25
25
|
|
|
26
|
-
For TUI visual QA
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`--from-file` replays a raw stream) and record `terminal.png`, `terminal.txt`, and
|
|
31
|
-
`metadata.json`. Mandatory when a PR or review must inspect the terminal screen.
|
|
26
|
+
For TUI visual QA (mandatory when a PR or review must inspect the terminal screen),
|
|
27
|
+
run `node script/qa/web-terminal-visual-qa.mjs --command "<cmd>" --input "{Enter}"
|
|
28
|
+
--evidence-dir <dir>` (live pty + xterm.js in Chrome; `--from-file` replays a raw
|
|
29
|
+
stream) and record `terminal.png`, `terminal.txt`, and `metadata.json`.
|
|
32
30
|
|
|
33
31
|
Auxiliary surfaces (CLI stdout / DB state diff / parsed config dump) are first-class evidence for CLI- or data-shaped criteria; use a channel scenario when the behavior is user-facing. `--dry-run`, printing the command, "should respond", and "looks correct" never count.
|
|
34
32
|
|
|
35
33
|
## Delegation model (ATLAS-STYLE — YOU CONDUCT, WORKERS PLAY)
|
|
36
|
-
You read, search, plan, integrate, and QA. You DELEGATE every code edit, test write, bug fix, and QA execution to a right-sized `
|
|
34
|
+
You read, search, plan, integrate, and QA. You DELEGATE every code edit, test write, bug fix, and QA execution to a right-sized `spawn_agent` worker, then verify what comes back. Fan out independent tasks in PARALLEL in one response; serialize only on a NAMED dependency (one task consumes another's output or edits the same file). Tool names here are MultiAgentV2 (gpt-5.6 sol/terra); on v1 models (gpt-5.5, gpt-5.6-luna) use the `multi_agent_v1.*` equivalents in the skill's Codex Tool Mapping.
|
|
37
35
|
|
|
38
36
|
Size each worker to the task. Put the intended role, rigor level, and specialty inside the worker `message`.
|
|
39
37
|
|
|
@@ -44,21 +42,24 @@ Size each worker to the task. Put the intended role, rigor level, and specialty
|
|
|
44
42
|
| Deep debugging / race / perf / subtle cross-module reasoning | `TASK: act as a deep debugging worker. ...` |
|
|
45
43
|
| QA execution (drive a channel, capture evidence) | `TASK: act as a QA execution worker. ...` |
|
|
46
44
|
| Read-only codebase search | `TASK: act as an explorer. ...` |
|
|
45
|
+
| Implementation — pick the tier by change SIZE: LOW small (one-file fix, boilerplate) / MEDIUM mid-sized (standard feature, a few files) / HIGH large (new module, cross-module, concurrency/security/migration, or a big complex problem with one clear goal) | `TASK: act as a <low|medium|high>-difficulty implementation worker. ...` + `agent_type: "lazycodex-worker-<low|medium|high>"` when exposed |
|
|
47
46
|
| External library / docs research | `TASK: act as a librarian. ...` |
|
|
48
47
|
| Final verification audit | `TASK: act as a rigorous final verification reviewer. ...` |
|
|
49
48
|
|
|
50
49
|
For reviewer work, use a self-contained reviewer assignment, tight scope, and explicit verification in `message`. Never spawn a context-only child for review.
|
|
51
50
|
|
|
51
|
+
Difficulty is orthogonal to LIGHT/HEAVY rigor. Tier roles bind via `agent_type` (v1); the deployed v2 spawn schema omits it — state the tier inside `message` there.
|
|
52
|
+
|
|
52
53
|
Every worker message MUST carry: goal + exact files in scope; the PIN + failing-first proof before production code; constraints + project rules; verification commands; the ONE Manual-QA channel and exact artifact; for git-tracked edits, require `git-master` plus repo and touched-path commit history before commit. Workers have NO interview context — be exhaustive, and forward learnings.
|
|
53
54
|
|
|
54
55
|
Codex subagent reliability:
|
|
55
|
-
- Start every `
|
|
56
|
-
- Use `fork_context: false` unless full history is truly required; paste only the context the child needs. Full-history forks can make the child continue old parent context instead of the delegated task.
|
|
57
|
-
- Plan and reviewer agents may run for a long time; spawn them in the background
|
|
56
|
+
- Start every `spawn_agent` message with `TASK: <imperative assignment>`, then name `DELIVERABLE`, `SCOPE`, and `VERIFY`. State that it is an executable assignment, not a context handoff.
|
|
57
|
+
- Use `fork_turns: "none"` (v1: `fork_context: false`) unless full history is truly required; paste only the context the child needs. Full-history forks can make the child continue old parent context instead of the delegated task.
|
|
58
|
+
- Plan and reviewer agents may run for a long time; spawn them in the background and keep doing independent root work. Between `wait_agent` calls, back off — double the timeout up to ~5 minutes — instead of spinning short cycles.
|
|
58
59
|
- For work likely to exceed one wait cycle, require the child to send `WORKING: <task> - <current phase>` before long reading, testing, or review passes, and `BLOCKED: <reason>` only when it cannot progress.
|
|
59
60
|
- While any child is active, keep the parent visibly alive with active subagent count, agent names, latest `WORKING:` phase, and whether the parent is waiting for mailbox updates.
|
|
60
|
-
- Track spawned agent names locally. Use `
|
|
61
|
-
- Fallback only when the child is completed without the deliverable, ack-only after
|
|
61
|
+
- Track spawned agent names locally. Use `wait_agent` for mailbox signals, not proof of completion. A timeout only means no new mailbox update arrived. Treat a running child as alive.
|
|
62
|
+
- Fallback only when the child is completed without the deliverable, ack-only after `followup_task`, explicitly `BLOCKED:`, or no longer running. Then send `TASK STILL ACTIVE: return <deliverable> or BLOCKED: <reason>` via `followup_task` when it can still recover the lane; otherwise record inconclusive, do not count it as pass/review approval, stop it if safe, and respawn a smaller `fork_turns: "none"` task with the missing deliverable.
|
|
62
63
|
|
|
63
64
|
## Artifacts
|
|
64
65
|
- `.omo/ulw-loop/brief.md`: original brief and durable constraints.
|
|
@@ -126,12 +127,12 @@ Write state through the CLI path. Do not hand-edit state files.
|
|
|
126
127
|
Gather context BEFORE planning with parallel `explorer` / `librarian` workers plus your own read-only tools.
|
|
127
128
|
First survey available skills: read every loosely-relevant skill's description, deliberately choose which this work uses, and prefer applying genuinely-relevant skills over working raw.
|
|
128
129
|
Then run tier triage per goal — rigor (LIGHT/HEAVY below) and shape (`delivery` default, or `research` when the deliverable is a cited answer, not an artifact) — and record both in an `annotate_ledger` steering entry. Default is LIGHT — a narrow change inside existing layers. Take HEAVY only on a fact you can point to: a new module / abstraction / domain model; auth, security, or session; an external integration; a DB schema or migration; concurrency, transaction boundaries, or cache invalidation; a cross-domain refactor; or the user signaled care or demanded review. When unsure, take HEAVY; upgrade the moment a HEAVY fact surfaces, never downgrade mid-run.
|
|
129
|
-
|
|
130
|
+
Planning depends on unresolved design uncertainty, not the rigor tier: after discovery, spawn the `plan` agent only when unclear boundaries, competing decompositions, or uncertain dependency ordering remain; otherwise plan directly, including for HEAVY goals with a known procedure. HEAVY goals carry 3+ successCriteria covering happy path, edge, regression, and adversarial risk. LIGHT goals carry 1-2 successCriteria (happy path + the riskiest edge) with one real-surface proof of the deliverable.
|
|
130
131
|
Research-shape goals change the cycle: BEFORE each investigation, read this goal's prior ledger findings and open hypotheses, then extend them — never re-investigate an answered question (the ledger is your research notebook). Record findings via `annotate_ledger` with their source (`file:line`, command output, doc URL) as `--evidence`. Track hypotheses as `HYPOTHESIS[id]: <claim> | status: open`, flipped to `confirmed`/`refuted` only on an observed source. A research criterion passes on a cited answer — skip QA-channel, cleanup, and commit, but keep source-observability (never "looks correct"). Keep hypotheses inside the user's stated question; a scope-widening one is an `add_subgoal` proposal you surface, never silent creep. For a `research`-shape goal you MAY load `ulw-research` without hesitation — otherwise explicit-request-only, a research-shape goal IS that explicit demand. Research-only: never for a `delivery` goal. It composes with the librarian routing above — `ulw-research` for saturation (many parallel sources, recursive expansion), a single `librarian` for one lookup.
|
|
131
132
|
For each criterion, define upfront: `id`, exact `scenario` (tool + inputs + binary pass/fail), `expectedEvidence` artifact path, adversarial classes, stop condition, and Manual-QA channel. Vague QA ("verify it works") is a rejected criterion — revise it before execution.
|
|
132
133
|
For optimization work, capture baseline speed before changes plus behavior/regression proof. Every attempt records speed, behavior/regression, and the keep/revert/iterate decision.
|
|
133
134
|
A criterion's adversarial classes are the ultraqa classes a fact about the change triggers: malformed input, prompt injection, cancel/resume, stale state, dirty worktree, hung or long commands, flaky tests, misleading success output, repeated interruptions. Record untriggered classes as not-applicable in one line.
|
|
134
|
-
Use channel-table evidence verbs
|
|
135
|
+
Use channel-table evidence verbs — not vibes.
|
|
135
136
|
|
|
136
137
|
**Plan for maximum parallelism (HEAVY goals).** Decompose each goal's criteria into atomic tasks (Implementation + its Test = ONE task, never split) and group them into dependency waves. Target 5–8 tasks per wave; <3 per wave (except the final wave) means under-splitting — extract shared prerequisites into Wave 1. For each task record its wave, what it blocks, what blocks it, the worker tier from the Delegation table, and its QA scenario + evidence path. Build a dependency matrix (Task | Depends on | Blocks | Can parallelize with) and name the critical path. Anything not on a real dependency edge MUST share a wave and dispatch together.
|
|
137
138
|
Revise any criterion that lacks observable `expectedEvidence` or a named channel before execution.
|
|
@@ -159,14 +160,14 @@ Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures a
|
|
|
159
160
|
### Per-Criterion Cycle
|
|
160
161
|
1. PLAN: read `criterion.scenario`, `criterion.expectedEvidence`, prior ledger entries, and safety bounds. Identify which tasks in the current wave are independent.
|
|
161
162
|
2. Register atomic todos via `update_plan` — one ultra-granular step per action, `path: <action> for <criterion> - verify by <check>`. Call `update_plan` on every transition (start → `in_progress`, finish → `completed`); exactly one `in_progress`, mark completed immediately, never batch, never let the rendered plan lag behind reality.
|
|
162
|
-
3. DELEGATE-IN-PARALLEL: dispatch every independent task in the wave at once via right-sized `
|
|
163
|
+
3. DELEGATE-IN-PARALLEL: dispatch every independent task in the wave at once via right-sized `spawn_agent` workers (Delegation table). Each worker captures evidence failing-first: when the task touches EXISTING behavior, PIN it FIRST — a characterization test that asserts the current observable behavior and PASSES on the unchanged code, as rigorous as the new-behavior scenario (exact inputs, exact observable, exact assertion). Then RED through the cheapest faithful channel — a unit test where a seam exists, an integration/e2e test where the behavior lives in wiring, or the criterion's scenario captured failing when no test seam exists — failing for the RIGHT reason (no syntax/import error). A test that mirrors its implementation (mock-call assertions, pinned constants, cannot fail under plausible regression) is not evidence; use the scenario as the failing proof instead. **When the target is PROSE (a prompt, `SKILL.md`, rule, or markdown/instruction file), the "observable behavior" is NOT the wording** — never pin sentences, phrase presence/absence, or word/char counts. PIN only a value a MACHINE consumes (a parsed frontmatter field, a sentinel token a hook greps, the doc's JSON sample run through its real validator), or guard two shipped copies with one `toBe` equality; a pure-prose change with no machine consumer has NO seam, so ship it on review + Manual-QA-by-read with NO automated test (a text grep there is pretend-coverage, not a RED proof). Then the SMALLEST GREEN change; before GREEN work that depends on external review, PR, issue, or branch state, refresh current branch/PR/issue state, preserve existing ordering/policy, and separate compatibility detection from policy changes unless the goal explicitly asks to change policy. A GREEN far larger than the criterion implies means the proof was too coarse — instruct a split. Serialize only on a NAMED dependency.
|
|
163
164
|
4. INTEGRATE + CRITICAL SELF-QA + GIT CHECKPOINT (EVERY WORKER RETURN): do NOT trust the worker's report. Read the diff yourself, re-run its tests, and run LSP diagnostics on the changed files. Treat "done" as a claim to disprove. If the diff drifts, the test is hollow, or evidence is missing, RESPAWN the worker with the specific failure context. Once the work unit is verified, use `git-master` before staging: inspect recent repository commits and touched-path history to infer commit language, Conventional Commit scope, message shape, and unit size. Stage only that unit's files and commit in the observed style; do not carry verified work forward into a later omnibus commit. If no git-tracked files changed or committing is unsafe, record the no-commit reason as evidence. Forward every finding/learning to subsequent workers.
|
|
164
|
-
5. EXECUTE-AS-SCENARIO: ACTUALLY run the Manual-QA scenario the criterion named (channel table above). Run it yourself for the orchestrator check; for heavier flows dispatch a dedicated QA worker (`worker
|
|
165
|
+
5. EXECUTE-AS-SCENARIO: ACTUALLY run the Manual-QA scenario the criterion named (channel table above). Run it yourself for the orchestrator check; for heavier flows dispatch a dedicated QA execution worker (`lazycodex-worker-medium` by default; `lazycodex-worker-high` when the QA flow itself is hard) whose ONLY job is to drive the channel and write the artifact to the named evidence path. If the scenario FAILS, respawn the implementing worker with the captured failure — do not hand-patch around it.
|
|
165
166
|
6. CAPTURE: collect the observable artifact path: transcript, stdout, screenshot, assertion, status+body, diff, or parsed dump. No artifact written at the evidence path — not done; record BLOCKED and respawn QA.
|
|
166
|
-
7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND `
|
|
167
|
-
8. RECORD
|
|
168
|
-
- PASS: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> | <cleanup receipt>" --json`
|
|
169
|
-
- FAIL: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> | <cleanup receipt>" --notes "<diagnosis>" --json`
|
|
167
|
+
7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND close every finished worker (v1 `close_agent`; on V2 finished workers end on their own — `interrupt_agent` any still running). Register each teardown as its own todo the moment the QA spawns the resource (scripts, tmux assets, browsers / agent-browser sessions, PIDs, ports) so none is forgotten. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD; interrupt_agent w-3`. Missing receipt → record BLOCKED, not PASS.
|
|
168
|
+
8. RECORD one result immediately from the artifact you just wrote — never from memory or a later turn — stamping the capture commit `$(git rev-parse --short HEAD)` into the evidence:
|
|
169
|
+
- PASS: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> @<short-sha> | <cleanup receipt>" --json`
|
|
170
|
+
- FAIL: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> @<short-sha> | <cleanup receipt>" --notes "<diagnosis>" --json`
|
|
170
171
|
- BLOCKED: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status blocked --evidence "<observable>" --notes "<safety/blocker/leftover-state>" --json`
|
|
171
172
|
9. If actual does not match expected, diagnose, respawn the right-sized worker with the failure context to fix minimally, and rerun the SAME criterion (including a fresh cleanup).
|
|
172
173
|
10. After 3 same-criterion failures, exit the goal with diagnosis.
|
|
@@ -183,9 +184,10 @@ Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures a
|
|
|
183
184
|
## Final Quality Gate
|
|
184
185
|
Trigger only for the final aggregate goal after every criterion in every goal is `pass`.
|
|
185
186
|
1. Run targeted verification for changed behavior.
|
|
186
|
-
2.
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
2. FREEZE first — no more edits or rebases. At the frozen `git rev-parse HEAD`, re-run Manual-QA for any PASS criterion whose stamped commit is not HEAD, so every criterion is proven at HEAD; each artifact exists and is non-empty.
|
|
188
|
+
3a. Spawn lazycodex-code-reviewer and lazycodex-qa-executor in parallel (`fork_context: false` on v1; `fork_turns: "none"` on v2) with brief, goals, desired outcome, diff, evidence; wait for BOTH and confirm their report artifacts exist on disk.
|
|
189
|
+
3b. Only then spawn lazycodex-gate-reviewer with those artifact paths.
|
|
190
|
+
4. Treat timeout, missing deliverable, ack-only, `BLOCKED:`, or inconclusive review as a blocker. Any fix restarts the freeze at the new HEAD: re-run ONLY the proofs it invalidated and stamp the fresh output — never regenerate all evidence or relabel stale output to HEAD — re-review the delta at most TWICE; then record-review-blockers (step 5) and surface to the user.
|
|
189
191
|
5. If review remains blocked, run `omo ulw-loop record-review-blockers --goal-id <id> --title "<...>" --objective "<...>" --evidence "<review findings>" --codex-goal-json <snapshot> --json`.
|
|
190
192
|
6. If clean, checkpoint final completion:
|
|
191
193
|
```sh
|
|
@@ -201,7 +203,7 @@ omo ulw-loop checkpoint --goal-id <id> --status complete --evidence "<e2e eviden
|
|
|
201
203
|
"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
204
|
}
|
|
203
205
|
```
|
|
204
|
-
Artifacts must be non-empty; counts alone fail. LIGHT without adversarial class records `"adversarialClassesCovered": ["none-applicable: <reason>"]
|
|
206
|
+
Artifacts must be non-empty; counts alone fail. LIGHT without adversarial class records `"adversarialClassesCovered": ["none-applicable: <reason>"]`; untriggered adversarialCases may use verdict `not_applicable` + `reason`; WATCH passes, notes surfaced.
|
|
205
207
|
|
|
206
208
|
## Dynamic Steering
|
|
207
209
|
Use steering only for structured evidence-backed mutation. Reject natural-language steering requests.
|
|
@@ -222,18 +224,18 @@ Structured prompt directives accepted: `OMO_ULW_LOOP_STEER: { ... }`, `omo.ulw-l
|
|
|
222
224
|
## Constraints
|
|
223
225
|
1. NEVER call `update_goal` mid-aggregate; only on final story after the quality gate passes.
|
|
224
226
|
2. NEVER call `create_goal` when `get_goal` shows a different active goal.
|
|
225
|
-
3. NEVER mark
|
|
227
|
+
3. Evidence is bound to the commit it was captured at; a later fix, rebase, or merge invalidates it — re-run the QA at the current HEAD and re-record. NEVER mark PASS from memory, and NEVER relabel, pin, refresh, or regenerate prior output to a moved HEAD.
|
|
226
228
|
4. NEVER bypass the criteria gate: non-final aggregate completion requires all essential criteria; final aggregate completion requires all criteria across the whole plan.
|
|
227
229
|
5. Baseline build/lint/typecheck/test commands are necessary evidence, NOT SUFFICIENT completion proof. Criteria coverage with observable evidence is the gate.
|
|
228
230
|
6. Treat `.omo/ulw-loop/ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.
|
|
229
231
|
7. Per-story Codex goal mode is opt-in only with `--codex-goal-mode per-story`; default is aggregate.
|
|
230
232
|
8. Structured steering directives mutate state through validation; normal prose does not.
|
|
231
|
-
9. Evidence MUST be observable from the real surface
|
|
233
|
+
9. Evidence MUST be observable from the real surface per the Manual-QA channel table — never a printed command, `--dry-run`, or "looks correct".
|
|
232
234
|
10. Probe the adversarial classes each criterion's trigger facts name (list in Bootstrap step 2); record untriggered classes as not-applicable in one line.
|
|
233
235
|
11. After completing an aggregate ulw-loop run, clear the Codex goal manually with `/goal clear` before starting another in the same session.
|
|
234
236
|
12. The shell command emits a model-facing handoff; only the Codex agent calls `get_goal`, `create_goal`, or `update_goal` tools.
|
|
235
|
-
13. NEVER record
|
|
236
|
-
14. DELEGATE all code edits, test writes, fixes, and QA execution to right-sized `
|
|
237
|
+
13. NEVER record PASS while any QA-spawned process, `tmux` session, browser context, bound port, container, temp path, or open worker is still alive; the evidence MUST carry the cleanup receipt. Leftover state = BLOCKED.
|
|
238
|
+
14. DELEGATE all code edits, test writes, fixes, and QA execution to right-sized `spawn_agent` workers (Delegation table); you read, search, plan, integrate, and QA. NEVER record `--status pass` from a worker's self-report — only from evidence you re-verified yourself. Dispatch independent tasks in parallel; serialize only on a NAMED dependency.
|
|
237
239
|
15. Every verified work unit that touched git-tracked files must leave either an atomic `git-master`-style commit hash or explicit no-commit blocker evidence before the next unit starts.
|
|
238
240
|
|
|
239
241
|
## Stop Rules
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ulwLoopAttemptEvidenceDir } from "./paths.js";
|
|
1
2
|
import { existsSync, statSync } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { resolve } from "node:path";
|
|
@@ -136,12 +137,13 @@ function buildLedger(
|
|
|
136
137
|
codexGoal: unknown,
|
|
137
138
|
aggregateCompletion: UlwLoopAggregateCompletion | undefined,
|
|
138
139
|
): UlwLoopLedgerEntry {
|
|
140
|
+
const watch = qualityGate?.codeReview.codeQualityStatus === "WATCH";
|
|
139
141
|
const entry: UlwLoopLedgerEntry = {
|
|
140
142
|
at: now,
|
|
141
143
|
kind: ledgerKind(args.status, goal, aggregateCompletion),
|
|
142
144
|
goalId: goal.id,
|
|
143
145
|
status: goal.status,
|
|
144
|
-
evidence: args.evidence,
|
|
146
|
+
evidence: watch ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence,
|
|
145
147
|
};
|
|
146
148
|
if (codexGoal !== undefined) entry.codexGoal = codexGoal;
|
|
147
149
|
if (qualityGate !== undefined) entry.qualityGate = qualityGate;
|
|
@@ -221,6 +223,9 @@ export async function checkpointUlwLoop(
|
|
|
221
223
|
qualityGate = validateQualityGate(await readJsonInput(args.qualityGateJson, repoRoot), {
|
|
222
224
|
repoRoot,
|
|
223
225
|
fs: QUALITY_GATE_FS,
|
|
226
|
+
...(plan.evidenceLayoutVersion === 2
|
|
227
|
+
? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) }
|
|
228
|
+
: {}),
|
|
224
229
|
});
|
|
225
230
|
goal.status = "complete";
|
|
226
231
|
goal.completedAt = now;
|
|
@@ -14,7 +14,7 @@ import { parseSteeringProposal, printSteerResult } from "./cli-steering.js";
|
|
|
14
14
|
import { buildCodexGoalInstruction } from "./codex-goal-instruction.js";
|
|
15
15
|
import { recordEvidence } from "./evidence.js";
|
|
16
16
|
import { isEssentialCriterion } from "./goal-status.js";
|
|
17
|
-
import type
|
|
17
|
+
import { type UlwLoopScope, ulwLoopAttemptEvidenceDir } from "./paths.js";
|
|
18
18
|
import { addUlwLoopGoal, createUlwLoopPlan, startNextUlwLoop, summarizeUlwLoopPlan } from "./plan-crud.js";
|
|
19
19
|
import { readUlwLoopPlan } from "./plan-io.js";
|
|
20
20
|
import { recordFinalReviewBlockers } from "./review-blockers.js";
|
|
@@ -62,8 +62,19 @@ export async function createGoals(
|
|
|
62
62
|
|
|
63
63
|
export async function status(repoRoot: string, json: boolean, scope?: UlwLoopScope): Promise<number> {
|
|
64
64
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
65
|
-
if (json)
|
|
66
|
-
|
|
65
|
+
if (json) {
|
|
66
|
+
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
67
|
+
const currentAttemptDir =
|
|
68
|
+
plan.evidenceLayoutVersion === 2 && active
|
|
69
|
+
? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope)
|
|
70
|
+
: undefined;
|
|
71
|
+
printJson({
|
|
72
|
+
ok: true,
|
|
73
|
+
plan,
|
|
74
|
+
summary: summarizeUlwLoopPlan(plan),
|
|
75
|
+
...(currentAttemptDir === undefined ? {} : { currentAttemptDir }),
|
|
76
|
+
});
|
|
77
|
+
} else printStatus(plan);
|
|
67
78
|
return 0;
|
|
68
79
|
}
|
|
69
80
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { isUlwLoopSubcommand, ulwLoopCommand } from "./cli-commands.js";
|
|
3
3
|
import { runPreToolUseGoalBudgetGuardCli, runUlwLoopHookCli } from "./codex-hook.js";
|
|
4
|
+
import { runSpawnGuardCli } from "./spawn-guard.js";
|
|
5
|
+
import { runStopResumeHookCli } from "./stop-resume-hook.js";
|
|
4
6
|
|
|
5
7
|
const TOP_LEVEL_HELP =
|
|
6
8
|
"Usage:\n omo ulw-loop <subcommand> [args]\n omo hook user-prompt-submit [--with-ultrawork] (Codex UserPromptSubmit hook)\n omo help | --help | -h (this message)\n\nRun `omo ulw-loop help` for ulw-loop subcommands.\n";
|
|
@@ -25,6 +27,14 @@ async function main(): Promise<number> {
|
|
|
25
27
|
await runPreToolUseGoalBudgetGuardCli(process.stdin, process.stdout);
|
|
26
28
|
return 0;
|
|
27
29
|
}
|
|
30
|
+
if (sub === "stop") {
|
|
31
|
+
await runStopResumeHookCli(process.stdin, process.stdout);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
if (sub === "pre-tool-use-spawn") {
|
|
35
|
+
await runSpawnGuardCli(process.stdin, process.stdout);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
28
38
|
process.stderr.write(`[omo] unknown hook subcommand: ${sub ?? "(none)"}\n`);
|
|
29
39
|
return 1;
|
|
30
40
|
}
|
|
@@ -52,6 +52,7 @@ function buildText(
|
|
|
52
52
|
"- Use the create_goal payload exactly as rendered: objective only.",
|
|
53
53
|
"- Goals are unlimited. Do not add numeric limits.",
|
|
54
54
|
...modeConstraintLines(mode, isFinal),
|
|
55
|
+
...evidenceLayoutLines(plan),
|
|
55
56
|
finalSection(plan, goal, isFinal, mode === "aggregate"),
|
|
56
57
|
...checkpointLines(plan, mode),
|
|
57
58
|
"",
|
|
@@ -103,6 +104,13 @@ function formatCriterionLine(criterion: UlwLoopSuccessCriterion): string {
|
|
|
103
104
|
return `-${remainingWork} [${criterion.id}] [${marker}] (${criterion.userModel}) ${criterion.scenario} — expect: ${criterion.expectedEvidence} — status: ${criterion.status}`;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
function evidenceLayoutLines(plan: UlwLoopPlan): string[] {
|
|
108
|
+
if (plan.evidenceLayoutVersion !== 2) return [];
|
|
109
|
+
return [
|
|
110
|
+
"- Evidence layout v2: write every artifact for the active goal (QA matrix, review reports, receipts) under the current attempt directory — read currentAttemptDir from `omo ulw-loop status --json` (.omo/evidence/ulw/<session>/<goalId>/a<attempt>). The final checkpoint rejects quality-gate artifacts outside that directory.",
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
106
114
|
function finalSection(plan: UlwLoopPlan, goal: UlwLoopItem, isFinal: boolean, aggregate: boolean): string {
|
|
107
115
|
if (!isFinal)
|
|
108
116
|
return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
|
|
@@ -113,8 +121,11 @@ function finalSection(plan: UlwLoopPlan, goal: UlwLoopItem, isFinal: boolean, ag
|
|
|
113
121
|
"Final story — run mandatory quality gate before update_goal:",
|
|
114
122
|
"- Run targeted verification for changed behavior.",
|
|
115
123
|
"- Confirm every manualQa artifact path exists and has non-zero size.",
|
|
116
|
-
"-
|
|
124
|
+
"- First spawn lazycodex-code-reviewer and lazycodex-qa-executor in parallel (fork_context: false on the v1 surface; fork_turns: \"none\" on v2). Include the original brief, goal objectives, desired user-visible outcome, diff, and evidence; wait for BOTH to return and confirm their report artifacts exist on disk (code-review report + manualQa matrix).",
|
|
125
|
+
"- Only then spawn lazycodex-gate-reviewer (same fork settings), passing those artifact paths.",
|
|
117
126
|
"- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
|
|
127
|
+
"- On a reviewer REJECT, fix only the cited blockers, rerun the affected verification/Manual-QA, and re-review the delta at most TWICE; if blockers remain, record them and surface to the user.",
|
|
128
|
+
"- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
|
|
118
129
|
"- If any reviewer is blocked/inconclusive or the quality gate is not clean, do not call update_goal. Record blocker work first:",
|
|
119
130
|
` ${blockerCommand}`,
|
|
120
131
|
aggregate
|
|
@@ -57,6 +57,7 @@ export interface UlwLoopAggregateCompletion {
|
|
|
57
57
|
|
|
58
58
|
export interface UlwLoopPlan {
|
|
59
59
|
version: 1;
|
|
60
|
+
evidenceLayoutVersion?: 2;
|
|
60
61
|
createdAt: string;
|
|
61
62
|
updatedAt: string;
|
|
62
63
|
briefPath: string;
|
|
@@ -94,7 +95,8 @@ export interface UlwLoopManualQaAdversarialCase {
|
|
|
94
95
|
readonly criterionRef: string;
|
|
95
96
|
readonly scenario: string;
|
|
96
97
|
readonly expectedBehavior: string;
|
|
97
|
-
readonly verdict: "passed";
|
|
98
|
+
readonly verdict: "passed" | "not_applicable";
|
|
99
|
+
readonly reason?: string;
|
|
98
100
|
readonly artifactRefs: readonly string[];
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -102,7 +104,7 @@ export interface UlwLoopQualityGate {
|
|
|
102
104
|
readonly codeReview: {
|
|
103
105
|
readonly by: string;
|
|
104
106
|
readonly recommendation: "APPROVE";
|
|
105
|
-
readonly codeQualityStatus: "CLEAR";
|
|
107
|
+
readonly codeQualityStatus: "CLEAR" | "WATCH";
|
|
106
108
|
readonly reportPath: string;
|
|
107
109
|
readonly evidence: string;
|
|
108
110
|
readonly blockers: readonly [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
2
2
|
import { ULW_LOOP_BRIEF, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export interface UlwLoopScope {
|
|
@@ -71,3 +71,29 @@ export function repoRelative(absolutePath: string, repoRoot: string): string {
|
|
|
71
71
|
return absolutePath.slice(backslashPrefix.length).split("\\").join("/");
|
|
72
72
|
return absolutePath.split("\\").join("/");
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
// Both the status --json emitter and the checkpoint enforcement resolve the attempt dir through
|
|
76
|
+
// this function; a second resolution path would let the gate reject its own advertised directory.
|
|
77
|
+
export function ulwLoopAttemptEvidenceDir(goalId: string, attempt: number, scope?: UlwLoopScope): string {
|
|
78
|
+
const sessionId = normalizeUlwLoopSessionId(scope?.sessionId) ?? resolveUlwLoopSessionIdFromEnv() ?? "session";
|
|
79
|
+
return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface AttemptPathApi {
|
|
83
|
+
relative(from: string, to: string): string;
|
|
84
|
+
isAbsolute(path: string): boolean;
|
|
85
|
+
readonly sep: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const PLATFORM_PATH_API: AttemptPathApi = { relative, isAbsolute, sep };
|
|
89
|
+
|
|
90
|
+
export function isWithinAttemptDir(
|
|
91
|
+
absolutePath: string,
|
|
92
|
+
attemptRoot: string,
|
|
93
|
+
pathApi: AttemptPathApi = PLATFORM_PATH_API,
|
|
94
|
+
): boolean {
|
|
95
|
+
const relativePath = pathApi.relative(attemptRoot, absolutePath);
|
|
96
|
+
if (relativePath === "") return true;
|
|
97
|
+
if (relativePath === ".." || relativePath.startsWith(`..${pathApi.sep}`)) return false;
|
|
98
|
+
return !pathApi.isAbsolute(relativePath);
|
|
99
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { literal, invalid, textField } from "./quality-gate-fields.js";
|
|
2
|
+
|
|
3
|
+
export function passedVerdict(value: unknown, field: string): "passed" {
|
|
4
|
+
if (value === "not_applicable") invalid(`${field} must not be not_applicable.`, field);
|
|
5
|
+
return literal(value, "passed", field);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function codeQualityStatusField(value: unknown, field: string): "CLEAR" | "WATCH" {
|
|
9
|
+
if (value === "CLEAR" || value === "WATCH") return value;
|
|
10
|
+
invalid(`${field} must be CLEAR or WATCH.`, field);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function adversarialVerdict(
|
|
14
|
+
row: Record<string, unknown>,
|
|
15
|
+
field: string,
|
|
16
|
+
): { verdict: "passed" | "not_applicable"; reason?: string } {
|
|
17
|
+
const value = row["verdict"];
|
|
18
|
+
if (value === "passed") return { verdict: "passed" };
|
|
19
|
+
if (value === "not_applicable") {
|
|
20
|
+
return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
|
|
21
|
+
}
|
|
22
|
+
invalid(`${field} must be passed or not_applicable with a reason.`, field);
|
|
23
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { isWithinAttemptDir } from "./paths.js";
|
|
2
3
|
import {
|
|
3
4
|
emptyBlockers,
|
|
4
5
|
invalid,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
stringArray,
|
|
9
10
|
textField,
|
|
10
11
|
} from "./quality-gate-fields.js";
|
|
12
|
+
import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
|
|
11
13
|
import type {
|
|
12
14
|
UlwLoopManualQaArtifactKind,
|
|
13
15
|
UlwLoopManualQaArtifactRef,
|
|
@@ -36,6 +38,7 @@ export interface QualityGateFs {
|
|
|
36
38
|
export interface ValidateQualityGateOptions {
|
|
37
39
|
readonly repoRoot: string;
|
|
38
40
|
readonly fs: QualityGateFs;
|
|
41
|
+
readonly currentAttemptDir?: string;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
function reviewerRoleField<T extends string>(value: unknown, expected: T, field: string): T {
|
|
@@ -70,11 +73,6 @@ function kindField(value: unknown, field: string): UlwLoopManualQaArtifactKind {
|
|
|
70
73
|
invalid(`${field} must be a supported artifact kind.`, field);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
function passedVerdict(value: unknown, field: string): "passed" {
|
|
74
|
-
if (value === "not_applicable") invalid(`${field} must not be not_applicable.`, field);
|
|
75
|
-
return literal(value, "passed", field);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
76
|
function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoopManualQaArtifactKind): boolean {
|
|
79
77
|
switch (surface) {
|
|
80
78
|
case "cli":
|
|
@@ -96,8 +94,15 @@ function checkFile(path: string, field: string, opts?: ValidateQualityGateOption
|
|
|
96
94
|
if (opts === undefined) return;
|
|
97
95
|
const absolute = resolve(opts.repoRoot, path);
|
|
98
96
|
if (!opts.fs.existsSync(absolute)) invalid(`${field} must point to an existing artifact.`, field);
|
|
99
|
-
|
|
100
|
-
if (
|
|
97
|
+
if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
|
|
98
|
+
if (opts.currentAttemptDir !== undefined) {
|
|
99
|
+
const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
|
|
100
|
+
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
101
|
+
invalid(
|
|
102
|
+
`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`,
|
|
103
|
+
field,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
function artifactMap(refs: readonly UlwLoopManualQaArtifactRef[]): Map<string, UlwLoopManualQaArtifactRef> {
|
|
@@ -160,7 +165,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
160
165
|
codeReview: {
|
|
161
166
|
by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
|
|
162
167
|
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
163
|
-
codeQualityStatus:
|
|
168
|
+
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
164
169
|
reportPath: codeReportPath,
|
|
165
170
|
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
166
171
|
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
|
|
@@ -246,12 +251,14 @@ function parseAdversarialCases(
|
|
|
246
251
|
`manualQa.adversarialCases[${index}].artifactRefs`,
|
|
247
252
|
byId,
|
|
248
253
|
);
|
|
254
|
+
const verdictInfo = adversarialVerdict(row, `manualQa.adversarialCases[${index}]`);
|
|
249
255
|
return {
|
|
250
256
|
id: textField(row["id"], `manualQa.adversarialCases[${index}].id`),
|
|
251
257
|
criterionRef: textField(row["criterionRef"], `manualQa.adversarialCases[${index}].criterionRef`),
|
|
252
258
|
scenario: textField(row["scenario"], `manualQa.adversarialCases[${index}].scenario`),
|
|
253
259
|
expectedBehavior: textField(row["expectedBehavior"], `manualQa.adversarialCases[${index}].expectedBehavior`),
|
|
254
|
-
verdict:
|
|
260
|
+
verdict: verdictInfo.verdict,
|
|
261
|
+
...(verdictInfo.reason === undefined ? {} : { reason: verdictInfo.reason }),
|
|
255
262
|
artifactRefs: artifacts.map((artifact) => artifact.id),
|
|
256
263
|
};
|
|
257
264
|
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { PreToolUsePayload } from "./codex-hook.js";
|
|
5
|
+
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
6
|
+
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
7
|
+
import { ulwLoopAttemptEvidenceDir, ulwLoopDir } from "./paths.js";
|
|
8
|
+
import type { UlwLoopPlan } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
|
|
11
|
+
// hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
|
|
12
|
+
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
13
|
+
const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
14
|
+
const DEFAULT_FANOUT_LIMIT = 60;
|
|
15
|
+
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
|
|
16
|
+
|
|
17
|
+
export function applySpawnGuards(payload: PreToolUsePayload): string {
|
|
18
|
+
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
|
|
19
|
+
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
20
|
+
const plan = readPlan(join(stateDir, "goals.json"));
|
|
21
|
+
if (plan === null) return "";
|
|
22
|
+
const fanOutDenial = consumeFanOutBudget(stateDir);
|
|
23
|
+
if (fanOutDenial !== null) return deny(fanOutDenial);
|
|
24
|
+
const missingArtifact = missingGateArtifact(payload, plan);
|
|
25
|
+
if (missingArtifact !== null)
|
|
26
|
+
return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runSpawnGuardCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
const chunks: Buffer[] = [];
|
|
33
|
+
for await (const chunk of stdin) chunks.push(Buffer.from(chunk));
|
|
34
|
+
const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
|
|
35
|
+
if (payload === null) return;
|
|
36
|
+
const output = applySpawnGuards(payload);
|
|
37
|
+
if (output.length > 0) stdout.write(output);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof Error) return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Per-session spawn counter; depth/lineage tracking is descoped — this is a
|
|
44
|
+
// total-volume backstop against fan-out explosions, not a recursion tracker.
|
|
45
|
+
function consumeFanOutBudget(stateDir: string): string | null {
|
|
46
|
+
const counterPath = join(stateDir, "spawn-count.json");
|
|
47
|
+
const count = readCount(counterPath) + 1;
|
|
48
|
+
writeFileSync(counterPath, JSON.stringify({ count }));
|
|
49
|
+
const limit = fanOutLimit();
|
|
50
|
+
if (count <= limit) return null;
|
|
51
|
+
return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function missingGateArtifact(payload: PreToolUsePayload, plan: UlwLoopPlan): string | null {
|
|
55
|
+
if (!isGateReviewerSpawn(payload.tool_input)) return null;
|
|
56
|
+
const goal = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
57
|
+
if (goal === undefined || goal.status === "complete") return null;
|
|
58
|
+
if (!goal.successCriteria.every((criterion) => criterion.status === "pass")) return null;
|
|
59
|
+
const scope = { sessionId: payload.session_id } as const;
|
|
60
|
+
if (plan.evidenceLayoutVersion === 2) {
|
|
61
|
+
const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
|
|
62
|
+
for (const name of [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`]) {
|
|
63
|
+
const relative = `${attemptDir}/${name}`;
|
|
64
|
+
if (!isNonEmptyFile(join(payload.cwd, relative))) return relative;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const flatReport = `.omo/evidence/${goal.id}-code-review.md`;
|
|
69
|
+
if (!isNonEmptyFile(join(payload.cwd, flatReport))) return flatReport;
|
|
70
|
+
// v1 manual-QA approximation: any other non-empty evidence file counts.
|
|
71
|
+
if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
|
|
72
|
+
return `.omo/evidence/<any manual-QA artifact besides ${goal.id}-code-review.md>`;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isGateReviewerSpawn(toolInput: unknown): boolean {
|
|
77
|
+
if (typeof toolInput !== "object" || toolInput === null) return false;
|
|
78
|
+
const record = toolInput as Record<string, unknown>;
|
|
79
|
+
const agentType = record["agent_type"];
|
|
80
|
+
if (typeof agentType === "string") return agentType === "lazycodex-gate-reviewer";
|
|
81
|
+
const message = record["message"];
|
|
82
|
+
return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function deny(reason: string): string {
|
|
86
|
+
return `${JSON.stringify({
|
|
87
|
+
hookSpecificOutput: {
|
|
88
|
+
hookEventName: "PreToolUse",
|
|
89
|
+
permissionDecision: "deny",
|
|
90
|
+
permissionDecisionReason: reason,
|
|
91
|
+
additionalContext: reason,
|
|
92
|
+
},
|
|
93
|
+
})}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function fanOutLimit(): number {
|
|
97
|
+
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
98
|
+
if (raw === undefined) return DEFAULT_FANOUT_LIMIT;
|
|
99
|
+
const parsed = Number.parseInt(raw, 10);
|
|
100
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isNonEmptyFile(path: string): boolean {
|
|
104
|
+
try {
|
|
105
|
+
return existsSync(path) && statSync(path).size > 0;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof Error) return false;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasOtherEvidenceFile(evidenceDir: string, excludedName: string): boolean {
|
|
113
|
+
try {
|
|
114
|
+
return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join(evidenceDir, name)));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error instanceof Error) return false;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readCount(counterPath: string): number {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(readFileSync(counterPath, "utf8")) as Record<string, unknown>;
|
|
124
|
+
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof Error) return 0;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readPlan(goalsPath: string): UlwLoopPlan | null {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(readFileSync(goalsPath, "utf8")) as UlwLoopPlan;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof Error) return null;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|