Skip to content

feat(delegation): per-task model selection and profile identity for delegate_task - #98031

Open
CaptainPickard wants to merge 5 commits into
NousResearch:mainfrom
CaptainPickard:feat/per-task-model-and-profile-identity
Open

CaptainPickard wants to merge 5 commits into
NousResearch:mainfrom
CaptainPickard:feat/per-task-model-and-profile-identity

Conversation

@CaptainPickard

Copy link
Copy Markdown

Summary

  • Per-task model selection: each task in a delegate_task batch can name a model ("opus", "gpt-5", "glm", or a full "vendor/model" slug), resolved leniently through the existing model_switch pipeline (same as /model). Provider is resolved, not dictated.
  • Per-task profile identity: each task can name a Hermes profile; the child loads that profile's SOUL.md, IDENTITY.md, and AGENTS.md as its system prompt, and reads model/provider from its config.yaml when not explicitly overridden. The child becomes the named bot rather than a generic subagent.
  • Both features are config-gated (default off) so the existing subagent contract is unchanged when the flags are off. Schema fields only appear when the corresponding flag is enabled.

Motivation

Multi-role workflows (e.g. dispatching a code reviewer, test validator, and security auditor in one delegate_task batch) currently require every child to run on the same model with the same generic identity. This PR adds two independent opt-in capabilities:

  1. Model routing lets the agent fan work across different models in a single batch -- useful for comparing results across models, or routing specialized work to specialized models.

  2. Profile identity lets each child adopt a named profile's identity files and model config, so a delegation batch can dispatch specialized bots with their own personas, rules, and model configurations.

Both flags are off by default because:

  • Per-task model routing can send work to a more expensive model than expected.
  • Loading arbitrary profile files into a child prompt is a trust boundary that should be deliberate.
  • The schema fields only appear when opted in, keeping the tool surface minimal otherwise.

The model selection feature overlaps conceptually with the kilocode-port/per-task-delegation-model branch, but this PR takes a different approach: it adds profile identity as a complementary feature and uses a unified code path. Happy to coordinate if there's interest in merging the approaches.

