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
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Guidelines

Guidelines for developing **this** repo (the strapped plugin), loaded only when working in the strapped repo. Rules for how the *harness itself* should plan/review/implement live in `plugins/strapped/conventions.md` and the stage prompts under `src/workflows/strapped-run/stages/` — those reach every run against any repo, so put harness-behavior guidance there, not here.

- **Always bump the plugin version when building new changes.** Any PR that changes the plugin's behavior (skills, workflows, scripts, hooks, conventions) must bump `version` in `plugins/strapped/.claude-plugin/plugin.json`. `claude plugin update` compares versions only — with an unbumped version, installed copies silently stay pinned to a stale commit even after the change merges.

## Design

- **This is a new repo — breaking changes are fine.** Don't preserve backward compatibility or carry compat weight when it complicates the design.
- **Don't add machinery for cases that carry no actionable signal.** Scope each mechanism to the state it can actually act on.
2 changes: 1 addition & 1 deletion plugins/strapped/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "strapped",
"version": "0.6.0",
"version": "0.7.0",
"description": "Adversarial plan → implement → stacked-PR coding harness: rule-partitioned reviewers, refute passes, DAG deliverables in persistent worktrees, CLAUDE.md learning loop",
"author": {
"name": "Christian Schuetz",
Expand Down
1 change: 1 addition & 0 deletions plugins/strapped/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ Deterministic executables under `$PLUGIN_ROOT/scripts/`, invocable via Bash by s
Node CLI that bundles `js-yaml` directly for frontmatter parsing and writing (a small `---` fence split does the rest — no `gray-matter` wrapper, so the artifact carries a single js-yaml). A write re-serializes the whole frontmatter block through the js-yaml engine, so it is not a byte-for-byte preservation of every line. The engine is pinned (`flowLevel: 1`, `condenseFlow`, `lineWidth: -1`) so the two shapes grep-based consumers depend on survive: the deliverable `deps: [...]` flow array (`sync-prs.sh` parses `[...]`) and the single-space `key: value` scalar block lines (`sync-prs.sh`/`preamble.sh` read `^status:`/`^pr:`/`^id:`). js-yaml quotes a scalar only when it contains a colon-SPACE `: ` (e.g. an agent-composed `parked_reason: 'typecheck failed: TS2322'`); a `pr:` URL stays unquoted (`pr: https://…`) because its `://` is colon-slash, a valid plain scalar. Either way the `key: value` line shape holds, and `sync-prs.sh` tolerates an optional surrounding quote. The manifest's `repos:`/`deliverables:`/`budgets:` maps are read only by state.ts (never grepped by bash), so their reflow under this engine is inconsequential.

- **`resolve <slug>`** — resolves `stateRoot` per [Config resolution](#config-resolution) (`$STRAPPED_STATE_ROOT` → `~/.claude/strapped.json` → default `~/.claude/strapped`; leading `~` expanded; a value still relative after expansion is invalid input → exit 1) and probes `<stateRoot>/runs/<slug>/manifest.md` — the cwd-independent direct path, no glob. Output: `{ slug, stateRoot, runRoot, runDir, manifest, exists, status, seed, budgets, repos: [{ name, root, config, configExists, validations, worktreeRoot, provisioning }] }`, where `repos` comes from the manifest `repos:` map joined with each repo's config at `<stateRoot>/repos/<name>/config.json`. A missing manifest is NOT an error: `exists: false`, exit 0 (the plan skill treats a miss as "no existing run"; slug-addressed downstream skills stop themselves).
- **`runroot`** — slug-less resolution for callers that scan across ALL runs (e.g. `/strapped:learn` globbing every `runs/*/critiques/user-critiques.md`). Resolves `stateRoot` per [Config resolution](#config-resolution) (same chain and hard-fail-on-invalid rules as `resolve`) and returns `{ stateRoot, runRoot }` where `runRoot` = `<stateRoot>/runs`. An unresolvable or non-absolute anchor → exit 1, so a subsequent empty cross-run glob is unambiguously "no runs" rather than a silently-wrong root. Never hand-roll `stateRoot`/`runRoot` from the cwd or an ad-hoc `~/.claude/strapped.json` read.
- **`dag <runDir> [--only <Did>]`** — reads the manifest `deliverables` list and every deliverable file's frontmatter. Output: `{ manifest: {status, seed, budgets}, nodes: [{id, file, title, status, deps, repo, branch, base, worktree, pr, review_rounds_used, feedback_rounds_used, parked_reason, estimated_diff_lines}], ready, topo, blocked: [{id, blockedOn}], remaining }`. `ready` = `status: pending` nodes whose deps are all `done`/`pr-open`/`merged`; with `--only`, a `parked`/`in-progress` node is additionally admitted (implement's `--only` resume semantics) and `ready` is intersected with the named node. `topo` = stable topological order, parents before children, ties broken by id. `remaining` = count of nodes NOT yet `done`/`pr-open`/`merged` — done-or-later counts as complete, so a partially-shipped run reports the true remaining work; consumers read this field verbatim and never recompute it. Unknown dep id or dependency cycle → exit 1 naming the offender.
- **`set <file> <field> <value>`** — idempotent single-field frontmatter write; `value` is written verbatim after `<field>: ` (`null` writes literal `null`). `value` must be a single line: a value containing `\n` or `\r` → exit 1, no write (a multi-line value would inject extra frontmatter lines). A field not already present in the file's frontmatter → exit 1 (no silent field invention). Output: `{file, field, old, new}`.
- **`transition <file> <to> [--from <expected>]`** — guarded deliverable status flip over the on-disk edge table below. `--from` adds an exact-current-status guard. Transitioning to the current status is an idempotent no-op: exit 0, `{changed: false}`. An illegal edge → exit 1 naming the current status and requested edge, no write. Output: `{file, from, to, changed}`.
Expand Down
10 changes: 9 additions & 1 deletion plugins/strapped/scripts/state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3277,6 +3277,10 @@ function cmdResolve(slug) {
repos
});
}
function cmdRunRoot() {
const stateRoot = resolveStateRoot(die2);
out({ stateRoot, runRoot: join2(stateRoot, "runs") });
}
var COMPLETE_STATUSES = new Set(["done", "pr-open", "merged"]);
function cmdDag(runDir, only) {
const manifestFile = join2(runDir, "manifest.md");
Expand Down Expand Up @@ -3567,7 +3571,7 @@ function cmdFeedbackIndexSet(runDir, externalId, status, commit) {
writeFeedbackIndex(path, index);
out({ externalId, from, to: status, commit: comment.commit, changed: true });
}
var USAGE = "usage: state.mjs <resolve|dag|set|transition|manifest-status|feedback-index> ...";
var USAGE = "usage: state.mjs <resolve|runroot|dag|set|transition|manifest-status|feedback-index> ...";
var [cmd, ...rest] = process.argv.slice(2);
function takeFlag(args, flag) {
const i = args.indexOf(flag);
Expand All @@ -3587,6 +3591,10 @@ switch (cmd) {
cmdResolve(slug);
break;
}
case "runroot": {
cmdRunRoot();
break;
}
case "dag": {
const only = takeFlag(rest, "--only");
const runDirArg = rest[0];
Expand Down
2 changes: 2 additions & 0 deletions plugins/strapped/skills/implement/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ The `resolve` output already carries the manifest `status`, `seed`, `budgets`, a

Each deliverable's `repo:` field is **required** and names one of the `repos:` entries — a deliverable with no `repo:` is invalid input.

**Baseline-freshness pre-flight.** The plan may have been written before `main` moved. Before dispatching, in each target repo `git -C <repoRoot> fetch` and diff the plan's architectural assumptions against current `origin/main` (recent commits touching the conventions/specs/modules the deliverables cite). If main has moved under the plan, surface the conflicts to the user and let them choose to amend the affected deliverables to match current main (or re-plan) — never implement a spec you know is stale.

## Step 2 — Rule assignments

As in /strapped:plan: read `reviews/rules-snapshot.md` (re-extract if missing — discover every applicable CLAUDE.md AND recurse into any skills/files it loads for additional rules, per the conventions' **Rule extraction**), compute the per-round rule splits (full rule objects) for rounds `1..code_rounds` using `random.Random(seed + round)` from the manifest seed.
Expand Down
29 changes: 21 additions & 8 deletions plugins/strapped/skills/learn/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: learn
description: Synthesize captured user critiques from strapped runs into proposed CLAUDE.md guideline additions — presented as a diff for approval, never auto-applied
description: Synthesize captured user critiques from strapped runs into proposed guidelines, routed by scope to the harness (stage prompts / SKILLs / conventions) or the pertaining repo's CLAUDE.md — presented as a diff for approval, never auto-applied
allowed-tools:
- Read
- Write
Expand All @@ -15,24 +15,37 @@ Turn the user's recurring corrections into durable guidelines. Source format is

## Step 1 — Collect

Collect every critique entry with `synthesized: false` across all runs, per the conventions' Config resolution: glob `<stateRoot>/runs/*/critiques/user-critiques.md` — every run under the global state root, so critiques from **every** run are collected. The `runs/` tier never touches `repos/` (its sibling dir).
Resolve the run root the SAME way every other strapped skill does — via the canonical resolver, never by hand-rolling the config chain or reading `~/.claude/strapped.json` yourself (a mis-resolved root silently globs to zero and is indistinguishable from "no critiques"):

If there are none, say so and stop.
```bash
node $PLUGIN_ROOT/scripts/state.mjs runroot # → { "stateRoot": "<abs>", "runRoot": "<abs>/runs" }
```

If that command exits non-zero (unresolvable or non-absolute anchor), **stop and report the resolution error** — do NOT treat it as "no critiques."

Then collect every critique entry with `synthesized: false` across all runs: glob `<runRoot>/*/critiques/user-critiques.md` — every run under the global state root, so critiques from **every** run are collected. The `runs/` tier never touches `repos/` (its sibling dir). State the resolved `runRoot` and how many critique files matched, so a zero is legibly "root X held no unsynthesized critiques" and not a swallowed resolution failure.

If the root resolved cleanly but there are genuinely no unsynthesized entries, say so and stop.

## Step 2 — Cluster and filter

1. Group entries expressing the same underlying rule (across runs).
2. Drop clusters already covered by an existing rule — read every applicable CLAUDE.md first and compare meaning, not wording.
2. Drop clusters already covered by an existing rule — compare **meaning**, not wording, against wherever a rule of that scope would already live (per Step 3's routing): every applicable `CLAUDE.md`, `$PLUGIN_ROOT/conventions.md`, and the stage prompts under `$PLUGIN_ROOT/../../src/workflows/strapped-run/`. A critique whose lesson is already encoded in a stage prompt is covered even if no `CLAUDE.md` mentions it.
3. Drop entries marked `generalizable: no` or that are plan-specific one-offs; flip those to `synthesized: no` with a short reason appended to the entry.

## Step 3 — Draft
## Step 3 — Classify scope and route

Critiques captured during runs are usually corrections about **how the harness behaves**, not about how to develop the plugin repo — and a rule only fires where it is actually loaded. Classify each surviving cluster and pick its target file accordingly (a cluster may be **both**, landing in more than one place):

- **harness-behavior** — how the strapped harness itself plans, reviews, implements, creates PRs, or otherwise operates; it must shape FUTURE runs against ANY repo. Route to the harness, most specific first: the exact agent prompt under `src/workflows/strapped-run/stages/*.ts` (or `review-loop.ts`) when the rule governs one agent's behavior; the relevant skill's `SKILL.md` step when it is an orchestrator/interactive concern; `conventions.md` when it is a cross-cutting format/procedure rule seeded to every subagent. Editing any `src/**` stage prompt requires a rebuild (`bun run build`) of the generated `plugins/strapped/workflows/strapped-run.js` and a plugin-version bump (per the repo's own CLAUDE.md).
- **repo-development** — how to develop a specific repo's code (its naming, testing style, build, architecture). Route to **that repo's** `CLAUDE.md`, NOT the plugin's. Determine the pertaining repo from the cluster's source critiques: each lives under `<runRoot>/<slug>/critiques/`, so resolve that run via `node $PLUGIN_ROOT/scripts/state.mjs resolve <slug>` and use its `repos[].root` — the guideline lands in that repo root's `CLAUDE.md`. Only when the pertaining repo genuinely IS the strapped plugin does it land in this repo's `CLAUDE.md`.

For each surviving cluster, draft one guideline line in the existing CLAUDE.md voice (terse imperative bullets, no explanations) and pick the section it belongs in (or propose a new section only when nothing fits). Prefer editing an existing rule over adding a near-duplicate.
Draft one guideline line per cluster in the target file's existing voice (terse imperative, no explanations), and pick the section it belongs in (or a new section only when nothing fits). Prefer editing an existing rule over a near-duplicate.

## Step 4 — Propose (the gate)

Present a **unified diff** of the proposed CLAUDE.md changes plus, per hunk, the source critiques that motivated it. Ask the user to approve/reject each proposed guideline (AskUserQuestion with one question per guideline when few, or a single multi-select). **Apply nothing without explicit approval.**
Present a **unified diff per target file**, and for each proposed guideline state its scope (harness-behavior/repo-development), its destination file, and the source critiques that motivated it. Ask the user to approve/reject each proposed guideline (AskUserQuestion with one question per guideline when few, or a single multi-select). **Apply nothing without explicit approval.**

## Step 5 — Apply approved changes only

Edit CLAUDE.md with the approved hunks only. Flip each consumed entry to `synthesized: true` (rejected clusters: `synthesized: no`). Report what was applied and what was rejected.
Edit each approved guideline into its routed target file (rebuild + bump the plugin version if any `src/**` stage prompt changed). Flip each consumed entry to `synthesized: true` (rejected clusters: `synthesized: no`). Report what was applied, where each guideline landed, and what was rejected.
Loading