@codyswann/lisa 4.81.4 → 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 +12 -10
- 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 +13 -6
- 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-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-usage.mjs +4 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +13 -1
- 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 +13 -6
- package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +4 -3
- 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-usage.mjs +4 -1
- package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +13 -1
- 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 +13 -6
- package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +4 -3
- 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-usage.mjs +4 -1
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +13 -1
- 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 +13 -6
- package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +4 -3
- 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-usage.mjs +4 -1
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +13 -1
- 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 +13 -6
- package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +4 -3
- 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-usage.mjs +4 -1
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +13 -1
- 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 +13 -6
- package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +4 -3
- 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-usage.mjs +4 -1
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +13 -1
- 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 +13 -6
- package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +4 -3
- 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-usage.mjs +4 -1
- package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +4 -0
- package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +13 -1
package/package.json
CHANGED
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"qs": "^6.16.0"
|
|
190
190
|
},
|
|
191
191
|
"name": "@codyswann/lisa",
|
|
192
|
-
"version": "4.81.
|
|
192
|
+
"version": "4.81.5",
|
|
193
193
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
194
194
|
"main": "dist/index.js",
|
|
195
195
|
"exports": {
|
|
@@ -345,7 +345,7 @@
|
|
|
345
345
|
"test": "tests"
|
|
346
346
|
},
|
|
347
347
|
"types": "./dist/index.d.ts",
|
|
348
|
-
"lisaReleaseCommit": "
|
|
349
|
-
"gitHead": "
|
|
350
|
-
"lisaReleaseTag": "v4.81.
|
|
348
|
+
"lisaReleaseCommit": "cf09d4d2b88095fe43bcff62a14251754b25d737",
|
|
349
|
+
"gitHead": "cf09d4d2b88095fe43bcff62a14251754b25d737",
|
|
350
|
+
"lisaReleaseTag": "v4.81.5"
|
|
351
351
|
}
|
|
@@ -727,6 +727,35 @@ function findCredentials(value, path) {
|
|
|
727
727
|
);
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Validate `sessionManager.operatorIds`: the people the manager works for,
|
|
732
|
+
* whose assigned items it dispatches first. Matched against the assignee the
|
|
733
|
+
* way "me" is, so the GitHub entry is a login, not the numeric id
|
|
734
|
+
* `trustedAuthorIds` takes. An empty list reads as configured but names no
|
|
735
|
+
* one, so it is refused like an empty `trustedAuthorIds`.
|
|
736
|
+
* @param {unknown} ids The value, if present.
|
|
737
|
+
* @returns {string[]} Problems found.
|
|
738
|
+
*/
|
|
739
|
+
function validateOperatorIds(ids) {
|
|
740
|
+
if (ids === undefined) return [];
|
|
741
|
+
if (
|
|
742
|
+
!Array.isArray(ids) ||
|
|
743
|
+
!ids.every(id => typeof id === "string" && id.trim() !== "")
|
|
744
|
+
) {
|
|
745
|
+
return [
|
|
746
|
+
`sessionManager.operatorIds must be an array of tracker user ids as ` +
|
|
747
|
+
`strings, matched against the assignee: the GitHub login, the Jira ` +
|
|
748
|
+
`accountId or the Linear user id.`,
|
|
749
|
+
];
|
|
750
|
+
}
|
|
751
|
+
return ids.length === 0
|
|
752
|
+
? [
|
|
753
|
+
`sessionManager.operatorIds must name at least one person. Remove ` +
|
|
754
|
+
`the key instead if the manager works only for its own identity.`,
|
|
755
|
+
]
|
|
756
|
+
: [];
|
|
757
|
+
}
|
|
758
|
+
|
|
730
759
|
/**
|
|
731
760
|
* Validate the opt-in `sessionManager` block.
|
|
732
761
|
*
|
|
@@ -786,6 +815,7 @@ export function validateSessionManager(block) {
|
|
|
786
815
|
`if every author's item may be dispatched.`,
|
|
787
816
|
]
|
|
788
817
|
: []),
|
|
818
|
+
...validateOperatorIds(block.operatorIds),
|
|
789
819
|
...findCredentials(block, "sessionManager"),
|
|
790
820
|
];
|
|
791
821
|
}
|
|
@@ -68,7 +68,12 @@ the manager.
|
|
|
68
68
|
claims go through `lisa-tracker-claim`. Never call a vendor API around them.
|
|
69
69
|
- **"Me".** Resolve once per manager run and store it with `ledger control --me <id>`: GitHub
|
|
70
70
|
`gh api user --jq .login`; Jira the accountId from the `lisa-atlassian-access` identity probe;
|
|
71
|
-
Linear `viewer.id` through `lisa-linear-access`.
|
|
71
|
+
Linear `viewer.id` through `lisa-linear-access`. "Me" is whoever the tracker credential belongs
|
|
72
|
+
to, which is often an agent account.
|
|
73
|
+
- **Operators.** `sessionManager.operatorIds` names the people the manager works for (the same
|
|
74
|
+
kind of id as "me": GitHub login, Jira accountId, Linear user id). Their assigned items come
|
|
75
|
+
first, then items assigned to "me", then unassigned. Items assigned to anyone else are never
|
|
76
|
+
dispatched. Never treat an operator's item as someone else's.
|
|
72
77
|
- **Workers** start only through `spawn`. The worker prompt is exactly
|
|
73
78
|
`/lisa:implement <REF> managedBy=<manager-name>`; `lisa-implement` owns what `managedBy=` means.
|
|
74
79
|
Never start a worker another way, and never reuse a worker for another item or a new attempt.
|
|
@@ -209,8 +214,8 @@ Skip while `capacity` reports `paused`. Otherwise:
|
|
|
209
214
|
Raw Jira issues carry `key` and `fields` (summary, priority, assignee, created, labels,
|
|
210
215
|
description, reporter, issuetype, subtasks); raw Linear nodes carry identifier, title, priority,
|
|
211
216
|
createdAt, assignee, labels, description, creator and children with their state type.
|
|
212
|
-
- `ordered` is the dispatch order
|
|
213
|
-
needs no action.
|
|
217
|
+
- `ordered` is the dispatch order: assigned to an operator, then to "me", then unassigned; each
|
|
218
|
+
group by priority, then age. `excluded` needs no action.
|
|
214
219
|
2. **While `capacity` says `can_spawn`** and items remain, take the next item that has no pending
|
|
215
220
|
question in the ledger:
|
|
216
221
|
1. Read it fully (`lisa-tracker-read`): title, description, labels, project, parent, comments.
|
|
@@ -225,9 +230,11 @@ Skip while `capacity` reports `paused`. Otherwise:
|
|
|
225
230
|
`cd "$(node "$SM/session-manager.mjs" repo-dir --repo <key> --raw --project-root "$ROOT")"`
|
|
226
231
|
(omit `--repo` on a single-repo project), invoke `lisa-tracker-claim <ref>`, then
|
|
227
232
|
`cd "$ROOT" && pwd` and confirm the path. Continue only on `claimed` or `reused` with the
|
|
228
|
-
assignee equal to "me"
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
assignee equal to "me" or to one of `sessionManager.operatorIds` (the claim assigns only an
|
|
234
|
+
unassigned item, so an operator's item keeps the operator); anything else means skip. Then
|
|
235
|
+
re-read the item with `lisa-tracker-read`: if the re-read shows an assignee that is neither
|
|
236
|
+
"me" nor an operator, or shows the state did not change to the `claimed` role, skip it
|
|
237
|
+
(someone else holds it, or the claim did not land).
|
|
231
238
|
Never spawn on the claim's word alone.
|
|
232
239
|
4. **Spawn.** `spawn <ref> [--repo <key>]`.
|
|
233
240
|
- exit 0: post `[<REF>] started in <repo> (<name>)` to Telegram.
|
package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md
CHANGED
|
@@ -18,9 +18,10 @@ session list from `claude agents`; and the project's own Telegram chat.
|
|
|
18
18
|
|
|
19
19
|
## Candidate selection
|
|
20
20
|
|
|
21
|
-
Leaf items in the `ready` role, ordered: assigned to
|
|
22
|
-
then
|
|
23
|
-
|
|
21
|
+
Leaf items in the `ready` role, ordered: assigned to an operator (`sessionManager.operatorIds`),
|
|
22
|
+
then to the manager's own tracker identity, then unassigned; within each, by priority, then oldest.
|
|
23
|
+
Items assigned to someone else, containers, human-held items and (when configured) items by
|
|
24
|
+
untrusted authors are skipped. As many as the machine has capacity for.
|
|
24
25
|
|
|
25
26
|
## Scope/bounds
|
|
26
27
|
|
|
@@ -123,9 +123,11 @@ function readEnvelopeQueue(ctx, tracker) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* `queue --tracker <github|jira|linear>`: the Ready queue,
|
|
126
|
+
* `queue --tracker <github|jira|linear>`: the Ready queue, the operator's
|
|
127
|
+
* items first (`sessionManager.operatorIds`), then mine, then unassigned.
|
|
127
128
|
* @param {object} ctx - CLI context
|
|
128
|
-
* @returns {Promise<{ json: object }>}
|
|
129
|
+
* @returns {Promise<{ json: object }>}
|
|
130
|
+
* `{tracker,me,operators,total,pages,ordered,excluded}`
|
|
129
131
|
*/
|
|
130
132
|
export async function queue(ctx) {
|
|
131
133
|
const tracker = ctx.values.tracker;
|
|
@@ -139,10 +141,14 @@ export async function queue(ctx) {
|
|
|
139
141
|
? fetchGithubQueue(ctx, project)
|
|
140
142
|
: readEnvelopeQueue(ctx, tracker);
|
|
141
143
|
const me = ctx.values.me ?? (await readLedger(ctx.root)).control.me ?? null;
|
|
144
|
+
const operators = config?.operatorIds ?? [];
|
|
142
145
|
const order = orderReadyQueue(items, {
|
|
143
146
|
me,
|
|
147
|
+
operators,
|
|
144
148
|
trustedAuthorIds: config?.trustedAuthorIds ?? undefined,
|
|
145
149
|
humanNeededLabel: humanNeededLabel(project, tracker),
|
|
146
150
|
});
|
|
147
|
-
return {
|
|
151
|
+
return {
|
|
152
|
+
json: { tracker, me, operators, total: items.length, pages, ...order },
|
|
153
|
+
};
|
|
148
154
|
}
|
|
@@ -111,7 +111,10 @@ Per cycle
|
|
|
111
111
|
queue --tracker jira|linear --input <file> [--me <id>]
|
|
112
112
|
<file> = {complete:true,pages,items:[raw vendor issues]}; exit 2 unless
|
|
113
113
|
complete is exactly true.
|
|
114
|
-
Both print {tracker,me,total,pages,ordered,excluded:[{ref,
|
|
114
|
+
Both print {tracker,me,operators,total,pages,ordered,excluded:[{ref,
|
|
115
|
+
reason}]}. ordered: items assigned to sessionManager.operatorIds
|
|
116
|
+
first, then to --me, then unassigned; each group by priority, then age.
|
|
117
|
+
Items assigned to anyone else are excluded (assigned-to-other).
|
|
115
118
|
--me defaults to the ledger's control.me.
|
|
116
119
|
ledger get [<ref>]
|
|
117
120
|
ledger record-spawn <ref> --worker <name> [--session-id <id>] [--repo <key>]
|
|
@@ -66,6 +66,7 @@ export const SESSION_MANAGER_DEFAULTS = Object.freeze({
|
|
|
66
66
|
* stall: StallBlock,
|
|
67
67
|
* notify: NotifyBlock,
|
|
68
68
|
* trustedAuthorIds: string[] | null,
|
|
69
|
+
* operatorIds?: string[] | null,
|
|
69
70
|
* }} SessionManagerConfig
|
|
70
71
|
* @typedef {{
|
|
71
72
|
* name: string,
|
|
@@ -120,6 +121,9 @@ export function resolveSessionManagerConfig(globalConfig, localConfig) {
|
|
|
120
121
|
...merged,
|
|
121
122
|
repos,
|
|
122
123
|
trustedAuthorIds: merged.trustedAuthorIds ?? null,
|
|
124
|
+
// Left as declared: absent stays undefined and an explicit null stays null,
|
|
125
|
+
// so validation can refuse the null instead of reading it as "no operators".
|
|
126
|
+
operatorIds: merged.operatorIds,
|
|
123
127
|
};
|
|
124
128
|
}
|
|
125
129
|
|
|
@@ -56,7 +56,19 @@ writes `$SM`, and pass `--project-root "$ROOT"` (the absolute project root) on e
|
|
|
56
56
|
bypass-permissions worker; say so plainly. The list holds stable tracker user ids as strings,
|
|
57
57
|
never logins: the GitHub numeric user id (`gh api users/<login> --jq .id`), the Jira
|
|
58
58
|
`accountId`, or the Linear user id. A login matches no one and silently excludes every item.
|
|
59
|
-
4. `
|
|
59
|
+
4. Set `sessionManager.operatorIds` to a non-empty **array** holding the operator's own tracker
|
|
60
|
+
user id, e.g. `"operatorIds": ["<operator-id>"]` (a bare string fails `validate`). Look up the
|
|
61
|
+
OPERATOR, not the credential: `gh api user` / `viewer.id` return whoever the tracker credential
|
|
62
|
+
belongs to, which is the agent account in exactly the case this key exists for. The manager dispatches
|
|
63
|
+
the operator's assigned items first, then items assigned to the tracker credential's identity
|
|
64
|
+
("me"), then unassigned ones, and skips items assigned to anyone else. When the tracker
|
|
65
|
+
credential is not the operator's (an agent or bot account), leaving this unset means the
|
|
66
|
+
operator's own Ready items are never dispatched; say so plainly. Unlike `trustedAuthorIds`, this
|
|
67
|
+
is matched against the assignee, exactly and case-sensitively, so use the same kind of id as
|
|
68
|
+
"me", spelled as the tracker API returns it: the GitHub **login** (`gh api users/<login> --jq
|
|
69
|
+
.login`), the Jira `accountId`, or the Linear user id (the operator's entry in the team's member
|
|
70
|
+
list, or `viewer.id` only when the operator runs the lookup with their own credential).
|
|
71
|
+
5. `node "$SM/session-manager.mjs" validate --project-root "$ROOT"` must print `valid: true`.
|
|
60
72
|
|
|
61
73
|
## Step 3: Store the bot token
|
|
62
74
|
|
|
@@ -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(
|
|
@@ -727,6 +727,35 @@ function findCredentials(value, path) {
|
|
|
727
727
|
);
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Validate `sessionManager.operatorIds`: the people the manager works for,
|
|
732
|
+
* whose assigned items it dispatches first. Matched against the assignee the
|
|
733
|
+
* way "me" is, so the GitHub entry is a login, not the numeric id
|
|
734
|
+
* `trustedAuthorIds` takes. An empty list reads as configured but names no
|
|
735
|
+
* one, so it is refused like an empty `trustedAuthorIds`.
|
|
736
|
+
* @param {unknown} ids The value, if present.
|
|
737
|
+
* @returns {string[]} Problems found.
|
|
738
|
+
*/
|
|
739
|
+
function validateOperatorIds(ids) {
|
|
740
|
+
if (ids === undefined) return [];
|
|
741
|
+
if (
|
|
742
|
+
!Array.isArray(ids) ||
|
|
743
|
+
!ids.every(id => typeof id === "string" && id.trim() !== "")
|
|
744
|
+
) {
|
|
745
|
+
return [
|
|
746
|
+
`sessionManager.operatorIds must be an array of tracker user ids as ` +
|
|
747
|
+
`strings, matched against the assignee: the GitHub login, the Jira ` +
|
|
748
|
+
`accountId or the Linear user id.`,
|
|
749
|
+
];
|
|
750
|
+
}
|
|
751
|
+
return ids.length === 0
|
|
752
|
+
? [
|
|
753
|
+
`sessionManager.operatorIds must name at least one person. Remove ` +
|
|
754
|
+
`the key instead if the manager works only for its own identity.`,
|
|
755
|
+
]
|
|
756
|
+
: [];
|
|
757
|
+
}
|
|
758
|
+
|
|
730
759
|
/**
|
|
731
760
|
* Validate the opt-in `sessionManager` block.
|
|
732
761
|
*
|
|
@@ -786,6 +815,7 @@ export function validateSessionManager(block) {
|
|
|
786
815
|
`if every author's item may be dispatched.`,
|
|
787
816
|
]
|
|
788
817
|
: []),
|
|
818
|
+
...validateOperatorIds(block.operatorIds),
|
|
789
819
|
...findCredentials(block, "sessionManager"),
|
|
790
820
|
];
|
|
791
821
|
}
|
|
@@ -66,7 +66,12 @@ the manager.
|
|
|
66
66
|
claims go through `lisa-tracker-claim`. Never call a vendor API around them.
|
|
67
67
|
- **"Me".** Resolve once per manager run and store it with `ledger control --me <id>`: GitHub
|
|
68
68
|
`gh api user --jq .login`; Jira the accountId from the `lisa-atlassian-access` identity probe;
|
|
69
|
-
Linear `viewer.id` through `lisa-linear-access`.
|
|
69
|
+
Linear `viewer.id` through `lisa-linear-access`. "Me" is whoever the tracker credential belongs
|
|
70
|
+
to, which is often an agent account.
|
|
71
|
+
- **Operators.** `sessionManager.operatorIds` names the people the manager works for (the same
|
|
72
|
+
kind of id as "me": GitHub login, Jira accountId, Linear user id). Their assigned items come
|
|
73
|
+
first, then items assigned to "me", then unassigned. Items assigned to anyone else are never
|
|
74
|
+
dispatched. Never treat an operator's item as someone else's.
|
|
70
75
|
- **Workers** start only through `spawn`. The worker prompt is exactly
|
|
71
76
|
`/lisa:implement <REF> managedBy=<manager-name>`; `lisa-implement` owns what `managedBy=` means.
|
|
72
77
|
Never start a worker another way, and never reuse a worker for another item or a new attempt.
|
|
@@ -207,8 +212,8 @@ Skip while `capacity` reports `paused`. Otherwise:
|
|
|
207
212
|
Raw Jira issues carry `key` and `fields` (summary, priority, assignee, created, labels,
|
|
208
213
|
description, reporter, issuetype, subtasks); raw Linear nodes carry identifier, title, priority,
|
|
209
214
|
createdAt, assignee, labels, description, creator and children with their state type.
|
|
210
|
-
- `ordered` is the dispatch order
|
|
211
|
-
needs no action.
|
|
215
|
+
- `ordered` is the dispatch order: assigned to an operator, then to "me", then unassigned; each
|
|
216
|
+
group by priority, then age. `excluded` needs no action.
|
|
212
217
|
2. **While `capacity` says `can_spawn`** and items remain, take the next item that has no pending
|
|
213
218
|
question in the ledger:
|
|
214
219
|
1. Read it fully (`lisa-tracker-read`): title, description, labels, project, parent, comments.
|
|
@@ -223,9 +228,11 @@ Skip while `capacity` reports `paused`. Otherwise:
|
|
|
223
228
|
`cd "$(node "$SM/session-manager.mjs" repo-dir --repo <key> --raw --project-root "$ROOT")"`
|
|
224
229
|
(omit `--repo` on a single-repo project), invoke `lisa-tracker-claim <ref>`, then
|
|
225
230
|
`cd "$ROOT" && pwd` and confirm the path. Continue only on `claimed` or `reused` with the
|
|
226
|
-
assignee equal to "me"
|
|
227
|
-
|
|
228
|
-
|
|
231
|
+
assignee equal to "me" or to one of `sessionManager.operatorIds` (the claim assigns only an
|
|
232
|
+
unassigned item, so an operator's item keeps the operator); anything else means skip. Then
|
|
233
|
+
re-read the item with `lisa-tracker-read`: if the re-read shows an assignee that is neither
|
|
234
|
+
"me" nor an operator, or shows the state did not change to the `claimed` role, skip it
|
|
235
|
+
(someone else holds it, or the claim did not land).
|
|
229
236
|
Never spawn on the claim's word alone.
|
|
230
237
|
4. **Spawn.** `spawn <ref> [--repo <key>]`.
|
|
231
238
|
- exit 0: post `[<REF>] started in <repo> (<name>)` to Telegram.
|
|
@@ -18,9 +18,10 @@ session list from `claude agents`; and the project's own Telegram chat.
|
|
|
18
18
|
|
|
19
19
|
## Candidate selection
|
|
20
20
|
|
|
21
|
-
Leaf items in the `ready` role, ordered: assigned to
|
|
22
|
-
then
|
|
23
|
-
|
|
21
|
+
Leaf items in the `ready` role, ordered: assigned to an operator (`sessionManager.operatorIds`),
|
|
22
|
+
then to the manager's own tracker identity, then unassigned; within each, by priority, then oldest.
|
|
23
|
+
Items assigned to someone else, containers, human-held items and (when configured) items by
|
|
24
|
+
untrusted authors are skipped. As many as the machine has capacity for.
|
|
24
25
|
|
|
25
26
|
## Scope/bounds
|
|
26
27
|
|
|
@@ -123,9 +123,11 @@ function readEnvelopeQueue(ctx, tracker) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* `queue --tracker <github|jira|linear>`: the Ready queue,
|
|
126
|
+
* `queue --tracker <github|jira|linear>`: the Ready queue, the operator's
|
|
127
|
+
* items first (`sessionManager.operatorIds`), then mine, then unassigned.
|
|
127
128
|
* @param {object} ctx - CLI context
|
|
128
|
-
* @returns {Promise<{ json: object }>}
|
|
129
|
+
* @returns {Promise<{ json: object }>}
|
|
130
|
+
* `{tracker,me,operators,total,pages,ordered,excluded}`
|
|
129
131
|
*/
|
|
130
132
|
export async function queue(ctx) {
|
|
131
133
|
const tracker = ctx.values.tracker;
|
|
@@ -139,10 +141,14 @@ export async function queue(ctx) {
|
|
|
139
141
|
? fetchGithubQueue(ctx, project)
|
|
140
142
|
: readEnvelopeQueue(ctx, tracker);
|
|
141
143
|
const me = ctx.values.me ?? (await readLedger(ctx.root)).control.me ?? null;
|
|
144
|
+
const operators = config?.operatorIds ?? [];
|
|
142
145
|
const order = orderReadyQueue(items, {
|
|
143
146
|
me,
|
|
147
|
+
operators,
|
|
144
148
|
trustedAuthorIds: config?.trustedAuthorIds ?? undefined,
|
|
145
149
|
humanNeededLabel: humanNeededLabel(project, tracker),
|
|
146
150
|
});
|
|
147
|
-
return {
|
|
151
|
+
return {
|
|
152
|
+
json: { tracker, me, operators, total: items.length, pages, ...order },
|
|
153
|
+
};
|
|
148
154
|
}
|
|
@@ -111,7 +111,10 @@ Per cycle
|
|
|
111
111
|
queue --tracker jira|linear --input <file> [--me <id>]
|
|
112
112
|
<file> = {complete:true,pages,items:[raw vendor issues]}; exit 2 unless
|
|
113
113
|
complete is exactly true.
|
|
114
|
-
Both print {tracker,me,total,pages,ordered,excluded:[{ref,
|
|
114
|
+
Both print {tracker,me,operators,total,pages,ordered,excluded:[{ref,
|
|
115
|
+
reason}]}. ordered: items assigned to sessionManager.operatorIds
|
|
116
|
+
first, then to --me, then unassigned; each group by priority, then age.
|
|
117
|
+
Items assigned to anyone else are excluded (assigned-to-other).
|
|
115
118
|
--me defaults to the ledger's control.me.
|
|
116
119
|
ledger get [<ref>]
|
|
117
120
|
ledger record-spawn <ref> --worker <name> [--session-id <id>] [--repo <key>]
|
|
@@ -66,6 +66,7 @@ export const SESSION_MANAGER_DEFAULTS = Object.freeze({
|
|
|
66
66
|
* stall: StallBlock,
|
|
67
67
|
* notify: NotifyBlock,
|
|
68
68
|
* trustedAuthorIds: string[] | null,
|
|
69
|
+
* operatorIds?: string[] | null,
|
|
69
70
|
* }} SessionManagerConfig
|
|
70
71
|
* @typedef {{
|
|
71
72
|
* name: string,
|
|
@@ -120,6 +121,9 @@ export function resolveSessionManagerConfig(globalConfig, localConfig) {
|
|
|
120
121
|
...merged,
|
|
121
122
|
repos,
|
|
122
123
|
trustedAuthorIds: merged.trustedAuthorIds ?? null,
|
|
124
|
+
// Left as declared: absent stays undefined and an explicit null stays null,
|
|
125
|
+
// so validation can refuse the null instead of reading it as "no operators".
|
|
126
|
+
operatorIds: merged.operatorIds,
|
|
123
127
|
};
|
|
124
128
|
}
|
|
125
129
|
|
|
@@ -54,7 +54,19 @@ writes `$SM`, and pass `--project-root "$ROOT"` (the absolute project root) on e
|
|
|
54
54
|
bypass-permissions worker; say so plainly. The list holds stable tracker user ids as strings,
|
|
55
55
|
never logins: the GitHub numeric user id (`gh api users/<login> --jq .id`), the Jira
|
|
56
56
|
`accountId`, or the Linear user id. A login matches no one and silently excludes every item.
|
|
57
|
-
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`.
|
|
58
70
|
|
|
59
71
|
## Step 3: Store the bot token
|
|
60
72
|
|
|
@@ -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(
|