Skip to content

[analyze 3/3] agentworkforce analyze: subcommand wiring + proposal walk + write to disk #77

@willwashburn

Description

@willwashburn

Part of the agentworkforce analyze feature. Issue 3 of 3. Depends on #75 (gather), #76 (persona-discoverer), and #71 (persona-kit publish).

This issue assumes the persona-kit migration (#64#71) has shipped. The spawn flow below uses the post-migration buildPersonaSpawnPlan / executePersonaSpawnPlan API, not the pre-migration runAgentSelector.

Goal

Wire up the user-facing agentworkforce analyze subcommand. This is the orchestration layer: invoke the gather module, launch the persona-discoverer persona, walk the proposals interactively, and write accepted personas to disk.

After this lands, a user in any repo can run agentworkforce analyze and end up with 3–7 starter personas in ./.agentworkforce/workforce/personas/, each grounded in a real cluster of work the repo has been doing.

Files to touch

New:

  • packages/cli/src/analyze-walk.ts — proposal parser + interactive accept-loop + disk writer.
  • packages/cli/src/analyze-walk.test.ts — Node test runner.

Modify:

  • packages/cli/src/cli.ts:
    • Add 'analyze' to the subcommand dispatcher.
    • Add parseAnalyzeArgs(rest) + runAnalyze(flags).
    • Update the USAGE const with the new line.
  • packages/cli/README.md — short analyze section: usage, flags, example.

Type imports: PersonaSpec (used by analyze-walk.ts) imports from @agentworkforce/persona-kit, not @agentworkforce/workload-router.

Pre-existing helpers that likely stay in cli.ts post-migration (interactive UX, not spawn logic — verify before assuming): parseProposals + applyAcceptedPatches from the auto-improve flow; readSingleCharChoice; promptYesNoSync; resolveCreateTarget / ensureCreateTargetDir / buildCreateInputValues. If any of these moved to persona-kit during the migration, adjust the import path — the behavior contract is unchanged.

Flags

Flag Default Purpose
--lookback-days <n> 90 git/pr window
--max-commits <n> 500 hard cap on commits gathered
--no-prs off skip gh call entirely
--no-sessions off skip burn-stamp scan
--save-in-directory=<t> cwd (./.agentworkforce/workforce/personas) matches create; supports cwd|user|library|dir:n|<path>
--overwrite off replace existing personas/<id>.json on disk
--tier <best|best-value|minimum> best-value analyzer tier
--dry-run off gather only; print summary; skip analyzer + walk
--no-launch-metadata off mirror agent / create

Flow

runAnalyze(flags) does:

  1. Resolve target dir via resolveCreateTarget / ensureCreateTargetDir. Same --save-in-directory semantics as create.
  2. Create a temp dir under os.tmpdir(); allocate analysisInputPath + proposalsOutputPath.
  3. Phase 1 — Gather. ora spinner. Call gather() from [analyze 1/3] analyze-gather: collect git/PR/codebase/session signal into JSON #75 with the resolved bounds, write the result to analysisInputPath. Print a one-line summary: Gathered N commits, M PRs, K sessions, P packages.
  4. If --dry-run: print summary, exit 0. Do not launch the persona, do not walk.
  5. Phase 2 — Synthesize. Launch persona-discoverer@<tier> via persona-kit's spawn API:
    const loaded   = loadPersonas({ cwd: process.cwd(), searchDirs });
    const spec     = loaded.byId.get('persona-discoverer');
    const persona  = resolvePersonaTier(spec, flags.tier);
    const plan     = buildPersonaSpawnPlan(persona, {
      cwd: process.cwd(),
      installRoot,
      envOverrides: { ANALYSIS_INPUT_PATH, PROPOSALS_OUTPUT_PATH, TARGET_DIR },
    });
    const handle   = await executePersonaSpawnPlan(plan, { cwd: process.cwd() });
    try {
      const child = spawn(plan.cli, plan.args, { cwd: process.cwd(), env: plan.env, stdio: 'inherit' });
      await waitForExit(child);
    } finally {
      await handle.dispose();
    }
    Persona inputs are wired via envOverrides on the plan — there is no inputValues argument anymore. The harness stdio is inherit so the user sees the analyzer working (matches create UX).
  6. Phase 3 — Walk + write. Read proposalsOutputPath, parse + validate, walk interactively.
  7. Print final tally.
  8. Clean up the temp dir.

analyze-walk.ts API

export interface AnalyzeProposal { id: string; summary: string; rationale: string; persona: PersonaSpec; }
export interface ParsedProposals { analysisInputPath: string; proposals: AnalyzeProposal[]; }

export function parseAnalyzeProposals(raw: string): ParsedProposals;
export async function walkAndWrite(opts: {
  proposals: ParsedProposals;
  targetDir: string;
  overwrite: boolean;
  io?: { write?: (s: string) => void; read?: () => string | undefined; isTTY?: boolean };
}): Promise<{ written: string[]; skipped: string[]; rejected: string[] }>;

Behavior:

  • parseAnalyzeProposals validates required fields (id kebab-case, summary <=80 chars, rationale non-empty, persona is a valid PersonaSpec). Reuse the validators in parseProposals at cli.ts:3164–3225 — share helpers; do not duplicate. Throws on schema violations with a clear pointer to the offending proposal.
  • walkAndWrite: for each proposal, print summary + rationale + a compact preview of the persona (id, intent, tags, description, model per tier), then prompt accept? [y/N/a/q] via readSingleCharChoice (cli.ts:3309).
    • y — accept this one.
    • N (default on empty) — skip.
    • a — accept this and all remaining.
    • q — quit; everything not yet decided is rejected.
  • Write accepted proposals to ${targetDir}/<id>.json (2-space indent + trailing newline — match existing applyAcceptedPatches write format).
  • On id collision: if --overwrite, replace; else skip with a warning. Track in skipped.

Tasks

  • Implement parseAnalyzeProposals with full schema validation reusing the helpers from cli.ts:3164–3225.
  • Implement walkAndWrite with the four-choice prompt loop.
  • Implement parseAnalyzeArgs(rest) + runAnalyze(flags) in cli.ts. Match the parser style used by parseCreateArgs / parseAgentArgs.
  • Add the analyze branch to the dispatcher.
  • Update USAGE const with analyze [flags] line.
  • Update packages/cli/README.md — short section, mirror the create section's tone.

Tests

  • parseAnalyzeProposals: canned valid JSON → parsed object. Canned invalid JSON (bad id, missing tier, etc.) → throws with line/proposal pointer.
  • walkAndWrite: stub IO that returns y, n, a, q in various sequences → assert exactly the expected files are written under a temp TARGET_DIR. Assert a short-circuits the remaining prompts.
  • Id collision: pre-populate TARGET_DIR/foo.json → walk a proposal with id: 'foo' → without overwrite, file unchanged + skipped: ['foo']; with overwrite: true, file replaced + written: ['foo'].
  • parseAnalyzeArgs: every flag in the table above produces the expected AnalyzeFlags shape; unknown flags error with a usage hint.

Verification

  • corepack pnpm --filter @agentworkforce/cli test passes.
  • corepack pnpm -r build clean.
  • Dry-run end-to-end (no LLM, no writes): npm run dev:cli -- analyze --dry-run --lookback-days 30 in this repo. Expected: gather summary line, exit 0, no persona launched, no files written.
  • Live run against this repo: npm run dev:cli -- analyze --tier minimum --lookback-days 90. Expected: spinner through gather → analyzer launches and runs → proposals walk → accepted personas land in ./.agentworkforce/workforce/personas/.
  • For each accepted persona: agentworkforce list shows it and agentworkforce agent <id>@best-value --dry-run passes cleanly.
  • Negative paths: with gh uninstalled, with AGENTWORKFORCE_LAUNCH_METADATA=0 and no prior burn data, and re-running analyze after a previous accept (id collision without --overwrite) — all behave per spec.

Constraints

  • No new runtime dependencies. Reuse ora, readSingleCharChoice, promptYesNoSync, resolveCreateTarget, etc.
  • Clean up the temp dir on every exit path, including SIGINT during the walk.
  • Don't crash on partial signal. Empty prs, empty sessions, empty commit history (brand-new repo) all need to produce a useful error message or proceed gracefully — not a stack trace.
  • Walk UX matches auto-improve. Same four-choice grammar, same prompt format, same spinner style — users should feel one is a sibling of the other.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions