@detergent-software/conductor 3.6.0-dev.3 → 3.6.0-dev.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/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
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
+ ## [3.6.0-dev.5](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.6.0-dev.4&targetVersion=GTconductor-v3.6.0-dev.5) (2026-11-07)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **conductor:** address review of the worker skill reload (ATK-937) ([9c54fe6](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/9c54fe6d752216936889c5c219c2530415cb2bc4))
10
+ * **conductor:** load a worker's work skill again after a compaction (ATK-937) ([1442ab2](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/1442ab21c94b862a95cc71bdc4b1ca7bbb5b0292))
11
+ * **conductor:** name the work skill only for a launch that runs it (ATK-937) ([05167ad](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/05167ad75d65e643fd22350c8b8f32e0d3486bd8))
12
+
13
+ ## [3.6.0-dev.4](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.6.0-dev.3&targetVersion=GTconductor-v3.6.0-dev.4) (2026-11-07)
14
+
15
+ ### Features
16
+
17
+ * **conductor:** close an item with conduct close (ATK-932) ([64c9f54](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/64c9f5405657cff3621eb7c09a41543dfc85b44c))
18
+
19
+ ### Refactoring
20
+
21
+ * **conductor:** one CLI flow for shelve and close (ATK-932) ([ab327ed](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/ab327ed3cc1bbbe4893c8eccc8dbf28de471ecac))
22
+
23
+ ### Documentation
24
+
25
+ * **conductor:** name conduct close in the protocol (ATK-932) ([6eff1b9](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/6eff1b96181f3dc477806d9cb17193fec2a3a2b7))
26
+
5
27
  ## [3.6.0-dev.3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.6.0-dev.2&targetVersion=GTconductor-v3.6.0-dev.3) (2026-11-07)
6
28
 
7
29
  ### Features
package/README.md CHANGED
@@ -34,6 +34,8 @@ conduct done <id> --answer <text> answer a scope, blocked, human, operator or fa
34
34
  conduct triage <item> --queue answer a triage item: take its triage label off (--hold: label it human too)
35
35
  conduct shelve <item> --reason <why>
36
36
  stop an item's work for good: back to not-started, labelled human, its worker retired
37
+ conduct close <item> --reason <why>
38
+ close an item that won't be done: canceled, unassigned, its worker retired
37
39
  conduct notify [--json] the notification profile, quiet hours, each channel's state
38
40
  conduct notify profile [<name>] [--until <when>]
39
41
  switch where pushes go (see Notifications)
@@ -126,7 +128,7 @@ The session manager owns the `claude --bg` sessions it launches, and only those:
126
128
  2. `git worktree add`, run in the project's clone (see Projects), makes `~/.conductor/projects/<project>/worktrees/<name>` on `worktree-<name>` from `origin/<base>`, fetched first (after `git worktree prune`, and on Windows with `core.longpaths` on). It doesn't fetch, pull, add worktrees to or change a checkout of yours (a `localFilesFrom` checkout is only read). A worktree git still reads there is reused, its own changes and all; one with a `.git` git can't read is never deleted, and fails the launch. A `worktree-<name>` branch that is left is checked out again, with any commits it kept;
127
129
  3. with `localFilesFrom` (an absolute path to a checkout the operator keeps), the gitignored files there that match its `.worktreeinclude` are copied in, as they are now;
128
130
  4. the profile's `setup` commands run in order in the worktree, through the shell, with `CONDUCTOR_PORT_<NAME>` (such as `CONDUCTOR_PORT_WEB=41000`), `CONDUCTOR_ITEM`, `CONDUCTOR_PROJECT` and `CONDUCTOR_WORKTREE` set. They build its gitignored files from what the repo commits (such as `pnpm install` and a script that writes the dev ports). The first that fails fails the launch, with the end of its output. So does a setup that leaves changes git sees, such as a committed lockfile that an install step rewrote: the worker would take them for its own;
