diff --git a/README.md b/README.md index 7d4cd0169..55b017311 100644 --- a/README.md +++ b/README.md @@ -111,14 +111,13 @@ catalog shared catalog shared catalog, warm-starts ## Skills are the product -Skills are not configuration — they are the capability surface. **26 skills** ship in the vsix from `packages/extension/skills/`, versioned with the product: **24 public** — they load for every session — plus **2 entitled surfaces**, `piccolissimo` and `intonatissimo`, which ship in the vsix but stage only for entitled sessions ([ADR-0011](docs/adr/0011-entitled-skill-surface.md)). Additional skills load from your own vault mounts and from co-located Julia packages behind entitlements. +Skills are not configuration — they are the capability surface. **38 skills** ship in the vsix from `packages/extension/skills/`, versioned with the product, and all of them are public: they load for every session. Premium-tier guidance does not ship in this repository ([ADR-0011](docs/adr/0011-entitled-skill-surface.md)). Additional skills load from your own vault mounts and from co-located Julia packages behind entitlements. | Surface | What it covers | |---|---| | Research loop | `research`, `analyze`, `hypothesis-review`, `dream-reflect`, `open-threads`, `paper-writer`, `create-research-project`, `migrate-research-project` | | Lab + catalog + vault | `amico-lab`, `amico-catalog`, `amico-vault`, `amico-strategy`, `amico-schema-check`, `amico-slack` | | Engineering | `debugging`, `tdd`, `verification`, `brainstorming`, `deliberate`, `grill-me`, `grill-with-docs`, `improve-codebase-architecture`, `teach`, `report-a-bug` | -| Entitled surfaces | `piccolissimo`, `intonatissimo` — usage guidance for the `-issimo` performance tiers; ship in the vsix, stage only for entitled sessions | | Internal library — not in the vsix | `transmon`, `fluxonium`, `atoms` (Rydberg), `bosonic`, `ions`, `setup`, `solve`, `simulate`, `warm-start`, `constraints`, `objectives`, `problem-types`, `structural-analysis`, `demo`, `pasqal`, `plot`, `compose`, `multistart` — these live in the internal library and resolve only from the private plugin checkout, per the skill-surface decisions of record ([ADR-0003](docs/adr/0003-skill-surfaces-two-tier-checkout-gate.md), [ADR-0011](docs/adr/0011-entitled-skill-surface.md)) | The extension stages the union of the shipped bundle and your vault mounts at startup; mount presence is the eligibility proof. The boundary as it stands: the extension, the `amico` CLI, and the public skills are open — entitled builds add proprietary capabilities behind entitlements. diff --git a/docs/adr/0011-entitled-skill-surface.md b/docs/adr/0011-entitled-skill-surface.md index fbc24ea1d..98eae6004 100644 --- a/docs/adr/0011-entitled-skill-surface.md +++ b/docs/adr/0011-entitled-skill-surface.md @@ -1,6 +1,6 @@ # Skill surfaces are three-tier: public ships and loads for all, entitled ships and loads only for entitled sessions, internal stays vault-only -Status: accepted (2026-08-27; content policy amended 2026-09-05 — see the Amendment record) +Status: accepted (2026-08-27; content policy amended 2026-09-05 and 2026-11-07 — see the Amendment records) ADR-0003's two surface tiers amend to three. `public` ships in the .vsix and loads for every session, as before. `internal` stays exactly as ADR-0003 left it: private-vault content @@ -99,3 +99,25 @@ review (director + independent reviewer, per the dev gate) — the pass had not was written; it completed 2026-09-05: **approved** (independent reviewer, findings tracked: the slice-3 content lens folded here; the internal-location vocabulary seam tracked as a chore for the parents' next amendment). + +--- + +**Amendment (2026-11-07): private-package skills leave the public repo.** In-repo entitled +placement no longer applies to skills about private packages. Shipping usage guidance +world-readable was the premise of the entitled tier ("ship usage, keep internals"); for +private packages that premise is withdrawn — usage guidance is itself proprietary. Such skills +live in the team vault as `surface: internal`, admitted by mount presence and staged for +headless sessions through the internal-skills allowlist (`ops/server/stage-internal-skills.sh`). +The `issimo` entitlement keeps its meaning — the staging gate described above — for any paid +guidance about public packages. The tier machinery is untouched: three surfaces, root admission +(`{public, entitled}` in-repo, `{internal}` vault-only), and the entitlement check stand as +written. + +Public skills refer to private capabilities only neutrally (the "premium tier"). The review-time +content policy gains a mechanical backstop: the skill-library lint's `private-package` rule fails +any shippable skill (surface `public`, `entitled`, or untagged) that names a package on the +denylist in `packages/extension/scripts/lint-skills-private-packages.txt`, which is the single +place the list lives. The rule is on by default for the repo library and exempts `internal` cards. +Exceptions go in `lint-skills-known.txt`, and that list is empty. + +Implementation: harmoniqs/amicode#2147. diff --git a/ops/server/stage-internal-skills.sh b/ops/server/stage-internal-skills.sh index 5ea6e0da2..cd022b5b2 100755 --- a/ops/server/stage-internal-skills.sh +++ b/ops/server/stage-internal-skills.sh @@ -33,6 +33,10 @@ ALLOWLIST=( harmony shape sweep + # private-package usage cards: vault-only, never in the public library + altissimo + piccolissimo + intonatissimo ) for skill in "${ALLOWLIST[@]}"; do diff --git a/packages/extension/scripts/lint-skills-private-packages.txt b/packages/extension/scripts/lint-skills-private-packages.txt new file mode 100644 index 000000000..f30fe1e55 --- /dev/null +++ b/packages/extension/scripts/lint-skills-private-packages.txt @@ -0,0 +1,25 @@ +# lint-skills-private-packages.txt — the private-package DENYLIST for the +# `private-package` rule of scripts/lint-skills.mjs. This file is the single +# place the list lives. +# +# This repository is public, so everything in the skill library is +# world-readable. A shippable skill (surface public, entitled, or untagged) +# must not name a private package. Usage guidance for private packages lives +# in the team vault as `surface: internal` skills, which this rule exempts. +# +# One package name per line, matched case-insensitively as a whole word. +# The rule is on by default for the repo library (packages/extension/skills) +# and opt-in for other roots via `--denylist `. Exceptions go in +# lint-skills-known.txt as `/SKILL.md:private-package`. Keep that list +# empty: reword to "premium tier" instead. +Piccolissimo +Intonatissimo +Altissimo +Prestissimo +Passaggissimo +Coro +Legatissimo +Telaio +Favo +Strettissimo +amicissimo diff --git a/packages/extension/scripts/lint-skills.mjs b/packages/extension/scripts/lint-skills.mjs index 8d494e0aa..6871b47d6 100644 --- a/packages/extension/scripts/lint-skills.mjs +++ b/packages/extension/scripts/lint-skills.mjs @@ -17,10 +17,18 @@ // `~/...armonia-data.../`) → WARNING — the rot-prone class // (the armonia-aaron-trowbridge rename rotted these once); // warnings never fail the lint +// private-package a shippable skill (surface public / entitled / untagged) +// names a package on the private-package DENYLIST +// (scripts/lint-skills-private-packages.txt — the one file +// the list lives in), whole-word, case-insensitive, anywhere +// in the file (frontmatter `name:` included) → ERROR. The repo +// is public; private-package skills live in the team vault as +// `surface: internal`, which this rule exempts. On by default +// for the repo library; opt-in elsewhere via --denylist // agents unknown `agents:` value — WARNING only (the ontology sweep // is a later slice); warnings never fail the lint // -// node scripts/lint-skills.mjs [--dir ] [--known ] [--json] +// node scripts/lint-skills.mjs [--dir ] [--known ] [--denylist ] [--json] // // --dir skills library root (default: this repo's source tree, // packages/extension/skills) @@ -31,6 +39,10 @@ // that referenced path (`skill/SKILL.md:companion:guide.md`) — // unlisted occurrences in the same file still fail. Listed // occurrences are reported as known-warnings, never errors. +// --denylist private-package denylist file (one name per line, `#` +// comments). Default: scripts/lint-skills-private-packages.txt +// when --dir is the repo library, otherwise the rule is off — +// vault roots legitimately carry private-package content. // --json machine-readable report on stdout (the dream-side pass consumes // this; findings land in the vault ledger) // @@ -47,6 +59,7 @@ import { fileURLToPath } from "node:url"; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_DIR = path.join(SCRIPT_DIR, "..", "skills"); +const DEFAULT_DENYLIST = path.join(SCRIPT_DIR, "lint-skills-private-packages.txt"); const AGENT_VALUES = new Set([ "researcher", "experimenter", "librarian", "hypothesizer", "analyzer", @@ -54,7 +67,7 @@ const AGENT_VALUES = new Set([ "engineer", "pulse-designer", ]); const SURFACE_VALUES = new Set(["public", "internal", "entitled"]); -const RULES = ["amico-plugin", "companion", "name", "surface", "vpath"]; +const RULES = ["amico-plugin", "companion", "name", "surface", "vpath", "private-package"]; // F28 — the known-rotted vault-path idioms: the old ops-repo vault tree, the // pre-rename personal vault name, and the retired transcript substrate. @@ -67,7 +80,7 @@ const VPATH_ERROR_SHAPES = [ // rotted once already (the armonia-aaron-trowbridge rename), so they warn. const VPATH_HOME_RE = /~\/[A-Za-z0-9_/.-]*(vaults|armonia-data)[A-Za-z0-9_/.-]*\//; -const USAGE = `usage: node scripts/lint-skills.mjs [--dir ] [--known ] [--json]`; +const USAGE = `usage: node scripts/lint-skills.mjs [--dir ] [--known ] [--denylist ] [--json]`; function fail(msg) { console.error(`lint-skills: ${msg}\n${USAGE}`); @@ -75,17 +88,18 @@ function fail(msg) { } function parseArgs(argv) { - const out = { dir: DEFAULT_DIR, known: null, json: false }; + const out = { dir: DEFAULT_DIR, known: null, denylist: null, json: false }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === "--help" || a === "-h") return { help: true }; if (a === "--json") out.json = true; else if (a === "--dir") out.dir = argv[++i]; else if (a === "--known") out.known = argv[++i]; + else if (a === "--denylist") out.denylist = argv[++i]; else if (a.startsWith("--")) return { error: `unknown flag ${a}` }; else return { error: `unexpected argument ${a}` }; } - if (out.dir === undefined || out.known === undefined) return { error: "missing value for flag" }; + if (out.dir === undefined || out.known === undefined || out.denylist === undefined) return { error: "missing value for flag" }; return out; } @@ -113,6 +127,22 @@ function loadKnown(file) { return set; } +// The private-package denylist → one whole-word, case-insensitive matcher per +// name. Names are escaped, so a denylist line can never become a regex. +function loadDenylist(file) { + let text; + try { + text = fs.readFileSync(file, "utf8"); + } catch (e) { + return { error: `cannot read --denylist file: ${e.message}` }; + } + return text + .split("\n") + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith("#")) + .map((name) => ({ name, re: new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i") })); +} + // Conservative companion-path collection: markdown-link targets ending in // `.md` (any shape), plus backticked paths ending in `.md` that carry a `/`. // Everything that smells like a template, an external path, or an escape is @@ -143,7 +173,7 @@ function isResolvable(ref) { return true; } -function lintSkill(skillDir, relDir) { +function lintSkill(skillDir, relDir, denylist = []) { const findings = []; const file = path.join(skillDir, "SKILL.md"); const rel = relDir ? path.join(relDir, "SKILL.md") : "SKILL.md"; @@ -164,6 +194,7 @@ function lintSkill(skillDir, relDir) { }); // (2) frontmatter (between the first two `---` lines) + let surface = null; if (lines[0]?.trim() === "---") { const end = lines.indexOf("---", 1); if (end > 0) { @@ -174,6 +205,7 @@ function lintSkill(skillDir, relDir) { if (key === "name" && value && value !== path.basename(skillDir)) { findings.push({ rel, line: i + 1, rule: "name", severity: "error", msg: `frontmatter name \`${value}\` ≠ directory \`${path.basename(skillDir)}\`` }); } + if (key === "surface") surface = value; if (key === "surface" && value && !SURFACE_VALUES.has(value)) { findings.push({ rel, line: i + 1, rule: "surface", severity: "error", msg: `surface \`${value}\` not in {public, internal, entitled}` }); } @@ -206,6 +238,18 @@ function lintSkill(skillDir, relDir) { } }); + // (3b) private-package names in a shippable card — internal cards are + // vault-only (the packaging leak-guard refuses them in the .vsix). + if (surface !== "internal") { + lines.forEach((line, idx) => { + for (const { name, re } of denylist) { + if (re.test(line)) { + findings.push({ rel, line: idx + 1, rule: "private-package", severity: "error", msg: `names private package \`${name}\` — the repo is public; private-package guidance lives in the team vault as surface: internal (reword to "premium tier")` }); + } + } + }); + } + // (4) dead companion files — resolve inside the skill's own directory only for (const { ref, line } of collectCompanionRefs(text)) { const cleaned = ref.replace(/^\.\//, "").split("#")[0].trim(); @@ -220,7 +264,7 @@ function lintSkill(skillDir, relDir) { return findings; } -function lintDir(root) { +function lintDir(root, denylist = []) { const findings = []; let skills = 0; const entries = fs.readdirSync(root, { withFileTypes: true }); @@ -229,7 +273,7 @@ function lintDir(root) { const skillMd = path.join(root, ent.name, "SKILL.md"); if (!fs.existsSync(skillMd)) continue; // not a skill dir skills++; - findings.push(...lintSkill(path.join(root, ent.name), ent.name)); + findings.push(...lintSkill(path.join(root, ent.name), ent.name, denylist)); } return { findings, skills }; } @@ -267,7 +311,12 @@ function main(argv) { const known = loadKnown(parsed.known); if (known.error) return fail(known.error); - const { findings, skills } = lintDir(parsed.dir); + const isLibrary = path.resolve(parsed.dir) === path.resolve(DEFAULT_DIR); + const denyFile = parsed.denylist ?? (isLibrary ? DEFAULT_DENYLIST : null); + const denylist = denyFile ? loadDenylist(denyFile) : []; + if (denylist.error) return fail(denylist.error); + + const { findings, skills } = lintDir(parsed.dir, denylist); const lines = render(findings, known); const errors = lines.filter((l) => l.includes(" error: ")).length; const warnings = lines.filter((l) => l.includes(" warning: ")).length; @@ -294,4 +343,4 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { process.exit(main(process.argv.slice(2))); } -export { main, lintDir, lintSkill, collectCompanionRefs, isResolvable, loadKnown }; +export { main, lintDir, lintSkill, collectCompanionRefs, isResolvable, loadKnown, loadDenylist }; diff --git a/packages/extension/skills/amico-catalog/SKILL.md b/packages/extension/skills/amico-catalog/SKILL.md index b6bd56cd0..d1afd514b 100644 --- a/packages/extension/skills/amico-catalog/SKILL.md +++ b/packages/extension/skills/amico-catalog/SKILL.md @@ -5,7 +5,7 @@ agents: [researcher, librarian, dreamer] surface: public --- -Pulse catalog management for the Amico system. Covers warm-start retrieval, pulse ingestion, and versioning. Phase 0 scope: basic warm-start retrieval and ingestion only — the catalog CLI's query/ingest surface does not yet filter by or record the schema-v3 compiler stack fields (system hash, device id, verification block); that follow-up is tracked at harmoniqs/amicissimo#497. The schema-v3 fields themselves have landed in the compilation package (see "Compiler Stack Fields" below) — the ground truth lives there, not here. +Pulse catalog management for the Amico system. Covers warm-start retrieval, pulse ingestion, and versioning. Phase 0 scope: basic warm-start retrieval and ingestion only — the catalog CLI's query/ingest surface does not yet filter by or record the schema-v3 compiler stack fields (system hash, device id, verification block); that follow-up is tracked separately. The schema-v3 fields themselves have landed in the compilation package (see "Compiler Stack Fields" below) — the ground truth lives there, not here. ## Catalog Structure @@ -52,7 +52,7 @@ The compiler-stack fields phase landed 2026-10-04 with the compilation package's - **Query/ranking** — filters by platform, gate, device id, system hash (exact-hash option); superseded versions drop from ranking unless requested. - **Ingestion** — provenance per block (git commit, source script); the bundled public library is IP-gated by a device-profile allowlist. -Phase 0 of THIS card's surface (the vault catalog + `amico catalog` CLI) does not implement these yet: retrieval remains by platform+gate only. The CLI follow-up (schema-v3 fields in query/ingest, verification-aware ranking) is tracked at **harmoniqs/amicissimo#497**. Until it lands, a session needing hash-exact or verification-aware lookup should use the compilation package's query API directly. +Phase 0 of THIS card's surface (the vault catalog + `amico catalog` CLI) does not implement these yet: retrieval remains by platform+gate only. The CLI follow-up (schema-v3 fields in query/ingest, verification-aware ranking) is a tracked follow-up. Until it lands, a session needing hash-exact or verification-aware lookup should use the compilation package's query API directly. ## Warm-Start Retrieval @@ -101,4 +101,4 @@ Old versions remain in catalog (never deleted) but are not candidates for warm-s ## Phase 0 Scope -No compiler stack fields in THIS catalog's metadata or CLI yet (the fields themselves landed in the compilation package's schema v3 — see "Compiler Stack Fields" above; CLI adoption is harmoniqs/amicissimo#497). No multi-branch catalog management yet. Retrieval is by platform+gate only; system-specific matching (e.g., by Hamiltonian hash) is done through the compilation package's query API, not this catalog. +No compiler stack fields in THIS catalog's metadata or CLI yet (the fields themselves landed in the compilation package's schema v3 — see "Compiler Stack Fields" above; CLI adoption is a tracked follow-up). No multi-branch catalog management yet. Retrieval is by platform+gate only; system-specific matching (e.g., by Hamiltonian hash) is done through the compilation package's query API, not this catalog. diff --git a/packages/extension/skills/amico-schema-check/SKILL.md b/packages/extension/skills/amico-schema-check/SKILL.md index f2b55ddab..bebed413f 100644 --- a/packages/extension/skills/amico-schema-check/SKILL.md +++ b/packages/extension/skills/amico-schema-check/SKILL.md @@ -78,7 +78,7 @@ These are ambiguous and reported for human review: ## Escalation -Ambiguous issues are logged to `dream-journal.md` with tag `needs-human-review`. If more than 5 unfixable issues are found, route the finding to the amicissimo proposals surface (agents never edit INTENT): "Schema issues found in N notes — human review needed (see dream-journal)." +Ambiguous issues are logged to `dream-journal.md` with tag `needs-human-review`. If more than 5 unfixable issues are found, route the finding to the house proposals surface (agents never edit INTENT): "Schema issues found in N notes — human review needed (see dream-journal)." ## Output diff --git a/packages/extension/skills/amico-strategy/SKILL.md b/packages/extension/skills/amico-strategy/SKILL.md index 3596d2e5c..a03aa6b12 100644 --- a/packages/extension/skills/amico-strategy/SKILL.md +++ b/packages/extension/skills/amico-strategy/SKILL.md @@ -9,7 +9,7 @@ On-demand loader for the Amicode research **composition** (the D6 model, spec-20 ## What exists (and what no longer does) -- **Direction** lives in `INTENT.md` (the amicissimo repo's vault dir), PI-owned: research bets, access filters, partner tracks, quality bars, banned directions, the campaign→direction map. It is amended **only by PR** — the human merge is the ownership transfer. `STRATEGY.md` in the team vault is **frozen evidence** of a retired ranked-P-list model; its superseded pointer is its only live content. +- **Direction** lives in `INTENT.md` (the house repo's vault dir), PI-owned: research bets, access filters, partner tracks, quality bars, banned directions, the campaign→direction map. It is amended **only by PR** — the human merge is the ownership transfer. `STRATEGY.md` in the team vault is **frozen evidence** of a retired ranked-P-list model; its superseded pointer is its only live content. - **The portfolio** (what is actually being worked on) is **derived mechanically from campaign session-ledgers** — never the session DB, never the board. A campaign ledger is a `sessions/session-*.md` note in the personal vault. - **The strategy brief** is the rendered composition: intent × portfolio, with **health stamps inline** (intent SHA + merged date + age; ledger anomalies; coverage) — staleness is IN the render. @@ -30,7 +30,7 @@ If the CLI is unavailable, read `INTENT.md` directly and fold the ledgers yourse - **No ranked priority list exists or should be synthesized.** Direction tiers order the portfolio's render; they are not a to-do list. Priorities are what is actually being worked on — the derived portfolio plus PI judgment. - **Health stamps are load-bearing:** an aged intent stamp or listed anomalies mean the render is stale or partial — say so before reasoning from it. - **Before proposing a new experiment,** check the campaign→direction map and the portfolio: does an existing campaign cover this? Don't duplicate effort. -- **Direction changes are proposals, not edits:** intent-direction proposals go to the amicissimo proposals surface (one file per proposal); survey-derived suggestions go to the hopper with a triage tag. This skill exposes **no INTENT write verb** — there is no update path from an agent. +- **Direction changes are proposals, not edits:** intent-direction proposals go to the house proposals surface (one file per proposal); survey-derived suggestions go to the hopper with a triage tag. This skill exposes **no INTENT write verb** — there is no update path from an agent. ## Who Uses This diff --git a/packages/extension/skills/analyze/SKILL.md b/packages/extension/skills/analyze/SKILL.md index a858728e4..d2d2fd6fb 100644 --- a/packages/extension/skills/analyze/SKILL.md +++ b/packages/extension/skills/analyze/SKILL.md @@ -17,7 +17,7 @@ Post-experiment analysis for Amico optimization results. ## Instructions ### When to Run -After a batch of optimization experiments (3+), or when research direction needs informing (route findings via the D6 rules: survey-shaped → the hopper with a triage tag; intent-direction → the amicissimo proposals surface — agents never edit INTENT). +After a batch of optimization experiments (3+), or when research direction needs informing (route findings via the D6 rules: survey-shaped → the hopper with a triage tag; intent-direction → the house proposals surface — agents never edit INTENT). ### Step 1: Gather Data - Read recent experiment notes in `/ledger/observations/` diff --git a/packages/extension/skills/develop/SKILL.md b/packages/extension/skills/develop/SKILL.md index f00b5ff4a..9417b4f19 100644 --- a/packages/extension/skills/develop/SKILL.md +++ b/packages/extension/skills/develop/SKILL.md @@ -4,7 +4,7 @@ description: The develop mode in one skill — the director loop protocol bound agents: [implementer, orchestrator] surface: public source: amicode -revision: 5 +revision: 6 --- # Develop — the director's protocol & issue-DAG walk @@ -192,7 +192,7 @@ A cast that returns **empty** — state `completed`, zero output: no commits, no ### Standing directives (AFK mandates) -A standing directive recorded in the ledger (the user's words + date — "do it all", "keep going for a while") is the explicit approval for the arc's issue filings AND its merge ladder, within the scope the directive names; every filing and every merge under it is reported in the loop record and the session summary. The mandate does not extend past its named scope: outward-publishing acts (releases, registration, visibility flips) always need the word. Worked example: the Favo campaign merged twelve standard-tier issues under two AFK mandates while holding its General-registry trigger parked for the user's explicit word (finding F34). +A standing directive recorded in the ledger (the user's words + date — "do it all", "keep going for a while") is the explicit approval for the arc's issue filings AND its merge ladder, within the scope the directive names; every filing and every merge under it is reported in the loop record and the session summary. The mandate does not extend past its named scope: outward-publishing acts (releases, registration, visibility flips) always need the word. Worked example: one package campaign merged twelve standard-tier issues under two AFK mandates while holding its General-registry trigger parked for the user's explicit word (finding F34). ### When NOT to use diff --git a/packages/extension/skills/formulation-display/SKILL.md b/packages/extension/skills/formulation-display/SKILL.md index 144b9e087..2377efae3 100644 --- a/packages/extension/skills/formulation-display/SKILL.md +++ b/packages/extension/skills/formulation-display/SKILL.md @@ -153,8 +153,8 @@ forms follow the papers' notation. | `NonhermitianExponentialIntegrator` (density / multidensity) | $\rho_{k+1} = e^{\mathcal{L}(\mathbf{a}_k)\Delta t}\rho_k$, $\mathcal{L}(\cdot) = -i[H,\cdot] + \text{dissipation}$ | Daleckii–Krein for the Fréchet derivative (`daleckii_krein.jl`) | | `BilinearIntegrator` | Piecewise-constant control, first-order | The simplest collocation step | | `SplineIntegrator` (Piccolo) | The constraint integrates the **exact spline waveform** reconstructed from knot values — the emitted pulse and the optimized pulse are the same object | Per trajectory type (unitary/ket/multiket/density/multidensity) | -| Piccolissimo `SplineIntegrator` cores | Magnus series: `MagnusGL4Alg`, `MagnusAdapt4Alg`; `ChebyshevAlg` (ket); Duhamel for density (`density_duhamel.jl`) | State which core ran — they are different approximations, and "Magnus does not apply to the dissipative density ODE (non-skew rhs)" is a recorded lesson | -| `SplitOperatorIntegrator` (Piccolissimo) | Strang split-step, matrix-free: kinetic factor $e^{-ik^2\Delta t/2m}$ (control-independent) + potential kicks; analytic JVP/VJP, no AD near the FFT | Atom-transport venue (issue #270) | +| Premium-tier spline integrator cores | Higher-order propagators for the same spline waveform (Magnus-series and Chebyshev-type cores) | State which core ran — they are different approximations, and "Magnus does not apply to the dissipative density ODE (non-skew rhs)" is a recorded lesson | +| Premium-tier split-operator integrator | Strang split-step: kinetic factor $e^{-ik^2\Delta t/2m}$ (control-independent) + potential kicks | Atom-transport problems | | Tsit5 (explicit RK) | Adaptive rollout only | **Never** a collocation constraint; verification/rollout path — quote its agreement separately (density paths are Tsit5-primary + Tsit5-cross-check: weaker integrator independence, a recorded honesty note) | ### Objective terms @@ -168,10 +168,10 @@ forms follow the papers' notation. | `LeakageObjective` | $\lVert(I - \Pi)\psi_k\rVert^2$ at knot points | `objectives.jl` | | `QuadraticRegularizer` | $R = \sum_k r\lVert\mathbf{a}_k\rVert^2$ | `docs_cache.jl` usage | | Minimum-time term | $D\sum_k \Delta t_k$ | `templates/minimum_time_problem.jl`; the two-stage construction follows arXiv:2305.03261's continuation | -| Piccolissimo regularizers (`HermiteBendingEnergyRegularizer`, `HermiteC2Regularizer`) | Bending energy $\int\lvert u''\rvert^2$ and C² forms on the spline | `objectives/hermite_bending_energy_regularizer.jl` | -| Adjoint robustness / `UnitarySensitivityObjective` | First-order infidelity response $\lvert\partial J/\partial\theta\rvert$ to Hamiltonian-parameter perturbations, via adjoints (no finite differences) | `objectives_robustness/`; Piccolissimo reexports | -| Channel process infidelity (Piccolissimo) | $F_\mathrm{pro}$ exact vs Choi, linear coordinates + HS-dual $\tau_k$ | `objectives/channel_process_infidelity.jl` | -| `MeasurementMatchingObjective` (Intonatissimo) | $\lVert y(z) - y_\text{target}\rVert^2_{Q_\text{meas}}$ | The intonatissimo card's display section | +| Spline smoothness regularizers (`HermiteBendingEnergyRegularizer`; the premium tier adds C² forms) | Bending energy $\int\lvert u''\rvert^2$ and C² forms on the spline | `objectives.jl` | +| Adjoint robustness / `UnitarySensitivityObjective` | First-order infidelity response $\lvert\partial J/\partial\theta\rvert$ to Hamiltonian-parameter perturbations, via adjoints (no finite differences) | `objectives.jl`, `objectives_robustness/` | +| Channel process infidelity (premium tier) | $F_\mathrm{pro}$, exact against the Choi representation | The premium tier's usage guidance | +| Measurement matching (premium calibration tier) | $\lVert y(z) - y_\text{target}\rVert^2_{Q_\text{meas}}$ | The premium calibration tier's usage guidance | ### Constraints @@ -182,15 +182,15 @@ forms follow the papers' notation. | Leakage bound (`KnotPointConstraint` / `NonlinearKnotPointConstraint`) | $\lVert(I-\Pi)\psi(t_k)\rVert^2 \le \varepsilon$ at knots | `constraints.jl` | | `FinalUnitaryFidelityConstraint` | $\mathcal{F}(U_N) \ge \bar{\mathcal{F}}$ | `templates/minimum_time_problem.jl` | | `DurationConstraint` | $\sum_k \Delta t_k \le T_{\max}$ (or per-interval $\Delta t$ bounds) | `templates/`; per-interval floors encode physics (settling, clock) | -| QILC trust region | $\lVert u - u_\text{ref}\rVert^2_{R_\text{tr}}$ recentered on $z_\text{ref}$ each iteration | The intonatissimo card | +| QILC trust region | $\lVert u - u_\text{ref}\rVert^2_{R_\text{tr}}$ recentered on $z_\text{ref}$ each iteration | The premium calibration tier's usage guidance | ### Solvers | Solver | What it is | When stated | | --- | --- | --- | | IPOPT (via DirectTrajOpt) | Interior-point, exact sparse NLP | Every Piccolo solve; `exact_hessian=false` on MultiKet is a recorded caveat | -| Altissimo (Piccolissimo) | Matrix-free interior-point; callback fields $f_\text{val}$, $eq\_viol$, $kkt\_error$ are the honest convergence line | Piccolissimo solves | -| Gauss–Newton second-order mode | Matrix-free HVP composition | When Piccolissimo's GN default runs | +| Premium-tier matrix-free solver | Matrix-free NLP solver; its own reported objective value, constraint violation, and optimality error are the honest convergence line | Premium-tier solves | +| Gauss–Newton second-order mode | Matrix-free HVP composition | When a premium-tier Gauss–Newton default runs | | Armijo backtracking (ILC) | Accept/reject on objective descent; show accepted **and** rejected steps | QILC convergence displays | **Completeness rule.** A formulation statement that omits any row the diff --git a/packages/extension/skills/intonatissimo/SKILL.md b/packages/extension/skills/intonatissimo/SKILL.md deleted file mode 100644 index f86c6b333..000000000 --- a/packages/extension/skills/intonatissimo/SKILL.md +++ /dev/null @@ -1,362 +0,0 @@ ---- -name: intonatissimo -description: Intonatissimo.jl usage — the entitled calibration tier. Measurement-matching objectives, NLP subproblem construction/updates, and the public-API ILC loop over Intonato's PulseTuningProblem chassis. Use when authoring a closed-loop pulse-tuning solve.jl in an Intonatissimo-enabled environment. -surface: entitled -entitlement: issimo -revision: 1 ---- - -# Intonatissimo — usage guide - -Intonatissimo is the entitled calibration tier: the measurement-matching -objective family, the control/calibration subproblem constructors, and the ILC -tuning strategy, all riding Intonato's public `PulseTuningProblem` loop. One -import reaches everything — `using Intonatissimo` re-exports the Intonato -chassis (which re-exports Piccolo), so `PulseTuningProblem`, `solve!`, -`Measurement`, `Learn` and friends are all in scope. - -> **Delivery mode (read first).** Intonatissimo requires the **issimo -> entitlement**. It is delivered either as the **HP-mode prebuilt sysimage** or -> via a **private-registry sandbox environment** — it is **NOT in the default -> provisioned solve env**. Recipes in this skill run only in those environments; -> smoke-test signatures in the actual environment before launching. - -## Measurement-matching objective - -`MeasurementMatchingObjective(model, y_target, traj; Q = 1.0, weights = nothing)` -builds a `KnotPointObjective` whose loss at each measurement knot is -$\sum_i \big(w_i (g_i(x) - y_i)\big)^2$ — the objective that matches simulated -measurements to a target vector inside an NLP. - -```julia -model = MeasurementModel(state_name(qtraj), [pop(), pop(), pop()], [1, 25, 50]) -kpo, params = MeasurementMatchingObjective(model, y_target, traj; weights = w_vec) -``` - -- Returns **two** values: the objective (compose it like any other - `AbstractObjective`) and a `Vector{MeasurementParam}` — **keep the params**. -- `MeasurementParam` is a mutable shared-reference container for one measurement - point's target and weights. Mutate through the references between outer - iterations; rebuilding the objective allocates and loses warm-start structure. -- `update_targets!(params, y_target)` — retarget in place. `y_target` is a - `Vector{Measurement}` (a `Measurement` wraps one measurement vector plus an - integer `index`). Here, as in `MeasurementModel` and `SimulatedExperiment`, the - index is a **knot index** π_j ∈ 1:N of the pulse — the state is read at - `get_knot_times(pulse)[index]`. See the hardware-path caveat below before - mixing these with measurements from a soc. -- `update_weights!(params, w_flat)` — write the flattened per-element effective - weights in model element order (measurement-major), in place. -- `weights` are per-measurement task-importance vectors. The legacy `Q` / - `Q_meas` kwargs are deprecated aliases that fold into task weights — pass - `weights`. - -## Subproblem family - -The subproblem constructors build the per-iteration control (and calibration) -NLPs that reuse the original problem's dynamics and constraints: - -| Call | Role | -|---|---| -| `build_subproblem(qcp, model, y_target; R_tr, R_θ, Q_meas)` | Control NLP: measurement matching + per-component trust-region weights (`R_tr`, e.g. `(u = 1e-2, du = 1e-1)`) + global-variable regularizers (`R_θ`, e.g. `(ω = 1.0,)`) → `SubproblemHandle` | -| `build_subproblem(qcp, model, y_target; include_original_objective = true)` | Same, with the QCP's original objective (including infidelity) composed in — use when measurement-matching alone doesn't drive fidelity | -| `build_calibration_subproblem(qcp, model, y_exp; R_θ, Q_cal)` | Calibration NLP: matches **raw** experiment data `y_exp` (not ILC-shifted targets), states free, everything else pinned | -| `update_subproblem!(handle, y_target, z_ref[, model])` | In-place iteration step: retarget, recenter trust-region baselines on `z_ref`, warm-start the trajectory; the 4-arg form also refreshes precision weights at the current prediction | -| `update_pinned_values!(handle, z_ref)` | Refresh a calibration subproblem's pin values to the new reference trajectory | -| `make_global_regularizer(name, traj, R)` | Quadratic penalty $(R/2)\|\theta - \text{baseline}\|^2$ on one global variable — compose your own `R_θ` set if the constructors' NamedTuple form doesn't express it | - -The discipline that makes this fast: **build once, update in place**. The -`SubproblemHandle` holds shared references to the objective's mutable parameters, so -`update_subproblem!` touches only targets, baselines, and the warm start — -no reconstruction between outer iterations. - -```julia -handle = build_subproblem(qcp, model, y_target; R_tr = (u = 1e-2, du = 1e-1)) -solve!(handle.prob; max_iter = 200) -update_subproblem!(handle, y_target_next, z_ref) # next outer iteration, same problem -``` - -## Driving the public ILC loop - -The loop chassis is Intonato's `PulseTuningProblem`: it wraps a solved -`QuantumControlProblem`, an experiment, and a measurement model, and runs the -outer propose → run experiment → retarget loop. The strategy kwarg selects the -inner step — pass an `ILCStrategy` to get Intonatissimo's ILC. - -```julia -using Intonatissimo - -mm = MeasurementModel(state_name(qtraj), [pop(), pop(), pop()], [1, 25, 50]) -expmt = SimulatedExperiment(qtraj_true, mm; rng = MersenneTwister(0)) # or a HardwareExperiment - -ptp = PulseTuningProblem(qcp, expmt, mm; strategy = ILCStrategy()) -solve!(ptp; max_iter = 5, tol = 1e-3, γ = 0.8, min_nominal_fidelity = 0.8) -# ptp.qcp is updated in place with the tuned result -``` - -- **Hardware-path index caveat (Intonato's QICK seam).** Through - `StrumentoExperiment` → `StrumentoBackend` → a soc (`MockSoc`, `TwinSoc`, - `StrumentoSoc`), the backend's `indices` are documented as knot indices, but - every soc resamples the pulse onto its DAC grid - (`floor(Int, T*dac_rate) + 1` points) and uses the integers as **DAC-sample** - indices of that grid — while the returned `Measurement`s still carry the knot - label. With N = 11, T = 5, `dac_rate = 20`, "index 11" is measured at t = 0.5, - not t = 5, so `y_exp` and the model targets disagree for a reason that is not - model error and ILC will "correct" it. Until Intonato measures at - `t = knot_times[k]` on the soc path: pass each knot's sample index - (`round(Int, t_k * dac_rate) + 1`) to the backend, or put the pulse's knots on - the DAC grid, and check one evaluation against `SimulatedExperiment` before a - loop. `SimulatedExperiment` itself is unaffected. -- **Plain ILC** is the bare `ILCStrategy()` — no parameter calibration. -- `solve!` kwargs: `max_iter` (outer iterations, default 5), `tol` (measurement - error convergence, default 1e-3), `γ` (trust-region schedule factor, default - 0.8), `line_search` (Armijo backtracking, default `true`), `ipopt_options` - (forwarded to the inner NLP solve), `max_rejections` (early stop after n - consecutive line-search rejections), `min_nominal_fidelity` (refuses to run on a - poorly-converged pulse; set 0 to skip). The old `polyak_avg` solve kwarg is - **deprecated** — average the last n iterates before syncing by passing - `selector = PolyakAverage(n)` to `PulseTuningProblem` instead. -- The goal measurements are resolved **once** at solve start and fixed for the - whole solve — pass `y_goal = [...]` to `PulseTuningProblem` to supply them - explicitly. -- The device model the loop plans against defaults to the nominal model of the - QCP's system; pass `device_model = ...` to plan against something predictive - (e.g. a parametric-Hamiltonian sysid model) instead. - -### The QP-ILC step: which representation to use - -`QPILCStrategy` (the QP inner step) is the production choice for correlator/population readouts. How to -shape its step was measured head-to-head in a research campaign (2026-10, transmon + Rydberg fixtures). -The production configuration (the relative-dose readout-null-space penalty, "FR_prod") and its measured -numbers live under **Production guidance** in the QP-ILC section below; the step-shaping lessons around it: - -- **Don't shrink the box instead of penalizing the null space.** A smaller trust box removes visible and invisible motion in - proportion. It helped at one fixture and did nothing at the other. -- **If you do project onto a basis** (`project_step` above, or your own), **clamp the projected step to - the production box afterwards**. A raw projection discards the QP's pull back inside the drive bounds, - so it produces out-of-bound pulses whenever the pulse sits at a bound (measured: on every accepted - step). -- **Only correct drift that persists, and expect a modest gain.** If the drift is redrawn independently - each round, no step representation beats leaving the pulse alone when the correction is scored on the - next round. Under persistent drift a row-space-confined step helps on average, but recovers only part - of the drift-induced infidelity, not most of it — see **Size expectations honestly** below for the - measured, seed-dependent size. The gain is roughly a fixed fraction of the drift loss minus a per-step - cost, so when the drift loss is small every representation did slightly worse than doing nothing. -- **Watch the in-loop refit.** A refit that re-centres its search on the previous estimate each round, - with no prior pull, can random-walk away from the device. Measured: on 2 of 16 seeds the fitted Rabi - scale drifted about 10% from truth while the true drift was about 1%, and fidelity fell round after - round even though the accept gate kept accepting. Bound or anchor the learned parameters, and compare - the refit's movement with the expected drift scale. -- **Check before trusting a full-QP run:** compare the step norm with the trust-box diagonal. A step at - the diagonal is spending its norm in the null space. - -### Calibration: learnable device parameters - -`ILCStrategy(mode; kwargs...)` adds parameter calibration on top of plain ILC. -`mode` is `Alternating()` (a separate calibration NLP before each control NLP — -the robust default when controls outnumber measurements) or `Joint()` -(parameters and controls co-optimized in one NLP — use under strong measurement -coverage). Learnable parameters are declared once as `Learn` values keyed by -system-global name; each `Learn`'s `weight` becomes that parameter's -regularization strength, and the constructor validates the QCP against the -declarations before any device time is spent. - -```julia -strat = ILCStrategy(Alternating(); - learn = (ω = Learn(ω_nom; bounds = (0.5ω_nom, 2.0ω_nom), weight = 5.0),), - Q_cal = 1.0, # calibration-NLP measurement weight - pin_Δt = true, # pin per-knot Δt across outer iterations - cal_eval_hessian = false, # calibration NLP on Ipopt L-BFGS (the fast default) -) -ptp = PulseTuningProblem(qcp, expmt, mm; strategy = strat) -solve!(ptp; max_iter = 5) -``` - -- `Learn(init; bounds = (lo, hi), weight = 1.0)` — `bounds` is required (an - unbounded learnable global is the failure mode this declaration exists to - prevent); `Learn(; bounds = ...)` starts from the nominal value. -- A mode object with an empty `learn` set errors — bare `ILCStrategy()` is the - plain-ILC form. -- `include_fidelity_in_u_nlp = true` also keeps the QCP's original objective in - the control NLP, driving fidelity directly when measurement-matching has - nontrivial kernel directions. -- The feedforward step itself is one call if you drive targets by hand: - `y_target = compute_ilc_targets(y_goal, y_model, y_exp)` — element-wise - $y_\text{goal} + y_\text{model} - y_\text{exp}$, pre-compensating the - estimated model error $\hat\varepsilon = y_\text{exp} - y_\text{model}$. - -## QP-ILC: `QPILCStrategy` (the one-shot hardware step) - -`QPILCStrategy(; …)` is the second inner step for the same `PulseTuningProblem` chassis: -each outer iteration solves one box-constrained QP -$\min_\delta \tfrac12\delta^\top(G_w^\top G_w + \lambda I)\delta - r_w^\top G_w\delta$ on a -whitened measurement Jacobian $G_w$, inside per-channel trust boxes, and the chassis -accepts or reverts on the measured outcome. Typical pairing: -`acceptance = OneShotAcceptance(β = 0.25, ρ_rej = 2.0, k = 3.0)` plus an explicit -`y_goal`. - -```julia -strat = QPILCStrategy(; - jacobian = AnalyticJacobian(), # opt-in eigen/Daleckii–Krein G (10–100× faster) - qp_backend = :lowrank, # opt-in identity-plus-low-rank box QP - robust_mode = true, # null-space penalty λ‖P_null δ‖² - robust_lambda_mode = :relative, robust_lambda = 1e2, # the H25 production dose -) # robust_rtol = nothing ⇒ 1e-8 under robust_mode -ptp = PulseTuningProblem(qcp, expmt, mm; strategy = strat, - acceptance = OneShotAcceptance(β = 0.25, ρ_rej = 2.0, k = 3.0), y_goal = y_goal) -solve!(ptp; max_iter = 12) -strat.last_linearization # (; Gw, rw, δ_lo, δ_hi) of the last successful step -``` - -The knobs merged since 2026-10-26 (Intonatissimo `main` @ `29a4c91`; docstring -`src/strategies/qp_ilc_strategy.jl:26-195`): - -- **`jacobian`** — `ModelJacobian()` (default; per-interval AD on the nominal or - recalibrated system), **`AnalyticJacobian()`** (#117, opt-in), `OpenSystemJacobian` - (Lindblad FD), `InjectedJacobian` (empirical G). `AnalyticJacobian` differentiates the - SAME discrete map as the AD source (midpoint substeps, the pulse's own interpolation) - by exact eigen-exponentials and Daleckii–Krein divided differences — agrees with AD to - its Taylor error (≲ 1e-12 relative on the campaign fixtures). It supports - `ZeroOrderPulse` (one exact exponential per interval), `LinearSplinePulse` and - `CubicSplinePulse` on **Hermitian, time-independent systems affine in the controls** - (ket and stacked multi-ket); anything else falls back to the AD source with a one-time - warning. It changes only G — the F-model diagnostic keeps the default Taylor rollout - (`src/optimizers/analytic_jacobian.jl:33-77`). -- **`qp_backend`** — `:clarabel` (default), `:osqp`, or **`:lowrank`** (#115, opt-in): one - thin SVD of $G_w$, the Hessian held exactly as $\alpha I + V D V^\top$ with the same - robust-mode rank cut, pinned variables eliminated, bounds satisfied exactly, never an - $n \times n$ matrix (≫ 50× faster than Clarabel on the 1206-DOF Rydberg shape — - the Intonatissimo repo's 2026-10-27 low-rank QP benchmark note, #113). The #64 ensemble - normal equations and the #66 `dt_dof` column are not identity-plus-low-rank and fall - back to `:clarabel` with a one-time warning. Telemetry: `last_qp_backend`, - `last_qp_iterations`, `last_qp_status`. Its solution tolerance is ~1e-8 — don't write - equivalence clauses tighter than that (H27: a 1e-10 clause sat 3 decades below it). -- **`robust_mode`** (default `false`) — adds $\lambda\|P_\text{null}\delta\|^2$, - $P_\text{null} = I - G_w^+ G_w$: penalizes J-invisible displacement (the E11 plateau fix). - `robust_lambda` (default `1e-2`) is read per **`robust_lambda_mode`**: `:absolute` - (default, prior numerics) or `:relative`, $\lambda_\text{eff} = $ `robust_lambda` · - $\sigma_\max(G_w)^2$ recomputed each step (telemetry `last_robust_lambda_eff`). -- **`robust_rtol`** (#110) — the singular-value cut of the projector. `nothing` (default) - now means **`1e-8` under `robust_mode`** (`DEFAULT_ROBUST_RTOL`, `:23`); the old - `pinv` cut (`eps·min(m, n)`) counted round-off modes as range and let the rank flip - across iterations, so $I - G^+G$ stopped being a projector and the QP failed. Spell the - old cut explicitly (`robust_rtol = eps(Float64) * min(size(Gw)...)`) only to reproduce - pre-#110 runs. Telemetry `last_robust_rtol`. Also from #110: a pulse whose interior knot - sits beyond the trust-box half-width outside its drive bound is clipped into bounds and - re-linearized instead of asserting (`last_restored_to_bound`, `restored_to_bound_count`). -- **`last_linearization`** (#114, read-only) — after a successful step, that step's - `(; Gw, rw, δ_lo, δ_hi)` (with the δT column under `dt_dof`), `==` a fresh - linearization at the same inputs, so analysis code need not re-assemble G; `nothing` - before the first step and after a failed QP. #114 also made the ZOH Jacobian hot paths - fast and bit-identical (substep-exp hoist, F-model rollout reuse, threaded intervals). -- **Pulse bases** — the QP proposes in the pulse's native space: ZOH and linear knot - values, cubic `[u; du]` (`du_frac` scales the tangent box); candidates are rebuilt in - the same type (a ZOH stays a `ZeroOrderPulse`, `src/optimizers/qp_inner_solver.jl:814-830`). - Design the pulse in its platform basis (PWL for AHS atoms, cubic elsewhere — ZOH is a - playback property); the QP inherits it. #116 also replaced the fixed-order Taylor - exponential with a norm-scaled one. Main's tests are re-paired to Piccolo #377's basis - guards (#119) — on a Piccolo with #377, the nominal `qcp` must pass its pairing check. -- `ridge` is numerical damping, **not** a step-size control; the box radius - (`tr_frac`, `tr_mode = :range | :gain`) is the knob. - -**Production guidance (Rydberg persistent-drift fixture, H25 16 seeds / H27 32 seeds × 3 -parameterizations; qpilc campaign ledger, personal vault):** - -- For the full (unrestricted) QP use **`robust_mode = true`, `robust_lambda_mode = - :relative`, `robust_lambda = 1e2`, `robust_rtol` default** ("FR_prod"). H25: 0 QP - failures (the old pinv cut failed 17/192 rounds at the same λ — the rtol default, not - the dose, removes them), non-inferior to the labelled restricted step, dose-insensitive - over c ∈ {10, 100, 1000}. H27: FR_prod beat the unpenalized full QP at every - parameterization (PWL13 25/32, PWL25 31/32, ZOH201 30/32 seeds). -- **Size expectations honestly:** the out-of-sample benefit vs doing nothing is small and - seed-dependent (ZOH201 median 1.2e-4 at 32 seeds; H25's 6.08e-4 was an upper-tail draw). - Read seed-paired contrasts, never absolute benefit at n ≈ 16. -- **The refit is the lever, not the step.** The per-seed losers are drift below the - per-step cost and in-loop **refit runaways** (Spearman −0.63…−0.73 at ZOH), not step - representation. rank($G_w$) = 5 at every parameterization tried (gate + 45-row readout - physics), so a readout null space exists regardless of knot count; the next work is a - prior-anchored / bounded refit (campaign R2 H26). -- At too few design columns the penalty costs **reach** (PWL13: ‖G_w δ‖ 0.50× the - unpenalized step at 33 free columns) — check reach before adopting a coarse design. - -## Error channels (call level) - -For updates confined to a labelled set of dissipative error channels, the -public surface is four calls: - -```julia -chans = [ErrorChannel(:T1_12, L_t1, 1e-3), ErrorChannel(:dephase, L_phi, 2e-4)] -sub = error_channel_subspace(integrator, traj, chans) # labelled gradient directions + orthonormal span -δx_p = project_step(δx, sub) # project a proposed step onto the span -c = capture(sub, direction) # fraction of the direction's energy the span explains -``` - -- `ErrorChannel(label::Symbol, L, γ)` — jump operator `L`, rate `γ > 0`, and a - label (e.g. `:T1_12`, `:leakage_2`, `:dephase`) that rides through for - attribution. -- `error_channel_subspace(integ, traj, channels; P_enc = nothing)` — one - normalized gradient direction per channel plus the orthonormal span. The - result carries an honest rank audit: structurally dependent channel sets - (e.g. two diagonal channels differing by a multiple of identity) are dropped - from the basis with a warning — read `size(sub.basis, 2)` for the working - dimension, never assume your channel count. -- `capture` is the diagnostic for "are these K channels enough to explain this - residual direction?" — a low value says the residual has components your - channel set doesn't span. - -> **Environment note (re-verified 2026-10-26).** The encoded-decay kernels this family consumes -> (`conjugation_frames` and companions, imported from Piccolissimo) are on **Piccolissimo main** -> since its PR #422 (merged 2026-09-05). The old `wip/414-conjugation-kernels` branch pairing is no -> longer needed. If `using Intonatissimo` fails with a missing-name error, your Piccolissimo -> predates that merge: update it. The calls above do not change. - -## Display — the QILC problem in classic form - -A QILC write-up (report finding, demo, paper section) states the problem -in classic optimization form, in the `formulation-display` house style, -before any results. Three equations, all from the constructors above: - -**The measurement model.** Operators $\{M_j\}$ evaluated on the state at -measurement knots $t_k$: - -$$y_k = \big(\langle\psi(t_k)| M_j |\psi(t_k)\rangle\big)_{j}, \qquad -y_{\text{model}} = h(z) \ \text{(NLP rollout)}, \qquad -y_{\text{exp}} = h_{\text{device}}(\text{pulse}) + \text{noise}$$ - -**The measurement-matching subproblem** (`build_subproblem`): control NLP -minimizing measurement error against shifted targets under a trust region: - -$$\begin{split} -\underset{z}{\text{minimize}}\quad & -\lVert y(z) - y_{\text{target}} \rVert^2_{Q_{\text{meas}}} -+ R_{\text{tr}}\lVert u - u_{\text{ref}}\rVert^2 -+ R_{\theta}\lVert\theta - \theta_{\text{ref}}\rVert^2 \\ -\text{subject to}\quad & \text{trajectory dynamics (collocation)},\quad -\text{control + global bounds} -\end{split}$$ - -with the option to compose the original objective (infidelity) in when -measurement-matching alone does not drive fidelity. - -**The ILC correction** (`compute_ilc_targets`): the model error estimate -$\hat\varepsilon = y_{\text{exp}} - y_{\text{model}}$ is pre-compensated -into the targets, -$y_{\text{target}} = y_{\text{goal}} + y_{\text{model}} - y_{\text{exp}}$, -and the loop iterates solve $\to$ apply $\to$ measure with Armijo -backtracking acceptance and the trust-region schedule $\gamma$; the -convergence criterion is measurement error $\le$ `tol`. - -### The required display set (a QILC write-up always carries) - -1. **The measurement model, drawn**: the operators, their knots, the goal - targets $y_{\text{goal}}$ — a reader must see *what is being matched* - before any correction makes sense. -2. **Pulse + correction plots, per iteration**: the applied pulse against - the nominal, and the correction $\delta u$ per accepted iteration — - the ILC's actual movement. -3. **Convergence**: measurement error and objective per outer iteration, - accepted/rejected Armijo steps marked, trust-region radius against - iteration. - -A QILC result reported without these three is incomplete — the -calibration tier's results live or die on whether the measurement model -was honest, and only these displays let the reader check. diff --git a/packages/extension/skills/intonatoqick/SKILL.md b/packages/extension/skills/intonatoqick/SKILL.md index c09a27d20..3af87e824 100644 --- a/packages/extension/skills/intonatoqick/SKILL.md +++ b/packages/extension/skills/intonatoqick/SKILL.md @@ -3,7 +3,7 @@ name: intonatoqick description: Drive the public IntonatoQICK.jl QICK/RFSoC hardware backend — mock loop, channel map, measurement model, and composing a QickExperiment into Intonato's closed-loop chassis. Use when running or testing quantum optimal control on QICK hardware (or its pure-Julia mock). agents: [experimenter, engineer] surface: public -revision: 1 +revision: 2 --- # IntonatoQICK — the public QICK hardware backend @@ -34,7 +34,7 @@ not push board specifics into Julia. closed-loop path — `PulseTuningProblem` → `StrumentoExperiment` → `StrumentoBackend` → Strumento.jl → the Python `strumento` side — plus MockSoc-first validation and the unattended-loop safety gates; **strumento** owns the board-side tProc-v2 experiment framework -itself; **intonatissimo** is the entitled tuning-strategy tier (below). This skill stays at +itself; the **premium calibration tier** supplies the convergent tuning strategies (below). This skill stays at the IntonatoQICK package boundary. ## Core types @@ -76,7 +76,7 @@ solve!(ptp; max_iter = 10) knots on the DAC grid. - Swap `MockQickSoc` → `PyQickSoc` for a real board; nothing else in the loop changes. - **Intonato ≥ v0.4:** the `R_tr` / `Q_meas` constructor kwargs are **removed** — trust-region - weighting belongs to the strategy (Intonatissimo's subproblem constructors take `R_tr`) and + weighting belongs to the strategy (the tuning strategy's own constructors take `R_tr`) and task weighting to the whitening `W_task`; the chassis constructor takes neither. ## The tuning strategy is where public stops @@ -88,8 +88,8 @@ Newton in the low-rank principal subspace of the model-cost Hessian, where the d supplies the directions/curvatures and the gradient is measured on the device by central finite differences along each direction. **Algorithmic convergence** (matching hardware measurements iteration-over-iteration) beyond that needs a concrete tuning strategy, which ships as the -**entitlement-gated** `intonatissimo` tier — see -the in-repo `intonatissimo` skill if you hold that entitlement. Without it, the public path is: +**entitlement-gated** premium calibration tier — its usage guidance loads in sessions that hold +that entitlement. Without it, the public path is: drive the backend, roll the mock, validate the seam. ## Verification contract diff --git a/packages/extension/skills/migrate-research-project/SKILL.md b/packages/extension/skills/migrate-research-project/SKILL.md index 7d1ffb681..d730f7c0a 100644 --- a/packages/extension/skills/migrate-research-project/SKILL.md +++ b/packages/extension/skills/migrate-research-project/SKILL.md @@ -90,7 +90,7 @@ Read the file content — do not classify by filename alone. - Reference material that doesn't change between experiments **Experiment scripts** — destination: `scripts/experiment/` -- `.jl` files with `using Piccolo`, `QuantumCollocation`, `Piccolissimo` +- `.jl` files with `using Piccolo`, `QuantumCollocation`, or another Piccolo-family package - Contains `solve!`, `UnitarySmoothPulseProblem`, trajectory definitions - The bulk of a researcher's scripts — optimization solves, gate synthesis @@ -160,7 +160,7 @@ Read these sources for manifest fields — do NOT ask for what you can read: reasoning, let the researcher correct **Domain pack** (optional — infer silently): -- `using Piccolo` or `using Piccolissimo` → `quantum-control` +- `using Piccolo` (or any Piccolo-family package) → `quantum-control` - Otherwise: omit (no default) **Author** (optional — infer silently): diff --git a/packages/extension/skills/piccolissimo/SKILL.md b/packages/extension/skills/piccolissimo/SKILL.md deleted file mode 100644 index 9e651e3ac..000000000 --- a/packages/extension/skills/piccolissimo/SKILL.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -name: piccolissimo -description: Piccolissimo.jl usage — the entitled fast-path tier for smooth-pulse problems. Spline-faithful integrators for Piccolo problems, adjoint robustness objectives, Gauss–Newton solver configuration, Magnus algorithm selection, and warm-start idioms. Use when authoring a solve.jl in a Piccolissimo-enabled environment. -surface: entitled -entitlement: issimo ---- - -# Piccolissimo — usage guide - -Piccolissimo is the entitled fast-path tier for smooth-pulse problems: you author -Piccolo problems exactly as usual (`SplinePulseProblem`, `SmoothPulseProblem`, -`UnitaryTrajectory`, …) and plug in Piccolissimo's faster integrators, robustness -objectives, and solver configuration on top. `using Piccolo, Piccolissimo` reaches -both surfaces. - -> **Delivery mode (read first).** Piccolissimo requires the **issimo entitlement**. -> It is delivered either as the **HP-mode prebuilt sysimage** or via a -> **private-registry sandbox environment** — it is **NOT in the default provisioned -> solve env**. Recipes in this skill run only in those environments; smoke-test -> signatures in the actual environment before launching. - -## Selecting the spline integrator - -Loading Piccolissimo registers a `SplineIntegrator` into Piccolo's integrator -registry. In direct problem construction you pass the integrator explicitly: - -```julia -using Piccolo, Piccolissimo - -sys = QuantumSystem(H_drift, H_drives, drive_bounds) # your model, as usual -pulse = CubicSplinePulse(u_init, du_init, times) # spline pulse -qtraj = UnitaryTrajectory(sys, pulse, U_goal) - -N = 50 -integrator = SplineIntegrator(qtraj, N) # spline-faithful dynamics -qcp = SplinePulseProblem(qtraj, N; integrator = integrator, Q = 100.0, du_bound = 10.0) - -solve!(qcp; max_iter = 200, tol = 1e-8) -``` - -Three facts worth internalizing: - -- **`SplineIntegrator` is Piccolo open-core (since the 2026-09 slices) — but check - your resolution.** The old error text saying it is "Piccolissimo only" is stale - (the S-18 finding): it is TRUE of the provisioned env's Piccolo git-tree but - FALSE of the registered v2.1.0 *tag* — the same version string covers both - realities, so probe before relying on it: - `isdefined(Piccolo, :SplineIntegrator)`. The `:pwc` fallback - (`BilinearIntegrator`) models the drive as piecewise-constant and ignores - `:du` — a spline pulse optimized against it is not the pulse its name promises - (Piccolo now errors on the CubicSplinePulse+Bilinear pairing by design). - What this tier still exclusively owns: the performance flags (GPU spline - cells, matrix-free HVP), the HermitianExponential specializations, adjoint - robustness, and the conjugation kernels (P1 on main, unreleased). -- **`global_names` requires this integrator.** Optimizing global variables - (frequencies, couplings) with `SplinePulseProblem(qtraj, N; global_names = [...])` - errors unless you pass a globals-aware integrator — the spline integrator above, - constructed with `global_names = [:ω, ...]`. -- **Algorithm choice is a constructor kwarg.** `SplineIntegrator(qtraj, N; - alg = Tsit5Alg())` is the default; see the Magnus section below for the other - algorithms and when to reach for them. - -On ket and multiket trajectories the constructor additionally accepts -`exact_hessian` (request the exact second-order sensitivity path where implemented) -and `use_ket_sensitivity` (ket-level sensitivity propagation). Some combinations -error at construction rather than silently falling back — if a requested algorithm -does not support the sensitivity mode you asked for, you will know immediately. - -## Robustness objectives - -Two public objectives make a pulse robust to parameter error, and one wrapper -composes them into a solvable problem: - -- `AdjointRobustnessObjective` — operator-space adjoint sensitivity objective. -- `KetAdjointRobustnessObjective(integrator, error_operators, goal, traj; Q = 1.0)` - — ket-space variant, constructed from an exponential integrator over a - `KetTrajectory` (or its multiket sibling over `MultiKetTrajectory` with a vector - of goals). -- `RobustControlProblem(qcp; kwargs...)` — wraps an existing solved problem to - minimize error susceptibility subject to a fidelity floor. Deep-copies the - trajectory and constraints, composes the robustness term with the original - regularizers, and adds a `FinalUnitaryFidelityConstraint`-style floor — the same - composition pattern as `MinimumTimeProblem`. - -```julia -# qcp: a solved unitary problem (spline- or bilinear-integrated) -rcp = RobustControlProblem(qcp; - error_operators = [E_detune, E_amp], # Hermitian error matrices - sys = sys, # REQUIRED when qcp integrates with SplineIntegrator - final_fidelity = 0.9999, # fidelity floor constraint - Q_robustness = 1.0, # weight on the robustness term -) -solve!(rcp; max_iter = 200) -``` - -`keep_infidelity_objective = true` keeps the original infidelity term in the -objective alongside robustness (default `false` — the floor constraint carries -fidelity). `error_operators` are the Hermitian matrices whose susceptibility you -want minimized (e.g. a detuning shift, an amplitude-scale error). - -## Gauss–Newton solver configuration - -The spline integrator's default second-order mode is Gauss–Newton. Configuration -guidance, all at the `solve!`/constructor level: - -- **Iteration caps and tolerances** pass straight through `solve!` as Ipopt - options: `solve!(qcp; max_iter = 200, tol = 1e-8, constr_viol_tol = 1e-6, - acceptable_tol = 1e-6)`. `max_iter` 100–300 is the working band for spline - problems; tighten `tol` when you need the last digit of fidelity. -- **Linear-drive models** (affine drive coefficients — the standard transmon - bilinear form) are the GN path's home regime: keep the defaults. -- **Nonlinear-drive models** (models with drive coefficients like $|\alpha|^2$ - — dispersive transmon-cavity) converge poorly on the GN spline path. The - composed doctrine: **exact Hessian when feasible; L-BFGS as the stall - escape.** Concretely: - - when the exact-Hessian path is feasible for your model, keep it — or move - the problem to the exponential integrator, whose second-order path is - exact for its piecewise-constant controls; - - when the exact path stalls, `solve!(qcp; eval_hessian = false)` — switches - Ipopt to L-BFGS, which routinely fixes the stall on these models. An - L-BFGS escape is a stall fix, not a verified answer: **always follow it - with an independent rollout (`simulate`)** before trusting the result. -- **Ket/multiket problems** can pass `exact_hessian = true` to the - `SplineIntegrator` constructor to request the exact second-order sensitivity - path. The unitary path does not offer this flag. - -## Magnus algorithms and substep sizing - -The spline integrator's forward-propagation algorithm is a kwarg — these are the -choices and their accuracy knobs: - -| Algorithm | Constructor | Accuracy knob | -|---|---|---| -| `Tsit5Alg()` (default) | `Tsit5Alg(; adaptive = true, tol = 1e-6, ode_h = 0.1)` | `tol` (adaptive) / `ode_h` (fixed) | -| `MagnusGL4Alg` | `MagnusGL4Alg(; n_steps = 10, tol = 1e-6)` | `n_steps` — steps per knot interval, the **sole** accuracy knob (`tol` is inert) | -| `MagnusAdapt4Alg` | `MagnusAdapt4Alg(; tol = 1e-6)` | `tol` — adaptive on unitary problems; on ket problems it routes to the fixed-step cell and `tol` is inert (a warning fires — control accuracy via `MagnusGL4Alg(n_steps = …)`) | -| `ChebyshevAlg` | `ChebyshevAlg(; n_sub = :auto, bracket = nothing, …)` | `n_sub = :auto` self-sizes each interval at construction and freezes after; ket-only, matrix-free | - -Rule of thumb: keep the default `Tsit5Alg` for ordinary problems; reach for the -Magnus algorithms when stiffness, long gates, or large-$\|H\|$ regimes actually -demand them (e.g. `MagnusGL4Alg(n_steps ≈ 50)` for a deep Rydberg blockade, where -the default under-resolves and optimizer fidelity diverges from a fine re-rollout). -For simple bilinear single-qubit gates the plain path usually converges faster -than the Magnus variants. - -For manual substep sizing of the fixed-step cells, two exported diagnostics do -the arithmetic for you: `suggest_n_sub(H_drift, H_drives, bracket, coeff!, Ψ0, Δt; -phase_budget = 2.0, dyn_tol = 1e-8, grad_tol = 1e-6)` returns a substep count for -one interval, and `expl_discretization_error(...; same kwargs, n_sub)` (positional) reports the -estimated discretization error at a given count. Sizing is frozen after -construction — a mid-solve re-size is never silently attempted. - -## Warm-start idioms - -The pulse round-trips through disk, and a solved problem rehydrates directly: - -```julia -# after a solved run — extract and save the optimized pulse -pulse_v2 = CubicSplinePulse(get_trajectory(qcp)) # rehydrate from the solved trajectory -JLD2.jldsave("pulse.jld2"; pulse = pulse_v2, fidelity = 0.9998) - -# next script — reload and warm-start -pulse = load_pulse("pulse.jld2") # returns the pulse (only the "pulse" key) -qtraj = UnitaryTrajectory(sys, pulse, U_goal) -qcp = SplinePulseProblem(qtraj) # native knot times — best for warm-starting -solve!(qcp; max_iter = 60) -``` - -- `SplinePulseProblem(qtraj)` with no knot count uses the pulse's **native knot - times** — the intended warm-start path. -- `CubicSplinePulse(controls, derivatives, times)` builds a spline pulse from raw - knot data when you have it. -- `load_pulse` returns only the pulse object; bundle metadata (fidelity, gate - name) with `JLD2.jldsave(...; pulse = ..., fidelity = ...)` at save time. - -## Display — the spline NLP in classic form - -A Piccolissimo write-up states the problem in the `formulation-display` -house style, with the spline parameterization explicit. The controls are -**spline coefficients at knots** (not per-timestep samples): the emitted -waveform is by construction the spline the integrator integrates — that -is the package's defining honesty property (spline-faithful dynamics). -Classic form: - -$$\begin{split} -\underset{\text{knot values } \mathbf{a}_{1:K},\ (\Delta t,\ \theta)}{\text{minimize}}\quad & -\ell(\widetilde{U}_N) + R\,\|\text{control regularization}\| \\ -\text{subject to}\quad & -\text{spline-faithful collocation: } f(z_k, z_{k+1}; \text{spline}(\mathbf{a})) = 0 \\ -& \widetilde{U}_1 = I,\quad \text{amplitude bounds},\quad -\text{slew bound } \|\mathrm{d}u\| \le \texttt{du\_bound} -\end{split}$$ - -with $\theta$ the (optionally free) globals and the second-order mode -Gauss–Newton (matrix-free). State which integrator ran (`:spline` vs the -Piccolo default) — the two are not interchangeable claims (the -Piccolissimo-vs-Piccolo integrator comparison is a recorded lesson, not -lore). - -### The required display set - -1. **The emitted spline against the optimizer's knots** — the device - plays the spline, not the knot values; the two rendered together - expose any knot-vs-emitted mismatch. -2. **Gauss–Newton convergence** — the objective and constraint violation - trace; Altissimo's callback fields (`f_val`, `eq_viol`, `kkt_error`) - are the honest convergence line. -3. **Slew/roughness against the bound** — the `du_bound` margin, since - the smoothness axis is Piccolissimo's reason for existing. - -## Cross-reference - -The adjoint-robustness objective's own form (when used) is stated -alongside, from the robustness section above — a Piccolissimo report -never presents robustness numbers without the objective that produced -them. diff --git a/packages/extension/test/native_skills.test.ts b/packages/extension/test/native_skills.test.ts index 044c2a7f7..cd2a9f709 100644 --- a/packages/extension/test/native_skills.test.ts +++ b/packages/extension/test/native_skills.test.ts @@ -87,10 +87,16 @@ describe("stageNativeUserSkills (#1949 §1)", () => { }); it("does not stage an entitled skill without its entitlement", () => { - const r = stage(); - // piccolissimo is surface: entitled — no entitlements.toml in the temp HOME - expect(r.staged).not.toContain("piccolissimo"); - expect(fs.existsSync(path.join(r.dir, "piccolissimo"))).toBe(false); + // the in-repo library ships no entitled skill, so pin the gate on a fixture + // library root: a public sibling stages, the entitled one does not (no + // entitlements.toml in the temp HOME) + const lib = path.join(home, "fixture-library"); + writeSkill(lib, "public-usage", "public"); + writeSkill(lib, "premium-usage", "entitled", "entitlement: premium\n"); + const r = stage({ skillLibraryRoots: [{ path: lib, surfaces: ["public", "entitled"] }] }); + expect(r.staged).toContain("public-usage"); + expect(r.staged).not.toContain("premium-usage"); + expect(fs.existsSync(path.join(r.dir, "premium-usage"))).toBe(false); }); it("re-staging is idempotent", () => { diff --git a/packages/extension/test/ops/stage_internal_skills_goldens.test.ts b/packages/extension/test/ops/stage_internal_skills_goldens.test.ts index e98fd804d..a5c9aaa1a 100644 --- a/packages/extension/test/ops/stage_internal_skills_goldens.test.ts +++ b/packages/extension/test/ops/stage_internal_skills_goldens.test.ts @@ -50,6 +50,10 @@ const ALLOWLIST = [ "harmony", "shape", "sweep", + // private-package usage cards — vault-only (never in the public library) + "altissimo", + "piccolissimo", + "intonatissimo", ]; /** Vault content NOT on the allowlist — the hub leak-pin direction. */ diff --git a/packages/extension/test/skill_salvage_851.test.ts b/packages/extension/test/skill_salvage_851.test.ts index 7de83858e..be7e1b391 100644 --- a/packages/extension/test/skill_salvage_851.test.ts +++ b/packages/extension/test/skill_salvage_851.test.ts @@ -10,8 +10,9 @@ // amico-vault spec + plan schemas and the brainstorming spec template // carry the field; // - the `intonatoqick` skill (amico-plugin PR #42): public surface, -// provenance, the entitled tier named as `intonatissimo` (current -// package reality), and the sibling skills referenced not duplicated; +// provenance, the entitled tier named neutrally (no private-package +// name — the library is public), and the sibling skills referenced +// not duplicated; // - the restricted 6th vault kind (amico-plugin PRs #48/#27): the // amico-vault kind table carries all SIX kinds and the prompt-side // condensed routing (stack_state.ts) agrees — the prompt↔skill @@ -112,8 +113,9 @@ describe("salvaged intonatoqick skill (amico-plugin PR #42 → amicode#851)", () expect(skill).toMatch(/#851/); }); - it("names the entitled tuning tier as `intonatissimo` (current package reality), not the archived private path", () => { - expect(skill).toMatch(/intonatissimo/); + it("names the entitled tuning tier neutrally (premium calibration tier), never a private package or the archived private path", () => { + expect(skill).toMatch(/premium calibration tier/); + expect(skill).not.toMatch(/issimo/i); // private-package names stay out of the public library expect(skill).not.toMatch(/qilc-calibration/); // the archived repo's private-skill name expect(skill).toMatch(/IdentityStrategy/); // the public no-op default the boundary is defined against }); diff --git a/packages/extension/test/skills_integrity.test.ts b/packages/extension/test/skills_integrity.test.ts index 9bcab8c14..c74b3678b 100644 --- a/packages/extension/test/skills_integrity.test.ts +++ b/packages/extension/test/skills_integrity.test.ts @@ -6,7 +6,7 @@ // fail. import { describe, expect, it } from "vitest"; import { spawnSync } from "node:child_process"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -212,3 +212,94 @@ describe("skills-integrity lint [vpath] rotted vault-path rule (F28)", () => { expect(r.stdout + r.stderr).not.toMatch(/\[vpath\] error:/); }); }); + +// [private-package] — the public repo is world-readable, so a shippable skill +// (public / entitled / untagged surface) must not name a private package. The +// denylist lives in ONE file (scripts/lint-skills-private-packages.txt); the +// rule is on by default for the repo library and opt-in (`--denylist`) for +// any other root. `surface: internal` cards are vault-only and exempt. +const DENYLIST_FILE = join(EXT_ROOT, "scripts", "lint-skills-private-packages.txt"); + +function readDenylist(file: string): string[] { + return readFileSync(file, "utf8") + .split("\n") + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith("#")); +} + +describe("skills-integrity lint [private-package] denylist rule", () => { + it("errors on a shippable skill naming a denylisted package in its body or frontmatter name", () => { + const dir = mkdtempSync(join(tmpdir(), "skills-integrity-priv-")); + try { + const deny = join(dir, "deny.txt"); + writeFileSync(deny, "# fixture denylist\nSecretissimo\nhushpkg\n"); + makeSkill(dir, "body-skill", "Plug in SECRETISSIMO integrators for speed."); + makeSkill(dir, "hushpkg-usage", "Body without the word."); + makeSkill(dir, "untagged-skill", "Uses secretissimo.", "---\nname: untagged-skill\ndescription: x\n---\n"); + makeSkill(dir, "internal-skill", "Secretissimo internals.", "---\nname: internal-skill\ndescription: x\nsurface: internal\n---\n"); + makeSkill(dir, "near-miss-skill", "Secretissimos and hushpkgs are different words."); + const r = run(["--dir", dir, "--denylist", deny]); + expect(r.status).toBe(1); + const out = r.stdout + r.stderr; + expect(out).toMatch(/body-skill\/SKILL\.md:\d+: \[private-package\] error:.*Secretissimo/i); + expect(out).toMatch(/hushpkg-usage\/SKILL\.md:2: \[private-package\] error:.*hushpkg/); + expect(out).toMatch(/untagged-skill\/SKILL\.md:\d+: \[private-package\] error:/); + expect(out).not.toMatch(/internal-skill\/SKILL\.md/); // vault-only cards may name them + expect(out).not.toMatch(/near-miss-skill\/SKILL\.md/); // whole-word match only + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("every entry of the checked-in denylist trips the rule", () => { + const names = readDenylist(DENYLIST_FILE); + expect(names.length).toBeGreaterThanOrEqual(11); + const dir = mkdtempSync(join(tmpdir(), "skills-integrity-priv-default-")); + try { + names.forEach((n, i) => makeSkill(dir, `skill-${i}`, `See ${n}.jl for details.`)); + const r = run(["--dir", dir, "--denylist", DENYLIST_FILE]); + expect(r.status).toBe(1); + const hits = (r.stdout.match(/\[private-package\] error:/g) ?? []).length; + expect(hits).toBe(names.length); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("is off for a non-library root unless --denylist is passed (vault roots keep their verdict)", () => { + const dir = mkdtempSync(join(tmpdir(), "skills-integrity-priv-off-")); + try { + const deny = join(dir, "deny.txt"); + writeFileSync(deny, "Secretissimo\n"); + makeSkill(dir, "body-skill", "Plug in Secretissimo."); + expect(run(["--dir", dir]).status).toBe(0); + expect(run(["--dir", dir, "--denylist", deny]).status).toBe(1); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("--known can exempt a private-package hit (the documented exception list)", () => { + const dir = mkdtempSync(join(tmpdir(), "skills-integrity-priv-known-")); + try { + const deny = join(dir, "deny.txt"); + writeFileSync(deny, "Secretissimo\n"); + makeSkill(dir, "body-skill", "Plug in Secretissimo."); + const known = join(dir, "known.txt"); + writeFileSync(known, "body-skill/SKILL.md:private-package\n"); + const r = run(["--dir", dir, "--denylist", deny, "--known", known]); + expect(r.status).toBe(0); + expect(r.stdout).toMatch(/\[private-package\] known:/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("the repo library is clean by default, with ZERO private-package exceptions", () => { + const r = run(["--dir", REAL_SKILLS, "--known", KNOWN_FILE]); + expect(r.stdout + r.stderr).not.toMatch(/\[private-package\] (error|known):/); + expect(readFileSync(KNOWN_FILE, "utf8")).not.toMatch(/:private-package/); + // default-on for the library: the denylist file is actually consulted + expect(readDenylist(DENYLIST_FILE).length).toBeGreaterThan(0); + }); +});