@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 +10 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/plugin/hooks/model.ts +18 -3
- package/plugin/hooks/register.tsx +68 -13
- package/plugin/skills/conductor-console/SKILL.md +1 -1
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
|
|
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
package/plugin/hooks/model.ts
CHANGED
|
@@ -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`)
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
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
|
-
|
|
721
|
-
|
|
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
|
-
//
|
|
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.
|