diff --git a/AGENTS.history.json b/AGENTS.history.json new file mode 100644 index 0000000..c83960e --- /dev/null +++ b/AGENTS.history.json @@ -0,0 +1,56 @@ +{ + "version": 2, + "entries": [ + { + "rules": [ + "SKILLS-ENF-01", + "SKILLS-ENF-02", + "ENF-01", + "ENF-02" + ], + "decision": "Project-local instructions remain mandatory and closure-gating within the global instruction stack.", + "reason": "Repository-specific workflow rules must retain the same enforcement force as global instructions.", + "regression": "A local rule cannot be treated as optional or bypass completion blocking when applicable." + }, + { + "rules": [ + "SKILLS-CHECKOUT-01", + "SKILLS-WORKTREE-01", + "SKILLS-STAGE-01", + "FILE-02", + "FILE-03" + ], + "decision": "Skill development occurs in one thread-owned external worktree while the primary checkout remains a controlled main or release surface.", + "reason": "Separating development from release and runtime generation prevents branch-state drift and accidental staging.", + "regression": "Skill source must not be developed in the primary checkout or staged into release without explicit staging, shipping, or preview intent." + }, + { + "rules": [ + "SKILLS-PREVIEW-01", + "SKILLS-SHIP-01", + "SKILLS-CREATE-01", + "SKILLS-SHIP-02" + ], + "decision": "Installed previews and shipments are generated from the controlled checkout and release branch, with new-skill creation retaining its promotion exception.", + "reason": "Runtime copies must correspond to an intentional release state rather than an arbitrary task worktree.", + "regression": "Do not install from task worktrees, ship from feature branches, or lose post-shipment restoration and reinstall steps." + }, + { + "rules": [ + "SKILLS-GOV-01", + "SKILLS-GOV-02", + "SKILLS-PORT-01", + "SKILLS-RUNTIME-01", + "SKILLS-MAINT-01", + "SKILLS-STYLE-01", + "SKILLS-VERIFY-01", + "SKILLS-AUTO-01", + "SKILLS-HELP-01", + "SKILLS-CREDIT-01" + ], + "decision": "Repository governance uses current control sources, portable runtime paths, aligned helpers, targeted verification, and explicit credit-cost review.", + "reason": "These constraints keep public skill sources portable and generated or recurring behavior aligned with its executable owner.", + "regression": "Do not apply advisory changes implicitly, embed user-local paths, bypass declared helper locations, or finish with prompt/helper or credit-cost drift." + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 80edb8d..3dcd317 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,74 +1,93 @@ ## Instruction enforcement -- All instruction bullets in this file are mandatory, blocking, and - closure-gating for the phase, action, decision, artifact, or response they - govern. -- Do not proceed with or claim completion for any action, decision, artifact, or - response when an applicable instruction bullet is unmet, unverifiable, or in - conflict; report the blocker or conflict instead. +- [SKILLS-ENF-01] All instruction bullets in this file are mandatory, + blocking, and closure-gating for the phase, action, decision, artifact, or + response they govern. + - overlaps: ENF-01 +- [SKILLS-ENF-02] Do not proceed with or claim completion for any action, + decision, artifact, or response when an applicable instruction bullet is + unmet, unverifiable, or in conflict; report the blocker or conflict instead. + - overlaps: ENF-02 Metadata: Project-specific rules for this skills repository. Skills repo checkout and worktrees: -- The primary skills repo checkout used to generate installed Ceratops skill - copies must stay either on local `main` tracking `origin/main` or on a local - `release/*` branch created from `main` for an active unpublished batch. -- Do not develop or patch Ceratops skill source directly in the skills repo - checkout during create, update, audit, or repair work; for any task that - modifies skills, work in one thread-owned git worktree, name that worktree +- [SKILLS-CHECKOUT-01] The primary skills repo checkout used to generate + installed Ceratops skill copies must stay on local `main` tracking + `origin/main` or a local `release/*` branch created from `main` for an active + unpublished batch. + - limits: FILE-02 +- [SKILLS-WORKTREE-01] Do not develop or patch Ceratops skill source directly + in the skills repo checkout during create, update, audit, or repair work. For + any task that modifies skills, work in one thread-owned git worktree, name it after the thread rather than a subtask, reuse it for follow-on skill changes in the same thread unless conflicting branch histories or explicit user direction require a new one, and do not place it inside the skills repo checkout. -- Keep installed Ceratops skill folders generated from the skills repo checkout - path, not from task worktrees. For local preview of unpublished batches, - refresh remote refs with `git fetch --prune origin`, then merge ready worktree - branches into the skills repo checkout's local `release/*` branch and run - `python scripts/install-skills.py` from that checkout instead of generating - installed skills from task worktrees. -- Do not stage skill-source changes into a local `release/*` batch unless the - task explicitly requests staging, shipping, or local preview sync. -- Skills-repo changes must ship from `release/*`, never directly from task or - feature branches. -- New Ceratops skill creation is the only default-staging exception: - `$ceratops-skill-lifecycle` create must finish with change-promotion and - install verification unless the user opts out. -- For staging or shipping, use `$ceratops-skill-lifecycle` change-promotion or - ship-to-remote. After shipping, restore the checkout from `origin/main`, - reinstall managed skills from `main`, and report retained worktrees or release - branches. + - self: exceeds-limit, list-heavy +- [SKILLS-PREVIEW-01] Keep installed Ceratops skill folders generated from the + skills repo checkout, not task worktrees. For unpublished local previews, + refresh remote refs with `git fetch --prune origin`, merge ready worktree + branches into the checkout's local `release/*` branch, and run + `python scripts/install-skills.py` there instead of generating installed + skills from task worktrees. + - self: list-heavy +- [SKILLS-STAGE-01] Do not stage skill-source changes into a local `release/*` + batch unless the task explicitly requests staging, shipping, or local preview + sync. + - limits: SKILLS-PREVIEW-01 +- [SKILLS-SHIP-01] Skills-repo changes must ship from `release/*`, never + directly from task or feature branches. +- [SKILLS-CREATE-01] New Ceratops skill creation is the only default-staging + exception: `$ceratops-skill-lifecycle` create must finish with + change-promotion and install verification unless the user opts out. + - overrides: SKILLS-STAGE-01 +- [SKILLS-SHIP-02] For staging or shipping, use `$ceratops-skill-lifecycle` + change-promotion or ship-to-remote. After shipping, restore the checkout from + `origin/main`, reinstall managed skills from `main`, and report retained + worktrees or release branches. + - self: list-heavy Instruction and skill maintenance: -- Before proposing or editing `AGENTS.md`, `automation.toml`, `SKILL.md`, shared - skill sections, skill manifests, or helper-contract text, re-open the relevant - files from disk and use the current contents as the source of truth. -- Treat recommendations about instruction, automation, skill, and - helper-contract changes as advisory unless the user explicitly asks to apply a - named change. -- In repo-tracked files intended for public sharing or GitHub, including - repo-tracked `AGENTS.md`, `automation.toml`, `SKILL.md`, generated runtime +- [SKILLS-GOV-01] Before proposing or editing a repository control surface, + including `AGENTS.md`, `automation.toml`, `SKILL.md`, skill manifests, shared + sections, or helper contracts, re-open the relevant files from disk and use + the current contents as the source of truth. + - self: list-heavy +- [SKILLS-GOV-02] Treat recommendations about instruction, automation, skill, + and helper-contract changes as advisory unless the user explicitly asks to + apply a named change. +- [SKILLS-PORT-01] In repo-tracked files intended for public sharing or GitHub, + including `AGENTS.md`, `automation.toml`, `SKILL.md`, generated runtime skill files, scripts, docs, and examples, do not hardcode user-local absolute filesystem paths unless an external runtime explicitly requires them; use repo-relative paths or portable variables such as `$CODEX_HOME`. -- For skill runtime workflows, invoke shared helpers through installed console - commands, `python -m ` entrypoints, or scripts copied into the - installed skill folder; do not locate shared helpers by absolute paths or by - the repo's parent directory. -- When a workflow needs a shared repo-maintenance script, run `scripts/` - from the active source checkout root when available, otherwise from the + - self: list-heavy +- [SKILLS-RUNTIME-01] For skill runtime workflows, invoke shared helpers through + installed console commands, `python -m ` entrypoints, or scripts in + the installed skill folder; do not locate shared helpers by absolute paths or + by the repo's parent directory. + - overlaps: HELP-01 +- [SKILLS-MAINT-01] When a workflow needs a shared repo-maintenance script, run + `scripts/` from the active source checkout root when available, or the installed skill folder; when a helper is skill-local, run it from that skill folder or the corresponding source skill folder; stop as blocked if neither declared location contains it. -- Prefer concise, principle-based, machine-oriented wording; avoid example lists - unless the examples are needed to disambiguate behavior. -- After instruction edits, verify the changed diff or reopened section and - confirm no new duplicate, contradiction, or dropped behavior was introduced. -- When an automation uses a script or helper, compare prompt and code before - finishing and keep outcome, blocker, cleanup, alert, and memory paths aligned. -- Put deterministic, testable, or procedural automation behavior in scripts or - helpers rather than prompt text when such helpers exist. -- When updating an automation, skill, instruction, or related helper script, +- [SKILLS-STYLE-01] Prefer concise, principle-based, machine-oriented wording; + avoid example lists unless needed to disambiguate behavior. + - overlaps: OUT-01, OUT-02 +- [SKILLS-VERIFY-01] After instruction edits, verify the changed diff or + reopened section and confirm no new duplicate, contradiction, or dropped + behavior was introduced. +- [SKILLS-AUTO-01] When an automation uses a script or helper, compare prompt + and code before finishing and keep outcome, blocker, cleanup, alert, and + memory paths aligned. + - self: list-heavy +- [SKILLS-HELP-01] Put deterministic, testable, or procedural automation + behavior in scripts or helpers rather than prompt text when helpers exist. +- [SKILLS-CREDIT-01] When updating an automation, skill, instruction, or helper, assess whether the change could materially increase recurring or avoidable credit usage; if so, report that before treating the update as done. + - self: list-heavy diff --git a/README.md b/README.md index 2ff425b..ada9a10 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,8 @@ Reusable Ceratops skills for Codex and other `SKILL.md`-compatible agents. | Skill | Purpose | | --- | --- | | `ceratops-gh-repo-lifecycle` | Route GitHub repo lifecycle work across create-or-publish, contracts-review, health-audit, dependency-maintenance, ensure-pr, ship-change, and merge-pr actions. | -| `ceratops-propose-rules-update` | Design compact, regression-safe changes across interacting instruction scopes. | +| `ceratops-governance-lifecycle` | Route prompt optimization, advisory skill optimization, regression-safe instruction updates, and cross-scope governance consistency audits across action references. | | `ceratops-credit-savings-analysis` | Analyze recent Codex runs for avoidable credit spend and recommend low-maintenance controls. | -| `ceratops-prompt-optimizer` | Rewrite rough prompts into clearer structured prompts without changing intent. | -| `ceratops-skill-optimize` | Propose advisory-only improvements for one skill by default or an explicitly requested skill set. | | `ceratops-skill-lifecycle` | Route skill lifecycle work across create, make-repo-compatible, update, skills-contract-review, skills-consistency-review, fast-change, change-promotion, and ship-to-remote actions. | | `ceratops-automation-run` | Run recurring automations with shared Ceratops alert, memory, and completion policy. | | `ceratops-task-lifecycle` | Route task execution, fix-loop break, same-thread resume, handoff, and closure-check work across action references. | diff --git a/skills/ceratops-credit-savings-analysis/SKILL.md b/skills/ceratops-credit-savings-analysis/SKILL.md index 7344ab9..ebeb57c 100644 --- a/skills/ceratops-credit-savings-analysis/SKILL.md +++ b/skills/ceratops-credit-savings-analysis/SKILL.md @@ -57,6 +57,7 @@ cannot be identified. inspected evidence as implemented or still unimplemented; omit implemented controls unless needed to justify that no still-unimplemented proposal remains. +- Merge recommendations that share the same producer and control. - When prompt-level savings cases exist, rank the top five evidence-backed cases, or all available cases when fewer exist, using only information available when each prompt was written; present them as @@ -71,7 +72,7 @@ cannot be identified. - If the active issue is one unresolved bug, use `$ceratops-task-lifecycle` with the `fixloop-break` or `execute-in-stages` action first. - If the user already knows the specific rule to change, use - `$ceratops-propose-rules-update`. + `$ceratops-governance-lifecycle` action `propose-rules-update`. ### Workflow diff --git a/skills/ceratops-governance-lifecycle/SKILL.md b/skills/ceratops-governance-lifecycle/SKILL.md new file mode 100644 index 0000000..aaa879d --- /dev/null +++ b/skills/ceratops-governance-lifecycle/SKILL.md @@ -0,0 +1,98 @@ +--- +name: ceratops-governance-lifecycle +description: Route Ceratops governance work across prompt optimization, advisory skill optimization, regression-safe instruction updates, and cross-scope governance consistency audits. Use when Codex should rewrite a rough prompt without executing it, recommend exact changes to existing skills without applying them, diagnose and route approved instruction changes, or audit alignment across AGENTS files, automations, directly referenced helpers, and governance owners. +--- + +# Ceratops Governance Lifecycle + +## Goal + +Route governance work to the narrowest action reference while keeping prompt, +skill, instruction, and cross-scope audit decisions in one capability surface. + +## Context + +### Action References + +- Optimize a raw prompt without executing it: `references/optimize-prompt.md` +- Propose advisory-only improvements to existing skills: + `references/optimize-skill.md` +- Design or apply an approved regression-safe instruction change: + `references/propose-rules-update.md` +- Audit cross-scope governance consistency: + `references/governance-consistency-audit.md` + +### Inputs To Capture + +- Action intent and the target prompt, skill set, instruction stack, automation, + helper contract, policy surface, or governance scope. +- Expected deliverable, strict constraints, current source text, and available + regression or history evidence. +- Whether the task is advisory-only or authorizes an exact mutation. + +Infer missing inputs from current context and local sources before asking. + +## Constraints + +### Shared Action Rules + +- Preserve the target's intended meaning, purpose, constraints, and established + behavior except where the selected action explicitly permits an authorized + change. +- Use only the action-scoped work and evidence needed to satisfy the selected + action's completion gate; do not inspect or change unrelated surfaces. +- Do not mutate an artifact unless the selected action pre-authorizes that exact + mutation or the user authorizes the exact artifact and change. +- Keep prompt optimization and skill optimization advisory-only: do not execute + the underlying task or mutate artifacts. +- For skill optimization, rule updates, and governance audits, inspect companion + artifacts only when they govern the same behavior, evidence, or output + contract. + +### Skill-Specific Rules + +- For instruction updates, mutate only the exact artifacts the user authorized + after the proposal action accepts the candidate. + +### Boundaries + +- Use this skill for prompt optimization, advisory skill optimization, + instruction-system change design or approved application, and cross-scope + governance consistency audits. +- Use `$ceratops-skill-lifecycle` to create a skill or apply skill-source, + metadata, manifest, helper, validation, or documentation changes. +- Use the owning lifecycle audit for domain-specific repository, code, runtime, + GitHub, or skill-contract consistency. + +### Workflow + +#### 1. Classify the action + +- Use `optimize-prompt` when the deliverable is a rewritten prompt and the task + must not be executed. +- Use `optimize-skill` when the deliverable is an exact advisory skill-change + proposal and files must remain unchanged. +- Use `propose-rules-update` when the task changes or reviews durable rules, + instructions, automation prompts, skill rules, helper contracts, or their + interactions. +- Use `governance-consistency-audit` when the task checks alignment across + AGENTS files, automations, directly referenced helpers, and delegated + governance owners. + +#### 2. Close from action evidence + +- Follow only the selected action unless its explicit boundary requires a + lifecycle handoff. +- Match the final claim and output to the selected action's completion gate. + +## Done When + +### Completion Gate + +- The narrowest action was selected and its completion gate passed or its exact + blocker was reported. + +### Output Contract + +Return only the selected action's required output, unresolved blockers, and +important retained or unverified state. diff --git a/skills/ceratops-governance-lifecycle/agents/openai.yaml b/skills/ceratops-governance-lifecycle/agents/openai.yaml new file mode 100644 index 0000000..f54f49d --- /dev/null +++ b/skills/ceratops-governance-lifecycle/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Ceratops Governance Lifecycle" + short_description: "Optimize and audit Codex governance" + icon_small: "./assets/ceratops-logo-500.png" + icon_large: "./assets/ceratops-logo-500.png" + default_prompt: "Use $ceratops-governance-lifecycle to select and run the narrowest prompt, skill, instruction, or governance-audit action for this request: {{USER_REQUEST}}" diff --git a/skills/ceratops-prompt-optimizer/assets/ceratops-logo-500.png b/skills/ceratops-governance-lifecycle/assets/ceratops-logo-500.png similarity index 100% rename from skills/ceratops-prompt-optimizer/assets/ceratops-logo-500.png rename to skills/ceratops-governance-lifecycle/assets/ceratops-logo-500.png diff --git a/skills/ceratops-governance-lifecycle/references/governance-consistency-audit.md b/skills/ceratops-governance-lifecycle/references/governance-consistency-audit.md new file mode 100644 index 0000000..188fba7 --- /dev/null +++ b/skills/ceratops-governance-lifecycle/references/governance-consistency-audit.md @@ -0,0 +1,204 @@ +# Governance Consistency Audit Action + +## Goal + +Audit active Codex governance as one cross-scope system without duplicating +domain audits owned by other lifecycle actions. + +## Context + +### Deterministic Helper Contract + +- (D) From this skill directory, run `python scripts/governance-snapshot.py + --projects-root ` before deep-reading. +- (D) The helper exits zero with compact JSON using schema + `global-governance-consistency-audit/snapshot.v2`; nonzero execution or + unreadable JSON blocks the audit. +- Use helper output as cheap-pass evidence for automation metadata, AGENTS + classification, Git/worktree state, automation ignore coverage, directly + referenced paths, structured rule-graph facts, and overlong `(D)` rule + candidates. + +### Scope + +- Global `$CODEX_HOME/AGENTS.md` and every project-local or nested `AGENTS.md` + under the selected projects root, evaluated through its complete applicable + global-to-local instruction stack. +- Installed `$CODEX_HOME/automations/*/automation.toml`, automation repository + control files relevant to generated or runtime artifacts, and helpers directly + referenced by an in-scope control file. +- Cross-scope ownership, instruction interaction, prompt/helper alignment, + automation metadata, worktree policy, verification scope, and recurring + credit-cost controls. + +### Delegated Owners + +- Use `$ceratops-skill-lifecycle` `skills-contract-review` for skill-design + contracts, shared sections, runtime payloads, and standards upkeep. +- Use `$ceratops-skill-lifecycle` `skills-consistency-review` for active skill + catalog, source, installer, and managed-runtime consistency. +- Use `$ceratops-gh-repo-lifecycle` `contracts-review` for GitHub organization, + repository, code, PR, artifact, and release contracts. + +### Inputs To Capture + +- `$CODEX_HOME`, automation root, projects root, current automation caller, and + whether project-thread context or saved credit-usage evidence is available. +- Current official OpenAI model evidence only when model freshness or prompt + guidance is materially in question. +- Any task-specific exceptions that narrow findings or authorize a mutation. + +Infer roots from the active automation workspace and local environment; ask only +when the projects root remains ambiguous. + +## Constraints + +### Skill-Specific Rules + +- Keep the audit report-only. When invoked by the + `global-governance-consistency-audit` automation, updating a stale installed + automation `model` field to the latest officially verified model is the only + pre-authorized mutation; otherwise report it. +- Do not mutate AGENTS files, skills, helpers, repository configuration, Git + state, managed runtime, or external services during the audit. +- Compare every inspected automation prompt with each helper it directly + invokes; keep outcome, blocker, cleanup, alert, and memory paths aligned. +- Prefer local evidence and helper contracts. Use official OpenAI sources only + to identify the latest model or resolve a concrete prompt-guidance ambiguity. +- Do not report `xhigh` alone. Reasoning-effort findings require evidence of + avoidable cost, excessive breadth, weak stopping rules, or a better comparison + path. +- Review config- and prompt-level credit waste without inferring actual usage or + billing when saved local evidence is absent. +- Do not treat portable variables or relative paths alone as contradictions. +- Treat [rule-design.md](rule-design.md) as the only prose rule-graph standard; + helpers implement its closed syntax and audit prompts must not redefine it. +- Treat accurate `self` statuses as approved non-blocking debt that remains + reportable until the underlying rule is corrected. + +### Accepted Exceptions + +- Do not report `global-dev-tools-update` requiring two routine tables, setting + `report_required` to true, or monitoring Docker Hub MCP PRs. +- Do not report repeated instruction-enforcement or no-memory policy text in + installed automation prompts. +- Do not report the `ceratops-all-contracts-review` display identity or the + existing Sunday 07:00 schedule overlap among `ceratops-all-contracts-review`, + `credits-saving-analysis`, and `global-repo-health-consistency`. + +### Boundaries + +- Use this action for cross-scope governance alignment, not broad repository, + skill-contract, or GitHub contract audits. +- Do not run expensive tooling, broad live checks, large test suites, or web + research unless the cheap pass exposes a concrete decision that needs them. + +## Workflow + +### 1. Build the cheap-pass inventory + +- Run the deterministic helper once and inventory files, schedules, model and + effort, workspaces, prompt names, helper references, memory/alert contracts, + Git/worktree state, repeated text, rule IDs, metadata, statuses, histories, + relation edges and cycles, stack interaction, and `(D)` rule-brevity + candidates. +- If history contains obsolete references, invalid fields, or exceeds its + deterministic limit, route the exact cleanup through `propose-rules-update` + before any other audit work. Apply it when authorized; otherwise stop and + report only the cleanup blocker. +- Treat helper blockers, missing roots, parse failures, and inaccessible + governing sources as explicit audit limits. + +### 2. Select evidence clusters + +- Group evidence by topic and deep-read only clusters with plausible + contradiction, duplication, stale state, recurring credit waste, ambiguous + ownership, classification drift, or current model/prompt-guidance drift. +- Do not invoke delegated domain audits unless the cheap pass exposes a concrete + cross-scope dependency that this action cannot classify alone. + +### 3. Review governance consistency + +- Accept deterministic findings for rule syntax, IDs, metadata placement, + targets, duplicates, size status, cycles, history references, and stack + legality. Review each rule body once for semantic rule form, enumeration, and + `self` accuracy without performing pairwise comparison. +- Use focused semantic review to detect missing, unnecessary, mistyped, + misdirected, or incompatible relations and to assess `list-heavy`, + `overlaps`, `conflicts`, and non-override cycles. Keep unresolved review edges + as findings. Preserve the `CLOSE-01` and `REWORK-05` conflict until a manual + decision changes it. +- Build relation candidates from sections, explicit ID references, semantic + similarity, deterministic findings, and direct neighbors. Deep-read only + those candidate pairs; do not compare every possible rule pair. +- Check instruction bullets and explicit class labels against declared or + inherited force definitions; report conflicting closure behavior as + instruction-classification drift. +- Check contradictions, duplicated broader rules, stale placeholders, + wrong-scope rules, local files that should be delta-only, missing numbered + response-shape rules, and rule shapes that should be split, merged, shortened, + or moved into deterministic helpers. +- Check automation prompt/helper alignment, stale metadata and identities, + overlapping schedules or responsibilities, avoidable inbox items, memory and + alert contracts, noisy output, broad refreshes, unnecessary live checks, and + weak evidence or stopping budgets. +- Check worktree-root placement, automation ignore coverage, duplicated + worktree rules, and task work merged into local `release/*` branches without + explicit staging or preview intent. +- Check claim-scope and verification-scope drift, including artifact-specific + checks or end-to-end validation required beyond changed artifacts and claims. +- Review automation prompts against current GPT guidance for outcome-first + contracts, success criteria, evidence budgets, missing-evidence behavior, + stopping conditions, output shape, and reasoning effort. + +### 4. Preserve known regression cases + +- Keep analogous cases in scope for duplicated update automations, unconditional + governing-file reopens, AGENTS-only audits, missing automation ignore coverage, + artifact-closure overreach, recurring audits that spend credits detecting + credit waste, and prompt stacks stale against current model guidance. + +### 5. Apply the model-only exception + +- When the automation-specific mutation exception applies, verify the latest + model from an official source, update only stale automation `model` fields, + and record each automation id with its old and new values. +- Do not apply any recommendation beyond that exact exception. + +### 6. Close from aligned evidence + +- Recompare the action, automation prompt, and bundled helper before finishing. +- Route each confirmed rule or relation defect through this lifecycle's + `propose-rules-update` action for the smallest exact advisory correction; do + not apply it during the audit. +- Rank findings by severity and config/prompt-level credit recommendations by + impact and safety. +- Classify inaccessible thread context, absent usage evidence, and skipped + delegated audits as explicit limits rather than silently weakening the claim. + +## Done When + +### Completion Gate + +- The helper result, inspected control files, directly referenced helpers, + accepted exceptions, delegated ownership, mutation boundary, and report agree. +- Every model field changed under the exception has current official evidence + and old/new values. +- Important unverified areas and unavailable evidence are explicit. + +### Output Contract + +Report only: + +- findings ordered by severity, with conflicting artifacts, exact inconsistency, + risk, and smallest credible change +- approved `self` debt grouped by status and rule ID without treating it as a + blocking defect +- a `Recommendations` section covering every finding and confirmed config- or + prompt-level credit opportunity, or `None` +- automation model fields changed, with automation id and old/new model +- important areas checked and consistent +- important unverified areas and unavailable evidence + +If no material finding exists, keep the report short and state the checked scope +and main residual limits. diff --git a/skills/ceratops-prompt-optimizer/SKILL.md b/skills/ceratops-governance-lifecycle/references/optimize-prompt.md similarity index 81% rename from skills/ceratops-prompt-optimizer/SKILL.md rename to skills/ceratops-governance-lifecycle/references/optimize-prompt.md index c01a7ae..7cc9c4b 100644 --- a/skills/ceratops-prompt-optimizer/SKILL.md +++ b/skills/ceratops-governance-lifecycle/references/optimize-prompt.md @@ -1,9 +1,4 @@ ---- -name: ceratops-prompt-optimizer -description: Transform raw, simple, vague, or under-specified prompts into clearer structured prompts without changing intent or answering the prompt. ---- - -# Ceratops Prompt Optimizer +# Optimize Prompt Action ## Goal @@ -27,7 +22,6 @@ Ask for the missing raw prompt only when none was provided. ### Skill-Specific Rules -- Do not answer the raw prompt; rewrite it. - Do not change the target task, intent, audience, output target, terminology, placeholders, variables, examples, or strict constraints. - Add only assumptions or defaults that reduce execution ambiguity. @@ -42,12 +36,12 @@ Ask for the missing raw prompt only when none was provided. ### Boundaries -- Use this skill for prompt rewrites, prompt templates, and prompt-clarity +- Use this action for prompt rewrites, prompt templates, and prompt-clarity improvements. -- If the requested change is to a durable rule or instruction file, use - `$ceratops-propose-rules-update`. -- If the requested change is to a skill workflow, use - `$ceratops-skill-optimize`. +- If the requested change is to a durable rule or instruction file, return to + the parent skill and select `propose-rules-update`. +- If the requested change is to a skill workflow, return to the parent skill + and select `optimize-skill`. ### Workflow diff --git a/skills/ceratops-skill-optimize/SKILL.md b/skills/ceratops-governance-lifecycle/references/optimize-skill.md similarity index 79% rename from skills/ceratops-skill-optimize/SKILL.md rename to skills/ceratops-governance-lifecycle/references/optimize-skill.md index ca78851..310817e 100644 --- a/skills/ceratops-skill-optimize/SKILL.md +++ b/skills/ceratops-governance-lifecycle/references/optimize-skill.md @@ -1,9 +1,4 @@ ---- -name: ceratops-skill-optimize -description: Propose advisory-only improvements for one existing Codex or Ceratops skill by default, or multiple skills when explicitly requested, including multi-action skill text, action references, triggers, metadata, runtime payload declarations, validators, docs, workflow constraints, output contracts, and done criteria. Use when Codex should recommend exact skill changes without applying them. ---- - -# Ceratops Skill Optimizer +# Optimize Skill Action ## Goal @@ -33,28 +28,23 @@ target set is available. ### Skill-Specific Rules -- Do not apply edits or mutate files. - Review one skill by default. Review multiple or all skills only when the user explicitly requests that broader target set. -- Preserve the skill's existing intent unless the user explicitly asks to change - behavior. -- Prefer targeted proposals over whole-skill rewrites. - Put trigger information in YAML frontmatter descriptions rather than only in the body. - Keep `SKILL.md` focused on execution rules that are useful after the skill has already triggered. - Include a role only when it changes how Codex should judge tradeoffs, inspect inputs, choose tools, or decide completion. -- Inspect companion artifacts only when they exist and govern trigger behavior, - runtime packaging, helper usage, validation, docs, or user-visible output for - the target skill. - For multi-action skills, inspect the `SKILL.md` and action reference files; inspect metadata, runtime payload declarations, validators, and docs only when they govern the action surface. - When companion artifacts govern the same behavior, include the aligned companion update or state why it is intentionally retained. -- Remove or merge duplicate responsibilities and avoid overlong, brittle, vague, - contradictory, or purely stylistic changes. +- Merge semantically shared behavior even when wording differs; for multi-action + skills, place the common invariant in the parent `SKILL.md` and retain only + action-specific deltas in references. +- Avoid overlong, brittle, vague, contradictory, or purely stylistic changes. - Prefer deterministic scripts or helpers for repeatable procedural behavior only when text instructions are insufficient. - Do not add auxiliary documentation, README files, examples, or unrelated @@ -65,14 +55,14 @@ target set is available. ### Boundaries -- Use this skill for advisory skill optimization and exact proposed skill text +- Use this action for advisory skill optimization and exact proposed skill text changes. - If the user asks to apply changes to Ceratops skills, use `$ceratops-skill-lifecycle` with the `update` action. - If the user asks to create a new skill, use `$ceratops-skill-lifecycle` with the `create` action. -- If the requested change is one durable rule, use - `$ceratops-propose-rules-update`. +- If the requested change is one durable rule, return to the parent skill and + select `propose-rules-update`. ### Workflow diff --git a/skills/ceratops-governance-lifecycle/references/propose-rules-update.md b/skills/ceratops-governance-lifecycle/references/propose-rules-update.md new file mode 100644 index 0000000..4c1f701 --- /dev/null +++ b/skills/ceratops-governance-lifecycle/references/propose-rules-update.md @@ -0,0 +1,90 @@ +# Propose Rules Update Action + +## Goal + +Every confirmed failure must change the controlling instruction surface or its +deterministic enforcement. + +Read [rule-design.md](rule-design.md) before drafting. + +## Constraints + +### Boundaries + +Use this action for instruction-system changes. Route general prompt rewrites +through the parent skill's `optimize-prompt` action; answer diagnosis-only +requests without forcing a rule change. +Route approved skill-source mutations through `$ceratops-skill-lifecycle` +`update` after accepting the proposal. + +## Workflow + +1. Reconstruct the failed decision from current evidence. Identify the active + instruction stack, chosen behavior, and required behavior without assuming a + relevant rule, single cause, or owning artifact exists. +2. Inspect exact current text from every involved source. For global and local + instructions, determine scope and precedence before evaluating interaction. +3. Resolve the current rule graph and structured history before drafting. For + global rules, check + `$CODEX_HOME/AGENTS.history.json`; for local rules, check + `AGENTS.history.json` beside their `AGENTS.md`. From this skill directory, + run `python scripts/rule_history.py lookup --history --rules + ID...`, repeating both options in effective global-to-local order for + an interacting instruction stack. + The helper must parse the canonical metadata syntax and select every direct + directional or review-edge neighbor. Use the compact default for graph + selection and add `--full` only when selected causal or regression evidence + is needed. Obsolete references, invalid fields, or an over-limit history + block the remaining proposal workflow: apply the smallest authorized cleanup + first, rerun lookup, and only then continue. If history does not exist, use + targeted source history and state that recorded behavioral history was + unavailable. +4. Compare a local correction with a structural or non-rule correction. Select + by prevention of the failure, regression safety, behavioral scope, and + complexity; textual minimality does not win automatically. +5. Draft under the rule-design contract. Resolve structural defects and every + affected semantic review state inside the candidate, and identify every + intentional behavior change. +6. Replay the current failure and relevant recorded history. Reject a candidate + that leaves the failed decision possible or regresses a recorded outcome. +7. Report the selected correction, material alternative, regression result, + and uncertainty. + +## Applying an approved change + +Clean history before applying any other approved rule change. Keep decisions +that still constrain current behavior; delete obsolete references, retracted or +overridden outcomes, and entries with no regression value. Replace renamed IDs +when their rationale remains active. Store only the decision-only fields defined +by the rule-design contract, and compact history when its deterministic limit is +reached. Name every deletion in the proposal so approval covers the exact +durable history change. Run the source's rule checker; its history binding must +reject unrecorded rule changes, obsolete references, invalid fields, and +over-limit history. + +## Iterative optimization + +Use `scripts/iteration_controller.py` when repeated optimization is requested or +one-pass comparison remains materially uncertain. The controller owns numbering, +artifact hashes, and stopping state; it does not judge semantic quality. + +For each issued iteration, produce one candidate and one assessment. Compare it +with the original, champion, and regression evidence, then submit `improved` or +`no-improvement`. Improvement resets the streak. Stop when the controller +reports 200 iterations or ten consecutive non-improvements. Never claim state +the controller did not report. + +## Done When + +### Completion Gate + +A proposal is complete only when it prevents the current recorded failure, +leaves the rule graph structurally valid, retains only applicable regression +history within its size limits, and is better than the current state and +material alternative. +Otherwise change the intervention or report the unresolved decision point. + +### Output Contract + +Report only the selected exact change, its decision and regression evidence, +and unresolved impact. diff --git a/skills/ceratops-governance-lifecycle/references/rule-design.md b/skills/ceratops-governance-lifecycle/references/rule-design.md new file mode 100644 index 0000000..8cb202f --- /dev/null +++ b/skills/ceratops-governance-lifecycle/references/rule-design.md @@ -0,0 +1,109 @@ +# Rule Design Contract + +## Behavioral objective + +Minimize behavioral delta: change only decisions needed to prevent the failure +while preserving recorded correct behavior. Word count and diff size are +secondary evidence. + +## Rule form + +Every instruction rule in an applicable global or local `AGENTS.md` must use +the structured syntax parsed by `scripts/rule_graph.py`. A rule starts with one +globally stack-unique ID, uses the parser's canonical metadata syntax, and may +reference only IDs available in its effective global-to-local instruction +stack. Nested local files extend the stack of their ancestors. + +Use one sentence when one condition determines one required behavior: + +```text +[RULE-ID] When , require . +``` + +Use the strongest unambiguous wording that accurately expresses the intended +rule. + +Add an exception or relationship only when it changes the decision. Keep +scenarios outside governing instructions and use them as regression evidence. + +Place metadata immediately after the rule body. Use only the parser's closed +metadata-key and value sets, canonical order, indentation, separators, and ID +grammar. Do not accept prose or backticked relation syntax as an alternative. + +## Size and enumeration + +Keep a canonical rule within six 80-character lines. Split it when independent +conditions or actions can be named without weakening their interaction. + +A normative enumeration may name at most three members. For a larger set, +state the common deciding property; when exact membership is operationally +required, store the closed set in deterministic data or a helper. + +`self: exceeds-limit` and `self: list-heavy` are approved non-blocking debt, +not structural failures. Their presence must match the current rule, remain an +audit finding, and be removed when the underlying debt is fixed. Size status is +deterministic; enumeration status requires focused semantic review. + +## Relationships + +Use `limits` when an independent guardrail narrows another rule and `overrides` +when a specific rule wins incompatible application. Do not introduce an +unresolved interaction in a candidate. + +These relations are directional from the declaring rule to the target. Detect +every directed cycle. An `overrides` cycle is structurally invalid; `limits` +and mixed cycles require focused semantic review. + +Treat `overlaps` and `conflicts` as symmetric review edges declared once. They +remain unresolved findings until semantic review selects a coherent outcome; +do not change involved behavior without resolving the edge or recording the +manual decision to retain it. Keep the current `CLOSE-01` and `REWORK-05` +conflict as a finding until that decision is made. + +Merge coextensive guidance into one rule. Keep independently reusable behavior +separate and express only the directional relationship needed to interpret it. +When evidence cannot select one coherent result, present the exact decision +point before treating a candidate as complete. + +Use deterministic checks for syntax, targets, duplicates, placement, statuses, +size, cycles, and stack legality. Use focused semantic review for missing, +unnecessary, mistyped, misdirected, incompatible, overlap, and conflict edges. + +## Scope interaction + +Treat global instructions as the baseline for local instructions. A local rule +may add project behavior or narrow discretion the global rules leave open. It +may override a global rule only when that rule explicitly delegates the +decision to local scope. Otherwise repair the local rule or propose a global +delegation instead of retaining a contradiction. + +A global rule must not depend on a local rule. A local relation may target only +the global rules and ancestor-local rules in its effective stack. Validate IDs, +relations, and compatibility across the complete stack, not each file alone. + +Store local history beside its local rule source and query every applicable +history in a cross-scope change. Revalidate current global and ancestor-local +rules directly; do not store dates, hashes, or source snapshots in history. + +Use a JSON history object with `version: 2` and an `entries` array. Each entry +uses the decision-only schema owned by `scripts/rule_graph.py`: it names current +rule IDs and states the decision, why it exists, and its regression boundary. +History is regression memory, not an audit log: retain every decision and +rationale that still constrains current behavior, and delete obsolete +references, retracted or overridden outcomes, and entries with no remaining +regression value. Resolve obsolete history before other governance work; do not +continue with a report-only finding. + +When a history file exceeds 8 KiB or 20 entries, compact it before other +governance work. Merge decisions only when their behavior, rationale, and +regression boundary remain explicit; the compacted file must meet both limits. + +## Acceptance + +State the required invariant. Add a negative boundary only when it excludes a +distinct plausible failure not excluded by the invariant. Apply broad override +policy without repeating local user-override clauses. + +Evaluate both local and structural intervention. Accept a candidate only when +it prevents the current failure and relevant recorded failures without +regressing recorded correct outcomes unless explicitly superseded. diff --git a/skills/ceratops-governance-lifecycle/scripts/governance-snapshot.py b/skills/ceratops-governance-lifecycle/scripts/governance-snapshot.py new file mode 100644 index 0000000..9228cd6 --- /dev/null +++ b/skills/ceratops-governance-lifecycle/scripts/governance-snapshot.py @@ -0,0 +1,727 @@ +#!/usr/bin/env python3 +"""Build the compact deterministic inventory for governance consistency audits. + +The helper is read-only. It inventories only declared governance surfaces and +emits one JSON document consumed by the governance lifecycle audit action. +""" + +from __future__ import annotations + +import argparse +import json +import os +import pathlib +import re +import subprocess +import tomllib +from collections import Counter +from datetime import datetime, timezone +from typing import Any, Iterable, cast + +from rule_graph import ( + history_limit_findings, + load_history_references, + load_history_source, + parse_rule_source, + rule_source_summary, + validate_rule_stack, +) + + +D_RULE_CHAR_LIMIT = 220 +D_RULE_RE = re.compile(r"^\s*-\s+\(D\)\s+(.+)$") +ALL_BULLETS_FORCE_RE = re.compile( + r"All instruction bullets in this file are mandatory,\s*blocking,\s*and\s*closure-gating", + re.IGNORECASE, +) +D_FORCE_PRESERVATION_RE = re.compile( + r"The\s+`?\(D\)`?\s+label\s+marks\b.*?\bdoes not change the mandatory status", + re.IGNORECASE | re.DOTALL, +) +MEMORY_TERM_RE = re.compile(r"\bmemory(?:\.md)?\b", re.IGNORECASE) +MEMORY_FORBIDDEN_RE = re.compile( + r"\b(?:do\s+not|must\s+not)\b[^\n.]*\b(?:read|use|create|append|update|write|rely\s+on)\b[^\n.]*\bmemory(?:\.md)?\b" + r"|\bdo\s+not\s+use\s+automation\s+memory\b", + re.IGNORECASE, +) +MEMORY_REQUIRED_RE = re.compile( + r"\b(?:must|always|required\s+to)\b[^\n.]*\b(?:read|use|create|append|update|write)\b[^\n.]*\bmemory(?:\.md)?\b" + r"|\b(?:read|create|append|update|write)\b[^\n.]*\bmemory(?:\.md)?\b", + re.IGNORECASE, +) +REFERENCE_RE = re.compile( + r"(?:[`\"'](?P[^`\"'\r\n]+\.(?:py|ps1|json|toml|md)|[^`\"'\r\n]*\.gitignore)[`\"']" + r"|(?P[^\s`\"']+\.(?:py|ps1|json|toml|md)|[^\s`\"']*\.gitignore))", + re.IGNORECASE, +) + + +def utc_now() -> str: + return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + + +def default_codex_home() -> pathlib.Path: + return pathlib.Path(os.environ.get("CODEX_HOME") or pathlib.Path.home() / ".codex") + + +def read_text(path: pathlib.Path) -> str: + return path.read_text(encoding="utf-8", errors="replace") + + +def maybe_rel(path: pathlib.Path, base: pathlib.Path) -> str: + try: + return path.relative_to(base).as_posix() + except ValueError: + return path.as_posix() + + +def reference_inventory(prompt: str) -> dict[str, list[str]]: + """Classify referenced paths so helpers never include memory or result artifacts.""" + helpers: set[str] = set() + memory: set[str] = set() + artifacts: set[str] = set() + controls: set[str] = set() + + for match in REFERENCE_RE.finditer(prompt): + reference = str(match.group("quoted") or match.group("bare")).rstrip(".,;:)]}") + reference = re.sub( + r"^\$env:CODEX_HOME", "$CODEX_HOME", reference, flags=re.IGNORECASE + ).replace("\\", "/") + lowered = reference.lower() + if lowered.endswith((".py", ".ps1")): + helpers.add(reference) + elif lowered.endswith("memory.md"): + memory.add(reference) + elif lowered.endswith(("automation.toml", "agents.md", ".gitignore")): + controls.add(reference) + else: + artifacts.add(reference) + + return { + "helper_refs": sorted(helpers), + "memory_refs": sorted(memory), + "artifact_refs": sorted(artifacts), + "control_refs": sorted(controls), + } + + +def parse_automation(path: pathlib.Path, root: pathlib.Path) -> dict[str, object]: + data = tomllib.loads(read_text(path)) + prompt = str(data.get("prompt", "")) + references = reference_inventory(prompt) + memory_contract = classify_memory_contract(prompt) + inbox_contract = classify_inbox_contract(prompt) + return { + "path": maybe_rel(path, root), + "id": data.get("id"), + "name": data.get("name"), + "status": data.get("status"), + "schedule": data.get("rrule"), + "model": data.get("model"), + "reasoning_effort": data.get("reasoning_effort"), + "cwds": data.get("cwds", []), + "prompt_chars": len(prompt), + **references, + "memory_contract": memory_contract["contract"], + "memory_mention_count": memory_contract["mention_count"], + "memory_incidental_mentions": memory_contract["incidental_mentions"], + "inbox_contract": inbox_contract, + } + + +def classify_memory_contract(prompt: str) -> dict[str, object]: + mention_count = len(MEMORY_TERM_RE.findall(prompt)) + if mention_count == 0: + return {"contract": "not_mentioned", "mention_count": 0, "incidental_mentions": 0} + + forbidden = False + required = False + incidental_mentions = 0 + for line in prompt.splitlines(): + if not MEMORY_TERM_RE.search(line): + continue + if MEMORY_FORBIDDEN_RE.search(line): + forbidden = True + continue + if MEMORY_REQUIRED_RE.search(line): + required = True + continue + incidental_mentions += len(MEMORY_TERM_RE.findall(line)) + + if forbidden and required: + contract = "conflicting" + elif forbidden: + contract = "forbidden" + elif required: + contract = "required" + else: + contract = "not_mentioned" + return { + "contract": contract, + "mention_count": mention_count, + "incidental_mentions": incidental_mentions, + } + + +def classify_inbox_contract(prompt: str) -> str: + lowered = prompt.lower() + if "::inbox-item" not in prompt and "inbox item" not in lowered: + return "not_mentioned" + if re.search(r"(?:always|exactly one)[^\n.]*inbox item", lowered): + return "required" + if "do not emit an inbox item" in lowered and not re.search(r"(?:when|if)[^\n.]*inbox item", lowered): + return "forbidden" + if re.search(r"(?:when|if|only when)[^\n.]*inbox item", lowered) or "inbox_required" in prompt: + return "conditional" + if "alert state" in lowered and "inbox item" in lowered: + return "conditional" + return "mentioned" + + +def automations_inventory(automation_root: pathlib.Path) -> dict[str, object]: + paths = sorted(automation_root.glob("*/automation.toml")) + items = [parse_automation(path, automation_root) for path in paths] + schedules = Counter(str(item["schedule"]) for item in items) + models = Counter(str(item["model"]) for item in items) + return { + "root": str(automation_root), + "count": len(items), + "items": items, + "duplicate_schedules": {key: value for key, value in schedules.items() if value > 1}, + "models": dict(models), + } + + +def iter_project_agents(projects_root: pathlib.Path) -> Iterable[pathlib.Path]: + """Yield every project AGENTS file from root or nested repository paths.""" + if projects_root.exists(): + local_paths = { + path.resolve() + for path in projects_root.rglob("AGENTS.md") + if ".git" not in path.parts + } + yield from sorted(local_paths) + + +def iter_agents(projects_root: pathlib.Path, codex_home: pathlib.Path) -> Iterable[pathlib.Path]: + global_agents = codex_home / "AGENTS.md" + if global_agents.exists(): + yield global_agents + yield from ( + path + for path in iter_project_agents(projects_root) + if path != global_agents.resolve() + ) + + +def run_git(repo: pathlib.Path, *args: str) -> tuple[str | None, str | None]: + """Run a bounded read-only Git probe and return compact output or an error.""" + try: + result = subprocess.run( + ["git", "-C", str(repo), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=10, + check=False, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + return None, str(exc) + if result.returncode != 0: + return None, (result.stderr or result.stdout).strip()[:240] + return result.stdout.rstrip("\r\n"), None + + +def parse_worktrees(output: str) -> list[dict[str, object]]: + """Reduce `git worktree list --porcelain` to stable branch/path evidence.""" + worktrees: list[dict[str, object]] = [] + current: dict[str, object] = {} + for line in [*output.splitlines(), ""]: + if not line: + if current: + worktrees.append(current) + current = {} + continue + key, _, value = line.partition(" ") + if key == "worktree": + current["path"] = str(pathlib.Path(value).resolve()) + elif key == "HEAD": + current["head"] = value + elif key == "branch": + current["branch"] = value.removeprefix("refs/heads/") + elif key in {"bare", "detached", "locked", "prunable"}: + current[key] = True + return worktrees + + +def is_within(path: pathlib.Path, root: pathlib.Path) -> bool: + """Return whether a resolved path stays within the expected worktree root.""" + try: + path.resolve().relative_to(root.resolve()) + return True + except ValueError: + return False + + +def repo_git_state(repo: pathlib.Path, kind: str) -> dict[str, object]: + """Inventory branch, dirty state, and secondary-worktree placement without mutation.""" + top_level, top_error = run_git(repo, "rev-parse", "--show-toplevel") + if top_error or not top_level: + return { + "kind": kind, + "path": str(repo.resolve()), + "is_git_repo": False, + "error": top_error, + } + + root = pathlib.Path(top_level).resolve() + branch, branch_error = run_git(root, "branch", "--show-current") + head, head_error = run_git(root, "rev-parse", "HEAD") + upstream, upstream_error = run_git( + root, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}" + ) + status, status_error = run_git(root, "status", "--porcelain=v1", "--untracked-files=normal") + worktree_output, worktree_error = run_git(root, "worktree", "list", "--porcelain") + + status_lines = status.splitlines() if status is not None else [] + changed_paths = [line[3:] for line in status_lines if len(line) > 3] + worktrees = parse_worktrees(worktree_output or "") + primary_root = ( + pathlib.Path(str(worktrees[0]["path"])).resolve() if worktrees else root + ) + expected_root = primary_root.parent / "worktrees" / primary_root.name + misplaced = [ + str(item["path"]) + for item in worktrees + if pathlib.Path(str(item["path"])).resolve() != primary_root + and not is_within(pathlib.Path(str(item["path"])), expected_root) + ] + errors = [ + error + for error in (branch_error, head_error, status_error, worktree_error) + if error + ] + if upstream_error and "no upstream configured" not in upstream_error.lower(): + errors.append(upstream_error) + + return { + "kind": kind, + "path": str(root), + "primary_worktree": str(primary_root), + "is_git_repo": True, + "branch": branch, + "head": head, + "upstream": upstream, + "active_release_branch": bool(branch and branch.startswith("release/")), + "dirty": bool(status_lines), + "changed_count": len(status_lines), + "changed_paths": changed_paths[:20], + "changed_paths_truncated": len(changed_paths) > 20, + "expected_secondary_worktree_root": str(expected_root.resolve()), + "worktrees": worktrees, + "misplaced_worktrees": misplaced, + "errors": errors, + } + + +def git_inventory(automation_root: pathlib.Path, projects_root: pathlib.Path) -> dict[str, object]: + """Collect compact Git/worktree state for the automation repo and AGENTS projects.""" + candidates = [ + ("automation", automation_root), + *(("project", path.parent) for path in iter_project_agents(projects_root)), + ] + targets: list[tuple[str, pathlib.Path]] = [] + seen_roots: set[pathlib.Path] = set() + for kind, candidate in candidates: + top_level, _ = run_git(candidate, "rev-parse", "--show-toplevel") + root = pathlib.Path(top_level).resolve() if top_level else candidate.resolve() + if root in seen_roots: + continue + seen_roots.add(root) + targets.append((kind, root)) + items = [repo_git_state(path, kind) for kind, path in targets] + return { + "count": len(items), + "dirty_count": sum(1 for item in items if item.get("dirty")), + "misplaced_worktree_count": sum( + len(value) + for item in items + if isinstance((value := item.get("misplaced_worktrees")), list) + ), + "items": items, + } + + +def classify_force_definitions(text: str) -> dict[str, bool]: + return { + "all_instruction_bullets_mandatory_blocking": bool(ALL_BULLETS_FORCE_RE.search(text)), + "d_label_preserves_force": bool(D_FORCE_PRESERVATION_RE.search(text)), + } + + +def _path_is_within(path: pathlib.Path, parent: pathlib.Path) -> bool: + try: + path.resolve().relative_to(parent.resolve()) + return True + except ValueError: + return False + + +def _history_inventory( + path: pathlib.Path, + current_rule_ids: set[str], + owned_rule_ids: set[str], +) -> dict[str, object]: + history_path = path.with_name("AGENTS.history.json") + if not history_path.exists(): + return { + "path": str(history_path), + "exists": False, + "findings": [{"code": "missing_rule_history"}], + } + try: + entries = load_history_source(history_path) + except (OSError, ValueError, json.JSONDecodeError) as error: + return { + "path": str(history_path), + "exists": True, + "findings": [ + {"code": "invalid_rule_history", "detail": str(error)} + ], + } + findings = [ + *load_history_references(entries, current_rule_ids), + *history_limit_findings(history_path, entries), + ] + for entry_index, entry in enumerate(entries): + rules = cast(list[object], entry["rules"]) + if "*" in rules: + continue + owned_references = { + str(value) for value in rules if value != "*" + } + if owned_rule_ids and not owned_references.intersection(owned_rule_ids): + findings.append( + { + "code": "history_entry_without_owned_rule", + "entry": entry_index, + } + ) + return { + "path": str(history_path), + "exists": True, + "entry_count": len(entries), + "findings": findings, + } + + +def _touches_rules(item: dict[str, object], rule_ids: set[str]) -> bool: + values: set[str] = set() + for key in ("rule_id", "source", "target"): + value = item.get(key) + if isinstance(value, str): + values.add(value) + members = item.get("rules") + if isinstance(members, list): + values.update(str(value) for value in members) + return bool(values.intersection(rule_ids)) + + +def _compact_edges(edges: list[dict[str, object]]) -> list[dict[str, object]]: + compact: list[dict[str, object]] = [] + for edge in edges: + item: dict[str, object] = { + key: edge[key] for key in ("source", "relation", "target") + } + if edge.get("source_file") != edge.get("target_file"): + item["cross_scope"] = True + compact.append(item) + return compact + + +def agents_rule_graph_inventory( + projects_root: pathlib.Path, codex_home: pathlib.Path +) -> dict[str, object]: + """Validate every global-to-local AGENTS stack against one graph parser.""" + paths = list(iter_agents(projects_root, codex_home)) + global_path = (codex_home / "AGENTS.md").resolve() + parsed = {path.resolve(): parse_rule_source(path) for path in paths} + local_paths = sorted(path for path in parsed if path != global_path) + file_items: list[dict[str, Any]] = [] + stacks: list[dict[str, Any]] = [] + + if global_path in parsed: + global_source = parsed[global_path] + global_ids = {record.rule_id for record in global_source.records} + global_summary = rule_source_summary(global_source) + global_summary["scope"] = "global" + global_summary["history"] = _history_inventory( + global_path, global_ids, global_ids + ) + file_items.append(global_summary) + global_validation = validate_rule_stack( + [global_source], global_source=global_source.source + ) + global_validation["edges"] = _compact_edges(global_validation["edges"]) + global_validation["path"] = str(global_path) + global_validation["scope"] = "global" + stacks.append(global_validation) + + for path in local_paths: + ancestor_paths = sorted( + ( + candidate + for candidate in local_paths + if _path_is_within(path.parent, candidate.parent) + ), + key=lambda candidate: len(candidate.parts), + ) + stack_sources = [parsed[candidate] for candidate in ancestor_paths] + if global_path in parsed: + stack_sources.insert(0, parsed[global_path]) + current_ids = { + record.rule_id + for source in stack_sources + for record in source.records + } + local_summary = rule_source_summary(parsed[path]) + local_summary["scope"] = "local" + local_ids = {record.rule_id for record in parsed[path].records} + local_summary["history"] = _history_inventory( + path, current_ids, local_ids + ) + file_items.append(local_summary) + + validation = validate_rule_stack( + stack_sources, + global_source=parsed[global_path].source + if global_path in parsed + else "", + ) + validation["edges"] = [ + edge + for edge in validation["edges"] + if edge.get("source_file") == parsed[path].source + ] + validation["relation_counts"] = dict( + sorted( + Counter(str(edge["relation"]) for edge in validation["edges"]).items() + ) + ) + validation["rule_count"] = len(local_ids) + validation["edges"] = _compact_edges(validation["edges"]) + validation["cycles"] = [ + cycle + for cycle in validation["cycles"] + if _touches_rules(cycle, local_ids) + ] + validation["findings"] = [ + finding + for finding in validation["findings"] + if _touches_rules(finding, local_ids) + ] + validation["semantic_reviews"] = [ + review + for review in validation["semantic_reviews"] + if _touches_rules(review, local_ids) + ] + validation["path"] = str(path) + validation["scope"] = "local-stack-delta" + validation["stack_paths"] = [source.source for source in stack_sources] + stacks.append(validation) + + structural_finding_count = sum( + int(item["findings"]["count"]) + len(item["history"]["findings"]) + for item in file_items + ) + sum(len(stack["findings"]) for stack in stacks) + return { + "standard": "references/rule-design.md", + "file_count": len(file_items), + "files": file_items, + "stacks": stacks, + "structural_finding_count": structural_finding_count, + "approved_debt_count": sum( + int(item["approved_debt"]["count"]) for item in file_items + ), + "semantic_review_count": sum( + len(stack["semantic_reviews"]) for stack in stacks + ) + + sum(int(item["semantic_reviews"]["count"]) for item in file_items), + } + + +def agents_inventory(projects_root: pathlib.Path, codex_home: pathlib.Path) -> dict[str, object]: + items = [] + repeated_lines: Counter[str] = Counter() + global_path = codex_home / "AGENTS.md" + global_force = classify_force_definitions(read_text(global_path)) if global_path.exists() else { + "all_instruction_bullets_mandatory_blocking": False, + "d_label_preserves_force": False, + } + for path in iter_agents(projects_root, codex_home): + text = read_text(path) + lines = text.splitlines() + declared_force = classify_force_definitions(text) + effective_force = { + key: declared_force[key] or global_force[key] + for key in declared_force + } + label_counts: Counter[str] = Counter() + instruction_bullets = 0 + for raw_line in lines: + if raw_line.startswith("- "): + line = raw_line.strip() + instruction_bullets += 1 + repeated_lines[line] += 1 + d_rule_match = D_RULE_RE.match(line) + if d_rule_match: + label_counts["d"] += 1 + else: + label_match = re.match(r"^-\s+([A-Za-z]+):", line) + if label_match: + label_counts[label_match.group(1).lower()] += 1 + classification_complete = ( + effective_force["all_instruction_bullets_mandatory_blocking"] + and (label_counts.get("d", 0) == 0 or effective_force["d_label_preserves_force"]) + ) + items.append( + { + "path": str(path), + "instruction_bullets": instruction_bullets, + "explicit_class_labels": dict(sorted(label_counts.items())), + "force_definitions": { + "declared": declared_force, + "effective": effective_force, + }, + "classification_complete": classification_complete, + "classification_review_required": not classification_complete, + "blocking": label_counts.get("blocking", 0), + "mandatory": label_counts.get("mandatory", 0), + "metadata": label_counts.get("metadata", 0), + } + ) + return { + "count": len(items), + "items": items, + "repeated_rule_lines": [ + {"line": line, "count": count} + for line, count in repeated_lines.most_common(30) + if count > 1 + ], + } + + +def gitignore_inventory(automation_root: pathlib.Path) -> dict[str, object]: + path = automation_root / ".gitignore" + text = read_text(path) if path.exists() else "" + expected = ( + ".run-jitter-salt", + "*.bck", + "*/memory.md", + "*/downloads/", + "*/__pycache__/", + "pc-cleanup/deleted-files/", + "global-dependabot-alert-review/dependabot-org-queue-snapshot.json", + "global-dependabot-alert-review/local-checkout-sync.json", + "worktrees/", + ) + return { + "path": str(path), + "exists": path.exists(), + "missing_expected_entries": [entry for entry in expected if entry not in text], + } + + +def collect_overlong_d_rules(text: str, source: str, source_kind: str) -> list[dict[str, object]]: + candidates = [] + for line_number, line in enumerate(text.splitlines(), start=1): + if not D_RULE_RE.match(line): + continue + rule = line.strip() + if len(rule) <= D_RULE_CHAR_LIMIT: + continue + candidates.append( + { + "source": source, + "source_kind": source_kind, + "line": line_number, + "chars": len(rule), + "text": rule[:360] + ("..." if len(rule) > 360 else ""), + } + ) + return candidates + + +def d_rule_brevity_inventory( + automation_root: pathlib.Path, + projects_root: pathlib.Path, + codex_home: pathlib.Path, +) -> dict[str, object]: + candidates: list[dict[str, object]] = [] + sources_checked = 0 + + for path in sorted(automation_root.glob("*/automation.toml")): + data = tomllib.loads(read_text(path)) + prompt = str(data.get("prompt", "")) + sources_checked += 1 + candidates.extend( + collect_overlong_d_rules(prompt, maybe_rel(path, automation_root), "automation_prompt") + ) + + for path in iter_agents(projects_root, codex_home): + sources_checked += 1 + candidates.extend(collect_overlong_d_rules(read_text(path), str(path), "agents_file")) + + candidates.sort( + key=lambda item: ( + -cast(int, item["chars"]), + cast(str, item["source"]), + cast(int, item["line"]), + ) + ) + return { + "char_limit": D_RULE_CHAR_LIMIT, + "sources_checked": sources_checked, + "candidate_count": len(candidates), + "candidates": candidates, + } + + +def build_snapshot(args: argparse.Namespace) -> dict[str, object]: + return { + "schema": "global-governance-consistency-audit/snapshot.v2", + "generated_at": utc_now(), + "automations": automations_inventory(args.automation_root.resolve()), + "agents": agents_inventory(args.projects_root.resolve(), args.codex_home.resolve()), + "agents_rule_graph": agents_rule_graph_inventory( + args.projects_root.resolve(), args.codex_home.resolve() + ), + "git": git_inventory(args.automation_root.resolve(), args.projects_root.resolve()), + "automation_gitignore": gitignore_inventory(args.automation_root.resolve()), + "d_rule_brevity": d_rule_brevity_inventory( + args.automation_root.resolve(), + args.projects_root.resolve(), + args.codex_home.resolve(), + ), + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--codex-home", type=pathlib.Path, default=default_codex_home()) + parser.add_argument("--automation-root", type=pathlib.Path, default=default_codex_home() / "automations") + parser.add_argument("--projects-root", type=pathlib.Path, required=True) + parser.add_argument("--pretty", action="store_true") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + payload = build_snapshot(args) + print(json.dumps(payload, indent=2 if args.pretty else None, sort_keys=True, separators=None if args.pretty else (",", ":"))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/ceratops-propose-rules-update/scripts/iteration_controller.py b/skills/ceratops-governance-lifecycle/scripts/iteration_controller.py similarity index 100% rename from skills/ceratops-propose-rules-update/scripts/iteration_controller.py rename to skills/ceratops-governance-lifecycle/scripts/iteration_controller.py diff --git a/skills/ceratops-governance-lifecycle/scripts/rule_graph.py b/skills/ceratops-governance-lifecycle/scripts/rule_graph.py new file mode 100644 index 0000000..a1cf126 --- /dev/null +++ b/skills/ceratops-governance-lifecycle/scripts/rule_graph.py @@ -0,0 +1,715 @@ +#!/usr/bin/env python3 +"""Parse and validate the canonical structured AGENTS rule graph. + +This module is the deterministic syntax owner shared by governance inventory +and history lookup helpers. It never changes rule or history sources. Semantic +decisions such as missing relations and relation compatibility remain with the +calling governance action. +""" + +from __future__ import annotations + +import json +import re +from collections import Counter +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Iterable, cast + + +RULE_ID_PATTERN = r"[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+" +RULE_START = re.compile( + rf"^- \[(?P{RULE_ID_PATTERN})\] (?P\S.*)$" +) +BRACKET_RULE_START = re.compile(r"^- \[(?P[^]]+)\](?:\s|$)") +METADATA = re.compile( + r"^ - (?P[a-z][a-z-]*): (?P\S.*)$" +) +RELATION_VALUE = re.compile( + rf"{RULE_ID_PATTERN}(?:, {RULE_ID_PATTERN})*" +) +LEGACY_RELATION = re.compile( + rf"`(?:limits|overrides|overlaps|conflicts) " + rf"{RULE_ID_PATTERN}(?:, {RULE_ID_PATTERN})*`" +) + +RELATION_KEYS = ("limits", "overrides", "overlaps", "conflicts") +DIRECTIONAL_KEYS = ("limits", "overrides") +SYMMETRIC_KEYS = ("overlaps", "conflicts") +SELF_KEY = "self" +SELF_STATUSES = ("exceeds-limit", "list-heavy") +METADATA_KEYS = (*RELATION_KEYS, SELF_KEY) +METADATA_ORDER = {key: index for index, key in enumerate(METADATA_KEYS)} +HISTORY_VERSION = 2 +HISTORY_ENTRY_KEYS = ("rules", "decision", "reason", "regression") +HISTORY_MAX_BYTES = 8 * 1024 +HISTORY_MAX_ENTRIES = 20 + + +@dataclass +class RuleRecord: + """One parsed rule and the metadata needed by graph consumers.""" + + rule_id: str + source: str + line: int + body_lines: list[str] + relations: dict[str, list[str]] = field(default_factory=dict) + self_statuses: list[str] = field(default_factory=list) + + +@dataclass +class ParsedRuleSource: + """Parsed rules plus deterministic findings for one AGENTS source.""" + + source: str + records: list[RuleRecord] + findings: list[dict[str, object]] + debts: list[dict[str, object]] + semantic_reviews: list[dict[str, object]] + + +def _finding( + code: str, + source: str, + line: int, + *, + rule_id: str | None = None, + detail: str | None = None, +) -> dict[str, object]: + item: dict[str, object] = {"code": code, "source": source, "line": line} + if rule_id: + item["rule_id"] = rule_id + if detail: + item["detail"] = detail + return item + + +def parse_rule_text(text: str, source: str) -> ParsedRuleSource: + """Parse canonical rule and metadata lines without accepting legacy syntax.""" + records: list[RuleRecord] = [] + findings: list[dict[str, object]] = [] + debts: list[dict[str, object]] = [] + semantic_reviews: list[dict[str, object]] = [] + current: RuleRecord | None = None + metadata_started = False + last_metadata_order = -1 + seen_metadata: set[str] = set() + + for line_number, line in enumerate(text.splitlines(), start=1): + rule_match = RULE_START.match(line) + if rule_match: + current = RuleRecord( + rule_id=rule_match.group("id"), + source=source, + line=line_number, + body_lines=[line], + ) + records.append(current) + metadata_started = False + last_metadata_order = -1 + seen_metadata = set() + continue + + if line.startswith("- "): + bracket_match = BRACKET_RULE_START.match(line) + code = "malformed_rule_id" if bracket_match else "missing_rule_id" + detail = bracket_match.group("id") if bracket_match else line[:160] + findings.append(_finding(code, source, line_number, detail=detail)) + current = None + continue + + indented_bullet = line[:1].isspace() and line.lstrip().startswith("- ") + if current is None: + if indented_bullet: + findings.append( + _finding( + "orphan_metadata", + source, + line_number, + detail=line.strip(), + ) + ) + continue + + if indented_bullet and not line.startswith(" - "): + findings.append( + _finding( + "malformed_metadata", + source, + line_number, + rule_id=current.rule_id, + detail=line.strip(), + ) + ) + continue + + if line.startswith(" - "): + metadata_started = True + metadata_match = METADATA.fullmatch(line) + if not metadata_match: + findings.append( + _finding( + "malformed_metadata", + source, + line_number, + rule_id=current.rule_id, + detail=line.strip(), + ) + ) + continue + key = metadata_match.group("key") + value = metadata_match.group("value") + if key not in METADATA_ORDER: + findings.append( + _finding( + "unknown_metadata_key", + source, + line_number, + rule_id=current.rule_id, + detail=key, + ) + ) + continue + if key in seen_metadata: + findings.append( + _finding( + "duplicate_metadata_key", + source, + line_number, + rule_id=current.rule_id, + detail=key, + ) + ) + seen_metadata.add(key) + key_order = METADATA_ORDER[key] + if key_order < last_metadata_order: + findings.append( + _finding( + "metadata_order", + source, + line_number, + rule_id=current.rule_id, + detail=key, + ) + ) + last_metadata_order = max(last_metadata_order, key_order) + + if key in RELATION_KEYS: + if not RELATION_VALUE.fullmatch(value): + findings.append( + _finding( + "invalid_relation_targets", + source, + line_number, + rule_id=current.rule_id, + detail=value, + ) + ) + continue + targets = value.split(", ") + if len(targets) != len(set(targets)): + findings.append( + _finding( + "duplicate_relation_target", + source, + line_number, + rule_id=current.rule_id, + detail=value, + ) + ) + current.relations[key] = targets + else: + statuses = value.split(", ") + if len(statuses) != len(set(statuses)): + findings.append( + _finding( + "duplicate_self_status", + source, + line_number, + rule_id=current.rule_id, + detail=value, + ) + ) + unknown = sorted(set(statuses) - set(SELF_STATUSES)) + if unknown: + findings.append( + _finding( + "unknown_self_status", + source, + line_number, + rule_id=current.rule_id, + detail=", ".join(unknown), + ) + ) + current.self_statuses = statuses + continue + + if line.startswith(" ") and line.strip(): + if metadata_started: + findings.append( + _finding( + "body_after_metadata", + source, + line_number, + rule_id=current.rule_id, + detail=line.strip(), + ) + ) + else: + current.body_lines.append(line) + continue + + current = None + + seen_ids: dict[str, int] = {} + for record in records: + prior_line = seen_ids.get(record.rule_id) + if prior_line is not None: + findings.append( + _finding( + "duplicate_rule_id", + source, + record.line, + rule_id=record.rule_id, + detail=f"first declared at line {prior_line}", + ) + ) + else: + seen_ids[record.rule_id] = record.line + + legacy_match = LEGACY_RELATION.search(" ".join(record.body_lines)) + if legacy_match: + findings.append( + _finding( + "legacy_relation_syntax", + source, + record.line, + rule_id=record.rule_id, + detail=legacy_match.group(0), + ) + ) + + exceeds = len(record.body_lines) > 6 or any( + len(body_line) > 80 for body_line in record.body_lines + ) + has_exceeds_status = "exceeds-limit" in record.self_statuses + if exceeds and not has_exceeds_status: + findings.append( + _finding( + "missing_exceeds_limit_status", + source, + record.line, + rule_id=record.rule_id, + ) + ) + elif has_exceeds_status and not exceeds: + findings.append( + _finding( + "unnecessary_exceeds_limit_status", + source, + record.line, + rule_id=record.rule_id, + ) + ) + elif has_exceeds_status: + debts.append( + _finding( + "exceeds-limit", + source, + record.line, + rule_id=record.rule_id, + ) + ) + + if "list-heavy" in record.self_statuses: + debt = _finding( + "list-heavy", + source, + record.line, + rule_id=record.rule_id, + ) + debts.append(debt) + semantic_reviews.append(debt) + + return ParsedRuleSource(source, records, findings, debts, semantic_reviews) + + +def parse_rule_source(path: Path) -> ParsedRuleSource: + """Read and parse one required UTF-8 AGENTS source.""" + resolved = path.resolve() + if not resolved.is_file(): + raise ValueError(f"rules does not exist: {resolved}") + text = resolved.read_text(encoding="utf-8") + if not text.strip(): + raise ValueError(f"rules is empty: {resolved}") + return parse_rule_text(text, str(resolved)) + + +def _strong_components( + nodes: Iterable[str], edges: dict[str, set[str]] +) -> list[list[str]]: + """Return strongly connected components that contain a directed cycle.""" + index = 0 + indices: dict[str, int] = {} + lowlinks: dict[str, int] = {} + stack: list[str] = [] + on_stack: set[str] = set() + components: list[list[str]] = [] + + def visit(node: str) -> None: + nonlocal index + indices[node] = index + lowlinks[node] = index + index += 1 + stack.append(node) + on_stack.add(node) + for target in edges.get(node, set()): + if target not in indices: + visit(target) + lowlinks[node] = min(lowlinks[node], lowlinks[target]) + elif target in on_stack: + lowlinks[node] = min(lowlinks[node], indices[target]) + if lowlinks[node] != indices[node]: + return + component: list[str] = [] + while stack: + member = stack.pop() + on_stack.remove(member) + component.append(member) + if member == node: + break + if len(component) > 1 or node in edges.get(node, set()): + components.append(sorted(component)) + + for node in sorted(set(nodes)): + if node not in indices: + visit(node) + return sorted(components) + + +def validate_rule_stack( + sources: list[ParsedRuleSource], *, global_source: str +) -> dict[str, Any]: + """Validate IDs, targets, relation cycles, and cross-scope legality.""" + findings: list[dict[str, object]] = [] + semantic_reviews: list[dict[str, object]] = [] + records_by_id: dict[str, RuleRecord] = {} + all_records = [record for source in sources for record in source.records] + for record in all_records: + prior = records_by_id.get(record.rule_id) + if prior is not None: + findings.append( + { + "code": "duplicate_stack_rule_id", + "rule_id": record.rule_id, + "sources": [prior.source, record.source], + } + ) + else: + records_by_id[record.rule_id] = record + + edges: list[dict[str, object]] = [] + symmetric_seen: set[tuple[str, str, str]] = set() + directional_graphs: dict[str, dict[str, set[str]]] = { + key: {rule_id: set() for rule_id in records_by_id} + for key in DIRECTIONAL_KEYS + } + combined_graph: dict[str, set[str]] = { + rule_id: set() for rule_id in records_by_id + } + for record in all_records: + for relation, targets in record.relations.items(): + for target in targets: + target_record = records_by_id.get(target) + edge: dict[str, object] = { + "source": record.rule_id, + "relation": relation, + "target": target, + "source_file": record.source, + } + if target_record is not None: + edge["target_file"] = target_record.source + edges.append(edge) + if target == record.rule_id: + findings.append( + { + "code": "self_relation", + "rule_id": record.rule_id, + "relation": relation, + } + ) + if target_record is None: + findings.append( + { + "code": "unknown_relation_target", + "rule_id": record.rule_id, + "relation": relation, + "target": target, + } + ) + continue + if record.source == global_source and target_record.source != global_source: + findings.append( + { + "code": "global_relation_targets_local", + "rule_id": record.rule_id, + "relation": relation, + "target": target, + } + ) + if ( + record.source != global_source + and target_record.source == global_source + and relation == "overrides" + ): + semantic_reviews.append( + { + "code": "local_override_delegation", + "rule_id": record.rule_id, + "target": target, + } + ) + if relation in DIRECTIONAL_KEYS: + directional_graphs[relation][record.rule_id].add(target) + combined_graph[record.rule_id].add(target) + else: + first, second = sorted((record.rule_id, target)) + pair = (first, second) + marker = (relation, *pair) + review: dict[str, object] = { + "code": relation, + "rules": list(pair), + } + if marker in symmetric_seen: + findings.append( + { + "code": "duplicate_symmetric_edge", + "relation": relation, + "rules": list(pair), + } + ) + else: + symmetric_seen.add(marker) + semantic_reviews.append(review) + + cycles: list[dict[str, object]] = [] + for relation, graph in directional_graphs.items(): + for members in _strong_components(records_by_id, graph): + cycle: dict[str, object] = { + "relation": relation, + "rules": members, + } + cycles.append(cycle) + cycle_findings = ( + findings if relation == "overrides" else semantic_reviews + ) + cycle_findings.append( + {"code": f"{relation}_cycle", "rules": members} + ) + for members in _strong_components(records_by_id, combined_graph): + relation_types = sorted( + { + str(edge["relation"]) + for edge in edges + if edge["relation"] in DIRECTIONAL_KEYS + and edge["source"] in members + and edge["target"] in members + } + ) + if len(relation_types) > 1: + cycle = { + "relation": "mixed", + "types": relation_types, + "rules": members, + } + cycles.append(cycle) + semantic_reviews.append({"code": "mixed_cycle", **cycle}) + + return { + "rule_count": len(all_records), + "relation_counts": dict( + sorted(Counter(str(edge["relation"]) for edge in edges).items()) + ), + "edges": edges, + "cycles": cycles, + "findings": findings, + "semantic_reviews": semantic_reviews, + } + + +def rule_source_summary(source: ParsedRuleSource) -> dict[str, Any]: + """Return compact per-source facts for inventory output.""" + def compact(items: list[dict[str, object]]) -> dict[str, object]: + grouped: dict[str, list[dict[str, object]]] = {} + for item in items: + grouped.setdefault(str(item["code"]), []).append(item) + summaries: list[dict[str, object]] = [] + for code, matches in sorted(grouped.items()): + summary: dict[str, object] = {"code": code, "count": len(matches)} + rule_ids = sorted( + {str(item["rule_id"]) for item in matches if "rule_id" in item} + ) + lines = sorted( + line + for item in matches + if isinstance((line := item.get("line")), int) + ) + details = sorted( + {str(item["detail"]) for item in matches if "detail" in item} + ) + if rule_ids: + summary["rule_ids"] = rule_ids + if lines: + summary["lines"] = lines + if details: + summary["details"] = details[:3] + if len(details) > 3: + summary["details_truncated"] = len(details) - 3 + summaries.append(summary) + return {"count": len(items), "items": summaries} + + relation_counts = Counter( + relation + for record in source.records + for relation, targets in record.relations.items() + for _ in targets + ) + return { + "path": source.source, + "rule_count": len(source.records), + "relation_counts": dict(sorted(relation_counts.items())), + "findings": compact(source.findings), + "approved_debt": compact(source.debts), + "semantic_reviews": compact(source.semantic_reviews), + } + + +def load_history_references( + entries: list[dict[str, object]], current_rule_ids: set[str] +) -> list[dict[str, object]]: + """Report history references that cannot constrain any current rule.""" + findings: list[dict[str, object]] = [] + for index, entry in enumerate(entries): + values = cast(list[object], entry["rules"]) + obsolete = sorted( + str(value) + for value in values + if value != "*" and value not in current_rule_ids + ) + if obsolete: + findings.append( + { + "code": "obsolete_history_reference", + "entry": index, + "references": obsolete, + } + ) + return findings + + +def load_history_source(path: Path) -> list[dict[str, object]]: + """Load the canonical non-empty regression-memory history object.""" + resolved = path.resolve() + if not resolved.is_file(): + raise ValueError(f"history does not exist: {resolved}") + text = resolved.read_text(encoding="utf-8") + if not text.strip(): + raise ValueError(f"history is empty: {resolved}") + data = json.loads(text) + if not isinstance(data, dict): + raise ValueError("history root must be an object") + if set(data) != {"version", "entries"}: + raise ValueError("history root fields must be ('version', 'entries')") + if data.get("version") != HISTORY_VERSION: + raise ValueError(f"history version must be {HISTORY_VERSION}") + entries = data.get("entries") + if not isinstance(entries, list) or not entries: + raise ValueError("history entries must be a non-empty list") + if not all(isinstance(entry, dict) for entry in entries): + raise ValueError("each history entry must be an object") + typed_entries = cast(list[dict[str, object]], entries) + expected_keys = set(HISTORY_ENTRY_KEYS) + for index, entry in enumerate(typed_entries): + actual_keys = set(entry) + if actual_keys != expected_keys: + raise ValueError( + f"history entry {index} fields must be {HISTORY_ENTRY_KEYS}; " + f"found {tuple(sorted(actual_keys))}" + ) + rules = entry["rules"] + if ( + not isinstance(rules, list) + or not rules + or not all(isinstance(value, str) and value for value in rules) + ): + raise ValueError(f"history entry {index} rules must be non-empty strings") + if len(rules) != len(set(rules)): + raise ValueError(f"history entry {index} rules must be unique") + if "*" in rules and rules != ["*"]: + raise ValueError(f"history entry {index} wildcard must be the only rule") + invalid_rules = [ + value + for value in rules + if value != "*" and not re.fullmatch(RULE_ID_PATTERN, value) + ] + if invalid_rules: + raise ValueError( + f"history entry {index} has invalid rule IDs: {invalid_rules}" + ) + for field_name in HISTORY_ENTRY_KEYS[1:]: + value = entry[field_name] + if not isinstance(value, str) or not value.strip(): + raise ValueError( + f"history entry {index} {field_name} must be non-empty text" + ) + return typed_entries + + +def history_limit_findings( + path: Path, entries: list[dict[str, object]] +) -> list[dict[str, object]]: + """Report deterministic triggers that require semantic history compaction.""" + findings: list[dict[str, object]] = [] + byte_count = path.resolve().stat().st_size + if byte_count > HISTORY_MAX_BYTES: + findings.append( + { + "code": "history_size_limit", + "bytes": byte_count, + "limit": HISTORY_MAX_BYTES, + } + ) + if len(entries) > HISTORY_MAX_ENTRIES: + findings.append( + { + "code": "history_entry_limit", + "entries": len(entries), + "limit": HISTORY_MAX_ENTRIES, + } + ) + return findings + + +def load_graph(paths: list[Path]) -> tuple[dict[str, set[str]], set[str]]: + """Load a structurally valid stack and return undirected adjacency.""" + parsed = [parse_rule_source(path) for path in paths] + source_findings = [finding for source in parsed for finding in source.findings] + if source_findings: + first = source_findings[0] + raise ValueError(f"invalid rule source: {first}") + global_source = parsed[0].source if parsed else "" + validation = validate_rule_stack(parsed, global_source=global_source) + if validation["findings"]: + raise ValueError(f"invalid rule graph: {validation['findings'][0]}") + graph: dict[str, set[str]] = { + record.rule_id: set() + for source in parsed + for record in source.records + } + for edge in validation["edges"]: + source_id = str(edge["source"]) + target_id = str(edge["target"]) + graph[source_id].add(target_id) + graph[target_id].add(source_id) + return graph, set(graph) diff --git a/skills/ceratops-governance-lifecycle/scripts/rule_history.py b/skills/ceratops-governance-lifecycle/scripts/rule_history.py new file mode 100644 index 0000000..37eabc6 --- /dev/null +++ b/skills/ceratops-governance-lifecycle/scripts/rule_history.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Return history relevant to named rules and their direct graph neighbors. + +Usage: + python scripts/rule_history.py lookup \ + --history HISTORY --rules RULES [--history ... --rules ...] \ + [--full] ID... + +The helper is read-only. It gives the proposal workflow a targeted history view +without loading unrelated entries or inferring graph neighbors manually. Lookup +output is compact unless the caller explicitly requests full entry evidence. +Pass rule sources in effective global-to-local order. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +from rule_graph import ( + history_limit_findings, + load_graph, + load_history_references, + load_history_source, +) + + +COMPACT_ENTRY_FIELDS = ( + "decision", +) + + +def entry_view(entry: dict[str, Any], *, full: bool, consulted: set[str]) -> dict[str, Any]: + """Return full evidence only when the caller explicitly requests it.""" + if full: + return entry + view = {key: entry[key] for key in COMPACT_ENTRY_FIELDS if key in entry} + affected = set(entry["rules"]) + view["matched_rules"] = sorted( + consulted if "*" in affected else consulted.intersection(affected) + ) + if "*" in affected: + view["wildcard"] = True + return view + + +def command_lookup(args: argparse.Namespace) -> None: + """Print only baseline or directly relevant history entries.""" + history_paths = [path.resolve() for path in args.history] + graph, current_rule_ids = load_graph(args.rules) + requested = set(args.rule_ids) + known = requested & set(graph) + neighbors = ( + set().union(*(graph[rule_id] for rule_id in known)) if known else set() + ) + consulted = requested | neighbors + relevant = [] + for history_path in history_paths: + entries = load_history_source(history_path) + maintenance_findings = [ + *load_history_references(entries, current_rule_ids), + *history_limit_findings(history_path, entries), + ] + if maintenance_findings: + raise ValueError( + "history cleanup required before lookup: " + + json.dumps(maintenance_findings, separators=(",", ":")) + ) + for entry in entries: + affected = entry.get("rules") + if not isinstance(affected, list): + raise ValueError("history entry rules must be a list") + if "*" in affected or consulted.intersection(affected): + relevant.append( + { + "history": str(history_path), + "entry": entry_view(entry, full=args.full, consulted=consulted), + } + ) + result = { + "detail": "full" if args.full else "compact", + "requested": sorted(requested), + "unknown": sorted(requested - set(graph)), + "neighbors": sorted(neighbors), + "consulted": sorted(consulted), + "entries": relevant, + } + print(json.dumps(result, separators=(",", ":"), sort_keys=True)) + + +def build_parser() -> argparse.ArgumentParser: + """Build the single read-only command consumed by the skill.""" + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + lookup = commands.add_parser("lookup", help="query targeted rule history") + lookup.add_argument("--history", type=Path, action="append", required=True) + lookup.add_argument("--rules", type=Path, action="append", required=True) + lookup.add_argument( + "--full", + action="store_true", + help="include complete causal and regression evidence", + ) + lookup.add_argument("rule_ids", nargs="+") + lookup.set_defaults(handler=command_lookup) + return parser + + +def main() -> int: + """Run a command with compact errors for agent consumption.""" + try: + args = build_parser().parse_args() + args.handler(args) + return 0 + except (OSError, ValueError, json.JSONDecodeError) as error: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/ceratops-prompt-optimizer/agents/openai.yaml b/skills/ceratops-prompt-optimizer/agents/openai.yaml deleted file mode 100644 index 2d795b3..0000000 --- a/skills/ceratops-prompt-optimizer/agents/openai.yaml +++ /dev/null @@ -1,6 +0,0 @@ -interface: - display_name: "Ceratops Prompt Optimizer" - short_description: "Transform raw prompts" - icon_small: "./assets/ceratops-logo-500.png" - icon_large: "./assets/ceratops-logo-500.png" - default_prompt: "Use $ceratops-prompt-optimizer to optimize this rough prompt: {{USER_PROMPT}}" diff --git a/skills/ceratops-propose-rules-update/SKILL.md b/skills/ceratops-propose-rules-update/SKILL.md deleted file mode 100644 index d4029f5..0000000 --- a/skills/ceratops-propose-rules-update/SKILL.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -name: ceratops-propose-rules-update -description: Diagnose instruction-system failures and design compact, regression-safe rule, workflow, or helper changes. Use when asked to add, change, reconcile, or review AGENTS.md rules, skill instructions, automation prompts, helper contracts, policy lines, or interactions between global and local instructions. ---- - -# Ceratops Propose Rules Update - -## Goal - -Produce the smallest safe behavioral correction, not the smallest text patch. -Every confirmed failure must change the controlling instruction surface or its -deterministic enforcement. - -Read [references/rule-design.md](references/rule-design.md) before drafting. - -## Constraints - -### Boundaries - -Use this skill for instruction-system changes. Route general prompt rewrites to -`$ceratops-prompt-optimizer`; answer diagnosis-only requests without forcing a -rule change. - -## Workflow - -1. Reconstruct the failed decision from current evidence. Identify the active - instruction stack, chosen behavior, and required behavior without assuming a - relevant rule, single cause, or owning artifact exists. -2. Inspect exact current text from every involved source. For global and local - instructions, determine scope and precedence before evaluating interaction. -3. Resolve structured history before drafting. For global rules, check - `$CODEX_HOME/AGENTS.history.json`; for local rules, check - `AGENTS.history.json` beside their `AGENTS.md`. From this skill directory, - run `python scripts/rule_history.py lookup --history --rules - ID...`, repeating both options for an interacting instruction stack. - Query relation targets when adding a rule. If the applicable history does - not exist, use targeted source history and state that recorded behavioral - history was unavailable. -4. Compare a local correction with a structural or non-rule correction. Select - by prevention of the failure, regression safety, behavioral scope, and - complexity; textual minimality does not win automatically. -5. Draft under the rule-design contract. Resolve interacting guidance inside - the candidate and identify every intentional behavior change. -6. Replay the current failure and relevant recorded history. Reject a candidate - that leaves the failed decision possible or regresses a recorded outcome. -7. Report the selected correction, material alternative, regression result, - and uncertainty. Mutate only the exact authorized artifacts. - -## Applying an approved change - -Append history in each changed rule scope, naming changed rules and direct -relation neighbors. Record causal and regression evidence with validation. Run -the source's rule checker when one exists; its history binding must reject -unrecorded rule changes. - -## Iterative optimization - -Use `scripts/iteration_controller.py` when repeated optimization is requested or -one-pass comparison remains materially uncertain. The controller owns numbering, -artifact hashes, and stopping state; it does not judge semantic quality. - -For each issued iteration, produce one candidate and one assessment. Compare it -with the original, champion, and regression evidence, then submit `improved` or -`no-improvement`. Improvement resets the streak. Stop when the controller -reports 200 iterations or ten consecutive non-improvements. Never claim state -the controller did not report. - -## Completion gate - -A proposal is complete only when it prevents the current recorded failure, -preserves earlier recorded behavior unless intentionally superseded, and is -better than the current state and material alternative. Otherwise change the -intervention or report the unresolved decision point. - -## Output - -### Output Contract - -Report only the selected exact change, its decision and regression evidence, -and unresolved impact. diff --git a/skills/ceratops-propose-rules-update/agents/openai.yaml b/skills/ceratops-propose-rules-update/agents/openai.yaml deleted file mode 100644 index cf0b70b..0000000 --- a/skills/ceratops-propose-rules-update/agents/openai.yaml +++ /dev/null @@ -1,6 +0,0 @@ -interface: - display_name: "Ceratops Propose Rules Update" - short_description: "Design compact, regression-safe rule changes" - icon_small: "./assets/ceratops-logo-500.png" - icon_large: "./assets/ceratops-logo-500.png" - default_prompt: "Use $ceratops-propose-rules-update to design a compact rule change that prevents this failure without regressing recorded behavior." diff --git a/skills/ceratops-propose-rules-update/assets/ceratops-logo-500.png b/skills/ceratops-propose-rules-update/assets/ceratops-logo-500.png deleted file mode 100644 index adcc81a..0000000 Binary files a/skills/ceratops-propose-rules-update/assets/ceratops-logo-500.png and /dev/null differ diff --git a/skills/ceratops-propose-rules-update/references/rule-design.md b/skills/ceratops-propose-rules-update/references/rule-design.md deleted file mode 100644 index 6da6909..0000000 --- a/skills/ceratops-propose-rules-update/references/rule-design.md +++ /dev/null @@ -1,69 +0,0 @@ -# Rule Design Contract - -## Behavioral objective - -Minimize behavioral delta: change only decisions needed to prevent the failure -while preserving recorded correct behavior. Word count and diff size are -secondary evidence. - -## Rule form - -Use one sentence when one condition determines one required behavior: - -```text -[RULE-ID] When , require . -``` - -Use the strongest unambiguous wording that accurately expresses the intended -rule. - -Add an exception or relationship only when it changes the decision. Keep -scenarios outside governing instructions and use them as regression evidence. - -## Size and enumeration - -Keep a canonical rule within six 80-character lines. Split it when independent -conditions or actions can be named without weakening their interaction. - -A normative enumeration may name at most three members. For a larger set, -state the common deciding property; when exact membership is operationally -required, store the closed set in deterministic data or a helper. - -## Relationships - -Use `requires` for a co-gate, `limits` when an independent guardrail narrows -another rule, and `overrides` when a specific rule wins incompatible -application. Do not carry an unresolved interaction into a candidate. - -Merge coextensive guidance into one rule. Keep independently reusable behavior -separate and express only the directional relationship needed to interpret it. -When evidence cannot select one coherent result, present the exact decision -point before treating a candidate as complete. - -## Scope interaction - -Treat global instructions as the baseline for local instructions. A local rule -may add project behavior or narrow discretion the global rules leave open. It -may override a global rule only when that rule explicitly delegates the -decision to local scope. Otherwise repair the local rule or propose a global -delegation instead of retaining a contradiction. - -Store local history with the local rule source and record any related global -rule IDs and the evaluated global-rule hash. Query both histories for a -cross-scope change. A project stack check must detect a stale global hash. -Revalidate known local references after changing a global rule; without a -registry, report broader local compatibility as unverified. - -Use a JSON history object with `version: 2` and an `entries` array. Each entry -names affected rule IDs and records the causal and regression evidence required -by the source's checker. - -## Acceptance - -State the required invariant. Add a negative boundary only when it excludes a -distinct plausible failure not excluded by the invariant. Apply broad override -policy without repeating local user-override clauses. - -Evaluate both local and structural intervention. Accept a candidate only when -it prevents the current failure and relevant recorded failures without -regressing recorded correct outcomes unless explicitly superseded. diff --git a/skills/ceratops-propose-rules-update/scripts/rule_history.py b/skills/ceratops-propose-rules-update/scripts/rule_history.py deleted file mode 100644 index 9def210..0000000 --- a/skills/ceratops-propose-rules-update/scripts/rule_history.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python3 -"""Return history relevant to named rules and their direct graph neighbors. - -Usage: - python scripts/rule_history.py lookup \ - --history HISTORY --rules RULES [--history ... --rules ...] ID... - -The helper is read-only. It gives the proposal workflow a targeted history view -without loading unrelated entries or inferring graph neighbors manually. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import re -import sys -from pathlib import Path -from typing import Any - - -VERSION = 2 -RULE_START = re.compile(r"^- \[(?P[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\] ") -RELATION = re.compile( - r"`(?:requires|limits|overrides) " - r"(?P[A-Z][A-Z0-9-]*(?:, [A-Z][A-Z0-9-]*)*)`" -) - - -def read_text(path: Path, label: str) -> str: - """Read a required non-empty UTF-8 file.""" - if not path.is_file(): - raise ValueError(f"{label} does not exist: {path}") - value = path.read_text(encoding="utf-8") - if not value.strip(): - raise ValueError(f"{label} is empty: {path}") - return value - - -def file_hash(path: Path) -> str: - """Return the SHA-256 hash identifying the consulted history state.""" - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(65536), b""): - digest.update(chunk) - return digest.hexdigest() - - -def parse_relations(text: str) -> dict[str, set[str]]: - """Parse rule IDs and outgoing relationships from one rule source.""" - records: dict[str, list[str]] = {} - current: str | None = None - for line in text.splitlines(): - match = RULE_START.match(line) - if match: - current = match.group("id") - records[current] = [line] - elif current and line.startswith(" ") and line.strip(): - records[current].append(line.strip()) - elif not line.strip() or line.startswith("## "): - current = None - - relations: dict[str, set[str]] = {rule_id: set() for rule_id in records} - for rule_id, parts in records.items(): - record = " ".join(parts) - for match in RELATION.finditer(record): - relations[rule_id].update(match.group("targets").split(", ")) - return relations - - -def load_graph(paths: list[Path]) -> dict[str, set[str]]: - """Merge rule sources and return undirected cross-source adjacency.""" - outgoing: dict[str, set[str]] = {} - for path in paths: - for rule_id, targets in parse_relations( - read_text(path.resolve(), "rules") - ).items(): - if rule_id in outgoing: - raise ValueError(f"duplicate rule ID across sources: {rule_id}") - outgoing[rule_id] = targets - graph: dict[str, set[str]] = {rule_id: set() for rule_id in outgoing} - for rule_id, targets in outgoing.items(): - for target in targets: - if target not in graph: - raise ValueError(f"{rule_id} relates to unknown rule {target}") - graph[rule_id].add(target) - graph[target].add(rule_id) - return graph - - -def load_history(path: Path) -> list[dict[str, Any]]: - """Load the supported history schema.""" - data = json.loads(read_text(path, "history")) - if not isinstance(data, dict) or data.get("version") != VERSION: - raise ValueError(f"history version must be {VERSION}") - entries = data.get("entries") - if not isinstance(entries, list) or not entries: - raise ValueError("history entries must be a non-empty list") - if not all(isinstance(entry, dict) for entry in entries): - raise ValueError("each history entry must be an object") - return entries - - -def command_lookup(args: argparse.Namespace) -> None: - """Print only baseline or directly relevant history entries.""" - history_paths = [path.resolve() for path in args.history] - graph = load_graph(args.rules) - requested = set(args.rule_ids) - known = requested & set(graph) - neighbors = ( - set().union(*(graph[rule_id] for rule_id in known)) if known else set() - ) - consulted = requested | neighbors - relevant = [] - for history_path in history_paths: - for entry in load_history(history_path): - affected = entry.get("rules") - if not isinstance(affected, list): - raise ValueError("history entry rules must be a list") - if "*" in affected or consulted.intersection(affected): - relevant.append( - {"history": str(history_path), "entry": entry} - ) - result = { - "requested": sorted(requested), - "unknown": sorted(requested - set(graph)), - "neighbors": sorted(neighbors), - "consulted": sorted(consulted), - "history_sha256": { - str(path): file_hash(path) for path in history_paths - }, - "entries": relevant, - } - print(json.dumps(result, separators=(",", ":"), sort_keys=True)) - - -def build_parser() -> argparse.ArgumentParser: - """Build the single read-only command consumed by the skill.""" - parser = argparse.ArgumentParser(description=__doc__) - commands = parser.add_subparsers(dest="command", required=True) - lookup = commands.add_parser("lookup", help="query targeted rule history") - lookup.add_argument("--history", type=Path, action="append", required=True) - lookup.add_argument("--rules", type=Path, action="append", required=True) - lookup.add_argument("rule_ids", nargs="+") - lookup.set_defaults(handler=command_lookup) - return parser - - -def main() -> int: - """Run a command with compact errors for agent consumption.""" - try: - args = build_parser().parse_args() - args.handler(args) - return 0 - except (OSError, ValueError, json.JSONDecodeError) as error: - print(f"ERROR: {error}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/skills/ceratops-skill-lifecycle/SKILL.md b/skills/ceratops-skill-lifecycle/SKILL.md index 704e649..a382328 100644 --- a/skills/ceratops-skill-lifecycle/SKILL.md +++ b/skills/ceratops-skill-lifecycle/SKILL.md @@ -79,7 +79,7 @@ Infer missing inputs from the current repo state before asking. fast direct release-branch changes, promoting committed skill branches into a local release branch, and shipping staged skills repo changes through GitHub. - If the task is advisory-only skill optimization, use - `$ceratops-skill-optimize`. + `$ceratops-governance-lifecycle` action `optimize-skill`. - If the task is Ceratops skill-contract standards upkeep, use `references/skills-contract-review.md`. - If the task is repository skill consistency and contract compliance, use diff --git a/skills/ceratops-skill-lifecycle/references/contracts/skill-deterministic-contract.json b/skills/ceratops-skill-lifecycle/references/contracts/skill-deterministic-contract.json index 8a8e70a..d562690 100644 --- a/skills/ceratops-skill-lifecycle/references/contracts/skill-deterministic-contract.json +++ b/skills/ceratops-skill-lifecycle/references/contracts/skill-deterministic-contract.json @@ -74,6 +74,7 @@ "skill.runtime_payload_portability", "skill.runtime_source_ownership", "skill.shared_section_generation", + "skill.multi_action_contract", "skill.resource_layout", "skill.public_doc_alignment", "skill.private_path_and_secret_absence", @@ -163,6 +164,20 @@ "skills/ceratops-skill-lifecycle/scripts/skills-consistency-source-validator.py:check_skill" ] }, + { + "id": "skill.multi_action_contract", + "category": "source", + "deterministic_evidence": [ + "templates/skill-sections.json multi-action-skill assignments", + "skills//SKILL.md Action References", + "skills//references/*.md action titles" + ], + "pass_condition": "Every skill assigned the multi-action-skill section has one complete Action References index with at least two unique direct references/*.md paths; every mapped file exists and uses an Action title, and every top-level reference with an Action title is indexed.", + "source_lines": [ + "templates/sections/multi-action-skill.md:Multi-Action Skill Contract", + "skills/ceratops-skill-lifecycle/scripts/skills-consistency-source-validator.py:check_multi_action_skill_contract" + ] + }, { "id": "skill.resource_layout", "category": "source", diff --git a/skills/ceratops-skill-lifecycle/references/skills-consistency-review.md b/skills/ceratops-skill-lifecycle/references/skills-consistency-review.md index 0ce71aa..8e7f543 100644 --- a/skills/ceratops-skill-lifecycle/references/skills-consistency-review.md +++ b/skills/ceratops-skill-lifecycle/references/skills-consistency-review.md @@ -73,8 +73,8 @@ Infer missing inputs from the repository and installed manifests before asking. - Update source repositories only through task worktrees and regenerate runtime copies only through the owning repository installer. - Do not turn repository consistency review into general skill optimization; - use `$ceratops-skill-optimize` for advisory improvement beyond contract or - coupled-surface defects. + use `$ceratops-governance-lifecycle` action `optimize-skill` for advisory + improvement beyond contract or coupled-surface defects. ### Skill-Specific Rules diff --git a/skills/ceratops-skill-lifecycle/scripts/skills-consistency-source-validator.py b/skills/ceratops-skill-lifecycle/scripts/skills-consistency-source-validator.py index 70863f3..00d750c 100644 --- a/skills/ceratops-skill-lifecycle/scripts/skills-consistency-source-validator.py +++ b/skills/ceratops-skill-lifecycle/scripts/skills-consistency-source-validator.py @@ -54,6 +54,10 @@ NAME_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$") SKILL_REF_RE = re.compile(r"\$([a-z0-9]+(?:-[a-z0-9]+)+)(?![A-Za-z0-9_-])") README_SKILL_ROW_RE = re.compile(r"^\|\s*`(?P[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)`\s*\|", re.MULTILINE) +ACTION_REFERENCES_HEADING = "### Action References" +ACTION_REFERENCE_TOKEN_RE = re.compile(r"`(?Preferences/[^`\s]+\.md)`") +DIRECT_ACTION_REFERENCE_RE = re.compile(r"references/[a-z0-9]+(?:-[a-z0-9]+)*\.md") +ACTION_TITLE_RE = re.compile(r"# .+ Action") ALLOWED_EXTERNAL_SKILL_REFS = {"skill-creator", "skill-name"} INTERFACE_FIELD_RE = re.compile( r"^\s*(display_name|short_description|icon_small|icon_large|default_prompt):\s*(.+?)\s*$", @@ -579,6 +583,95 @@ def check_skill_nondeterministic_contract() -> list[str]: return errors +def has_action_title(path: pathlib.Path) -> bool: + """Return whether a Markdown reference uses the reserved action title.""" + + lines = path.read_text(encoding="utf-8").splitlines() + return bool(lines and ACTION_TITLE_RE.fullmatch(lines[0]) is not None) + + +def check_multi_action_skill_contract( + manifest: dict[str, object], + selected_skill_names: set[str] | None = None, +) -> list[str]: + """Validate every manifest-declared multi-action index and action file.""" + + errors: list[str] = [] + assignments = manifest.get("skills", {}) + if not isinstance(assignments, dict): + return errors + multi_action_skill_names = sorted( + skill_name + for skill_name, section_names in assignments.items() + if isinstance(skill_name, str) + and isinstance(section_names, list) + and "multi-action-skill" in section_names + and (selected_skill_names is None or skill_name in selected_skill_names) + ) + for skill_name in multi_action_skill_names: + skill_dir = SKILLS_DIR / skill_name + skill_path = skill_dir / "SKILL.md" + if not skill_path.is_file(): + errors.append(f"{skill_name}: missing multi-action SKILL.md") + continue + skill_lines = skill_path.read_text(encoding="utf-8").splitlines() + heading_indexes = [ + index for index, line in enumerate(skill_lines) if line == ACTION_REFERENCES_HEADING + ] + if len(heading_indexes) != 1: + errors.append( + f"{skill_name}: requires exactly one {ACTION_REFERENCES_HEADING} section" + ) + continue + section_start = heading_indexes[0] + 1 + section_end = next( + ( + index + for index in range(section_start, len(skill_lines)) + if re.match(r"^#{1,3}\s", skill_lines[index]) + ), + len(skill_lines), + ) + action_references = ACTION_REFERENCE_TOKEN_RE.findall( + "\n".join(skill_lines[section_start:section_end]) + ) + duplicates = sorted( + {path for path in action_references if action_references.count(path) > 1} + ) + for action_reference in duplicates: + errors.append(f"{skill_name}: duplicate action reference {action_reference}") + if len(set(action_references)) < 2: + errors.append(f"{skill_name}: multi-action index requires at least two actions") + + valid_references: set[str] = set() + for action_reference in action_references: + if DIRECT_ACTION_REFERENCE_RE.fullmatch(action_reference) is None: + errors.append( + f"{skill_name}: action reference must be one direct references/*.md path: " + f"{action_reference}" + ) + continue + valid_references.add(action_reference) + action_path = skill_dir / pathlib.PurePosixPath(action_reference) + if not action_path.is_file(): + errors.append(f"{skill_name}: missing action reference {action_reference}") + continue + if not has_action_title(action_path): + errors.append( + f"{skill_name}: {action_reference} must be titled # Action" + ) + + action_files: set[str] = set() + references_dir = skill_dir / "references" + if references_dir.is_dir(): + for action_path in references_dir.glob("*.md"): + if has_action_title(action_path): + action_files.add(f"references/{action_path.name}") + for action_reference in sorted(action_files - valid_references): + errors.append(f"{skill_name}: unlisted action reference {action_reference}") + return errors + + def check_skill_scope_validator() -> list[str]: """Check objective multi-action skill rules without judging prose quality.""" @@ -595,7 +688,11 @@ def check_skill_scope_validator() -> list[str]: errors.append(f"{skill_name}: missing multi-action SKILL.md") continue multi_action_text = multi_action_path.read_text(encoding="utf-8") - actual_actions = {path.name for path in (skill_dir / "references").glob("*.md")} + actual_actions = { + path.name + for path in (skill_dir / "references").glob("*.md") + if has_action_title(path) + } unexpected_actions = sorted(actual_actions - set(expected_actions)) for action_file in unexpected_actions: errors.append(f"{skill_name}: unexpected action reference references/{action_file}") @@ -700,6 +797,7 @@ def check_source_governance_consistency( errors: list[str] = [] errors.extend(check_repo_skill_refs(skill_names)) + errors.extend(check_multi_action_skill_contract(manifest)) if profile == PROFILE_CERATOPS: errors.extend(check_validation_command_surface()) errors.extend(check_skill_contract_remediation_policy()) @@ -812,6 +910,7 @@ def check_selected_skills( errors.extend(check_skill(skill_dir_by_name[skill_name], readme_rows, manifest, skill_names, profile)) errors.extend(check_runtime_payloads(manifest, skill_names, selected_skill_names)) + errors.extend(check_multi_action_skill_contract(manifest, selected_skill_names)) selected_dirs = [skill_dir_by_name[name] for name in sorted(selected_skill_names & skill_names)] errors.extend(check_secrets(selected_dirs)) return errors @@ -1026,6 +1125,7 @@ def main() -> int: errors.extend(check_source_installer(profile)) if args.mode == "sections": errors.extend(check_section_sources(manifest, skill_dirs)) + errors.extend(check_multi_action_skill_contract(manifest)) if errors: print(f"errors: {len(errors)}", file=sys.stderr) for error in errors: diff --git a/skills/ceratops-skill-optimize/agents/openai.yaml b/skills/ceratops-skill-optimize/agents/openai.yaml deleted file mode 100644 index 54d255f..0000000 --- a/skills/ceratops-skill-optimize/agents/openai.yaml +++ /dev/null @@ -1,6 +0,0 @@ -interface: - display_name: "Ceratops Skill Optimizer" - short_description: "Propose skill improvements without editing" - icon_small: "./assets/ceratops-logo-500.png" - icon_large: "./assets/ceratops-logo-500.png" - default_prompt: "Use $ceratops-skill-optimize to propose exact advisory-only improvements for this skill, or for the explicitly requested skill set, including action references, metadata, payloads, validators, and docs when they govern the same behavior: {{SKILL_TEXT}}" diff --git a/skills/ceratops-skill-optimize/assets/ceratops-logo-500.png b/skills/ceratops-skill-optimize/assets/ceratops-logo-500.png deleted file mode 100644 index adcc81a..0000000 Binary files a/skills/ceratops-skill-optimize/assets/ceratops-logo-500.png and /dev/null differ diff --git a/skills/ceratops-task-lifecycle/references/closure-check.md b/skills/ceratops-task-lifecycle/references/closure-check.md index 042db88..e718923 100644 --- a/skills/ceratops-task-lifecycle/references/closure-check.md +++ b/skills/ceratops-task-lifecycle/references/closure-check.md @@ -2,8 +2,9 @@ ## Goal -Give a concise, evidence-based answer about whether required work remains at -the end of a thread, session, or task. +At the end of a thread, session, or task, give a concise, evidence-based answer +about whether required work remains and include the credit-savings analysis +result. ## Context @@ -86,15 +87,19 @@ before asking. questions, warnings, or deferred decisions. - Classify each as required, optional, superseded, or irrelevant. -#### 5. Classify Closure State +#### 5. Include Credit-Saving Analysis + +- Invoke `$ceratops-credit-savings-analysis` for the current thread, reuse fresh + closure evidence, and include its required result under `Credit savings`. + +#### 6. Classify Closure State - Classify relevant state as required remaining work, blocker, intentionally retained, optional cleanup, stale or out-of-scope, unverified, or no longer relevant. -#### 6. Answer From Checked Evidence +#### 7. Answer From Checked Evidence -- Use the strongest closure claim justified by checked evidence. - Keep the answer concise and omit routine command logs or process narration. ## Done When @@ -105,19 +110,14 @@ before asking. - Required remaining work and blockers are not omitted. - Uncommitted, unpushed, retained, stale, warning, forgotten-follow-up, and unverified states from any same-thread touched artifact are reported. -- Any no-required-work-left claim is limited to evidence actually checked. +- A response that reports no unresolved items is supported by checked evidence. +- The `$ceratops-credit-savings-analysis` result or blocker is included under + `Credit savings`. - No mutation was performed unless explicitly requested. ### Output Contract -First line must be exactly one of: - -- No required work left. -- Required work remains. -- Blocked. -- Unclear from checked evidence. - -Then include only relevant concise items: +Return only relevant concise bullets: - checked scope, only when it limits the answer - required next actions @@ -128,6 +128,9 @@ Then include only relevant concise items: - important unverified claims - relevant forgotten follow-ups - optional cleanup +- `Credit savings`: the required `$ceratops-credit-savings-analysis` result + +If no listed item applies, return only `- No unresolved items.` Omit routine command logs and process narration. diff --git a/templates/sections/multi-action-skill.md b/templates/sections/multi-action-skill.md index a7dbc36..f3d4f9c 100644 --- a/templates/sections/multi-action-skill.md +++ b/templates/sections/multi-action-skill.md @@ -2,6 +2,9 @@ ## Multi-Action Skill Contract +- List every supported action exactly once under `### Action References` in the + parent `SKILL.md`, map it to one direct `references/*.md` file titled + `# Action`, and reserve that title form for action files. - Load only the selected action reference unless the current action explicitly hands off to another action. - Read the selected action file under `references/` and follow its inputs, diff --git a/templates/skill-sections.json b/templates/skill-sections.json index 17dcfc3..a848cc9 100644 --- a/templates/skill-sections.json +++ b/templates/skill-sections.json @@ -33,18 +33,13 @@ "ceratops-automation-run": [ "core" ], - "ceratops-propose-rules-update": [ - "core" + "ceratops-governance-lifecycle": [ + "core", + "multi-action-skill" ], "ceratops-credit-savings-analysis": [ "core" ], - "ceratops-prompt-optimizer": [ - "core" - ], - "ceratops-skill-optimize": [ - "core" - ], "ceratops-code-consistency-audit": [ "core" ],