arcane-cli 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +37 -0
  2. package/dist/assets/.arcane/governance/agent-approved-paths.md +1 -1
  3. package/dist/assets/.arcane/governance/decision-documentation-standard.md +37 -1
  4. package/dist/assets/.arcane/governance/development-methodology.md +20 -0
  5. package/dist/assets/.arcane/governance/git-conventions.md +25 -3
  6. package/dist/assets/.arcane/governance/naming-conventions.md +1 -1
  7. package/dist/assets/.arcane/governance/spell-authoring-standards.md +40 -0
  8. package/dist/assets/.arcane/governance/universal-agent-rules.md +1 -1
  9. package/dist/assets/.arcane/spells/_fragments/push-policy-check.md +7 -0
  10. package/dist/assets/.arcane/spells/spell-address-review.md +15 -3
  11. package/dist/assets/.arcane/spells/spell-arcane-version.md +26 -0
  12. package/dist/assets/.arcane/spells/spell-check-drift.md +1 -0
  13. package/dist/assets/.arcane/spells/spell-close-session.md +45 -1
  14. package/dist/assets/.arcane/spells/spell-commit-work.md +23 -5
  15. package/dist/assets/.arcane/spells/spell-create-pull-request.md +19 -4
  16. package/dist/assets/.arcane/spells/spell-feedback.md +9 -0
  17. package/dist/assets/.arcane/spells/spell-manifest.md +25 -3
  18. package/dist/assets/.arcane/spells/spell-open-session.md +5 -0
  19. package/dist/assets/.arcane/spells/spell-save-idea.md +7 -1
  20. package/dist/assets/.arcane/spells/spell-ship.md +13 -1
  21. package/dist/assets/.arcane/spells/spell-sync-pull-request.md +12 -0
  22. package/dist/assets/.arcane/spells/spell-todo.md +84 -2
  23. package/dist/index.js +1170 -382
  24. package/dist/index.js.map +1 -1
  25. package/package.json +1 -1
package/README.md CHANGED
@@ -220,6 +220,43 @@ spell init --profile full
220
220
  spell add agent-policies # or install any component à la carte
221
221
  ```
222
222
 
223
+ ### Setup questions, answered by flags
224
+
225
+ `spell init` asks a few questions about the repository and records the answers in `.arcane.json`; `spell update` asks any that an older install predates. Each question has a flag, on both commands, so a script or an agent harness can answer without a terminal:
226
+
227
+ | Flag | Answers |
228
+ | --- | --- |
229
+ | `--role <hub\|consumer>` | Does this repository manage other ventures as a hub? |
230
+ | `--tracking-mode <internal\|external>` | Is work tracked in this repository, or in an external tracker? |
231
+ | `--external-provider <ado\|github\|jira\|other>` | Which tracker. Only with `--tracking-mode external`, which requires it. |
232
+ | `--content-sensitivity <standard\|sensitive>` | May agents quote this repository's contents? |
233
+ | `--push-policy <open\|guarded\|blocked>` | May this repository push to a remote? See [Push safety](#push-safety). |
234
+ | `--subject-root <path>` | Which directory holds the subject's documents (docs profile). |
235
+
236
+ ```bash
237
+ spell init --profile lite --tracking-mode internal --push-policy guarded
238
+ spell update --role consumer --content-sensitivity standard
239
+ ```
240
+
241
+ A flag answers its question with no prompt, even under `--profile`; a question without a flag is asked on a terminal and skipped without one. A run that skips questions prints the exact `spell update` line that answers them. Values are checked by the same validator as `.arcane.json`, and a bad one exits 1.
242
+
243
+ A flag only ever answers a question that has no recorded answer yet. To change a recorded answer, edit `.arcane.json` deliberately; the flag is refused and names the current value. The one exception is `--push-policy`, which may also **tighten** a recorded policy (`open` → `guarded` → `blocked`). It can never loosen one.
244
+
245
+ ### Push safety
246
+
247
+ A repository whose history must never reach a remote records `push_policy: "blocked"`, and gets two controls: a pre-push hook, and a disabled push URL on every remote. `guarded` installs nothing; `spell doctor` reports it, and the spells that push state it and ask first.
248
+
249
+ ```bash
250
+ spell block-push # enforce "blocked" on an initialized repository
251
+ spell unblock-push # lift it: interactive terminal only, and you type the repository name
252
+ ```
253
+
254
+ - **`spell block-push`** installs exactly what `spell init` installs for `blocked`, with the same refusals. It never takes over a `core.hooksPath` that another hook manager owns, and it refuses when a push URL is configured outside the repository. It records `blocked` only once both controls are in force, so a refusal changes nothing. Running it again on an enforced repository reports that and changes nothing. It needs no terminal, because it can only tighten.
255
+ - **`spell update` never installs push controls.** When it records `blocked`, from a question or from `--push-policy`, it tells you the policy is not enforced yet and to run `spell block-push` ([ARC-049](DECISIONS.md#arc-049--enforcing-a-recorded-push-policy-after-init)).
256
+ - **Loosening is only possible through `spell unblock-push`, from an interactive terminal.** No flag, script or other command can move a repository from `blocked` or `guarded` toward `open`: `spell update --push-policy open` on a blocked repository exits 1 and points you at `spell unblock-push`.
257
+
258
+ These controls stop an accidental push, not a determined operator ([ARC-034](DECISIONS.md#arc-034--push-safety-for-sensitive-repositories)).
259
+
223
260
  ### Once per machine
224
261
 
225
262
  Working across many Arcane repositories at once? Install the spells a single time at the **user tier**, instead of (or as well as) per repository:
@@ -2,7 +2,7 @@
2
2
  title: Agent Approved Paths Registry
3
3
  audience: both
4
4
  last_updated: YYYY-MM-DD
5
- status: active
5
+ status: template
6
6
  distributable: true
7
7
  tags: [agent, security, filesystem, autonomy]
8
8
  ---
@@ -36,7 +36,43 @@ Arcane separates framework-level decisions from org-specific ones using two pref
36
36
  - If the decision would apply to any organization using Arcane, it is `ARC-NNN`.
37
37
  - If the decision is specific to how your organization operates, it is `ADR-NNN`.
38
38
  - When uncertain, default to `ADR-NNN` in ops; promote to `ARC-NNN` later if the decision proves framework-generic. **Enforcement: explicitly advisory prose (ARC-023) — whether a decision is framework-generic enough for `ARC-NNN` (this rule and the two prefix-scoping rules above) is a judgment call; no script evaluates a decision's content to confirm the chosen prefix.**
39
- - Never renumber existing entries in either sequence. **Enforcement: explicitly advisory prose (ARC-023) — `scripts/check-distributed-adr-references.ts` only verifies that a bare `ADR-NNN` citation resolves to a heading declared in `framework-decisions.md` (and separately flags certain `ARC-NNN`/`EF-NNN` link forms as cross-repo hazards); it never checks that numbers are sequential, unique, or never reused.**
39
+ - Never renumber existing entries in either sequence. **Enforcement: explicitly advisory prose (ARC-023) — `scripts/check-distributed-adr-references.ts` only verifies that a bare `ADR-NNN` citation resolves to a heading declared in `framework-decisions.md` (and separately flags certain `ARC-NNN`/`EF-NNN` link forms as cross-repo hazards); it never checks that numbers are sequential, unique, or never reused. Uniqueness alone has an executable check in Arcane's own repository only: `scripts/check-stale-claims.ts` (run in CI as `npm run check:stale-claims`) fails when two decision headings in its `DECISIONS.md` share an ID (ARC-050). That check does not ship to consuming repositories, and it cannot tell a renumbered entry from a new one.**
40
+
41
+ ## Allocating Decision Numbers
42
+
43
+ Decision numbers are a shared sequence (ARC-028 R4). Two sessions on separate branches that each take
44
+ "the next number" both take the same one. The collision surfaces at merge, or never, and
45
+ cross-references then point at the wrong decision. ARC-050 sets how a number is allocated:
46
+
47
+ 1. **Fetch, then take the maximum.** Before allocating a decision number, fetch the trunk when a
48
+ usable remote exists. "Usable" is the classification `spell-open-session`'s Mutation Guard uses: a
49
+ supported provider with authenticated provider tooling, where a remote URL alone is not enough.
50
+ Then allocate `max(highest number on the local branch, highest number on the remote trunk) + 1`.
51
+ 2. **A local-only repository allocates from the local maximum and says so**, so the number is
52
+ visibly unchecked against any other branch.
53
+
54
+ **Enforcement (rules 1 and 2): structured spell gate (ARC-023) — `spell-close-session` step 3
55
+ requires the observed trunk maximum, or its stated local-only line, before it writes a new entry.
56
+ A decision written outside that spell, by hand or by another workflow, follows these rules as
57
+ explicitly advisory prose: nothing else fetches or compares for it.**
58
+ 3. **No reservation markers.** Never commit a placeholder or "reserved" entry to claim a number. A
59
+ marker is itself an edit to the same shared sequence, so it collides the same way. **Enforcement:
60
+ explicitly advisory prose (ARC-023) — no script recognizes a placeholder entry as a reservation.**
61
+ 4. **Several decisions at once are allocated once.** A program that drafts several decisions
62
+ allocates all of them at the start of the wave that needs them, from the fetched trunk. Its
63
+ parallel lanes use those numbers and never allocate their own. A single session that records
64
+ several decisions allocates them consecutively from one fetched maximum. **Enforcement:
65
+ explicitly advisory prose (ARC-023) — no spell gate or script tracks which numbers a program
66
+ pre-allocated.**
67
+ 5. **When a collision still happens, the unmerged branch renumbers.** Fetching narrows the race but
68
+ does not close it: two sessions can fetch before either pushes. "Never renumber existing entries"
69
+ applies to merged entries. When two branches hold the same number, the branch that has not merged
70
+ yet renumbers its own entry and every reference to it on that branch. **Enforcement: explicitly
71
+ advisory prose (ARC-023) for the renumbering — no script performs it or checks which branch did
72
+ it. Detecting the collision is an executable check in Arcane's own repository only:
73
+ `scripts/check-stale-claims.ts`, run in CI as `npm run check:stale-claims`, fails when two
74
+ decision headings share an ID. It matches any `## <PREFIX>-<number>` heading, not only `ARC`, so
75
+ a consuming repository can adopt it, but it is not installed there.**
40
76
 
