@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.
- 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 +52 -14
- 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-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +15 -8
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +4 -3
- 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-queue.mjs +9 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- 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 +36 -9
- 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 +22 -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 +57 -22
- package/plugins/lisa/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa/skills/lisa-session-manager/SKILL.md +15 -8
- package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-setup.mjs +165 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +36 -9
- package/plugins/lisa/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +22 -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 +57 -22
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +15 -8
- package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- 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 +36 -9
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +22 -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 +57 -22
- 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/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa-copilot/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +15 -8
- package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- 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 +36 -9
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +22 -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 +57 -22
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -0
- package/plugins/lisa-cursor/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +15 -8
- package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- 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 +36 -9
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +22 -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 +57 -22
- package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa-devin/scripts/ready-queue-order.mjs +34 -14
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +15 -8
- package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- 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 +36 -9
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +22 -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 +57 -22
- 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/rules/reference/config-resolution.md +1 -0
- package/plugins/src/base/scripts/ready-queue-order.mjs +34 -14
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +30 -0
- package/plugins/src/base/skills/lisa-session-manager/SKILL.md +15 -8
- package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +4 -3
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-manager.mjs +5 -76
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +9 -3
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-runtime.mjs +1 -0
- 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 +36 -9
- package/plugins/src/base/skills/lisa-session-manager/scripts/clipboard.mjs +119 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +22 -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 +57 -22
|
@@ -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
|
|
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({
|
|
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
|
|
100
|
-
|
|
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
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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",
|
|
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
|
-
|
|
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
|
|
|
@@ -42,16 +42,31 @@ writes `$SM`, and pass `--project-root "$ROOT"` (the absolute project root) on e
|
|
|
42
42
|
|
|
43
43
|
1. Preview: `node "$SM/session-manager.mjs" init-config --project-root "$ROOT"`. It detects
|
|
44
44
|
`projects/*/.lisa.config.json` and proposes `name`, `interval`, `repos`, `capacity`, `stall` and a
|
|
45
|
-
per-project `notify.stateDir`.
|
|
45
|
+
per-project `notify.stateDir`. Repo detection leaves out linked worktrees and keeps one checkout
|
|
46
|
+
per remote, so ticket worktrees and second clones under `projects/` are not proposed. Show the
|
|
47
|
+
block to the operator and ask whether any repo is missing or should not be routed.
|
|
46
48
|
2. Write it: the same command with `--write`. It adds only non-secret fields and never overwrites an
|
|
47
49
|
existing block (`reason: "exists"` means keep it).
|
|
48
|
-
3.
|
|
49
|
-
|
|
50
|
+
3. Each repo's `hints` are a draft: its name plus one per well-known dependency (`NestJS`, `Expo`,
|
|
51
|
+
`AWS CDK`, ...). Ask the operator to correct them and add the areas that identify the repo
|
|
52
|
+
(e.g. `api`: `["NestJS","migrations"]`, `web`: `["Expo","screens"]`), and whether to
|
|
50
53
|
set `sessionManager.trustedAuthorIds`. Without it, any author's Ready item reaches a
|
|
51
54
|
bypass-permissions worker; say so plainly. The list holds stable tracker user ids as strings,
|
|
52
55
|
never logins: the GitHub numeric user id (`gh api users/<login> --jq .id`), the Jira
|
|
53
56
|
`accountId`, or the Linear user id. A login matches no one and silently excludes every item.
|
|
54
|
-
4. `
|
|
57
|
+
4. Set `sessionManager.operatorIds` to a non-empty **array** holding the operator's own tracker
|
|
58
|
+
user id, e.g. `"operatorIds": ["<operator-id>"]` (a bare string fails `validate`). Look up the
|
|
59
|
+
OPERATOR, not the credential: `gh api user` / `viewer.id` return whoever the tracker credential
|
|
60
|
+
belongs to, which is the agent account in exactly the case this key exists for. The manager dispatches
|
|
61
|
+
the operator's assigned items first, then items assigned to the tracker credential's identity
|
|
62
|
+
("me"), then unassigned ones, and skips items assigned to anyone else. When the tracker
|
|
63
|
+
credential is not the operator's (an agent or bot account), leaving this unset means the
|
|
64
|
+
operator's own Ready items are never dispatched; say so plainly. Unlike `trustedAuthorIds`, this
|
|
65
|
+
is matched against the assignee, exactly and case-sensitively, so use the same kind of id as
|
|
66
|
+
"me", spelled as the tracker API returns it: the GitHub **login** (`gh api users/<login> --jq
|
|
67
|
+
.login`), the Jira `accountId`, or the Linear user id (the operator's entry in the team's member
|
|
68
|
+
list, or `viewer.id` only when the operator runs the lookup with their own credential).
|
|
69
|
+
5. `node "$SM/session-manager.mjs" validate --project-root "$ROOT"` must print `valid: true`.
|
|
55
70
|
|
|
56
71
|
## Step 3: Store the bot token
|
|
57
72
|
|
|
@@ -61,8 +76,14 @@ The operator creates a bot with @BotFather, one per project. Then run
|
|
|
61
76
|
- `stored: true`: done.
|
|
62
77
|
- Exit 2 because a token already exists: keep it (re-running setup is safe). Only when the operator
|
|
63
78
|
says the stored token is wrong or belongs to another project, re-run with `store-token --replace`.
|
|
64
|
-
- `reason: "no-tty"` (an agent's Bash has no terminal):
|
|
65
|
-
|
|
79
|
+
- `reason: "no-tty"` (an agent's Bash has no terminal): offer the operator both routes, and say
|
|
80
|
+
plainly that Claude Code's `!` prefix has no terminal either, so it fails the same way:
|
|
81
|
+
- **Clipboard (simplest):** they copy the token from the BotFather chat and tell you; then run
|
|
82
|
+
`clipboardCommand` from the output (`store-token --from-clipboard`). It validates the token,
|
|
83
|
+
stores it, never prints it, and clears the clipboard. On `stored: false`, say what `message`
|
|
84
|
+
reports and ask them to copy it again.
|
|
85
|
+
- **Separate terminal app:** print `command` and ask them to run it in Terminal, iTerm or similar,
|
|
86
|
+
outside the agent, where it prompts with echo off. Wait for them to confirm.
|
|
66
87
|
- `empty` / `invalid`: say what was wrong and ask again.
|
|
67
88
|
|
|
68
89
|
Never ask for the token in chat, never echo, log or commit it, and never put it in
|
|
@@ -97,14 +118,24 @@ If `.lisa/automations/session-manager.runbook.md` does not exist, copy
|
|
|
97
118
|
`$SM/../references/runbook-template.md` there. Never overwrite an existing runbook: it is the
|
|
98
119
|
project's. It is checked in, like every automation runbook (`automation-runbook-contract`).
|
|
99
120
|
|
|
100
|
-
## Step 7:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
## Step 7: Check for competing intake loops
|
|
122
|
+
|
|
123
|
+
The scheduled `intake-repair` loop can re-dispatch an item a local worker is still building (it treats
|
|
124
|
+
a claimed item with no tracker activity for `intake.repair.staleAfterHours` as stalled), and
|
|
125
|
+
`intake-tickets` competes for the same Ready items. Find out whether either is running before you
|
|
126
|
+
start the manager; do not leave the operator to check.
|
|
127
|
+
|
|
128
|
+
1. Run `node "$SM/session-manager.mjs" coexistence --project-root "$ROOT"`. For each loop it reports
|
|
129
|
+
the last run recorded in this project's run log and whether that is `recent`. `live` lists the
|
|
130
|
+
loops that ran recently.
|
|
131
|
+
2. A run log is evidence, not proof: also list your runtime's scheduler for those loop names (Claude
|
|
132
|
+
Code: the `/schedule` listing of claude.ai routines; Codex: automations; a `lisa-<loop>.yml`
|
|
133
|
+
workflow with a `schedule:` trigger on GitHub Actions).
|
|
134
|
+
3. Report what you found, per loop, in one line each. Only for a loop that is live (a recent run, or a
|
|
135
|
+
scheduler entry): recommend tearing down `intake-tickets` (`/lisa:tear-down-automations
|
|
136
|
+
intake-tickets`) and either raising `intake.repair.staleAfterHours` above
|
|
137
|
+
`stall.noOutputMinutes × stall.maxAttempts` or tearing down `intake-repair`. When neither is live,
|
|
138
|
+
say so and move on.
|
|
108
139
|
|
|
109
140
|
## Step 8: Accept workspace trust in each worker repo
|
|
110
141
|
|
|
@@ -113,12 +144,15 @@ folder?" prompt: until someone has accepted it in a directory, `claude --bg` ref
|
|
|
113
144
|
and `spawn` exits 2 saying so. Do this once per routed repo (or once for the root on a single-repo
|
|
114
145
|
project), on every machine that runs the manager:
|
|
115
146
|
|
|
116
|
-
1.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
accept
|
|
147
|
+
1. Run `node "$SM/session-manager.mjs" trust-status --project-root "$ROOT"`. It lists every
|
|
148
|
+
directory workers start in (each routed repo, or the root on a single-repo project) with whether
|
|
149
|
+
Claude Code has already recorded trust for it. `needsTrust` lists the ones that are not
|
|
150
|
+
`trusted: true`. When it is empty, this step is done: say so.
|
|
151
|
+
2. For each directory in `needsTrust`, ask the operator, in a separate terminal app (not the agent's
|
|
152
|
+
Bash or Claude Code's `!` prefix), to run `claude` once in that directory, accept the trust prompt,
|
|
153
|
+
then exit. An agent's Bash cannot answer that prompt, so never try to accept it for them. Then
|
|
154
|
+
re-run `trust-status` to confirm. (`trusted: "unknown"` means Claude Code's state file could not be
|
|
155
|
+
read; treat it as needing trust.)
|
|
122
156
|
3. Re-run this step whenever a repo is added to `sessionManager.repos`.
|
|
123
157
|
|
|
124
158
|
## Step 9: Start and pair
|
|
@@ -152,5 +186,6 @@ confirm the "Session manager Telegram isolation?" check is green. Fix every warn
|
|
|
152
186
|
`node "$SM/session-manager.mjs" status --project-root "$ROOT"` should show the manager running.
|
|
153
187
|
|
|
154
188
|
Report: the config block written (or kept), where the token lives (the path, never the value), the
|
|
155
|
-
plugin scope result, the runbook path, which repo dirs
|
|
156
|
-
tmux session name, and the
|
|
189
|
+
plugin scope result, the runbook path, which repo dirs were already trusted and which the operator
|
|
190
|
+
accepted trust in, what the competing-loop check found, the manager's tmux session name, and the
|
|
191
|
+
doctor result.
|
|
@@ -733,6 +733,7 @@ bypassed). `/lisa:setup:session-manager` writes it, detecting a multi-repo layou
|
|
|
733
733
|
| `sessionManager.notify.provider` | no | `telegram` | Only `telegram` is implemented. |
|
|
734
734
|
| `sessionManager.notify.stateDir` | **yes** | — | This project's own Telegram state dir. Never the shared default `~/.claude/channels/telegram`, because one bot per state dir is how projects stay isolated. Setup writes `~/.claude/channels/telegram-<project>`. |
|
|
735
735
|
| `sessionManager.trustedAuthorIds` | no | — | Optional list of **stable tracker user ids**, as strings: the GitHub numeric user id (`gh api users/<login> --jq .id`), the Jira `accountId`, or the Linear user id. Not logins or display names: matching compares ids, so a login silently matches no one and excludes every item. When set, the manager takes only items authored by these people. |
|
|
736
|
+
| `sessionManager.operatorIds` | no | — | Optional, non-empty list of the people the manager works for, as tracker user ids matched against the **assignee** the same way "me" is: the GitHub **login**, the Jira `accountId`, or the Linear user id, spelled exactly as the tracker API returns it (matching is case-sensitive, so look the OPERATOR up rather than typing from memory: `gh api users/<operator-login> --jq .login` returns the canonical GitHub spelling. Do not use `gh api user`, which returns whoever the credential belongs to — the agent account, when that is the case this key exists for). Dispatch order is assigned to an operator, then to "me" (the tracker credential's identity), then unassigned; items assigned to anyone else are skipped. Set it whenever the tracker credential is not the operator's own (an agent account), or the operator's Ready items are never dispatched. |
|
|
736
737
|
|
|
737
738
|
- **Non-secret only.** The bot token lives in `<stateDir>/.env` (directory 0700, file 0600),
|
|
738
739
|
written by setup. A credential-named field or a token-shaped value anywhere in this block is a
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Vendor-neutral Ready-queue normalization and
|
|
3
|
+
* Vendor-neutral Ready-queue normalization and operator-first dispatch order.
|
|
4
4
|
*
|
|
5
5
|
* ## Why this exists
|
|
6
6
|
*
|
|
7
7
|
* Intake has always taken Ready work in whatever order the tracker returned
|
|
8
8
|
* it, and `intake.assignee` only NARROWS a selection — it never ranks one. A
|
|
9
9
|
* dispatcher that runs several workers at once needs a stable answer to "what
|
|
10
|
-
* next?":
|
|
11
|
-
* then
|
|
12
|
-
*
|
|
10
|
+
* next?": work assigned to the operator first (the person the dispatcher works
|
|
11
|
+
* for), then work assigned to me (the identity the tracker credential belongs
|
|
12
|
+
* to, often an agent account rather than the operator), then unassigned work,
|
|
13
|
+
* most urgent and oldest first. Work assigned to anyone else is theirs, so it
|
|
14
|
+
* is excluded, never re-ordered. When the credential IS the operator's, the
|
|
15
|
+
* first two groups coincide. With "me" as the only identity, an operator whose
|
|
16
|
+
* credential is an agent account had every one of their own items excluded as
|
|
17
|
+
* someone else's (CodySwannGT/lisa#4467).
|
|
13
18
|
*
|
|
14
19
|
* The three trackers disagree on every field this needs — GitHub has priority
|
|
15
20
|
* LABELS, Linear a native 0..4 number where 0 means "none", Jira a priority
|
|
@@ -252,12 +257,18 @@ function shape(input) {
|
|
|
252
257
|
* Order matters only for the reason reported: an item can fail several rules,
|
|
253
258
|
* and "someone else owns it" is the most useful thing to say about it.
|
|
254
259
|
* @param {Record<string, any>} item
|
|
255
|
-
* @param {{ me: string | null, trusted: ReadonlySet<string>, humanNeededLabel: string }} context
|
|
260
|
+
* @param {{ me: string | null, operators: ReadonlySet<string>, trusted: ReadonlySet<string>, humanNeededLabel: string }} context
|
|
256
261
|
* @returns {string | null}
|
|
257
262
|
*/
|
|
258
263
|
function exclusionReason(item, context) {
|
|
259
264
|
const assignee = idOrNull(item.assigneeId);
|
|
260
|
-
if (
|
|
265
|
+
if (
|
|
266
|
+
assignee !== null &&
|
|
267
|
+
assignee !== context.me &&
|
|
268
|
+
!context.operators.has(assignee)
|
|
269
|
+
) {
|
|
270
|
+
return "assigned-to-other";
|
|
271
|
+
}
|
|
261
272
|
if (item.isContainer === true) return "container";
|
|
262
273
|
if (
|
|
263
274
|
isHumanGated({
|
|
@@ -293,20 +304,25 @@ function compareWithinGroup(left, right) {
|
|
|
293
304
|
}
|
|
294
305
|
|
|
295
306
|
/**
|
|
296
|
-
* Order normalized Ready items for dispatch, mine
|
|
307
|
+
* Order normalized Ready items for dispatch: the operator's, then mine, then
|
|
308
|
+
* unassigned.
|
|
297
309
|
*
|
|
298
|
-
* Group 0 is assigned to `me`, group
|
|
299
|
-
* rank ascending, then creation time ascending, then
|
|
300
|
-
* tiebreak. Excluded items keep their input order. With no
|
|
301
|
-
* assigned item is someone else's — the safe
|
|
302
|
-
* assigned work steals it.
|
|
310
|
+
* Group 0 is assigned to an operator, group 1 to `me`, group 2 unassigned;
|
|
311
|
+
* within a group, priority rank ascending, then creation time ascending, then
|
|
312
|
+
* ref as a stable tiebreak. Excluded items keep their input order. With no
|
|
313
|
+
* `me` and no operators, every assigned item is someone else's — the safe
|
|
314
|
+
* direction, since claiming assigned work steals it.
|
|
315
|
+
*
|
|
316
|
+
* `operators` match the way `me` does, against the normalized assignee id:
|
|
317
|
+
* GitHub login, Jira accountId, Linear user id.
|
|
303
318
|
* @param {readonly Record<string, any>[]} items - Normalized items
|
|
304
|
-
* @param {{ me?: string | null, trustedAuthorIds?: readonly unknown[], humanNeededLabel?: string }} [options]
|
|
319
|
+
* @param {{ me?: string | null, operators?: readonly unknown[], trustedAuthorIds?: readonly unknown[], humanNeededLabel?: string }} [options]
|
|
305
320
|
* @returns {{ ordered: Record<string, any>[], excluded: { ref: string, reason: string }[] }}
|
|
306
321
|
*/
|
|
307
322
|
export function orderReadyQueue(items, options = {}) {
|
|
308
323
|
const context = {
|
|
309
324
|
me: idOrNull(options.me),
|
|
325
|
+
operators: new Set((options.operators ?? []).map(idOrNull).filter(Boolean)),
|
|
310
326
|
trusted: new Set(
|
|
311
327
|
(options.trustedAuthorIds ?? []).map(idOrNull).filter(Boolean)
|
|
312
328
|
),
|
|
@@ -319,7 +335,11 @@ export function orderReadyQueue(items, options = {}) {
|
|
|
319
335
|
const eligible = judged
|
|
320
336
|
.filter(one => one.reason === null)
|
|
321
337
|
.map(one => one.item);
|
|
322
|
-
const group = item =>
|
|
338
|
+
const group = item => {
|
|
339
|
+
const assignee = idOrNull(item.assigneeId);
|
|
340
|
+
if (assignee === null) return 2;
|
|
341
|
+
return context.operators.has(assignee) ? 0 : 1;
|
|
342
|
+
};
|
|
323
343
|
|
|
324
344
|
return {
|
|
325
345
|
ordered: [...eligible].sort(
|