Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .agents/skills/agent-conduct/SKILL.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .agents/skills/session-handoff/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,10 @@ the asking falls to the next session that has one. The rules below settle which
- **Reference every pull request as a clickable link.** When you mention a PR on a surface that renders Markdown (chat, a summary, a report), render it as a Markdown link to the PR. Write it as `[#N](https://github.com/OWNER/REPO/pull/N)`, never a bare `#N`. The same applies to issues and commits. **The form follows the surface.** Some surfaces link neither a Markdown link nor a bare URL, an interactive prompt's question and option text among them. Pasting a full URL into one of those does not rescue it, since the reader gets a string to copy. That is the outcome this rule exists to prevent. There the reference is a bare `#N`. The clickable link goes in the message that comes **before** the prompt rather than merely alongside it. That is because the prompt blocks on an answer, and a message emitted after it is read once that answer is already given. That is the one moment the link is no longer any use. The test is whether the reader can click it where it is read, not whether it was written in the syntax that works elsewhere.
- **Put a command the user must run in the question's own text.** Where a question asks the user to run something, the question carries the exact command, complete and ready to paste. One example is a command the agent cannot run itself because the command prompts for a password. A message before the prompt may repeat the command, but the question never points at that message. A question pointing at it can leave the reader to find the command among a long run of earlier output. In the clickable-link bullet above, only the rendered link moves out of the prompt. A command needs no rendering, since the prompt shows it as plain text the reader can copy.
- **Ask every question through the interface's prompt, never in prose.** When you need the user to decide, answer, or approve anything, put it to them through the interface's own prompt mechanism. Do so whether or not work is blocked on it. An offer to do more work ("want me to file that?") is a question like any other. A question written into a message is lost in the report around it, however short it is. One closing a long report sits where the reader is least likely to reach it. Where no prompt mechanism is available, the questions go as a numbered list opening the message rather than closing it. The user can then reply per number. This bullet settles how a question is put once it is asked. Whether a question is asked now or recorded for later is the parking bullet's to settle. That is the one opening "A question filed as an issue is parked rather than asked".
- **Raise work blocked on the user as a direct interactive prompt.** When progress needs a decision, an authorization, or an answer only the user can give, ask for it at the point the work stops. Ask through the interface's own prompt mechanism. Never leave it as prose in a summary. A handoff buried in a paragraph is a handoff that did not happen. That is because a summary reads as a report of finished work. The one line still waiting on the user is the easiest in it to skim past. The blocked item is the message, not a closing remark on a message about something else. **The options offered are the actions themselves.** The one that unblocks the work names the action it authorizes ("squash and merge it"). Selecting it is then the go-ahead rather than a note to act on later. Offering only ways to wait is the same failure in interactive clothing. That is because a prompt whose every choice is inaction reports the block rather than clearing it. Where the agent may not perform the authorized action itself, the option says who does it. Where no interactive prompt is available, the numbered list the bullet above names is the fallback.
- **Raise work blocked on the user as a direct interactive prompt.** When progress needs a decision, an authorization, or an answer only the user can give, ask for it at the point the work stops. Ask through the interface's own prompt mechanism. Never leave it as prose in a summary. A handoff buried in a paragraph is a handoff that did not happen. That is because a summary reads as a report of finished work. It is also because the one line still waiting on the user is the easiest in a summary to skim past. The blocked item is the message, not a closing remark on a message about something else. **The options offered are the actions themselves.** The one that unblocks the work names the action it authorizes ("squash and merge it"). Selecting it is then the go-ahead rather than a note to act on later. Offering only ways to wait is the same failure in interactive clothing. That is because a prompt whose every choice is inaction reports the block rather than clearing it. Where the agent may not perform the authorized action itself, the option says who does it. Where no interactive prompt is available, the numbered list the bullet above names is the fallback.
- **Lead every choice with a recommendation and its reason.** Wherever the user is asked to choose, in a prompt or in a numbered list, the option the agent recommends comes first. It is marked as the recommendation, and every option states the reason for it. A user who takes the recommendation then reads one line, and one who does not sees what the other options trade away. A question with no defensible recommendation says so rather than inventing one. Where an open question has no options and the agent does have an answer, that answer and its reason go in the question's text. They never go in as an invented option.
- **A question filed as an issue is parked rather than asked, and it stays owed.** Where the work cannot continue without the answer, the interactive-prompt bullet governs and the question is asked at the point the work stops. Wherever the question is recorded rather than asked, recording it is the right thing to do, and recording it is still not asking it. That holds whether the work can continue without the answer or the question was asked once and deferred. So the issue carries the fleet's `decision` label. When the filing session knows the choices the question is between, the issue states them. The label and the stated choices are what let a later session, one that was not there when the issue was filed, find the question. That session can then put the question to the user without inventing its answers. **The parked queue is the failure, not the parking.** An issue holding a question the user has never seen reads to every later session as tracked work rather than as a block. So each session files correctly and moves on. Presenting the accumulation is the step nobody owns.
- **The session that writes a handoff presents the parked queue in the same act.** This binds at the moment the session writes the handoff that `AGENTS.md` "Session Scope" defines, rather than at the moment the session ends. That is because a session also ends by interruption, where no agent acts at all and no rule reaches it. The session enumerates this repository's open issues carrying that label, and states how many issues the queue holds. It puts the highest-ranked of them to the user as questions, **one question per parked decision, carrying that decision's own answers as its options**. That is the interactive-prompt bullet's own shape applied per decision, rather than a single prompt whose options are topics. An issue that states no choices is put as the open question it is, never as invented options. That is because the label marks every decision waiting on the user, rather than only the ones filed under this rule. It is also because the issues that already carry the label predate the rule. Rank them longest-waited first. Every issue records how long it has waited, so any session can reproduce that order. Promote one ahead of that order where it blocks work in flight, and say in the handoff that it was promoted. That is because two sessions order the same queue differently where the key is subjective and nobody records it. Where more are parked than one round of questions can carry, the stated count still covers every one of them. The handoff names the remainder by issue number in that order, a list of numbers rather than of questions. That is what keeps the handoff inside the size rule that `AGENTS.md` "Session Scope" sets for it. **A count nobody states is a queue nobody can see**, which is how a backlog reported as healthy hides the questions inside it. Where a user is present but no prompt mechanism is available, the questions go as a numbered list. That is the fallback the interactive-prompt bullet already names, and it reaches a reader who is there to read it. Where no user is present at all, the session asks nothing and names the whole queue by issue number in the handoff. It leaves the asking to the next session that has one. An item leaves the queue when the user answers it, and also at triage where a later change has already answered or overtaken its decision. Either way it leaves by losing the label, with the answer or the reason recorded on the issue. The issue itself closes only where it held nothing but the question. That is because the queue holds issues that outlive their decision. Closing such an issue just to clear the queue discards the other work that issue tracks.
- **The session that writes a handoff presents the parked queue in the same act.** This binds at the moment the session writes the handoff that `AGENTS.md` "Session Scope" defines, rather than at the moment the session ends. That is because a session also ends by interruption, where no agent acts at all and no rule reaches it. The session enumerates this repository's open issues carrying that label, and states how many issues the queue holds. It puts the highest-ranked of them to the user as questions, **one question per parked decision, carrying that decision's own answers as its options**. That is the interactive-prompt bullet's own shape applied per decision, rather than a single prompt whose options are topics. An issue that states no choices is put as the open question it is, never as invented options. That is because the label marks every decision waiting on the user, rather than only the ones filed under this rule. It is also because the issues that already carry the label predate the rule. Rank them longest-waited first. Every issue records how long it has waited, so any session can reproduce that order. Promote one ahead of that order where it blocks work in flight, and say in the handoff that it was promoted. That is because two sessions order the same queue differently where the key is subjective and nobody records it. Where more are parked than one round of questions can carry, the stated count still covers every one of them. The handoff names the remainder by issue number in that order, a list of numbers rather than of questions. That is what keeps the handoff inside the size rule that `AGENTS.md` "Session Scope" sets for it. **A count nobody states is a queue nobody can see**, which is how a backlog reported as healthy hides the questions inside it. Where a user is present but no prompt mechanism is available, the questions go as a numbered list. That is the fallback the interactive-prompt bullet already names, and it reaches a reader who is there to read it. Where no user is present at all, the session asks nothing and names the whole queue by issue number in the handoff. It leaves the asking to the next session that has one. An item leaves the queue when the user answers it, and also at triage where a later change has already answered or overtaken its decision. Either way it leaves by losing the label, with the answer or the reason recorded on the issue. The issue itself closes only where it held nothing but the question. That is because the queue holds issues that outlive their decision. It is also because closing such an issue just to clear the queue discards the other work that issue tracks.

`GOVERNANCE.md` "Communicating with the User" keeps the full rules. The `agent-conduct` and `session-handoff` Skills at `.agents/skills/agent-conduct/SKILL.md` and `.agents/skills/session-handoff/SKILL.md` in the hub each carry it whole as a generated include. Each surfaces it at its own decision moment. Those paths are not repo-relative links, since they are hub-local and not carried into every fleet repo.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ flowchart TD

### Release Model

Each publish builds a **single branch**, the trigger ref (`main` a release, `develop` a prerelease). So there is no branch matrix, and `github.ref` always names the built branch. A **human merge never auto-publishes**. A `plan` job (`publish-plan-task.yml`) decides once, and a job downstream of it either gates on its outputs or inherits the skip through `needs:`. A run publishes on a **code-affecting bot push to `main`**. The App merges every Dependabot/codegen PR, so `github.actor` gates it. The publisher's own `on.push.paths` list also drops a non-substantive change like an Actions bump. A run also publishes on a **manual dispatch** of `main`/`develop`, or on a **main-only weekly schedule** (Docker, to refresh the base image). The `push` is main-only, so a develop bot merge publishes nothing (its prerelease comes via dispatch). A **source-only** repo publishes on **dispatch only**. Every release is a tag on the built commit plus a source archive, README, and LICENSE. Targets amend it with `release-asset-*` files. A registry push contributes none, and is made by the Docker leaf for an image and by the separate `publish-<target>` job for a package. An unchanged version cuts no second release on a schedule or push trigger, while a dispatch refreshes it (D4.4). Docker re-pushes by design.
Each publish builds a **single branch**, the trigger ref (`main` a release, `develop` a prerelease). So there is no branch matrix, and `github.ref` always names the built branch. A **human merge never auto-publishes**. A `plan` job (`publish-plan-task.yml`) decides once, and a job downstream of it either gates on its outputs or inherits the skip through `needs:`. A run publishes on a **code-affecting bot push to `main`**. The App merges every Dependabot/codegen PR, so `github.actor` gates that push. The publisher's own `on.push.paths` list also drops a non-substantive change like an Actions bump. A run also publishes on a **manual dispatch** of `main`/`develop`, or on a **main-only weekly schedule** (Docker, to refresh the base image). The `push` is main-only, so a develop bot merge publishes nothing (its prerelease comes via dispatch). A **source-only** repo publishes on **dispatch only**. Every release is a tag on the built commit plus a source archive, README, and LICENSE. Targets amend it with `release-asset-*` files. A registry push contributes none, and is made by the Docker leaf for an image and by the separate `publish-<target>` job for a package. An unchanged version cuts no second release on a schedule or push trigger, while a dispatch refreshes it (D4.4). Docker re-pushes by design.

