optbrew 0.1.1 → 0.1.3

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.
package/README.md CHANGED
@@ -1,73 +1,148 @@
1
1
  # optbrew
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/optbrew.svg)](https://www.npmjs.com/package/optbrew)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
3
6
  [한국어](README.ko.md)
4
7
 
5
- Optbrew is a local skill and CLI for checking a site's search readiness, making approved local changes, and comparing evidence before and after. It combines five pinned source bundles—mblode SEO, Naver Search Advisor, Firecrawl SEO Audit, Magnus SEO, and SE Ranking SEO—with seven selected marketing guides. One top-level `SKILL.md` routes to the included guides. The bundle works from its pinned files; it does not fetch a moving upstream revision during a run.
8
+ A search-readiness audit skill for coding agents. optbrew checks a site for Google, Naver and AI answer engines, fixes the local files you approve, checks again under the same conditions, and writes a scored report built from replayable evidence.
9
+
10
+ - One skill for five profiles: **SEO**, **NEO** (Naver), **AEO**, **GEO** and **LLMO**.
11
+ - Five pinned audit guide bundles merged into 22 check families. An issue that several guides report is counted once.
12
+ - Every verdict in the report can be recomputed from the raw responses that were collected and hashed.
13
+ - No headline score when the evidence is thin: below 80% coverage the report shows the measured part and its bounds.
14
+ - Works with Codex, Claude Code, Cursor, OpenCode and Pi. Scoring and reports need no API account.
15
+
16
+ ## Why optbrew
17
+
18
+ Being found now means three different things: ranking in Google, showing in Naver, and being cited in AI answers. Each has its own guides, and the guides overlap and disagree. An agent asked to "improve SEO" usually picks one checklist, edits files, and reports success without proof.
19
+
20
+ optbrew gives the agent one workflow and one scoring rule for all five profiles. It counts what it measured, marks what it could not measure as unknown instead of passing it, and keeps a local fix separate from a production result. A high readiness score is not a promise of traffic or citations, and the report says so.
21
+
22
+ ## How it works
23
+
24
+ 1. **Audit.** You give the URL, the goal and the scope. A collector makes at most 50 requests (pages, redirects and robots or sitemap files together; depth 3, two at a time) and stores every response with its hash.
25
+ 2. **Fix.** The agent proposes changes to local files. Nothing is edited, deployed or submitted without your approval.
26
+ 3. **Re-check.** The same collection runs again under the same conditions, so the before and after are comparable.
27
+ 4. **Report.** A score per profile with its coverage, blockers, open core checks, and separate tables for readiness, fixes, real outcomes and remaining work.
28
+
29
+ What the script measures by itself, and what still needs the guides and the agent's judgment:
30
+
31
+ | Profile | Target | Measured by the script | Left to judgment |
32
+ |---|---|---|---|
33
+ | SEO | Google and web search | Fetch status, `noindex` in meta and `X-Robots-Tag`, titles, robots.txt for Googlebot, canonical, redirect chains, discovery through links and sitemap, JSON-LD syntax | Content quality and intent fit |
34
+ | NEO | Naver | robots.txt for Yeti, Open Graph, description, canonical, viewport, title repetition, blocked CSS or JS | Naver-specific content guidance |
35
+ | AEO, GEO | Answer engines and AI search | robots.txt for the AI search bots in [ai-crawlers.md](skills/optbrew/references/ai-crawlers.md) (training bots are never scored) | Answer structure, claims, authorship, freshness |
36
+ | LLMO | How models describe you | Whether name, address, phone and founding year agree across pages, and whether the pages describe one business | Whether those facts are true |
6
37
 
7
- The CLI can calculate a score and write a report from stored JSON without an API account. A site audit needs a declared target and scope. Publication, search engine submission, remote site changes, and paid calls require separate authority. Read [platform support](docs/platforms.md) for the difference between installing a file and confirming that a host invoked it.
38
+ Engine-specific checks run for the engines you name as targets. Say that Naver matters, or name the AI answer engines you care about, such as ChatGPT, Claude or Perplexity. The scoring rule is in [scoring.md](skills/optbrew/references/scoring.md). The 80% coverage threshold is optbrew's own rule, not a search engine standard.
8
39
 
9
40
  ## Install
10
41
 
11
- Requires Node.js 22 or later. The internal Python audit utilities need Python 3.10 or later; missing Python leaves those utilities unrun. From this checkout, first inspect the targets in an isolated project directory named `site`:
42
+ Requires [Node.js](https://nodejs.org) 22 or later. Python 3.10 or later is optional; without it, the internal Python audit utilities stay unrun. Pick one of the five methods. The examples install for Codex and Claude Code at user level; the agent IDs are `codex`, `claude-code`, `cursor`, `opencode` and `pi`.
12
43
 
13
- Bash and PowerShell use the same commands:
44
+ In Windows PowerShell, if `npx` or `optbrew` reports that running scripts is disabled, type `npx.cmd` or `optbrew.cmd` instead.
14
45
 
15
- ```text
16
- node bin/optbrew.mjs install --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --strategy copy --dry-run --json
17
- node bin/optbrew.mjs install --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --strategy copy --json
18
- node bin/optbrew.mjs doctor --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --json
46
+ ### 1. npm
47
+
48
+ ```bash
49
+ npm install -g optbrew@0.1.3
50
+ optbrew install --agents codex,claude-code --scope user --dry-run
51
+ optbrew install --agents codex,claude-code --scope user
52
+ optbrew doctor --agents codex,claude-code --scope user
19
53
  ```
20
54
 
21
- Create `site` first and run these commands from the repository root. `--strategy link` is the default; `copy` also works when links are unavailable. The managed record lives under that project's `.optbrew/install`. For one host, replace the comma-separated list with its exact ID: `codex`, `claude-code`, `cursor`, `opencode`, or `pi`. Use `--scope user` only when you intend to install into user-level host directories. The install command checks the bundle manifest and refuses to replace an unmanaged target.
55
+ The dry run prints the folders it will write. `doctor` checks the files and their hashes.
22
56
 
23
- To get the checkout, clone `https://github.com/nowwcastle-sudo/optbrew` and run the commands above from its root. `npm install -g optbrew` installs the same CLI as an `optbrew` command; replace `node bin/optbrew.mjs` with `optbrew` in the commands above. Native host distribution and the ZIP path are described in [platform support](docs/platforms.md).
57
+ ### 2. npx
24
58
 
25
- ## First use
59
+ Runs the same CLI without a global install:
60
+
61
+ ```bash
62
+ npx optbrew@0.1.3 install --agents codex,claude-code --scope user
63
+ npx optbrew@0.1.3 doctor --agents codex,claude-code --scope user
64
+ ```
26
65
 
27
- Ask the host to use optbrew with the target URL, goal, site ownership or access, allowed local changes, and available evidence. For a local audit, declare the URL scope before collection. The default collection ceilings are 50 URLs, depth 3, and concurrency 2; private addresses need an explicit local target. The workflow records raw evidence, readiness observations, local changes, and follow-up checks. It does not infer a site's production result from a passing local test.
66
+ ### 3. PowerShell (Windows)
28
67
 
29
- The examples below replay bundled synthetic fixtures. They make no provider call. Run them from the checkout root and choose fresh output paths:
68
+ ```powershell
69
+ irm https://raw.githubusercontent.com/nowwcastle-sudo/optbrew/v0.1.3/install.ps1 | iex
70
+ ```
30
71
 
31
- Bash and PowerShell use the same commands:
72
+ The script is pinned to this release. It checks Node.js, shows the target folders, installs for Codex and Claude Code, and stops with an error if `doctor` finds a problem. To read it first, or to choose agents or a project folder:
32
73
 
33
- ```text
34
- node bin/optbrew.mjs score --input ./before.json --output ./score.json
35
- node bin/optbrew.mjs compare --before ./before.json --after ./after.json --output ./comparison.json
36
- node bin/optbrew.mjs report --input ./remote-only.json --output-dir ./report-output
74
+ ```powershell
75
+ irm https://raw.githubusercontent.com/nowwcastle-sudo/optbrew/v0.1.3/install.ps1 -OutFile install.ps1
76
+ powershell -ExecutionPolicy Bypass -File .\install.ps1 -Agents codex,claude-code,cursor
37
77
  ```
38
78
 
39
- For those literal commands, copy `tests/fixtures/scoring/before.json` and `after.json` into the current directory, and copy `tests/fixtures/reports/remote-only.json` plus its sibling evidence files into the current directory. For real work, pass your own validated `ScoreInput` or `ReportPacket` paths. See [scoring](skills/optbrew/references/scoring.md), [measurement](skills/optbrew/references/measurement.md), and [report evidence](skills/optbrew/references/report.md) for the contracts.
79
+ Add `-Scope project -Project C:\path\to\site` to install into one project.
40
80
 
41
- ## Score and report
81
+ ### 4. Plugin
42
82
 
43
- The five optbrew readiness profiles are SEO, NEO when Naver is a target, AEO, GEO, and LLMO. The 22 unique check families are counted once even when several guides find the same issue. An unknown applicable check remains in the denominator; an evidenced `not_applicable` check is excluded. The result includes score, measured coverage, missing-data bounds, blockers, and unresolved core checks. The 80% coverage threshold is an optbrew rule, not a search engine standard. A high readiness score does not prove indexing, traffic, citations, accuracy, conversions, or revenue.
83
+ Claude Code:
44
84
 
45
- The report keeps readiness, remedies, actual outcomes, and remaining work in separate tables. Actual outcomes need owner exports or a fixed AI question panel. A normal outcome comparison uses two complete, adjacent 28-day windows with matching definitions and filters. A local edit remains local until deployment and a production observation have their own evidence.
85
+ ```bash
86
+ claude plugin marketplace add nowwcastle-sudo/optbrew
87
+ claude plugin install optbrew@optbrew
88
+ ```
46
89
 
47
- ## APIs and cost
90
+ Codex:
48
91
 
49
- API access is optional. A Claude or Claude Code subscription does not supply Anthropic API billing. To use a direct API key, follow the [official key guide](https://platform.claude.com/docs/en/get-api-key): Console → Settings → API keys. Put the key in the `ANTHROPIC_API_KEY` environment variable in your own shell or secret manager; never put a real value in a report. A key spanning multiple Anthropic workspaces may require an `anthropic-workspace-id` header. The direct adapter introduced in v0.1.0 still does not set that header, so use a compatible key or a separately approved host capability.
92
+ ```bash
93
+ codex plugin marketplace add nowwcastle-sudo/optbrew
94
+ codex plugin add optbrew@optbrew
95
+ ```
96
+
97
+ Plugins follow this repository's `main` branch and update through the host's plugin commands.
98
+
99
+ ### 5. Ask your agent
50
100
 
51
- The direct adapter accepts only text `{model,max_tokens,messages}` requests for the exact model IDs `claude-fable-5-1`, `claude-opus-5-5`, `claude-sonnet-5-5`, and `claude-haiku-5-5`. `max_tokens` cannot exceed 128,000. Each attempt needs one token reservation of at least `1,000,000 + max_tokens`, a matching ledger budget, and an approval reference. This is a worst-case capacity bound, not a bill estimate. Record the current price source, estimated spend, and approved stop condition before the first paid call. Extra fields, tools, search, non-text input, unknown models, or currency-only bounds make zero direct calls. See [API boundaries](skills/optbrew/references/api.md).
101
+ Paste this into Codex, Claude Code or another coding agent:
52
102
 
53
- SE Ranking has its own [API](https://seranking.com/api.html), [MCP instructions](https://seranking.com/api/integrations/mcp), account plan, and credits. Its [HTTP MCP endpoint](https://api.seranking.com/mcp) uses its documented OAuth or direct-token path, independently of `ANTHROPIC_API_KEY`. Available tools depend on the subscription. Neither connection is required for offline scoring. Use actual account prices and credits when deciding whether to run a paid capability; no price is built into this README.
103
+ ```text
104
+ Install the optbrew skill for me. Read https://raw.githubusercontent.com/nowwcastle-sudo/optbrew/v0.1.3/AGENT_INSTALL.md and follow it step by step. Ask me which agents and which scope to use, and show me the planned changes before you install.
105
+ ```
54
106
 
55
- ## Update and rollback
107
+ [AGENT_INSTALL.md](AGENT_INSTALL.md) gives the agent a command and a pass check for each step, and the cases where it must stop and ask you.
56
108
 
57
- For a managed project installation, rebuild or obtain a verified newer optbrew bundle before updating. From the checkout root:
109
+ Installing from a source checkout, the Agent Skills CLI, Pi packages and Claude web uploads are covered in [platform support](docs/platforms.md).
58
110
 
59
- Bash and PowerShell use the same commands:
111
+ ## Quick start
112
+
113
+ Start a new agent session and ask, for example:
60
114
 
61
115
  ```text
62
- node bin/optbrew.mjs check-updates --json
63
- node bin/optbrew.mjs update --scope project --project ./site --json
64
- node bin/optbrew.mjs rollback --scope project --project ./site --release-version 0.1.0 --json
116
+ Run optbrew on https://example.com. Goal: show up in Naver and get cited in AI answers.
65
117
  ```
66
118
 
67
- Without a confirmed registry package, `check-updates` reports `local_only`. `update` uses the current CLI bundle, verifies it, and updates all targets in that install record; it does not silently replace local edits. `rollback` needs that version in the record's retained prior releases. The shown rollback will therefore fail on a fresh 0.1.1 install and applies to a managed installation that retains local 0.1.0. `npm update -g optbrew` updates npm's package files after release; it does not rewrite a copy already placed in an agent skills directory. Run the managed `update` command again after updating the CLI. Native plugin and Pi package update paths are in [platform support](docs/platforms.md).
119
+ optbrew asks for anything it is missing: site ownership or access, which local changes are allowed, and which evidence you already have. Raw evidence goes to `.optbrew/runs`. Private network addresses are fetched only when you declare a local target.
120
+
121
+ ## Update
122
+
123
+ ```bash
124
+ npm install -g optbrew@latest
125
+ optbrew update --scope user
126
+ ```
127
+
128
+ With npx, run `npx optbrew@latest update --scope user`. `optbrew check-updates --package optbrew` tells you, without changing anything, whether npm has a newer release than the CLI you are running; `optbrew doctor --json` shows the `release_version` installed in your agent folders. `npm update` alone does not touch copies already placed in agent folders; the `update` command does.
129
+
130
+ An install from 0.1.2 or earlier stops with `migration_required`, because 0.1.3 scores with a new evaluation version. Scores from different evaluation versions are not comparable. Follow [Reinstall after a migration_required update](docs/platforms.md#reinstall-after-a-migration_required-update); the steps delete only paths that a read-only check printed and never touch your audit evidence.
131
+
132
+ ## APIs and cost
133
+
134
+ Nothing here requires a paid account. A direct Anthropic API call or an [SE Ranking](https://seranking.com/api.html) call needs your approval, a recorded budget and a stop condition first; a Claude subscription does not include API billing. Keys stay in your environment variables and never go into a report. Details: [API boundaries](skills/optbrew/references/api.md).
135
+
136
+ ## Documentation
137
+
138
+ - [Platform support](docs/platforms.md): install paths per agent, checkout commands, reinstall steps
139
+ - [Scoring](skills/optbrew/references/scoring.md), [measurement](skills/optbrew/references/measurement.md) and [report evidence](skills/optbrew/references/report.md)
140
+ - [Changelog](CHANGELOG.md) and [release process](docs/release.md)
141
+
142
+ ## Contributing
68
143
 
69
- ## Support and license
144
+ Contributions are welcome: bug reports, false positives from a real site, guide updates, translations and code. See [CONTRIBUTING.md](CONTRIBUTING.md).
70
145
 
71
- Managed install, doctor, update, and rollback cover Codex, Claude Code, Cursor, OpenCode, and Pi at the documented default paths. Custom host directories, project trust, and session reload rules can change discovery. Ordinary ChatGPT chat uses attached files and instructions; it is not the same installation path as Codex or a Work/desktop plugin. Claude web requires manual ZIP upload and re-upload for updates. Host discovery and a successful real invocation require separate checks.
146
+ ## License
72
147
 
73
- The Naver bundle retains an index of official [Search Advisor guides](https://searchadvisor.naver.com/guide). It excludes the 55 original Naver article bodies, so full offline article lookup is unavailable. Follow the official links while online. Included source licenses and changes are listed in [third-party notices](skills/optbrew/THIRD_PARTY_NOTICES.md); optbrew's own code is [MIT licensed](LICENSE). Service access and API data rights are separate from those code licenses. Release packaging and verification are in [release instructions](docs/release.md).
148
+ optbrew's own code is [MIT licensed](LICENSE). The bundled guides keep their own licenses, listed in [third-party notices](skills/optbrew/THIRD_PARTY_NOTICES.md). The Naver bundle keeps an index of the official [Search Advisor guides](https://searchadvisor.naver.com/guide) but not the 55 article bodies; follow the official links while online.
package/docs/platforms.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Platform installation and update paths
2
2
 
3
- Optbrew v0.1.1 contains one skill source at `skills/optbrew`. The managed CLI can place it at five hosts' documented default paths. A file in a host path proves placement, not that the host discovered, loaded, or invoked the skill. Confirm those three steps in a fresh host session before reporting host support as exercised.
3
+ Optbrew v0.1.3 contains one skill source at `skills/optbrew`. The managed CLI can place it at five hosts' documented default paths. A file in a host path proves placement, not that the host discovered, loaded, or invoked the skill. Confirm those three steps in a fresh host session before reporting host support as exercised.
4
4
 
5
5
  | Host | Managed project target | Managed user target | Discovery and invocation boundary |
6
6
  |---|---|---|---|
@@ -14,17 +14,130 @@ The CLI maps only these default targets. Custom roots, host policies, remote age
14
14
 
15
15
  ## Managed checkout and npm paths
16
16
 
17
- The [README](../README.md) shows parser-checked checkout commands for install, doctor, update, and rollback. The CLI validates bundle hashes before an install or update. `update` replaces every target in one managed record as a unit. An edited target makes the default update stop; the explicit `--local-changes backup-and-replace` policy retains a readable backup. `rollback --release-version VERSION` works only when that version is in the record's retained prior releases. A fresh installation has no older release.
17
+ The [README](../README.md) shows the npm, npx, PowerShell, plugin and agent install routes. This section holds the parser-checked commands for a source checkout. Clone `https://github.com/nowwcastle-sudo/optbrew`, create an empty `site` folder, and run them from the repository root. With the npm package installed, replace `node bin/optbrew.mjs` with `optbrew`.
18
18
 
19
- After a public npm release, `npm install -g optbrew` and `npm update -g optbrew` manage the package in npm. Neither command rewrites an old copied skill target. Run the installed CLI's managed `update` after the package update, then `doctor` and a new-session invocation check. `check-updates --json` is read-only; without a confirmed registry name it returns `local_only`. The package is not public merely because `package.json` names it.
19
+ Install into an isolated project folder, after a dry run:
20
+
21
+ Bash and PowerShell use the same commands:
22
+
23
+ ```text
24
+ node bin/optbrew.mjs install --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --strategy copy --dry-run --json
25
+ node bin/optbrew.mjs install --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --strategy copy --json
26
+ node bin/optbrew.mjs doctor --agents codex,claude-code,cursor,opencode,pi --scope project --project ./site --json
27
+ ```
28
+
29
+ `--strategy link` is the default; `copy` also works when links are unavailable. The managed record lives under that project's `.optbrew/install`. For one host, replace the comma-separated list with its exact ID. Use `--scope user` only when you intend to install into user-level host directories. The install command checks the bundle manifest and refuses to replace an unmanaged target.
30
+
31
+ Replay the bundled synthetic fixtures without any provider call. Copy `tests/fixtures/scoring/before.json` and `after.json`, and `tests/fixtures/reports/remote-only.json` with its sibling evidence files, into the current directory first:
32
+
33
+ Bash and PowerShell use the same commands:
34
+
35
+ ```text
36
+ node bin/optbrew.mjs score --input ./before.json --output ./score.json
37
+ node bin/optbrew.mjs compare --before ./before.json --after ./after.json --output ./comparison.json
38
+ node bin/optbrew.mjs report --input ./remote-only.json --output-dir ./report-output
39
+ ```
40
+
41
+ Check for updates, update, and roll back a managed project install:
42
+
43
+ Bash and PowerShell use the same commands:
44
+
45
+ ```text
46
+ node bin/optbrew.mjs check-updates --json
47
+ node bin/optbrew.mjs update --scope project --project ./site --json
48
+ node bin/optbrew.mjs rollback --scope project --project ./site --release-version 0.1.0 --json
49
+ ```
50
+
51
+ The shown rollback fails on a fresh 0.1.3 install, because no earlier release is retained; it applies to a managed install that retains local 0.1.0.
52
+
53
+ The CLI validates bundle hashes before an install or update. `update` replaces every target in one managed record as a unit. An edited target makes the default update stop; the explicit `--local-changes backup-and-replace` policy retains a readable backup. `rollback --release-version VERSION` works only when that version is in the record's retained prior releases. A fresh installation has no older release.
54
+
55
+ After a public npm release, `npm install -g optbrew` and `npm update -g optbrew` manage the package in npm. Neither command rewrites an old copied skill target. Run the installed CLI's managed `update` after the package update, then `doctor` and a new-session invocation check. `check-updates --package optbrew --json` is read-only and compares the installed release with the npm registry; without `--package` it returns `local_only`.
56
+
57
+ ## Reinstall after a migration_required update
58
+
59
+ Release 0.1.3 uses evaluation version 0.3.0; 0.1.2 used 0.2.0. The managed updater refuses a change of evaluation version, so `optbrew update` on a managed 0.1.2 install stops with `migration_required` and changes nothing. There is no `uninstall` command. Replace the install by hand: the blocks below delete the install's targets and then its managed root, and run `install` again from the 0.1.3 checkout. Scores and baselines made with 0.1.2 are not comparable with 0.1.3; `compare` returns `not_comparable` (`evaluation_version_changed`). Run the baseline audit again with 0.1.3.
60
+
61
+ The blocks do not trust the install record. Each one first runs the read-only `node bin/optbrew.mjs reinstall-plan`, which resolves the home directory the way the CLI does (`HOME`, else the operating system's home directory), checks that the record belongs to this scope, that every recorded target is exactly the agent's `skills/optbrew` path (absolute, no `..`, no linked folder on the way, inside the scope or project), and that an existing target is a link to the managed bundle or a real folder holding a bundle manifest. If any check fails it prints the reason, exits with a nonzero code, and the block deletes nothing. Otherwise the shell deletes only the paths it printed: the targets first, then `<scope root>/.optbrew/installs/user` (user scope) or `<project>/.optbrew/install` (project scope), which holds the install record. Because the record goes last, you can run the same block again after a failure. The commands never touch `<scope root>/.optbrew/runs`, where your audit evidence lives; do not delete the whole `.optbrew` folder. Each agent is installed again with the strategy (`link` or `copy`) the record had, then `doctor` runs. Run the blocks from the 0.1.3 checkout root. If `reinstall-plan` refuses the record, fix or remove the named path by hand and look at it first; the CLI also refuses to overwrite a target it does not own.
62
+
63
+ User scope:
64
+
65
+ ```powershell user
66
+ $ErrorActionPreference = 'Stop'
67
+ [Console]::OutputEncoding = [Text.Encoding]::UTF8
68
+ $plan = @(node bin/optbrew.mjs reinstall-plan --scope user)
69
+ if ($LASTEXITCODE) { exit $LASTEXITCODE }
70
+ foreach ($line in $plan) {
71
+ $f = $line -split "`t"
72
+ if ($f[0] -eq 'delete-link') { [IO.Directory]::Delete($f[1]) }
73
+ elseif ($f[0] -eq 'delete-dir') { Remove-Item -Recurse -Force -LiteralPath $f[1] }
74
+ }
75
+ foreach ($line in $plan) {
76
+ $f = $line -split "`t"
77
+ if ($f[0] -eq 'install') { node bin/optbrew.mjs install --agents $f[1] --scope user --strategy $f[2]; if ($LASTEXITCODE) { exit $LASTEXITCODE } }
78
+ elseif ($f[0] -eq 'agents') { node bin/optbrew.mjs doctor --agents $f[1] --scope user; exit $LASTEXITCODE }
79
+ }
80
+ ```
81
+
82
+ ```bash user
83
+ set -e
84
+ plan=$(node bin/optbrew.mjs reinstall-plan --scope user)
85
+ while IFS=$'\t' read -r kind a b; do
86
+ case "$kind" in delete-link | delete-dir) rm -r -- "$a" ;; esac
87
+ done <<< "$plan"
88
+ while IFS=$'\t' read -r kind a b; do
89
+ case "$kind" in
90
+ install) node bin/optbrew.mjs install --agents "$a" --scope user --strategy "$b" ;;
91
+ agents) node bin/optbrew.mjs doctor --agents "$a" --scope user ;;
92
+ esac
93
+ done <<< "$plan"
94
+ ```
95
+
96
+ Project scope (replace `./site` with your project folder; it appears once):
97
+
98
+ ```powershell project
99
+ $ErrorActionPreference = 'Stop'
100
+ [Console]::OutputEncoding = [Text.Encoding]::UTF8
101
+ $project = './site'
102
+ $plan = @(node bin/optbrew.mjs reinstall-plan --scope project --project $project)
103
+ if ($LASTEXITCODE) { exit $LASTEXITCODE }
104
+ foreach ($line in $plan) {
105
+ $f = $line -split "`t"
106
+ if ($f[0] -eq 'delete-link') { [IO.Directory]::Delete($f[1]) }
107
+ elseif ($f[0] -eq 'delete-dir') { Remove-Item -Recurse -Force -LiteralPath $f[1] }
108
+ }
109
+ foreach ($line in $plan) {
110
+ $f = $line -split "`t"
111
+ if ($f[0] -eq 'install') { node bin/optbrew.mjs install --agents $f[1] --scope project --project $project --strategy $f[2]; if ($LASTEXITCODE) { exit $LASTEXITCODE } }
112
+ elseif ($f[0] -eq 'agents') { node bin/optbrew.mjs doctor --agents $f[1] --scope project --project $project; exit $LASTEXITCODE }
113
+ }
114
+ ```
115
+
116
+ ```bash project
117
+ set -e
118
+ project='./site'
119
+ plan=$(node bin/optbrew.mjs reinstall-plan --scope project --project "$project")
120
+ while IFS=$'\t' read -r kind a b; do
121
+ case "$kind" in delete-link | delete-dir) rm -r -- "$a" ;; esac
122
+ done <<< "$plan"
123
+ while IFS=$'\t' read -r kind a b; do
124
+ case "$kind" in
125
+ install) node bin/optbrew.mjs install --agents "$a" --scope project --project "$project" --strategy "$b" ;;
126
+ agents) node bin/optbrew.mjs doctor --agents "$a" --scope project --project "$project" ;;
127
+ esac
128
+ done <<< "$plan"
129
+ ```
130
+
131
+ Inside 0.1.3 and later releases that keep evaluation version 0.3.0, `update` and `rollback` work as before. The same refusal and steps apply to any future release that changes the evaluation version.
20
132
 
21
133
  ## Native host routes
22
134
 
23
135
  These are alternatives to the managed CLI. Do not mix their ownership records for the same target path.
24
136
 
25
137
  - **Agent Skills tool:** Current [Vercel Skills CLI](https://github.com/vercel-labs/skills) documents `npx skills add ./` for a local source and `npx skills update optbrew` for an installation it owns. Run from a checkout containing the skill. Confirm its host selection and destination before writing. The public repository is `https://github.com/nowwcastle-sudo/optbrew`.
