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

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

FlagDefaultPurpose
--lookback-days <n>90git/pr window
--max-commits <n>500hard cap on commits gathered
--no-prsoffskip gh call entirely
--no-sessionsoffskip burn-stamp scan
--save-in-directory=<t>cwd (./.agentworkforce/workforce/personas)matches create; supports cwd|user|library|dir:n|<path>
--overwriteoffreplace existing personas/<id>.json on disk
--tier <best|best-value|minimum>best-valueanalyzer tier
--dry-runoffgather only; print summary; skip analyzer + walk
--no-launch-metadataoffmirror 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:
    constloaded=loadPersonas({cwd: process.cwd(), searchDirs });constspec=loaded.byId.get('persona-discoverer');constpersona=resolvePersonaTier(spec,flags.tier);constplan=buildPersonaSpawnPlan(persona,{cwd: process.cwd(),
    installRoot,envOverrides: {ANALYSIS_INPUT_PATH,PROPOSALS_OUTPUT_PATH,TARGET_DIR},});consthandle=awaitexecutePersonaSpawnPlan(plan,{cwd: process.cwd()});try{constchild=spawn(plan.cli,plan.args,{cwd: process.cwd(),env: plan.env,stdio: 'inherit'});awaitwaitForExit(child);}finally{awaithandle.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

exportinterfaceAnalyzeProposal{id: string;summary: string;rationale: string;persona: PersonaSpec;}exportinterfaceParsedProposals{analysisInputPath: string;proposals: AnalyzeProposal[];}exportfunctionparseAnalyzeProposals(raw: string): ParsedProposals;exportasyncfunctionwalkAndWrite(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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

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

      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

      FlagDefaultPurpose
      --lookback-days <n>90git/pr window
      --max-commits <n>500hard cap on commits gathered
      --no-prsoffskip gh call entirely
      --no-sessionsoffskip burn-stamp scan
      --save-in-directory=<t>cwd (./.agentworkforce/workforce/personas)matches create; supports cwd|user|library|dir:n|<path>
      --overwriteoffreplace existing personas/<id>.json on disk
      --tier <best|best-value|minimum>best-valueanalyzer tier
      --dry-runoffgather only; print summary; skip analyzer + walk
      --no-launch-metadataoffmirror 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:
        constloaded=loadPersonas({cwd: process.cwd(), searchDirs });constspec=loaded.byId.get('persona-discoverer');constpersona=resolvePersonaTier(spec,flags.tier);constplan=buildPersonaSpawnPlan(persona,{cwd: process.cwd(),
        installRoot,envOverrides: {ANALYSIS_INPUT_PATH,PROPOSALS_OUTPUT_PATH,TARGET_DIR},});consthandle=awaitexecutePersonaSpawnPlan(plan,{cwd: process.cwd()});try{constchild=spawn(plan.cli,plan.args,{cwd: process.cwd(),env: plan.env,stdio: 'inherit'});awaitwaitForExit(child);}finally{awaithandle.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

      exportinterfaceAnalyzeProposal{id: string;summary: string;rationale: string;persona: PersonaSpec;}exportinterfaceParsedProposals{analysisInputPath: string;proposals: AnalyzeProposal[];}exportfunctionparseAnalyzeProposals(raw: string): ParsedProposals;exportasyncfunctionwalkAndWrite(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.

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      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

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

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

          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

          FlagDefaultPurpose
          --lookback-days <n>90git/pr window
          --max-commits <n>500hard cap on commits gathered
          --no-prsoffskip gh call entirely
          --no-sessionsoffskip burn-stamp scan
          --save-in-directory=<t>cwd (./.agentworkforce/workforce/personas)matches create; supports cwd|user|library|dir:n|<path>
          --overwriteoffreplace existing personas/<id>.json on disk
          --tier <best|best-value|minimum>best-valueanalyzer tier
          --dry-runoffgather only; print summary; skip analyzer + walk
          --no-launch-metadataoffmirror 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:
            constloaded=loadPersonas({cwd: process.cwd(), searchDirs });constspec=loaded.byId.get('persona-discoverer');constpersona=resolvePersonaTier(spec,flags.tier);constplan=buildPersonaSpawnPlan(persona,{cwd: process.cwd(),
            installRoot,envOverrides: {ANALYSIS_INPUT_PATH,PROPOSALS_OUTPUT_PATH,TARGET_DIR},});consthandle=awaitexecutePersonaSpawnPlan(plan,{cwd: process.cwd()});try{constchild=spawn(plan.cli,plan.args,{cwd: process.cwd(),env: plan.env,stdio: 'inherit'});awaitwaitForExit(child);}finally{awaithandle.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

          exportinterfaceAnalyzeProposal{id: string;summary: string;rationale: string;persona: PersonaSpec;}exportinterfaceParsedProposals{analysisInputPath: string;proposals: AnalyzeProposal[];}exportfunctionparseAnalyzeProposals(raw: string): ParsedProposals;exportasyncfunctionwalkAndWrite(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.

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          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

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

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

              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

              FlagDefaultPurpose
              --lookback-days <n>90git/pr window
              --max-commits <n>500hard cap on commits gathered
              --no-prsoffskip gh call entirely
              --no-sessionsoffskip burn-stamp scan
              --save-in-directory=<t>cwd (./.agentworkforce/workforce/personas)matches create; supports cwd|user|library|dir:n|<path>
              --overwriteoffreplace existing personas/<id>.json on disk
              --tier <best|best-value|minimum>best-valueanalyzer tier
              --dry-runoffgather only; print summary; skip analyzer + walk
              --no-launch-metadataoffmirror 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:
                constloaded=loadPersonas({cwd: process.cwd(), searchDirs });constspec=loaded.byId.get('persona-discoverer');constpersona=resolvePersonaTier(spec,flags.tier);constplan=buildPersonaSpawnPlan(persona,{cwd: process.cwd(),
                installRoot,envOverrides: {ANALYSIS_INPUT_PATH,PROPOSALS_OUTPUT_PATH,TARGET_DIR},});consthandle=awaitexecutePersonaSpawnPlan(plan,{cwd: process.cwd()});try{constchild=spawn(plan.cli,plan.args,{cwd: process.cwd(),env: plan.env,stdio: 'inherit'});awaitwaitForExit(child);}finally{awaithandle.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

              exportinterfaceAnalyzeProposal{id: string;summary: string;rationale: string;persona: PersonaSpec;}exportinterfaceParsedProposals{analysisInputPath: string;proposals: AnalyzeProposal[];}exportfunctionparseAnalyzeProposals(raw: string): ParsedProposals;exportasyncfunctionwalkAndWrite(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.

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              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

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

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

                  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

                  FlagDefaultPurpose
                  --lookback-days <n>90git/pr window
                  --max-commits <n>500hard cap on commits gathered
                  --no-prsoffskip gh call entirely
                  --no-sessionsoffskip burn-stamp scan
                  --save-in-directory=<t>cwd (./.agentworkforce/workforce/personas)matches create; supports cwd|user|library|dir:n|<path>
                  --overwriteoffreplace existing personas/<id>.json on disk
                  --tier <best|best-value|minimum>best-valueanalyzer tier
                  --dry-runoffgather only; print summary; skip analyzer + walk
                  --no-launch-metadataoffmirror 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:
                    constloaded=loadPersonas({cwd: process.cwd(), searchDirs });constspec=loaded.byId.get('persona-discoverer');constpersona=resolvePersonaTier(spec,flags.tier);constplan=buildPersonaSpawnPlan(persona,{cwd: process.cwd(),
                    installRoot,envOverrides: {ANALYSIS_INPUT_PATH,PROPOSALS_OUTPUT_PATH,TARGET_DIR},});consthandle=awaitexecutePersonaSpawnPlan(plan,{cwd: process.cwd()});try{constchild=spawn(plan.cli,plan.args,{cwd: process.cwd(),env: plan.env,stdio: 'inherit'});awaitwaitForExit(child);}finally{awaithandle.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

                  exportinterfaceAnalyzeProposal{id: string;summary: string;rationale: string;persona: PersonaSpec;}exportinterfaceParsedProposals{analysisInputPath: string;proposals: AnalyzeProposal[];}exportfunctionparseAnalyzeProposals(raw: string): ParsedProposals;exportasyncfunctionwalkAndWrite(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.

                  Activity

                  Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                  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

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

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

                      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

                      FlagDefaultPurpose
                      --lookback-days <n>90git/pr window
                      --max-commits <n>500hard cap on commits gathered
                      --no-prsoffskip gh call entirely
                      --no-sessionsoffskip burn-stamp scan
                      --save-in-directory=<t>cwd (./.agentworkforce/workforce/personas)matches create; supports cwd|user|library|dir:n|<path>
                      --overwriteoffreplace existing personas/<id>.json on disk
                      --tier <best|best-value|minimum>best-valueanalyzer tier
                      --dry-runoffgather only; print summary; skip analyzer + walk
                      --no-launch-metadataoffmirror 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:
                        constloaded=loadPersonas({cwd: process.cwd(), searchDirs });constspec=loaded.byId.get('persona-discoverer');constpersona=resolvePersonaTier(spec,flags.tier);constplan=buildPersonaSpawnPlan(persona,{cwd: process.cwd(),
                        installRoot,envOverrides: {ANALYSIS_INPUT_PATH,PROPOSALS_OUTPUT_PATH,TARGET_DIR},});consthandle=awaitexecutePersonaSpawnPlan(plan,{cwd: process.cwd()});try{constchild=spawn(plan.cli,plan.args,{cwd: process.cwd(),env: plan.env,stdio: 'inherit'});awaitwaitForExit(child);}finally{awaithandle.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

                      exportinterfaceAnalyzeProposal{id: string;summary: string;rationale: string;persona: PersonaSpec;}exportinterfaceParsedProposals{analysisInputPath: string;proposals: AnalyzeProposal[];}exportfunctionparseAnalyzeProposals(raw: string): ParsedProposals;exportasyncfunctionwalkAndWrite(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.

                      Activity

                      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                      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

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

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

                          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

                          FlagDefaultPurpose
                          --lookback-days <n>90git/pr window
                          --max-commits <n>500hard cap on commits gathered
                          --no-prsoffskip gh call entirely
                          --no-sessionsoffskip burn-stamp scan
                          --save-in-directory=<t>cwd (./.agentworkforce/workforce/personas)matches create; supports cwd|user|library|dir:n|<path>
                          --overwriteoffreplace existing personas/<id>.json on disk
                          --tier <best|best-value|minimum>best-valueanalyzer tier
                          --dry-runoffgather only; print summary; skip analyzer + walk
                          --no-launch-metadataoffmirror 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:
                            constloaded=loadPersonas({cwd: process.cwd(), searchDirs });constspec=loaded.byId.get('persona-discoverer');constpersona=resolvePersonaTier(spec,flags.tier);constplan=buildPersonaSpawnPlan(persona,{cwd: process.cwd(),
                            installRoot,envOverrides: {ANALYSIS_INPUT_PATH,PROPOSALS_OUTPUT_PATH,TARGET_DIR},});consthandle=awaitexecutePersonaSpawnPlan(plan,{cwd: process.cwd()});try{constchild=spawn(plan.cli,plan.args,{cwd: process.cwd(),env: plan.env,stdio: 'inherit'});awaitwaitForExit(child);}finally{awaithandle.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

                          exportinterfaceAnalyzeProposal{id: string;summary: string;rationale: string;persona: PersonaSpec;}exportinterfaceParsedProposals{analysisInputPath: string;proposals: AnalyzeProposal[];}exportfunctionparseAnalyzeProposals(raw: string): ParsedProposals;exportasyncfunctionwalkAndWrite(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.

                          Activity

                          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                          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

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

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

                              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

                              FlagDefaultPurpose
                              --lookback-days <n>90git/pr window
                              --max-commits <n>500hard cap on commits gathered
                              --no-prsoffskip gh call entirely
                              --no-sessionsoffskip burn-stamp scan
                              --save-in-directory=<t>cwd (./.agentworkforce/workforce/personas)matches create; supports cwd|user|library|dir:n|<path>
                              --overwriteoffreplace existing personas/<id>.json on disk
                              --tier <best|best-value|minimum>best-valueanalyzer tier
                              --dry-runoffgather only; print summary; skip analyzer + walk
                              --no-launch-metadataoffmirror 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:
                                constloaded=loadPersonas({cwd: process.cwd(), searchDirs });constspec=loaded.byId.get('persona-discoverer');constpersona=resolvePersonaTier(spec,flags.tier);constplan=buildPersonaSpawnPlan(persona,{cwd: process.cwd(),
                                installRoot,envOverrides: {ANALYSIS_INPUT_PATH,PROPOSALS_OUTPUT_PATH,TARGET_DIR},});consthandle=awaitexecutePersonaSpawnPlan(plan,{cwd: process.cwd()});try{constchild=spawn(plan.cli,plan.args,{cwd: process.cwd(),env: plan.env,stdio: 'inherit'});awaitwaitForExit(child);}finally{awaithandle.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

                              exportinterfaceAnalyzeProposal{id: string;summary: string;rationale: string;persona: PersonaSpec;}exportinterfaceParsedProposals{analysisInputPath: string;proposals: AnalyzeProposal[];}exportfunctionparseAnalyzeProposals(raw: string): ParsedProposals;exportasyncfunctionwalkAndWrite(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.

                              Activity

                              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                              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