```mermaid
flowchart TD
Expand All @@ -101,8 +101,8 @@ flowchart TD

Pick each output's path by **where the artifact goes**:

- **File on the GitHub release** (zip, binary, packaged library): one leaf per output uploading `release-asset-<branch>-<name>`. Or, for a repo calling the hub-hosted task, the files its `build-release-asset` hook writes, which the task uploads as one such artifact. The repo keeps `expect_release_assets: true` (its default).
- **Package-registry push** (NuGet, PyPI): the leaf builds and uploads a build artifact (`nuget-build-<branch>` / `pypi-build-<branch>`). A separate `publish-<target>` job in the **publishing repository's own** publisher consumes it and pushes. Each registry publishes through OIDC Trusted Publishing, never a stored API key, and that push sits outside the leaf for the reasons that follow. Trusted publishing validates the OIDC token's `job_workflow_ref` claim, which names the workflow the job actually ran from. So a push made from a reusable workflow a *different* repository hosts is rejected at the token exchange. NuGet.org answers `HTTP 401` with `does not start with <owner>/<repo>/.github/workflows/`. That alone rules out a leaf another repository hosts. A leaf this repository hosts clears the claim, and the split still applies to it. That is because a called job declaring no `permissions:` runs under the calling job's whole grant. So a push anywhere inside the release task would put `id-token: write` on every job in it, rather than at the one entry point D7.2 requires. The registered trusted-publishing policy therefore names the publisher, `publish-release.yml`. PyPI additionally gates its publish job behind an environment. NuGet.org binds its policy to the workflow file rather than to an environment and needs none. NuGet's leaf also uploads a `release-asset-*` carrying the package, and PyPI contributes none.
- **File on the GitHub release** (zip, binary, packaged library): one leaf per output uploading `release-asset-<branch>-<name>`. For a repo calling the hub-hosted task, the path is instead the files its `build-release-asset` hook writes, which the task uploads as one such artifact. The repo keeps `expect_release_assets: true` (its default).
- **Package-registry push** (NuGet, PyPI): the leaf builds and uploads a build artifact (`nuget-build-<branch>` / `pypi-build-<branch>`). A separate `publish-<target>` job in the **publishing repository's own** publisher consumes it and pushes. Each registry publishes through OIDC Trusted Publishing, never a stored API key, and that push sits outside the leaf for the reasons that follow. Trusted publishing validates the OIDC token's `job_workflow_ref` claim, which names the workflow the job actually ran from. So a push made from a reusable workflow a *different* repository hosts is rejected at the token exchange. There NuGet.org answers `HTTP 401` with `does not start with <owner>/<repo>/.github/workflows/`. That alone rules out a leaf another repository hosts. A leaf this repository hosts clears the claim, and the split still applies to it. That is because a called job declaring no `permissions:` runs under the calling job's whole grant. So a push anywhere inside the release task would put `id-token: write` on every job in it, rather than at the one entry point D7.2 requires. The registered trusted-publishing policy therefore names the publisher, `publish-release.yml`. PyPI additionally gates its publish job behind an environment. NuGet.org binds its policy to the workflow file rather than to an environment and needs none. NuGet's leaf also uploads a `release-asset-*` carrying the package, and PyPI contributes none.
- **Image-registry push** (Docker): the leaf pushes the default branch multi-arch (amd64+arm64) and any other branch `amd64`-only, and contributes no `release-asset-*`. The arm64 emulation is reserved for the released image.
- **Filesystem on a host the project owns** (a static site, a config tree): the leaf builds the tree and ships it to the host. It contributes no `release-asset-*`. The transport is the repo's own. The contract fixes the rest. The deploy is a **separate `workflow_dispatch`** from the release, so a redeploy of an unchanged commit mints no tag. A host rebuild, a rollback, or proving a branch on a non-production environment then costs nothing. Its credentials come from a **per-environment GitHub Environment** rather than the repository secret store. The deploy also ends by asserting **what the host serves** rather than the transport's exit status (D4.6). Retention at the destination is bounded by a declared count, with one side recorded as owning the prune. That side is the deploy where its credential can observe the destination, and the host where that credential is deliberately write-only (D5.6).
- **No file target via the release task** (Docker-only, PyPI-only, source-only): the release is tag + source zip + README + LICENSE. The caller **MUST pass `expect_release_assets: false`** to the release task. A publisher with file targets retains the default `true`. This setting is caller-specific. The default `true` fails on `fail_on_unmatched_files` when no assets exist. A **source-only** repo also passes every `enable_*` input as false because it has no build leaf (see `WORKFLOW.md` section 6).
Expand Down
Loading
Loading