Skip to content

Commit d7f21a0

Browse files
committed
Release conductor workflow orchestration
1 parent 0bbfbcc commit d7f21a0

13 files changed

Lines changed: 903 additions & 95 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Download packaged builds from GitHub Releases:
88

99
https://github.com/TerminallyLazy/misconducting/releases/latest
1010

11-
Current app metadata in this checkout is `0.1.14`. The public latest release page may
12-
still show older assets until tag `v0.1.14` is pushed and the desktop release workflow
11+
Current app metadata in this checkout is `0.1.15`. The public latest release page may
12+
still show older assets until tag `v0.1.15` is pushed and the desktop release workflow
1313
publishes new artifacts.
1414

1515
The GitHub Actions workflow `.github/workflows/release-desktop.yml` is configured to build macOS, Windows, Linux x86_64, and Linux ARM64 packages from release tags.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "symphony-desktop",
3-
"version": "0.1.14",
3+
"version": "0.1.15",
44
"private": true,
55
"type": "module",
66
"scripts": {

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "symphony_desktop"
3-
version = "0.1.14"
3+
version = "0.1.15"
44
edition = "2021"
55

66
[build-dependencies]

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"productName": "Symphony Console",
3-
"version": "0.1.14",
3+
"version": "0.1.15",
44
"identifier": "dev.symphony.console",
55
"build": {
66
"beforeDevCommand": "npm run dev",

src/main.tsx

Lines changed: 144 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ const KanbanCardSchema = z.object({
7878
last_message: z.string().nullable().optional(),
7979
turn_count: z.number().optional(),
8080
tokens: z.object({ total_tokens: z.number().default(0) }).passthrough().optional(),
81+
score_path: z.string().nullable().optional(),
82+
score_summary: z.string().nullable().optional(),
83+
phase_history: z.array(z.any()).optional(),
84+
conversation: z.array(z.any()).optional(),
8185
agent_profile: z.any().optional(),
8286
agent_profile_id: z.string().nullable().optional(),
8387
agent_name: z.string().nullable().optional(),
@@ -149,6 +153,20 @@ type MusicProfile = {
149153
dynamic?: string;
150154
register?: string;
151155
};
156+
type AgentConversationMessage = {
157+
at?: string;
158+
from: string;
159+
to: string;
160+
stage?: string;
161+
kind?: string;
162+
message: string;
163+
};
164+
type PhaseHistoryEntry = {
165+
at?: string;
166+
phase?: string;
167+
status?: string;
168+
agent?: string;
169+
};
152170
type IdleMusician = {
153171
id: string;
154172
profile: AgentProfile;
@@ -239,6 +257,10 @@ type Card = {
239257
section: string;
240258
music?: MusicProfile;
241259
stagePosition?: StagePosition;
260+
scorePath?: string;
261+
scoreSummary?: string;
262+
phaseHistory: PhaseHistoryEntry[];
263+
conversation: AgentConversationMessage[];
242264
intensity: number;
243265
source: 'live';
244266
raw?: unknown;
@@ -408,7 +430,7 @@ function normalizeColumn(title: string) {
408430
function liveAgentFor(column: string, status: string) {
409431
const s = status.toLowerCase();
410432
if (column === 'Retry' || s.includes('retry')) return 'Backoff Agent';
411-
if (column === 'Done' || s.includes('complete')) return 'Finisher';
433+
if (column === 'Done' || s.includes('complete')) return 'Completed Movement';
412434
if (column === 'Blocked' || s.includes('block')) return 'Guardian';
413435
if (s.includes('refin')) return 'Refiner';
414436
if (column === 'Human Review' || s.includes('review') || s.includes('judge')) return 'Reviewer';
@@ -536,7 +558,7 @@ function useAgentOrchestra({ enabled, cards, idleMusicians, offline }: { enabled
536558
const startAudio = () => {
537559
if (cancelled) return;
538560
masterRef.current!.gain.cancelScheduledValues(ctx.currentTime);
539-
masterRef.current!.gain.linearRampToValueAtTime(0.32, ctx.currentTime + 0.28);
561+
masterRef.current!.gain.linearRampToValueAtTime(0.48, ctx.currentTime + 0.28);
540562
nextTimeRef.current = ctx.currentTime + 0.05;
541563
if (timerRef.current) window.clearInterval(timerRef.current);
542564
scheduleAuditionCue(ctx, masterRef.current!);
@@ -634,7 +656,7 @@ function scheduleAuditionCue(ctx: AudioContext, out: AudioNode) {
634656
time: now + index * 0.09,
635657
freq: midiToFreq(midi),
636658
duration: 0.22,
637-
gain: 0.055,
659+
gain: 0.14,
638660
type: 'sine',
639661
filterHz: 6200,
640662
pan: 0
@@ -648,7 +670,7 @@ function scheduleConductorVoice(ctx: AudioContext, out: AudioNode, cards: Card[]
648670
if (!shouldPlay(rhythm, step, 0)) return;
649671

650672
const midi = tuning === 'dissonant' ? 38 : tuning === 'retuning' ? 43 : tuning === 'reviewing' ? 55 : 48;
651-
const gain = tuning === 'dissonant' ? 0.05 : tuning === 'retuning' ? 0.042 : 0.035;
673+
const gain = tuning === 'dissonant' ? 0.075 : tuning === 'retuning' ? 0.062 : 0.052;
652674
const type: OscillatorType = tuning === 'dissonant' ? 'sawtooth' : 'triangle';
653675
playSynthNote(ctx, out, {
654676
time,
@@ -700,25 +722,25 @@ function scheduleIdleVoice(ctx: AudioContext, out: AudioNode, musician: IdleMusi
700722
const degree = (step + index + seed) % scale.length;
701723
const midi = 36 + spec.octave * 12 + scale[degree] + (index % 2 === 0 ? 0 : 7);
702724
const pan = total <= 1 ? 0 : -0.7 + (index / (total - 1)) * 1.4;
703-
playSynthNote(ctx, out, { time, freq: midiToFreq(midi), duration: spec.duration * 1.35, gain: spec.gain * 0.45, type: spec.type, filterHz: spec.filterHz, pan });
725+
playSynthNote(ctx, out, { time, freq: midiToFreq(midi), duration: spec.duration * 1.35, gain: spec.gain * 0.62, type: spec.type, filterHz: spec.filterHz, pan });
704726
}
705727

706728
type SynthSpec = { type: OscillatorType; octave: number; gain: number; duration: number; filterHz: number; rhythm: string };
707729

708730
function synthSpec(voice: { column?: string; status: string; agent: string; instrumentName?: string; section?: string; music?: MusicProfile }) {
709731
const state = `${voice.column || ''} ${voice.status} ${voice.agent}`.toLowerCase();
710732
const instrument = `${voice.instrumentName || ''} ${voice.section || ''}`.toLowerCase();
711-
let spec: SynthSpec = { type: 'sine' as OscillatorType, octave: 4, gain: 0.014, duration: 0.22, filterHz: 3000, rhythm: 'sparse' };
712-
if (state.includes('block') || state.includes('guardian')) spec = { type: 'triangle' as OscillatorType, octave: 2, gain: 0.026, duration: 0.7, filterHz: 420, rhythm: 'drone' };
713-
else if (state.includes('retry') || state.includes('backoff')) spec = { type: 'sawtooth' as OscillatorType, octave: 3, gain: 0.028, duration: 0.16, filterHz: 900, rhythm: 'retry' };
714-
else if (instrument.includes('timpani') || instrument.includes('percussion') || instrument.includes('drum')) spec = { type: 'sawtooth' as OscillatorType, octave: 2, gain: 0.026, duration: 0.18, filterHz: 760, rhythm: 'retry' };
715-
else if (instrument.includes('piano')) spec = { type: 'triangle' as OscillatorType, octave: 4, gain: 0.024, duration: 0.32, filterHz: 2400, rhythm: 'review' };
716-
else if (instrument.includes('horn') || instrument.includes('trumpet') || instrument.includes('brass')) spec = { type: 'triangle' as OscillatorType, octave: 3, gain: 0.021, duration: 0.48, filterHz: 1150, rhythm: 'sparse' };
717-
else if (instrument.includes('clarinet') || instrument.includes('oboe') || instrument.includes('flute') || instrument.includes('woodwind')) spec = { type: 'triangle' as OscillatorType, octave: 4, gain: 0.017, duration: 0.3, filterHz: 2100, rhythm: 'sparse' };
718-
else if (instrument.includes('glockenspiel') || instrument.includes('bell')) spec = { type: 'sine' as OscillatorType, octave: 5, gain: 0.022, duration: 0.38, filterHz: 6200, rhythm: 'cadence' };
719-
else if (state.includes('review') || state.includes('judge')) spec = { type: 'triangle' as OscillatorType, octave: 4, gain: 0.024, duration: 0.32, filterHz: 2400, rhythm: 'review' };
720-
else if (state.includes('done') || state.includes('finish')) spec = { type: 'sine' as OscillatorType, octave: 5, gain: 0.024, duration: 0.42, filterHz: 6000, rhythm: 'cadence' };
721-
else if (state.includes('progress') || state.includes('run') || state.includes('builder') || instrument.includes('violin') || instrument.includes('viola') || instrument.includes('strings')) spec = { type: 'square' as OscillatorType, octave: 4, gain: 0.022, duration: 0.13, filterHz: 1700, rhythm: 'arpeggio' };
733+
let spec: SynthSpec = { type: 'sine' as OscillatorType, octave: 4, gain: 0.024, duration: 0.22, filterHz: 3000, rhythm: 'sparse' };
734+
if (state.includes('block') || state.includes('guardian')) spec = { type: 'triangle' as OscillatorType, octave: 2, gain: 0.038, duration: 0.7, filterHz: 420, rhythm: 'drone' };
735+
else if (state.includes('retry') || state.includes('backoff')) spec = { type: 'sawtooth' as OscillatorType, octave: 3, gain: 0.04, duration: 0.16, filterHz: 900, rhythm: 'retry' };
736+
else if (instrument.includes('timpani') || instrument.includes('percussion') || instrument.includes('drum')) spec = { type: 'sawtooth' as OscillatorType, octave: 2, gain: 0.044, duration: 0.18, filterHz: 760, rhythm: 'retry' };
737+
else if (instrument.includes('piano')) spec = { type: 'triangle' as OscillatorType, octave: 4, gain: 0.034, duration: 0.32, filterHz: 2400, rhythm: 'review' };
738+
else if (instrument.includes('horn') || instrument.includes('trumpet') || instrument.includes('brass')) spec = { type: 'triangle' as OscillatorType, octave: 3, gain: 0.034, duration: 0.48, filterHz: 1150, rhythm: 'sparse' };
739+
else if (instrument.includes('clarinet') || instrument.includes('oboe') || instrument.includes('flute') || instrument.includes('woodwind')) spec = { type: 'triangle' as OscillatorType, octave: 4, gain: 0.03, duration: 0.3, filterHz: 2100, rhythm: 'sparse' };
740+
else if (instrument.includes('glockenspiel') || instrument.includes('bell')) spec = { type: 'sine' as OscillatorType, octave: 5, gain: 0.034, duration: 0.38, filterHz: 6200, rhythm: 'cadence' };
741+
else if (state.includes('review') || state.includes('judge')) spec = { type: 'triangle' as OscillatorType, octave: 4, gain: 0.034, duration: 0.32, filterHz: 2400, rhythm: 'review' };
742+
else if (state.includes('done') || state.includes('finish')) spec = { type: 'sine' as OscillatorType, octave: 5, gain: 0.034, duration: 0.42, filterHz: 6000, rhythm: 'cadence' };
743+
else if (state.includes('progress') || state.includes('run') || state.includes('builder') || instrument.includes('violin') || instrument.includes('viola') || instrument.includes('strings')) spec = { type: 'square' as OscillatorType, octave: 4, gain: 0.032, duration: 0.13, filterHz: 1700, rhythm: 'arpeggio' };
722744
return applyMusicProfile(spec, voice.music);
723745
}
724746

@@ -796,6 +818,8 @@ function makeCardsFromKanban(kanban?: KanbanState): Card[] {
796818
const instrumentName = assignedProfile?.instrument_name || stringField(rawRecord, 'instrument_name') || instrumentNameFor(section, agent, status);
797819
const music = musicProfile(assignedProfile?.music);
798820
const profileStagePosition = stagePosition(assignedProfile?.stage_position);
821+
const conversation = conversationFromCard(rawRecord);
822+
const phaseHistory = phaseHistoryFromCard(rawRecord);
799823

800824
return {
801825
id: `${column.id}-${backendId}`,
@@ -820,6 +844,10 @@ function makeCardsFromKanban(kanban?: KanbanState): Card[] {
820844
section,
821845
music,
822846
stagePosition: profileStagePosition,
847+
scorePath: stringField(rawRecord, 'score_path'),
848+
scoreSummary: stringField(rawRecord, 'score_summary'),
849+
phaseHistory,
850+
conversation,
823851
intensity: intensityFor(tokens, turns, priority),
824852
source: 'live' as const,
825853
raw
@@ -899,6 +927,55 @@ function agentProfileFromCard(raw: z.infer<typeof KanbanCardSchema>) {
899927
};
900928
}
901929

930+
function conversationFromCard(raw: Record<string, unknown>): AgentConversationMessage[] {
931+
const messages = raw.conversation;
932+
if (!Array.isArray(messages)) return [];
933+
934+
return messages
935+
.map<AgentConversationMessage | null>(item => {
936+
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
937+
const record = item as Record<string, unknown>;
938+
const message = stringField(record, 'message');
939+
if (!message) return null;
940+
return {
941+
at: stringField(record, 'at') || undefined,
942+
from: stringField(record, 'from') || 'Agent',
943+
to: stringField(record, 'to') || 'Conductor',
944+
stage: stringField(record, 'stage') || undefined,
945+
kind: stringField(record, 'kind') || undefined,
946+
message
947+
};
948+
})
949+
.filter((item): item is AgentConversationMessage => Boolean(item))
950+
.slice()
951+
.reverse();
952+
}
953+
954+
function phaseHistoryFromCard(raw: Record<string, unknown>): PhaseHistoryEntry[] {
955+
const history = raw.phase_history;
956+
if (!Array.isArray(history)) return [];
957+
958+
return history
959+
.map<PhaseHistoryEntry | null>(item => {
960+
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
961+
const record = item as Record<string, unknown>;
962+
const phase = stringField(record, 'phase');
963+
const profile = record.agent_profile && typeof record.agent_profile === 'object' && !Array.isArray(record.agent_profile)
964+
? record.agent_profile as Record<string, unknown>
965+
: {};
966+
if (!phase) return null;
967+
return {
968+
at: stringField(record, 'at') || undefined,
969+
phase,
970+
status: stringField(record, 'status') || undefined,
971+
agent: stringField(profile, 'name') || undefined
972+
};
973+
})
974+
.filter((item): item is PhaseHistoryEntry => Boolean(item))
975+
.slice()
976+
.reverse();
977+
}
978+
902979
function stringField(value: Record<string, unknown>, key: string) {
903980
const field = value[key];
904981
return typeof field === 'string' && field.trim() ? field : '';
@@ -1972,11 +2049,11 @@ function Conductor({ active }: { active: boolean }) {
19722049

19732050
function Station({ station, count }: { station: StageStation; count: number }) {
19742051
return (
1975-
<button className={`station station-${station.kind}`} style={{ left: `${station.x}%`, top: `${station.y}%` }}>
2052+
<div className={`station station-${station.kind}`} style={{ left: `${station.x}%`, top: `${station.y}%` }}>
19762053
<span className="standTop"><FileText size={15} /></span>
19772054
<span className="stationLabel">{station.label}</span>
19782055
<span className="stationCount">{count}</span>
1979-
</button>
2056+
</div>
19802057
);
19812058
}
19822059

@@ -2179,17 +2256,65 @@ function MovementPanel({ card }: { card: Card }) {
21792256
<div><dt>Agent</dt><dd>{card.agent}</dd></div>
21802257
<div><dt>Backend ID</dt><dd><code>{card.backendId}</code></dd></div>
21812258
</dl>
2259+
{(card.scoreSummary || card.scorePath) && (
2260+
<div className="scoreArtifact">
2261+
<b>Conductor score</b>
2262+
{card.scoreSummary && <p>{card.scoreSummary}</p>}
2263+
{card.scorePath && <code>{card.scorePath}</code>}
2264+
</div>
2265+
)}
2266+
<PhaseTrace entries={card.phaseHistory} />
2267+
<AgentConversation messages={card.conversation} activeAgent={card.agent} />
21822268
<p>{card.message || card.retry || 'Waiting for the next orchestration cue.'}</p>
21832269
</>
21842270
);
21852271
}
21862272

2273+
function PhaseTrace({ entries }: { entries: PhaseHistoryEntry[] }) {
2274+
if (!entries.length) return null;
2275+
2276+
return (
2277+
<div className="phaseTrace">
2278+
{entries.slice(-8).map((entry, index) => (
2279+
<span key={`${entry.phase}-${entry.status}-${entry.at || index}`} className={`phaseChip ${statusClass(entry.status || entry.phase || '')}`}>
2280+
{entry.phase}
2281+
{entry.agent ? ` · ${entry.agent}` : ''}
2282+
</span>
2283+
))}
2284+
</div>
2285+
);
2286+
}
2287+
2288+
function AgentConversation({ messages, activeAgent }: { messages: AgentConversationMessage[]; activeAgent: string }) {
2289+
if (!messages.length) return <div className="conversationPanel empty"><p>No agent messages yet.</p></div>;
2290+
2291+
return (
2292+
<div className="conversationPanel" aria-label="Agent conversation">
2293+
{messages.slice(-12).map((message, index) => {
2294+
const own = message.from === activeAgent;
2295+
return (
2296+
<div className={own ? 'speechRow own' : 'speechRow'} key={`${message.at || index}-${message.from}-${message.kind || 'message'}`}>
2297+
<div className="speechBubble">
2298+
<div className="speechMeta">
2299+
<b>{message.from}</b>
2300+
<span>{message.to}{message.stage ? ` · ${message.stage}` : ''}</span>
2301+
</div>
2302+
<p>{message.message}</p>
2303+
</div>
2304+
</div>
2305+
);
2306+
})}
2307+
</div>
2308+
);
2309+
}
2310+
21872311
function MovementEvents({ card, debugPayload }: { card: Card; debugPayload: string | null }) {
21882312
const events = [
21892313
['Movement', card.movement],
21902314
['Status', card.status],
21912315
['Turns', String(card.turns)],
21922316
['Notes', card.tokens.toLocaleString()],
2317+
['Score path', card.scorePath || 'none'],
21932318
['Retry due', card.retry || 'none'],
21942319
['Last message', card.message || 'none reported']
21952320
];
@@ -2603,7 +2728,7 @@ function WorkflowPanel({
26032728
<div className="workflowComposer pixelPanel">
26042729
<div className="panelHeader">
26052730
<div>
2606-
<p className="eyebrow">Compose Score</p>
2731+
<p className="eyebrow">Compose Workflow</p>
26072732
<h2><Workflow size={20} /> WORKFLOW.md Builder</h2>
26082733
</div>
26092734
<span className={`pill ${validation?.valid ? 'success' : validation?.writable ? 'warning' : 'idle'}`}>{validation?.valid ? 'ready to activate' : validation?.writable ? 'writable draft' : 'draft'}</span>

0 commit comments

Comments
 (0)