Skip to content

Migrate workforce CLI to consume @agentworkforce/persona-kit#78

Merged
willwashburn merged 3 commits into
mainfrom
claude/resolve-github-issue-n0PFo
May 11, 2026
Merged

Migrate workforce CLI to consume @agentworkforce/persona-kit#78
willwashburn merged 3 commits into
mainfrom
claude/resolve-github-issue-n0PFo

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Closes #67 (the part of 4/8 that removes the CLI's hard dependency on @agentworkforce/harness-kit).

Summary

  • Rewires every persona-shape import in packages/cli/ to come from @agentworkforce/persona-kit. The CLI now imports only personaCatalog, listBuiltInPersonas, and routingProfiles from @agentworkforce/workload-router, per the issue's "keep the routing-profile lookup" constraint.
  • Drops @agentworkforce/harness-kit from packages/cli/package.json. grep '@agentworkforce/harness-kit' packages/cli/ returns zero hits after this PR.
  • Replaces useSelection().install (from workload-router) inside the spawn flow with an inline CLI helper that calls persona-kit's pure helpers (materializeSkills + buildInstallArtifacts + buildCleanupArtifacts). This keeps the CLI's spinner-driven install/cleanup UX intact while moving the persona-shape logic onto persona-kit.
  • Replaces useRunnableSelection (from harness-kit) inside runPersonaImprover with a direct composition: resolvePersonaInputs + buildNonInteractiveSpec + spawn, with stderr capture, timeout enforcement, and config-file restore. Behavior is preserved (same task body, same input env-bindings, same failure-on-non-zero-exit semantics).
  • Adds buildNonInteractiveSpec (and NonInteractiveSpec) to persona-kit next to buildInteractiveSpec. The harness-specific one-shot argv translation that previously lived in harness-kit/runner.ts now has a canonical home in persona-kit so the CLI can build non-interactive spawns without depending on harness-kit.

What this PR does not do

The issue describes a deeper rewrite that swaps the entire spawn flow over to buildPersonaSpawnPlan + executePersonaSpawnPlan. The CLI's mount-branch UX (setup spinner, three-phase SIGINT handler, autosync abort/syncback with partial-sync messaging, configureGitForMount, sidecar extend mode that reads from the real cwd while writing into the mount cwd) is not covered by persona-kit's current executor. Migrating that would require either expanding applyPersonaMount / executePersonaSpawnPlan with a progress-and-lifecycle API, or losing CLI UX — both of which felt out of scope for "make the CLI a consumer of persona-kit's surface" and risky to land in one shot.

Instead, this PR achieves the issue's explicit no-imports-from-harness-kit goal and keeps the door open for a follow-up that swaps the orchestration onto executePersonaSpawnPlan once the executor grows the hooks the CLI needs. The next sibling issue (5/8) is the natural place for that.

Test plan

  • pnpm -r build — clean across all packages.
  • pnpm --filter @agentworkforce/cli test — 147/147 pass.
  • pnpm --filter @agentworkforce/persona-kit test — 82/82 pass.
  • pnpm -r test — all workspace tests pass.
  • grep '@agentworkforce/harness-kit' packages/cli/ → zero hits.

https://claude.ai/code/session_01SV82aorUkAHjh3hS34kKzY


Generated by Claude Code

Drops `@agentworkforce/harness-kit` from the CLI's dependency surface and
rewires every persona-shape import (constants, types, parsers, sidecar
resolver, env-ref/MCP/inputs helpers, harness detection,
buildInteractiveSpec) to come from `@agentworkforce/persona-kit`. The
CLI now imports only `personaCatalog`, `listBuiltInPersonas`, and
`routingProfiles` from `@agentworkforce/workload-router`.

- packages/cli/src/cli.ts: imports rewired to persona-kit. The
  `useSelection().install` shape used by the spawn flow is now built
  inline via persona-kit's pure helpers (`materializeSkills` +
  `buildInstallArtifacts` + `buildCleanupArtifacts`) so the CLI keeps
  its existing spinner-driven install/cleanup orchestration.
  `runPersonaImprover` no longer wraps `useRunnableSelection`; instead
  it composes `resolvePersonaInputs` + `buildNonInteractiveSpec` from
  persona-kit and spawns the improver subprocess directly with stderr
  capture and timeout enforcement.
- packages/cli/src/local-personas.ts: persona-shape symbols imported
  from persona-kit; only `personaCatalog`/`listBuiltInPersonas` remain
  from workload-router.
- packages/cli/package.json: drops the `@agentworkforce/harness-kit`
  workspace dependency.
