demiurge-chiron 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +34 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/chiron.js +241 -0
- package/docs/HONEST-NUMBERS.md +25 -0
- package/hooks/chiron-hook.js +82 -0
- package/lib/archive.js +61 -0
- package/lib/compiler.js +117 -0
- package/lib/governance.js +129 -0
- package/lib/ledger.js +156 -0
- package/lib/mine.js +122 -0
- package/lib/siblings.js +36 -0
- package/package.json +16 -0
- package/skill/SKILL.md +66 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# CHIRON companion guide (for AI agents helping a human set up or use CHIRON)
|
|
2
|
+
|
|
3
|
+
You are reading this because a human opened their agent in the CHIRON repo and asked for help. Walk them through ONE step at a time. Explain before doing. Never pressure an optional step.
|
|
4
|
+
|
|
5
|
+
## What CHIRON is (tell them in one line)
|
|
6
|
+
|
|
7
|
+
Every correction they make to an agent becomes a permanent rule, compiled into every agent's memory file, so the same mistake never happens twice.
|
|
8
|
+
|
|
9
|
+
## Setup, conversationally
|
|
10
|
+
|
|
11
|
+
1. **Check state first:** run `chiron setup` in their project and read the output to them in plain language. It detects what is done and what is missing and changes nothing.
|
|
12
|
+
2. **If no ledger:** explain that the ledger (`.chiron/ledger.md`) is where rules live, git-diffable. Then run `chiron init`.
|
|
13
|
+
3. **First rule:** ask if there is a correction they keep repeating to their agents. Distill it with them into Mistake / Rule / Apply (one sentence each), then run `chiron add`. If the CLI reports a duplicate or contradiction, show them what it said; the gates exist to keep their rule set clean.
|
|
14
|
+
4. **Compile:** explain that rules only work when agents read them at session start. Show the dry-run first (`chiron compile`), let them see the plan, then `chiron compile --apply`. Their own file content outside the managed markers is never touched.
|
|
15
|
+
5. **Optional, live hook:** explain honestly: it reads each prompt locally and adds one line of context nudging the agent to capture a rule in two cases, when the prompt looks like a correction, or when HORKOS (if installed) caught a false-completion or evidence gap this session (its `caught` counter grew). It fires once per new catch, never blocks, and sends nothing anywhere. If they want it, add to `~/.claude/settings.json` under `hooks.UserPromptSubmit`: `node <repo>/hooks/chiron-hook.js`. If they decline, they lose nothing except automation; `/chiron capture` and `chiron mine` still work.
|
|
16
|
+
6. **Optional, mine history:** `chiron mine` sweeps their past transcripts read-only for corrections they already paid for and never captured. Offer to review the candidates with them (skill Mode B). Expect false positives; discard freely.
|
|
17
|
+
7. **Skill install (if not done):** copy `skill/` to `~/.claude/skills/chiron` so capture works conversationally.
|
|
18
|
+
|
|
19
|
+
## Story, not setup
|
|
20
|
+
|
|
21
|
+
The author wires CHIRON into a broader discipline stack (HORKOS audits actions, HYPNOS consolidates memory). That is how WE use it; none of it is required. CHIRON alone is complete.
|
|
22
|
+
|
|
23
|
+
## Day-to-day use (tell them once)
|
|
24
|
+
|
|
25
|
+
- They correct their agent -> agent offers to capture (hook) or they say "capture that" (skill).
|
|
26
|
+
- `chiron check` any time: health, duplicates, contradictions, drift.
|
|
27
|
+
- `chiron promote` when a rule proves itself across projects.
|
|
28
|
+
- Retire with `chiron archive <id>`; nothing is ever deleted, `chiron restore` brings rules back.
|
|
29
|
+
|
|
30
|
+
<!-- chiron:begin (managed by CHIRON, do not edit inside) -->
|
|
31
|
+
## Rules learned from corrections (CHIRON)
|
|
32
|
+
|
|
33
|
+
- **CHI-R001:** When writing a banned-character lint, spell the banned characters as backslash-u escape sequences in the lint source so it never matches itself
|
|
34
|
+
<!-- chiron:end -->
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lee Jun Ying
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
# CHIRON: Corrections Harvested Into Rules, Obeyed Next-session
|
|
6
|
+
|
|
7
|
+
*You corrected your AI agent Tuesday. It forgot by Thursday. I make the correction stick, in Claude Code, Cursor, and every agent at once.*
|
|
8
|
+
|
|
9
|
+
**The self-improving loop for AI agents. Correct once, never twice.**
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
**I am Chiron. I trained Achilles.** Also Asclepius, who became medicine, and Jason, who sailed for the Fleece. I never once swung a sword for them. My work was quieter and it lasted longer: a hero learns a thing wrong, I correct it, and the hero never gets it wrong again. That is the whole art of teaching, and it is exactly what your agent lacks. Your agent is bright and it is tireless and it has no memory of yesterday's lesson. So you teach it the same thing Tuesday, Thursday, and again next month in a different editor. I am here to end that. Correct me once. I will not make you do it twice.
|
|
20
|
+
|
|
21
|
+
**One correction becomes a permanent rule, compiled into every agent's memory.** Deterministic gates, zero LLM in the capture path, 35 benchmarks you can rerun in seconds.
|
|
22
|
+
|
|
23
|
+
## Before / after
|
|
24
|
+
|
|
25
|
+
**Without CHIRON** (every AI agent user, every week):
|
|
26
|
+
|
|
27
|
+
> Tuesday: "no, use `py` not `python` on Windows." Agent: "You're right, my mistake."
|
|
28
|
+
> Thursday, new session: agent runs `python`. It has no idea Tuesday happened.
|
|
29
|
+
> Next month, in Cursor: same mistake. Different agent, same you, same correction, third time.
|
|
30
|
+
|
|
31
|
+
**With CHIRON:**
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
> chiron add --mistake "ran python on Windows, hit the Store stub" \
|
|
35
|
+
--rule "Use py, not python or python3, on Windows" \
|
|
36
|
+
--how "py -c ... for inline, py script.py for files"
|
|
37
|
+
Captured CHI-R007. Compile it into your agents: chiron compile --apply
|
|
38
|
+
|
|
39
|
+
> chiron compile --apply
|
|
40
|
+
WROTE claude: CLAUDE.md
|
|
41
|
+
WROTE agentsmd: AGENTS.md
|
|
42
|
+
WROTE cursor: .cursor/rules/chiron.mdc
|
|
43
|
+
WROTE windsurf: .windsurf/rules/chiron.md
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
One correction. Four agents. Every future session. The ledger is git-diffable markdown; run `git diff` and watch the rule land everywhere at once.
|
|
47
|
+
|
|
48
|
+
## Why the incumbents miss it
|
|
49
|
+
|
|
50
|
+
| | Claude auto-memory | Cursor Rules / Windsurf Memories | CHIRON |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| Captures corrections | automatically, invisibly, unreviewed | manually, per editor | as three-line rules you approve, gated by dedup and contradiction checks |
|
|
53
|
+
| Reach | Claude only | that one editor only | compiled into Claude Code, Cursor, Windsurf, and AGENTS.md at once |
|
|
54
|
+
| Your past sessions | forgotten | forgotten | mined: `chiron mine` recovers corrections you already paid for |
|
|
55
|
+
|
|
56
|
+
A lesson that lives in one editor is a lesson you get to teach again in the next one. I do not train a hero to fight only on Tuesdays.
|
|
57
|
+
|
|
58
|
+
## Not for you if
|
|
59
|
+
|
|
60
|
+
- You work in exactly one agent and its native rules file is enough. CHIRON earns its keep at two agents and up.
|
|
61
|
+
- You want fully automatic memory with no review step. Claude auto-memory does that; you just never get to see what it learned.
|
|
62
|
+
- You expect semantic "these two rules mean the same thing" merging. CHIRON's gates are deterministic on purpose; the judgment calls stay yours.
|
|
63
|
+
|
|
64
|
+
## How it works
|
|
65
|
+
|
|
66
|
+
1. **Capture.** A correction becomes a three-line rule (Mistake / Rule / Apply), but only once its cause is verified, never from a first-guess diagnosis, because a rule outlives the mistake that spawned it; a repeated lesson bumps a counter instead of duplicating. Attach a multi-line `--detail` note for the full technical story, and tag a discovery you hit yourself (not a user correction) with `--type gotcha`. A failure-class tag in `--source` (scope, evidence, adversarial, verify, report) turns the ledger into a defect map: the class that accumulates the most rules is the discipline to harden next.
|
|
67
|
+
2. **Compile.** Active corrections land inside managed markers in CLAUDE.md, AGENTS.md, `.cursor/rules/*.mdc`, and Windsurf rules. Your own content is never touched; dry-run by default. `gotcha` entries stay in the ledger as a searchable, deduped, git-diffable technical journal (the role a hand-kept lessons file used to fill) and never bloat the agent files.
|
|
68
|
+
3. **Mine.** `chiron mine` sweeps your past Claude Code and Codex transcripts for corrections you never captured. Read-only, zero LLM.
|
|
69
|
+
4. **Govern.** Duplicates surfaced, contradictions ASKED (never auto-resolved), cross-project rules proposed for your global ledger, health score 0 to 100.
|
|
70
|
+
5. **Archive, never delete.** Retired rules get a dated archive and an append-only changelog; `chiron restore` brings any rule back, forever.
|
|
71
|
+
|
|
72
|
+
Optional live hook (ships dormant): a one-line nudge to capture a rule the moment you type a correction, or the moment HORKOS (if installed) catches a false-completion this session.
|
|
73
|
+
|
|
74
|
+
## Install for your agent
|
|
75
|
+
|
|
76
|
+
> **From npm:** `npm install -g demiurge-chiron`, then `chiron init` and `chiron compile --apply` — or `npx demiurge-chiron init` with no install. (The capture skill still ships in the repo; clone to copy it.) Source build below.
|
|
77
|
+
|
|
78
|
+
CHIRON compiles rules into the files each agent already reads at session start. Install the CLI once, then compile wherever you work.
|
|
79
|
+
|
|
80
|
+
**Windows PowerShell:**
|
|
81
|
+
```powershell
|
|
82
|
+
git clone https://github.com/eragonlonelyboy-lab/chiron.git; cd chiron; npm link
|
|
83
|
+
Copy-Item -Recurse skill "$env:USERPROFILE\.claude\skills\chiron"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**macOS / Linux:**
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/eragonlonelyboy-lab/chiron.git && cd chiron && npm link
|
|
89
|
+
cp -r skill ~/.claude/skills/chiron
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Node 18+, zero dependencies. Then in any project: `chiron init` creates the ledger, `chiron compile --apply` writes the rules into your agents.
|
|
93
|
+
|
|
94
|
+
**Where the rules land, honestly.** `chiron compile` writes to the instruction files each agent reads natively:
|
|
95
|
+
|
|
96
|
+
| Agent | File CHIRON writes | Note |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Claude Code | `CLAUDE.md` | managed markers, your content untouched |
|
|
99
|
+
| Codex, Copilot, OpenCode, Amp, Devin, Cursor, Kiro, Cline, and ~7 more | `AGENTS.md` | read natively by ~15 agents; one file, most reach |
|
|
100
|
+
| Cursor | `.cursor/rules/*.mdc` | `.mdc` frontmatter emitted correctly |
|
|
101
|
+
| Windsurf | `.windsurf/rules/` | respects the 12k workspace / 6k global caps; fails loud if a rule set overflows |
|
|
102
|
+
| Cline | `.clinerules/` | rules file, cross-platform |
|
|
103
|
+
| Kiro | `.kiro/steering/` | steering doc |
|
|
104
|
+
|
|
105
|
+
The root `AGENTS.md` anchor is the big lever: one file, read natively by about fifteen agents (add a one-line `@AGENTS.md` import for Claude Code, a `context.fileName` line for Gemini CLI, and the reach widens further). The **capture skill** (`/chiron capture`, "capture that") runs on skill-capable hosts: Claude Code, Codex, Cursor, OpenCode, Kiro, Cline, and the rest of the skill roster. Everywhere else, the CLI captures and compiles just the same.
|
|
106
|
+
|
|
107
|
+
## Benchmarks
|
|
108
|
+
|
|
109
|
+
Reproducible, seeded, no network: `node benchmarks/run.js`
|
|
110
|
+
|
|
111
|
+
| Suite | Result |
|
|
112
|
+
|---|---|
|
|
113
|
+
| Ledger round-trip, dedup, contradiction (zero auto-fix asserted) | pass |
|
|
114
|
+
| Compiler: surgical markers, idempotent, .mdc frontmatter, Windsurf cap fails loud | pass |
|
|
115
|
+
| Mining: planted-correction detection | 6/6 detected, 0 noise hits |
|
|
116
|
+
| Promote-to-global: cross-project proposed, single-project not, idempotent | pass |
|
|
117
|
+
| Archive/restore round-trip + changelog | pass |
|
|
118
|
+
| Hook: nudge on correction, silent on normal prompts, survives garbage stdin | pass |
|
|
119
|
+
| **Total** | **35/35** |
|
|
120
|
+
|
|
121
|
+
Honest limits, measured and admitted: [docs/HONEST-NUMBERS.md](docs/HONEST-NUMBERS.md). And do not take my word for the table: `npm test` reruns all 35 checks on your machine in seconds. A teacher who cannot show the work is just a man with opinions.
|
|
122
|
+
|
|
123
|
+
## CLI
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
chiron init # create the ledger (.chiron/ledger.md)
|
|
127
|
+
chiron add --mistake .. --rule .. --how .. # capture one correction as a rule
|
|
128
|
+
chiron compile # dry-run: show what would be written where
|
|
129
|
+
chiron compile --apply # write rules into every agent's memory file
|
|
130
|
+
chiron mine # sweep past transcripts for uncaptured corrections
|
|
131
|
+
chiron check # health score, duplicates, contradictions, drift
|
|
132
|
+
chiron promote <id> # raise a rule that proved itself to your global ledger
|
|
133
|
+
chiron archive <id> # retire a rule (restore brings it back, forever)
|
|
134
|
+
chiron setup # state-aware guided walkthrough (changes nothing)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Zero config works: `init`, `add`, `compile` need nothing else. Everything that writes defaults to dry-run; `--apply` is always explicit.
|
|
138
|
+
|
|
139
|
+
## FAQ
|
|
140
|
+
|
|
141
|
+
**Will you scold my agent every time it slips?**
|
|
142
|
+
I have never once scolded a student, and I trained the ones the poets remember. Scolding teaches nothing. A rule teaches. You give me the correction; I turn it into a line every future session reads before it starts.
|
|
143
|
+
|
|
144
|
+
**I have made the same correction for a year. Is it too late?**
|
|
145
|
+
It is never too late to stop repeating yourself. Run `chiron mine`; it walks back through your old transcripts and hands you the corrections you already paid for, still uncaptured. A year of tuition, refunded as rules.
|
|
146
|
+
|
|
147
|
+
**Do you read my code with some AI, or send it somewhere?**
|
|
148
|
+
No. The capture path is deterministic: no model, no network in the gates that dedupe and check contradictions. A lesson you cannot inspect is not a lesson, it is a rumor. Your rules stay in git-diffable markdown you can read.
|
|
149
|
+
|
|
150
|
+
**What if two of my rules disagree?**
|
|
151
|
+
Then I stop and ask you, and I do not guess. I trained a healer and a warrior from the same cave; I know two right answers can look like a contradiction. The judgment stays yours. I only make sure you never lose the question.
|
|
152
|
+
|
|
153
|
+
**You wrote a rule I do not want anymore.**
|
|
154
|
+
Archive it. `chiron archive <id>` retires it with a dated record and an append-only changelog, and it is never deleted. Change your mind next spring? `chiron restore` brings it back. I keep every lesson a hero ever outgrew.
|
|
155
|
+
|
|
156
|
+
## From the same forge
|
|
157
|
+
|
|
158
|
+
CHIRON is a [Demiurge](https://github.com/eragonlonelyboy-lab/demiurge) product. Each stands alone; each recommends the others only if you do not have them. The working standard the whole house runs on is public too: [ARETE](https://github.com/eragonlonelyboy-lab/arete), five discipline gates any model can run; CHIRON is its correction-to-permanent-rule loop shipped as a product, and `arete panel` reads this ledger's failure-class tags to name the gate that leaks most.
|
|
159
|
+
|
|
160
|
+
| Product | Job |
|
|
161
|
+
|---|---|
|
|
162
|
+
| **HORKOS** | Evidence-audit loop: no receipts, no "done" |
|
|
163
|
+
| **VERITAS** | Slop-free prose that audits its own output |
|
|
164
|
+
| **MONETA** | Token discipline with honest accounting |
|
|
165
|
+
| **HYPNOS** | Memory consolidation in your agents' sleep: every change a diff, nothing deleted |
|
|
166
|
+
| **ATHENA** | Decision trials with verdicts on the record |
|
|
167
|
+
| **CALLIOPE** | A full design agency in the terminal, gated by a QA lead who does not accept "looks fine" |
|
|
168
|
+
| **MAAT** | Multi-agent attention terminal: receipts across every session |
|
|
169
|
+
| **ZOILUS** | The merciless critic: a rejection-class it names once becomes a CHIRON rule |
|
|
170
|
+
| **PEITHO** | Go-to-market: positioning, angles and offers that refuse to sound generic |
|
|
171
|
+
| **PYRRHO** | The skeptic: suspends judgment until the data earns it |
|
|
172
|
+
|
|
173
|
+
**Pair CHIRON with [HORKOS](../horkos).** HORKOS catches the failure this time; CHIRON makes sure there is no next time. He audits the action NOW; I learn from the correction FOREVER. The mistake he catches once, I make sure never happens again.
|
|
174
|
+
|
|
175
|
+
## The fair trade
|
|
176
|
+
|
|
177
|
+
If CHIRON saves you from typing the same correction one more time, the star costs you nothing. ⭐
|
|
178
|
+
|
|
179
|
+
[](https://star-history.com/#eragonlonelyboy-lab/chiron&Date)
|
|
180
|
+
|
|
181
|
+
MIT: see [LICENSE](LICENSE). Free, the way good teaching should be.
|
package/bin/chiron.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CHIRON: Corrections Harvested Into Rules, Obeyed Next-session.
|
|
3
|
+
// The centaur who trained the heroes now trains your agents.
|
|
4
|
+
'use strict';
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const ledger = require('../lib/ledger');
|
|
8
|
+
const governance = require('../lib/governance');
|
|
9
|
+
const compiler = require('../lib/compiler');
|
|
10
|
+
const archive = require('../lib/archive');
|
|
11
|
+
const miner = require('../lib/mine');
|
|
12
|
+
const siblings = require('../lib/siblings');
|
|
13
|
+
|
|
14
|
+
const argv = process.argv.slice(2);
|
|
15
|
+
const cmd = argv[0];
|
|
16
|
+
const flags = new Set(argv.filter(a => a.startsWith('--')));
|
|
17
|
+
const root = process.cwd();
|
|
18
|
+
const APPLY = flags.has('--apply');
|
|
19
|
+
|
|
20
|
+
function out(s) { process.stdout.write(s + '\n'); }
|
|
21
|
+
function json(o) { out(JSON.stringify(o, null, 2)); }
|
|
22
|
+
function arg(name, fallback) {
|
|
23
|
+
const i = argv.indexOf(name);
|
|
24
|
+
return i !== -1 && argv[i + 1] ? argv[i + 1] : fallback;
|
|
25
|
+
}
|
|
26
|
+
function ledgerPathFor() {
|
|
27
|
+
return flags.has('--global') ? ledger.globalLedgerPath() : ledger.projectLedgerPath(root);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const commands = {
|
|
31
|
+
init() {
|
|
32
|
+
const p = ledger.projectLedgerPath(root);
|
|
33
|
+
if (fs.existsSync(p)) { out(`ledger already exists: ${p}`); return; }
|
|
34
|
+
ledger.save(p, []);
|
|
35
|
+
ledger.registerProject(root);
|
|
36
|
+
out(`CHIRON ledger created: ${p}`);
|
|
37
|
+
out('Next: capture your first rule with "chiron add", or sweep history with "chiron mine".');
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Deterministic add (the skill calls this after distilling a correction).
|
|
41
|
+
add() {
|
|
42
|
+
const p = ledgerPathFor();
|
|
43
|
+
const rules = ledger.load(p);
|
|
44
|
+
const rule = {
|
|
45
|
+
id: ledger.nextId(rules),
|
|
46
|
+
status: 'active',
|
|
47
|
+
created: new Date().toISOString().slice(0, 10),
|
|
48
|
+
mistake: arg('--mistake', ''),
|
|
49
|
+
rule: arg('--rule', ''),
|
|
50
|
+
apply: arg('--how', ''),
|
|
51
|
+
detail: arg('--detail', ''),
|
|
52
|
+
type: ['gotcha', 'dissent'].includes(arg('--type', 'correction')) ? arg('--type', 'correction') : 'correction',
|
|
53
|
+
source: arg('--source', 'manual'),
|
|
54
|
+
projects: [path.basename(root)],
|
|
55
|
+
occurrences: 1,
|
|
56
|
+
updated: new Date().toISOString().slice(0, 10),
|
|
57
|
+
};
|
|
58
|
+
if (!rule.rule) { out('ERROR: --rule is required (the durable rule text).'); process.exit(1); }
|
|
59
|
+
// dedup gate: refuse silently duplicating; surface the match instead
|
|
60
|
+
const dupe = rules.find(r => r.status === 'active' && ledger.isDuplicate(r, rule));
|
|
61
|
+
if (dupe && !flags.has('--force')) {
|
|
62
|
+
out(`DUPLICATE of ${dupe.id}: "${dupe.rule}"`);
|
|
63
|
+
out(`If it truly is the same lesson, bump it instead: chiron bump ${dupe.id}. Use --force to add anyway.`);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
const contra = governance.findContradictions([...rules, rule]).filter(c => c.a === rule.id || c.b === rule.id);
|
|
67
|
+
if (contra.length && !flags.has('--force')) {
|
|
68
|
+
out(`CONTRADICTION with ${contra.map(c => c.a === rule.id ? c.b : c.a).join(', ')}. CHIRON never picks a winner.`);
|
|
69
|
+
out('Reconcile the rules, archive the outdated one, or re-run with --force if they truly coexist.');
|
|
70
|
+
process.exit(3);
|
|
71
|
+
}
|
|
72
|
+
rules.push(rule);
|
|
73
|
+
ledger.save(p, rules);
|
|
74
|
+
ledger.registerProject(root);
|
|
75
|
+
archive.logChange(path.dirname(p), `added ${rule.id}: ${rule.rule}`);
|
|
76
|
+
out(`Captured ${rule.id}. Compile it into your agents: chiron compile --apply`);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
bump() {
|
|
80
|
+
const id = argv[1];
|
|
81
|
+
const p = ledgerPathFor();
|
|
82
|
+
const rules = ledger.load(p);
|
|
83
|
+
const r = rules.find(x => x.id === id);
|
|
84
|
+
if (!r) { out(`rule ${id} not found`); process.exit(1); }
|
|
85
|
+
r.occurrences++;
|
|
86
|
+
r.updated = new Date().toISOString().slice(0, 10);
|
|
87
|
+
const proj = path.basename(root);
|
|
88
|
+
if (!r.projects.includes(proj)) r.projects.push(proj);
|
|
89
|
+
ledger.save(p, rules);
|
|
90
|
+
out(`${id} occurrences -> ${r.occurrences}`);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
list() {
|
|
94
|
+
const rules = ledger.load(ledgerPathFor());
|
|
95
|
+
if (!rules.length) { out('ledger empty. capture with "chiron add" or sweep history with "chiron mine".'); return; }
|
|
96
|
+
for (const r of rules) out(`${r.id} [${r.status}${r.type === 'gotcha' ? '/gotcha' : ''}] (${r.occurrences}x)${r.detail ? ' +detail' : ''} ${r.rule}`);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
compile() {
|
|
100
|
+
const rules = ledger.load(ledger.projectLedgerPath(root));
|
|
101
|
+
const globalRules = ledger.load(ledger.globalLedgerPath());
|
|
102
|
+
const all = [...globalRules, ...rules];
|
|
103
|
+
const plan = compiler.compile(root, all, { apply: APPLY });
|
|
104
|
+
for (const p of plan) {
|
|
105
|
+
if (p.error) out(`FAIL ${p.target}: ${p.error}`);
|
|
106
|
+
else out(`${APPLY && p.changed ? 'WROTE' : p.changed ? 'WOULD WRITE (dry-run)' : 'up-to-date'} ${p.target}: ${p.file}`);
|
|
107
|
+
}
|
|
108
|
+
if (!APPLY && plan.some(p => p.changed)) out('\nDry-run. Re-run with --apply to write.');
|
|
109
|
+
if (APPLY) archive.logChange(ledger.projectDir(root), `compiled ${all.filter(r => r.status === 'active').length} rules -> ${plan.filter(p => p.changed).map(p => p.target).join(', ') || 'no changes'}`);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
mine() {
|
|
113
|
+
const storesArg = arg('--store');
|
|
114
|
+
const res = miner.mine({
|
|
115
|
+
stores: storesArg ? [storesArg] : undefined,
|
|
116
|
+
limit: parseInt(arg('--limit', '40'), 10),
|
|
117
|
+
});
|
|
118
|
+
out(`Scanned ${res.filesScanned} transcripts across ${res.stores.length} store(s). Read-only, nothing written.`);
|
|
119
|
+
if (!res.candidates.length) { out('No correction candidates found.'); return; }
|
|
120
|
+
out(`\n${res.candidates.length} candidate correction(s). These are heuristic hits, NOT rules; distill the real ones with /chiron capture:\n`);
|
|
121
|
+
for (const c of res.candidates) {
|
|
122
|
+
out(`[${c.confidence}] (${c.tag}) ${c.excerpt}`);
|
|
123
|
+
out(` at ${c.file}:${c.line}\n`);
|
|
124
|
+
}
|
|
125
|
+
const outFile = arg('--out');
|
|
126
|
+
if (outFile) { fs.writeFileSync(outFile, JSON.stringify(res, null, 2), 'utf8'); out(`Saved: ${outFile}`); }
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
check() {
|
|
130
|
+
const rules = ledger.load(ledgerPathFor());
|
|
131
|
+
const drift = compiler.verify(root, [...ledger.load(ledger.globalLedgerPath()), ...ledger.load(ledger.projectLedgerPath(root))]);
|
|
132
|
+
const h = governance.health(rules, { compileDrift: drift.length > 0 });
|
|
133
|
+
out(`CHIRON health: ${h.score}/100 (${h.active} active rules)`);
|
|
134
|
+
if (h.duplicates.length) out(`duplicates: ${h.duplicates.map(d => `${d.a}~${d.b}(${d.similarity})`).join(', ')}`);
|
|
135
|
+
if (h.contradictions.length) for (const c of h.contradictions) out(`CONTRADICTION ${c.a} vs ${c.b}: ${c.note}`);
|
|
136
|
+
if (h.stale.length) out(`stale (>180d): ${h.stale.join(', ')}`);
|
|
137
|
+
if (h.compileDrift) out(`compile drift: targets out of date -> chiron compile --apply (${drift.join(', ')})`);
|
|
138
|
+
if (flags.has('--json')) json(h);
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
promote() {
|
|
142
|
+
const candidates = governance.findPromotions(parseInt(arg('--min', '2'), 10));
|
|
143
|
+
if (!candidates.length) { out('No promotion candidates (no rule appears in 2+ project ledgers yet).'); return; }
|
|
144
|
+
for (const c of candidates) {
|
|
145
|
+
out(`PROPOSE global: "${c.rule.rule}" (seen in: ${c.projects.join(', ')})`);
|
|
146
|
+
if (APPLY) {
|
|
147
|
+
const promoted = governance.applyPromotion(c);
|
|
148
|
+
archive.logChange(ledger.globalDir(), `promoted ${promoted.id} from ${c.projects.join(', ')}`);
|
|
149
|
+
out(` -> promoted as ${promoted.id} in the global ledger`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!APPLY) out('\nDry-run. Re-run with --apply to promote.');
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
archive() {
|
|
156
|
+
const id = argv[1];
|
|
157
|
+
if (!id) { out('usage: chiron archive <rule-id> [--reason "..."] '); process.exit(1); }
|
|
158
|
+
const res = archive.archiveRule(ledgerPathFor(), id, arg('--reason', 'user request'));
|
|
159
|
+
out(res.ok ? `archived ${id} -> ${res.archive} (restore any time: chiron restore ${id})` : `ERROR: ${res.error}`);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
restore() {
|
|
163
|
+
const q = argv.slice(1).filter(a => !a.startsWith('--')).join(' ');
|
|
164
|
+
if (!q) { out('usage: chiron restore <rule-id or search text>'); process.exit(1); }
|
|
165
|
+
const res = archive.restoreRule(ledgerPathFor(), q);
|
|
166
|
+
out(res.ok ? `restored ${res.rule.id} from ${res.from}` : `ERROR: ${res.error}`);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
setup() {
|
|
170
|
+
out('CHIRON setup: state readout. This command changes nothing; every step is optional and explained.\n');
|
|
171
|
+
const steps = [];
|
|
172
|
+
const lp = ledger.projectLedgerPath(root);
|
|
173
|
+
steps.push({
|
|
174
|
+
name: 'Project ledger', done: fs.existsSync(lp),
|
|
175
|
+
why: 'the canonical store your rules live in. Without it, corrections stay trapped in chat history.',
|
|
176
|
+
next: 'chiron init'
|
|
177
|
+
});
|
|
178
|
+
const targets = compiler.detectTargets(root);
|
|
179
|
+
const drift = fs.existsSync(lp) ? compiler.verify(root, [...ledger.load(ledger.globalLedgerPath()), ...ledger.load(lp)]) : targets;
|
|
180
|
+
steps.push({
|
|
181
|
+
name: `Agent files compiled (${targets.join(', ')})`, done: fs.existsSync(lp) && drift.length === 0,
|
|
182
|
+
why: 'rules only work if the agent reads them at session start. Compile projects your ledger into each agent\'s memory file inside managed markers; your own content is never touched.',
|
|
183
|
+
next: 'chiron compile --apply'
|
|
184
|
+
});
|
|
185
|
+
const hookHint = path.join(require('os').homedir(), '.claude', 'settings.json');
|
|
186
|
+
let hookRegistered = false;
|
|
187
|
+
try { hookRegistered = (fs.readFileSync(hookHint, 'utf8')).includes('chiron-hook'); } catch {}
|
|
188
|
+
steps.push({
|
|
189
|
+
name: 'Live correction hook (optional)', done: hookRegistered,
|
|
190
|
+
why: 'nudges the agent to capture a rule the moment you correct it, so nothing is lost. Without it, you rely on remembering to run /chiron capture or on periodic mining. It reads your prompt locally, adds one line of context, never blocks.',
|
|
191
|
+
next: `add to ${hookHint} under hooks.UserPromptSubmit: node ${path.resolve(__dirname, '..', 'hooks', 'chiron-hook.js')}`
|
|
192
|
+
});
|
|
193
|
+
steps.push({
|
|
194
|
+
name: 'History mined at least once (optional)', done: fs.existsSync(path.join(ledger.projectDir(root), 'mine-report.json')),
|
|
195
|
+
why: 'your past sessions already contain corrections you paid for. Mining sweeps them read-only and lists candidates worth turning into rules.',
|
|
196
|
+
next: `chiron mine --out ${path.join(ledger.projectDir(root), 'mine-report.json')}`
|
|
197
|
+
});
|
|
198
|
+
for (const s of steps) {
|
|
199
|
+
out(`${s.done ? '[done] ' : '[missing]'} ${s.name}`);
|
|
200
|
+
out(` why: ${s.why}`);
|
|
201
|
+
if (!s.done) out(` next: ${s.next}`);
|
|
202
|
+
out('');
|
|
203
|
+
}
|
|
204
|
+
const missing = siblings.recommendMissing();
|
|
205
|
+
if (missing.length) {
|
|
206
|
+
out('Demiurge siblings you do not have yet (recommendations only, nothing auto-installs):');
|
|
207
|
+
for (const m of missing) out(` - ${m.name}: ${m.why}`);
|
|
208
|
+
}
|
|
209
|
+
const first = steps.find(s => !s.done);
|
|
210
|
+
out(first ? `\nExact next action: ${first.next}` : '\nEverything is set. Correct your agent once; CHIRON keeps it corrected.');
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
status() { commands.check(); },
|
|
214
|
+
|
|
215
|
+
help() {
|
|
216
|
+
out(`CHIRON: Corrections Harvested Into Rules, Obeyed Next-session.
|
|
217
|
+
|
|
218
|
+
Chiron trained the heroes. CHIRON trains your agents: every correction you
|
|
219
|
+
make becomes a permanent rule, compiled into every agent's memory.
|
|
220
|
+
|
|
221
|
+
usage: chiron <command> [flags]
|
|
222
|
+
|
|
223
|
+
init create the project ledger (.chiron/ledger.md)
|
|
224
|
+
add --rule "..." capture a rule (--mistake, --how, --detail, --type gotcha, --source; dedup + contradiction gated)
|
|
225
|
+
bump <id> same lesson happened again: count it
|
|
226
|
+
list show the ledger
|
|
227
|
+
compile [--apply] project rules into CLAUDE.md / AGENTS.md / .cursor / .windsurf (dry-run default)
|
|
228
|
+
mine [--limit N] [--out f] sweep past transcripts for uncaptured corrections (read-only)
|
|
229
|
+
check [--json] health: duplicates, contradictions, stale rules, compile drift
|
|
230
|
+
promote [--apply] propose project rules seen in 2+ projects for the global ledger
|
|
231
|
+
archive <id> retire a rule (never deleted; dated archive + changelog)
|
|
232
|
+
restore <id|text> bring an archived rule back
|
|
233
|
+
setup state-aware guided setup (reads, explains, changes nothing)
|
|
234
|
+
|
|
235
|
+
--global operate on the global ledger (~/.chiron/global)
|
|
236
|
+
--apply apply changes (everything defaults to dry-run)
|
|
237
|
+
`);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
(commands[cmd] || commands.help)();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# HONEST-NUMBERS: when CHIRON loses
|
|
2
|
+
|
|
3
|
+
Every Demiurge product tells you where it does NOT win. Here is CHIRON's list.
|
|
4
|
+
|
|
5
|
+
## Mining is heuristic, and we measured it
|
|
6
|
+
|
|
7
|
+
`chiron mine` finds corrections with deterministic text patterns, zero LLM. On the seeded benchmark corpus (6 planted corrections + 4 noise turns, in `benchmarks/run.js`, reproducible): 6/6 planted detected, 0 noise hits. Real transcripts are messier than any fixture: expect FALSE POSITIVES (a "no," that opens a clarification, not a correction) and misses (corrections phrased gently). That is why mine outputs candidates for review, never auto-rules. Run it expecting to discard some of what it finds.
|
|
8
|
+
|
|
9
|
+
## Distillation is judgment, not measurement
|
|
10
|
+
|
|
11
|
+
Turning a correction into a well-worded rule is LLM work (the skill layer). No benchmark can bless judgment; what IS checkable are the receipts: every capture shows the rule id and the exact files compiled, the ledger is git-diffable markdown, and every change lands in an append-only changelog. Trust the trail, not the claim.
|
|
12
|
+
|
|
13
|
+
## If you live in one agent and never leave, you get less
|
|
14
|
+
|
|
15
|
+
The headline is cross-agent: one correction compiled into CLAUDE.md, AGENTS.md, Cursor, and Windsurf at once. If you use Claude Code only, Anthropic's native auto-memory already captures casual corrections for you, and CHIRON's remaining value narrows to governance: dedup, contradiction surfacing, promote-to-global, archive-not-delete, and the mining sweep. Real, but a smaller win.
|
|
16
|
+
|
|
17
|
+
## Rules compete for context
|
|
18
|
+
|
|
19
|
+
Every compiled rule costs context tokens in every future session. A 200-rule ledger is a liability, not an asset. CHIRON's health check flags staleness and duplicates, and the Windsurf compiler enforces the platform's hard caps loudly, but the discipline of keeping rules FEW and SHARP is yours. Archive freely; nothing is ever lost.
|
|
20
|
+
|
|
21
|
+
## What CHIRON does not do
|
|
22
|
+
|
|
23
|
+
- It does not stop the mistake happening the FIRST time. That is HORKOS's job (evidence-audit of every action). CHIRON stops the second time.
|
|
24
|
+
- It does not consolidate or reorganize your broader memory files. That is HYPNOS's job. CHIRON owns one pipeline: correction to rule.
|
|
25
|
+
- It does not sync rules across machines or teams. v1 is local-first, single user.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CHIRON live hook (UserPromptSubmit). Two triggers, both add a one-line capture
|
|
3
|
+
// nudge and never block:
|
|
4
|
+
// 1. the user's message looks like a correction (scanText), or
|
|
5
|
+
// 2. HORKOS caught a false-completion or evidence gap this session: its monotonic
|
|
6
|
+
// `caught` counter grew. Auditor catches are prime rule material, and this fires
|
|
7
|
+
// even when the catch was resolved before the user's next message.
|
|
8
|
+
'use strict';
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { scanText } = require('../lib/mine');
|
|
13
|
+
|
|
14
|
+
function readJSON(p, fallback) {
|
|
15
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^/, '')); } catch { return fallback; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Nudge once per NEW HORKOS catch this session. HORKOS bumps `caught` on every
|
|
19
|
+
// Stop-hook block; a per-session marker records the count we last nudged at, so a
|
|
20
|
+
// fresh catch (counter grew) fires exactly once and a resolved-and-quiet session
|
|
21
|
+
// stays silent. Baseline is set on first sight so prior-session catches never fire.
|
|
22
|
+
function detectHorkosCatch(sessionId) {
|
|
23
|
+
if (!sessionId) return null;
|
|
24
|
+
try {
|
|
25
|
+
const horkosHome = process.env.HORKOS_HOME || path.join(os.homedir(), '.horkos');
|
|
26
|
+
const stats = readJSON(path.join(horkosHome, 'stats.json'), null);
|
|
27
|
+
if (!stats || typeof stats.caught !== 'number') return null;
|
|
28
|
+
const caught = stats.caught;
|
|
29
|
+
|
|
30
|
+
const chironHome = process.env.CHIRON_HOME || path.join(os.homedir(), '.chiron');
|
|
31
|
+
const markerPath = path.join(chironHome, 'horkos-watch', String(sessionId) + '.json');
|
|
32
|
+
const marker = readJSON(markerPath, null);
|
|
33
|
+
if (!marker) {
|
|
34
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
35
|
+
fs.writeFileSync(markerPath, JSON.stringify({ baseline: caught, nudgedCaught: caught }), 'utf8');
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (caught > marker.nudgedCaught) {
|
|
39
|
+
marker.nudgedCaught = caught;
|
|
40
|
+
fs.writeFileSync(markerPath, JSON.stringify(marker), 'utf8');
|
|
41
|
+
const audit = readJSON(path.join(horkosHome, 'sessions', String(sessionId), 'audit.json'), null);
|
|
42
|
+
const s = audit && audit.summary;
|
|
43
|
+
if (s) {
|
|
44
|
+
const bits = [];
|
|
45
|
+
if (s.phantom_claims) bits.push(s.phantom_claims + ' phantom claim(s)');
|
|
46
|
+
if (s.fail) bits.push(s.fail + ' failed verification(s)');
|
|
47
|
+
if (s.silent_failures) bits.push(s.silent_failures + ' silent failure(s)');
|
|
48
|
+
if (bits.length) return ' (' + bits.join(', ') + ')';
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let input = '';
|
|
59
|
+
process.stdin.on('data', d => { input += d; });
|
|
60
|
+
process.stdin.on('end', () => {
|
|
61
|
+
try {
|
|
62
|
+
const payload = JSON.parse(input);
|
|
63
|
+
const prompt = payload.prompt || payload.user_prompt || '';
|
|
64
|
+
const sessionId = payload.session_id || payload.sessionId || '';
|
|
65
|
+
const parts = [];
|
|
66
|
+
const tag = scanText(String(prompt));
|
|
67
|
+
if (tag) parts.push(`this message looks like a correction (${tag})`);
|
|
68
|
+
const horkos = detectHorkosCatch(sessionId);
|
|
69
|
+
if (horkos !== null) parts.push(`HORKOS caught a false-completion or evidence gap this session${horkos}`);
|
|
70
|
+
if (parts.length) {
|
|
71
|
+
process.stdout.write(JSON.stringify({
|
|
72
|
+
hookSpecificOutput: {
|
|
73
|
+
hookEventName: 'UserPromptSubmit',
|
|
74
|
+
additionalContext: `CHIRON: ${parts.join('; and ')}. Once resolved, capture the durable lesson as a permanent rule with /chiron capture so the class of mistake never repeats.`
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Garbage stdin must never break the user's session. Exit clean, say nothing.
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
});
|
package/lib/archive.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// CHIRON archive: nothing is ever deleted. Retired rules land in a dated
|
|
2
|
+
// archive file and can be restored. Every applied change is logged.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const ledger = require('./ledger');
|
|
7
|
+
|
|
8
|
+
function archiveDir(chironDir) { return path.join(chironDir, 'archive'); }
|
|
9
|
+
function changelogPath(chironDir) { return path.join(chironDir, 'CHANGELOG.md'); }
|
|
10
|
+
|
|
11
|
+
function today() { return new Date().toISOString().slice(0, 10); }
|
|
12
|
+
|
|
13
|
+
function logChange(chironDir, line) {
|
|
14
|
+
const p = changelogPath(chironDir);
|
|
15
|
+
const stamp = new Date().toISOString();
|
|
16
|
+
const entry = `- ${stamp} | ${line}\n`;
|
|
17
|
+
fs.mkdirSync(chironDir, { recursive: true });
|
|
18
|
+
fs.appendFileSync(p, entry, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Archive a rule: move it out of the ledger into archive/YYYY-MM-DD.md
|
|
22
|
+
function archiveRule(ledgerPath, ruleId, reason) {
|
|
23
|
+
const chironDir = path.dirname(ledgerPath);
|
|
24
|
+
const rules = ledger.load(ledgerPath);
|
|
25
|
+
const idx = rules.findIndex(r => r.id === ruleId);
|
|
26
|
+
if (idx === -1) return { ok: false, error: `rule ${ruleId} not found` };
|
|
27
|
+
const [rule] = rules.splice(idx, 1);
|
|
28
|
+
rule.status = 'archived';
|
|
29
|
+
const archPath = path.join(archiveDir(chironDir), `${today()}.md`);
|
|
30
|
+
fs.mkdirSync(archiveDir(chironDir), { recursive: true });
|
|
31
|
+
const block = `<!-- archived from ${path.basename(ledgerPath)} | reason: ${reason || 'unspecified'} -->\n## ${rule.id} | archived | ${rule.created}\n**Mistake:** ${rule.mistake}\n**Rule:** ${rule.rule}\n**Apply:** ${rule.apply}\n- source: ${rule.source}\n- projects: ${rule.projects.join(', ')}\n- occurrences: ${rule.occurrences}\n- updated: ${rule.updated}\n\n`;
|
|
32
|
+
fs.appendFileSync(archPath, block, 'utf8');
|
|
33
|
+
ledger.save(ledgerPath, rules);
|
|
34
|
+
logChange(chironDir, `archived ${ruleId} (${reason || 'unspecified'}) -> archive/${today()}.md`);
|
|
35
|
+
return { ok: true, rule, archive: archPath };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Restore: search archive files for a rule id or a query string, put it back.
|
|
39
|
+
function restoreRule(ledgerPath, query) {
|
|
40
|
+
const chironDir = path.dirname(ledgerPath);
|
|
41
|
+
const dir = archiveDir(chironDir);
|
|
42
|
+
let files = [];
|
|
43
|
+
try { files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); } catch { return { ok: false, error: 'no archive' }; }
|
|
44
|
+
for (const f of files.sort().reverse()) {
|
|
45
|
+
const content = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
46
|
+
const rules = ledger.parse(content);
|
|
47
|
+
const hit = rules.find(r => r.id === query || (r.rule || '').toLowerCase().includes(query.toLowerCase()));
|
|
48
|
+
if (hit) {
|
|
49
|
+
const live = ledger.load(ledgerPath);
|
|
50
|
+
hit.status = 'active';
|
|
51
|
+
if (live.some(r => r.id === hit.id)) hit.id = ledger.nextId(live);
|
|
52
|
+
live.push(hit);
|
|
53
|
+
ledger.save(ledgerPath, live);
|
|
54
|
+
logChange(chironDir, `restored ${hit.id} from archive/${f}`);
|
|
55
|
+
return { ok: true, rule: hit, from: f };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { ok: false, error: `no archived rule matches "${query}"` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { archiveDir, changelogPath, logChange, archiveRule, restoreRule };
|
package/lib/compiler.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// CHIRON cross-agent compiler: projects active rules into each agent's memory
|
|
2
|
+
// layer inside managed markers. Surgical: content outside markers is never
|
|
3
|
+
// touched. Per-target quirks follow the verified HYPNOS table.
|
|
4
|
+
'use strict';
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const ledger = require('./ledger');
|
|
8
|
+
|
|
9
|
+
const BEGIN = '<!-- chiron:begin (managed by CHIRON, do not edit inside) -->';
|
|
10
|
+
const END = '<!-- chiron:end -->';
|
|
11
|
+
const WINDSURF_CAP = 12000; // hard per-workspace-file cap, fail loud
|
|
12
|
+
|
|
13
|
+
function ruleLines(rules, opts = {}) {
|
|
14
|
+
// Only enforceable corrections are projected into agent memory files, keeping
|
|
15
|
+
// them terse. type: gotcha entries stay in the ledger as the technical journal
|
|
16
|
+
// (the role lessons.md used to fill) and never bloat CLAUDE.md.
|
|
17
|
+
const active = rules.filter(r => r.status === 'active' && (r.type || 'correction') === 'correction');
|
|
18
|
+
const lines = ['## Rules learned from corrections (CHIRON)', ''];
|
|
19
|
+
for (const r of active) {
|
|
20
|
+
lines.push(`- **${r.id}:** ${r.rule}${opts.withApply && r.apply ? ` (apply: ${r.apply})` : ''}`);
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function injectBlock(existing, block) {
|
|
26
|
+
const body = `${BEGIN}\n${block}\n${END}`;
|
|
27
|
+
if (existing === null || existing === undefined) return body + '\n';
|
|
28
|
+
const bi = existing.indexOf(BEGIN), ei = existing.indexOf(END);
|
|
29
|
+
if (bi !== -1 && ei !== -1 && ei > bi) {
|
|
30
|
+
return existing.slice(0, bi) + body + existing.slice(ei + END.length);
|
|
31
|
+
}
|
|
32
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
33
|
+
return existing + sep + body + '\n';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Target definitions. Each returns { path, content } or an error.
|
|
37
|
+
const TARGETS = {
|
|
38
|
+
claude: {
|
|
39
|
+
file: root => path.join(root, 'CLAUDE.md'),
|
|
40
|
+
render: rules => injectBlockInto(rules, {}),
|
|
41
|
+
},
|
|
42
|
+
agentsmd: {
|
|
43
|
+
file: root => path.join(root, 'AGENTS.md'),
|
|
44
|
+
render: rules => injectBlockInto(rules, {}), // plain markdown only (frontmatter spec not merged)
|
|
45
|
+
},
|
|
46
|
+
cursor: {
|
|
47
|
+
file: root => path.join(root, '.cursor', 'rules', 'chiron.mdc'),
|
|
48
|
+
render: rules => ({
|
|
49
|
+
full: [
|
|
50
|
+
'---',
|
|
51
|
+
'description: Rules learned from user corrections, compiled by CHIRON',
|
|
52
|
+
'alwaysApply: true',
|
|
53
|
+
'---',
|
|
54
|
+
'',
|
|
55
|
+
ruleLines(rules)
|
|
56
|
+
].join('\n') + '\n',
|
|
57
|
+
replaceWhole: true // .mdc is CHIRON-owned, whole-file write is safe
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
windsurf: {
|
|
61
|
+
file: root => path.join(root, '.windsurf', 'rules', 'chiron.md'),
|
|
62
|
+
render: rules => {
|
|
63
|
+
const content = ruleLines(rules) + '\n';
|
|
64
|
+
if (content.length > WINDSURF_CAP) {
|
|
65
|
+
return { error: `windsurf output ${content.length} chars exceeds hard cap ${WINDSURF_CAP}. Archive or tighten rules first.` };
|
|
66
|
+
}
|
|
67
|
+
return { full: content, replaceWhole: true };
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function injectBlockInto(rules) {
|
|
73
|
+
return { block: ruleLines(rules) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Detect which targets exist in this project (zero tokens, local checks only).
|
|
77
|
+
function detectTargets(root) {
|
|
78
|
+
const found = [];
|
|
79
|
+
if (fs.existsSync(path.join(root, 'CLAUDE.md'))) found.push('claude');
|
|
80
|
+
if (fs.existsSync(path.join(root, 'AGENTS.md'))) found.push('agentsmd');
|
|
81
|
+
if (fs.existsSync(path.join(root, '.cursor'))) found.push('cursor');
|
|
82
|
+
if (fs.existsSync(path.join(root, '.windsurf'))) found.push('windsurf');
|
|
83
|
+
if (found.length === 0) found.push('claude'); // sensible default: create CLAUDE.md block
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Compile: dry-run by default. Returns per-target planned writes as diffs.
|
|
88
|
+
function compile(root, rules, opts = {}) {
|
|
89
|
+
const targets = opts.targets || detectTargets(root);
|
|
90
|
+
const plan = [];
|
|
91
|
+
for (const name of targets) {
|
|
92
|
+
const t = TARGETS[name];
|
|
93
|
+
if (!t) { plan.push({ target: name, error: 'unknown target' }); continue; }
|
|
94
|
+
const filePath = t.file(root);
|
|
95
|
+
const rendered = t.render(rules);
|
|
96
|
+
if (rendered.error) { plan.push({ target: name, file: filePath, error: rendered.error }); continue; }
|
|
97
|
+
const existing = ledger.readFileSafe(filePath);
|
|
98
|
+
const next = rendered.replaceWhole ? rendered.full : injectBlock(existing, rendered.block);
|
|
99
|
+
const changed = existing !== next;
|
|
100
|
+
plan.push({ target: name, file: filePath, changed, before: existing, after: next });
|
|
101
|
+
}
|
|
102
|
+
if (opts.apply) {
|
|
103
|
+
for (const p of plan) {
|
|
104
|
+
if (p.error || !p.changed) continue;
|
|
105
|
+
ledger.writeFileUtf8(p.file, p.after);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return plan;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Verify: does each compiled block match the current ledger? (compile drift check)
|
|
112
|
+
function verify(root, rules, opts = {}) {
|
|
113
|
+
const plan = compile(root, rules, Object.assign({}, opts, { apply: false }));
|
|
114
|
+
return plan.filter(p => !p.error && p.changed).map(p => p.target);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { BEGIN, END, WINDSURF_CAP, ruleLines, injectBlock, detectTargets, compile, verify };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// CHIRON governance: dedup, contradiction surfacing, promote-to-global, health.
|
|
2
|
+
// All deterministic. CHIRON surfaces and proposes; it never auto-resolves.
|
|
3
|
+
'use strict';
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const ledger = require('./ledger');
|
|
6
|
+
|
|
7
|
+
// --- Dedup -----------------------------------------------------------------
|
|
8
|
+
function findDuplicates(rules, threshold = 0.6) {
|
|
9
|
+
const dupes = [];
|
|
10
|
+
for (let i = 0; i < rules.length; i++) {
|
|
11
|
+
for (let j = i + 1; j < rules.length; j++) {
|
|
12
|
+
if (rules[i].status !== 'active' || rules[j].status !== 'active') continue;
|
|
13
|
+
const sim = ledger.jaccard(ledger.fingerprint(rules[i]), ledger.fingerprint(rules[j]));
|
|
14
|
+
if (sim >= threshold) dupes.push({ a: rules[i].id, b: rules[j].id, similarity: +sim.toFixed(2) });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return dupes;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Contradiction ----------------------------------------------------------
|
|
21
|
+
// Heuristic polarity detection: two rules sharing a token core but pulling in
|
|
22
|
+
// opposite directions (always vs never, use vs avoid, do vs do not).
|
|
23
|
+
const POS = /\b(always|must|use|do|prefer|require)\b/i;
|
|
24
|
+
const NEG = /\b(never|don'?t|do not|avoid|ban|forbid|stop)\b/i;
|
|
25
|
+
function polarity(text) {
|
|
26
|
+
// Negation dominates: "never use X" is a negative directive even though it
|
|
27
|
+
// contains a positive verb.
|
|
28
|
+
if (NEG.test(text)) return -1;
|
|
29
|
+
if (POS.test(text)) return 1;
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
function findContradictions(rules, coreOverlap = 0.5) {
|
|
33
|
+
const hits = [];
|
|
34
|
+
const active = rules.filter(r => r.status === 'active');
|
|
35
|
+
for (let i = 0; i < active.length; i++) {
|
|
36
|
+
for (let j = i + 1; j < active.length; j++) {
|
|
37
|
+
const pi = polarity(active[i].rule), pj = polarity(active[j].rule);
|
|
38
|
+
if (pi === 0 || pj === 0 || pi === pj) continue;
|
|
39
|
+
const sim = ledger.jaccard(ledger.fingerprint(active[i]), ledger.fingerprint(active[j]));
|
|
40
|
+
if (sim >= coreOverlap) {
|
|
41
|
+
hits.push({
|
|
42
|
+
a: active[i].id, b: active[j].id, similarity: +sim.toFixed(2),
|
|
43
|
+
note: 'opposite polarity on a shared core. CHIRON does not pick a winner: review and archive one, or reconcile.'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return hits;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Promote-to-global -------------------------------------------------------
|
|
52
|
+
// A rule whose fingerprint appears in >= minProjects project ledgers is proposed
|
|
53
|
+
// for the global ledger. Proposal only; apply is explicit.
|
|
54
|
+
function findPromotions(minProjects = 2) {
|
|
55
|
+
const reg = ledger.loadRegistry();
|
|
56
|
+
const globalRules = ledger.load(ledger.globalLedgerPath());
|
|
57
|
+
const seen = []; // { rule, roots: [] }
|
|
58
|
+
for (const root of reg.projects) {
|
|
59
|
+
const rules = ledger.load(ledger.projectLedgerPath(root));
|
|
60
|
+
for (const r of rules) {
|
|
61
|
+
if (r.status !== 'active') continue;
|
|
62
|
+
const match = seen.find(s => ledger.isDuplicate(s.rule, r));
|
|
63
|
+
if (match) { if (!match.roots.includes(root)) match.roots.push(root); }
|
|
64
|
+
else seen.push({ rule: r, roots: [root] });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return seen
|
|
68
|
+
.filter(s => s.roots.length >= minProjects)
|
|
69
|
+
.filter(s => !globalRules.some(g => g.status === 'active' && ledger.isDuplicate(g, s.rule)))
|
|
70
|
+
.map(s => ({
|
|
71
|
+
rule: s.rule,
|
|
72
|
+
projects: s.roots.map(r => path.basename(r)),
|
|
73
|
+
proposal: `seen in ${s.roots.length} projects: promote to global ledger`
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function applyPromotion(candidate) {
|
|
78
|
+
const globalPath = ledger.globalLedgerPath();
|
|
79
|
+
const globalRules = ledger.load(globalPath);
|
|
80
|
+
const promoted = Object.assign({}, candidate.rule, {
|
|
81
|
+
id: ledger.nextId(globalRules),
|
|
82
|
+
projects: candidate.projects,
|
|
83
|
+
source: `promoted from ${candidate.projects.join(', ')} (origin ${candidate.rule.id})`
|
|
84
|
+
});
|
|
85
|
+
globalRules.push(promoted);
|
|
86
|
+
ledger.save(globalPath, globalRules);
|
|
87
|
+
return promoted;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Parked dissent never compiles into agent rules. When a later incident shares
|
|
91
|
+
// its concrete vocabulary, resurface it for human review without changing policy.
|
|
92
|
+
function resurfaceDissent(rules, incident, threshold = 0.2) {
|
|
93
|
+
const probe = { rule: String(incident || '') };
|
|
94
|
+
return rules
|
|
95
|
+
.filter(r => r.type === 'dissent' && ['active', 'parked'].includes(r.status))
|
|
96
|
+
.map(r => ({ rule: r, similarity: ledger.jaccard(ledger.fingerprint(r), ledger.fingerprint(probe)) }))
|
|
97
|
+
.filter(x => x.similarity >= threshold)
|
|
98
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
99
|
+
.map(x => ({ id: x.rule.id, rule: x.rule.rule, source: x.rule.source, similarity: +x.similarity.toFixed(2), action: 'review only; do not auto-change policy' }));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Health ------------------------------------------------------------------
|
|
103
|
+
// 0-100, deterministic. Deductions: duplicates, contradictions, stale actives,
|
|
104
|
+
// compile drift (checked by caller via compiler.verify and passed in).
|
|
105
|
+
function health(rules, opts = {}) {
|
|
106
|
+
const active = rules.filter(r => r.status === 'active');
|
|
107
|
+
const dupes = findDuplicates(rules);
|
|
108
|
+
const contras = findContradictions(rules);
|
|
109
|
+
const now = opts.now ? new Date(opts.now) : new Date();
|
|
110
|
+
const stale = active.filter(r => {
|
|
111
|
+
const d = new Date(r.updated || r.created);
|
|
112
|
+
return !isNaN(d) && (now - d) / 86400000 > (opts.staleDays || 180);
|
|
113
|
+
});
|
|
114
|
+
let score = 100;
|
|
115
|
+
score -= Math.min(30, dupes.length * 10);
|
|
116
|
+
score -= Math.min(40, contras.length * 20);
|
|
117
|
+
score -= Math.min(20, stale.length * 2);
|
|
118
|
+
if (opts.compileDrift) score -= 10;
|
|
119
|
+
return {
|
|
120
|
+
score: Math.max(0, score),
|
|
121
|
+
active: active.length,
|
|
122
|
+
duplicates: dupes,
|
|
123
|
+
contradictions: contras,
|
|
124
|
+
stale: stale.map(r => r.id),
|
|
125
|
+
compileDrift: !!opts.compileDrift
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { findDuplicates, findContradictions, polarity, findPromotions, applyPromotion, resurfaceDissent, health };
|
package/lib/ledger.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// CHIRON ledger: the canonical rule store. Git-diffable markdown, strict structure.
|
|
2
|
+
// Rule format is Eragon's proven lessons discipline: Mistake / Rule / Apply + metadata.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const LEDGER_NAME = 'ledger.md';
|
|
9
|
+
const HEADER = '# CHIRON Ledger\n\nRules distilled from corrections. Managed by CHIRON; edit by hand only if you keep the structure.\n';
|
|
10
|
+
|
|
11
|
+
function projectDir(root) { return path.join(root, '.chiron'); }
|
|
12
|
+
function projectLedgerPath(root) { return path.join(projectDir(root), LEDGER_NAME); }
|
|
13
|
+
function globalDir() { return path.join(os.homedir(), '.chiron', 'global'); }
|
|
14
|
+
function globalLedgerPath() { return path.join(globalDir(), LEDGER_NAME); }
|
|
15
|
+
function registryPath() { return path.join(os.homedir(), '.chiron', 'registry.json'); }
|
|
16
|
+
|
|
17
|
+
function readFileSafe(p) {
|
|
18
|
+
try { return fs.readFileSync(p, 'utf8').replace(/^/, ''); } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
function writeFileUtf8(p, content) {
|
|
21
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
22
|
+
fs.writeFileSync(p, content, 'utf8'); // Node writes BOM-less UTF-8
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Parse a ledger file into rule objects.
|
|
26
|
+
// Rule block shape:
|
|
27
|
+
// ## CHI-R003 | active | 2026-07-05
|
|
28
|
+
// **Mistake:** ...
|
|
29
|
+
// **Rule:** ...
|
|
30
|
+
// **Apply:** ...
|
|
31
|
+
// - source: ...
|
|
32
|
+
// - projects: a, b
|
|
33
|
+
// - occurrences: 2
|
|
34
|
+
// - updated: 2026-07-05
|
|
35
|
+
function parse(content) {
|
|
36
|
+
if (!content) return [];
|
|
37
|
+
const rules = [];
|
|
38
|
+
const blocks = content.split(/^## /m).slice(1);
|
|
39
|
+
for (const block of blocks) {
|
|
40
|
+
const lines = block.split(/\r?\n/);
|
|
41
|
+
const head = lines[0].split('|').map(s => s.trim());
|
|
42
|
+
const rule = {
|
|
43
|
+
id: head[0] || '',
|
|
44
|
+
status: head[1] || 'active',
|
|
45
|
+
created: head[2] || '',
|
|
46
|
+
mistake: '', rule: '', apply: '', detail: '', type: 'correction',
|
|
47
|
+
source: '', projects: [], occurrences: 1, updated: head[2] || ''
|
|
48
|
+
};
|
|
49
|
+
const body = lines.slice(1);
|
|
50
|
+
// meta keys are a fixed set; a "- " line is meta ONLY if it names one of them,
|
|
51
|
+
// so bullet lists inside a multi-line Detail block are never mistaken for meta.
|
|
52
|
+
const metaRe = /^- (source|type|projects|occurrences|updated):\s*(.*)$/;
|
|
53
|
+
for (let li = 0; li < body.length; li++) {
|
|
54
|
+
const line = body[li];
|
|
55
|
+
const m = line.match(/^\*\*(Mistake|Rule|Apply):\*\*\s*(.*)$/);
|
|
56
|
+
if (m) { rule[m[1].toLowerCase()] = m[2].trim(); continue; }
|
|
57
|
+
const dm = line.match(/^\*\*Detail:\*\*\s*(.*)$/);
|
|
58
|
+
if (dm) {
|
|
59
|
+
const buf = [];
|
|
60
|
+
if (dm[1]) buf.push(dm[1]);
|
|
61
|
+
while (li + 1 < body.length && !metaRe.test(body[li + 1])) { li++; buf.push(body[li]); }
|
|
62
|
+
rule.detail = buf.join('\n').replace(/\s+$/, '');
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const meta = line.match(metaRe);
|
|
66
|
+
if (meta) {
|
|
67
|
+
if (meta[1] === 'projects') rule.projects = meta[2].split(',').map(s => s.trim()).filter(Boolean);
|
|
68
|
+
else if (meta[1] === 'occurrences') rule.occurrences = parseInt(meta[2], 10) || 1;
|
|
69
|
+
else rule[meta[1]] = meta[2].trim();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (rule.id) rules.push(rule);
|
|
73
|
+
}
|
|
74
|
+
return rules;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function serializeRule(r) {
|
|
78
|
+
const lines = [
|
|
79
|
+
`## ${r.id} | ${r.status} | ${r.created}`,
|
|
80
|
+
`**Mistake:** ${r.mistake}`,
|
|
81
|
+
`**Rule:** ${r.rule}`,
|
|
82
|
+
`**Apply:** ${r.apply}`,
|
|
83
|
+
];
|
|
84
|
+
if (r.detail && r.detail.trim()) {
|
|
85
|
+
// Strip leading markdown heading markers so a Detail line can never start
|
|
86
|
+
// with "## " and get mis-split as a new rule block on the next parse.
|
|
87
|
+
lines.push('**Detail:**');
|
|
88
|
+
lines.push(r.detail.replace(/^\s*#{1,6}\s+/gm, '').replace(/\s+$/, ''));
|
|
89
|
+
}
|
|
90
|
+
lines.push(`- source: ${r.source || 'manual'}`);
|
|
91
|
+
lines.push(`- type: ${r.type || 'correction'}`);
|
|
92
|
+
lines.push(`- projects: ${(r.projects || []).join(', ')}`);
|
|
93
|
+
lines.push(`- occurrences: ${r.occurrences || 1}`);
|
|
94
|
+
lines.push(`- updated: ${r.updated || r.created}`);
|
|
95
|
+
lines.push('');
|
|
96
|
+
return lines.join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function serialize(rules) {
|
|
100
|
+
return HEADER + '\n' + rules.map(serializeRule).join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function load(ledgerPath) { return parse(readFileSafe(ledgerPath)); }
|
|
104
|
+
|
|
105
|
+
function save(ledgerPath, rules) { writeFileUtf8(ledgerPath, serialize(rules)); }
|
|
106
|
+
|
|
107
|
+
function nextId(rules) {
|
|
108
|
+
let max = 0;
|
|
109
|
+
for (const r of rules) {
|
|
110
|
+
const m = r.id.match(/^CHI-R(\d+)$/);
|
|
111
|
+
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
112
|
+
}
|
|
113
|
+
return `CHI-R${String(max + 1).padStart(3, '0')}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fingerprint: normalized token set of the Rule line. Deterministic, zero-LLM.
|
|
117
|
+
const STOP = new Set(['a','an','the','to','of','in','on','for','and','or','is','are','be','it','this','that','with','at','by','as','do','not','dont','never','always','before','after','when','use','must']);
|
|
118
|
+
function tokens(text) {
|
|
119
|
+
return (text || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/)
|
|
120
|
+
.filter(t => t.length > 2 && !STOP.has(t));
|
|
121
|
+
}
|
|
122
|
+
function fingerprint(rule) { return [...new Set(tokens(rule.rule))].sort(); }
|
|
123
|
+
function jaccard(aSet, bSet) {
|
|
124
|
+
const a = new Set(aSet), b = new Set(bSet);
|
|
125
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
126
|
+
let inter = 0;
|
|
127
|
+
for (const t of a) if (b.has(t)) inter++;
|
|
128
|
+
return inter / (a.size + b.size - inter);
|
|
129
|
+
}
|
|
130
|
+
// Duplicate when rule-line token overlap is high.
|
|
131
|
+
function isDuplicate(r1, r2, threshold = 0.6) {
|
|
132
|
+
return jaccard(fingerprint(r1), fingerprint(r2)) >= threshold;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Registry of project ledgers (for promote-to-global scanning).
|
|
136
|
+
function loadRegistry() {
|
|
137
|
+
const raw = readFileSafe(registryPath());
|
|
138
|
+
if (!raw) return { projects: [] };
|
|
139
|
+
try { return JSON.parse(raw); } catch { return { projects: [] }; }
|
|
140
|
+
}
|
|
141
|
+
function registerProject(root) {
|
|
142
|
+
const reg = loadRegistry();
|
|
143
|
+
const abs = path.resolve(root);
|
|
144
|
+
if (!reg.projects.includes(abs)) {
|
|
145
|
+
reg.projects.push(abs);
|
|
146
|
+
writeFileUtf8(registryPath(), JSON.stringify(reg, null, 2));
|
|
147
|
+
}
|
|
148
|
+
return reg;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
projectDir, projectLedgerPath, globalDir, globalLedgerPath, registryPath,
|
|
153
|
+
parse, serialize, serializeRule, load, save, nextId,
|
|
154
|
+
tokens, fingerprint, jaccard, isDuplicate,
|
|
155
|
+
loadRegistry, registerProject, readFileSafe, writeFileUtf8
|
|
156
|
+
};
|
package/lib/mine.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// CHIRON retro transcript mining: sweep past session transcripts for user
|
|
2
|
+
// corrections that were never captured as rules. Deterministic heuristics,
|
|
3
|
+
// zero LLM, read-only. Output = CANDIDATES with pointers, never auto-rules.
|
|
4
|
+
// Honesty rule: heuristics have false positives; the skill distills, a human gates.
|
|
5
|
+
'use strict';
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
// User-turn correction signals. Anchored to reduce noise.
|
|
11
|
+
const USER_PATTERNS = [
|
|
12
|
+
{ re: /^no[,.]?\s/i, tag: 'flat-no' },
|
|
13
|
+
{ re: /\bthat'?s (wrong|not right|incorrect|not what i)/i, tag: 'wrong' },
|
|
14
|
+
{ re: /\byou (didn'?t|did not|forgot|missed|ignored)/i, tag: 'omission' },
|
|
15
|
+
{ re: /\bi (said|told you|asked for|meant)\b/i, tag: 'restate' },
|
|
16
|
+
{ re: /\bwhy (did|are) you\b/i, tag: 'challenge' },
|
|
17
|
+
{ re: /\b(don'?t|do not|stop|never) (do|use|write|add|delete|touch)\b/i, tag: 'prohibit' },
|
|
18
|
+
{ re: /\bagain\b.*\b(wrong|same|mistake|error)\b/i, tag: 'repeat-offense' },
|
|
19
|
+
{ re: /\bshould (be|have been|not)\b/i, tag: 'should' },
|
|
20
|
+
{ re: /\bwrong (file|folder|branch|name|path|format|place)\b/i, tag: 'wrong-target' },
|
|
21
|
+
];
|
|
22
|
+
// Assistant-turn acknowledgement markers (strong confirmation the prior user turn was a correction).
|
|
23
|
+
const ACK_PATTERNS = [
|
|
24
|
+
/\byou'?re right\b/i,
|
|
25
|
+
/\bmy (mistake|error|bad)\b/i,
|
|
26
|
+
/\bi (apologize|misread|misunderstood|shouldn'?t have)\b/i,
|
|
27
|
+
/\bcorrecting (that|this|now)\b/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function extractText(message) {
|
|
31
|
+
if (!message) return '';
|
|
32
|
+
const c = message.content;
|
|
33
|
+
if (typeof c === 'string') return c;
|
|
34
|
+
if (Array.isArray(c)) return c.filter(b => b && b.type === 'text').map(b => b.text || '').join('\n');
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scanText(text) {
|
|
39
|
+
if (!text) return null;
|
|
40
|
+
// skip pasted logs / tool noise: correction signals live in short human turns
|
|
41
|
+
const head = text.slice(0, 600);
|
|
42
|
+
for (const p of USER_PATTERNS) {
|
|
43
|
+
if (p.re.test(head)) return p.tag;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasAck(text) { return ACK_PATTERNS.some(re => re.test(text.slice(0, 800))); }
|
|
49
|
+
|
|
50
|
+
// Scan one JSONL transcript. Returns candidate corrections with line pointers.
|
|
51
|
+
function scanTranscript(filePath) {
|
|
52
|
+
let raw;
|
|
53
|
+
try { raw = fs.readFileSync(filePath, 'utf8'); } catch { return []; }
|
|
54
|
+
const lines = raw.split('\n');
|
|
55
|
+
const candidates = [];
|
|
56
|
+
let prev = null; // { tag, lineNo, excerpt }
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
let obj;
|
|
59
|
+
try { obj = JSON.parse(lines[i]); } catch { continue; }
|
|
60
|
+
const type = obj.type || (obj.message && obj.message.role);
|
|
61
|
+
if (type === 'user') {
|
|
62
|
+
const text = extractText(obj.message || obj);
|
|
63
|
+
// system-injected content is not a human correction
|
|
64
|
+
if (text.includes('<system-reminder>') || text.includes('tool_result')) { prev = null; continue; }
|
|
65
|
+
const tag = scanText(text);
|
|
66
|
+
prev = tag ? { tag, lineNo: i + 1, excerpt: text.slice(0, 200).replace(/\s+/g, ' ').trim() } : null;
|
|
67
|
+
} else if (type === 'assistant' && prev) {
|
|
68
|
+
const text = extractText(obj.message || obj);
|
|
69
|
+
const confirmed = hasAck(text);
|
|
70
|
+
candidates.push({
|
|
71
|
+
file: filePath, line: prev.lineNo, tag: prev.tag,
|
|
72
|
+
confidence: confirmed ? 'high' : 'medium',
|
|
73
|
+
excerpt: prev.excerpt
|
|
74
|
+
});
|
|
75
|
+
prev = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return candidates;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default transcript stores (Claude Code + Codex), overridable.
|
|
82
|
+
function defaultStores() {
|
|
83
|
+
return [
|
|
84
|
+
path.join(os.homedir(), '.claude', 'projects'),
|
|
85
|
+
path.join(os.homedir(), '.codex', 'sessions'),
|
|
86
|
+
].filter(p => fs.existsSync(p));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function listTranscripts(storeDir, maxFiles = 500) {
|
|
90
|
+
const out = [];
|
|
91
|
+
function walk(dir, depth) {
|
|
92
|
+
if (depth > 4 || out.length >= maxFiles) return;
|
|
93
|
+
let entries = [];
|
|
94
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
95
|
+
for (const e of entries) {
|
|
96
|
+
if (out.length >= maxFiles) return;
|
|
97
|
+
const p = path.join(dir, e.name);
|
|
98
|
+
if (e.isDirectory()) walk(p, depth + 1);
|
|
99
|
+
else if (e.name.endsWith('.jsonl')) out.push(p);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
walk(storeDir, 0);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mine(opts = {}) {
|
|
107
|
+
const stores = opts.stores || defaultStores();
|
|
108
|
+
const results = { stores, filesScanned: 0, candidates: [] };
|
|
109
|
+
for (const store of stores) {
|
|
110
|
+
const files = listTranscripts(store, opts.maxFiles || 500);
|
|
111
|
+
for (const f of files) {
|
|
112
|
+
results.filesScanned++;
|
|
113
|
+
results.candidates.push(...scanTranscript(f));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// high-confidence first, then by tag specificity
|
|
117
|
+
results.candidates.sort((a, b) => (a.confidence === b.confidence) ? 0 : (a.confidence === 'high' ? -1 : 1));
|
|
118
|
+
if (opts.limit) results.candidates = results.candidates.slice(0, opts.limit);
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { USER_PATTERNS, ACK_PATTERNS, scanText, hasAck, scanTranscript, defaultStores, listTranscripts, mine };
|
package/lib/siblings.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Demiurge sibling detection: cheap local checks, zero tokens.
|
|
2
|
+
// House rule 3: detect what is installed, recommend only what is MISSING.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const SIBLINGS = [
|
|
9
|
+
{ name: 'HORKOS', why: 'audits every action NOW with evidence; CHIRON learns from every correction FOREVER. Together they are the full discipline loop.', checks: [['skills', 'horkos'], ['bin', 'horkos']] },
|
|
10
|
+
{ name: 'HYPNOS', why: 'consolidates your memory files in their sleep; CHIRON\'s ledger is one more file it keeps healthy.', checks: [['skills', 'hypnos'], ['bin', 'hypnos']] },
|
|
11
|
+
{ name: 'VERITAS', why: 'keeps the prose your agent writes free of AI slop.', checks: [['skills', 'veritas']] },
|
|
12
|
+
{ name: 'MONETA', why: 'keeps your agent honest about token cost.', checks: [['bin', 'moneta']] },
|
|
13
|
+
{ name: 'ZOILUS', why: 'a blind panel judges the craft and rejects on doubt; the rejection-class it names once becomes a CHIRON rule.', checks: [['skills', 'zoilus'], ['bin', 'zoilus']] },
|
|
14
|
+
{ name: 'PEITHO', why: 'writes the go-to-market copy, and refuses to let it sound generic.', checks: [['skills', 'peitho'], ['bin', 'peitho']] },
|
|
15
|
+
{ name: 'PYRRHO', why: 'suspends judgment until the data earns it, so no numeric claim ships unaudited.', checks: [['skills', 'pyrrho'], ['bin', 'pyrrho']] },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function skillInstalled(name) {
|
|
19
|
+
return fs.existsSync(path.join(os.homedir(), '.claude', 'skills', name));
|
|
20
|
+
}
|
|
21
|
+
function binInstalled(name) {
|
|
22
|
+
const dirs = (process.env.PATH || '').split(path.delimiter);
|
|
23
|
+
const exts = process.platform === 'win32' ? ['.cmd', '.ps1', '.exe', ''] : [''];
|
|
24
|
+
return dirs.some(d => exts.some(x => { try { return fs.existsSync(path.join(d, name + x)); } catch { return false; } }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detect() {
|
|
28
|
+
return SIBLINGS.map(s => ({
|
|
29
|
+
name: s.name, why: s.why,
|
|
30
|
+
installed: s.checks.some(([kind, id]) => kind === 'skills' ? skillInstalled(id) : binInstalled(id))
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function recommendMissing() { return detect().filter(s => !s.installed); }
|
|
35
|
+
|
|
36
|
+
module.exports = { SIBLINGS, detect, recommendMissing };
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "demiurge-chiron",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Your agent makes the same mistake every session. CHIRON makes it make each mistake exactly once.",
|
|
5
|
+
"bin": { "chiron": "bin/chiron.js" },
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node benchmarks/run.js",
|
|
8
|
+
"bench": "node benchmarks/run.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": { "node": ">=18" },
|
|
11
|
+
"keywords": ["claude-code", "agents", "rules", "self-improvement", "corrections", "memory", "cross-agent"],
|
|
12
|
+
"author": "Eragon Lee (Demiurge)",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": { "type": "git", "url": "https://github.com/eragonlonelyboy-lab/chiron" },
|
|
15
|
+
"files": ["bin", "lib", "hooks", "docs", "skill", "CLAUDE.md"]
|
|
16
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chiron
|
|
3
|
+
description: Use when the user corrects the agent (wrong output, wrong approach, "no, I said...", "that's wrong", "you forgot..."), when the user asks to capture a lesson or rule ("/chiron", "capture that", "remember this rule", "never do that again"), when reviewing mined correction candidates from `chiron mine`, or at session end to sweep for uncaptured corrections. CHIRON turns corrections into permanent rules compiled into every agent's memory (CLAUDE.md, AGENTS.md, Cursor, Windsurf) so the same mistake never happens twice.
|
|
4
|
+
argument-hint: "[capture | review-mine | sweep] or just describe the correction"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# CHIRON: Corrections Harvested Into Rules, Obeyed Next-session
|
|
8
|
+
|
|
9
|
+
Chiron, the immortal centaur, trained Achilles, Asclepius, and Jason. He did not fight for the heroes; he made the heroes better. CHIRON does the same for your agents: every correction becomes a permanent rule, recalled in every future session, in every agent you use.
|
|
10
|
+
|
|
11
|
+
The cycle: **correction → rule → permanent recall.**
|
|
12
|
+
|
|
13
|
+
## When you are invoked
|
|
14
|
+
|
|
15
|
+
**Mode A, capture (default):** a correction just happened, or the user says "capture that."
|
|
16
|
+
**Mode B, review-mine:** the user ran `chiron mine` and wants candidates distilled.
|
|
17
|
+
**Mode C, sweep:** session is ending; check whether any correction this session went uncaptured.
|
|
18
|
+
|
|
19
|
+
## Mode A: Capture
|
|
20
|
+
|
|
21
|
+
1. **Identify the correction, then verify the diagnosis before distilling.** What did the agent do, and what did the user actually want? If it is ambiguous which of two things was the mistake, ask one short question, never guess. Then investigate *why* it happened and confirm the real cause before harvesting a rule: a rule distilled from a misdiagnosed or surface-level correction is worse than none, because it becomes permanent. Correction → **verified cause** → rule.
|
|
22
|
+
2. **Distill it into the rule format.** Three parts, each one sentence, durable and general enough to prevent the CLASS of mistake, specific enough to be actionable:
|
|
23
|
+
- **Mistake:** what went wrong (past tense, factual)
|
|
24
|
+
- **Rule:** the instruction that would have prevented it (imperative)
|
|
25
|
+
- **Apply:** how to follow it in practice (the check, the command, the habit)
|
|
26
|
+
Bad rule: "be more careful with files." Good rule: "Validate canvas JSON with a parser after every write; unescaped quotes break Obsidian."
|
|
27
|
+
Two optional fields carry the richer context a one-line rule cannot:
|
|
28
|
+
- **Detail** (`--detail`): a multi-line technical note (the failure scenario, code specifics, the fix). This is what a running lessons journal holds; put it here instead of losing it.
|
|
29
|
+
- **type** (`--type gotcha`): mark a technical discovery you hit yourself (not a user correction) as a `gotcha`. Corrections (the default) compile into every agent file; `gotcha` entries stay in the ledger as the searchable technical journal and never bloat the agent files. Use `gotcha` for API quirks, environment traps, and hard-won findings; use the default for behavioral rules the agent must obey.
|
|
30
|
+
- **Failure-class tag** (in `--source`): when the mistake maps to a working-discipline stage, append `class:<scope|evidence|adversarial|verify|report>` to the source string (e.g. `--source "session 2026-07-10 class:verify"`). Counting classes across the ledger turns it into a defect map: the class that accumulates the most rules is the habit to harden next. Tag honestly; skip it when none fits.
|
|
31
|
+
3. **Gate it through the CLI** (dedup + contradiction checks are deterministic, let them run):
|
|
32
|
+
```
|
|
33
|
+
chiron add --mistake "..." --rule "..." --how "..." --detail "..." --type gotcha --source "session YYYY-MM-DD"
|
|
34
|
+
```
|
|
35
|
+
(`--detail` and `--type` are optional; omit them for a plain behavioral correction.)
|
|
36
|
+
- **Exit 2 (duplicate):** the CLI names the existing rule. Bump it instead: `chiron bump <id>`. A repeated lesson is a signal, not a new rule.
|
|
37
|
+
- **Exit 3 (contradiction):** the CLI names the conflicting rule. NEVER pick a winner yourself. Show the user both rules and ask which stands; archive the loser with `chiron archive <id> --reason "superseded by <new>"`.
|
|
38
|
+
4. **Compile:** `chiron compile --apply` so every agent file gets the rule now, not someday.
|
|
39
|
+
5. **Confirm with receipts:** show the rule id, and the list of files the compiler wrote.
|
|
40
|
+
|
|
41
|
+
## Mode B: Review mined candidates
|
|
42
|
+
|
|
43
|
+
`chiron mine` output is heuristic: candidates, not truth. For each candidate the user wants processed:
|
|
44
|
+
1. Read the excerpt (and the transcript pointer if more context is needed).
|
|
45
|
+
2. Decide honestly: was this a real correction with a durable lesson, or noise (a one-off preference, a misunderstanding, an already-captured rule)? Say which and why in one line.
|
|
46
|
+
3. Real ones go through Mode A steps 2-5. Skip the rest, tell the user what was skipped.
|
|
47
|
+
|
|
48
|
+
## Mode C: Session sweep
|
|
49
|
+
|
|
50
|
+
Scan the current session for moments the user corrected you. For each one not yet captured, propose the distilled rule (Mode A format) and ask in ONE batch which to capture. Capture approved ones through Mode A steps 3-5.
|
|
51
|
+
|
|
52
|
+
## Laws (non-negotiable)
|
|
53
|
+
|
|
54
|
+
### Project-earned rules and vindicated dissent
|
|
55
|
+
|
|
56
|
+
- A project template may carry `[RESEED]` rules, but the marker stays until a real incident from that project supplies positive and negative evidence.
|
|
57
|
+
- A rejected or parked reviewer finding is not a correction yet. Store it as dissent linked to the decision and its tripwires, not as an active rule.
|
|
58
|
+
- When a tripwire fires or later evidence vindicates the finding, resurface it for review. Do not silently activate it and do not erase the original rejection reason.
|
|
59
|
+
- Compile only confirmed corrections into agent instructions. Agreement between multiple models is not confirmation.
|
|
60
|
+
|
|
61
|
+
- **The CLI gates every write.** Never write to the ledger file directly; `chiron add` runs the dedup and contradiction checks you cannot do reliably by eye.
|
|
62
|
+
- **Contradictions are asked, never picked.** CHIRON's trust model is the user's memory stays the user's.
|
|
63
|
+
- **Archive, never delete.** Retiring a rule goes through `chiron archive`; it is restorable forever.
|
|
64
|
+
- **A rule must name the class, not the instance.** "Do not call the API twice on THIS page" is an instance; "Re-fetch before every full-page overwrite" is a class.
|
|
65
|
+
- **Receipts on every capture:** rule id + compiled files, shown to the user.
|
|
66
|
+
- **Sibling note:** if HORKOS is installed, its audit failures are prime capture material; offer to distill repeated audit findings into rules.
|