Author Guide

Maintaining the Package

The author workflow for regenerating dependencies, the squad source layout, customization knobs, and the tag-based release process.

Repository structure

Scripts

Defined in apm.yml:

Maintainer workflow

  1. Regenerate dependencies:
    apm run sync-deps
  2. Install dependencies locally:
    apm install
    # or
    apm run install-sync
  3. Refresh lockfile:
    apm lock
  4. [OPTIONAL] Build package artifact (only needed to build a plugin):
    apm pack
  5. [OPTIONAL] Publish (if applicable):
    apm publish

Recommended sequence before release:

  1. apm run sync-deps
  2. apm lock
  3. apm pack (optional)
  4. apm run release-prep — or run the Release Prep workflow, which does the same thing and pushes.

How dependency generation works

The generator script:

This keeps package metadata stable while updating the dependency list safely.

Upgrading hve-core: the two paths

The squad roster binds every role to an hve-core agent by its exact name: frontmatter value. That makes an hve-core upgrade two different operations wearing the same clothes, and only one of them is safe to automate end to end. The split is decided by one question: does this hve-core revision move an agent the squad still names?

flowchart TD A["Cron 06:00 UTC
sync-hve-core.yml"] --> B["Get-HveCoreCastDelta.ps1
pinned SHA vs hve-core main"] B --> C{"Does squad-src still reference
a removed or un-dispatched agent?"} C -- "no (mechanical)" --> D["Bump pin + assemble release
from .changes fragments + push"] D --> E["release.yml
tag + GitHub Release"] C -- "yes (breaking)" --> F["Open issue labeled squad/auto
body = full adaptation brief"] F --> G["squad-watch.yml
fires on the label"] G --> H["Sub-squad issue-N
autopilot, unattended"] H --> I["DRAFT PR that closes the issue"] I --> J{"Label the PR squad/review?"} J -- yes --> K["squad-watch.yml again
sub-squad pr-N, tester role"] K --> L["Review comments on the PR"] L --> M["Human merges"] J -- no --> M

Note that no cron runs a squad. The only scheduled job is the mechanical sync; when it finds a breaking delta it writes an issue and stops. The issue is both the trigger and the brief, so the squad's task description is a durable, reviewable artifact rather than a string buried in a workflow file.

Path 1 — mechanical (no cast change)

sync-hve-core.yml moves the pinned SHA and records the move as a change fragment, exactly as a contributor would — so the pin move queues alongside whatever contributors merged that day and ships with the next release someone cuts, instead of cutting a release of its own every morning. It pushes and stops there. One addition to the older behavior: it runs the cast-delta script as a guard first. A SHA move cannot repoint a roster row, and shipping it anyway releases a squad whose roles resolve to nothing — which is exactly what happened in 0.10.11.

Path 2 — adaptive (the cast changed)

The sync workflow does not touch apm.yml. It opens an issue labeled squad/auto whose body carries the whole brief: what changed and why it breaks, eight numbered steps, the editing constraints, acceptance criteria, and the generated cast-delta table. One open issue per target SHA — a repeat cron on an unresolved delta comments rather than opening a duplicate.

squad-watch.yml fires on that label, runs the squad headlessly in sub-squad issue-<N>, and opens a draft PR that closes the issue. Labeling that PR squad/review fires the same workflow again in sub-squad pr-<N>, routed to the tester role, which posts its findings as PR comments.

The adaptation PR carries a .changes/unreleased/ fragment with bump: minor, because a cast change means the roster that ships is not the one that shipped before. It does not edit apm.yml version: or CHANGELOG.md — the same rule every contributor follows, and pr-validation.yml enforces it here too. If a run forgets, the workflow writes a placeholder patch fragment so the PR can still go green, marked PLACEHOLDER, rewrite before merging.

Merging the adaptation pull request queues the minor release by itself. The fragment lands in .changes/unreleased/ on main, where Rolling Preview publishes it as an installable pre-release within minutes. It ships to consumers with the next release you cut.

How gates behave when nobody is watching

An unattended run cannot wait for an approval that will never arrive, so Watch Mode moves where the gate is satisfied rather than removing it. See Unattended Gate Disposition in the Watch Mode instructions.

Run the delta check yourself at any time:

./scripts/Get-HveCoreCastDelta.ps1
./scripts/Get-HveCoreCastDelta.ps1 -ToRef main -MarkdownPath delta.md

Configuration required

Testing the loop by hand

Run Squad Watch from the Actions tab with an issue_number input to exercise the whole path against an issue you wrote yourself, without waiting for an upstream change. Do this once before relying on the cron.

Reviewing an adaptation PR

The PR body carries the checklist; the two items that matter most are:

Squad source

In addition to the microsoft/hve-core dependencies, this package ships a locally authored "squad" — a Squad Coordinator plus supporting agents, instructions, a prompt, and a skill. The squad source lives under:

Why the source is separate from the deployed .github/ tree

apm install deploys agents and prompts flattened into the consumer's .github/ tree. If the squad source lived directly under the top-level .github/agents/ (or .github/prompts/), a later apm install would clobber it. Keeping the authored source under squad-src/.github/... places it outside the deploy flatten zone while preserving the .github/{agents,prompts,instructions,skills}/ prefix the deploy mapping expects. Runtime squad state is created per-project under .copilot-tracking/squad/ and is never packaged.

How squad entries are generated

Update-ApmDependencies.ps1 enumerates the squad source from the local filesystem (it is not cloned, because the source lives in this working tree and may not be pushed yet). Squad entries are emitted as remote virtual paths of the form <SquadRepoSlug>/<SquadSourceRoot>/.github/..., for example Peter-N91/hve-squad/squad-src/.github/agents/squad/squad-coordinator.agent.md. They are appended after the hve-core entries in dependencies.apm.

The sync command accepts these squad-specific parameters:

pwsh -File scripts/Update-ApmDependencies.ps1 `
  -SquadSourceRoot squad-src `
  -SquadRepoSlug Peter-N91/hve-squad `
  -SquadRef vX.Y.Z

Use -DryRun to preview both the hve-core and squad entries without writing apm.yml.

Reproducible release tags

The squad self-references ship as bare paths on main, and APM resolves a bare path against the default branch. Left alone, a tag would freeze the dependency list but not its contents: installing #v0.14.0 was measured returning main's coordinator, while the hve-core entries in the same manifest resolved correctly because they carry #<sha>. APM warns about it directly — 47 dependencies unpinned: Peter-N91/hve-squad.

release.yml closes this without any manual step. Before creating the release it runs scripts/Set-SquadSelfRefPin.ps1 -Ref vX.Y.Z, commits the pinned manifest, and pushes that commit straight to refs/tags/vX.Y.Z. A manifest cannot name its own tag before the tag exists, so the pin lives in a commit that only the tag points at. main never receives it and keeps the unpinned manifest, so development installs continue to track main.

This applies from v0.15.2 onward. Every earlier tag resolves its squad files from main and is not reproducible. Fixing one retroactively means force-moving its tag, which changes what its GitHub Release points at.

apm lock and apm install can only resolve the squad virtual paths once the squad-src/ tree has been pushed to the SquadRepoSlug remote. Release tags are pinned automatically by release.yml, so verify apm install from a fresh clone of the tag rather than from the working tree.

Reproducible pins

Every generated microsoft/hve-core entry is pinned to the exact commit SHA that -Ref resolves to (appended as #<sha>), so a published version keeps resolving the same files even after hve-core's main moves on. Without this, an unpinned path that hve-core later renames or removes makes the install fail for every consumer of that version. With no -Ref, the script reuses the pin already in apm.yml; pass -Ref <hve-core-ref> to move it to a specific commit, tag, or branch.

Customization

You can tune generation behavior in scripts/Update-ApmDependencies.ps1:

Change fragments

Version and changelog are release outputs, not things a pull request edits. A version line is a single line and the newest changelog heading is a single position, so two pull requests that both touch them always conflict — and the second one to merge silently reuses a version number the first already claimed. Moving a target branch does not help: two pull requests bumping to the same number on a develop branch conflict identically. The fix is to take the shared state out of the pull request entirely.

Each pull request instead adds one new file under .changes/unreleased/, generated by apm run change. Two pull requests adding two differently named files never conflict. The format is documented in .changes/README.md and repeated for contributors on the Contributing page.

pr-validation.yml makes the rule mechanical rather than a review comment:

It runs on pull_request, not pull_request_target: fork pull requests get a read-only token and no secrets, and nothing in the check needs either. Create the skip-changelog label before relying on the escape hatch.

Who decides the release level

The version level tracks ideas, not artifacts. 0.10.0 was federation and 0.11.0 was the hve-core attachment rework, because each introduced a concept that did not exist before. Every agent, skill, role, and instruction added underneath one of those ideas is a patch, however many files it touches. A contributor who adds a squad role to federation is extending an idea that already ships.

A fragment's bump is a suggestion, and because merging releases immediately, the pull request is where you overrule it. Two things make that easy:

The release-time overrides still exist for the batch you assemble yourself, and for anything that reached main without a release:

pwsh -File scripts/Invoke-ReleasePrep.ps1 -Bump minor -DryRun   # this batch adds up to a new idea
pwsh -File scripts/Invoke-ReleasePrep.ps1 -Version 1.0.0        # exact number, overrides everything

The Release Prep workflow exposes the same choice as a bump dropdown, defaulting to fragments. The script prints “Bump override: fragments resolved to X, releasing as Y” whenever you overrule them, so the decision is visible in the run log.

Release process

Releases use a tag-based flow on top of the default branch (main). The tag is the release artifact consumers install with apm install "Peter-N91/hve-squad#vX.Y.Z". You do not choose the version number by hand — the highest bump among the pending fragments selects it.

Releases are cut by hand. Merging a pull request that carries a fragment pushes it to .changes/unreleased/ on main and releases nothing. Nothing releases on a timer either. When the pending batch is ready, you dispatch Release Prep, which ships it as one version: it resolves the level, prepends the CHANGELOG section with the consumer-install block and the link reference, writes version: into apm.yml, deletes the fragments it consumed, commits chore(release): X.Y.Z, dispatches release.yml to cut the tag and the GitHub Release, and retires the pre-release.

That makes releasing a judgement about readiness rather than a date. A quick fix ships as soon as you have verified it in the preview; a larger change sits in preview until it is ready, and anything merged in the meantime rides along with it.

Between releases, main is installable. Every merge triggers Rolling Preview, which renders the pending fragments into a single GitHub pre-release tagged with the version they resolve to (v0.15.0-pre) and force-moves that tag to the new head. It only reads: no fragment is consumed, no version is bumped, and CHANGELOG.md is untouched. Exactly one pre-release is open at a time, so when a minor fragment lands the old one is deleted and the new name takes over. A pull request with no fragment (the skip-changelog label) contributes no bump.

Tick dry run when you dispatch to see the resolved version and the assembled section without committing anything. Use the bump and version inputs to overrule the level the fragments resolved to or to pin an exact number. release branch. Cut a short-lived hotfix/vX.Y.Z branch off an existing tag only when main holds work that must not ship yet and a fix cannot wait for it — a release ships a commit, so a tag cut from main carries that pending work out with the fix.

git switch --detach v0.14.0
git switch -c hotfix/v0.14.1
# apply the fix, then record it
apm run change            # Type: Fixed, Bump: patch
git push -u origin hotfix/v0.14.1

Dispatch Release Prep with ref set to hotfix/v0.14.1. It consumes only that branch's fragments, bumps apm.yml there, and cuts the tag from that branch; release.yml receives the same ref and tags the commit it checked out. The rolling pre-release is deliberately not retired, because the batch it describes is still pending on main.

Merge the branch back afterwards so the fix survives the next release, and label that pull request release-merge-back. That label is the only exemption to the CHANGELOG.md and version: guards in pr-validation.yml, and it exists precisely because Release Prep — not a human — wrote both. If a higher version has already shipped, the release workflow compares versions and passes --latest=false, so the hotfix does not take the Latest badge from the release that supersedes itrelease-prep # resolve version, assemble, write pwsh -File scripts/Invoke-ReleasePrep.ps1 -DryRun # preview only pwsh -File scripts/Invoke-ReleasePrep.ps1 -Bump minor # overrule the fragments pwsh -File scripts/Invoke-ReleasePrep.ps1 -Version 1.0.0 # force an exact version

Before a release that also re-pins hve-core, regenerate the list first: pwsh -File scripts/Update-ApmDependencies.ps1 -Ref main pins every hve-core entry to main's commit SHA. Refresh the lockfile with apm lock. The squad self-references need no action — release.yml pins them into the tagged commit.

Avoid a long-lived release branch. Only create release/vX.Y.Z from an existing tag when you need to patch an older version after newer work has landed on main.