129
- 5. the ports go into the worktree's `.claude/settings.local.json` as `env`, merged into what the file had. A `claude --bg` session doesn't inherit the environment it is started from, but Claude Code applies that file's `env` to every session in the folder and to each resume. With `localFilesFrom` and a `.mcp.json` in the worktree (copied in, committed or made by setup), the same file also gets the checkout's project-MCP approval: the `enabledMcpjsonServers`, `disabledMcpjsonServers` and `enableAllProjectMcpServers` its own `.claude/settings.local.json` sets, so the worker's session doesn't stop at Claude Code's prompt for a server the operator already answered (ATK-866). Decisions the worktree's file already holds win (one that enables every server isn't given the checkout's disabled ones), and a server the checkout hasn't answered stays unapproved: the conductor never approves one itself. With no ports and nothing to carry, no file is written;
131
+ 5. the ports, and the work skill's name as `CONDUCTOR_WORK_SKILL` (the profile's `skills.work`, by which the mod tells a worker; see Mod, ATK-937), go into the worktree's `.claude/settings.local.json` as `env`, merged into what the file had. A `claude --bg` session doesn't inherit the environment it is started from, but Claude Code applies that file's `env` to every session in the folder and to each resume. With `localFilesFrom` and a `.mcp.json` in the worktree (copied in, committed or made by setup), the same file also gets the checkout's project-MCP approval: the `enabledMcpjsonServers`, `disabledMcpjsonServers` and `enableAllProjectMcpServers` its own `.claude/settings.local.json` sets, so the worker's session doesn't stop at Claude Code's prompt for a server the operator already answered (ATK-866). Decisions the worktree's file already holds win (one that enables every server isn't given the checkout's disabled ones), and a server the checkout hasn't answered stays unapproved: the conductor never approves one itself. A launch that runs the work skill always writes the file, for the skill's name; a probe or a launch with a prompt of its own gets no skill name, so with no ports and nothing to carry it writes none;
130
132
  6. the profile's work and verify skills must be there: a `conductor:<skill>` in the plugin, any other in the worktree's `.claude/skills` or `~/.claude/skills`.
131
133
 
132
134
  A launch that fails removes the worktree it made when it is clean, with its branch while that still has the launch's name; a worktree that was there before the launch stays.
@@ -187,7 +189,7 @@ A phase mapped to `null` moves nothing (`started` still assigns, `shelved` still
187
189
 
188
190
  **Shelving** (ATK-669). A `Shelved` note says the work stops for good, with the reason: a blocker, a decision someone else owns, a PR dropped with `conduct pr abandon`. It is the one move back: the human label goes on first (a label that doesn't take leaves the item where it is), then the item goes to not-started as above, unassigned, so the scheduler doesn't launch it again. With no state in `tracker.transitions.shelved`, Linear takes the team's own not-started state by its type. A revise never holds it back. `conduct note` refuses it, posting nothing, for a done or canceled item and while the item's PR is open, merged or doesn't read, and after it refuses the item's other notes (Handoffs aside) until a `Started` takes the item up again, so a worker still running can't undo an operator's shelve. The scheduler's next tick fires `item.shelved` for a worker whose newest lifecycle note since its launch is `Shelved` (a `Started` after it, from the operator in its tab, means it isn't), and the default rule `retire-shelved` retires it with its worktree and branch kept, whatever its PR, which frees its Area. That kept worktree raises no inbox item. The brief and the conductor's plan records start after the newest `Shelved` note: a closed PR with no `PR opened` note after it, and every Plan, approval and revise before it, belong to the run it ended. Taking the human label off puts the item back in the queue, and its next worker starts at orient and writes a new plan, which an auto-approve rule can still approve. `conduct shelve <item> --reason "…"` posts the same note for the operator (on the item's worker when one is in flight) through the daemon; it refuses a Done or Canceled item, an open PR (abandon it first) and a merged one (`conduct land`), posting nothing.
189
191
 
190
- **Closing** (ATK-931). A `Closed` note says the item won't be done: the operator said close, cancel or won't do, and the note carries the reason and their words. It moves the item to the tracker's canceled state as above, unassigned, with no label: the scheduler never launches a canceled item. With no state in `tracker.transitions.closed`, Linear takes the team's canceled-type state (the one named Canceled first). `conduct note` holds it to everything a `Shelved` note is held to: refused for a done or canceled item and while the item's PR is open, merged or doesn't read; not held back by a revise; the item's other notes after it refused (Handoffs aside) until a `Started`; and the brief and plan records start after it. The scheduler retires its worker on the note, as for a shelve, with its worktree and branch kept and no inbox item: by `item.closed` (`retire-closed`) once the read shows the item canceled, or by `item.shelved` (`retire-shelved`) while it doesn't, so a Canceled move that didn't take doesn't strand the worker. There is no operator verb for it yet.
192
+ **Closing** (ATK-931). A `Closed` note says the item won't be done: the operator said close, cancel or won't do, and the note carries the reason and their words. It moves the item to the tracker's canceled state as above, unassigned, with no label: the scheduler never launches a canceled item. With no state in `tracker.transitions.closed`, Linear takes the team's canceled-type state (the one named Canceled first). `conduct note` holds it to everything a `Shelved` note is held to: refused for a done or canceled item and while the item's PR is open, merged or doesn't read; not held back by a revise; the item's other notes after it refused (Handoffs aside) until a `Started`; and the brief and plan records start after it. The scheduler retires its worker on the note, as for a shelve, with its worktree and branch kept and no inbox item: by `item.closed` (`retire-closed`) once the read shows the item canceled, or by `item.shelved` (`retire-shelved`) while it doesn't, so a Canceled move that didn't take doesn't strand the worker. `conduct close <item> --reason "…"` posts the same note for the operator (ATK-932): through the daemon, on the item's worker when one is in flight, or with no daemon from the item's project checkout as its account, as `conduct note` posts one. It refuses what the note refuses, posting nothing.
191
193
 
192
194
  **Worker protocol 2.** The notes, their phases and kinds, the waits and the wake text are the package's spec, [`protocol/PROTOCOL.md`](protocol/PROTOCOL.md), held as data in `src/protocol.ts`. `conduct note` checks each note against it before anything is posted, on the daemon and with no daemon alike. It refuses one with an unknown phase, a Handoff without a kind and its line, a kind its phase doesn't carry (`jason` is `operator` since protocol 2), or a Plan without its `**Size tier**` and `**Landing:**` lines, and the error names the rule. The readers (watchers, briefs, history) still read older notes and people's comments.
193
195
 
@@ -350,7 +352,7 @@ For an item raised from a note:
350
352
  `conduct approve ENG-312` (or the inbox id written `#12`; a bare number is refused, since it could mean an issue) posts `approve` on the item through the tracker, or `approve: <text>` with `--text`. It uses the same key as notes, so the comment carries the operator's name and counts as their answer. `conduct revise ENG-312 "<text>"` posts `revise: <text>`; every word after the item is the text, quoted or not. Either one then:
351
353
 
352
354
  1. resolves the plan item and the worker's open `approval` wait;
353
- 2. has the session manager resume a parked worker with `[conductor] event: approval ENG-312: approve HH:MM`. The worker reads the comment on the tracker before it acts.
355
+ 2. has the session manager resume a parked worker with `[conductor] event: approval ENG-312: approve HH:MM (conduct brief ENG-312 --fresh)`. The worker reads the comment on the tracker before it acts; the line names the command that reads it (ATK-937).
354
356
 
355
357
  If the tracker refuses the comment, nothing is resolved. Only a plan takes approve or revise; `conduct done <id>` resolves any item.
356
358
 
@@ -556,7 +558,7 @@ A worker waits by posting its note, running `conduct wait …` and ending the tu
556
558
  | `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 |
557
559
  | `approval` | `approval:linear:ENG-312` (`approval:ado-boards:CAD-12` on Boards, `approval:github-issues:PAR-12` on GitHub Issues) | an `approve` / `revise:` comment by an approver after the item's newest `<work skill> · Plan` note: the Linear key's owner (on Boards, the `az login` identity; on GitHub Issues, the gh account), or an account the profile's `approvers` names by email (Linear), unique name (Boards: the UPN, usually the email) or login (GitHub). An answer by anyone else is ignored, and the plan's inbox item says whose it was (`approval.ignored`) |
558
560
  | `item ENG-312 done` / `label human` / `note Landed` | `item:linear:ENG-312: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 |
