@paigy/mcp 0.40.14 → 0.40.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 +14 -0
- package/dist/{chunk-SO5NRTXG.js → chunk-37FS33UP.js} +2 -2
- package/dist/{chunk-R2GTL7N3.js → chunk-PHTQITTC.js} +351 -38
- package/dist/{chunk-NOZRU5CD.js → chunk-SE2JCKDI.js} +42 -34
- package/dist/{chunk-SKLVQ73Q.js → chunk-X6G7XXCP.js} +1 -1
- package/dist/dist-DKP3DJQJ.js +110 -0
- package/dist/enable.js +3 -3
- package/dist/index.js +4 -4
- package/dist/listen.js +9 -8
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +35 -0
- package/dist/statusline.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -215,6 +215,20 @@ script and branch on `$PAIGY_EVENT` there:
|
|
|
215
215
|
codex exec "Paigy goal $PAIGY_GOAL_ID. claim_goal it first. The user said: $PAIGY_TEXT. Reply with contact when done."
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
## Stalled work (Claude Code stop hook)
|
|
219
|
+
|
|
220
|
+
An agent that goes quiet leaves its Goals behind. Three things hand it that work back, all
|
|
221
|
+
reading one rule (no progress for 3 days, `COLD_AFTER_MS`): `check_replies` lists it under
|
|
222
|
+
`stalled`, `paigy-listen` carries it in `PAIGY_WORK` (and launches for it on boot), and this
|
|
223
|
+
hook holds a Claude Code session's stop once a day to list it:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "npx -y -p @paigy/mcp paigy-stalled" }] }] } }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
It is silent when nothing is stalled, when the stop is already being continued by a hook, and
|
|
230
|
+
after it has reminded that session once today.
|
|
231
|
+
|
|
218
232
|
## Statusline (Claude Code)
|
|
219
233
|
|
|
220
234
|
`paigy-statusline` prints which Paigy identity this Claude Code session is, for the
|
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
serverInstructions,
|
|
5
5
|
sessionStartHook,
|
|
6
6
|
withSessionStartHook
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-SE2JCKDI.js";
|
|
8
8
|
import {
|
|
9
9
|
agentName
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-PHTQITTC.js";
|
|
11
11
|
|
|
12
12
|
// src/toolset.ts
|
|
13
13
|
var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact, and any time you're unsure who you are. One call, and it does whatever the situation needs: NOT SET UP \u2192 hatches an identity instantly if this machine holds a device credential (the user ran the Paigy desktop app or harness), otherwise starts the code ceremony; ALREADY SET UP \u2192 returns your current identity and offers the two things left to decide, renaming it or unpairing; TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only. If it returns a `user_code`, print it to the user immediately and call onboard again with the `device_code`. If it returns `enable_command`, PRINT that command for the user to run \u2014 you cannot apply it yourself (it writes your own permission allowlist, which hosts block as privilege escalation), so print it, don't wait for it, and carry on.";
|
|
@@ -79,6 +79,9 @@ function isNetworkError(e) {
|
|
|
79
79
|
async function reach(url, init) {
|
|
80
80
|
return send(url, init, sessionId());
|
|
81
81
|
}
|
|
82
|
+
function reachAs(session) {
|
|
83
|
+
return (url, init) => send(url, init, session);
|
|
84
|
+
}
|
|
82
85
|
async function send(url, init, session) {
|
|
83
86
|
try {
|
|
84
87
|
const headers = {
|
|
@@ -1319,6 +1322,174 @@ var OptionSchema = z.object({
|
|
|
1319
1322
|
var OptionInputSchema = OptionSchema.omit({ id: true }).extend({
|
|
1320
1323
|
label: z.string().trim().min(1).max(1e3)
|
|
1321
1324
|
}).strict();
|
|
1325
|
+
function tzOffsetMinutes(tz, atMs) {
|
|
1326
|
+
try {
|
|
1327
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
1328
|
+
timeZone: tz,
|
|
1329
|
+
hour12: false,
|
|
1330
|
+
year: "numeric",
|
|
1331
|
+
month: "2-digit",
|
|
1332
|
+
day: "2-digit",
|
|
1333
|
+
hour: "2-digit",
|
|
1334
|
+
minute: "2-digit",
|
|
1335
|
+
second: "2-digit"
|
|
1336
|
+
});
|
|
1337
|
+
const p = {};
|
|
1338
|
+
for (const { type, value } of dtf.formatToParts(new Date(atMs))) p[type] = value;
|
|
1339
|
+
const asUtc = Date.UTC(
|
|
1340
|
+
Number(p["year"]),
|
|
1341
|
+
Number(p["month"]) - 1,
|
|
1342
|
+
Number(p["day"]),
|
|
1343
|
+
Number(p["hour"]) === 24 ? 0 : Number(p["hour"]),
|
|
1344
|
+
Number(p["minute"]),
|
|
1345
|
+
Number(p["second"])
|
|
1346
|
+
);
|
|
1347
|
+
return Math.round((asUtc - atMs) / 6e4);
|
|
1348
|
+
} catch {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
function instantAt(tz, local) {
|
|
1353
|
+
const naive = Date.UTC(local.y, local.mo - 1, local.d, local.h, local.mi, 0);
|
|
1354
|
+
const first = tzOffsetMinutes(tz, naive);
|
|
1355
|
+
if (first === null) return null;
|
|
1356
|
+
const guess = naive - first * 6e4;
|
|
1357
|
+
const second = tzOffsetMinutes(tz, guess);
|
|
1358
|
+
return second === null ? guess : naive - second * 6e4;
|
|
1359
|
+
}
|
|
1360
|
+
function localDate(tz, atMs) {
|
|
1361
|
+
const off = tzOffsetMinutes(tz, atMs);
|
|
1362
|
+
if (off === null) return null;
|
|
1363
|
+
const shifted = new Date(atMs + off * 6e4);
|
|
1364
|
+
return {
|
|
1365
|
+
y: shifted.getUTCFullYear(),
|
|
1366
|
+
mo: shifted.getUTCMonth() + 1,
|
|
1367
|
+
d: shifted.getUTCDate(),
|
|
1368
|
+
h: shifted.getUTCHours(),
|
|
1369
|
+
mi: shifted.getUTCMinutes()
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
var PARTS = {
|
|
1373
|
+
earliest: { morning: 8, afternoon: 13, evening: 18, night: 20, noon: 12, midday: 12, lunch: 13 },
|
|
1374
|
+
ring: { morning: 9, afternoon: 13, evening: 18, night: 20, noon: 12, midday: 12, lunch: 13 }
|
|
1375
|
+
};
|
|
1376
|
+
var END_OF_DAY = 17;
|
|
1377
|
+
var VAGUE = /\b(later|in a bit|in a while)\b/i;
|
|
1378
|
+
var TODAY_ONLY = /\b(tonight|this evening)\b/i;
|
|
1379
|
+
var RELATIVE = /\bin\s+(a\s+couple\s+of|a\s+few|a|an|one|two|three|four|five|six|seven|eight|nine|ten|fifteen|twenty|thirty|forty|forty[- ]five|half\s+an|\d{1,3})\s*(minute|minutes|min|mins|hour|hours|hr|hrs|day|days|week|weeks)\b/i;
|
|
1380
|
+
var WORD_NUM = {
|
|
1381
|
+
a: 1,
|
|
1382
|
+
an: 1,
|
|
1383
|
+
one: 1,
|
|
1384
|
+
two: 2,
|
|
1385
|
+
three: 3,
|
|
1386
|
+
four: 4,
|
|
1387
|
+
five: 5,
|
|
1388
|
+
six: 6,
|
|
1389
|
+
seven: 7,
|
|
1390
|
+
eight: 8,
|
|
1391
|
+
nine: 9,
|
|
1392
|
+
ten: 10,
|
|
1393
|
+
fifteen: 15,
|
|
1394
|
+
twenty: 20,
|
|
1395
|
+
thirty: 30,
|
|
1396
|
+
forty: 40,
|
|
1397
|
+
"forty-five": 45,
|
|
1398
|
+
"forty five": 45,
|
|
1399
|
+
// Vague-but-real quantities the SDK's parser read and this one did not (#1367). "A few"
|
|
1400
|
+
// is deliberately small and "a couple" is exactly two — a caller who says either has
|
|
1401
|
+
// named a rough size, and rounding it away to null loses the request entirely.
|
|
1402
|
+
"a couple of": 2,
|
|
1403
|
+
"a few": 3
|
|
1404
|
+
};
|
|
1405
|
+
var CLOCK = /\b(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(a\.?m\.?|p\.?m\.?|o'?clock)?\b/i;
|
|
1406
|
+
var SPOKEN_HOUR = {
|
|
1407
|
+
one: 1,
|
|
1408
|
+
two: 2,
|
|
1409
|
+
three: 3,
|
|
1410
|
+
four: 4,
|
|
1411
|
+
five: 5,
|
|
1412
|
+
six: 6,
|
|
1413
|
+
seven: 7,
|
|
1414
|
+
eight: 8,
|
|
1415
|
+
nine: 9,
|
|
1416
|
+
ten: 10,
|
|
1417
|
+
eleven: 11,
|
|
1418
|
+
twelve: 12
|
|
1419
|
+
};
|
|
1420
|
+
function parseWhen(said2, tz, nowMs, read = "earliest") {
|
|
1421
|
+
const text = said2.trim();
|
|
1422
|
+
if (!text) return null;
|
|
1423
|
+
const rel = RELATIVE.exec(text);
|
|
1424
|
+
if (rel) {
|
|
1425
|
+
const rawN = rel[1].toLowerCase();
|
|
1426
|
+
const half = /half/.test(rawN);
|
|
1427
|
+
const n = half ? 0.5 : WORD_NUM[rawN] ?? Number(rawN);
|
|
1428
|
+
if (Number.isFinite(n) && n > 0) {
|
|
1429
|
+
const unit = rel[2].toLowerCase();
|
|
1430
|
+
const ms = unit.startsWith("min") ? n * 6e4 : unit.startsWith("h") ? n * 36e5 : unit.startsWith("d") ? n * 864e5 : n * 7 * 864e5;
|
|
1431
|
+
return new Date(nowMs + ms).toISOString();
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const today = localDate(tz, nowMs);
|
|
1435
|
+
if (!today) return null;
|
|
1436
|
+
let dayShift = /\btomorrow\b/i.test(text) ? 1 : 0;
|
|
1437
|
+
if (/\bnext week\b/i.test(text)) dayShift = 7;
|
|
1438
|
+
let hour = null;
|
|
1439
|
+
let minute = 0;
|
|
1440
|
+
const spoken = new RegExp(`\\bat\\s+(${Object.keys(SPOKEN_HOUR).join("|")})\\b`, "i").exec(text);
|
|
1441
|
+
const clock2 = CLOCK.exec(text);
|
|
1442
|
+
if (spoken) {
|
|
1443
|
+
hour = SPOKEN_HOUR[spoken[1].toLowerCase()];
|
|
1444
|
+
} else if (clock2 && (clock2[3] || /\bat\s/i.test(clock2[0]))) {
|
|
1445
|
+
hour = Number(clock2[1]);
|
|
1446
|
+
minute = clock2[2] ? Number(clock2[2]) : 0;
|
|
1447
|
+
if (hour > 23 || minute > 59) return null;
|
|
1448
|
+
}
|
|
1449
|
+
if (hour !== null) {
|
|
1450
|
+
const mer = clock2?.[3]?.toLowerCase().replace(/\./g, "") ?? "";
|
|
1451
|
+
const pmWord = /\b(p\.?m\.?|afternoon|evening|tonight|at night)\b/i.test(text);
|
|
1452
|
+
const amWord = /\b(a\.?m\.?|morning)\b/i.test(text);
|
|
1453
|
+
if (mer.startsWith("p") || pmWord && !amWord) {
|
|
1454
|
+
if (hour < 12) hour += 12;
|
|
1455
|
+
} else if (mer.startsWith("a") || amWord) {
|
|
1456
|
+
if (hour === 12) hour = 0;
|
|
1457
|
+
} else if (hour < 12 && dayShift === 0 && hour <= today.h) hour += 12;
|
|
1458
|
+
} else {
|
|
1459
|
+
const parts = PARTS[read];
|
|
1460
|
+
const part = Object.keys(parts).find((k) => new RegExp(`\\b${k}\\b`, "i").test(text));
|
|
1461
|
+
if (/\btonight\b/i.test(text)) hour = parts["night"];
|
|
1462
|
+
else if (part) hour = parts[part];
|
|
1463
|
+
else if (dayShift > 0) hour = 9;
|
|
1464
|
+
else if (/\bend of (the )?day\b|\beod\b/i.test(text)) hour = END_OF_DAY;
|
|
1465
|
+
else if (read === "ring" && VAGUE.test(text)) return new Date(nowMs + 30 * 6e4).toISOString();
|
|
1466
|
+
else return null;
|
|
1467
|
+
}
|
|
1468
|
+
const at2 = instantAt(tz, {
|
|
1469
|
+
y: today.y,
|
|
1470
|
+
mo: today.mo,
|
|
1471
|
+
d: today.d + dayShift,
|
|
1472
|
+
h: hour,
|
|
1473
|
+
mi: minute
|
|
1474
|
+
});
|
|
1475
|
+
if (at2 === null) return null;
|
|
1476
|
+
if (at2 <= nowMs && dayShift === 0 && TODAY_ONLY.test(text)) {
|
|
1477
|
+
return new Date(nowMs + 36e5).toISOString();
|
|
1478
|
+
}
|
|
1479
|
+
const rolled = at2 <= nowMs && dayShift === 0 ? instantAt(tz, {
|
|
1480
|
+
y: today.y,
|
|
1481
|
+
mo: today.mo,
|
|
1482
|
+
d: today.d + 1,
|
|
1483
|
+
h: hour,
|
|
1484
|
+
mi: minute
|
|
1485
|
+
}) : at2;
|
|
1486
|
+
return new Date(rolled ?? at2).toISOString();
|
|
1487
|
+
}
|
|
1488
|
+
function parseDelay(said2, tz, nowMs, read = "earliest") {
|
|
1489
|
+
const at2 = parseWhen(said2, tz, nowMs, read);
|
|
1490
|
+
if (!at2) return null;
|
|
1491
|
+
return Math.max(0, Math.round((Date.parse(at2) - nowMs) / 1e3));
|
|
1492
|
+
}
|
|
1322
1493
|
var NIGHT = { from: 23, to: 7 };
|
|
1323
1494
|
function draft2020(node) {
|
|
1324
1495
|
if (Array.isArray(node)) return node.map(draft2020);
|
|
@@ -1354,7 +1525,7 @@ var AskInputSchema = z2.object({
|
|
|
1354
1525
|
parentId: z2.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
|
|
1355
1526
|
repo: z2.string().optional().describe("Optional repository context."),
|
|
1356
1527
|
ask: z2.string().trim().min(1).max(1e4).describe(
|
|
1357
|
-
"
|
|
1528
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
|
|
1358
1529
|
),
|
|
1359
1530
|
options: z2.array(OptionInputSchema).min(2).max(6).optional()
|
|
1360
1531
|
}).strict();
|
|
@@ -1365,7 +1536,7 @@ var StartContactSchema = z2.object({
|
|
|
1365
1536
|
}).strict();
|
|
1366
1537
|
var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
|
|
1367
1538
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
1368
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}.
|
|
1539
|
+
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals.";
|
|
1369
1540
|
var CreateGoalSchema = z3.object({
|
|
1370
1541
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
1371
1542
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -1413,7 +1584,7 @@ var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).stric
|
|
|
1413
1584
|
var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
|
|
1414
1585
|
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
|
|
1415
1586
|
var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
|
|
1416
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
1587
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
1417
1588
|
var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
|
|
1418
1589
|
var CheckRepliesSchema = z3.object({}).strict();
|
|
1419
1590
|
var AGENT_TOOLS = [
|
|
@@ -1796,32 +1967,6 @@ var AgendaTurnSchema = z4.object({
|
|
|
1796
1967
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
1797
1968
|
var InboxItemSchema = z4.object({
|
|
1798
1969
|
id: z4.string(),
|
|
1799
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
1800
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
1801
|
-
parentId: z4.string().optional(),
|
|
1802
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
1803
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
1804
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
1805
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
1806
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
1807
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
1808
|
-
* thread, which is what the client did for all rows until now. */
|
|
1809
|
-
askId: z4.string().optional(),
|
|
1810
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
1811
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
1812
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
1813
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
1814
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
1815
|
-
seq: z4.number().int().optional(),
|
|
1816
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
1817
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
1818
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
1819
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
1820
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
1821
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
1822
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
1823
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
1824
|
-
units: z4.number().int().positive().optional(),
|
|
1825
1970
|
tokenId: z4.string().optional(),
|
|
1826
1971
|
status: NotifyStatusSchema,
|
|
1827
1972
|
context: ContextSchema,
|
|
@@ -1866,7 +2011,12 @@ var InboxItemSchema = z4.object({
|
|
|
1866
2011
|
entryId: z4.string(),
|
|
1867
2012
|
title: z4.string(),
|
|
1868
2013
|
state: z4.enum(["open", "answered"]),
|
|
1869
|
-
answer: z4.string().nullable()
|
|
2014
|
+
answer: z4.string().nullable(),
|
|
2015
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
2016
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
2017
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
2018
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
2019
|
+
agent: z4.object({ tokenId: z4.string(), name: z4.string(), voice: VoiceKeySchema.optional() }).optional()
|
|
1870
2020
|
})).optional(),
|
|
1871
2021
|
visuals: z4.array(VisualSchema).optional(),
|
|
1872
2022
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -1884,9 +2034,14 @@ var InboxItemSchema = z4.object({
|
|
|
1884
2034
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
1885
2035
|
turn: z4.enum(["you", "agent"]).default("you"),
|
|
1886
2036
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
1887
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
1888
|
-
*
|
|
2037
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
2038
|
+
* error badge + Retry. */
|
|
1889
2039
|
error: z4.string().optional(),
|
|
2040
|
+
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
2041
|
+
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
2042
|
+
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
2043
|
+
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
2044
|
+
cold: z4.string().datetime().optional(),
|
|
1890
2045
|
clarifies: z4.string().optional(),
|
|
1891
2046
|
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
1892
2047
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
@@ -1936,7 +2091,10 @@ var InboxItemSchema = z4.object({
|
|
|
1936
2091
|
goalIds: z4.array(z4.string()),
|
|
1937
2092
|
decisionNeedId: z4.string().optional(),
|
|
1938
2093
|
sealed: z4.boolean(),
|
|
1939
|
-
goalState: z4.string().optional()
|
|
2094
|
+
goalState: z4.string().optional(),
|
|
2095
|
+
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
2096
|
+
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
2097
|
+
goalTitle: z4.string().optional()
|
|
1940
2098
|
}).optional()
|
|
1941
2099
|
});
|
|
1942
2100
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
@@ -2035,7 +2193,6 @@ var UserSettingsSchema = z4.object({
|
|
|
2035
2193
|
});
|
|
2036
2194
|
var HistoryItemSchema = z4.object({
|
|
2037
2195
|
id: z4.string(),
|
|
2038
|
-
parentId: z4.string(),
|
|
2039
2196
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
2040
2197
|
initiator: z4.enum(["user", "agent"]),
|
|
2041
2198
|
title: z4.string(),
|
|
@@ -2194,6 +2351,24 @@ var QueueQuestionSchema = z4.object({
|
|
|
2194
2351
|
* gate at all. */
|
|
2195
2352
|
blocked: z4.boolean().default(false)
|
|
2196
2353
|
});
|
|
2354
|
+
var QueueReplySchema = z4.object({
|
|
2355
|
+
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
2356
|
+
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
2357
|
+
* walk can name it in its zoom. */
|
|
2358
|
+
id: z4.string(),
|
|
2359
|
+
/** The Goal the note is on. */
|
|
2360
|
+
goalId: z4.string(),
|
|
2361
|
+
/** What the note said. */
|
|
2362
|
+
note: z4.string(),
|
|
2363
|
+
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
2364
|
+
deliveryId: z4.string(),
|
|
2365
|
+
requestEntryId: z4.string(),
|
|
2366
|
+
askedAt: z4.string(),
|
|
2367
|
+
/** When the person last replied — the window's start. */
|
|
2368
|
+
repliedAt: z4.string(),
|
|
2369
|
+
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
2370
|
+
reply: z4.string().nullable()
|
|
2371
|
+
});
|
|
2197
2372
|
var QueueItemSchema = z4.object({
|
|
2198
2373
|
id: z4.string(),
|
|
2199
2374
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -2221,6 +2396,10 @@ var QueueItemSchema = z4.object({
|
|
|
2221
2396
|
blocked: z4.boolean().default(false),
|
|
2222
2397
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
2223
2398
|
questions: z4.array(QueueQuestionSchema).default([]),
|
|
2399
|
+
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
2400
|
+
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
2401
|
+
* not spell an empty list. */
|
|
2402
|
+
replies: z4.array(QueueReplySchema).optional(),
|
|
2224
2403
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
2225
2404
|
repo: z4.string().nullable().optional(),
|
|
2226
2405
|
createdAt: z4.string(),
|
|
@@ -2413,6 +2592,7 @@ function sessionSlot(sessionId2) {
|
|
|
2413
2592
|
function agentName() {
|
|
2414
2593
|
return process.env.PAIGY_AGENT || sessionSlot();
|
|
2415
2594
|
}
|
|
2595
|
+
var PROTO = "paigy-pair-v2|sas=24";
|
|
2416
2596
|
var TOKEN_PATH = join2(homedir2(), ".paigy", "token.json");
|
|
2417
2597
|
var KEY_PATH = join2(homedir2(), ".paigy", "key.json");
|
|
2418
2598
|
function readTokenFile() {
|
|
@@ -2453,6 +2633,20 @@ function saveToken(token, agent2 = agentName()) {
|
|
|
2453
2633
|
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
2454
2634
|
});
|
|
2455
2635
|
}
|
|
2636
|
+
function updateSlot(agent2, patch) {
|
|
2637
|
+
return withTokenLock(() => {
|
|
2638
|
+
const slots = readTokenFile();
|
|
2639
|
+
const existing = slots[agent2];
|
|
2640
|
+
if (!existing) return false;
|
|
2641
|
+
slots[agent2] = { ...existing, ...patch };
|
|
2642
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
2643
|
+
return true;
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
function slotIdentity(agent2) {
|
|
2647
|
+
const t = readTokenFile()[agent2];
|
|
2648
|
+
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null };
|
|
2649
|
+
}
|
|
2456
2650
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2457
2651
|
function readToken(agent2 = agentName()) {
|
|
2458
2652
|
if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
|
|
@@ -2603,6 +2797,14 @@ async function setIdentity(patch, opts = {}) {
|
|
|
2603
2797
|
if (!res.ok) throw new Error(`set_identity failed: ${res.status} ${await res.text()}`);
|
|
2604
2798
|
return await res.json();
|
|
2605
2799
|
}
|
|
2800
|
+
async function claimSessions(opts = {}) {
|
|
2801
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
2802
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
2803
|
+
// the HOST's identity
|
|
2804
|
+
}));
|
|
2805
|
+
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
2806
|
+
return (await res.json()).sessions;
|
|
2807
|
+
}
|
|
2606
2808
|
async function heartbeat(runtime, opts = {}) {
|
|
2607
2809
|
const body = {
|
|
2608
2810
|
...runtime !== void 0 ? { runtime } : {},
|
|
@@ -2624,6 +2826,53 @@ async function registerDelivery(mode, opts = {}) {
|
|
|
2624
2826
|
if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
|
|
2625
2827
|
return await res.json();
|
|
2626
2828
|
}
|
|
2829
|
+
async function listNotes(opts = {}) {
|
|
2830
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
|
|
2831
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
2832
|
+
}));
|
|
2833
|
+
if (!res.ok) throw new Error(`list_notes failed: ${res.status} ${await res.text()}`);
|
|
2834
|
+
return await res.json();
|
|
2835
|
+
}
|
|
2836
|
+
async function listConnections(opts = {}) {
|
|
2837
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
2838
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}`, "x-paigy-model": "goal-entry-v1" }
|
|
2839
|
+
}));
|
|
2840
|
+
if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
|
|
2841
|
+
return await res.json();
|
|
2842
|
+
}
|
|
2843
|
+
async function submitTriage(run, opts = {}) {
|
|
2844
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
2845
|
+
method: "POST",
|
|
2846
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
2847
|
+
body: JSON.stringify(run)
|
|
2848
|
+
}));
|
|
2849
|
+
if (!res.ok) throw new Error(`submit_triage failed: ${res.status} ${await res.text()}`);
|
|
2850
|
+
return await res.json();
|
|
2851
|
+
}
|
|
2852
|
+
async function getTriage(opts = {}) {
|
|
2853
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
2854
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
2855
|
+
}));
|
|
2856
|
+
if (!res.ok) throw new Error(`get_triage failed: ${res.status} ${await res.text()}`);
|
|
2857
|
+
return await res.json();
|
|
2858
|
+
}
|
|
2859
|
+
async function acceptTriage(proposalId, body, opts = {}) {
|
|
2860
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
|
|
2861
|
+
method: "POST",
|
|
2862
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
2863
|
+
body: JSON.stringify(body)
|
|
2864
|
+
}));
|
|
2865
|
+
if (!res.ok) throw new Error(`accept_triage failed: ${res.status} ${await res.text()}`);
|
|
2866
|
+
return await res.json();
|
|
2867
|
+
}
|
|
2868
|
+
async function dismissTriage(proposalId, opts = {}) {
|
|
2869
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/dismiss`, {
|
|
2870
|
+
method: "POST",
|
|
2871
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
2872
|
+
}));
|
|
2873
|
+
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
2874
|
+
return await res.json();
|
|
2875
|
+
}
|
|
2627
2876
|
function repoFromRemote(url) {
|
|
2628
2877
|
const said = (url ?? "").trim();
|
|
2629
2878
|
if (!said) return null;
|
|
@@ -2685,6 +2934,9 @@ async function contact(input, opts = {}) {
|
|
|
2685
2934
|
if (!res.ok) await fail("batch contact", res);
|
|
2686
2935
|
const receipt = await res.json();
|
|
2687
2936
|
deliveries = receipt.deliveries ?? [];
|
|
2937
|
+
if (deliveries.length === 0 && receipt.recorded?.length) {
|
|
2938
|
+
return { recorded: receipt.recorded, message: receipt.message ?? "Recorded as progress on its Goal; nothing was sent to the person." };
|
|
2939
|
+
}
|
|
2688
2940
|
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
2689
2941
|
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
2690
2942
|
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
@@ -2776,6 +3028,14 @@ function goalView(g) {
|
|
|
2776
3028
|
owner: g.ownerParticipant,
|
|
2777
3029
|
parentGoalId: g.parentGoalId,
|
|
2778
3030
|
childGoalIds: g.childGoalIds,
|
|
3031
|
+
// EVERYTHING BELOW, read through this Goal (umbrella reads): one line per Goal, indented by
|
|
3032
|
+
// depth, then each question with its answer — so an agent that asked through an umbrella
|
|
3033
|
+
// reads every answer in one call, whoever owns the child it landed on.
|
|
3034
|
+
subtree: g.subtree?.length ? g.subtree.map((c) => compact({
|
|
3035
|
+
goal: `${" ".repeat(c.depth - 1)}${c.title} (${c.state})`,
|
|
3036
|
+
goalId: c.goalId,
|
|
3037
|
+
questions: c.questions.length ? c.questions.map((q) => q.answer ? `${q.question} \u2192 ${q.answer}` : `${q.question} \u2192 (open)`) : void 0
|
|
3038
|
+
})) : void 0,
|
|
2779
3039
|
blockedBy: g.blockedDependencyGoalIds,
|
|
2780
3040
|
dueAt: g.dueAt,
|
|
2781
3041
|
leaseExpiresAt: g.leaseExpiresAt,
|
|
@@ -2797,6 +3057,8 @@ function deliveryView(d) {
|
|
|
2797
3057
|
}
|
|
2798
3058
|
function repliesView(r) {
|
|
2799
3059
|
const deliveries = r.deliveries ?? [];
|
|
3060
|
+
const stalled = r.stalled ?? [];
|
|
3061
|
+
const others = r.stalledOthers ?? [];
|
|
2800
3062
|
return {
|
|
2801
3063
|
deliveries: deliveries.map((d) => compact({
|
|
2802
3064
|
deliveryId: d.deliveryId,
|
|
@@ -2805,15 +3067,26 @@ function repliesView(r) {
|
|
|
2805
3067
|
openDecisions: d.decisionNeeds.filter((n) => n.state === "open").length || void 0,
|
|
2806
3068
|
latest: "about" in d ? d.about : void 0
|
|
2807
3069
|
})),
|
|
2808
|
-
|
|
3070
|
+
// YOUR STALLED WORK (#2257): Goals you own with no progress for three days. Named here so an
|
|
3071
|
+
// agent that reads its replies also sees what it left, and says what to do about each.
|
|
3072
|
+
...stalled.length ? { stalled } : {},
|
|
3073
|
+
// OTHER AGENTS' STALLED WORK (owner, 2026-09-23): any agent may take it over and close it.
|
|
3074
|
+
...others.length ? { stalledOthers: others } : {},
|
|
3075
|
+
next: [
|
|
3076
|
+
deliveries.length ? "Act on a Goal with claim_goal; reread one Delivery in full with contact({deliveryId})." : "Nothing is waiting.",
|
|
3077
|
+
...stalled.length ? [`${stalled.length} of your Goals have had no progress for 3 days: update_goal each with progress, finish it, or cancel it.`] : [],
|
|
3078
|
+
...others.length ? [`${others.length} of other agents' Goals have gone quiet for 3 days: claim_goal({goalId}) takes one over, then finish or cancel it with update_goal.`] : []
|
|
3079
|
+
].join(" ")
|
|
2809
3080
|
};
|
|
2810
3081
|
}
|
|
2811
3082
|
async function runTool(name, args, opts) {
|
|
2812
3083
|
const { waits, signal, ...client } = opts;
|
|
2813
3084
|
const input = args ?? {};
|
|
2814
3085
|
switch (name) {
|
|
2815
|
-
case "contact":
|
|
2816
|
-
|
|
3086
|
+
case "contact": {
|
|
3087
|
+
const sent = await contact(input, { ...client, signal, waits });
|
|
3088
|
+
return "recorded" in sent ? sent : deliveryView(sent);
|
|
3089
|
+
}
|
|
2817
3090
|
case "check_replies":
|
|
2818
3091
|
CheckRepliesSchema.parse(input);
|
|
2819
3092
|
return repliesView(await checkReplies(client));
|
|
@@ -2935,14 +3208,37 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
2935
3208
|
}
|
|
2936
3209
|
};
|
|
2937
3210
|
}
|
|
3211
|
+
function parseDue(detail, now = /* @__PURE__ */ new Date()) {
|
|
3212
|
+
const tz = machineZone();
|
|
3213
|
+
if (!tz) return null;
|
|
3214
|
+
return parseDelay(detail, tz, now.getTime(), "ring");
|
|
3215
|
+
}
|
|
3216
|
+
function machineZone() {
|
|
3217
|
+
try {
|
|
3218
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
3219
|
+
} catch {
|
|
3220
|
+
return null;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
2938
3223
|
|
|
2939
3224
|
export {
|
|
3225
|
+
BACKEND_URL,
|
|
3226
|
+
NETWORK_MSG,
|
|
2940
3227
|
sessionId,
|
|
2941
3228
|
isNetworkError,
|
|
3229
|
+
reach,
|
|
3230
|
+
reachAs,
|
|
3231
|
+
AGENT_TOOLS,
|
|
3232
|
+
AGENT_TOOL_NAMES,
|
|
3233
|
+
NotifyRequestSchema,
|
|
2942
3234
|
sessionSlot,
|
|
2943
3235
|
agentName,
|
|
3236
|
+
PROTO,
|
|
2944
3237
|
TOKEN_PATH,
|
|
3238
|
+
KEY_PATH,
|
|
2945
3239
|
saveToken,
|
|
3240
|
+
updateSlot,
|
|
3241
|
+
slotIdentity,
|
|
2946
3242
|
sleep,
|
|
2947
3243
|
readToken,
|
|
2948
3244
|
listSlots,
|
|
@@ -2952,14 +3248,31 @@ export {
|
|
|
2952
3248
|
requestCode,
|
|
2953
3249
|
pollToken,
|
|
2954
3250
|
UnpairedError,
|
|
3251
|
+
ApiError,
|
|
2955
3252
|
overrideToken,
|
|
3253
|
+
authToken,
|
|
3254
|
+
createGoal,
|
|
3255
|
+
claimGoal,
|
|
2956
3256
|
getGoal,
|
|
3257
|
+
updateGoal,
|
|
3258
|
+
AWAIT_WINDOW_MS,
|
|
2957
3259
|
hatch,
|
|
2958
3260
|
whoAmI,
|
|
2959
3261
|
setIdentity,
|
|
3262
|
+
claimSessions,
|
|
2960
3263
|
heartbeat,
|
|
3264
|
+
registerDelivery,
|
|
3265
|
+
listNotes,
|
|
3266
|
+
listConnections,
|
|
3267
|
+
submitTriage,
|
|
3268
|
+
getTriage,
|
|
3269
|
+
acceptTriage,
|
|
3270
|
+
dismissTriage,
|
|
3271
|
+
repoFromRemote,
|
|
3272
|
+
currentRepo,
|
|
2961
3273
|
contact,
|
|
2962
3274
|
checkReplies,
|
|
2963
3275
|
runTool,
|
|
2964
|
-
subscribeWake
|
|
3276
|
+
subscribeWake,
|
|
3277
|
+
parseDue
|
|
2965
3278
|
};
|
|
@@ -57,7 +57,7 @@ var AskInputSchema = z2.object({
|
|
|
57
57
|
parentId: z2.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
|
|
58
58
|
repo: z2.string().optional().describe("Optional repository context."),
|
|
59
59
|
ask: z2.string().trim().min(1).max(1e4).describe(
|
|
60
|
-
"
|
|
60
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
|
|
61
61
|
),
|
|
62
62
|
options: z2.array(OptionInputSchema).min(2).max(6).optional()
|
|
63
63
|
}).strict();
|
|
@@ -68,7 +68,7 @@ var StartContactSchema = z2.object({
|
|
|
68
68
|
}).strict();
|
|
69
69
|
var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
|
|
70
70
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
71
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}.
|
|
71
|
+
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals.";
|
|
72
72
|
var CreateGoalSchema = z3.object({
|
|
73
73
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
74
74
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -116,7 +116,7 @@ var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).stric
|
|
|
116
116
|
var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
|
|
117
117
|
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
|
|
118
118
|
var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
|
|
119
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
119
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
120
120
|
var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
|
|
121
121
|
var CheckRepliesSchema = z3.object({}).strict();
|
|
122
122
|
var AGENT_TOOLS = [
|
|
@@ -509,32 +509,6 @@ var AgendaTurnSchema = z4.object({
|
|
|
509
509
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
510
510
|
var InboxItemSchema = z4.object({
|
|
511
511
|
id: z4.string(),
|
|
512
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
513
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
514
|
-
parentId: z4.string().optional(),
|
|
515
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
516
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
517
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
518
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
519
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
520
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
521
|
-
* thread, which is what the client did for all rows until now. */
|
|
522
|
-
askId: z4.string().optional(),
|
|
523
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
524
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
525
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
526
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
527
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
528
|
-
seq: z4.number().int().optional(),
|
|
529
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
530
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
531
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
532
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
533
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
534
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
535
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
536
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
537
|
-
units: z4.number().int().positive().optional(),
|
|
538
512
|
tokenId: z4.string().optional(),
|
|
539
513
|
status: NotifyStatusSchema,
|
|
540
514
|
context: ContextSchema,
|
|
@@ -579,7 +553,12 @@ var InboxItemSchema = z4.object({
|
|
|
579
553
|
entryId: z4.string(),
|
|
580
554
|
title: z4.string(),
|
|
581
555
|
state: z4.enum(["open", "answered"]),
|
|
582
|
-
answer: z4.string().nullable()
|
|
556
|
+
answer: z4.string().nullable(),
|
|
557
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
558
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
559
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
560
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
561
|
+
agent: z4.object({ tokenId: z4.string(), name: z4.string(), voice: VoiceKeySchema.optional() }).optional()
|
|
583
562
|
})).optional(),
|
|
584
563
|
visuals: z4.array(VisualSchema).optional(),
|
|
585
564
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -597,9 +576,14 @@ var InboxItemSchema = z4.object({
|
|
|
597
576
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
598
577
|
turn: z4.enum(["you", "agent"]).default("you"),
|
|
599
578
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
600
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
601
|
-
*
|
|
579
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
580
|
+
* error badge + Retry. */
|
|
602
581
|
error: z4.string().optional(),
|
|
582
|
+
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
583
|
+
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
584
|
+
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
585
|
+
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
586
|
+
cold: z4.string().datetime().optional(),
|
|
603
587
|
clarifies: z4.string().optional(),
|
|
604
588
|
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
605
589
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
@@ -649,7 +633,10 @@ var InboxItemSchema = z4.object({
|
|
|
649
633
|
goalIds: z4.array(z4.string()),
|
|
650
634
|
decisionNeedId: z4.string().optional(),
|
|
651
635
|
sealed: z4.boolean(),
|
|
652
|
-
goalState: z4.string().optional()
|
|
636
|
+
goalState: z4.string().optional(),
|
|
637
|
+
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
638
|
+
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
639
|
+
goalTitle: z4.string().optional()
|
|
653
640
|
}).optional()
|
|
654
641
|
});
|
|
655
642
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
@@ -748,7 +735,6 @@ var UserSettingsSchema = z4.object({
|
|
|
748
735
|
});
|
|
749
736
|
var HistoryItemSchema = z4.object({
|
|
750
737
|
id: z4.string(),
|
|
751
|
-
parentId: z4.string(),
|
|
752
738
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
753
739
|
initiator: z4.enum(["user", "agent"]),
|
|
754
740
|
title: z4.string(),
|
|
@@ -907,6 +893,24 @@ var QueueQuestionSchema = z4.object({
|
|
|
907
893
|
* gate at all. */
|
|
908
894
|
blocked: z4.boolean().default(false)
|
|
909
895
|
});
|
|
896
|
+
var QueueReplySchema = z4.object({
|
|
897
|
+
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
898
|
+
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
899
|
+
* walk can name it in its zoom. */
|
|
900
|
+
id: z4.string(),
|
|
901
|
+
/** The Goal the note is on. */
|
|
902
|
+
goalId: z4.string(),
|
|
903
|
+
/** What the note said. */
|
|
904
|
+
note: z4.string(),
|
|
905
|
+
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
906
|
+
deliveryId: z4.string(),
|
|
907
|
+
requestEntryId: z4.string(),
|
|
908
|
+
askedAt: z4.string(),
|
|
909
|
+
/** When the person last replied — the window's start. */
|
|
910
|
+
repliedAt: z4.string(),
|
|
911
|
+
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
912
|
+
reply: z4.string().nullable()
|
|
913
|
+
});
|
|
910
914
|
var QueueItemSchema = z4.object({
|
|
911
915
|
id: z4.string(),
|
|
912
916
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -934,6 +938,10 @@ var QueueItemSchema = z4.object({
|
|
|
934
938
|
blocked: z4.boolean().default(false),
|
|
935
939
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
936
940
|
questions: z4.array(QueueQuestionSchema).default([]),
|
|
941
|
+
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
942
|
+
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
943
|
+
* not spell an empty list. */
|
|
944
|
+
replies: z4.array(QueueReplySchema).optional(),
|
|
937
945
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
938
946
|
repo: z4.string().nullable().optional(),
|
|
939
947
|
createdAt: z4.string(),
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_TOOLS,
|
|
3
|
+
AGENT_TOOL_NAMES,
|
|
4
|
+
AWAIT_WINDOW_MS,
|
|
5
|
+
ApiError,
|
|
6
|
+
BACKEND_URL,
|
|
7
|
+
KEY_PATH,
|
|
8
|
+
NETWORK_MSG,
|
|
9
|
+
NotifyRequestSchema,
|
|
10
|
+
PROTO,
|
|
11
|
+
TOKEN_PATH,
|
|
12
|
+
UnpairedError,
|
|
13
|
+
acceptTriage,
|
|
14
|
+
agentName,
|
|
15
|
+
authToken,
|
|
16
|
+
checkReplies,
|
|
17
|
+
claimGoal,
|
|
18
|
+
claimSessions,
|
|
19
|
+
contact,
|
|
20
|
+
createGoal,
|
|
21
|
+
currentRepo,
|
|
22
|
+
deleteToken,
|
|
23
|
+
dismissTriage,
|
|
24
|
+
getGoal,
|
|
25
|
+
getTriage,
|
|
26
|
+
hatch,
|
|
27
|
+
heartbeat,
|
|
28
|
+
isNetworkError,
|
|
29
|
+
listConnections,
|
|
30
|
+
listNotes,
|
|
31
|
+
listSlots,
|
|
32
|
+
overrideToken,
|
|
33
|
+
parseDue,
|
|
34
|
+
pollToken,
|
|
35
|
+
reach,
|
|
36
|
+
reachAs,
|
|
37
|
+
readToken,
|
|
38
|
+
registerDelivery,
|
|
39
|
+
repoFromRemote,
|
|
40
|
+
requestCode,
|
|
41
|
+
revokeToken,
|
|
42
|
+
runTool,
|
|
43
|
+
saveToken,
|
|
44
|
+
sessionId,
|
|
45
|
+
sessionSlot,
|
|
46
|
+
setIdentity,
|
|
47
|
+
sleep,
|
|
48
|
+
slotIdentity,
|
|
49
|
+
slotName,
|
|
50
|
+
submitTriage,
|
|
51
|
+
subscribeWake,
|
|
52
|
+
updateGoal,
|
|
53
|
+
updateSlot,
|
|
54
|
+
whoAmI
|
|
55
|
+
} from "./chunk-PHTQITTC.js";
|
|
56
|
+
export {
|
|
57
|
+
AGENT_TOOLS,
|
|
58
|
+
AGENT_TOOL_NAMES,
|
|
59
|
+
AWAIT_WINDOW_MS,
|
|
60
|
+
ApiError,
|
|
61
|
+
BACKEND_URL,
|
|
62
|
+
KEY_PATH,
|
|
63
|
+
NETWORK_MSG,
|
|
64
|
+
NotifyRequestSchema,
|
|
65
|
+
PROTO,
|
|
66
|
+
TOKEN_PATH,
|
|
67
|
+
UnpairedError,
|
|
68
|
+
acceptTriage,
|
|
69
|
+
agentName,
|
|
70
|
+
authToken,
|
|
71
|
+
checkReplies,
|
|
72
|
+
claimGoal,
|
|
73
|
+
claimSessions,
|
|
74
|
+
contact,
|
|
75
|
+
createGoal,
|
|
76
|
+
currentRepo,
|
|
77
|
+
deleteToken,
|
|
78
|
+
dismissTriage,
|
|
79
|
+
getGoal,
|
|
80
|
+
getTriage,
|
|
81
|
+
hatch,
|
|
82
|
+
heartbeat,
|
|
83
|
+
isNetworkError,
|
|
84
|
+
listConnections,
|
|
85
|
+
listNotes,
|
|
86
|
+
listSlots,
|
|
87
|
+
overrideToken,
|
|
88
|
+
parseDue,
|
|
89
|
+
pollToken,
|
|
90
|
+
reach,
|
|
91
|
+
reachAs,
|
|
92
|
+
readToken,
|
|
93
|
+
registerDelivery,
|
|
94
|
+
repoFromRemote,
|
|
95
|
+
requestCode,
|
|
96
|
+
revokeToken,
|
|
97
|
+
runTool,
|
|
98
|
+
saveToken,
|
|
99
|
+
sessionId,
|
|
100
|
+
sessionSlot,
|
|
101
|
+
setIdentity,
|
|
102
|
+
sleep,
|
|
103
|
+
slotIdentity,
|
|
104
|
+
slotName,
|
|
105
|
+
submitTriage,
|
|
106
|
+
subscribeWake,
|
|
107
|
+
updateGoal,
|
|
108
|
+
updateSlot,
|
|
109
|
+
whoAmI
|
|
110
|
+
};
|
package/dist/enable.js
CHANGED
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
PAIGY_TOOL_IDS,
|
|
4
4
|
enablePaigyTools,
|
|
5
5
|
installSessionListening
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-37FS33UP.js";
|
|
7
|
+
import "./chunk-SE2JCKDI.js";
|
|
8
|
+
import "./chunk-PHTQITTC.js";
|
|
9
9
|
|
|
10
10
|
// src/enable.ts
|
|
11
11
|
function main() {
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
resolvePairing,
|
|
6
6
|
startPairing,
|
|
7
7
|
suggestedAgentName
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-X6G7XXCP.js";
|
|
9
9
|
import {
|
|
10
10
|
ENABLE_COMMAND,
|
|
11
11
|
OnboardSchema,
|
|
@@ -14,11 +14,11 @@ import {
|
|
|
14
14
|
SERVER_INSTRUCTIONS,
|
|
15
15
|
TOOLS,
|
|
16
16
|
paigyToolsAllowlisted
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-37FS33UP.js";
|
|
18
18
|
import {
|
|
19
19
|
decideListen,
|
|
20
20
|
listenerAlive
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-SE2JCKDI.js";
|
|
22
22
|
import {
|
|
23
23
|
clearSurface,
|
|
24
24
|
writeSurface
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
setIdentity,
|
|
40
40
|
slotName,
|
|
41
41
|
whoAmI
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-PHTQITTC.js";
|
|
43
43
|
|
|
44
44
|
// src/index.ts
|
|
45
45
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
package/dist/listen.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
entryWords,
|
|
4
4
|
removeListenMark,
|
|
5
5
|
writeListenMark
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SE2JCKDI.js";
|
|
7
7
|
import {
|
|
8
8
|
agentName,
|
|
9
9
|
checkReplies,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
getGoal,
|
|
12
12
|
slotName,
|
|
13
13
|
subscribeWake
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-PHTQITTC.js";
|
|
15
15
|
|
|
16
16
|
// src/listen.ts
|
|
17
17
|
import { spawn } from "child_process";
|
|
@@ -132,11 +132,12 @@ function leadOf(deliveries, goalId) {
|
|
|
132
132
|
}
|
|
133
133
|
function brief(waiting, reason, as, goalId) {
|
|
134
134
|
const lead = leadOf(waiting.deliveries, goalId);
|
|
135
|
-
|
|
135
|
+
const stalled = waiting.stalled?.length ? ` \xB7 ${waiting.stalled.length} of your Goals stalled 3+ days: update, finish or cancel each` : "";
|
|
136
|
+
if (!lead) return (reason === "boot" ? `Paigy \xB7 listening as ${as} \xB7 nothing waiting` : `Paigy \xB7 ${reason} \xB7 nothing waiting`) + stalled;
|
|
136
137
|
const said = saidIn(lead) || ("about" in lead ? lead.about : "");
|
|
137
138
|
const shown = said.length > 160 ? `${said.slice(0, 160)}\u2026` : said;
|
|
138
139
|
const goal = waiting.goal?.goalId ?? lead.goalIds[0];
|
|
139
|
-
return `Paigy \xB7 ${reason} \xB7 ${waiting.deliveries.length} waiting \xB7 lead ${lead.deliveryId} on Goal ${goal}: "${shown}"`;
|
|
140
|
+
return `Paigy \xB7 ${reason} \xB7 ${waiting.deliveries.length} waiting \xB7 lead ${lead.deliveryId} on Goal ${goal}: "${shown}"${stalled}`;
|
|
140
141
|
}
|
|
141
142
|
function launchEnv(waiting, reason, goalId) {
|
|
142
143
|
const env = { PAIGY_WORK: JSON.stringify(waiting), PAIGY_EVENT: reason };
|
|
@@ -153,16 +154,16 @@ function launchEnv(waiting, reason, goalId) {
|
|
|
153
154
|
async function sweep(reason, goalId) {
|
|
154
155
|
try {
|
|
155
156
|
const cmd = process.env.PAIGY_ON_WAKE;
|
|
156
|
-
const { deliveries: rows } = await checkReplies();
|
|
157
|
+
const { deliveries: rows, stalled } = await checkReplies();
|
|
157
158
|
const lead = leadOf(rows, goalId);
|
|
158
|
-
const full = lead ? await contact({ deliveryId: lead.deliveryId }, { waits: false }).catch(() => null) : null;
|
|
159
|
+
const full = lead ? await contact({ deliveryId: lead.deliveryId }, { waits: false }).then((d) => "recorded" in d ? null : d).catch(() => null) : null;
|
|
159
160
|
const deliveries = rows.map((d) => d === lead && full ? full : d);
|
|
160
161
|
const leadGoalId = lead?.goalIds[0];
|
|
161
162
|
const goal = leadGoalId ? await getGoal(leadGoalId).then((g) => ({ goalId: leadGoalId, progress: g.progress })).catch(() => null) : null;
|
|
162
|
-
const waiting = { deliveries, goal };
|
|
163
|
+
const waiting = { deliveries, goal, ...stalled?.length ? { stalled } : {} };
|
|
163
164
|
if (BRIEF) line(brief(waiting, reason, listeningAs(), goalId));
|
|
164
165
|
else emit({ type: "sweep", reason, ...goalId ? { about: goalId } : {}, ...waiting });
|
|
165
|
-
if (cmd && deliveries.length) {
|
|
166
|
+
if (cmd && (deliveries.length || reason === "boot" && waiting.stalled?.length)) {
|
|
166
167
|
spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, ...launchEnv(waiting, reason, goalId) } });
|
|
167
168
|
}
|
|
168
169
|
} catch (e) {
|
package/dist/onboard.js
CHANGED
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
resolvePairing,
|
|
4
4
|
startPairing,
|
|
5
5
|
suggestedAgentName
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-X6G7XXCP.js";
|
|
7
7
|
import {
|
|
8
8
|
autoConfigureClients,
|
|
9
9
|
claudeInstallHint,
|
|
10
10
|
openBrowser
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-37FS33UP.js";
|
|
12
|
+
import "./chunk-SE2JCKDI.js";
|
|
13
13
|
import {
|
|
14
14
|
TOKEN_PATH,
|
|
15
15
|
agentName,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
saveToken,
|
|
21
21
|
setIdentity,
|
|
22
22
|
whoAmI
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-PHTQITTC.js";
|
|
24
24
|
|
|
25
25
|
// src/onboard.ts
|
|
26
26
|
function reportRegistered() {
|
package/dist/slot.js
CHANGED
package/dist/stalled.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/stalled.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
function stopReply(stalled) {
|
|
8
|
+
if (!stalled.length) return null;
|
|
9
|
+
const list = stalled.slice(0, 8).map((g) => `"${g.title}" (${g.goalId})`).join(", ");
|
|
10
|
+
const more = stalled.length > 8 ? ` and ${stalled.length - 8} more` : "";
|
|
11
|
+
return {
|
|
12
|
+
decision: "block",
|
|
13
|
+
reason: `Before you stop: ${stalled.length} of your Paigy Goals have had no progress for 3 days: ${list}${more}. For each, update_goal with progress, finish it, or cancel it.`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function main() {
|
|
17
|
+
const input = JSON.parse(readFileSync(0, "utf8") || "{}");
|
|
18
|
+
if (input.stop_hook_active) return;
|
|
19
|
+
const session = input.session_id ?? "";
|
|
20
|
+
if (session) process.env.PAIGY_SESSION_ID = session;
|
|
21
|
+
const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
22
|
+
const dir = join(homedir(), ".paigy", "stalled-reminded");
|
|
23
|
+
const mark = join(dir, `${session || "session"}-${day}`);
|
|
24
|
+
if (existsSync(mark)) return;
|
|
25
|
+
const { checkReplies } = await import("./dist-DKP3DJQJ.js");
|
|
26
|
+
const reply = stopReply((await checkReplies()).stalled ?? []);
|
|
27
|
+
if (!reply) return;
|
|
28
|
+
mkdirSync(dir, { recursive: true });
|
|
29
|
+
writeFileSync(mark, "");
|
|
30
|
+
process.stdout.write(JSON.stringify(reply));
|
|
31
|
+
}
|
|
32
|
+
if (process.argv[1] && /stalled(\.[jt]s)?$/.test(process.argv[1])) main().catch(() => void 0);
|
|
33
|
+
export {
|
|
34
|
+
stopReply
|
|
35
|
+
};
|
package/dist/statusline.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paigy/mcp",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.15",
|
|
4
4
|
"description": "Paigy MCP server — the AI agent harness that calls you. Lets an agent notify a user and await their reply.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"paigy-listen": "dist/listen.js",
|
|
12
12
|
"paigy-statusline": "dist/statusline.js",
|
|
13
13
|
"paigy-enable-tools": "dist/enable.js",
|
|
14
|
-
"paigy-slot": "dist/slot.js"
|
|
14
|
+
"paigy-slot": "dist/slot.js",
|
|
15
|
+
"paigy-stalled": "dist/stalled.js"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"dist"
|