oh-my-opencode 5.1.29 → 5.1.32

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.
Files changed (146) hide show
  1. package/README.ja.md +2 -0
  2. package/README.ko.md +2 -0
  3. package/README.md +2 -0
  4. package/README.ru.md +2 -0
  5. package/README.zh-cn.md +2 -0
  6. package/dist/cli/cli-program.d.ts +1 -20
  7. package/dist/cli/index.js +950 -51
  8. package/dist/cli/install-command-options.d.ts +20 -0
  9. package/dist/cli/service-auth/browser.d.ts +3 -0
  10. package/dist/cli/service-auth/cli-keystore.fixture.d.ts +7 -0
  11. package/dist/cli/service-auth/commands.d.ts +19 -0
  12. package/dist/cli/service-auth/device-flow.d.ts +15 -0
  13. package/dist/cli/service-auth/device-keys.d.ts +10 -0
  14. package/dist/cli/service-auth/device-name.d.ts +3 -0
  15. package/dist/cli/service-auth/keystore.d.ts +20 -0
  16. package/dist/cli/service-auth/loopback.d.ts +11 -0
  17. package/dist/cli/service-auth/protocol.d.ts +86 -0
  18. package/dist/cli/service-auth/refresh-child.fixture.d.ts +1 -0
  19. package/dist/cli/service-auth/session.d.ts +17 -0
  20. package/dist/cli/service-auth/terminal-text.d.ts +1 -0
  21. package/dist/cli/service-auth/test-support.d.ts +43 -0
  22. package/dist/cli/service-auth/tls-test-certificate.d.ts +4 -0
  23. package/dist/cli-node/index.js +950 -51
  24. package/dist/features/background-agent/task-poller.d.ts +3 -0
  25. package/dist/features/opencode-skill-loader/opencode-shell-skill-content.d.ts +8 -0
  26. package/dist/features/opencode-skill-loader/skill-content.d.ts +1 -0
  27. package/dist/features/team-mode/tools/lifecycle-inline-spec.d.ts +1 -0
  28. package/dist/hooks/keyword-detector/constants.d.ts +1 -0
  29. package/dist/hooks/non-interactive-env/non-interactive-env-hook.d.ts +5 -1
  30. package/dist/index.js +40791 -40412
  31. package/dist/shared/disabled-agents.d.ts +1 -0
  32. package/dist/shared/opencode-shell.d.ts +27 -0
  33. package/dist/skills/browser/SKILL.md +1 -1
  34. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  35. package/dist/skills/ultimate-browsing/SKILL.md +14 -16
  36. package/dist/skills/ulw-execute/SKILL.md +2 -2
  37. package/dist/skills/ulw-research/SKILL.md +17 -18
  38. package/dist/skills/ulw-research/scripts/cli-support.test.ts +5 -3
  39. package/dist/skills/ulw-research/scripts/outcome.test.ts +10 -4
  40. package/dist/skills/ulw-research/scripts/report-tools.test.ts +3 -2
  41. package/dist/skills/visual-qa/scripts/cli.test.ts +5 -3
  42. package/dist/tools/delegate-task/executor-types.d.ts +1 -0
  43. package/dist/tools/delegate-task/sync-session-creator.d.ts +1 -0
  44. package/dist/tools/delegate-task/types.d.ts +2 -0
  45. package/dist/tools/session-manager/sdk-storage.d.ts +15 -2
  46. package/dist/tools/session-manager/session-error.d.ts +7 -0
  47. package/dist/tools/session-manager/storage.d.ts +4 -2
  48. package/dist/tui.js +89 -21
  49. package/package.json +15 -14
  50. package/packages/git-bash-mcp/dist/cli.js +1 -1
  51. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +90 -0
  52. package/packages/lsp-core/src/lsp/client.ts +3 -1
  53. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +5 -2
  54. package/packages/lsp-daemon/dist/cli.js +1 -1
  55. package/packages/lsp-daemon/dist/client.js +1 -1
  56. package/packages/lsp-daemon/dist/index.js +1 -1
  57. package/packages/lsp-tools-mcp/dist/cli.js +1 -1
  58. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1 -1
  59. package/packages/lsp-tools-mcp/dist/mcp.js +1 -1
  60. package/packages/lsp-tools-mcp/dist/tools.js +1 -1
  61. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  62. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +7 -4
  63. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  64. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  65. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  66. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  67. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  68. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  69. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  70. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  71. package/packages/omo-codex/plugin/components/lcx/skills/lcx-contribute-bug-fix/SKILL.md +7 -4
  72. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  73. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +1 -1
  74. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  75. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  76. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  77. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  78. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  79. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  80. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +3 -3
  81. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  82. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  83. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +18 -15
  84. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  85. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  86. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  87. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  88. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  89. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +18 -15
  90. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  91. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  92. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +2 -2
  93. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  94. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  95. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  96. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  98. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  99. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  100. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  115. package/packages/omo-codex/plugin/package-lock.json +12 -12
  116. package/packages/omo-codex/plugin/package.json +1 -1
  117. package/packages/omo-codex/plugin/scripts/auto-update-plan.mjs +2 -0
  118. package/packages/omo-codex/plugin/scripts/auto-update.mjs +2 -0
  119. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -0
  120. package/packages/omo-codex/plugin/scripts/build-components.mjs +1 -0
  121. package/packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs +42 -10
  122. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -2
  123. package/packages/omo-codex/plugin/skills/browser/SKILL.md +1 -1
  124. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  125. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +7 -4
  126. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +3 -3
  127. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +14 -16
  128. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +18 -15
  129. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +2 -2
  130. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -2
  131. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  132. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +17 -18
  133. package/packages/omo-codex/plugin/test/materialize-shared-upstreams.test.mjs +179 -0
  134. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -2
  135. package/packages/omo-codex/plugin/test/windows-console-hide.test.mjs +150 -0
  136. package/packages/omo-codex/scripts/install-dist/install-local.mjs +11 -6
  137. package/packages/prompts-core/prompts/ultrawork/codex.md +18 -15
  138. package/packages/shared-skills/skills/browser/SKILL.md +1 -1
  139. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  140. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +14 -16
  141. package/packages/shared-skills/skills/ulw-execute/SKILL.md +2 -2
  142. package/packages/shared-skills/skills/ulw-research/SKILL.md +17 -18
  143. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +5 -3
  144. package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +10 -4
  145. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +3 -2
  146. package/packages/shared-skills/skills/visual-qa/scripts/cli.test.ts +5 -3
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: ultimate-browsing
3
- description: "Renders, drives, and screenshots web pages: JS-rendered sources, clicks and forms, persistent logins, WAF-blocked hosts (platform-native readers, stealth Chrome), and the browsing lane of a research run, with screenshots as provenance. Not for plain search or unblocked static fetch."
3
+ description: "Reaches web pages the user's browser cannot open: WAF-blocked and bot-scored hosts, platform-native readers (Chinese and social platforms), archive snapshots with provenance, and a browser you own for stealth or throwaway profiles. Not for plain search, an unblocked static fetch, or a page the user's browser can open."
4
4
  ---
5
5
 
6
6
  # Ultimate Browsing
7
7
 
8
- Web access for everything a plain fetch cannot finish: a page that renders in JS, a click or a form, a screenshot, a login that must persist across pages, or a host that blocks generic fetchers (WAF / 403 / Cloudflare). Start at the cheapest tier that can do the job and climb only when it cannot:
8
+ Web access for the hosts the user's browser cannot open (WAF / 403 / Cloudflare / bot-scored pages) and for platforms with a native reader. Reading and interaction on a page the user's browser can open belong to the `browser` skill's attached engine, not here. Start at the cheapest tier that can reach the blocked host and climb only when it cannot:
9
9
 
10
10
  **Tier 1 — insane-search** (headless extraction + WAF bypass) -> **Tier 1.5 — agent-reach** (platform-native APIs, esp. Chinese platforms) -> **Tier 2 — a real browser** through omowright from js eval: 2a the owned engine (a browser your code launches, CloakBrowser for stealth), 2b the attached engine (the user's own signed-in browser).
11
11
 
@@ -14,29 +14,27 @@ Web access for everything a plain fetch cannot finish: a page that renders in JS
14
14
  ```
15
15
  User request
16
16
  |
17
- +- extract text/data from a URL --------------------- TIER 1 insane-search
17
+ +- a page the user's browser can open (read, click, screenshot, QA, login) NOT this skill (browser skill, attached engine)
18
+ +- simple search query ------------------------------ NOT this skill (use web-search)
19
+ |
18
20
  +- URL blocked / 403 / Cloudflare / WAF ------------- TIER 1 insane-search
19
21
  +- YouTube/Vimeo/TikTok subtitles or metadata ------- TIER 1 insane-search (yt-dlp)
20
- +- read an article / blog / Reddit / HN / arXiv ----- TIER 1 insane-search
22
+ +- archive snapshot of a dead or changed page ------- TIER 1 insane-search (surrogates, provenance-tagged)
21
23
  |
22
24
  +- Chinese platform (xhs/douyin/weibo/bilibili/v2ex/wechat) TIER 1.5 agent-reach
23
25
  +- podcast transcript / stock forum ----------------- TIER 1.5 agent-reach
24
26
  +- Twitter feed / LinkedIn profile / GitHub via CLI - TIER 1.5 agent-reach
25
27
  |
26
- +- Tier 1/1.5 returned empty or partial ------------- TIER 2 2a owned engine -> 2b attached engine
27
- +- click / fill form / scroll / interact ------------ TIER 2 2a owned engine -> 2b attached engine
28
- +- screenshot / render / play video ----------------- TIER 2 2a owned engine -> 2b attached engine
29
- +- login session across pages / the user's account --- TIER 2 2b attached engine (their browser)
30
- +- test web app / QA / dogfood ---------------------- TIER 2 2a owned engine -> 2b attached engine
31
- |
32
- +- simple search query ------------------------------ NOT this skill (use web-search)
28
+ +- Tier 1/1.5 returned empty or partial ------------- TIER 2 2a owned engine (stealth)
29
+ +- a blocked host that also needs a click, a scroll or a screenshot TIER 2 2a owned engine
30
+ +- the user's account on a host the owned engine cannot pass TIER 2 2b attached engine (their browser)
33
31
  ```
34
32
 
35
33
  Read the matching reference before acting: [`references/insane-search/README.md`](references/insane-search/README.md), [`references/agent-reach/README.md`](references/agent-reach/README.md), or [`references/chrome-stealth.md`](references/chrome-stealth.md).
36
34
 
37
35
  ## Tier 1 — insane-search (headless extraction)
38
36
 
39
- **When**: content extraction, blocked-URL bypass, media metadata — no browser UI needed.
37
+ **When**: blocked-URL bypass, media metadata, archive surrogates — no browser UI needed.
40
38
  **Why first**: ~10x faster than a browser, no process spin-up; handles most "fetch this blocked page" requests via curl_cffi TLS impersonation, yt-dlp (1858 sites), official public APIs, mobile URL transforms, **Phase-2.5 surrogate archives** (Wayback / archive.today snapshots, provenance-tagged — see [`references/insane-search/cache-archive.md`](references/insane-search/cache-archive.md)), a key-gated Jina Reader (`JINA_API_KEY`), and a Playwright real-Chrome fallback. The engine lives **inside this skill** at `engine/` and is invoked as a module. Surrogate results are dated COPIES: a result whose `provenance` is `snapshot` must be reported with its `snapshot_timestamp`, never presented as the live page.
41
39
 
42
40
  ```bash
@@ -56,7 +54,7 @@ The full engine harness (rules R1-R7, the Phase 0 official-API index, the no-sit
56
54
 
57
55
  ### Escalate to Tier 1.5 or Tier 2 when
58
56
  - The target is a Chinese / social platform with a native reader -> Tier 1.5.
59
- - insane-search returns empty/partial, or the page needs JS interaction, a screenshot, a persistent login, or media playback -> Tier 2.
57
+ - insane-search returns empty/partial, or the blocked page needs JS interaction, a screenshot, or media playback -> Tier 2.
60
58
 
61
59
  ## Tier 1.5 — agent-reach (platform-native readers)
62
60
 
@@ -82,7 +80,7 @@ Routing table, per-platform auth (set `TWITTER_*` env vars, `gh auth login`, a t
82
80
 
83
81
  ## Tier 2 — a real browser (real interaction)
84
82
 
85
- **When**: real interaction is needed (clicks, forms, screenshots, video, persistent login), or Tier 1/1.5 failed.
83
+ **When**: Tier 1/1.5 failed on a blocked host, or that host needs interaction (clicks, scroll, screenshots, video). A page the user's browser can open is the `browser` skill's job, attached engine first.
86
84
 
87
85
  Both tiers are omowright, staged inside the `browser` skill and loaded from js eval:
88
86
 
@@ -110,9 +108,9 @@ try {
110
108
 
111
109
  The rest of the surface (CUA coordinates, captcha solving, routes, traces, frames, human handoff) is in the `browser` skill's `references/owned-engine/`. A stealth binary is not proof of access: inspect the rendered result and report challenges that remain.
112
110
 
113
- ### Tier 2b — attached engine (logged-in pages)
111
+ ### Tier 2b — attached engine (the user's account on a blocked host)
114
112
 
115
- When the page needs the user's account, drive the browser they are already signed into instead of cloning their profile: `connectBrowserSkill()` → `session.navigate` → `bskSnapshot(session)` / `session.observe()` → `session.click` → `session.stop()`. NEVER launch against or clear cookies/cache/site data from the user's live profile, and never fall back to the owned engine for an authenticated criterion: if no extension is connected, run the `browser` skill's onboarding script and relay its one human step. The full loop is the `browser` skill.
113
+ When the blocked flow needs the user's account, drive the browser they are already signed into instead of cloning their profile: `connectBrowserSkill()` → `session.navigate` → `bskSnapshot(session)` / `session.observe()` → `session.click` → `session.stop()`. NEVER launch against or clear cookies/cache/site data from the user's live profile, and never fall back to the owned engine for an authenticated criterion: if no extension is connected, run the `browser` skill's onboarding script and relay its one human step. The full loop is the `browser` skill.
116
114
 
117
115
  ### Cookie login (cross-platform)
118
116
 
@@ -112,7 +112,11 @@ degrades color and wide-glyph width. In this repo:
112
112
  stream). Outside this repo, capture equivalent browser-rendered terminal
113
113
  evidence: screenshot + plain transcript + cleanup receipt.
114
114
 
115
- # Bootstrap (DO ALL FOUR BEFORE ANY OTHER WORK — NO SKIPPING)
115
+ # Bootstrap (COMPLETE EVERY SECTION BEFORE ANY OTHER WORK — NO SKIPPING)
116
+
117
+ ## Task resources
118
+ Before the first task command, create one `RUN_TMP=$(mktemp -d)` per task, tracking its paths under section 3 immediately. Export `TMPDIR="$RUN_TMP" NODE_COMPILE_CACHE="$RUN_TMP/node-compile-cache" BUN_RUNTIME_TRANSPILER_CACHE_PATH="$RUN_TMP/bun-transpiler"` for every command; pass the same environment explicitly to children that do not inherit it. Give independent child tasks their own run roots.
119
+ At task end, after owned processes exit, remove only owned directories, including runtime caches. Never wipe shared caches; retain worktrees still needed by open PRs and any parent directory containing them.
116
120
 
117
121
  ## 0. Survey the skills, gather context, then size the work
118
122
  First, survey the loaded skill list and read the description of each
@@ -206,7 +210,7 @@ steps.
206
210
 
207
211
  ## 3. Register obsessive todos via `update_plan`
208
212
  The todo tool is Codex `update_plan` — your live, user-visible
209
- checklist. Translate every action from the plan into one `update_plan`
213
+ checklist. Keep resource-cleanup todos registered earlier. Translate every action from the plan into one `update_plan`
210
214
  step — one step per atomic work unit: an edit plus its verification, a
211
215
  QA scenario run, a teardown. Keep each step small enough to finish
212
216
  within a few tool calls.
@@ -218,6 +222,9 @@ behind reality. Add newly discovered steps the moment they surface
218
222
  instead of waiting for the next pass. Step text encodes WHERE / WHY
219
223
  (which criterion it advances) / HOW / VERIFY:
220
224
  `path: <action> for <criterion> — verify by <check>`.
225
+ ALWAYS add a cleanup todo AT CREATION for each worktree, cache directory (including
226
+ the run root), and QA resource (PID, tmux session, browser context, port, container,
227
+ script or temp path); complete it only after a read-back teardown receipt.
221
228
 
222
229
  GOOD pair (ordered):
223
230
  `test/foo.test.ts: read the validateEmail cases for criterion 2 — verify by noting intent / coverage / pass in the notepad`
@@ -280,19 +287,15 @@ Until every success criterion PASSES with its evidence captured:
280
287
  above; auxiliary surface for CLI- or data-shaped criteria), end to
281
288
  end, yourself, plus the step-2 tests; a reproduction now passes.
282
289
  Paste the artifact path into the notepad.
283
- 5. CLEANUP (PAIRED — NEVER SKIP): the moment a QA scenario spawns any
284
- resource, register its teardown as its own todo (e.g.
285
- `cleanup: kill server pid for criterion 2 — verify kill -0 fails`).
286
- Every runtime artifact the QA spawned in step 4 MUST be torn down
287
- before this step completes:
288
- server PIDs (`kill <pid>`; verify `kill -0` fails), `tmux` sessions
289
- (`tmux kill-session -t ulw-qa-<criterion>`; verify with `tmux ls`),
290
- browsers / sessions (`browser.close()` / `session.stop()`), containers
291
- (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp
292
- sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env
293
- vars. Append a one-line cleanup receipt to the notepad next to the
294
- artifact, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo;
295
- rm -rf /tmp/ulw.aB12cD`. No receipt → criterion stays in_progress.
290
+ 5. CLEANUP (PAIRED — NEVER SKIP):
291
+ Before recording QA, tear down owned server PIDs (`kill <pid>`; verify
292
+ `kill -0` fails), tmux sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`),
293
+ browsers / sessions (`browser.close()` / `session.stop()`), containers (`docker rm -f`), ports
294
+ (`lsof -i :<port>` empty), QA scripts, temp paths and QA-only env vars.
295
+ Remove QA temp paths inside `$RUN_TMP` per criterion; remove the run root
296
+ and eligible worktrees at task end under Task resources.
297
+ Append the read-back receipt beside the artifact in the notepad.
298
+ No receipt → criterion stays in_progress.
296
299
  6. Verify: LSP diagnostics clean on changed files; no test skipped or
297
300
  xfail-ed this turn.
298
301
  7. Mark completed. Append non-obvious findings / learnings.
@@ -105,7 +105,7 @@ Write `.omo/boulder.json` before implementation starts. Prefix session ids with
105
105
  }
106
106
  ```
107
107
 
108
- Every phase (plan wave) runs in its own task-owned worktree with its own goal: before the wave's first dispatch, record the wave's goal — its checkboxes and their acceptance criteria — as a ledger entry, then `git worktree add <repo>-wt/<plan>-<wave> <branch off the integration base>` (or verify a `--worktree` path with `git worktree list --porcelain`), store the absolute path as `worktree_path`, run every edit, command, test, and evidence capture inside it; the wave lands on the integration base once its checkboxes are verified (direct merge, or the PR under `--make-pr`/`--ship`), and the next wave branches from that landed base.
108
+ Every phase (plan wave) runs in its own task-owned worktree with its own goal: before the wave's first dispatch, record the wave's goal — its checkboxes and their acceptance criteria — as a ledger entry, then `git worktree add <repo>-wt/<plan>-<wave> <branch off the integration base>` (or verify a `--worktree` path with `git worktree list --porcelain`), store the absolute path as `worktree_path`, run every edit, command, test, and evidence capture inside it; the wave lands on the integration base once its checkboxes are verified (direct merge, or the PR under `--make-pr`/`--ship`), and the next wave branches from that landed base. Use the bundled ultrawork directive’s Task resources and todo rules at wave/worktree creation; read those sections if absent from context.
109
109
 
110
110
  ## Parallel delivery lanes (teams and worktrees)
111
111
 
@@ -238,7 +238,7 @@ When all top-level checkboxes in `## TODOs` and `## Final Verification Wave` are
238
238
  1. Run the plan's final verification commands.
239
239
  2. Record a self-review in the notepad: re-read the diff, run diagnostics, and capture evidence for every acceptance criterion. Run your own manual QA on the real surface.
240
240
  3. Only when the user demanded strict, rigorous, or high-accuracy review, spawn ONE `lazycodex-gate-reviewer`; otherwise your self-review is the final verification.
241
- 4. Finish the PR/branch lifecycle from its task-owned worktree: sync `.omo/` state back to the main repo, create or update the PR, wait for review/verification gates, merge by default unless explicitly opted out, and remove the worktree only after successful merge or explicit handoff.
241
+ 4. Finish the PR/branch lifecycle from its task-owned worktree: sync `.omo/` state back to the main repo, create or update the PR, wait for review/verification gates, merge by default unless explicitly opted out, and remove the worktree only after successful merge or explicit handoff. Finish directory teardown per Task resources and record its receipts.
242
242
  5. Remove or mark the Boulder work as completed.
243
243
  6. Print an `ORCHESTRATION COMPLETE` block with the plan path, verification commands, artifacts, and cleanup receipts.
244
244
 
@@ -45,7 +45,7 @@ This skill is intentionally compact. The full workflow lives in `references/full
45
45
  ## Non-Negotiables
46
46
 
47
47
  - Use the ulw-loop CLI state under `.omo/ulw-loop`; do not hand-edit goal state.
48
- - Register goals up front, shaped by `references/define-goal.md` (`omo-agent-toolkit ulw-loop create-goals`, then `create_goal` from the printed handoff), and mirror every atomic step into the live `update_plan` checklist: one ultra-granular step per action, exactly one in_progress, transitions marked the instant they happen.
48
+ - Register goals up front, shaped by `references/define-goal.md` (`omo-agent-toolkit ulw-loop create-goals`, then `create_goal` from the printed handoff), and mirror every atomic step into the live `update_plan` checklist: one ultra-granular step per action, exactly one in_progress, transitions marked the instant they happen. Use the ultrawork directive’s Task resources and todo rules; read those sections of the bundled `ultrawork` skill if absent from context.
49
49
  - After any compaction or context loss, re-read brief + goals + ledger FIRST plus `omo-agent-toolkit ulw-loop status --json`, then resume; never re-plan from scratch.
50
50
  - Every ulw-loop command needs the session scope: pass `--session-id <id>` (the printed handoff and resume directive carry it; `CODEX_THREAD_ID` in the environment also resolves it). The CLI refuses unscoped state (`ULW_LOOP_SESSION_SCOPE_REQUIRED`) instead of touching the shared `.omo/ulw-loop` root.
51
51
  - If `omo-agent-toolkit ulw-loop create-goals` says this session's aggregate is already complete, start unrelated new work with a fresh `--session-id <new-id>` (passed on every later call) instead of steering or forcing the completed state. Use `--force` only to intentionally overwrite completed evidence.
@@ -73,7 +73,7 @@ When the units are genuinely independent — separate files, no shared contract
73
73
 
74
74
  Under team mode, isolate and land per unit:
75
75
 
76
- - **One git worktree per member**, never a shared checkout — concurrent members editing one working tree corrupt each other's diffs and evidence. Give each member its own branch off the base and its own worktree path.
76
+ - **One git worktree per member**, never a shared checkout — concurrent members editing one working tree corrupt each other's diffs and evidence. Give each member its own branch off the base and its own worktree path. Apply the same resource rules when creating member worktrees.
77
77
  - **Merge per work unit, as each unit is verified.** A member's unit lands when its own evidence is captured and its gates are green; it does not wait for the slowest sibling. Integrate each merged unit back into the base the others branch from, so overlapping members rebase onto real merged work rather than guessing at it.
78
78
  - **Conflicts are the lead's job.** When two members' units touch the same lines, the lead decides the order they land and tells the later member what changed; members never resolve a sibling's conflict blind.
79
79
 
@@ -70,7 +70,7 @@ Codex subagent reliability:
70
70
  - Never invent state outside `.omo/ulw-loop` artifacts or `omo-agent-toolkit ulw-loop status --json`.
71
71
 
72
72
  ## Bootstrap
73
- Do all three steps before execution. No edits, goal tools, or checkpointing before bootstrap completes.
73
+ Do all three steps before execution; no edits, goal tools or checkpointing until bootstrap completes. Follow the parent skill’s resource-rule pointer before execution.
74
74
 
75
75
  ### 1. Create goals from the brief
76
76
  Resolve the CLI before the first command. If `omo` is absent from PATH or lacks `ulw-loop`, use the stable local installer bin or cached Codex component CLI — same CLI, so PATH absence is not a blocker. If PATH is empty, the fallback uses shell builtins and absolute Node locations before reporting guidance, recording the failure in `.omo/ulw-loop/bootstrap-notepad.md`.
@@ -165,7 +165,7 @@ Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures a
165
165
  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.
166
166
  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.
167
167
  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.
168
- 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, browser contexts, 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
+ 7. CLEAN (PAIRED, NEVER SKIP): execute the teardown todos before recording QA: owned PIDs (`kill`; verify `kill -0` fails), tmux sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser contexts (`.close()`), containers (`docker rm -f`), ports (`lsof -i :<port>` empty), QA scripts, temp paths and QA-only env vars. Close finished v1 workers with `close_agent`; V2 workers end normally, so `interrupt_agent` any still running. Apply the ultrawork Task resources rule at task end, not after each criterion. Embed read-back receipts in the evidence and complete the cleanup todos. Missing receipt → record BLOCKED, not PASS.
169
169
  8. RECORD one result immediately from the artifact you just wrote — never from memory or a later turn — stamping the capture tree `$(git rev-parse --short "HEAD^{tree}")` into the evidence:
170
170
  - PASS: `omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> @tree:<short-tree> | <cleanup receipt>" --json`
171
171
  - FAIL: `omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> @tree:<short-tree> | <cleanup receipt>" --notes "<diagnosis>" --json`
@@ -88,7 +88,7 @@ Saturation research defaults to teammode, not isolated fire-and-forget workers:
88
88
 
89
89
  ## Worker ground rules
90
90
 
91
- Research workers (explore, librarian, browsing) differ by harness, but assume:
91
+ Research workers (explore, web, repo-dive) differ by harness, but assume:
92
92
 
93
93
  - **Read-only.** Most research workers cannot write files. Never ask a worker to write the journal or any session file — every journal write is yours.
94
94
  - **No recursion — workers AND members.** Workers cannot spawn their own subagents, and team members must not re-orchestrate: a member researches its axis and reports; it never stands up its own team, loads this skill, or fans out a research swarm of its own. Depth comes from YOUR expansion waves. Say so in every spawn message — a member that starts its own research protocol burns the run's budget on duplicated orchestration and returns nothing you can cite.
@@ -121,11 +121,13 @@ Before spawning anything, decompose the query. Start from "what must be true if
121
121
  <analysis>
122
122
  Core question: <the actual information need>
123
123
  Axes (3+ orthogonal): <axis — what to search, where, why> ...
124
- Codebase relevant: <yes/no> · External: <yes/no> · Browsing: <yes/no> · Verification likely: <yes/no>
124
+ Codebase relevant: <yes/no> · External: <yes/no> · Collection: <search-only | browser> · Verification likely: <yes/no>
125
125
  Scale: <axis count, source territories, target document length> · Precision demand: <what a wrong claim costs here> → lifecycle: <single team | research team then refinement team>
126
126
  </analysis>
127
127
  ```
128
128
 
129
+ `Collection` is `search-only` only when a result snippet settles every axis (a date, a version, a spec value); anything that needs reading a source is `browser`.
130
+
129
131
  Then create the session directory:
130
132
 
131
133
  ```bash
@@ -171,26 +173,23 @@ Otherwise launch the entire first wave in one turn — every axis at once, as te
171
173
 
172
174
  Scaling floor — more angles always justify more workers:
173
175
 
174
- | Query scope | explore | librarian | browsing | repo-dive | floor |
175
- |---|---|---|---|---|---|
176
- | Single topic, codebase only | 3 | 0 | 0 | 0 | 3 |
177
- | Single topic, web only | 0 | 4 | 1 | 1 | 6 |
178
- | Single topic, both | 2 | 3 | 1 | 1 | 7 |
179
- | Multi-faceted | 4 | 6 | 2 | 2 | 14 |
180
- | Full due diligence | 4 | 6 | 3 | 2 | 15 |
181
-
182
- The browsing column is BINDING, not advisory: when the brief says `Browsing: yes`, the roster names a browsing-worker angle, armed with the `ultimate-browsing` skill, before the first wave launches, and that worker is spawned in the same turn as the rest of the wave. A run that reaches wave 2 with zero browsing workers on a `Browsing: yes` brief has silently downgraded every source to what plain fetch happened to return.
176
+ | Query scope | explore | web | repo-dive | floor |
177
+ |---|---|---|---|---|
178
+ | Single topic, codebase only | 3 | 0 | 0 | 3 |
179
+ | Single topic, web only | 0 | 5 | 1 | 6 |
180
+ | Single topic, both | 2 | 4 | 1 | 7 |
181
+ | Multi-faceted | 4 | 8 | 2 | 14 |
182
+ | Full due diligence | 4 | 9 | 2 | 15 |
183
183
 
184
184
  **Disambiguate before you expand.** When the topic names something that could resolve several ways — a product, a person, a codename, a version — the first wave settles WHICH entity before any worker researches its history, benchmarks, or controversies: canonical name, first-party URL or account, whether it exists in the claimed category, and a confidence line. An unresolved entity never becomes a premise in a later wave's spawn message; that is exactly how a run starts inventing facts about something that does not exist.
185
185
 
186
186
  Role protocols — embed the relevant one in each spawn message; every worker gets a unique angle:
187
187
 
188
188
  - **Codebase (explore), 2-4 workers.** Grep with 3+ keyword variations; structural/AST search; LSP definitions and references; file-name globs; `git log --all -S '<keyword>'` and `--grep` for history including deleted code. Cross-validate hits across tools. Report absolute file paths, patterns with `file:line`, and how findings connect.
189
- - **Web (librarian), 3-6 workers.** At least 10 distinct websearch queries per worker, each with a different operator or angle (see Search craft); fetch the full page for every result that matters — snippets lie. Context7 with 3+ queries per known library. grep.app and `gh search code|repos|issues` for real-world usage. Official docs via sitemap discovery (`<base>/sitemap.xml`), then targeted pages.
190
- - **Browsing, 1-3 workers on a `Browsing: yes` brief (0 otherwise), every one loaded with the `ultimate-browsing` skill.** The skill owns the routing — the harness's own browser first, then its extraction engine with archive surrogates, platform-native readers, and Chrome stealth as each source demands; the worker owns the deliverable. This worker RENDERS pages, it does not re-fetch them: its standing deliverable is a full-page screenshot of every top source plus the rendered text plain fetch could not reach; a worker that returns only fetched text has not done its job. JS-rendered, login-gated, WAF-blocked, and screenshot-bearing sources all belong here rather than in the web lane. **Provenance is part of the claim**: when a source came back with `provenance` of `snapshot` (an archive copy), cite it with its `snapshot_timestamp` and never state it as the current live page; content from a `proxy` route is `untrusted` and needs a second independent route before any claim rests on it. When one blocked territory hides many leads, fan out more browsing subagents in parallel for breadth instead of serializing one worker through them.
189
+ - **Web, 3-9 workers, every one loaded with the `browser` skill.** Search with 10+ websearch queries per worker, each with a different operator or angle (see Search craft); a result is a candidate, not a source. Read every candidate in the user's browser through the `browser` skill's attached engine: one named session per worker, one tab per source, several tabs open side by side while sources are compared — `session.tabCreate` / `tabSelect` / `tabClose` where the harness has a JS eval kernel, `bsk tab create --url <url>` / `tab select <id>` / `tab close <id>` / `tab list` on the CLI — the page scrolled to its end, a full-page screenshot of each top source saved as provenance, the tab closed once reported. A browser on another machine is reached through the `browser` skill's remote reference or a user-level skill that bridges to it, never replaced by a local headless one. `webfetch` is for raw files only: PDF, JSON, feeds, sitemaps. A host the browser cannot open (WAF, bot score, 403) goes to `ultimate-browsing`, whose provenance travels with the claim: a `snapshot` result cites its `snapshot_timestamp` and is never stated as the live page; a `proxy` result is `untrusted` until a second independent route agrees. Context7 with 3+ queries per known library; grep.app and `gh search code|repos|issues` for real-world usage; official docs via sitemap discovery (`<base>/sitemap.xml`), then targeted pages. A worker whose sources are all fetched text has searched, not read.
191
190
  - **Repo deep-dive (librarian), 0-2 workers.** Shallow-clone the most relevant repos to `${TMPDIR:-/tmp}`, pin the HEAD SHA, read core modules, follow call chains, return SHA-pinned permalinks.
192
191
 
193
- Example spawn (codebase axis; librarian, browsing, and repo-dive follow the same contract with their own protocol):
192
+ Example spawn (codebase axis; web and repo-dive follow the same contract with their own protocol):
194
193
 
195
194
  ```
196
195
  task(subagent_type="explore", run_in_background=true, prompt="TASK: act as a codebase researcher. AXIS: <specific angle>.
@@ -209,9 +208,9 @@ This loop is what makes the mode research rather than search. Collect returns as
209
208
  3. Spawn an expansion worker immediately for each new unchecked lead:
210
209
 
211
210
  ```
212
- task(subagent_type="librarian", run_in_background=true, prompt="TASK: expansion wave <N> — investigate: <lead>.
211
+ task(category="deep-low", load_skills=["browser"], run_in_background=true, prompt="TASK: expansion wave <N> — investigate: <lead>.
213
212
  PARENT: <which return surfaced it>. This is an explicit exhaustive-research assignment; budgets do not apply.
214
- <role protocol for the lead's territory — librarian protocol for external leads, explore protocol for codebase leads>
213
+ <role protocol for the lead's territory — web protocol for external leads, explore protocol for codebase leads>
215
214
  End your reply with the ## EXPAND tail.")
216
215
  ```
217
216
 
@@ -333,7 +332,7 @@ Asset workers (background, parallel, each fed `design-spec.md`) — visuals are
333
332
  - **Charts for every quantitative finding, computed from real data.** Pull the numbers into an actual table first (CSV/JSON under `$SESSION_DIR`), then plot from that table, never from prose. Follow the data-scientist tool doctrine — numpy always, Polars for filtering/sorting/transforms, DuckDB for joins/aggregations/window functions, never pandas — and load the `data-scientist` skill when this session has it: `uv run --with numpy --with polars --with duckdb --with pyarrow --with matplotlib python`. Keep `pyarrow` in that set — the DuckDB-to-Polars handoff (`.pl()`) fails without it, and `.df()` fails without pandas, so hand data across through `.pl()`, never `.df()`. Save to `$SESSION_DIR/assets/`.
334
333
  - **Mermaid graphs** for process, architecture, argument, timeline, and evidence-flow structure, themed to the spec's fonts and palette. Render each to SVG and confirm the file exists before the document references it.
335
334
  - **Generated visuals through the imagegen skill whenever the session has it:** a cover plus a concept illustration per major theme, prompted from the spec's style, palette, and mood — document-styled illustration, never generic stock art dropped into a designed page.
336
- - **Full-page screenshots** of the top 5-10 sources (browsing worker) as provenance you can show.
335
+ - **Full-page screenshots** of the top 5-10 sources, saved by the web workers while reading, as provenance you can show.
337
336
 
338
337
  **Verify the asset manifest before rendering.** List every asset the document references, assert each file exists and is non-empty on disk, and re-render whatever is missing. A document that renders with three broken diagrams is a document you will publish twice.
339
338
 
@@ -391,7 +390,7 @@ High-yield combinations: official docs (`site:<docs domain>`), GitHub implementa
391
390
  | Obeying a surrounding "stop exploring" rule mid-research | Authority section — those rules do not bind this mode |
392
391
  | Asking a worker to write journal or session files | Workers are read-only; you journal every return |
393
392
  | Two workers given the same angle | One unique angle per worker, always |
394
- | A `Browsing: yes` run whose roster carries no browsing worker, or a browsing worker spawned without `ultimate-browsing` | The browsing column is binding — name the angle in the brief and spawn it, armed with the skill, in the first wave before any lead is chased |
393
+ | A web worker that returned only fetched text, or a `Collection: browser` run whose workers never opened a tab | Sources are read in the user's browser through the `browser` skill; `webfetch` is for raw files; every web worker carries the skill from the first wave |
395
394
  | Contested claim settled by judgment | Phase 3 — run code, capture output, verdict |
396
395
  | Deliverable claims without citations | Every claim cites a source or a verification artifact |
397
396
  | Guessing the deliverable format | Derive it from the request and destination; ask the empty-info interview only for what is still missing, and record `answered_by` for every field |
@@ -0,0 +1,179 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import test from "node:test";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ const scripts = join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
10
+
11
+ // Drive the shipped CLI in a disposable layout. Only external Git results and
12
+ // backoff time are controlled; no test-only export or option is added to it.
13
+ function runCli(t, { failures = 0, strict = true, strictEnv = false, skip = false, pins = "valid" } = {}) {
14
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "omo-materialize-retry-")));
15
+ t.after(() => rmSync(root, { recursive: true, force: true }));
16
+ const entry = join(root, "packages", "omo-codex", "plugin", "scripts");
17
+ const shared = join(root, "packages", "shared-skills");
18
+ mkdirSync(entry, { recursive: true });
19
+ mkdirSync(join(shared, "scripts"), { recursive: true });
20
+ for (const name of ["materialize-shared-upstreams.mjs", "entry-guard.mjs"]) {
21
+ copyFileSync(join(scripts, name), join(entry, name));
22
+ }
23
+ writeFileSync(join(shared, "scripts", "materialize-frontend-refs.mjs"),
24
+ 'export async function materializeFrontendRefs({ strict }) { globalThis.record({ op: "materialize", strict }); return { skipped: false }; }\n');
25
+ writeFileSync(join(shared, "stage-omowright-runtime.mjs"),
26
+ 'export async function stageOmowrightRuntime() { globalThis.record({ op: "stage" }); return { version: "fixture" }; }\n');
27
+ const trace = join(root, "trace.json");
28
+ writeFileSync(trace, "[]");
29
+ const preload = join(root, "git-fixture.mjs");
30
+ writeFileSync(preload, `
31
+ import childProcess from "node:child_process";
32
+ import timers from "node:timers/promises";
33
+ import { syncBuiltinESMExports } from "node:module";
34
+ import { writeFileSync } from "node:fs";
35
+ const events = [];
36
+ const realSleep = timers.setTimeout;
37
+ let attempts = 0;
38
+ let paths = [];
39
+ globalThis.record = (event) => {
40
+ events.push(event);
41
+ writeFileSync(${JSON.stringify(trace)}, JSON.stringify(events));
42
+ };
43
+ childProcess.execFileSync = (command, args, options) => {
44
+ if (command !== "git") throw new Error("unexpected command: " + command);
45
+ record({ op: args[0], args, cwd: options.cwd, windowsHide: options.windowsHide });
46
+ if (args[0] === "submodule") {
47
+ paths = args.slice(4);
48
+ if (++attempts <= ${failures}) throw new Error("fixture fetch unavailable");
49
+ return Buffer.alloc(0);
50
+ }
51
+ if (args[0] !== "ls-files") throw new Error("unexpected Git operation");
52
+ if (${JSON.stringify(pins)} === "error") throw new Error("fixture index unavailable");
53
+ return paths.map((path, i) => {
54
+ const mode = ${JSON.stringify(pins)} === "invalid" ? "100644" : "160000";
55
+ const stage = ${JSON.stringify(pins)} === "unmerged" ? "2" : "0";
56
+ const sha = String(i + 1).repeat(${JSON.stringify(pins)} === "sha256" ? 64 : 40);
57
+ return mode + " " + sha + " " + stage + "\\t" + path + "\\0";
58
+ }).join("");
59
+ };
60
+ timers.setTimeout = async (ms) => {
61
+ record({ op: "sleep", ms });
62
+ await realSleep(15);
63
+ record({ op: "wake", ms });
64
+ };
65
+ syncBuiltinESMExports();
66
+ `);
67
+ const env = { ...process.env };
68
+ delete env.NODE_OPTIONS;
69
+ delete env.OMO_SKIP_MATERIALIZE;
70
+ delete env.OMO_MATERIALIZE_STRICT;
71
+ if (strictEnv) env.OMO_MATERIALIZE_STRICT = "1";
72
+ if (skip) env.OMO_SKIP_MATERIALIZE = "1";
73
+ const result = spawnSync(process.execPath, ["--import", pathToFileURL(preload).href, join(entry, "materialize-shared-upstreams.mjs"), ...(strict ? ["--strict"] : [])], {
74
+ cwd: root, env, encoding: "utf8", windowsHide: true, timeout: 10000,
75
+ });
76
+ assert.ifError(result.error);
77
+ assert.equal(result.signal, null, result.stderr);
78
+ return { ...result, events: JSON.parse(readFileSync(trace, "utf8")), root };
79
+ }
80
+
81
+ const operations = (result) => result.events.map(({ op }) => op);
82
+ const attempts = (result) => result.events.filter(({ op }) => op === "submodule");
83
+ const delays = (result) => result.events.filter(({ op }) => op === "sleep").map(({ ms }) => ms);
84
+
85
+ function assertFetchContract(result, count) {
86
+ assert.equal(attempts(result).length, count);
87
+ for (const call of attempts(result)) {
88
+ assert.deepEqual(call.args.slice(0, 4), ["submodule", "update", "--init", "--recursive"]);
89
+ assert.equal(call.args.includes("--remote"), false);
90
+ assert.equal(call.cwd, result.root);
91
+ assert.equal(call.windowsHide, true);
92
+ }
93
+ }
94
+
95
+ test("#given reachable pins #when strict materialization runs #then fetch once without backoff", (t) => {
96
+ const r = runCli(t);
97
+ assert.equal(r.status, 0, r.stderr);
98
+ assertFetchContract(r, 1);
99
+ assert.deepEqual(operations(r), ["submodule", "materialize", "stage"]);
100
+ });
101
+
102
+ test("#given one failed fetch #when strict materialization retries #then await recovery before staging", (t) => {
103
+ const r = runCli(t, { failures: 1 });
104
+ assert.equal(r.status, 0, r.stderr);
105
+ assertFetchContract(r, 2);
106
+ assert.deepEqual(delays(r), [1000]);
107
+ assert.deepEqual(operations(r), ["submodule", "sleep", "wake", "submodule", "materialize", "stage"]);
108
+ });
109
+
110
+ test("#given two failed fetches #when the final attempt recovers #then stop retrying with bounded backoff", (t) => {
111
+ const r = runCli(t, { failures: 2 });
112
+ assert.equal(r.status, 0, r.stderr);
113
+ assertFetchContract(r, 3);
114
+ assert.deepEqual(delays(r), [1000, 2000]);
115
+ assert.equal(r.events.filter(({ op }) => op === "materialize").length, 1);
116
+ assert.equal(r.events.some(({ op }) => op === "ls-files"), false);
117
+ });
118
+
119
+ test("#given persistent fetch failure #when strict attempts exhaust #then fail with every path and pinned SHA", (t) => {
120
+ const r = runCli(t, { failures: 9 });
121
+ assert.equal(r.status, 1, r.stderr);
122
+ assertFetchContract(r, 3);
123
+ assert.deepEqual(delays(r), [1000, 2000]);
124
+ assert.equal(r.events.some(({ op }) => op === "materialize" || op === "stage"), false);
125
+ assert.match(r.stderr, /failed after 3 attempts/);
126
+ assert.match(r.stderr, /fixture fetch unavailable/);
127
+ const paths = attempts(r)[0].args.slice(4);
128
+ paths.forEach((path, i) => assert.ok(r.stderr.includes(`${path}@${String(i + 1).repeat(40)}`), r.stderr));
129
+ const lookup = r.events.find(({ op }) => op === "ls-files");
130
+ assert.deepEqual(lookup.args, ["ls-files", "--stage", "-z", "--", ...paths]);
131
+ assert.equal(lookup.cwd, r.root);
132
+ assert.equal(lookup.windowsHide, true);
133
+ });
134
+
135
+ test("#given persistent fetch failure #when best-effort attempts exhaust #then warn and materialize once", (t) => {
136
+ const r = runCli(t, { failures: 9, strict: false });
137
+ assert.equal(r.status, 0, r.stderr);
138
+ assertFetchContract(r, 3);
139
+ assert.deepEqual(delays(r), [1000, 2000]);
140
+ assert.deepEqual(operations(r).slice(-3), ["ls-files", "materialize", "stage"]);
141
+ assert.match(r.stderr, /continuing without submodule refresh/);
142
+ assert.equal(r.events.find(({ op }) => op === "materialize").strict, false);
143
+ });
144
+
145
+ test("#given an unreadable index #when retries exhaust #then preserve the original fetch failure and name paths", (t) => {
146
+ const r = runCli(t, { failures: 9, pins: "error" });
147
+ assert.equal(r.status, 1, r.stderr);
148
+ assertFetchContract(r, 3);
149
+ assert.match(r.stderr, /fixture fetch unavailable/);
150
+ for (const path of attempts(r)[0].args.slice(4)) assert.ok(r.stderr.includes(`${path}@unknown`), r.stderr);
151
+ assert.match(r.stderr, /pinned SHA unavailable/);
152
+ });
153
+
154
+ for (const pins of ["invalid", "unmerged"]) {
155
+ test(`#given ${pins} index entries #when reporting failure #then do not invent resolved gitlink pins`, (t) => {
156
+ const r = runCli(t, { failures: 9, pins });
157
+ assert.equal(r.status, 1, r.stderr);
158
+ for (const path of attempts(r)[0].args.slice(4)) assert.ok(r.stderr.includes(`${path}@unknown`), r.stderr);
159
+ });
160
+ }
161
+
162
+ test("#given SHA-256 gitlinks #when retries exhaust #then retain complete pinned object IDs", (t) => {
163
+ const r = runCli(t, { failures: 9, pins: "sha256" });
164
+ assert.equal(r.status, 1, r.stderr);
165
+ attempts(r)[0].args.slice(4).forEach((path, i) => assert.ok(r.stderr.includes(`${path}@${String(i + 1).repeat(64)}`), r.stderr));
166
+ });
167
+
168
+ test("#given strict mode from the environment #when retries exhaust #then fail before downstream materialization", (t) => {
169
+ const r = runCli(t, { failures: 9, strict: false, strictEnv: true });
170
+ assert.equal(r.status, 1, r.stderr);
171
+ assertFetchContract(r, 3);
172
+ assert.equal(r.events.some(({ op }) => op === "materialize"), false);
173
+ });
174
+
175
+ test("#given materialization was already performed #when skip is set #then perform no Git or materializer calls", (t) => {
176
+ const r = runCli(t, { failures: 9, skip: true });
177
+ assert.equal(r.status, 0, r.stderr);
178
+ assert.deepEqual(r.events, []);
179
+ });
@@ -85,7 +85,7 @@ export function removeCodexCompatibilityGuidance(content) {
85
85
  const ulwExecuteOriginalCompletion = `When all top-level checkboxes in \`## TODOs\` and \`## Final Verification Wave\` are complete:
86
86
 
87
87
  1. Run the plan's final verification commands.
88
- 2. For PR/branch work, finish the lifecycle from the last phase's worktree: sync \`.omo/\` state back to the main repo, create or update the PR, wait for review/verification gates, merge by default unless explicitly opted out, and remove the worktree only after successful merge or explicit handoff.
88
+ 2. For PR/branch work, finish the lifecycle from the last phase's worktree: sync \`.omo/\` state back to the main repo, create or update the PR, wait for review/verification gates, merge by default unless explicitly opted out, and remove the worktree only after successful merge or explicit handoff. Finish directory teardown per Task resources and record its receipts.
89
89
  3. Remove or mark the Boulder work as completed.
90
90
  4. Print an \`ORCHESTRATION COMPLETE\` block with the plan path, verification commands, artifacts, and cleanup receipts.`;
91
91
 
@@ -94,7 +94,7 @@ const ulwExecuteCodexCompletion = `When all top-level checkboxes in \`## TODOs\`
94
94
  1. Run the plan's final verification commands.
95
95
  2. Record a self-review in the notepad: re-read the diff, run diagnostics, and capture evidence for every acceptance criterion. Run your own manual QA on the real surface.
96
96
  3. Only when the user demanded strict, rigorous, or high-accuracy review, spawn ONE \`lazycodex-gate-reviewer\`; otherwise your self-review is the final verification.
97
- 4. Finish the PR/branch lifecycle from its task-owned worktree: sync \`.omo/\` state back to the main repo, create or update the PR, wait for review/verification gates, merge by default unless explicitly opted out, and remove the worktree only after successful merge or explicit handoff.
97
+ 4. Finish the PR/branch lifecycle from its task-owned worktree: sync \`.omo/\` state back to the main repo, create or update the PR, wait for review/verification gates, merge by default unless explicitly opted out, and remove the worktree only after successful merge or explicit handoff. Finish directory teardown per Task resources and record its receipts.
98
98
  5. Remove or mark the Boulder work as completed.
99
99
  6. Print an \`ORCHESTRATION COMPLETE\` block with the plan path, verification commands, artifacts, and cleanup receipts.`;
100
100