Skip to content

v0.99.17

Choose a tag to compare

@github-actions github-actions released this 02 May 13:08
· 2394 commits to main since this release

Fixed

  • conduct-ts: fixed Fatal: __dirname is not defined crash on startup. src/conductor/src/index.ts referenced the CommonJS-only __dirname global inside readHarnessVersion(), but the bundle is ESM (tsup format: ['esm'], shims: false), so the binary aborted before the CLI could parse args. Derived __dirname from import.meta.url using the same pattern already in src/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_at and the manual_test, retro, and finish completion predicates require fresh, feature-scoped evidence:
    • manual_test requires .docs/manual-test-results.md with no | FAIL rows AND mtime >= session_started_at (previously had no completion gate at all — any clean REPL exit marked it done)
    • retro requires a .docs/retros/*-<slug>.md file matching the current feature_desc slug 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)
    • finish requires a fresh .pipeline/finish-choice marker (mtime >= session_started_at); for choice="pr", additionally requires state.pr_url to be set; the conductor sweeps stale .pipeline/finish-choice from prior sessions on Conductor.run() entry (previously the marker could survive across sessions and state.pr_url alone could pass the gate)
  • conduct-ts: build completion predicate now fails when .pipeline/halt-user-input-required is present, even with all-complete task-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_status and 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-required before exiting and MUST NOT mark unfinished tasks as completed/skipped. Without the marker the conductor reads task-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 --diagnose CLI 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_complete event payload fields (featureDesc, sessionStartedAt) and a multi-line bg-green completion banner in TerminalRenderer so 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 every Conductor.run() entry, used by SHIP-phase freshness checks. Purely additive; old state files deserialize fine.
  • conduct-ts: new complete-verifier.ts module with verifyCompleteState(worktreePath) and formatGapReport(...) helpers, shared between auto-resume's recovery path and the --diagnose flag.
  • UIRenderer interface (handle(event): Promise<void> + stop()) in src/conductor/src/ui/types.ts — new plugin contract for UI renderers
  • TerminalRenderer class in src/conductor/src/ui/terminal-renderer.ts implementing UIRenderer (replaces the createRenderer factory function; backward-compat factory retained in create-renderer.ts)
  • dispatchRenderers(renderers, event) in src/conductor/src/ui/dispatch.ts — fan-out via Promise.allSettled, renderer degradation (one throw doesn't kill others), re-emits renderer_error event to survivors
  • renderer_error event type in src/conductor/src/types/events.ts — carries rendererName and error string
  • RecordingRenderer test double in test/ui/recording-renderer.ts — records events, supports delayMs and throwError injection
  • registerBuiltins() now accepts optional TerminalRendererOptions and registers TerminalRenderer as ui_renderer:terminal_renderer alongside the existing TerminalSubscriber
  • New test files: test/ui/terminal-renderer.test.ts (TerminalRenderer class), test/ui/dispatch.test.ts (dispatch + degradation + slow-renderer + dup-renderer scenarios)
  • RecorderProvider reference LLM provider plugin at plugins/recorder-provider/ — logs every invoke() and invokeInteractive() call as a JSONL line to a configurable path, returns a canned response, creates parent directories on first write, and throws RecorderProviderError on 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?: string field on StepConfig — conditional step skip evaluated before dispatch
  • parallel?: ParallelBranch[] field on StepConfig — concurrent step groups via Promise.all
  • ParallelBranch type: { name, skill?, model?, effort?, advisory? } — discriminated from skill steps (mutual exclusion)
  • evaluateWhen(expression, state) in src/engine/when-expression.ts — five grammar forms: tier == L, tier in [M, L], phase == BUILD, ${key} == value, A && B
  • validateWhenSyntax(expression) — config-load-time syntax check, returns error string or null
  • Four new ConductorEvent variants: when_skip, parallel_started, parallel_completed, parallel_failure
  • Conductor evaluates when: before dispatching each step; emits when_skip when false
  • Conductor fans out parallel: branches via Promise.all; writes synthetic state keys <group>__<branch> to conduct-state.json
  • Gating branch failure (advisory: false, the default) → group fails → downstream blocked
  • Advisory branch failure (advisory: true) → logged via parallel_failure event, 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_failure events in create-renderer.ts
  • Config validator (engine/config.ts) validates when: syntax and parallel: 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-stdout in config. Each line includes all original event fields plus a ts ISO timestamp. handle() before start() is a no-op (no crash). Plugin discovered automatically by the plugin loader — no changes to src/conductor/src/index.ts required.
  • 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 in conductor.ts or step-runners.ts.
  • Feature 4.1: conduct --report subcommand — reads .pipeline/events.jsonl and 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 tokenUsage field on InvokeResult — backwards-compatible; ClaudeProvider parses from Claude CLI stream-json output; RecorderProvider synthesizes deterministic counts ({ input: 10, output: 5 }) for stable test fixtures. Report gracefully omits token rows when field is absent.
  • Plugin manifest schema (plugin.yml) with kind, name, entrypoint, harness_version, capabilities? fields
  • PluginKind enum: 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 checks
  • loadManifestFromFile() wrapping YAML parse and I/O errors with file path context
  • PluginRegistry class: register<K>(), get<T>(), list(), markInitialized() with initialization guard
  • discoverPlugins(): scans global (~/.ai-conductor/plugins/) and project-local (.ai-conductor/plugins/) directories; project-local shadows global with debug log
  • registerBuiltins(): ClaudeProviderllm_provider:claude, TerminalSubscriberui_renderer:terminal
  • src/index.ts refactored: no longer hardcodes new ClaudeProvider() or new 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
EOF

Existing 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.