| name | attractor-scout |
| version | 1.0.0 |
| description | Mine YOUR OWN local context-intelligence session history to find the attractor-shaped opportunities hiding in your real recurring work — the units you do again and again, that cost real effort, and that would survive being handed to a loop. Surfaces them ranked, with honest-NOs as first-class output, and writes a self-contained HTML opportunity map. Own data only; nothing leaves the machine. Triggers: "/attractor-scout", "what should I automate?", "find my attractor opportunities", "scout my sessions", "what do I keep doing by hand?", "mine my own work for pipelines".
|
| user-invocable | true |
| model_role | reasoning |
| allowed-tools | ["bash","read_file","write_file","delegate"] |
/attractor-scout — mine your own work for the loops worth building
This skill runs inline in the current session so it can read the repo's
AGENTS.md, resolve an output path, and shell out to the bundled mining
scripts. It reads your own context-intelligence session history — the
record of what you actually did, session after session — and finds the
recurring units of work that are shaped like an attractor: a loop, gated on
evidence, that would survive a bad day. Then it hands you a ranked map, and it
is just as willing to tell you which of your habits are not worth
automating and why.
What "attractor-shaped" means — the three-question test, in your terms.
The same test /attractorify applies to one piece of work, this skill
applies to every recurring unit in your history:
- Q1 — Is there a cycle? Does the work actually iterate (try, check,
fix, repeat), or does it run once straight through? Once-through is a
recipe, not an attractor.
- Q2 — Is the exit gated on evidence? Does it stop on a machine-checkable
condition — a test, a lint, a build, a readback — or does it stop when the
model feels done? An ungated loop is a one-shot: one gate away from
converting.
- Q3 — Would it survive one node having a bad day? When a step errored,
did the work recover and still finish? If errors were never even observed,
that is unproven — a caveat, never a failure.
A unit that clears all three, recurs at least twice, and cost you real toil
is an opportunity. Everything else that recurs and cost toil is an
honest-NO, reported with the sub-test it failed and what would change
the answer. The classification is the value.
Setup — do this first (defines every variable the commands use)
The pipeline shells out to the bundled scripts and writes intermediate JSON to
a scratch directory. Establish these three shell variables before running any
command below — every command references them, and none of them may be left
undefined:
SKILL_DIR="/absolute/path/to/skills/attractor-scout"
CLI="python $SKILL_DIR/scripts/attractor_scout_cli.py"
WORK="$(mktemp -d -t attractor-scout.XXXXXX)"
OUTPUT_PATH="$PWD/attractor-scout-report.html"
cat > "$WORK/env.sh" <<EOF
SKILL_DIR="$SKILL_DIR"; CLI="$CLI"; WORK="$WORK"; OUTPUT_PATH="$OUTPUT_PATH"
EOF
If you cannot resolve SKILL_DIR (no skill_directory from load_skill and you
are not inside the skill's own tree), STOP and say so — do not guess a path.
$WORK lives under mktemp, so its path is stable once created; re-source
$WORK/env.sh at the top of each step rather than re-running mktemp.
The pipeline (fixed order; do not improvise the stages)
The logic lives once, in the bundled library (scripts/attractor_scout/); the
CLI (scripts/attractor_scout_cli.py, invoked here as $CLI) is the
workhorse. You orchestrate — you do not re-implement a detector.
1 — Deterministic mining spine (NO LLM). Discover → qualify → extract, all
in the bundled scripts:
source "$WORK/env.sh"
$CLI extract --out "$WORK/extracts.jsonl"
This resolves the context-intelligence root
(AMPLIFIER_CONTEXT_INTELLIGENCE_BASE_PATH → ~/.amplifier/projects),
version-checks every metadata.json and fails loud on a schema mismatch,
selects prompt-carrying root sessions (never top-N-by-workspace-size — the
biggest recurring unit lives smeared across dozens of tiny workspaces and is
invisible to size-ranked selection), reads errors from tool:post.result.error,
keys on full session ids, and caps span at 7,200 s. It fails loud on an
empty root with the exact message looked in <root>, found 0 — if you see
that, STOP and report it; do not fabricate a count from a shallower glob.
The same pass classifies PROVENANCE — whose work each session is. A
prompt:submit proves a prompt was submitted and nothing whatsoever about who
submitted it: a harness firing one non-interactive run emits a byte-identical
event. So every mined session is put through a deterministic ladder
(scripts/attractor_scout/provenance.py) before anything downstream sees it:
| Rung | Verdict | What fires it |
|---|
R0 | agent | a parent session id, a delegate id shape, or a session:fork opener |
R1 | agent | a pipeline or recipe start event |
R2 | agent | an ephemeral or lane-shaped working_dir (tmp, scratch, worktree, lane) |
R3 | likely-agent | a one-shot templated long brief |
R4 | human-presumed | two or more prompts with a real gap between them |
R5 | unknown | nothing decisive was recorded |
Each verdict records which rung fired, on what signal, with its evidence,
and that record is rendered in the artifact.
The policy, and it is not negotiable inside a run: opportunities are mined
from R4 only. R0–R3 are excluded and counted as an already-automated
footprint (context — what agents already do for you — never an opportunity to
hand back). R5 is excluded and counted too: nothing recorded at session start
says who launched a run, so a single-prompt session in a normal workspace
genuinely cannot be told apart from an agent one-shot, and the contamination in
that band is exactly the complaint this pass exists to fix. UNKNOWN never
silently becomes human. Say the exclusion out loud when you report; do not
present a shrunken ranking as if nothing was held back.
2 — Semantic label + cluster (fast role). The deterministic spine dedups
by tool-signature, but the large majority of real opportunities are found ONLY
by reading the work's meaning across differently-worded sessions. Delegate
this to fast-role sub-agents over local text only, in batches of ~40
sessions, run in waves of 5 batches in series. Those two numbers are the
calibrated shape: ~40 keeps a batch inside one workspace's coherence so a
cluster is not split across unrelated work, and 5-at-a-time in series stays
under provider rate limits on a corpus of any size. The fast
role does this reliably at scale but not perfectly: a full-corpus run
measured it returning a small number of session ids that were in no supplied
batch. So the discipline is ENFORCED rather than assumed — every id a batch
returns is checked against the ids THAT batch was handed; one that is not is
dropped, counted, and reported as invented_ids_rejected in the run summary.
Write each batch's response next to the ids that batch was handed, and collect
them THROUGH that check into $WORK/fast-clusters.json (the verdict-carrying
$WORK/clusters.json is assembled in step 4):
{"batch_id": "b07", "session_ids": ["<sid>", "<sid>"],
"clusters": [{"id": "b07-c1", "name": "short label", "members": ["<sid>"]}]}
$CLI label-merge --batches "$WORK/label-batches" --out "$WORK/fast-clusters.json"
3 — Merge + fit verdicts (reasoning role). Merge the per-batch clusters
(staged: batch → regional → global) and assign each cluster its fit verdict
with a reasoning-role sub-agent. This tier is not optional and not cosmetic:
the verdict-tier A/B (Gate 1, evidence in evals/README.md) measured a ~18%
verdict-flip rate between the fast and reasoning tiers, 78% of them the
reasoning tier upgrading a verdict the fast tier mechanically declined. Fast
labels; reasoning judges.
4 — Author adjudication (general role). The deterministic spine already
carries an author prior (harness / human / mixed) from fingerprints,
sentinels, and the step-1 provenance verdict. That prior over-calls human,
because it cannot read intent from prompt text — a templated autonomous "lane"
mission looks human to it but is machine-launched. So adjudicate author at the
cluster level, reading the prompt text, with a general-role sub-agent
(the author-gate A/B, Gate 2 in evals/README.md, built this step: it admitted
0 of 2 harness clusters in 10/10 trials). The adjudicated label
overrides the prior.
Give the adjudicator the provenance verdict, and bind it with one rule.
Each member record carries provenance.rung, provenance.signal and
provenance.evidence (prompt count, session span, workspace class, first-prompt
shape). Pass those into the adjudication prompt alongside the text — they are
facts the prompt text cannot show, and withholding them is what made this step
guess. State this rule verbatim in the prompt you send:
You are given a deterministic provenance verdict per session. You may move a
cluster AWAY from human — to mixed or harness — when the prompt text shows
machine authorship the ladder could not see. You may NEVER move a cluster
toward human: a rung of R0, R1, R2 or R3 is evidence a machine started that
work, and no reading of prompt text overrides it. If the verdicts disagree
with each other inside one cluster, say mixed.
The adjudicator can tighten; it cannot launder an agent session into human
work.
After steps 2–4, write $WORK/clusters.json in exactly this shape — one entry
per global cluster, carrying the fast-tier members and the reasoning/general
verdicts. members are session ids from the extract; cycle and
evidence_gate are the reasoning-tier fit booleans; author is the
general-tier adjudicated label (human | mixed | harness):
{"clusters": [
{"id": "c1", "name": "short label", "members": ["<sid>", "<sid>"],
"cycle": true, "evidence_gate": true, "author": "human"}
]}
5 — Deterministic re-verification (trust, then verify — a FATAL gate).
Every count the LLM emitted — cluster membership above all — is handed BACK to
the deterministic layer and re-checked against the extract. This runs as part
of ranking, with --strict:
$CLI rank --strict \
--extracts "$WORK/extracts.jsonl" \
--clusters "$WORK/clusters.json" \
--out "$WORK/ranked.json"
--strict makes a re-verification mismatch FATAL: if any member id the LLM
emitted does not resolve against the extract, the command prints the offending
ids and exits non-zero. If that happens, STOP — do not render, do not
report a ranking. A ranking that rests on counts you could not verify is worse
than no ranking. Re-run the label/cluster pass or fix the cluster JSON first.
6 — Rank (inside the same rank call). score = n_sessions × leverage × fit / 100, where leverage = med_tool_calls + med_llm_cycles + med_span_capped/60 + 2·errs/n (median within cluster, never p75; n_prompts
carries zero signal and is dropped). Fit is binary {0,1}. Two gates run
BEFORE scoring, in this order. First the provenance boundary narrows every
unit's membership to its R4 sessions; a unit left with none is reported in
the provenance panel (as already-automated, or as unattributed) instead of
ranked. Then the author admission gate admits only human/mixed units;
harness ceremony is routed to the waste-findings channel — reported (it is time
you could reclaim), not offered as an opportunity to act on. A unit carrying no
author verdict at all is not treated as yours: it lands in unattributed,
reported and unranked, because an absent verdict is an absence of evidence.
7 — Render (NO LLM). The bundled deterministic renderer turns the ranked
JSON into ONE self-contained HTML file — a sampled simple→complex range across
the top, in-page modal deep-dives into the full list, honest-NOs shown with
verdict and remediation, waste-findings in their own channel, and a
provenance panel carrying the per-rung counts, sample evidence, the
already-automated footprint, and the honest UNKNOWN story:
$CLI render --ranked "$WORK/ranked.json" --out "$OUTPUT_PATH"
8 — Demonstrate (teach with their top opportunity). The map shows what
recurs; this step shows the pipeline that would have converged it. Run it for
opportunities[0] in $WORK/ranked.json — the ranking already made the pick, so
do not open with a menu. Skip this step entirely if the user asked for the map
only. If opportunities is empty there is no subject to demonstrate: write the
primer-only document and re-render, then say so plainly.
source "$WORK/env.sh"
UNIT=$(python -c 'import json,sys; print(json.load(open(sys.argv[1]))["opportunities"][0]["unit_id"])' \
"$WORK/ranked.json")
SLUG=$($CLI demo brief --ranked "$WORK/ranked.json" --unit "$UNIT" \
--extracts "$WORK/extracts.jsonl" --workdir "$WORK/demo")
demo brief writes $WORK/demo/$SLUG/brief.md — deterministically assembled
from their verified stats, their fit detail, the verify-class tools actually
seen in their own sessions' terminal windows, the A0–A10 authoring contract, and
the engine's attribute vocabulary. Delegate to a fresh-context reasoning
sub-agent whose instruction is exactly that file; it writes pipeline.dot,
pipeline.md and narrative.json into $WORK/demo/$SLUG/. Cost: one
delegation; at most two if the gates reject the first draft; never more.
Then gate, validate and publish:
$CLI demo assemble --ranked "$WORK/ranked.json" --unit "$UNIT" \
--workdir "$WORK/demo/$SLUG" --output-dir "$(dirname "$OUTPUT_PATH")" \
--out "$WORK/demos.json" --append
brief and assemble must both point at the SAME unit. --unit defaults
to opportunities[0] on each command independently, so an assemble that
omits it silently validates the draft against the top-ranked unit's numbers —
which is why $UNIT is pinned once above and passed to both. Mismatch it and
the count check fails on numbers the delegate never wrote.
assemble runs the verification ladder (attractor lint if it is on PATH; the
bundled doctrine checker always), validates every number in the six teaching-
prose slots against the re-verified ranking — an invented count there is
FATAL, same as step 5 — and copies the .dot + companion beside the HTML
only after the gates finish. (Numbers written inside the generated .dot
— budgets, max_iterations, thresholds — are gate-checked by lint+doctrine, not
digit-whitelisted: a pipeline legitimately carries parameters, and the panel's
"what nothing checked" names that surface out loud.)
If attractor is missing it will say so in the artifact rather than imply a
pass. You may then ask the user ONCE whether to fetch the public linter via
uvx — an inbound package fetch; none of their mined data leaves the machine;
never run it without their yes. On yes, re-run assemble with
--lint-cmd "uvx --from git+https://github.com/microsoft/amplifier-bundle-dot-runner@main#subdirectory=modules/pipeline-runner attractor".
If the gates reject the draft it exits non-zero and leaves the verbatim reports
at $WORK/demo/$SLUG/gate-report.txt: re-delegate ONCE with those reports
appended, and if it is still red, do not publish — say so and move on. Then
re-render the same file:
$CLI render --ranked "$WORK/ranked.json" --demos "$WORK/demos.json" --out "$OUTPUT_PATH"
Generation is stochastic; verification, assembly and rendering are not.
And: what you authored — via your delegate — you cannot certify. The
artifact carries the machine verdicts verbatim, says which checks did NOT run,
and offers the independent path (examples/authoring/pipeline-author.dot plus
the CLI install line). If the user asks you to vouch for the demo, answer in
those three parts — never "yes, I'm sure."
9 — Offer more (their call). List the top five not-yet-demonstrated
opportunities by name and ask exactly one question: "Want another one
demonstrated? Name or number — or no." Each yes repeats step 8 for that unit —
re-point $UNIT at the chosen unit_id so both demo brief and demo assemble carry the same --unit, keep --append, and re-render. Never
generate a second demonstration without a fresh explicit yes — the first one
is the skill's second half; every one after it is marginal spend for marginal
personalization.
10 — Deck mode (OPT-IN; ask once, and only after step 9). Steps 7–9 produce
a deterministic report: a renderer places every number, so no model can invent
one. Deck mode produces something different — ONE authored, deck-grade,
personalized page over the same verified data — and pays for that freedom with
machine gates instead of with a deterministic renderer. Offer it in one
question, naming the cost out loud:
"I can also build a deck-grade version of this — one self-contained page
that teaches the ideas and walks your own results, authored rather than
templated. It costs one more reasoning-model delegation (two if the gates
reject the first draft), and it only publishes if it passes them. Want it?"
Never build a deck without an explicit yes. On yes:
source "$WORK/env.sh"
RUNDIR="$(dirname "$OUTPUT_PATH")"
BRIEF=$($CLI deck brief --ranked "$WORK/ranked.json" --demos "$WORK/demos.json" \
--workdir "$WORK/deck")
deck brief writes $WORK/deck/deck-brief.md — deterministically assembled
from the same verified data the report rests on: the ranking, the honest-NOs,
the waste channel, every generated .dot verbatim with its gate verdicts, the
house style/technique contract, the hard self-containment constraints, and the
four MANDATES that make the gates passable. Delegate to a fresh-context
reasoning sub-agent whose instruction is exactly that file; it writes ONE
file, deck.html, into $WORK/deck/. Cost: one delegation; at most two if
the gates reject the first draft; never more.
Drive that delegation across resumed turns, not one giant request. A deck is
tens of thousands of tokens of markup, and a single request that tries to emit
the whole file in one response reliably exceeds the provider's ~600 s request
timeout and loses the work. Have the fresh-context author build the file up in
several turns — the document head/stylesheet/<defs> first, then one section
per turn, then the dialogs and the closing script — resuming the same session
each turn so it is still ONE fresh context. The brief tells the author this too;
it is stated here because the orchestrating session is what sequences the turns.
Then gate it:
$CLI deck verify --deck "$WORK/deck/deck.html" \
--ranked "$WORK/ranked.json" --demos "$WORK/demos.json" \
--report "$WORK/deck/deck-gate-report.txt"
deck verify is deterministic and it is the only thing that decides whether the
deck publishes. Six gates: (a) the HTML parses; (b) the page is
self-contained — no <img>/<link>/<script src>/@import/<iframe>/
srcset, url( only as url(#…), exactly two https links, zero file://;
(c) every modal has a trigger and every trigger has a modal; (d) every
number displayed in visible text re-verifies against the run data or against a
derivation the deck itself declares, with provenance, in its
<script type="application/json" id="derived-values"> block — an undeclared
number is FATAL, same as step 5; (e) every pipeline diagram matches its
real .dot node-for-node and edge-for-edge (an edge MULTISET comparison, so a
back-edge quietly not drawn is caught); (f) the structural depth gate —
every <dialog> carries the five parts the style contract mandates (a title and
kicker, at least two <h4> sub-sections, an evidence inset quoting the
reader's own verified data, a why, and an entry point). It counts structure,
never length, so padding cannot buy a pass and an honest short modal is never
punished. Exit 0 means all six passed; exit 3 means a gate came back red and
the deck must NOT be published.
If it exits 3, re-delegate ONCE with $WORK/deck/deck-gate-report.txt
appended verbatim, then re-verify. Still red? Do not publish. Say which
gates failed and move on — the report and the demonstrations are already
complete artifacts. On green, publish it beside the report:
cp "$WORK/deck/deck.html" "$RUNDIR/attractor-scout-deck.html"
Optional vision rung — only AFTER deck verify passes. If a
vision-analysis tool is available in this session, render the published deck to
images, stitch them, and inspect for layout defects (overlap, clipping,
unreadable contrast, a diagram running off its viewBox). At most one fix
round: hand the defects back to the same fresh-context author, re-run
deck verify (a visual fix must not break a gate), and republish. If no such
tool is available, do not imply it was checked — leave the honest label in
the deck's footer comment:
<!-- vision QA: NOT RUN -->
Same ladder philosophy as the demo lint rung: a rung that could not run is
labelled, never silently treated as a pass.
Hard rules (non-negotiable — these are the trust contract)
- Own data only. Nothing leaves the machine. The label/cluster/verdict/
author passes reason over local text; there is no network egress. Skip
any write-only or read-blocked endpoint; sessions whose metadata explicitly
labels them as originating from another source are out of scope. The
top-level
--root lever (it precedes the subcommand: $CLI --root <path> extract ...) exists only to point at your own context-intelligence tree —
never point it at another user's corpus or a shared mount; this skill
mines the caller's own history, nobody else's.
- Tier C (local JSONL) is the floor, and it is fully sufficient. Every
signal expresses at Tier C — this is the proven path. A personal graph
(Tier A/B) is an optional sharpener for counts and joins; it is never a
precondition. If no graph answers, run at Tier C and say so with an honest
one-line note. Never let the graph become required for a rung to appear.
- Whose work it is is PRESUMED, never asserted — and never presumed to be
yours. Nothing in the session record positively marks a human: there is no
argv, no mode, no tty flag, no launcher recorded at session start, so
human-presumed (R4) is the strongest claim the data supports and
unknown (R5) is a real answer, not a soft yes. Only R4 sessions feed
the ranking; R0–R3 and R5 are counted in the provenance panel and kept
out of it. A unit with no author verdict is unattributed, never human. When
you report, name what was held back — a smaller, honest map beats a fuller
one that mixes an agent's work in with the user's.
- Honest-NOs are first-class output, never dropped, never padded into the
opportunity list to lengthen it. Each carries its verdict, the sub-test it
failed (
recipe / one-shot / fragile), and its remediation.
- Provisional flags surface in the artifact verbatim. A unit seen in only
2–3 sessions is admitted but flagged
provisional. A PASS-provisional
recovery is labelled as such. And UNKNOWN never renders as FAIL — a unit
whose resilience was never stress-tested is OPPORTUNITY(unproven), a
caveat on an opportunity, not a decline. Most sessions never hit an error at
all; "no bad day observed" is not "would not survive a bad day."
- Fail loud on an empty root — exact string ,
non-zero exit. Never invent a count.
Output
One self-contained HTML opportunity map (inlined CSS/JS, no network
references), written to $OUTPUT_PATH. It carries the ranked opportunities,
the honest-NOs, the waste channel, and a provenance panel: how many of your
sessions landed on each rung, sample evidence for each, the already-automated
footprint, and the honest note about what could not be attributed and why. That is the current working directory
by default, or an AGENTS.md-guided output path if the repo declares one.
Never write the user's mined data into a shared repo — their session history
belongs in their own artifact.
Plus, when demonstrated: attractor-scout-demos/<slug>.dot + .md beside it.
The HTML embeds the pipeline text as well as naming that path, so the artifact
stays self-contained if the folder ever moves. The one hyperlink it carries is
the published explainer — an anchor a reader may follow, not a resource the
page loads.
Plus, when deck mode was accepted AND every gate passed:
attractor-scout-deck.html beside them — one authored, self-contained,
deck-grade page over the same verified data, carrying exactly two outbound
anchors (the explainer and the bundle repository) and loading nothing. A deck
that failed deck verify is never written there at all.
Notes
- The working name
attractor-scout lives in one place,
scripts/attractor_scout/naming.py (SKILL_NAME). Renaming the skill means
changing that constant, this directory, and the name: above — nothing else
hardcodes the string.
- Every quantitative claim above ("~18% flip", "78% upgrades", "0 of 2 in
10/10") is pinned to the in-repo evidence file
evals/README.md, and a
doc-guard test (tests/test_skill_doc_claims.py) fails if a pinned number in
this file drifts from that source. The deterministic acceptance gates are in
tests/ and run standalone with pytest tests.