559
- | `item ENG-312 comment` | `item:linear:ENG-312:comment` | a comment by an approver (as for `approval`) after the item's newest work-skill note of any phase, and no more than 10 minutes before the wait registered: a `scope`, `blocked`, `human`, `operator` or `failed` Handoff's answer. A note never counts. A comment by anyone else is ignored, and the Handoff's inbox item says whose it was (`comment.ignored`). The wake names the author and the comment's time |
561
+ | `item ENG-312 comment` | `item:linear:ENG-312:comment` | a comment by an approver (as for `approval`) after the item's newest work-skill note of any phase, and no more than 10 minutes before the wait registered: a `scope`, `blocked`, `human`, `operator` or `failed` Handoff's answer. A note never counts. A comment by anyone else is ignored, and the Handoff's inbox item says whose it was (`comment.ignored`). The wake names the author and the comment's time, and the command that reads it: `(conduct comments ENG-312 --since <a millisecond before it> --fresh)` (ATK-937) |
560
562
  | `item ENG-312 unlabelled human` | `item:linear:ENG-312:unlabelled=human` | the label (a tag on Boards) comes off: a check since the wait registered saw it on, and a later one sees it off, so a label a rule adds after the Handoff doesn't resolve it at once. The daemon keeps that in memory, so after a restart it has to see the label on again |
561
563
  | `login ado` / `github` / `linear` | `login:ado:web` | the project's login to the provider works again: a probe as its account (see Accounts) succeeds |
562
564
  | `time 2026-10-10T09:00` | `time:2026-10-10T14:00:00.000Z` | the time has passed (no zone means local time) |
@@ -804,6 +806,7 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
804
806
  - `GET /items/:id/brief` (`?fresh=true`): the item's brief (see Briefs), as `{ "brief": … }`. 404 when the tracker has no such item, 409 when no project tracks it, 502 when the tracker couldn't be read.
805
807
  - `POST /items/:id/revise` with `{ "text" }`: `conduct revise` on an approved plan (see Inbox); 200 with `{ "revise": { comment, item, project, revised, woken, landed, worker, pending } }`. 409 when the item has no approved Plan (nothing posted); 502 when the tracker didn't take the comment, with `posted`.
806
808
  - `POST /items/:id/shelve` with `{ "reason": "<why>" }`: `conduct shelve` (see Notes); 200 with `{ "shelved": { item, project, state, url, worker } }`, `state` as a note's. 409 when it refuses the item (no project tracks it, Done or Canceled, a PR open, merged or unread: nothing posted); 502 with `posted` when the tracker didn't take the note.
809
+ - `POST /items/:id/close` with `{ "reason": "<why>" }`: `conduct close` (see Notes), the same as `shelve` with a `Closed` note; 200 with `{ "closed": { item, project, state, url, worker } }`. 409 and 502 as for `shelve`.
807
810
  - `POST /items/:id/triage` with `{ "action": "queue" | "hold" }`: `conduct triage` (see Scheduler); 200 with `{ "triaged": { action, edits, item, project } }`, each edit's `result` `added`, `removed` or `already`. 409 when no project tracks the item or its profile has no `triageLabel` (nothing written), or when the tracker refused an edit (such as a label it doesn't have); 503 for a lost login; 502 when the tracker failed. A refused or failed edit leaves any edit before it in place: a `--hold` stopped at its second edit leaves the item labelled human, so still never launched, and its error says so; running it again finishes it.
808
811
  - `GET /inbox`: the open inbox items, oldest first.