26
- - **Claude Code plugin:** The local `.claude-plugin/marketplace.json` and `plugin.json` name `optbrew@optbrew-local` at version 0.1.1. The installed Claude CLI help accepts `claude plugin marketplace add --scope project .`, then `claude plugin install --scope project optbrew@optbrew-local` from this checkout. Later, `claude plugin update --scope project optbrew@optbrew-local` applies the marketplace's newer version and requires a restart. These commands change Claude settings, so documentation checks inspect `--help` without registering a marketplace. [Claude Code plugin instructions](https://code.claude.com/docs/en/plugins) also describe the interactive `/plugin` path. Marketplace auto-update is a user setting; the marketplace file does not enable it. Verify the selected release version and a fresh-session invocation after a native update.
27
- - **Pi package:** Current [Pi package instructions](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) document `pi install ./local-package`, `pi install npm:package-name@version`, `pi list`, and `pi update --extensions`. `pi install ./` installs from a local checkout when Pi is installed; `pi install npm:optbrew@0.1.1` installs the published npm package. The package's `pi.skills` entry points to `./skills/optbrew`. A project-local package declaration uses Pi's `--local` option and is subject to project trust. Check the installed Pi version's help and package list before updating.
138
+ - **Claude Code plugin:** `.claude-plugin/marketplace.json` and `plugin.json` name the plugin `optbrew@optbrew` at version 0.1.3. Run `claude plugin marketplace add nowwcastle-sudo/optbrew`, then `claude plugin install optbrew@optbrew` (add `--scope project` to keep it in one project). Later, `claude plugin update optbrew@optbrew` applies the marketplace's newer version and requires a restart. `claude plugin validate .` checks both files from a checkout.
139
+ - **Codex plugin:** `.agents/plugins/marketplace.json` lists the same `optbrew` plugin. Run `codex plugin marketplace add nowwcastle-sudo/optbrew`, then `codex plugin add optbrew@optbrew`, or choose it in `/plugins`. Start a new Codex task afterwards. [Claude Code plugin instructions](https://code.claude.com/docs/en/plugins) also describe the interactive `/plugin` path. Marketplace auto-update is a user setting; the marketplace file does not enable it. Verify the selected release version and a fresh-session invocation after a native update.
140
+ - **Pi package:** Current [Pi package instructions](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) document `pi install ./local-package`, `pi install npm:package-name@version`, `pi list`, and `pi update --extensions`. `pi install ./` installs from a local checkout when Pi is installed; `pi install npm:optbrew@0.1.3` installs the published npm package. The package's `pi.skills` entry points to `./skills/optbrew`. A project-local package declaration uses Pi's `--local` option and is subject to project trust. Check the installed Pi version's help and package list before updating.
28
141
  - **Claude web:** Upload the built skill ZIP manually in the supported web interface. Upload a new ZIP again for each update. There is no verified automatic update of that upload.
29
142
  - **Ordinary ChatGPT chat:** Attach the relevant skill files or ZIP and give the assistant the target and scope. This is attachment-guided use. It is not the Codex installation path and does not establish Work/desktop plugin discovery.
30
143
 
package/docs/release.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # Release procedure
2
2
 
3
- This checkout is version 0.1.1. A package name, local manifest, or generated ZIP does not establish a public release. Publication, a Git tag, a push, and account changes require their own authorization. This page describes the reviewable local release artifacts and the checks to run before that decision.
3
+ This checkout is version 0.1.3. A package name, local manifest, or generated ZIP does not establish a public release. Publication, a Git tag, a push, and account changes require their own authorization. This page describes the reviewable local release artifacts and the checks to run before that decision.
4
4
 
5
5
  1. Confirm the source commit and clean intended diff. Match `package.json`, the portable plugin, Codex plugin, Claude plugin, marketplace entry, and ZIP name to one release version. Keep `evaluation_version` and `protocol_version` separate from the release version.
6
6
  2. Run `npm test` and `python -m unittest discover -s tests/python` on Python 3.10 or later. Run the focused `node --test tests/docs-commands.test.mjs` check when changing examples. The command check runs optbrew against isolated local fixtures; it makes no provider call.
7
- 3. Run `npm run build-bundle`, `node scripts/build-release.mjs --version 0.1.1 --output-dir dist`, `npm pack --pack-destination dist`, `node scripts/verify-release.mjs --dist dist`, and `npm pack --dry-run --json` from the repository root. Rebuild after changing the README or packaged files. Do not treat an older tarball as containing a later documentation edit.
7
+ 3. Run `npm run build-bundle`, `node scripts/build-release.mjs --version 0.1.3 --output-dir dist`, `npm pack --pack-destination dist`, `node scripts/verify-release.mjs --dist dist`, and `npm pack --dry-run --json` from the repository root. Rebuild after changing the README or packaged files. Do not treat an older tarball as containing a later documentation edit.
8
8
  4. Inspect the npm file list, the ZIP file list, `bundle-manifest.json`, `source-lock.json`, licenses, and secret scan output. The same skill bytes and version must be traceable across the npm package and ZIP. Source notices belong in both. User run evidence, account configuration, keys, private cards, and authoring materials must stay outside both.
9
9
  5. Test a fresh isolated managed install, `doctor`, update from a retained earlier release, rollback, and fresh host discovery or invocation where a host is available. A dry run or manifest check does not prove invocation. Record the untested hosts and capabilities.
10
10
  6. Only after a reviewed release decision, publish or upload the approved artifacts and verify the public npm and repository readback. Do not infer publication from successful local builds.
11
+ 7. `main` is protected by a repository ruleset: no force push, no deletion, changes only through a pull request, linear history. Tags matching `v*` cannot be moved or deleted. Merge the release branch through a pull request, then tag the merge commit `vX.Y.Z`. The README's PowerShell installer and agent prompt load `install.ps1` and `AGENT_INSTALL.md` from that tag, so test both raw URLs after the tag is pushed.
11
12
 
12
13
  The bundle includes five pinned source bundles plus seven selected marketing guides. The Naver source articles under `references/guide/` (55 original bodies) remain excluded; the index links to official pages and full offline article lookup is unavailable. The published notice must retain upstream MIT/ISC permission texts and modifications. Optbrew's own code uses MIT. These code licenses do not grant service subscriptions or API data rights.
13
14
 
package/lib/cli.mjs CHANGED
@@ -8,10 +8,11 @@ import { checkUpdates, update, rollback } from './update.mjs';
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { readInstallRecord } from './records.mjs';
10
10
  import { runCli } from '../skills/optbrew/scripts/cli.mjs';
11
+ import { reinstallPlan } from './reinstall.mjs';
11
12
 
12
13
  const defaultBundle = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../skills/optbrew');
13
14
  class UsageError extends Error {}
14
- const usage = 'usage: optbrew <install|doctor|check-updates|update|rollback|score|compare|report> [options]';
15
+ const usage = 'usage: optbrew <install|doctor|check-updates|update|rollback|reinstall-plan|score|compare|report> [options]';
15
16
 
16
17
  function parseOptions(argv, allowed) {
17
18
  const options = {};
@@ -61,7 +62,19 @@ export async function main(argv, { stdout = process.stdout, stderr = process.std
61
62
  stdout.write(`${command} complete\n`);
62
63
  return 0;
63
64
  }
64
- if (!['install', 'doctor', 'check-updates', 'update', 'rollback'].includes(command)) throw new UsageError(usage);
65
+ if (!['install', 'doctor', 'check-updates', 'update', 'rollback', 'reinstall-plan'].includes(command)) throw new UsageError(usage);
66
+ if (command === 'reinstall-plan') {
67
+ // Read-only: validates the install record and prints what the documented manual reinstall may delete.
68
+ const parsed = parseOptions(rest, ['--scope', '--home', '--project']);
69
+ const scope = parsed['--scope'] ?? 'user';
70
+ if (!['user', 'project'].includes(scope)) throw new UsageError('--scope must be user or project');
71
+ if (scope === 'project' && !parsed['--project']) throw new UsageError('project scope requires --project');
72
+ if (scope === 'user' && parsed['--project']) throw new UsageError('--project requires project scope');
73
+ const rows = await reinstallPlan({ scope, home: parsed['--home'] ? path.resolve(cwd, parsed['--home']) : env?.HOME ?? os.homedir(),
74
+ projectRoot: parsed['--project'] ? path.resolve(cwd, parsed['--project']) : undefined });
75
+ stdout.write(`${rows.map(row => row.join('\t')).join('\n')}\n`);
76
+ return 0;
77
+ }
65
78
  if (command === 'check-updates') {
66
79
  const parsed = parseOptions(rest, ['--package', '--json']);
67
80
  const manifest = JSON.parse(await readFile(path.join(bundleRoot, 'bundle-manifest.json'), 'utf8'));
package/lib/doctor.mjs CHANGED
@@ -30,6 +30,7 @@ export async function doctor({ agents, scope, home, projectRoot }) {
30
30
  if (saved.strategy === 'copy' && (await lstat(target.path)).isSymbolicLink()) throw new Error('copy replaced by link');
31
31
  const manifest = JSON.parse(await readFile(path.join(target.path, 'bundle-manifest.json'), 'utf8'));
32
32
  const check = await verifyBundle({ bundleRoot: target.path, manifest });
33
+ if (check.errors.some(e => e.code === 'migration_required')) warnings.push(`${target.agent}: installed release ${manifest.release_version} was built for another evaluation version; update is refused (migration_required). See docs/platforms.md, "Reinstall after a migration_required update".`);
33
34
  hashOk = !recoveryPending && record.transaction.state === 'committed' && check.status === 'valid' && check.bundle_hash === record.bundle_hash && manifest.release_version === record.release_version;
34
35
  } catch (error) { warnings.push(`${target.agent}: ${error.message}`); }
35
36
  }
@@ -0,0 +1,92 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readInstallRecord } from './records.mjs';
4
+ import { scopePaths } from './paths.mjs';
5
+ import { validateScope, statOrNull } from './transaction.mjs';
6
+
7
+ const powershell = value => `'${String(value).replaceAll("'", "''")}'`;
8
+ const posix = value => `'${String(value).replaceAll("'", "'\\''")}'`;
9
+
10
+ /**
11
+ * Read-only plan for the manual reinstall of a managed scope. It validates the install record before anything is
12
+ * deleted: the record belongs to this scope, every recorded target is exactly the expected skills/optbrew path of its
13
+ * agent (absolute, no "..", no linked ancestor, inside the scope), and every target that exists is what the installer
14
+ * created (a link to the managed bundle, or a real directory holding a bundle manifest). Any failed check throws, so a
15
+ * caller that deletes only what this returns never deletes anything for a tampered record.
16
+ *
17
+ * Rows: `delete-link|delete-dir <path>` (recorded targets first, the managed root last), then `install <agent> <strategy>`
18
+ * per recorded agent in record order, then `agents <csv>` for the final doctor call.
19
+ */
20
+ export async function reinstallPlan({ scope, home, projectRoot }) {
21
+ const refuse = message => { throw new Error(`reinstall plan refused: ${message}`); };
22
+ // One output line per path: a line break, tab or other control character would split a row or inject another one.
23
+ const plain = value => typeof value === 'string' && [...value].every(ch => { const c = ch.codePointAt(0); return c > 31 && (c < 127 || c > 159) && c !== 8232 && c !== 8233; });
24
+ const { scopeRoot, managedRoot } = scopePaths({ scope, home, projectRoot });
25
+ if (!plain(scopeRoot) || !plain(managedRoot)) refuse('the scope path contains a control character (a line break, tab or similar)');
26
+ const record = await readInstallRecord(managedRoot).catch(error => refuse(`install record unreadable (${error.message})`));
27
+ if (!record) refuse(`no install record in ${managedRoot}`);
28
+ if (record.scope_id !== `${scope}:${scopeRoot}` || record.managed_root !== managedRoot) refuse('the install record belongs to another scope');
29
+ if (!record.targets.length) refuse('the install record lists no target');
30
+ for (const target of record.targets) {
31
+ if (!plain(target.path) && typeof target.path === 'string') refuse('a recorded target path contains a control character (a line break, tab or similar)');
32
+ if (typeof target.path !== 'string' || !path.isAbsolute(target.path) || target.path.split(/[\\/]/).includes('..'))
33
+ refuse(`recorded target is not a plain absolute path: ${JSON.stringify(target.path)}`);
34
+ }
35
+ try { await validateScope(managedRoot, record); }
36
+ catch (error) { refuse(`${error.message} (expected exactly the agent's skills/optbrew path inside ${scopeRoot})`); }
37
+ const managedBundle = path.join(managedRoot, 'bundle');
38
+ const rows = [];
39
+ for (const target of record.targets) {
40
+ const found = await statOrNull(target.path);
41
+ if (!found) continue;
42
+ if (found.isSymbolicLink()) {
43
+ if (target.strategy !== 'link' || await realpath(target.path) !== await realpath(managedBundle))
44
+ refuse(`${target.path} is a link that does not lead to the managed bundle`);
45
+ rows.push(['delete-link', target.path]);
46
+ } else if (found.isDirectory()) {
47
+ if (target.strategy !== 'copy') refuse(`${target.path} was recorded as a link but is a directory`);
48
+ try { await lstat(path.join(target.path, 'bundle-manifest.json')); }
49
+ catch { refuse(`${target.path} holds no optbrew bundle manifest`); }
50
+ rows.push(['delete-dir', target.path]);
51
+ } else refuse(`${target.path} is neither a link nor a directory`);
52
+ }
53
+ rows.push(['delete-dir', managedRoot]);
54
+ for (const target of record.targets) rows.push(['install', target.agent, target.strategy]);
55
+ rows.push(['agents', record.targets.map(target => target.agent).join(',')]);
56
+ if (!rows.every(row => row.every(plain))) refuse('a path or name in the plan contains a control character (a line break, tab or similar)');
57
+ return rows;
58
+ }
59
+
60
+ /**
61
+ * Exact manual reinstall steps for a managed scope whose update was refused with migration_required.
62
+ * Targets are deleted first and the managed root (which holds the install record) last, so the steps can be repeated
63
+ * after a partial failure. `.optbrew/runs` is never deleted.
64
+ */
65
+ export function reinstallGuidance({ record, recordRoot, from, to }) {
66
+ const separator = record.scope_id.indexOf(':');
67
+ const scope = record.scope_id.slice(0, separator), scopeRoot = record.scope_id.slice(separator + 1);
68
+ const agents = record.targets.map(target => target.agent).join(',');
69
+ const place = quote => scope === 'user' ? `--scope user --home ${quote(scopeRoot)}` : `--scope project --project ${quote(scopeRoot)}`;
70
+ const removePowershell = record.targets.map(target => target.strategy === 'link' ?
71
+ `[IO.Directory]::Delete(${powershell(target.path)})` : `Remove-Item -Recurse -Force -LiteralPath ${powershell(target.path)}`);
72
+ removePowershell.push(`Remove-Item -Recurse -Force -LiteralPath ${powershell(recordRoot)}`);
73
+ const removePosix = `rm -r -- ${[...record.targets.map(target => target.path), recordRoot].map(posix).join(' ')}`;
74
+ const install = quote => record.targets.map(target =>
75
+ `optbrew install --agents ${target.agent} ${place(quote)} --strategy ${target.strategy}`);
76
+ const doctor = quote => `optbrew doctor --agents ${agents} ${place(quote)}`;
77
+ return [
78
+ `Release ${to} uses a new evaluation contract, so a managed update from ${from} cannot carry this install over, and scores from the two releases are not comparable. Reinstall instead; your runs folder (${scopeRoot}/.optbrew/runs) is not touched, so never delete it.`,
79
+ '1. Check the install record first. This read-only command refuses a tampered record and prints exactly what will be deleted:',
80
+ ` optbrew reinstall-plan ${place(posix)}`,
81
+ '2. Delete only those paths, the targets first and the managed root last (repeat the same steps if one fails):',
82
+ ' PowerShell:',
83
+ ...removePowershell.map(line => ` ${line}`),
84
+ ` POSIX shell: ${removePosix}`,
85
+ '3. Install again, one command per recorded agent, from the checkout of the new release:',
86
+ ...['PowerShell', 'POSIX shell'].flatMap((label, i) => [` ${label}:`, ...install(i ? posix : powershell).map(line => ` ${line}`)]),
87
+ '4. Verify:',
88
+ ` PowerShell: ${doctor(powershell)}`,
89
+ ` POSIX shell: ${doctor(posix)}`,
90
+ 'The same steps as copy-paste blocks that do these checks for you: docs/platforms.md, "Reinstall after a migration_required update".',
91
+ ].join('\n');
92
+ }
package/lib/update.mjs CHANGED
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { createBundleManifest } from '../skills/optbrew/scripts/core/bundle.mjs';
5
5
  import { readInstallRecord, writeDurableJson, writeInstallRecord } from './records.mjs';
6
+ import { reinstallGuidance } from './reinstall.mjs';
6
7
  import { assertManagedPath, copyBundle, fingerprint, finishJournal, managedRename, operationPaths,
7
8
  readJournal, recoverTransaction, statOrNull, validateScope, verifiedBundle, withScopeLock } from './transaction.mjs';
8
9
 
@@ -110,9 +111,16 @@ async function applyRelease({ bundleRoot, recordRoot, localChanges, faultHook, k
110
111
  const managed = path.join(recordRoot, 'bundle');
111
112
  let previousRoot = managed;
112
113
  if (conflicts.some(c => c.path === managed)) previousRoot = before.source;
113
- const previous = await verifiedBundle(previousRoot);
114
+ // The guard stays as is. A refusal for another evaluation version carries the exact manual reinstall steps.
115
+ const refuse = error => {
116
+ if (!/migration_required/.test(error.message)) throw error;
117
+ const reason = error.message.startsWith('migration_required') ? error.message
118
+ : 'migration_required: evaluation_version changed (the installed bundle was built for another evaluation version)';
119
+ throw new Error(`${reason}\n${reinstallGuidance({ record: before, recordRoot, from: before.release_version, to: source.manifest.release_version })}`);
120
+ };
121
+ const previous = await verifiedBundle(previousRoot).catch(refuse);
114
122
  if (!matchesRecord(previous.manifest, before)) throw new Error('previous verified bundle unavailable; preserve edits and recover original release first');
115
- await compatible(previous.manifest, source.manifest);
123
+ await compatible(previous.manifest, source.manifest).catch(refuse);
116
124
  const id = randomUUID();
117
125
  const snapshotRoot = path.join(recordRoot, 'releases', id, 'bundle');
118
126
  await assertManagedPath(scopeRoot, snapshotRoot);
package/package.json CHANGED
@@ -1,16 +1,33 @@
1
1
  {
2
2
  "name": "optbrew",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Pinned search audit and improvement skill bundle",
6
6
  "license": "MIT",
7
- "repository": { "type": "git", "url": "git+https://github.com/nowwcastle-sudo/optbrew.git" },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/nowwcastle-sudo/optbrew.git"
10
+ },
8
11
  "homepage": "https://github.com/nowwcastle-sudo/optbrew#readme",
9
- "bugs": { "url": "https://github.com/nowwcastle-sudo/optbrew/issues" },
10
- "keywords": ["pi-package", "seo", "agent-skill"],
11
- "pi": { "skills": ["./skills/optbrew"] },
12
- "engines": { "node": ">=22" },
13
- "bin": { "optbrew": "bin/optbrew.mjs" },
12
+ "bugs": {
13
+ "url": "https://github.com/nowwcastle-sudo/optbrew/issues"
14
+ },
15
+ "keywords": [
16
+ "pi-package",
17
+ "seo",
18
+ "agent-skill"
19
+ ],
20
+ "pi": {
21
+ "skills": [
22
+ "./skills/optbrew"
23
+ ]
24
+ },
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "bin": {
29
+ "optbrew": "bin/optbrew.mjs"
30
+ },
14
31
  "files": [
15
32
  "bin/",
16
33
  "lib/",
@@ -20,6 +37,7 @@
20
37
  ".claude-plugin/",
21
38
  "README.md",
22
39
  "README.ko.md",
40
+ "AGENT_INSTALL.md",
23
41
  "CHANGELOG.md",
24
42
  "docs/platforms.md",
25
43
  "docs/release.md",
@@ -34,7 +52,7 @@
34
52
  ],
35
53
  "scripts": {
36
54
  "test": "node --test",
37
- "build-bundle": "node scripts/build-bundle.mjs --bundle skills/optbrew --version 0.1.1",
55
+ "build-bundle": "node scripts/build-bundle.mjs --bundle skills/optbrew --version 0.1.3",
38
56
  "vendor": "node scripts/vendor.mjs",
39
57
  "scan-secrets": "node scripts/scan-secrets.mjs --tracked-and-staged"
40
58
  }