Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions ACCEPTANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ Ids are stable. Tests must name or comment the id they prove.
| C5 | `skill check` after an installed skill's frontmatter name is corrupted | Exit ≠ 0; reports the skill-name mismatch |
| C6 | `skill check` after an installed `SKILL.md` loses its YAML frontmatter | Exit ≠ 0; reports that YAML frontmatter is required |
| C7 | `skill check` after an installed `SKILL.md` has unclosed frontmatter | Exit ≠ 0; reports that the frontmatter is not closed |
| C8 | `skill check` or `skill lock` after embedded `manage-tink` differs from the active binary | Exit ≠ 0; reports the drift and the exact `tink skill refresh manage-tink` repair command; lockfiles are not written |

### Project manifest

Expand Down Expand Up @@ -259,6 +260,12 @@ Ids are stable. Tests must name or comment the id they prove.
| P6 | `skill refresh` when upstream revision moves but skill tree bytes match | Exit 0; bumps project + library receipts |
| P7 | `skill refresh` when project already at HEAD but library is stale | Exit 0; repairs library from project |
| P8 | `skill refresh` for all when a later imported skill has local modifications | Exit ≠ 0; no project skill is updated |
| P9 | `skill refresh manage-tink` when the embedded copy is missing | Installs the active binary's copy; reconciles library and catalog; subsequent `skill check` passes |
| P10 | `skill refresh manage-tink` when the embedded copy already matches the active binary | Exit 0 with `Unchanged`; reconciles library and catalog; project tree remains identical |
| P11 | `skill refresh manage-tink` when a receipt-free reserved copy differs from the active binary | Atomically replaces it with the active binary's copy; reconciles library and catalog; subsequent `skill check` passes |
| P12 | `skill refresh manage-tink` when the same-named skill has remote provenance | Exit ≠ 0; reports the provenance collision; user-owned tree remains byte-identical |
| P13 | `skill refresh manage-tink` when the project copy is missing and the same-named library skill has remote provenance | Exit ≠ 0 before publication; project remains missing; library tree and receipt remain byte-identical |
| P14 | `skill refresh manage-tink` when a current or stale receipt-free project copy exists and the same-named library skill has remote provenance | Exit ≠ 0 before publication; project trees plus library tree and receipt remain byte-identical |

### Remove

Expand All @@ -268,7 +275,7 @@ Ids are stable. Tests must name or comment the id they prove.
| X2 | `skill remove <missing>` | Exit ≠ 0; mentions not found / missing; nothing deleted |
| X3 | `skill remove` when `.agents` is a symlink | Exit ≠ 0; mentions symlink; tree unchanged |
| X4 | Successful `skill remove <name>` | Does **not** delete `$TINK_HOME/skills/<name>/` |
| X5 | `init` installs `manage-tink` | Embedded skill covers standalone lifecycle, operation-specific proof and partial-state reporting, project-contained lock sources, session-versus-persistent completion authority, any-update re-embed warning, library/catalog effects, skillsets, update, and destroy |
| X5 | `init` installs `manage-tink` | Embedded skill covers standalone lifecycle, operation-specific proof and partial-state reporting, project-contained lock sources, session-versus-persistent completion authority, any-update refresh warning, library/catalog effects, skillsets, update, and destroy |
| X6 | `skill remove <name>` when that project's catalog metadata is malformed | Exit ≠ 0; project and library skill trees remain intact |
| X7 | `skill remove <name>` when `$TINK_HOME/catalog` is a symlink | Exit ≠ 0; mentions the symlink; project skill and external catalog target remain byte-identical |

Expand All @@ -288,7 +295,7 @@ Ids are stable. Tests must name or comment the id they prove.
|---|---|---|
| U1 | `update` when releases API is unreachable | Exit ≠ 0; clear download/metadata failure; binary unchanged |
| U2 | `update` when latest release version matches this binary | Exit 0; stdout notes up to date; binary unchanged |
| U3 | `update` when a newer release asset exists for this host | Exit 0; replaces the running binary; stdout notes updated version |
| U3 | `update` when a newer release asset exists for this host | Exit 0; replaces the running binary; stdout notes updated version and the explicit per-project `tink skill refresh manage-tink` next step; does not mutate a project |
| U4 | `update` receives a valid-digest archive whose payload fails the exact version probe | Exit ≠ 0; running binary remains byte-identical; no success output |
| U5 | `update` metadata names an older semantic version | Exit ≠ 0; refuses downgrade before publication; running binary remains unchanged |
| U6 | `install.sh` receives an invalid or non-executable verified payload while a binary exists | Exit ≠ 0; existing binary remains byte-identical; no success output |
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ tink skill list
tink skill check
tink skill refresh
tink skill refresh skill-name
tink skill refresh manage-tink
tink skill remove skill-name
```

Expand All @@ -151,7 +152,11 @@ tink skill remove skill-name
by the error or by `tink inspect`.
- GitHub tree URLs are inspection inputs, not `skill add` sources. Remote adds
follow the repository's default branch and record the selected skill path.
- Refresh only clean GitHub imports; local edits are refused.
- Refresh only clean GitHub imports; local edits are refused. The explicit
`tink skill refresh manage-tink` path instead owns the reserved embedded
package: it installs a missing copy, leaves a current copy unchanged, or
atomically replaces differing receipt-free contents. Remote provenance is
refused.
- tink does not overwrite a project skill that differs from what it would
install.
- tink never inits Git, stages, commits, or pushes.
Expand Down Expand Up @@ -227,7 +232,7 @@ header, including for an empty catalog. Within fields, backslash, tab, carriage
return, and newline are escaped as `\\\\`, `\\t`, `\\r`, and `\\n` so every
skill remains one three-column row.

**Breaking in 0.3.0:** `skill list --home` → `--catalog`; `skill list --stash` → `--library`. On-disk layout is still `$TINK_HOME/skills/` and `catalog/by-project/`. After a major CLI upgrade, refresh the live project skill: `tink skill remove manage-tink && tink init --no-zen --no-tink-skills`.
**Breaking in 0.3.0:** `skill list --home` → `--catalog`; `skill list --stash` → `--library`. On-disk layout is still `$TINK_HOME/skills/` and `catalog/by-project/`. After updating the binary, refresh each project's embedded skill with `tink skill refresh manage-tink`.

Project lockfiles now use digest format/version 2 so file boundaries and Unix
executable modes are actually pinned. An older lock is deliberately refused;
Expand Down
31 changes: 17 additions & 14 deletions skills/manage-tink/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: manage-tink
description: "Tink CLI for repository-owned Agent Skills. Use when the user asks to initialize Tink; inspect GitHub skill sources; add, list, check, lock, verify, sync, refresh, or remove project skills; manage grouped skillsets; use the home library or catalog; harvest harness skills; configure shell completion; update Tink; re-embed manage-tink; or destroy project agent scaffolding."
description: "Tink CLI for repository-owned Agent Skills. Use when the user asks to initialize Tink; inspect GitHub skill sources; add, list, check, lock, verify, sync, refresh, or remove project skills; manage grouped skillsets; use the home library or catalog; harvest harness skills; configure shell completion; update Tink; refresh embedded manage-tink; or destroy project agent scaffolding."
---

# Manage Tink
Expand Down Expand Up @@ -76,7 +76,7 @@ harness roots, and only the matching `tink skillset add`, `refresh`, or
known.

**On failure:** Stop and report the failure. Do not repair Tink-managed state
by hand. Init, add, skillset refresh, and re-embedding can fail after an
by hand. Init, add, skillset refresh, and embedded-skill refresh can fail after an
earlier project, library, catalog, or guidance write succeeded. Report each
surface known or possibly changed; do not describe the failure as a no-op
unless that was proved.
Expand Down Expand Up @@ -118,20 +118,22 @@ mutation and requires authority for that exact file.
**On failure:** Report the shell and command failure. Do not edit unrelated
shell configuration.

### Step 6: Re-embed Manage Tink When Separately Authorized
### Step 6: Refresh Manage Tink When Separately Authorized

After any binary update or observed contract mismatch, explain that the live
skill may be stale. Do not replace it automatically. Before `destroy`, compare
the active `tink destroy --help` boundary with this skill's ownership contract;
if it is broader, stop and renew approval. If the user explicitly authorizes
re-embedding, run `tink skill remove manage-tink`, then
`tink init --no-zen --no-tink-skills`.
refreshing the embedded package, run `tink skill refresh manage-tink`.

**Expected:** Separate approval exists and the binary's embedded copy becomes
the live project skill.
**Expected:** Missing copies are installed, current copies report `Unchanged`,
and differing receipt-free copies are atomically replaced. The project,
library, and catalog are reconciled, and the binary's embedded copy becomes
the live project skill. A same-named skill with remote provenance is refused.

**On failure:** Stop after the failing command and report whether the old skill
was removed. Do not conceal a partially completed replacement.
**On failure:** Stop after the failing command and report which project,
library, and catalog states were proven. Do not conceal a partially completed
publication.

### Step 7: Prove the Post-state

Expand All @@ -149,8 +151,8 @@ was removed. Do not conceal a partially completed replacement.
- After `skillset remove`, verify project absence and library presence; its
external definition should remain.
- After update, resolve the active binary and probe its exact version. After
re-embedding, run project/catalog/library listings plus `tink skill check`;
structural check alone does not prove embedded payload identity.
refreshing embedded `manage-tink`, run project/catalog/library listings plus
`tink skill check`; check compares the live payload with the active binary.
- After `destroy`, confirm `.agents/skills/` is gone, `.agents/` is gone only if
it became empty, `ZEN.md` and `AGENTS.md` are preserved, unrelated `.agents/`
siblings remain, and the project has no catalog rows; do not run `skill check`.
Expand Down Expand Up @@ -202,12 +204,13 @@ from the mutation command alone.
| Configure shell completion | Only the matching shell command |
| Persist shell completion | Only the exact startup file the user authorizes |
| Lock / verify / sync reproducible state | Only the matching `tink skill …` command; lock requires a project-contained source mapping for each local skill |
| Re-embed manage-tink | `tink skill remove manage-tink`, then `tink init --no-zen --no-tink-skills` |
| Refresh embedded manage-tink | `tink skill refresh manage-tink` |
| Remove one project skill | `tink skill remove NAME` |
| Update the Tink binary | `tink update` only; re-embedding requires separate authority |
| Update the Tink binary | `tink update` only; refreshing embedded `manage-tink` requires separate authority |
| Remove managed project skills / destroy Tink setup | `tink destroy` (TTY) or `tink destroy --yes` (scripts); guidance and unrelated `.agents/` siblings are preserved |

"Set up Tink" does **not** authorize ZEN, tink-skills, re-embedding, or destroy.
"Set up Tink" does **not** authorize ZEN, tink-skills, refreshing embedded
`manage-tink`, or destroy.

## Ownership (always)

Expand Down
2 changes: 1 addition & 1 deletion skills/manage-tink/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ form for add, list, check, refresh, and remove.
| Sync the exact pinned manifest set | `tink skill sync` (preflights expected project/library/catalog refusals, then publishes sequentially; rerun after an operational interruption) |
| Refresh all clean imports | `tink skill refresh` |
| Refresh one | `tink skill refresh NAME` |
| Refresh the active binary's embedded manage-tink | `tink skill refresh manage-tink` (explicitly replaces a differing receipt-free reserved copy; refuses remote provenance) |
| Remove one project skill | `tink skill remove NAME` |
| Add a pinned skillset | `tink skillset add NAME-skillset` |
| List project skillsets | `tink skillset list` |
| List library skillsets | `tink skillset list --library` |
| Refresh a clean pinned skillset | `tink skillset refresh NAME-skillset` |
| Remove one project skillset | `tink skillset remove NAME-skillset` |
| Update the tink CLI binary | `tink update` (newer host asset only; verifies release digest, archive shape, and exact candidate version before replacement) |
| Re-embed manage-tink after separate approval | `tink skill remove manage-tink`, then `tink init --no-zen --no-tink-skills` |
| Destroy managed project skills | `tink destroy --yes` (non-TTY/scripts) or `tink destroy` (TTY, confirm `y`); preserves guidance and unrelated `.agents/` siblings |

## Layout facts
Expand Down
5 changes: 4 additions & 1 deletion src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ fn read_skill_entry(path: &Path) -> Result<Option<Skill>, Error> {
}
let skill = skills::read_skill(path, true)?;
skills::validate_skill_tree(path)?;
provenance::read(&skill)?;
let provenance = provenance::read(&skill)?;
if skill.name == "manage-tink" && provenance.is_none() {
crate::manage_tink::require_current(&skill)?;
Comment on lines +32 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit embedded drift enforcement to check and lock

After upgrading the binary while a project still has the previous receipt-free manage-tink, putting this validation in load_project_skills blocks every caller of the generic loader, not just the C8 check/lock boundaries. In particular, project listing, verification, synchronization, and refreshing an unrelated imported skill all fail on manage-tink drift before performing their requested operation; keep the currentness gate in the check and lock command paths so unrelated project operations remain usable while the explicit repair is pending.

Useful? React with 👍 / 👎.

}
Ok(Some(skill))
}

Expand Down
12 changes: 11 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ pub enum SkillCommand {
},
/// Install missing or pinned skills from the project manifest and lockfile
Sync,
/// Refresh clean GitHub-imported skills; refuse local modifications
/// Refresh clean GitHub imports or the reserved embedded manage-tink
Refresh {
/// Optional skill name; default refreshes all imported skills
name: Option<String>,
Expand Down Expand Up @@ -722,6 +722,16 @@ fn dispatch_skill_harvest(cwd: &Path) -> Result<(), Error> {
fn dispatch_skill_refresh(cwd: &Path, name: Option<&str>) -> Result<(), Error> {
let style = CliStyle::auto_stdout();
match name {
Some("manage-tink") => {
let outcome = manage_tink::refresh_manage_tink(cwd)?;
let action = match outcome {
manage_tink::RefreshOutcome::Installed => style.success("Installed"),
manage_tink::RefreshOutcome::Unchanged => style.muted("Unchanged"),
manage_tink::RefreshOutcome::Refreshed => style.success("Refreshed"),
};
println!("{} {}", action, style.skill("manage-tink"));
Ok(())
}
Some(name) => {
let changed = refresh::refresh_skill(cwd, name)?;
if changed {
Expand Down
91 changes: 90 additions & 1 deletion src/manage_tink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@
use std::path::Path;

use crate::add;
use crate::catalog;
use crate::check;
use crate::error::Error;
use crate::paths::map_io;
use crate::library;
use crate::paths::{map_io, refuse_symlink};
use crate::provenance;
use crate::skills::{self, Skill};

const SKILL_MD: &str = include_str!("../skills/manage-tink/SKILL.md");
const OPENAI_YAML: &str = include_str!("../skills/manage-tink/agents/openai.yaml");
const COMMANDS_MD: &str = include_str!("../skills/manage-tink/references/commands.md");

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RefreshOutcome {
Installed,
Unchanged,
Refreshed,
}

/// Materialize the embedded tree for read-only validation or later publication.
/// The returned guard owns the bytes referenced by `Skill`.
pub(crate) fn prepare_manage_tink() -> Result<(tempfile::TempDir, Skill), Error> {
Expand All @@ -33,6 +44,84 @@ pub(crate) fn prepare_manage_tink() -> Result<(tempfile::TempDir, Skill), Error>
Ok((staging, skill))
}

pub(crate) fn is_current(installed: &Skill) -> Result<bool, Error> {
let (_staging, embedded) = prepare_manage_tink()?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep current-payload comparison read-only

Whenever a receipt-free manage-tink is installed, tink skill check reaches this call through require_current, and prepare_manage_tink creates a temporary directory and writes three files into it. This violates the C4 contract that skill check performs no filesystem writes and also makes an otherwise read-only check fail when the project is readable but the temporary filesystem is unavailable or unwritable; compare against an in-memory representation or precomputed digest instead.

Useful? React with 👍 / 👎.

skills::skill_contents_equal(&installed.path, &embedded.path)
}

/// Require an installed embedded copy to match the payload in this binary.
pub(crate) fn require_current(installed: &Skill) -> Result<(), Error> {
if is_current(installed)? {
return Ok(());
}
Err(Error::msg(
"manage-tink differs from this Tink binary; run `tink skill refresh manage-tink`",
))
}

fn refuse_remote_library_collision() -> Result<(), Error> {
let Some(home) = crate::home::existing_inventory_root(None)? else {
return Ok(());
};
let target = crate::home::skills_library_path(&home).join("manage-tink");
refuse_symlink(&target)?;
if !target.is_dir() {
return Ok(());
}
let library_skill = skills::read_skill(&target, true)?;
if provenance::read(&library_skill)?.is_some() {
return Err(Error::msg(
"Refusing to replace library manage-tink with remote provenance",
));
}
Ok(())
}

pub(crate) fn refresh_manage_tink(project_root: &Path) -> Result<RefreshOutcome, Error> {
check::check_zen_coupling(project_root)?;
let agents = crate::home::project_agents_path(project_root);
let skills_root = crate::home::project_skills_path(project_root);
let target = skills_root.join("manage-tink");
refuse_symlink(&agents)?;
refuse_symlink(&skills_root)?;
refuse_symlink(&target)?;
refuse_remote_library_collision()?;

if !target.exists() {
install_manage_tink(project_root)?;
return Ok(RefreshOutcome::Installed);
}
if !target.is_dir() {
return Err(Error::msg("Installed manage-tink is not a directory"));
}

let installed = skills::read_skill(&target, true)?;
skills::validate_skill_tree(&target)?;
if provenance::read(&installed)?.is_some() {
return Err(Error::msg(
"Refusing to replace manage-tink with remote provenance",
));
}
let (_staging, embedded) = prepare_manage_tink()?;
if !skills::skill_contents_equal(&installed.path, &embedded.path)? {
library::preflight_deposit(&embedded, None)?;
catalog::preflight_deposit_skill(project_root)?;
skills::replace_embedded_verified(&embedded, &skills_root)?;
library::deposit(&embedded, None)?;
catalog::deposit_skill(project_root, "manage-tink")?;

let refreshed = skills::read_skill(&target, true)?;
require_current(&refreshed)?;
return Ok(RefreshOutcome::Refreshed);
}

library::preflight_deposit(&installed, None)?;
catalog::preflight_deposit_skill(project_root)?;
library::sync_from_installed(&installed)?;
catalog::deposit_skill(project_root, "manage-tink")?;
Ok(RefreshOutcome::Unchanged)
}

/// Stage the embedded skill and install it into the project via `add`.
///
/// Uses the quiet add path so init can own the closing narrative.
Expand Down
26 changes: 22 additions & 4 deletions src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,11 +717,10 @@ pub(crate) fn publish_staged_tree(
Ok(target.to_path_buf())
}

/// Replace an existing imported skill after dirty-tree preflight elsewhere.
pub fn replace_verified(
fn replace_verified_inner(
skill: &Skill,
destination_root: &Path,
provenance: &Provenance,
provenance: Option<&Provenance>,
) -> Result<PathBuf, Error> {
require_safe_tree(&skill.path)?;
let target = destination_root.join(&skill.name);
Expand All @@ -744,10 +743,29 @@ pub fn replace_verified(
output::display_path(&skill.path)
)));
}
provenance::write_file(&staged.join(provenance::SIDECAR_FILE), provenance)?;
if let Some(provenance) = provenance {
provenance::write_file(&staged.join(provenance::SIDECAR_FILE), provenance)?;
}
publish_staged_tree(staging, staged, &target)
}

/// Replace an existing imported skill after dirty-tree preflight elsewhere.
pub fn replace_verified(
skill: &Skill,
destination_root: &Path,
provenance: &Provenance,
) -> Result<PathBuf, Error> {
replace_verified_inner(skill, destination_root, Some(provenance))
}

/// Replace the receipt-free embedded skill after its ownership preflight.
pub(crate) fn replace_embedded_verified(
skill: &Skill,
destination_root: &Path,
) -> Result<PathBuf, Error> {
replace_verified_inner(skill, destination_root, None)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading