phaedo-mcp 0.3.14 → 0.3.15
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 +7 -0
- package/agreement.js +1 -1
- package/asks.js +140 -0
- package/package.json +4 -2
- package/pull.js +8 -2
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -528,8 +528,15 @@ Quick readout on your own machine:
|
|
|
528
528
|
cd mcp && npm run receipts # decrypts locally, lists the last 50 (with a short id per line)
|
|
529
529
|
node receipts.js mark <id|prefix|latest> approved # record what YOU did → approved | rejected | modified | unknown
|
|
530
530
|
npm run agreement # per-domain agreement rate (oracle vs your actual decisions)
|
|
531
|
+
npm run asks # are you being asked less? last 4 weeks vs the 4 before
|
|
532
|
+
npm run asks -- --weeks 12 --table # …week by week
|
|
531
533
|
```
|
|
532
534
|
|
|
535
|
+
`npm run asks` is the number Phaedo is judged by: how often it brought an action to you, whether
|
|
536
|
+
that is falling, how often you reversed what it decided alone, and which asks were about things
|
|
537
|
+
that could be undone and you approved as-is (the asks it should learn to drop). `/phaedo:status`
|
|
538
|
+
adds the headline once each half of the window holds 5 checks.
|
|
539
|
+
|
|
533
540
|
`user_action` is **subject-authored**: set it from what *you* decided, not from whether an
|
|
534
541
|
agent proceeded. Until receipts carry one, `npm run agreement` shows `—`
|
|
535
542
|
(no data ≠ 0%); each one you mark makes the rate live. Metric definition:
|
package/agreement.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
export const DECISIVE = ['proceed', 'proceed_with_note', 'decline'];
|
|
31
31
|
export const DEFAULT_WINDOW_DAYS = 30;
|
|
32
32
|
|
|
33
|
-
function agrees(signal, userAction) {
|
|
33
|
+
export function agrees(signal, userAction) {
|
|
34
34
|
if (userAction === 'approved') return signal === 'proceed' || signal === 'proceed_with_note';
|
|
35
35
|
if (userAction === 'rejected') return signal === 'decline';
|
|
36
36
|
return false; // 'modified' — the action was not accepted as-judged
|
package/asks.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// "Are you being asked less?" — the number Phaedo's purpose is judged by.
|
|
3
|
+
//
|
|
4
|
+
// Phaedo's promise: agents stop interrupting you for things you'd have said yes to, and still
|
|
5
|
+
// bring you the irreversible, high-stakes ones. weekly.js counts one week; this reads the local
|
|
6
|
+
// decision records (receipts.js) week by week so the trend shows, together with the two checks
|
|
7
|
+
// that keep a falling number honest:
|
|
8
|
+
//
|
|
9
|
+
// checked — every action an agent brought to Phaedo
|
|
10
|
+
// asked — Phaedo sent it to you (escalate / clarify)
|
|
11
|
+
// quiet — Phaedo decided without asking (proceed / proceed_with_note / decline)
|
|
12
|
+
// overruled — a quiet call you then reversed (agreement.js scoring). The cost of asking less.
|
|
13
|
+
// confirmed — a quiet call you then agreed with
|
|
14
|
+
// avoidable — an ask about something that could be undone, which you approved as-is: the asks
|
|
15
|
+
// Phaedo should learn to stop making. An ask about an irreversible action is never
|
|
16
|
+
// avoidable — those are the subject's to decide.
|
|
17
|
+
// unsure — insufficient_signal: neither an ask nor a call
|
|
18
|
+
//
|
|
19
|
+
// The trend compares the ask rate of the recent half of the window with the half before it, and
|
|
20
|
+
// says nothing until each half holds MIN_TO_COMPARE checks — a rate over a handful is noise.
|
|
21
|
+
// Pure over the receipts list; the CLI reads the local store.
|
|
22
|
+
//
|
|
23
|
+
// npm run asks last 8 weeks, in plain lines
|
|
24
|
+
// npm run asks -- --weeks 12 --table week by week
|
|
25
|
+
|
|
26
|
+
import { readReceipts } from './receipts.js';
|
|
27
|
+
import { defaultStateDir } from './cache.js';
|
|
28
|
+
import { DECISIVE, agrees } from './agreement.js';
|
|
29
|
+
import { isMainModule } from './lib/is-main.js';
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_WEEKS = 8;
|
|
32
|
+
export const MIN_TO_COMPARE = 5;
|
|
33
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
34
|
+
const ASKED = new Set(['escalate', 'clarify']);
|
|
35
|
+
|
|
36
|
+
const empty = () => ({ checked: 0, asked: 0, quiet: 0, overruled: 0, confirmed: 0, avoidable: 0, unsure: 0 });
|
|
37
|
+
|
|
38
|
+
function tally(row, r) {
|
|
39
|
+
const signal = r?.response?.signal;
|
|
40
|
+
const action = r?.user_action;
|
|
41
|
+
if (ASKED.has(signal)) {
|
|
42
|
+
row.checked++; row.asked++;
|
|
43
|
+
if (action === 'approved' && r?.request?.action_descriptor?.reversible === true) row.avoidable++;
|
|
44
|
+
} else if (DECISIVE.includes(signal)) {
|
|
45
|
+
row.checked++; row.quiet++;
|
|
46
|
+
if (action && action !== 'unknown') { if (agrees(signal, action)) row.confirmed++; else row.overruled++; }
|
|
47
|
+
} else if (signal === 'insufficient_signal') {
|
|
48
|
+
row.checked++; row.unsure++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sum(rows) {
|
|
53
|
+
const out = empty();
|
|
54
|
+
for (const row of rows) for (const k of Object.keys(out)) out[k] += row[k];
|
|
55
|
+
out.ask_rate = out.checked ? out.asked / out.checked : null;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* (receipts, { now, weeks }) -> { weeks: [row…] oldest first, recent, earlier, half_weeks, total }.
|
|
61
|
+
* `recent` is the newest half of the window, `earlier` the equal span before it (an odd oldest week
|
|
62
|
+
* is left out of the comparison, never folded into one side).
|
|
63
|
+
*/
|
|
64
|
+
export function asksByWeek(receipts, { now = Date.now(), weeks = DEFAULT_WEEKS } = {}) {
|
|
65
|
+
const n = Math.max(2, Math.floor(Number(weeks)) || DEFAULT_WEEKS);
|
|
66
|
+
const start = now - n * WEEK_MS;
|
|
67
|
+
const rows = Array.from({ length: n }, (_, i) => ({ start: new Date(start + i * WEEK_MS).toISOString(), ...empty() }));
|
|
68
|
+
for (const r of Array.isArray(receipts) ? receipts : []) {
|
|
69
|
+
const t = Date.parse(r?.created_at);
|
|
70
|
+
if (!Number.isFinite(t) || t < start || t > now) continue;
|
|
71
|
+
tally(rows[Math.min(n - 1, Math.floor((t - start) / WEEK_MS))], r);
|
|
72
|
+
}
|
|
73
|
+
for (const row of rows) row.ask_rate = row.checked ? row.asked / row.checked : null;
|
|
74
|
+
const half = Math.floor(n / 2);
|
|
75
|
+
return {
|
|
76
|
+
weeks: rows,
|
|
77
|
+
half_weeks: half,
|
|
78
|
+
recent: sum(rows.slice(n - half)),
|
|
79
|
+
earlier: sum(rows.slice(n - 2 * half, n - half)),
|
|
80
|
+
total: sum(rows),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
85
|
+
const pct = (x) => `${Math.round(x * 100)}%`;
|
|
86
|
+
|
|
87
|
+
/** The headline: how often Phaedo asked lately, and whether that is falling. */
|
|
88
|
+
export function headline(rep) {
|
|
89
|
+
const { recent: r, earlier: e, half_weeks: h } = rep;
|
|
90
|
+
const span = plural(h, 'week');
|
|
91
|
+
if (!r.checked) return `No actions came to Phaedo in the last ${span}.`;
|
|
92
|
+
const first = `In the last ${span} Phaedo checked ${plural(r.checked, 'action')} and asked you about ${r.asked} (${pct(r.ask_rate)}).`;
|
|
93
|
+
if (r.checked < MIN_TO_COMPARE || e.checked < MIN_TO_COMPARE) return `${first} Not enough yet to say whether that is falling.`;
|
|
94
|
+
const diff = Math.round(r.ask_rate * 100) - Math.round(e.ask_rate * 100);
|
|
95
|
+
if (diff === 0) return `${first} That's the same as the ${span} before.`;
|
|
96
|
+
return `${first} That's ${diff < 0 ? 'down' : 'up'} from ${pct(e.ask_rate)} the ${span} before.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Plain lines for a person: the headline, what asking less cost, and the asks it could drop. */
|
|
100
|
+
export function formatAsks(rep) {
|
|
101
|
+
const r = rep.recent;
|
|
102
|
+
const lines = [headline(rep)];
|
|
103
|
+
if (r.quiet) {
|
|
104
|
+
const answered = r.confirmed + r.overruled;
|
|
105
|
+
// An answer on a quiet call means the subject was in the loop anyway — from the ship and
|
|
106
|
+
// agent hooks, only ever because the app showed its own approval prompt. Said plainly: it
|
|
107
|
+
// is the next interruption to remove, not a win to count.
|
|
108
|
+
lines.push(answered
|
|
109
|
+
? `It decided ${plural(r.quiet, 'time')} without asking. You still had to answer ${answered} of those yourself, and reversed ${r.overruled}.`
|
|
110
|
+
: `It decided ${plural(r.quiet, 'time')} without asking; none of those has an answer from you yet.`);
|
|
111
|
+
}
|
|
112
|
+
if (r.avoidable) {
|
|
113
|
+
lines.push(`${r.avoidable === 1 ? 'One ask was' : `${r.avoidable} asks were`} about something that could be undone and you said yes as-is — ${r.avoidable === 1 ? 'that is' : 'those are'} what it should learn to stop asking.`);
|
|
114
|
+
}
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** For /phaedo:status: the headline only once there is a trend to show; '' until then. */
|
|
119
|
+
export function trendLine(rep) {
|
|
120
|
+
return rep.recent.checked >= MIN_TO_COMPARE && rep.earlier.checked >= MIN_TO_COMPARE ? headline(rep) : '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Week by week, for the curious: `--table`. */
|
|
124
|
+
export function formatTable(rep) {
|
|
125
|
+
return rep.weeks.map((w) => {
|
|
126
|
+
const day = new Date(w.start).toLocaleDateString('en-US', { month: 'short', day: '2-digit', timeZone: 'UTC' });
|
|
127
|
+
if (!w.checked) return `${day} —`;
|
|
128
|
+
return `${day} ${String(w.checked).padStart(3)} checked ${String(w.asked).padStart(3)} asked (${pct(w.ask_rate).padStart(4)}) ${w.overruled} reversed ${w.avoidable} could have gone ahead`;
|
|
129
|
+
}).join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isMainModule(import.meta.url)) {
|
|
133
|
+
const args = process.argv.slice(2);
|
|
134
|
+
const i = args.indexOf('--weeks');
|
|
135
|
+
const weeks = i >= 0 ? Number(args[i + 1]) : DEFAULT_WEEKS;
|
|
136
|
+
const receipts = await readReceipts(defaultStateDir()).catch(() => []);
|
|
137
|
+
const rep = asksByWeek(receipts, { weeks });
|
|
138
|
+
console.log(formatAsks(rep));
|
|
139
|
+
if (args.includes('--table')) console.log(`\n${formatTable(rep)}`);
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phaedo-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"receipts-sync.js",
|
|
52
52
|
"agreement.js",
|
|
53
53
|
"weekly.js",
|
|
54
|
+
"asks.js",
|
|
54
55
|
"precedent.js",
|
|
55
56
|
"rules-files.js",
|
|
56
57
|
"learned.js",
|
|
@@ -99,7 +100,7 @@
|
|
|
99
100
|
"unsetup": "node install.mjs --uninstall",
|
|
100
101
|
"pair": "node pair.js",
|
|
101
102
|
"pull": "node pull.js",
|
|
102
|
-
"test": "node test-smoke.mjs && node test-no-profile.mjs && node test-phone-boundary.mjs && node test-cache.mjs && node test-pairing.mjs && node test-revoke.mjs && node test-install.mjs && node test-consult.mjs && node test-consult-policy.mjs && node test-amount-bands.mjs && node test-resolver-revision.mjs && node test-ship-actions.mjs && node test-ship-hook.mjs && node test-ship-readout.mjs && node test-action-binding.mjs && node test-autonomy-promotion.mjs && node test-ap2.mjs && node test-delegation-sync.mjs && node test-delegation-relay.mjs && node test-capture-sync.mjs && node test-capture-hook.mjs && node test-stop-capture.mjs && node test-capture-local.mjs && node test-capture-a11y.mjs && node test-vendored-capture.mjs && node test-escalation.mjs && node test-domain.mjs && node test-receipts.mjs && node test-receipts-sync.mjs && node test-agreement.mjs && node test-weekly.mjs && node test-precedent.mjs && node test-rules-files.mjs && node test-agent-hook.mjs && node test-learned.mjs && node test-history-import.mjs && node test-calibration.mjs && node test-identity.mjs && node test-signing.mjs && node test-sign-request.mjs && node test-signing-audit.mjs && node test-delegation-domain-fragmentation.mjs && node test-stale-authority.mjs && node test-config-home-fold.mjs && node test-precedence-and-ceiling.mjs && node test-package-closure.mjs && node test-plugin.mjs",
|
|
103
|
+
"test": "node test-smoke.mjs && node test-no-profile.mjs && node test-phone-boundary.mjs && node test-cache.mjs && node test-pairing.mjs && node test-revoke.mjs && node test-install.mjs && node test-consult.mjs && node test-consult-policy.mjs && node test-amount-bands.mjs && node test-resolver-revision.mjs && node test-ship-actions.mjs && node test-ship-hook.mjs && node test-ship-readout.mjs && node test-action-binding.mjs && node test-autonomy-promotion.mjs && node test-ap2.mjs && node test-delegation-sync.mjs && node test-delegation-relay.mjs && node test-capture-sync.mjs && node test-capture-hook.mjs && node test-stop-capture.mjs && node test-capture-local.mjs && node test-capture-a11y.mjs && node test-vendored-capture.mjs && node test-escalation.mjs && node test-domain.mjs && node test-receipts.mjs && node test-receipts-sync.mjs && node test-agreement.mjs && node test-weekly.mjs && node test-asks.mjs && node test-precedent.mjs && node test-rules-files.mjs && node test-agent-hook.mjs && node test-learned.mjs && node test-history-import.mjs && node test-calibration.mjs && node test-identity.mjs && node test-signing.mjs && node test-sign-request.mjs && node test-signing-audit.mjs && node test-delegation-domain-fragmentation.mjs && node test-stale-authority.mjs && node test-config-home-fold.mjs && node test-precedence-and-ceiling.mjs && node test-package-closure.mjs && node test-plugin.mjs",
|
|
103
104
|
"receipts": "node receipts.js list",
|
|
104
105
|
"agreement": "node agreement.js",
|
|
105
106
|
"ship-readout": "node ship-readout.js",
|
|
@@ -123,6 +124,7 @@
|
|
|
123
124
|
"smoke:autonomy": "node test-autonomy-promotion.mjs",
|
|
124
125
|
"smoke:ap2": "node test-ap2.mjs",
|
|
125
126
|
"week": "node weekly.js",
|
|
127
|
+
"asks": "node asks.js",
|
|
126
128
|
"rules": "node rules-files.js",
|
|
127
129
|
"learned": "node learned.js",
|
|
128
130
|
"import": "node history-import.js"
|
package/pull.js
CHANGED
|
@@ -11,12 +11,18 @@ import { loadFingerprint, sourceOf, summarizeStarter } from './fingerprint-sourc
|
|
|
11
11
|
import { readReceipts } from './receipts.js';
|
|
12
12
|
import { defaultStateDir } from './cache.js';
|
|
13
13
|
import { weeklyHandled, formatWeekly } from './weekly.js';
|
|
14
|
+
import { asksByWeek, trendLine } from './asks.js';
|
|
14
15
|
import { learnedItems, readLearnedState, reviewLine } from './learned.js';
|
|
15
16
|
|
|
16
17
|
const BASE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
|
|
18
|
-
// Every outcome ends with what Phaedo did for the subject this week — the reason to have it
|
|
19
|
-
|
|
19
|
+
// Every outcome ends with what Phaedo did for the subject this week — the reason to have it —
|
|
20
|
+
// and, once there is enough to compare, whether it is asking them less than before.
|
|
21
|
+
const week = async () => {
|
|
22
|
+
const receipts = await readReceipts(defaultStateDir()).catch(() => []);
|
|
23
|
+
const trend = trendLine(asksByWeek(receipts));
|
|
24
|
+
return ` ${formatWeekly(weeklyHandled(receipts))}\n${trend ? ` ${trend}\n` : ''}`;
|
|
25
|
+
};
|
|
20
26
|
// …and, when there is any, what Phaedo learned that the subject hasn't looked at yet.
|
|
21
27
|
const review = (vd) => { const line = reviewLine(learnedItems(vd), readLearnedState(defaultStateDir())); return line ? ` ${line}\n` : ''; };
|
|
22
28
|
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.phaedo-labs/phaedo-mcp",
|
|
4
4
|
"description": "Make AI apps answer your way, check with you before acting, and get approvals on your phone.",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.15",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/phaedo-labs/phaedo-mcp",
|
|
8
8
|
"source": "github",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "phaedo-mcp",
|
|
16
|
-
"version": "0.3.
|
|
16
|
+
"version": "0.3.15",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|