| name | im-persona-forge |
| description | Distill how someone communicates in IM work chat into a ready-to-use persona skill — their expression style, per-recipient tone, and crucially their decision policy (what they answer, hand off, or never settle alone). Use to initialize a persona, pull IM history into a local corpus, rebuild and publish the persona skill, set the autonomy scope, check language or platform coverage, or diagnose why an inbound-message agent has gone quiet. |
im-persona-forge
A forge, not a persona. It ships with zero personal data, and it is
language-agnostic and platform-agnostic by construction. On the owner's own
machine, using their own credentials, it distills how they handle work chat
into two installed skills:
<slug>-persona — decides whether a message is theirs to answer, then
drafts or sends it in their voice. Discovery is the host's job: whatever
dispatches messages already knows which one is new.
The architecture in one rule
Skills are products. The corpus is the source.
IM platform ──pull──▶ local corpus (SQLite, full fidelity, 700/600)
│ │
source adapter ├── build ──▶ measured features
(dws / jsonl) │ ↑
│ locale pack (zh-CN / en / null)
└── publish ─▶ <slug>-persona
Delete an installed skill and publish reproduces it exactly. Every improvement
is made here and re-published — a loading agent never tunes the persona at
runtime. Hand-written corrections live in <!-- owner:begin --> blocks inside
the published files and survive every rebuild.
Three seams that keep it general
| Concern | Lives in | Rule |
|---|
| Language | forge/locales/*.json | No natural-language text in forge/*.py. Enforced by forge scan --scope repo. |
| Platform | forge/sources/*.py | Every read goes through MessageSource; an adapter declares what it cannot do. |
| Thresholds | signals.json → thresholds | One named place per cutoff, versioned into rulesVersion. |
rulesVersion is signals-v3+<pack>@<version>, so changing the locale pack
invalidates derived numbers exactly like changing a threshold does.
NULL_PACK is the important case. A corpus in a language nobody has written
a pack for still produces a valid skill: the structural layers (length, latency,
silence, reciprocity, opener distribution) are fully measured, every lexical
measurement honestly reports absent, and the published fidelity.md says so.
It never guesses, and it never lets "unmeasured" read as "they never do this".
The published skill must be executable by a weak model
A skill that is only a well-written document makes reply quality a property of
the model reading it, not of the forge. A weaker model given five reference
files will skip the live context, forget to scope a lookup to the recipient, read
a measured 92% reply-rate as permission to answer anything, and never think to ask
whether a fact is in the corpus at all.
None of those are language failures — they are orchestration failures, and
orchestration belongs in a script. So every decision that can be made
mechanically is the return value of a command, not a paragraph of advice:
| Decision | Command |
|---|
| What is this message answering? | brief.respondingTo |
| Which ask kind, which risk classes? | brief.classification (locale-pack regexes) |
| Reply, draft, hand off, or stay silent? | brief.verdict + because |
| Is this fact in the corpus at all? | facts → evidence / none |
| Does this draft match their habits? | check → pass / warn / block |
| May this be sent? | scope + allowlist + length + the draft's own risk classes |
references/rules.json is the machine-readable twin of decisions.md — the same
policy, one rendering for a reader (with rates, so a capable model can weigh
them) and one for a script (conclusions only). A self-test invariant asserts the
two agree exactly; two sources of truth for "may an agent answer this" would
drift silently.
The most consequential gate is in runtime.DwsClient.send: it now inspects the
outgoing text, not only the recipient, and fails closed when the patterns
are unavailable — missing or empty, since a pack that detects no risk classes
would otherwise let every draft pass a check that never ran. It runs before the
scope check, because a draft that states an approval is wrong in every scope —
reporting "sending is disabled" first would teach the caller that widening the
scope is the fix.
Commands
python3 -m forge doctor
python3 -m forge init --display-name "<real name>" --since 2026-01-28
python3 -m forge pull --since full
python3 -m forge build
python3 -m forge publish
python3 -m forge refresh
python3 -m forge locales
python3 -m forge sources
python3 -m forge report
python3 -m forge autonomy --scope allowlist --allow "<name>"
python3 -m forge lock
python3 -m forge export --out <file>.tar.gz
python3 -m forge inspect
python3 -m forge scan --scope repo
python3 -m forge selftest
Everything prints JSON. Anything touching a remote or writing accepts --dry-run.
Non-DingTalk platforms come in through the jsonl source — export your history,
convert it to the schema in forge/sources/jsonl.py, and the whole engine works
unchanged:
python3 -m forge init --source jsonl \
--source-option path=~/exports/slack \
--source-option 'identity={"openIds":["U0123456"],"name":"Real Name"}'
identity must be explicit. Every id in an export looks alike, and guessing the
owner wrong is the worst failure available — it attributes someone else's
messages to them and produces a confident, wrong persona.
Giving the skill to another agent
The point of lock is that "do not tune this at runtime" stops being a request
and becomes a property of the filesystem:
python3 -m forge lock
A loading agent gets PermissionError if it tries to edit SKILL.md or any
reference. Reads are unaffected, and forge publish still rebuilds them (it
writes a temp file and renames, then re-applies the lock), so updates keep
flowing from the forge only. --unlock reverses it.
Same machine, another agent → nothing else to do; the skill is already in
~/.claude/skills/ and ~/.codex/skills/.
Another machine → forge export. The bundle is Markdown + scripts only: no
corpus, no config, no logs, no absolute paths. recall / who / send report
degraded: markdown-only there, while the decision layer, style, people and
scenes work fully.
What gets distilled
| Layer | Published as | Source of truth |
|---|
| Decision policy | decisions.md | every incoming ask, answered and ignored |
| Expression style | style.md | their own messages, measured |
| Per-recipient tone | people.md | interaction volume + overrides |
| Situations | scenes.md | scene-tagged messages + real turns |
| Honest limits | limits.md | window, counts, what chat cannot show |
| Coverage | fidelity.md | which layers had evidence, and which did not |
The decision layer is what makes the persona usable unattended. Mining silence
alongside replies is what makes it possible: a question they consistently left
unanswered is evidence about what is not theirs to answer, and it exists nowhere
else.
fidelity.md is what makes it safe to hand on. It reports coverage, never
quality — and behavioral fidelity is deliberately not self-assessed: a persona
grading its own likeness produces a number that looks like evidence and is not
one. forge report --rubric emits a blind two-agent protocol instead, whose
result the owner pastes into an owner block. The rubric contains the answer key
and stays in the data root, never in a published skill.
Boundaries
- The corpus keeps everything locally — every message, real names, real ids —
because an agent cannot judge "is this mine to answer" from hashes. It lives at
600 inside a 700 data root, is never shared, and is
.gitignored. Credentials
are the one thing scrubbed on write.
- Published skills carry no raw ids, no paths, no credentials.
forge scan --scope skill enforces this and runs automatically on publish, checking every
known platform's id shape rather than one vendor's.
- Default autonomy is
draft_only. Scope only widens who may receive a
reply, never what may be said. The risk gate (commitments, approvals, money,
scheduling, personnel, external positions, org decisions, destructive actions)
holds in every scope — and a risk class the active locale cannot even detect is
treated as never-settle, not as absent.
- Never commit a corpus, config, ledger, rubric, or published persona.
Reading order for an operating agent
README.md — the full pipeline, the failure modes it was built against.
forge/locale.py — how a language pack is chosen, and what NULL_PACK
guarantees when none fits.
forge/sources/__init__.py — the platform contract and the normalized
message schema.
forge/signals.json — every language-independent threshold; changing one
changes rulesVersion and therefore every derived number.
forge/decide.py — how the decision policy is mined.
forge/compose.py — how measurements become skill text, and how owner blocks
are preserved.