oh-my-claude-sisyphus 3.8.15 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +1 -1
  4. package/README.md +9 -11
  5. package/agents/analyst.md +41 -0
  6. package/agents/architect.md +45 -0
  7. package/agents/critic.md +42 -0
  8. package/agents/deep-executor.md +193 -0
  9. package/agents/planner.md +82 -0
  10. package/bridge/mcp-server.cjs +1 -1
  11. package/commands/autopilot.md +2 -6
  12. package/commands/hud.md +7 -2
  13. package/commands/ralph.md +3 -3
  14. package/commands/ultrapilot.md +2 -6
  15. package/dist/__tests__/agent-registry.test.js +1 -1
  16. package/dist/__tests__/delegation-enforcement-levels.test.js +0 -1
  17. package/dist/__tests__/delegation-enforcement-levels.test.js.map +1 -1
  18. package/dist/__tests__/hooks/learner/parser.test.d.ts +5 -0
  19. package/dist/__tests__/hooks/learner/parser.test.d.ts.map +1 -0
  20. package/dist/__tests__/hooks/learner/parser.test.js +201 -0
  21. package/dist/__tests__/hooks/learner/parser.test.js.map +1 -0
  22. package/dist/__tests__/hud/cwd.test.d.ts +2 -0
  23. package/dist/__tests__/hud/cwd.test.d.ts.map +1 -0
  24. package/dist/__tests__/hud/cwd.test.js +62 -0
  25. package/dist/__tests__/hud/cwd.test.js.map +1 -0
  26. package/dist/__tests__/hud/defaults.test.d.ts +2 -0
  27. package/dist/__tests__/hud/defaults.test.d.ts.map +1 -0
  28. package/dist/__tests__/hud/defaults.test.js +21 -0
  29. package/dist/__tests__/hud/defaults.test.js.map +1 -0
  30. package/dist/__tests__/hud/render.test.d.ts +2 -0
  31. package/dist/__tests__/hud/render.test.d.ts.map +1 -0
  32. package/dist/__tests__/hud/render.test.js +141 -0
  33. package/dist/__tests__/hud/render.test.js.map +1 -0
  34. package/dist/__tests__/hud/thinking.test.d.ts +2 -0
  35. package/dist/__tests__/hud/thinking.test.d.ts.map +1 -0
  36. package/dist/__tests__/hud/thinking.test.js +32 -0
  37. package/dist/__tests__/hud/thinking.test.js.map +1 -0
  38. package/dist/__tests__/installer.test.js +8 -8
  39. package/dist/__tests__/installer.test.js.map +1 -1
  40. package/dist/__tests__/mnemosyne/parser.test.js +1 -1
  41. package/dist/__tests__/mnemosyne/parser.test.js.map +1 -1
  42. package/dist/__tests__/omc-tools-server.test.js +2 -2
  43. package/dist/__tests__/omc-tools-server.test.js.map +1 -1
  44. package/dist/__tests__/skills.test.js +5 -4
  45. package/dist/__tests__/skills.test.js.map +1 -1
  46. package/dist/agents/deep-executor.d.ts +15 -0
  47. package/dist/agents/deep-executor.d.ts.map +1 -0
  48. package/dist/agents/deep-executor.js +47 -0
  49. package/dist/agents/deep-executor.js.map +1 -0
  50. package/dist/agents/definitions.d.ts +15 -0
  51. package/dist/agents/definitions.d.ts.map +1 -1
  52. package/dist/agents/definitions.js +25 -0
  53. package/dist/agents/definitions.js.map +1 -1
  54. package/dist/agents/index.d.ts +1 -0
  55. package/dist/agents/index.d.ts.map +1 -1
  56. package/dist/agents/index.js +1 -0
  57. package/dist/agents/index.js.map +1 -1
  58. package/dist/cli/commands/doctor-conflicts.d.ts +55 -0
  59. package/dist/cli/commands/doctor-conflicts.d.ts.map +1 -0
  60. package/dist/cli/commands/doctor-conflicts.js +261 -0
  61. package/dist/cli/commands/doctor-conflicts.js.map +1 -0
  62. package/dist/cli/index.js +16 -1
  63. package/dist/cli/index.js.map +1 -1
  64. package/dist/features/auto-update.d.ts +12 -0
  65. package/dist/features/auto-update.d.ts.map +1 -1
  66. package/dist/features/auto-update.js +4 -1
  67. package/dist/features/auto-update.js.map +1 -1
  68. package/dist/features/context-injector/types.d.ts +1 -1
  69. package/dist/features/context-injector/types.d.ts.map +1 -1
  70. package/dist/features/continuation-enforcement.js +1 -1
  71. package/dist/features/state-manager/index.d.ts.map +1 -1
  72. package/dist/features/state-manager/index.js +7 -4
  73. package/dist/features/state-manager/index.js.map +1 -1
  74. package/dist/features/verification/example.d.ts.map +1 -1
  75. package/dist/features/verification/example.js +4 -2
  76. package/dist/features/verification/example.js.map +1 -1
  77. package/dist/hooks/__tests__/bridge.test.d.ts +2 -0
  78. package/dist/hooks/__tests__/bridge.test.d.ts.map +1 -0
  79. package/dist/hooks/__tests__/bridge.test.js +199 -0
  80. package/dist/hooks/__tests__/bridge.test.js.map +1 -0
  81. package/dist/hooks/beads-context/__tests__/index.test.d.ts +2 -0
  82. package/dist/hooks/beads-context/__tests__/index.test.d.ts.map +1 -0
  83. package/dist/hooks/beads-context/__tests__/index.test.js +150 -0
  84. package/dist/hooks/beads-context/__tests__/index.test.js.map +1 -0
  85. package/dist/hooks/beads-context/constants.d.ts +3 -0
  86. package/dist/hooks/beads-context/constants.d.ts.map +1 -0
  87. package/dist/hooks/beads-context/constants.js +35 -0
  88. package/dist/hooks/beads-context/constants.js.map +1 -0
  89. package/dist/hooks/beads-context/index.d.ts +21 -0
  90. package/dist/hooks/beads-context/index.d.ts.map +1 -0
  91. package/dist/hooks/beads-context/index.js +62 -0
  92. package/dist/hooks/beads-context/index.js.map +1 -0
  93. package/dist/hooks/beads-context/types.d.ts +7 -0
  94. package/dist/hooks/beads-context/types.d.ts.map +1 -0
  95. package/dist/hooks/beads-context/types.js +2 -0
  96. package/dist/hooks/beads-context/types.js.map +1 -0
  97. package/dist/hooks/bridge.d.ts +4 -0
  98. package/dist/hooks/bridge.d.ts.map +1 -1
  99. package/dist/hooks/bridge.js +80 -47
  100. package/dist/hooks/bridge.js.map +1 -1
  101. package/dist/hooks/index.d.ts +2 -1
  102. package/dist/hooks/index.d.ts.map +1 -1
  103. package/dist/hooks/index.js +4 -1
  104. package/dist/hooks/index.js.map +1 -1
  105. package/dist/hooks/learner/parser.d.ts.map +1 -1
  106. package/dist/hooks/learner/parser.js +12 -5
  107. package/dist/hooks/learner/parser.js.map +1 -1
  108. package/dist/hooks/mode-registry/index.d.ts +2 -0
  109. package/dist/hooks/mode-registry/index.d.ts.map +1 -1
  110. package/dist/hooks/mode-registry/index.js +8 -19
  111. package/dist/hooks/mode-registry/index.js.map +1 -1
  112. package/dist/hooks/permission-handler/index.d.ts.map +1 -1
  113. package/dist/hooks/permission-handler/index.js +3 -1
  114. package/dist/hooks/permission-handler/index.js.map +1 -1
  115. package/dist/hooks/persistent-mode/index.d.ts +1 -1
  116. package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
  117. package/dist/hooks/persistent-mode/index.js +5 -33
  118. package/dist/hooks/persistent-mode/index.js.map +1 -1
  119. package/dist/hooks/ralph/index.d.ts +1 -1
  120. package/dist/hooks/ralph/index.d.ts.map +1 -1
  121. package/dist/hooks/ralph/index.js +1 -1
  122. package/dist/hooks/ralph/index.js.map +1 -1
  123. package/dist/hooks/ralph/loop.d.ts +1 -9
  124. package/dist/hooks/ralph/loop.d.ts.map +1 -1
  125. package/dist/hooks/ralph/loop.js +1 -37
  126. package/dist/hooks/ralph/loop.js.map +1 -1
  127. package/dist/hooks/ralph/prd.js +1 -1
  128. package/dist/hooks/ralph/verifier.d.ts +4 -5
  129. package/dist/hooks/ralph/verifier.d.ts.map +1 -1
  130. package/dist/hooks/ralph/verifier.js +7 -10
  131. package/dist/hooks/ralph/verifier.js.map +1 -1
  132. package/dist/hooks/session-end/index.d.ts +13 -0
  133. package/dist/hooks/session-end/index.d.ts.map +1 -1
  134. package/dist/hooks/session-end/index.js +69 -0
  135. package/dist/hooks/session-end/index.js.map +1 -1
  136. package/dist/hooks/setup/index.d.ts.map +1 -1
  137. package/dist/hooks/setup/index.js +12 -5
  138. package/dist/hooks/setup/index.js.map +1 -1
  139. package/dist/hooks/subagent-tracker/index.d.ts.map +1 -1
  140. package/dist/hooks/subagent-tracker/index.js +25 -9
  141. package/dist/hooks/subagent-tracker/index.js.map +1 -1
  142. package/dist/hooks/ultrawork/index.d.ts +2 -2
  143. package/dist/hooks/ultrawork/index.d.ts.map +1 -1
  144. package/dist/hooks/ultrawork/index.js +2 -46
  145. package/dist/hooks/ultrawork/index.js.map +1 -1
  146. package/dist/hud/elements/cwd.d.ts +15 -0
  147. package/dist/hud/elements/cwd.d.ts.map +1 -0
  148. package/dist/hud/elements/cwd.js +39 -0
  149. package/dist/hud/elements/cwd.js.map +1 -0
  150. package/dist/hud/elements/index.d.ts +1 -0
  151. package/dist/hud/elements/index.d.ts.map +1 -1
  152. package/dist/hud/elements/index.js +1 -0
  153. package/dist/hud/elements/index.js.map +1 -1
  154. package/dist/hud/elements/thinking.d.ts +7 -5
  155. package/dist/hud/elements/thinking.d.ts.map +1 -1
  156. package/dist/hud/elements/thinking.js +18 -6
  157. package/dist/hud/elements/thinking.js.map +1 -1
  158. package/dist/hud/index.js +5 -3
  159. package/dist/hud/index.js.map +1 -1
  160. package/dist/hud/omc-state.d.ts +1 -1
  161. package/dist/hud/omc-state.d.ts.map +1 -1
  162. package/dist/hud/omc-state.js +14 -31
  163. package/dist/hud/omc-state.js.map +1 -1
  164. package/dist/hud/render.d.ts +9 -0
  165. package/dist/hud/render.d.ts.map +1 -1
  166. package/dist/hud/render.js +27 -7
  167. package/dist/hud/render.js.map +1 -1
  168. package/dist/hud/state.d.ts +2 -2
  169. package/dist/hud/state.d.ts.map +1 -1
  170. package/dist/hud/state.js +4 -33
  171. package/dist/hud/state.js.map +1 -1
  172. package/dist/hud/transcript.d.ts +4 -1
  173. package/dist/hud/transcript.d.ts.map +1 -1
  174. package/dist/hud/transcript.js +4 -9
  175. package/dist/hud/transcript.js.map +1 -1
  176. package/dist/hud/types.d.ts +20 -1
  177. package/dist/hud/types.d.ts.map +1 -1
  178. package/dist/hud/types.js +38 -9
  179. package/dist/hud/types.js.map +1 -1
  180. package/dist/index.js +1 -1
  181. package/dist/index.js.map +1 -1
  182. package/dist/installer/__tests__/claude-md-merge.test.d.ts +6 -0
  183. package/dist/installer/__tests__/claude-md-merge.test.d.ts.map +1 -0
  184. package/dist/installer/__tests__/claude-md-merge.test.js +220 -0
  185. package/dist/installer/__tests__/claude-md-merge.test.js.map +1 -0
  186. package/dist/installer/__tests__/safe-installer.test.d.ts +6 -0
  187. package/dist/installer/__tests__/safe-installer.test.d.ts.map +1 -0
  188. package/dist/installer/__tests__/safe-installer.test.js +172 -0
  189. package/dist/installer/__tests__/safe-installer.test.js.map +1 -0
  190. package/dist/installer/hooks.d.ts +1 -1
  191. package/dist/installer/hooks.d.ts.map +1 -1
  192. package/dist/installer/hooks.js +4 -2
  193. package/dist/installer/hooks.js.map +1 -1
  194. package/dist/installer/index.d.ts +27 -1
  195. package/dist/installer/index.d.ts.map +1 -1
  196. package/dist/installer/index.js +209 -85
  197. package/dist/installer/index.js.map +1 -1
  198. package/dist/mcp/omc-tools-server.d.ts +1 -1
  199. package/dist/mcp/omc-tools-server.d.ts.map +1 -1
  200. package/dist/mcp/omc-tools-server.js +3 -3
  201. package/dist/mcp/omc-tools-server.js.map +1 -1
  202. package/dist/mcp/standalone-server.js +1 -1
  203. package/dist/mcp/standalone-server.js.map +1 -1
  204. package/dist/verification/tier-selector.d.ts +40 -0
  205. package/dist/verification/tier-selector.d.ts.map +1 -0
  206. package/dist/verification/tier-selector.js +95 -0
  207. package/dist/verification/tier-selector.js.map +1 -0
  208. package/dist/verification/tier-selector.test.d.ts +2 -0
  209. package/dist/verification/tier-selector.test.d.ts.map +1 -0
  210. package/dist/verification/tier-selector.test.js +282 -0
  211. package/dist/verification/tier-selector.test.js.map +1 -0
  212. package/docs/AGENTS.md +100 -0
  213. package/docs/ARCHITECTURE.md +11 -7
  214. package/docs/CLAUDE.md +89 -379
  215. package/docs/DELEGATION-ENFORCER.md +1 -2
  216. package/docs/MIGRATION.md +1 -1
  217. package/docs/REFERENCE.md +29 -9
  218. package/docs/SYNC-SYSTEM.md +0 -2
  219. package/docs/partials/agent-tiers.md +165 -0
  220. package/docs/partials/features.md +131 -0
  221. package/docs/partials/mode-hierarchy.md +120 -0
  222. package/docs/partials/mode-selection-guide.md +82 -0
  223. package/docs/partials/verification-tiers.md +107 -0
  224. package/docs/shared/agent-tiers.md +165 -0
  225. package/docs/shared/features.md +131 -0
  226. package/docs/shared/mode-hierarchy.md +120 -0
  227. package/docs/shared/mode-selection-guide.md +82 -0
  228. package/docs/shared/verification-tiers.md +107 -0
  229. package/package.json +4 -3
  230. package/scripts/compose-docs.mjs +44 -0
  231. package/scripts/keyword-detector.mjs +13 -3
  232. package/scripts/persistent-mode.mjs +78 -47
  233. package/scripts/test-mutual-exclusion.ts +3 -3
  234. package/skills/AGENTS.md +59 -44
  235. package/skills/autopilot/SKILL.md +0 -2
  236. package/skills/cancel/SKILL.md +13 -32
  237. package/skills/deep-executor/SKILL.md +50 -0
  238. package/skills/ecomode/SKILL.md +58 -104
  239. package/skills/hud/SKILL.md +3 -2
  240. package/skills/omc-setup/SKILL.md +197 -20
  241. package/skills/plan/SKILL.md +62 -0
  242. package/skills/project-session-manager/SKILL.md +87 -4
  243. package/skills/project-session-manager/lib/config.sh +54 -5
  244. package/skills/project-session-manager/lib/parse.sh +65 -11
  245. package/skills/project-session-manager/lib/providers/github.sh +52 -0
  246. package/skills/project-session-manager/lib/providers/interface.sh +76 -0
  247. package/skills/project-session-manager/lib/providers/jira.sh +79 -0
  248. package/skills/project-session-manager/lib/session.sh +49 -12
  249. package/skills/project-session-manager/lib/worktree.sh +37 -4
  250. package/skills/project-session-manager/psm.sh +116 -51
  251. package/skills/ralph/SKILL.md +48 -44
  252. package/skills/ultrawork/SKILL.md +56 -67
  253. package/templates/hooks/keyword-detector.mjs +21 -13
  254. package/templates/hooks/lib/stdin.mjs +62 -0
  255. package/templates/hooks/persistent-mode.mjs +75 -34
  256. package/templates/hooks/post-tool-use.mjs +8 -10
  257. package/templates/hooks/pre-tool-use.mjs +9 -6
  258. package/templates/hooks/session-start.mjs +7 -8
  259. package/agents/AGENTS.md +0 -144
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # PSM GitHub Provider
3
+
4
+ provider_github_available() {
5
+ command -v gh &> /dev/null
6
+ }
7
+
8
+ provider_github_detect_ref() {
9
+ local ref="$1"
10
+ # Matches github URLs or owner/repo#num patterns
11
+ [[ "$ref" =~ ^https://github\.com/ ]] || [[ "$ref" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+#[0-9]+$ ]]
12
+ }
13
+
14
+ provider_github_fetch_pr() {
15
+ local pr_number="$1"
16
+ local repo="$2"
17
+ gh pr view "$pr_number" --repo "$repo" --json number,title,author,headRefName,baseRefName,body,url 2>/dev/null
18
+ }
19
+
20
+ provider_github_fetch_issue() {
21
+ local issue_number="$1"
22
+ local repo="$2"
23
+ gh issue view "$issue_number" --repo "$repo" --json number,title,body,labels,url 2>/dev/null
24
+ }
25
+
26
+ provider_github_pr_merged() {
27
+ local pr_number="$1"
28
+ local repo="$2"
29
+ local merged
30
+ merged=$(gh pr view "$pr_number" --repo "$repo" --json merged 2>/dev/null | jq -r '.merged')
31
+ [[ "$merged" == "true" ]]
32
+ }
33
+
34
+ provider_github_issue_closed() {
35
+ local issue_number="$1"
36
+ local repo="$2"
37
+ local closed
38
+ closed=$(gh issue view "$issue_number" --repo "$repo" --json closed 2>/dev/null | jq -r '.closed')
39
+ [[ "$closed" == "true" ]]
40
+ }
41
+
42
+ provider_github_clone_url() {
43
+ local repo="$1"
44
+
45
+ # Validate owner/repo format
46
+ if [[ ! "$repo" =~ ^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$ ]]; then
47
+ echo "error|Invalid repository format: $repo" >&2
48
+ return 1
49
+ fi
50
+
51
+ echo "https://github.com/${repo}.git"
52
+ }
@@ -0,0 +1,76 @@
1
+ #!/bin/bash
2
+ # PSM Provider Interface
3
+ # Each provider implements: _available, _detect_ref, _fetch_issue, _issue_closed,
4
+ # _fetch_pr (optional), _pr_merged (optional), _clone_url
5
+
6
+ # List available providers
7
+ provider_list() {
8
+ echo "github jira"
9
+ }
10
+
11
+ # Allowlist of valid providers
12
+ readonly VALID_PROVIDERS="github jira"
13
+
14
+ # Check if a provider is available (CLI installed)
15
+ # Usage: provider_available "github"
16
+ provider_available() {
17
+ local provider="$1"
18
+
19
+ # Validate provider against allowlist
20
+ if ! echo "$VALID_PROVIDERS" | grep -qw "$provider"; then
21
+ echo "error|Invalid provider: $provider" >&2
22
+ return 1
23
+ fi
24
+
25
+ "provider_${provider}_available"
26
+ }
27
+
28
+ # Dispatch to provider function
29
+ # Usage: provider_call "github" "fetch_issue" "123" "owner/repo"
30
+ provider_call() {
31
+ local provider="$1"
32
+ local func="$2"
33
+ shift 2
34
+
35
+ # Validate provider against allowlist
36
+ if ! echo "$VALID_PROVIDERS" | grep -qw "$provider"; then
37
+ echo "error|Invalid provider: $provider" >&2
38
+ return 1
39
+ fi
40
+
41
+ # Validate function name (alphanumeric and underscore only)
42
+ if [[ ! "$func" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
43
+ echo "error|Invalid function name: $func" >&2
44
+ return 1
45
+ fi
46
+
47
+ "provider_${provider}_${func}" "$@"
48
+ }
49
+
50
+ # Detect provider from reference (with config validation)
51
+ # Usage: provider_detect_from_ref "PROJ-123"
52
+ # Returns: provider name or empty
53
+ provider_detect_from_ref() {
54
+ local ref="$1"
55
+
56
+ # Check Jira pattern first (config-validated)
57
+ if psm_detect_jira_key "$ref" >/dev/null 2>&1; then
58
+ echo "jira"
59
+ return 0
60
+ fi
61
+
62
+ # GitHub URL patterns
63
+ if [[ "$ref" =~ ^https://github\.com/ ]]; then
64
+ echo "github"
65
+ return 0
66
+ fi
67
+
68
+ # owner/repo#num pattern -> GitHub
69
+ if [[ "$ref" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+#[0-9]+$ ]]; then
70
+ echo "github"
71
+ return 0
72
+ fi
73
+
74
+ # Default
75
+ echo "github"
76
+ }
@@ -0,0 +1,79 @@
1
+ #!/bin/bash
2
+ # PSM Jira Provider
3
+ # Uses `jira` CLI (https://github.com/ankitpokhrel/jira-cli)
4
+
5
+ provider_jira_available() {
6
+ command -v jira &> /dev/null
7
+ }
8
+
9
+ provider_jira_detect_ref() {
10
+ local ref="$1"
11
+ # Config-validated detection only
12
+ psm_detect_jira_key "$ref" >/dev/null 2>&1
13
+ }
14
+
15
+ provider_jira_fetch_issue() {
16
+ local issue_key="$1" # e.g., "PROJ-123"
17
+ # Note: second arg (repo) is ignored for Jira
18
+ jira issue view "$issue_key" --output json 2>/dev/null
19
+ }
20
+
21
+ provider_jira_issue_closed() {
22
+ local issue_key="$1"
23
+ local status_category
24
+ status_category=$(jira issue view "$issue_key" --output json 2>/dev/null | jq -r '.fields.status.statusCategory.key')
25
+ # Jira status categories: "new", "indeterminate", "done"
26
+ [[ "$status_category" == "done" ]]
27
+ }
28
+
29
+ # Jira has no PRs - return error
30
+ provider_jira_fetch_pr() {
31
+ echo '{"error": "Jira does not support pull requests"}' >&2
32
+ return 1
33
+ }
34
+
35
+ provider_jira_pr_merged() {
36
+ return 1 # Always false - Jira has no PRs
37
+ }
38
+
39
+ provider_jira_clone_url() {
40
+ local alias="$1"
41
+ # For Jira, we need to get clone_url from config
42
+ # First try explicit clone_url, then fall back to repo as GitHub
43
+ local clone_url
44
+ clone_url=$(psm_get_project_clone_url "$alias")
45
+ if [[ -n "$clone_url" ]]; then
46
+ echo "$clone_url"
47
+ return 0
48
+ fi
49
+
50
+ local repo
51
+ repo=$(psm_get_project_repo "$alias")
52
+ if [[ -n "$repo" ]]; then
53
+ echo "https://github.com/${repo}.git"
54
+ return 0
55
+ fi
56
+
57
+ echo "error: No clone_url or repo configured for alias '$alias'" >&2
58
+ return 1
59
+ }
60
+
61
+ # Parse Jira reference into components
62
+ # Input: "PROJ-123" or "mywork#123"
63
+ # Output: Extended format for session creation
64
+ provider_jira_parse_ref() {
65
+ local ref="$1"
66
+ local jira_info
67
+
68
+ # Try direct PROJ-123 pattern
69
+ if jira_info=$(psm_detect_jira_key "$ref"); then
70
+ IFS='|' read -r alias project_key issue_number <<< "$jira_info"
71
+ local project_info
72
+ project_info=$(psm_get_project "$alias")
73
+ IFS='|' read -r repo local_path base <<< "$project_info"
74
+ echo "issue|${alias}|${repo}|${issue_number}|${local_path}|${base}|jira|${project_key}-${issue_number}"
75
+ return 0
76
+ fi
77
+
78
+ return 1
79
+ }
@@ -1,9 +1,24 @@
1
1
  #!/bin/bash
2
2
  # PSM Session Registry Management
3
3
 
4
- # Add session to registry
5
- # Usage: psm_add_session <id> <type> <project> <ref> <branch> <base> <tmux> <worktree> <source_repo> <metadata_json>
6
- psm_add_session() {
4
+ # Lock file for atomic registry operations
5
+ PSM_LOCK_FILE="${PSM_DATA_DIR:-.psm}/.psm-lock"
6
+
7
+ # Wrapper for atomic operations with file locking
8
+ # Usage: psm_with_lock <command> [args...]
9
+ psm_with_lock() {
10
+ local timeout="${PSM_LOCK_TIMEOUT:-5}"
11
+ (
12
+ flock -w "$timeout" 200 || {
13
+ echo "error|Failed to acquire lock after ${timeout}s" >&2
14
+ return 1
15
+ }
16
+ "$@"
17
+ ) 200>"$PSM_LOCK_FILE"
18
+ }
19
+
20
+ # Internal: Add session to registry (must be called via psm_with_lock)
21
+ _psm_add_session_impl() {
7
22
  local id="$1"
8
23
  local type="$2"
9
24
  local project="$3"
@@ -14,6 +29,8 @@ psm_add_session() {
14
29
  local worktree="$8"
15
30
  local source_repo="$9"
16
31
  local metadata="${10:-{}}"
32
+ local provider="${11:-github}"
33
+ local provider_ref="${12:-}"
17
34
 
18
35
  local now=$(date -Iseconds)
19
36
 
@@ -28,6 +45,8 @@ psm_add_session() {
28
45
  --arg worktree "$worktree" \
29
46
  --arg source "$source_repo" \
30
47
  --arg now "$now" \
48
+ --arg provider "$provider" \
49
+ --arg provider_ref "$provider_ref" \
31
50
  --argjson meta "$metadata" \
32
51
  '.sessions[$id] = {
33
52
  "id": $id,
@@ -42,21 +61,28 @@ psm_add_session() {
42
61
  "created_at": $now,
43
62
  "last_accessed": $now,
44
63
  "state": "active",
64
+ "provider": $provider,
65
+ "provider_ref": $provider_ref,
45
66
  "metadata": $meta
46
67
  } | .stats.total_created += 1' \
47
68
  "$PSM_SESSIONS" > "$tmp" && mv "$tmp" "$PSM_SESSIONS"
48
69
  }
49
70
 
71
+ # Add session to registry (with file locking)
72
+ # Usage: psm_add_session <id> <type> <project> <ref> <branch> <base> <tmux> <worktree> <source_repo> <metadata_json> [provider] [provider_ref]
73
+ psm_add_session() {
74
+ psm_with_lock _psm_add_session_impl "$@"
75
+ }
76
+
50
77
  # Get session by ID
51
78
  # Usage: psm_get_session <id>
52
79
  psm_get_session() {
53
80
  local id="$1"
54
- jq -r ".sessions[\"$id\"] // empty" "$PSM_SESSIONS"
81
+ jq -r --arg i "$id" '.sessions[$i] // empty' "$PSM_SESSIONS"
55
82
  }
56
83
 
57
- # Update session state
58
- # Usage: psm_update_session_state <id> <state>
59
- psm_update_session_state() {
84
+ # Internal: Update session state (must be called via psm_with_lock)
85
+ _psm_update_session_state_impl() {
60
86
  local id="$1"
61
87
  local state="$2"
62
88
  local now=$(date -Iseconds)
@@ -69,9 +95,14 @@ psm_update_session_state() {
69
95
  "$PSM_SESSIONS" > "$tmp" && mv "$tmp" "$PSM_SESSIONS"
70
96
  }
71
97
 
72
- # Remove session from registry
73
- # Usage: psm_remove_session <id>
74
- psm_remove_session() {
98
+ # Update session state (with file locking)
99
+ # Usage: psm_update_session_state <id> <state>
100
+ psm_update_session_state() {
101
+ psm_with_lock _psm_update_session_state_impl "$@"
102
+ }
103
+
104
+ # Internal: Remove session from registry (must be called via psm_with_lock)
105
+ _psm_remove_session_impl() {
75
106
  local id="$1"
76
107
 
77
108
  local tmp=$(mktemp)
@@ -80,13 +111,19 @@ psm_remove_session() {
80
111
  "$PSM_SESSIONS" > "$tmp" && mv "$tmp" "$PSM_SESSIONS"
81
112
  }
82
113
 
114
+ # Remove session from registry (with file locking)
115
+ # Usage: psm_remove_session <id>
116
+ psm_remove_session() {
117
+ psm_with_lock _psm_remove_session_impl "$@"
118
+ }
119
+
83
120
  # List all sessions
84
121
  # Usage: psm_list_sessions [project]
85
122
  psm_list_sessions() {
86
123
  local project="$1"
87
124
 
88
125
  if [[ -n "$project" ]]; then
89
- jq -r ".sessions | to_entries[] | select(.value.project == \"$project\") | .value | \"\(.id)|\(.type)|\(.state)|\(.worktree)\"" "$PSM_SESSIONS"
126
+ jq -r --arg p "$project" '.sessions | to_entries[] | select(.value.project == $p) | .value | "\(.id)|\(.type)|\(.state)|\(.worktree)"' "$PSM_SESSIONS"
90
127
  else
91
128
  jq -r '.sessions | to_entries[] | .value | "\(.id)|\(.type)|\(.state)|\(.worktree)"' "$PSM_SESSIONS"
92
129
  fi
@@ -95,7 +132,7 @@ psm_list_sessions() {
95
132
  # Get sessions by state
96
133
  psm_get_sessions_by_state() {
97
134
  local state="$1"
98
- jq -r ".sessions | to_entries[] | select(.value.state == \"$state\") | .value.id" "$PSM_SESSIONS"
135
+ jq -r --arg s "$state" '.sessions | to_entries[] | select(.value.state == $s) | .value.id' "$PSM_SESSIONS"
99
136
  }
100
137
 
101
138
  # Get session count
@@ -1,6 +1,32 @@
1
1
  #!/bin/bash
2
2
  # PSM Worktree Management
3
3
 
4
+ # Validate worktree path is under PSM worktree root before deletion
5
+ # Returns 0 if valid, 1 if invalid
6
+ # Usage: validate_worktree_path <path>
7
+ validate_worktree_path() {
8
+ local path="$1"
9
+ local worktree_root
10
+ worktree_root=$(psm_get_worktree_root 2>/dev/null) || return 1
11
+
12
+ # Path must exist and be a directory
13
+ if [[ ! -d "$path" ]]; then
14
+ return 1
15
+ fi
16
+
17
+ # Resolve to absolute paths for comparison
18
+ local abs_path abs_root
19
+ abs_path=$(cd "$path" 2>/dev/null && pwd) || return 1
20
+ abs_root=$(cd "$worktree_root" 2>/dev/null && pwd) || return 1
21
+
22
+ # Check path is under root and doesn't contain ..
23
+ if [[ "$abs_path" != "$abs_root"/* ]] || [[ "$path" == *".."* ]]; then
24
+ echo "error|Invalid worktree path: not under PSM root" >&2
25
+ return 1
26
+ fi
27
+ return 0
28
+ }
29
+
4
30
  # Create a worktree for PR review
5
31
  # Usage: psm_create_pr_worktree <local_repo> <alias> <pr_number> <pr_branch>
6
32
  psm_create_pr_worktree() {
@@ -146,10 +172,17 @@ psm_remove_worktree() {
146
172
  fi
147
173
 
148
174
  cd "$local_repo" || return 1
149
- git worktree remove "$worktree_path" --force 2>/dev/null || {
150
- # Force remove the directory if git worktree remove fails
151
- rm -rf "$worktree_path"
152
- }
175
+
176
+ # Validate path is under PSM worktree root before any deletion
177
+ if validate_worktree_path "$worktree_path"; then
178
+ git worktree remove "$worktree_path" --force 2>/dev/null || {
179
+ # Force remove the directory if git worktree remove fails
180
+ rm -rf "$worktree_path"
181
+ }
182
+ else
183
+ echo "error|Refusing to delete path outside worktree root: $worktree_path" >&2
184
+ return 1
185
+ fi
153
186
 
154
187
  echo "removed|$worktree_path"
155
188
  return 0
@@ -13,6 +13,11 @@ source "$SCRIPT_DIR/lib/worktree.sh"
13
13
  source "$SCRIPT_DIR/lib/tmux.sh"
14
14
  source "$SCRIPT_DIR/lib/session.sh"
15
15
 
16
+ # Source provider files
17
+ source "$SCRIPT_DIR/lib/providers/interface.sh"
18
+ source "$SCRIPT_DIR/lib/providers/github.sh"
19
+ source "$SCRIPT_DIR/lib/providers/jira.sh"
20
+
16
21
  # Colors for output
17
22
  RED='\033[0;31m'
18
23
  GREEN='\033[0;32m'
@@ -38,15 +43,14 @@ check_dependencies() {
38
43
  missing+=("jq")
39
44
  fi
40
45
 
41
- if ! command -v gh &> /dev/null; then
42
- missing+=("gh (GitHub CLI)")
43
- fi
46
+ # Note: gh and jira are checked per-operation, not globally
47
+ # This allows users without gh to still use Jira, and vice versa
44
48
 
45
49
  if [[ ${#missing[@]} -gt 0 ]]; then
46
50
  log_error "Missing required dependencies: ${missing[*]}"
47
51
  log_info "Install with:"
48
- log_info " Ubuntu/Debian: sudo apt install git jq gh"
49
- log_info " macOS: brew install git jq gh"
52
+ log_info " Ubuntu/Debian: sudo apt install git jq"
53
+ log_info " macOS: brew install git jq"
50
54
  exit 1
51
55
  fi
52
56
 
@@ -106,7 +110,21 @@ cmd_review() {
106
110
  return 1
107
111
  fi
108
112
 
109
- IFS='|' read -r type alias repo pr_number local_path base <<< "$parsed"
113
+ IFS='|' read -r type alias repo pr_number local_path base provider provider_ref <<< "$parsed"
114
+
115
+ # Provider guard: Jira doesn't have PRs
116
+ if [[ "$provider" == "jira" ]]; then
117
+ log_error "Jira issues cannot be 'reviewed' - Jira has no PR concept."
118
+ log_info "Use 'psm fix $ref' to work on a Jira issue instead."
119
+ log_info "Jira integration supports: fix, feature"
120
+ return 1
121
+ fi
122
+
123
+ # Check GitHub CLI availability
124
+ if ! provider_github_available; then
125
+ log_error "GitHub CLI (gh) not found. Install: brew install gh"
126
+ return 1
127
+ fi
110
128
 
111
129
  if [[ -z "$repo" ]]; then
112
130
  log_error "Could not determine repository"
@@ -117,7 +135,7 @@ cmd_review() {
117
135
 
118
136
  # Fetch PR info
119
137
  local pr_info
120
- pr_info=$(gh pr view "$pr_number" --repo "$repo" --json number,title,author,headRefName,baseRefName,body,url 2>/dev/null) || {
138
+ pr_info=$(provider_call "github" fetch_pr "$pr_number" "$repo") || {
121
139
  log_error "Failed to fetch PR #${pr_number}. Check if the PR exists and you have access."
122
140
  return 1
123
141
  }
@@ -143,7 +161,9 @@ cmd_review() {
143
161
  local_path="${HOME}/Workspace/$(basename "$repo")"
144
162
  if [[ ! -d "$local_path" ]]; then
145
163
  log_info "Cloning repository to $local_path..."
146
- git clone "https://github.com/${repo}.git" "$local_path" || {
164
+ local clone_url
165
+ clone_url=$(provider_call "github" clone_url "$repo")
166
+ git clone "$clone_url" "$local_path" || {
147
167
  log_error "Failed to clone repository"
148
168
  return 1
149
169
  }
@@ -198,18 +218,16 @@ cmd_review() {
198
218
  fi
199
219
 
200
220
  # Create session metadata
201
- local metadata=$(cat << EOF
202
- {
203
- "pr_number": $pr_number,
204
- "pr_title": $(echo "$pr_title" | jq -R .),
205
- "pr_author": "$pr_author",
206
- "pr_url": "$pr_url"
207
- }
208
- EOF
209
- )
221
+ local metadata
222
+ metadata=$(jq -n \
223
+ --argjson pr_number "$pr_number" \
224
+ --arg pr_title "$pr_title" \
225
+ --arg pr_author "$pr_author" \
226
+ --arg pr_url "$pr_url" \
227
+ '{pr_number: $pr_number, pr_title: $pr_title, pr_author: $pr_author, pr_url: $pr_url}')
210
228
 
211
229
  # Add to registry
212
- psm_add_session "$session_id" "review" "$alias" "pr-${pr_number}" "$head_branch" "$base_branch" "$session_name" "$worktree_path" "$local_path" "$metadata"
230
+ psm_add_session "$session_id" "review" "$alias" "pr-${pr_number}" "$head_branch" "$base_branch" "$session_name" "$worktree_path" "$local_path" "$metadata" "github" "${repo}#${pr_number}"
213
231
 
214
232
  # Output summary
215
233
  echo ""
@@ -242,24 +260,45 @@ cmd_fix() {
242
260
  return 1
243
261
  fi
244
262
 
245
- IFS='|' read -r type alias repo issue_number local_path base <<< "$parsed"
263
+ IFS='|' read -r type alias repo issue_number local_path base provider provider_ref <<< "$parsed"
246
264
 
247
- if [[ -z "$repo" ]]; then
265
+ # Check provider CLI availability
266
+ if [[ "$provider" == "jira" ]]; then
267
+ if ! provider_jira_available; then
268
+ log_error "Jira CLI not found. Install: brew install ankitpokhrel/jira-cli/jira-cli"
269
+ return 1
270
+ fi
271
+ else
272
+ if ! provider_github_available; then
273
+ log_error "GitHub CLI (gh) not found. Install: brew install gh"
274
+ return 1
275
+ fi
276
+ fi
277
+
278
+ if [[ -z "$repo" && "$provider" != "jira" ]]; then
248
279
  log_error "Could not determine repository"
249
280
  return 1
250
281
  fi
251
282
 
252
- log_info "Fetching issue #${issue_number} from ${repo}..."
283
+ log_info "Fetching issue #${issue_number}..."
253
284
 
254
285
  # Fetch issue info
255
286
  local issue_info
256
- issue_info=$(gh issue view "$issue_number" --repo "$repo" --json number,title,body,labels,url 2>/dev/null) || {
257
- log_error "Failed to fetch issue #${issue_number}"
258
- return 1
259
- }
260
-
261
- local issue_title=$(echo "$issue_info" | jq -r '.title')
262
- local issue_url=$(echo "$issue_info" | jq -r '.url')
287
+ if [[ "$provider" == "jira" ]]; then
288
+ issue_info=$(provider_call "jira" fetch_issue "$provider_ref") || {
289
+ log_error "Failed to fetch Jira issue ${provider_ref}"
290
+ return 1
291
+ }
292
+ local issue_title=$(echo "$issue_info" | jq -r '.fields.summary')
293
+ local issue_url=$(echo "$issue_info" | jq -r '.self // empty')
294
+ else
295
+ issue_info=$(provider_call "github" fetch_issue "$issue_number" "$repo") || {
296
+ log_error "Failed to fetch issue #${issue_number}"
297
+ return 1
298
+ }
299
+ local issue_title=$(echo "$issue_info" | jq -r '.title')
300
+ local issue_url=$(echo "$issue_info" | jq -r '.url')
301
+ fi
263
302
  local slug=$(psm_slugify "$issue_title" 20)
264
303
 
265
304
  log_info "Issue: #${issue_number} - ${issue_title}"
@@ -271,10 +310,19 @@ cmd_fix() {
271
310
 
272
311
  # Determine local path
273
312
  if [[ -z "$local_path" || ! -d "$local_path" ]]; then
274
- local_path="${HOME}/Workspace/$(basename "$repo")"
313
+ local_path="${HOME}/Workspace/$(basename "${repo:-$alias}")"
275
314
  if [[ ! -d "$local_path" ]]; then
276
315
  log_info "Cloning repository..."
277
- git clone "https://github.com/${repo}.git" "$local_path" || return 1
316
+ local clone_url
317
+ if [[ "$provider" == "jira" ]]; then
318
+ clone_url=$(provider_call "jira" clone_url "$alias") || {
319
+ log_error "Failed to get clone URL for '$alias'. Configure 'repo' or 'clone_url' in projects.json"
320
+ return 1
321
+ }
322
+ else
323
+ clone_url=$(provider_call "github" clone_url "$repo")
324
+ fi
325
+ git clone "$clone_url" "$local_path" || return 1
278
326
  fi
279
327
  fi
280
328
 
@@ -309,16 +357,14 @@ cmd_fix() {
309
357
  fi
310
358
 
311
359
  # Create metadata
312
- local metadata=$(cat << EOF
313
- {
314
- "issue_number": $issue_number,
315
- "issue_title": $(echo "$issue_title" | jq -R .),
316
- "issue_url": "$issue_url"
317
- }
318
- EOF
319
- )
360
+ local metadata
361
+ metadata=$(jq -n \
362
+ --argjson issue_number "$issue_number" \
363
+ --arg issue_title "$issue_title" \
364
+ --arg issue_url "$issue_url" \
365
+ '{issue_number: $issue_number, issue_title: $issue_title, issue_url: $issue_url}')
320
366
 
321
- psm_add_session "$session_id" "fix" "$alias" "issue-${issue_number}" "$branch_name" "$base" "$session_name" "$worktree_path" "$local_path" "$metadata"
367
+ psm_add_session "$session_id" "fix" "$alias" "issue-${issue_number}" "$branch_name" "$base" "$session_name" "$worktree_path" "$local_path" "$metadata" "$provider" "$provider_ref"
322
368
 
323
369
  echo ""
324
370
  log_success "Session ready!"
@@ -473,15 +519,20 @@ cmd_cleanup() {
473
519
 
474
520
  local cleaned=0
475
521
 
476
- # Check PR sessions (use process substitution to avoid subshell)
522
+ # Check PR sessions (GitHub only)
477
523
  while IFS='|' read -r id pr_number project; do
478
524
  if [[ -z "$id" ]]; then continue; fi
479
525
 
526
+ local session_json=$(psm_get_session "$id")
527
+ local provider=$(echo "$session_json" | jq -r '.provider // "github"')
528
+
529
+ # Only GitHub has PRs
530
+ if [[ "$provider" != "github" ]]; then continue; fi
531
+
480
532
  local repo=$(psm_get_project "$project" | cut -d'|' -f1)
481
533
 
482
534
  if [[ -n "$repo" && -n "$pr_number" ]]; then
483
- local pr_state=$(gh pr view "$pr_number" --repo "$repo" --json merged 2>/dev/null | jq -r '.merged')
484
- if [[ "$pr_state" == "true" ]]; then
535
+ if provider_github_available && provider_call "github" pr_merged "$pr_number" "$repo"; then
485
536
  log_info "PR #${pr_number} is merged - cleaning up $id"
486
537
  cmd_kill "$id"
487
538
  ((cleaned++))
@@ -489,18 +540,32 @@ cmd_cleanup() {
489
540
  fi
490
541
  done < <(psm_get_review_sessions)
491
542
 
492
- # Check issue sessions (use process substitution to avoid subshell)
543
+ # Check issue sessions (GitHub and Jira)
493
544
  while IFS='|' read -r id issue_number project; do
494
545
  if [[ -z "$id" ]]; then continue; fi
495
546
 
496
- local repo=$(psm_get_project "$project" | cut -d'|' -f1)
497
-
498
- if [[ -n "$repo" && -n "$issue_number" ]]; then
499
- local issue_state=$(gh issue view "$issue_number" --repo "$repo" --json closed 2>/dev/null | jq -r '.closed')
500
- if [[ "$issue_state" == "true" ]]; then
501
- log_info "Issue #${issue_number} is closed - cleaning up $id"
502
- cmd_kill "$id"
503
- ((cleaned++))
547
+ local session_json=$(psm_get_session "$id")
548
+ local provider=$(echo "$session_json" | jq -r '.provider // "github"')
549
+ local provider_ref=$(echo "$session_json" | jq -r '.provider_ref // empty')
550
+
551
+ if [[ "$provider" == "jira" ]]; then
552
+ # Jira cleanup
553
+ if provider_jira_available && [[ -n "$provider_ref" ]]; then
554
+ if provider_call "jira" issue_closed "$provider_ref"; then
555
+ log_info "Jira issue ${provider_ref} is done - cleaning up $id"
556
+ cmd_kill "$id"
557
+ ((cleaned++))
558
+ fi
559
+ fi
560
+ else
561
+ # GitHub cleanup
562
+ local repo=$(psm_get_project "$project" | cut -d'|' -f1)
563
+ if provider_github_available && [[ -n "$repo" && -n "$issue_number" ]]; then
564
+ if provider_call "github" issue_closed "$issue_number" "$repo"; then
565
+ log_info "Issue #${issue_number} is closed - cleaning up $id"
566
+ cmd_kill "$id"
567
+ ((cleaned++))
568
+ fi
504
569
  fi
505
570
  fi
506
571
  done < <(psm_get_fix_sessions)