feat(cli): deploy a persona by id, not just by path - #320

Draft
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id
Draft

feat(cli): deploy a persona by id, not just by path#320
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id

Conversation

@willwashburn

@willwashburnwillwashburn commented Aug 20, 2026

Copy link
Copy Markdown
Member

agentworkforce deploy <persona-id> resolves through the registry cascade, so an agent kept in .agentworkforce/workforce/agents/<name>/ deploys by name from anywhere in the repo. Completes the loop opened by #316, which made those agents discoverable to list/show/agent but left deploy path-only.

$ agentworkforce deploy proposal-agent --dry-run # in ../sales
workforce deploy → .../agents/proposal-agent/persona.json
persona proposal-agent: 1 integration(s), 1 schedule(s)
ok: proposal-agent (dry-run)

Which selectors are paths

Syntax decides, never the filesystem. A path separator, a leading .///~, or a persona-source extension means a path; anything else is an id. Probing disk instead would let a bare proposal-agent mean the persona in one directory and a same-named folder in another — the same command doing different things depending on where it ran.

Handler resolution is unaffected: onEvent resolves against the persona file's directory, and a compiled persona.json sits in the same agent directory as agent.ts, so an id-resolved deploy bundles exactly what a path-resolved one does.

Errors

A built-in id resolves but has no file to deploy, and says so:

deploy: persona "persona-maker" resolves to the built-in catalog, which has no
file to deploy. Pass a path to a persona.json or persona.ts instead.

An unknown id lists what is available, from the registry:

deploy: Unknown persona "no-such-agent". Known personas: customer-dev,
persona-improver, persona-maker, proposal-agent

Verification

Exercised against the real ../sales checkout: bare id, explicit persona.ts path (unchanged), unknown id, and built-in id. deploy-command and local-personas suites pass locally (74). cli.test.ts spawns subprocesses and is being OOM-killed on this machine, so it is left to CI.

Semver: minor — new selector form, no change to existing path behavior.

🤖 Generated with Claude Code

Review in cubic

`agentworkforce deploy proposal-agent` resolves through the registry cascade,
so an agent kept in `.agentworkforce/workforce/agents/<name>/` deploys by name
from anywhere in the repo instead of by spelling out its path.
Syntax decides which a selector is: path separators, a leading `.`/`/`/`~`, or
a persona-source extension mean a path; anything else is an id. The filesystem
does not get a vote, so a bare `proposal-agent` cannot mean the persona in one
directory and a same-named folder in another.
A built-in id resolves but has no file to deploy, and says so rather than
failing on a path that was never going to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d670f133-5b4c-4895-b1ee-83ca054b8018

📥 Commits

Reviewing files that changed from the base of the PR and between 558784d and 01f746a.

📒 Files selected for processing (3)
  • packages/cli/src/cli-impl.ts
  • packages/cli/src/deploy-command.test.ts
  • packages/cli/src/deploy-command.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:01f746a99a

ℹ️ 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".


let resolved;
try {
resolved = resolvePersonaReference(selector);

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 Avoid interactive validation for deploy selectors

When a valid handler-based deploy persona omits harness, model, or systemPrompt, this call fails before returning its file path because resolvePersonaReference unconditionally builds an interactive PersonaSelection, whose validator rejects those omissions. Deploy's persona parser explicitly permits these fields to be absent when onEvent is present, so the same persona deploys by path but cannot deploy by the newly supported bare ID; use a registry lookup that does not require the interactive projection.

Useful? React with 👍 / 👎.

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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 Deploy the resolved cascade rather than the overlay file

When the selected ID is defined by a partial registry overlay, returning its declaring path discards the merged resolved.spec that made the persona valid. Deploy subsequently rereads the raw file in compileAgentSource; for example, the new test fixture containing only id and extends fails with missing top-level "intent", and other inherited deploy fields are similarly lost. The ID path needs to deploy a materialized merged spec, while preserving the declaring directory for relative handler and asset paths.

Useful? React with 👍 / 👎.

@cubic-dev-aicubic-dev-aiBot left a comment

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.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/deploy-command.ts">
<violation number="1" location="packages/cli/src/deploy-command.ts:342">
P1: Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</violation>
<violation number="2" location="packages/cli/src/deploy-command.ts:355">
P1: When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</violation>
</file>
<file name="packages/cli/src/deploy-command.test.ts">
<violation number="1" location="packages/cli/src/deploy-command.test.ts:548">
P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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.

P1: When a higher-priority partial overlay shadows an agent persona, resolved.path points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited cloud/onEvent, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 355:
<comment>When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+ 'Pass a path to a persona.json or persona.ts instead.'
+ );
+ }
+ return resolved.path;
+}
+
</file context>


let resolved;
try {
resolved = resolvePersonaReference(selector);

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.

P1: Resolve ID selectors without constructing an interactive PersonaSelection; otherwise handler personas that omit harness, model, or systemPrompt fail before deploy can obtain their path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 342:
<comment>Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+
+ let resolved;
+ try {
+ resolved = resolvePersonaReference(selector);
+ } catch (err) {
+ if (err instanceof PersonaResolutionError) {
</file context>

const trap = trapExit();
try {
assert.throws(
() => resolveDeployPersonaSelector('persona-maker'),

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.

P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. resolveDeployPersonaSelector('persona-maker') resolves through the registry cascade, where a local persona named persona-maker under the runner's cwd (process.cwd()) or in the configurable persona dirs (default ~/.agentworkforce/workforce/personas) wins over the built-in catalog and returns a real file path, so the /no file to deploy/ assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.test.ts, line 548:
<comment>The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</comment>
<file context>
@@ -480,3 +482,74 @@ test('runLogin canonicalizes origin.agentrelay.cloud apiUrl before resolving the
+ const trap = trapExit();
+ try {
+ assert.throws(
+ () => resolveDeployPersonaSelector('persona-maker'),
+ /__exit_trap__/
+ );
</file context>

@willwashburn
willwashburn marked this pull request as draft August 20, 2026 16:40
@willwashburn

Copy link
Copy Markdown
MemberAuthor

Both P1s are real. Reproduced each before changing anything, and I've converted this to draft.

Interactive projection blocks handler personas. Built a handler persona from examples/weekly-digest/persona.json with harness/model/systemPrompt removed, in .agentworkforce/workforce/agents/digest/:

$ agentworkforce deploy .../agents/digest/persona.json --dry-run
persona digest: 1 integration(s), 1 schedule(s)
ok: digest (dry-run)
$ agentworkforce deploy digest --dry-run
warning: [cwd:agents] digest: standalone persona "digest".harness is required for standalone personas
deploy: Unknown persona "digest". Known personas: persona-improver, persona-maker

Same file, two selectors, opposite outcomes. The rejection is deeper than the PersonaSelection projection — it happens during registry resolution, so the persona never enters byId at all. That also means #316's cwd:agents layer cannot load exactly the class of agent it was built for unless that agent happens to declare interactive fields it does not use, and the warning it emits reads as "your persona is malformed" when the persona is a valid deployable one.

Overlay shadowing loses the agent file. A partial overlay at personas/digest.json containing only {"id","env"}:

$ agentworkforce list
digest cwd opencode opencode/gpt-5-nano # merges correctly
$ agentworkforce deploy digest --dry-run
workforce deploy → .../personas/digest.json
agentworkforce deploy failed: persona at .../personas/digest.json is missing top-level "intent"

resolved.path is the declaring file of the winning layer, which for an overlay is not a deployable persona. Deploy rereads it raw and loses onEvent, cloud, and intent.

The fix is the one you both point at — hand deploy the materialized merged spec plus the directory that owns the handler, rather than a path — and it needs the registry to be able to hold handler personas in the first place. That is a larger change than this PR, and it touches ground #316 already shipped, so I would rather land it deliberately than patch it here.

The P3 on test isolation is also correct; the built-in-id assertion reads ambient cwd and personal config.

Sign up for freeto 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.

1 participant

@willwashburn
, '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

feat(cli): deploy a persona by id, not just by path - #320

Draft
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id
Draft

feat(cli): deploy a persona by id, not just by path#320
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id

Conversation

@willwashburn

@willwashburnwillwashburn commented Aug 20, 2026

Copy link
Copy Markdown
Member

agentworkforce deploy <persona-id> resolves through the registry cascade, so an agent kept in .agentworkforce/workforce/agents/<name>/ deploys by name from anywhere in the repo. Completes the loop opened by #316, which made those agents discoverable to list/show/agent but left deploy path-only.

$ agentworkforce deploy proposal-agent --dry-run # in ../sales
workforce deploy → .../agents/proposal-agent/persona.json
persona proposal-agent: 1 integration(s), 1 schedule(s)
ok: proposal-agent (dry-run)

Which selectors are paths

Syntax decides, never the filesystem. A path separator, a leading .///~, or a persona-source extension means a path; anything else is an id. Probing disk instead would let a bare proposal-agent mean the persona in one directory and a same-named folder in another — the same command doing different things depending on where it ran.

Handler resolution is unaffected: onEvent resolves against the persona file's directory, and a compiled persona.json sits in the same agent directory as agent.ts, so an id-resolved deploy bundles exactly what a path-resolved one does.

Errors

A built-in id resolves but has no file to deploy, and says so:

deploy: persona "persona-maker" resolves to the built-in catalog, which has no
file to deploy. Pass a path to a persona.json or persona.ts instead.

An unknown id lists what is available, from the registry:

deploy: Unknown persona "no-such-agent". Known personas: customer-dev,
persona-improver, persona-maker, proposal-agent

Verification

Exercised against the real ../sales checkout: bare id, explicit persona.ts path (unchanged), unknown id, and built-in id. deploy-command and local-personas suites pass locally (74). cli.test.ts spawns subprocesses and is being OOM-killed on this machine, so it is left to CI.

Semver: minor — new selector form, no change to existing path behavior.

🤖 Generated with Claude Code

Review in cubic

`agentworkforce deploy proposal-agent` resolves through the registry cascade,
so an agent kept in `.agentworkforce/workforce/agents/<name>/` deploys by name
from anywhere in the repo instead of by spelling out its path.
Syntax decides which a selector is: path separators, a leading `.`/`/`/`~`, or
a persona-source extension mean a path; anything else is an id. The filesystem
does not get a vote, so a bare `proposal-agent` cannot mean the persona in one
directory and a same-named folder in another.
A built-in id resolves but has no file to deploy, and says so rather than
failing on a path that was never going to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d670f133-5b4c-4895-b1ee-83ca054b8018

📥 Commits

Reviewing files that changed from the base of the PR and between 558784d and 01f746a.

📒 Files selected for processing (3)
  • packages/cli/src/cli-impl.ts
  • packages/cli/src/deploy-command.test.ts
  • packages/cli/src/deploy-command.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:01f746a99a

ℹ️ 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".


let resolved;
try {
resolved = resolvePersonaReference(selector);

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 Avoid interactive validation for deploy selectors

When a valid handler-based deploy persona omits harness, model, or systemPrompt, this call fails before returning its file path because resolvePersonaReference unconditionally builds an interactive PersonaSelection, whose validator rejects those omissions. Deploy's persona parser explicitly permits these fields to be absent when onEvent is present, so the same persona deploys by path but cannot deploy by the newly supported bare ID; use a registry lookup that does not require the interactive projection.

Useful? React with 👍 / 👎.

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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 Deploy the resolved cascade rather than the overlay file

When the selected ID is defined by a partial registry overlay, returning its declaring path discards the merged resolved.spec that made the persona valid. Deploy subsequently rereads the raw file in compileAgentSource; for example, the new test fixture containing only id and extends fails with missing top-level "intent", and other inherited deploy fields are similarly lost. The ID path needs to deploy a materialized merged spec, while preserving the declaring directory for relative handler and asset paths.

Useful? React with 👍 / 👎.

@cubic-dev-aicubic-dev-aiBot left a comment

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.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/deploy-command.ts">
<violation number="1" location="packages/cli/src/deploy-command.ts:342">
P1: Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</violation>
<violation number="2" location="packages/cli/src/deploy-command.ts:355">
P1: When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</violation>
</file>
<file name="packages/cli/src/deploy-command.test.ts">
<violation number="1" location="packages/cli/src/deploy-command.test.ts:548">
P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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.

P1: When a higher-priority partial overlay shadows an agent persona, resolved.path points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited cloud/onEvent, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 355:
<comment>When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+ 'Pass a path to a persona.json or persona.ts instead.'
+ );
+ }
+ return resolved.path;
+}
+
</file context>


let resolved;
try {
resolved = resolvePersonaReference(selector);

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.

P1: Resolve ID selectors without constructing an interactive PersonaSelection; otherwise handler personas that omit harness, model, or systemPrompt fail before deploy can obtain their path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 342:
<comment>Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+
+ let resolved;
+ try {
+ resolved = resolvePersonaReference(selector);
+ } catch (err) {
+ if (err instanceof PersonaResolutionError) {
</file context>

const trap = trapExit();
try {
assert.throws(
() => resolveDeployPersonaSelector('persona-maker'),

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.

P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. resolveDeployPersonaSelector('persona-maker') resolves through the registry cascade, where a local persona named persona-maker under the runner's cwd (process.cwd()) or in the configurable persona dirs (default ~/.agentworkforce/workforce/personas) wins over the built-in catalog and returns a real file path, so the /no file to deploy/ assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.test.ts, line 548:
<comment>The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</comment>
<file context>
@@ -480,3 +482,74 @@ test('runLogin canonicalizes origin.agentrelay.cloud apiUrl before resolving the
+ const trap = trapExit();
+ try {
+ assert.throws(
+ () => resolveDeployPersonaSelector('persona-maker'),
+ /__exit_trap__/
+ );
</file context>

@willwashburn
willwashburn marked this pull request as draft August 20, 2026 16:40
@willwashburn

Copy link
Copy Markdown
MemberAuthor

Both P1s are real. Reproduced each before changing anything, and I've converted this to draft.

Interactive projection blocks handler personas. Built a handler persona from examples/weekly-digest/persona.json with harness/model/systemPrompt removed, in .agentworkforce/workforce/agents/digest/:

$ agentworkforce deploy .../agents/digest/persona.json --dry-run
persona digest: 1 integration(s), 1 schedule(s)
ok: digest (dry-run)
$ agentworkforce deploy digest --dry-run
warning: [cwd:agents] digest: standalone persona "digest".harness is required for standalone personas
deploy: Unknown persona "digest". Known personas: persona-improver, persona-maker

Same file, two selectors, opposite outcomes. The rejection is deeper than the PersonaSelection projection — it happens during registry resolution, so the persona never enters byId at all. That also means #316's cwd:agents layer cannot load exactly the class of agent it was built for unless that agent happens to declare interactive fields it does not use, and the warning it emits reads as "your persona is malformed" when the persona is a valid deployable one.

Overlay shadowing loses the agent file. A partial overlay at personas/digest.json containing only {"id","env"}:

$ agentworkforce list
digest cwd opencode opencode/gpt-5-nano # merges correctly
$ agentworkforce deploy digest --dry-run
workforce deploy → .../personas/digest.json
agentworkforce deploy failed: persona at .../personas/digest.json is missing top-level "intent"

resolved.path is the declaring file of the winning layer, which for an overlay is not a deployable persona. Deploy rereads it raw and loses onEvent, cloud, and intent.

The fix is the one you both point at — hand deploy the materialized merged spec plus the directory that owns the handler, rather than a path — and it needs the registry to be able to hold handler personas in the first place. That is a larger change than this PR, and it touches ground #316 already shipped, so I would rather land it deliberately than patch it here.

The P3 on test isolation is also correct; the built-in-id assertion reads ambient cwd and personal config.

Sign up for freeto 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.

1 participant

@willwashburn
, '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

feat(cli): deploy a persona by id, not just by path - #320

Draft
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id
Draft

feat(cli): deploy a persona by id, not just by path#320
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id

Conversation

@willwashburn

@willwashburnwillwashburn commented Aug 20, 2026

Copy link
Copy Markdown
Member

agentworkforce deploy <persona-id> resolves through the registry cascade, so an agent kept in .agentworkforce/workforce/agents/<name>/ deploys by name from anywhere in the repo. Completes the loop opened by #316, which made those agents discoverable to list/show/agent but left deploy path-only.

$ agentworkforce deploy proposal-agent --dry-run # in ../sales
workforce deploy → .../agents/proposal-agent/persona.json
persona proposal-agent: 1 integration(s), 1 schedule(s)
ok: proposal-agent (dry-run)

Which selectors are paths

Syntax decides, never the filesystem. A path separator, a leading .///~, or a persona-source extension means a path; anything else is an id. Probing disk instead would let a bare proposal-agent mean the persona in one directory and a same-named folder in another — the same command doing different things depending on where it ran.

Handler resolution is unaffected: onEvent resolves against the persona file's directory, and a compiled persona.json sits in the same agent directory as agent.ts, so an id-resolved deploy bundles exactly what a path-resolved one does.

Errors

A built-in id resolves but has no file to deploy, and says so:

deploy: persona "persona-maker" resolves to the built-in catalog, which has no
file to deploy. Pass a path to a persona.json or persona.ts instead.

An unknown id lists what is available, from the registry:

deploy: Unknown persona "no-such-agent". Known personas: customer-dev,
persona-improver, persona-maker, proposal-agent

Verification

Exercised against the real ../sales checkout: bare id, explicit persona.ts path (unchanged), unknown id, and built-in id. deploy-command and local-personas suites pass locally (74). cli.test.ts spawns subprocesses and is being OOM-killed on this machine, so it is left to CI.

Semver: minor — new selector form, no change to existing path behavior.

🤖 Generated with Claude Code

Review in cubic

`agentworkforce deploy proposal-agent` resolves through the registry cascade,
so an agent kept in `.agentworkforce/workforce/agents/<name>/` deploys by name
from anywhere in the repo instead of by spelling out its path.
Syntax decides which a selector is: path separators, a leading `.`/`/`/`~`, or
a persona-source extension mean a path; anything else is an id. The filesystem
does not get a vote, so a bare `proposal-agent` cannot mean the persona in one
directory and a same-named folder in another.
A built-in id resolves but has no file to deploy, and says so rather than
failing on a path that was never going to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d670f133-5b4c-4895-b1ee-83ca054b8018

📥 Commits

Reviewing files that changed from the base of the PR and between 558784d and 01f746a.

📒 Files selected for processing (3)
  • packages/cli/src/cli-impl.ts
  • packages/cli/src/deploy-command.test.ts
  • packages/cli/src/deploy-command.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:01f746a99a

ℹ️ 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".


let resolved;
try {
resolved = resolvePersonaReference(selector);

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 Avoid interactive validation for deploy selectors

When a valid handler-based deploy persona omits harness, model, or systemPrompt, this call fails before returning its file path because resolvePersonaReference unconditionally builds an interactive PersonaSelection, whose validator rejects those omissions. Deploy's persona parser explicitly permits these fields to be absent when onEvent is present, so the same persona deploys by path but cannot deploy by the newly supported bare ID; use a registry lookup that does not require the interactive projection.

Useful? React with 👍 / 👎.

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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 Deploy the resolved cascade rather than the overlay file

When the selected ID is defined by a partial registry overlay, returning its declaring path discards the merged resolved.spec that made the persona valid. Deploy subsequently rereads the raw file in compileAgentSource; for example, the new test fixture containing only id and extends fails with missing top-level "intent", and other inherited deploy fields are similarly lost. The ID path needs to deploy a materialized merged spec, while preserving the declaring directory for relative handler and asset paths.

Useful? React with 👍 / 👎.

@cubic-dev-aicubic-dev-aiBot left a comment

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.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/deploy-command.ts">
<violation number="1" location="packages/cli/src/deploy-command.ts:342">
P1: Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</violation>
<violation number="2" location="packages/cli/src/deploy-command.ts:355">
P1: When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</violation>
</file>
<file name="packages/cli/src/deploy-command.test.ts">
<violation number="1" location="packages/cli/src/deploy-command.test.ts:548">
P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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.

P1: When a higher-priority partial overlay shadows an agent persona, resolved.path points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited cloud/onEvent, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 355:
<comment>When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+ 'Pass a path to a persona.json or persona.ts instead.'
+ );
+ }
+ return resolved.path;
+}
+
</file context>


let resolved;
try {
resolved = resolvePersonaReference(selector);

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.

P1: Resolve ID selectors without constructing an interactive PersonaSelection; otherwise handler personas that omit harness, model, or systemPrompt fail before deploy can obtain their path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 342:
<comment>Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+
+ let resolved;
+ try {
+ resolved = resolvePersonaReference(selector);
+ } catch (err) {
+ if (err instanceof PersonaResolutionError) {
</file context>

const trap = trapExit();
try {
assert.throws(
() => resolveDeployPersonaSelector('persona-maker'),

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.

P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. resolveDeployPersonaSelector('persona-maker') resolves through the registry cascade, where a local persona named persona-maker under the runner's cwd (process.cwd()) or in the configurable persona dirs (default ~/.agentworkforce/workforce/personas) wins over the built-in catalog and returns a real file path, so the /no file to deploy/ assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.test.ts, line 548:
<comment>The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</comment>
<file context>
@@ -480,3 +482,74 @@ test('runLogin canonicalizes origin.agentrelay.cloud apiUrl before resolving the
+ const trap = trapExit();
+ try {
+ assert.throws(
+ () => resolveDeployPersonaSelector('persona-maker'),
+ /__exit_trap__/
+ );
</file context>

@willwashburn
willwashburn marked this pull request as draft August 20, 2026 16:40
@willwashburn

Copy link
Copy Markdown
MemberAuthor

Both P1s are real. Reproduced each before changing anything, and I've converted this to draft.

Interactive projection blocks handler personas. Built a handler persona from examples/weekly-digest/persona.json with harness/model/systemPrompt removed, in .agentworkforce/workforce/agents/digest/:

$ agentworkforce deploy .../agents/digest/persona.json --dry-run
persona digest: 1 integration(s), 1 schedule(s)
ok: digest (dry-run)
$ agentworkforce deploy digest --dry-run
warning: [cwd:agents] digest: standalone persona "digest".harness is required for standalone personas
deploy: Unknown persona "digest". Known personas: persona-improver, persona-maker

Same file, two selectors, opposite outcomes. The rejection is deeper than the PersonaSelection projection — it happens during registry resolution, so the persona never enters byId at all. That also means #316's cwd:agents layer cannot load exactly the class of agent it was built for unless that agent happens to declare interactive fields it does not use, and the warning it emits reads as "your persona is malformed" when the persona is a valid deployable one.

Overlay shadowing loses the agent file. A partial overlay at personas/digest.json containing only {"id","env"}:

$ agentworkforce list
digest cwd opencode opencode/gpt-5-nano # merges correctly
$ agentworkforce deploy digest --dry-run
workforce deploy → .../personas/digest.json
agentworkforce deploy failed: persona at .../personas/digest.json is missing top-level "intent"

resolved.path is the declaring file of the winning layer, which for an overlay is not a deployable persona. Deploy rereads it raw and loses onEvent, cloud, and intent.

The fix is the one you both point at — hand deploy the materialized merged spec plus the directory that owns the handler, rather than a path — and it needs the registry to be able to hold handler personas in the first place. That is a larger change than this PR, and it touches ground #316 already shipped, so I would rather land it deliberately than patch it here.

The P3 on test isolation is also correct; the built-in-id assertion reads ambient cwd and personal config.

Sign up for freeto 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.

1 participant

@willwashburn
, '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

feat(cli): deploy a persona by id, not just by path - #320

Draft
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id
Draft

feat(cli): deploy a persona by id, not just by path#320
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id

Conversation

@willwashburn

@willwashburnwillwashburn commented Aug 20, 2026

Copy link
Copy Markdown
Member

agentworkforce deploy <persona-id> resolves through the registry cascade, so an agent kept in .agentworkforce/workforce/agents/<name>/ deploys by name from anywhere in the repo. Completes the loop opened by #316, which made those agents discoverable to list/show/agent but left deploy path-only.

$ agentworkforce deploy proposal-agent --dry-run # in ../sales
workforce deploy → .../agents/proposal-agent/persona.json
persona proposal-agent: 1 integration(s), 1 schedule(s)
ok: proposal-agent (dry-run)

Which selectors are paths

Syntax decides, never the filesystem. A path separator, a leading .///~, or a persona-source extension means a path; anything else is an id. Probing disk instead would let a bare proposal-agent mean the persona in one directory and a same-named folder in another — the same command doing different things depending on where it ran.

Handler resolution is unaffected: onEvent resolves against the persona file's directory, and a compiled persona.json sits in the same agent directory as agent.ts, so an id-resolved deploy bundles exactly what a path-resolved one does.

Errors

A built-in id resolves but has no file to deploy, and says so:

deploy: persona "persona-maker" resolves to the built-in catalog, which has no
file to deploy. Pass a path to a persona.json or persona.ts instead.

An unknown id lists what is available, from the registry:

deploy: Unknown persona "no-such-agent". Known personas: customer-dev,
persona-improver, persona-maker, proposal-agent

Verification

Exercised against the real ../sales checkout: bare id, explicit persona.ts path (unchanged), unknown id, and built-in id. deploy-command and local-personas suites pass locally (74). cli.test.ts spawns subprocesses and is being OOM-killed on this machine, so it is left to CI.

Semver: minor — new selector form, no change to existing path behavior.

🤖 Generated with Claude Code

Review in cubic

`agentworkforce deploy proposal-agent` resolves through the registry cascade,
so an agent kept in `.agentworkforce/workforce/agents/<name>/` deploys by name
from anywhere in the repo instead of by spelling out its path.
Syntax decides which a selector is: path separators, a leading `.`/`/`/`~`, or
a persona-source extension mean a path; anything else is an id. The filesystem
does not get a vote, so a bare `proposal-agent` cannot mean the persona in one
directory and a same-named folder in another.
A built-in id resolves but has no file to deploy, and says so rather than
failing on a path that was never going to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d670f133-5b4c-4895-b1ee-83ca054b8018

📥 Commits

Reviewing files that changed from the base of the PR and between 558784d and 01f746a.

📒 Files selected for processing (3)
  • packages/cli/src/cli-impl.ts
  • packages/cli/src/deploy-command.test.ts
  • packages/cli/src/deploy-command.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:01f746a99a

ℹ️ 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".


let resolved;
try {
resolved = resolvePersonaReference(selector);

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 Avoid interactive validation for deploy selectors

When a valid handler-based deploy persona omits harness, model, or systemPrompt, this call fails before returning its file path because resolvePersonaReference unconditionally builds an interactive PersonaSelection, whose validator rejects those omissions. Deploy's persona parser explicitly permits these fields to be absent when onEvent is present, so the same persona deploys by path but cannot deploy by the newly supported bare ID; use a registry lookup that does not require the interactive projection.

Useful? React with 👍 / 👎.

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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 Deploy the resolved cascade rather than the overlay file

When the selected ID is defined by a partial registry overlay, returning its declaring path discards the merged resolved.spec that made the persona valid. Deploy subsequently rereads the raw file in compileAgentSource; for example, the new test fixture containing only id and extends fails with missing top-level "intent", and other inherited deploy fields are similarly lost. The ID path needs to deploy a materialized merged spec, while preserving the declaring directory for relative handler and asset paths.

Useful? React with 👍 / 👎.

@cubic-dev-aicubic-dev-aiBot left a comment

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.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/deploy-command.ts">
<violation number="1" location="packages/cli/src/deploy-command.ts:342">
P1: Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</violation>
<violation number="2" location="packages/cli/src/deploy-command.ts:355">
P1: When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</violation>
</file>
<file name="packages/cli/src/deploy-command.test.ts">
<violation number="1" location="packages/cli/src/deploy-command.test.ts:548">
P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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.

P1: When a higher-priority partial overlay shadows an agent persona, resolved.path points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited cloud/onEvent, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 355:
<comment>When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+ 'Pass a path to a persona.json or persona.ts instead.'
+ );
+ }
+ return resolved.path;
+}
+
</file context>


let resolved;
try {
resolved = resolvePersonaReference(selector);

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.

P1: Resolve ID selectors without constructing an interactive PersonaSelection; otherwise handler personas that omit harness, model, or systemPrompt fail before deploy can obtain their path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 342:
<comment>Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+
+ let resolved;
+ try {
+ resolved = resolvePersonaReference(selector);
+ } catch (err) {
+ if (err instanceof PersonaResolutionError) {
</file context>

const trap = trapExit();
try {
assert.throws(
() => resolveDeployPersonaSelector('persona-maker'),

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.

P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. resolveDeployPersonaSelector('persona-maker') resolves through the registry cascade, where a local persona named persona-maker under the runner's cwd (process.cwd()) or in the configurable persona dirs (default ~/.agentworkforce/workforce/personas) wins over the built-in catalog and returns a real file path, so the /no file to deploy/ assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.test.ts, line 548:
<comment>The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</comment>
<file context>
@@ -480,3 +482,74 @@ test('runLogin canonicalizes origin.agentrelay.cloud apiUrl before resolving the
+ const trap = trapExit();
+ try {
+ assert.throws(
+ () => resolveDeployPersonaSelector('persona-maker'),
+ /__exit_trap__/
+ );
</file context>

@willwashburn
willwashburn marked this pull request as draft August 20, 2026 16:40
@willwashburn

Copy link
Copy Markdown
MemberAuthor

Both P1s are real. Reproduced each before changing anything, and I've converted this to draft.

Interactive projection blocks handler personas. Built a handler persona from examples/weekly-digest/persona.json with harness/model/systemPrompt removed, in .agentworkforce/workforce/agents/digest/:

$ agentworkforce deploy .../agents/digest/persona.json --dry-run
persona digest: 1 integration(s), 1 schedule(s)
ok: digest (dry-run)
$ agentworkforce deploy digest --dry-run
warning: [cwd:agents] digest: standalone persona "digest".harness is required for standalone personas
deploy: Unknown persona "digest". Known personas: persona-improver, persona-maker

Same file, two selectors, opposite outcomes. The rejection is deeper than the PersonaSelection projection — it happens during registry resolution, so the persona never enters byId at all. That also means #316's cwd:agents layer cannot load exactly the class of agent it was built for unless that agent happens to declare interactive fields it does not use, and the warning it emits reads as "your persona is malformed" when the persona is a valid deployable one.

Overlay shadowing loses the agent file. A partial overlay at personas/digest.json containing only {"id","env"}:

$ agentworkforce list
digest cwd opencode opencode/gpt-5-nano # merges correctly
$ agentworkforce deploy digest --dry-run
workforce deploy → .../personas/digest.json
agentworkforce deploy failed: persona at .../personas/digest.json is missing top-level "intent"

resolved.path is the declaring file of the winning layer, which for an overlay is not a deployable persona. Deploy rereads it raw and loses onEvent, cloud, and intent.

The fix is the one you both point at — hand deploy the materialized merged spec plus the directory that owns the handler, rather than a path — and it needs the registry to be able to hold handler personas in the first place. That is a larger change than this PR, and it touches ground #316 already shipped, so I would rather land it deliberately than patch it here.

The P3 on test isolation is also correct; the built-in-id assertion reads ambient cwd and personal config.

Sign up for freeto 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.

1 participant

@willwashburn
, '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

feat(cli): deploy a persona by id, not just by path - #320

Draft
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id
Draft

feat(cli): deploy a persona by id, not just by path#320
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id

Conversation

@willwashburn

@willwashburnwillwashburn commented Aug 20, 2026

Copy link
Copy Markdown
Member

agentworkforce deploy <persona-id> resolves through the registry cascade, so an agent kept in .agentworkforce/workforce/agents/<name>/ deploys by name from anywhere in the repo. Completes the loop opened by #316, which made those agents discoverable to list/show/agent but left deploy path-only.

$ agentworkforce deploy proposal-agent --dry-run # in ../sales
workforce deploy → .../agents/proposal-agent/persona.json
persona proposal-agent: 1 integration(s), 1 schedule(s)
ok: proposal-agent (dry-run)

Which selectors are paths

Syntax decides, never the filesystem. A path separator, a leading .///~, or a persona-source extension means a path; anything else is an id. Probing disk instead would let a bare proposal-agent mean the persona in one directory and a same-named folder in another — the same command doing different things depending on where it ran.

Handler resolution is unaffected: onEvent resolves against the persona file's directory, and a compiled persona.json sits in the same agent directory as agent.ts, so an id-resolved deploy bundles exactly what a path-resolved one does.

Errors

A built-in id resolves but has no file to deploy, and says so:

deploy: persona "persona-maker" resolves to the built-in catalog, which has no
file to deploy. Pass a path to a persona.json or persona.ts instead.

An unknown id lists what is available, from the registry:

deploy: Unknown persona "no-such-agent". Known personas: customer-dev,
persona-improver, persona-maker, proposal-agent

Verification

Exercised against the real ../sales checkout: bare id, explicit persona.ts path (unchanged), unknown id, and built-in id. deploy-command and local-personas suites pass locally (74). cli.test.ts spawns subprocesses and is being OOM-killed on this machine, so it is left to CI.

Semver: minor — new selector form, no change to existing path behavior.

🤖 Generated with Claude Code

Review in cubic

`agentworkforce deploy proposal-agent` resolves through the registry cascade,
so an agent kept in `.agentworkforce/workforce/agents/<name>/` deploys by name
from anywhere in the repo instead of by spelling out its path.
Syntax decides which a selector is: path separators, a leading `.`/`/`/`~`, or
a persona-source extension mean a path; anything else is an id. The filesystem
does not get a vote, so a bare `proposal-agent` cannot mean the persona in one
directory and a same-named folder in another.
A built-in id resolves but has no file to deploy, and says so rather than
failing on a path that was never going to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d670f133-5b4c-4895-b1ee-83ca054b8018

📥 Commits

Reviewing files that changed from the base of the PR and between 558784d and 01f746a.

📒 Files selected for processing (3)
  • packages/cli/src/cli-impl.ts
  • packages/cli/src/deploy-command.test.ts
  • packages/cli/src/deploy-command.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:01f746a99a

ℹ️ 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".


let resolved;
try {
resolved = resolvePersonaReference(selector);

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 Avoid interactive validation for deploy selectors

When a valid handler-based deploy persona omits harness, model, or systemPrompt, this call fails before returning its file path because resolvePersonaReference unconditionally builds an interactive PersonaSelection, whose validator rejects those omissions. Deploy's persona parser explicitly permits these fields to be absent when onEvent is present, so the same persona deploys by path but cannot deploy by the newly supported bare ID; use a registry lookup that does not require the interactive projection.

Useful? React with 👍 / 👎.

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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 Deploy the resolved cascade rather than the overlay file

When the selected ID is defined by a partial registry overlay, returning its declaring path discards the merged resolved.spec that made the persona valid. Deploy subsequently rereads the raw file in compileAgentSource; for example, the new test fixture containing only id and extends fails with missing top-level "intent", and other inherited deploy fields are similarly lost. The ID path needs to deploy a materialized merged spec, while preserving the declaring directory for relative handler and asset paths.

Useful? React with 👍 / 👎.

@cubic-dev-aicubic-dev-aiBot left a comment

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.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/deploy-command.ts">
<violation number="1" location="packages/cli/src/deploy-command.ts:342">
P1: Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</violation>
<violation number="2" location="packages/cli/src/deploy-command.ts:355">
P1: When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</violation>
</file>
<file name="packages/cli/src/deploy-command.test.ts">
<violation number="1" location="packages/cli/src/deploy-command.test.ts:548">
P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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.

P1: When a higher-priority partial overlay shadows an agent persona, resolved.path points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited cloud/onEvent, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 355:
<comment>When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+ 'Pass a path to a persona.json or persona.ts instead.'
+ );
+ }
+ return resolved.path;
+}
+
</file context>


let resolved;
try {
resolved = resolvePersonaReference(selector);

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.

P1: Resolve ID selectors without constructing an interactive PersonaSelection; otherwise handler personas that omit harness, model, or systemPrompt fail before deploy can obtain their path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 342:
<comment>Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+
+ let resolved;
+ try {
+ resolved = resolvePersonaReference(selector);
+ } catch (err) {
+ if (err instanceof PersonaResolutionError) {
</file context>

const trap = trapExit();
try {
assert.throws(
() => resolveDeployPersonaSelector('persona-maker'),

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.

P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. resolveDeployPersonaSelector('persona-maker') resolves through the registry cascade, where a local persona named persona-maker under the runner's cwd (process.cwd()) or in the configurable persona dirs (default ~/.agentworkforce/workforce/personas) wins over the built-in catalog and returns a real file path, so the /no file to deploy/ assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.test.ts, line 548:
<comment>The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</comment>
<file context>
@@ -480,3 +482,74 @@ test('runLogin canonicalizes origin.agentrelay.cloud apiUrl before resolving the
+ const trap = trapExit();
+ try {
+ assert.throws(
+ () => resolveDeployPersonaSelector('persona-maker'),
+ /__exit_trap__/
+ );
</file context>

@willwashburn
willwashburn marked this pull request as draft August 20, 2026 16:40
@willwashburn

Copy link
Copy Markdown
MemberAuthor

Both P1s are real. Reproduced each before changing anything, and I've converted this to draft.

Interactive projection blocks handler personas. Built a handler persona from examples/weekly-digest/persona.json with harness/model/systemPrompt removed, in .agentworkforce/workforce/agents/digest/:

$ agentworkforce deploy .../agents/digest/persona.json --dry-run
persona digest: 1 integration(s), 1 schedule(s)
ok: digest (dry-run)
$ agentworkforce deploy digest --dry-run
warning: [cwd:agents] digest: standalone persona "digest".harness is required for standalone personas
deploy: Unknown persona "digest". Known personas: persona-improver, persona-maker

Same file, two selectors, opposite outcomes. The rejection is deeper than the PersonaSelection projection — it happens during registry resolution, so the persona never enters byId at all. That also means #316's cwd:agents layer cannot load exactly the class of agent it was built for unless that agent happens to declare interactive fields it does not use, and the warning it emits reads as "your persona is malformed" when the persona is a valid deployable one.

Overlay shadowing loses the agent file. A partial overlay at personas/digest.json containing only {"id","env"}:

$ agentworkforce list
digest cwd opencode opencode/gpt-5-nano # merges correctly
$ agentworkforce deploy digest --dry-run
workforce deploy → .../personas/digest.json
agentworkforce deploy failed: persona at .../personas/digest.json is missing top-level "intent"

resolved.path is the declaring file of the winning layer, which for an overlay is not a deployable persona. Deploy rereads it raw and loses onEvent, cloud, and intent.

The fix is the one you both point at — hand deploy the materialized merged spec plus the directory that owns the handler, rather than a path — and it needs the registry to be able to hold handler personas in the first place. That is a larger change than this PR, and it touches ground #316 already shipped, so I would rather land it deliberately than patch it here.

The P3 on test isolation is also correct; the built-in-id assertion reads ambient cwd and personal config.

Sign up for freeto 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.

1 participant

@willwashburn
, '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

feat(cli): deploy a persona by id, not just by path - #320

Draft
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id
Draft

feat(cli): deploy a persona by id, not just by path#320
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id

Conversation

@willwashburn

@willwashburnwillwashburn commented Aug 20, 2026

Copy link
Copy Markdown
Member

agentworkforce deploy <persona-id> resolves through the registry cascade, so an agent kept in .agentworkforce/workforce/agents/<name>/ deploys by name from anywhere in the repo. Completes the loop opened by #316, which made those agents discoverable to list/show/agent but left deploy path-only.

$ agentworkforce deploy proposal-agent --dry-run # in ../sales
workforce deploy → .../agents/proposal-agent/persona.json
persona proposal-agent: 1 integration(s), 1 schedule(s)
ok: proposal-agent (dry-run)

Which selectors are paths

Syntax decides, never the filesystem. A path separator, a leading .///~, or a persona-source extension means a path; anything else is an id. Probing disk instead would let a bare proposal-agent mean the persona in one directory and a same-named folder in another — the same command doing different things depending on where it ran.

Handler resolution is unaffected: onEvent resolves against the persona file's directory, and a compiled persona.json sits in the same agent directory as agent.ts, so an id-resolved deploy bundles exactly what a path-resolved one does.

Errors

A built-in id resolves but has no file to deploy, and says so:

deploy: persona "persona-maker" resolves to the built-in catalog, which has no
file to deploy. Pass a path to a persona.json or persona.ts instead.

An unknown id lists what is available, from the registry:

deploy: Unknown persona "no-such-agent". Known personas: customer-dev,
persona-improver, persona-maker, proposal-agent

Verification

Exercised against the real ../sales checkout: bare id, explicit persona.ts path (unchanged), unknown id, and built-in id. deploy-command and local-personas suites pass locally (74). cli.test.ts spawns subprocesses and is being OOM-killed on this machine, so it is left to CI.

Semver: minor — new selector form, no change to existing path behavior.

🤖 Generated with Claude Code

Review in cubic

`agentworkforce deploy proposal-agent` resolves through the registry cascade,
so an agent kept in `.agentworkforce/workforce/agents/<name>/` deploys by name
from anywhere in the repo instead of by spelling out its path.
Syntax decides which a selector is: path separators, a leading `.`/`/`/`~`, or
a persona-source extension mean a path; anything else is an id. The filesystem
does not get a vote, so a bare `proposal-agent` cannot mean the persona in one
directory and a same-named folder in another.
A built-in id resolves but has no file to deploy, and says so rather than
failing on a path that was never going to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d670f133-5b4c-4895-b1ee-83ca054b8018

📥 Commits

Reviewing files that changed from the base of the PR and between 558784d and 01f746a.

📒 Files selected for processing (3)
  • packages/cli/src/cli-impl.ts
  • packages/cli/src/deploy-command.test.ts
  • packages/cli/src/deploy-command.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:01f746a99a

ℹ️ 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".


let resolved;
try {
resolved = resolvePersonaReference(selector);

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 Avoid interactive validation for deploy selectors

When a valid handler-based deploy persona omits harness, model, or systemPrompt, this call fails before returning its file path because resolvePersonaReference unconditionally builds an interactive PersonaSelection, whose validator rejects those omissions. Deploy's persona parser explicitly permits these fields to be absent when onEvent is present, so the same persona deploys by path but cannot deploy by the newly supported bare ID; use a registry lookup that does not require the interactive projection.

Useful? React with 👍 / 👎.

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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 Deploy the resolved cascade rather than the overlay file

When the selected ID is defined by a partial registry overlay, returning its declaring path discards the merged resolved.spec that made the persona valid. Deploy subsequently rereads the raw file in compileAgentSource; for example, the new test fixture containing only id and extends fails with missing top-level "intent", and other inherited deploy fields are similarly lost. The ID path needs to deploy a materialized merged spec, while preserving the declaring directory for relative handler and asset paths.

Useful? React with 👍 / 👎.

@cubic-dev-aicubic-dev-aiBot left a comment

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.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/deploy-command.ts">
<violation number="1" location="packages/cli/src/deploy-command.ts:342">
P1: Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</violation>
<violation number="2" location="packages/cli/src/deploy-command.ts:355">
P1: When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</violation>
</file>
<file name="packages/cli/src/deploy-command.test.ts">
<violation number="1" location="packages/cli/src/deploy-command.test.ts:548">
P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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.

P1: When a higher-priority partial overlay shadows an agent persona, resolved.path points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited cloud/onEvent, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 355:
<comment>When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+ 'Pass a path to a persona.json or persona.ts instead.'
+ );
+ }
+ return resolved.path;
+}
+
</file context>


let resolved;
try {
resolved = resolvePersonaReference(selector);

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.

P1: Resolve ID selectors without constructing an interactive PersonaSelection; otherwise handler personas that omit harness, model, or systemPrompt fail before deploy can obtain their path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 342:
<comment>Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+
+ let resolved;
+ try {
+ resolved = resolvePersonaReference(selector);
+ } catch (err) {
+ if (err instanceof PersonaResolutionError) {
</file context>

const trap = trapExit();
try {
assert.throws(
() => resolveDeployPersonaSelector('persona-maker'),

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.

P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. resolveDeployPersonaSelector('persona-maker') resolves through the registry cascade, where a local persona named persona-maker under the runner's cwd (process.cwd()) or in the configurable persona dirs (default ~/.agentworkforce/workforce/personas) wins over the built-in catalog and returns a real file path, so the /no file to deploy/ assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.test.ts, line 548:
<comment>The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</comment>
<file context>
@@ -480,3 +482,74 @@ test('runLogin canonicalizes origin.agentrelay.cloud apiUrl before resolving the
+ const trap = trapExit();
+ try {
+ assert.throws(
+ () => resolveDeployPersonaSelector('persona-maker'),
+ /__exit_trap__/
+ );
</file context>

@willwashburn
willwashburn marked this pull request as draft August 20, 2026 16:40
@willwashburn

Copy link
Copy Markdown
MemberAuthor

Both P1s are real. Reproduced each before changing anything, and I've converted this to draft.

Interactive projection blocks handler personas. Built a handler persona from examples/weekly-digest/persona.json with harness/model/systemPrompt removed, in .agentworkforce/workforce/agents/digest/:

$ agentworkforce deploy .../agents/digest/persona.json --dry-run
persona digest: 1 integration(s), 1 schedule(s)
ok: digest (dry-run)
$ agentworkforce deploy digest --dry-run
warning: [cwd:agents] digest: standalone persona "digest".harness is required for standalone personas
deploy: Unknown persona "digest". Known personas: persona-improver, persona-maker

Same file, two selectors, opposite outcomes. The rejection is deeper than the PersonaSelection projection — it happens during registry resolution, so the persona never enters byId at all. That also means #316's cwd:agents layer cannot load exactly the class of agent it was built for unless that agent happens to declare interactive fields it does not use, and the warning it emits reads as "your persona is malformed" when the persona is a valid deployable one.

Overlay shadowing loses the agent file. A partial overlay at personas/digest.json containing only {"id","env"}:

$ agentworkforce list
digest cwd opencode opencode/gpt-5-nano # merges correctly
$ agentworkforce deploy digest --dry-run
workforce deploy → .../personas/digest.json
agentworkforce deploy failed: persona at .../personas/digest.json is missing top-level "intent"

resolved.path is the declaring file of the winning layer, which for an overlay is not a deployable persona. Deploy rereads it raw and loses onEvent, cloud, and intent.

The fix is the one you both point at — hand deploy the materialized merged spec plus the directory that owns the handler, rather than a path — and it needs the registry to be able to hold handler personas in the first place. That is a larger change than this PR, and it touches ground #316 already shipped, so I would rather land it deliberately than patch it here.

The P3 on test isolation is also correct; the built-in-id assertion reads ambient cwd and personal config.

Sign up for freeto 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.

1 participant

@willwashburn
, '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

feat(cli): deploy a persona by id, not just by path - #320

Draft
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id
Draft

feat(cli): deploy a persona by id, not just by path#320
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id

Conversation

@willwashburn

@willwashburnwillwashburn commented Aug 20, 2026

Copy link
Copy Markdown
Member

agentworkforce deploy <persona-id> resolves through the registry cascade, so an agent kept in .agentworkforce/workforce/agents/<name>/ deploys by name from anywhere in the repo. Completes the loop opened by #316, which made those agents discoverable to list/show/agent but left deploy path-only.

$ agentworkforce deploy proposal-agent --dry-run # in ../sales
workforce deploy → .../agents/proposal-agent/persona.json
persona proposal-agent: 1 integration(s), 1 schedule(s)
ok: proposal-agent (dry-run)

Which selectors are paths

Syntax decides, never the filesystem. A path separator, a leading .///~, or a persona-source extension means a path; anything else is an id. Probing disk instead would let a bare proposal-agent mean the persona in one directory and a same-named folder in another — the same command doing different things depending on where it ran.

Handler resolution is unaffected: onEvent resolves against the persona file's directory, and a compiled persona.json sits in the same agent directory as agent.ts, so an id-resolved deploy bundles exactly what a path-resolved one does.

Errors

A built-in id resolves but has no file to deploy, and says so:

deploy: persona "persona-maker" resolves to the built-in catalog, which has no
file to deploy. Pass a path to a persona.json or persona.ts instead.

An unknown id lists what is available, from the registry:

deploy: Unknown persona "no-such-agent". Known personas: customer-dev,
persona-improver, persona-maker, proposal-agent

Verification

Exercised against the real ../sales checkout: bare id, explicit persona.ts path (unchanged), unknown id, and built-in id. deploy-command and local-personas suites pass locally (74). cli.test.ts spawns subprocesses and is being OOM-killed on this machine, so it is left to CI.

Semver: minor — new selector form, no change to existing path behavior.

🤖 Generated with Claude Code

Review in cubic

`agentworkforce deploy proposal-agent` resolves through the registry cascade,
so an agent kept in `.agentworkforce/workforce/agents/<name>/` deploys by name
from anywhere in the repo instead of by spelling out its path.
Syntax decides which a selector is: path separators, a leading `.`/`/`/`~`, or
a persona-source extension mean a path; anything else is an id. The filesystem
does not get a vote, so a bare `proposal-agent` cannot mean the persona in one
directory and a same-named folder in another.
A built-in id resolves but has no file to deploy, and says so rather than
failing on a path that was never going to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d670f133-5b4c-4895-b1ee-83ca054b8018

📥 Commits

Reviewing files that changed from the base of the PR and between 558784d and 01f746a.

📒 Files selected for processing (3)
  • packages/cli/src/cli-impl.ts
  • packages/cli/src/deploy-command.test.ts
  • packages/cli/src/deploy-command.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:01f746a99a

ℹ️ 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".


let resolved;
try {
resolved = resolvePersonaReference(selector);

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 Avoid interactive validation for deploy selectors

When a valid handler-based deploy persona omits harness, model, or systemPrompt, this call fails before returning its file path because resolvePersonaReference unconditionally builds an interactive PersonaSelection, whose validator rejects those omissions. Deploy's persona parser explicitly permits these fields to be absent when onEvent is present, so the same persona deploys by path but cannot deploy by the newly supported bare ID; use a registry lookup that does not require the interactive projection.

Useful? React with 👍 / 👎.

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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 Deploy the resolved cascade rather than the overlay file

When the selected ID is defined by a partial registry overlay, returning its declaring path discards the merged resolved.spec that made the persona valid. Deploy subsequently rereads the raw file in compileAgentSource; for example, the new test fixture containing only id and extends fails with missing top-level "intent", and other inherited deploy fields are similarly lost. The ID path needs to deploy a materialized merged spec, while preserving the declaring directory for relative handler and asset paths.

Useful? React with 👍 / 👎.

@cubic-dev-aicubic-dev-aiBot left a comment

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.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/deploy-command.ts">
<violation number="1" location="packages/cli/src/deploy-command.ts:342">
P1: Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</violation>
<violation number="2" location="packages/cli/src/deploy-command.ts:355">
P1: When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</violation>
</file>
<file name="packages/cli/src/deploy-command.test.ts">
<violation number="1" location="packages/cli/src/deploy-command.test.ts:548">
P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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.

P1: When a higher-priority partial overlay shadows an agent persona, resolved.path points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited cloud/onEvent, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 355:
<comment>When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+ 'Pass a path to a persona.json or persona.ts instead.'
+ );
+ }
+ return resolved.path;
+}
+
</file context>


let resolved;
try {
resolved = resolvePersonaReference(selector);

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.

P1: Resolve ID selectors without constructing an interactive PersonaSelection; otherwise handler personas that omit harness, model, or systemPrompt fail before deploy can obtain their path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 342:
<comment>Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+
+ let resolved;
+ try {
+ resolved = resolvePersonaReference(selector);
+ } catch (err) {
+ if (err instanceof PersonaResolutionError) {
</file context>

const trap = trapExit();
try {
assert.throws(
() => resolveDeployPersonaSelector('persona-maker'),

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.

P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. resolveDeployPersonaSelector('persona-maker') resolves through the registry cascade, where a local persona named persona-maker under the runner's cwd (process.cwd()) or in the configurable persona dirs (default ~/.agentworkforce/workforce/personas) wins over the built-in catalog and returns a real file path, so the /no file to deploy/ assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.test.ts, line 548:
<comment>The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</comment>
<file context>
@@ -480,3 +482,74 @@ test('runLogin canonicalizes origin.agentrelay.cloud apiUrl before resolving the
+ const trap = trapExit();
+ try {
+ assert.throws(
+ () => resolveDeployPersonaSelector('persona-maker'),
+ /__exit_trap__/
+ );
</file context>

@willwashburn
willwashburn marked this pull request as draft August 20, 2026 16:40
@willwashburn

Copy link
Copy Markdown
MemberAuthor

Both P1s are real. Reproduced each before changing anything, and I've converted this to draft.

Interactive projection blocks handler personas. Built a handler persona from examples/weekly-digest/persona.json with harness/model/systemPrompt removed, in .agentworkforce/workforce/agents/digest/:

$ agentworkforce deploy .../agents/digest/persona.json --dry-run
persona digest: 1 integration(s), 1 schedule(s)
ok: digest (dry-run)
$ agentworkforce deploy digest --dry-run
warning: [cwd:agents] digest: standalone persona "digest".harness is required for standalone personas
deploy: Unknown persona "digest". Known personas: persona-improver, persona-maker

Same file, two selectors, opposite outcomes. The rejection is deeper than the PersonaSelection projection — it happens during registry resolution, so the persona never enters byId at all. That also means #316's cwd:agents layer cannot load exactly the class of agent it was built for unless that agent happens to declare interactive fields it does not use, and the warning it emits reads as "your persona is malformed" when the persona is a valid deployable one.

Overlay shadowing loses the agent file. A partial overlay at personas/digest.json containing only {"id","env"}:

$ agentworkforce list
digest cwd opencode opencode/gpt-5-nano # merges correctly
$ agentworkforce deploy digest --dry-run
workforce deploy → .../personas/digest.json
agentworkforce deploy failed: persona at .../personas/digest.json is missing top-level "intent"

resolved.path is the declaring file of the winning layer, which for an overlay is not a deployable persona. Deploy rereads it raw and loses onEvent, cloud, and intent.

The fix is the one you both point at — hand deploy the materialized merged spec plus the directory that owns the handler, rather than a path — and it needs the registry to be able to hold handler personas in the first place. That is a larger change than this PR, and it touches ground #316 already shipped, so I would rather land it deliberately than patch it here.

The P3 on test isolation is also correct; the built-in-id assertion reads ambient cwd and personal config.

Sign up for freeto 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.

1 participant

@willwashburn
, '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

feat(cli): deploy a persona by id, not just by path - #320

Draft
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id
Draft

feat(cli): deploy a persona by id, not just by path#320
willwashburn wants to merge 1 commit into
mainfrom
feat/deploy-persona-by-id

Conversation

@willwashburn

@willwashburnwillwashburn commented Aug 20, 2026

Copy link
Copy Markdown
Member

agentworkforce deploy <persona-id> resolves through the registry cascade, so an agent kept in .agentworkforce/workforce/agents/<name>/ deploys by name from anywhere in the repo. Completes the loop opened by #316, which made those agents discoverable to list/show/agent but left deploy path-only.

$ agentworkforce deploy proposal-agent --dry-run # in ../sales
workforce deploy → .../agents/proposal-agent/persona.json
persona proposal-agent: 1 integration(s), 1 schedule(s)
ok: proposal-agent (dry-run)

Which selectors are paths

Syntax decides, never the filesystem. A path separator, a leading .///~, or a persona-source extension means a path; anything else is an id. Probing disk instead would let a bare proposal-agent mean the persona in one directory and a same-named folder in another — the same command doing different things depending on where it ran.

Handler resolution is unaffected: onEvent resolves against the persona file's directory, and a compiled persona.json sits in the same agent directory as agent.ts, so an id-resolved deploy bundles exactly what a path-resolved one does.

Errors

A built-in id resolves but has no file to deploy, and says so:

deploy: persona "persona-maker" resolves to the built-in catalog, which has no
file to deploy. Pass a path to a persona.json or persona.ts instead.

An unknown id lists what is available, from the registry:

deploy: Unknown persona "no-such-agent". Known personas: customer-dev,
persona-improver, persona-maker, proposal-agent

Verification

Exercised against the real ../sales checkout: bare id, explicit persona.ts path (unchanged), unknown id, and built-in id. deploy-command and local-personas suites pass locally (74). cli.test.ts spawns subprocesses and is being OOM-killed on this machine, so it is left to CI.

Semver: minor — new selector form, no change to existing path behavior.

🤖 Generated with Claude Code

Review in cubic

`agentworkforce deploy proposal-agent` resolves through the registry cascade,
so an agent kept in `.agentworkforce/workforce/agents/<name>/` deploys by name
from anywhere in the repo instead of by spelling out its path.
Syntax decides which a selector is: path separators, a leading `.`/`/`/`~`, or
a persona-source extension mean a path; anything else is an id. The filesystem
does not get a vote, so a bare `proposal-agent` cannot mean the persona in one
directory and a same-named folder in another.
A built-in id resolves but has no file to deploy, and says so rather than
failing on a path that was never going to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@willwashburn, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d670f133-5b4c-4895-b1ee-83ca054b8018

📥 Commits

Reviewing files that changed from the base of the PR and between 558784d and 01f746a.

📒 Files selected for processing (3)
  • packages/cli/src/cli-impl.ts
  • packages/cli/src/deploy-command.test.ts
  • packages/cli/src/deploy-command.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

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:01f746a99a

ℹ️ 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".


let resolved;
try {
resolved = resolvePersonaReference(selector);

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 Avoid interactive validation for deploy selectors

When a valid handler-based deploy persona omits harness, model, or systemPrompt, this call fails before returning its file path because resolvePersonaReference unconditionally builds an interactive PersonaSelection, whose validator rejects those omissions. Deploy's persona parser explicitly permits these fields to be absent when onEvent is present, so the same persona deploys by path but cannot deploy by the newly supported bare ID; use a registry lookup that does not require the interactive projection.

Useful? React with 👍 / 👎.

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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 Deploy the resolved cascade rather than the overlay file

When the selected ID is defined by a partial registry overlay, returning its declaring path discards the merged resolved.spec that made the persona valid. Deploy subsequently rereads the raw file in compileAgentSource; for example, the new test fixture containing only id and extends fails with missing top-level "intent", and other inherited deploy fields are similarly lost. The ID path needs to deploy a materialized merged spec, while preserving the declaring directory for relative handler and asset paths.

Useful? React with 👍 / 👎.

@cubic-dev-aicubic-dev-aiBot left a comment

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.

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/deploy-command.ts">
<violation number="1" location="packages/cli/src/deploy-command.ts:342">
P1: Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</violation>
<violation number="2" location="packages/cli/src/deploy-command.ts:355">
P1: When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</violation>
</file>
<file name="packages/cli/src/deploy-command.test.ts">
<violation number="1" location="packages/cli/src/deploy-command.test.ts:548">
P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

'Pass a path to a persona.json or persona.ts instead.'
);
}
return resolved.path;

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.

P1: When a higher-priority partial overlay shadows an agent persona, resolved.path points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited cloud/onEvent, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 355:
<comment>When a higher-priority partial overlay shadows an agent persona, `resolved.path` points to the overlay instead of the merged agent file. Deploy then rereads only that overlay, loses inherited `cloud`/`onEvent`, and rejects the ID deployment before bundling; pass the merged registry result and the handler-owning path into deploy, or resolve the owning file before returning.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+ 'Pass a path to a persona.json or persona.ts instead.'
+ );
+ }
+ return resolved.path;
+}
+
</file context>


let resolved;
try {
resolved = resolvePersonaReference(selector);

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.

P1: Resolve ID selectors without constructing an interactive PersonaSelection; otherwise handler personas that omit harness, model, or systemPrompt fail before deploy can obtain their path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.ts, line 342:
<comment>Resolve ID selectors without constructing an interactive `PersonaSelection`; otherwise handler personas that omit `harness`, `model`, or `systemPrompt` fail before deploy can obtain their path.</comment>
<file context>
@@ -303,6 +312,49 @@ Flags:
+
+ let resolved;
+ try {
+ resolved = resolvePersonaReference(selector);
+ } catch (err) {
+ if (err instanceof PersonaResolutionError) {
</file context>

const trap = trapExit();
try {
assert.throws(
() => resolveDeployPersonaSelector('persona-maker'),

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.

P3: The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. resolveDeployPersonaSelector('persona-maker') resolves through the registry cascade, where a local persona named persona-maker under the runner's cwd (process.cwd()) or in the configurable persona dirs (default ~/.agentworkforce/workforce/personas) wins over the built-in catalog and returns a real file path, so the /no file to deploy/ assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/deploy-command.test.ts, line 548:
<comment>The built-in-id test is not isolated from ambient developer configuration, so it can fail (or false-pass) depending on the machine it runs on. `resolveDeployPersonaSelector('persona-maker')` resolves through the registry cascade, where a local persona named `persona-maker` under the runner's cwd (`process.cwd()`) or in the configurable persona dirs (default `~/.agentworkforce/workforce/personas`) wins over the built-in catalog and returns a real file path, so the `/no file to deploy/` assertion fails even though the behavior under test is correct. The other new selector test isolates this by chdir'ing into a fresh mkdtemp root; this one leaves cwd and the ambient config untouched. Run the assertion from an isolated temporary cwd (and remove it in finally) so only the built-in resolution drives the outcome.</comment>
<file context>
@@ -480,3 +482,74 @@ test('runLogin canonicalizes origin.agentrelay.cloud apiUrl before resolving the
+ const trap = trapExit();
+ try {
+ assert.throws(
+ () => resolveDeployPersonaSelector('persona-maker'),
+ /__exit_trap__/
+ );
</file context>

@willwashburn
willwashburn marked this pull request as draft August 20, 2026 16:40
@willwashburn

Copy link
Copy Markdown
MemberAuthor

Both P1s are real. Reproduced each before changing anything, and I've converted this to draft.

Interactive projection blocks handler personas. Built a handler persona from examples/weekly-digest/persona.json with harness/model/systemPrompt removed, in .agentworkforce/workforce/agents/digest/:

$ agentworkforce deploy .../agents/digest/persona.json --dry-run
persona digest: 1 integration(s), 1 schedule(s)
ok: digest (dry-run)
$ agentworkforce deploy digest --dry-run
warning: [cwd:agents] digest: standalone persona "digest".harness is required for standalone personas
deploy: Unknown persona "digest". Known personas: persona-improver, persona-maker

Same file, two selectors, opposite outcomes. The rejection is deeper than the PersonaSelection projection — it happens during registry resolution, so the persona never enters byId at all. That also means #316's cwd:agents layer cannot load exactly the class of agent it was built for unless that agent happens to declare interactive fields it does not use, and the warning it emits reads as "your persona is malformed" when the persona is a valid deployable one.

Overlay shadowing loses the agent file. A partial overlay at personas/digest.json containing only {"id","env"}:

$ agentworkforce list
digest cwd opencode opencode/gpt-5-nano # merges correctly
$ agentworkforce deploy digest --dry-run
workforce deploy → .../personas/digest.json
agentworkforce deploy failed: persona at .../personas/digest.json is missing top-level "intent"

resolved.path is the declaring file of the winning layer, which for an overlay is not a deployable persona. Deploy rereads it raw and loses onEvent, cloud, and intent.

The fix is the one you both point at — hand deploy the materialized merged spec plus the directory that owns the handler, rather than a path — and it needs the registry to be able to hold handler personas in the first place. That is a larger change than this PR, and it touches ground #316 already shipped, so I would rather land it deliberately than patch it here.

The P3 on test isolation is also correct; the built-in-id assertion reads ambient cwd and personal config.

Sign up for freeto 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.

1 participant

@willwashburn