@codyswann/lisa 4.81.3 → 4.81.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +44 -8
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +2 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +18 -36
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/launch.mjs +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token.mjs +98 -28
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +44 -21
- package/plugins/lisa/skills/lisa-session-manager/SKILL.md +2 -2
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
- package/plugins/lisa/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +18 -36
- package/plugins/lisa/skills/lisa-session-manager/scripts/launch.mjs +1 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
- package/plugins/lisa/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/token.mjs +98 -28
- package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +44 -21
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +2 -2
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +18 -36
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/launch.mjs +1 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token.mjs +98 -28
- package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +44 -21
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +2 -2
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +18 -36
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/launch.mjs +1 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token.mjs +98 -28
- package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +44 -21
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +2 -2
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +18 -36
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/launch.mjs +1 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token.mjs +98 -28
- package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +44 -21
- package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +2 -2
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +18 -36
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/launch.mjs +1 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token.mjs +98 -28
- package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +44 -21
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-session-manager/SKILL.md +2 -2
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +32 -8
- package/plugins/src/base/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +18 -36
- package/plugins/src/base/skills/lisa-session-manager/scripts/launch.mjs +1 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/repo-detect.mjs +205 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/session-manager.mjs +16 -13
- package/plugins/src/base/skills/lisa-session-manager/scripts/setup-checks.mjs +168 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/token.mjs +98 -28
- package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +44 -21
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* registry populates defaults into every project, and the manager starts
|
|
12
12
|
* yolo-mode sessions; it must exist only where an operator wrote it.
|
|
13
13
|
*
|
|
14
|
-
* Pure
|
|
14
|
+
* Pure. Repo detection, which reads the filesystem, lives in repo-detect.mjs
|
|
15
|
+
* and is re-exported here so callers keep one import.
|
|
15
16
|
* @module session-manager/config
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
import {
|
|
19
|
-
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
19
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
mergeConfigLayers as mergeLayers,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "../../lisa-secrets-access/scripts/validate-config.mjs";
|
|
26
26
|
|
|
27
27
|
export { parseLoopInterval, validateSessionManager };
|
|
28
|
+
export { detectRepos, draftHints } from "./repo-detect.mjs";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Documented defaults for every optional key.
|
|
@@ -122,33 +123,6 @@ export function resolveSessionManagerConfig(globalConfig, localConfig) {
|
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
/**
|
|
126
|
-
* List the Lisa-configured child repos of a multi-repo project.
|
|
127
|
-
*
|
|
128
|
-
* A child counts only when it carries its own `.lisa.config.json`: workers run
|
|
129
|
-
* `/lisa:implement` inside it, which needs that file, and a stray directory
|
|
130
|
-
* under `projects/` is not something to route tickets into.
|
|
131
|
-
* @param {string} root Project root.
|
|
132
|
-
* @param {{
|
|
133
|
-
* reposRoot?: string,
|
|
134
|
-
* fs?: { existsSync: (p: string) => boolean,
|
|
135
|
-
* readdirSync: (p: string, o: { withFileTypes: true }) =>
|
|
136
|
-
* Array<{ name: string, isDirectory: () => boolean }> },
|
|
137
|
-
* }} [options] Repos root (default `projects`) and filesystem seam.
|
|
138
|
-
* @returns {string[]} Child directory names, sorted.
|
|
139
|
-
*/
|
|
140
|
-
export function detectRepos(root, options = {}) {
|
|
141
|
-
const fs = options.fs ?? { existsSync, readdirSync };
|
|
142
|
-
const base = join(root, options.reposRoot ?? DEFAULT_REPOS_ROOT);
|
|
143
|
-
if (!fs.existsSync(base)) return [];
|
|
144
|
-
return fs
|
|
145
|
-
.readdirSync(base, { withFileTypes: true })
|
|
146
|
-
.filter(entry => entry.isDirectory() && SAFE_SEGMENT.test(entry.name))
|
|
147
|
-
.filter(entry => fs.existsSync(join(base, entry.name, ".lisa.config.json")))
|
|
148
|
-
.map(entry => entry.name)
|
|
149
|
-
.sort();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
126
|
/**
|
|
153
127
|
* Turn a project name into a slug the validator accepts.
|
|
154
128
|
* @param {string} projectSlug Project name, possibly with dots or capitals.
|
|
@@ -177,13 +151,18 @@ function normalizeSlug(projectSlug) {
|
|
|
177
151
|
* Defaults are written out rather than left implicit, so an operator sees
|
|
178
152
|
* every knob in the file they will edit. With no detected repos the block has
|
|
179
153
|
* no `repos` key, which is what makes a project single-repo. Each detected
|
|
180
|
-
* repo's hints
|
|
181
|
-
*
|
|
182
|
-
* @param {{ projectSlug: string, detectedRepos?: string[]
|
|
183
|
-
* slug
|
|
154
|
+
* repo's hints are its drafted hints when given (`draftHints`), else its own
|
|
155
|
+
* name: valid either way, and setup asks the operator to review them.
|
|
156
|
+
* @param {{ projectSlug: string, detectedRepos?: string[],
|
|
157
|
+
* hints?: Record<string, string[]> }} input Project slug, the output of
|
|
158
|
+
* `detectRepos`, and drafted hints per repo.
|
|
184
159
|
* @returns {SessionManagerBlock} A block `validateSessionManager` accepts.
|
|
185
160
|
*/
|
|
186
|
-
export function buildDefaultBlock({
|
|
161
|
+
export function buildDefaultBlock({
|
|
162
|
+
projectSlug,
|
|
163
|
+
detectedRepos = [],
|
|
164
|
+
hints = {},
|
|
165
|
+
}) {
|
|
187
166
|
const slug = normalizeSlug(projectSlug);
|
|
188
167
|
const repos = [...detectedRepos].sort();
|
|
189
168
|
return {
|
|
@@ -194,7 +173,10 @@ export function buildDefaultBlock({ projectSlug, detectedRepos = [] }) {
|
|
|
194
173
|
repos: {
|
|
195
174
|
root: DEFAULT_REPOS_ROOT,
|
|
196
175
|
entries: Object.fromEntries(
|
|
197
|
-
repos.map(name => [
|
|
176
|
+
repos.map(name => [
|
|
177
|
+
name,
|
|
178
|
+
{ hints: hints[name]?.length ? [...hints[name]] : [name] },
|
|
179
|
+
])
|
|
198
180
|
),
|
|
199
181
|
},
|
|
200
182
|
}
|
|
@@ -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
|
|
22
|
-
* manager in tmux whatever agent ran it
|
|
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,
|
|
25
|
-
*
|
|
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
|
-
|
|
49
|
-
} from "./cli-
|
|
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
|
+
}
|