mirror of
https://github.com/razzant/ouroboros.git
synced 2026-08-05 00:29:46 +00:00
feat(skills): restore light skill creation from scratch + capability-regression review guard
Light could no longer CREATE a new skill: decide_payload_short_form's blanket is_dir gate (f705b37)
errored when the payload directory did not exist yet, so writing a new skill's SKILL.md manifest was
rejected. Treat a missing payload as CREATE when the write IS the skill manifest (SKILL.md/skill.json)
— the explicit authoring signal — while a non-manifest file into a missing payload still errors (typo
guard). The downstream write already mkdir-parents, so the manifest write provisions the new
external/<name>/ payload. Restore the explicit light-create contract in prompts/SYSTEM.md.
Anti-regression: a new advisory `capability_regression` item in docs/CHECKLISTS.md asks reviewers
(LLM-first, severity-driven) whether a diff silently removes/narrows a previously-supported capability
— the failure class this very regression belonged to — plus golden "from zero" capability tests
(tests/test_skill_create_golden.py) that turn red on exactly that removal: light creates a new external
skill from scratch, and acting (mutative) subagents stay available in light under the owner MASTER toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
48cbf4afd0
commit
b5f492f03b
6 changed files with 136 additions and 3 deletions
|
|
@ -165,6 +165,7 @@ Used by `commit_reviewed` for all changes to the Ouroboros repository.
|
|||
| 18 | subagent_isolation | If the diff changes `schedule_subagent`, child-task queueing, task constraints, tool discovery/execution, data reads, or memory handoff, does it preserve the accepted live-subagent contract: strict `objective` + `expected_output` schema, inferred lineage/workspace/contract/deadline/resource inheritance, `local_readonly_subagent` schema and execute-time allowlist, subagent-scoped secret/control-file denial for data tools, nested readonly delegation only within configured depth/cap limits with depth>1 coerced to light, no local writes/commits/review/runtime/tool-expansion/skills-lifecycle/shell, enabled external tools allowed only by owner policy and inherited resources, the subagent browser boundary (external HTTP(S) + `file://` scoped to `workspace_root` + loopback EXCEPT Ouroboros control-plane ports; private/link-local/reserved/DNS-rebind still blocked; `evaluate` JS unavailable; `vlm_query`/`analyze_screenshot` available), full task-result handoff, new/changed wait/timeout paths for cognitive work using progress-aware/re-decidable waiting rather than a fixed cutoff that discards in-flight work (P5), and tests for both allowed and blocked paths? | critical |
|
||||
| 19 | evolution_durability | If the diff touches `supervisor/git_ops.py`, `launcher.py`, `server.py`, `ouroboros/preflight_runner.py`, `ouroboros/tools/review_helpers.py`, `ouroboros/tools/git.py`, tests, review gates, or evolution code, does it preserve hermetic preflight, live repo/data mutation fuses, remote-optional local commit success, and transaction/rescue evidence for interrupted self-modification? | critical |
|
||||
| 20 | context_budget_ssot | If the diff changes context-size budgets/constants (`ouroboros/context_budget.py`), the context layout/manifest, a section's tier/policy, or compaction thresholds: does it keep the low/max context split coherent (single SSOT + both profiles + docs + drift-guard tests in sync), preserve the tier-0 always-full core (BIBLE/SYSTEM/identity/scratchpad/knowledge-index/recent-dialogue) in EVERY mode, use a visible on-demand pointer instead of silent truncation (P1), and leave the blocking scope-reviewer >=1M floor untouched? (PASS with "Not applicable" if no context-budget/layout change.) | critical |
|
||||
| 21 | capability_regression | Does the diff REMOVE or NARROW a previously-supported user-facing behavior or capability — a tool/flag/mode/path that worked before now errors or is gated tighter (e.g. a new `is_dir`/existence guard that blocks a legitimate create, a tightened allowlist that drops a real path, a removed fallback)? If so, is it INTENTIONAL and disclosed as a breaking/capability change in the commit message + changelog? Accidental capability removal is the failure class this item names. Ask whether a golden "from zero" test would have caught it. Severity follows the `Critical surface whitelist` below — silently removing a documented capability or a safety/release contract is critical; a deliberate, disclosed narrowing or an internal-only refactor is advisory. | advisory |
|
||||
|
||||
### Severity rules
|
||||
|
||||
|
|
@ -182,6 +183,10 @@ Used by `commit_reviewed` for all changes to the Ouroboros repository.
|
|||
drift, off-by-one test counts, and minor descriptive inaccuracies in the
|
||||
README changelog row MUST NOT be raised as critical under `self_consistency`
|
||||
or `changelog_and_badge`. They surface here and do not block.
|
||||
- Item 21 (`capability_regression`) is advisory by default but escalates to
|
||||
critical under the `Critical surface whitelist` below: a SILENT removal/narrowing
|
||||
of a documented capability or a safety/release contract is critical; a
|
||||
deliberate, disclosed narrowing or an internal-only refactor stays advisory.
|
||||
|
||||
### Retry convergence for tests_affected
|
||||
|
||||
|
|
|
|||
|
|
@ -305,6 +305,12 @@ would pick. Supply both args together — passing only one returns a clear
|
|||
`bucket and skill_name must be supplied together` error instead of silently
|
||||
writing into the drive root.
|
||||
|
||||
To **create a new skill** the payload directory need not pre-exist: writing the
|
||||
manifest at the payload root (`path="SKILL.md"` or `path="skill.json"`) is the
|
||||
authoring signal and provisions the new payload (and marks it `self_authored`).
|
||||
A non-manifest path into a not-yet-existing payload still errors as a typo guard
|
||||
— write the manifest first, then add the rest of the files.
|
||||
|
||||
Equivalent ways to address `data/skills/external/weather/plugin.py` under
|
||||
light:
|
||||
|
||||
|
|
|
|||
|
|
@ -445,6 +445,21 @@ def _explicit_path_kind(path_text: str, *, repo_dir: Path, drive_root: Path) ->
|
|||
return ""
|
||||
|
||||
|
||||
_SKILL_MANIFEST_BASENAMES = frozenset({"skill.md", "skill.json"})
|
||||
|
||||
|
||||
def _is_skill_create_signal(path_text: str) -> bool:
|
||||
"""A missing payload is a typo for an arbitrary file but a legitimate NEW skill when the write IS
|
||||
the skill manifest at the payload ROOT (``SKILL.md``/``skill.json`` — NOT ``nested/SKILL.md`` and
|
||||
NOT an absolute path) — the explicit authoring signal. Keying CREATE on the root manifest (not on
|
||||
mere directory existence or a bare basename anywhere) restores light skill creation that the
|
||||
f705b37 blanket is_dir gate regressed, while a misspelled or nested path still errors (typo guard)."""
|
||||
raw = str(path_text or "").replace("\\", "/").strip()
|
||||
if raw.startswith("./"):
|
||||
raw = raw[2:]
|
||||
return "/" not in raw and raw.lower() in _SKILL_MANIFEST_BASENAMES
|
||||
|
||||
|
||||
def decide_payload_short_form(
|
||||
*,
|
||||
bucket: str,
|
||||
|
|
@ -476,11 +491,15 @@ def decide_payload_short_form(
|
|||
)
|
||||
)
|
||||
payload_root = (Path(drive_root) / synth.payload_root).resolve(strict=False)
|
||||
if not payload_root.is_dir():
|
||||
# CREATE-from-scratch is for AGENT-authored skills only: the `external` bucket. The marketplace
|
||||
# buckets (clawhub/ouroboroshub) are installed FROM the marketplace, never authored into a
|
||||
# missing payload, so a missing marketplace payload stays an error (install it, don't create).
|
||||
if not payload_root.is_dir() and not (clean_bucket == "external" and _is_skill_create_signal(path_text)):
|
||||
return PayloadShortFormDecision(
|
||||
error=(
|
||||
f"skill payload not found: {synth.payload_root}. "
|
||||
"Use an existing skill_name, or omit bucket/skill_name for a repo/data edit."
|
||||
"Use an existing skill_name; for a NEW skill write its manifest (SKILL.md/skill.json) "
|
||||
"under bucket=external; or omit bucket/skill_name for a repo/data edit."
|
||||
)
|
||||
)
|
||||
return PayloadShortFormDecision(constraint=synth)
|
||||
|
|
|
|||
|
|
@ -667,11 +667,16 @@ def _data_write(
|
|||
marker_path: pathlib.Path | None = None
|
||||
if (
|
||||
mode == "overwrite"
|
||||
and not (task_constraint and task_constraint.mode == "skill_repair")
|
||||
# A genuine NEW external skill is self-authored even when reached via the bucket+skill_name
|
||||
# short-form (which synthesizes a skill_repair constraint, so the old `not skill_repair`
|
||||
# guard wrongly suppressed provenance on create). Require BOTH the manifest AND the payload
|
||||
# directory to be new (`not marker_payload[2].exists()`, evaluated before the mkdir below) so
|
||||
# writing a SKILL.md into an ALREADY-EXISTING external skill is never mis-marked self-authored.
|
||||
and marker_payload is not None
|
||||
and marker_payload[0] == "external"
|
||||
and pathlib.PurePosixPath(str(path or "")).name.lower() in {"skill.md", "skill.json"}
|
||||
and not target_path.exists()
|
||||
and not marker_payload[2].exists()
|
||||
):
|
||||
marker_path = marker_payload[2] / _SELF_AUTHORED_MARKER
|
||||
should_mark_self_authored = not marker_path.exists()
|
||||
|
|
|
|||
|
|
@ -220,6 +220,10 @@ When creating or repairing a skill:
|
|||
- use skill-scoped tools/paths under the structured `task_constraint.mode=skill_repair`;
|
||||
- inspect payloads with `read_file`/`list_files` using `root=skill_payload`;
|
||||
- edit with `edit_text` for exact changes and `write_file` for new/full files using `root=skill_payload`;
|
||||
- create a NEW skill by writing its `SKILL.md` manifest (the authoring signal) into a fresh
|
||||
`external/<name>/` payload — `write_file(root="skill_payload", bucket="external", skill_name="<name>", path="SKILL.md", …)`;
|
||||
the payload directory need not pre-exist, and create works in
|
||||
`runtime_mode=light` (a missing payload errors only for a non-manifest path, as a typo guard);
|
||||
- run `skill_preflight`, then `skill_review`;
|
||||
- do not call a skill ready until review, grants, dependencies, enablement, and widget/extension visibility are checked as applicable.
|
||||
|
||||
|
|
|
|||
94
tests/test_skill_create_golden.py
Normal file
94
tests/test_skill_create_golden.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""Golden capability tests — product promises exercised from zero, so a silent capability
|
||||
regression turns a test red. Flagship: light can CREATE a new external skill from scratch (no
|
||||
pre-mkdir payload), the exact behavior the f705b37 blanket is_dir gate regressed."""
|
||||
|
||||
from ouroboros.contracts.skill_payload_policy import decide_payload_short_form
|
||||
|
||||
|
||||
def test_light_creates_new_external_skill_from_scratch(tmp_path):
|
||||
"""A NEW external skill is created by writing its SKILL.md/skill.json manifest even though the
|
||||
payload directory does not exist yet (the authoring signal). A non-manifest file into a missing
|
||||
payload still errors (typo guard); an existing payload edits normally. Red on f705b37."""
|
||||
drive_root = tmp_path / "data"
|
||||
repo_dir = tmp_path / "repo"
|
||||
drive_root.mkdir(parents=True)
|
||||
repo_dir.mkdir(parents=True)
|
||||
|
||||
# 1. CREATE: payload does NOT exist, the write IS the manifest -> allowed (constraint, no error).
|
||||
for manifest in ("SKILL.md", "skill.json"):
|
||||
decision = decide_payload_short_form(
|
||||
bucket="external", skill_name="weather", path_text=manifest,
|
||||
repo_dir=repo_dir, drive_root=drive_root,
|
||||
)
|
||||
assert decision.error == "", f"writing {manifest} into a fresh payload must create, not error"
|
||||
assert decision.constraint is not None and decision.constraint.mode == "skill_repair"
|
||||
|
||||
# 2. TYPO GUARD: payload does NOT exist, the write is a non-manifest file -> still errors.
|
||||
typo = decide_payload_short_form(
|
||||
bucket="external", skill_name="weather", path_text="plugin.py",
|
||||
repo_dir=repo_dir, drive_root=drive_root,
|
||||
)
|
||||
assert "skill payload not found" in (typo.error or "")
|
||||
assert typo.constraint is None
|
||||
|
||||
# 3. EDIT: an EXISTING payload edits normally, any filename.
|
||||
(drive_root / "skills" / "external" / "weather").mkdir(parents=True)
|
||||
edit = decide_payload_short_form(
|
||||
bucket="external", skill_name="weather", path_text="plugin.py",
|
||||
repo_dir=repo_dir, drive_root=drive_root,
|
||||
)
|
||||
assert edit.error == "" and edit.constraint is not None
|
||||
|
||||
# 4. MARKETPLACE buckets are INSTALLED, not authored from scratch: a manifest into a missing
|
||||
# clawhub/ouroboroshub payload still errors (only `external` is the agent-authoring bucket).
|
||||
for marketplace in ("clawhub", "ouroboroshub"):
|
||||
market = decide_payload_short_form(
|
||||
bucket=marketplace, skill_name="fromhub", path_text="SKILL.md",
|
||||
repo_dir=repo_dir, drive_root=drive_root,
|
||||
)
|
||||
assert "skill payload not found" in (market.error or ""), f"{marketplace} create must error"
|
||||
assert market.constraint is None
|
||||
|
||||
|
||||
def test_skill_create_signal_only_fires_for_root_manifest():
|
||||
"""The CREATE carve-out must be tight: only the manifest at the payload ROOT is an authoring
|
||||
signal — a nested or absolute path ending in SKILL.md is NOT, so it cannot smuggle a write into
|
||||
data/skills/<bucket>/<skill>/nested/ on a missing payload."""
|
||||
from ouroboros.contracts.skill_payload_policy import _is_skill_create_signal
|
||||
|
||||
assert _is_skill_create_signal("SKILL.md") is True
|
||||
assert _is_skill_create_signal("skill.json") is True
|
||||
assert _is_skill_create_signal("./SKILL.md") is True
|
||||
assert _is_skill_create_signal("nested/SKILL.md") is False
|
||||
assert _is_skill_create_signal("/etc/SKILL.md") is False
|
||||
assert _is_skill_create_signal("plugin.py") is False
|
||||
assert _is_skill_create_signal("") is False
|
||||
|
||||
|
||||
def test_short_form_create_marks_new_skill_self_authored(tmp_path, monkeypatch):
|
||||
"""End-to-end: creating a new external skill via the bucket+skill_name short-form (SKILL.md into
|
||||
a non-existent payload) succeeds AND marks it self-authored. The short-form synthesizes a
|
||||
skill_repair constraint, so provenance marking must NOT be suppressed for a genuine create
|
||||
(target/marker absent) — only for repair of an existing skill."""
|
||||
from ouroboros import config
|
||||
from ouroboros.tools.registry import ToolContext, ToolRegistry
|
||||
|
||||
repo_dir = tmp_path / "repo"
|
||||
drive_root = tmp_path / "drive"
|
||||
repo_dir.mkdir()
|
||||
drive_root.mkdir()
|
||||
# the self-authored marker keys off the GLOBAL config DATA_DIR — align it with this drive.
|
||||
monkeypatch.setattr(config, "DATA_DIR", str(drive_root))
|
||||
ctx = ToolContext(repo_dir=repo_dir, drive_root=drive_root)
|
||||
registry = ToolRegistry(repo_dir=repo_dir, drive_root=drive_root)
|
||||
registry._ctx = ctx
|
||||
|
||||
result = registry.execute("write_file", {
|
||||
"root": "skill_payload", "bucket": "external", "skill_name": "fresh",
|
||||
"path": "SKILL.md", "content": "---\nname: fresh\n---\nA fresh skill.\n",
|
||||
})
|
||||
|
||||
assert "OK" in result and "ERROR" not in result and "BLOCKED" not in result, result
|
||||
payload = drive_root / "skills" / "external" / "fresh"
|
||||
assert (payload / "SKILL.md").exists(), "the manifest must be created"
|
||||
assert (payload / ".self_authored.json").exists(), "a newly created external skill must be marked self-authored"
|
||||
Loading…
Add table
Add a link
Reference in a new issue