@detergent-software/conductor 1.4.0-dev.2 → 1.4.0-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +57 -9
- package/build/brief-cli.js +105 -23
- package/build/brief-cli.js.map +1 -1
- package/build/brief.js +107 -9
- package/build/brief.js.map +1 -1
- package/build/cli.js +21 -2
- package/build/cli.js.map +1 -1
- package/build/code-host-cli.js +46 -2
- package/build/code-host-cli.js.map +1 -1
- package/build/conditions.js +20 -11
- package/build/conditions.js.map +1 -1
- package/build/daemon.js +51 -12
- package/build/daemon.js.map +1 -1
- package/build/follow-ups.js +72 -0
- package/build/follow-ups.js.map +1 -0
- package/build/help.js +22 -4
- package/build/help.js.map +1 -1
- package/build/in-process.js +30 -0
- package/build/in-process.js.map +1 -0
- package/build/note.js +95 -14
- package/build/note.js.map +1 -1
- package/build/notes.js +104 -16
- package/build/notes.js.map +1 -1
- package/build/profile.js +54 -13
- package/build/profile.js.map +1 -1
- package/build/server.js +30 -7
- package/build/server.js.map +1 -1
- package/build/tracker-writes.js +8 -1
- package/build/tracker-writes.js.map +1 -1
- package/build/trackers.js +6 -1
- package/build/trackers.js.map +1 -1
- package/build/watchers/github.js +19 -1
- package/build/watchers/github.js.map +1 -1
- package/build/watchers/issues-brief.js +48 -0
- package/build/watchers/issues-brief.js.map +1 -0
- package/build/watchers/issues-queue.js +85 -0
- package/build/watchers/issues-queue.js.map +1 -0
- package/build/watchers/issues-writes.js +233 -0
- package/build/watchers/issues-writes.js.map +1 -0
- package/build/watchers/issues.js +264 -0
- package/build/watchers/issues.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Releases are tagged `conductor-v<version>` and staged on npm by `atk-conductor-release` (`pipelines/release-conductor.yml`), which writes each section below. `scripts/check-changelog.mjs` fails when a tag has no section here.
|
|
4
4
|
|
|
5
|
+
## [1.4.0-dev.4](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.4.0-dev.3&targetVersion=GTconductor-v1.4.0-dev.4) (2026-10-15)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **conductor:** a GitHub Issues tracker kind (ATK-458) ([6028a13](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/6028a136cc39e1ed51072571629e68dead466d35))
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **conductor:** review fixes for the github-issues tracker (ATK-458) ([79abe60](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/79abe602f80138714098a61821d16dfc015787c0))
|
|
14
|
+
|
|
15
|
+
## [1.4.0-dev.3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.4.0-dev.2&targetVersion=GTconductor-v1.4.0-dev.3) (2026-10-15)
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **conductor:** a brief with the phase, the PR and the comments (ATK-456) ([cfd0f11](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/cfd0f11e8881f43eb0d4d4d4f93800738f81d57f))
|
|
20
|
+
* **conductor:** conduct file, in-process notes and the PR reference (ATK-456) ([0b3311b](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/0b3311b7ec877cc7846d1c5607c35c653c43b58d))
|
|
21
|
+
* **conductor:** phase notes move the tracker state (ATK-456) ([6c93b91](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/6c93b91fc7f0487c41ddfe357248a3bfdc97a463))
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **conductor:** guard the brief found in review (ATK-456) ([5d7debe](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/5d7debe7cd670bc20798f7d55438bfc24d1095be))
|
|
26
|
+
* **conductor:** guard the note and follow-up paths found in review (ATK-456) ([6d881da](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/6d881daca5d5c5103943fba6a1560f7ae4db36de)), closes [AB#12](https://emergentcode.visualstudio.com/Emergent%20Software/_workitems/edit/12)
|
|
27
|
+
|
|
28
|
+
### Refactoring
|
|
29
|
+
|
|
30
|
+
* **conductor:** the brief names its PR with the notes' helper (ATK-456) ([7b3f009](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/7b3f009715cce46a4ac02697bdca3680ab9f78f5))
|
|
31
|
+
|
|
5
32
|
## [1.4.0-dev.2](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.4.0-dev.1&targetVersion=GTconductor-v1.4.0-dev.2) (2026-10-15)
|
|
6
33
|
|
|
7
34
|
### Features
|
package/README.md
CHANGED
|
@@ -104,18 +104,28 @@ Worker statuses: `launching`, `running`, `parked`, `stopped-unexpectedly`, `fail
|
|
|
104
104
|
|
|
105
105
|
## Notes
|
|
106
106
|
|
|
107
|
-
A worker writes each phase note (`## <work skill> · <Phase>[ · <kind>: <one line>]`, such as `## work-issue · PR opened`) with `conduct note <item> <file>`. The daemon posts the file as a comment on the item in the tracker, then records the phase at once: on the worker (`phase` in `GET /fleet` and `conduct status`), on the item, and as a `note.posted` event. A PR opened note also records the worker's PR (`pr`, such as `ado:Emergent.AgenticToolkit#1850` or `github:<owner>/<repo>#458`) from its first PR link, replacing an earlier one; until then, the worker's first `pr:` wait records it. `conduct status` and the console's fleet pane show it. The worker is `--worker <worker>`, else the one whose worktree is the current checkout, else the project's live worker on that item. The project is the worker's, else the one whose tracker key (the Linear team, or the Boards `key`) is the item's key prefix.
|
|
107
|
+
A worker writes each phase note (`## <work skill> · <Phase>[ · <kind>: <one line>]`, such as `## work-issue · PR opened`) with `conduct note <item> <file>`. The daemon posts the file as a comment on the item in the tracker, then records the phase at once: on the worker (`phase` in `GET /fleet` and `conduct status`), on the item, and as a `note.posted` event. A PR opened note also records the worker's PR (`pr`, such as `ado:Emergent.AgenticToolkit#1850` or `github:<owner>/<repo>#458`) from its first PR link, replacing an earlier one; until then, the worker's first `pr:` wait records it. `conduct status` and the console's fleet pane show it. The worker is `--worker <worker>`, else the one whose worktree is the current checkout, else the project's live worker on that item. The project is the worker's, else the one whose tracker key (the Linear team, or the Boards or GitHub Issues `key`) is the item's key prefix.
|
|
108
|
+
|
|
109
|
+
**State follows the notes** (providers design note, section 5). A `Started`, `PR opened` or `Landed` note (not a Handoff) also moves the item, to the state its phase maps to in the profile's `tracker.transitions`:
|
|
110
|
+
|
|
111
|
+
| Phase | Linear default | ADO Boards default (Scrum) | GitHub Issues default |
|
|
112
|
+
|---|---|---|---|
|
|
113
|
+
| `started` | In Progress, assigned to the key's owner | Committed, assigned to the `az` identity | the label `in-progress`, assigned to the gh account |
|
|
114
|
+
| `pr-opened` | In Review, the PR attached | no change, the PR linked (an `ArtifactLink` for an ADO PR, a `Hyperlink` for a GitHub one) | the label `in-review` in place of `in-progress`; a GitHub PR is linked by the `Refs <owner>/<repo>#<n>` its body carries, any other by a comment with its URL |
|
|
115
|
+
| `landed` | Done | Done | closed as completed, the phase label taken off |
|
|
116
|
+
|
|
117
|
+
A phase mapped to `null` moves nothing (`started` still assigns). A `Started` never reopens a closed item (`kept`); a `PR opened` on one does, for a follow-up PR. The PR linked is the worker's, else the note's first PR link. A write the tracker refuses is a warning on the note's answer and stderr, and a `tracker.transition-failed` event; the note stands, and the verb still exits 0. A write that took is `tracker.transitioned`. With no daemon, `conduct note` posts from the item's project checkout as its account, and moves the item the same way; nothing is recorded then.
|
|
108
118
|
|
|
109
119
|
The work skill is the profile's `skills.work`. A note headed with one of `skills.aliases` counts the same, there and wherever the daemon reads notes (approvals, `conduct wait item … note`, the review-thread checks of `conduct wait pr … comment`). So when a project renames its work skill (ATK's `work-issue` became `work-item`), the old name goes in `aliases` and items in flight keep their phases.
|
|
110
120
|
|
|
111
121
|
| Exit | Means | The worker then |
|
|
112
122
|
|---|---|---|
|
|
113
123
|
| 0 | posted | goes on |
|
|
114
|
-
| 3 | nobody posted it:
|
|
124
|
+
| 3 | nobody posted it: the tracker refused it (such as a read-only key), or no daemon and this isn't the item's project checkout | posts the note to the tracker itself, as before protocol v1 |
|
|
115
125
|
| 4 | not posted: Jason wrote `revise: …` after the item's newest Plan was approved, by him (a comment, `conduct approve`, the worker's tab) or by the conductor (`auto-approve now`, see Rules); stdout has his words | stops, re-plans with them, posts a new Plan and waits for approval, which the gate now holds |
|
|
116
126
|
| 1 | refused (no heading, no project) or unclear (the daemon or Linear didn't answer the write) | reads the message; when it says the note may be there, looks before posting again |
|
|
117
127
|
|
|
118
|
-
On Linear the daemon uses the key the watchers read (below), so that key needs **Create comments** as well as Read. Comments it writes carry the key owner's name, as the Linear MCP's do. On ADO Boards it posts a markdown discussion comment as the `az login` identity.
|
|
128
|
+
On Linear the daemon uses the key the watchers read (below), so that key needs **Create comments** as well as Read. Comments it writes carry the key owner's name, as the Linear MCP's do. On ADO Boards it posts a markdown discussion comment as the `az login` identity, and on GitHub Issues an issue comment as the project's gh account.
|
|
119
129
|
|
|
120
130
|
## Briefs
|
|
121
131
|
|
|
@@ -123,20 +133,23 @@ On Linear the daemon uses the key the watchers read (below), so that key needs *
|
|
|
123
133
|
- the title, URL, status (name and type), the tracker's project, Area and labels;
|
|
124
134
|
- the blockers, the items it blocks and the related items, each with its status and title;
|
|
125
135
|
- its phase notes (time, phase, kind and line), the newest Plan, and the `approve` / `revise:` after it with its words;
|
|
136
|
+
- the work skill's `phase`: `orient`, `plan`, `awaiting-approval`, `implement`, `wait`, `land`, `done`, `abandoned` or `canceled`, from the status, the notes and the PR;
|
|
137
|
+
- the `pr`: its worker's, else the newest `PR opened` note's link, read on the project's code host, with its state and verdict (or why it couldn't be read);
|
|
126
138
|
- the description.
|
|
127
139
|
|
|
128
|
-
`--json` prints it as one object.
|
|
140
|
+
`--json` prints it as one object. `--comments` adds every comment's words, time, and whether the key's owner wrote it. With no daemon it is read here, from the item's project checkout as its account.
|
|
129
141
|
|
|
130
142
|
The queue's snapshot has no descriptions, relations or comments, so the daemon reads the item itself:
|
|
131
143
|
- **Linear:** one GraphQL request with the watchers' key, and more only for comments past the first 100.
|
|
132
144
|
- **ADO Boards:** the work item with its links, its linked items' titles and states, its discussion comments and the `az login` identity. Predecessor is a blocker, Successor an item it blocks.
|
|
145
|
+
- **GitHub Issues:** the issue, its comments, the issues it is blocked by and blocks (GitHub's issue dependencies; one in another repo is `<owner>/<repo>#<n>`) and the gh account's login. GitHub has no related relation, so the brief lists none.
|
|
133
146
|
|
|
134
147
|
A brief is kept for 30 seconds. Anything the daemon sees happen to the item drops it: a note it posted, an `approve` or `revise` it posted, a wait resolved. `--fresh` reads the tracker again, as a worker checking for an approval after a wake should.
|
|
135
148
|
|
|
136
149
|
| Exit | Means | The worker then |
|
|
137
150
|
|---|---|---|
|
|
138
151
|
| 0 | the brief | uses it |
|
|
139
|
-
| 3 | no daemon
|
|
152
|
+
| 3 | no daemon and this isn't the item's project checkout, or the tracker couldn't be read (no key, the tracker down) | reads the item from the tracker itself (the Linear MCP, or the work skill's helper) |
|
|
140
153
|
| 1 | refused: no project tracks the item, or the tracker has no such item | reads the message |
|
|
141
154
|
|
|
142
155
|
## Worker verbs
|
|
@@ -147,6 +160,8 @@ Protocol 2's verbs on git and the code host (providers design note, section 4),
|
|
|
147
160
|
- `conduct pr create` checks everything before it pushes, `--dry-run` included: the title carries the item key, the branch carries it too and isn't the base, the description fits the host (ADO 4,000 characters, GitHub 65,536), no PR is open from the branch yet, nothing is uncommitted, the branch is ahead of its base, and commitlint passes (skipped in a clone without it installed, whose own CI lints the PR). Then it pushes from the checkout and has the daemon open the PR (`POST /workers/:id/prs`). The daemon checks ownership again from its own records and the worktree, never from the request: the worker is live, the branch the worktree is on now (or the `--repo` checkout's) carries the worker's item, and the title carries it. It records the PR on the worker (`pr`; another repo's only while the worker has none) and logs `code-host.pr-created`. `conduct branch` renames only the item's own branch, never the base, `main` or another item's.
|
|
148
161
|
- `conduct land <pr>` sets the worker's PR up to land, as the helper's `land` did, and is safe to repeat. The PR must be the worker's (the same checks, and from the branch the checkout is on now, into its base). A brake already on it (a comment, a failed build, conflicts) is reported, and nothing is armed. A Landing note whose landing armed something, with that gone now, means someone cancelled it: `auto-merge-cancelled`, until `--rearm`. `--mode` is held to the profile's landing rules (`work.landing`) for the PR's files as pushed (`origin/<base>...origin/<head>`, fetched first) and the plan's tier. The daemon reads the tier from the item's Plan note; in-process it takes `--tier`, and auto is refused without one. Without `--mode` the rules pick it. Then it arms the host's own landing behind its capability check (`landingCheck`). On ADO that is a +10 vote in auto mode, with auto-complete and noFastForward. On GitHub it is auto-merge with a merge commit in auto mode, where the repo allows it; in review mode nothing is armed. It posts the Landing note last, and a new one whenever the mode changes, then prints the verdict. A failed build comes with its log, and a merge with whether it has two parents. The daemon logs `code-host.landing-armed` with the mode.
|
|
149
162
|
- `conduct wait pr <pr>` (no event word) is the verdict wait, `pr:<host>:<repo>#<n>:verdict`: it holds on the first verdict that isn't `waiting`, with a line such as `pr 2030 build-failed: <pipeline>`. While it waits on a PR whose landing the daemon recorded, the ADO watcher keeps it armed. It queues an expired build again (once per wait) and, in auto mode, restores a cleared vote (never over a negative one), logged as `code-host.kept-armed`. A local wait (no daemon) gives the same verdicts but keeps nothing armed.
|
|
163
|
+
- `conduct pr create` ends the description with the tracker's reference when it has one, before the length check: `AB#<id>` for an ADO Boards item on a GitHub PR, so the Boards GitHub app links it, and `Refs <owner>/<repo>#<n>` for a GitHub Issues item on a GitHub PR. Linear has none.
|
|
164
|
+
- `conduct file --title … --body-file … --area … [--related …] [--blocked-by …] [--human]` files a follow-up of the checkout's item: in Backlog, in the item's project, with one Area (a label of the profile's Area group, a path below a board's `areaRoot`, or the name after a GitHub Issues `areaPrefix` label), the profile's `triageLabel`, and its `humanLabel` with `--human`. It is related to the item and each `--related`, and waits on each `--blocked-by` (keys, repeated or comma-separated). A key of another project, or an Area outside the group, is refused before anything is written. On Linear a relation that fails after the issue exists is a warning; on Boards the item and its links are one request. On GitHub Issues the follow-up also carries the `queueLabel`, the related items are a `Related: #n` line in its body (GitHub has no related relation), and a dependency that fails after the issue exists is a warning. The daemon files it for the worker (`POST /workers/:id/follow-ups`) and logs `tracker.filed`. The worker searches for a duplicate first, with the tracker's own tools: this only files.
|
|
150
165
|
- `conduct pr threads <pr>` lists the PR's threads (on GitHub each comment and review) with `needsAnswer` (ATK-384). `conduct reply <pr> [<thread>]` answers on the worker's own PR: in the ADO thread named, or as a note on the PR. The text is headed `## <work skill> · Reply` unless it starts with a note heading. The daemon logs `code-host.replied`. `--repo` takes `github:<owner>/<repo>` (from its `.scratch/repos` checkout, into its default branch) or `ado:<repository>`; the profile's `codeHost` is the default.
|
|
151
166
|
- `conduct runs <sha>` is one look at a merge's runs, as the helper's `runs-watch --once` and `stage` were (ATK-455). It is a read, so it always runs here.
|
|
152
167
|
- **Which runs.** On Azure Pipelines (the profile's `ci: ado-pipelines`, for its Azure DevOps repo), they are the pipelines whose YAML trigger the merge fires. The YAML and the merge's changed files are read in the worktree, which fetches the base when it hasn't got the commit. On GitHub Actions (`--repo github:<owner>/<repo>`, or `ci: github-actions` for a GitHub repo), they are the runs the merge's push started.
|
|
@@ -161,7 +176,9 @@ Every write the conductor makes outside its own store, with what triggers it and
|
|
|
161
176
|
|
|
162
177
|
| Write | Trigger | Identity |
|
|
163
178
|
|---|---|---|
|
|
164
|
-
| A phase note on the item | `conduct note` (a worker) | the Linear key's owner; on Boards, the project's `az` identity |
|
|
179
|
+
| A phase note on the item | `conduct note` (a worker) | the Linear key's owner; on Boards, the project's `az` identity; on GitHub Issues, its gh account |
|
|
180
|
+
| The item's state, its assignee on `started`, and the PR's link (see Notes) | a `Started`, `PR opened` or `Landed` note | the Linear key's owner, the `az` identity, or the gh account |
|
|
181
|
+
| A follow-up item and its relations | `conduct file` → `POST /workers/:id/follow-ups` | the Linear key's owner, the `az` identity, or the gh account |
|
|
165
182
|
| `approve` / `revise:` on the item | `conduct approve` / `revise`, the console | the Linear key's owner, or the `az` identity |
|
|
166
183
|
| An auto-approval comment | a rule's `auto-approve now` / `after` | the Linear key's owner, or the `az` identity |
|
|
167
184
|
| A label added or removed (`human`, `triage`, `shipped-open`) | a rule's `label add` / `remove` | the Linear key's owner, or the `az` identity (a tag) |
|
|
@@ -172,7 +189,7 @@ Every write the conductor makes outside its own store, with what triggers it and
|
|
|
172
189
|
| A reply on the worker's PR | `conduct reply` → `POST /workers/:id/replies` | the project's `az` identity, or its gh account |
|
|
173
190
|
| A train: its branch, the members' merges into it, its PR, a note on each member | `conduct train` (you) | the project's gh account |
|
|
174
191
|
|
|
175
|
-
Accounts are per project (see Accounts). Each code-host write is an event: `code-host.pr-created`, `code-host.landing-armed`, `code-host.kept-armed`, `code-host.replied`.
|
|
192
|
+
Accounts are per project (see Accounts). Each code-host write is an event: `code-host.pr-created`, `code-host.landing-armed`, `code-host.kept-armed`, `code-host.replied`. So is each tracker write a worker causes: `tracker.transitioned` (or `tracker.transition-failed`) and `tracker.filed`. Rules still never change an item's state: only its phase notes do.
|
|
176
193
|
|
|
177
194
|
## Inbox
|
|
178
195
|
|
|
@@ -363,8 +380,8 @@ A worker waits by posting its note, running `conduct wait …` and ending the tu
|
|
|
363
380
|
| `run atk-api-deploy 12345 done` | `run:ado:atk-api-deploy#12345:done` | the run completed, with its result |
|
|
364
381
|
| `npm 8.5.0-dev.2 --tag dev` | `npm:@detergent-software/atk@8.5.0-dev.2:dev` | the dist-tag points at the version, read through the `npm-stage` release adapter |
|
|
365
382
|
| `health dev --version 1.2.3` / `--commit <sha>` | `health:dev:version=1.2.3` | the profile's `health.dev` URL answers with that version or commit |
|
|
366
|
-
| `approval` | `approval:linear:ATK-310` (`approval:ado-boards:CAD-12` on Boards) | an `approve` / `revise:` comment by the Linear key's owner (on Boards, the `az login` identity) after the item's newest `<work skill> · Plan` note |
|
|
367
|
-
| `item ATK-271 done` / `label human` / `note Landed` | `item:linear:ATK-271:state=done` | the state (name or type), a label (a tag on Boards), or a phase note |
|
|
383
|
+
| `approval` | `approval:linear:ATK-310` (`approval:ado-boards:CAD-12` on Boards, `approval:github-issues:PAR-12` on GitHub Issues) | an `approve` / `revise:` comment by the Linear key's owner (on Boards, the `az login` identity; on GitHub Issues, the gh account) after the item's newest `<work skill> · Plan` note |
|
|
384
|
+
| `item ATK-271 done` / `label human` / `note Landed` | `item:linear:ATK-271:state=done` | the state (name or type), a label (a tag on Boards), or a phase note; on GitHub Issues the state is `open`, `closed`, a phase label or its type |
|
|
368
385
|
| `login ado` / `github` / `linear` | `login:ado:atk` | the project's login to the provider works again: a probe as its account (see Accounts) succeeds |
|
|
369
386
|
| `time 2026-10-10T09:00` | `time:2026-10-10T14:00:00.000Z` | the time has passed (no zone means local time) |
|
|
370
387
|
|
|
@@ -433,6 +450,37 @@ A project whose work items live in Azure DevOps Boards (Cadence) sets `tracker.k
|
|
|
433
450
|
|
|
434
451
|
Everything goes through the project's `az` token (see Accounts). The reads (the scheduler's queue and the watchers' waits) take from the `ado` watcher's budget, as PR and pipeline waits do. A wait pass reads every waited item of a board in one batch, and an item's comments only when its rev or comment count moved. Notes and the rules' `label` edits (a patch of `System.Tags` alone, refused if the item changed since it was read) are writes, outside the budget, as on Linear.
|
|
435
452
|
|
|
453
|
+
## GitHub Issues
|
|
454
|
+
|
|
455
|
+
A project whose items are a GitHub repo's issues sets `tracker.kind` to `github-issues` (providers design note, section 5; ATK-458):
|
|
456
|
+
|
|
457
|
+
```jsonc
|
|
458
|
+
"tracker": {
|
|
459
|
+
"kind": "github-issues",
|
|
460
|
+
"key": "PAR", // items are PAR-<issue number>: issue numbers are numbers alone
|
|
461
|
+
"humanLabel": "human", // a label
|
|
462
|
+
"triageLabel": "triage", // optional, a label too: new items Jason hasn't triaged (see Scheduler)
|
|
463
|
+
"queueLabel": "conduct", // the default: the open issues carrying it are the queue
|
|
464
|
+
"areaPrefix": "area:", // the default: area:docs is the Area docs
|
|
465
|
+
"priority": 3, // the queue's priority against other projects' (1 urgent … 4 low)
|
|
466
|
+
"transitions": { "started": "in-progress", "pr-opened": "in-review" } // the defaults
|
|
467
|
+
// "owner", "repo": the GitHub code host's by default; required beside an Azure DevOps one
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
| Tracker concept | On GitHub Issues |
|
|
472
|
+
|---|---|
|
|
473
|
+
| the queue | the open issues labelled `queueLabel`, oldest first, one tracker project named after the repo with the profile's `priority`. PRs, which the issues API lists too, are left out. In-flight issues are read as well, so a closed one retires its worker. |
|
|
474
|
+
| Area | the name after `areaPrefix` on the issue's first such label |
|
|
475
|
+
| blockers | GitHub's issue dependencies (`blocked by`) still open, inside the queue or not; one in another repo is listed as `<owner>/<repo>#<n>` |
|
|
476
|
+
| human, triage, labels | labels, matched in any case |
|
|
477
|
+
| states | open → unstarted; open with a `transitions` label → started (named after the label); closed as completed → completed; closed as not planned or a duplicate → canceled |
|
|
478
|
+
| phase notes, `approve` / `revise:` | issue comments; the gh account's own comments count as Jason's, so the note heading tells them apart |
|
|
479
|
+
|
|
480
|
+
`Closes #n` would close an issue only on a merge to the default branch, so the conductor closes it on the `Landed` note instead, and a project on a `develop` branch works the same. Phase labels are added and taken off one at a time, never replaced as a set, so a label someone else added meanwhile stays. GitHub makes a label it doesn't have yet when a phase or rule first adds it; `conduct file` refuses an Area with no label.
|
|
481
|
+
|
|
482
|
+
Everything goes through the project's gh account (`projects.<name>.github.user`, else gh's active account; see Accounts). The reads (the scheduler's queue and the watchers' waits) take from the `github` watcher's budget, as PR and merge waits on GitHub do. A wait pass reads each waited issue once, and its comments only when its update time or comment count moved. Notes, label edits and the tracker writes are outside the budget, as on Linear. That account's rate limit is shared with anything else it runs (ATK's prod API reads with Jason's), so the `github` watcher's `perMinute` bounds it.
|
|
483
|
+
|
|
436
484
|
## Broker
|
|
437
485
|
|
|
438
486
|
The broker shares what every project on the machine uses (ATK-318).
|
package/build/brief-cli.js
CHANGED
|
@@ -1,70 +1,152 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { briefLines } from "./brief.js";
|
|
2
|
+
import { BriefError, briefLines, briefOf, briefPrs, linearBriefs, notedPr, } from "./brief.js";
|
|
3
3
|
import { callApi, NotRunningError } from "./client.js";
|
|
4
|
+
import { projectHosts } from "./code-host.js";
|
|
5
|
+
import {} from "./credentials.js";
|
|
4
6
|
import { ConductorError } from "./errors.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import { checkoutProject, localCredentials } from "./in-process.js";
|
|
8
|
+
import { ITEM } from "./notes.js";
|
|
9
|
+
import { notePrefixes } from "./profile.js";
|
|
10
|
+
import { routedBriefs } from "./trackers.js";
|
|
11
|
+
import { boardsBriefs } from "./watchers/boards-brief.js";
|
|
12
|
+
import { issuesBriefs } from "./watchers/issues-brief.js";
|
|
13
|
+
// Exit code for a brief nobody gave (no conductor answered, and this isn't the item's project
|
|
14
|
+
// checkout to read it from; or the conductor couldn't read the tracker): the worker reads the item
|
|
15
|
+
// from the tracker itself. A refusal (no project tracks the item, the tracker has no such item)
|
|
16
|
+
// exits 1.
|
|
8
17
|
export const NOT_BRIEFED = 3;
|
|
9
|
-
// The daemon asks the tracker once or a few times, each within 20 s.
|
|
10
|
-
const BRIEF_TIMEOUT_MS =
|
|
18
|
+
// The daemon asks the tracker once or a few times, and the code host for the PR, each within 20 s.
|
|
19
|
+
const BRIEF_TIMEOUT_MS = 90_000;
|
|
11
20
|
const link = z.object({ id: z.string(), status: z.string().nullable(), title: z.string().nullable() });
|
|
12
21
|
export const briefSchema = z.object({
|
|
13
22
|
approval: z.object({ at: z.string(), kind: z.enum(['approve', 'revise']), text: z.string() }).nullable(),
|
|
14
23
|
area: z.string().nullable(),
|
|
15
24
|
blockedBy: z.array(link),
|
|
16
25
|
blocks: z.array(link),
|
|
26
|
+
comments: z.array(z.object({ at: z.string(), body: z.string(), byViewer: z.boolean() })),
|
|
17
27
|
description: z.string(),
|
|
18
28
|
item: z.string(),
|
|
19
29
|
labels: z.array(z.string()),
|
|
20
30
|
notes: z.array(z.object({ at: z.string(), kind: z.string().nullable(), line: z.string().nullable(), phase: z.string() })),
|
|
31
|
+
phase: z.enum(['abandoned', 'awaiting-approval', 'canceled', 'done', 'implement', 'land', 'orient', 'plan', 'wait']),
|
|
21
32
|
plan: z.string().nullable(),
|
|
33
|
+
pr: z
|
|
34
|
+
.object({
|
|
35
|
+
error: z.string().optional(),
|
|
36
|
+
outcome: z
|
|
37
|
+
.enum([
|
|
38
|
+
'abandoned',
|
|
39
|
+
'auto-merge-cancelled',
|
|
40
|
+
'build-failed',
|
|
41
|
+
'changes-requested',
|
|
42
|
+
'conflicts',
|
|
43
|
+
'merge-failed',
|
|
44
|
+
'merged',
|
|
45
|
+
'review-comment',
|
|
46
|
+
'waiting',
|
|
47
|
+
])
|
|
48
|
+
.nullable(),
|
|
49
|
+
ref: z.string(),
|
|
50
|
+
state: z.enum(['closed', 'merged', 'open']).nullable(),
|
|
51
|
+
title: z.string().nullable(),
|
|
52
|
+
url: z.string().nullable(),
|
|
53
|
+
})
|
|
54
|
+
.nullable(),
|
|
22
55
|
project: z.string(),
|
|
23
56
|
readAt: z.string(),
|
|
24
57
|
related: z.array(link),
|
|
25
|
-
|
|
26
|
-
revise: z.object({ at: z.string(), text: z.string() }).nullable().default(null),
|
|
58
|
+
revise: z.object({ at: z.string(), text: z.string() }).nullable(),
|
|
27
59
|
status: z.object({ name: z.string(), type: z.string().nullable() }),
|
|
28
60
|
title: z.string(),
|
|
29
61
|
trackerProject: z.string().nullable(),
|
|
30
62
|
url: z.string().nullable(),
|
|
31
63
|
});
|
|
32
64
|
const refused = z.object({ details: z.array(z.string()).optional(), error: z.string() });
|
|
33
|
-
// `conduct brief <item>`: the item's context
|
|
34
|
-
//
|
|
65
|
+
// `conduct brief <item>`: the item's context, as text or JSON; its comments with --comments. From
|
|
66
|
+
// the daemon, else read here from this checkout's project. Returns the exit code: 0, NOT_BRIEFED,
|
|
67
|
+
// or 1 (thrown) when it was refused.
|
|
35
68
|
export async function briefCommand(paths, item, options, io) {
|
|
36
|
-
const notBriefed = (why) => {
|
|
37
|
-
io.err(`no brief: ${why}; read the item from the tracker yourself`);
|
|
38
|
-
return NOT_BRIEFED;
|
|
39
|
-
};
|
|
40
69
|
let answer;
|
|
41
70
|
try {
|
|
42
71
|
answer = await callApi(paths, {
|
|
43
72
|
method: 'GET',
|
|
44
|
-
path: `/items/${encodeURIComponent(item)}/brief
|
|
73
|
+
path: `/items/${encodeURIComponent(item)}/brief?${new URLSearchParams({
|
|
74
|
+
...(options.fresh ? { fresh: 'true' } : {}),
|
|
75
|
+
...(options.comments ? { comments: 'true' } : {}),
|
|
76
|
+
}).toString()}`,
|
|
45
77
|
timeoutMs: BRIEF_TIMEOUT_MS,
|
|
46
78
|
});
|
|
47
79
|
}
|
|
48
80
|
catch (error) {
|
|
49
81
|
if (error instanceof NotRunningError)
|
|
50
|
-
return
|
|
82
|
+
return briefHere(paths, item, options, io, 'the conductor is not running');
|
|
51
83
|
throw error;
|
|
52
84
|
}
|
|
53
85
|
if (answer.status === 200) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
const given = z.object({ brief: briefSchema }).safeParse(answer.body);
|
|
87
|
+
// A conductor of another version gives a brief of another shape: read it here instead.
|
|
88
|
+
if (!given.success)
|
|
89
|
+
return briefHere(paths, item, options, io, "this conductor's brief is of another version");
|
|
90
|
+
print(given.data.brief, options, io);
|
|
59
91
|
return 0;
|
|
60
92
|
}
|
|
61
93
|
const error = refused.safeParse(answer.body).data;
|
|
62
94
|
// A daemon from before the brief API answers its route's `not_found`; a missing item says which.
|
|
63
95
|
if (answer.status === 404 && error?.error === 'not_found')
|
|
64
|
-
return
|
|
96
|
+
return briefHere(paths, item, options, io, 'this conductor has no brief API');
|
|
65
97
|
// The conductor couldn't read the tracker (no key, the tracker down): the worker still may.
|
|
66
98
|
if (answer.status === 502)
|
|
67
|
-
return notBriefed(`${error?.error ?? 'the conductor could not read the tracker'}${error?.details?.length ? ` (${error.details.join('; ')})` : ''}`);
|
|
99
|
+
return notBriefed(io, `${error?.error ?? 'the conductor could not read the tracker'}${error?.details?.length ? ` (${error.details.join('; ')})` : ''}`);
|
|
68
100
|
throw new ConductorError(error?.error ?? `the API answered ${answer.status}`, error?.details ?? []);
|
|
69
101
|
}
|
|
102
|
+
// The brief read here, with no conductor: the tracker and the PR as this checkout's project's
|
|
103
|
+
// account. Without a daemon there is no worker, so the PR is the newest PR opened note's.
|
|
104
|
+
async function briefHere(paths, item, options, io, why) {
|
|
105
|
+
if (!ITEM.test(item))
|
|
106
|
+
throw new ConductorError(`"${item}" is not a tracker item, such as ATK-311`);
|
|
107
|
+
const key = item.toUpperCase();
|
|
108
|
+
const here = await checkoutProject(io.cwd, key, why, 'read it from');
|
|
109
|
+
if ('refusal' in here)
|
|
110
|
+
return notBriefed(io, here.refusal);
|
|
111
|
+
const { profile } = here;
|
|
112
|
+
// The machine's accounts, built once, and only where a seam doesn't stand in for them. A config
|
|
113
|
+
// that can't be read is a brief that couldn't be read here (exit 3), as a tracker that is down.
|
|
114
|
+
let credentials;
|
|
115
|
+
const accounts = () => (credentials ??= localCredentials(paths));
|
|
116
|
+
// A PR it can't read (the accounts included) is an `error` on the brief, never a throw.
|
|
117
|
+
const prs = io.prs ?? briefPrs((project) => projectHosts(accounts(), project));
|
|
118
|
+
let read;
|
|
119
|
+
try {
|
|
120
|
+
const reader = io.reader ??
|
|
121
|
+
routedBriefs({
|
|
122
|
+
'ado-boards': boardsBriefs({ credentials: accounts() }),
|
|
123
|
+
'github-issues': issuesBriefs({ credentials: accounts() }),
|
|
124
|
+
linear: linearBriefs({ credentials: accounts() }),
|
|
125
|
+
});
|
|
126
|
+
read = await reader.read(key, profile);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof BriefError && error.reason === 'missing')
|
|
130
|
+
throw new ConductorError(error.message);
|
|
131
|
+
return notBriefed(io, error instanceof Error ? error.message : String(error));
|
|
132
|
+
}
|
|
133
|
+
const prefixes = notePrefixes(profile);
|
|
134
|
+
const ref = notedPr(read.comments, prefixes);
|
|
135
|
+
const brief = briefOf(key, profile.name, read, prefixes, new Date(), ref === null ? null : await prs(profile, ref));
|
|
136
|
+
io.err(`read here (${why})`);
|
|
137
|
+
print(brief, options, io);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
function notBriefed(io, why) {
|
|
141
|
+
io.err(`no brief: ${why}; read the item from the tracker yourself`);
|
|
142
|
+
return NOT_BRIEFED;
|
|
143
|
+
}
|
|
144
|
+
function print(brief, options, io) {
|
|
145
|
+
if (options.json) {
|
|
146
|
+
const { comments: _comments, ...rest } = brief;
|
|
147
|
+
io.out(JSON.stringify(options.comments ? brief : rest, null, 2));
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
briefLines(brief, { comments: options.comments === true }).forEach((line) => io.out(line));
|
|
151
|
+
}
|
|
70
152
|
//# sourceMappingURL=brief-cli.js.map
|
package/build/brief-cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brief-cli.js","sourceRoot":"","sources":["../src/brief-cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,
|
|
1
|
+
{"version":3,"file":"brief-cli.js","sourceRoot":"","sources":["../src/brief-cli.ts"],"names":[],"mappings":"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"}
|
package/build/brief.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ConductorError } from "./errors.js";
|
|
3
|
-
import { noteHeading } from "./lib/providers/index.js";
|
|
4
|
-
import { ITEM, projectFor } from "./notes.js";
|
|
3
|
+
import { noteHeading, prVerdict } from "./lib/providers/index.js";
|
|
4
|
+
import { firstPr, ITEM, projectFor } from "./notes.js";
|
|
5
5
|
import { notePrefixes } from "./profile.js";
|
|
6
|
+
import { ACTIVE_STATUSES } from "./store.js";
|
|
7
|
+
import { linkedPr } from "./tracker-writes.js";
|
|
6
8
|
import { linearApi, LinearRequestError } from "./watchers/linear-api.js";
|
|
7
9
|
import { approvalAfterPlan } from "./watchers/linear.js";
|
|
8
10
|
// `conduct brief` (design note, section 13): one item's context for a worker's orient step, read
|
|
@@ -18,8 +20,8 @@ export class BriefError extends ConductorError {
|
|
|
18
20
|
this.reason = reason;
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
|
-
// The brief as a reader sees it, for `conduct brief` without --json
|
|
22
|
-
export function briefLines(brief) {
|
|
23
|
+
// The brief as a reader sees it, for `conduct brief` without --json; its comments with `comments`.
|
|
24
|
+
export function briefLines(brief, { comments = false } = {}) {
|
|
23
25
|
const links = (label, list) => list.length === 0
|
|
24
26
|
? []
|
|
25
27
|
: [`${label}:`, ...list.map((l) => ` ${l.id} ${l.status ?? '?'} ${l.title ?? ''}`.trimEnd())];
|
|
@@ -28,10 +30,18 @@ export function briefLines(brief) {
|
|
|
28
30
|
: brief.approval === null
|
|
29
31
|
? `none after the Plan of ${brief.plan}`
|
|
30
32
|
: `${brief.approval.kind} ${brief.approval.at}: "${brief.approval.text}"`;
|
|
33
|
+
const { pr } = brief;
|
|
34
|
+
const prLine = pr === null
|
|
35
|
+
? 'pr: none'
|
|
36
|
+
: pr.error !== undefined
|
|
37
|
+
? `pr: ${pr.ref} unread: ${pr.error}`
|
|
38
|
+
: `pr: ${pr.ref} ${pr.state ?? '?'} ${pr.outcome ?? '?'}${pr.url ? ` ${pr.url}` : ''}`;
|
|
31
39
|
return [
|
|
32
40
|
`${brief.item} ${brief.title}`,
|
|
33
41
|
...(brief.url ? [brief.url] : []),
|
|
34
42
|
`status: ${brief.status.name}${brief.status.type && brief.status.type !== brief.status.name.toLowerCase() ? ` (${brief.status.type})` : ''}`,
|
|
43
|
+
`phase: ${brief.phase}`,
|
|
44
|
+
prLine,
|
|
35
45
|
`project: ${brief.project}${brief.trackerProject ? ` · ${brief.trackerProject}` : ''}`,
|
|
36
46
|
`area: ${brief.area ?? 'none'}`,
|
|
37
47
|
`labels: ${brief.labels.length > 0 ? brief.labels.join(', ') : 'none'}`,
|
|
@@ -48,11 +58,24 @@ export function briefLines(brief) {
|
|
|
48
58
|
...(brief.revise ? [`revised after it: ${brief.revise.at}: "${brief.revise.text}"`] : []),
|
|
49
59
|
'',
|
|
50
60
|
brief.description.trim() || '(no description)',
|
|
61
|
+
...(comments
|
|
62
|
+
? [
|
|
63
|
+
'',
|
|
64
|
+
brief.comments.length === 0 ? 'comments: none' : 'comments:',
|
|
65
|
+
...brief.comments.flatMap((c) => [
|
|
66
|
+
` ${c.at} ${c.byViewer ? 'the key owner' : 'someone else'}`,
|
|
67
|
+
...c.body
|
|
68
|
+
.trim()
|
|
69
|
+
.split(/\r?\n/)
|
|
70
|
+
.map((line) => ` ${line}`.trimEnd()),
|
|
71
|
+
]),
|
|
72
|
+
]
|
|
73
|
+
: []),
|
|
51
74
|
];
|
|
52
75
|
}
|
|
53
76
|
// The brief from what the tracker gave: the comments as phase notes, the newest Plan, and the
|
|
54
77
|
// approval after it.
|
|
55
|
-
export function briefOf(item, project, read, prefixes, at) {
|
|
78
|
+
export function briefOf(item, project, read, prefixes, at, pr = null) {
|
|
56
79
|
const { comments, viewer, ...fields } = read;
|
|
57
80
|
const sorted = [...comments].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
58
81
|
const notes = sorted.flatMap((c) => {
|
|
@@ -74,18 +97,78 @@ export function briefOf(item, project, read, prefixes, at) {
|
|
|
74
97
|
const revised = answers
|
|
75
98
|
.slice(answers.findLastIndex((c) => /^approved?\b/i.test(c.body.trim())) + 1)
|
|
76
99
|
.find((c) => /^revise\s*:/i.test(c.body.trim()));
|
|
100
|
+
const approval = found ? { ...found, text: said ? said.body.trim() : '' } : null;
|
|
101
|
+
const revise = revised ? { at: revised.createdAt, text: revised.body.trim() } : null;
|
|
77
102
|
return {
|
|
78
103
|
...fields,
|
|
79
|
-
approval
|
|
104
|
+
approval,
|
|
105
|
+
comments: sorted.map((c) => ({
|
|
106
|
+
at: c.createdAt,
|
|
107
|
+
body: c.body,
|
|
108
|
+
byViewer: viewer !== null && c.authorId === viewer,
|
|
109
|
+
})),
|
|
80
110
|
item,
|
|
81
111
|
notes,
|
|
112
|
+
phase: briefPhase(fields.status, notes, { approval, revise }, pr),
|
|
82
113
|
plan: notes.findLast((n) => n.phase === 'Plan')?.at ?? null,
|
|
114
|
+
pr,
|
|
83
115
|
project,
|
|
84
116
|
readAt: at.toISOString(),
|
|
85
|
-
revise
|
|
117
|
+
revise,
|
|
86
118
|
};
|
|
87
119
|
}
|
|
88
|
-
|
|
120
|
+
// The work skill's phase for an item: done or canceled by its status; then by its PR (open, merged,
|
|
121
|
+
// abandoned, or one that couldn't be read, which still waits); not started; then by its newest
|
|
122
|
+
// Plan: a `revise:` newer than its approval sends it back to plan, an approval (a `Plan approved`
|
|
123
|
+
// note or Jason's `approve`) to implement, and no answer yet waits.
|
|
124
|
+
export function briefPhase(status, notes, answers, pr) {
|
|
125
|
+
if (status.type === 'completed')
|
|
126
|
+
return 'done';
|
|
127
|
+
if (status.type === 'canceled')
|
|
128
|
+
return 'canceled';
|
|
129
|
+
if (pr?.state === 'merged')
|
|
130
|
+
return 'land';
|
|
131
|
+
if (pr?.state === 'closed')
|
|
132
|
+
return 'abandoned';
|
|
133
|
+
if (pr !== null)
|
|
134
|
+
return 'wait';
|
|
135
|
+
if (status.type === 'backlog' || status.type === 'unstarted' || status.type === 'triage')
|
|
136
|
+
return 'orient';
|
|
137
|
+
const plan = notes.findLast((n) => n.phase === 'Plan');
|
|
138
|
+
if (!plan)
|
|
139
|
+
return 'plan';
|
|
140
|
+
const { approval, revise } = answers;
|
|
141
|
+
const noted = notes.findLast((n) => n.phase === 'Plan approved' && Date.parse(n.at) > Date.parse(plan.at));
|
|
142
|
+
const approvedAt = Math.max(noted ? Date.parse(noted.at) : -Infinity, approval?.kind === 'approve' ? Date.parse(approval.at) : -Infinity);
|
|
143
|
+
const revisedAt = Math.max(revise ? Date.parse(revise.at) : -Infinity, approval?.kind === 'revise' ? Date.parse(approval.at) : -Infinity);
|
|
144
|
+
if (revisedAt > approvedAt)
|
|
145
|
+
return 'plan';
|
|
146
|
+
return approvedAt > -Infinity ? 'implement' : 'awaiting-approval';
|
|
147
|
+
}
|
|
148
|
+
// The PR reader of the projects' code hosts: the PR as the host has it, with its verdict.
|
|
149
|
+
export function briefPrs(hosts) {
|
|
150
|
+
return async (profile, ref) => {
|
|
151
|
+
const link = linkedPr(ref, profile);
|
|
152
|
+
const unread = (error) => ({
|
|
153
|
+
error,
|
|
154
|
+
outcome: null,
|
|
155
|
+
ref,
|
|
156
|
+
state: null,
|
|
157
|
+
title: null,
|
|
158
|
+
url: link?.url ?? null,
|
|
159
|
+
});
|
|
160
|
+
if (link === null)
|
|
161
|
+
return unread(`${ref} isn't a PR ${profile.name}'s code host reads`);
|
|
162
|
+
try {
|
|
163
|
+
const pr = await hosts(profile.name)(link.repo).read(link.number, { notePrefixes: notePrefixes(profile) });
|
|
164
|
+
return { outcome: prVerdict(pr).outcome, ref, state: pr.state, title: pr.title, url: pr.url };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return unread(error instanceof Error ? error.message : String(error));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function briefService({ now = () => new Date(), prs, reader, store, ttlMs = TTL_MS, }) {
|
|
89
172
|
const cache = new Map();
|
|
90
173
|
// Bumped by each event about an item, so a read that began before one isn't kept.
|
|
91
174
|
const changes = new Map();
|
|
@@ -129,13 +212,28 @@ export function briefService({ now = () => new Date(), reader, store, ttlMs = TT
|
|
|
129
212
|
const details = error instanceof ConductorError ? error.details : [];
|
|
130
213
|
throw new BriefError(`couldn't read ${key}: ${error instanceof Error ? error.message : String(error)}`, 'unreadable', details);
|
|
131
214
|
}
|
|
132
|
-
const
|
|
215
|
+
const prefixes = notePrefixes(project.profile);
|
|
216
|
+
// The PR its live worker has, else the one its newest PR opened note links.
|
|
217
|
+
const recorded = store
|
|
218
|
+
.listWorkers(ACTIVE_STATUSES)
|
|
219
|
+
.find((w) => !w.probe && w.project === project.name && w.item.toUpperCase() === key)?.pr;
|
|
220
|
+
const ref = recorded ?? notedPr(read.comments, prefixes);
|
|
221
|
+
const pr = prs && ref ? await prs(project.profile, ref) : null;
|
|
222
|
+
const brief = briefOf(key, project.name, read, prefixes, now(), pr);
|
|
133
223
|
if ((changes.get(key) ?? 0) === seen)
|
|
134
224
|
cache.set(key, { at: now().getTime(), brief });
|
|
135
225
|
return brief;
|
|
136
226
|
},
|
|
137
227
|
};
|
|
138
228
|
}
|
|
229
|
+
// The PR the newest PR opened note links, as a worker's `pr`; null without one.
|
|
230
|
+
export function notedPr(comments, prefixes) {
|
|
231
|
+
const opened = [...comments]
|
|
232
|
+
.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt))
|
|
233
|
+
.filter((c) => noteHeading(c.body, prefixes)?.phase === 'PR opened')
|
|
234
|
+
.at(-1);
|
|
235
|
+
return firstPr(opened?.body ?? '');
|
|
236
|
+
}
|
|
139
237
|
const STATE = 'state { name type }';
|
|
140
238
|
const LINKED = `identifier title ${STATE}`;
|
|
141
239
|
const COMMENTS = 'comments(first: 100, after: $after) { pageInfo { hasNextPage endCursor } nodes { body createdAt user { id } } }';
|