@detergent-software/conductor 1.0.0-dev.32 → 1.0.0-dev.33

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,14 @@
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.0.0-dev.33](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.32&targetVersion=GTconductor-v1.0.0-dev.33) (2026-10-12)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **conductor:** log when the mod asks a console to load its skill (ATK-385) ([10f7e58](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/10f7e58ec830ef3ec15e26cb7f7f03375aff235e))
10
+ * **conductor:** reset the console's skill check on /clear (ATK-385) ([6b47df4](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/6b47df436acfb57cda034bf79b98f3d65db2c065))
11
+ * **conductor:** tell a console without its skill to load it (ATK-385) ([c080637](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/c08063760f2035ebea6c53f21113296731d98237))
12
+
5
13
  ## [1.0.0-dev.32](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.31&targetVersion=GTconductor-v1.0.0-dev.32) (2026-10-12)
6
14
 
7
15
  ### Features
package/README.md CHANGED
@@ -252,7 +252,7 @@ The scheduler moves conduct-atk's tick into the daemon (design note, section 8).
252
252
  - **Order:** the tracker project's priority, then the conductor project's `weight` (profile, or `conduct focus --weight`), then the item's priority, then when it was created.
253
253
  - **Areas:** one in-flight item per Area per project. An Area is held by every worker in flight and every started item, and by the Areas it covers or is covered by.
254
254
  - **Not picked:** items labelled with the profile's human label, and items whose blockers are still open, which wait (pending, or "not being worked" when nothing will clear them).
255
- - **Slots:** `scheduler.slots` (4) across projects and the profile's `slots` (2) per project, or `conduct focus --slots`, never more than the global ones. Slots count running workers only: a parked worker keeps its Area and frees its slot.
255
+ - **Slots:** `scheduler.slots` (4) across projects and the profile's `slots` per project (ATK's is 4), or `conduct focus --slots`, never more than the global ones. Slots count running workers only: a parked worker keeps its Area and frees its slot.
256
256
  - **Holds:** quiet hours and `conduct pause` turn picks into waits that say why.
257
257
  3. **Act.** It runs the rules on what it found: `queue.pick` → `launch` (default `launch`), `item.closed` → `retire` (default `retire-closed`; a refused retire raises an inbox item once, except one refused while the worker's turn runs or its session starts, which is tried again each tick and raised only after 15 minutes), `queue.drained` and `queue.ready-to-release` → an inbox item once a day. Each action is a `rule.fired` event, then `rule.done` or `rule.failed`. A launch that failed isn't tried again while its inbox item is open.
258
258
 
@@ -487,6 +487,7 @@ It runs `claude -n conductor --permission-mode auto -- /conductor-console` with
487
487
  - **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;
488
488
  - **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, copy a command, open a link, done), the workers by project (each project's rule has [pause] and [focus]; a row expands to attach and why), and today's spend;
489
489
  - **commands answered in code**, never by the model: `/conductor`, `/inbox`, `/approve <item | #id>`, `/revise <item | #id> <text>`, `/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);
490
+ - **a check that `/conductor-console` ran**: a console started without it (by hand, as `claude -n conductor`) shows `console without its skill: run /conductor-console` in the band, and each prompt, Jason's or an escalation's, carries a line telling the model to load the skill before it answers. Running the skill (`/conductor-console`, or the model's Skill tool) clears both, until a `/clear` or a resume starts a conversation without it;
490
491
  - **the stream**: it runs `node <package>/build/cli.js 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:
491
492
 
492
493
  | Event | Data | The mod |
package/mod/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Conductor mod
2
2
 
3
- A Claude Code plugin (a hooks module) that does the conductor console's plumbing in code, so it costs no tokens. Only the console (`CONDUCTOR_CONSOLE=1`) gets anything from it: the inbox band, the fleet pane, `/conductor` `/inbox` `/approve` `/revise` `/attach` `/notify` `/pause` `/unpause` `/focus-project`, toasts, Remote Control pushes (and the one warning when Remote Control is off), and the one model wake-up for an escalation. What it shows and does is in the conductor README's *Mod* section (`apps/conductor/README.md`).
3
+ A Claude Code plugin (a hooks module) that does the conductor console's plumbing in code, so it costs no tokens. Only the console (`CONDUCTOR_CONSOLE=1`) gets anything from it: the inbox band, the fleet pane, `/conductor` `/inbox` `/approve` `/revise` `/attach` `/notify` `/pause` `/unpause` `/focus-project`, toasts, Remote Control pushes (and the one warning when Remote Control is off), the one model wake-up for an escalation, and a nudge to load `/conductor-console` in a console started without it. What it shows and does is in the conductor README's *Mod* section (`apps/conductor/README.md`).
4
4
 
5
5
  Install it with the daemon's CLI, which ships this folder:
6
6
 
@@ -46,6 +46,13 @@ export function reportedState(outcome: PushOutcome): 'failing' | 'inactive' | 'o
46
46
  return outcome.state === 'failed' ? 'failing' : outcome.state;
47
47
  }