809
812
  - `POST /inbox/:id/resolve`: the operator's answer (see Inbox). The body is `{ "action": "approve", "text"? }`, `{ "action": "revise", "text" }`, or `{ "action": "done", "answer"? }` (with `answer`, only on a `scope`, `blocked`, `human` or `operator` item, or a `failed` one with an item wait attached: 409 otherwise, with nothing posted).
@@ -868,7 +871,7 @@ Then `conduct project check <name>` says whether the project can be conducted, b
868
871
  - **auth:** which credential each provider the project uses signs in with (project token, machine token or CLI login; see Accounts), probed as the daemon would. For each that isn't the project's own token, a detail line gives the scopes, the line that saves a token in the platform's store (`cmdkey`, `security` or `secret-tool`) and the config key that would scope it; that is advice, and the check passes. A token that is there but refused, or no credential at all, fails.
869
872
  - **tracker:** the tracker's login answers, and the queue reads (Linear: the team, and whether any of its projects has a priority; Boards: the saved query; GitHub Issues: the labelled issues).
870
873
  - **code-host** and **ci:** the code host's login answers and the base branch exists; each release's `pipeline` names exactly one of the repo's pipelines.
871
- - **worktree:** the launch's worktree steps on a throwaway `conduct-check-<time>` in the project's worktrees folder: `git worktree add` in the clone from `origin/<base>`, `localFilesFrom`'s files, the ports the broker would give, and `setup`. Then the profile's skills must be there after it, and a `warn` says when git doesn't ignore `.claude/settings.local.json`, where the ports and the MCP approval go. A `claude --bg` with no prompt then checks that Claude Code would start a session there: it starts idle, costs no tokens and is removed at once. Claude Code keeps workspace trust per repository, so a clone nobody has opened in Claude Code fails here with the one step that trusts it, `claude` run in the clone once with its trust prompt accepted (ATK-571). A `warn` names each server the worktree's `.mcp.json` lists that no settings file approves or denies (the worktree's `.claude/settings.local.json` and `.claude/settings.json`, and `~/.claude/settings.json`): Claude Code would ask a worker to approve it before its session starts. The fix approves it in the `localFilesFrom` checkout, which each new worktree then carries (see Workers), or in a settings file without one (ATK-866). It is removed with its branch, whatever happened. The one check that writes; `--no-worktree` skips it, since setup can take a while (such as a `pnpm install`).
874
+ - **worktree:** the launch's worktree steps on a throwaway `conduct-check-<time>` in the project's worktrees folder: `git worktree add` in the clone from `origin/<base>`, `localFilesFrom`'s files, the ports the broker would give, and `setup`. Then the profile's skills must be there after it, and a `warn` says when git doesn't ignore `.claude/settings.local.json`, where the work skill's name, the ports and the MCP approval go (every launch of the work skill writes it). A `claude --bg` with no prompt then checks that Claude Code would start a session there: it starts idle, costs no tokens and is removed at once. Claude Code keeps workspace trust per repository, so a clone nobody has opened in Claude Code fails here with the one step that trusts it, `claude` run in the clone once with its trust prompt accepted (ATK-571). A `warn` names each server the worktree's `.mcp.json` lists that no settings file approves or denies (the worktree's `.claude/settings.local.json` and `.claude/settings.json`, and `~/.claude/settings.json`): Claude Code would ask a worker to approve it before its session starts. The fix approves it in the `localFilesFrom` checkout, which each new worktree then carries (see Workers), or in a settings file without one (ATK-866). It is removed with its branch, whatever happened. The one check that writes; `--no-worktree` skips it, since setup can take a while (such as a `pnpm install`).
872
875
  - **skills:** a `conductor:<skill>` is in this package's plugin, and `skills.work` declares `protocol: 2` in its SKILL.md frontmatter (worker protocol 2). A project's own skills arrive through its `setup` (a command that installs them), not in the clone, so the worktree check looks for them.
873
876
  - **ports:** the machine's pool has a free port for each of the profile's `ports`, saving nothing; `skip` for a profile with none.
874
877
  - **health:** each `health` URL answers; a `warn` at most, since an environment can be down.
@@ -965,7 +968,8 @@ It loads only into the sessions the conductor starts, the workers and the consol
965
968
 
