feat(transcend): carry session scripts, tests and skills into the repos
Adds kvb-transcend (collector), kvb-skills-transcend (into the skills repo) and kvb-code-transcend (into the current repo). transcend.py scans the own scratchpad, peer scratchpads and the agent homes for scripts, tests and skills the skills repo does not ship, skips secret-named and oversized files, and remembers carried/dropped decisions by content digest in the gitignored .state/transcend.json, so a rescan offers only new or changed candidates. Wired into kvb-skills-tune (own scripts), kvb-code-tune step 6 (measurements) and kvb-skills-dream step 7. Seven descriptions are tightened to stay inside the 1300-word listing budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42b66e3ddf
commit
2fbbd7386c
16 files changed
+506
-25
No files matched your search
@@ -17,6 +17,8 @@ npm-debug.log*
|
|||||||
.claude/
|
.claude/
|
||||||
.mcp.json
|
.mcp.json
|
||||||
|
|
||||||
|
.state/
|
||||||
|
|
||||||
# Editor / OS
|
# Editor / OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
@@ -9,7 +9,7 @@ Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline
|
|||||||
|
|
||||||
## Naming 🏷️
|
## Naming 🏷️
|
||||||
|
|
||||||
Every first-party skill is named `kvb-<category>-<name>` with one of these categories: `think`, `create`, `code`, `ci`, `git`, `check`, `org`, `skills`. Only `kvb-cleanup` and `kvb-help` stand without a category. Names use `a-z`, `0-9` and single hyphens, at most 64 characters, and equal their directory name. `tests/test_naming.py` enforces all of it, including that the organigram below lists every skill under its category.
|
Every first-party skill is named `kvb-<category>-<name>` with one of these categories: `think`, `create`, `code`, `ci`, `git`, `check`, `org`, `skills`. Only `kvb-cleanup`, `kvb-help` and the collector `kvb-transcend` stand without a category. Names use `a-z`, `0-9` and single hyphens, at most 64 characters, and equal their directory name. `tests/test_naming.py` enforces all of it, including that the organigram below lists every skill under its category.
|
||||||
|
|
||||||
## Organigram 🏛️
|
## Organigram 🏛️
|
||||||
|
|
||||||
@@ -39,6 +39,7 @@ flowchart LR
|
|||||||
code --> code_refactor[kvb-code-refactor]
|
code --> code_refactor[kvb-code-refactor]
|
||||||
code --> code_test[kvb-code-test]
|
code --> code_test[kvb-code-test]
|
||||||
code --> code_tune[kvb-code-tune]
|
code --> code_tune[kvb-code-tune]
|
||||||
|
code --> code_transcend[kvb-code-transcend]
|
||||||
ci --> ci_triage[kvb-ci-triage]
|
ci --> ci_triage[kvb-ci-triage]
|
||||||
ci --> ci_artifacts[kvb-ci-artifacts]
|
ci --> ci_artifacts[kvb-ci-artifacts]
|
||||||
ci --> ci_observe[kvb-ci-observe]
|
ci --> ci_observe[kvb-ci-observe]
|
||||||
@@ -65,8 +66,10 @@ flowchart LR
|
|||||||
skills --> skills_tune[kvb-skills-tune]
|
skills --> skills_tune[kvb-skills-tune]
|
||||||
skills --> skills_auto[kvb-skills-auto]
|
skills --> skills_auto[kvb-skills-auto]
|
||||||
skills --> skills_essence[kvb-skills-essence]
|
skills --> skills_essence[kvb-skills-essence]
|
||||||
|
skills --> skills_transcend[kvb-skills-transcend]
|
||||||
none --> cleanup[kvb-cleanup]
|
none --> cleanup[kvb-cleanup]
|
||||||
none --> help[kvb-help]
|
none --> help[kvb-help]
|
||||||
|
none --> transcend_all[kvb-transcend]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Skill map 🗺️
|
## Skill map 🗺️
|
||||||
@@ -100,6 +103,7 @@ flowchart LR
|
|||||||
test[kvb-code-test]
|
test[kvb-code-test]
|
||||||
tune[kvb-code-tune]
|
tune[kvb-code-tune]
|
||||||
comments_clean[kvb-code-no-comments]
|
comments_clean[kvb-code-no-comments]
|
||||||
|
code_transcend[kvb-code-transcend]
|
||||||
end
|
end
|
||||||
subgraph cat_ci [ci]
|
subgraph cat_ci [ci]
|
||||||
triage[kvb-ci-triage]
|
triage[kvb-ci-triage]
|
||||||
@@ -136,10 +140,12 @@ flowchart LR
|
|||||||
autotune[kvb-skills-tune]
|
autotune[kvb-skills-tune]
|
||||||
autoskill[kvb-skills-auto]
|
autoskill[kvb-skills-auto]
|
||||||
essence[kvb-skills-essence]
|
essence[kvb-skills-essence]
|
||||||
|
skills_transcend[kvb-skills-transcend]
|
||||||
end
|
end
|
||||||
subgraph cat_none [no category]
|
subgraph cat_none [no category]
|
||||||
cleanup[kvb-cleanup]
|
cleanup[kvb-cleanup]
|
||||||
help_skills[kvb-help]
|
help_skills[kvb-help]
|
||||||
|
transcend[kvb-transcend]
|
||||||
end
|
end
|
||||||
|
|
||||||
commit_req -.-> commit_next
|
commit_req -.-> commit_next
|
||||||
@@ -209,6 +215,14 @@ flowchart LR
|
|||||||
dream --> autoskill
|
dream --> autoskill
|
||||||
autoskill --> autotune
|
autoskill --> autotune
|
||||||
autotune --> essence
|
autotune --> essence
|
||||||
|
dream --> transcend
|
||||||
|
transcend --> skills_transcend
|
||||||
|
transcend --> code_transcend
|
||||||
|
skills_transcend --> active_listening
|
||||||
|
skills_transcend --> autotune
|
||||||
|
code_transcend --> active_listening
|
||||||
|
autotune -. own scripts .-> skills_transcend
|
||||||
|
tune -. measurements .-> code_transcend
|
||||||
|
|
||||||
test --> triage
|
test --> triage
|
||||||
test --> dialectic
|
test --> dialectic
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
name: kvb-code-transcend
|
||||||
|
description: >
|
||||||
|
Carry the session's ad-hoc tests and scripts into the current repo.
|
||||||
|
Trigger on /kvb-code-transcend.
|
||||||
|
---
|
||||||
|
|
||||||
|
The test the agent ran from its scratchpad, the probe that proved a cause, the
|
||||||
|
benchmark behind an optimization: they protected this session's work and
|
||||||
|
nothing after it. This skill moves them into the repository the agent works
|
||||||
|
in (`git rev-parse --show-toplevel`), in that repository's own form. It
|
||||||
|
changes nothing before the operator confirms.
|
||||||
|
|
||||||
|
## Step 1: inventory
|
||||||
|
|
||||||
|
Read-only:
|
||||||
|
|
||||||
|
- **Tests and scripts outside the repo**: `transcend.py scan` of the
|
||||||
|
`kvb-transcend` skill lists them in the own and peers' scratchpads and
|
||||||
|
skips what was already decided and has not changed; peer candidates follow
|
||||||
|
that skill's consent step. Add the `python -c` checks and negative controls
|
||||||
|
this session ran inline.
|
||||||
|
- **Probes and reproductions** that proved a root cause or a fix.
|
||||||
|
- **Measurements**: prototypes and before/after harnesses of the
|
||||||
|
`kvb-code-tune` skill.
|
||||||
|
- **Repeated commands** a make target or script of the repository could own.
|
||||||
|
- **Untracked helpers** the agent left inside the repository.
|
||||||
|
|
||||||
|
One line each: `candidate -> source -> what it guards`.
|
||||||
|
|
||||||
|
## Step 2: classify
|
||||||
|
|
||||||
|
Read the repository's rules first (AGENTS.md, CONTRIBUTING, the Makefile, the
|
||||||
|
test layout). Per candidate one target in that repository's form: a test in
|
||||||
|
its test tree and framework, a make target, a script under its scripts
|
||||||
|
directory, a CI step, or drop. No new framework, dependency or language
|
||||||
|
without the operator. A kept optimization keeps its measurement as a
|
||||||
|
benchmark or regression test.
|
||||||
|
|
||||||
|
## Step 3: listen and confirm
|
||||||
|
|
||||||
|
Run the `kvb-think-listen` skill on the open points: target path, framework,
|
||||||
|
which repository line owns the file. Then `AskUserQuestion`: one
|
||||||
|
`multiSelect` with the candidates, nothing preselected, and per pick a
|
||||||
|
single-select `Write it` / `Revise it` / `Discard` with the drafted diff.
|
||||||
|
|
||||||
|
## Step 4: write
|
||||||
|
|
||||||
|
- Write only the confirmed files, path-scoped; a peer's line goes to its
|
||||||
|
owner through the `kvb-org-coordinate` skill.
|
||||||
|
- Every carried test fails without the code it guards: show the negative
|
||||||
|
control once.
|
||||||
|
- Run the repository's tests only on the resource chair's grant (the
|
||||||
|
`kvb-org-organize` skill); leave the result uncommitted for the
|
||||||
|
`kvb-git-commit-next` skill.
|
||||||
|
- `transcend.py mark` every offered candidate with its verdict.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
| Candidate | Source | Target in the repo | Verdict |
|
||||||
|
|
||||||
|
Dropped candidates stay with their reason.
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
name: kvb-code-tune
|
name: kvb-code-tune
|
||||||
description: >
|
description: >
|
||||||
Find and apply performance optimizations (CI, deploy, build, gate, hot
|
Find and apply performance optimizations (CI, deploy, build, gate, hot
|
||||||
paths) without reducing logic, coverage or correctness. Trigger on
|
paths) without losing logic or coverage. Trigger on /kvb-code-tune, a job
|
||||||
/kvb-code-tune, a job dying on its time budget, a slower pipeline or deploy,
|
hitting its time budget, a slower pipeline, or asked where time goes.
|
||||||
or asked where time goes.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Tune
|
# Tune
|
||||||
@@ -205,6 +204,9 @@ failure, or only moves the cost somewhere the measurement does not look.
|
|||||||
proposals. Re-measure from the same source under the same conditions and
|
proposals. Re-measure from the same source under the same conditions and
|
||||||
report a before and an after number.
|
report a before and an after number.
|
||||||
5. Repeat until the next candidate is worth less than the run that measures it.
|
5. Repeat until the next candidate is worth less than the run that measures it.
|
||||||
|
6. Hand the prototypes and before/after harnesses of every kept candidate to
|
||||||
|
the `kvb-code-transcend` skill, so the measurement stays in the repository
|
||||||
|
as a benchmark or regression test.
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: kvb-git-changelog
|
name: kvb-git-changelog
|
||||||
description: >
|
description: >
|
||||||
Write the release message since the last release for `pkgmgr release`;
|
Write the release message for `pkgmgr release`;
|
||||||
prints it only, never edits CHANGELOG.md, tags or releases. Trigger on
|
prints only, never edits, tags or releases. Trigger on
|
||||||
/kvb-git-changelog, "release message", "changelog entry", before cutting a release,
|
/kvb-git-changelog, "release message", "changelog entry", before cutting a release,
|
||||||
or on pkgmgr's markdown-lint rejection.
|
or on pkgmgr's markdown-lint rejection.
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: kvb-git-merge-fix
|
name: kvb-git-merge-fix
|
||||||
description: >
|
description: >
|
||||||
Hunt down the commits on any branch that carry a described fix and merge it
|
Find the commits on any branch that carry a described fix and merge them
|
||||||
into the current branch. Trigger on /kvb-git-merge-fix <description> or when the
|
here. Trigger on /kvb-git-merge-fix <description> or a request to bring over
|
||||||
operator asks to bring over a fix from another branch.
|
a fix from another branch.
|
||||||
---
|
---
|
||||||
|
|
||||||
The free text after `/kvb-git-merge-fix` is the fix description, exactly like `/loop`
|
The free text after `/kvb-git-merge-fix` is the fix description, exactly like `/loop`
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: kvb-org-organize
|
name: kvb-org-organize
|
||||||
description: >
|
description: >
|
||||||
Keep parallel agents from duplicating work: the first to claim it owns it;
|
Keep parallel agents from duplicating work: the first claim owns it; a
|
||||||
line representatives and a resource chair coordinate. Run AUTOMATICALLY
|
resource chair allocates. Run AUTOMATICALLY
|
||||||
before work another agent might also do (failure, cluster, feature, gate,
|
before work another agent might also do (failure, cluster, feature, gate,
|
||||||
deploy, cleanup), and on /kvb-org-organize.
|
deploy, cleanup), and on /kvb-org-organize.
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: kvb-org-split
|
name: kvb-org-split
|
||||||
description: >
|
description: >
|
||||||
Keep agent work in its own git worktree until it reaches 90% fix confidence
|
Keep agent work in its own worktree until 90% fix confidence and under 5%
|
||||||
and under 5% break risk; below that bar it becomes a proposal. Trigger when
|
break risk; below that it becomes a proposal. Trigger when delegating to or
|
||||||
delegating work to agents, when agents work in parallel, or on /kvb-org-split.
|
parallelizing agents, or on /kvb-org-split.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Funnel
|
# Funnel
|
||||||
|
|||||||
@@ -109,12 +109,13 @@ as radio buttons: the improvement, the evidence behind it, and the options
|
|||||||
`Apply now`, `Record as a task`, `Skip`. Ranked by evidence, one question per
|
`Apply now`, `Record as a task`, `Skip`. Ranked by evidence, one question per
|
||||||
improvement. No repository file changes before an answer comes back.
|
improvement. No repository file changes before an answer comes back.
|
||||||
|
|
||||||
## Step 7: kvb-skills-tune, then kvb-skills-auto
|
## Step 7: kvb-skills-tune, kvb-skills-auto, kvb-transcend
|
||||||
|
|
||||||
With the dialectic done and memory written, run `kvb-skills-tune` — hand it this run's
|
With the dialectic done and memory written, run `kvb-skills-tune` — hand it this run's
|
||||||
findings as its evidence so it does not re-collect them — and then `kvb-skills-auto`
|
findings as its evidence so it does not re-collect them — then `kvb-skills-auto`
|
||||||
for the operator corrections the window revealed. Both keep their own
|
for the operator corrections the window revealed, and `kvb-transcend` for the
|
||||||
confirmation steps; the dream never writes a skill on their behalf.
|
scripts and tests the window's sessions wrote outside a repository. All keep
|
||||||
|
their own confirmation steps; the dream never writes a skill on their behalf.
|
||||||
|
|
||||||
Close the run by refreshing this repository's reminder stamp, so the next hint
|
Close the run by refreshing this repository's reminder stamp, so the next hint
|
||||||
is twelve hours out: `date +%s > "$(git rev-parse --path-format=absolute --git-common-dir)/.dream-stamp"`.
|
is twelve hours out: `date +%s > "$(git rev-parse --path-format=absolute --git-common-dir)/.dream-stamp"`.
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: kvb-skills-transcend
|
||||||
|
description: >
|
||||||
|
Carry the session's skill use and own scripts into the skills repo.
|
||||||
|
Trigger on /kvb-skills-transcend.
|
||||||
|
---
|
||||||
|
|
||||||
|
What an agent improvised in one session dies with it: the helper script in its
|
||||||
|
scratchpad, the command sequence it ran five times, the skill it had to work
|
||||||
|
around. This skill carries that into the operator's skills repository, so the
|
||||||
|
next agent starts from it. It changes nothing before the operator confirms.
|
||||||
|
|
||||||
|
## Step 1: inventory
|
||||||
|
|
||||||
|
Read-only:
|
||||||
|
|
||||||
|
- **Scripts and skills**: `transcend.py scan` of the `kvb-transcend` skill
|
||||||
|
lists the own scratchpad's scripts, peers' scripts, and skills, commands,
|
||||||
|
hooks and rules in `~/.claude`, `~/.agents` and `~/.codex` that the skills
|
||||||
|
repository does not ship; it skips what was already decided and has not
|
||||||
|
changed. Peer and home candidates follow that skill's consent step.
|
||||||
|
- **Skills used**: every skill this session invoked, how often, and where it
|
||||||
|
fell short (a workaround, a step skipped, an operator correction).
|
||||||
|
- **Recurring moves without a skill**: command sequences, peer exchanges or
|
||||||
|
lookups run three times or more. `rasci.py tools` of the `kvb-org-rasci`
|
||||||
|
register lists the scripts the session's tasks recorded.
|
||||||
|
|
||||||
|
One line each: `candidate -> source -> evidence`.
|
||||||
|
|
||||||
|
## Step 2: classify
|
||||||
|
|
||||||
|
Per candidate one target: a script under an existing skill's `scripts/`, a
|
||||||
|
new skill, a sharper step in an existing skill, a memory entry instead, or
|
||||||
|
drop. Recurring moves without a script go to the `kvb-skills-tune` skill as
|
||||||
|
evidence. A script is portable only once its host paths, names and values
|
||||||
|
became explicit parameters (the `kvb-code-no-defaults` skill); one that needs
|
||||||
|
a secret, a credential or a single host is dropped or stays a memory entry.
|
||||||
|
|
||||||
|
## Step 3: listen and confirm
|
||||||
|
|
||||||
|
Run the `kvb-think-listen` skill on the open points: scope, target skill,
|
||||||
|
generality. Then `AskUserQuestion`: one `multiSelect` with the candidates
|
||||||
|
(target and evidence in each description, nothing preselected), and per pick a
|
||||||
|
single-select `Write it` / `Revise it` / `Discard` showing the drafted file or
|
||||||
|
diff.
|
||||||
|
|
||||||
|
## Step 4: write
|
||||||
|
|
||||||
|
- Write where the `kvb-skills-tune` skill's **Persistence** section says:
|
||||||
|
the skills repository as source of truth, then the live mirrors.
|
||||||
|
- Read every script whole before carrying it; strip comments the way the
|
||||||
|
`kvb-code-no-comments` skill does.
|
||||||
|
- A carried script gets a unit test in the repository's `tests/` with a
|
||||||
|
negative control. Tests run only on the resource chair's grant (the
|
||||||
|
`kvb-org-organize` skill).
|
||||||
|
- `transcend.py mark` every offered candidate with its verdict.
|
||||||
|
- Leave the result uncommitted; committing is the `kvb-git-commit-next`
|
||||||
|
skill's job.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
| Candidate | Source | Target | Verdict |
|
||||||
|
|
||||||
|
Dropped candidates stay with their reason.
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: kvb-skills-tune
|
name: kvb-skills-tune
|
||||||
description: >
|
description: >
|
||||||
Generate new skills or optimize existing ones (fewer tokens, fewer errors,
|
Generate new skills or optimize existing ones (fewer tokens and errors,
|
||||||
persistent learnings), each proposal confirmed by single-select question.
|
persistent learnings), each proposal confirmed. Trigger ONLY on an explicit
|
||||||
Trigger ONLY on an explicit request: /kvb-skills-tune, "autotune", "tune the
|
request: /kvb-skills-tune, "autotune", "tune the skills".
|
||||||
skills"; never on your own initiative.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Autotune turns observed friction into durable skills. It runs on demand only,
|
Autotune turns observed friction into durable skills. It runs on demand only,
|
||||||
@@ -63,6 +62,9 @@ where readable, from the project's memory and agent instruction files:
|
|||||||
deploys, questions that stalled an autonomous run.
|
deploys, questions that stalled an autonomous run.
|
||||||
- **Lost learnings**: facts re-derived this session that a memory file or a
|
- **Lost learnings**: facts re-derived this session that a memory file or a
|
||||||
skill should already have carried.
|
skill should already have carried.
|
||||||
|
- **Own scripts**: helpers the agent wrote outside any repository belong to
|
||||||
|
the `kvb-skills-transcend` skill, which carries them over; this skill
|
||||||
|
takes the frictions they reveal.
|
||||||
|
|
||||||
List each finding as one line: `symptom -> cost -> candidate fix`. Skip
|
List each finding as one line: `symptom -> cost -> candidate fix`. Skip
|
||||||
anything that happened once and is not a pattern.
|
anything that happened once and is not a pattern.
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ name: kvb-think-dialectic
|
|||||||
description: >
|
description: >
|
||||||
Reach a defensible conclusion by thesis, independent skeptic antithesis and
|
Reach a defensible conclusion by thesis, independent skeptic antithesis and
|
||||||
synthesis, iterated to ~99% confidence. Trigger for any consequential root
|
synthesis, iterated to ~99% confidence. Trigger for any consequential root
|
||||||
cause, design or decision where being wrong is expensive and the answer is
|
cause, design or decision that is uncertain and expensive to get wrong.
|
||||||
uncertain.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Reach the truth by dialectic, not by first guess. Do not stop at a plausible
|
Reach the truth by dialectic, not by first guess. Do not stop at a plausible
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: kvb-transcend
|
||||||
|
description: >
|
||||||
|
Run both transcend skills in one pass. Trigger on /kvb-transcend.
|
||||||
|
---
|
||||||
|
|
||||||
|
Collector for the `kvb-skills-transcend` and `kvb-code-transcend` skills, so
|
||||||
|
one harvest needs one round of questions. Its script finds the candidates and
|
||||||
|
remembers every decision, so a rescan offers only what is new or changed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
transcend.py scan --state <skills repo>/.state/transcend.json \
|
||||||
|
--skills-repo <skills repo> --session-dir <own scratchpad> \
|
||||||
|
--peers-root /tmp/claude-<uid> --home ~ [--json]
|
||||||
|
transcend.py mark --state <same> <path> carried|dropped|later
|
||||||
|
```
|
||||||
|
|
||||||
|
`${CLAUDE_SKILL_DIR}/scripts/transcend.py` scans three sources: the own
|
||||||
|
scratchpad, every peer session's scratchpad, and the agent homes
|
||||||
|
(`~/.claude`, `~/.agents`, `~/.codex`) for skills, commands, agents, hooks
|
||||||
|
and rules the skills repository does not ship. It keeps scripts, tests and
|
||||||
|
skills, and skips files named like secrets and files over 1 MB. Carried or
|
||||||
|
dropped candidates return only when their content changes; `later` returns
|
||||||
|
every time.
|
||||||
|
|
||||||
|
1. Scan, then classify each candidate by both skills' classify steps.
|
||||||
|
2. Consent: a peer's candidates go to that peer, one message listing them,
|
||||||
|
and only what it releases stays; home candidates need the operator's yes.
|
||||||
|
3. One `kvb-think-listen` round and one confirmation for all candidates,
|
||||||
|
grouped by target: the skills repository or the current repository. In
|
||||||
|
the skills repository itself, scripts and skills go to
|
||||||
|
`kvb-skills-transcend`, tests to `kvb-code-transcend`.
|
||||||
|
4. Hand each confirmed candidate to its skill's write step, then `mark` every
|
||||||
|
offered candidate with its verdict.
|
||||||
|
5. One table: `| Candidate | Source | Target | Verdict |`.
|
||||||
Executable
+187
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find scripts, tests and skills worth carrying over, and remember every decision.
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 done, 2 usage error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT_SUFFIXES = {".py", ".sh", ".bash", ".js", ".mjs", ".ts", ".rb", ".pl"}
|
||||||
|
TEST_NAME = re.compile(r"(^test_.+|.+_test|.+\.(spec|test))\.[a-z]+$")
|
||||||
|
SECRET_NAME = re.compile(
|
||||||
|
r"(secret|token|credential|passw|auth|\.env$|id_[a-z0-9]+$|\.pem$|\.key$)", re.IGNORECASE
|
||||||
|
)
|
||||||
|
MAX_BYTES = 1_000_000
|
||||||
|
HOME_SOURCES = {
|
||||||
|
".claude/skills": "skill",
|
||||||
|
".agents/skills": "skill",
|
||||||
|
".codex/skills": "skill",
|
||||||
|
".claude/commands": "command",
|
||||||
|
".claude/agents": "agent",
|
||||||
|
".claude/hooks": "script",
|
||||||
|
".claude/scripts": "script",
|
||||||
|
".codex/rules": "rule",
|
||||||
|
}
|
||||||
|
SETTLED = ("carried", "dropped")
|
||||||
|
VERDICTS = (*SETTLED, "later")
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message: str) -> None:
|
||||||
|
print(f"transcend: {message}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def digest(path: Path) -> str:
|
||||||
|
sha = hashlib.sha256()
|
||||||
|
files = sorted(p for p in path.rglob("*") if p.is_file()) if path.is_dir() else [path]
|
||||||
|
for file in files:
|
||||||
|
sha.update(str(file.relative_to(path) if path.is_dir() else file.name).encode())
|
||||||
|
sha.update(file.read_bytes())
|
||||||
|
return sha.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def file_kind(path: Path) -> str | None:
|
||||||
|
if SECRET_NAME.search(path.name) or path.stat().st_size > MAX_BYTES:
|
||||||
|
return None
|
||||||
|
if TEST_NAME.match(path.name):
|
||||||
|
return "test"
|
||||||
|
if path.suffix in SCRIPT_SUFFIXES or os.access(path, os.X_OK):
|
||||||
|
return "script"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def session_names(home: Path) -> dict[str, str]:
|
||||||
|
names = {}
|
||||||
|
for path in (home / ".claude" / "sessions").glob("*.json"):
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, ValueError):
|
||||||
|
continue
|
||||||
|
if data.get("sessionId"):
|
||||||
|
names[data["sessionId"]] = data.get("name") or data["sessionId"]
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def scratch_candidates(root: Path, source: str, owner: str) -> list[dict]:
|
||||||
|
found = []
|
||||||
|
for path in sorted(p for p in root.rglob("*") if p.is_file()):
|
||||||
|
kind = file_kind(path)
|
||||||
|
if kind:
|
||||||
|
found.append({"source": source, "owner": owner, "kind": kind, "path": str(path)})
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def shipped(skills_repo: Path) -> set[str]:
|
||||||
|
names = {p.name for p in (skills_repo / "skills").iterdir() if p.is_dir()}
|
||||||
|
lock = json.loads((skills_repo / "skills-lock.json").read_text(encoding="utf-8"))
|
||||||
|
return names | set(lock.get("skills", {}))
|
||||||
|
|
||||||
|
|
||||||
|
def home_candidates(home: Path, skills_repo: Path) -> list[dict]:
|
||||||
|
known = shipped(skills_repo)
|
||||||
|
found = []
|
||||||
|
for sub, kind in HOME_SOURCES.items():
|
||||||
|
base = home / sub
|
||||||
|
if not base.is_dir():
|
||||||
|
continue
|
||||||
|
for entry in sorted(base.iterdir()):
|
||||||
|
if entry.name.startswith(".") or SECRET_NAME.search(entry.name):
|
||||||
|
continue
|
||||||
|
if kind == "skill" and (entry.name in known or not (entry / "SKILL.md").is_file()):
|
||||||
|
continue
|
||||||
|
if kind != "skill" and not entry.is_file():
|
||||||
|
continue
|
||||||
|
found.append({"source": "home", "owner": "operator", "kind": kind, "path": str(entry)})
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def load_state(path: Path) -> dict[str, dict]:
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
except ValueError:
|
||||||
|
fail(f"{path} is not valid JSON")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_scan(args: argparse.Namespace) -> int:
|
||||||
|
session = Path(args.session_dir).resolve()
|
||||||
|
home = Path(args.home)
|
||||||
|
names = session_names(home)
|
||||||
|
candidates = scratch_candidates(session, "own", "self")
|
||||||
|
for scratch in sorted(Path(args.peers_root).glob("*/*/scratchpad")):
|
||||||
|
if scratch.resolve() == session:
|
||||||
|
continue
|
||||||
|
sid = scratch.parent.name
|
||||||
|
candidates += scratch_candidates(scratch, "peer", names.get(sid, sid))
|
||||||
|
candidates += home_candidates(home, Path(args.skills_repo))
|
||||||
|
|
||||||
|
state = load_state(Path(args.state))
|
||||||
|
offered = []
|
||||||
|
for item in candidates:
|
||||||
|
seen = state.get(item["path"])
|
||||||
|
item["digest"] = digest(Path(item["path"]))
|
||||||
|
if seen and seen["verdict"] in SETTLED and seen["digest"] == item["digest"]:
|
||||||
|
continue
|
||||||
|
item["previous"] = seen["verdict"] if seen else None
|
||||||
|
offered.append(item)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(offered, indent=1))
|
||||||
|
else:
|
||||||
|
for item in offered:
|
||||||
|
note = f"\t(was {item['previous']})" if item["previous"] else ""
|
||||||
|
print(f"{item['source']}\t{item['owner']}\t{item['kind']}\t{item['path']}{note}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_mark(args: argparse.Namespace) -> int:
|
||||||
|
target = Path(args.path)
|
||||||
|
if not target.exists():
|
||||||
|
fail(f"{target} does not exist")
|
||||||
|
state_path = Path(args.state)
|
||||||
|
state = load_state(state_path)
|
||||||
|
state[str(target)] = {
|
||||||
|
"verdict": args.verdict,
|
||||||
|
"digest": digest(target),
|
||||||
|
"at": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"),
|
||||||
|
}
|
||||||
|
state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = state_path.with_suffix(".tmp")
|
||||||
|
tmp.write_text(json.dumps(state, indent=1, sort_keys=True), encoding="utf-8")
|
||||||
|
tmp.replace(state_path)
|
||||||
|
print(f"{args.verdict} {target}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
p = sub.add_parser("scan")
|
||||||
|
p.add_argument("--state", required=True)
|
||||||
|
p.add_argument("--skills-repo", required=True)
|
||||||
|
p.add_argument("--session-dir", required=True)
|
||||||
|
p.add_argument("--peers-root", required=True)
|
||||||
|
p.add_argument("--home", required=True)
|
||||||
|
p.add_argument("--json", action="store_true")
|
||||||
|
p.set_defaults(func=cmd_scan)
|
||||||
|
p = sub.add_parser("mark")
|
||||||
|
p.add_argument("--state", required=True)
|
||||||
|
p.add_argument("path")
|
||||||
|
p.add_argument("verdict", choices=VERDICTS)
|
||||||
|
p.set_defaults(func=cmd_mark)
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -10,7 +10,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
|
|||||||
SKILLS = REPO_ROOT / "skills"
|
SKILLS = REPO_ROOT / "skills"
|
||||||
README = REPO_ROOT / "README.md"
|
README = REPO_ROOT / "README.md"
|
||||||
CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "skills")
|
CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "skills")
|
||||||
UNCATEGORIZED = frozenset({"kvb-cleanup", "kvb-help"})
|
UNCATEGORIZED = frozenset({"kvb-cleanup", "kvb-help", "kvb-transcend"})
|
||||||
SPEC = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
SPEC = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||||
SCHEMA = re.compile(rf"^kvb-(?:{'|'.join(CATEGORIES)})-[a-z0-9]+(?:-[a-z0-9]+)*$")
|
SCHEMA = re.compile(rf"^kvb-(?:{'|'.join(CATEGORIES)})-[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||||
NAME_LINE = re.compile(r"^name:\s*(\S+)\s*$", re.MULTILINE)
|
NAME_LINE = re.compile(r"^name:\s*(\S+)\s*$", re.MULTILINE)
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"""Exercise transcend.py: sources, secret and size filters, and the decision memory."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPT = REPO_ROOT / "skills" / "kvb-transcend" / "scripts" / "transcend.py"
|
||||||
|
|
||||||
|
|
||||||
|
class TestTranscend(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
root = Path(self.tmp.name)
|
||||||
|
self.home = root / "home"
|
||||||
|
self.peers = root / "peers"
|
||||||
|
self.own = self.peers / "proj" / "s1" / "scratchpad"
|
||||||
|
self.peer = self.peers / "proj" / "s2" / "scratchpad"
|
||||||
|
self.repo = root / "skills-repo"
|
||||||
|
self.state = self.repo / ".state" / "transcend.json"
|
||||||
|
for path in (self.own, self.peer, self.repo / "skills" / "kvb-x"):
|
||||||
|
path.mkdir(parents=True)
|
||||||
|
(self.repo / "skills-lock.json").write_text(
|
||||||
|
json.dumps({"skills": {"caveman": {}}}), encoding="utf-8"
|
||||||
|
)
|
||||||
|
sessions = self.home / ".claude" / "sessions"
|
||||||
|
sessions.mkdir(parents=True)
|
||||||
|
(sessions / "s2.json").write_text(
|
||||||
|
json.dumps({"sessionId": "s2", "name": "Peer"}), encoding="utf-8"
|
||||||
|
)
|
||||||
|
self._write(self.own / "probe.py", "print(1)")
|
||||||
|
self._write(self.own / "test_probe.py", "assert True")
|
||||||
|
self._write(self.own / "notes.log", "noise")
|
||||||
|
self._write(self.own / "auth_token.py", "secret")
|
||||||
|
self._write(self.own / "big.py", "x" * 1_000_001)
|
||||||
|
self._write(self.peer / "helper.sh", "echo hi")
|
||||||
|
for name in ("kvb-x", "caveman", "custom"):
|
||||||
|
self._write(self.home / ".claude" / "skills" / name / "SKILL.md", name)
|
||||||
|
self._write(self.home / ".codex" / "rules" / "default.rules", "allow")
|
||||||
|
|
||||||
|
def _write(self, path: Path, text: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
|
||||||
|
def _run(self, *args: str) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, str(SCRIPT), *args], capture_output=True, text=True, check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def _scan(self) -> list[dict]:
|
||||||
|
result = self._run(
|
||||||
|
"scan",
|
||||||
|
"--state", str(self.state),
|
||||||
|
"--skills-repo", str(self.repo),
|
||||||
|
"--session-dir", str(self.own),
|
||||||
|
"--peers-root", str(self.peers),
|
||||||
|
"--home", str(self.home),
|
||||||
|
"--json",
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
def _found(self) -> dict[str, tuple[str, str, str]]:
|
||||||
|
return {
|
||||||
|
Path(i["path"]).name: (i["source"], i["owner"], i["kind"]) for i in self._scan()
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_scan_finds_own_peer_and_home_candidates(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self._found(),
|
||||||
|
{
|
||||||
|
"probe.py": ("own", "self", "script"),
|
||||||
|
"test_probe.py": ("own", "self", "test"),
|
||||||
|
"helper.sh": ("peer", "Peer", "script"),
|
||||||
|
"custom": ("home", "operator", "skill"),
|
||||||
|
"default.rules": ("home", "operator", "rule"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_settled_candidates_are_remembered_until_they_change(self):
|
||||||
|
probe = self.own / "probe.py"
|
||||||
|
self.assertEqual(self._run("mark", "--state", str(self.state), str(probe), "carried").returncode, 0)
|
||||||
|
self._run("mark", "--state", str(self.state), str(self.own / "test_probe.py"), "later")
|
||||||
|
found = {Path(i["path"]).name: i["previous"] for i in self._scan()}
|
||||||
|
self.assertNotIn("probe.py", found)
|
||||||
|
self.assertEqual(found["test_probe.py"], "later")
|
||||||
|
|
||||||
|
probe.write_text("print(2)", encoding="utf-8")
|
||||||
|
found = {Path(i["path"]).name: i["previous"] for i in self._scan()}
|
||||||
|
self.assertEqual(found["probe.py"], "carried")
|
||||||
|
|
||||||
|
def test_skill_directory_change_is_reoffered(self):
|
||||||
|
custom = self.home / ".claude" / "skills" / "custom"
|
||||||
|
self._run("mark", "--state", str(self.state), str(custom), "dropped")
|
||||||
|
self.assertNotIn("custom", self._found())
|
||||||
|
self._write(custom / "scripts" / "run.py", "print(3)")
|
||||||
|
self.assertIn("custom", self._found())
|
||||||
|
|
||||||
|
def test_invalid_input_exits_2(self):
|
||||||
|
self.assertEqual(self._run("mark", "--state", str(self.state), "/no/such/path", "carried").returncode, 2)
|
||||||
|
self.assertEqual(self._run("mark", "--state", str(self.state), str(self.own), "kept").returncode, 2)
|
||||||
|
self.assertEqual(self._run("scan", "--state", str(self.state)).returncode, 2)
|
||||||
|
self.assertFalse(self.state.exists())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user