48
48
 
49
+ // The skill a console runs first (ATK-385). `conduct console` starts with it as the first prompt;
50
+ // a console started by hand (`claude -n conductor`) has to be told.
51
+ export const CONSOLE_SKILL = 'conductor-console';
52
+ // What the model reads beside each prompt of a console whose skill hasn't run.
53
+ 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.`;
54
+ export const NO_SKILL_WARNING = `console without its skill: run /${CONSOLE_SKILL}`;
55
+
49
56
  // A band entry and the act its press does.
50
57
  export type BandEntry = { act: 'approve' | 'copy' | 'pane'; hotkey: string; id: number; label: string; text: string };
51
58
 
@@ -1,5 +1,5 @@
1
1
  import { atom, read, update } from 'claude-code';
2
- import type { EngineInterface, Register } from 'claude-code';
2
+ import type { EngineInterface, Register, RenderElement } from 'claude-code';
3
3
 
4
4
  import type { ConductorSnapshot } from '../types';
5
5
 
@@ -8,6 +8,7 @@ import {
8
8
  type ApiAnswer,
9
9
  answerText,
10
10
  bandEntries,
11
+ CONSOLE_SKILL,
11
12
  endpointOf,
12
13
  errorOf,
13
14
  findPlan,
@@ -15,6 +16,8 @@ import {
15
16
  glyph,
16
17
  homeOf,
17
18
  inboxText,
19
+ NO_SKILL_CONTEXT,
20
+ NO_SKILL_WARNING,
18
21
  notifyArgs,
19
22
  notifyText,
20
23
  overrideArgs,
@@ -49,6 +52,7 @@ const NOT_RUNNING = 'the conductor is not running (conduct serve)';
49
52
  const snapshot = atom({ plugin: 'conductor', key: 'snapshot' } as const, null);
50
53
  const confirming = atom({ plugin: 'conductor', key: 'confirming' } as const, null);
51
54
  const expanded = atom({ plugin: 'conductor', key: 'expanded' } as const, null);
55
+ const hasSkill = atom({ plugin: 'conductor', key: 'hasSkill' } as const, false);
52
56
 
53
57
  // The console is the session started with CONDUCTOR_CONSOLE=1 (`conduct console` sets it): only it
54
58
  // draws the band and the pane, answers the commands, follows the stream and acts on events. Both
@@ -315,6 +319,27 @@ export const register: Register = (on) => {
315
319
  return started;
316
320
  });
317
321
 
322
+ // A console the skill never ran in (one started by hand as `claude -n conductor`, ATK-385) is
323
+ // told to load it before it answers, each prompt until it has. `/conductor-console` or the Skill
324
+ // tool expands it before its prompt is submitted; a /clear or a resume starts a conversation
325
+ // without it.
326
+ on('skill.prompt', { skill: CONSOLE_SKILL }, async ($, e, next) => {
327
+ const prompt = await next(e);
328
+ if (isConsole) await update($, hasSkill, () => true);
329
+ return prompt;
330
+ });
331
+
332
+ on('session.end', async ($, e, next) => {
333
+ if (isConsole) await update($, hasSkill, () => false);
334
+ return next(e);
335
+ });
336
+
337
+ on('prompt.submit', async ($, e, next) => {
338
+ if (!isConsole || (await read($, hasSkill))) return next(e);
339
+ $.ui.log(`conductor: /${CONSOLE_SKILL} hasn't run; asking the model to load it`, { to: 'debug' });
340
+ return next({ ...e, context: [...(e.context ?? []), NO_SKILL_CONTEXT] });
341
+ });
342
+
318
343
  on('command.run', { command: 'conductor' }, async ($) => {
319
344
  await openPane($);
320
345
  return { text: 'Conductor pane opened.' };
@@ -369,17 +394,32 @@ export const register: Register = (on) => {
369
394
  on('command.run', { command: 'focus-project' }, async ($, e) => ({ text: await override($, 'focus', e.args) }));
370
395
 
371
396
  // The band above the prompt, in the console, while the inbox has items. A bare digit typed into
372
- // an empty prompt presses a band Button, so a plan's approve takes a second press (`y`).
397
+ // an empty prompt presses a band Button, so a plan's approve takes a second press (`y`). Until
398
+ // /conductor-console runs, a warning sits above it, or above the engine's own (ATK-385).
373
399
  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
374
400
  if (!isConsole || e.props.hasSurvey) return next(e);
375
- const snap = await read($, snapshot);
376
- if (snap === null || snap.error !== null || snap.inbox.length === 0) return next(e);
377
401
  const { Box, Button, Text } = $.ui.resolve(e);
402
+ const isWarned = !(await read($, hasSkill));
403
+ const withWarning = (row: RenderElement): RenderElement =>
404
+ isWarned ? (
405
+ <Box flexDirection="column">
406
+ <Box key="no-skill">
407
+ <Text bold color="warning">
408
+ {NO_SKILL_WARNING}
409
+ </Text>
410
+ </Box>
411
+ {row}
412
+ </Box>
413
+ ) : (
414
+ row
415
+ );
416
+ const snap = await read($, snapshot);
417
+ if (snap === null || snap.error !== null || snap.inbox.length === 0) return isWarned ? withWarning(await next(e)) : next(e);
378
418
  const entries = bandEntries(snap.inbox);
379
419
  const confirmId = await read($, confirming);
380
420
  const pending = entries.find((x) => x.act === 'approve' && x.id === confirmId);
381
421
  if (pending) {
382
- return (
422
+ return withWarning(
383
423
  <Box columnGap={2} flexWrap="wrap">
384
424
  <Text bold>conductor</Text>
385
425
  <Text>
@@ -396,10 +436,10 @@ export const register: Register = (on) => {
396
436
  variant="primary"
397
437
  />
398
438
  <Button hotkey="n" key="cancel" label="n cancel" onPress={() => update($, confirming, () => null)} />
399
- </Box>
439
+ </Box>,
400
440
  );
401
441
  }
402
- return (
442
+ return withWarning(
403
443
  <Box columnGap={2} flexWrap="wrap">
404
444
  <Text bold>conductor · inbox {snap.inbox.length}</Text>
405
445
  {entries.map((x) => (
@@ -416,7 +456,7 @@ export const register: Register = (on) => {
416
456
  ))}
417
457
  {snap.inbox.length > entries.length && <Text dimColor>+{snap.inbox.length - entries.length} more</Text>}
418
458
  <Button hotkey="o" key="open-pane" label="o panel" onPress={() => openPane($)} />
419
- </Box>
459
+ </Box>,
420
460
  );
421
461
  });
422
462
 
@@ -51,6 +51,9 @@ declare module 'claude-code' {
51
51
  confirming: number | null;
52
52
  // The worker whose row the pane shows expanded, by name.
53
53
  expanded: string | null;
54
+ // Whether /conductor-console ran in this conversation (ATK-385). Kept in $.state, so a reload
55
+ // of the mod doesn't forget it; a /clear or a resume (session.end) sets it back to false.
56
+ hasSkill: boolean;
54
57
  snapshot: ConductorSnapshot | null;
55
58
  };
56
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detergent-software/conductor",
3
- "version": "1.0.0-dev.32",
3
+ "version": "1.0.0-dev.33",
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": {