arcane-cli 0.16.4 → 0.17.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.
@@ -306,6 +306,7 @@ When more than one agent may work in the same repository simultaneously:
306
306
  5. **Story ownership.** Only the agent assigned to a feature modifies that feature's `stories.json`. If the assigned agent does not match your slug, halt and report.
307
307
  6. **No cross-repo mutations without coordination.** If a story requires changes in two repos, complete and push one before starting the second. Never hold uncommitted changes across two repos simultaneously.
308
308
  7. **Merge window.** When two agents have PRs open for the same repo, the first to merge wins; the second must rebase before merging. The operator coordinates merge order if there are conflicts.
309
+ 8. **Working-tree vantage point (EF-33 / ARC-028 R7) — applies even to a solo agent, not only when multiple agents are active.** Listed here because it's the working-tree/multi-primitive dimension ARC-028 R7 defines, not because the hazard requires a second agent: a lone agent working through a bridged/remote mount hits the identical near-miss. All linked worktrees share one physical `.git` and `.git/config` — treat config-mutating operations as fleet-wide, not scoped to the worktree that ran them. Before any `git worktree prune`, `worktree remove`, `gc`, or branch deletion, confirm the target path from the *current process's own filesystem* (not just Git's own report) — a repository or its linked worktrees reached through a bridged/remote mount can report a live, healthy worktree as safe to prune with no error distinguishing "confirmed absent" from "not resolvable from here." See [governance/git-conventions.md](git-conventions.md) Same-Vantage-Point Check section for the full procedure, including how to establish that your own vantage point is trustworthy before relying on it.
309
310
 
310
311
  ---
311
312
 
@@ -57,6 +57,17 @@ For docs-only repos (Arcane):
57
57
 
58
58
  ### .NET Backend Pipeline
59
59
 
60
+ Fail-safe path filter (ARC-022): **exclude** known-inert doc paths rather than **include** a
61
+ named code directory — a new code directory added outside `src/**` would silently never trigger
62
+ this pipeline under an include filter. The exclude entry is a **filetype** glob, not a directory
63
+ glob, and deliberately just one entry: `docs/**`/`journal/**`/`.arcane/governance/**`-style
64
+ directory-wide excludes would ALSO exclude any non-Markdown file a consumer happens to place in
65
+ those directories (a build script, a manifest, tooling config) — silently violating ARC-022's own
66
+ unconditional requirement that scripts, manifests, lockfiles, migrations, containers, and
67
+ infrastructure always remain triggering inputs, regardless of which directory they live in. Filetype-scoping to
68
+ `**/*.md` closes that gap: only actual Markdown files are excluded, anywhere in the tree; anything
69
+ else — including a `.md`-adjacent script — still triggers CI by default.
70
+
60
71
  ```yaml
61
72
  # azure-pipelines.yml
62
73
  trigger:
@@ -64,8 +75,8 @@ trigger:
64
75
  include:
65
76
  - main
66
77
  paths:
67
- include:
68
- - src/**
78
+ exclude:
79
+ - '**/*.md'
69
80
 
70
81
  pr:
71
82
  branches:
@@ -97,11 +108,19 @@ steps:
97
108
 
98
109
  ### Node.js Pipeline
99
110
 
111
+ Previously had no path filter at all — fail-safe (never misses a code change) but wasteful (a
112
+ pure docs commit still burns a full pipeline run). Fail-safe path filter (ARC-022): the same
113
+ filetype-scoped `**/*.md` **exclude** entry as the .NET template above — never a directory-wide
114
+ exclude, and never an include list.
115
+
100
116
  ```yaml
101
117
  trigger:
102
118
  branches:
103
119
  include:
104
120
  - main
121
+ paths:
122
+ exclude:
123
+ - '**/*.md'
105
124
 
106
125
  pr:
107
126
  branches:
@@ -129,7 +148,27 @@ steps:
129
148
 
130
149
  ### Terraform Pipeline
131
150
 
151
+ An **include** filter is correct here, not the ARC-022 anti-pattern — but only if it's a
152
+ **filetype** glob, not a directory-prefix one. An earlier version of this template included only
153
+ `infrastructure/terraform/**`: a directory-prefix pattern that silently misses any `.tf` file
154
+ added *outside* that one named directory (a new `modules/**` root, a renamed infra folder) —
155
+ exactly the "new code locations fail open" failure ARC-022 rejects, and exactly what EF-22's own
156
+ report flagged for this template specifically, grouped with .NET's `src/**` problem, not treated
157
+ as a different category. `**/*.tf`/`**/*.tfvars` has the same closure property `**/*.md` has for
158
+ the Markdown-lint template below: Terraform files trigger this pipeline from *anywhere* in the
159
+ tree, while a Node.js/`.NET` source change still correctly does not. Widened to also cover this
160
+ pipeline's own definition file, so an edit to the pipeline itself is validated rather than
161
+ silently unreviewed.
162
+
163
+ Widened further to cover Terraform's JSON-syntax file variants (`.tf.json`/`.tfvars.json`,
164
+ HashiCorp's own first-class alternative syntax to `.tf`/`.tfvars`) and its dependency lock file
165
+ (`.terraform.lock.hcl`, one per root module directory). ARC-022 names lockfiles as a non-negotiable
166
+ always-trigger category in their own right, independent of the general "Terraform files trigger
167
+ this pipeline" argument above — a lock file's own name doesn't end in `.tf`/`.tfvars`, so it would
168
+ have silently fallen through the filetype glob above without a named entry of its own.
169
+
132
170
  ```yaml
171
+ # azure-pipelines.terraform.yml
133
172
  trigger: none # Manual only for apply
134
173
 
135
174
  pr:
@@ -138,7 +177,12 @@ pr:
138
177
  - main
139
178
  paths:
140
179
  include:
141
- - infrastructure/terraform/**
180
+ - '**/*.tf'
181
+ - '**/*.tfvars'
182
+ - '**/*.tf.json'
183
+ - '**/*.tfvars.json'
184
+ - '**/.terraform.lock.hcl'
185
+ - azure-pipelines.terraform.yml
142
186
 
143
187
  pool:
144
188
  vmImage: 'ubuntu-latest'
@@ -160,7 +204,14 @@ steps:
160
204
 
161
205
  ### Markdown Lint Pipeline
162
206
 
207
+ Also correctly include-scoped, for the same reason as the Terraform pipeline: this pipeline
208
+ exists specifically to lint markdown, not to validate code generally, and `**.md` already has the
209
+ same anywhere-in-the-tree closure property `**/*.tf` gives the Terraform template above (unlike a
210
+ directory-prefix pattern, it was never the gap this fix needed to close). Widened to cover its
211
+ own definition file too.
212
+
163
213
  ```yaml
214
+ # azure-pipelines.markdown.yml
164
215
  trigger:
165
216
  branches:
166
217
  include:
@@ -168,6 +219,7 @@ trigger:
168
219
  paths:
169
220
  include:
170
221
  - '**.md'
222
+ - azure-pipelines.markdown.yml
171
223
 
172
224
  pool:
173
225
  vmImage: 'ubuntu-latest'
@@ -187,6 +239,54 @@ steps:
187
239
 
188
240
  ---
189
241
 
242
+ ## Never Trust Commit Metadata for CI Skipping (ARC-022)
243
+
244
+ CI skip/run decisions are based **only on changed paths** — never on commit message, author
245
+ identity, or branch name. Do not add a *new* mechanism (a custom `docs(...)`-style commit-prefix
246
+ convention, a bot rule, a pipeline-level check) that lets a commit opt itself out of validation by
247
+ what it *says* rather than what it *changed*.
248
+
249
+ Commit messages, authorship, and branch names are attacker-controlled metadata — anyone who can
250
+ push a commit controls all three. A CI-skip mechanism keyed on any of them is a bypass primitive:
251
+ it lets a malicious or careless commit self-certify as "safe to skip" regardless of what it
252
+ actually touched. Path-based filtering (above) is the only trust signal this repository
253
+ recognizes, because the pipeline itself independently observes the changed paths rather than
254
+ trusting a claim embedded in the commit.
255
+
256
+ **Know the platform default, don't assume this rule alone controls it.** Azure Pipelines honors
257
+ `[skip ci]`, `[ci skip]`, `skip-checks: true`, `[skip azurepipelines]`, `[skip azpipelines]`,
258
+ `[skip azp]`, and `***NO_CI***` in a pushed commit message **by default**, on `trigger:`-driven
259
+ runs — this is platform behavior, not something a consumer opts into, and this governance rule
260
+ cannot disable it from the YAML alone. The required-before-merge gate this repo's own Branch
261
+ Policies table relies on (**build validation on the PR's merge commit**) is documented to run
262
+ *regardless* of `[skip ci]` and its variants, so the merge gate itself stays sound. What is **not**
263
+ protected: any other trigger-based run this repo depends on for signal outside that one gate —
264
+ direct pushes to a non-PR branch, a secondary environment build, a dashboard keyed off pipeline
265
+ status. Treat those as a residual, platform-level exposure this document can flag but not close;
266
+ if a trigger-based run must be tamper-resistant, that requires an organization-level policy
267
+ outside this repo's own YAML, not a governance sentence.
268
+
269
+ ## Branch-Policy Path-Filter Alignment (ARC-022)
270
+
271
+ A YAML pipeline's own `trigger`/`pr` path filter and an Azure DevOps branch policy's **build
272
+ validation path filter** are two independent mechanisms, and only one of them controls whether a
273
+ passing build is actually *required* before a PR can merge. If the branch policy's path filter is
274
+ narrower than the pipeline's own trigger scope, a change that falls outside the policy's filter
275
+ can merge with **no build check required at all** — even though the pipeline itself is correctly
276
+ scoped and would have run and caught a problem, because nothing forced it to be a merge
277
+ requirement for that specific change.
278
+
279
+ **Concrete failure mode:** the .NET pipeline above triggers on everything except docs (exclude
280
+ filter, fail-safe). If the repo's branch policy still requires the build check only for changes
281
+ under `paths: src/**` (an older include-style policy filter, or one configured before this repo
282
+ adopted exclude-based YAML triggers), a change to a new top-level code directory outside `src/`
283
+ triggers the pipeline (YAML is fail-safe) but does **not** require it to pass before merge (branch
284
+ policy is not) — the build can run, fail, and be ignored, or the PR can merge before it finishes.
285
+ Whenever a pipeline's YAML trigger scope changes, the branch policy's own path filter (if it has
286
+ one) must be reviewed and updated to match or be a strict superset — never left narrower.
287
+
288
+ ---
289
+
190
290
  ## Deployment Gates
191
291
 
192
292
  | Environment | Gate | Who Approves |
@@ -359,6 +359,21 @@ git push origin --delete type/old-topic # delete remote branch (safety net; ign
359
359
 
360
360
  This applies to all actors — humans, interactive tools (Copilot, Claude), and autonomous agents. The `spell-commit-work` and `spell-close-session` prompts enforce this check.
361
361
 
362
+ ### Same-Vantage-Point Check (EF-33 / ARC-028 R7)
363
+
364
+ `git worktree list` can **truthfully** report a live, healthy linked worktree as `prunable` when read through a cross-filesystem bridge — a Linux-side mount of a Windows host, a remote-mounted volume, a container bind-mount, any path-translation layer between the reading process and the repository's actual owning environment. The registered absolute path simply doesn't resolve from that vantage point, even though the directory exists and is valid from the machine that owns it. Nothing in Git's own output distinguishes "confirmed absent" from "not resolvable from this process's filesystem view" — both render identically as `prunable`. (A separate, unconditional hazard applies to `git branch --merged` regardless of vantage point — see the note at the end of this section; do not assume that one only matters when a bridge is in play.)
365
+
366
+ **Before running any Git command with irreversible effects on worktree, branch, or ref state** — `git worktree prune`, `git worktree remove`, `git gc --prune=now`, `git branch -d`/`-D`, or manual deletion of `.git/worktrees/<name>` metadata — **when the repository or its linked worktrees might be reached through more than one filesystem view**, perform the same-vantage-point check first:
367
+
368
+ 1. Resolve the registered path exactly as Git sees it (`git worktree list --porcelain`'s `worktree` field).
369
+ 2. **First confirm your OWN vantage point, before trusting anything else.** Checking `Test-Path`/`[ -e "$path" ]`/`os.path.exists()` against the registered path is only meaningful if the process running that check is itself in the same environment that registered the worktree — running the "independent" check from the *same* bridged/mounted process that produced the `git worktree list` read reproduces the exact failure this section exists to prevent, since both signals are corrupted by the identical wrong vantage point. A concrete, checkable signal for this: does the registered path's own syntax match the current process's native OS conventions (drive letter + backslashes for Windows; POSIX root + forward-slashes for Linux/macOS)? A registered path in a **foreign** syntax (e.g. a `C:\...` path seen from a POSIX shell, or vice versa) is itself proof you are not viewing this repository from the environment that registered it — treat every entry as unconfirmed on sight, skip step 3 entirely, and go straight to step 4. Matching syntax alone is not proof of a shared vantage point (two different containers can both use POSIX paths) — it only rules out the specific cross-OS case; it does not establish confidence on its own.
370
+ 3. Only if step 2 gives you a genuine reason to trust your own vantage point (matching path syntax, plus independent knowledge that this is the same machine/container/session that registered the worktree — not merely "the check ran without error"): confirm the path from the current process's own filesystem (`Test-Path` / `[ -e "$path" ]` / `os.path.exists()` / equivalent). Never treat Git's own `prunable` annotation as sufficient evidence on its own, and never treat "the existence check returned a clean answer" as sufficient either — a clean answer from the wrong vantage point is not a safe answer.
371
+ 4. If step 2 raised any doubt, if the path syntax doesn't match, or if there is any uncertainty about which environment actually owns the repository, **stop and ask** rather than trusting the more convenient answer. This is the default outcome whenever step 2's confidence isn't genuinely established — not a rare escape hatch reserved for exceptional cases.
372
+
373
+ This is documented as a standing operational caution, not a code-level gate: EF-33's own intake report frames the underlying defect as cross-machine/cross-mount filesystem visibility, which no single CI runner can reproduce — the confirmation step above must be followed manually by whichever human or agent is about to run the destructive command. Confirmed as a live near-miss on 2026-08-03: a Linux-side read reported eight healthy linked worktrees `prunable`; the same repository read from Windows (their actual owning environment) showed zero prunable entries, and `git worktree prune -v` there removed nothing.
374
+
375
+ **Unconditional, vantage-point-independent note on branch deletion:** `git branch --merged` can also report a fully-landed branch as unmerged — or the reverse confusion, a branch never actually reviewed but ancestry-visible — because Git's rebase-and-fast-forward and squash merge strategies (both sanctioned by this repo's own merge-strategy table above) rewrite commit SHAs, defeating ancestry-based detection. This has nothing to do with filesystem bridges — it fires identically on a single machine with no mount involved. See the ancestry-vs-content-verification finding in `TODO.md`'s PR Workflow section for the full mechanism; verify branch content before deleting regardless of what vantage-point confidence steps 1-4 above establish.
376
+
362
377
  ### ADO PR Lifecycle — Complete Command Reference
363
378
 
364
379
  When working in an Azure DevOps repo with branch protection (direct push to `main` rejected), use this exact sequence. Several shortcut tools have known reliability issues on Windows — use the commands below instead.
@@ -27,8 +27,8 @@ Use these files first:
27
27
  - [ai-context/system-prompt-context.md](../../ai-context/system-prompt-context.md)
28
28
  - [agents/agent-policies.md](../../.arcane/governance/agent-policies.md)
29
29
  - Most recent journal file(s) in [journal/](../../journal/)
30
- - Relevant business overviews under [ventures/](../../ventures/)
31
- - `ventures/registry.json`, if present (hub-role and registry-consistency detectors)
30
+ - Relevant business overviews under `{BUSINESS_ROOT}/` (resolve from `.arcane.json`'s `business_root` field, default `ventures/` if unset)
31
+ - `{BUSINESS_ROOT}/registry.json`, if present (hub-role and registry-consistency detectors)
32
32
 
33
33
  Scope selection (`--scope <value>`, default `all`):
34
34
 
@@ -49,12 +49,12 @@ Concrete detectors to run (in addition to general contradiction/stale-date check
49
49
  - If normalized content differs, report **real content drift** with normal severity grading.
50
50
  - 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.
51
51
  - Report exact path counts for both classes. Never summarize all byte-different files as content drift.
52
- - **Hub-artifact leak (non-hub repos)** — if `.arcane.json` does not have `role: "hub"`, the existence of any `ventures/*/IDEAS.md`, `ventures/*/TODO.md`, or `ventures/registry.json` 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.
52
+ - **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.
53
53
  - **Hub role/registry consistency (hub repos only)** — when `.arcane.json` has `role: "hub"`:
54
- - `role: "hub"` with no `{business_root}` directory (default `ventures/`) present — **High**: the manifest claims a role the repo can't actually fulfill.
55
- - `ventures/registry.json` slugs that don't match an existing `ventures/<slug>/` folder, or venture folders with no registry entry — **Medium**, both directions.
54
+ - `role: "hub"` with no `{BUSINESS_ROOT}` directory (default `ventures/`) present — **High**: the manifest claims a role the repo can't actually fulfill.
55
+ - `{BUSINESS_ROOT}/registry.json` slugs that don't match an existing `{BUSINESS_ROOT}/<slug>/` folder, or venture folders with no registry entry — **Medium**, both directions.
56
56
  - Malformed `status: promoted → …` / `status: dropped …` markers in any book (unparseable destination, missing date) — **Low**.
57
- - A `ventures/<slug>/` folder with a `role`-shaped `.arcane.json`-like marker of its own (i.e., it looks like it's trying to be a hub itself) — **Medium**: flag for operator review; nested hubs are unsupported.
57
+ - A `{BUSINESS_ROOT}/<slug>/` folder with a `role`-shaped `.arcane.json`-like marker of its own (i.e., it looks like it's trying to be a hub itself) — **Medium**: flag for operator review; nested hubs are unsupported.
58
58
 
59
59
  For each drift finding include:
60
60
 
@@ -148,7 +148,7 @@ Rules:
148
148
  - Resolve `<remote>` and `<trunk>` from observed Git/provider state: use the usable authenticated remote selected in step 9 and the merged PR's target branch (falling back to that remote's default branch). Never assume `origin` or `main`.
149
149
  - Verify through the detected provider that the PR is merged before changing branches.
150
150
  - Run `git switch <trunk>` followed by `git pull --ff-only <remote> <trunk>`. Do not end a remote-backed session on a topic branch.
151
- - Delete the local topic branch: `git branch -d <branch>`.
151
+ - Delete the local topic branch: `git branch -d <branch>` — if the repository or its linked worktrees might be reached through more than one filesystem view, run the same-vantage-point check first (EF-33 / ARC-028 R7, [governance/git-conventions.md](../../.arcane/governance/git-conventions.md) Same-Vantage-Point Check section).
152
152
  - Verify `git log --oneline -3` shows the merged change at HEAD.
153
153
  - If stale local branches exist (merged or older than 7 days), list them and suggest cleanup.
154
154
  - See [governance/git-conventions.md](../../.arcane/governance/git-conventions.md) Post-Merge Cleanup section.
@@ -101,7 +101,7 @@ Workflow:
101
101
  - `infrastructure` — infrastructure/ directory, hardware, OS setup
102
102
  - `decisions` — DECISIONS.md updates
103
103
  - `journal` — journal/ entries
104
- - `business` — ventures/ directory
104
+ - `business` — `{BUSINESS_ROOT}` directory (resolve from `.arcane.json`'s `business_root` field, default `ventures/` if unset)
105
105
  - `playbooks` — playbooks/ directory
106
106
  - `governance` — governance/ directory
107
107
  - Multiple scopes? Pick the primary one or use a broader scope like `docs` or `repo`.
@@ -206,7 +206,7 @@ Workflow:
206
206
  - `git push origin --delete <branch>` (if already deleted, treat as non-fatal)
207
207
  - `git fetch --prune origin`
208
208
  - If `<branch>` is attached to an active worktree, skip local branch deletion.
209
- - If branch is not attached to any active worktree, run `git branch -d <branch>`.
209
+ - If branch is not attached to any active worktree, run `git branch -d <branch>` — but if the repository or its linked worktrees might be reached through more than one filesystem view, run the same-vantage-point check first (EF-33 / ARC-028 R7, [governance/git-conventions.md](../../.arcane/governance/git-conventions.md) Same-Vantage-Point Check section) before trusting a "safe to delete" read.
210
210
  - Return to `main` only when appropriate for the active session/worktree context.
211
211
  - If other stale local branches exist (merged or older than 7 days), list them and suggest cleanup.
212
212
  - See [governance/git-conventions.md](../../.arcane/governance/git-conventions.md) Post-Merge Cleanup section.
@@ -28,7 +28,7 @@ Use these files first:
28
28
  - [ai-context/system-prompt-context.md](../../ai-context/system-prompt-context.md)
29
29
  - [agents/agent-policies.md](../../.arcane/governance/agent-policies.md)
30
30
  - Most recent journal file(s) in [journal/](../../journal/)
31
- - Relevant business overview(s) under [ventures/](../../ventures/)
31
+ - Relevant business overview(s) under `{BUSINESS_ROOT}/` (resolve from `.arcane.json`'s `business_root` field, default `ventures/` if unset)
32
32
 
33
33
  **Handoff Detection (run before anything else):**
34
34
 
@@ -70,17 +70,17 @@ Then produce output in this exact structure:
70
70
 
71
71
  Before anything else, check:
72
72
 
73
- - **Current branch:** Run `git branch --show-current` and `git worktree list`. Report the current state; do not switch during this read-only check. Branch creation/switching is owned by the lazy Mutation Guard.
73
+ - **Current branch:** Run `git branch --show-current` and `git worktree list`. Report the current state; do not switch during this read-only check. Branch creation/switching is owned by the lazy Mutation Guard. **Caveat (EF-33):** if this repository or its linked worktrees might be reached through more than one filesystem view (a bridged/remote mount, a container bind-mount), `git worktree list` can truthfully report a live, healthy worktree as `prunable` when read from a vantage point that can't resolve its registered path — this is a read-only report, not a reason to act; never run a destructive worktree/branch command on its output alone (see git-conventions.md's Same-Vantage-Point Check section before any such command elsewhere in this or later sessions).
74
74
  - **Session branch naming compliance:** Branch names must be human-readable and policy-compliant:
75
75
  - Interactive session default: `sessions/YYYY-MM-DD-<topic-slug>` (kebab-case topic from current task/session objective).
76
76
  - Disallow random adjective-noun generator names (example: `ideal-disco`) as session defaults.
77
77
  - If current branch is non-compliant and no active PR depends on it, record the required rename for the Mutation Guard. Do not rename during a read-only session.
78
78
  - If an active PR uses the old branch name, do not force-delete the old remote branch; flag it and continue with a follow-up rename plan.
79
- - **Stale local branches:** Run `git branch --merged main` to list branches already merged that should be deleted.
80
- - **Tracker configuration check (early):** resolve active tracking settings before planning. Read root `.arcane.json` first (if present), then the current feature PRD frontmatter if available. A self-host marker under `src/assets/` is doctor metadata, not active repository configuration; never use it as tracking provenance. If neither active source sets tracking, ask the operator and treat the answer as session-scoped until EF-14 defines persistent configuration:
79
+ - **Stale local branches:** Run `git branch --merged main` to list branches already merged that should be deleted. This lists candidates only — before deleting any, apply the same-vantage-point check (EF-33) and, separately, verify by content rather than trusting ancestry alone (rebase-and-fast-forward merges are invisible to `--merged`; see the merged-branch cleanup item in TODO.md).
80
+ - **Tracker configuration check (early):** resolve active tracking settings before planning, in this order: root `.arcane.json` (if present) -> the committed self-hosted source manifest (`src/assets/.arcane.json`, read only when it declares `selfHosted: true` -- this is EF-14's recorded self-hosting resolution tier, distinct from treating the mere presence of a self-host marker as license to infer other, unrelated config) -> the current feature PRD frontmatter -> ask. Persisted once at `spell init`/`spell update` (EF-14); do not re-ask when any of the first three sources already sets it.
81
81
  - `tracking_mode: internal | external`
82
82
  - `external_provider: ado | jira | other`
83
- - If missing, ask the operator to choose now. Default to `external` + `ado` only when existing ADO context already exists (backward compatibility).
83
+ - If all three sources are absent, ask: "Track work in this repo (TODO.md / PRDs)" [internal] vs "Track work in an external tracker (Azure DevOps / Jira / other)" [external]. Default to `external` + `ado` only when existing ADO context already exists (backward compatibility).
84
84
  - **Open PRs (external/ado mode only):** if tracking mode is `external` with provider `ado` and ADO MCP is available, list all open PRs across all repos in the configured repo list (resolve from `.arcane.json`; if unset, ask the operator which repos to scan). Flag PRs older than 3 days as stale, older than 7 days as overdue. Format each as a clickable markdown link: `[PR #{id} — {title}](https://dev.azure.com/{ADO_ORG}/{ADO_PROJECT}/_git/{repo}/pullrequest/{id})` — resolve `{ADO_ORG}` and `{ADO_PROJECT}` from `.arcane.json` / PRD frontmatter; ask if unset. Never list a bare `PR #NNN`.
85
85
  - **Uncommitted changes:** Run `git status` and report any uncommitted files.
86
86
  - **Arcane version check (two-axis):** If `.arcane.json` exists in the repo root, check both whether the repo's managed files are behind the installed CLI _and_ whether the installed CLI is behind the latest published version of `arcane-cli`:
@@ -31,8 +31,14 @@ Workflow:
31
31
  external_provider: ado | jira | other
32
32
  ```
33
33
 
34
+ Resolve in this order, matching spell-open-session's own resolution chain (EF-14): root
35
+ `.arcane.json` (if present) → the committed self-hosted source manifest (`src/assets/.arcane.json`,
36
+ read only when it declares `selfHosted: true`) → the current feature PRD frontmatter → ask.
37
+ Do not re-ask when any of the first three sources already sets it.
38
+
34
39
  Rules:
35
40
  - If not explicitly set and ADO context is already present (work item ID + org/project), default to `tracking_mode: external`, `external_provider: ado` for backward compatibility.
41
+ - If none of the three sources above set it and no ADO context exists, ask: "Track work in this repo (TODO.md / PRDs)" [internal] vs "Track work in an external tracker (Azure DevOps / Jira / other)" [external].
36
42
  - If `tracking_mode=internal`, do not require an external tracker ID.
37
43
  - If `tracking_mode=external` and `external_provider=ado`, collect org/project and resolve process template + available work item types before planning:
38
44
  ```bash
@@ -64,7 +70,7 @@ Workflow:
64
70
  - Are there existing ADRs or constraints?
65
71
  - **If `tracking_mode=external` and `external_provider=ado`: What is the ADO work item ID?** — required before proceeding in ADO mode. Store it as `adoWorkItemId` in PRD frontmatter so it flows to `spell-architect` and `stories.json`.
66
72
 
67
- 3. **Research context** — check the relevant business docs in `ventures/` and any existing code repos.
73
+ 3. **Research context** — check the relevant business docs under `{BUSINESS_ROOT}/` (resolve from `.arcane.json`'s `business_root` field, default `ventures/` if unset) and any existing code repos.
68
74
 
69
75
  4. **Generate PRD.md** with these sections:
70
76
  ```markdown
@@ -21,10 +21,12 @@ The user's input IS the idea. If no idea text was provided, ask one short questi
21
21
 
22
22
  ## Step 0 — Venture Targeting (Hub Only)
23
23
 
24
- If the input names a venture ("save this idea for ordo", `--venture <slug>`), resolve it through `ventures/registry.json`'s aliases first (exact slug → alias → closest match offered — never guessed). Unknown slug:
24
+ Resolve `{BUSINESS_ROOT}` from `.arcane.json`'s `business_root` field (default `ventures/` if unset) before anything below.
25
+
26
+ If the input names a venture ("save this idea for ordo", `--venture <slug>`), resolve it through `{BUSINESS_ROOT}/registry.json`'s aliases first (exact slug → alias → closest match offered — never guessed). Unknown slug:
25
27
 
26
28
  ```
27
- No venture "<slug>" under ventures/ (closest: ordovica, tidewright).
29
+ No venture "<slug>" under {BUSINESS_ROOT}/ (closest: ordovica, tidewright).
28
30
  1) use <closest> 2) save to hub root IDEAS.md 3) cancel (create the venture first: spell-summon-venture)
29
31
  ```
30
32
 
@@ -93,7 +93,7 @@ Workflow:
93
93
  9. **Post-merge branch cleanup** (after the PR merges) — the merge was the human gate, so cleanup is automatic:
94
94
  - Switch to the main branch (e.g. `git checkout main`).
95
95
  - Pull the latest (e.g. `git pull`).
96
- - Delete the merged topic branch locally (e.g. `git branch -d <branch>`).
96
+ - Delete the merged topic branch locally (e.g. `git branch -d <branch>`) — if the repository or its linked worktrees might be reached through more than one filesystem view, run the same-vantage-point check first (EF-33 / ARC-028 R7, [governance/git-conventions.md](../../.arcane/governance/git-conventions.md) Same-Vantage-Point Check section).
97
97
  - Delete the merged topic branch on the remote (e.g. `git push origin --delete <branch>`); tolerate an already-deleted remote branch.
98
98
  - Prune stale remote-tracking refs (e.g. `git fetch --prune` or `git remote prune origin`).
99
99
 
@@ -7,7 +7,7 @@ agent: agent
7
7
 
8
8
  ## Executive Summary
9
9
 
10
- - This prompt creates a new venture in a **hub** repo: the venture folder, its starter docs, its two idea/todo books, and its `ventures/registry.json` entry — one operation instead of several manual steps.
10
+ - This prompt creates a new venture in a **hub** repo: the venture folder, its starter docs, its two idea/todo books, and its registry entry — one operation instead of several manual steps.
11
11
  - **Hub-gated:** requires `role: "hub"` in `.arcane.json`. Refuses cleanly in a consumer repo, pointing at the right place to capture the idea instead.
12
12
  - Use this when starting a new venture (business, product, or personal project) that the hub should track.
13
13
  - Input format is: `Venture Name | Venture Type | Description` — prompt will ask for missing fields.
@@ -36,7 +36,7 @@ Use these files first (each is optional — if missing, proceed as noted):
36
36
 
37
37
  - [playbooks/new-business-setup.md](../../playbooks/new-business-setup.md) — setup playbook. If `playbooks/` doesn't exist, skip and derive steps from the venture-type checklist below.
38
38
  - [naming-conventions.md](../../.arcane/governance/naming-conventions.md) — slug/file naming rules. If missing, default to lowercase-with-dashes slugs.
39
- - [ventures/\_template/overview.md](../../ventures/_template/overview.md) — overview template. If missing, generate a minimal overview from the fields below.
39
+ - `{BUSINESS_ROOT}/_template/overview.md` — overview template. If missing, generate a minimal overview from the fields below.
40
40
  - [.arcane/templates/venture-template/IDEAS.md](../../.arcane/templates/venture-template/IDEAS.md) and [.arcane/templates/venture-template/TODO.md](../../.arcane/templates/venture-template/TODO.md) — book stubs. If missing, use the header format from [[spell-save-idea|spell-save-idea]] / [[spell-todo|spell-todo]].
41
41
  - [README.md](../../README.md) — repo navigation. If missing, skip cross-linking.
42
42
  - [project.md](../../project.md) — project context. If missing, skip.
@@ -54,7 +54,7 @@ Use these files first (each is optional — if missing, proceed as noted):
54
54
 
55
55
  ## Step 3 — Register the Venture
56
56
 
57
- Open (or create, with the standard header — see `ventures/registry.json`'s own `_comment` field for the expected shape) `ventures/registry.json` and append an entry keyed by the slug:
57
+ Open (or create, with the standard header — see `{BUSINESS_ROOT}/registry.json`'s own `_comment` field for the expected shape) `{BUSINESS_ROOT}/registry.json` and append an entry keyed by the slug:
58
58
 
59
59
  ```json
60
60
  "<slug>": {
@@ -83,7 +83,7 @@ Output format:
83
83
 
84
84
  ## Created
85
85
 
86
- - File and folder paths created, including the `ventures/registry.json` entry.
86
+ - File and folder paths created, including the `{BUSINESS_ROOT}/registry.json` entry.
87
87
 
88
88
  ## Defaults Applied
89
89
 
@@ -15,11 +15,13 @@ agent: agent
15
15
 
16
16
  ---
17
17
 
18
+ Resolve `{BUSINESS_ROOT}` from `.arcane.json`'s `business_root` field (default `ventures/` if unset) before either mode below.
19
+
18
20
  ## Sweep Mode (`--sweep`)
19
21
 
20
22
  If the argument is (or starts with) `--sweep`, skip every other step in this spell entirely and run this instead:
21
23
 
22
- - **In a hub repo:** report `status: new` counts and oldest-entry age for the hub root `TODO.md` plus every `ventures/<slug>/TODO.md` that exists. Group by book, point at `spell-manifest` for entries ready to promote. Report-only — no edits.
24
+ - **In a hub repo:** report `status: new` counts and oldest-entry age for the hub root `TODO.md` plus every `{BUSINESS_ROOT}/<slug>/TODO.md` that exists. Group by book, point at `spell-manifest` for entries ready to promote. Report-only — no edits.
23
25
  - **In a consumer repo:** report the same, for the repo-root `TODO.md` only (no venture books exist there).
24
26
 
25
27
  Output format:
@@ -33,10 +35,10 @@ ventures/ordovica/TODO.md 3 open, oldest 2026-08-10
33
35
 
34
36
  ## Step 0 — Venture Targeting (Hub Only)
35
37
 
36
- If the input names a venture ("add a todo for ordo", `--venture <slug>`), resolve it through `ventures/registry.json`'s aliases first (exact slug → alias → closest match offered — never guessed). Unknown slug:
38
+ If the input names a venture ("add a todo for ordo", `--venture <slug>`), resolve it through `{BUSINESS_ROOT}/registry.json`'s aliases first (exact slug → alias → closest match offered — never guessed). Unknown slug:
37
39
 
38
40
  ```
39
- No venture "<slug>" under ventures/ (closest: ordovica, tidewright).
41
+ No venture "<slug>" under {BUSINESS_ROOT}/ (closest: ordovica, tidewright).
40
42
  1) use <closest> 2) add to hub root TODO.md 3) cancel (create the venture first: spell-summon-venture)
41
43
  ```
42
44
 
@@ -81,7 +83,7 @@ Classify the idea along two axes:
81
83
  - `infrastructure` — hardware, OS, cloud, networking, provisioning
82
84
  - `governance` — git conventions, agent autonomy, spending controls, work management
83
85
  - `ci-cd` — repos, pipelines, build/release automation
84
- - `business` — a specific business under `ventures/` — name which one
86
+ - `business` — a specific business under `{BUSINESS_ROOT}/` — name which one
85
87
  - `product-ideas` — new product concepts or packaging ideas
86
88
  - `legal` — LLC, ToS, contracts
87
89
  - `playbooks` — runbooks, setup guides, automation scripts
@@ -116,7 +118,7 @@ Determine the target file(s) for each item:
116
118
  | Speculative idea or future-facing concept | `TODO.md` under `## Product Ideas` or the best matching section |
117
119
  | ADR candidate | `DECISIONS.md` — flag it as pending, do not write the full ADR yet |
118
120
  | Journal-worthy context | `journal/` — propose a filename with today's date slug, do not create the file yet |
119
- | Business-specific execution item | Both `TODO.md` AND the relevant `ventures/<name>/overview.md` reference section |
121
+ | Business-specific execution item | Both `TODO.md` AND the relevant `{BUSINESS_ROOT}/<name>/overview.md` reference section |
120
122
 
121
123
  ## Step 4 — Show Proposal
122
124
 
package/dist/index.js CHANGED
@@ -87,10 +87,28 @@ var ManifestCorruptError = class extends Error {
87
87
  this.name = "ManifestCorruptError";
88
88
  }
89
89
  };
90
+ var ManifestInvalidFieldError = class extends Error {
91
+ constructor(manifestPath2, field, value) {
92
+ super(
93
+ `Manifest at "${manifestPath2}" has an unsupported value for "${field}": ${JSON.stringify(value)}.`
94
+ );
95
+ this.name = "ManifestInvalidFieldError";
96
+ }
97
+ };
90
98
  var MANIFEST_FILE = ".arcane.json";
99
+ var VALID_TRACKING_MODES = ["internal", "external"];
100
+ var VALID_EXTERNAL_PROVIDERS = ["ado", "jira", "other"];
91
101
  function manifestPath(targetDir) {
92
102
  return path2.join(targetDir, MANIFEST_FILE);
93
103
  }
104
+ function validateTrackingFields(manifest, filePath) {
105
+ if (manifest.tracking_mode !== void 0 && !VALID_TRACKING_MODES.includes(manifest.tracking_mode)) {
106
+ throw new ManifestInvalidFieldError(filePath, "tracking_mode", manifest.tracking_mode);
107
+ }
108
+ if (manifest.external_provider !== void 0 && manifest.external_provider !== null && !VALID_EXTERNAL_PROVIDERS.includes(manifest.external_provider)) {
109
+ throw new ManifestInvalidFieldError(filePath, "external_provider", manifest.external_provider);
110
+ }
111
+ }
94
112
  async function readManifest(targetDir) {
95
113
  const filePath = manifestPath(targetDir);
96
114
  let content;
@@ -102,11 +120,14 @@ async function readManifest(targetDir) {
102
120
  }
103
121
  throw err;
104
122
  }
123
+ let manifest;
105
124
  try {
106
- return JSON.parse(content);
125
+ manifest = JSON.parse(content);
107
126
  } catch {
108
127
  throw new ManifestCorruptError(filePath);
109
128
  }
129
+ validateTrackingFields(manifest, filePath);
130
+ return manifest;
110
131
  }
111
132
  async function writeManifest(targetDir, manifest) {
112
133
  const filePath = manifestPath(targetDir);
@@ -2018,12 +2039,40 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
2018
2039
  });
2019
2040
  role = isHub ? "hub" : "consumer";
2020
2041
  }
2042
+ let tracking_mode;
2043
+ let external_provider;
2044
+ if (profile === "governance-only" || profile === "methodology") {
2045
+ tracking_mode = "internal";
2046
+ external_provider = null;
2047
+ } else if (!options.profile) {
2048
+ console.log();
2049
+ tracking_mode = await select2({
2050
+ message: "How will work be tracked in this repo?",
2051
+ choices: [
2052
+ { value: "internal", name: "Track work in this repo (TODO.md / PRDs)" },
2053
+ { value: "external", name: "Track work in an external tracker (Azure DevOps / Jira / other)" }
2054
+ ]
2055
+ });
2056
+ if (tracking_mode === "external") {
2057
+ external_provider = await select2({
2058
+ message: "Which external tracker?",
2059
+ choices: [
2060
+ { value: "ado", name: "Azure DevOps" },
2061
+ { value: "jira", name: "Jira" },
2062
+ { value: "other", name: "Other" }
2063
+ ]
2064
+ });
2065
+ } else {
2066
+ external_provider = null;
2067
+ }
2068
+ }
2021
2069
  const manifest = {
2022
2070
  version: packageVersion,
2023
2071
  profile,
2024
2072
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2025
2073
  components: installedComponents,
2026
- ...role ? { role } : {}
2074
+ ...role ? { role } : {},
2075
+ ...tracking_mode ? { tracking_mode, external_provider } : {}
2027
2076
  };
2028
2077
  await writeManifest(targetDir, manifest);
2029
2078
  printSuccess(`Initialized with profile "${profile}" \u2014 ${fileCount} files installed`);
@@ -2150,7 +2199,7 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
2150
2199
  import { join as join7 } from "path";
2151
2200
 
2152
2201
  // src/modules/hub.ts
2153
- import { confirm as confirm2 } from "@inquirer/prompts";
2202
+ import { confirm as confirm2, select as select3 } from "@inquirer/prompts";
2154
2203
  import { readdir as readdir3, writeFile as writeFile5 } from "fs/promises";
2155
2204
  import { join as join6 } from "path";
2156
2205
  var MANIFEST_RETROFITS = [
@@ -2164,6 +2213,36 @@ var MANIFEST_RETROFITS = [
2164
2213
  });
2165
2214
  return { role: isHub ? "hub" : "consumer" };
2166
2215
  }
2216
+ },
2217
+ {
2218
+ field: "tracking_mode",
2219
+ needsRetrofit: (m) => m.tracking_mode === void 0,
2220
+ // Mirrors init.ts's Step 5b branching exactly (EF-14 D5): docs-only
2221
+ // profiles get a silent default, full/lite get asked.
2222
+ ask: async (manifest) => {
2223
+ if (manifest.profile === "governance-only" || manifest.profile === "methodology") {
2224
+ return { tracking_mode: "internal", external_provider: null };
2225
+ }
2226
+ const tracking_mode = await select3({
2227
+ message: "How will work be tracked in this repo?",
2228
+ choices: [
2229
+ { value: "internal", name: "Track work in this repo (TODO.md / PRDs)" },
2230
+ { value: "external", name: "Track work in an external tracker (Azure DevOps / Jira / other)" }
2231
+ ]
2232
+ });
2233
+ if (tracking_mode === "external") {
2234
+ const external_provider = await select3({
2235
+ message: "Which external tracker?",
2236
+ choices: [
2237
+ { value: "ado", name: "Azure DevOps" },
2238
+ { value: "jira", name: "Jira" },
2239
+ { value: "other", name: "Other" }
2240
+ ]
2241
+ });
2242
+ return { tracking_mode, external_provider };
2243
+ }
2244
+ return { tracking_mode, external_provider: null };
2245
+ }
2167
2246
  }
2168
2247
  ];
2169
2248
  async function runManifestRetrofits(manifest) {
@@ -2175,7 +2254,7 @@ async function runManifestRetrofits(manifest) {
2175
2254
  );
2176
2255
  let patch = {};
2177
2256
  for (const retrofit of applicable) {
2178
- const answer = await retrofit.ask();
2257
+ const answer = await retrofit.ask({ ...manifest, ...patch });
2179
2258
  patch = { ...patch, ...answer };
2180
2259
  }
2181
2260
  return patch;