41
77
  ## Problem Statement
42
78
 
@@ -161,6 +161,26 @@ GitHub Issues has no configurable work-item-type hierarchy the way Azure DevOps
161
161
  - **Jira (`external_provider=jira`)** — TODO: define issue-type mapping, parent-child/epic linkage strategy, and command examples.
162
162
  - **Other providers (`external_provider=other`)** — TODO: define required metadata contract and fallback behavior before enabling automation.
163
163
 
164
+ ### Tracker-ID Suffix on Captured Items
165
+
166
+ When `tracking_mode=external` and a `TODO.md` item or an `IDEAS.md` entry corresponds to a tracker work item that already exists, the item carries that work item's id as a suffix, in the provider's own reference format. This is the one definition of the suffix; `spell-todo`, `spell-save-idea` and `spell-manifest` point here rather than restating it.
167
+
168
+ | `external_provider` | Suffix | Example |
169
+ | --- | --- | --- |
170
+ | `github` | `[#<number>]` | `- [ ] Add export button [#123]` |
171
+ | `ado` | `[AB#<id>]` | `- [ ] Add export button [AB#123]` |
172
+ | `jira` | `[<issue key>]` — the key exactly as Jira prints it | `- [ ] Add export button [PROJ-123]` |
173
+ | `other` | no suffix until that provider's metadata contract is defined (see External Provider TODOs) | — |
174
+
175
+ - **Placement:** at the end of the item's text, after any file reference, and before a trailing `<!-- status: … -->` comment on an idea entry.
176
+ - **Only a real id:** the suffix records an id that exists — one the operator supplied, or one a spell just got back from the tracker. Never guess an id, and never file a work item just to have a suffix.
177
+ - **One suffix per item:** an item that already carries a suffix keeps it; it is not duplicated or rewritten.
178
+ - **`tracking_mode=internal`:** no suffix. Items are written exactly as they are without this section.
179
+
180
+ The suffix uses the same reference forms as the commit-link syntax `spell-bug` writes (`Fixes AB#{id}` for Azure DevOps, `Fixes #{id}` for GitHub Issues — the GitHub form is also under GitHub Issues Conventions above), so an item and the commit that closes it point at the work item the same way.
181
+
182
+ **Enforcement: explicitly advisory prose (ARC-023) — no script checks that an externally tracked item carries its suffix or that a suffix matches the provider's format; the three spells apply it at write time and compliance depends on the executing agent.**
183
+
164
184
  ### Fresh Context Per Iteration
165
185
 
166
186
  Each implementation iteration starts with clean context. The only memory between iterations is:
@@ -296,7 +296,7 @@ The agent slug prefix makes branch ownership obvious in `git branch -r` output a
296
296
  instead of the `spell-create-pull-request` spell.**
297
297
  ```bash
298
298
  git fetch origin
299
- git rebase origin/main
299
+ git rebase --autostash origin/main
300
300
  # If conflicts arise: resolve → git add → git rebase --continue
301
301
  ```
302
302
  This ensures the PR diff is clean, the CI runs against current main, and reviewers see no
