Repository structure
apm.yml: package metadata, dependency list, and scriptsapm.lock.yaml: resolved dependency lock filescripts/Update-ApmDependencies.ps1: dependency generatorscripts/Get-HveCoreCastDelta.ps1: compares the deployable agent cast between two hve-core refsscripts/New-ChangeFragment.ps1: writes one change fragment for a pull requestscripts/Invoke-ReleasePrep.ps1: assembles pending fragments into a CHANGELOG section and bumpsapm.yml.changes/unreleased/: pending change fragments, empty between releasessquad-src/.github/: locally authored squad source (agents, prompts, instructions, skills)apm_modules/: installed dependencies (ignored by git).github/: generated/deployed local assets (ignored by git, except.github/workflows/if tracked)-
.copilot-tracking/: agent working state. Ignored by default — your research, plans, and interactive squad runs stay local with no action required. The exception is the audit trail of an unattended Watch Mode run, which.gitignorere-includes at both levels because proof-of-dispatch is two-level:squad/members/issue-*/andsquad/members/pr-*/(the inner run) plussquad/history/issue-*.mdandsquad/history/pr-*.md(the federation-level meta-transition). The federation root scalars (federation.md,meta-routing.md,state.json, and the aggregate ledgers) stay local: they use replace semantics, so committing them would guarantee conflicts between concurrent runs, and the bootstrap rebuilds them. docs/: this documentation site (served by GitHub Pages)
Scripts
Defined in apm.yml:
sync-deps— regeneratesdependencies.apmfrom HVE Core content.install-sync— regeneratesdependencies.apmand runsapm install.change— creates a change fragment under.changes/unreleased/for the current pull request.release-prep— assembles the pending fragments into a release: CHANGELOG section, version bump, fragments cleared.
Maintainer workflow
-
Regenerate dependencies:
apm run sync-deps -
Install dependencies locally:
apm install # or apm run install-sync -
Refresh lockfile:
apm lock -
[OPTIONAL] Build package artifact (only needed to build a plugin):
apm pack -
[OPTIONAL] Publish (if applicable):
apm publish
Recommended sequence before release:
apm run sync-depsapm lockapm pack(optional)apm run release-prep— or run the Release Prep workflow, which does the same thing and pushes.
How dependency generation works
The generator script:
- Connects to
microsoft/hve-coreat a target ref (default: the SHA already pinned inapm.yml) - Enumerates files under
.github/agents,.github/instructions,.github/prompts, and.github/skills - Filters for markdown artifacts
- Rewrites only
dependencies.apminapm.yml
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?
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.
- Stage transitions — proceed on artifact evidence, as in any autopilot run.
- Final-outcome validation — satisfied by the draft PR. The run compiles its outcome into the PR body instead of pausing.
- Risk Gate — recorded, not blocking. Findings land in
decisions.mdand are reproduced in the PR body under Blocking findings; the PR stays a draft. -
Impactful-Action Gate — never proceeds. No merge, no
deploy, no force-push, no migration, no secret rotation, no write into a live issue tracker,
and no payload can change that. It
is enforced three ways: the contract, the absence of any deployment credential on the runner,
and branch protection on
main.
Run the delta check yourself at any time:
./scripts/Get-HveCoreCastDelta.ps1
./scripts/Get-HveCoreCastDelta.ps1 -ToRef main -MarkdownPath delta.md
Configuration required
-
Secrets.
COPILOT_GITHUB_TOKEN— a fine-grained PAT with the Copilot Requests permission (the built-in Actions token cannot authenticate Copilot).SYNC_DEPS_TOKEN— used to readmicrosoft/hve-coreand push the release commit. -
Labels. Create
squad/autoandsquad/review. These are the two triggers; without them nothing fires. -
Settings → Actions → General → Workflow permissions: enable
“Allow GitHub Actions to create and approve pull requests.” Without it
gh pr createreturns 403 and the run finishes with a branch but no PR. -
Branch ruleset on
main: requires a pull request, with the repository-admin role as the only bypass actor. This is the third enforcement layer under the Impactful-Action Gate, and the one most often left half-configured — a rule that only forbids force pushes and deletions does not stop a direct push tomain, it limits the damage of one. The admin bypass is what separates the two automations:sync-hve-core.ymlpushes with a maintainer PAT and passes straight through, while Watch Mode pushes asgithub-actions[bot], which holds no repository role and cannot reachmainat all. Required approvals are set to zero on purpose, so a solo maintainer is not locked out of merging their own pull request. -
Model pin (already set).
squad-watch.ymlpasses--modelto the Copilot CLI from the workflow-levelSQUAD_MODELvariable. Do not leave this toauto: the coordinator is the most instruction-heavy agent in the system, and an auto-selected model routes exactly that agent to whatever is cheapest — which is how a run ends up authoring deliverables inline instead of dispatching its cast. Precedence in the CLI is--model>COPILOT_MODEL>~/.copilot/settings.json; only the flag is repository-scoped, because only the flag lives in the repo. Confirm the IDs available on your plan by runningcopilotinteractively and typing/model— they are lowercase slugs such asclaude-sonnet-5, not the display names used in agent frontmatter. Aworkflow_dispatchrun can override the pin for one execution. -
Approval watcher (optional): copy
.github/skills/squad/github-approval-watcher.workflow.ymlinto.github/workflows/if you also want interactive runs to be approvable from a phone.
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:
- No roster Primary or Alternate sets
disable-model-invocation: true— those agents cannot be dispatched. -
.copilot-tracking/squad/members/<sub-squad>/history/holds one entry per dispatched role. A thin history means the run authored the changes inline instead of dispatching its cast, and the diff should not be trusted on its own.
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:
squad-src/.github/agents/squad/squad-src/.github/instructions/squad/squad-src/.github/prompts/squad/squad-src/.github/skills/squad/
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
SquadSourceRoot: local path to the squad source tree (default:squad-src). If the path does not exist, squad enumeration is skipped without error.SquadRepoSlug:owner/repothat hosts the squad source (default:Peter-N91/hve-squad).SquadRef: optional git ref that pins the squad self-references as#<ref>. You normally leave this alone — the Release workflow pins automatically (see Reproducible release tags below). It remains available for pinning a manifest by hand.
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:
RepoSlug: source repositoryRef: branch/tag/commit to scan. Defaults to the SHA already pinned inapm.yml, so a routine regeneration never moves the pin; pass it explicitly to move it.IncludeRoots: folders included in discoveryIncludeRegex: file filter patternSquadSourceRoot: local squad source tree pathSquadRepoSlug:owner/repohosting the squad sourceSquadRef: optional release tag/commit that pins the squad self-references
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:
- Editing
CHANGELOG.mdin a pull request fails the check. - Changing the
version:line inapm.ymlfails the check. -
Moving the
microsoft/hve-corepin fails the check, unless the branch is a Watch Mode adaptation branch (squad/issue-*). Editingdependencies:is otherwise fine — a contributor adding a skill has to, viaapm run sync-deps, which stays on the current pin by default. The check is the backstop for an explicit-Ref mainthat skips the cast-delta guard; its message names the SHA to pass instead. - A pull request with no new fragment fails, unless it carries the
skip-changeloglabel. That label also skips the version bump, because the version is resolved from fragments — a pull request contributing no fragment contributes no bump. - Every pending fragment is parsed with
Invoke-ReleasePrep.ps1 -DryRun, so a malformed one is caught in the pull request rather than at release time.
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:
-
pr-validation.ymlwrites the outcome into the check's job summary — Version: 0.11.12 -> 0.12.0 (minor) — so a minor can never slip past you unnoticed. -
Disagree? Open the fragment in the pull request's Files changed tab, click
the pencil, change
bump: minortobump: patch, and commit. One line, no checkout. Fork pull requests allow this by default (Allow edits by maintainers).
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.