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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions