mirror of
https://fastgit.cc/github.com/Yeachan-Heo/oh-my-claudecode
synced 2026-04-20 21:00:50 +08:00
chore(release): bump version to v4.11.6
- Ralph security hardening: PRD gating non-bypassable, approval spoofing closed - Permission handler: narrowed trust boundary, read-only gh commands allowed - HUD: MiniMax coding plan provider, extra usage spend data, per-provider cache split - tmux/openclaw: dead-pane suppression, stale replay suppression, keyword false-positive reduction - Context dedup: no duplicate rule/skill injection from coexisting plugin+standalone - Installer: user skills with OMC-style frontmatter preserved during updates - Test isolation fix: hud-marketplace-resolution afterAll + setup-contracts-regression beforeAll restore hooks/hooks.json to prevent parallel worker race in full test suite Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
{
|
||||
"name": "oh-my-claudecode",
|
||||
"description": "Claude Code native multi-agent orchestration with intelligent model routing, 28 agent variants, and 32 powerful skills. Zero learning curve. Maximum power.",
|
||||
"version": "4.11.5",
|
||||
"version": "4.11.6",
|
||||
"author": {
|
||||
"name": "Yeachan Heo",
|
||||
"email": "hurrc04@gmail.com"
|
||||
@@ -27,5 +27,5 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"version": "4.11.5"
|
||||
"version": "4.11.6"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-claudecode",
|
||||
"version": "4.11.5",
|
||||
"version": "4.11.6",
|
||||
"description": "Multi-agent orchestration system for Claude Code",
|
||||
"author": {
|
||||
"name": "oh-my-claudecode contributors"
|
||||
|
||||
4
.clawhip/project.json
Normal file
4
.clawhip/project.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"project": "oh-my-claudecode",
|
||||
"repo_name": "oh-my-claudecode"
|
||||
}
|
||||
8
.clawhip/state/prompt-submit.json
Normal file
8
.clawhip/state/prompt-submit.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"observed_at": "2026-04-13T13:59:50.878Z",
|
||||
"provider": "claude-code",
|
||||
"event_name": "UserPromptSubmit",
|
||||
"session_id": "c28baf7e-6eed-453a-89a3-dad010991c11",
|
||||
"turn_id": null,
|
||||
"prompt_summary": "oh sorry it was misunderstanding, proceed to release sicne we bumped. make sure we commit and push dist too"
|
||||
}
|
||||
142
.github/release-body.md
vendored
142
.github/release-body.md
vendored
@@ -1,93 +1,85 @@
|
||||
# oh-my-claudecode v4.11.5: Bug Fixes & Release Skill
|
||||
# oh-my-claudecode v4.11.6: add MiniMax coding, display extra usage, split usage cache
|
||||
|
||||
## Release Notes
|
||||
|
||||
Release with **30+ bug fixes** across **50+ merged PRs** and **1 new feature**.
|
||||
|
||||
### New Features
|
||||
|
||||
- **feat(release): rewrite release skill as generic repo-aware assistant** (#2501) — `/oh-my-claudecode:release` now inspects any repo's CI, version files, and release rules on first run and caches them in `.omc/RELEASE_RULE.md`.
|
||||
Release with **4 new features**, **30 bug fixes**, **14 other changes** across **50 merged PRs**.
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Autopilot/Team stability** — surface hidden dependency stalls before `/autopilot` looks hung; preserve live team workflows when coarse staged state drifts; bound MCP restarts; repair retired team MCP config on upgrade.
|
||||
- **Keyword-detector accuracy** — prevent mode activation from quoted reference prose; keep help-style queries informational; preserve activation in mixed command/help prompts.
|
||||
- **Ralph robustness** — preserve continuation across interrupted tool turns; silence repeated idle follow-up nudges once backlog is truly zero.
|
||||
- **HUD reliability** — prevent setup docs from deleting the installed wrapper; surface import errors; prevent stale root state revival.
|
||||
- **Setup/installer correctness** — always update CLAUDE.md on install; avoid hook re-injection for plugin installs; validate and select cache version candidates deterministically.
|
||||
- **tmux centralization** — all tmux execution routed through wrapper functions; Windows `.cmd` availability checks aligned; tmux-utils API compatibility restored.
|
||||
- **feat(hud): add MiniMax coding plan usage provider** (#2568)
|
||||
- **feat(hud): display extra usage spend data in HUD** (#2571)
|
||||
- **feat(hud): split usage cache by provider to eliminate cross-session thrashing** (#2556)
|
||||
- **feat(release): rewrite release skill as generic repo-aware assistant** (#2501)
|
||||
|
||||
### New Features
|
||||
|
||||
- **feat(hud): add MiniMax coding plan usage provider** (#2568)
|
||||
- **feat(hud): display extra usage spend data in HUD** (#2571)
|
||||
- **feat(hud): split usage cache by provider to eliminate cross-session thrashing** (#2556)
|
||||
- **feat(release): rewrite release skill as generic repo-aware assistant** (#2501)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
#### Autopilot / Team
|
||||
- Surface hidden dependency stalls before /autopilot looks hung
|
||||
- Preserve live team workflows when coarse staged state drifts
|
||||
- Bound launcher-backed MCP restarts without changing user intent
|
||||
- Repair retired team MCP config on upgrade and launch
|
||||
- Prevent stale team worktrees from blocking startup
|
||||
- Preserve team state for explicit shutdown instead of terminal auto-cleanup
|
||||
- Keep bridge autopilot blocker regression aligned with active-session ownership
|
||||
- Prevent autopilot runtime insight from leaking unrelated team blockers
|
||||
- Let scale-up workers follow the existing provider launch contract
|
||||
- Avoid Claude onboarding on default omc launches
|
||||
- Prefer consensus planning blockers over team-stage continuation
|
||||
- Back off shipped idle follow-ups once zero backlog is unchanged
|
||||
- Collapse duplicate native lifecycle bursts for attached tmux sessions (#2494)
|
||||
|
||||
#### Keyword Detector / Modes
|
||||
- Prevent shipped keyword-detector hooks from re-triggering on explanatory follow-ups
|
||||
- Prevent mode activation from quoted reference prose
|
||||
- Prevent explanatory mode references from re-triggering orchestration
|
||||
- Prevent bundled help-question regexes from collapsing in keyword detection
|
||||
- Preserve activation in mixed command/help prompts (#2428)
|
||||
- Keep help-style use queries informational (#2428)
|
||||
- Prevent stale ralplan terminal states from re-triggering stop enforcement
|
||||
- Prevent stale stop-hook state from blocking fresh sessions
|
||||
- Silence repeated idle follow-up nudges once backlog is truly zero
|
||||
|
||||
#### Ralph
|
||||
- Preserve Ralph continuation across interrupted tool turns
|
||||
|
||||
#### HUD
|
||||
- Prevent HUD setup docs from deleting the installed wrapper
|
||||
- Prevent session-recreated HUD panes from reviving stale root state
|
||||
- Surface HUD import errors from plugin root wrapper (#2457)
|
||||
- Remove stale inline wrapper from HUD skill, copy from canonical template (#2433)
|
||||
- Prevent nested tmux HUD panes from surviving cleanup (#2492)
|
||||
|
||||
#### Setup / Installer / Doctor
|
||||
- Always update claude config CLAUDE.md on install (#2431)
|
||||
- Avoid hook re-injection for plugin installs (#2430)
|
||||
- Validate and strictly select cache version candidates (#2422)
|
||||
- Prefer latest cache version over stale installed path (#2422)
|
||||
- Detect CLAUDE.md version drift against plugin cache (#2423)
|
||||
- Support companion version markers and mingw-safe checks (#2423)
|
||||
- Use deterministic CLAUDE source for version drift check (#2423)
|
||||
- Remove extra brace in version drift command (#2423)
|
||||
|
||||
#### tmux / CLI
|
||||
- Centralize all tmux execution through wrapper functions (#2427)
|
||||
- Keep Windows tmux.cmd execution consistent with availability checks (#2444)
|
||||
- Restore tmux-utils API compatibility (#2442)
|
||||
- Allow completed ultrawork sessions to exit cleanly (#2439)
|
||||
|
||||
#### Hooks / Tools
|
||||
- Avoid .json false positive in source extension matching (#2432)
|
||||
- Recognize `ty` in the supported Python LSP registry (#2439)
|
||||
- Align learned skill templates with flat-file discovery (#2438)
|
||||
- **fix: suppress optional OMX startup MCP method-not-found pane noise** (#2592)
|
||||
- **fix(tmux): suppress stale pane alert replays after session death** (#2590)
|
||||
- **fix(hooks): wrap wiki hook additionalContext in hookSpecificOutput** (#2588)
|
||||
- **fix(installer): preserve concurrent settings updates during install** (#2586)
|
||||
- **fix(hooks): prevent duplicate hook firing when plugin and standalone coexist** (#2579)
|
||||
- **fix(openclaw): suppress dead-session pane replay alerts** (#2563)
|
||||
- **fix(tmux-detector): suppress stale pane history and commit/UI text false-positives** (#2574)
|
||||
- **fix(installer): preserve user skills with OMC-style frontmatter during updates** (#2575)
|
||||
- **fix(permission-handler): allow read-only gh issue/pr commands; add installer lib assertions** (#2576)
|
||||
- **fix(context-bloat): eliminate three sources of repeated rule/skill injection** (#2578)
|
||||
- **fix(ask): close stdin for provider spawns to prevent hang in piped environments** (#2564)
|
||||
- **fix(post-tool-verifier): suppress non-actionable error token noise** (#2559)
|
||||
- **fix(openclaw): suppress late lifecycle alerts for completed/cleaned-up sessions** (#2554)
|
||||
- **fix(keyword-detector): suppress review-seed echo from tripping code-review alerts** (#2550)
|
||||
- **fix(purge): symlink stale plugin version dirs to prevent post-upgrade hook failures** (#2549)
|
||||
- **fix(deep-interview): replace five remaining hardcoded 20%/0.2 threshold signals (issue #2545)** (#2547)
|
||||
- **fix(stop-hook): cap echoed task prompt to 150 chars** (#2544)
|
||||
- **fix(mcp): wire wiki, shared_memory, skills, and deepinit tools into standalone server** (#2537)
|
||||
- **fix(openclaw): suppress stale tmux pane history in stop/session-end alerts** (#2535)
|
||||
- **fix(state-root): centralize OMC_STATE_DIR resolution across hook entrypoints** (#2533)
|
||||
- **fix: restrict setup stale-skill cleanup to OMC-managed dirs** (#2528)
|
||||
- **fix: reduce post-tool bash failure false positives** (#2526)
|
||||
- **fix: pipe multiline ask advisor prompts via stdin** (#2524)
|
||||
- **fix(config): warn on deprecated delegation routing** (#2522)
|
||||
- **fix(notifications): suppress usage-text tmux alert noise** (#2515)
|
||||
- **fix(psm): launch trusted sessions with initial prompt** (#2512)
|
||||
- **fix(openclaw): dedupe multi-pane native lifecycle bursts** (#2494)
|
||||
- **fix(tmux): keep HUD pane cleanup on the current tmux server** (#2492)
|
||||
- **fix(autopilot): scope runtime insight to the active session** (#2491)
|
||||
- **fix(team): scaleUp() should honor agentType launch contracts** (#2489)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **docs: add omc symlink bootstrap and .mcp.json conflict resolution to CONTRIBUTING** (#2493)
|
||||
- **docs: add omc symlink bootstrap and .mcp.json conflict resolution** (#2493)
|
||||
|
||||
### Other Changes
|
||||
|
||||
- **Make Ralph enforce real PRD and story review gates** (#2604)
|
||||
- **Keep PR review verification focused by default** (#2600)
|
||||
- **Reduce false-severe PR review noise in clean worktrees** (#2598)
|
||||
- **Guard shipped permission-handler parity at the runtime entrypoint** (#2596)
|
||||
- **Reduce approval stalls for safe repo inspection and single-test runs** (#2594)
|
||||
- **Harden live tmux keyword alerts against prompt/search noise** (#2585)
|
||||
- **Harden tmux keyword alerting against review/payload noise** (#2582)
|
||||
- **Fix stop-hook timeout enforcement for issue #2565** (#2569)
|
||||
- **Fix persistent-mode.cjs OMC_STATE_DIR state resolution mismatch** (#2531)
|
||||
- **Suppress stale repo-level CI replay noise after zero backlog** (#2530)
|
||||
- **Fix issue #2506: keep review/fix tmux sessions on their requested task context** (#2507)
|
||||
- **Fix issue #2504: suppress tmux keyword-alert false positives from PR review seed prompts** (#2505)
|
||||
- **Fix tmux keyword-alert noise from prompt-mode startup echo** (#2502)
|
||||
- **Back off zero-backlog follow-up spam on unchanged repo state** (#2498)
|
||||
|
||||
### Stats
|
||||
|
||||
- **50+ PRs merged** | **1 new feature** | **30+ bug fixes** | **0 breaking changes**
|
||||
- **50 PRs merged** | **4 new features** | **30 bug fixes** | **0 security/hardening improvements** | **14 other changes**
|
||||
|
||||
### Install / Update
|
||||
|
||||
```bash
|
||||
npm install -g oh-my-claude-sisyphus@4.11.5
|
||||
npm install -g oh-my-claude-sisyphus@4.11.6
|
||||
```
|
||||
|
||||
Or reinstall the plugin:
|
||||
@@ -95,4 +87,10 @@ Or reinstall the plugin:
|
||||
claude /install-plugin oh-my-claudecode
|
||||
```
|
||||
|
||||
**Full Changelog**: https://github.com/Yeachan-Heo/oh-my-claudecode/compare/v4.11.4...v4.11.5
|
||||
**Full Changelog**: https://github.com/Yeachan-Heo/oh-my-claudecode/compare/v4.11.3...v4.11.6
|
||||
|
||||
## Contributors
|
||||
|
||||
Thank you to all contributors who made this release possible!
|
||||
|
||||
@DdangJin @ohprettyhak @pgagarinov @pgagarinov-hvp @Yeachan-Heo
|
||||
|
||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -1,30 +1,77 @@
|
||||
# oh-my-claudecode v4.11.3: Bug Fixes
|
||||
# oh-my-claudecode v4.11.6: add MiniMax coding, display extra usage, split usage cache
|
||||
|
||||
## Release Notes
|
||||
|
||||
Release with **7 bug fixes** across **9 merged PRs**.
|
||||
Release with **4 new features**, **30 bug fixes**, **14 other changes** across **50 merged PRs**.
|
||||
|
||||
### Highlights
|
||||
|
||||
- **fix(node): prefer PATH node over unstable execPath** (#2400)
|
||||
- **fix(hooks): prevent .js false positives in .json/.jsonl source extension check** (#2395)
|
||||
- **fix(autoresearch): strip TMUX env for nested tmux compatibility** (#2385)
|
||||
- **feat(hud): add MiniMax coding plan usage provider** (#2568)
|
||||
- **feat(hud): display extra usage spend data in HUD** (#2571)
|
||||
- **feat(hud): split usage cache by provider to eliminate cross-session thrashing** (#2556)
|
||||
- **feat(release): rewrite release skill as generic repo-aware assistant** (#2501)
|
||||
|
||||
### New Features
|
||||
|
||||
- **feat(hud): add MiniMax coding plan usage provider** (#2568)
|
||||
- **feat(hud): display extra usage spend data in HUD** (#2571)
|
||||
- **feat(hud): split usage cache by provider to eliminate cross-session thrashing** (#2556)
|
||||
- **feat(release): rewrite release skill as generic repo-aware assistant** (#2501)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **fix(node): prefer PATH node over unstable execPath** (#2400)
|
||||
- **fix(hooks): prevent .js false positives in .json/.jsonl source extension check** (#2395)
|
||||
- **fix(autoresearch): strip TMUX env for nested tmux compatibility** (#2385)
|
||||
- **fix: resolve asymmetric symlink path resolution** (#2372)
|
||||
- **fix(installer): detect enabledPlugins (Claude Code 1.x) in hasEnabledOmcPlugin (#2252 follow-up)** (#2371)
|
||||
- **fix: deactivate stale ralplan stop enforcement after consensus completion** (#2370)
|
||||
- **fix(hud): fall back to path-based version when package.json is missing** (#2362)
|
||||
- **fix: suppress optional OMX startup MCP method-not-found pane noise** (#2592)
|
||||
- **fix(tmux): suppress stale pane alert replays after session death** (#2590)
|
||||
- **fix(hooks): wrap wiki hook additionalContext in hookSpecificOutput** (#2588)
|
||||
- **fix(installer): preserve concurrent settings updates during install** (#2586)
|
||||
- **fix(hooks): prevent duplicate hook firing when plugin and standalone coexist** (#2579)
|
||||
- **fix(openclaw): suppress dead-session pane replay alerts** (#2563)
|
||||
- **fix(tmux-detector): suppress stale pane history and commit/UI text false-positives** (#2574)
|
||||
- **fix(installer): preserve user skills with OMC-style frontmatter during updates** (#2575)
|
||||
- **fix(permission-handler): allow read-only gh issue/pr commands; add installer lib assertions** (#2576)
|
||||
- **fix(context-bloat): eliminate three sources of repeated rule/skill injection** (#2578)
|
||||
- **fix(ask): close stdin for provider spawns to prevent hang in piped environments** (#2564)
|
||||
- **fix(post-tool-verifier): suppress non-actionable error token noise** (#2559)
|
||||
- **fix(openclaw): suppress late lifecycle alerts for completed/cleaned-up sessions** (#2554)
|
||||
- **fix(keyword-detector): suppress review-seed echo from tripping code-review alerts** (#2550)
|
||||
- **fix(purge): symlink stale plugin version dirs to prevent post-upgrade hook failures** (#2549)
|
||||
- **fix(deep-interview): replace five remaining hardcoded 20%/0.2 threshold signals (issue #2545)** (#2547)
|
||||
- **fix(stop-hook): cap echoed task prompt to 150 chars** (#2544)
|
||||
- **fix(mcp): wire wiki, shared_memory, skills, and deepinit tools into standalone server** (#2537)
|
||||
- **fix(openclaw): suppress stale tmux pane history in stop/session-end alerts** (#2535)
|
||||
- **fix(state-root): centralize OMC_STATE_DIR resolution across hook entrypoints** (#2533)
|
||||
- **fix: restrict setup stale-skill cleanup to OMC-managed dirs** (#2528)
|
||||
- **fix: reduce post-tool bash failure false positives** (#2526)
|
||||
- **fix: pipe multiline ask advisor prompts via stdin** (#2524)
|
||||
- **fix(config): warn on deprecated delegation routing** (#2522)
|
||||
- **fix(notifications): suppress usage-text tmux alert noise** (#2515)
|
||||
- **fix(psm): launch trusted sessions with initial prompt** (#2512)
|
||||
- **fix(openclaw): dedupe multi-pane native lifecycle bursts** (#2494)
|
||||
- **fix(tmux): keep HUD pane cleanup on the current tmux server** (#2492)
|
||||
- **fix(autopilot): scope runtime insight to the active session** (#2491)
|
||||
- **fix(team): scaleUp() should honor agentType launch contracts** (#2489)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **docs: document --plugin-dir and add CONTRIBUTING.md for local development** (#2399)
|
||||
- **docs(getting-started): document plugin + npm CLI as two coexisting surfaces** (#2367)
|
||||
- **docs: add omc symlink bootstrap and .mcp.json conflict resolution** (#2493)
|
||||
|
||||
### Other Changes
|
||||
|
||||
- **Make Ralph enforce real PRD and story review gates** (#2604)
|
||||
- **Keep PR review verification focused by default** (#2600)
|
||||
- **Reduce false-severe PR review noise in clean worktrees** (#2598)
|
||||
- **Guard shipped permission-handler parity at the runtime entrypoint** (#2596)
|
||||
- **Reduce approval stalls for safe repo inspection and single-test runs** (#2594)
|
||||
- **Harden live tmux keyword alerts against prompt/search noise** (#2585)
|
||||
- **Harden tmux keyword alerting against review/payload noise** (#2582)
|
||||
- **Fix stop-hook timeout enforcement for issue #2565** (#2569)
|
||||
- **Fix persistent-mode.cjs OMC_STATE_DIR state resolution mismatch** (#2531)
|
||||
- **Suppress stale repo-level CI replay noise after zero backlog** (#2530)
|
||||
- **Fix issue #2506: keep review/fix tmux sessions on their requested task context** (#2507)
|
||||
- **Fix issue #2504: suppress tmux keyword-alert false positives from PR review seed prompts** (#2505)
|
||||
- **Fix tmux keyword-alert noise from prompt-mode startup echo** (#2502)
|
||||
- **Back off zero-backlog follow-up spam on unchanged repo state** (#2498)
|
||||
|
||||
### Stats
|
||||
|
||||
- **9 PRs merged** | **0 new features** | **7 bug fixes** | **0 security/hardening improvements** | **0 other changes**
|
||||
- **50 PRs merged** | **4 new features** | **30 bug fixes** | **0 security/hardening improvements** | **14 other changes**
|
||||
|
||||
14
README.md
14
README.md
@@ -519,18 +519,18 @@ MIT
|
||||
|
||||
Top personal non-fork, non-archived repos from all-time OMC contributors (100+ GitHub stars).
|
||||
|
||||
- [@Yeachan-Heo](https://github.com/Yeachan-Heo) — [oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode) (⭐ 27k)
|
||||
- [@junhoyeo](https://github.com/junhoyeo) — [tokscale](https://github.com/junhoyeo/tokscale) (⭐ 1.7k)
|
||||
- [@psmux](https://github.com/psmux) — [psmux](https://github.com/psmux/psmux) (⭐ 1.1k)
|
||||
- [@Yeachan-Heo](https://github.com/Yeachan-Heo) — [oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode) (⭐ 28k)
|
||||
- [@junhoyeo](https://github.com/junhoyeo) — [tokscale](https://github.com/junhoyeo/tokscale) (⭐ 1.8k)
|
||||
- [@psmux](https://github.com/psmux) — [psmux](https://github.com/psmux/psmux) (⭐ 1.2k)
|
||||
- [@BowTiedSwan](https://github.com/BowTiedSwan) — [buildflow](https://github.com/BowTiedSwan/buildflow) (⭐ 290)
|
||||
- [@alohays](https://github.com/alohays) — [awesome-visual-representation-learning-with-transformers](https://github.com/alohays/awesome-visual-representation-learning-with-transformers) (⭐ 268)
|
||||
- [@alohays](https://github.com/alohays) — [awesome-visual-representation-learning-with-transformers](https://github.com/alohays/awesome-visual-representation-learning-with-transformers) (⭐ 269)
|
||||
- [@jcwleo](https://github.com/jcwleo) — [random-network-distillation-pytorch](https://github.com/jcwleo/random-network-distillation-pytorch) (⭐ 261)
|
||||
- [@emgeee](https://github.com/emgeee) — [mean-tutorial](https://github.com/emgeee/mean-tutorial) (⭐ 200)
|
||||
- [@shaun0927](https://github.com/shaun0927) — [openchrome](https://github.com/shaun0927/openchrome) (⭐ 173)
|
||||
- [@shaun0927](https://github.com/shaun0927) — [openchrome](https://github.com/shaun0927/openchrome) (⭐ 177)
|
||||
- [@anduinnn](https://github.com/anduinnn) — [HiFiNi-Auto-CheckIn](https://github.com/anduinnn/HiFiNi-Auto-CheckIn) (⭐ 170)
|
||||
- [@Znuff](https://github.com/Znuff) — [consolas-powerline](https://github.com/Znuff/consolas-powerline) (⭐ 146)
|
||||
- [@MeroZemory](https://github.com/MeroZemory) — [ida-multi-mcp](https://github.com/MeroZemory/ida-multi-mcp) (⭐ 134)
|
||||
- [@HaD0Yun](https://github.com/HaD0Yun) — [Gopeak-godot-mcp](https://github.com/HaD0Yun/Gopeak-godot-mcp) (⭐ 114)
|
||||
- [@MeroZemory](https://github.com/MeroZemory) — [ida-multi-mcp](https://github.com/MeroZemory/ida-multi-mcp) (⭐ 143)
|
||||
- [@HaD0Yun](https://github.com/HaD0Yun) — [Gopeak-godot-mcp](https://github.com/HaD0Yun/Gopeak-godot-mcp) (⭐ 122)
|
||||
|
||||
<!-- OMC:FEATURED-CONTRIBUTORS:END -->
|
||||
|
||||
|
||||
Binary file not shown.
3884
bridge/cli.cjs
3884
bridge/cli.cjs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -47,13 +47,13 @@ __export(bridge_entry_exports, {
|
||||
});
|
||||
module.exports = __toCommonJS(bridge_entry_exports);
|
||||
var import_fs12 = require("fs");
|
||||
var import_path12 = require("path");
|
||||
var import_path13 = require("path");
|
||||
var import_os3 = require("os");
|
||||
|
||||
// src/team/mcp-team-bridge.ts
|
||||
var import_child_process3 = require("child_process");
|
||||
var import_fs10 = require("fs");
|
||||
var import_path10 = require("path");
|
||||
var import_path11 = require("path");
|
||||
|
||||
// src/team/fs-utils.ts
|
||||
var import_fs = require("fs");
|
||||
@@ -109,7 +109,7 @@ function validateResolvedPath(resolvedPath, expectedBase) {
|
||||
|
||||
// src/team/task-file-ops.ts
|
||||
var import_fs4 = require("fs");
|
||||
var import_path5 = require("path");
|
||||
var import_path6 = require("path");
|
||||
|
||||
// src/utils/config-dir.ts
|
||||
var import_path2 = require("path");
|
||||
@@ -136,14 +136,68 @@ function getClaudeConfigDir() {
|
||||
}
|
||||
|
||||
// src/team/tmux-session.ts
|
||||
var import_child_process = require("child_process");
|
||||
var import_fs2 = require("fs");
|
||||
var import_path4 = require("path");
|
||||
var import_promises = __toESM(require("fs/promises"), 1);
|
||||
|
||||
// src/cli/tmux-utils.ts
|
||||
var import_child_process = require("child_process");
|
||||
var import_path3 = require("path");
|
||||
var import_util = require("util");
|
||||
var import_promises = __toESM(require("fs/promises"), 1);
|
||||
function tmuxEnv() {
|
||||
const { TMUX: _, ...env } = process.env;
|
||||
return env;
|
||||
}
|
||||
function resolveEnv(opts) {
|
||||
return opts?.stripTmux ? tmuxEnv() : process.env;
|
||||
}
|
||||
function quoteForCmd(arg) {
|
||||
if (arg.length === 0) return '""';
|
||||
if (!/[\s"%^&|<>()]/.test(arg)) return arg;
|
||||
return `"${arg.replace(/(["%])/g, "$1$1")}"`;
|
||||
}
|
||||
function resolveTmuxInvocation(args) {
|
||||
const resolvedBinary = resolveTmuxBinaryPath();
|
||||
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(resolvedBinary)) {
|
||||
const comspec = process.env.COMSPEC || "cmd.exe";
|
||||
const commandLine = [quoteForCmd(resolvedBinary), ...args.map(quoteForCmd)].join(" ");
|
||||
return {
|
||||
command: comspec,
|
||||
args: ["/d", "/s", "/c", commandLine]
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: resolvedBinary,
|
||||
args
|
||||
};
|
||||
}
|
||||
function tmuxExec(args, opts) {
|
||||
const { stripTmux: _, ...execOpts } = opts ?? {};
|
||||
const invocation = resolveTmuxInvocation(args);
|
||||
return (0, import_child_process.execFileSync)(invocation.command, invocation.args, { encoding: "utf-8", ...execOpts, env: resolveEnv(opts) });
|
||||
}
|
||||
function resolveTmuxBinaryPath() {
|
||||
if (process.platform !== "win32") {
|
||||
return "tmux";
|
||||
}
|
||||
try {
|
||||
const result = (0, import_child_process.spawnSync)("where", ["tmux"], {
|
||||
timeout: 5e3,
|
||||
encoding: "utf8"
|
||||
});
|
||||
if (result.status !== 0) return "tmux";
|
||||
const candidates = result.stdout?.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) ?? [];
|
||||
const first = candidates[0];
|
||||
if (first && ((0, import_path3.isAbsolute)(first) || import_path3.win32.isAbsolute(first))) {
|
||||
return first;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return "tmux";
|
||||
}
|
||||
|
||||
// src/team/tmux-session.ts
|
||||
var TMUX_SESSION_PREFIX = "omc-team";
|
||||
var promisifiedExec = (0, import_util.promisify)(import_child_process.exec);
|
||||
var promisifiedExecFile = (0, import_util.promisify)(import_child_process.execFile);
|
||||
function sanitizeName(name) {
|
||||
const sanitized = name.replace(/[^a-zA-Z0-9-]/g, "");
|
||||
if (sanitized.length === 0) {
|
||||
@@ -160,7 +214,7 @@ function sessionName(teamName, workerName) {
|
||||
function killSession(teamName, workerName) {
|
||||
const name = sessionName(teamName, workerName);
|
||||
try {
|
||||
(0, import_child_process.execFileSync)("tmux", ["kill-session", "-t", name], { stdio: "pipe", timeout: 5e3 });
|
||||
tmuxExec(["kill-session", "-t", name], { stripTmux: true, stdio: "pipe", timeout: 5e3 });
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -191,7 +245,7 @@ function isProcessAlive(pid) {
|
||||
var PLATFORM = process.platform;
|
||||
|
||||
// src/team/state-paths.ts
|
||||
var import_path4 = require("path");
|
||||
var import_path5 = require("path");
|
||||
function normalizeTaskFileStem(taskId) {
|
||||
const trimmed = String(taskId).trim().replace(/\.json$/i, "");
|
||||
if (/^task-\d+$/.test(trimmed)) return trimmed;
|
||||
@@ -232,15 +286,15 @@ var TeamPaths = {
|
||||
};
|
||||
function getTaskStoragePath(cwd, teamName, taskId) {
|
||||
if (taskId !== void 0) {
|
||||
return (0, import_path4.join)(cwd, TeamPaths.taskFile(teamName, taskId));
|
||||
return (0, import_path5.join)(cwd, TeamPaths.taskFile(teamName, taskId));
|
||||
}
|
||||
return (0, import_path4.join)(cwd, TeamPaths.tasks(teamName));
|
||||
return (0, import_path5.join)(cwd, TeamPaths.tasks(teamName));
|
||||
}
|
||||
function getLegacyTaskStoragePath(claudeConfigDir, teamName, taskId) {
|
||||
if (taskId !== void 0) {
|
||||
return (0, import_path4.join)(claudeConfigDir, "tasks", teamName, `${taskId}.json`);
|
||||
return (0, import_path5.join)(claudeConfigDir, "tasks", teamName, `${taskId}.json`);
|
||||
}
|
||||
return (0, import_path4.join)(claudeConfigDir, "tasks", teamName);
|
||||
return (0, import_path5.join)(claudeConfigDir, "tasks", teamName);
|
||||
}
|
||||
|
||||
// src/team/task-file-ops.ts
|
||||
@@ -249,7 +303,7 @@ function acquireTaskLock(teamName, taskId, opts) {
|
||||
const staleLockMs = opts?.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
|
||||
const dir = canonicalTasksDir(teamName, opts?.cwd);
|
||||
ensureDirWithMode(dir);
|
||||
const lockPath = (0, import_path5.join)(dir, `${sanitizeTaskId(taskId)}.lock`);
|
||||
const lockPath = (0, import_path6.join)(dir, `${sanitizeTaskId(taskId)}.lock`);
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
const fd = (0, import_fs4.openSync)(lockPath, import_fs4.constants.O_CREAT | import_fs4.constants.O_EXCL | import_fs4.constants.O_WRONLY, 384);
|
||||
@@ -311,27 +365,27 @@ function sanitizeTaskId(taskId) {
|
||||
function canonicalTasksDir(teamName, cwd) {
|
||||
const root = cwd ?? process.cwd();
|
||||
const dir = getTaskStoragePath(root, sanitizeName(teamName));
|
||||
validateResolvedPath(dir, (0, import_path5.join)(root, ".omc", "state", "team"));
|
||||
validateResolvedPath(dir, (0, import_path6.join)(root, ".omc", "state", "team"));
|
||||
return dir;
|
||||
}
|
||||
function legacyTasksDir(teamName) {
|
||||
const claudeConfigDir = getClaudeConfigDir();
|
||||
const dir = getLegacyTaskStoragePath(claudeConfigDir, sanitizeName(teamName));
|
||||
validateResolvedPath(dir, (0, import_path5.join)(claudeConfigDir, "tasks"));
|
||||
validateResolvedPath(dir, (0, import_path6.join)(claudeConfigDir, "tasks"));
|
||||
return dir;
|
||||
}
|
||||
function resolveTaskPathForRead(teamName, taskId, cwd) {
|
||||
const canonical = (0, import_path5.join)(canonicalTasksDir(teamName, cwd), `${sanitizeTaskId(taskId)}.json`);
|
||||
const canonical = (0, import_path6.join)(canonicalTasksDir(teamName, cwd), `${sanitizeTaskId(taskId)}.json`);
|
||||
if ((0, import_fs4.existsSync)(canonical)) return canonical;
|
||||
const legacy = (0, import_path5.join)(legacyTasksDir(teamName), `${sanitizeTaskId(taskId)}.json`);
|
||||
const legacy = (0, import_path6.join)(legacyTasksDir(teamName), `${sanitizeTaskId(taskId)}.json`);
|
||||
if ((0, import_fs4.existsSync)(legacy)) return legacy;
|
||||
return canonical;
|
||||
}
|
||||
function resolveTaskPathForWrite(teamName, taskId, cwd) {
|
||||
return (0, import_path5.join)(canonicalTasksDir(teamName, cwd), `${sanitizeTaskId(taskId)}.json`);
|
||||
return (0, import_path6.join)(canonicalTasksDir(teamName, cwd), `${sanitizeTaskId(taskId)}.json`);
|
||||
}
|
||||
function failureSidecarPath(teamName, taskId, cwd) {
|
||||
return (0, import_path5.join)(canonicalTasksDir(teamName, cwd), `${sanitizeTaskId(taskId)}.failure.json`);
|
||||
return (0, import_path6.join)(canonicalTasksDir(teamName, cwd), `${sanitizeTaskId(taskId)}.failure.json`);
|
||||
}
|
||||
function readTask(teamName, taskId, opts) {
|
||||
const filePath = resolveTaskPathForRead(teamName, taskId, opts?.cwd);
|
||||
@@ -467,30 +521,30 @@ function listTaskIds(teamName, opts) {
|
||||
|
||||
// src/team/inbox-outbox.ts
|
||||
var import_fs5 = require("fs");
|
||||
var import_path6 = require("path");
|
||||
var import_path7 = require("path");
|
||||
var MAX_INBOX_READ_SIZE = 10 * 1024 * 1024;
|
||||
function teamsDir(teamName) {
|
||||
const result = (0, import_path6.join)(getClaudeConfigDir(), "teams", sanitizeName(teamName));
|
||||
validateResolvedPath(result, (0, import_path6.join)(getClaudeConfigDir(), "teams"));
|
||||
const result = (0, import_path7.join)(getClaudeConfigDir(), "teams", sanitizeName(teamName));
|
||||
validateResolvedPath(result, (0, import_path7.join)(getClaudeConfigDir(), "teams"));
|
||||
return result;
|
||||
}
|
||||
function inboxPath(teamName, workerName) {
|
||||
return (0, import_path6.join)(teamsDir(teamName), "inbox", `${sanitizeName(workerName)}.jsonl`);
|
||||
return (0, import_path7.join)(teamsDir(teamName), "inbox", `${sanitizeName(workerName)}.jsonl`);
|
||||
}
|
||||
function inboxCursorPath(teamName, workerName) {
|
||||
return (0, import_path6.join)(teamsDir(teamName), "inbox", `${sanitizeName(workerName)}.offset`);
|
||||
return (0, import_path7.join)(teamsDir(teamName), "inbox", `${sanitizeName(workerName)}.offset`);
|
||||
}
|
||||
function outboxPath(teamName, workerName) {
|
||||
return (0, import_path6.join)(teamsDir(teamName), "outbox", `${sanitizeName(workerName)}.jsonl`);
|
||||
return (0, import_path7.join)(teamsDir(teamName), "outbox", `${sanitizeName(workerName)}.jsonl`);
|
||||
}
|
||||
function signalPath(teamName, workerName) {
|
||||
return (0, import_path6.join)(teamsDir(teamName), "signals", `${sanitizeName(workerName)}.shutdown`);
|
||||
return (0, import_path7.join)(teamsDir(teamName), "signals", `${sanitizeName(workerName)}.shutdown`);
|
||||
}
|
||||
function drainSignalPath(teamName, workerName) {
|
||||
return (0, import_path6.join)(teamsDir(teamName), "signals", `${sanitizeName(workerName)}.drain`);
|
||||
return (0, import_path7.join)(teamsDir(teamName), "signals", `${sanitizeName(workerName)}.drain`);
|
||||
}
|
||||
function ensureDir(filePath) {
|
||||
const dir = (0, import_path6.dirname)(filePath);
|
||||
const dir = (0, import_path7.dirname)(filePath);
|
||||
ensureDirWithMode(dir);
|
||||
}
|
||||
function appendOutbox(teamName, workerName, message) {
|
||||
@@ -634,7 +688,7 @@ function deleteDrainSignal(teamName, workerName) {
|
||||
|
||||
// src/team/team-registration.ts
|
||||
var import_fs7 = require("fs");
|
||||
var import_path7 = require("path");
|
||||
var import_path8 = require("path");
|
||||
|
||||
// src/lib/file-lock.ts
|
||||
var import_fs6 = require("fs");
|
||||
@@ -785,13 +839,13 @@ function withFileLockSync(lockPath, fn, opts) {
|
||||
|
||||
// src/team/team-registration.ts
|
||||
function configPath(teamName) {
|
||||
const result = (0, import_path7.join)(getClaudeConfigDir(), "teams", sanitizeName(teamName), "config.json");
|
||||
validateResolvedPath(result, (0, import_path7.join)(getClaudeConfigDir(), "teams"));
|
||||
const result = (0, import_path8.join)(getClaudeConfigDir(), "teams", sanitizeName(teamName), "config.json");
|
||||
validateResolvedPath(result, (0, import_path8.join)(getClaudeConfigDir(), "teams"));
|
||||
return result;
|
||||
}
|
||||
function shadowRegistryPath(workingDirectory) {
|
||||
const result = (0, import_path7.join)(workingDirectory, ".omc", "state", "team-mcp-workers.json");
|
||||
validateResolvedPath(result, (0, import_path7.join)(workingDirectory, ".omc", "state"));
|
||||
const result = (0, import_path8.join)(workingDirectory, ".omc", "state", "team-mcp-workers.json");
|
||||
validateResolvedPath(result, (0, import_path8.join)(workingDirectory, ".omc", "state"));
|
||||
return result;
|
||||
}
|
||||
function unregisterMcpWorker(teamName, workerName, workingDirectory) {
|
||||
@@ -855,9 +909,9 @@ function listMcpWorkers(teamName, workingDirectory) {
|
||||
|
||||
// src/team/heartbeat.ts
|
||||
var import_fs8 = require("fs");
|
||||
var import_path8 = require("path");
|
||||
var import_path9 = require("path");
|
||||
function heartbeatPath(workingDirectory, teamName, workerName) {
|
||||
return (0, import_path8.join)(workingDirectory, ".omc", "state", "team-bridge", sanitizeName(teamName), `${sanitizeName(workerName)}.heartbeat.json`);
|
||||
return (0, import_path9.join)(workingDirectory, ".omc", "state", "team-bridge", sanitizeName(teamName), `${sanitizeName(workerName)}.heartbeat.json`);
|
||||
}
|
||||
function writeHeartbeat(workingDirectory, data) {
|
||||
const filePath = heartbeatPath(workingDirectory, data.teamName, data.workerName);
|
||||
@@ -1054,7 +1108,7 @@ function getBuiltinExternalDefaultModel(provider) {
|
||||
|
||||
// src/team/team-status.ts
|
||||
var import_fs9 = require("fs");
|
||||
var import_path9 = require("path");
|
||||
var import_path10 = require("path");
|
||||
|
||||
// src/team/usage-tracker.ts
|
||||
var import_node_fs = require("node:fs");
|
||||
@@ -1143,7 +1197,7 @@ function emptyUsageReport(teamName) {
|
||||
function peekRecentOutboxMessages(teamName, workerName, maxMessages = 10) {
|
||||
const safeName = sanitizeName(teamName);
|
||||
const safeWorker = sanitizeName(workerName);
|
||||
const outboxPath2 = (0, import_path9.join)(getClaudeConfigDir(), "teams", safeName, "outbox", `${safeWorker}.jsonl`);
|
||||
const outboxPath2 = (0, import_path10.join)(getClaudeConfigDir(), "teams", safeName, "outbox", `${safeWorker}.jsonl`);
|
||||
if (!(0, import_fs9.existsSync)(outboxPath2)) return [];
|
||||
try {
|
||||
const content = (0, import_fs9.readFileSync)(outboxPath2, "utf-8");
|
||||
@@ -1428,18 +1482,18 @@ function buildTaskPrompt(task, messages, config) {
|
||||
return result;
|
||||
}
|
||||
function writePromptFile(config, taskId, prompt) {
|
||||
const dir = (0, import_path10.join)(config.workingDirectory, ".omc", "prompts");
|
||||
const dir = (0, import_path11.join)(config.workingDirectory, ".omc", "prompts");
|
||||
ensureDirWithMode(dir);
|
||||
const filename = `team-${config.teamName}-task-${taskId}-${Date.now()}.md`;
|
||||
const filePath = (0, import_path10.join)(dir, filename);
|
||||
const filePath = (0, import_path11.join)(dir, filename);
|
||||
writeFileWithMode(filePath, prompt);
|
||||
return filePath;
|
||||
}
|
||||
function getOutputPath(config, taskId) {
|
||||
const dir = (0, import_path10.join)(config.workingDirectory, ".omc", "outputs");
|
||||
const dir = (0, import_path11.join)(config.workingDirectory, ".omc", "outputs");
|
||||
ensureDirWithMode(dir);
|
||||
const suffix = Math.random().toString(36).slice(2, 8);
|
||||
return (0, import_path10.join)(
|
||||
return (0, import_path11.join)(
|
||||
dir,
|
||||
`team-${config.teamName}-task-${taskId}-${Date.now()}-${suffix}.md`
|
||||
);
|
||||
@@ -2019,7 +2073,7 @@ var import_crypto = require("crypto");
|
||||
var import_child_process4 = require("child_process");
|
||||
var import_fs11 = require("fs");
|
||||
var import_os2 = require("os");
|
||||
var import_path11 = require("path");
|
||||
var import_path12 = require("path");
|
||||
var MAX_WORKTREE_CACHE_SIZE = 8;
|
||||
var worktreeCacheMap = /* @__PURE__ */ new Map();
|
||||
function getWorktreeRoot(cwd) {
|
||||
@@ -2052,15 +2106,15 @@ function getWorktreeRoot(cwd) {
|
||||
|
||||
// src/team/bridge-entry.ts
|
||||
function validateConfigPath(configPath2, homeDir, claudeConfigDir) {
|
||||
const resolved = (0, import_path12.resolve)(configPath2);
|
||||
const resolved = (0, import_path13.resolve)(configPath2);
|
||||
const isUnderHome = resolved.startsWith(homeDir + "/") || resolved === homeDir;
|
||||
const normalizedConfigDir = (0, import_path12.resolve)(claudeConfigDir);
|
||||
const normalizedOmcDir = (0, import_path12.resolve)(homeDir, ".omc");
|
||||
const normalizedConfigDir = (0, import_path13.resolve)(claudeConfigDir);
|
||||
const normalizedOmcDir = (0, import_path13.resolve)(homeDir, ".omc");
|
||||
const hasOmcComponent = resolved.includes("/.omc/") || resolved.endsWith("/.omc");
|
||||
const isTrustedSubpath = resolved === normalizedConfigDir || resolved.startsWith(normalizedConfigDir + "/") || resolved === normalizedOmcDir || resolved.startsWith(normalizedOmcDir + "/") || hasOmcComponent;
|
||||
if (!isUnderHome || !isTrustedSubpath) return false;
|
||||
try {
|
||||
const parentDir = (0, import_path12.resolve)(resolved, "..");
|
||||
const parentDir = (0, import_path13.resolve)(resolved, "..");
|
||||
const realParent = (0, import_fs12.realpathSync)(parentDir);
|
||||
if (!realParent.startsWith(homeDir + "/") && realParent !== homeDir) {
|
||||
return false;
|
||||
@@ -2095,7 +2149,7 @@ function main() {
|
||||
console.error("Usage: node bridge-entry.js --config <path-to-config.json>");
|
||||
process.exit(1);
|
||||
}
|
||||
const configPath2 = (0, import_path12.resolve)(process.argv[configIdx + 1]);
|
||||
const configPath2 = (0, import_path13.resolve)(process.argv[configIdx + 1]);
|
||||
const home = (0, import_os3.homedir)();
|
||||
const claudeConfigDir = getClaudeConfigDir();
|
||||
if (!validateConfigPath(configPath2, home, claudeConfigDir)) {
|
||||
|
||||
@@ -17776,17 +17776,15 @@ var StdioServerTransport = class {
|
||||
|
||||
// src/mcp/team-server.ts
|
||||
var import_node_crypto = require("node:crypto");
|
||||
var import_child_process4 = require("child_process");
|
||||
var import_path6 = require("path");
|
||||
var import_child_process3 = require("child_process");
|
||||
var import_path7 = require("path");
|
||||
var import_url = require("url");
|
||||
var import_fs7 = require("fs");
|
||||
var import_promises2 = require("fs/promises");
|
||||
|
||||
// src/team/tmux-session.ts
|
||||
var import_child_process = require("child_process");
|
||||
var import_fs = require("fs");
|
||||
var import_path = require("path");
|
||||
var import_util5 = require("util");
|
||||
var import_path2 = require("path");
|
||||
var import_promises = __toESM(require("fs/promises"), 1);
|
||||
|
||||
// src/team/team-name.ts
|
||||
@@ -17800,17 +17798,85 @@ function validateTeamName(teamName) {
|
||||
return teamName;
|
||||
}
|
||||
|
||||
// src/team/tmux-session.ts
|
||||
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
var promisifiedExec = (0, import_util5.promisify)(import_child_process.exec);
|
||||
var promisifiedExecFile = (0, import_util5.promisify)(import_child_process.execFile);
|
||||
async function tmuxAsync(args) {
|
||||
// src/cli/tmux-utils.ts
|
||||
var import_child_process = require("child_process");
|
||||
var import_path = require("path");
|
||||
var import_util5 = require("util");
|
||||
function tmuxEnv() {
|
||||
const { TMUX: _, ...env } = process.env;
|
||||
return env;
|
||||
}
|
||||
function resolveEnv(opts) {
|
||||
return opts?.stripTmux ? tmuxEnv() : process.env;
|
||||
}
|
||||
function quoteForCmd(arg) {
|
||||
if (arg.length === 0) return '""';
|
||||
if (!/[\s"%^&|<>()]/.test(arg)) return arg;
|
||||
return `"${arg.replace(/(["%])/g, "$1$1")}"`;
|
||||
}
|
||||
function resolveTmuxInvocation(args) {
|
||||
const resolvedBinary = resolveTmuxBinaryPath();
|
||||
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(resolvedBinary)) {
|
||||
const comspec = process.env.COMSPEC || "cmd.exe";
|
||||
const commandLine = [quoteForCmd(resolvedBinary), ...args.map(quoteForCmd)].join(" ");
|
||||
return {
|
||||
command: comspec,
|
||||
args: ["/d", "/s", "/c", commandLine]
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: resolvedBinary,
|
||||
args
|
||||
};
|
||||
}
|
||||
async function tmuxExecAsync(args, opts) {
|
||||
const { stripTmux: _, timeout, ...rest } = opts ?? {};
|
||||
const invocation = resolveTmuxInvocation(args);
|
||||
return (0, import_util5.promisify)(import_child_process.execFile)(invocation.command, invocation.args, {
|
||||
encoding: "utf-8",
|
||||
env: resolveEnv(opts),
|
||||
...timeout !== void 0 ? { timeout } : {},
|
||||
...rest
|
||||
});
|
||||
}
|
||||
async function tmuxShellAsync(command, opts) {
|
||||
const { stripTmux: _, timeout, ...rest } = opts ?? {};
|
||||
return (0, import_util5.promisify)(import_child_process.exec)(`tmux ${command}`, {
|
||||
encoding: "utf-8",
|
||||
env: resolveEnv(opts),
|
||||
...timeout !== void 0 ? { timeout } : {},
|
||||
...rest
|
||||
});
|
||||
}
|
||||
async function tmuxCmdAsync(args, opts) {
|
||||
if (args.some((a) => a.includes("#{"))) {
|
||||
const escaped = args.map((a) => "'" + a.replace(/'/g, "'\\''") + "'").join(" ");
|
||||
return promisifiedExec(`tmux ${escaped}`);
|
||||
return tmuxShellAsync(escaped, opts);
|
||||
}
|
||||
return promisifiedExecFile("tmux", args);
|
||||
return tmuxExecAsync(args, opts);
|
||||
}
|
||||
function resolveTmuxBinaryPath() {
|
||||
if (process.platform !== "win32") {
|
||||
return "tmux";
|
||||
}
|
||||
try {
|
||||
const result = (0, import_child_process.spawnSync)("where", ["tmux"], {
|
||||
timeout: 5e3,
|
||||
encoding: "utf8"
|
||||
});
|
||||
if (result.status !== 0) return "tmux";
|
||||
const candidates = result.stdout?.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) ?? [];
|
||||
const first = candidates[0];
|
||||
if (first && ((0, import_path.isAbsolute)(first) || import_path.win32.isAbsolute(first))) {
|
||||
return first;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return "tmux";
|
||||
}
|
||||
|
||||
// src/team/tmux-session.ts
|
||||
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
function sanitizeName(name) {
|
||||
const sanitized = name.replace(/[^a-zA-Z0-9-]/g, "");
|
||||
if (sanitized.length === 0) {
|
||||
@@ -17824,9 +17890,9 @@ function sanitizeName(name) {
|
||||
function normalizeTmuxCapture(value) {
|
||||
return value.replace(/\r/g, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
async function capturePaneAsync(paneId, execFileAsync2) {
|
||||
async function capturePaneAsync(paneId) {
|
||||
try {
|
||||
const result = await execFileAsync2("tmux", ["capture-pane", "-t", paneId, "-p", "-S", "-80"]);
|
||||
const result = await tmuxExecAsync(["capture-pane", "-t", paneId, "-p", "-S", "-80"]);
|
||||
return result.stdout;
|
||||
} catch {
|
||||
return "";
|
||||
@@ -17871,7 +17937,7 @@ function paneTailContainsLiteralLine(captured, text) {
|
||||
}
|
||||
async function paneInCopyMode(paneId) {
|
||||
try {
|
||||
const result = await tmuxAsync(["display-message", "-t", paneId, "-p", "#{pane_in_mode}"]);
|
||||
const result = await tmuxCmdAsync(["display-message", "-t", paneId, "-p", "#{pane_in_mode}"]);
|
||||
return result.stdout.trim() === "1";
|
||||
} catch {
|
||||
return false;
|
||||
@@ -17894,47 +17960,43 @@ async function sendToWorker(_sessionName, paneId, message) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const sendKey = async (key) => {
|
||||
await execFileAsync2("tmux", ["send-keys", "-t", paneId, key]);
|
||||
await tmuxExecAsync(["send-keys", "-t", paneId, key]);
|
||||
};
|
||||
if (await paneInCopyMode(paneId)) {
|
||||
return false;
|
||||
}
|
||||
const initialCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
const initialCapture = await capturePaneAsync(paneId);
|
||||
const paneBusy = paneHasActiveTask(initialCapture);
|
||||
if (paneHasTrustPrompt(initialCapture)) {
|
||||
await sendKey("C-m");
|
||||
await sleep2(120);
|
||||
await sleep(120);
|
||||
await sendKey("C-m");
|
||||
await sleep2(200);
|
||||
await sleep(200);
|
||||
}
|
||||
await execFileAsync2("tmux", ["send-keys", "-t", paneId, "-l", "--", message]);
|
||||
await sleep2(150);
|
||||
await tmuxExecAsync(["send-keys", "-t", paneId, "-l", "--", message]);
|
||||
await sleep(150);
|
||||
const submitRounds = 6;
|
||||
for (let round = 0; round < submitRounds; round++) {
|
||||
await sleep2(100);
|
||||
await sleep(100);
|
||||
if (round === 0 && paneBusy) {
|
||||
await sendKey("Tab");
|
||||
await sleep2(80);
|
||||
await sleep(80);
|
||||
await sendKey("C-m");
|
||||
} else {
|
||||
await sendKey("C-m");
|
||||
await sleep2(200);
|
||||
await sleep(200);
|
||||
await sendKey("C-m");
|
||||
}
|
||||
await sleep2(140);
|
||||
const checkCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
await sleep(140);
|
||||
const checkCapture = await capturePaneAsync(paneId);
|
||||
if (!paneTailContainsLiteralLine(checkCapture, message)) return true;
|
||||
await sleep2(140);
|
||||
await sleep(140);
|
||||
}
|
||||
if (await paneInCopyMode(paneId)) {
|
||||
return false;
|
||||
}
|
||||
const finalCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
const finalCapture = await capturePaneAsync(paneId);
|
||||
const paneModeBeforeAdaptiveRetry = await paneInCopyMode(paneId);
|
||||
if (shouldAttemptAdaptiveRetry({
|
||||
paneBusy,
|
||||
@@ -17947,18 +18009,18 @@ async function sendToWorker(_sessionName, paneId, message) {
|
||||
return false;
|
||||
}
|
||||
await sendKey("C-u");
|
||||
await sleep2(80);
|
||||
await sleep(80);
|
||||
if (await paneInCopyMode(paneId)) {
|
||||
return false;
|
||||
}
|
||||
await execFileAsync2("tmux", ["send-keys", "-t", paneId, "-l", "--", message]);
|
||||
await sleep2(120);
|
||||
await tmuxExecAsync(["send-keys", "-t", paneId, "-l", "--", message]);
|
||||
await sleep(120);
|
||||
for (let round = 0; round < 4; round++) {
|
||||
await sendKey("C-m");
|
||||
await sleep2(180);
|
||||
await sleep(180);
|
||||
await sendKey("C-m");
|
||||
await sleep2(140);
|
||||
const retryCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
await sleep(140);
|
||||
const retryCapture = await capturePaneAsync(paneId);
|
||||
if (!paneTailContainsLiteralLine(retryCapture, message)) return true;
|
||||
}
|
||||
}
|
||||
@@ -17966,10 +18028,10 @@ async function sendToWorker(_sessionName, paneId, message) {
|
||||
return false;
|
||||
}
|
||||
await sendKey("C-m");
|
||||
await sleep2(120);
|
||||
await sleep(120);
|
||||
await sendKey("C-m");
|
||||
await sleep2(140);
|
||||
const finalCheckCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
await sleep(140);
|
||||
const finalCheckCapture = await capturePaneAsync(paneId);
|
||||
if (!finalCheckCapture || finalCheckCapture.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
@@ -17980,7 +18042,7 @@ async function sendToWorker(_sessionName, paneId, message) {
|
||||
}
|
||||
async function isWorkerAlive(paneId) {
|
||||
try {
|
||||
const result = await tmuxAsync([
|
||||
const result = await tmuxCmdAsync([
|
||||
"display-message",
|
||||
"-t",
|
||||
paneId,
|
||||
@@ -17995,7 +18057,7 @@ async function isWorkerAlive(paneId) {
|
||||
async function killWorkerPanes(opts) {
|
||||
const { paneIds, leaderPaneId, teamName, cwd, graceMs = 1e4 } = opts;
|
||||
if (!paneIds.length) return;
|
||||
const shutdownPath = (0, import_path.join)(cwd, ".omc", "state", "team", teamName, "shutdown.json");
|
||||
const shutdownPath = (0, import_path2.join)(cwd, ".omc", "state", "team", teamName, "shutdown.json");
|
||||
try {
|
||||
await import_promises.default.writeFile(shutdownPath, JSON.stringify({ requestedAt: Date.now() }));
|
||||
const aliveChecks = await Promise.all(paneIds.map((id) => isWorkerAlive(id)));
|
||||
@@ -18004,28 +18066,22 @@ async function killWorkerPanes(opts) {
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
for (const paneId of paneIds) {
|
||||
if (paneId === leaderPaneId) continue;
|
||||
try {
|
||||
await execFileAsync2("tmux", ["kill-pane", "-t", paneId]);
|
||||
await tmuxExecAsync(["kill-pane", "-t", paneId]);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
async function killTeamSession(sessionName, workerPaneIds, leaderPaneId, options = {}) {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
const sessionMode = options.sessionMode ?? (sessionName.includes(":") ? "split-pane" : "detached-session");
|
||||
if (sessionMode === "split-pane") {
|
||||
if (!workerPaneIds?.length) return;
|
||||
for (const id of workerPaneIds) {
|
||||
if (id === leaderPaneId) continue;
|
||||
try {
|
||||
await execFileAsync2("tmux", ["kill-pane", "-t", id]);
|
||||
await tmuxExecAsync(["kill-pane", "-t", id]);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -18033,7 +18089,7 @@ async function killTeamSession(sessionName, workerPaneIds, leaderPaneId, options
|
||||
}
|
||||
if (sessionMode === "dedicated-window") {
|
||||
try {
|
||||
await execFileAsync2("tmux", ["kill-window", "-t", sessionName]);
|
||||
await tmuxExecAsync(["kill-window", "-t", sessionName]);
|
||||
} catch {
|
||||
}
|
||||
return;
|
||||
@@ -18041,7 +18097,7 @@ async function killTeamSession(sessionName, workerPaneIds, leaderPaneId, options
|
||||
const sessionTarget = sessionName.split(":")[0] ?? sessionName;
|
||||
if (process.env.OMC_TEAM_ALLOW_KILL_CURRENT_SESSION !== "1" && process.env.TMUX) {
|
||||
try {
|
||||
const current = await tmuxAsync(["display-message", "-p", "#S"]);
|
||||
const current = await tmuxCmdAsync(["display-message", "-p", "#S"]);
|
||||
const currentSessionName = current.stdout.trim();
|
||||
if (currentSessionName && currentSessionName === sessionTarget) {
|
||||
return;
|
||||
@@ -18050,25 +18106,24 @@ async function killTeamSession(sessionName, workerPaneIds, leaderPaneId, options
|
||||
}
|
||||
}
|
||||
try {
|
||||
await execFileAsync2("tmux", ["kill-session", "-t", sessionTarget]);
|
||||
await tmuxExecAsync(["kill-session", "-t", sessionTarget]);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
// src/team/idle-nudge.ts
|
||||
var import_child_process2 = require("child_process");
|
||||
var DEFAULT_NUDGE_CONFIG = {
|
||||
delayMs: 3e4,
|
||||
maxCount: 3,
|
||||
message: "Continue working on your assigned task and report concrete progress (not ACK-only)."
|
||||
};
|
||||
function capturePane(paneId) {
|
||||
return new Promise((resolve2) => {
|
||||
(0, import_child_process2.execFile)("tmux", ["capture-pane", "-t", paneId, "-p", "-S", "-80"], (err, stdout) => {
|
||||
if (err) resolve2("");
|
||||
else resolve2(stdout ?? "");
|
||||
});
|
||||
});
|
||||
async function capturePane(paneId) {
|
||||
try {
|
||||
const result = await tmuxExecAsync(["capture-pane", "-t", paneId, "-p", "-S", "-80"]);
|
||||
return result.stdout ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
async function isPaneIdle(paneId) {
|
||||
const captured = await capturePane(paneId);
|
||||
@@ -18146,7 +18201,7 @@ var NudgeTracker = class {
|
||||
|
||||
// src/mcp/team-job-convergence.ts
|
||||
var import_fs5 = require("fs");
|
||||
var import_path3 = require("path");
|
||||
var import_path4 = require("path");
|
||||
|
||||
// src/team/git-worktree.ts
|
||||
var import_node_fs = require("node:fs");
|
||||
@@ -18155,9 +18210,9 @@ var import_node_child_process = require("node:child_process");
|
||||
|
||||
// src/team/fs-utils.ts
|
||||
var import_fs2 = require("fs");
|
||||
var import_path2 = require("path");
|
||||
var import_path3 = require("path");
|
||||
function atomicWriteJson(filePath, data, mode = 384) {
|
||||
const dir = (0, import_path2.dirname)(filePath);
|
||||
const dir = (0, import_path3.dirname)(filePath);
|
||||
if (!(0, import_fs2.existsSync)(dir)) (0, import_fs2.mkdirSync)(dir, { recursive: true, mode: 448 });
|
||||
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
|
||||
(0, import_fs2.writeFileSync)(tmpPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode });
|
||||
@@ -18171,25 +18226,25 @@ function safeRealpath(p) {
|
||||
return (0, import_fs2.realpathSync)(p);
|
||||
} catch {
|
||||
const segments = [];
|
||||
let current = (0, import_path2.resolve)(p);
|
||||
let current = (0, import_path3.resolve)(p);
|
||||
while (!(0, import_fs2.existsSync)(current)) {
|
||||
segments.unshift((0, import_path2.basename)(current));
|
||||
const parent = (0, import_path2.dirname)(current);
|
||||
segments.unshift((0, import_path3.basename)(current));
|
||||
const parent = (0, import_path3.dirname)(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
try {
|
||||
return (0, import_path2.join)((0, import_fs2.realpathSync)(current), ...segments);
|
||||
return (0, import_path3.join)((0, import_fs2.realpathSync)(current), ...segments);
|
||||
} catch {
|
||||
return (0, import_path2.resolve)(p);
|
||||
return (0, import_path3.resolve)(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
function validateResolvedPath(resolvedPath, expectedBase) {
|
||||
const absResolved = safeRealpath(resolvedPath);
|
||||
const absBase = safeRealpath(expectedBase);
|
||||
const rel = (0, import_path2.relative)(absBase, absResolved);
|
||||
if (rel.startsWith("..") || (0, import_path2.resolve)(absBase, rel) !== absResolved) {
|
||||
const rel = (0, import_path3.relative)(absBase, absResolved);
|
||||
if (rel.startsWith("..") || (0, import_path3.resolve)(absBase, rel) !== absResolved) {
|
||||
throw new Error(`Path traversal detected: "${resolvedPath}" escapes base "${expectedBase}"`);
|
||||
}
|
||||
}
|
||||
@@ -18222,10 +18277,10 @@ var path2 = __toESM(require("path"), 1);
|
||||
var import_fs3 = require("fs");
|
||||
|
||||
// src/platform/process-utils.ts
|
||||
var import_child_process3 = require("child_process");
|
||||
var import_child_process2 = require("child_process");
|
||||
var import_util6 = require("util");
|
||||
var fsPromises = __toESM(require("fs/promises"), 1);
|
||||
var execFileAsync = (0, import_util6.promisify)(import_child_process3.execFile);
|
||||
var execFileAsync = (0, import_util6.promisify)(import_child_process2.execFile);
|
||||
function isProcessAlive(pid) {
|
||||
if (!Number.isInteger(pid) || pid <= 0) return false;
|
||||
try {
|
||||
@@ -18429,7 +18484,7 @@ function cleanupTeamWorktrees(teamName, repoRoot) {
|
||||
|
||||
// src/mcp/team-job-convergence.ts
|
||||
function readResultArtifact(omcJobsDir, jobId) {
|
||||
const artifactPath = (0, import_path3.join)(omcJobsDir, `${jobId}-result.json`);
|
||||
const artifactPath = (0, import_path4.join)(omcJobsDir, `${jobId}-result.json`);
|
||||
if (!(0, import_fs5.existsSync)(artifactPath)) return { kind: "none" };
|
||||
let raw;
|
||||
try {
|
||||
@@ -18495,7 +18550,7 @@ function clearScopedTeamState(job) {
|
||||
} catch (error2) {
|
||||
return `team state cleanup skipped (invalid teamName): ${error2 instanceof Error ? error2.message : String(error2)}`;
|
||||
}
|
||||
const stateDir = (0, import_path3.join)(job.cwd, ".omc", "state", "team", job.teamName);
|
||||
const stateDir = (0, import_path4.join)(job.cwd, ".omc", "state", "team", job.teamName);
|
||||
let worktreeMessage = "worktree cleanup skipped.";
|
||||
try {
|
||||
cleanupTeamWorktrees(job.teamName, job.cwd);
|
||||
@@ -18515,20 +18570,20 @@ function clearScopedTeamState(job) {
|
||||
}
|
||||
|
||||
// src/utils/paths.ts
|
||||
var import_path5 = require("path");
|
||||
var import_path6 = require("path");
|
||||
var import_fs6 = require("fs");
|
||||
var import_os2 = require("os");
|
||||
|
||||
// src/utils/config-dir.ts
|
||||
var import_path4 = require("path");
|
||||
var import_path5 = require("path");
|
||||
var import_os = require("os");
|
||||
|
||||
// src/utils/paths.ts
|
||||
function getStateDir() {
|
||||
if (process.platform === "win32") {
|
||||
return process.env.LOCALAPPDATA || (0, import_path5.join)((0, import_os2.homedir)(), "AppData", "Local");
|
||||
return process.env.LOCALAPPDATA || (0, import_path6.join)((0, import_os2.homedir)(), "AppData", "Local");
|
||||
}
|
||||
return process.env.XDG_STATE_HOME || (0, import_path5.join)((0, import_os2.homedir)(), ".local", "state");
|
||||
return process.env.XDG_STATE_HOME || (0, import_path6.join)((0, import_os2.homedir)(), ".local", "state");
|
||||
}
|
||||
function prefersXdgOmcDirs() {
|
||||
return process.platform !== "win32" && process.platform !== "darwin";
|
||||
@@ -18540,20 +18595,20 @@ function getUserHomeDir() {
|
||||
return process.env.HOME || (0, import_os2.homedir)();
|
||||
}
|
||||
function getLegacyOmcDir() {
|
||||
return (0, import_path5.join)(getUserHomeDir(), ".omc");
|
||||
return (0, import_path6.join)(getUserHomeDir(), ".omc");
|
||||
}
|
||||
function getGlobalOmcStateRoot() {
|
||||
const explicitRoot = process.env.OMC_HOME?.trim();
|
||||
if (explicitRoot) {
|
||||
return (0, import_path5.join)(explicitRoot, "state");
|
||||
return (0, import_path6.join)(explicitRoot, "state");
|
||||
}
|
||||
if (prefersXdgOmcDirs()) {
|
||||
return (0, import_path5.join)(getStateDir(), "omc");
|
||||
return (0, import_path6.join)(getStateDir(), "omc");
|
||||
}
|
||||
return (0, import_path5.join)(getLegacyOmcDir(), "state");
|
||||
return (0, import_path6.join)(getLegacyOmcDir(), "state");
|
||||
}
|
||||
function getGlobalOmcStatePath(...segments) {
|
||||
return (0, import_path5.join)(getGlobalOmcStateRoot(), ...segments);
|
||||
return (0, import_path6.join)(getGlobalOmcStateRoot(), ...segments);
|
||||
}
|
||||
var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
||||
|
||||
@@ -18654,19 +18709,19 @@ function createDeprecatedCliOnlyEnvelopeWithArgs(toolName, args) {
|
||||
function persistJob(jobId, job) {
|
||||
try {
|
||||
if (!(0, import_fs7.existsSync)(OMC_JOBS_DIR)) (0, import_fs7.mkdirSync)(OMC_JOBS_DIR, { recursive: true });
|
||||
(0, import_fs7.writeFileSync)((0, import_path6.join)(OMC_JOBS_DIR, `${jobId}.json`), JSON.stringify(job), "utf-8");
|
||||
(0, import_fs7.writeFileSync)((0, import_path7.join)(OMC_JOBS_DIR, `${jobId}.json`), JSON.stringify(job), "utf-8");
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
function loadJobFromDisk(jobId) {
|
||||
try {
|
||||
return JSON.parse((0, import_fs7.readFileSync)((0, import_path6.join)(OMC_JOBS_DIR, `${jobId}.json`), "utf-8"));
|
||||
return JSON.parse((0, import_fs7.readFileSync)((0, import_path7.join)(OMC_JOBS_DIR, `${jobId}.json`), "utf-8"));
|
||||
} catch {
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
async function loadPaneIds(jobId) {
|
||||
const p = (0, import_path6.join)(OMC_JOBS_DIR, `${jobId}-panes.json`);
|
||||
const p = (0, import_path7.join)(OMC_JOBS_DIR, `${jobId}-panes.json`);
|
||||
try {
|
||||
return JSON.parse(await (0, import_promises2.readFile)(p, "utf-8"));
|
||||
} catch {
|
||||
@@ -18729,10 +18784,10 @@ async function handleStart(args) {
|
||||
const input = startSchema.parse(args);
|
||||
validateTeamName(input.teamName);
|
||||
const jobId = `omc-${Date.now().toString(36)}${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
|
||||
const runtimeCliPath = (0, import_path6.join)(__ownDir, "runtime-cli.cjs");
|
||||
const runtimeCliPath = (0, import_path7.join)(__ownDir, "runtime-cli.cjs");
|
||||
const job = { status: "running", startedAt: Date.now(), teamName: input.teamName, cwd: input.cwd };
|
||||
omcTeamJobs.set(jobId, job);
|
||||
const child = (0, import_child_process4.spawn)("node", [runtimeCliPath], {
|
||||
const child = (0, import_child_process3.spawn)("node", [runtimeCliPath], {
|
||||
env: { ...process.env, OMC_JOB_ID: jobId, OMC_JOBS_DIR },
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
});
|
||||
|
||||
381
bridge/team.js
381
bridge/team.js
@@ -1579,6 +1579,103 @@ var init_team_name = __esm({
|
||||
}
|
||||
});
|
||||
|
||||
// src/cli/tmux-utils.ts
|
||||
import {
|
||||
exec,
|
||||
execFile,
|
||||
execFileSync,
|
||||
execSync,
|
||||
spawnSync
|
||||
} from "child_process";
|
||||
import { basename as basename2, isAbsolute as isAbsolute2, win32 as win32Path } from "path";
|
||||
import { promisify } from "util";
|
||||
function tmuxEnv() {
|
||||
const { TMUX: _, ...env } = process.env;
|
||||
return env;
|
||||
}
|
||||
function resolveEnv(opts) {
|
||||
return opts?.stripTmux ? tmuxEnv() : process.env;
|
||||
}
|
||||
function quoteForCmd(arg) {
|
||||
if (arg.length === 0) return '""';
|
||||
if (!/[\s"%^&|<>()]/.test(arg)) return arg;
|
||||
return `"${arg.replace(/(["%])/g, "$1$1")}"`;
|
||||
}
|
||||
function resolveTmuxInvocation(args) {
|
||||
const resolvedBinary = resolveTmuxBinaryPath();
|
||||
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(resolvedBinary)) {
|
||||
const comspec = process.env.COMSPEC || "cmd.exe";
|
||||
const commandLine = [quoteForCmd(resolvedBinary), ...args.map(quoteForCmd)].join(" ");
|
||||
return {
|
||||
command: comspec,
|
||||
args: ["/d", "/s", "/c", commandLine]
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: resolvedBinary,
|
||||
args
|
||||
};
|
||||
}
|
||||
function tmuxExec(args, opts) {
|
||||
const { stripTmux: _, ...execOpts } = opts ?? {};
|
||||
const invocation = resolveTmuxInvocation(args);
|
||||
return execFileSync(invocation.command, invocation.args, { encoding: "utf-8", ...execOpts, env: resolveEnv(opts) });
|
||||
}
|
||||
async function tmuxExecAsync(args, opts) {
|
||||
const { stripTmux: _, timeout, ...rest } = opts ?? {};
|
||||
const invocation = resolveTmuxInvocation(args);
|
||||
return promisify(execFile)(invocation.command, invocation.args, {
|
||||
encoding: "utf-8",
|
||||
env: resolveEnv(opts),
|
||||
...timeout !== void 0 ? { timeout } : {},
|
||||
...rest
|
||||
});
|
||||
}
|
||||
function tmuxShell(command, opts) {
|
||||
const { stripTmux: _, ...execOpts } = opts ?? {};
|
||||
return execSync(`tmux ${command}`, { encoding: "utf-8", ...execOpts, env: resolveEnv(opts) });
|
||||
}
|
||||
async function tmuxShellAsync(command, opts) {
|
||||
const { stripTmux: _, timeout, ...rest } = opts ?? {};
|
||||
return promisify(exec)(`tmux ${command}`, {
|
||||
encoding: "utf-8",
|
||||
env: resolveEnv(opts),
|
||||
...timeout !== void 0 ? { timeout } : {},
|
||||
...rest
|
||||
});
|
||||
}
|
||||
async function tmuxCmdAsync(args, opts) {
|
||||
if (args.some((a) => a.includes("#{"))) {
|
||||
const escaped = args.map((a) => "'" + a.replace(/'/g, "'\\''") + "'").join(" ");
|
||||
return tmuxShellAsync(escaped, opts);
|
||||
}
|
||||
return tmuxExecAsync(args, opts);
|
||||
}
|
||||
function resolveTmuxBinaryPath() {
|
||||
if (process.platform !== "win32") {
|
||||
return "tmux";
|
||||
}
|
||||
try {
|
||||
const result = spawnSync("where", ["tmux"], {
|
||||
timeout: 5e3,
|
||||
encoding: "utf8"
|
||||
});
|
||||
if (result.status !== 0) return "tmux";
|
||||
const candidates = result.stdout?.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) ?? [];
|
||||
const first = candidates[0];
|
||||
if (first && (isAbsolute2(first) || win32Path.isAbsolute(first))) {
|
||||
return first;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return "tmux";
|
||||
}
|
||||
var init_tmux_utils = __esm({
|
||||
"src/cli/tmux-utils.ts"() {
|
||||
"use strict";
|
||||
}
|
||||
});
|
||||
|
||||
// src/team/tmux-session.ts
|
||||
var tmux_session_exports = {};
|
||||
__export(tmux_session_exports, {
|
||||
@@ -1611,10 +1708,8 @@ __export(tmux_session_exports, {
|
||||
validateTmux: () => validateTmux,
|
||||
waitForPaneReady: () => waitForPaneReady
|
||||
});
|
||||
import { exec, execFile, execSync, execFileSync } from "child_process";
|
||||
import { existsSync as existsSync5 } from "fs";
|
||||
import { join as join6, basename as basename2, isAbsolute as isAbsolute2, win32 } from "path";
|
||||
import { promisify } from "util";
|
||||
import { join as join6, basename as basename3, isAbsolute as isAbsolute3, win32 } from "path";
|
||||
import fs from "fs/promises";
|
||||
function detectTeamMultiplexerContext(env = process.env) {
|
||||
if (env.TMUX) return "tmux";
|
||||
@@ -1624,23 +1719,13 @@ function detectTeamMultiplexerContext(env = process.env) {
|
||||
function isUnixLikeOnWindows() {
|
||||
return process.platform === "win32" && !!(process.env.MSYSTEM || process.env.MINGW_PREFIX);
|
||||
}
|
||||
async function tmuxAsync(args) {
|
||||
if (args.some((a) => a.includes("#{"))) {
|
||||
const escaped = args.map((a) => "'" + a.replace(/'/g, "'\\''") + "'").join(" ");
|
||||
return promisifiedExec(`tmux ${escaped}`);
|
||||
}
|
||||
return promisifiedExecFile("tmux", args);
|
||||
}
|
||||
async function applyMainVerticalLayout(teamTarget) {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
try {
|
||||
await execFileAsync2("tmux", ["select-layout", "-t", teamTarget, "main-vertical"]);
|
||||
await tmuxExecAsync(["select-layout", "-t", teamTarget, "main-vertical"]);
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
const widthResult = await tmuxAsync([
|
||||
const widthResult = await tmuxCmdAsync([
|
||||
"display-message",
|
||||
"-p",
|
||||
"-t",
|
||||
@@ -1650,8 +1735,8 @@ async function applyMainVerticalLayout(teamTarget) {
|
||||
const width = parseInt(widthResult.stdout.trim(), 10);
|
||||
if (Number.isFinite(width) && width >= 40) {
|
||||
const half = String(Math.floor(width / 2));
|
||||
await execFileAsync2("tmux", ["set-window-option", "-t", teamTarget, "main-pane-width", half]);
|
||||
await execFileAsync2("tmux", ["select-layout", "-t", teamTarget, "main-vertical"]);
|
||||
await tmuxExecAsync(["set-window-option", "-t", teamTarget, "main-pane-width", half]);
|
||||
await tmuxExecAsync(["select-layout", "-t", teamTarget, "main-vertical"]);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
@@ -1661,7 +1746,7 @@ function getDefaultShell() {
|
||||
return process.env.COMSPEC || "cmd.exe";
|
||||
}
|
||||
const shell = process.env.SHELL || "/bin/bash";
|
||||
const name = basename2(shell.replace(/\\/g, "/")).replace(/\.(exe|cmd|bat)$/i, "");
|
||||
const name = basename3(shell.replace(/\\/g, "/")).replace(/\.(exe|cmd|bat)$/i, "");
|
||||
if (!SUPPORTED_POSIX_SHELLS.has(name)) {
|
||||
return "/bin/sh";
|
||||
}
|
||||
@@ -1671,7 +1756,7 @@ function pathEntries(envPath) {
|
||||
return (envPath ?? "").split(process.platform === "win32" ? ";" : ":").map((entry) => entry.trim()).filter(Boolean);
|
||||
}
|
||||
function pathCandidateNames(candidatePath) {
|
||||
const base = basename2(candidatePath.replace(/\\/g, "/"));
|
||||
const base = basename3(candidatePath.replace(/\\/g, "/"));
|
||||
const bare = base.replace(/\.(exe|cmd|bat)$/i, "");
|
||||
if (process.platform === "win32") {
|
||||
return Array.from(/* @__PURE__ */ new Set([`${bare}.exe`, `${bare}.cmd`, `${bare}.bat`, bare]));
|
||||
@@ -1697,7 +1782,7 @@ function resolveShellFromCandidates(paths, rcFile) {
|
||||
}
|
||||
function resolveSupportedShellAffinity(shellPath) {
|
||||
if (!shellPath) return null;
|
||||
const name = basename2(shellPath.replace(/\\/g, "/")).replace(/\.(exe|cmd|bat)$/i, "");
|
||||
const name = basename3(shellPath.replace(/\\/g, "/")).replace(/\.(exe|cmd|bat)$/i, "");
|
||||
if (name !== "zsh" && name !== "bash") return null;
|
||||
if (!existsSync5(shellPath)) return null;
|
||||
const home = process.env.HOME ?? "";
|
||||
@@ -1723,7 +1808,7 @@ function escapeForCmdSet(value) {
|
||||
return value.replace(/"/g, '""');
|
||||
}
|
||||
function shellNameFromPath(shellPath) {
|
||||
const shellName = basename2(shellPath.replace(/\\/g, "/"));
|
||||
const shellName = basename3(shellPath.replace(/\\/g, "/"));
|
||||
return shellName.replace(/\.(exe|cmd|bat)$/i, "");
|
||||
}
|
||||
function shellEscape(value) {
|
||||
@@ -1735,7 +1820,7 @@ function assertSafeEnvKey(key) {
|
||||
}
|
||||
}
|
||||
function isAbsoluteLaunchBinaryPath(value) {
|
||||
return isAbsolute2(value) || win32.isAbsolute(value);
|
||||
return isAbsolute3(value) || win32.isAbsolute(value);
|
||||
}
|
||||
function assertSafeLaunchBinary(launchBinary) {
|
||||
if (launchBinary.trim().length === 0) {
|
||||
@@ -1818,9 +1903,12 @@ function buildWorkerStartCommand(config) {
|
||||
}
|
||||
return `env ${envString} ${shell} -c "${sourceCmd}exec ${launchWords[0]}"`;
|
||||
}
|
||||
function validateTmux() {
|
||||
function validateTmux(hasTmuxContext = false) {
|
||||
if (hasTmuxContext) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
execSync("tmux -V", { encoding: "utf-8", timeout: 5e3, stdio: "pipe" });
|
||||
tmuxShell("-V", { stripTmux: true, timeout: 5e3, stdio: "pipe" });
|
||||
} catch {
|
||||
throw new Error(
|
||||
"tmux is not available. Install it:\n macOS: brew install tmux\n Ubuntu/Debian: sudo apt-get install tmux\n Fedora: sudo dnf install tmux\n Arch: sudo pacman -S tmux\n Windows: winget install psmux"
|
||||
@@ -1843,27 +1931,27 @@ function sessionName(teamName, workerName) {
|
||||
function createSession(teamName, workerName, workingDirectory) {
|
||||
const name = sessionName(teamName, workerName);
|
||||
try {
|
||||
execFileSync("tmux", ["kill-session", "-t", name], { stdio: "pipe", timeout: 5e3 });
|
||||
tmuxExec(["kill-session", "-t", name], { stripTmux: true, stdio: "pipe", timeout: 5e3 });
|
||||
} catch {
|
||||
}
|
||||
const args = ["new-session", "-d", "-s", name, "-x", "200", "-y", "50"];
|
||||
if (workingDirectory) {
|
||||
args.push("-c", workingDirectory);
|
||||
}
|
||||
execFileSync("tmux", args, { stdio: "pipe", timeout: 5e3 });
|
||||
tmuxExec(args, { stripTmux: true, stdio: "pipe", timeout: 5e3 });
|
||||
return name;
|
||||
}
|
||||
function killSession(teamName, workerName) {
|
||||
const name = sessionName(teamName, workerName);
|
||||
try {
|
||||
execFileSync("tmux", ["kill-session", "-t", name], { stdio: "pipe", timeout: 5e3 });
|
||||
tmuxExec(["kill-session", "-t", name], { stripTmux: true, stdio: "pipe", timeout: 5e3 });
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
function isSessionAlive(teamName, workerName) {
|
||||
const name = sessionName(teamName, workerName);
|
||||
try {
|
||||
execFileSync("tmux", ["has-session", "-t", name], { stdio: "pipe", timeout: 5e3 });
|
||||
tmuxExec(["has-session", "-t", name], { stripTmux: true, stdio: "pipe", timeout: 5e3 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -1872,8 +1960,7 @@ function isSessionAlive(teamName, workerName) {
|
||||
function listActiveSessions(teamName) {
|
||||
const prefix = `${TMUX_SESSION_PREFIX}-${sanitizeName(teamName)}-`;
|
||||
try {
|
||||
const output2 = execSync("tmux list-sessions -F '#{session_name}'", {
|
||||
encoding: "utf-8",
|
||||
const output2 = tmuxShell("list-sessions -F '#{session_name}'", {
|
||||
timeout: 5e3,
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
});
|
||||
@@ -1884,15 +1971,15 @@ function listActiveSessions(teamName) {
|
||||
}
|
||||
function spawnBridgeInSession(tmuxSession, bridgeScriptPath, configFilePath) {
|
||||
const cmd = `node "${bridgeScriptPath}" --config "${configFilePath}"`;
|
||||
execFileSync("tmux", ["send-keys", "-t", tmuxSession, cmd, "Enter"], { stdio: "pipe", timeout: 5e3 });
|
||||
tmuxExec(["send-keys", "-t", tmuxSession, cmd, "Enter"], { stripTmux: true, stdio: "pipe", timeout: 5e3 });
|
||||
}
|
||||
async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
const multiplexerContext = detectTeamMultiplexerContext();
|
||||
const inTmux = multiplexerContext === "tmux";
|
||||
const useDedicatedWindow = Boolean(options.newWindow && inTmux);
|
||||
if (!inTmux) {
|
||||
validateTmux();
|
||||
}
|
||||
const envPaneIdRaw = (process.env.TMUX_PANE ?? "").trim();
|
||||
const envPaneId = /^%\d+$/.test(envPaneIdRaw) ? envPaneIdRaw : "";
|
||||
let sessionAndWindow = "";
|
||||
@@ -1900,7 +1987,7 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
let sessionMode = inTmux ? "split-pane" : "detached-session";
|
||||
if (!inTmux) {
|
||||
const detachedSessionName = `${TMUX_SESSION_PREFIX}-${sanitizeName(teamName)}-${Date.now().toString(36)}`;
|
||||
const detachedResult = await execFileAsync2("tmux", [
|
||||
const detachedResult = await tmuxExecAsync([
|
||||
"new-session",
|
||||
"-d",
|
||||
"-P",
|
||||
@@ -1910,7 +1997,7 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
detachedSessionName,
|
||||
"-c",
|
||||
cwd
|
||||
]);
|
||||
], { stripTmux: true });
|
||||
const detachedLine = detachedResult.stdout.trim();
|
||||
const detachedMatch = detachedLine.match(/^(\S+)\s+(%\d+)$/);
|
||||
if (!detachedMatch) {
|
||||
@@ -1921,7 +2008,7 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
}
|
||||
if (inTmux && envPaneId) {
|
||||
try {
|
||||
const targetedContextResult = await execFileAsync2("tmux", [
|
||||
const targetedContextResult = await tmuxExecAsync([
|
||||
"display-message",
|
||||
"-p",
|
||||
"-t",
|
||||
@@ -1935,7 +2022,7 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
}
|
||||
}
|
||||
if (!sessionAndWindow || !leaderPaneId) {
|
||||
const contextResult = await tmuxAsync([
|
||||
const contextResult = await tmuxCmdAsync([
|
||||
"display-message",
|
||||
"-p",
|
||||
"#S:#I #{pane_id}"
|
||||
@@ -1951,7 +2038,7 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
if (useDedicatedWindow) {
|
||||
const targetSession = sessionAndWindow.split(":")[0] ?? sessionAndWindow;
|
||||
const windowName = `omc-${sanitizeName(teamName)}`.slice(0, 32);
|
||||
const newWindowResult = await execFileAsync2("tmux", [
|
||||
const newWindowResult = await tmuxExecAsync([
|
||||
"new-window",
|
||||
"-d",
|
||||
"-P",
|
||||
@@ -1978,12 +2065,12 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
const workerPaneIds = [];
|
||||
if (workerCount <= 0) {
|
||||
try {
|
||||
await execFileAsync2("tmux", ["set-option", "-t", resolvedSessionName, "mouse", "on"]);
|
||||
await tmuxExecAsync(["set-option", "-t", resolvedSessionName, "mouse", "on"]);
|
||||
} catch {
|
||||
}
|
||||
if (sessionMode !== "dedicated-window") {
|
||||
try {
|
||||
await execFileAsync2("tmux", ["select-pane", "-t", leaderPaneId]);
|
||||
await tmuxExecAsync(["select-pane", "-t", leaderPaneId]);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -1993,7 +2080,7 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
for (let i = 0; i < workerCount; i++) {
|
||||
const splitTarget = i === 0 ? leaderPaneId : workerPaneIds[i - 1];
|
||||
const splitType = i === 0 ? "-h" : "-v";
|
||||
const splitResult = await tmuxAsync([
|
||||
const splitResult = await tmuxCmdAsync([
|
||||
"split-window",
|
||||
splitType,
|
||||
"-t",
|
||||
@@ -2012,12 +2099,12 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
}
|
||||
await applyMainVerticalLayout(teamTarget);
|
||||
try {
|
||||
await execFileAsync2("tmux", ["set-option", "-t", resolvedSessionName, "mouse", "on"]);
|
||||
await tmuxExecAsync(["set-option", "-t", resolvedSessionName, "mouse", "on"]);
|
||||
} catch {
|
||||
}
|
||||
if (sessionMode !== "dedicated-window") {
|
||||
try {
|
||||
await execFileAsync2("tmux", ["select-pane", "-t", leaderPaneId]);
|
||||
await tmuxExecAsync(["select-pane", "-t", leaderPaneId]);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -2025,26 +2112,23 @@ async function createTeamSession(teamName, workerCount, cwd, options = {}) {
|
||||
return { sessionName: teamTarget, leaderPaneId, workerPaneIds, sessionMode };
|
||||
}
|
||||
async function spawnWorkerInPane(sessionName2, paneId, config) {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
validateTeamName(config.teamName);
|
||||
const startCmd = buildWorkerStartCommand(config);
|
||||
await execFileAsync2("tmux", [
|
||||
await tmuxExecAsync([
|
||||
"send-keys",
|
||||
"-t",
|
||||
paneId,
|
||||
"-l",
|
||||
startCmd
|
||||
]);
|
||||
await execFileAsync2("tmux", ["send-keys", "-t", paneId, "Enter"]);
|
||||
await tmuxExecAsync(["send-keys", "-t", paneId, "Enter"]);
|
||||
}
|
||||
function normalizeTmuxCapture(value) {
|
||||
return value.replace(/\r/g, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
async function capturePaneAsync(paneId, execFileAsync2) {
|
||||
async function capturePaneAsync(paneId) {
|
||||
try {
|
||||
const result = await execFileAsync2("tmux", ["capture-pane", "-t", paneId, "-p", "-S", "-80"]);
|
||||
const result = await tmuxExecAsync(["capture-pane", "-t", paneId, "-p", "-S", "-80"]);
|
||||
return result.stdout;
|
||||
} catch {
|
||||
return "";
|
||||
@@ -2090,7 +2174,7 @@ async function waitForPaneReady(paneId, opts = {}) {
|
||||
const pollIntervalMs = Number.isFinite(opts.pollIntervalMs) && (opts.pollIntervalMs ?? 0) > 0 ? Number(opts.pollIntervalMs) : 250;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const captured = await capturePaneAsync(paneId, promisifiedExecFile);
|
||||
const captured = await capturePaneAsync(paneId);
|
||||
if (paneLooksReady(captured) && !paneHasActiveTask(captured)) {
|
||||
return true;
|
||||
}
|
||||
@@ -2106,7 +2190,7 @@ function paneTailContainsLiteralLine(captured, text) {
|
||||
}
|
||||
async function paneInCopyMode(paneId) {
|
||||
try {
|
||||
const result = await tmuxAsync(["display-message", "-t", paneId, "-p", "#{pane_in_mode}"]);
|
||||
const result = await tmuxCmdAsync(["display-message", "-t", paneId, "-p", "#{pane_in_mode}"]);
|
||||
return result.stdout.trim() === "1";
|
||||
} catch {
|
||||
return false;
|
||||
@@ -2129,47 +2213,43 @@ async function sendToWorker(_sessionName, paneId, message) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
const sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const sendKey = async (key) => {
|
||||
await execFileAsync2("tmux", ["send-keys", "-t", paneId, key]);
|
||||
await tmuxExecAsync(["send-keys", "-t", paneId, key]);
|
||||
};
|
||||
if (await paneInCopyMode(paneId)) {
|
||||
return false;
|
||||
}
|
||||
const initialCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
const initialCapture = await capturePaneAsync(paneId);
|
||||
const paneBusy = paneHasActiveTask(initialCapture);
|
||||
if (paneHasTrustPrompt(initialCapture)) {
|
||||
await sendKey("C-m");
|
||||
await sleep3(120);
|
||||
await sleep(120);
|
||||
await sendKey("C-m");
|
||||
await sleep3(200);
|
||||
await sleep(200);
|
||||
}
|
||||
await execFileAsync2("tmux", ["send-keys", "-t", paneId, "-l", "--", message]);
|
||||
await sleep3(150);
|
||||
await tmuxExecAsync(["send-keys", "-t", paneId, "-l", "--", message]);
|
||||
await sleep(150);
|
||||
const submitRounds = 6;
|
||||
for (let round = 0; round < submitRounds; round++) {
|
||||
await sleep3(100);
|
||||
await sleep(100);
|
||||
if (round === 0 && paneBusy) {
|
||||
await sendKey("Tab");
|
||||
await sleep3(80);
|
||||
await sleep(80);
|
||||
await sendKey("C-m");
|
||||
} else {
|
||||
await sendKey("C-m");
|
||||
await sleep3(200);
|
||||
await sleep(200);
|
||||
await sendKey("C-m");
|
||||
}
|
||||
await sleep3(140);
|
||||
const checkCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
await sleep(140);
|
||||
const checkCapture = await capturePaneAsync(paneId);
|
||||
if (!paneTailContainsLiteralLine(checkCapture, message)) return true;
|
||||
await sleep3(140);
|
||||
await sleep(140);
|
||||
}
|
||||
if (await paneInCopyMode(paneId)) {
|
||||
return false;
|
||||
}
|
||||
const finalCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
const finalCapture = await capturePaneAsync(paneId);
|
||||
const paneModeBeforeAdaptiveRetry = await paneInCopyMode(paneId);
|
||||
if (shouldAttemptAdaptiveRetry({
|
||||
paneBusy,
|
||||
@@ -2182,18 +2262,18 @@ async function sendToWorker(_sessionName, paneId, message) {
|
||||
return false;
|
||||
}
|
||||
await sendKey("C-u");
|
||||
await sleep3(80);
|
||||
await sleep(80);
|
||||
if (await paneInCopyMode(paneId)) {
|
||||
return false;
|
||||
}
|
||||
await execFileAsync2("tmux", ["send-keys", "-t", paneId, "-l", "--", message]);
|
||||
await sleep3(120);
|
||||
await tmuxExecAsync(["send-keys", "-t", paneId, "-l", "--", message]);
|
||||
await sleep(120);
|
||||
for (let round = 0; round < 4; round++) {
|
||||
await sendKey("C-m");
|
||||
await sleep3(180);
|
||||
await sleep(180);
|
||||
await sendKey("C-m");
|
||||
await sleep3(140);
|
||||
const retryCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
await sleep(140);
|
||||
const retryCapture = await capturePaneAsync(paneId);
|
||||
if (!paneTailContainsLiteralLine(retryCapture, message)) return true;
|
||||
}
|
||||
}
|
||||
@@ -2201,10 +2281,10 @@ async function sendToWorker(_sessionName, paneId, message) {
|
||||
return false;
|
||||
}
|
||||
await sendKey("C-m");
|
||||
await sleep3(120);
|
||||
await sleep(120);
|
||||
await sendKey("C-m");
|
||||
await sleep3(140);
|
||||
const finalCheckCapture = await capturePaneAsync(paneId, execFileAsync2);
|
||||
await sleep(140);
|
||||
const finalCheckCapture = await capturePaneAsync(paneId);
|
||||
if (!finalCheckCapture || finalCheckCapture.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
@@ -2216,15 +2296,12 @@ async function sendToWorker(_sessionName, paneId, message) {
|
||||
async function injectToLeaderPane(sessionName2, leaderPaneId, message) {
|
||||
const prefixed = `[OMC_TMUX_INJECT] ${message}`.slice(0, 200);
|
||||
try {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
if (await paneInCopyMode(leaderPaneId)) {
|
||||
return false;
|
||||
}
|
||||
const captured = await capturePaneAsync(leaderPaneId, execFileAsync2);
|
||||
const captured = await capturePaneAsync(leaderPaneId);
|
||||
if (paneHasActiveTask(captured)) {
|
||||
await execFileAsync2("tmux", ["send-keys", "-t", leaderPaneId, "C-c"]);
|
||||
await tmuxExecAsync(["send-keys", "-t", leaderPaneId, "C-c"]);
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
} catch {
|
||||
@@ -2233,7 +2310,7 @@ async function injectToLeaderPane(sessionName2, leaderPaneId, message) {
|
||||
}
|
||||
async function isWorkerAlive(paneId) {
|
||||
try {
|
||||
const result = await tmuxAsync([
|
||||
const result = await tmuxCmdAsync([
|
||||
"display-message",
|
||||
"-t",
|
||||
paneId,
|
||||
@@ -2257,13 +2334,10 @@ async function killWorkerPanes(opts) {
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
for (const paneId of paneIds) {
|
||||
if (paneId === leaderPaneId) continue;
|
||||
try {
|
||||
await execFileAsync2("tmux", ["kill-pane", "-t", paneId]);
|
||||
await tmuxExecAsync(["kill-pane", "-t", paneId]);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -2285,7 +2359,7 @@ async function resolveSplitPaneWorkerPaneIds(sessionName2, recordedPaneIds, lead
|
||||
const resolved = dedupeWorkerPaneIds(recordedPaneIds ?? [], leaderPaneId);
|
||||
if (!sessionName2.includes(":")) return resolved;
|
||||
try {
|
||||
const paneResult = await tmuxAsync(["list-panes", "-t", sessionName2, "-F", "#{pane_id}"]);
|
||||
const paneResult = await tmuxCmdAsync(["list-panes", "-t", sessionName2, "-F", "#{pane_id}"]);
|
||||
return dedupeWorkerPaneIds(
|
||||
[...resolved, ...paneResult.stdout.split("\n").map((paneId) => paneId.trim())],
|
||||
leaderPaneId
|
||||
@@ -2295,16 +2369,13 @@ async function resolveSplitPaneWorkerPaneIds(sessionName2, recordedPaneIds, lead
|
||||
}
|
||||
}
|
||||
async function killTeamSession(sessionName2, workerPaneIds, leaderPaneId, options = {}) {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
const sessionMode = options.sessionMode ?? (sessionName2.includes(":") ? "split-pane" : "detached-session");
|
||||
if (sessionMode === "split-pane") {
|
||||
if (!workerPaneIds?.length) return;
|
||||
for (const id of workerPaneIds) {
|
||||
if (id === leaderPaneId) continue;
|
||||
try {
|
||||
await execFileAsync2("tmux", ["kill-pane", "-t", id]);
|
||||
await tmuxExecAsync(["kill-pane", "-t", id]);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
@@ -2312,7 +2383,7 @@ async function killTeamSession(sessionName2, workerPaneIds, leaderPaneId, option
|
||||
}
|
||||
if (sessionMode === "dedicated-window") {
|
||||
try {
|
||||
await execFileAsync2("tmux", ["kill-window", "-t", sessionName2]);
|
||||
await tmuxExecAsync(["kill-window", "-t", sessionName2]);
|
||||
} catch {
|
||||
}
|
||||
return;
|
||||
@@ -2320,7 +2391,7 @@ async function killTeamSession(sessionName2, workerPaneIds, leaderPaneId, option
|
||||
const sessionTarget = sessionName2.split(":")[0] ?? sessionName2;
|
||||
if (process.env.OMC_TEAM_ALLOW_KILL_CURRENT_SESSION !== "1" && process.env.TMUX) {
|
||||
try {
|
||||
const current = await tmuxAsync(["display-message", "-p", "#S"]);
|
||||
const current = await tmuxCmdAsync(["display-message", "-p", "#S"]);
|
||||
const currentSessionName = current.stdout.trim();
|
||||
if (currentSessionName && currentSessionName === sessionTarget) {
|
||||
return;
|
||||
@@ -2329,19 +2400,18 @@ async function killTeamSession(sessionName2, workerPaneIds, leaderPaneId, option
|
||||
}
|
||||
}
|
||||
try {
|
||||
await execFileAsync2("tmux", ["kill-session", "-t", sessionTarget]);
|
||||
await tmuxExecAsync(["kill-session", "-t", sessionTarget]);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
var sleep, TMUX_SESSION_PREFIX, promisifiedExec, promisifiedExecFile, SUPPORTED_POSIX_SHELLS, ZSH_CANDIDATES, BASH_CANDIDATES, DANGEROUS_LAUNCH_BINARY_CHARS;
|
||||
var sleep, TMUX_SESSION_PREFIX, SUPPORTED_POSIX_SHELLS, ZSH_CANDIDATES, BASH_CANDIDATES, DANGEROUS_LAUNCH_BINARY_CHARS;
|
||||
var init_tmux_session = __esm({
|
||||
"src/team/tmux-session.ts"() {
|
||||
"use strict";
|
||||
init_team_name();
|
||||
init_tmux_utils();
|
||||
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
TMUX_SESSION_PREFIX = "omc-team";
|
||||
promisifiedExec = promisify(exec);
|
||||
promisifiedExecFile = promisify(execFile);
|
||||
SUPPORTED_POSIX_SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "fish", "ksh"]);
|
||||
ZSH_CANDIDATES = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh", "/opt/homebrew/bin/zsh"];
|
||||
BASH_CANDIDATES = ["/bin/bash", "/usr/bin/bash"];
|
||||
@@ -2351,12 +2421,12 @@ var init_tmux_session = __esm({
|
||||
|
||||
// src/agents/utils.ts
|
||||
import { readFileSync } from "fs";
|
||||
import { join as join7, dirname as dirname4, basename as basename3, resolve as resolve2, relative as relative2, isAbsolute as isAbsolute3 } from "path";
|
||||
import { join as join7, dirname as dirname4, basename as basename4, resolve as resolve2, relative as relative2, isAbsolute as isAbsolute4 } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
function getPackageDir() {
|
||||
if (typeof __dirname !== "undefined" && __dirname) {
|
||||
const currentDirName = basename3(__dirname);
|
||||
const parentDirName = basename3(dirname4(__dirname));
|
||||
const currentDirName = basename4(__dirname);
|
||||
const parentDirName = basename4(dirname4(__dirname));
|
||||
if (currentDirName === "bridge") {
|
||||
return join7(__dirname, "..");
|
||||
}
|
||||
@@ -2367,7 +2437,7 @@ function getPackageDir() {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname2 = dirname4(__filename);
|
||||
const currentDirName = basename3(__dirname2);
|
||||
const currentDirName = basename4(__dirname2);
|
||||
if (currentDirName === "bridge") {
|
||||
return join7(__dirname2, "..");
|
||||
}
|
||||
@@ -2397,7 +2467,7 @@ function loadAgentPrompt(agentName) {
|
||||
const resolvedPath = resolve2(agentPath);
|
||||
const resolvedAgentsDir = resolve2(agentsDir);
|
||||
const rel = relative2(resolvedAgentsDir, resolvedPath);
|
||||
if (rel.startsWith("..") || isAbsolute3(rel)) {
|
||||
if (rel.startsWith("..") || isAbsolute4(rel)) {
|
||||
throw new Error(`Invalid agent name: path traversal detected`);
|
||||
}
|
||||
const content = readFileSync(agentPath, "utf-8");
|
||||
@@ -2433,12 +2503,12 @@ var init_skininthegamebros_guidance = __esm({
|
||||
|
||||
// src/agents/prompt-helpers.ts
|
||||
import { readdirSync } from "fs";
|
||||
import { join as join8, dirname as dirname5, basename as basename4 } from "path";
|
||||
import { join as join8, dirname as dirname5, basename as basename5 } from "path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "url";
|
||||
function getPackageDir2() {
|
||||
if (typeof __dirname !== "undefined" && __dirname) {
|
||||
const currentDirName = basename4(__dirname);
|
||||
const parentDirName = basename4(dirname5(__dirname));
|
||||
const currentDirName = basename5(__dirname);
|
||||
const parentDirName = basename5(dirname5(__dirname));
|
||||
if (currentDirName === "bridge") {
|
||||
return join8(__dirname, "..");
|
||||
}
|
||||
@@ -2449,7 +2519,7 @@ function getPackageDir2() {
|
||||
try {
|
||||
const __filename = fileURLToPath2(import.meta.url);
|
||||
const __dirname2 = dirname5(__filename);
|
||||
const currentDirName = basename4(__dirname2);
|
||||
const currentDirName = basename5(__dirname2);
|
||||
if (currentDirName === "bridge") {
|
||||
return join8(__dirname2, "..");
|
||||
}
|
||||
@@ -2470,7 +2540,7 @@ function getValidAgentRoles() {
|
||||
try {
|
||||
const agentsDir = join8(getPackageDir2(), "agents");
|
||||
const files = readdirSync(agentsDir);
|
||||
_cachedRoles = files.filter((f) => f.endsWith(".md")).map((f) => basename4(f, ".md")).sort();
|
||||
_cachedRoles = files.filter((f) => f.endsWith(".md")).map((f) => basename5(f, ".md")).sort();
|
||||
} catch (err) {
|
||||
console.error("[prompt-injection] CRITICAL: Could not scan agents/ directory for role discovery:", err);
|
||||
_cachedRoles = [];
|
||||
@@ -2505,10 +2575,10 @@ var init_prompt_helpers = __esm({
|
||||
});
|
||||
|
||||
// src/utils/omc-cli-rendering.ts
|
||||
import { spawnSync } from "child_process";
|
||||
import { spawnSync as spawnSync2 } from "child_process";
|
||||
function commandExists(command, env) {
|
||||
const lookupCommand = process.platform === "win32" ? "where" : "which";
|
||||
const result = spawnSync(lookupCommand, [command], {
|
||||
const result = spawnSync2(lookupCommand, [command], {
|
||||
stdio: "ignore",
|
||||
env
|
||||
});
|
||||
@@ -2550,7 +2620,7 @@ var init_config_dir = __esm({
|
||||
|
||||
// src/utils/paths.ts
|
||||
import { join as join10 } from "path";
|
||||
import { existsSync as existsSync6, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync, unlinkSync, rmSync } from "fs";
|
||||
import { existsSync as existsSync6, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync, unlinkSync, rmSync, symlinkSync } from "fs";
|
||||
import { homedir as homedir2 } from "os";
|
||||
function getConfigDir() {
|
||||
if (process.platform === "win32") {
|
||||
@@ -3645,8 +3715,8 @@ var init_security_config = __esm({
|
||||
});
|
||||
|
||||
// src/team/model-contract.ts
|
||||
import { spawnSync as spawnSync2 } from "child_process";
|
||||
import { isAbsolute as isAbsolute4, normalize as normalize2, win32 as win32Path } from "path";
|
||||
import { spawnSync as spawnSync3 } from "child_process";
|
||||
import { isAbsolute as isAbsolute5, normalize as normalize2, win32 as win32Path2 } from "path";
|
||||
function getTrustedPrefixes() {
|
||||
const trusted = [
|
||||
"/usr/local/bin",
|
||||
@@ -3659,7 +3729,7 @@ function getTrustedPrefixes() {
|
||||
trusted.push(`${home}/.nvm/`);
|
||||
trusted.push(`${home}/.cargo/bin`);
|
||||
}
|
||||
const custom = (process.env.OMC_TRUSTED_CLI_DIRS ?? "").split(":").map((part) => part.trim()).filter(Boolean).filter((part) => isAbsolute4(part));
|
||||
const custom = (process.env.OMC_TRUSTED_CLI_DIRS ?? "").split(":").map((part) => part.trim()).filter(Boolean).filter((part) => isAbsolute5(part));
|
||||
trusted.push(...custom);
|
||||
return trusted;
|
||||
}
|
||||
@@ -3677,7 +3747,7 @@ function resolveCliBinaryPath(binary) {
|
||||
const cached = resolvedPathCache.get(binary);
|
||||
if (cached) return cached;
|
||||
const finder = process.platform === "win32" ? "where" : "which";
|
||||
const result = spawnSync2(finder, [binary], {
|
||||
const result = spawnSync3(finder, [binary], {
|
||||
timeout: 5e3,
|
||||
env: process.env
|
||||
});
|
||||
@@ -3690,7 +3760,7 @@ function resolveCliBinaryPath(binary) {
|
||||
throw new Error(`CLI binary '${binary}' not found in PATH`);
|
||||
}
|
||||
const resolvedPath = normalize2(firstLine);
|
||||
if (!isAbsolute4(resolvedPath)) {
|
||||
if (!isAbsolute5(resolvedPath)) {
|
||||
throw new Error(`Resolved CLI binary '${binary}' to relative path`);
|
||||
}
|
||||
if (UNTRUSTED_PATH_PATTERNS.some((pattern) => pattern.test(resolvedPath))) {
|
||||
@@ -3715,20 +3785,20 @@ function getContract(agentType) {
|
||||
return contract;
|
||||
}
|
||||
function validateBinaryRef(binary) {
|
||||
if (isAbsolute4(binary)) return;
|
||||
if (isAbsolute5(binary)) return;
|
||||
if (/^[A-Za-z0-9._-]+$/.test(binary)) return;
|
||||
throw new Error(`Unsafe CLI binary reference: ${binary}`);
|
||||
}
|
||||
function resolveBinaryPath(binary) {
|
||||
validateBinaryRef(binary);
|
||||
if (isAbsolute4(binary)) return binary;
|
||||
if (isAbsolute5(binary)) return binary;
|
||||
try {
|
||||
const resolver = process.platform === "win32" ? "where" : "which";
|
||||
const result = spawnSync2(resolver, [binary], { timeout: 5e3, encoding: "utf8" });
|
||||
const result = spawnSync3(resolver, [binary], { timeout: 5e3, encoding: "utf8" });
|
||||
if (result.status !== 0) return binary;
|
||||
const lines = result.stdout?.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) ?? [];
|
||||
const firstPath = lines[0];
|
||||
const isResolvedAbsolute = !!firstPath && (isAbsolute4(firstPath) || win32Path.isAbsolute(firstPath));
|
||||
const isResolvedAbsolute = !!firstPath && (isAbsolute5(firstPath) || win32Path2.isAbsolute(firstPath));
|
||||
return isResolvedAbsolute ? firstPath : binary;
|
||||
} catch {
|
||||
return binary;
|
||||
@@ -3913,6 +3983,10 @@ function generateTriggerMessage(teamName, workerName, teamStateRoot3 = ".omc/sta
|
||||
}
|
||||
return `Read ${inboxPath}, start work now, report concrete progress (not ACK-only), and keep executing your assigned or next feasible work.`;
|
||||
}
|
||||
function generatePromptModeStartupPrompt(teamName, workerName, teamStateRoot3 = ".omc/state") {
|
||||
const inboxPath = buildInstructionPath(teamStateRoot3, "team", teamName, "workers", workerName, "inbox.md");
|
||||
return `Open ${inboxPath}. Follow it and begin the assigned work.`;
|
||||
}
|
||||
function generateMailboxTriggerMessage(teamName, workerName, count = 1, teamStateRoot3 = ".omc/state") {
|
||||
const normalizedCount = Number.isFinite(count) ? Math.max(1, Math.floor(count)) : 1;
|
||||
const mailboxPath = buildInstructionPath(teamStateRoot3, "team", teamName, "mailbox", `${workerName}.json`);
|
||||
@@ -4343,7 +4417,7 @@ var init_file_lock = __esm({
|
||||
});
|
||||
|
||||
// src/team/git-worktree.ts
|
||||
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
|
||||
import { existsSync as existsSync10, readFileSync as readFileSync7, rmSync as rmSync2 } from "node:fs";
|
||||
import { join as join15 } from "node:path";
|
||||
import { execFileSync as execFileSync3 } from "node:child_process";
|
||||
function getWorktreePath(repoRoot, teamName, workerName) {
|
||||
@@ -4786,7 +4860,6 @@ __export(runtime_v2_exports, {
|
||||
startTeamV2: () => startTeamV2,
|
||||
writeWatchdogFailedMarker: () => writeWatchdogFailedMarker
|
||||
});
|
||||
import { execFile as execFile3 } from "child_process";
|
||||
import { join as join18, resolve as resolve3 } from "path";
|
||||
import { existsSync as existsSync15 } from "fs";
|
||||
import { mkdir as mkdir7, readdir as readdir2, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
||||
@@ -4813,12 +4886,12 @@ async function isWorkerPaneAlive(paneId) {
|
||||
}
|
||||
async function captureWorkerPane(paneId) {
|
||||
if (!paneId) return "";
|
||||
return await new Promise((resolve4) => {
|
||||
execFile3("tmux", ["capture-pane", "-t", paneId, "-p", "-S", "-80"], (err, stdout) => {
|
||||
if (err) resolve4("");
|
||||
else resolve4(stdout ?? "");
|
||||
});
|
||||
});
|
||||
try {
|
||||
const result = await tmuxExecAsync(["capture-pane", "-t", paneId, "-p", "-S", "-80"]);
|
||||
return result.stdout ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function isFreshTimestamp(value, maxAgeMs = MONITOR_SIGNAL_STALE_MS) {
|
||||
if (!value) return false;
|
||||
@@ -4838,6 +4911,12 @@ function findOutstandingWorkerTask(worker, taskById, inProgressByOwner) {
|
||||
const owned = inProgressByOwner.get(worker.name) ?? [];
|
||||
return owned[0] ?? null;
|
||||
}
|
||||
function getTaskDependencyIds(task) {
|
||||
return task.depends_on ?? task.blocked_by ?? [];
|
||||
}
|
||||
function getMissingDependencyIds(task, taskById) {
|
||||
return getTaskDependencyIds(task).filter((dependencyId) => !taskById.has(dependencyId));
|
||||
}
|
||||
function buildV2TaskInstruction(teamName, workerName, task, taskId) {
|
||||
const claimTaskCommand = formatOmcCliInvocation(
|
||||
`team api claim-task --input '${JSON.stringify({ team_name: teamName, task_id: taskId, worker: workerName })}' --json`,
|
||||
@@ -4920,12 +4999,9 @@ async function waitForWorkerStartupEvidence(teamName, workerName, taskId, cwd, a
|
||||
return false;
|
||||
}
|
||||
async function spawnV2Worker(opts) {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
const splitTarget = opts.existingWorkerPaneIds.length === 0 ? opts.leaderPaneId : opts.existingWorkerPaneIds[opts.existingWorkerPaneIds.length - 1];
|
||||
const splitType = opts.existingWorkerPaneIds.length === 0 ? "-h" : "-v";
|
||||
const splitResult = await execFileAsync2("tmux", [
|
||||
const splitResult = await tmuxExecAsync([
|
||||
"split-window",
|
||||
splitType,
|
||||
"-t",
|
||||
@@ -4949,6 +5025,7 @@ async function spawnV2Worker(opts) {
|
||||
opts.taskId
|
||||
);
|
||||
const inboxTriggerMessage = generateTriggerMessage(opts.teamName, opts.workerName);
|
||||
const promptModeStartupPrompt = generatePromptModeStartupPrompt(opts.teamName, opts.workerName);
|
||||
if (usePromptMode) {
|
||||
await composeInitialInbox(opts.teamName, opts.workerName, instruction, opts.cwd);
|
||||
}
|
||||
@@ -4975,7 +5052,7 @@ async function spawnV2Worker(opts) {
|
||||
model: modelForAgent
|
||||
});
|
||||
if (usePromptMode) {
|
||||
launchArgs.push(...getPromptModeArgs(opts.agentType, instruction));
|
||||
launchArgs.push(...getPromptModeArgs(opts.agentType, promptModeStartupPrompt));
|
||||
}
|
||||
const paneConfig = {
|
||||
teamName: opts.teamName,
|
||||
@@ -5392,8 +5469,11 @@ async function monitorTeamV2(teamName, cwd) {
|
||||
const statusFresh = isFreshTimestamp(status.updated_at);
|
||||
const heartbeatFresh = isFreshTimestamp(heartbeat?.last_turn_at);
|
||||
const hasWorkStartEvidence = expectedTaskId !== "" && hasWorkerStatusProgress(status, expectedTaskId);
|
||||
const missingDependencyIds = outstandingTask ? getMissingDependencyIds(outstandingTask, taskById) : [];
|
||||
let stallReason = null;
|
||||
if (paneSuggestsIdle && expectedTaskId !== "" && !hasWorkStartEvidence) {
|
||||
if (paneSuggestsIdle && missingDependencyIds.length > 0) {
|
||||
stallReason = "missing_dependency";
|
||||
} else if (paneSuggestsIdle && expectedTaskId !== "" && !hasWorkStartEvidence) {
|
||||
stallReason = "no_work_start_evidence";
|
||||
} else if (paneSuggestsIdle && expectedTaskId !== "" && (!statusFresh || !heartbeatFresh)) {
|
||||
stallReason = "stale_or_missing_worker_reports";
|
||||
@@ -5402,7 +5482,11 @@ async function monitorTeamV2(teamName, cwd) {
|
||||
}
|
||||
if (stallReason) {
|
||||
nonReportingWorkers.push(w.name);
|
||||
if (stallReason === "no_work_start_evidence") {
|
||||
if (stallReason === "missing_dependency") {
|
||||
recommendations.push(
|
||||
`Investigate ${w.name}: task-${outstandingTask?.id ?? expectedTaskId} is blocked by missing task ids [${missingDependencyIds.join(", ")}]; pane is idle at prompt`
|
||||
);
|
||||
} else if (stallReason === "no_work_start_evidence") {
|
||||
recommendations.push(`Investigate ${w.name}: assigned work but no work-start evidence; pane is idle at prompt`);
|
||||
} else if (stallReason === "stale_or_missing_worker_reports") {
|
||||
recommendations.push(`Investigate ${w.name}: pane is idle while status/heartbeat are stale or missing`);
|
||||
@@ -5420,6 +5504,15 @@ async function monitorTeamV2(teamName, cwd) {
|
||||
failed: allTasks.filter((t) => t.status === "failed").length
|
||||
};
|
||||
const allTasksTerminal = taskCounts.pending === 0 && taskCounts.blocked === 0 && taskCounts.in_progress === 0;
|
||||
for (const task of allTasks) {
|
||||
const missingDependencyIds = getMissingDependencyIds(task, taskById);
|
||||
if (missingDependencyIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
recommendations.push(
|
||||
`Investigate task-${task.id}: depends on missing task ids [${missingDependencyIds.join(", ")}]`
|
||||
);
|
||||
}
|
||||
const phase = inferPhase(allTasks.map((t) => ({
|
||||
status: t.status,
|
||||
metadata: void 0
|
||||
@@ -5628,11 +5721,8 @@ async function resumeTeamV2(teamName, cwd) {
|
||||
const config = await readTeamConfig(sanitized, cwd);
|
||||
if (!config) return null;
|
||||
try {
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
const sessionName2 = config.tmux_session || `omc-team-${sanitized}`;
|
||||
await execFileAsync2("tmux", ["has-session", "-t", sessionName2.split(":")[0]]);
|
||||
await tmuxExecAsync(["has-session", "-t", sessionName2.split(":")[0]]);
|
||||
return {
|
||||
teamName: sanitized,
|
||||
sanitizedName: sanitized,
|
||||
@@ -5664,6 +5754,7 @@ var MONITOR_SIGNAL_STALE_MS, CIRCUIT_BREAKER_THRESHOLD, CircuitBreakerV2;
|
||||
var init_runtime_v2 = __esm({
|
||||
"src/team/runtime-v2.ts"() {
|
||||
"use strict";
|
||||
init_tmux_utils();
|
||||
init_state_paths();
|
||||
init_allocation_policy();
|
||||
init_monitor();
|
||||
@@ -5726,6 +5817,7 @@ import { existsSync as existsSync16, readFileSync as readFileSync9 } from "node:
|
||||
import { dirname as dirname12, join as join19, resolve as resolvePath } from "node:path";
|
||||
|
||||
// src/team/runtime.ts
|
||||
init_tmux_utils();
|
||||
init_model_contract();
|
||||
init_team_name();
|
||||
init_tmux_session();
|
||||
@@ -5893,17 +5985,14 @@ async function resumeTeam(teamName, cwd) {
|
||||
const root = stateRoot(cwd, teamName);
|
||||
const configData = await readJsonSafe2(join17(root, "config.json"));
|
||||
if (!configData) return null;
|
||||
const { execFile: execFile4 } = await import("child_process");
|
||||
const { promisify: promisify3 } = await import("util");
|
||||
const execFileAsync2 = promisify3(execFile4);
|
||||
const sName = configData.tmuxSession || `omc-team-${teamName}`;
|
||||
try {
|
||||
await execFileAsync2("tmux", ["has-session", "-t", sName.split(":")[0]]);
|
||||
await tmuxExecAsync(["has-session", "-t", sName.split(":")[0]]);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const paneTarget = sName.includes(":") ? sName : sName.split(":")[0];
|
||||
const panesResult = await execFileAsync2("tmux", [
|
||||
const panesResult = await tmuxExecAsync([
|
||||
"list-panes",
|
||||
"-t",
|
||||
paneTarget,
|
||||
|
||||
88
dist/__tests__/auto-update.test.js
generated
vendored
88
dist/__tests__/auto-update.test.js
generated
vendored
@@ -28,7 +28,6 @@ import { execSync, execFileSync } from 'child_process';
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { install, isProjectScopedPlugin, checkNodeVersion, CLAUDE_CONFIG_DIR } from '../installer/index.js';
|
||||
import * as hooksModule from '../installer/hooks.js';
|
||||
import { reconcileUpdateRuntime, performUpdate, shouldBlockStandaloneUpdateInCurrentSession, syncPluginCache, } from '../features/auto-update.js';
|
||||
const mockedExecSync = vi.mocked(execSync);
|
||||
const mockedExecFileSync = vi.mocked(execFileSync);
|
||||
@@ -82,11 +81,12 @@ describe('auto-update reconciliation', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
delete process.env.OMC_UPDATE_RECONCILE;
|
||||
delete process.env.CLAUDE_PLUGIN_ROOT;
|
||||
if (originalPlatformDescriptor) {
|
||||
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
|
||||
}
|
||||
});
|
||||
it('reconciles runtime state and refreshes hooks after update', () => {
|
||||
it('reconciles runtime state without re-injecting settings hooks', () => {
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
const result = reconcileUpdateRuntime({ verbose: false });
|
||||
expect(result.success).toBe(true);
|
||||
@@ -399,10 +399,11 @@ describe('auto-update reconciliation', () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockedExecSync).toHaveBeenCalledWith('npm install -g oh-my-claude-sisyphus@latest', expect.any(Object));
|
||||
});
|
||||
it('runs reconciliation as part of performUpdate', async () => {
|
||||
it('runs reconciliation as part of performUpdate without plugin hook reinjection', async () => {
|
||||
// Set env var so performUpdate takes the direct reconciliation path
|
||||
// (simulates being in the re-exec'd process after npm install)
|
||||
process.env.OMC_UPDATE_RECONCILE = '1';
|
||||
process.env.CLAUDE_PLUGIN_ROOT = join(CLAUDE_CONFIG_DIR, 'plugins', 'cache', 'omc', 'oh-my-claudecode', '4.1.5');
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@@ -423,8 +424,8 @@ describe('auto-update reconciliation', () => {
|
||||
force: true,
|
||||
verbose: false,
|
||||
skipClaudeCheck: true,
|
||||
forceHooks: true,
|
||||
refreshHooksInPlugin: true,
|
||||
forceHooks: false,
|
||||
refreshHooksInPlugin: false,
|
||||
});
|
||||
delete process.env.OMC_UPDATE_RECONCILE;
|
||||
});
|
||||
@@ -693,82 +694,5 @@ describe('auto-update reconciliation', () => {
|
||||
refreshHooksInPlugin: false,
|
||||
});
|
||||
});
|
||||
it('preserves non-OMC hooks when refreshing plugin hooks during reconciliation', () => {
|
||||
const existingSettings = {
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node $HOME/.claude/hooks/other-plugin.mjs',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const settingsPath = join(CLAUDE_CONFIG_DIR, 'settings.json');
|
||||
const baseHooks = hooksModule.getHooksSettingsConfig();
|
||||
const freshHooks = {
|
||||
...baseHooks,
|
||||
hooks: {
|
||||
...baseHooks.hooks,
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node $HOME/.claude/hooks/keyword-detector.mjs',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
const normalized = String(path).replace(/\\/g, '/');
|
||||
if (normalized === settingsPath) {
|
||||
return true;
|
||||
}
|
||||
if (normalized.endsWith('/.claude/hud')) {
|
||||
return false;
|
||||
}
|
||||
if (normalized.includes('/hooks/')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
mockedIsProjectScopedPlugin.mockReturnValue(false);
|
||||
mockedReadFileSync.mockImplementation((path) => {
|
||||
if (String(path) === settingsPath) {
|
||||
return JSON.stringify(existingSettings);
|
||||
}
|
||||
if (String(path).includes('/hooks/')) {
|
||||
return 'hook-script';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
vi.spyOn(hooksModule, 'getHooksSettingsConfig').mockReturnValue(freshHooks);
|
||||
const originalPluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
process.env.CLAUDE_PLUGIN_ROOT = join(CLAUDE_CONFIG_DIR, 'plugins', 'cache', 'omc', 'oh-my-claudecode', '4.1.5');
|
||||
const result = install({
|
||||
force: true,
|
||||
skipClaudeCheck: true,
|
||||
refreshHooksInPlugin: true,
|
||||
});
|
||||
if (originalPluginRoot !== undefined) {
|
||||
process.env.CLAUDE_PLUGIN_ROOT = originalPluginRoot;
|
||||
}
|
||||
else {
|
||||
delete process.env.CLAUDE_PLUGIN_ROOT;
|
||||
}
|
||||
const settingsWrite = mockedWriteFileSync.mock.calls.find((call) => String(call[0]).includes('settings.json'));
|
||||
if (settingsWrite) {
|
||||
const writtenSettings = JSON.parse(String(settingsWrite[1]));
|
||||
expect(writtenSettings.hooks.UserPromptSubmit[0].hooks[0].command).toBe('node $HOME/.claude/hooks/other-plugin.mjs');
|
||||
}
|
||||
expect(result.hooksConfigured).toBe(true);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=auto-update.test.js.map
|
||||
2
dist/__tests__/auto-update.test.js.map
generated
vendored
2
dist/__tests__/auto-update.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
12
dist/__tests__/background-cleanup-directory.test.js
generated
vendored
12
dist/__tests__/background-cleanup-directory.test.js
generated
vendored
@@ -18,7 +18,7 @@ describe('background-cleanup directory propagation', () => {
|
||||
// defaulting to process.cwd() instead of the actual project directory.
|
||||
readHudStateMock.mockReturnValue(null);
|
||||
await cleanupStaleBackgroundTasks(undefined, '/custom/project/dir');
|
||||
expect(readHudStateMock).toHaveBeenCalledWith('/custom/project/dir');
|
||||
expect(readHudStateMock).toHaveBeenCalledWith('/custom/project/dir', undefined);
|
||||
});
|
||||
it('cleanupStaleBackgroundTasks should pass directory to writeHudState when cleaning', async () => {
|
||||
const staleTask = {
|
||||
@@ -28,12 +28,12 @@ describe('background-cleanup directory propagation', () => {
|
||||
};
|
||||
readHudStateMock.mockReturnValue({ backgroundTasks: [staleTask] });
|
||||
await cleanupStaleBackgroundTasks(undefined, '/custom/project/dir');
|
||||
expect(writeHudStateMock).toHaveBeenCalledWith(expect.objectContaining({ backgroundTasks: expect.any(Array) }), '/custom/project/dir');
|
||||
expect(writeHudStateMock).toHaveBeenCalledWith(expect.objectContaining({ backgroundTasks: expect.any(Array) }), '/custom/project/dir', undefined);
|
||||
});
|
||||
it('markOrphanedTasksAsStale should pass directory to readHudState', async () => {
|
||||
readHudStateMock.mockReturnValue(null);
|
||||
await markOrphanedTasksAsStale('/custom/project/dir');
|
||||
expect(readHudStateMock).toHaveBeenCalledWith('/custom/project/dir');
|
||||
expect(readHudStateMock).toHaveBeenCalledWith('/custom/project/dir', undefined);
|
||||
});
|
||||
it('markOrphanedTasksAsStale should pass directory to writeHudState when marking', async () => {
|
||||
const orphanedTask = {
|
||||
@@ -43,15 +43,15 @@ describe('background-cleanup directory propagation', () => {
|
||||
};
|
||||
readHudStateMock.mockReturnValue({ backgroundTasks: [orphanedTask] });
|
||||
await markOrphanedTasksAsStale('/custom/project/dir');
|
||||
expect(writeHudStateMock).toHaveBeenCalledWith(expect.objectContaining({ backgroundTasks: expect.any(Array) }), '/custom/project/dir');
|
||||
expect(writeHudStateMock).toHaveBeenCalledWith(expect.objectContaining({ backgroundTasks: expect.any(Array) }), '/custom/project/dir', undefined);
|
||||
});
|
||||
it('functions should default to no directory when not provided', async () => {
|
||||
readHudStateMock.mockReturnValue(null);
|
||||
await cleanupStaleBackgroundTasks();
|
||||
expect(readHudStateMock).toHaveBeenCalledWith(undefined);
|
||||
expect(readHudStateMock).toHaveBeenCalledWith(undefined, undefined);
|
||||
readHudStateMock.mockReset();
|
||||
await markOrphanedTasksAsStale();
|
||||
expect(readHudStateMock).toHaveBeenCalledWith(undefined);
|
||||
expect(readHudStateMock).toHaveBeenCalledWith(undefined, undefined);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=background-cleanup-directory.test.js.map
|
||||
2
dist/__tests__/background-cleanup-directory.test.js.map
generated
vendored
2
dist/__tests__/background-cleanup-directory.test.js.map
generated
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"background-cleanup-directory.test.js","sourceRoot":"","sources":["../../src/__tests__/background-cleanup-directory.test.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"background-cleanup-directory.test.js","sourceRoot":"","sources":["../../src/__tests__/background-cleanup-directory.test.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/__tests__/bridge-help-question-regex.test.d.ts
generated
vendored
Normal file
2
dist/__tests__/bridge-help-question-regex.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=bridge-help-question-regex.test.d.ts.map
|
||||
1
dist/__tests__/bridge-help-question-regex.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/bridge-help-question-regex.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"bridge-help-question-regex.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bridge-help-question-regex.test.ts"],"names":[],"mappings":""}
|
||||
20
dist/__tests__/bridge-help-question-regex.test.js
generated
vendored
Normal file
20
dist/__tests__/bridge-help-question-regex.test.js
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const REPO_ROOT = join(__dirname, '..', '..');
|
||||
describe('bridge/cli.cjs help-question regex regression (#2482)', () => {
|
||||
it('keeps escaped help-question regex sequences intact in the baked bridge artifact', () => {
|
||||
const source = readFileSync(join(REPO_ROOT, 'bridge', 'cli.cjs'), 'utf-8');
|
||||
const marker = 'const helpQuestionPatterns = [';
|
||||
const start = source.indexOf(marker);
|
||||
const snippet = start === -1 ? '' : source.slice(start, start + 260);
|
||||
expect(snippet).toContain("\\\\bhow\\\\s+do\\\\s+i\\\\s+use\\\\b[^\\\\n]{0,40}\\\\b${escaped}\\\\b");
|
||||
expect(snippet).toContain("\\\\bwhat(?:'s|\\\\s+is)\\\\b[^\\\\n]{0,40}\\\\b${escaped}\\\\b[^\\\\n]{0,40}\\\\bhow\\\\s+to\\\\s+use\\\\b");
|
||||
expect(snippet).not.toContain("\\bhows+dos+is+use\\b");
|
||||
expect(snippet).not.toContain("\\bwhat(?:'s|s+is)\\b");
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=bridge-help-question-regex.test.js.map
|
||||
1
dist/__tests__/bridge-help-question-regex.test.js.map
generated
vendored
Normal file
1
dist/__tests__/bridge-help-question-regex.test.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"bridge-help-question-regex.test.js","sourceRoot":"","sources":["../../src/__tests__/bridge-help-question-regex.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AACtC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAE9C,QAAQ,CAAC,uDAAuD,EAAE,GAAG,EAAE;IACrE,EAAE,CAAC,iFAAiF,EAAE,GAAG,EAAE;QACzF,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;QAC3E,MAAM,MAAM,GAAG,gCAAgC,CAAC;QAChD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,OAAO,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,GAAG,CAAC,CAAC;QAErE,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,yEAAyE,CAAC,CAAC;QACrG,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,6GAA6G,CAAC,CAAC;QACzI,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;QACvD,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
||||
16
dist/__tests__/context-bloat-2577.test.d.ts
generated
vendored
Normal file
16
dist/__tests__/context-bloat-2577.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Regression tests for issue #2577: context window bloat
|
||||
*
|
||||
* Three bugs fixed:
|
||||
* Bug 1 – Skill-injector fallback used an in-memory Map that reset on every
|
||||
* process spawn. Fixed by persisting state to a JSON file.
|
||||
* Bug 2 – Rules-injector module was never wired into hooks.json.
|
||||
* Fixed by adding post-tool-rules-injector.mjs to PostToolUse.
|
||||
* Bug 3 – In a git worktree nested inside the parent repo, rules from the
|
||||
* parent repo could bleed into the worktree session.
|
||||
* Fixed: projectRoot is derived from the accessed FILE's path via
|
||||
* findProjectRoot, not from data.cwd, so the .git FILE at the
|
||||
* worktree root terminates the upward walk before the parent.
|
||||
*/
|
||||
export {};
|
||||
//# sourceMappingURL=context-bloat-2577.test.d.ts.map
|
||||
1
dist/__tests__/context-bloat-2577.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/context-bloat-2577.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"context-bloat-2577.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/context-bloat-2577.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
|
||||
171
dist/__tests__/context-bloat-2577.test.js
generated
vendored
Normal file
171
dist/__tests__/context-bloat-2577.test.js
generated
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Regression tests for issue #2577: context window bloat
|
||||
*
|
||||
* Three bugs fixed:
|
||||
* Bug 1 – Skill-injector fallback used an in-memory Map that reset on every
|
||||
* process spawn. Fixed by persisting state to a JSON file.
|
||||
* Bug 2 – Rules-injector module was never wired into hooks.json.
|
||||
* Fixed by adding post-tool-rules-injector.mjs to PostToolUse.
|
||||
* Bug 3 – In a git worktree nested inside the parent repo, rules from the
|
||||
* parent repo could bleed into the worktree session.
|
||||
* Fixed: projectRoot is derived from the accessed FILE's path via
|
||||
* findProjectRoot, not from data.cwd, so the .git FILE at the
|
||||
* worktree root terminates the upward walk before the parent.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { createRulesInjectorHook, clearInjectedRules, } from '../hooks/rules-injector/index.js';
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
function tmpDir() {
|
||||
const p = join(tmpdir(), `omc-2577-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
mkdirSync(p, { recursive: true });
|
||||
return p;
|
||||
}
|
||||
function makeProjectRoot(dir) {
|
||||
// Simulate a git repo root by writing a .git FILE (as git worktree does)
|
||||
writeFileSync(join(dir, '.git'), 'gitdir: placeholder');
|
||||
}
|
||||
/** Wrap content with alwaysApply frontmatter so shouldApplyRule returns applies:true */
|
||||
function ruleContent(body) {
|
||||
return `---\nalwaysApply: true\n---\n${body}`;
|
||||
}
|
||||
function addRule(projectDir, name, content, subdir = '.claude/rules') {
|
||||
const rulesDir = join(projectDir, subdir);
|
||||
mkdirSync(rulesDir, { recursive: true });
|
||||
writeFileSync(join(rulesDir, name), content);
|
||||
}
|
||||
function addFile(projectDir, relPath) {
|
||||
const full = join(projectDir, relPath);
|
||||
mkdirSync(join(full, '..'), { recursive: true });
|
||||
writeFileSync(full, '// test');
|
||||
return full;
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bug 2 – rules-injector injection correctness
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Bug 2 – rules-injector injects on first access, deduplicates on second', () => {
|
||||
let dir;
|
||||
let sessionId;
|
||||
beforeEach(() => {
|
||||
dir = tmpDir();
|
||||
makeProjectRoot(dir);
|
||||
sessionId = `s-${Date.now()}`;
|
||||
});
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
clearInjectedRules(sessionId);
|
||||
});
|
||||
it('injects rule content on the first read of a project file', () => {
|
||||
addRule(dir, 'style.md', ruleContent('# Style Guide\nUse single quotes.'));
|
||||
const file = addFile(dir, 'src/foo.ts');
|
||||
const hook = createRulesInjectorHook(dir);
|
||||
const result = hook.processToolExecution('read', file, sessionId);
|
||||
expect(result).toContain('Style Guide');
|
||||
expect(result).toContain('style.md');
|
||||
});
|
||||
it('does NOT re-inject the same rule on a subsequent file access (content-hash dedup)', () => {
|
||||
addRule(dir, 'style.md', ruleContent('# Style Guide\nUse single quotes.'));
|
||||
const file1 = addFile(dir, 'src/foo.ts');
|
||||
const file2 = addFile(dir, 'src/bar.ts');
|
||||
const hook = createRulesInjectorHook(dir);
|
||||
const first = hook.processToolExecution('read', file1, sessionId);
|
||||
const second = hook.processToolExecution('read', file2, sessionId);
|
||||
expect(first).toBeTruthy(); // injected first time
|
||||
expect(second).toBe(''); // same content-hash → skip
|
||||
});
|
||||
it('does NOT re-inject when a new hook instance loads the same session (file-backed dedup)', () => {
|
||||
addRule(dir, 'style.md', ruleContent('# Style Guide\nUse single quotes.'));
|
||||
const file = addFile(dir, 'src/foo.ts');
|
||||
// First hook instance (simulates first process spawn)
|
||||
const hook1 = createRulesInjectorHook(dir);
|
||||
hook1.processToolExecution('read', file, sessionId);
|
||||
// Second hook instance with same sessionId (simulates next process spawn)
|
||||
const hook2 = createRulesInjectorHook(dir);
|
||||
const result = hook2.processToolExecution('read', file, sessionId);
|
||||
expect(result).toBe(''); // already injected → skip
|
||||
});
|
||||
it('returns empty string for non-tracked tools', () => {
|
||||
addRule(dir, 'style.md', ruleContent('# Style Guide\nUse single quotes.'));
|
||||
const file = addFile(dir, 'src/foo.ts');
|
||||
const hook = createRulesInjectorHook(dir);
|
||||
expect(hook.processToolExecution('bash', file, sessionId)).toBe('');
|
||||
expect(hook.processToolExecution('listfiles', file, sessionId)).toBe('');
|
||||
});
|
||||
it('injects rules from .github/instructions', () => {
|
||||
addRule(dir, 'coding.instructions.md', ruleContent('# Coding Instructions\nAlways add tests.'), '.github/instructions');
|
||||
const file = addFile(dir, 'src/feature.ts');
|
||||
const hook = createRulesInjectorHook(dir);
|
||||
const result = hook.processToolExecution('edit', file, sessionId);
|
||||
expect(result).toContain('Always add tests');
|
||||
});
|
||||
it('handles multiedit tool', () => {
|
||||
addRule(dir, 'style.md', ruleContent('# Style\nNo semicolons.'));
|
||||
const file = addFile(dir, 'src/multi.ts');
|
||||
const hook = createRulesInjectorHook(dir);
|
||||
const result = hook.processToolExecution('multiedit', file, sessionId);
|
||||
expect(result).toContain('No semicolons');
|
||||
});
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bug 3 – worktree isolation: parent-repo rules must not bleed in
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('Bug 3 – nested worktree isolation: only worktree rules are injected', () => {
|
||||
let base;
|
||||
let mainRepo;
|
||||
let worktree;
|
||||
let sessionId;
|
||||
beforeEach(() => {
|
||||
// Layout:
|
||||
// base/ ← main repo (.git/ directory)
|
||||
// .claude/rules/main.md
|
||||
// src/main.ts
|
||||
// feature/ ← nested git worktree (.git FILE)
|
||||
// .claude/rules/feature.md
|
||||
// src/feature.ts
|
||||
base = tmpDir();
|
||||
mainRepo = base;
|
||||
worktree = join(base, 'feature');
|
||||
// Main repo: use a .git DIRECTORY to simulate a real repo root
|
||||
mkdirSync(join(mainRepo, '.git'), { recursive: true });
|
||||
addRule(mainRepo, 'main.md', ruleContent('# Main Repo Rule'));
|
||||
addFile(mainRepo, 'src/main.ts');
|
||||
// Nested worktree: .git FILE stops findProjectRoot before the parent
|
||||
mkdirSync(worktree, { recursive: true });
|
||||
writeFileSync(join(worktree, '.git'), 'gitdir: ../.git/worktrees/feature');
|
||||
addRule(worktree, 'feature.md', ruleContent('# Feature Branch Rule'));
|
||||
addFile(worktree, 'src/feature.ts');
|
||||
sessionId = `wt-${Date.now()}`;
|
||||
});
|
||||
afterEach(() => {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
clearInjectedRules(sessionId);
|
||||
});
|
||||
it('injects only worktree rules when accessing a worktree file, even when cwd=mainRepo', () => {
|
||||
// Claude was started from mainRepo (data.cwd = mainRepo) but accesses a
|
||||
// worktree file. findProjectRoot(file) finds worktree/.git FILE first.
|
||||
const hook = createRulesInjectorHook(mainRepo);
|
||||
const result = hook.processToolExecution('read', join(worktree, 'src', 'feature.ts'), sessionId);
|
||||
expect(result).toContain('Feature Branch Rule');
|
||||
expect(result).not.toContain('Main Repo Rule');
|
||||
});
|
||||
it('injects only main-repo rules when accessing a main-repo file', () => {
|
||||
const hook = createRulesInjectorHook(mainRepo);
|
||||
const result = hook.processToolExecution('read', join(mainRepo, 'src', 'main.ts'), sessionId);
|
||||
expect(result).toContain('Main Repo Rule');
|
||||
expect(result).not.toContain('Feature Branch Rule');
|
||||
});
|
||||
it('deduplicates across roots within the same session', () => {
|
||||
// Access worktree file → feature rule injected
|
||||
const hook = createRulesInjectorHook(mainRepo);
|
||||
const r1 = hook.processToolExecution('read', join(worktree, 'src', 'feature.ts'), sessionId);
|
||||
expect(r1).toContain('Feature Branch Rule');
|
||||
// Access same worktree file again → already injected
|
||||
const r2 = hook.processToolExecution('read', join(worktree, 'src', 'feature.ts'), sessionId);
|
||||
expect(r2).toBe('');
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=context-bloat-2577.test.js.map
|
||||
1
dist/__tests__/context-bloat-2577.test.js.map
generated
vendored
Normal file
1
dist/__tests__/context-bloat-2577.test.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
38
dist/__tests__/context-guard-stop.test.js
generated
vendored
38
dist/__tests__/context-guard-stop.test.js
generated
vendored
@@ -1,11 +1,11 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { delimiter, join } from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
const SCRIPT_PATH = join(process.cwd(), 'scripts', 'context-guard-stop.mjs');
|
||||
function runContextGuardStop(input) {
|
||||
const stdout = execSync(`node "${SCRIPT_PATH}"`, {
|
||||
const stdout = execFileSync(process.execPath, [SCRIPT_PATH], {
|
||||
input: JSON.stringify(input),
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
@@ -13,6 +13,15 @@ function runContextGuardStop(input) {
|
||||
});
|
||||
return JSON.parse(stdout.trim());
|
||||
}
|
||||
function runContextGuardStopWithEnv(input, env) {
|
||||
const stdout = execFileSync(process.execPath, [SCRIPT_PATH], {
|
||||
input: JSON.stringify(input),
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
env: { ...process.env, NODE_ENV: 'test', ...env },
|
||||
});
|
||||
return JSON.parse(stdout.trim());
|
||||
}
|
||||
function writeTranscriptWithContext(filePath, contextWindow, inputTokens) {
|
||||
const line = JSON.stringify({
|
||||
usage: { context_window: contextWindow, input_tokens: inputTokens },
|
||||
@@ -74,5 +83,26 @@ describe('context-guard-stop safe recovery messaging (issue #1373)', () => {
|
||||
expect(String(first.reason)).toContain('(Block 1/2)');
|
||||
expect(String(second.reason)).toContain('(Block 1/2)');
|
||||
});
|
||||
it('skips git worktree probing in non-git directories without a local .git marker', () => {
|
||||
const missingTranscriptPath = join(tempDir, 'missing-transcript.jsonl');
|
||||
const fakeBinDir = join(tempDir, 'fake-bin');
|
||||
mkdirSync(fakeBinDir, { recursive: true });
|
||||
const gitLogPath = join(tempDir, 'git-invocations.log');
|
||||
writeFileSync(join(fakeBinDir, 'git'), '#!/usr/bin/env node\n' +
|
||||
'require("fs").appendFileSync(process.env.OMC_FAKE_GIT_LOG, process.argv.slice(2).join(" ") + "\\n");\n' +
|
||||
'process.exit(1);\n', { mode: 0o755 });
|
||||
writeFileSync(join(fakeBinDir, 'git.cmd'), '@echo off\r\nnode "%~dp0\\git" %*\r\n');
|
||||
const out = runContextGuardStopWithEnv({
|
||||
session_id: `session-${Date.now()}`,
|
||||
transcript_path: missingTranscriptPath,
|
||||
cwd: tempDir,
|
||||
stop_reason: 'normal',
|
||||
}, {
|
||||
PATH: `${fakeBinDir}${delimiter}${process.env.PATH ?? ''}`,
|
||||
OMC_FAKE_GIT_LOG: gitLogPath,
|
||||
});
|
||||
expect(out).toEqual({ continue: true, suppressOutput: true });
|
||||
expect(() => readFileSync(gitLogPath, 'utf-8')).toThrow();
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=context-guard-stop.test.js.map
|
||||
2
dist/__tests__/context-guard-stop.test.js.map
generated
vendored
2
dist/__tests__/context-guard-stop.test.js.map
generated
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"context-guard-stop.test.js","sourceRoot":"","sources":["../../src/__tests__/context-guard-stop.test.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"context-guard-stop.test.js","sourceRoot":"","sources":["../../src/__tests__/context-guard-stop.test.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/__tests__/delegation-enforcement-levels.test.js
generated
vendored
2
dist/__tests__/delegation-enforcement-levels.test.js
generated
vendored
@@ -508,7 +508,7 @@ describe('delegation-enforcement-levels', () => {
|
||||
directory: '/tmp/test-project',
|
||||
});
|
||||
expect(result.continue).toBe(true);
|
||||
expect(mockAddTask).toHaveBeenCalledWith(expect.stringContaining('task-'), 'Test task', 'executor', process.cwd());
|
||||
expect(mockAddTask).toHaveBeenCalledWith(expect.stringContaining('task-'), 'Test task', 'executor', process.cwd(), undefined);
|
||||
});
|
||||
});
|
||||
// ─── Helper function unit tests ───
|
||||
|
||||
2
dist/__tests__/delegation-enforcement-levels.test.js.map
generated
vendored
2
dist/__tests__/delegation-enforcement-levels.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
36
dist/__tests__/hud-marketplace-resolution.test.js
generated
vendored
36
dist/__tests__/hud-marketplace-resolution.test.js
generated
vendored
@@ -91,6 +91,42 @@ describe('HUD marketplace resolution', () => {
|
||||
});
|
||||
expect(readFileSync(sentinelPath, 'utf-8')).toBe('marketplace-loaded');
|
||||
});
|
||||
it('omc-hud.mjs surfaces dynamic import errors from OMC_PLUGIN_ROOT HUD paths', () => {
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'omc-hud-import-error-'));
|
||||
tempDirs.push(configDir);
|
||||
const fakeHome = join(configDir, 'home');
|
||||
mkdirSync(fakeHome, { recursive: true });
|
||||
execFileSync(process.execPath, [join(root, 'scripts', 'plugin-setup.mjs')], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_CONFIG_DIR: configDir,
|
||||
HOME: fakeHome,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const pluginRoot = join(configDir, 'broken-plugin-root');
|
||||
const pluginHudDir = join(pluginRoot, 'dist', 'hud');
|
||||
mkdirSync(pluginHudDir, { recursive: true });
|
||||
writeFileSync(join(pluginRoot, 'package.json'), '{"type":"module"}\n');
|
||||
writeFileSync(join(pluginHudDir, 'index.js'), "import '../platform/index.js';\n");
|
||||
const hudScriptPath = join(configDir, 'hud', 'omc-hud.mjs');
|
||||
const output = execFileSync(process.execPath, [hudScriptPath], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_CONFIG_DIR: configDir,
|
||||
HOME: fakeHome,
|
||||
OMC_PLUGIN_ROOT: pluginRoot,
|
||||
OMC_HUD_DISABLE_NPM_FALLBACK: '1',
|
||||
},
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
const normalized = output.replace(/\\/g, '/');
|
||||
expect(normalized).toContain('[OMC HUD] HUD import failed from');
|
||||
expect(normalized).toContain('/broken-plugin-root/dist/hud/index.js');
|
||||
});
|
||||
it('omc-hud.mjs loads a global npm install outside a Node project via npm prefix resolution', () => {
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'omc-hud-global-prefix-'));
|
||||
tempDirs.push(configDir);
|
||||
|
||||
2
dist/__tests__/hud-marketplace-resolution.test.js.map
generated
vendored
2
dist/__tests__/hud-marketplace-resolution.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/__tests__/hud-skill-no-inline-wrapper.test.d.ts
generated
vendored
Normal file
2
dist/__tests__/hud-skill-no-inline-wrapper.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=hud-skill-no-inline-wrapper.test.d.ts.map
|
||||
1
dist/__tests__/hud-skill-no-inline-wrapper.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/hud-skill-no-inline-wrapper.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"hud-skill-no-inline-wrapper.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hud-skill-no-inline-wrapper.test.ts"],"names":[],"mappings":""}
|
||||
26
dist/__tests__/hud-skill-no-inline-wrapper.test.js
generated
vendored
Normal file
26
dist/__tests__/hud-skill-no-inline-wrapper.test.js
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const root = join(__dirname, '..', '..');
|
||||
const SKILL_PATH = join(root, 'skills', 'hud', 'SKILL.md');
|
||||
describe('HUD skill — no inline wrapper', () => {
|
||||
const content = readFileSync(SKILL_PATH, 'utf8');
|
||||
it('does not embed an inline HUD wrapper script', () => {
|
||||
// The canonical wrapper lives in scripts/lib/hud-wrapper-template.txt.
|
||||
// The skill must copy from there, not embed its own version.
|
||||
// Match signatures unique to the wrapper body that should never appear inline.
|
||||
expect(content).not.toMatch(/async function main\(\)\s*\{/);
|
||||
expect(content).not.toMatch(/OMC_DEV.*===.*"1"/);
|
||||
expect(content).not.toMatch(/import.*from\s*["']node:fs["']/);
|
||||
});
|
||||
it('references the canonical template for installation', () => {
|
||||
expect(content).toMatch(/hud-wrapper-template\.txt/);
|
||||
});
|
||||
it('copies config-dir.mjs dependency', () => {
|
||||
expect(content).toMatch(/config-dir\.mjs/);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=hud-skill-no-inline-wrapper.test.js.map
|
||||
1
dist/__tests__/hud-skill-no-inline-wrapper.test.js.map
generated
vendored
Normal file
1
dist/__tests__/hud-skill-no-inline-wrapper.test.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"hud-skill-no-inline-wrapper.test.js","sourceRoot":"","sources":["../../src/__tests__/hud-skill-no-inline-wrapper.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AACtC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEzC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;AAE3D,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC7C,MAAM,OAAO,GAAG,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAEjD,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,uEAAuE;QACvE,6DAA6D;QAC7D,+EAA+E;QAC/E,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC;QAC5D,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACjD,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
||||
3
dist/__tests__/hud-windows.test.js
generated
vendored
3
dist/__tests__/hud-windows.test.js
generated
vendored
@@ -177,7 +177,8 @@ describe('HUD Windows Compatibility', () => {
|
||||
const usageApiPath = join(packageRoot, 'src', 'hud', 'usage-api.ts');
|
||||
const content = readFileSync(usageApiPath, 'utf-8');
|
||||
// Should use join() with separate segments, not forward-slash literals
|
||||
expect(content).toContain("'plugins', 'oh-my-claudecode', '.usage-cache.json'");
|
||||
// Provider-specific cache files use template literals with the same join() pattern
|
||||
expect(content).toContain("'plugins', 'oh-my-claudecode', `.usage-cache-${source}.json`");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
2
dist/__tests__/hud-windows.test.js.map
generated
vendored
2
dist/__tests__/hud-windows.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/__tests__/hud/background-tasks.test.js
generated
vendored
2
dist/__tests__/hud/background-tasks.test.js
generated
vendored
@@ -80,7 +80,7 @@ describe('background-tasks', () => {
|
||||
sessionId: 'dir-session',
|
||||
});
|
||||
clearBackgroundTasks('/some/dir');
|
||||
expect(mockReadHudState).toHaveBeenCalledWith('/some/dir');
|
||||
expect(mockReadHudState).toHaveBeenCalledWith('/some/dir', undefined);
|
||||
const writtenState = mockWriteHudState.mock.calls[0][0];
|
||||
expect(writtenState.sessionStartTimestamp).toBe(sessionStart);
|
||||
expect(writtenState.sessionId).toBe('dir-session');
|
||||
|
||||
2
dist/__tests__/hud/background-tasks.test.js.map
generated
vendored
2
dist/__tests__/hud/background-tasks.test.js.map
generated
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"background-tasks.test.js","sourceRoot":"","sources":["../../../src/__tests__/hud/background-tasks.test.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"background-tasks.test.js","sourceRoot":"","sources":["../../../src/__tests__/hud/background-tasks.test.ts"],"names":[],"mappings":"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"}
|
||||
6
dist/__tests__/hud/extra-usage.test.d.ts
generated
vendored
Normal file
6
dist/__tests__/hud/extra-usage.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Tests for extra usage (metered spend) data parsing and rendering.
|
||||
* Covers issue #2570: display $spent/$limit in HUD.
|
||||
*/
|
||||
export {};
|
||||
//# sourceMappingURL=extra-usage.test.d.ts.map
|
||||
1
dist/__tests__/hud/extra-usage.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/hud/extra-usage.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"extra-usage.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/hud/extra-usage.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
||||
222
dist/__tests__/hud/extra-usage.test.js
generated
vendored
Normal file
222
dist/__tests__/hud/extra-usage.test.js
generated
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Tests for extra usage (metered spend) data parsing and rendering.
|
||||
* Covers issue #2570: display $spent/$limit in HUD.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseUsageResponse } from '../../hud/usage-api.js';
|
||||
import { renderRateLimits, renderRateLimitsCompact, renderRateLimitsWithBar, } from '../../hud/elements/limits.js';
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseUsageResponse — extra_usage parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('parseUsageResponse — extra_usage', () => {
|
||||
it('ignores extra_usage when limit_usd is absent', () => {
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { spent_usd: 3.1 },
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.extraUsagePercent).toBeUndefined();
|
||||
expect(result.extraUsageSpentUsd).toBeUndefined();
|
||||
expect(result.extraUsageLimitUsd).toBeUndefined();
|
||||
});
|
||||
it('ignores extra_usage when limit_usd is zero', () => {
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { spent_usd: 0, limit_usd: 0 },
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.extraUsagePercent).toBeUndefined();
|
||||
});
|
||||
it('parses extra_usage with API-provided utilization', () => {
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { utilization: 18, spent_usd: 3.1, limit_usd: 17.0 },
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.extraUsagePercent).toBe(18);
|
||||
expect(result.extraUsageSpentUsd).toBeCloseTo(3.1);
|
||||
expect(result.extraUsageLimitUsd).toBeCloseTo(17.0);
|
||||
expect(result.extraUsageResetsAt).toBeNull();
|
||||
});
|
||||
it('derives utilization from spent/limit when API utilization is absent', () => {
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { spent_usd: 5, limit_usd: 20 },
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
// 5/20 = 25%
|
||||
expect(result.extraUsagePercent).toBe(25);
|
||||
});
|
||||
it('clamps utilization above 100 to 100', () => {
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { utilization: 150, spent_usd: 20, limit_usd: 17 },
|
||||
});
|
||||
expect(result.extraUsagePercent).toBe(100);
|
||||
});
|
||||
it('clamps negative utilization to 0', () => {
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { utilization: -5, spent_usd: 0, limit_usd: 17 },
|
||||
});
|
||||
expect(result.extraUsagePercent).toBe(0);
|
||||
});
|
||||
it('parses resets_at as a Date when present', () => {
|
||||
const resetIso = '2026-05-01T00:00:00Z';
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { spent_usd: 3.1, limit_usd: 17, resets_at: resetIso },
|
||||
});
|
||||
expect(result.extraUsageResetsAt).toBeInstanceOf(Date);
|
||||
expect(result.extraUsageResetsAt.getTime()).toBe(new Date(resetIso).getTime());
|
||||
});
|
||||
it('treats invalid resets_at as null', () => {
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { spent_usd: 3.1, limit_usd: 17, resets_at: 'not-a-date' },
|
||||
});
|
||||
expect(result.extraUsageResetsAt).toBeNull();
|
||||
});
|
||||
it('defaults spent_usd to 0 when absent', () => {
|
||||
const result = parseUsageResponse({
|
||||
five_hour: { utilization: 10 },
|
||||
extra_usage: { limit_usd: 17 },
|
||||
});
|
||||
expect(result.extraUsageSpentUsd).toBe(0);
|
||||
expect(result.extraUsagePercent).toBe(0);
|
||||
});
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// renderRateLimits — extra usage display
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('renderRateLimits — extra usage', () => {
|
||||
const base = { fiveHourPercent: 10 };
|
||||
it('omits extra section when extraUsagePercent is absent', () => {
|
||||
const result = renderRateLimits(base);
|
||||
expect(result).not.toContain('extra:');
|
||||
});
|
||||
it('omits extra section when extraUsageLimitUsd is absent', () => {
|
||||
const limits = { ...base, extraUsagePercent: 18 };
|
||||
const result = renderRateLimits(limits);
|
||||
expect(result).not.toContain('extra:');
|
||||
});
|
||||
it('renders extra usage with dollar amounts', () => {
|
||||
const limits = {
|
||||
...base,
|
||||
extraUsagePercent: 18,
|
||||
extraUsageSpentUsd: 3.10,
|
||||
extraUsageLimitUsd: 17.00,
|
||||
};
|
||||
const result = renderRateLimits(limits);
|
||||
expect(result).toContain('extra:');
|
||||
expect(result).toContain('18%');
|
||||
expect(result).toContain('$3.10');
|
||||
expect(result).toContain('$17.00');
|
||||
});
|
||||
it('renders 0% extra usage with correct dollar amounts', () => {
|
||||
const limits = {
|
||||
...base,
|
||||
extraUsagePercent: 0,
|
||||
extraUsageSpentUsd: 0,
|
||||
extraUsageLimitUsd: 17.00,
|
||||
};
|
||||
const result = renderRateLimits(limits);
|
||||
expect(result).toContain('extra:');
|
||||
expect(result).toContain('0%');
|
||||
expect(result).toContain('$0.00');
|
||||
expect(result).toContain('$17.00');
|
||||
});
|
||||
it('defaults spent to $0.00 when extraUsageSpentUsd is absent', () => {
|
||||
const limits = {
|
||||
...base,
|
||||
extraUsagePercent: 5,
|
||||
extraUsageLimitUsd: 10,
|
||||
};
|
||||
const result = renderRateLimits(limits);
|
||||
expect(result).toContain('$0.00');
|
||||
expect(result).toContain('$10.00');
|
||||
});
|
||||
it('uses red color at >= 90%', () => {
|
||||
const limits = {
|
||||
...base,
|
||||
extraUsagePercent: 95,
|
||||
extraUsageSpentUsd: 16,
|
||||
extraUsageLimitUsd: 17,
|
||||
};
|
||||
const result = renderRateLimits(limits);
|
||||
// Red ANSI code before the percentage
|
||||
expect(result).toContain('\x1b[31m');
|
||||
});
|
||||
it('uses green color at < 70%', () => {
|
||||
const limits = {
|
||||
...base,
|
||||
extraUsagePercent: 18,
|
||||
extraUsageSpentUsd: 3.1,
|
||||
extraUsageLimitUsd: 17,
|
||||
};
|
||||
const result = renderRateLimits(limits);
|
||||
// Green ANSI code before the extra percentage
|
||||
const extraIndex = result.indexOf('extra:');
|
||||
const afterExtra = result.slice(extraIndex);
|
||||
expect(afterExtra).toContain('\x1b[32m');
|
||||
});
|
||||
it('renders stale marker when stale=true', () => {
|
||||
const limits = {
|
||||
...base,
|
||||
extraUsagePercent: 18,
|
||||
extraUsageSpentUsd: 3.1,
|
||||
extraUsageLimitUsd: 17,
|
||||
};
|
||||
const result = renderRateLimits(limits, true);
|
||||
expect(result).toContain('*');
|
||||
});
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// renderRateLimitsCompact — extra usage
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('renderRateLimitsCompact — extra usage', () => {
|
||||
it('omits extra from compact when absent', () => {
|
||||
const limits = { fiveHourPercent: 10 };
|
||||
const result = renderRateLimitsCompact(limits);
|
||||
// only one percentage in output
|
||||
expect(result).not.toBeNull();
|
||||
expect((result.match(/%/g) ?? []).length).toBe(1);
|
||||
});
|
||||
it('appends extra percentage in compact when present', () => {
|
||||
const limits = {
|
||||
fiveHourPercent: 10,
|
||||
extraUsagePercent: 18,
|
||||
extraUsageLimitUsd: 17,
|
||||
};
|
||||
const result = renderRateLimitsCompact(limits);
|
||||
expect(result).not.toBeNull();
|
||||
// Should contain 18% somewhere
|
||||
expect(result).toContain('18%');
|
||||
});
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// renderRateLimitsWithBar — extra usage
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('renderRateLimitsWithBar — extra usage', () => {
|
||||
it('omits extra bar when absent', () => {
|
||||
const limits = { fiveHourPercent: 10 };
|
||||
const result = renderRateLimitsWithBar(limits);
|
||||
expect(result).not.toContain('extra:');
|
||||
});
|
||||
it('renders extra bar with dollar amounts', () => {
|
||||
const limits = {
|
||||
fiveHourPercent: 10,
|
||||
extraUsagePercent: 18,
|
||||
extraUsageSpentUsd: 3.10,
|
||||
extraUsageLimitUsd: 17.00,
|
||||
};
|
||||
const result = renderRateLimitsWithBar(limits);
|
||||
expect(result).toContain('extra:');
|
||||
expect(result).toContain('18%');
|
||||
expect(result).toContain('$3.10');
|
||||
expect(result).toContain('$17.00');
|
||||
// Bar characters present
|
||||
expect(result).toMatch(/[█░]/);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=extra-usage.test.js.map
|
||||
1
dist/__tests__/hud/extra-usage.test.js.map
generated
vendored
Normal file
1
dist/__tests__/hud/extra-usage.test.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/__tests__/hud/session-state.test.d.ts
generated
vendored
Normal file
2
dist/__tests__/hud/session-state.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=session-state.test.d.ts.map
|
||||
1
dist/__tests__/hud/session-state.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/hud/session-state.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"session-state.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/hud/session-state.test.ts"],"names":[],"mappings":""}
|
||||
59
dist/__tests__/hud/session-state.test.js
generated
vendored
Normal file
59
dist/__tests__/hud/session-state.test.js
generated
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
let rootDir = '';
|
||||
vi.mock('../../lib/worktree-paths.js', () => ({
|
||||
validateWorkingDirectory: (workingDirectory) => workingDirectory ?? rootDir,
|
||||
getOmcRoot: (worktreeRoot) => join(worktreeRoot ?? rootDir, '.omc'),
|
||||
ensureSessionStateDir: (sessionId, worktreeRoot) => {
|
||||
const sessionDir = join(worktreeRoot ?? rootDir, '.omc', 'state', 'sessions', sessionId);
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
return sessionDir;
|
||||
},
|
||||
resolveSessionStatePath: (stateName, sessionId, worktreeRoot) => {
|
||||
const normalizedName = stateName.endsWith('-state') ? stateName : `${stateName}-state`;
|
||||
return join(worktreeRoot ?? rootDir, '.omc', 'state', 'sessions', sessionId, `${normalizedName}.json`);
|
||||
},
|
||||
}));
|
||||
import { readHudState, writeHudState } from '../../hud/state.js';
|
||||
describe('HUD session-scoped state', () => {
|
||||
beforeEach(() => {
|
||||
rootDir = mkdtempSync(join(tmpdir(), 'hud-session-state-'));
|
||||
});
|
||||
afterEach(() => {
|
||||
rmSync(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
it('writes HUD state into the current session directory and clears stale root fallback', () => {
|
||||
const staleRootDir = join(rootDir, '.omc');
|
||||
mkdirSync(staleRootDir, { recursive: true });
|
||||
writeFileSync(join(staleRootDir, 'hud-state.json'), JSON.stringify({ timestamp: '2024-01-01T00:00:00.000Z', sessionId: 'session-123', backgroundTasks: [] }));
|
||||
const result = writeHudState({
|
||||
timestamp: new Date().toISOString(),
|
||||
backgroundTasks: [],
|
||||
}, rootDir, 'session-123');
|
||||
expect(result).toBe(true);
|
||||
const sessionFile = join(rootDir, '.omc', 'state', 'sessions', 'session-123', 'hud-state.json');
|
||||
expect(existsSync(sessionFile)).toBe(true);
|
||||
expect(existsSync(join(rootDir, '.omc', 'hud-state.json'))).toBe(false);
|
||||
const written = JSON.parse(readFileSync(sessionFile, 'utf-8'));
|
||||
expect(written.sessionId).toBe('session-123');
|
||||
});
|
||||
it('reads only the session-scoped HUD state when a sessionId is provided', () => {
|
||||
mkdirSync(join(rootDir, '.omc', 'state'), { recursive: true });
|
||||
writeFileSync(join(rootDir, '.omc', 'state', 'hud-state.json'), JSON.stringify({ timestamp: '2024-01-01T00:00:00.000Z', backgroundTasks: [{ id: 'stale-root' }] }));
|
||||
writeFileSync(join(rootDir, '.omc', 'hud-state.json'), JSON.stringify({ timestamp: '2024-01-01T00:00:00.000Z', backgroundTasks: [{ id: 'legacy-root' }] }));
|
||||
mkdirSync(join(rootDir, '.omc', 'state', 'sessions', 'session-999'), { recursive: true });
|
||||
writeFileSync(join(rootDir, '.omc', 'state', 'sessions', 'session-999', 'hud-state.json'), JSON.stringify({ timestamp: '2024-01-02T00:00:00.000Z', backgroundTasks: [{ id: 'session-state' }], sessionId: 'session-999' }));
|
||||
const sessionState = readHudState(rootDir, 'session-999');
|
||||
expect(sessionState?.backgroundTasks).toEqual([{ id: 'session-state' }]);
|
||||
expect(sessionState?.sessionId).toBe('session-999');
|
||||
});
|
||||
it('does not revive root HUD state when the current session-scoped file is missing', () => {
|
||||
mkdirSync(join(rootDir, '.omc', 'state'), { recursive: true });
|
||||
writeFileSync(join(rootDir, '.omc', 'state', 'hud-state.json'), JSON.stringify({ timestamp: '2024-01-01T00:00:00.000Z', backgroundTasks: [{ id: 'stale-root' }] }));
|
||||
writeFileSync(join(rootDir, '.omc', 'hud-state.json'), JSON.stringify({ timestamp: '2024-01-01T00:00:00.000Z', backgroundTasks: [{ id: 'legacy-root' }] }));
|
||||
expect(readHudState(rootDir, 'session-missing')).toBeNull();
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=session-state.test.js.map
|
||||
1
dist/__tests__/hud/session-state.test.js.map
generated
vendored
Normal file
1
dist/__tests__/hud/session-state.test.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"session-state.test.js","sourceRoot":"","sources":["../../../src/__tests__/hud/session-state.test.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/__tests__/hud/usage-api-lock.test.js
generated
vendored
2
dist/__tests__/hud/usage-api-lock.test.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { EventEmitter } from 'events';
|
||||
const CLAUDE_CONFIG_DIR = '/tmp/test-claude';
|
||||
const CACHE_PATH = `${CLAUDE_CONFIG_DIR}/plugins/oh-my-claudecode/.usage-cache.json`;
|
||||
const CACHE_PATH = `${CLAUDE_CONFIG_DIR}/plugins/oh-my-claudecode/.usage-cache-zai.json`;
|
||||
const LOCK_PATH = `${CACHE_PATH}.lock`;
|
||||
function createFsMock(initialFiles) {
|
||||
const files = new Map(Object.entries(initialFiles));
|
||||
|
||||
2
dist/__tests__/hud/usage-api-lock.test.js.map
generated
vendored
2
dist/__tests__/hud/usage-api-lock.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/__tests__/hud/usage-api-stale.test.js
generated
vendored
2
dist/__tests__/hud/usage-api-stale.test.js
generated
vendored
@@ -8,7 +8,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { EventEmitter } from 'events';
|
||||
const CLAUDE_CONFIG_DIR = '/tmp/test-claude';
|
||||
const CACHE_PATH = `${CLAUDE_CONFIG_DIR}/plugins/oh-my-claudecode/.usage-cache.json`;
|
||||
const CACHE_PATH = `${CLAUDE_CONFIG_DIR}/plugins/oh-my-claudecode/.usage-cache-zai.json`;
|
||||
const CACHE_DIR = `${CLAUDE_CONFIG_DIR}/plugins/oh-my-claudecode`;
|
||||
function createFsMock(initialFiles) {
|
||||
const files = new Map(Object.entries(initialFiles));
|
||||
|
||||
2
dist/__tests__/hud/usage-api-stale.test.js.map
generated
vendored
2
dist/__tests__/hud/usage-api-stale.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/__tests__/hud/usage-api.test.d.ts
generated
vendored
2
dist/__tests__/hud/usage-api.test.d.ts
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Tests for z.ai host validation, response parsing, and getUsage routing.
|
||||
* Tests for z.ai/MiniMax host validation, response parsing, and getUsage routing.
|
||||
*/
|
||||
export {};
|
||||
//# sourceMappingURL=usage-api.test.d.ts.map
|
||||
369
dist/__tests__/hud/usage-api.test.js
generated
vendored
369
dist/__tests__/hud/usage-api.test.js
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Tests for z.ai host validation, response parsing, and getUsage routing.
|
||||
* Tests for z.ai/MiniMax host validation, response parsing, and getUsage routing.
|
||||
*/
|
||||
import { createHash } from 'crypto';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
|
||||
@@ -7,7 +7,7 @@ import * as fs from 'fs';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as os from 'os';
|
||||
import { EventEmitter } from 'events';
|
||||
import { isZaiHost, parseZaiResponse, getUsage } from '../../hud/usage-api.js';
|
||||
import { isZaiHost, parseZaiResponse, isMinimaxHost, parseMinimaxResponse, getUsage } from '../../hud/usage-api.js';
|
||||
// Mock file-lock so withFileLock always executes the callback (tests focus on routing, not locking)
|
||||
vi.mock('../../lib/file-lock.js', () => ({
|
||||
withFileLock: vi.fn((_lockPath, fn) => fn()),
|
||||
@@ -450,9 +450,9 @@ describe('getUsage routing', () => {
|
||||
vi.setSystemTime(new Date('2026-03-07T00:00:00Z'));
|
||||
const mockedExistsSync = vi.mocked(fs.existsSync);
|
||||
const mockedReadFileSync = vi.mocked(fs.readFileSync);
|
||||
mockedExistsSync.mockImplementation((path) => String(path).endsWith('.usage-cache.json'));
|
||||
mockedExistsSync.mockImplementation((path) => String(path).endsWith('.usage-cache-anthropic.json'));
|
||||
mockedReadFileSync.mockImplementation((path) => {
|
||||
if (String(path).endsWith('.usage-cache.json')) {
|
||||
if (String(path).endsWith('.usage-cache-anthropic.json')) {
|
||||
return JSON.stringify({
|
||||
timestamp: Date.now() - 60_000,
|
||||
source: 'anthropic',
|
||||
@@ -486,7 +486,7 @@ describe('getUsage routing', () => {
|
||||
const mockedReadFileSync = vi.mocked(fs.readFileSync);
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
const file = String(path);
|
||||
return file.endsWith('settings.json') || file.endsWith('.usage-cache.json');
|
||||
return file.endsWith('settings.json') || file.endsWith('.usage-cache-anthropic.json');
|
||||
});
|
||||
mockedReadFileSync.mockImplementation((path) => {
|
||||
const file = String(path);
|
||||
@@ -497,7 +497,7 @@ describe('getUsage routing', () => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (file.endsWith('.usage-cache.json')) {
|
||||
if (file.endsWith('.usage-cache-anthropic.json')) {
|
||||
return JSON.stringify({
|
||||
timestamp: Date.now() - 120_000,
|
||||
source: 'anthropic',
|
||||
@@ -579,7 +579,7 @@ describe('getUsage routing', () => {
|
||||
const mockedWriteFileSync = vi.mocked(fs.writeFileSync);
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
const file = String(path);
|
||||
return file.endsWith('settings.json') || file.endsWith('.usage-cache.json');
|
||||
return file.endsWith('settings.json') || file.endsWith('.usage-cache-zai.json');
|
||||
});
|
||||
mockedReadFileSync.mockImplementation((path) => {
|
||||
const file = String(path);
|
||||
@@ -590,7 +590,7 @@ describe('getUsage routing', () => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (file.endsWith('.usage-cache.json')) {
|
||||
if (file.endsWith('.usage-cache-zai.json')) {
|
||||
return JSON.stringify({
|
||||
timestamp: Date.now() - 300_000,
|
||||
rateLimitedUntil: Date.now() - 1,
|
||||
@@ -629,7 +629,7 @@ describe('getUsage routing', () => {
|
||||
const mockedReadFileSync = vi.mocked(fs.readFileSync);
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
const file = String(path);
|
||||
return file.endsWith('settings.json') || file.endsWith('.usage-cache.json');
|
||||
return file.endsWith('settings.json') || file.endsWith('.usage-cache-zai.json');
|
||||
});
|
||||
mockedReadFileSync.mockImplementation((path) => {
|
||||
const file = String(path);
|
||||
@@ -640,7 +640,7 @@ describe('getUsage routing', () => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (file.endsWith('.usage-cache.json')) {
|
||||
if (file.endsWith('.usage-cache-zai.json')) {
|
||||
return JSON.stringify({
|
||||
timestamp: Date.now() - 90_000,
|
||||
source: 'zai',
|
||||
@@ -657,4 +657,353 @@ describe('getUsage routing', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
describe('isMinimaxHost', () => {
|
||||
it('accepts exact minimax.io hostname', () => {
|
||||
expect(isMinimaxHost('https://minimax.io')).toBe(true);
|
||||
expect(isMinimaxHost('https://minimax.io/')).toBe(true);
|
||||
expect(isMinimaxHost('https://minimax.io/v1')).toBe(true);
|
||||
});
|
||||
it('accepts subdomains of minimax.io', () => {
|
||||
expect(isMinimaxHost('https://api.minimax.io')).toBe(true);
|
||||
expect(isMinimaxHost('https://api.minimax.io/anthropic')).toBe(true);
|
||||
expect(isMinimaxHost('https://foo.bar.minimax.io')).toBe(true);
|
||||
});
|
||||
it('accepts minimaxi.com (China endpoint)', () => {
|
||||
expect(isMinimaxHost('https://minimaxi.com')).toBe(true);
|
||||
expect(isMinimaxHost('https://api.minimaxi.com')).toBe(true);
|
||||
expect(isMinimaxHost('https://api.minimaxi.com/anthropic')).toBe(true);
|
||||
});
|
||||
it('accepts minimax.com (China alternative)', () => {
|
||||
expect(isMinimaxHost('https://minimax.com')).toBe(true);
|
||||
expect(isMinimaxHost('https://api.minimax.com')).toBe(true);
|
||||
expect(isMinimaxHost('https://api.minimax.com/anthropic')).toBe(true);
|
||||
});
|
||||
it('rejects hosts that merely contain minimax as substring', () => {
|
||||
expect(isMinimaxHost('https://minimax.io.evil.tld')).toBe(false);
|
||||
expect(isMinimaxHost('https://notminimax.io')).toBe(false);
|
||||
expect(isMinimaxHost('https://minimax.io.example.com')).toBe(false);
|
||||
expect(isMinimaxHost('https://minimaxi.com.evil.tld')).toBe(false);
|
||||
});
|
||||
it('rejects unrelated hosts', () => {
|
||||
expect(isMinimaxHost('https://api.anthropic.com')).toBe(false);
|
||||
expect(isMinimaxHost('https://z.ai')).toBe(false);
|
||||
expect(isMinimaxHost('https://localhost:8080')).toBe(false);
|
||||
});
|
||||
it('rejects invalid URLs gracefully', () => {
|
||||
expect(isMinimaxHost('')).toBe(false);
|
||||
expect(isMinimaxHost('not-a-url')).toBe(false);
|
||||
expect(isMinimaxHost('://missing-protocol')).toBe(false);
|
||||
});
|
||||
it('is case-insensitive', () => {
|
||||
expect(isMinimaxHost('https://MINIMAX.IO/v1')).toBe(true);
|
||||
expect(isMinimaxHost('https://API.MINIMAX.IO')).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('parseMinimaxResponse', () => {
|
||||
it('returns null for empty response', () => {
|
||||
expect(parseMinimaxResponse({})).toBeNull();
|
||||
expect(parseMinimaxResponse({ model_remains: [] })).toBeNull();
|
||||
});
|
||||
it('returns null when base_resp.status_code is non-zero', () => {
|
||||
const response = {
|
||||
model_remains: [
|
||||
{
|
||||
model_name: 'MiniMax-M1',
|
||||
current_interval_total_count: 1500,
|
||||
current_interval_usage_count: 750,
|
||||
start_time: Date.now(),
|
||||
end_time: Date.now() + 3600_000,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 15000,
|
||||
current_weekly_usage_count: 7500,
|
||||
weekly_start_time: Date.now(),
|
||||
weekly_end_time: Date.now() + 86400_000,
|
||||
weekly_remains_time: 86400_000,
|
||||
},
|
||||
],
|
||||
base_resp: { status_code: 1001, status_msg: 'error' },
|
||||
};
|
||||
expect(parseMinimaxResponse(response)).toBeNull();
|
||||
});
|
||||
it('returns null when no MiniMax-M* model exists', () => {
|
||||
const response = {
|
||||
model_remains: [
|
||||
{
|
||||
model_name: 'speech-hd',
|
||||
current_interval_total_count: 100,
|
||||
current_interval_usage_count: 50,
|
||||
start_time: Date.now(),
|
||||
end_time: Date.now() + 3600_000,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 700,
|
||||
current_weekly_usage_count: 350,
|
||||
weekly_start_time: Date.now(),
|
||||
weekly_end_time: Date.now() + 86400_000,
|
||||
weekly_remains_time: 86400_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(parseMinimaxResponse(response)).toBeNull();
|
||||
});
|
||||
it('parses MiniMax-M* model usage as fiveHourPercent and weeklyPercent', () => {
|
||||
const endTime = Date.now() + 3600_000;
|
||||
const weeklyEndTime = Date.now() + 86400_000 * 3;
|
||||
const response = {
|
||||
model_remains: [
|
||||
{
|
||||
model_name: 'MiniMax-M2.7',
|
||||
current_interval_total_count: 1500,
|
||||
current_interval_usage_count: 1416,
|
||||
start_time: Date.now(),
|
||||
end_time: endTime,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 15000,
|
||||
current_weekly_usage_count: 14997,
|
||||
weekly_start_time: Date.now(),
|
||||
weekly_end_time: weeklyEndTime,
|
||||
weekly_remains_time: 86400_000 * 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = parseMinimaxResponse(response);
|
||||
expect(result).not.toBeNull();
|
||||
// 1416/1500 * 100 = 94.4 (clamp does not round; rendering layer rounds)
|
||||
expect(result.fiveHourPercent).toBeCloseTo(94.4, 1);
|
||||
// 14997/15000 * 100 = 99.98
|
||||
expect(result.weeklyPercent).toBeCloseTo(99.98, 1);
|
||||
expect(result.fiveHourResetsAt).toBeInstanceOf(Date);
|
||||
expect(result.fiveHourResetsAt.getTime()).toBe(endTime);
|
||||
expect(result.weeklyResetsAt).toBeInstanceOf(Date);
|
||||
expect(result.weeklyResetsAt.getTime()).toBe(weeklyEndTime);
|
||||
});
|
||||
it('handles division by zero when total_count is 0', () => {
|
||||
const response = {
|
||||
model_remains: [
|
||||
{
|
||||
model_name: 'MiniMax-M1',
|
||||
current_interval_total_count: 0,
|
||||
current_interval_usage_count: 0,
|
||||
start_time: Date.now(),
|
||||
end_time: Date.now() + 3600_000,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 0,
|
||||
current_weekly_usage_count: 0,
|
||||
weekly_start_time: Date.now(),
|
||||
weekly_end_time: Date.now() + 86400_000,
|
||||
weekly_remains_time: 86400_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = parseMinimaxResponse(response);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.fiveHourPercent).toBe(0);
|
||||
expect(result.weeklyPercent).toBe(0);
|
||||
});
|
||||
it('uses first MiniMax-M* model when multiple exist', () => {
|
||||
const response = {
|
||||
model_remains: [
|
||||
{
|
||||
model_name: 'speech-hd',
|
||||
current_interval_total_count: 100,
|
||||
current_interval_usage_count: 100,
|
||||
start_time: Date.now(), end_time: Date.now() + 3600_000,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 700, current_weekly_usage_count: 700,
|
||||
weekly_start_time: Date.now(), weekly_end_time: Date.now() + 86400_000,
|
||||
weekly_remains_time: 86400_000,
|
||||
},
|
||||
{
|
||||
model_name: 'MiniMax-M2.7',
|
||||
current_interval_total_count: 1500,
|
||||
current_interval_usage_count: 750,
|
||||
start_time: Date.now(), end_time: Date.now() + 3600_000,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 15000, current_weekly_usage_count: 7500,
|
||||
weekly_start_time: Date.now(), weekly_end_time: Date.now() + 86400_000,
|
||||
weekly_remains_time: 86400_000,
|
||||
},
|
||||
{
|
||||
model_name: 'MiniMax-M1',
|
||||
current_interval_total_count: 1000,
|
||||
current_interval_usage_count: 200,
|
||||
start_time: Date.now(), end_time: Date.now() + 3600_000,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 10000, current_weekly_usage_count: 2000,
|
||||
weekly_start_time: Date.now(), weekly_end_time: Date.now() + 86400_000,
|
||||
weekly_remains_time: 86400_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = parseMinimaxResponse(response);
|
||||
expect(result).not.toBeNull();
|
||||
// Should use MiniMax-M2.7 (first MiniMax-M* match): 750/1500 = 50%
|
||||
expect(result.fiveHourPercent).toBe(50);
|
||||
expect(result.weeklyPercent).toBe(50);
|
||||
});
|
||||
it('succeeds when base_resp.status_code is 0', () => {
|
||||
const response = {
|
||||
model_remains: [
|
||||
{
|
||||
model_name: 'MiniMax-M1',
|
||||
current_interval_total_count: 100,
|
||||
current_interval_usage_count: 50,
|
||||
start_time: Date.now(), end_time: Date.now() + 3600_000,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 700, current_weekly_usage_count: 350,
|
||||
weekly_start_time: Date.now(), weekly_end_time: Date.now() + 86400_000,
|
||||
weekly_remains_time: 86400_000,
|
||||
},
|
||||
],
|
||||
base_resp: { status_code: 0, status_msg: 'success' },
|
||||
};
|
||||
const result = parseMinimaxResponse(response);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.fiveHourPercent).toBe(50);
|
||||
});
|
||||
});
|
||||
describe('getUsage routing - minimax', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
let httpsModule;
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
||||
vi.mocked(childProcess.execSync).mockImplementation(() => { throw new Error('mock: no keychain'); });
|
||||
vi.mocked(childProcess.execFileSync).mockImplementation(() => { throw new Error('mock: no keychain'); });
|
||||
delete process.env.ANTHROPIC_BASE_URL;
|
||||
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
httpsModule = await import('https');
|
||||
});
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
it('routes to minimax when ANTHROPIC_BASE_URL is minimax host with MINIMAX_API_KEY', async () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic';
|
||||
process.env.MINIMAX_API_KEY = 'test-minimax-key';
|
||||
const result = await getUsage();
|
||||
expect(result.rateLimits).toBeNull();
|
||||
expect(result.error).toBe('network');
|
||||
expect(httpsModule.default.request).toHaveBeenCalledTimes(1);
|
||||
const callArgs = httpsModule.default.request.mock.calls[0][0];
|
||||
expect(callArgs.hostname).toBe('api.minimax.io');
|
||||
expect(callArgs.path).toBe('/v1/api/openplatform/coding_plan/remains');
|
||||
expect(callArgs.headers.Authorization).toBe('Bearer test-minimax-key');
|
||||
});
|
||||
it('falls back to ANTHROPIC_AUTH_TOKEN when MINIMAX_API_KEY is not set', async () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic';
|
||||
process.env.ANTHROPIC_AUTH_TOKEN = 'test-auth-token';
|
||||
const result = await getUsage();
|
||||
expect(result.error).toBe('network');
|
||||
expect(httpsModule.default.request).toHaveBeenCalledTimes(1);
|
||||
const callArgs = httpsModule.default.request.mock.calls[0][0];
|
||||
expect(callArgs.hostname).toBe('api.minimax.io');
|
||||
expect(callArgs.headers.Authorization).toBe('Bearer test-auth-token');
|
||||
});
|
||||
it('returns no_credentials when minimax host detected but no API key', async () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic';
|
||||
// Neither MINIMAX_API_KEY nor ANTHROPIC_AUTH_TOKEN set
|
||||
const result = await getUsage();
|
||||
expect(result.rateLimits).toBeNull();
|
||||
expect(result.error).toBe('no_credentials');
|
||||
expect(httpsModule.default.request).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does NOT route to minimax for look-alike hosts', async () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://minimax.io.evil.tld/v1';
|
||||
process.env.MINIMAX_API_KEY = 'test-key';
|
||||
const result = await getUsage();
|
||||
expect(result.rateLimits).toBeNull();
|
||||
expect(result.error).toBe('no_credentials');
|
||||
expect(httpsModule.default.request).not.toHaveBeenCalled();
|
||||
});
|
||||
it('returns parsed rate limits on successful API response (E2E happy path)', async () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic';
|
||||
process.env.MINIMAX_API_KEY = 'test-key';
|
||||
const endTime = Date.now() + 3600_000;
|
||||
const weeklyEndTime = Date.now() + 86400_000 * 3;
|
||||
httpsModule.default.request.mockImplementationOnce((_options, callback) => {
|
||||
const req = new EventEmitter();
|
||||
req.destroy = vi.fn();
|
||||
req.end = () => {
|
||||
const res = new EventEmitter();
|
||||
res.statusCode = 200;
|
||||
callback(res);
|
||||
res.emit('data', JSON.stringify({
|
||||
model_remains: [
|
||||
{
|
||||
model_name: 'MiniMax-M2.7',
|
||||
current_interval_total_count: 1500,
|
||||
current_interval_usage_count: 750,
|
||||
start_time: Date.now(),
|
||||
end_time: endTime,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 15000,
|
||||
current_weekly_usage_count: 3000,
|
||||
weekly_start_time: Date.now(),
|
||||
weekly_end_time: weeklyEndTime,
|
||||
weekly_remains_time: 86400_000 * 3,
|
||||
},
|
||||
],
|
||||
base_resp: { status_code: 0, status_msg: 'success' },
|
||||
}));
|
||||
res.emit('end');
|
||||
};
|
||||
return req;
|
||||
});
|
||||
const result = await getUsage();
|
||||
expect(result.rateLimits).not.toBeNull();
|
||||
expect(result.rateLimits.fiveHourPercent).toBe(50); // 750/1500
|
||||
expect(result.rateLimits.weeklyPercent).toBe(20); // 3000/15000
|
||||
expect(result.rateLimits.fiveHourResetsAt).toBeInstanceOf(Date);
|
||||
expect(result.rateLimits.fiveHourResetsAt.getTime()).toBe(endTime);
|
||||
expect(result.rateLimits.weeklyResetsAt).toBeInstanceOf(Date);
|
||||
expect(result.rateLimits.weeklyResetsAt.getTime()).toBe(weeklyEndTime);
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
it('prefers MINIMAX_API_KEY over ANTHROPIC_AUTH_TOKEN', async () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic';
|
||||
process.env.MINIMAX_API_KEY = 'preferred-key';
|
||||
process.env.ANTHROPIC_AUTH_TOKEN = 'fallback-key';
|
||||
await getUsage();
|
||||
expect(httpsModule.default.request).toHaveBeenCalledTimes(1);
|
||||
const callArgs = httpsModule.default.request.mock.calls[0][0];
|
||||
expect(callArgs.headers.Authorization).toBe('Bearer preferred-key');
|
||||
});
|
||||
it('writes cache with source minimax', async () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.minimax.io/anthropic';
|
||||
process.env.MINIMAX_API_KEY = 'test-key';
|
||||
httpsModule.default.request.mockImplementationOnce((_options, callback) => {
|
||||
const req = new EventEmitter();
|
||||
req.destroy = vi.fn();
|
||||
req.end = () => {
|
||||
const res = new EventEmitter();
|
||||
res.statusCode = 200;
|
||||
callback(res);
|
||||
res.emit('data', JSON.stringify({
|
||||
model_remains: [
|
||||
{
|
||||
model_name: 'MiniMax-M1',
|
||||
current_interval_total_count: 100,
|
||||
current_interval_usage_count: 50,
|
||||
start_time: Date.now(), end_time: Date.now() + 3600_000,
|
||||
remains_time: 3600_000,
|
||||
current_weekly_total_count: 700, current_weekly_usage_count: 350,
|
||||
weekly_start_time: Date.now(), weekly_end_time: Date.now() + 86400_000,
|
||||
weekly_remains_time: 86400_000,
|
||||
},
|
||||
],
|
||||
base_resp: { status_code: 0, status_msg: 'success' },
|
||||
}));
|
||||
res.emit('end');
|
||||
};
|
||||
return req;
|
||||
});
|
||||
await getUsage();
|
||||
const writeCall = vi.mocked(fs.writeFileSync).mock.calls.find(c => String(c[0]).includes('.usage-cache-minimax.json'));
|
||||
expect(writeCall).toBeTruthy();
|
||||
const written = JSON.parse(String(writeCall[1]));
|
||||
expect(written.source).toBe('minimax');
|
||||
expect(written.data.fiveHourPercent).toBe(50);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=usage-api.test.js.map
|
||||
2
dist/__tests__/hud/usage-api.test.js.map
generated
vendored
2
dist/__tests__/hud/usage-api.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/__tests__/hud/watch-mode-init.test.js
generated
vendored
2
dist/__tests__/hud/watch-mode-init.test.js
generated
vendored
@@ -141,7 +141,7 @@ describe('HUD watch mode initialization', () => {
|
||||
initializeHUDState.mockClear();
|
||||
await hud.main(true, false);
|
||||
// initializeHUDState must receive the resolved cwd from stdin, not undefined/process.cwd()
|
||||
expect(initializeHUDState).toHaveBeenCalledWith('/tmp/worktree');
|
||||
expect(initializeHUDState).toHaveBeenCalledWith('/tmp/worktree', undefined);
|
||||
});
|
||||
it('passes the current session id to OMC state readers', async () => {
|
||||
const hud = await importHudModule();
|
||||
|
||||
2
dist/__tests__/hud/watch-mode-init.test.js.map
generated
vendored
2
dist/__tests__/hud/watch-mode-init.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/__tests__/installer-plugin-agents.test.js
generated
vendored
2
dist/__tests__/installer-plugin-agents.test.js
generated
vendored
@@ -104,6 +104,8 @@ describe('installer legacy agent sync gating (issue #1502)', () => {
|
||||
expect(result.installedAgents.length).toBeGreaterThan(0);
|
||||
expect(existsSync(join(claudeConfigDir, 'agents'))).toBe(true);
|
||||
expect(readdirSync(join(claudeConfigDir, 'agents')).some(file => file.endsWith('.md'))).toBe(true);
|
||||
expect(existsSync(join(claudeConfigDir, 'hooks', 'lib', 'stdin.mjs'))).toBe(true);
|
||||
expect(existsSync(join(claudeConfigDir, 'hooks', 'lib', 'atomic-write.mjs'))).toBe(true);
|
||||
expect(installer.hasPluginProvidedAgentFiles()).toBe(false);
|
||||
expect(installer.isInstalled()).toBe(true);
|
||||
});
|
||||
|
||||
2
dist/__tests__/installer-plugin-agents.test.js.map
generated
vendored
2
dist/__tests__/installer-plugin-agents.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
45
dist/__tests__/keyword-detector-script.test.js
generated
vendored
45
dist/__tests__/keyword-detector-script.test.js
generated
vendored
@@ -88,5 +88,50 @@ OMC Ultrawork = "특수부대 작전 반"
|
||||
expect(context).not.toContain('[MAGIC KEYWORD: ULTRAWORK]');
|
||||
expect(context).toBe('');
|
||||
});
|
||||
it('does not activate ultrawork for single-mode explanatory definitions followed by a budget question', () => {
|
||||
const output = runKeywordDetector('OMC Ultrawork = "special ops". how much would it cost?');
|
||||
const context = output.hookSpecificOutput?.additionalContext ?? '';
|
||||
expect(output.continue).toBe(true);
|
||||
expect(context).not.toContain('[MAGIC KEYWORD: ULTRAWORK]');
|
||||
expect(context).toBe('');
|
||||
});
|
||||
// Regression: issue #2541 — review-seed echo must not trip code-review / security-review alerts
|
||||
it('does not activate code-review when prompt is echoed review-instruction text with approve/request-changes/merge-ready', () => {
|
||||
const prompt = [
|
||||
'You are performing a code review of PR #2541.',
|
||||
'Reply with exactly one verdict:',
|
||||
'- approve',
|
||||
'- request-changes',
|
||||
'- merge-ready',
|
||||
].join('\n');
|
||||
const output = runKeywordDetector(prompt);
|
||||
const context = output.hookSpecificOutput?.additionalContext ?? '';
|
||||
expect(output.continue).toBe(true);
|
||||
expect(context).not.toContain('[MAGIC KEYWORD: CODE-REVIEW]');
|
||||
expect(context).not.toContain('<code-review-mode>');
|
||||
expect(context).toBe('');
|
||||
});
|
||||
it('does not activate security-review when prompt is echoed review-instruction text with approve/request-changes/blocked', () => {
|
||||
const prompt = [
|
||||
'You are performing a security review.',
|
||||
'Choose one verdict:',
|
||||
'- approve',
|
||||
'- request-changes',
|
||||
'- blocked',
|
||||
].join('\n');
|
||||
const output = runKeywordDetector(prompt);
|
||||
const context = output.hookSpecificOutput?.additionalContext ?? '';
|
||||
expect(output.continue).toBe(true);
|
||||
expect(context).not.toContain('[MAGIC KEYWORD: SECURITY-REVIEW]');
|
||||
expect(context).not.toContain('<security-review-mode>');
|
||||
expect(context).toBe('');
|
||||
});
|
||||
it('still activates code-review for a genuine user request (positive control)', () => {
|
||||
const output = runKeywordDetector('code review this diff');
|
||||
const context = output.hookSpecificOutput?.additionalContext ?? '';
|
||||
expect(output.continue).toBe(true);
|
||||
expect(context).toContain('<code-review-mode>');
|
||||
expect(context).not.toContain('[MAGIC KEYWORD: CODE-REVIEW]');
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=keyword-detector-script.test.js.map
|
||||
2
dist/__tests__/keyword-detector-script.test.js.map
generated
vendored
2
dist/__tests__/keyword-detector-script.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
10
dist/__tests__/lsp-servers.test.js
generated
vendored
10
dist/__tests__/lsp-servers.test.js
generated
vendored
@@ -32,7 +32,7 @@ describe('LSP Server Configurations', () => {
|
||||
describe('getServerForFile', () => {
|
||||
const cases = [
|
||||
['app.ts', 'TypeScript Language Server'],
|
||||
['app.py', 'Python Language Server (pylsp)'],
|
||||
['app.py', 'Python Language Server (ty)'],
|
||||
['main.rs', 'Rust Analyzer'],
|
||||
['main.go', 'gopls'],
|
||||
['main.c', 'clangd'],
|
||||
@@ -74,7 +74,7 @@ describe('getServerForLanguage', () => {
|
||||
const cases = [
|
||||
['typescript', 'TypeScript Language Server'],
|
||||
['javascript', 'TypeScript Language Server'],
|
||||
['python', 'Python Language Server (pylsp)'],
|
||||
['python', 'Python Language Server (ty)'],
|
||||
['rust', 'Rust Analyzer'],
|
||||
['go', 'gopls'],
|
||||
['golang', 'gopls'],
|
||||
@@ -130,4 +130,10 @@ describe('OmniSharp command casing', () => {
|
||||
expect(LSP_SERVERS.csharp.command).toBe('omnisharp');
|
||||
});
|
||||
});
|
||||
describe('Python server selection', () => {
|
||||
it('should invoke ty via its LSP subcommand', () => {
|
||||
expect(LSP_SERVERS.python.command).toBe('ty');
|
||||
expect(LSP_SERVERS.python.args).toEqual(['server']);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=lsp-servers.test.js.map
|
||||
2
dist/__tests__/lsp-servers.test.js.map
generated
vendored
2
dist/__tests__/lsp-servers.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/__tests__/permission-handler-runtime-entrypoint.test.d.ts
generated
vendored
Normal file
2
dist/__tests__/permission-handler-runtime-entrypoint.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=permission-handler-runtime-entrypoint.test.d.ts.map
|
||||
1
dist/__tests__/permission-handler-runtime-entrypoint.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/permission-handler-runtime-entrypoint.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"permission-handler-runtime-entrypoint.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/permission-handler-runtime-entrypoint.test.ts"],"names":[],"mappings":""}
|
||||
2
dist/__tests__/post-tool-use-failure.test.d.ts
generated
vendored
Normal file
2
dist/__tests__/post-tool-use-failure.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=post-tool-use-failure.test.d.ts.map
|
||||
1
dist/__tests__/post-tool-use-failure.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/post-tool-use-failure.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"post-tool-use-failure.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/post-tool-use-failure.test.ts"],"names":[],"mappings":""}
|
||||
67
dist/__tests__/post-tool-use-failure.test.js
generated
vendored
Normal file
67
dist/__tests__/post-tool-use-failure.test.js
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
const NODE = process.execPath;
|
||||
const REPO_ROOT = resolve(join(__dirname, '..', '..'));
|
||||
const SCRIPT_PATH = join(REPO_ROOT, 'scripts', 'post-tool-use-failure.mjs');
|
||||
const TEST_TMP_ROOT = join(REPO_ROOT, '.tmp-post-tool-use-failure-tests');
|
||||
function runHook(input) {
|
||||
const raw = execFileSync(NODE, [SCRIPT_PATH], {
|
||||
input: JSON.stringify(input),
|
||||
encoding: 'utf-8',
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_PLUGIN_ROOT: REPO_ROOT,
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
timeout: 15000,
|
||||
}).trim();
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
describe('post-tool-use-failure.mjs', () => {
|
||||
const tempDirs = [];
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
rmSync(tempDirs.pop(), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
function makeRepoLocalTempDir() {
|
||||
mkdirSync(TEST_TMP_ROOT, { recursive: true });
|
||||
const cwd = mkdtempSync(join(TEST_TMP_ROOT, 'case-'));
|
||||
tempDirs.push(cwd);
|
||||
return cwd;
|
||||
}
|
||||
it('suppresses optional omx startup read method-not-found noise', () => {
|
||||
const cwd = makeRepoLocalTempDir();
|
||||
const errorPath = join(cwd, '.omc', 'state', 'last-tool-error.json');
|
||||
const result = runHook({
|
||||
tool_name: 'mcp__omx_state__state_read',
|
||||
tool_input: { mode: 'deep-interview' },
|
||||
error: 'Method not found',
|
||||
cwd,
|
||||
});
|
||||
expect(result).toEqual({ continue: true, suppressOutput: true });
|
||||
expect(existsSync(errorPath)).toBe(false);
|
||||
});
|
||||
it('preserves real failures for the same optional startup reads', () => {
|
||||
const cwd = makeRepoLocalTempDir();
|
||||
const errorPath = join(cwd, '.omc', 'state', 'last-tool-error.json');
|
||||
const result = runHook({
|
||||
tool_name: 'mcp__omx_state__state_read',
|
||||
tool_input: { mode: 'deep-interview' },
|
||||
error: 'Connection refused',
|
||||
cwd,
|
||||
});
|
||||
expect(result.continue).toBe(true);
|
||||
expect(result.suppressOutput).not.toBe(true);
|
||||
expect(result.hookSpecificOutput?.hookEventName).toBe('PostToolUseFailure');
|
||||
expect(result.hookSpecificOutput?.additionalContext).toContain('Tool "mcp__omx_state__state_read" failed.');
|
||||
expect(existsSync(errorPath)).toBe(true);
|
||||
const errorState = JSON.parse(readFileSync(errorPath, 'utf-8'));
|
||||
expect(errorState.tool_name).toBe('mcp__omx_state__state_read');
|
||||
expect(errorState.error).toBe('Connection refused');
|
||||
expect(errorState.retry_count).toBe(1);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=post-tool-use-failure.test.js.map
|
||||
1
dist/__tests__/post-tool-use-failure.test.js.map
generated
vendored
Normal file
1
dist/__tests__/post-tool-use-failure.test.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"post-tool-use-failure.test.js","sourceRoot":"","sources":["../../src/__tests__/post-tool-use-failure.test.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/__tests__/project-session-manager-review-worktree.test.d.ts
generated
vendored
Normal file
2
dist/__tests__/project-session-manager-review-worktree.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=project-session-manager-review-worktree.test.d.ts.map
|
||||
1
dist/__tests__/project-session-manager-review-worktree.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/project-session-manager-review-worktree.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"project-session-manager-review-worktree.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/project-session-manager-review-worktree.test.ts"],"names":[],"mappings":""}
|
||||
63
dist/__tests__/project-session-manager-review-worktree.test.js
generated
vendored
Normal file
63
dist/__tests__/project-session-manager-review-worktree.test.js
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join, resolve } from 'path';
|
||||
const WORKTREE_LIB_PATH = join(process.cwd(), 'skills', 'project-session-manager', 'lib', 'worktree.sh');
|
||||
const WORKTREE_LIB = readFileSync(WORKTREE_LIB_PATH, 'utf-8');
|
||||
const PR_REVIEW_TEMPLATE = readFileSync(join(process.cwd(), 'skills', 'project-session-manager', 'templates', 'pr-review.md'), 'utf-8');
|
||||
describe('project-session-manager review worktree hardening', () => {
|
||||
const tempDirs = [];
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
rmSync(tempDirs.pop(), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it('bootstraps review worktrees with best-effort dependency reuse when package.json matches', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'omc-psm-review-'));
|
||||
tempDirs.push(root);
|
||||
const repo = join(root, 'repo');
|
||||
const worktree = join(root, 'worktree');
|
||||
mkdirSync(join(repo, 'node_modules', 'vitest'), { recursive: true });
|
||||
mkdirSync(worktree, { recursive: true });
|
||||
writeFileSync(join(repo, 'package.json'), '{"name":"demo","version":"1.0.0"}\n');
|
||||
writeFileSync(join(worktree, 'package.json'), '{"name":"demo","version":"1.0.0"}\n');
|
||||
execFileSync('bash', ['-lc', 'source "$SCRIPT_PATH"; psm_bootstrap_review_dependencies "$REPO_DIR" "$WORKTREE_DIR"'], {
|
||||
env: {
|
||||
...process.env,
|
||||
SCRIPT_PATH: WORKTREE_LIB_PATH,
|
||||
REPO_DIR: repo,
|
||||
WORKTREE_DIR: worktree,
|
||||
},
|
||||
});
|
||||
const linkedNodeModules = join(worktree, 'node_modules');
|
||||
expect(lstatSync(linkedNodeModules).isSymbolicLink()).toBe(true);
|
||||
expect(resolve(worktree, readlinkSync(linkedNodeModules))).toBe(join(repo, 'node_modules'));
|
||||
expect(WORKTREE_LIB).toContain('psm_bootstrap_review_dependencies "$local_repo" "$worktree_path"');
|
||||
});
|
||||
it('skips dependency reuse when package.json differs', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'omc-psm-review-mismatch-'));
|
||||
tempDirs.push(root);
|
||||
const repo = join(root, 'repo');
|
||||
const worktree = join(root, 'worktree');
|
||||
mkdirSync(join(repo, 'node_modules', 'vitest'), { recursive: true });
|
||||
mkdirSync(worktree, { recursive: true });
|
||||
writeFileSync(join(repo, 'package.json'), '{"name":"demo","version":"1.0.0"}\n');
|
||||
writeFileSync(join(worktree, 'package.json'), '{"name":"demo","version":"2.0.0"}\n');
|
||||
execFileSync('bash', ['-lc', 'source "$SCRIPT_PATH"; psm_bootstrap_review_dependencies "$REPO_DIR" "$WORKTREE_DIR"'], {
|
||||
env: {
|
||||
...process.env,
|
||||
SCRIPT_PATH: WORKTREE_LIB_PATH,
|
||||
REPO_DIR: repo,
|
||||
WORKTREE_DIR: worktree,
|
||||
},
|
||||
});
|
||||
expect(() => lstatSync(join(worktree, 'node_modules'))).toThrow();
|
||||
});
|
||||
it('guides PR review flows toward focused verification before full-suite fallback', () => {
|
||||
expect(PR_REVIEW_TEMPLATE).toContain('npm run test:run -- <changed-test-paths>');
|
||||
expect(PR_REVIEW_TEMPLATE).toContain('preferred focused verification');
|
||||
expect(PR_REVIEW_TEMPLATE).toContain('symlinked node_modules from the source repo');
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=project-session-manager-review-worktree.test.js.map
|
||||
1
dist/__tests__/project-session-manager-review-worktree.test.js.map
generated
vendored
Normal file
1
dist/__tests__/project-session-manager-review-worktree.test.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"project-session-manager-review-worktree.test.js","sourceRoot":"","sources":["../../src/__tests__/project-session-manager-review-worktree.test.ts"],"names":[],"mappings":"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"}
|
||||
10
dist/__tests__/psm-launch-trust.test.d.ts
generated
vendored
Normal file
10
dist/__tests__/psm-launch-trust.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Regression tests for issue #2508:
|
||||
* PSM tmux sessions stalling on approval/confirm prompts.
|
||||
*
|
||||
* These are contract tests: they read the shell script source and assert that
|
||||
* the fix is in place. A reversion to bare `claude` (no trust flag) or removal
|
||||
* of the initial-context handling will immediately break these tests.
|
||||
*/
|
||||
export {};
|
||||
//# sourceMappingURL=psm-launch-trust.test.d.ts.map
|
||||
1
dist/__tests__/psm-launch-trust.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/psm-launch-trust.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"psm-launch-trust.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/psm-launch-trust.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
||||
66
dist/__tests__/psm-launch-trust.test.js
generated
vendored
Normal file
66
dist/__tests__/psm-launch-trust.test.js
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Regression tests for issue #2508:
|
||||
* PSM tmux sessions stalling on approval/confirm prompts.
|
||||
*
|
||||
* These are contract tests: they read the shell script source and assert that
|
||||
* the fix is in place. A reversion to bare `claude` (no trust flag) or removal
|
||||
* of the initial-context handling will immediately break these tests.
|
||||
*/
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
const PSM_ROOT = join(__dirname, '../../skills/project-session-manager');
|
||||
const TMUX_SH = join(PSM_ROOT, 'lib/tmux.sh');
|
||||
const PSM_SH = join(PSM_ROOT, 'psm.sh');
|
||||
function readScript(path) {
|
||||
return readFileSync(path, 'utf-8');
|
||||
}
|
||||
describe('PSM launch trust fix (issue #2508)', () => {
|
||||
describe('tmux.sh psm_launch_claude', () => {
|
||||
let source;
|
||||
beforeAll(() => { source = readScript(TMUX_SH); });
|
||||
it('passes --dangerously-skip-permissions to claude', () => {
|
||||
expect(source).toContain('claude --dangerously-skip-permissions');
|
||||
});
|
||||
it('does NOT launch bare claude without the trust flag', () => {
|
||||
const bareClaudePattern = /send-keys[^\n]*"\s*claude\s*"\s*Enter/;
|
||||
expect(source).not.toMatch(bareClaudePattern);
|
||||
});
|
||||
it('accepts an initial_context parameter', () => {
|
||||
expect(source).toContain('local initial_context=');
|
||||
});
|
||||
it('preserves context-file injection for relative task files', () => {
|
||||
expect(source).toContain("tmux display-message -p -t \"$session_name\" '#{pane_current_path}'");
|
||||
expect(source).toContain('-f "$session_path/$initial_context"');
|
||||
expect(source).toContain('psm_inject_prompt "$session_name" "$initial_context"');
|
||||
});
|
||||
it('delivers literal prompts via tmux send-keys in a background subshell', () => {
|
||||
expect(source).toContain('send-keys -t "$session_name" -l -- "$initial_context"');
|
||||
expect(source).toMatch(/\(\s*[\s\S]*?sleep[\s\S]*?send-keys[\s\S]*?\)\s*&/m);
|
||||
});
|
||||
it('documents PSM_CLAUDE_STARTUP_DELAY env var for tuning', () => {
|
||||
expect(source).toContain('PSM_CLAUDE_STARTUP_DELAY');
|
||||
});
|
||||
});
|
||||
describe('psm.sh command functions — task context delivery', () => {
|
||||
let source;
|
||||
beforeAll(() => { source = readScript(PSM_SH); });
|
||||
it('cmd_review passes the rendered review context file to psm_launch_claude', () => {
|
||||
expect(source).toContain('local context_rel=".psm/review.md"');
|
||||
expect(source).toContain('psm_launch_claude "$session_name" "$context_rel"');
|
||||
});
|
||||
it('cmd_fix passes the rendered issue context file to psm_launch_claude', () => {
|
||||
expect(source).toContain('local fix_context_rel=".psm/fix.md"');
|
||||
expect(source).toContain('psm_launch_claude "$session_name" "$fix_context_rel"');
|
||||
});
|
||||
it('cmd_feature still passes a literal feature prompt to psm_launch_claude', () => {
|
||||
expect(source).toMatch(/feature_prompt=.*feature_name/s);
|
||||
expect(source).toContain('psm_launch_claude "$session_name" "$feature_prompt"');
|
||||
});
|
||||
it('no command calls psm_launch_claude with only a session name (no task context)', () => {
|
||||
const bareCall = /^\s*psm_launch_claude\s+"\$session_name"\s*$/m;
|
||||
expect(source).not.toMatch(bareCall);
|
||||
});
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=psm-launch-trust.test.js.map
|
||||
1
dist/__tests__/psm-launch-trust.test.js.map
generated
vendored
Normal file
1
dist/__tests__/psm-launch-trust.test.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"psm-launch-trust.test.js","sourceRoot":"","sources":["../../src/__tests__/psm-launch-trust.test.ts"],"names":[],"mappings":"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"}
|
||||
129
dist/__tests__/purge-stale-cache.test.js
generated
vendored
129
dist/__tests__/purge-stale-cache.test.js
generated
vendored
@@ -10,18 +10,20 @@ vi.mock('fs', async () => {
|
||||
statSync: vi.fn(),
|
||||
rmSync: vi.fn(),
|
||||
unlinkSync: vi.fn(),
|
||||
symlinkSync: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('../utils/config-dir.js', () => ({
|
||||
getClaudeConfigDir: vi.fn(() => '/mock/.claude'),
|
||||
}));
|
||||
import { existsSync, readFileSync, readdirSync, statSync, rmSync } from 'fs';
|
||||
import { existsSync, readFileSync, readdirSync, statSync, rmSync, symlinkSync } from 'fs';
|
||||
import { purgeStalePluginCacheVersions } from '../utils/paths.js';
|
||||
const mockedExistsSync = vi.mocked(existsSync);
|
||||
const mockedReadFileSync = vi.mocked(readFileSync);
|
||||
const mockedReaddirSync = vi.mocked(readdirSync);
|
||||
const mockedStatSync = vi.mocked(statSync);
|
||||
const mockedRmSync = vi.mocked(rmSync);
|
||||
const mockedSymlinkSync = vi.mocked(symlinkSync);
|
||||
function dirent(name) {
|
||||
return { name, isDirectory: () => true };
|
||||
}
|
||||
@@ -83,9 +85,14 @@ describe('purgeStalePluginCacheVersions', () => {
|
||||
return [];
|
||||
});
|
||||
const result = purgeStalePluginCacheVersions();
|
||||
expect(result.removed).toBe(1);
|
||||
expect(result.removedPaths).toEqual([staleVersion]);
|
||||
// Stale version shares a namespace with the active version, so it is
|
||||
// symlinked rather than deleted (fix for #2543).
|
||||
expect(result.symlinked).toBe(1);
|
||||
expect(result.removed).toBe(0);
|
||||
expect(result.symlinkPaths).toEqual([staleVersion]);
|
||||
// safeRmSync still removes the real dir before creating the symlink
|
||||
expect(mockedRmSync).toHaveBeenCalledWith(staleVersion, { recursive: true, force: true });
|
||||
expect(mockedSymlinkSync).toHaveBeenCalledWith(activeVersion, staleVersion, 'dir');
|
||||
// Active version should NOT be removed
|
||||
expect(mockedRmSync).not.toHaveBeenCalledWith(activeVersion, expect.anything());
|
||||
});
|
||||
@@ -127,9 +134,11 @@ describe('purgeStalePluginCacheVersions', () => {
|
||||
return [];
|
||||
});
|
||||
const result = purgeStalePluginCacheVersions();
|
||||
expect(result.removed).toBe(2);
|
||||
expect(result.removedPaths).toContain(stale1);
|
||||
expect(result.removedPaths).toContain(stale2);
|
||||
// Both stale hookify versions share a namespace with active1 → symlinked.
|
||||
expect(result.symlinked).toBe(2);
|
||||
expect(result.removed).toBe(0);
|
||||
expect(result.symlinkPaths).toContain(stale1);
|
||||
expect(result.symlinkPaths).toContain(stale2);
|
||||
});
|
||||
it('does nothing when all cache versions are active', () => {
|
||||
const cacheDir = '/mock/.claude/plugins/cache';
|
||||
@@ -279,5 +288,113 @@ describe('purgeStalePluginCacheVersions', () => {
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0]).toContain('unexpected top-level structure');
|
||||
});
|
||||
// --- #2543 regression: symlink-instead-of-delete ---
|
||||
it('replaces stale version dir with symlink to active version in same namespace', () => {
|
||||
// Scenario: CLAUDE_PLUGIN_ROOT=4.14.4 in a running session; 4.14.5 installed;
|
||||
// purge runs after grace period. 4.14.4 must become a symlink, not disappear.
|
||||
const cacheDir = '/mock/.claude/plugins/cache';
|
||||
const activeVersion = join(cacheDir, 'omc/oh-my-claudecode/4.14.5');
|
||||
const staleVersion = join(cacheDir, 'omc/oh-my-claudecode/4.14.4');
|
||||
mockedExistsSync.mockImplementation((p) => {
|
||||
const ps = String(p);
|
||||
if (ps.includes('installed_plugins.json'))
|
||||
return true;
|
||||
if (ps === cacheDir)
|
||||
return true;
|
||||
if (ps === staleVersion || ps === activeVersion)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
mockedReadFileSync.mockReturnValue(JSON.stringify({
|
||||
version: 2,
|
||||
plugins: {
|
||||
'oh-my-claudecode@omc': [{ installPath: activeVersion, version: '4.14.5' }],
|
||||
},
|
||||
}));
|
||||
mockedReaddirSync.mockImplementation((p, _opts) => {
|
||||
const ps = String(p);
|
||||
if (ps === cacheDir)
|
||||
return [dirent('omc')];
|
||||
if (ps.endsWith('omc'))
|
||||
return [dirent('oh-my-claudecode')];
|
||||
if (ps.endsWith('oh-my-claudecode'))
|
||||
return [dirent('4.14.4'), dirent('4.14.5')];
|
||||
return [];
|
||||
});
|
||||
const result = purgeStalePluginCacheVersions();
|
||||
expect(result.symlinked).toBe(1);
|
||||
expect(result.removed).toBe(0);
|
||||
expect(result.symlinkPaths).toEqual([staleVersion]);
|
||||
// Real dir removed first, then symlink created
|
||||
expect(mockedRmSync).toHaveBeenCalledWith(staleVersion, { recursive: true, force: true });
|
||||
expect(mockedSymlinkSync).toHaveBeenCalledWith(activeVersion, staleVersion, 'dir');
|
||||
// Active version untouched
|
||||
expect(mockedRmSync).not.toHaveBeenCalledWith(activeVersion, expect.anything());
|
||||
expect(mockedSymlinkSync).not.toHaveBeenCalledWith(expect.anything(), activeVersion, expect.anything());
|
||||
});
|
||||
it('deletes stale version dir when no active version exists in namespace', () => {
|
||||
// When the active installPath is outside the plugin namespace there is no
|
||||
// live version to redirect to, so deletion (original behaviour) applies.
|
||||
const cacheDir = '/mock/.claude/plugins/cache';
|
||||
const staleVersion = join(cacheDir, 'omc/plugin/1.0.0');
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedReadFileSync.mockReturnValue(JSON.stringify({
|
||||
version: 2,
|
||||
plugins: {
|
||||
// installPath is outside the omc/plugin namespace
|
||||
'plugin@other': [{ installPath: '/completely/different/path/2.0.0' }],
|
||||
},
|
||||
}));
|
||||
mockedReaddirSync.mockImplementation((p, _opts) => {
|
||||
const ps = String(p);
|
||||
if (ps === cacheDir)
|
||||
return [dirent('omc')];
|
||||
if (ps.endsWith('omc'))
|
||||
return [dirent('plugin')];
|
||||
if (ps.endsWith('plugin'))
|
||||
return [dirent('1.0.0')];
|
||||
return [];
|
||||
});
|
||||
const result = purgeStalePluginCacheVersions();
|
||||
expect(result.removed).toBe(1);
|
||||
expect(result.symlinked).toBe(0);
|
||||
expect(result.removedPaths).toEqual([staleVersion]);
|
||||
expect(mockedRmSync).toHaveBeenCalledWith(staleVersion, { recursive: true, force: true });
|
||||
expect(mockedSymlinkSync).not.toHaveBeenCalled();
|
||||
});
|
||||
it('skips version directory entries where isDirectory() returns false (existing symlinks)', () => {
|
||||
// readdirSync with withFileTypes returns isDirectory()=false for symlinks on
|
||||
// Linux/macOS. The purge loop must leave these alone.
|
||||
const cacheDir = '/mock/.claude/plugins/cache';
|
||||
const activeVersion = join(cacheDir, 'omc/oh-my-claudecode/4.14.5');
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedReadFileSync.mockReturnValue(JSON.stringify({
|
||||
version: 2,
|
||||
plugins: {
|
||||
'oh-my-claudecode@omc': [{ installPath: activeVersion }],
|
||||
},
|
||||
}));
|
||||
mockedReaddirSync.mockImplementation((p, _opts) => {
|
||||
const ps = String(p);
|
||||
if (ps === cacheDir)
|
||||
return [dirent('omc')];
|
||||
if (ps.endsWith('omc'))
|
||||
return [dirent('oh-my-claudecode')];
|
||||
if (ps.endsWith('oh-my-claudecode')) {
|
||||
// 4.14.4 is a symlink (isDirectory returns false), 4.14.5 is a real dir
|
||||
return [
|
||||
{ name: '4.14.4', isDirectory: () => false },
|
||||
dirent('4.14.5'),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const result = purgeStalePluginCacheVersions();
|
||||
// The symlink entry must not be touched
|
||||
expect(result.removed).toBe(0);
|
||||
expect(result.symlinked).toBe(0);
|
||||
expect(mockedRmSync).not.toHaveBeenCalled();
|
||||
expect(mockedSymlinkSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=purge-stale-cache.test.js.map
|
||||
2
dist/__tests__/purge-stale-cache.test.js.map
generated
vendored
2
dist/__tests__/purge-stale-cache.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
75
dist/__tests__/ralph-prd-mandatory.test.js
generated
vendored
75
dist/__tests__/ralph-prd-mandatory.test.js
generated
vendored
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { existsSync, mkdirSync, rmSync } from 'fs';
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { detectNoPrdFlag, stripNoPrdFlag, detectCriticModeFlag, stripCriticModeFlag, createRalphLoopHook, readRalphState, findPrdPath, initPrd, readPrd, writePrd, } from '../hooks/ralph/index.js';
|
||||
@@ -18,31 +18,17 @@ describe('Ralph PRD-Mandatory', () => {
|
||||
}
|
||||
});
|
||||
// ==========================================================================
|
||||
// Flag Detection & Stripping
|
||||
// Prompt Flag Sanitization
|
||||
// ==========================================================================
|
||||
describe('detectNoPrdFlag', () => {
|
||||
it('should detect --no-prd in prompt', () => {
|
||||
it('still detects legacy --no-prd syntax for sanitization', () => {
|
||||
expect(detectNoPrdFlag('ralph --no-prd fix this')).toBe(true);
|
||||
});
|
||||
it('should detect --no-prd at start of prompt', () => {
|
||||
expect(detectNoPrdFlag('--no-prd fix this bug')).toBe(true);
|
||||
expect(detectNoPrdFlag('fix this bug --NO-PRD')).toBe(true);
|
||||
});
|
||||
it('should detect --no-prd at end of prompt', () => {
|
||||
expect(detectNoPrdFlag('fix this bug --no-prd')).toBe(true);
|
||||
});
|
||||
it('should detect --NO-PRD (case insensitive)', () => {
|
||||
expect(detectNoPrdFlag('ralph --NO-PRD fix this')).toBe(true);
|
||||
});
|
||||
it('should detect --No-Prd (mixed case)', () => {
|
||||
expect(detectNoPrdFlag('ralph --No-Prd fix this')).toBe(true);
|
||||
});
|
||||
it('should return false when flag is absent', () => {
|
||||
it('returns false when the legacy flag is absent', () => {
|
||||
expect(detectNoPrdFlag('ralph fix this bug')).toBe(false);
|
||||
});
|
||||
it('should return false for empty string', () => {
|
||||
expect(detectNoPrdFlag('')).toBe(false);
|
||||
});
|
||||
it('should return false for --prd (without no)', () => {
|
||||
expect(detectNoPrdFlag('ralph --prd build a todo app')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -157,6 +143,7 @@ describe('Ralph PRD-Mandatory', () => {
|
||||
acceptanceCriteria: ['It works'],
|
||||
priority: 1,
|
||||
passes: false,
|
||||
architectVerified: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -182,6 +169,7 @@ describe('Ralph PRD-Mandatory', () => {
|
||||
acceptanceCriteria: [],
|
||||
priority: 1,
|
||||
passes: true,
|
||||
architectVerified: true,
|
||||
},
|
||||
{
|
||||
id: 'US-002',
|
||||
@@ -190,6 +178,7 @@ describe('Ralph PRD-Mandatory', () => {
|
||||
acceptanceCriteria: [],
|
||||
priority: 2,
|
||||
passes: false,
|
||||
architectVerified: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -199,12 +188,32 @@ describe('Ralph PRD-Mandatory', () => {
|
||||
const state = readRalphState(testDir);
|
||||
expect(state.current_story_id).toBe('US-002');
|
||||
});
|
||||
it('should NOT enable prd_mode when no prd.json exists', () => {
|
||||
it('should create and enable prd_mode when no prd.json exists', () => {
|
||||
const hook = createRalphLoopHook(testDir);
|
||||
hook.startLoop(undefined, 'test prompt');
|
||||
const state = readRalphState(testDir);
|
||||
expect(state).not.toBeNull();
|
||||
expect(state.prd_mode).toBeUndefined();
|
||||
expect(state.prd_mode).toBe(true);
|
||||
expect(findPrdPath(testDir)).not.toBeNull();
|
||||
});
|
||||
it('should ignore legacy --no-prd and still require PRD startup', () => {
|
||||
const hook = createRalphLoopHook(testDir);
|
||||
const started = hook.startLoop(undefined, 'test prompt --no-prd');
|
||||
expect(started).toBe(true);
|
||||
const state = readRalphState(testDir);
|
||||
expect(state).not.toBeNull();
|
||||
expect(state.prd_mode).toBe(true);
|
||||
expect(state.prompt).toBe('test prompt');
|
||||
expect(findPrdPath(testDir)).not.toBeNull();
|
||||
});
|
||||
it('should refuse to start when an existing prd.json is invalid', () => {
|
||||
const invalidPrdPath = join(testDir, 'prd.json');
|
||||
mkdirSync(join(testDir, '.git'), { recursive: true });
|
||||
writeFileSync(invalidPrdPath, '{ invalid json');
|
||||
const hook = createRalphLoopHook(testDir);
|
||||
const started = hook.startLoop(undefined, 'test prompt');
|
||||
expect(started).toBe(false);
|
||||
expect(readRalphState(testDir)).toBeNull();
|
||||
});
|
||||
});
|
||||
// ==========================================================================
|
||||
@@ -268,23 +277,41 @@ describe('Ralph PRD-Mandatory', () => {
|
||||
const prompt = getArchitectVerificationPrompt({
|
||||
...baseVerificationState,
|
||||
critic_mode: 'critic',
|
||||
request_id: 'req-critic',
|
||||
});
|
||||
expect(prompt).toContain('[CRITIC VERIFICATION REQUIRED');
|
||||
expect(prompt).toContain('Task(subagent_type="critic"');
|
||||
expect(prompt).toContain('<ralph-approved critic="critic">VERIFIED_COMPLETE</ralph-approved>');
|
||||
expect(prompt).toContain('<ralph-approved critic="critic" request-id="req-critic">VERIFIED_COMPLETE</ralph-approved>');
|
||||
});
|
||||
it('should support codex verification prompts', () => {
|
||||
const prompt = getArchitectVerificationPrompt({
|
||||
...baseVerificationState,
|
||||
critic_mode: 'codex',
|
||||
request_id: 'req-codex',
|
||||
});
|
||||
expect(prompt).toContain('[CODEX CRITIC VERIFICATION REQUIRED');
|
||||
expect(prompt).toContain('omc ask codex --agent-prompt critic');
|
||||
expect(prompt).toContain('<ralph-approved critic="codex">VERIFIED_COMPLETE</ralph-approved>');
|
||||
expect(prompt).toContain('<ralph-approved critic="codex" request-id="req-codex">VERIFIED_COMPLETE</ralph-approved>');
|
||||
});
|
||||
it('detects generic Ralph approval markers', () => {
|
||||
expect(detectArchitectApproval('<ralph-approved critic="codex">VERIFIED_COMPLETE</ralph-approved>')).toBe(true);
|
||||
});
|
||||
it('requires matching correlated approval attributes when expected', () => {
|
||||
const staleApproval = '<ralph-approved critic="codex" request-id="old-request" story-id="US-001">VERIFIED_COMPLETE</ralph-approved>';
|
||||
const freshApproval = '<ralph-approved critic="codex" request-id="new-request" story-id="US-001">VERIFIED_COMPLETE</ralph-approved>';
|
||||
expect(detectArchitectApproval(`${staleApproval}\n${freshApproval}`, { request_id: 'new-request', story_id: 'US-001' })).toBe(true);
|
||||
expect(detectArchitectApproval(staleApproval, { request_id: 'new-request', story_id: 'US-001' })).toBe(false);
|
||||
});
|
||||
it('ignores approval tags embedded inside the verification prompt itself', () => {
|
||||
const state = {
|
||||
...baseVerificationState,
|
||||
critic_mode: 'codex',
|
||||
request_id: 'req-injected',
|
||||
story_id: 'US-001',
|
||||
};
|
||||
const prompt = getArchitectVerificationPrompt(state);
|
||||
expect(detectArchitectApproval(prompt, { request_id: 'req-injected', story_id: 'US-001' })).toBe(false);
|
||||
});
|
||||
it('detects codex-style rejection language', () => {
|
||||
const result = detectArchitectRejection('Codex reviewer found issues: Missing tests.');
|
||||
expect(result.rejected).toBe(true);
|
||||
@@ -314,6 +341,7 @@ describe('Ralph PRD-Mandatory', () => {
|
||||
],
|
||||
priority: 1,
|
||||
passes: false,
|
||||
architectVerified: false,
|
||||
},
|
||||
{
|
||||
id: 'US-002',
|
||||
@@ -322,6 +350,7 @@ describe('Ralph PRD-Mandatory', () => {
|
||||
acceptanceCriteria: ['Prometheus endpoint exposes cache metrics'],
|
||||
priority: 2,
|
||||
passes: false,
|
||||
architectVerified: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
2
dist/__tests__/ralph-prd-mandatory.test.js.map
generated
vendored
2
dist/__tests__/ralph-prd-mandatory.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
71
dist/__tests__/ralph-prd.test.js
generated
vendored
71
dist/__tests__/ralph-prd.test.js
generated
vendored
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { readPrd, writePrd, findPrdPath, getPrdStatus, markStoryComplete, markStoryIncomplete, getStory, getNextStory, createPrd, createSimplePrd, initPrd, formatPrdStatus, formatStory, PRD_FILENAME } from '../hooks/ralph/index.js';
|
||||
import { readPrd, writePrd, findPrdPath, getPrdStatus, markStoryComplete, markStoryIncomplete, markStoryArchitectVerified, getStory, getNextStory, createPrd, createSimplePrd, initPrd, ensurePrdForStartup, formatPrdStatus, formatStory, PRD_FILENAME } from '../hooks/ralph/index.js';
|
||||
describe('Ralph PRD Module', () => {
|
||||
let testDir;
|
||||
beforeEach(() => {
|
||||
@@ -54,7 +54,8 @@ describe('Ralph PRD Module', () => {
|
||||
description: 'As a user, I want to test',
|
||||
acceptanceCriteria: ['Criterion 1', 'Criterion 2'],
|
||||
priority: 1,
|
||||
passes: false
|
||||
passes: false,
|
||||
architectVerified: false
|
||||
},
|
||||
{
|
||||
id: 'US-002',
|
||||
@@ -62,7 +63,8 @@ describe('Ralph PRD Module', () => {
|
||||
description: 'As a user, I want more tests',
|
||||
acceptanceCriteria: ['Criterion A'],
|
||||
priority: 2,
|
||||
passes: true
|
||||
passes: true,
|
||||
architectVerified: true
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -96,9 +98,9 @@ describe('Ralph PRD Module', () => {
|
||||
branchName: 'test',
|
||||
description: 'Test',
|
||||
userStories: [
|
||||
{ id: 'US-001', title: 'A', description: '', acceptanceCriteria: [], priority: 1, passes: true },
|
||||
{ id: 'US-002', title: 'B', description: '', acceptanceCriteria: [], priority: 2, passes: false },
|
||||
{ id: 'US-003', title: 'C', description: '', acceptanceCriteria: [], priority: 3, passes: false }
|
||||
{ id: 'US-001', title: 'A', description: '', acceptanceCriteria: [], priority: 1, passes: true, architectVerified: true },
|
||||
{ id: 'US-002', title: 'B', description: '', acceptanceCriteria: [], priority: 2, passes: false, architectVerified: false },
|
||||
{ id: 'US-003', title: 'C', description: '', acceptanceCriteria: [], priority: 3, passes: false, architectVerified: false }
|
||||
]
|
||||
};
|
||||
const status = getPrdStatus(prd);
|
||||
@@ -115,8 +117,8 @@ describe('Ralph PRD Module', () => {
|
||||
branchName: 'test',
|
||||
description: 'Test',
|
||||
userStories: [
|
||||
{ id: 'US-001', title: 'A', description: '', acceptanceCriteria: [], priority: 1, passes: true },
|
||||
{ id: 'US-002', title: 'B', description: '', acceptanceCriteria: [], priority: 2, passes: true }
|
||||
{ id: 'US-001', title: 'A', description: '', acceptanceCriteria: [], priority: 1, passes: true, architectVerified: true },
|
||||
{ id: 'US-002', title: 'B', description: '', acceptanceCriteria: [], priority: 2, passes: true, architectVerified: true }
|
||||
]
|
||||
};
|
||||
const status = getPrdStatus(prd);
|
||||
@@ -130,9 +132,9 @@ describe('Ralph PRD Module', () => {
|
||||
branchName: 'test',
|
||||
description: 'Test',
|
||||
userStories: [
|
||||
{ id: 'US-001', title: 'Low', description: '', acceptanceCriteria: [], priority: 3, passes: false },
|
||||
{ id: 'US-002', title: 'High', description: '', acceptanceCriteria: [], priority: 1, passes: false },
|
||||
{ id: 'US-003', title: 'Med', description: '', acceptanceCriteria: [], priority: 2, passes: false }
|
||||
{ id: 'US-001', title: 'Low', description: '', acceptanceCriteria: [], priority: 3, passes: false, architectVerified: false },
|
||||
{ id: 'US-002', title: 'High', description: '', acceptanceCriteria: [], priority: 1, passes: false, architectVerified: false },
|
||||
{ id: 'US-003', title: 'Med', description: '', acceptanceCriteria: [], priority: 2, passes: false, architectVerified: false }
|
||||
]
|
||||
};
|
||||
const status = getPrdStatus(prd);
|
||||
@@ -158,7 +160,7 @@ describe('Ralph PRD Module', () => {
|
||||
branchName: 'test',
|
||||
description: 'Test',
|
||||
userStories: [
|
||||
{ id: 'US-001', title: 'A', description: '', acceptanceCriteria: [], priority: 1, passes: false }
|
||||
{ id: 'US-001', title: 'A', description: '', acceptanceCriteria: [], priority: 1, passes: false, architectVerified: false }
|
||||
]
|
||||
};
|
||||
writePrd(testDir, prd);
|
||||
@@ -167,6 +169,7 @@ describe('Ralph PRD Module', () => {
|
||||
expect(markStoryComplete(testDir, 'US-001', 'Done!')).toBe(true);
|
||||
const prd = readPrd(testDir);
|
||||
expect(prd?.userStories[0].passes).toBe(true);
|
||||
expect(prd?.userStories[0].architectVerified).toBe(false);
|
||||
expect(prd?.userStories[0].notes).toBe('Done!');
|
||||
});
|
||||
it('should mark story as incomplete', () => {
|
||||
@@ -174,8 +177,17 @@ describe('Ralph PRD Module', () => {
|
||||
expect(markStoryIncomplete(testDir, 'US-001', 'Needs rework')).toBe(true);
|
||||
const prd = readPrd(testDir);
|
||||
expect(prd?.userStories[0].passes).toBe(false);
|
||||
expect(prd?.userStories[0].architectVerified).toBe(false);
|
||||
expect(prd?.userStories[0].notes).toBe('Needs rework');
|
||||
});
|
||||
it('should mark story as architect verified', () => {
|
||||
markStoryComplete(testDir, 'US-001');
|
||||
expect(markStoryArchitectVerified(testDir, 'US-001', 'Approved')).toBe(true);
|
||||
const prd = readPrd(testDir);
|
||||
expect(prd?.userStories[0].passes).toBe(true);
|
||||
expect(prd?.userStories[0].architectVerified).toBe(true);
|
||||
expect(prd?.userStories[0].notes).toBe('Approved');
|
||||
});
|
||||
it('should return false for non-existent story', () => {
|
||||
expect(markStoryComplete(testDir, 'US-999')).toBe(false);
|
||||
});
|
||||
@@ -191,8 +203,8 @@ describe('Ralph PRD Module', () => {
|
||||
branchName: 'test',
|
||||
description: 'Test',
|
||||
userStories: [
|
||||
{ id: 'US-001', title: 'First', description: '', acceptanceCriteria: [], priority: 1, passes: true },
|
||||
{ id: 'US-002', title: 'Second', description: '', acceptanceCriteria: [], priority: 2, passes: false }
|
||||
{ id: 'US-001', title: 'First', description: '', acceptanceCriteria: [], priority: 1, passes: true, architectVerified: true },
|
||||
{ id: 'US-002', title: 'Second', description: '', acceptanceCriteria: [], priority: 2, passes: false, architectVerified: false }
|
||||
]
|
||||
};
|
||||
writePrd(testDir, prd);
|
||||
@@ -218,7 +230,9 @@ describe('Ralph PRD Module', () => {
|
||||
expect(prd.userStories[0].priority).toBe(1);
|
||||
expect(prd.userStories[1].priority).toBe(2);
|
||||
expect(prd.userStories[0].passes).toBe(false);
|
||||
expect(prd.userStories[0].architectVerified).toBe(false);
|
||||
expect(prd.userStories[1].passes).toBe(false);
|
||||
expect(prd.userStories[1].architectVerified).toBe(false);
|
||||
});
|
||||
it('should respect provided priorities', () => {
|
||||
const prd = createPrd('Project', 'branch', 'Description', [
|
||||
@@ -259,6 +273,21 @@ describe('Ralph PRD Module', () => {
|
||||
expect(prd?.userStories.length).toBe(2);
|
||||
});
|
||||
});
|
||||
describe('ensurePrdForStartup', () => {
|
||||
it('creates a scaffold when startup has no prd.json', () => {
|
||||
const result = ensurePrdForStartup(testDir, 'Project', 'branch', 'Description');
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.created).toBe(true);
|
||||
expect(result.prd?.userStories.length).toBe(1);
|
||||
});
|
||||
it('fails clearly when an existing prd.json is invalid', () => {
|
||||
writeFileSync(join(testDir, PRD_FILENAME), '{ invalid json');
|
||||
const result = ensurePrdForStartup(testDir, 'Project', 'branch', 'Description');
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.created).toBe(false);
|
||||
expect(result.error).toContain('Failed to read');
|
||||
});
|
||||
});
|
||||
describe('formatPrdStatus / formatStory', () => {
|
||||
it('should format status correctly', () => {
|
||||
const status = {
|
||||
@@ -294,6 +323,7 @@ describe('Ralph PRD Module', () => {
|
||||
acceptanceCriteria: ['Criterion 1', 'Criterion 2'],
|
||||
priority: 1,
|
||||
passes: false,
|
||||
architectVerified: false,
|
||||
notes: 'Some notes'
|
||||
};
|
||||
const formatted = formatStory(story);
|
||||
@@ -303,6 +333,19 @@ describe('Ralph PRD Module', () => {
|
||||
expect(formatted).toContain('Criterion 1');
|
||||
expect(formatted).toContain('Some notes');
|
||||
});
|
||||
it('should format awaiting architect review status', () => {
|
||||
const story = {
|
||||
id: 'US-002',
|
||||
title: 'Needs review',
|
||||
description: 'Pending approval',
|
||||
acceptanceCriteria: ['Criterion'],
|
||||
priority: 2,
|
||||
passes: true,
|
||||
architectVerified: false
|
||||
};
|
||||
const formatted = formatStory(story);
|
||||
expect(formatted).toContain('AWAITING ARCHITECT REVIEW');
|
||||
});
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=ralph-prd.test.js.map
|
||||
2
dist/__tests__/ralph-prd.test.js.map
generated
vendored
2
dist/__tests__/ralph-prd.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
11
dist/__tests__/rate-limit-wait/pane-fresh-capture.test.js
generated
vendored
11
dist/__tests__/rate-limit-wait/pane-fresh-capture.test.js
generated
vendored
@@ -23,6 +23,7 @@ function noStateFile() {
|
||||
/** Set up fs mock so state file contains the given pane positions. */
|
||||
function withStateFile(positions) {
|
||||
vi.mocked(existsSync).mockReturnValue(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(positions));
|
||||
}
|
||||
/** Queue tmuxExec to return history_size then (optionally) captured lines. */
|
||||
@@ -170,7 +171,7 @@ describe('pane-fresh-capture', () => {
|
||||
vi.mocked(tmuxExec).mockReturnValue(' 137 \n');
|
||||
const result = getPaneHistorySize('%3');
|
||||
expect(result).toBe(137);
|
||||
expect(tmuxExec).toHaveBeenCalledWith(['display-message', '-t', '%3', '-p', '#{history_size}'], expect.objectContaining({ timeout: 3000 }));
|
||||
expect(tmuxExec).toHaveBeenCalledWith(['display-message', '-t', '%3', '-p', '#{pane_dead} #{history_size}'], expect.objectContaining({ timeout: 3000 }));
|
||||
});
|
||||
it('returns null when tmuxExec throws', () => {
|
||||
vi.mocked(tmuxExec).mockImplementation(() => {
|
||||
@@ -178,6 +179,14 @@ describe('pane-fresh-capture', () => {
|
||||
});
|
||||
expect(getPaneHistorySize('%3')).toBeNull();
|
||||
});
|
||||
it('returns null when tmux reports the pane as dead', () => {
|
||||
vi.mocked(tmuxExec).mockReturnValue('1 137\n');
|
||||
expect(getPaneHistorySize('%3')).toBeNull();
|
||||
});
|
||||
it('parses history size when tmux reports a live pane', () => {
|
||||
vi.mocked(tmuxExec).mockReturnValue('0 137\n');
|
||||
expect(getPaneHistorySize('%3')).toBe(137);
|
||||
});
|
||||
it('returns null for non-numeric output', () => {
|
||||
vi.mocked(tmuxExec).mockReturnValue('not-a-number');
|
||||
expect(getPaneHistorySize('%3')).toBeNull();
|
||||
|
||||
2
dist/__tests__/rate-limit-wait/pane-fresh-capture.test.js.map
generated
vendored
2
dist/__tests__/rate-limit-wait/pane-fresh-capture.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
166
dist/__tests__/rate-limit-wait/tmux-detector.test.js
generated
vendored
166
dist/__tests__/rate-limit-wait/tmux-detector.test.js
generated
vendored
@@ -2,13 +2,19 @@
|
||||
* Tests for tmux-detector.ts
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { analyzePaneContent, isTmuxAvailable, listTmuxPanes, capturePaneContent, formatBlockedPanesSummary, } from '../../features/rate-limit-wait/tmux-detector.js';
|
||||
// Mock child_process
|
||||
vi.mock('child_process', () => ({
|
||||
execFileSync: vi.fn(),
|
||||
spawnSync: vi.fn(),
|
||||
import { analyzePaneContent, isTmuxAvailable, listTmuxPanes, capturePaneContent, formatBlockedPanesSummary, scanForBlockedPanes, } from '../../features/rate-limit-wait/tmux-detector.js';
|
||||
// Mock tmux-utils wrappers
|
||||
vi.mock('../../cli/tmux-utils.js', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return { ...actual, tmuxExec: vi.fn(), tmuxSpawn: vi.fn() };
|
||||
});
|
||||
// Mock pane-fresh-capture for scanForBlockedPanes cursor-tracking tests
|
||||
vi.mock('../../features/rate-limit-wait/pane-fresh-capture.js', () => ({
|
||||
getNewPaneTail: vi.fn(),
|
||||
getPaneHistorySize: vi.fn(),
|
||||
}));
|
||||
import { execFileSync, spawnSync } from 'child_process';
|
||||
import { tmuxExec, tmuxSpawn } from '../../cli/tmux-utils.js';
|
||||
import { getNewPaneTail } from '../../features/rate-limit-wait/pane-fresh-capture.js';
|
||||
describe('tmux-detector', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -103,7 +109,7 @@ describe('tmux-detector', () => {
|
||||
});
|
||||
describe('isTmuxAvailable', () => {
|
||||
it('should return true when tmux is installed', () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
vi.mocked(tmuxSpawn).mockReturnValue({
|
||||
status: 0,
|
||||
stdout: '/usr/bin/tmux\n',
|
||||
stderr: '',
|
||||
@@ -114,7 +120,7 @@ describe('tmux-detector', () => {
|
||||
expect(isTmuxAvailable()).toBe(true);
|
||||
});
|
||||
it('should return false when tmux is not installed', () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
vi.mocked(tmuxSpawn).mockReturnValue({
|
||||
status: 1,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
@@ -125,7 +131,7 @@ describe('tmux-detector', () => {
|
||||
expect(isTmuxAvailable()).toBe(false);
|
||||
});
|
||||
it('should return false when spawnSync throws', () => {
|
||||
vi.mocked(spawnSync).mockImplementation(() => {
|
||||
vi.mocked(tmuxSpawn).mockImplementation(() => {
|
||||
throw new Error('Command not found');
|
||||
});
|
||||
expect(isTmuxAvailable()).toBe(false);
|
||||
@@ -133,7 +139,7 @@ describe('tmux-detector', () => {
|
||||
});
|
||||
describe('listTmuxPanes', () => {
|
||||
it('should parse tmux pane list correctly', () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
vi.mocked(tmuxSpawn).mockReturnValue({
|
||||
status: 0,
|
||||
stdout: '/usr/bin/tmux',
|
||||
stderr: '',
|
||||
@@ -141,7 +147,7 @@ describe('tmux-detector', () => {
|
||||
pid: 1234,
|
||||
output: [],
|
||||
});
|
||||
vi.mocked(execFileSync).mockReturnValue('main:0.0 %0 1 dev Claude\nmain:0.1 %1 0 dev Other\n');
|
||||
vi.mocked(tmuxExec).mockReturnValue('main:0.0 %0 1 dev Claude\nmain:0.1 %1 0 dev Other\n');
|
||||
const panes = listTmuxPanes();
|
||||
expect(panes).toHaveLength(2);
|
||||
expect(panes[0]).toEqual({
|
||||
@@ -164,7 +170,7 @@ describe('tmux-detector', () => {
|
||||
});
|
||||
});
|
||||
it('should return empty array when tmux not available', () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
vi.mocked(tmuxSpawn).mockReturnValue({
|
||||
status: 1,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
@@ -178,7 +184,7 @@ describe('tmux-detector', () => {
|
||||
});
|
||||
describe('capturePaneContent', () => {
|
||||
it('should capture pane content', () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
vi.mocked(tmuxSpawn).mockReturnValue({
|
||||
status: 0,
|
||||
stdout: '/usr/bin/tmux',
|
||||
stderr: '',
|
||||
@@ -186,13 +192,13 @@ describe('tmux-detector', () => {
|
||||
pid: 1234,
|
||||
output: [],
|
||||
});
|
||||
vi.mocked(execFileSync).mockReturnValue('Line 1\nLine 2\nLine 3\n');
|
||||
vi.mocked(tmuxExec).mockReturnValue('Line 1\nLine 2\nLine 3\n');
|
||||
const content = capturePaneContent('%0', 3);
|
||||
expect(content).toBe('Line 1\nLine 2\nLine 3\n');
|
||||
expect(execFileSync).toHaveBeenCalledWith('tmux', ['capture-pane', '-t', '%0', '-p', '-S', '-3'], expect.any(Object));
|
||||
expect(tmuxExec).toHaveBeenCalledWith(['capture-pane', '-t', '%0', '-p', '-S', '-3'], expect.any(Object));
|
||||
});
|
||||
it('should return empty string when tmux not available', () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
vi.mocked(tmuxSpawn).mockReturnValue({
|
||||
status: 1,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
@@ -206,7 +212,7 @@ describe('tmux-detector', () => {
|
||||
});
|
||||
describe('security: input validation', () => {
|
||||
it('should reject invalid pane IDs in capturePaneContent', () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
vi.mocked(tmuxSpawn).mockReturnValue({
|
||||
status: 0,
|
||||
stdout: '/usr/bin/tmux',
|
||||
stderr: '',
|
||||
@@ -215,7 +221,7 @@ describe('tmux-detector', () => {
|
||||
output: [],
|
||||
});
|
||||
// Valid pane ID should work
|
||||
vi.mocked(execFileSync).mockReturnValue('content');
|
||||
vi.mocked(tmuxExec).mockReturnValue('content');
|
||||
const validResult = capturePaneContent('%0');
|
||||
expect(validResult).toBe('content');
|
||||
// Invalid pane IDs should return empty string (not execute command)
|
||||
@@ -229,13 +235,13 @@ describe('tmux-detector', () => {
|
||||
'abc',
|
||||
];
|
||||
for (const invalidId of invalidIds) {
|
||||
vi.mocked(execFileSync).mockClear();
|
||||
vi.mocked(tmuxExec).mockClear();
|
||||
const result = capturePaneContent(invalidId);
|
||||
expect(result).toBe('');
|
||||
}
|
||||
});
|
||||
it('should validate lines parameter bounds', () => {
|
||||
vi.mocked(spawnSync).mockReturnValue({
|
||||
vi.mocked(tmuxSpawn).mockReturnValue({
|
||||
status: 0,
|
||||
stdout: '/usr/bin/tmux',
|
||||
stderr: '',
|
||||
@@ -243,14 +249,14 @@ describe('tmux-detector', () => {
|
||||
pid: 1234,
|
||||
output: [],
|
||||
});
|
||||
vi.mocked(execFileSync).mockReturnValue('content');
|
||||
vi.mocked(tmuxExec).mockReturnValue('content');
|
||||
// Should clamp negative to 1
|
||||
capturePaneContent('%0', -5);
|
||||
expect(execFileSync).toHaveBeenCalledWith('tmux', expect.arrayContaining(['-S', '-1']), expect.any(Object));
|
||||
expect(tmuxExec).toHaveBeenCalledWith(expect.arrayContaining(['-S', '-1']), expect.any(Object));
|
||||
// Should clamp excessive values to 100
|
||||
vi.mocked(execFileSync).mockClear();
|
||||
vi.mocked(tmuxExec).mockClear();
|
||||
capturePaneContent('%0', 1000);
|
||||
expect(execFileSync).toHaveBeenCalledWith('tmux', expect.arrayContaining(['-S', '-100']), expect.any(Object));
|
||||
expect(tmuxExec).toHaveBeenCalledWith(expect.arrayContaining(['-S', '-100']), expect.any(Object));
|
||||
});
|
||||
});
|
||||
describe('formatBlockedPanesSummary', () => {
|
||||
@@ -308,5 +314,117 @@ describe('tmux-detector', () => {
|
||||
expect(result).toContain('[RESUMED]');
|
||||
});
|
||||
});
|
||||
// ── Regression: stale tmux keyword false-positives ────────────────────────
|
||||
describe('analyzePaneContent — false-positive suppression', () => {
|
||||
it('should NOT flag git log with "weekly" in a commit message as rate-limited', () => {
|
||||
// Reproduces: running `git log` in a Claude Code session pane where a
|
||||
// commit message contains "weekly" caused a false blocked-pane alert.
|
||||
const content = `
|
||||
Claude Code v1.0
|
||||
$ git log --oneline -3
|
||||
commit abc1234def5678901234
|
||||
Author: Dev <dev@example.com>
|
||||
Date: Mon Jan 1 10:00:00 2024 +0000
|
||||
|
||||
Fix weekly report generation bug
|
||||
|
||||
commit def5678abc1234567890
|
||||
Author: Dev <dev@example.com>
|
||||
Date: Sun Dec 31 09:00:00 2023 +0000
|
||||
|
||||
Update assistant configuration docs
|
||||
|
||||
> `;
|
||||
const result = analyzePaneContent(content);
|
||||
expect(result.hasRateLimitMessage).toBe(false);
|
||||
expect(result.isBlocked).toBe(false);
|
||||
});
|
||||
it('should NOT flag git diff patch containing "weekly" in diff context', () => {
|
||||
const content = `
|
||||
claude
|
||||
$ git diff HEAD~1
|
||||
diff --git a/src/reports/weekly.ts b/src/reports/weekly.ts
|
||||
--- a/src/reports/weekly.ts
|
||||
+++ b/src/reports/weekly.ts
|
||||
@@ -1,3 +1,4 @@
|
||||
-// weekly report generator
|
||||
+// weekly report generator (updated)
|
||||
> `;
|
||||
const result = analyzePaneContent(content);
|
||||
expect(result.hasRateLimitMessage).toBe(false);
|
||||
expect(result.isBlocked).toBe(false);
|
||||
});
|
||||
it('should STILL detect genuine "weekly usage limit" rate-limit message', () => {
|
||||
// Positive control: genuine Claude Code rate-limit screen must still trigger.
|
||||
const content = `
|
||||
Claude Code
|
||||
|
||||
⚠️ Weekly usage limit reached
|
||||
|
||||
You've used your weekly allocation of tokens.
|
||||
Limit resets Monday at 12:00 AM UTC.
|
||||
|
||||
[1] Continue when limit resets
|
||||
[2] Exit
|
||||
|
||||
Enter choice: `;
|
||||
const result = analyzePaneContent(content);
|
||||
expect(result.hasRateLimitMessage).toBe(true);
|
||||
expect(result.isBlocked).toBe(true);
|
||||
expect(result.rateLimitType).toBe('weekly');
|
||||
});
|
||||
it('should STILL detect "weekly quota exceeded" phrasing', () => {
|
||||
const content = `
|
||||
Claude Code
|
||||
Weekly usage quota exceeded
|
||||
Please try again later
|
||||
`;
|
||||
const result = analyzePaneContent(content);
|
||||
expect(result.hasRateLimitMessage).toBe(true);
|
||||
expect(result.rateLimitType).toBe('weekly');
|
||||
});
|
||||
});
|
||||
// ── Regression: scanForBlockedPanes stale-history via cursor tracking ──────
|
||||
describe('scanForBlockedPanes — cursor-tracked stateDir path', () => {
|
||||
const tmuxAvailableReturn = {
|
||||
status: 0,
|
||||
stdout: '/usr/bin/tmux',
|
||||
stderr: '',
|
||||
signal: null,
|
||||
pid: 1234,
|
||||
output: [],
|
||||
};
|
||||
it('skips panes with no new output when stateDir is provided (stale suppression)', () => {
|
||||
vi.mocked(tmuxSpawn).mockReturnValue(tmuxAvailableReturn);
|
||||
vi.mocked(tmuxExec).mockReturnValue('main:0.0 %0 1 dev Claude\n');
|
||||
// getNewPaneTail returns '' → no new lines → pane should be skipped
|
||||
vi.mocked(getNewPaneTail).mockReturnValue('');
|
||||
const blocked = scanForBlockedPanes(15, '/project/.omc/state');
|
||||
expect(blocked).toHaveLength(0);
|
||||
// getNewPaneTail must be called with the provided stateDir
|
||||
expect(getNewPaneTail).toHaveBeenCalledWith('%0', '/project/.omc/state', 15);
|
||||
});
|
||||
it('detects a blocked pane from fresh delta lines when stateDir is provided', () => {
|
||||
vi.mocked(tmuxSpawn).mockReturnValue(tmuxAvailableReturn);
|
||||
vi.mocked(tmuxExec).mockReturnValue('main:0.0 %0 1 dev Claude\n');
|
||||
// getNewPaneTail returns new rate-limit content
|
||||
vi.mocked(getNewPaneTail).mockReturnValue('Claude Code\nYou\'ve hit your limit · resets Feb 17 at 2pm\n❯ 1. Stop and wait\nEnter to confirm');
|
||||
const blocked = scanForBlockedPanes(15, '/project/.omc/state');
|
||||
expect(blocked).toHaveLength(1);
|
||||
expect(blocked[0].id).toBe('%0');
|
||||
expect(blocked[0].analysis.isBlocked).toBe(true);
|
||||
});
|
||||
it('falls back to capturePaneContent when no stateDir provided', () => {
|
||||
vi.mocked(tmuxSpawn).mockReturnValue(tmuxAvailableReturn);
|
||||
// listTmuxPanes + capturePaneContent both use tmuxExec
|
||||
vi.mocked(tmuxExec)
|
||||
.mockReturnValueOnce('main:0.0 %0 1 dev Claude\n') // listTmuxPanes
|
||||
.mockReturnValueOnce(''); // capturePaneContent → empty
|
||||
const blocked = scanForBlockedPanes(15);
|
||||
// capturePaneContent used, getNewPaneTail must NOT be called
|
||||
expect(getNewPaneTail).not.toHaveBeenCalled();
|
||||
expect(blocked).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=tmux-detector.test.js.map
|
||||
2
dist/__tests__/rate-limit-wait/tmux-detector.test.js.map
generated
vendored
2
dist/__tests__/rate-limit-wait/tmux-detector.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
57
dist/__tests__/run-cjs-graceful-fallback.test.js
generated
vendored
57
dist/__tests__/run-cjs-graceful-fallback.test.js
generated
vendored
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from 'fs';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { execFileSync } from 'child_process';
|
||||
@@ -76,9 +76,10 @@ describe('run.cjs — graceful fallback for stale plugin paths', () => {
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
it('falls back to latest version when target version is missing', () => {
|
||||
const markerPath = join(tmpDir, 'hook-ok.txt');
|
||||
// Create a valid latest version with the target script
|
||||
const _latestDir = createFakeVersion('4.4.5', {
|
||||
'test-hook.cjs': '#!/usr/bin/env node\nconsole.log("hook-ok"); process.exit(0);',
|
||||
'test-hook.cjs': `#!/usr/bin/env node\nrequire('fs').writeFileSync(${JSON.stringify(markerPath)}, "hook-ok"); process.exit(0);`,
|
||||
});
|
||||
// Target points to a non-existent old version
|
||||
const staleVersion = join(fakeCacheBase, '4.2.14');
|
||||
@@ -88,15 +89,16 @@ describe('run.cjs — graceful fallback for stale plugin paths', () => {
|
||||
});
|
||||
// Should find the script in 4.4.5 and run it successfully
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain('hook-ok');
|
||||
expect(readFileSync(markerPath, 'utf-8')).toBe('hook-ok');
|
||||
});
|
||||
it('falls back to latest version when multiple versions exist', () => {
|
||||
const markerPath = join(tmpDir, 'version-picked.txt');
|
||||
// Create two valid versions
|
||||
createFakeVersion('4.4.3', {
|
||||
'test-hook.cjs': '#!/usr/bin/env node\nconsole.log("from-4.4.3"); process.exit(0);',
|
||||
'test-hook.cjs': `#!/usr/bin/env node\nrequire('fs').writeFileSync(${JSON.stringify(markerPath)}, "from-4.4.3"); process.exit(0);`,
|
||||
});
|
||||
createFakeVersion('4.4.5', {
|
||||
'test-hook.cjs': '#!/usr/bin/env node\nconsole.log("from-4.4.5"); process.exit(0);',
|
||||
'test-hook.cjs': `#!/usr/bin/env node\nrequire('fs').writeFileSync(${JSON.stringify(markerPath)}, "from-4.4.5"); process.exit(0);`,
|
||||
});
|
||||
// Target points to a deleted old version
|
||||
const staleVersion = join(fakeCacheBase, '4.2.14');
|
||||
@@ -106,12 +108,13 @@ describe('run.cjs — graceful fallback for stale plugin paths', () => {
|
||||
});
|
||||
// Should pick the highest version (4.4.5)
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain('from-4.4.5');
|
||||
expect(readFileSync(markerPath, 'utf-8')).toBe('from-4.4.5');
|
||||
});
|
||||
it('resolves target through symlinked version directory', () => {
|
||||
const markerPath = join(tmpDir, 'symlink-hit.txt');
|
||||
// Create a real latest version
|
||||
const _latestDir = createFakeVersion('4.4.5', {
|
||||
'test-hook.cjs': '#!/usr/bin/env node\nconsole.log("via-symlink"); process.exit(0);',
|
||||
'test-hook.cjs': `#!/usr/bin/env node\nrequire('fs').writeFileSync(${JSON.stringify(markerPath)}, "via-symlink"); process.exit(0);`,
|
||||
});
|
||||
// Create a symlink from old version to latest
|
||||
const symlinkVersion = join(fakeCacheBase, '4.4.3');
|
||||
@@ -122,18 +125,19 @@ describe('run.cjs — graceful fallback for stale plugin paths', () => {
|
||||
CLAUDE_PLUGIN_ROOT: symlinkVersion,
|
||||
});
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain('via-symlink');
|
||||
expect(readFileSync(markerPath, 'utf-8')).toBe('via-symlink');
|
||||
});
|
||||
it('runs target normally when path is valid (fast path)', () => {
|
||||
const markerPath = join(tmpDir, 'direct-hit.txt');
|
||||
const versionDir = createFakeVersion('4.4.5', {
|
||||
'test-hook.cjs': '#!/usr/bin/env node\nconsole.log("direct-ok"); process.exit(0);',
|
||||
'test-hook.cjs': `#!/usr/bin/env node\nrequire('fs').writeFileSync(${JSON.stringify(markerPath)}, "direct-ok"); process.exit(0);`,
|
||||
});
|
||||
const target = join(versionDir, 'scripts', 'test-hook.cjs');
|
||||
const result = runCjs(target, {
|
||||
CLAUDE_PLUGIN_ROOT: versionDir,
|
||||
});
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain('direct-ok');
|
||||
expect(readFileSync(markerPath, 'utf-8')).toBe('direct-ok');
|
||||
});
|
||||
it('exits 0 when no CLAUDE_PLUGIN_ROOT is set and target is missing', () => {
|
||||
const result = runCjs('/nonexistent/path/to/hook.mjs', {
|
||||
@@ -163,5 +167,38 @@ describe('run.cjs — graceful fallback for stale plugin paths', () => {
|
||||
// No version has test-hook.cjs, so exit 0 gracefully
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
it('honors hooks.json timeouts so wrapped hooks fail open instead of blocking', () => {
|
||||
const pluginRoot = join(tmpDir, 'plugin-root');
|
||||
const scriptsDir = join(pluginRoot, 'scripts');
|
||||
const hooksDir = join(pluginRoot, 'hooks');
|
||||
mkdirSync(scriptsDir, { recursive: true });
|
||||
mkdirSync(hooksDir, { recursive: true });
|
||||
const slowTarget = join(scriptsDir, 'slow-stop-hook.cjs');
|
||||
writeFileSync(slowTarget, 'setTimeout(() => { process.stdout.write("slow-stop-done\\n"); process.exit(0); }, 3000);');
|
||||
writeFileSync(join(hooksDir, 'hooks.json'), JSON.stringify({
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
matcher: '',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
command: 'node "$CLAUDE_PLUGIN_ROOT"/scripts/run.cjs "$CLAUDE_PLUGIN_ROOT"/scripts/slow-stop-hook.cjs',
|
||||
timeout: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}, null, 2));
|
||||
const startedAt = Date.now();
|
||||
const result = runCjs(slowTarget, {
|
||||
CLAUDE_PLUGIN_ROOT: pluginRoot,
|
||||
});
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).not.toContain('slow-stop-done');
|
||||
expect(elapsedMs).toBeLessThan(2500);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=run-cjs-graceful-fallback.test.js.map
|
||||
2
dist/__tests__/run-cjs-graceful-fallback.test.js.map
generated
vendored
2
dist/__tests__/run-cjs-graceful-fallback.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
14
dist/__tests__/runtime-task-orphan.test.js
generated
vendored
14
dist/__tests__/runtime-task-orphan.test.js
generated
vendored
@@ -8,12 +8,10 @@ import { tmpdir } from 'os';
|
||||
* instead of leaving it orphaned.
|
||||
*/
|
||||
// --- Mocks ---
|
||||
const mockExecFileAsync = vi.fn();
|
||||
vi.mock('child_process', () => {
|
||||
const execFile = Object.assign(vi.fn(), {
|
||||
[Symbol.for('nodejs.util.promisify.custom')]: mockExecFileAsync,
|
||||
});
|
||||
return { execFile };
|
||||
const mockTmuxExecAsync = vi.fn();
|
||||
vi.mock('../cli/tmux-utils.js', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return { ...actual, tmuxExecAsync: mockTmuxExecAsync };
|
||||
});
|
||||
vi.mock('../team/model-contract.js', () => ({
|
||||
buildWorkerArgv: vi.fn(() => ['/usr/bin/claude', '--flag']),
|
||||
@@ -50,7 +48,7 @@ describe('spawnWorkerForTask task orphan prevention', () => {
|
||||
let tmpDir;
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'runtime-task-orphan-'));
|
||||
mockExecFileAsync.mockReset();
|
||||
mockTmuxExecAsync.mockReset();
|
||||
});
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
@@ -73,7 +71,7 @@ describe('spawnWorkerForTask task orphan prevention', () => {
|
||||
createdAt: new Date().toISOString(),
|
||||
}));
|
||||
// Mock tmux split-window to return empty stdout (pane creation failure)
|
||||
mockExecFileAsync.mockResolvedValue({ stdout: '\n', stderr: '' });
|
||||
mockTmuxExecAsync.mockResolvedValue({ stdout: '\n', stderr: '' });
|
||||
const runtime = {
|
||||
teamName,
|
||||
sessionName: 'test-session',
|
||||
|
||||
2
dist/__tests__/runtime-task-orphan.test.js.map
generated
vendored
2
dist/__tests__/runtime-task-orphan.test.js.map
generated
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"runtime-task-orphan.test.js","sourceRoot":"","sources":["../../src/__tests__/runtime-task-orphan.test.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"runtime-task-orphan.test.js","sourceRoot":"","sources":["../../src/__tests__/runtime-task-orphan.test.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/__tests__/session-history-search.test.js
generated
vendored
2
dist/__tests__/session-history-search.test.js
generated
vendored
@@ -4,7 +4,7 @@ import { homedir, tmpdir } from 'os';
|
||||
import { basename, join } from 'path';
|
||||
import { parseSinceSpec, searchSessionHistory, } from '../features/session-history-search/index.js';
|
||||
function encodeProjectPath(projectPath) {
|
||||
return projectPath.replace(/[\\/]/g, '-');
|
||||
return projectPath.replace(/[/\\.]/g, '-');
|
||||
}
|
||||
function writeTranscript(filePath, entries) {
|
||||
mkdirSync(join(filePath, '..'), { recursive: true });
|
||||
|
||||
2
dist/__tests__/session-history-search.test.js.map
generated
vendored
2
dist/__tests__/session-history-search.test.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/__tests__/skills-frontmatter-regression.test.d.ts
generated
vendored
Normal file
2
dist/__tests__/skills-frontmatter-regression.test.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=skills-frontmatter-regression.test.d.ts.map
|
||||
1
dist/__tests__/skills-frontmatter-regression.test.d.ts.map
generated
vendored
Normal file
1
dist/__tests__/skills-frontmatter-regression.test.d.ts.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"skills-frontmatter-regression.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/skills-frontmatter-regression.test.ts"],"names":[],"mappings":""}
|
||||
36
dist/__tests__/skills-frontmatter-regression.test.js
generated
vendored
Normal file
36
dist/__tests__/skills-frontmatter-regression.test.js
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { createBuiltinSkills, clearSkillsCache, getBuiltinSkill } from '../features/builtin-skills/skills.js';
|
||||
describe('builtin skill drafting contracts for learned skills (issue #2425)', () => {
|
||||
const originalUserType = process.env.USER_TYPE;
|
||||
beforeEach(() => {
|
||||
process.env.USER_TYPE = 'ant';
|
||||
clearSkillsCache();
|
||||
});
|
||||
afterEach(() => {
|
||||
if (originalUserType === undefined) {
|
||||
delete process.env.USER_TYPE;
|
||||
}
|
||||
else {
|
||||
process.env.USER_TYPE = originalUserType;
|
||||
}
|
||||
clearSkillsCache();
|
||||
});
|
||||
it('learner skill instructs writing YAML frontmatter and flat learned-skill file paths', () => {
|
||||
const learner = getBuiltinSkill('learner');
|
||||
expect(learner).toBeDefined();
|
||||
expect(learner.template).toContain('MUST start with YAML frontmatter');
|
||||
expect(learner.template).toContain('Do **not** write plain markdown without frontmatter.');
|
||||
expect(learner.template).toContain('.omc/skills/<skill-name>.md');
|
||||
expect(learner.template).toContain('skills/omc-learned/<skill-name>.md');
|
||||
});
|
||||
it('skillify skill instructs drafting flat file-backed skills with YAML frontmatter', () => {
|
||||
const skills = createBuiltinSkills();
|
||||
const skillify = skills.find((skill) => skill.name === 'skillify');
|
||||
expect(skillify).toBeDefined();
|
||||
expect(skillify.template).toContain('output a complete skill file that starts with YAML frontmatter');
|
||||
expect(skillify.template).toContain('Never emit plain markdown-only skill files.');
|
||||
expect(skillify.template).toContain('.omc/skills/<skill-name>.md');
|
||||
expect(skillify.template).toContain('skills/omc-learned/<skill-name>.md');
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=skills-frontmatter-regression.test.js.map
|
||||
1
dist/__tests__/skills-frontmatter-regression.test.js.map
generated
vendored
Normal file
1
dist/__tests__/skills-frontmatter-regression.test.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"skills-frontmatter-regression.test.js","sourceRoot":"","sources":["../../src/__tests__/skills-frontmatter-regression.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAE9G,QAAQ,CAAC,mEAAmE,EAAE,GAAG,EAAE;IACjF,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;IAE/C,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,CAAC,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC;QAC9B,gBAAgB,EAAE,CAAC;IACrB,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAC/B,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,SAAS,GAAG,gBAAgB,CAAC;QAC3C,CAAC;QACD,gBAAgB,EAAE,CAAC;IACrB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oFAAoF,EAAE,GAAG,EAAE;QAC5F,MAAM,OAAO,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;QAE3C,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9B,MAAM,CAAC,OAAQ,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAC;QACxE,MAAM,CAAC,OAAQ,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,sDAAsD,CAAC,CAAC;QAC5F,MAAM,CAAC,OAAQ,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAC;QACnE,MAAM,CAAC,OAAQ,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,oCAAoC,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iFAAiF,EAAE,GAAG,EAAE;QACzF,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;QAEnE,MAAM,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,MAAM,CAAC,QAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,gEAAgE,CAAC,CAAC;QACvG,MAAM,CAAC,QAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,6CAA6C,CAAC,CAAC;QACpF,MAAM,CAAC,QAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAC;QACpE,MAAM,CAAC,QAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,oCAAoC,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
||||
53
dist/__tests__/standalone-server.test.js
generated
vendored
53
dist/__tests__/standalone-server.test.js
generated
vendored
@@ -6,9 +6,13 @@ import { stateTools } from '../tools/state-tools.js';
|
||||
import { notepadTools } from '../tools/notepad-tools.js';
|
||||
import { memoryTools } from '../tools/memory-tools.js';
|
||||
import { traceTools } from '../tools/trace-tools.js';
|
||||
import { sharedMemoryTools } from '../tools/shared-memory-tools.js';
|
||||
import { deepinitManifestTool } from '../tools/deepinit-manifest.js';
|
||||
import { wikiTools } from '../tools/wiki-tools.js';
|
||||
import { skillsTools } from '../tools/skills-tools.js';
|
||||
describe('standalone-server tool composition', () => {
|
||||
// These are the exact same tool arrays that standalone-server.ts imports
|
||||
// This test validates our expectations about tool counts
|
||||
// These are the raw tool arrays aggregated by tool-registry.ts into allTools.
|
||||
// This test validates per-array counts; for the live MCP surface use standalone-listtools.test.ts.
|
||||
const expectedTools = [
|
||||
...lspTools,
|
||||
...astTools,
|
||||
@@ -17,11 +21,16 @@ describe('standalone-server tool composition', () => {
|
||||
...notepadTools,
|
||||
...memoryTools,
|
||||
...traceTools,
|
||||
...sharedMemoryTools,
|
||||
deepinitManifestTool,
|
||||
...wikiTools,
|
||||
...skillsTools,
|
||||
];
|
||||
it('should have at least the expected total tool count', () => {
|
||||
// 12 LSP + 2 AST + 1 python + 5 state + 6 notepad + 4 memory + 3 trace = 33 baseline.
|
||||
// 12 LSP + 2 AST + 1 python + 5 state + 6 notepad + 4 memory + 3 trace
|
||||
// + 5 shared_memory + 1 deepinit + 7 wiki + 3 skills = 49 baseline.
|
||||
// Use ≥ so this guard doesn't break when new tools are legitimately added.
|
||||
expect(expectedTools.length).toBeGreaterThanOrEqual(33);
|
||||
expect(expectedTools.length).toBeGreaterThanOrEqual(49);
|
||||
});
|
||||
it('should include 3 trace tools', () => {
|
||||
expect(traceTools).toHaveLength(3);
|
||||
@@ -38,6 +47,42 @@ describe('standalone-server tool composition', () => {
|
||||
const names = traceTools.map(t => t.name);
|
||||
expect(names).toContain('session_search');
|
||||
});
|
||||
it('should include 7 wiki tools', () => {
|
||||
expect(wikiTools).toHaveLength(7);
|
||||
});
|
||||
it('should include wiki_ingest tool', () => {
|
||||
const names = wikiTools.map(t => t.name);
|
||||
expect(names).toContain('wiki_ingest');
|
||||
});
|
||||
it('should include wiki_query tool', () => {
|
||||
const names = wikiTools.map(t => t.name);
|
||||
expect(names).toContain('wiki_query');
|
||||
});
|
||||
it('should include 5 shared_memory tools', () => {
|
||||
expect(sharedMemoryTools).toHaveLength(5);
|
||||
});
|
||||
it('should include shared_memory_write tool', () => {
|
||||
const names = sharedMemoryTools.map(t => t.name);
|
||||
expect(names).toContain('shared_memory_write');
|
||||
});
|
||||
it('should include shared_memory_read tool', () => {
|
||||
const names = sharedMemoryTools.map(t => t.name);
|
||||
expect(names).toContain('shared_memory_read');
|
||||
});
|
||||
it('should include 3 skills tools', () => {
|
||||
expect(skillsTools).toHaveLength(3);
|
||||
});
|
||||
it('should include load_omc_skills_local tool', () => {
|
||||
const names = skillsTools.map(t => t.name);
|
||||
expect(names).toContain('load_omc_skills_local');
|
||||
});
|
||||
it('should include list_omc_skills tool', () => {
|
||||
const names = skillsTools.map(t => t.name);
|
||||
expect(names).toContain('list_omc_skills');
|
||||
});
|
||||
it('should include deepinit_manifest tool', () => {
|
||||
expect(deepinitManifestTool.name).toBe('deepinit_manifest');
|
||||
});
|
||||
it('should have no duplicate tool names', () => {
|
||||
const names = expectedTools.map(t => t.name);
|
||||
const uniqueNames = new Set(names);
|
||||
|
||||
2
dist/__tests__/standalone-server.test.js.map
generated
vendored
2
dist/__tests__/standalone-server.test.js.map
generated
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"standalone-server.test.js","sourceRoot":"","sources":["../../src/__tests__/standalone-server.test.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"standalone-server.test.js","sourceRoot":"","sources":["../../src/__tests__/standalone-server.test.ts"],"names":[],"mappings":"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"}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user