966
969
  The mod is the plugin's hooks module, code that costs no tokens, which does the console's plumbing.
967
970
 
968
- - **Every session** sends its plugin report on start, and nothing else: no status line, and no other calls to the daemon.
971
+ - **Every session** sends its plugin report on start: no status line, and no other calls to the daemon.
972
+ - **A worker**, a session whose worktree's `.claude/settings.local.json` names its work skill in `CONDUCTOR_WORK_SKILL` (the profile's `skills.work`, which the daemon writes there beside the ports at each launch of the work skill; a probe gets none), gets its work skill back once its conversation has lost it (ATK-937). Compact-on-park keeps only a summary of the skill, and each wake is a new process, so the mod decides at each prompt: when the conversation as the model is sent it (`$.session.messages({ as: 'api' })`: the plain rows leave out the meta message that holds a skill) has messages but not the skill's first line of prose, the prompt (most often the daemon's wake line) carries the skill's `SKILL.md` without its frontmatter as context, after a line to go on with the item and handle the prompt by the skill. It writes `conductor: the conversation lost /<skill>; loaded it with this prompt` to the debug log. The file is the plugin's own for `conductor:<name>`, else the project's `.claude/skills/<name>`, then the user's; when none can be read, the prompt carries a line asking the model to load the skill. A slash command (the daemon's `/compact`, the work skill's own) and the first prompt load nothing. A worker launched before the daemon wrote `CONDUCTOR_WORK_SKILL` gets none of this until its next launch.
969
973
  - **The console**, the session started with `CONDUCTOR_CONSOLE=1` (`conduct console` starts it; see Console), reads `GET /fleet`, `GET /inbox` and `GET /notify` every 30 s and gets:
970
974
  - **the band** above the prompt while the inbox has items: one hotkey per item (a plan approves after a second press, `y`, since a digit typed into an empty prompt presses a band button; an npm OTP copies its `npm stage approve` line; the rest open the pane), and `o` for the pane;
971
975
  - **the pane** (`/conductor`, `o`, or opened at the start, docked as a sidebar on a fullscreen terminal of 144 columns or more): the notification profile (a picker; buttons on mobile) and Remote Control's state, the inbox with each item's actions (approve, revise, answer on a `scope`, `blocked`, `human` or `operator` Handoff, or a `failed` one whose worker waits on its item, which fills `/answer #<id> ` in the prompt, copy a command, open a link, done; on a Windows terminal, open is a button that opens a web link in the default browser once, since a ctrl+click on a link in a pane opened it twice, ATK-494), the workers by project (each project's rule has [pause] and [focus]), and today's spend. The header counts the workers running and parked and the inbox in words. Each worker is one line in columns that line up across projects, cut to the pane's width so it never wraps: a glyph, the item, its phase, its PR, what it waits on in words (`PR #557 to merge (skills-registry)`, `npm 8.9.0-dev.1 on dev (cli)`, `login (ado)`) or else its activity, and the age. As the pane narrows, the age goes first, then the wait, then the PR and the phase (ATK-521). The glyphs are one cell wide: `●` in a turn, `◌` up between turns, `!` stuck on a prompt, `○` parked, `×` failed or gone, `·` starting. A row expands to attach and why, its tokens today, its whole PR ref and its raw wait keys;
package/build/cli.js CHANGED
@@ -27,7 +27,7 @@ import { npmStage } from "./lib/providers/index.js";
27
27
  import { processAlive, readPid } from "./lock.js";
28
28
  import { rotatingLog } from "./log.js";
29
29
  import { fleetWorkerSchema, storedFleet, workerByRef, workerLine } from "./manager.js";
30
- import { noteCommand } from "./note.js";
30
+ import { closeHere, noteCommand } from "./note.js";
31
31
  import { isQuiet, notifyLines, notifyStatusSchema, parseUntil, testResultSchema } from "./notify.js";
32
32
  import { sharedNpmReads } from "./npm-reads.js";
33
33
  import { overrideViewSchema, PAUSE } from "./overrides.js";
@@ -37,6 +37,7 @@ import { refreshConductorPlugin } from "./plugin.js";
37
37
  import { profileLines, projectProfile } from "./profile-layers.js";
38
38
  import { projectCheckCommand, projectCheckReport } from "./project-check-cli.js";
39
39
  import { noProject } from "./project-check.js";
40
+ import { STOP_VERBS } from "./protocol.js";
40
41
  import { pushoverCredential, pushoverSender } from "./pushover.js";
41
42
  import { cloneProject, registerProject } from "./registry.js";
42
43
  import { rulesLines, rulesService, rulesTestResultSchema, rulesViewSchema, testLines, } from "./rules-service.js";
@@ -44,7 +45,7 @@ import { resolveUntil } from "./rules.js";
44
45
  import { outputOf, run } from "./run.js";
45
46
  import { queueLines, queueViewSchema } from "./scheduler.js";
46
47
  import { awaitStart, serveDetached } from "./serve.js";
47
- import { shelvedLines, shelvedSchema } from "./shelve.js";
48
+ import { closedLines, shelvedLines, shelvedSchema } from "./shelve.js";
48
49
  import { localDay } from "./spend.js";
49
50
  import { answerStopRequest, dropStaleStopRequest, pendingStopRequest, STOP_POLL_MS, stopSupervisor, } from "./stop-request.js";
50
51
  import { inboxItemSchema, openStore, projectSchema, waitSchema, workerSchema, } from "./store.js";
@@ -71,8 +72,8 @@ const ANSWER_TIMEOUT_MS = 60_000;
71
72
  // A revise of an approved plan adds up to three brief reads (the tracker and the PR, each within
72
73
  // 20 s) 2 s apart after the comment, so a slow tracker doesn't cut off an answer whose comment landed.
73
74
  const REVISE_TIMEOUT_MS = 3 * 60_000;
74
- // A shelve reads the brief fresh (the tracker and the PR), then posts the note and its state move,
75
- // human label and PR checks, each within 20 s.
75
+ // A shelve or close reads the brief fresh (the tracker and the PR), then posts the note and its
76
+ // state move, human label and PR checks, each within 20 s.
76
77
  const SHELVE_TIMEOUT_MS = 3 * 60_000;
77
78
  const projectsBody = z.object({ projects: z.array(projectSchema) });
78
79
  const addBody = z.object({ created: z.boolean(), project: projectSchema });
@@ -234,6 +235,15 @@ async function awaitDaemon(paths, failure) {
234
235
  function brokerIo() {
235
236
  return { cwd: process.cwd(), err: (line) => process.stderr.write(`${line}\n`), out };
236
237
  }
238
+ // `conduct close <item> --reason "…"` (ATK-932): the daemon posts a Closed note on the item, which
239
+ // moves it to the tracker's canceled state, unassigned, and the next tick retires its worker with its
240
+ // worktree kept. It refuses a closed item and an open or merged PR, posting nothing. With no
241
+ // conductor, the note is posted from the item's project checkout, as `conduct note` posts one.
242
+ async function close(paths, item, reason) {
243
+ await stopItem(paths, item, reason, 'Closed', async () => {
244
+ process.exitCode = await closeHere(paths, item, reason, verbIo());
245
+ });
246
+ }
237
247
  // `conduct console`: the console (design note, section 11), the Claude session the conductor mod
238
248
  // serves, started with CONDUCTOR_CONSOLE=1 in <CONDUCTOR_HOME>/console with the conductor plugin,
239
249
  // which brings the console skill. It belongs to no project, so it opens with none registered, and it
@@ -643,6 +653,12 @@ async function main(argv) {
643
653
  throw usageError('checkout');
644
654
  process.exitCode = await checkoutCommand(paths, sub, arg, fourth, { ...item, ...(values.base === undefined ? {} : { base: values.base }) }, verbIo());
645
655
  return;
656
+ case 'close':
657
+ if (sub === undefined || arg !== undefined)
658
+ throw usageError('close');
659
+ if (values.reason === undefined || values.reason.trim() === '')
660
+ throw usageError('close', 'conduct close says why: --reason "<why>"');
661
+ return close(paths, sub, values.reason);
646
662
  case 'comments': {
647
663
  if (sub === undefined || arg !== undefined)
648
664
  throw usageError('comments');
@@ -1320,28 +1336,11 @@ async function serve(paths) {
1320
1336
  // back to not-started, unassigned and labelled human, and the next tick retires its worker with its
1321
1337
  // worktree kept (ATK-669). It refuses a closed item and an open or merged PR, posting nothing.
1322
1338
  async function shelve(paths, item, reason) {
1323
- let answer;
1324
- try {
1325
- answer = await callApi(paths, {
1326
- body: { reason },
1327
- method: 'POST',
1328
- path: `/items/${encodeURIComponent(item)}/shelve`,
1329
- timeoutMs: SHELVE_TIMEOUT_MS,
1330
- });
1331
- }
1332
- catch (error) {
1333
- if (!(error instanceof NotRunningError))
1334
- throw error;
1339
+ await stopItem(paths, item, reason, 'Shelved', () => {
1335
1340
  throw new ConductorError('the conductor is not running: conduct shelve needs it', [
1336
1341
  `without it, post the Shelved note from ${item}'s checkout: conduct note ${item} <file>, headed "## <skill> · Shelved"`,
1337
1342
  ]);
1338
- }
1339
- if (answer.status === 404)
1340
- throw olderDaemon('no conduct shelve');
1341
- const body = z.object({ shelved: shelvedSchema }).parse(await expectOk(Promise.resolve(answer)));
1342
- const { errors, lines } = shelvedLines(body.shelved);
1343
- lines.forEach(out);
1344
- errors.forEach((e) => process.stderr.write(`${e}\n`));
1343
+ });
1345
1344
  }
1346
1345
  // POST /shutdown, then wait for the daemon to go. True when one stopped, false when none answered.
1347
1346
  async function shutdownDaemon(paths) {
@@ -1477,6 +1476,34 @@ async function stopDaemon(paths) {
1477
1476
  }
1478
1477
  }
1479
1478
  }
1479
+ // `conduct shelve` or `conduct close` through the daemon (POST /items/:id/<verb>), printing what it
1480
+ // did; `notRunning` runs when no conductor answers. Any other failure may come after the note was
1481
+ // posted, so it says to look first.
1482
+ async function stopItem(paths, item, reason, phase, notRunning) {
1483
+ const verb = STOP_VERBS[phase];
1484
+ let answer;
1485
+ try {
1486
+ answer = await callApi(paths, {
1487
+ body: { reason },
1488
+ method: 'POST',
1489
+ path: `/items/${encodeURIComponent(item)}/${verb}`,
1490
+ timeoutMs: SHELVE_TIMEOUT_MS,
1491
+ });
1492
+ }
1493
+ catch (error) {
1494
+ if (error instanceof NotRunningError)
1495
+ return notRunning();
1496
+ throw new ConductorError(`the conductor didn't answer; ${item} may have the ${phase} note already, so look before running conduct ${verb} again`, [error instanceof Error ? error.message : String(error)]);
1497
+ }
1498
+ if (answer.status === 404)
1499
+ throw olderDaemon(`no conduct ${verb}`);
1500
+ const ok = await expectOk(Promise.resolve(answer));
1501
+ const { errors, lines } = phase === 'Closed'
1502
+ ? closedLines(z.object({ closed: shelvedSchema }).parse(ok).closed)
1503
+ : shelvedLines(z.object({ shelved: shelvedSchema }).parse(ok).shelved);
1504
+ lines.forEach(out);
1505
+ errors.forEach((e) => process.stderr.write(`${e}\n`));
1506
+ }
1480
1507
  // `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon startup runs
1481
1508
  // it; `conduct start` runs it with --fail-fast. The config is read again for each push, so one fixed
1482
1509
  // while the daemon is down counts, and one that doesn't load (a reason it may be down) leaves the