Changes

  • tools/delegate_tool.py:
    • _get_allow_model_selection() / _get_allow_profile_identity(): config-gated flag getters
    • _resolve_task_model_creds(): resolves a per-task model name via model_switch.switch_model() (reuses the /model command's resolution pipeline)
    • _load_profile_identity(): loads a named profile's SOUL.md, IDENTITY.md, AGENTS.md, and config.yaml model/provider
    • _build_child_system_prompt(): accepts optional profile_identity to replace the generic subagent preamble with the profile's identity files
    • _build_child_agent(): passes profile_identity through to the system prompt builder
    • delegate_task(): accepts model and profile params; per-task resolution in the child loop (gated)
    • _build_dynamic_schema_overrides(): adds model/profile fields only when the corresponding flag is on; deep-copies tasks schema to avoid mutating the static schema
    • Registry handler passes model/profile through
  • hermes_cli/config_defaults.py: adds allow_model_selection and allow_profile_identity defaults (both False)
  • website/docs/user-guide/features/delegation.md: documents both features with config examples
  • tests/tools/test_delegate_per_task_overrides.py: 17 tests covering schema gating, flag getters, model resolution, profile identity loading, and system prompt construction

Test Plan

  • 17 new tests pass (python3 -m pytest tests/tools/test_delegate_per_task_overrides.py -v)
  • 70 existing delegate tests pass (no regressions: python3 -m pytest tests/tools/test_delegate.py -v)
  • Syntax checks pass for all modified files
  • CI passes on GitHub Actions

Notes for Reviewers

  • The flag-off path is byte-identical to prior behavior -- no schema changes, no code path changes when both flags are False.
  • The _resolve_task_model_creds function reuses the existing model_switch.switch_model() pipeline rather than reinventing model resolution, so aggregator-aware aliasing, fuzzy matching, and cross-provider fallback all come for free.
  • The profile identity feature is the unique contribution not present in the kilocode-port/per-task-delegation-model branch. Both features can be combined: a task can name both model and profile, with the explicit model taking precedence over the profile's config.
  • The dynamic schema override deep-copies the tasks property before adding per-task fields, so DELEGATE_TASK_SCHEMA is never mutated.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard tool/delegate Subagent delegation area/config Config system, migrations, profiles needs-decision Awaiting maintainer decision before any implementation labels Aug 29, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

Overall: Well-gated feature — per-task model selection and profile identity for delegate_task, both off by default.

What it does

  • hermes_cli/config_defaults.py:17 + :28 add delegation.allow_model_selection / allow_profile_identity (default False) with clear trust/cost rationale in comments/docs.
  • tools/delegate_tool.py:346 + :358 — flag getters; _resolve_task_model_creds (tools/delegate_tool.py:370) reuses model_switch.switch_model anchored on parent provider/creds; raises ValueError on unresolvable name instead of silent fallback.
  • tools/delegate_tool.py:444_load_profile_identity loads SOUL.md/IDENTITY.md/AGENTS.md + config.yaml model/provider best-effort; _build_child_system_prompt (tools/delegate_tool.py:524) replaces generic preamble when soul or identity present.
  • tools/delegate_tool.py:605 + :628 — per-task model/profile resolved inside delegate_task loop; task_creds threaded through _build_child_preserving_parent_tools; dynamic schema (tools/delegate_tool.py:706) only advertises fields when flags on (prompt-cache stable).
  • Docs website/docs/user-guide/features/delegation.md:780 explain both flags.

Non-blocking / consider before merge

  • Path traversal: _load_profile_identity does get_default_hermes_root() / "profiles" / profile_name with raw profile_name.strip() (tools/delegate_tool.py:453). A task value like ../../.ssh or foo/../../other could escape the profiles root via Path joining. Consider validating profile_name against ^[A-Za-z0-9_-]+$ and rejecting slashes/dots, or resolving and checking is_relative_to(profiles_root).
  • Profile config.yaml model fallback only applies when allow_model_selection is also true (tools/delegate_tool.py:642) — intentional but worth calling out in docs (already noted).
  • _build_dynamic_schema_overrides deep-copies _tasks_prop before mutation (tools/delegate_tool.py:711) — good; static schema immutability tested (tests/tools/test_delegate_per_task_overrides.py:152).

Please use your judgment — the traversal guard is the only blocking-adjacent item.

@andrexibiza andrexibiza 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.

I reviewed exact head 4c37a70d976c8b2a9144fd6e412f1fc791c1214c against current main@4f22543509d1b91dc45bcb369447126c5eb14fb7, including the full 16-file diff / 9-commit history, the behavior-critical child-construction and session-DB paths, the new tests, exact-head check state, and the overlapping delegation/profile/session PRs.

There is good engineering in this branch. In particular, the earlier traversal finding was actually absorbed; model switching now anchors on the effective delegation provider rather than blindly on the parent; cross-provider switches clear stale ACP/pin transport fields; and the model/profile schema remains default-off. Those are real improvements.

I do not think this exact object is safe to land yet, though. The remaining blockers are architectural/correctness issues rather than polish.

P1 — batch override/profile preflight is not atomic; a later bad task can orphan earlier constructed children

At this head, delegate_task() resolves each task's model/profile and then immediately constructs that child in the same for i, t in enumerate(task_list) loop (tools/delegate_tool.py, around the new per-task routing block at ~4246 and _build_child_preserving_parent_tools at ~4332). A later task can then fail model resolution, profile lookup, allowlist validation, or profile-model resolution and return tool_error(...) after earlier children already exist.

That is load-bearing because child construction is not inert: _build_child_agent() opens a dedicated SessionDB and registers the child on parent_agent._active_children. The early return does not run those already-constructed children through the normal lifecycle/cleanup path.

There is already a narrower concurrent implementation of this exact slice in #98128 / #97653. Its two-pass preflight is explicit about the same failure mode: resolve every task before any child construction because construction opens the DB handle and registers _active_children. That PR is overlap, not a duplicate of this whole profile-identity feature, but this branch needs the same atomic boundary regardless of which vehicle survives.

Required fix: preflight all model/profile inputs for the whole batch before constructing task 0: profile syntax/existence/allowlist, effective profile provider+model, explicit task model/provider, and the final credential bundle. Then build children from immutable pre-resolved task plans. Add a public-path regression where task 0 is valid and task 1 is invalid and assert zero child constructors ran / _active_children is unchanged (and no child DB ownership is left behind).

P1 — the selected profile's configured provider is loaded but never applied

_load_profile_identity() reads both:

  • model.defaultresult["model"]
  • model.providerresult["provider"]

and the PR/schema promise that a profile's model/provider config is used when not explicitly overridden.

But the dispatch path only consumes _profile_identity["model"]:

_profile_model = _profile_identity.get("model")
if _profile_model and allow_model_selection:
    task_creds = _resolve_task_model_creds(_profile_model, parent_agent, creds)

_profile_identity["provider"] is never used. _resolve_task_model_creds() then anchors on base_creds["provider"], which is the delegation/parent route, not the selected profile route.

So a parent/delegation running on provider A can select a profile configured for provider B and still resolve that profile's model against A. That can mean the wrong endpoint/credential/billing route or a false model-resolution failure. The loader tests prove the provider string was read, but not that the child actually receives it.

Required fix: resolve a selected profile's provider+model as one routing unit through the canonical runtime resolver, with the documented precedence (explicit task override > profile config > delegation config > parent). Add a dispatch-level regression where parent/delegation provider != profile provider and assert the child gets the expected provider, model, base URL, API key/API mode, and transport fields—not merely the loader dictionary.

P1 — the new model-facing profile input still has a known filesystem exception path

Commit 51ea414ba4abbdc8b19f796168b074fc6d8aef1f records the remaining bug directly: a regex-valid profile name longer than the filesystem component limit can raise OSError: ENAMETOOLONG at profile_path.is_dir() because that probe sits outside the read-side exception handling.

I would not defer that as a follow-up. This PR is the code introducing a model-facing profile-name → filesystem path boundary, so rejecting hostile/invalid names belongs to this boundary. #90331 is adjacent profile-management work and already uses Hermes' canonical normalize_profile_name / validate_profile_name path rather than defining a second name law.

Required fix: reuse the canonical profile-name validation/bounds (or equivalently make the filesystem probe fail closed) before touching disk, and add a delegate_task regression for an overlong alphanumeric name. It should return a bounded tool error and construct nothing.

P1 — provenance/ownership needs recomposition; two substantial slices already have open upstream owners

The 9-commit branch has grown well beyond the model/profile feature in the PR description. Two of those additions are already open upstream as their own reviewable objects:

  • #91252 (teknium1, exact head 7e60ebc5d495efa1fba8c25201727c54792ca3cb) is the existing fork implementation porting MoonshotAI/kimi-code#3007. This branch's fork commit explicitly ports that upstream head and carries the same module/tests/schema behavior. That slice is duplicate/derived work; #91252 remains the original repository owner.
  • #91271 (teknium1, exact head 037ae2724c99d354392a14d1aff92e63e4755f3a) is the existing per-delegation usage ledger inspired by Copilot CLI. This branch carries that ledger plus a useful profile attribution extension. The profile column is complementary work; the underlying ledger is not new ownership.
  • #98128 / #97653 (DavidMetcalfe) directly overlaps the per-dispatch model/provider routing slice and already supplies the atomic preflight this branch is missing. It is not a duplicate of profile identity, but the two cannot land independently in delegate_tool.py without choosing/recomposing one routing owner.

Please preserve those edges and authorship rather than landing rewritten copies as a single omnibus commit train. The clean shape is to drop/rebase onto the surviving upstream owners (or cherry-pick the original objects with authorship intact) and keep only this PR's genuinely complementary profile-identity delta. External source credit to Kilo, Moonshot/Kimi, and Copilot CLI should remain as well.

There are additional adjacent/complementary edges worth preserving during that rebase rather than flattening them into duplicates: #95534 (bounded reusable roles/personas), #95578 (delegate anti-propagation prompt guidance), and #92001 (explicit profile-home ownership for delegation persistence). In particular, if #95578 lands first, the profile-identity prompt path must not replace/drop its security guidance; if #92001 lands first, new child persistence must reuse its explicit-home boundary rather than recreate ambient storage selection.

P1 landing gate — this exact object has no hosted acceptance and is far off current main

Current topology at review time:

  • PR head: 4c37a70d976c8b2a9144fd6e412f1fc791c1214c
  • current main: 4f22543509d1b91dc45bcb369447126c5eb14fb7
  • merge base: 1e21fe862421081e14af4564076af74bc58c050b
  • branch: 9 ahead / 154 behind, mergeable=false
  • exact head: 0 workflow runs, 0 check runs, 0 commit statuses

The local targeted tests are useful evidence, but they cannot substitute for hosted acceptance of the submitted object—especially after the branch has accreted fork, usage, and session-persistence behavior not reflected in the original test-plan summary. Recompose on current main, then reacquire exact-head CI/Docker/Nix (and every submitted commit must be green, not only a merge ref/head snapshot).

The size/ownership gate also still fails: this PR edits tools/delegate_tool.py past ~5.5K lines, cli.py around ~14K, and agent/agent_init.py past ~2.9K. New routing/identity/persistence authority should move behind bounded owners rather than add another concern to those godfiles. tools/delegation_fork.py is the right shape of extraction; it just already belongs to #91252's lineage.

Separate the session-title side channel from this feature

The late session-title commits are also orthogonal to per-task model/profile identity and currently write sessions.title with raw sqlite3 from delegate_tool.py, including a fallback that can synthesize a profile DB path and finally falls back to /home/hermeswebui/.hermes/profiles/io/state.db.

That creates a second persistence authority beside SessionDB exactly while session/profile ownership is being hardened elsewhere (#92001 and the broader session-lifecycle work). The pre-run UPDATE is also necessarily best-effort because the row may not exist yet, then _finalize_child_results repeats the write.

I would remove that ride-along from this PR and give descriptive subagent titles their own canonical session-owner change, or route it through the existing session title API with real lifecycle/profile-home tests. It should not be coupled to whether profile identity lands.

What I would re-review

A much cleaner next object would be: current-main rebase; fork and usage work removed/recomposed onto #91252/#91271 with original credit; one owner selected for the #97653 model-routing slice; all task routes/profile identities preflighted before any child construction; profile provider actually honored; canonical profile-name validation; bounded module ownership; and fresh exact-object hosted CI for every commit.

The core product idea is useful, and the conservative defaults plus the resolver hardening are solid. The branch now needs the same care at the composition boundary that it already applies inside the resolver. Once those ownership and atomicity seams are closed, this will be much easier to reason about and much safer to merge.

@CaptainPickard

Copy link
Copy Markdown
Author

Thanks for the thorough review. The bug findings are well-taken and we agree with the composition direction. Here is our plan.

What we are doing: full rebase + recompose

We will rebase on current main (4f22543) and slim the branch down to only the profile-identity delta. The fork, usage-ledger, and session-title commits will be removed from this PR.

Commits being dropped from this PR

Commits staying (the profile-identity delta)

  • f43cdce6c0 per-task model selection and profile identity
  • 52c0861e9f profile_name path traversal guard (will be replaced with canonical validate_profile_name — see P1 Architecture planning #3 below)
  • 3d535fd8c0 docs convergence
  • 51ea414ba4 test gap closure (will be updated for the new code paths)

Fixes for each P1

P1 #1 — atomic batch preflight

Agreed. The current loop constructs children inline with resolution, so a later failure orphans earlier children that already have open SessionDB handles and _active_children registrations.

Fix: Two-pass design. Pass 1 resolves every task's effective model, provider, profile identity, allowlist status, and credential bundle into an immutable plan list. If any task fails resolution, return a bounded tool error with zero child construction. Pass 2 iterates the pre-resolved plans and constructs children.

We will add a regression test where task 0 is valid and task 1 has an invalid profile, then assert _active_children is unchanged and no child DB handle was opened.

We will also review #98128 (DavidMetcalfe) and coordinate so we do not land conflicting routing owners in delegate_tool.py. If #98128 lands first, we will rebase onto its preflight boundary rather than reinventing one.

P1 #2 — profile provider loaded but never applied

Confirmed bug. _load_profile_identity() reads model.provider but the dispatch path only consumes model, then _resolve_task_model_creds() anchors on base_creds["provider"] (the parent/delegation route) instead of the profile's route.

Fix: Resolve a selected profile's provider+model as one routing unit through the canonical resolver. Precedence: explicit task override > profile config > delegation config > parent. The preflight pass will produce the final (provider, model, base_url, api_key, transport fields) tuple per task, not just the model string.

Regression test: parent/delegation provider A, profile configured for provider B, no explicit task override. Assert the child receives provider B's endpoint, credentials, and transport fields, not A's.

P1 #3 — ENAMETOOLONG filesystem exception

Agreed that name-to-filesystem-path validation belongs to this boundary since this PR introduces the model-facing profile name input.

Fix: Replace the ad-hoc regex guard with Hermes' canonical normalize_profile_name / validate_profile_name (the path #90331 uses). The validation runs in the preflight pass before any disk probe. Overlong, hostile, or invalid names return a bounded tool error and construct nothing.

Regression test: delegate with an overlong alphanumeric profile name, assert bounded error, zero child construction.

P1 #4 — provenance/ownership

Acknowledged. The fork slice is #91252's work (teknium1, porting kimi-code#3007). The usage ledger is #91271's work (teknium1, Copilot CLI port). We are dropping both from this PR (see above).

For the #97653/#98128 model-routing overlap (DavidMetcalfe): #97653 is the feature issue, #98128 is the PR. We will coordinate on a single routing owner rather than landing parallel implementations. If #98128 lands first, we rebase onto it. If ours lands first, we will ensure the preflight boundary is compatible.

Adjacent edges we will respect during rebase:

P1 #5 — landing gate

Agreed. After the rebase, we will push the exact head and verify hosted CI/Docker/Nix checks run green on every commit, not just the head snapshot. The branch will be current-main-based with a clean mergeable state before requesting re-review.

On the godfile concern: we agree delegate_tool.py is too large. tools/delegation_fork.py was the right extraction shape, but it belongs to #91252's lineage. For this PR's scope, we will keep the profile-identity logic behind bounded helpers and avoid growing delegate_tool.py further where possible. A broader extraction of routing authority into its own module is worth doing, but we will scope that as a follow-up rather than expanding this PR.

Timeline

  1. Rebase on current main (drop fork, usage, session-title commits).
  2. Implement atomic two-pass preflight (P1 Terminal tool #1).
  3. Fix profile provider routing (P1 Support passing morph snapshot id #2).
  4. Replace ad-hoc name validation with canonical path (P1 Architecture planning #3).
  5. Update and add regression tests for all three fixes.
  6. Force-push the clean branch, verify hosted CI green on every commit.
  7. Request re-review.

Appreciate the review. The core resolver hardening and default-off schema will carry forward into the clean object.

…elegate_task

Two opt-in features for delegate_task, both gated behind config flags
(default off) to preserve the existing subagent contract:

1. Per-task model selection (delegation.allow_model_selection):
   Each task in a batch can name a model ('opus', 'gpt-5', 'glm', or a
   full 'vendor/model' slug). Resolution reuses the existing model_switch
   pipeline (same as the /model command) so names are matched leniently
   and the provider is resolved, not dictated. Unresolvable names return
   a clear per-task error instead of silently falling back.

2. Per-task profile identity (delegation.allow_profile_identity):
   Each task can name a Hermes profile. The child loads that profile's
   SOUL.md, IDENTITY.md, and AGENTS.md as its system prompt identity,
   and reads model/provider from its config.yaml when not explicitly
   overridden. The child becomes the named bot rather than a generic
   subagent. Useful for multi-role workflows (code reviewer, test
   validator, security auditor) in a single delegate_task batch.

Both schema fields only appear in the tool definition when the
corresponding flag is enabled, keeping the tool surface minimal when off.
The flag-off code path is byte-identical to prior behavior.
Validate profile_name against ^[A-Za-z0-9_-]+$ before path joining to
prevent traversal attacks (e.g. ../../.ssh) from escaping the profiles
root. Also clarify in docs that a profile's config.yaml model fallback
requires allow_model_selection to be enabled.

Addresses AI code review feedback on PR NousResearch#98031.
…odel

Add Kilo-Org/kilocode#11786 attribution to _get_allow_model_selection()
and _resolve_task_model_creds() docstrings, noting our two bugfixes
(provider anchoring, stale ACP clearing) over the upstream version.

Create tests/tools/test_delegate_model_selection.py mirroring the
kilocode branch test file name/structure with integration-style tests
using real switch_model() calls (9 tests).

Add convergence note to delegation.md documenting the shared origin
with the kilocode-port branch.

Both implementations use the same config key (allow_model_selection),
resolver function (_resolve_task_model_creds), and switch_model()
pipeline. This convergence prevents perpetual merge conflicts on
future upstream merges. Our profile-identity half remains the unique
differentiator with no upstream equivalent.
… integration tests

Add tests/tools/test_delegate_test_gap.py with 26 tests + 48 subtests
across 4 categories closing the audit-identified test gaps:

1. Property-based path traversal (Hypothesis): Fuzz
   _load_profile_identity with arbitrary strings, unicode, null bytes,
   control chars, path separators across platforms, and extremely long
   names. Verifies the ^[A-Za-z0-9_-]+$ guard rejects all adversarial
   inputs before filesystem access.

2. allow_model_selection=false gating regression: Verifies that when
   model selection is disabled but profile identity is enabled, the
   schema advertises profile but NOT model. Tests flag getter isolation
   and profile-model suppression at the dispatch level.

3. Fallback chain semantics: Verifies fail-loudly contract: unresolvable
   models raise ValueError (no silent fallback), None/empty/whitespace
   model names return base creds unchanged, base creds not mutated on
   resolution failure, error message surfaced in ValueError.

4. Integration tests with mock child spawns: Exercises delegate_task
   end-to-end with mocked AIAgent to verify profile identity appears in
   child system prompt, generic preamble used without profile, fork
   snapshot attached when fork=True, explicit model takes precedence
   over profile model, and usage ledger populated after delegation.

Discovered follow-up bug (out of scope, test-only card):
_load_profile_identity raises OSError ENAMETOOLONG for valid-regex
names >255 bytes because profile_path.is_dir() is not exception-wrapped.
Recommend a separate one-line fix card.

No production code changes. All 156 existing tests still pass.
…ation, profile propagation

P1 fixes from PR NousResearch#98031 review:

1. Atomic two-pass batch preflight: resolve all tasks' model/profile/creds
   into immutable plans BEFORE constructing any child. A mid-batch failure
   returns tool_error with zero children constructed, preventing orphaned
   SessionDB handles and _active_children registrations.

2. Profile provider routing: new _resolve_profile_model_creds() resolves
   a profile's model+provider as one routing unit against the PROFILE's
   provider, not the parent's. Precedence: explicit task model > profile
   config (model+provider) > delegation config > parent inherit.

3. ENAMETOOLONG guard: new _validate_profile_name() checks regex + length
   (<=255) before any filesystem access. try/except OSError around
   profile_path.is_dir() as defense-in-depth.

4. _profile_name added to _load_profile_identity() return dict.
   child._delegate_profile set in _build_child_agent from the identity dict.

Regression tests:
- TestAtomicBatchPreflight: task 0 valid + task 1 invalid = zero children
- TestProfileProviderRouting: profile provider used, not parent provider
- TestOverlongProfileName: 256-char name returns None, no OSError
- All 3 pre-existing test failures fixed (54/54 pass)
@CaptainPickard
CaptainPickard force-pushed the feat/per-task-model-and-profile-identity branch from 4c37a70 to 7d1ad50 Compare August 30, 2026 20:31
@CaptainPickard

Copy link
Copy Markdown
Author

Updated branch pushed. Summary of changes since the review:

Branch recomposed

P1 fixes applied (commit 7d1ad50)

P1 #1 -- atomic preflight: The dispatch loop is now two-pass. Pass 1 resolves every task's role, model, profile identity, profile model fallback, and credential bundle into immutable per-task plan dicts. Any resolution failure returns tool_error with zero children constructed. Pass 2 iterates the pre-resolved plans and constructs children -- no model resolution or profile loading happens in this pass. Regression test: task 0 valid + task 1 invalid profile -> error names task 1, _build_child_preserving_parent_tools called 0 times, _active_children unchanged.

P1 #2 -- profile provider routing: New _resolve_profile_model_creds() resolves a profile's model+provider as one routing unit through resolve_runtime_provider, anchored on the profile's configured provider (not the parent's). The profile's base_url, api_key, api_mode, and request_overrides all come from the profile's provider. Stale ACP/pin fields are cleared on cross-provider switches. Precedence: explicit task model > profile config (model+provider) > delegation config > parent. Regression test: parent on provider A, profile configured for provider B -> child receives provider B's endpoint and credentials.

P1 #3 -- ENAMETOOLONG guard: New _validate_profile_name() checks ^[A-Za-z0-9_-]+$ regex and length <= 255 before any filesystem access. profile_path.is_dir() is wrapped in try/except OSError as defense-in-depth. Regression test: 256-char alphanumeric name returns None, no OSError.

P1 #4 -- profile name propagation: _profile_name added to _load_profile_identity() return dict. child._delegate_profile is set in _build_child_agent from the identity dict's validated name.

Ownership

Test results

54 passed, 0 failures across test_delegate_per_task_overrides.py, test_delegate_model_selection.py, test_delegate_test_gap.py. 4 new regression tests added. Import check passes. No fork/usage/session-title references in the P1 commit.

CI is pending on the new head. Will confirm once checks complete.

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

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have tool/delegate Subagent delegation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants