@cntxt-labs/medha-cli 0.5.0 → 0.6.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/README.md +77 -2
- package/SKILL.md +98 -0
- package/bin/medha.cjs +42 -5
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -115,6 +115,32 @@ Report the result with `medha guard --ok` or `--fail`.
|
|
|
115
115
|
Entity state is a fold over that log, so it can always be rebuilt, audited, or corrected
|
|
116
116
|
(`medha retract`, `medha remove-episode`).
|
|
117
117
|
|
|
118
|
+
**Decision tree.** An entity can carry a tree of *branches*, each a condition plus a decision
|
|
119
|
+
(`apply`, `ignore`, or a probability). Branches earn their own trust from their own evidence, which
|
|
120
|
+
is what you want when a rule only holds in one situation: "always skip migration backups" can be
|
|
121
|
+
excellent advice in CI and wrong on a laptop.
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
medha decision --id migration-notes --condition "in CI" --apply
|
|
125
|
+
medha decision --id migration-notes --condition "on a laptop" --ignore --parent <case-id>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Evidence is then attributed to a branch as well as to the entity, so a branch can read `trusted`
|
|
129
|
+
while the entity as a whole reads `probation`:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
medha record --id migration-notes --signal APPLY --case-id <case-id>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`caseId` must name a branch of that entity — a typo is rejected rather than silently filed against
|
|
136
|
+
the entity. Attributing evidence to a branch never *replaces* the entity-level fold; it is a second
|
|
137
|
+
view of the same signal.
|
|
138
|
+
|
|
139
|
+
Editing a branch (`medha decision --id .. --condition .. --case-id <case-id>`) changes its decision
|
|
140
|
+
and leaves it where it is. Pass `--parent <case-id>` to move it, or `--detach` to promote it to the
|
|
141
|
+
top level. Writes that name an unknown parent, a descendant of themselves, or a condition a sibling
|
|
142
|
+
already uses are rejected.
|
|
143
|
+
|
|
118
144
|
### How trust is computed
|
|
119
145
|
|
|
120
146
|
```
|
|
@@ -163,7 +189,8 @@ Then copy [`SKILL.md`](SKILL.md) to
|
|
|
163
189
|
| `hints` | Batch-fetch trust hints for the entities you are about to rely on. Returns `{ hints, unknown }`; treat `unknown` as probation. |
|
|
164
190
|
| `list_entities` | Paginated search by kind, status, namespace or drift. |
|
|
165
191
|
| `show_entity` | Full detail for one entity: components, temporal state, recent episodes. |
|
|
166
|
-
| `record_signal` | Record `APPLY`, `REJECT_RULE`, `SKIP`, and so on. Returns `recorded: false` for an unknown entity unless `ensure` is set. |
|
|
192
|
+
| `record_signal` | Record `APPLY`, `REJECT_RULE`, `SKIP`, and so on. Returns `recorded: false` for an unknown entity unless `ensure` is set. Pass `caseId` to also teach a decision-tree branch. |
|
|
193
|
+
| `record_decision` | Create or edit a branch of an entity's decision tree. Pass `parentId` to nest it, or `detach: true` to promote it to the top level. Returns the minted `caseId`. |
|
|
167
194
|
| `report_guard` | Record a guard result. |
|
|
168
195
|
| `propose` | Submit a candidate entity. |
|
|
169
196
|
| `drift` | List drifting entities. |
|
|
@@ -186,9 +213,57 @@ host can inject the most trusted guidance without overrunning its context.
|
|
|
186
213
|
|
|
187
214
|
- **Kinds and signals.** Register your own in `.medha/config.json`. A kind can set its own trust
|
|
188
215
|
thresholds and recency half-life, and can weight evidence by a signal's value (for example, a
|
|
189
|
-
timeout counts less against a tool than a crash).
|
|
216
|
+
timeout counts less against a tool than a crash). See [Kind specs](#kind-specs) below.
|
|
190
217
|
- **Weight updaters.** Swap how evidence moves trust with `medha updater list` and `medha updater fork <name>`, which scaffolds a custom one.
|
|
191
218
|
|
|
219
|
+
### Kind specs
|
|
220
|
+
|
|
221
|
+
A **kind spec** gives one kind its own policy. Pass them to `medha init --config <file>`:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"kindSpecs": [
|
|
226
|
+
{
|
|
227
|
+
"name": "lint",
|
|
228
|
+
"description": "linter rules: frequent, cheap evidence",
|
|
229
|
+
"thresholds": { "active": 0.15, "trusted": 0.4, "minUsesForTrusted": 3 },
|
|
230
|
+
"recency": { "halfLifeDays": 20, "floor": 0.2 },
|
|
231
|
+
"evidenceWeighting": "signal-value"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "rule",
|
|
235
|
+
"signalLimits": { "maxSuccessesPerAuthor": 3 },
|
|
236
|
+
"decisionPolicy": { "requireHumanFor": ["apply"] }
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
medha init --config medha.config.json
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`--config` adds to the built-in kinds (`rule`, `recipe`, `tool`) rather than replacing them. A spec
|
|
247
|
+
naming a new kind (`lint` above) also registers it, and a spec naming a built-in kind (`rule`) sets
|
|
248
|
+
that kind's policy. Every field except `name` is optional, and a field you leave out keeps its
|
|
249
|
+
default.
|
|
250
|
+
|
|
251
|
+
| Field | Keys (default) | What it does |
|
|
252
|
+
|---|---|---|
|
|
253
|
+
| `thresholds` | `active` (0.25), `trusted` (0.6), `minUsesForTrusted` (5), `retiredTrustThreshold` (0.1), `minUsesForRetired` (3), `unguardedCeiling` (0.5) | Where the lifecycle transitions sit for this kind. `unguardedCeiling` caps trust while no guard has passed; no threshold lets an unguarded entity become `trusted`. |
|
|
254
|
+
| `recency` | `halfLifeDays` (45), `floor` (0.3) | How fast old evidence fades, and the least it fades to. |
|
|
255
|
+
| `evidenceWeighting` | `"count"` (default) or `"signal-value"` | Count each signal as one trial, or weight it by the signal's value. |
|
|
256
|
+
| `signalLimits` | `minIntervalMs`, `maxSuccessesPerAuthor` (both off) | Limit how much one author can raise trust. Negative evidence is never limited. |
|
|
257
|
+
| `decisionPolicy` | `requireHumanFor`: `"apply"` or a list of `apply`/`ignore`/`probability` (off) | Decision-tree branches of these types need a `human:` author. This is a label the caller sets, not verification: it stops an agent that follows instructions, not one that lies. |
|
|
258
|
+
|
|
259
|
+
Config files are strict: an unknown key, such as `kindPolicies` or `thresholds.bogus`, is refused with
|
|
260
|
+
the list of allowed keys, so a typo cannot quietly leave a policy switched off.
|
|
261
|
+
|
|
262
|
+
To change specs after `init`, edit `.medha/config.json`. There they live under
|
|
263
|
+
`registries.kindSpecs`, and a new kind's name must also be added to `registries.kinds`. Run
|
|
264
|
+
`medha maintain preflight` to check the result. [Extending medha](https://nimishph.github.io/medha/guide/extending)
|
|
265
|
+
covers kinds, signals and updaters in depth.
|
|
266
|
+
|
|
192
267
|
## Maintain it
|
|
193
268
|
|
|
194
269
|
```sh
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: medha
|
|
3
|
+
description: Evidential memory for rules, recipes and tools. Use when deciding how far to trust a review rule, recipe or tool based on how it has actually performed — record uses, rejections and guard results, and read back trust hints. Medha reports evidence; it never decides.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# medha
|
|
7
|
+
|
|
8
|
+
Medha remembers how well rules, recipes and tools have actually worked and returns **trust hints**.
|
|
9
|
+
You record evidence; **you** decide what to do with it.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Run once per project (creates `.medha/`; add it to `.gitignore` or commit it deliberately):
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
medha init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
MCP server (register in `.mcp.json`): `{ "mcpServers": { "medha": { "command": "medha", "args": ["mcp", "serve"] } } }`
|
|
20
|
+
|
|
21
|
+
## Entities
|
|
22
|
+
|
|
23
|
+
An entity is addressed by `namespace` (default empty), `kind` (`rule` | `recipe` | `tool`, default
|
|
24
|
+
`rule`) and `id`. New entities start on **probation**.
|
|
25
|
+
|
|
26
|
+
## Per-kind policy (kindSpecs)
|
|
27
|
+
|
|
28
|
+
A kind can carry its own policy. Set it at init with `medha init --config <file>`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{ "kindSpecs": [
|
|
32
|
+
{ "name": "lint", "thresholds": { "active": 0.15, "trusted": 0.4, "minUsesForTrusted": 3 },
|
|
33
|
+
"recency": { "halfLifeDays": 20 }, "evidenceWeighting": "signal-value" },
|
|
34
|
+
{ "name": "rule", "signalLimits": { "maxSuccessesPerAuthor": 3 },
|
|
35
|
+
"decisionPolicy": { "requireHumanFor": ["apply"] } }
|
|
36
|
+
] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- Allowed keys: `name`, `description`, `thresholds` (`active`, `trusted`, `minUsesForTrusted`,
|
|
40
|
+
`retiredTrustThreshold`, `minUsesForRetired`, `unguardedCeiling`), `recency` (`halfLifeDays`,
|
|
41
|
+
`floor`), `evidenceWeighting` (`count` | `signal-value`), `signalLimits` (`minIntervalMs`,
|
|
42
|
+
`maxSuccessesPerAuthor`), `decisionPolicy` (`requireHumanFor`). An unknown key is an error.
|
|
43
|
+
- A spec for a new kind registers it too; built-in kinds stay. Omitted fields keep their defaults.
|
|
44
|
+
- After init, specs live in `.medha/config.json` under `registries.kindSpecs`, and a new kind's
|
|
45
|
+
name must also be listed in `registries.kinds`. Check with `medha maintain preflight`.
|
|
46
|
+
- `decisionPolicy.requireHumanFor` means **a human must run that command**. If you get
|
|
47
|
+
`CORE_PERMISSION_DENIED`, do not retry with a `human:` author. You can set that label yourself,
|
|
48
|
+
so passing it proves nothing. Escalate to a human instead.
|
|
49
|
+
|
|
50
|
+
## Workflow
|
|
51
|
+
|
|
52
|
+
1. **Read** before relying on a rule: MCP `hints` (pass `compact: true` for cheap output) or
|
|
53
|
+
`medha show --id <id>`. `hints` returns `{ hints, unknown }` — treat `unknown` keys as probation.
|
|
54
|
+
2. **Propose** a new rule: `propose` / `medha propose --id <id> --source <who> [--text ..]`.
|
|
55
|
+
3. **Record** evidence as it happens: `record_signal` / `medha record --id <id> --signal APPLY --ensure`.
|
|
56
|
+
Signals: `APPLY` (used and worked), `REJECT_RULE` (a human rejected it), `SKIP` (not applicable).
|
|
57
|
+
If the entity is unknown and `ensure` is not set, the response says `recorded: false` — nothing was written.
|
|
58
|
+
4. **Report guards**: `report_guard` / `medha guard --id <id> --ok|--fail --guard review`.
|
|
59
|
+
Without a passing guard, trust is capped at 0.5 — usage alone never makes a rule `trusted`.
|
|
60
|
+
5. **Explain**: `medha explain-threshold --id <id>` lists each threshold with ok/no and the numbers.
|
|
61
|
+
6. **Preview** with `simulate` (persists nothing) before recording a consequential signal.
|
|
62
|
+
|
|
63
|
+
## Branching a rule (only when a rule is only good sometimes)
|
|
64
|
+
|
|
65
|
+
A rule that holds in one situation and not another belongs in a decision tree, not in a single
|
|
66
|
+
trust number. `record_decision` mints a branch and returns its `caseId`:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
record_decision { id, condition: "in CI", decision: { type: "apply" } }
|
|
70
|
+
record_decision { id, condition: "on a laptop", decision: { type: "ignore" }, parentId: <first> }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then attribute evidence to the branch, not just the entity:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
record_signal { id, signal: "APPLY", caseId: <branch> }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Read a branch's own trust with `show_entity` — `decisionTree` lists each branch with its `parentId`,
|
|
80
|
+
condition, evidence and status. Branch evidence is *in addition to* the entity's aggregate, never a
|
|
81
|
+
replacement for it, so an entity can be `probation` while one branch is `trusted`.
|
|
82
|
+
|
|
83
|
+
- `caseId` must be a real branch of that entity; a typo is rejected, not filed against the entity.
|
|
84
|
+
- Editing a branch by `caseId` without `parentId` leaves it exactly where it is. Pass `parentId` to
|
|
85
|
+
move it, or `detach: true` to make it a root.
|
|
86
|
+
- Two branches with the same condition under the same parent are rejected — the same condition once
|
|
87
|
+
under each of two roots is fine.
|
|
88
|
+
|
|
89
|
+
## Lifecycle
|
|
90
|
+
|
|
91
|
+
`probation → active → trusted`, and down to `quarantined` / `retired` on repeated rejection.
|
|
92
|
+
`drift` lists entities whose recent behavior diverges from their baseline.
|
|
93
|
+
|
|
94
|
+
## Rules of thumb
|
|
95
|
+
|
|
96
|
+
- Trust is a hint, not a verdict: Wilson lower bound over successes/trials, decayed, gated by guards.
|
|
97
|
+
- Use `--json` on any CLI command for machine output; `--home <dir>` to point at a shared home.
|
|
98
|
+
- `medha maintain preflight` checks store integrity; `medha maintain backup <path>` exports a snapshot.
|
package/bin/medha.cjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// that matches the machine. This file finds it and runs it.
|
|
7
7
|
|
|
8
8
|
const { spawnSync } = require('node:child_process');
|
|
9
|
+
const { chmodSync } = require('node:fs');
|
|
9
10
|
const path = require('node:path');
|
|
10
11
|
|
|
11
12
|
const SUPPORTED = ['linux-x64', 'linux-arm64', 'darwin-arm64', 'darwin-x64', 'win32-x64'];
|
|
@@ -37,6 +38,29 @@ function locate(platform, cpu, resolve) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Restore the executable bit on the program, and say whether it worked.
|
|
43
|
+
*
|
|
44
|
+
* The published tarball is supposed to carry mode 0755, but a package can still arrive without it:
|
|
45
|
+
* an install that strips permissions, a copy through a filesystem or archive tool that does not keep
|
|
46
|
+
* them, or a version published before the mode was fixed. The program is always a program, so the
|
|
47
|
+
* bit is safe to set here — this is the difference between a one-line `chmod` the user has to guess
|
|
48
|
+
* and a command that just runs.
|
|
49
|
+
*/
|
|
50
|
+
function makeExecutable(program) {
|
|
51
|
+
try {
|
|
52
|
+
chmodSync(program, 0o755);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when a spawn failed only because the file was not executable. */
|
|
60
|
+
function isPermissionFailure(error) {
|
|
61
|
+
return error !== undefined && error !== null && error.code === 'EACCES';
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
function main() {
|
|
41
65
|
const found = locate(process.platform, process.arch, require.resolve);
|
|
42
66
|
if (found.problem !== undefined) {
|
|
@@ -44,14 +68,27 @@ function main() {
|
|
|
44
68
|
process.exitCode = 1;
|
|
45
69
|
return;
|
|
46
70
|
}
|
|
47
|
-
const
|
|
48
|
-
stdio: 'inherit',
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
const run = () =>
|
|
72
|
+
spawnSync(found.program, process.argv.slice(2), { stdio: 'inherit', cwd: process.cwd() });
|
|
73
|
+
|
|
74
|
+
let child = run();
|
|
75
|
+
if (
|
|
76
|
+
isPermissionFailure(child.error) &&
|
|
77
|
+
process.platform !== 'win32' &&
|
|
78
|
+
makeExecutable(found.program)
|
|
79
|
+
) {
|
|
80
|
+
child = run();
|
|
81
|
+
}
|
|
51
82
|
if (child.error) {
|
|
52
83
|
process.stderr.write(
|
|
53
84
|
`medha: could not start ${path.basename(found.program)}: ${child.error.message}\n`,
|
|
54
85
|
);
|
|
86
|
+
if (isPermissionFailure(child.error)) {
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
`hint: ${found.program} is not executable and medha could not make it so. ` +
|
|
89
|
+
`Run: chmod +x "${found.program}"\n`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
55
92
|
process.exitCode = 1;
|
|
56
93
|
return;
|
|
57
94
|
}
|
|
@@ -62,6 +99,6 @@ function main() {
|
|
|
62
99
|
process.exitCode = child.status ?? 1;
|
|
63
100
|
}
|
|
64
101
|
|
|
65
|
-
module.exports = { SUPPORTED, platformPackage, locate };
|
|
102
|
+
module.exports = { SUPPORTED, platformPackage, locate, makeExecutable, isPermissionFailure };
|
|
66
103
|
|
|
67
104
|
if (require.main === module) main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntxt-labs/medha-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Evidential memory for rules, recipes and tools: records what happened and returns trust hints. CLI and MCP server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"node": ">=18"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@cntxt-labs/medha-linux-x64": "0.
|
|
44
|
-
"@cntxt-labs/medha-linux-arm64": "0.
|
|
45
|
-
"@cntxt-labs/medha-darwin-arm64": "0.
|
|
46
|
-
"@cntxt-labs/medha-darwin-x64": "0.
|
|
47
|
-
"@cntxt-labs/medha-win32-x64": "0.
|
|
43
|
+
"@cntxt-labs/medha-linux-x64": "0.6.0",
|
|
44
|
+
"@cntxt-labs/medha-linux-arm64": "0.6.0",
|
|
45
|
+
"@cntxt-labs/medha-darwin-arm64": "0.6.0",
|
|
46
|
+
"@cntxt-labs/medha-darwin-x64": "0.6.0",
|
|
47
|
+
"@cntxt-labs/medha-win32-x64": "0.6.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.30.0",
|