@codyswann/lisa 4.81.3 → 4.81.5

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 (198) hide show
  1. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  2. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  3. package/dist/core/upstream-evidence-manifest.js +52 -14
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/package.json +4 -4
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +15 -8
  10. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +4 -3
  11. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  12. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  13. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +36 -9
  16. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +22 -36
  18. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  21. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  22. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token.mjs +98 -28
  23. package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +57 -22
  24. package/plugins/lisa/rules/reference/config-resolution.md +1 -0
  25. package/plugins/lisa/scripts/ready-queue-order.mjs +34 -14
  26. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  27. package/plugins/lisa/skills/lisa-session-manager/SKILL.md +15 -8
  28. package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +4 -3
  29. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  30. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  31. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  32. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  33. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +36 -9
  34. package/plugins/lisa/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  35. package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +22 -36
  36. package/plugins/lisa/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  37. package/plugins/lisa/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  38. package/plugins/lisa/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  39. package/plugins/lisa/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  40. package/plugins/lisa/skills/lisa-session-manager/scripts/token.mjs +98 -28
  41. package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +57 -22
  42. package/plugins/lisa-agy/plugin.json +1 -1
  43. package/plugins/lisa-agy/scripts/ready-queue-order.mjs +34 -14
  44. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  45. package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +15 -8
  46. package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +4 -3
  47. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  48. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  49. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  50. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  51. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +36 -9
  52. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  53. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +22 -36
  54. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  55. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  56. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  57. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  58. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token.mjs +98 -28
  59. package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +57 -22
  60. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  63. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  67. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -0
  69. package/plugins/lisa-copilot/scripts/ready-queue-order.mjs +34 -14
  70. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  71. package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +15 -8
  72. package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +4 -3
  73. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  74. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  75. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  76. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  77. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +36 -9
  78. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  79. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +22 -36
  80. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  81. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  82. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  83. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  84. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token.mjs +98 -28
  85. package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +57 -22
  86. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -0
  88. package/plugins/lisa-cursor/scripts/ready-queue-order.mjs +34 -14
  89. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  90. package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +15 -8
  91. package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +4 -3
  92. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  93. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  94. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  95. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  96. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +36 -9
  97. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  98. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +22 -36
  99. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  100. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  101. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  102. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  103. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token.mjs +98 -28
  104. package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +57 -22
  105. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  107. package/plugins/lisa-devin/rules/reference/config-resolution.md +1 -0
  108. package/plugins/lisa-devin/scripts/ready-queue-order.mjs +34 -14
  109. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  110. package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +15 -8
  111. package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +4 -3
  112. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  113. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  114. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  115. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  116. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +36 -9
  117. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  118. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +22 -36
  119. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  120. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  121. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  122. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  123. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token.mjs +98 -28
  124. package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +57 -22
  125. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  127. package/plugins/lisa-expo-agy/plugin.json +1 -1
  128. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  130. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  132. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  134. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  135. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  136. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  137. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  139. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  140. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  141. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  142. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  143. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  144. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  145. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  146. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  147. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  148. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  149. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  150. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  151. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  153. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  154. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  155. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  156. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  158. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  159. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  160. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  161. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  162. package/plugins/lisa-rails-agy/plugin.json +1 -1
  163. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  164. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  165. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  166. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  167. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  168. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  169. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  170. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  171. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  172. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  173. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  174. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  175. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  176. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  177. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  178. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  179. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  180. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  181. package/plugins/src/base/rules/reference/config-resolution.md +1 -0
  182. package/plugins/src/base/scripts/ready-queue-order.mjs +34 -14
  183. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
  184. package/plugins/src/base/skills/lisa-session-manager/SKILL.md +15 -8
  185. package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +4 -3
  186. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
  187. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
  188. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
  189. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
  190. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +36 -9
  191. package/plugins/src/base/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
  192. package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +22 -36
  193. package/plugins/src/base/skills/lisa-session-manager/scripts/launch.mjs +1 -0
  194. package/plugins/src/base/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
  195. package/plugins/src/base/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
  196. package/plugins/src/base/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
  197. package/plugins/src/base/skills/lisa-session-manager/scripts/token.mjs +98 -28
  198. package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +57 -22
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Which children of a multi-repo project's repos root are real, routable
3
+ * repos, and a first draft of the hints the manager routes on.
4
+ *
5
+ * A host's `projects/` dir collects more than one checkout per repo: linked
6
+ * worktrees agents made for a ticket (`.git` is a FILE pointing back at the
7
+ * main clone), and whole second clones of the same remote (`api-se1234`).
8
+ * Each carries the repo's own `.lisa.config.json`, so counting config files
9
+ * alone proposed 110 routed repos for a project with 4 (CodySwannGT/lisa#4465)
10
+ * and the operator had to prune them by hand. A worker routed into a stale
11
+ * ticket worktree builds on the wrong branch, so the detector keeps one
12
+ * checkout per remote: the one named after the repo when there is one.
13
+ *
14
+ * Every filesystem call goes through an injectable seam.
15
+ * @module session-manager/repo-detect
16
+ */
17
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ /** One path segment that cannot climb out of, or hide inside, its parent. */
21
+ const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
22
+
23
+ /** `url = <value>` inside the `[remote "origin"]` section of a git config. */
24
+ const ORIGIN_SECTION = /^\s*\[remote\s+"origin"\]\s*$/;
25
+ const ANY_SECTION = /^\s*\[/;
26
+ const URL_LINE = /^\s*url\s*=\s*(\S+)\s*$/;
27
+
28
+ /**
29
+ * Well-known dependencies and the hint each one implies. Order is the order
30
+ * hints are listed in. A key ending in `/` matches every package under that
31
+ * scope.
32
+ */
33
+ const DEPENDENCY_HINTS = Object.freeze([
34
+ ["@nestjs/", "NestJS"],
35
+ ["typeorm", "TypeORM"],
36
+ ["prisma", "Prisma"],
37
+ ["@prisma/client", "Prisma"],
38
+ ["@apollo/server", "GraphQL API"],
39
+ ["graphql", "GraphQL"],
40
+ ["express", "Express"],
41
+ ["fastify", "Fastify"],
42
+ ["serverless", "Serverless"],
43
+ ["expo", "Expo"],
44
+ ["react-native", "React Native"],
45
+ ["next", "Next.js"],
46
+ ["react", "React"],
47
+ ["vue", "Vue"],
48
+ ["@angular/core", "Angular"],
49
+ ["@apollo/client", "Apollo client"],
50
+ ["aws-cdk-lib", "AWS CDK"],
51
+ ["@pulumi/pulumi", "Pulumi"],
52
+ ["@langchain/langgraph", "LangGraph"],
53
+ ["@langchain/core", "LangChain"],
54
+ ["@anthropic-ai/sdk", "Claude API"],
55
+ ["@aws-sdk/client-bedrock-runtime", "Bedrock"],
56
+ ["phaser", "Phaser"],
57
+ ["@playwright/test", "Playwright"],
58
+ ]);
59
+
60
+ /** The default filesystem seam. */
61
+ const NODE_FS = { existsSync, readdirSync, readFileSync, statSync };
62
+
63
+ /**
64
+ * Reduce a git remote URL to `host/owner/repo` so the SSH and HTTPS spellings
65
+ * of one remote compare equal. Returns null for anything unparseable.
66
+ * @param {string} url - Remote URL as written in `.git/config`
67
+ * @returns {string | null} Normalized identity, lowercased
68
+ */
69
+ export function normalizeRemote(url) {
70
+ const trimmed = String(url ?? "").trim();
71
+ const scp = /^[^@/]+@([^:/]+):(.+)$/.exec(trimmed);
72
+ const parsed = scp
73
+ ? null
74
+ : /^[a-z+]+:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(trimmed);
75
+ const match = scp ?? parsed;
76
+ if (!match) return null;
77
+ const repoPath = match[2].replace(/\/+$/, "").replace(/\.git$/, "");
78
+ return repoPath ? `${match[1]}/${repoPath}`.toLowerCase() : null;
79
+ }
80
+
81
+ /**
82
+ * The `origin` URL from a clone's `.git/config`, or null.
83
+ * @param {string} dir - Child directory
84
+ * @param {typeof NODE_FS} fs - Filesystem seam
85
+ * @returns {string | null} Raw URL
86
+ */
87
+ function readOrigin(dir, fs) {
88
+ let text;
89
+ try {
90
+ text = fs.readFileSync(join(dir, ".git", "config"), "utf8");
91
+ } catch {
92
+ return null;
93
+ }
94
+ let inOrigin = false;
95
+ for (const line of text.split(/\r?\n/)) {
96
+ if (ANY_SECTION.test(line)) inOrigin = ORIGIN_SECTION.test(line);
97
+ const url = inOrigin ? URL_LINE.exec(line) : null;
98
+ if (url) return url[1];
99
+ }
100
+ return null;
101
+ }
102
+
103
+ /** The `gitdir:` line of a linked worktree points into `.git/worktrees/`. */
104
+ const WORKTREE_GITDIR = /^gitdir:\s*\S*[\\/]worktrees[\\/]/m;
105
+
106
+ /**
107
+ * Whether `<dir>` is a linked worktree: its `.git` is a file whose `gitdir:`
108
+ * points into another clone's `worktrees/`. A `.git` file that points
109
+ * anywhere else (a submodule, `--separate-git-dir`) may be the only checkout
110
+ * of its repo, so it is kept.
111
+ * @param {string} dir - Child directory
112
+ * @param {typeof NODE_FS} fs - Filesystem seam
113
+ * @returns {boolean} True for a linked worktree
114
+ */
115
+ function isLinkedWorktree(dir, fs) {
116
+ const marker = join(dir, ".git");
117
+ try {
118
+ if (!fs.statSync(marker).isFile()) return false;
119
+ return WORKTREE_GITDIR.test(fs.readFileSync(marker, "utf8"));
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Keep one child per remote: the one whose name is the remote's repo name,
127
+ * else the lexically first. Children with no readable origin are each kept.
128
+ * @param {Array<{ name: string, remote: string | null }>} children - Sorted
129
+ * @returns {string[]} Kept names, sorted
130
+ */
131
+ function onePerRemote(children) {
132
+ const groups = new Map();
133
+ const kept = [];
134
+ for (const child of children) {
135
+ if (child.remote === null) kept.push(child.name);
136
+ else groups.set(child.remote, [...(groups.get(child.remote) ?? []), child]);
137
+ }
138
+ for (const [remote, members] of groups) {
139
+ const repoName = remote.split("/").at(-1);
140
+ const named = members.find(
141
+ member => member.name.toLowerCase() === repoName
142
+ );
143
+ kept.push((named ?? members[0]).name);
144
+ }
145
+ return kept.sort();
146
+ }
147
+
148
+ /**
149
+ * List the routable child repos of a multi-repo project.
150
+ *
151
+ * A child counts only when it carries its own `.lisa.config.json` (workers
152
+ * run `/lisa:implement` inside it, which needs that file), is not a linked
153
+ * worktree, and is the chosen checkout of its remote.
154
+ * @param {string} root - Project root
155
+ * @param {{ reposRoot?: string, fs?: Partial<typeof NODE_FS> }} [options] -
156
+ * Repos root (default `projects`) and filesystem seam
157
+ * @returns {string[]} Child directory names, sorted
158
+ */
159
+ export function detectRepos(root, options = {}) {
160
+ const fs = { ...NODE_FS, ...options.fs };
161
+ const base = join(root, options.reposRoot ?? "projects");
162
+ if (!fs.existsSync(base)) return [];
163
+ const children = fs
164
+ .readdirSync(base, { withFileTypes: true })
165
+ .filter(entry => entry.isDirectory() && SAFE_SEGMENT.test(entry.name))
166
+ .map(entry => ({ name: entry.name, dir: join(base, entry.name) }))
167
+ .filter(child => fs.existsSync(join(child.dir, ".lisa.config.json")))
168
+ .filter(child => !isLinkedWorktree(child.dir, fs))
169
+ .map(child => ({
170
+ name: child.name,
171
+ remote: normalizeRemote(readOrigin(child.dir, fs) ?? ""),
172
+ }))
173
+ .sort((a, b) => (a.name < b.name ? -1 : 1));
174
+ return onePerRemote(children);
175
+ }
176
+
177
+ /**
178
+ * Draft routing hints for one child: its name, then a hint per well-known
179
+ * dependency in its `package.json`. Only a draft; setup asks the operator to
180
+ * review it. A child with no `package.json`, or none of the known
181
+ * dependencies, keeps just its name.
182
+ * @param {string} dir - Child directory
183
+ * @param {string} name - Child name (always the first hint)
184
+ * @param {{ fs?: Partial<typeof NODE_FS> }} [options] - Filesystem seam
185
+ * @returns {string[]} Hints, de-duplicated, name first
186
+ */
187
+ export function draftHints(dir, name, options = {}) {
188
+ const fs = { ...NODE_FS, ...options.fs };
189
+ let manifest;
190
+ try {
191
+ manifest = JSON.parse(fs.readFileSync(join(dir, "package.json"), "utf8"));
192
+ } catch {
193
+ return [name];
194
+ }
195
+ const deps = Object.keys({
196
+ ...manifest?.dependencies,
197
+ ...manifest?.devDependencies,
198
+ });
199
+ const hints = DEPENDENCY_HINTS.filter(([key]) =>
200
+ key.endsWith("/")
201
+ ? deps.some(dep => dep.startsWith(key))
202
+ : deps.includes(key)
203
+ ).map(([, hint]) => hint);
204
+ return [...new Set([name, ...hints])];
205
+ }
@@ -18,11 +18,14 @@
18
18
  * background sessions (`claude --bg`, `claude agents`), cross-session
19
19
  * messaging and channels, which no other supported agent has. Codex, Cursor,
20
20
  * OpenCode, Antigravity, Copilot and Devin can run the setup-time subcommands
21
- * (init-config, store-token, validate, start), and `start` hosts a Claude Code
22
- * manager in tmux whatever agent ran it; they cannot run a manager cycle
21
+ * (init-config, store-token, trust-status, coexistence, validate, start), and
22
+ * `start` hosts a Claude Code manager in tmux whatever agent ran it. Note that
23
+ * trust-status reads Claude Code's own trust record, which is the one that
24
+ * matters: workers are always Claude Code. They cannot run a manager cycle
23
25
  * themselves. OpenCode copies skill folders without the plugin-level
24
- * `scripts/` directory, so `queue` and `record-cycle` (which import from it)
25
- * are unavailable there unless the npm package provides that directory.
26
+ * `scripts/` directory, and this entry point statically imports modules that
27
+ * reach into it (cli-cycle.mjs, cli-runtime.mjs), so on OpenCode every
28
+ * subcommand runs only from the npm package, never from the skill-folder copy.
26
29
  * @module session-manager
27
30
  */
28
31
  import { readFileSync, realpathSync } from "node:fs";
@@ -38,17 +41,15 @@ import {
38
41
  recordCycle,
39
42
  route,
40
43
  } from "./cli-cycle.mjs";
44
+ import { repoDir, start, status, stop, validate } from "./cli-manager.mjs";
45
+ import { queue } from "./cli-queue.mjs";
46
+ import { CliError, EXIT } from "./cli-runtime.mjs";
41
47
  import {
48
+ coexistence,
42
49
  initConfig,
43
- repoDir,
44
- start,
45
- status,
46
- stop,
47
50
  storeToken,
48
- validate,
49
- } from "./cli-manager.mjs";
50
- import { queue } from "./cli-queue.mjs";
51
- import { CliError, EXIT } from "./cli-runtime.mjs";
51
+ trustStatus,
52
+ } from "./cli-setup.mjs";
52
53
  import { USAGE } from "./cli-usage.mjs";
53
54
  import { capacity, classify, resume, spawn } from "./cli-workers.mjs";
54
55
 
@@ -67,9 +68,11 @@ const COMMANDS = {
67
68
  stop: { run: stop, options: {}, positionals: 0 },
68
69
  "store-token": {
69
70
  run: storeToken,
70
- options: { replace: flag },
71
+ options: { replace: flag, "from-clipboard": flag },
71
72
  positionals: 0,
72
73
  },
74
+ "trust-status": { run: trustStatus, options: {}, positionals: 0 },
75
+ coexistence: { run: coexistence, options: {}, positionals: 0 },
73
76
  "repo-dir": {
74
77
  run: repoDir,
75
78
  options: { repo: text, raw: flag },
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Read-only facts setup needs before it asks the operator for anything:
3
+ * which worker directories Claude Code already trusts, and whether a
4
+ * scheduled intake loop is still running alongside the manager.
5
+ *
6
+ * Both used to be manual (CodySwannGT/lisa#4465). Setup asked the operator to
7
+ * open `claude` in every worker directory to accept workspace trust, even
8
+ * where Claude Code had already recorded it; and it only warned that
9
+ * `intake-tickets` / `intake-repair` compete with the manager, leaving the
10
+ * agent to dig through run logs to find out whether either was alive.
11
+ *
12
+ * Neither check writes anything, and neither answers a trust prompt.
13
+ * @module session-manager/setup-checks
14
+ */
15
+ import * as nodeFs from "node:fs";
16
+ import path from "node:path";
17
+
18
+ /** The scheduled loops that compete with the manager for the same items. */
19
+ export const COMPETING_LOOPS = Object.freeze([
20
+ "intake-tickets",
21
+ "intake-repair",
22
+ ]);
23
+
24
+ /** A loop counts as recent when it ran within at least this long. */
25
+ const MIN_RECENT_MS = 24 * 60 * 60 * 1000;
26
+
27
+ /**
28
+ * Where Claude Code keeps its global state, including per-directory trust:
29
+ * `$CLAUDE_CONFIG_DIR/.claude.json` when that is set, else `~/.claude.json`.
30
+ * @param {{ env?: Record<string, string | undefined>, homedir: string }} input
31
+ * - Environment and home directory
32
+ * @returns {string} Absolute path
33
+ */
34
+ export function claudeStateFile({ env = {}, homedir }) {
35
+ const configDir = env.CLAUDE_CONFIG_DIR;
36
+ return configDir
37
+ ? path.join(configDir, ".claude.json")
38
+ : path.join(homedir, ".claude.json");
39
+ }
40
+
41
+ /**
42
+ * Whether Claude Code has recorded workspace trust for each directory.
43
+ *
44
+ * `true` and `false` come from Claude Code's own record
45
+ * (`projects[<dir>].hasTrustDialogAccepted`). A directory and the key Claude
46
+ * Code recorded can name the same place through different symlinks (macOS
47
+ * `/var` is `/private/var`), so a key matches when the two realpaths agree;
48
+ * only keys with the same final segment are resolved, so a large state file
49
+ * costs a handful of realpath calls. `unknown` means the record could not be
50
+ * read, so the operator still has to check that directory by hand.
51
+ * @param {{ dirs: string[], env?: Record<string, string | undefined>,
52
+ * homedir: string, fs?: Pick<typeof nodeFs, "readFileSync" | "realpathSync"> }}
53
+ * input - Worker directories, environment, home directory and fs seam
54
+ * @returns {{ file: string, dirs: Array<{ dir: string,
55
+ * trusted: true | false | "unknown" }> }} One row per directory
56
+ */
57
+ export function trustStatus({ dirs, env, homedir, fs = nodeFs }) {
58
+ const file = claudeStateFile({ env, homedir });
59
+ let projects = null;
60
+ try {
61
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
62
+ projects =
63
+ parsed && typeof parsed.projects === "object" ? parsed.projects : {};
64
+ } catch {
65
+ projects = null;
66
+ }
67
+ const realpath = dir => {
68
+ try {
69
+ return fs.realpathSync(dir);
70
+ } catch {
71
+ return dir;
72
+ }
73
+ };
74
+ const entryFor = dir => {
75
+ if (Object.hasOwn(projects, dir)) return projects[dir];
76
+ const target = realpath(dir);
77
+ const key = Object.keys(projects).find(
78
+ candidate =>
79
+ path.basename(candidate) === path.basename(dir) &&
80
+ realpath(candidate) === target
81
+ );
82
+ return key === undefined ? undefined : projects[key];
83
+ };
84
+ return {
85
+ file,
86
+ dirs: dirs.map(dir => {
87
+ if (projects === null) return { dir, trusted: "unknown" };
88
+ return { dir, trusted: entryFor(dir)?.hasTrustDialogAccepted === true };
89
+ }),
90
+ };
91
+ }
92
+
93
+ /**
94
+ * The last parseable record of a loop's run log, or null.
95
+ * @param {string} file - `.lisa/automations/runs/<loop>.jsonl`
96
+ * @param {Pick<typeof nodeFs, "readFileSync">} fs - Filesystem seam
97
+ * @returns {{ ts: string, outcome: string | null } | null} Last record
98
+ */
99
+ function lastRunRecord(file, fs) {
100
+ let text;
101
+ try {
102
+ text = fs.readFileSync(file, "utf8");
103
+ } catch {
104
+ return null;
105
+ }
106
+ const lines = text.split("\n").filter(line => line.trim());
107
+ for (const line of lines.reverse()) {
108
+ try {
109
+ const record = JSON.parse(line);
110
+ if (
111
+ typeof record?.ts === "string" &&
112
+ !Number.isNaN(Date.parse(record.ts))
113
+ ) {
114
+ return { ts: record.ts, outcome: record.outcome ?? null };
115
+ }
116
+ } catch {
117
+ // A torn or foreign line: keep looking further back.
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * How long a loop may go without a recorded run and still count as live:
125
+ * twice the time the manager gives a silent worker before giving up on it
126
+ * (`stall.noOutputMinutes × stall.maxAttempts`), and never under a day.
127
+ * @param {{ noOutputMinutes?: number, maxAttempts?: number } | undefined} stall
128
+ * - The manager's stall settings
129
+ * @returns {number} Milliseconds
130
+ */
131
+ export function recentWindowMs(stall) {
132
+ const minutes = (stall?.noOutputMinutes ?? 60) * (stall?.maxAttempts ?? 3);
133
+ return Math.max(MIN_RECENT_MS, 2 * minutes * 60 * 1000);
134
+ }
135
+
136
+ /**
137
+ * What this project's own run logs say about each competing intake loop.
138
+ *
139
+ * A loop with a recent recorded run is probably still scheduled and will
140
+ * compete with the manager. A stale or missing log is evidence, not proof,
141
+ * that it stopped: the runtime's scheduler (claude.ai routines, Codex
142
+ * automations, GitHub Actions) is the authority, and setup lists it too.
143
+ * @param {{ root: string, now: Date, stall?: object,
144
+ * fs?: Pick<typeof nodeFs, "readFileSync"> }} input - Project root, the
145
+ * current time, the manager's stall settings and fs seam
146
+ * @returns {{ windowHours: number, loops: Array<{ loop: string,
147
+ * log: string, lastRun: { ts: string, outcome: string | null } | null,
148
+ * recent: boolean }> }} One row per competing loop
149
+ */
150
+ export function coexistence({ root, now, stall, fs = nodeFs }) {
151
+ const windowMs = recentWindowMs(stall);
152
+ return {
153
+ windowHours: windowMs / 3_600_000,
154
+ loops: COMPETING_LOOPS.map(loop => {
155
+ const log = path.join(
156
+ root,
157
+ ".lisa",
158
+ "automations",
159
+ "runs",
160
+ `${loop}.jsonl`
161
+ );
162
+ const lastRun = lastRunRecord(log, fs);
163
+ const recent =
164
+ lastRun !== null && now.getTime() - Date.parse(lastRun.ts) <= windowMs;
165
+ return { loop, log, lastRun, recent };
166
+ }),
167
+ };
168
+ }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The Telegram bot token the manager needs: validated, stored in
3
3
  * `<stateDir>/.env` (dir 0700, file 0600), read from the operator's terminal
4
- * with echo off, and never put in argv, a return value, an error message or a
4
+ * with echo off or from the clipboard (`--from-clipboard`, for when no TTY is
5
+ * reachable), and never put in argv, a return value, an error message or a
5
6
  * log line. An existing token is replaced only on request. Split out of
6
7
  * launch.mjs, which re-exports everything here.
7
8
  * @module token
@@ -10,6 +11,7 @@ import { randomUUID } from "node:crypto";
10
11
  import * as nodeFs from "node:fs";
11
12
  import path from "node:path";
12
13
 
14
+ import { readClipboard } from "./clipboard.mjs";
13
15
  import { LaunchError, resolveStateDir } from "./state-dir.mjs";
14
16
 
15
17
  /** The shape of a BotFather token: `<bot id>:<secret>`. */
@@ -94,41 +96,110 @@ function shellQuote(word) {
94
96
  * input - Script and project locations, and whether to carry `--replace`
95
97
  * @returns {string} One shell line
96
98
  */
97
- export function buildStoreTokenCommand({ scriptsDir, projectRoot, replace }) {
99
+ export function buildStoreTokenCommand({
100
+ scriptsDir,
101
+ projectRoot,
102
+ replace,
103
+ fromClipboard = false,
104
+ }) {
98
105
  const script = shellQuote(path.join(scriptsDir, "session-manager.mjs"));
99
- const flag = replace ? " --replace" : "";
100
- return `node ${script} store-token${flag} --project-root ${shellQuote(projectRoot)}`;
106
+ const flags =
107
+ (replace ? " --replace" : "") + (fromClipboard ? " --from-clipboard" : "");
108
+ return `node ${script} store-token${flags} --project-root ${shellQuote(projectRoot)}`;
109
+ }
110
+
111
+ /** Why `store-token` cannot prompt here, and the two ways forward. */
112
+ const NO_TTY_HINT =
113
+ "No terminal to prompt on, and Claude Code's ! prefix has no TTY either. " +
114
+ "Run `command` in a separate terminal app, or copy the token from " +
115
+ "BotFather and run `clipboardCommand`.";
116
+
117
+ /** The `invalid` outcome; names the expected shape, never the input. */
118
+ const INVALID = Object.freeze({
119
+ stored: false,
120
+ reason: "invalid",
121
+ message:
122
+ "That does not look like a Telegram bot token " +
123
+ "(expected <digits>:<30 or more letters, digits, _ or ->).",
124
+ });
125
+
126
+ /**
127
+ * Refuse to overwrite an existing token unless `replace` was asked for: a
128
+ * second project that shares this state dir by mistake would otherwise
129
+ * silently take over the first project's bot.
130
+ * @param {string} stateDir - Resolved state dir
131
+ * @param {boolean} replace - `--replace`
132
+ * @param {{ fs?: { existsSync?: (p: string) => boolean } }} io - Seams
133
+ */
134
+ function assertReplaceable(stateDir, replace, io) {
135
+ const existing = path.join(stateDir, TOKEN_FILE_NAME);
136
+ const exists = io.fs?.existsSync ?? nodeFs.existsSync;
137
+ if (!replace && exists(existing)) {
138
+ throw new LaunchError(
139
+ `A bot token already exists at ${existing}. Nothing was changed. ` +
140
+ `Re-run store-token with --replace to overwrite it.`
141
+ );
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Store the bot token from the system clipboard, with no TTY needed.
147
+ *
148
+ * Same validation and the same existing-token rule as the prompt. The
149
+ * clipboard is cleared only after a successful store, so a failed attempt
150
+ * leaves the operator's copy in place to retry with. The token is never
151
+ * returned, and an invalid value is reported by shape, never echoed.
152
+ * @param {{ stateDir: string, replace?: boolean,
153
+ * env?: Record<string, string | undefined>, homedir?: string,
154
+ * io?: { readClipboard?: typeof readClipboard, fs?: object } }} input -
155
+ * Configured state dir, the `--replace` flag and seams
156
+ * @returns {{ stored: true, path: string, clipboardCleared: boolean } |
157
+ * { stored: false, reason: "empty" | "invalid" | "unsupported" |
158
+ * "unavailable" | "failed", message: string }} Outcome
159
+ */
160
+ export function storeTokenFromClipboard(input) {
161
+ const { replace = false, io = {} } = input;
162
+ const stateDir = resolveStateDir(input.stateDir, input);
163
+ assertReplaceable(stateDir, replace, io);
164
+ const clip = (io.readClipboard ?? readClipboard)();
165
+ if (!clip.ok) {
166
+ return { stored: false, reason: clip.reason, message: clip.message };
167
+ }
168
+ const token = clip.text.trim();
169
+ if (!token) {
170
+ return {
171
+ stored: false,
172
+ reason: "empty",
173
+ message: "The clipboard is empty.",
174
+ };
175
+ }
176
+ if (!validateBotToken(token)) return { ...INVALID };
177
+ const written = writeTokenFile(stateDir, token, io);
178
+ return { stored: true, ...written, clipboardCleared: clip.clear() };
101
179
  }
102
180
 
103
181
  /**
104
182
  * Prompt for the bot token on the operator's terminal and store it.
105
183
  *
106
- * An existing token is never overwritten without `replace`: a second project
107
- * that shares this state dir by mistake would otherwise silently take over the
108
- * first project's bot. With no TTY this does not prompt and does not fail: it
109
- * returns the exact command for the operator to run themselves, because an
110
- * agent's Bash can never answer a prompt and a hung setup looks like a slow
111
- * one.
184
+ * An existing token is never overwritten without `replace`. With no TTY this
185
+ * does not prompt and does not fail: it returns the exact command for the
186
+ * operator to run in a separate terminal app, and the `--from-clipboard` form
187
+ * of it, because an agent's Bash (and Claude Code's `!` prefix) can never
188
+ * answer a prompt and a hung setup looks like a slow one.
112
189
  * @param {{ stateDir: string, scriptsDir: string, projectRoot: string,
113
190
  * replace?: boolean, env?: Record<string, string | undefined>,
114
191
  * homedir?: string, io?: { canPrompt?: () => boolean,
115
192
  * promptSecret?: (message: string) => string, fs?: object } }} input -
116
193
  * Configured state dir, locations, the `--replace` flag and seams
117
194
  * @returns {Promise<{ stored: true, path: string } |
118
- * { stored: false, reason: "no-tty", command: string } |
195
+ * { stored: false, reason: "no-tty", message: string, command: string,
196
+ * clipboardCommand: string } |
119
197
  * { stored: false, reason: "empty" | "invalid", message: string }>} Outcome
120
198
  */
121
199
  export async function storeToken(input) {
122
200
  const { scriptsDir, projectRoot, replace = false, io = {} } = input;
123
201
  const stateDir = resolveStateDir(input.stateDir, input);
124
- const existing = path.join(stateDir, TOKEN_FILE_NAME);
125
- const exists = io.fs?.existsSync ?? nodeFs.existsSync;
126
- if (!replace && exists(existing)) {
127
- throw new LaunchError(
128
- `A bot token already exists at ${existing}. Nothing was changed. ` +
129
- `Re-run store-token with --replace to overwrite it.`
130
- );
131
- }
202
+ assertReplaceable(stateDir, replace, io);
132
203
  const loaded =
133
204
  io.canPrompt && io.promptSecret ? null : await loadPromptSecret();
134
205
  const canPrompt = io.canPrompt ?? (() => loaded.canPrompt());
@@ -138,7 +209,14 @@ export async function storeToken(input) {
138
209
  return {
139
210
  stored: false,
140
211
  reason: "no-tty",
212
+ message: NO_TTY_HINT,
141
213
  command: buildStoreTokenCommand({ scriptsDir, projectRoot, replace }),
214
+ clipboardCommand: buildStoreTokenCommand({
215
+ scriptsDir,
216
+ projectRoot,
217
+ replace,
218
+ fromClipboard: true,
219
+ }),
142
220
  };
143
221
  }
144
222
  const typed = promptSecret(
@@ -147,15 +225,7 @@ export async function storeToken(input) {
147
225
  if (!typed) {
148
226
  return { stored: false, reason: "empty", message: "Nothing entered." };
149
227
  }
150
- if (!validateBotToken(typed)) {
151
- return {
152
- stored: false,
153
- reason: "invalid",
154
- message:
155
- "That does not look like a Telegram bot token " +
156
- "(expected <digits>:<30 or more letters, digits, _ or ->).",
157
- };
158
- }
228
+ if (!validateBotToken(typed)) return { ...INVALID };
159
229
  return { stored: true, ...writeTokenFile(stateDir, typed, io) };
160
230
  }
161
231