diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index fdd5a883..8498772a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -18,15 +18,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "cli", - "ai", - "mcp", - "templates", - "simulator", - "review", - "skills" - ], + "keywords": [], "category": "tools" }, { @@ -38,15 +30,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "ci", - "automation", - "testing", - "build", - "quality-gates", - "agents", - "workflow" - ], + "keywords": [], "category": "tools" }, { @@ -58,12 +42,7 @@ "name": "synaptic-canvas" }, "license": "MIT", - "keywords": [ - "codex", - "agents", - "task-tool", - "cli" - ], + "keywords": [], "category": "tools" }, { @@ -75,14 +54,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "agents", - "prompt-hardening", - "qa", - "review", - "orchestration", - "skills" - ], + "keywords": [], "category": "tools" }, { @@ -94,13 +66,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "git", - "pr", - "github", - "azure-devops", - "workflow" - ], + "keywords": [], "category": "tools" }, { @@ -112,14 +78,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "delay", - "polling", - "scheduler", - "ci", - "workflow", - "agents" - ], + "keywords": [], "category": "tools" }, { @@ -131,13 +90,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "git", - "worktree", - "workflow", - "agents", - "branching" - ], + "keywords": [], "category": "tools" }, { @@ -149,14 +102,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "github", - "issues", - "workflow", - "agents", - "pull-requests", - "worktree" - ], + "keywords": [], "category": "tools" }, { @@ -168,14 +114,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "kanban", - "task-tracking", - "workflow", - "git", - "pr", - "gates" - ], + "keywords": [], "category": "tools" }, { @@ -187,16 +126,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "terminal", - "launcher", - "macos", - "windows", - "tmux", - "claude", - "codex", - "gemini" - ], + "keywords": [], "category": "tools" }, { @@ -208,13 +138,7 @@ "name": "synaptic-canvas" }, "license": "MIT", - "keywords": [ - "background-agents", - "claude", - "codex", - "gemini", - "atm" - ], + "keywords": [], "category": "tools" }, { @@ -226,12 +150,7 @@ "name": "synaptic-canvas" }, "license": "MIT", - "keywords": [ - "management", - "packages", - "installer", - "agents" - ], + "keywords": [], "category": "tools" }, { @@ -243,13 +162,7 @@ "name": "synaptic-canvas" }, "license": "MIT", - "keywords": [ - "nuget", - "repomix", - "documentation", - "api", - "context" - ], + "keywords": [], "category": "tools" }, { @@ -261,16 +174,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "diff", - "roslyn", - "semantic", - "csharp", - "vb", - "git", - "azure", - "github" - ], + "keywords": [], "category": "tools" }, { @@ -282,17 +186,7 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "rust", - "development", - "service-hardening", - "tokio", - "code-review", - "architecture", - "qa", - "agents", - "skills" - ], + "keywords": [], "category": "tools" }, { @@ -304,15 +198,20 @@ "name": "randlee" }, "license": "MIT", - "keywords": [ - "startup", - "checklist", - "git", - "ci", - "automation", - "agents" - ], + "keywords": [], + "category": "tools" + }, + { + "name": "sc-refactory", + "source": "./packages/sc-refactory", + "description": "Design and install a rule-driven refactoring toolkit with startup policy injection, approved-fix lookup, curated rule authoring, and named-teammate orchestration for large migration campaigns.\n", + "version": "0.1.0", + "author": { + "name": "synaptic-canvas" + }, + "license": "MIT", + "keywords": [], "category": "tools" } ] -} +} \ No newline at end of file diff --git a/.claude-plugin/registry.json b/.claude-plugin/registry.json index faab9db5..917c58df 100644 --- a/.claude-plugin/registry.json +++ b/.claude-plugin/registry.json @@ -16,12 +16,12 @@ "category": "tools", "artifacts": { "commands": 0, - "skills": 3, + "skills": 23, "agents": 0, "scripts": 0, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.788270+00:00" + "lastUpdated": "2026-04-28T20:57:09.931997" }, { "name": "sc-ci-automation", @@ -38,7 +38,7 @@ "scripts": 1, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.789239+00:00" + "lastUpdated": "2026-04-28T20:57:09.932851" }, { "name": "sc-codex", @@ -52,10 +52,10 @@ "commands": 1, "skills": 1, "agents": 1, - "scripts": 2, + "scripts": 9, "schemas": 2 }, - "lastUpdated": "2026-04-20T04:35:16.790035+00:00" + "lastUpdated": "2026-04-28T20:57:09.933546" }, { "name": "sc-coding-agent-hardening", @@ -67,12 +67,12 @@ "category": "tools", "artifacts": { "commands": 0, - "skills": 1, + "skills": 5, "agents": 0, "scripts": 0, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.790514+00:00" + "lastUpdated": "2026-04-28T20:57:09.934001" }, { "name": "sc-commit-push-pr", @@ -89,7 +89,7 @@ "scripts": 9, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.791173+00:00" + "lastUpdated": "2026-04-28T20:57:09.934600" }, { "name": "sc-delay-tasks", @@ -106,7 +106,7 @@ "scripts": 2, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.791779+00:00" + "lastUpdated": "2026-04-28T20:57:09.935179" }, { "name": "sc-git-worktree", @@ -123,7 +123,7 @@ "scripts": 7, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.792776+00:00" + "lastUpdated": "2026-04-28T20:57:09.936225" }, { "name": "sc-github-issue", @@ -140,7 +140,7 @@ "scripts": 1, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.793861+00:00" + "lastUpdated": "2026-04-28T20:57:09.937297" }, { "name": "sc-kanban", @@ -153,11 +153,11 @@ "artifacts": { "commands": 1, "skills": 1, - "agents": 4, + "agents": 8, "scripts": 5, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.794575+00:00" + "lastUpdated": "2026-04-28T20:57:09.937938" }, { "name": "sc-manage", @@ -174,7 +174,7 @@ "scripts": 8, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.795262+00:00" + "lastUpdated": "2026-04-28T20:57:09.939601" }, { "name": "sc-repomix-nuget", @@ -186,12 +186,12 @@ "category": "tools", "artifacts": { "commands": 1, - "skills": 1, + "skills": 3, "agents": 3, "scripts": 3, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.795931+00:00" + "lastUpdated": "2026-04-28T20:57:09.941192" }, { "name": "sc-roslyn-diff", @@ -208,7 +208,7 @@ "scripts": 6, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.796629+00:00" + "lastUpdated": "2026-04-28T20:57:09.941795" }, { "name": "sc-rust", @@ -220,12 +220,12 @@ "category": "tools", "artifacts": { "commands": 0, - "skills": 3, + "skills": 25, "agents": 7, "scripts": 0, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.797723+00:00" + "lastUpdated": "2026-04-28T20:57:09.942965" }, { "name": "sc-startup", @@ -238,11 +238,11 @@ "artifacts": { "commands": 1, "skills": 1, - "agents": 2, + "agents": 4, "scripts": 1, "schemas": 0 }, - "lastUpdated": "2026-04-20T04:35:16.798463+00:00" + "lastUpdated": "2026-04-28T20:57:09.943623" }, { "name": "sc-launch-term", @@ -259,7 +259,7 @@ "scripts": 3, "schemas": 0 }, - "lastUpdated": "2026-04-26T00:00:00+00:00" + "lastUpdated": "2026-04-28T20:57:09.938449" }, { "name": "sc-launchpad", @@ -276,17 +276,34 @@ "scripts": 2, "schemas": 0 }, - "lastUpdated": "2026-04-25T23:48:47.713457" + "lastUpdated": "2026-04-28T20:57:09.939012" + }, + { + "name": "sc-refactory", + "version": "0.1.0", + "description": "Design and install a rule-driven refactoring toolkit with startup policy injection, approved-fix lookup, curated rule authoring, and named-teammate orchestration for large migration campaigns.\n", + "author": "synaptic-canvas", + "license": "MIT", + "keywords": [], + "category": "tools", + "artifacts": { + "commands": 5, + "skills": 8, + "agents": 7, + "scripts": 10, + "schemas": 0 + }, + "lastUpdated": "2026-04-28T20:57:09.940607" } ], "metadata": { - "totalPackages": 16, - "totalCommands": 15, - "totalSkills": 19, - "totalAgents": 45, - "totalScripts": 50, + "totalPackages": 17, + "totalCommands": 20, + "totalSkills": 75, + "totalAgents": 58, + "totalScripts": 67, "totalSchemas": 2 }, - "generated": "2026-04-25T23:48:47.713478", - "lastUpdated": "2026-04-25T23:48:47.713479" -} + "generated": "2026-04-28T20:57:09.943637", + "lastUpdated": "2026-04-28T20:57:09.943638" +} \ No newline at end of file diff --git a/packages/docs/refactory-design.md b/packages/docs/refactory-design.md new file mode 100644 index 00000000..7b3ef1c9 --- /dev/null +++ b/packages/docs/refactory-design.md @@ -0,0 +1,1193 @@ +# Refactory Design + +**Status:** Draft +**Author:** Codex +**Created:** April 28, 2026 +**Package:** `sc-refactory` +**Related:** `sc-startup`, `sc-codex`, `sc-manage` + +## Purpose + +`refactory` should primarily be a design skill for creating constrained refactoring systems like the one we just designed. + +The skill should help an agent: + +- design the rule system first +- define the policy boundaries for approved fixes +- choose the runtime layout and hook model +- scaffold the toolkit package and installed repo layout +- then bootstrap the operational scripts, skills, and agents + +The resulting package must be able to install the same system we just proved out manually: + +- concise startup trigger injection +- explicit startup policy constraints +- graph-backed lookup of approved fixes +- curated authoring flow for new rules +- orchestration of authorized change waves +- QA verification of rule compliance +- background agents that isolate lookup/write/orchestration/QA work +- local scripts for preflight, startup, repair, logging, and graph maintenance + +This is not a generic refactoring assistant. It is a policy-system design and deployment workflow for approved fixes only. + +## Problem + +In large migrations, coding agents are dangerous when left to infer fixes from partial context. The common failure modes are: + +- making many superficially plausible but incorrect edits +- forgetting repeated migration rules across repos +- blowing up context with build logs, example fixes, and tool output +- re-discovering the same fix shape repeatedly + +The system needs a local memory layer with strict guardrails: + +- startup must remind the agent which fix patterns are approved +- startup must state what is and is not allowed +- lookup must retrieve the exact rule document and sample fixes +- write must let humans or trusted agents add new approved rules +- QA must verify that every edit is explained by approved rules +- only committed source-of-truth rules should matter + +## Design Goals + +- Package the toolkit as a reusable skill bundle, not ad hoc repo code. +- Install into a target repo with a predictable layout. +- Use the existing global Claude/Codex startup dispatcher model. +- Follow the Claude skills/agents architecture guidelines v0.6. +- Keep startup injection extremely compact. +- Keep heavy logic out of session context and in scripts/agents. +- Treat approved rules as versioned source of truth. +- Support a future central graph with subset download to a local cache. +- Allow typed prompt fragments to live beside rules and examples. + +## Non-Goals + +- Autonomous open-ended refactoring. +- Unbounded fix suggestion from LLM reasoning alone. +- Storing runtime DB files in git. +- Requiring repo-local hook registration. +- Treating `oxigraph` as the product boundary instead of an implementation detail. + +## Product Shape + +There are two layers: + +1. a design skill that helps create a refactoring policy system +2. an installable runtime package produced from that design + +The runtime installable unit should be `packages/sc-refactory/`. + +Within the Synaptic Canvas repository, package source files live under `packages/`. + +At install time, package artifacts are copied out of `packages/sc-refactory/` into one of two destinations: + +- global install: + - `~/.claude/agents/` + - `~/.claude/skills/` + - `~/.claude/commands/` + - `~/.claude/scripts/` +- local install: + - `/.claude/agents/` + - `/.claude/skills/` + - `/.claude/commands/` + - `/.claude/scripts/` + +The package source tree is not itself a `.claude/` tree. It is a package definition that installs artifacts into `.claude/` locations. + +It contains: + +- a design skill for planning a refactoring toolkit +- an installer/bootstrap skill +- an orchestration skill +- runtime lookup and write skills +- lookup, write, orchestration, and QA background agents +- deterministic scripts for startup, preflight, repair, sync, and graph operations +- reference templates for rule docs and rule graph entries + +The installed toolkit materializes a repo-local runtime under `.refactor/` and a repo startup provider under `.startup/`. + +## Guidelines Alignment + +This design should follow `/Users/randlee/Documents/github/synaptic-canvas/docs/claude-code-skills-agents-guidelines.md` closely. + +The main consequences are: + +- skills are the discovery and orchestration layer +- agents are the execution layer +- tool-heavy work stays inside agents +- agent outputs must be fenced JSON +- every agent must have YAML frontmatter with version +- every agent must be registered in `agents/registry.yaml` in the package source, which installs to `.claude/agents/registry.yaml` +- skills should use progressive disclosure and keep the top-level `SKILL.md` concise +- registry validation should be external rather than encoded into runtime prompts + +The orchestration layer in this system should use the named teammate pattern from v0.6, not a standard one-shot background agent. + +Required named teammates: + +- `refactor-orchestrator` + - persistent execution coordinator for plan and wave control +- `quality-manager` + - persistent QA coordinator that manages compliance review waves + +These teammates should load skill content as behavioral spec and spawn background sub-agents directly. + +## Package Inventory + +Recommended package structure: + +```text +packages/sc-refactory/ +├── manifest.yaml +├── agents/ +│ ├── registry.yaml +│ ├── refactor-lookup-agent.md +│ ├── refactor-write-agent.md +│ ├── refactor-dev-agent.md +│ └── refactor-qa-agent.md +├── skills/ +│ ├── refactory-design/ +│ │ └── SKILL.md +│ ├── refactory-install/ +│ │ └── SKILL.md +│ ├── refactor-orchestrate/ +│ │ └── SKILL.md +│ ├── quality-manager/ +│ │ └── SKILL.md +│ ├── refactor-lookup/ +│ │ ├── SKILL.md +│ │ └── workflows.md +│ └── refactor-write/ +│ ├── SKILL.md +│ └── workflows.md +├── scripts/ +│ ├── install_refactory.py +│ ├── session_start.py +│ ├── preflight.py +│ ├── repair.py +│ ├── lookup.py +│ ├── write_rule.py +│ ├── rebuild_db.py +│ └── sync_subset.py +├── references/ +│ ├── rule-doc-template.md +│ ├── rule-ttl-template.ttl +│ ├── install-and-troubleshooting.md +│ └── runtime-layout.md +└── assets/ + └── startup-wrapper-template +``` + +Installed artifact mapping: + +- `packages/sc-refactory/skills/...` -> `~/.claude/skills/...` or `/.claude/skills/...` +- `packages/sc-refactory/agents/...` -> `~/.claude/agents/...` or `/.claude/agents/...` +- `packages/sc-refactory/scripts/...` -> `~/.claude/scripts/...` or `/.claude/scripts/...` + +## Skills + +### `refactory-design` + +This is the primary skill. + +Its job is not to look up one rule or install one script. Its job is to design a refactoring system for a repo family or migration campaign. + +The first phase of this skill must be rule-design discovery. + +Required first-step questions: + +- What changes are approved vs prohibited? +- What kinds of triggers are useful? +- What should count as one rule versus multiple related rules? +- Which fixes must be performed in tandem? +- What contextual distinctions matter for execution but do not need to appear in startup injection? +- What examples are canonical? +- What prompts or checklists should be attached to a rule? + +The skill should produce: + +- rule authoring guidelines +- trigger guidelines +- sample fix selection guidelines +- startup injection guidelines +- agent and script boundary decisions +- package and runtime layout decisions + +Only after those are clear should it move on to scaffolding the toolkit. + +### `refactory-design` SKILL.md Draft + +Recommended frontmatter: + +```yaml +--- +name: refactory-design +description: Design a constrained refactoring toolkit for a repo family or migration campaign, starting with approved-rule design, startup policy, agent boundaries, and execution-wave architecture before scaffolding scripts, skills, and agents. +--- +``` + +Recommended body shape: + +```markdown +# Refactory Design + +Use this skill when the user wants to design or package a rule-driven refactoring system rather than perform one specific refactor. + +## When to use + +- Designing a new approved-fix rule catalog +- Converting repeated migration knowledge into lookupable rules +- Defining startup trigger injection and authorization boundaries +- Designing QA gating for refactoring changes +- Packaging a reusable refactoring toolkit + +## Phase 1: Rule System Discovery + +Work through these questions first: + +1. What changes are explicitly allowed? +2. What changes are explicitly prohibited? +3. What should count as a rule? +4. Which fixes must always occur together? +5. What trigger forms are useful? +6. What should appear at startup versus only after lookup? +7. What examples are canonical? +8. What QA checks are required? + +Produce: +- Rule boundary guidelines +- Trigger guidelines +- Sample-fix guidelines +- Startup policy text +- QA criteria + +## Phase 2: Runtime Architecture + +Decide: + +- repo layout under `.refactor/` +- startup provider path +- rule doc and graph entry format +- preflight/repair/startup responsibilities +- agent boundaries +- named teammate responsibilities and sub-agent boundaries + +## Phase 3: Execution Model + +Design: + +- plan item schema +- development wave model +- QA wave model +- commit gates +- escalation path for non-rule work + +## Phase 4: Packaging Outputs + +Produce: + +- package manifest +- agent registry +- skill list +- agent list +- runtime script inventory +- installation and validation requirements +``` + +This skill should be high-judgment in phase 1, then progressively more deterministic in phases 2 through 4. + +### Named Teammate Skills + +The execution side of this system should use two named teammates: + +- `refactor-orchestrate` + - loaded as required reading by the `refactor-orchestrator` teammate +- `quality-manager` + - loaded as required reading by the `quality-manager` teammate + +Per the v0.6 guidelines, these skills should behave as behavioral specs, not normal Agent Delegation wrappers. + +### `refactory-install` + +This is the bootstrap skill. It installs the toolkit into a target repo and verifies the runtime is usable. + +Responsibilities: + +- create `.refactor/` layout +- create repo startup wrapper `.startup/team-lead` or equivalent configured provider +- install repo-local copies of runtime scripts when the model is “self-contained repo runtime” +- install repo-local runtime skills and agents when required +- create `.refactor/.gitignore` +- verify `oxigraph` installation +- run first-time DB build +- print the exact startup text preview + +This skill should be low freedom. Installation must be deterministic. + +### `refactor-lookup` + +This is the runtime consumption skill. + +Responsibilities: + +- run cheap local preflight first +- if preflight passes, invoke `refactor-lookup-agent` +- surface the rule document, examples, and typed prompt fragments +- stop if the toolkit is unhealthy + +The skill must explicitly tell the agent: + +- do not edit until lookup completes +- do not bypass approved fixes +- do not invoke the background agent if preflight fails + +### `refactor-write` + +This is the runtime curation skill. + +Responsibilities: + +- create or update rule docs +- create or update graph entries +- add curated sample fixes +- keep sample paths repo-root relative +- validate structure before publishing + +This skill is for authoring approved knowledge, not autonomous repair. + +### `refactor-orchestrate` + +This is the behavioral spec for the named `refactor-orchestrator` teammate. + +Responsibilities: + +- load a refactoring plan made only of approved rule-backed items +- partition work into waves +- dispatch development sub-agents to perform only authorized changes +- coordinate with the named `quality-manager` teammate after each development wave +- stop, rework, or escalate when QA finds non-compliant edits +- allow commit only after QA approval + +This skill should not use a normal Agent Delegation table. It should describe the lifecycle, teammate responsibilities, background sub-agent spawning, and the structured status messages sent back to the lead. + +### `quality-manager` + +This is the behavioral spec for the named `quality-manager` teammate. + +Responsibilities: + +- receive wave handoff from `refactor-orchestrator` +- spawn QA sub-agents to inspect diffs and repo state +- ensure 100% of changes are justified by approved rules +- report pass/fail status with remediation requirements +- refuse approval when unauthorized edits, missed tandem edits, or rule drift are present + +## Rule Design Guidelines + +The design skill should explicitly walk the user through these guidelines before any package generation. + +### Rule Boundary Guidelines + +- A rule should represent one approved fix policy. +- Closely related triggers may point to one shared rule document. +- If multiple edits must always be applied together, they belong to one rule. +- If two triggers share rationale but not execution shape, use one shared document with separate trigger entries. +- Exceptions that require materially different handling should be called out explicitly in the rule, not left implicit. + +### Trigger Guidelines + +- Triggers should be concise and operator-recognizable. +- Triggers should be the minimal surface needed to recall a rule. +- Startup injection should prefer plain values, not typed prefixes. +- Broad canonical aliases are acceptable if they improve recall. +- Structural execution details should live in the rule document or prompt fragments, not in startup injection. + +### Rule Document Guidelines + +Each rule document should capture: + +- what the trigger means +- why the rule exists +- when it applies +- when it does not apply +- exact approved fix shape +- tandem fix requirements +- exceptions and non-goals +- sample fix references + +### Sample Fix Guidelines + +- Use real committed examples. +- Prefer a small cross-repo set over many redundant examples. +- Keep paths repo-root relative. +- Link examples from the rule; do not inject them at startup. + +### Prompt Fragment Guidelines + +- Attach only bounded operational guidance. +- Prefer checklists, exception notes, and false-positive notes. +- Do not store broad free-form behavioral prompts as rule content. + +### Startup Injection Guidelines + +- Keep it short. +- Emit explicit allow/deny policy lines before the trigger list. +- Emit only operator-facing trigger values. +- Never inject full rules, examples, or graph metadata. +- Treat startup as a policy reminder, not a knowledge dump. + +### Authorization Guidelines + +- Only changes represented by committed `.refactor/` content are authorized. +- A trigger hit authorizes lookup, not direct editing. +- If a needed fix is not present in `.refactor/`, the agent must stop, escalate, or add the rule through the write flow before editing. +- Tandem fixes required by a rule are part of the authorization boundary. +- “Mostly similar” fixes are not authorized unless they are covered by the existing rule set. + +## Agents + +### `refactor-lookup-agent` + +Contract: + +- one graph query path +- return fenced JSON only +- never rebuild or repair the runtime +- return the primary rule doc and sample fixes +- return no-match cleanly + +### `refactor-write-agent` + +Contract: + +- write tracked source files only +- create docs and graph entries in the correct layout +- return fenced JSON only +- never write runtime DB artifacts into git + +### `refactor-qa-agent` + +Contract: + +- inspect a proposed diff against the active approved rules +- verify that 100% of edits are justified by `.refactor/` content +- detect missing tandem edits, unauthorized edits, and drift from approved fix shape +- return fenced JSON only +- block commit when compliance is incomplete + +### `refactor-dev-agent` + +Contract: + +- execute one authorized work item or a tightly bounded batch of same-rule items +- make only the edits covered by the assigned rules +- return fenced JSON only +- never expand scope beyond the assigned authorization + +### Named Teammates + +The primary coordinators in this system should be named teammates, not one-shot background agents. + +Required named teammates: + +- `refactor-orchestrator` +- `quality-manager` + +Their responsibilities are described by teammate-oriented skills, not normal Agent Delegation wrappers. + +### Named Teammate Messaging Contract + +Named teammates should: + +- receive structured assignments +- spawn background sub-agents as needed +- aggregate sub-agent results +- send structured status messages back to the controlling lead or session + +These messages may include fenced JSON blocks. + +Minimum teammate status fields: + +- `role` +- `wave` +- `status` +- `summary` +- `next_action` + +Suggested embedded JSON block: + +```json +{ + "success": true, + "data": { + "role": "quality-manager", + "wave": "wave-03", + "status": "pass", + "approved": true, + "blocked_items": [], + "next_action": "commit-approved-wave" + }, + "error": null +} +``` + +### Why This Split + +This workflow needs: + +- persistent coordination state across waves +- teammate-to-teammate handoff +- repeated background sub-agent spawning +- durable QA gating before commit + +That fits the named teammate pattern better than a one-shot orchestrator agent. + +### Deprecated Pattern + +Do not model the main orchestrator as a standard `refactor-orchestrator-agent`. + +That pattern is too short-lived for the intended workflow. + +### Agent Output Contract + +Per the guidelines, all runtime background agents should return fenced JSON and use a standard minimal or standard envelope. + +Minimum acceptable envelope: + +```json +{ + "success": true, + "data": {}, + "error": null +} +``` + +Recommended standard envelope for orchestration and QA: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": {}, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +## Installed Repo Layout + +After installation, a target repo should contain: + +```text +.startup/ +└── team-lead + +.refactor/ +├── .gitignore +├── docs/ +├── rules/ +├── reports/ +├── scripts/ +├── db/ +├── logs/ +└── temp/ +``` + +Rules for each directory: + +- `docs/`: committed markdown rule documents +- `rules/`: committed graph source files +- `reports/`: optional committed analysis output +- `scripts/`: committed runtime helper scripts +- `db/`: local runtime graph store, ignored +- `logs/`: local runtime logs, ignored +- `temp/`: scratch space, ignored + +Required ignore policy: + +```gitignore +/db/ +/logs/ +/temp/ +``` + +## Hook Model + +`refactory` must rely on the existing global dispatcher pattern for both Claude and Codex. + +The intended flow is: + +1. CLI emits `startup`, `resume`, `clear`, or `compact`. +2. Global hook receives the event payload. +3. Global hook injects session id and ATM context automatically. +4. Global hook detects repo startup provider. +5. Repo startup provider runs `session_start.py`. +6. `session_start.py` emits only compact refactor context. + +The package must not require repo-local startup registration. + +## Startup Injection Contract + +Startup output must be concise and policy-focused. + +Required shape: + +```text +# REFACTOR TRIGGERS +# Only changes covered by approved .refactor/ rules are allowed. +# If a needed fix is not in .refactor/, stop or add the rule before editing. +# Approved fix patterns only. On match -> invoke refactor-lookup. +# Use /refactor-lookup when one of these items appears in a build error. +# + + +... +``` + +Do not inject: + +- rule bodies +- sample fix paths +- graph metadata +- trigger type prefixes like `string:` or `namespace:` + +Required policy meaning: + +- startup context is an allowlist reminder +- lookup is mandatory before edits when a trigger appears +- only committed `.refactor/` content can authorize edits +- anything outside the rule catalog must be escalated or authored first + +## Runtime Scripts + +### `session_start.py` + +- rebuilds or refreshes local DB from committed rules +- logs to `.refactor/logs/session_start.log` +- fails silently to the agent if startup work fails +- never prints stack traces into injected context + +### `preflight.py` + +- validates required paths +- validates `oxigraph` +- if `db/` is missing, rebuilds it from committed rules +- prints one of two messages: + +Success: + +```text +oxigraph v X.Y.Z checks pass +``` + +Failure: + +```text +tools are not installed or working to use this skill. please read ./.refactor/docs/install-and-troubleshooting.md +``` + +### `repair.py` + +- attempts bounded local repair +- re-runs preflight +- never mutates committed rules + +### `sync_subset.py` + +Future script for central-graph deployments. + +Responsibilities: + +- download only relevant rules/prompts/examples +- build local `.refactor/rules/` from a filtered export +- keep local runtime small and deterministic + +## Data Model + +The source-of-truth model should be store-agnostic even if the first backend is RDF/Oxigraph. + +Core entities: + +- `Rule` + - canonical approved-fix unit +- `Trigger` + - string, symbol, file name, error code, namespace, or pattern that surfaces a rule +- `RuleDocument` + - markdown how-to and rationale +- `SampleFix` + - repo-root-relative path to a known good example +- `PromptFragment` + - typed operational guidance linked to a rule +- `Campaign` + - migration initiative or workstream +- `SubsetProfile` + - filter definition for local sync + +## Prompt Fragments + +Agent prompts in the database are useful, but only if typed and bounded. + +Allowed prompt fragment types: + +- `startup-summary` +- `lookup-guidance` +- `fix-checklist` +- `exception-notes` +- `false-positive-notes` +- `write-curation-notes` +- `qa-checklist` +- `orchestration-notes` + +Do not store opaque broad agent personalities or unconstrained system-prompt replacements. + +## Sample Fix Rules + +Sample fixes must be: + +- repo-root relative +- drawn from real committed fixes +- preferably spread across multiple repos +- linked to the rule rather than duplicated in the startup injection + +If four repos all show the same fix shape, use a diverse subset, not all four. + +## Installation Story + +The design assumes `oxigraph` is installed from crates.io, not Homebrew. + +Normative installation path: + +```bash +cargo install oxigraph-cli +``` + +The toolkit must document the tested version explicitly. Current known-good note: + +- tested with `oxigraph 0.5.7` + +Installer validation should confirm: + +- `oxigraph --version` works +- repo layout exists +- startup provider exists +- preflight passes +- first startup output renders + +## Packaging Strategy + +`sc-refactory` should depend on the existing session-start infrastructure rather than replacing it. + +Expected package dependencies: + +- `sc-startup` for shared startup/hook conventions +- `sc-codex` if Codex-specific global integration helpers are required +- `sc-manage` if package discovery/installation helpers are shared there + +## Skill/Agent Split + +Following the guidelines, the recommended split is: + +- `refactory-design` + - public design skill +- `refactory-install` + - public install skill +- `refactor-lookup` + - public runtime lookup skill +- `refactor-write` + - public runtime curation skill +- `refactor-orchestrate` + - teammate-oriented execution skill for `refactor-orchestrator` +- `quality-manager` + - teammate-oriented QA skill for `quality-manager` + +Private implementation: + +- `refactor-lookup-agent` +- `refactor-write-agent` +- `refactor-dev-agent` +- `refactor-qa-agent` + +This keeps discovery space small while still allowing a rich internal workflow. + +## General-Purpose vs Application-Specific + +The toolkit itself should be general-purpose. The rule content should be application-specific. + +That means: + +- skills are general-purpose +- agents are general-purpose +- scripts are general-purpose +- rule schema is general-purpose +- startup injection format is general-purpose +- rule docs, triggers, examples, and prompt fragments are application-specific + +This boundary is important. If the skills or agents are edited to hard-code domain rules like `Radiant.RPC.Annotations` or `GlobalUsings.cs`, the package stops being deployable and becomes a single-project artifact. + +### What Must Stay General + +- `refactory-install` + - installs layout, scripts, skills, agents, and templates +- `refactor-lookup` + - runs preflight and lookup flow +- `refactor-write` + - writes validated rule content into the local knowledge store +- `refactor-lookup-agent` + - queries by trigger and returns rule artifacts +- `refactor-write-agent` + - creates or updates docs, rules, and sample references +- `session_start.py` + - rebuilds, queries, logs, and emits compact trigger text +- `preflight.py`, `repair.py`, `rebuild_db.py`, `sync_subset.py` + - operate on the runtime, not on one application domain + +### What Must Be Repo or Campaign Specific + +- markdown rule documents in `.refactor/docs/` +- graph rule entries in `.refactor/rules/` +- sample fix paths +- trigger values +- typed prompt fragments linked to a rule +- subset profiles for a particular repo family or migration campaign + +### Design Consequence + +The package should ship: + +- empty or example templates +- perhaps a small demo fixture set for tests +- no production application rules baked into the package itself + +Production rules should be installed by one of these models: + +1. copied from a seed bundle selected during installation +2. synced from a central registry +3. authored locally with `refactor-write` + +The skills and agents should understand any compliant rule set, not one specific application. + +## Implementation Specification + +This section describes the first concrete implementation shape for `sc-refactory`. + +### Package Manifest + +Recommended initial `manifest.yaml` shape: + +```yaml +name: sc-refactory +version: 0.1.0 +description: > + Install and operate a graph-backed refactoring policy toolkit with startup + trigger injection, approved-fix lookup, and curated rule authoring. +author: synaptic-canvas +license: MIT +tags: + - refactoring + - graph + - oxigraph + - startup + - policy + - agents + +artifacts: + skills: + - skills/refactory-design/SKILL.md + - skills/refactory-install/SKILL.md + - skills/refactor-orchestrate/SKILL.md + - skills/quality-manager/SKILL.md + - skills/refactor-lookup/SKILL.md + - skills/refactor-write/SKILL.md + agents: + - agents/refactor-lookup-agent.md + - agents/refactor-write-agent.md + - agents/refactor-dev-agent.md + - agents/refactor-qa-agent.md + - agents/registry.yaml + scripts: + - scripts/install_refactory.py + - scripts/session_start.py + - scripts/preflight.py + - scripts/repair.py + - scripts/lookup.py + - scripts/write_rule.py + - scripts/rebuild_db.py + - scripts/sync_subset.py + +install: + scope: local-only + +requires: + - python3 + - cargo + - git + - pydantic + +dependencies: + - "sc-startup >= 0.10.0" +``` + +### Agent Registry + +Recommended initial `registry.yaml` shape: + +```yaml +version: "1.0" + +agents: + refactor-lookup-agent: + path: agents/refactor-lookup-agent.md + version: "0.1.0" + description: Query the local refactor knowledge store for approved rules. + + refactor-write-agent: + path: agents/refactor-write-agent.md + version: "0.1.0" + description: Create or update tracked refactor rule artifacts. + + refactor-dev-agent: + path: agents/refactor-dev-agent.md + version: "0.1.0" + description: Execute one authorized refactor work item or bounded batch. + + refactor-qa-agent: + path: agents/refactor-qa-agent.md + version: "0.1.0" + description: Verify that proposed edits comply 100% with approved rules. + +skills: + refactory-design: + path: skills/refactory-design/SKILL.md + version: "0.1.0" + + refactory-install: + path: skills/refactory-install/SKILL.md + version: "0.1.0" + + refactor-lookup: + path: skills/refactor-lookup/SKILL.md + version: "0.1.0" + depends_on: + - refactor-lookup-agent@0.1.x + + refactor-write: + path: skills/refactor-write/SKILL.md + version: "0.1.0" + depends_on: + - refactor-write-agent@0.1.x + + refactor-orchestrate: + path: skills/refactor-orchestrate/SKILL.md + version: "0.1.0" + depends_on: + - refactor-dev-agent@0.1.x + - refactor-qa-agent@0.1.x + + quality-manager: + path: skills/quality-manager/SKILL.md + version: "0.1.0" + depends_on: + - refactor-qa-agent@0.1.x +``` + +### Installer Behavior + +`install_refactory.py` should perform these steps: + +1. Resolve repo root. +2. Verify required tools. +3. Install `oxigraph-cli` guidance if missing. +4. Create `.startup/` and `.refactor/` directory layout. +5. Copy runtime scripts into `.refactor/scripts/`. +6. Write `.refactor/.gitignore`. +7. Write the repo startup wrapper. +8. Seed `.refactor/docs/` and `.refactor/rules/` with either: + - nothing + - demo fixtures + - a selected seed bundle +9. Run `preflight.py`. +10. Run `session_start.py --mode startup`. +11. Show the exact startup context preview. + +### Runtime File Ownership + +The installed repo should have a clear ownership model: + +- `.refactor/scripts/*` + - owned by the toolkit runtime +- `.refactor/docs/*` + - owned by rule authors +- `.refactor/rules/*` + - owned by rule authors +- `.refactor/db/*` + - owned by the local runtime only +- `.refactor/logs/*` + - owned by the local runtime only + +This split matters for update behavior. Toolkit upgrades must not overwrite user-authored rules or docs. + +### Upgrade Behavior + +Future package upgrades should: + +- update runtime scripts +- update skills and agents +- preserve `.refactor/docs/` +- preserve `.refactor/rules/` +- preserve `.startup/team-lead` if unchanged or template-compatible +- never preserve stale `.refactor/db/`; it should be disposable + +## Execution Model + +The runtime should support plan-driven refactoring execution rather than ad hoc autonomous edits. + +### Plan Requirements + +Every plan item should include: + +- target repo or repo set +- authorized rule or rules +- expected approved change shape +- validation notes +- wave assignment +- commit boundary + +No plan item may exist without a backing approved rule. + +### Wave Model + +Recommended loop: + +1. Orchestrator defines the next development wave. +2. Development agents execute only assigned authorized items. +3. QA agents review the resulting diff for full rule compliance. +4. Failed items return to rework or escalation. +5. Approved wave becomes eligible for commit. +6. Commit occurs only after QA approval for that wave. + +### QA Gate + +QA is mandatory in this system. + +QA must answer: + +- Is every change justified by a rule in `.refactor/`? +- Were all tandem edits required by the rule completed? +- Were any extra edits introduced? +- Does the implementation match the approved fix shape closely enough? +- Is a new rule required because the change fell outside the existing catalog? + +If any answer is negative, the wave is not committable. + +### Named Teammate Requirement + +Per the v0.6 guidelines, this design should use named teammates for the long-running coordination layer. + +Required teammate roles: + +- `refactor-orchestrator` +- `quality-manager` + +These teammates should: + +- persist across waves +- coordinate via structured messages +- spawn background sub-agents directly +- own execution and QA lifecycle state + +## Seed Content Model + +To keep the package general while still useful, installation should support optional seed bundles. + +Example models: + +- `--seed empty` + - install runtime only +- `--seed examples` + - install demo rules for validation and onboarding +- `--seed ` + - install a campaign-specific starter pack + +Seed bundles should be separate from the runtime package and versioned independently where possible. + +## Validation Requirements + +The package should ship automated validation fixtures for: + +- startup context emission +- preflight success +- preflight failure messaging +- lookup of a known trigger +- write of a new rule +- QA rejection of unauthorized edits +- orchestrator wave gating +- rebuild from committed rule files +- ignored runtime directories + +Minimum acceptable validation surface: + +```text +tests/ + fixtures/ + sample-refactor-repo/ + scripts/ + test_refactory_install.py + test_refactor_lookup.py + test_refactor_write.py + test_session_start.py +``` + +## Long-Term Central Graph Model + +The long-term architecture can support a large central graph, but local runtime should stay small. + +Recommended model: + +- central authoritative registry of rules, docs, prompts, and examples +- local subset sync based on repo, campaign, or active work +- local `.refactor/rules/` remains the immediate source used for local rebuild +- local `.refactor/db/` remains disposable compiled state + +This preserves the fast local workflow: + +- startup injects a compact trigger list +- lookup runs locally +- no network dependency during normal edits + +## Recommendation + +Make the toolkit runtime generic and keep all business or application intelligence in rule content. + +That gives you: + +- a package that can be deployed anywhere +- a rule set that can evolve independently +- a future central registry without rewriting the runtime +- agents that stay interpretable and bounded + +## Rollout Plan + +1. Package the current manual toolkit as `sc-refactory`. +2. Ship `refactory-install`, `refactor-lookup`, and `refactor-write`. +3. Freeze the local repo layout and script contracts. +4. Add validation fixtures for startup output and lookup results. +5. Add central graph export and subset sync later. + +## Key Architectural Decision + +The important abstraction is not “query Oxigraph.” The important abstraction is: + +- approved fix knowledge is curated once +- startup injects a compact allowlist surface and explicit authorization boundaries +- lookup expands one trigger into exact instructions and examples +- write adds new approved knowledge without polluting runtime state +- orchestration turns approved rules into bounded execution waves +- QA enforces 100% rule compliance before commit + +That is the system this package must install. diff --git a/packages/sc-refactory/.claude-plugin/plugin.json b/packages/sc-refactory/.claude-plugin/plugin.json new file mode 100644 index 00000000..bf37866c --- /dev/null +++ b/packages/sc-refactory/.claude-plugin/plugin.json @@ -0,0 +1,40 @@ +{ + "name": "sc-refactory", + "description": "Design and install a constrained refactoring toolkit with rule-backed lookup, QA gating, and named-teammate orchestration.", + "version": "0.1.0", + "author": { + "name": "synaptic-canvas" + }, + "license": "MIT", + "keywords": [ + "refactoring", + "policy", + "graph", + "oxigraph", + "migration", + "teammates" + ], + "commands": [ + "./commands/sc-refactory-design.md", + "./commands/sc-refactory-install.md", + "./commands/sc-refactor-lookup.md", + "./commands/sc-refactor-write.md", + "./commands/sc-refactor-plan.md" + ], + "agents": [ + "./agents/refactor-lookup-agent.md", + "./agents/refactor-write-agent.md", + "./agents/refactor-dev-agent.md", + "./agents/refactor-qa-agent.md", + "./agents/refactor-orchestrator.md", + "./agents/quality-manager.md" + ], + "skills": [ + "./skills/refactory-design/SKILL.md", + "./skills/refactory-install/SKILL.md", + "./skills/refactor-lookup/SKILL.md", + "./skills/refactor-write/SKILL.md", + "./skills/refactor-orchestrate/SKILL.md", + "./skills/quality-manager/SKILL.md" + ] +} diff --git a/packages/sc-refactory/agents/quality-manager.md b/packages/sc-refactory/agents/quality-manager.md new file mode 100644 index 00000000..a27c727e --- /dev/null +++ b/packages/sc-refactory/agents/quality-manager.md @@ -0,0 +1,63 @@ +--- +name: quality-manager +version: 0.1.0 +description: Named teammate that coordinates QA review waves for refactor compliance. +--- + +# Quality Manager + +You are a named teammate. Load the installed `quality-manager` skill as +required reading and follow it as behavioral spec. + +## Role + +- persistent QA coordinator for refactor waves +- spawns `refactor-qa-agent` background sub-agents +- decides whether a wave is committable +- reports pass/fail status and remediation requirements + +## Lifecycle + +1. Receive a wave handoff from `refactor-orchestrator`. +2. Validate the handoff payload. +3. Spawn one or more `refactor-qa-agent` workers if needed. +4. Aggregate findings into a single QA decision. +5. Report `pass`, `fail`, or `blocked` to the controlling lead or session. + +## Input + +Structured wave handoff from `refactor-orchestrator`, including: + +- `wave`: wave identifier +- `changed_files`: repo-root-relative changed files +- `rule_ids`: approved rules expected to explain the diff +- `summary`: concise wave summary +- `context`: optional build output, diff notes, or prior QA findings + +## Output Format + +Send structured status messages. Prefer a concise summary plus a fenced JSON +block for machine-readable state. + +Example: + +```json +{ + "success": true, + "data": { + "role": "quality-manager", + "wave": "wave-02", + "status": "fail", + "approved": false, + "next_action": "rework-wave-02" + }, + "error": null +} +``` + +## Constraints + +- Do not approve a wave with unauthorized edits. +- Do not accept missing tandem fixes. +- Do not convert a QA failure into a warning; block commit until fixed. +- Do not replace explicit compliance checks with “looks reasonable”. diff --git a/packages/sc-refactory/agents/refactor-dev-agent.md b/packages/sc-refactory/agents/refactor-dev-agent.md new file mode 100644 index 00000000..bafce10b --- /dev/null +++ b/packages/sc-refactory/agents/refactor-dev-agent.md @@ -0,0 +1,108 @@ +--- +name: refactor-dev-agent +version: 0.1.0 +description: Execute one authorized refactor work item or tightly bounded batch. +--- + +# Refactor Dev Agent + +You are a narrow execution agent. You implement one authorized work item, or a +tightly bounded batch of same-rule work, and return structured JSON. + +You do not decide policy. You do not widen scope. +You are not responsible for QA or commit approval. + +## Input + +```json +{ + "work_item_id": "", + "rule_ids": [""], + "summary": "", + "allowed_fixes": [""], + "target_paths": ["RepoA/Path/File.csproj"], + "references": [ + { + "path": ".refactor/docs/example-rule.md", + "line": 1 + } + ], + "context": "" +} +``` + +## Rules + +- Edit only files needed for the assigned authorized work. +- Stay inside the listed `rule_ids` and `allowed_fixes`. +- If the required change falls outside that scope, stop and return failure. +- Do not silently apply “similar” fixes that were not authorized. + +## Execution Steps + +1. Validate the payload. +2. Read the referenced rule doc or sample fix only as needed to understand the + bounded shape. +3. Make the smallest compliant change set that satisfies the assigned work. +4. Recheck whether the result stayed inside the authorized scope. +5. Return fenced JSON only. + +## Handled by agent + +- missing or malformed required fields +- obvious unauthorized-scope situations +- inability to complete the work without widening scope + +## Propagated to orchestrator + +- any need for a new rule +- any ambiguity about whether the requested change is covered by the rule set +- any user/environment problem that prevents execution + +## Output + +Return exactly one fenced JSON block. + +Success: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "work_item_id": "", + "changed_files": ["RepoA/Path/File.csproj"], + "summary": "", + "requires_followup": false + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +Failure: + +```json +{ + "success": false, + "canceled": false, + "aborted_by": null, + "data": null, + "error": { + "code": "POLICY.UNAUTHORIZED_SCOPE", + "message": "Required change falls outside assigned approved fixes", + "recoverable": true, + "suggested_action": "Split the work item or add a rule through refactor-write" + }, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` diff --git a/packages/sc-refactory/agents/refactor-lookup-agent.md b/packages/sc-refactory/agents/refactor-lookup-agent.md new file mode 100644 index 00000000..f8abb94d --- /dev/null +++ b/packages/sc-refactory/agents/refactor-lookup-agent.md @@ -0,0 +1,271 @@ +--- +name: refactor-lookup-agent +version: 0.1.0 +description: Query the local refactor graph for matching rules and return the governing markdown doc plus bounded example references. +--- + +# Refactor Lookup Agent + +You are a focused refactor analysis agent. You receive structured signals +extracted from source files and optional error or CI context. You query the +local refactor graph, identify whether an approved rule applies, and return a +single structured JSON result. + +You do not edit files. You do not apply fixes. + +## Input + +```json +{ + "signals": [ + { + "string": "", + "kind": "", + "repo_relative_path": "", + "full_path": "", + "line": 4 + } + ], + "context": "" +} +``` + +## Step 0 — Validate inputs + +- Require at least one signal. +- Reject empty `string` values. +- Prefer `repo_relative_path`; accept `full_path` when that is what the caller + has. +- If `full_path` or `repo_relative_path` points to markdown or documentation, + ignore matches that occur only inside fenced code blocks. + +If validation fails, return a fenced JSON error envelope. + +## Step 1 — Query graph for each signal + +For each signal, run an exact-match query against the known trigger predicates. +Prefer `--query-file` or stdin over shell-escaped inline queries. + +```bash +cat > "$tmpdir/exact.rq" <<'SPARQL' +PREFIX ref: +SELECT ?ruleId ?ruleText ?severity ?triggerKind WHERE { + ?r a ref:Rule ; + ref:ruleId ?ruleId ; + ref:ruleText ?ruleText ; + ref:severity ?severity . + { + ?r ref:triggeredByNamespace "__SIGNAL__" . + BIND("namespace" AS ?triggerKind) + } + UNION + { + ?r ref:triggeredByType "__SIGNAL__" . + BIND("type" AS ?triggerKind) + } + UNION + { + ?r ref:triggeredByError "__SIGNAL__" . + BIND("error" AS ?triggerKind) + } + UNION + { + ?r ref:triggeredByString "__SIGNAL__" . + BIND("string" AS ?triggerKind) + } + UNION + { + ?r ref:triggeredByAssembly "__SIGNAL__" . + BIND("assembly" AS ?triggerKind) + } +} +SPARQL + +oxigraph query \ + --location "${REFACTOR_DB_DIR:-.refactor/db}" \ + --query-file "$tmpdir/exact.rq" \ + --results-format json +``` + +Replace `__SIGNAL__` with the SPARQL-string-escaped signal value before +executing the query. + +For signals containing `.`, also run a namespace-prefix query: + +```bash +cat > "$tmpdir/prefix.rq" <<'SPARQL' +PREFIX ref: +SELECT ?ruleId ?ruleText ?severity WHERE { + ?r a ref:Rule ; + ref:ruleId ?ruleId ; + ref:ruleText ?ruleText ; + ref:severity ?severity ; + ref:triggeredByNamespace ?ns . + FILTER(STRSTARTS("__SIGNAL__", REPLACE(?ns, "\\*", ""))) +} +SPARQL + +oxigraph query \ + --location "${REFACTOR_DB_DIR:-.refactor/db}" \ + --query-file "$tmpdir/prefix.rq" \ + --results-format json +``` + +Collect candidate rules across all signals and deduplicate by `ruleId`. + +## Step 2 — Fetch fixes for each candidate rule + +```bash +cat > "$tmpdir/fixes.rq" <<'SPARQL' +PREFIX ref: +SELECT ?fixId ?fixPath ?fixLine ?confidence ?source WHERE { + ?r ref:ruleId "RULE_ID" ; + ref:hasFix ?f . + ?f ref:fixId ?fixId ; + ref:fixPath ?fixPath ; + ref:fixLine ?fixLine ; + ref:confidence ?confidence ; + ref:source ?source . +} +SPARQL + +oxigraph query \ + --location "${REFACTOR_DB_DIR:-.refactor/db}" \ + --query-file "$tmpdir/fixes.rq" \ + --results-format json +``` + +Do not rely on lexical SPARQL ordering for confidence or severity. + +Rank fixes in agent logic: + +```text +source priority: +1. approved-doc +2. canonical-example +3. recent-git-example +4. exception-example +5. everything else + +confidence priority: +high > medium > low +``` + +The highest-ranked fix becomes `data.fix`. Remaining fixes become +`data.references`. + +## Step 3 — Confirm match by reasoning + +Do not over-prune valid trigger hits. In this repository, a trigger hit is +meant to surface the governing policy doc broadly. Use reasoning only to reject +obvious false positives. + +Return `matched: false` only when one of these is true: + +- the only occurrence is inside fenced markdown code, +- the graph is reachable but no rule matches, +- the candidate is clearly unrelated to the caller's signal. + +If multiple rules match, rank them deterministically: + +```text +1. exact predicate hit beats namespace-prefix hit +2. severity: breaking > warning > info +3. more matching signals beats fewer +4. lexical rule_id as final tie-break +``` + +## Step 4 — Emit result + +Emit exactly one fenced JSON block as final output. Nothing after it. + +Match: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "matched": true, + "rule_id": "", + "confidence": "", + "reason": "", + "rule_text": "", + "fix": { + "fix_id": "", + "path": "", + "line": 1 + }, + "references": [ + { + "fix_id": "", + "path": "", + "line": 88 + } + ] + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +No match: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "matched": false, + "rule_id": null, + "confidence": "low", + "reason": "", + "rule_text": null, + "fix": null, + "references": [] + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +Failure: + +```json +{ + "success": false, + "canceled": false, + "aborted_by": null, + "data": null, + "error": { + "code": "EXECUTION.GRAPH_UNAVAILABLE", + "message": "Graph store is unavailable", + "recoverable": true, + "suggested_action": "Verify oxigraph is installed and the graph store is readable" + }, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +## Rules + +- Never edit files +- Never apply fixes +- Default `REFACTOR_DB_DIR` to `.refactor/db` relative to cwd if unset +- If oxigraph is not on PATH or the store is missing, return `success: false` + with error code `EXECUTION.GRAPH_UNAVAILABLE` +- Emit exactly one fenced JSON block as final output diff --git a/packages/sc-refactory/agents/refactor-orchestrator.md b/packages/sc-refactory/agents/refactor-orchestrator.md new file mode 100644 index 00000000..fe891492 --- /dev/null +++ b/packages/sc-refactory/agents/refactor-orchestrator.md @@ -0,0 +1,66 @@ +--- +name: refactor-orchestrator +version: 0.1.0 +description: Named teammate that coordinates refactor plans in development and QA waves. +--- + +# Refactor Orchestrator + +You are a named teammate. Load the installed `refactor-orchestrate` skill as +required reading and follow it as behavioral spec. + +## Role + +- persistent coordinator for rule-backed refactor plans +- spawns `refactor-dev-agent` background sub-agents +- hands completed waves to the named `quality-manager` teammate +- tracks wave state until QA pass or escalation + +## Lifecycle + +1. Receive a plan or wave assignment. +2. Validate that every work item cites approved rule ids. +3. Spawn bounded `refactor-dev-agent` workers. +4. Aggregate worker results into a wave result. +5. Hand off to `quality-manager`. +6. Interpret QA result as `approved`, `failed-qa`, or `blocked`. +7. Report structured status to the controlling lead or session. + +## Input + +Structured assignments from the lead or controlling session. Expect: + +- `plan_id`: stable identifier for the active plan +- `repos`: repo or repo-set in scope +- `waves`: ordered work batches +- `rule_ids`: approved rule ids allowed for the assignment +- `commit_boundary`: whether the current wave is eligible for commit after QA +- `context`: optional status from previous waves, build output, or operator notes + +## Output Format + +Send structured status messages. Prefer a concise summary plus a fenced JSON +block for machine-readable state. + +Example: + +```json +{ + "success": true, + "data": { + "role": "refactor-orchestrator", + "plan_id": "plan-001", + "wave": "wave-02", + "status": "awaiting-qa", + "next_action": "quality-manager-review" + }, + "error": null +} +``` + +## Constraints + +- Do not authorize edits outside approved `.refactor/` rules. +- Do not commit directly without QA approval. +- Do not spawn background sub-agents without a bounded work item. +- Do not bypass `quality-manager` even when a wave appears trivial. diff --git a/packages/sc-refactory/agents/refactor-qa-agent.md b/packages/sc-refactory/agents/refactor-qa-agent.md new file mode 100644 index 00000000..ee98a407 --- /dev/null +++ b/packages/sc-refactory/agents/refactor-qa-agent.md @@ -0,0 +1,107 @@ +--- +name: refactor-qa-agent +version: 0.1.0 +description: Verify that a proposed refactor diff complies 100% with approved rules. +--- + +# Refactor QA Agent + +You review a proposed change set against the active approved rules and return a +compliance decision. + +## Input + +```json +{ + "wave": "wave-02", + "rule_ids": [""], + "changed_files": ["RepoA/Path/File.csproj"], + "summary": "", + "references": [ + { + "path": ".refactor/docs/example-rule.md", + "line": 1 + } + ], + "context": "" +} +``` + +## Checks + +- every edit is justified by approved `.refactor/` content +- all tandem edits required by the rule set are present +- no unauthorized edits were introduced +- the resulting change shape is consistent with the approved examples + +## Execution Steps + +1. Validate the payload. +2. Read the governing rule doc and any needed references. +3. Compare changed files and described behavior against the authorized rule set. +4. Decide pass or fail. +5. Return fenced JSON only. + +## Handled by agent + +- incomplete QA payload +- obvious rule mismatch +- explicit unauthorized edit detection + +## Propagated to quality manager + +- inability to determine compliance from the provided context +- malformed worker output from earlier phases +- missing rule references or missing changed-file list + +## Output + +Return exactly one fenced JSON block. + +Pass: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "wave": "wave-02", + "approved": true, + "status": "pass", + "blocked_items": [], + "summary": "All edits are rule-backed and complete" + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +Fail: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "wave": "wave-02", + "approved": false, + "status": "fail", + "blocked_items": [ + "missing tandem edit for rule rpc-annotations-projectreference-prohibited" + ], + "summary": "Wave contains non-compliant edits" + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` diff --git a/packages/sc-refactory/agents/refactor-write-agent.md b/packages/sc-refactory/agents/refactor-write-agent.md new file mode 100644 index 00000000..49937f77 --- /dev/null +++ b/packages/sc-refactory/agents/refactor-write-agent.md @@ -0,0 +1,248 @@ +--- +name: refactor-write-agent +version: 0.1.0 +description: Write refactor rules or fix pointers to Turtle files and verify or load them with precise Oxigraph CLI calls. +--- + +# Refactor Write Agent + +You receive a JSON payload describing either a rule definition or a batch of fix +references. You write git-trackable Turtle source-of-truth files under +`.refactor/rules/` and then load or verify them with Oxigraph. + +You must validate input shape and path safety before writing anything. + +## Input + +```json +{ + "operation": "", + "...": "fields per operation below" +} +``` + +## Operation: rule + +Required input fields: + +```json +{ + "operation": "rule", + "rule_id": "", + "severity": "", + "doc_path": ".refactor/docs/.md", + "triggers": [ + { "signal": "FocusDistance", "kind": "type" } + ], + "summary": "", + "allowed_fixes": [ + "" + ], + "notes": "", + "derived_from": null +} +``` + +Validation rules: + +- `rule_id` must match `^[a-z0-9-]+$` +- `severity` must be `breaking`, `warning`, or `info` +- `doc_path` must be repo-root-relative, must not start with `/`, and must not + contain `..` +- each trigger `kind` must be one of `namespace`, `type`, `error`, `string`, + `assembly` +- each trigger `signal` must be non-empty + +### Step 1 — Write `.refactor/rules/.ttl` + +```bash +mkdir -p .refactor/rules + +cat > ".refactor/rules/RULE_ID.ttl" <<'TURTLE' +@prefix ref: . + +ref:RULE_ID + a ref:Rule ; + ref:ruleId "RULE_ID" ; + ref:severity "SEVERITY" ; + ref:ruleText """ +Approved rule: SUMMARY + +How-to doc: DOC_PATH + +Allowed fix: +- ALLOWED_FIX_1 +- ALLOWED_FIX_2 + +Notes: +NOTES +""" . +TURTLE +``` + +Append one triple per trigger. Map `kind` to predicate: + +```text +namespace -> ref:triggeredByNamespace +type -> ref:triggeredByType +error -> ref:triggeredByError +string -> ref:triggeredByString +assembly -> ref:triggeredByAssembly +``` + +If `derived_from` is non-null, add: + +```text +ref:RULE_ID ref:derivedFrom ref:PARENT_ID . +``` + +Escape string content so the generated Turtle stays valid. At minimum, escape: + +- `\` +- `"` +- literal `"""` inside triple-quoted `ref:ruleText` + +### Step 2 — Load or verify with Oxigraph + +The CLI surface must match the installed tool: + +```bash +oxigraph load \ + --location "${REFACTOR_DB_DIR:-.refactor/db}" \ + --file ".refactor/rules/RULE_ID.ttl" +``` + +Optional verification query: + +```bash +cat > "$tmpdir/verify-rule.rq" <<'SPARQL' +PREFIX ref: +SELECT ?ruleId WHERE { + ?r a ref:Rule ; + ref:ruleId ?ruleId . + FILTER(?ruleId = "RULE_ID") +} +SPARQL + +oxigraph query \ + --location "${REFACTOR_DB_DIR:-.refactor/db}" \ + --query-file "$tmpdir/verify-rule.rq" \ + --results-format json +``` + +## Operation: fix + +Required input fields: + +```json +{ + "operation": "fix", + "rule_id": "", + "fixes": [ + { + "fix_id": "", + "path": ".refactor/docs/example.md", + "line": 1, + "confidence": "", + "source": "" + } + ] +} +``` + +Validation rules: + +- `rule_id` and `fix_id` must match `^[a-z0-9-]+$` +- `path` must be repo-root-relative, must not start with `/`, and must not + contain `..` +- `line` must be a positive integer +- `confidence` must be `high`, `medium`, or `low` + +### Step 1 — Write `.refactor/rules/-fixes.ttl` + +Write all fixes for the rule in a single file. The first fix should normally be +the authoritative markdown doc with `source: approved-doc`. + +```bash +mkdir -p .refactor/rules + +cat > ".refactor/rules/RULE_ID-fixes.ttl" <<'TURTLE' +@prefix ref: . + +ref:FIX_ID + a ref:Fix ; + ref:fixId "FIX_ID" ; + ref:fixPath "PATH" ; + ref:fixLine LINE ; + ref:confidence "CONFIDENCE" ; + ref:source "SOURCE" . + +ref:RULE_ID ref:hasFix ref:FIX_ID . +TURTLE +``` + +`ref:fixLine` is a bare integer. + +### Step 2 — Load into store + +```bash +oxigraph load \ + --location "${REFACTOR_DB_DIR:-.refactor/db}" \ + --file ".refactor/rules/RULE_ID-fixes.ttl" +``` + +## Step 3 — Emit result + +Success: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "operation": "", + "ids": [""], + "ttl_paths": [".refactor/rules/.ttl"], + "loaded_to_store": true + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +Failure: + +```json +{ + "success": false, + "canceled": false, + "aborted_by": null, + "data": null, + "error": { + "code": "VALIDATION.INPUT", + "message": "Fix path must be repo-root-relative", + "recoverable": true, + "suggested_action": "Replace the absolute path with a repo-root-relative path" + }, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +## Rules + +- Validate IDs and repo-relative paths before writing +- `ref:fixLine` must be a bare integer, not a quoted string +- If oxigraph is not on PATH, return `success: false` with error code + `EXECUTION.GRAPH_UNAVAILABLE` +- Default `REFACTOR_DB_DIR` to `.refactor/db` if unset +- Do not use absolute paths outside the workspace +- Emit exactly one fenced JSON block as final output diff --git a/packages/sc-refactory/agents/registry.yaml b/packages/sc-refactory/agents/registry.yaml new file mode 100644 index 00000000..6dec24e7 --- /dev/null +++ b/packages/sc-refactory/agents/registry.yaml @@ -0,0 +1,42 @@ +agents: + refactor-lookup-agent: + version: 0.1.0 + path: .claude/agents/refactor-lookup-agent.md + refactor-write-agent: + version: 0.1.0 + path: .claude/agents/refactor-write-agent.md + refactor-dev-agent: + version: 0.1.0 + path: .claude/agents/refactor-dev-agent.md + refactor-qa-agent: + version: 0.1.0 + path: .claude/agents/refactor-qa-agent.md + +skills: + refactory-design: + version: 0.1.0 + path: .claude/skills/refactory-design/SKILL.md + refactory-install: + version: 0.1.0 + path: .claude/skills/refactory-install/SKILL.md + refactor-lookup: + version: 0.1.0 + path: .claude/skills/refactor-lookup/SKILL.md + depends_on: + refactor-lookup-agent: "0.1.x" + refactor-write: + version: 0.1.0 + path: .claude/skills/refactor-write/SKILL.md + depends_on: + refactor-write-agent: "0.1.x" + refactor-orchestrate: + version: 0.1.0 + path: .claude/skills/refactor-orchestrate/SKILL.md + depends_on: + refactor-dev-agent: "0.1.x" + refactor-qa-agent: "0.1.x" + quality-manager: + version: 0.1.0 + path: .claude/skills/quality-manager/SKILL.md + depends_on: + refactor-qa-agent: "0.1.x" diff --git a/packages/sc-refactory/assets/startup-wrapper-template/team-lead.py b/packages/sc-refactory/assets/startup-wrapper-template/team-lead.py new file mode 100644 index 00000000..e2f45772 --- /dev/null +++ b/packages/sc-refactory/assets/startup-wrapper-template/team-lead.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def main() -> int: + repo_root = Path(__file__).resolve().parents[1] + script = repo_root / ".refactor" / "scripts" / "session_start.py" + args = ["python3", str(script), *sys.argv[1:]] + return subprocess.call(args, cwd=repo_root) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/sc-refactory/commands/sc-refactor-lookup.md b/packages/sc-refactory/commands/sc-refactor-lookup.md new file mode 100644 index 00000000..230e1b09 --- /dev/null +++ b/packages/sc-refactory/commands/sc-refactor-lookup.md @@ -0,0 +1,23 @@ +--- +name: sc-refactor-lookup +version: 0.1.0 +description: Look up whether a trigger maps to an approved refactor rule before editing. +options: + - name: --signal + args: + - name: value + description: Trigger value such as a type, namespace, assembly, file, or error code. + description: Required trigger value to look up. + - name: --kind + args: + - name: type + description: "Optional kind: type, namespace, assembly, string, or error." + description: Optional trigger kind. +--- + +# /sc-refactor-lookup + +Thin entrypoint for the `refactor-lookup` skill. + +Use before editing when a known trigger appears in build errors, CI output, +source files, or project files. diff --git a/packages/sc-refactory/commands/sc-refactor-plan.md b/packages/sc-refactory/commands/sc-refactor-plan.md new file mode 100644 index 00000000..e9c73107 --- /dev/null +++ b/packages/sc-refactory/commands/sc-refactor-plan.md @@ -0,0 +1,21 @@ +--- +name: sc-refactor-plan +version: 0.1.0 +description: Prepare or execute a rule-backed refactor plan using the named teammates. +--- + +# /sc-refactor-plan + +User-facing entrypoint for plan-driven execution. + +This command is backed by the named teammates: + +- `refactor-orchestrator` +- `quality-manager` + +Policy: + +- every plan item must be backed by approved `.refactor/` rules +- dev waves make only authorized changes +- QA waves verify 100% compliance +- only QA-approved waves are committable diff --git a/packages/sc-refactory/commands/sc-refactor-write.md b/packages/sc-refactory/commands/sc-refactor-write.md new file mode 100644 index 00000000..502ef04d --- /dev/null +++ b/packages/sc-refactory/commands/sc-refactor-write.md @@ -0,0 +1,12 @@ +--- +name: sc-refactor-write +version: 0.1.0 +description: Author or update approved refactor rules and sample fix references. +--- + +# /sc-refactor-write + +Thin entrypoint for the `refactor-write` skill. + +Use when a new approved rule must be added or an existing rule needs updated +docs, triggers, or sample references. diff --git a/packages/sc-refactory/commands/sc-refactory-design.md b/packages/sc-refactory/commands/sc-refactory-design.md new file mode 100644 index 00000000..a4be7c8a --- /dev/null +++ b/packages/sc-refactory/commands/sc-refactory-design.md @@ -0,0 +1,17 @@ +--- +name: sc-refactory-design +version: 0.1.0 +description: Design a rule-driven refactoring system before packaging or installation. +--- + +# /sc-refactory-design + +Thin entrypoint for the `refactory-design` skill. + +Use this command to: + +- define approved-rule boundaries +- define startup allow/deny policy +- define named-teammate responsibilities +- define QA and wave gating +- scaffold a reusable `sc-refactory` package shape diff --git a/packages/sc-refactory/commands/sc-refactory-install.md b/packages/sc-refactory/commands/sc-refactory-install.md new file mode 100644 index 00000000..f48b56c6 --- /dev/null +++ b/packages/sc-refactory/commands/sc-refactory-install.md @@ -0,0 +1,24 @@ +--- +name: sc-refactory-install +version: 0.1.0 +description: Install the refactory runtime into the current repository. +options: + - name: --force + description: Overwrite existing runtime files managed by the installer. + - name: --seed + args: + - name: mode + description: "Seed mode: empty or templates." + description: Control whether starter templates are installed. +--- + +# /sc-refactory-install + +Thin entrypoint for the `refactory-install` skill and installer script. + +Expected result: + +- `.refactor/` runtime layout exists +- `.startup/team-lead` exists +- local install/troubleshooting guide exists +- startup preview can be rendered diff --git a/packages/sc-refactory/manifest.yaml b/packages/sc-refactory/manifest.yaml new file mode 100644 index 00000000..5191aa52 --- /dev/null +++ b/packages/sc-refactory/manifest.yaml @@ -0,0 +1,69 @@ +name: sc-refactory +version: 0.1.0 +description: > + Design and install a rule-driven refactoring toolkit with startup policy + injection, approved-fix lookup, curated rule authoring, and named-teammate + orchestration for large migration campaigns. +author: synaptic-canvas +license: MIT +tags: + - refactoring + - policy + - graph + - oxigraph + - migration + - teammates + +artifacts: + commands: + - commands/sc-refactory-design.md + - commands/sc-refactory-install.md + - commands/sc-refactor-lookup.md + - commands/sc-refactor-write.md + - commands/sc-refactor-plan.md + skills: + - skills/refactory-design/SKILL.md + - skills/refactory-install/SKILL.md + - skills/refactor-lookup/SKILL.md + - skills/refactor-lookup/workflows.md + - skills/refactor-write/SKILL.md + - skills/refactor-write/workflows.md + - skills/refactor-orchestrate/SKILL.md + - skills/quality-manager/SKILL.md + agents: + - agents/registry.yaml + - agents/refactor-lookup-agent.md + - agents/refactor-write-agent.md + - agents/refactor-dev-agent.md + - agents/refactor-qa-agent.md + - agents/refactor-orchestrator.md + - agents/quality-manager.md + scripts: + - scripts/install_refactory.py + - scripts/runtime.py + - scripts/sc_shared.py + - scripts/session_start.py + - scripts/preflight.py + - scripts/repair.py + - scripts/log_lookup.py + - scripts/rebuild_db.py + - scripts/sync_subset.py + - scripts/validate_agents.py + assets: + - assets/startup-wrapper-template/team-lead.py + plugin: + - .claude-plugin/plugin.json + +install: + scope: local-only + +requires: + - python3 + - git + - cargo + - oxigraph + - PyYAML + - pydantic + +dependencies: + - "sc-startup >= 0.10.0" diff --git a/packages/sc-refactory/references/install-and-troubleshooting.md b/packages/sc-refactory/references/install-and-troubleshooting.md new file mode 100644 index 00000000..cb7e8965 --- /dev/null +++ b/packages/sc-refactory/references/install-and-troubleshooting.md @@ -0,0 +1,166 @@ +# Refactor Install And Troubleshooting + +Use this guide when a refactor skill pre-flight fails. + +## Claude Repair Contract + +The goal is for the Claude session to repair this environment through the +skill's documented workflow whenever the repair path is local and scripted. + +That means: + +- run pre-flight, +- if it fails, read this guide, +- attempt the scripted local repair steps below, +- rerun pre-flight, +- only stop and ask for help if the scripted repair path does not restore the + environment. + +## Expected Layout + +- rules: `.refactor/rules/` +- docs: `.refactor/docs/` +- runtime DB: `.refactor/db/` +- startup/rebuild scripts: `.refactor/scripts/` +- temp files and logs: `.refactor/temp/` + +## Supported Runtime + +Install `oxigraph` from crates.io, not from Homebrew. + +This workflow was tested with: + +```text +oxigraph 0.5.7 +``` + +## Pre-flight Command + +Run: + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/preflight.py" --skill refactor-lookup +``` + +or: + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/preflight.py" --skill refactor-write +``` + +Success output: + +```text +oxigraph v 0.5.7 checks pass +``` + +If pre-flight fails, do not invoke the background agent yet. + +Failure output: + +```text +tools are not installed or working to use this skill. please read ./.refactor/docs/install-and-troubleshooting.md +``` + +## Scripted Repair + +After a pre-flight failure, Claude should attempt the scripted repair path +before stopping: + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/repair.py" --skill refactor-lookup +``` + +or: + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/repair.py" --skill refactor-write +``` + +This repair path rebuilds `.refactor/db/` through `session_start.py` and then +reruns the matching pre-flight check. + +## Installation + +Install the latest `oxigraph-cli` from crates.io: + +```bash +cargo install oxigraph-cli +``` + +If an older `oxigraph` is already shadowing the cargo install, remove or unlink +it so `oxigraph` resolves to `~/.cargo/bin/oxigraph`. + +Verify: + +```bash +oxigraph --version +``` + +## Rebuild Runtime DB + +If `.refactor/db/` is missing or unreadable, rebuild it through the startup +script: + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/session_start.py" --mode startup >/tmp/refactor-startup.out +``` + +This rebuilds `.refactor/db/` from committed turtles in `.refactor/rules/`. + +This is the first repair step Claude should attempt when pre-flight fails and +`oxigraph` itself is installed. + +## Direct Health Checks + +Check that the DB can be queried: + +```bash +repo_root="$(git rev-parse --show-toplevel)" + +oxigraph query \ + --location "$repo_root/.refactor/db" \ + --query 'PREFIX ref: SELECT ?s WHERE { ?s ?p ?o } LIMIT 1' \ + --results-format json +``` + +## Troubleshooting + +If `oxigraph --version` fails: + +- install `oxigraph` +- ensure it is on `PATH` + +If `.refactor/db/` is missing: + +- run `python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/session_start.py" --mode startup` + +If `.refactor/db/` exists but query fails: + +- run `python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/repair.py" --skill refactor-lookup` +- rerun the pre-flight command + +If pre-flight still fails after rebuilding the DB: + +- surface a concise environment error +- include the exact failing command and output +- do not invoke the background refactor agent yet + +If startup succeeds but the background lookup agent still reports +`EXECUTION.GRAPH_UNAVAILABLE`: + +- verify the agent is running in this repo +- verify it is querying `.refactor/db` +- treat that as an agent cwd/path problem, not a rule-authoring problem + +## Source Of Truth + +The committed source of truth is: + +- `.refactor/rules/*.ttl` +- `.refactor/docs/*.md` + +Do not commit: + +- `.refactor/db/` +- `.refactor/temp/` diff --git a/packages/sc-refactory/references/rule-doc-template.md b/packages/sc-refactory/references/rule-doc-template.md new file mode 100644 index 00000000..ea7f1ebc --- /dev/null +++ b/packages/sc-refactory/references/rule-doc-template.md @@ -0,0 +1,43 @@ +# + +## Summary + + + +## Triggers + +- `` +- `` + +## Why This Rule Exists + + + +## When It Applies + +- +- + +## When It Does Not Apply + +- +- + +## Approved Fix Shape + +1. +2. + +## Tandem Fix Requirements + +- +- + +## Exceptions + +- + +## Sample Fixes + +- `RepoA/Path/File.ext:123` +- `RepoB/Path/File.ext:88` diff --git a/packages/sc-refactory/references/rule-ttl-template.ttl b/packages/sc-refactory/references/rule-ttl-template.ttl new file mode 100644 index 00000000..f54d5b0d --- /dev/null +++ b/packages/sc-refactory/references/rule-ttl-template.ttl @@ -0,0 +1,16 @@ +@prefix ref: . + +ref:RULE_ID + a ref:Rule ; + ref:ruleId "RULE_ID" ; + ref:severity "warning" ; + ref:ruleText """ +Approved rule: SUMMARY + +How-to doc: .refactor/docs/DOC_NAME.md + +Allowed fix: +- BOUNDED_FIX_1 +- BOUNDED_FIX_2 +""" ; + ref:triggeredByString "TRIGGER_VALUE" . diff --git a/packages/sc-refactory/references/runtime-layout.md b/packages/sc-refactory/references/runtime-layout.md new file mode 100644 index 00000000..79d5dfda --- /dev/null +++ b/packages/sc-refactory/references/runtime-layout.md @@ -0,0 +1,20 @@ +# Refactory Runtime Layout + +Package source lives under `packages/sc-refactory/`. + +When installed, the package artifacts are copied into: + +- `~/.claude/agents/`, `~/.claude/skills/`, `~/.claude/scripts/` +- or `/.claude/agents/`, `/.claude/skills/`, `/.claude/scripts/` + +After `refactory-install` runs in a repo, the repo-local runtime should include: + +- `.startup/team-lead` +- `.refactor/docs/` +- `.refactor/rules/` +- `.refactor/profiles/` +- `.refactor/scripts/` +- `.refactor/reports/` +- `.refactor/db/` +- `.refactor/logs/` +- `.refactor/temp/` diff --git a/packages/sc-refactory/scripts/install_refactory.py b/packages/sc-refactory/scripts/install_refactory.py new file mode 100644 index 00000000..53952db2 --- /dev/null +++ b/packages/sc-refactory/scripts/install_refactory.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +"""Install the refactory runtime into a target repository.""" + +from __future__ import annotations + +import argparse +import shutil +import stat +import subprocess +from pathlib import Path + + +INSTALL_GUIDE_FALLBACK = """# Refactor Install And Troubleshooting + +Use this guide when a refactor skill pre-flight fails. + +## Expected Layout + +- rules: `.refactor/rules/` +- docs: `.refactor/docs/` +- runtime DB: `.refactor/db/` +- startup/rebuild scripts: `.refactor/scripts/` +- temp files and logs: `.refactor/temp/` and `.refactor/logs/` + +## Supported Runtime + +Install `oxigraph` from crates.io, not from Homebrew. + +This workflow was tested with: + +```text +oxigraph 0.5.7 +``` + +## Installation + +```bash +cargo install oxigraph-cli +oxigraph --version +``` + +## Pre-flight + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/preflight.py" --skill refactor-lookup +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/preflight.py" --skill refactor-write +``` + +Success output: + +```text +oxigraph v 0.5.7 checks pass +``` + +Failure output: + +```text +tools are not installed or working to use this skill. please read ./.refactor/docs/install-and-troubleshooting.md +``` + +## Repair + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/repair.py" --skill refactor-lookup +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/repair.py" --skill refactor-write +``` + +## Source Of Truth + +The committed source of truth is: + +- `.refactor/rules/*.ttl` +- `.refactor/docs/*.md` + +Do not commit: + +- `.refactor/db/` +- `.refactor/logs/` +- `.refactor/temp/` +""" + + +RULE_DOC_TEMPLATE_FALLBACK = """# + +## Summary + + + +## Triggers + +- `` +- `` + +## Why This Rule Exists + + +""" + + +RULE_TTL_TEMPLATE_FALLBACK = """@prefix ref: . + +ref:RULE_ID + a ref:Rule ; + ref:ruleId "RULE_ID" ; + ref:severity "warning" ; + ref:ruleText "Replace this template with an approved rule." ; + ref:triggeredByString "TRIGGER_VALUE" . +""" + + +STARTUP_WRAPPER_FALLBACK = """#!/usr/bin/env python3 +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def main() -> int: + repo_root = Path(__file__).resolve().parents[1] + script = repo_root / ".refactor" / "scripts" / "session_start.py" + args = ["python3", str(script), *sys.argv[1:]] + return subprocess.call(args, cwd=repo_root) + + +if __name__ == "__main__": + raise SystemExit(main()) +""" + + +GITIGNORE = "/db/\n/logs/\n/temp/\n" + +SCRIPT_NAMES = [ + "runtime.py", + "session_start.py", + "preflight.py", + "repair.py", + "log_lookup.py", + "rebuild_db.py", + "sync_subset.py", +] + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Install refactory runtime into a repository") + parser.add_argument("--repo-root", default=None, help="Target repository root") + parser.add_argument("--force", action="store_true", help="Overwrite existing runtime files") + parser.add_argument( + "--seed", + choices=["empty", "templates"], + default="templates", + help="Whether to install empty runtime only or include starter templates", + ) + return parser.parse_args() + + +def resolve_repo_root(explicit: str | None) -> Path: + if explicit: + return Path(explicit).resolve() + try: + root = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], + text=True, + stderr=subprocess.DEVNULL, + ).strip() + return Path(root) + except subprocess.CalledProcessError: + return Path.cwd().resolve() + + +def write_text(path: Path, content: str, force: bool) -> None: + if path.exists() and not force: + return + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def copy_file(src: Path, dst: Path, force: bool) -> None: + if dst.exists() and not force: + return + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + + +def make_executable(path: Path) -> None: + mode = path.stat().st_mode + path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def read_reference(package_root: Path, relative_path: str, fallback: str) -> str: + path = package_root / relative_path + if path.exists(): + return path.read_text(encoding="utf-8") + return fallback + + +def install_runtime(repo_root: Path, package_root: Path, force: bool) -> None: + refactor_root = repo_root / ".refactor" + for rel in [ + "docs", + "rules", + "profiles", + "scripts", + "reports", + "db", + "logs", + "temp", + ]: + (refactor_root / rel).mkdir(parents=True, exist_ok=True) + + write_text(refactor_root / ".gitignore", GITIGNORE, force) + write_text( + refactor_root / "docs" / "install-and-troubleshooting.md", + read_reference( + package_root, + "references/install-and-troubleshooting.md", + INSTALL_GUIDE_FALLBACK, + ), + force, + ) + + package_script_dir = package_root / "scripts" + for name in SCRIPT_NAMES: + src = package_script_dir / name + dst = refactor_root / "scripts" / name + copy_file(src, dst, force) + make_executable(dst) + + startup_dir = repo_root / ".startup" + startup_dir.mkdir(parents=True, exist_ok=True) + startup = startup_dir / "team-lead" + write_text( + startup, + read_reference( + package_root, + "assets/startup-wrapper-template/team-lead.py", + STARTUP_WRAPPER_FALLBACK, + ), + force, + ) + make_executable(startup) + + +def install_templates(repo_root: Path, package_root: Path, force: bool) -> None: + docs_dir = repo_root / ".refactor" / "docs" + rules_dir = repo_root / ".refactor" / "rules" + + write_text( + docs_dir / "rule-template.md", + read_reference( + package_root, + "references/rule-doc-template.md", + RULE_DOC_TEMPLATE_FALLBACK, + ), + force, + ) + write_text( + rules_dir / "rule-template.ttl", + read_reference( + package_root, + "references/rule-ttl-template.ttl", + RULE_TTL_TEMPLATE_FALLBACK, + ), + force, + ) + + +def preview_startup(repo_root: Path) -> str: + script = repo_root / ".refactor" / "scripts" / "session_start.py" + result = subprocess.run( + ["python3", str(script), "--mode", "startup"], + cwd=repo_root, + capture_output=True, + text=True, + check=False, + ) + return result.stdout.strip() + + +def main() -> int: + args = parse_args() + repo_root = resolve_repo_root(args.repo_root) + package_root = Path(__file__).resolve().parents[1] + + if not (repo_root / ".git").exists(): + print(f"warning: {repo_root} does not look like a git repo; continuing anyway") + + install_runtime(repo_root, package_root, args.force) + if args.seed == "templates": + install_templates(repo_root, package_root, args.force) + + print(f"installed refactory runtime into {repo_root}") + print(f"- startup wrapper: {repo_root / '.startup' / 'team-lead'}") + print(f"- runtime root: {repo_root / '.refactor'}") + + preview = preview_startup(repo_root) + if preview: + print("\nstartup preview:\n") + print(preview) + else: + print("\nstartup preview: (no triggers registered yet)") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/sc-refactory/scripts/log_lookup.py b/packages/sc-refactory/scripts/log_lookup.py new file mode 100644 index 00000000..21aad68b --- /dev/null +++ b/packages/sc-refactory/scripts/log_lookup.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Append refactor lookup request/result logs.""" + +from __future__ import annotations + +import argparse +import json +import sys + +from runtime import append_json_log, find_repo_root + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Log refactor lookup activity") + parser.add_argument("--stage", required=True, choices=["request", "result"]) + parser.add_argument("--status", default=None) + parser.add_argument("--rule-id", default=None) + parser.add_argument("--error-code", default=None) + parser.add_argument("--source", default="refactor-lookup") + parser.add_argument("--payload-file", default=None) + return parser.parse_args() + + +def load_payload(payload_file: str | None) -> dict: + if payload_file: + with open(payload_file, encoding="utf-8") as handle: + return json.load(handle) + raw = sys.stdin.read().strip() + if not raw: + return {} + return json.loads(raw) + + +def main() -> None: + args = parse_args() + root = find_repo_root() + payload = load_payload(args.payload_file) + append_json_log( + root, + "lookup.log", + { + "source": args.source, + "stage": args.stage, + "status": args.status, + "rule_id": args.rule_id, + "error_code": args.error_code, + "payload": payload, + }, + ) + + +if __name__ == "__main__": + main() diff --git a/packages/sc-refactory/scripts/preflight.py b/packages/sc-refactory/scripts/preflight.py new file mode 100644 index 00000000..19488cec --- /dev/null +++ b/packages/sc-refactory/scripts/preflight.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Pre-flight check for refactor skills.""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +from runtime import append_log, find_repo_root, resolve_oxigraph + +GUIDE_PATH = "./.refactor/docs/install-and-troubleshooting.md" + + +def check_oxigraph() -> tuple[bool, str]: + oxigraph = resolve_oxigraph() + if oxigraph is None: + return False, "oxigraph is not installed or not on PATH" + try: + result = subprocess.run( + [str(oxigraph), "--version"], + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError: + return False, "oxigraph is not installed or not on PATH" + if result.returncode != 0: + return False, "oxigraph is not installed or not on PATH" + version = result.stdout.strip() + if version.startswith("oxigraph "): + version = "oxigraph v " + version.removeprefix("oxigraph ").strip() + return True, version + + +def check_paths(root: Path) -> tuple[bool, str]: + required = [ + root / ".refactor" / "rules", + root / ".refactor" / "docs", + root / ".refactor" / "scripts" / "session_start.py", + ] + missing = [str(p.relative_to(root)) for p in required if not p.exists()] + if missing: + return False, f"missing required paths: {', '.join(missing)}" + return True, "paths present" + + +def check_db_query(root: Path) -> tuple[bool, str]: + db_dir = root / ".refactor" / "db" + if not db_dir.is_dir(): + return False, "missing .refactor/db; run the session_start rebuild path" + + oxigraph = resolve_oxigraph() + if oxigraph is None: + return False, "oxigraph is not installed or not on PATH" + + try: + result = subprocess.run( + [ + str(oxigraph), + "query", + "--location", + str(db_dir), + "--query", + "PREFIX ref: SELECT ?s WHERE { ?s ?p ?o } LIMIT 1", + "--results-format", + "json", + ], + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError: + return False, "oxigraph is not installed or not on PATH" + if result.returncode != 0: + return False, "unable to query .refactor/db" + return True, "db query ok" + + +def rebuild_db(root: Path) -> tuple[bool, str]: + startup = root / ".refactor" / "scripts" / "session_start.py" + if not startup.is_file(): + return False, "missing .refactor/scripts/session_start.py" + + result = subprocess.run( + ["python3", str(startup), "--mode", "startup"], + cwd=root, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return False, "session_start.py returned non-zero" + + if not (root / ".refactor" / "db").is_dir(): + return False, "session_start.py did not rebuild .refactor/db" + + return True, "db rebuilt" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Pre-flight check for refactor skills") + parser.add_argument( + "--skill", + default="refactor", + choices=["refactor", "refactor-lookup", "refactor-write"], + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + root = find_repo_root(Path(__file__)) + append_log(root, "preflight.log", f"start skill={args.skill}") + + oxigraph_ok, oxigraph_version = check_oxigraph() + path_ok, path_message = check_paths(root) + db_dir = root / ".refactor" / "db" + + db_check: tuple[bool, str] + if oxigraph_ok and path_ok and not db_dir.is_dir(): + append_log(root, "preflight.log", "db missing; invoking session_start rebuild") + rebuild_ok, rebuild_message = rebuild_db(root) + append_log(root, "preflight.log", f"db rebuild result ok={rebuild_ok} message='{rebuild_message}'") + if rebuild_ok: + db_check = check_db_query(root) + else: + db_check = (False, rebuild_message) + else: + db_check = check_db_query(root) + + checks = [ + (oxigraph_ok, oxigraph_version), + (path_ok, path_message), + db_check, + ] + failures = [message for ok, message in checks if not ok] + + if not failures: + append_log(root, "preflight.log", f"success skill={args.skill} version='{oxigraph_version}'") + print(f"{oxigraph_version} checks pass") + sys.exit(0) + + append_log(root, "preflight.log", f"failure skill={args.skill} reasons={failures}") + print(f"tools are not installed or working to use this skill. please read {GUIDE_PATH}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/packages/sc-refactory/scripts/rebuild_db.py b/packages/sc-refactory/scripts/rebuild_db.py new file mode 100644 index 00000000..6ad39a32 --- /dev/null +++ b/packages/sc-refactory/scripts/rebuild_db.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Rebuild `.refactor/db` from committed rule files by reusing session_start.""" + +from __future__ import annotations + +import argparse +import subprocess +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Rebuild the local refactor DB") + parser.add_argument("--repo-root", default=".") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + repo_root = Path(args.repo_root).resolve() + script = repo_root / ".refactor" / "scripts" / "session_start.py" + result = subprocess.run( + ["python3", str(script), "--mode", "startup"], + cwd=repo_root, + check=False, + ) + return result.returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/sc-refactory/scripts/repair.py b/packages/sc-refactory/scripts/repair.py new file mode 100644 index 00000000..7bb18fe6 --- /dev/null +++ b/packages/sc-refactory/scripts/repair.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Scripted local repair path for refactor skills.""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +from runtime import append_log, find_repo_root + + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Repair local refactor skill environment") + parser.add_argument( + "--skill", + default="refactor", + choices=["refactor", "refactor-lookup", "refactor-write"], + ) + return parser.parse_args() + + +def run(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + command, + cwd=cwd, + capture_output=True, + text=True, + check=False, + ) + + +def main() -> None: + args = parse_args() + root = find_repo_root(Path(__file__)) + startup = root / ".refactor" / "scripts" / "session_start.py" + preflight = root / ".refactor" / "scripts" / "preflight.py" + append_log(root, "repair.log", f"start skill={args.skill}") + + if not startup.is_file() or not preflight.is_file(): + append_log(root, "repair.log", "failure missing startup or preflight script") + print( + "tools are not installed or working to use this skill. " + "please read ./.refactor/docs/install-and-troubleshooting.md" + ) + sys.exit(1) + + startup_result = run( + ["python3", str(startup), "--mode", "startup"], + cwd=root, + ) + append_log( + root, + "repair.log", + f"startup returncode={startup_result.returncode} stdout_lines={len(startup_result.stdout.splitlines())} stderr_lines={len(startup_result.stderr.splitlines())}", + ) + if startup_result.returncode != 0: + append_log(root, "repair.log", "failure startup returned non-zero") + print( + "tools are not installed or working to use this skill. " + "please read ./.refactor/docs/install-and-troubleshooting.md" + ) + sys.exit(1) + + preflight_result = run( + ["python3", str(preflight), "--skill", args.skill], + cwd=root, + ) + append_log( + root, + "repair.log", + f"preflight returncode={preflight_result.returncode} output='{preflight_result.stdout.strip()}'", + ) + sys.stdout.write(preflight_result.stdout) + if preflight_result.stderr: + sys.stderr.write(preflight_result.stderr) + sys.exit(preflight_result.returncode) + + +if __name__ == "__main__": + main() diff --git a/packages/sc-refactory/scripts/runtime.py b/packages/sc-refactory/scripts/runtime.py new file mode 100644 index 00000000..d1dd21e5 --- /dev/null +++ b/packages/sc-refactory/scripts/runtime.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Shared runtime helpers for refactor scripts.""" + +from __future__ import annotations + +import json +import shutil +import subprocess +from datetime import datetime, timezone +from pathlib import Path + + +def find_repo_root(anchor: Path | None = None) -> Path: + if anchor is not None: + candidate = anchor.resolve() + search = [candidate] + list(candidate.parents) + for parent in search: + if (parent / ".refactor" / "scripts").is_dir(): + return parent + + try: + root = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], + stderr=subprocess.DEVNULL, + text=True, + ).strip() + return Path(root) + except subprocess.CalledProcessError: + return Path.cwd() + + +def resolve_oxigraph() -> Path | None: + path = shutil.which("oxigraph") + if path: + return Path(path) + + candidates = [ + Path("/opt/homebrew/bin/oxigraph"), + Path("/usr/local/bin/oxigraph"), + ] + for candidate in candidates: + if candidate.is_file(): + return candidate + return None + + +def logs_dir(root: Path) -> Path: + return root / ".refactor" / "logs" + + +def append_log(root: Path, log_name: str, message: str) -> None: + log_dir = logs_dir(root) + log_dir.mkdir(parents=True, exist_ok=True) + log_file = log_dir / log_name + timestamp = datetime.now(timezone.utc).isoformat() + with log_file.open("a", encoding="utf-8") as handle: + handle.write(f"[{timestamp}] {message}\n") + + +def append_json_log(root: Path, log_name: str, payload: dict) -> None: + log_dir = logs_dir(root) + log_dir.mkdir(parents=True, exist_ok=True) + log_file = log_dir / log_name + record = { + "timestamp": datetime.now(timezone.utc).isoformat(), + **payload, + } + with log_file.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(record, sort_keys=True) + "\n") diff --git a/packages/sc-refactory/scripts/sc_shared.py b/packages/sc-refactory/scripts/sc_shared.py new file mode 100644 index 00000000..acc2f933 --- /dev/null +++ b/packages/sc-refactory/scripts/sc_shared.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python3 +""" +Shared utilities for Synaptic Canvas package scripts. + +Provides: +- Allowed-path validation against runtime-configured directories. +- Agent Runner helpers (registry validation + task prompt build + audit). +- Shared runtime context helpers. +""" +from __future__ import annotations + +import datetime as _dt +import hashlib +import json +import os +import re +import subprocess +from pathlib import Path +from typing import Any, Dict, Iterable, Optional, Set, Tuple + +from pydantic import BaseModel, Field, field_validator + +try: + import yaml # type: ignore +except Exception: # pragma: no cover + yaml = None + + +# ============================================================================= +# Paths and settings +# ============================================================================= + + +def _normalize_path(value: Optional[str | Path]) -> Optional[Path]: + if value is None: + return None + return Path(value).expanduser().resolve() + + +def _read_json(path: Path) -> Optional[dict]: + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception: + return None + + +class PathPolicy(BaseModel): + """Resolved allowed-path policy.""" + + cwd: Path + project_dir: Optional[Path] = None + codex_home: Optional[Path] = None + additional_dirs: Set[Path] = Field(default_factory=set) + + @field_validator("cwd", "project_dir", "codex_home", mode="before") + @classmethod + def _validate_path(cls, v): + return _normalize_path(v) + + +class RuntimeContext(BaseModel): + """Shared runtime context derived from environment + filesystem.""" + + cwd: Path + project_dir: Optional[Path] + codex_home: Optional[Path] + allowed_dirs: Set[Path] = Field(default_factory=set) + + +def get_project_dir() -> Optional[Path]: + """Return project root from environment variables if set.""" + project_dir = os.getenv("CLAUDE_PROJECT_DIR") or os.getenv("CODEX_PROJECT_DIR") + return _normalize_path(project_dir) + + +def _collect_additional_dirs(project_dir: Optional[Path]) -> Set[Path]: + """Collect additionalDirectories from settings files.""" + settings_paths = [ + Path("~/.claude/settings.json").expanduser(), + Path("~/.codex/settings.json").expanduser(), + ] + if project_dir: + settings_paths.extend( + [ + project_dir / ".claude" / "settings.json", + project_dir / ".codex" / "settings.json", + ] + ) + + codex_home = os.getenv("CODEX_HOME") + if codex_home: + settings_paths.append(Path(codex_home) / "settings.json") + + allowed: Set[Path] = set() + for path in settings_paths: + if not path.exists(): + continue + data = _read_json(path) + if not data: + continue + extra = (data.get("permissions") or {}).get("additionalDirectories") + if isinstance(extra, list): + for entry in extra: + if isinstance(entry, str) and entry.strip(): + allowed.add(_normalize_path(entry)) + return {p for p in allowed if p is not None} + + +def build_path_policy(cwd: Optional[Path] = None) -> PathPolicy: + cwd = _normalize_path(cwd or Path.cwd()) + project_dir = get_project_dir() + codex_home = _normalize_path(os.getenv("CODEX_HOME")) + additional = _collect_additional_dirs(project_dir) + return PathPolicy(cwd=cwd, project_dir=project_dir, codex_home=codex_home, additional_dirs=additional) + + +def collect_allowed_dirs(policy: PathPolicy) -> Set[Path]: + allowed = {policy.cwd} + if policy.project_dir: + allowed.add(policy.project_dir) + if policy.codex_home: + allowed.add(policy.codex_home) + allowed.update(policy.additional_dirs) + return allowed + + +def _is_relative_to(path: Path, base: Path) -> bool: + try: + return path.is_relative_to(base) + except AttributeError: + try: + path.relative_to(base) + return True + except ValueError: + return False + + +def is_path_allowed(target: Path, allowed_dirs: Iterable[Path]) -> bool: + target = _normalize_path(target) + if target is None: + return False + for base in allowed_dirs: + if base and _is_relative_to(target, base): + return True + return False + + +def validate_allowed_path(target: Path, allowed_dirs: Iterable[Path], label: str = "path") -> Path: + resolved = _normalize_path(target) + if resolved is None: + raise ValueError(f"Invalid {label}: {target}") + if not is_path_allowed(resolved, allowed_dirs): + raise ValueError(f"{label} is outside allowed directories: {resolved}") + return resolved + + +def load_runtime_context(cwd: Optional[Path] = None) -> RuntimeContext: + policy = build_path_policy(cwd=cwd) + allowed = collect_allowed_dirs(policy) + return RuntimeContext( + cwd=policy.cwd, + project_dir=policy.project_dir, + codex_home=policy.codex_home, + allowed_dirs=allowed, + ) + + +def find_repo_root(start: Optional[Path] = None) -> Optional[Path]: + """Find git repo root by walking up to a .git directory.""" + current = _normalize_path(start or Path.cwd()) + if current is None: + return None + for parent in [current, *current.parents]: + if (parent / ".git").exists(): + return parent + return None + + +def is_git_repo(path: Path) -> bool: + """Return True if the path is inside a valid git repository.""" + repo_path = _normalize_path(path) + if repo_path is None: + return False + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=repo_path, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + ) + return bool(result.stdout.strip()) + except Exception: + return False + + +# ============================================================================= +# Hook JSON validation helpers +# ============================================================================= + + +def get_tool_command(payload: Dict[str, Any]) -> str: + """Extract tool command string from a hook payload.""" + if not isinstance(payload, dict): + return "" + tool_input = payload.get("tool_input") or {} + if isinstance(tool_input, dict): + for key in ("command", "input"): + val = tool_input.get(key) + if isinstance(val, str): + return val + for key in ("command", "input"): + val = payload.get(key) + if isinstance(val, str): + return val + return "" + + +def extract_json_from_command(command: str) -> Dict[str, Any]: + """Extract a JSON object embedded in a command string.""" + if not isinstance(command, str): + raise ValueError("Command must be a string") + + text = command.strip() + if text.startswith("{") and text.endswith("}"): + try: + data = json.loads(text) + if isinstance(data, dict): + return data + except Exception: + pass + + greedy = re.search(r"\{.*\}", text, re.DOTALL) + if greedy: + try: + data = json.loads(greedy.group(0)) + if isinstance(data, dict): + return data + except Exception: + pass + + for match in re.finditer(r"\{.*?\}", text, re.DOTALL): + try: + data = json.loads(match.group(0)) + if isinstance(data, dict): + return data + except Exception: + continue + + raise ValueError("Expected JSON object in command") + + +def extract_hook_json(payload: Dict[str, Any]) -> Dict[str, Any]: + """Extract JSON object from hook payload tool command.""" + command = get_tool_command(payload) + if not command: + raise ValueError("Expected tool_input.command in payload") + return extract_json_from_command(command) + + +def validate_json_payload(payload: Dict[str, Any], schema: type[BaseModel]) -> BaseModel: + """Validate payload dict with a pydantic schema and return the model.""" + if not isinstance(payload, dict): + raise ValueError("Expected JSON object") + return schema.model_validate(payload) + + +def validate_hook_json(payload: Dict[str, Any], schema: type[BaseModel]) -> BaseModel: + """Extract and validate JSON from hook payload with schema.""" + data = extract_hook_json(payload) + return validate_json_payload(data, schema) + + +# ============================================================================= +# Agent Runner helpers (self-contained) +# ============================================================================= + +REGISTRY_DEFAULT = os.path.join(".claude", "agents", "registry.yaml") +LOGS_DIR = os.path.join(".claude", "state", "logs") + + +class AgentSpec(BaseModel): + name: str + path: str + expected_version: Optional[str] = None + + +class AgentFileInfo(BaseModel): + path: str + version_frontmatter: Optional[str] + sha256: str + + +class AgentInvokeRequest(BaseModel): + agent: str + params: Dict[str, Any] = Field(default_factory=dict) + registry_path: str = REGISTRY_DEFAULT + timeout_s: int = 120 + + +class AgentInvokeResult(BaseModel): + ok: bool + agent: Dict[str, Any] + task_prompt: str + timeout_s: int + audit_path: str + note: str + + +def _read_text(path: str) -> str: + with open(path, "r", encoding="utf-8") as f: + return f.read() + + +def _read_bytes(path: str) -> bytes: + with open(path, "rb") as f: + return f.read() + + +def _extract_frontmatter(text: str) -> str: + lines = text.splitlines() + fm_start = None + for i, line in enumerate(lines): + if line.strip() == "---": + fm_start = i + break + if fm_start is None: + return "" + for j in range(fm_start + 1, len(lines)): + if lines[j].strip() == "---": + return "\n".join(lines[fm_start + 1 : j]) + return "" + + +def _parse_yaml(s: str) -> Dict[str, Any]: + if not s: + return {} + if yaml is not None: + return yaml.safe_load(s) or {} + out: Dict[str, Any] = {} + for line in s.splitlines(): + match = re.match(r"^([A-Za-z0-9_\-]+):\s*(.*)$", line.strip()) + if match: + key, val = match.group(1), match.group(2) + out[key] = val if val else None + return out + + +def _load_yaml_file(path: str) -> Dict[str, Any]: + text = _read_text(path) + if yaml is not None: + return yaml.safe_load(text) or {} + data: Dict[str, Any] = {} + current = None + for line in text.splitlines(): + if line.strip().startswith("agents:"): + data["agents"] = {} + current = "agents" + continue + if current == "agents": + match = re.match(r"^\s{2}([A-Za-z0-9_\-]+):\s*$", line) + if match: + data["agents"][match.group(1)] = {} + match_ver = re.match(r"^\s{4}version:\s*(.+)$", line) + if match_ver: + last = list(data["agents"].keys())[-1] + data["agents"][last]["version"] = match_ver.group(1) + match_path = re.match(r"^\s{4}path:\s*(.+)$", line) + if match_path: + last = list(data["agents"].keys())[-1] + data["agents"][last]["path"] = match_path.group(1) + return data + + +def load_registry(path: str = REGISTRY_DEFAULT) -> Dict[str, Any]: + if not os.path.isfile(path): + raise FileNotFoundError(f"Registry not found: {path}") + return _load_yaml_file(path) + + +def get_agent_spec(registry: Dict[str, Any], name: str) -> AgentSpec: + agents = (registry or {}).get("agents", {}) + if name not in agents: + raise KeyError(f"Agent '{name}' not found in registry") + ent = agents[name] + return AgentSpec(name=name, path=ent.get("path", ""), expected_version=ent.get("version")) + + +def read_agent_file_info(path: str) -> AgentFileInfo: + text = _read_text(path) + fm_text = _extract_frontmatter(text) + fm = _parse_yaml(fm_text) + version = fm.get("version") if isinstance(fm, dict) else None + digest = hashlib.sha256(_read_bytes(path)).hexdigest() + return AgentFileInfo(path=path, version_frontmatter=version, sha256=digest) + + +def validate_agent(registry_path: str, agent_name: str) -> Tuple[AgentSpec, AgentFileInfo]: + reg = load_registry(registry_path) + spec = get_agent_spec(reg, agent_name) + if not spec.path: + raise ValueError(f"Agent '{agent_name}' has no path in registry") + agent_path = spec.path + if not os.path.isabs(agent_path): + agent_path = os.path.abspath(agent_path) + if not os.path.isfile(agent_path): + raise FileNotFoundError(f"Agent file not found: {agent_path}") + info = read_agent_file_info(agent_path) + if spec.expected_version and info.version_frontmatter and str(spec.expected_version) != str(info.version_frontmatter): + raise ValueError( + f"Version mismatch for '{agent_name}': file={info.version_frontmatter} registry={spec.expected_version}" + ) + return spec, info + + +def build_task_prompt(agent_file_path: str, params: Dict[str, Any]) -> str: + lines = [ + f"Load {agent_file_path} and execute with parameters:", + ] + for k, v in params.items(): + lines.append(f"- {k}: {v}") + lines.append("Return ONLY fenced JSON as per the agent's Output Format section.") + return "\n".join(lines) + + +def _ensure_dir(path: str) -> None: + os.makedirs(path, exist_ok=True) + + +def write_audit(agent: AgentSpec, info: AgentFileInfo, outcome: str, duration_ms: Optional[int] = None) -> str: + _ensure_dir(LOGS_DIR) + ts = _dt.datetime.utcnow().isoformat(timespec="seconds") + "Z" + record = { + "timestamp": ts, + "agent": agent.name, + "version_frontmatter": info.version_frontmatter, + "file_sha256": info.sha256, + "invoker": "agent-runner", + "outcome": outcome, + } + if duration_ms is not None: + record["duration_ms"] = duration_ms + fname = f"agent-runner-{agent.name}-{ts.replace(':','').replace('-','').replace('T','_')}.json" + fpath = os.path.join(LOGS_DIR, fname) + with open(fpath, "w", encoding="utf-8") as f: + json.dump(record, f, indent=2) + return fpath + + +def invoke_agent_runner(request: AgentInvokeRequest) -> AgentInvokeResult: + spec, info = validate_agent(request.registry_path, request.agent) + prompt = build_task_prompt(info.path, request.params) + audit_path = write_audit(spec, info, outcome="prepared") + result = AgentInvokeResult( + ok=True, + agent={ + "name": spec.name, + "path": info.path, + "version": info.version_frontmatter, + "sha256": info.sha256, + }, + task_prompt=prompt, + timeout_s=request.timeout_s, + audit_path=audit_path, + note="Agent Runner does not launch the Task tool; pass task_prompt to the Task tool.", + ) + return result diff --git a/packages/sc-refactory/scripts/session_start.py b/packages/sc-refactory/scripts/session_start.py new file mode 100644 index 00000000..7476439a --- /dev/null +++ b/packages/sc-refactory/scripts/session_start.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Inject the refactor trigger index into session startup context.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +from runtime import append_log, find_repo_root, resolve_oxigraph + + +QUERY = """ +PREFIX ref: +SELECT DISTINCT ?signal WHERE { + ?r a ref:Rule . + { ?r ref:triggeredByNamespace ?signal } + UNION + { ?r ref:triggeredByType ?signal } + UNION + { ?r ref:triggeredByError ?signal } + UNION + { ?r ref:triggeredByString ?signal } + UNION + { ?r ref:triggeredByAssembly ?signal } +} +ORDER BY ?signal +""" + +def load_rules_into_db(rules_dir: Path, db_dir: Path, oxigraph: Path) -> None: + """Load all tracked Turtle rule files into a fresh Oxigraph store.""" + db_dir.mkdir(parents=True, exist_ok=True) + + for ttl in sorted(rules_dir.glob("*.ttl")): + result = subprocess.run( + [str(oxigraph), "load", "--location", str(db_dir), "--file", str(ttl)], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + raise RuntimeError(f"failed to load {ttl.name}: {result.stderr.strip()}") + + +def query_triggers(db_dir: Path, query_dir: Path, oxigraph: Path) -> list[dict]: + """Run the trigger query against a store and return JSON bindings.""" + query_dir.mkdir(parents=True, exist_ok=True) + query_file = query_dir / "startup-query.rq" + query_file.write_text(QUERY, encoding="utf-8") + + result = subprocess.run( + [ + str(oxigraph), + "query", + "--location", + str(db_dir), + "--query-file", + str(query_file), + "--results-format", + "json", + ], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + raise RuntimeError(f"startup trigger query failed: {result.stderr.strip()}") + + try: + data = json.loads(result.stdout) + except json.JSONDecodeError: + raise RuntimeError("startup trigger query returned invalid JSON") + return data.get("results", {}).get("bindings", []) + + +def publish_db(build_dir: Path, db_dir: Path, temp_dir: Path) -> None: + """Swap a validated build directory into place atomically enough for startup.""" + backup_dir = temp_dir / "db-previous" + if backup_dir.exists(): + shutil.rmtree(backup_dir, ignore_errors=True) + + if db_dir.exists(): + db_dir.rename(backup_dir) + + try: + build_dir.rename(db_dir) + except Exception: + if backup_dir.exists() and not db_dir.exists(): + backup_dir.rename(db_dir) + raise + else: + if backup_dir.exists(): + shutil.rmtree(backup_dir, ignore_errors=True) + + +def rebuild_db_from_rules(rules_dir: Path, db_dir: Path, temp_dir: Path, oxigraph: Path) -> list[dict]: + """ + Rebuild the runtime DB from committed Turtle rules, validate it with the + startup query, and only then publish it to .refactor/db. + """ + temp_dir.mkdir(parents=True, exist_ok=True) + build_dir = Path(tempfile.mkdtemp(prefix="db-build-", dir=temp_dir)) + + try: + load_rules_into_db(rules_dir, build_dir, oxigraph) + bindings = query_triggers(build_dir, temp_dir, oxigraph) + publish_db(build_dir, db_dir, temp_dir) + return bindings + finally: + if build_dir.exists(): + shutil.rmtree(build_dir) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Refactor trigger index for session context") + parser.add_argument("--mode", default="startup", choices=["startup", "resume", "clear", "compact"]) + parser.add_argument("--atm-team", default=None) + parser.add_argument("--atm-identity", default=None) + return parser.parse_args() + + +def emit_trigger_index(bindings: list[dict], args: argparse.Namespace) -> None: + print("# REFACTOR TRIGGERS") + print("# Approved fix patterns only. On match -> invoke refactor-lookup.") + print("# Use /refactor-lookup when one of these items appears in a build error.") + if args.atm_team or args.atm_identity: + team = args.atm_team or "-" + identity = args.atm_identity or "-" + print(f"# ATM team: {team} identity: {identity}") + print("#") + if not bindings: + print("# (no triggers registered)") + return + for binding in bindings: + signal = binding["signal"]["value"] + print(signal) + + +def main() -> None: + args = parse_args() + repo_root = find_repo_root(Path(__file__)) + refactor_root = repo_root / ".refactor" + temp_dir = refactor_root / "temp" + oxigraph = resolve_oxigraph() + append_log(repo_root, "session_start.log", f"start mode={args.mode}") + + if oxigraph is None: + append_log(repo_root, "session_start.log", "skip reason='oxigraph binary not found'") + sys.exit(0) + + rules_dir = refactor_root / "rules" + db_dir = refactor_root / "db" + + if not rules_dir.is_dir(): + append_log(repo_root, "session_start.log", "skip reason='rules directory missing'") + sys.exit(0) + + try: + append_log( + repo_root, + "session_start.log", + f"rebuild begin rules_dir='{rules_dir}' db_dir='{db_dir}' oxigraph='{oxigraph}'", + ) + bindings = rebuild_db_from_rules(rules_dir, db_dir, temp_dir, oxigraph) + except RuntimeError as exc: + append_log(repo_root, "session_start.log", f"failure {exc}") + sys.exit(0) + + append_log(repo_root, "session_start.log", f"success trigger_count={len(bindings)}") + emit_trigger_index(bindings, args) + + +if __name__ == "__main__": + main() diff --git a/packages/sc-refactory/scripts/sync_subset.py b/packages/sc-refactory/scripts/sync_subset.py new file mode 100644 index 00000000..9b55f25c --- /dev/null +++ b/packages/sc-refactory/scripts/sync_subset.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +"""Sync a filtered local subset of rule/doc source files into `.refactor/`.""" + +from __future__ import annotations + +import argparse +import json +import re +import shutil +import subprocess +from pathlib import Path + +try: + import yaml +except Exception: # pragma: no cover + yaml = None + +from runtime import append_json_log, find_repo_root + +DOC_REF_RE = re.compile(r"\.refactor/docs/[A-Za-z0-9._/\-]+\.md") +RULE_ID_RE = re.compile(r'ref:ruleId\s+"([^"]+)"') + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Sync a local subset of refactor rules into a repo") + parser.add_argument("--repo-root", default=".") + parser.add_argument("--source-root", default=None, help="Bundle root containing rules/ and docs/") + parser.add_argument("--profile", default=None, help="Profile name to resolve under profiles/") + parser.add_argument("--profile-file", default=None, help="Explicit YAML or JSON profile path") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--clean", action="store_true", help="Remove files synced by the previous run before copying") + parser.add_argument("--rebuild-db", action="store_true", help="Rebuild .refactor/db after syncing") + return parser.parse_args() + + +def load_structured_file(path: Path) -> dict: + text = path.read_text(encoding="utf-8") + if path.suffix.lower() == ".json": + return json.loads(text) + if yaml is None: + raise RuntimeError("PyYAML is required to read YAML sync profiles") + return yaml.safe_load(text) or {} + + +def resolve_profile_path(args: argparse.Namespace, repo_root: Path) -> Path | None: + if args.profile_file: + return Path(args.profile_file).resolve() + if not args.profile: + return None + + candidates = [ + repo_root / ".refactor" / "profiles" / f"{args.profile}.yaml", + repo_root / ".refactor" / "profiles" / f"{args.profile}.yml", + repo_root / ".refactor" / "profiles" / f"{args.profile}.json", + ] + for candidate in candidates: + if candidate.is_file(): + return candidate + raise RuntimeError(f"profile not found: {args.profile}") + + +def resolve_source_root(args: argparse.Namespace, profile: dict, profile_path: Path | None) -> Path: + if args.source_root: + return Path(args.source_root).resolve() + source_value = profile.get("source_root") + if source_value: + source_path = Path(source_value) + if not source_path.is_absolute() and profile_path is not None: + source_path = (profile_path.parent / source_path).resolve() + return source_path.resolve() + raise RuntimeError("source root is required; pass --source-root or define source_root in the profile") + + +def load_profile(args: argparse.Namespace, repo_root: Path) -> tuple[dict, Path | None]: + profile_path = resolve_profile_path(args, repo_root) + if profile_path is None: + return {}, None + if not profile_path.is_file(): + raise RuntimeError(f"profile file not found: {profile_path}") + return load_structured_file(profile_path), profile_path + + +def parse_rule_id(ttl_path: Path) -> str | None: + match = RULE_ID_RE.search(ttl_path.read_text(encoding="utf-8")) + return match.group(1) if match else None + + +def match_any(path: Path, patterns: list[str], base: Path) -> bool: + rel = path.relative_to(base).as_posix() + name = path.name + for pattern in patterns: + if Path(rel).match(pattern) or Path(name).match(pattern): + return True + return False + + +def select_rule_files(rules_dir: Path, profile: dict) -> list[Path]: + ttl_files = sorted(rules_dir.glob("*.ttl")) + if not profile: + return ttl_files + + selected: list[Path] = [] + rule_ids = set(profile.get("rule_ids") or []) + rule_files = list(profile.get("rule_files") or []) + include_globs = list(profile.get("include_globs") or []) + + for ttl in ttl_files: + if rule_ids: + parsed_id = parse_rule_id(ttl) + if parsed_id and parsed_id in rule_ids: + selected.append(ttl) + continue + if rule_files and match_any(ttl, rule_files, rules_dir): + selected.append(ttl) + continue + if include_globs and match_any(ttl, include_globs, rules_dir): + selected.append(ttl) + continue + + deduped: dict[str, Path] = {} + for ttl in selected: + deduped[ttl.name] = ttl + return sorted(deduped.values()) + + +def collect_doc_paths(source_root: Path, selected_ttls: list[Path], profile: dict) -> list[Path]: + docs_dir = source_root / "docs" + selected: dict[str, Path] = {} + + for ttl in selected_ttls: + text = ttl.read_text(encoding="utf-8") + for rel in DOC_REF_RE.findall(text): + doc_path = source_root / rel.removeprefix(".refactor/") + if doc_path.is_file(): + selected[doc_path.relative_to(docs_dir).as_posix()] = doc_path + + doc_files = list(profile.get("doc_files") or []) + if docs_dir.is_dir() and doc_files: + for doc in docs_dir.rglob("*.md"): + if match_any(doc, doc_files, docs_dir): + selected[doc.relative_to(docs_dir).as_posix()] = doc + + return sorted(selected.values()) + + +def manifest_path(repo_root: Path) -> Path: + return repo_root / ".refactor" / "temp" / "sync_subset_manifest.json" + + +def load_previous_manifest(repo_root: Path) -> dict: + path = manifest_path(repo_root) + if not path.is_file(): + return {} + try: + return json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + +def write_manifest(repo_root: Path, payload: dict, dry_run: bool) -> None: + if dry_run: + return + path = manifest_path(repo_root) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + + +def remove_previous(repo_root: Path, dry_run: bool) -> list[str]: + previous = load_previous_manifest(repo_root) + removed: list[str] = [] + for rel in previous.get("synced_files", []): + path = repo_root / rel + if path.exists(): + removed.append(rel) + if not dry_run: + path.unlink() + return removed + + +def copy_files(files: list[Path], source_base: Path, dest_base: Path, dry_run: bool) -> list[str]: + copied: list[str] = [] + for src in files: + rel = src.relative_to(source_base) + dst = dest_base / rel + copied.append(dst.as_posix()) + if dry_run: + continue + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + return copied + + +def rebuild_db(repo_root: Path) -> tuple[int, str]: + script = repo_root / ".refactor" / "scripts" / "rebuild_db.py" + result = subprocess.run( + ["python3", str(script), "--repo-root", str(repo_root)], + cwd=repo_root, + capture_output=True, + text=True, + check=False, + ) + return result.returncode, result.stdout.strip() or result.stderr.strip() + + +def main() -> int: + args = parse_args() + repo_root = find_repo_root(Path(args.repo_root)) + profile, profile_path = load_profile(args, repo_root) + source_root = resolve_source_root(args, profile, profile_path) + + rules_dir = source_root / "rules" + docs_dir = source_root / "docs" + if not rules_dir.is_dir(): + raise SystemExit("source root must contain a rules/ directory") + + selected_ttls = select_rule_files(rules_dir, profile) + selected_docs = collect_doc_paths(source_root, selected_ttls, profile) + + removed: list[str] = [] + if args.clean or profile.get("clean") is True: + removed = remove_previous(repo_root, args.dry_run) + + copied_rules = copy_files( + selected_ttls, + rules_dir, + repo_root / ".refactor" / "rules", + args.dry_run, + ) + copied_docs = copy_files( + selected_docs, + docs_dir, + repo_root / ".refactor" / "docs", + args.dry_run, + ) + + rebuild_requested = args.rebuild_db or profile.get("rebuild_db") is True + rebuild_result: dict | None = None + if rebuild_requested and not args.dry_run: + code, output = rebuild_db(repo_root) + rebuild_result = {"returncode": code, "output": output} + + synced_files = copied_rules + copied_docs + write_manifest( + repo_root, + { + "profile": args.profile or profile.get("name"), + "profile_file": str(profile_path) if profile_path else None, + "source_root": str(source_root), + "synced_files": synced_files, + }, + args.dry_run, + ) + + result = { + "success": rebuild_result is None or rebuild_result["returncode"] == 0, + "data": { + "profile": args.profile or profile.get("name"), + "profile_file": str(profile_path) if profile_path else None, + "source_root": str(source_root), + "rule_count": len(copied_rules), + "doc_count": len(copied_docs), + "rules": copied_rules, + "docs": copied_docs, + "removed": removed, + "dry_run": args.dry_run, + "rebuild_db": rebuild_requested, + "rebuild_result": rebuild_result, + }, + "error": None, + } + + append_json_log(repo_root, "sync_subset.log", result["data"]) + print(json.dumps(result, indent=2)) + return 0 if result["success"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/sc-refactory/scripts/validate_agents.py b/packages/sc-refactory/scripts/validate_agents.py new file mode 100644 index 00000000..81e66aaf --- /dev/null +++ b/packages/sc-refactory/scripts/validate_agents.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Validate agent and skill versions against agents/registry.yaml.""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +REGISTRY = ROOT / "agents" / "registry.yaml" +MANIFEST = ROOT / "manifest.yaml" + + +def load_yaml(path: Path) -> dict: + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def frontmatter(path: Path) -> dict: + text = path.read_text(encoding="utf-8") + if not text.startswith("---\n"): + return {} + _, rest = text.split("---\n", 1) + header, _, _ = rest.partition("\n---\n") + return yaml.safe_load(header) or {} + + +def main() -> int: + registry = load_yaml(REGISTRY) + manifest = load_yaml(MANIFEST) + package_version = manifest.get("version") + failures: list[str] = [] + + for name, info in registry.get("agents", {}).items(): + rel_path = info["path"].removeprefix(".claude/") + agent_path = ROOT / rel_path + if not agent_path.exists(): + failures.append(f"missing agent file: {info['path']}") + continue + fm = frontmatter(agent_path) + if fm.get("version") != info.get("version"): + failures.append( + f"agent version mismatch for {name}: file={fm.get('version')} registry={info.get('version')}" + ) + if package_version and fm.get("version") != package_version: + failures.append( + f"agent package-version mismatch for {name}: file={fm.get('version')} package={package_version}" + ) + + for name, info in registry.get("skills", {}).items(): + path = info.get("path") + if not path: + failures.append(f"missing skill path in registry: {name}") + continue + rel_path = path.removeprefix(".claude/") + skill_path = ROOT / rel_path + if not skill_path.exists(): + failures.append(f"missing skill file: {path}") + continue + fm = frontmatter(skill_path) + if fm.get("version") != info.get("version"): + failures.append( + f"skill version mismatch for {name}: file={fm.get('version')} registry={info.get('version')}" + ) + if package_version and fm.get("version") != package_version: + failures.append( + f"skill package-version mismatch for {name}: file={fm.get('version')} package={package_version}" + ) + + if failures: + for failure in failures: + print(f"ERROR: {failure}") + return 1 + + print("All agent and skill versions validated") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packages/sc-refactory/skills/quality-manager/SKILL.md b/packages/sc-refactory/skills/quality-manager/SKILL.md new file mode 100644 index 00000000..b76a0ffc --- /dev/null +++ b/packages/sc-refactory/skills/quality-manager/SKILL.md @@ -0,0 +1,106 @@ +--- +name: quality-manager +version: 0.1.0 +description: > + Behavioral spec for the named `quality-manager` teammate. Use when a + completed refactor wave must be checked for 100% compliance with approved + `.refactor/` rules before commit approval. +--- + +# Quality Manager + +This skill is required reading for the named `quality-manager` teammate. + +## Responsibilities + +- receive wave handoff from `refactor-orchestrator` +- spawn `refactor-qa-agent` background sub-agents as needed +- verify that every change is justified by approved `.refactor/` content +- detect unauthorized edits, missed tandem edits, and drift from approved fix + shape +- report pass/fail status and remediation requirements + +## Inputs + +Expect structured handoff from `refactor-orchestrator` containing: + +- `plan_id` +- `wave` +- `work_item_ids` +- `changed_files` +- `rule_ids` +- `summary` +- optional build/test context + +If rule ids are missing, do not attempt best-effort QA. Fail the wave and ask +for a corrected handoff. + +## QA Questions + +For each wave, answer: + +1. Is every edit justified by one or more approved rules? +2. Were all tandem edits required by those rules completed? +3. Were any extra edits introduced? +4. Does the implementation stay within the approved fix shape? +5. Is a new rule required because the work fell outside the catalog? + +If any answer is negative, the wave is not committable. + +## QA Process + +1. Validate that the handoff is complete enough to audit. +2. Spawn one or more `refactor-qa-agent` workers if parallel review is useful. +3. Aggregate findings by rule id and changed file. +4. Decide `pass` or `fail`. +5. Return a structured status update with blocked items and next action. + +## Review Heuristics + +- Prefer rule documents as primary authority. +- Use sample fixes to judge fix shape, not to invent new scope. +- Treat missing tandem edits as failures, not warnings. +- Treat edits with no clear rule justification as failures, not warnings. + +## Status Reporting + +Return structured status to the controlling lead or session. Include a fenced +JSON block when useful. + +Suggested JSON block: + +```json +{ + "success": true, + "data": { + "role": "quality-manager", + "wave": "wave-02", + "status": "fail", + "approved": false, + "blocked_items": [ + "unauthorized edit in RepoA/Foo.cs", + "missing tandem fix for rule radiant-data-conditional-reference-required" + ], + "next_action": "rework-wave-02" + }, + "error": null +} +``` + +## Pass Criteria + +Approve only when: + +- every changed file is covered by approved rules +- required tandem edits are present +- no unauthorized edits remain +- the wave stays within the approved fix shape closely enough to be safe + +## Failure Boundaries + +Fail the wave when: + +- rule coverage is incomplete +- changed files exceed the declared scope +- the worker output is malformed or missing essential context +- the wave introduces unrelated cleanup or opportunistic edits diff --git a/packages/sc-refactory/skills/refactor-lookup/SKILL.md b/packages/sc-refactory/skills/refactor-lookup/SKILL.md new file mode 100644 index 00000000..3a90166f --- /dev/null +++ b/packages/sc-refactory/skills/refactor-lookup/SKILL.md @@ -0,0 +1,184 @@ +--- +name: refactor-lookup +version: 0.1.0 +description: > + Use this skill before editing when a known trigger appears during a refactor + session. It delegates graph lookup to a focused agent, returns the governing + markdown policy document plus a few repo-root fix references, and enforces the + approved-fixes-only workflow. +--- + +# Refactor Lookup + +Use this skill to answer one question: + +`Does this signal map to an approved refactor rule, and if so what document and sample fixes govern it?` + +Read [workflows.md](./workflows.md) for the full workflow and rationale. +If pre-flight fails, read +`.refactor/docs/install-and-troubleshooting.md`. + +## Agent Delegation + +Use Agent Runner to invoke `refactor-lookup-agent` as defined in +`.claude/agents/registry.yaml`. + +The agent must be invoked with a `0.1.x` version constraint. Treat unfenced or +malformed JSON as failure. + +Before invoking the agent, run the local pre-flight: + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/preflight.py" --skill refactor-lookup +``` + +If this prints `oxigraph v ... checks pass`, proceed. + +If it fails, do not invoke the agent. Read +`.refactor/docs/install-and-troubleshooting.md`, run the scripted repair path, +and rerun pre-flight. The background agent should be reserved for actual rule +lookup, not basic environment diagnosis. + +Runner payload: + +```json +{ + "agent": "refactor-lookup-agent", + "version_constraint": "0.1.x", + "timeout_s": 120, + "params": { + "signals": [ + { + "string": "FocusDistance", + "kind": "type", + "repo_relative_path": "RepoA/src/Optics/LensOperationsTests.cs", + "line": 183 + } + ], + "context": "Compiler error text, CI output, or review context here" + } +} +``` + +## Input to subagent + +```json +{ + "signals": [ + { + "string": "", + "kind": "", + "repo_relative_path": "", + "full_path": "", + "line": 4 + } + ], + "context": "" +} +``` + +- `string` — the signal value +- `kind` — optional but preferred +- `repo_relative_path` — preferred file path for policy references +- `full_path` — optional absolute path when that is what the caller has +- `line` — optional; include when known +- `context` — free text: compile output, test failure, log snippet + +## Subagent returns + +Match: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "matched": true, + "rule_id": "", + "confidence": "", + "reason": "", + "rule_text": "", + "fix": { + "fix_id": "", + "path": "", + "line": 1 + }, + "references": [ + { + "fix_id": "", + "path": "", + "line": 88 + } + ] + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +No match: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "matched": false, + "rule_id": null, + "confidence": "low", + "reason": "", + "rule_text": null, + "fix": null, + "references": [] + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +Failure: + +```json +{ + "success": false, + "canceled": false, + "aborted_by": null, + "data": null, + "error": { + "code": "EXECUTION.GRAPH_UNAVAILABLE", + "message": "Graph store is unavailable", + "recoverable": true, + "suggested_action": "Verify oxigraph is installed and the graph store is readable" + }, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +## After receiving result + +- `success: true` and `data.matched: true` — read the primary markdown doc + first. Read code examples only if the doc alone does not answer the bounded + fix shape. +- `success: true` and `data.matched: false` — proceed without a rule. If you + discover a new approved pattern, invoke `refactor-write`. +- `success: false` — stop. Do not edit until graph access or JSON contract + issues are resolved. +- When signals come from markdown or documentation files, ignore occurrences + that appear only inside fenced code blocks. Fenced examples are documentation, + not actionable source matches. +- A trigger hit should surface the policy document broadly. The document may + still authorize only a narrower edit shape. diff --git a/packages/sc-refactory/skills/refactor-lookup/workflows.md b/packages/sc-refactory/skills/refactor-lookup/workflows.md new file mode 100644 index 00000000..b6a19c4d --- /dev/null +++ b/packages/sc-refactory/skills/refactor-lookup/workflows.md @@ -0,0 +1,73 @@ +# Refactor Lookup Workflows + +## Why This Skill Exists + +This repo is using a strict allowlist model for multi-repo refactors. The main +session should know the concise trigger index, but the expensive graph lookup +and example discovery should stay isolated in the lookup agent. + +Use this skill to answer one question: + +`Does this signal map to an approved refactor rule, and if so what document and sample fixes govern it?` + +Runtime convention: + +- tracked docs: `.refactor/docs/` +- tracked rules: `.refactor/rules/` +- persistent Oxigraph store: `.refactor/db/` +- startup/context scripts: `.refactor/scripts/` +- temp query files and scratch stores: `.refactor/temp/` + +## When To Invoke + +Invoke lookup before editing when any of these show up: + +- a known type or namespace +- a package or project name +- an error code +- a file name used as a policy trigger +- a string from the session-start trigger index + +## Workflow + +1. Extract the smallest useful set of signals from the current problem. +2. Run `python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/preflight.py" --skill refactor-lookup`. +3. If it prints `oxigraph v ... checks pass`, continue. +4. If it fails, read `.refactor/docs/install-and-troubleshooting.md` and run + `python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/repair.py" --skill refactor-lookup`. +5. Re-run pre-flight. +6. If pre-flight now passes, invoke `refactor-lookup-agent` through Agent + Runner. +7. If pre-flight still fails after the scripted repair path, stop and surface a + concise environment problem. +8. If lookup returns a match, read the approved markdown doc first. +9. Read the primary code example only if the doc alone is not enough. +10. Apply only the fix shapes authorized by the matched rule. + +## Interpretation Rules + +- A trigger hit is enough to surface the policy document. +- The lookup agent should reject only obvious false positives, such as hits that + occur only inside fenced code blocks in markdown. +- The approved markdown doc is the primary authority. +- Code examples are secondary references that show shape, not permission to go + beyond the rule. +- Some rules intentionally use one document for multiple triggers. The lookup + result should still surface that single shared document. + +## Repo Path Rules + +- Sample fix paths must start at the multi-repo root: + `RepoA/...`, `RepoB/...`, `.refactor/docs/...` +- Do not return absolute paths in fix references. + +## Failure Handling + +- Prefer local pre-flight failure over background-agent failure. If the + environment is obviously broken, do not spend an agent call on it. +- The skill should attempt the documented scripted repair path itself before + surfacing the problem to the user. +- If the agent returns unfenced JSON or malformed JSON, treat it as failure. +- If the graph is unavailable, stop and surface a concise error. +- If no rule matches, proceed without a rule only after lookup completes with a + valid `matched: false` result. diff --git a/packages/sc-refactory/skills/refactor-orchestrate/SKILL.md b/packages/sc-refactory/skills/refactor-orchestrate/SKILL.md new file mode 100644 index 00000000..f4d98143 --- /dev/null +++ b/packages/sc-refactory/skills/refactor-orchestrate/SKILL.md @@ -0,0 +1,129 @@ +--- +name: refactor-orchestrate +version: 0.1.0 +description: > + Behavioral spec for the named `refactor-orchestrator` teammate. Use when a + rule-backed refactoring plan must be executed in development waves with QA + handoff to the named `quality-manager` teammate before commit. +--- + +# Refactor Orchestrate + +This skill is required reading for the named `refactor-orchestrator` teammate. + +Do not use a normal Agent Delegation table here. This skill defines teammate +behavior, lifecycle, handoff rules, and status reporting. + +## Responsibilities + +- load a refactoring plan made only of approved rule-backed items +- partition work into bounded development waves +- spawn `refactor-dev-agent` background sub-agents for authorized work +- hand off each completed wave to the named `quality-manager` teammate +- collect QA results and decide whether to rework, escalate, or mark approved +- allow commit only after explicit QA approval + +## Inputs + +Expect structured assignments containing: + +- `plan_id` +- repos or repo-set in scope +- ordered wave list +- work items per wave +- approved rule ids per work item +- commit boundary guidance +- optional prior status from earlier waves + +## Rules + +- Never authorize edits outside committed `.refactor/` rules. +- Never let a dev wave proceed without a backing rule id. +- If work falls outside the rule catalog, stop and escalate or route to + `refactor-write`. +- Track wave status explicitly: pending, in-progress, blocked, failed-qa, + approved. +- Do not blur dev and QA responsibilities. Hand off to `quality-manager` for + explicit approval. + +## Development Wave Pattern + +1. Validate the next wave against the active plan. +2. Spawn one or more `refactor-dev-agent` workers with narrow scope. +3. Wait for worker results and aggregate changed files. +4. Send the wave result to `quality-manager`. +5. Do not approve commit until QA returns pass. + +## Sub-Agent Spawning Rules + +- Spawn only bounded `refactor-dev-agent` work items. +- Prefer one rule family per worker when possible. +- Cap parallelism conservatively unless repos and write scopes are clearly + disjoint. +- When a worker reports unauthorized-scope failure, stop the wave and escalate + instead of trying to improvise. + +## Handoff To Quality Manager + +The handoff should include: + +- wave id +- work item ids +- changed files +- approved rule ids +- summary of what was intended +- any known caveats or partial failures + +## Wave State Model + +Use this state model: + +- `pending` +- `in-progress` +- `awaiting-qa` +- `failed-qa` +- `approved` +- `blocked` + +## Status Reporting + +Send structured status messages to the controlling lead or session. Include a +fenced JSON block when useful. + +Minimum status fields: + +- `role` +- `wave` +- `status` +- `summary` +- `next_action` + +Suggested JSON block: + +```json +{ + "success": true, + "data": { + "role": "refactor-orchestrator", + "wave": "wave-02", + "status": "awaiting-qa", + "approved": false, + "next_action": "quality-manager-review" + }, + "error": null +} +``` + +## Failure Boundaries + +Escalate instead of improvising when: + +- a work item needs edits not covered by existing `.refactor/` rules +- a dev worker returns malformed or unfenced JSON +- a dev worker edits files outside assigned scope +- QA reports unauthorized edits or missing tandem fixes + +## Commit Rule + +The orchestrator never treats “looks fine” as sufficient. A wave is committable +only after an explicit QA pass from `quality-manager`. diff --git a/packages/sc-refactory/skills/refactor-write/SKILL.md b/packages/sc-refactory/skills/refactor-write/SKILL.md new file mode 100644 index 00000000..61752f82 --- /dev/null +++ b/packages/sc-refactory/skills/refactor-write/SKILL.md @@ -0,0 +1,142 @@ +--- +name: refactor-write +version: 0.1.0 +description: > + Use this skill to author or update approved refactor rules. The workflow is: + write the authoritative markdown doc, capture a minimal trigger set, add a + few repo-root sample fix references, write the Turtle source of truth under + `.refactor/rules/`, and verify lookup in Oxigraph using `.refactor/db/` or a + temporary store. +--- + +# Refactor Write + +Read [workflows.md](./workflows.md) for the full authoring workflow and the +temporary-store verification pattern. +If pre-flight fails, read +`.refactor/docs/install-and-troubleshooting.md`. + +## Agent Delegation + +Use Agent Runner to invoke `refactor-write-agent` as defined in +`.claude/agents/registry.yaml`. + +The agent must be invoked with a `0.1.x` version constraint. Treat unfenced or +malformed JSON as failure. + +Before invoking the agent, run the local pre-flight: + +```bash +python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/preflight.py" --skill refactor-write +``` + +If this prints `oxigraph v ... checks pass`, proceed. + +If it fails, do not invoke the agent. Read +`.refactor/docs/install-and-troubleshooting.md`, run the scripted repair path, +and rerun pre-flight first. + +## /rule-write + +```json +{ + "operation": "rule", + "rule_id": "", + "severity": "", + "doc_path": ".refactor/docs/.md", + "triggers": [ + { "signal": "FocusDistance", "kind": "type" }, + { "signal": "Legacy.Imaging.ExposureTime", "kind": "string" } + ], + "summary": "", + "allowed_fixes": [ + "", + "" + ], + "notes": "", + "derived_from": null +} +``` + +Valid `kind` values: `namespace`, `type`, `error`, `string`, `assembly` + +## /fix-write + +Multiple fixes may be written in one call. Each fix is a pointer to where the +rule is documented or already applied in the codebase. Use repo-root-relative +paths only. + +```json +{ + "operation": "fix", + "rule_id": "", + "fixes": [ + { + "fix_id": "fix-001", + "path": ".refactor/docs/example-rule.md", + "line": 1, + "confidence": "high", + "source": "approved-doc" + }, + { + "fix_id": "fix-002", + "path": "RepoA/Path/Example.cs", + "line": 177, + "confidence": "high", + "source": "recent-git-example" + } + ] +} +``` + +- `path` — repo-root-relative path to the file containing the approved example +- `line` — line number where the fix is visible +- `confidence` — `high`, `medium`, or `low` +- `source` — use values such as `approved-doc`, `canonical-example`, + `recent-git-example`, or another concise descriptive label + +## Agent returns + +Success: + +```json +{ + "success": true, + "canceled": false, + "aborted_by": null, + "data": { + "operation": "", + "ids": ["", ""], + "ttl_paths": [".refactor/rules/.ttl"], + "loaded_to_store": true + }, + "error": null, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` + +Failure: + +```json +{ + "success": false, + "canceled": false, + "aborted_by": null, + "data": null, + "error": { + "code": "VALIDATION.INPUT", + "message": "Fix path must be repo-root-relative", + "recoverable": true, + "suggested_action": "Replace the absolute path with a repo-root-relative path" + }, + "metadata": { + "duration_ms": 0, + "tool_calls": 0, + "retry_count": 0 + } +} +``` diff --git a/packages/sc-refactory/skills/refactor-write/workflows.md b/packages/sc-refactory/skills/refactor-write/workflows.md new file mode 100644 index 00000000..13ec30bd --- /dev/null +++ b/packages/sc-refactory/skills/refactor-write/workflows.md @@ -0,0 +1,84 @@ +# Refactor Write Workflows + +## Why This Skill Exists + +New refactor rules are policy, not just notes. They need: + +- a concise trigger or trigger set, +- one authoritative markdown how-to document, +- a few repo-root sample fix references, +- a Turtle source-of-truth entry that lookup can query safely. + +This keeps future sessions bounded to approved fixes instead of open-ended +refactoring. + +## Authoring Workflow + +1. Write or update the authoritative markdown document first. +2. Keep the document scoped to one coherent fix policy. +3. Choose the minimal trigger set that should surface that document. +4. Run `python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/preflight.py" --skill refactor-write`. +5. If it prints `oxigraph v ... checks pass`, continue. +6. If it fails, read `.refactor/docs/install-and-troubleshooting.md` and run + `python3 "$(git rev-parse --show-toplevel)/.refactor/scripts/repair.py" --skill refactor-write`. +7. Re-run pre-flight. +8. If pre-flight still fails after the scripted repair path, stop and surface a + concise environment problem. +9. Pick 1-4 sample fix references from multiple repos when possible. +10. Write the TTL rule and fix entries under `.refactor/rules/`. +11. Verify lookup in a temporary Oxigraph store before relying on the rule. + +## Authoring Rules + +- One document may support multiple precise triggers. +- The first fix reference should normally be the markdown doc itself with + `source: approved-doc`. +- Sample fix paths must be repo-root relative. +- Prefer one sample per repo over many samples from a single repo. +- Skip examples that appear only inside fenced code blocks in markdown. + +## Verification Workflow + +Use a temporary store so verification does not depend on stale local DB state. + +```bash +tmpdir="$(mktemp -d)" + +oxigraph load \ + --location "$tmpdir" \ + --file .refactor/rules/example-rule.ttl + +cat > "$tmpdir/query.rq" <<'SPARQL' +PREFIX ref: +SELECT ?ruleId ?fixPath ?fixLine WHERE { + ?r a ref:Rule ; + ref:ruleId ?ruleId ; + ref:triggeredByString "ExampleTrigger" ; + ref:hasFix ?f . + ?f ref:fixPath ?fixPath ; + ref:fixLine ?fixLine . +} +ORDER BY ?fixLine +SPARQL + +oxigraph query \ + --location "$tmpdir" \ + --query-file "$tmpdir/query.rq" \ + --results-format json + +rm -rf "$tmpdir" +``` + +## Persistent Store Note + +`oxigraph load` appends triples. Rewriting an existing `.ttl` file and loading it +again can create duplicate local store state. The git-tracked `.ttl` files are +the source of truth; use a temporary store for verification whenever you are +changing a rule definition. + +Persistent runtime convention: + +- tracked docs: `.refactor/docs/` +- tracked rules: `.refactor/rules/` +- persistent Oxigraph store: `.refactor/db/` +- temp query files and scratch stores: `.refactor/temp/` diff --git a/packages/sc-refactory/skills/refactory-design/SKILL.md b/packages/sc-refactory/skills/refactory-design/SKILL.md new file mode 100644 index 00000000..3fa39775 --- /dev/null +++ b/packages/sc-refactory/skills/refactory-design/SKILL.md @@ -0,0 +1,154 @@ +--- +name: refactory-design +version: 0.1.0 +description: > + Use this skill to design a constrained refactoring system before any package + or runtime is built. It walks through approved-rule design, trigger and + sample-fix guidelines, startup authorization policy, named-teammate + responsibilities, QA gating, and package/runtime layout. +--- + +# Refactory Design + +Use this skill when the goal is to create or package a rule-driven refactoring +toolkit, not to apply one specific refactor. + +## When To Use + +- designing a new approved-fix catalog +- converting repeated migration knowledge into reusable rules +- deciding what startup should and should not inject +- defining QA and commit-gate policy for large refactors +- packaging the system for reuse in other repos + +## Phase 1: Rule Discovery + +Work through these questions first: + +1. What changes are explicitly allowed? +2. What changes are explicitly prohibited? +3. What should count as one rule versus several related rules? +4. Which fixes must always happen in tandem? +5. What trigger values are concise and operator-recognizable? +6. What belongs in startup context versus only after lookup? +7. Which examples are canonical and cross-repo? +8. What QA checks must always pass before commit? + +Produce: + +- rule boundary guidelines +- trigger guidelines +- sample-fix guidelines +- startup allow/deny policy text +- QA criteria + +Do not skip this phase. If the rules are underspecified, the graph, startup +context, teammates, and QA layer all become noisy or unsafe. + +## Rule Design Checklist + +For each proposed rule, decide: + +- what exact change is approved +- what exact change is prohibited +- what trigger or trigger set should surface the rule +- whether multiple triggers should share one document +- whether multiple edits must happen in tandem +- what examples best demonstrate the approved fix shape +- what false positives should be ignored +- what QA assertions must be true before commit + +Minimum startup policy text should include: + +```text +# Only changes covered by approved .refactor/ rules are allowed. +# If a needed fix is not in .refactor/, stop or add the rule before editing. +``` + +## Phase 2: Runtime Design + +Decide: + +- installed runtime layout under `.refactor/` +- startup provider path under `.startup/` +- package source layout under `packages/sc-refactory/` +- rule doc and rule graph formats +- preflight, repair, startup, and logging responsibilities +- named teammate responsibilities +- background sub-agent boundaries + +Runtime decisions must be explicit about source-of-truth: + +- committed docs live in `.refactor/docs/` +- committed Turtle rules live in `.refactor/rules/` +- runtime DB is `.refactor/db/` and is disposable +- logs live in `.refactor/logs/` +- temp artifacts live in `.refactor/temp/` + +## Phase 3: Execution Model + +Design: + +- plan item schema +- development wave model +- QA wave model +- handoff between `refactor-orchestrator` and `quality-manager` +- commit gates +- escalation path for work not covered by `.refactor/` + +The intended execution model is: + +1. build a plan where every item cites approved rule ids +2. `refactor-orchestrator` launches bounded development waves +3. `quality-manager` launches QA review waves +4. failed waves return to rework or escalation +5. only QA-approved waves are committable + +## Phase 4: Package Outputs + +Produce: + +- package manifest +- plugin metadata +- background-agent registry +- skill list +- teammate prompts +- runtime script inventory +- installation steps +- validation plan + +## Recommended Output Shape + +When you finish the design pass, provide: + +1. a short summary of the policy model +2. the rule-design guidelines +3. the startup allow/deny text +4. the teammate and sub-agent boundaries +5. the package skeleton plan +6. open issues that need a human decision + +## Larger Design Risks To Surface + +Always call out: + +- rules that are too broad or too vague +- startup trigger lists that are too long +- teammates that would need to coordinate mutable shared state +- QA expectations that cannot be checked mechanically +- missing examples for high-risk rules + +## Core Policy + +- Only changes covered by committed `.refactor/` content are authorized. +- Trigger hits authorize lookup, not immediate editing. +- If a needed fix is not represented in `.refactor/`, stop, escalate, or add + the rule through the write flow before editing. +- QA must verify that 100% of changes are justified by approved rules before + commit. + +## Constraints + +- Do not jump straight to graph schema or scripting before rule design. +- Do not treat “similar” fixes as equivalent without explicit rule coverage. +- Do not recommend startup injection of full docs or examples. diff --git a/packages/sc-refactory/skills/refactory-install/SKILL.md b/packages/sc-refactory/skills/refactory-install/SKILL.md new file mode 100644 index 00000000..fac7e142 --- /dev/null +++ b/packages/sc-refactory/skills/refactory-install/SKILL.md @@ -0,0 +1,106 @@ +--- +name: refactory-install +version: 0.1.0 +description: > + Use this skill to materialize the refactory runtime into a repo by creating + `.refactor/` and `.startup/`, copying the runtime scripts, writing the local + install/troubleshooting guide, and verifying startup output and preflight. +--- + +# Refactory Install + +Use this skill after the policy design is stable enough to bootstrap a repo. + +## What This Installs + +- `.refactor/docs/` +- `.refactor/rules/` +- `.refactor/profiles/` +- `.refactor/scripts/` +- `.refactor/reports/` +- `.refactor/db/` +- `.refactor/logs/` +- `.refactor/temp/` +- `.startup/team-lead` + +The runtime scripts are copied from the installed package scripts into the +repo-local `.refactor/scripts/` folder. + +## Installation Preconditions + +Before running the installer, confirm: + +- the repo root is known +- the rule design is stable enough to define runtime layout +- `oxigraph` is installed from crates.io or the user accepts installer guidance +- the repo should receive a local-only install + +## How To Invoke + +Resolve the installed package script from the local repo first, then fall back +to the global install if needed. + +Local-first pattern: + +```bash +repo_root="$(git rev-parse --show-toplevel)" + +if [ -f "$repo_root/.claude/scripts/install_refactory.py" ]; then + python3 "$repo_root/.claude/scripts/install_refactory.py" --repo-root "$repo_root" +else + python3 "$HOME/.claude/scripts/install_refactory.py" --repo-root "$repo_root" +fi +``` + +Optional flags: + +- `--force` to refresh runtime-managed files +- `--seed empty` for runtime only +- `--seed templates` to include starter rule templates + +## What The Installer Must Do + +1. Resolve the target repo root. +2. Create `.refactor/` directories. +3. Create `.startup/team-lead`. +4. Copy runtime scripts into `.refactor/scripts/`. +5. Write `.refactor/.gitignore`. +6. Write `.refactor/docs/install-and-troubleshooting.md`. +7. Optionally install starter templates. +8. Render a startup preview. + +## Expected Result + +- `.refactor/.gitignore` ignores `db/`, `logs/`, and `temp/` +- `.startup/team-lead` exists +- `.refactor/scripts/session_start.py` exists +- `.refactor/docs/install-and-troubleshooting.md` exists +- `python3 .refactor/scripts/preflight.py --skill refactor-lookup` prints + `oxigraph v ... checks pass` when the environment is healthy + +## Verification Checklist + +After installation, verify: + +1. `.startup/team-lead` executes without traceback +2. `python3 .refactor/scripts/session_start.py --mode startup` prints a trigger + block or `(no triggers registered yet)` +3. `python3 .refactor/scripts/preflight.py --skill refactor-lookup` succeeds + when the runtime is healthy +4. `.refactor/db/`, `.refactor/logs/`, and `.refactor/temp/` are ignored by + `.refactor/.gitignore` + +## Failure Handling + +- If `oxigraph` is missing, surface the install guidance and stop. +- If the repo already contains user-authored rules or docs, do not overwrite + them unless `--force` or explicit approval says to do so. +- If startup preview fails, keep the installed files but report the exact stage + that failed. + +## Safety + +- Do not overwrite existing rule docs or TTL files unless the user asked for it. +- Runtime scripts may be refreshed during reinstall. +- The installer should be deterministic and low freedom. +- Treat `.refactor/docs/` and `.refactor/rules/` as user-owned content. diff --git a/packages/sc-refactory/tests/test_package_layout.py b/packages/sc-refactory/tests/test_package_layout.py new file mode 100644 index 00000000..deecb769 --- /dev/null +++ b/packages/sc-refactory/tests/test_package_layout.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +from pathlib import Path +import subprocess +import tempfile + +import yaml + + +def test_manifest_artifacts_exist() -> None: + root = Path(__file__).resolve().parents[1] + manifest = yaml.safe_load((root / "manifest.yaml").read_text(encoding="utf-8")) + + for _, paths in manifest["artifacts"].items(): + for rel_path in paths: + assert (root / rel_path).exists(), rel_path + + +def test_registry_paths_match_installed_layout() -> None: + root = Path(__file__).resolve().parents[1] + registry = yaml.safe_load((root / "agents" / "registry.yaml").read_text(encoding="utf-8")) + + for info in registry["agents"].values(): + assert info["path"].startswith(".claude/agents/") + + for info in registry["skills"].values(): + assert info["path"].startswith(".claude/skills/") + + +def test_install_refactory_smoke() -> None: + root = Path(__file__).resolve().parents[1] + with tempfile.TemporaryDirectory() as tmp: + repo = Path(tmp) / "repo" + repo.mkdir() + subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True) + + script = root / "scripts" / "install_refactory.py" + subprocess.run( + ["python3", str(script), "--repo-root", str(repo), "--seed", "templates"], + check=True, + capture_output=True, + text=True, + ) + + expected = [ + repo / ".startup" / "team-lead", + repo / ".refactor" / ".gitignore", + repo / ".refactor" / "docs" / "install-and-troubleshooting.md", + repo / ".refactor" / "docs" / "rule-template.md", + repo / ".refactor" / "profiles", + repo / ".refactor" / "rules" / "rule-template.ttl", + repo / ".refactor" / "scripts" / "session_start.py", + repo / ".refactor" / "scripts" / "preflight.py", + repo / ".refactor" / "scripts" / "rebuild_db.py", + repo / ".refactor" / "scripts" / "sync_subset.py", + ] + + for path in expected: + assert path.exists(), str(path) + + +def test_sc_install_package_smoke() -> None: + root = Path(__file__).resolve().parents[1] + repo_root = root.parents[1] + + with tempfile.TemporaryDirectory() as tmp: + dest = Path(tmp) / ".claude" + subprocess.run( + [ + "python3", + str(repo_root / "tools" / "sc-install.py"), + "install", + "sc-refactory", + "--dest", + str(dest), + ], + cwd=repo_root, + check=True, + capture_output=True, + text=True, + ) + + installed = [ + dest / "commands" / "sc-refactory-install.md", + dest / "skills" / "refactor-lookup" / "SKILL.md", + dest / "agents" / "refactor-lookup-agent.md", + dest / "agents" / "registry.yaml", + dest / "scripts" / "install_refactory.py", + dest / ".claude-plugin" / "plugin.json", + dest / "assets" / "startup-wrapper-template" / "team-lead.py", + ] + for path in installed: + assert path.exists(), str(path) + + with tempfile.TemporaryDirectory() as repo_tmp: + repo = Path(repo_tmp) / "repo" + repo.mkdir() + subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True) + subprocess.run( + [ + "python3", + str(dest / "scripts" / "install_refactory.py"), + "--repo-root", + str(repo), + "--seed", + "templates", + ], + check=True, + capture_output=True, + text=True, + ) + + runtime_paths = [ + repo / ".startup" / "team-lead", + repo / ".refactor" / ".gitignore", + repo / ".refactor" / "docs" / "install-and-troubleshooting.md", + repo / ".refactor" / "docs" / "rule-template.md", + repo / ".refactor" / "profiles", + repo / ".refactor" / "rules" / "rule-template.ttl", + repo / ".refactor" / "scripts" / "session_start.py", + repo / ".refactor" / "scripts" / "preflight.py", + ] + for path in runtime_paths: + assert path.exists(), str(path) + + +def test_sync_subset_smoke() -> None: + root = Path(__file__).resolve().parents[1] + with tempfile.TemporaryDirectory() as tmp: + repo = Path(tmp) / "repo" + repo.mkdir() + subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True) + + install_script = root / "scripts" / "install_refactory.py" + subprocess.run( + ["python3", str(install_script), "--repo-root", str(repo), "--seed", "empty"], + check=True, + capture_output=True, + text=True, + ) + + bundle = Path(tmp) / "bundle" + (bundle / "rules").mkdir(parents=True) + (bundle / "docs").mkdir(parents=True) + (bundle / "profiles").mkdir(parents=True) + + (bundle / "docs" / "focus-distance.md").write_text( + "# Focus Distance\n", + encoding="utf-8", + ) + (bundle / "rules" / "focus-distance.ttl").write_text( + """@prefix ref: . + +ref:focus-distance + a ref:Rule ; + ref:ruleId "focus-distance" ; + ref:severity "warning" ; + ref:ruleText "Use the shared focus-distance policy." ; + ref:triggeredByType "FocusDistance" ; + ref:hasFix [ + ref:path ".refactor/docs/focus-distance.md" ; + ref:line 1 + ] . +""", + encoding="utf-8", + ) + (bundle / "profiles" / "sample.yaml").write_text( + f"""name: sample +source_root: {bundle} +rule_ids: + - focus-distance +""", + encoding="utf-8", + ) + + sync_script = repo / ".refactor" / "scripts" / "sync_subset.py" + subprocess.run( + ["python3", str(sync_script), "--repo-root", str(repo), "--profile-file", str(bundle / "profiles" / "sample.yaml")], + check=True, + capture_output=True, + text=True, + ) + + assert (repo / ".refactor" / "rules" / "focus-distance.ttl").exists() + assert (repo / ".refactor" / "docs" / "focus-distance.md").exists() + assert (repo / ".refactor" / "temp" / "sync_subset_manifest.json").exists() + + +if __name__ == "__main__": + test_manifest_artifacts_exist() + test_registry_paths_match_installed_layout() + test_install_refactory_smoke() + test_sc_install_package_smoke() + test_sync_subset_smoke() diff --git a/scripts/audit-versions.py b/scripts/audit-versions.py index e893ab18..dd80f20d 100755 --- a/scripts/audit-versions.py +++ b/scripts/audit-versions.py @@ -393,7 +393,7 @@ def audit_version_consistency( if ( not package_dir.is_dir() or package_dir.name.startswith(".") - or package_dir.name == "shared" + or package_dir.name in {"shared", "docs"} ): continue @@ -493,7 +493,7 @@ def audit_changelogs(repo_root: Path, verbose: bool = False) -> Result[list[Chec if ( not package_dir.is_dir() or package_dir.name.startswith(".") - or package_dir.name == "shared" + or package_dir.name in {"shared", "docs"} ): continue diff --git a/scripts/sync-shared-scripts.py b/scripts/sync-shared-scripts.py index 0b935c62..067af2bc 100644 --- a/scripts/sync-shared-scripts.py +++ b/scripts/sync-shared-scripts.py @@ -21,7 +21,7 @@ def iter_packages(packages_dir: Path) -> Iterable[Path]: for path in sorted(packages_dir.iterdir()): - if path.is_dir() and not path.name.startswith("."): + if path.is_dir() and not path.name.startswith(".") and path.name not in {"shared", "docs"}: yield path @@ -114,8 +114,6 @@ def sync_shared_script(canonical: Path, packages_dir: Path) -> list[str]: repo_root = packages_dir.parent for package_dir in iter_packages(packages_dir): - if package_dir.name == "shared": - continue for mapping in shared_script_mappings(package_dir, repo_root, canonical): if not mapping.canonical.exists(): continue diff --git a/scripts/update-registry.py b/scripts/update-registry.py index 24e366fc..ce25ff61 100644 --- a/scripts/update-registry.py +++ b/scripts/update-registry.py @@ -143,7 +143,7 @@ def find_packages(packages_dir: Path) -> list[Path]: [ d for d in packages_dir.iterdir() - if d.is_dir() and not d.name.startswith(".") and d.name != "shared" + if d.is_dir() and not d.name.startswith(".") and d.name not in {"shared", "docs"} ] ) diff --git a/scripts/validate-manifest-artifacts.py b/scripts/validate-manifest-artifacts.py index c6045e49..0e7f5845 100755 --- a/scripts/validate-manifest-artifacts.py +++ b/scripts/validate-manifest-artifacts.py @@ -411,8 +411,8 @@ def main() -> int: # Determine packages to validate if args.package: - if args.package == "shared": - print("Skipping packages/shared (not a package)") + if args.package in {"shared", "docs"}: + print(f"Skipping packages/{args.package} (not a package)") return 0 package_dirs = [args.packages_dir / args.package] else: @@ -423,7 +423,7 @@ def main() -> int: package_dirs = [ d for d in args.packages_dir.iterdir() - if d.is_dir() and not d.name.startswith(".") and d.name != "shared" + if d.is_dir() and not d.name.startswith(".") and d.name not in {"shared", "docs"} ] if not package_dirs: diff --git a/scripts/validate-marketplace-sync.py b/scripts/validate-marketplace-sync.py index ac582fd5..2763b40e 100755 --- a/scripts/validate-marketplace-sync.py +++ b/scripts/validate-marketplace-sync.py @@ -260,7 +260,7 @@ def get_package_dirs(packages_dir: Path) -> list[Path]: return [ d for d in packages_dir.iterdir() - if d.is_dir() and not d.name.startswith(".") and d.name != "shared" + if d.is_dir() and not d.name.startswith(".") and d.name not in {"shared", "docs"} ] @@ -614,8 +614,8 @@ def main() -> int: parser.add_argument( "--registry", type=Path, - default=Path("docs/registries/nuget/registry.json"), - help="Path to registry.json (default: docs/registries/nuget/registry.json)", + default=Path(".claude-plugin/registry.json"), + help="Path to registry.json (default: .claude-plugin/registry.json)", ) args = parser.parse_args() diff --git a/scripts/validate-shared-scripts.py b/scripts/validate-shared-scripts.py index 5b211b99..8772a2b5 100644 --- a/scripts/validate-shared-scripts.py +++ b/scripts/validate-shared-scripts.py @@ -24,7 +24,7 @@ def iter_packages(packages_dir: Path) -> Iterable[Path]: for path in sorted(packages_dir.iterdir()): - if path.is_dir() and not path.name.startswith("."): + if path.is_dir() and not path.name.startswith(".") and path.name not in {"shared", "docs"}: yield path @@ -125,9 +125,6 @@ def compare_shared_script(canonical: Path, packages_dir: Path) -> tuple[list[str repo_root = packages_dir.parent for package_dir in iter_packages(packages_dir): - if package_dir.name == "shared": - continue - for mapping in shared_script_mappings(package_dir, repo_root, canonical): if not mapping.canonical.exists(): invalid_sources.append(f"{describe_mapping(package_dir, mapping.target)} -> {mapping.canonical}") @@ -148,9 +145,6 @@ def sync_shared_script(canonical: Path, packages_dir: Path) -> list[str]: repo_root = packages_dir.parent for package_dir in iter_packages(packages_dir): - if package_dir.name == "shared": - continue - for mapping in shared_script_mappings(package_dir, repo_root, canonical): if not mapping.canonical.exists(): continue diff --git a/src/sc_cli/install.py b/src/sc_cli/install.py index 4d2fcf1d..b95d28a0 100644 --- a/src/sc_cli/install.py +++ b/src/sc_cli/install.py @@ -117,7 +117,14 @@ def _parse_manifest(pkg_dir: Path) -> Manifest: ) # Fallback: minimal line parser for artifacts sections - artifacts: Dict[str, List[str]] = {"commands": [], "skills": [], "agents": [], "scripts": [], "assets": []} + artifacts: Dict[str, List[str]] = { + "commands": [], + "skills": [], + "agents": [], + "scripts": [], + "assets": [], + "plugin": [], + } current: Optional[str] = None version = "" for line in _read_file(manifest_path).splitlines(): @@ -875,7 +882,7 @@ def _git_repo_basename(dest_dir: Path) -> str: def _iter_artifacts(m: Manifest) -> Iterable[str]: - order = ["commands", "skills", "agents", "scripts", "assets"] + order = ["commands", "skills", "agents", "scripts", "assets", "plugin"] for key in order: for item in m.artifacts.get(key, []): yield item diff --git a/tests/test_sc_install.py b/tests/test_sc_install.py index 7866cdd0..5ae79d29 100644 --- a/tests/test_sc_install.py +++ b/tests/test_sc_install.py @@ -71,3 +71,16 @@ def test_install_sc_ai_cli_copies_template_assets(tmp_path: Path): assert (dest / "skills/creating-ai-clis/SKILL.md").exists() assert (dest / "skills/creating-ai-clis/assets/templates/rust/Cargo.toml.j2").exists() assert (dest / "skills/designing-cli-simulators/assets/templates/go/simulator.go.j2").exists() + + +def test_install_sc_refactory_copies_plugin_artifact(tmp_path: Path): + repo = tmp_path / "repo" + repo.mkdir() + _init_git_repo(repo) + dest = repo / ".claude" + + rc = sc_install.main(["install", "sc-refactory", "--dest", str(dest)]) + assert rc == 0 + + assert (dest / ".claude-plugin" / "plugin.json").exists() + assert (dest / "assets" / "startup-wrapper-template" / "team-lead.py").exists()