diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6f29b6b..bfe7d24 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -90,6 +90,26 @@ "debugging" ] }, + { + "name": "self-improving-operator", + "source": "./plugins/self-improving-operator", + "description": "Persistent autonomy plugin for Claude Code that maintains repo-local state, refreshes a prioritized backlog, checkpoints verified progress, and keeps going until a real stop reason is reached.", + "version": "2.0.0", + "author": { + "name": "Da Wei", + "url": "https://github.com/wd041216-bit" + }, + "category": "Workflow Orchestration", + "homepage": "https://github.com/wd041216-bit/claude-code-self-improving-operator", + "keywords": [ + "workflow", + "automation", + "autonomy", + "backlog", + "checkpoint", + "subagent" + ] + }, { "name": "code-review", "source": "./plugins/code-review", @@ -1672,4 +1692,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/README-zh.md b/README-zh.md index 2c2de0e..543dae2 100644 --- a/README-zh.md +++ b/README-zh.md @@ -61,6 +61,7 @@ - [lyra](./plugins/lyra) - [model-context-protocol-mcp-expert](./plugins/model-context-protocol-mcp-expert) - [problem-solver-specialist](./plugins/problem-solver-specialist) +- [self-improving-operator](./plugins/self-improving-operator) - [studio-coach](./plugins/studio-coach) - [ultrathink](./plugins/ultrathink) @@ -217,4 +218,4 @@ * 添加你喜欢的插件 * 分享最佳实践 -* 提交你自己的插件 \ No newline at end of file +* 提交你自己的插件 diff --git a/README.md b/README.md index e4de615..234c0f5 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Install or disable them dynamically with the `/plugin` command — enabling you - [lyra](./plugins/lyra) - [model-context-protocol-mcp-expert](./plugins/model-context-protocol-mcp-expert) - [problem-solver-specialist](./plugins/problem-solver-specialist) +- [self-improving-operator](./plugins/self-improving-operator) - [studio-coach](./plugins/studio-coach) - [ultrathink](./plugins/ultrathink) @@ -212,4 +213,4 @@ Example: ## Contributing Contributions are welcome! - You can add your favorite plugins, share best practices, or submit your own marketplace. \ No newline at end of file + You can add your favorite plugins, share best practices, or submit your own marketplace. diff --git a/plugins/self-improving-operator/.claude-plugin/plugin.json b/plugins/self-improving-operator/.claude-plugin/plugin.json new file mode 100644 index 0000000..7f0d651 --- /dev/null +++ b/plugins/self-improving-operator/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "self-improving-operator", + "version": "2.0.0", + "description": "Claude Code plugin for persistent diagnose-build-verify-checkpoint loops with repo-local state and aggressive in-scope continuation.", + "author": { + "name": "Da Wei", + "url": "https://github.com/wd041216-bit" + }, + "homepage": "https://github.com/wd041216-bit/claude-code-self-improving-operator", + "repository": "https://github.com/wd041216-bit/claude-code-self-improving-operator", + "license": "Apache-2.0", + "keywords": [ + "claude-code", + "plugin", + "autonomy", + "subagent", + "backlog", + "checkpoint" + ] +} diff --git a/plugins/self-improving-operator/.github/workflows/validate.yml b/plugins/self-improving-operator/.github/workflows/validate.yml new file mode 100644 index 0000000..f738bba --- /dev/null +++ b/plugins/self-improving-operator/.github/workflows/validate.yml @@ -0,0 +1,20 @@ +name: Validate Plugin + +on: + push: + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Validate plugin structure and generation parity + run: python3 scripts/validate_plugin.py diff --git a/plugins/self-improving-operator/.gitignore b/plugins/self-improving-operator/.gitignore new file mode 100644 index 0000000..027445c --- /dev/null +++ b/plugins/self-improving-operator/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +__pycache__/ +.pytest_cache/ +.operator/ diff --git a/plugins/self-improving-operator/CHANGELOG.md b/plugins/self-improving-operator/CHANGELOG.md new file mode 100644 index 0000000..46c917a --- /dev/null +++ b/plugins/self-improving-operator/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 2.0.0 - generated from the canonical autonomy kernel + +- Added persistent `.operator/` state support +- Added backlog scanning, plan decomposition, and checkpoint continuation +- Synced this plugin from the canonical Codex kernel diff --git a/plugins/self-improving-operator/LICENSE b/plugins/self-improving-operator/LICENSE new file mode 100644 index 0000000..4d990d0 --- /dev/null +++ b/plugins/self-improving-operator/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf of + any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets.) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/plugins/self-improving-operator/README.md b/plugins/self-improving-operator/README.md new file mode 100644 index 0000000..af6ca7f --- /dev/null +++ b/plugins/self-improving-operator/README.md @@ -0,0 +1,49 @@ +# Claude Code Self-Improving Operator + +Claude Code plugin for persistent project improvement. + +This plugin is generated from the canonical autonomy kernel in `https://github.com/wd041216-bit/codex-self-improving-operator`. + +## What it does + +- creates or resumes durable `.operator/` state +- scans repo and GitHub signals into a prioritized backlog +- filters out fake work from prose, string literals, and generated outputs +- drops stale scan-derived pending items on refresh +- executes one bounded improvement at a time +- verifies, checkpoints, and keeps going +- stops only on a real stop reason + +## Core command + +```text +/self-improving-operator:improve-project improve onboarding and keep going until you hit a real blocker +``` + +## Durable state + +- `.operator/mission.md`: mission, scope, work sources, stop reasons, and publish mode. +- `.operator/backlog.json`: prioritized work items discovered from repo and GitHub signals. +- `.operator/state.json`: current item, verification state, last stop reason, and `next_action`. +- `.operator/checkpoints/*.md`: durable checkpoints written after verified work. + +## Runtime loop + +```bash +python3 scripts/operator_runtime.py bootstrap --repo /path/to/repo --goal "Stabilize onboarding and keep shipping" +python3 scripts/operator_runtime.py scan --repo /path/to/repo +python3 scripts/operator_runtime.py next --repo /path/to/repo +``` + +Use `ingest-plan` when a broad strategy needs to be decomposed into executable backlog items, and `checkpoint` after each verified improvement. + +## Validation + +```bash +python3 scripts/validate_plugin.py +``` + +## Canonical source + +- Codex kernel repo: `https://github.com/wd041216-bit/codex-self-improving-operator` +- Claude plugin repo: `https://github.com/wd041216-bit/claude-code-self-improving-operator` diff --git a/plugins/self-improving-operator/agents/self-improving-operator-executor.md b/plugins/self-improving-operator/agents/self-improving-operator-executor.md new file mode 100644 index 0000000..a94d599 --- /dev/null +++ b/plugins/self-improving-operator/agents/self-improving-operator-executor.md @@ -0,0 +1,22 @@ +--- +name: self-improving-operator-executor +description: Use for proactive project-improvement work that should maintain `.operator/` state, execute bounded improvements, verify them, checkpoint them, and keep finding the next safe task. +model: sonnet +effort: high +maxTurns: 24 +skills: + - self-improving-operator:operator-playbook +--- + +You are the execution operator for continuous project improvement. + +When you are delegated a task: + +1. Load `.operator/mission.md`, `.operator/backlog.json`, and `.operator/state.json` if present. +2. If they do not exist, initialize them before deciding what to do next. +3. Refresh repo and GitHub signals into the backlog. +4. Choose one bounded in-scope item with the highest leverage. +5. Implement it, verify it, checkpoint it, and save the next action. +6. Continue until a real stop reason exists. + +Do not treat planning as completion. Plans must become backlog items. diff --git a/plugins/self-improving-operator/docs/architecture.md b/plugins/self-improving-operator/docs/architecture.md new file mode 100644 index 0000000..c3a8d44 --- /dev/null +++ b/plugins/self-improving-operator/docs/architecture.md @@ -0,0 +1,58 @@ +# Architecture + +`codex-self-improving-operator` v2 is built as a canonical autonomy kernel plus generated variants. + +## Layers + +1. `kernel/` + Canonical spec and prompt templates +2. `scripts/operator_runtime.py` + Deterministic runtime for `.operator/` state, backlog refresh, plan decomposition, next-item selection, and checkpoints +3. `scripts/sync_variants.py` + Generates the Codex skill bundle, the local installed skill bundle, and the Claude plugin bundle +4. `skill/self-improving-operator/` + Upstream-ready Codex skill bundle +5. `generated/claude-code-self-improving-operator/` + Canonical generated snapshot for the Claude plugin variant + +## Durable state model + +Target repositories use a repo-local `.operator/` directory: + +- `mission.md` +- `backlog.json` +- `state.json` +- `checkpoints/*.md` + +This state is the continuity layer that lets a fresh thread resume without reconstructing prior decisions from chat history. + +## Execution loop + +1. Bootstrap or load mission/state +2. Scan repo and GitHub signals +3. Merge and prioritize backlog items +4. Select the next in-scope item +5. Implement and verify +6. Write a checkpoint and save `next_action` +7. Re-scan and continue + +## Signal hygiene + +The runtime only turns actionable signals into backlog items: + +- comment-style TODO/FIXME markers in source files +- explicit doc headings or bullets such as `Next steps` and `Known blockers` +- current GitHub failures, issues, review states, and discussions + +Literal mentions in prose, regex strings, and generated output directories are ignored. Scan-derived pending items are rebuilt on each refresh, so resolved or vanished signals fall out of the queue instead of lingering forever. + +## Variant strategy + +The Codex skill and the Claude plugin share the same kernel semantics: + +- same `.operator/` state model +- same stop reasons +- same priority order +- same publish default of checkpoint branch/commit without auto-PR + +The generated variants may differ in surface syntax, but they should not drift in behavior. diff --git a/plugins/self-improving-operator/docs/examples/fresh-repo.md b/plugins/self-improving-operator/docs/examples/fresh-repo.md new file mode 100644 index 0000000..19d9435 --- /dev/null +++ b/plugins/self-improving-operator/docs/examples/fresh-repo.md @@ -0,0 +1,19 @@ +# Example: Fresh Repo Bootstrap + +## Situation + +A new thread opens on a repository that has no prior operator state. + +## Commands + +```bash +python3 scripts/operator_runtime.py bootstrap --repo /workspace/demo --goal "Ship a reliable onboarding flow" +python3 scripts/operator_runtime.py scan --repo /workspace/demo +python3 scripts/operator_runtime.py next --repo /workspace/demo +``` + +## Expected outcome + +- `.operator/` is created +- the backlog contains repo and GitHub signals +- `next_action` points at the first bounded improvement instead of returning a plan-only answer diff --git a/plugins/self-improving-operator/docs/examples/github-reprioritization.md b/plugins/self-improving-operator/docs/examples/github-reprioritization.md new file mode 100644 index 0000000..e07f2c6 --- /dev/null +++ b/plugins/self-improving-operator/docs/examples/github-reprioritization.md @@ -0,0 +1,18 @@ +# Example: GitHub Signal Reprioritization + +## Situation + +The repo already has pending polish tasks, then a failing CI run or review-blocking PR appears. + +## Commands + +```bash +python3 scripts/operator_runtime.py scan --repo /workspace/demo +python3 scripts/operator_runtime.py next --repo /workspace/demo +``` + +## Expected outcome + +- failing CI and review blockers move above polish items +- the next active task becomes the GitHub blocker +- `.operator/backlog.json` and `.operator/state.json` reflect the reprioritized queue diff --git a/plugins/self-improving-operator/docs/examples/resume-thread.md b/plugins/self-improving-operator/docs/examples/resume-thread.md new file mode 100644 index 0000000..4dcf3f7 --- /dev/null +++ b/plugins/self-improving-operator/docs/examples/resume-thread.md @@ -0,0 +1,18 @@ +# Example: Resume In A Fresh Thread + +## Situation + +The previous thread stopped after a verified checkpoint. + +## Commands + +```bash +python3 scripts/operator_runtime.py status --repo /workspace/demo +python3 scripts/operator_runtime.py next --repo /workspace/demo +``` + +## Expected outcome + +- the new thread reads `.operator/state.json` +- the next item is selected from the existing backlog +- work resumes from `next_action` instead of rebuilding context from memory diff --git a/plugins/self-improving-operator/docs/operator-flow.md b/plugins/self-improving-operator/docs/operator-flow.md new file mode 100644 index 0000000..9c552fa --- /dev/null +++ b/plugins/self-improving-operator/docs/operator-flow.md @@ -0,0 +1,74 @@ +# Operator Flow + +## Bootstrap + +Run: + +```bash +python3 scripts/operator_runtime.py bootstrap --repo /path/to/repo --goal "Stabilize onboarding and keep shipping" +``` + +This creates `.operator/mission.md`, `.operator/backlog.json`, `.operator/state.json`, and `.operator/checkpoints/`. + +## Refresh signals + +Run: + +```bash +python3 scripts/operator_runtime.py scan --repo /path/to/repo +``` + +The scan merges: + +- actionable TODO/FIXME comments in repo files +- explicit doc headings or bullets such as `Next steps`, `Follow-up`, and `Known blockers` +- failing GitHub Actions runs +- open issues +- PR review states +- discussions when available + +Resolved scan-derived pending items automatically disappear on the next refresh, so the queue tracks the current repo state instead of accumulating stale noise. + +## Choose the next item + +Run: + +```bash +python3 scripts/operator_runtime.py next --repo /path/to/repo +``` + +This marks the top pending item as `in_progress` and writes `next_action` into `.operator/state.json`. + +## Turn plans into work + +If a broad plan is needed, do not stop there. Convert it into executable backlog items: + +```bash +python3 scripts/operator_runtime.py ingest-plan --repo /path/to/repo --plan-file /path/to/plan.md +``` + +## Checkpoint and continue + +After one bounded improvement is implemented and verified: + +```bash +python3 scripts/operator_runtime.py checkpoint \ + --repo /path/to/repo \ + --item-id \ + --summary "Added retry diagnostics and tightened the smoke path." \ + --verification-status passed \ + --verification-summary "Smoke path passed locally and CI failure is gone." \ + --publish-checkpoint +``` + +This writes a durable checkpoint, updates state, refreshes the backlog, selects the next item, and optionally creates a checkpoint commit. + +## Stop only on a real reason + +Allowed stop reasons: + +- `needs_user_decision` +- `external_blocker` +- `risk_budget_exceeded` +- `no_safe_work` +- `mission_complete` diff --git a/plugins/self-improving-operator/scripts/operator_runtime.py b/plugins/self-improving-operator/scripts/operator_runtime.py new file mode 100644 index 0000000..e5ba209 --- /dev/null +++ b/plugins/self-improving-operator/scripts/operator_runtime.py @@ -0,0 +1,898 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +STATE_SCHEMA_VERSION = 2 +DEFAULT_WORK_SOURCES = [ + "local_tests", + "ci_failures", + "runtime_failures", + "todo_fixme", + "docs_handoff_gaps", + "github_issues", + "github_pr_reviews", + "github_discussions", +] +DEFAULT_PRIORITY_BUCKETS = { + "ci_failure": 1, + "runtime_failure": 1, + "verification_failure": 1, + "github_pr_review": 2, + "github_issue": 2, + "github_discussion": 2, + "carryover": 3, + "plan_decomposition": 3, + "todo_fixme": 4, + "docs_handoff_gap": 4, + "cleanup": 5, +} +PERSISTENT_PENDING_SOURCES = { + "carryover", + "plan_decomposition", + "runtime_failure", + "verification_failure", +} +DEFAULT_STOP_REASONS = { + "needs_user_decision", + "external_blocker", + "risk_budget_exceeded", + "no_safe_work", + "mission_complete", +} +SKIP_DIR_NAMES = { + ".git", + ".hg", + ".svn", + ".venv", + "node_modules", + "dist", + "build", + "generated", + ".next", + ".turbo", + "__pycache__", + ".operator", +} +TEXT_SIGNAL_EXTENSIONS = {".md", ".mdx", ".txt", ".rst", ".adoc"} +COMMENT_TOKENS = { + ".c": ("//", "/*", "*"), + ".cc": ("//", "/*", "*"), + ".cpp": ("//", "/*", "*"), + ".cs": ("//", "/*", "*"), + ".go": ("//", "/*", "*"), + ".java": ("//", "/*", "*"), + ".js": ("//", "/*", "*"), + ".jsx": ("//", "/*", "*"), + ".kt": ("//", "/*", "*"), + ".lua": ("--",), + ".py": ("#",), + ".rb": ("#",), + ".rs": ("//", "/*", "*"), + ".sh": ("#",), + ".sql": ("--",), + ".swift": ("//", "/*", "*"), + ".ts": ("//", "/*", "*"), + ".tsx": ("//", "/*", "*"), + ".yaml": ("#",), + ".yml": ("#",), +} +ACTIONABLE_TODO_PATTERN = re.compile(r"^(TODO|FIXME|HACK|XXX)\b(?::|\s+)", re.IGNORECASE) +ACTIONABLE_HANDOFF_PATTERN = re.compile( + r"^(next step|next steps|follow[- ]up|known blocker|known blockers|remaining work)\b", + re.IGNORECASE, +) + + +@dataclass +class SignalItem: + source: str + title: str + evidence: list[str] + impact: str + effort: str + risk: str + next_action: str + fingerprint: str + + +def utc_now() -> str: + return datetime.now(timezone.utc).replace(microsecond=0).isoformat() + + +def slugify(value: str) -> str: + slug = re.sub(r"[^a-zA-Z0-9]+", "-", value.strip().lower()).strip("-") + return slug or "item" + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def write_text(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def read_json(path: Path, default: Any) -> Any: + if not path.exists(): + return default + return json.loads(path.read_text(encoding="utf-8")) + + +def write_json(path: Path, payload: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, ensure_ascii=True) + "\n", encoding="utf-8") + + +def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]: + if not text.startswith("---\n"): + return {}, text + + match = re.match(r"^---\n(.*?)\n---\n?", text, re.DOTALL) + if not match: + return {}, text + + data: dict[str, Any] = {} + current_key: str | None = None + for raw_line in match.group(1).splitlines(): + if raw_line.startswith(" - ") and current_key: + data.setdefault(current_key, []) + data[current_key].append(raw_line[4:].strip()) + continue + if ":" not in raw_line: + current_key = None + continue + key, value = raw_line.split(":", 1) + key = key.strip() + value = value.strip().strip('"') + if value == "": + data[key] = [] + else: + data[key] = value + current_key = key + + return data, text[match.end() :] + + +def render_mission(repo_name: str, goal: str, github_repo: str | None) -> str: + github_line = github_repo or "unknown" + return ( + "---\n" + f"title: Improve {repo_name}\n" + f"goal: {goal}\n" + "autonomy_mode: aggressive_in_scope\n" + "continuity: cross_thread_cross_day\n" + "publish_mode: checkpoint_commit\n" + "risk_budget: medium\n" + f"github_repo: {github_line}\n" + "work_sources:\n" + " - local_tests\n" + " - ci_failures\n" + " - runtime_failures\n" + " - todo_fixme\n" + " - docs_handoff_gaps\n" + " - github_issues\n" + " - github_pr_reviews\n" + " - github_discussions\n" + "stop_reasons:\n" + " - needs_user_decision\n" + " - external_blocker\n" + " - risk_budget_exceeded\n" + " - no_safe_work\n" + " - mission_complete\n" + "---\n\n" + "# Mission\n\n" + "## In Scope\n\n" + "- Repository code, tests, docs, CI, and backlog items directly related to the current project.\n" + "- Follow-up work discovered through repo signals or GitHub signals.\n\n" + "## Out Of Scope\n\n" + "- New product lines or marketing tracks not implied by the current repository.\n" + "- Large irreversible architecture shifts without evidence.\n\n" + "## Success Signals\n\n" + "- The current mission is moving through verified checkpoints.\n" + "- `next_action` always points at the next safe, high-leverage task.\n" + ) + + +def run_command(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]: + result = subprocess.run( + args, + cwd=str(cwd) if cwd else None, + capture_output=True, + text=True, + check=False, + ) + return result.returncode, result.stdout, result.stderr + + +def infer_goal(repo_path: Path) -> str: + readme = repo_path / "README.md" + if not readme.exists(): + return f"Improve {repo_path.name} through verified high-leverage iterations." + + text = read_text(readme) + lines = [line.strip() for line in text.splitlines() if line.strip()] + for line in lines: + if line.startswith("#"): + continue + return line + return f"Improve {repo_path.name} through verified high-leverage iterations." + + +def infer_github_repo(repo_path: Path) -> str | None: + code, stdout, _ = run_command(["git", "-C", str(repo_path), "remote", "get-url", "origin"]) + if code != 0: + return None + + remote = stdout.strip() + https_match = re.search(r"github\.com[:/](?P[^/]+)/(?P[^/.]+)(?:\.git)?$", remote) + if not https_match: + return None + return f"{https_match.group('owner')}/{https_match.group('repo')}" + + +def operator_paths(repo_path: Path) -> dict[str, Path]: + root = repo_path / ".operator" + return { + "root": root, + "mission": root / "mission.md", + "backlog": root / "backlog.json", + "state": root / "state.json", + "checkpoints": root / "checkpoints", + } + + +def default_state(github_repo: str | None = None) -> dict[str, Any]: + return { + "schema_version": STATE_SCHEMA_VERSION, + "status": "active", + "current_item_id": None, + "next_action": "Run scan, choose the next item, and continue executing.", + "last_scan_at": None, + "last_checkpoint_at": None, + "last_stop_reason": None, + "last_completed_item_id": None, + "verification": { + "status": "unknown", + "summary": None, + "updated_at": None, + }, + "github_repo": github_repo, + "publish_mode": "checkpoint_commit", + } + + +def bootstrap_state(repo_path: Path, goal: str | None = None, github_repo: str | None = None) -> dict[str, Any]: + paths = operator_paths(repo_path) + paths["checkpoints"].mkdir(parents=True, exist_ok=True) + github_repo = github_repo or infer_github_repo(repo_path) + if not paths["mission"].exists(): + mission_goal = goal or infer_goal(repo_path) + write_text(paths["mission"], render_mission(repo_path.name, mission_goal, github_repo)) + if not paths["backlog"].exists(): + write_json(paths["backlog"], {"schema_version": STATE_SCHEMA_VERSION, "items": []}) + if not paths["state"].exists(): + write_json(paths["state"], default_state(github_repo)) + return load_state(repo_path) + + +def load_mission(repo_path: Path) -> dict[str, Any]: + mission_path = operator_paths(repo_path)["mission"] + if not mission_path.exists(): + bootstrap_state(repo_path) + frontmatter, body = parse_frontmatter(read_text(mission_path)) + frontmatter["body"] = body.strip() + return frontmatter + + +def load_state(repo_path: Path) -> dict[str, Any]: + return read_json(operator_paths(repo_path)["state"], default_state()) + + +def load_backlog(repo_path: Path) -> dict[str, Any]: + return read_json(operator_paths(repo_path)["backlog"], {"schema_version": STATE_SCHEMA_VERSION, "items": []}) + + +def signal_to_item(signal: SignalItem) -> dict[str, Any]: + bucket = DEFAULT_PRIORITY_BUCKETS.get(signal.source, 5) + digest = hashlib.sha1(f"{signal.source}:{signal.fingerprint}".encode("utf-8")).hexdigest()[:10] + return { + "id": f"{slugify(signal.source)}-{digest}", + "title": signal.title, + "source": signal.source, + "evidence": signal.evidence, + "impact": signal.impact, + "effort": signal.effort, + "risk": signal.risk, + "priority_bucket": bucket, + "status": "pending", + "next_action": signal.next_action, + "updated_at": utc_now(), + } + + +def existing_item_map(items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + return {item["id"]: item for item in items} + + +def merge_items(existing: list[dict[str, Any]], incoming: list[dict[str, Any]]) -> list[dict[str, Any]]: + merged = existing_item_map(existing) + for item in incoming: + current = merged.get(item["id"]) + if current and current.get("status") == "completed": + continue + if current: + current.update(item) + current.setdefault("status", "pending") + else: + merged[item["id"]] = item + items = list(merged.values()) + items.sort(key=lambda item: (item["priority_bucket"], item["title"])) + return items + + +def iter_candidate_files(repo_path: Path) -> list[Path]: + files: list[Path] = [] + for root, dirs, filenames in os.walk(repo_path): + dirs[:] = [name for name in dirs if name not in SKIP_DIR_NAMES] + for filename in filenames: + path = Path(root) / filename + if path.is_file(): + files.append(path) + return files + + +def strip_doc_prefix(line: str) -> str: + return re.sub(r"^\s*(?:[#>*-]+|\d+[.)])\s*", "", line).strip() + + +def extract_comment_text(path: Path, line: str) -> str | None: + stripped = line.strip() + tokens = COMMENT_TOKENS.get(path.suffix.lower(), ()) + for token in tokens: + index = line.find(token) + if index == -1: + continue + prefix = line[:index].strip() + if prefix: + continue + return line[index + len(token) :].strip() + if path.suffix.lower() in TEXT_SIGNAL_EXTENSIONS: + return strip_doc_prefix(stripped) + return None + + +def actionable_todo_text(path: Path, line: str) -> str | None: + candidate = extract_comment_text(path, line) + if candidate and ACTIONABLE_TODO_PATTERN.search(candidate): + return candidate + return None + + +def actionable_handoff_text(path: Path, line: str) -> str | None: + if path.suffix.lower() not in TEXT_SIGNAL_EXTENSIONS: + return None + candidate = strip_doc_prefix(line) + if candidate and ACTIONABLE_HANDOFF_PATTERN.search(candidate): + return candidate + return None + + +def scan_todo_signals(repo_path: Path) -> list[SignalItem]: + signals: list[SignalItem] = [] + for path in iter_candidate_files(repo_path): + if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".zip"}: + continue + try: + text = read_text(path) + except UnicodeDecodeError: + continue + for index, line in enumerate(text.splitlines(), start=1): + candidate = actionable_todo_text(path, line) + if candidate: + relpath = str(path.relative_to(repo_path)) + fingerprint = f"{relpath}:{index}:{candidate}" + signals.append( + SignalItem( + source="todo_fixme", + title=f"Resolve TODO in {relpath}:{index}", + evidence=[f"{relpath}:{index}: {candidate}"], + impact="medium", + effort="small", + risk="low", + next_action=f"Inspect {relpath}:{index} and decide whether to implement, remove, or formalize the follow-up.", + fingerprint=fingerprint, + ) + ) + return signals + + +def scan_handoff_signals(repo_path: Path) -> list[SignalItem]: + signals: list[SignalItem] = [] + for path in iter_candidate_files(repo_path): + if path.suffix.lower() not in {".md", ".txt"}: + continue + try: + text = read_text(path) + except UnicodeDecodeError: + continue + for index, line in enumerate(text.splitlines(), start=1): + candidate = actionable_handoff_text(path, line) + if candidate: + relpath = str(path.relative_to(repo_path)) + fingerprint = f"{relpath}:{index}:{candidate}" + signals.append( + SignalItem( + source="docs_handoff_gap", + title=f"Review follow-up note in {relpath}:{index}", + evidence=[f"{relpath}:{index}: {candidate}"], + impact="medium", + effort="small", + risk="low", + next_action=f"Confirm whether the follow-up in {relpath}:{index} is still true and either execute it or update the doc.", + fingerprint=fingerprint, + ) + ) + return signals + + +def command_available(name: str) -> bool: + return shutil.which(name) is not None + + +def github_json(repo: str, args: list[str]) -> Any | None: + if not command_available("gh"): + return None + code, stdout, _ = run_command(["gh", *args, "-R", repo]) + if code != 0 or not stdout.strip(): + return None + return json.loads(stdout) + + +def scan_github_issue_signals(github_repo: str | None) -> list[SignalItem]: + if not github_repo: + return [] + payload = github_json(github_repo, ["issue", "list", "--state", "open", "--limit", "20", "--json", "number,title,url,labels"]) + if not payload: + return [] + + signals: list[SignalItem] = [] + for issue in payload: + fingerprint = f"issue:{issue['number']}" + labels = ", ".join(label["name"] for label in issue.get("labels", [])) or "no labels" + signals.append( + SignalItem( + source="github_issue", + title=f"Resolve GitHub issue #{issue['number']}: {issue['title']}", + evidence=[issue["url"], f"labels: {labels}"], + impact="high", + effort="medium", + risk="medium", + next_action=f"Inspect GitHub issue #{issue['number']} and decide whether it should become the next active work item.", + fingerprint=fingerprint, + ) + ) + return signals + + +def scan_github_pr_review_signals(github_repo: str | None) -> list[SignalItem]: + if not github_repo: + return [] + payload = github_json( + github_repo, + ["pr", "list", "--state", "open", "--limit", "20", "--json", "number,title,url,reviewDecision,isDraft"], + ) + if not payload: + return [] + + signals: list[SignalItem] = [] + for pr in payload: + decision = pr.get("reviewDecision") or "UNKNOWN" + if decision not in {"CHANGES_REQUESTED", "REVIEW_REQUIRED"}: + continue + fingerprint = f"pr:{pr['number']}:{decision}" + signals.append( + SignalItem( + source="github_pr_review", + title=f"Address PR #{pr['number']} review state: {pr['title']}", + evidence=[pr["url"], f"reviewDecision: {decision}"], + impact="high", + effort="medium", + risk="medium", + next_action=f"Inspect PR #{pr['number']} and resolve requested review work before lower-priority tasks.", + fingerprint=fingerprint, + ) + ) + return signals + + +def scan_github_run_signals(github_repo: str | None) -> list[SignalItem]: + if not github_repo: + return [] + payload = github_json( + github_repo, + ["run", "list", "--limit", "20", "--json", "databaseId,workflowName,displayTitle,conclusion,url"], + ) + if not payload: + return [] + + signals: list[SignalItem] = [] + for run in payload: + if run.get("conclusion") != "failure": + continue + fingerprint = f"run:{run['databaseId']}" + title = run.get("displayTitle") or run.get("workflowName") or "Failing workflow" + signals.append( + SignalItem( + source="ci_failure", + title=f"Fix failing CI run: {title}", + evidence=[run["url"], f"workflow: {run.get('workflowName') or 'unknown'}"], + impact="high", + effort="medium", + risk="medium", + next_action=f"Inspect the failing run for {title} and turn it into the next active repair task.", + fingerprint=fingerprint, + ) + ) + return signals + + +def scan_github_discussion_signals(github_repo: str | None) -> list[SignalItem]: + if not github_repo or not command_available("gh"): + return [] + code, stdout, _ = run_command(["gh", "api", f"repos/{github_repo}/discussions?per_page=10"]) + if code != 0 or not stdout.strip(): + return [] + try: + payload = json.loads(stdout) + except json.JSONDecodeError: + return [] + + signals: list[SignalItem] = [] + for discussion in payload: + fingerprint = f"discussion:{discussion.get('number')}" + title = discussion.get("title") or "GitHub discussion" + signals.append( + SignalItem( + source="github_discussion", + title=f"Review actionable discussion #{discussion.get('number')}: {title}", + evidence=[discussion.get("html_url", ""), f"category: {discussion.get('category', {}).get('name', 'unknown')}"], + impact="medium", + effort="medium", + risk="low", + next_action=f"Check whether discussion #{discussion.get('number')} implies a concrete backlog item.", + fingerprint=fingerprint, + ) + ) + return signals + + +def carry_over_items(backlog: dict[str, Any]) -> list[dict[str, Any]]: + items = [] + for item in backlog.get("items", []): + status = item.get("status") + source = item.get("source", "carryover") + keep_pending = status == "pending" and source in PERSISTENT_PENDING_SOURCES + if status in {"in_progress", "blocked"} or keep_pending: + item = dict(item) + item["source"] = source + item["priority_bucket"] = DEFAULT_PRIORITY_BUCKETS.get(source, item.get("priority_bucket", 5)) + items.append(item) + return items + + +def refresh_backlog(repo_path: Path) -> dict[str, Any]: + bootstrap_state(repo_path) + mission = load_mission(repo_path) + state = load_state(repo_path) + existing_backlog = load_backlog(repo_path) + github_repo = mission.get("github_repo") or state.get("github_repo") or infer_github_repo(repo_path) + + incoming: list[dict[str, Any]] = carry_over_items(existing_backlog) + for scanner in ( + scan_todo_signals, + scan_handoff_signals, + ): + incoming.extend(signal_to_item(item) for item in scanner(repo_path)) + + for scanner in ( + scan_github_run_signals, + scan_github_pr_review_signals, + scan_github_issue_signals, + scan_github_discussion_signals, + ): + incoming.extend(signal_to_item(item) for item in scanner(github_repo)) + + completed_items = [item for item in existing_backlog.get("items", []) if item.get("status") == "completed"] + merged = merge_items(completed_items, incoming) + payload = {"schema_version": STATE_SCHEMA_VERSION, "items": merged} + write_json(operator_paths(repo_path)["backlog"], payload) + state["last_scan_at"] = utc_now() + state["github_repo"] = github_repo + write_json(operator_paths(repo_path)["state"], state) + return payload + + +def choose_next_item(repo_path: Path) -> dict[str, Any] | None: + backlog = load_backlog(repo_path) + state = load_state(repo_path) + pending = [item for item in backlog.get("items", []) if item.get("status") == "pending"] + pending.sort(key=lambda item: (item.get("priority_bucket", 99), item.get("title", ""))) + next_item = pending[0] if pending else None + if next_item: + next_item["status"] = "in_progress" + state["current_item_id"] = next_item["id"] + state["next_action"] = next_item["next_action"] + state["last_stop_reason"] = None + else: + state["current_item_id"] = None + state["next_action"] = "No safe pending work found. Stop only after recording `no_safe_work` or a more specific stop reason." + state["last_stop_reason"] = "no_safe_work" + write_json(operator_paths(repo_path)["backlog"], backlog) + write_json(operator_paths(repo_path)["state"], state) + return next_item + + +def parse_plan_items(plan_text: str) -> list[str]: + items: list[str] = [] + for raw_line in plan_text.splitlines(): + match = re.match(r"^\s*(?:[-*]|\d+\.)\s+(.*)$", raw_line) + if match: + text = match.group(1).strip() + if text: + items.append(text) + return items + + +def ingest_plan(repo_path: Path, plan_text: str) -> dict[str, Any]: + backlog = refresh_backlog(repo_path) + new_items = [] + for line in parse_plan_items(plan_text): + signal = SignalItem( + source="plan_decomposition", + title=line, + evidence=["decomposed from a broad plan"], + impact="high", + effort="medium", + risk="low", + next_action=f"Execute the planned step: {line}", + fingerprint=line, + ) + new_items.append(signal_to_item(signal)) + payload = {"schema_version": STATE_SCHEMA_VERSION, "items": merge_items(backlog["items"], new_items)} + write_json(operator_paths(repo_path)["backlog"], payload) + return payload + + +def current_branch(repo_path: Path) -> str | None: + code, stdout, _ = run_command(["git", "-C", str(repo_path), "branch", "--show-current"]) + return stdout.strip() if code == 0 and stdout.strip() else None + + +def default_branch(repo_path: Path) -> str | None: + for branch_name in ("main", "master"): + code, _, _ = run_command(["git", "-C", str(repo_path), "rev-parse", "--verify", branch_name]) + if code == 0: + return branch_name + return None + + +def ensure_checkpoint_branch(repo_path: Path, item: dict[str, Any]) -> None: + branch = current_branch(repo_path) + default = default_branch(repo_path) + if branch and default and branch == default: + checkpoint_branch = f"codex/checkpoint/{datetime.now().strftime('%Y%m%d-%H%M%S')}-{slugify(item['title'])[:32]}" + run_command(["git", "-C", str(repo_path), "checkout", "-b", checkpoint_branch]) + + +def maybe_commit_checkpoint(repo_path: Path, item: dict[str, Any]) -> None: + code, stdout, _ = run_command(["git", "-C", str(repo_path), "status", "--short"]) + if code != 0 or not stdout.strip(): + return + ensure_checkpoint_branch(repo_path, item) + run_command(["git", "-C", str(repo_path), "add", "-A"]) + run_command(["git", "-C", str(repo_path), "commit", "-m", f"checkpoint: {item['title']}"]) + + +def write_checkpoint( + repo_path: Path, + item: dict[str, Any], + summary: str, + verification_status: str, + verification_summary: str, + stop_reason: str | None, +) -> Path: + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + checkpoint_path = operator_paths(repo_path)["checkpoints"] / f"{timestamp}-{slugify(item['title'])}.md" + content = ( + f"# Checkpoint: {item['title']}\n\n" + f"- Time: {utc_now()}\n" + f"- Item ID: {item['id']}\n" + f"- Verification: {verification_status}\n" + f"- Stop reason: {stop_reason or 'none'}\n\n" + "## Summary\n\n" + f"{summary.strip()}\n\n" + "## Verification\n\n" + f"{verification_summary.strip()}\n" + ) + write_text(checkpoint_path, content) + return checkpoint_path + + +def apply_checkpoint( + repo_path: Path, + item_id: str, + summary: str, + verification_status: str, + verification_summary: str, + stop_reason: str | None, + publish_checkpoint: bool, +) -> dict[str, Any]: + if stop_reason and stop_reason not in DEFAULT_STOP_REASONS: + raise SystemExit(f"Unsupported stop reason: {stop_reason}") + + backlog = load_backlog(repo_path) + state = load_state(repo_path) + item = next((candidate for candidate in backlog["items"] if candidate["id"] == item_id), None) + if not item: + raise SystemExit(f"Unknown backlog item: {item_id}") + + checkpoint_path = write_checkpoint(repo_path, item, summary, verification_status, verification_summary, stop_reason) + if verification_status == "passed": + item["status"] = "completed" + state["last_completed_item_id"] = item_id + elif verification_status == "failed": + item["status"] = "blocked" + failure_signal = signal_to_item( + SignalItem( + source="verification_failure", + title=f"Repair failed verification for {item['title']}", + evidence=[verification_summary.strip()], + impact="high", + effort="medium", + risk="medium", + next_action=f"Fix the failed verification for {item['title']} and rerun validation.", + fingerprint=f"verification:{item_id}:{verification_summary.strip()}", + ) + ) + backlog["items"] = merge_items(backlog["items"], [failure_signal]) + else: + item["status"] = "completed" + state["last_completed_item_id"] = item_id + + state["verification"] = { + "status": verification_status, + "summary": verification_summary.strip(), + "updated_at": utc_now(), + } + state["last_checkpoint_at"] = utc_now() + state["last_stop_reason"] = stop_reason + state["current_item_id"] = None + write_json(operator_paths(repo_path)["backlog"], backlog) + write_json(operator_paths(repo_path)["state"], state) + + if publish_checkpoint: + maybe_commit_checkpoint(repo_path, item) + + refresh_backlog(repo_path) + next_item = choose_next_item(repo_path) + state = load_state(repo_path) + state["next_action"] = next_item["next_action"] if next_item else "Wait for a user decision or a new safe signal before continuing." + write_json(operator_paths(repo_path)["state"], state) + return { + "checkpoint": str(checkpoint_path), + "next_item": next_item, + } + + +def status_payload(repo_path: Path) -> dict[str, Any]: + mission = load_mission(repo_path) + backlog = load_backlog(repo_path) + state = load_state(repo_path) + counts: dict[str, int] = {} + for item in backlog.get("items", []): + status = item.get("status", "pending") + counts[status] = counts.get(status, 0) + 1 + return { + "mission_title": mission.get("title"), + "goal": mission.get("goal"), + "github_repo": state.get("github_repo"), + "current_item_id": state.get("current_item_id"), + "next_action": state.get("next_action"), + "last_stop_reason": state.get("last_stop_reason"), + "counts": counts, + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Persistent runtime for the self-improving operator.") + subparsers = parser.add_subparsers(dest="command", required=True) + + bootstrap_parser = subparsers.add_parser("bootstrap") + bootstrap_parser.add_argument("--repo", required=True) + bootstrap_parser.add_argument("--goal") + bootstrap_parser.add_argument("--github-repo") + + scan_parser = subparsers.add_parser("scan") + scan_parser.add_argument("--repo", required=True) + + next_parser = subparsers.add_parser("next") + next_parser.add_argument("--repo", required=True) + + status_parser = subparsers.add_parser("status") + status_parser.add_argument("--repo", required=True) + + ingest_parser = subparsers.add_parser("ingest-plan") + ingest_parser.add_argument("--repo", required=True) + ingest_parser.add_argument("--plan-file") + ingest_parser.add_argument("--plan-text") + + checkpoint_parser = subparsers.add_parser("checkpoint") + checkpoint_parser.add_argument("--repo", required=True) + checkpoint_parser.add_argument("--item-id", required=True) + checkpoint_parser.add_argument("--summary", required=True) + checkpoint_parser.add_argument("--verification-status", choices=["passed", "failed", "unknown"], required=True) + checkpoint_parser.add_argument("--verification-summary", required=True) + checkpoint_parser.add_argument("--stop-reason") + checkpoint_parser.add_argument("--publish-checkpoint", action="store_true") + + return parser.parse_args() + + +def main() -> int: + args = parse_args() + repo_path = Path(getattr(args, "repo")).resolve() + + if args.command == "bootstrap": + state = bootstrap_state(repo_path, goal=args.goal, github_repo=args.github_repo) + print(json.dumps(state, indent=2)) + return 0 + if args.command == "scan": + payload = refresh_backlog(repo_path) + print(json.dumps(payload, indent=2)) + return 0 + if args.command == "next": + next_item = choose_next_item(repo_path) + print(json.dumps(next_item or {"stop_reason": "no_safe_work"}, indent=2)) + return 0 + if args.command == "status": + print(json.dumps(status_payload(repo_path), indent=2)) + return 0 + if args.command == "ingest-plan": + if not args.plan_file and not args.plan_text: + raise SystemExit("Provide --plan-file or --plan-text.") + plan_text = args.plan_text or read_text(Path(args.plan_file)) + payload = ingest_plan(repo_path, plan_text) + print(json.dumps(payload, indent=2)) + return 0 + if args.command == "checkpoint": + payload = apply_checkpoint( + repo_path=repo_path, + item_id=args.item_id, + summary=args.summary, + verification_status=args.verification_status, + verification_summary=args.verification_summary, + stop_reason=args.stop_reason, + publish_checkpoint=args.publish_checkpoint, + ) + print(json.dumps(payload, indent=2)) + return 0 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/self-improving-operator/scripts/validate_plugin.py b/plugins/self-improving-operator/scripts/validate_plugin.py new file mode 100644 index 0000000..faf8c9a --- /dev/null +++ b/plugins/self-improving-operator/scripts/validate_plugin.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import json +import shutil +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "variant-manifest.json" + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def main() -> int: + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + errors: list[str] = [] + for item in manifest["files"]: + target = ROOT / item["path"] + if not target.exists(): + errors.append(f"Missing generated file: {target}") + continue + digest = sha256(target) + if digest != item["sha256"]: + errors.append(f"Generated file drift: {target}") + if errors: + for error in errors: + print(f"ERROR: {error}") + return 1 + + if shutil.which("claude") is not None: + result = subprocess.run(["claude", "plugin", "validate"], cwd=ROOT, check=False) + if result.returncode != 0: + return result.returncode + + print("Claude plugin variant matches manifest.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/self-improving-operator/skills/improve-project/SKILL.md b/plugins/self-improving-operator/skills/improve-project/SKILL.md new file mode 100644 index 0000000..26c74a4 --- /dev/null +++ b/plugins/self-improving-operator/skills/improve-project/SKILL.md @@ -0,0 +1,26 @@ +--- +name: improve-project +description: Inspect the current state of a project, execute one high-leverage bounded improvement, verify it, checkpoint it, and keep going until a real stop reason is reached. +argument-hint: [mission-or-focus] +context: fork +agent: self-improving-operator-executor +user-invocable: true +--- + +Take ownership of this project improvement mission: $ARGUMENTS + +Use `.operator/` as the durable operating system for this repo. + +Required loop: + +1. Load or infer the mission. +2. Refresh the backlog from repo and GitHub signals. +3. Pick the next safe, high-leverage item. +4. Implement one bounded improvement. +5. Verify with direct evidence. +6. Write a checkpoint and update `next_action`. +7. Continue until a real stop reason is recorded. + +Do not stop because a plan exists, because one file was edited, or because the repo merely looks better. + +If a broad plan is needed, decompose it into multiple backlog items and keep executing. diff --git a/plugins/self-improving-operator/skills/operator-playbook/SKILL.md b/plugins/self-improving-operator/skills/operator-playbook/SKILL.md new file mode 100644 index 0000000..1cd21ae --- /dev/null +++ b/plugins/self-improving-operator/skills/operator-playbook/SKILL.md @@ -0,0 +1,37 @@ +--- +name: operator-playbook +description: Operational discipline for persistent diagnose-build-verify-checkpoint loops in ongoing software projects. +user-invocable: false +--- + +Use this playbook whenever the goal is to keep a project moving without losing continuity. + +## Required files + +- `.operator/mission.md`: mission, scope, work sources, stop reasons, and publish mode. +- `.operator/backlog.json`: prioritized work items discovered from repo and GitHub signals. +- `.operator/state.json`: current item, verification state, last stop reason, and `next_action`. +- `.operator/checkpoints/*.md`: durable checkpoints written after verified work. + +## Priority order + +1. broken runtime or ci +2. github blockers +3. existing backlog commitments +4. tests diagnostics onboarding docs +5. cleanup and polish + +## Stop reasons + +- `needs_user_decision` +- `external_blocker` +- `risk_budget_exceeded` +- `no_safe_work` +- `mission_complete` + +## Guardrails + +- Stay inside the active mission. +- Prefer the smallest improvement that changes the trajectory. +- Record state before stopping. +- Resume from `.operator/state.json`, not from memory. diff --git a/plugins/self-improving-operator/variant-manifest.json b/plugins/self-improving-operator/variant-manifest.json new file mode 100644 index 0000000..4b13daa --- /dev/null +++ b/plugins/self-improving-operator/variant-manifest.json @@ -0,0 +1,69 @@ +{ + "generated_from": "https://github.com/wd041216-bit/codex-self-improving-operator", + "files": [ + { + "path": ".claude-plugin/plugin.json", + "sha256": "a184fbb49db0a8445e2fcd560dfa066ca2e6e4a5c66a6de357358a7a83e544ea" + }, + { + "path": ".github/workflows/validate.yml", + "sha256": "a6809df80d49855582cf8ca13e596b76f222d8eb5d456b8452c6d4a1bc6a85b2" + }, + { + "path": ".gitignore", + "sha256": "d0900d6ecd5a290f8fe1eeeeac31cea8ebecdac26a37a9cbffeb878451cacc3e" + }, + { + "path": "CHANGELOG.md", + "sha256": "ee4dd64af72fa27271da815b3c960e90fb471933ee6330394a56fcb90d4391f9" + }, + { + "path": "LICENSE", + "sha256": "64bbe894803d8cb9bdd510ea4591c3e1790a575583280dd350656864d4befe22" + }, + { + "path": "README.md", + "sha256": "e06763045cc674930ee51a715fc070e51db9be02c180945724b12466c082a53b" + }, + { + "path": "agents/self-improving-operator-executor.md", + "sha256": "b98abcf223bbfd27b90eb015e325d6f52e3f85a1de57d95be366995de4223498" + }, + { + "path": "docs/architecture.md", + "sha256": "9f587c755cf02874303723ef65b35ef659426ba665659a5ee69368d68380c7f2" + }, + { + "path": "docs/examples/fresh-repo.md", + "sha256": "2c7c4151a735cf26d2c5276e7a3cfec60ff3dbc50f4b761f31f1c43eaeef6b03" + }, + { + "path": "docs/examples/github-reprioritization.md", + "sha256": "d8115fe90d476373b5dfc417881187efc87506e2d37b81417f3a6519c751c3b9" + }, + { + "path": "docs/examples/resume-thread.md", + "sha256": "203e79586bf49c52ba9d12c0395c097bff8928f593f2ed35a8211253ebb156bb" + }, + { + "path": "docs/operator-flow.md", + "sha256": "e98b1d3eab937732ff979b16953af7fbd91c7ba2add738a0625e6afaa7fe9eb2" + }, + { + "path": "scripts/operator_runtime.py", + "sha256": "02671c304232485c2cb97891d327c570e8733872160abcc3b1b66a461e1ced03" + }, + { + "path": "scripts/validate_plugin.py", + "sha256": "0e7bae4d840664b9e33a7168537bef84210c9d433f5f87c6648c314ebda98c97" + }, + { + "path": "skills/improve-project/SKILL.md", + "sha256": "d88b39516f4165ee04c468b8cc83a1b10a546ec1118ef2d6948abdfa1e60249b" + }, + { + "path": "skills/operator-playbook/SKILL.md", + "sha256": "e5b7eb73b1360732985ff7d2ecf826ccd4ac7199551e391d7adf790688a8afa4" + } + ] +}