@sjawhar/claude-envoy 1.12.1 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.omp-plugin/plugin.json +1 -1
- package/dist/dispatch.js +58 -27
- package/dist/envoy-channel.js +111 -55
- package/dist/session-hook.js +53 -23
- package/legion/skills/legion-architect/SKILL.md +10 -8
- package/legion/skills/legion-architect/references/acceptance.md +1 -1
- package/legion/skills/legion-architect/references/decomposition.md +27 -18
- package/legion/skills/legion-architect/references/design-gate.md +4 -4
- package/legion/skills/legion-architect/references/wake-routing.md +5 -5
- package/legion/skills/legion-controller/SKILL.md +5 -2
- package/legion/skills/legion-controller/references/daily-report.md +7 -0
- package/legion/skills/legion-controller/references/weekly-audit.md +146 -0
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +2 -2
- package/skills/dispatch-first/SKILL.md +3 -7
- package/skills/dispatch-writing-specs/SKILL.md +1 -1
- package/src/session-title.ts +3 -42
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# The weekly audit
|
|
2
|
+
|
|
3
|
+
Read this on your first turn of each UTC week, whatever woke you, and run all four passes below
|
|
4
|
+
before you post the day's report. They are the week's own numbers, beside the daily ones. The
|
|
5
|
+
fourth pass, the duplicate audit, serves R8 of dispatch://INFRA-1160 and is not optional.
|
|
6
|
+
|
|
7
|
+
## The week
|
|
8
|
+
|
|
9
|
+
The week's boundary is your first turn on or after Monday 00:00 UTC. The window is the seven days
|
|
10
|
+
before that boundary: `date -u +%Y-%m-%dT%H:%M:%SZ` prints the time now, and the window runs from
|
|
11
|
+
seven days before it to it. Name the window's two times in the report, so a reader can recount.
|
|
12
|
+
|
|
13
|
+
You already ran this week when the report issue carries a message of yours from on or after the
|
|
14
|
+
boundary whose text names the window. Read the report issue before you start
|
|
15
|
+
(`skill://legion-controller/references/daily-report.md`).
|
|
16
|
+
|
|
17
|
+
## 1. The week's forced creates
|
|
18
|
+
|
|
19
|
+
A create that went past Dispatch's near-copy refusal with `--force` carries `forced` on its
|
|
20
|
+
`issue.created` event, with `past` naming the issues the refusal named. A forced create the
|
|
21
|
+
refusal would not have stopped carries nothing, so this is a count of overrides, not of uses of
|
|
22
|
+
`--force`. A `past` that is the empty list is a create whose check did not answer: report those
|
|
23
|
+
apart, since nobody forced past anything there. Each form reads as itself on the `issue.created`
|
|
24
|
+
line: an override ends `forced past <keys>`, and a create whose check did not answer ends
|
|
25
|
+
`forced, the near-copy check did not answer`.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
dispatch issues --project <PROJECT> --all
|
|
29
|
+
dispatch read --issue <KEY> --all-events # read the issue.created line
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--all-events` is not optional: a bare `dispatch read` prints the last ten events alone, and on
|
|
33
|
+
any issue with more than ten the `issue.created` line is not printed at all.
|
|
34
|
+
|
|
35
|
+
Keep the issues created inside the window, read each one's `issue.created` line, and count by
|
|
36
|
+
filer. The line names its writer as `session <id> (<the session's title>)`, and the filer is that
|
|
37
|
+
title cut at its last middle dot (`·`), so `Legion implementer · INFRA-1160` counts as
|
|
38
|
+
`Legion implementer`, with every person counted together as one filer. A session that carries no
|
|
39
|
+
title prints its id alone and is counted by that id. Report, per filer, how many creates forced
|
|
40
|
+
past a refusal and which issues each named, then the no-answer creates as their own line.
|
|
41
|
+
|
|
42
|
+
The recount is the same two commands: name them in the report beside the number.
|
|
43
|
+
|
|
44
|
+
## 2. The week's tree sizes
|
|
45
|
+
|
|
46
|
+
A Legion tree is an open issue with the `legion` label and no ancestor carrying it, with its
|
|
47
|
+
descendants. A tree closed inside the window counts at its close.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
dispatch issues --project <PROJECT> --label legion --all
|
|
51
|
+
dispatch read --issue <KEY> # Referenced by: child_of names its children
|
|
52
|
+
dispatch issues --project <PROJECT> --parent <KEY> # the same children, one level at a time
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`Links: child_of` on that read gives the issue's own parent, never its children, so walk down
|
|
56
|
+
instead: read the root, take each `child_of` row under `Referenced by:` as a child, and read each
|
|
57
|
+
child the same way until a read names none. `dispatch issues --parent` lists one level in one
|
|
58
|
+
request and is the shorter walk on a wide tree.
|
|
59
|
+
|
|
60
|
+
Count each tree's descendants at any depth, and report the trees in five buckets: 0, 1-2, 3-5,
|
|
61
|
+
6-10, and 11 or more. Count the parked descendants of each bucket too: a descendant in `backlog`
|
|
62
|
+
or `icebox`. Name the recount commands beside the numbers, since a reader checks them against
|
|
63
|
+
Dispatch's own parent links.
|
|
64
|
+
|
|
65
|
+
The report and, when you lead a stream that keeps a scoreboard, the stream's scoreboard artifact
|
|
66
|
+
both carry the two measures, under the keys `forced_creates_by_filer` and `tree_sizes`.
|
|
67
|
+
|
|
68
|
+
## 3. The nudges
|
|
69
|
+
|
|
70
|
+
Ask the architect of each tree the counts name to fold what can ride in an open pull request.
|
|
71
|
+
Both rules below count only a tree with at least one open descendant, a descendant whose status
|
|
72
|
+
is not `done`. A tree whose descendants are all closed has nothing left to fold, and a nudge
|
|
73
|
+
sent to one asks its architect to fold work that is already finished.
|
|
74
|
+
|
|
75
|
+
- a tree with at least one open descendant, and more than 8 descendants in all or more than 2
|
|
76
|
+
levels below its root;
|
|
77
|
+
- a tree with at least one open descendant that filed a child no red-team verdict names. The
|
|
78
|
+
verdict is a comment on the root whose first line reads `Red-team verdict for <root key>:`
|
|
79
|
+
(`skill://legion-architect/references/decomposition.md`). Read the root's comments with
|
|
80
|
+
`dispatch read --issue <ROOT> --all-events` and the replies under each verdict comment; a child
|
|
81
|
+
named in none of them was filed without the check.
|
|
82
|
+
|
|
83
|
+
Count no child created before that first line existed. The architects write it from the Legion
|
|
84
|
+
release that carries this file, so every child filed before then has no verdict comment to
|
|
85
|
+
find and is not evidence of a skipped check: read the release's own publication time from its
|
|
86
|
+
tag and leave out every child created at or before it. Without that, the first run of this
|
|
87
|
+
pass nudges nearly every tree that has children. Until a release carries this file, there is
|
|
88
|
+
no such tag to read: when `git tag --contains <the commit that added this file>` names no
|
|
89
|
+
release, the cutoff is the time of your run, so every child that exists counts as filed
|
|
90
|
+
before the rule and no tree is nudged for a missing verdict yet.
|
|
91
|
+
|
|
92
|
+
Each nudge is one `dispatch comment` on the tree's root, naming what you counted and asking the
|
|
93
|
+
architect to fold what can ride in an open pull request of the tree. End the comment with
|
|
94
|
+
`descendants: <n>, levels: <m>`, the counts you sent it at. When the daemon's state names an
|
|
95
|
+
architect for the tree (`read_state`'s `issues.<KEY>`), send the same text to that architect's
|
|
96
|
+
role topic with `envoy_publish`.
|
|
97
|
+
|
|
98
|
+
Send no new comment to a tree whose architect replied in the thread of your newest nudge comment
|
|
99
|
+
there, until the tree's descendants pass the number that comment ends with. A tree whose children
|
|
100
|
+
were all filed before the verdict's first line existed has no such comment and is nudged once;
|
|
101
|
+
the architect's reply then settles it until the tree grows.
|
|
102
|
+
|
|
103
|
+
List every nudge you sent in the day's report.
|
|
104
|
+
|
|
105
|
+
## 4. The duplicate audit
|
|
106
|
+
|
|
107
|
+
For each open issue of the project, read the related items of its title and spec through the
|
|
108
|
+
no-write related route, leaving the issue, its ancestors and its descendants out, and judge
|
|
109
|
+
whether each pair the two issues' own records do not already settle is the same work.
|
|
110
|
+
|
|
111
|
+
The project holds hundreds of open issues and one related read takes about a second, so a serial
|
|
112
|
+
pass is ten minutes of HTTP before any judging. Shard the open list and dispatch one
|
|
113
|
+
`task(agent="scout")` per shard of at most 50 issues, then `wait` for them: each returns the pairs
|
|
114
|
+
it found with its evidence, and you judge and fold them yourself.
|
|
115
|
+
|
|
116
|
+
Give each scout the shard's keys and these commands:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
dispatch read --issue <KEY> # title, spec artifact, Links:, Referenced by:
|
|
120
|
+
dispatch doc-read --issue <KEY> # the spec text the related read uses
|
|
121
|
+
dispatch related --project <PROJECT> --title '<its title>' --text-file - \
|
|
122
|
+
--exclude-issue <KEY> --exclude-issue <each ancestor> --exclude-issue <each descendant>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`--exclude-issue` is repeatable and takes up to 50 issues; the issue itself, its ancestors and its
|
|
126
|
+
descendants all go in one call. The read names both: a `child_of` row under `Links:` is the
|
|
127
|
+
issue's own parent, read again on that parent for the next ancestor up, and each `child_of` row
|
|
128
|
+
under `Referenced by:` is a child, read again on each child for the next level down. Ask each
|
|
129
|
+
scout for, per issue, every related item that is an issue with the reason it looks like the same
|
|
130
|
+
work, and nothing else: no comment, no close, no message.
|
|
131
|
+
|
|
132
|
+
A pair is settled, and is skipped rather than proposed, when the two issues already say so: one
|
|
133
|
+
was closed as a duplicate of the other, one was reopened after such a close, or one was made a
|
|
134
|
+
child of the other. Read the first two from `dispatch read --issue <KEY> --all-events`'s
|
|
135
|
+
`Events:`: the close's reason is the `message.created` line just before `issue.closed`, and a
|
|
136
|
+
reopen is an `issue.updated` line that moves the status off `done`. No `Events:` line reports a
|
|
137
|
+
parent change, so read the third from the same read's `Links:` and `Referenced by:`.
|
|
138
|
+
|
|
139
|
+
Fold each pair you judge the same work by the duplicate rule of `skill://dispatch-first`: keep the
|
|
140
|
+
issue with the spec, copy to it what only the other has, and close the other
|
|
141
|
+
(`--status done --reason 'Duplicate of dispatch://KEY.'`). An issue carrying the `legion` label,
|
|
142
|
+
or another holder's claim, is not yours to close: comment the kept issue's name on it instead. A
|
|
143
|
+
pair inside a Legion tree goes to that tree's architect with `envoy_publish`, which decides it.
|
|
144
|
+
|
|
145
|
+
The report lists every pair you judged the same work, with what you did about it, and every pair
|
|
146
|
+
you skipped with what settles it: the event, or the parent link.
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ Dispatch is a record for the people who decide, never a log of your work.
|
|
|
9
9
|
|
|
10
10
|
- Use the `dispatch` command; the HTTP API only for what no command does. `dispatch <command> --help` gives its rules and an example.
|
|
11
11
|
- Put long text on stdin: `--<field>-file -` with a quoted here-document (`<<'EOF'`); give object and list fields as JSON.
|
|
12
|
-
- Exit 1 is a refusal, 2 a usage error. A refusal lists
|
|
12
|
+
- Exit 1 is a refusal, 2 a usage error. A refusal lists every problem: fix all, call once.
|
|
13
13
|
- A Dispatch command hard to use, or storing or losing something wrongly, is a defect to report, not work around: open an issue in the owning project's code and tell that project's controller; a Legion role tells its architect (`skill://legion-worker/references/findings.md`).
|
|
14
14
|
|
|
15
15
|
## Asks and ask blocks
|
|
@@ -35,7 +35,7 @@ The question and the recommendation, as `skill://dispatch-writing-specs` writes
|
|
|
35
35
|
|
|
36
36
|
## Choosing what to work on
|
|
37
37
|
|
|
38
|
-
-
|
|
38
|
+
- Finishing an issue, take the top `todo` one by priority then board rank, per project.
|
|
39
39
|
- `todo` means specced and waiting on nothing; one that waits goes in `backlog`, saying on what.
|
|
40
40
|
- Hold at most three issues in flight; move the rest to `backlog`, saying where each is.
|
|
41
41
|
- Never reorder the board: one agent keeps each project's order (`issue-update --before`/`--after`). If the top looks wrong, tell `notifications.role.legion-<project>-controller`.
|
|
@@ -5,19 +5,17 @@ description: "Use when a session has the dispatch command, before you plan, file
|
|
|
5
5
|
|
|
6
6
|
# Dispatch first
|
|
7
7
|
|
|
8
|
-
Dispatch holds the open issues, their specs and people's answers.
|
|
9
|
-
|
|
10
8
|
## Search before you act
|
|
11
9
|
|
|
12
10
|
- Before you plan, file an issue, ask, post a finding or start work, run `dispatch search`.
|
|
13
|
-
- Each query word must match. Use two or three
|
|
11
|
+
- Each query word must match. Use two or three: the thing and what is wrong. Never paste a draft.
|
|
14
12
|
|
|
15
13
|
```bash
|
|
16
14
|
dispatch search --query 'broadcast send order'
|
|
17
15
|
dispatch search --query 'reviewer threads OR review comments'
|
|
18
16
|
```
|
|
19
17
|
|
|
20
|
-
- No hits: remove a word before
|
|
18
|
+
- No hits: remove a word before adding one. Try two or three wordings before deciding nothing exists.
|
|
21
19
|
- Open each hit that can be yours with `dispatch read --issue KEY`, then its parent (`child_of` under `Links:`, or a document's `Issue:`), with its status and recent events.
|
|
22
20
|
|
|
23
21
|
## What to do with what you find
|
|
@@ -26,7 +24,7 @@ dispatch search --query 'reviewer threads OR review comments'
|
|
|
26
24
|
|
|
27
25
|
- **Work already tracked:** put the finding on that issue. File a new issue only when no hit covers the work, and cite the nearest one you ruled out (`dispatch://KEY`).
|
|
28
26
|
- **Question already decided:** the answered ask is the record. Cite it (`dispatch://KEY/ask/<id>`). Do not ask again.
|
|
29
|
-
- **Duplicate found:**
|
|
27
|
+
- **Duplicate found, or a likely duplicate a create names:** read it; fold only when it is the same work. Keep the issue with the spec, copy to it what only the duplicate has, and close the duplicate (`--status done --reason 'Duplicate of dispatch://KEY.'`). When it has the `legion` label or another holder's claim, comment the kept issue's name on it instead.
|
|
30
28
|
|
|
31
29
|
## Research before you ask
|
|
32
30
|
|
|
@@ -36,8 +34,6 @@ dispatch search --query 'reviewer threads OR review comments'
|
|
|
36
34
|
|
|
37
35
|
## Reach a person only through Dispatch
|
|
38
36
|
|
|
39
|
-
Reach a person through an ask, an ask block or an approval request.
|
|
40
|
-
|
|
41
37
|
- The moment a person blocks your work, open an ask: an ask block for a design question its document records, else `dispatch ask`. A blocker that is not an open ask does not exist for them. A Legion phase worker sends decisions to its architect instead. Before you say you wait on a person, run `dispatch open-asks`.
|
|
42
38
|
- A result a person asked for is the same: a document or artifact notifies nobody. End that document with an ask block asking their next step, else `dispatch ask`.
|
|
43
39
|
- Record every request you accept in Dispatch before you start it: an issue, or a line in your issue's `open-items.md` artifact. Before you end a turn, read that record. Each item is done, in progress, or an open ask. Never drop one.
|
|
@@ -28,7 +28,7 @@ Part | Content
|
|
|
28
28
|
`Design` | How it meets them; a value and its reason for each limit, size and time
|
|
29
29
|
`Decisions` | One line: `The decisions are in dispatch://KEY/artifact/decisions-md.`
|
|
30
30
|
|
|
31
|
-
`serves` and `needs` name a block of this issue or an ancestor. Another agent can build and check the work from these alone. A small change gets a few sentences per part, no matrix; split a large design into
|
|
31
|
+
`serves` and `needs` name a block of this issue or an ancestor. Another agent can build and check the work from these alone. A small change gets a few sentences per part, no matrix; split a large design into children.
|
|
32
32
|
|
|
33
33
|
## Rules for the text
|
|
34
34
|
|
package/src/session-title.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process"
|
|
2
|
-
import { readFileSync } from "node:fs"
|
|
3
1
|
import { basename } from "node:path"
|
|
2
|
+
import { isAncestor } from "@legion/envoy-client/process-ancestry"
|
|
3
|
+
import { tmuxServer } from "@legion/envoy-client/tmux-server"
|
|
4
4
|
import { configuredValue } from "./claude-session"
|
|
5
5
|
|
|
6
6
|
export interface SessionTitleEnvironment {
|
|
@@ -48,9 +48,6 @@ const TMUX_PANE_FORMAT = [
|
|
|
48
48
|
const TMUX_TIMEOUT_MS = 1_000
|
|
49
49
|
const SHORT_ID_LENGTH = 8
|
|
50
50
|
|
|
51
|
-
/** How many parents `isAncestor` follows before it stops. */
|
|
52
|
-
const MAX_ANCESTORS = 64
|
|
53
|
-
|
|
54
51
|
/** A terminal control sequence (a colour code: ESC [ parameters final byte). Built from the ESC
|
|
55
52
|
* code, since a control character in a regular expression literal is a lint error. */
|
|
56
53
|
const CSI_SEQUENCE = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-?]*[ -/]*[@-~]`, "gu")
|
|
@@ -80,39 +77,6 @@ function visible(value: string): boolean {
|
|
|
80
77
|
)
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
/** The parent of process `pid`, or undefined when it cannot be read. */
|
|
84
|
-
function parentPid(pid: number): number | undefined {
|
|
85
|
-
try {
|
|
86
|
-
if (process.platform === "linux") {
|
|
87
|
-
// The command name, in parentheses, may hold spaces; the parent is the second field after it.
|
|
88
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf8")
|
|
89
|
-
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1])
|
|
90
|
-
return Number.isInteger(parent) ? parent : undefined
|
|
91
|
-
}
|
|
92
|
-
const parent = Number(
|
|
93
|
-
execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
|
|
94
|
-
encoding: "utf8",
|
|
95
|
-
timeout: TMUX_TIMEOUT_MS,
|
|
96
|
-
killSignal: "SIGKILL",
|
|
97
|
-
}).trim(),
|
|
98
|
-
)
|
|
99
|
-
return Number.isInteger(parent) ? parent : undefined
|
|
100
|
-
} catch {
|
|
101
|
-
return undefined
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Whether process `candidate` is `pid` or one of its ancestors, walking up from `pid` and
|
|
106
|
-
* stopping at the first match, at init, or after `MAX_ANCESTORS` steps. */
|
|
107
|
-
export function isAncestor(candidate: string, pid: number = process.pid): boolean {
|
|
108
|
-
let current: number | undefined = pid
|
|
109
|
-
for (let step = 0; current !== undefined && current > 1 && step < MAX_ANCESTORS; step++) {
|
|
110
|
-
if (String(current) === candidate) return true
|
|
111
|
-
current = parentPid(current)
|
|
112
|
-
}
|
|
113
|
-
return false
|
|
114
|
-
}
|
|
115
|
-
|
|
116
80
|
/**
|
|
117
81
|
* `tmux -u -S <socket> display-message -p -t <pane> <format>`; undefined when tmux is missing, slow
|
|
118
82
|
* or refuses, or prints nothing: a running tmux server answers a pane it does not have with an
|
|
@@ -232,10 +196,7 @@ export async function sessionTitle(
|
|
|
232
196
|
// A title with nothing that shows once cleaned (a zero-width space, a colour code alone) would
|
|
233
197
|
// be a blank row.
|
|
234
198
|
if (visible(explicit)) return () => explicit
|
|
235
|
-
|
|
236
|
-
const parts = configuredValue(environment.TMUX)?.split(",") ?? []
|
|
237
|
-
const socket = parts.slice(0, -2).join(",")
|
|
238
|
-
const serverPid = parts.at(-2) ?? ""
|
|
199
|
+
const { serverPid, socket } = tmuxServer(configuredValue(environment.TMUX))
|
|
239
200
|
const pane = configuredValue(environment.TMUX_PANE)?.trim()
|
|
240
201
|
// A directory name holds whatever the filesystem allows, newlines and overrides among them.
|
|
241
202
|
const directoryName = oneLine(basename(directory) || directory)
|