baesian 0.0.0-stage → 1.0.1
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/LICENSE.md +105 -0
- package/README.md +217 -2
- package/dist/agents.js +249 -0
- package/dist/api.js +13 -0
- package/dist/bin.js +14 -0
- package/dist/ci.js +82 -0
- package/dist/cite.js +66 -0
- package/dist/cli.js +831 -0
- package/dist/client/assets/alegreya-cyrillic-ext-wght-italic-DzWRXZb0.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-ext-wght-normal-CN0-BfWD.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-wght-italic-Dhrv2xfk.woff2 +0 -0
- package/dist/client/assets/alegreya-cyrillic-wght-normal-CU5lb38f.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-ext-wght-italic-CoUI9EEu.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-ext-wght-normal-C8PPRmwJ.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-wght-italic-CVt9-Jr4.woff2 +0 -0
- package/dist/client/assets/alegreya-greek-wght-normal-BPx8yWcZ.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-ext-wght-italic-BIuo_2sR.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-ext-wght-normal-o8axeAGu.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-wght-italic-Bxkcgmge.woff2 +0 -0
- package/dist/client/assets/alegreya-latin-wght-normal-BG9rTJo4.woff2 +0 -0
- package/dist/client/assets/alegreya-vietnamese-wght-italic-BAPuWgnQ.woff2 +0 -0
- package/dist/client/assets/alegreya-vietnamese-wght-normal-DOV9JYup.woff2 +0 -0
- package/dist/client/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
- package/dist/client/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
- package/dist/client/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
- package/dist/client/assets/index-9yf70Vqv.css +1 -0
- package/dist/client/assets/index-Dwg9e0YR.js +15 -0
- package/dist/client/index.html +15 -0
- package/dist/facts.js +278 -0
- package/dist/folder-storage.js +71 -0
- package/dist/frontmatter.js +38 -0
- package/dist/gate-paths.js +77 -0
- package/dist/gate.js +237 -0
- package/dist/git.js +100 -0
- package/dist/globs.js +120 -0
- package/dist/hookblock.js +146 -0
- package/dist/hooks.js +911 -0
- package/dist/ids.js +23 -0
- package/dist/login.js +301 -0
- package/dist/mcp.js +303 -0
- package/dist/merges.js +849 -0
- package/dist/modes.js +245 -0
- package/dist/notify.js +91 -0
- package/dist/ops.js +381 -0
- package/dist/passkey.js +86 -0
- package/dist/personal.js +163 -0
- package/dist/practice-answers.js +42 -0
- package/dist/proposals.js +197 -0
- package/dist/quote.js +28 -0
- package/dist/report.js +261 -0
- package/dist/routing.js +411 -0
- package/dist/schema.js +1178 -0
- package/dist/server.js +514 -0
- package/dist/similarity.js +45 -0
- package/dist/sshsig.js +159 -0
- package/dist/state.js +135 -0
- package/dist/storage.js +61 -0
- package/dist/store.js +2259 -0
- package/dist/style.js +408 -0
- package/dist/sync.js +797 -0
- package/dist/trailers.js +121 -0
- package/dist/trust.js +222 -0
- package/dist/usage-fields.js +161 -0
- package/dist/vault.js +849 -0
- package/dist/verify.js +443 -0
- package/dist/version.js +70 -0
- package/dist/wait.js +449 -0
- package/dist/wikilinks.js +25 -0
- package/package.json +71 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Gus Ellerm
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,218 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/GusEllerm/baesian/main/docs/media/wordmark-dark.png">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/GusEllerm/baesian/main/docs/media/wordmark.png" alt="Baesian" width="280">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
<p align="center"><b>Swipe review for the decisions your coding agents shouldn't make alone.</b><br>
|
|
9
|
+
They put each one to you as a card, and you choose before you see their pick.</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://baesian.dev">baesian.dev</a> ·
|
|
13
|
+
<a href="#try-the-demo">Try the demo</a> ·
|
|
14
|
+
<a href="#how-it-works">How it works</a> ·
|
|
15
|
+
<a href="#set-it-up">Set it up</a> ·
|
|
16
|
+
<a href="https://github.com/GusEllerm/baesian/blob/main/docs/setup.md">Setup guide</a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="https://raw.githubusercontent.com/GusEllerm/baesian/main/docs/media/demo.gif" width="74%" alt="Answering two agents' decision cards in Baesian: choose first, then see the agent's pick, while each agent's terminal gets the answer">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/GusEllerm/baesian/main/docs/media/phone.png" width="22%" alt="A Baesian card held on a phone until it lifts, with its four answers named at the screen's edges">
|
|
22
|
+
</p>
|
|
23
|
+
<p align="center"><sub>At your desk, two agents building a made-up bakery's shop stop for you: you choose, see their pick,
|
|
24
|
+
overrule one, and each agent's terminal gets your answer. On your phone, you hold a card until it lifts and drag it to
|
|
25
|
+
an answer.</sub></p>
|
|
26
|
+
|
|
27
|
+
Your Claude Code and Codex agents make design decisions all day, and a few of them are hard to undo: where a
|
|
28
|
+
signed-in user's session lives, how prices are stored, what deleting an account removes. Baesian has the agent put
|
|
29
|
+
those to you as cards, before it builds on them. You pick one of its two options before you see which it chose, then
|
|
30
|
+
confirm or overrule it. Your answer goes back to the agent, and into git with the code it shapes.
|
|
31
|
+
|
|
32
|
+
## Try the demo
|
|
33
|
+
|
|
34
|
+
You need Node 22 or later, and git.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
git clone https://github.com/GusEllerm/baesian.git
|
|
38
|
+
cd baesian
|
|
39
|
+
npm install
|
|
40
|
+
npm run demo
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The demo makes a throwaway repo where two agents are building Crumb, a made-up bakery's online shop, and have filed
|
|
44
|
+
five decisions. It opens the client, and its terminal shows what each agent gets as you answer.
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
1. **An agent files a card** when a decision is hard to undo or isn't its call alone: a schema, a public API, a
|
|
49
|
+
dependency, CI, security, anything user-facing. `baesian init` tells your agents when, in `AGENTS.md`.
|
|
50
|
+
2. **You choose first,** between its two options, then see which it picked and why, and confirm or switch.
|
|
51
|
+
3. **The agent waits as long as the decision needs:** on most cards it carries on and gets your answer at its next
|
|
52
|
+
pause; on a *blocking* card it waits for you; on a *hard* card, the default for security, privacy, data loss, money
|
|
53
|
+
and publishing, it never goes ahead without you, and you hold to confirm.
|
|
54
|
+
4. **Your answer goes back to the agent,** and into git, committed with the code it shapes.
|
|
55
|
+
|
|
56
|
+
Two things keep decisions from slipping past: a **commit gate** blocks a commit that adds a dependency, changes a
|
|
57
|
+
schema or touches CI until a card covers it or the agent says why it's safe; and the **plugin** sends an agent that
|
|
58
|
+
asks you to choose in chat back to file a card.
|
|
59
|
+
|
|
60
|
+
## Why choose before you see the pick?
|
|
61
|
+
|
|
62
|
+
Seeing the agent's case first anchors you: a confident argument is easy to agree with. Choosing first records, for
|
|
63
|
+
each kind of decision, how often you'd have picked what the agent picked. That record is how Baesian suggests which
|
|
64
|
+
kinds you could leave to the agent. When you keep answering one kind the same way, Baesian offers to make it a rule
|
|
65
|
+
your agents follow without asking.
|
|
66
|
+
|
|
67
|
+
## Decisions live in git
|
|
68
|
+
|
|
69
|
+
Each branch carries its own cards, committed with its code, so decisions merge with the code they shaped. The client's
|
|
70
|
+
**Your decisions, by branch** (press B) draws that history: where each branch began, what was decided on it, and every
|
|
71
|
+
merge with what it brought.
|
|
72
|
+
|
|
73
|
+
<p align="center"><img src="https://raw.githubusercontent.com/GusEllerm/baesian/main/docs/media/branches.png" width="100%" alt="Baesian's Branches drawer on its own history: merges and cards by branch, with one card opened to the three commits that cite it"></p>
|
|
74
|
+
<p align="center"><sub>Baesian's own decisions on one evening: worktrees branching off v1/next, the cards filed on them
|
|
75
|
+
and the commits citing those cards, merging back.</sub></p>
|
|
76
|
+
|
|
77
|
+
Every commit names the cards it builds on, so if you change an answer later, the agent is told which commits built on
|
|
78
|
+
the old one. In CI, `baesian verify` checks each commit's decisions, and holds a merge until its cards are answered.
|
|
79
|
+
|
|
80
|
+
<p align="center"><img src="https://raw.githubusercontent.com/GusEllerm/baesian/main/docs/media/checkpoints.png" width="100%" alt="Four commits on feat/checkout, each carrying the decisions as they stood; when an answer later changes, the agent is told which commits built on the old one"></p>
|
|
81
|
+
<p align="center"><sub>Each commit carries the decisions as they stood. When an answer changes, the commits that built on
|
|
82
|
+
the old one are named.</sub></p>
|
|
83
|
+
|
|
84
|
+
## Set it up
|
|
85
|
+
|
|
86
|
+
You need Node 22 or later, git, and Claude Code or Codex. The
|
|
87
|
+
[setup guide](https://github.com/GusEllerm/baesian/blob/main/docs/setup.md) has each step in full.
|
|
88
|
+
|
|
89
|
+
### 1. Install the CLI
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
npm install -g baesian
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Make an account
|
|
96
|
+
|
|
97
|
+
Sign up at [baesian.dev](https://baesian.dev) and make a passkey. Add one on each device you answer from, your phone
|
|
98
|
+
included.
|
|
99
|
+
|
|
100
|
+
### 3. Set up your repo
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
cd your-repo
|
|
104
|
+
baesian init
|
|
105
|
+
baesian login
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`init` sets up the commit gate and tells your agents about Baesian. `login` links the repo to baesian.dev: approve
|
|
109
|
+
this machine on the page it prints, then run the commit it prints.
|
|
110
|
+
|
|
111
|
+
### 4. Install the plugin
|
|
112
|
+
|
|
113
|
+
In Claude Code:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
/plugin marketplace add GusEllerm/baesian
|
|
117
|
+
/plugin install baesian@baesian
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
<details>
|
|
121
|
+
<summary><b>Claude Code in VS Code, and Codex</b></summary>
|
|
122
|
+
|
|
123
|
+
In the VS Code extension, run the same from a terminal, then start a new conversation:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
claude plugin marketplace add GusEllerm/baesian
|
|
127
|
+
claude plugin install baesian@baesian
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
In Codex:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
codex plugin marketplace add GusEllerm/baesian
|
|
134
|
+
codex plugin add baesian@baesian
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Codex runs the plugin's hooks only after you accept its "Hooks need review" prompt in interactive mode, and again after
|
|
138
|
+
any update.
|
|
139
|
+
</details>
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary><b>Updating</b></summary>
|
|
143
|
+
|
|
144
|
+
Update the CLI and the plugin together, to the same release: `npm install -g baesian`, then
|
|
145
|
+
`claude plugin update baesian@baesian` (in Codex, `codex plugin marketplace upgrade baesian`, then
|
|
146
|
+
`codex plugin add baesian@baesian`). When they differ, the plugin tells your agent to ask you to update. Each release's
|
|
147
|
+
notes say what else to run again; [the setup guide](https://github.com/GusEllerm/baesian/blob/main/docs/setup.md#updating)
|
|
148
|
+
has the details.
|
|
149
|
+
</details>
|
|
150
|
+
|
|
151
|
+
<details>
|
|
152
|
+
<summary><b>Checking decisions in CI</b></summary>
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
baesian ci --github
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
sets the repository variable CI reads its keys from: the service's, a spare it keeps offline, and your passkeys'. Then
|
|
159
|
+
add the workflow steps it prints, which run the release that printed them until you run `baesian ci` again. See
|
|
160
|
+
[the setup guide](https://github.com/GusEllerm/baesian/blob/main/docs/setup.md#5-check-decisions-in-ci).
|
|
161
|
+
</details>
|
|
162
|
+
|
|
163
|
+
<details>
|
|
164
|
+
<summary><b>Without an account, or in a repo you share</b></summary>
|
|
165
|
+
|
|
166
|
+
`baesian init` then `baesian serve` gives you the client on your own machine, with nothing sent anywhere; `baesian
|
|
167
|
+
login` links the repo later. In a repo whose other contributors don't use Baesian, `baesian init --personal` keeps
|
|
168
|
+
everything in your clone and commits nothing. Both are in
|
|
169
|
+
[the setup guide](https://github.com/GusEllerm/baesian/blob/main/docs/setup.md#without-an-account).
|
|
170
|
+
</details>
|
|
171
|
+
|
|
172
|
+
## Releases
|
|
173
|
+
|
|
174
|
+
Each release's notes, with anything to run again after updating, are on
|
|
175
|
+
[GitHub Releases](https://github.com/GusEllerm/baesian/releases). From 1.0, every 1.x release reads the files earlier
|
|
176
|
+
ones wrote, and keeps the hosted service's protocol and the commands, flags, exit codes and MCP tools your agents are
|
|
177
|
+
told to use: the setup guide's [Compatibility](https://github.com/GusEllerm/baesian/blob/main/docs/setup.md#compatibility)
|
|
178
|
+
section says how. To report a security problem, write to security@baesian.dev
|
|
179
|
+
([SECURITY.md](https://github.com/GusEllerm/baesian/blob/main/SECURITY.md)).
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
To work on Baesian, build it from a clone and link it onto your PATH, in place of the `baesian` from npm:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
git clone https://github.com/GusEllerm/baesian.git
|
|
187
|
+
cd baesian
|
|
188
|
+
npm install
|
|
189
|
+
npm run build && npm link
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
To try the plugin from your clone, add its marketplace by path, `/plugin marketplace add /path/to/baesian`, then install
|
|
193
|
+
it as above. Both marketplaces are named `baesian`, so remove the one from GitHub first:
|
|
194
|
+
`/plugin marketplace remove baesian`.
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
npm test # unit tests
|
|
198
|
+
npm run e2e # browser tests, with Playwright
|
|
199
|
+
npm run typecheck
|
|
200
|
+
npm run readme-media # remakes this README's images (needs ffmpeg)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The project's memory is the Obsidian vault in
|
|
204
|
+
[`docs/vault/`](https://github.com/GusEllerm/baesian/blob/main/docs/vault/Home.md): the design, the decision log, and a
|
|
205
|
+
log of each working session. Start at `Home.md`.
|
|
206
|
+
|
|
207
|
+
## Licence
|
|
208
|
+
|
|
209
|
+
Baesian is source-available under the
|
|
210
|
+
[Functional Source License 1.1](https://github.com/GusEllerm/baesian/blob/main/LICENSE.md) (FSL-1.1-ALv2): you may use, change
|
|
211
|
+
and run it for any purpose except a product or service that competes with it, and each version becomes available under
|
|
212
|
+
the Apache License 2.0 two years after its release.
|
|
213
|
+
|
|
214
|
+
## Why "Baesian"
|
|
215
|
+
|
|
216
|
+
It's a dating app for decisions. Your agents line up their options, you swipe on the one you'd pick before you see
|
|
217
|
+
which one they picked, and over time you build up preferences for each kind of decision. The decisions you keep
|
|
218
|
+
agreeing on become rules, so your agents can make them for you from then on.
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { cite } from "./cite.js";
|
|
4
|
+
// Tier 1 (D27): plain instructions any harness can read. `baesian init` keeps this block in AGENTS.md.
|
|
5
|
+
export const BEGIN = "<!-- baesian:begin -->";
|
|
6
|
+
export const END = "<!-- baesian:end -->";
|
|
7
|
+
export const AGENTS_BLOCK = `${BEGIN}
|
|
8
|
+
## Decisions (Baesian)
|
|
9
|
+
|
|
10
|
+
Design decisions that are hard to undo go to a person as cards; you don't make them silently. Run
|
|
11
|
+
\`baesian inbox\` before starting work, and again when you finish a task: it shows new verdicts on this branch's
|
|
12
|
+
cards, to act on then rather than mid-task.
|
|
13
|
+
|
|
14
|
+
**File a card** (\`baesian template > card.md\`, fill it in, \`baesian file card.md\`) when a decision
|
|
15
|
+
changes a schema or on-disk format; changes a public API, URL scheme or module boundary; adds a runtime
|
|
16
|
+
dependency; changes CI, infrastructure or deployment; is user-facing; publishes something; touches
|
|
17
|
+
security or privacy; spends money; deletes or overwrites stored data; or would break something to
|
|
18
|
+
reverse. Otherwise decide yourself. Every card states its strongest alternative, and the one question
|
|
19
|
+
the choice turns on (the crux). A topic names one question: reuse one from \`baesian topics\` only for the
|
|
20
|
+
same question, and give a follow-on question its own, narrower topic. The reviewer chooses before seeing
|
|
21
|
+
your pick, your case or the card's body; put what they need in order to choose, kept neutral, in
|
|
22
|
+
\`crux_fact\`, and background in the body. Make your case in \`rationale\`, and the alternative's best case
|
|
23
|
+
in \`alternative_rationale\`, as you'd argue it if it were your pick. The decision record is committed
|
|
24
|
+
with the code, so don't paste secrets, credentials or private output into a card.
|
|
25
|
+
|
|
26
|
+
A choice you'd put to the reviewer in chat goes as a card, even when they're in the conversation: say
|
|
27
|
+
"it's in your deck" and don't list the options there, so they still choose before seeing your pick. A
|
|
28
|
+
choice the reviewer has already answered in chat is filed all the same, with their answer as your pick and
|
|
29
|
+
their words in \`answered_in_chat\`, for them to confirm with one tap; otherwise, file it before you ask. A
|
|
30
|
+
change that only restores what a card or rule decided cites it, and needs no card.
|
|
31
|
+
|
|
32
|
+
**The commit gate** detects some of these decisions (dependencies, schemas and migrations, CI and
|
|
33
|
+
deploy files) and blocks a commit until each one is covered. Either cite a card whose \`touches\`
|
|
34
|
+
include it, \`Baesian-Card: <id>\`, or, for a two-way door, acknowledge it with a reason:
|
|
35
|
+
\`Baesian-Ack: dep:httpx <why this is fine>\`. When it blocks, the gate prints the lines to paste.
|
|
36
|
+
\`baesian check\` previews it before you commit. Trailers go at the end of the message, after a blank
|
|
37
|
+
line.
|
|
38
|
+
|
|
39
|
+
- \`level: provisional\` (the default): carry on with your proposal. \`blocking\`: run \`baesian wait <id>\`
|
|
40
|
+
before building on it. \`hard\` (the default for publishing, security, privacy, money and data loss): never
|
|
41
|
+
proceed without a verdict. A level is a minimum only when the card is shown: the repo's preferences may
|
|
42
|
+
hold a card back instead, and \`baesian file\` says so. A held card means go ahead with your pick, as
|
|
43
|
+
provisional, citing it as usual; it counts as settled at merge, and the reviewer may answer it later.
|
|
44
|
+
- \`baesian wait <id>\` exits 0 when resolved (use the decision. If it went against your pick and you hold
|
|
45
|
+
a fact the reviewer lacked, or a check that would settle it, you may reply once:
|
|
46
|
+
\`baesian reply <id> "<the fact or check>"\`. Not your pick again); 4 when answered otherwise (returned:
|
|
47
|
+
read the comment, then \`baesian revise <id>\` or \`baesian withdraw <id>\`); 5 with no verdict yet
|
|
48
|
+
(proceed provisionally); 6 with no verdict on a hard card (don't proceed: park the task or stop); 7 while
|
|
49
|
+
the reviewer is still there (run \`baesian wait <id>\` again); 8 when the card is held back (go ahead with
|
|
50
|
+
your pick, as provisional). Each run lasts at most 100 seconds.
|
|
51
|
+
- If you end your turn with cards still with the reviewer whose answers you'll act on (blocking or hard
|
|
52
|
+
ones, or provisional ones), and your harness wakes you when a background command finishes (Claude Code
|
|
53
|
+
does), leave \`baesian wait <id> <id>… --until-answered\` running in the background, or \`--deck <deck>\`
|
|
54
|
+
for your open cards in a deck. It has no time limit: it ends when every card is answered (0 when all are
|
|
55
|
+
resolved, else 4), or sooner when one is returned (4) or held back (8). When it ends, act on the answers
|
|
56
|
+
it printed; \`baesian inbox\` won't repeat them. Without that (Codex), answers come with the next message.
|
|
57
|
+
- If an answer asks for a rule (\`wait\` and \`inbox\` say so), draft it, with the gate patterns it covers:
|
|
58
|
+
\`baesian rule <id> "<the rule>" --covers dep:pytest-*\`. It goes to the reviewer as a card.
|
|
59
|
+
- If you're a long-lived agent, set \`BAESIAN_AGENT\` to your name; otherwise your harness's name is used.
|
|
60
|
+
- If \`baesian file\` exits 3, the decision may already be open or decided: attach with
|
|
61
|
+
\`baesian attach <id>\`, or follow the decision. File with \`--new\` only to argue against one, and set
|
|
62
|
+
\`supersedes:\`. If yours is a different question on the same subject, give it a narrower topic
|
|
63
|
+
(\`client/rule-drafts\` beside \`client/rules\`) and list what it builds on in \`depends_on\`.
|
|
64
|
+
- Commits that build on a card carry the trailer \`Baesian-Card: <id>\`. The gate blocks a commit that
|
|
65
|
+
builds on a rejected card, or on a hard card that isn't resolved yet.
|
|
66
|
+
- Never record a verdict yourself, or write the repo's preference files: only a person answers, and sets
|
|
67
|
+
the preferences, in the Baesian client.
|
|
68
|
+
${END}`;
|
|
69
|
+
/**
|
|
70
|
+
* Who is running a command (D102): `--agent`, then `$BAESIAN_AGENT` (a long-lived agent's own name), then the
|
|
71
|
+
* name the harness gives itself, then "agent". The MCP server passes the name its client gave in the protocol, which
|
|
72
|
+
* comes before the harness's variables, since a stdio server isn't given them all (P18).
|
|
73
|
+
*/
|
|
74
|
+
export function agentName(opt, env = process.env, client) {
|
|
75
|
+
return opt ?? (env.BAESIAN_AGENT?.trim() || undefined) ?? clientAgentName(client) ?? harnessName(env) ?? "agent";
|
|
76
|
+
}
|
|
77
|
+
/** An MCP client's name as an agent's: Claude Code's and Codex's become the names their commands get; others stay. */
|
|
78
|
+
export function clientAgentName(client) {
|
|
79
|
+
const name = client?.trim();
|
|
80
|
+
if (!name)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (/^claude[-_ ]?code\b/i.test(name))
|
|
83
|
+
return "claude-code";
|
|
84
|
+
if (/^codex\b/i.test(name))
|
|
85
|
+
return "codex";
|
|
86
|
+
return name;
|
|
87
|
+
}
|
|
88
|
+
/** The harness's own name, from the variables it sets for the commands it runs. */
|
|
89
|
+
/**
|
|
90
|
+
* The harness session a command runs in, when the harness says: Claude Code gives its commands
|
|
91
|
+
* `CLAUDE_CODE_SESSION_ID`, and Codex `CODEX_THREAD_ID`, each the same ID its hooks receive as `session_id`. So an
|
|
92
|
+
* agent's own `baesian inbox` reads its session's place, as the plugin's hooks do (B-ghhym), and never hears twice.
|
|
93
|
+
*/
|
|
94
|
+
export function harnessSession(env = process.env) {
|
|
95
|
+
return env.CLAUDE_CODE_SESSION_ID?.trim() || env.CODEX_THREAD_ID?.trim() || undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The sessions a command's model is looked up by (B-k0w7d), in order: Claude Code's `CLAUDE_CODE_SESSION_ID`; and in
|
|
99
|
+
* Codex `CODEX_SESSION_ID`, which Codex's source names but which wasn't seen set, then `CODEX_THREAD_ID`, which rehearse
|
|
100
|
+
* found equal to the hooks' `session_id` in Codex 0.166.0. Codex's source fills the hooks' `session_id` from the session
|
|
101
|
+
* and the variable from the thread, so in a thread of its own (a subagent's, say) they may differ: then no file
|
|
102
|
+
* matches, and the card has no model rather than a wrong one.
|
|
103
|
+
*/
|
|
104
|
+
export function modelSessions(env = process.env) {
|
|
105
|
+
const ids = [env.CLAUDE_CODE_SESSION_ID, env.CODEX_SESSION_ID, env.CODEX_THREAD_ID].map((v) => v?.trim()).filter((v) => Boolean(v));
|
|
106
|
+
return [...new Set(ids)];
|
|
107
|
+
}
|
|
108
|
+
export function harnessName(env) {
|
|
109
|
+
// Claude Code sets AI_AGENT to "<name>_<version>_<role>", for example "claude-code_2-1-314_agent".
|
|
110
|
+
const ai = env.AI_AGENT?.trim();
|
|
111
|
+
if (ai)
|
|
112
|
+
return ai.split("_")[0] || undefined;
|
|
113
|
+
if (env.CLAUDE_CODE_ENTRYPOINT || env.CLAUDECODE)
|
|
114
|
+
return "claude-code";
|
|
115
|
+
// Codex sets no AI_AGENT, but gives the commands it runs a per-session CODEX_THREAD_ID (seen in 0.166.0, by
|
|
116
|
+
// rehearse's agent, with no model calls).
|
|
117
|
+
if (env.CODEX_THREAD_ID)
|
|
118
|
+
return "codex";
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The block for a repo, with its vault's guidance (M6, D173): with a vault, one sentence points agents to its routing
|
|
123
|
+
* notes and its rules, to read when deciding whether to file. They're guidance: the gate takes rules only from the store.
|
|
124
|
+
*/
|
|
125
|
+
export function agentsBlock(vault) {
|
|
126
|
+
if (!vault)
|
|
127
|
+
return AGENTS_BLOCK;
|
|
128
|
+
const read = [...vault.notes.map((n) => `\`${n}\``), `the rules in force, in \`${vault.rules}\``];
|
|
129
|
+
const sentence = `Before deciding whether to file, read this repo's guidance in its vault: ${read.length > 2 ? `${read.slice(0, -1).join(", ")} and ${read.at(-1)}` : read.join(" and ")}.`;
|
|
130
|
+
return AGENTS_BLOCK.replace("\n\n**The commit gate**", `\n\n${sentence}\n\n**The commit gate**`);
|
|
131
|
+
}
|
|
132
|
+
/** Write or refresh the Baesian block in `AGENTS.md`, leaving everything outside the markers alone. */
|
|
133
|
+
export function writeAgentsBlock(toplevel, vault) {
|
|
134
|
+
const path = join(toplevel, "AGENTS.md");
|
|
135
|
+
const block = agentsBlock(vault);
|
|
136
|
+
if (!existsSync(path)) {
|
|
137
|
+
writeFileSync(path, `# Agent instructions\n\n${block}\n`);
|
|
138
|
+
return "created";
|
|
139
|
+
}
|
|
140
|
+
const text = readFileSync(path, "utf8");
|
|
141
|
+
const start = text.indexOf(BEGIN);
|
|
142
|
+
const end = text.indexOf(END);
|
|
143
|
+
if (start < 0 || end < start) {
|
|
144
|
+
writeFileSync(path, `${text.replace(/\s*$/, "")}\n\n${block}\n`);
|
|
145
|
+
return "appended";
|
|
146
|
+
}
|
|
147
|
+
const next = text.slice(0, start) + block + text.slice(end + END.length);
|
|
148
|
+
if (next === text)
|
|
149
|
+
return "unchanged";
|
|
150
|
+
writeFileSync(path, next);
|
|
151
|
+
return "updated";
|
|
152
|
+
}
|
|
153
|
+
/** In personal mode, what the block says about the record, which isn't committed there. */
|
|
154
|
+
const RECORD_SENTENCE = "The decision record is committed\nwith the code, so don't paste secrets, credentials or private output into a card.";
|
|
155
|
+
/**
|
|
156
|
+
* What an agent is told in a clone that uses Baesian in personal mode (B-4rwyz), where AGENTS.md says nothing of it: the
|
|
157
|
+
* plugin's session-start hook hands it over once a session (`baesian inbox --quiet`). It's `AGENTS_BLOCK`'s instructions,
|
|
158
|
+
* after a paragraph on what personal mode changes for the agent.
|
|
159
|
+
*/
|
|
160
|
+
export function personalInstructions() {
|
|
161
|
+
const body = AGENTS_BLOCK.slice(BEGIN.length, -END.length).trim();
|
|
162
|
+
if (!body.includes(RECORD_SENTENCE))
|
|
163
|
+
throw new Error("AGENTS_BLOCK no longer says the record is committed in the words personal mode replaces");
|
|
164
|
+
return [
|
|
165
|
+
`Baesian, in personal mode${cite("B-4rwyz")}: this clone uses Baesian for the person you're working with, and the repo's`,
|
|
166
|
+
"collaborators don't, so AGENTS.md doesn't mention it and nothing of Baesian's is committed. Its instructions still apply",
|
|
167
|
+
"to you, as below. The commit gate checks your commits as usual, and takes `Baesian-Card` and `Baesian-Ack` trailers out",
|
|
168
|
+
"of the message once it has, into this clone's ledger. Keep card IDs and Baesian itself out of anything others see: code,",
|
|
169
|
+
"comments, commit messages, pull requests. `baesian inbox` says when this clone's setup needs fixing.",
|
|
170
|
+
"",
|
|
171
|
+
body.replace(RECORD_SENTENCE, "The decision record stays in this\nclone, but a card reaches the reviewer's website in a linked repo, so don't paste secrets, credentials\nor private output into a card."),
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
/** Claude Code reads CLAUDE.md, not AGENTS.md. Warn when CLAUDE.md exists and doesn't import it. */
|
|
175
|
+
export function claudeMdMissesAgents(toplevel) {
|
|
176
|
+
const path = join(toplevel, "CLAUDE.md");
|
|
177
|
+
return existsSync(path) && !/@AGENTS\.md/.test(readFileSync(path, "utf8"));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The card's fields, with their notes, in the template's order: one table, so `baesian template` and the MCP filing
|
|
181
|
+
* tool's schema can't drift apart (D167).
|
|
182
|
+
*/
|
|
183
|
+
export const CARD_FIELDS = [
|
|
184
|
+
{ field: "title", value: '""', note: 'a neutral short name: "Where the access token lives"' },
|
|
185
|
+
{ field: "question", value: '""', note: "the decision, as a neutral question" },
|
|
186
|
+
{ field: "doing", value: '""', note: "what you were doing when it came up: \"writing the server's file watcher\"" },
|
|
187
|
+
{ field: "topic", value: '""', note: "area/subject, naming this one question; reuse one from `baesian topics` only for the same question" },
|
|
188
|
+
{ field: "touches", value: "[]", note: "any of: schema, public-api, dependency, infra, user-facing, publishes, security, privacy, money, data-loss" },
|
|
189
|
+
{ field: "reverse", value: '""', note: "how costly it is to undo: cheap, moderate or one-way" },
|
|
190
|
+
{ field: "proposal", value: '""', note: "your pick, in a few words" },
|
|
191
|
+
{ field: "proposal_then", value: '""', note: "what happens if it's chosen, read after \"then\": \"the relay keeps…\"" },
|
|
192
|
+
{ field: "alternative", value: '""', note: "the strongest alternative, in a few words" },
|
|
193
|
+
{ field: "alternative_then", value: '""', note: "what happens if it's chosen, read after \"then\"" },
|
|
194
|
+
{ field: "crux", value: '""', note: "the one question the choice turns on" },
|
|
195
|
+
{ field: "crux_proposal", value: '""', note: 'the answer that leads to your pick: "If it does"' },
|
|
196
|
+
{ field: "crux_alternative", value: '""', note: "the answer that leads to the alternative: \"If it doesn't\"" },
|
|
197
|
+
{ field: "crux_preference", value: "true", optional: true, note: "the crux is about what the reviewer values, not a fact" },
|
|
198
|
+
{ field: "crux_fact", value: '""', optional: true, note: "what the reviewer needs to choose, kept neutral: the only context they see first" },
|
|
199
|
+
{ field: "rationale", value: '""', optional: true, note: "why you pick the proposal" },
|
|
200
|
+
{ field: "alternative_rationale", value: '""', optional: true, note: "the best case for the alternative: why you'd pick it, if it were your pick" },
|
|
201
|
+
{
|
|
202
|
+
field: "answered_in_chat", value: '""', optional: true,
|
|
203
|
+
note: "only if the reviewer already answered it in chat: what they said, quoted and short; your pick is their answer, which they confirm with one tap",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
field: "wrong_if", value: '""', optional: true,
|
|
207
|
+
note: "what would show your pick is wrong: required if reverse is one-way, touches includes security or privacy, or a premise is set",
|
|
208
|
+
line: "(what would show your pick is wrong: required if reverse is one-way,",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
field: "check", value: '""', optional: true,
|
|
212
|
+
note: "where to look to check wrong_if: required with it",
|
|
213
|
+
line: " touches includes security or privacy, or a premise is set; check says where to look)",
|
|
214
|
+
},
|
|
215
|
+
{ field: "premise", value: '""', optional: true, note: "a claim an option rests on that nobody has checked; give premise_option and premise_check with it", line: "(a claim an option rests on that nobody has checked," },
|
|
216
|
+
{ field: "premise_option", value: "proposal", optional: true, note: "which option the premise rests on: proposal or alternative", line: " which option rests on it," },
|
|
217
|
+
{ field: "premise_check", value: '""', optional: true, note: "how to check the premise", line: " and how to check it)" },
|
|
218
|
+
{ field: "acted", value: '""', optional: true, note: "what you've already built for one option; give acted_option with it", line: "(what you've already built for one option," },
|
|
219
|
+
{ field: "acted_option", value: "proposal", optional: true, note: "which option you've already built for: proposal or alternative", line: " and which)" },
|
|
220
|
+
{ field: "serves", value: '""', optional: true, note: "the criterion your pick serves, when the crux is a preference" },
|
|
221
|
+
{ field: "also_considered", value: "[]", optional: true, note: "other options, by name" },
|
|
222
|
+
{ field: "rules", value: "[R-xxxxx]", optional: true, note: "rules this decision bears on" },
|
|
223
|
+
{ field: "proposal_breaks", value: "[]", optional: true, note: "decisions or rules your pick would replace or break" },
|
|
224
|
+
{ field: "alternative_breaks", value: "[]", optional: true, note: "decisions or rules the alternative would replace or break", line: "" },
|
|
225
|
+
{ field: "level", value: "blocking", optional: true, note: "provisional by default; hard by default for publishes, security, privacy, money, data-loss; a minimum only if the card is shown" },
|
|
226
|
+
{ field: "depends_on", value: "[B-xxxxx]", optional: true, note: "the cards this decision builds on", line: "" },
|
|
227
|
+
{ field: "deck", value: "storage-layer", optional: true, note: "your branch's deck by default" },
|
|
228
|
+
{ field: "supersedes", value: "", template: false, note: "a decision this card argues against, filed with new; it becomes superseded once this card resolves" },
|
|
229
|
+
{ field: "modules_touched", value: "", template: false, note: "how many modules the decision touches" },
|
|
230
|
+
{ field: "confidence", value: "", template: false, note: "your confidence in your pick, from 0 to 1: recorded, not used for routing" },
|
|
231
|
+
{ field: "domain", value: "", template: false, note: "routing tags" },
|
|
232
|
+
{ field: "evidence", value: "", template: false, note: "links that bear on the decision" },
|
|
233
|
+
{
|
|
234
|
+
field: "secret_reason", value: "", template: false,
|
|
235
|
+
note: "why you file a card that looks like it holds a secret: in a hosted repo filing refuses one without it (B-jfkv9). Kept on the card, where reviewers see it",
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
function templateLine(f) {
|
|
239
|
+
const head = f.optional ? `# ${f.field}: ${f.value}` : `${f.field}: ${f.value}`;
|
|
240
|
+
const comment = f.line ?? (f.optional ? `(${f.note})` : `# ${f.note}`);
|
|
241
|
+
// The comments start in one column, and a field too long for it keeps a space before its own.
|
|
242
|
+
return comment ? `${head.padEnd((f.optional ? 27 : 23) - 1)} ${comment}` : head;
|
|
243
|
+
}
|
|
244
|
+
export const CARD_TEMPLATE = `---
|
|
245
|
+
${CARD_FIELDS.filter((f) => f.template !== false).map(templateLine).join("\n")}
|
|
246
|
+
# Below the closing line, optionally: background, shown with your case once the reviewer has chosen.
|
|
247
|
+
---
|
|
248
|
+
`;
|
|
249
|
+
//# sourceMappingURL=agents.js.map
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The fields a verdict may be sent with (`ApiVerdictInput`); anything else is refused. The local server and the relay share them. */
|
|
2
|
+
export const VERDICT_FIELDS = new Set(["card", "revision", "response", "choice", "comment", "first_choice", "make_rule", "retire_rules", "should_have_asked"]);
|
|
3
|
+
/** The fields a preference change may be sent with (`ApiPreferencesInput`); anything else is refused. */
|
|
4
|
+
export const PREFERENCE_FIELDS = new Set([
|
|
5
|
+
"replaces", "preset", "mode", "rate_agent_decides", "rate_sample", "burn_in", "reason", "survey_see", "survey_fine",
|
|
6
|
+
"gate_schema", "gate_infra", "gate_api", "gate_ignore",
|
|
7
|
+
...["public_api", "schema", "dependency", "infra", "user_facing", "security", "privacy", "other"].map((r) => `row_${r}`),
|
|
8
|
+
]);
|
|
9
|
+
/** The fields a practice entry may be sent with (`ApiPracticeInput`); anything else is refused. The local server and the relay share them. */
|
|
10
|
+
export const PRACTICE_FIELDS = new Set(["kind", "card", "revision", "answered", "blind", "final", "test", "mark", "practice"]);
|
|
11
|
+
/** The milliseconds after which a waiting marker counts as gone; the same as the store's. */
|
|
12
|
+
export const WAITING_STALE_MS = 90_000;
|
|
13
|
+
//# sourceMappingURL=api.js.map
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from "./cli.js";
|
|
3
|
+
// A reader that stops early (`baesian show B-x | head -1`) closes the pipe it reads: what's left to write has nowhere to
|
|
4
|
+
// go, so the command ends there, quietly, as a shell's own tools do, rather than with Node's unhandled EPIPE. Any other
|
|
5
|
+
// failure to write is still one.
|
|
6
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
7
|
+
stream.on("error", (e) => {
|
|
8
|
+
if (e.code === "EPIPE")
|
|
9
|
+
process.exit(0);
|
|
10
|
+
throw e;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
process.exitCode = await main();
|
|
14
|
+
//# sourceMappingURL=bin.js.map
|