@alessandroraffa/tangyr 0.21.5 → 1.0.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.
package/README.md CHANGED
@@ -53,6 +53,27 @@ tangyr <command>
53
53
  | `tangyr config` | Display or edit Tangyr configuration |
54
54
  | `tangyr validate` | Validate the kit against kitFormat 1 conformity rules |
55
55
 
56
+ ### Command options
57
+
58
+ Options declared on individual commands in addition to the [global flags](#global-flags) below. `--offline`, `--refresh`, and `install`/`sync`'s narrow `--json` are documented in [Remote kit source](#remote-kit-source); `--scope` in [Scope](#scope).
59
+
60
+ | Command | Option | Description |
61
+ | -------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
+ | `install`, `sync` | `--loss-report <path>` | Write this run's semantic loss report to `<path>` as JSON — one entry per target/component that could not be translated natively, with its severity. Written whether or not the run exits `3`. |
63
+ | `install`, `sync` | `--ignore-errors` | Suppress exit code `3` when the loss report contains `error`-severity entries |
64
+ | `install`, `sync` | `--kit <name>` | Kit to install/synchronize: `<name>` for a local source, `<name>@<version>` for a remote one |
65
+ | `install`, `uninstall`, `assess` | `--tools <list>` | Comma-separated list of target tools |
66
+ | `sync`, `status`, `verify`, `doctor`, `cleanup`, `probe` | `--target <tool>` | Limit the command to a single target |
67
+ | `sync` | `--component <name>` | Limit sync to one component type: `instructions`, `agents`, `skills`, `commands`, `rules`, `hooks`, `mcp`, `settings`, `output-styles`, `lsp` |
68
+ | `sync` | `--force` | Rewrite managed artifacts even when provenance matches |
69
+ | `doctor` | `--fix` | Apply safe automated fixes |
70
+ | `cleanup` | `--all` | Include all enabled targets |
71
+ | `config` | `--edit` | Interactively edit mutable config fields |
72
+ | `auth login` | `--skip-validation` | Store the credential without contacting the configured origin |
73
+ | `kit update` | `--kit <name>@<version>` | Remote kit reference to re-fetch into the cache |
74
+ | `cache clear` | `[target]` | Positional: kit name or `<name>@<version>`; omit to clear the whole cache |
75
+ | `assess`, `detect`, `list`, `probe`, `validate` | `--json` | Replace the command's output with JSON |
76
+
56
77
  ### Authentication
57
78
 
58
79
  | Command | Description |
@@ -96,7 +117,9 @@ tangyr auth login
96
117
  # type: remote
97
118
  # url: https://kits.example.com
98
119
 
99
- # 3. Install by name and explicit version no "latest".
120
+ # 3. Install by name. A bare name resolves through the channel pointer;
121
+ # pin a version instead when you want an exact, immutable one.
122
+ tangyr install --kit acme-kit
100
123
  tangyr install --kit acme-kit@1.4.0
101
124
  ```
102
125
 
@@ -107,24 +130,28 @@ pipeline.
107
130
 
108
131
  #### Remote source configuration
109
132
 
110
- Set the source scheme in `tangyr.config.yaml`. Both keys are optional and additive — a config that omits them behaves exactly as before.
133
+ Set the source scheme in `tangyr.config.yaml`. All three keys are optional and additive — a config that omits them behaves exactly as before.
111
134
 
112
135
  ```yaml
113
136
  source:
114
137
  type: remote # "local" (default) or "remote"
115
138
  url: https://kits.example.com # required when type is remote
139
+ channel: stable # channel a versionless --kit resolves through; default "stable"
116
140
  ```
117
141
 
118
142
  `type` defaults to `local` when absent. When it is `remote`, the local `source.*` path keys are ignored at resolution; they are retained rather than stripped, so switching back to `local` needs no restoration. A `type` that is neither `local` nor `remote` is rejected at load rather than silently treated as local, and `type: remote` with no `url` is rejected as well.
119
143
 
120
- A remote kit is referenced by name and explicit version on both commands:
144
+ A remote kit is referenced on both commands either by name and explicit version, or by name alone:
121
145
 
122
146
  ```bash
123
- tangyr install --kit acme-kit@1.4.0
147
+ tangyr install --kit acme-kit@1.4.0 # exact, immutable
148
+ tangyr install --kit acme-kit # resolves through the channel pointer
124
149
  tangyr sync --kit acme-kit@1.4.0
125
150
  ```
126
151
 
127
- The version is required and immutable there is no `latest`. Passing a bare URL as `--kit` is rejected with a message pointing at `source.url`.
152
+ An explicit `<name>@<version>` is immutable and never consults a channel. A bare `<name>` resolves through the signed channel pointer named by `source.channel`, defaulting to `stable`; `source.channel` must be lowercase alphanumeric with hyphens, 1-32 characters, and is rejected at config load rather than mid-resolution. Passing a bare URL as `--kit` is rejected with a message pointing at `source.url`.
153
+
154
+ A channel pointer is served `no-store` and is never cached, because "the newest version as of some earlier run" is not what a channel means. Two paths therefore answer from the last version this machine saw, and both say so: `--offline`, which has no other option and exits `8` if the channel has never been resolved here, and a genuine network failure, which falls back and reports the run as degraded. A pointer that is rolled back, expired, or signed for another channel does **not** fall back — it fails with exit `7`, because proceeding on a remembered version would convert a detected downgrade into a silent one.
128
155
 
129
156
  | Variable | Default | Purpose |
130
157
  | ------------------ | ------------------ | --------------------------------------------------------------------- |
@@ -156,7 +183,7 @@ A warm, currently-valid entry that is also freshness-fresh (within the
156
183
  24-hour authorization-freshness window from the last successful check for
157
184
  that origin) resolves fully offline, with no network call at all. Beyond
158
185
  the window, the next `install`/`sync` performs a lightweight authorization
159
- re-check against the origin, which also consults the revocation deny-list.
186
+ re-check against the origin.
160
187
  A definitive denial fails closed with no cache fallback; a genuine network
161
188
  failure during that re-check falls back to the warm cache with a warning
162
189
  and, under `--json`, a `{"degraded":true}` signal. See
@@ -165,8 +192,8 @@ below.
165
192
 
166
193
  The cache root also carries a `.tangyr-freshness.json` file, recording, per
167
194
  configured origin, the timestamp of the last successful authorization
168
- check — used by the cache-first freshness gate a later activity in this
169
- StepLedger adds. This file is not itself a cache entry: it is excluded from
195
+ check — used by the cache-first freshness gate. This file is not itself a
196
+ cache entry: it is excluded from
170
197
  `tangyr cache clear`.
171
198
 
172
199
  #### Limits and file modes
@@ -224,7 +251,7 @@ configured origin, the timestamp of the last successful authorization
224
251
  check. A warm, currently-valid entry resolves fully offline — no network
225
252
  call at all — for **24 hours** from that timestamp. Beyond the window, the
226
253
  next `install`/`sync` performs a lightweight re-check against the origin
227
- (and consults the revocation deny-list) before resolving.
254
+ before resolving.
228
255
 
229
256
  `--offline` forces cache-only resolution: it never fetches and never
230
257
  mutates. A complete, valid entry resolves immediately; a missing or
@@ -235,7 +262,7 @@ bypassing the freshness gate entirely — it always reaches the origin.
235
262
 
236
263
  On a network failure during the lightweight re-check (not a definitive
237
264
  denial), resolution falls back to the warm cache with a warning printed to
238
- stderr, since the check could not confirm freshness or revocation status.
265
+ stderr, since the check could not confirm freshness.
239
266
  Under `--json`, this same condition also emits a `{"degraded":true,"kit":
240
267
  "<name>@<version>"}` line to stderr. Neither `install` nor `sync` has a
241
268
  full JSON output mode yet: `--json` is scoped to exactly this one signal
@@ -297,6 +324,45 @@ declared via `.option()`. Commander delivers the positional argument's
297
324
  value as the `.action()` callback's first parameter, ahead of any options
298
325
  object — the reverse of every option-only command's callback signature.
299
326
 
327
+ ## The kit's instructions file
328
+
329
+ One resolution, used by `install`, `sync`, every adapter and `verify` alike:
330
+
331
+ 1. `source.instructions` in `tangyr.config.yaml`, when set to something other than the `./AGENTS.md` default. An explicit declaration naming a file the kit does not carry is an error, reported before anything is written.
332
+ 2. `entrypoint` in `tangyr-kit.yaml`.
333
+ 3. `ORIENTATION.md`, then `AGENTS.md`, then the legacy `CLAUDE.md` (which installs, with a notice naming the rename).
334
+
335
+ A kit whose entrypoint is `ORIENTATION.md` therefore needs no `source.instructions`, and `verify` recognises it.
336
+
337
+ ## Configuration file
338
+
339
+ `tangyr.config.yaml` declares `schema: 1`. A key this schema does not define is rejected, naming the key it was probably meant to be — an unrecognized key is never read, so whatever it was meant to set stays at its default, and `target:` for `targets:` silently reduced an install to one tool. Prefix a key with `x-` to keep it as an annotation.
340
+
341
+ In `tangyr-kit.yaml`, `discipline` is optional: declare it to opt into the corpus-version check, omit it for a kit that is not derived from the shared corpus.
342
+
343
+ ## Conflict policy
344
+
345
+ Before writing, each destination is classified and resolved according to `onConflict` in `tangyr.config.yaml`: `ask` (default; prompts, and resolves to backup-and-replace in a non-interactive run), `skip`, `backup`, or `overwrite`.
346
+
347
+ `--yes` supplies the confirmation a non-interactive run needs; it does **not** override the posture. Under `--yes`, `skip` leaves conflicting destinations unchanged and says which, `overwrite` replaces without a backup, and `backup` copies the existing file — or the existing directory, recursively — into `.tangyr/backups/<timestamp>/` and records it in the manifest so `uninstall` can put it back. A symlink is replaced without a backup: what it points at is untouched.
348
+
349
+ One decision is taken per destination, not per target, so a file several targets write — `AGENTS.md` is claimed by four — is backed up once.
350
+
351
+ ## Accepted loss
352
+
353
+ Some losses are structural: OpenCode expresses hooks only as plugin code, Cline has no committable persona file. A kit with hooks installed for OpenCode therefore exits `3` on every run, however correct the kit is.
354
+
355
+ `acceptedLoss` names those, so that a genuinely new error entry is still loud:
356
+
357
+ ```yaml
358
+ acceptedLoss:
359
+ - opencode/hook-as-plugin # <target>/<entry-type>
360
+ - cline/* # every error entry for one target
361
+ - persona-not-representable # this entry type on any target
362
+ ```
363
+
364
+ `--ignore-errors` remains the blunt instrument: it suppresses the exit code for every error entry, including ones nobody has looked at yet.
365
+
300
366
  ## Exit codes
301
367
 
302
368
  The full exit-code taxonomy, in numeric order. Every code below `5`
@@ -310,11 +376,11 @@ reaches them).
310
376
  | `0` | Success | The command completed with no fatal error. A `sync`/`install` whose loss report contains only `info`/`warning`-severity entries also exits `0`. |
311
377
  | `1` | General failure | The generic, catch-all failure code for any error not mapped to a more specific code below — including a CLI usage error such as an unrecognized command, a malformed `--kit <name>@<version>` reference, or `--offline` and `--refresh` passed together; a corrupt or legacy-format installation manifest (`ManifestCorruptError`, `ManifestLegacyError`, `src/core/manifest.ts`) also falls through to this code, since `handleFatalError` carries no dedicated branch for either. |
312
378
  | `2` | Conflict | Reserved for a conflict-resolution failure (`ConflictError`, `src/core/exit.ts`); declared and routed by `handleFatalError`, but not thrown by any current production code path. |
313
- | `3` | Completed with error-severity loss | An `install`/`sync` run completes but its loss report contains at least one `error`-severity entry, distinct from the generic failure code `1`. An opt-out flag forces a zero exit for authors who accept the loss. |
379
+ | `3` | Completed with error-severity loss | An `install`/`sync` run completes but its loss report contains at least one `error`-severity entry, distinct from the generic failure code `1`. `--ignore-errors` on `install`/`sync` forces a zero exit for authors who accept the loss. |
314
380
  | `4` | Target not detected | Reserved for a configured target tool whose runtime cannot be detected (`TargetNotDetectedError`, `src/core/exit.ts`); declared and routed the same way as exit `2`, and likewise not thrown by any current production code path. |
315
381
  | `5` | Authentication required | The resolved credential is missing, partially configured (see the credential-precedence trap above), or rejected by the origin (`401`/`403`) — at login, at a cold fetch, or at the post-freshness-window re-check. |
316
382
  | `6` | Remote unavailable | A transient fetch failure exhausts the bounded retry (3 attempts, 250 ms then 1000 ms backoff) with no warm cache to fall back to, or a warm-cache freshness re-check fails the same way with no usable fallback path. |
317
- | `7` | Integrity failure | The signed bundle fails signature verification (including an unknown or out-of-window signing-key identifier), identity binding, a per-file content-hash check, a size ceiling, or path sanitization, or the requested `<name>@<version>` is present on the signed revocation deny-list. |
383
+ | `7` | Integrity failure | The signed bundle fails signature verification (including an unknown or out-of-window signing-key identifier), identity binding, a per-file content-hash check, a size ceiling, or path sanitization; or the channel pointer for a versionless `--kit <name>` cannot be resolved, is not signed by a pinned key, or names a version older than the one already seen. |
318
384
  | `8` | Offline cache miss | `--offline` with no complete, valid cache entry for the requested reference. |
319
385
  | `9` | Incompatible version | The running CLI is older than the bundle's declared minimum-compatible version, or the origin rejects this CLI's contract version. |
320
386
  | `10` | Configuration missing | No `tangyr.config.yaml` is found (`ConfigNotFoundError`, `src/core/config.ts`) for a command that requires one. |
@@ -326,8 +392,10 @@ reaches them).
326
392
  | ---------------------------- | ----------------------------------------------------------------------------------------------------- |
327
393
  | `--config <path>` | Use an explicit configuration file |
328
394
  | `--kit-path <path>` | Use an explicit operating kit root |
395
+ | `--project-root <path>` | Directory a project-scope operation writes into (see [Scope](#scope)) |
329
396
  | `--dry-run` | Report actions without modifying files |
330
397
  | `--yes` | Skip confirmation prompts |
398
+ | `--non-interactive` | Never prompt; fail closed when a required input is missing (`auth login`) |
331
399
  | `--verbose` | Detailed output |
332
400
  | `--quiet` | Errors only |
333
401
  | `--no-color` | Disable colors |
@@ -335,12 +403,12 @@ reaches them).
335
403
 
336
404
  ## Supported tools
337
405
 
338
- - **Claude Code** — artifacts placed in scope-resolved Claude Code paths
339
- - **GitHub Copilot** — agents and commands compiled to VS Code user directories
340
- - **OpenAI Codex** — `AGENTS.md`, agent TOML files, rules, hooks, and MCP compiled to Codex paths
341
- - **OpenCode** — instructions, agents, commands, skills, and MCP placed in XDG config paths
342
- - **Cursor** — instructions, subagents, rules, commands, skills, hooks, and MCP
343
- - **Cline** — instructions, rules, workflows, skills, and MCP
406
+ - **Claude Code** — instructions (`AGENTS.md` plus the `CLAUDE.md` shim), agents, skills, commands, rules, output styles, hooks, settings, and MCP, placed in scope-resolved Claude Code paths (fanned out across every configured global root — see [Multiple Claude Code global roots](#multiple-claude-code-global-roots))
407
+ - **GitHub Copilot** — instructions (`AGENTS.md` plus the always-read `.github/copilot-instructions.md` mirror; project scope only, since Copilot has no global instructions slot), agents, prompts (compiled from commands), skills, hooks (`~/.copilot/hooks` or `.github/hooks`), and MCP. Hook matchers are rewritten to Copilot's own lowercase tool names (`bash`, `view`, `create`, `edit`, `grep`) — an untranslated `Bash` matcher never fires
408
+ - **OpenAI Codex** — `AGENTS.md`, agent TOML files, rules (`.rules` files for `exec-policy`, appended to the instructions file as guidance otherwise), hooks (`hooks.json` plus the shared `~/.agents/hooks` script bridge; macOS and Linux only), and MCP in `config.toml`
409
+ - **OpenCode** — instructions (`AGENTS.md`), agents, commands, skills (OpenCode-local plus the shared `~/.agents/skills` link), and `opencode.json` (MCP, permissions, custom provider), in XDG config paths. Hooks are **not** delivered: OpenCode exposes hooks only as plugin code, so every hook event is dropped and recorded as a `hook-as-plugin` loss
410
+ - **Cursor** — instructions, subagents, `.mdc` rules, commands, skills (Cursor-local plus shared), `hooks.json` plus the shared hook-script bridge (matchers rewritten to Cursor's tool names — `Shell`, `Read`, `Write`, `Grep`), `mcp.json`, and `permissions.json`
411
+ - **Cline** — instructions (`AGENTS.md`, project scope only), rules, workflows (compiled from commands), skills, and MCP. Agents and hooks are **not** delivered: Cline has no committable persona file (`persona-not-representable`) and exposes hooks only as `@cline/sdk` plugin code (`hook-as-plugin`); both are recorded as losses rather than silently skipped
344
412
 
345
413
  ## Quick start
346
414
 
@@ -366,6 +434,17 @@ Every write command accepts `--scope global` or `--scope project`. The CLI flag
366
434
  | `global` | `~/.tangyr/manifest.json` | `~/.claude/` |
367
435
  | `project` | `.tangyr/manifest.json` | repo root (`AGENTS.md`, `.claude/`) |
368
436
 
437
+ ### Which directory is the project
438
+
439
+ A project-scope operation resolves its root in this order:
440
+
441
+ 1. `--project-root <path>`, when given.
442
+ 2. The directory of the `tangyr.config.yaml` this run **discovered** — one found in the working directory or an ancestor. That file sits at the project root by construction, and `kitPath` is already resolved against it.
443
+ 3. The working directory, when the config was named explicitly with `-c`. An explicit path may name a shared or CI config outside the tree and says nothing about where the project is.
444
+ 4. The nearest enclosing directory carrying `tangyr.config.yaml` or `.tangyr/`, for the commands that run without a config at all; failing that, the working directory.
445
+
446
+ Running from a subdirectory therefore installs at the project root rather than in the subdirectory. Deliberately only Tangyr's own markers are searched in step 4: a `package.json` or `.git` above the working directory says where some project starts, not that this installation belongs there.
447
+
369
448
  ## Multiple Claude Code global roots
370
449
 
371
450
  If you maintain more than one Claude Code account (e.g. `~/.claude` and `~/.claude-work`), you can keep them identical with a single sync operation.