- packages/persona-kit: adds `buildNonInteractiveSpec` (and its
  `NonInteractiveSpec` type) next to `buildInteractiveSpec`. The
  harness-specific one-shot argv translation that previously lived in
  `harness-kit/runner.ts` now has a canonical home in persona-kit so
  the CLI can build non-interactive spawns without depending on
  harness-kit.

Per issue #67, harness-kit stays in the monorepo (gets removed in
6/8); this PR just removes the CLI's dependency on it.

https://claude.ai/code/session_01SV82aorUkAHjh3hS34kKzY
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b6d41c1d-3ea1-4755-a5c6-1d80c5a51e44

📥 Commits

Reviewing files that changed from the base of the PR and between 06dc6bb and 6408e62.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI removes direct harness-kit selection/runnable usage and adopts persona-kit builders. Persona-kit adds NonInteractiveSpec and buildNonInteractiveSpec. CLI now builds install context via persona-kit helpers and spawns non-interactive harness runs with config materialization and timeout-aware stderr capture.

Changes

Harness-Kit Migration and Non-Interactive Spec Support

Layer / File(s) Summary
Data Contracts
packages/persona-kit/src/interactive-spec.ts
NonInteractiveSpec interface defines bin, args, configFiles, and warnings for one-shot harness commands.
Spec Builder
packages/persona-kit/src/interactive-spec.ts
buildNonInteractiveSpec composes buildInteractiveSpec(...) and adds harness-specific one-shot CLI args for claude, codex, and opencode, returning a NonInteractiveSpec.
Public Exports
packages/persona-kit/src/index.ts
Exports buildNonInteractiveSpec and NonInteractiveSpec type.
Import Reorganization
packages/cli/src/cli.ts, packages/cli/src/local-personas.ts
CLI imports spec/install builders and helpers from @agentworkforce/persona-kit; listBuiltInPersonas and personaCatalog now come from @agentworkforce/workload-router.
Install Context & Persona Improver
packages/cli/src/cli.ts
Adds buildInstallContext using persona-kit builders; runPersonaImprover builds non-interactive spec, writes spec.configFiles into cwd, spawns harness directly with stderr capture and optional timeout, then cleans up. runInteractive now uses buildInstallContext.
Dependency Cleanup
packages/cli/package.json
Removed @agentworkforce/harness-kit dependency.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • AgentWorkforce/workforce#73: Both PRs move/replace harness-kit/workload-router persona helpers with @agentworkforce/persona-kit exports and update packages/cli to use persona-kit instead of harness-kit.
  • AgentWorkforce/workforce#72: Extends persona-kit functionality and exports that this PR adds (buildNonInteractiveSpec).
  • AgentWorkforce/workforce#61: Touches the same CLI persona-improver flow and relates to the rewritten runPersonaImprover logic.

Poem

🐰 The CLI hops free from harness chains,
Builders stitch the paths with careful reins,
One-shot specs whisper the exact command,
Spawning runs and tidying files by hand,
Persona-kit blooms — a nimble, tidy land.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and concisely summarizes the main change: migrating the CLI to consume persona-kit instead of harness-kit, which aligns with the primary objective of the changeset.
Description check ✅ Passed The description provides comprehensive context about the refactoring work, including specific changes, motivations, test results, and scope boundaries that directly relate to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/resolve-github-issue-n0PFo

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ee1f8f853

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/cli.ts Outdated
Comment on lines +3244 to +3247
timeoutMs !== undefined
? setTimeout(() => {
child.kill('SIGTERM');
}, timeoutMs)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Force-kill improver after timeout

The new timeout path only sends SIGTERM and then waits for 'close', so runPersonaImprover can hang forever if the harness process traps or ignores SIGTERM (spinner never finishes, proposals flow is blocked). This is a regression from the previous useRunnableSelection path, which used spawnCapture with a SIGKILL fallback after a grace period (packages/harness-kit/src/runner.ts). Add a post-timeout force-kill (or equivalent bounded wait) so timeout enforcement is guaranteed.

Useful? React with 👍 / 👎.

If the improver harness traps or ignores SIGTERM, the timeout path
would hang forever — the spinner never finishes and the post-session
proposals flow is blocked. Mirror the prior spawnCapture behavior in
harness-kit by following the SIGTERM with a 1s grace window before
sending SIGKILL.