@@ -542,13 +542,31 @@ If your tracker links commits to work items by ID (Azure Boards, GitHub Issues,
542
542
 
543
543
  ---
544
544
 
545
+ ## Push safety
546
+
547
+ A repository that holds sensitive material can record how far it trusts a push, in the `push_policy` field of `.arcane.json`. One accidental push can publish a repository's whole history, and deleting the content afterwards does not undo that. The threat this guards against is an **accidental** push (the wrong remote, habit carried over from another repository, an unsupervised agent). It does not stop a determined operator: every control below can be reversed by someone who decides to reverse it. See ARC-034 and ARC-049 in Arcane's own decision log for the full reasoning. **Enforcement: explicitly advisory prose (ARC-023) — this paragraph describes the model; the rules below carry their own annotations.**
548
+
549
+ | `push_policy` | Meaning | What is installed |
550
+ | --- | --- | --- |
551
+ | `open` (the default, and what an absent field means) | Pushes proceed as normal. | Nothing. |
552
+ | `guarded` | Every push needs the operator's explicit confirmation. | Nothing. `spell doctor` reports the policy and the push targets, and push-performing spells ask before each push. |
553
+ | `blocked` | No push is attempted. | A `pre-push` hook, and a sentinel push URL on every remote configured when the policy is applied. Fetch URLs are untouched, so the repository can still pull. |
554
+
555
+ - **Set:** `spell init` asks the question once and records the answer. `spell update` asks it in a repository that has no answer yet, records the answer, and installs nothing mid-update; when it records `blocked` it says the policy is not yet enforced and names `spell block-push`. Both commands also take `--push-policy <open|guarded|blocked>`, which may set an unset policy or tighten one, never loosen it. **Enforcement: executable check (ARC-023) — the CLI validates the value against the three allowed ones and refuses a loosening flag; the question and notice text are the CLI's own output.**
556
+ - **Enforce:** `spell block-push` installs the `blocked` controls, the same ones `spell init` installs, and records `blocked` if the repository was `open` or `guarded`. It only tightens, so it needs no interactive terminal, and it is idempotent. As `spell init` does, it refuses rather than overwrite a `core.hooksPath` another hook manager owns; unlike `spell init`, it then touches no push URL. A remote whose push URL is configured outside the repository cannot be covered: the command protects everything else it can, leaves the manifest's policy unchanged, and exits non-zero, so the record never claims a protection the repository lacks. When `spell init` itself cannot put a recorded block fully in force, it names `spell block-push` and exits non-zero. **Enforcement: executable check (ARC-023) — the pre-push hook and the sentinel push URL refuse the push at the git level; the hook alone is skipped by `--no-verify`, and the URL alone is skipped by a push to an explicit URL, which is why both are installed.**
557
+ - **Verify:** `spell doctor` reports a `guarded` policy on every run, with the remotes a push would reach, and reports a `blocked` policy whose hook or push URLs are missing as declared but not enforced. **Enforcement: executable check (ARC-023) — `spell doctor`'s push-policy check reads the effective hook and push-URL state; it is a non-blocking finding the operator runs on demand, not a CI gate.**
558
+ - **Lift:** `spell unblock-push` is the only way to loosen a policy. It requires an interactive terminal and the repository name typed back, and records `open` with a timestamp. No flag, spell, or non-interactive path may move a repository from `blocked` or `guarded` toward `open`, and `spell uninstall` refuses while a block stands. **Enforcement: executable check (ARC-023) — the command refuses without an interactive terminal; a pseudo-terminal defeats that check, so it bars scripts rather than a determined actor.**
559
+ - **Read by spells:** every spell that pushes — `spell-commit-work`, `spell-create-pull-request`, `spell-ship`, `spell-sync-pull-request`, `spell-close-session` and `spell-address-review` — reads `push_policy` before each push, through the shared `_fragments/push-policy-check.md` text. Under `guarded` it states the policy and asks the operator before the push; in an autonomous run with no operator to ask, it does not push and reports the pending push. Under `blocked` it does not attempt the push, says why, and names `spell unblock-push`. **Enforcement: structured spell gate (ARC-023) — for `guarded`, the spell's confirmation step is the only control, and an agent that pushes outside these spells is not held by it; for `blocked`, the installed controls above refuse the push as well.**
560
+
561
+ ---
562
+
545
563
  ## PR Standards
546
564
 
547
565
  All pull requests — whether created by humans or agents — must meet these requirements.
548
566
 
549
567
  ### 🛑 Agent-mandatory pre-PR guard
550
568
 
551
- > **AGENTS: BEFORE running any PR-creation tool (`az repos pr create`, `gh pr create`, the `create_pull_request` MCP tool, or any equivalent), you MUST run `git fetch origin && git rebase origin/<target-branch>` and resolve any conflicts. This is not optional. Skipping this step is a governance violation. Enforcement: structured spell gate (ARC-023) — `spell-create-pull-request` Step 0's mandatory rebase guard (its Step 0.6) and `spell-commit-work` Step 9b both require this fetch/rebase sequence before any PR-creation call — backed by an executable check: `.github/workflows/ci.yml`'s `rebase-check` job ("PR branch is rebased on target") independently re-verifies `origin/<base>` is an ancestor of HEAD via `git merge-base --is-ancestor` on every PR, and this repo's own live GitHub ruleset ("protect main") lists that exact check in `required_status_checks` with `current_user_can_bypass: "never"` — an unrebased PR cannot merge even if the spell-level guard were skipped.**
569
+ > **AGENTS: BEFORE running any PR-creation tool (`az repos pr create`, `gh pr create`, the `create_pull_request` MCP tool, or any equivalent), you MUST run `git fetch origin && git rebase --autostash origin/<target-branch>` and resolve any conflicts. This is not optional. Skipping this step is a governance violation. Enforcement: structured spell gate (ARC-023) — `spell-create-pull-request` Step 0's mandatory rebase guard (its Step 0.6) and `spell-commit-work` Step 9b both require this fetch/rebase sequence before any PR-creation call — backed by an executable check: `.github/workflows/ci.yml`'s `rebase-check` job ("PR branch is rebased on target") independently re-verifies `origin/<base>` is an ancestor of HEAD via `git merge-base --is-ancestor` on every PR, and this repo's own live GitHub ruleset ("protect main") lists that exact check in `required_status_checks` with `current_user_can_bypass: "never"` — an unrebased PR cannot merge even if the spell-level guard were skipped.**
552
570
 
553
571
  This rule applies **every single time**, regardless of:
554
572
 
@@ -560,11 +578,15 @@ This rule applies **every single time**, regardless of:
560
578
 
561
579
  ```bash
562
580
  git fetch origin
563
- git rebase origin/<target-branch> # resolve conflicts locally, never push a conflicted branch
581
+ git rebase --autostash origin/<target-branch> # resolve conflicts locally, never push a conflicted branch
564
582
  git push --force-with-lease # only if the branch existed remotely before the rebase
565
583
  # ...only THEN run the PR-creation tool of your choice
566
584
  ```
567
585
 
586
+ `--autostash` stashes uncommitted changes before the rebase and re-applies them afterwards, so a dirty working tree does not stop the rebase before it starts. If re-applying them conflicts, git leaves them in the stash and says so; report that before going further.
587
+
588
+ **Keep the PR body safe across a halted rebase:** never create a PR whose body is empty. A flow that drafts the body before its pre-PR rebase must write the PR body file before the rebase (`<git-dir>/arcane-pr-body.md`, resolved with `git rev-parse --git-dir`; the `.git` directory is never committed and a rebase does not touch it), so a rebase that halts cannot lose it. A flow that builds the body afterwards, from the rebased commit log, must check the file when it reaches the PR-creation step and rebuild it if it is missing or empty — that is the case a halted-then-resumed flow lands in. Enforcement: structured spell gate (ARC-023) — `spell-commit-work` writes the file at Step 9a2, before its Step 9b rebase, and its raw-CLI paths (Steps 9d/9e) refuse a missing or empty file; `spell-create-pull-request` Step 5 refuses a missing or empty file and sends the agent back to its Step 4. No executable check verifies that a created PR's description is non-empty: `spell-create-pull-request`'s post-create read-back is the only re-check, and it is a spell step too.
589
+
568
590
  If a rebase produces conflicts the agent cannot confidently resolve, **STOP** and hand off to the human — do **not** open the PR and hope reviewers will sort it out. Pushing a branch that will produce a merge conflict on the target is a governance violation and wastes reviewer time.
569
591
 
570
592
  The `spell-create-pull-request` spell encodes this check in Step 0, but the guard is on **the agent**, not on the spell. Bypassing the spell (by calling `az repos pr create` / `gh pr create` directly) does **not** bypass this rule.
@@ -2,7 +2,7 @@
2
2
  title: Naming Conventions
3
3
  audience: both
4
4
  last_updated: YYYY-MM-DD
5
- status: active
5
+ status: template
6
6
  distributable: true
7
7
  tags: [naming, convention, machines, agents, identity]
8
8
  ---
@@ -173,6 +173,46 @@ Arcane spells ship to other repositories and, eventually, open source. Keep them
173
173
  > never candidates for that scan regardless of this clause, and no check specifically verifies the
174
174
  > carve-out itself.**
175
175
 
176
+ ### Runtime placeholders (ARC-051)
177
+
178
+ A `{UPPER_SNAKE}` token in a shipped governance document is one of two kinds
179
+ ([ARC-051](https://github.com/codemagicianhq/arcane/blob/main/DECISIONS.md#arc-051--placeholder-taxonomy-for-governance-documents)):
180
+
181
+ - **Runtime-resolved:** an agent resolves it when it uses the document, from the source named in the
182
+ list below; if that source is unset, it asks (the resolution rule above). These tokens are legal in a
183
+ `status: active` document, and the list below is the only place they are enumerated.
184
+ - **Fill-in:** a slot the operator replaces by hand. A document built around such slots carries
185
+ `status: template` (for example `agent-approved-paths.md` and `naming-conventions.md`), and the
186
+ placeholder check skips it.
187
+
188
+ A token that is neither is **unknown**: a typo, or a slot nobody filled. To make a new token legal,
189
+ add it to the list in the same form: one item per token, a code span holding the token first, then
190
+ where it resolves from. The list is read by machine, so keep it between the two markers.
191
+
192
+ <!-- runtime-placeholders:start -->
193
+ - `{ADO_ORG}` — the Azure DevOps organization: `tracking.ado.org` in the PRD frontmatter or `.arcane.json` ([[development-methodology]]).
194
+ - `{ADO_PROJECT}` — the Azure DevOps project: `tracking.ado.project`, from the same places.
195
+ - `{AGENT_EMAIL}` — the acting agent's commit email: the agent roster ([[agent-policies]]); ask if unset.
196
+ - `{AGENT_NAME}` — the acting agent's name: the agent roster ([[agent-policies]]); ask if unset.
197
+ - `{BUSINESS_NAME}` — the business or venture the work is for: the project's own context (its README or PRD frontmatter); ask if unset.
198
+ - `{HOST}` — the hostname of the machine being configured: the machine itself.
199
+ - `{LLC_NAME}` — the operator's legal entity: the project's own context; ask if unset.
200
+ - `{OPERATOR_DOMAIN}` — the domain the operator uses for agent and tool Git identities: ask if unset.
201
+ - `{OPERATOR_EMAIL}` — the operator's email: `git config user.email`.
202
+ - `{OPERATOR_NAME}` — the operator's name: `git config user.name`.
203
+ - `{OPERATOR_USERNAME}` — the operator's account on the host: the machine itself.
204
+ - `{ORG}` — the organization or owner that hosts the repositories: the repository's remote URL.
205
+ - `{PUBLIC_VISIBILITY_PREDICATE}` — the application's own public-visibility rule: the codebase (WD-07 in [[web-discoverability-standards]]).
206
+ - `{THE_REAL_VALUE}` — a secret's real value in an uncommitted `.env` example: the operator's secret store; never a committed file.
207
+ <!-- runtime-placeholders:end -->
208
+
209
+ **Enforcement: executable check (ARC-023) — in an installed repository, `spell doctor` warns (never
210
+ fails; the exit code is unchanged) on each token in a `status: active` `.arcane/governance/` document
211
+ that is not in this list, naming the file; it skips the check when this list is not installed. In
212
+ Arcane's own repository, a test (`test/up02-d-runtime-placeholders.test.ts`) fails when a shipped
213
+ `status: active` governance document uses a token that is not in this list. Whether an agent actually
214
+ resolves a listed token from the named source is explicitly advisory prose: no check exercises it.**
215
+
176
216
  ## How to audit a spell
177
217
 
178
218
  For each dimension, score Bronze/Silver/Gold with one line of evidence. Record the overall (weakest)
@@ -99,7 +99,7 @@ See also: [[README]], [[DECISIONS]], [[governance/git-conventions|Git Convention
99
99
  - This is a governance default, not an access control. It constrains what
100
100
  agents *write down*; it does not restrict what they may read, and it is
101
101
  not a substitute for repository permissions or the push-safety controls in
102
- `git-conventions.md`.
102
+ `git-conventions.md#push-safety` (the "Push safety" section).
103
103
  - Absent or `"standard"` means normal behaviour — this rule adds nothing.
104
104
 
105
105
  ---
@@ -0,0 +1,7 @@
1
+ **Push-policy check (ARC-049) — before any push.** Read `push_policy` from the repository's `.arcane.json` at the repository root (a self-hosted source repository with no root manifest keeps it in `src/assets/.arcane.json` when that file declares `selfHosted: true`). An absent file or an absent field means `open`. The check covers every push this spell makes: a branch push, a `--force-with-lease` push, and a remote branch deletion (`git push <remote> --delete <branch>`).
2
+ - **`open` or absent:** proceed with the push.
3
+ - **`guarded`:** before the push, state the policy ("This repository's push policy is `guarded`: every push needs your explicit confirmation."), name the remote, the branch and the kind of push, and ask the operator for explicit confirmation. Push only on an explicit operator confirmation of that push. A timeout, a cancellation, a delegated or host-generated response, or assent given to something else is not confirmation. **In an autonomous run with no operator to ask, do not push:** report the push as pending operator confirmation, naming the branch and the commits it would carry. This fails closed.
4
+ - **`blocked`:** do not attempt the push. Say why: the repository's push policy is recorded as `blocked`, so pushes are refused on purpose. Name `spell unblock-push` as the only way to lift it, run by the operator from an interactive terminal. Never run it yourself, and never work around the block (`--no-verify`, another remote or URL, editing git config or `.arcane.json`).
5
+ - **Any other value, or a `.arcane.json` that cannot be parsed:** do not push. Report what was found and suggest `spell doctor`.
6
+ - When a push does not happen, skip every step that depends on it (opening or updating a PR, remote branch cleanup) and report the commits as committed locally and not pushed.
7
+ - Enforcement: structured spell gate (ARC-023) — for `guarded`, this check is the only thing that holds a push for confirmation. A `blocked` push is also refused by the pre-push hook and the sentinel push URL that `spell init` or `spell block-push` install, and `spell doctor` reports a `blocked` policy whose controls are missing.
@@ -100,9 +100,21 @@ Resolve threads via the provider:
100
100
  ## Step 6 — Sweep, push, report
101
101
 
102
102
  1. Re-fetch threads; resolve any still-active ones (this is the merge gate on many providers). Any thread that genuinely cannot be auto-resolved (see Step 5) is reported as "needs manual resolution," not silently left open.
103
- 2. `git push` all commits.
104
- 3. Print a summary table of actions taken (category, file, action, commit), thread status (resolved / deferred-with-TODO / needs manual resolution), commits pushed, and next steps (CI, notify reviewer, ready for re-review).
105
- 4. Optionally post the summary back to the PR as a top-level comment for a durable record.
103
+ 2. Before pushing, run the push-policy check:
104
+
105
+ <!-- fragment:push-policy-check:start -->
106
+ **Push-policy check (ARC-049) — before any push.** Read `push_policy` from the repository's `.arcane.json` at the repository root (a self-hosted source repository with no root manifest keeps it in `src/assets/.arcane.json` when that file declares `selfHosted: true`). An absent file or an absent field means `open`. The check covers every push this spell makes: a branch push, a `--force-with-lease` push, and a remote branch deletion (`git push <remote> --delete <branch>`).
107
+ - **`open` or absent:** proceed with the push.
108
+ - **`guarded`:** before the push, state the policy ("This repository's push policy is `guarded`: every push needs your explicit confirmation."), name the remote, the branch and the kind of push, and ask the operator for explicit confirmation. Push only on an explicit operator confirmation of that push. A timeout, a cancellation, a delegated or host-generated response, or assent given to something else is not confirmation. **In an autonomous run with no operator to ask, do not push:** report the push as pending operator confirmation, naming the branch and the commits it would carry. This fails closed.
109
+ - **`blocked`:** do not attempt the push. Say why: the repository's push policy is recorded as `blocked`, so pushes are refused on purpose. Name `spell unblock-push` as the only way to lift it, run by the operator from an interactive terminal. Never run it yourself, and never work around the block (`--no-verify`, another remote or URL, editing git config or `.arcane.json`).
110
+ - **Any other value, or a `.arcane.json` that cannot be parsed:** do not push. Report what was found and suggest `spell doctor`.
111
+ - When a push does not happen, skip every step that depends on it (opening or updating a PR, remote branch cleanup) and report the commits as committed locally and not pushed.
112
+ - Enforcement: structured spell gate (ARC-023) — for `guarded`, this check is the only thing that holds a push for confirmation. A `blocked` push is also refused by the pre-push hook and the sentinel push URL that `spell init` or `spell block-push` install, and `spell doctor` reports a `blocked` policy whose controls are missing.
113
+ <!-- fragment:push-policy-check:end -->
114
+
115
+ 3. `git push` all commits.
116
+ 4. Print a summary table of actions taken (category, file, action, commit), thread status (resolved / deferred-with-TODO / needs manual resolution), commits pushed, and next steps (CI, notify reviewer, ready for re-review).
117
+ 5. Optionally post the summary back to the PR as a top-level comment for a durable record.
106
118
 
107
119
  ## Rules
108
120
 
@@ -70,6 +70,32 @@ If the npm registry is unreachable, report:
70
70
  ℹ Could not reach npm registry — update check skipped.
71
71
  ```
72
72
 
73
+ ### Setup questions from an AI harness (no interactive terminal, PRD D-02 / #282)
74
+
75
+ This spell itself stays read-only. If the operator then asks you to run `spell update`, note that a
76
+ harness shell is not an interactive terminal. The CLI then skips every unanswered manifest question
77
+ and prints ``! Skipped N manifest question(s) (<fields>) — no interactive terminal. Run `spell update` from a terminal, or answer by flags:``,
78
+ followed by the exact line to re-run, such as
79
+ `spell update --tracking-mode <internal|external> --push-policy <open|guarded|blocked>`.
80
+ `spell init --profile <profile>` prints the same kind of `spell update` line when it leaves
81
+ questions unanswered. Do not send the operator off to find a terminal. Instead:
82
+
83
+ 1. Ask the operator each skipped question through the harness's own question UI (a plain chat
84
+ question if the harness has none): one question per flag the CLI printed, offering exactly the
85
+ values that flag lists. The flags are `--role`, `--tracking-mode`, `--external-provider`,
86
+ `--content-sensitivity`, `--push-policy` and `--subject-root`. `--tracking-mode external` also
87
+ needs `--external-provider <ado|github|jira|other>`, so ask that too when the answer is
88
+ `external`.
89
+ 2. Re-run `spell update` with the flags the CLI printed, filled with the operator's answers, for
90
+ example `spell update --tracking-mode internal --push-policy guarded`.
91
+ 3. Never pass a flag the operator did not answer. No guessed, default or "recommended" value stands
92
+ in for an answer. An unanswered question stays unset: the next interactive run asks it, and the
93
+ next harness run reports it skipped again.
94
+ 4. Never loosen `push_policy` by flag. `--push-policy` only answers an unset question or tightens a
95
+ recorded policy, and the CLI refuses a loosening. Moving a repository from `blocked` or `guarded`
96
+ toward `open` is only ever `spell unblock-push`, which the operator runs from an interactive
97
+ terminal (ARC-034 decision 6, ARC-049 decision 6). Tell them so; do not try to run it for them.
98
+
73
99
  ## Step 3 — Component Detail (optional)
74
100
 
75
101
  Only include this section when the user passed "components" as the prompt argument.
@@ -51,6 +51,7 @@ Concrete detectors to run (in addition to general contradiction/stale-date check
51
51
  - If normalized content differs, report **real content drift** with normal severity grading.
52
52
  - If normalized content matches, report **line-ending-only drift** separately. Do not count these paths as real content findings and do not include them in severity totals or the Go / No-Go decision.
53
53
  - Report exact path counts for both classes. Never summarize all byte-different files as content drift.
54
+ - **Unfilled governance placeholders** (ARC-051) — `spell doctor` owns this check: it warns on each `{UPPER_SNAKE}` token in a `status: active` `.arcane/governance/` document that is not in the runtime-placeholders list in `spell-authoring-standards.md`, and skips `status: template` documents. Run `spell doctor` and report each file and token its "Governance placeholders (ARC-051)" row names as a **Low** finding; do not re-implement the scan here. If `spell doctor` cannot run, or reports that check as skipped, say so in the report and move on.
54
55
  - **Hub-artifact leak (non-hub repos)** — if `.arcane.json` does not have `role: "hub"`, the existence of any `{BUSINESS_ROOT}/*/IDEAS.md`, `{BUSINESS_ROOT}/*/TODO.md`, or `{BUSINESS_ROOT}/registry.json` (resolve `{BUSINESS_ROOT}` the same way as above -- a consumer repo can still have a configured `business_root` left over from a prior role change, so this must not skip resolution just because the repo isn't currently a hub) is **Critical**: hub-only artifacts (potentially including sibling-venture and portfolio data) do not belong in a repo that isn't a declared hub. This check is deliberately structural (file existence only) — a non-hub repo cannot safely hold the sibling-name list a content-level check would need, so that asymmetry is by design, not a gap.
55
56
  - **Hub role/registry consistency (hub repos only)** — when `.arcane.json` has `role: "hub"`:
56
57
  - `role: "hub"` with no `{BUSINESS_ROOT}` directory (default `ventures/`) present — **High**: the manifest claims a role the repo can't actually fulfill.
@@ -53,13 +53,39 @@ Workflow:
53
53
  - Journal entries must include:
54
54
  - `### Prompt Context` — original prompt text or a faithful paraphrase of the multi-turn request when the session began from a substantive user ask. Include follow-up scope inputs if they materially changed the direction of the work.
55
55
  - `### What Got Done` — numbered list of concrete outcomes with file/ADR references. Only list an outcome here if step 1b classified it `succeeded` (or it never involved async work at all, e.g. a local file edit). A `pending`/`dispatched`/`unverifiable` item belongs in Open Items Carried Forward, not here — do not let this section imply completion step 1b couldn't confirm.
56
- - `### Decisions Made` — table of ADR number, decision, and rationale. If no decisions were made, omit this section.
56
+ - `### Decisions Made` — table of ADR number, decision, and rationale. If no decisions were made, omit this section. Take every number in this table from step 3's allocation rule (ARC-050). If step 3 has not allocated yet, run that allocation now and use the same numbers in both places. Never write a number from memory or from this branch's `DECISIONS.md` alone.
57
57
  - `### Lessons Learned` — one heading per lesson with enough context to prevent the mistake again. Use narrative, not bullet fragments.
58
58
  - `### Open Items Carried Forward` — items that remain undone from this session, including every `pending`/`dispatched`/`failed`/`unverifiable` item from step 1b.
59
59
 
60
+ 2b. **Lesson routing: send each lesson where it can be acted on.** A lesson left only in the journal never reaches the people who could fix its cause. Skip this step when step 2 wrote no `### Lessons Learned`, or when the invocation includes `--no-route`. With `--no-route`, the lessons stay in the journal only; say `Lesson routing skipped (--no-route)` in the closure report.
61
+ - **Classify each lesson** written in step 2 into exactly one class:
62
+ - **Framework-shaped:** the fix belongs in Arcane itself, meaning a spell prompt, template, governance doc, playbook, or CLI behavior this repository installed rather than authored. This is `spell-feedback` Step 3's definition. Destination: `FEEDBACK.md`, queued for upstream.
63
+ - **Product idea:** the lesson suggests something to build in this repository's own product. Destination: `spell-save-idea`.
64
+ - **Repo-local:** anything about this repository's own content, configuration, or way of working. Destination: the journal only. It is already there, so nothing more is written.
65
+ - **Show one batch proposal and ask once.** List every lesson with its class and destination, then ask a single question: `Route these lessons? (yes / edit / skip)`. `edit` lets the operator change a class or drop a lesson, then shows the proposal again. Route nothing before an explicit `yes`. A timeout, a cancellation, a delegated or host-generated response, or assent given to something else is not approval. Neither is an autonomous run with no operator to ask. In each of those cases route nothing, keep the lessons in the journal, and list the proposal in the closure report as not routed. `skip` is a complete, valid answer.
66
+ - **Framework-shaped, after `yes`:** append **one** block to `FEEDBACK.md` at the repository root, in `spell-feedback`'s Step 5 block format. Create the file with a `# Feedback Log` header first if it is missing. Fill the block this way:
67
+ - Heading: `## Feedback — <the spell or doc the lessons concern, or "Arcane workflow" when they span several> (<YYYY-MM-DD>)`.
68
+ - `**Session:**` names this session and adds `(routed by spell-close-session from Lessons Learned)`.
69
+ - `**Rating:**` and `**Would use again:**` are `N/A`, because nobody rated anything.
70
+ - Friction Points: one line per lesson saying what happened.
71
+ - Improvement Items: one imperative item per lesson, each line ending with `<!-- upstream: queued -->`.
72
+ - Raw Notes: cite the journal file and each lesson's heading.
73
+ - **Product idea, after `yes`:** hand each one to `spell-save-idea` as a one-sentence idea, citing the journal file. That spell appends it to the idea book.
74
+ - **No public write from this step.** Close-session never files an issue and never runs `gh issue create`. It also never runs `spell-feedback`'s Step 6 filing flow. Queuing is the whole outcome here. The `yes` above approves local appends only. Filing upstream still needs `spell-feedback`'s own literal `disclose` confirmation on each item, given in a later `spell-feedback` or `spell-feedback --flush` run, which re-offers every queued item through its genericize-and-`disclose` flow.
75
+ - **Append-only.** Add new entries to `FEEDBACK.md` and the idea book. Never edit, reorder, or remove an existing entry in either.
76
+ - Enforcement: structured spell gate (ARC-023) — nothing is routed without the one explicit batch `yes`, and it fails closed. The no-public-write and append-only rules are explicitly advisory prose: no hook stops this step from running `gh` or rewriting a book, so they hold by the step's own instructions. Classifying a lesson is a judgment call, and the operator checks it at the batch proposal.
77
+
60
78
  3. **Update [DECISIONS.md](../../DECISIONS.md).**
61
79
  - Add ADRs for any decisions made during the session (naming choices, tool preferences, workflow standards, security policies, etc.).
62
80
  - Use the next sequential ADR number.
81
+ - **Allocate that number against the trunk, not only this branch (ARC-050; ARC-028 R4 names decision numbers a shared sequence).** Another session on another branch may already have taken "the next number". Do this immediately before writing the first new entry:
82
+ - **Classify the remote first.** Use the classification from step 9's remote-capability check, which is the same one `spell-open-session`'s Mutation Guard uses. A usable remote is a supported provider (`github.com`, `dev.azure.com`, or `visualstudio.com`) with authenticated provider tooling; a remote URL alone is not enough. Resolve `<remote>` and `<trunk>` from observed state as step 10 does. Never assume `origin` or `main`.
83
+ - **Usable remote: fetch, then take the maximum.** Run `git fetch <remote> <trunk>`. Read the highest decision ID (the number in the log's `## <PREFIX>-NNN` headings) from this branch's `DECISIONS.md`, and from the trunk's copy with `git show <remote>/<trunk>:DECISIONS.md`. A trunk with no `DECISIONS.md` has no maximum. Allocate `max(highest ID on this branch, highest ID on <remote>/<trunk>) + 1`. In the Decision Updates output, name both maxima and the number allocated.
84
+ - **Local-only: allocate from the local maximum and say so.** When there is no usable remote, allocate `highest ID in the local DECISIONS.md + 1`. Say so in the Decision Updates output: `Allocated <PREFIX>-NNN from the local maximum only; not checked against any other branch.` Treat a usable remote whose fetch fails the same way, and name the failed fetch in that line.
85
+ - **Several decisions in one session:** allocate them consecutively from the one maximum you just computed, in a single pass.
86
+ - **No reservation markers.** Never commit a placeholder or "reserved" entry to claim a number before the real entry exists. A marker is itself an edit to the same shared sequence, so it collides the same way.
87
+ - **If a collision still surfaces later** (two sessions fetched before either pushed), the branch that has not merged yet renumbers its own entry and every reference to it on that branch. Merged entries are never renumbered (`decision-documentation-standard.md`).
88
+ - Enforcement: structured spell gate (ARC-023) — this step requires the observed trunk maximum, or the stated local-only line, before a new entry is written. The renumbering rule above is explicitly advisory prose: nothing in this spell detects a collision after the fact.
63
89
  - Follow the existing format: Date, Status, Context, Decision, Reasoning, Rejected alternatives.
64
90
 
65
91
  4. **Update [TODO.md](../../TODO.md).**
@@ -149,6 +175,18 @@ Rules:
149
175
  - Timeout, cancellation, fallback, or delegated response is not approval. Print the visible authorization downgrade and halt without committing.
150
176
 
151
177
  c. **Push and open a PR through `spell-commit-work` step 9, including its separate merge gate:**
178
+ - Run the push-policy check before the push. `spell-commit-work` step 9a runs the same check; run it once per push, not twice. It also governs step 10's remote branch deletions. If it stops the push, end the session with the commits local and report them in the PR Readiness section as not pushed.
179
+
180
+ <!-- fragment:push-policy-check:start -->
181
+ **Push-policy check (ARC-049) — before any push.** Read `push_policy` from the repository's `.arcane.json` at the repository root (a self-hosted source repository with no root manifest keeps it in `src/assets/.arcane.json` when that file declares `selfHosted: true`). An absent file or an absent field means `open`. The check covers every push this spell makes: a branch push, a `--force-with-lease` push, and a remote branch deletion (`git push <remote> --delete <branch>`).
182
+ - **`open` or absent:** proceed with the push.
183
+ - **`guarded`:** before the push, state the policy ("This repository's push policy is `guarded`: every push needs your explicit confirmation."), name the remote, the branch and the kind of push, and ask the operator for explicit confirmation. Push only on an explicit operator confirmation of that push. A timeout, a cancellation, a delegated or host-generated response, or assent given to something else is not confirmation. **In an autonomous run with no operator to ask, do not push:** report the push as pending operator confirmation, naming the branch and the commits it would carry. This fails closed.
184
+ - **`blocked`:** do not attempt the push. Say why: the repository's push policy is recorded as `blocked`, so pushes are refused on purpose. Name `spell unblock-push` as the only way to lift it, run by the operator from an interactive terminal. Never run it yourself, and never work around the block (`--no-verify`, another remote or URL, editing git config or `.arcane.json`).
185
+ - **Any other value, or a `.arcane.json` that cannot be parsed:** do not push. Report what was found and suggest `spell doctor`.
186
+ - When a push does not happen, skip every step that depends on it (opening or updating a PR, remote branch cleanup) and report the commits as committed locally and not pushed.
187
+ - Enforcement: structured spell gate (ARC-023) — for `guarded`, this check is the only thing that holds a push for confirmation. A `blocked` push is also refused by the pre-push hook and the sentinel push URL that `spell init` or `spell block-push` install, and `spell doctor` reports a `blocked` policy whose controls are missing.
188
+ <!-- fragment:push-policy-check:end -->
189
+
152
190
  - Commit approval is not merge approval.
153
191
  - Never invoke merge, auto-complete, or `--status completed` below loader-validated Magus authority.
154
192
  - Missing/invalid authority visibly downgrades to PR creation only and requires human completion.
@@ -212,9 +250,15 @@ Output format:
212
250
 
213
251
  - Exact file updates made.
214
252
 
253
+ ## Lesson Routing
254
+
255
+ - Each lesson from step 2b with its class and where it went: `FEEDBACK.md` (queued for upstream), `spell-save-idea`, or journal only.
256
+ - Or one of: `Lesson routing skipped (--no-route)`, `No lessons recorded`, or `Not routed (no explicit yes)` followed by the proposal.
257
+
215
258
  ## Decision Updates
216
259
 
217
260
  - ADRs added or note that none were needed.
261
+ - For each ADR added, how its number was allocated (step 3): `max(<this branch's highest>, <remote>/<trunk>'s highest) + 1`, or the local-only line.
218
262
 
219
263
  ## TODO Updates
220
264
 
@@ -158,19 +158,35 @@ Workflow:
158
158
 
159
159
  **Separate merge authorization gate:** commit approval never authorizes merge. Bind any interactive merge approval to the exact PR ID and current head SHA, and re-check both immediately before completion. Missing/changed approval invalidates the gate. Never invoke merge, auto-merge, auto-complete, or `--status completed` below loader-validated Magus authority; create/update the PR, print the visible downgrade, and leave completion to a human.
160
160
 
161
- a. Run `git push origin <branch>`.
161
+ a. **Push-policy check, then push.** Run this check before the push below. It also governs every later push in this step and in Step 10: the step 9b `--force-with-lease` push and `git push origin --delete <branch>`.
162
+
163
+ <!-- fragment:push-policy-check:start -->
164
+ **Push-policy check (ARC-049) — before any push.** Read `push_policy` from the repository's `.arcane.json` at the repository root (a self-hosted source repository with no root manifest keeps it in `src/assets/.arcane.json` when that file declares `selfHosted: true`). An absent file or an absent field means `open`. The check covers every push this spell makes: a branch push, a `--force-with-lease` push, and a remote branch deletion (`git push <remote> --delete <branch>`).
165
+ - **`open` or absent:** proceed with the push.
166
+ - **`guarded`:** before the push, state the policy ("This repository's push policy is `guarded`: every push needs your explicit confirmation."), name the remote, the branch and the kind of push, and ask the operator for explicit confirmation. Push only on an explicit operator confirmation of that push. A timeout, a cancellation, a delegated or host-generated response, or assent given to something else is not confirmation. **In an autonomous run with no operator to ask, do not push:** report the push as pending operator confirmation, naming the branch and the commits it would carry. This fails closed.
167
+ - **`blocked`:** do not attempt the push. Say why: the repository's push policy is recorded as `blocked`, so pushes are refused on purpose. Name `spell unblock-push` as the only way to lift it, run by the operator from an interactive terminal. Never run it yourself, and never work around the block (`--no-verify`, another remote or URL, editing git config or `.arcane.json`).
168
+ - **Any other value, or a `.arcane.json` that cannot be parsed:** do not push. Report what was found and suggest `spell doctor`.
169
+ - When a push does not happen, skip every step that depends on it (opening or updating a PR, remote branch cleanup) and report the commits as committed locally and not pushed.
170
+ - Enforcement: structured spell gate (ARC-023) — for `guarded`, this check is the only thing that holds a push for confirmation. A `blocked` push is also refused by the pre-push hook and the sentinel push URL that `spell init` or `spell block-push` install, and `spell doctor` reports a `blocked` policy whose controls are missing.
171
+ <!-- fragment:push-policy-check:end -->
172
+
173
+ When the check allows it, run `git push origin <branch>`.
162
174
 
163
175
  a1. **MCP fail-fast / fallback.** If an MCP tool used anywhere in this step (e.g. `create_pull_request`) fails abnormally once — a hang, an idle-timeout abort, a transport error, or an empty response where data is clearly expected — treat that server as down for the rest of this session. Do not retry it blindly; fall back to the raw CLI paths below (`gh pr create` / `az repos pr create`) and report the downgrade. Full rule: `.arcane/governance/git-conventions.md` → Known issues.
164
176
 
177
+ a2. **Write the PR body file first.** Before step 9b — the first step here that can halt — write the PR description to a file, so a rebase that stops (a conflict, a hand-off to `spell-sync-pull-request`, a resumed session) cannot lose it. Resolve the path with `git rev-parse --git-dir` (it handles worktrees) and write `<git-dir>/arcane-pr-body.md`, following the quality rules in 9f, with the first line `<!-- arcane-pr-body: <current-branch> -->` naming the branch it was written for; this is the same file `spell-create-pull-request` Step 4 writes. The `.git` directory is never committed and a rebase does not touch it, so the file survives a halted rebase. Confirm it is non-empty before continuing. If you delegate the PR to `spell-create-pull-request`, it writes this file itself at its Step 4.
178
+
165
179
  b. **🛑 Mandatory pre-PR rebase (governance guard, applies to every path below).** Before invoking any PR-creation command — whether via `spell-create-pull-request`, raw `gh pr create`, raw `az repos pr create`, or an MCP `create_pull_request` tool — you MUST:
166
180
 
167
181
  ```bash
168
182
  git fetch origin
169
- git rebase origin/<target-branch> # default: main
183
+ git rebase --autostash origin/<target-branch> # default: main
170
184
  # resolve conflicts locally; never open a PR on a branch that will conflict with target
171
185
  git push --force-with-lease # only if the branch already existed on origin
172
186
  ```
173
187
 
188
+ `--autostash` stashes any uncommitted change before the rebase and re-applies it afterwards, so a dirty tree does not stop the rebase before it starts. If the re-apply itself conflicts, git keeps the change in the stash and says so; report that and do not open the PR.
189
+
174
190
  This is not optional. Skipping the rebase (for example by shelling out to `az repos pr create` directly and hoping reviewers will merge over the conflict) is a governance violation. See `.arcane/governance/git-conventions.md` → **🛑 Agent-mandatory pre-PR guard**. If a rebase produces conflicts you cannot confidently resolve, **STOP** and hand off to the human — do not open the PR.
175
191
 
176
192
  Prefer delegating to `spell-create-pull-request`, which encodes this check as its Step 0.6. The steps below are the raw-CLI fallback and still require the rebase above to have already been performed.
@@ -180,7 +196,8 @@ Workflow:
180
196
  - `dev.azure.com` / `visualstudio.com` → Azure DevOps flow
181
197
 
182
198
  d. **GitHub flow (when remote is GitHub):**
183
- - Create PR with `gh pr create --title "<Conventional Commits title>" --body-file <pr-body-file.md>`.
199
+ - **Refuse to create the PR if the body file is missing, empty, or written for another branch.** Check `<git-dir>/arcane-pr-body.md` (step 9a2) exists, has content, and its first line names the current branch; if not, stop, rewrite it per 9a2, and only then continue. Never fall back to an empty or placeholder `--body`.
200
+ - Create PR with `gh pr create --title "<Conventional Commits title>" --body-file <git-dir>/arcane-pr-body.md`.
184
201
  - Use `--body-file` (not inline `--body`) to preserve multi-line markdown reliably.
185
202
  - Assign reviewer by default (operator/reviewer identity) if available.
186
203
  - Self-approve only when platform/policy allows. If blocked by policy, report and continue with human approval required.
@@ -190,9 +207,10 @@ Workflow:
190
207
  - Check if an active PR already exists for the source branch:
191
208
  - `az repos pr list --source-branch <branch> --status active --output json`
192
209
  - If one exists, reuse it; otherwise create via `az repos pr create`.
210
+ - **Refuse to create the PR if the body file is missing, empty, or written for another branch.** Check `<git-dir>/arcane-pr-body.md` (step 9a2) exists, has content, and its first line names the current branch before `az repos pr create`; if not, stop, rewrite it per 9a2, and only then continue.
193
211
  - For multi-line markdown descriptions:
194
- - Bash: `--description "$(cat <pr-body-file.md>)"`
195
- - PowerShell: `--description (Get-Content -Raw .\<pr-body-file.md>)`
212
+ - Bash: `--description "$(cat <git-dir>/arcane-pr-body.md)"`
213
+ - PowerShell: `--description (Get-Content -Raw <git-dir>\arcane-pr-body.md)`
196
214
  - Assign reviewer by default to the operator/reviewer identity (for example from `git config user.email`), idempotently (skip if already assigned).
197
215
  - Approve via CLI first: `az repos pr set-vote --id <PR_ID> --vote approve`.
198
216
  - If self-approval is blocked by policy, report that a second human approval is required and continue without forcing approval.
@@ -51,7 +51,7 @@ Before enabling auto-complete/auto-merge or completing an existing PR, resolve `
51
51
  > **🛑 AGENT-MANDATORY PRE-PR CHECKLIST — READ FIRST.** Before *any* PR-creation call (this spell, raw `az repos pr create`, raw `gh pr create`, the `create_pull_request` MCP tool, REST, web hook, or anything else):
52
52
  >
53
53
  > 1. `git fetch origin`
54
- > 2. `git rebase origin/<target-branch>` (default `main`) and resolve conflicts locally
54
+ > 2. `git rebase --autostash origin/<target-branch>` (default `main`) and resolve conflicts locally
55
55
  > 3. `git push --force-with-lease` if the branch was already on origin
56
56
  > 4. *Then* run the PR-creation call
57
57
  >
@@ -59,10 +59,22 @@ Before enabling auto-complete/auto-merge or completing an existing PR, resolve `
59
59
 
60
60
  1. Run `git branch --show-current`. **STOP** if on `main`/`master` — you cannot PR the integration branch into itself. Also **STOP** if detached HEAD (empty output) — check out a named branch first.
61
61
  2. **Verify the target exists:** `git rev-parse --verify origin/<target>`. If it fails, `git fetch origin <target>`; if still missing, **STOP** and ask the user for the correct `--target`.
62
- 3. **Ensure the branch is on origin (upstream):** if `git rev-parse --abbrev-ref --symbolic-full-name @{u}` fails, the branch was never pushed — run `git push -u origin <branch>`. The provider CLIs can only open a PR for a branch that exists on the remote.
62
+ 3. **Ensure the branch is on origin (upstream):** run the push-policy check below before any push in this Step 0 — this item's `git push -u` and item 6's `--force-with-lease` push. If it stops a push, stop the spell: a PR cannot be opened for a branch the remote does not have.
63
+
64
+ <!-- fragment:push-policy-check:start -->
65
+ **Push-policy check (ARC-049) — before any push.** Read `push_policy` from the repository's `.arcane.json` at the repository root (a self-hosted source repository with no root manifest keeps it in `src/assets/.arcane.json` when that file declares `selfHosted: true`). An absent file or an absent field means `open`. The check covers every push this spell makes: a branch push, a `--force-with-lease` push, and a remote branch deletion (`git push <remote> --delete <branch>`).
66
+ - **`open` or absent:** proceed with the push.
67
+ - **`guarded`:** before the push, state the policy ("This repository's push policy is `guarded`: every push needs your explicit confirmation."), name the remote, the branch and the kind of push, and ask the operator for explicit confirmation. Push only on an explicit operator confirmation of that push. A timeout, a cancellation, a delegated or host-generated response, or assent given to something else is not confirmation. **In an autonomous run with no operator to ask, do not push:** report the push as pending operator confirmation, naming the branch and the commits it would carry. This fails closed.
68
+ - **`blocked`:** do not attempt the push. Say why: the repository's push policy is recorded as `blocked`, so pushes are refused on purpose. Name `spell unblock-push` as the only way to lift it, run by the operator from an interactive terminal. Never run it yourself, and never work around the block (`--no-verify`, another remote or URL, editing git config or `.arcane.json`).
69
+ - **Any other value, or a `.arcane.json` that cannot be parsed:** do not push. Report what was found and suggest `spell doctor`.
70
+ - When a push does not happen, skip every step that depends on it (opening or updating a PR, remote branch cleanup) and report the commits as committed locally and not pushed.
71
+ - Enforcement: structured spell gate (ARC-023) — for `guarded`, this check is the only thing that holds a push for confirmation. A `blocked` push is also refused by the pre-push hook and the sentinel push URL that `spell init` or `spell block-push` install, and `spell doctor` reports a `blocked` policy whose controls are missing.
72
+ <!-- fragment:push-policy-check:end -->
73
+
74
+ Then, if `git rev-parse --abbrev-ref --symbolic-full-name @{u}` fails, the branch was never pushed — run `git push -u origin <branch>`. The provider CLIs can only open a PR for a branch that exists on the remote.
63
75
  4. **STOP** if the branch has no commits ahead of the target: `git log origin/<target>..HEAD --oneline` is empty → nothing to PR.
64
76
  5. Check for an existing **open** PR for this branch (provider-specific, Step 2). If one exists, print its URL and stop — never create duplicates.
65
- 6. **Mandatory rebase on target (governance guard):** `git fetch origin` then `git rebase origin/<target>`. On clean rebase, `git push --force-with-lease` (only if the branch existed remotely before the rebase). On conflicts: **STOP**, list conflicting files, ask the user to resolve **or run `spell-sync-pull-request`** — it draws the same mechanical-vs-ambiguous conflict line this guard doesn't attempt to, with a recoverable ref before it touches anything; never push a branch that will produce a merge conflict on the target (see git-conventions "🛑 Agent-mandatory pre-PR guard"). This step is not optional and is not skippable by calling a different PR-creation tool.
77
+ 6. **Mandatory rebase on target (governance guard):** `git fetch origin` then `git rebase --autostash origin/<target>`. `--autostash` carries any uncommitted change through the rebase instead of refusing to start; if re-applying it conflicts, git keeps it in the stash — report that and stop. On clean rebase, `git push --force-with-lease` (only if the branch existed remotely before the rebase). On conflicts: **STOP**, list conflicting files, ask the user to resolve **or run `spell-sync-pull-request`** — it draws the same mechanical-vs-ambiguous conflict line this guard doesn't attempt to, with a recoverable ref before it touches anything; never push a branch that will produce a merge conflict on the target (see git-conventions "🛑 Agent-mandatory pre-PR guard"). This step is not optional and is not skippable by calling a different PR-creation tool.
66
78
  7. **Auto-suggest `--docs-only`** (only if not already passed): if every changed file vs target is documentation/session files (`*.md`, `TODO.md`, `DECISIONS.md`, `journal/**`, `IDEAS.md`, `FEEDBACK.md`), print a one-line hint suggesting `--docs-only` and proceed with the standard path.
67
79
 
68
80
  ## Step 1 — Gather context
@@ -102,7 +114,7 @@ Format: `type(scope): summary`
102
114
 
103
115
  ## Step 4 — Build the description
104
116
 
105
- Write the body to a **temp file** and pass it to the provider CLI by file reference — never pass multiline content as a shell argument (this avoids shell/PowerShell truncation, quoting, and newline-mangling). Resolve the scratch path with `git rev-parse --git-dir` (handles worktrees and non-default `.git` locations) and write to `<git-dir>/arcane-pr-body.md`. The `.git` directory is never committed, so the body file is safe and ignored. Delete it after a successful create (Step 6).
117
+ Write the body to a **temp file** and pass it to the provider CLI by file reference — never pass multiline content as a shell argument (this avoids shell/PowerShell truncation, quoting, and newline-mangling). Resolve the scratch path with `git rev-parse --git-dir` (handles worktrees and non-default `.git` locations) and write to `<git-dir>/arcane-pr-body.md`, with the first line `<!-- arcane-pr-body: <current-branch> -->` naming the branch it was written for (a leftover body from another branch in the same checkout must never be reused). The `.git` directory is never committed, so the body file is safe and ignored. Delete it after a successful create (Step 6).
106
118
 
107
119
  Standard template:
108
120
 
@@ -169,6 +181,8 @@ so they need no edit.
169
181
 
170
182
  ## Step 5 — Create the PR
171
183
 
184
+ **Refuse to create the PR if the body file is missing, empty, or written for another branch.** Before either create command, check that `<git-dir>/arcane-pr-body.md` exists, has content, and its first line names the current branch. If it does not — most often because a halted rebase interrupted the flow and it was resumed here — stop and run Step 4 first, then return to this step. Never fall back to an empty or placeholder description.
185
+
172
186
  - **GitHub:**
173
187
  ```bash
174
188
  gh pr create --title "<title>" --body-file <temp> --base <target> --head <branch> [--draft] [--reviewer <r1,r2>]
@@ -219,6 +233,7 @@ so they need no edit.
219
233
  - Never create duplicate PRs — always check first (Step 2).
220
234
  - Never push directly to `main` — the PR is the only path.
221
235
  - Always write the description to a file before passing it to the provider CLI.
236
+ - Never create a PR whose body file is missing or empty (Step 5) — rebuild it with Step 4 first.
222
237
  - Respect the repo merge strategy in `.arcane/governance/git-conventions.md` — **do not squash** (it breaks per-commit attribution).
223
238
  - Print the PR URL clearly so it can be recorded by `spell-ship` / `spell-close-session`.
224
239
  - Never include secrets or tokens in the title or description.
@@ -127,6 +127,15 @@ For each framework-shaped item:
127
127
 
128
128
  Maintainer side is unchanged: GitHub issues are arcane's normal public intake; triage into arcane's own `IDEAS.md`/`DECISIONS.md` happens in arcane sessions like any other issue. This spell never writes to a hub's venture books and never reads `ventures/registry.json` — framework feedback and venture ideas are different channels on purpose (venture ideas → hub books → `spell-manifest`; framework lessons → here → arcane's GitHub).
129
129
 
130
+ ## Entry Point — Lessons Routed by `spell-close-session`
131
+
132
+ `FEEDBACK.md` has a second writer. `spell-close-session`'s lesson-routing step (step 2b) classifies each of the session's Lessons Learned. After one batch confirmation, it appends the framework-shaped ones as a single block in Step 5's format:
133
+ - `**Session:**` carries `(routed by spell-close-session from Lessons Learned)`.
134
+ - `**Rating:**` and `**Would use again:**` are `N/A`.
135
+ - Each improvement item ends with `<!-- upstream: queued -->`.
136
+
137
+ That step never files anything publicly. Its entries reach `{ARCANE_UPSTREAM_REPO}` only through this spell: `spell-feedback --flush` picks them up with every other queued item and runs Step 6 on each one (genericize, the literal `disclose` confirmation, then file or re-queue). A routed entry gets no shortcut, because close-session's batch confirmation approved a local append, not a public issue. An `N/A` rating is not a low rating, so it never triggers the `⚠️ LOW RATING` flag. **Enforcement: structured spell gate (ARC-023) — Step 6's literal `disclose` confirmation gates filing a routed item exactly as it gates any other; nothing in this spell distinguishes a routed item that could skip it.**
138
+
130
139
  ## Rules
131
140
 
132
141
  - Never overwrite existing feedback — always append.