@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/claude-envoy",
3
- "version": "1.12.1",
3
+ "version": "1.14.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Claude Code channel plugin for Legion's Envoy bus",
6
6
  "type": "module",
@@ -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 each problem: fix them all, then call once.
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
- - When you finish an issue, take the top ready one: `todo`, by priority then board rank, per project.
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 words: the thing and what is wrong with it. Never paste a draft.
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 you add one. Try two or three wordings before you decide nothing exists.
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:** 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.
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 child issues.
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
 
@@ -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
- // `TMUX` is `<socket>,<server pid>,<session>`, and a socket path may hold a comma.
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)