Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,21 @@
"category": "Development & Workflow",
"description": "Budgeted `rg`/`grep` replacement for Codex that narrows broad searches before they waste model context."
},
{
"name": "debt-ops",
"displayName": "debt-ops",
"source": {
"source": "local",
"path": "./plugins/bcanfield/agentic-tech-debt"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Development & Workflow",
"description": "Catches AI-introduced tech debt at write-time: hooks log every deferral to a registry in your repo and a review skill ranks paydown by file churn.",
"icon": "./plugins/bcanfield/agentic-tech-debt/assets/icon.svg"
},
{
"name": "dev-skills",
"displayName": "Dev Skills",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ Third-party plugins built by the community. [PRs welcome](#contributing)!
- [Codex Reviewer](https://github.com/schuettc/codex-reviewer) - Second-pass review of Claude-driven plans and implementations.
- [Codex rg Guard](https://github.com/Rycen7822/codex-rg-guard) - Budgeted `rg`/`grep` replacement for Codex that narrows broad searches before they waste model context.
- [Commit Narrator](./plugins/mturac/commit-narrator) - Generate semantic commit message from staged diff, including the _why_.
- [debt-ops](https://github.com/bcanfield/agentic-tech-debt) - Catches AI-introduced tech debt at write-time: hooks log every deferral to a registry in your repo and a review skill ranks paydown by file churn.
- [Deps Doctor](./plugins/mturac/deps-doctor) - Multi-ecosystem dependency audit (npm, pip, cargo, go) in one report.
- [Dev Skills](https://github.com/Jason-chen-coder/dev-skills) - Team workflow skills for specs, plans, TDD, debugging, verification, review, branch finishing, and design context.
- [Development Skills](https://github.com/reidemeister94/development-skills) - Three-tier triage (PASS_THROUGH / LIGHT / FULL 4-phase) development workflow for Codex and Claude Code with language auto-detection (Python, Java, TypeScript, Swift, frontend) and a staff-reviewer subagent for fresh-eyes review on every change.
Expand Down
12 changes: 11 additions & 1 deletion plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "awesome-codex-plugins",
"version": "1.0.0",
"last_updated": "2026-06-08",
"total": 105,
"total": 107,
"categories": [
"Development & Workflow",
"Tools & Integrations"
Expand Down Expand Up @@ -229,6 +229,16 @@
"source": "awesome-codex-plugins",
"install_url": "https://raw.githubusercontent.com/Rycen7822/codex-rg-guard/HEAD/.codex-plugin/plugin.json"
},
{
"name": "debt-ops",
"url": "https://github.com/bcanfield/agentic-tech-debt",
"owner": "bcanfield",
"repo": "agentic-tech-debt",
"description": "Catches AI-introduced tech debt at write-time: hooks log every deferral to a registry in your repo and a review skill ranks paydown by file churn.",
"category": "Development & Workflow",
"source": "awesome-codex-plugins",
"install_url": "https://raw.githubusercontent.com/bcanfield/agentic-tech-debt/HEAD/codex/.codex-plugin/plugin.json"
},
{
"name": "Dev Skills",
"url": "https://github.com/Jason-chen-coder/dev-skills",
Expand Down
30 changes: 30 additions & 0 deletions plugins/bcanfield/agentic-tech-debt/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "debt-ops",
"version": "0.9.1",
"description": "Catches AI-introduced tech debt at write-time. Every \"I'll fix this later,\" every shortcut, every punt your AI agent writes gets caught.",
"author": {
"name": "Brandin Canfield",
"email": "brandincanfield@gmail.com"
},
"homepage": "https://github.com/bcanfield/agentic-tech-debt",
"repository": "https://github.com/bcanfield/agentic-tech-debt",
"license": "MIT",
"keywords": [
"tech-debt",
"code-health",
"hotspots",
"adr",
"productivity"
],
"skills": "./skills/",
"hooks": "./hooks/hooks.json",
"interface": {
"displayName": "debt-ops",
"shortDescription": "Catches AI-introduced tech debt at write-time",
"longDescription": "Every \"I'll fix this later,\" shortcut, and punt your AI agent writes lands in a folder in your repo. Architectural calls get a short ADR. When you're ready to clean up, a review skill ranks the backlog by how often each file changes, so you pay down the debt that actually hurts first. Plain Python, fully local, no network calls.",
"developerName": "Brandin Canfield",
"category": "Coding",
"composerIcon": "./assets/icon.svg",
"brandColor": "#d97757"
}
}
21 changes: 21 additions & 0 deletions plugins/bcanfield/agentic-tech-debt/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Brandin Canfield

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
40 changes: 40 additions & 0 deletions plugins/bcanfield/agentic-tech-debt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# debt-ops — Codex adapter

The [debt-ops](../README.md) disciplines, packaged as a [Codex](https://developers.openai.com/codex) plugin. Behavior matches the [Claude Code adapter](../claude-code); this README covers only what's Codex-specific.

## Install

Register the marketplace from your shell, then install from the plugin browser:

```bash
codex plugin marketplace add bcanfield/agentic-tech-debt
```

Open `/plugins` inside Codex and install **debt-ops**. Working *inside* this repo, Codex auto-discovers the bundled marketplace at `.agents/plugins/marketplace.json` once the project is trusted — no add step needed. Requires a git repo and Python 3.10+ (stdlib only).

## What's wired

Layout follows Codex's plugin conventions: hooks bundle their scripts under `hooks/` (referenced via `${PLUGIN_ROOT}/hooks/…`), and each skill bundles its helper under its own `scripts/` (referenced by a relative path, the documented skill convention).

| Codex primitive | File | Role |
| --- | --- | --- |
| `SessionStart` hook | `hooks/session-start.py` | Injects the disciplines + detects/caches quality commands and ADR/registry dirs |
| `PostToolUse` hook (`apply_patch\|Edit\|Write`) | `hooks/feedback.py` | Runs quality commands on edited files under a 3s/command budget |
| `Stop` hook | `hooks/stop.py` | TODO-sniff safety net — nudges when deferrals went unregistered |
| `UserPromptSubmit` hook | `hooks/drop.py` | Handles `drop A` / `drop A,C` / `drop all` shorthand |
| `$add` skill | `skills/add/` (+ `scripts/register.py`) | Registers a debt entry, assigns a batch letter |
| `$review` skill | `skills/review/` (+ `scripts/review.py`) | Audits + ranks the registry; walks paydown |
| `$init` skill *(explicit-only)* | `skills/init/` | Writes the `## Tech debt operations` charter into `AGENTS.md` |
| `$metrics` skill | `skills/metrics/` | Read-only health summary from the metrics log |

## Codex-specific notes

- **Charter file is `AGENTS.md`**, not `CLAUDE.md` — `$init` writes the managed `## Tech debt operations` section there, and the hooks read quality commands from it.
- **Edits are `apply_patch`.** The feedback hook parses the V4A patch envelope (`*** Add/Update File:`, `*** Move to:`) to learn which files changed, since there's no `tool_input.file_path`.
- **Cache** lives at `~/.cache/debt-ops/cache/<repo-hash>/` (override with `DEBT_OPS_CACHE`) so the hooks and skill Bash always agree on one path ([ADR 0012](../docs/adr/0012-codex-deterministic-cache-base.md)).
- **Skill invocation** is `$add` / `$review` / `$init` / `$metrics` (or the `/skills` picker). `$init` is explicit-only (`skills/init/agents/openai.yaml`).
- **Debug:** set `DEBT_OPS_DEBUG=1` to log every hook fire to `<cache>/debug.log`.

## Research

Disciplines map to the [nine tool-agnostic pillars](../docs/tech-debt-pillars.md); the [Claude Code mapping](../docs/tech-debt-plugin-plan.md) explains why each hook exists. Same evidence base, different agent.
9 changes: 9 additions & 0 deletions plugins/bcanfield/agentic-tech-debt/assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
202 changes: 202 additions & 0 deletions plugins/bcanfield/agentic-tech-debt/hooks/drop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""debt-ops UserPromptSubmit hook: handle 'drop A,B' / 'drop all' shorthand.

Codex adapter. When the user types `drop A`, `drop A,C`, or `drop all` as the
entire prompt, this hook deletes the matching entries from the most recent
batch and blocks the prompt with a one-line confirmation — no model turn
consumed.

Other "drop" forms ("drop it", "drop foo-slug") aren't matched and fall through
to the model's normal handling per the add skill.
"""

import hashlib
import json
import os
import re
import subprocess
import sys
import time
from pathlib import Path

# Strict: full input must be 'drop' + 'all' OR 1-3 letters separated by commas/spaces.
# Trailing period optional. Case-insensitive.
# "drop it" doesn't match (i,t can't be one 1-3-char token without a separator
# — actually `it` IS two letters so [a-z]{1,3} matches it. Guard below).
DROP_RE = re.compile(
r"^\s*drop\s+(all|[a-z]{1,3}(?:[\s,]+[a-z]{1,3})*)\s*\.?\s*$",
re.IGNORECASE,
)

DEFAULT_REGISTRY_DIR = "docs/debt"


# Single deterministic cache base so hook subprocesses and skill Bash (which
# never sees PLUGIN_DATA) resolve the same path. Override with DEBT_OPS_CACHE.
def cache_base():
override = os.environ.get("DEBT_OPS_CACHE")
return Path(override) if override else (Path.home() / ".cache" / "debt-ops")


# Read session-start.py's cached registry-dir path; default if missing/empty.
def read_registry_dir(cache_dir):
f = cache_dir / "registry-dir"
if f.is_file():
try:
val = f.read_text(encoding="utf-8").strip()
if val:
return val
except OSError:
pass
return DEFAULT_REGISTRY_DIR


def git_toplevel():
try:
out = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, check=True, timeout=2,
)
s = out.stdout.strip()
return Path(s) if s else None
except (subprocess.SubprocessError, FileNotFoundError):
return None


def repo_hash(toplevel):
return hashlib.sha1(str(toplevel).encode()).hexdigest()[:12]


def emit_block(reason):
payload = {
"decision": "block",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": reason,
},
"reason": reason,
}
sys.stdout.write(json.dumps(payload) + "\n")


def log_metric(cache_dir, payload):
if not cache_dir.is_dir():
return
payload["ts"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
try:
with (cache_dir / "metrics.jsonl").open("a", encoding="utf-8") as f:
f.write(json.dumps(payload, separators=(",", ":")) + "\n")
except OSError:
pass


def read_batch(path):
"""Parse `LETTER\tslug\tfname` rows into a dict keyed by uppercase letter."""
if not path.is_file():
return {}
try:
text = path.read_text(encoding="utf-8")
except OSError:
return {}
mapping = {}
for ln in text.splitlines():
parts = ln.split("\t")
if len(parts) >= 3 and parts[0].strip():
mapping[parts[0].strip().upper()] = (parts[1].strip(), parts[2].strip())
return mapping


def write_batch(path, mapping):
try:
if not mapping:
path.unlink(missing_ok=True)
return
lines = [f"{L}\t{slug}\t{fname}" for L, (slug, fname) in mapping.items()]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
except OSError:
pass


def main():
try:
raw = sys.stdin.read()
except OSError:
return 0
if not raw:
return 0
try:
data = json.loads(raw)
except (json.JSONDecodeError, ValueError):
return 0
prompt = (data.get("prompt") or "").strip()
if not prompt:
return 0

m = DROP_RE.match(prompt)
if not m:
return 0

# Guard: don't intercept "drop it" — let the model handle it conversationally.
tokens = re.split(r"[\s,]+", m.group(1).strip().lower())
if tokens == ["it"]:
return 0

toplevel = git_toplevel()
if toplevel is None:
return 0

cache_dir = cache_base() / "cache" / repo_hash(toplevel)

# Look in both files: current-turn.txt (just-finished turn, not yet rotated)
# and last-batch.txt (turn before that). Merge with current-turn winning.
current = read_batch(cache_dir / "current-turn.txt")
last = read_batch(cache_dir / "last-batch.txt")
mapping = {**last, **current}
if not mapping:
return 0

if tokens == ["all"]:
letters = list(mapping.keys())
else:
letters = [t.upper() for t in tokens]

registry_dir = toplevel / read_registry_dir(cache_dir)
deleted = []
not_found = []
for L in letters:
if L not in mapping:
not_found.append(L)
continue
slug, fname = mapping[L]
target = registry_dir / fname
try:
target.unlink(missing_ok=True)
deleted.append(slug)
# Remove from whichever source file held it.
current.pop(L, None)
last.pop(L, None)
except OSError:
not_found.append(L)

if not deleted:
# Nothing actually deleted — pass through to the model so they can ask why.
return 0

write_batch(cache_dir / "current-turn.txt", current)
write_batch(cache_dir / "last-batch.txt", last)

log_metric(cache_dir, {"event": "drop", "slugs": deleted, "missed": not_found})

parts = [f"Dropped: {', '.join(deleted)}."]
if not_found:
parts.append(f"Not in batch: {', '.join(not_found)}.")
emit_block(" ".join(parts))
return 0


if __name__ == "__main__":
try:
sys.exit(main())
except Exception:
# A hook bug must never block the user's prompt — exit clean.
sys.exit(0)
Loading