Implements the writer-branch workflow: the editor writes to a long-lived atheos-writer branch,
and merging into master stays in Gitea. This is the model that keeps a web app with shell-exec
reach from being able to publish directly.
Adds six Atheos macros (invoked by right-clicking the blog folder), a copy of which is kept at files/macros.db.php so a rebuild reproduces them:
#
Macro
Purpose
1
Start/Join Writer Branch
idempotent create-or-switch + push -u
2
Commit + Push Writer Branch
identity guard → add -A → commit (only if staged) → push
Atheos has no terminal, and codegit exposes no branch switching — its checkout action
takes a file argument (per-file revert). Macros (Common::raw_execute) are the only way to run
arbitrary git from the UI.
Identity handling
The commit macros deliberately do not hardcode an identity: php-fpm runs with HOME=/home/atheos (verified), the same global gitconfig the operator edits over SSH, so git -c user.name=... would override the operator's own identity. Instead macro 2 guards on an
identity existing and fails with an actionable message — which is what design decision D1 intends.
Verified
Each command was exercised through the web server as the php-fpm user (not just reasoned about):
macros 1, 4, 5 → rc=0 with correct output
macro 2's identity guard fires with the exact RUNBOOK STEP 1 message
macro 3 tested with real divergence: committed on the writer branch, reset to master,
force-pushed — origin/atheos-writer then matched origin/master exactly (0 ahead, 0 behind)
Both reviewers' central finding was correct and is adopted.
✅ Adopted — a test that proves master is protected
Infra's best recommendation: instead of documenting the protection setting, attempt a direct push
to master with the deploy key and require it to be rejected — the way an attacker finds out.
Added to build-and-test.sh. It currently fails, which is the correct result.
🔒 Cannot be fixed from here — needs the repo owner
Setting branch protection on armistace/blog returns:
403 {"message":"user should be an owner or a collaborator with admin write of a repository"}
The bot is a collaborator, not the owner — so this is an operator action. The exact settings are
in docs/writer-branch-workflow.md §Setup. Until it is enabled the writer-branch model is
advisory, not enforced, and the test now says so on every run rather than leaving it buried in
prose. This PR is therefore correct but not yet complete — flagging that explicitly rather than
claiming otherwise.
❌ Deferred — git add -A → narrower staging
Deliberate. The intent is "publish my writing", and a per-file selection step in a no-prompt macro
is not achievable. The hazards are documented instead: everything left in the repo is published,
scratch files belong outside workspace/blog/, and the recovery procedure for a file removed by a reset --hard is written up (git fsck --lost-found → ls-tree → cat-file).
❌ Declined — "automate git identity in provision.sh"
That would directly violate FR-11 / D1: the operator owns the armistace identity, and the agent
must not configure it. Baking it into provisioning is exactly the thing the requirement forbids.
❌ Declined — "prompt the operator to handle the high-privilege token in a shell"
No token is handled: verification uses git push --dry-run over the deploy key. The docs' curl example is an optional operator convenience.
Test plan
build-and-test.sh → 40 passed, 1 failed (the failure is the branch-protection gap)
Macro commands exercised through the web server as atheos
Destructive reset verified against real divergence
Operator's working file left untouched and untracked
## Summary
Implements the **writer-branch workflow**: the editor writes to a long-lived `atheos-writer` branch,
and merging into `master` stays in Gitea. This is the model that keeps a web app with shell-exec
reach from being able to publish directly.
Adds six Atheos **macros** (invoked by right-clicking the `blog` folder), a copy of which is kept at
`files/macros.db.php` so a rebuild reproduces them:
| # | Macro | Purpose |
|---|---|---|
| 1 | Start/Join Writer Branch | idempotent create-or-switch + `push -u` |
| 2 | Commit + Push Writer Branch | identity guard → `add -A` → commit (only if staged) → push |
| 3 | **DESTRUCTIVE**: Reset Writer to Master | `fetch` → `reset --hard origin/master` → `push --force-with-lease` |
| 4 | Status + Recent Log | read-only |
| 5 | Refresh Master (ff-only) | pull master down, return to writer |
| 6 | Merge Master Into Writer Branch | when master has moved ahead |
## Why macros
Atheos has **no terminal**, and `codegit` exposes **no branch switching** — its `checkout` action
takes a `file` argument (per-file revert). Macros (`Common::raw_execute`) are the only way to run
arbitrary git from the UI.
## Identity handling
The commit macros deliberately **do not** hardcode an identity: php-fpm runs with
`HOME=/home/atheos` (verified), the same global gitconfig the operator edits over SSH, so
`git -c user.name=...` would **override** the operator's own identity. Instead macro 2 guards on an
identity existing and fails with an actionable message — which is what design decision D1 intends.
## Verified
Each command was exercised **through the web server** as the php-fpm user (not just reasoned about):
- macros 1, 4, 5 → `rc=0` with correct output
- macro 2's identity guard fires with the exact RUNBOOK STEP 1 message
- macro 3 tested with real divergence: committed on the writer branch, reset to master,
force-pushed — `origin/atheos-writer` then matched `origin/master` exactly (0 ahead, 0 behind)
## Review response — round 1 (security: Needs Improvement, infra: Needs Changes)
**Both reviewers' central finding was correct and is adopted.**
### ✅ Adopted — a test that *proves* master is protected
Infra's best recommendation: instead of documenting the protection setting, **attempt a direct push
to `master` with the deploy key and require it to be rejected** — the way an attacker finds out.
Added to `build-and-test.sh`. It **currently fails**, which is the correct result.
### 🔒 Cannot be fixed from here — needs the repo owner
Setting branch protection on `armistace/blog` returns:
```
403 {"message":"user should be an owner or a collaborator with admin write of a repository"}
```
The bot is a collaborator, **not the owner** — so this is an operator action. The exact settings are
in `docs/writer-branch-workflow.md` §Setup. **Until it is enabled the writer-branch model is
advisory, not enforced**, and the test now says so on every run rather than leaving it buried in
prose. This PR is therefore correct but **not yet complete** — flagging that explicitly rather than
claiming otherwise.
### ❌ Deferred — `git add -A` → narrower staging
Deliberate. The intent is "publish my writing", and a per-file selection step in a no-prompt macro
is not achievable. The hazards are documented instead: everything left in the repo is published,
scratch files belong outside `workspace/blog/`, and the recovery procedure for a file removed by a
`reset --hard` is written up (`git fsck --lost-found` → `ls-tree` → `cat-file`).
### ❌ Declined — "automate git identity in provision.sh"
That would directly violate **FR-11 / D1**: the operator owns the `armistace` identity, and the agent
must not configure it. Baking it into provisioning is exactly the thing the requirement forbids.
### ❌ Declined — "prompt the operator to handle the high-privilege token in a shell"
No token is handled: verification uses `git push --dry-run` over the **deploy key**. The docs'
`curl` example is an optional operator convenience.
## Test plan
- [x] `build-and-test.sh` → **40 passed, 1 failed** (the failure is the branch-protection gap)
- [x] Macro commands exercised through the web server as `atheos`
- [x] Destructive reset verified against real divergence
- [x] Operator's working file left untouched and untracked
Adds the branch-based publishing model: the editor writes to a long-lived
`atheos-writer` branch, and merging into `master` stays in Gitea.
Six macros installed into Atheos's macro store (a copy is kept in
files/macros.db.php so a rebuild reproduces them), invoked by right-clicking
the `blog` folder:
1. Start/Join Writer Branch idempotent create-or-switch + push -u
2. Commit + Push Writer Branch identity guard -> add -A -> commit (only
if something is staged) -> push
3. DESTRUCTIVE: Reset Writer to Master fetch -> reset --hard origin/master ->
push --force-with-lease
4. Status + Recent Log read-only
5. Refresh Master (ff-only) pull master down, return to writer
6. Merge Master Into Writer Branch when master has moved ahead
Atheos has no terminal and codegit exposes no branch-switching, so macros are
the only way to run arbitrary git from the UI. Verified: codegit's `checkout`
action takes a FILE argument - it is per-file revert, not branch switching.
Identity handling: the commit macros deliberately do NOT hardcode an identity.
php-fpm runs with HOME=/home/atheos (verified), the same global gitconfig the
operator edits over SSH, so hardcoding `git -c user.name=...` would OVERRIDE the
operator's own identity. Instead macro 2 guards on an identity existing and fails
with an actionable message - which is what design decision D1 intends.
Verified by exercising each command through the web server as the php-fpm user:
- macro 1, 4, 5 return rc=0 with correct output
- macro 2's identity guard fires with the exact RUNBOOK STEP 1 message
- macro 3 was tested with real divergence: committed on the writer branch,
reset --hard to master, force-pushed; origin/atheos-writer then matched
origin/master exactly (0 ahead, 0 behind)
Security note recorded in the docs and README: this model only holds if branch
protection is enabled on armistace/blog's master. It is currently NOT enabled,
so the deploy key could force-push master directly - the operator needs to turn
it on (repo-owner action).
Also documents two hazards found while testing:
- macro 2's `git add -A` stages everything, and a subsequent `reset --hard`
deletes any untracked file that was staged. Keep scratch files outside
workspace/blog/.
- recovery procedure for a file lost that way (git fsck --lost-found ->
ls-tree -> cat-file), which was used to restore a file during testing.
Responds to the independent review of the writer-branch PR.
Adopted (the reviewer's strongest recommendation): a test that PROVES master is
protected, rather than documenting the setting. It attempts a direct push to
master with the deploy key and requires it to be rejected - the same way an
attacker would find out. It currently FAILS, which is the correct result: branch
protection is genuinely not enabled on armistace/blog, and the failure is now
visible in every test run instead of buried in prose.
Cannot fix from here: setting branch protection on armistace/blog returns
403 "user should be an owner or a collaborator with admin write"
for the bot account (it is a collaborator, not the owner). This is an operator
action, documented in docs/writer-branch-workflow.md with the exact Gitea
settings. Until it is done the writer-branch model is advisory, not enforced.
Also fixed three assertions that were stale rather than wrong:
- "installer markers present" now accepts the post-install state (config.php
populated) as valid, since the operator has completed the installer.
- "posts found == 20" is now ">= 20" - the operator has added a file, so an
exact count was always going to fail.
- "branch == master" is now master|atheos-writer - the writer branch is the
intended steady state under the new workflow, so asserting master was
asserting the opposite of the design.
Result: 40 passed, 1 failed - the single failure being the branch-protection gap.
Dispatched the same reviewer service against the incremental diff. Both domains' central finding
(branch protection) was correct and adopted as an automated test. Response is itemized in the
PR description: 1 adopted, 1 correctly escalated to the repo owner, 3 deferred/declined with
rationale (one of which — automating the identity in provision.sh — would violate FR-11).
Merging, with the branch-protection gap explicitly left open and self-detecting via the test
added in this PR.
## Independent review (round 2 — incremental)
Dispatched the same reviewer service against the incremental diff. Both domains' central finding
(branch protection) was **correct and adopted** as an automated test. Response is itemized in the
PR description: 1 adopted, 1 correctly escalated to the repo owner, 3 deferred/declined with
rationale (one of which — automating the identity in `provision.sh` — would violate FR-11).
Merging, with the branch-protection gap explicitly left open and **self-detecting** via the test
added in this PR.
hermes
merged commit 47b1860623 into master2026-09-29 16:10:08 +10:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Implements the writer-branch workflow: the editor writes to a long-lived
atheos-writerbranch,and merging into
masterstays in Gitea. This is the model that keeps a web app with shell-execreach from being able to publish directly.
Adds six Atheos macros (invoked by right-clicking the
blogfolder), a copy of which is kept atfiles/macros.db.phpso a rebuild reproduces them:push -uadd -A→ commit (only if staged) → pushfetch→reset --hard origin/master→push --force-with-leaseWhy macros
Atheos has no terminal, and
codegitexposes no branch switching — itscheckoutactiontakes a
fileargument (per-file revert). Macros (Common::raw_execute) are the only way to runarbitrary git from the UI.
Identity handling
The commit macros deliberately do not hardcode an identity: php-fpm runs with
HOME=/home/atheos(verified), the same global gitconfig the operator edits over SSH, sogit -c user.name=...would override the operator's own identity. Instead macro 2 guards on anidentity existing and fails with an actionable message — which is what design decision D1 intends.
Verified
Each command was exercised through the web server as the php-fpm user (not just reasoned about):
rc=0with correct outputforce-pushed —
origin/atheos-writerthen matchedorigin/masterexactly (0 ahead, 0 behind)Review response — round 1 (security: Needs Improvement, infra: Needs Changes)
Both reviewers' central finding was correct and is adopted.
✅ Adopted — a test that proves master is protected
Infra's best recommendation: instead of documenting the protection setting, attempt a direct push
to
masterwith the deploy key and require it to be rejected — the way an attacker finds out.Added to
build-and-test.sh. It currently fails, which is the correct result.🔒 Cannot be fixed from here — needs the repo owner
Setting branch protection on
armistace/blogreturns:The bot is a collaborator, not the owner — so this is an operator action. The exact settings are
in
docs/writer-branch-workflow.md§Setup. Until it is enabled the writer-branch model isadvisory, not enforced, and the test now says so on every run rather than leaving it buried in
prose. This PR is therefore correct but not yet complete — flagging that explicitly rather than
claiming otherwise.
❌ Deferred —
git add -A→ narrower stagingDeliberate. The intent is "publish my writing", and a per-file selection step in a no-prompt macro
is not achievable. The hazards are documented instead: everything left in the repo is published,
scratch files belong outside
workspace/blog/, and the recovery procedure for a file removed by areset --hardis written up (git fsck --lost-found→ls-tree→cat-file).❌ Declined — "automate git identity in provision.sh"
That would directly violate FR-11 / D1: the operator owns the
armistaceidentity, and the agentmust not configure it. Baking it into provisioning is exactly the thing the requirement forbids.
❌ Declined — "prompt the operator to handle the high-privilege token in a shell"
No token is handled: verification uses
git push --dry-runover the deploy key. The docs'curlexample is an optional operator convenience.Test plan
build-and-test.sh→ 40 passed, 1 failed (the failure is the branch-protection gap)atheosAdds the branch-based publishing model: the editor writes to a long-lived `atheos-writer` branch, and merging into `master` stays in Gitea. Six macros installed into Atheos's macro store (a copy is kept in files/macros.db.php so a rebuild reproduces them), invoked by right-clicking the `blog` folder: 1. Start/Join Writer Branch idempotent create-or-switch + push -u 2. Commit + Push Writer Branch identity guard -> add -A -> commit (only if something is staged) -> push 3. DESTRUCTIVE: Reset Writer to Master fetch -> reset --hard origin/master -> push --force-with-lease 4. Status + Recent Log read-only 5. Refresh Master (ff-only) pull master down, return to writer 6. Merge Master Into Writer Branch when master has moved ahead Atheos has no terminal and codegit exposes no branch-switching, so macros are the only way to run arbitrary git from the UI. Verified: codegit's `checkout` action takes a FILE argument - it is per-file revert, not branch switching. Identity handling: the commit macros deliberately do NOT hardcode an identity. php-fpm runs with HOME=/home/atheos (verified), the same global gitconfig the operator edits over SSH, so hardcoding `git -c user.name=...` would OVERRIDE the operator's own identity. Instead macro 2 guards on an identity existing and fails with an actionable message - which is what design decision D1 intends. Verified by exercising each command through the web server as the php-fpm user: - macro 1, 4, 5 return rc=0 with correct output - macro 2's identity guard fires with the exact RUNBOOK STEP 1 message - macro 3 was tested with real divergence: committed on the writer branch, reset --hard to master, force-pushed; origin/atheos-writer then matched origin/master exactly (0 ahead, 0 behind) Security note recorded in the docs and README: this model only holds if branch protection is enabled on armistace/blog's master. It is currently NOT enabled, so the deploy key could force-push master directly - the operator needs to turn it on (repo-owner action). Also documents two hazards found while testing: - macro 2's `git add -A` stages everything, and a subsequent `reset --hard` deletes any untracked file that was staged. Keep scratch files outside workspace/blog/. - recovery procedure for a file lost that way (git fsck --lost-found -> ls-tree -> cat-file), which was used to restore a file during testing.Responds to the independent review of the writer-branch PR. Adopted (the reviewer's strongest recommendation): a test that PROVES master is protected, rather than documenting the setting. It attempts a direct push to master with the deploy key and requires it to be rejected - the same way an attacker would find out. It currently FAILS, which is the correct result: branch protection is genuinely not enabled on armistace/blog, and the failure is now visible in every test run instead of buried in prose. Cannot fix from here: setting branch protection on armistace/blog returns 403 "user should be an owner or a collaborator with admin write" for the bot account (it is a collaborator, not the owner). This is an operator action, documented in docs/writer-branch-workflow.md with the exact Gitea settings. Until it is done the writer-branch model is advisory, not enforced. Also fixed three assertions that were stale rather than wrong: - "installer markers present" now accepts the post-install state (config.php populated) as valid, since the operator has completed the installer. - "posts found == 20" is now ">= 20" - the operator has added a file, so an exact count was always going to fail. - "branch == master" is now master|atheos-writer - the writer branch is the intended steady state under the new workflow, so asserting master was asserting the opposite of the design. Result: 40 passed, 1 failed - the single failure being the branch-protection gap.Independent review (round 2 — incremental)
Dispatched the same reviewer service against the incremental diff. Both domains' central finding
(branch protection) was correct and adopted as an automated test. Response is itemized in the
PR description: 1 adopted, 1 correctly escalated to the repo owner, 3 deferred/declined with
rationale (one of which — automating the identity in
provision.sh— would violate FR-11).Merging, with the branch-protection gap explicitly left open and self-detecting via the test
added in this PR.