D-cli-draft-next-step

Workstream D — CLI command gxwf draft-next-step

Goal

Wire nextDraftStep (workstream B) behind a gxwf draft-next-step CLI command. Default output is the locked JSON shape (it IS the wire format for agent loops); a --format markdown rendering is the nice-to-have. Pure pass-through — no extra logic, no I/O beyond reading the file and printing the result.

Inputs

Locked decisions (this subplan)

DecisionOutcome
Default outputJSON to stdout. This is the agent-loop wire format; no flag needed to get JSON.
Markdown renderingNice-to-have via --format markdown. Renders the work[] as a checklist; useful for humans during onboarding. v1 must ship JSON; markdown can land in a follow-up commit if scope expands. Recommend ship together.
Non-draft inputEmit { "draft": false } and exit 0. The function is total; the CLI mirrors that. Same for fully-concrete draft.
Pretty-printing JSONYes, 2-space indent. Human-readable; agents that want compact can jq -c.
Tree variantOut of scope (v2). No draft-next-step-tree.
Format flagHonor --format <fmt> for input auto-detection alignment but reject non-format2 (drafts only).
Exit codes0 always when input parses + is a (possibly non-draft) workflow document; 2 only for parse / read failure. No “draft has work” vs “draft has none” exit distinction — both are exit 0.
IdempotencePreserved end-to-end. JSON stringify of two runs MUST be byte-identical.
Tie-break for list-form steps without label:Per B (already implemented): label key falls back to step id then array index. CLI inherits this.

Pipeline gaps D closes

1. Spec entry — packages/cli/spec/gxwf.json

{
  "name": "draft-next-step",
  "description": "Pick the next step a downstream agent should work on (or report no remaining work)",
  "handler": "draftNextStep",
  "args": [{ "raw": "<file>", "description": "Draft workflow file (.gxwf.yml)" }],
  "options": [
    { "flags": "--format <fmt>", "description": "Input format: format2 (default; native is rejected)" },
    { "flags": "--output-format <fmt>", "description": "Output format: json (default) or markdown", "default": "json" }
  ]
}

Note: --format controls input (matches validate/clean convention); --output-format selects JSON vs markdown. Avoids overloading --format two ways. Alternative names considered: --render, --as. Pick --output-format — descriptive, no abbreviation collisions with existing flags.

2. Handler — packages/cli/src/commands/draft-next-step.ts

export interface DraftNextStepOptions {
  format?: string;          // input format
  outputFormat?: "json" | "markdown";
}

export async function runDraftNextStep(file: string, opts: DraftNextStepOptions): Promise<void>;

Pipeline:

  1. readWorkflowFile(file) — parse failure → exit 2.
  2. resolveFormat(parsed, opts.format) — native rejected.
  3. const result = nextDraftStep(parsed);
  4. Emit per outputFormat:
    • "json" (default): console.log(JSON.stringify(result, null, 2));
    • "markdown": render via a tiny inline renderer (see template below).
  5. Exit 0.

3. Handler registry — packages/cli/src/programs/gxwf.ts

import { runDraftNextStep } from "../commands/draft-next-step.js";
// ...
const handlers: HandlerRegistry = {
  // ...
  draftNextStep: runDraftNextStep,
};

4. Markdown renderer (inline, no template engine needed)

function renderMarkdown(result: NextStepResult): string {
  if (!result.draft) return "_No remaining draft work._\n";
  const heading = `## Next step: \`${result.step.join(" / ")}\`\n\n`;
  const items = result.work.map((w) => `- [ ] ${w}`).join("\n");
  return heading + items + "\n";
}

Embedded in draft-next-step.ts; no new template file. (HTML report is NOT needed for this command — agents consume JSON; humans get markdown.)

5. Report model — report-models.ts

The NextStepResult type is already exported from @galaxy-tool-util/schema (workstream B). D does NOT add a new report-model interface; NextStepResult IS the wire format. If a NextStepSuggestion alias is desired for Python parity, add a type NextStepSuggestion = NextStepResult re-export — but only if reviewers want the naming bridge.

Test plan (vitest)

Tests live in packages/cli/test/draft-next-step.test.ts (sibling-style, matches the convention C actually shipped — packages/cli/test/draft-validate.test.ts, clean.test.ts, lint.test.ts; the test/commands/ subdir is not used). Reuse the CLI draft fixture dir at packages/cli/test/fixtures/draft/ that C established (synthetic-draft-tool-step.gxwf.yml, synthetic-draft-plan-top-level.gxwf.yml, synthetic-draft-plan-subworkflow.gxwf.yml).

Use the shared createCliTestContext harness from packages/cli/test/helpers/cli-test-context.ts — it sets up a tmp dir, mocks console.log / console.error / process.stdout.write, and resets process.exitCode. Pattern (from draft-validate.test.ts):

import { createCliTestContext, type CliTestContext } from "./helpers/cli-test-context.js";
// ...
let ctx: CliTestContext;
beforeEach(async () => { ctx = await createCliTestContext("draft-next-step"); });
afterEach(async () => { await ctx.cleanup(); });

Red-to-green:

  1. JSON output on draft fixture — synthetic-draft-tool-step → exit 0, stdout parses as JSON matching { draft: true, step: ["fastp"], work: [...] } with the locked work[] order.
  2. Non-draft documentclass: GalaxyWorkflow → exit 0, stdout { "draft": false }.
  3. Fully concrete draft — draft with no TODOs / _plan_* → exit 0, stdout { "draft": false }.
  4. Subworkflow descent — outer concrete + inner draft → step path is [outer, inner_step].
  5. Tie-break determinism — two steps at level 0 → alphabetical wins, asserts the order.
  6. Markdown rendering--output-format markdown → checklist with - [ ] items.
  7. Native input rejected.ga file → exit 2.
  8. Idempotence — run the command twice on the same fixture, capture stdout, byte-compare.

Call runDraftNextStep directly with stdout captured via the createCliTestContext spies (matches the convention C settled on; spawning the full commander program is unnecessary for handler-level coverage). See packages/cli/test/draft-validate.test.ts for the shape.

Out of scope for D

Acceptance criteria

Sequencing inside D

commit 1  cli: draft-next-step handler + spec entry + handler registry + JSON output      (+ tests)
commit 2  cli: --output-format markdown renderer                                            (+ test)
commit 3  Changeset + regen gxwf-cli skill doc

(Commits 1 + 2 could collapse into one — both are small. Keep separate if the user wants finer-grained review history.)

Post-C carry-overs

What landed during C that this plan should inherit verbatim:

Open questions for D