v0.99.17
·
2394 commits
to main
since this release
Fixed
- conduct-ts: fixed
Fatal: __dirname is not definedcrash on startup.src/conductor/src/index.tsreferenced the CommonJS-only__dirnameglobal insidereadHarnessVersion(), but the bundle is ESM (tsupformat: ['esm'],shims: false), so the binary aborted before the CLI could parse args. Derived__dirnamefromimport.meta.urlusing the same pattern already insrc/conductor/src/engine/plugin-manifest.ts. - conduct-ts: SHIP-phase steps no longer silently mark a feature complete when pipeline exits mid-implementation. The conductor now stamps each invocation with
state.session_started_atand themanual_test,retro, andfinishcompletion predicates require fresh, feature-scoped evidence:manual_testrequires.docs/manual-test-results.mdwith no| FAILrows AND mtime >=session_started_at(previously had no completion gate at all — any clean REPL exit marked itdone)retrorequires a.docs/retros/*-<slug>.mdfile matching the currentfeature_descslug AND fresh mtime; falls back to "any retro fresh in this session" when slug is unavailable (previously matched any file under.docs/retros/, including stale prior-feature retros)finishrequires a fresh.pipeline/finish-choicemarker (mtime >=session_started_at); forchoice="pr", additionally requiresstate.pr_urlto be set; the conductor sweeps stale.pipeline/finish-choicefrom prior sessions onConductor.run()entry (previously the marker could survive across sessions andstate.pr_urlalone could pass the gate)
- conduct-ts:
buildcompletion predicate now fails when.pipeline/halt-user-input-requiredis present, even with all-completetask-status.json. A halt marker that survives to gate-check time means a true halt that bypassed the conductor's stall handler — the predicate now treats it as a build failure so the cascade through SHIP-phase steps doesn't fire. - conduct-ts: when auto-resume detects an "already complete" feature, the conductor now re-verifies the SHIP-phase predicates and offers a recovery prompt (roll back
feature_statusand resume at the first failing step, or keep state as-is). Self-heals worktrees that hit the prior false-completion bug. - skills/pipeline/SKILL.md: documents the "User-requested exit during a run" contract — when the user asks to "exit to harness", "stop and continue later", etc., the skill MUST write
.pipeline/halt-user-input-requiredbefore exiting and MUST NOT mark unfinished tasks ascompleted/skipped. Without the marker the conductor readstask-status.json, sees nothing in flight, and concludes the build step is done — silently cascading through SHIP to mark the feature complete while the user's actual blocker is still open. - skills/manual-test/SKILL.md: instructs the skill to save results to
.docs/manual-test-results.md(in addition to displaying in chat) so the conductor's completion gate can verify them. The previous "do NOT write to a file" wording contradicted what the bash conductor was already injecting at dispatch time. - CHANGELOG.md: fixed unclosed backtick in the preamble that the release workflow had to step around.
Added
- conduct-ts: new
--diagnoseCLI flag — non-mutating diagnostic that loads state for the named (or current) feature, re-verifies the SHIP-phase predicates, and prints any inconsistencies. Exits 0 when state is consistent, 1 when state is marked complete but evidence is missing. - conduct-ts: new
feature_completeevent payload fields (featureDesc,sessionStartedAt) and a multi-line bg-green completion banner inTerminalRendererso a finished run is impossible to read as "stopped processing without error" — the previous single-line green render could be missed in a long pipeline run. - conduct-ts: new
state.session_started_at?: number(epoch ms) — set on everyConductor.run()entry, used by SHIP-phase freshness checks. Purely additive; old state files deserialize fine. - conduct-ts: new
complete-verifier.tsmodule withverifyCompleteState(worktreePath)andformatGapReport(...)helpers, shared between auto-resume's recovery path and the--diagnoseflag. UIRendererinterface (handle(event): Promise<void>+stop()) insrc/conductor/src/ui/types.ts— new plugin contract for UI renderersTerminalRendererclass insrc/conductor/src/ui/terminal-renderer.tsimplementingUIRenderer(replaces thecreateRendererfactory function; backward-compat factory retained increate-renderer.ts)dispatchRenderers(renderers, event)insrc/conductor/src/ui/dispatch.ts— fan-out viaPromise.allSettled, renderer degradation (one throw doesn't kill others), re-emitsrenderer_errorevent to survivorsrenderer_errorevent type insrc/conductor/src/types/events.ts— carriesrendererNameanderrorstringRecordingRenderertest double intest/ui/recording-renderer.ts— records events, supportsdelayMsandthrowErrorinjectionregisterBuiltins()now accepts optionalTerminalRendererOptionsand registersTerminalRendererasui_renderer:terminal_rendereralongside the existingTerminalSubscriber- New test files:
test/ui/terminal-renderer.test.ts(TerminalRenderer class),test/ui/dispatch.test.ts(dispatch + degradation + slow-renderer + dup-renderer scenarios) RecorderProviderreference LLM provider plugin atplugins/recorder-provider/— logs everyinvoke()andinvokeInteractive()call as a JSONL line to a configurable path, returns a canned response, creates parent directories on first write, and throwsRecorderProviderErroron write failure- Unit tests for RecorderProvider (11 tests) covering JSONL format, canned response, parent-dir creation, error handling, concurrent writes, and invokeInteractive
- Integration tests for RecorderProvider flow (7 tests) covering happy path, misspelled kind rejection, missing plugin dir, version-incompatible manifest, and empty prompt
- RecorderProvider installs through the plugin loader with zero edits to
src/conductor/src/index.ts when?: stringfield onStepConfig— conditional step skip evaluated before dispatchparallel?: ParallelBranch[]field onStepConfig— concurrent step groups viaPromise.allParallelBranchtype:{ name, skill?, model?, effort?, advisory? }— discriminated from skill steps (mutual exclusion)evaluateWhen(expression, state)insrc/engine/when-expression.ts— five grammar forms:tier == L,tier in [M, L],phase == BUILD,${key} == value,A && BvalidateWhenSyntax(expression)— config-load-time syntax check, returns error string or null- Four new
ConductorEventvariants:when_skip,parallel_started,parallel_completed,parallel_failure - Conductor evaluates
when:before dispatching each step; emitswhen_skipwhen false - Conductor fans out
parallel:branches viaPromise.all; writes synthetic state keys<group>__<branch>toconduct-state.json - Gating branch failure (
advisory: false, the default) → group fails → downstream blocked - Advisory branch failure (
advisory: true) → logged viaparallel_failureevent, group continues to success when:on a parallel group → all synthetic keys set to"skipped"when expression is false- Terminal renderer handles
when_skip,parallel_started,parallel_completed,parallel_failureevents increate-renderer.ts - Config validator (
engine/config.ts) validateswhen:syntax andparallel:structure at config-load time - 59 new tests across
when-expression.test.ts,when-parallel.test.ts,when-parallel-renderer.test.ts - Feature 3.2: json-stdout-subscriber plugin — emits ConductorEvents as newline-delimited JSON to stdout; selectable via
ui_renderer: json-stdoutin config. Each line includes all original event fields plus atsISO timestamp. handle() before start() is a no-op (no crash). Plugin discovered automatically by the plugin loader — no changes tosrc/conductor/src/index.tsrequired. - Feature 4.1: EventPersister — every ConductorEvent persisted with timestamp to
.pipeline/events.jsonl(newline-delimited JSON, replayable). Subscribes to event bus as a listener; zero changes to emission sites inconductor.tsorstep-runners.ts. - Feature 4.1:
conduct --reportsubcommand — reads.pipeline/events.jsonland renders step durations (sorted descending), retry hotspots (with failed-step annotation), and token spend tables. Read-only; does not start a Claude session. - Feature 4.1: Optional
tokenUsagefield onInvokeResult— backwards-compatible;ClaudeProviderparses from Claude CLIstream-jsonoutput;RecorderProvidersynthesizes deterministic counts ({ input: 10, output: 5 }) for stable test fixtures. Report gracefully omits token rows when field is absent. - Plugin manifest schema (
plugin.yml) withkind,name,entrypoint,harness_version,capabilities?fields PluginKindenum:llm_provider | ui_renderer | step | hook | visualizer- Five typed error classes:
PluginManifestError,PluginVersionError,PluginLoadError,PluginNotFoundError,PluginRegistryError validateManifest()with required-field, kind-enum, name-format (/^[a-z0-9-]+$/), and semver compatibility checksloadManifestFromFile()wrapping YAML parse and I/O errors with file path contextPluginRegistryclass:register<K>(),get<T>(),list(),markInitialized()with initialization guarddiscoverPlugins(): scans global (~/.ai-conductor/plugins/) and project-local (.ai-conductor/plugins/) directories; project-local shadows global with debug logregisterBuiltins():ClaudeProvider→llm_provider:claude,TerminalSubscriber→ui_renderer:terminalsrc/index.tsrefactored: no longer hardcodesnew ClaudeProvider()ornew TerminalSubscriber()— both retrieved from registry- Integration tests: default-fallback (blank config → claude provider), EchoProvider E2E (external plugin discovery and invocation), version-mismatch and missing-entrypoint negative paths
Migration
New optional when: and parallel: stanzas in .ai-conductor/config.yml (Feature 3.1):
# Conditionally skip a step — skip 'brainstorm' on small features:
cat >> .ai-conductor/config.yml << 'EOF'
steps:
brainstorm:
when: "tier in [M, L]"
EOF
# Skip a step based on bootstrap mode:
cat >> .ai-conductor/config.yml << 'EOF'
steps:
assess:
when: "${bootstrap_mode} == fresh"
EOF
# Run two skills concurrently in a parallel group:
cat >> .ai-conductor/config.yml << 'EOF'
steps:
build:
parallel:
- name: frontend
skill: skills/build-frontend/SKILL.md
- name: backend
skill: skills/build-backend/SKILL.md
advisory: false # failure blocks the group (default)
EOF
# Combine when: with parallel: to skip the entire group on S-tier:
cat >> .ai-conductor/config.yml << 'EOF'
steps:
build:
when: "tier in [M, L]"
parallel:
- name: unit-tests
- name: integration-tests
advisory: true # failure is logged but group succeeds
EOFExisting projects require no changes — both when: and parallel: are opt-in.
New optional config stanzas in .ai-conductor/config.yml to select non-default plugins:
# Select a custom LLM provider (must be discoverable via plugin.yml in plugin dirs)
# Default is 'claude' (ClaudeProvider built-in); omit to keep using ClaudeProvider
echo "llm_provider: my-custom-provider" >> .ai-conductor/config.yml
# Select a custom UI renderer (default is 'terminal'; omit to keep using TerminalSubscriber)
echo "ui_renderer: my-custom-renderer" >> .ai-conductor/config.yml
# Install a plugin by placing plugin.yml + entrypoint in either:
# ~/.ai-conductor/plugins/<plugin-name>/ (global — all projects)
# .ai-conductor/plugins/<plugin-name>/ (project-local — overrides global)Existing projects require no changes — built-in defaults are preserved.