Skip to content

feat(#6966): add fullsend agent new to generate a complete custom agent - #6972

Open
waynesun09 wants to merge 17 commits into
mainfrom
agent-6966-agent-new
Open

feat(#6966): add fullsend agent new to generate a complete custom agent#6972
waynesun09 wants to merge 17 commits into
mainfrom
agent-6966-agent-new

Conversation

@waynesun09

@waynesun09 waynesun09 commented Sep 3, 2026

Copy link
Copy Markdown
Member

What

Adds fullsend agent new <name> — generates a complete, valid, runnable custom agent from a name and a role, registers it, and validates the result before returning.

fullsend agent new lint-docs --fullsend-dir .fullsend \
  --role triage --description "Check docs changes for broken links"

Writes harness/, agents/, schemas/ and scripts/post-*.sh for the agent, plus the policies/base.yaml, providers/ and profiles/ a per-repo install does not vendor (written only when absent, never overwritten — not even with --force).

Why

Building a custom agent by hand is the most common piece of negative user feedback. Three of the failure modes are silent:

  • A harness with no trigger: is silently undispatchable. It registers, validates, and shows up in agent list, then ListTriggeredHarnesses skips it with a bare continue and no annotation — while resolve and load failures both emit ::error::. None of the seven fleet harnesses has a trigger: to copy from. agent new refuses to write one without a trigger.
  • A role: the mint does not serve surfaces as an opaque 403 at first dispatch (Mint service returns opaque 403 for unregistered BYO agent roles #6563). --role is a closed five-row table with a drift test against mintcore.RolePermissionsFor.
  • A missing policies/base.yaml or provider file fails at run time, or degrades to a warning and a sandbox that cannot reach Vertex (v0.37 scaffold should generate a default policies/base.yaml for sandbox-enabled agents #6834). The generator writes them and stats them.

gh aw new is the precedent. #4839 recorded a July "prefer guides over a CLI" decision — taken in Slack, never written up as an ADR — which is why ADR 0102 is a new ADR rather than a superseding one.

Also fixes a live documentation bug

docs/guides/user/cel-triggers-reference.md gave this as the canonical slash-command trigger:

&& event.state.change_proposal != null
&& !event.state.change_proposal.is_fork

state.change_proposal is absent on a comment posted to a plain issue, not present-and-null — the NormalizedEvent schema requires only labels under state, and the jira-fs-triage-comment / discussion-fs-vouch-comment fixtures confirm it. Comparing an absent key against null raises a missing-key error, and MatchHarnesses reports that as ::error:: harness dispatch: skipping agent <name>: trigger eval failed on every issue comment in the repository. Anyone who copied that pattern has an agent that looks permanently broken.

Corrected to !has(event.state.change_proposal) || !event.state.change_proposal.is_fork. The doc block is now byte-identical to what --on command: emits, and TestCommandPresetMatchesTheReferenceDoc asserts they stay that way.

The BYO guide also claimed twice that github setup creates policies/, providers/ and profiles/. It creates none of them — that sentence is what walks authors into #6834.

How to test

mkdir -p /tmp/demo/.fullsend && cd /tmp/demo
git init -q . && git remote add origin git@github.com:my-org/my-repo.git
printf 'version: "1"\nroles: [triage]\n' > .fullsend/config.yaml

go run ./cmd/fullsend agent new lint-docs --fullsend-dir .fullsend \
  --role triage --description "Check docs changes for broken links"
go run ./cmd/fullsend lock lint-docs --fullsend-dir .fullsend --offline

Validated commands

Every command in the documentation was executed against a real temporary --fullsend-dir and the pasted output is what it produced.

# Command Executed Output in docs
1 agent new lint-docs --role triage --description ... yes created-file list, registration line, next steps
2 find .fullsend -type f | sort yes the generated tree
3 agent new -f link-check.agent.yaml yes same shape, with shared assets reported as already present
4 agent new report --dry-run yes planned file list + rendered bodies
5 agent new report --role scribe yes the unknown-role error with the full table
6 lock lint-docs --offline yes loader output — the "did I build a valid agent" check
7 agent list yes NAME/SOURCE table, with the caveat that it does not validate
8 agent set lint-docs --model sonnet yes the Set agent ... line
9 agent remove link-check yes the removal line
10 agent new 'a;rm -rf /' / bad --trigger / bad --on yes the literal error strings in the troubleshooting table

Documented but not executed here: a full fullsend run of the generated agent, and CI dispatch. Both need GCP credentials, a sandbox image pull and a live forge, and the post-script's comment path mutates a real work item. The BYO guide points at running-agents-locally.md for prerequisites and documents POST_<NAME>_DRY_RUN=1 so the post-script prints instead of posting; no success block is fabricated for either.

Gates

  • go test ./internal/agentnew/... ./internal/cli/... ./internal/harness/... ./internal/config/... — green
  • Patch coverage 87.9% (452/514 statements), measured with plain go test -cover per package — threshold is 80%, and CI is the authority. An earlier revision of this body claimed 88.2%; that figure came from go test -coverpkg, which credits cross-package calls in a way Codecov does not, and the first CI run scored the patch at 72.23%. The tests for harness.CheckGenerated now live in package harness where Codecov can see them.
  • make lint clean, including the ADR and markdown-link checks
  • make e2e-test not run — it needs live GitHub pool orgs

Known gap

Generate rolls back the files it wrote if the write loop itself fails, and a
name already in config.yaml is now refused before anything is written. But if
runAgentAdd fails for some other reason after the files have landed — an
unwritable config.yaml, or a cfg.Validate() failure from unrelated existing
content — the generated files stay on disk and the command exits with
agent files were written but registration failed: ....

That message is accurate rather than misleading, and re-running with --force
recovers, so this is a follow-up rather than a blocker. Closing it properly
means either extending the rollback across the registration step or writing the
config first and the files second; both are a larger change to the ordering than
belongs in this PR. Flagged by Qodo #1's request for transaction semantics and
by the review agent's partial transcript.

Notes for review

  • The ADR is bundled per CONTRIBUTING.md's allowance for human-submitted PRs. Happy to split it out on request.
  • ADR 0058 carries a new note. Accepted ADRs are point-in-time records, so its Context, Decision and Consequences are untouched; the addition is a ## Notes entry recording that agent new sits in front of the registration model 0058 describes, which is the kind of forward cross-reference CONTRIBUTING.md explicitly allows.
  • docs/architecture.md gains the matching entry in the agent-registration Decided: list.
  • ADR number 0102 will drift/renumber-adr before merge.
  • lock and run are deliberately not rewired onto the new harness.CheckGenerated helper. They interleave minting, runner-env validation and ${VAR} expansion between the same steps in different orders — run expands paths so they resolve before ValidateFilesExist stats them, and lock never calls ValidateFilesExist at all. Whether lock should gain it is a real question, but a behaviour change on those paths does not belong in a generator PR.
  • ValidateRunnerEnvWith is intentionally not part of the generation-time check, and says so in the helper's doc comment: it requires every ${VAR} to be set in the calling process, which is true in CI and false on a developer's machine. The run-time error is in the troubleshooting table.
  • The two sandbox image digests are compiled in and repinned by hand on the fleet's cadence; a golden test makes a repin visible in review.

Closes #6966

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Generate complete custom agents with fullsend agent new

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds fullsend agent new to generate, validate, and register complete custom agents.
• Embeds role-aware scaffolds, safe trigger presets, specs, dry runs, and collision handling.
• Fixes slash-command CEL fork guards and documents the generated workflow.
Diagram

graph TD
  A["agent new CLI"] --> B["Option resolver"] --> C["Agent renderer"] --> D["Scratch tree"] --> E["Harness validator"] --> F["Generated files"] --> G["Agent registry"]
  C --> H["Embedded assets"] --> D
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Documentation-only onboarding
  • ➕ Avoids maintaining generator templates in the CLI
  • ➕ Leaves harness creation fully flexible
  • ➖ Preserves silent trigger, role, and resource failures
  • ➖ Requires users to hand-author substantial plumbing
2. Fetch templates from the agents repository
  • ➕ Keeps templates aligned with fleet release cadence
  • ➕ Allows template evolution without releasing the CLI
  • ➖ Makes generation network-dependent
  • ➖ Introduces remote-resource and cross-repository version coordination risks

Recommendation: Keep the PR's embedded-template approach for the initial implementation. It provides deterministic, offline generation and permits validation before writes; drift is appropriately mitigated through real harness structs, permission checks, fixture tests, and golden outputs. A pinned remote-template override could be considered later without replacing the reliable default.

Files changed (44) +5364 / -13

Enhancement (14) +1532 / -1
assets.goCollect shared policy and provider assets +55/-0

Collect shared policy and provider assets

• Loads the base policy and role-specific provider/profile files from embedded resources. Marks shared assets as create-only so '--force' cannot overwrite repository customizations.

internal/agentnew/assets.go

generate.goImplement validated, collision-safe file generation +230/-0

Implement validated, collision-safe file generation

• Renders and validates a complete temporary tree before writing, supports dry runs and controlled overwrites, and preserves shared assets. It also derives harness slugs from Git origin owners.

internal/agentnew/generate.go

options.goDefine and validate resolved generator options +72/-0

Define and validate resolved generator options

• Introduces defaults and validates names, roles, mandatory CEL triggers, models, effort, slugs, timeouts, and images before filesystem access.

internal/agentnew/options.go

render.goRender role-aware custom-agent files +237/-0

Render role-aware custom-agent files

• Builds a real harness structure and renders the prompt, JSON schema, executable post-script, and shared assets. Optional validation-loop wiring is emitted as a consistent unit.

internal/agentnew/render.go

roles.goDefine supported hosted-mint role mappings +147/-0

Define supported hosted-mint role mappings

• Adds a closed five-role table mapping permissions, sandbox images, providers, and profiles. Actionable help rejects unsupported or infrastructure-only roles before dispatch.

internal/agentnew/roles.go

spec.goParse strict versioned agent specifications +77/-0

Parse strict versioned agent specifications

• Adds single-document YAML specs mirroring CLI flags, with unknown-field rejection, version validation, and mutually exclusive trigger forms.

internal/agentnew/spec.go

agent-body.md.tmplAdd editable custom-agent prompt template +44/-0

Add editable custom-agent prompt template

• Provides marked prompt sections, inputs, analysis steps, a constrained JSON output contract, and side-effect boundaries.

internal/agentnew/templates/agent-body.md.tmpl

post.sh.tmplAdd secure sticky-comment post-script template +85/-0

Add secure sticky-comment post-script template

• Validates untrusted model output, bounds content, validates GitHub URLs, supports post-script dry runs, and posts findings through the sticky-comment primitive.

internal/agentnew/templates/post.sh.tmpl

validate-output-schema.shAdd optional output-schema validator +70/-0

Add optional output-schema validator

• Validates generated output JSON against the configured schema using Python and reports actionable validation failures.

internal/agentnew/templates/scripts/validate-output-schema.sh

trigger.goAdd safe CEL trigger presets +116/-0

Add safe CEL trigger presets

• Expands command, label, issue-opened, and pull-request presets into validated CEL. The default command preset handles absent change-proposal state and rejects fork pull requests.

internal/agentnew/trigger.go

agent.goRegister the 'agent new' subcommand +2/-1

Register the 'agent new' subcommand

• Adds generation to the agent command description and command hierarchy.

internal/cli/agent.go

agent_new.goImplement the 'fullsend agent new' workflow +303/-0

Implement the 'fullsend agent new' workflow

• Adds Cobra flags, spec/flag merging, defaults, trigger and slug resolution, generation output, dry-run behavior, registration, runtime overrides, and next-step guidance.

internal/cli/agent_new.go

checkgenerated.goAdd generated-harness validation pipeline +80/-0

Add generated-harness validation pipeline

• Introduces generation-specific linting, path resolution, owned-file checks, and explicit provider/profile existence checks without requiring developer environment variables.

internal/harness/checkgenerated.go

harness.goExport agent-name and slug validators +14/-0

Export agent-name and slug validators

• Exposes existing harness validation patterns so generators can reject unsafe names and invalid slugs before writing files.

internal/harness/harness.go

Bug fix (1) +14 / -4
cel-triggers-reference.mdFix slash-command triggers for issue comments +14/-4

Fix slash-command triggers for issue comments

• Replaces the unsafe absent-field comparison with a 'has()' guard, allowing issue comments while still rejecting fork pull requests. Explains the prior missing-key dispatch failure.

docs/guides/user/cel-triggers-reference.md

Tests (18) +3321 / -1
checkgenerated_test.goTest complete generated-tree validation +115/-0

Test complete generated-tree validation

• Verifies fresh trees for every role and confirms missing policies, prompts, scripts, schemas, providers, or profiles are detected.

internal/agentnew/checkgenerated_test.go

generate_slug_test.goTest repository-owner and slug derivation +83/-0

Test repository-owner and slug derivation

• Covers HTTPS, SSH, and SCP-style remotes, invalid owners, upward Git-directory discovery, and fallback slugs.

internal/agentnew/generate_slug_test.go

golden_test.goPin generated agent trees with golden tests +135/-0

Pin generated agent trees with golden tests

• Adds stable whole-tree snapshots for role, trigger, and validation-loop variants, including file modes and shared-asset metadata.

internal/agentnew/golden_test.go

options_test.goTest generator option validation +85/-0

Test generator option validation

• Exercises accepted roles and optional values plus unsafe names, unsupported roles, invalid triggers, and malformed runtime settings.

internal/agentnew/options_test.go

render_test.goVerify rendered harnesses and assets +268/-0

Verify rendered harnesses and assets

• Ensures generated trees load for every role, avoid deprecated shapes, safely marshal descriptions, produce valid schemas and scripts, and correctly classify shared files.

internal/agentnew/render_test.go

roles_test.goGuard role mappings against mint drift +128/-0

Guard role mappings against mint drift

• Checks role permissions against mintcore, validates provider/profile pairing, pins intentional exclusions, and verifies special coder and retro resources.

internal/agentnew/roles_test.go

spec_test.goTest agent specification parsing +82/-0

Test agent specification parsing

• Covers complete specs, file loading, unsupported versions, unknown keys, missing names, conflicting triggers, negative timeouts, and multiple documents.

internal/agentnew/spec_test.go

coder.txtPin coder-role generated output +231/-0

Pin coder-role generated output

• Captures the exact coder harness, prompt, schema, post-script, code image, and writable GitHub provider selection.

internal/agentnew/testdata/golden/coder.txt

label-trigger.txtPin label-trigger generated output +230/-0

Pin label-trigger generated output

• Captures the generated tree when a label-added CEL trigger is selected.

internal/agentnew/testdata/golden/label-trigger.txt

prioritize.txtPin prioritize-role generated output +231/-0

Pin prioritize-role generated output

• Captures the exact prioritize harness and its sandbox, provider, profile, and output resources.

internal/agentnew/testdata/golden/prioritize.txt

retro.txtPin retro-role generated output +237/-0

Pin retro-role generated output

• Captures the retro harness including its additional GitHub Actions artifact provider and profile.

internal/agentnew/testdata/golden/retro.txt

review.txtPin review-role generated output +231/-0

Pin review-role generated output

• Captures the exact review harness, code image, read-only GitHub provider, and common generated files.

internal/agentnew/testdata/golden/review.txt

triage.txtPin default triage generated output +231/-0

Pin default triage generated output

• Captures the default command-triggered triage agent tree and all owned and shared file metadata.

internal/agentnew/testdata/golden/triage.txt

validation-loop.txtPin validation-loop generated output +237/-0

Pin validation-loop generated output

• Captures the optional validation-loop harness block and shared schema-validation script alongside the standard tree.

internal/agentnew/testdata/golden/validation-loop.txt

trigger_fixtures_test.goEvaluate trigger presets against normative events +239/-0

Evaluate trigger presets against normative events

• Tests presets against real normalized-event fixtures, including issue comments and fork variants, and locks the command preset to the reference documentation.

internal/agentnew/trigger_fixtures_test.go

trigger_test.goPin trigger expansions and resource availability +148/-0

Pin trigger expansions and resource availability

• Checks exact preset text, CEL compilation, invalid arguments, actionable errors, and the availability of all role resources in the embedded scaffold.

internal/agentnew/trigger_test.go

agent_new_test.goTest the agent generation command end to end +407/-0

Test the agent generation command end to end

• Covers generation and registration, executable scripts, dry runs, collisions, force protection, no-register mode, runtime settings, spec precedence, and option resolution.

internal/cli/agent_new_test.go

agent_test.goExpect the new agent subcommand +3/-1

Expect the new agent subcommand

• Updates command-tree assertions to include 'new' and explicitly retain the existing 'set' command.

internal/cli/agent_test.go

Documentation (9) +468 / -7
0058-agent-registration.mdRelate generation to existing registration architecture +8/-0

Relate generation to existing registration architecture

• Clarifies that 'agent new' generates files before delegating registration to the existing 'agent add' model.

docs/ADRs/0058-agent-registration.md

0102-generate-custom-agents-from-the-cli.mdRecord the custom-agent generator decision +130/-0

Record the custom-agent generator decision

• Documents the motivation, alternatives, embedded-template strategy, mandatory triggers, closed role table, shared assets, and pre-write validation guarantees.

docs/ADRs/0102-generate-custom-agents-from-the-cli.md

agent.mdDocument the complete 'agent new' command +274/-1

Document the complete 'agent new' command

• Adds usage, generated layout, flags, roles, triggers, spec files, validation behavior, execution guidance, and troubleshooting for the new command.

docs/cli/agent.md

cli-internals.mdAdd 'agent new' to the CLI command tree +10/-1

Add 'agent new' to the CLI command tree

• Documents the new command and its principal generation, trigger, validation, overwrite, and registration flags.

docs/guides/dev/cli-internals.md

operations.mdAdd agent generation to developer operations +1/-0

Add agent generation to developer operations

• Lists 'fullsend agent new' as the developer command for creating and registering custom agents.

docs/guides/getting-started/operations.md

bring-your-own-agent.mdMake CLI generation the recommended starting point +36/-2

Make CLI generation the recommended starting point

• Adds a generation-first workflow and corrects claims about files vendored by per-repository setup. It also documents missing-trigger and unsupported-role failure modes.

docs/guides/user/bring-your-own-agent.md

building-custom-agents.mdRecommend generation over hand-written scaffolding +5/-3

Recommend generation over hand-written scaffolding

• Redirects new custom-agent authors to 'fullsend agent new' while retaining the guide for understanding existing manual agents.

docs/guides/user/building-custom-agents.md

customizing-agents.mdLink customization guidance to agent generation +2/-0

Link customization guidance to agent generation

• Directs users creating a new agent to the new generator before discussing existing-agent customization.

docs/guides/user/customizing-agents.md

customizing-with-skills.mdLink generated agents to skills guidance +2/-0

Link generated agents to skills guidance

• Adds 'fullsend agent new' as the starting point for agents that will mount custom skills.

docs/guides/user/customizing-with-skills.md

Other (2) +29 / -0
base.yamlEmbed a safe base sandbox policy +22/-0

Embed a safe base sandbox policy

• Defines shared filesystem, Landlock, and process restrictions for generated agents while leaving network access to provider profiles.

internal/agentnew/templates/policies/base.yaml

config.goDefine default generated sandbox image pins +7/-0

Define default generated sandbox image pins

• Adds separate sandbox and code image digests used by role-aware generated harnesses and overridable through '--image'.

internal/config/config.go

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 4:33 PM UTC · Ended 4:36 PM UTC

Commit: a86869c · View workflow run →

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Site preview

Preview: https://fe7fa397-site.fullsend-ai.workers.dev

Commit: c73568c4d5f9ac170058c47c54b68b36270930b5

@qodo-code-review

qodo-code-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Registration checked after overwrite ✓ Resolved 🐞 Bug ☼ Reliability
Description
runAgentNew writes or force-overwrites all generated files before runAgentAdd checks whether the
agent is already registered. For an existing registration, the command can alter the live agent and
then fail with “agent already exists in config,” leaving changed files despite unsuccessful
completion.
Code

internal/cli/agent_new.go[R251-253]

+		if err := runAgentAdd(ctx, result.HarnessPath, opts.Name, f.fullsendDir, nil, printer); err != nil {
+			return fmt.Errorf("agent files were written but registration failed: %w", err)
+		}
Relevance

●●● Strong

Recent history accepts fixes preventing partial side effects and improving failure cleanup.

PR-#6874
PR-#6036

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
agentnew.Generate is called first and directly writes permitted destinations. runAgentAdd is
invoked afterward, while its duplicate-name check rejects an existing config entry before updating
config and cannot undo the earlier file writes.

internal/cli/agent_new.go[206-258]
internal/agentnew/generate.go[51-99]
internal/cli/agent.go[280-300]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Agent registration conflicts are detected only after generated files have already been created or overwritten.

## Issue Context
Perform all registration/config validation before generation, or make file installation and registration one rollback-capable transaction. Preserve `--no-register` behavior.

## Fix Focus Areas
- internal/cli/agent_new.go[206-258]
- internal/agentnew/generate.go[51-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Architecture omits ADR 0102 ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The PR adds ADR 0102 with Accepted status but does not update docs/architecture.md with a
corresponding Decided: entry and link. The architecture overview therefore does not reflect the
newly accepted CLI generation decision.
Code

docs/ADRs/0102-generate-custom-agents-from-the-cli.md[R2-3]

+title: "102. Generate custom agents from the CLI"
+status: Accepted
Relevance

●●● Strong

Recent documentation reviews accepted adding or restoring architecture cross-references and overview
links.

PR-#6329
PR-#2392

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
ADR 0102 declares status: Accepted, while the Agent Harness decisions in docs/architecture.md
contain no entry or link for ADR 0102. Compliance rule 1062100 requires every accepted ADR to be
accompanied by such an architecture update.

docs/ADRs/0102-generate-custom-agents-from-the-cli.md[1-18]
docs/architecture.md[93-136]
Skill: writing-adrs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ADR 0102 is introduced as Accepted without the required update to `docs/architecture.md`.

## Issue Context
Add a short `Decided:` entry under the relevant Agent Harness or Agent Registry component that summarizes custom-agent generation and links to ADR 0102. Keep the living-document change surgical.

## Fix Focus Areas
- docs/ADRs/0102-generate-custom-agents-from-the-cli.md[1-18]
- docs/architecture.md[93-136]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Procedure precedes prerequisites ✓ Resolved 📜 Skill insight ✧ Quality
Description
The guide introduces Step 0 and its command before the Before you begin prerequisites section at
line 56. Readers are consequently instructed to run generation before the guide states the required
CLI, repository setup, inference, and GitHub App prerequisites.
Code

docs/guides/user/bring-your-own-agent.md[R17-20]

+### Step 0: generate the skeleton
+
+Start here. [`fullsend agent new`](../../cli/agent.md#agent-new) writes a
+complete, valid, registered agent from a name and a role, so you edit prose
Relevance

●●● Strong

Recent guide reviews accepted reordering detailed procedures after prerequisite and setup context.

PR-#5201

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added procedure starts under Step 0 at line 17, whereas the guide's prerequisite-equivalent
Before you begin section does not appear until line 56. Rule 1062078 requires prerequisites before
procedural steps.

docs/guides/user/bring-your-own-agent.md[17-26]
docs/guides/user/bring-your-own-agent.md[56-61]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added generation procedure appears before the guide's prerequisites.

## Issue Context
Move or duplicate the clearly labeled prerequisite section so all requirements appear before the first procedural step, including the new `agent new` workflow.

## Fix Focus Areas
- docs/guides/user/bring-your-own-agent.md[17-61]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (2)
4. Shared assets bypass validation ✓ Resolved 🐞 Bug ≡ Correctness
Description
When an existing shared asset cannot be read, validateInTempTree silently substitutes the embedded
default and validates that instead. For example, an existing directory at policies/base.yaml is
skipped during installation, but validation succeeds against the template even though the generated
agent cannot use the actual path.
Code

internal/agentnew/generate.go[R120-122]

+			if existing, readErr := os.ReadFile(filepath.Join(absDir, f.Path)); readErr == nil {
+				data = existing
+			}
Relevance

●● Moderate

Validation mismatch is a plausible correctness issue, but no closely matching historical precedent
was found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Every existing shared path is marked skipped without checking its type. During temporary validation,
its contents replace the template only when os.ReadFile succeeds; all read errors are ignored,
leaving the embedded bytes in the scratch tree and making validation inspect different content from
the target installation.

internal/agentnew/generate.go[58-66]
internal/agentnew/generate.go[117-130]
internal/agentnew/assets.go[20-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Unreadable or non-regular existing shared assets are silently replaced only in the validation tree, allowing generation to succeed against files different from those used at runtime.

## Issue Context
Shared paths are always skipped when they exist. Failure to read their real contents must be returned rather than falling back to embedded bytes.

## Fix Focus Areas
- internal/agentnew/generate.go[58-66]
- internal/agentnew/generate.go[117-130]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Symlinks escape destination directory ✓ Resolved 🐞 Bug ⛨ Security
Description
Generate follows symlinks while checking and writing generated paths, so a symlinked file or
parent directory beneath .fullsend can cause agent new to overwrite files outside the requested
directory. A repository-controlled .fullsend/harness symlink or a dangling final symlink is
sufficient without --force when the destination appears not to exist.
Code

internal/agentnew/generate.go[R96-97]

+		if err := os.WriteFile(path, f.Data, os.FileMode(f.Mode)); err != nil {
+			return nil, fmt.Errorf("writing %s: %w", f.Path, err)
Relevance

●● Moderate

Symlink-safe installation is a substantive security concern, but repository-specific precedent was
unavailable.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Collision detection uses os.Stat, which follows symlinks and treats a dangling symlink as absent,
permitting the destination when forced or when a broken link appears missing. Installation then uses
os.MkdirAll and os.WriteFile on the joined path; because no component is checked with Lstat or
protected by no-follow semantics, a symlinked parent directory or final target can redirect writes
outside absDir.

internal/agentnew/generate.go[57-66]
internal/agentnew/generate.go[89-97]
internal/agentnew/generate.go[51-66]
internal/agentnew/generate.go[89-99]
internal/agentnew/render.go[73-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Generation can write through symlinks beneath the configured Fullsend directory, allowing generated output to overwrite targets outside that directory. Reject symlinks and ensure writes cannot follow them.

## Issue Context
Collision detection currently uses `os.Stat`, while installation uses `os.MkdirAll` and `os.WriteFile`; these operations follow symlinks, and `os.Stat` incorrectly treats a dangling symlink as a missing destination. Validate every destination component and final target with `Lstat`, or use no-follow file-creation semantics, while preserving intended overwrite behavior only for regular files.

## Fix Focus Areas
- internal/agentnew/generate.go[51-66]
- internal/agentnew/generate.go[89-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Consequences lack required bullets ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0102's Consequences section consists of prose paragraphs instead of 3–5 one-sentence bullet
points. This makes the new ADR structurally noncompliant with the required consequences format.
Code

docs/ADRs/0102-generate-custom-agents-from-the-cli.md[R104-107]

+## Consequences
+
+Creating a working custom agent becomes one command, and the three
+silently-fatal mistakes above become generation-time errors with actionable
Relevance

●●● Strong

ADR structure and section-format compliance are routinely enforced through accepted documentation
findings.

PR-#2743

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new Consequences section begins at line 104 and continues as several prose paragraphs through
line 130; rule 1062091 requires 3–5 bullets, each containing one sentence.

docs/ADRs/0102-generate-custom-agents-from-the-cli.md[104-130]
Skill: writing-adrs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The ADR Consequences section uses multi-sentence prose rather than 3–5 one-sentence bullets.

## Issue Context
Preserve the substantive positive and negative consequences while condensing them into the mandated bullet format.

## Fix Focus Areas
- docs/ADRs/0102-generate-custom-agents-from-the-cli.md[104-130]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. ADR 0058 edit unannounced ✗ Dismissed 📘 Rule violation § Compliance
Description
The PR adds a note to accepted ADR 0058, but the PR description only calls out ADR 0102 and does not
identify or summarize the ADR 0058 edit. Reviewers therefore are not explicitly notified that an
accepted historical ADR is being modified.
Code

docs/ADRs/0058-agent-registration.md[R98-101]

+## Notes
+
+- `fullsend agent new` (added for
+  [#6966](https://github.com/fullsend-ai/fullsend/issues/6966)) generates a
Relevance

●●● Strong

Accepted ADR edits are explicitly reviewed; documenting every accepted-ADR change aligns with recent
precedent.

PR-#5244

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
ADR 0058 is an existing Accepted ADR, and this PR adds a new Notes section describing `fullsend
agent new`. Rule 1062059 requires the PR description to identify and summarize every edit to an
accepted ADR; the supplied description discusses ADR 0102 but does not call out ADR 0058.

Rule 1062059: Call out edits to accepted ADRs in PR descriptions
docs/ADRs/0058-agent-registration.md[1-20]
docs/ADRs/0058-agent-registration.md[97-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The accepted ADR 0058 is modified without an explicit callout in the PR description.

## Issue Context
Update the PR description to identify ADR 0058 by number or filename and briefly state that a note was added connecting `fullsend agent new` to the existing registration decision.

## Fix Focus Areas
- docs/ADRs/0058-agent-registration.md[97-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Malformed trailing YAML ignored ✓ Resolved 🐞 Bug ≡ Correctness
Description
ParseSpec rejects a second YAML document only when decoding it succeeds, but treats every
second-decode error—including malformed trailing YAML or a wrong document type—as EOF. This violates
the exactly-one-document contract and can silently generate an agent from the valid first document
while ignoring corrupted or type-invalid trailing content.
Code

internal/agentnew/spec.go[R59-62]

+	var extra AgentSpec
+	if err := dec.Decode(&extra); err == nil {
+		return nil, fmt.Errorf("spec file must contain exactly one YAML document")
+	}
Relevance

●●● Strong

Deterministic parser error handling violates the exactly-one-document contract; no close rejection
precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
After successfully decoding the first document, the second Decode rejects only err == nil.
Unlike the first decode, it does not distinguish io.EOF from syntax, parsing, or type errors, so
malformed or type-invalid trailing content returns a non-nil error, falls through the condition, and
allows ParseSpec to return the first document successfully.

internal/agentnew/spec.go[47-65]
internal/agentnew/spec.go[64-77]
internal/agentnew/spec.go[51-62]
internal/agentnew/spec.go[64-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Malformed, corrupted, or type-invalid content after the first YAML document is silently accepted because all errors from the second decode are treated like end-of-file, violating the exactly-one-document contract.

## Issue Context
After decoding the first spec, decode once more and accept only `io.EOF`. A nil error means a second document was successfully decoded and must be rejected, while every other error must be returned as a parsing error.

## Fix Focus Areas
- internal/agentnew/spec.go[47-65]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (4)
9. Runtime validated after registration ✓ Resolved 🐞 Bug ≡ Correctness
Description
The requested --runtime is not validated while resolving generation options; it is applied only
after files are written and the agent is registered. An invalid runtime therefore makes the command
fail while leaving a registered agent without the requested setting.
Code

internal/cli/agent_new.go[R254-258]

+		if runtimeName != "" {
+			if err := runAgentSet(f.fullsendDir, opts.Name, agentSetFlags{
+				runtime: runtimeName, runtimeSet: true,
+			}, printer); err != nil {
+				return fmt.Errorf("agent registered but setting the runtime failed: %w", err)
Relevance

●●● Strong

Recent precedent accepted validating or applying runtime settings before completing installation.

PR-#5428
PR-#5953

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
resolveAgentNewOptions carries runtimeName through without validation, while Options.Validate
has no runtime field. After registration, runAgentSet inserts the runtime and calls config
validation, which rejects values outside config.ValidRuntimes.

internal/cli/agent_new.go[100-203]
internal/cli/agent_new.go[251-258]
internal/agentnew/options.go[38-71]
internal/cli/agent.go[203-227]
internal/config/config.go[314-320]
internal/config/config.go[549-550]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Invalid runtime values are discovered only after files and registration have already been committed.

## Issue Context
Validate the resolved flag/spec runtime against the supported runtime set before calling `Generate` or `runAgentAdd`.

## Fix Focus Areas
- internal/cli/agent_new.go[100-203]
- internal/cli/agent_new.go[251-258]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Step 0 uses prose ✓ Resolved 📜 Skill insight ✧ Quality
Description
The newly added generation procedure is presented as a heading followed by narrative prose and an
unnumbered command block rather than an ordered-list step. This violates the guide requirement that
procedural content use numbered steps.
Code

docs/guides/user/bring-your-own-agent.md[R17-20]

+### Step 0: generate the skeleton
+
+Start here. [`fullsend agent new`](../../cli/agent.md#agent-new) writes a
+complete, valid, registered agent from a name and a role, so you edit prose
Relevance

●● Moderate

Numbered-procedure findings are often accepted, but a recent similar formatting request was
rejected.

PR-#2663
PR-#6540

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Lines 17–26 introduce the procedure through a Step 0 heading, prose beginning Start here, and a
command block, without an ordered-list item. Rule 1062079 requires all guide procedures to use
numbered lists rather than prose paragraphs.

docs/guides/user/bring-your-own-agent.md[17-26]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `agent new` procedure is expressed as narrative prose beneath a `Step 0` heading instead of an ordered list.

## Issue Context
Rewrite the procedure as numbered steps with the action or command first, followed by its explanation and expected output.

## Fix Focus Areas
- docs/guides/user/bring-your-own-agent.md[17-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Failed writes leave partial agents ✓ Resolved 🐞 Bug ☼ Reliability
Description
Installation writes files sequentially and returns immediately on the first filesystem error without
restoring overwritten files or removing newly created ones. A failure on a later path therefore
leaves a partially generated agent despite Generate returning an error.
Code

internal/agentnew/generate.go[R94-97]

+		// Written directly rather than renamed in from the temp tree: a
+		// rename across filesystems fails, and the bytes are already here.
+		if err := os.WriteFile(path, f.Data, os.FileMode(f.Mode)); err != nil {
+			return nil, fmt.Errorf("writing %s: %w", f.Path, err)
Relevance

●● Moderate

Rollback semantics are a substantive reliability concern, but no closely matching historical
precedent was found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The final loop calls MkdirAll and WriteFile for each destination and immediately returns on
either error. There is no cleanup or restoration of destinations successfully processed during
previous iterations.

internal/agentnew/generate.go[75-99]
internal/agentnew/render.go[73-84]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Filesystem errors during installation leave earlier generated or overwritten files in place even though generation reports failure.

## Issue Context
Temporary-tree validation protects only the validation phase. The final sequential write loop has no atomic commit, backup, or rollback behavior.

## Fix Focus Areas
- internal/agentnew/generate.go[75-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Validation loop skips dependency preflight ✓ Resolved 🐞 Bug ☼ Reliability
Description
--validation-loop configures a script that requires python3 with jsonschema, but leaves
preflight_check empty. A runner missing that dependency therefore creates and runs the sandbox
before failing the validation step, despite the runtime providing a preflight mechanism specifically
to fail before sandbox creation.
Code

internal/agentnew/render.go[R129-134]

+	if opts.ValidationLoop {
+		h.ValidationLoop = &harness.ValidationLoop{
+			Script:        "scripts/validate-output-schema.sh",
+			Schema:        "schemas/" + opts.Name + "-result.schema.json",
+			MaxIterations: 2,
+		}
Relevance

●● Moderate

Dependency preflight behavior is a substantive runtime concern without closely matching historical
acceptance evidence.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The generated validation loop supplies only script, schema, and iteration count. The validator
explicitly exits when jsonschema is unavailable, whereas run.go runs a configured
PreflightCheck before sandbox availability and sandbox creation precisely to catch this dependency
class early.

internal/agentnew/render.go[129-134]
internal/agentnew/templates/scripts/validate-output-schema.sh[42-47]
internal/cli/run.go[1165-1185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Generated validation-loop harnesses do not preflight the `python3`/`jsonschema` dependency required by their generated validator script.

## Issue Context
Set `ValidationLoop.PreflightCheck` to a shell command that verifies `python3 -c 'import jsonschema'`, so `fullsend run` fails before sandbox creation when the host cannot run the validation script.

## Fix Focus Areas
- internal/agentnew/render.go[129-134]
- internal/agentnew/templates/scripts/validate-output-schema.sh[42-47]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

13. ADR 0102 exceeds limit ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0102 has 120 content lines after its ten-line frontmatter, exceeding the 100-line maximum. The
oversized ADR should be shortened by moving detailed implementation and conformance material to
appropriate reference documentation.
Code

docs/ADRs/0102-generate-custom-agents-from-the-cli.md[R127-130]

+The generator does not replace `fullsend lock` or `fullsend run` validation and
+is not wired into them: those interleave minting, runner-env validation and
+`${VAR}` expansion between the same steps in different orders, so sharing one
+helper would change their behaviour.
Relevance

● Weak

A closely matching recent finding to shorten an oversized ADR was rejected by the team.

PR-#2582

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The ADR frontmatter ends at line 10 and its content continues through line 130, yielding 120 content
lines. Rule 1062092 sets a maximum of 100 content lines excluding frontmatter.

docs/ADRs/0102-generate-custom-agents-from-the-cli.md[1-130]
Skill: writing-adrs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ADR 0102 exceeds the 100-line content limit after excluding frontmatter.

## Issue Context
Keep the point-in-time context, genuine alternatives, direct decision, and concise consequences in the ADR. Move detailed implementation behavior, test strategy, and operational limitations into CLI or contributing documentation and link to it where necessary.

## Fix Focus Areas
- docs/ADRs/0102-generate-custom-agents-from-the-cli.md[12-130]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 65 rules
Review mode: 🧠 Deep: This introduces a substantial CLI generation pipeline spanning rendering, validation, triggers, roles, filesystem writes, registration, templates, and documentation, creating many independent logic paths with plausible subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can route each action level your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/ADRs/0102-generate-custom-agents-from-the-cli.md
Comment thread docs/ADRs/0102-generate-custom-agents-from-the-cli.md Outdated
Comment thread docs/ADRs/0058-agent-registration.md
Comment thread docs/guides/user/bring-your-own-agent.md Outdated
Comment thread docs/guides/user/bring-your-own-agent.md Outdated
Comment thread internal/cli/agent_new.go
Comment thread internal/cli/agent_new.go
Comment thread internal/agentnew/spec.go
Comment thread internal/agentnew/generate.go Outdated
Comment thread internal/agentnew/render.go
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 4:38 PM UTC · Ended 5:19 PM UTC

Commit: fe8f8bc · View workflow run →

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 5:21 PM UTC · Ended 5:26 PM UTC

Commit: f237f80 · View workflow run →

@waynesun09

Copy link
Copy Markdown
Member Author

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 5:27 PM UTC · Ended 5:30 PM UTC

Commit: f237f80 · View workflow run →

@waynesun09

Copy link
Copy Markdown
Member Author

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure (validation failed after 2 iteration(s)) · Started 5:32 PM UTC · Completed 6:15 PM UTC

Commit: 85abbad · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high

@rh-hemartin

Copy link
Copy Markdown
Member

This introduces a lot of machinery for something that could be a cp -r. Why we don't have a template and copy that over and call it a day? We can replace some placeholders, ok, but do we need 6k lines? Maybe I'm just getting tired and burned out.

@waynesun09

Copy link
Copy Markdown
Member Author

Fair question. Two things:

It's an interface, not a copy. The loudest user feedback we have is that creating an agent is hard to figure out (#4839, #5804, #6830, #6834). What users want is: name + role in, working agent out. Working means: commit it, type /fs-<name> on an issue, get a comment back. Only the prompt body is left to write. That's what agent new delivers.

There's nothing to cp -r. The fleet repo is laid out by kind, not by agent:

  • triage is 15 files across 7 dirs, and its harness references 26 more (providers, profiles, skills, env, scripts)
  • its post-script is 1,719 lines of triage-specific label logic
  • 211 triage occurrences to rename in the core files alone
  • no fleet harness has a trigger:, so a copied agent registers and never fires (enumerate.go:79)
  • a per-repo install has no policies/providers/profiles to copy from (v0.37 scaffold should generate a default policies/base.yaml for sandbox-enabled agents #6834)

About half of the 1.5k lines of Go handles exactly that: the role→provider table, trigger compile, pre-write checks, registration, and the symlink/rollback safety the bot review asked for. The templates themselves are 226 lines.

On the 6k: 2.2k is tests and 1.7k is golden fixtures. I'll cut the goldens from 7 trees to 2 (about 1,200 lines fewer, same coverage) and can trim CLI-level tests that duplicate package tests if you'd like.

@waynesun09

Copy link
Copy Markdown
Member Author

Numbers after the trim, head 4c103f162:

lines
Production Go 1,516
Tests + golden fixtures 2,616
Docs + ADR 498
Embedded templates (the files a user gets) 226
Total 4,856 (was ~6,050)

The golden trees went 7 → 2 and the duplicated CLI tests are gone: −1,200 lines, patch coverage 87.4% (plain go test -cover, unchanged floor). Golden fixtures are now 479 lines of the 2,616.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 3:05 PM UTC · Ended 3:11 PM UTC

Commit: 4c103f1 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 3:13 PM UTC · Ended 3:49 PM UTC

Commit: db8b53b · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 3:51 PM UTC · Ended 3:58 PM UTC

Commit: 5bfe011 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 4:00 PM UTC · Ended 4:11 PM UTC

Commit: ce5b57d · View workflow run →

…nt new

Groundwork for `fullsend agent new`: the three pure, network-free pieces
the generator is built on.

The role table is hardcoded rather than derived from
mintcore.BuiltInRoles(). Derivation would re-admit `scribe`, which
config.ValidRoles() deliberately excludes as a mint-only dogfood role
that "must not silently pass config validation", and it would fail open
for any future canonical role with no provider pairing. Drift is caught
by a test asserting each role is in both BuiltInRoles() and ValidRoles()
and that its permissions equal mintcore.RolePermissionsFor(role) —
BuiltInRoles rather than HasRole, because HasRole also returns true for
standalone-mint custom roles registered at runtime.

Trigger presets are pinned by string equality, not just compiled. An
expression can compile and still touch an absent optional field, which
fails only at first dispatch — MatchHarnesses turns that into a red
::error:: annotation on every matching event. The `command` preset
guards fork pull requests with has() rather than the reference doc's
`!= null`: state.change_proposal is absent, not null, on a non-PR
comment, so `!= null` raises a missing-key error on every issue comment.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Render turns resolved options into the full file set: harness, agent
definition, result schema, post-script, and the shared scaffold assets a
generated agent depends on but does not own.

The harness is built as a harness.Harness value and marshalled, never
formatted as text, so the generator cannot emit a field the validator
does not know about. The agent definition's frontmatter is marshalled
for the same reason: a --description containing a colon or a leading ">"
would otherwise produce a broken document. The only user value that
reaches the generated shell script is the agent name, substituted after
it has passed harness.ValidAgentBasename.

Providers and profiles are referenced by path, not bare name. A bare
name with no definition on disk does not fail loudly — the embedded
fallback fills in only the OpenAI provider, so any other name degrades
to a warning and then a sandbox that cannot reach Vertex. The generator
therefore also writes policies/, providers/ and profiles/ when absent: a
per-repo install vendors none of them, CI's workspace layering skips
policies/ because the embedded scaffold has no such directory, and
profiles/ was never in LAYERED_DIRS at all.

ValidAgentBasename and ValidSlug are exported alongside the existing
ValidPluginBasename so a caller that constructs a harness, rather than
loading one, can reject an unsafe name before writing it anywhere.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
CheckGenerated runs the checks a freshly constructed harness needs:
Lint diagnostics, ResolveRelativeTo, ValidateFilesExist, and a stat of
every provider and profile named by path.

That fourth step is the point of the helper. ValidateFilesExist skips
providers and profiles deliberately — ResolveHarness reads them later,
during the run's resolve step, and reports its own error. A generator
has no resolve step, so without it the failure the generator most needs
to catch is invisible: a harness naming providers/vertex-ai.yaml with no
such file validates cleanly and then fails at run time, or degrades to a
warning and a sandbox that cannot reach Vertex.

CheckGenerated is deliberately not wired into run or lock. Those two
interleave minting, runner-env validation and ${VAR} expansion between
the same steps, in different orders — run expands paths so they resolve
before ValidateFilesExist stat-checks them, and lock never calls
ValidateFilesExist at all. Collapsing them would change behaviour on the
two hottest execution paths. ValidateRunnerEnvWith is likewise excluded,
and says so: it requires every ${VAR} to be set in the calling process,
which is true in CI and false on a developer's machine.

The trigger presets are now evaluated against the normative
NormalizedEvent v1 fixtures. The command preset is the default, so every
generated agent carries it, including role coder — it must fire for a
non-fork pull request comment and an issue comment, and must not fire
for a comment on a fork. A companion test evaluates it against all
thirteen fixtures and asserts it errors on none: that is the property
the reference doc's `!= null` does not have.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Generates a complete, valid, runnable custom agent from a name and a
role, registers it, and validates the result with the same loader
dispatch uses — so the errors that used to appear at the first dispatch
after merge appear at generation time instead.

Ordering is the substance of the command. The name is checked against
the shell-safety pattern before anything touches disk; the trigger is
expanded and compiled before any file is written; everything is rendered
and validated in a scratch directory first, so a harness that would fail
validation never leaves a half-written .fullsend behind. Where a shared
asset already exists in the target, the target's copy is what gets
validated, since a hand-edited policies/base.yaml is common and its
content matters.

--force overwrites the four files an agent owns but never a shared
scaffold asset, and never papers over a config name collision.

A trigger is mandatory. ListTriggeredHarnesses skips a trigger-less
harness with a bare `continue` and no annotation, so such an agent
registers, validates, appears in `agent list`, and is then silently
never dispatched — the single failure this command is best placed to
prevent. The printed CI instruction is derived from the same trigger
that went into the harness, so the two cannot disagree.

The existing subcommand-count assertion is updated for the sixth
subcommand and gains positive assertions for `new` and `set`.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Every command shown was run against a real temporary --fullsend-dir and
the output pasted is the output it produced.

The CEL reference's slash-command pattern is corrected to use
!has(state.change_proposal) rather than `!= null`. change_proposal is
ABSENT on a comment posted to a plain issue, not present-and-null — the
NormalizedEvent schema requires only `labels` under `state` — so
comparing it against null raises a missing-key error, and dispatch
reports that as `trigger eval failed` on every issue comment in the
repository. The corrected block is byte-identical to what
`agent new --on command:` emits, and a test asserts the two stay that
way.

The Bring Your Own Agent guide claimed twice that `github setup` creates
policies/, providers/ and profiles/. It creates none of them, and that
sentence is what walks authors into #6834; it now says so and points at
the generator.

ADR 0102 records the decision. It is a new ADR, not a superseding one:
#4839's July "prefer guides over a CLI" call was taken in Slack and
never written up.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
In a linked worktree, .git is a FILE containing "gitdir: <path>" rather
than a directory, and the repository config lives in the main
repository's common directory. The owner lookup read dir/.git/config
directly, so inside a worktree it found nothing and every generated
agent silently fell back to the "fullsend-<name>" slug.

Worktrees are the normal way to work in this project, so that was the
common path rather than an edge case. readGitConfig now follows the
gitdir pointer and the commondir file; a malformed pointer still
degrades to the fallback rather than failing.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Six correctness findings, all on paths that could leave a repository
changed by a failed run or accept input that should be refused.

A name already registered in config.yaml was only detected during
registration, which happens after every file has been written — so a
duplicate name left the directory modified by a run that then failed.
The check now happens before anything is written, and the fullsend-dir
check happens before that, so a missing directory is still reported as
"run github setup first" rather than as a failure to read config.yaml
from inside it.

Destinations are now resolved with Lstat rather than Stat, and every
directory between the fullsend dir and each destination is checked. A
repository shipping a `.fullsend/harness` symlink, or a dangling symlink
at a destination, could otherwise redirect a generated file outside the
fullsend directory entirely. Anything that is not a regular file is
refused rather than overwritten.

A failure part-way through the write loop now removes the files that run
created, rather than leaving a partial agent: some files present, the
harness perhaps missing, nothing registered.

An existing shared asset that cannot be read — a directory in its place,
or bad permissions — now fails naming the path. It previously fell back
to validating the embedded copy, which is not the file the agent would
have run against.

--runtime is validated while options are resolved rather than during
registration, and only a real io.EOF ends a spec document: any other
error from the second decode is a malformed second document and is now
reported instead of being read as absence.

The opt-in validation_loop gains a preflight_check. The validation
script hard-fails when python3 or jsonschema is missing, and it does so
only after the agent has run; preflight_check is evaluated before
sandbox creation, so the same missing dependency costs nothing.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
codecov/patch failed at 72.23%. The cause was mine: internal/harness's
CheckGenerated and the exported name validators were exercised only from
internal/agentnew's tests, and Codecov credits a file from tests in its
own package. My local measurement had used -coverpkg, which does credit
cross-package calls, so it reported 88% for a patch CI scored at 72%.

CheckGenerated now has direct tests in package harness: the happy path,
each missing-file branch including the provider and profile stat that
ValidateFilesExist deliberately skips, a lint diagnostic returned
non-fatally, a path escaping the fullsend directory, and a bare provider
name correctly not stat-ed. ValidAgentBasename and ValidSlug get table
tests; the rejected half of ValidAgentBasename is the security-relevant
one, since the agent name reaches shell interpolation.

The generator's write paths are covered in their own package too:
force-overwrite versus shared-asset preservation, collision refusal,
dry-run, the injected-failure rollback, both symlink refusals, and the
unreadable shared asset.

Measured without -coverpkg, as CI measures: 87.9%.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Adds the ADR 0102 entry to the agent-registration Decided list in
architecture.md, which the repo's convention expects for a new ADR, and
converts the ADR's Consequences section to bullets per the template.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Step 0 opened with a runnable command while the prerequisites it needs —
the CLI on PATH, a scaffolded repository, inference and the GitHub Apps —
were two sections further down. It now points at them first, and the
overview leads with the numbered sequence rather than prose so step 0
reads as part of the flow instead of an aside.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
The generated post-script cut the comment at MAX_COMMENT_CHARS and then
appended the truncation marker, so a truncated comment came out longer
than the 16384-character limit the generated result schema declares.

It now reserves the marker's length before cutting. Verified against the
generated script: a 17,000-character comment produces exactly 16384.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Seven golden trees for a five-role table was mostly repetition: the roles
differ only in table data — image, providers, profiles — which
TestRoleTableMatchesMint and its siblings already assert directly, and
more precisely than a byte diff does.

Kept triage as the default and validation-loop as the second. Not retro
or coder, despite those having the larger diffs: their differences are
data, while validation-loop is the only variant that exercises an actual
conditional in the generator — `if opts.ValidationLoop` in both
buildHarness and sharedAssets, which is also the only path that writes a
conditional shared asset.

Two branches the dropped trees did cover are now asserted directly
rather than incidentally: that each role's image and provider set reach
the generated harness, including that both image constants are reachable
so neither becomes dead configuration; and that each --on preset
survives marshalling into the harness. Those are checks a table test
cannot make, which is why they are worth keeping when the trees go.

On the CLI side, TestAgentNewForceNeverOverwritesSharedAssets and
TestAgentNewMissingDir duplicated package-level tests in internal/agentnew
outright, and the dry-run test's "wrote nothing" half did too; it now
asserts only what this layer can see, that the command reports its plan.
treeSnapshot lost its last caller and is removed.

Net 1,200 lines lighter. Patch coverage 87.4%, measured with plain
`go test -cover` per package as CI measures it.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
A reader who has not used fullsend cannot follow the new pages. Three
reviewer questions on the sibling PR were all the same defect — a term
of art used as if it were plain English — so this fixes the class rather
than the instances.

`mint` was used four times in docs/cli/agent.md before anything said
what it is; it now gets a sentence saying agents hold no long-lived
credentials and ask a service for a short-lived token, which is what
makes the role table matter. `--slug` said "harness slug", which tells a
reader nothing they did not already have; it now says what the value is
for. "A per-repo install does not vendor them" is now "`fullsend github
setup` does not copy these into your repository". "You edit prose, not
plumbing" is now the actual claim: every file is written for you except
the instructions the agent follows. Triggers are described before the
presets table rather than assumed.

The Bring Your Own Agent step 0 listed harness, agent definition,
schema, post-script, trigger and registration — six terms, none defined
until later in the page, in the first thing a reader sees. It now says
what those files do.

The generated harness header called the image "the fleet's current pin".
Every generated agent carries that comment, so the jargon shipped to
users; it now explains that the digest pins the image so every run uses
the same one. Goldens regenerated.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Four reviewers ran over this branch; three independently found the same
thing, and it is the failure this whole command exists to prevent.

`postCmd.Dir = runDir` (internal/cli/run.go:1645) — the post-script runs
from the RUN directory, and the agent's output is one level down, in
iteration-<N>/output/. The template hardcoded `output/agent-result.json`,
a path that never exists, so every agent this command generated would
have failed its first real run. The fleet's own post-script gets this
right, and I had quoted its pattern in the research for this change.

The tests could not have caught it. They matched substrings, and
`output/` appears in both the right and the wrong form; the shell test
built `<tmp>/output/` and ran from there, encoding the same wrong
assumption, so seventeen green assertions were confirming the bug. The
post-script is now executed by a Go test against the real layout —
iteration directories, the validated-iteration override, and the
no-output case — which is the only shape of test that distinguishes
them.

Selecting the last iteration by glob order was also wrong past nine:
iteration-10 sorts before iteration-9. It is compared numerically now.

Also from this round, each verified before fixing:

- A symlinked `.fullsend` root wrote straight through it. The earlier
  symlink guard only walked segments BENEATH the root, and
  ValidateFullsendDir used Stat, which follows. Reproduced by writing
  three files outside the tree, then fixed with Lstat.
- Rollback deleted `--force`-overwritten files instead of restoring
  them, so an interrupted regeneration destroyed a working agent rather
  than leaving it alone. Originals are now kept and restored.
- os.WriteFile leaves an existing file's mode alone, so a re-generated
  post-script could keep a non-executable bit. Chmod is now explicit.
- `_lead` passed generation and then failed registration, leaving ten
  files behind: harness and config disagreed on the first character.
  config.ValidConfigAgentName is exported and both rules now apply.
- The command trigger fired on GitHub Discussions, which the docs say it
  does not. Verified against the normative fixture — entity.kind is
  "conversation" — and the doc's own pattern had the work_item guard I
  had dropped. Restored in the preset and the reference together.
- The body told every agent to run fullsend-check-output, which exits
  non-zero unless FULLSEND_OUTPUT_SCHEMA is set — and the runner only
  sets it when a validation loop is configured, which is off by default.
  The instruction is now rendered only when it will work, and the tools
  list matches what the body actually invokes rather than claiming to.
- ${#var} counts bytes under a C locale while the schema's maxLength
  counts code points, so a valid multibyte summary was rejected and
  truncation could slice mid-character. The script pins LC_ALL.
- `jq -e .` called a bare `null` document invalid JSON and let a
  top-level array reach a raw jq error; it now checks for an object.
  A model-supplied summary containing a newline broke the heading it is
  interpolated into, and is refused.
- ownerFromGitConfig kept its section state across headers, so an origin
  with no url could return a submodule's owner.
- The slug lookup was handed a relative path it could not walk above.
- ADR 0102 documented `--template-dir`, which does not exist.
- --runtime offered the dummy runtimes, which do no inference.
- `timeout_minutes: 0` was indistinguishable from omitted.
- A comment described an implementation the code does not use, two named
  a function that does not exist, and a dead branch returned the same
  value twice.

Patch coverage 87.8%, plain `go test -cover` per package.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
The generated prompt says the summary is one line and the generated
post-script refuses one that is not, but the generated schema only
constrained type and length. So a multi-line summary passed the
validation loop, no retry was triggered, and the run died in the
post-script with nothing posted — the three files that are supposed to
describe one contract described two.

Found reviewing the reference agent in fullsend-ai/agents#1167, which is
generated from this template.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
The result-path bug this branch fixed could only ever surface at run
time, and nothing in the PR had actually run a generated agent. So I ran
one: `--runtime dummy` exercises the real sandbox and the real
post-script with a scripted result in place of the model, which proves
the whole pipeline without spending inference or touching a forge.

It works — agent exit code 0, and the post-script finds
iteration-1/output/agent-result.json and renders the comment. That
output is now in the docs as what success looks like, because it is the
one command whose failure this branch exists to prevent.

Running it also found a gap in the command's own next-steps text. The
generated harness copies GOOGLE_APPLICATION_CREDENTIALS into the
sandbox, so a run stops at validation without it — even under the dummy
runtime, which does no inference — and the printed list did not mention
it. GH_TOKEN also has to be a real token, because a connectivity check
runs before the agent does; a placeholder fails with `Bad credentials`.
Both are now in the printed next steps and in the troubleshooting prose,
alongside the `--forge github` requirement.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit to fullsend-ai/agents that referenced this pull request Sep 4, 2026
The generator template had the post-script looking for its input in the
wrong directory (fullsend-ai/fullsend#6972), so the example carried the
same defect and this repository's test confirmed it: run_post built
<tmp>/output/agent-result.json and ran from there, which is not the
layout fullsend provides. Seventeen assertions passed against a script
that could not have worked.

The test now builds iteration-<N>/output/ and runs from the run
directory, as internal/cli/run.go does, and adds the two cases the old
shape could not express: that the highest-numbered iteration wins, and
that FULLSEND_VALIDATED_ITERATION_DIR overrides it.

The example is regenerated from the fixed template, which also brings
the locale pin, the JSON-object shape check in place of `jq -e .`, the
single-line summary check, and the work_item guard that keeps the
trigger off GitHub Discussions. Three more reject cases cover shapes the
old `jq -e .` gate got wrong: a bare null, a bare false, and a
top-level array.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 4:13 PM UTC · Ended 4:38 PM UTC

Commit: 366e64a · View workflow run →

The dummy run proves the plumbing; it cannot prove that a real model can
follow the generated prompt with the tools and network profile the
harness grants. This run does: the reference agent against a real pull
request, exit 0, schema validation passed, post-script printing rather
than posting.

Getting there took isolating an unrelated failure, which is worth
recording because the symptom is misleading. A local `--runtime claude`
run died with `Could not refresh access token: policy_denied`, which
reads as a credentials problem. It was not: the composed sandbox policy
contained no Vertex network policy at all, because this host's
registered `vertex-ai` provider had drifted to zero credential keys. The
provider file and the profile were both byte-correct. Deleting the
provider and letting the run re-ensure it produced a healthy
registration and the run worked.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure (validation failed after 2 iteration(s)) · Started 4:40 PM UTC · Completed 5:22 PM UTC

Commit: c73568c · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): fullsend agent new — generate a complete custom agent from minimal parameters

2 participants