https://claude.ai/code/session_01SV82aorUkAHjh3hS34kKzY
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/cli/src/cli.ts Outdated
Comment on lines +3243 to +3248
});
// SIGTERM first; if the harness traps or ignores it, escalate to
// SIGKILL after a 1s grace so the timeout is actually enforced
// (matches the previous spawnCapture behavior in harness-kit).
const timeout =
timeoutMs !== undefined
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Missing SIGKILL escalation after timeout SIGTERM can hang the CLI indefinitely

The refactored runPersonaImprover subprocess spawn sends only SIGTERM on timeout, but never escalates to SIGKILL if the child process ignores SIGTERM. The old spawnCapture in packages/harness-kit/src/runner.ts:377-386 sent SIGTERM followed by a SIGKILL after a 1-second grace period (FORCE_KILL_GRACE_MS). Without this escalation, if the harness binary (e.g. claude --print) becomes stuck (unresponsive API call, kernel-level I/O wait) and doesn't exit on SIGTERM, the await new Promise(...) never resolves, hanging the CLI indefinitely. The old code had an explicit test for this scenario (useRunnableSelection force-kills a child that ignores timeout SIGTERM).

Suggested change
});
// SIGTERM first; if the harness traps or ignores it, escalate to
// SIGKILL after a 1s grace so the timeout is actually enforced
// (matches the previous spawnCapture behavior in harness-kit).
const timeout =
timeoutMs !== undefined
const timeout =
timeoutMs !== undefined
? setTimeout(() => {
child.kill('SIGTERM');
setTimeout(() => {
if (!settled) child.kill('SIGKILL');
}, 1_000);
}, timeoutMs)
: undefined;
let settled = false;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread packages/cli/src/cli.ts Outdated
Comment on lines +3229 to +3265
const captureResult = await new Promise<{ exitCode: number | null; stderr: string }>(
(resolveResult) => {
const child = spawn(spec.bin, [...spec.args], {
cwd,
env: childEnv,
stdio: ['ignore', 'pipe', 'pipe'],
shell: false
});
let stderrBuf = '';
let forceKillTimeout: NodeJS.Timeout | undefined;
child.stdout?.setEncoding('utf8');
child.stderr?.setEncoding('utf8');
child.stderr?.on('data', (chunk: string) => {
stderrBuf += chunk;
});
// SIGTERM first; if the harness traps or ignores it, escalate to
// SIGKILL after a 1s grace so the timeout is actually enforced
// (matches the previous spawnCapture behavior in harness-kit).
const timeout =
timeoutMs !== undefined
? setTimeout(() => {
child.kill('SIGTERM');
forceKillTimeout = setTimeout(() => {
if (!child.killed) child.kill('SIGKILL');
}, 1000);
}, timeoutMs)
: undefined;
const clearTimers = () => {
if (timeout) clearTimeout(timeout);
if (forceKillTimeout) clearTimeout(forceKillTimeout);
};
child.on('error', (err) => {
clearTimers();
resolveResult({ exitCode: 1, stderr: `${stderrBuf}${err.message}\n` });
});
child.on('close', (code, signal) => {
clearTimers();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Config files written to user's cwd not restored if spawn promise rejects

The restoreConfigWrites() call at packages/cli/src/cli.ts:3261 is outside any try/finally block. The old code in packages/harness-kit/src/runner.ts:228-229 used try/finally to guarantee config file restoration. If spawn() throws synchronously inside the promise constructor (e.g., due to invalid env values), the promise rejects, the await throws, and restoreConfigWrites() is never reached — leaving orphaned config files (e.g. opencode.json) in the user's working directory. While synchronous spawn throws are rare with these hardcoded options, the defensive pattern from the original code should be preserved.

Prompt for agents
In runPersonaImprover (packages/cli/src/cli.ts around line 3229), the config file restoration (restoreConfigWrites) is called after the await but not inside a try/finally. The original code in packages/harness-kit/src/runner.ts:193-229 used try/finally to guarantee restoration. Wrap the spawn+await+restore in a try/finally so that restoreConfigWrites() always runs, even if the promise unexpectedly rejects. The exit code check and error throw (currently at lines 3262-3265) should remain outside the finally block but still after restoreConfigWrites.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

If `spawn()` throws synchronously inside the promise constructor (or
the await rejects for any unexpected reason), the prior layout would
skip `restoreConfigWrites()` and leave the persona-supplied
`opencode.json` (or future configFile producers) sitting in the user's
real working directory. Wrap the spawn-and-await in try/finally so
restoration is guaranteed before the exit-code check rethrows.

https://claude.ai/code/session_01SV82aorUkAHjh3hS34kKzY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[persona-kit 4/8] Migrate workforce CLI to use @agentworkforce/persona-kit

2 participants