@detergent-software/conductor 3.5.0-dev.1 → 3.5.0-dev.2

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,16 @@
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.5.0-dev.2](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.5.0-dev.1&targetVersion=GTconductor-v3.5.0-dev.2) (2026-10-29)
6
+
7
+ ### Features
8
+
9
+ * **conductor:** load the console skill in code after a /clear (ATK-840) ([6b13e41](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/6b13e41d3b47946d12a8503f7d6512ee2b3e8f8a))
10
+
11
+ ### Bug Fixes
12
+
13
+ * **conductor:** mark the console skill loaded only when it is (ATK-840) ([0c8aa3a](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/0c8aa3a02fe55416776456d81dc2b3c6b3b760a8))
14
+
5
15
  ## [3.5.0-dev.1](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.4.0&targetVersion=GTconductor-v3.5.0-dev.1) (2026-10-27)
6
16
 
7
17
  ### Features
package/README.md CHANGED
@@ -955,8 +955,8 @@ The mod is the plugin's hooks module, code that costs no tokens, which does the
955
955
  - **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;
956
956
  - **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;
957
957
  - **commands answered in code**, never by the model: `/conductor`, `/inbox`, `/approve <item | #id>`, `/revise <item | #id> <text>`, `/answer <item | #id> <text>` (`conduct done #<id> --answer <text>`, the text as typed), `/attach <item | worker>`, `/notify [<profile> [--until <when>]]`, `/pause [<project>] [--until <when>]`, `/unpause [<project>]`, `/focus-project <project> [--until <when>]` (puts its items first, until midnight without `--until`; not `/focus`, which is Claude Code's own);
958
- - **a check that `/conductor:conductor-console` ran**: a console started without it (by hand, as `claude --plugin-dir … -n conductor`) shows `console without its skill: run /conductor:conductor-console` in the band, and each prompt, the operator's or an escalation's, carries a line telling the model to load the skill before it answers. Running the skill (`/conductor:conductor-console`, or the model's Skill tool) clears both, until a `/clear` or a resume starts a conversation without it;
959
- - **a warm prompt cache while it idles** (ATK-386). The console's cache lasts an hour after its last use, and a wake after that writes the whole context again. So 55 minutes after the console's last turn, the mod sends one ping (`$.model.fork`, the console's own last request plus a one-word ask, no tools, no turn in the transcript). The API serves the ping from the cache for a tenth of a write's price, and the hour starts over. It pings again every 55 minutes until a turn comes, and each turn starts the timer over. At an idle point it runs `/clear` instead when the context is 150k or more, or after 12 pings in a row (the 13th idle point, about 12 hours after the last turn), and the band says `fresh console (cleared while idle: …)` until the next prompt loads the skill. A ping that read less than half the context from the cache (the cache was gone anyway, as under a 5-minute TTL) stops the pings until the next turn. Each ping and clear goes to `POST /console/cache`, which logs a `console.cache` event with the context size and what the ping cost. A fork isn't in the transcript, so that event is the only place its cost shows;
958
+ - **the skill, loaded in code** (ATK-385, ATK-840): a console whose conversation lacks `/conductor:conductor-console` (one started by hand, as `claude --plugin-dir … -n conductor`, or after a `/clear` or a resume) shows `console without its skill: /conductor:conductor-console loads with the next prompt` in the band, and the mod loads the skill itself with the next prompt, not the model. The operator's prompt carries the skill's text (its `SKILL.md` without the frontmatter, read from the plugin folder) as context the model reads first, with a line to run its Start, then answer. An escalation runs `/conductor:conductor-console` with the escalation as its arguments, since the mod's own `prompt.submit` hook never sees a prompt the mod submits. Nothing runs at the clear itself. A slash command loads nothing (`/conductor:conductor-console` loads itself): it carries a line asking the model to load the skill, as before, and so does every prompt while the file can't be read, or an escalation whose run the engine refuses;
959
+ - **a warm prompt cache while it idles** (ATK-386). The console's cache lasts an hour after its last use, and a wake after that writes the whole context again. So 55 minutes after the console's last turn, the mod sends one ping (`$.model.fork`, the console's own last request plus a one-word ask, no tools, no turn in the transcript). The API serves the ping from the cache for a tenth of a write's price, and the hour starts over. It pings again every 55 minutes until a turn comes, and each turn starts the timer over. At an idle point it runs `/clear` instead when the context is 150k or more, or after 12 pings in a row (the 13th idle point, about 12 hours after the last turn), and the band says `fresh console (cleared while idle: …)` until the next prompt, when the mod loads the skill (above). A ping that read less than half the context from the cache (the cache was gone anyway, as under a 5-minute TTL) stops the pings until the next turn. Each ping and clear goes to `POST /console/cache`, which logs a `console.cache` event with the context size and what the ping cost. A fork isn't in the transcript, so that event is the only place its cost shows;
960
960
  - **the stream**: it runs `node <the cli.js that conductor.json names> events --follow --console` (`--console` tells the daemon a console is open), redraws on each event, toasts each new inbox item, and acts on three events:
961
961
 
962
962
  | Event | Data | The mod |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detergent-software/conductor",
3
- "version": "3.5.0-dev.1",
3
+ "version": "3.5.0-dev.2",
4
4
  "description": "Conductor: the local daemon that runs conducted projects' work loops (conduct serve) and its conduct CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,11 +48,26 @@ export function reportedState(outcome: PushOutcome): 'failing' | 'inactive' | 'o
48
48
 
49
49
  // The skill a console runs first (ATK-385), as this plugin namespaces it: the engine names a plugin
50
50
  // skill `<plugin>:<skill>` in skill.prompt (ATK-525). `conduct console` starts with it as the first
51
- // prompt; a console started by hand (`claude --plugin-dir … -n conductor`) has to be told.
51
+ // prompt; in a console started by hand (`claude --plugin-dir … -n conductor`), or after a /clear or
52
+ // a resume, the mod loads it with the next prompt (ATK-840).
52
53
  export const CONSOLE_SKILL = 'conductor:conductor-console';
53
- // What the model reads beside each prompt of a console whose skill hasn't run.
54
+ // The skill's file, under the plugin folder ($.plugin.root).
55
+ export const CONSOLE_SKILL_FILE = 'skills/conductor-console/SKILL.md';
56
+ // What the model reads beside a prompt when the mod couldn't read the skill to load it.
54
57
  export const NO_SKILL_CONTEXT = `This session is the conductor console (CONDUCTOR_CONSOLE=1), but /${CONSOLE_SKILL} hasn't run in it, so its rules don't hold. Load the ${CONSOLE_SKILL} skill with the Skill tool and follow it, then answer this prompt.`;
55
- export const NO_SKILL_WARNING = `console without its skill: run /${CONSOLE_SKILL}`;
58
+ export const NO_SKILL_WARNING = `console without its skill: /${CONSOLE_SKILL} loads with the next prompt`;
59
+
60
+ // What the model reads beside the first prompt of a console without its skill: the skill, as the
61
+ // mod loaded it (ATK-840). It comes after the prompt, so it says to run Start first.
62
+ export function loadedSkillContext(body: string): string {
63
+ return `This session is the conductor console (CONDUCTOR_CONSOLE=1), and this conversation started without /${CONSOLE_SKILL} (a /clear, a resume, or a console started by hand), so the conductor mod loaded it: the skill follows. Follow it from its Start, then answer the prompt above.\n\n${body}`;
64
+ }
65
+
66
+ // A SKILL.md without its frontmatter: the text the engine expands for the model.
67
+ export function skillBody(file: string): string {
68
+ const head = /^\uFEFF?---\r?\n(?:[\s\S]*?\r?\n)?---[ \t]*(?:\r?\n|$)/.exec(file);
69
+ return (head === null ? file : file.slice(head[0].length)).trim();
70
+ }
56
71
 
57
72
  // ATK-386: the console idles for hours, and its prompt cache lives an hour past its last use, so a
58
73
  // wake after that wrote its whole context again (89k in ATK-365's run). A fork reads that context
@@ -13,6 +13,7 @@ import {
13
13
  bandEntries,
14
14
  clearedText,
15
15
  CONSOLE_SKILL,
16
+ CONSOLE_SKILL_FILE,
16
17
  daemonOf,
17
18
  endpointOf,
18
19
  findHandoff,
@@ -24,6 +25,7 @@ import {
24
25
  idleAction,
25
26
  inboxText,
26
27
  isWebLink,
28
+ loadedSkillContext,
27
29
  MAYBE_POSTED,
28
30
  NO_SKILL_CONTEXT,
29
31
  NO_SKILL_WARNING,
@@ -43,6 +45,7 @@ import {
43
45
  reportedState,
44
46
  reviseText,
45
47
  runText,
48
+ skillBody,
46
49
  snapshotOf,
47
50
  spendToday,
48
51
  statusText,
@@ -275,6 +278,31 @@ async function endpoint($: EngineInterface): Promise<Endpoint> {
275
278
  return found;
276
279
  }
277
280
 
281
+ // ATK-840: an escalation to a console without its skill runs the skill, with the escalation as its
282
+ // arguments: one turn, the skill as the engine expands it. The mod's prompt.submit hook can't load it
283
+ // there, as the engine never runs a hook on what its own plugin submits (the re-entry rule). The
284
+ // console counts as loaded from here, so a prompt or escalation meanwhile doesn't load it twice, and
285
+ // the stream reads on rather than wait out the skill's turn. Should the run be refused, it isn't
286
+ // loaded after all, and the escalation goes as it is, asking the model to load the skill.
287
+ async function escalateWithSkill($: EngineInterface, text: string): Promise<void> {
288
+ await markSkillLoaded($);
289
+ void $.command.run({ args: text, command: CONSOLE_SKILL }).catch(async (error: unknown) => {
290
+ await update($, hasSkill, () => false);
291
+ $.ui.log(
292
+ `conductor: /${CONSOLE_SKILL} not run (${error instanceof Error ? error.message : String(error)}); asking the model to load it`,
293
+ { to: 'debug' },
294
+ );
295
+ try {
296
+ await $.prompt.submit({ text: `${text}\n\n${NO_SKILL_CONTEXT}` });
297
+ } catch (submitError) {
298
+ $.ui.log(
299
+ `conductor: escalate failed: ${submitError instanceof Error ? submitError.message : String(submitError)}`,
300
+ { to: 'debug' },
301
+ );
302
+ }
303
+ });
304
+ }
305
+
278
306
  // `conduct events --follow` from this package's own build, so the mod and the CLI are one version
279
307
  // and no .cmd shim is in the way. It reconnects by itself; a child that ends is started again after
280
308
  // the last event it showed, so nothing in between is lost. One that keeps exiting at once (no
@@ -318,6 +346,18 @@ async function follow($: EngineInterface): Promise<void> {
318
346
  $.ui.log('conductor: conduct events keeps exiting; the console polls instead', { to: 'debug' });
319
347
  }
320
348
 
349
+ // The console skill as the engine would expand it: its file's text without the frontmatter. A mod's
350
+ // `$` has no `$.skill` to run it through the skill.prompt hooks (engine 2.1.312), so none sees it.
351
+ async function loadSkill($: EngineInterface): Promise<string> {
352
+ return skillBody(await $.fs.read(`${$.plugin.root}/${CONSOLE_SKILL_FILE}`));
353
+ }
354
+
355
+ // The console's conversation has its skill: the band's warning and its "cleared while idle" go.
356
+ async function markSkillLoaded($: EngineInterface): Promise<void> {
357
+ await update($, hasSkill, () => true);
358
+ await update($, cleared, () => null);
359
+ }
360
+
321
361
  // The console has been idle WARM_AFTER_MS: keep its cache warm with a ping, or clear it (ATK-386).
322
362
  // Either is told to the daemon, which logs it as a `console.cache` event: a fork is no turn of the
323
363
  // transcript, so the event is the only place its cost shows.
@@ -384,7 +424,8 @@ async function onLine($: EngineInterface, text: string): Promise<void> {
384
424
  }
385
425
  case 'escalate':
386
426
  // The one place the mod wakes the console's model: facts only, as the rules engine wrote them.
387
- await $.prompt.submit({ text: act.text });
427
+ if (await read($, hasSkill)) await $.prompt.submit({ text: act.text });
428
+ else await escalateWithSkill($, act.text);
388
429
  return;
389
430
  case 'ignore':
390
431
  return;
@@ -667,16 +708,12 @@ export const register: Register = (on) => {
667
708
  return started;
668
709
  });
669
710
 
670
- // A console the skill never ran in (one started by hand as `claude -n conductor`, ATK-385) is
671
- // told to load it before it answers, each prompt until it has. `/conductor:conductor-console` or the Skill
672
- // tool expands it before its prompt is submitted; a /clear or a resume starts a conversation
673
- // without it.
711
+ // Whether the console's conversation has its skill (ATK-385): `/conductor:conductor-console` or
712
+ // the Skill tool expands it before its prompt is submitted, and prompt.submit loads it when
713
+ // neither has (ATK-840). A /clear or a resume starts a conversation without it.
674
714
  on('skill.prompt', { skill: CONSOLE_SKILL }, async ($, e, next) => {
675
715
  const prompt = await next(e);
676
- if (isConsole) {
677
- await update($, hasSkill, () => true);
678
- await update($, cleared, () => null);
679
- }
716
+ if (isConsole) await markSkillLoaded($);
680
717
  return prompt;
681
718
  });
682
719
 
@@ -715,10 +752,28 @@ export const register: Register = (on) => {
715
752
  return done;
716
753
  });
717
754
 
755
+ // ATK-840: the first prompt of a console without its skill (the operator's or an escalation's)
756
+ // carries the skill, loaded by the mod, so its rules hold without the model choosing to load it;
757
+ // it counts once the prompt has entered. A slash command loads nothing: the skill's own expands
758
+ // before it, and the rest carry the line asking the model to load it, as does every prompt while
759
+ // the skill's file can't be read.
718
760
  on('prompt.submit', async ($, e, next) => {
719
761
  if (!isConsole || (await read($, hasSkill))) return next(e);
720
- $.ui.log(`conductor: /${CONSOLE_SKILL} hasn't run; asking the model to load it`, { to: 'debug' });
721
- return next({ ...e, context: [...(e.context ?? []), NO_SKILL_CONTEXT] });
762
+ const ask = (): ReturnType<typeof next> => next({ ...e, context: [...(e.context ?? []), NO_SKILL_CONTEXT] });
763
+ if (e.command !== undefined) return ask();
764
+ let loaded: string;
765
+ try {
766
+ loaded = await loadSkill($);
767
+ } catch (error) {
768
+ $.ui.log(
769
+ `conductor: /${CONSOLE_SKILL} not loaded (${error instanceof Error ? error.message : String(error)}); asking the model to load it`,
770
+ { to: 'debug' },
771
+ );
772
+ return ask();
773
+ }
774
+ const entered = await next({ ...e, context: [...(e.context ?? []), loadedSkillContext(loaded)] });
775
+ if (entered.drop === undefined) await markSkillLoaded($);
776
+ return entered;
722
777
  });
723
778
 
724
779
  on('command.run', { command: 'conductor' }, async ($) => {
@@ -794,8 +849,8 @@ export const register: Register = (on) => {
794
849
 
795
850
  // The band above the prompt, in the console, while the inbox has items. A bare digit typed into
796
851
  // an empty prompt presses a band Button, so a plan's approve takes a second press (`y`). Until
797
- // /conductor:conductor-console runs, a warning sits above it, or above the engine's own (ATK-385); after the
798
- // mod cleared an idle console, a plain line that says so instead (ATK-386).
852
+ // the console has its skill, a warning sits above it, or above the engine's own (ATK-385); after
853
+ // the mod cleared an idle console, a plain line that says so instead (ATK-386).
799
854
  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
800
855
  if (!isConsole || e.props.hasSurvey) return next(e);
801
856
  const { Box, Button, Text } = $.ui.resolve(e);
@@ -32,7 +32,7 @@ When `/conductor:conductor-console` runs:
32
32
  2. Check the setup in one Bash call: `echo $CONDUCTOR_CONSOLE` and `conduct --version`.
33
33
  - Not `1`: say that the band, the pane and the escalation wake-ups are off in this session, and that `conduct console` opens a proper one. Go on with chat.
34
34
  - If the operator says the band or pane is missing: the mod comes with the conductor plugin in `~/.conductor/plugin`, which only `conduct console` loads (`--plugin-dir`). Check that the folder holds `hooks/register.tsx`, then run `/reload-plugins`; if it doesn't, `conduct start` (a daemon refreshes it as it starts), then close this session and run `conduct console`.
35
- 3. `conduct escalations`. These are escalations raised while no console was open. Handle each one, oldest first, as below.
35
+ 3. `conduct escalations`. These are escalations raised while no console was open. Handle each one, oldest first, as below. When the skill ran with an escalation as its arguments (`[conductor] escalation #<id> …`: the mod woke a console that had lost its skill after a `/clear` or a resume), handle that one here too, once, whether or not the list has it.
36
36
  4. Report in two or three lines from `conduct status`, `conduct inbox` and the first line of `conduct queue`: workers running and parked, what waits on the operator, and the scheduler's mode. If the mode is `off`, say that the daemon launches nothing until `"scheduler": { "mode": "on" }` is set in `~/.conductor/config.json` and the daemon is restarted (`conduct stop`, then `conduct start`).
37
37
 
38
38
  Then end the turn.